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:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* EntityEditor — the right-rail drawer for creating and editing an entity.
|
||||
*
|
||||
* Composes the generic `EntityFields` renderer plus, for characters, the
|
||||
* `RelationshipEditor`. Emits the full entity on Save and deletes on demand.
|
||||
*/
|
||||
import React from "react";
|
||||
import type { Character, Entity } from "@pubscript/core";
|
||||
import { Button } from "@pubscript/ui";
|
||||
import { EntityFields } from "./EntityFields.js";
|
||||
import { RelationshipEditor } from "./RelationshipEditor.js";
|
||||
import { RelationshipMap } from "./RelationshipMap.js";
|
||||
|
||||
interface EntityEditorProps {
|
||||
entity: Entity | null;
|
||||
entities: Entity[];
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (entity: Entity) => void;
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
function isCharacter(e: Entity): e is Character {
|
||||
return "relationships" in e;
|
||||
}
|
||||
|
||||
export function EntityEditor({
|
||||
entity,
|
||||
entities,
|
||||
open,
|
||||
onClose,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: EntityEditorProps) {
|
||||
const [draft, setDraft] = React.useState<Entity | null>(entity);
|
||||
|
||||
React.useEffect(() => {
|
||||
setDraft(entity);
|
||||
}, [entity]);
|
||||
|
||||
if (!open || !draft) return null;
|
||||
|
||||
const isExisting = entities.some((e) => e.id === draft.id);
|
||||
const isCharacterDraft = isCharacter(draft);
|
||||
const canDelete = isExisting && isCharacterDraft;
|
||||
|
||||
const title = isCharacterDraft
|
||||
? (draft.name || "Edit character")
|
||||
: "fields" in draft
|
||||
? `Edit ${draft.kind}`
|
||||
: "Edit note";
|
||||
|
||||
const patch = (p: Partial<Entity>) => {
|
||||
setDraft({ ...draft, ...p });
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
onSave(draft);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="ps-entity-editor">
|
||||
<header className="ps-entity-editor-header">
|
||||
<h2>{isExisting ? title : "New entity"}</h2>
|
||||
<button type="button" className="ps-icon-btn" aria-label="Close" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="ps-entity-editor-body">
|
||||
{isCharacterDraft ? (
|
||||
<>
|
||||
<EntityFields entity={draft} onChange={patch} />
|
||||
<div className="ps-relationship-section">
|
||||
<h3>Relationships</h3>
|
||||
<RelationshipEditor
|
||||
character={draft}
|
||||
entities={entities}
|
||||
onChange={(relationships) => patch({ relationships })}
|
||||
/>
|
||||
<div className="ps-relationship-map-wrap">
|
||||
<RelationshipMap entities={entities} focusId={draft.id} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EntityFields entity={draft} onChange={patch} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<footer className="ps-entity-editor-actions">
|
||||
<div className="ps-spacer" />
|
||||
{canDelete ? (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => onDelete(draft.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
) : null}
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={save}>Save</Button>
|
||||
</footer>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user