getfilepress 0.1.11 → 0.1.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getfilepress",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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`.",
@@ -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}
@@ -1165,6 +1227,61 @@
1165
1227
  font-size: 0.82rem;
1166
1228
  }
1167
1229
 
1230
+ .genie-ver-did {
1231
+ margin: 0;
1232
+ font-size: 0.78rem;
1233
+ line-height: 1.4;
1234
+ color: var(--ink, #eee);
1235
+ }
1236
+
1237
+ .genie-swatches {
1238
+ display: inline-flex;
1239
+ gap: 0.2rem;
1240
+ margin-right: 0.35rem;
1241
+ vertical-align: middle;
1242
+ }
1243
+
1244
+ .genie-swatch {
1245
+ display: inline-block;
1246
+ width: 0.7rem;
1247
+ height: 0.7rem;
1248
+ border-radius: 2px;
1249
+ border: 1px solid var(--rule, #444);
1250
+ vertical-align: -0.05em;
1251
+ }
1252
+
1253
+ .genie-versions button.genie-ver-prompt {
1254
+ display: grid;
1255
+ gap: 0.15rem;
1256
+ width: 100%;
1257
+ text-align: left;
1258
+ padding: 0.4rem 0.45rem;
1259
+ border: 1px dashed var(--rule, #444);
1260
+ border-radius: 6px;
1261
+ background: color-mix(in srgb, var(--surface, #1c1c22) 70%, transparent);
1262
+ color: inherit;
1263
+ cursor: pointer;
1264
+ }
1265
+
1266
+ .genie-versions button.genie-ver-prompt:hover,
1267
+ .genie-versions button.genie-ver-prompt.copied {
1268
+ border-color: var(--accent, #f0c040);
1269
+ }
1270
+
1271
+ .genie-ver-prompt-kicker {
1272
+ font-size: 0.68rem;
1273
+ letter-spacing: 0.02em;
1274
+ text-transform: uppercase;
1275
+ color: var(--ink-soft, #999);
1276
+ }
1277
+
1278
+ .genie-ver-prompt-text {
1279
+ font-size: 0.78rem;
1280
+ line-height: 1.4;
1281
+ white-space: pre-wrap;
1282
+ overflow-wrap: anywhere;
1283
+ }
1284
+
1168
1285
  .genie-ver-when {
1169
1286
  margin: 0;
1170
1287
  font-size: 0.72rem;
@@ -27,7 +27,7 @@ import {
27
27
  ensureBaseline,
28
28
  genieRoot,
29
29
  getActive,
30
- listVersions,
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: listVersions(siteRoot).map((v) => ({
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
+ }
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Essay chrome + fonts, then a named preset, then the site theme.
3
- * Site rules use higher-specificity selectors (`:root:root`, `header.site-header …`)
4
- * so they win even if Vite emits Essay CSS after the site sheet.
2
+ * Layer order is declared first. Essay chrome, then preset, then site theme.
3
+ * Site rules live in `@layer site` so they win even if Vite injects Essay CSS
4
+ * after the site sheet (dev) or splits the CSS (build).
5
5
  */
6
+ import './theme-layers.css';
6
7
  import '@filepress/core/theme';
7
8
  import '$site-preset';
8
9
  import '$site-theme';
@@ -0,0 +1,3 @@
1
+ /* Essay first, site last. Declared before either sheet so order does not
2
+ * depend on Vite inject timing. */
3
+ @layer filepress, site;
@@ -2,28 +2,42 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import type { Plugin } from 'vite';
3
3
 
4
4
  /**
5
- * Extract early paint tokens from the site theme: `:root` blocks (including
6
- * those nested in `prefers-color-scheme` media) and top-level `body { ... }`.
5
+ * @import cannot live inside @layer. Pull them out, wrap the rest.
6
+ * Quoted URLs may contain `;` (Google Fonts `wght@400;700`).
7
7
  */
8
- export function extractCriticalTheme(css: string): string {
9
- const chunks: string[] = [];
8
+ const IMPORT_RE = /@import\s+(?:url\(\s*)?(["'])(?:\\.|(?!\1).)*\1\s*\)?\s*;/g;
10
9
 
11
- // Prefer `:root:root` (higher specificity site tokens) when present.
12
- for (const m of css.matchAll(/:root(?::root)?\s*\{[^}]*\}/g)) {
13
- chunks.push(m[0]);
14
- }
10
+ export function takeImports(css: string): { imports: string[]; rest: string } {
11
+ const imports: string[] = [];
12
+ const rest = css.replace(IMPORT_RE, (m) => {
13
+ imports.push(m.trim());
14
+ return '';
15
+ });
16
+ return { imports, rest };
17
+ }
15
18
 
16
- for (const m of css.matchAll(
17
- /@media\s*\([^)]*prefers-color-scheme[^)]*\)\s*\{\s*:root(?::root)?\s*\{[^}]*\}\s*\}/g
18
- )) {
19
- chunks.push(m[0]);
20
- }
19
+ export function hoistImportsAndLayer(css: string, layer: string): string {
20
+ const { imports, rest } = takeImports(css);
21
+ const body = rest.trim();
22
+ const hoisted = imports.length ? `${imports.join('\n')}\n` : '';
23
+ if (!body) return hoisted;
24
+ return `${hoisted}@layer ${layer} {\n${body}\n}\n`;
25
+ }
21
26
 
22
- for (const m of css.matchAll(/(?:^|\n)(?:html\s+)?body\s*\{[^}]*\}/g)) {
23
- chunks.push(m[0].trim());
24
- }
27
+ /** Site tokens must beat later Essay `:root` (same specificity, later source). */
28
+ export function boostRoot(css: string): string {
29
+ return css.replace(/:root(?::root)?/g, ':root:root');
30
+ }
25
31
 
26
- return chunks.join('\n');
32
+ /**
33
+ * Inline the site theme for first paint (minus remote @import).
34
+ * Cascade layer `site` wins over Essay even after Vite injects core CSS.
35
+ */
36
+ export function extractCriticalTheme(css: string): string {
37
+ const withoutImport = takeImports(css).rest;
38
+ const meaningful = withoutImport.replace(/\/\*[\s\S]*?\*\//g, '').trim();
39
+ if (!meaningful) return '';
40
+ return `@layer filepress, site;\n${hoistImportsAndLayer(boostRoot(withoutImport), 'site')}`;
27
41
  }
28
42
 
29
43
  export function writeCriticalThemeModule(siteThemePath: string, outPath: string): string {
@@ -41,16 +55,32 @@ export function writeCriticalThemeModule(siteThemePath: string, outPath: string)
41
55
  return critical;
42
56
  }
43
57
 
58
+ function fileId(id: string): string {
59
+ return id.replace(/\\/g, '/').split('?')[0] ?? id;
60
+ }
61
+
44
62
  /**
45
- * Keep `$lib/critical-theme.generated.ts` in sync with the site theme so
46
- * layout can inline tokens without a Vite virtual module (SSR-safe).
63
+ * Keep `$critical-theme` in sync, and put Essay vs site CSS in cascade
64
+ * layers so Vite's inject order cannot flash the default look.
47
65
  */
48
66
  export function criticalThemePlugin(siteThemePath: string, outPath: string): Plugin {
67
+ const siteNorm = siteThemePath.replace(/\\/g, '/');
68
+
49
69
  return {
50
70
  name: 'filepress-critical-theme',
51
71
  buildStart() {
52
72
  writeCriticalThemeModule(siteThemePath, outPath);
53
73
  },
74
+ transform(code, id) {
75
+ const path = fileId(id);
76
+ if (path === siteNorm) {
77
+ return hoistImportsAndLayer(boostRoot(code), 'site');
78
+ }
79
+ if (path.endsWith('/lib/styles/theme.css') || path.includes('/lib/styles/presets/')) {
80
+ return hoistImportsAndLayer(code, 'filepress');
81
+ }
82
+ return null;
83
+ },
54
84
  configureServer(server) {
55
85
  writeCriticalThemeModule(siteThemePath, outPath);
56
86
  server.watcher.add(siteThemePath);
@@ -11,7 +11,7 @@ import {
11
11
  runInspire,
12
12
  scanOllamaHosts
13
13
  } from './src/lib/genie/ops.ts';
14
- import { deleteVersion, duplicateVersion, listVersions, updateVersionMeta } from './src/lib/genie/store.ts';
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: listVersions(siteRoot),
57
+ versions: listVersionRows(siteRoot),
58
58
  brief: h.brief
59
59
  });
60
60
  }