Skip to main content

max / makenotwork

16.0 KB · 276 lines History Blame Raw
1 # MNW Frontend
2
3 Server-rendered HTML via Askama templates, HTMX for interactivity, hand-authored CSS. No JS framework. There is one build step: `frontend/` is TypeScript compiled to `static/dist/`, and the tab strips and several other regions are described in Rust under `src/quasi/` and rendered into the templates as strings.
4
5 ## Design System
6
7 Typography and brand colors come from the cross-project brand system (three-tier type system, base palette, diamond mark rule). `docs/design-system.md` is the charter and the source of truth for token names; this file records how templates and CSS use them.
8
9 ### Where the tokens live
10
11 | Group | Tokens | File |
12 |-------|--------|------|
13 | Surfaces | `--surface-page`, `--surface-raised`, `--surface-sunken`, `--surface-overlay`, `--surface-well`, `--bevel-light`, `--bevel-dark` | `static/style.css` `:root` |
14 | Content | `--content`, `--content-secondary`, `--content-muted`, `--content-on-action` | `static/style.css` `:root` |
15 | Action and status | `--action`, `--action-hover`, `--success`, `--warning`, `--danger`, `--info` | `static/style.css` `:root` |
16 | Lines and focus | `--border`, `--border-strong`, `--focus-ring`, `--hover-surface`, `--overlay` | `static/style.css` `:root` |
17 | Typeface | `--font-mono`, `--font-sans`, `--font-display` | `static/typography.css` (generated) |
18 | Type size, spacing, radius | `--text-*`, `--gap-*`, `--radius-*` | `static/geometry.css` (generated) |
19
20 The `:root` block in `style.css` holds the platform default. Creator profile, project and item pages inject a `<style>` that supersedes it with the chosen theme, so every one of these is themeable.
21
22 ### Rules
23
24 - **No pure white/black.** Use `--surface-page` and `--content` instead.
25 - **Accent color is for the dot only.** Do not use `--action` for arbitrary borders or decoration.
26 - **No hex or `rgba()` literals outside `:root`.** Every colour elsewhere is a `var()` or a `color-mix()` over one. Write the token, not `var(--token, #fallback)`.
27
28 ## Template Structure
29
30 ```
31 templates/
32 base.html Head from crate::shell, page blocks, footer, script tail
33 index.html Landing page
34 pages/ Full page templates (extend base.html)
35 login.html, discover.html, item.html, project.html, library.html, ...
36 dashboards/ Dashboard pages (extend base.html, tabbed)
37 dashboard-user.html User dashboard (Projects, Payments, Analytics, Settings, Support)
38 dashboard-project.html Project dashboard (Overview, Content, Analytics, Monetization, Code, Cloud Sync, Settings)
39 dashboard-item.html Item dashboard (Overview, Details, Pricing, Files, Sales)
40 dashboard-blog-editor.html, dashboard-export.html, dashboard-import.html
41 admin-*.html Admin panels (waitlist, users, uploads, appeals, reports, metrics)
42 wizards/ Multi-step create flows (join, project, item)
43 partials/ HTMX fragments (no base.html, returned directly)
44 tabs/ Dashboard tab and settings-section content
45 user_profile.html, user_account.html, user_payments.html,
46 user_creator.html, user_media.html, user_sessions.html,
47 user_settings.html, user_synckit.html
48 project_code.html, project_monetization.html, project_promotions.html,
49 project_settings.html, project_subscriptions.html, project_synckit.html
50 item_details.html, item_embed.html, item_overview.html, item_pricing.html
51 library_collections.html, library_purchases.html
52 admin_*.html Admin table rows (HTMX swap targets)
53 _ui.html Shared Askama macros
54 alert.html, form_status.html, save_status.html Status feedback fragments
55 ```
56
57 ### Conventions
58
59 - Full pages extend `base.html` and define `{% block content %}`.
60 - The document head is not written in `base.html`. `crate::shell::head()` emits it, so a described screen that owns its whole document and an Askama page get the same head.
61 - Tab content is loaded via HTMX (`hx-get="/dashboard/tabs/payments"` etc).
62 - Partials are standalone HTML fragments, no `{% extends %}`.
63 - Template structs live in `src/templates/dashboard.rs` (pages), `src/templates/partials.rs` (fragments), `src/templates/public/` (landing, auth, git, health) and `src/templates/embed.rs`.
64
65 ## CSS Architecture
66
67 Five stylesheets, loaded in this order by `crate::shell::head()`: `typography.css`, `geometry.css`, `timing.css`, `layout.css`, `style.css`. The first four are generated by `build.rs` through makeover-build and are marked do-not-edit at the top of each file. `style.css` is the only hand-authored one. No preprocessor, no minification pipeline.
68
69 ### Section Order
70
71 `style.css` opens with a FILE STRUCTURE comment that indexes every section by name; search the file for a section name to jump to it. The order is:
72
73 1. **Foundations** (`@font-face`, `:root` tokens, element base)
74 2. **Buttons** (`.btn-primary`, `.btn-secondary`, `.btn-danger`, plus `.small` / `.btn-compact` / `.btn-link` / `.btn-tiny` modifiers)
75 3. **Forms** (`.form-group`, `.form-section`, `.field-row`, the `input--*` modifiers)
76 4. **Tabs** (`.tabs`, `.tab`, `.tab.chosen`)
77 5. **Tables** (`.data-table`, then `.compact-table`)
78 6. **Utilities** (spacing, sizing, text, state, shapes)
79 7. **Badges** and **Tags**
80 8. **Cards** and **Stats** (`.card`, `.card-muted`, `.card--bordered`, `.stat-box`)
81 9. **Filter bar**, **Discussion**, **Notification surfaces** (the `.info-box` / `.warning-box` / `.error-message` roles)
82 10. **Page-scoped layouts**, the long run: error, buy, sectioned content, item, project, library, article, admin, import wizard, delete account, export, receipt, Fan+, purchase, project settings, cart, profile
83 11. **Sections, Navigation, Header and footer, Layout utilities, Specific components**, then **HTMX loading indicators**, **Toast**, **Save status**, **HTMX transitions**, **Focus styles**, **Paywall**, **Modals**
84 12. A second, larger run of **page-scoped layouts** (landing, docs, use cases, git browser, discover, and one section per tab or partial), ending with **Layer handoffs**, the **described modal** and the **media picker**
85
86 The notification-surface primitives are not all in the section that names them: `.empty-state` sits in Utilities, `.banner` just above Sections, `.toast` and `.modal` in their own sections after Specific components, `.progress-bar` with the audio upload, `.callout` in the Account tab section and `.alert` with the documentation pages. Search the file for the class, not for the section.
87
88 Responsive rules are not all at the end either: the 768px and 480px blocks sit in the middle of the second page-scoped run, and the later sections carry their own `@media` queries.
89
90 ### Utility Classes
91
92 | Class | Styles | Use for |
93 |-------|--------|---------|
94 | `.text-sm` | `font-size: var(--text-note)` | Secondary text (emails, dates) |
95 | `.text-xs` | `font-size: var(--text-fine)` | Tertiary text (IDs, annotations) |
96 | `.muted` | `opacity: 0.7` | De-emphasized content |
97 | `.dimmed` | `opacity: 0.6` | Further de-emphasized |
98 | `.dimmer` | `opacity: 0.5` | Furthest de-emphasized |
99 | `.meta` | `font-size: var(--text-note); opacity: 0.7` | Labels, captions |
100 | `.nowrap` | `white-space: nowrap` | Dates, status badges |
101 | `.scroll-x` | `overflow-x: auto; -webkit-overflow-scrolling: touch` | Horizontal scroll wrappers |
102 | `.text-center` | `text-align: center` | Centered blocks |
103 | `.text-right` | `text-align: right` | Right-aligned cells |
104 | `.fw-bold` | `font-weight: bold` | Emphasis without a heading |
105 | `.small` | `padding: var(--gap-bound) var(--gap-group); font-size: var(--text-fine)` | Compact buttons in tables |
106 | `.empty-state` | `text-align: center; padding: var(--gap-page); opacity: 0.6` | Empty list messages |
107
108 ### Table Classes
109
110 **`.data-table`**: Full-featured tables with alternating row backgrounds, hover states, and sortable headers. Used for transactions, blog posts, content items.
111
112 **`.compact-table`**: Lighter admin/dashboard tables. Provides `width: 100%`, `border-collapse`, cell padding, header styling and row borders. Used for admin user/waitlist/upload tables, invite codes, wave history.
113
114 ### Badges
115
116 `.badge` is the base. Tone comes from a `data-tone` attribute rather than a modifier class:
117
118 ```html
119 <span class="badge" data-tone="success">Trusted</span>
120 ```
121
122 `success`, `warning` and `danger` are the tones. The modifiers that exist beside them are `.badge--founder-locked`, `.badge--founder-pending`, `.badge--inline-l`, `.badge--inline-tiny`, `.badge-new`, the `.badge.ai-tier*` disclosure set and `.badge.is-faded`.
123
124 ### Button Variants
125
126 Buttons use the `.btn-*` family. See `design-system.md` ยง Buttons for the current class names, colors, and modifiers. Do not use the bare `.primary` / `.secondary` / `.danger` classes; use `.btn-primary`, `.btn-secondary`, `.btn-danger`.
127
128 ## HTMX Patterns
129
130 ### Tab Navigation
131
132 Tab strips are not written as markup. Each one is described in Rust and rendered into its Askama page as a string: `src/quasi/user_tabs.rs`, `project_tabs.rs`, `item_tabs.rs`, `library_tabs.rs` and `settings_tabs.rs`. The page passes the result through:
133
134 ```html
135 {{ tabs|safe }}
136 ```
137
138 The description names each tab, the region its panel lands in, and what fetches that panel:
139
140 ```rust
141 let mut strip = Slot::new(STRIP, RegionKind::TabGroup)
142 .across(layout::Fallback::Menu)
143 .showing_one(shown);
144
145 for (at, tab) in tabs.iter().enumerate() {
146 let mut region = Slot::bespoke(tab.panel, "item-panel").label(tab.label);
147 if at != shown {
148 region = region.fed_by(
149 Action::get(format!("/dashboard/item/{item_id}/tabs/{}", tab.route))
150 .awaiting()
151 .replacing(tab.panel),
152 );
153 }
154 strip = strip.with(Node::Region(region));
155 }
156 ```
157
158 Three things follow from this and are worth knowing before touching a strip.
159
160 - **Each panel is its own region.** There is no single `#tab-content` pane for every tab to swap into, so an action outside the strip names the panel it means (`#item-sales`, `#user-settings`).
161 - **The shown tab is rendered inline by the page handler** and is the one tab that does not fetch. The page arrives at final geometry rather than fetching its first panel after load.
162 - **A deep link is a `?tab=` query**, read server-side by each module's `shown_at`, not a `#tab-*` hash restored by JS. Unknown names answer the first tab.
163
164 `Action::awaiting()` is what an act in flight says. There is no `setActiveTab`, no `tab-spinner` indicator and no hash restore.
165
166 ### Admin Table Updates
167
168 Admin actions (suspend, approve, trust) return the full table partial to replace the table body:
169
170 ```html
171 <button class="btn-secondary small"
172 hx-post="/api/admin/users/{{ user.id }}/trust"
173 hx-target="#users-table"
174 hx-swap="innerHTML">Trust</button>
175 ```
176
177 ### Form Feedback
178
179 Form submissions target a status element for inline feedback:
180
181 ```html
182 <form hx-put="/api/users/me"
183 hx-target="#profile-save-status"
184 hx-swap="innerHTML"
185 hx-indicator="#profile-spinner">
186 ```
187
188 The handler returns an `AlertTemplate` or `FormStatusTemplate` partial. `hx-indicator` is the idiom in Askama markup; a described region uses `Action::awaiting()` instead.
189
190 ### CSRF
191
192 All mutating requests include CSRF tokens. `base.html` emits a `csrf-token` meta tag. `frontend/src/core/net.ts` reads it and returns the `X-CSRF-Token` header from `csrfHeaders()`, which `core/index.ts` also exports on `window` for hand-written `fetch()` calls. HTMX requests get the header from `frontend/src/core/htmx-glue.ts`, which attaches it on `htmx:config:request`. The token is read live on every request because it rotates mid-session.
193
194 ## JavaScript
195
196 Two layers. `frontend/src/` is TypeScript, compiled to `static/dist/` and loaded as a module. Everything else in `static/` is hand-written JS loaded with a plain `<script>`.
197
198 | File | Purpose |
199 |------|---------|
200 | `frontend/src/core/dispatch.ts` | Delegated event dispatcher: `data-action`, `data-change`, `data-input`, `data-submit`, `data-after` |
201 | `frontend/src/core/net.ts` | `csrfHeaders()` and the fetch helpers |
202 | `frontend/src/core/toast.ts` | `showToast(message)` |
203 | `frontend/src/core/htmx-glue.ts` | htmx lifecycle hooks |
204 | `static/htmx.min.js` | HTMX library (vendored), plus the `hx-history-cache` and `hx-prompt` extensions |
205 | `static/quasi-*.js` | The renderer's behaviour modules, one per described capability |
206 | `static/actions-*.js` | Verb registration shims for pages, partials, tabs and dashboards |
207 | `static/passkey.js` | WebAuthn passkey registration and authentication |
208 | `static/upload.js` | `S3Uploader`, file upload with progress bars |
209 | `static/page-*.js`, `tab-*.js`, `wizard-*.js` | Per-screen behaviour, loaded by the screen that needs it |
210
211 ### Conventions
212
213 - No inline `<script>` in templates and no `on*` or `hx-on::` attributes. `script-src 'self'` with no `unsafe-inline` is enforced, and logic in an attribute value breaks it.
214 - Behaviour is named with a verb in `data-action` and resolved against the dispatcher's registry. Never invent a private attribute for a behaviour; add a verb.
215 - Use `csrfHeaders()` for all `fetch()` calls.
216 - Use `showToast(message)` for user notifications.
217 - Use `hx-confirm` for destructive actions.
218 - No external JS dependencies beyond HTMX.
219 - No JS framework, vanilla DOM manipulation only.
220 - `_hyperscript` is declined permanently. See `design-system.md` ยง Interaction idioms.
221
222 ## Static Assets
223
224 ```
225 static/
226 style.css Main stylesheet
227 typography.css Generated: the three typeface tokens
228 geometry.css Generated: type size, spacing, radius tokens
229 timing.css Generated: duration tokens
230 layout.css Generated: layout primitives and the bevel pair
231 wizard.css, media-player.css, no-js.css, embed-*.css Loaded per screen
232 htmx.min.js HTMX library
233 dist/ Compiled output of frontend/src (core/ and islands/)
234 quasi-*.js Renderer behaviour modules
235 actions-*.js Verb registration shims
236 passkey.js WebAuthn integration
237 upload.js File upload with progress
238 fonts/ Quasi Body, Quasi Mono (woff2), Young Serif (ttf + woff2)
239 images/ Logo, favicons, og-card, screenshots
240 bases/ Content-addressed theme base images
241 patterns/ Background SVGs
242 ```
243
244 ## Progressive Disclosure
245
246 The dashboard conditionally shows features based on user state. The gates are the `Gate` enums in the tab-strip modules, not template conditionals:
247
248 - **Projects and Analytics tabs** (`src/quasi/user_tabs.rs`): only visible when `can_create_projects` is true.
249 - **Every user tab except Support**: hidden once the account is deactivated. Support is the one tab a deactivated account keeps, because it is the only thing it can do.
250 - **Media settings section** (`src/quasi/settings_tabs.rs`): only when the reader can create projects.
251 - **SSH Keys settings section**: only when git hosting is enabled on the server.
252 - **Forums settings section**: only when the Multithreaded integration is configured.
253 - **Cloud Sync settings section**: only when the reader owns at least one sync app.
254 - **Code tab** (`src/quasi/project_tabs.rs`): only when the project has git enabled.
255 - **Cloud Sync tab** (project dashboard): only when SyncKit is configured for the project.
256 - **Files tab** (`src/quasi/item_tabs.rs`): absent on a bundle, which carries other items rather than files of its own.
257 - **Onboarding checklist**: auto-dismissed when all steps complete (or manually skipped).
258
259 ## Key Paths
260
261 | What | Where |
262 |------|-------|
263 | CSS | `static/style.css`, `static/layout.css` |
264 | Generated tokens | `static/typography.css`, `static/geometry.css`, `static/timing.css` |
265 | Document head and script tail | `src/shell.rs` |
266 | Base template | `templates/base.html` |
267 | Dashboard pages | `templates/dashboards/` |
268 | Tab partials | `templates/partials/tabs/` |
269 | Admin partials | `templates/partials/admin_*.html` |
270 | Described regions | `src/quasi/` |
271 | Template structs | `src/templates/dashboard.rs`, `src/templates/partials.rs` |
272 | Dashboard handlers | `src/routes/pages/dashboard/` |
273 | TypeScript core | `frontend/src/core/` |
274 | JS (passkeys) | `static/passkey.js` |
275 | JS (uploads) | `static/upload.js` |
276