tina4js 1.2.2 → 1.2.7

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
@@ -16,6 +16,7 @@
16
16
  | API | `await api.get('/path')`, `.post`, `.put`, `.patch`, `.delete` |
17
17
  | PWA | `pwa.register({ name, themeColor, cacheStrategy })` |
18
18
  | WebSocket | `ws.connect('wss://...', { reconnect: true })` — signals for status/messages |
19
+ | Storage | `persist(signal('light'), { key: 'theme' })` from `tina4js/storage` — survives a refresh |
19
20
  | Debug | `import 'tina4js/debug'` — toggle with Ctrl+Shift+D |
20
21
 
21
22
  ## File Conventions
@@ -40,6 +41,8 @@
40
41
  10. Use `static shadow = false` for light DOM components
41
42
  11. `route(pattern, handler)` — pattern is ALWAYS the first argument, handler/config is second
42
43
  12. `api.configure()` must be called before any API calls if you need auth or a base URL
44
+ 13. `persist()` is for user preferences only — never tokens, passwords, personal data, or any
45
+ credentials. localStorage is XSS-readable. See `STORAGE.md` for the full dangers list.
43
46
 
44
47
  ## Signal Patterns
45
48
 
package/bin/tina4.js CHANGED
@@ -163,7 +163,9 @@ export default defineConfig({
163
163
  let mainTs = `import { signal, computed, html, route, router, navigate, api } from 'tina4js';
164
164
  import './routes/index';
165
165
 
166
- // Debug overlay in dev mode (Ctrl+Shift+D to toggle, tree-shaken from production builds)
166
+ // Debug overlay in dev mode (Ctrl+Shift+D to toggle, tree-shaken from
167
+ // production builds). Signals created before this dynamic import resolves —
168
+ // including module-level store signals — are buffered and still tracked.
167
169
  if (import.meta.env.DEV) import('tina4js/debug');
168
170
  `;
169
171
 
@@ -14,7 +14,7 @@ export declare function _getEffectCollector(): (() => void)[] | null;
14
14
  export declare let __debugSignalCreate: ((s: Signal<unknown>, label?: string) => void) | null;
15
15
  /** @internal Called when a signal value changes. */
16
16
  export declare let __debugSignalUpdate: ((s: Signal<unknown>, oldVal: unknown, newVal: unknown) => void) | null;
17
- /** @internal Set the debug hooks. */
17
+ /** @internal Set the debug hooks, then replay signals created before now. */
18
18
  export declare function __setDebugSignalHooks(onCreate: typeof __debugSignalCreate, onUpdate: typeof __debugSignalUpdate): void;
19
19
  export interface Signal<T> {
20
20
  value: T;
@@ -1 +1 @@
1
- {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/core/signal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,IAAI,CAE1E;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,IAAI,CAE3D;AAID,iDAAiD;AACjD,eAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAW,CAAC;AAC7F,oDAAoD;AACpD,eAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAW,CAAC;AAC/G,qCAAqC;AACrC,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,mBAAmB,EACpC,QAAQ,EAAE,OAAO,mBAAmB,QAIrC;AAUD,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB;IAChB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACvC,sCAAsC;IACtC,IAAI,IAAI,CAAC,CAAC;CACX;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,gBAAgB;IAChB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,gBAAgB;IAChB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACvC,IAAI,IAAI,CAAC,CAAC;CACX;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAoD/D;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CA2B1D;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAgCjD;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAgB1C;AAID,0CAA0C;AAC1C,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,CAEjE"}
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../src/core/signal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,IAAI,GAAG,IAAI,CAE1E;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,GAAG,IAAI,CAE3D;AAID,iDAAiD;AACjD,eAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAW,CAAC;AAC7F,oDAAoD;AACpD,eAAO,IAAI,mBAAmB,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,IAAW,CAAC;AAqB/G,6EAA6E;AAC7E,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,mBAAmB,EACpC,QAAQ,EAAE,OAAO,mBAAmB,QAqBrC;AAUD,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,CAAC;IACT,gBAAgB;IAChB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACvC,sCAAsC;IACtC,IAAI,IAAI,CAAC,CAAC;CACX;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAClB,gBAAgB;IAChB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,gBAAgB;IAChB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IACvC,IAAI,IAAI,CAAC,CAAC;CACX;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CA6D/D;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CA2B1D;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAgCjD;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAgB1C;AAID,0CAA0C;AAC1C,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,OAAO,CAAC,CAEjE"}
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,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=`
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"),d=[],b={add(t,o){const e=t._debugInfo;d.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 d)if(o.ref.deref()===t){o.updateCount++;break}},getAll(){var o;const t=[];for(let e=d.length-1;e>=0;e--){const n=d[e],r=n.ref.deref();if(!r){d.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 d.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}},l=[],D=50;let f=null;const p={setGetRoutes(t){f=t},getRegisteredRoutes(){return f?f():[]},onNavigate(t){l.unshift({path:t.path,pattern:t.pattern,params:t.params,durationMs:t.durationMs,timestamp:Date.now()}),l.length>D&&l.pop()},getHistory(){return l},get count(){return l.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}},v=`
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 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>
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>Create a signal it appears here, even ones made before the overlay loaded.</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>
@@ -213,12 +213,12 @@ tr:hover td { background: rgba(255,255,255,0.02); }
213
213
  <thead><tr><th>Label</th><th>Value</th><th>Subs</th><th>Updates</th></tr></thead>
214
214
  <tbody>${o}</tbody>
215
215
  </table>`}function H(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function z(){const t=h.getAll();if(t.length===0)return'<div class="t4-empty">No Tina4Elements mounted.<br>Custom elements extending Tina4Element will appear here.</div>';let o="";for(const e of t){const n=Object.keys(e.props).length>0?Object.entries(e.props).map(([r,s])=>`${r}=${JSON.stringify(s)??"null"}`).join(", "):"—";o+=`<tr>
216
- <td>&lt;${v(e.tagName)}&gt;</td>
217
- <td>${v(n.length>60?n.slice(0,60)+"...":n)}</td>
216
+ <td>&lt;${w(e.tagName)}&gt;</td>
217
+ <td>${w(n.length>60?n.slice(0,60)+"...":n)}</td>
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 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>
221
+ </table>`}function w(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>
@@ -243,7 +243,7 @@ tr:hover td { background: rgba(255,255,255,0.02); }
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
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
- <style>${w}</style>
246
+ <style>${v}</style>
247
247
  <div class="t4-mini" id="t4-mini">
248
248
  <span class="t4-mini-dot"></span>
249
249
  Debug
@@ -251,7 +251,7 @@ tr:hover td { background: rgba(255,255,255,0.02); }
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>
253
253
  </button>`).join("");this._shadow.innerHTML=`
254
- <style>${w}</style>
254
+ <style>${v}</style>
255
255
  <div class="t4-debug">
256
256
  <div class="t4-header">
257
257
  <div>
package/dist/debug.es.js CHANGED
@@ -2,7 +2,7 @@ import { d as C } from "./signal.es.js";
2
2
  import { _ as S } from "./component.es.js";
3
3
  import { _ as R, a as A } from "./index.es.js";
4
4
  import { api as x } from "./api.es.js";
5
- const d = [], b = {
5
+ const d = [], h = {
6
6
  add(t, o) {
7
7
  const e = t._debugInfo;
8
8
  d.push({
@@ -43,7 +43,7 @@ const d = [], b = {
43
43
  get count() {
44
44
  return d.length;
45
45
  }
46
- }, a = [], h = {
46
+ }, a = [], b = {
47
47
  onMount(t) {
48
48
  a.push({
49
49
  ref: new WeakRef(t),
@@ -351,9 +351,9 @@ function L(t) {
351
351
  return { text: String(t), cls: "val-object" };
352
352
  }
353
353
  function j() {
354
- const t = b.getAll();
354
+ const t = h.getAll();
355
355
  if (t.length === 0)
356
- return '<div class="t4-empty">No signals tracked yet.<br>Signals created after debug is enabled will appear here.</div>';
356
+ return '<div class="t4-empty">No signals tracked yet.<br>Create a signal it appears here, even ones made before the overlay loaded.</div>';
357
357
  let o = "";
358
358
  for (let e = 0; e < t.length; e++) {
359
359
  const n = t[e], { text: r, cls: s } = L(n.value);
@@ -373,15 +373,15 @@ function H(t) {
373
373
  return t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
374
374
  }
375
375
  function z() {
376
- const t = h.getAll();
376
+ const t = b.getAll();
377
377
  if (t.length === 0)
378
378
  return '<div class="t4-empty">No Tina4Elements mounted.<br>Custom elements extending Tina4Element will appear here.</div>';
379
379
  let o = "";
380
380
  for (const e of t) {
381
381
  const n = Object.keys(e.props).length > 0 ? Object.entries(e.props).map(([r, s]) => `${r}=${JSON.stringify(s) ?? "null"}`).join(", ") : "—";
382
382
  o += `<tr>
383
- <td>&lt;${w(e.tagName)}&gt;</td>
384
- <td>${w(n.length > 60 ? n.slice(0, 60) + "..." : n)}</td>
383
+ <td>&lt;${v(e.tagName)}&gt;</td>
384
+ <td>${v(n.length > 60 ? n.slice(0, 60) + "..." : n)}</td>
385
385
  </tr>`;
386
386
  }
387
387
  return `<table>
@@ -389,7 +389,7 @@ function z() {
389
389
  <tbody>${o}</tbody>
390
390
  </table>`;
391
391
  }
392
- function w(t) {
392
+ function v(t) {
393
393
  return t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
394
394
  }
395
395
  function I(t) {
@@ -406,7 +406,7 @@ function P() {
406
406
  let n = "";
407
407
  for (const r of t)
408
408
  n += `<tr>
409
- <td><span class="route-pattern">${_(r.pattern)}</span></td>
409
+ <td><span class="route-pattern">${w(r.pattern)}</span></td>
410
410
  <td>${r.hasGuard ? "Yes" : "—"}</td>
411
411
  </tr>`;
412
412
  e += `<table>
@@ -421,7 +421,7 @@ function P() {
421
421
  const { text: s, cls: i } = I(r.durationMs), $ = Object.keys(r.params).length > 0 ? Object.entries(r.params).map(([k, T]) => `<span class="route-param">${k}=${T}</span>`).join(" ") : "";
422
422
  n += `<tr>
423
423
  <td>${N(r.timestamp)}</td>
424
- <td>${_(r.path)}</td>
424
+ <td>${w(r.path)}</td>
425
425
  <td>${$ || "—"}</td>
426
426
  <td><span class="${i}">${s}</span></td>
427
427
  </tr>`;
@@ -433,7 +433,7 @@ function P() {
433
433
  } else t.length === 0 && (e = '<div class="t4-empty">No routes registered yet.</div>');
434
434
  return e;
435
435
  }
436
- function _(t) {
436
+ function w(t) {
437
437
  return t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
438
438
  }
439
439
  function O(t) {
@@ -519,8 +519,8 @@ class F extends HTMLElement {
519
519
  }
520
520
  _updateTabCounts() {
521
521
  const o = {
522
- signals: b.count,
523
- components: h.count,
522
+ signals: h.count,
523
+ components: b.count,
524
524
  routes: p.count,
525
525
  api: g.count
526
526
  };
@@ -578,14 +578,14 @@ class F extends HTMLElement {
578
578
  function V() {
579
579
  typeof customElements < "u" && !customElements.get("tina4-debug") && customElements.define("tina4-debug", F);
580
580
  }
581
- let u = null, v = !1;
581
+ let u = null, _ = !1;
582
582
  function W() {
583
- v || (v = !0, C(
584
- (t, o) => b.add(t, o),
585
- (t) => b.onUpdate(t)
583
+ _ || (_ = !0, C(
584
+ (t, o) => h.add(t, o),
585
+ (t) => h.onUpdate(t)
586
586
  ), S(
587
- (t) => h.onMount(t),
588
- (t) => h.onUnmount(t)
587
+ (t) => b.onMount(t),
588
+ (t) => b.onUnmount(t)
589
589
  ), p.setGetRoutes(R), A.on("change", (t) => {
590
590
  p.onNavigate(t);
591
591
  }), x.intercept("request", (t) => (g.onRequest(t), t)), x.intercept("response", (t) => (g.onResponse(t), t)), typeof document < "u" && (V(), u = document.createElement("tina4-debug"), document.body.appendChild(u), document.addEventListener("keydown", (t) => {
package/dist/index.d.ts CHANGED
@@ -18,4 +18,6 @@ export { ws } from './ws/ws';
18
18
  export type { SocketStatus, SocketOptions, ManagedSocket } from './ws/ws';
19
19
  export { sse } from './sse/sse';
20
20
  export type { StreamStatus, StreamOptions, ManagedStream } from './sse/sse';
21
+ export { persist, clearPersistedKeys } from './storage/persist';
22
+ export type { PersistOptions, PersistSerializer, PersistedSignal } from './storage/persist';
21
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC1E,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGjD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG1F,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG1E,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG1E,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC1E,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGjD,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG1F,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG1E,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG1E,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG5E,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
@@ -1 +1 @@
1
- "use strict";let l=null,f=null,i=null;function C(t){i=t}function y(){return i}let d=null,_=null;function E(t,n){d=t,_=n}let s=0;const b=new Set;function g(t,n){let e=t;const o=new Set,u={_t4:!0,get value(){if(l&&(o.add(l),f)){const r=l;f.push(()=>o.delete(r))}return e},set value(r){if(Object.is(r,e))return;const a=e;if(e=r,u._debugInfo&&u._debugInfo.updateCount++,_&&_(u,a,r),s>0)for(const c of o)b.add(c);else{let c;for(const h of[...o])try{h()}catch(v){c===void 0&&(c=v)}if(c!==void 0)throw c}},_subscribe(r){return o.add(r),()=>{o.delete(r)}},peek(){return e}};return d&&(u._debugInfo={label:n,createdAt:Date.now(),updateCount:0,subs:o},d(u,n)),u}function S(t){const n=g(void 0);return p(()=>{n.value=t()}),{_t4:!0,get value(){return n.value},set value(e){throw new Error("[tina4] computed signals are read-only")},_subscribe(e){return n._subscribe(e)},peek(){return n.peek()}}}function p(t){let n=!1,e=[];const o=()=>{if(n)return;for(const c of e)c();e=[];const r=l,a=f;l=o,f=e;try{t()}finally{l=r,f=a}};o();const u=()=>{n=!0;for(const r of e)r();e=[]};return i&&i.push(u),u}function w(t){s++;try{t()}finally{if(s--,s===0){const n=[...b];b.clear();let e;for(const o of n)try{o()}catch(u){e===void 0&&(e=u)}if(e!==void 0)throw e}}}function k(t){return t!==null&&typeof t=="object"&&t._t4===!0}exports.__setDebugSignalHooks=E;exports._getEffectCollector=y;exports._setEffectCollector=C;exports.batch=w;exports.computed=S;exports.effect=p;exports.isSignal=k;exports.signal=g;
1
+ "use strict";let f=null,s=null,i=null;function S(n){i=n}function w(){return i}let _=null,b=null;const a=[],E=512;function y(n,o){if(_=n,b=o,n)for(const e of a){const t=e.ref.deref();t&&(t._debugInfo||(t._debugInfo={label:e.label,createdAt:e.createdAt,updateCount:0,subs:e.subs}),n(t,e.label))}a.length=0}let l=0;const g=new Set;function p(n,o){let e=n;const t=new Set,r={_t4:!0,get value(){if(f&&(t.add(f),s)){const u=f;s.push(()=>t.delete(u))}return e},set value(u){if(Object.is(u,e))return;const d=e;if(e=u,r._debugInfo&&r._debugInfo.updateCount++,b&&b(r,d,u),l>0)for(const c of t)g.add(c);else{let c;for(const v of[...t])try{v()}catch(C){c===void 0&&(c=C)}if(c!==void 0)throw c}},_subscribe(u){return t.add(u),()=>{t.delete(u)}},peek(){return e}};return _?(r._debugInfo={label:o,createdAt:Date.now(),updateCount:0,subs:t},_(r,o)):a.length<E&&a.push({ref:new WeakRef(r),label:o,createdAt:Date.now(),subs:t}),r}function I(n){const o=p(void 0);return h(()=>{o.value=n()}),{_t4:!0,get value(){return o.value},set value(e){throw new Error("[tina4] computed signals are read-only")},_subscribe(e){return o._subscribe(e)},peek(){return o.peek()}}}function h(n){let o=!1,e=[];const t=()=>{if(o)return;for(const c of e)c();e=[];const u=f,d=s;f=t,s=e;try{n()}finally{f=u,s=d}};t();const r=()=>{o=!0;for(const u of e)u();e=[]};return i&&i.push(r),r}function k(n){l++;try{n()}finally{if(l--,l===0){const o=[...g];g.clear();let e;for(const t of o)try{t()}catch(r){e===void 0&&(e=r)}if(e!==void 0)throw e}}}function A(n){return n!==null&&typeof n=="object"&&n._t4===!0}exports.__setDebugSignalHooks=y;exports._getEffectCollector=w;exports._setEffectCollector=S;exports.batch=k;exports.computed=I;exports.effect=h;exports.isSignal=A;exports.signal=p;
package/dist/signal.es.js CHANGED
@@ -1,125 +1,141 @@
1
- let c = null, f = null, i = null;
2
- function y(t) {
3
- i = t;
1
+ let f = null, c = null, i = null;
2
+ function C(n) {
3
+ i = n;
4
4
  }
5
- function C() {
5
+ function S() {
6
6
  return i;
7
7
  }
8
- let d = null, b = null;
9
- function w(t, n) {
10
- d = t, b = n;
8
+ let b = null, _ = null;
9
+ const a = [], v = 512;
10
+ function E(n, o) {
11
+ if (b = n, _ = o, n)
12
+ for (const e of a) {
13
+ const t = e.ref.deref();
14
+ t && (t._debugInfo || (t._debugInfo = {
15
+ label: e.label,
16
+ createdAt: e.createdAt,
17
+ updateCount: 0,
18
+ subs: e.subs
19
+ }), n(t, e.label));
20
+ }
21
+ a.length = 0;
11
22
  }
12
23
  let l = 0;
13
24
  const p = /* @__PURE__ */ new Set();
14
- function v(t, n) {
15
- let e = t;
16
- const r = /* @__PURE__ */ new Set(), u = {
25
+ function w(n, o) {
26
+ let e = n;
27
+ const t = /* @__PURE__ */ new Set(), r = {
17
28
  _t4: !0,
18
29
  get value() {
19
- if (c && (r.add(c), f)) {
20
- const o = c;
21
- f.push(() => r.delete(o));
30
+ if (f && (t.add(f), c)) {
31
+ const u = f;
32
+ c.push(() => t.delete(u));
22
33
  }
23
34
  return e;
24
35
  },
25
- set value(o) {
26
- if (Object.is(o, e)) return;
27
- const a = e;
28
- if (e = o, u._debugInfo && u._debugInfo.updateCount++, b && b(u, a, o), l > 0)
29
- for (const s of r) p.add(s);
36
+ set value(u) {
37
+ if (Object.is(u, e)) return;
38
+ const d = e;
39
+ if (e = u, r._debugInfo && r._debugInfo.updateCount++, _ && _(r, d, u), l > 0)
40
+ for (const s of t) p.add(s);
30
41
  else {
31
42
  let s;
32
- for (const _ of [...r])
43
+ for (const g of [...t])
33
44
  try {
34
- _();
35
- } catch (g) {
36
- s === void 0 && (s = g);
45
+ g();
46
+ } catch (h) {
47
+ s === void 0 && (s = h);
37
48
  }
38
49
  if (s !== void 0) throw s;
39
50
  }
40
51
  },
41
- _subscribe(o) {
42
- return r.add(o), () => {
43
- r.delete(o);
52
+ _subscribe(u) {
53
+ return t.add(u), () => {
54
+ t.delete(u);
44
55
  };
45
56
  },
46
57
  peek() {
47
58
  return e;
48
59
  }
49
60
  };
50
- return d && (u._debugInfo = { label: n, createdAt: Date.now(), updateCount: 0, subs: r }, d(u, n)), u;
61
+ return b ? (r._debugInfo = { label: o, createdAt: Date.now(), updateCount: 0, subs: t }, b(r, o)) : a.length < v && a.push({
62
+ ref: new WeakRef(r),
63
+ label: o,
64
+ createdAt: Date.now(),
65
+ subs: t
66
+ }), r;
51
67
  }
52
- function E(t) {
53
- const n = v(void 0);
54
- return h(() => {
55
- n.value = t();
68
+ function I(n) {
69
+ const o = w(void 0);
70
+ return y(() => {
71
+ o.value = n();
56
72
  }), {
57
73
  _t4: !0,
58
74
  get value() {
59
- return n.value;
75
+ return o.value;
60
76
  },
61
77
  set value(e) {
62
78
  throw new Error("[tina4] computed signals are read-only");
63
79
  },
64
80
  _subscribe(e) {
65
- return n._subscribe(e);
81
+ return o._subscribe(e);
66
82
  },
67
83
  peek() {
68
- return n.peek();
84
+ return o.peek();
69
85
  }
70
86
  };
71
87
  }
72
- function h(t) {
73
- let n = !1, e = [];
74
- const r = () => {
75
- if (n) return;
88
+ function y(n) {
89
+ let o = !1, e = [];
90
+ const t = () => {
91
+ if (o) return;
76
92
  for (const s of e) s();
77
93
  e = [];
78
- const o = c, a = f;
79
- c = r, f = e;
94
+ const u = f, d = c;
95
+ f = t, c = e;
80
96
  try {
81
- t();
97
+ n();
82
98
  } finally {
83
- c = o, f = a;
99
+ f = u, c = d;
84
100
  }
85
101
  };
86
- r();
87
- const u = () => {
88
- n = !0;
89
- for (const o of e) o();
102
+ t();
103
+ const r = () => {
104
+ o = !0;
105
+ for (const u of e) u();
90
106
  e = [];
91
107
  };
92
- return i && i.push(u), u;
108
+ return i && i.push(r), r;
93
109
  }
94
- function S(t) {
110
+ function A(n) {
95
111
  l++;
96
112
  try {
97
- t();
113
+ n();
98
114
  } finally {
99
115
  if (l--, l === 0) {
100
- const n = [...p];
116
+ const o = [...p];
101
117
  p.clear();
102
118
  let e;
103
- for (const r of n)
119
+ for (const t of o)
104
120
  try {
105
- r();
106
- } catch (u) {
107
- e === void 0 && (e = u);
121
+ t();
122
+ } catch (r) {
123
+ e === void 0 && (e = r);
108
124
  }
109
125
  if (e !== void 0) throw e;
110
126
  }
111
127
  }
112
128
  }
113
- function k(t) {
114
- return t !== null && typeof t == "object" && t._t4 === !0;
129
+ function k(n) {
130
+ return n !== null && typeof n == "object" && n._t4 === !0;
115
131
  }
116
132
  export {
117
- C as _,
118
- y as a,
119
- S as b,
120
- E as c,
121
- w as d,
122
- h as e,
133
+ S as _,
134
+ C as a,
135
+ A as b,
136
+ I as c,
137
+ E as d,
138
+ y as e,
123
139
  k as i,
124
- v as s
140
+ w as s
125
141
  };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * tina4js/storage — Persistent signals.
3
+ *
4
+ * Read STORAGE.md before using this module. localStorage is XSS-readable;
5
+ * never put credentials, tokens, personal data, or secrets here.
6
+ */
7
+ export { persist, clearPersistedKeys } from './persist';
8
+ export type { PersistOptions, PersistSerializer, PersistedSignal } from './persist';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * tina4-js/storage — Persistent signals.
3
+ *
4
+ * Wrap a signal with persist() to read its initial value from localStorage
5
+ * (or sessionStorage) and write every change back. The value survives a
6
+ * page refresh. Opt-in per signal. Zero dependencies.
7
+ *
8
+ * SAFE FOR: theme, language, sidebar state, last-used filters, onboarding
9
+ * flags, draft text the user expects back, guest cart contents.
10
+ *
11
+ * NEVER STORE: auth tokens, JWTs, session IDs, API keys, passwords, personal
12
+ * data, payment details, permission flags, secrets, OTP seeds,
13
+ * or any server-of-record state. localStorage is XSS-readable.
14
+ * See STORAGE.md for the full dangers list.
15
+ */
16
+ import { type Signal } from '../core/signal';
17
+ export interface PersistSerializer<T> {
18
+ /** Parse a stored string back into a value. */
19
+ read(raw: string): T;
20
+ /** Serialize a value into a string that storage can hold. */
21
+ write(value: T): string;
22
+ }
23
+ export interface PersistOptions<T> {
24
+ /** Storage key. Required. */
25
+ key: string;
26
+ /** 'local' (default) survives a refresh; 'session' lives until the tab closes. */
27
+ storage?: 'local' | 'session';
28
+ /** JSON by default. Provide for Date, Map, Set, or any non-JSON shape. */
29
+ serializer?: PersistSerializer<T>;
30
+ /** Stored-shape version. Defaults to 1. */
31
+ version?: number;
32
+ /** Convert an older stored value into the current shape. */
33
+ migrate?: (oldValue: unknown, oldVersion: number | undefined) => T;
34
+ /** Subscribe to the storage event so other tabs see writes. Opt-in. */
35
+ syncTabs?: boolean;
36
+ /**
37
+ * Silence the credential-shape warning. Use only when you are certain
38
+ * the key or value is a coincidence (e.g. tokenColor for a UI palette).
39
+ */
40
+ silenceCredentialWarning?: boolean;
41
+ }
42
+ export interface PersistedSignal<T> extends Signal<T> {
43
+ /** Remove the key from storage. The signal keeps its current in-memory value. */
44
+ clear(): void;
45
+ /** Stop watching storage events and stop the write effect. */
46
+ dispose(): void;
47
+ }
48
+ /** @internal Reset the warning cache. Tests only. */
49
+ export declare function _resetWarnedKeys(): void;
50
+ /**
51
+ * Wrap a signal so its value is read from storage on creation and written
52
+ * back on every change. Survives a page refresh.
53
+ *
54
+ * @param source - The signal to persist. Its initial value is overwritten
55
+ * by whatever is in storage, if anything.
56
+ * @param options - Persistence options. `key` is required.
57
+ *
58
+ * @example
59
+ * const theme = persist(signal('light'), { key: 'theme' });
60
+ * theme.value = 'dark'; // survives a refresh
61
+ */
62
+ export declare function persist<T>(source: Signal<T>, options: PersistOptions<T>): PersistedSignal<T>;
63
+ /**
64
+ * Remove a list of persisted keys at once. Wire this to your logout handler
65
+ * so persisted state does not leak to the next user on the device.
66
+ *
67
+ * @example
68
+ * function logout() {
69
+ * api.post('/auth/logout');
70
+ * clearPersistedKeys(['cart', 'lastFilter', 'draftReply']);
71
+ * window.location.reload();
72
+ * }
73
+ */
74
+ export declare function clearPersistedKeys(keys: string[], kind?: 'local' | 'session'): void;
75
+ //# sourceMappingURL=persist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persist.d.ts","sourceRoot":"","sources":["../../src/storage/persist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,+CAA+C;IAC/C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,0EAA0E;IAC1E,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAClC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,KAAK,CAAC,CAAC;IACnE,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,eAAe,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;IACnD,iFAAiF;IACjF,KAAK,IAAI,IAAI,CAAC;IACd,8DAA8D;IAC9D,OAAO,IAAI,IAAI,CAAC;CACjB;AAyDD,qDAAqD;AACrD,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAqBD;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CA+I5F;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,GAAE,OAAO,GAAG,SAAmB,GAAG,IAAI,CAW5F"}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const O=require("./signal.cjs.js"),p={read:t=>JSON.parse(t),write:t=>JSON.stringify(t)},w=/(token|password|passwd|secret|api[_-]?key|apikey|auth(?!or)|credential|jwt|bearer|otp|seed|private[_-]?key|session[_-]?id)/i,T=/^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}$/,N=/^[A-Za-z0-9+/_=-]{40,}$/,h=new Set;function S(t,r){if(w.test(t))return`key name "${t}" looks like a credential`;if(typeof r=="string"){if(T.test(r))return"value looks like a JWT";if(r.length>=40&&N.test(r))return"value looks like a long base64 / token"}if(r&&typeof r=="object"&&!Array.isArray(r)){for(const e of Object.keys(r))if(w.test(e))return`object contains a credential-shape field "${e}"`}return null}function b(t,r){h.has(r)||(h.add(r),console.warn(`[tina4 persist] ${t} (key: ${JSON.stringify(r)}). localStorage is XSS-readable and never appropriate for credentials, tokens, passwords, personal data, or secrets. See STORAGE.md.`))}function E(t){if(typeof globalThis>"u")return null;try{const r=t==="local"?globalThis.localStorage:globalThis.sessionStorage;return!r||typeof r.getItem!="function"?null:r}catch{return null}}function J(t,r){var k;const{key:e,storage:y="local",serializer:a=p,version:l=1,migrate:u,syncTabs:$=!1,silenceCredentialWarning:v=!1}=r;if(!e||typeof e!="string")throw new Error("[tina4 persist] options.key is required and must be a string");const f=E(y);if(!f)return m(t,()=>{},()=>{});try{const s=f.getItem(e);if(s!==null){let i,o;try{const n=JSON.parse(s);n&&typeof n=="object"&&"value"in n?(i=n.v,o=n.value):o=n}catch{o=a===p?s:a.read(s)}if(i===l||i===void 0){const n=a===p?o:a.read(typeof o=="string"?o:JSON.stringify(o));t.value=n}else if(u)try{t.value=u(o,i)}catch(n){console.warn(`[tina4 persist] migrate() threw for key "${e}":`,n)}else console.warn(`[tina4 persist] stored version ${i} does not match current ${l} for key "${e}", and no migrate() was provided. Discarding the stored value.`)}}catch(s){console.warn(`[tina4 persist] failed to read key "${e}":`,s)}if(!v){const s=S(e,t.peek());s&&b(s,e)}const A=O.effect(()=>{const s=t.value;if(!v){const i=S(e,s);i&&b(i,e)}try{const o=JSON.stringify(a===p?{v:l,value:s}:{v:l,value:a.write(s)});f.setItem(e,o)}catch(i){console.warn(`[tina4 persist] failed to write key "${e}":`,i)}});let g=null;if($&&typeof globalThis<"u"&&"addEventListener"in globalThis){const s=i=>{const o=i;if(o.storageArea===f&&o.key===e&&o.newValue!==null)try{const n=JSON.parse(o.newValue),c=n&&typeof n=="object"&&"v"in n?n.v:void 0,d=c!==void 0?n.value:n;c!==void 0&&c!==l&&u?t.value=u(d,c):(c===l||c===void 0)&&(t.value=a===p?d:a.read(typeof d=="string"?d:JSON.stringify(d)))}catch(n){console.warn(`[tina4 persist] failed to parse storage event for key "${e}":`,n)}};(k=globalThis.addEventListener)==null||k.call(globalThis,"storage",s),g=()=>{var i;(i=globalThis.removeEventListener)==null||i.call(globalThis,"storage",s)}}return m(t,()=>{try{f.removeItem(e)}catch(s){console.warn(`[tina4 persist] failed to clear key "${e}":`,s)}},()=>{A(),g&&g()})}function m(t,r,e){return Object.assign(t,{clear:r,dispose:e})}function _(t,r="local"){const e=E(r);if(e)for(const y of t)try{e.removeItem(y)}catch(a){console.warn(`[tina4 persist] failed to clear key "${y}":`,a)}}exports.clearPersistedKeys=_;exports.persist=J;
@@ -0,0 +1,144 @@
1
+ import { e as O } from "./signal.es.js";
2
+ const p = {
3
+ read: (t) => JSON.parse(t),
4
+ write: (t) => JSON.stringify(t)
5
+ }, w = /(token|password|passwd|secret|api[_-]?key|apikey|auth(?!or)|credential|jwt|bearer|otp|seed|private[_-]?key|session[_-]?id)/i, T = /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}$/, N = /^[A-Za-z0-9+/_=-]{40,}$/, h = /* @__PURE__ */ new Set();
6
+ function S(t, r) {
7
+ if (w.test(t))
8
+ return `key name "${t}" looks like a credential`;
9
+ if (typeof r == "string") {
10
+ if (T.test(r)) return "value looks like a JWT";
11
+ if (r.length >= 40 && N.test(r))
12
+ return "value looks like a long base64 / token";
13
+ }
14
+ if (r && typeof r == "object" && !Array.isArray(r)) {
15
+ for (const e of Object.keys(r))
16
+ if (w.test(e))
17
+ return `object contains a credential-shape field "${e}"`;
18
+ }
19
+ return null;
20
+ }
21
+ function b(t, r) {
22
+ h.has(r) || (h.add(r), console.warn(
23
+ `[tina4 persist] ${t} (key: ${JSON.stringify(r)}). localStorage is XSS-readable and never appropriate for credentials, tokens, passwords, personal data, or secrets. See STORAGE.md.`
24
+ ));
25
+ }
26
+ function E(t) {
27
+ if (typeof globalThis > "u") return null;
28
+ try {
29
+ const r = t === "local" ? globalThis.localStorage : globalThis.sessionStorage;
30
+ return !r || typeof r.getItem != "function" ? null : r;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function _(t, r) {
36
+ var k;
37
+ const {
38
+ key: e,
39
+ storage: y = "local",
40
+ serializer: a = p,
41
+ version: l = 1,
42
+ migrate: g,
43
+ syncTabs: $ = !1,
44
+ silenceCredentialWarning: v = !1
45
+ } = r;
46
+ if (!e || typeof e != "string")
47
+ throw new Error("[tina4 persist] options.key is required and must be a string");
48
+ const f = E(y);
49
+ if (!f)
50
+ return m(t, () => {
51
+ }, () => {
52
+ });
53
+ try {
54
+ const s = f.getItem(e);
55
+ if (s !== null) {
56
+ let o, i;
57
+ try {
58
+ const n = JSON.parse(s);
59
+ n && typeof n == "object" && "value" in n ? (o = n.v, i = n.value) : i = n;
60
+ } catch {
61
+ i = a === p ? s : a.read(s);
62
+ }
63
+ if (o === l || o === void 0) {
64
+ const n = a === p ? i : a.read(typeof i == "string" ? i : JSON.stringify(i));
65
+ t.value = n;
66
+ } else if (g)
67
+ try {
68
+ t.value = g(i, o);
69
+ } catch (n) {
70
+ console.warn(`[tina4 persist] migrate() threw for key "${e}":`, n);
71
+ }
72
+ else
73
+ console.warn(
74
+ `[tina4 persist] stored version ${o} does not match current ${l} for key "${e}", and no migrate() was provided. Discarding the stored value.`
75
+ );
76
+ }
77
+ } catch (s) {
78
+ console.warn(`[tina4 persist] failed to read key "${e}":`, s);
79
+ }
80
+ if (!v) {
81
+ const s = S(e, t.peek());
82
+ s && b(s, e);
83
+ }
84
+ const A = O(() => {
85
+ const s = t.value;
86
+ if (!v) {
87
+ const o = S(e, s);
88
+ o && b(o, e);
89
+ }
90
+ try {
91
+ const i = JSON.stringify(a === p ? { v: l, value: s } : { v: l, value: a.write(s) });
92
+ f.setItem(e, i);
93
+ } catch (o) {
94
+ console.warn(`[tina4 persist] failed to write key "${e}":`, o);
95
+ }
96
+ });
97
+ let u = null;
98
+ if ($ && typeof globalThis < "u" && "addEventListener" in globalThis) {
99
+ const s = (o) => {
100
+ const i = o;
101
+ if (i.storageArea === f && i.key === e && i.newValue !== null)
102
+ try {
103
+ const n = JSON.parse(i.newValue), c = n && typeof n == "object" && "v" in n ? n.v : void 0, d = c !== void 0 ? n.value : n;
104
+ c !== void 0 && c !== l && g ? t.value = g(d, c) : (c === l || c === void 0) && (t.value = a === p ? d : a.read(typeof d == "string" ? d : JSON.stringify(d)));
105
+ } catch (n) {
106
+ console.warn(`[tina4 persist] failed to parse storage event for key "${e}":`, n);
107
+ }
108
+ };
109
+ (k = globalThis.addEventListener) == null || k.call(globalThis, "storage", s), u = () => {
110
+ var o;
111
+ (o = globalThis.removeEventListener) == null || o.call(globalThis, "storage", s);
112
+ };
113
+ }
114
+ return m(
115
+ t,
116
+ () => {
117
+ try {
118
+ f.removeItem(e);
119
+ } catch (s) {
120
+ console.warn(`[tina4 persist] failed to clear key "${e}":`, s);
121
+ }
122
+ },
123
+ () => {
124
+ A(), u && u();
125
+ }
126
+ );
127
+ }
128
+ function m(t, r, e) {
129
+ return Object.assign(t, { clear: r, dispose: e });
130
+ }
131
+ function L(t, r = "local") {
132
+ const e = E(r);
133
+ if (e)
134
+ for (const y of t)
135
+ try {
136
+ e.removeItem(y);
137
+ } catch (a) {
138
+ console.warn(`[tina4 persist] failed to clear key "${y}":`, a);
139
+ }
140
+ }
141
+ export {
142
+ L as clearPersistedKeys,
143
+ _ as persist
144
+ };
package/dist/tina4.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./signal.cjs.js"),r=require("./core.cjs.js"),i=require("./component.cjs.js"),t=require("./index.cjs.js"),s=require("./api.cjs.js"),n=require("./pwa.cjs.js"),o=require("./ws.cjs.js"),a=require("./sse.cjs.js");exports.batch=e.batch;exports.computed=e.computed;exports.effect=e.effect;exports.isSignal=e.isSignal;exports.signal=e.signal;exports.html=r.html;exports.Tina4Element=i.Tina4Element;exports.navigate=t.navigate;exports.route=t.route;exports.router=t.router;exports.api=s.api;exports.pwa=n.pwa;exports.ws=o.ws;exports.sse=a.sse;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./signal.cjs.js"),s=require("./core.cjs.js"),i=require("./component.cjs.js"),r=require("./index.cjs.js"),a=require("./api.cjs.js"),n=require("./pwa.cjs.js"),o=require("./ws.cjs.js"),c=require("./sse.cjs.js"),t=require("./storage.cjs.js");exports.batch=e.batch;exports.computed=e.computed;exports.effect=e.effect;exports.isSignal=e.isSignal;exports.signal=e.signal;exports.html=s.html;exports.Tina4Element=i.Tina4Element;exports.navigate=r.navigate;exports.route=r.route;exports.router=r.router;exports.api=a.api;exports.pwa=n.pwa;exports.ws=o.ws;exports.sse=c.sse;exports.clearPersistedKeys=t.clearPersistedKeys;exports.persist=t.persist;
package/dist/tina4.es.js CHANGED
@@ -1,24 +1,27 @@
1
- import { b as a, c as e, e as t, i as s, s as m } from "./signal.es.js";
1
+ import { b as o, c as s, e as a, i as t, s as p } from "./signal.es.js";
2
2
  import { html as f } from "./core.es.js";
3
- import { T as i } from "./component.es.js";
3
+ import { T as x } from "./component.es.js";
4
4
  import { n as c, r as l, a as g } from "./index.es.js";
5
5
  import { api as b } from "./api.es.js";
6
- import { pwa as w } from "./pwa.es.js";
7
- import { ws as d } from "./ws.es.js";
8
- import { sse as E } from "./sse.es.js";
6
+ import { pwa as h } from "./pwa.es.js";
7
+ import { ws as T } from "./ws.es.js";
8
+ import { sse as y } from "./sse.es.js";
9
+ import { clearPersistedKeys as K, persist as P } from "./storage.es.js";
9
10
  export {
10
- i as Tina4Element,
11
+ x as Tina4Element,
11
12
  b as api,
12
- a as batch,
13
- e as computed,
14
- t as effect,
13
+ o as batch,
14
+ K as clearPersistedKeys,
15
+ s as computed,
16
+ a as effect,
15
17
  f as html,
16
- s as isSignal,
18
+ t as isSignal,
17
19
  c as navigate,
18
- w as pwa,
20
+ P as persist,
21
+ h as pwa,
19
22
  l as route,
20
23
  g as router,
21
- m as signal,
22
- E as sse,
23
- d as ws
24
+ p as signal,
25
+ y as sse,
26
+ T as ws
24
27
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4js",
3
- "version": "1.2.2",
3
+ "version": "1.2.7",
4
4
  "description": "1.5KB core gzipped, reactive framework — signals, web components, routing, PWA",
5
5
  "type": "module",
6
6
  "main": "dist/tina4.cjs.js",
@@ -39,6 +39,10 @@
39
39
  "./sse": {
40
40
  "import": "./dist/sse.es.js",
41
41
  "types": "./dist/sse/index.d.ts"
42
+ },
43
+ "./storage": {
44
+ "import": "./dist/storage.es.js",
45
+ "types": "./dist/storage/index.d.ts"
42
46
  }
43
47
  },
44
48
  "sideEffects": false,
package/readme.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  <p align="center">
13
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-265%20passing-brightgreen" alt="Tests">
14
+ <img src="https://img.shields.io/badge/tests-303%20passing-brightgreen" alt="Tests">
15
15
  <img src="https://img.shields.io/badge/core-1.5KB%20gzipped-blue" alt="Core size">
16
16
  <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="Zero Deps">
17
17
  <a href="https://tina4.com/js"><img src="https://img.shields.io/badge/docs-tina4.com%2Fjs-7b1fa2" alt="Docs"></a>
@@ -58,9 +58,11 @@ Every module is built from scratch -- no node_modules bloat, no third-party runt
58
58
  | **API** | 1.49 KB | Fetch client with auth (Bearer + formToken + FreshToken rotation), interceptors, per-request headers/params |
59
59
  | **WebSocket** | 0.91 KB | Signal-driven status, auto-reconnect with exponential backoff, pipe() to signal, JSON auto-parse |
60
60
  | **PWA** | 1.16 KB | Service worker + manifest generation, cache strategies (network-first, cache-first, stale-while-revalidate) |
61
+ | **SSE** | 1.33 KB | Server-Sent Events and NDJSON streaming, dual-mode (EventSource + fetch), reactive status signal |
62
+ | **Storage** | 1.67 KB | `persist()` wrapper for signals — survives a page refresh, opt-in per signal, SSR-safe, with loud warnings on credential-shaped keys |
61
63
  | **Debug** | 5.11 KB | Dev overlay (Ctrl+Shift+D) -- signals, components, routes, API panels |
62
64
 
63
- **238 tests across 10 test files. Zero dependencies. Under 3KB for the full core.**
65
+ **303 tests across 14 test files. Zero dependencies. Under 3KB for the full core.**
64
66
 
65
67
  For full documentation visit **[tina4.com/javascript](https://tina4.com/js)**.
66
68