The toggleFavorite() function body was missing its closing },
causing togglePublish(){...} to be parsed inside toggleFavorite()
body. The { at togglePublish() column was the 'Unexpected token {'
SyntaxError that prevented ALL JavaScript from executing.
Root cause of ALL Alpine 'is not defined' errors since the refactor.
Root cause: x-data HTML attribute with tojson creates quote conflicts
that survive even Cache-Control: no-store (likely nginx proxy cache).
Solution: page data is now in a <script type=application/json> tag,
completely decoupled from Alpine x-data HTML attribute. Zero quoting
issues regardless of page content.
- x-data="editorState()" + x-init="loadPage()" replaces editor(...)
- Server passes page_data dict, template renders as JSON script tag
- Same approach used for page_share_mode and page_published
- All 73 tests pass
The @input handler on the title called autoSave() which wasn't in
scope. Changed to save() which is available on the Alpine x-data object.
The debounce is already handled internally.
tojson outputs JSON with double quotes, which clash with x-data="..."
HTML attribute delimiter. Changed x-data delimiters to single quotes
so JSON double quotes are contained properly within the attribute.
x-data editor() arguments used manual |e escaping with single quotes.
Page content containing newlines, quotes or backslashes broke the
JavaScript string literal. Now uses |tojson filter for all fields
(proper JSON escaping).
- DB: pages.share_mode (private/invited/anyone) + published (bool)
- API: POST /board/api/share/{id} saves share settings to DB
- view_page: passes page_share_mode + page_published to template
- page_editor: inits share state from server, auto-saves on change
- Clipboard: fallback to textarea for HTTP (navigator.clipboard blocked)
- Toast: showToast() with proper timeout clearing
Jinja2 tried to evaluate {{ a.email[0].toUpperCase() }} inside Alpine.js
x-for template, causing UndefinedError. Wrapped in {% raw %} block.
Also changed avatars to use x-text binding instead of {{ }} interpolation.
Share panel restructured to match Notion:
- General Access: dropdown with 'Only people invited' / 'Anyone with the link'
- Invite: email + permission selector
- Publish to web: toggle + URL input + Copy link button
- Copy page link at bottom
CSS: .share-access-btn and .share-access-menu for the access dropdown
- base.html topbar wrapped in {% block topbar %} for override
- page_editor.html overrides topbar block with empty content (uses its own)
- Get Started toolbar: fixed to bottom center with gradient fade
- No more double Share/Link/Star buttons
Replicates Notion's page layout from reference images:
- Top bar: Private badge, Share button, Link copy, Star/favorite toggle, ⋮ menu
- Share modal: Publish toggle, Copy link, Invite people with permission select
- Empty page toolbar: Get started with H1/H2/H3/bullet/todo/callout/quote
- Star toggle integrated with favorites API (POST/DELETE /board/api/favorites)
- board.py view_page now passes workspace context + page_favorited
- dashboard.py view_page_root also passes page_favorited
- CSS: .page-topbar, .share-modal, .empty-page-toolbar, toggle switch, more menu
- Sidebar: 7 collapsible sections (Meetings, Recents, Favorites, Agents, Shared, Private, Notion apps)
- Nav icon row: Home with text + icon, Chat/Inbox/Search icon-only, collapse on right
- Notion apps: Library, My Tasks, Trash, Help
- Footer: New chat Ctrl+O + New page buttons
- Auto-update: sidebar items reflect title changes immediately on save
- CSS: nav-icon-btn, home-btn, collapse-btn, empty-hint, outline none
- sidebar items: attribut data-page-id ajouté pour ciblage DOM
- save(): après PUT réussi, met à jour .page-name dans le sidebar
- saveTitle(): reload la page (changement de titre = reload ok)
- CSRF middleware: excluded_paths utilise maintenant startswith (pas exact match)
- /board/api/pages/* exempté — les PUT depuis page_editor passent
- saveTitle() reload la page après sauvegarde → sidebar mis à jour