SPB Git

spb/ultra-sharp-agent-skills Public

Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.

Python 100%
5.2 KB · 148 lines markdown
Rendered Raw Blame History
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# Form Patterns — Copy-Paste Reference78## Contents9- Complete field markup10- Common autocomplete values11- Validation timing (blur, then input)12- Error summary after failed submit13- Submit button pending state14- Multi-step form skeleton15- Gotchas1617## Complete field markup1819```html20<div class="field">21  <label for="email">Email</label>22  <input id="email" name="email" type="email" required23         autocomplete="email" placeholder="name@example.com"24         aria-describedby="email-hint email-error">25  <p id="email-hint" class="hint">We only use this for receipts.</p>26  <p id="email-error" class="error" hidden></p>27</div>28```2930The error node exists from the start (hidden) so `aria-describedby` stays stable.3132## Common autocomplete values3334| Field | type | autocomplete |35|---|---|---|36| Email | email | email |37| Phone | tel | tel |38| First / last name | text | given-name / family-name |39| Street / city / postal | text | address-line1 / address-level2 / postal-code |40| Country | (select) | country-name |41| Card number / expiry / CVC | text | cc-number / cc-exp / cc-csc |42| New / current password | password | new-password / current-password |43| One-time code | text | one-time-code |4445## Validation timing (blur, then input)4647```js48const messages = {49  valueMissing: (label) => `Enter your ${label.toLowerCase()}.`,50  typeMismatch: () => 'Enter an email address with an @, like name@example.com.',51  tooShort: (label, input) => `${label} must be at least ${input.minLength} characters.`,52};5354function validate(input) {55  const label = input.labels[0].textContent;56  const errEl = document.getElementById(input.getAttribute('aria-describedby')57                                        .split(' ').find(id => id.endsWith('-error')));58  let msg = '';59  for (const key of Object.keys(messages)) {60    if (input.validity[key]) { msg = messages[key](label, input); break; }61  }62  input.setAttribute('aria-invalid', msg ? 'true' : 'false');63  errEl.textContent = msg;64  errEl.hidden = !msg;65  return !msg;66}6768document.querySelectorAll('input, select, textarea').forEach((input) => {69  input.addEventListener('blur', () => validate(input), { once: false });70  input.addEventListener('input', () => {71    // re-validate live only after the field has already errored72    if (input.getAttribute('aria-invalid') === 'true') validate(input);73  });74});75```7677Pair with `<form novalidate>` so these messages replace the browser bubbles.7879## Error summary after failed submit8081```js82form.addEventListener('submit', (e) => {83  const fields = [...form.querySelectorAll('input, select, textarea')];84  const invalid = fields.filter((f) => !validate(f));85  if (invalid.length) {86    e.preventDefault();87    const summary = document.getElementById('form-alert');88    summary.innerHTML = `<p>Fix ${invalid.length} error(s):</p><ul>` +89      invalid.map((f) => `<li><a href="#${f.id}">${f.labels[0].textContent}</a></li>`).join('') +90      '</ul>';91    invalid[0].focus();92  }93});94```9596```html97<div id="form-alert" role="alert" tabindex="-1"></div>98```99100## Submit button pending state101102```js103form.addEventListener('submit', async (e) => {104  e.preventDefault();105  const btn = form.querySelector('button[type="submit"]');106  btn.disabled = true;107  btn.dataset.label = btn.textContent;108  btn.textContent = 'Creating account…';109  try {110    await submit(new FormData(form));111  } catch (err) {112    showServerErrors(err);           // role="alert" summary; inputs keep their values113  } finally {114    btn.disabled = false;115    btn.textContent = btn.dataset.label;116  }117});118```119120Disable only during the request — a permanently disabled submit hides *why* the form can't be sent.121122## Multi-step form skeleton123124```html125<form id="wizard">126  <p class="progress" aria-live="polite">Step <span id="step-n">1</span> of 3</p>127  <fieldset data-step="1"><legend>Your details</legend>…</fieldset>128  <fieldset data-step="2" hidden><legend>Shipping</legend>…</fieldset>129  <fieldset data-step="3" hidden><legend>Review &amp; confirm</legend>…</fieldset>130  <button type="button" id="back" hidden>Back</button>131  <button type="button" id="next">Continue</button>132  <button type="submit" id="finish" hidden>Place order</button>133</form>134```135136Rules encoded above: steps are fieldsets shown one at a time (data survives because137nothing unmounts), per-step validation runs on "Continue", the last step is a138read-only review, and the progress line is announced on change.139140## Gotchas141142- `<form novalidate>` disables browser bubbles but NOT the `validity` API — exactly what you want for custom messages.143- Disabled inputs are skipped on submit AND invisible to autofill/screen readers in some browsers — prefer `readonly` for locked-but-submitted values.144- `pattern` is anchored (implicit `^…$`) and silently fails on typos in the regex; always pair with a `title`/error message that states the format.145- iOS zooms into inputs with font-size <16px — keep inputs ≥16px.146- Numeric codes (postal, OTP): use `inputmode="numeric" pattern="[0-9]*"`, NOT `type="number"` (which strips leading zeros and adds spinners).147- Autofocus on page load disorients screen-reader users — reserve `autofocus` for single-purpose pages (search, login).148