feat(brief): locked sections, list formatting, auto/manual placeholder chips
- brief.machine: reducer refuses edits to locked (predefined) sections as
defense-in-depth; LetterSection gains a `locked` flag
- rich-text: paragraphs gain optional `list` kind; editor gets bullet/numbered
list buttons, keyboard shortcuts, and backspace-deletes-adjacent-chip
- placeholder chips distinguish auto-resolvable (grey) vs manual (yellow), in
both the editor and the read-only preview
- fix: preview chip now renders matching {…} braces (was a one-sided ⌗ glyph),
aligned with the editor's chip styling
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
} from '@shared/infrastructure/api-client';
|
||||
import { Brief, BriefStatus, LetterBlock, LetterSection, LibraryPassage, PassageScope } from '@brief/domain/brief';
|
||||
import { PlaceholderDef } from '@brief/domain/placeholders';
|
||||
import { Mark, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';
|
||||
import { Mark, Paragraph, RichTextBlock, RichTextNode } from '@shared/kernel/rich-text';
|
||||
|
||||
/**
|
||||
* The only place brief HTTP lives (ADR-0001 anti-corruption boundary). The wire
|
||||
@@ -66,6 +66,12 @@ export class BriefAdapter {
|
||||
const r = await runSubmit(() => this.client.send(), BRIEF_ACTION_FAILED);
|
||||
return r.ok ? parseBrief(r.value) : r;
|
||||
}
|
||||
|
||||
/** Demo "start over" — recreate a fresh brief server-side and return the new view. */
|
||||
async reset(): Promise<Result<string, BriefView>> {
|
||||
const r = await runSubmit(() => this.client.briefReset(), BRIEF_ACTION_FAILED);
|
||||
return r.ok ? parseBriefView(r.value) : r;
|
||||
}
|
||||
}
|
||||
|
||||
// --- parse: wire (flat) → domain (discriminated unions), validating at the boundary ---
|
||||
@@ -90,7 +96,7 @@ export function parseNode(dto: RichTextNodeDto): Result<string, RichTextNode> {
|
||||
|
||||
export function parseBlockContent(dto: RichTextBlockDto | undefined): Result<string, RichTextBlock> {
|
||||
if (!dto || !Array.isArray(dto.paragraphs)) return err('content: paragraphs not an array');
|
||||
const paragraphs = [];
|
||||
const paragraphs: Paragraph[] = [];
|
||||
for (const p of dto.paragraphs) {
|
||||
const nodes: RichTextNode[] = [];
|
||||
for (const n of p.nodes ?? []) {
|
||||
@@ -98,7 +104,8 @@ export function parseBlockContent(dto: RichTextBlockDto | undefined): Result<str
|
||||
if (!parsed.ok) return parsed;
|
||||
nodes.push(parsed.value);
|
||||
}
|
||||
paragraphs.push({ nodes });
|
||||
const list = p.list === 'bullet' || p.list === 'number' ? p.list : undefined;
|
||||
paragraphs.push(list ? { nodes, list } : { nodes });
|
||||
}
|
||||
return ok({ paragraphs });
|
||||
}
|
||||
@@ -135,7 +142,7 @@ function parseSection(dto: LetterSectionDto): Result<string, LetterSection> {
|
||||
if (!parsed.ok) return parsed;
|
||||
blocks.push(parsed.value);
|
||||
}
|
||||
return ok({ sectionKey: dto.sectionKey, title: dto.title, required: dto.required, blocks });
|
||||
return ok({ sectionKey: dto.sectionKey, title: dto.title, required: dto.required, locked: dto.locked ?? false, blocks });
|
||||
}
|
||||
|
||||
function parsePlaceholderDef(dto: PlaceholderDefDto): Result<string, PlaceholderDef> {
|
||||
@@ -245,7 +252,7 @@ function nodeToDto(n: RichTextNode): RichTextNodeDto {
|
||||
}
|
||||
|
||||
function contentToDto(content: RichTextBlock): RichTextBlockDto {
|
||||
return { paragraphs: content.paragraphs.map((p) => ({ nodes: p.nodes.map(nodeToDto) })) };
|
||||
return { paragraphs: content.paragraphs.map((p) => ({ nodes: p.nodes.map(nodeToDto), ...(p.list ? { list: p.list } : {}) })) };
|
||||
}
|
||||
|
||||
function blockToDto(b: LetterBlock): LetterBlockDto {
|
||||
@@ -255,5 +262,5 @@ function blockToDto(b: LetterBlock): LetterBlockDto {
|
||||
}
|
||||
|
||||
function sectionToDto(s: LetterSection): LetterSectionDto {
|
||||
return { sectionKey: s.sectionKey, title: s.title, required: s.required, blocks: s.blocks.map(blockToDto) };
|
||||
return { sectionKey: s.sectionKey, title: s.title, required: s.required, locked: s.locked, blocks: s.blocks.map(blockToDto) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user