Replace plain textarea with Tiptap (ProseMirror) WYSIWYG editor for notes and tasks. Headings, bold, lists, and code blocks now render inline while editing. Tags and wikilinks get visual highlighting via decoration plugins, and autocomplete uses @tiptap/suggestion with heading disambiguation. Layout: pin navbar with app-shell flex column (100dvh), sticky editor toolbar above scrolling content, AI Assist panel fixed at bottom 1/3 with full-height section selector and prompt. Increase max-width to 960px across all views. Hide assist controls during streaming/review. Other fixes: markdown paste handling, auto-syncing assist sections via body watcher, trailing newline on AI accept, ChatView height fix. Add README.md describing the app, usage guide, and technical overview. Update summary.md with Phase 5.2 roadmap and current status. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.1 KiB
TypeScript
80 lines
2.1 KiB
TypeScript
import { createApp, type App } from "vue";
|
|
import SuggestionDropdown from "@/components/SuggestionDropdown.vue";
|
|
import type { SuggestionItem } from "@/components/SuggestionDropdown.vue";
|
|
import type { SuggestionProps, SuggestionKeyDownProps } from "@tiptap/suggestion";
|
|
|
|
interface DropdownExposed {
|
|
onKeyDown: (e: KeyboardEvent) => boolean;
|
|
}
|
|
|
|
export function createSuggestionRenderer() {
|
|
let app: App | null = null;
|
|
let el: HTMLElement | null = null;
|
|
let componentRef: DropdownExposed | null = null;
|
|
let commandFn: ((item: SuggestionItem) => void) | null = null;
|
|
|
|
function mountDropdown(items: SuggestionItem[]) {
|
|
if (!el) return;
|
|
if (app) app.unmount();
|
|
app = createApp(SuggestionDropdown, {
|
|
items,
|
|
command: (item: SuggestionItem) => commandFn?.(item),
|
|
});
|
|
const instance = app.mount(el);
|
|
componentRef = instance as unknown as DropdownExposed;
|
|
}
|
|
|
|
return {
|
|
onStart(props: SuggestionProps) {
|
|
commandFn = (item: SuggestionItem) => props.command(item);
|
|
|
|
el = document.createElement("div");
|
|
el.style.position = "fixed";
|
|
el.style.zIndex = "100";
|
|
document.body.appendChild(el);
|
|
|
|
mountDropdown(props.items as SuggestionItem[]);
|
|
updatePosition(props);
|
|
},
|
|
|
|
onUpdate(props: SuggestionProps) {
|
|
commandFn = (item: SuggestionItem) => props.command(item);
|
|
mountDropdown(props.items as SuggestionItem[]);
|
|
updatePosition(props);
|
|
},
|
|
|
|
onKeyDown(props: SuggestionKeyDownProps) {
|
|
if (props.event.key === "Escape") {
|
|
cleanup();
|
|
return true;
|
|
}
|
|
return componentRef?.onKeyDown(props.event) ?? false;
|
|
},
|
|
|
|
onExit() {
|
|
cleanup();
|
|
},
|
|
};
|
|
|
|
function updatePosition(props: SuggestionProps) {
|
|
if (!el || !props.clientRect) return;
|
|
const rect = props.clientRect();
|
|
if (!rect) return;
|
|
el.style.top = rect.bottom + "px";
|
|
el.style.left = rect.left + "px";
|
|
}
|
|
|
|
function cleanup() {
|
|
if (app) {
|
|
app.unmount();
|
|
app = null;
|
|
}
|
|
if (el) {
|
|
el.remove();
|
|
el = null;
|
|
}
|
|
componentRef = null;
|
|
commandFn = null;
|
|
}
|
|
}
|