@cueloop/client 0.1.0-alpha.43 → 0.1.0-alpha.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.43",
3
+ "version": "0.1.0-alpha.45",
4
4
  "description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
5
5
  "homepage": "https://github.com/mmurakaru/cueloop#readme",
6
6
  "bugs": {
@@ -26,10 +26,10 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@cueloop/daemon": "0.1.0-alpha.43",
30
- "@cueloop/extension-api": "0.1.0-alpha.43",
31
- "@cueloop/integration-obsidian": "0.1.0-alpha.43",
32
- "@cueloop/schema": "0.1.0-alpha.43",
29
+ "@cueloop/daemon": "0.1.0-alpha.45",
30
+ "@cueloop/extension-api": "0.1.0-alpha.45",
31
+ "@cueloop/integration-obsidian": "0.1.0-alpha.45",
32
+ "@cueloop/schema": "0.1.0-alpha.45",
33
33
  "@opentui/core": "^0.5.2",
34
34
  "@opentui/keymap": "0.5.2",
35
35
  "@opentui/react": "^0.5.2",
@@ -87,18 +87,18 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
87
87
  return backgrounds;
88
88
  }
89
89
 
90
- /** Whether any border character (box-drawing) is painted in the given foreground hex. */
91
- function hasBorderColor(setup: Setup, hex: string): boolean {
90
+ function foregroundsOf(setup: Setup, needle: string): string[] {
91
+ const foregrounds: string[] = [];
92
92
  for (const line of setup.captureSpans().lines) {
93
93
  for (const span of line.spans) {
94
- if (!/[╭╮╰╯│─┌┐└┘]/.test(span.text)) continue;
94
+ if (!span.text.includes(needle)) continue;
95
95
  const [red, green, blue] = span.fg.toInts();
96
- const rendered =
97
- "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
98
- if (rendered === hex) return true;
96
+ foregrounds.push(
97
+ "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join(""),
98
+ );
99
99
  }
100
100
  }
101
- return false;
101
+ return foregrounds;
102
102
  }
103
103
 
104
104
  /** Move the cursor to "The daemon persists sessions to disk atomically." */
@@ -629,9 +629,9 @@ describe("edit-exit reconciliation", () => {
629
629
 
630
630
  // Act
631
631
  // deselect the card so e reaches the editor hand-off, then edit; deselection
632
- // shows as the card's accent border fading to a dim border (transparent fill)
632
+ // shows as the card's quote line dropping the selection tone (blue)
633
633
  await press(setup, "escape");
634
- await waitForState(setup, () => !hasBorderColor(setup, DARK.accent));
634
+ await waitForState(setup, () => !foregroundsOf(setup, "The daemon").includes(DARK.blue));
635
635
  await press(setup, "e");
636
636
 
637
637
  // Assert
@@ -150,10 +150,10 @@ describe("observer navigation still works", () => {
150
150
  await press(setup, "n");
151
151
  await setup.renderOnce();
152
152
 
153
- // Assert - focus shows as the card's bright kind-colored border (transparent
154
- // fill); a selected card is the only accent-colored border in the frame
153
+ // Assert - focus shows as the card's blue border (transparent fill); a
154
+ // comment card is the only blue-bordered box in the frame
155
155
  await waitForText(setup, "COMMENT · me");
156
- await waitForState(setup, () => hasBorderColor(setup, DARK.accent));
156
+ await waitForState(setup, () => hasBorderColor(setup, DARK.blue));
157
157
  });
158
158
  });
159
159
 
@@ -87,9 +87,8 @@ describe("rail submit confirm", () => {
87
87
  // Assert
88
88
  await waitForText(setup, "submit review");
89
89
  const frame = setup.captureCharFrame();
90
- // the card: title, honest counts, verdict selector, word-buttons
90
+ // the card: title, verdict selector, word-buttons
91
91
  expect(frame).toContain("submit review");
92
- expect(frame).toContain("0 annotations · 0 blocking");
93
92
  expect(frame).toContain("[Approve]"); // nothing pending: approve default
94
93
  expect(frame).toContain(" Submit ");
95
94
  expect(frame).toContain(" Cancel ");
@@ -136,7 +135,7 @@ describe("rail submit confirm", () => {
136
135
  // Assert
137
136
  // a bare ESC settles after the parser's escape-sequence window
138
137
  const frame = await waitForTextGone(setup, "[Approve]");
139
- expect(frame).not.toContain("0 annotations");
138
+ expect(frame).not.toContain("submit review");
140
139
  expect(frame).toContain("Submit review");
141
140
  });
142
141
 
@@ -149,10 +148,9 @@ describe("rail submit confirm", () => {
149
148
  await press(setup, "enter");
150
149
 
151
150
  // Assert
152
- await waitForText(setup, "1 annotations · 0 blocking");
151
+ await waitForText(setup, "[Changes]"); // pending items: request changes default
153
152
  const frame = setup.captureCharFrame();
154
- expect(frame).toContain("1 annotations · 0 blocking");
155
- expect(frame).toContain("[Changes]"); // pending items: request changes default
153
+ expect(frame).toContain("submit review");
156
154
 
157
155
  // Act
158
156
  await setup.mockInput.typeText("Tighten the steps.");
@@ -183,7 +181,7 @@ describe("rail submit confirm", () => {
183
181
  await press(setup, "enter");
184
182
 
185
183
  // Assert
186
- await waitForText(setup, "12 annotations · 0 blocking");
184
+ await waitForText(setup, "[Changes]");
187
185
  // the card is pinned outside the scrollbox; the stack stays scrolled off the top
188
186
  expect(setup.captureCharFrame()).not.toContain("note 01");
189
187
 
@@ -196,7 +194,7 @@ describe("rail submit confirm", () => {
196
194
  // Assert
197
195
  await waitForText(setup, "note 12");
198
196
  const scrolledWithCard = setup.captureCharFrame();
199
- expect(scrolledWithCard).toContain("12 annotations · 0 blocking");
197
+ expect(scrolledWithCard).toContain("submit review");
200
198
  expect(scrolledWithCard).not.toContain("note 01");
201
199
  });
202
200
 
package/src/App.tsx CHANGED
@@ -58,7 +58,7 @@ import { CLIENT_VERSION } from "./version";
58
58
  import { Breadcrumb, type BreadcrumbItem } from "./components/Breadcrumb";
59
59
  import { PlanSheet, type PlanSheetHandle } from "./components/PlanSheet";
60
60
  import { DiffSheet } from "./components/DiffSheet";
61
- import { annotationBlocking, type ReviewRailHandle } from "./components/ReviewRail";
61
+ import { type ReviewRailHandle } from "./components/ReviewRail";
62
62
  import type { AgentTerminalHandle } from "./components/agent-launcher";
63
63
  import { ReviewPanel } from "./components/ReviewPanel";
64
64
  import {
@@ -853,30 +853,36 @@ export function App({
853
853
  }
854
854
  : null;
855
855
 
856
+ // a mouse drag leaves a native selection: turn it into a word span so the
857
+ // marker popover opens at the dragged range, mirroring the `v` grammar.
858
+ // Returns whether a native selection existed (the release ended a drag).
859
+ const activateSpanFromSelection = (preferredIndex?: number): boolean => {
860
+ if (!renderer?.hasSelection) return false;
861
+ // the release block re-anchors the span, so each drag replaces the last
862
+ const selection = planSheetRef.current?.readSelection(preferredIndex);
863
+ const block = selection ? display[selection.displayIndex] : undefined;
864
+ const span =
865
+ selection && block
866
+ ? spanFromRange(selection.displayIndex, displayText(block), selection.start, selection.end)
867
+ : null;
868
+ if (span) setMode({ type: "span", span });
869
+ return true;
870
+ };
871
+
856
872
  const onLineActivate = (displayIndex: number): void => {
857
- // a mouse drag leaves a native selection: turn it into a word span so the
858
- // marker popover opens at the dragged range, mirroring the `v` grammar
859
- if (renderer?.hasSelection) {
860
- // the release block re-anchors the span, so each drag replaces the last
861
- const selection = planSheetRef.current?.readSelection(displayIndex);
862
- const block = selection ? display[selection.displayIndex] : undefined;
863
- const span =
864
- selection && block
865
- ? spanFromRange(
866
- selection.displayIndex,
867
- displayText(block),
868
- selection.start,
869
- selection.end,
870
- )
871
- : null;
872
- if (span) setMode({ type: "span", span });
873
- return;
874
- }
873
+ if (activateSpanFromSelection(displayIndex)) return;
875
874
  setCursor(displayIndex);
876
875
  const annotationId = marks.get(displayIndex)?.[0]?.annotationId;
877
876
  if (annotationId) selectCardFromDocument(annotationId);
878
877
  };
879
878
 
879
+ // a drag released outside any block's text (the gutter, past a line end, a
880
+ // gap between blocks) never reaches a block handler; the sheet-level release
881
+ // still turns the finished drag into a span
882
+ const onSelectionRelease = (): void => {
883
+ activateSpanFromSelection();
884
+ };
885
+
880
886
  const onEditRequest = (): void => {
881
887
  // A share viewer/observer has no Edit affordance (the button is hidden), so
882
888
  // this is owner-only; stay silent rather than nag if it is ever reached.
@@ -916,8 +922,6 @@ export function App({
916
922
  ? {
917
923
  verdict: mode.verdict,
918
924
  summary: mode.summary,
919
- annotationCount: reviewerAnnotations(activeSession).length,
920
- blockingCount: reviewerAnnotations(activeSession).filter(annotationBlocking).length,
921
925
  // walk coverage keeps partial passes honest at the verdict
922
926
  viewedSummary:
923
927
  isDiff && activeSession.viewedPaths !== undefined
@@ -1044,6 +1048,7 @@ export function App({
1044
1048
  popover={popoverState}
1045
1049
  editOrphanCount={editOrphanCount}
1046
1050
  onLineActivate={onLineActivate}
1051
+ onSelectionRelease={onSelectionRelease}
1047
1052
  />
1048
1053
  )}
1049
1054
  <ReviewPanel
@@ -242,7 +242,8 @@ describe("the guided walk", () => {
242
242
  await press(setup, "escape");
243
243
  await waitForTextGone(setup, "agent note");
244
244
  await press(setup, "enter");
245
- await waitForText(setup, "0 annotations");
245
+ // nothing pending despite the note, so the confirm card defaults to approve
246
+ await waitForText(setup, "[Approve]");
246
247
  await press(setup, "enter");
247
248
 
248
249
  // Assert
@@ -0,0 +1,100 @@
1
+ // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
+
3
+ exports[`view-plan regression harness (M0) blockRuns output is stable across the representative blocks 1`] = `
4
+ [
5
+ {
6
+ "runs": [
7
+ {
8
+ "role": "plain",
9
+ "start": 0,
10
+ "text": "Heading with ",
11
+ },
12
+ {
13
+ "role": "strong",
14
+ "start": 15,
15
+ "text": "bold",
16
+ },
17
+ ],
18
+ "text": "Heading with **bold**",
19
+ "type": "same",
20
+ },
21
+ {
22
+ "runs": [
23
+ {
24
+ "role": "plain",
25
+ "start": 0,
26
+ "text": "A paragraph with ",
27
+ },
28
+ {
29
+ "role": "strong",
30
+ "start": 19,
31
+ "text": "strong",
32
+ },
33
+ {
34
+ "role": "plain",
35
+ "start": 27,
36
+ "text": ", ",
37
+ },
38
+ {
39
+ "role": "em",
40
+ "start": 30,
41
+ "text": "em",
42
+ },
43
+ {
44
+ "role": "plain",
45
+ "start": 33,
46
+ "text": ", ",
47
+ },
48
+ {
49
+ "role": "code",
50
+ "start": 36,
51
+ "text": "code",
52
+ },
53
+ {
54
+ "role": "plain",
55
+ "start": 41,
56
+ "text": ", ",
57
+ },
58
+ {
59
+ "role": "strike",
60
+ "start": 45,
61
+ "text": "strike",
62
+ },
63
+ {
64
+ "role": "plain",
65
+ "start": 53,
66
+ "text": " and a ",
67
+ },
68
+ {
69
+ "href": "https://example.com",
70
+ "role": "link",
71
+ "start": 61,
72
+ "text": "label",
73
+ },
74
+ {
75
+ "role": "plain",
76
+ "start": 88,
77
+ "text": ".",
78
+ },
79
+ ],
80
+ "text": "A paragraph with **strong**, *em*, \`code\`, ~~strike~~ and a [label](https://example.com).",
81
+ "type": "same",
82
+ },
83
+ {
84
+ "runs": [
85
+ {
86
+ "role": "plain",
87
+ "start": 0,
88
+ "text": "a list item with ",
89
+ },
90
+ {
91
+ "role": "em",
92
+ "start": 18,
93
+ "text": "emphasis",
94
+ },
95
+ ],
96
+ "text": "a list item with *emphasis*",
97
+ "type": "same",
98
+ },
99
+ ]
100
+ `;
@@ -15,7 +15,7 @@ export const CommentDraft: Story = {
15
15
  draft={{ text: "Which daemon?", ...draftCallbacks }}
16
16
  />
17
17
  ),
18
- expectedColors: [DARK.accent],
18
+ expectedColors: [DARK.blue],
19
19
  };
20
20
 
21
21
  export const SavedSelected: Story = {
@@ -27,14 +27,14 @@ export const SavedSelected: Story = {
27
27
  body: "Which daemon owns this?",
28
28
  isSelected: true,
29
29
  isOrphan: false,
30
- isBlocking: false,
31
30
  author: "me",
31
+ tone: DARK.blue,
32
32
  editing: null,
33
33
  onPress: () => {},
34
34
  }}
35
35
  />
36
36
  ),
37
- expectedColors: [DARK.accent],
37
+ expectedColors: [DARK.blue],
38
38
  };
39
39
 
40
40
  export const SavedCollaborator: Story = {
@@ -46,17 +46,17 @@ export const SavedCollaborator: Story = {
46
46
  body: "Which daemon owns this?",
47
47
  isSelected: false,
48
48
  isOrphan: false,
49
- isBlocking: false,
50
49
  author: "Priya",
50
+ tone: DARK.blue,
51
51
  editing: null,
52
52
  onPress: () => {},
53
53
  }}
54
54
  />
55
55
  ),
56
- expectedColors: [DARK.border],
56
+ expectedColors: [DARK.blue],
57
57
  };
58
58
 
59
- export const SavedOrphanBlocking: Story = {
59
+ export const SavedOrphan: Story = {
60
60
  render: () => (
61
61
  <AnnotationCard
62
62
  kind="comment"
@@ -65,14 +65,14 @@ export const SavedOrphanBlocking: Story = {
65
65
  body: "The passage is gone.",
66
66
  isSelected: false,
67
67
  isOrphan: true,
68
- isBlocking: true,
69
68
  author: "me",
69
+ tone: DARK.blue,
70
70
  editing: null,
71
71
  onPress: () => {},
72
72
  }}
73
73
  />
74
74
  ),
75
- expectedColors: [DARK.border],
75
+ expectedColors: [DARK.blue],
76
76
  };
77
77
 
78
78
  export const SavedEditing: Story = {
@@ -84,8 +84,8 @@ export const SavedEditing: Story = {
84
84
  body: "old body",
85
85
  isSelected: true,
86
86
  isOrphan: false,
87
- isBlocking: false,
88
87
  author: "me",
88
+ tone: DARK.blue,
89
89
  editing: { text: "new body", ...draftCallbacks },
90
90
  onPress: () => {},
91
91
  }}
@@ -31,9 +31,10 @@ export interface AnnotationSaved {
31
31
  body: string;
32
32
  isSelected: boolean;
33
33
  isOrphan: boolean;
34
- isBlocking: boolean;
35
34
  /** Border-title author: a collaborator's display name, or "me" for the reviewer's own note. */
36
35
  author: string;
36
+ /** Border + title color for this card (comment cards read blue). */
37
+ tone: string;
37
38
  /** Non-null while the card body is being rewritten in place. */
38
39
  editing: AnnotationDraft | null;
39
40
  onPress: () => void;
@@ -153,7 +154,8 @@ export function AnnotationCard({
153
154
  theme,
154
155
  }: AnnotationCardProps): React.ReactNode {
155
156
  const tokens = useComponentTheme(theme);
156
- const kindColor = tokens.accent;
157
+ // the composer wears the same blue as the saved comment card it becomes
158
+ const kindColor = tokens.blue;
157
159
  const activeDraft = draft ?? saved?.editing ?? null;
158
160
  // The composer's visible height, grown from the wrapped text. It is shared
159
161
  // with the card frame (contentRows) so the border never clips the textarea
@@ -184,10 +186,8 @@ export function AnnotationCard({
184
186
  );
185
187
  }
186
188
  const card = saved!;
187
- // border = "what + who" (+ any exceptional flag); body = the content only
188
- const flags = [card.isBlocking ? "BLOCKING" : null, card.isOrphan ? "ORPHANED" : null].filter(
189
- (flag): flag is string => flag !== null,
190
- );
189
+ // border = "what + who" (+ an orphaned flag when the anchor no longer resolves)
190
+ const flags = card.isOrphan ? ["ORPHANED"] : [];
191
191
  const title = ` ${[kind.toUpperCase(), card.author, ...flags].join(" · ")} `;
192
192
  // every saved card is bordered, so the border + side padding always eat this width
193
193
  const borderInset = 4;
@@ -202,15 +202,21 @@ export function AnnotationCard({
202
202
  height: cardHeight(contentRows),
203
203
  border: true,
204
204
  borderStyle: FRAME_BORDER_STYLE,
205
- // cards stay transparent in every theme; selection is the bright
206
- // kind-colored border, while unselected cards recede to a dim border
207
- borderColor: card.isSelected || card.editing ? kindColor : tokens.border,
205
+ // the border (and its title) always wears the card's tone - salmon for
206
+ // own notes, blue for a collaborator's; the card background stays
207
+ // transparent so it sits flat on the terminal theme. Selection reads
208
+ // from the quote line taking the card's tone (below) and from the
209
+ // matching text highlight in the document.
210
+ borderColor: card.tone,
211
+ backgroundColor: "transparent",
208
212
  paddingLeft: 1,
209
213
  paddingRight: 1,
210
214
  }}
211
215
  onMouseUp={card.onPress}
212
216
  >
213
- <text fg={tokens.textDim}>"{truncateToSingleLine(quote, 26 - borderInset)}"</text>
217
+ <text
218
+ fg={card.isSelected ? card.tone : tokens.textDim}
219
+ >{`"${truncateToSingleLine(quote, 26 - borderInset)}"`}</text>
214
220
  {card.editing ? (
215
221
  <DraftEditor
216
222
  draft={card.editing}
@@ -13,28 +13,14 @@ const callbacks = {
13
13
  };
14
14
 
15
15
  export const ApproveDefault: Story = {
16
- render: () => (
17
- <ConfirmCard
18
- verdict="approve"
19
- summary=""
20
- annotationCount={0}
21
- blockingCount={0}
22
- {...callbacks}
23
- />
24
- ),
16
+ render: () => <ConfirmCard verdict="approve" summary="" {...callbacks} />,
25
17
  expectedColors: [DARK.green, DARK.accent],
26
18
  size: { width: 40, height: 14 },
27
19
  };
28
20
 
29
21
  export const RequestChanges: Story = {
30
22
  render: () => (
31
- <ConfirmCard
32
- verdict="request_changes"
33
- summary="Tighten the steps."
34
- annotationCount={3}
35
- blockingCount={1}
36
- {...callbacks}
37
- />
23
+ <ConfirmCard verdict="request_changes" summary="Tighten the steps." {...callbacks} />
38
24
  ),
39
25
  expectedColors: [DARK.red],
40
26
  size: { width: 40, height: 14 },
@@ -42,14 +28,7 @@ export const RequestChanges: Story = {
42
28
 
43
29
  export const WithViewedSummary: Story = {
44
30
  render: () => (
45
- <ConfirmCard
46
- verdict="approve"
47
- summary=""
48
- annotationCount={1}
49
- blockingCount={0}
50
- viewedSummary="2/3 files viewed"
51
- {...callbacks}
52
- />
31
+ <ConfirmCard verdict="approve" summary="" viewedSummary="2/3 files viewed" {...callbacks} />
53
32
  ),
54
33
  expectedColors: [DARK.green, DARK.textDim],
55
34
  size: { width: 40, height: 14 },
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * The Submit button expanded into a bordered confirm card at the rail bottom:
3
- * honest counts, the verdict selector (arrow keys or click), the optional
4
- * summary, and plain word-buttons - key hints live in the status line only.
5
- * The bordered height derives from the content rows through Card, so layout
6
- * and render never drift.
3
+ * the verdict selector (arrow keys or click), the optional summary, and plain
4
+ * word-buttons - key hints live in the status line only. The bordered height
5
+ * derives from the content rows through Card, so layout and render never drift.
7
6
  */
8
7
 
9
8
  import React from "react";
@@ -26,8 +25,6 @@ export const VERDICT_LABEL: Record<VerdictKind, string> = {
26
25
  export interface ConfirmCardProps {
27
26
  verdict: VerdictKind;
28
27
  summary: string;
29
- annotationCount: number;
30
- blockingCount: number;
31
28
  /**
32
29
  * The guided walk's honest coverage line for diff sessions, e.g.
33
30
  * "2/3 files viewed". Undefined = no walk data, the row does not render.
@@ -40,8 +37,8 @@ export interface ConfirmCardProps {
40
37
  theme?: Theme;
41
38
  }
42
39
 
43
- /** Counts, spacer, 1-row verdict selector, spacer, summary input, spacer, buttons. */
44
- const CONFIRM_CONTENT_ROWS = 7;
40
+ /** 1-row verdict selector, spacer, summary input, spacer, buttons. */
41
+ const CONFIRM_CONTENT_ROWS = 5;
45
42
 
46
43
  export function verdictColor(verdict: VerdictKind, tokens: Theme): string {
47
44
  return verdict === "approve"
@@ -89,8 +86,6 @@ function VerdictSelector({
89
86
  export function ConfirmCard({
90
87
  verdict,
91
88
  summary,
92
- annotationCount,
93
- blockingCount,
94
89
  viewedSummary,
95
90
  onInput,
96
91
  onSelectVerdict,
@@ -102,17 +97,14 @@ export function ConfirmCard({
102
97
  return (
103
98
  <Card
104
99
  title=" submit review "
105
- contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 1 : 0)}
106
- borderColor={tokens.accent}
100
+ contentRows={CONFIRM_CONTENT_ROWS + (viewedSummary !== undefined ? 2 : 0)}
101
+ borderColor={tokens.text}
107
102
  backgroundColor="transparent"
108
103
  marginRight={1}
109
104
  theme={theme}
110
105
  >
111
- <text
112
- fg={tokens.textDim}
113
- >{`${annotationCount} annotations · ${blockingCount} blocking`}</text>
114
106
  {viewedSummary !== undefined ? <text fg={tokens.textDim}>{viewedSummary}</text> : null}
115
- <box style={{ height: 1 }} />
107
+ {viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
116
108
  <VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
117
109
  <box style={{ height: 1 }} />
118
110
  <input
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import React from "react";
11
+ import type { MouseEvent } from "@opentui/core";
11
12
  import type { Theme } from "../theme";
12
13
  import type { QuickAction } from "../config";
13
14
  import { useComponentTheme } from "./theme-context";
@@ -56,7 +57,12 @@ export function MarkerPopover({
56
57
  };
57
58
  const open = view === "actions";
58
59
  return (
59
- <box style={{ flexDirection: "column", alignItems: "flex-start" }}>
60
+ <box
61
+ style={{ flexDirection: "column", alignItems: "flex-start" }}
62
+ // a release on the popover is the popover's own click; it must never
63
+ // bubble to the sheet's selection-release fallback and re-enter span mode
64
+ onMouseUp={(event: MouseEvent) => event.stopPropagation()}
65
+ >
60
66
  <box style={{ ...cardStyle, height: cardHeight(1) }}>
61
67
  <box style={{ flexDirection: "row" }}>
62
68
  <text fg={tokens.accent} onMouseUp={onComment}>