feat: add support for stat sheet SVG rendering
Introduces the ability to discover and render `*.sheet.svg` files.
These files can contain `<text>` elements with `${key}` templates
that reactively update based on the current journal stats.
- Add `StatSheet` type and CLI scanning logic
- Implement `SheetView` for rendering SVGs with live bindings
- Add `parseSheet` utility to extract template patterns from SVG
- Update file indexer to include `.svg` files
This commit is contained in:
@@ -22,13 +22,14 @@ import {
|
||||
parseStatModifiers,
|
||||
} from "../../cli/completions/stat-parser";
|
||||
import type { StatDef, StatTemplate } from "../../cli/completions/stat-parser";
|
||||
import type { StatSheet } from "../../cli/completions/types";
|
||||
import {
|
||||
FENCED_BLOCK_RE,
|
||||
parseBlockAttrs,
|
||||
parseSparkTableCsv,
|
||||
} from "../../cli/completions/block-scanner";
|
||||
|
||||
export type { StatDef, StatTemplate };
|
||||
export type { StatDef, StatTemplate, StatSheet };
|
||||
|
||||
// ------------------- Types (mirrors CLI) -------------------
|
||||
|
||||
@@ -58,6 +59,7 @@ export interface JournalCompletions {
|
||||
sparkTables: SparkTableCompletion[];
|
||||
stats: StatDef[];
|
||||
statTemplates: StatTemplate[];
|
||||
statSheets: StatSheet[];
|
||||
}
|
||||
|
||||
export type CompletionsState =
|
||||
@@ -87,6 +89,7 @@ async function tryServer(): Promise<JournalCompletions | null> {
|
||||
statTemplates: Array.isArray(data.statTemplates)
|
||||
? data.statTemplates
|
||||
: [],
|
||||
statSheets: Array.isArray(data.statSheets) ? data.statSheets : [],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -169,7 +172,46 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||
}
|
||||
}
|
||||
|
||||
return { dice, links, sparkTables, stats, statTemplates };
|
||||
return { dice, links, sparkTables, stats, statTemplates, statSheets: [] };
|
||||
}
|
||||
|
||||
// ------------------- Stat sheet helpers -------------------
|
||||
|
||||
function extractSvgTitle(svg: string): string | null {
|
||||
const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(svg);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
function labelFromId(id: string): string {
|
||||
return id
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
async function scanStatSheets(): Promise<StatSheet[]> {
|
||||
const paths = await getPathsByExtension("svg");
|
||||
const sheets: StatSheet[] = [];
|
||||
|
||||
for (const filePath of paths) {
|
||||
if (!/\.sheet\.svg$/i.test(filePath)) continue;
|
||||
try {
|
||||
const content = await getIndexedData(filePath);
|
||||
if (!content) continue;
|
||||
const fileName = filePath.split("/").filter(Boolean).pop() || filePath;
|
||||
const id = fileName.replace(/\.sheet\.svg$/i, "");
|
||||
const title = extractSvgTitle(content);
|
||||
sheets.push({
|
||||
id,
|
||||
label: title || labelFromId(id),
|
||||
svg: content,
|
||||
source: filePath,
|
||||
});
|
||||
} catch {
|
||||
// skip unreadable files
|
||||
}
|
||||
}
|
||||
|
||||
return sheets;
|
||||
}
|
||||
|
||||
// ------------------- Init (runs eagerly at import time) -------------------
|
||||
@@ -186,7 +228,8 @@ const _initPromise: Promise<void> = (async () => {
|
||||
// 2. Fall back to client-side scan (dev/browser mode)
|
||||
try {
|
||||
const data = await scanClientSide();
|
||||
if (data.dice.length > 0 || data.links.length > 0) {
|
||||
data.statSheets = await scanStatSheets();
|
||||
if (data.dice.length > 0 || data.links.length > 0 || data.statSheets.length > 0) {
|
||||
setCompletionsState({ status: "loaded", data });
|
||||
} else {
|
||||
setCompletionsState({ status: "empty" });
|
||||
@@ -237,6 +280,7 @@ export function useJournalCompletions(): {
|
||||
sparkTables: [],
|
||||
stats: [],
|
||||
statTemplates: [],
|
||||
statSheets: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user