Commit History
-
fix(editor): use explicit class for multi-count avatar z-index
Replace the :has() selector with an explicit --multi modifier class for reliable cross-browser support of the overlapping collaborator avatars. Co-authored-by: Cursor <cursoragent@cursor.com>6b2bf82
-
feat(editor): dedupe connected collaborators by identity
The presence avatars were keyed by Yjs awareness clientId, so the same person showed up once per connection: every extra tab/device, and every lingering pre-timeout socket left behind by a reconnect (amplified by the refocus re-sync), rendered the same circle again. Collapse awareness clients sharing an identity (HF username, falling back to avatar URL then clientId) into a single avatar, with a "xN" badge + tooltip when a person has multiple live sessions. Co-authored-by: Cursor <cursoragent@cursor.com>a0b96b1 -
fix(editor): preview the banner correctly in the Embed Studio
The studio built every preview srcdoc without fullBleed and let the iframe fill the whole preview pane, so the banner (an edge-to-edge 5:2 embed) was rendered like a height-reporting chart and looked nothing like the hero. When editing banner.html, build the srcdoc with `fullBleed: true` and render it inside a centered 5:2 stage (matching .hero-banner dimensions), so the studio preview is a true WYSIWYG of the published header. Regular charts are unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>3d7f9ad -
feat(editor): Embed Studio button in the top bar
Adds a chart button to the TopBar that opens the Embed Studio. Smart entry point: when charts already exist it opens the studio on one of them (prefers a real chart over the always-present banner) without mutating the document; only when there is no chart does it fall back to inserting a fresh one, mirroring the slash menu's "New Chart" flow. Both paths route through the existing `open-embed-studio` event so session/key handling stays in one place. Co-authored-by: Cursor <cursoragent@cursor.com>2dfb335
-
feat(auth): surface canEdit in the topbar with an access badge
`/api/auth/status` already returned a `canEdit` flag derived from the HF whoami roleInOrg (Space owner, or org write/admin/contributor with a write resource group), but the frontend was ignoring it. Result: read-only users saw an enabled Publish button + the regular user chip, then hit a confusing 403 from the backend on every mutating action. - App.tsx plumbs `canEdit` from /api/auth/status into a new state and forwards it to TopBar. - TopBar renders a small badge next to the user chip: "Editor" (green outline + PencilLine icon) when canEdit, "Read-only" (muted + Lock icon) otherwise, both with a tooltip explaining the state. - Publish button is disabled when !canEdit with a matching tooltip, so the read-only restriction is visible up front instead of after a failed network round-trip. - `.chip--editor` and `.chip--readonly` variants added to _ui.css. Co-authored-by: Cursor <cursoragent@cursor.com>0e7d2a3
-
feat(storage): first-class data - no silent failures in the persistence pipeline
The "Saved" label in the top bar used to be a comforting lie: several failure modes (createRepo 403 on a missing manage-repos scope, writeFileSync on a readonly FS, uploadFile mid-debounce network blip) all collapsed into a console.error that the editor never surfaced. A user could type for an hour, see "Saved" the whole time, lose their tab, and discover only after the container rebuilt that nothing had ever made it to the dataset. Make every link in the chain observable, and make the worst signal always win in the UI. Backend ------- - In-memory `StorageStatus` tracker in hf-storage.ts: - `datasetReady` (createRepo succeeded or returned 409) - `lastLocalSaveAt` / `lastCloudPushAt` epoch ms - `pendingPush` (a debounce timer is armed) - `lastError {stage, message, statusCode, at, docName}` where `stage` is one of `dataset-create | local-save | cloud-push` Every write path records success; every error path records the failure. Success on one stage clears the error for …7a42df5
-
feat(editor): show connected collaborators in the top bar
The collab cursors already render inline next to each peer's selection, but if no one's actively editing right now you have no way to see who else is in the room. Surface that in the top bar as an overlapping row of small avatars (24px circles, -6px overlap, max 4 visible + "+N" badge for the tail), placed just before the SyncIndicator. Data source: the same Y.js awareness states that CollaborationC ursorV3 already populates with \`{ name, color, avatarUrl }\` for every client. We listen to awareness "change" events and re-derive the list on each tick, excluding our own clientID so the user doesn't show up to themselves (they already see their own chip on the far right). Implementation notes: - Border color of each avatar mirrors the user's collab color (same hue as their cursor caret in the doc) so it's obvious at a glance which avatar maps to which inline cursor. - Fallback to a colored initial when avatarUrl is missing (Anonymous sessions before HF OAuth resolves). - Sort by…aa09c01 -
fix(editor): SyncIndicator now reflects hero/settings/citation edits
The "Saving..." / "Saved" badge in the topbar used to listen only to TipTap's editor.on('update') event. That event fires solely for prosemirror document changes - i.e. when the article BODY is edited. The hero (title/subtitle/authors/links/...), the per-article settings, citations, embeds and the comment store all live in sibling Y.Maps on the same Yjs document. Edits there don't round-trip through TipTap, so they never triggered the indicator. Net effect for users: editing the hero felt like a no-op - no "Saving..." feedback - and the natural conclusion was "this isn't being saved", even though the backend's Hocuspocus onChange handler was happily persisting the full ydoc on every Yjs update. Subscribe one layer lower, on ydoc.on('update', ...). That callback fires for any Y.Map / Y.Array / Y.XmlFragment change in the document, which exactly mirrors what the backend persists. The indicator now flashes "Saving..." for every kind of edit and settles back to "Saved" once the write deb…adb1aa1 -
feat(auth): sign-out button in the editor topbar
Wraps the user chip in the topbar with a dropdown that exposes a "Sign out" action. Matches the visual + interaction pattern of the adjacent "More actions" menu so the two feel like a coherent toolbar element rather than a one-off. Backend (routes/auth.ts + auth.ts): - New POST /api/auth/logout (with a GET alias for ops/debug). handleOAuthLogout clears the hf_access_token cookie, mirroring every attribute the original res.cookie(...) used in handleOAuthCallback - browsers only delete a cookie if the new "expired" Set-Cookie matches path/secure/sameSite, otherwise we end up shipping two cookies and the session sticks. - POST is the canonical method so the call can't be hijacked by a cross-site <img>/<form> request; GET stays available for curl debugging and incident response. Frontend (TopBar.tsx): - Chip becomes a <button> with aria-haspopup="menu" that toggles an account menu. Color/avatar/name unchanged - same visual. - Dedicated state + ref (accountMenu…d10f68e -
fix(editor): SyncIndicator was stuck on "Disconnected" forever
The <Editor> creates the HocuspocusProvider lazily and assigns it to providerRef.current AFTER the surrounding components have mounted. <SyncIndicator> was wired with \`useEffect(..., [providerRef])\` and early-returned when providerRef.current was null. Because a ref doesn't trigger re-renders, the effect never re-ran once the provider arrived, so no listeners were attached and the status stayed "disconnected" regardless of the actual WebSocket state. The bug was always latent but became visible now that org-owned private Spaces actually get a working WS connection - the auth fix in the previous commit just exposed it. Replace the dependency-only effect with a small polling bootstrap: - Poll providerRef.current every 100ms until it lands. - Once we have the provider, attach connect/disconnect/synced listeners and seed the initial status from configuration.websocketProvider.status. - Add a 1s reconciliation interval that re-reads websocketProvider.status while attached. Some Ho…50f7a20 -
feat(embed-studio): clickable chart list to switch files
The sidebar already lists every chart in the doc but only the current one was highlighted - the others were inert <li>s. Make non-current entries clickable so the user can jump between charts without going back to the article and reopening another embed. - FilesSidebar: optional onSelectChart prop. Non-current items render as a <button class="es-files__item-main"> (same look as the clickable data files), the current item stays a plain <li> with the "editing" badge. - EmbedStudio: forwards onSelectChart to the sidebar. - App: rotates embedStudioSession when switching, so the studio remounts and useEmbedChat's mount-only loadMessages picks up the chat history persisted for the newly-selected file. - CSS: existing .es-files__item--data { padding: 0 } pattern extended via :has() to any .es-files__item that contains a button child, so the <li> padding doesn't stack with the button's own padding. Co-authored-by: Cursor <cursoragent@cursor.com>79008fb -
fix(embed-studio): contextual empty state and input placeholder
The chat panel always said "Describe the chart you want to create" even when a chart was already rendered in the preview pane next to it - confusing because the obvious next action is to ask for tweaks, not to recreate from scratch. Branch the empty state and the textarea placeholder on three situations: - no chart yet (scratch): "Describe the chart you want to create" + the original example prompt - existing chart: "Ask for changes to the chart" + a tweak example - banner mode: phrase both as "banner" instead of "chart" so the hint matches the banner system prompt's mood-setter intent Co-authored-by: Cursor <cursoragent@cursor.com>56a91d6 -
fix(embed): SVG text now follows the iframe theme
Three stacked bugs were keeping the chart text black in dark mode: 1. EmbedStudio mounted both preview iframes with sandbox="allow-scripts" only (no allow-same-origin). The bootstrap script couldn't reach window.parent.document, so the cascade fell through localStorage to matchMedia(prefers-color-scheme) - which returns the OS preference, not the app theme. Result: the iframe could land on a different theme than the article. Aligned the sandbox with HtmlEmbedView ("allow-scripts allow-same-origin"). 2. The THEME_BOOTSTRAP in shared/embed-doc.ts always overwrote the srcdoc-provided data-theme attribute, even when every detection path failed. Honour the existing data-theme before falling back to matchMedia so an explicit isDark from buildEmbedSrcdoc wins. 3. SVG text/line/path don't inherit CSS color the way HTML elements do - their default fill/stroke is "black", not "currentColor". Charts that scoped colours via `body { color: var(--text-color) }` …be7d6fa -
feat(embed-studio): make agent-generated charts theme-aware
The iframe already supports live light/dark swap via CSS variables and a postMessage(setTheme) hot-swap, but the LLM kept generating charts that hardcode colours into SVG attributes (fill="#333", stroke="black", ...). Those values can't be overridden by the parent's data-theme attribute, so the chart looked correct on whatever theme the model guessed and broken on the other one. Two complementary fixes: 1. Forward the current theme (light/dark) from EmbedStudio through useEmbedChat into the request context, and surface it in the system prompt as a "Current theme" section. The model now knows which background it's painting against and can self-correct. 2. Rewrite the "Colors & theming" section of the embed system prompt with a concrete CSS pattern (scoped .d3-* rules driving fill/stroke via var(--text-color), --axis-color, --grid-color, ...) and an explicit ban on hardcoded SVG colour attributes. Add a hint about window.__chartRerender for JS-driven palettes. No…27abbe5
-
fix(publish-dialog): show Space URL instead of /published/<doc> path
The success state used to display `Live at /published/default` and link to `/published/default/index.html`, surfacing an implementation detail of the publisher pipeline (used internally for PDF rendering and og:image) rather than the canonical URL readers actually visit. When a published article exists, the backend serves it at the Space root (`app.get("*")` in `create-app.ts`). Switch the modal to surface `window.location.origin` instead, both for the displayed URL and the "View published article" link target. Made-with: Cursor8d2864f
-
feat(demo): full showcase polish - mouse cursor, robust selection, HF affiliations
Demo orchestration: - Visible mouse cursor overlay (popover top-layer, MutationObserver to stay above native dialogs) + humanMove/humanClick/humanHover helpers - Yjs Node.js clients for Bob and Carol with awareness cursors and a set of high-level helpers (writeBlockMathAtSlot, setAffiliationUrl, waitForAffiliationCount, etc.) so peer interactions never collide - Pre-bold sync barrier: Alice waits for Bob's block math and Carol's code block to land before doing the bubble-toolbar bold so layout is frozen during the drag selection (no more cross-paragraph selections) - Progressive cursor-driven text selection (visible glide + step-wise setTextSelection) for bold and rephrase actions - Custom rAF-eased scroll-to-top on the actual .editor-scroll container before the hue drag so the banner retint reads on camera Embed Studio: - New architecture.json companion data file (transformer-block layers) seeded alongside model-accuracy.csv, so the agent has something concrete to r…f469961
-
feat(editor): embed studio with data files and agent-aware editing
Editor - New Embed Studio panel for AI-assisted chart creation: upload and manage data files (3MB limit), browse them in a dedicated viewer, and iterate on the generated HTML via a chat wired to a dedicated embed agent (embed-chat + embed-system-prompt + embed-tools + tiptap-catalog) that can create, rename and edit embeds. - Tiptap extensions for agent-driven editing: - agent-focus: Yjs-awareness ranges so every peer sees where another user's AI is working ("<Name> agent" label). - agent-highlight: local PM decoration to mark the paragraph the agent is currently rewriting. - agent-rewrite: typewriter-animated range rewrite command used by the rephrase flow. - Rephrase flow now goes through agent-rewrite (typewriter) instead of a plain replace; bold demonstration moved to the floating bubble toolbar above a selection. - SlashMenu reorganised with grouping and dev-only items dropped. - New components: DataFileViewer, FilesSidebar, UploadZone. - …8fc8501
-
feat(frontend): editor refresh (embed studio, comment popover, shiki, top bar, hooks, styles)
- New top bar + publish dialog + sync indicator + mobile TOC sidebar replace the previous floating actions and full sidebar stack - Add EmbedStudio chat panel wired to the backend embed agent (embed-tools + stream handler), with fullBleed iframe mode for banner rendering and double-buffered previews - Swap legacy comments sidebar for a floating CommentPopover anchored to the active thread - Shiki-based code block extension + scroll guard on top of TipTap v3; richer frontmatter settings (hue slider, template variant, SEO) and Editor footer - Shared hooks (useTheme, useEmbedChat, usePublishStatus) and a singleton MutationObserver so all components subscribe to theme changes from the same source - Reorganize CSS into scoped modules (_ui, _footer, _embed, _embed-studio, _hero, _mermaid, _toc, _form, _code, tokens, layout, publisher, print) and drop the dead postcss config - Add frontend vitest suite + tsconfig(+node) + vite-env typings + typed utils/types modules Made-with: Curs…76fc93a
-
refactor: remove MUI, unify publisher pipeline, add shared component registry
Phase 1 - Publisher pipeline: - Extract ~370 lines of inline CSS from html-renderer.ts into _publisher.css - Create shared/component-defs.ts as single source of truth for component definitions - Replace 6 regex blocks in postProcess() with linkedom DOM manipulation - Add GET /api/preview/:docName for testing renders without publishing - Add 12 snapshot tests for each postProcess transformation (46 tests total) Phase 2 - Remove MUI: - Replace all MUI components with native HTML elements + custom CSS - Create _ui.css with editor chrome styles using --ed-* CSS tokens - Create Tooltip.tsx using Floating UI (lightweight replacement) - Replace @mui/icons-material with lucide-react - Delete theme.ts, remove @mui/* and @emotion/* dependencies Phase 3 - Documentation: - Add docs/ARCHITECTURE.md covering CSS layers, HF Spaces constraints, shared registry, publisher pipeline, and test coverage Made-with: Cursord15d7f7 -
feat: show real HF username and avatar for connected users
- Fetch /api/auth/status on mount to get HF user info (name, avatar) - Replace random names with actual HF fullName and profile picture - Update collaboration cursor to display avatar next to username - Update top bar Chip with avatar component - Sync awareness when user data arrives after initial render - Use deterministic color from username hash instead of random Made-with: Cursor15dff0c -
fix: align published article with template design
Theme toggle: - Replace emoji button with SVG sun/moon icons from ThemeToggle.astro - Fix id mismatch (class="theme-toggle" -> id="theme-toggle") - Add spin animation and system theme detection Hero: - Add .hero-title class to h1 so clamp font-size styles apply DOM structure: - Change <article> to <main> in content-grid to match template selectors (.content-grid main) used across _base.css, _layout.css, _table.css for typography, figures, references Component CSS: - Add styles for note, quoteBlock, sidenote, stack, reference, wide, fullWidth components (div[data-component="..."]) Code highlighting: - Load highlight.js runtime + call hljs.highlightAll() for syntax coloring in published code blocks Performance: - Debounce CommentsSidebar position updates and TOC heading extraction Made-with: Cursor0094189 -
feat: adopt template CSS architecture, hero/TOC/color alignment
- Copy template foundation CSS (_variables, _reset, _base, _layout, _print) and component CSS (_code, _table, _tag, _card, _mermaid) - Extract hero styles from HeroArticle.astro into shared _hero.css - Extract TOC styles from TableOfContents.astro into shared _toc.css - Create _editor-tokens.css for editor-specific design tokens - Scope _reset.css and _base.css global styles to article area to avoid overriding MUI portals (Dialog, Menu, Tooltip) - Exclude _button.css and _form.css from editor (bare element selectors conflict with MUI components) - Align FrontmatterHero class names with template (.hero, .meta, .meta-container-cell, .authors, .affiliations) - Align publisher HTML renderer class names accordingly - Fix TOC sticky positioning (align-self:stretch + sticky wrapper) - Add hue-based color picker (OKLCH slider like template ColorPicker) synced via Yjs settings, driving both MUI theme and CSS variables - Align editor link styles with template (--primary-color) - Add Po…9b7d64e -
refactor: rename to Science Article Template Editor, remove MDX export, fix settings drawer
- Rename project from "Collab Editor" to "Science Article Template Editor" across README, package.json, HTML titles, and docs - Remove MDX export: delete to-mdx.ts, serializers, UI button, and registerComponentSerializers; use editor.getText() for agent context - Settings drawer: flatten Advanced accordion, fix toggle clipping (ml: 0), widen to 380px, use slotProps.paper (MUI v9) Made-with: Cursor20354ec
-
chore: initial commit
Collaborative article editor with TipTap, Yjs, and HF integration. Features: - Real-time collaborative editing (Hocuspocus + Yjs) - Notion-like block handles, slash commands, bubble toolbar - Academic citations (Citation.js + DOI/BibTeX) - KaTeX math, code highlighting, tables - Custom component system (accordion, note, quote, stack, etc.) - LLM agent sidebar (OpenRouter) - Image upload with HF dataset storage - MDX export - Frontmatter management (title, authors, affiliations) - Comments system Made-with: Cursor561e6f0