@ -4,6 +4,8 @@
// settings drawer) lands in F3.
// settings drawer) lands in F3.
import Words from '$uix/eidos/components/words';
import Words from '$uix/eidos/components/words';
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
import { Eye , Pencil , FileText , Check } from '$uix/eidos/components/icon';
import { serializeHtml } from '$soma/components/words';
import type { WordsInspectorMode } from '$uix/eidos/components/words/types';
import type { WordsInspectorMode } from '$uix/eidos/components/words/types';
import type {
import type {
WordsDocument,
WordsDocument,
@ -191,6 +193,48 @@
}
}
]
]
});
});
// ── Topbar state ──────────────────────────────────────────────────────
// Edit / Preview. Preview renders the document as clean read-only HTML
// via the export-only `serializeHtml` serializer.
let mode = $state< 'edit' | 'preview'>('edit');
const EDIT_MODES: readonly { v : 'edit' | 'preview' ; label : string } [] = [
{ v : 'edit' , label : 'Edit' } ,
{ v : 'preview' , label : 'Preview' }
];
const previewHtml = $derived(mode === 'preview' ? serializeHtml(value) : '');
// Breadcrumb title — the first heading's text, else a placeholder.
const docTitle = $derived.by(() => {
for (const b of value.children) {
if (b.type === 'heading') {
const text = b.children
.map((c) => (c.type === 'text' ? c.text : ''))
.join('')
.trim();
if (text) return text;
}
}
return 'Untitled document';
});
// "Saved" indicator — flips to "Editing…" on any document change, settles
// back to "Saved" after a short idle. Demo-only chrome (a real app would
// debounce a network save here).
let saveState = $state< 'saved' | 'editing'>('saved');
let saveTimer: ReturnType< typeof setTimeout > | undefined;
let trackedInitial = false;
$effect(() => {
void value; // re-run whenever the bound document changes
if (!trackedInitial) {
trackedInitial = true;
return;
}
saveState = 'editing';
clearTimeout(saveTimer);
saveTimer = setTimeout(() => (saveState = 'saved'), 600);
return () => clearTimeout(saveTimer);
});
< / script >
< / script >
< header >
< header >
@ -202,27 +246,67 @@
< / p >
< / p >
< / header >
< / header >
< div class = "demo-controls" >
< div class = "demo-topbar" >
< span class = "demo-controls-label" > Inspector< / span >
< div class = "demo-doc" >
< ToggleGroup
< FileText size = "sm" / >
type="single"
< span class = "demo-doc-title" > { docTitle } </ span >
size="xs"
< span class = "demo-save" data-state = { saveState } >
variant="outline"
{ #if saveState === 'saved' } < Check size = "sm" /> Saved{ : else } Editing…{ /if }
attached
< / span >
value={[ inspector ]}
< / div >
onValueChange={( v : string []) => {
< div class = "demo-topbar-actions" >
if (v[0]) inspector = v[0] as WordsInspectorMode;
< span class = "demo-controls-label" > Inspector< / span >
}}
< ToggleGroup
aria-label="Inspector mode"
type="single"
>
size="xs"
{ #each INSPECTOR_MODES as m ( m . v )}
variant="outline"
< ToggleGroup.Item value = { m . v } > { m . label } </ToggleGroup.Item >
attached
{ /each }
value={[ inspector ]}
< / ToggleGroup >
onValueChange={( v : string []) => {
if (v[0]) inspector = v[0] as WordsInspectorMode;
}}
aria-label="Inspector mode"
>
{ #each INSPECTOR_MODES as m ( m . v )}
< ToggleGroup.Item value = { m . v } > { m . label } </ToggleGroup.Item >
{ /each }
< / ToggleGroup >
< ToggleGroup
type="single"
size="xs"
variant="outline"
attached
value={[ mode ]}
onValueChange={( v : string []) => {
if (v[0]) mode = v[0] as 'edit' | 'preview';
}}
aria-label="Edit or preview"
>
{ #each EDIT_MODES as m ( m . v )}
< ToggleGroup.Item value = { m . v } >
{ #if m . v === 'edit' } < Pencil size = "sm" /> { : else } < Eye size = "sm" /> { /if }
{ m . label }
< / ToggleGroup.Item >
{ /each }
< / ToggleGroup >
< / div >
< / div >
< / div >
< div class = "demo-canvas" >
< div class = "demo-canvas" data-mode = { mode } >
< Words bind:value { inspector } placeholder = "Write something…" onUploadImage = { handleUploadImage } / >
{ #if mode === 'edit' }
< Words
bind:value
{ inspector }
placeholder="Write something…"
onUploadImage={ handleUploadImage }
/>
{ : else }
<!-- Preview: the document rendered as clean, read - only HTML (the
export-only `serializeHtml`). Content is the user's own document,
escaped + URL-sanitized by the serializer. -->
<!-- eslint - disable - next - line svelte/no - at - html - tags -->
< article class = "demo-preview" > { @html previewHtml } </ article >
{ /if }
< / div >
< / div >
< details class = "demo-trace" >
< details class = "demo-trace" >
@ -244,11 +328,52 @@
inline-size: 100%;
inline-size: 100%;
max-inline-size: 62rem;
max-inline-size: 62rem;
}
}
.demo-controls {
.demo-topbar {
display: flex;
display: flex;
align-items: center;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-3);
gap: var(--space-3);
margin-block-end: var(--space-3);
margin-block-end: var(--space-3);
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-md);
background: var(--color-surface-raised);
}
.demo-doc {
display: flex;
align-items: center;
gap: var(--space-2);
min-inline-size: 0;
color: var(--color-content-secondary);
}
.demo-doc-title {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--color-content-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.demo-save {
display: inline-flex;
align-items: center;
gap: var(--space-1);
margin-inline-start: var(--space-1);
font-size: var(--font-size-xs);
transition: color var(--duration-fast) var(--ease-default);
}
.demo-save[data-state='saved'] {
color: var(--color-affirm-text, var(--color-content-secondary));
}
.demo-save[data-state='editing'] {
color: var(--color-content-muted);
}
.demo-topbar-actions {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
}
}
.demo-controls-label {
.demo-controls-label {
font-size: var(--font-size-sm);
font-size: var(--font-size-sm);
@ -270,4 +395,82 @@
background: var(--color-surface-overlay);
background: var(--color-surface-overlay);
border-radius: var(--radius-md);
border-radius: var(--radius-md);
}
}
/* Preview mode — the document as a clean reading view. The preview is
bare serialized HTML, so it carries its own typography (the editor's
`[data-words]` CSS does not apply here). */
.demo-canvas[data-mode='preview'] {
justify-content: flex-start;
}
.demo-preview {
inline-size: 100%;
max-inline-size: 48rem;
margin-inline: auto;
color: var(--color-content-primary);
font-size: var(--font-size-md);
line-height: 1.7;
}
.demo-preview :global(h1) {
font-size: var(--font-size-2xl);
font-weight: 700;
margin-block: var(--space-5) var(--space-3);
}
.demo-preview :global(h2) {
font-size: var(--font-size-xl);
font-weight: 700;
margin-block: var(--space-4) var(--space-2);
}
.demo-preview :global(h3) {
font-size: var(--font-size-lg);
font-weight: 600;
margin-block: var(--space-4) var(--space-2);
}
.demo-preview :global(p) {
margin-block: var(--space-3);
}
.demo-preview :global(ul),
.demo-preview :global(ol) {
margin-block: var(--space-3);
padding-inline-start: var(--space-6);
}
.demo-preview :global(blockquote) {
margin-block: var(--space-3);
padding-inline-start: var(--space-4);
border-inline-start: 3px solid var(--color-border-default);
color: var(--color-content-secondary);
}
.demo-preview :global(pre) {
margin-block: var(--space-3);
padding: var(--space-3);
overflow: auto;
background: var(--color-surface-overlay);
border-radius: var(--radius-md);
font-family: var(--font-mono);
font-size: var(--font-size-sm);
}
.demo-preview :global(table) {
inline-size: 100%;
margin-block: var(--space-4);
border-collapse: collapse;
}
.demo-preview :global(th),
.demo-preview :global(td) {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border-subtle);
text-align: start;
vertical-align: top;
}
.demo-preview :global(th) {
background: var(--color-surface-raised);
font-weight: 600;
}
.demo-preview :global(img) {
max-inline-size: 100%;
height: auto;
border-radius: var(--radius-md);
}
.demo-preview :global(a) {
color: var(--color-primary-text, var(--color-primary-element));
text-decoration: underline;
}
< / style >
< / style >