المكوّنات · زرّ بحالة انتظار
زرّ بحالة انتظار
يمنع النقر المزدوج ويحافظ على عرضه أثناء الانتظار
12 تصاميم من هذه العائلة
<div class="bt btl">
<button class="pay">
<span class="sp" hidden aria-hidden="true"></span>
<span class="tx" data-ar="ادفع الآن" data-en="Pay now">ادفع الآن</span>
</button>
<div class="log" role="status" aria-live="polite"></div>
<p class="lbl" data-ar="جرّب النقر مرّتين بسرعة — الطلب يُرسل مرّة واحدة" data-en="Try double-clicking — the request fires once">جرّب النقر مرّتين بسرعة — الطلب يُرسل مرّة واحدة</p>
</div>
/* pt-scoped */
/* button-loading — بوّابة دفع */
.btl{width:100%;height:100%;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:9px;padding:10px;
font-size:10px;color:var(--pt-text-secondary);overflow:hidden}
.btl .lbl{font-size:8px;color:var(--pt-text-muted);text-align:center}
.btl button{font:inherit;cursor:pointer;display:inline-flex;align-items:center;
justify-content:center;gap:5px;white-space:nowrap;
transition:background var(--pt-motion-duration-fast) var(--pt-motion-easing-standard),
border-color var(--pt-motion-duration-fast) var(--pt-motion-easing-standard),
color var(--pt-motion-duration-fast) var(--pt-motion-easing-standard),
transform var(--pt-motion-duration-fast) var(--pt-motion-easing-standard),
box-shadow var(--pt-motion-duration-fast) var(--pt-motion-easing-standard)}
.btl button:disabled{cursor:not-allowed}
.btl button:focus-visible{outline:2px solid var(--pt-focus-ring);outline-offset:2px}
/* أي زرّ ينزل بمقدار بكسل عند الضغط — تأكيد لمسيّ قبل وصول الردّ */
.btl button:not(:disabled):active{transform:translateY(1px)}
@media(prefers-reduced-motion:reduce){.btl button{transition:none}
.btl button:not(:disabled):active{transform:none}
}
/* هدف اللمس 44 بكسلًا على الشاشات اللمسية دون تضخيم سطح المكتب */
@media(pointer:coarse){.btl button{min-height:44px;min-width:44px}
}
/* شكل: كبسولة عريضة، عرضها ثابت أثناء الانتظار */
.btl .pay{min-width:132px;padding:9px 18px;border-radius:var(--pt-radius-full);border:0;
background:var(--pt-accent);color:var(--pt-color-ink-900);font-size:10px;font-weight:700}
.btl .pay:not(:disabled):hover{background:color-mix(in srgb,var(--pt-accent) 84%,#fff)}
.btl .pay[aria-busy=true]{opacity:.72;cursor:progress}
.btl .pay.ok{background:var(--pt-accent-muted);color:var(--pt-accent)}
.btl .sp{width:11px;height:11px;border-radius:50%;flex:none;
border:2px solid color-mix(in srgb,var(--pt-color-ink-900) 30%,transparent);
border-top-color:var(--pt-color-ink-900);animation:btspin .65s linear infinite}
@keyframes btspin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.btl .sp{animation:none;border-top-color:transparent;
border-inline-start-color:var(--pt-color-ink-900)}
}
.btl .log{min-height:22px;font-size:8px;color:var(--pt-text-muted);text-align:center;
font-variant-numeric:tabular-nums}
/* يعمل داخل إطار معزول — لا يعتمد على شيء خارجه */
/* pt-remount — يعاد التركيب عند تبديل اللغة */
(function () {
var host = document.querySelector('.btl');
if (!host) return;
var snapshot = host.innerHTML;
var timers = [];
var ac = null;
/* الجسم كما كتبه المولِّد. يستقبل بدائل مظلَّلة لـ root وdocument
والمؤقّتات، فينظّف نفسه دون أن يعرف أنه قابل لإعادة التركيب. */
function run(root, document, setTimeout, setInterval) {
var ar = document.documentElement.lang !== 'en';
var n = function (v) { return Number(v).toLocaleString('en-US'); };
var pay = root.querySelector('.pay'), sp = root.querySelector('.sp');
var tx = root.querySelector('.tx'), log = root.querySelector('.log');
var busy = false, sent = 0;
pay.addEventListener('click', function () {
/* القفل قبل أي شيء: زرّ دفع يبقى قابلًا للنقر أثناء الطلب يخصم مرّتين */
if (busy) { log.textContent = ar ? 'نقرة مُهمَلة — الطلب قيد التنفيذ'
: 'Click ignored — request in flight'; return; }
busy = true;
pay.setAttribute('aria-busy', 'true');
pay.disabled = true;
sp.hidden = false;
tx.textContent = ar ? 'جارٍ التنفيذ…' : 'Processing…';
log.textContent = ar ? 'أُرسل طلب واحد (' + (++sent) + ')' : 'One request sent (' + (++sent) + ')';
setTimeout(function () {
sp.hidden = true;
pay.classList.add('ok');
tx.textContent = ar ? '✓ تمّ الدفع' : '✓ Paid';
setTimeout(function () {
busy = false;
pay.disabled = false;
pay.removeAttribute('aria-busy');
pay.classList.remove('ok');
tx.textContent = ar ? 'ادفع الآن' : 'Pay now';
}, 1300);
}, 1500);
});
}
function mount() {
ac = new AbortController();
var real = window.document;
/* وسيط يمرّر كل شيء إلى المستند الحقيقي، ويعلّق كل مستمع بإشارة
تُقطع عند إعادة التركيب — فلا يبقى مستمع من نسخة ماتت. */
var doc = new Proxy(real, {
get: function (t, k) {
if (k === 'addEventListener') {
return function (type, fn, opts) {
var o = (opts && typeof opts === 'object') ? Object.assign({}, opts)
: { capture: !!opts };
o.signal = ac.signal;
return t.addEventListener(type, fn, o);
};
}
var v = t[k];
return typeof v === 'function' ? v.bind(t) : v;
}
});
function track(fn) {
return function (f, ms) { var id = fn(f, ms); timers.push(id); return id; };
}
run(host, doc, track(window.setTimeout.bind(window)),
track(window.setInterval.bind(window)));
}
function unmount() {
if (ac) ac.abort();
// المعرّفان يتشاركان فضاءً واحدًا في المتصفّح، فالإلغاء المزدوج آمن
timers.forEach(function (id) { window.clearTimeout(id); window.clearInterval(id); });
timers = [];
host.innerHTML = snapshot;
}
mount();
addEventListener('pt-lang', function () { unmount(); mount(); });
})();
# Async Loading Button
**Role:** You are an Arabic-first UI designer and front-end engineer. Read the whole
specification before writing a single line, then follow it literally. If two requirements
conflict, favour usability over visual effect.
**Goal:** Async Loading Button — Blocks the double click and holds its width while pending
**Why this component matters:** A payment button that stays clickable while the request is in flight will charge twice. Lock it on the first click and keep the width fixed so the layout does not jump.
**Direction:** Genuine RTL — not a mirrored Latin layout. Use logical properties
(`margin-inline-start`, `padding-inline`, `inset-inline-start`, `text-align: start`),
never physical ones (`margin-left`, `left`). The layout must flip with `dir` on its own,
with no duplicated rules.
**Typography:** Cairo for Arabic, IBM Plex Sans for Latin. Arabic line-height must be
`0.18` higher than the Latin equivalent — dots, hamzas and maddas need more vertical room.
Arabic font-size runs `1.06×` the Latin size at the same optical weight.
**Numerals:** Western digits in both languages (`1,234.50`). Only the currency changes:
`ر.س` **after** the number in Arabic, `$` **before** it in English. Jordanian dinar
uses **three** decimal places, not two.
**Colors:** Dark mode — background `#0D142B` · surfaces `#211F54` · borders `#545C91` · text `#918FC2`.
Accent `#ADA9E8` must never exceed 8% of the screen area.
**Language:** Bilingual — every string carries both Arabic and English, and the layout
flips with `dir` automatically.
**States:** `default` · `hover` · `focus` · `active` · `disabled` · `loading` · `error`.
Do not design the resting state alone.
**Accessibility:** Contrast ratio at least `4.5:1`, complete keyboard navigation,
visible focus rings, and honour `prefers-reduced-motion`.
**Output:** Clean HTML and CSS in two separate files. No frameworks, no libraries,
no external dependencies.