diff --git a/src/components/familiar-work-queue-sections.tsx b/src/components/familiar-work-queue-sections.tsx index ce3b5c414..9191a17e8 100644 --- a/src/components/familiar-work-queue-sections.tsx +++ b/src/components/familiar-work-queue-sections.tsx @@ -5,7 +5,6 @@ import { Icon } from "@/lib/icon"; import { Button } from "@/components/ui/button"; import { Modal } from "@/components/ui/modal"; import { SkeletonRows } from "@/components/ui/skeleton"; -import { StandardSelect } from "@/components/ui/select"; import { useAnnouncer } from "@/components/ui/live-region"; import { relativeTime } from "@/lib/relative-time"; import type { ResolvedFamiliar } from "@/lib/familiar-resolve"; @@ -222,6 +221,224 @@ export function AttentionStrip({ ); } +/** The row's left accent-rail tone (a finite enum → a `fwq-row--rail-*` class, + * so the colour stays in CSS rather than an inline style). Bead-only rows read + * by priority (what to pick up first); PR-backed rows read by lane state (what + * is blocking the merge). */ +function railClass(item: WorkQueueItem): string { + if (!item.pr && !item.merged && item.bead) { + if (item.bead.priority === 0) return "danger"; + if (item.bead.priority === 1) return "warning"; + return "neutral"; + } + switch (item.lane) { + case "checks-failing": + return "danger"; + case "changes-requested": + return "warning"; + case "waiting": + return "waiting"; + case "ready-to-merge": + case "post-merge-cleanup": + return "success"; + default: + return "neutral"; + } +} + +// ── Inline markdown note editor (queue redesign) ────────────────────────────── +// A lightweight Write/Preview composer with a formatting toolbar, ported from +// the design prototype. The heavy CodeMirror MdEditor is overkill for a short +// handoff note; this stays self-contained and matches the mock pixel-for-pixel. + +type MdKind = "bold" | "italic" | "code" | "h" | "ul" | "quote" | "link"; + +/** Wrap/insert markdown around the textarea's current selection, returning the + * next value and the caret position to restore. */ +function applyMarkdown(value: string, start: number, end: number, kind: MdKind): { next: string; caret: number } { + const sel = value.slice(start, end); + const atLineStart = start === 0 || value[start - 1] === "\n"; + const nl = atLineStart ? "" : "\n"; + let ins: string; + switch (kind) { + case "bold": + ins = `**${sel || "bold"}**`; + break; + case "italic": + ins = `*${sel || "italic"}*`; + break; + case "code": + ins = `\`${sel || "code"}\``; + break; + case "h": + ins = `${nl}## ${sel || "Heading"}`; + break; + case "ul": + ins = `${nl}- ${sel || "List item"}`; + break; + case "quote": + ins = `${nl}> ${sel || "Quote"}`; + break; + case "link": + ins = `[${sel || "label"}](https://)`; + break; + default: + ins = sel; + } + return { next: value.slice(0, start) + ins + value.slice(end), caret: start + ins.length }; +} + +/** Minimal, escaping markdown → HTML for the preview pane. Every user string is + * HTML-escaped before any tag is emitted, so the innerHTML is inert. */ +function renderMarkdown(src: string): string { + if (!src || !src.trim()) { + return '

Nothing to preview yet — switch to Write to add details.

'; + } + const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); + const inline = (s: string) => + esc(s) + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2") + .replace( + /`([^`]+)`/g, + '$1', + ) + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + const lines = src.split(/\r?\n/); + let html = ""; + let list: "ul" | "ol" | null = null; + const closeList = () => { + if (list) { + html += list === "ul" ? "" : ""; + list = null; + } + }; + for (const raw of lines) { + const line = raw.replace(/\s+$/, ""); + let m: RegExpMatchArray | null; + if (!line.trim()) { + closeList(); + continue; + } + if ((m = line.match(/^(#{1,3})\s+(.*)/))) { + closeList(); + const lv = m[1].length; + const sz = lv === 1 ? 18 : lv === 2 ? 15.5 : 13.5; + html += `
${inline(m[2])}
`; + continue; + } + if ((m = line.match(/^>\s?(.*)/))) { + closeList(); + html += `
${inline(m[1])}
`; + continue; + } + if ((m = line.match(/^[-*]\s+(.*)/))) { + if (list !== "ul") { + closeList(); + html += '