Skip to content

Aooth-flavoured field components ​

@atscript/vue-aooth ships a small set of pre-built custom field components used across Aooth flows — multi-consent collection, password-policy display, QR-code enrolment, one-shot link/token sharing, and SSO / social-login provider picking. All five implement the TAsComponentProps contract from @atscript/vue-form and drop into <AsForm :components> via the @ui.form.component annotation.

None of them are auto-resolved — register each one by name in the components map. The component name in the .as annotation is just a key into that map; match whichever convention you prefer ('AsConsentArray', 'consent-array', 'consents' — pick one, stay consistent).

ts
import {
  AsConsentArray,
  AsCopy,
  AsPasswordRules,
  AsQrCode,
  AsSsoProviders,
} from "@atscript/vue-aooth";

const components = {
  "consent-array": AsConsentArray,
  copy: AsCopy,
  "password-rules": AsPasswordRules,
  "qr-code": AsQrCode,
  "sso-providers": AsSsoProviders,
};

Styles ship two ways. Pre-built CSS lives at @atscript/ui-styles/dist/css/aooth.css — import it once at app start to get the entire aooth bundle. Apps already running vunor pick the classes up through the runtime UnoCSS preset without an extra import.

AsConsentArray ​

Multi-consent checkbox group with per-item required validation and inline markdown links. The bound value is string[]; checked ids are committed in insertion order.

ts
interface AsConsentArrayItem {
  id: string;
  text: string; // supports [label](url) for http(s)/mailto
  required?: string; // non-empty string ⇒ mandatory; doubles as the error message
}

interface AsConsentArrayProps extends TAsComponentProps<string[]> {
  pendingConsents?: AsConsentArrayItem[];
}
  • Empty / missing pendingConsents hides the entire shell — useful for "show the legal block only when the backend has pending consents".
  • Required misses surface their required string under the row; the shell footer is suppressed to avoid duplicate messaging.
  • Only http://, https://, and mailto: link schemes render as anchors — anything else falls back to verbatim text.
atscript
@ui.form.component 'consent-array'
@ui.form.fn.attr 'pendingConsents', '(v, data, ctx) => ctx.pendingConsents'
consents: string[]

The full server-driven flow lives in the API reference at @atscript/vue-aooth.

AsPasswordRules ​

Display-only readout of password-policy fulfilment. Pair it with a sibling password field; the component re-evaluates on every keystroke and toggles each row's met / unmet state via data-passed="true" | "false".

ts
interface AsPasswordRulesPolicy {
  rule: string; // function-string, evaluated through compileFieldFn
  description?: string;
  errorMessage?: string;
}

interface AsPasswordRulesProps extends TAsComponentProps {
  policies?: AsPasswordRulesPolicy[];
  password?: string;
}
  • Rule strings reuse @atscript/ui-fns' compileFieldFn — the same FNPool cache the framework uses for @ui.form.fn.*. Two policies with the same rule body share one compiled instance. A rule is called as (password, data, context, entry, t); its t is the app's (provideAsLocale) since 0.1.165.
  • Empty password ⇒ every row reads as unpassed, even if a no-op rule would technically return true.
  • Throwing rules are treated as unpassed and logged once per distinct source.
atscript
@ui.form.type 'paragraph'
@ui.form.component 'password-rules'
@ui.form.fn.attr 'policies', '(v, data, ctx) => ctx.passwordPolicies'
@ui.form.fn.attr 'password', '(v, data) => data.newPassword'
passwordHints: string

See @atscript/vue-aooth for the full reference and a worked example.

AsQrCode ​

Phantom field that renders an SVG QR code from any string. Primarily used for TOTP enrolment — for otpauth:// URIs it also surfaces the ?secret= query param as a manual-entry fallback so users on devices that can't scan can type the secret in by hand.

ts
interface AsQrCodeProps extends TAsComponentProps<string | undefined> {
  size?: number; // SVG width in px, default 192
  errorCorrection?: "L" | "M" | "Q" | "H"; // default "M"
  manualSecret?: boolean; // default true — hide the secret fallback for non-otpauth use cases
}
  • The value is read as props.value ?? props.model?.value. Phantom registrations (ui.paragraph + @ui.form.fn.value) push through props.value; data-bound fields use model.value.
  • qrcode is an optional peer dependency — install it in the consumer app when you ship a flow that uses AsQrCode. The module is dynamic-imported, so apps without the dep don't pay the bundle cost.
bash
npm install qrcode
atscript
@meta.label 'Scan with your authenticator app'
@ui.form.fn.value '(v, data, ctx) => ctx.totpUri'
@ui.form.component 'qr-code'
totpUri: ui.paragraph

When the value is an otpauth:// URI, the parsed secret is rendered beneath the SVG as plain text — set :manualSecret="false" when surfacing the secret defeats the threat model (e.g. screen-sharing demos).

AsCopy ​

Phantom field that surfaces a one-shot string with a read-only input and a Copy button. Click writes through navigator.clipboard.writeText and swaps the button label to "Copied" for ~1.5 s; focusing the input selects the full value for fallback Ctrl+C / Cmd+C copy.

ts
interface AsCopyProps extends TAsComponentProps<string | undefined> {
  copyLabel?: string; // default: the UI text UI_AOOTH_COPY ("Copy")
  copiedLabel?: string; // default: the UI text UI_AOOTH_COPIED ("Copied")
}
  • Value is read as props.value ?? props.model?.value, same phantom + data-bound dual-mode as AsQrCode.
  • Failed clipboard writes (no permission, no API) flip the button back and surface "Copy failed — select and copy manually" through the field's error slot.
atscript
@meta.label 'Magic link'
@ui.form.fn.value '(v, data, ctx) => ctx.magicLink'
@ui.form.component 'copy'
magicLink: ui.paragraph

Use it for magic links, share tokens, generated identifiers — any one-off value the user needs to paste somewhere else.

AsSsoProviders ​

SSO / social-login provider picker. Renders providers as a main stack of full-width buttons, with any flagged secondary: true dropping below an "or" divider as compact chips. Each button is a one-click action: clicking selects the provider id and fires the form action in a single click — there is no separate submit button. The bound value is the selected provider id (string | undefined).

ts
interface AsSsoProvider {
  id: string; // committed to the model on click AND carried by the fired action
  text: string; // rendered verbatim — the backend owns the full display string
  icon?: string; // CSS class for the brand glyph, applied as-is (same contract as prefixIcon)
  secondary?: boolean; // true ⇒ compact chip below the "or" divider; omitted ⇒ full-width button (default)
}

interface AsSsoProvidersProps extends TAsComponentProps<string | undefined> {
  providers?: AsSsoProvider[];
}
  • One-click: a click sets model.value = provider.id then emits the form action declared by @ui.form.action on the field. <AsForm> surfaces it as @action(name, data) with the selected provider in data. Without a wired @ui.form.action the click still selects but emits nothing.
  • Default providers land in the prominent main stack; secondary: true drops to compact chips below the "or" divider. The divider renders only when both groups are non-empty.
  • text is rendered verbatim — the component never composes a "Continue with {name}" prefix; the backend supplies the full string.
  • icon is applied as-is — you manage the icon collection and UnoCSS safelist (icon classes referenced from .as files aren't seen by the static extractor; safelist them).
  • Empty / missing providers hides the whole field — same "render only when the backend supplied providers" pattern as AsConsentArray.
  • No separate submit / "Continue" button — the provider buttons are the action (the component renders chromeless and suppresses the shell's footer action link).
atscript
@ui.form.component 'sso-providers'
@ui.form.action 'sso', 'Continue'
@ui.form.fn.attr 'providers', '(_v, _d, ctx) => ctx.ssoProviders'
ssoProvider?: string

In a workflow form add @wf.action.withData 'sso' on the same field so the chosen provider rides the submission (the same mechanism forgotPassword uses to carry a typed value). The full reference, plus a Vue mount showing the @action handler, lives at @atscript/vue-aooth.

Phantom vs data-bound ​

AsQrCode and AsCopy are most useful as phantom fields backed by context. The pattern combines three pieces:

  1. The field type is ui.paragraph so the form treats it as non-data chrome — nothing is read or written on submit.
  2. @ui.form.fn.value '(v, data, ctx) => ctx.someKey' supplies the displayed value from the form context.
  3. @wf.context.pass 'someKey' (on workflow forms) opts the key into the client form context — without it, ctx.someKey is undefined.

Worked example for a workflow form rendering both at once:

atscript
@wf.context.pass 'totpUri'
@wf.context.pass 'magicLink'
@meta.label 'Activate TOTP & share invite link'
@ui.form.submit.text 'Continue'
export interface QrCopyDemoForm {
    @meta.label 'Scan with your authenticator app'
    @ui.form.fn.value '(v, data, ctx) => ctx.totpUri'
    @ui.form.component 'qr-code'
    totpUri: ui.paragraph

    @meta.label 'Magic link'
    @ui.form.fn.value '(v, data, ctx) => ctx.magicLink'
    @ui.form.component 'copy'
    magicLink: ui.paragraph
}

Field-level fn signature

Field-level @ui.form.fn.* callbacks are (v, data, ctx, entry) — that's the full signature, though most usages only consume the first two or three arguments (entry, the resolved metadata entry, is rarely needed). The two-argument shape (_, ctx) => ... is wrong — the second argument is data, not the context, and ctx would silently resolve to the current form-data object. Only form-level keys (@ui.form.fn.title, @ui.form.fn.description, @ui.form.fn.submit.text, @ui.form.fn.submit.disabled) use (data, ctx). See Dynamic Fields.

When the value should land in the bound data slot (typed columns, downstream serialization), use a data field with @meta.readonly + @ui.form.fn.value instead. AsField resolves the function and writes the result into the bound path through a watcher. Phantom is the cleaner shape whenever the value is display-only.

Released under the MIT License.