getfilepress 0.1.9 → 0.1.11
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 +290 -21
- package/packages/app/src/lib/genie/ops.ts +11 -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 +332 -77
- 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.11",
|
|
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,17 @@
|
|
|
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
|
+
The raw Ollama JSON is printed in the <code>filepress dev</code> terminal and saved to
|
|
558
|
+
<code>.filepress-genie/last-ollama.json</code>. “Icy / Antarctica / bright” means a
|
|
559
|
+
light page — activate <strong>baseline</strong> in History first if a prior dark look
|
|
560
|
+
is still the seed.
|
|
561
|
+
<code>gemma4:12b</code> often spends the first few minutes loading into VRAM — that is
|
|
562
|
+
normal. Progress prints in the filepress terminal every 15s. Raise the budget with
|
|
563
|
+
<code>FILEPRESS_OLLAMA_TIMEOUT_MS</code> (default 10 minutes). Tip: set
|
|
564
|
+
<code>FILEPRESS_OLLAMA_MODEL</code> / <code>OLLAMA_HOST</code> for defaults;
|
|
430
565
|
<a href="https://ollanet.dev" target="_blank" rel="noreferrer">ollanet</a> finds other
|
|
431
566
|
boxes. Finetuna can tune a named variant.
|
|
432
567
|
</p>
|
|
@@ -605,25 +740,82 @@
|
|
|
605
740
|
<h3>Versions</h3>
|
|
606
741
|
<p class="genie-howto">
|
|
607
742
|
Every Genie action saves a snapshot under <code>.filepress-genie/</code> (gitignored).
|
|
608
|
-
|
|
609
|
-
the pre-Genie look.
|
|
743
|
+
Star keepers, rename, duplicate, then activate to bake into the working tree.
|
|
744
|
+
<code>baseline</code> is the pre-Genie look and cannot be deleted.
|
|
610
745
|
</p>
|
|
611
746
|
<p class="genie-muted">
|
|
612
747
|
Active: {health.active?.versionId ?? '(none)'}
|
|
748
|
+
{#if health.versions.length}
|
|
749
|
+
· {health.versions.length} saved
|
|
750
|
+
{/if}
|
|
613
751
|
</p>
|
|
614
752
|
<ul class="genie-versions">
|
|
615
753
|
{#each health.versions as v (v.id)}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
754
|
+
{@const isActive = health.active?.versionId === v.id}
|
|
755
|
+
<li class:active={isActive} class:starred={v.starred}>
|
|
756
|
+
<div class="genie-ver-head">
|
|
757
|
+
<button
|
|
758
|
+
type="button"
|
|
759
|
+
class="genie-star"
|
|
760
|
+
class:on={v.starred}
|
|
761
|
+
disabled={loading || v.id === 'baseline'}
|
|
762
|
+
aria-pressed={v.starred}
|
|
763
|
+
aria-label={v.starred ? 'Unstar version' : 'Star version'}
|
|
764
|
+
onclick={() => toggleStar(v)}
|
|
765
|
+
>
|
|
766
|
+
{v.starred ? '★' : '☆'}
|
|
767
|
+
</button>
|
|
768
|
+
{#if renamingId === v.id}
|
|
769
|
+
<input
|
|
770
|
+
class="genie-rename"
|
|
771
|
+
type="text"
|
|
772
|
+
bind:value={renameDraft}
|
|
773
|
+
disabled={loading}
|
|
774
|
+
aria-label="Version label"
|
|
775
|
+
onkeydown={(e) => {
|
|
776
|
+
if (e.key === 'Enter') void saveLabel(v);
|
|
777
|
+
if (e.key === 'Escape') renamingId = null;
|
|
778
|
+
}}
|
|
779
|
+
/>
|
|
780
|
+
{:else}
|
|
781
|
+
<strong>{v.label}</strong>
|
|
782
|
+
{/if}
|
|
783
|
+
{#if isActive}
|
|
784
|
+
<span class="genie-ver-badge">Active</span>
|
|
785
|
+
{/if}
|
|
786
|
+
</div>
|
|
787
|
+
<p class="genie-ver-when">{versionWhen(v)}</p>
|
|
788
|
+
<div class="genie-ver-actions">
|
|
789
|
+
{#if renamingId === v.id}
|
|
790
|
+
<button type="button" disabled={loading} onclick={() => saveLabel(v)}>
|
|
791
|
+
Save name
|
|
792
|
+
</button>
|
|
793
|
+
<button type="button" disabled={loading} onclick={() => (renamingId = null)}>
|
|
794
|
+
Cancel
|
|
795
|
+
</button>
|
|
796
|
+
{:else}
|
|
797
|
+
<button
|
|
798
|
+
type="button"
|
|
799
|
+
disabled={loading || isActive}
|
|
800
|
+
onclick={() => activate(v.id)}
|
|
801
|
+
>
|
|
802
|
+
Activate
|
|
803
|
+
</button>
|
|
804
|
+
<button type="button" disabled={loading} onclick={() => startRename(v)}>
|
|
805
|
+
Rename
|
|
806
|
+
</button>
|
|
807
|
+
<button type="button" disabled={loading} onclick={() => duplicate(v)}>
|
|
808
|
+
Duplicate
|
|
809
|
+
</button>
|
|
810
|
+
<button
|
|
811
|
+
type="button"
|
|
812
|
+
disabled={loading || v.id === 'baseline' || isActive}
|
|
813
|
+
onclick={() => removeVersion(v)}
|
|
814
|
+
>
|
|
815
|
+
Delete
|
|
816
|
+
</button>
|
|
817
|
+
{/if}
|
|
818
|
+
</div>
|
|
627
819
|
</li>
|
|
628
820
|
{/each}
|
|
629
821
|
</ul>
|
|
@@ -816,6 +1008,16 @@
|
|
|
816
1008
|
flex-shrink: 0;
|
|
817
1009
|
}
|
|
818
1010
|
|
|
1011
|
+
.genie-progress {
|
|
1012
|
+
margin: 0 0.25rem 0.65rem;
|
|
1013
|
+
padding: 0.5rem 0.65rem;
|
|
1014
|
+
border-radius: 6px;
|
|
1015
|
+
border: 1px solid color-mix(in srgb, var(--accent, #f0c040) 40%, var(--rule, #444));
|
|
1016
|
+
font-size: 0.82rem;
|
|
1017
|
+
color: var(--ink-soft, #ccc);
|
|
1018
|
+
flex-shrink: 0;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
819
1021
|
.genie-row {
|
|
820
1022
|
display: grid;
|
|
821
1023
|
gap: 0.35rem;
|
|
@@ -898,19 +1100,86 @@
|
|
|
898
1100
|
margin: 0.5rem 0;
|
|
899
1101
|
padding: 0;
|
|
900
1102
|
display: grid;
|
|
901
|
-
gap: 0.
|
|
1103
|
+
gap: 0.55rem;
|
|
902
1104
|
}
|
|
903
1105
|
|
|
904
1106
|
.genie-versions li {
|
|
1107
|
+
display: grid;
|
|
1108
|
+
gap: 0.3rem;
|
|
1109
|
+
padding: 0.5rem 0.55rem;
|
|
1110
|
+
border: 1px solid var(--rule, #444);
|
|
1111
|
+
border-radius: 8px;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
.genie-versions li.active {
|
|
1115
|
+
border-color: color-mix(in srgb, var(--accent, #f0c040) 55%, var(--rule, #444));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.genie-ver-head {
|
|
905
1119
|
display: flex;
|
|
906
1120
|
align-items: center;
|
|
907
|
-
|
|
908
|
-
|
|
1121
|
+
gap: 0.4rem;
|
|
1122
|
+
min-width: 0;
|
|
909
1123
|
}
|
|
910
1124
|
|
|
911
|
-
.genie-
|
|
912
|
-
|
|
1125
|
+
.genie-ver-head strong {
|
|
1126
|
+
flex: 1 1 auto;
|
|
1127
|
+
min-width: 0;
|
|
1128
|
+
overflow: hidden;
|
|
1129
|
+
text-overflow: ellipsis;
|
|
1130
|
+
white-space: nowrap;
|
|
1131
|
+
font-size: 0.84rem;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.genie-star {
|
|
1135
|
+
flex: 0 0 auto;
|
|
1136
|
+
width: 1.7rem;
|
|
1137
|
+
padding: 0.15rem 0;
|
|
1138
|
+
border: 1px solid transparent;
|
|
1139
|
+
background: none;
|
|
1140
|
+
color: var(--ink-soft, #999);
|
|
1141
|
+
font-size: 0.95rem;
|
|
1142
|
+
line-height: 1;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.genie-star.on {
|
|
1146
|
+
color: var(--accent, #f0c040);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.genie-ver-badge {
|
|
1150
|
+
flex: 0 0 auto;
|
|
1151
|
+
font-size: 0.62rem;
|
|
1152
|
+
letter-spacing: 0.08em;
|
|
1153
|
+
text-transform: uppercase;
|
|
913
1154
|
color: var(--accent, #f0c040);
|
|
1155
|
+
border: 1px solid color-mix(in srgb, var(--accent, #f0c040) 45%, var(--rule, #444));
|
|
1156
|
+
border-radius: 999px;
|
|
1157
|
+
padding: 0.08rem 0.4rem;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
.genie-rename {
|
|
1161
|
+
flex: 1 1 auto;
|
|
1162
|
+
min-width: 0;
|
|
1163
|
+
box-sizing: border-box;
|
|
1164
|
+
font: inherit;
|
|
1165
|
+
font-size: 0.82rem;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.genie-ver-when {
|
|
1169
|
+
margin: 0;
|
|
1170
|
+
font-size: 0.72rem;
|
|
1171
|
+
color: var(--ink-soft, #999);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.genie-ver-actions {
|
|
1175
|
+
display: flex;
|
|
1176
|
+
flex-wrap: wrap;
|
|
1177
|
+
gap: 0.3rem;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.genie-ver-actions button {
|
|
1181
|
+
padding: 0.28rem 0.5rem;
|
|
1182
|
+
font-size: 0.72rem;
|
|
914
1183
|
}
|
|
915
1184
|
|
|
916
1185
|
.genie-linkish {
|
|
@@ -25,6 +25,7 @@ import type { DesignBrief, SiteIR } from '../../../../import/src/ir.ts';
|
|
|
25
25
|
import {
|
|
26
26
|
activateVersion,
|
|
27
27
|
ensureBaseline,
|
|
28
|
+
genieRoot,
|
|
28
29
|
getActive,
|
|
29
30
|
listVersions,
|
|
30
31
|
readVersionBrief,
|
|
@@ -313,6 +314,7 @@ function stubSiteIr(siteRoot: string): SiteIR {
|
|
|
313
314
|
nav: [],
|
|
314
315
|
topics: [],
|
|
315
316
|
lede: null,
|
|
317
|
+
homeMarkdown: null,
|
|
316
318
|
notes: [],
|
|
317
319
|
assets: []
|
|
318
320
|
};
|
|
@@ -405,13 +407,20 @@ export async function refineWithOllama(
|
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
const seed = loadWorkingBrief(siteRoot);
|
|
410
|
+
const logLabel = `filepress genie: refine ${model} @ ${host}`;
|
|
411
|
+
console.log(`${logLabel}: prompt “${prompt.slice(0, 80)}${prompt.length > 80 ? '…' : ''}”`);
|
|
408
412
|
const brief = await generateDesignBrief({
|
|
409
413
|
host,
|
|
410
414
|
model,
|
|
411
415
|
ir: stubSiteIr(siteRoot),
|
|
412
416
|
inspireSummaries: [`Author steer: ${prompt}`],
|
|
413
417
|
inspireSignals: [],
|
|
414
|
-
seed
|
|
418
|
+
seed,
|
|
419
|
+
strictParse: true,
|
|
420
|
+
logLabel,
|
|
421
|
+
intent: 'steer',
|
|
422
|
+
steer: prompt,
|
|
423
|
+
dumpPath: join(genieRoot(siteRoot), 'last-ollama.json')
|
|
415
424
|
});
|
|
416
425
|
const themeCss = themeCssFromBrief(brief);
|
|
417
426
|
const meta = writeSnapshot(siteRoot, {
|
|
@@ -423,6 +432,7 @@ export async function refineWithOllama(
|
|
|
423
432
|
llm: { used: true, model, host },
|
|
424
433
|
steers: [{ type: 'refine', prompt }]
|
|
425
434
|
});
|
|
435
|
+
console.log(`${logLabel}: wrote ${meta.id}`);
|
|
426
436
|
const active =
|
|
427
437
|
opts.activate !== false ? activateVersion(siteRoot, meta.id) : getActive(siteRoot);
|
|
428
438
|
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">
|