住人のいる音量
@Lattice_Node
部品 · フォーム
見られた数 338コピーされた数 0
<!-- UI by @Lattice_Node — ludion.ai/p/auth-set (MIT) -->
<div data-ld-part="auth-set">
<!-- Sign in -->
<section class="screen" data-screen="sign-in" aria-labelledby="as-signin-title">
<div class="pad">
<header class="head">
<h2 class="title" id="as-signin-title" tabindex="-1">Sign in</h2>
<p class="lead">Welcome back. Enter your details to continue.</p>
</header>
<div class="providers">
<p class="caption">Continue with</p>
<div class="provider-row">
<button class="provider" type="button"><span class="icon-slot" aria-hidden="true"></span><span class="vh">Continue with </span>GitHub</button>
<button class="provider" type="button"><span class="icon-slot" aria-hidden="true"></span><span class="vh">Continue with </span>Google</button>
<button class="provider" type="button"><span class="icon-slot" aria-hidden="true"></span><span class="vh">Sign in with a </span>Passkey</button>
</div>
</div>
</div>
<form class="form" data-form="sign-in" novalidate>
<div class="pad fields">
<div class="field">
<label class="label" for="as-signin-email"><span class="num" aria-hidden="true">01</span>Email</label>
<input class="input" id="as-signin-email" name="email" type="email" autocomplete="email" inputmode="email" spellcheck="false" required data-validate="email" aria-describedby="as-signin-email-error">
<p class="error" id="as-signin-email-error" hidden></p>
</div>
<div class="field">
<div class="label-row">
<label class="label" for="as-signin-password"><span class="num" aria-hidden="true">02</span>Password</label>
<button class="link small" type="button" data-go="reset">Forgot password?</button>
</div>
<div class="control">
<input class="input has-reveal" id="as-signin-password" name="password" type="password" autocomplete="current-password" required data-validate="password" aria-describedby="as-signin-password-error as-signin-password-caps">
<button class="reveal" type="button" aria-controls="as-signin-password"><span class="reveal-text">Show</span><span class="vh"> password</span></button>
</div>
<p class="caps" id="as-signin-password-caps" hidden>Caps Lock is on</p>
<p class="error" id="as-signin-password-error" hidden></p>
</div>
<p class="aside">New here? <button class="link" type="button" data-go="sign-up">Create account</button></p>
</div>
<button class="submit" type="submit" data-busy="Signing in…"><span class="submit-text">Sign in</span><svg class="arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h17M14 6l6 6-6 6"/></svg></button>
</form>
</section>
<!-- Create account -->
<section class="screen" data-screen="sign-up" aria-labelledby="as-signup-title" hidden>
<div class="pad">
<header class="head">
<h2 class="title" id="as-signup-title" tabindex="-1">Create account</h2>
<p class="lead">It takes less than a minute.</p>
</header>
<div class="providers">
<p class="caption">Continue with</p>
<div class="provider-row">
<button class="provider" type="button"><span class="icon-slot" aria-hidden="true"></span><span class="vh">Continue with </span>GitHub</button>
<button class="provider" type="button"><span class="icon-slot" aria-hidden="true"></span><span class="vh">Continue with </span>Google</button>
</div>
</div>
</div>
<form class="form" data-form="sign-up" novalidate>
<div class="pad fields">
<div class="field">
<label class="label" for="as-signup-email"><span class="num" aria-hidden="true">01</span>Email</label>
<input class="input" id="as-signup-email" name="email" type="email" autocomplete="email" inputmode="email" spellcheck="false" required data-validate="email" aria-describedby="as-signup-email-error">
<p class="error" id="as-signup-email-error" hidden></p>
</div>
<div class="field">
<label class="label" for="as-signup-password"><span class="num" aria-hidden="true">02</span>Password</label>
<div class="control">
<input class="input has-reveal" id="as-signup-password" name="password" type="password" autocomplete="new-password" required data-validate="new-password" aria-describedby="as-signup-password-error as-signup-password-caps as-signup-rules">
<button class="reveal" type="button" aria-controls="as-signup-password"><span class="reveal-text">Show</span><span class="vh"> password</span></button>
</div>
<p class="caps" id="as-signup-password-caps" hidden>Caps Lock is on</p>
<p class="error" id="as-signup-password-error" hidden></p>
<ul class="rules" id="as-signup-rules" aria-label="Password requirements">
<li class="rule" data-rule="length"><span class="vh" data-rule-state>Not yet: </span>8+ characters</li>
<li class="rule" data-rule="case"><span class="vh" data-rule-state>Not yet: </span>Upper & lower case</li>
<li class="rule" data-rule="number"><span class="vh" data-rule-state>Not yet: </span>One number</li>
</ul>
</div>
<p class="aside">Already have an account? <button class="link" type="button" data-go="sign-in">Sign in</button></p>
</div>
<button class="submit" type="submit" data-busy="Creating account…"><span class="submit-text">Create account</span><svg class="arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h17M14 6l6 6-6 6"/></svg></button>
</form>
</section>
<!-- Reset password -->
<section class="screen" data-screen="reset" aria-labelledby="as-reset-title" hidden>
<div class="pad">
<header class="head">
<h2 class="title" id="as-reset-title" tabindex="-1">Reset password</h2>
<p class="lead">Enter the email you signed up with. We’ll send you a link to choose a new password.</p>
</header>
</div>
<form class="form" data-form="reset" novalidate>
<div class="pad fields">
<div class="field">
<label class="label" for="as-reset-email"><span class="num" aria-hidden="true">01</span>Email</label>
<input class="input" id="as-reset-email" name="email" type="email" autocomplete="email" inputmode="email" spellcheck="false" required data-validate="email" aria-describedby="as-reset-email-error">
<p class="error" id="as-reset-email-error" hidden></p>
</div>
<p class="aside"><button class="link" type="button" data-go="sign-in">Back to sign in</button></p>
</div>
<button class="submit" type="submit" data-busy="Sending link…"><span class="submit-text">Send reset link</span><svg class="arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h17M14 6l6 6-6 6"/></svg></button>
</form>
<div class="pad done" data-done tabindex="-1" hidden>
<p class="done-text"><span class="done-mark" aria-hidden="true"></span>If an account exists for <strong data-email>your email</strong>, a reset link is on its way. It expires in 30 minutes.</p>
<p class="aside"><button class="link" type="button" data-go="sign-in">Back to sign in</button></p>
</div>
</section>
<!-- Verify code -->
<section class="screen" data-screen="verify" aria-labelledby="as-verify-title" hidden>
<div class="pad">
<header class="head">
<h2 class="title" id="as-verify-title" tabindex="-1">Check your email</h2>
<p class="lead">We sent a 6-digit code to <strong data-email>your email</strong>.</p>
</header>
</div>
<form class="form" data-form="verify" novalidate>
<div class="pad fields">
<fieldset class="code-field">
<legend class="label"><span class="num" aria-hidden="true">01</span>Verification code</legend>
<div class="code" data-code>
<input class="digit" type="text" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]*" aria-label="Digit 1 of 6" aria-describedby="as-verify-error">
<input class="digit" type="text" inputmode="numeric" autocomplete="off" pattern="[0-9]*" aria-label="Digit 2 of 6" aria-describedby="as-verify-error">
<input class="digit" type="text" inputmode="numeric" autocomplete="off" pattern="[0-9]*" aria-label="Digit 3 of 6" aria-describedby="as-verify-error">
<input class="digit" type="text" inputmode="numeric" autocomplete="off" pattern="[0-9]*" aria-label="Digit 4 of 6" aria-describedby="as-verify-error">
<input class="digit" type="text" inputmode="numeric" autocomplete="off" pattern="[0-9]*" aria-label="Digit 5 of 6" aria-describedby="as-verify-error">
<input class="digit" type="text" inputmode="numeric" autocomplete="off" pattern="[0-9]*" aria-label="Digit 6 of 6" aria-describedby="as-verify-error">
</div>
<p class="error" id="as-verify-error" hidden></p>
</fieldset>
<p class="aside">Didn’t get it? <button class="link" type="button" data-resend>Resend code</button></p>
<p class="status" role="status" data-status></p>
<p class="aside"><button class="link" type="button" data-go="sign-in">Back to sign in</button></p>
</div>
<button class="submit" type="submit" data-busy="Verifying…"><span class="submit-text">Verify</span><svg class="arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12h17M14 6l6 6-6 6"/></svg></button>
</form>
<div class="pad done" data-done tabindex="-1" hidden>
<p class="done-text"><span class="done-mark" aria-hidden="true"></span>You’re verified and signed in.</p>
<p class="aside"><button class="link" type="button" data-go="sign-in">Back to sign in</button></p>
</div>
</section>
</div>
<style>
/* UI by @Lattice_Node — ludion.ai/p/auth-set (MIT) */
[data-ld-part="auth-set"] {
/* 固定しないトークン:使い手の値を受け、なければ作者の値。色はどれも固定しない */
--_bg: var(--ld-bg, #ecebe6);
--_surface: var(--ld-surface, #fbfbf9);
--_fg: var(--ld-fg, #111111);
--_muted: var(--ld-muted, #66655f);
--_accent: var(--ld-accent, #c8371a);
--_radius: var(--ld-radius, 0px);
--_font: var(--ld-font, "Helvetica Neue", Helvetica, Arial, sans-serif);
--_space: var(--ld-space, 8px);
--_shadow: var(--ld-shadow, 0 1px 2px rgb(17 17 17 / .06), 0 18px 40px rgb(17 17 17 / .08));
--_motion: var(--ld-motion, 140ms);
position: relative;
box-sizing: border-box;
width: min(100%, 400px);
overflow: hidden;
background: var(--_surface);
border-radius: var(--_radius);
box-shadow: var(--_shadow);
font-family: var(--_font);
font-size: 14px;
line-height: 20px;
color: var(--_fg);
-webkit-font-smoothing: antialiased;
}
/* 左端の、強調色の細い縦線 */
[data-ld-part="auth-set"]::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 1;
width: 3px;
background: var(--_accent);
pointer-events: none;
}
[data-ld-part="auth-set"] *,
[data-ld-part="auth-set"] *::before,
[data-ld-part="auth-set"] *::after {
box-sizing: border-box;
}
[data-ld-part="auth-set"] [hidden] {
display: none !important;
}
[data-ld-part="auth-set"] p {
margin: 0;
}
/* 読み上げだけに渡す文字 */
[data-ld-part="auth-set"] .vh {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
/* 段の左右の余白。帯だけは端まで出す */
[data-ld-part="auth-set"] .pad {
padding: 0 calc(var(--_space) * 4) 0 calc(var(--_space) * 4.5);
}
/* 見出し:大きく、太く、詰めて。下に細い罫線 */
[data-ld-part="auth-set"] .head {
padding-top: calc(var(--_space) * 5);
}
[data-ld-part="auth-set"] .title {
margin: 0;
padding-bottom: calc(var(--_space) * 2);
border-bottom: 1px solid var(--_fg);
font-size: 48px;
line-height: .95;
font-weight: 800;
letter-spacing: -.03em;
overflow-wrap: break-word;
}
[data-ld-part="auth-set"] .title:focus {
outline: none;
}
[data-ld-part="auth-set"] .lead {
max-width: 30ch;
margin-top: calc(var(--_space) * 1.5);
color: var(--_muted);
}
[data-ld-part="auth-set"] .lead strong {
color: var(--_fg);
font-weight: 600;
overflow-wrap: anywhere;
}
/* 外のアカウント:高さを抑えた横一列 */
[data-ld-part="auth-set"] .providers {
display: grid;
gap: calc(var(--_space) * 1);
margin-top: calc(var(--_space) * 3.5);
}
[data-ld-part="auth-set"] .caption,
[data-ld-part="auth-set"] .label {
font-size: 11px;
line-height: 16px;
font-weight: 600;
letter-spacing: .16em;
text-transform: uppercase;
}
[data-ld-part="auth-set"] .caption {
color: var(--_muted);
}
[data-ld-part="auth-set"] .provider-row {
display: flex;
gap: calc(var(--_space) * .75);
}
[data-ld-part="auth-set"] .provider {
display: inline-flex;
flex: 1 1 0;
align-items: center;
justify-content: center;
gap: calc(var(--_space) * .75);
min-width: 0;
height: 32px;
margin: 0;
padding: 0 calc(var(--_space) * 1);
border: 1px solid color-mix(in oklab, var(--_fg) 28%, transparent);
border-radius: var(--_radius);
background: transparent;
color: var(--_fg);
font: 600 12px/1 var(--_font);
letter-spacing: .02em;
white-space: nowrap;
cursor: pointer;
transition: background var(--_motion) ease, border-color var(--_motion) ease;
}
[data-ld-part="auth-set"] .provider:hover {
border-color: var(--_fg);
background: color-mix(in oklab, var(--_fg) 5%, transparent);
}
[data-ld-part="auth-set"] .icon-slot {
flex: none;
width: 12px;
height: 12px;
border-radius: calc(var(--_radius) * .5);
background: color-mix(in oklab, var(--_muted) 30%, transparent);
}
/* フォーム */
[data-ld-part="auth-set"] .form {
margin: 0;
}
[data-ld-part="auth-set"] .fields {
display: grid;
gap: calc(var(--_space) * 3);
margin-top: calc(var(--_space) * 3.5);
padding-bottom: calc(var(--_space) * 3.5);
}
[data-ld-part="auth-set"] .field {
display: grid;
gap: calc(var(--_space) * .5);
}
[data-ld-part="auth-set"] .label-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: calc(var(--_space) * 1);
}
[data-ld-part="auth-set"] .label {
display: flex;
align-items: baseline;
gap: calc(var(--_space) * 1);
padding: 0;
}
[data-ld-part="auth-set"] .num {
color: var(--_accent);
font-variant-numeric: tabular-nums;
letter-spacing: .04em;
}
[data-ld-part="auth-set"] .control {
position: relative;
}
/* 入力欄は下線だけ。フォーカスで線が太くなり、強調色に変わる */
[data-ld-part="auth-set"] .input,
[data-ld-part="auth-set"] .digit {
display: block;
width: 100%;
height: 44px;
margin: 0;
padding: 0;
border: 0;
border-bottom: 1px solid color-mix(in oklab, var(--_fg) 55%, transparent);
border-radius: 0;
background: transparent;
color: var(--_fg);
font: 400 18px/24px var(--_font);
caret-color: var(--_accent);
transition: border-color var(--_motion) ease;
}
[data-ld-part="auth-set"] .input.has-reveal {
padding-right: calc(var(--_space) * 7);
}
[data-ld-part="auth-set"] .input:hover,
[data-ld-part="auth-set"] .digit:hover {
border-bottom-color: var(--_fg);
}
[data-ld-part="auth-set"] .input:focus,
[data-ld-part="auth-set"] .digit:focus {
outline: none;
border-bottom-color: var(--_accent);
background: linear-gradient(var(--_accent), var(--_accent)) bottom / 100% 1px no-repeat;
}
/* エラー:色の token がないので、太い線と印で知らせる */
[data-ld-part="auth-set"] .input[aria-invalid="true"],
[data-ld-part="auth-set"] .digit[aria-invalid="true"] {
border-bottom-color: var(--_fg);
background: linear-gradient(var(--_fg), var(--_fg)) bottom / 100% 1px no-repeat;
}
[data-ld-part="auth-set"] .input[aria-invalid="true"]:focus,
[data-ld-part="auth-set"] .digit[aria-invalid="true"]:focus {
border-bottom-color: var(--_accent);
background: linear-gradient(var(--_accent), var(--_accent)) bottom / 100% 1px no-repeat;
}
[data-ld-part="auth-set"] .error {
display: flex;
align-items: flex-start;
gap: calc(var(--_space) * .75);
padding-top: calc(var(--_space) * .75);
font-size: 13px;
line-height: 18px;
font-weight: 600;
color: var(--_fg);
}
[data-ld-part="auth-set"] .error::before {
content: "!";
display: grid;
place-items: center;
flex: none;
width: 16px;
height: 16px;
margin-top: 1px;
border-radius: 50%;
background: var(--_fg);
color: var(--_surface);
font-size: 11px;
line-height: 1;
font-weight: 800;
}
/* Caps Lock の知らせ */
[data-ld-part="auth-set"] .caps {
padding-top: calc(var(--_space) * .75);
font-size: 11px;
line-height: 16px;
font-weight: 600;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--_muted);
}
/* 表示と非表示 */
[data-ld-part="auth-set"] .reveal {
position: absolute;
top: 50%;
right: 0;
height: 32px;
margin: 0;
padding: 0 0 0 calc(var(--_space) * 1);
border: 0;
border-radius: 0;
background: transparent;
color: var(--_muted);
font: 600 11px/1 var(--_font);
letter-spacing: .16em;
text-transform: uppercase;
cursor: pointer;
transform: translateY(-50%);
}
[data-ld-part="auth-set"] .reveal:hover {
color: var(--_fg);
}
/* パスワードの条件:小さな四角が、満たすと埋まる */
[data-ld-part="auth-set"] .rules {
display: flex;
flex-wrap: wrap;
gap: calc(var(--_space) * .75) calc(var(--_space) * 2);
margin: 0;
padding: calc(var(--_space) * 1.25) 0 0;
list-style: none;
}
[data-ld-part="auth-set"] .rule {
display: flex;
align-items: center;
gap: calc(var(--_space) * .75);
font-size: 12px;
line-height: 16px;
color: var(--_muted);
transition: color var(--_motion) ease;
}
[data-ld-part="auth-set"] .rule::before {
content: "";
flex: none;
width: 9px;
height: 9px;
border: 1px solid color-mix(in oklab, var(--_muted) 80%, transparent);
border-radius: 0;
transition: background var(--_motion) ease, border-color var(--_motion) ease;
}
[data-ld-part="auth-set"] .rule[data-met] {
color: var(--_fg);
}
[data-ld-part="auth-set"] .rule[data-met]::before {
border-color: var(--_accent);
background: var(--_accent);
}
/* 小さな文とリンク */
[data-ld-part="auth-set"] .aside {
font-size: 13px;
line-height: 18px;
color: var(--_muted);
}
[data-ld-part="auth-set"] .link {
margin: 0;
padding: 0;
border: 0;
background: none;
color: var(--_fg);
font: inherit;
font-weight: 600;
text-decoration: underline;
text-decoration-thickness: 1px;
text-decoration-color: var(--_accent);
text-underline-offset: 3px;
cursor: pointer;
}
[data-ld-part="auth-set"] .link:hover {
text-decoration-thickness: 2px;
}
[data-ld-part="auth-set"] .link.small {
font-size: 12px;
font-weight: 500;
}
[data-ld-part="auth-set"] .status {
font-size: 13px;
line-height: 18px;
font-weight: 600;
color: var(--_fg);
}
[data-ld-part="auth-set"] .status:empty {
display: none;
}
/* 送信:カードの下端いっぱいの帯。右端に矢印 */
[data-ld-part="auth-set"] .submit {
display: flex;
align-items: center;
justify-content: space-between;
gap: calc(var(--_space) * 1.5);
width: 100%;
min-height: 64px;
margin: 0;
padding: calc(var(--_space) * 1.5) calc(var(--_space) * 4) calc(var(--_space) * 1.5) calc(var(--_space) * 4.5);
border: 0;
border-radius: 0;
background: var(--_accent);
color: var(--_surface);
font: 700 17px/22px var(--_font);
letter-spacing: -.01em;
text-align: left;
cursor: pointer;
transition: background var(--_motion) ease;
}
[data-ld-part="auth-set"] .submit:hover {
background: color-mix(in oklab, var(--_accent) 88%, black);
}
[data-ld-part="auth-set"] .arrow {
flex: none;
width: 24px;
height: 24px;
fill: none;
stroke: currentColor;
stroke-width: 1.75;
stroke-linecap: square;
transition: transform var(--_motion) ease;
}
[data-ld-part="auth-set"] .submit:hover .arrow {
transform: translateX(4px);
}
[data-ld-part="auth-set"] .submit:disabled {
cursor: progress;
background: color-mix(in oklab, var(--_accent) 88%, black);
}
/* 送信中は、矢印の場所で回る */
[data-ld-part="auth-set"] .submit[aria-busy="true"] .arrow {
display: none;
}
[data-ld-part="auth-set"] .submit[aria-busy="true"]::after {
content: "";
flex: none;
width: 18px;
height: 18px;
margin-right: calc(var(--_space) * .375);
border: 2px solid color-mix(in oklab, var(--_surface) 35%, transparent);
border-top-color: var(--_surface);
border-radius: 50%;
animation: auth-set-spin calc(var(--_motion) * 5) linear infinite;
}
@keyframes auth-set-spin {
to { transform: rotate(1turn); }
}
/* 6桁:下線の枠が6つ */
[data-ld-part="auth-set"] .code-field {
display: grid;
gap: calc(var(--_space) * .5);
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
[data-ld-part="auth-set"] .code {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: calc(var(--_space) * 1.5);
margin-top: calc(var(--_space) * .5);
}
[data-ld-part="auth-set"] .digit {
height: 56px;
text-align: center;
font-size: 30px;
line-height: 1;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* 送ったあと */
[data-ld-part="auth-set"] .done {
display: grid;
gap: calc(var(--_space) * 2.5);
margin-top: calc(var(--_space) * 3.5);
padding-bottom: calc(var(--_space) * 5);
}
[data-ld-part="auth-set"] .done:focus {
outline: none;
}
[data-ld-part="auth-set"] .done-text {
font-size: 16px;
line-height: 22px;
}
[data-ld-part="auth-set"] .done-text strong {
overflow-wrap: anywhere;
}
[data-ld-part="auth-set"] .done-mark {
display: inline-block;
width: 10px;
height: 10px;
margin-right: calc(var(--_space) * 1);
background: var(--_accent);
}
/* キーボードのフォーカス */
[data-ld-part="auth-set"] .provider:focus-visible,
[data-ld-part="auth-set"] .reveal:focus-visible,
[data-ld-part="auth-set"] .link:focus-visible {
outline: 2px solid var(--_accent);
outline-offset: 2px;
}
[data-ld-part="auth-set"] .submit:focus-visible {
outline: 2px solid var(--_surface);
outline-offset: -6px;
}
@media (max-width: 420px) {
[data-ld-part="auth-set"] .pad {
padding: 0 calc(var(--_space) * 2.5) 0 calc(var(--_space) * 3);
}
[data-ld-part="auth-set"] .submit {
padding-right: calc(var(--_space) * 2.5);
padding-left: calc(var(--_space) * 3);
}
[data-ld-part="auth-set"] .title {
font-size: 40px;
}
}
@media (prefers-reduced-motion: reduce) {
[data-ld-part="auth-set"] *,
[data-ld-part="auth-set"] *::before,
[data-ld-part="auth-set"] *::after {
transition: none !important;
animation-duration: calc(var(--_motion) * 20) !important;
}
}
</style>
<script type="module">
/* UI by @Lattice_Node — ludion.ai/p/auth-set (MIT) */
// part.js — ESモジュール。rootの中だけを触る
// 4つの画面(Sign in、Create account、Reset password、Verify code)を1枚のカードの中で切り替える。
// エラーは、入力欄を離れたときか送信したときだけ出す。打っている最中には新しく出さない。
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const WAIT_MS = 800;
const RULES = {
length: (v) => v.length >= 8,
case: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v),
number: (v) => /\d/.test(v),
};
function messageFor(input) {
const value = input.value;
switch (input.dataset.validate) {
case 'email':
if (!value.trim()) return 'Enter your email address.';
if (!EMAIL.test(value.trim())) return 'Enter an email address like name@example.com.';
return '';
case 'password':
return value ? '' : 'Enter your password.';
case 'new-password':
if (!value) return 'Create a password.';
return Object.values(RULES).every((rule) => rule(value)) ? '' : 'Your password doesn’t meet every requirement yet.';
default:
return '';
}
}
function mount(root) {
const cleanups = [];
const timers = new Set();
const on = (el, type, fn) => {
el.addEventListener(type, fn);
cleanups.push(() => el.removeEventListener(type, fn));
};
const later = (fn, ms) => {
const t = setTimeout(() => {
timers.delete(t);
fn();
}, ms);
timers.add(t);
};
const byId = (id) => root.querySelector(`[id="${id}"]`);
const screens = [...root.querySelectorAll('[data-screen]')];
const screen = (name) => root.querySelector(`[data-screen="${name}"]`);
// ---- エラー
const errorFor = (input) => byId(input.getAttribute('aria-describedby').split(' ')[0]);
const setError = (input, message) => {
const error = errorFor(input);
error.textContent = message;
error.hidden = !message;
if (message) input.setAttribute('aria-invalid', 'true');
else input.removeAttribute('aria-invalid');
};
// ---- 画面の行き来
const digits = [...root.querySelectorAll('.digit')];
const codeError = byId('as-verify-error');
const verifyStatus = root.querySelector('[data-status]');
const clearCode = () => {
for (const d of digits) {
d.value = '';
d.removeAttribute('aria-invalid');
}
codeError.textContent = '';
codeError.hidden = true;
verifyStatus.textContent = '';
};
const go = (name, focusTarget) => {
for (const s of screens) s.hidden = s.dataset.screen !== name;
const target = screen(name);
// 前に送った画面は、フォームに戻しておく
const form = target.querySelector('form');
const done = target.querySelector('[data-done]');
if (form) form.hidden = false;
if (done) done.hidden = true;
if (name === 'verify') clearCode();
(focusTarget ?? target.querySelector('.title')).focus();
};
for (const link of root.querySelectorAll('[data-go]')) {
on(link, 'click', () => go(link.dataset.go));
}
// リンクやボタンを押し下げた瞬間に入力欄から離れると、エラーの文が出てボタンが下へずれ、
// 指を離す位置がボタンの外になってクリックが成り立たない。押し下げではフォーカスを動かさない。
for (const button of root.querySelectorAll('[data-go], [data-resend], .reveal, .submit')) {
on(button, 'mousedown', (event) => event.preventDefault());
}
// ---- 入力欄:離れたときに確かめる。打っている間は、直ったエラーを消すだけ
for (const input of root.querySelectorAll('[data-validate]')) {
on(input, 'input', () => {
input.dataset.dirty = 'true';
if (input.hasAttribute('aria-invalid') && !messageFor(input)) setError(input, '');
});
on(input, 'blur', () => {
if (input.dataset.dirty === 'true') setError(input, messageFor(input));
});
}
// ---- パスワード:表示と非表示、Caps Lock
for (const button of root.querySelectorAll('.reveal')) {
const input = byId(button.getAttribute('aria-controls'));
const text = button.querySelector('.reveal-text');
on(button, 'click', () => {
const showing = input.type === 'text';
input.type = showing ? 'password' : 'text';
text.textContent = showing ? 'Show' : 'Hide';
});
}
for (const input of root.querySelectorAll('.has-reveal')) {
const caps = byId(`${input.id}-caps`);
const update = (event) => {
caps.hidden = !(event.getModifierState && event.getModifierState('CapsLock'));
};
on(input, 'keydown', update);
on(input, 'keyup', update);
on(input, 'blur', () => {
caps.hidden = true;
});
}
// ---- 新しいパスワードの条件:満たした順に、その場でチェック
const newPassword = byId('as-signup-password');
const rules = [...root.querySelectorAll('[data-rule]')];
on(newPassword, 'input', () => {
for (const li of rules) {
const met = RULES[li.dataset.rule](newPassword.value);
li.toggleAttribute('data-met', met);
li.querySelector('[data-rule-state]').textContent = met ? 'Done: ' : 'Not yet: ';
}
});
// ---- 6桁の枠
const codeComplete = () => digits.every((d) => /^\d$/.test(d.value));
const fill = (from, text) => {
const chars = text.replace(/\D/g, '').slice(0, digits.length - from).split('');
chars.forEach((c, i) => {
digits[from + i].value = c;
});
const next = Math.min(from + chars.length, digits.length - 1);
digits[next].focus();
digits[next].select();
if (codeComplete()) {
codeError.hidden = true;
codeError.textContent = '';
for (const d of digits) d.removeAttribute('aria-invalid');
}
};
digits.forEach((box, i) => {
on(box, 'focus', () => box.select());
on(box, 'keydown', (event) => {
if (/^\d$/.test(event.key)) {
event.preventDefault();
fill(i, event.key);
} else if (event.key === 'Backspace') {
event.preventDefault();
if (box.value) box.value = '';
else if (i > 0) {
digits[i - 1].value = '';
digits[i - 1].focus();
}
} else if (event.key === 'ArrowLeft' && i > 0) {
event.preventDefault();
digits[i - 1].focus();
} else if (event.key === 'ArrowRight' && i < digits.length - 1) {
event.preventDefault();
digits[i + 1].focus();
}
});
// スマホのキーボードや自動入力は keydown を通らないので、input でも受ける
on(box, 'input', () => {
const value = box.value.replace(/\D/g, '');
if (!value) {
box.value = '';
return;
}
fill(i, value);
});
on(box, 'paste', (event) => {
event.preventDefault();
const text = (event.clipboardData && event.clipboardData.getData('text')) || '';
if (/\d/.test(text)) fill(0, text);
});
});
// 6つの枠の外へ出たとき、途中まで打っていたら知らせる
const showCodeError = () => {
codeError.textContent = 'Enter all 6 digits.';
codeError.hidden = false;
for (const d of digits) d.setAttribute('aria-invalid', 'true');
};
const code = root.querySelector('[data-code]');
on(code, 'focusout', (event) => {
if (code.contains(event.relatedTarget)) return;
if (digits.some((d) => d.value) && !codeComplete()) showCodeError();
});
// ---- 送信:約0.8秒の見せかけの待ち。二度押しできない
const busy = (form, onDone) => {
const button = form.querySelector('.submit');
const text = button.querySelector('.submit-text');
const idle = text.textContent;
button.disabled = true;
button.setAttribute('aria-busy', 'true');
text.textContent = button.dataset.busy;
later(() => {
button.disabled = false;
button.removeAttribute('aria-busy');
text.textContent = idle;
onDone();
}, WAIT_MS);
};
const validateForm = (form) => {
let first = null;
for (const input of form.querySelectorAll('[data-validate]')) {
const message = messageFor(input);
setError(input, message);
if (message && !first) first = input;
}
return first;
};
const emailOf = (form) => form.querySelector('[type="email"]').value.trim();
const showEmail = (name, email) => {
for (const el of screen(name).querySelectorAll('[data-email]')) el.textContent = email;
};
const finish = (section) => {
section.querySelector('form').hidden = true;
const done = section.querySelector('[data-done]');
done.hidden = false;
done.focus();
};
// 送信は、送信ボタンの click で受ける。Ludion の iframe は sandbox="allow-scripts" だけで
// allow-forms がないので、submit イベントが来ない。Enter で送ったときも、ブラウザが送信ボタンを click する。
// click を止めればフォームは送られないが、ふつうのページに貼られたときのために submit も止めておく。
for (const form of root.querySelectorAll('form')) {
const button = form.querySelector('.submit');
on(button, 'click', (event) => {
event.preventDefault();
send(form);
});
on(form, 'submit', (event) => {
event.preventDefault();
send(form);
});
}
function send(form) {
if (form.querySelector('.submit').disabled) return;
const kind = form.dataset.form;
if (kind === 'verify') {
if (!codeComplete()) {
showCodeError();
(digits.find((d) => !d.value) ?? digits[0]).focus();
return;
}
busy(form, () => finish(screen('verify')));
return;
}
const invalid = validateForm(form);
if (invalid) {
invalid.focus();
return;
}
const email = emailOf(form);
if (kind === 'reset') {
busy(form, () => {
showEmail('reset', email);
finish(screen('reset'));
});
} else {
busy(form, () => {
showEmail('verify', email);
go('verify', digits[0]);
});
}
}
// ---- コードの再送
on(root.querySelector('[data-resend]'), 'click', () => {
verifyStatus.textContent = 'We sent a new code.';
});
return () => {
for (const off of cleanups) off();
for (const t of timers) clearTimeout(t);
timers.clear();
};
}
mount(document.querySelector('[data-ld-part="auth-set"]'));
</script>
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node
@Lattice_Node