refactor(words): drop vertical toolbar orientation

The companion drawer + new floating overlays (image bar, block handle,
block inserter) assume a horizontal toolbar above the content. The
vertical layout was a CSS-grid two-column repositioning that fights
that geometry and was the cause of the new design breaking when the
user switched to vertical.

`horizontal` is the only orientation now. Cuts:

Morfo (`morfo/components/words.ts`)
  - Toolbar + ToolbarGroup parts: `data-orientation` becomes a literal
    `'horizontal'` (no longer reads from `propRef('orientation')`).
  - Same for the toolbar's `aria-orientation`.

Soma (`soma/components/words/types.ts`, `exports.ts`, provider)
  - Removed `WordsToolbarOrientation` type entirely.
  - Removed `orientation?` from WordsToolbarProps and WordsToolbarGroupProps.
  - Provider opts no longer read or pass through `orientation`.

Eidos (`eidos/components/words/{types,words.svelte,words-toolbar.svelte,words.css}`)
  - `WordsProps.toolbarOrientation` removed.
  - `<Words.Toolbar>` / `<Words.ToolbarGroup>` no longer accept or
    forward `orientation`.
  - Six chunks of CSS that targeted `[data-orientation='vertical']`
    deleted (toolbar layout, content grid placement, status, find-replace,
    link-editor, placeholder).

Demo (`web/routes/uix/components/words/+page.svelte`)
  - `orientations` const, `orientation` state, the chip-control UI,
    the `toolbarOrientation={orientation}` prop, and the corresponding
    line in the generated code snippet all removed.
  - Recipe-selector entry for `[data-orientation]` deleted.
  - A11y row text simplified.

Verified: `npm run check` clean (only pre-existing 6 errors unrelated);
soma words tests 152/152 pass. Eidos vertical CSS is gone end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent a62b23538d
commit 71c24dca9b

@ -20,8 +20,7 @@ import type {
WordsHeadingLevel, WordsHeadingLevel,
WordsListKind, WordsListKind,
WordsSlashCommandId, WordsSlashCommandId,
WordsSlashCommandItem, WordsSlashCommandItem
WordsToolbarOrientation
} from '$soma/components/words'; } from '$soma/components/words';
import type { ButtonRounded, ButtonSize, ButtonVariant } from '$uix/eidos/components/button'; import type { ButtonRounded, ButtonSize, ButtonVariant } from '$uix/eidos/components/button';
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'; import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
@ -141,8 +140,6 @@ export type WordsProps = ProviderProps & {
color?: WordsColor; color?: WordsColor;
/** Optional default toolbar renderer. Manual compound children still win for custom UI. */ /** Optional default toolbar renderer. Manual compound children still win for custom UI. */
toolbar?: WordsToolbarConfig; toolbar?: WordsToolbarConfig;
/** Orientation for the toolbar rendered from the `toolbar` prop. @default 'horizontal' */
toolbarOrientation?: WordsToolbarOrientation;
/** /**
* Toolbar layout strategy (`grouped` family popovers vs `inline` flat * Toolbar layout strategy (`grouped` family popovers vs `inline` flat
* commands). Accepts ResponsiveProp for breakpoint-aware switching — * commands). Accepts ResponsiveProp for breakpoint-aware switching —

@ -5,7 +5,6 @@
import type { WordsToolbarProps } from './types'; import type { WordsToolbarProps } from './types';
let { let {
orientation = 'horizontal',
'aria-label': ariaLabel = 'Formatting toolbar', 'aria-label': ariaLabel = 'Formatting toolbar',
children, children,
child: outerChild, child: outerChild,
@ -15,9 +14,9 @@
const toolbarAriaLabel = $derived(ariaLabel ?? undefined); const toolbarAriaLabel = $derived(ariaLabel ?? undefined);
</script> </script>
<Words.Toolbar {...rest} {orientation} aria-label={toolbarAriaLabel}> <Words.Toolbar {...rest} aria-label={toolbarAriaLabel}>
{#snippet child({ props: wordsProps })} {#snippet child({ props: wordsProps })}
<Toolbar {orientation} aria-label={toolbarAriaLabel} size="sm" variant="ghost"> <Toolbar aria-label={toolbarAriaLabel} size="sm" variant="ghost">
{#snippet child({ props: toolbarProps })} {#snippet child({ props: toolbarProps })}
{@const props = mergeProps(toolbarProps, wordsProps)} {@const props = mergeProps(toolbarProps, wordsProps)}
{#if outerChild} {#if outerChild}

@ -125,12 +125,6 @@
opacity: var(--words-disabled-opacity); opacity: var(--words-disabled-opacity);
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']) {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
grid-template-rows: auto auto;
}
/* ── Companion Drawer (right-rail) ─────────────────────────────────────── /* ── Companion Drawer (right-rail) ───────────────────────────────────────
Layout: when the editor host contains a [data-words-drawer], place Layout: when the editor host contains a [data-words-drawer], place
the drawer in a dedicated right column that spans toolbar+content+ the drawer in a dedicated right column that spans toolbar+content+
@ -1022,10 +1016,6 @@
color: var(--words-threat-text); color: var(--words-threat-text);
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']):has([data-words-find-replace]) {
grid-template-rows: auto auto auto;
}
[data-words-toolbar] { [data-words-toolbar] {
/* Horizontal layout: single row with horizontal scroll on narrow /* Horizontal layout: single row with horizontal scroll on narrow
viewports. Was previously `flex-wrap: wrap` which produced 2-3 viewports. Was previously `flex-wrap: wrap` which produced 2-3
@ -1063,20 +1053,6 @@
if the toolbar's intrinsic content fits, the mask still applies but if the toolbar's intrinsic content fits, the mask still applies but
is harmless (it fades empty space). Keeping for simplicity. */ is harmless (it fades empty space). Keeping for simplicity. */
[data-words-toolbar][data-orientation='vertical'] {
grid-column: 1;
grid-row: 1;
flex-direction: column;
flex-wrap: nowrap;
align-items: stretch;
inline-size: max-content;
border-block-end: 0;
border-inline-end: var(--words-border-width) solid var(--words-toolbar-border);
overflow-x: visible;
overflow-y: auto;
mask-image: none;
}
[data-words-toolbar-group] { [data-words-toolbar-group] {
display: inline-flex; display: inline-flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -1086,12 +1062,6 @@
flex-shrink: 0; flex-shrink: 0;
} }
[data-words-toolbar-group][data-orientation='vertical'] {
flex-direction: column;
flex-wrap: nowrap;
align-items: stretch;
}
[data-words-bubble-menu] { [data-words-bubble-menu] {
--_words-bubble-offset: var(--words-toolbar-py-md); --_words-bubble-offset: var(--words-toolbar-py-md);
--_words-bubble-y-shift: calc(-100% - var(--_words-bubble-offset)); --_words-bubble-y-shift: calc(-100% - var(--_words-bubble-offset));
@ -1677,15 +1647,6 @@
font-size: var(--words-status-font-size); font-size: var(--words-status-font-size);
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-link-editor] {
grid-template-columns: minmax(0, 1fr) auto;
}
[data-words]:has([data-words-toolbar][data-orientation='vertical']):has([data-words-link-editor])
[data-words-status] {
grid-row: 3;
}
[data-words-placeholder] { [data-words-placeholder] {
display: block; display: block;
padding-block-start: var(--_words-content-py); padding-block-start: var(--_words-content-py);
@ -1694,12 +1655,6 @@
pointer-events: none; pointer-events: none;
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-placeholder] {
grid-column: 2;
grid-row: 1;
z-index: 1;
}
[data-words-placeholder][hidden], [data-words-placeholder][hidden],
[data-words-hidden-input][hidden] { [data-words-hidden-input][hidden] {
display: none; display: none;
@ -1723,11 +1678,6 @@
-webkit-user-select: text; -webkit-user-select: text;
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-content] {
grid-column: 2;
grid-row: 1;
}
[data-words-placeholder]:not([hidden]) + [data-words-content] { [data-words-placeholder]:not([hidden]) + [data-words-content] {
padding-block-start: var(--words-content-py-after-placeholder); padding-block-start: var(--words-content-py-after-placeholder);
} }
@ -2161,11 +2111,6 @@
font-size: var(--words-status-font-size); font-size: var(--words-status-font-size);
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-status] {
grid-column: 1 / -1;
grid-row: 2;
}
[data-words-status][data-status='error'] { [data-words-status][data-status='error'] {
color: var(--words-status-error-color); color: var(--words-status-error-color);
} }
@ -2198,16 +2143,6 @@
color: var(--_words-accent-text); color: var(--_words-accent-text);
} }
[data-words]:has([data-words-toolbar][data-orientation='vertical']) [data-words-find-replace] {
grid-column: 1 / -1;
grid-row: 2;
}
[data-words]:has([data-words-toolbar][data-orientation='vertical']):has([data-words-find-replace])
[data-words-status] {
grid-row: 3;
}
[data-words-find-row], [data-words-find-row],
[data-words-replace-row] { [data-words-replace-row] {
display: flex; display: flex;

@ -32,7 +32,6 @@
variant = 'surface', variant = 'surface',
color = 'primary', color = 'primary',
toolbar = false, toolbar = false,
toolbarOrientation = 'horizontal',
toolbarLayout = 'grouped', toolbarLayout = 'grouped',
value = $bindable(), value = $bindable(),
selection = $bindable(null), selection = $bindable(null),
@ -302,9 +301,9 @@
{#snippet children(snippetProps)} {#snippet children(snippetProps)}
{#if visibleToolbarGroups.length} {#if visibleToolbarGroups.length}
<Toolbar orientation={toolbarOrientation} aria-label="Formatting toolbar"> <Toolbar aria-label="Formatting toolbar">
{#each visibleToolbarGroups as group} {#each visibleToolbarGroups as group}
<ToolbarGroup orientation={toolbarOrientation}> <ToolbarGroup>
{#each group as item} {#each group as item}
{#if isToolbarFamilyConfig(item)} {#if isToolbarFamilyConfig(item)}
{#if resolvedToolbarLayout === 'inline'} {#if resolvedToolbarLayout === 'inline'}

@ -381,13 +381,13 @@ export const wordsMorfo = {
data: [ data: [
{ {
attr: 'data-orientation', attr: 'data-orientation',
values: ['horizontal', 'vertical'], values: ['horizontal'],
value: v.propRef('orientation') value: v.literal('horizontal')
}, },
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' } { attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' }
], ],
aria: [ aria: [
{ attr: 'aria-orientation', value: v.propRef('orientation') }, { attr: 'aria-orientation', value: v.literal('horizontal') },
{ {
attr: 'aria-label', attr: 'aria-label',
value: v.translationRef('#?components.words.toolbar|Editor toolbar'), value: v.translationRef('#?components.words.toolbar|Editor toolbar'),
@ -406,8 +406,8 @@ export const wordsMorfo = {
data: [ data: [
{ {
attr: 'data-orientation', attr: 'data-orientation',
values: ['horizontal', 'vertical'], values: ['horizontal'],
value: v.propRef('orientation') value: v.literal('horizontal')
} }
], ],
aria: [] aria: []

@ -21,7 +21,6 @@ export type {
WordsSlashCommandId, WordsSlashCommandId,
WordsSlashCommandItem, WordsSlashCommandItem,
WordsStatus, WordsStatus,
WordsToolbarOrientation,
WordsProps as ProviderProps, WordsProps as ProviderProps,
WordsContentProps as ContentProps, WordsContentProps as ContentProps,
WordsPlaceholderProps as PlaceholderProps, WordsPlaceholderProps as PlaceholderProps,

@ -24,7 +24,6 @@ import type {
export type WordsSelectionKind = 'none' | 'collapsed' | 'range'; export type WordsSelectionKind = 'none' | 'collapsed' | 'range';
export type WordsStatus = 'idle' | 'error'; export type WordsStatus = 'idle' | 'error';
export type WordsToolbarOrientation = 'horizontal' | 'vertical';
export type WordsCommitReason = 'programmatic' | 'blur' | 'button'; export type WordsCommitReason = 'programmatic' | 'blur' | 'button';
export type WordsBubbleMenuSide = 'top' | 'bottom'; export type WordsBubbleMenuSide = 'top' | 'bottom';
export type WordsSlashCommandId = export type WordsSlashCommandId =
@ -313,13 +312,11 @@ export type WordsPlaceholderProps = WithChild<{ id?: string }> &
export type WordsToolbarProps = WithChild<{ export type WordsToolbarProps = WithChild<{
id?: string; id?: string;
orientation?: WordsToolbarOrientation;
}> & }> &
Without<PrimitiveDivAttributes, { role?: unknown; 'aria-orientation'?: unknown }>; Without<PrimitiveDivAttributes, { role?: unknown; 'aria-orientation'?: unknown }>;
export type WordsToolbarGroupProps = WithChild<{ export type WordsToolbarGroupProps = WithChild<{
id?: string; id?: string;
orientation?: WordsToolbarOrientation;
}> & }> &
Without<PrimitiveDivAttributes, { role?: unknown }>; Without<PrimitiveDivAttributes, { role?: unknown }>;

@ -22,8 +22,7 @@ import type {
WordsSelectionKind, WordsSelectionKind,
WordsSlashCommandId, WordsSlashCommandId,
WordsSlashCommandItem, WordsSlashCommandItem,
WordsStatus, WordsStatus
WordsToolbarOrientation
} from './types'; } from './types';
import { import {
actionFromBeforeInput, actionFromBeforeInput,
@ -98,10 +97,8 @@ interface WordsOpts
interface WordsContentOpts extends WithRefOpts {} interface WordsContentOpts extends WithRefOpts {}
interface WordsPlaceholderOpts extends WithRefOpts {} interface WordsPlaceholderOpts extends WithRefOpts {}
interface WordsToolbarOpts interface WordsToolbarOpts extends WithRefOpts {}
extends WithRefOpts, ActiveProps<{ orientation: WordsToolbarOrientation }> {} interface WordsToolbarGroupOpts extends WithRefOpts {}
interface WordsToolbarGroupOpts
extends WithRefOpts, ActiveProps<{ orientation: WordsToolbarOrientation }> {}
interface WordsBubbleMenuOpts extends WithRefOpts, ActiveProps<{ side: WordsBubbleMenuSide }> {} interface WordsBubbleMenuOpts extends WithRefOpts, ActiveProps<{ side: WordsBubbleMenuSide }> {}
interface WordsSlashMenuOpts extends WithRefOpts {} interface WordsSlashMenuOpts extends WithRefOpts {}
interface WordsCommandButtonOpts interface WordsCommandButtonOpts
@ -1833,7 +1830,6 @@ export class WordsToolbarProvider {
ref: opts.ref, ref: opts.ref,
owner: this, owner: this,
props: { props: {
orientation: () => this.opts.orientation.current,
disabled: () => this.provider.isDisabled disabled: () => this.provider.isDisabled
}, },
syncAttrs: true syncAttrs: true
@ -1843,8 +1839,7 @@ export class WordsToolbarProvider {
readonly props = $derived.by(() => readonly props = $derived.by(() =>
this.runtimePart.assert({ this.runtimePart.assert({
...this.runtimePart.props, ...this.runtimePart.props,
role: 'toolbar' as const, role: 'toolbar' as const
'aria-orientation': this.opts.orientation.current
} as const) } as const)
); );
} }
@ -1865,9 +1860,7 @@ export class WordsToolbarGroupProvider {
id: opts.id, id: opts.id,
ref: opts.ref, ref: opts.ref,
owner: this, owner: this,
props: { props: {},
orientation: () => this.opts.orientation.current
},
syncAttrs: true syncAttrs: true
}); });
} }

@ -18,8 +18,7 @@
WordsExportFormat, WordsExportFormat,
WordsInline, WordsInline,
WordsImportFormat, WordsImportFormat,
WordsSelection, WordsSelection
WordsToolbarOrientation
} from '$soma/components/words'; } from '$soma/components/words';
import { import {
parseWordsHtml, parseWordsHtml,
@ -92,7 +91,6 @@
'threat', 'threat',
'loss' 'loss'
]; ];
const orientations: WordsToolbarOrientation[] = ['horizontal', 'vertical'];
const presets: DocumentPreset[] = ['sample', 'empty', 'long']; const presets: DocumentPreset[] = ['sample', 'empty', 'long'];
const exportFormats: WordsExportFormat[] = ['json', 'text', 'html', 'markdown']; const exportFormats: WordsExportFormat[] = ['json', 'text', 'html', 'markdown'];
const importFormats: WordsImportFormat[] = ['json', 'text', 'html', 'markdown']; const importFormats: WordsImportFormat[] = ['json', 'text', 'html', 'markdown'];
@ -193,7 +191,6 @@
let showSlashMenu = $state(true); let showSlashMenu = $state(true);
let showStatus = $state(true); let showStatus = $state(true);
let showDrawer = $state(true); let showDrawer = $state(true);
let orientation = $state<WordsToolbarOrientation>('horizontal');
let toolbarLayout = $state<'grouped' | 'inline'>('grouped'); let toolbarLayout = $state<'grouped' | 'inline'>('grouped');
let size = $state<WordsSize>('md'); let size = $state<WordsSize>('md');
let variant = $state<WordsVariant>('surface'); let variant = $state<WordsVariant>('surface');
@ -236,7 +233,7 @@
const somaBodyLines = $derived( const somaBodyLines = $derived(
[ [
...somaToolbarSnippet(toolbarSnippetGroups, orientation), ...somaToolbarSnippet(toolbarSnippetGroups),
...(somaUsesCodeLanguagePicker ? somaCodeLanguageSnippet() : []), ...(somaUsesCodeLanguagePicker ? somaCodeLanguageSnippet() : []),
...(showLinkEditor && toolbarChoice === 'none' ? somaLinkEditorSnippet() : []), ...(showLinkEditor && toolbarChoice === 'none' ? somaLinkEditorSnippet() : []),
showFindReplace && toolbarChoice === 'none' && ' <Words.FindReplace />', showFindReplace && toolbarChoice === 'none' && ' <Words.FindReplace />',
@ -297,7 +294,6 @@
color !== 'primary' && ` color="${color}"`, color !== 'primary' && ` color="${color}"`,
toolbarChoice !== 'none' && ` toolbar={${JSON.stringify(toolbarSnippetGroups)}}`, toolbarChoice !== 'none' && ` toolbar={${JSON.stringify(toolbarSnippetGroups)}}`,
toolbarChoice === 'none' && ' toolbar={false}', toolbarChoice === 'none' && ' toolbar={false}',
orientation !== 'horizontal' && ` toolbarOrientation="${orientation}"`,
disabled && ' disabled', disabled && ' disabled',
readonly && ' readonly', readonly && ' readonly',
required && ' required', required && ' required',
@ -333,12 +329,6 @@
{ selector: '[data-words][data-focused]', source: 'morfo', kind: 'soma', note: 'focus ring' }, { selector: '[data-words][data-focused]', source: 'morfo', kind: 'soma', note: 'focus ring' },
{ selector: '[data-words][data-invalid]', source: 'morfo', kind: 'soma', note: 'risk border' }, { selector: '[data-words][data-invalid]', source: 'morfo', kind: 'soma', note: 'risk border' },
{ selector: '[data-words-toolbar]', source: 'morfo', kind: 'soma', note: 'toolbar shell' }, { selector: '[data-words-toolbar]', source: 'morfo', kind: 'soma', note: 'toolbar shell' },
{
selector: '[data-words-toolbar][data-orientation]',
source: 'morfo',
kind: 'soma',
note: 'toolbar axis'
},
{ {
selector: '[data-words-command-button][data-state]', selector: '[data-words-command-button][data-state]',
source: 'morfo', source: 'morfo',
@ -782,10 +772,7 @@
return JSON.stringify(value); return JSON.stringify(value);
} }
function somaToolbarSnippet( function somaToolbarSnippet(groups: readonly WordsToolbarGroupConfig[]): string[] {
groups: readonly WordsToolbarGroupConfig[],
toolbarOrientation: WordsToolbarOrientation
): string[] {
if (groups.length === 0) return []; if (groups.length === 0) return [];
const expandedGroups = groups.map((group) => group.flatMap(expandToolbarItem)); const expandedGroups = groups.map((group) => group.flatMap(expandToolbarItem));
const toolbarGroups = expandedGroups const toolbarGroups = expandedGroups
@ -795,7 +782,7 @@
return [ return [
...(toolbarGroups.length ...(toolbarGroups.length
? [ ? [
` <Words.Toolbar orientation="${toolbarOrientation}">`, ' <Words.Toolbar>',
...toolbarGroups.flatMap((group) => [ ...toolbarGroups.flatMap((group) => [
' <Words.ToolbarGroup>', ' <Words.ToolbarGroup>',
...group.flatMap((command) => somaCommandSnippet(command)), ...group.flatMap((command) => somaCommandSnippet(command)),
@ -1037,7 +1024,6 @@
{variant} {variant}
{color} {color}
toolbar={toolbarConfig} toolbar={toolbarConfig}
toolbarOrientation={orientation}
{toolbarLayout} {toolbarLayout}
onValueCommit={(_, reason) => (lastCommit = reason)} onValueCommit={(_, reason) => (lastCommit = reason)}
> >
@ -1181,20 +1167,6 @@
{/each} {/each}
</span> </span>
</label> </label>
<label data-uix-control>
<span data-uix-control-label>toolbar orientation</span>
<span data-uix-chips role="radiogroup">
{#each orientations as item}
<button
data-uix-chip
data-active={orientation === item}
onclick={() => (orientation = item)}
>
{item}
</button>
{/each}
</span>
</label>
<label data-uix-control> <label data-uix-control>
<span data-uix-control-label>export format</span> <span data-uix-control-label>export format</span>
<span data-uix-chips role="radiogroup"> <span data-uix-chips role="radiogroup">
@ -1787,7 +1759,7 @@
> >
<tr <tr
><td class="name">Toolbar</td><td ><td class="name">Toolbar</td><td
><code>role="toolbar"</code> with orientation propagated to ARIA.</td ><code>role="toolbar"</code> with <code>aria-orientation="horizontal"</code>.</td
></tr ></tr
> >
<tr <tr

Loading…
Cancel
Save

Powered by TurnKey Linux.