Skip to content

T-A0HT-md-ui-layer-skeleton

State: closed/done · Impact: high · Complexity: medium

Every product that renders or edits markdown today rebuilds the same chrome: the token bridge, the editor CSS, the mode switch, the doc-sync guard, the mermaid theme binding. This task creates the @sksizer/markdown-ui Nuxt layer at packages/ts/markdown-ui/ with the one component products mount, MdSurface, plus MdViewer, MdModeToggle, MdMermaidBlock and the two composables that own sync and plugin registration. It runs against today’s engine and needs nothing new from it.

LocationRole today
packages/ts/ui/nuxt.config.tsLayer shape to copy: absolute-path CSS, typescript.tsConfig include and exclude, no ssr key
packages/ts/ui/moon.ymlLayer check (nuxi typecheck) and test tasks
packages/ts/ui/.storybook/main.tsStorybook over @nuxt/ui/vite with auto-import dirs; port 4890
packages/ts/ui/tests/layerConfig.test.tsRegression test that a layer declares no ssr
solutions/determined/experiences/notes/nuxt.config.tsResolves @sksizer/obsidian-md/theme.css and katex/dist/katex.min.css via createRequire; transpiles both engines
solutions/determined/experiences/notes/app/assets/notes.css:root block of 25 --obsmd-*-override variables mapped to --d-*; .notes-reading prose rules; .notes-editor chrome rules
solutions/determined/experiences/notes/app/components/NotesEditor.vueapplying guard between the store and the editor doc ref; redraw() selection re-dispatch
solutions/determined/experiences/agent-pants/app/components/SkillBodyEditor.vueSecond copy of the applying guard
solutions/determined/experiences/notes/app/components/NotesNoteView.vueSegmented read/live/source control (data-testid="notes-mode-switch")
solutions/determined/experiences/notes/app/composables/useNoteComponents.tsThemedMermaidBlock binds MermaidBlock’s theme to useDTheme().isDark
solutions/determined/experiences/agent-pants/app/composables/useAgentPantsEditorProvider.tsAn EditorIndexProvider that already satisfies the MarkdownProvider shape below
scripts/guard-raw-palette.ts#RINGSPalette guard enrolment table; the packages/ts/ui components ring is the all-enforce() precedent
package.jsonRoot workspaces globs packages/ts/*
docs/LIBRARIES.mdPackage roster; no markdown-ui row
packages/ts/markdown-ui/
package.json name @sksizer/markdown-ui, main ./nuxt.config.ts, exports ./components/* ./composables/* ./utils/* ./css/*
deps: @determined/ui, @sksizer/obsidian-md, @sksizer/obsidian-editor (workspace:*), @iconify-json/lucide,
@nuxt/ui + tailwindcss for resolution only (never in `modules`, never imported)
nuxt.config.ts extends ['@determined/ui']; no `ssr` key; build.transpile both engines; css: obsidian-md theme.css,
katex.min.css (createRequire from the layer), app/assets/css/markdown-ui.css;
typescript.tsConfig allowImportingTsExtensions; vite.optimizeDeps.include ['unified','extend','debug']
moon.yml layer library, stack frontend; check = nuxi typecheck, test = bun test
tsconfig.json extends ./.nuxt/tsconfig.json
.storybook/ ported from packages/ts/ui/.storybook (port 4892); auto-import dirs include @determined/ui's app/utils
and app/composables; fixtures/demoProvider.ts, fixtures/sample.md
app/assets/css/markdown-ui.css
app/components/Md*.vue (+ .stories.ts) app/composables/use*.ts app/utils/md*.ts tests/*.test.ts

Components (prefix Md*; every auto-imported util is md/MD_-prefixed):

ComponentRoleEngine exports used
MdSurfaceThe one component products mount. v-model = note body; frontmatter?: string; mode: 'read'|'live'|'source' (v-model:mode), modes, readonly, provider: MarkdownProvider, plugins, components, renderOptions, foldable, foldedHeadings, foldedLists, measure, keys.cycleMode (default mod+shift+m). Emits update:modelValue, save, open-link, tag-click, task-toggle, fold-toggle, outline, rendered, ready. Exposes view, scroller, focus(), cycleMode(), scrollToLine(n), scrollToHeading(id), redraw(). read renders MdViewer; live/source render the editor inside <ClientOnly>. Provides NoteSourceKey built from provider by mdNoteSource; subscribes provider.onInvalidate to redraw(); registers in useActiveMarkdownSurface()useObsidianEditor (view, focus, cursorLine), setMode, scrollLineToTop, extractOutline
MdViewerRead-only wrapper over ObsidianMarkdown; merges plugin component maps; root class .md-reading—
MdModeToggleSegmented read/live/source control lifted from NotesNoteView.vue; data-testid="md-mode-toggle"; disabledModes prop with tooltip text—
MdMermaidBlockThemedMermaidBlock promoted to a layer built-in bound to useDTheme().isDark; registered as lang:mermaid in MdViewer’s default map—

Composables and utils:

interface MarkdownProvider extends EditorIndexProvider {
loadNote?(path: string): Promise<LoadedNote | null>
saveAttachment?(file: File, notePath?: string): Promise<string>
searchHeadings?(path: string, query: string): Promise<OutlineEntry[]>
onInvalidate?(cb: () => void): () => void
}
mdNoteSource(provider): NoteSource // app/utils/mdProvider.ts
useMarkdownEditorSync({ model, view, doc }) // owns the `applying` guard and `redraw()` once
interface MarkdownPlugin { id; editor?: EditorPlugin | EditorPlugin[]; components?: ComponentMap; renderOptions?; commands?: MdChromeCommand[]; toolbar?: {group, ids}[] }
provideMarkdownPlugins(plugins) // component-scoped
registerMarkdownPlugin(plugin) // module-level reactive registry for Nuxt plugins
useMarkdownPlugins() // injected ∪ registered, id-deduped
useActiveMarkdownSurface() // shallowRef of the focused surface's exposed API

redraw() re-dispatches the current selection, the mechanism NotesEditor.vue uses today; requestRedraw from T-0GV4-md-engine-line-endings-corpus replaces that one call later.

The layer stylesheet markdown-ui.css, sections 1–4: the --obsmd-*-override ← --d-* bridge moved verbatim from notes.css onto :root (its two undeclared tokens --d-shadow-md and --d-bg-input replaced with --d-shadow-popover and --d-bg-control from the ui tokens stylesheet); layer variables --md-measure: 46rem, --md-font-size, --md-bottom-pad; reading prose rules for .md-reading (the generic parts of the .notes-reading rules); editor chrome for .md-editor (the .notes-editor rules). Dark mode needs no block. Enrol packages/ts/markdown-ui/app in RINGS with all five rules enforce().

  1. Scaffold packages/ts/markdown-ui/ per the tree above; bun install; add the markdown-ui row to docs/LIBRARIES.md.
  2. Port the layer-config regression test from ui as layer-config.test.ts (no ssr; extends contains @determined/ui).
  3. Write mdProvider.ts (MarkdownProvider, mdNoteSource) and useMarkdownPlugins.ts with its test (dedupe by id, injected wins over registered, registry is reactive).
  4. Write useMarkdownEditorSync.ts with its test under happy-dom: external write does not emit; user edit emits once; redraw() dispatches.
  5. Write MdMermaidBlock.vue (from ThemedMermaidBlock), MdViewer.vue, MdModeToggle.vue.
  6. Write MdSurface.vue composing the above; useActiveMarkdownSurface.ts registers on focus.
  7. Write the stylesheet sections 1–4; add the guard ring; run just ci-guards.
  8. Port .storybook/ (port 4892) with fixtures/demoProvider.ts (in-memory two-note vault) and fixtures/sample.md; one story per component.
  9. moon run markdown-ui:check markdown-ui:test; bun run storybook:build.
LocationKindChange
packages/ts/markdown-ui/package.jsonnewLayer manifest
packages/ts/markdown-ui/nuxt.config.tsnewLayer config
packages/ts/markdown-ui/moon.ymlnewcheck, test
packages/ts/markdown-ui/tsconfig.jsonnewExtends .nuxt/tsconfig.json
packages/ts/markdown-ui/README.mdnewWhat the layer is, how a product extends it, the MarkdownProvider contract
packages/ts/markdown-ui/.storybook/newPorted from ui (main, preview, stubs) at port 4892, plus fixtures/demoProvider.ts and fixtures/sample.md
packages/ts/markdown-ui/app/assets/css/markdown-ui.cssnewBridge, layer variables, reading prose, editor chrome
packages/ts/markdown-ui/app/components/MdSurface.vuenewThe mount point
packages/ts/markdown-ui/app/components/MdSurface.stories.tsnewRead, live, source stories
packages/ts/markdown-ui/app/components/MdViewer.vuenewRead-only renderer wrapper
packages/ts/markdown-ui/app/components/MdViewer.stories.tsnewStory
packages/ts/markdown-ui/app/components/MdModeToggle.vuenewSegmented control
packages/ts/markdown-ui/app/components/MdModeToggle.stories.tsnewStory
packages/ts/markdown-ui/app/components/MdMermaidBlock.vuenewTheme-bound mermaid block
packages/ts/markdown-ui/app/composables/useMarkdownEditorSync.tsnewapplying guard, redraw()
packages/ts/markdown-ui/app/composables/useMarkdownPlugins.tsnewprovideMarkdownPlugins, registerMarkdownPlugin, useMarkdownPlugins
packages/ts/markdown-ui/app/composables/useActiveMarkdownSurface.tsnewFocused-surface registry
packages/ts/markdown-ui/app/utils/mdProvider.tsnewMarkdownProvider, mdNoteSource
packages/ts/markdown-ui/tests/layer-config.test.tsnewNo ssr; extends ui
packages/ts/markdown-ui/tests/useMarkdownPlugins.test.tsnewRegistry tests
packages/ts/markdown-ui/tests/useMarkdownEditorSync.test.tsnewSync guard tests
packages/ts/markdown-ui/tests/mdSurface.test.tsnewMount, v-model, mode cycle under happy-dom
scripts/guard-raw-palette.tsmodifymarkdown-ui ring, all rules enforce()
docs/LIBRARIES.mdmodifymarkdown-ui row in the TypeScript table
  • AC-1: moon run markdown-ui:check markdown-ui:test passes.
  • AC-2: the layer-config test asserts the config has no ssr key and extends includes @determined/ui.
  • AC-3: the surface test: an external modelValue write does not emit update:modelValue; one user edit emits exactly once; cycleMode() from live to source and back keeps the document and the cursor position.
  • AC-4: grep -rn "from '@nuxt/ui'\|from 'tailwindcss'\|from '@codemirror/" packages/ts/markdown-ui/app returns nothing, and the layer manifest lists no @codemirror/* dependency.
  • AC-5: just ci-guards exits 0 with a markdown-ui ring in scripts/guard-raw-palette.ts whose five rules are enforce().
  • AC-6: the layer stylesheet declares every --obsmd-*-override name that solutions/determined/experiences/notes/app/assets/notes.css declares today (25 names), and references no token absent from packages/ts/ui/app/assets/css/tokens.css.
  • AC-7: bun run storybook:build in the package succeeds with stories for MdSurface, MdViewer, MdModeToggle.
  • AC-8: useMarkdownPlugins() returns injected and registered plugins deduplicated by id (asserted in the registry test).
  • AC-9: docs/LIBRARIES.md has a markdown-ui row.
  • No task dependency; runs against today’s engine exports.
  • S-0010 / D-DLNG: no @nuxt/ui or Tailwind import; colours are --d-* tokens; the package enrols in the palette guard. D-4QGN: the surface takes the body and frontmatter separately.
  • D-0001 placement: tier 1 packages/ts/, main: ./nuxt.config.ts, consumed via extends.

Member of milestone M-931L; chrome step C1 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.


← Back to Tasks