release: WRNexusJS 0.4.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# WRNexus UI component reference
|
||||
|
||||
This reference is generated from the 86 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
||||
This reference is generated from the 85 packaged `.wrn` component sources. Props marked required have no default; all others show their runtime default.
|
||||
|
||||
## Advanced-forms
|
||||
|
||||
@@ -330,17 +330,6 @@ Theme-aware, responsive tree view component.
|
||||
- Slots: `default`
|
||||
- Events: None
|
||||
|
||||
## Core
|
||||
|
||||
### Captcha
|
||||
|
||||
Reusable captcha component.
|
||||
|
||||
- Mount: `data-component="Captcha"`
|
||||
- Props: `provider: string = "self-hosted"`, `siteKey: string = ""`, `type: string = "alphanumeric"`, `action: string = "form-submit"`, `presentation: string = "visual"`, `difficulty: string = "normal"`, `locale: string = "en"`, `size: string = "default"`, `color: string = "primary"`, `class: string = ""`, `id: string = ""`, `name: string = "wrn-captcha-response"`, `endpoint: string = "/__wrnexus/captcha/challenge"`, `verifyEndpoint: string = "/__wrnexus/captcha/verify"`, `responseField: string = "wrn-captcha-response"`, `label: string = "Security verification"`, `description: string = "Complete the challenge to continue."`, `helpText: string = ""`, `requiredMessage: string = "Please complete the security check."`, `incorrectMessage: string = "That answer was not correct. Try again."`, `expiredMessage: string = "This challenge expired. Load a new one."`, `networkMessage: string = "The verification service is unavailable. Try again."`, `autoLoad: boolean = true`, `autoVerify: boolean = false`, `showVerify: boolean = true`, `showRefresh: boolean = true`, `showAudio: boolean = true`, `showStatus: boolean = true`, `disabled: boolean = false`, `required: boolean = true`, `compact: boolean = false`
|
||||
- Slots: None
|
||||
- Events: `ready`, `challenge`, `input`, `verify`, `success`, `failure`, `expired`, `refresh`, `audioStart`, `audioEnd`, `error`
|
||||
|
||||
## Forms
|
||||
|
||||
### Checkbox
|
||||
|
||||
+14
-12
@@ -15,7 +15,7 @@ driven by `var(--wire-*)` theme tokens, so components restyle instantly when the
|
||||
theme changes. The tiny JS surface (`src/index.ts`) exists only so the toolchain
|
||||
(CLI build + dev server) can locate the component directory and stylesheet.
|
||||
|
||||
The complete PDF-aligned catalog currently contains **891 components**. The
|
||||
The complete PDF-aligned catalog currently contains **85 components**. The
|
||||
generated `COMPONENTS.md` and `component-reference.json` files document every
|
||||
mount name, prop, inferred type, default/required status, slot, event, category,
|
||||
and source file directly from the packaged `.wrn` source.
|
||||
@@ -35,10 +35,11 @@ In practice you rarely install this directly: `@wrnexus/cli` and
|
||||
|
||||
## Components
|
||||
|
||||
Components live as `.wrn` files under `packages/ui/components/`. The mount name
|
||||
is the **lowercase file basename** (e.g. `button.wrn` → `data-component="button"`).
|
||||
Each accepts a `class` prop (appended to its root element) and most render their
|
||||
body from either a named prop or the default slot.
|
||||
Components live as `.wrn` files under `packages/ui/components/`. The canonical mount
|
||||
name comes from the component declaration (for example, `component Button` mounts as
|
||||
`data-component="Button"`). Component lookup is case-insensitive, so existing lowercase
|
||||
mounts continue to work. Each component accepts a `class` prop (appended to its root
|
||||
element), and most render their body from either a named prop or the default slot.
|
||||
|
||||
### Layout
|
||||
|
||||
@@ -91,16 +92,17 @@ equivalent is exported as `@wrnexus/ui/component-reference.json`.
|
||||
|
||||
## API
|
||||
|
||||
The JS module (`@wrnexus/ui`) exposes four helpers used by the build tooling to
|
||||
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
|
||||
locate the component assets. There is no component code to import — the components
|
||||
are `.wrn` files rendered server-side.
|
||||
|
||||
| Export | Signature | Returns |
|
||||
| ------------------ | ---------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
|
||||
| `uiCssPath` | `() => string` | Absolute path to `ui.css`. |
|
||||
| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). |
|
||||
| `uiComponentNames` | `() => string[]` | Sorted list of built-in component names (e.g. for `wrnexus eject` listing). |
|
||||
| Export | Signature | Returns |
|
||||
| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `uiComponentsDir` | `() => string` | Absolute path to the `.wrn` component directory (feed to `buildRouter`'s `componentDirs`). |
|
||||
| `uiCssPath` | `() => string` | Absolute path to `ui.css`. |
|
||||
| `uiCss` | `() => string` | The `ui.css` file contents (all `.wire-*` classes, themed via tokens). |
|
||||
| `uiComponentNames` | `() => string[]` | Sorted list of declared built-in component names. |
|
||||
| `uiComponentPath` | `(name: string) => string` | Absolute source path for a declared component name or case-insensitive alias. |
|
||||
|
||||
### `./ui.css` asset export
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,320 +0,0 @@
|
||||
component Captcha {
|
||||
props {
|
||||
provider = "self-hosted"
|
||||
siteKey = ""
|
||||
type = "alphanumeric"
|
||||
action = "form-submit"
|
||||
presentation = "visual"
|
||||
difficulty = "normal"
|
||||
disturbance = 50
|
||||
imageStyle = "random"
|
||||
allowedStyles = ""
|
||||
excludedStyles = ""
|
||||
randomizeStyle = false
|
||||
locale = "en"
|
||||
size = "normal"
|
||||
color = "primary"
|
||||
class = ""
|
||||
|
||||
id = ""
|
||||
name = "wrn-captcha-response"
|
||||
endpoint = "/__wrnexus/captcha/challenge"
|
||||
verifyEndpoint = "/__wrnexus/captcha/verify"
|
||||
responseField = "wrn-captcha-response"
|
||||
|
||||
label = "Security verification"
|
||||
description = "Complete the challenge to continue."
|
||||
helpText = ""
|
||||
requiredMessage = "Please complete the security check."
|
||||
incorrectMessage = "That answer was not correct. Try again."
|
||||
expiredMessage = "This challenge expired. Load a new one."
|
||||
networkMessage = "The verification service is unavailable. Try again."
|
||||
|
||||
autoLoad = true
|
||||
autoVerify = false
|
||||
showVerify = true
|
||||
showRefresh = true
|
||||
showAudio = true
|
||||
showListen = true
|
||||
showStatus = true
|
||||
disabled = false
|
||||
required = true
|
||||
compact = false
|
||||
|
||||
@event ready = function
|
||||
@event challenge = function
|
||||
@event input = function
|
||||
@event verify = function
|
||||
@event success = function
|
||||
@event failure = function
|
||||
@event expired = function
|
||||
@event refresh = function
|
||||
@event audioStart = function
|
||||
@event audioEnd = function
|
||||
@event error = function
|
||||
}
|
||||
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
id='{id}'
|
||||
data-wrn-captcha
|
||||
data-captcha-provider='{provider}'
|
||||
data-captcha-site-key='{siteKey}'
|
||||
data-captcha-type='{type}'
|
||||
data-captcha-action='{action}'
|
||||
data-captcha-presentation='{presentation}'
|
||||
data-captcha-difficulty='{difficulty}'
|
||||
data-captcha-disturbance='{disturbance}'
|
||||
data-captcha-image-style='{imageStyle}'
|
||||
data-captcha-allowed-styles='{allowedStyles}'
|
||||
data-captcha-excluded-styles='{excludedStyles}'
|
||||
data-captcha-randomize-style='{randomizeStyle}'
|
||||
data-captcha-locale='{locale}'
|
||||
data-captcha-size='{size}'
|
||||
data-captcha-color='{color}'
|
||||
data-captcha-endpoint='{endpoint}'
|
||||
data-captcha-verify-endpoint='{verifyEndpoint}'
|
||||
data-captcha-response-field='{responseField}'
|
||||
data-captcha-name='{name}'
|
||||
data-captcha-auto-load='{autoLoad}'
|
||||
data-captcha-auto-verify='{autoVerify}'
|
||||
data-captcha-show-verify='{showVerify}'
|
||||
data-captcha-show-refresh='{showRefresh}'
|
||||
data-captcha-show-audio='{showAudio}'
|
||||
data-captcha-show-listen='{showListen}'
|
||||
data-captcha-show-status='{showStatus}'
|
||||
data-captcha-disabled='{disabled}'
|
||||
data-captcha-required='{required}'
|
||||
data-captcha-compact='{compact}'
|
||||
data-captcha-required-message='{requiredMessage}'
|
||||
data-captcha-incorrect-message='{incorrectMessage}'
|
||||
data-captcha-expired-message='{expiredMessage}'
|
||||
data-captcha-network-message='{networkMessage}'
|
||||
data-captcha-status="idle"
|
||||
data-server-verification-required="true"
|
||||
aria-busy="false"
|
||||
class='group/captcha relative flex w-full max-w-lg flex-col gap-3 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-4 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-1)] [--captcha-accent:var(--wire-color-primary)] data-[captcha-color=secondary]:[--captcha-accent:var(--wire-color-secondary)] data-[captcha-color=success]:[--captcha-accent:var(--wire-color-success)] data-[captcha-color=warning]:[--captcha-accent:var(--wire-color-warning)] data-[captcha-color=danger]:[--captcha-accent:var(--wire-color-danger)] data-[captcha-color=info]:[--captcha-accent:var(--wire-color-info)] data-[captcha-size=compact]:max-w-xs data-[captcha-size=compact]:gap-1.5 data-[captcha-size=compact]:p-2 data-[captcha-size=big]:max-w-2xl data-[captcha-size=big]:gap-4 data-[captcha-size=big]:p-5 data-[captcha-compact=true]:max-w-xs data-[captcha-compact=true]:gap-1.5 data-[captcha-compact=true]:p-2 data-[captcha-type=not-robot]:max-w-sm data-[captcha-type=not-robot]:gap-0 data-[captcha-type=not-robot]:p-0 data-[captcha-disabled=true]:pointer-events-none data-[captcha-disabled=true]:opacity-60 data-[captcha-status=verified]:border-[var(--wire-color-success)] data-[captcha-status=incorrect]:border-[var(--wire-color-danger)] data-[captcha-status=expired]:border-[var(--wire-color-danger)] data-[captcha-status=network-error]:border-[var(--wire-color-danger)] data-[captcha-status=provider-error]:border-[var(--wire-color-danger)] {class}'
|
||||
>
|
||||
<script
|
||||
src="/assets/wrnexus/captcha.js"
|
||||
defer
|
||||
data-wrn-captcha-runtime
|
||||
></script>
|
||||
|
||||
<header data-captcha-header class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<div class='min-w-0'>
|
||||
<div class='flex items-center gap-2 group-data-[captcha-size=compact]/captcha:gap-1.5'>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class='icon-[lucide--shield-check] size-5 shrink-0 text-[var(--captcha-accent)] group-data-[captcha-size=compact]/captcha:size-4'
|
||||
></span>
|
||||
<h3 class='m-0 text-sm font-semibold leading-5 text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:text-base'>
|
||||
{label}
|
||||
</h3>
|
||||
</div>
|
||||
<p class='m-0 mt-1 text-xs leading-5 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-size=big]/captcha:text-sm'>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span
|
||||
data-captcha-verified-badge
|
||||
hidden
|
||||
class='shrink-0 items-center gap-1 rounded-full bg-[color-mix(in_srgb,var(--wire-color-success)_12%,transparent)] px-2 py-1 text-xs font-semibold text-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:gap-0.5 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:py-0.5 group-data-[captcha-size=compact]/captcha:text-[10px]'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--circle-check] size-3.5 group-data-[captcha-size=compact]/captcha:size-3'></span>
|
||||
Verified
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div
|
||||
data-captcha-provider-mount
|
||||
hidden
|
||||
class='flex min-h-16 w-full items-center justify-center overflow-hidden rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] p-2 group-data-[captcha-size=compact]/captcha:min-h-12 group-data-[captcha-size=compact]/captcha:p-1.5'
|
||||
></div>
|
||||
|
||||
<div
|
||||
data-captcha-loading
|
||||
hidden
|
||||
class='flex min-h-28 items-center justify-center gap-2 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-surface-2)] text-sm text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:min-h-14 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:min-h-36 group-data-[captcha-type=not-robot]/captcha:m-3 group-data-[captcha-type=not-robot]/captcha:min-h-16'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--loader-circle] size-5 animate-spin group-data-[captcha-size=compact]/captcha:size-4'></span>
|
||||
Loading challenge…
|
||||
</div>
|
||||
|
||||
<div data-captcha-challenge hidden class='flex flex-col gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-size=big]/captcha:gap-4 group-data-[captcha-type=not-robot]/captcha:p-3'>
|
||||
<div data-captcha-prompt-row class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-2'>
|
||||
<p data-captcha-prompt class='m-0 text-sm font-medium text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4'></p>
|
||||
<span data-captcha-countdown hidden class='shrink-0 text-xs tabular-nums text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px]'></span>
|
||||
</div>
|
||||
|
||||
<div data-captcha-not-robot hidden class='w-full'>
|
||||
<button
|
||||
data-captcha-not-robot-button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
class='flex min-h-20 w-full items-center gap-3 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-4 py-3 text-left transition-[border-color,box-shadow,background-color] hover:border-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-wait disabled:opacity-80 group-data-[captcha-size=compact]/captcha:min-h-16 group-data-[captcha-size=compact]/captcha:gap-2.5 group-data-[captcha-size=compact]/captcha:px-3 group-data-[captcha-size=compact]/captcha:py-2.5 group-data-[captcha-size=big]/captcha:min-h-24 group-data-[captcha-size=big]/captcha:px-5 group-data-[captcha-size=big]/captcha:py-4'
|
||||
>
|
||||
<span
|
||||
data-captcha-not-robot-control
|
||||
aria-hidden="true"
|
||||
class='inline-flex size-7 shrink-0 items-center justify-center rounded-md border-2 border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] text-white transition-colors data-[verified=true]:border-[var(--wire-color-success)] data-[verified=true]:bg-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:size-6 group-data-[captcha-size=big]/captcha:size-8'
|
||||
>
|
||||
<span data-captcha-not-robot-empty class='size-full'></span>
|
||||
<span data-captcha-not-robot-spinner hidden class='icon-[lucide--loader-circle] size-4 animate-spin text-[var(--captcha-accent)]'></span>
|
||||
<span data-captcha-not-robot-check hidden class='icon-[lucide--check] size-5'></span>
|
||||
</span>
|
||||
|
||||
<span data-captcha-not-robot-label class='min-w-0 flex-1 text-sm font-semibold text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[11px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-size=big]/captcha:text-base'>
|
||||
I'm not a robot
|
||||
</span>
|
||||
|
||||
<span class='flex shrink-0 flex-col items-center gap-0.5 text-[10px] leading-none text-[var(--wire-color-muted)]'>
|
||||
<span aria-hidden="true" class='icon-[lucide--shield-check] size-6 text-[var(--captcha-accent)] group-data-[captcha-size=compact]/captcha:size-5 group-data-[captcha-size=big]/captcha:size-7'></span>
|
||||
<span>WRNexus</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-captcha-image-wrap
|
||||
hidden
|
||||
class='overflow-hidden rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-white'
|
||||
>
|
||||
<img
|
||||
data-captcha-image
|
||||
src=""
|
||||
alt="CAPTCHA challenge"
|
||||
draggable="false"
|
||||
class='block h-auto min-h-20 w-full select-none object-contain group-data-[captcha-size=compact]/captcha:max-h-24 group-data-[captcha-size=compact]/captcha:min-h-12 group-data-[captcha-size=big]/captcha:min-h-28'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-captcha-items
|
||||
hidden
|
||||
role="group"
|
||||
class='grid grid-cols-2 gap-2 group-data-[captcha-size=compact]/captcha:gap-1 sm:grid-cols-3 group-data-[captcha-size=big]/captcha:gap-3'
|
||||
></div>
|
||||
|
||||
<template data-captcha-item-template>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed="false"
|
||||
class='group relative overflow-hidden rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-white p-1 group-data-[captcha-size=compact]/captcha:p-0.5 transition-[border-color,box-shadow,transform] duration-[var(--wire-motion-fast)] hover:border-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] aria-[pressed=true]:border-[var(--captcha-accent)] aria-[pressed=true]:ring-2 aria-[pressed=true]:ring-[var(--captcha-accent)]'
|
||||
>
|
||||
<img
|
||||
data-captcha-item-image
|
||||
src=""
|
||||
alt=""
|
||||
draggable="false"
|
||||
class='aspect-[4/3] w-full select-none object-cover'
|
||||
/>
|
||||
<span
|
||||
data-captcha-item-check
|
||||
hidden
|
||||
aria-hidden="true"
|
||||
class='absolute right-1.5 top-1.5 inline-flex size-5 group-data-[captcha-size=compact]/captcha:right-1 group-data-[captcha-size=compact]/captcha:top-1 group-data-[captcha-size=compact]/captcha:size-4 items-center justify-center rounded-full bg-[var(--captcha-accent)] text-white shadow-sm'
|
||||
>
|
||||
<span class='icon-[lucide--check] size-3.5'></span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<label data-captcha-answer-wrap hidden class='flex flex-col gap-1.5 group-data-[captcha-size=compact]/captcha:gap-1'>
|
||||
<span class='text-xs font-semibold text-[var(--wire-color-text)] group-data-[captcha-size=compact]/captcha:text-[10px]'>Your answer</span>
|
||||
<input
|
||||
data-captcha-answer
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
autocapitalize="characters"
|
||||
spellcheck="false"
|
||||
class='h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] px-3 text-base font-semibold tracking-[0.18em] text-[var(--wire-color-text)] outline-none transition-[border-color,box-shadow] placeholder:tracking-normal focus:border-[var(--captcha-accent)] focus:ring-2 focus:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:bg-[var(--wire-color-surface-2)] aria-[invalid=true]:border-[var(--wire-color-danger)] aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-[var(--wire-color-danger)] group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:px-2 group-data-[captcha-size=compact]/captcha:text-xs group-data-[captcha-size=compact]/captcha:tracking-[0.12em] group-data-[captcha-size=big]/captcha:h-12 group-data-[captcha-size=big]/captcha:px-4 group-data-[captcha-size=big]/captcha:text-lg'
|
||||
placeholder="Enter the answer"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<input
|
||||
data-captcha-honeypot
|
||||
type="text"
|
||||
name=""
|
||||
value=""
|
||||
tabindex="-1"
|
||||
autocomplete="off"
|
||||
aria-hidden="true"
|
||||
class='pointer-events-none absolute -left-[10000px] top-auto h-px w-px overflow-hidden opacity-0'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div data-captcha-success hidden role="status" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wire-color-success)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<span aria-hidden="true" class='icon-[lucide--circle-check] mt-0.5 size-3.5 shrink-0'></span>
|
||||
<span data-captcha-success-message>Verification completed.</span>
|
||||
</div>
|
||||
|
||||
<div data-captcha-error hidden role="alert" aria-live="polite" class='flex items-start gap-1.5 text-xs leading-5 text-[var(--wire-color-danger)] group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:mx-3 group-data-[captcha-type=not-robot]/captcha:mb-3'>
|
||||
<span aria-hidden="true" class='icon-[lucide--circle-alert] mt-0.5 size-3.5 shrink-0'></span>
|
||||
<span data-captcha-error-message></span>
|
||||
</div>
|
||||
|
||||
<p data-captcha-help class='m-0 text-xs leading-5 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:text-[10px] group-data-[captcha-size=compact]/captcha:leading-4 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
{helpText}
|
||||
</p>
|
||||
|
||||
<footer data-captcha-footer class='flex flex-wrap items-center justify-between gap-2 group-data-[captcha-size=compact]/captcha:flex-nowrap group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
<div class='flex flex-wrap items-center gap-1.5 group-data-[captcha-size=compact]/captcha:flex-nowrap group-data-[captcha-size=compact]/captcha:gap-0.5'>
|
||||
<button
|
||||
data-captcha-audio
|
||||
hidden
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wire-color-muted)] transition-colors hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--volume-2] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
Listen
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-captcha-audio-alternative
|
||||
hidden
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wire-color-muted)] transition-colors hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--ear] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
Use audio
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-captcha-refresh
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] px-2.5 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-1.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-[var(--wire-color-muted)] transition-colors hover:bg-[var(--wire-color-surface-2)] hover:text-[var(--captcha-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span aria-hidden="true" class='icon-[lucide--refresh-cw] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
New challenge
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-captcha-verify
|
||||
type="button"
|
||||
class='inline-flex h-9 items-center justify-center gap-1.5 rounded-[var(--wire-radius-sm)] bg-[var(--captcha-accent)] px-3 text-xs group-data-[captcha-size=compact]/captcha:h-8 group-data-[captcha-size=compact]/captcha:gap-1 group-data-[captcha-size=compact]/captcha:px-2.5 group-data-[captcha-size=compact]/captcha:text-[10px] font-semibold text-white shadow-sm transition-[filter,transform] hover:brightness-95 active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--captcha-accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--wire-color-surface)] disabled:cursor-not-allowed disabled:opacity-50'
|
||||
>
|
||||
<span data-captcha-verify-icon aria-hidden="true" class='icon-[lucide--shield-check] size-4 group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
<span data-captcha-verify-spinner hidden aria-hidden="true" class='icon-[lucide--loader-circle] size-4 animate-spin group-data-[captcha-size=compact]/captcha:size-3.5'></span>
|
||||
<span data-captcha-verify-label>Verify</span>
|
||||
</button>
|
||||
</footer>
|
||||
|
||||
<input
|
||||
data-captcha-response
|
||||
type="hidden"
|
||||
name='{responseField}'
|
||||
value=""
|
||||
/>
|
||||
|
||||
<p data-captcha-credit class='m-0 text-[11px] leading-4 text-[var(--wire-color-muted)] group-data-[captcha-size=compact]/captcha:hidden group-data-[captcha-type=not-robot]/captcha:hidden'>
|
||||
Protected by WRNexus CAPTCHA. Server-side verification is required.
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.3.6",
|
||||
"version": "0.4.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -21,7 +21,7 @@ import { fileURLToPath } from "node:url";
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = join(here, "..");
|
||||
let cachedCss: string | undefined;
|
||||
let cachedNames: string[] | undefined;
|
||||
let cachedComponentFiles: Map<string, string> | undefined;
|
||||
|
||||
/** Absolute path to the directory of Wire UI component `.wrn` files. */
|
||||
export function uiComponentsDir(): string {
|
||||
@@ -38,11 +38,55 @@ export function uiCss(): string {
|
||||
return (cachedCss ??= readFileSync(uiCssPath(), "utf8"));
|
||||
}
|
||||
|
||||
/** Names of the built-in components (e.g. for `wrnexus eject` listing). */
|
||||
export function uiComponentNames(): string[] {
|
||||
cachedNames ??= readdirSync(uiComponentsDir())
|
||||
.filter((f) => f.endsWith(".wrn"))
|
||||
.map((f) => f.replace(/\.wrn$/, ""))
|
||||
.sort();
|
||||
return [...cachedNames];
|
||||
function uiComponentFiles(): Map<string, string> {
|
||||
if (cachedComponentFiles) return cachedComponentFiles;
|
||||
|
||||
const files = new Map<string, string>();
|
||||
|
||||
const componentFiles = readdirSync(uiComponentsDir()).filter((entry) => entry.endsWith(".wrn"));
|
||||
|
||||
for (const file of componentFiles) {
|
||||
const path = join(uiComponentsDir(), file);
|
||||
const source = readFileSync(path, "utf8");
|
||||
const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(source);
|
||||
|
||||
if (!declaration) {
|
||||
throw new Error(`UI component source does not declare a component: ${path}`);
|
||||
}
|
||||
|
||||
const name = declaration[1]!;
|
||||
const previous = files.get(name);
|
||||
|
||||
if (previous) {
|
||||
throw new Error(`Duplicate UI component declaration '${name}' in ${previous} and ${path}`);
|
||||
}
|
||||
|
||||
files.set(name, path);
|
||||
}
|
||||
|
||||
cachedComponentFiles = files;
|
||||
return files;
|
||||
}
|
||||
|
||||
/** Names declared by the bundled components, independent of filename casing. */
|
||||
export function uiComponentNames(): string[] {
|
||||
return [...uiComponentFiles().keys()].sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
/** Absolute path to a bundled component by its declared component name. */
|
||||
export function uiComponentPath(name: string): string {
|
||||
const files = uiComponentFiles();
|
||||
const exact = files.get(name);
|
||||
if (exact) return exact;
|
||||
|
||||
const normalized = name.toLowerCase();
|
||||
const matches = [...files.entries()].filter(
|
||||
([componentName]) => componentName.toLowerCase() === normalized,
|
||||
);
|
||||
|
||||
if (matches.length === 1) return matches[0]![1];
|
||||
throw new Error(`Unknown WRNexus UI component '${name}'.`);
|
||||
}
|
||||
|
||||
export { uiComponentReference, findUiComponent, auditUiComponents } from "./metadata.ts";
|
||||
export type { UiComponentMetadata, UiComponentReference } from "./metadata.ts";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { uiComponentsDir } from "./index.ts";
|
||||
export interface UiComponentMetadata {
|
||||
name: string;
|
||||
mount: string;
|
||||
category?: string;
|
||||
purpose?: string;
|
||||
props?: Array<{ name: string; default?: unknown }>;
|
||||
events?: string[];
|
||||
}
|
||||
export interface UiComponentReference {
|
||||
count: number;
|
||||
components: UiComponentMetadata[];
|
||||
}
|
||||
let referenceCache: UiComponentReference | undefined;
|
||||
export function uiComponentReference(): UiComponentReference {
|
||||
if (!referenceCache)
|
||||
referenceCache = JSON.parse(
|
||||
readFileSync(join(uiComponentsDir(), "..", "component-reference.json"), "utf8"),
|
||||
) as UiComponentReference;
|
||||
return structuredClone(referenceCache);
|
||||
}
|
||||
export function findUiComponent(name: string): UiComponentMetadata | undefined {
|
||||
return uiComponentReference().components.find(
|
||||
(component) => component.name === name || component.mount === name,
|
||||
);
|
||||
}
|
||||
export function auditUiComponents(): Array<{ component: string; issue: string }> {
|
||||
const issues: Array<{ component: string; issue: string }> = [];
|
||||
for (const component of uiComponentReference().components) {
|
||||
const props = new Set((component.props ?? []).map((prop) => prop.name));
|
||||
if (!props.has("class"))
|
||||
issues.push({ component: component.name, issue: "missing class prop" });
|
||||
if (!props.has("size")) issues.push({ component: component.name, issue: "missing size prop" });
|
||||
if (!props.has("color"))
|
||||
issues.push({ component: component.name, issue: "missing color prop" });
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
+26
-27
@@ -3,14 +3,14 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { compileWireFile } from "../../compiler/src/index.ts";
|
||||
import { mountHtml, renderComponent } from "../../test/src/index.ts";
|
||||
import { uiComponentNames, uiComponentsDir, uiCss } from "../src/index.ts";
|
||||
import { uiComponentNames, uiComponentsDir, uiComponentPath, uiCss } from "../src/index.ts";
|
||||
|
||||
test("bundled UI assets are discoverable and readable", () => {
|
||||
expect(uiComponentNames()).toContain("Button");
|
||||
expect(uiComponentNames()).toContain("Accordion");
|
||||
expect(uiComponentNames()).toContain("DataTable");
|
||||
expect(uiComponentNames()).toContain("WysiwygEditor");
|
||||
expect(readFileSync(join(uiComponentsDir(), "Button.wrn"), "utf8")).toContain("component Button");
|
||||
expect(readFileSync(uiComponentPath("Button"), "utf8")).toContain("component Button");
|
||||
expect(uiCss()).toContain("--wire-");
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ test("bundled components do not duplicate another component implementation", ()
|
||||
const implementations = new Map<string, string>();
|
||||
|
||||
for (const name of uiComponentNames()) {
|
||||
const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "utf8")
|
||||
const source = readFileSync(uiComponentPath(name), "utf8")
|
||||
.replace(/^component\s+[A-Za-z0-9_]+/, "component __NAME__")
|
||||
.replace(/\r\n/g, "\n")
|
||||
.trim();
|
||||
@@ -142,7 +142,7 @@ test("component-system CSS includes responsive, theme-token, focus, and reduced-
|
||||
|
||||
test("component boundaries have no default margins and expose the canonical class prop", () => {
|
||||
for (const name of uiComponentNames()) {
|
||||
const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "utf8");
|
||||
const source = readFileSync(uiComponentPath(name), "utf8");
|
||||
const root = source.match(/view\s*\{\s*<[A-Za-z][^>]*>/)?.[0] ?? "";
|
||||
const classes = root.match(/\bclass="([^"]*)"/)?.[1]?.split(/\s+/) ?? [];
|
||||
expect(classes.filter((token) => /^-?m[trblxy]?-/.test(token))).toEqual([]);
|
||||
@@ -154,7 +154,7 @@ test("component boundaries have no default margins and expose the canonical clas
|
||||
|
||||
test("every component exposes universal color and size configuration", () => {
|
||||
for (const name of uiComponentNames()) {
|
||||
const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "utf8");
|
||||
const source = readFileSync(uiComponentPath(name), "utf8");
|
||||
expect(source).toMatch(/^\s+color\s*=/m);
|
||||
expect(source).toMatch(/^\s+size\s*=/m);
|
||||
}
|
||||
@@ -162,7 +162,7 @@ test("every component exposes universal color and size configuration", () => {
|
||||
|
||||
// test("component markup keeps user-facing content and data behind props", () => {
|
||||
// for (const name of uiComponentNames()) {
|
||||
// const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "utf8");
|
||||
// const source = readFileSync(uiComponentPath(name), "utf8");
|
||||
// const view = source.slice(source.indexOf("view {"));
|
||||
// const literalText = [...view.matchAll(/>([^<>{}]*[A-Za-z][^<>{}]*)</g)]
|
||||
// .map((match) => match[1].trim())
|
||||
@@ -182,7 +182,7 @@ test("every component exposes universal color and size configuration", () => {
|
||||
|
||||
test("layout boundaries do not add default padding", () => {
|
||||
for (const name of ["Container", "Columns", "Grid", "LayoutSplitter", "Typography"]) {
|
||||
const source = readFileSync(join(uiComponentsDir(), `${name}.wrn`), "utf8");
|
||||
const source = readFileSync(uiComponentPath(name), "utf8");
|
||||
const root = source.match(/view\s*\{\s*<[A-Za-z][^>]*>/)?.[0] ?? "";
|
||||
const classes = root.match(/\bclass="([^"]*)"/)?.[1]?.split(/\s+/) ?? [];
|
||||
expect(classes.filter((token) => /^p[trblxy]?-/.test(token))).toEqual([]);
|
||||
@@ -191,13 +191,13 @@ test("layout boundaries do not add default padding", () => {
|
||||
|
||||
test("every bundled UI component compiles", () => {
|
||||
for (const name of uiComponentNames()) {
|
||||
const path = join(uiComponentsDir(), `${name}.wrn`);
|
||||
const path = uiComponentPath(name);
|
||||
expect(() => compileWireFile(readFileSync(path, "utf8"), path)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
test("icon buttons expose their accessible label as a hover and focus tooltip", () => {
|
||||
const button = readFileSync(join(uiComponentsDir(), "Button.wrn"), "utf8");
|
||||
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
||||
expect(button).toContain('class="wire-btn__tooltip"');
|
||||
expect(button).toContain("{ariaLabel || label}");
|
||||
expect(uiCss()).toContain(".wire-btn:hover > .wire-btn__tooltip");
|
||||
@@ -205,7 +205,7 @@ test("icon buttons expose their accessible label as a hover and focus tooltip",
|
||||
});
|
||||
|
||||
test("button links navigate by default and only download when the native attribute is passed", () => {
|
||||
const button = readFileSync(join(uiComponentsDir(), "Button.wrn"), "utf8");
|
||||
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
||||
expect(button).not.toMatch(/^\s+download\s*=/m);
|
||||
expect(button).not.toContain('download="{download}"');
|
||||
expect(button).toContain("{...attrs}");
|
||||
@@ -213,14 +213,14 @@ test("button links navigate by default and only download when the native attribu
|
||||
|
||||
test("every bundled UI component forwards undeclared native attributes", () => {
|
||||
for (const name of uiComponentNames()) {
|
||||
const path = join(uiComponentsDir(), `${name}.wrn`);
|
||||
const path = uiComponentPath(name);
|
||||
const output = compileWireFile(readFileSync(path, "utf8"), path);
|
||||
expect(output).toContain("${__wireSpreadAttrs(__attrs)}");
|
||||
}
|
||||
});
|
||||
|
||||
test("advanced select opens, filters, and selects without reactive handler errors", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "AdvancedSelect.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "team",
|
||||
options: [
|
||||
@@ -256,7 +256,7 @@ test("advanced select opens, filters, and selects without reactive handler error
|
||||
});
|
||||
|
||||
test("advanced select clears, restores its placeholder, and keeps multiple counters accurate", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "AdvancedSelect.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "team",
|
||||
multiple: true,
|
||||
@@ -300,7 +300,7 @@ test("advanced select clears, restores its placeholder, and keeps multiple count
|
||||
});
|
||||
|
||||
test("advanced select closes when a pointer event occurs outside it", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "AdvancedSelect.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "team",
|
||||
options: [{ value: "design", label: "Design" }],
|
||||
@@ -315,7 +315,7 @@ test("advanced select closes when a pointer event occurs outside it", async () =
|
||||
});
|
||||
|
||||
test("advanced select fetches remote options and appends infinite pages", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "AdvancedSelect.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: string[] = [];
|
||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||
@@ -394,7 +394,7 @@ test("advanced select fetches remote options and appends infinite pages", async
|
||||
});
|
||||
|
||||
test("combobox filters editable input, selects suggestions, exposes methods, and clears", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "ComboBox.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("ComboBox"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "team",
|
||||
placeholder: "Search teams",
|
||||
@@ -460,7 +460,7 @@ test("combobox filters editable input, selects suggestions, exposes methods, and
|
||||
});
|
||||
|
||||
test("combobox auto-loads remote suggestions and retains its selected value while searching", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "ComboBox.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("ComboBox"), "utf8");
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests: string[] = [];
|
||||
globalThis.fetch = (async (input: string | URL | Request) => {
|
||||
@@ -514,7 +514,7 @@ test("combobox auto-loads remote suggestions and retains its selected value whil
|
||||
});
|
||||
|
||||
test("combobox emits search, select, change, clear, open, and close events", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "ComboBox.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("ComboBox"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "events-team",
|
||||
options: [
|
||||
@@ -560,7 +560,7 @@ test("combobox emits search, select, change, clear, open, and close events", asy
|
||||
});
|
||||
|
||||
test("advanced select emits selection events and remote load and error events", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "AdvancedSelect.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("AdvancedSelect"), "utf8");
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalConsoleError = console.error;
|
||||
let rejectRequest = false;
|
||||
@@ -629,7 +629,7 @@ test("advanced select emits selection events and remote load and error events",
|
||||
});
|
||||
|
||||
test("pin input accepts matching characters, advances focus, navigates backward, and completes", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "PinInput.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("PinInput"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "verificationCode",
|
||||
length: 4,
|
||||
@@ -693,7 +693,7 @@ test("pin input accepts matching characters, advances focus, navigates backward,
|
||||
});
|
||||
|
||||
test("pin input renders four separate cells when length is omitted", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "PinInput.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("PinInput"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "defaultPin",
|
||||
});
|
||||
@@ -703,7 +703,7 @@ test("pin input renders four separate cells when length is omitted", async () =>
|
||||
});
|
||||
|
||||
test("pin input distributes filtered clipboard content and emits paste details", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "PinInput.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("PinInput"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "emailCode",
|
||||
length: 6,
|
||||
@@ -737,7 +737,7 @@ test("pin input distributes filtered clipboard content and emits paste details",
|
||||
});
|
||||
|
||||
test("pin input accepts lowercase alphanumeric entry and normalizes it to the configured pattern", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "PinInput.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("PinInput"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "securityCode",
|
||||
length: 4,
|
||||
@@ -757,7 +757,7 @@ test("pin input accepts lowercase alphanumeric entry and normalizes it to the co
|
||||
});
|
||||
|
||||
test("strong password scores input, supports custom special characters, and opens its popover", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "StrongPassword.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("StrongPassword"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "newPassword",
|
||||
presentation: "popover",
|
||||
@@ -766,7 +766,6 @@ test("strong password scores input, supports custom special characters, and open
|
||||
const dom = mountHtml(html);
|
||||
const root = dom.querySelector("[data-wrn-strong-password]") as HTMLElement;
|
||||
const input = dom.querySelector('input[name="newPassword"]') as HTMLInputElement;
|
||||
const popover = dom.querySelector(".wire-next__strong-password-popover") as HTMLElement;
|
||||
const strengthEvents: Array<{ level: string; percent: number }> = [];
|
||||
root.addEventListener("strength", (event) => {
|
||||
strengthEvents.push(
|
||||
@@ -807,7 +806,7 @@ test("strong password scores input, supports custom special characters, and open
|
||||
});
|
||||
|
||||
test("toggle password reveals and conceals its value with accessible declared events", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "TogglePassword.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("TogglePassword"), "utf8");
|
||||
const html = await renderComponent(source, {
|
||||
name: "accountPassword",
|
||||
value: "correct-horse",
|
||||
@@ -837,7 +836,7 @@ test("toggle password reveals and conceals its value with accessible declared ev
|
||||
});
|
||||
|
||||
test("toggle password supports checkbox control and synchronized password fields", async () => {
|
||||
const source = readFileSync(join(uiComponentsDir(), "TogglePassword.wrn"), "utf8");
|
||||
const source = readFileSync(uiComponentPath("TogglePassword"), "utf8");
|
||||
const synchronizedHtml = await renderComponent(source, {
|
||||
name: "credentials",
|
||||
fields: [
|
||||
|
||||
Reference in New Issue
Block a user