enigmatic 0.33.0 → 0.34.0

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.
@@ -1,4 +1,118 @@
1
- # client.js Documentation
1
+ # Enigmatic
2
+
3
+ ![Tests](https://img.shields.io/badge/tests-26%20passing-brightgreen) ![Size](https://img.shields.io/badge/size-3.4%20KB-blue) ![Version](https://img.shields.io/npm/v/enigmatic)
4
+
5
+ A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.
6
+
7
+ ## Quick Start
8
+
9
+ ### Using client.js via CDN
10
+
11
+ Include `client.js` in any HTML file using the unpkg CDN:
12
+
13
+ ```html
14
+ <!DOCTYPE html>
15
+ <html>
16
+ <head>
17
+ <script src="https://unpkg.com/enigmatic@0.34.0/public/client.js"></script>
18
+ <script>
19
+ // Configure your API URL
20
+ window.api_url = 'https://your-server.com';
21
+
22
+ // Define custom elements
23
+ window.custom = {
24
+ "hello-world": (data) => `Hello ${data || 'World'}`
25
+ };
26
+ </script>
27
+ </head>
28
+ <body>
29
+ <hello-world data="message"></hello-world>
30
+ <script>
31
+ window.state.message = "Hello World";
32
+ </script>
33
+ </body>
34
+ </html>
35
+ ```
36
+
37
+ **Note:** Replace `0.34.0` with the latest version number from [npm](https://www.npmjs.com/package/enigmatic).
38
+
39
+ ### Using the Bun Server
40
+
41
+ The Bun server provides a complete backend implementation with:
42
+ - Key-value storage (using BeeMap)
43
+ - File storage (using Cloudflare R2/S3)
44
+ - Authentication (using Auth0)
45
+ - Static file serving
46
+
47
+ #### Installation
48
+
49
+ 1. Install Bun (if not already installed):
50
+ ```bash
51
+ curl -fsSL https://bun.sh/install | bash
52
+ ```
53
+
54
+ 2. Install dependencies:
55
+ ```bash
56
+ bun install
57
+ ```
58
+
59
+ 3. Generate HTTPS certificates (for local development):
60
+ ```bash
61
+ cd server
62
+ ./generate-certs.sh
63
+ cd ..
64
+ ```
65
+
66
+ #### Environment Variables
67
+
68
+ Create a `.env` file in the project root with the following variables:
69
+
70
+ ```bash
71
+ # Auth0 Configuration
72
+ AUTH0_DOMAIN=your-tenant.auth0.com
73
+ AUTH0_CLIENT_ID=your-client-id
74
+ AUTH0_CLIENT_SECRET=your-client-secret
75
+
76
+ # Cloudflare R2 Configuration (optional, for file storage)
77
+ CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
78
+ CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
79
+ CLOUDFLARE_BUCKET_NAME=your-bucket-name
80
+ CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com
81
+ ```
82
+
83
+ #### Running the Server
84
+
85
+ Start the server with hot reload:
86
+ ```bash
87
+ npm start
88
+ # or
89
+ bun --hot ./bun-server.js
90
+ ```
91
+
92
+ The server will start on `https://localhost:3000` (HTTPS is required for Auth0 cookies).
93
+
94
+ #### Server Features
95
+
96
+ - **Static File Serving**: Automatically serves any files from the `public/` folder
97
+ - **Key-Value Storage**: Per-user KV storage using BeeMap (persisted to JSONL files)
98
+ - **File Storage**: Per-user file storage using Cloudflare R2 (or compatible S3)
99
+ - **Authentication**: OAuth2 flow with Auth0
100
+ - **CORS**: Enabled for all origins (configurable)
101
+
102
+ #### Server Endpoints
103
+
104
+ - `GET /` or `GET /index.html` - Serves `public/index.html`
105
+ - `GET /{path}` - Serves static files from `public/` folder
106
+ - `GET /login` - Initiates Auth0 login flow
107
+ - `GET /callback` - Auth0 callback handler
108
+ - `GET /logout` - Logs out user
109
+ - `GET /{key}` - Retrieves KV value (requires auth)
110
+ - `POST /{key}` - Stores KV value (requires auth)
111
+ - `DELETE /{key}` - Deletes KV value (requires auth)
112
+ - `PUT /{key}` - Uploads file to R2 (requires auth)
113
+ - `PURGE /{key}` - Deletes file from R2 (requires auth)
114
+ - `PROPFIND /` - Lists files in R2 (requires auth)
115
+ - `PATCH /{key}` - Downloads file from R2 (requires auth)
2
116
 
3
117
  ## Overview
4
118
 
@@ -36,13 +150,15 @@ Configures the base URL for all API requests. Modify this to point to your serve
36
150
  - Set a property: `window.state.myKey = 'value'`
37
151
  - Elements with `data="myKey"` attribute are automatically updated
38
152
  - The system looks for custom element handlers in `window.custom[tagName]`
153
+ - Only elements with matching custom element handlers are updated
39
154
  - Supports both function and object-based custom elements
40
155
 
41
156
  **Example:**
42
157
  ```html
43
- <div data="message">Initial</div>
158
+ <my-element data="message">Initial</my-element>
44
159
  <script>
45
- window.state.message = "Updated!"; // Automatically updates the div
160
+ window.custom['my-element'] = (data) => `<div>${data}</div>`;
161
+ window.state.message = "Updated!"; // Automatically updates the element
46
162
  </script>
47
163
  ```
48
164
 
@@ -169,7 +285,7 @@ window.logout();
169
285
 
170
286
  ## Custom Elements System
171
287
 
172
- Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads.
288
+ Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads or when elements are added dynamically.
173
289
 
174
290
  ### Initialization
175
291
 
@@ -178,6 +294,17 @@ The library automatically:
178
294
  2. Iterates through all keys in `window.custom`
179
295
  3. Finds all matching HTML elements by tag name
180
296
  4. Calls the custom element handler and sets `innerHTML`
297
+ 5. Watches for new elements added to the DOM via MutationObserver and initializes them automatically
298
+
299
+ ### Proxy Behavior
300
+
301
+ `window.custom` is a Proxy that automatically initializes matching elements when you add a new custom element definition:
302
+
303
+ ```javascript
304
+ // Adding a new custom element automatically initializes all matching elements in the DOM
305
+ window.custom['my-element'] = (data) => `<div>${data}</div>`;
306
+ // All <my-element> tags are immediately initialized
307
+ ```
181
308
 
182
309
  ### Defining Custom Elements
183
310
 
@@ -196,12 +323,15 @@ window.custom = {
196
323
  <my-element></my-element>
197
324
  ```
198
325
 
199
- When `window.state.myKey = 'value'` is set and an element has `data="myKey"`:
326
+ When used with reactive state, the function receives the state value:
200
327
  ```html
201
328
  <my-element data="myKey"></my-element>
329
+ <script>
330
+ window.state.myKey = 'value'; // Function is called with 'value'
331
+ </script>
202
332
  ```
203
333
 
204
- The function receives the state value as the first parameter.
334
+ The function receives the state value as the first parameter. If no state value is set, it receives `undefined`.
205
335
 
206
336
  #### Object-based Custom Element
207
337
 
@@ -267,18 +397,23 @@ window.get('key').catch(err => console.error(err));
267
397
  <!DOCTYPE html>
268
398
  <html>
269
399
  <head>
270
- <script src="custom.js"></script>
271
- <script src="client.js"></script>
400
+ <script src="https://unpkg.com/enigmatic@0.34.0/public/client.js"></script>
401
+ <script>
402
+ // Configure API URL
403
+ window.api_url = 'https://localhost:3000';
404
+
405
+ // Define custom elements
406
+ window.custom = {
407
+ "hello-world": (data) => `Hello ${data || 'World'}`
408
+ };
409
+ </script>
272
410
  </head>
273
411
  <body>
274
- <!-- Custom element -->
275
- <file-widget></file-widget>
276
-
277
- <!-- Reactive state element -->
278
- <div data="message">Initial</div>
412
+ <!-- Custom element with reactive state -->
413
+ <hello-world data="message"></hello-world>
279
414
 
280
415
  <script>
281
- // Set reactive state
416
+ // Set reactive state (triggers updates to elements with data="message")
282
417
  window.state.message = "Hello World";
283
418
 
284
419
  // Use API functions
@@ -305,18 +440,44 @@ window.get('key').catch(err => console.error(err));
305
440
 
306
441
  ## Dependencies
307
442
 
308
- - Requires `custom.js` to be loaded before `client.js` if using custom elements
309
- - Requires a backend server that implements the API endpoints
443
+ - Requires a backend server that implements the API endpoints (or use the included Bun server)
310
444
  - Requires browser support for:
311
445
  - `fetch` API
312
446
  - `Proxy` API
313
447
  - `Blob` API
314
448
  - `URL.createObjectURL`
449
+ - `MutationObserver` API
450
+
451
+ **Note:** Custom element definitions can be loaded before or after `client.js` - the Proxy system will handle initialization either way.
315
452
 
316
453
  ## Notes
317
454
 
318
455
  - All API functions automatically encode keys using `encodeURIComponent`
319
456
  - The `window.download()` function uses PATCH method internally (browsers don't support custom HTTP methods)
320
- - Custom elements are initialized once on page load; use `location.reload()` to refresh
457
+ - Custom elements are automatically initialized:
458
+ - On page load (when DOM is ready)
459
+ - When new custom element definitions are added to `window.custom`
460
+ - When new matching elements are added to the DOM (via MutationObserver)
321
461
  - The reactive state system only updates elements with matching `data` attributes
322
462
  - Custom element handlers can be async functions
463
+ - When a custom element has a `data` attribute, it automatically reads from `window.state[dataValue]` if no explicit value is provided
464
+
465
+ ## Development
466
+
467
+ ### Running Tests
468
+
469
+ ```bash
470
+ npm test
471
+ ```
472
+
473
+ ### Building
474
+
475
+ The library is ready to use as-is. For production, you can use the minified version:
476
+
477
+ ```html
478
+ <script src="https://unpkg.com/enigmatic@0.34.0/public/client.min.js"></script>
479
+ ```
480
+
481
+ ## License
482
+
483
+ MIT
@@ -11,13 +11,37 @@ describe('client.js', () => {
11
11
  global.document.body.innerHTML = ''
12
12
  global.document.head.innerHTML = ''
13
13
 
14
- // Clear window.custom
15
- global.window.custom = {}
14
+ // Set api_url
15
+ global.window.api_url = 'https://localhost:3000'
16
16
 
17
- // Execute custom.js first (defines window.custom)
18
- eval(customCode)
19
- // Execute client.js code
17
+ // Clear window properties that might have descriptors
18
+ try {
19
+ delete global.window.custom
20
+ delete global.window.state
21
+ delete global.window.$
22
+ delete global.window.$$
23
+ delete global.window.$c
24
+ delete global.window.get
25
+ delete global.window.set
26
+ delete global.window.put
27
+ delete global.window.delete
28
+ delete global.window.purge
29
+ delete global.window.list
30
+ delete global.window.login
31
+ delete global.window.logout
32
+ delete global.window.download
33
+ delete global.window.initCustomElements
34
+ } catch (e) {
35
+ // Ignore errors
36
+ }
37
+
38
+ // Execute client.js code first (sets up Proxy)
20
39
  eval(clientCode)
40
+ // Execute custom.js (defines window.custom components)
41
+ eval(customCode)
42
+
43
+ // Wait for initialization
44
+ return new Promise(resolve => setTimeout(resolve, 100))
21
45
  })
22
46
 
23
47
  describe('$ and $$ selectors', () => {
@@ -71,7 +95,8 @@ describe('client.js', () => {
71
95
  const result = await window.get('test key')
72
96
 
73
97
  expect(global.fetch).toHaveBeenCalledWith(
74
- `${window.api_url}/test%20key`
98
+ `${window.api_url}/test%20key`,
99
+ expect.objectContaining({ method: 'GET' })
75
100
  )
76
101
  expect(result).toEqual({ value: 'test' })
77
102
  })
@@ -207,7 +232,7 @@ describe('client.js', () => {
207
232
  const result = await window.list()
208
233
 
209
234
  expect(global.fetch).toHaveBeenCalledWith(
210
- window.api_url,
235
+ `${window.api_url}/`,
211
236
  expect.objectContaining({
212
237
  method: 'PROPFIND'
213
238
  })
@@ -255,29 +280,32 @@ describe('client.js', () => {
255
280
 
256
281
 
257
282
  describe('window.state proxy', () => {
258
- test('state.set updates DOM elements with data attribute', () => {
283
+ test('state.set updates DOM elements with data attribute', async () => {
259
284
  global.document.body.innerHTML = '<hello-world data="name"></hello-world>'
260
285
 
261
286
  window.state.name = 'John'
287
+ await new Promise(resolve => setTimeout(resolve, 50))
262
288
 
263
289
  const el = global.document.querySelector('hello-world')
264
290
  expect(el.innerHTML).toBe('Hello John')
265
291
  })
266
292
 
267
- test('state.set updates multiple elements with same data attribute', () => {
293
+ test('state.set updates multiple elements with same data attribute', async () => {
268
294
  global.document.body.innerHTML = '<hello-world data="name"></hello-world><hello-world data="name"></hello-world>'
269
295
 
270
296
  window.state.name = 'Jane'
297
+ await new Promise(resolve => setTimeout(resolve, 50))
271
298
 
272
299
  const els = global.document.querySelectorAll('hello-world')
273
300
  expect(els[0].innerHTML).toBe('Hello Jane')
274
301
  expect(els[1].innerHTML).toBe('Hello Jane')
275
302
  })
276
303
 
277
- test('state.set works with object components', () => {
304
+ test('state.set works with object components', async () => {
278
305
  global.document.body.innerHTML = '<hello-world-2 data="test"></hello-world-2>'
279
306
 
280
307
  window.state.test = 'Hello'
308
+ await new Promise(resolve => setTimeout(resolve, 50))
281
309
 
282
310
  const el = global.document.querySelector('hello-world-2')
283
311
  expect(el.innerHTML).toBe('Hello World')
@@ -288,20 +316,22 @@ describe('client.js', () => {
288
316
  expect(window.state.test).toBe('value')
289
317
  })
290
318
 
291
- test('state.set handles multiple properties', () => {
319
+ test('state.set handles multiple properties', async () => {
292
320
  global.document.body.innerHTML = '<hello-world data="a"></hello-world><hello-world data="b"></hello-world>'
293
321
 
294
322
  window.state.a = 'A'
295
323
  window.state.b = 'B'
324
+ await new Promise(resolve => setTimeout(resolve, 50))
296
325
 
297
326
  expect(global.document.querySelector('[data="a"]').innerHTML).toBe('Hello A')
298
327
  expect(global.document.querySelector('[data="b"]').innerHTML).toBe('Hello B')
299
328
  })
300
329
 
301
- test('state.set does not update elements without matching data attribute', () => {
330
+ test('state.set does not update elements without matching data attribute', async () => {
302
331
  global.document.body.innerHTML = '<hello-world data="name"></hello-world><div data="other">Original</div>'
303
332
 
304
333
  window.state.name = 'John'
334
+ await new Promise(resolve => setTimeout(resolve, 50))
305
335
 
306
336
  expect(global.document.querySelector('[data="name"]').innerHTML).toBe('Hello John')
307
337
  expect(global.document.querySelector('[data="other"]').innerHTML).toBe('Original')
package/beemap.js ADDED
@@ -0,0 +1,47 @@
1
+ class BeeMap extends Map {
2
+ constructor(jsonlFile, timems) {
3
+ super();
4
+ this.jsonlFile = jsonlFile;
5
+ this.timems = timems;
6
+ this.intervalId = null;
7
+
8
+ // Load existing data
9
+ this.load();
10
+
11
+ // Set up interval to save
12
+ if (!timems) return;
13
+ this.intervalId = setInterval(() => {
14
+ this.save();
15
+ }, timems);
16
+ }
17
+
18
+ async load() {
19
+ const file = Bun.file(this.jsonlFile);
20
+ if (!(await file.exists())) return;
21
+
22
+ const text = await file.text();
23
+ const lines = text.trim().split('\n').filter(line => line.length > 0);
24
+
25
+ for (const line of lines) {
26
+ const [key, value] = JSON.parse(line);
27
+ super.set(key, value);
28
+ }
29
+ }
30
+
31
+ async save() {
32
+ const lines = [];
33
+ for (const [key, value] of this) {
34
+ lines.push(JSON.stringify([key, value]));
35
+ }
36
+ await Bun.write(this.jsonlFile, lines.join('\n') + '\n');
37
+ }
38
+
39
+ destroy() {
40
+ if (this.intervalId) {
41
+ clearInterval(this.intervalId);
42
+ this.intervalId = null;
43
+ }
44
+ }
45
+ }
46
+
47
+ export { BeeMap };
package/bun-server.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { S3Client } from "bun";
2
+ import { BeeMap } from "./beemap.js";
2
3
 
3
- const dbPath = "db.json";
4
- const db = await Bun.file(dbPath).json().catch(() => ({}));
4
+ const sessions = new BeeMap("sessions.jsonl", 20000), beeMaps = {};
5
5
  const s3 = new S3Client({
6
6
  accessKeyId: Bun.env.CLOUDFLARE_ACCESS_KEY_ID,
7
7
  secretAccessKey: Bun.env.CLOUDFLARE_SECRET_ACCESS_KEY,
8
8
  bucket: Bun.env.CLOUDFLARE_BUCKET_NAME,
9
9
  endpoint: Bun.env.CLOUDFLARE_PUBLIC_URL
10
10
  });
11
+
11
12
  const json = (data, status = 200, extraHeaders = {}) => new Response(JSON.stringify(data), {
12
- status,
13
- headers: {
13
+ status, headers: {
14
14
  "Access-Control-Allow-Origin": "*",
15
15
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, DOWNLOAD, OPTIONS",
16
16
  "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie, X-HTTP-Method-Override",
@@ -19,7 +19,7 @@ const json = (data, status = 200, extraHeaders = {}) => new Response(JSON.string
19
19
  ...extraHeaders
20
20
  }
21
21
  });
22
- const writeDb = () => Bun.write(dbPath, JSON.stringify(db, null, 2));
22
+
23
23
  const redirect = (url, cookie = null) => new Response(null, {
24
24
  status: 302,
25
25
  headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) }
@@ -27,21 +27,21 @@ const redirect = (url, cookie = null) => new Response(null, {
27
27
 
28
28
  export default {
29
29
  async fetch(req) {
30
- console.log('req.method', req.method);
31
- const url = new URL(req.url);
32
- const key = url.pathname.slice(1);
33
- const cb = `${url.origin}/callback`;
30
+ const url = new URL(req.url), key = url.pathname.slice(1);
34
31
  const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
35
- const user = token ? db[`session:${token}`] : null;
32
+ const user = token ? sessions.get(token) : null;
36
33
 
37
34
  if (req.method === "OPTIONS") return json(null, 204);
38
35
 
36
+ // Serve static files from public folder
37
+ if (req.method === 'GET') {
38
+ const file = Bun.file(`./public${url.pathname === '/' ? '/index.html' : url.pathname}`);
39
+ if (await file.exists()) return new Response(file);
40
+ }
41
+
39
42
  if (url.pathname === '/login') {
40
43
  return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({
41
- response_type: "code",
42
- client_id: Bun.env.AUTH0_CLIENT_ID,
43
- redirect_uri: cb,
44
- scope: "openid email profile"
44
+ response_type: "code", client_id: Bun.env.AUTH0_CLIENT_ID, redirect_uri: cb, scope: "openid email profile"
45
45
  })}`);
46
46
  }
47
47
 
@@ -49,14 +49,10 @@ export default {
49
49
  const code = url.searchParams.get("code");
50
50
  if (!code) return json({ error: 'No code' }, 400);
51
51
  const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, {
52
- method: "POST",
53
- headers: { "content-type": "application/json" },
52
+ method: "POST", headers: { "content-type": "application/json" },
54
53
  body: JSON.stringify({
55
- grant_type: "authorization_code",
56
- client_id: Bun.env.AUTH0_CLIENT_ID,
57
- client_secret: Bun.env.AUTH0_CLIENT_SECRET,
58
- code,
59
- redirect_uri: cb
54
+ grant_type: "authorization_code", client_id: Bun.env.AUTH0_CLIENT_ID,
55
+ client_secret: Bun.env.AUTH0_CLIENT_SECRET, code, redirect_uri: `${url.origin}/callback`
60
56
  })
61
57
  });
62
58
  if (!tRes.ok) return json({ error: 'Auth error' }, 401);
@@ -65,37 +61,32 @@ export default {
65
61
  headers: { Authorization: `Bearer ${tokens.access_token}` }
66
62
  })).json();
67
63
  const session = crypto.randomUUID();
68
- db[`session:${session}`] = {
64
+ sessions.set(session, {
69
65
  ...userInfo,
70
66
  login_time: new Date().toISOString(),
71
67
  access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null
72
- };
73
- await writeDb();
68
+ });
74
69
  return redirect(url.origin, `token=${session}; HttpOnly; Path=/; Secure; SameSite=Lax; Max-Age=86400`);
75
70
  }
76
71
 
77
72
  if (!token || !user) return json({ error: 'Unauthorized' }, 401);
78
73
 
79
74
  if (url.pathname === '/logout') {
80
- delete db[`session:${token}`];
81
- await writeDb();
75
+ sessions.delete(token);
82
76
  return redirect(url.origin, "token=; Max-Age=0; Path=/");
83
77
  }
84
78
 
85
- const files = { '/': 'index.html', '/client.js': 'client.js', '/custom.js': 'custom.js' };
86
- if (req.method === 'GET' && files[url.pathname]) return new Response(Bun.file(`./public/${files[url.pathname]}`));
79
+ // Initialize user's beeMap if needed
80
+ if (!beeMaps[user.sub]) beeMaps[user.sub] = new BeeMap(`kv_${user.sub}.jsonl`, 20000);
87
81
 
88
- console.log(req.method);
89
82
  switch (req.method) {
90
- case 'GET': return json(db[key]);
83
+ case 'GET': return json(beeMaps[user.sub].get(key) || null);
91
84
  case 'POST':
92
85
  const value = await req.text();
93
- db[key] = (() => { try { return JSON.parse(value); } catch { return value; } })();
94
- await writeDb();
86
+ beeMaps[user.sub].set(key, (() => { try { return JSON.parse(value); } catch { return value; } })());
95
87
  return json({ key, value });
96
88
  case 'DELETE':
97
- delete db[key];
98
- await writeDb();
89
+ beeMaps[user.sub].delete(key);
99
90
  return json({ status: "Deleted" });
100
91
  case 'PUT':
101
92
  await s3.write(`${user.sub}/${key}`, req.body);
@@ -125,6 +116,7 @@ export default {
125
116
  default: return json({ error: 'Method not allowed' }, 405);
126
117
  }
127
118
  },
119
+
128
120
  port: 3000,
129
121
  tls: { cert: Bun.file("cert.pem"), key: Bun.file("key.pem") }
130
122
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigmatic",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "unpkg": "./public/client.js",
5
5
  "scripts": {
6
6
  "start": "bun --hot ./bun-server.js",
package/public/client.js CHANGED
@@ -6,7 +6,13 @@ const ren = async (el, v) => {
6
6
  if (f) {
7
7
  const dataAttr = el.getAttribute('data');
8
8
  const val = v !== undefined ? v : (dataAttr ? W.state[dataAttr] : undefined);
9
- try { el.innerHTML = await (f.render || f)(val) } catch(e) { console.error(e) }
9
+ try {
10
+ if (f.render) {
11
+ el.innerHTML = await f.render.call(f, val);
12
+ } else if (typeof f === 'function') {
13
+ el.innerHTML = await f(val);
14
+ }
15
+ } catch(e) { console.error(e) }
10
16
  }
11
17
  };
12
18
 
@@ -14,11 +20,25 @@ const ren = async (el, v) => {
14
20
  const cProx = new Proxy({}, {
15
21
  set(t, p, v) {
16
22
  t[p] = v;
17
- setTimeout(() => W.$$(p).forEach(el => ren(el)), 0);
23
+ setTimeout(() => {
24
+ if (W.$$ && D.body) {
25
+ W.$$(p).forEach(el => ren(el));
26
+ }
27
+ }, 0);
18
28
  return true;
19
29
  }
20
30
  });
21
- Object.defineProperty(W, 'custom', { get: () => cProx, set: v => Object.assign(cProx, v) });
31
+ Object.defineProperty(W, 'custom', {
32
+ get: () => cProx,
33
+ set: v => {
34
+ Object.keys(v || {}).forEach(k => cProx[k] = v[k]);
35
+ // Defer initialization to ensure DOM and functions are ready
36
+ setTimeout(() => {
37
+ if (W.initCustomElements && D.body) W.initCustomElements();
38
+ }, 50);
39
+ },
40
+ configurable: true
41
+ });
22
42
 
23
43
  const sProx = new Proxy({}, {
24
44
  set(o, p, v) {
@@ -55,26 +75,44 @@ Object.assign(W, {
55
75
  a.click();
56
76
  URL.revokeObjectURL(a.href);
57
77
  },
58
- initCustomElements: () => Object.keys(W.custom || {}).forEach(t => W.$$(t).forEach(el => ren(el)))
78
+ initCustomElements: () => {
79
+ if (!D.body) return;
80
+ Object.keys(W.custom || {}).forEach(t => {
81
+ const elements = W.$$(t);
82
+ if (elements.length > 0) {
83
+ elements.forEach(el => ren(el));
84
+ }
85
+ });
86
+ }
59
87
  });
60
88
 
61
89
  // 4. Initialization & Observers
62
90
  const boot = () => {
63
- W.initCustomElements();
64
- new MutationObserver((mutations) => {
65
- mutations.forEach(m => {
66
- m.addedNodes.forEach(node => {
67
- if (node.nodeType === 1) { // Element node
68
- const tag = node.tagName?.toLowerCase();
69
- if (tag && W.custom?.[tag]) ren(node);
70
- // Also check children
71
- node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach(child => {
72
- const childTag = child.tagName?.toLowerCase();
73
- if (childTag && W.custom?.[childTag]) ren(child);
74
- });
75
- }
91
+ if (W.initCustomElements) {
92
+ // Run immediately and also after a short delay to catch any elements added during script execution
93
+ W.initCustomElements();
94
+ setTimeout(() => W.initCustomElements(), 10);
95
+ }
96
+ if (D.body) {
97
+ new MutationObserver((mutations) => {
98
+ mutations.forEach(m => {
99
+ m.addedNodes.forEach(node => {
100
+ if (node.nodeType === 1) { // Element node
101
+ const tag = node.tagName?.toLowerCase();
102
+ if (tag && W.custom?.[tag]) ren(node);
103
+ // Also check children
104
+ node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach(child => {
105
+ const childTag = child.tagName?.toLowerCase();
106
+ if (childTag && W.custom?.[childTag]) ren(child);
107
+ });
108
+ }
109
+ });
76
110
  });
77
- });
78
- }).observe(D.body, { childList: true, subtree: true });
111
+ }).observe(D.body, { childList: true, subtree: true });
112
+ }
79
113
  };
80
- D.readyState === 'loading' ? D.addEventListener('DOMContentLoaded', boot) : boot();
114
+ if (D.readyState === 'loading') {
115
+ D.addEventListener('DOMContentLoaded', boot);
116
+ } else {
117
+ setTimeout(boot, 0);
118
+ }
package/public/custom.js CHANGED
@@ -6,24 +6,29 @@ window.custom = {
6
6
  return this.prop(data);
7
7
  }
8
8
  },
9
- "file-widget": async () => {
10
- const list = await window.list();
11
- const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-i{display:flex;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #f0f0f0;align-items:center}.w-i:hover{background:#f9f9f9}.w-d{border:none;background:none;cursor:pointer;opacity:.5;transition:.2s}.w-d:hover{opacity:1}.w-u{display:block;padding:10px;background:#f5f5f5;text-align:center;cursor:pointer;color:#555;font-weight:600;transition:.2s}.w-u:hover{background:#eee}</style>`;
12
-
13
- const items = list.map(item => `
14
- <div class="w-i">
15
- <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px">${item.name}</span>
16
- <button class="w-d" onclick="window.download('${item.name}')" title="Download">⬇️</button>
17
- <button class="w-d" onclick="(async()=>{await window.purge('${item.name}');location.reload()})()" title="Delete">🗑️</button>
18
- </div>`
19
- ).join('');
9
+ "file-widget": async () => {
10
+ try {
11
+ const list = await window.list();
12
+ const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-i{display:flex;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #f0f0f0;align-items:center}.w-i:hover{background:#f9f9f9}.w-d{border:none;background:none;cursor:pointer;opacity:.5;transition:.2s}.w-d:hover{opacity:1}.w-u{display:block;padding:10px;background:#f5f5f5;text-align:center;cursor:pointer;color:#555;font-weight:600;transition:.2s}.w-u:hover{background:#eee}.w-e{padding:20px;text-align:center;color:#999}</style>`;
13
+
14
+ const items = Array.isArray(list) ? list.map(item => `
15
+ <div class="w-i">
16
+ <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px">${item.name}</span>
17
+ <button class="w-d" onclick="window.download('${item.name}')" title="Download">⬇️</button>
18
+ <button class="w-d" onclick="(async()=>{await window.purge('${item.name}');location.reload()})()" title="Delete">🗑️</button>
19
+ </div>`
20
+ ).join('') : '';
20
21
 
21
- const upload = `
22
- <label class="w-u">
23
- 📂 Upload
24
- <input type="file" style="display:none" onchange="(async()=>{const f=this.files[0];if(f){await window.put(f.name,f);location.reload()}})()">
25
- </label>`;
22
+ const upload = `
23
+ <label class="w-u">
24
+ 📂 Upload
25
+ <input type="file" style="display:none" onchange="(async()=>{const f=this.files[0];if(f){await window.put(f.name,f);location.reload()})()">
26
+ </label>`;
26
27
 
27
- return style + `<div class="w-c">${items}${upload}</div>`;
28
+ return style + `<div class="w-c">${items || '<div class="w-e">No files</div>'}${upload}</div>`;
29
+ } catch (err) {
30
+ const style = `<style>.w-c{font:13px sans-serif;border:1px solid #ddd;border-radius:6px;overflow:hidden;max-width:320px}.w-e{padding:20px;text-align:center;color:#999}</style>`;
31
+ return style + `<div class="w-c"><div class="w-e">Please <button onclick="window.login()" style="background:#007bff;color:white;border:none;padding:5px 10px;border-radius:3px;cursor:pointer">Login</button> to view files</div></div>`;
32
+ }
33
+ }
28
34
  }
29
- }
package/public/index.html CHANGED
@@ -4,6 +4,9 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>API Test</title>
7
+ <script>
8
+ window.api_url = 'https://localhost:3000';
9
+ </script>
7
10
  <script src="custom.js"></script>
8
11
  <script src="client.js"></script>
9
12
  <style>
@@ -1,4 +1,5 @@
1
- <script src='client.js'></script>
1
+ <script src='https://unpkg.com/enigmatic'></script>
2
+ <script src='custom.js'></script>
2
3
 
3
4
  <script>
4
5
  window.api_url = 'http://localhost:3000';
@@ -6,4 +7,4 @@
6
7
  state.message = "World";
7
8
  </script>
8
9
 
9
- <hw data="message"></hw>
10
+ <file-widget></file-widget>
package/public/client.css DELETED
@@ -1,286 +0,0 @@
1
- html {
2
- max-width: 70ch;
3
- /* larger spacing on larger screens, very small spacing on tiny screens */
4
- padding: calc(1vmin + .5rem);
5
- /* shorthand for margin-left/margin-right */
6
- margin-inline: auto;
7
- /* fluid sizing: https://frontaid.io/blog/fluid-typography-2d-css-locks-clamp/ */
8
- font-size: clamp(1em, 0.909em + 0.45vmin, 1.25em);
9
- /* use system font stack: https://developer.mozilla.org/en-US/docs/Web/CSS/font-family */
10
- font-family: system-ui
11
- }
12
-
13
- /* increase line-height for everything except headings */
14
- body :not(:is(h1, h2, h3, h4, h5, h6)) {
15
- line-height: 1.75;
16
- }
17
-
18
- h1,
19
- h2,
20
- h3,
21
- h4,
22
- h5,
23
- h6 {
24
- margin: 3em 0 1em;
25
- }
26
-
27
- p,
28
- ul,
29
- ol {
30
- margin-bottom: 2em;
31
- color: #1d1d1d;
32
- }
33
-
34
- body,
35
- section {
36
- display: grid;
37
- margin: 0;
38
- grid-template-columns: var(--cols, 1fr 4fr 1fr);
39
- grid-template-rows: var(--rows, 1fr 9fr 1fr);
40
- }
41
-
42
- * {
43
- grid-column: span var(--span, 1);
44
- grid-row: span var(--span-rows, 1);
45
- }
46
-
47
- .flex {
48
- display: flex;
49
- }
50
-
51
- /** positioning ***/
52
-
53
- .center {
54
- position: fixed;
55
- top: 50%;
56
- left: 50%;
57
- margin-top: -50px;
58
- margin-left: -100px;
59
- }
60
-
61
- .right {
62
- float: right;
63
- }
64
-
65
- .left {
66
- float: left;
67
- }
68
-
69
- .fixed {
70
- position: fixed;
71
- }
72
-
73
- .top {
74
- top: 0;
75
- }
76
-
77
- .bottom {
78
- bottom: 0
79
- }
80
-
81
- .fill {
82
- height: 100vh;
83
- width: 100wh
84
- }
85
-
86
- .hide {
87
- opacity: 0;
88
- transition: opacity 0.25s linear;
89
- }
90
-
91
- .show {
92
- opacity: 1;
93
- transition: opacity 0.25s linear;
94
- }
95
-
96
- .slide-in {
97
- animation: slide-in 0.1s forwards;
98
- -webkit-animation: slide-in 0.1s forwards;
99
- }
100
-
101
- .slide-out {
102
- animation: slide-out 0.1s forwards;
103
- -webkit-animation: slide-out 0.1s forwards;
104
- }
105
-
106
- @keyframes slide-in {
107
- 100% {
108
- transform: translateX(0%);
109
- }
110
- }
111
-
112
- @-webkit-keyframes slide-in {
113
- 100% {
114
- -webkit-transform: translateX(0%);
115
- }
116
- }
117
-
118
- @keyframes slide-out {
119
- 0% {
120
- transform: translateX(0%);
121
- }
122
-
123
- 100% {
124
- transform: translateX(-100%);
125
- }
126
- }
127
-
128
- @-webkit-keyframes slide-out {
129
- 0% {
130
- -webkit-transform: translateX(0%);
131
- }
132
-
133
- 100% {
134
- -webkit-transform: translateX(-100%);
135
- }
136
- }
137
-
138
- a {
139
- text-decoration: none;
140
- }
141
-
142
- .shadow {
143
- box-shadow: 6px 6px 6px #dbdbdb;
144
- }
145
-
146
- .cursor {
147
- cursor: default;
148
- }
149
-
150
- .margins {
151
- margin: var(--margins, 15px);
152
- }
153
-
154
- .padding {
155
- padding: var(--padding, 15px);
156
- }
157
-
158
- .rem {
159
- font-size: var(--rem, 2rem);
160
- }
161
-
162
- .faded {
163
- opacity: 0.5;
164
- }
165
-
166
- .fade {
167
- opacity: 1;
168
- transition: opacity 0.25s ease-in-out;
169
- -moz-transition: opacity 0.25s ease-in-out;
170
- -webkit-transition: opacity 0.25s ease-in-out;
171
- }
172
-
173
- .fade:hover {
174
- opacity: 0.5;
175
- }
176
-
177
- .unfade {
178
- opacity: 0.5;
179
- transition: opacity 0.25s ease-in-out;
180
- -moz-transition: opacity 0.25s ease-in-out;
181
- -webkit-transition: opacity 0.25s ease-in-out;
182
- }
183
-
184
- .unfade:hover {
185
- opacity: 1;
186
- }
187
-
188
- .rounded {
189
- -moz-border-radius: 10px;
190
- -webkit-border-radius: 10px;
191
- border-radius: 10px;
192
- -khtml-border-radius: 10px;
193
- }
194
-
195
- .round {
196
- vertical-align: middle;
197
- width: 50px;
198
- height: 50px;
199
- border-radius: 50%;
200
- }
201
-
202
- /** html elements **/
203
-
204
- canvas {
205
- position: fixed;
206
- top: 0;
207
- left: 0;
208
- }
209
-
210
- ul {
211
- list-style-type: none;
212
- border: 20px;
213
- padding: 20px;
214
- width: 50%;
215
- }
216
-
217
- li {
218
- list-style-type: none;
219
- border: 10px;
220
- padding: 10px;
221
- }
222
-
223
- li:hover {
224
- background-color: rgb(243, 241, 241);
225
- cursor: pointer;
226
- }
227
-
228
- .opacity1 {
229
- opacity: .1
230
- }
231
-
232
- .opacity2 {
233
- opacity: .2
234
- }
235
-
236
- .opacity3 {
237
- opacity: .3
238
- }
239
-
240
- .opacity4 {
241
- opacity: .4
242
- }
243
-
244
- .opacity5 {
245
- opacity: .5
246
- }
247
-
248
- .opacity6 {
249
- opacity: .6
250
- }
251
-
252
- .opacity7 {
253
- opacity: .7
254
- }
255
-
256
- .opacity8 {
257
- opacity: .8
258
- }
259
-
260
- .opacity9 {
261
- opacity: .9
262
- }
263
-
264
- .bg-red {
265
- background-color: red;
266
- }
267
-
268
- .bg-blue {
269
- background-color: blue;
270
- }
271
-
272
- .bg-yellow {
273
- background-color: yellow;
274
- }
275
-
276
- .bg-green {
277
- background-color: green;
278
- }
279
-
280
- .bg-black {
281
- background-color: black;
282
- }
283
-
284
- .white {
285
- color: white;
286
- }
package/public/theme.css DELETED
@@ -1,9 +0,0 @@
1
- :root {
2
- --bg-color: #1d1d1d;
3
- --color-primary: #f0dda3;
4
- --color-secondary: #ff9800;
5
- --color-tertiary: #f44336;
6
- --color-quaternary: #9c27b0;
7
- --color-quinary: #2196f3;
8
- --color-senary: #009688;
9
- }