@faqir-ui/core 1.0.0 → 1.1.1

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.
Files changed (41) hide show
  1. package/README.md +45 -5
  2. package/cdn.json +38 -22
  3. package/dist/faqir-core.js +121 -57
  4. package/dist/faqir-core.min.js +3 -3
  5. package/dist/faqir-core.min.js.map +3 -3
  6. package/dist/faqir-core.mjs +75 -59
  7. package/dist/faqir.aurora.css +1209 -507
  8. package/dist/faqir.brutalist.css +1184 -489
  9. package/dist/faqir.candy.css +14152 -0
  10. package/dist/faqir.clinical.css +14152 -0
  11. package/dist/faqir.contrast.css +1198 -526
  12. package/dist/faqir.default.css +1261 -588
  13. package/dist/faqir.document-serif.css +1211 -491
  14. package/dist/faqir.document.css +1193 -490
  15. package/dist/faqir.editorial-document.css +14116 -0
  16. package/dist/faqir.editorial.css +14152 -0
  17. package/dist/faqir.fintech.css +14152 -0
  18. package/dist/faqir.glass.css +1214 -517
  19. package/dist/faqir.ink-document.css +14116 -0
  20. package/dist/faqir.ink.css +14127 -0
  21. package/dist/faqir.luxe.css +14211 -0
  22. package/dist/faqir.midnight.css +1229 -507
  23. package/dist/faqir.neo.css +14153 -0
  24. package/dist/faqir.neumorph.css +14151 -0
  25. package/dist/faqir.nordic.css +14151 -0
  26. package/dist/faqir.organic.css +14152 -0
  27. package/dist/faqir.paper.css +1200 -508
  28. package/dist/faqir.slate.css +1296 -564
  29. package/dist/faqir.soft.css +1180 -494
  30. package/dist/faqir.sunset.css +14153 -0
  31. package/dist/faqir.swiss-document.css +14116 -0
  32. package/dist/faqir.swiss.css +14153 -0
  33. package/dist/faqir.terminal.css +1187 -491
  34. package/dist/plugins/faqir-collapse.js +1 -1
  35. package/dist/plugins/faqir-intersect.js +2 -0
  36. package/dist/plugins/faqir-mask.js +27 -17
  37. package/dist/plugins/faqir-rules.js +63 -0
  38. package/dist/plugins/faqir-validate.js +471 -58
  39. package/dist/sri.json +36 -20
  40. package/faqir-core.d.ts +119 -6
  41. package/package.json +1 -1
package/README.md CHANGED
@@ -12,10 +12,10 @@ any HTML file and you have a styled, interactive Faqir page. No build step, no i
12
12
 
13
13
  ```html
14
14
  <!-- 1. A theme's full CSS bundle (tokens + theme + base + every component) -->
15
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@faqir-ui/core@0.2/dist/faqir.default.css">
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/faqir.default.css">
16
16
 
17
17
  <!-- 2. The reactive engine — sets window.Faqir and auto-boots on DOMContentLoaded -->
18
- <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@0.2/dist/faqir-core.min.js" defer></script>
18
+ <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/faqir-core.min.js" defer></script>
19
19
  ```
20
20
 
21
21
  That's it. Now write Faqir markup:
@@ -38,10 +38,10 @@ for tamper-proof loads:
38
38
 
39
39
  ```html
40
40
  <link rel="stylesheet"
41
- href="https://cdn.jsdelivr.net/npm/@faqir-ui/core@0.2/dist/faqir.default.css"
41
+ href="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/faqir.default.css"
42
42
  integrity="sha384-…paste from sri.json…"
43
43
  crossorigin="anonymous">
44
- <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@0.2/dist/faqir-core.min.js"
44
+ <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/faqir-core.min.js"
45
45
  integrity="sha384-…paste from sri.json…"
46
46
  crossorigin="anonymous" defer></script>
47
47
  ```
@@ -54,9 +54,49 @@ for tamper-proof loads:
54
54
  | `faqir-core.js` | Canonical UMD engine — what `<script src>` and the CDN load. Reachable explicitly at `@faqir-ui/core/faqir-core.js`. |
55
55
  | `faqir-core.min.js` (+ `.map`) | Minified classic-script build; a plain `<script src>` sets `window.Faqir`. |
56
56
  | `faqir.{theme}.css` | Full CSS bundle per theme: all tokens + theme + base + every component. Self-contained (no `@import`). |
57
- | `plugins/` | Official self-registering drops: `faqir-persist`, `faqir-intersect`, `faqir-collapse`, and `faqir-validate`. |
57
+ | `plugins/` | Official self-registering drops — see [Plugins](#plugins) below. |
58
58
  | `sri.json` | SHA-384 integrity hashes for every file above. |
59
59
 
60
+ ## Plugins
61
+
62
+ Each file in `dist/plugins/` is a self-registering classic script: load it
63
+ **after** the engine and it installs itself. Every one is dependency-free.
64
+
65
+ | Plugin | Provides | What it does |
66
+ |---|---|---|
67
+ | `faqir-collapse.js` | `l-collapse` | height auto-animation for a boolean expression |
68
+ | `faqir-intersect.js` | `l-intersect` | declarative IntersectionObserver hooks |
69
+ | `faqir-mask.js` | `l-mask` | caret-safe input masking |
70
+ | `faqir-persist.js` | `l-persist`, `$persist()` | localStorage-backed reactive state |
71
+ | `faqir-rules.js` | `l-rules`, `$rules` | a form's conditional logic, from one JSON definition |
72
+ | `faqir-validate.js` | `l-validate` | declarative + programmatic form validation |
73
+
74
+ ```html
75
+ <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/faqir-core.min.js" defer></script>
76
+ <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/plugins/faqir-validate.js" defer></script>
77
+ <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/plugins/faqir-rules.js" defer></script>
78
+ ```
79
+
80
+ One pairing to know: `faqir-rules` registers its cross-field and remote checks
81
+ through `Faqir.validate.register`, so a definition carrying `validate` rules
82
+ needs `faqir-validate` on the page too. That is a presence requirement, not a
83
+ script order — every plugin installs before the engine boots — and a page
84
+ missing one is told so rather than quietly skipping the checks.
85
+
86
+ Pin the plugin drops the same way as the engine — their hashes are in
87
+ `dist/sri.json` beside the rest:
88
+
89
+ ```html
90
+ <script src="https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1/dist/plugins/faqir-rules.js"
91
+ integrity="sha384-…paste from sri.json…"
92
+ crossorigin="anonymous" defer></script>
93
+ ```
94
+
95
+ `faqir-rules` is the browser half of
96
+ [`@faqir-ui/rules`](https://www.npmjs.com/package/@faqir-ui/rules) — the same
97
+ evaluator, bundled in, so the verdict in the page is the verdict on your server
98
+ by construction. Install that package to re-check a submission server-side.
99
+
60
100
  ## Module usage
61
101
 
62
102
  The package is **ESM-only**. The default export is the engine; every member is
package/cdn.json CHANGED
@@ -1,28 +1,44 @@
1
1
  {
2
2
  "package": "@faqir-ui/core",
3
- "version": "1.0.0",
4
- "base": "https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.0.0/dist/",
3
+ "version": "1.1.1",
4
+ "base": "https://cdn.jsdelivr.net/npm/@faqir-ui/core@1.1.1/dist/",
5
5
  "integrity": {
6
- "faqir-core.js": "sha384-vH2gsWr7R/OAJLvYeBC4txN6mleQzdRiuLsc5AH2xbdge5+zQipF5AqSJS7Yc4Vz",
7
- "faqir-core.min.js": "sha384-BHt1gSursbCylX/U3FT6iW5dWukyZcjDXKWOanw4baYgwn8qj1LIpI1It8ct7CyK",
8
- "faqir-core.min.js.map": "sha384-io5FkzZ1meZXs3UY/kEJkmvB8YXwO/JfezQe4yZop6KZZjjwn6UCjeDUSh0FG1sz",
9
- "faqir-core.mjs": "sha384-jqsZ/HqqXSuUjt6SikvDjynm7EdDVAfVLXYSoJHjzvOF+3V5CDmEZgzXDJPWvQ4I",
10
- "faqir.aurora.css": "sha384-M7yDwTC7MuWfGfiIFLv9MFibCkYDDSHx6RFszEQ7iyd4Lp/bEG/SIyRcvSsfn2Vv",
11
- "faqir.brutalist.css": "sha384-aRtTqpJetmoMa6uL2krMPEHsjTwF11oNlTzS9zZrdtqXSvzkSUCKKoHyr1WbCBnb",
12
- "faqir.contrast.css": "sha384-Fy3tZU7cwb7LGNm9B/5nFNK1yRgwAZqv+FF69pY/w6AEGykZA4jSoDeFKU4hXdBf",
13
- "faqir.default.css": "sha384-w2sH1K7AwYMeLQIhxBnAU1LuRrexg/xJx/eLLMgAjG7avxLYhYeuvM2pTu1yJJVi",
14
- "faqir.document-serif.css": "sha384-2xWhB1DahXndhbbk7D+RV6w1NI7lPoO3tPgVtDvgWp8G+qE4jjI5nCWZa0IC1XiV",
15
- "faqir.document.css": "sha384-loA3uWvclgLBd8OntpB4YrQP/jBsLScb06KI61EMauFjYxOadmCszUBkYYNoo4Wc",
16
- "faqir.glass.css": "sha384-OQOzZy8BxLxPl1pFNkCwizX0oqOsmPf49FKhFa4d9HUM6bcJrJnzw3XU8IZGqSkG",
17
- "faqir.midnight.css": "sha384-J0SQyAklYwD+yJ7jubtIJ+0t9DWBymcJikhI242PpTvzAL9SU4iWTYCwJBqSUbj0",
18
- "faqir.paper.css": "sha384-OCmkztWvVBgacdb9HlGiRl+RRQ1QaFU8MCrQIYLeL17EMsT2tG8lZeU0mfQKLo6N",
19
- "faqir.slate.css": "sha384-BxHSyxsLdi690Q1LBWIyAXodugjLNT764ZjqBRAaCtAaQeei81qzV46C5rMYonb4",
20
- "faqir.soft.css": "sha384-a77S0Tcfd1I2V+r6Prt4gTtIzSZahltCuQ0jEzOKXUd+taDRjM0EsIy3n5VycLJT",
21
- "faqir.terminal.css": "sha384-YvP4CkNUhcsFYJjBgnhcSMthsEheRg2RlZ0837GBzDVfmeJx9IYpMSrG7BkAb697",
22
- "plugins/faqir-collapse.js": "sha384-y16PnFkcJiJLQIxuQMtdS+QalrC6lJ0EsPT/oceZWpLMGdqASXq3HhVHebfUWktm",
23
- "plugins/faqir-intersect.js": "sha384-AfbB6icyS1ngl1JdiAMZ9//m5lHTMdgXXHWCdi5INfE9FmLbYRUeiyKTv4E4g6ri",
24
- "plugins/faqir-mask.js": "sha384-cSq2DpUnoJCxEDAQjsbzAa+jSuHf4WsOaKT8t7b9p8pA7MaaRhA1DLzjMNChSa/I",
6
+ "faqir-core.js": "sha384-ypj/JvdM2DNbSf7TIOUBlbV3kKw3JE0YuKDPkDuroqFt8H/jB2O80j0rUa/s8N9l",
7
+ "faqir-core.min.js": "sha384-+0VjcV+UV4XDD6Y5YZ+yJG9qiwlcZRr+LgT6uVspQgw4ynLLzyNKrDc6HDvo6uiM",
8
+ "faqir-core.min.js.map": "sha384-3NMITzrdJEWU6VH6d64NSzEhMGdilUMAqm7tG37R9mq1oQcbiifI7azkysTVSK/a",
9
+ "faqir-core.mjs": "sha384-bFYiYI3mnO3fluPZh14FGac4ZgBXPEfENyv+/ewSMHNyataamJyDJYENbGaXnV8t",
10
+ "faqir.aurora.css": "sha384-TrPBHGXGxwY6sLypt3kbjbhEHz87wsYgqQhzPqi5zghmsiqewgykQx6aS+WP2O0h",
11
+ "faqir.brutalist.css": "sha384-N/VfEAEdOZ2PGFX4xsPzlHEmDkXtjzmkXXW1Ta3PCtBV/RZm1rF4EYsfwLEvwqPZ",
12
+ "faqir.candy.css": "sha384-/lk9HAr46O2pYwsM8vE8MBVkPLB1t2otA3Edfux98b564DvNkWqpsD4DSVdcmHAX",
13
+ "faqir.clinical.css": "sha384-quTFS0lcUB0sz5ysan8vVX75oBDNzY+of7CmKDEoX5fM5OCIbYEttiZ8eLL/vQ4u",
14
+ "faqir.contrast.css": "sha384-Kkc5S9mDWhATTvHHfZhNd+rN06hSCvbKLuqa5y+cwtvlF7jSunMH1bLkfrZsVw5f",
15
+ "faqir.default.css": "sha384-bbc2p6aV7UYxrmv8M6x0buppf4uT7yh8DZ9d///ykZjXQokcZNtOk6xK1UUl6Mnj",
16
+ "faqir.document-serif.css": "sha384-gruBhX1KhlPRKzCVMODRBoOB+IpJM3lgYB+xQgCbu35onXyy+OFvh0MBDThrRFSq",
17
+ "faqir.document.css": "sha384-oC4VnHJi2ez8nAPSBwN4GWOz+BraAWcXXcGjNrQmdKujuA3fVmawKwolH1zIEzT0",
18
+ "faqir.editorial-document.css": "sha384-sKWLbBQ4IvgSvhLYo0nE95l6pqku+T0VTmkkoTwg/YOx5vM9gBAPA7f6CLE8gLNa",
19
+ "faqir.editorial.css": "sha384-N5sYSpAAOB81nDI+eShZ+gTZ5zbTMwaUt9fIPxwiNWBkKdaEJnsvyP3na/2SyNbv",
20
+ "faqir.fintech.css": "sha384-xY6baIhCB0boozwIAPzhPg2FFZ8RhqSf/13CWeda9Bd+9D5bvZbNuiFcO67GGLOL",
21
+ "faqir.glass.css": "sha384-MW9qUMfNSCgUyQjY/UqCpRxsXxuDEoVFvvpLy2htmV1Vc2mtn/z7uCXscj6Yn5zq",
22
+ "faqir.ink-document.css": "sha384-uWhbYEZwSDk3QHPMWe7lzYM0HELolHkW8xyqoowVvXrToqTXnSYVw7zTVET3kTVC",
23
+ "faqir.ink.css": "sha384-57zpUenZ+vVG/PViYuf1yxB0VxdV/5hxhZbKy9vi8XawKTMpbbU7AtnvgZ81ZIvs",
24
+ "faqir.luxe.css": "sha384-AP+OFzUaZ0RNEOGS95dsf09tD8ltts0VslqgCyaJ3CBHHkesKJ+/MT3SA4gZPxbe",
25
+ "faqir.midnight.css": "sha384-xsSMTT7Pf9i0CDFfx5hlzCBkHx5gsfORYS3rZuKnasgMkHumNUsy0fTBFcHBxSo3",
26
+ "faqir.neo.css": "sha384-rNHNMTeReWOmjhAayIam2CSmH3cUpHI3VbCOxg1hhk0GE3pAB2GX6tqzXH8OU9kX",
27
+ "faqir.neumorph.css": "sha384-7N6RHoYWSlnTHeTwVWrCn4ey1KR62fsAS0p1QVyb0gcljIYhcMWADZrmH/hOjhx+",
28
+ "faqir.nordic.css": "sha384-A/O40XHz2mMUs3tKWT7usnwgAxSsdFpb8uXKNMtoIa3OBaR2Ibpqg5mGNov90PaT",
29
+ "faqir.organic.css": "sha384-acTW5ka4Zy5SXHHn1My078WAnPkj6/KbLHKuWzToArB8rWlwZGRqlFxfq8VNAaqY",
30
+ "faqir.paper.css": "sha384-O26GS1gp6lFBZmollq/g21aUGVoWixAfBEsFxhWzPif1X2ZrjeXyIOEmCn25R1Dz",
31
+ "faqir.slate.css": "sha384-lSeSUhJCMUsQxM1oS2AbeqbfdLzquz4zYDYSF1N2bpA4JC0g/7fcJA/lWvkZugyq",
32
+ "faqir.soft.css": "sha384-G7xaO/Rr9RdMdBch66kUiS+atQOluVXhwIb039G7WJg6KvAxDBSbA/7v8y9ouhZH",
33
+ "faqir.sunset.css": "sha384-7FyjK1RJrkGTvOSFLH15WiRI1i8V3bbfpUuv4Gq3stG1NwC56/JkXOH4uPscY95b",
34
+ "faqir.swiss-document.css": "sha384-hQKtiQvaCu9ot5TJnCIlPjZhUJif5cacuE27IvJi1j99iF9HuNOvfGp+epSzJlMT",
35
+ "faqir.swiss.css": "sha384-Ki39mfAWeHD1KRjAB07b4MygOk5swzXSp5sUKcTWldXuiNhpNTWo+vOeponF4yfB",
36
+ "faqir.terminal.css": "sha384-EE2EAqi6sG4GTe8l+7w0p8gqCjZu8A+dkfNFCx90Ue/loSNUSnLIYDqdAsApyOOR",
37
+ "plugins/faqir-collapse.js": "sha384-3l5Z+isXlDkidRevDGkuNOiDp6eJ2ML11RkXA0j4ixQBaepuu8pAhyY4YRnR1qB7",
38
+ "plugins/faqir-intersect.js": "sha384-Q/akZjAcnTnGYPKBP7b8c3iyYbHTIW1IPjBivzOH9dE7Nflr9Cu6adjeDXU2bhk6",
39
+ "plugins/faqir-mask.js": "sha384-7yA/VdjGdg/6vwX+zUbughO6ERyV2rEsVyNYBO5ghqJbAhj9912F9qMPTkLc55B6",
25
40
  "plugins/faqir-persist.js": "sha384-o0jfddebC8QZ3S26VXUlOme723BNMlb/N7vqpn+fWpSeEFXzpq+3MoeXhVL/0PSv",
26
- "plugins/faqir-validate.js": "sha384-DSzQ7I94Qm+VKjpgBo3qgALyuU3zgQZ+jXOIqgRHkJ6utr8nqF7tQMKWnOv1/YH2"
41
+ "plugins/faqir-rules.js": "sha384-xE90r5W3AlWKu+idcwx6NAkVUcRWMgM9ZJ1VgJLOguj/0GYP0jfflCojiCWt4FVq",
42
+ "plugins/faqir-validate.js": "sha384-TIyGNL8dHBnFdV56LAhdklvhQLsClQqGuQ7DT0Zgxva8DIzfONXZSJYTXhQxJcN0"
27
43
  }
28
44
  }
@@ -10,11 +10,11 @@
10
10
  // PRODUCTION BUILD — dev-only diagnostics stripped at assembly time.
11
11
  // Load faqir-core.dev.js instead while developing for full warnings.
12
12
  // Regenerate with: bun run build:core
13
- // Package version: 1.0.0
13
+ // Package version: 1.1.1
14
14
  // ============================================================================
15
15
 
16
16
  /**
17
- * Faqir Core v0.1.0
17
+ * Faqir Core — the running version is `Faqir.version` (stamped at build time).
18
18
  * Alpine-style reactivity for the Faqir UI component system.
19
19
  * Zero dependencies. CDN-ready. Agent-native.
20
20
  *
@@ -176,7 +176,11 @@
176
176
  flushScheduled = false;
177
177
  var iterations = 0;
178
178
  while (pendingEffects.size > 0) {
179
+ // Effects that keep re-triggering each other (a writes b, b writes a)
180
+ // would loop forever. The cap stops that, but dropping work silently
181
+ // hides the bug, so it is reported in every build.
179
182
  if (++iterations > 100) {
183
+ console.warn('[Faqir] Effect loop: dropped ' + pendingEffects.size + ' effect(s) after 100 flushes; effects keep re-triggering each other');
180
184
  pendingEffects.clear();
181
185
  break;
182
186
  }
@@ -917,6 +921,16 @@
917
921
  function signalOf(ac) {
918
922
  return ac ? ac.signal : undefined;
919
923
  }
924
+ // Where a row is NOW — `row` by identity, else the row whose idKey is `id`.
925
+ // Never an index saved before the request: an interleaved load()/remove()
926
+ // shifts or replaces the list, and a stale index writes over a real row.
927
+ function locate(id, row) {
928
+ var items = scope[name];
929
+ for (var i = 0; i < items.length; i++) {
930
+ if (row ? items[i] === row : items[i] && items[i][idKey] === id) return i;
931
+ }
932
+ return -1;
933
+ }
920
934
 
921
935
  // Inject reactive data properties into scope
922
936
  scope[name] = [];
@@ -959,12 +973,12 @@
959
973
  create: function(payload) {
960
974
  if (destroyed) return Promise.resolve(null);
961
975
  scope[name + 'Error'] = null;
962
- var tempIndex = -1;
976
+ var temp = null;
963
977
 
964
978
  if (isOptimistic) {
965
- var temp = Object.assign({}, payload, { _pending: true });
966
- scope[name].push(temp);
967
- tempIndex = scope[name].length - 1;
979
+ scope[name].push(Object.assign({}, payload, { _pending: true }));
980
+ // Read back through the list: that is the handle `locate` compares.
981
+ temp = scope[name][scope[name].length - 1];
968
982
  }
969
983
 
970
984
  var ac = beginRequest();
@@ -981,9 +995,12 @@
981
995
  .then(function(created) {
982
996
  endRequest(ac);
983
997
  if (destroyed) return null;
984
- if (isOptimistic && tempIndex >= 0) {
985
- scope[name][tempIndex] = created;
986
- } else {
998
+ var at = temp ? locate(null, temp) : -1;
999
+ if (at >= 0) {
1000
+ scope[name][at] = created;
1001
+ } else if (!temp || !created || created[idKey] == null || locate(created[idKey]) < 0) {
1002
+ // The temp row is gone when a load() replaced the list meanwhile;
1003
+ // that list may already hold the created row.
987
1004
  scope[name].push(created);
988
1005
  }
989
1006
  return created;
@@ -992,9 +1009,8 @@
992
1009
  endRequest(ac);
993
1010
  if (destroyed) return null;
994
1011
  scope[name + 'Error'] = e.message;
995
- if (isOptimistic && tempIndex >= 0) {
996
- scope[name].splice(tempIndex, 1);
997
- }
1012
+ var at = temp ? locate(null, temp) : -1;
1013
+ if (at >= 0) scope[name].splice(at, 1);
998
1014
  return null;
999
1015
  });
1000
1016
  },
@@ -1003,10 +1019,7 @@
1003
1019
  if (destroyed) return Promise.resolve(null);
1004
1020
  scope[name + 'Error'] = null;
1005
1021
  var items = scope[name];
1006
- var idx = -1;
1007
- for (var i = 0; i < items.length; i++) {
1008
- if (items[i][idKey] === id) { idx = i; break; }
1009
- }
1022
+ var idx = locate(id);
1010
1023
  var snapshot = null;
1011
1024
 
1012
1025
  if (isOptimistic && idx >= 0) {
@@ -1015,7 +1028,7 @@
1015
1028
  }
1016
1029
 
1017
1030
  var ac = beginRequest();
1018
- return fetch(endpoint + '/' + id, {
1031
+ return fetch(endpoint + '/' + encodeURIComponent(id), {
1019
1032
  method: 'PATCH',
1020
1033
  headers: { 'Content-Type': 'application/json' },
1021
1034
  body: JSON.stringify(payload),
@@ -1028,16 +1041,16 @@
1028
1041
  .then(function(updated) {
1029
1042
  endRequest(ac);
1030
1043
  if (destroyed) return null;
1031
- if (idx >= 0) scope[name][idx] = updated;
1044
+ var at = locate(id);
1045
+ if (at >= 0) scope[name][at] = updated;
1032
1046
  return updated;
1033
1047
  })
1034
1048
  .catch(function(e) {
1035
1049
  endRequest(ac);
1036
1050
  if (destroyed) return null;
1037
1051
  scope[name + 'Error'] = e.message;
1038
- if (isOptimistic && snapshot && idx >= 0) {
1039
- scope[name][idx] = snapshot;
1040
- }
1052
+ var at = snapshot ? locate(id) : -1;
1053
+ if (at >= 0) scope[name][at] = snapshot;
1041
1054
  return null;
1042
1055
  });
1043
1056
  },
@@ -1046,10 +1059,7 @@
1046
1059
  if (destroyed) return Promise.resolve();
1047
1060
  scope[name + 'Error'] = null;
1048
1061
  var items = scope[name];
1049
- var idx = -1;
1050
- for (var i = 0; i < items.length; i++) {
1051
- if (items[i][idKey] === id) { idx = i; break; }
1052
- }
1062
+ var idx = locate(id);
1053
1063
  var snapshot = null;
1054
1064
 
1055
1065
  if (isOptimistic && idx >= 0) {
@@ -1058,20 +1068,20 @@
1058
1068
  }
1059
1069
 
1060
1070
  var ac = beginRequest();
1061
- return fetch(endpoint + '/' + id, { method: 'DELETE', signal: signalOf(ac) })
1071
+ return fetch(endpoint + '/' + encodeURIComponent(id), { method: 'DELETE', signal: signalOf(ac) })
1062
1072
  .then(function(res) {
1063
1073
  if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
1064
1074
  endRequest(ac);
1065
1075
  if (destroyed) return;
1066
- if (!isOptimistic && idx >= 0) {
1067
- scope[name].splice(idx, 1);
1068
- }
1076
+ var at = isOptimistic ? -1 : locate(id);
1077
+ if (at >= 0) scope[name].splice(at, 1);
1069
1078
  })
1070
1079
  .catch(function(e) {
1071
1080
  endRequest(ac);
1072
1081
  if (destroyed) return;
1073
1082
  scope[name + 'Error'] = e.message;
1074
- if (isOptimistic && snapshot) {
1083
+ // `idx` is only a position hint here; skip if a reload restored it.
1084
+ if (snapshot && locate(id) < 0) {
1075
1085
  scope[name].splice(idx, 0, snapshot);
1076
1086
  }
1077
1087
  });
@@ -1600,14 +1610,11 @@
1600
1610
  });
1601
1611
 
1602
1612
  var inputHandler = function() {
1613
+ // Trim first: `.number.trim` must not call trim() on the parsed number.
1603
1614
  var value = el.value;
1604
- if (modifiers.has('number')) value = parseFloat(value) || 0;
1605
1615
  if (modifiers.has('trim')) value = value.trim();
1606
- if (typeof value === 'number') {
1607
- writeModel(prop, value, scope, el);
1608
- } else {
1609
- writeModel(prop, value, scope, el);
1610
- }
1616
+ if (modifiers.has('number')) value = parseFloat(value) || 0;
1617
+ writeModel(prop, value, scope, el);
1611
1618
  };
1612
1619
 
1613
1620
  if (modifiers.has('debounce')) {
@@ -1904,12 +1911,17 @@
1904
1911
  // rebinds per-row DOM state to the wrong items. Once per list; keyed lists
1905
1912
  // never reach here. Production neither warns nor snapshots the list.
1906
1913
 
1907
- // old key -> entry, consumed as matched so duplicate keys fall through to
1908
- // fresh nodes and leftovers are stale. source[i] = reused entry's old
1909
- // position + 1, or 0 for a fresh entry (the getSequence sentinel).
1914
+ // old key -> entries holding it, in order, consumed as matched; the
1915
+ // leftovers are stale. A list rather than one entry because keys can
1916
+ // repeat: a Map of single entries overwrote the first row of a duplicate
1917
+ // pair, which then was neither reused nor removed and leaked a row per
1918
+ // render. source[i] = reused entry's old position + 1, or 0 for a fresh
1919
+ // entry (the getSequence sentinel).
1910
1920
  var oldMap = new Map();
1911
1921
  for (var i = 0; i < currentEntries.length; i++) {
1912
- oldMap.set(currentEntries[i].key, currentEntries[i]);
1922
+ var k = currentEntries[i].key;
1923
+ if (oldMap.has(k)) oldMap.get(k).push(currentEntries[i]);
1924
+ else oldMap.set(k, [currentEntries[i]]);
1913
1925
  }
1914
1926
 
1915
1927
  var n = items.length;
@@ -1917,9 +1929,9 @@
1917
1929
  var source = new Array(n);
1918
1930
  for (var i = 0; i < n; i++) {
1919
1931
  var key = keyFor(items[i], i);
1920
- var entry = oldMap.get(key);
1932
+ var olds = oldMap.get(key);
1933
+ var entry = olds && olds.shift();
1921
1934
  if (entry !== undefined) {
1922
- oldMap.delete(key);
1923
1935
  // Reuse in place: one write per slot, re-renders only on change.
1924
1936
  entry.scope[itemName] = items[i];
1925
1937
  entry.scope[indexName] = i;
@@ -1932,10 +1944,12 @@
1932
1944
  }
1933
1945
 
1934
1946
  // Remove stale entries first so the DOM holds only reused nodes.
1935
- oldMap.forEach(function(stale) {
1936
- for (var j = 0; j < stale.nodes.length; j++) {
1937
- destroyScope(stale.nodes[j]);
1938
- stale.nodes[j].remove();
1947
+ oldMap.forEach(function(olds) {
1948
+ for (var e = 0; e < olds.length; e++) {
1949
+ for (var j = 0; j < olds[e].nodes.length; j++) {
1950
+ destroyScope(olds[e].nodes[j]);
1951
+ olds[e].nodes[j].remove();
1952
+ }
1939
1953
  }
1940
1954
  });
1941
1955
 
@@ -4400,13 +4414,17 @@ function createDialog(root) {
4400
4414
  let cancelExitWait = null;
4401
4415
 
4402
4416
  function open() {
4417
+ // A second open() (trigger plus an external trigger, say) must not stack
4418
+ // another focus trap or overwrite the focus to restore on close.
4419
+ if (root.dataset.state === "open" && focusCleanup) return;
4403
4420
  cancelExitWait?.();
4404
4421
  cancelExitWait = null;
4405
- previouslyFocused = document.activeElement;
4422
+ // Re-opened mid-exit: the trap and the focus to restore are still live.
4423
+ if (!focusCleanup) previouslyFocused = document.activeElement;
4406
4424
  root.dataset.state = "open";
4407
4425
  overlay.hidden = false;
4408
4426
  panel.hidden = false;
4409
- focusCleanup = trapFocus(panel);
4427
+ if (!focusCleanup) focusCleanup = trapFocus(panel);
4410
4428
  focusInitial();
4411
4429
  }
4412
4430
 
@@ -10455,7 +10473,8 @@ function createToastContainer(root) {
10455
10473
  */
10456
10474
  function dismiss(id) {
10457
10475
  const entry = toasts.get(id);
10458
- if (!entry) return;
10476
+ // Already exiting (close clicked while the timer fires, say): one exit wait.
10477
+ if (!entry || entry.cancelExitWait) return;
10459
10478
 
10460
10479
  const { el, timer, closeBtn, onCloseClick, actionBtn, onActionClick } = entry;
10461
10480
 
@@ -11541,7 +11560,18 @@ function createTreeView(root) {
11541
11560
  * Diagnostics recorded so far, oldest first. Always an array: the
11542
11561
  * production engine records nothing, so it is always empty there.
11543
11562
  */
11544
- warnings: function() { return devHooks ? devHooks.warnings() : []; }
11563
+ warnings: function() { return devHooks ? devHooks.warnings() : []; },
11564
+ /**
11565
+ * Record one diagnostic on a plugin's behalf. The dev build prints it and
11566
+ * keeps it in `warnings()` (deduped like every other); production does
11567
+ * nothing and answers `false`. A plugin is a separate file with no reach
11568
+ * into this seam's other side, so without it a plugin can only be silent
11569
+ * or noisy in a shipped page — and "silent" is what the contract forbids.
11570
+ * [1.1B-03]
11571
+ */
11572
+ report: function(message, el) {
11573
+ return false;
11574
+ }
11545
11575
  };
11546
11576
 
11547
11577
 
@@ -11602,7 +11632,7 @@ function createTreeView(root) {
11602
11632
  // to call `start()` again. [W3-1]
11603
11633
  if (rootEl.__faqirScope) {
11604
11634
  processed.add(rootEl);
11605
- var seen = rootEl.querySelectorAll('[l-data]');
11635
+ var seen = rootEl.querySelectorAll('[l-data], [data-ui]');
11606
11636
  for (var sd = 0; sd < seen.length; sd++) processed.add(seen[sd]);
11607
11637
  continue;
11608
11638
  }
@@ -11634,7 +11664,13 @@ function createTreeView(root) {
11634
11664
 
11635
11665
  if (!skipThis) {
11636
11666
  initTree(rootEl, null);
11637
- var descendants = rootEl.querySelectorAll('[l-data]');
11667
+ // `initTree` walked the whole subtree, so every root candidate inside it
11668
+ // — nested `[l-data]` scopes AND nested `[data-ui]` components — is
11669
+ // bound already. Claiming only the `[l-data]` ones left a `[data-ui]`
11670
+ // inside a standalone `[data-ui]` looking standalone too: it became a
11671
+ // root of its own and re-bound its subtree, so a button four components
11672
+ // deep fired its `@click` four times.
11673
+ var descendants = rootEl.querySelectorAll('[l-data], [data-ui]');
11638
11674
  for (var d = 0; d < descendants.length; d++) processed.add(descendants[d]);
11639
11675
  processed.add(rootEl);
11640
11676
  }
@@ -11688,7 +11724,14 @@ function createTreeView(root) {
11688
11724
  initController(uiName, node);
11689
11725
  }
11690
11726
 
11691
- if (node.hasAttribute && node.hasAttribute('l-data')) {
11727
+ // Already initialized — by `Faqir.start()` / `Faqir.initTree()` in the
11728
+ // same task that inserted it (the record is only delivered on the
11729
+ // next microtask), or by the l-for / l-if render that stamped it.
11730
+ // Re-running `initTree` would build a second scope and bind every
11731
+ // directive twice — the same guard `bootstrap` applies. [1.1A-23]
11732
+ if (node.__faqirScope) {
11733
+ // fall through to the descendant sweep below
11734
+ } else if (node.hasAttribute && node.hasAttribute('l-data')) {
11692
11735
  initTree(node, findParentScope(node));
11693
11736
  } else if (node.hasAttribute && node.hasAttribute('data-ui') && !findParentScope(node)) {
11694
11737
  // Standalone data-ui (no parent scope) — create its own scope
@@ -11729,14 +11772,35 @@ function createTreeView(root) {
11729
11772
  }
11730
11773
 
11731
11774
  // Auto-start logic
11775
+ //
11776
+ // `data-manual` opts out. A classic script is its own `currentScript`; a
11777
+ // module (or anything a bundler emitted) has none, so there the attribute is
11778
+ // honoured on the page's module `<script>` instead.
11732
11779
  var currentScript = typeof document !== 'undefined' ? document.currentScript : null;
11733
- var isManual = currentScript && currentScript.hasAttribute('data-manual');
11780
+ var isManual = currentScript
11781
+ ? currentScript.hasAttribute('data-manual')
11782
+ : typeof document !== 'undefined' && !!document.querySelector('script[type="module"][data-manual]');
11783
+
11784
+ // Once the document is parsed — always true for a module, a `defer` script
11785
+ // or a bundler import — booting right here ran before the importing code
11786
+ // got past its `import`, so every `Faqir.data()` / `Faqir.directive()` it
11787
+ // registered next was never seen. The boot waits one task instead: long
11788
+ // enough for the rest of the module graph and any `import().then(...)`, and
11789
+ // `DOMContentLoaded` still wins if it has not fired yet. `bootstrap` is
11790
+ // re-entry safe, so an explicit `Faqir.start()` before then costs nothing.
11791
+ var autoStarted = false;
11792
+ function autoStart() {
11793
+ if (autoStarted) return;
11794
+ autoStarted = true;
11795
+ bootstrap();
11796
+ }
11734
11797
 
11735
11798
  if (!isManual && typeof document !== 'undefined') {
11736
11799
  if (document.readyState === 'loading') {
11737
- document.addEventListener('DOMContentLoaded', bootstrap);
11800
+ document.addEventListener('DOMContentLoaded', autoStart);
11738
11801
  } else {
11739
- bootstrap();
11802
+ if (document.readyState === 'interactive') document.addEventListener('DOMContentLoaded', autoStart);
11803
+ setTimeout(autoStart, 0);
11740
11804
  }
11741
11805
  }
11742
11806
 
@@ -11750,7 +11814,7 @@ function createTreeView(root) {
11750
11814
  // one time it was hand-maintained (0.1.0 in a package versioned 0.2.4), so
11751
11815
  // the built artifact is now the only place the number is authoritative and
11752
11816
  // the build fails if this marker goes missing.
11753
- version: '1.0.0', // @faqir:version
11817
+ version: '1.1.1', // @faqir:version
11754
11818
  reactive: reactive,
11755
11819
  effect: effect,
11756
11820
  batch: batch,