diff --git a/src/uix/eidos/components/words/types.ts b/src/uix/eidos/components/words/types.ts index f8b104134..e44b8ab98 100644 --- a/src/uix/eidos/components/words/types.ts +++ b/src/uix/eidos/components/words/types.ts @@ -31,4 +31,10 @@ export type WordsProps = Omit & { size?: ResponsiveProp; /** Where the block inspector lives. @default 'sidebar' */ inspector?: WordsInspectorMode; + /** + * Show the formatting menu bar at the top of the editor (history / block + * style / marks / lists / align / insert + Edit·Preview + inspector + * toggle). @default true + */ + toolbar?: boolean; }; diff --git a/src/uix/eidos/components/words/words-menubar.svelte b/src/uix/eidos/components/words/words-menubar.svelte new file mode 100644 index 000000000..f9469dea7 --- /dev/null +++ b/src/uix/eidos/components/words/words-menubar.svelte @@ -0,0 +1,311 @@ + + + + +
+ + +
+ + +
+ + + {blockLabel} + {#snippet endIcon()}{/snippet} + + + {#each TURN_INTO as item (item.id)} + + {item.label} + {#if item.active}{/if} + + {/each} + + +
+ + +
+ {#each MARKS as mark (mark.command)} + {@const Glyph = mark.icon} + + {#snippet child({ props })} + + {/snippet} + + {/each} +
+ + +
+ {#each LISTS as list (list.kind)} + {@const Glyph = list.icon} + + {/each} +
+ + +
+ {#each ALIGN as a (a.command)} + {@const Glyph = a.icon} + + {#snippet child({ props })} + + {/snippet} + + {/each} +
+ + +
+ + + {#snippet icon()}{/snippet} + + + {#each INSERTS as ins (ins.id)} + {@const Glyph = ins.icon} + api.insertBlockOfType(ins.id)}> + + {ins.label} + + {/each} + + +
+ + + + + {#if showInspectorToggle} + + {/if} + + { + if (v[0] === 'edit' || v[0] === 'preview') mode = v[0]; + }} + aria-label="Edit or preview" + > + Edit + Preview + +
diff --git a/src/uix/eidos/components/words/words.css b/src/uix/eidos/components/words/words.css index 6ed2b1fe7..f4e7f771e 100644 --- a/src/uix/eidos/components/words/words.css +++ b/src/uix/eidos/components/words/words.css @@ -26,6 +26,9 @@ --_words-content-max: var(--words-content-max-block-size-md); --_words-content-font-size: var(--words-font-size-lg); --_words-radius: var(--words-radius-md); + /* Fixed height of the top menu bar — shared so the absolute inspector + panels (sidebar / drawer) can offset themselves below it. */ + --_words-bar-h: 2.75rem; position: relative; display: flex; @@ -80,6 +83,53 @@ min-block-size: calc(var(--_words-content-min) + 2 * var(--_words-content-py)); } +/* ── Menu bar (formatting toolbar) ────────────────────────────────────── */ + +/* A fixed-height top row of formatting controls + view toggles. The frame + is `flex-direction: column`, so the bar sits above the content naturally; + the absolute inspector panels offset below it via `--_words-bar-h`. */ +[data-words-menubar] { + display: flex; + align-items: center; + gap: var(--space-1); + flex-wrap: nowrap; + block-size: var(--_words-bar-h); + padding-inline: var(--space-2); + overflow-x: auto; + overflow-y: hidden; + border-block-end: var(--words-border-width) solid var(--words-border); + background: var(--words-bg); + border-start-start-radius: var(--_words-radius); + border-start-end-radius: var(--_words-radius); + scrollbar-width: thin; +} +/* Groups read as clusters separated by hairline dividers. */ +[data-words-menubar-group] { + display: flex; + align-items: center; + gap: 1px; + padding-inline: var(--space-1); + border-inline-end: 1px solid var(--words-border); +} +[data-words-menubar-group]:last-of-type { + border-inline-end: none; +} +[data-words-menubar-spacer] { + flex: 1 1 auto; +} +/* Active formatting button — marks reflect via `aria-pressed` (CommandButton), + list / align / inspector via `data-active`. Both read as the same accent. */ +[data-words-menubar] [data-active], +[data-words-menubar] [aria-pressed='true'] { + color: var(--color-primary-solid); + background: color-mix(in srgb, var(--color-primary-solid) 12%, transparent); +} +/* Park the docked inspector panels below the bar so they don't cover it. */ +[data-words]:has([data-words-menubar]) [data-words-sidebar], +[data-words]:has([data-words-menubar]) [data-words-drawer-panel] { + inset-block-start: var(--_words-bar-h); +} + /* ── Left gutter — block handle ───────────────────────────────────────── */ [data-words-block-gutter] { diff --git a/src/uix/eidos/components/words/words.svelte b/src/uix/eidos/components/words/words.svelte index d10d9e3e8..9430de2f3 100644 --- a/src/uix/eidos/components/words/words.svelte +++ b/src/uix/eidos/components/words/words.svelte @@ -23,6 +23,7 @@ import WordsBlockGutter from './words-block-gutter.svelte'; import WordsBubble from './words-bubble.svelte'; import WordsInspector from './words-inspector.svelte'; + import WordsMenubar from './words-menubar.svelte'; import { WORDS_INSPECTOR_BUNDLE } from './langs-inspector'; import { WORDS_MENU_BUNDLE } from './langs-menu'; import type { WordsProps } from './types'; @@ -32,6 +33,7 @@ value = $bindable(), selection = $bindable(null), inspector = 'sidebar', + toolbar = true, ...headlessProps }: WordsProps = $props(); @@ -59,6 +61,23 @@ let contentEl = $state(null); let drawerOpen = $state(false); + let sidebarOpen = $state(true); + + // Edit vs preview. Preview maps to the provider's `readonly` — the surface + // becomes non-editable (contenteditable=false) and the editing chrome + // (gutter / drag / bubble) is suppressed; the menu bar stays visible so the + // toggle is reachable. The menu bar owns the toggle via `bind:mode`. + let mode = $state<'edit' | 'preview'>('edit'); + + // One "is the inspector open" concept the menu bar's toggle drives: + // the drawer's open state, or the sidebar's visibility. + const inspectorOpen = $derived( + inspector === 'drawer' ? drawerOpen : inspector === 'sidebar' ? sidebarOpen : false + ); + function toggleInspector() { + if (inspector === 'drawer') drawerOpen = !drawerOpen; + else if (inspector === 'sidebar') sidebarOpen = !sidebarOpen; + } {#if mounted} @@ -66,17 +85,35 @@ {...headlessProps} bind:value bind:selection + readonly={mode === 'preview'} data-size={resolvedSize} data-inspector={inspector} + data-mode={mode} > {#snippet children(api)} + {#if toolbar} + + {/if} - + + {#if mode === 'edit'} + + {/if} {#if contentEl} - - + {#if mode === 'edit'} + + + {/if} {/if} - {#if inspector === 'sidebar'} + {#if inspector === 'sidebar' && sidebarOpen} {:else if inspector === 'drawer'} - - {#if !drawerOpen} - - {/if} +