Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions jottit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
48 changes: 48 additions & 0 deletions jottit/chrome.py
Original file line number Diff line number Diff line change
@@ -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,
}
25 changes: 21 additions & 4 deletions jottit/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
104 changes: 104 additions & 0 deletions jottit/static/base.css
Original file line number Diff line number Diff line change
@@ -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); }
Loading
Loading