diff --git a/packages/app/package.json b/packages/app/package.json index dea1a3e7..74a08fa2 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -27,6 +27,11 @@ "bump:set": "node scripts/bump-version.mjs --set" }, "dependencies": { + "@codemirror/commands": "^6.8.0", + "@codemirror/lang-markdown": "^6.3.3", + "@codemirror/language": "^6.11.0", + "@codemirror/state": "^6.5.2", + "@codemirror/view": "^6.38.0", "@opencode-ai/sdk": "^1.1.31", "@radix-ui/colors": "^3.0.0", "@solid-primitives/event-bus": "^1.1.2", diff --git a/packages/app/src/app/components/live-markdown-editor.tsx b/packages/app/src/app/components/live-markdown-editor.tsx new file mode 100644 index 00000000..515173af --- /dev/null +++ b/packages/app/src/app/components/live-markdown-editor.tsx @@ -0,0 +1,341 @@ +import { createEffect, onCleanup, onMount } from "solid-js"; + +import { EditorState, StateField } from "@codemirror/state"; +import { + Decoration, + type DecorationSet, + EditorView, + WidgetType, + keymap, + placeholder as cmPlaceholder, +} from "@codemirror/view"; +import { history, defaultKeymap, historyKeymap, indentWithTab } from "@codemirror/commands"; +import { markdown } from "@codemirror/lang-markdown"; + +type Props = { + value: string; + onChange: (value: string) => void; + placeholder?: string; + ariaLabel?: string; + autofocus?: boolean; + class?: string; +}; + +type EmphasisRange = { + kind: "em" | "strong"; + openFrom: number; + openTo: number; + closeFrom: number; + closeTo: number; + contentFrom: number; + contentTo: number; +}; + +const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n)); + +const findEmphasisRanges = (line: string): EmphasisRange[] => { + // Minimal, line-local emphasis parsing. + // - Strong: **text** + // - Emphasis: *text* + // Avoid matching markers that wrap whitespace. + + const ranges: EmphasisRange[] = []; + + const used = new Array(line.length).fill(false); + const markUsed = (from: number, to: number) => { + for (let i = clamp(from, 0, line.length); i < clamp(to, 0, line.length); i += 1) used[i] = true; + }; + const isUsed = (from: number, to: number) => { + for (let i = clamp(from, 0, line.length); i < clamp(to, 0, line.length); i += 1) { + if (used[i]) return true; + } + return false; + }; + + // Strong first. + { + const re = /\*\*(?!\s)([^*\n]+?)(? { + const headingLine = (level: number) => + Decoration.line({ attributes: { class: `cm-ow-heading cm-ow-heading-${level}` } }); + const hide = Decoration.replace({ widget: new HiddenMarkerWidget() }); + const emMark = Decoration.mark({ class: "cm-ow-em" }); + const strongMark = Decoration.mark({ class: "cm-ow-strong" }); + + const compute = (state: EditorState): DecorationSet => { + const ranges: any[] = []; + const add = (from: number, to: number, deco: any) => { + ranges.push(deco.range(from, to)); + }; + + const doc = state.doc; + const selections = state.selection.ranges; + const activeLines = new Set(); + for (const r of selections) { + const fromLine = doc.lineAt(r.from).number; + const toLine = doc.lineAt(r.to).number; + for (let n = fromLine; n <= toLine; n += 1) activeLines.add(n); + if (r.empty) activeLines.add(doc.lineAt(r.head).number); + } + + const cursorPos = state.selection.main.head; + for (let lineNumber = 1; lineNumber <= doc.lines; lineNumber += 1) { + const line = doc.line(lineNumber); + const lineText = line.text; + const lineActive = activeLines.has(line.number); + + // Headings: hide leading '#' when line is not active. + const headingMatch = /^(#{1,6})\s+/.exec(lineText); + if (headingMatch) { + const level = headingMatch[1]?.length ?? 1; + add(line.from, line.from, headingLine(level)); + + if (!lineActive) { + const markerLen = (headingMatch[0] ?? "").length; + add(line.from, line.from + markerLen, hide); + } + } + + // Emphasis / strong emphasis: style inner text; hide markers unless cursor is inside. + for (const r of findEmphasisRanges(lineText)) { + const absOpenFrom = line.from + r.openFrom; + const absOpenTo = line.from + r.openTo; + const absCloseFrom = line.from + r.closeFrom; + const absCloseTo = line.from + r.closeTo; + const absContentFrom = line.from + r.contentFrom; + const absContentTo = line.from + r.contentTo; + if (absContentTo <= absContentFrom) continue; + + const cursorInside = cursorPos >= absOpenFrom && cursorPos <= absCloseTo; + const mark = r.kind === "strong" ? strongMark : emMark; + + if (!cursorInside) { + add(absOpenFrom, absOpenTo, hide); + } + + add(absContentFrom, absContentTo, mark); + + if (!cursorInside) { + add(absCloseFrom, absCloseTo, hide); + } + } + + } + + return Decoration.set(ranges, true); + }; + + const field = StateField.define({ + create(state) { + return compute(state); + }, + update(value, tr) { + if (tr.docChanged || tr.selection) return compute(tr.state); + return value; + }, + provide: (f) => EditorView.decorations.from(f), + }); + + return field; +}; + +const editorTheme = EditorView.theme({ + "&": { + fontSize: "14px", + }, + ".cm-scroller": { + fontFamily: "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial", + }, + ".cm-content": { + padding: "12px 14px", + caretColor: "var(--dls-text-primary)", + }, + ".cm-line": { + padding: "0 2px", + }, + ".cm-focused": { + outline: "none", + }, + ".cm-selectionBackground": { + backgroundColor: "rgba(var(--dls-accent-rgb) / 0.18)", + }, + ".cm-focused .cm-selectionBackground": { + backgroundColor: "rgba(var(--dls-accent-rgb) / 0.22)", + }, + ".cm-cursor": { + borderLeftColor: "var(--dls-text-primary)", + }, + ".cm-placeholder": { + color: "var(--dls-text-secondary)", + }, + ".cm-ow-em": { + fontStyle: "italic", + }, + ".cm-ow-strong": { + fontWeight: "650", + }, + ".cm-ow-heading": { + letterSpacing: "-0.01em", + }, + ".cm-line.cm-ow-heading-1": { + fontSize: "28px", + fontWeight: "750", + lineHeight: "1.15", + paddingTop: "6px", + paddingBottom: "6px", + }, + ".cm-line.cm-ow-heading-2": { + fontSize: "22px", + fontWeight: "720", + lineHeight: "1.2", + paddingTop: "6px", + paddingBottom: "6px", + }, + ".cm-line.cm-ow-heading-3": { + fontSize: "18px", + fontWeight: "700", + lineHeight: "1.25", + paddingTop: "5px", + paddingBottom: "5px", + }, + ".cm-line.cm-ow-heading-4": { + fontSize: "16px", + fontWeight: "680", + lineHeight: "1.3", + paddingTop: "4px", + paddingBottom: "4px", + }, + ".cm-line.cm-ow-heading-5": { + fontSize: "15px", + fontWeight: "660", + lineHeight: "1.35", + paddingTop: "3px", + paddingBottom: "3px", + }, + ".cm-line.cm-ow-heading-6": { + fontSize: "14px", + fontWeight: "650", + lineHeight: "1.4", + paddingTop: "2px", + paddingBottom: "2px", + }, +}); + +export default function LiveMarkdownEditor(props: Props) { + let hostEl: HTMLDivElement | undefined; + let view: EditorView | undefined; + + const createState = (doc: string) => + EditorState.create({ + doc, + extensions: [ + history(), + keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), + markdown(), + EditorView.lineWrapping, + cmPlaceholder(props.placeholder ?? ""), + editorTheme, + obsidianishLivePreview(), + EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + props.onChange(update.state.doc.toString()); + }), + ], + }); + + onMount(() => { + if (!hostEl) return; + view = new EditorView({ + state: createState(props.value ?? ""), + parent: hostEl, + }); + + if (props.autofocus) { + queueMicrotask(() => view?.focus()); + } + }); + + createEffect(() => { + if (!view) return; + const next = props.value ?? ""; + const current = view.state.doc.toString(); + if (next === current) return; + view.dispatch({ changes: { from: 0, to: current.length, insert: next } }); + }); + + onCleanup(() => { + view?.destroy(); + view = undefined; + }); + + return ( +
(hostEl = el)} + /> + ); +} diff --git a/packages/app/src/app/components/session/scratchpad-panel.tsx b/packages/app/src/app/components/session/scratchpad-panel.tsx new file mode 100644 index 00000000..93ef2701 --- /dev/null +++ b/packages/app/src/app/components/session/scratchpad-panel.tsx @@ -0,0 +1,62 @@ +import { Show } from "solid-js"; +import { FileText, Trash2, X } from "lucide-solid"; + +import LiveMarkdownEditor from "../live-markdown-editor"; + +type Props = { + value: string; + onChange: (value: string) => void; + onClose?: () => void; + onClear?: () => void; + title?: string; +}; + +export default function ScratchpadPanel(props: Props) { + const title = () => props.title ?? "Notes"; + const canClear = () => typeof props.onClear === "function"; + + return ( +
+
+
+ +
{title()}
+
+
+ + + + + + +
+
+ +
+ +
+
+ ); +} diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index 6aa266bc..a7a8b367 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -27,6 +27,8 @@ import { Check, ChevronDown, ChevronRight, + Cpu, + FileText, HardDrive, History, ListTodo, @@ -63,6 +65,7 @@ import soulSetupTemplate from "../data/commands/give-me-a-soul.md?raw"; import MessageList from "../components/session/message-list"; import Composer from "../components/session/composer"; import type { SidebarSectionState } from "../components/session/sidebar"; +import ScratchpadPanel from "../components/session/scratchpad-panel"; import FlyoutItem from "../components/flyout-item"; import QuestionModal from "../components/question-modal"; import ArtifactsPanel from "../components/session/artifacts-panel"; @@ -238,6 +241,78 @@ export default function SessionView(props: SessionViewProps) { const [markdownEditorOpen, setMarkdownEditorOpen] = createSignal(false); const [markdownEditorPath, setMarkdownEditorPath] = createSignal(null); + const SCRATCHPAD_OPEN_KEY = "openwork.scratchpad.open.v1"; + const SCRATCHPAD_VALUE_PREFIX = "openwork.scratchpad.value.v1"; + const readBool = (key: string, fallback: boolean) => { + if (typeof window === "undefined") return fallback; + try { + const raw = window.localStorage.getItem(key); + if (!raw) return fallback; + if (raw === "true") return true; + if (raw === "false") return false; + return fallback; + } catch { + return fallback; + } + }; + const writeBool = (key: string, value: boolean) => { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(key, value ? "true" : "false"); + } catch { + // ignore + } + }; + const readString = (key: string) => { + if (typeof window === "undefined") return ""; + try { + return window.localStorage.getItem(key) ?? ""; + } catch { + return ""; + } + }; + const writeString = (key: string, value: string) => { + if (typeof window === "undefined") return; + try { + const trimmed = value ?? ""; + if (!trimmed.trim()) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, trimmed); + } + } catch { + // ignore + } + }; + + const [scratchpadOpen, setScratchpadOpen] = createSignal(readBool(SCRATCHPAD_OPEN_KEY, true)); + createEffect(() => writeBool(SCRATCHPAD_OPEN_KEY, scratchpadOpen())); + + const scratchpadStorageKey = createMemo(() => { + const workspaceId = props.activeWorkspaceId || "workspace"; + const sessionId = props.selectedSessionId || "draft"; + return `${SCRATCHPAD_VALUE_PREFIX}.${workspaceId}.${sessionId}`; + }); + const [scratchpadValue, setScratchpadValue] = createSignal(""); + + createEffect( + on(scratchpadStorageKey, (key) => { + setScratchpadValue(readString(key)); + }) + ); + + createEffect(() => { + const key = scratchpadStorageKey(); + const value = scratchpadValue(); + let timer: number | undefined; + if (typeof window !== "undefined") { + timer = window.setTimeout(() => writeString(key, value), 150); + } + onCleanup(() => { + if (typeof window !== "undefined" && timer) window.clearTimeout(timer); + }); + }); + // When a session is selected (i.e. we are in SessionView), the right sidebar is // navigation-only. Avoid showing any tab as "selected" to reduce confusion. const showRightSidebarSelection = createMemo(() => !props.selectedSessionId); @@ -2024,7 +2099,7 @@ export default function SessionView(props: SessionViewProps) {
-
+
-

- {selectedSessionTitle() || "New task"} -

+ +

{selectedSessionTitle() || "New task"}

{props.headerStatus} @@ -2065,6 +2139,20 @@ export default function SessionView(props: SessionViewProps) {
+ + -
- -
+ 0}> +
+ +
+
+
+ + + + + 0}>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3a73f14..852fb582 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,21 @@ importers: packages/app: dependencies: + '@codemirror/commands': + specifier: ^6.8.0 + version: 6.10.2 + '@codemirror/lang-markdown': + specifier: ^6.3.3 + version: 6.5.0 + '@codemirror/language': + specifier: ^6.11.0 + version: 6.12.1 + '@codemirror/state': + specifier: ^6.5.2 + version: 6.5.4 + '@codemirror/view': + specifier: ^6.38.0 + version: 6.39.14 '@opencode-ai/sdk': specifier: ^1.1.31 version: 1.1.39 @@ -384,6 +399,36 @@ packages: resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} + '@codemirror/autocomplete@6.20.0': + resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} + + '@codemirror/commands@6.10.2': + resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-html@6.4.11': + resolution: {integrity: sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==} + + '@codemirror/lang-javascript@6.2.4': + resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==} + + '@codemirror/lang-markdown@6.5.0': + resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + + '@codemirror/language@6.12.1': + resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==} + + '@codemirror/lint@6.9.4': + resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==} + + '@codemirror/state@6.5.4': + resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==} + + '@codemirror/view@6.39.14': + resolution: {integrity: sha512-WJcvgHm/6Q7dvGT0YFv/6PSkoc36QlR0VCESS6x9tGsnF1lWLmmYxOgX3HH6v8fo6AvSLgpcs+H0Olre6MKXlg==} + '@dimforge/rapier2d-simd-compat@0.17.3': resolution: {integrity: sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg==} @@ -838,6 +883,30 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.1': + resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} + + '@lezer/css@1.3.0': + resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/lr@1.4.8': + resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==} + + '@lezer/markdown@1.6.3': + resolution: {integrity: sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==} + + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@next/env@14.2.5': resolution: {integrity: sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==} @@ -1512,6 +1581,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2318,6 +2390,9 @@ packages: resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} engines: {node: '>=10'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -2480,6 +2555,9 @@ packages: vite: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + web-tree-sitter@0.25.10: resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} peerDependencies: @@ -2764,6 +2842,86 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@codemirror/autocomplete@6.20.0': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + '@lezer/common': 1.5.1 + + '@codemirror/commands@6.10.2': + dependencies: + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + '@lezer/common': 1.5.1 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.0 + + '@codemirror/lang-html@6.4.11': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.4 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + '@lezer/common': 1.5.1 + '@lezer/css': 1.3.0 + '@lezer/html': 1.3.13 + + '@codemirror/lang-javascript@6.2.4': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/language': 6.12.1 + '@codemirror/lint': 6.9.4 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + '@lezer/common': 1.5.1 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-markdown@6.5.0': + dependencies: + '@codemirror/autocomplete': 6.20.0 + '@codemirror/lang-html': 6.4.11 + '@codemirror/language': 6.12.1 + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + '@lezer/common': 1.5.1 + '@lezer/markdown': 1.6.3 + + '@codemirror/language@6.12.1': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + style-mod: 4.1.3 + + '@codemirror/lint@6.9.4': + dependencies: + '@codemirror/state': 6.5.4 + '@codemirror/view': 6.39.14 + crelt: 1.0.6 + + '@codemirror/state@6.5.4': + dependencies: + '@marijn/find-cluster-break': 1.0.2 + + '@codemirror/view@6.39.14': + dependencies: + '@codemirror/state': 6.5.4 + crelt: 1.0.6 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@dimforge/rapier2d-simd-compat@0.17.3': optional: true @@ -3139,6 +3297,41 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.1': {} + + '@lezer/css@1.3.0': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.8 + + '@lezer/lr@1.4.8': + dependencies: + '@lezer/common': 1.5.1 + + '@lezer/markdown@1.6.3': + dependencies: + '@lezer/common': 1.5.1 + '@lezer/highlight': 1.2.3 + + '@marijn/find-cluster-break@1.0.2': {} + '@next/env@14.2.5': {} '@next/swc-darwin-arm64@14.2.5': @@ -3744,6 +3937,8 @@ snapshots: convert-source-map@2.0.0: {} + crelt@1.0.6: {} + cssesc@3.0.0: {} csstype@3.2.3: {} @@ -4534,6 +4729,8 @@ snapshots: '@tokenizer/token': 0.3.0 peek-readable: 4.1.0 + style-mod@4.1.3: {} + styled-jsx@5.1.1(react@18.2.0): dependencies: client-only: 0.0.1 @@ -4680,6 +4877,8 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + w3c-keyname@2.2.8: {} + web-tree-sitter@0.25.10: {} webidl-conversions@3.0.1: {} diff --git a/pr/hybrid-editor-live-preview/full.png b/pr/hybrid-editor-live-preview/full.png new file mode 100644 index 00000000..a2718e4d Binary files /dev/null and b/pr/hybrid-editor-live-preview/full.png differ diff --git a/pr/hybrid-editor-live-preview/heading-active-line.png b/pr/hybrid-editor-live-preview/heading-active-line.png new file mode 100644 index 00000000..8ea540c2 Binary files /dev/null and b/pr/hybrid-editor-live-preview/heading-active-line.png differ diff --git a/pr/hybrid-editor-live-preview/heading-live-preview.png b/pr/hybrid-editor-live-preview/heading-live-preview.png new file mode 100644 index 00000000..fa45405d Binary files /dev/null and b/pr/hybrid-editor-live-preview/heading-live-preview.png differ diff --git a/pr/hybrid-editor-live-preview/italic-live-preview.png b/pr/hybrid-editor-live-preview/italic-live-preview.png new file mode 100644 index 00000000..bc185190 Binary files /dev/null and b/pr/hybrid-editor-live-preview/italic-live-preview.png differ diff --git a/pr/hybrid-editor-live-preview/list-live-preview.png b/pr/hybrid-editor-live-preview/list-live-preview.png new file mode 100644 index 00000000..7260b6de Binary files /dev/null and b/pr/hybrid-editor-live-preview/list-live-preview.png differ