fix(password-field): chrome layout + meter visibility + form field styling

Three real visual bugs caught after browser verification:

1. **No visible border around input + meter row.** The CSS used
   `:has([data-password-field-strength-meter])::before` to lift the
   border onto a pseudo-element that spans grid row 1. The pseudo
   collapsed to 2px tall because it had no `align-self: stretch` and no
   explicit `min-block-size`. Added both — the pseudo now fills its
   grid cell at the full `--_password-field-height`. Plus the same
   `data-focused` / `data-invalid` state propagation already in place.

2. **Strength meter segments invisible.** Default
   `--password-field-strength-segment-bg` resolved to
   `var(--color-neutral-track)` which is `rgb(17,17,17)` — identical to
   the surface in dark mode. Changed to `var(--color-border-default)`
   so inactive segments read as visible grey rails. Also bumped segment
   height from 4 → 6 px so the bar is clearly readable.

3. **Eye icon too small inside a thin trigger.** `trigger-size`
   resolved to `--icon-size-md` (≈18 px) which made the touch target
   too small and the SVG cramped. Promoted trigger-size to use
   `--control-height-{xs..md}` so it matches the input row height per
   size. SVG sizing switched from `calc(size - 8px)` (which clipped at
   small sizes) to `65%` of trigger.

4. **Stage wrapper collapsed to content width.** The inner
   `<div style="max-inline-size: 24rem; margin-inline: auto">` lacked
   `inline-size: 100%`, so inside the stage's `display: grid` it
   shrank to its content. Added the explicit `inline-size: 100%`.

5. **Username/Email rendered as native unstyled inputs** in the
   Form/SIUM demo because they wrapped raw `<input>` instead of
   `<Field.Input>` (Field.Control only styles its dedicated Input
   subpart, not arbitrary children). Swapped to `<Field.Input>`.

6. **SIUM error refs (`#?sium.errors.min_length|...`) rendered
   literal.** `Field.ErrorText` just renders the string it receives.
   Added a local `issueText(form, field)` helper that calls
   `uix.langs.ts(raw)` to resolve the langref and then interpolates
   `{{param}}` from `form.issues[field][0].params`. Now errors render
   as proper Spanish/English text from the registered siumLangs.

Verified visually via Claude_Preview: input row has a clean border with
the eye icon at the right edge, strength meter sits below with 4 visible
segments + label, all three form columns render with consistent chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 90afe27d81
commit bb56e94864

@ -67,6 +67,9 @@
content: '';
grid-column: 1 / -1;
grid-row: 1 / 2;
align-self: stretch;
justify-self: stretch;
min-block-size: var(--_password-field-height);
border: var(--password-field-border-width) solid var(--password-field-border);
border-radius: var(--password-field-radius);
background: var(--password-field-bg);
@ -271,8 +274,8 @@
}
[data-password-field-visibility-trigger] svg {
inline-size: calc(var(--_password-field-trigger-size) - 8px);
block-size: calc(var(--_password-field-trigger-size) - 8px);
inline-size: 65%;
block-size: 65%;
}
/* ── CapsLockIndicator ─────────────────────────────────────────────── */

@ -15,10 +15,10 @@ rich-text ni wrapper visual reutilizable.
#### Feature parity matrix
| Feature | React Aria / WAI-ARIA | Lexical / svelte-lexical | Edra / Tiptap | UIX V1 | Decision |
| ------------------------ | ----------------------------------- | ------------------------ | --------------------- | ----------------------------- | --------- |
| ------------------------ | ----------------------------------- | ------------------------ | --------------------- | ---------------------------------------------- | --------- |
| Multiline editable | Textbox multiline + toolbar pattern | ContentEditable root | Editor view | `Words.Content` role textbox | implement |
| Document state | Consumer-owned value | EditorState | ProseMirror doc | JSON canonico `WordsDocument` | implement |
| Toolbar commands | Native buttons with labels | Commands/plugins | Command chain | `Words.CommandButton` | implement |
| Toolbar commands | Native buttons with labels | Commands/plugins | Command chain | `Words.CommandButton` + root `toolbar` presets | implement |
| Active formatting state | aria-pressed on toggles | Selection state | Extension state | `data-state`, `aria-pressed` | implement |
| Placeholder | Input hint pattern | Plugin/placeholder | Placeholder extension | `Words.Placeholder` | implement |
| Form serialization | Native input when needed | external | external | `Words.HiddenInput` JSON | implement |
@ -32,7 +32,9 @@ rich-text ni wrapper visual reutilizable.
selection, input, history and commands; Sema maps high-level events; Eidos
owns only wrappers, `size`/`variant`/`color` attrs and CSS.
- Composition: one compound root `<Words>` with explicit children
`<Words.Toolbar>`, `<Words.Content>`, `<Words.HiddenInput>`, etc.
`<Words.Toolbar>`, `<Words.Content>`, `<Words.HiddenInput>`, etc. The root
also offers a `toolbar` preset/config prop as a convenience renderer; manual
composition remains the full custom path.
- Sizes covered: `sm`, `md`, `lg` as composite panel sizes.
- Variants: full `ControlVariant`: `surface`, `outline`, `ghost`.
- Color intent palette: full `ColorRole`: `primary`, `secondary`, `neutral`,
@ -76,6 +78,9 @@ rich-text ni wrapper visual reutilizable.
- `WordsVariant = ControlVariant`.
- `WordsColor = ColorRole`, no paleta truncada.
- Los wrappers no exportan `Provider`; la API publica es compound.
- `toolbar` en el root acepta `false`, `true`, `minimal`, `formatting`, `full`
o grupos de comandos. Es un renderer de conveniencia; no reemplaza la
composicion manual de `<Words.Toolbar>`.
- `words.css` usa solo tokens publicos `--words-*` declarados en
`THEME_BASE_RECIPE_TOKENS.words`.
- Las marcas inline se pintan desde `data-words-marks`, emitido por el motor

@ -29,10 +29,7 @@ export type WordsToolbarCommandConfig =
listKind?: WordsListKind;
};
export type WordsToolbarGroupConfig = readonly WordsToolbarCommandConfig[];
export type WordsToolbarConfig =
| boolean
| WordsToolbarPreset
| readonly WordsToolbarGroupConfig[];
export type WordsToolbarConfig = boolean | WordsToolbarPreset | readonly WordsToolbarGroupConfig[];
export type WordsProps = ProviderProps & {
/** Composite editor density. @default 'md' */

@ -94,7 +94,10 @@
const command = typeof config === 'string' ? config : config.command;
return {
command,
label: typeof config === 'string' ? COMMAND_LABELS[command] : (config.label ?? COMMAND_LABELS[command]),
label:
typeof config === 'string'
? COMMAND_LABELS[command]
: (config.label ?? COMMAND_LABELS[command]),
...(typeof config === 'string' ? {} : { level: config.level, listKind: config.listKind })
};
}

@ -1596,11 +1596,11 @@
--password-field-trigger-color: var(--color-content-secondary);
--password-field-trigger-color-hover: var(--color-content-primary);
--password-field-trigger-focus-shadow: 0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
--password-field-icon-size-xs: var(--icon-size-xs);
--password-field-icon-size-sm: var(--icon-size-sm);
--password-field-icon-size-md: var(--icon-size-md);
--password-field-icon-size-lg: var(--icon-size-md);
--password-field-icon-size-xl: var(--icon-size-lg);
--password-field-icon-size-xs: var(--control-height-xs);
--password-field-icon-size-sm: var(--control-height-xs);
--password-field-icon-size-md: var(--control-height-sm);
--password-field-icon-size-lg: var(--control-height-sm);
--password-field-icon-size-xl: var(--control-height-md);
--password-field-caps-color: var(--color-risk-text);
--password-field-caps-bg: color-mix(in srgb, var(--color-risk-track) 50%, transparent);
--password-field-caps-font-size-xs: var(--font-size-xs);
@ -1608,9 +1608,9 @@
--password-field-caps-font-size-md: var(--font-size-xs);
--password-field-caps-font-size-lg: var(--font-size-sm);
--password-field-caps-font-size-xl: var(--font-size-sm);
--password-field-strength-segment-bg: var(--color-neutral-track);
--password-field-strength-segment-bg: var(--color-border-default);
--password-field-strength-segment-gap: 4px;
--password-field-strength-segment-height: 4px;
--password-field-strength-segment-height: 6px;
--password-field-strength-radius: var(--radius-sm);
--password-field-transition-duration: var(--duration-fast);
--password-field-transition-ease: var(--ease-default);

@ -1354,11 +1354,11 @@ export const THEME_BASE_RECIPE_TOKENS = {
'trigger-color-hover': 'var(--color-content-primary)',
'trigger-focus-shadow':
'0 0 0 var(--focus-ring-offset) var(--color-surface-default), 0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color)',
'icon-size-xs': 'var(--icon-size-xs)',
'icon-size-sm': 'var(--icon-size-sm)',
'icon-size-md': 'var(--icon-size-md)',
'icon-size-lg': 'var(--icon-size-md)',
'icon-size-xl': 'var(--icon-size-lg)',
'icon-size-xs': 'var(--control-height-xs)',
'icon-size-sm': 'var(--control-height-xs)',
'icon-size-md': 'var(--control-height-sm)',
'icon-size-lg': 'var(--control-height-sm)',
'icon-size-xl': 'var(--control-height-md)',
'caps-color': 'var(--color-risk-text)',
'caps-bg': 'color-mix(in srgb, var(--color-risk-track) 50%, transparent)',
'caps-font-size-xs': 'var(--font-size-xs)',
@ -1366,9 +1366,9 @@ export const THEME_BASE_RECIPE_TOKENS = {
'caps-font-size-md': 'var(--font-size-xs)',
'caps-font-size-lg': 'var(--font-size-sm)',
'caps-font-size-xl': 'var(--font-size-sm)',
'strength-segment-bg': 'var(--color-neutral-track)',
'strength-segment-bg': 'var(--color-border-default)',
'strength-segment-gap': '4px',
'strength-segment-height': '4px',
'strength-segment-height': '6px',
'strength-radius': 'var(--radius-sm)',
'transition-duration': 'var(--duration-fast)',
'transition-ease': 'var(--ease-default)',

@ -179,6 +179,9 @@ type WordsMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code';
- El Provider usa `batch: 'typing'` de `beforeinput` para agrupar escritura,
borrado e IME contiguos en una sola entrada de undo; seleccion externa,
blur, commit, undo/redo o comandos de formato cierran el batch.
- La seleccion de rango se sincroniza desde `selectionchange`, teclado,
puntero y `select`; `CommandButton` preserva la seleccion del editor antes
de aplicar comandos de toolbar sobre el rango seleccionado.
## Correcciones cerradas en slice 2

@ -11,7 +11,7 @@
import { compileMorfo } from '$uix/morfo';
import { passwordFieldMorfo } from '@/uix/morfo/components/password-field';
import { getActiveUix } from '$active-uix';
import { createForm } from '$libs/forms';
import { createForm, type FormIssue } from '$libs/forms';
import { object, string, pipe, min, regex, meta, refine } from '$sium/core';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
@ -144,6 +144,26 @@
estimateStrength(String(registrationForm.values.password ?? ''))
);
/**
* Resolve a SIUM error langref to the active locale.
* `#?sium.errors.min_length|Must have at least {{min}} characters` →
* the registered Spanish/English text with `{{min}}` interpolated.
*/
function issueText<T extends Record<string, unknown>>(
form: { errors: Record<string, string[] | undefined>; issues: Record<string, FormIssue[] | undefined> },
field: string
): string {
const issue = form.issues[field]?.[0];
const raw = issue?.message ?? form.errors[field]?.[0];
if (!raw) return '';
const resolved = uix.langs.ts(raw);
if (!issue?.params) return resolved;
return Object.entries(issue.params).reduce(
(acc, [key, value]) => acc.replaceAll(`{{${key}}}`, String(value)),
resolved
);
}
$effect(() => {
const el = stageRef;
if (!el) return;
@ -263,7 +283,7 @@
<!-- Live preview always rendered (between header and tabs). -->
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef} style="padding: var(--uix-space-4);">
<div style="max-inline-size: 24rem; margin-inline: auto;">
<div style="inline-size: 100%; max-inline-size: 24rem; margin-inline: auto;">
<PasswordField
bind:value
bind:visible
@ -517,13 +537,13 @@
<Field name="username" label="Username" required>
<Field.Label>Username</Field.Label>
<Field.Control>
<input
<Field.Input
type="text"
autocomplete="username"
bind:value={loginForm.values.username}
/>
</Field.Control>
<Field.ErrorText>{loginForm.errors.username?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(loginForm, 'username')}</Field.ErrorText>
</Field>
<Field name="password" label="Password" required>
<Field.Label>Password</Field.Label>
@ -536,7 +556,7 @@
<PasswordField.CapsLockIndicator />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{loginForm.errors.password?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(loginForm, 'password')}</Field.ErrorText>
</Field>
<Form.Submit>Sign in</Form.Submit>
{#if loginResult !== '—'}
@ -555,13 +575,13 @@
<Field name="email" label="Email" required>
<Field.Label>Email</Field.Label>
<Field.Control>
<input
<Field.Input
type="email"
autocomplete="email"
bind:value={registrationForm.values.email}
/>
</Field.Control>
<Field.ErrorText>{registrationForm.errors.email?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(registrationForm, 'email')}</Field.ErrorText>
</Field>
<Field name="password" label="Password" required>
<Field.Label>Password</Field.Label>
@ -575,7 +595,7 @@
<PasswordField.VisibilityTrigger />
<PasswordField.StrengthMeter strength={registrationStrength} />
</PasswordField>
<Field.ErrorText>{registrationForm.errors.password?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(registrationForm, 'password')}</Field.ErrorText>
</Field>
<Form.Submit>Create account</Form.Submit>
{#if registrationResult !== '—'}
@ -601,7 +621,7 @@
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{changeForm.errors.current?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(changeForm, 'current')}</Field.ErrorText>
</Field>
<Field name="next" label="New password" required>
<Field.Label>New password</Field.Label>
@ -613,7 +633,7 @@
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{changeForm.errors.next?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(changeForm, 'next')}</Field.ErrorText>
</Field>
<Field name="confirm" label="Confirm new password" required>
<Field.Label>Confirm</Field.Label>
@ -625,7 +645,7 @@
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{changeForm.errors.confirm?.[0]}</Field.ErrorText>
<Field.ErrorText>{issueText(changeForm, 'confirm')}</Field.ErrorText>
</Field>
<Form.Submit>Update password</Form.Submit>
{#if changeResult !== '—'}

@ -3,6 +3,8 @@
Words,
type WordsColor,
type WordsSize,
type WordsToolbarConfig,
type WordsToolbarPreset,
type WordsVariant
} from '$uix/eidos/components/words';
import type {
@ -11,22 +13,6 @@
WordsSelection,
WordsToolbarOrientation
} from '$soma/components/words';
import {
Bold,
Code,
Eraser,
Heading1,
Italic,
List,
ListOrdered,
Pilcrow,
Quote,
Redo2,
Save,
Strikethrough,
Underline,
Undo2
} from '$uix/eidos/components/icon';
import { compileMorfo } from '$uix/morfo';
import { wordsMorfo } from '@/uix/morfo/components/words';
import { getActiveUix } from '$active-uix';
@ -34,6 +20,7 @@
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type DocumentPreset = 'sample' | 'empty' | 'long';
type ToolbarChoice = 'none' | WordsToolbarPreset;
type TraceEntry = { event: string; family: string; intent?: string; at: number };
type RawValue = { kind?: string; value?: unknown; prop?: string; part?: string };
@ -85,6 +72,20 @@
];
const orientations: WordsToolbarOrientation[] = ['horizontal', 'vertical'];
const presets: DocumentPreset[] = ['sample', 'empty', 'long'];
const toolbarChoices: ToolbarChoice[] = ['none', 'minimal', 'formatting', 'full'];
const toolbarPresetGroups: Record<WordsToolbarPreset, string[][]> = {
minimal: [['bold', 'italic', 'underline']],
formatting: [
['bold', 'italic', 'underline', 'strike', 'code'],
['paragraph', 'heading', 'quote']
],
full: [
['undo', 'redo'],
['bold', 'italic', 'underline', 'strike', 'code'],
['paragraph', 'heading', 'quote', 'unordered-list', 'ordered-list'],
['clear-formatting', 'clear-document', 'commit']
]
};
let tab = $state<Tab>('live');
let preset = $state<DocumentPreset>('sample');
@ -96,7 +97,7 @@
let required = $state(false);
let invalid = $state(false);
let commitOnBlur = $state(true);
let showToolbar = $state(true);
let toolbarChoice = $state<ToolbarChoice>('full');
let showStatus = $state(true);
let orientation = $state<WordsToolbarOrientation>('horizontal');
let size = $state<WordsSize>('md');
@ -111,6 +112,12 @@
const wordCount = $derived(countWords(plainText));
const selectionState = $derived(selectionLabel(selection));
const documentJson = $derived(JSON.stringify(value, null, 2));
const toolbarConfig = $derived<WordsToolbarConfig>(
toolbarChoice === 'none' ? false : toolbarChoice
);
const toolbarSnippetGroups = $derived(
toolbarChoice === 'none' ? [] : toolbarPresetGroups[toolbarChoice]
);
const somaSnippet = $derived(
[
@ -130,16 +137,7 @@
invalid && ' invalid',
!commitOnBlur && ' commitOnBlur={false}',
'>',
showToolbar && ` <Words.Toolbar orientation="${orientation}">`,
showToolbar && ' <Words.ToolbarGroup>',
showToolbar && ' <Words.CommandButton command="bold" aria-label="Bold" />',
showToolbar && ' <Words.CommandButton command="italic" aria-label="Italic" />',
showToolbar &&
' <Words.CommandButton command="heading" level={1} aria-label="Heading" />',
showToolbar && ' <Words.CommandButton command="unordered-list" aria-label="List" />',
showToolbar && ' <Words.CommandButton command="clear-document" aria-label="Clear" />',
showToolbar && ' </Words.ToolbarGroup>',
showToolbar && ' </Words.Toolbar>',
...somaToolbarSnippet(toolbarSnippetGroups, orientation),
' <Words.Placeholder />',
' <Words.Content />',
' <Words.HiddenInput />',
@ -154,7 +152,6 @@
[
"<script lang='ts'>",
" import { Words } from '$uix/eidos/components/words';",
" import { Bold, Heading1, Italic, List, Save } from '$uix/eidos/components/icon';",
" import type { WordsDocument } from '$soma/components/words';",
` let value = $state<WordsDocument>(${documentJson});`,
'</' + 'script>',
@ -165,26 +162,15 @@
size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
toolbarChoice !== 'none' && ` toolbar="${toolbarChoice}"`,
toolbarChoice === 'none' && ' toolbar={false}',
orientation !== 'horizontal' && ` toolbarOrientation="${orientation}"`,
disabled && ' disabled',
readonly && ' readonly',
required && ' required',
invalid && ' invalid',
!commitOnBlur && ' commitOnBlur={false}',
'>',
showToolbar && ` <Words.Toolbar orientation="${orientation}">`,
showToolbar && ' <Words.ToolbarGroup>',
showToolbar &&
' <Words.CommandButton command="bold" aria-label="Bold"><Bold size="sm" decorative /></Words.CommandButton>',
showToolbar &&
' <Words.CommandButton command="italic" aria-label="Italic"><Italic size="sm" decorative /></Words.CommandButton>',
showToolbar &&
' <Words.CommandButton command="heading" level={1} aria-label="Heading"><Heading1 size="sm" decorative /></Words.CommandButton>',
showToolbar &&
' <Words.CommandButton command="unordered-list" aria-label="List"><List size="sm" decorative /></Words.CommandButton>',
showToolbar &&
' <Words.CommandButton command="commit" aria-label="Commit"><Save size="sm" decorative /></Words.CommandButton>',
showToolbar && ' </Words.ToolbarGroup>',
showToolbar && ' </Words.Toolbar>',
' <Words.Placeholder />',
' <Words.Content />',
' <Words.HiddenInput />',
@ -437,6 +423,34 @@
return JSON.stringify(value);
}
function somaToolbarSnippet(
groups: string[][],
toolbarOrientation: WordsToolbarOrientation
): string[] {
if (groups.length === 0) return [];
return [
` <Words.Toolbar orientation="${toolbarOrientation}">`,
...groups.flatMap((group) => [
' <Words.ToolbarGroup>',
...group.map((command) => somaCommandSnippet(command)),
' </Words.ToolbarGroup>'
]),
' </Words.Toolbar>'
];
}
function somaCommandSnippet(command: string): string {
const level = command === 'heading' ? ' level={1}' : '';
return ` <Words.CommandButton command="${command}"${level} aria-label="${commandLabel(command)}" />`;
}
function commandLabel(command: string): string {
return command
.split('-')
.map((part) => part[0]?.toUpperCase() + part.slice(1))
.join(' ');
}
function describeValue(value: unknown): string {
if (!value || typeof value !== 'object') return '-';
const rec = value as RawValue;
@ -521,65 +535,10 @@
{size}
{variant}
{color}
toolbar={toolbarConfig}
toolbarOrientation={orientation}
onValueCommit={(_, reason) => (lastCommit = reason)}
>
{#if showToolbar}
<Words.Toolbar {orientation}>
<Words.ToolbarGroup {orientation}>
<Words.CommandButton command="undo" aria-label="Undo"
><Undo2 size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="redo" aria-label="Redo"
><Redo2 size="sm" decorative /></Words.CommandButton
>
</Words.ToolbarGroup>
<Words.ToolbarGroup {orientation}>
<Words.CommandButton command="bold" aria-label="Bold"
><Bold size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="italic" aria-label="Italic"
><Italic size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="underline" aria-label="Underline"
><Underline size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="strike" aria-label="Strike"
><Strikethrough size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="code" aria-label="Code"
><Code size="sm" decorative /></Words.CommandButton
>
</Words.ToolbarGroup>
<Words.ToolbarGroup {orientation}>
<Words.CommandButton command="paragraph" aria-label="Paragraph"
><Pilcrow size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="heading" level={1} aria-label="Heading"
><Heading1 size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="quote" aria-label="Quote"
><Quote size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="unordered-list" aria-label="Unordered list"
><List size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="ordered-list" aria-label="Ordered list"
><ListOrdered size="sm" decorative /></Words.CommandButton
>
</Words.ToolbarGroup>
<Words.ToolbarGroup {orientation}>
<Words.CommandButton command="clear-formatting" aria-label="Clear formatting"
><Eraser size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="clear-document" aria-label="Clear document"
><Eraser size="sm" decorative /></Words.CommandButton
>
<Words.CommandButton command="commit" aria-label="Commit document"
><Save size="sm" decorative /></Words.CommandButton
>
</Words.ToolbarGroup>
</Words.Toolbar>
{/if}
<Words.Placeholder>{placeholder}</Words.Placeholder>
<Words.Content
data-perm-step={!disabled && !readonly ? '0' : undefined}
@ -621,7 +580,7 @@
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>16</span>
API <span data-uix-tab-count>17</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
@ -665,9 +624,16 @@
</label>
<label data-uix-control>
<span data-uix-control-label>toolbar</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showToolbar} />
<span data-uix-switch-label>{showToolbar ? 'shown' : 'hidden'}</span>
<span data-uix-chips role="radiogroup">
{#each toolbarChoices as item}
<button
data-uix-chip
data-active={toolbarChoice === item}
onclick={() => (toolbarChoice = item)}
>
{item}
</button>
{/each}
</span>
</label>
<label data-uix-control>
@ -829,10 +795,15 @@
>contenteditable textbox surface.</td
></tr
>
<tr
><td class="name">toolbar</td><td>eidos</td><td class="default">false</td><td
>Optional preset/config property: none, minimal, formatting or full.</td
></tr
>
<tr
><td class="name">Toolbar / ToolbarGroup</td><td>soma</td><td class="default"
>optional</td
><td>Composition controls command affordances.</td></tr
><td>Manual composition stays available for fully custom command layouts.</td></tr
>
<tr
><td class="name">CommandButton</td><td>soma</td><td class="default">optional</td><td

Loading…
Cancel
Save

Powered by TurnKey Linux.