getfilepress 0.1.9 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/package.json +6 -3
- package/packages/app/src/lib/genie/GeniePanel.svelte +286 -21
- package/packages/app/src/lib/genie/ops.ts +7 -1
- package/packages/app/src/lib/genie/store.ts +54 -1
- package/packages/app/src/lib/theme-entry.ts +2 -1
- package/packages/app/src/routes/+error.svelte +38 -0
- package/packages/app/src/routes/posts/[slug]/+page.svelte +2 -1
- package/packages/app/src/site-theme.d.ts +3 -0
- package/packages/app/vite-plugin-genie.ts +45 -1
- package/packages/app/vite.config.ts +38 -7
- package/packages/core/src/lib/components/PostCard.svelte +2 -1
- package/packages/core/src/lib/config.ts +47 -1
- package/packages/core/src/lib/content/parse.ts +2 -0
- package/packages/core/src/lib/content/types.ts +2 -0
- package/packages/core/src/lib/format.ts +11 -0
- package/packages/core/src/lib/index.ts +7 -3
- package/packages/core/src/lib/redirects.ts +88 -0
- package/packages/core/src/lib/server.ts +9 -2
- package/packages/core/src/lib/styles/presets/essay.css +2 -0
- package/packages/core/src/lib/styles/presets/folio.css +27 -0
- package/packages/core/src/lib/styles/presets/ink.css +28 -0
- package/packages/core/src/lib/styles/theme.css +37 -0
- package/packages/import/src/cli.ts +1 -0
- package/packages/import/src/extract.ts +15 -2
- package/packages/import/src/ir.ts +2 -0
- package/packages/import/src/ollama.ts +119 -27
- package/packages/import/src/redirects.ts +17 -0
- package/packages/import/src/write-site.ts +25 -3
- package/scripts/copy-path-mounts.mjs +30 -1
- package/scripts/create-site.mjs +1 -0
- package/scripts/filepress.mjs +24 -14
- package/scripts/new-post.ts +125 -0
- package/scripts/preview.mjs +95 -0
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ export default defineFilepressConfig({
|
|
|
96
96
|
{ label: 'RSS', href: '/rss.xml' },
|
|
97
97
|
{ label: 'GitHub', href: 'https://github.com/acme/site', icon: 'github' }
|
|
98
98
|
],
|
|
99
|
+
theme: 'essay', // or 'ink' | 'folio'; site theme.css still wins last
|
|
99
100
|
newsletter: {
|
|
100
101
|
url: 'https://buttondown.email/me',
|
|
101
102
|
blurb: 'Occasional notes.',
|
|
@@ -124,8 +125,9 @@ paths: [
|
|
|
124
125
|
|
|
125
126
|
## Theming
|
|
126
127
|
|
|
127
|
-
The engine ships a default Essay look
|
|
128
|
-
`
|
|
128
|
+
The engine ships a default Essay look, plus two token presets (`ink`, `folio`)
|
|
129
|
+
via config `theme`. To restyle a site, add `theme.css` next to
|
|
130
|
+
`filepress.config.ts`. It loads last, so you can override CSS
|
|
129
131
|
variables and structural classes without forking the app.
|
|
130
132
|
|
|
131
133
|
In local `filepress dev` / `pnpm dev`, **Genie Mode** (floating FAB) is a design cockpit:
|
|
@@ -147,7 +149,14 @@ Token and class reference: [`docs/THEME.md`](docs/THEME.md).
|
|
|
147
149
|
|
|
148
150
|
## Writing a post
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
From the site root (or with `--site` / `--root` in this monorepo):
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
filepress new "My Post"
|
|
156
|
+
filepress new "My Post" --draft
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
That writes `posts/YYYY-MM-DD-my-post.md`. Or add a file by hand:
|
|
151
160
|
|
|
152
161
|
```markdown
|
|
153
162
|
---
|
|
@@ -174,7 +183,7 @@ Body content in **Markdown**.
|
|
|
174
183
|
| `draft` | no | Hidden from production listings/feeds/sitemap; still builds at its URL. Listed with a Draft label under `pnpm dev` (localhost). |
|
|
175
184
|
| `updated` | no | Shown when different from `date`. |
|
|
176
185
|
|
|
177
|
-
**Images:** put files in `static/images/posts/<slug>/` and reference them as `/images/posts/<slug>/photo.jpg`.
|
|
186
|
+
**Images:** put files in `static/images/posts/<slug>/` and reference them as `/images/posts/<slug>/photo.jpg`. The demo site’s [Images in posts](sites/demo/posts/images-in-posts.md) post is a worked example.
|
|
178
187
|
|
|
179
188
|
**Captions:** an image alone on a line becomes a `<figure>`; the title attribute is the caption:
|
|
180
189
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getfilepress",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "FilePress — file-based Markdown blog engine. Link this package from a content-only site (config + posts), then run `filepress build`.",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
36
|
"scripts/filepress.mjs",
|
|
37
|
+
"scripts/new-post.ts",
|
|
38
|
+
"scripts/preview.mjs",
|
|
37
39
|
"scripts/copy-path-mounts.mjs",
|
|
38
40
|
"scripts/create-site.mjs",
|
|
39
41
|
"scripts/ensure-lease.mjs",
|
|
@@ -49,7 +51,7 @@
|
|
|
49
51
|
"scripts": {
|
|
50
52
|
"postinstall": "node scripts/postinstall.mjs",
|
|
51
53
|
"filepress": "node scripts/filepress.mjs",
|
|
52
|
-
"test": "pnpm --filter @filepress/core test && pnpm --filter @filepress/import test && pnpm --filter @filepress/app test && tsx --test scripts/sync-sibling-sites.test.ts",
|
|
54
|
+
"test": "pnpm --filter @filepress/core test && pnpm --filter @filepress/import test && pnpm --filter @filepress/app test && tsx --test scripts/sync-sibling-sites.test.ts scripts/new-post.test.ts scripts/preview.test.ts",
|
|
53
55
|
"check": "pnpm filepress check --site demo",
|
|
54
56
|
"build": "pnpm filepress build --site demo",
|
|
55
57
|
"build:demo": "pnpm filepress build --site demo",
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
"ship": "pnpm build:www && wrangler pages deploy sites/getfilepress/build --project-name getfilepress",
|
|
58
60
|
"deploy:www": "pnpm ship",
|
|
59
61
|
"dev": "node scripts/ensure-lease.mjs demo 5179 && pnpm filepress dev --site demo --host 0.0.0.0",
|
|
62
|
+
"serve": "node scripts/ensure-lease.mjs demo 5179 && pnpm filepress dev --site demo --host 0.0.0.0",
|
|
60
63
|
"dev:demo": "node scripts/ensure-lease.mjs demo 5179 && pnpm filepress dev --site demo --host 0.0.0.0",
|
|
61
64
|
"dev:www": "node scripts/ensure-lease.mjs getfilepress 5180 && pnpm filepress dev --site getfilepress --host 0.0.0.0",
|
|
62
65
|
"create-site": "node scripts/create-site.mjs",
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
"gray-matter": "^4.0.3",
|
|
80
83
|
"highlight.js": "^11.11.1",
|
|
81
84
|
"linkedom": "^0.18.12",
|
|
82
|
-
"ollanet": "^0.
|
|
85
|
+
"ollanet": "^0.6.6",
|
|
83
86
|
"rehype-autolink-headings": "^7.1.0",
|
|
84
87
|
"rehype-highlight": "^7.0.2",
|
|
85
88
|
"rehype-raw": "^7.0.0",
|
|
@@ -66,6 +66,33 @@
|
|
|
66
66
|
let cfgLede = $state('');
|
|
67
67
|
let cfgTagline = $state('');
|
|
68
68
|
let cfgLogo = $state('');
|
|
69
|
+
let renamingId = $state<string | null>(null);
|
|
70
|
+
let renameDraft = $state('');
|
|
71
|
+
let jobNote = $state('');
|
|
72
|
+
let jobSecs = $state(0);
|
|
73
|
+
let jobTimer: ReturnType<typeof setInterval> | null = null;
|
|
74
|
+
|
|
75
|
+
function formatWait(secs: number): string {
|
|
76
|
+
const m = Math.floor(secs / 60);
|
|
77
|
+
const r = secs % 60;
|
|
78
|
+
return m ? `${m}m ${String(r).padStart(2, '0')}s` : `${r}s`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function beginJob(note: string) {
|
|
82
|
+
jobNote = note;
|
|
83
|
+
jobSecs = 0;
|
|
84
|
+
if (jobTimer) clearInterval(jobTimer);
|
|
85
|
+
jobTimer = setInterval(() => {
|
|
86
|
+
jobSecs += 1;
|
|
87
|
+
}, 1000);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function endJob() {
|
|
91
|
+
if (jobTimer) clearInterval(jobTimer);
|
|
92
|
+
jobTimer = null;
|
|
93
|
+
jobNote = '';
|
|
94
|
+
jobSecs = 0;
|
|
95
|
+
}
|
|
69
96
|
|
|
70
97
|
async function api(path: string, init?: RequestInit) {
|
|
71
98
|
const res = await fetch(`/__filepress/genie${path}`, {
|
|
@@ -129,6 +156,89 @@
|
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
158
|
|
|
159
|
+
function versionWhen(v: VersionRow) {
|
|
160
|
+
if (v.id === 'baseline') return 'pre-Genie snapshot';
|
|
161
|
+
const d = new Date(v.createdAt);
|
|
162
|
+
if (Number.isNaN(d.getTime())) return v.createdAt;
|
|
163
|
+
return d.toLocaleString(undefined, {
|
|
164
|
+
month: 'short',
|
|
165
|
+
day: 'numeric',
|
|
166
|
+
hour: '2-digit',
|
|
167
|
+
minute: '2-digit'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function startRename(v: VersionRow) {
|
|
172
|
+
renamingId = v.id;
|
|
173
|
+
renameDraft = v.label;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function toggleStar(v: VersionRow) {
|
|
177
|
+
if (v.id === 'baseline') return;
|
|
178
|
+
loading = true;
|
|
179
|
+
error = '';
|
|
180
|
+
try {
|
|
181
|
+
await api('/star', {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body: JSON.stringify({ versionId: v.id, starred: !v.starred })
|
|
184
|
+
});
|
|
185
|
+
await refresh();
|
|
186
|
+
} catch (e) {
|
|
187
|
+
error = e instanceof Error ? e.message : String(e);
|
|
188
|
+
loading = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function saveLabel(v: VersionRow) {
|
|
193
|
+
const label = renameDraft.trim();
|
|
194
|
+
if (!label || label === v.label) {
|
|
195
|
+
renamingId = null;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
loading = true;
|
|
199
|
+
error = '';
|
|
200
|
+
try {
|
|
201
|
+
await api('/label', { method: 'POST', body: JSON.stringify({ versionId: v.id, label }) });
|
|
202
|
+
renamingId = null;
|
|
203
|
+
await refresh();
|
|
204
|
+
} catch (e) {
|
|
205
|
+
error = e instanceof Error ? e.message : String(e);
|
|
206
|
+
loading = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function duplicate(v: VersionRow) {
|
|
211
|
+
loading = true;
|
|
212
|
+
error = '';
|
|
213
|
+
try {
|
|
214
|
+
await api('/duplicate', { method: 'POST', body: JSON.stringify({ versionId: v.id }) });
|
|
215
|
+
await refresh();
|
|
216
|
+
} catch (e) {
|
|
217
|
+
error = e instanceof Error ? e.message : String(e);
|
|
218
|
+
loading = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function removeVersion(v: VersionRow) {
|
|
223
|
+
if (v.id === 'baseline') return;
|
|
224
|
+
if (
|
|
225
|
+
!confirm(
|
|
226
|
+
`Delete “${v.label}”? This only removes the snapshot under .filepress-genie/. The working tree stays as it is.`
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
loading = true;
|
|
232
|
+
error = '';
|
|
233
|
+
try {
|
|
234
|
+
await api('/delete', { method: 'POST', body: JSON.stringify({ versionId: v.id }) });
|
|
235
|
+
await refresh();
|
|
236
|
+
} catch (e) {
|
|
237
|
+
error = e instanceof Error ? e.message : String(e);
|
|
238
|
+
loading = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
132
242
|
async function applyStock() {
|
|
133
243
|
loading = true;
|
|
134
244
|
error = '';
|
|
@@ -253,6 +363,11 @@
|
|
|
253
363
|
async function runInspire() {
|
|
254
364
|
loading = true;
|
|
255
365
|
error = '';
|
|
366
|
+
beginJob(
|
|
367
|
+
useLlm
|
|
368
|
+
? `Inspire + ${selectedModel || 'Ollama'} — first load of a 12B model can take several minutes`
|
|
369
|
+
: 'Crawling inspiration URLs'
|
|
370
|
+
);
|
|
256
371
|
try {
|
|
257
372
|
await api('/inspire', {
|
|
258
373
|
method: 'POST',
|
|
@@ -270,12 +385,17 @@
|
|
|
270
385
|
} catch (e) {
|
|
271
386
|
error = e instanceof Error ? e.message : String(e);
|
|
272
387
|
loading = false;
|
|
388
|
+
} finally {
|
|
389
|
+
endJob();
|
|
273
390
|
}
|
|
274
391
|
}
|
|
275
392
|
|
|
276
393
|
async function runRefine() {
|
|
277
394
|
loading = true;
|
|
278
395
|
error = '';
|
|
396
|
+
beginJob(
|
|
397
|
+
`Asking ${selectedModel || 'Ollama'} — first load of a 12B model can take several minutes`
|
|
398
|
+
);
|
|
279
399
|
try {
|
|
280
400
|
await api('/refine', {
|
|
281
401
|
method: 'POST',
|
|
@@ -289,6 +409,8 @@
|
|
|
289
409
|
} catch (e) {
|
|
290
410
|
error = e instanceof Error ? e.message : String(e);
|
|
291
411
|
loading = false;
|
|
412
|
+
} finally {
|
|
413
|
+
endJob();
|
|
292
414
|
}
|
|
293
415
|
}
|
|
294
416
|
|
|
@@ -324,12 +446,18 @@
|
|
|
324
446
|
<strong>Genie</strong>
|
|
325
447
|
<p class="genie-tagline">Try a look → activate → commit baked files</p>
|
|
326
448
|
</div>
|
|
327
|
-
<button type="button" class="genie-x" onclick={() =>
|
|
449
|
+
<button type="button" class="genie-x" onclick={() => { open = false; endJob(); }}>Close</button>
|
|
328
450
|
</header>
|
|
329
451
|
|
|
330
452
|
{#if error}
|
|
331
453
|
<p class="genie-err">{error}</p>
|
|
332
454
|
{/if}
|
|
455
|
+
{#if jobNote}
|
|
456
|
+
<p class="genie-progress" aria-live="polite">
|
|
457
|
+
{jobNote} · {formatWait(jobSecs)} elapsed. Watch the <code>filepress dev</code> terminal for
|
|
458
|
+
“still generating…”.
|
|
459
|
+
</p>
|
|
460
|
+
{/if}
|
|
333
461
|
|
|
334
462
|
{#if health}
|
|
335
463
|
<div class="genie-shell">
|
|
@@ -423,10 +551,13 @@
|
|
|
423
551
|
disabled={loading || scanning || !hostIsReady() || !refinePrompt.trim()}
|
|
424
552
|
onclick={runRefine}
|
|
425
553
|
>
|
|
426
|
-
{loading ?
|
|
554
|
+
{loading ? `Asking Ollama… ${formatWait(jobSecs)}` : 'Refine & activate'}
|
|
427
555
|
</button>
|
|
428
556
|
<p class="genie-muted">
|
|
429
|
-
|
|
557
|
+
<code>gemma4:12b</code> often spends the first few minutes loading into VRAM — that is
|
|
558
|
+
normal. Progress prints in the filepress terminal every 15s. Raise the budget with
|
|
559
|
+
<code>FILEPRESS_OLLAMA_TIMEOUT_MS</code> (default 10 minutes). Tip: set
|
|
560
|
+
<code>FILEPRESS_OLLAMA_MODEL</code> / <code>OLLAMA_HOST</code> for defaults;
|
|
430
561
|
<a href="https://ollanet.dev" target="_blank" rel="noreferrer">ollanet</a> finds other
|
|
431
562
|
boxes. Finetuna can tune a named variant.
|
|
432
563
|
</p>
|
|
@@ -605,25 +736,82 @@
|
|
|
605
736
|
<h3>Versions</h3>
|
|
606
737
|
<p class="genie-howto">
|
|
607
738
|
Every Genie action saves a snapshot under <code>.filepress-genie/</code> (gitignored).
|
|
608
|
-
|
|
609
|
-
the pre-Genie look.
|
|
739
|
+
Star keepers, rename, duplicate, then activate to bake into the working tree.
|
|
740
|
+
<code>baseline</code> is the pre-Genie look and cannot be deleted.
|
|
610
741
|
</p>
|
|
611
742
|
<p class="genie-muted">
|
|
612
743
|
Active: {health.active?.versionId ?? '(none)'}
|
|
744
|
+
{#if health.versions.length}
|
|
745
|
+
· {health.versions.length} saved
|
|
746
|
+
{/if}
|
|
613
747
|
</p>
|
|
614
748
|
<ul class="genie-versions">
|
|
615
749
|
{#each health.versions as v (v.id)}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
750
|
+
{@const isActive = health.active?.versionId === v.id}
|
|
751
|
+
<li class:active={isActive} class:starred={v.starred}>
|
|
752
|
+
<div class="genie-ver-head">
|
|
753
|
+
<button
|
|
754
|
+
type="button"
|
|
755
|
+
class="genie-star"
|
|
756
|
+
class:on={v.starred}
|
|
757
|
+
disabled={loading || v.id === 'baseline'}
|
|
758
|
+
aria-pressed={v.starred}
|
|
759
|
+
aria-label={v.starred ? 'Unstar version' : 'Star version'}
|
|
760
|
+
onclick={() => toggleStar(v)}
|
|
761
|
+
>
|
|
762
|
+
{v.starred ? '★' : '☆'}
|
|
763
|
+
</button>
|
|
764
|
+
{#if renamingId === v.id}
|
|
765
|
+
<input
|
|
766
|
+
class="genie-rename"
|
|
767
|
+
type="text"
|
|
768
|
+
bind:value={renameDraft}
|
|
769
|
+
disabled={loading}
|
|
770
|
+
aria-label="Version label"
|
|
771
|
+
onkeydown={(e) => {
|
|
772
|
+
if (e.key === 'Enter') void saveLabel(v);
|
|
773
|
+
if (e.key === 'Escape') renamingId = null;
|
|
774
|
+
}}
|
|
775
|
+
/>
|
|
776
|
+
{:else}
|
|
777
|
+
<strong>{v.label}</strong>
|
|
778
|
+
{/if}
|
|
779
|
+
{#if isActive}
|
|
780
|
+
<span class="genie-ver-badge">Active</span>
|
|
781
|
+
{/if}
|
|
782
|
+
</div>
|
|
783
|
+
<p class="genie-ver-when">{versionWhen(v)}</p>
|
|
784
|
+
<div class="genie-ver-actions">
|
|
785
|
+
{#if renamingId === v.id}
|
|
786
|
+
<button type="button" disabled={loading} onclick={() => saveLabel(v)}>
|
|
787
|
+
Save name
|
|
788
|
+
</button>
|
|
789
|
+
<button type="button" disabled={loading} onclick={() => (renamingId = null)}>
|
|
790
|
+
Cancel
|
|
791
|
+
</button>
|
|
792
|
+
{:else}
|
|
793
|
+
<button
|
|
794
|
+
type="button"
|
|
795
|
+
disabled={loading || isActive}
|
|
796
|
+
onclick={() => activate(v.id)}
|
|
797
|
+
>
|
|
798
|
+
Activate
|
|
799
|
+
</button>
|
|
800
|
+
<button type="button" disabled={loading} onclick={() => startRename(v)}>
|
|
801
|
+
Rename
|
|
802
|
+
</button>
|
|
803
|
+
<button type="button" disabled={loading} onclick={() => duplicate(v)}>
|
|
804
|
+
Duplicate
|
|
805
|
+
</button>
|
|
806
|
+
<button
|
|
807
|
+
type="button"
|
|
808
|
+
disabled={loading || v.id === 'baseline' || isActive}
|
|
809
|
+
onclick={() => removeVersion(v)}
|
|
810
|
+
>
|
|
811
|
+
Delete
|
|
812
|
+
</button>
|
|
813
|
+
{/if}
|
|
814
|
+
</div>
|
|
627
815
|
</li>
|
|
628
816
|
{/each}
|
|
629
817
|
</ul>
|
|
@@ -816,6 +1004,16 @@
|
|
|
816
1004
|
flex-shrink: 0;
|
|
817
1005
|
}
|
|
818
1006
|
|
|
1007
|
+
.genie-progress {
|
|
1008
|
+
margin: 0 0.25rem 0.65rem;
|
|
1009
|
+
padding: 0.5rem 0.65rem;
|
|
1010
|
+
border-radius: 6px;
|
|
1011
|
+
border: 1px solid color-mix(in srgb, var(--accent, #f0c040) 40%, var(--rule, #444));
|
|
1012
|
+
font-size: 0.82rem;
|
|
1013
|
+
color: var(--ink-soft, #ccc);
|
|
1014
|
+
flex-shrink: 0;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
819
1017
|
.genie-row {
|
|
820
1018
|
display: grid;
|
|
821
1019
|
gap: 0.35rem;
|
|
@@ -898,19 +1096,86 @@
|
|
|
898
1096
|
margin: 0.5rem 0;
|
|
899
1097
|
padding: 0;
|
|
900
1098
|
display: grid;
|
|
901
|
-
gap: 0.
|
|
1099
|
+
gap: 0.55rem;
|
|
902
1100
|
}
|
|
903
1101
|
|
|
904
1102
|
.genie-versions li {
|
|
1103
|
+
display: grid;
|
|
1104
|
+
gap: 0.3rem;
|
|
1105
|
+
padding: 0.5rem 0.55rem;
|
|
1106
|
+
border: 1px solid var(--rule, #444);
|
|
1107
|
+
border-radius: 8px;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.genie-versions li.active {
|
|
1111
|
+
border-color: color-mix(in srgb, var(--accent, #f0c040) 55%, var(--rule, #444));
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.genie-ver-head {
|
|
905
1115
|
display: flex;
|
|
906
1116
|
align-items: center;
|
|
907
|
-
|
|
908
|
-
|
|
1117
|
+
gap: 0.4rem;
|
|
1118
|
+
min-width: 0;
|
|
909
1119
|
}
|
|
910
1120
|
|
|
911
|
-
.genie-
|
|
912
|
-
|
|
1121
|
+
.genie-ver-head strong {
|
|
1122
|
+
flex: 1 1 auto;
|
|
1123
|
+
min-width: 0;
|
|
1124
|
+
overflow: hidden;
|
|
1125
|
+
text-overflow: ellipsis;
|
|
1126
|
+
white-space: nowrap;
|
|
1127
|
+
font-size: 0.84rem;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.genie-star {
|
|
1131
|
+
flex: 0 0 auto;
|
|
1132
|
+
width: 1.7rem;
|
|
1133
|
+
padding: 0.15rem 0;
|
|
1134
|
+
border: 1px solid transparent;
|
|
1135
|
+
background: none;
|
|
1136
|
+
color: var(--ink-soft, #999);
|
|
1137
|
+
font-size: 0.95rem;
|
|
1138
|
+
line-height: 1;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.genie-star.on {
|
|
1142
|
+
color: var(--accent, #f0c040);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.genie-ver-badge {
|
|
1146
|
+
flex: 0 0 auto;
|
|
1147
|
+
font-size: 0.62rem;
|
|
1148
|
+
letter-spacing: 0.08em;
|
|
1149
|
+
text-transform: uppercase;
|
|
913
1150
|
color: var(--accent, #f0c040);
|
|
1151
|
+
border: 1px solid color-mix(in srgb, var(--accent, #f0c040) 45%, var(--rule, #444));
|
|
1152
|
+
border-radius: 999px;
|
|
1153
|
+
padding: 0.08rem 0.4rem;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.genie-rename {
|
|
1157
|
+
flex: 1 1 auto;
|
|
1158
|
+
min-width: 0;
|
|
1159
|
+
box-sizing: border-box;
|
|
1160
|
+
font: inherit;
|
|
1161
|
+
font-size: 0.82rem;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.genie-ver-when {
|
|
1165
|
+
margin: 0;
|
|
1166
|
+
font-size: 0.72rem;
|
|
1167
|
+
color: var(--ink-soft, #999);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.genie-ver-actions {
|
|
1171
|
+
display: flex;
|
|
1172
|
+
flex-wrap: wrap;
|
|
1173
|
+
gap: 0.3rem;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
.genie-ver-actions button {
|
|
1177
|
+
padding: 0.28rem 0.5rem;
|
|
1178
|
+
font-size: 0.72rem;
|
|
914
1179
|
}
|
|
915
1180
|
|
|
916
1181
|
.genie-linkish {
|
|
@@ -313,6 +313,7 @@ function stubSiteIr(siteRoot: string): SiteIR {
|
|
|
313
313
|
nav: [],
|
|
314
314
|
topics: [],
|
|
315
315
|
lede: null,
|
|
316
|
+
homeMarkdown: null,
|
|
316
317
|
notes: [],
|
|
317
318
|
assets: []
|
|
318
319
|
};
|
|
@@ -405,13 +406,17 @@ export async function refineWithOllama(
|
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
const seed = loadWorkingBrief(siteRoot);
|
|
409
|
+
const logLabel = `filepress genie: refine ${model} @ ${host}`;
|
|
410
|
+
console.log(`${logLabel}: prompt “${prompt.slice(0, 80)}${prompt.length > 80 ? '…' : ''}”`);
|
|
408
411
|
const brief = await generateDesignBrief({
|
|
409
412
|
host,
|
|
410
413
|
model,
|
|
411
414
|
ir: stubSiteIr(siteRoot),
|
|
412
415
|
inspireSummaries: [`Author steer: ${prompt}`],
|
|
413
416
|
inspireSignals: [],
|
|
414
|
-
seed
|
|
417
|
+
seed,
|
|
418
|
+
strictParse: true,
|
|
419
|
+
logLabel
|
|
415
420
|
});
|
|
416
421
|
const themeCss = themeCssFromBrief(brief);
|
|
417
422
|
const meta = writeSnapshot(siteRoot, {
|
|
@@ -423,6 +428,7 @@ export async function refineWithOllama(
|
|
|
423
428
|
llm: { used: true, model, host },
|
|
424
429
|
steers: [{ type: 'refine', prompt }]
|
|
425
430
|
});
|
|
431
|
+
console.log(`${logLabel}: wrote ${meta.id}`);
|
|
426
432
|
const active =
|
|
427
433
|
opts.activate !== false ? activateVersion(siteRoot, meta.id) : getActive(siteRoot);
|
|
428
434
|
return { meta, active, brief, llm: { used: true, model, host } };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
copyFileSync,
|
|
3
|
+
cpSync,
|
|
3
4
|
existsSync,
|
|
4
5
|
mkdirSync,
|
|
5
6
|
readFileSync,
|
|
@@ -62,7 +63,10 @@ export function listVersions(siteRoot: string): GenieVersionMeta[] {
|
|
|
62
63
|
const meta = readJson<GenieVersionMeta | null>(join(dir, id, 'meta.json'), null);
|
|
63
64
|
if (meta) out.push(meta);
|
|
64
65
|
}
|
|
65
|
-
return out.sort((a, b) =>
|
|
66
|
+
return out.sort((a, b) => {
|
|
67
|
+
if (a.starred !== b.starred) return a.starred ? -1 : 1;
|
|
68
|
+
return b.createdAt.localeCompare(a.createdAt);
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
export function readVersionBrief(siteRoot: string, id: string): DesignBrief | null {
|
|
@@ -161,6 +165,55 @@ export function activateVersion(siteRoot: string, versionId: string): GenieActiv
|
|
|
161
165
|
return active;
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
export function readVersionMeta(siteRoot: string, versionId: string): GenieVersionMeta {
|
|
169
|
+
const meta = readJson<GenieVersionMeta | null>(join(versionPath(siteRoot, versionId), 'meta.json'), null);
|
|
170
|
+
if (!meta) throw new Error(`Unknown Genie version: ${versionId}`);
|
|
171
|
+
return meta;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function updateVersionMeta(
|
|
175
|
+
siteRoot: string,
|
|
176
|
+
versionId: string,
|
|
177
|
+
patch: { label?: string; starred?: boolean }
|
|
178
|
+
): GenieVersionMeta {
|
|
179
|
+
const meta = readVersionMeta(siteRoot, versionId);
|
|
180
|
+
if (typeof patch.label === 'string') {
|
|
181
|
+
const label = patch.label.trim();
|
|
182
|
+
if (!label) throw new Error('label cannot be empty');
|
|
183
|
+
meta.label = label.slice(0, 80);
|
|
184
|
+
}
|
|
185
|
+
if (typeof patch.starred === 'boolean') {
|
|
186
|
+
if (versionId === 'baseline' && patch.starred === false) {
|
|
187
|
+
throw new Error('Cannot unstar the baseline version');
|
|
188
|
+
}
|
|
189
|
+
meta.starred = patch.starred;
|
|
190
|
+
}
|
|
191
|
+
writeJson(join(versionPath(siteRoot, versionId), 'meta.json'), meta);
|
|
192
|
+
return meta;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Copy a snapshot folder to a new id. Does not activate. */
|
|
196
|
+
export function duplicateVersion(siteRoot: string, sourceId: string, label?: string): GenieVersionMeta {
|
|
197
|
+
const src = versionPath(siteRoot, sourceId);
|
|
198
|
+
if (!existsSync(join(src, 'theme.css'))) {
|
|
199
|
+
throw new Error(`Unknown Genie version: ${sourceId}`);
|
|
200
|
+
}
|
|
201
|
+
const source = readVersionMeta(siteRoot, sourceId);
|
|
202
|
+
const id = newVersionId();
|
|
203
|
+
const dest = versionPath(siteRoot, id);
|
|
204
|
+
cpSync(src, dest, { recursive: true });
|
|
205
|
+
const meta: GenieVersionMeta = {
|
|
206
|
+
...source,
|
|
207
|
+
id,
|
|
208
|
+
createdAt: new Date().toISOString(),
|
|
209
|
+
parentId: sourceId,
|
|
210
|
+
label: (label?.trim() || `Copy of ${source.label}`).slice(0, 80),
|
|
211
|
+
starred: false
|
|
212
|
+
};
|
|
213
|
+
writeJson(join(dest, 'meta.json'), meta);
|
|
214
|
+
return meta;
|
|
215
|
+
}
|
|
216
|
+
|
|
164
217
|
export function deleteVersion(siteRoot: string, versionId: string) {
|
|
165
218
|
if (versionId === 'baseline') {
|
|
166
219
|
throw new Error('Cannot delete the baseline version');
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Essay chrome + fonts, then site theme.
|
|
2
|
+
* Essay chrome + fonts, then a named preset, then the site theme.
|
|
3
3
|
* Site rules use higher-specificity selectors (`:root:root`, `header.site-header …`)
|
|
4
4
|
* so they win even if Vite emits Essay CSS after the site sheet.
|
|
5
5
|
*/
|
|
6
6
|
import '@filepress/core/theme';
|
|
7
|
+
import '$site-preset';
|
|
7
8
|
import '$site-theme';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/stores';
|
|
3
|
+
import { postsIndexPath } from '@filepress/core';
|
|
4
|
+
import config from '$site-config';
|
|
5
|
+
|
|
6
|
+
const homeHref = $derived(postsIndexPath(config));
|
|
7
|
+
const missing = $derived($page.status === 404);
|
|
8
|
+
const detail = $derived.by(() => {
|
|
9
|
+
const raw = $page.error?.message?.trim() ?? '';
|
|
10
|
+
if (!raw || /^not found$/i.test(raw)) return '';
|
|
11
|
+
return raw;
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<svelte:head>
|
|
16
|
+
<title>{missing ? 'Not found' : 'Error'} — {config.title}</title>
|
|
17
|
+
<meta name="robots" content="noindex" />
|
|
18
|
+
</svelte:head>
|
|
19
|
+
|
|
20
|
+
<section class="empty-state error-page">
|
|
21
|
+
<p class="error-code">{$page.status}</p>
|
|
22
|
+
<h1>{missing ? 'This page is not here.' : 'Something went wrong.'}</h1>
|
|
23
|
+
<p>
|
|
24
|
+
{detail ||
|
|
25
|
+
(missing
|
|
26
|
+
? 'The address may have changed, or the file was never published.'
|
|
27
|
+
: 'The build or request failed. Try the index, or check the terminal if you are in dev.')}
|
|
28
|
+
</p>
|
|
29
|
+
<p class="error-actions">
|
|
30
|
+
{#if config.homePage}
|
|
31
|
+
<a href="/">Home</a>
|
|
32
|
+
<span aria-hidden="true"> · </span>
|
|
33
|
+
<a href={homeHref}>Posts</a>
|
|
34
|
+
{:else}
|
|
35
|
+
<a href={homeHref}>Back to the index</a>
|
|
36
|
+
{/if}
|
|
37
|
+
</p>
|
|
38
|
+
</section>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { PageData } from './$types';
|
|
3
|
-
import { Newsletter, absoluteUrl, formatDate, ogImageUrl } from '@filepress/core';
|
|
3
|
+
import { Newsletter, absoluteUrl, formatDate, formatReadingTime, ogImageUrl } from '@filepress/core';
|
|
4
4
|
import config from '$site-config';
|
|
5
5
|
|
|
6
6
|
let { data }: { data: PageData } = $props();
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
{#if showUpdated}
|
|
52
52
|
· updated <time datetime={post.updated}>{formatDate(post.updated!)}</time>
|
|
53
53
|
{/if}
|
|
54
|
+
· <span class="reading-time">{formatReadingTime(post.readingMinutes)}</span>
|
|
54
55
|
</p>
|
|
55
56
|
{#if data.tags.length}
|
|
56
57
|
<ul class="tag-list" style="margin-top:0.75rem">
|