Editors · Words

Words

Rich-text editor primitive with canonical JSON, contenteditable bridge, command toolbar, form serialization and semantic events. Morfo declares the editor parts and semantic events; Soma owns state, selection, input, links and history; Eidos only paints the emitted data contract.

parts{compiled.parts.order.length} events{events.length} sizes{sizes.length} colors{colors.length} apg textbox ↗
{#key documentRevision} (lastCommit = reason)} > {#snippet children({ selectedLink })} {#if showLinkEditor && toolbarChoice === 'none'} {/if} {placeholder} {#if showBubbleMenu} {#if showLinkEditor} {/if} {/if} {#if showStatus} {invalid ? 'Invalid content' : `${required ? 'Required editor' : 'Optional editor'} · ${wordCount} words · ${selectedLink ? selectedLink.href : lastCommit}`} {/if} {/snippet} {/key}
trace {#if trace.length === 0} focus the editor or use toolbar commands to see events {:else} {#each trace.slice(0, 3) as entry} {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''} {fmtTime(entry.at)} {/each} {/if} selection {selectionState} words {wordCount}
{#if tab === 'live'}

Controls

soma owns document, selection and input. eidos owns size, variant and color.

soma props · editor state
{#each ['disabled', 'readonly', 'required', 'invalid', 'commitOnBlur', 'linkEditor', 'bubbleMenu', 'status'] as key} {/each}
eidos props · visual treatment
Live document
blocks{value.children.length}
words{wordCount}
selection{selectionState}
toolbar{toolbarShape}
bubble menu{bubbleState}
export{exportFormat}
import{importFeedback}
link editor{showLinkEditor ? 'shown' : 'hidden'}
last commit{lastCommit}
Document exchange
soma export · {exportFormat} {exportFormat}
{exportPreview}
soma headless · document and commands svelte
{somaSnippet}
eidos visual · compound editor svelte
{eidosSnippet}
{/if} {#if tab === 'api'}

API surface

Prop / partOwnerDefaultNotes
valuesomaempty documentCanonical JSON document, bindable.
selectionsomanullDocument selection, bindable.
placeholdersoma-Used by Placeholder while empty.
namesoma-Enables HiddenInput JSON serialization.
disabled / readonlysomafalseDisables editing or keeps read-only focus semantics.
required / invalidsomafalseARIA and recipe state, Field-compatible.
commitOnBlursomatrueFires commit-content on blur.
onValueCommitsoma-Receives document and reason.
importContentsomajson | text | html | markdownSnippet helper that replaces the document through the normalized engine.
exportContentsomajson | text | html | markdownSnippet helper for clean export without DOM metadata.
Contentsomarequiredcontenteditable textbox surface.
toolbareidosfalseOptional property: false, true, minimal, formatting, full or custom command groups.
Toolbar / ToolbarGroupsomaoptionalManual composition stays available for fully custom command layouts.
CommandButtonsomaoptionalRuns command names and exposes active state.
BubbleMenusoma/eidosoptionalSelection-anchored toolbar that appears only for text ranges and reuses command buttons.
ToolPopovereidosoptionalReusable toolbar tool shell: icon trigger, floating panel, close button and dismiss guard.
LinkEditorsoma/eidosoptionalForm part that applies or removes links against the current selection.
selectedLinksomanullSnippet state for the link under the current document selection.
insertLink / unlinksomahelpersSelection-aware commands exposed to custom toolbars and link forms.
HiddenInput / StatussomaoptionalForm serialization and polite status.
sizeeidosmd{sizes.join(' | ')}
varianteidossurface{variants.join(' | ')}
coloreidosprimary{colors.join(' | ')}
reference rowauditV1React Aria/WAI-ARIA inform textbox + toolbar; Lexical informs editor model; Edra/Tiptap stays a benchmark, not a dependency.
{/if} {#if tab === 'morfo'}

Morfo contract

name{wordsMorfo.name}
kebab{wordsMorfo.kebab}
scope{wordsMorfo.scope.join(' + ')}
apgtextbox
parts{wordsMorfo.parts.length}
events{wordsMorfo.events.length}
Parts overview
{#each partsList as part} {/each}
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab} [{part.marker}] <{part.defaultElement}> {part.role ?? '-'} {part.archetype ?? '-'} {part.states?.join(' | ') ?? '-'} {part.optional ? 'yes' : 'no'}
{#each wordsMorfo.parts as rawPart} {@const partAny = rawPart as unknown as RawPart} {@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []} {@const ariaAttrs = partAny.aria ?? []} {@const keyboard = partAny.keyboard ?? []} {#if dataAttrs.length || ariaAttrs.length || keyboard.length}
{partAny.kebab}
{#if dataAttrs.length}
{#each dataAttrs as attr} {/each}
Data attrValuesSource
{attr.attr} {attr.values ? attr.values.join(' | ') : '-'} {'value' in attr ? describeValue((attr as { value?: unknown }).value) : '-'}
{/if} {#if ariaAttrs.length}
{#each ariaAttrs as attr} {/each}
ARIA attrSourceConditionSeverity
{attr.attr} {describeValue(attr.value)} {describeCondition(attr.condition)} {attr.severity ?? '-'}
{/if} {#if keyboard.length}
{#each keyboard as key} {/each}
KeyAction
{key.key}{key.action}
{/if} {/if} {/each}
Events declaration
{#each wordsMorfo.events as event} {@const eventAny = event as unknown as RawEvent} {/each}
NameFamilyVerbSequenceIntentTargetPrewriteCommit
{eventAny.name} {eventAny.semantic.family} {eventAny.semantic.verb ?? '-'} {eventAny.semantic.sequence ?? '-'} {typeof eventAny.semantic.intent === 'string' ? eventAny.semantic.intent : describeValue(eventAny.semantic.intent)} {describeTarget(eventAny.semantic.target)} {eventAny.prewrite ? 'yes' : '-'} {eventAny.commit ? 'yes' : '-'}
{/if} {#if tab === 'sema'}

sema · events + playback

Typing stays local until it commits; discrete focus, format, link, history, clear and invalid input actions emit semantic events.

{#each events as action} {/each}
NameFamilyVerbSequenceIntentTargetPlay
{action.name} {action.semantic.family} {action.semantic.verb ?? '-'} {action.semantic.sequence ?? '-'} {action.semantic.intent ?? '-'} {action.target}
{/if} {#if tab === 'recipe'}

Eidos recipe

Selectors live in src/uix/eidos/components/words/words.css; public tokens are declared under THEME_BASE_RECIPE_TOKENS.words.

{#each recipeSelectors as row} {/each}
SelectorSourceRole
{row.selector} {row.source} {row.note}
{/if} {#if tab === 'a11y'}

Accessibility

ConcernContract
Contentrole="textbox", aria-multiline="true" and contenteditable managed by Soma.
Toolbarrole="toolbar" with orientation propagated to ARIA.
ButtonsCommand buttons are native buttons; toggle commands expose aria-pressed.
BubbleMenuContextual selection toolbar uses role="toolbar", keeps focus from collapsing the editor selection on pointer down, and hides when no text range is available.
Link editorLink editing is a native form inside a reusable tool popover; URL, apply, unlink and close controls stay keyboard reachable and operate on Soma selection state.
ReadonlyReadonly keeps content focusable and sets aria-readonly.
DisabledDisabled removes editing and prevents command execution.
StatusStatus uses role="status" and aria-live="polite".
FormHiddenInput serializes the full JSON document when name is present.
Keyboard
{#each (wordsMorfo.parts.find((part) => part.kebab === 'content') as unknown as RawPart | undefined)?.keyboard ?? [] as key} {/each}
KeyAction
{key.key}{key.action}
{/if}