chaiwind 2.0.0 → 2.1.3

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.
package/chaiwind.js CHANGED
@@ -1,339 +1,383 @@
1
1
  (function () {
2
+ // ─── TOKENS ───────────────────────────────────────────────────────────────
3
+ // same tokens as before — just used for lookup, not for building CSS strings
2
4
 
3
- // ---------------------------------------------------------------------------
4
- // tokens
5
- // ---------------------------------------------------------------------------
6
-
7
- var colors = {
5
+ const colors = {
8
6
  // chaicode brand
9
- 'chaicode': '#f97316',
10
- 'chaicode-dark': '#1a1a2e',
11
-
12
- // hitesh sir
13
- 'chai': '#c8843a',
14
- 'adrak': '#d4a056',
15
- 'masala': '#8b4513',
16
- 'kulhad': '#b5651d',
17
-
18
- // piyush sir
19
- 'piyush': '#ec4899',
20
- 'piyush-dark': '#be185d',
21
-
22
- // akash sir
23
- 'midnight': '#1d1d1f',
24
- 'silver': '#e8e8ed',
25
-
26
- // general
27
- 'white': '#ffffff',
28
- 'black': '#000000',
29
- 'gray': '#6b7280',
30
- 'red': '#ef4444',
31
- 'green': '#22c55e',
32
- 'blue': '#3b82f6',
33
- }
34
-
35
- var spacing = {
36
- '0': '0px',
37
- '1': '4px',
38
- '2': '8px',
39
- '3': '12px',
40
- '4': '16px',
41
- '6': '24px',
42
- '8': '32px',
43
- '10': '40px',
44
- '12': '48px',
45
- }
46
-
47
- var fontSizes = {
48
- 'xs': '12px',
49
- 'sm': '14px',
50
- 'base': '16px',
51
- 'lg': '20px',
52
- 'xl': '24px',
53
- '2xl': '32px',
54
- }
55
-
56
- var radii = {
57
- 'none': '0px',
58
- 'sm': '4px',
59
- 'md': '8px',
60
- 'lg': '12px',
61
- 'full': '9999px',
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // prefix map
66
- //
67
- // each entry: prefix -> { map: tokenMap, props: [cssProp, ...] }
68
- // multi-value props e.g. px- sets both paddingLeft and paddingRight
69
- //
70
- // FIX: stored as array of pairs (not object) so order is guaranteed
71
- // FIX: sorted longest-first so px- is checked before p-, mx- before m- etc.
72
- // ---------------------------------------------------------------------------
73
-
74
- var prefixEntries = [
75
- ['px-', { map: spacing, props: ['paddingLeft', 'paddingRight'] }],
76
- ['py-', { map: spacing, props: ['paddingTop', 'paddingBottom'] }],
77
- ['pt-', { map: spacing, props: ['paddingTop'] }],
78
- ['pb-', { map: spacing, props: ['paddingBottom'] }],
79
- ['pl-', { map: spacing, props: ['paddingLeft'] }],
80
- ['pr-', { map: spacing, props: ['paddingRight'] }],
81
- ['p-', { map: spacing, props: ['padding'] }],
82
- ['mx-', { map: spacing, props: ['marginLeft', 'marginRight'] }],
83
- ['my-', { map: spacing, props: ['marginTop', 'marginBottom'] }],
84
- ['mt-', { map: spacing, props: ['marginTop'] }],
85
- ['mb-', { map: spacing, props: ['marginBottom'] }],
86
- ['ml-', { map: spacing, props: ['marginLeft'] }],
87
- ['mr-', { map: spacing, props: ['marginRight'] }],
88
- ['m-', { map: spacing, props: ['margin'] }],
89
- ['gap-', { map: spacing, props: ['gap'] }],
90
- ['w-', { map: spacing, props: ['width'] }],
91
- ['h-', { map: spacing, props: ['height'] }],
92
- ['bg-', { map: colors, props: ['backgroundColor'] }],
93
- ['text-', { map: colors, props: ['color'] }],
94
- ['border-', { map: colors, props: ['borderColor'] }],
95
- ['fs-', { map: fontSizes, props: ['fontSize'] }],
96
- ['rounded-', { map: radii, props: ['borderRadius'] }],
97
- ]
98
-
99
- // ---------------------------------------------------------------------------
100
- // statics map
101
- //
102
- // exact-match classes that do not follow the prefix+token pattern
103
- // one object lookup replaces 40+ individual if blocks
104
- // ---------------------------------------------------------------------------
105
-
106
- var statics = {
107
- // display
108
- 'block': { display: 'block' },
109
- 'inline': { display: 'inline' },
110
- 'inline-block': { display: 'inline-block' },
111
- 'flex': { display: 'flex' },
112
- 'grid': { display: 'grid' },
113
- 'hidden': { display: 'none' },
114
-
115
- // flexbox
116
- 'flex-row': { flexDirection: 'row' },
117
- 'flex-col': { flexDirection: 'column' },
118
- 'flex-wrap': { flexWrap: 'wrap' },
119
- 'flex-1': { flex: '1 1 0%' },
120
- 'items-start': { alignItems: 'flex-start' },
121
- 'items-center': { alignItems: 'center' },
122
- 'items-end': { alignItems: 'flex-end' },
123
- 'justify-start': { justifyContent: 'flex-start' },
124
- 'justify-center': { justifyContent: 'center' },
125
- 'justify-end': { justifyContent: 'flex-end' },
126
- 'justify-between': { justifyContent: 'space-between' },
127
-
128
- // sizing
129
- 'w-full': { width: '100%' },
130
- 'w-screen': { width: '100vw' },
131
- 'w-auto': { width: 'auto' },
132
- 'h-full': { height: '100%' },
133
- 'h-screen': { height: '100vh' },
134
- 'h-auto': { height: 'auto' },
135
-
136
- // position
137
- 'relative': { position: 'relative' },
138
- 'absolute': { position: 'absolute' },
139
- 'fixed': { position: 'fixed' },
140
- 'sticky': { position: 'sticky', top: '0' },
141
-
142
- // text
143
- 'text-left': { textAlign: 'left' },
144
- 'text-center': { textAlign: 'center' },
145
- 'text-right': { textAlign: 'right' },
146
- 'font-normal': { fontWeight: '400' },
147
- 'font-medium': { fontWeight: '500' },
148
- 'font-semibold':{ fontWeight: '600' },
149
- 'font-bold': { fontWeight: '700' },
150
- 'uppercase': { textTransform: 'uppercase' },
151
- 'lowercase': { textTransform: 'lowercase' },
152
- 'capitalize': { textTransform: 'capitalize' },
153
- 'italic': { fontStyle: 'italic' },
154
- 'underline': { textDecoration: 'underline' },
155
- 'no-underline': { textDecoration: 'none' },
156
- 'truncate': { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
157
-
158
- // border
159
- 'border': { border: '1px solid currentColor' },
160
- 'border-2': { border: '2px solid currentColor' },
161
- 'border-none': { border: 'none' },
162
- 'border-dashed': { borderStyle: 'dashed' },
163
-
164
- // overflow
165
- 'overflow-hidden': { overflow: 'hidden' },
166
- 'overflow-auto': { overflow: 'auto' },
167
-
168
- // cursor
169
- 'cursor-pointer': { cursor: 'pointer' },
170
- 'cursor-not-allowed': { cursor: 'not-allowed' },
171
-
172
- // misc
173
- 'select-none': { userSelect: 'none' },
174
- 'box-border': { boxSizing: 'border-box' },
175
- 'opacity-0': { opacity: '0' },
176
- 'opacity-50': { opacity: '0.5' },
177
- 'opacity-100': { opacity: '1' },
178
- }
179
-
180
- // ---------------------------------------------------------------------------
181
- // parse(raw)
182
- //
183
- // raw = class suffix after stripping 'chai-'
184
- // returns a style object or null
185
- //
186
- // order of checks:
187
- // 1. statics exact match (O1 hash lookup)
188
- // 2. prefix entries (guaranteed longest-first no partial match bug)
189
- // ---------------------------------------------------------------------------
190
-
191
- function parse(raw) {
192
- // 1. exact match
193
- if (statics[raw]) return statics[raw]
194
-
195
- // 2. prefix match — array order is longest-first, so px- beats p-
196
- for (var i = 0; i < prefixEntries.length; i++) {
197
- var prefix = prefixEntries[i][0]
198
- var entry = prefixEntries[i][1]
199
-
200
- if (raw.indexOf(prefix) !== 0) continue
201
-
202
- var token = raw.slice(prefix.length)
203
- var value = entry.map[token]
204
-
205
- if (!value) continue
206
-
207
- var style = {}
208
- for (var j = 0; j < entry.props.length; j++) {
209
- style[entry.props[j]] = value
210
- }
211
- return style
212
- }
213
-
214
- return null
215
- }
216
-
217
- // ---------------------------------------------------------------------------
218
- // hasChai(el)
219
- //
220
- // FIX: early exit check — called before any processing
221
- // avoids running parse() on elements that have no chai- classes at all
222
- // critical for MutationObserver performance in JS-heavy apps
223
- // ---------------------------------------------------------------------------
224
-
225
- function hasChai(el) {
226
- if (!el || !el.classList) return false
227
- var list = el.classList
228
- for (var i = 0; i < list.length; i++) {
229
- if (list[i].indexOf('chai-') === 0) return true
230
- }
231
- return false
232
- }
233
-
234
- // ---------------------------------------------------------------------------
235
- // apply(el)
236
- //
237
- // runs parse() on each chai-* class of one element
238
- // applies inline styles and removes the processed class
239
- //
240
- // FIX: nodeType check — skips text nodes and comment nodes
241
- // FIX: stores processed classes in data-chaiwind attr before removing
242
- // so other scripts can still read what was applied
243
- // ---------------------------------------------------------------------------
244
-
245
- function apply(el) {
246
- if (!el || el.nodeType !== 1) return
247
- if (!hasChai(el)) return
248
-
249
- var applied = []
250
- var toRemove = []
251
- var classes = Array.from(el.classList)
252
-
253
- for (var i = 0; i < classes.length; i++) {
254
- var cls = classes[i]
255
- if (cls.indexOf('chai-') !== 0) continue
256
-
257
- var styles = parse(cls.slice(5))
258
- if (!styles) continue
259
-
260
- Object.assign(el.style, styles)
261
- applied.push(cls)
262
- toRemove.push(cls)
263
- }
264
-
265
- // record what was applied before removing (FIX: class removed permanently)
266
- if (applied.length) {
267
- el.setAttribute('data-chaiwind', applied.join(' '))
7
+ chaicode: "#f97316",
8
+ "chaicode-dark": "#1a1a2e",
9
+
10
+ // hitesh sir
11
+ chai: "#c8843a",
12
+ adrak: "#d4a056",
13
+ masala: "#8b4513",
14
+ kulhad: "#b5651d",
15
+ tapri: "#6b3a2a",
16
+ dudh: "#f5f0e8",
17
+
18
+ // piyush sir
19
+ piyush: "#ec4899",
20
+ "piyush-light": "#f9a8d4",
21
+ "piyush-dark": "#be185d",
22
+ rose: "#fb7185",
23
+ blush: "#fce7f3",
24
+ fuschia: "#d946ef",
25
+
26
+ // akash sir
27
+ midnight: "#1d1d1f",
28
+ spacegray: "#86868b",
29
+ silver: "#e8e8ed",
30
+ starlight: "#f5f1eb",
31
+ "macos-blue": "#0071e3",
32
+ "macos-green": "#34c759",
33
+ "macos-red": "#ff3b30",
34
+ aluminum: "#d1d1d6",
35
+
36
+ // basics
37
+ white: "#ffffff",
38
+ black: "#000000",
39
+ gray: "#6b7280",
40
+ red: "#ef4444",
41
+ green: "#22c55e",
42
+ blue: "#3b82f6",
43
+ yellow: "#eab308",
44
+ purple: "#a855f7",
45
+ orange: "#f97316",
46
+ pink: "#ec4899",
47
+ teal: "#14b8a6",
48
+ indigo: "#6366f1",
49
+ };
50
+
51
+ const spacing = {
52
+ 0: "0px",
53
+ 1: "4px",
54
+ 2: "8px",
55
+ 3: "12px",
56
+ 4: "16px",
57
+ 5: "20px",
58
+ 6: "24px",
59
+ 7: "28px",
60
+ 8: "32px",
61
+ 9: "36px",
62
+ 10: "40px",
63
+ 12: "48px",
64
+ 16: "64px",
65
+ 20: "80px",
66
+ 24: "96px",
67
+ };
68
+
69
+ const fontSizes = {
70
+ xs: "11px",
71
+ sm: "13px",
72
+ base: "16px",
73
+ lg: "18px",
74
+ xl: "20px",
75
+ "2xl": "24px",
76
+ "3xl": "30px",
77
+ "4xl": "36px",
78
+ "5xl": "48px",
79
+ };
80
+
81
+ const radii = {
82
+ none: "0px",
83
+ sm: "4px",
84
+ md: "8px",
85
+ lg: "12px",
86
+ xl: "16px",
87
+ "2xl": "24px",
88
+ full: "9999px",
89
+ };
90
+
91
+ const shadows = {
92
+ sm: "0 1px 3px rgba(0,0,0,0.10)",
93
+ md: "0 4px 12px rgba(0,0,0,0.12)",
94
+ lg: "0 8px 24px rgba(0,0,0,0.15)",
95
+ xl: "0 16px 48px rgba(0,0,0,0.20)",
96
+ chai: "0 4px 20px rgba(200,132,58,0.35)",
97
+ piyush: "0 4px 20px rgba(236,72,153,0.30)",
98
+ mac: "0 8px 32px rgba(29,29,31,0.25)",
99
+ none: "none",
100
+ };
101
+
102
+ // ─── PARSER ─────────────────────────────────
103
+ // takes one element + one class name
104
+ // parses the class → applies inline style → removes class
105
+
106
+ function applyClass(el, cls) {
107
+ // strip 'chai-' prefix 'p-4' or 'bg-chai' or 'text-center' etc
108
+ const raw = cls.slice(5); // removes 'chai-'
109
+ const dashIndex = raw.indexOf("-"); // find first dash
110
+
111
+ // if no dash → single word utility like 'chai-flex', 'chai-block'
112
+ const utility = dashIndex === -1 ? raw : raw.slice(0, dashIndex);
113
+ const value = dashIndex === -1 ? "" : raw.slice(dashIndex + 1);
114
+
115
+ // resolve color token or fall back to raw value (e.g. 'red', '#fff')
116
+ const color = colors[value] || value;
117
+ const space = spacing[value] || value;
118
+ const size = fontSizes[value];
119
+ const radius = radii[value] || value;
120
+ const shadow = shadows[value] || value;
121
+
122
+ switch (utility) {
123
+ // ── spacing ───────────────────────────────────────────────────────────
124
+ case "p":
125
+ el.style.padding = space;
126
+ break;
127
+ case "px":
128
+ el.style.paddingLeft = space;
129
+ el.style.paddingRight = space;
130
+ break;
131
+ case "py":
132
+ el.style.paddingTop = space;
133
+ el.style.paddingBottom = space;
134
+ break;
135
+ case "pt":
136
+ el.style.paddingTop = space;
137
+ break;
138
+ case "pb":
139
+ el.style.paddingBottom = space;
140
+ break;
141
+ case "pl":
142
+ el.style.paddingLeft = space;
143
+ break;
144
+ case "pr":
145
+ el.style.paddingRight = space;
146
+ break;
147
+ case "m":
148
+ el.style.margin = space;
149
+ break;
150
+ case "mx":
151
+ el.style.marginLeft = space;
152
+ el.style.marginRight = space;
153
+ break;
154
+ case "my":
155
+ el.style.marginTop = space;
156
+ el.style.marginBottom = space;
157
+ break;
158
+ case "mt":
159
+ el.style.marginTop = space;
160
+ break;
161
+ case "mb":
162
+ el.style.marginBottom = space;
163
+ break;
164
+ case "ml":
165
+ el.style.marginLeft = space;
166
+ break;
167
+ case "mr":
168
+ el.style.marginRight = space;
169
+ break;
170
+ case "gap":
171
+ el.style.gap = space;
172
+ break;
173
+
174
+ // ── colors ────────────────────────────────────────────────────────────
175
+ case "bg":
176
+ el.style.backgroundColor = color;
177
+ break;
178
+ case "border":
179
+ el.style.border = "1px solid " + color;
180
+ break;
181
+ case "outline":
182
+ el.style.outline = "2px solid " + color;
183
+ break;
184
+
185
+ // ── text ──────────────────────────────────────────────────────────────
186
+ // chai-text-red color
187
+ // chai-text-sm → font size
188
+ // chai-text-center / left / right / justify → alignment
189
+ case "text":
190
+ if (colors[value]) el.style.color = color;
191
+ else if (fontSizes[value]) el.style.fontSize = size;
192
+ else el.style.textAlign = value;
193
+ break;
194
+
195
+ // ── font ──────────────────────────────────────────────────────────────
196
+ case "font":
197
+ if (value === "bold") el.style.fontWeight = "700";
198
+ else if (value === "semibold") el.style.fontWeight = "600";
199
+ else if (value === "medium") el.style.fontWeight = "500";
200
+ else if (value === "normal") el.style.fontWeight = "400";
201
+ else if (value === "light") el.style.fontWeight = "300";
202
+ else if (value === "italic") el.style.fontStyle = "italic";
203
+ break;
204
+
205
+ // ── sizing ────────────────────────────────────────────────────────────
206
+ case "w":
207
+ if (value === "full") el.style.width = "100%";
208
+ else if (value === "screen") el.style.width = "100vw";
209
+ else if (value === "auto") el.style.width = "auto";
210
+ else el.style.width = spacing[value] || value;
211
+ break;
212
+ case "h":
213
+ if (value === "full") el.style.height = "100%";
214
+ else if (value === "screen") el.style.height = "100vh";
215
+ else if (value === "auto") el.style.height = "auto";
216
+ else el.style.height = spacing[value] || value;
217
+ break;
218
+ case "min":
219
+ // chai-min-h-screen etc value = 'h-screen'
220
+ if (value === "h-screen") el.style.minHeight = "100vh";
221
+ if (value === "w-full") el.style.minWidth = "100%";
222
+ break;
223
+ case "max":
224
+ if (value === "w-full") el.style.maxWidth = "100%";
225
+ break;
226
+
227
+ // ── border radius ─────────────────────────────────────────────────────
228
+ case "rounded":
229
+ el.style.borderRadius = radius;
230
+ break;
231
+
232
+ // ── shadow ────────────────────────────────────────────────────────────
233
+ case "shadow":
234
+ el.style.boxShadow = shadow;
235
+ break;
236
+
237
+ // ── opacity ───────────────────────────────────────────────────────────
238
+ case "opacity":
239
+ el.style.opacity = String(Number(value) / 100);
240
+ break;
241
+
242
+ // ── display ───────────────────────────────────────────────────────────
243
+ case "flex":
244
+ el.style.display = "flex";
245
+ break;
246
+ case "grid":
247
+ el.style.display = "grid";
248
+ break;
249
+ case "block":
250
+ el.style.display = "block";
251
+ break;
252
+ case "hidden":
253
+ el.style.display = "none";
254
+ break;
255
+ case "inline":
256
+ el.style.display = "inline";
257
+ break;
258
+
259
+ // ── flex helpers ──────────────────────────────────────────────────────
260
+ case "items":
261
+ if (value === "center") el.style.alignItems = "center";
262
+ if (value === "start") el.style.alignItems = "flex-start";
263
+ if (value === "end") el.style.alignItems = "flex-end";
264
+ break;
265
+ case "justify":
266
+ if (value === "center") el.style.justifyContent = "center";
267
+ if (value === "start") el.style.justifyContent = "flex-start";
268
+ if (value === "end") el.style.justifyContent = "flex-end";
269
+ if (value === "between") el.style.justifyContent = "space-between";
270
+ if (value === "around") el.style.justifyContent = "space-around";
271
+ break;
272
+ case "flex-col":
273
+ el.style.flexDirection = "column";
274
+ break;
275
+ case "flex-row":
276
+ el.style.flexDirection = "row";
277
+ break;
278
+ case "flex-wrap":
279
+ el.style.flexWrap = "wrap";
280
+ break;
281
+
282
+ // ── position ──────────────────────────────────────────────────────────
283
+ case "relative":
284
+ el.style.position = "relative";
285
+ break;
286
+ case "absolute":
287
+ el.style.position = "absolute";
288
+ break;
289
+ case "fixed":
290
+ el.style.position = "fixed";
291
+ break;
292
+ case "sticky":
293
+ el.style.position = "sticky";
294
+ break;
295
+
296
+ // ── overflow ──────────────────────────────────────────────────────────
297
+ case "overflow":
298
+ el.style.overflow = value;
299
+ break;
300
+
301
+ // ── cursor ────────────────────────────────────────────────────────────
302
+ case "cursor":
303
+ el.style.cursor = value;
304
+ break;
305
+
306
+ // ── misc ──────────────────────────────────────────────────────────────
307
+ case "uppercase":
308
+ el.style.textTransform = "uppercase";
309
+ break;
310
+ case "lowercase":
311
+ el.style.textTransform = "lowercase";
312
+ break;
313
+ case "capitalize":
314
+ el.style.textTransform = "capitalize";
315
+ break;
316
+ case "underline":
317
+ el.style.textDecoration = "underline";
318
+ break;
319
+ case "line-through":
320
+ el.style.textDecoration = "line-through";
321
+ break;
322
+ case "italic":
323
+ el.style.fontStyle = "italic";
324
+ break;
325
+ case "truncate":
326
+ el.style.overflow = "hidden";
327
+ el.style.textOverflow = "ellipsis";
328
+ el.style.whiteSpace = "nowrap";
329
+ break;
330
+ case "transition":
331
+ el.style.transition = "all 150ms ease";
332
+ break;
333
+ case "select-none":
334
+ el.style.userSelect = "none";
335
+ break;
336
+
337
+ default:
338
+ // unknown class — silently skip
339
+ break;
268
340
  }
269
341
 
270
- for (var k = 0; k < toRemove.length; k++) {
271
- el.classList.remove(toRemove[k])
272
- }
342
+ // requirement: remove the chai- class after applying
343
+ el.classList.remove(cls);
273
344
  }
274
345
 
275
- // ---------------------------------------------------------------------------
276
- // scan()
277
- //
278
- // runs apply() on every element in the DOM at call time
279
- // ---------------------------------------------------------------------------
346
+ // ─── SCANNER ──────────────────────────────────────────────────────────────
347
+ // traverses the full DOM once
348
+ // finds every element that has at least one chai- class
349
+ // applies all chai- classes on that element
280
350
 
281
351
  function scan() {
282
- var elements = document.querySelectorAll('[class]')
283
- for (var i = 0; i < elements.length; i++) {
284
- apply(elements[i])
285
- }
286
- }
352
+ const elements = document.querySelectorAll("[class]");
287
353
 
288
- // ---------------------------------------------------------------------------
289
- // watch()
290
- //
291
- // observes DOM for elements added after initial load
292
- //
293
- // FIX: hasChai() called first on every added node — skips nodes with no
294
- // chai- classes immediately, near-zero cost in React/Vue apps
295
- // FIX: nodeType check inside apply() handles text/comment nodes safely
296
- // ---------------------------------------------------------------------------
297
-
298
- function watch() {
299
- var observer = new MutationObserver(function (mutations) {
300
- for (var i = 0; i < mutations.length; i++) {
301
- var added = mutations[i].addedNodes
302
- for (var j = 0; j < added.length; j++) {
303
- var node = added[j]
304
-
305
- // apply to the node itself
306
- apply(node)
307
-
308
- // apply to any children inside it
309
- if (node.querySelectorAll) {
310
- var children = node.querySelectorAll('[class]')
311
- for (var k = 0; k < children.length; k++) {
312
- apply(children[k])
313
- }
314
- }
354
+ elements.forEach(function (el) {
355
+ // snapshot classList into array first
356
+ // because we're removing classes mid-loop
357
+ const classes = Array.from(el.classList);
358
+
359
+ classes.forEach(function (cls) {
360
+ if (cls.startsWith("chai-")) {
361
+ applyClass(el, cls);
315
362
  }
316
- }
317
- })
363
+ });
364
+ });
318
365
 
319
- observer.observe(document.body, { childList: true, subtree: true })
366
+ console.log(
367
+ "%c☕ chaiwind done — DOM scanned",
368
+ "color: #c8843a; font-weight: bold; font-size: 13px;",
369
+ );
320
370
  }
321
371
 
322
- // ---------------------------------------------------------------------------
323
- // init
324
- //
325
- // FIX: readyState check handles script in <head> vs end of <body>
326
- // recommendation: place script at bottom of <body> to avoid timing edge cases
327
- // ---------------------------------------------------------------------------
328
-
329
- if (document.readyState === 'loading') {
330
- document.addEventListener('DOMContentLoaded', function () {
331
- scan()
332
- watch()
333
- })
372
+ // ─── INIT ─────────────────────────────────────────────────────────────────
373
+ // run scan() only after HTML is fully parsed
374
+ // handles both cases: script in head vs script at end of body
375
+
376
+ if (document.readyState === "loading") {
377
+ // HTML not done parsing yet — wait for it
378
+ document.addEventListener("DOMContentLoaded", scan);
334
379
  } else {
335
- scan()
336
- watch()
380
+ // HTML already parsed (script is at bottom of body)
381
+ scan();
337
382
  }
338
-
339
- })()
383
+ })();