Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
form-validation-js.tsx5.4 KB · 139 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
 * BLOCK O2 — Inline client-side form validation.
 *
 * Vanilla JS that mounts a `<span class="field-error" aria-live="polite">`
 * under every matched form input. Fires on blur AND on input (after the
 * first blur). Per-field override via data-validation-message attribute.
 *
 * Visual states:
 *   .input-invalid → red border
 *   .input-valid   → green border + checkmark icon
 *
 * Server-side `?error=…` redirect validation still works — this is
 * additive UX polish.
 */

import type { FC } from "hono/jsx";

export const formValidationScript = /* js */ `
(function () {
  if (window.__gluecronFormValidationMounted) return;
  window.__gluecronFormValidationMounted = true;

  function ready(fn) {
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", fn);
    } else { fn(); }
  }

  function validateInput(el) {
    var v = el.value;
    if (el.disabled || el.readOnly) return "";
    if (el.hasAttribute("required") && v.trim() === "") {
      return el.getAttribute("data-validation-required")
        || (el.labels && el.labels[0] ? el.labels[0].textContent + " is required" : "This field is required");
    }
    if (v === "") return "";
    if (el.type === "email") {
      var emailRe = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
      if (!emailRe.test(v)) {
        return el.getAttribute("data-validation-message") || "Enter a valid email address";
      }
    }
    var pat = el.getAttribute("pattern");
    if (pat) {
      try {
        if (!new RegExp("^(?:" + pat + ")$").test(v)) {
          return el.getAttribute("data-validation-message") || "Doesn't match the required format";
        }
      } catch (_e) {}
    }
    var minL = parseInt(el.getAttribute("minlength") || "0", 10);
    if (minL > 0 && v.length < minL) {
      return el.getAttribute("data-validation-message") || ("Must be at least " + minL + " characters");
    }
    var maxL = parseInt(el.getAttribute("maxlength") || "0", 10);
    if (maxL > 0 && v.length > maxL) {
      return el.getAttribute("data-validation-message") || ("Must be at most " + maxL + " characters");
    }
    return "";
  }

  function ensureErrorSpan(el) {
    var id = el.id || el.name;
    if (!id) { id = "fv-" + Math.random().toString(36).slice(2, 8); el.id = id; }
    var errId = id + "-fv-error";
    var span = document.getElementById(errId);
    if (!span) {
      span = document.createElement("span");
      span.id = errId;
      span.className = "field-error";
      span.setAttribute("aria-live", "polite");
      el.parentNode && el.parentNode.insertBefore(span, el.nextSibling);
      var describedBy = el.getAttribute("aria-describedby") || "";
      var parts = describedBy.split(/\\s+/).filter(Boolean);
      if (parts.indexOf(errId) < 0) {
        parts.push(errId);
        el.setAttribute("aria-describedby", parts.join(" "));
      }
    }
    return span;
  }

  function applyState(el, msg) {
    var span = ensureErrorSpan(el);
    if (msg) {
      el.classList.add("input-invalid");
      el.classList.remove("input-valid");
      el.setAttribute("aria-invalid", "true");
      span.textContent = msg;
      span.classList.add("field-error-shown");
    } else if (el.value === "") {
      el.classList.remove("input-invalid");
      el.classList.remove("input-valid");
      el.removeAttribute("aria-invalid");
      span.textContent = "";
      span.classList.remove("field-error-shown");
    } else {
      el.classList.remove("input-invalid");
      el.classList.add("input-valid");
      el.removeAttribute("aria-invalid");
      span.textContent = "";
      span.classList.remove("field-error-shown");
    }
  }

  function wire(el) {
    if (el.__fvWired) return;
    el.__fvWired = true;
    el.addEventListener("blur", function () { el.__fvBlurred = true; applyState(el, validateInput(el)); });
    el.addEventListener("input", function () { if (!el.__fvBlurred) return; applyState(el, validateInput(el)); });
  }

  function scan(root) {
    var sel = "input[required],input[pattern],input[minlength],input[maxlength],input[type=email]";
    var nodes = (root || document).querySelectorAll(sel);
    for (var i = 0; i < nodes.length; i++) {
      var el = nodes[i];
      if (el.type === "hidden" || el.type === "submit" || el.type === "button") continue;
      wire(el);
    }
  }

  ready(function () { scan(document); });
})();
`;

export const formValidationCss = `
.input-invalid { border-color: var(--red, #ff6a6a) !important; box-shadow: 0 0 0 2px rgba(255,106,106,0.18) !important; }
.input-valid { border-color: var(--green, #4ade80) !important; box-shadow: 0 0 0 2px rgba(74,222,128,0.18) !important; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%234ade80' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.5 3.5L13 4.5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; background-size: 14px 14px; padding-right: 32px !important; }
.field-error { display: block; margin-top: 4px; font-size: 12px; color: var(--red, #ff6a6a); line-height: 1.4; min-height: 0; transition: min-height 120ms ease; }
.field-error-shown { min-height: 1.4em; }
`;

export const FormValidationAssets: FC = () => (
  <>
    <style dangerouslySetInnerHTML={{ __html: formValidationCss }} />
    <script dangerouslySetInnerHTML={{ __html: formValidationScript }} />
  </>
);