@cueloop/client 0.1.0-alpha.56 → 0.1.0-alpha.59

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.
@@ -50,6 +50,9 @@ export interface PrototypeSheetProps {
50
50
  type SheetStatus = "loading" | "ready" | "unsupported" | "error";
51
51
 
52
52
  const POPOVER_ROWS = 3;
53
+ /** Compose-card width, so the floating composer reads like the plan/diff one
54
+ * instead of shrinking to its content. */
55
+ const COMPOSE_COLS = 46;
53
56
  const PROTOTYPE_IMAGE_ID = 811;
54
57
  /** Page pixels scrolled per wheel notch. */
55
58
  const SCROLL_STEP = 240;
@@ -143,7 +146,11 @@ function PrototypeSheetImpl({
143
146
  const [status, setStatus] = useState<SheetStatus>("loading");
144
147
  const [errorMessage, setErrorMessage] = useState<string>("");
145
148
  const [selected, setSelected] = useState<PrototypeElement | null>(null);
146
- const [overlayCell, setOverlayCell] = useState<{ left: number; top: number } | null>(null);
149
+ const [overlayCell, setOverlayCell] = useState<{
150
+ left: number;
151
+ top: number;
152
+ regionColumns: number;
153
+ } | null>(null);
147
154
  const [actionsOpen, setActionsOpen] = useState(false);
148
155
  const [composing, setComposing] = useState(false);
149
156
  const [draftText, setDraftText] = useState("");
@@ -289,6 +296,7 @@ function PrototypeSheetImpl({
289
296
  setOverlayCell({
290
297
  left: Math.max(0, cell.column - geometry.x),
291
298
  top: Math.max(0, topRaw < POPOVER_ROWS ? topRaw + 1 : topRaw - POPOVER_ROWS),
299
+ regionColumns: geometry.width,
292
300
  });
293
301
  setActionsOpen(false);
294
302
  setComposing(false);
@@ -325,6 +333,15 @@ function PrototypeSheetImpl({
325
333
  flexDirection: "column" as const,
326
334
  backgroundColor: tokens.elevated,
327
335
  };
336
+ // keep the composer inside the region: never wider than the region, and
337
+ // clamped so it cannot run off either edge on a narrow preview
338
+ const regionColumns = overlayCell?.regionColumns ?? 0;
339
+ const composeWidth = regionColumns > 0 ? Math.min(COMPOSE_COLS, regionColumns) : COMPOSE_COLS;
340
+ const composeStyle = {
341
+ ...overlayStyle,
342
+ left: Math.max(0, Math.min(overlayStyle.left, regionColumns - composeWidth)),
343
+ width: composeWidth,
344
+ };
328
345
 
329
346
  return (
330
347
  <box
@@ -372,7 +389,7 @@ function PrototypeSheetImpl({
372
389
  </box>
373
390
  ) : null}
374
391
  {selected && composing ? (
375
- <box style={overlayStyle}>
392
+ <box style={composeStyle}>
376
393
  <AnnotationCard
377
394
  kind="comment"
378
395
  quote={selected.quote}
@@ -0,0 +1,86 @@
1
+ import { createTextAttributes } from "@opentui/core";
2
+ import type { DisplayBlock, StyleRun } from "../view-plan";
3
+ import type { Theme } from "../theme";
4
+
5
+ /** A cut block reads as removed: struck through and grayed, never red. */
6
+ const CUT_ATTRIBUTES = createTextAttributes({ strikethrough: true, dim: true });
7
+
8
+ /** Inline-emphasis attributes, reused per run so the render loop allocates none. */
9
+ const STRONG_ATTRIBUTES = createTextAttributes({ bold: true });
10
+ const EM_ATTRIBUTES = createTextAttributes({ italic: true });
11
+ const STRIKE_ATTRIBUTES = createTextAttributes({ strikethrough: true });
12
+ const LINK_ATTRIBUTES = createTextAttributes({ underline: true });
13
+ /** Block-level base attributes: headings read bold, blockquotes read italic. */
14
+ const HEADING_ATTRIBUTES = createTextAttributes({ bold: true });
15
+ const QUOTE_ATTRIBUTES = createTextAttributes({ italic: true });
16
+
17
+ interface BlockBaseStyle {
18
+ baseFg: string;
19
+ baseAttributes: number;
20
+ }
21
+
22
+ function isHeadingBlock(block: DisplayBlock): boolean {
23
+ return block.kind === "h1" || block.kind === "h2" || block.kind === "h3";
24
+ }
25
+
26
+ // headings are all bold; level reads from descending brightness alone (a
27
+ // terminal cannot scale font size), leaving the salmon accent to annotations
28
+ function headingForeground(block: DisplayBlock, tokens: Theme): string | undefined {
29
+ if (block.kind === "h1") return tokens.text;
30
+ if (block.kind === "h2") return tokens.textMuted;
31
+ if (block.kind === "h3") return tokens.textDim;
32
+ return undefined;
33
+ }
34
+
35
+ function blockBaseStyle(block: DisplayBlock, tokens: Theme): BlockBaseStyle {
36
+ const isHeading = isHeadingBlock(block);
37
+ const isQuote = block.kind === "quote";
38
+ const headingFg = headingForeground(block, tokens);
39
+ // block-level base: headings bold, quotes muted italic; inline roles compose on top
40
+ const baseFg = headingFg ?? (isQuote ? tokens.textMuted : tokens.text);
41
+ const baseAttributes = (isHeading ? HEADING_ATTRIBUTES : 0) | (isQuote ? QUOTE_ATTRIBUTES : 0);
42
+ return { baseFg, baseAttributes };
43
+ }
44
+
45
+ function inlineRoleStyle(
46
+ run: StyleRun,
47
+ base: BlockBaseStyle,
48
+ tokens: Theme,
49
+ ): { fg?: string; bg?: string; attributes?: number } {
50
+ const { baseFg, baseAttributes } = base;
51
+ const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
52
+ switch (run.role) {
53
+ case "ins":
54
+ return { fg: tokens.insertedForeground, attributes: baseAttributes || undefined };
55
+ case "del":
56
+ return { fg: tokens.deletedForeground };
57
+ case "mark-comment":
58
+ return { fg: tokens.text, bg: tokens.markCommentBackground };
59
+ case "mark-focus":
60
+ return { fg: tokens.accentInk, bg: tokens.accent };
61
+ case "kspan":
62
+ return { fg: tokens.accentInk, bg: tokens.accent };
63
+ case "strong":
64
+ return { fg: baseFg, attributes: withBase(STRONG_ATTRIBUTES) };
65
+ case "em":
66
+ return { fg: baseFg, attributes: withBase(EM_ATTRIBUTES) };
67
+ case "code":
68
+ return { fg: tokens.text, bg: tokens.elevated };
69
+ case "strike":
70
+ return { fg: tokens.textMuted, attributes: withBase(STRIKE_ATTRIBUTES) };
71
+ case "link":
72
+ return { fg: tokens.blue, attributes: withBase(LINK_ATTRIBUTES) };
73
+ default:
74
+ return { fg: baseFg, attributes: baseAttributes || undefined };
75
+ }
76
+ }
77
+
78
+ export function runStyle(
79
+ run: StyleRun,
80
+ block: DisplayBlock,
81
+ tokens: Theme,
82
+ ): { fg?: string; bg?: string; attributes?: number } {
83
+ // a cut block reads as removed: every run struck through and grayed, never red
84
+ if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
85
+ return inlineRoleStyle(run, blockBaseStyle(block, tokens), tokens);
86
+ }
package/src/config.ts CHANGED
@@ -160,6 +160,63 @@ function parseActions(raw: unknown): QuickAction[] | undefined {
160
160
  return actions.length ? actions : undefined;
161
161
  }
162
162
 
163
+ function mergeAuthors(target: Record<string, string>, raw: unknown): void {
164
+ const authors = raw as Record<string, unknown> | undefined;
165
+ if (!authors) return;
166
+ for (const [id, value] of Object.entries(authors)) {
167
+ if (typeof value === "string") target[id] = value;
168
+ }
169
+ }
170
+
171
+ function mergeKeys(target: Record<string, string[]>, raw: unknown): void {
172
+ const keys = raw as KeymapConfig | undefined;
173
+ if (!keys) return;
174
+ for (const [action, combo] of Object.entries(keys)) {
175
+ target[action] = Array.isArray(combo) ? combo : [combo];
176
+ }
177
+ }
178
+
179
+ function mergeAutoClose(target: CueloopConfig["ui"], value: unknown): void {
180
+ if (value === undefined) return;
181
+ if (value === "off") target.autoClose = "off";
182
+ else if (typeof value === "number" && value >= 0) target.autoClose = value;
183
+ }
184
+
185
+ function mergeUi(target: CueloopConfig["ui"], raw: unknown): void {
186
+ const ui = raw as
187
+ | { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
188
+ | undefined;
189
+ if (!ui) return;
190
+ mergeAutoClose(target, ui.auto_close);
191
+ if (typeof ui.editor === "string" && ui.editor.trim()) target.editor = ui.editor.trim();
192
+ if (typeof ui.review_width === "number" && Number.isFinite(ui.review_width)) {
193
+ target.reviewWidth = clampWidth(ui.review_width);
194
+ }
195
+ if (
196
+ ui.review_state === "expanded" ||
197
+ ui.review_state === "compact" ||
198
+ ui.review_state === "hidden"
199
+ ) {
200
+ target.reviewState = ui.review_state;
201
+ }
202
+ }
203
+
204
+ function mergeObsidian(target: ObsidianConfig, raw: unknown): void {
205
+ const integrations = raw as Record<string, unknown> | undefined;
206
+ const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
207
+ if (!obsidian) return;
208
+ if (typeof obsidian["vault"] === "string") target.vault = obsidian["vault"];
209
+ if (typeof obsidian["folder"] === "string") target.folder = obsidian["folder"];
210
+ if (typeof obsidian["filenameFormat"] === "string")
211
+ target.filenameFormat = obsidian["filenameFormat"];
212
+ const separator = obsidian["separator"];
213
+ if (separator === "space" || separator === "dash" || separator === "underscore")
214
+ target.separator = separator;
215
+ const exportOn = obsidian["exportOn"];
216
+ if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
217
+ target.exportOn = exportOn;
218
+ }
219
+
163
220
  function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig {
164
221
  const out: CueloopConfig = {
165
222
  keys: { ...base.keys },
@@ -172,53 +229,10 @@ function layer(base: CueloopConfig, raw: Record<string, unknown>): CueloopConfig
172
229
  };
173
230
  const actions = parseActions(raw["actions"]);
174
231
  if (actions) out.actions = actions;
175
- const authors = raw["authors"] as Record<string, unknown> | undefined;
176
- if (authors) {
177
- for (const [id, value] of Object.entries(authors)) {
178
- if (typeof value === "string") out.authors[id] = value;
179
- }
180
- }
181
- const keys = raw["keys"] as KeymapConfig | undefined;
182
- if (keys) {
183
- for (const [action, combo] of Object.entries(keys)) {
184
- out.keys[action] = Array.isArray(combo) ? combo : [combo];
185
- }
186
- }
187
- const ui = raw["ui"] as
188
- | { auto_close?: unknown; editor?: unknown; review_width?: unknown; review_state?: unknown }
189
- | undefined;
190
- if (ui && ui.auto_close !== undefined) {
191
- if (ui.auto_close === "off") out.ui.autoClose = "off";
192
- else if (typeof ui.auto_close === "number" && ui.auto_close >= 0)
193
- out.ui.autoClose = ui.auto_close;
194
- }
195
- if (ui && typeof ui.editor === "string" && ui.editor.trim()) out.ui.editor = ui.editor.trim();
196
- if (ui && typeof ui.review_width === "number" && Number.isFinite(ui.review_width)) {
197
- out.ui.reviewWidth = clampWidth(ui.review_width);
198
- }
199
- if (
200
- ui &&
201
- (ui.review_state === "expanded" ||
202
- ui.review_state === "compact" ||
203
- ui.review_state === "hidden")
204
- ) {
205
- out.ui.reviewState = ui.review_state;
206
- }
207
- const integrations = raw["integrations"] as Record<string, unknown> | undefined;
208
- const obsidian = integrations?.["obsidian"] as Record<string, unknown> | undefined;
209
- if (obsidian) {
210
- const obsidianConfig = out.integrations.obsidian;
211
- if (typeof obsidian["vault"] === "string") obsidianConfig.vault = obsidian["vault"];
212
- if (typeof obsidian["folder"] === "string") obsidianConfig.folder = obsidian["folder"];
213
- if (typeof obsidian["filenameFormat"] === "string")
214
- obsidianConfig.filenameFormat = obsidian["filenameFormat"];
215
- const separator = obsidian["separator"];
216
- if (separator === "space" || separator === "dash" || separator === "underscore")
217
- obsidianConfig.separator = separator;
218
- const exportOn = obsidian["exportOn"];
219
- if (exportOn === "approve" || exportOn === "resolve" || exportOn === "manual")
220
- obsidianConfig.exportOn = exportOn;
221
- }
232
+ mergeAuthors(out.authors, raw["authors"]);
233
+ mergeKeys(out.keys, raw["keys"]);
234
+ mergeUi(out.ui, raw["ui"]);
235
+ mergeObsidian(out.integrations.obsidian, raw["integrations"]);
222
236
  return out;
223
237
  }
224
238