diff --git a/jottit/__init__.py b/jottit/__init__.py index 6cb1e43..e8e0036 100644 --- a/jottit/__init__.py +++ b/jottit/__init__.py @@ -10,6 +10,7 @@ from jottit.blueprints.root import root_bp from jottit.blueprints.secret import secret_bp from jottit.blueprints.site import site_bp +from jottit.chrome import chrome_context from jottit.db import close_request_conn, make_engine from jottit.site_resolver import resolve_site @@ -32,5 +33,6 @@ def create_app() -> Flask: app.before_request(resolve_site) app.teardown_request(close_request_conn) + app.context_processor(chrome_context) return app diff --git a/jottit/chrome.py b/jottit/chrome.py new file mode 100644 index 0000000..dea5464 --- /dev/null +++ b/jottit/chrome.py @@ -0,0 +1,48 @@ +"""Shared template context: site, page list, signin state, and design row. + +Wired into the Flask app as a `context_processor` so every render_template +call sees these variables without each view having to pass them. +""" + +from __future__ import annotations + +from typing import Any + +from flask import g, request + +from jottit import auth +from jottit.db import get_design, get_request_conn, list_pages +from jottit.urls import page_slug, site_root + + +def chrome_context() -> dict[str, Any]: + """Variables every page template can rely on. + + `site` and `pages` are `None` on apex-domain routes (the front page); + templates should branch on `site` before reading from it. + """ + site = getattr(g, "site", None) + if site is None: + return { + "site": None, + "pages": [], + "design": None, + "is_signed_in": False, + "is_unclaimed": False, + "site_root_path": "/", + "page_slug": page_slug, + } + + conn = get_request_conn() + pages = list_pages(conn, site_id=site.id) if conn is not None else [] + design = get_design(conn, site_id=site.id) if conn is not None else None + return { + "site": site, + "pages": pages, + "design": design, + "is_signed_in": auth.is_signed_in_to(site.id), + "is_unclaimed": site.password is None, + "site_root_path": site_root(), + "page_slug": page_slug, + "current_path": request.path, + } diff --git a/jottit/db.py b/jottit/db.py index eccfe0e..cfa9cf2 100644 --- a/jottit/db.py +++ b/jottit/db.py @@ -635,6 +635,19 @@ def update_design(conn: Connection, *, site_id: int, **fields: object) -> None: # ---- Export ---- +def list_pages(conn: Connection, *, site_id: int) -> list[Row]: + """List a site's non-deleted pages, alphabetically by name. + + Used by the chrome sidebar to render a navigation list. Home (the + empty-name page) sorts first. + """ + rows = conn.execute( + select(pages.c.name).where(pages.c.site_id == site_id, pages.c.deleted.is_(False)) + ).all() + # Empty-name (home) goes to the top; remaining pages sort case-insensitively. + return sorted(rows, key=lambda r: (r.name != "", r.name.lower())) + + def get_pages_for_export(conn: Connection, *, site_id: int) -> list[Row]: """Latest non-deleted pages with their latest revision content, for export. @@ -704,13 +717,17 @@ def new_site( .returning(sites.c.id) ).scalar_one() + # `system-ui` resolves to whatever the visitor's OS uses for UI text + # (San Francisco on macOS, Segoe UI on Windows, etc); `sans-serif` is + # the fallback. Replaces the 2007 default of "Lucida_Grande". + default_font = "system-ui, sans-serif" conn.execute( insert(designs).values( site_id=site_id, - title_font="Lucida_Grande", - subtitle_font="Lucida_Grande", - headings_font="Lucida_Grande", - content_font="Lucida_Grande", + title_font=default_font, + subtitle_font=default_font, + headings_font=default_font, + content_font=default_font, header_color=scheme.header_color, title_color=scheme.title_color, subtitle_color=scheme.subtitle_color, diff --git a/jottit/static/base.css b/jottit/static/base.css new file mode 100644 index 0000000..eff48d2 --- /dev/null +++ b/jottit/static/base.css @@ -0,0 +1,104 @@ +html { + font-family: var(--content-font); + font-size: var(--content-size); + line-height: 1.5; + color: var(--fg); + background: var(--bg); +} + +body { + min-height: 100vh; +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--headings-font); + line-height: 1.2; + margin-top: var(--space-5); + margin-bottom: var(--space-3); +} + +h1 { font-size: var(--headings-size); } +h2 { font-size: calc(var(--headings-size) * 0.85); } +h3 { font-size: calc(var(--headings-size) * 0.72); } + +p, ul, ol, dl, blockquote, pre, table, figure { + margin-top: 0; + margin-bottom: var(--space-4); +} + +ol, ul { + padding-left: var(--space-5); + list-style: revert; +} + +a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 2px; +} + +a:visited { color: var(--link-visited); } +a:hover, a:focus { text-decoration-thickness: 2px; } + +button { + cursor: pointer; + padding: var(--space-2) var(--space-4); + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); +} + +button:hover { background: var(--surface-alt); } + +input[type="text"], +input[type="password"], +input[type="email"], +input[type="number"], +textarea { + width: 100%; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--surface); +} + +textarea { font-family: var(--content-font); resize: vertical; min-height: 12rem; } + +label { display: block; } + +fieldset { + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--space-3) var(--space-4); + margin-bottom: var(--space-4); +} + +legend { padding: 0 var(--space-2); font-weight: 600; } + +hr { + border: 0; + border-top: 1px solid var(--border); + margin: var(--space-5) 0; +} + +code, pre, kbd, samp { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.95em; +} + +pre { + padding: var(--space-3); + background: var(--surface-alt); + border-radius: var(--radius); + overflow-x: auto; +} + +blockquote { + border-left: 3px solid var(--border); + padding-left: var(--space-4); + color: var(--muted); +} + +small { color: var(--muted); } + +time { color: var(--muted); } diff --git a/jottit/static/components.css b/jottit/static/components.css new file mode 100644 index 0000000..13f6d4f --- /dev/null +++ b/jottit/static/components.css @@ -0,0 +1,238 @@ +.page { + max-width: 60rem; + margin: 0 auto; + padding: var(--space-4); + display: grid; + grid-template-columns: 1fr; + gap: var(--space-5); +} + +@media (min-width: 50rem) { + .page { + grid-template-columns: minmax(0, 1fr) 16rem; + grid-template-areas: + "header header" + "main sidebar" + "footer footer"; + } + .site-header { grid-area: header; } + main { grid-area: main; } + .site-sidebar { grid-area: sidebar; } + .site-footer { grid-area: footer; } +} + +.site-header { + background: var(--header-color); + color: var(--title-color); + padding: var(--space-5) var(--space-5) var(--space-3); + border-radius: var(--radius); +} + +.site-header a { color: var(--title-color); } + +.site-header hgroup h1 { + font-family: var(--title-font); + font-size: var(--title-size); + margin: 0; +} + +.site-header .subtitle { + font-family: var(--subtitle-font); + font-size: var(--subtitle-size); + color: var(--subtitle-color); + margin: var(--space-1) 0 0; +} + +.claim-banner { + background: rgba(255,255,255,0.15); + padding: var(--space-2) var(--space-3); + margin: 0 0 var(--space-3); + border-radius: var(--radius); +} + +.site-nav { + margin-top: var(--space-3); +} + +.site-nav ul { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + padding: 0; + list-style: none; + margin: 0; +} + +.site-nav a, +.site-nav button.link { + color: var(--subtitle-color); + text-decoration: none; + font-size: 0.9em; +} + +.site-nav a:hover, +.site-nav button.link:hover { text-decoration: underline; } + +.site-sidebar { + border-left: 1px solid var(--border); + padding-left: var(--space-4); +} + +@media (max-width: 50rem) { + .site-sidebar { + border-left: 0; + border-top: 1px solid var(--border); + padding-left: 0; + padding-top: var(--space-4); + } +} + +.page-list { + list-style: none; + padding: 0; + margin: 0 0 var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.page-list strong { color: var(--fg); } + +.new-page { + display: flex; + gap: var(--space-2); +} + +.new-page input[type="text"] { flex: 1; } + +.site-footer { + text-align: center; + color: var(--muted); + font-size: 0.9em; + padding-top: var(--space-4); + border-top: 1px solid var(--border); +} + +main > article { + max-width: var(--measure); +} + +.front .lede { + font-size: 1.2em; + color: var(--muted); + margin-bottom: var(--space-5); +} + +.new-site, +.site-form, +.form { + display: flex; + flex-direction: column; + gap: var(--space-3); + max-width: var(--measure); +} + +.form-error { + background: var(--danger-bg); + color: var(--danger); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius); + border: 1px solid var(--danger); +} + +button.destructive { + background: var(--danger); + color: #fff; + border-color: var(--danger); +} + +button.destructive:hover { filter: brightness(0.9); } + +button.link { + background: none; + border: 0; + padding: 0; + color: var(--link); + text-decoration: underline; + cursor: pointer; +} + +.form.inline, +form.inline { display: inline; } + +.revision-banner { + background: var(--surface-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: var(--space-3); + margin-bottom: var(--space-4); + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + align-items: center; +} + +.page-meta { + color: var(--muted); + font-size: 0.9em; + margin-bottom: var(--space-3); + display: flex; + flex-wrap: wrap; + gap: var(--space-3); +} + +.admin .admin-links { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + margin-bottom: var(--space-4); +} + +.revisions { + list-style: none; + padding: 0; + margin: 0 0 var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.revisions li { + padding: var(--space-2) 0; + border-bottom: 1px solid var(--border); + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + align-items: baseline; +} + +.revisions .changes { + color: var(--muted); + font-size: 0.9em; +} + +.pagination { + margin-top: var(--space-4); +} + +.diff-body ins { + background: #e6ffed; + text-decoration: none; +} + +.diff-body del { + background: #ffebe9; + text-decoration: line-through; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + margin-top: var(--space-4); +} + +.hint { + color: var(--muted); + font-size: 0.9em; +} diff --git a/jottit/static/reset.css b/jottit/static/reset.css new file mode 100644 index 0000000..98538e2 --- /dev/null +++ b/jottit/static/reset.css @@ -0,0 +1,17 @@ +*, *::before, *::after { box-sizing: border-box; } + +html, body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ol, ul, fieldset { + margin: 0; + padding: 0; +} + +ol, ul { list-style: none; } + +img, picture, svg, video { max-width: 100%; display: block; } + +button, input, select, textarea { + font: inherit; + color: inherit; +} + +a { color: inherit; } diff --git a/jottit/static/tokens.css b/jottit/static/tokens.css new file mode 100644 index 0000000..105fb69 --- /dev/null +++ b/jottit/static/tokens.css @@ -0,0 +1,40 @@ +:root { + --hue: 210; + --brightness: 100; + + --header-color: hsl(var(--hue) 60% calc(var(--brightness) * 0.22%)); + --title-color: #fff; + --subtitle-color: #bfe8ff; + + --bg: hsl(var(--hue) 30% calc(var(--brightness) * 0.99%)); + --fg: #222; + --muted: #666; + --link: #06c; + --link-visited: #639; + --border: #ddd; + --surface: #fff; + --surface-alt: #f7f7f7; + --danger: #b00020; + --danger-bg: #fdecea; + + --title-font: system-ui, sans-serif; + --subtitle-font: system-ui, sans-serif; + --headings-font: system-ui, sans-serif; + --content-font: system-ui, sans-serif; + + --title-size: 200%; + --subtitle-size: 100%; + --headings-size: 150%; + --content-size: 100%; + + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-8: 3rem; + + --radius: 4px; + --measure: 38rem; +} diff --git a/jottit/static/utilities.css b/jottit/static/utilities.css new file mode 100644 index 0000000..19a3c90 --- /dev/null +++ b/jottit/static/utilities.css @@ -0,0 +1,19 @@ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +.deleted { color: var(--muted); } + +.empty { color: var(--muted); } + +.cancel { color: var(--muted); } + +.count { color: var(--muted); } diff --git a/jottit/templates/admin_change_password.html b/jottit/templates/admin_change_password.html index ca6c8e6..4895e5a 100644 --- a/jottit/templates/admin_change_password.html +++ b/jottit/templates/admin_change_password.html @@ -1,25 +1,25 @@ - - - - - Change password - - -

Change password

- {% if error %}

{{ error }}

{% endif %} -
-

- -

-

- -

-

-
-

Back to settings

- - +{% extends "base.html" %} +{% block title %}Change password{% endblock %} +{% block content %} +
+

Change password

+ + {% include "partials/form_error.html" %} + +
+

+ +

+

+ +

+

+
+ +

Back to settings

+
+{% endblock %} diff --git a/jottit/templates/admin_change_site_address.html b/jottit/templates/admin_change_site_address.html index 3dc546e..0f12da7 100644 --- a/jottit/templates/admin_change_site_address.html +++ b/jottit/templates/admin_change_site_address.html @@ -1,21 +1,22 @@ - - - - - Change site address - - -

Change site address

- {% if error %}

{{ error }}

{% endif %} -
-

- - Letters, numbers, and dashes. Leave blank to remove the subdomain. -

-

-
-

Back to settings

- - +{% extends "base.html" %} +{% block title %}Change site address{% endblock %} +{% block content %} +
+

Change site address

+ + {% include "partials/form_error.html" %} + +
+

+ +

+

+
+ +

Back to settings

+
+{% endblock %} diff --git a/jottit/templates/admin_delete.html b/jottit/templates/admin_delete.html index 1bf449f..f550055 100644 --- a/jottit/templates/admin_delete.html +++ b/jottit/templates/admin_delete.html @@ -1,15 +1,14 @@ - - - - - Delete site - - -

Delete site

-

This will remove the site from public listings and free its subdomain. Pages and revisions stay on file in case we ever add a restore path.

-
-

-
-

Cancel

- - +{% extends "base.html" %} +{% block title %}Delete site{% endblock %} +{% block content %} +
+

Delete site

+

This will remove the site from public listings and free its subdomain. Pages and revisions stay on file in case we ever add a restore path.

+ +
+

+
+ +

Cancel

+
+{% endblock %} diff --git a/jottit/templates/admin_design.html b/jottit/templates/admin_design.html index 8e3c353..ae6312b 100644 --- a/jottit/templates/admin_design.html +++ b/jottit/templates/admin_design.html @@ -1,44 +1,44 @@ - - - - - Design - - -

Design

- {% if error %}

{{ error }}

{% endif %} -
-
- Fonts -

-

-

-

-
+{% extends "base.html" %} +{% block title %}Design{% endblock %} +{% block content %} +
+

Design

-
- Colors -

-

-

-
+ {% include "partials/form_error.html" %} -
- Sizes (percent) -

-

-

-

-
+ +
+ Fonts +

+

+

+

+
-
- Background -

-

-
+
+ Colors +

+

+

+
-

- -

Back to settings

- - +
+ Sizes (percent) +

+

+

+

+
+ +
+ Background +

+

+
+ +

+ + +

Back to settings

+
+{% endblock %} diff --git a/jottit/templates/admin_settings.html b/jottit/templates/admin_settings.html index 5049629..c9ce99d 100644 --- a/jottit/templates/admin_settings.html +++ b/jottit/templates/admin_settings.html @@ -1,37 +1,46 @@ - - - - - Settings - - -

Settings

- {% if error %}

{{ error }}

{% endif %} -
-

- -

-

- -

-

- -

-
- Security -

-

-

-
-

-
-

Site address: {{ public_url or "(none)" }}

-

Change password

- - +{% extends "base.html" %} +{% block title %}Settings{% endblock %} +{% block content %} +
+

Settings

+ + {% include "partials/form_error.html" %} + +
+

+ +

+

+ +

+

+ +

+ +
+ Security +

+

+

+
+ +

+
+ + +
+{% endblock %} diff --git a/jottit/templates/base.html b/jottit/templates/base.html new file mode 100644 index 0000000..b95f82c --- /dev/null +++ b/jottit/templates/base.html @@ -0,0 +1,40 @@ + + + + + + {% block title %}{% if site and site.title %}{{ site.title }}{% else %}Jottit{% endif %}{% endblock %} + {% if site and (site.security == "private" or not site.public_url) %} + + {% endif %} + + + + + + + {% block head %}{% endblock %} + {% if design %} + {% include "partials/design_style.html" %} + {% endif %} + + +
+ {% if site %} + {% include "partials/site_header.html" %} + {% endif %} + +
+ {% block content %}{% endblock %} +
+ + {% if site %} + {% include "partials/site_sidebar.html" %} + {% endif %} + + +
+ + diff --git a/jottit/templates/change_password.html b/jottit/templates/change_password.html index a16ca6b..d4019fa 100644 --- a/jottit/templates/change_password.html +++ b/jottit/templates/change_password.html @@ -1,20 +1,19 @@ - - - - - Set a new password - - -

Set a new password

- {% if error %}

{{ error }}

{% endif %} -
- -

- -

-

-
- - +{% extends "base.html" %} +{% block title %}Set a new password{% endblock %} +{% block content %} +
+

Set a new password

+ + {% include "partials/form_error.html" %} + +
+ +

+ +

+

+
+
+{% endblock %} diff --git a/jottit/templates/changes.html b/jottit/templates/changes.html index b01f799..123e8a9 100644 --- a/jottit/templates/changes.html +++ b/jottit/templates/changes.html @@ -1,30 +1,36 @@ - - - - - Recent changes - - -

Recent changes

- {% if changes %} - - {% if older_before %} -

Older →

+{% extends "base.html" %} +{% block title %}Recent changes{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+

Recent changes

+ + {% if changes %} +
    + {% for c in changes %} +
  1. + {{ c.page_name or "Home" }} — revision {{ c.revision }} + + {% if c.page_deleted %}(page since deleted){% endif %} + {% if c.changes %}{{ c.changes|safe }}{% endif %} +
  2. + {% endfor %} +
+ + {% if older_before %} +

Older →

+ {% endif %} + {% else %} +

No changes yet.

{% endif %} - {% else %} -

No changes yet.

- {% endif %} -

Back to site

- - + +

Back to site

+
+{% endblock %} diff --git a/jottit/templates/claim_site.html b/jottit/templates/claim_site.html index a7b2056..b9b4993 100644 --- a/jottit/templates/claim_site.html +++ b/jottit/templates/claim_site.html @@ -1,30 +1,32 @@ - - - - - Claim this site - - -

Claim this site

- {% if error %}

{{ error }}

{% endif %} -
-

- -

-

- -

-
- Security -

-

-

-
-

-
- - +{% extends "base.html" %} +{% block title %}Claim this site{% endblock %} +{% block content %} +
+

Claim this site

+

Set a password and pick how open you want this site to be. We'll email you a recovery link if you forget the password later.

+ + {% include "partials/form_error.html" %} + +
+

+ +

+

+ +

+ +
+ Security +

+

+

+
+ +

+
+
+{% endblock %} diff --git a/jottit/templates/deleted.html b/jottit/templates/deleted.html index 959f5ba..f99da8c 100644 --- a/jottit/templates/deleted.html +++ b/jottit/templates/deleted.html @@ -1,11 +1,14 @@ - - - - - Deleted - - -

This page has been deleted

-

“{{ page_name }}” is gone. Append ?r=N to view a specific revision.

- - +{% extends "base.html" %} +{% block title %}Deleted{% endblock %} +{% block content %} +
+

This page has been deleted

+

“{{ page_name }}” is gone. Append ?r=N to view a specific revision, or restore it from the page's history.

+ {% if is_signed_in or is_unclaimed %} +
+ + +
+ {% endif %} +
+{% endblock %} diff --git a/jottit/templates/diff.html b/jottit/templates/diff.html index 82187d2..d769b02 100644 --- a/jottit/templates/diff.html +++ b/jottit/templates/diff.html @@ -1,18 +1,20 @@ - - - - - Diff: {{ page_name or "Home" }} - - -

Diff: {{ page_name or "Home" }}

-

- Comparing - revision {{ a.revision }} - with - revision {{ b.revision }}. -

-
{{ diff_html|safe }}
-

Back to history

- - +{% extends "base.html" %} +{% block title %}Diff: {{ page_name or "Home" }}{% endblock %} + +{% block content %} +
+
+

Diff: {{ page_name or "Home" }}

+

+ Comparing + revision {{ a.revision }} + with + revision {{ b.revision }}. +

+
+ +
{{ diff_html|safe }}
+ +

Back to history

+
+{% endblock %} diff --git a/jottit/templates/edit_page.html b/jottit/templates/edit_page.html index 18f81ff..d80d160 100644 --- a/jottit/templates/edit_page.html +++ b/jottit/templates/edit_page.html @@ -1,18 +1,26 @@ - - - - - Edit: {{ page_name or "Home" }} - - -

Edit: {{ page_name or "Home" }}

-
- -

-

- - {% if page_name %}{% endif %} -

-
- - +{% extends "base.html" %} + +{% block title %}Edit: {{ page_name or "Home" }}{% if site and site.title %} — {{ site.title }}{% endif %}{% endblock %} + +{% block content %} +
+

Edit: {{ page_name or "Home" }}

+ +
+ +

+ +

+

+ + Cancel + {% if page_name %} + + {% endif %} +

+
+
+{% endblock %} diff --git a/jottit/templates/forgot_password.html b/jottit/templates/forgot_password.html index 97b49bf..3ed320c 100644 --- a/jottit/templates/forgot_password.html +++ b/jottit/templates/forgot_password.html @@ -1,18 +1,16 @@ - - - - - Forgot your password? - - -

Forgot your password?

- {% if sent %} -

We've emailed a password reset link to the address on file.

- {% else %} -

We'll email a one-time password reset link to the email address on file for this site.

-
-

-
- {% endif %} - - +{% extends "base.html" %} +{% block title %}Forgot your password?{% endblock %} +{% block content %} +
+

Forgot your password?

+ + {% if sent %} +

We've emailed a password-reset link to the address on file. The link works once — if you need another, come back to this page.

+ {% else %} +

We'll email a one-time password-reset link to the email address on file for this site.

+
+

+
+ {% endif %} +
+{% endblock %} diff --git a/jottit/templates/history.html b/jottit/templates/history.html index 40e99e3..3a9e384 100644 --- a/jottit/templates/history.html +++ b/jottit/templates/history.html @@ -1,24 +1,36 @@ - - - - - History: {{ page_name or "Home" }} - - -

History: {{ page_name or "Home" }}

-

{{ total }} revision{{ "s" if total != 1 }}.

- - {% if older_before %} -

Older →

- {% endif %} -

Back to page

- - +{% extends "base.html" %} +{% block title %}History: {{ page_name or "Home" }}{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+
+

History: {{ page_name or "Home" }}

+

{{ total }} revision{{ "s" if total != 1 }}.

+
+ +
    + {% for r in revisions %} +
  1. + Revision {{ r.revision }} + + {% if r.changes %}{{ r.changes|safe }}{% endif %} +
  2. + {% endfor %} +
+ + {% if older_before %} +

Older →

+ {% endif %} + +

Back to page

+
+{% endblock %} diff --git a/jottit/templates/index.html b/jottit/templates/index.html index cff2767..3f33a0e 100644 --- a/jottit/templates/index.html +++ b/jottit/templates/index.html @@ -1,11 +1,28 @@ - - -Jottit - -

Jottit

-
-

-

-
- - +{% extends "base.html" %} +{% block title %}Jottit — make a site by filling out a textbox{% endblock %} +{% block content %} +
+
+

Jottit

+

Getting a website should be as easy as filling out a textbox.

+
+ +
+

+ +

+

+ +

+

+
+
+{% endblock %} diff --git a/jottit/templates/notfound.html b/jottit/templates/notfound.html index 263ec3d..3b6cbe4 100644 --- a/jottit/templates/notfound.html +++ b/jottit/templates/notfound.html @@ -1,11 +1,11 @@ - - - - - Not found - - -

Not found

-

There's no page named “{{ page_name }}” on this site.

- - +{% extends "base.html" %} +{% block title %}Not found{% endblock %} +{% block content %} +
+

Not found

+

There's no page named “{{ page_name }}” on this site.

+ {% if is_signed_in or is_unclaimed %} +

Create this page

+ {% endif %} +
+{% endblock %} diff --git a/jottit/templates/partials/design_style.html b/jottit/templates/partials/design_style.html new file mode 100644 index 0000000..628f361 --- /dev/null +++ b/jottit/templates/partials/design_style.html @@ -0,0 +1,23 @@ +{# Inline CSS variables sourced from the site's design row. + + The 2007 site embedded full per-site CSS rules here; we surface the + values as custom properties instead and let the global stylesheet + reference them. That keeps the per-request payload small and lets + the M9 stylesheet rewrite without coordinating with this template. #} + diff --git a/jottit/templates/partials/form_error.html b/jottit/templates/partials/form_error.html new file mode 100644 index 0000000..89de835 --- /dev/null +++ b/jottit/templates/partials/form_error.html @@ -0,0 +1,3 @@ +{# Inline form error. Hidden when no error message is set, so callers can + include this unconditionally above their form fields. #} +{% if error %}{% endif %} diff --git a/jottit/templates/partials/site_header.html b/jottit/templates/partials/site_header.html new file mode 100644 index 0000000..3cf80fe --- /dev/null +++ b/jottit/templates/partials/site_header.html @@ -0,0 +1,34 @@ +{# Header chrome rendered on every site page: title, subtitle, primary nav. + The unclaimed-site banner sits above the header so it's the most visible + action for a brand-new site. #} + diff --git a/jottit/templates/partials/site_sidebar.html b/jottit/templates/partials/site_sidebar.html new file mode 100644 index 0000000..2dd5b69 --- /dev/null +++ b/jottit/templates/partials/site_sidebar.html @@ -0,0 +1,32 @@ +{# Page list for the current site. Hidden when the site has only the + home page — it's noise on a fresh site. The "new page" form is a + plain link to the edit URL; M10 will replace the inline form with + a JS-driven inline create. #} +{% if pages|length > 1 or is_signed_in %} + +{% endif %} diff --git a/jottit/templates/signin.html b/jottit/templates/signin.html index 160488c..a716dbc 100644 --- a/jottit/templates/signin.html +++ b/jottit/templates/signin.html @@ -1,21 +1,21 @@ - - - - - Sign in - - -

Sign in

- {% if error %}

{{ error }}

{% endif %} -
- -

- -

-

-
-

Forgot your password?

- - +{% extends "base.html" %} +{% block title %}Sign in{% endblock %} +{% block content %} +
+

Sign in

+ + {% include "partials/form_error.html" %} + +
+ +

+ +

+

+
+ +

Forgot your password?

+
+{% endblock %} diff --git a/jottit/templates/view_page.html b/jottit/templates/view_page.html index b594a43..44327f6 100644 --- a/jottit/templates/view_page.html +++ b/jottit/templates/view_page.html @@ -1,13 +1,59 @@ - - - - - {{ page_name or "Home" }} - - -
{{ content_html|safe }}
- - - +{% extends "base.html" %} + +{% block title %}{{ page_name or "Home" }}{% if site and site.title %} — {{ site.title }}{% endif %}{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
+
+

{{ page_name or "Home" }}

+ {% if is_old_revision %} +

+ Viewing revision {{ revision.revision }} + of {{ latest_revision_number }}. + Latest + {% if revision.revision > 1 %} + · prev + {% endif %} + {% if revision.revision < latest_revision_number %} + · next + {% endif %} +

+ + {% if is_signed_in or is_unclaimed %} +
+ + + +
+ {% endif %} + {% endif %} +
+ +
+ {{ content_html|safe }} +
+ + +
+{% endblock %} diff --git a/jottit/views/page.py b/jottit/views/page.py index dfe64ee..9ad1895 100644 --- a/jottit/views/page.py +++ b/jottit/views/page.py @@ -99,11 +99,16 @@ def _render_view(conn: Connection, page_name: str) -> ResponseReturnValue: if revision is None: abort(404) + latest = get_revision(conn, page_id=page.id) rendered = format_content(revision.content, site_root=site_root()) return render_template( "view_page.html", page_name=page_name, revision=revision, + latest_revision_number=latest.revision if latest is not None else revision.revision, + is_old_revision=requested_revision is not None + and latest is not None + and revision.revision != latest.revision, content_html=rendered, ) diff --git a/tests/test_admin_design.py b/tests/test_admin_design.py index 24d9fce..9bdf9b6 100644 --- a/tests/test_admin_design.py +++ b/tests/test_admin_design.py @@ -85,9 +85,9 @@ def test_get_renders_form_with_current_design(client: FlaskClient, db_engine: En assert response.status_code == 200 body = response.data.decode() - # new_site populates the design row from a random ColorScheme; the - # title font default is Lucida_Grande. - assert "Lucida_Grande" in body + # new_site seeds the design row with "system-ui, sans-serif" so the + # site resolves to the visitor's OS font without shipping a webfont. + assert "system-ui, sans-serif" in body # ---- POST: happy path ---- diff --git a/tests/test_db_queries.py b/tests/test_db_queries.py index 9947c12..8ebc300 100644 --- a/tests/test_db_queries.py +++ b/tests/test_db_queries.py @@ -84,7 +84,7 @@ def test_new_site_creates_design_row_with_random_scheme(db_conn: Connection) -> site_id = new_site(db_conn, content="hi", secret_url="d1") design = db_conn.execute(select(designs).where(designs.c.site_id == site_id)).one() - assert design.title_font == "Lucida_Grande" + assert design.title_font == "system-ui, sans-serif" assert design.header_color.startswith("#") assert design.title_color.startswith("#") assert design.subtitle_color.startswith("#") @@ -574,7 +574,7 @@ def test_get_design_returns_row_created_by_new_site(db_conn: Connection) -> None assert design is not None assert design.site_id == site_id - assert design.title_font == "Lucida_Grande" + assert design.title_font == "system-ui, sans-serif" def test_update_design_patches_provided_fields(db_conn: Connection) -> None: diff --git a/tests/test_page_view.py b/tests/test_page_view.py index d94b066..b8931f6 100644 --- a/tests/test_page_view.py +++ b/tests/test_page_view.py @@ -40,7 +40,7 @@ def test_home_on_public_subdomain_renders_latest_revision( assert response.status_code == 200 body = response.data.decode() assert "hi there" in body - assert "revision 1" in body + assert "Revision 1" in body def test_named_page_renders_content(client: FlaskClient, db_engine: Engine) -> None: