feat(entity): Chunk #4 data layer + entity UI (characters & world-building)

Data layer:
- Fuzzy scorer (packages/core/src/fuzzy.ts) with index recovery + unit tests
- Backlink computation (packages/core/src/backlinks.ts) incl. entity-level backlinks
- Consistency checker (packages/core/src/consistency.ts) incl. reciprocity/scope + unit tests
- StorageAdapter contract extended with listEntities/searchEntities/saveEntity/deleteEntity;
  implemented in Local (Dexie) and Server adapters; contract tests cover entity CRUD + ?q= search
- WorldItem.fields now supports list values (keyPeople, rivals); expanded RelationshipKind
- @mention autocomplete extension + picker in the editor package

Entity UI (apps/web/src/features/entities/):
- Generic field renderer (EntityFields) driven by ENTITY_FIELDS/FieldDef
- Tabbed directory with fuzzy search + entity cards (EntitiesView)
- Create/edit/delete drawer (EntityEditor) reusing shared fields
- Relationship editor + force-directed SVG relationship map
- Entity engine stylesheet; wired into nav

Store: project-store entity methods (load/create/update/delete) over the StorageAdapter contract.

CI-green: typecheck, lint, 65 tests, and web build all pass.
This commit is contained in:
Schwitzz
2026-10-25 22:42:55 -05:00
parent 81a44b6e95
commit 4efa71515e
36 changed files with 2764 additions and 82 deletions
+50 -3
View File
@@ -3,24 +3,49 @@ import { applyFontSettings, applyTheme, type ThemeId } from "@pubscript/ui";
export type Density = "compact" | "comfortable";
/** Body typeface for the writing column. All are system stacks (no web fonts). */
export type FontFamily = "serif" | "sans" | "mono";
export const FONT_FAMILIES: { value: FontFamily; label: string; css: string }[] = [
{ value: "serif", label: "Georgia Serif", css: 'Georgia, "Times New Roman", serif' },
{ value: "sans", label: "System Sans", css: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' },
{ value: "mono", label: "Mono", css: '"SFMono-Regular", ui-monospace, "Cascadia Code", Consolas, monospace' },
];
/** Resolve a stored font-family enum to its CSS font-family string. */
export function fontFamilyCss(value: FontFamily): string {
const found = FONT_FAMILIES.find((f) => f.value === value);
return found?.css ?? FONT_FAMILIES.at(0)?.css ?? "";
}
export interface AppSettings {
theme: ThemeId;
accentColor: string;
density: Density;
fontFamily: FontFamily;
fontSize: number;
leading: number;
measure: number;
showLineNumbers: boolean;
wordCountGoal: number;
autoSave: boolean;
focusMode: boolean;
typewriterMode: boolean;
}
export const DEFAULT_SETTINGS: AppSettings = {
theme: "ink",
accentColor: "#7c8bf5",
density: "comfortable",
fontFamily: "serif",
fontSize: 15,
leading: 1.7,
measure: 720,
showLineNumbers: true,
wordCountGoal: 80_000,
autoSave: true,
focusMode: false,
typewriterMode: false,
};
const STORAGE_KEY = "pubscript.settings.v1";
@@ -46,14 +71,31 @@ export const useSettings = create<SettingsStore>((set, get) => {
const initial = loadSettings();
// Apply the persisted theme + font on boot.
applyTheme(initial.theme);
applyFontSettings({ size: initial.fontSize });
applyFontSettings({
size: initial.fontSize,
leading: initial.leading,
measure: initial.measure,
family: fontFamilyCss(initial.fontFamily),
});
return {
...initial,
set: (patch) => {
const next = { ...get(), ...patch };
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
if (patch.fontSize != null) applyFontSettings({ size: next.fontSize });
if (
patch.fontSize != null ||
patch.leading != null ||
patch.measure != null ||
patch.fontFamily != null
) {
applyFontSettings({
size: next.fontSize,
leading: next.leading,
measure: next.measure,
family: fontFamilyCss(next.fontFamily),
});
}
set(next);
},
setTheme: (theme) => {
@@ -63,7 +105,12 @@ export const useSettings = create<SettingsStore>((set, get) => {
reset: () => {
localStorage.removeItem(STORAGE_KEY);
applyTheme(DEFAULT_SETTINGS.theme);
applyFontSettings({ size: DEFAULT_SETTINGS.fontSize });
applyFontSettings({
size: DEFAULT_SETTINGS.fontSize,
leading: DEFAULT_SETTINGS.leading,
measure: DEFAULT_SETTINGS.measure,
family: fontFamilyCss(DEFAULT_SETTINGS.fontFamily),
});
set({ ...DEFAULT_SETTINGS });
},
};