Compare commits
3 Commits
master
...
268ffcdd27
| Author | SHA1 | Date | |
|---|---|---|---|
| 268ffcdd27 | |||
| ec94580ab5 | |||
| e0a2d427cc |
16
CLAUDE.md
16
CLAUDE.md
@@ -54,8 +54,20 @@ Prefer separate, human-readable `.html` files over inlined HTML strings in Go. E
|
|||||||
- Do not inline JS in templates or merge unrelated features into one file
|
- Do not inline JS in templates or merge unrelated features into one file
|
||||||
- `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others
|
- `ALT+SHIFT` is the modifier for all keyboard shortcuts — do not introduce others
|
||||||
- Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut
|
- Editor toolbar buttons use `data-action` + `data-key`; adding `data-key` auto-registers the shortcut
|
||||||
- Prefer generic, descriptive CSS classes (`btn`, `btn-small`, `muted`, `danger`) over element-specific names (`save-button`, `cancel-button`, `form-name-input`). Use a modifier + base class pattern (`btn btn-small`) rather than one-off classes that duplicate shared styles.
|
|
||||||
- Where possible, re-use existing CSS classes
|
### CSS / HTML — Pico CSS
|
||||||
|
|
||||||
|
[Pico CSS](https://picocss.com) is the styling framework. Strictly stay within it.
|
||||||
|
|
||||||
|
- Do not add other CSS frameworks, utility libraries, or icon fonts.
|
||||||
|
- Prefer pico's **class-less semantic HTML**: `<button>`, `<section>`, `<hgroup>`, `<header>`, `<nav><ul></ul></nav>`, `<details>`, etc. — let the element itself carry the styling.
|
||||||
|
- Use `<section>` for thematic blocks. Do **not** use `<article>` (project convention — always reach for `<section>` instead).
|
||||||
|
- Buttons: native `<button>` or `<a role="button">`. For variants use pico's modifiers (`.secondary`, `.contrast`, `.outline`) — do not invent new button classes. For button groups use `role="group"`.
|
||||||
|
- Layout: wrap top-level blocks in `.container` for the centered viewport. Use pico's `.grid` when a simple responsive grid is needed.
|
||||||
|
- Forms: rely on pico's default `<input>`/`<textarea>`/`<select>` styling; use `aria-invalid` for validation states.
|
||||||
|
- Custom CSS belongs in `style.css` and must only cover what pico does not provide. Reference pico's CSS custom properties (`var(--pico-border-color)`, `var(--pico-muted-color)`, `var(--pico-spacing)`, `var(--pico-card-background-color)`, etc.) — never hardcode colors or spacing.
|
||||||
|
- Prefer generic descriptive class names (`muted`, `danger`, `listing`, `diary-section`) over element-specific ones. Re-use existing classes before creating new ones.
|
||||||
|
- `pico.min.css` is the served stylesheet; keep `pico.css` around only as the unminified reference.
|
||||||
|
|
||||||
## Development Priorities
|
## Development Priorities
|
||||||
|
|
||||||
|
|||||||
52
assets.go
Normal file
52
assets.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var devMode bool
|
||||||
|
|
||||||
|
// assetFS defaults to the embedded FS so package-level initializers (e.g. icon
|
||||||
|
// vars in render.go) can read assets before main() runs. initAssets() swaps it
|
||||||
|
// for os.DirFS when -dev is set.
|
||||||
|
var assetFS fs.FS = assets
|
||||||
|
|
||||||
|
func initAssets(dev bool) {
|
||||||
|
devMode = dev
|
||||||
|
if dev {
|
||||||
|
assetFS = os.DirFS(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readAsset reads a file from the asset FS (embedded in prod, live disk in dev).
|
||||||
|
func readAsset(path string) ([]byte, error) {
|
||||||
|
return fs.ReadFile(assetFS, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tmplLoader holds a lazily-parsed template. In dev mode it re-parses on every
|
||||||
|
// get() call so HTML changes are visible without recompiling.
|
||||||
|
type tmplLoader struct {
|
||||||
|
name string
|
||||||
|
patterns []string
|
||||||
|
once sync.Once
|
||||||
|
t *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTemplate creates a tmplLoader. name is the root template name passed to
|
||||||
|
// template.New; patterns are the glob/path arguments forwarded to ParseFS.
|
||||||
|
func newTemplate(name string, patterns ...string) *tmplLoader {
|
||||||
|
return &tmplLoader{name: name, patterns: patterns}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *tmplLoader) get() *template.Template {
|
||||||
|
if devMode {
|
||||||
|
return template.Must(template.New(l.name).ParseFS(assetFS, l.patterns...))
|
||||||
|
}
|
||||||
|
l.once.Do(func() {
|
||||||
|
l.t = template.Must(template.New(l.name).ParseFS(assetFS, l.patterns...))
|
||||||
|
})
|
||||||
|
return l.t
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
{{if .Photos}}
|
{{if .Photos}}
|
||||||
<div class="diary-photo-grid">
|
<section class="diary-section">
|
||||||
|
<div class="diary-photo-grid">
|
||||||
{{range .Photos}}
|
{{range .Photos}}
|
||||||
<a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a>
|
<a href="{{.URL}}" target="_blank"><img src="{{.URL}}" alt="{{.Name}}" loading="lazy"></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{{range .Days}}
|
{{range .Days}}
|
||||||
<div class="diary-section">
|
<section class="diary-section">
|
||||||
<h2>
|
<header class="diary-section-header">
|
||||||
{{if .URL}}<a href="{{.URL}}">{{.Heading}}</a>{{else}}{{.Heading}}{{end}}
|
<h2>{{if .URL}}<a href="{{.URL}}">{{.Heading}}</a>{{else}}{{.Heading}}{{end}}</h2>
|
||||||
{{if .EditURL}}<a href="{{.EditURL}}" class="btn btn-small">edit</a>{{end}}
|
{{if .EditURL}}<a href="{{.EditURL}}" role="button" class="secondary outline section-edit">edit</a>{{end}}
|
||||||
</h2>
|
</header>
|
||||||
{{if .Content}}<div class="content">{{.Content}}</div>{{end}}
|
{{if .Content}}<div class="content">{{.Content}}</div>{{end}}
|
||||||
{{if .Photos}}
|
{{if .Photos}}
|
||||||
<div class="diary-photo-grid">
|
<div class="diary-photo-grid">
|
||||||
@@ -12,5 +12,5 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{{range .Months}}
|
{{range .Months}}
|
||||||
<div class="diary-section">
|
<section class="diary-section">
|
||||||
<h2 class="diary-heading">
|
<hgroup>
|
||||||
<a href="{{.URL}}">{{.Name}}</a>
|
<h2><a href="{{.URL}}">{{.Name}}</a></h2>
|
||||||
{{if .PhotoCount}}<span class="diary-photo-count">({{.PhotoCount}} photos)</span>{{end}}
|
{{if .PhotoCount}}<p>{{.PhotoCount}} photos</p>{{end}}
|
||||||
</h2>
|
</hgroup>
|
||||||
</div>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
111
assets/page.html
111
assets/page.html
@@ -5,47 +5,67 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
<link rel="icon" href="/_/favicon.ico" />
|
<link rel="icon" href="/_/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="/_/pico.min.css" />
|
||||||
<link rel="stylesheet" href="/_/style.css" />
|
<link rel="stylesheet" href="/_/style.css" />
|
||||||
<script src="/_/global-shortcuts.js"></script>
|
<script src="/_/global-shortcuts.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<nav class="breadcrumb">
|
<div class="container-fluid">
|
||||||
<a href="/"><svg class="logo" viewBox="0 0 26.052269 26.052269" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="currentColor" stroke-linejoin="miter" transform="matrix(0.05463483,8.1519706e-6,-8.1519706e-6,0.05463483,-64.560546,-24.6949)"><rect x="1188.537" y="457.92056" width="461.87488" height="462.15189" stroke-width="20.2288"/><path d="m1348.9955 456.59572.046 309.36839" stroke-width="19.6849"/><path d="m1200.3996 765.80237 441.8362-.0659" stroke-width="19.6849"/><path d="m1648.2897 620.244-299.2012.0446" stroke-width="20.5676"/><path d="m1491.6148 909.24806-.021-136.93117" stroke-width="19.6849"/><rect x="1191.6504" y="461.66092" width="457.09634" height="457.09634" stroke-width="19.6761"/></g></svg></a>
|
<nav>
|
||||||
{{range .Crumbs}}<span class="sep">/</span
|
<nav aria-label="breadcrumb">
|
||||||
><a href="{{.URL}}">{{.Name}}</a>{{end}}
|
<ul>
|
||||||
</nav>
|
<li>
|
||||||
{{if .EditMode}}
|
<a href="/"><svg style="height: 1rem; width: 1rem; vertical-align: center;" viewBox="0 0 26.052269 26.052269" xmlns="http://www.w3.org/2000/svg"><g fill="none" stroke="currentColor" stroke-linejoin="miter" transform="matrix(0.05463483,8.1519706e-6,-8.1519706e-6,0.05463483,-64.560546,-24.6949)"><rect x="1188.537" y="457.92056" width="461.87488" height="462.15189" stroke-width="20.2288"/><path d="m1348.9955 456.59572.046 309.36839" stroke-width="19.6849"/><path d="m1200.3996 765.80237 441.8362-.0659" stroke-width="19.6849"/><path d="m1648.2897 620.244-299.2012.0446" stroke-width="20.5676"/><path d="m1491.6148 909.24806-.021-136.93117" stroke-width="19.6849"/><rect x="1191.6504" y="461.66092" width="457.09634" height="457.09634" stroke-width="19.6761"/></g></svg></a>
|
||||||
<a class="btn" href="{{.PostURL}}">CANCEL</a>
|
</li>
|
||||||
<button class="btn" type="submit" form="edit-form" data-action="save" data-key="S" title="Save (S)">SAVE</button>
|
{{range .Crumbs}}
|
||||||
{{else if .CanEdit}}
|
<li><a href="{{.URL}}">{{.Name}}</a></li>
|
||||||
<button class="btn" onclick="newPage()" title="New page (N)">NEW</button>
|
|
||||||
<a class="btn" href="?edit" title="Edit page (E)">EDIT</a>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{if .EditMode}}
|
||||||
|
<li><a href="{{.PostURL}}" class="secondary">Cancel</a></li>
|
||||||
|
<li><a href="#" onclick="document.getElementById('edit-form').submit()" data-action="save" data-key="S" title="Save (S)">Save</a></li>
|
||||||
|
{{else if .CanEdit}}
|
||||||
|
<li><a href="#" onclick="newPage()" class="secondary" title="New page (N)">New</a></li>
|
||||||
|
<li><a href="?edit" title="Edit page (E)">Edit</a></li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main class="container">
|
||||||
{{if .EditMode}}
|
{{if .EditMode}}
|
||||||
<form id="edit-form" class="edit-form" method="POST" action="{{.PostURL}}">
|
<form id="edit-form" class="edit-form" method="POST" action="{{.PostURL}}">
|
||||||
{{if ge .SectionIndex 0}}<input type="hidden" name="section" value="{{.SectionIndex}}">{{end}}
|
{{if ge .SectionIndex 0}}<input type="hidden" name="section" value="{{.SectionIndex}}">{{end}}
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<button type="button" class="btn btn-tool" data-action="bold" data-key="B" title="Bold (B)">**</button>
|
<div role="group">
|
||||||
<button type="button" class="btn btn-tool" data-action="italic" data-key="I" title="Italic (I)">*</button>
|
<button type="button" data-action="bold" data-key="B" title="Bold (B)">B</button>
|
||||||
<span class="toolbar-sep"></span>
|
<button type="button" data-action="italic" data-key="I" title="Italic (I)"><i>I</i></button>
|
||||||
<button type="button" class="btn btn-tool" data-action="h1" data-key="1" title="Heading 1 (1)">#</button>
|
</div>
|
||||||
<button type="button" class="btn btn-tool" data-action="h2" data-key="2" title="Heading 2 (2)">##</button>
|
<div role="group">
|
||||||
<button type="button" class="btn btn-tool" data-action="h3" data-key="3" title="Heading 3 (3)">###</button>
|
<button type="button" data-action="h1" data-key="1" title="Heading 1 (1)">H1</button>
|
||||||
<span class="toolbar-sep"></span>
|
<button type="button" data-action="h2" data-key="2" title="Heading 2 (2)">H2</button>
|
||||||
<button type="button" class="btn btn-tool" data-action="code" data-key="C" title="Inline code (C)">`</button>
|
<button type="button" data-action="h3" data-key="3" title="Heading 3 (3)">H3</button>
|
||||||
<button type="button" class="btn btn-tool" data-action="codeblock" data-key="K" title="Code block (K)">```</button>
|
</div>
|
||||||
<span class="toolbar-sep"></span>
|
<div role="group">
|
||||||
<button type="button" class="btn btn-tool" data-action="link" data-key="L" title="Link (L)">[]</button>
|
<button type="button" data-action="code" data-key="C" title="Inline code (C)">`</button>
|
||||||
<button type="button" class="btn btn-tool" data-action="quote" data-key="Q" title="Blockquote (Q)">></button>
|
<button type="button" data-action="codeblock" data-key="K" title="Code block (K)">```</button>
|
||||||
<button type="button" class="btn btn-tool" data-action="ul" data-key="U" title="Unordered list (U)">-</button>
|
</div>
|
||||||
<button type="button" class="btn btn-tool" data-action="ol" data-key="O" title="Ordered list (O)">1.</button>
|
<div role="group">
|
||||||
<button type="button" class="btn btn-tool" data-action="hr" data-key="R" title="Horizontal rule (R)">---</button>
|
<button type="button" data-action="link" data-key="L" title="Link (L)">[ ]</button>
|
||||||
<span class="toolbar-sep"></span>
|
<button type="button" data-action="quote" data-key="Q" title="Blockquote (Q)">></button>
|
||||||
<button type="button" class="btn btn-tool toolbar-dropdown" data-action="tbldrop" title="Table (T)">T▾</button>
|
<button type="button" data-action="ul" data-key="U" title="Unordered list (U)">•</button>
|
||||||
<button type="button" class="btn btn-tool toolbar-dropdown" data-action="datedrop" title="Insert date (D/W)">D▾</button>
|
<button type="button" data-action="ol" data-key="O" title="Ordered list (O)">1.</button>
|
||||||
|
<button type="button" data-action="hr" data-key="R" title="Horizontal rule (R)">―</button>
|
||||||
|
</div>
|
||||||
|
<div role="group">
|
||||||
|
<button type="button" class="toolbar-dropdown" data-action="tbldrop" title="Table (T)">T▾</button>
|
||||||
|
<button type="button" class="toolbar-dropdown" data-action="datedrop" title="Insert date (D/W)">D▾</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea name="content" id="editor" autofocus>{{.RawContent}}</textarea>
|
<textarea name="content" id="editor" autofocus>{{.RawContent}}</textarea>
|
||||||
</form>
|
</form>
|
||||||
@@ -55,7 +75,7 @@
|
|||||||
<script src="/_/editor.js"></script>
|
<script src="/_/editor.js"></script>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if .Content}}
|
{{if .Content}}
|
||||||
<div class="content">{{.Content}}</div>
|
<section class="content">{{.Content}}</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .SpecialContent}}
|
{{if .SpecialContent}}
|
||||||
<div class="diary">{{.SpecialContent}}</div>
|
<div class="diary">{{.SpecialContent}}</div>
|
||||||
@@ -67,19 +87,30 @@
|
|||||||
<script src="/_/sections.js"></script>
|
<script src="/_/sections.js"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Entries}}
|
{{if .Entries}}
|
||||||
<div class="listing">
|
<section class="listing">
|
||||||
<div class="listing-header">Contents</div>
|
<header>Contents</header>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col"></th>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
{{range .Entries}}
|
{{range .Entries}}
|
||||||
<div class="listing-item">
|
<tr>
|
||||||
<span class="icon">{{.Icon}}</span>
|
<td>{{.Icon}}</td>
|
||||||
<a href="{{.URL}}">{{.Name}}</a>
|
<td><a href="{{.URL}}">{{.Name}}</a></td>
|
||||||
<span class="meta">{{.Meta}}</span>
|
<td>{{.Meta}}</td>
|
||||||
</div>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
{{else if not .Content}}
|
{{else if not .Content}}
|
||||||
{{if not .SpecialContent}}
|
{{if not .SpecialContent}}
|
||||||
<p class="empty">Empty folder — <a href="?edit">[CREATE]</a></p>
|
<p class="empty">Empty folder — <a href="?edit">create</a></p>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
2835
assets/pico.css
Normal file
2835
assets/pico.css
Normal file
File diff suppressed because it is too large
Load Diff
4
assets/pico.min.css
vendored
Normal file
4
assets/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@
|
|||||||
headings.forEach(function (h, i) {
|
headings.forEach(function (h, i) {
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.href = '?edit§ion=' + (i + 1);
|
a.href = '?edit§ion=' + (i + 1);
|
||||||
a.className = 'btn btn-small section-edit';
|
a.className = 'secondary';
|
||||||
a.textContent = 'edit';
|
a.textContent = 'edit';
|
||||||
h.appendChild(a);
|
h.appendChild(a);
|
||||||
});
|
});
|
||||||
|
|||||||
418
assets/style.css
418
assets/style.css
@@ -1,417 +1 @@
|
|||||||
/* === Fonts === */
|
/* === Pico customizations === */
|
||||||
@font-face {
|
|
||||||
font-family: "Iosevka Etoile";
|
|
||||||
src: url("/_/fonts/IosevkaEtoile.woff2") format("woff2");
|
|
||||||
}
|
|
||||||
@font-face {
|
|
||||||
font-family: "Iosevka Slab";
|
|
||||||
src: url("/_/fonts/IosevkaSlab.woff2") format("woff2");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Theme === */
|
|
||||||
:root {
|
|
||||||
--bg: #2e2e2e;
|
|
||||||
--bg-panel: #434343;
|
|
||||||
--bg-panel-hover: #585858;
|
|
||||||
--text: #e6e6e6;
|
|
||||||
--text-muted: #cfcfcf;
|
|
||||||
--primary: #87458a;
|
|
||||||
--primary-hover: #d64d95;
|
|
||||||
--secondary: #c48401;
|
|
||||||
--link: #01b6c4;
|
|
||||||
--link-hover: #d6d24d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Base === */
|
|
||||||
body {
|
|
||||||
background-color: var(--bg);
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--text);
|
|
||||||
font:
|
|
||||||
1rem "Iosevka Etoile",
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Links === */
|
|
||||||
a {
|
|
||||||
color: var(--text);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content a {
|
|
||||||
color: var(--link);
|
|
||||||
}
|
|
||||||
.content a:hover {
|
|
||||||
color: var(--link-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Header === */
|
|
||||||
header {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 1px dashed var(--secondary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.breadcrumb {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 1.1em;
|
|
||||||
height: 1.1em;
|
|
||||||
vertical-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sep {
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text);
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.btn::before {
|
|
||||||
content: "[";
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
.btn::after {
|
|
||||||
content: "]";
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
.btn:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Button modifiers === */
|
|
||||||
/* For inline buttons */
|
|
||||||
.btn-small {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: normal;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For toolbars */
|
|
||||||
.btn-tool {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
padding: 0 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Main === */
|
|
||||||
main {
|
|
||||||
max-width: 860px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Markdown content === */
|
|
||||||
.content {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content h1,
|
|
||||||
.content h2,
|
|
||||||
.content h3,
|
|
||||||
.content h4,
|
|
||||||
.content h5,
|
|
||||||
.content h6 {
|
|
||||||
color: var(--text);
|
|
||||||
margin: 1.25rem 0 0.5rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
.content h1 {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
border-bottom: 1px dashed var(--secondary);
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
.content h2 {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
.content h3 {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
}
|
|
||||||
.content p {
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
}
|
|
||||||
.content ul,
|
|
||||||
.content ol {
|
|
||||||
margin: 0.75rem 0 0.75rem 1.5rem;
|
|
||||||
}
|
|
||||||
.content li {
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
.content blockquote {
|
|
||||||
border-left: 3px solid var(--secondary);
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
}
|
|
||||||
.content code {
|
|
||||||
font-family: "Iosevka Etoile", monospace;
|
|
||||||
font-size: 0.875em;
|
|
||||||
background: var(--bg-panel);
|
|
||||||
padding: 0.1em 0.35em;
|
|
||||||
}
|
|
||||||
.content pre {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
border: 1px solid var(--secondary);
|
|
||||||
padding: 1rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
}
|
|
||||||
.content pre code {
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.content table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 0.75rem 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.content th,
|
|
||||||
.content td {
|
|
||||||
border: 1px solid var(--secondary);
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.content th {
|
|
||||||
background: var(--bg-panel);
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
.content hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px dashed var(--secondary);
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
.content img {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === File listing === */
|
|
||||||
.listing {
|
|
||||||
border: 1px solid var(--secondary);
|
|
||||||
}
|
|
||||||
.listing-header {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: var(--text);
|
|
||||||
border-bottom: 1px solid var(--secondary);
|
|
||||||
}
|
|
||||||
.listing-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.6rem 1rem;
|
|
||||||
border-top: 1px solid var(--secondary);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.listing-item:hover {
|
|
||||||
background: var(--bg-panel-hover);
|
|
||||||
}
|
|
||||||
.listing-item .icon {
|
|
||||||
width: 1.25rem;
|
|
||||||
text-align: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.listing-item a {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
.listing-item .meta {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Editor toolbar === */
|
|
||||||
.editor-toolbar {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.25rem;
|
|
||||||
border: 1px solid var(--secondary);
|
|
||||||
border-bottom: none;
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
background: var(--bg-panel-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-sep {
|
|
||||||
width: 1px;
|
|
||||||
background: var(--secondary);
|
|
||||||
margin: 0 0.2rem;
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Toolbar dropdowns === */
|
|
||||||
.toolbar-dropdown {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown-menu {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
display: none;
|
|
||||||
z-index: 100;
|
|
||||||
background: var(--bg-panel);
|
|
||||||
border: 1px solid var(--secondary);
|
|
||||||
min-width: 9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown-menu.is-open {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-dropdown-item {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 0.2rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Edit form === */
|
|
||||||
.edit-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 60vh;
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--secondary);
|
|
||||||
border-top: none;
|
|
||||||
color: var(--text);
|
|
||||||
font-family: "Iosevka Slab", monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
padding: 1rem;
|
|
||||||
resize: vertical;
|
|
||||||
outline: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Diary views === */
|
|
||||||
.diary-section {
|
|
||||||
margin: 2rem 0;
|
|
||||||
padding-top: 1.5rem;
|
|
||||||
border-top: 1px dashed var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-section:first-child {
|
|
||||||
border-top: none;
|
|
||||||
padding-top: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-section h2 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--text);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-photo-count {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-photo-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
||||||
gap: 0.4rem;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-photo-grid a {
|
|
||||||
display: block;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-photo-grid img {
|
|
||||||
width: 100%;
|
|
||||||
height: 140px;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diary-section .content {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Section edit links === */
|
|
||||||
.section-edit {
|
|
||||||
margin-left: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Empty state === */
|
|
||||||
.empty {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === hr === */
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
border-top: 1px dashed var(--secondary);
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Scrollbars === */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: #111;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--primary);
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Responsive === */
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
header {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
padding: 1rem 0.75rem;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
min-height: 50vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
12
diary.go
12
diary.go
@@ -86,9 +86,9 @@ type diaryYearData struct{ Months []diaryMonthSummary }
|
|||||||
type diaryMonthData struct{ Days []diaryDaySection }
|
type diaryMonthData struct{ Days []diaryDaySection }
|
||||||
type diaryDayData struct{ Photos []diaryPhoto }
|
type diaryDayData struct{ Photos []diaryPhoto }
|
||||||
|
|
||||||
var diaryYearTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-year.html"))
|
var diaryYearTmpl = newTemplate("diary-year.html", "assets/diary/diary-year.html")
|
||||||
var diaryMonthTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-month.html"))
|
var diaryMonthTmpl = newTemplate("diary-month.html", "assets/diary/diary-month.html")
|
||||||
var diaryDayTmpl = template.Must(template.ParseFS(assets, "assets/diary/diary-day.html"))
|
var diaryDayTmpl = newTemplate("diary-day.html", "assets/diary/diary-day.html")
|
||||||
|
|
||||||
var germanWeekdays = map[time.Weekday]string{
|
var germanWeekdays = map[time.Weekday]string{
|
||||||
time.Sunday: "Sonntag",
|
time.Sunday: "Sonntag",
|
||||||
@@ -198,7 +198,7 @@ func renderDiaryYear(fsPath, urlPath string) template.HTML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := diaryYearTmpl.Execute(&buf, diaryYearData{Months: months}); err != nil {
|
if err := diaryYearTmpl.get().Execute(&buf, diaryYearData{Months: months}); err != nil {
|
||||||
log.Printf("diary year template: %v", err)
|
log.Printf("diary year template: %v", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ func renderDiaryMonth(fsPath, urlPath string) template.HTML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := diaryMonthTmpl.Execute(&buf, diaryMonthData{Days: sections}); err != nil {
|
if err := diaryMonthTmpl.get().Execute(&buf, diaryMonthData{Days: sections}); err != nil {
|
||||||
log.Printf("diary month template: %v", err)
|
log.Printf("diary month template: %v", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -329,7 +329,7 @@ func renderDiaryDay(fsPath, urlPath string) template.HTML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := diaryDayTmpl.Execute(&buf, diaryDayData{Photos: photos}); err != nil {
|
if err := diaryDayTmpl.get().Execute(&buf, diaryDayData{Photos: photos}); err != nil {
|
||||||
log.Printf("diary day template: %v", err)
|
log.Printf("diary day template: %v", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
9
main.go
9
main.go
@@ -17,7 +17,7 @@ import (
|
|||||||
//go:embed assets
|
//go:embed assets
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
var tmpl = template.Must(template.New("page.html").ParseFS(assets, "assets/page.html"))
|
var tmpl = newTemplate("page.html", "assets/page.html")
|
||||||
|
|
||||||
// specialPage is the result returned by a pageTypeHandler.
|
// specialPage is the result returned by a pageTypeHandler.
|
||||||
// Content is injected into the page after the standard markdown content.
|
// Content is injected into the page after the standard markdown content.
|
||||||
@@ -41,8 +41,11 @@ func main() {
|
|||||||
wikiDir := flag.String("dir", "./wiki", "wiki root directory")
|
wikiDir := flag.String("dir", "./wiki", "wiki root directory")
|
||||||
user := flag.String("user", "", "basic auth username (empty = no auth)")
|
user := flag.String("user", "", "basic auth username (empty = no auth)")
|
||||||
pass := flag.String("pass", "", "basic auth password")
|
pass := flag.String("pass", "", "basic auth password")
|
||||||
|
dev := flag.Bool("dev", false, "serve assets from disk (no recompile needed for HTML/CSS changes)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
initAssets(*dev)
|
||||||
|
|
||||||
root, err := filepath.Abs(*wikiDir)
|
root, err := filepath.Abs(*wikiDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -53,7 +56,7 @@ func main() {
|
|||||||
|
|
||||||
h := &handler{root: root, user: *user, pass: *pass}
|
h := &handler{root: root, user: *user, pass: *pass}
|
||||||
|
|
||||||
staticFS, _ := fs.Sub(assets, "assets")
|
staticFS, _ := fs.Sub(assetFS, "assets")
|
||||||
http.Handle("/_/", http.StripPrefix("/_/", http.FileServer(http.FS(staticFS))))
|
http.Handle("/_/", http.StripPrefix("/_/", http.FileServer(http.FS(staticFS))))
|
||||||
http.Handle("/", h)
|
http.Handle("/", h)
|
||||||
|
|
||||||
@@ -185,7 +188,7 @@ func (h *handler) serveDir(w http.ResponseWriter, r *http.Request, urlPath, fsPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.get().Execute(w, data); err != nil {
|
||||||
log.Printf("template error: %v", err)
|
log.Printf("template error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ func listEntries(fsPath, urlPath string) []entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readIcon(name string) template.HTML {
|
func readIcon(name string) template.HTML {
|
||||||
b, _ := assets.ReadFile("assets/icons/" + name + ".svg")
|
b, _ := readAsset("assets/icons/" + name + ".svg")
|
||||||
return template.HTML(strings.TrimSpace(string(b)))
|
return template.HTML(strings.TrimSpace(string(b)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user