add tinyweb-site theme: clean, light, minimal

This commit is contained in:
lichenblankie 2026-06-05 04:26:22 +00:00
parent 2c8f3e39b5
commit 1b682e266a
3 changed files with 299 additions and 43 deletions

View file

@ -1,43 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What is TinyWeb
A personal, decentralized search engine built on the Reticulum mesh network. Users curate and search their own index of web pages, share collections over an encrypted mesh, and subscribe to friends' indexes. No algorithms, no tracking.
## Running
```bash
pip install -r requirements.txt
python app.py # Starts RNS server + HTTP gateway on 0.0.0.0:8080
python gateway.py <hash> # Run as HTTP gateway to a remote TinyWeb instance
```
There are no tests, linter, or build step.
## Architecture
Three entry points form a pipeline:
- **app.py** — Boots Reticulum, loads/creates identity from `tinyweb_identity`, announces on mesh, starts HTTP gateway as a daemon thread, then loops handling RNS requests.
- **gateway.py**`BaseHTTPRequestHandler` that translates HTTP GET/POST into a request dict and dispatches it. When `local_dispatch` is set (the default when launched from app.py), it calls handlers directly; otherwise it sends requests over a Reticulum link.
- **handlers.py** — Central router (`handle_request`) that pattern-matches the path and calls the appropriate handler. Every handler returns `{"status", "content_type", "body", "headers"}`.
## Database (db.py → index.db)
SQLite with FTS5. Schema is initialized and migrated in `init_db()` on every startup.
Key tables: `pages` (indexed URLs), `links` (extracted same-domain links), `tags`/`page_tags` (many-to-many tagging), `pages_fts` (full-text search via triggers), `subscriptions` (remote instances), `remote_pages`/`remote_pages_fts` (synced content).
`get_db()` opens a fresh connection each call — no connection pooling.
## Patterns to follow
- All HTML output is built as inline strings in handlers.py; there is no template engine. Use `templates.wrap_page(title, body_html)` to wrap content with boilerplate and custom CSS.
- Use `esc()` (html.escape) for all user-supplied content rendered in HTML.
- Handlers receive `(path_segment, ...)` args extracted by the router and return a response dict.
- Tags are stored in a join table; orphaned rows in `tags` can accumulate — always query through `page_tags` for accurate counts.
- Link extraction (`extract_links`) only follows same-domain URLs and skips binary file extensions and Wikipedia special pages.
- URL cleanup: fragments are stripped, tracking params (utm_*, fbclid, gclid, etc.) are removed before storing.
- Settings are stored as key-value pairs in the `settings` table; access via `get_setting(key, default)`.

View file

@ -143,6 +143,7 @@
input[type="text"], input[type="text"],
input[type="url"], input[type="url"],
input:not([type]),
input[name="q"], input[name="q"],
input[name="url"], input[name="url"],
input[name="note"], input[name="note"],
@ -328,6 +329,12 @@
label { color: #5a7880; } label { color: #5a7880; }
input[type="checkbox"] { accent-color: #3a6858; } input[type="checkbox"] { accent-color: #3a6858; }
a.forum-action, a.forum-action-inline {
color: #5a7880; border: 1px solid rgba(40, 70, 65, 0.3); border-radius: 4px; padding: 6px 12px;
transition: background 0.2s, color 0.2s; font-size: 0.85rem;
}
a.forum-action:hover, a.forum-action-inline:hover { color: #90b4ac; background: rgba(10, 22, 25, 0.6); }
a.forum-action-inline { padding: 2px 6px; border: none; }
hr { border: none; border-top: 1px solid rgba(30, 55, 50, 0.3); margin: 1rem 0; } hr { border: none; border-top: 1px solid rgba(30, 55, 50, 0.3); margin: 1rem 0; }
small { small {

292
themes/tinyweb-site.html Normal file
View file

@ -0,0 +1,292 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta http-equiv="x-dns-prefetch-control" content="off">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { min-height: 100vh; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
font-size: 18px;
line-height: 1.6;
color: #444;
background: #fff;
padding: 60px 40px;
display: flex;
flex-direction: column;
align-items: center;
}
a { color: #444; text-decoration: none; cursor: pointer; }
a:hover { color: #222; }
input, textarea {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
}
input[type="text"],
input[type="url"],
input[type="search"],
input:not([type]),
textarea, select {
border: 1px solid #ccc;
padding: 8px 12px;
font-size: 15px;
border-radius: 0;
background: #fff;
color: #444;
}
input:focus, textarea:focus {
outline: none;
border-color: #999;
}
button, input[type="submit"] {
border: 1px solid #ccc;
padding: 8px 18px;
font-size: 13px;
text-transform: uppercase;
background: #fff;
color: #444;
cursor: pointer;
border-radius: 0;
transition: background 0.2s;
}
button:hover, input[type="submit"]:hover {
background: #f5f5f5;
}
.shell {
width: 100%;
max-width: 650px;
}
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 12px;
}
nav .site {
font-size: 28px;
font-weight: bold;
color: #222;
text-decoration: none;
border-bottom: none;
}
nav .site:hover { color: #222; }
nav .links { display: flex; gap: 8px; flex-wrap: wrap; }
nav .links a {
font-size: 13px;
text-transform: uppercase;
padding: 6px 14px;
border: 1px solid #ccc;
background: #fff;
color: #444;
text-decoration: none;
transition: background 0.2s;
}
nav .links a:hover {
background: #f5f5f5;
color: #444;
}
.content { width: 100%; animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
h1 {
font-size: 32px;
font-weight: bold;
color: #222;
margin-bottom: 20px;
line-height: 1.2;
}
h1 a { color: #222; text-decoration: none; }
h2 {
font-size: 22px;
font-weight: bold;
color: #222;
margin: 24px 0 12px;
}
p { margin: 12px 0; color: #444; }
a { color: #444; text-decoration: none; border-bottom: 1px solid #ddd; }
a:hover { color: #222; border-bottom-color: #999; }
em { color: #666; }
.result {
padding: 20px 0;
border-bottom: 1px solid #eee;
}
.result:last-child { border-bottom: none; }
.result > a:first-child {
font-size: 18px;
color: #222;
border-bottom: none;
}
.result > a:first-child:hover { color: #222; }
.note { margin-top: 4px; font-size: 15px; color: #666; }
.meta {
font-size: 13px;
color: #999;
}
.pagination {
font-size: 13px;
color: #999;
margin: 24px 0;
text-align: center;
}
.pagination a {
color: #444;
border-bottom: 1px solid #ccc;
}
.pagination a:hover { color: #222; }
.success {
color: #444;
background: #f5f5f5;
border: 1px solid #ddd;
padding: 10px 16px;
font-size: 15px;
}
.tags { margin-top: 4px; }
.tag, .tags a {
font-size: 12px;
color: #999;
border: 1px solid #ddd;
padding: 2px 8px;
margin-right: 4px;
text-decoration: none;
}
.tag:hover, .tags a:hover {
color: #444;
border-color: #ccc;
}
details {
margin: 16px 0;
border: 1px solid #eee;
padding: 12px 16px;
background: #fafafa;
}
summary { font-size: 15px; color: #666; cursor: pointer; }
summary:hover { color: #444; }
details ul { margin-top: 8px; padding-left: 20px; }
details li { margin: 6px 0; font-size: 15px; }
ul, ol { padding-left: 20px; margin: 8px 0; }
li { margin: 6px 0; color: #444; }
li a { border-bottom: none; }
pre {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 14px;
background: #f5f5f5;
border: 1px solid #eee;
padding: 16px;
overflow-x: auto;
color: #444;
margin: 12px 0;
}
code {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 14px;
background: #f5f5f5;
padding: 2px 6px;
color: #444;
}
textarea {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 14px;
line-height: 1.6;
resize: vertical;
width: 100%;
border: 1px solid #ccc;
padding: 10px 12px;
background: #fff;
color: #444;
}
table { width: 100%; border-collapse: collapse; margin: 16px 0; }
th {
text-align: left;
font-size: 12px;
font-weight: 600;
color: #999;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
td {
padding: 8px 12px;
border-bottom: 1px solid #f5f5f5;
font-size: 15px;
}
label { color: #666; }
input[type="checkbox"] { accent-color: #444; }
.forum-form input, .forum-form button, .forum-toolbar input { border-radius: 0; }
.forum-actions a, a.forum-action, a.forum-action-inline {
border: 1px solid #ccc; padding: 6px 14px; text-transform: uppercase; font-size: 13px;
}
.forum-actions a:hover, a.forum-action:hover, a.forum-action-inline:hover {
background: #f5f5f5;
}
a.forum-action-inline { text-transform: none; font-size: 13px; padding: 2px 6px; border: none; }
hr { border: none; border-top: 1px solid #eee; margin: 16px 0; }
small {
font-size: 13px;
color: #999;
}
footer {
width: 100%;
margin-top: 40px;
padding-top: 16px;
border-top: 1px solid #eee;
text-align: center;
color: #999;
font-size: 13px;
}
footer .clock {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 12px;
color: #ccc;
margin-top: 4px;
}
::selection { background: #222; color: #fff; }
@media (max-width: 600px) {
body { padding: 30px 20px; font-size: 16px; }
h1 { font-size: 26px; }
nav { flex-direction: column; align-items: flex-start; }
nav .links { gap: 6px; }
nav .links a { padding: 5px 10px; font-size: 12px; }
}
</style>
</head>
<body>
<div class="shell">
<nav>
<a class="site" href="/">{{site_name}}</a>
<div class="links">
<a href="/pages">browse</a>
<a href="/tags">tags</a>
<a href="/subscriptions">network</a>
{{forum_link}}
<a href="/style">customize</a>
<a href="/about">about</a>
</div>
</nav>
<div class="content">
{{content}}
</div>
<footer>
<div>curated by hand &middot; shared over mesh</div>
<div class="clock" id="clock"></div>
</footer>
</div>
<script>
(function() {
var d = document.getElementById('clock');
if (d) {
function tick() {
var n = new Date();
d.textContent = n.toLocaleString();
}
tick();
setInterval(tick, 1000);
}
})();
</script>
</body>
</html>