@faqir-ui/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ // @ui:plugin faqir-validate
2
+ // @ui:provides l-validate
3
+ /**
4
+ * faqir-validate — declarative form validation for the field-group contract. [0.6-02 · §7.1, §A5]
5
+ *
6
+ * <form l-validate>
7
+ * <div data-ui="field-group">
8
+ * <label data-part="label" for="email">Work email</label>
9
+ * <input data-part="input" id="email" name="email" type="email" required
10
+ * l-validate:company="isCompanyEmail(value)"
11
+ * data-error-company="Use your company address.">
12
+ * <p data-part="error"></p>
13
+ * </div>
14
+ * </form>
15
+ *
16
+ * Reflects each control's native `ValidityState` (plus any custom validators)
17
+ * into the enclosing `[data-ui="field-group"]`: sets `data-state="invalid"`,
18
+ * fills the group's `[data-part="error"]` with a message, and wires
19
+ * `aria-invalid` (+ `aria-describedby` to the error part when it has an id).
20
+ * No JavaScript is required from the page author for the full validation UX.
21
+ *
22
+ * ── Revalidation policy (the exact UX contract) ─────────────────────────────
23
+ * • On **submit**: every field is validated. If any is invalid the submit is
24
+ * blocked (`preventDefault`) and focus moves to the first offender. When all
25
+ * are clean the browser submits natively — unless `l-validate` carries an
26
+ * expression, which is then treated as an on-valid hook (SPA submit) and run
27
+ * while the native submit is suppressed.
28
+ * • **After the first submit attempt**, each field additionally revalidates
29
+ * live — on `blur` (error surfaces when you leave it) and on `input` (error
30
+ * clears as you fix it). Before that first attempt, typing is never
31
+ * interrupted with errors. (`blur` doesn't bubble, so both are captured.)
32
+ *
33
+ * ── Custom validators ───────────────────────────────────────────────────────
34
+ * `l-validate:<name>="expr"` on a control runs only after native constraints
35
+ * pass. `expr` is evaluated with the control's current `value` (and `$el`) in
36
+ * scope; a falsy result marks the field invalid. Its message comes from
37
+ * `data-error-<name>`, then `data-error`, then a default. Multiple named
38
+ * validators may coexist on one control; the first to fail wins.
39
+ *
40
+ * ── Message resolution (when a field is invalid) ────────────────────────────
41
+ * custom-validator message → `data-error-<constraint>` → `data-error`
42
+ * → the browser's native `validationMessage` → a built-in default.
43
+ *
44
+ * Escape hatch: `data-validate-ignore` on a control opts it out entirely.
45
+ *
46
+ * Self-registers via `Faqir.plugin` when a global `Faqir` is present (load it
47
+ * after faqir-core) and is also exported for bundlers/tests. Zero dependencies,
48
+ * ≤ 2 KB gzip. The only attributes it owns are `data-state="invalid"` on the
49
+ * group and `aria-invalid`/`aria-describedby` on the control — the frozen
50
+ * five-attribute protocol is untouched.
51
+ */
52
+ (function () {
53
+ "use strict";
54
+
55
+ // Non-field / non-validatable input types — skipped when scanning a form.
56
+ var SKIP_TYPES = { submit: 1, button: 1, reset: 1, image: 1, hidden: 1 };
57
+
58
+ // ValidityState flag → (author-message attribute suffix, built-in fallback).
59
+ // Checked in order; the first failing flag decides the message.
60
+ var CONSTRAINTS = [
61
+ ["valueMissing", "required", "This field is required."],
62
+ ["typeMismatch", "type", "Please enter a valid value."],
63
+ ["patternMismatch", "pattern", "Please match the requested format."],
64
+ ["tooShort", "short", "Please lengthen this value."],
65
+ ["tooLong", "long", "Please shorten this value."],
66
+ ["rangeUnderflow", "min", "Value is too small."],
67
+ ["rangeOverflow", "max", "Value is too large."],
68
+ ["stepMismatch", "step", "Please enter a valid value."],
69
+ ["badInput", "input", "Please enter a valid value."]
70
+ ];
71
+
72
+ function isFormField(el) {
73
+ var tag = el.tagName;
74
+ if (tag !== "INPUT" && tag !== "SELECT" && tag !== "TEXTAREA") return false;
75
+ if (el.disabled) return false;
76
+ if (el.hasAttribute("data-validate-ignore")) return false;
77
+ if (tag === "INPUT" && SKIP_TYPES[el.type]) return false;
78
+ return true;
79
+ }
80
+
81
+ function fields(form) {
82
+ var out = [];
83
+ var all = form.querySelectorAll("input, select, textarea");
84
+ for (var i = 0; i < all.length; i++) if (isFormField(all[i])) out.push(all[i]);
85
+ return out;
86
+ }
87
+
88
+ // Suffixes of the custom `l-validate:<name>` validators declared on a control.
89
+ function customNames(el) {
90
+ var names = [];
91
+ for (var i = 0; i < el.attributes.length; i++) {
92
+ var n = el.attributes[i].name;
93
+ if (n.indexOf("l-validate:") === 0) names.push(n.slice(11));
94
+ }
95
+ return names;
96
+ }
97
+
98
+ // Message shown once a control is invalid — see header for the priority order.
99
+ function messageFor(el) {
100
+ var v = el.validity;
101
+ if (v.customError) return el.validationMessage || "This field is invalid.";
102
+ for (var i = 0; i < CONSTRAINTS.length; i++) {
103
+ if (v[CONSTRAINTS[i][0]]) {
104
+ return (
105
+ el.getAttribute("data-error-" + CONSTRAINTS[i][1]) ||
106
+ el.getAttribute("data-error") ||
107
+ el.validationMessage ||
108
+ CONSTRAINTS[i][2]
109
+ );
110
+ }
111
+ }
112
+ return el.validationMessage || "This field is invalid.";
113
+ }
114
+
115
+ function group(el) {
116
+ return el.closest('[data-ui="field-group"]');
117
+ }
118
+
119
+ // Reflect a control's verdict onto itself + its field-group. The plugin owns
120
+ // the group's `[data-part="error"]` text and toggles `data-state` only between
121
+ // "invalid" and cleared (an author's "disabled"/"validating" state is left be).
122
+ function applyState(el, valid, message) {
123
+ var g = group(el);
124
+ var err = g && g.querySelector('[data-part="error"]');
125
+ if (valid) {
126
+ el.removeAttribute("aria-invalid");
127
+ if (g && g.getAttribute("data-state") === "invalid") g.removeAttribute("data-state");
128
+ if (err) {
129
+ err.textContent = "";
130
+ if (err.id && el.getAttribute("aria-describedby") === err.id)
131
+ el.removeAttribute("aria-describedby");
132
+ }
133
+ } else {
134
+ el.setAttribute("aria-invalid", "true");
135
+ if (g) g.setAttribute("data-state", "invalid");
136
+ if (err) {
137
+ err.textContent = message;
138
+ if (err.id && !el.getAttribute("aria-describedby"))
139
+ el.setAttribute("aria-describedby", err.id);
140
+ }
141
+ }
142
+ }
143
+
144
+ function install(Faqir) {
145
+ Faqir.directive("validate", function (form, dir, scope) {
146
+ var submitted = false;
147
+
148
+ // Run a control's custom validators (native constraints already passed).
149
+ // `value` and `$el` are exposed so `l-validate:x="fn(value)"` resolves; the
150
+ // first falsy verdict sets a custom validity message and stops.
151
+ function runCustom(el) {
152
+ var names = customNames(el);
153
+ if (!names.length) return;
154
+ var local = Object.create(scope);
155
+ local.value = "value" in el ? el.value : null;
156
+ local.$el = el;
157
+ for (var i = 0; i < names.length; i++) {
158
+ var ok = !!Faqir.evaluate(el.getAttribute("l-validate:" + names[i]), local, el);
159
+ if (!ok) {
160
+ el.setCustomValidity(
161
+ el.getAttribute("data-error-" + names[i]) ||
162
+ el.getAttribute("data-error") ||
163
+ "This field is invalid."
164
+ );
165
+ return;
166
+ }
167
+ }
168
+ }
169
+
170
+ function validateField(el) {
171
+ el.setCustomValidity(""); // clear any prior custom verdict
172
+ if (el.validity.valid) runCustom(el); // native ok → try custom
173
+ var valid = el.validity.valid;
174
+ applyState(el, valid, valid ? "" : messageFor(el));
175
+ return valid;
176
+ }
177
+
178
+ // Submit: validate everything; block + focus the first offender when dirty.
179
+ form.addEventListener("submit", function (e) {
180
+ submitted = true;
181
+ var list = fields(form),
182
+ firstBad = null;
183
+ for (var i = 0; i < list.length; i++) {
184
+ if (!validateField(list[i]) && !firstBad) firstBad = list[i];
185
+ }
186
+ if (firstBad) {
187
+ e.preventDefault();
188
+ if (typeof firstBad.focus === "function") firstBad.focus();
189
+ return;
190
+ }
191
+ // Clean. A non-empty `l-validate` expression is an on-valid hook (SPA
192
+ // submit): run it and keep the browser from navigating. Otherwise let
193
+ // the native submit proceed.
194
+ if (dir.expression && dir.expression.trim()) {
195
+ e.preventDefault();
196
+ Faqir.evaluateAssignment(dir.expression, scope, form);
197
+ }
198
+ });
199
+
200
+ // After the first submit attempt, revalidate the touched field live.
201
+ function live(e) {
202
+ if (submitted && isFormField(e.target)) validateField(e.target);
203
+ }
204
+ form.addEventListener("blur", live, true);
205
+ form.addEventListener("input", live, true);
206
+ });
207
+ }
208
+
209
+ // CommonJS / bundler export (tests import the installer directly).
210
+ if (typeof module !== "undefined" && module.exports) {
211
+ module.exports = install;
212
+ }
213
+
214
+ // Browser self-registration: attach as soon as a global Faqir exists.
215
+ var F =
216
+ (typeof globalThis !== "undefined" && globalThis.Faqir) ||
217
+ (typeof window !== "undefined" && window.Faqir);
218
+ if (F && typeof F.plugin === "function") {
219
+ F.plugin(install);
220
+ }
221
+ })();
package/dist/sri.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "faqir-core.js": "sha384-vH2gsWr7R/OAJLvYeBC4txN6mleQzdRiuLsc5AH2xbdge5+zQipF5AqSJS7Yc4Vz",
3
+ "faqir-core.min.js": "sha384-BHt1gSursbCylX/U3FT6iW5dWukyZcjDXKWOanw4baYgwn8qj1LIpI1It8ct7CyK",
4
+ "faqir-core.min.js.map": "sha384-io5FkzZ1meZXs3UY/kEJkmvB8YXwO/JfezQe4yZop6KZZjjwn6UCjeDUSh0FG1sz",
5
+ "faqir-core.mjs": "sha384-jqsZ/HqqXSuUjt6SikvDjynm7EdDVAfVLXYSoJHjzvOF+3V5CDmEZgzXDJPWvQ4I",
6
+ "faqir.aurora.css": "sha384-M7yDwTC7MuWfGfiIFLv9MFibCkYDDSHx6RFszEQ7iyd4Lp/bEG/SIyRcvSsfn2Vv",
7
+ "faqir.brutalist.css": "sha384-aRtTqpJetmoMa6uL2krMPEHsjTwF11oNlTzS9zZrdtqXSvzkSUCKKoHyr1WbCBnb",
8
+ "faqir.contrast.css": "sha384-Fy3tZU7cwb7LGNm9B/5nFNK1yRgwAZqv+FF69pY/w6AEGykZA4jSoDeFKU4hXdBf",
9
+ "faqir.default.css": "sha384-w2sH1K7AwYMeLQIhxBnAU1LuRrexg/xJx/eLLMgAjG7avxLYhYeuvM2pTu1yJJVi",
10
+ "faqir.document-serif.css": "sha384-2xWhB1DahXndhbbk7D+RV6w1NI7lPoO3tPgVtDvgWp8G+qE4jjI5nCWZa0IC1XiV",
11
+ "faqir.document.css": "sha384-loA3uWvclgLBd8OntpB4YrQP/jBsLScb06KI61EMauFjYxOadmCszUBkYYNoo4Wc",
12
+ "faqir.glass.css": "sha384-OQOzZy8BxLxPl1pFNkCwizX0oqOsmPf49FKhFa4d9HUM6bcJrJnzw3XU8IZGqSkG",
13
+ "faqir.midnight.css": "sha384-J0SQyAklYwD+yJ7jubtIJ+0t9DWBymcJikhI242PpTvzAL9SU4iWTYCwJBqSUbj0",
14
+ "faqir.paper.css": "sha384-OCmkztWvVBgacdb9HlGiRl+RRQ1QaFU8MCrQIYLeL17EMsT2tG8lZeU0mfQKLo6N",
15
+ "faqir.slate.css": "sha384-BxHSyxsLdi690Q1LBWIyAXodugjLNT764ZjqBRAaCtAaQeei81qzV46C5rMYonb4",
16
+ "faqir.soft.css": "sha384-a77S0Tcfd1I2V+r6Prt4gTtIzSZahltCuQ0jEzOKXUd+taDRjM0EsIy3n5VycLJT",
17
+ "faqir.terminal.css": "sha384-YvP4CkNUhcsFYJjBgnhcSMthsEheRg2RlZ0837GBzDVfmeJx9IYpMSrG7BkAb697",
18
+ "plugins/faqir-collapse.js": "sha384-y16PnFkcJiJLQIxuQMtdS+QalrC6lJ0EsPT/oceZWpLMGdqASXq3HhVHebfUWktm",
19
+ "plugins/faqir-intersect.js": "sha384-AfbB6icyS1ngl1JdiAMZ9//m5lHTMdgXXHWCdi5INfE9FmLbYRUeiyKTv4E4g6ri",
20
+ "plugins/faqir-mask.js": "sha384-cSq2DpUnoJCxEDAQjsbzAa+jSuHf4WsOaKT8t7b9p8pA7MaaRhA1DLzjMNChSa/I",
21
+ "plugins/faqir-persist.js": "sha384-o0jfddebC8QZ3S26VXUlOme723BNMlb/N7vqpn+fWpSeEFXzpq+3MoeXhVL/0PSv",
22
+ "plugins/faqir-validate.js": "sha384-DSzQ7I94Qm+VKjpgBo3qgALyuU3zgQZ+jXOIqgRHkJ6utr8nqF7tQMKWnOv1/YH2"
23
+ }
@@ -0,0 +1,49 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="light">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Faqir UI — two-tag CDN demo</title>
7
+
8
+ <!-- The two tags. Local dist paths (swap for the jsDelivr URLs in production). -->
9
+ <link rel="stylesheet" href="../dist/faqir.default.css">
10
+ <script src="../dist/faqir-core.min.js" defer></script>
11
+ </head>
12
+ <body>
13
+ <main l-data="{ count: 0 }" data-ui="stack" data-gap="lg"
14
+ style="max-width: 40rem; margin: 3rem auto; padding: 0 1rem;">
15
+
16
+ <div data-ui="card">
17
+ <div data-part="header">
18
+ <h1 data-part="title">Faqir UI is running</h1>
19
+ <p data-part="description">
20
+ Styled by <code>faqir.default.css</code>, made interactive by
21
+ <code>faqir-core.min.js</code> — no build step, no install.
22
+ </p>
23
+ </div>
24
+
25
+ <div data-part="body" data-ui="stack" data-gap="md">
26
+ <p>
27
+ Clicked <span data-ui="badge" data-variant="primary" l-text="count">0</span> times.
28
+ </p>
29
+ <div data-ui="button-group">
30
+ <button data-ui="button" data-variant="primary" l-on:click="count++">Increment</button>
31
+ <button data-ui="button" data-variant="outline" l-on:click="count = 0">Reset</button>
32
+ </div>
33
+ <p data-ui="callout" data-variant="success" l-show="count >= 5">
34
+ 🎉 Reactivity works — this callout appears once you reach 5.
35
+ </p>
36
+ </div>
37
+
38
+ <div data-part="footer">
39
+ <button data-ui="button" data-variant="ghost"
40
+ l-on:click="document.documentElement.dataset.theme =
41
+ document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'">
42
+ Toggle dark mode
43
+ </button>
44
+ </div>
45
+ </div>
46
+
47
+ </main>
48
+ </body>
49
+ </html>