enigmatic 0.34.0 → 0.35.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.
- package/README.md +61 -95
- package/bin/enigmatic.js +3 -0
- package/client/public/AGENTS.md +314 -0
- package/{public → client/public}/client.js +13 -6
- package/client/public/index.html +197 -0
- package/clientserver.png +0 -0
- package/package.json +7 -9
- package/server/bun-server.js +119 -0
- package/__tests__/e2.test.js +0 -340
- package/__tests__/jest.config.js +0 -7
- package/__tests__/jest.setup.js +0 -9
- package/beemap.js +0 -47
- package/bun-server.js +0 -122
- package/public/index.html +0 -48
- package/public/index2.html +0 -10
- /package/{public → client/public}/custom.js +0 -0
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# Enigmatic
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
The diagram above shows the interaction between the client (browser), Bun server, and external services (Auth0 and Cloudflare R2/S3).
|
|
12
|
+
|
|
7
13
|
## Quick Start
|
|
8
14
|
|
|
9
15
|
### Using client.js via CDN
|
|
@@ -14,66 +20,54 @@ Include `client.js` in any HTML file using the unpkg CDN:
|
|
|
14
20
|
<!DOCTYPE html>
|
|
15
21
|
<html>
|
|
16
22
|
<head>
|
|
17
|
-
<script src="https://unpkg.com/enigmatic
|
|
23
|
+
<script src="https://unpkg.com/enigmatic"></script>
|
|
24
|
+
<script src="https://unpkg.com/enigmatic/client/public/custom.js"></script>
|
|
18
25
|
<script>
|
|
19
|
-
// Configure your API URL
|
|
20
26
|
window.api_url = 'https://your-server.com';
|
|
21
|
-
|
|
22
|
-
// Define custom elements
|
|
23
|
-
window.custom = {
|
|
24
|
-
"hello-world": (data) => `Hello ${data || 'World'}`
|
|
25
|
-
};
|
|
27
|
+
window.state.message = 'Hello World';
|
|
26
28
|
</script>
|
|
27
29
|
</head>
|
|
28
30
|
<body>
|
|
29
31
|
<hello-world data="message"></hello-world>
|
|
30
|
-
<script>
|
|
31
|
-
window.state.message = "Hello World";
|
|
32
|
-
</script>
|
|
33
32
|
</body>
|
|
34
33
|
</html>
|
|
35
34
|
```
|
|
36
35
|
|
|
37
|
-
**Note:**
|
|
36
|
+
**Note:** Use `enigmatic@0.35.0` (or latest) in the URL to pin a version.
|
|
38
37
|
|
|
39
38
|
### Using the Bun Server
|
|
40
39
|
|
|
41
|
-
The Bun server provides a complete backend
|
|
42
|
-
- Key-value storage (
|
|
43
|
-
- File storage
|
|
44
|
-
- Authentication
|
|
45
|
-
- Static
|
|
40
|
+
The Bun server provides a complete backend with:
|
|
41
|
+
- **Key-value storage** – Per-user KV persisted as append-only JSONL (`server/kv/{user}.jsonl`) with `update`/`delete` actions and timestamps
|
|
42
|
+
- **File storage** – Per-user files via Cloudflare R2 (or S3-compatible API)
|
|
43
|
+
- **Authentication** – Auth0 OAuth2 login/logout
|
|
44
|
+
- **Static files** – Served from `client/public/`
|
|
46
45
|
|
|
47
46
|
#### Installation
|
|
48
47
|
|
|
49
|
-
1. Install Bun
|
|
48
|
+
1. Install [Bun](https://bun.sh):
|
|
50
49
|
```bash
|
|
51
50
|
curl -fsSL https://bun.sh/install | bash
|
|
52
51
|
```
|
|
53
52
|
|
|
54
|
-
2. Install dependencies:
|
|
53
|
+
2. Install dependencies (if any):
|
|
55
54
|
```bash
|
|
56
55
|
bun install
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
3.
|
|
60
|
-
```bash
|
|
61
|
-
cd server
|
|
62
|
-
./generate-certs.sh
|
|
63
|
-
cd ..
|
|
64
|
-
```
|
|
58
|
+
3. TLS certificates: place `cert.pem` and `key.pem` in `server/certs/` for HTTPS (required for Auth0 in production).
|
|
65
59
|
|
|
66
60
|
#### Environment Variables
|
|
67
61
|
|
|
68
|
-
Create a `.env` file in the project root
|
|
62
|
+
Create a `.env` file in the project root (or set env vars):
|
|
69
63
|
|
|
70
64
|
```bash
|
|
71
|
-
# Auth0
|
|
65
|
+
# Auth0
|
|
72
66
|
AUTH0_DOMAIN=your-tenant.auth0.com
|
|
73
67
|
AUTH0_CLIENT_ID=your-client-id
|
|
74
68
|
AUTH0_CLIENT_SECRET=your-client-secret
|
|
75
69
|
|
|
76
|
-
# Cloudflare R2
|
|
70
|
+
# Cloudflare R2 (optional, for file storage)
|
|
77
71
|
CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
|
|
78
72
|
CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
|
|
79
73
|
CLOUDFLARE_BUCKET_NAME=your-bucket-name
|
|
@@ -82,37 +76,33 @@ CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com
|
|
|
82
76
|
|
|
83
77
|
#### Running the Server
|
|
84
78
|
|
|
85
|
-
Start the server with hot reload:
|
|
86
79
|
```bash
|
|
87
80
|
npm start
|
|
88
81
|
# or
|
|
89
|
-
|
|
82
|
+
npx enigmatic
|
|
83
|
+
# or with hot reload
|
|
84
|
+
npm run hot
|
|
90
85
|
```
|
|
91
86
|
|
|
92
|
-
|
|
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)
|
|
87
|
+
Server runs at **https://localhost:3000** (HTTPS is required for Auth0 cookies).
|
|
101
88
|
|
|
102
89
|
#### Server Endpoints
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
91
|
+
| Method | Path | Description |
|
|
92
|
+
|----------|------------|-------------|
|
|
93
|
+
| GET | `/` | Serves `client/public/index.html` |
|
|
94
|
+
| GET | `/index.html`, `/*.js`, etc. | Static files from `client/public/` |
|
|
95
|
+
| GET | `/login` | Redirects to Auth0 login |
|
|
96
|
+
| GET | `/callback`| Auth0 OAuth callback |
|
|
97
|
+
| GET | `/logout` | Logs out and clears session |
|
|
98
|
+
| GET | `/me` | Current user or 401 (no auth) |
|
|
99
|
+
| GET | `/{key}` | KV get (auth required) |
|
|
100
|
+
| POST | `/{key}` | KV set (auth required) |
|
|
101
|
+
| DELETE | `/{key}` | KV delete (auth required) |
|
|
102
|
+
| PUT | `/{key}` | Upload file to R2 (auth required) |
|
|
103
|
+
| PURGE | `/{key}` | Delete file from R2 (auth required) |
|
|
104
|
+
| PROPFIND | `/` | List R2 files (auth required) |
|
|
105
|
+
| PATCH | `/{key}` | Download file from R2 (auth required) |
|
|
116
106
|
|
|
117
107
|
## Overview
|
|
118
108
|
|
|
@@ -283,6 +273,16 @@ window.logout();
|
|
|
283
273
|
|
|
284
274
|
**Behavior:** Sets `window.location.href` to `{api_url}/logout`
|
|
285
275
|
|
|
276
|
+
#### `window.me()`
|
|
277
|
+
Returns the current user if authenticated, or `null` if not (e.g. 401).
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
const user = await window.me();
|
|
281
|
+
// user is { sub, email, ... } or null
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Endpoint:** `GET {api_url}/me` (with credentials)
|
|
285
|
+
|
|
286
286
|
## Custom Elements System
|
|
287
287
|
|
|
288
288
|
Custom elements are defined in `window.custom` object and automatically initialized when the DOM loads or when elements are added dynamically.
|
|
@@ -397,42 +397,18 @@ window.get('key').catch(err => console.error(err));
|
|
|
397
397
|
<!DOCTYPE html>
|
|
398
398
|
<html>
|
|
399
399
|
<head>
|
|
400
|
-
<script src="https://unpkg.com/enigmatic
|
|
400
|
+
<script src="https://unpkg.com/enigmatic"></script>
|
|
401
|
+
<script src="https://unpkg.com/enigmatic/client/public/custom.js"></script>
|
|
401
402
|
<script>
|
|
402
|
-
|
|
403
|
-
window.
|
|
404
|
-
|
|
405
|
-
// Define custom elements
|
|
406
|
-
window.custom = {
|
|
407
|
-
"hello-world": (data) => `Hello ${data || 'World'}`
|
|
408
|
-
};
|
|
403
|
+
window.api_url = window.api_url || window.location.origin;
|
|
404
|
+
window.state.message = 'World';
|
|
409
405
|
</script>
|
|
410
406
|
</head>
|
|
411
407
|
<body>
|
|
412
|
-
<!-- Custom element with reactive state -->
|
|
413
408
|
<hello-world data="message"></hello-world>
|
|
414
|
-
|
|
409
|
+
<file-widget></file-widget>
|
|
415
410
|
<script>
|
|
416
|
-
|
|
417
|
-
window.state.message = "Hello World";
|
|
418
|
-
|
|
419
|
-
// Use API functions
|
|
420
|
-
(async () => {
|
|
421
|
-
await window.set('test', 'value');
|
|
422
|
-
const value = await window.get('test');
|
|
423
|
-
console.log(value);
|
|
424
|
-
|
|
425
|
-
// Upload file
|
|
426
|
-
const fileInput = document.querySelector('input[type="file"]');
|
|
427
|
-
fileInput.onchange = async (e) => {
|
|
428
|
-
const file = e.target.files[0];
|
|
429
|
-
await window.put(file.name, file);
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
// List files
|
|
433
|
-
const files = await window.list();
|
|
434
|
-
console.log(files);
|
|
435
|
-
})();
|
|
411
|
+
window.me().then(u => console.log(u ? 'Logged in as ' + u.email : 'Not logged in'));
|
|
436
412
|
</script>
|
|
437
413
|
</body>
|
|
438
414
|
</html>
|
|
@@ -448,7 +424,7 @@ window.get('key').catch(err => console.error(err));
|
|
|
448
424
|
- `URL.createObjectURL`
|
|
449
425
|
- `MutationObserver` API
|
|
450
426
|
|
|
451
|
-
**Note:**
|
|
427
|
+
**Note:** Load `client.js` first, then your custom element definitions (e.g. `custom.js`); the Proxy initializes elements when definitions are assigned.
|
|
452
428
|
|
|
453
429
|
## Notes
|
|
454
430
|
|
|
@@ -464,19 +440,9 @@ window.get('key').catch(err => console.error(err));
|
|
|
464
440
|
|
|
465
441
|
## Development
|
|
466
442
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
```
|
|
443
|
+
- **Start server:** `npm start` or `npx enigmatic`
|
|
444
|
+
- **Hot reload:** `npm run hot`
|
|
445
|
+
- **Client:** Load `client.js` from unpkg or from `client/public/client.js` when serving locally. Load `custom.js` (or your definitions) after `client.js`; set `window.api_url` before making API calls.
|
|
480
446
|
|
|
481
447
|
## License
|
|
482
448
|
|
package/bin/enigmatic.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Custom Web Components Instructions for LLMs
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
When users request web components to be created, you MUST use the `window.custom` system. You can see examples at https://unpkg.com/enigmatic/client/public/custom.js or in the repo at `client/public/custom.js`. This system allows you to define custom HTML elements that automatically render and can be reactive to state changes.
|
|
6
|
+
|
|
7
|
+
## System Architecture
|
|
8
|
+
|
|
9
|
+
The custom component system works by:
|
|
10
|
+
1. Defining components in `window.custom` object
|
|
11
|
+
2. Using custom HTML tags in the DOM (e.g., `<my-component></my-component>`)
|
|
12
|
+
3. Components automatically initialize when the DOM loads or when elements are added
|
|
13
|
+
4. Components can be reactive to `window.state` changes via `data` attributes
|
|
14
|
+
|
|
15
|
+
## Component Definition Format
|
|
16
|
+
|
|
17
|
+
### Format 1: Function-Based Component (Simple)
|
|
18
|
+
|
|
19
|
+
**Use this format for simple components that just render HTML:**
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
window.custom = {
|
|
23
|
+
"mycomponent": (data) => {
|
|
24
|
+
// data is the value from window.state if element has data="key" attribute
|
|
25
|
+
// If no data attribute, data will be undefined
|
|
26
|
+
return `<div>Your HTML here: ${data || 'default'}</div>`;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Example from custom.js:**
|
|
32
|
+
```javascript
|
|
33
|
+
window.custom = {
|
|
34
|
+
"hello-world": (data) => `Hello ${data}`,
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**HTML Usage:**
|
|
39
|
+
```html
|
|
40
|
+
<hello-world></hello-world>
|
|
41
|
+
<!-- or with reactive state -->
|
|
42
|
+
<hello-world data="message"></hello-world>
|
|
43
|
+
<script>
|
|
44
|
+
window.state.message = "World"; // Component updates automatically
|
|
45
|
+
</script>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Format 2: Object-Based Component (Advanced)
|
|
49
|
+
|
|
50
|
+
**Use this format when you need methods, properties, or more complex logic:**
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
window.custom = {
|
|
54
|
+
"component-name": {
|
|
55
|
+
// Optional helper methods
|
|
56
|
+
prop: (data) => `Processed: ${data}`,
|
|
57
|
+
|
|
58
|
+
// Required: render method that returns HTML string
|
|
59
|
+
render: function(data) {
|
|
60
|
+
// Use 'this' to access other methods/properties
|
|
61
|
+
return `<div>${this.prop(data)}</div>`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Example from custom.js:**
|
|
68
|
+
```javascript
|
|
69
|
+
window.custom = {
|
|
70
|
+
"hello-world-2": {
|
|
71
|
+
prop: (data) => `${data} World`,
|
|
72
|
+
render: function(data) {
|
|
73
|
+
return this.prop(data);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**HTML Usage:**
|
|
80
|
+
```html
|
|
81
|
+
<hello-world-2 data="greeting"></hello-world-2>
|
|
82
|
+
<script>
|
|
83
|
+
window.state.greeting = "Hello"; // Component updates automatically
|
|
84
|
+
</script>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Format 3: Async Component
|
|
88
|
+
|
|
89
|
+
**Use this format when you need to fetch data or perform async operations:**
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
window.custom = {
|
|
93
|
+
"async-component": async () => {
|
|
94
|
+
try {
|
|
95
|
+
const data = await window.get('some-key');
|
|
96
|
+
// Or use other API functions: window.list(), window.set(), etc.
|
|
97
|
+
return `<div>${JSON.stringify(data)}</div>`;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return `<div>Error: ${err.message}</div>`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Example from custom.js:**
|
|
106
|
+
```javascript
|
|
107
|
+
window.custom = {
|
|
108
|
+
"file-widget": async () => {
|
|
109
|
+
try {
|
|
110
|
+
const list = await window.list();
|
|
111
|
+
// Render file list with styles and interactive elements
|
|
112
|
+
return `<div>...</div>`;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return `<div>Error occurred</div>`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Key Rules and Patterns
|
|
121
|
+
|
|
122
|
+
### 1. Component Names
|
|
123
|
+
- Use kebab-case (e.g., `"my-component"`, `"file-widget"`)
|
|
124
|
+
- Must match the HTML tag name exactly (case-insensitive)
|
|
125
|
+
- Example: `window.custom["my-component"]` → `<my-component></my-component>`
|
|
126
|
+
|
|
127
|
+
### 2. Return Value
|
|
128
|
+
- Components MUST return a string containing HTML
|
|
129
|
+
- Can include inline `<style>` tags for component-specific styles
|
|
130
|
+
- Can include inline event handlers using `onclick`, `onchange`, etc.
|
|
131
|
+
|
|
132
|
+
### 3. Reactive State Integration
|
|
133
|
+
- Components automatically receive state values when element has `data="key"` attribute
|
|
134
|
+
- When `window.state.key = value` is set, all elements with `data="key"` are re-rendered
|
|
135
|
+
- Access state value as the first parameter: `(data) => ...` or `render: function(data) { ... }`
|
|
136
|
+
|
|
137
|
+
### 4. Async Operations
|
|
138
|
+
- Use `async` functions when you need to fetch data
|
|
139
|
+
- Use `window.get()`, `window.set()`, `window.list()`, `window.put()`, `window.delete()`, `window.purge()`, `window.me()`, `window.download()`, etc. for API calls (all use `window.api_url`)
|
|
140
|
+
- Always wrap async operations in try-catch for error handling
|
|
141
|
+
|
|
142
|
+
### 5. Styling
|
|
143
|
+
- Include styles inline using `<style>` tags within the returned HTML
|
|
144
|
+
- Use scoped class names (e.g., `.w-c`, `.w-i`) to avoid conflicts
|
|
145
|
+
- Keep styles minimal and component-specific
|
|
146
|
+
|
|
147
|
+
### 6. Event Handlers
|
|
148
|
+
- Use inline event handlers: `onclick="..."`, `onchange="..."`
|
|
149
|
+
- Can call `window` API functions directly: `onclick="window.download('file.txt')"`
|
|
150
|
+
- For async operations, wrap in IIFE: `onclick="(async()=>{await window.purge('file');location.reload()})()"`
|
|
151
|
+
|
|
152
|
+
## Complete Examples
|
|
153
|
+
|
|
154
|
+
### Example 1: Simple Counter Component
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
window.custom = {
|
|
158
|
+
"counter": (data) => {
|
|
159
|
+
const count = data || 0;
|
|
160
|
+
return `
|
|
161
|
+
<div style="padding: 20px; text-align: center;">
|
|
162
|
+
<h2>Count: ${count}</h2>
|
|
163
|
+
<button onclick="window.state.count = (window.state.count || 0) + 1">+</button>
|
|
164
|
+
<button onclick="window.state.count = (window.state.count || 0) - 1">-</button>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**HTML:**
|
|
172
|
+
```html
|
|
173
|
+
<counter data="count"></counter>
|
|
174
|
+
<script>
|
|
175
|
+
window.state.count = 0;
|
|
176
|
+
</script>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Example 2: Data Display Component
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
window.custom = {
|
|
183
|
+
"data-display": async () => {
|
|
184
|
+
try {
|
|
185
|
+
const data = await window.get('user-data');
|
|
186
|
+
return `
|
|
187
|
+
<div style="padding: 15px; border: 1px solid #ddd; border-radius: 5px;">
|
|
188
|
+
<h3>User Data</h3>
|
|
189
|
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
|
190
|
+
</div>
|
|
191
|
+
`;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
return `<div style="color: red;">Error loading data: ${err.message}</div>`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**HTML:**
|
|
200
|
+
```html
|
|
201
|
+
<data-display></data-display>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Example 3: Form Component with Object Methods
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
window.custom = {
|
|
208
|
+
"contact-form": {
|
|
209
|
+
validate: (email) => email && email.includes('@'),
|
|
210
|
+
|
|
211
|
+
render: function(data) {
|
|
212
|
+
const email = data?.email || '';
|
|
213
|
+
const isValid = this.validate(email);
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
<style>
|
|
217
|
+
.form-container { padding: 20px; max-width: 400px; }
|
|
218
|
+
.form-input { width: 100%; padding: 8px; margin: 5px 0; }
|
|
219
|
+
.form-submit { background: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; }
|
|
220
|
+
</style>
|
|
221
|
+
<div class="form-container">
|
|
222
|
+
<input type="email" class="form-input"
|
|
223
|
+
placeholder="Email"
|
|
224
|
+
value="${email}"
|
|
225
|
+
onchange="window.state.formData = {...(window.state.formData || {}), email: this.value}">
|
|
226
|
+
<button class="form-submit"
|
|
227
|
+
onclick="alert('Form submitted!')"
|
|
228
|
+
${!isValid ? 'disabled' : ''}>
|
|
229
|
+
Submit
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**HTML:**
|
|
239
|
+
```html
|
|
240
|
+
<contact-form data="formData"></contact-form>
|
|
241
|
+
<script>
|
|
242
|
+
window.state.formData = { email: '' };
|
|
243
|
+
</script>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## When to Use Each Format
|
|
247
|
+
|
|
248
|
+
- **Function format**: Simple components, static content, or when you only need the data parameter
|
|
249
|
+
- **Object format**: When you need helper methods, want to organize code better, or need `this` context
|
|
250
|
+
- **Async format**: When you need to fetch data from APIs, perform async operations, or load external resources
|
|
251
|
+
|
|
252
|
+
## Important Notes
|
|
253
|
+
|
|
254
|
+
1. **Always return HTML strings** - Components must return valid HTML strings
|
|
255
|
+
2. **Handle errors** - Wrap async operations in try-catch blocks
|
|
256
|
+
3. **Use kebab-case** - Component names must be in kebab-case to match HTML tag names
|
|
257
|
+
4. **State reactivity** - Use `data="key"` attribute to make components reactive to `window.state[key]`
|
|
258
|
+
5. **Auto-initialization** - Components automatically initialize when:
|
|
259
|
+
- DOM loads
|
|
260
|
+
- Component is added to `window.custom`
|
|
261
|
+
- New matching elements are added to the DOM
|
|
262
|
+
|
|
263
|
+
## Common Patterns
|
|
264
|
+
|
|
265
|
+
### Pattern: Loading State
|
|
266
|
+
```javascript
|
|
267
|
+
window.custom = {
|
|
268
|
+
"loading-component": async () => {
|
|
269
|
+
try {
|
|
270
|
+
const data = await window.get('data');
|
|
271
|
+
return data ? `<div>${data}</div>` : '<div>No data</div>';
|
|
272
|
+
} catch (err) {
|
|
273
|
+
return `<div>Error: ${err.message}</div>`;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Pattern: Error Handling
|
|
280
|
+
```javascript
|
|
281
|
+
window.custom = {
|
|
282
|
+
"safe-component": async () => {
|
|
283
|
+
try {
|
|
284
|
+
const result = await window.list();
|
|
285
|
+
return `<div>Success: ${result.length} items</div>`;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return `<div style="color: red;">Error: ${err.message}</div>`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Pattern: Conditional Rendering
|
|
294
|
+
```javascript
|
|
295
|
+
window.custom = {
|
|
296
|
+
"conditional": (data) => {
|
|
297
|
+
if (!data) return '<div>No data</div>';
|
|
298
|
+
if (data.error) return `<div>Error: ${data.error}</div>`;
|
|
299
|
+
return `<div>Data: ${data.value}</div>`;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Summary
|
|
305
|
+
|
|
306
|
+
When creating web components:
|
|
307
|
+
1. Define them in `window.custom` object
|
|
308
|
+
2. Use kebab-case names matching HTML tag names
|
|
309
|
+
3. Return HTML strings (can include styles and event handlers)
|
|
310
|
+
4. Use function format for simple components
|
|
311
|
+
5. Use object format with `render` method for complex components
|
|
312
|
+
6. Use async functions when fetching data
|
|
313
|
+
7. Make components reactive by using `data="key"` attributes
|
|
314
|
+
8. Always handle errors in async components
|
|
@@ -53,17 +53,24 @@ const req = (m, k, b) => fetch(`${W.api_url}/${k ? Enc(k) : ''}`, {
|
|
|
53
53
|
method: m, body: b instanceof Blob || typeof b === 'string' ? b : JSON.stringify(b)
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
const toJson = (r) => {
|
|
57
|
+
const ct = (r.headers.get('content-type') || '').toLowerCase();
|
|
58
|
+
if (!ct.includes('application/json')) return r.text().then((t) => { throw new Error('Server returned non-JSON (HTML?): ' + (t.slice(0, 60) || r.status)); });
|
|
59
|
+
return r.json();
|
|
60
|
+
};
|
|
61
|
+
|
|
56
62
|
Object.assign(W, {
|
|
57
63
|
$: s => D.querySelector(s),
|
|
58
64
|
$$: s => D.querySelectorAll(s),
|
|
59
65
|
$c: s => $0.closest(s),
|
|
60
66
|
state: sProx,
|
|
61
|
-
get: k => req('GET', k).then(
|
|
62
|
-
set: (k, v) => req('POST', k, v).then(
|
|
63
|
-
put: (k, v) => req('PUT', k, v).then(
|
|
64
|
-
delete: k => req('DELETE', k).then(
|
|
65
|
-
purge: k => req('PURGE', k).then(
|
|
66
|
-
list: () => req('PROPFIND').then(
|
|
67
|
+
get: k => req('GET', k).then(toJson),
|
|
68
|
+
set: (k, v) => req('POST', k, v).then(toJson),
|
|
69
|
+
put: (k, v) => req('PUT', k, v).then(toJson),
|
|
70
|
+
delete: k => req('DELETE', k).then(toJson),
|
|
71
|
+
purge: k => req('PURGE', k).then(toJson),
|
|
72
|
+
list: () => req('PROPFIND').then(toJson),
|
|
73
|
+
me: () => fetch(`${W.api_url}/me`, { credentials: "include" }).then((r) => (r.ok ? r.json() : null)),
|
|
67
74
|
login: () => W.location.href = `${W.api_url}/login`,
|
|
68
75
|
logout: () => W.location.href = `${W.api_url}/logout`,
|
|
69
76
|
download: async (k) => {
|