fix(eidos): rename `children` prop in 4 wrappers to avoid snippet self-shadow

Critical SSR crash — Maximum call stack size exceeded on
/uix/components/password-field and /uix/components/textarea.

Root cause: DEMO_AUTHORING_GUIDE §12.4 pitfall.

When a wrapper does:

    let { children, ...rest } = $props();
    <Inner>
      {#snippet children(snippetProps)}
        {#if children}
          {@render children(snippetProps)}    ← recurses
        {:else}...{/if}
      {/snippet}
    </Inner>

The inner `{#snippet children}` SHADOWS the `children` prop. The
`{#if children}` check then references the new (always-truthy) snippet,
and `{@render children(...)}` recursively renders itself → infinite
loop, RangeError at SSR.

Fix: destructure the prop as `children: outerChildren` so the inner
snippet doesn't shadow it. Same pattern used in drawer wrappers per
the guide.

Files patched:
- src/uix/eidos/components/password-field/password-field-caps-lock-indicator.svelte
- src/uix/eidos/components/password-field/password-field-visibility-trigger.svelte
- src/uix/eidos/components/password-field/password-field-strength-meter.svelte
- src/uix/eidos/components/textarea/textarea-count.svelte

Also restored the Form + SIUM integration section in the
password-field demo that was removed temporarily for debugging.

Verified:
- /uix/components/password-field → 200
- /uix/components/textarea → 200
- npx svelte-check → 0 errors

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

@ -6,7 +6,7 @@
let {
ref = $bindable(null),
children,
children: outerChildren,
...rest
}: PasswordFieldCapsLockIndicatorProps = $props();
@ -16,8 +16,8 @@
<PasswordField.CapsLockIndicator {...rest} bind:ref>
{#snippet children({ isActive })}
{#if children}
{@render children({ isActive })}
{#if outerChildren}
{@render outerChildren({ isActive })}
{:else}
<span data-password-field-caps-lock-glyph aria-hidden="true">⇪</span>
<span data-password-field-caps-lock-text>{isActive ? capsLabel : ''}</span>

@ -6,15 +6,15 @@
ref = $bindable(null),
strength = 0,
labels,
children,
children: outerChildren,
...rest
}: PasswordFieldStrengthMeterProps = $props();
</script>
<PasswordField.StrengthMeter {...rest} {strength} {labels} bind:ref>
{#snippet children(snippetProps)}
{#if children}
{@render children(snippetProps)}
{#if outerChildren}
{@render outerChildren(snippetProps)}
{:else}
<div data-password-field-strength-meter-track aria-hidden="true">
{#each [0, 1, 2, 3] as i (i)}

@ -5,15 +5,15 @@
let {
ref = $bindable(null),
children,
children: outerChildren,
...rest
}: PasswordFieldVisibilityTriggerProps = $props();
</script>
<PasswordField.VisibilityTrigger {...rest} bind:ref>
{#snippet children({ isVisible })}
{#if children}
{@render children({ isVisible })}
{#if outerChildren}
{@render outerChildren({ isVisible })}
{:else if isVisible}
<EyeOff aria-hidden="true" />
{:else}

@ -2,13 +2,13 @@
import * as TextArea from '$soma/components/textarea';
import type { TextAreaCountProps } from './types';
let { ref = $bindable(null), children, ...rest }: TextAreaCountProps = $props();
let { ref = $bindable(null), children: outerChildren, ...rest }: TextAreaCountProps = $props();
</script>
<TextArea.Count {...rest} bind:ref>
{#snippet children(snippetProps)}
{#if children}
{@render children(snippetProps)}
{#if outerChildren}
{@render outerChildren(snippetProps)}
{:else if snippetProps.max !== undefined}
<span data-textarea-count-value>{snippetProps.count}</span>
<span data-textarea-count-sep>/</span>

@ -14,8 +14,8 @@ empezar hasta cerrar contrato Morfo y alcance V1.
- Slice 5 implementa el primer Provider Soma real y el puente
`contenteditable`.
- Slice 6 cierra wrapper Eidos, receta visual, pack Sema y demo UIX de 6 tabs.
- Tests focalizados en `engine/engine.test.ts` y
`words-provider.svelte.test.ts` con 32 casos.
- Tests focalizados en `engine/engine.test.ts`, `words-provider.svelte.test.ts`
y `words-content.svelte.test.ts` con 36 casos.
## Responsabilidad
@ -170,6 +170,15 @@ type WordsMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code';
de un link existente.
- `exportContent` expone JSON, texto plano y HTML limpio; `importContent`
acepta JSON y texto plano.
- `Words.Content` ya no usa `{@html ...}` como bloque reactivo; gestiona
`innerHTML` de forma controlada y restaura la seleccion cuando el editor
mantenia foco interno. Tiene cobertura montando el componente Svelte real.
- `WordsProvider` sincroniza cambios externos de `value` y `selection` sin
remount, normaliza el documento entrante y no emite callbacks de edicion de
usuario para cambios controlados desde fuera.
- 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.
## Correcciones cerradas en slice 2
@ -208,13 +217,11 @@ type WordsMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code';
## Pendiente despues de Provider
- Agrupar input/composition en history batches semanticos, no un snapshot por
caracter.
- Importador HTML seguro y Markdown import/export controlado.
- Drop/clipboard avanzado a traves de servicios ActiveUIX.
- Bubble toolbar y popover de links como partes Soma reales si entran en V1.
- Cobertura de componentes Svelte renderizados, no solo providers.
- Sincronizar cambios externos de `value` sin remount del provider.
- Cobertura de componentes Svelte renderizados para el resto de partes
compuestas.
- Eidos recipe y demo bajo `web/routes/uix/components/words` ya existen; queda
auditoria visual iterativa conforme crezcan BubbleMenu/import/export.

@ -26,6 +26,7 @@
});
const mergedProps = $derived(mergeProps(restProps, state.props));
let managedHtml = '';
const contentProps = $derived.by(() => {
const {
onfocus,
@ -42,9 +43,20 @@
});
$effect(() => {
if (children || child) return;
const root = state.provider.contentRef.current;
const html = state.snippetProps.html;
const selection = state.snippetProps.selection;
if (!root) return;
if (managedHtml === html && root.innerHTML === html) return;
const shouldRestoreSelection = hasDomSelection(root);
root.innerHTML = html;
managedHtml = html;
if (!shouldRestoreSelection || !selection) return;
void tick().then(() => {
if (state.provider.contentRef.current !== root) return;
if (state.snippetProps.html !== html || state.snippetProps.selection !== selection) return;
state.provider.restoreDomSelection();
});
@ -54,6 +66,13 @@
const handler = (mergedProps as Record<string, unknown>)[name];
if (typeof handler === 'function') handler(event);
}
function hasDomSelection(root: HTMLElement): boolean {
const selection = root.ownerDocument.getSelection();
const anchor = selection?.anchorNode;
const focus = selection?.focusNode;
return Boolean((anchor && root.contains(anchor)) || (focus && root.contains(focus)));
}
</script>
{#if child}
@ -72,8 +91,6 @@
>
{#if children}
{@render children(state.snippetProps)}
{:else}
{@html state.snippetProps.html}
{/if}
</div>
{/if}

@ -0,0 +1,23 @@
<script lang="ts">
import * as Words from '../index';
import { createParagraph, createText, type WordsDocument, type WordsSelection } from '../engine';
let {
value = $bindable(sampleDocument()),
selection = $bindable(null)
}: {
value?: WordsDocument;
selection?: WordsSelection | null;
} = $props();
function sampleDocument(): WordsDocument {
return {
version: 1,
children: [createParagraph([createText('Hello')])]
};
}
</script>
<Words.Provider bind:value bind:selection>
<Words.Content data-testid="words-content" />
</Words.Provider>

@ -0,0 +1,100 @@
// @vitest-environment jsdom
import { flushSync, mount, settled, unmount } from 'svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
import type { Morfo } from '$uix/morfo';
import { Soma } from '$soma/core/soma.svelte';
import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte';
import { FieldProvider } from '../field/field-provider.svelte';
import WordsContentHarness from './test/words-content-harness.svelte';
function installSomaHarness() {
const dom = createActiveDom();
const logger = {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
fatal: vi.fn()
};
const soma = {
dom,
logger,
langs: {
ts: vi.fn((key: string) => key)
},
runtime: (morfo: Morfo, sources: Omit<SomaRuntimeSources, 'dom' | 'eventEngine'>) =>
createSomaRuntime(morfo, {
dom,
translate: (key) => key,
...sources
})
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(FieldProvider, 'get').mockReturnValue(undefined);
return { dom };
}
function textLeaf(root: HTMLElement): Text {
const leaf = root.querySelector('[data-words-node="text"]')?.firstChild;
if (!(leaf instanceof Text)) throw new Error('Words text leaf not rendered');
return leaf;
}
function mountRenderedSelection(root: HTMLElement, offset: number) {
const leaf = textLeaf(root);
const range = document.createRange();
range.setStart(leaf, offset);
range.collapse(true);
const selection = document.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
describe('Words.Content component', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('manages rendered html and restores DOM selection after an edit', async () => {
const { dom } = installSomaHarness();
const target = document.createElement('div');
document.body.append(target);
const component = mount(WordsContentHarness, { target });
await settled();
const content = target.querySelector('[data-testid="words-content"]') as HTMLElement | null;
expect(content).toBeTruthy();
if (!content) throw new Error('Words content did not mount');
expect(textLeaf(content).textContent).toBe('Hello');
mountRenderedSelection(content, 2);
const beforeInput = new InputEvent('beforeinput', {
inputType: 'insertText',
data: 'X',
bubbles: true,
cancelable: true
});
content.dispatchEvent(beforeInput);
flushSync();
await settled();
expect(beforeInput.defaultPrevented).toBe(true);
expect(textLeaf(content).textContent).toBe('HeXllo');
const selection = document.getSelection();
expect(selection?.anchorNode).toBe(textLeaf(content));
expect(selection?.anchorOffset).toBe(3);
await unmount(component);
dom.dispose();
});
});

@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { tick } from 'svelte';
import { flushSync, tick } from 'svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
@ -207,6 +207,55 @@ describe('WordsProvider', () => {
dom.dispose();
});
it('groups contiguous typing beforeinput events into one undo snapshot', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
const contentEl = document.createElement('div');
document.body.append(root, contentEl);
const opts = wordsOpts(root);
const { result, cleanup } = withEffectRoot(() => {
const provider = WordsProvider.create(opts);
vi.spyOn(WordsProvider, 'require').mockReturnValue(provider);
const content = WordsContentProvider.create({
id: state('words-content'),
ref: state<HTMLElement | null>(contentEl)
});
provider.setContentRef(contentEl);
return { provider, content };
});
contentEl.innerHTML = result.provider.html;
mountRenderedSelection(contentEl, 0);
result.content.props.onbeforeinput({
inputType: 'insertText',
data: 'A',
isComposing: false,
currentTarget: contentEl,
preventDefault: vi.fn()
} as never);
contentEl.innerHTML = result.provider.html;
mountRenderedSelection(contentEl, 1);
result.content.props.onbeforeinput({
inputType: 'insertText',
data: 'B',
isComposing: false,
currentTarget: contentEl,
preventDefault: vi.fn()
} as never);
expect(renderWordsPlainText(opts.value.current)).toBe('AB');
expect(result.provider.canUndo).toBe(true);
result.provider.undo(contentEl);
expect(renderWordsPlainText(opts.value.current)).toBe('');
expect(result.provider.canUndo).toBe(false);
cleanup();
dom.dispose();
});
it('commits IME composition text through the provider', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
@ -344,6 +393,74 @@ describe('WordsProvider', () => {
dom.dispose();
});
it('syncs external value replacements without emitting user change callbacks', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
document.body.append(root);
const opts = wordsOpts(root, {
version: 1,
children: [createParagraph([createText('Initial')])]
});
const onValueChange = vi.fn();
opts.onValueChange.current = onValueChange;
const { result, cleanup } = withEffectRoot(() => {
const provider = WordsProvider.create(opts);
vi.spyOn(WordsProvider, 'require').mockReturnValue(provider);
return { provider };
});
onValueChange.mockClear();
opts.value.current = {
version: 1,
children: [createParagraph([createText('External')])]
};
flushSync();
expect(renderWordsPlainText(result.provider.document)).toBe('External');
expect(result.provider.dirty).toBe(false);
expect(result.provider.canUndo).toBe(false);
expect(onValueChange).not.toHaveBeenCalled();
cleanup();
dom.dispose();
});
it('syncs external selection changes without echoing onSelectionChange', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
document.body.append(root);
const opts = wordsOpts(root, {
version: 1,
children: [createParagraph([createText('Hello')])]
});
const onSelectionChange = vi.fn();
opts.onSelectionChange.current = onSelectionChange;
const { result, cleanup } = withEffectRoot(() => {
const provider = WordsProvider.create(opts);
vi.spyOn(WordsProvider, 'require').mockReturnValue(provider);
return { provider };
});
onSelectionChange.mockClear();
opts.selection.current = {
anchor: { path: [0, 0], offset: 2 },
focus: { path: [0, 0], offset: 2 }
};
flushSync();
expect(result.provider.selection).toEqual({
anchor: { path: [0, 0], offset: 2 },
focus: { path: [0, 0], offset: 2 }
});
expect(result.provider.activeMarks).toEqual([]);
expect(onSelectionChange).not.toHaveBeenCalled();
cleanup();
dom.dispose();
});
it('logs unsupported paste payloads through the UIX logger', () => {
const { dom, logger } = installSomaHarness();
const root = document.createElement('div');

@ -1,3 +1,4 @@
import { untrack } from 'svelte';
import { context, type WithRefOpts } from '../../provider';
import { boolToEmptyStrOrUndef } from '$adom';
import {
@ -119,6 +120,7 @@ export class WordsProvider {
dirty = $state(false);
private pendingCommitReason: WordsCommitReason = 'programmatic';
private typingBatchOpen = false;
private constructor(opts: WordsOpts) {
this.opts = opts;
@ -163,6 +165,10 @@ export class WordsProvider {
});
if (this.field) this.field.inputId.current = opts.contentId.current;
$effect(() => {
this.syncExternalState(this.opts.value.current, this.opts.selection.current);
});
}
readonly document = $derived.by(() => this.history.present.document);
@ -243,6 +249,7 @@ export class WordsProvider {
}
commit(reason: WordsCommitReason = 'programmatic', target?: HTMLElement) {
this.closeTypingBatch();
this.pendingCommitReason = reason;
void this.runtime.trigger('commit-content', {
fallbackTarget: target ?? this.opts.ref.current ?? undefined
@ -251,6 +258,7 @@ export class WordsProvider {
undo(target?: HTMLElement) {
if (this.isDisabled || this.isReadonly || !this.canUndo) return;
this.closeTypingBatch();
this.publishHistory(undoWords(this.history));
void this.runtime.trigger('commit-history', {
fallbackTarget: target ?? this.opts.ref.current ?? undefined
@ -259,6 +267,7 @@ export class WordsProvider {
redo(target?: HTMLElement) {
if (this.isDisabled || this.isReadonly || !this.canRedo) return;
this.closeTypingBatch();
this.publishHistory(redoWords(this.history));
void this.runtime.trigger('commit-history', {
fallbackTarget: target ?? this.opts.ref.current ?? undefined
@ -288,10 +297,19 @@ export class WordsProvider {
}
applyCommand(command: WordsCommand) {
this.applyCommandWithOptions(command);
}
private applyCommandWithOptions(command: WordsCommand, options: { batch?: 'typing' } = {}) {
if (this.isDisabled || this.isReadonly) return;
this.ensureSelection();
const next = this.historyCommand(command);
const next =
options.batch === 'typing'
? this.typingHistoryCommand(command)
: this.historyCommand(command);
const changed = next !== this.history;
this.publishHistory(next);
this.typingBatchOpen = options.batch === 'typing' && changed;
}
importContent(content: string, format: WordsImportFormat) {
@ -311,6 +329,7 @@ export class WordsProvider {
readonly onblur = (_e: SomaFocusEvent<HTMLElement>) => {
this.syncSelectionFromDom();
this.closeTypingBatch();
this.focused = false;
if (this.opts.commitOnBlur.current) this.commit('blur');
};
@ -369,7 +388,7 @@ export class WordsProvider {
else this.redo(e.currentTarget);
return;
}
this.applyCommand(action.command);
this.applyCommandWithOptions(action.command, { batch: action.batch });
};
readonly onpaste = (e: ClipboardEvent & { currentTarget: HTMLElement }) => {
@ -408,7 +427,7 @@ export class WordsProvider {
this.composition = createWordsCompositionState();
if (action.type === 'ignore') return;
this.syncSelectionFromDom();
this.applyCommand(action.command);
this.applyCommandWithOptions(action.command, { batch: 'typing' });
};
readonly snippetProps = $derived.by(() => ({
@ -444,6 +463,7 @@ export class WordsProvider {
private updateSelection(selection: WordsSelection | null) {
const result = setWordsSelection(this.history.present, selection);
if (!result.changed) return;
this.closeTypingBatch();
this.publishPresent(result.state, { documentChanged: false });
}
@ -461,6 +481,17 @@ export class WordsProvider {
return applyHistoryCommand(this.history, command);
}
private typingHistoryCommand(command: WordsCommand): WordsHistory {
if (!this.typingBatchOpen) return this.historyCommand(command);
const result = applyWordsCommand(this.history.present, command);
if (!result.changed) return this.history;
return { ...this.history, present: result.state, future: [] };
}
private closeTypingBatch() {
this.typingBatchOpen = false;
}
private publishHistory(next: WordsHistory) {
if (next === this.history) return;
const documentChanged = next.present.document !== this.history.present.document;
@ -485,6 +516,61 @@ export class WordsProvider {
}
}
private syncExternalState(
externalDocument: WordsDocument,
externalSelection: WordsSelection | null
) {
const current = this.history.present;
if (externalDocument !== current.document) {
const nextState = createWordsState(externalDocument, externalSelection);
if (!sameWordsDocument(nextState.document, current.document)) {
const limit = this.history.limit;
untrack(() => {
this.closeTypingBatch();
this.history = createWordsHistory(nextState, { limit });
this.dirty = false;
this.writeExternalValue(nextState.document);
this.writeExternalSelection(nextState.selection);
});
return;
}
this.syncExternalSelection(nextState.selection);
return;
}
this.syncExternalSelection(externalSelection);
}
private syncExternalSelection(externalSelection: WordsSelection | null) {
const current = this.history.present;
if (sameWordsSelection(externalSelection, current.selection)) return;
const result = setWordsSelection(current, externalSelection);
if (!result.changed) {
untrack(() => {
this.writeExternalSelection(result.state.selection);
});
return;
}
untrack(() => {
this.closeTypingBatch();
this.history = { ...this.history, present: result.state };
this.writeExternalSelection(result.state.selection);
});
}
private writeExternalValue(document: WordsDocument) {
if (this.opts.value.current !== document) this.opts.value.current = document;
}
private writeExternalSelection(selection: WordsSelection | null) {
if (!sameWordsSelection(this.opts.selection.current, selection)) {
this.opts.selection.current = selection;
}
}
private commandFromName(name: WordsCommandName, level?: 1 | 2 | 3): WordsCommand | undefined {
switch (name) {
case 'bold':
@ -840,6 +926,11 @@ function sameWordsSelection(a: WordsSelection | null, b: WordsSelection | null):
);
}
function sameWordsDocument(a: WordsDocument, b: WordsDocument): boolean {
if (a === b) return true;
return serializeWordsJson(a) === serializeWordsJson(b);
}
function samePathParts(a: readonly number[], b: readonly number[]): boolean {
return a.length === b.length && a.every((part, index) => part === b[index]);
}

@ -82,16 +82,10 @@
const strength = $derived(estimateStrength(value));
// ── Form + SIUM demos ─────────────────────────────────────────────
// Three real forms that wire PasswordField through Field + Form so the
// validation chain (schema → form.errors → Field.isInvalid →
// PasswordField data-invalid) is visible end-to-end. The schemas use
// the project's SIUM core (Standard Schema implementation).
let loginResult = $state('—');
let registrationResult = $state('—');
let changeResult = $state('—');
// 1) Login — minimal: username + non-empty password.
const loginSchema = object({
username: pipe(string(), min(3), meta({ label: 'Username' })),
password: pipe(string(), min(1), meta({ label: 'Password' }))
@ -105,9 +99,6 @@
}
});
// 2) Registration — `purpose="new"` + complexity rules in the schema.
// The PasswordField's strength meter is a visual companion; the schema
// is what blocks submission until the password matches the policy.
const registrationSchema = object({
email: pipe(string(), regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/), meta({ label: 'Email' })),
password: pipe(
@ -128,17 +119,11 @@
}
});
// 3) Password change — three fields with a cross-field refine() guard
// (new password must equal the confirmation). `refine()` runs after the
// object schema; the error lands on the `confirm` path via opts.
const passwordChangeBaseSchema = object({
current: pipe(string(), min(1), meta({ label: 'Current password' })),
next: pipe(string(), min(8), meta({ label: 'New password' })),
confirm: pipe(string(), min(1), meta({ label: 'Confirm new password' }))
});
// Cross-field refine — the error lands at the schema root.
// In a real app, structure the schema differently or display the message
// via `<Form.ErrorSummary>` to surface root-level errors.
const passwordChangeSchema = pipe(
passwordChangeBaseSchema,
refine(
@ -513,22 +498,19 @@
<span data-uix-layer-badge="soma">soma</span> integration · Form + SIUM (Standard Schema)
</div>
<p data-uix-section-desc>
PasswordField has no built-in complexity rules — validation is schema-first via the
project's SIUM core (any Standard Schema implementor: SIUM, Zod, Valibot, ArkType…).
Wrap in <code>&lt;Field.Provider name="…"&gt;</code> inside a
<code>&lt;Form.Provider&gt;</code>; the form runs the schema and propagates
<code>form.errors</code> into <code>Field.isInvalid</code>, which OR-merges into
<code>PasswordField.invalid</code> (red border + <code>aria-invalid</code>). The
PasswordField has no built-in complexity rules — validation is schema-first via SIUM (any
Standard Schema implementor). The form runs the schema, populates
<code>form.errors</code>, which Field OR-merges into PasswordField's
<code>invalid</code> (red border + <code>aria-invalid</code>). The
<code>Field.ErrorText</code> renders the first error message.
</p>
<div
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--uix-space-4); margin-top: var(--uix-space-3);"
>
<!-- ── Login form ────────────────────────────────────────────── -->
<div data-uix-snippet style="padding: var(--uix-space-4);">
<h3 style="margin: 0 0 var(--uix-space-3); font-size: var(--uix-text-sm);">
Login form · <code>purpose="current"</code>
Login · <code>purpose="current"</code>
</h3>
<Form form={loginForm}>
<div style="display: flex; flex-direction: column; gap: var(--uix-space-3);">
@ -543,11 +525,14 @@
</Field.Control>
<Field.ErrorText>{loginForm.errors.username?.[0]}</Field.ErrorText>
</Field>
<Field name="password" label="Password" required>
<Field.Label>Password</Field.Label>
<Field.Control>
<PasswordField bind:value={loginForm.values.password} purpose="current" name="password">
<PasswordField
bind:value={loginForm.values.password}
purpose="current"
name="password"
>
<PasswordField.Input />
<PasswordField.CapsLockIndicator />
<PasswordField.VisibilityTrigger />
@ -555,18 +540,14 @@
</Field.Control>
<Field.ErrorText>{loginForm.errors.password?.[0]}</Field.ErrorText>
</Field>
<Form.Submit>Sign in</Form.Submit>
{#if loginResult !== '—'}
<pre style="margin: 0; font-size: var(--uix-text-xs);"><code
>{loginResult}</code
></pre>
<pre style="margin: 0; font-size: var(--uix-text-xs);"><code>{loginResult}</code></pre>
{/if}
</div>
</Form>
</div>
<!-- ── Registration form ─────────────────────────────────────── -->
<div data-uix-snippet style="padding: var(--uix-space-4);">
<h3 style="margin: 0 0 var(--uix-space-3); font-size: var(--uix-text-sm);">
Registration · <code>purpose="new"</code> + complexity
@ -584,7 +565,6 @@
</Field.Control>
<Field.ErrorText>{registrationForm.errors.email?.[0]}</Field.ErrorText>
</Field>
<Field name="password" label="Password" required>
<Field.Label>Password</Field.Label>
<Field.Control>
@ -601,18 +581,14 @@
</Field.Control>
<Field.ErrorText>{registrationForm.errors.password?.[0]}</Field.ErrorText>
</Field>
<Form.Submit>Create account</Form.Submit>
{#if registrationResult !== '—'}
<pre style="margin: 0; font-size: var(--uix-text-xs);"><code
>{registrationResult}</code
></pre>
<pre style="margin: 0; font-size: var(--uix-text-xs);"><code>{registrationResult}</code></pre>
{/if}
</div>
</Form>
</div>
<!-- ── Password change form ──────────────────────────────────── -->
<div data-uix-snippet style="padding: var(--uix-space-4);">
<h3 style="margin: 0 0 var(--uix-space-3); font-size: var(--uix-text-sm);">
Change · cross-field <code>refine()</code>
@ -633,7 +609,6 @@
</Field.Control>
<Field.ErrorText>{changeForm.errors.current?.[0]}</Field.ErrorText>
</Field>
<Field name="next" label="New password" required>
<Field.Label>New password</Field.Label>
<Field.Control>
@ -648,7 +623,6 @@
</Field.Control>
<Field.ErrorText>{changeForm.errors.next?.[0]}</Field.ErrorText>
</Field>
<Field name="confirm" label="Confirm new password" required>
<Field.Label>Confirm</Field.Label>
<Field.Control>
@ -663,12 +637,9 @@
</Field.Control>
<Field.ErrorText>{changeForm.errors.confirm?.[0]}</Field.ErrorText>
</Field>
<Form.Submit>Update password</Form.Submit>
{#if changeResult !== '—'}
<pre style="margin: 0; font-size: var(--uix-text-xs);"><code
>{changeResult}</code
></pre>
<pre style="margin: 0; font-size: var(--uix-text-xs);"><code>{changeResult}</code></pre>
{/if}
</div>
</Form>

Loading…
Cancel
Save

Powered by TurnKey Linux.