REASON docs sidebar — files on the document routes, opened in CardMirror
The two collapsible sections, Topic Starters and My Personal Documents, that sit above the Videos library tree, and the URL round trip that turns a click on one of them into "that file, loaded in CardMirror, in the main column."
- Routes:
/research/cards(and below it) and/reason-editor— the routes the documents are the subject of, perapps/debate-ai.com/lib/reason-docs/sidebar-routes.ts - Panels:
packages/debate-webview/src/components/reason-docs/(ReasonDocsSidebarPanels,FileTree,TopicStarterTree,FileManagerDialog), overReasonDocsProviderin the root layout - URL rules:
apps/debate-ai.com/lib/reason-docs/route-selection.ts - Where they show:
apps/debate-ai.com/lib/reason-docs/sidebar-routes.ts - Editor:
/reason-editor(debate-editor's CardMirror embed), with its tab strip incomponents/reason-editor/EditorTabStrip.tsxand the tab rules inlib/reason-docs/tab-actions.ts
Where the panels mount
ReasonDocsSidebarPanels is one component mounted in two places:
AppSidebarShell's<aside>, on the routesshowsReasonDocsPanelsadmits —/research/cards(the dock's "Shared" destination, and any page below it) and/reason-editor, whose desktop file navigation is this sidebar, the editor route having no<aside>of its own./reason-editoritself, as a collapsible strip belowmd, since that sidebar ishidden md:flex.
They are deliberately absent everywhere else. The panels used to ride in
every sidebar, /videos included — which took a third mount, an app-owned
docsSlot on LecturesPage, since /videos renders its own sidebar rather
than the shell. That put a document tree above the video library's own nav
on a page that is not about documents; the slot is gone and the videos
sidebar is the dock plus the video nav. The trade is that
a file opened from /research/cards is no longer one click away after hopping to the
video library — the dock hop back is.
In the sidebar the sections sit under the dock and above the Videos tree: the tree is long enough (a section auto-expands to show where you are) that anything below it starts under the fold. Topic Starters comes first, then My Personal Documents. There is no panel switcher and no Open Tabs list: the open files are the tab strip above the editor (below).
Each section has a chevron header and is expanded by default. Collapsing one
sticks, per-device, in localStorage (reason-editor-sidebar-topics-open,
reason-editor-sidebar-files-open). Nothing is fetched until a section is
actually open, so a page that leaves both collapsed makes no document
request at all.
Tabs above the editor
Every file opened on /reason-editor gets a tab in the strip above
CardMirror. Click a tab to switch to it, or its × to close it. Right-click a
tab for:
- Close that tab.
- Close others: every tab but this one, which becomes the open file.
- Close to the right: every tab after this one.
- Close all.
- Undo close: reopens the most recently closed tab. It walks back through the last 20 closed tabs, skipping files deleted since.
Closing the open tab moves to its neighbour, preferring the tab that slid into its place.
Opening a file
Picking a file — an owned document or a public topic starter — does two things:
- sets it active in
ReasonDocsProvider, so a client-side hop switches the editor instantly; and - routes to
/reason-editor/<the file's name>—/reason-editor/cp-answer-to-states, not/reason-editor?topic=2— so the selection is addressable, and legible in a shared link.
Provider state alone covers the hop but not a reload, a pasted link, or a
hard navigation — /videos is its own layout branch and can boot the editor
with an empty provider. Carrying the file in the URL is what makes "click a
file, get that file in CardMirror" hold from every sidebar rather than
only from the ones that hop client-side.
The name is the title slugified (lib/reason-docs/doc-slug.ts): lowercased,
accents folded, apostrophes dropped, everything else non-alphanumeric
collapsed to single hyphens, capped at 80 characters on a word boundary, and
untitled when a title leaves nothing behind. Titles are not unique and ids
are, so two files that slugify the same are told apart by a ~<id> suffix —
impact-turns~d12 for document 12, impact-turns~t7 for topic starter 7.
No title can forge one: ~ is stripped out of titles by the same slugifier.
Already on the editor route, a pick routes nothing at all. /reason-editor
and /reason-editor/<slug> are separate Next routes, so routing between
them would remount CardMirror — and its undo history — on every tab switch;
the provider already has the file open, and ReasonDocsRouteSync renames
the address bar in place with history.replaceState. That same rewrite is
what upgrades an older ?doc=<id> / ?topic=<id> link (still read, and
still resolved) to the named path once the catalogue says what that file is
called, and what keeps the address on a file that is renamed while open. Any
other query parameter is kept — ?share= and ?shareWith= are read by the
same page.
ReasonDocsRouteSync (mounted once on /reason-editor, inside a
<Suspense> since it reads useSearchParams, renders nothing) reads it
back. The rules themselves are pure functions in
lib/reason-docs/route-selection.ts, shared with the sidebar so the two
ends of the round trip cannot drift, and unit-tested in
lib/reason-docs/__tests__/route-selection.test.ts.
Resolution order, per URL:
- the path's name — an exact
~<id>discriminator first, then the bare slug matched against topic starters and then the reader's own documents, then the segment read as an id; - the URL's
topicid, if it names a live topic starter; - the URL's
docid, if it names one of the reader's own non-folder documents; - otherwise, and only when nothing is open, the first file — so the reader lands on something readable instead of an empty pane.
A name or id that matches nothing (a renamed file, a deleted one, someone
else's link) falls through to that same fallback rather than erroring — a
stale link is not a 404 — and a malformed id (?doc=abc, ?doc=3.5) reads
as absent. The deep link applies once per URL: after it opens its file the
reader can pick another from the sidebar without the stale URL dragging them
back, and every pick rewrites the address, which re-arms it.
The deep-link rule and the first-file fallback are one function on purpose. As two React effects they raced: the fallback's closure still saw no selection in the commit where the deep link opened its file, and opened the first file over the top of it.
Read-only topic starters
A topic starter opens in the same CardMirror embed as an owned document, but
read-only: no toolbar, no AI tools, no onChange, a readOnly title, and a
"Public topic starter" label beside it. It gets its own contentKey
namespace (topic-<id>) so the engine treats it as a distinct claim from an
owned document that happens to share the id — the same reason ?doc=7 and
?topic=7 are different URLs.
Topic starters are stored as .cmir
The admin importer on /admin takes a .docx (or a .zip of them) and
stores each one as a CardMirror native file — .cmir, gzipped JSON,
base64-encoded into the row's content column, with format set to
"cmir".
The conversion is CardMirror's own OOXML importer (docxToCmir in
packages/debate-editor/src/native/convert.ts — fromDocxFull piped into
serializeNative, the same pair the desktop bulk converter runs), wrapped
for the route by apps/debate-ai.com/lib/topic-starters/import.ts. What that
buys over the card HTML the importer used to store is everything HTML has no
element for: the Verbatim outline (pocket / hat / block / tag / analytic) as
typed nodes rather than <h1>…<h4>, comment threads, inline images, and
the named character styles that distinguish a cite from an emphasis from a
plain underline.
Reading it back is client-side: topicStarterHtml
(lib/topic-starters/content.ts) parses the file with parseNative and
serializes it through the schema's own toDOM specs for the embed's
content prop, memoized per file on /reason-editor. Doing it in the
browser keeps the catalogue endpoint — which returns up to 100 rows — from
converting files nobody opens.
Rows imported before this carry HTML and still open: format says which a
row is (lib/topic-starters/format.ts), and content that arrives without
its row falls back to sniffing the bytes for the gzip magic. A .cmir that
will not parse renders a notice naming the failure rather than a blank
document.
Organising files
Every file is a CardMirror file whatever it was uploaded as, so no row in
either tree carries a DOCX or CMIR badge; the name is the whole label.
Right-clicking a row opens the same menu as its "…" button, so every action on a file is one gesture from the file itself:
- Files: Open, Edit, Share…, Rename, Duplicate, Move to (any folder, or the top level), Download as .docx, Add to Topic Starters, Delete. Folders get New File / New Folder in place of Open, Edit and Share.
- Topic Starters: Open, Edit a copy, Save to my files, Copy link, Download as .docx. The catalogue is read-only, so a topic starter is edited by editing your own copy of it.
Share… opens the file in the editor with the share-with-contacts dialog
already up (?shareOpen=1, read and stripped by ShareWithContacts).
Sharing starts a live co-editing session in the editor, which is why the file
has to be open there first.
The Sort button in the My Personal Documents header orders the tree by name (A–Z or Z–A)
or by last modified (lib/reason-docs/file-sort.ts). Folders always come
before files. The choice is remembered per device.
Moving files between Files and Topic Starters
Rows drag both ways between the two trees, and each drop means what it can
(lib/reason-docs/drag-data.ts):
- Topic Starters → Files: saves your own copy into the folder you drop
on. A folder comes with everything in it, and a
.cmirstays a.cmir. "Save to my files" in the menu does the same. - Files → Topic Starters: publishes the file to the public catalogue
(
POST /api/admin/topic-starters/items). Only admins can do this. Anyone else sees a message saying so. - Files → Files: moves the row, as before. A folder can't be dropped into itself.
- Desktop → either tree: an upload, as before.
File manager
The File manager button in the My Personal Documents header opens a full-window file
manager over the same files: SVAR's @svar-ui/react-filemanager, the widget
quick search's REASON sidebar opens from its own file manager dialog. Every
action it offers is routed back to the real rows
(components/reason-docs/FileManagerView.tsx, with the path ↔ row mapping
in lib/reason-docs/file-manager-data.ts):
- Open (double-click or Enter) opens the file in this app's editor.
- Add file, add folder and upload create rows in your folders.
- Rename, delete, cut/paste and drag rename, delete and move your rows. Copy/paste duplicates them.
- Download is the same
.docxdownload as the sidebar menu.
Topic Starters appear under a read-only "Topic Starters" folder. Copying or moving out of it saves your own copy. Copying or moving one of your files into it publishes it (admins only). Renaming or deleting inside it is refused. The widget is loaded only when the dialog first opens.
Searching files from Ctrl/Cmd-K
The app-wide search palette (Ctrl/Cmd-K) lists matching files under
Files, below the tools: your own files and the public Topic Starters,
matched by name, folder, outline headings and contents
(lib/reason-docs/file-search.ts). Every word you type has to appear
somewhere. Name matches rank first, then outline matches (a pocket, hat,
block or tag heading holding every word, shown as § Heading), then contents
matches, which show the text around the match. Picking a file opens it in the
editor.
Below the files, Videos & Lectures lists up to eight matches from the
whole video library, rounds and lectures alike, most-viewed first. The
palette asks the Videos page's own feed (/api/videos?q=…) once typing
pauses, so nothing about the library is downloaded until you search. Picking
one opens its watch page (lib/videos/video-palette-search.ts).
A .cmir file's text is read with CardMirror's engine, which is loaded the
first time a query needs it, so the first search over card files can take a
moment longer. On /reason-editor the palette answers Ctrl/Cmd-K, while
Ctrl/Cmd-P stays the editor's own palette.
Known gaps
The docs section starts collapsed onFixed: both sections start expanded everywhere; a reader's own collapse is remembered per device./research/cards, so the files are one click away there rather than on screen.- Undo close lives in the tab strip's right-click menu, and the strip is hidden once every tab is closed, so after Close all there is no tab left to right-click. Reopen a file from the sidebar instead. There is no keyboard shortcut for it either: browsers keep Ctrl/Cmd-Shift-T for themselves.
showsReasonDocsPanelsis a hard-coded route list, not something derived from the nav data. A new documents route has to be added to it by hand.Fixed:SharedCardOpener's?share=<id>flow replaces the URL with a bare/reason-editor, dropping any?doc=/?topic=alongside it.ShareWithContacts.tsx'sdropQueryParam(used by both the?share=and?shareWith=one-shot params) callsroute-selection.ts#urlWithoutParam, which drops only the named parameter and keeps the rest of the URL —?doc=/?topic=alongside a share link now survive being joined. Pinned byroute-selection.test.ts'surlWithoutParamcases.- Conversion runs in the import request, so a 100-file ZIP converts 100 documents inside one Worker invocation — heavier than the regex pass it replaces. A batch large enough to hit the CPU limit fails as a dead request, leaving the files converted before it in the library. Splitting the archive is the workaround; moving the conversion into the browser (as the Parquet card importer does) is the fix.
There is no way to download a topic starter'sFixed: both the Files panel's and the Topic Starters panel's row menu now have a "Download as .docx" item — your own documents and public topic starters alike can be gotten out as a file without opening them in the editor first..cmir. The file is stored and opened in the editor, but the library exposes no file endpoint.The Topic Starters catalogue sorted files before folders, so once it held more than its 100-row cap the folders were cut off and every file inside them vanished from the tree.Fixed:/api/topic-starterssorts folders first, and both trees show a row whose folder is missing at the top level instead of dropping it.Folders in both trees started collapsed whenever the files loaded after the panel mounted (the usual case), and the Topic Starters search matched words from a row's raw JSON, so searching "folder" matched everything.Fixed: folders start open, and the search matches names and tags only.- Publishing to Topic Starters is admin-only and always publishes at once. There is no draft or review step for a reader's own file.
- Ctrl/Cmd-K's file search converts every
.cmirfile it searches on the reader's device. That is fine for the 100-row catalogue and a typical library, but a very large library would want a server-side text index. - Choosing an outline match opens the file at the top, not at the heading that matched.
- The video rows show only the first eight matches; there is no "more videos" row that carries the query over to the Videos page.
Save status above the editor
The status line over the REASON editor (/reason-editor) says where the open
document stands: Unsaved changes (an edit is queued), Saving…, or
Couldn't save — retrying (shown in red; your edits stay in the tab and the
queue retries with backoff). Once the document is saved the line disappears. A public topic starter
reads Public topic starter because it is read-only. The text comes from
describeDocSaveStatus (lib/reason-docs/doc-save-status.ts).
