tina4js 1.0.11 → 1.0.13

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/TINA4.md CHANGED
@@ -311,13 +311,13 @@ if (import.meta.env.DEV) import('tina4js/debug');
311
311
 
312
312
  | Module | Gzipped |
313
313
  |--------|---------|
314
- | Core (signals + html + component) | ~1.5 KB |
314
+ | Core (signals + html + component) | ~1.51 KB |
315
315
  | Router | ~0.12 KB |
316
- | API | ~0.97 KB |
316
+ | API | ~1.49 KB |
317
317
  | WebSocket | ~0.91 KB |
318
318
  | PWA | ~1.16 KB |
319
- | Debug overlay | ~5.1 KB |
320
- | **Total (core modules)** | **~4.66 KB** |
319
+ | Debug overlay | ~5.11 KB |
320
+ | **Total (core modules)** | **~5.19 KB** |
321
321
 
322
322
  ## Architecture
323
323
 
package/bin/tina4.js CHANGED
@@ -162,6 +162,9 @@ export default defineConfig({
162
162
 
163
163
  let mainTs = `import { signal, computed, html, route, router, navigate, api } from 'tina4js';
164
164
  import './routes/index';
165
+
166
+ // Debug overlay in dev mode (Ctrl+Shift+D to toggle, tree-shaken from production builds)
167
+ if (import.meta.env.DEV) import('tina4js/debug');
165
168
  `;
166
169
 
167
170
  if (withPwa) {
package/dist/debug.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const R=require("./signal.cjs.js"),A=require("./component.cjs.js"),x=require("./index.cjs.js"),y=require("./api.cjs.js"),l=[],b={add(t,o){const e=t._debugInfo;l.push({ref:new WeakRef(t),label:o,createdAt:(e==null?void 0:e.createdAt)??Date.now(),updateCount:0,subs:new WeakRef((e==null?void 0:e.subs)??new Set)})},onUpdate(t){for(const o of l)if(o.ref.deref()===t){o.updateCount++;break}},getAll(){var o;const t=[];for(let e=l.length-1;e>=0;e--){const n=l[e],r=n.ref.deref();if(!r){l.splice(e,1);continue}const s=n.subs.deref();t.push({label:n.label,value:r.peek(),subscriberCount:s?s.size:0,updateCount:((o=r._debugInfo)==null?void 0:o.updateCount)??n.updateCount,alive:!0})}return t},get count(){return l.length}},a=[],h={onMount(t){a.push({ref:new WeakRef(t),tagName:t.tagName.toLowerCase(),mountedAt:Date.now()})},onUnmount(t){const o=a.findIndex(e=>e.ref.deref()===t);o>=0&&a.splice(o,1)},getAll(){const t=[];for(let o=a.length-1;o>=0;o--){const e=a[o],n=e.ref.deref();if(!n||!n.isConnected){a.splice(o,1);continue}const r={},s=n.constructor;if(s.props)for(const i of Object.keys(s.props))try{r[i]=n.prop(i).peek()}catch{}t.push({tagName:e.tagName,props:r,alive:!0})}return t},get count(){return a.length}},d=[],D=50;let f=null;const p={setGetRoutes(t){f=t},getRegisteredRoutes(){return f?f():[]},onNavigate(t){d.unshift({path:t.path,pattern:t.pattern,params:t.params,durationMs:t.durationMs,timestamp:Date.now()}),d.length>D&&d.pop()},getHistory(){return d},get count(){return d.length}};let M=0;const c=[],m=new Map,E=100,g={onRequest(t){var n;const o=t._requestId??++M,e={id:o,method:t.method??"GET",url:t._url??"",hasAuth:!!((n=t.headers)!=null&&n.Authorization),timestamp:Date.now(),pending:!0};m.set(o,e),c.unshift(e),c.length>E&&c.pop()},onResponse(t){const o=t._requestId,e=o!=null?m.get(o):void 0;e&&(e.status=t.status,e.durationMs=Date.now()-e.timestamp,e.pending=!1,t.ok||(e.error=`HTTP ${t.status}`),m.delete(o))},getLog(){return c},get count(){return c.length}},w=`
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const R=require("./signal.cjs.js"),A=require("./component.cjs.js"),x=require("./index.cjs.js"),y=require("./api.cjs.js"),l=[],b={add(t,o){const e=t._debugInfo;l.push({ref:new WeakRef(t),label:o,createdAt:(e==null?void 0:e.createdAt)??Date.now(),updateCount:0,subs:new WeakRef((e==null?void 0:e.subs)??new Set)})},onUpdate(t){for(const o of l)if(o.ref.deref()===t){o.updateCount++;break}},getAll(){var o;const t=[];for(let e=l.length-1;e>=0;e--){const n=l[e],r=n.ref.deref();if(!r){l.splice(e,1);continue}const s=n.subs.deref();t.push({label:n.label,value:r.peek(),subscriberCount:s?s.size:0,updateCount:((o=r._debugInfo)==null?void 0:o.updateCount)??n.updateCount,alive:!0})}return t},get count(){return l.length}},a=[],h={onMount(t){a.push({ref:new WeakRef(t),tagName:t.tagName.toLowerCase(),mountedAt:Date.now()})},onUnmount(t){const o=a.findIndex(e=>e.ref.deref()===t);o>=0&&a.splice(o,1)},getAll(){const t=[];for(let o=a.length-1;o>=0;o--){const e=a[o],n=e.ref.deref();if(!n||!n.isConnected){a.splice(o,1);continue}const r={},s=n.constructor;if(s.props)for(const i of Object.keys(s.props))try{r[i]=n.prop(i).peek()}catch{}t.push({tagName:e.tagName,props:r,alive:!0})}return t},get count(){return a.length}},d=[],D=50;let f=null;const p={setGetRoutes(t){f=t},getRegisteredRoutes(){return f?f():[]},onNavigate(t){d.unshift({path:t.path,pattern:t.pattern,params:t.params,durationMs:t.durationMs,timestamp:Date.now()}),d.length>D&&d.pop()},getHistory(){return d},get count(){return d.length}};let M=0;const c=[],m=new Map,j=100,g={onRequest(t){var n;const o=t._requestId??++M,e={id:o,method:t.method??"GET",url:t._url??"",hasAuth:!!((n=t.headers)!=null&&n.Authorization),timestamp:Date.now(),pending:!0};m.set(o,e),c.unshift(e),c.length>j&&c.pop()},onResponse(t){const o=t._requestId,e=o!=null?m.get(o):void 0;e&&(e.status=t.status,e.durationMs=Date.now()-e.timestamp,e.pending=!1,t.ok||(e.error=`HTTP ${t.status}`),m.delete(o))},getLog(){return c},get count(){return c.length}},w=`
2
2
  :host {
3
3
  all: initial;
4
4
  position: fixed;
@@ -204,7 +204,7 @@ tr:hover td { background: rgba(255,255,255,0.02); }
204
204
  border-radius: 50%;
205
205
  background: #66bb6a;
206
206
  }
207
- `;function L(t){if(t==null)return{text:String(t),cls:"val-null"};if(typeof t=="string")return{text:`"${t.length>30?t.slice(0,30)+"...":t}"`,cls:"val-string"};if(typeof t=="number")return{text:String(t),cls:"val-number"};if(typeof t=="boolean")return{text:String(t),cls:"val-boolean"};if(Array.isArray(t))return{text:`Array(${t.length})`,cls:"val-object"};if(typeof t=="object")try{return{text:JSON.stringify(t).slice(0,40),cls:"val-object"}}catch{}return{text:String(t),cls:"val-object"}}function j(){const t=b.getAll();if(t.length===0)return'<div class="t4-empty">No signals tracked yet.<br>Signals created after debug is enabled will appear here.</div>';let o="";for(let e=0;e<t.length;e++){const n=t[e],{text:r,cls:s}=L(n.value);o+=`<tr>
207
+ `;function E(t){if(t==null)return{text:String(t),cls:"val-null"};if(typeof t=="string")return{text:`"${t.length>30?t.slice(0,30)+"...":t}"`,cls:"val-string"};if(typeof t=="number")return{text:String(t),cls:"val-number"};if(typeof t=="boolean")return{text:String(t),cls:"val-boolean"};if(Array.isArray(t))return{text:`Array(${t.length})`,cls:"val-object"};if(typeof t=="object")try{return{text:JSON.stringify(t).slice(0,40),cls:"val-object"}}catch{}return{text:String(t),cls:"val-object"}}function L(){const t=b.getAll();if(t.length===0)return'<div class="t4-empty">No signals tracked yet.<br>Signals created after debug is enabled will appear here.</div>';let o="";for(let e=0;e<t.length;e++){const n=t[e],{text:r,cls:s}=E(n.value);o+=`<tr>
208
208
  <td>${n.label||`signal_${e}`}</td>
209
209
  <td><span class="${s}">${H(r)}</span></td>
210
210
  <td>${n.subscriberCount}</td>
@@ -218,21 +218,21 @@ tr:hover td { background: rgba(255,255,255,0.02); }
218
218
  </tr>`}return`<table>
219
219
  <thead><tr><th>Element</th><th>Props</th></tr></thead>
220
220
  <tbody>${o}</tbody>
221
- </table>`}function v(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function I(t){const o=t<1?"<1ms":`${Math.round(t)}ms`,e=t<5?"duration fast":t<50?"duration":t<200?"duration slow":"duration very-slow";return{text:o,cls:e}}function q(t){return new Date(t).toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"})}function N(){const t=p.getRegisteredRoutes(),o=p.getHistory();let e="";if(t.length>0){let n="";for(const r of t)n+=`<tr>
221
+ </table>`}function v(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function q(t){const o=t<1?"<1ms":`${Math.round(t)}ms`,e=t<5?"duration fast":t<50?"duration":t<200?"duration slow":"duration very-slow";return{text:o,cls:e}}function I(t){return new Date(t).toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"})}function P(){const t=p.getRegisteredRoutes(),o=p.getHistory();let e="";if(t.length>0){let n="";for(const r of t)n+=`<tr>
222
222
  <td><span class="route-pattern">${_(r.pattern)}</span></td>
223
223
  <td>${r.hasGuard?"Yes":"—"}</td>
224
224
  </tr>`;e+=`<table>
225
225
  <thead><tr><th>Pattern</th><th>Guard</th></tr></thead>
226
226
  <tbody>${n}</tbody>
227
- </table>`}if(o.length>0){e+='<div style="margin-top:8px;padding-top:8px;border-top:1px solid #333;">';let n="";for(const r of o){const{text:s,cls:i}=I(r.durationMs),T=Object.keys(r.params).length>0?Object.entries(r.params).map(([C,S])=>`<span class="route-param">${C}=${S}</span>`).join(" "):"";n+=`<tr>
228
- <td>${q(r.timestamp)}</td>
227
+ </table>`}if(o.length>0){e+='<div style="margin-top:8px;padding-top:8px;border-top:1px solid #333;">';let n="";for(const r of o){const{text:s,cls:i}=q(r.durationMs),T=Object.keys(r.params).length>0?Object.entries(r.params).map(([C,S])=>`<span class="route-param">${C}=${S}</span>`).join(" "):"";n+=`<tr>
228
+ <td>${I(r.timestamp)}</td>
229
229
  <td>${_(r.path)}</td>
230
230
  <td>${T||"—"}</td>
231
231
  <td><span class="${i}">${s}</span></td>
232
232
  </tr>`}e+=`<table>
233
233
  <thead><tr><th>Time</th><th>Path</th><th>Params</th><th>Duration</th></tr></thead>
234
234
  <tbody>${n}</tbody>
235
- </table></div>`}else t.length===0&&(e='<div class="t4-empty">No routes registered yet.</div>');return e}function _(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function P(t){if(t===void 0)return{text:"...",cls:"status-pending"};const o=t<1?"<1ms":`${Math.round(t)}ms`,e=t<100?"duration fast":t<500?"duration":t<2e3?"duration slow":"duration very-slow";return{text:o,cls:e}}function O(t,o){return o?{text:"pending",cls:"status-pending"}:t?t>=200&&t<300?{text:String(t),cls:"status-ok"}:{text:String(t),cls:"status-err"}:{text:"—",cls:""}}function U(t){return new Date(t).toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"})}function G(){const t=g.getLog();if(t.length===0)return'<div class="t4-empty">No API calls yet.<br>Requests made via api.get/post/put/patch/delete will appear here.</div>';let o="";for(const e of t){const{text:n,cls:r}=O(e.status,e.pending),{text:s,cls:i}=P(e.durationMs);o+=`<tr>
235
+ </table></div>`}else t.length===0&&(e='<div class="t4-empty">No routes registered yet.</div>');return e}function _(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function N(t){if(t===void 0)return{text:"...",cls:"status-pending"};const o=t<1?"<1ms":`${Math.round(t)}ms`,e=t<100?"duration fast":t<500?"duration":t<2e3?"duration slow":"duration very-slow";return{text:o,cls:e}}function O(t,o){return o?{text:"pending",cls:"status-pending"}:t?t>=200&&t<300?{text:String(t),cls:"status-ok"}:{text:String(t),cls:"status-err"}:{text:"—",cls:""}}function U(t){return new Date(t).toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"})}function G(){const t=g.getLog();if(t.length===0)return'<div class="t4-empty">No API calls yet.<br>Requests made via api.get/post/put/patch/delete will appear here.</div>';let o="";for(const e of t){const{text:n,cls:r}=O(e.status,e.pending),{text:s,cls:i}=N(e.durationMs);o+=`<tr>
236
236
  <td>${U(e.timestamp)}</td>
237
237
  <td><strong>${e.method}</strong></td>
238
238
  <td>${B(e.url||"(url)")}</td>
@@ -242,11 +242,11 @@ tr:hover td { background: rgba(255,255,255,0.02); }
242
242
  </tr>`}return`<table>
243
243
  <thead><tr><th>Time</th><th>Method</th><th>URL</th><th>Status</th><th>Duration</th><th>Auth</th></tr></thead>
244
244
  <tbody>${o}</tbody>
245
- </table>`}function B(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}class F extends HTMLElement{constructor(){super(),this._visible=!0,this._activeTab="signals",this._refreshTimer=null,this._shadow=this.attachShadow({mode:"open"})}connectedCallback(){this._render(),this._startAutoRefresh()}disconnectedCallback(){this._stopAutoRefresh()}toggle(){this._visible=!this._visible,this._render()}show(){this._visible=!0,this._render()}hide(){this._visible=!1,this._render()}_startAutoRefresh(){this._refreshTimer=window.setInterval(()=>{this._visible&&this._renderBody()},1e3)}_stopAutoRefresh(){this._refreshTimer!==null&&(clearInterval(this._refreshTimer),this._refreshTimer=null)}_switchTab(o){this._activeTab=o,this._render()}_getTabContent(){switch(this._activeTab){case"signals":return j();case"components":return z();case"routes":return N();case"api":return G()}}_renderBody(){const o=this._shadow.querySelector(".t4-body");o&&(o.innerHTML=this._getTabContent()),this._updateTabCounts()}_updateTabCounts(){const o={signals:b.count,components:h.count,routes:p.count,api:g.count};for(const[e,n]of Object.entries(o)){const r=this._shadow.querySelector(`[data-tab-count="${e}"]`);r&&(r.textContent=n>0?`(${n})`:"")}}_render(){var n,r;const o=[{id:"signals",label:"Signals"},{id:"components",label:"Components"},{id:"routes",label:"Routes"},{id:"api",label:"API"}];if(!this._visible){this._shadow.innerHTML=`
245
+ </table>`}function B(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}class F extends HTMLElement{constructor(){super(),this._visible=!0,this._activeTab="signals",this._refreshTimer=null,this._shadow=this.attachShadow({mode:"open"})}connectedCallback(){this._render(),this._startAutoRefresh()}disconnectedCallback(){this._stopAutoRefresh()}toggle(){this._visible=!this._visible,this._render()}show(){this._visible=!0,this._render()}hide(){this._visible=!1,this._render()}_startAutoRefresh(){this._refreshTimer=window.setInterval(()=>{this._visible&&this._renderBody()},1e3)}_stopAutoRefresh(){this._refreshTimer!==null&&(clearInterval(this._refreshTimer),this._refreshTimer=null)}_switchTab(o){this._activeTab=o,this._render()}_getTabContent(){switch(this._activeTab){case"signals":return L();case"components":return z();case"routes":return P();case"api":return G()}}_renderBody(){const o=this._shadow.querySelector(".t4-body");o&&(o.innerHTML=this._getTabContent()),this._updateTabCounts()}_updateTabCounts(){const o={signals:b.count,components:h.count,routes:p.count,api:g.count};for(const[e,n]of Object.entries(o)){const r=this._shadow.querySelector(`[data-tab-count="${e}"]`);r&&(r.textContent=n>0?`(${n})`:"")}}_render(){var n,r;const o=[{id:"signals",label:"Signals"},{id:"components",label:"Components"},{id:"routes",label:"Routes"},{id:"api",label:"API"}];if(!this._visible){this._shadow.innerHTML=`
246
246
  <style>${w}</style>
247
247
  <div class="t4-mini" id="t4-mini">
248
248
  <span class="t4-mini-dot"></span>
249
- T4 Debug
249
+ Debug
250
250
  </div>
251
251
  `,(n=this._shadow.getElementById("t4-mini"))==null||n.addEventListener("click",()=>this.show());return}const e=o.map(s=>`<button class="t4-tab${this._activeTab===s.id?" active":""}" data-tab="${s.id}">
252
252
  ${s.label}<span class="t4-tab-count" data-tab-count="${s.id}"></span>
@@ -255,8 +255,8 @@ tr:hover td { background: rgba(255,255,255,0.02); }
255
255
  <div class="t4-debug">
256
256
  <div class="t4-header">
257
257
  <div>
258
- <span class="t4-logo">TINA4</span>
259
- <span class="t4-badge">DEBUG</span>
258
+ <span class="t4-logo">Tina4js</span>
259
+ <span class="t4-badge">Debug</span>
260
260
  </div>
261
261
  <div class="t4-header-right">
262
262
  <button class="t4-close" id="t4-close" title="Close (Ctrl+Shift+D)">×</button>
package/dist/debug.es.js CHANGED
@@ -103,11 +103,11 @@ const p = {
103
103
  return l.length;
104
104
  }
105
105
  };
106
- let E = 0;
107
- const c = [], m = /* @__PURE__ */ new Map(), M = 100, g = {
106
+ let M = 0;
107
+ const c = [], m = /* @__PURE__ */ new Map(), E = 100, g = {
108
108
  onRequest(t) {
109
109
  var n;
110
- const o = t._requestId ?? ++E, e = {
110
+ const o = t._requestId ?? ++M, e = {
111
111
  id: o,
112
112
  method: t.method ?? "GET",
113
113
  url: t._url ?? "",
@@ -115,7 +115,7 @@ const c = [], m = /* @__PURE__ */ new Map(), M = 100, g = {
115
115
  timestamp: Date.now(),
116
116
  pending: !0
117
117
  };
118
- m.set(o, e), c.unshift(e), c.length > M && c.pop();
118
+ m.set(o, e), c.unshift(e), c.length > E && c.pop();
119
119
  },
120
120
  onResponse(t) {
121
121
  const o = t._requestId, e = o != null ? m.get(o) : void 0;
@@ -350,7 +350,7 @@ function L(t) {
350
350
  }
351
351
  return { text: String(t), cls: "val-object" };
352
352
  }
353
- function H() {
353
+ function j() {
354
354
  const t = b.getAll();
355
355
  if (t.length === 0)
356
356
  return '<div class="t4-empty">No signals tracked yet.<br>Signals created after debug is enabled will appear here.</div>';
@@ -359,7 +359,7 @@ function H() {
359
359
  const n = t[e], { text: r, cls: s } = L(n.value);
360
360
  o += `<tr>
361
361
  <td>${n.label || `signal_${e}`}</td>
362
- <td><span class="${s}">${j(r)}</span></td>
362
+ <td><span class="${s}">${H(r)}</span></td>
363
363
  <td>${n.subscriberCount}</td>
364
364
  <td>${n.updateCount}</td>
365
365
  </tr>`;
@@ -369,7 +369,7 @@ function H() {
369
369
  <tbody>${o}</tbody>
370
370
  </table>`;
371
371
  }
372
- function j(t) {
372
+ function H(t) {
373
373
  return t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
374
374
  }
375
375
  function z() {
@@ -504,7 +504,7 @@ class F extends HTMLElement {
504
504
  _getTabContent() {
505
505
  switch (this._activeTab) {
506
506
  case "signals":
507
- return H();
507
+ return j();
508
508
  case "components":
509
509
  return z();
510
510
  case "routes":
@@ -542,7 +542,7 @@ class F extends HTMLElement {
542
542
  <style>${y}</style>
543
543
  <div class="t4-mini" id="t4-mini">
544
544
  <span class="t4-mini-dot"></span>
545
- T4 Debug
545
+ Debug
546
546
  </div>
547
547
  `, (n = this._shadow.getElementById("t4-mini")) == null || n.addEventListener("click", () => this.show());
548
548
  return;
@@ -557,8 +557,8 @@ class F extends HTMLElement {
557
557
  <div class="t4-debug">
558
558
  <div class="t4-header">
559
559
  <div>
560
- <span class="t4-logo">TINA4</span>
561
- <span class="t4-badge">DEBUG</span>
560
+ <span class="t4-logo">Tina4js</span>
561
+ <span class="t4-badge">Debug</span>
562
562
  </div>
563
563
  <div class="t4-header-right">
564
564
  <button class="t4-close" id="t4-close" title="Close (Ctrl+Shift+D)">×</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4js",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Sub-3KB reactive framework — signals, web components, routing, PWA",
5
5
  "type": "module",
6
6
  "main": "dist/tina4.cjs.js",
@@ -53,6 +53,7 @@
53
53
  "test": "vitest run",
54
54
  "test:watch": "vitest",
55
55
  "test:size": "vitest run tests/size.test.ts",
56
+ "build:gallery": "vite build --config examples/gallery/vite.gallery.config.ts",
56
57
  "preview": "vite preview"
57
58
  },
58
59
  "devDependencies": {
package/readme.md CHANGED
@@ -1,26 +1,70 @@
1
- # Tina4-JS
1
+ <p align="center">
2
+ <img src="https://tina4.com/logo.svg" alt="Tina4" width="200">
3
+ </p>
4
+
5
+ <h1 align="center">tina4-js</h1>
6
+ <h3 align="center">This is not a framework</h3>
7
+
8
+ <p align="center">
9
+ Sub-3KB reactive frontend. Signals. Web Components. Zero dependencies.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/tina4js"><img src="https://img.shields.io/npm/v/tina4js?color=7b1fa2&label=npm" alt="npm"></a>
14
+ <img src="https://img.shields.io/badge/tests-238%20passing-brightgreen" alt="Tests">
15
+ <img src="https://img.shields.io/badge/size-%3C3KB-blue" alt="Size">
16
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
17
+ <a href="https://tina4.com/js"><img src="https://img.shields.io/badge/docs-tina4.com%2Fjs-7b1fa2" alt="Docs"></a>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <a href="https://tina4.com/js">Documentation</a> &bull;
22
+ <a href="#quick-start">Quick Start</a> &bull;
23
+ <a href="#whats-included">What's Included</a> &bull;
24
+ <a href="https://tina4stack.github.io/tina4-js/examples/gallery/">Live Gallery</a> &bull;
25
+ <a href="https://tina4.com">tina4.com</a>
26
+ </p>
2
27
 
3
- Sub-3KB reactive framework — signals, web components, routing, PWA, WebSocket, and API client.
28
+ ---
4
29
 
5
- Works standalone or embedded inside [tina4-php](https://github.com/tina4stack/tina4-php) / [tina4-python](https://github.com/tina4stack/tina4-python).
30
+ ## Quick Start
6
31
 
7
- **[Live Gallery — 9 real-world examples](https://tina4stack.github.io/tina4-js/examples/gallery/)** · dashboards, CRUD, chat, auth, cart, forms, PWA, data tables, and live search — all self-contained, no build step.
32
+ ```bash
33
+ # Create a project
34
+ npx tina4js create my-app
8
35
 
9
- ## Why?
36
+ # With optional CSS framework
37
+ npx tina4js create my-app --css
10
38
 
11
- | Feature | React | Preact | Vue | **tina4-js** |
12
- |-----------------------|--------|--------|--------|--------------|
13
- | Size (gzip) | 42KB | 3KB | 33KB | **~2KB** |
14
- | Virtual DOM | Yes | Yes | Yes | **No** |
15
- | Components | Custom | Custom | Custom | **Native Web Components** |
16
- | Reactivity | Hooks | Hooks | Proxy | **Signals** |
17
- | Router included | No | No | No | **Yes** |
18
- | HTTP client included | No | No | No | **Yes** |
19
- | PWA support | No | No | No | **Yes** |
20
- | Backend integration | None | None | None | **tina4-php/python** |
21
- | Works without build | No | No | No | **Yes** (ESM) |
39
+ # With PWA support
40
+ npx tina4js create my-app --css --pwa
22
41
 
23
- No virtual DOM. Signals track exactly which DOM nodes need updating — O(1) updates.
42
+ # Run it
43
+ cd my-app && npm install && npm run dev
44
+ ```
45
+
46
+ Open http://localhost:3000 -- your app is running.
47
+
48
+ ---
49
+
50
+ ## What's Included
51
+
52
+ Every module is built from scratch -- no node_modules bloat, no third-party runtime dependencies.
53
+
54
+ | Module | Gzipped | What it does |
55
+ |--------|---------|-------------|
56
+ | **Core** | 1.51 KB | Signals, computed, effect, batch, html tagged templates, Tina4Element web components |
57
+ | **Router** | 0.12 KB | Client-side SPA routing, path params (`{id}`), guards, history/hash mode |
58
+ | **API** | 1.49 KB | Fetch client with auth (Bearer + formToken + FreshToken rotation), interceptors, per-request headers/params |
59
+ | **WebSocket** | 0.91 KB | Signal-driven status, auto-reconnect with exponential backoff, pipe() to signal, JSON auto-parse |
60
+ | **PWA** | 1.16 KB | Service worker + manifest generation, cache strategies (network-first, cache-first, stale-while-revalidate) |
61
+ | **Debug** | 5.11 KB | Dev overlay (Ctrl+Shift+D) -- signals, components, routes, API panels |
62
+
63
+ **238 tests across 10 test files. Zero dependencies. Under 3KB for the full core.**
64
+
65
+ For full documentation visit **[tina4.com/javascript](https://tina4.com/js)**.
66
+
67
+ ---
24
68
 
25
69
  ## Install
26
70
 
@@ -32,84 +76,75 @@ Or use via CDN with zero build tools:
32
76
 
33
77
  ```html
34
78
  <script type="module">
35
- import { signal, html } from 'https://cdn.jsdelivr.net/npm/tina4js/dist/tina4.esm.js';
79
+ import { signal, html } from 'https://cdn.jsdelivr.net/npm/tina4js/dist/tina4.es.js';
36
80
  </script>
37
81
  ```
38
82
 
39
- ## Quick Start
83
+ ---
84
+
85
+ ## Getting Started
86
+
87
+ ### 1. Create a project
40
88
 
41
89
  ```bash
42
- npm run dev # dev server with HMR
43
- npm run build # production build
44
- npm test # run tests
90
+ npx tina4js create my-app --css
91
+ cd my-app && npm install
45
92
  ```
46
93
 
47
- ---
94
+ This creates:
48
95
 
49
- ## API Reference
96
+ ```
97
+ my-app/
98
+ index.html # Entry point
99
+ package.json # Dependencies: tina4js, vite, typescript
100
+ src/
101
+ main.ts # App entry -- imports routes, starts router
102
+ routes/
103
+ index.ts # Route definitions
104
+ pages/
105
+ home.ts # Home page handler
106
+ components/
107
+ app-header.ts # Example web component
108
+ public/
109
+ css/
110
+ default.css # Default styles
111
+ ```
50
112
 
51
- ### Signals Reactive State
113
+ ### 2. Create a signal
52
114
 
53
115
  ```ts
54
- import { signal, computed, effect, batch } from 'tina4js';
116
+ import { signal, computed, html } from 'tina4js';
55
117
 
56
- // Create a reactive value
57
118
  const count = signal(0);
58
- count.value; // read: 0
59
- count.value = 5; // write: triggers subscribers
60
-
61
- // Derived value (auto-tracks dependencies)
62
119
  const doubled = computed(() => count.value * 2);
63
- doubled.value; // 10 (read-only)
64
120
 
65
- // Side effect (auto-tracks dependencies)
66
- const dispose = effect(() => {
67
- console.log(`Count is ${count.value}`);
68
- });
69
- // Runs immediately, then re-runs when count changes.
70
- // Call dispose() to stop.
71
-
72
- // Batch multiple updates (one notification)
73
- batch(() => {
74
- a.value = 1;
75
- b.value = 2;
76
- }); // subscribers notified once
121
+ const view = html`
122
+ <button @click=${() => count.value--}>-</button>
123
+ <span>${count}</span>
124
+ <button @click=${() => count.value++}>+</button>
125
+ <p>Doubled: ${doubled}</p>
126
+ `;
127
+
128
+ document.body.append(view);
77
129
  ```
78
130
 
79
- ### html`` Tagged Template Renderer
131
+ ### 3. Create a route
80
132
 
81
133
  ```ts
82
- import { html, signal } from 'tina4js';
83
-
84
- const name = signal('World');
134
+ import { route, router, html } from 'tina4js';
85
135
 
86
- // Creates real DOM nodes (not strings)
87
- const el = html`<h1>Hello ${name}!</h1>`;
88
- document.body.append(el);
89
-
90
- name.value = 'Tina4'; // DOM updates surgically — no diffing
91
-
92
- // Event handlers
93
- html`<button @click=${() => alert('clicked')}>Go</button>`;
94
-
95
- // Conditional rendering
96
- const show = signal(true);
97
- html`<div>${() => show.value ? html`<p>Visible</p>` : null}</div>`;
98
-
99
- // List rendering
100
- const items = signal(['a', 'b', 'c']);
101
- html`<ul>${() => items.value.map(i => html`<li>${i}</li>`)}</ul>`;
102
-
103
- // Reactive attributes
104
- const cls = signal('active');
105
- html`<div class=${cls}>Styled</div>`;
136
+ route('/', () => html`<h1>Home</h1>`);
137
+ route('/user/{id}', ({ id }) => html`<h1>User ${id}</h1>`);
138
+ route('/admin', {
139
+ guard: () => isLoggedIn() || '/login',
140
+ handler: () => html`<h1>Admin</h1>`,
141
+ });
142
+ route('*', () => html`<h1>404</h1>`);
106
143
 
107
- // Boolean attributes
108
- const disabled = signal(false);
109
- html`<button ?disabled=${disabled}>Submit</button>`;
144
+ router.start({ target: '#root', mode: 'history' });
110
145
  ```
111
146
 
112
- ### Tina4Element Web Components
147
+ ### 4. Create a component
113
148
 
114
149
  ```ts
115
150
  import { Tina4Element, html, signal } from 'tina4js';
@@ -135,52 +170,55 @@ customElements.define('my-counter', MyCounter);
135
170
  <my-counter label="Clicks"></my-counter>
136
171
  ```
137
172
 
138
- ### Router Client-Side Routing
139
-
140
- ```ts
141
- import { route, router, navigate, html } from 'tina4js';
142
-
143
- route('/', () => html`<h1>Home</h1>`);
144
- route('/user/{id}', ({ id }) => html`<h1>User ${id}</h1>`);
145
- route('/admin', {
146
- guard: () => isLoggedIn() || '/login',
147
- handler: () => html`<h1>Admin</h1>`,
148
- });
149
- route('*', () => html`<h1>404</h1>`);
150
-
151
- router.start({ target: '#root', mode: 'history' });
152
-
153
- // Programmatic navigation
154
- navigate('/user/42');
155
- ```
156
-
157
- ### API — Fetch Client
173
+ ### 5. Talk to your backend
158
174
 
159
175
  ```ts
160
176
  import { api } from 'tina4js';
161
177
 
162
178
  api.configure({
163
179
  baseUrl: '/api',
164
- auth: true, // auto Bearer + formToken (tina4-php/python compatible)
180
+ auth: true, // Bearer + formToken (tina4-php/python compatible)
165
181
  });
166
182
 
167
183
  const users = await api.get('/users');
168
- const user = await api.get('/users/{id}', { id: 42 });
184
+ const user = await api.get('/users/42');
169
185
  const result = await api.post('/users', { name: 'Andre' });
170
186
 
171
- // Interceptors
172
- api.intercept('request', (config) => {
173
- config.headers['X-Custom'] = 'value';
174
- return config;
187
+ // Query params
188
+ const admins = await api.get('/users', {
189
+ params: { role: 'admin', active: true },
175
190
  });
176
191
 
192
+ // Per-request headers
193
+ const data = await api.get('/data', {
194
+ headers: { 'X-API-Version': '2' },
195
+ });
196
+
197
+ // Interceptors
177
198
  api.intercept('response', (res) => {
178
199
  if (res.status === 401) navigate('/login');
179
200
  return res;
180
201
  });
181
202
  ```
182
203
 
183
- ### PWA Progressive Web App
204
+ ### 6. Real-time with WebSocket
205
+
206
+ ```ts
207
+ import { ws, signal } from 'tina4js';
208
+
209
+ const socket = ws.connect('wss://api.example.com/ws');
210
+ const messages = signal([]);
211
+
212
+ socket.pipe(messages, (msg, current) => [...current, msg]);
213
+
214
+ // Reactive signals
215
+ socket.status.value; // 'connecting' | 'open' | 'closed' | 'reconnecting'
216
+ socket.connected.value; // boolean
217
+ socket.send({ type: 'ping' }); // auto-JSON
218
+ socket.close(); // intentional -- no reconnect
219
+ ```
220
+
221
+ ### 7. Make it a PWA
184
222
 
185
223
  ```ts
186
224
  import { pwa } from 'tina4js';
@@ -191,55 +229,17 @@ pwa.register({
191
229
  themeColor: '#1a1a2e',
192
230
  cacheStrategy: 'network-first',
193
231
  precache: ['/', '/css/default.css'],
194
- offlineRoute: '/offline',
195
232
  });
196
233
  ```
197
234
 
198
- ### WebSocket Signal-driven real-time
235
+ ### 8. Debug everything
199
236
 
200
237
  ```ts
201
- import { ws } from 'tina4js/ws';
202
-
203
- const socket = ws.connect('wss://api.example.com/ws');
204
-
205
- // Reactive signals — use in html templates
206
- socket.status.value; // 'connecting' | 'open' | 'closed' | 'reconnecting'
207
- socket.connected.value; // boolean — true when status is 'open'
208
- socket.lastMessage.value; // last received message (JSON auto-parsed)
209
-
210
- // Pipe messages into a signal
211
- const messages = signal([]);
212
- socket.pipe(messages, (msg, current) => [...current, msg]);
213
-
214
- // Send
215
- socket.send({ type: 'ping' }); // objects auto-JSON serialised
216
-
217
- // Auto-reconnects with exponential backoff by default
218
- socket.close(); // intentional close — no reconnect
219
- ```
220
-
221
- ### Debug Overlay
222
-
223
- A built-in debug overlay that shows live signal values, component tree, route history, and API calls.
224
-
225
- ```ts
226
- // Always-on (remove for production)
227
- import 'tina4js/debug';
228
-
229
- // Dev-only (recommended) — tree-shaken out of production builds
238
+ // Dev-only tree-shaken out of production builds
230
239
  if (import.meta.env.DEV) import('tina4js/debug');
231
240
  ```
232
241
 
233
- Once enabled, toggle the overlay with **Ctrl+Shift+D**.
234
-
235
- The overlay shows four tabs:
236
-
237
- | Tab | What it shows |
238
- |-----|---------------|
239
- | **Signals** | All signals with current value, subscriber count, and update count |
240
- | **Components** | Mounted `Tina4Element` web components |
241
- | **Routes** | Navigation history with timing |
242
- | **API** | Intercepted `api.*` requests and responses |
242
+ Toggle with **Ctrl+Shift+D**. Shows live signal values, mounted components, route history, and API calls.
243
243
 
244
244
  ---
245
245
 
@@ -248,60 +248,67 @@ The overlay shows four tabs:
248
248
  | Mode | Description |
249
249
  |------|-------------|
250
250
  | **Standalone** | `npm run build` → deploy `dist/` to any static host |
251
- | **tina4-php** | `npm run build` → JS bundle into `src/public/js/`, uses `TINA4_APP_DOCUMENT_ROOT` |
252
- | **tina4-python** | `npm run build` → JS bundle into `src/public/js/`, with catch-all route |
251
+ | **tina4-php** | `npm run build` → JS bundle into `src/public/js/` |
252
+ | **tina4-python** | `npm run build` → JS bundle into `src/public/js/` |
253
253
  | **Islands** | No SPA — hydrate individual web components in server-rendered pages |
254
254
 
255
255
  ---
256
256
 
257
+ ## Live Gallery
258
+
259
+ **[9 real-world examples](https://tina4stack.github.io/tina4-js/examples/gallery/)** you can learn from, copy, and build on:
260
+
261
+ 1. Admin Dashboard -- reactive KPIs, polling, notification feed
262
+ 2. Contact Manager -- full CRUD with search/filter
263
+ 3. Real-time Chat -- WebSocket with typing indicators
264
+ 4. Auth Flow -- JWT login, protected routes, token refresh
265
+ 5. Shopping Cart -- shared signals, computed totals, localStorage
266
+ 6. Dynamic Form Builder -- drag fields, live preview, JSON export
267
+ 7. PWA Notes -- offline-capable, installable
268
+ 8. Data Table -- sort, search, pagination
269
+ 9. Live Search -- debounced API calls
270
+
271
+ ---
272
+
257
273
  ## Development
258
274
 
259
275
  ```bash
260
- npm test # run all tests
276
+ npm test # run all tests (238 passing)
261
277
  npm run test:watch # watch mode
262
278
  npm run build # production build
263
- npm run dev # dev server
279
+ npm run build:types # TypeScript declarations
280
+ npm run dev # dev server with HMR
264
281
  ```
265
282
 
283
+ ---
284
+
266
285
  ## Changelog
267
286
 
287
+ ### 1.0.12
288
+ - Added comprehensive boolean attribute tests (opposing pairs, inside reactive blocks, computed, multi-signal)
289
+
290
+ ### 1.0.11
291
+ - **Fix:** `?attr=${() => expr}` now calls the function reactively instead of treating it as truthy
292
+
268
293
  ### 1.0.9
269
- - **Fix:** All `@event` handlers are now automatically wrapped in `batch()` multiple signal writes inside a single handler produce exactly one re-render after the event finishes, preventing mid-event DOM rebuilds and duplicate handler calls on re-rendered elements
294
+ - **Fix:** All `@event` handlers auto-wrapped in `batch()` -- one re-render per handler, no mid-event DOM rebuilds
270
295
 
271
296
  ### 1.0.8
272
- - Added `--css` flag to `tina4 create` scaffolds with [tina4-css](https://www.npmjs.com/package/tina4-css) included
273
- - Added gallery of 9 real-world examples: [live demo](https://tina4stack.github.io/tina4-js/examples/gallery/)
297
+ - Added `--css` flag to `tina4 create` for optional tina4-css integration
298
+ - Added gallery of 9 real-world examples
274
299
 
275
300
  ### 1.0.7
276
- - Added WebSocket module (`tina4js/ws`) with signal-driven status, auto-reconnect with exponential backoff, `pipe()` for streaming messages into signals, and JSON auto-parse/serialise
277
- - Fixed effect error isolation a throwing effect no longer blocks sibling effects
278
- - Fixed API request/response correlation for concurrent requests
279
- - Fixed API tracker always showing empty URL in debug overlay
280
- - Added per-request `headers` and `params` to all API methods
301
+ - Added WebSocket module with signal-driven auto-reconnect and `pipe()`
302
+ - Fixed effect error isolation, API tracker bugs, added per-request headers/params
281
303
  - 231 tests across 10 test files
282
304
 
283
305
  ### 1.0.5
284
- - **Fix:** Effects now properly unsubscribe from signals on dispose — prevents stale subscriptions accumulating in signal subscriber sets across navigations
285
- - **Fix:** Function bindings in `html` templates now dispose inner effects when re-evaluated — fixes duplicate DOM nodes from nested reactive lists and conditionals
286
- - Added 9 new tests covering effect subscription cleanup, inner effect disposal, and multi-navigation accumulation (116 total)
287
-
288
- ### 1.0.4
289
- - Added router reactive effect cleanup tests (navigate away/back, stale effects, async handlers, stale async discard)
290
- - Added debug overlay documentation to README and TINA4.md
291
-
292
- ### 1.0.3
293
- - **Fix:** `renderContent` now uses `replaceChildren` instead of `appendChild`, preventing duplicate content when async route handlers resolve.
306
+ - Fixed effect subscription cleanup and inner effect disposal on re-evaluation
294
307
 
295
- ### 1.0.2
296
- - **Fix:** Router now disposes reactive effects when navigating between routes. Previously, signal subscriptions created by `html` templates survived DOM removal via `innerHTML = ''`, causing duplicate renders when revisiting a page.
297
- - **Fix:** Stale async route handlers are discarded if navigation occurs before they resolve.
298
-
299
- ### 1.0.1
300
- - Debug overlay module with signal, component, route, and API inspectors
301
- - Todo app example and exports map file extension fixes
302
- - CLI scaffolding tool and TINA4.md AI context file
303
- - Fetch, PWA, integration, and size tests (102 total)
308
+ ---
304
309
 
305
310
  ## License
306
311
 
307
312
  MIT
313
+
314
+ *tina4-js — This is not a framework. [tina4.com](https://tina4.com)*