// The building blocks of the window's editors (console parity, docs/console-parity.md): // - useEditable: the values the files hold (project.editable = `storebound edit --show --json`, offline) // - fields (text, multi-line, number, yes / no, choice, list) that keep unsaved edits in lib/edits.ts, with the // store limit as a live counter, the checks of `check` / `scan` as the owner types, and "was: …" with a reset // - EditBar: unsaved count, undo / redo, discard, and "Review and save": the dry run of project.edit in the window // (old -> new, the diff per file, the checks, git), then Save (native dialog, then the files are written) // - ApplyPanel: a store write's dry run in the window (old -> new per store and language), then Apply (native // dialog, the real run) // - GitNote: the edited files are uncommitted in git (storebound never commits) import { useCallback, useEffect, useState, useSyncExternalStore, type ReactNode } from 'react'; import { api, type EditableResult, type EditResult, type Project, type ToolResult } from '../api'; import { t } from '../i18n'; import { type EditArea, type EditOp, edits, type FieldOp, keyOf } from '../lib/edits'; import { failureText } from '../lib/results'; import { useRead, useWrite } from '../store'; import { Icon, type IconName } from './icons'; import { Chip, Notice } from './kit'; import { emailIssue, urlIssue } from '../../../../cli/lib/listing-rules'; import { WriteButton } from './WriteButton'; /** Characters as the stores count them (code points). */ export const chars = (s: string): number => [...s].length; export function useEditable(project: Project) { return useRead(project.id, 'project.editable', { network: false }); } /** Re-renders on every change of the unsaved edits. */ export function useEdits(): typeof edits { useSyncExternalStore(edits.subscribe, edits.snapshot); return edits; } export interface FieldIssueView { level: 'error' | 'warn' | 'info'; msg: string } /** The current value of a field: the unsaved edit, else the saved one. */ export function useValue(project: Project, op: EditOp, saved: T): { value: T; dirty: boolean } { const e = useEdits(); const p = e.get(project.id, keyOf(op)); if (!p) return { value: saved, dirty: false }; const v = p.op.kind === 'config' ? (p.op.remove ? undefined : p.op.value) : p.op.value; return { value: (v ?? (typeof saved === 'string' ? '' : v)) as T, dirty: true }; } function Counter({ n, limit }: { n: number; limit: number }) { const tone = n > limit ? 'bad' : n > limit * 0.95 ? 'warn' : ''; return {n} / {limit}; } function Issues({ issues }: { issues: FieldIssueView[] }) { if (!issues.length) return null; return ( ); } interface FieldBase { project: Project; area: EditArea; label: string; hint?: ReactNode; issues?: FieldIssueView[]; id?: string; disabled?: boolean } /** A text field: one line or several, a live character counter against the store limit. */ export function TextField({ project, area, op, saved, label, limit, multiline, rows = 3, placeholder, hint, issues = [], mono, id, disabled, kind }: FieldBase & { /** The edit without its value: { kind: 'listing', store, lang, field } or { kind: 'config', path } or contact. */ op: FieldOp; saved: string; limit?: number; multiline?: boolean; rows?: number; placeholder?: string; mono?: boolean; kind?: 'url' | 'email' | 'text'; }) { const e = useEdits(); const make = (v: string): EditOp => (op.kind === 'config' ? (v === '' ? { kind: 'config', path: op.path, remove: true } : { kind: 'config', path: op.path, value: v }) : { ...op, value: v } as EditOp); const { value, dirty } = useValue(project, make(saved), saved); const fid = id ?? `ed-${keyOf(make('')).replace(/[^A-Za-z0-9_-]/g, '-')}`; const n = chars(value ?? ''); const set = (v: string) => e.set(project.id, make(v), area, saved); const common = { id: fid, className: `input${mono ? ' mono' : ''}`, value: value ?? '', placeholder, disabled, 'aria-invalid': issues.some(i => i.level === 'error') || undefined, onChange: (ev: { target: { value: string } }) => set(ev.target.value), 'data-edit': keyOf(make('')) }; return (
{limit !== undefined && }
{multiline ?