getfilepress 0.1.12 → 0.1.16
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/package.json +1 -1
- package/packages/app/src/lib/genie/GeniePanel.svelte +122 -7
- package/packages/app/src/lib/genie/ops.ts +2 -8
- package/packages/app/src/lib/genie/store.ts +23 -1
- package/packages/app/src/lib/genie/types.ts +8 -0
- package/packages/app/src/lib/genie/version-summary.ts +31 -0
- package/packages/app/src/routes/+layout.svelte +2 -1
- package/packages/app/vite-plugin-genie.ts +2 -2
- package/packages/core/src/lib/components/SiteHeader.svelte +8 -1
- package/packages/core/src/lib/config.ts +18 -5
- package/packages/import/src/write-site.ts +1 -1
- package/scripts/create-site.mjs +1 -0
package/package.json
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
createdAt: string;
|
|
6
6
|
starred?: boolean;
|
|
7
7
|
parentId?: string | null;
|
|
8
|
+
prompt?: string;
|
|
9
|
+
did?: string;
|
|
10
|
+
tokens?: { accent?: string; bg?: string; ink?: string };
|
|
8
11
|
};
|
|
9
12
|
|
|
10
13
|
type Health = {
|
|
@@ -68,6 +71,8 @@
|
|
|
68
71
|
let cfgLogo = $state('');
|
|
69
72
|
let renamingId = $state<string | null>(null);
|
|
70
73
|
let renameDraft = $state('');
|
|
74
|
+
let copiedId = $state<string | null>(null);
|
|
75
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
|
71
76
|
let jobNote = $state('');
|
|
72
77
|
let jobSecs = $state(0);
|
|
73
78
|
let jobTimer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -156,6 +161,29 @@
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
async function copyPrompt(v: VersionRow) {
|
|
165
|
+
const text = v.prompt?.trim();
|
|
166
|
+
if (!text) return;
|
|
167
|
+
try {
|
|
168
|
+
await navigator.clipboard.writeText(text);
|
|
169
|
+
} catch {
|
|
170
|
+
const ta = document.createElement('textarea');
|
|
171
|
+
ta.value = text;
|
|
172
|
+
ta.setAttribute('readonly', '');
|
|
173
|
+
ta.style.position = 'fixed';
|
|
174
|
+
ta.style.left = '-9999px';
|
|
175
|
+
document.body.appendChild(ta);
|
|
176
|
+
ta.select();
|
|
177
|
+
document.execCommand('copy');
|
|
178
|
+
ta.remove();
|
|
179
|
+
}
|
|
180
|
+
copiedId = v.id;
|
|
181
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
182
|
+
copyTimer = setTimeout(() => {
|
|
183
|
+
copiedId = null;
|
|
184
|
+
}, 1600);
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
function versionWhen(v: VersionRow) {
|
|
160
188
|
if (v.id === 'baseline') return 'pre-Genie snapshot';
|
|
161
189
|
const d = new Date(v.createdAt);
|
|
@@ -740,6 +768,7 @@
|
|
|
740
768
|
<h3>Versions</h3>
|
|
741
769
|
<p class="genie-howto">
|
|
742
770
|
Every Genie action saves a snapshot under <code>.filepress-genie/</code> (gitignored).
|
|
771
|
+
Each row shows what that pass applied. Click the prompt to copy it.
|
|
743
772
|
Star keepers, rename, duplicate, then activate to bake into the working tree.
|
|
744
773
|
<code>baseline</code> is the pre-Genie look and cannot be deleted.
|
|
745
774
|
</p>
|
|
@@ -784,6 +813,39 @@
|
|
|
784
813
|
<span class="genie-ver-badge">Active</span>
|
|
785
814
|
{/if}
|
|
786
815
|
</div>
|
|
816
|
+
{#if v.did}
|
|
817
|
+
<p class="genie-ver-did">
|
|
818
|
+
{#if v.tokens?.bg || v.tokens?.accent || v.tokens?.ink}
|
|
819
|
+
<span class="genie-swatches" aria-hidden="true">
|
|
820
|
+
{#if v.tokens.bg}
|
|
821
|
+
<span class="genie-swatch" style:background={v.tokens.bg}></span>
|
|
822
|
+
{/if}
|
|
823
|
+
{#if v.tokens.ink}
|
|
824
|
+
<span class="genie-swatch" style:background={v.tokens.ink}></span>
|
|
825
|
+
{/if}
|
|
826
|
+
{#if v.tokens.accent}
|
|
827
|
+
<span class="genie-swatch" style:background={v.tokens.accent}></span>
|
|
828
|
+
{/if}
|
|
829
|
+
</span>
|
|
830
|
+
{/if}
|
|
831
|
+
{v.did}
|
|
832
|
+
</p>
|
|
833
|
+
{/if}
|
|
834
|
+
{#if v.prompt}
|
|
835
|
+
<button
|
|
836
|
+
type="button"
|
|
837
|
+
class="genie-ver-prompt"
|
|
838
|
+
class:copied={copiedId === v.id}
|
|
839
|
+
disabled={loading}
|
|
840
|
+
title="Copy prompt"
|
|
841
|
+
onclick={() => copyPrompt(v)}
|
|
842
|
+
>
|
|
843
|
+
<span class="genie-ver-prompt-kicker">
|
|
844
|
+
{copiedId === v.id ? 'Copied' : 'Prompt — click to copy'}
|
|
845
|
+
</span>
|
|
846
|
+
<span class="genie-ver-prompt-text">{v.prompt}</span>
|
|
847
|
+
</button>
|
|
848
|
+
{/if}
|
|
787
849
|
<p class="genie-ver-when">{versionWhen(v)}</p>
|
|
788
850
|
<div class="genie-ver-actions">
|
|
789
851
|
{#if renamingId === v.id}
|
|
@@ -794,13 +856,11 @@
|
|
|
794
856
|
Cancel
|
|
795
857
|
</button>
|
|
796
858
|
{:else}
|
|
797
|
-
|
|
798
|
-
type="button"
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
Activate
|
|
803
|
-
</button>
|
|
859
|
+
{#if !isActive}
|
|
860
|
+
<button type="button" disabled={loading} onclick={() => activate(v.id)}>
|
|
861
|
+
Activate
|
|
862
|
+
</button>
|
|
863
|
+
{/if}
|
|
804
864
|
<button type="button" disabled={loading} onclick={() => startRename(v)}>
|
|
805
865
|
Rename
|
|
806
866
|
</button>
|
|
@@ -1165,6 +1225,61 @@
|
|
|
1165
1225
|
font-size: 0.82rem;
|
|
1166
1226
|
}
|
|
1167
1227
|
|
|
1228
|
+
.genie-ver-did {
|
|
1229
|
+
margin: 0;
|
|
1230
|
+
font-size: 0.78rem;
|
|
1231
|
+
line-height: 1.4;
|
|
1232
|
+
color: var(--ink, #eee);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.genie-swatches {
|
|
1236
|
+
display: inline-flex;
|
|
1237
|
+
gap: 0.2rem;
|
|
1238
|
+
margin-right: 0.35rem;
|
|
1239
|
+
vertical-align: middle;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
.genie-swatch {
|
|
1243
|
+
display: inline-block;
|
|
1244
|
+
width: 0.7rem;
|
|
1245
|
+
height: 0.7rem;
|
|
1246
|
+
border-radius: 2px;
|
|
1247
|
+
border: 1px solid var(--rule, #444);
|
|
1248
|
+
vertical-align: -0.05em;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
.genie-versions button.genie-ver-prompt {
|
|
1252
|
+
display: grid;
|
|
1253
|
+
gap: 0.15rem;
|
|
1254
|
+
width: 100%;
|
|
1255
|
+
text-align: left;
|
|
1256
|
+
padding: 0.4rem 0.45rem;
|
|
1257
|
+
border: 1px dashed var(--rule, #444);
|
|
1258
|
+
border-radius: 6px;
|
|
1259
|
+
background: color-mix(in srgb, var(--surface, #1c1c22) 70%, transparent);
|
|
1260
|
+
color: inherit;
|
|
1261
|
+
cursor: pointer;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.genie-versions button.genie-ver-prompt:hover,
|
|
1265
|
+
.genie-versions button.genie-ver-prompt.copied {
|
|
1266
|
+
border-color: var(--accent, #f0c040);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.genie-ver-prompt-kicker {
|
|
1270
|
+
font-size: 0.68rem;
|
|
1271
|
+
letter-spacing: 0.02em;
|
|
1272
|
+
text-transform: uppercase;
|
|
1273
|
+
color: var(--ink-soft, #999);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.genie-ver-prompt-text {
|
|
1277
|
+
font-size: 0.78rem;
|
|
1278
|
+
line-height: 1.4;
|
|
1279
|
+
white-space: pre-wrap;
|
|
1280
|
+
overflow-wrap: anywhere;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1168
1283
|
.genie-ver-when {
|
|
1169
1284
|
margin: 0;
|
|
1170
1285
|
font-size: 0.72rem;
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
ensureBaseline,
|
|
28
28
|
genieRoot,
|
|
29
29
|
getActive,
|
|
30
|
-
|
|
30
|
+
listVersionRows,
|
|
31
31
|
readVersionBrief,
|
|
32
32
|
writeSnapshot
|
|
33
33
|
} from './store.ts';
|
|
@@ -133,13 +133,7 @@ export async function health(siteRoot: string) {
|
|
|
133
133
|
: ollamaSetupHint(host)
|
|
134
134
|
},
|
|
135
135
|
active: getActive(siteRoot),
|
|
136
|
-
versions:
|
|
137
|
-
id: v.id,
|
|
138
|
-
label: v.label,
|
|
139
|
-
createdAt: v.createdAt,
|
|
140
|
-
starred: v.starred,
|
|
141
|
-
parentId: v.parentId
|
|
142
|
-
})),
|
|
136
|
+
versions: listVersionRows(siteRoot),
|
|
143
137
|
brief: loadWorkingBrief(siteRoot)
|
|
144
138
|
};
|
|
145
139
|
}
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
} from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { randomBytes } from 'node:crypto';
|
|
13
|
-
import type { DesignBrief, GenieActive, GenieVersionMeta } from './types.ts';
|
|
13
|
+
import type { DesignBrief, GenieActive, GenieVersionMeta, GenieVersionRow } from './types.ts';
|
|
14
14
|
import { applyConfigPatch, readConfigPatchFile, type GenieConfigPatch } from './config-patch.ts';
|
|
15
|
+
import { summarizeVersionDid, versionPrompt } from './version-summary.ts';
|
|
15
16
|
|
|
16
17
|
const GENIE_DIR = '.filepress-genie';
|
|
17
18
|
|
|
@@ -69,6 +70,27 @@ export function listVersions(siteRoot: string): GenieVersionMeta[] {
|
|
|
69
70
|
});
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export function toVersionRow(siteRoot: string, meta: GenieVersionMeta): GenieVersionRow {
|
|
74
|
+
const brief = readVersionBrief(siteRoot, meta.id);
|
|
75
|
+
return {
|
|
76
|
+
...meta,
|
|
77
|
+
prompt: versionPrompt(meta),
|
|
78
|
+
did: summarizeVersionDid(brief),
|
|
79
|
+
tokens: {
|
|
80
|
+
accent: brief?.tokens.accent,
|
|
81
|
+
bg: brief?.tokens.bg,
|
|
82
|
+
ink: brief?.tokens.ink
|
|
83
|
+
},
|
|
84
|
+
paletteMode: brief?.paletteMode,
|
|
85
|
+
hero: brief?.hero,
|
|
86
|
+
atmosphere: brief?.atmosphere
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function listVersionRows(siteRoot: string): GenieVersionRow[] {
|
|
91
|
+
return listVersions(siteRoot).map((v) => toVersionRow(siteRoot, v));
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
export function readVersionBrief(siteRoot: string, id: string): DesignBrief | null {
|
|
73
95
|
return readJson<DesignBrief | null>(join(versionPath(siteRoot, id), 'design-brief.json'), null);
|
|
74
96
|
}
|
|
@@ -48,6 +48,14 @@ export type GenieVersionMeta = {
|
|
|
48
48
|
llm: { used: boolean; model: string | null; host: string | null };
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
export type GenieVersionRow = GenieVersionMeta & {
|
|
52
|
+
did: string;
|
|
53
|
+
tokens: { accent?: string; bg?: string; ink?: string };
|
|
54
|
+
paletteMode?: string;
|
|
55
|
+
hero?: string;
|
|
56
|
+
atmosphere?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
51
59
|
export type GenieActive = {
|
|
52
60
|
versionId: string;
|
|
53
61
|
activatedAt: string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DesignBrief, GenieVersionMeta } from './types.ts';
|
|
2
|
+
|
|
3
|
+
/** Original author prompt, including older snapshots that only stored it on the steer. */
|
|
4
|
+
export function versionPrompt(meta: Pick<GenieVersionMeta, 'prompt' | 'steers'>): string {
|
|
5
|
+
const direct = meta.prompt?.trim();
|
|
6
|
+
if (direct) return direct;
|
|
7
|
+
for (const steer of meta.steers || []) {
|
|
8
|
+
if (typeof steer.prompt === 'string' && steer.prompt.trim()) return steer.prompt.trim();
|
|
9
|
+
}
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One line: mood, palette, colors, structure — what the snapshot actually applied. */
|
|
14
|
+
export function summarizeVersionDid(brief: DesignBrief | null | undefined): string {
|
|
15
|
+
if (!brief) return '';
|
|
16
|
+
const parts: string[] = [];
|
|
17
|
+
const mood = brief.mood?.trim();
|
|
18
|
+
if (mood) parts.push(mood);
|
|
19
|
+
if (brief.paletteMode) parts.push(brief.paletteMode);
|
|
20
|
+
const bg = brief.tokens?.bg?.trim();
|
|
21
|
+
const accent = brief.tokens?.accent?.trim();
|
|
22
|
+
if (bg) parts.push(bg);
|
|
23
|
+
if (accent) parts.push(accent);
|
|
24
|
+
if (brief.hero) parts.push(brief.hero);
|
|
25
|
+
if (brief.atmosphere && brief.atmosphere !== 'none') parts.push(brief.atmosphere);
|
|
26
|
+
if (brief.density) parts.push(brief.density);
|
|
27
|
+
const serif = brief.fonts?.serif?.trim();
|
|
28
|
+
const sans = brief.fonts?.sans?.trim();
|
|
29
|
+
if (serif && sans) parts.push(`${serif} / ${sans}`);
|
|
30
|
+
return parts.join(' · ');
|
|
31
|
+
}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
{#if criticalTheme}
|
|
23
23
|
{@html `<style data-filepress-critical>${criticalTheme}</style>`}
|
|
24
24
|
{/if}
|
|
25
|
-
<link rel="icon" href="/favicon.
|
|
25
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
26
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
26
27
|
<link rel="alternate" type="application/rss+xml" title={config.title} href="/rss.xml" />
|
|
27
28
|
</svelte:head>
|
|
28
29
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
runInspire,
|
|
12
12
|
scanOllamaHosts
|
|
13
13
|
} from './src/lib/genie/ops.ts';
|
|
14
|
-
import { deleteVersion, duplicateVersion,
|
|
14
|
+
import { deleteVersion, duplicateVersion, listVersionRows, updateVersionMeta } from './src/lib/genie/store.ts';
|
|
15
15
|
|
|
16
16
|
function readBody(req: IncomingMessage): Promise<string> {
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
@@ -54,7 +54,7 @@ export function geniePlugin(siteRoot: string): Plugin {
|
|
|
54
54
|
const h = await health(siteRoot);
|
|
55
55
|
return sendJson(res, 200, {
|
|
56
56
|
active: h.active,
|
|
57
|
-
versions:
|
|
57
|
+
versions: listVersionRows(siteRoot),
|
|
58
58
|
brief: h.brief
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
<div class="site-id">
|
|
12
12
|
<a class="site-title" href="/">
|
|
13
13
|
{#if site.logo}
|
|
14
|
-
<img
|
|
14
|
+
<img
|
|
15
|
+
class="site-logo"
|
|
16
|
+
src={site.logo}
|
|
17
|
+
alt=""
|
|
18
|
+
onerror={(e) => {
|
|
19
|
+
e.currentTarget.hidden = true;
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
15
22
|
{/if}
|
|
16
23
|
<span class="site-brand-copy">
|
|
17
24
|
<span class="site-wordmark">{site.title}</span>
|
|
@@ -46,7 +46,11 @@ export interface SiteConfig {
|
|
|
46
46
|
tagline: string;
|
|
47
47
|
/** One-line lede shown on the index hero, or null for no visible hero. */
|
|
48
48
|
lede: string | null;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Path to a masthead logo image (site-relative, e.g. "/logo.png"), or null for text.
|
|
51
|
+
* When omitted, defaults to `/logo.png` (drop the file in `static/`).
|
|
52
|
+
* Pass `""` or `null` to keep a text-only masthead.
|
|
53
|
+
*/
|
|
50
54
|
logo: string | null;
|
|
51
55
|
/**
|
|
52
56
|
* Absolute or site-relative Open Graph image (e.g. "/logo.png").
|
|
@@ -90,7 +94,8 @@ export interface SiteConfigInput {
|
|
|
90
94
|
description?: string;
|
|
91
95
|
tagline?: string;
|
|
92
96
|
lede?: string;
|
|
93
|
-
logo
|
|
97
|
+
/** Masthead logo path. Omitted → `/logo.png`. Empty or null → text only. */
|
|
98
|
+
logo?: string | null;
|
|
94
99
|
/** Open Graph image path; defaults to `logo`. */
|
|
95
100
|
ogImage?: string;
|
|
96
101
|
author?: string;
|
|
@@ -136,6 +141,13 @@ export function postsIndexPath(site: Pick<SiteConfig, 'homePage'>): string {
|
|
|
136
141
|
return site.homePage ? '/posts' : '/';
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
/** Omitted → fallback. Explicit null or blank → no image. */
|
|
145
|
+
function optionalChromePath(value: string | null | undefined, whenOmitted: string | null): string | null {
|
|
146
|
+
if (value === undefined) return whenOmitted;
|
|
147
|
+
if (value === null) return null;
|
|
148
|
+
return value.trim() || null;
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
/**
|
|
140
152
|
* Validate and normalize a site's config, applying defaults. Fails loudly if a
|
|
141
153
|
* required field is missing (edge case 19) so a misconfigured site never builds
|
|
@@ -161,6 +173,8 @@ export function defineFilepressConfig(input: SiteConfigInput): SiteConfig {
|
|
|
161
173
|
);
|
|
162
174
|
}
|
|
163
175
|
|
|
176
|
+
const logo = optionalChromePath(input.logo, '/logo.png');
|
|
177
|
+
|
|
164
178
|
const defaultNav: NavItem[] = homePage
|
|
165
179
|
? [
|
|
166
180
|
{ label: 'Home', href: '/' },
|
|
@@ -177,9 +191,8 @@ export function defineFilepressConfig(input: SiteConfigInput): SiteConfig {
|
|
|
177
191
|
description,
|
|
178
192
|
tagline: (input.tagline ?? '').trim() || description || title,
|
|
179
193
|
lede: (input.lede ?? '').trim() || null,
|
|
180
|
-
logo
|
|
181
|
-
ogImage:
|
|
182
|
-
(input.ogImage ?? '').trim() || (input.logo ?? '').trim() || null,
|
|
194
|
+
logo,
|
|
195
|
+
ogImage: (input.ogImage ?? '').trim() || logo,
|
|
183
196
|
url: url.replace(/\/+$/, ''),
|
|
184
197
|
author: (input.author ?? '').trim() || title,
|
|
185
198
|
postsPerPage:
|
|
@@ -262,7 +262,7 @@ export async function writeSite(
|
|
|
262
262
|
];
|
|
263
263
|
const imageMap = await downloadImages(allImages, staticDir);
|
|
264
264
|
await downloadRootAssets(ir.assets ?? [], staticDir);
|
|
265
|
-
// Layout
|
|
265
|
+
// Layout prefers /favicon.png, then /favicon.svg — keep an svg so prerender has a fallback.
|
|
266
266
|
const faviconDest = join(staticDir, 'favicon.svg');
|
|
267
267
|
if (!existsSync(faviconDest) && existsSync(DEFAULT_FAVICON)) {
|
|
268
268
|
copyFileSync(DEFAULT_FAVICON, faviconDest);
|