tina4js 1.0.6 → 1.0.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.
Files changed (54) hide show
  1. package/TINA4.md +189 -19
  2. package/dist/api/fetch.d.ts +44 -0
  3. package/dist/api/fetch.d.ts.map +1 -0
  4. package/dist/api/index.d.ts +6 -0
  5. package/dist/api/index.d.ts.map +1 -0
  6. package/dist/api.cjs.js +1 -1
  7. package/dist/api.es.js +62 -52
  8. package/dist/core/component.d.ts +59 -0
  9. package/dist/core/component.d.ts.map +1 -0
  10. package/dist/core/html.d.ts +8 -0
  11. package/dist/core/html.d.ts.map +1 -0
  12. package/dist/core/index.d.ts +9 -0
  13. package/dist/core/index.d.ts.map +1 -0
  14. package/dist/core/signal.d.ts +42 -0
  15. package/dist/core/signal.d.ts.map +1 -0
  16. package/dist/debug/index.d.ts +17 -0
  17. package/dist/debug/index.d.ts.map +1 -0
  18. package/dist/debug/overlay.d.ts +25 -0
  19. package/dist/debug/overlay.d.ts.map +1 -0
  20. package/dist/debug/panels/api.d.ts +5 -0
  21. package/dist/debug/panels/api.d.ts.map +1 -0
  22. package/dist/debug/panels/components.d.ts +5 -0
  23. package/dist/debug/panels/components.d.ts.map +1 -0
  24. package/dist/debug/panels/routes.d.ts +5 -0
  25. package/dist/debug/panels/routes.d.ts.map +1 -0
  26. package/dist/debug/panels/signals.d.ts +5 -0
  27. package/dist/debug/panels/signals.d.ts.map +1 -0
  28. package/dist/debug/styles.d.ts +5 -0
  29. package/dist/debug/styles.d.ts.map +1 -0
  30. package/dist/debug/trackers.d.ts +89 -0
  31. package/dist/debug/trackers.d.ts.map +1 -0
  32. package/dist/debug.cjs.js +6 -6
  33. package/dist/debug.es.js +16 -20
  34. package/dist/index.d.ts +19 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/pwa/index.d.ts +6 -0
  37. package/dist/pwa/index.d.ts.map +1 -0
  38. package/dist/pwa/pwa.d.ts +24 -0
  39. package/dist/pwa/pwa.d.ts.map +1 -0
  40. package/dist/router/index.d.ts +6 -0
  41. package/dist/router/index.d.ts.map +1 -0
  42. package/dist/router/router.d.ts +42 -0
  43. package/dist/router/router.d.ts.map +1 -0
  44. package/dist/signal.cjs.js +1 -1
  45. package/dist/signal.es.js +60 -45
  46. package/dist/tina4.cjs.js +1 -1
  47. package/dist/tina4.es.js +10 -8
  48. package/dist/ws/index.d.ts +6 -0
  49. package/dist/ws/index.d.ts.map +1 -0
  50. package/dist/ws/ws.d.ts +62 -0
  51. package/dist/ws/ws.d.ts.map +1 -0
  52. package/dist/ws.cjs.js +1 -0
  53. package/dist/ws.es.js +89 -0
  54. package/package.json +8 -3
package/TINA4.md CHANGED
@@ -15,6 +15,8 @@
15
15
  | Navigate | `navigate('/path')` |
16
16
  | API | `await api.get('/path')`, `.post`, `.put`, `.patch`, `.delete` |
17
17
  | PWA | `pwa.register({ name, themeColor, cacheStrategy })` |
18
+ | WebSocket | `ws.connect('wss://...', { reconnect: true })` — signals for status/messages |
19
+ | Debug | `import 'tina4js/debug'` — toggle with Ctrl+Shift+D |
18
20
 
19
21
  ## File Conventions
20
22
 
@@ -36,6 +38,8 @@
36
38
  8. Use `static props = { name: String }` for component attributes
37
39
  9. Use `` static styles = `css` `` for scoped styles (Shadow DOM)
38
40
  10. Use `static shadow = false` for light DOM components
41
+ 11. `route(pattern, handler)` — pattern is ALWAYS the first argument, handler/config is second
42
+ 12. `api.configure()` must be called before any API calls if you need auth or a base URL
39
43
 
40
44
  ## Signal Patterns
41
45
 
@@ -43,12 +47,18 @@
43
47
  // Create
44
48
  const count = signal(0);
45
49
 
50
+ // Create with debug label (shows in debug overlay)
51
+ const count = signal(0, 'count');
52
+
46
53
  // Read
47
54
  count.value; // 0
48
55
 
49
56
  // Write (triggers DOM updates)
50
57
  count.value = 5;
51
58
 
59
+ // Read without subscribing
60
+ count.peek(); // 5
61
+
52
62
  // Derived (auto-updates)
53
63
  const doubled = computed(() => count.value * 2);
54
64
 
@@ -58,6 +68,9 @@ effect(() => console.log(count.value));
58
68
  // Batch multiple updates
59
69
  batch(() => { a.value = 1; b.value = 2; }); // one notification
60
70
 
71
+ // Check if something is a signal
72
+ isSignal(count); // true
73
+
61
74
  // In templates — signals interpolate directly
62
75
  html`<span>${count}</span>`; // auto-updates when count changes
63
76
  ```
@@ -98,13 +111,32 @@ customElements.define('my-widget', MyWidget);
98
111
  ```ts
99
112
  import { route, router, navigate, html } from 'tina4js';
100
113
 
114
+ // Simple route
101
115
  route('/', () => html`<h1>Home</h1>`);
116
+
117
+ // Route with params
102
118
  route('/user/{id}', ({ id }) => html`<h1>User ${id}</h1>`);
103
- route('/admin', { guard: () => isLoggedIn() || '/login', handler: () => html`<h1>Admin</h1>` });
119
+
120
+ // Route with guard (returns false to block, or string to redirect)
121
+ route('/admin', {
122
+ guard: () => isLoggedIn() || '/login',
123
+ handler: () => html`<h1>Admin</h1>`
124
+ });
125
+
126
+ // Catch-all (must be last)
104
127
  route('*', () => html`<h1>404</h1>`);
105
128
 
129
+ // Start router — call AFTER all routes are registered
106
130
  router.start({ target: '#root', mode: 'hash' }); // or mode: 'history'
131
+
132
+ // Programmatic navigation
107
133
  navigate('/user/42');
134
+ navigate('/login', { replace: true }); // replace history entry
135
+
136
+ // Listen for route changes
137
+ router.on('change', (event) => {
138
+ // event: { path, params, pattern, durationMs }
139
+ });
108
140
  ```
109
141
 
110
142
  ## API Pattern (tina4-php/python compatible)
@@ -112,17 +144,69 @@ navigate('/user/42');
112
144
  ```ts
113
145
  import { api } from 'tina4js';
114
146
 
115
- api.configure({ baseUrl: '/api', auth: true });
147
+ // Configure call before making requests
148
+ api.configure({
149
+ baseUrl: '/api',
150
+ auth: true, // enables Bearer token + formToken
151
+ tokenKey: 'tina4_token', // localStorage key (default)
152
+ headers: { 'X-API-Key': 'abc' }, // default headers on every request
153
+ });
154
+
155
+ // GET with query params
156
+ const users = await api.get('/users', { params: { page: 1, limit: 20 } });
157
+ // => GET /api/users?page=1&limit=20
158
+
159
+ // GET with custom headers
160
+ const data = await api.get('/data', { headers: { 'Accept-Language': 'en' } });
116
161
 
117
- const users = await api.get('/users');
118
- const user = await api.get('/users/{id}', { id: 42 });
162
+ // POST with body
119
163
  await api.post('/users', { name: 'Andre' });
164
+
165
+ // POST with body + custom headers
166
+ await api.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } });
167
+
168
+ // PUT, PATCH, DELETE
120
169
  await api.put('/users/42', { name: 'Updated' });
170
+ await api.patch('/users/42', { active: false });
121
171
  await api.delete('/users/42');
122
172
 
123
- // Interceptors
124
- api.intercept('request', (config) => { config.headers['X-Custom'] = 'val'; return config; });
125
- api.intercept('response', (res) => { if (res.status === 401) navigate('/login'); return res; });
173
+ // DELETE with query params
174
+ await api.delete('/users/42', { params: { permanent: true } });
175
+
176
+ // Error handling — non-2xx throws an ApiResponse object
177
+ try {
178
+ await api.get('/protected');
179
+ } catch (err) {
180
+ // err: { status: 401, data: {...}, ok: false, headers: Headers }
181
+ }
182
+
183
+ // Interceptors — modify every request/response
184
+ api.intercept('request', (config) => {
185
+ config.headers['X-Custom'] = 'val';
186
+ return config;
187
+ });
188
+
189
+ api.intercept('response', (res) => {
190
+ if (res.status === 401) navigate('/login');
191
+ return res;
192
+ });
193
+ ```
194
+
195
+ ### RequestOptions
196
+
197
+ All API methods accept an optional `RequestOptions` object:
198
+
199
+ ```ts
200
+ interface RequestOptions {
201
+ headers?: Record<string, string>; // per-request headers
202
+ params?: Record<string, string | number | boolean>; // query string params
203
+ }
204
+
205
+ api.get(path, options?)
206
+ api.post(path, body?, options?)
207
+ api.put(path, body?, options?)
208
+ api.patch(path, body?, options?)
209
+ api.delete(path, options?)
126
210
  ```
127
211
 
128
212
  ## Conditional & List Rendering
@@ -156,6 +240,58 @@ tina4 build --target python
156
240
  - `formToken` injected into POST/PUT/PATCH/DELETE body
157
241
  - Token auto-rotated via `FreshToken` response header
158
242
 
243
+ ## WebSocket (Signal-Driven)
244
+
245
+ ```ts
246
+ import { ws } from 'tina4js/ws';
247
+ import { signal } from 'tina4js';
248
+
249
+ // Connect with auto-reconnect
250
+ const socket = ws.connect('wss://api.example.com/live', {
251
+ reconnect: true, // auto-reconnect (default: true)
252
+ reconnectDelay: 1000, // initial delay ms (default: 1000)
253
+ reconnectMaxDelay: 30000, // max backoff ms (default: 30000)
254
+ reconnectAttempts: Infinity, // max attempts (default: Infinity)
255
+ });
256
+
257
+ // Reactive state — all signals
258
+ socket.status.value; // 'connecting' | 'open' | 'closed' | 'reconnecting'
259
+ socket.connected.value; // boolean
260
+ socket.lastMessage.value; // last parsed message
261
+ socket.error.value; // last error or null
262
+ socket.reconnectCount.value; // number of reconnect attempts
263
+
264
+ // Send — objects auto-stringify
265
+ socket.send({ type: 'chat', text: 'hello' });
266
+ socket.send('raw string');
267
+
268
+ // Listen for events
269
+ socket.on('message', (data) => { /* parsed JSON or string */ });
270
+ socket.on('open', () => { });
271
+ socket.on('close', (code, reason) => { });
272
+ socket.on('error', (err) => { });
273
+
274
+ // Pipe messages directly into a signal
275
+ const messages = signal<ChatMessage[]>([]);
276
+ socket.pipe(messages, (msg, current) => [...current, msg as ChatMessage]);
277
+
278
+ // Disconnect (stops reconnect)
279
+ socket.close();
280
+ ```
281
+
282
+ ### Real-time UI with Signals
283
+
284
+ ```ts
285
+ // Chat example — messages auto-render in the template
286
+ const chatLog = signal<{user: string, text: string}[]>([]);
287
+ const socket = ws.connect('wss://chat.example.com');
288
+ socket.pipe(chatLog, (msg, log) => [...log, msg as any]);
289
+
290
+ route('/chat', () => html`
291
+ <ul>${() => chatLog.value.map(m => html`<li><b>${m.user}</b>: ${m.text}</li>`)}</ul>
292
+ `);
293
+ ```
294
+
159
295
  ## Debug Overlay
160
296
 
161
297
  ```ts
@@ -167,18 +303,21 @@ if (import.meta.env.DEV) import('tina4js/debug');
167
303
  ```
168
304
 
169
305
  - Toggle with **Ctrl+Shift+D**
170
- - Shows: live signal values + subscriber counts, mounted components, route navigation history with timing, API request/response log
306
+ - Panels: live signal values + subscriber counts, mounted components, route navigation history with timing, API request/response log
171
307
  - Zero cost in production when using the `import.meta.env.DEV` guard (tree-shaken by Vite/Rollup)
308
+ - Signals can have debug labels: `signal(0, 'count')` — shows in the signals panel
172
309
 
173
310
  ## Framework Size
174
311
 
175
312
  | Module | Gzipped |
176
313
  |--------|---------|
177
- | Core (signals + html + component) | 2.44 KB |
178
- | Router | 1.13 KB |
179
- | API | 0.82 KB |
180
- | PWA | 1.13 KB |
181
- | **Total (all modules)** | **5.52 KB** |
314
+ | Core (signals + html + component) | ~1.5 KB |
315
+ | Router | ~0.12 KB |
316
+ | API | ~0.97 KB |
317
+ | WebSocket | ~0.91 KB |
318
+ | PWA | ~1.16 KB |
319
+ | Debug overlay | ~5.1 KB |
320
+ | **Total (core modules)** | **~4.66 KB** |
182
321
 
183
322
  ## Architecture
184
323
 
@@ -186,15 +325,46 @@ if (import.meta.env.DEV) import('tina4js/debug');
186
325
  src/
187
326
  core/ signal.ts, html.ts, component.ts — reactive primitives
188
327
  router/ router.ts — client-side routing
189
- api/ fetch.ts — HTTP with auth
328
+ api/ fetch.ts — HTTP with auth + headers
329
+ ws/ ws.ts — WebSocket with auto-reconnect
190
330
  pwa/ pwa.ts — service worker + manifest
331
+ debug/ overlay, trackers, panels — dev debug overlay
191
332
  index.ts barrel re-export
192
333
  ```
193
334
 
194
- Tree-shakeable: import only what you need.
335
+ ### Exports Map
336
+
337
+ ```ts
338
+ import { signal, html, computed, effect, batch, isSignal, Tina4Element } from 'tina4js';
339
+ import { route, router, navigate } from 'tina4js';
340
+ import { api } from 'tina4js';
341
+ import { pwa } from 'tina4js';
342
+
343
+ // Or import individual modules (tree-shakeable):
344
+ import { signal, html } from 'tina4js/core';
345
+ import { route, router } from 'tina4js/router';
346
+ import { api } from 'tina4js/api';
347
+ import { pwa } from 'tina4js/pwa';
348
+ import { ws } from 'tina4js/ws';
349
+ import 'tina4js/debug';
350
+ ```
351
+
352
+ ### TypeScript Types
353
+
195
354
  ```ts
196
- import { signal, html } from 'tina4js/core'; // 2.44 KB
197
- import { route, router } from 'tina4js/router'; // 1.13 KB
198
- import { api } from 'tina4js/api'; // 0.82 KB
199
- import { pwa } from 'tina4js/pwa'; // 1.13 KB
355
+ import type { Signal, ReadonlySignal } from 'tina4js';
356
+ import type { RouteParams, RouteHandler, RouteGuard, RouteConfig } from 'tina4js';
357
+ import type { ApiConfig, ApiResponse, RequestOptions } from 'tina4js';
358
+ import type { SocketStatus, SocketOptions, ManagedSocket } from 'tina4js';
359
+ import type { PWAConfig } from 'tina4js';
360
+ import type { PropType } from 'tina4js';
200
361
  ```
362
+
363
+ ## Common Mistakes to Avoid
364
+
365
+ 1. **Reversed route arguments**: `route('/path', handler)` NOT `route(handler, '/path')`
366
+ 2. **Forgetting `.value`**: `count.value++` NOT `count++`
367
+ 3. **Calling `router.start()` before routes**: Register all routes first, then start
368
+ 4. **Not returning from `render()`**: Always `return html\`...\``
369
+ 5. **Missing `api.configure()`**: Must configure before making authenticated requests
370
+ 6. **Using `api.get('/users/{id}', { id: 42 })`**: Changed to `api.get('/users/42')` — use params for query strings: `api.get('/users', { params: { id: 42 } })`
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Tina4 API — Fetch wrapper with auth token management.
3
+ *
4
+ * Compatible with tina4-php and tina4-python backends:
5
+ * - Sends Authorization: Bearer <token>
6
+ * - Reads FreshToken response header for token rotation
7
+ * - Sends formToken in POST/PUT/PATCH/DELETE bodies
8
+ */
9
+ export interface ApiConfig {
10
+ baseUrl: string;
11
+ auth: boolean;
12
+ tokenKey: string;
13
+ headers: Record<string, string>;
14
+ }
15
+ export interface ApiResponse<T = unknown> {
16
+ status: number;
17
+ data: T;
18
+ ok: boolean;
19
+ headers: Headers;
20
+ /** @internal Used by debug tracker for request/response correlation. */
21
+ _requestId?: number;
22
+ }
23
+ export type RequestInterceptor = (config: RequestInit & {
24
+ headers: Record<string, string>;
25
+ }) => (RequestInit & {
26
+ headers: Record<string, string>;
27
+ }) | void;
28
+ export type ResponseInterceptor = (response: ApiResponse) => ApiResponse | void;
29
+ export interface RequestOptions {
30
+ headers?: Record<string, string>;
31
+ params?: Record<string, string | number | boolean>;
32
+ }
33
+ export declare const api: {
34
+ configure(c: Partial<ApiConfig>): void;
35
+ get<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
36
+ post<T = unknown>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
37
+ put<T = unknown>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
38
+ patch<T = unknown>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
39
+ delete<T = unknown>(path: string, options?: RequestOptions): Promise<T>;
40
+ intercept(type: "request" | "response", fn: RequestInterceptor | ResponseInterceptor): void;
41
+ /** @internal Reset state (for tests). */
42
+ _reset(): void;
43
+ };
44
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/api/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,CAAC;IACR,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,KAAK,CAAC,WAAW,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,GAAG,IAAI,CAAC;AAC3J,MAAM,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,WAAW,KAAK,WAAW,GAAG,IAAI,CAAC;AA+BhF,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACpD;AA+FD,eAAO,MAAM,GAAG;iBACD,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI;QAIlC,CAAC,kBAAkB,MAAM,YAAY,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;SAI/D,CAAC,kBAAkB,MAAM,SAAS,OAAO,YAAY,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;QAIjF,CAAC,kBAAkB,MAAM,SAAS,OAAO,YAAY,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;UAI9E,CAAC,kBAAkB,MAAM,SAAS,OAAO,YAAY,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;WAI/E,CAAC,kBAAkB,MAAM,YAAY,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC;oBAIvD,SAAS,GAAG,UAAU,MAAM,kBAAkB,GAAG,mBAAmB,GAAG,IAAI;IAQ3F,yCAAyC;cAC/B,IAAI;CAQf,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * tina4js/api — Fetch wrapper with auth token management.
3
+ */
4
+ export { api } from './fetch';
5
+ export type { ApiConfig, ApiResponse, RequestOptions, RequestInterceptor, ResponseInterceptor } from './fetch';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC"}
package/dist/api.cjs.js CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o={baseUrl:"",auth:!1,tokenKey:"tina4_token"},f=[],h=[];function g(){try{return localStorage.getItem(o.tokenKey)}catch{return null}}function y(e){try{localStorage.setItem(o.tokenKey,e)}catch{}}async function i(e,t,c){let a={method:e,headers:{"Content-Type":"application/json"}};if(o.auth){const n=g();n&&(a.headers.Authorization=`Bearer ${n}`)}if(c!==void 0&&e!=="GET"){let n=typeof c=="object"&&c!==null?{...c}:c;if(o.auth&&typeof n=="object"&&n!==null){const s=g();s&&(n.formToken=s)}a.body=JSON.stringify(n)}for(const n of f){const s=n(a);s&&(a=s)}const k=o.baseUrl+t,r=await fetch(k,a),p=r.headers.get("FreshToken");p&&y(p);const T=r.headers.get("Content-Type")??"";let l;T.includes("json")?l=await r.json():l=await r.text();let u={status:r.status,data:l,ok:r.ok,headers:r.headers};for(const n of h){const s=n(u);s&&(u=s)}if(!r.ok)throw u;return u.data}const d={configure(e){Object.assign(o,e)},get(e,t){return t&&(e=e.replace(/\{(\w+)\}/g,(c,a)=>String(t[a]??""))),i("GET",e)},post(e,t){return i("POST",e,t)},put(e,t){return i("PUT",e,t)},patch(e,t){return i("PATCH",e,t)},delete(e){return i("DELETE",e)},intercept(e,t){e==="request"?f.push(t):h.push(t)},_reset(){o.baseUrl="",o.auth=!1,o.tokenKey="tina4_token",f.length=0,h.length=0}};exports.api=d;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s={baseUrl:"",auth:!1,tokenKey:"tina4_token",headers:{}},d=[],h=[];let j=0;function T(){try{return localStorage.getItem(s.tokenKey)}catch{return null}}function I(e){try{localStorage.setItem(s.tokenKey,e)}catch{}}async function i(e,t,n,c){let o={method:e,headers:{"Content-Type":"application/json",...s.headers}};if(s.auth){const r=T();r&&(o.headers.Authorization=`Bearer ${r}`)}if(n!==void 0&&e!=="GET"){let r=typeof n=="object"&&n!==null?{...n}:n;if(s.auth&&typeof r=="object"&&r!==null){const a=T();a&&(r.formToken=a)}o.body=JSON.stringify(r)}if(c!=null&&c.headers&&Object.assign(o.headers,c.headers),c!=null&&c.params){const r=Object.entries(c.params).map(([a,y])=>`${encodeURIComponent(a)}=${encodeURIComponent(String(y))}`).join("&");t+=(t.includes("?")?"&":"?")+r}const g=s.baseUrl+t;o._url=g,o._requestId=++j;for(const r of d){const a=r(o);a&&(o=a)}const u=await fetch(g,o),k=u.headers.get("FreshToken");k&&I(k);const p=u.headers.get("Content-Type")??"";let f;p.includes("json")?f=await u.json():f=await u.text();let l={status:u.status,data:f,ok:u.ok,headers:u.headers,_requestId:o._requestId};for(const r of h){const a=r(l);a&&(l=a)}if(!u.ok)throw l;return l.data}const q={configure(e){Object.assign(s,e)},get(e,t){return i("GET",e,void 0,t)},post(e,t,n){return i("POST",e,t,n)},put(e,t,n){return i("PUT",e,t,n)},patch(e,t,n){return i("PATCH",e,t,n)},delete(e,t){return i("DELETE",e,void 0,t)},intercept(e,t){e==="request"?d.push(t):h.push(t)},_reset(){s.baseUrl="",s.auth=!1,s.tokenKey="tina4_token",s.headers={},d.length=0,h.length=0}};exports.api=q;
package/dist/api.es.js CHANGED
@@ -1,90 +1,100 @@
1
- const o = {
1
+ const s = {
2
2
  baseUrl: "",
3
3
  auth: !1,
4
- tokenKey: "tina4_token"
5
- }, l = [], h = [];
6
- function g() {
4
+ tokenKey: "tina4_token",
5
+ headers: {}
6
+ }, h = [], d = [];
7
+ let I = 0;
8
+ function T() {
7
9
  try {
8
- return localStorage.getItem(o.tokenKey);
10
+ return localStorage.getItem(s.tokenKey);
9
11
  } catch {
10
12
  return null;
11
13
  }
12
14
  }
13
- function y(e) {
15
+ function j(e) {
14
16
  try {
15
- localStorage.setItem(o.tokenKey, e);
17
+ localStorage.setItem(s.tokenKey, e);
16
18
  } catch {
17
19
  }
18
20
  }
19
- async function i(e, t, c) {
20
- let a = {
21
+ async function l(e, t, r, c) {
22
+ let o = {
21
23
  method: e,
22
24
  headers: {
23
- "Content-Type": "application/json"
25
+ "Content-Type": "application/json",
26
+ ...s.headers
24
27
  }
25
28
  };
26
- if (o.auth) {
27
- const n = g();
28
- n && (a.headers.Authorization = `Bearer ${n}`);
29
+ if (s.auth) {
30
+ const n = T();
31
+ n && (o.headers.Authorization = `Bearer ${n}`);
29
32
  }
30
- if (c !== void 0 && e !== "GET") {
31
- let n = typeof c == "object" && c !== null ? { ...c } : c;
32
- if (o.auth && typeof n == "object" && n !== null) {
33
- const s = g();
34
- s && (n.formToken = s);
33
+ if (r !== void 0 && e !== "GET") {
34
+ let n = typeof r == "object" && r !== null ? { ...r } : r;
35
+ if (s.auth && typeof n == "object" && n !== null) {
36
+ const a = T();
37
+ a && (n.formToken = a);
35
38
  }
36
- a.body = JSON.stringify(n);
39
+ o.body = JSON.stringify(n);
37
40
  }
38
- for (const n of l) {
39
- const s = n(a);
40
- s && (a = s);
41
+ if (c != null && c.headers && Object.assign(o.headers, c.headers), c != null && c.params) {
42
+ const n = Object.entries(c.params).map(([a, y]) => `${encodeURIComponent(a)}=${encodeURIComponent(String(y))}`).join("&");
43
+ t += (t.includes("?") ? "&" : "?") + n;
41
44
  }
42
- const k = o.baseUrl + t, r = await fetch(k, a), p = r.headers.get("FreshToken");
43
- p && y(p);
44
- const T = r.headers.get("Content-Type") ?? "";
45
- let f;
46
- T.includes("json") ? f = await r.json() : f = await r.text();
47
- let u = {
48
- status: r.status,
49
- data: f,
50
- ok: r.ok,
51
- headers: r.headers
52
- };
45
+ const g = s.baseUrl + t;
46
+ o._url = g, o._requestId = ++I;
53
47
  for (const n of h) {
54
- const s = n(u);
55
- s && (u = s);
48
+ const a = n(o);
49
+ a && (o = a);
50
+ }
51
+ const u = await fetch(g, o), k = u.headers.get("FreshToken");
52
+ k && j(k);
53
+ const p = u.headers.get("Content-Type") ?? "";
54
+ let i;
55
+ p.includes("json") ? i = await u.json() : i = await u.text();
56
+ let f = {
57
+ status: u.status,
58
+ data: i,
59
+ ok: u.ok,
60
+ headers: u.headers,
61
+ _requestId: o._requestId
62
+ };
63
+ for (const n of d) {
64
+ const a = n(f);
65
+ a && (f = a);
56
66
  }
57
- if (!r.ok)
58
- throw u;
59
- return u.data;
67
+ if (!u.ok)
68
+ throw f;
69
+ return f.data;
60
70
  }
61
- const d = {
71
+ const q = {
62
72
  configure(e) {
63
- Object.assign(o, e);
73
+ Object.assign(s, e);
64
74
  },
65
75
  get(e, t) {
66
- return t && (e = e.replace(/\{(\w+)\}/g, (c, a) => String(t[a] ?? ""))), i("GET", e);
76
+ return l("GET", e, void 0, t);
67
77
  },
68
- post(e, t) {
69
- return i("POST", e, t);
78
+ post(e, t, r) {
79
+ return l("POST", e, t, r);
70
80
  },
71
- put(e, t) {
72
- return i("PUT", e, t);
81
+ put(e, t, r) {
82
+ return l("PUT", e, t, r);
73
83
  },
74
- patch(e, t) {
75
- return i("PATCH", e, t);
84
+ patch(e, t, r) {
85
+ return l("PATCH", e, t, r);
76
86
  },
77
- delete(e) {
78
- return i("DELETE", e);
87
+ delete(e, t) {
88
+ return l("DELETE", e, void 0, t);
79
89
  },
80
90
  intercept(e, t) {
81
- e === "request" ? l.push(t) : h.push(t);
91
+ e === "request" ? h.push(t) : d.push(t);
82
92
  },
83
93
  /** @internal Reset state (for tests). */
84
94
  _reset() {
85
- o.baseUrl = "", o.auth = !1, o.tokenKey = "tina4_token", l.length = 0, h.length = 0;
95
+ s.baseUrl = "", s.auth = !1, s.tokenKey = "tina4_token", s.headers = {}, h.length = 0, d.length = 0;
86
96
  }
87
97
  };
88
98
  export {
89
- d as api
99
+ q as api
90
100
  };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Tina4 Component — Base class for web components.
3
+ *
4
+ * Extends HTMLElement with reactive props, lifecycle hooks,
5
+ * optional Shadow DOM, and scoped styles.
6
+ */
7
+ import { type Signal } from './signal';
8
+ /** @internal Called when a Tina4Element is connected to the DOM. */
9
+ export declare let __debugComponentMount: ((el: Tina4Element) => void) | null;
10
+ /** @internal Called when a Tina4Element is disconnected from the DOM. */
11
+ export declare let __debugComponentUnmount: ((el: Tina4Element) => void) | null;
12
+ /** @internal Set the debug hooks. */
13
+ export declare function __setDebugComponentHooks(onMount: typeof __debugComponentMount, onUnmount: typeof __debugComponentUnmount): void;
14
+ export type PropType = typeof String | typeof Number | typeof Boolean;
15
+ export declare abstract class Tina4Element extends HTMLElement {
16
+ /** Declare reactive props and their types. Override in subclass. */
17
+ static props: Record<string, PropType>;
18
+ /** Scoped CSS styles. Override in subclass. */
19
+ static styles: string;
20
+ /** Use Shadow DOM (true) or light DOM (false). Override in subclass. */
21
+ static shadow: boolean;
22
+ /** Internal reactive prop signals. */
23
+ private _props;
24
+ /** The render root (shadow or this). */
25
+ private _root;
26
+ /** Track if we've rendered. */
27
+ private _rendered;
28
+ static get observedAttributes(): string[];
29
+ constructor();
30
+ connectedCallback(): void;
31
+ disconnectedCallback(): void;
32
+ attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
33
+ /**
34
+ * Get a reactive signal for a declared prop.
35
+ *
36
+ * ```ts
37
+ * render() {
38
+ * return html`<span>${this.prop('name')}</span>`;
39
+ * }
40
+ * ```
41
+ */
42
+ prop<T = unknown>(name: string): Signal<T>;
43
+ /**
44
+ * Dispatch a custom event from this component.
45
+ *
46
+ * ```ts
47
+ * this.emit('activate', { detail: 42 });
48
+ * ```
49
+ */
50
+ emit(name: string, init?: CustomEventInit): void;
51
+ /** Called after first render. */
52
+ onMount(): void;
53
+ /** Called when removed from DOM. */
54
+ onUnmount(): void;
55
+ /** Return DOM content. Override in subclass. */
56
+ abstract render(): DocumentFragment | Node | null;
57
+ private _coerce;
58
+ }
59
+ //# sourceMappingURL=component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/core/component.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAI/C,oEAAoE;AACpE,eAAO,IAAI,qBAAqB,EAAE,CAAC,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,IAAW,CAAC;AAC7E,yEAAyE;AACzE,eAAO,IAAI,uBAAuB,EAAE,CAAC,CAAC,EAAE,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,IAAW,CAAC;AAC/E,qCAAqC;AACrC,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,qBAAqB,EACrC,SAAS,EAAE,OAAO,uBAAuB,QAI1C;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,MAAM,GAAG,OAAO,MAAM,GAAG,OAAO,OAAO,CAAC;AAEtE,8BAAsB,YAAa,SAAQ,WAAW;IACpD,oEAAoE;IACpE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAM;IAE5C,+CAA+C;IAC/C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM;IAE3B,wEAAwE;IACxE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAQ;IAE9B,sCAAsC;IACtC,OAAO,CAAC,MAAM,CAAuC;IAErD,wCAAwC;IACxC,OAAO,CAAC,KAAK,CAA2B;IAExC,+BAA+B;IAC/B,OAAO,CAAC,SAAS,CAAS;IAE1B,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;;IAaD,iBAAiB,IAAI,IAAI;IAuBzB,oBAAoB,IAAI,IAAI;IAK5B,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAQvF;;;;;;;;OAQG;IACH,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;IAO1C;;;;;;OAMG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,IAAI;IAUhD,iCAAiC;IACjC,OAAO,IAAI,IAAI;IAEf,oCAAoC;IACpC,SAAS,IAAI,IAAI;IAEjB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,IAAI,gBAAgB,GAAG,IAAI,GAAG,IAAI;IAIjD,OAAO,CAAC,OAAO;CAKhB"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Tina4 HTML — Tagged template literal renderer.
3
+ *
4
+ * html`<div>${value}</div>` returns real DOM nodes (DocumentFragment).
5
+ * When a signal is interpolated, the DOM updates surgically — no diffing.
6
+ */
7
+ export declare function html(strings: TemplateStringsArray, ...values: unknown[]): DocumentFragment;
8
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/core/html.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,wBAAgB,IAAI,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAiC1F"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * tina4js/core — Reactive primitives, HTML renderer, and web component base.
3
+ */
4
+ export { signal, computed, effect, batch, isSignal } from './signal';
5
+ export type { Signal, ReadonlySignal } from './signal';
6
+ export { html } from './html';
7
+ export { Tina4Element } from './component';
8
+ export type { PropType } from './component';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACrE,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tina4 Signals — Reactive state primitives.
3
+ *
4
+ * signal(value) — create a reactive value
5
+ * computed(fn) — derive a value that auto-tracks dependencies
6
+ * effect(fn) — run a side-effect that auto-tracks dependencies
7
+ * batch(fn) — batch multiple signal updates into one notification
8
+ */
9
+ /** @internal Start collecting effect disposers (used by router). */
10
+ export declare function _setEffectCollector(collector: (() => void)[] | null): void;
11
+ /** @internal Read the current effect collector (used by html renderer). */
12
+ export declare function _getEffectCollector(): (() => void)[] | null;
13
+ /** @internal Called when a signal is created. */
14
+ export declare let __debugSignalCreate: ((s: Signal<unknown>, label?: string) => void) | null;
15
+ /** @internal Called when a signal value changes. */
16
+ export declare let __debugSignalUpdate: ((s: Signal<unknown>, oldVal: unknown, newVal: unknown) => void) | null;
17
+ /** @internal Set the debug hooks. */
18
+ export declare function __setDebugSignalHooks(onCreate: typeof __debugSignalCreate, onUpdate: typeof __debugSignalUpdate): void;
19
+ export interface Signal<T> {
20
+ value: T;
21
+ /** @internal */
22
+ readonly _t4: true;
23
+ /** @internal subscribe directly (used by html renderer) */
24
+ _subscribe(fn: () => void): () => void;
25
+ /** @internal read without tracking */
26
+ peek(): T;
27
+ }
28
+ export interface ReadonlySignal<T> {
29
+ readonly value: T;
30
+ /** @internal */
31
+ readonly _t4: true;
32
+ /** @internal */
33
+ _subscribe(fn: () => void): () => void;
34
+ peek(): T;
35
+ }
36
+ export declare function signal<T>(initial: T, label?: string): Signal<T>;
37
+ export declare function computed<T>(fn: () => T): ReadonlySignal<T>;
38
+ export declare function effect(fn: () => void): () => void;
39
+ export declare function batch(fn: () => void): void;
40
+ /** Check if a value is a tina4 signal. */
41
+ export declare function isSignal(value: unknown): value is Signal<unknown>;
42
+ //# sourceMappingURL=signal.d.ts.map
@@ -0,0 +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,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAoD/D;AAID,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CA2B1D;AAID,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAgCjD;AAID,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"}