feat(entity): Chunk #4 remaining items — consistency UI, context rail, palette search, expanded seed & DATA_MODEL

- Consistency tab in EntitiesView surfacing findInconsistencies() results
- ContextRail + MentionPopover wired into WritingView for entity context/hover
- CommandPalette indexes entities and chapters for cross-entity search
- link field type for entity references in character fields
- findDeadCharacterMentions consistency check + tests
- Expand seed with a location, note, and a natural Elias Vell mention so the
  dead-character-after-death case is actually exercised
- Author docs/DATA_MODEL.md documenting core schemas, types, storage contract
- Rename CharacterMention id attr -> entityId to match core backlinks/consistency
This commit is contained in:
Schwitzz
2026-10-26 12:02:03 -05:00
parent 4efa71515e
commit 301e842c5c
17 changed files with 1265 additions and 65 deletions
@@ -12,6 +12,14 @@ import { ENTITY_FIELDS } from "@pubscript/core";
import { Input } from "@pubscript/ui";
import { Select } from "@pubscript/ui";
/** A short display label for an entity, regardless of kind. */
export function entityLabel(e: Entity): string {
if ("body" in e) {
return e.body.trim().slice(0, 40) || "(untitled note)";
}
return e.name;
}
/** Read a field value from an entity, regardless of where it is stored. */
export function getFieldValue(e: Entity, key: string): string | string[] {
if ("fields" in e) {
@@ -40,12 +48,15 @@ export function fieldsFor(e: Entity): FieldDef[] {
interface EntityFieldsProps {
entity: Entity;
onChange: (patch: Partial<Entity>) => void;
/** All entities in the book, used to resolve `link` field references. */
entities?: Entity[];
}
function renderField(
field: FieldDef,
value: string | string[],
onChange: (v: string | string[]) => void,
entities?: Entity[],
) {
const label = (
<label className="ps-field-label">
@@ -79,9 +90,22 @@ function renderField(
);
case "list":
return <TagField value={Array.isArray(value) ? value : []} onChange={onChange} />;
case "link":
case "link": {
const opts = (entities ?? [])
.filter((e) => e.id !== value)
.map((e) => ({ value: e.id, label: entityLabel(e) }));
return (
<div>
{label}
<Select
value={typeof value === "string" ? value : ""}
options={[{ value: "", label: "— none —" }, ...opts]}
onChange={(ev) => onChange(ev.target.value)}
/>
</div>
);
}
case "text":
default:
return (
<div className="ps-field-span-2">
{label}
@@ -146,7 +170,7 @@ function TagField({
);
}
export function EntityFields({ entity, onChange }: EntityFieldsProps) {
export function EntityFields({ entity, onChange, entities }: EntityFieldsProps) {
const fields = fieldsFor(entity);
return (
@@ -157,6 +181,7 @@ export function EntityFields({ entity, onChange }: EntityFieldsProps) {
field,
getFieldValue(entity, field.key),
(v) => onChange({ [field.key]: v } as Partial<Entity>),
entities,
)}
</div>
))}