tw-framework 1.0.0 → 1.0.2

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,452 @@
1
+ /**
2
+ * TW Framework -- client runtime v2 (~5KB, no dependencies).
3
+ *
4
+ * Loaded on every TW page. Provides:
5
+ * 1. Reactive state (seeded from <script id="__tw_state">)
6
+ * 2. Event delegation for [data-tw-event-*] handlers (.prevent/.stop)
7
+ * 3. Live interpolation spans [data-tw-i]
8
+ * 4. Two-way input binding [data-tw-model]
9
+ * 5. Conditional blocks [data-tw-if] / [data-tw-not]
10
+ * 6. Live list rendering <tw-for data-tw-var data-tw-list data-tw-tpl>
11
+ * 7. SPA client-side router (link interception, prefetch, popstate)
12
+ */
13
+ (function () {
14
+ "use strict";
15
+
16
+ var stateEl = document.getElementById("__tw_state");
17
+ var state = {};
18
+ try { state = stateEl ? JSON.parse(stateEl.textContent) : {}; } catch (e) { state = {}; }
19
+
20
+ // Strict symbol scope (spec): only the page's OWN imports are exposed
21
+ // via __twClient.$page; layout imports carry dependency, not symbols.
22
+ var twClientScope = function () {
23
+ try { var c = globalThis.__twClient; return (c && c.$page) || {}; } catch (e) { return {}; }
24
+ };
25
+
26
+ function evalExpr(expr) {
27
+ try {
28
+ return (new Function("state", "tw", "with (tw) { with (state) { return (" + expr + "); } }"))(state, twClientScope());
29
+ } catch (e) {
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ function runHandler(code) {
35
+ try {
36
+ (new Function("state", "event", "tw", "with (tw) { with (state) { " + code + "; } }"))(state, window.__twEvent || {}, twClientScope());
37
+ } catch (e) {
38
+ console.error("[tw] handler error:", code, e.message);
39
+ }
40
+ }
41
+
42
+ function setSpanText(el, expr) {
43
+ var val = evalExpr(expr);
44
+ var txt = val === undefined || val === null ? "" : String(val);
45
+ if (el.textContent !== txt) el.textContent = txt;
46
+ }
47
+
48
+ // --- Conditional blocks -----------------------------------------------------
49
+
50
+ function refreshIfs(scope) {
51
+ var ifs = (scope || document).querySelectorAll("[data-tw-if]");
52
+ for (var i = 0; i < ifs.length; i++) {
53
+ var el = ifs[i];
54
+ var val = evalExpr(el.getAttribute("data-tw-if"));
55
+ var show = !!val && val !== "false" && val !== "0" && val !== "";
56
+ el.style.display = show ? "" : "none";
57
+ }
58
+ var nots = (scope || document).querySelectorAll("[data-tw-not]");
59
+ for (var j = 0; j < nots.length; j++) {
60
+ var el2 = nots[j];
61
+ var val2 = evalExpr(el2.getAttribute("data-tw-not"));
62
+ var hide = !!val2 && val2 !== "false" && val2 !== "0" && val2 !== "";
63
+ el2.style.display = hide ? "none" : "";
64
+ }
65
+ }
66
+
67
+ // --- Live lists (<tw-for>) ---------------------------------------------------
68
+
69
+ function renderTemplateString(tpl, loopVar, itemRef) {
70
+ // Evaluate all data-tw-i spans in the template HTML string, resolve
71
+ // nested conditionals, and substitute the loop variable inside event
72
+ // handlers so `todos.filter(x => x !== t)` works per item.
73
+ var holder = document.createElement("tw-render");
74
+ holder.innerHTML = tpl;
75
+ var spans = holder.querySelectorAll("[data-tw-i]");
76
+ for (var i = 0; i < spans.length; i++) setSpanText(spans[i], spans[i].getAttribute("data-tw-i"));
77
+
78
+ // Nested if/not inside the loop body (evaluated while loopVar is bound)
79
+ var all = holder.querySelectorAll("*");
80
+ for (var a = 0; a < all.length; a++) {
81
+ var el = all[a];
82
+ var ifAttr = el.getAttribute && el.getAttribute("data-tw-if");
83
+ if (ifAttr !== null && ifAttr !== undefined && el.hasAttribute("data-tw-if")) {
84
+ var v1 = evalExpr(ifAttr);
85
+ el.style.display = (v1 && v1 !== "false" && v1 !== "0" && v1 !== "") ? "" : "none";
86
+ }
87
+ var notAttr = el.getAttribute("data-tw-not");
88
+ if (notAttr !== null && notAttr !== undefined && el.hasAttribute("data-tw-not")) {
89
+ var v2 = evalExpr(notAttr);
90
+ el.style.display = (v2 && v2 !== "false" && v2 !== "0" && v2 !== "") ? "none" : "";
91
+ }
92
+ }
93
+
94
+ // Substitute the loop variable inside event handler expressions.
95
+ // itemRef is a state key bound to the ACTUAL array item (identity is
96
+ // preserved, so `todos.filter(x => x !== t)` works by reference).
97
+ if (loopVar && itemRef) {
98
+ var re = new RegExp("\\b" + loopVar + "\\b", "g");
99
+ for (var e2 = 0; e2 < all.length; e2++) {
100
+ var el2 = all[e2];
101
+ if (!el2.attributes) continue;
102
+ for (var ai = 0; ai < el2.attributes.length; ai++) {
103
+ var attr = el2.attributes[ai];
104
+ if (attr.name && attr.name.indexOf("data-tw-event-") === 0 && attr.name.indexOf("-prevent") === -1 && attr.name.indexOf("-stop") === -1) {
105
+ var nv = attr.value.replace(re, itemRef);
106
+ if (nv !== attr.value) el2.setAttribute(attr.name, nv);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ var out = holder.innerHTML;
113
+ return out;
114
+ }
115
+
116
+ var twItemSeq = 0;
117
+
118
+ function refreshLoops() {
119
+ var loops = document.querySelectorAll("tw-for[data-tw-list]");
120
+ for (var i = 0; i < loops.length; i++) {
121
+ var el = loops[i];
122
+ var arr = evalExpr(el.getAttribute("data-tw-list"));
123
+ var v = el.getAttribute("data-tw-var");
124
+ var tpl = el.getAttribute("data-tw-tpl") || "";
125
+ if (!Array.isArray(arr)) arr = [];
126
+ // clean refs from the previous render
127
+ if (el.__twRefs) {
128
+ for (var r = 0; r < el.__twRefs.length; r++) delete state[el.__twRefs[r]];
129
+ }
130
+ el.__twRefs = [];
131
+ var html = "";
132
+ for (var k = 0; k < arr.length; k++) {
133
+ var ref = "__tw_item_" + (++twItemSeq);
134
+ state[ref] = arr[k];
135
+ el.__twRefs.push(ref);
136
+ state[v] = arr[k];
137
+ html += renderTemplateString(tpl, v, ref);
138
+ }
139
+ delete state[v];
140
+ if (el.__twLen !== arr.length + ":" + html.length || el.__twHtml !== html) {
141
+ el.innerHTML = html;
142
+ el.__twLen = arr.length + ":" + html.length;
143
+ el.__twHtml = html;
144
+ }
145
+ }
146
+ }
147
+
148
+ // --- Full refresh --------------------------------------------------------
149
+
150
+ function refresh() {
151
+ try { applyActiveClass(); } catch (e) { /* non-DOM context */ }
152
+ try { scanPrefetch(); } catch (e) { /* non-DOM context */ }
153
+ var spans = document.querySelectorAll("[data-tw-i]");
154
+ for (var i = 0; i < spans.length; i++) setSpanText(spans[i], spans[i].getAttribute("data-tw-i"));
155
+ var inputs = document.querySelectorAll("[data-tw-model]");
156
+ for (var j = 0; j < inputs.length; j++) {
157
+ var key = inputs[j].getAttribute("data-tw-model");
158
+ if (key in state) {
159
+ var v = state[key];
160
+ if (String(inputs[j].value) !== String(v)) inputs[j].value = v;
161
+ }
162
+ }
163
+ refreshLoops();
164
+ refreshIfs();
165
+ }
166
+
167
+ // --- Event delegation ----------------------------------------------------
168
+
169
+ var EVENTS = ["click", "dblclick", "input", "change", "submit", "keydown",
170
+ "keyup", "keypress", "focus", "blur", "mouseenter", "mouseleave", "wheel"];
171
+
172
+ EVENTS.forEach(function (ev) {
173
+ document.addEventListener(ev, function (e) {
174
+ var t = e.target;
175
+ if (!t || !t.closest) return;
176
+ var host = t.closest("[data-tw-event-" + ev + "]");
177
+ if (!host) return;
178
+ var code = host.getAttribute("data-tw-event-" + ev);
179
+ if (!code) return;
180
+ if (host.getAttribute("data-tw-event-" + ev + "-prevent") === "true") e.preventDefault();
181
+ if (host.getAttribute("data-tw-event-" + ev + "-stop") === "true") e.stopPropagation();
182
+ window.__twEvent = { target: t, value: t.value, type: ev };
183
+ runHandler(code);
184
+ refresh();
185
+ }, true);
186
+ });
187
+
188
+ // Two-way: input with data-tw-model writes to state
189
+ document.addEventListener("input", function (e) {
190
+ var t = e.target;
191
+ if (t && t.getAttribute && t.getAttribute("data-tw-model")) {
192
+ state[t.getAttribute("data-tw-model")] = t.value;
193
+ refresh();
194
+ }
195
+ });
196
+
197
+ // --- SPA client-side router ----------------------------------------------
198
+
199
+ var prefetched = {};
200
+
201
+ function sameOrigin(href) {
202
+ return href.charAt(0) === "/" && href.charAt(1) !== "/";
203
+ }
204
+
205
+ function swapPage(html, path) {
206
+ var newDoc = null;
207
+ try {
208
+ newDoc = new DOMParser().parseFromString(html, "text/html");
209
+ } catch (e) { newDoc = null; }
210
+
211
+ // Swap roots must stay symmetric: a page compiled with an interactive
212
+ // root ([data-tw-root]) swaps that node only; a fully static page (no
213
+ // interactive root) swaps the whole <body>, so layout chrome that only
214
+ // exists on the incoming page is not lost.
215
+ var liveRoot = document.querySelector("[data-tw-root]");
216
+ var newRoot = newDoc ? (liveRoot ? (newDoc.querySelector("[data-tw-root]") || newDoc.body) : newDoc.body) : null;
217
+ var oldRoot = liveRoot || document.body;
218
+ if (!newRoot) { window.location.href = path; return; }
219
+
220
+ // Merge new state into the live state object (keep identity)
221
+ var seedEl = newDoc ? newDoc.getElementById("__tw_state") : null;
222
+ if (seedEl) {
223
+ try {
224
+ var fresh = JSON.parse(seedEl.textContent);
225
+ for (var k in state) delete state[k];
226
+ for (var k2 in fresh) state[k2] = fresh[k2];
227
+ } catch (e) { /* keep state */ }
228
+ }
229
+ // The new page may stream signals (or may not) -- re-evaluate the
230
+ // connection and derived specs against the swapped-in DOM.
231
+ loadDerivedSpecs();
232
+ recomputeDerived();
233
+ connectSignalStream();
234
+
235
+ // Sync route stylesheets: SPA navigation must load any stylesheet the
236
+ // incoming page links that the live document does not have yet
237
+ // (route-split css assets). Links already present are kept as-is.
238
+ try {
239
+ var newLinks = newDoc ? newDoc.querySelectorAll('link[rel="stylesheet"]') : [];
240
+ var have = {};
241
+ var cur = document.querySelectorAll('link[rel="stylesheet"]');
242
+ for (var ci = 0; ci < cur.length; ci++) have[cur[ci].getAttribute("href")] = true;
243
+ for (var ni = 0; ni < newLinks.length; ni++) {
244
+ var href = newLinks[ni].getAttribute("href");
245
+ if (href && !have[href]) {
246
+ var l = document.createElement("link");
247
+ l.setAttribute("rel", "stylesheet");
248
+ l.setAttribute("href", href);
249
+ document.head.appendChild(l);
250
+ }
251
+ }
252
+ } catch (e) { /* stylesheet sync is best-effort on SPA swap */ }
253
+
254
+ // Keep the live <body> element (preserves document.body identity and
255
+ // any listeners bound to it); swap its content instead of replacing it.
256
+ if (oldRoot === document.body) {
257
+ oldRoot.innerHTML = newRoot.innerHTML;
258
+ } else {
259
+ var imported = document.importNode ? document.importNode(newRoot, true) : newRoot.cloneNode(true);
260
+ if (oldRoot.replaceWith) oldRoot.replaceWith(imported);
261
+ else oldRoot.innerHTML = newRoot.innerHTML;
262
+ }
263
+
264
+ var t = newDoc ? newDoc.querySelector("title") : null;
265
+ if (t) document.title = t.textContent;
266
+
267
+ // re-run scripts inside the new root? (SSR pages usually have none)
268
+ refresh();
269
+ }
270
+
271
+ function navigate(path, push) {
272
+ fetch(path, { headers: { "X-TW-Navigate": "1" } })
273
+ .then(function (r) { return r.text(); })
274
+ .then(function (html) {
275
+ swapPage(html, path);
276
+ if (push !== false) {
277
+ try { history.pushState({ tw: 1 }, "", path); } catch (e) { /* ignore */ }
278
+ }
279
+ window.scrollTo(0, 0);
280
+ })
281
+ .catch(function () { window.location.href = path; });
282
+ }
283
+
284
+ document.addEventListener("click", function (e) {
285
+ if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
286
+ var a = e.target.closest ? e.target.closest("a[href]") : null;
287
+ if (!a) return;
288
+ var href = a.getAttribute("href");
289
+ if (!href || !sameOrigin(href) || href.startsWith("/api/")) return;
290
+ if (a.hasAttribute("data-tw-native") || a.target === "_blank") return;
291
+ e.preventDefault();
292
+ navigate(href, true);
293
+ });
294
+
295
+ window.addEventListener("popstate", function () {
296
+ navigate(window.location.pathname + window.location.search, false);
297
+ });
298
+
299
+ // Prefetch on hover (warms the HTTP cache)
300
+ document.addEventListener("mouseover", function (e) {
301
+ var a = e.target.closest ? e.target.closest("a[href]") : null;
302
+ if (!a) return;
303
+ var href = a.getAttribute("href");
304
+ if (!href || !sameOrigin(href) || prefetched[href]) return;
305
+ if (a.hasAttribute("data-tw-no-prefetch")) return;
306
+ twPrefetch(href);
307
+ }, true);
308
+
309
+ // RouterLink prefetch strategies. The hover handler below covers the
310
+ // default; these cover the explicit ones:
311
+ // data-tw-prefetch="always" -> fetched immediately on load
312
+ // data-tw-prefetch="viewport" -> fetched when the link scrolls into view
313
+ // data-tw-no-prefetch -> never fetched
314
+ function twPrefetch(href) {
315
+ if (!href || !sameOrigin(href) || prefetched[href]) return;
316
+ prefetched[href] = true;
317
+ try { fetch(href).catch(function () {}); } catch (e) { /* ignore */ }
318
+ }
319
+
320
+ function scanPrefetch() {
321
+ var now = document.querySelectorAll('a[data-tw-prefetch="always"]');
322
+ for (var i = 0; i < now.length; i++) twPrefetch(now[i].getAttribute("href"));
323
+
324
+ var vp = document.querySelectorAll('a[data-tw-prefetch="viewport"]');
325
+ for (var j = 0; j < vp.length; j++) {
326
+ var a = vp[j];
327
+ if (a.__twObserved) continue;
328
+ a.__twObserved = true;
329
+ if (typeof IntersectionObserver === "undefined") {
330
+ twPrefetch(a.getAttribute("href"));
331
+ continue;
332
+ }
333
+ try {
334
+ var io = new IntersectionObserver(function (entries, obs) {
335
+ for (var k = 0; k < entries.length; k++) {
336
+ if (entries[k].isIntersecting) {
337
+ var el = entries[k].target;
338
+ el.__twViewportDone = true;
339
+ twPrefetch(el.getAttribute("href"));
340
+ obs.unobserve(el);
341
+ }
342
+ }
343
+ }, { rootMargin: "200px" });
344
+ io.observe(a);
345
+ } catch (e) { twPrefetch(a.getAttribute("href")); }
346
+ }
347
+ }
348
+
349
+ // RouterLink active-class: highlight the link whose route is current.
350
+ // Exact match wins; "/blog" also matches "/blog/post-1" (prefix).
351
+ function applyActiveClass() {
352
+ var links = document.querySelectorAll("a[data-tw-active-class]");
353
+ var path = window.location.pathname;
354
+ for (var i = 0; i < links.length; i++) {
355
+ var a = links[i];
356
+ var href = a.getAttribute("href") || "";
357
+ var on = href === path || (href !== "/" && path.indexOf(href + "/") === 0);
358
+ var cls = a.getAttribute("data-tw-active-class");
359
+ if (on) a.classList.add(cls);
360
+ else a.classList.remove(cls);
361
+ }
362
+ }
363
+
364
+ // Signal Streaming (docs/183): pages with a __TW_SIGNALS__ manifest open
365
+ // a persistent stream and patch their signal bindings in place. Reconnect
366
+ // resumes from the last applied sequence number.
367
+ var activeStream = null;
368
+
369
+ // Derived signals (docs/183): named computed state, recomputed from the
370
+ // other state vars on boot and after every stream frame.
371
+ var derivedSpecs = null;
372
+ function loadDerivedSpecs() {
373
+ var el = document.getElementById("__TW_DERIVED__");
374
+ derivedSpecs = null;
375
+ if (!el) return;
376
+ try {
377
+ var spec = JSON.parse(el.textContent || "{}");
378
+ derivedSpecs = spec.derived || null;
379
+ } catch (e) { derivedSpecs = null; }
380
+ }
381
+ function recomputeDerived() {
382
+ if (!derivedSpecs) return;
383
+ for (var name in derivedSpecs) {
384
+ try { state[name] = evalExpr(derivedSpecs[name]); } catch (e) { /* keep old */ }
385
+ }
386
+ }
387
+ loadDerivedSpecs();
388
+ recomputeDerived();
389
+
390
+ function connectSignalStream() {
391
+ if (activeStream) { activeStream.close(); activeStream = null; }
392
+ var el = document.getElementById("__TW_SIGNALS__");
393
+ if (!el) return;
394
+ var manifest;
395
+ try { manifest = JSON.parse(el.textContent || "{}"); } catch (e) { return; }
396
+ var names = Object.keys(manifest.signals || {});
397
+ if (!names.length) return;
398
+ var lastSeq = 0;
399
+
400
+ function apply(payload) {
401
+ if (!payload || payload.v !== 1) return;
402
+ if (payload.snapshot) {
403
+ for (var k in payload.snapshot) {
404
+ if (state[k] !== undefined || names.indexOf(k) >= 0) state[k] = payload.snapshot[k];
405
+ }
406
+ }
407
+ var ups = payload.updates || [];
408
+ for (var i = 0; i < ups.length; i++) {
409
+ var name = ups[i][0], value = ups[i][1];
410
+ if (names.indexOf(name) >= 0) state[name] = value;
411
+ }
412
+ if (payload.seq) lastSeq = payload.seq;
413
+ recomputeDerived();
414
+ refresh();
415
+ }
416
+
417
+ var es = null;
418
+ function open() {
419
+ es = new EventSource(
420
+ "/_tw/stream?s=" + encodeURIComponent(names.join(",")) + "&since=" + lastSeq
421
+ );
422
+ es.onmessage = function (ev) {
423
+ try { apply(JSON.parse(ev.data)); } catch (e) { /* malformed frame */ }
424
+ };
425
+ es.onerror = function () {
426
+ // EventSource retries on its own, but with the original URL; close
427
+ // and reopen so the connection resumes from lastSeq.
428
+ try { es.close(); } catch (e) { /* already closed */ }
429
+ setTimeout(open, 1500);
430
+ };
431
+ }
432
+ open();
433
+ activeStream = {
434
+ close: function () {
435
+ try { es.close(); } catch (e) { /* already closed */ }
436
+ activeStream = null;
437
+ },
438
+ };
439
+ window.__tw.signalStream = { reconnect: open, lastSeq: function () { return lastSeq; } };
440
+ }
441
+
442
+ // expose for debugging / userland
443
+ window.__tw = {
444
+ state: state,
445
+ refresh: refresh,
446
+ eval: evalExpr,
447
+ navigate: navigate,
448
+ };
449
+
450
+ refresh();
451
+ connectSignalStream();
452
+ })();
package/dist/tw.mjs CHANGED
@@ -7596,6 +7596,7 @@ function parseTWSelector(cursor, errors, state, parseBody) {
7596
7596
  if (token.type === "DOT") {
7597
7597
  cursor.advance();
7598
7598
  let classVal = "";
7599
+ let prevWasMinus = false;
7599
7600
  const classStart = cursor.peek()?.pos;
7600
7601
  while (!cursor.done) {
7601
7602
  const ct = cursor.peek();
@@ -7604,8 +7605,12 @@ function parseTWSelector(cursor, errors, state, parseBody) {
7604
7605
  const after = cursor.peek(1);
7605
7606
  if (after && after.type === "COLON") break;
7606
7607
  }
7608
+ if (ct.type === "IDENT" && !prevWasMinus && classVal.length > 0 && (CLASS_BOUNDARY_BOOLEAN_ATTRS.has(ct.value) || inlineAttrStartsAt(cursor))) {
7609
+ break;
7610
+ }
7607
7611
  if (ct.type === "IDENT" || ct.type === "TEXT" || ct.type === "KEYWORD") {
7608
7612
  classVal += ct.value;
7613
+ prevWasMinus = false;
7609
7614
  cursor.advance();
7610
7615
  } else if (ct.type === "NUMBER") {
7611
7616
  classVal += ct.value;
@@ -7615,6 +7620,7 @@ function parseTWSelector(cursor, errors, state, parseBody) {
7615
7620
  cursor.advance();
7616
7621
  } else if (ct.type === "MINUS") {
7617
7622
  classVal += "-";
7623
+ prevWasMinus = true;
7618
7624
  cursor.advance();
7619
7625
  } else {
7620
7626
  break;
@@ -7623,11 +7629,16 @@ function parseTWSelector(cursor, errors, state, parseBody) {
7623
7629
  while (!cursor.done && cursor.peek()?.type === "DOT") {
7624
7630
  cursor.advance();
7625
7631
  let nextClass = "";
7632
+ let nextPrevMinus = false;
7626
7633
  while (!cursor.done) {
7627
7634
  const ct = cursor.peek();
7628
7635
  if (!ct) break;
7636
+ if (ct.type === "IDENT" && !nextPrevMinus && nextClass.length > 0 && (CLASS_BOUNDARY_BOOLEAN_ATTRS.has(ct.value) || inlineAttrStartsAt(cursor))) {
7637
+ break;
7638
+ }
7629
7639
  if (ct.type === "IDENT" || ct.type === "TEXT" || ct.type === "KEYWORD") {
7630
7640
  nextClass += ct.value;
7641
+ nextPrevMinus = false;
7631
7642
  cursor.advance();
7632
7643
  } else if (ct.type === "NUMBER") {
7633
7644
  nextClass += ct.value;
@@ -7665,7 +7676,7 @@ function parseTWSelector(cursor, errors, state, parseBody) {
7665
7676
  const t2 = cursor.peek();
7666
7677
  if (!t2) break;
7667
7678
  if (t2.type === "LBRACE" || t2.type === "STRING") break;
7668
- if (t2.type === "IDENT" && VOID_HEAD_TAGS_ATTR.has(t2.value)) {
7679
+ if (t2.type === "IDENT" && VOID_HEAD_TAGS_ATTR.has(t2.value) && (el.voidElement || VOID_HEAD_TAGS_ATTR.has(tag.toLowerCase()))) {
7669
7680
  const after = cursor.peek(1);
7670
7681
  if (after && (after.type === "IDENT" || after.type === "STRING")) break;
7671
7682
  }
@@ -7734,13 +7745,21 @@ function parseTWSelector(cursor, errors, state, parseBody) {
7734
7745
  }
7735
7746
  if (t2.type === "IDENT" || t2.type === "KEYWORD") {
7736
7747
  if (t2.pos.line > tagToken.pos.line) break;
7737
- const attrName = t2.value;
7748
+ let attrName = t2.value;
7738
7749
  cursor.advance();
7750
+ while (cursor.peek() && cursor.peek().type === "MINUS") {
7751
+ const hyphNext = cursor.peek(1);
7752
+ if (hyphNext && (hyphNext.type === "IDENT" || hyphNext.type === "TEXT" || hyphNext.type === "KEYWORD")) {
7753
+ cursor.advance();
7754
+ cursor.advance();
7755
+ attrName += "-" + hyphNext.value;
7756
+ } else break;
7757
+ }
7739
7758
  const vToken = cursor.peek();
7740
7759
  if (vToken && (vToken.type === "STRING" || vToken.type === "NUMBER")) {
7741
7760
  cursor.advance();
7742
7761
  el.attrs.push(createAttribute(attrName, vToken.value, t2.pos.line, t2.pos.col));
7743
- } else if (vToken && (vToken.type === "IDENT" || vToken.type === "KEYWORD")) {
7762
+ } else if (vToken && (vToken.type === "IDENT" || vToken.type === "KEYWORD") && vToken.pos.line === t2.pos.line) {
7744
7763
  cursor.advance();
7745
7764
  el.attrs.push(createAttribute(attrName, vToken.value, t2.pos.line, t2.pos.col));
7746
7765
  } else {
@@ -7931,6 +7950,19 @@ function parseTextFromString(cursor) {
7931
7950
  if (!token) return null;
7932
7951
  return createText(token.value, token.pos.line, token.pos.col, false);
7933
7952
  }
7953
+ function inlineAttrStartsAt(cursor) {
7954
+ let k = 1;
7955
+ for (; ; ) {
7956
+ const a = cursor.peek(k);
7957
+ if (!a) return false;
7958
+ if (a.type === "STRING") return true;
7959
+ if (a.type === "MINUS") {
7960
+ k += 2;
7961
+ continue;
7962
+ }
7963
+ return false;
7964
+ }
7965
+ }
7934
7966
  function parseElement(cursor, errors, state, parseBody) {
7935
7967
  const tagToken = cursor.advance();
7936
7968
  if (!tagToken) return null;
@@ -8607,12 +8639,37 @@ function parseBlockBody2(cursor, parseBody) {
8607
8639
  }
8608
8640
  return parseBody(["KEYWORD", "RBRACE"]);
8609
8641
  }
8642
+ var CLASS_BOUNDARY_BOOLEAN_ATTRS;
8610
8643
  var init_statements = __esm({
8611
8644
  "../../packages/compiler/tw/parser/statements/index.ts"() {
8612
8645
  init_nodes();
8613
8646
  init_recovery();
8614
8647
  init_directives();
8615
8648
  init_tw();
8649
+ CLASS_BOUNDARY_BOOLEAN_ATTRS = /* @__PURE__ */ new Set([
8650
+ "disabled",
8651
+ "required",
8652
+ "readonly",
8653
+ "checked",
8654
+ "selected",
8655
+ "autofocus",
8656
+ "hidden",
8657
+ "multiple",
8658
+ "muted",
8659
+ "loop",
8660
+ "controls",
8661
+ "defer",
8662
+ "async",
8663
+ "autoplay",
8664
+ "reversed",
8665
+ "open",
8666
+ "inert",
8667
+ "itemscope",
8668
+ "novalidate",
8669
+ "allowfullscreen",
8670
+ "formnovalidate",
8671
+ "playsinline"
8672
+ ]);
8616
8673
  }
8617
8674
  });
8618
8675
 
@@ -18429,6 +18486,10 @@ function wrapInterpolations(rendered, raw) {
18429
18486
  for (const seg of segs) {
18430
18487
  if (/^\{[^{}]+\}$/.test(seg)) {
18431
18488
  const expr = seg.slice(1, -1);
18489
+ if (ctx_global_inhead) {
18490
+ out += escapeHTML(interpolate2(seg, lastStateVars));
18491
+ continue;
18492
+ }
18432
18493
  out += `<span data-tw-i="${escapeHTML(expr)}"${dataTwSFor(expr)}>${escapeHTML(interpolate2(seg, lastStateVars))}</span>`;
18433
18494
  } else if (seg) {
18434
18495
  out += escapeHTML(seg);
@@ -18438,9 +18499,10 @@ function wrapInterpolations(rendered, raw) {
18438
18499
  }
18439
18500
  function generateText(text, ctx) {
18440
18501
  ctx_global_interactive = ctx.interactive === true;
18502
+ ctx_global_inhead = ctx.inHead === true;
18441
18503
  lastStateVars = ctx.stateVars;
18442
18504
  if (text.isInterpolated) {
18443
- if (ctx.interactive) {
18505
+ if (ctx.interactive && ctx.inHead !== true) {
18444
18506
  return `<span data-tw-i="${escapeHTML(text.value)}"${dataTwSFor(text.value)}>${escapeHTML(interpolate2(`{${text.value}}`, ctx.stateVars))}</span>`;
18445
18507
  }
18446
18508
  return escapeHTML(interpolate2(`{${text.value}}`, ctx.stateVars));
@@ -18645,7 +18707,7 @@ function generateSlot(node, ctx) {
18645
18707
  const fallback = node.fallback.map((c) => generateNode(c, ctx)).join("");
18646
18708
  return `<!-- tw:slot:${node.name} -->${fallback}<!-- /tw:slot:${node.name} -->`;
18647
18709
  }
18648
- var VOID_TAGS5, BOOLEAN_ATTRS2, CSS_CHUNKS, CSS_ROUTE_CHUNKS, CSS_CAPTURE, activeBuiltinImports, SUSPENSE_BOOT_RUNTIME, suspenseCounter, ctx_global_interactive, lastStateVars, componentRegistry;
18710
+ var VOID_TAGS5, BOOLEAN_ATTRS2, CSS_CHUNKS, CSS_ROUTE_CHUNKS, CSS_CAPTURE, activeBuiltinImports, SUSPENSE_BOOT_RUNTIME, suspenseCounter, ctx_global_interactive, ctx_global_inhead, lastStateVars, componentRegistry;
18649
18711
  var init_html2 = __esm({
18650
18712
  "../../packages/compiler/tw/codegen/html.ts"() {
18651
18713
  init_types4();
@@ -18733,6 +18795,7 @@ var init_html2 = __esm({
18733
18795
  ].join("\n");
18734
18796
  suspenseCounter = 0;
18735
18797
  ctx_global_interactive = false;
18798
+ ctx_global_inhead = false;
18736
18799
  lastStateVars = {};
18737
18800
  componentRegistry = /* @__PURE__ */ new Map();
18738
18801
  }
@@ -48334,7 +48397,7 @@ async function buildCommand() {
48334
48397
  const _bundleDir = globalThis.__TW_BUNDLE_DIR;
48335
48398
  const _fu = import.meta.url.replace("file://", "");
48336
48399
  const _srcDir = _fu.substring(0, _fu.lastIndexOf("/"));
48337
- const runtimeSrc = _bundleDir ? resolve6(_bundleDir, "../../..", "packages/runtime/tw/client/hydration-runtime.js") : resolve6(_srcDir, "../../../..", "packages/runtime/tw/client/hydration-runtime.js");
48400
+ const runtimeSrc = _bundleDir ? resolve6(_bundleDir, "hydration-runtime.js") : resolve6(_srcDir, "../../../..", "packages/runtime/tw/client/hydration-runtime.js");
48338
48401
  if (existsSync16(runtimeSrc)) {
48339
48402
  copyFileSync2(runtimeSrc, join15(outDir, "__tw_runtime.js"));
48340
48403
  console.log(" \x1B[36m\u26A1\x1B[0m hydration runtime: .tw/__tw_runtime.js");
@@ -54433,7 +54496,9 @@ var helpText = `
54433
54496
 
54434
54497
  ship [target] Deploy to platform
54435
54498
  adapter [name] Generate deployment adapter files (node, bun, docker, vercel)
54436
- targets: vercel, netlify, cloudflare, docker, static
54499
+ targets: node, bun, docker, vercel, netlify, cloudflare,
54500
+ aws, digitalocean, render, railway, fly,
54501
+ github-pages, firebase, nginx, caddy
54437
54502
  (default: auto-detect)
54438
54503
 
54439
54504
  plugin <action> Manage plugins
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tw-framework",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Full-stack web framework \u2014 own template language (.tw), styling (.tss), compiler and VDOM; runs on Bun and Node",
5
5
  "type": "module",
6
6
  "main": "./dist/tw.mjs",