Files
WRNexusJS/packages/captcha/components/Captcha.wrn
T
Clintchiz 8d044565e3
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
feat: publish changed packages independently
2026-08-11 15:32:36 +05:30

352 lines
24 KiB
Plaintext

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
resetOnError = true
compact = false
layout = "vertical"
@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-wrnexus-runtime="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-reset-on-error='{resetOnError}'
data-captcha-compact='{compact}'
data-captcha-layout='{layout}'
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-layout=horizontal]:max-w-2xl data-[captcha-layout=horizontal]:grid data-[captcha-layout=horizontal]:grid-cols-1 sm:data-[captcha-layout=horizontal]:grid-cols-[minmax(0,1fr)_minmax(12rem,0.8fr)] sm:data-[captcha-layout=horizontal]:items-start 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}'
>
<header data-captcha-header class='flex items-start justify-between gap-3 group-data-[captcha-size=compact]/captcha:gap-1.5 group-data-[captcha-layout=horizontal]/captcha:col-span-full 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 min-w-0 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>
<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-response-panel class='flex min-w-0 flex-col gap-2 group-data-[captcha-layout=horizontal]/captcha:self-stretch'>
<label data-captcha-answer-wrap hidden class='flex min-w-0 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>
<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>
</div>
</section>
}
style {
/*
* Compact image challenges read as a single sequence: challenge,
* secondary actions, answer, then verification. Flattening the footer
* lets Listen/New challenge move directly beneath the image without
* changing the DOM or the client runtime selectors.
*/
[data-wrn-captcha]:is([data-captcha-compact="true"], [data-captcha-size="compact"])[data-captcha-layout="horizontal"] {
grid-template-columns: minmax(0, 1fr);
}
[data-wrn-captcha]:is([data-captcha-compact="true"], [data-captcha-size="compact"])
[data-captcha-response-panel]
[data-captcha-footer] {
display: contents;
}
[data-wrn-captcha]:is([data-captcha-compact="true"], [data-captcha-size="compact"])
[data-captcha-footer]
> div:first-child {
order: -1;
justify-content: flex-start;
}
[data-wrn-captcha]:is([data-captcha-compact="true"], [data-captcha-size="compact"])
[data-captcha-verify] {
align-self: flex-start;
}
}
}