ApiKeyField
A secret shown masked in a read-only field, with Show and Copy buttons and a hidden input for forms.
Demo
Not revealed yet.
Shown once; copy it now.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/api_key_field.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/api_key_field.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/api_key_field.qwe
component ApiKeyFieldDemoLive() { state looks = 0 <div.demo-stack> <ApiKeyField label="Live key" value="qk_live_4f9a2b7c1d3e5f6a7b8c9d0e" name="live_key" id="api-key-demo" on_reveal={(s) => { if s { looks += 1 } }}/> <p.demo-note>{looks == 0 ? "Not revealed yet." : `Revealed ${looks} time${looks == 1 ? "" : "s"}.`}</p> </div>}view ApiKeyFieldDemo() = <div.demo-stack.demo-narrow> <ApiKeyFieldDemoLive/> <ApiKeyField label="Webhook secret" value="whsec_9a8b7c6d5e4f3a2b1c0d" shown={true} hint="Shown once; copy it now." size="sm"/> </div>Source
ui/api_key_field.qwe
// ApiKeyField: a secret shown masked, with a button that reveals it and one that// copies it. The key is in a read-only field, so it can be selected, and in a// hidden input named `name`, so a form sends it whole.//// <ApiKeyField label="Live key" value="qk_live_4f9a2b7c1d3e5f6a7b8c9d0e"/>// <ApiKeyField label="Webhook secret" value={secret} name="secret" shown={true} hint="Shown once; copy it now."/>//// value: the secret. label: what it is ("API key"). id: the field's id// ("api-key-" + name, or "api-key"). name: a hidden input with the whole value.// keep: how many characters stay readable at each end while masked (4); the// rest are dots. shown: starts revealed. hint: a line under it. size: "sm" or// "md" (default). The reveal button is a toggle (aria-pressed), "Show key" or// "Hide key"; Copy says "Copied" for a moment (and tells screen readers), or// "Could not copy" when the browser refuses (the page is not https).// on_reveal(shown): inside one of your components, called when it is shown or// hidden, to log who looked.import ui/theme.qwe use ui_clslet api_key_field_css = css`.ui-apikey { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-apikey-label { font-size: 13px; font-weight: 500; }.ui-apikey-box { display: flex; align-items: stretch; min-width: 0; }.ui-apikey-control { box-sizing: border-box; flex: 1; min-width: 0; height: 38px; margin: 0; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm) 0 0 var(--ui-radius-sm); background: var(--ui-subtle); color: var(--ui-fg); font: 13px/1 var(--ui-mono); letter-spacing: .02em; text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-apikey-control:focus { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); position: relative; z-index: 1; }.ui-apikey-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 38px; margin-left: -1px; padding: 0 12px; border: 1px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-muted); font: 500 13px/1 var(--ui-font); cursor: pointer; white-space: nowrap;}.ui-apikey-btn:last-child { border-radius: 0 var(--ui-radius-sm) var(--ui-radius-sm) 0; }.ui-apikey-btn:hover { background: var(--ui-subtle); color: var(--ui-fg); }.ui-apikey-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); position: relative; z-index: 1; }.ui-apikey-btn[aria-pressed="true"] { color: var(--ui-accent); }.ui-apikey-btn svg { width: 16px; height: 16px; }.ui-apikey-ok { color: var(--ui-success); }.ui-apikey-bad { color: var(--ui-danger); }.ui-apikey-hint { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-apikey-sm .ui-apikey-control, .ui-apikey-sm .ui-apikey-btn { height: 30px; font-size: 12px; }.ui-apikey-sm .ui-apikey-btn { padding: 0 9px; }@media (max-width: 480px) { .ui-apikey-btn span { display: none; } }@media (prefers-reduced-motion: reduce) { .ui-apikey-control { transition: none; } }`// The secret with its middle as dots: "qk_l••••••••••••9d0e".fn api_key_field_mask(value: str, keep: int) -> str { let n = len(value) if n <= keep * 2 { return repeat("•", n) } slice(value, 0, keep) + repeat("•", min([n - keep * 2, 12])) + slice(value, n - keep, n)}component ApiKeyField(value: str, label: str = "API key", id: str? = null, name: str? = null, keep: int = 4, shown: bool = false, hint: str? = null, size: str = "md", on_reveal = null, class: str? = null) { style global api_key_field_css state revealed = shown state result = "" // "", "ok" or "bad" if result != "" { every(1500, () => { result = "" }) } let fid = id ?? (name != null ? `api-key-${name}` : "api-key") <div class={ui_cls(["ui-apikey", size == "sm" && "ui-apikey-sm", class])}> <label.ui-apikey-label for={fid}>{label}</label> <div.ui-apikey-box> <input.ui-apikey-control type="text" id={fid} value={revealed ? value : api_key_field_mask(value, keep)} readonly={true} spellcheck="false" autocomplete="off" aria-describedby={hint != null ? `${fid}-hint` : null} @focus={(e) => { if revealed { e.target.select() } }}/> <button.ui-apikey-btn type="button" aria-pressed={revealed ? "true" : "false"} aria-controls={fid} aria-label={revealed ? "Hide key" : "Show key"} title={revealed ? "Hide key" : "Show key"} @click={() => { revealed = !revealed on_reveal && on_reveal(revealed) }}> {revealed ? <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3l14 14"/><path d="M8.2 4.6A8.6 8.6 0 0 1 10 4.4c4.4 0 7.4 3.9 8.2 5.6-.4.8-1.2 2-2.4 3.1M12.8 15a7.9 7.9 0 0 1-2.8.6c-4.4 0-7.4-3.9-8.2-5.6.5-1 1.6-2.6 3.2-3.8"/><path d="M8.4 8.4a2.3 2.3 0 0 0 3.2 3.2"/></svg> : <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1.8 10c.8-1.7 3.8-5.6 8.2-5.6s7.4 3.9 8.2 5.6c-.8 1.7-3.8 5.6-8.2 5.6S2.6 11.7 1.8 10z"/><circle cx="10" cy="10" r="2.6"/></svg>} <span>{revealed ? "Hide" : "Show"}</span> </button> <button class={ui_cls(["ui-apikey-btn", result != "" && `ui-apikey-${result}`])} type="button" aria-label="Copy key" title="Copy key" @click={() => { if js("isSecureContext") != true { result = "bad" } else { js("navigator.clipboard.writeText", value).then((_ = null) => { result = "ok" }, (_ = null) => { result = "bad" }) } }}> {result == "ok" ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg> : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2ZM4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"/></svg>} <span>{result == "ok" ? "Copied" : result == "bad" ? "Could not copy" : "Copy"}</span> </button> </div> {name != null && <input type="hidden" name={name} value={value}/>} {hint != null && <p.ui-apikey-hint id={`${fid}-hint`}>{hint}</p>} <span.ui-sr role="status">{result == "ok" ? "Copied" : result == "bad" ? "Could not copy" : ""}</span> </div>}Related
DangerZoneA red-bordered block of destructive actions (DangerAction), each a form, link or callback that asks before it acts.6 views · 8 downloadsPermissionsMatrixRoles across, permissions down, a checkbox at each: a form sends the pairs ticked, a column box ticks a whole role.6 views · 6 downloadsSessionListSigned-in devices with browser, system, place and time, this one marked, and Sign out for each or all the others.6 views · 5 downloadsSettingsSectionA titled block of settings rows, each a label and hint with its control at the right (SettingsRow).5 views · 5 downloadsTokenListAPI tokens with name, prefix, created, last used, scopes and expiry, and a Revoke that asks first.6 views · 8 downloadsConsentCheckboxA required checkbox beside legal text with links to the terms and the privacy policy, posted as a real checkbox.5 views · 6 downloads
Thread
Thread 0 posts
On ApiKeyField: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.