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 +4 -4
- package/bin/tina4.js +3 -0
- package/dist/debug.cjs.js +10 -10
- package/dist/debug.es.js +11 -11
- package/package.json +2 -1
- package/readme.md +178 -171
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.
|
|
314
|
+
| Core (signals + html + component) | ~1.51 KB |
|
|
315
315
|
| Router | ~0.12 KB |
|
|
316
|
-
| API | ~
|
|
316
|
+
| API | ~1.49 KB |
|
|
317
317
|
| WebSocket | ~0.91 KB |
|
|
318
318
|
| PWA | ~1.16 KB |
|
|
319
|
-
| Debug overlay | ~5.
|
|
320
|
-
| **Total (core modules)** | **~
|
|
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,
|
|
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
|
|
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,"&").replace(/</g,"<").replace(/>/g,">")}function
|
|
221
|
+
</table>`}function v(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}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}=
|
|
228
|
-
<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,"&").replace(/</g,"<").replace(/>/g,">")}function
|
|
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,"&").replace(/</g,"<").replace(/>/g,">")}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,"&").replace(/</g,"<").replace(/>/g,">")}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
|
|
245
|
+
</table>`}function B(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}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
|
-
|
|
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">
|
|
259
|
-
<span class="t4-badge">
|
|
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
|
|
107
|
-
const c = [], m = /* @__PURE__ */ new Map(),
|
|
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 ?? ++
|
|
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 >
|
|
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
|
|
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}">${
|
|
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
|
|
372
|
+
function H(t) {
|
|
373
373
|
return t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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
|
-
|
|
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">
|
|
561
|
-
<span class="t4-badge">
|
|
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.
|
|
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
|
-
|
|
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> •
|
|
22
|
+
<a href="#quick-start">Quick Start</a> •
|
|
23
|
+
<a href="#whats-included">What's Included</a> •
|
|
24
|
+
<a href="https://tina4stack.github.io/tina4-js/examples/gallery/">Live Gallery</a> •
|
|
25
|
+
<a href="https://tina4.com">tina4.com</a>
|
|
26
|
+
</p>
|
|
2
27
|
|
|
3
|
-
|
|
28
|
+
---
|
|
4
29
|
|
|
5
|
-
|
|
30
|
+
## Quick Start
|
|
6
31
|
|
|
7
|
-
|
|
32
|
+
```bash
|
|
33
|
+
# Create a project
|
|
34
|
+
npx tina4js create my-app
|
|
8
35
|
|
|
9
|
-
|
|
36
|
+
# With optional CSS framework
|
|
37
|
+
npx tina4js create my-app --css
|
|
10
38
|
|
|
11
|
-
|
|
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
|
-
|
|
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.
|
|
79
|
+
import { signal, html } from 'https://cdn.jsdelivr.net/npm/tina4js/dist/tina4.es.js';
|
|
36
80
|
</script>
|
|
37
81
|
```
|
|
38
82
|
|
|
39
|
-
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Getting Started
|
|
86
|
+
|
|
87
|
+
### 1. Create a project
|
|
40
88
|
|
|
41
89
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
113
|
+
### 2. Create a signal
|
|
52
114
|
|
|
53
115
|
```ts
|
|
54
|
-
import { signal, computed,
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
###
|
|
131
|
+
### 3. Create a route
|
|
80
132
|
|
|
81
133
|
```ts
|
|
82
|
-
import {
|
|
83
|
-
|
|
84
|
-
const name = signal('World');
|
|
134
|
+
import { route, router, html } from 'tina4js';
|
|
85
135
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
108
|
-
const disabled = signal(false);
|
|
109
|
-
html`<button ?disabled=${disabled}>Submit</button>`;
|
|
144
|
+
router.start({ target: '#root', mode: 'history' });
|
|
110
145
|
```
|
|
111
146
|
|
|
112
|
-
###
|
|
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
|
-
###
|
|
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, //
|
|
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/
|
|
184
|
+
const user = await api.get('/users/42');
|
|
169
185
|
const result = await api.post('/users', { name: 'Andre' });
|
|
170
186
|
|
|
171
|
-
//
|
|
172
|
-
api.
|
|
173
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
235
|
+
### 8. Debug everything
|
|
199
236
|
|
|
200
237
|
```ts
|
|
201
|
-
|
|
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
|
-
|
|
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
|
|
252
|
-
| **tina4-python** | `npm run build` → JS bundle into `src/public/js
|
|
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
|
|
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
|
|
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`
|
|
273
|
-
- Added gallery of 9 real-world examples
|
|
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
|
|
277
|
-
- Fixed effect error isolation
|
|
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
|
-
-
|
|
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
|
-
|
|
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)*
|