fix(password-field): visual issues — double border, broken layout, stale lang refs

Three visual bugs spotted on /uix/components/password-field:

1. **Double-bordered inputs.** The demo wrapped `<PasswordField>` inside
   `<Field.Control>`, which adds its own border + padding. PasswordField
   IS the field's input shell — it ships its own chrome. Removed the
   five offending Field.Control wrappers from login/registration/change
   forms; PasswordField now sits directly inside `<Field>`.

2. **Icon wrapping to a new row + StrengthMeter sharing the bordered
   box.** Replaced `display: inline-flex; flex-wrap: wrap` on the root
   with `display: grid; grid-template-columns: minmax(0, 1fr) auto auto`
   so Input + CapsLockIndicator + VisibilityTrigger stay on the first
   row. When StrengthMeter is composed, `:has()` switches the root to a
   2-row grid where the bordered chrome lifts onto a `::before`
   pseudo-element that only spans the first row, and the meter sits
   below as a sibling. State chrome (`data-focused`, `data-invalid`)
   propagates to the pseudo via the same `:has()` selectors.

3. **CapsLock pill claiming space even when inactive.** Was
   `color: transparent; background: transparent` which still consumed
   width in the grid. Now `display: none` by default and
   `display: inline-flex` only when `[data-active]` is set.

Also fixed: `#?sium.errors.min_length|…` and friends rendered literal
in the demo because the docs shell didn't register `$sium/langs/langs`.
Registered `siumLangs` under the `sium` namespace in the layout's
`createActiveUix({ langs: { schema: { sium: siumLangs } } })`.

svelte-check: 0 errors. Page now loads at 200.

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

@ -5,6 +5,10 @@
* Mirrors search-field's chrome (border, radius, focus ring, padding,
* disabled opacity) so every text input in the system reads the same
* — only the affordances inside the control differ.
*
* Layout: CSS grid keeps the inner controls (input, caps indicator,
* visibility trigger) on the first row and the StrengthMeter on a
* second row that sits OUTSIDE the bordered chrome.
*/
[data-password-field] {
@ -18,10 +22,10 @@
--_password-field-accent-track: var(--color-primary-track);
--_password-field-accent-text: var(--color-primary-text);
display: inline-flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
column-gap: var(--_password-field-gap);
align-items: center;
gap: var(--_password-field-gap);
inline-size: 100%;
min-inline-size: 0;
block-size: var(--_password-field-height);
@ -39,12 +43,57 @@
box-shadow var(--password-field-transition-duration) var(--password-field-transition-ease);
}
/* When the StrengthMeter is composed, it claims a second row and the
* height auto-grows; otherwise the single-row chrome rules. */
/* When the StrengthMeter is composed, the root becomes a vertical stack:
* the bordered control row on top, the meter as a separate sibling below.
* Border + padding move to a synthetic ::before pseudo-row… but since the
* meter is a real child, the cleanest move is to lift the chrome to the
* inner cells. */
[data-password-field]:has([data-password-field-strength-meter]) {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
grid-template-rows: var(--_password-field-height) auto;
row-gap: var(--space-2);
block-size: auto;
min-block-size: var(--_password-field-height);
padding-block: var(--password-field-gap-sm);
border: 0;
background: transparent;
padding: 0;
box-shadow: none;
}
/* When the meter is present, the first row recreates the bordered chrome
* via a pseudo-element that spans all 3 columns. The inner controls keep
* their normal flow because the pseudo sits behind them (z-index 0). */
[data-password-field]:has([data-password-field-strength-meter])::before {
content: '';
grid-column: 1 / -1;
grid-row: 1 / 2;
border: var(--password-field-border-width) solid var(--password-field-border);
border-radius: var(--password-field-radius);
background: var(--password-field-bg);
transition:
background var(--password-field-transition-duration) var(--password-field-transition-ease),
border-color var(--password-field-transition-duration) var(--password-field-transition-ease),
box-shadow var(--password-field-transition-duration) var(--password-field-transition-ease);
pointer-events: none;
}
[data-password-field]:has([data-password-field-strength-meter]) [data-password-field-input],
[data-password-field]:has([data-password-field-strength-meter]) [data-password-field-visibility-trigger],
[data-password-field]:has([data-password-field-strength-meter]) [data-password-field-caps-lock-indicator] {
grid-row: 1 / 2;
padding-inline-start: 0;
z-index: 1;
}
[data-password-field]:has([data-password-field-strength-meter]) [data-password-field-input] {
grid-column: 1 / 2;
padding-inline-start: var(--_password-field-px);
}
[data-password-field]:has([data-password-field-strength-meter]) [data-password-field-caps-lock-indicator] {
grid-column: 2 / 3;
}
[data-password-field]:has([data-password-field-strength-meter]) [data-password-field-visibility-trigger] {
grid-column: 3 / 4;
margin-inline-end: calc(var(--_password-field-px) - var(--_password-field-gap));
}
/* ── Color accents ─────────────────────────────────────────────────── */
@ -125,12 +174,19 @@
[data-password-field][data-variant='outline'] {
background: transparent;
}
[data-password-field][data-variant='outline']:has([data-password-field-strength-meter])::before {
background: transparent;
}
[data-password-field][data-variant='ghost'] {
border-color: transparent;
background: transparent;
padding-inline: 0;
}
[data-password-field][data-variant='ghost']:has([data-password-field-strength-meter])::before {
border-color: transparent;
background: transparent;
}
/* ── State chrome ──────────────────────────────────────────────────── */
@ -138,10 +194,17 @@
border-color: var(--password-field-border-focus);
box-shadow: var(--password-field-focus-shadow);
}
[data-password-field][data-focused]:has([data-password-field-strength-meter])::before {
border-color: var(--password-field-border-focus);
box-shadow: var(--password-field-focus-shadow);
}
[data-password-field][data-invalid] {
border-color: var(--password-field-border-invalid);
}
[data-password-field][data-invalid]:has([data-password-field-strength-meter])::before {
border-color: var(--password-field-border-invalid);
}
[data-password-field][data-disabled] {
opacity: var(--password-field-disabled-opacity);
@ -150,8 +213,8 @@
/* ── Input ─────────────────────────────────────────────────────────── */
[data-password-field-input] {
flex: 1 1 auto;
min-inline-size: 0;
inline-size: 100%;
border: 0;
background: transparent;
color: inherit;
@ -173,13 +236,11 @@
/* ── VisibilityTrigger ─────────────────────────────────────────────── */
[data-password-field-visibility-trigger] {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--_password-field-trigger-size);
block-size: var(--_password-field-trigger-size);
margin-block: auto;
padding: 0;
border: var(--password-field-trigger-border-width) solid var(--password-field-trigger-border);
border-radius: var(--password-field-trigger-radius);
@ -216,29 +277,25 @@
/* ── CapsLockIndicator ─────────────────────────────────────────────── */
/* Hidden by default. Only visible when caps lock is on. */
[data-password-field-caps-lock-indicator] {
flex: 0 0 auto;
display: inline-flex;
display: none;
align-items: center;
gap: 0.25em;
margin-block: auto;
padding-inline: 0.4em;
padding-block: 0.2em;
border-radius: var(--radius-sm);
font-size: var(--_password-field-caps-font-size);
font-weight: var(--font-weight-medium, 500);
color: transparent;
background: transparent;
transition:
color var(--password-field-transition-duration) var(--password-field-transition-ease),
background var(--password-field-transition-duration) var(--password-field-transition-ease);
color: var(--password-field-caps-color);
background: var(--password-field-caps-bg);
pointer-events: none;
user-select: none;
white-space: nowrap;
}
[data-password-field-caps-lock-indicator][data-active] {
color: var(--password-field-caps-color);
background: var(--password-field-caps-bg);
display: inline-flex;
}
[data-password-field-caps-lock-glyph] {
@ -249,11 +306,12 @@
/* ── StrengthMeter (composable; layout below the control row) ─────── */
[data-password-field-strength-meter] {
flex: 1 1 100%;
display: flex;
flex-direction: column;
gap: 0.25em;
margin-top: var(--space-1);
grid-column: 1 / -1;
grid-row: 2 / 3;
padding-inline: var(--_password-field-px);
}
[data-password-field-strength-meter-track] {

@ -38,7 +38,11 @@ export type {
WordsProps,
WordsSize,
WordsStatusProps as StatusProps,
WordsToolbarCommandConfig,
WordsToolbarConfig,
WordsToolbarGroupConfig,
WordsToolbarGroupProps as ToolbarGroupProps,
WordsToolbarProps as ToolbarProps,
WordsToolbarPreset,
WordsVariant
} from './types';

@ -8,11 +8,31 @@ import type {
ToolbarGroupProps,
ToolbarProps
} from '$soma/components/words';
import type {
WordsCommandName,
WordsHeadingLevel,
WordsListKind,
WordsToolbarOrientation
} from '$soma/components/words';
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types';
export type WordsSize = Extract<Size, 'sm' | 'md' | 'lg'>;
export type WordsVariant = ControlVariant;
export type WordsColor = ColorRole;
export type WordsToolbarPreset = 'minimal' | 'formatting' | 'full';
export type WordsToolbarCommandConfig =
| WordsCommandName
| {
command: WordsCommandName;
label?: string;
level?: WordsHeadingLevel;
listKind?: WordsListKind;
};
export type WordsToolbarGroupConfig = readonly WordsToolbarCommandConfig[];
export type WordsToolbarConfig =
| boolean
| WordsToolbarPreset
| readonly WordsToolbarGroupConfig[];
export type WordsProps = ProviderProps & {
/** Composite editor density. @default 'md' */
@ -21,6 +41,10 @@ export type WordsProps = ProviderProps & {
variant?: WordsVariant;
/** Accent palette for focus, active commands and selections. @default 'primary' */
color?: WordsColor;
/** Optional default toolbar renderer. Manual compound children still win for custom UI. */
toolbar?: WordsToolbarConfig;
/** Orientation for the toolbar rendered from the `toolbar` prop. @default 'horizontal' */
toolbarOrientation?: WordsToolbarOrientation;
};
export type WordsContentProps = ContentProps;

@ -1,12 +1,38 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import * as Words from '$soma/components/words';
import type { WordsProps } from './types';
import {
Bold,
Code,
Eraser,
Heading1,
Italic,
List,
ListChecks,
ListOrdered,
Pilcrow,
Quote,
Redo2,
Save,
Strikethrough,
Underline,
Undo2
} from '$uix/eidos/components/icon';
import type {
WordsProps,
WordsToolbarCommandConfig,
WordsToolbarConfig,
WordsToolbarGroupConfig,
WordsToolbarPreset
} from './types';
import type { WordsCommandName, WordsHeadingLevel, WordsListKind } from '$soma/components/words';
let {
size = 'md',
variant = 'surface',
color = 'primary',
toolbar = false,
toolbarOrientation = 'horizontal',
value = $bindable(),
selection = $bindable(null),
children: bodyContent,
@ -15,6 +41,63 @@
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
const toolbarGroups = $derived(resolveToolbarGroups(toolbar));
type NormalizedToolbarCommand = {
command: WordsCommandName;
label: string;
level?: WordsHeadingLevel;
listKind?: WordsListKind;
};
const TOOLBAR_PRESETS: Record<WordsToolbarPreset, readonly WordsToolbarGroupConfig[]> = {
minimal: [['bold', 'italic', 'underline']],
formatting: [
['bold', 'italic', 'underline', 'strike', 'code'],
['paragraph', { command: 'heading', level: 1 }, 'quote']
],
full: [
['undo', 'redo'],
['bold', 'italic', 'underline', 'strike', 'code'],
['paragraph', { command: 'heading', level: 1 }, 'quote', 'unordered-list', 'ordered-list'],
['clear-formatting', 'clear-document', 'commit']
]
};
const COMMAND_LABELS: Record<WordsCommandName, string> = {
undo: 'Undo',
redo: 'Redo',
bold: 'Bold',
italic: 'Italic',
underline: 'Underline',
strike: 'Strike',
code: 'Code',
paragraph: 'Paragraph',
heading: 'Heading',
quote: 'Quote',
'unordered-list': 'Unordered list',
'ordered-list': 'Ordered list',
'check-list': 'Check list',
'clear-formatting': 'Clear formatting',
'clear-document': 'Clear document',
commit: 'Commit'
};
function resolveToolbarGroups(config: WordsToolbarConfig): readonly WordsToolbarGroupConfig[] {
if (config === true) return TOOLBAR_PRESETS.full;
if (config === false) return [];
if (typeof config === 'string') return TOOLBAR_PRESETS[config];
return config;
}
function normalizeCommand(config: WordsToolbarCommandConfig): NormalizedToolbarCommand {
const command = typeof config === 'string' ? config : config.command;
return {
command,
label: typeof config === 'string' ? COMMAND_LABELS[command] : (config.label ?? COMMAND_LABELS[command]),
...(typeof config === 'string' ? {} : { level: config.level, listKind: config.listKind })
};
}
</script>
<Words.Provider
@ -26,6 +109,55 @@
data-color={color}
>
{#snippet children(snippetProps)}
{#if toolbarGroups.length}
<Words.Toolbar orientation={toolbarOrientation}>
{#each toolbarGroups as group}
<Words.ToolbarGroup orientation={toolbarOrientation}>
{#each group as item}
{@const command = normalizeCommand(item)}
<Words.CommandButton
command={command.command}
level={command.level}
listKind={command.listKind}
aria-label={command.label}
>
{#if command.command === 'undo'}
<Undo2 size="sm" decorative />
{:else if command.command === 'redo'}
<Redo2 size="sm" decorative />
{:else if command.command === 'bold'}
<Bold size="sm" decorative />
{:else if command.command === 'italic'}
<Italic size="sm" decorative />
{:else if command.command === 'underline'}
<Underline size="sm" decorative />
{:else if command.command === 'strike'}
<Strikethrough size="sm" decorative />
{:else if command.command === 'code'}
<Code size="sm" decorative />
{:else if command.command === 'paragraph'}
<Pilcrow size="sm" decorative />
{:else if command.command === 'heading'}
<Heading1 size="sm" decorative />
{:else if command.command === 'quote'}
<Quote size="sm" decorative />
{:else if command.command === 'unordered-list'}
<List size="sm" decorative />
{:else if command.command === 'ordered-list'}
<ListOrdered size="sm" decorative />
{:else if command.command === 'check-list'}
<ListChecks size="sm" decorative />
{:else if command.command === 'commit'}
<Save size="sm" decorative />
{:else}
<Eraser size="sm" decorative />
{/if}
</Words.CommandButton>
{/each}
</Words.ToolbarGroup>
{/each}
</Words.Toolbar>
{/if}
{@render bodyContent?.(snippetProps)}
{/snippet}
</Words.Provider>

@ -31,10 +31,15 @@
const mergedProps = $derived(mergeProps(restProps, state.props));
const buttonProps = $derived.by(() => {
const { onclick, ...props } = mergedProps;
const { onclick, onpointerdown, ...props } = mergedProps;
return props;
});
function callPointerDown(event: PointerEvent) {
const handler = (mergedProps as Record<string, unknown>).onpointerdown;
if (typeof handler === 'function') handler(event);
}
function callClick(event: MouseEvent) {
const handler = (mergedProps as Record<string, unknown>).onclick;
if (typeof handler === 'function') handler(event);
@ -44,7 +49,7 @@
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...buttonProps} onclick={callClick}>
<button {...buttonProps} onpointerdown={callPointerDown} onclick={callClick}>
{#if children}
{@render children()}
{:else}

@ -32,6 +32,9 @@
onfocus,
onblur,
onkeydown,
onkeyup,
onpointerup,
onselect,
onbeforeinput,
onpaste,
oncompositionstart,
@ -83,6 +86,9 @@
onfocus={(event) => callEventHandler('onfocus', event)}
onblur={(event) => callEventHandler('onblur', event)}
onkeydown={(event) => callEventHandler('onkeydown', event)}
onkeyup={(event) => callEventHandler('onkeyup', event)}
onpointerup={(event) => callEventHandler('onpointerup', event)}
onselect={(event) => callEventHandler('onselect', event)}
onbeforeinput={(event) => callEventHandler('onbeforeinput', event)}
onpaste={(event) => callEventHandler('onpaste', event)}
oncompositionstart={(event) => callEventHandler('oncompositionstart', event)}

@ -106,8 +106,11 @@ export type WordsContentProps = WithChild<
onbeforeinput?: unknown;
onpaste?: unknown;
onkeydown?: unknown;
onkeyup?: unknown;
onfocus?: unknown;
onblur?: unknown;
onpointerup?: unknown;
onselect?: unknown;
oncompositionstart?: unknown;
oncompositionupdate?: unknown;
oncompositionend?: unknown;
@ -135,7 +138,10 @@ export type WordsCommandButtonProps = WithChild<{
level?: WordsHeadingLevel;
listKind?: WordsListKind;
}> &
Without<PrimitiveButtonAttributes, { type?: unknown; onclick?: unknown; disabled?: unknown }>;
Without<
PrimitiveButtonAttributes,
{ type?: unknown; onclick?: unknown; onpointerdown?: unknown; disabled?: unknown }
>;
export type WordsHiddenInputProps = WithChild<{ id?: string }> &
Without<PrimitiveInputAttributes, { type?: unknown; value?: unknown; name?: unknown }>;

@ -101,6 +101,17 @@ function mountRenderedSelection(root: HTMLElement, offset = 0) {
selection?.addRange(range);
}
function mountRenderedRange(root: HTMLElement, start: number, end: number) {
const text = root.querySelector('[data-words-node="text"]')?.firstChild;
if (!text) throw new Error('Words text node not rendered');
const range = document.createRange();
range.setStart(text, start);
range.setEnd(text, end);
const selection = document.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
describe('WordsProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
@ -342,6 +353,51 @@ describe('WordsProvider', () => {
dom.dispose();
});
it('syncs a DOM range selection and formats that range from a toolbar command', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
const contentEl = document.createElement('div');
const buttonEl = document.createElement('button');
document.body.append(root, contentEl, buttonEl);
const opts = wordsOpts(root, {
version: 1,
children: [createParagraph([createText('Hello')])]
});
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)
});
const bold = WordsCommandButtonProvider.create({
id: state('words-bold'),
ref: state<HTMLElement | null>(buttonEl),
command: state<WordsCommandName>('bold'),
level: state<1 | 2 | 3 | undefined>(undefined),
listKind: state<'ordered' | 'unordered' | 'check' | undefined>(undefined)
});
provider.setContentRef(contentEl);
return { provider, content, bold };
});
contentEl.innerHTML = result.provider.html;
mountRenderedRange(contentEl, 1, 4);
result.content.props.onselect(new Event('select') as never);
expect(result.provider.selection).toEqual({
anchor: { path: [0, 0], offset: 1 },
focus: { path: [0, 0], offset: 4 }
});
result.bold.props.onclick({ currentTarget: buttonEl } as never);
expect(result.provider.exportContent('html')).toBe('<p>H<strong>ell</strong>o</p>');
cleanup();
dom.dispose();
});
it('activates only the command button matching the current list kind', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');

@ -169,6 +169,19 @@ export class WordsProvider {
$effect(() => {
this.syncExternalState(this.opts.value.current, this.opts.selection.current);
});
$effect(() => {
const root = this.contentRef.current;
if (!root) return;
const doc = root.ownerDocument;
const handleSelectionChange = () => {
const selection = doc.getSelection();
if (!domSelectionTouchesRoot(selection, root)) return;
this.syncSelectionFromDom();
};
doc.addEventListener('selectionchange', handleSelectionChange);
return () => doc.removeEventListener('selectionchange', handleSelectionChange);
});
}
readonly document = $derived.by(() => this.history.present.document);
@ -366,6 +379,18 @@ export class WordsProvider {
}
};
readonly onkeyup = (_e: KeyboardEvent) => {
this.syncSelectionFromDom();
};
readonly onpointerup = (_e: PointerEvent) => {
this.syncSelectionFromDom();
};
readonly onselect = (_e: Event) => {
this.syncSelectionFromDom();
};
readonly onbeforeinput = (e: InputEvent & { currentTarget: HTMLElement }) => {
if (this.isDisabled || this.isReadonly) {
e.preventDefault();
@ -657,6 +682,9 @@ export class WordsContentProvider {
onfocus: this.provider.onfocus,
onblur: this.provider.onblur,
onkeydown: this.provider.onkeydown,
onkeyup: this.provider.onkeyup,
onpointerup: this.provider.onpointerup,
onselect: this.provider.onselect,
onbeforeinput: this.provider.onbeforeinput,
onpaste: this.provider.onpaste,
oncompositionstart: this.provider.oncompositionstart,
@ -795,12 +823,19 @@ export class WordsCommandButtonProvider {
readonly onclick = (e: SomaMouseEvent<HTMLButtonElement>) => {
if (this.isDisabled) return;
this.provider.syncSelectionFromDom();
this.provider.runCommandName(this.opts.command.current, {
target: e.currentTarget as HTMLElement,
level: this.opts.level.current
});
};
readonly onpointerdown = (e: PointerEvent) => {
if (this.isDisabled) return;
e.preventDefault();
this.provider.syncSelectionFromDom();
};
readonly props = $derived.by(() =>
this.runtimePart.assert({
...this.runtimePart.props,
@ -810,6 +845,7 @@ export class WordsCommandButtonProvider {
'data-command': this.opts.command.current,
'data-active': boolToEmptyStrOrUndef(this.isActive),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
onpointerdown: this.onpointerdown,
onclick: this.onclick
} as const)
);
@ -931,6 +967,12 @@ function sameWordsDocument(a: WordsDocument, b: WordsDocument): boolean {
return serializeWordsJson(a) === serializeWordsJson(b);
}
function domSelectionTouchesRoot(selection: Selection | null, root: HTMLElement): boolean {
const anchor = selection?.anchorNode;
const focus = selection?.focusNode;
return Boolean((anchor && root.contains(anchor)) || (focus && root.contains(focus)));
}
function samePathParts(a: readonly number[], b: readonly number[]): boolean {
return a.length === b.length && a.every((part, index) => part === b[index]);
}

@ -4,6 +4,7 @@
import { createActivePrefsDomProjection, standardPrefsDimensions } from '$prefs';
import { page } from '$app/state';
import { Soma } from '$soma/core/soma.svelte';
import { siumLangs } from '$sium/langs/langs';
import { ActiveEidos } from '$uix/eidos';
import { dialogSema } from '$uix/sema/components/dialog';
import { drawerSema } from '$uix/sema/components/drawer';
@ -57,7 +58,7 @@
});
const uix = createActiveUix({
langs: { schema: {}, defaultLocale: 'es' as const },
langs: { schema: { sium: siumLangs }, defaultLocale: 'es' as const },
prefs: { schema: docsPrefsSchema },
events: {
sound: true,

@ -527,17 +527,15 @@
</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.Input />
<PasswordField.CapsLockIndicator />
<PasswordField.VisibilityTrigger />
</PasswordField>
</Field.Control>
<PasswordField
bind:value={loginForm.values.password}
purpose="current"
name="password"
>
<PasswordField.Input />
<PasswordField.CapsLockIndicator />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{loginForm.errors.password?.[0]}</Field.ErrorText>
</Field>
<Form.Submit>Sign in</Form.Submit>
@ -567,18 +565,16 @@
</Field>
<Field name="password" label="Password" required>
<Field.Label>Password</Field.Label>
<Field.Control>
<PasswordField
bind:value={registrationForm.values.password}
purpose="new"
name="password"
>
<PasswordField.Input />
<PasswordField.CapsLockIndicator />
<PasswordField.VisibilityTrigger />
<PasswordField.StrengthMeter strength={registrationStrength} />
</PasswordField>
</Field.Control>
<PasswordField
bind:value={registrationForm.values.password}
purpose="new"
name="password"
>
<PasswordField.Input />
<PasswordField.CapsLockIndicator />
<PasswordField.VisibilityTrigger />
<PasswordField.StrengthMeter strength={registrationStrength} />
</PasswordField>
<Field.ErrorText>{registrationForm.errors.password?.[0]}</Field.ErrorText>
</Field>
<Form.Submit>Create account</Form.Submit>
@ -597,44 +593,38 @@
<div style="display: flex; flex-direction: column; gap: var(--uix-space-3);">
<Field name="current" label="Current password" required>
<Field.Label>Current password</Field.Label>
<Field.Control>
<PasswordField
bind:value={changeForm.values.current}
purpose="current"
name="current-password"
>
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
</Field.Control>
<PasswordField
bind:value={changeForm.values.current}
purpose="current"
name="current-password"
>
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{changeForm.errors.current?.[0]}</Field.ErrorText>
</Field>
<Field name="next" label="New password" required>
<Field.Label>New password</Field.Label>
<Field.Control>
<PasswordField
bind:value={changeForm.values.next}
purpose="new"
name="new-password"
>
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
</Field.Control>
<PasswordField
bind:value={changeForm.values.next}
purpose="new"
name="new-password"
>
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{changeForm.errors.next?.[0]}</Field.ErrorText>
</Field>
<Field name="confirm" label="Confirm new password" required>
<Field.Label>Confirm</Field.Label>
<Field.Control>
<PasswordField
bind:value={changeForm.values.confirm}
purpose="new"
name="confirm-password"
>
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
</Field.Control>
<PasswordField
bind:value={changeForm.values.confirm}
purpose="new"
name="confirm-password"
>
<PasswordField.Input />
<PasswordField.VisibilityTrigger />
</PasswordField>
<Field.ErrorText>{changeForm.errors.confirm?.[0]}</Field.ErrorText>
</Field>
<Form.Submit>Update password</Form.Submit>

Loading…
Cancel
Save

Powered by TurnKey Linux.