Skip to content

Re-implement client-side JS#19

Merged
simonbc merged 13 commits into
mainfrom
m9-js
May 16, 2026
Merged

Re-implement client-side JS#19
simonbc merged 13 commits into
mainfrom
m9-js

Conversation

@simonbc
Copy link
Copy Markdown
Owner

@simonbc simonbc commented May 16, 2026

Summary

Bring back the interactive features that the 2007 site had — as vanilla "islands": small, self-mounting JS files that bail gracefully when their target nodes aren't present. No framework, no bundler, served raw.

Sub-steps (one commit each):

  • new-page toggle + relative-time ticker (datestr)
  • drafts (localStorage autosave + restore banner)
  • live markdown preview in edit page
  • settings autosave
  • design page (hue/brightness sliders + color picker)

Note: 2007's `dragdrop.js` was only a YUI Slider dependency — there was no page-reordering feature. Dropped from the plan.

Test plan

  • 329 tests passing
  • Visit a site — sidebar "Create a new page" button toggles
  • View a page — dateline ticks ("changed 0 seconds ago" → "1 minute ago")
  • /history — each revision row shows relative time
  • JS off — page-creation form is already expanded, dates fall back to UTC strings

🤖 Generated with Claude Code

simonbc and others added 13 commits May 16, 2026 17:09
Two self-mounting vanilla islands:
- new-page.js: collapses the sidebar "Create a new page" button to a
  name-input form on click; close link collapses it back. With JS off
  the form starts expanded (the [hidden] attr is the JS-on state) so
  page creation still works.
- datestr.js: re-renders <time class="js-datestr"> as "3 minutes ago"
  / "2 days ago" / month-day for older, ticking the cadence to match
  the displayed unit (10s, 1m, 1h, static ≥4 days).

site_base.html loads both via {% block scripts %}; per-page templates
can extend the block with {{ super() }} for their own islands. view,
history, and changes mark their <time> elements with .js-datestr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
drafts.js: debounce textarea input by 1s, write the content into
localStorage under jottit-draft:${pathname}. On load, if the stored
draft differs from the server content (data-original-content), show
a "You have an unsaved draft" banner with Restore / Discard buttons.
Successful submit clears the draft; entries older than 30 days are
pruned on load.

No DB schema change, no new routes — the draft is purely on-device
so it survives navigation, tab close, and session expiry, but never
leaves the browser.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
preview.js: 150ms debounced re-render of the preview pane via
marked.js (self-hosted at /static/js/vendor/marked.min.js, v13.0.3).
The server-side mistune render still owns the saved output — minor
client/server render drift is accepted for live preview only.

edit_page.html grows a .edit-split flexbox holding the textarea and
a #preview pane; ≥768px puts them side-by-side, mobile stacks. The
preview hides itself via :empty until marked.js has rendered, so
no-JS clients get a clean full-width textarea.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The edit page now skips site_base.html and extends base.html directly
with body.edit, so the claim banner, header, sidebar, and content
footer don't render. The form fills the viewport: edge-to-edge
textarea on the left, live preview on the right (≥768px), action bar
pinned to the bottom with "Publish or Cancel". "Formatting help" sits
absolute top-right.

Draft banner re-copies to match the 2007 idiom: "Here's your draft
back. If you don't want it, revert to the live version" — drafts.js
auto-restores on load and the revert link wipes the localStorage
draft and reloads the server content into the textarea.

Adds caret.js — on desktop, focus the textarea on enter and restore
the saved selectionStart + scrollTop from the pages.caret_pos /
scroll_pos columns; on blur and submit, capture the current position
into hidden inputs so the server stores it for next time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The textarea's width: 100% and the preview's missing min-width: 0
combined with cols=80 forced the textarea to claim the full row,
squashing the preview to a sliver. Both panes now use flex: 1 1 0
with min-width: 0 so they actually share the row.

Drops the .edit-preview:empty { display: none } rule — it flickered
the layout whenever the textarea briefly went empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
body.edit becomes a height:100svh flex column with overflow:hidden;
#edit_form is flex:1 1 auto with its own flex column; the .edit-split
inside expands to fill the remaining space. Result: the textarea and
preview reach down to the action bar, no whitespace below.

Also move "Formatting help" inside .edit-split (position: relative)
so it anchors to the top-right of the editor row rather than the
body — the draft banner no longer overlaps it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add 8px inline padding to .edit-split and .edit-actions so the
textarea, preview, and Publish/Cancel row don't sit flush against
the viewport edges (the draft banner still spans full-width — it
breaks out by virtue of being outside both wrappers).

Add a 20px top reservation to .edit-split so "Formatting help" gets
its own row above the preview pane instead of sitting on top of the
preview's border.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design already lives in the site header (recent changes | settings |
design), so it's redundant at the bottom. 2007 jottit's settings
page didn't carry these links — match that. Export remains reachable
via /admin/export directly; a real link can come back later when we
decide where it lives.

Also pulls in the formatting-help vertical breathing room, the
draft-banner / publish-row inline padding, and the settings.js +
ruff-formatted test_draft.py changes from the same working set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the design form to proper HTML5 inputs: <input type="color">
for header / title / subtitle colors, <input type="range"> with an
<output> mirror for sizes and hue / brightness, and a <datalist> of
common font stacks for the four font fields.

design.js binds each control to its CSS custom property on :root —
typing a color or sliding a range re-themes the site chrome
immediately, no round-trip. The server still owns persistence on
submit and re-renders the saved values on the next load via
partials/design_style.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@simonbc simonbc merged commit 07bbada into main May 16, 2026
1 check passed
@simonbc simonbc deleted the m9-js branch May 16, 2026 19:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant