Skip to content

Latest commit

 

History

History
43 lines (37 loc) · 16 KB

File metadata and controls

43 lines (37 loc) · 16 KB

AGENTS.md

Learned User Preferences

  • Use Bun exclusively — never npm/yarn/pnpm (bun install, bun add, bun run, bunx)
  • Do not commit unless explicitly asked; short descriptive messages, no AI/Cursor branding or internal doc references
  • Production code quality: DRY, KISS, SOLID, industry-standard; avoid overengineering and overthinking; export names must match file names; fix typos in identifiers on every refactor
  • TDD for fixes/features; schema-first test design; fix behavior when the spec is wrong — don't weaken tests to match broken behavior
  • Always run bun run build after major refactors to verify type-safety before claiming completion
  • Before large migrations, audit all config/doc/rule paths (.cursor/rules, Notes/, AGENTS.md) for stale references to removed concepts
  • Notes/ folder is local-only (.gitignore); never commit local notes or plans; .cursor/hooks/state/continual-learning.json and continual-learning-index.json are local hook churn — keep out of commits (restore if staged). Committed agent context for regressions: AGENTS.md (e.g. Mobile document pad (iOS Safari) under Learned Workspace Facts) — mirror important findings there; do not rely only on the continual-learning plugin JSON
  • Keep debug/info loggers on editor core paths; don't strip them for "cleanup"
  • Cypress tests: split by concern with README for scope; kebab-case dirs, it() not test(); consolidate overlapping tests
  • Theme/UI color consistency is first-class — all surfaces (including third-party pickers) must follow design tokens
  • Full-doc paste (⌘A→⌘V) is a critical scenario to validate
  • User sometimes prefers commands/instructions to run locally rather than the agent auto-installing

Monorepo toolchain

  • Bun-only for install/run (bun install, bun run, bunx); see docs/engineering/toolchain.md for phases, CI parity, and version policy.
  • Quality gates: bun run check = lint + Prettier check + typecheck; bun run check:full adds Stylelint (used at end of pre-push); bun run check:static = lint + Prettier + Stylelint (no tsc, used in CI lint job).
  • Shared devtool versions (ESLint, TypeScript, Prettier, Stylelint, etc.) are owned at the repo root — avoid drifting copies in leaf packages; root catalog: in package.json centralizes pins where used — workspaces reference matching deps as \"package\": \"catalog:\".
  • Jest / library unit tests: jest, babel-jest, jest-environment-jsdom, @types/jest, @babel/preset-typescriptrepo root devDependencies only (single version pin). Library packages use a local jest.config.cjs (inline config is fine); no per-package Jest stack in package.json. @docs.plus/webapp keeps next/jest for the app suite; see .cursor/rules/monorepo-jest.mdc.
  • Tests: root test:allscripts/run-tests.sh; Jest (unit) + Cypress (E2E); parallel via CYPRESS_PARALLEL env.
  • Unit test order: run-tests.sh runs @docs.plus/extension-indent Jest (jest.config.cjs in that package) then @docs.plus/webapp Jest; webapp test uses jest --passWithNoTests so an empty or temporarily absent app Jest suite does not fail CI/local runs.
  • Dependency updates: npm-check-updates and per-package update:packages scripts are removed; scripts/reinstall-packages.sh / reinstall:all-packages are gone. Use bun update from the repo root, or bun run update:all-packages (scripts/update-packages.sh), then bun install at root if the lockfile or install tree needs healing. Do not run parallel bun update in multiple packages/* directories — shared bun.lock / hoisted installs can race and fail with EEXIST.
  • Stay on ESLint 9.x / TypeScript 5.x until a dedicated migration — ESLint 10 and TS 6 have breaking changes.

Learned Workspace Facts

  • docs.plus / docsy — Bun monorepo (packages/*); main app @docs.plus/webapp (Next.js Pages Router), backend @docs.plus/hocuspocus; editor code under packages/webapp/src/components/TipTap/ (extensions, nodes, plugins); src/lib/ removed — all shared utilities in src/utils/, feature-local helpers stay colocated; server-side TiptapTransformer.toYdoc / nested→flat migration must use an extension set that covers every node/mark in stored docs (e.g. TaskList / TaskItem from @tiptap/extension-list aligned with the webapp), not StarterKit alone — missing types fail encode, not flatten logic; batch migrations must fail closed (do not overwrite stored Yjs for a doc when transform/encode fails — keep prior bytes and surface the doc id); run migration CLI via bun run migrate:nested-to-flat from packages/hocuspocus.server after root bun install — invoking the script path alone from an arbitrary cwd can break Bun resolution of yjs for @hocuspocus/transformer
  • Editor uses flat heading schema (heading block*) with decoration-based sections; attrs['toc-id'] renders as data-toc-id; shared heading utilities (computeSection, moveSection, canMapDecorations, transactionAffectsNodeType, matchSections) in TipTap/extensions/shared/; section reorder is TOC-only (useTocDrag / moveHeading + moveSection) — there is no in-editor heading drag handle extension
  • HeadingScale (mandatory spec): extensions/heading-scale/heading-scale.tsdynamic heading font size by rank within a section, not fixed per HTML level or a Google-style ladder. Each H1 starts a new section; within a section, distinct heading levels are sorted and sizes are interpolated evenly between 20pt (max) and 12pt (min); same level twice in one section → same visual size; one distinct level in a section → 20pt. Title (first top-level H1) is included as part of section 1. Decorations only (--hd-size, --hd-rank, --hd-total); never write sizes into the document. Plugin state { fingerprint, decorations } with fingerprint = top-level heading levels in order (e.g. 1,2,4,1,3); full rebuild when fingerprint changes or y-sync$ meta; else map the decoration set. Do not replace this with fixed per-level pt maps — that breaks the agreed behavior.
  • Editor perf: jank is React/Zustand re-renders, not ProseMirror; never put UI flags in useEditor deps; shouldRerenderOnTransaction: false on collab; decoration plugins should avoid full rebuilds on every keystroke — use transactionAffectsNodeType(tr, 'heading') or a cheaper structural check (HeadingScale uses heading-level fingerprint, not only transactionAffectsNodeType); placeholder uses @docs.plus/extension-placeholder (O(1) via state.init/apply) — do NOT replace with TipTap's built-in (O(N) doc.descendants)
  • Zustand: monolithic 7-slice store; all useStore calls must use leaf selectors — never (state) => state or (state) => state.settings. ProseMirror: doc.nodeAt(pos) can throw RangeError for out-of-range — guards must not assume null-only; transaction.before is the pre-step document Node, not EditorState — never call PluginKey.getState(transaction.before); for fold-driven UI (e.g. TOC) snapshot heading-fold plugin state from editor.state and diff across transactions
  • TipTap pad-only SCSS lives under packages/webapp/src/styles/editor/ and loads via styles.scsscomponents/_index.scss@use '../editor'; do not add parallel .scss next to TipTap extensions (single source of truth). Pad chrome: PadTitle border-b for header↔toolbar; tiptap__toolbar uses border-b only (no border-t against PadTitle); pad sheet top border from _blocks.scss for toolbar↔editor; mobile .m_mobile .tiptap__toolbar in _blocks.scss for floating bar. Scrollbars: shared :root tokens in globals.scss; scrollbar-custom scrollbar-thin on .editorWrapper and TOC ScrollArea — one system, avoid ad-hoc scrollbar styling on the pad column
  • Mobile document pad (iOS Safari)packages/webapp (html.m_mobile in styles/_mobile.scss): html/body position: fixed; .mobileLayoutRoot tracks window.visualViewport via syncVisualViewportToCssVars (utils/visualViewportCss.ts) and AppProviders visualViewport resize + scroll (rAF-coalesced). Do not skip CSS sync when height deltas are small — after a large keyboard resize, WebKit can emit further sub-threshold steps; stale --visual-viewport-height leaves a dead band above Safari’s accessory bar. useVisualViewportCssSyncOnFocus (hooks/useVisualViewportCssSyncOnFocus.ts): focusin (capture) on .mobileLayoutRoot .tiptap__editor.docy_editor (pad + mobile history) re-runs syncVisualViewportToCssVars when a final resize is missing. Do not use transform: translateZ(0) on .editor.editorWrapper or contain / will-change: height on .mobileLayoutRoot — WebKit often mis-paints the contenteditable caret (e.g. over the pad header). In-pad scroll targets: scrollElementInMobilePadEditor (utils/scrollMobilePadEditor.ts) for headings/TOC/deep links; avoid raw Element.scrollIntoView on doc nodes — it scrolls the layout viewport and fights fixed chrome + CSS vars. Virtual keyboard / store: innerHeight - visualViewport.height can stay 0 while the keyboard is up after cycles; use applyVirtualKeyboardToStore in utils/virtualKeyboardMetrics.ts (peak visualViewport.height when closed, documentElement.clientHeight - vvh, scrollY + vvh when window.scrollY > 0). useVirtualKeyboard and nudgeVirtualKeyboardOpenFromVisualViewport both call that path; listen to vv scroll and resize. useEditableDocControl: never isEditable = isKeyboardOpen on every effect — keyboard opens before resize; only clear isEditable on keyboard close (true → false). The 500ms DOM sync must not set contenteditable: false when settings.editor.isEditable is still true. AppProviders: if .mobileLayoutRoot, visualViewport.offsetTop > 0, and window.scrollY > 0, window.scrollTo(0, 0) — layout scroll breaks fixed-shell getBoundingClientRect for ProseMirror. Edit entry: EditFAB and double-tap share enableAndFocus() (hooks/useCaretPosition.ts); FAB uses onTouchEnd + suppress synthetic click. enableAndFocus: editor.commands.focus() only — do not chain TipTap scrollIntoView() with ensureCaretVisible / scrollCaretIntoView (double nudge). Mobile caret scroll: behavior: 'auto'; ensureCaretVisible: 2× rAF + one ~300ms retry
  • Production: docker-compose.prod.yml with Traefik; dev compose backend services need context: . (repo root) to match Dockerfile.bun. Hocuspocus image: migration-extensions.ts imports @docs.plus/extension-hypermultimedia and @docs.plus/extension-inline-code at runtime (maindist/); root .dockerignore excludes **/dist, so those packages must be built inside the image — copying only package.json stubs is not enough (other @docs.plus/extension-* may stay stubs for lockfile/workspace only). Prod WebSocket issues: verify hocuspocus containers are healthy and read docker logs before chasing Traefik; crash loops often explain edge 404s or no backend.
  • Supabase client architecture (Pages Router): browser singleton at utils/supabase/index.ts, factory in component.ts, GSSP in server-props.ts, API route in api.ts, URL resolver in url.ts; all browser code imports supabaseClient singleton; types/supabase.ts for generated DB types
  • Standalone extension packages (extension-hyperlink, -hypermultimedia, -indent, -inline-code, -placeholder) share identical structure: TypeScript + tsup build + @tiptap/core peer dep; GFM markdown via @tiptap/markdown, paste at extensions/markdown-paste/, import/export in utils/markdown.ts + toolbar/desktop/DocumentSettingsPanel; sanitizeJsonContent on paste and import paths. @docs.plus/extension-indent: keep pad (TipTap.tsx) and chat composer (useTiptapEditor) on the same Indent.configure({ indentChars: '\t' }) (or widen together). Gating: allowedIndentContexts — allowlist of { textblock, parent } pairs (TipTap type.name) where literal indent/outdent runs; default body + blockquote paragraphs only; [] disables literal indent. Tab / Shift-Tab: sink/lift list (listItem / taskItem when in schema) → table cell nav when table extension is present → literal indent/outdent; extension priority 25 + delegation. Pad/chat default is paragraph under doc / blockquote only; other textblocks need explicit allowedIndentContexts rules. Cypress: packages/webapp/cypress/e2e/editor/indent/; Jest: packages/extension-indent.
  • Document version history (Hocuspocus): Stateless history.list / history.watch; server unicasts { msg: 'history.response', type, response } on the requesting connection (not broadcastStateless). Prisma always uses the collab room’s document id (Hocuspocus document.name); if the client sends a different documentId, respond history_failed. Current history.list returns { versions, latestSnapshot } for one RTT; client still accepts a legacy plain HistoryItem[]. applyHistoryItemToEditor (pages/history/applyHistoryToEditor.ts) is the single TipTap hydration path. loadingHistory clears only after a successful apply (not merely after the network response); useHistoryEditorApplyWhenReady applies when the editor mounts after data arrives; while pendingWatchVersion is set (in-flight history.watch), the apply-when-ready hook must not re-apply stale activeHistory, and late history.list must not reset pending or hydrate from latestSnapshot over that watch. On history_failed, clear pendingWatchVersion so the next watch isn’t dropped. Shareable revision URLs: same pathname/query + #history?version=<n> with <n> = HistoryItem.version; pages/history/historyShareUrl.tsparseHistoryHash, buildHistoryShareUrl, replaceHistoryHashVersion
  • TOC + heading chrome: components/toc/ with tocClasses.ts kept in sync with styles/components/_tableOfContents.scss; --color-docsy = var(--color-primary) in both @theme and :root (globals.scss), auto-tracks DaisyUI light/dark/HC; heading widgets in TipTap/extensions/HeadingActions/plugins/ (hoverChatPlugin, selectionChatPlugin) styled by styles/components/_heading-actions.scss (shared $ha-hit-size with plugins, DRY $ha-group-has-unread :has() selector for unread tray visibility); _unread-badge.scss only styles [data-unread-count] on .ha-chat-btn + notification bell — no .toc__chat-trigger/.ha-group rules; TOC uses React UnreadBadge only, UNREAD_SYNC clears data-unread-count on .toc__chat-trigger; active chat icon uses toc__chat-icon--active class with fill: none (Lucide icons are stroke-based); when nested ul.toc__children lives under the parent li, hide folded subtrees with &.closed > .toc__children { display: none } — fold class still comes from editor state, not CSS alone. TOC data path: useToc.ts throttles heading-driven TOC rebuilds (lodash/throttle); flat heading list → recursive NestedTocNode tree via buildNestedToc at TocDesktop/TocMobile roots (utils.ts); useHeadingScrollSpy.ts debounces scroll/active-heading work (lodash/debounce)
  • Heading fold crinkle: widget decoration driven by data-fold-phase attr for CSS animation; unique Decoration.widget key per phase (fold-${id}-folding/-unfolding/-${id}) forces ProseMirror remount so animation fires each toggle; width uses margin-left/right: calc(-1 * var(--tiptap-inline-pad-end)) to span full sheet; SCSS variables $crinkle-fold-duration/$crinkle-easing for timing (no CSS custom properties); Decoration.node on heading-section removed — animations live on the widget itself; strip count uses MIN_FOLD_STRIPS / MAX_FOLD_STRIPS / CONTENT_HEIGHT_PER_STRIP in heading-fold-plugin.ts — if MIN_FOLD_STRIPS === MAX_FOLD_STRIPS, strip count is fixed regardless of content height