enigmatic 0.35.0 → 0.36.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 CHANGED
@@ -127,7 +127,7 @@ const elements = window.$$('.my-class');
127
127
  ### API Base URL
128
128
 
129
129
  ```javascript
130
- window.api_url = "https://localhost:3000"
130
+ window.api_url = "https://localhost:3001"
131
131
  ```
132
132
 
133
133
  Configures the base URL for all API requests. Modify this to point to your server.
@@ -0,0 +1,202 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Enigmatic Webapp</title>
7
+
8
+ <script>
9
+ window.api_url = "https://digplan.app"
10
+ </script>
11
+
12
+ <style>
13
+ :root {
14
+ --bg: #f1f5f9;
15
+ --surface: #ffffff;
16
+ --text: #0f172a;
17
+ --text-muted: #64748b;
18
+ --accent: #6366f1;
19
+ --accent-hover: #4f46e5;
20
+ --border: #e2e8f0;
21
+ --input-bg: #f8fafc;
22
+ --radius: 12px;
23
+ --radius-sm: 8px;
24
+ --shadow: 0 1px 3px rgba(0,0,0,.06);
25
+ --shadow-md: 0 4px 12px rgba(0,0,0,.06);
26
+ }
27
+ * { box-sizing: border-box; }
28
+ body {
29
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
30
+ font-size: 15px;
31
+ line-height: 1.6;
32
+ max-width: 600px;
33
+ margin: 0 auto;
34
+ padding: 32px 24px;
35
+ background: var(--bg);
36
+ color: var(--text);
37
+ -webkit-font-smoothing: antialiased;
38
+ }
39
+ h1 {
40
+ font-size: 1.75rem;
41
+ font-weight: 700;
42
+ letter-spacing: -0.02em;
43
+ margin: 0 0 28px;
44
+ color: var(--text);
45
+ }
46
+ section {
47
+ background: var(--surface);
48
+ border-radius: var(--radius);
49
+ padding: 20px;
50
+ margin-bottom: 20px;
51
+ box-shadow: var(--shadow);
52
+ border: 1px solid var(--border);
53
+ }
54
+ section h2 {
55
+ font-size: 0.8125rem;
56
+ font-weight: 600;
57
+ letter-spacing: 0.02em;
58
+ text-transform: uppercase;
59
+ color: var(--text-muted);
60
+ margin: 0 0 14px;
61
+ }
62
+ section p { margin: 0 0 12px; color: var(--text-muted); font-size: 0.9375rem; }
63
+ input {
64
+ font-size: 14px;
65
+ padding: 10px 14px;
66
+ border-radius: var(--radius-sm);
67
+ border: 1px solid var(--border);
68
+ background: var(--input-bg);
69
+ color: var(--text);
70
+ width: 100%;
71
+ max-width: 200px;
72
+ transition: border-color .15s, box-shadow .15s;
73
+ }
74
+ input:focus {
75
+ outline: none;
76
+ border-color: var(--accent);
77
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, .15);
78
+ }
79
+ input::placeholder { color: var(--text-muted); opacity: .8; }
80
+ button {
81
+ font-size: 14px;
82
+ font-weight: 500;
83
+ padding: 10px 16px;
84
+ border-radius: var(--radius-sm);
85
+ border: none;
86
+ cursor: pointer;
87
+ transition: background .15s, transform .05s;
88
+ }
89
+ button:active { transform: scale(0.98); }
90
+ button:not(.secondary) {
91
+ background: var(--accent);
92
+ color: #fff;
93
+ margin-right: 8px;
94
+ }
95
+ button:not(.secondary):hover { background: var(--accent-hover); }
96
+ button.secondary {
97
+ background: var(--input-bg);
98
+ color: var(--text);
99
+ border: 1px solid var(--border);
100
+ }
101
+ button.secondary:hover { background: var(--border); }
102
+ #auth-status { margin-right: 12px; color: var(--text-muted); font-size: 14px; }
103
+ .row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
104
+ .row:last-of-type { margin-bottom: 0; }
105
+ #kv-result {
106
+ margin-top: 12px;
107
+ padding: 12px 14px;
108
+ background: var(--input-bg);
109
+ border-radius: var(--radius-sm);
110
+ font-size: 13px;
111
+ font-family: ui-monospace, monospace;
112
+ white-space: pre-wrap;
113
+ word-break: break-all;
114
+ min-height: 24px;
115
+ border: 1px solid var(--border);
116
+ }
117
+ .error { color: #dc2626; }
118
+ .reactive-split { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
119
+ .reactive-demo { flex: 1; min-width: 180px; }
120
+ .reactive-snippet {
121
+ flex: 1;
122
+ min-width: 220px;
123
+ font-size: 12px;
124
+ background: #1e293b;
125
+ color: #e2e8f0;
126
+ border-radius: var(--radius-sm);
127
+ padding: 14px;
128
+ overflow-x: auto;
129
+ border: none;
130
+ }
131
+ .reactive-snippet pre { margin: 0; font-family: ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
132
+ </style>
133
+ <link rel="preconnect" href="https://fonts.googleapis.com">
134
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
135
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
136
+ </head>
137
+ <body>
138
+ <h1>Enigmatic Webapp</h1>
139
+
140
+ <section>
141
+ <h2>Auth</h2>
142
+ <div class="row">
143
+ <span id="auth-status">Checking…</span>
144
+ <button onclick="window.login()">Login</button>
145
+ <button class="secondary" onclick="window.logout()">Logout</button>
146
+ </div>
147
+ </section>
148
+
149
+ <section>
150
+ <h2>Reactive state</h2>
151
+ <p>Edit the message; the custom element updates automatically.</p>
152
+ <div class="reactive-split">
153
+ <div class="reactive-demo">
154
+ <div class="row">
155
+ <input type="text" id="msg-input" placeholder="Message" value="World">
156
+ <button onclick="window.state.message = window.$('#msg-input').value">Update</button>
157
+ </div>
158
+ <p style="margin: 12px 0 0; font-size: 14px;"><hello-world data="message"></hello-world></p>
159
+ </div>
160
+ <div class="reactive-snippet">
161
+ <pre>&lt;!-- HTML: data="message" binds to state.message --&gt;
162
+ &lt;hello-world data="message"&gt;&lt;/hello-world&gt;
163
+
164
+ // JS: updating state re-renders the element
165
+ window.state.message = "World";</pre>
166
+ </div>
167
+ </div>
168
+ </section>
169
+
170
+ <section>
171
+ <h2>KV storage</h2>
172
+ <div class="row">
173
+ <input type="text" id="kv-key" placeholder="Key" value="greeting">
174
+ <input type="text" id="kv-value" placeholder="Value" value="Hello from KV">
175
+ </div>
176
+ <div class="row">
177
+ <button onclick="window.get(window.$('#kv-key').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Get</button>
178
+ <button onclick="window.set(window.$('#kv-key').value, window.$('#kv-value').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Set</button>
179
+ <button onclick="window.delete(window.$('#kv-key').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Delete</button>
180
+ </div>
181
+ <div id="kv-result"></div>
182
+ </section>
183
+
184
+ <section>
185
+ <h2>Files</h2>
186
+ <file-widget></file-widget>
187
+ </section>
188
+
189
+ <script src="client.js"></script>
190
+ <script src="custom.js"></script>
191
+
192
+ <script>
193
+ window.state.message = window.$('#msg-input').value;
194
+
195
+ window.me().then(function(u) {
196
+ window.$('#auth-status').textContent = u ? ('Logged in as ' + (u.email || u.sub)) : 'Not logged in';
197
+ }).catch(function() {
198
+ window.$('#auth-status').textContent = 'Not logged in';
199
+ });
200
+ </script>
201
+ </body>
202
+ </html>
@@ -1,91 +1,74 @@
1
- const D = document, W = window, Enc = encodeURIComponent;
2
-
3
- // 1. Unified Render Logic (Handles both State & Custom Elements)
4
1
  const ren = async (el, v) => {
5
- const f = W.custom?.[el.tagName.toLowerCase()];
2
+ const f = window.custom?.[el.tagName.toLowerCase()];
6
3
  if (f) {
7
4
  const dataAttr = el.getAttribute('data');
8
- const val = v !== undefined ? v : (dataAttr ? W.state[dataAttr] : undefined);
5
+ const val = v !== undefined ? v : (dataAttr ? window.state[dataAttr] : undefined);
9
6
  try {
10
7
  if (f.render) {
11
8
  el.innerHTML = await f.render.call(f, val);
12
9
  } else if (typeof f === 'function') {
13
10
  el.innerHTML = await f(val);
14
11
  }
15
- } catch(e) { console.error(e) }
12
+ } catch (e) {
13
+ console.error(e);
14
+ }
16
15
  }
17
16
  };
18
17
 
19
- // 2. Proxies setup
20
- const cProx = new Proxy({}, {
21
- set(t, p, v) {
22
- t[p] = v;
23
- setTimeout(() => {
24
- if (W.$$ && D.body) {
25
- W.$$(p).forEach(el => ren(el));
26
- }
27
- }, 0);
28
- return true;
29
- }
30
- });
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
- });
18
+ window.custom = {};
42
19
 
20
+ // 2. State proxy
43
21
  const sProx = new Proxy({}, {
44
22
  set(o, p, v) {
45
23
  o[p] = v;
46
- W.$$(`[data="${p}"]`).forEach(el => ren(el, v));
24
+ window.$$(`[data="${p}"]`).forEach(el => ren(el, v));
47
25
  return true;
48
26
  }
49
27
  });
50
28
 
51
- // 3. API & DOM Helpers
52
- const req = (m, k, b) => fetch(`${W.api_url}/${k ? Enc(k) : ''}`, {
53
- method: m, body: b instanceof Blob || typeof b === 'string' ? b : JSON.stringify(b)
54
- });
29
+ // 4. API helpers
30
+ const req = (method, key, body) =>
31
+ fetch(`${window.api_url}/${key ? encodeURIComponent(key) : ''}`, {
32
+ method,
33
+ body: body instanceof Blob || typeof body === 'string' ? body : JSON.stringify(body),
34
+ credentials: 'include',
35
+ });
55
36
 
56
37
  const toJson = (r) => {
57
38
  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)); });
39
+ if (!ct.includes('application/json')) {
40
+ return r.text().then((t) => { throw new Error('Server returned non-JSON (HTML?): ' + (t.slice(0, 60) || r.status)); });
41
+ }
59
42
  return r.json();
60
43
  };
61
44
 
62
- Object.assign(W, {
63
- $: s => D.querySelector(s),
64
- $$: s => D.querySelectorAll(s),
65
- $c: s => $0.closest(s),
45
+ Object.assign(window, {
46
+ $: (s) => document.querySelector(s),
47
+ $$: (s) => document.querySelectorAll(s),
48
+ $c: (s) => $0.closest(s),
66
49
  state: sProx,
67
- get: k => req('GET', k).then(toJson),
50
+ get: (k) => req('GET', k).then(toJson),
68
51
  set: (k, v) => req('POST', k, v).then(toJson),
69
52
  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),
53
+ delete: (k) => req('DELETE', k).then(toJson),
54
+ purge: (k) => req('PURGE', k).then(toJson),
72
55
  list: () => req('PROPFIND').then(toJson),
73
- me: () => fetch(`${W.api_url}/me`, { credentials: "include" }).then((r) => (r.ok ? r.json() : null)),
74
- login: () => W.location.href = `${W.api_url}/login`,
75
- logout: () => W.location.href = `${W.api_url}/logout`,
56
+ me: () => fetch(`${window.api_url}/me`, { credentials: 'include' }).then((r) => (r.ok ? r.json() : null)),
57
+ login: () => window.location.href = `${window.api_url}/login`,
58
+ logout: () => window.location.href = `${window.api_url}/logout`,
76
59
  download: async (k) => {
77
60
  const r = await req('PATCH', k);
78
61
  if (!r.ok) throw new Error('Download failed');
79
- const a = D.createElement('a');
62
+ const a = document.createElement('a');
80
63
  a.href = URL.createObjectURL(await r.blob());
81
64
  a.download = k;
82
65
  a.click();
83
66
  URL.revokeObjectURL(a.href);
84
67
  },
85
68
  initCustomElements: () => {
86
- if (!D.body) return;
87
- Object.keys(W.custom || {}).forEach(t => {
88
- const elements = W.$$(t);
69
+ if (!document.body) return;
70
+ Object.keys(window.custom || {}).forEach((t) => {
71
+ const elements = window.$$(t);
89
72
  if (elements.length > 0) {
90
73
  elements.forEach(el => ren(el));
91
74
  }
@@ -93,33 +76,31 @@ Object.assign(W, {
93
76
  }
94
77
  });
95
78
 
96
- // 4. Initialization & Observers
79
+ // 5. Boot
97
80
  const boot = () => {
98
- if (W.initCustomElements) {
99
- // Run immediately and also after a short delay to catch any elements added during script execution
100
- W.initCustomElements();
101
- setTimeout(() => W.initCustomElements(), 10);
81
+ if (window.initCustomElements) {
82
+ window.initCustomElements();
83
+ setTimeout(() => window.initCustomElements(), 10);
102
84
  }
103
- if (D.body) {
85
+ if (document.body) {
104
86
  new MutationObserver((mutations) => {
105
- mutations.forEach(m => {
106
- m.addedNodes.forEach(node => {
107
- if (node.nodeType === 1) { // Element node
87
+ mutations.forEach((m) => {
88
+ m.addedNodes.forEach((node) => {
89
+ if (node.nodeType === 1) {
108
90
  const tag = node.tagName?.toLowerCase();
109
- if (tag && W.custom?.[tag]) ren(node);
110
- // Also check children
111
- node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach(child => {
91
+ if (tag && window.custom?.[tag]) ren(node);
92
+ node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach((child) => {
112
93
  const childTag = child.tagName?.toLowerCase();
113
- if (childTag && W.custom?.[childTag]) ren(child);
94
+ if (childTag && window.custom?.[childTag]) ren(child);
114
95
  });
115
96
  }
116
97
  });
117
98
  });
118
- }).observe(D.body, { childList: true, subtree: true });
99
+ }).observe(document.body, { childList: true, subtree: true });
119
100
  }
120
101
  };
121
- if (D.readyState === 'loading') {
122
- D.addEventListener('DOMContentLoaded', boot);
102
+ if (document.readyState === 'loading') {
103
+ document.addEventListener('DOMContentLoaded', boot);
123
104
  } else {
124
105
  setTimeout(boot, 0);
125
- }
106
+ }
@@ -3,195 +3,412 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Enigmatic Webapp</title>
6
+ <title>Enigmatic - Lightweight JavaScript Library</title>
7
7
  <style>
8
8
  :root {
9
- --bg: #f1f5f9;
9
+ --bg: #f8fafc;
10
10
  --surface: #ffffff;
11
11
  --text: #0f172a;
12
12
  --text-muted: #64748b;
13
13
  --accent: #6366f1;
14
14
  --accent-hover: #4f46e5;
15
15
  --border: #e2e8f0;
16
- --input-bg: #f8fafc;
17
- --radius: 12px;
18
- --radius-sm: 8px;
19
- --shadow: 0 1px 3px rgba(0,0,0,.06);
20
- --shadow-md: 0 4px 12px rgba(0,0,0,.06);
16
+ --code-bg: #1e293b;
17
+ --code-text: #e2e8f0;
21
18
  }
22
- * { box-sizing: border-box; }
19
+ * { box-sizing: border-box; margin: 0; padding: 0; }
23
20
  body {
24
21
  font-family: "Inter", system-ui, -apple-system, sans-serif;
25
- font-size: 15px;
22
+ font-size: 16px;
26
23
  line-height: 1.6;
27
- max-width: 600px;
28
- margin: 0 auto;
29
- padding: 32px 24px;
30
24
  background: var(--bg);
31
25
  color: var(--text);
32
26
  -webkit-font-smoothing: antialiased;
33
27
  }
28
+ .container {
29
+ max-width: 1200px;
30
+ margin: 0 auto;
31
+ padding: 0 24px;
32
+ }
33
+ header {
34
+ background: var(--surface);
35
+ border-bottom: 1px solid var(--border);
36
+ padding: 24px 0;
37
+ margin-bottom: 48px;
38
+ }
34
39
  h1 {
35
- font-size: 1.75rem;
40
+ font-size: 2.5rem;
36
41
  font-weight: 700;
37
42
  letter-spacing: -0.02em;
38
- margin: 0 0 28px;
39
- color: var(--text);
43
+ margin-bottom: 12px;
44
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
45
+ -webkit-background-clip: text;
46
+ -webkit-text-fill-color: transparent;
47
+ background-clip: text;
48
+ }
49
+ .tagline {
50
+ font-size: 1.25rem;
51
+ color: var(--text-muted);
52
+ margin-bottom: 32px;
53
+ }
54
+ .badge {
55
+ display: inline-block;
56
+ padding: 4px 12px;
57
+ background: var(--accent);
58
+ color: white;
59
+ border-radius: 6px;
60
+ font-size: 0.875rem;
61
+ font-weight: 500;
62
+ margin-bottom: 24px;
40
63
  }
41
64
  section {
42
65
  background: var(--surface);
43
- border-radius: var(--radius);
44
- padding: 20px;
45
- margin-bottom: 20px;
46
- box-shadow: var(--shadow);
66
+ border-radius: 12px;
67
+ padding: 32px;
68
+ margin-bottom: 32px;
69
+ box-shadow: 0 1px 3px rgba(0,0,0,.06);
47
70
  border: 1px solid var(--border);
48
71
  }
49
- section h2 {
50
- font-size: 0.8125rem;
72
+ h2 {
73
+ font-size: 1.5rem;
51
74
  font-weight: 600;
52
- letter-spacing: 0.02em;
53
- text-transform: uppercase;
54
- color: var(--text-muted);
55
- margin: 0 0 14px;
75
+ margin-bottom: 16px;
76
+ color: var(--text);
56
77
  }
57
- section p { margin: 0 0 12px; color: var(--text-muted); font-size: 0.9375rem; }
58
- input {
59
- font-size: 14px;
60
- padding: 10px 14px;
61
- border-radius: var(--radius-sm);
62
- border: 1px solid var(--border);
63
- background: var(--input-bg);
78
+ h3 {
79
+ font-size: 1.125rem;
80
+ font-weight: 600;
81
+ margin-top: 24px;
82
+ margin-bottom: 12px;
64
83
  color: var(--text);
65
- width: 100%;
66
- max-width: 200px;
67
- transition: border-color .15s, box-shadow .15s;
68
84
  }
69
- input:focus {
70
- outline: none;
71
- border-color: var(--accent);
72
- box-shadow: 0 0 0 3px rgba(99, 102, 241, .15);
85
+ p {
86
+ margin-bottom: 16px;
87
+ color: var(--text-muted);
73
88
  }
74
- input::placeholder { color: var(--text-muted); opacity: .8; }
75
- button {
76
- font-size: 14px;
77
- font-weight: 500;
78
- padding: 10px 16px;
79
- border-radius: var(--radius-sm);
80
- border: none;
81
- cursor: pointer;
82
- transition: background .15s, transform .05s;
83
- }
84
- button:active { transform: scale(0.98); }
85
- button:not(.secondary) {
89
+ ul, ol {
90
+ margin-left: 24px;
91
+ margin-bottom: 16px;
92
+ color: var(--text-muted);
93
+ }
94
+ li {
95
+ margin-bottom: 8px;
96
+ }
97
+ code {
98
+ background: var(--code-bg);
99
+ color: var(--code-text);
100
+ padding: 2px 6px;
101
+ border-radius: 4px;
102
+ font-family: ui-monospace, monospace;
103
+ font-size: 0.9em;
104
+ }
105
+ pre {
106
+ background: var(--code-bg);
107
+ color: var(--code-text);
108
+ padding: 20px;
109
+ border-radius: 8px;
110
+ overflow-x: auto;
111
+ margin: 16px 0;
112
+ font-family: ui-monospace, monospace;
113
+ font-size: 0.875rem;
114
+ line-height: 1.6;
115
+ }
116
+ pre code {
117
+ background: none;
118
+ padding: 0;
119
+ }
120
+ .btn {
121
+ display: inline-block;
122
+ padding: 12px 24px;
86
123
  background: var(--accent);
87
- color: #fff;
88
- margin-right: 8px;
124
+ color: white;
125
+ text-decoration: none;
126
+ border-radius: 8px;
127
+ font-weight: 500;
128
+ transition: background 0.15s;
129
+ margin-right: 12px;
130
+ margin-bottom: 12px;
131
+ }
132
+ .btn:hover {
133
+ background: var(--accent-hover);
89
134
  }
90
- button:not(.secondary):hover { background: var(--accent-hover); }
91
- button.secondary {
92
- background: var(--input-bg);
135
+ .btn-secondary {
136
+ background: var(--border);
93
137
  color: var(--text);
94
- border: 1px solid var(--border);
95
138
  }
96
- button.secondary:hover { background: var(--border); }
97
- #auth-status { margin-right: 12px; color: var(--text-muted); font-size: 14px; }
98
- .row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
99
- .row:last-of-type { margin-bottom: 0; }
100
- #kv-result {
101
- margin-top: 12px;
102
- padding: 12px 14px;
103
- background: var(--input-bg);
104
- border-radius: var(--radius-sm);
105
- font-size: 13px;
106
- font-family: ui-monospace, monospace;
107
- white-space: pre-wrap;
108
- word-break: break-all;
109
- min-height: 24px;
139
+ .btn-secondary:hover {
140
+ background: #cbd5e1;
141
+ }
142
+ .features {
143
+ display: grid;
144
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
145
+ gap: 24px;
146
+ margin-top: 24px;
147
+ }
148
+ .feature {
149
+ padding: 20px;
150
+ background: var(--bg);
151
+ border-radius: 8px;
110
152
  border: 1px solid var(--border);
111
153
  }
112
- .error { color: #dc2626; }
113
- .reactive-split { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
114
- .reactive-demo { flex: 1; min-width: 180px; }
115
- .reactive-snippet {
116
- flex: 1;
117
- min-width: 220px;
118
- font-size: 12px;
119
- background: #1e293b;
120
- color: #e2e8f0;
121
- border-radius: var(--radius-sm);
122
- padding: 14px;
123
- overflow-x: auto;
124
- border: none;
154
+ .feature h4 {
155
+ font-size: 1rem;
156
+ font-weight: 600;
157
+ margin-bottom: 8px;
158
+ color: var(--text);
159
+ }
160
+ .feature p {
161
+ font-size: 0.9375rem;
162
+ margin: 0;
163
+ }
164
+ table {
165
+ width: 100%;
166
+ border-collapse: collapse;
167
+ margin: 16px 0;
168
+ }
169
+ th, td {
170
+ padding: 12px;
171
+ text-align: left;
172
+ border-bottom: 1px solid var(--border);
173
+ }
174
+ th {
175
+ font-weight: 600;
176
+ color: var(--text);
177
+ background: var(--bg);
178
+ }
179
+ td {
180
+ color: var(--text-muted);
181
+ font-size: 0.9375rem;
182
+ }
183
+ .links {
184
+ display: flex;
185
+ gap: 16px;
186
+ flex-wrap: wrap;
187
+ margin-top: 24px;
188
+ }
189
+ footer {
190
+ text-align: center;
191
+ padding: 48px 0;
192
+ color: var(--text-muted);
193
+ font-size: 0.875rem;
125
194
  }
126
- .reactive-snippet pre { margin: 0; font-family: ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
127
195
  </style>
128
196
  <link rel="preconnect" href="https://fonts.googleapis.com">
129
197
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
130
198
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
131
199
  </head>
132
200
  <body>
133
- <h1>Enigmatic Webapp</h1>
134
-
135
- <section>
136
- <h2>Auth</h2>
137
- <div class="row">
138
- <span id="auth-status">Checking…</span>
139
- <button onclick="window.login()">Login</button>
140
- <button class="secondary" onclick="window.logout()">Logout</button>
201
+ <header>
202
+ <div class="container">
203
+ <span class="badge">v0.35.0</span>
204
+ <h1>Enigmatic</h1>
205
+ <p class="tagline">A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.</p>
206
+ <div class="links">
207
+ <a href="api.html" class="btn">Try Demo</a>
208
+ <a href="https://www.npmjs.com/package/enigmatic" class="btn btn-secondary" target="_blank">View on npm</a>
209
+ </div>
141
210
  </div>
142
- </section>
143
-
144
- <section>
145
- <h2>Reactive state</h2>
146
- <p>Edit the message; the custom element updates automatically.</p>
147
- <div class="reactive-split">
148
- <div class="reactive-demo">
149
- <div class="row">
150
- <input type="text" id="msg-input" placeholder="Message" value="World">
151
- <button onclick="window.state.message = window.$('#msg-input').value">Update</button>
211
+ </header>
212
+
213
+ <div class="container">
214
+ <section>
215
+ <h2>Quick Start</h2>
216
+ <h3>Using client.js via CDN</h3>
217
+ <p>Include <code>client.js</code> in any HTML file using the unpkg CDN:</p>
218
+ <pre><code>&lt;script src="https://unpkg.com/enigmatic"&gt;&lt;/script&gt;
219
+ &lt;script src="https://unpkg.com/enigmatic/client/public/custom.js"&gt;&lt;/script&gt;
220
+ &lt;script&gt;
221
+ window.api_url = 'https://your-server.com';
222
+ window.state.message = 'Hello World';
223
+ &lt;/script&gt;</code></pre>
224
+
225
+ <h3>Using the Bun Server</h3>
226
+ <p>The Bun server provides a complete backend with:</p>
227
+ <ul>
228
+ <li><strong>Key-value storage</strong> – Per-user KV persisted as append-only JSONL</li>
229
+ <li><strong>File storage</strong> – Per-user files via Cloudflare R2 (or S3-compatible API)</li>
230
+ <li><strong>Authentication</strong> – Auth0 OAuth2 login/logout</li>
231
+ <li><strong>Static files</strong> – Served from <code>client/public/</code></li>
232
+ </ul>
233
+
234
+ <h3>Installation</h3>
235
+ <ol>
236
+ <li>Install <a href="https://bun.sh" target="_blank">Bun</a>:
237
+ <pre><code>curl -fsSL https://bun.sh/install | bash</code></pre>
238
+ </li>
239
+ <li>Install dependencies:
240
+ <pre><code>bun install</code></pre>
241
+ </li>
242
+ <li>TLS certificates: place <code>cert.pem</code> and <code>key.pem</code> in <code>server/certs/</code> for HTTPS</li>
243
+ </ol>
244
+
245
+ <h3>Running the Server</h3>
246
+ <pre><code>npm start
247
+ # or
248
+ npx enigmatic
249
+ # or with hot reload
250
+ npm run hot</code></pre>
251
+ <p>Server runs at <strong>https://localhost:3000</strong> (HTTPS is required for Auth0 cookies).</p>
252
+ </section>
253
+
254
+ <section>
255
+ <h2>Features</h2>
256
+ <div class="features">
257
+ <div class="feature">
258
+ <h4>DOM Utilities</h4>
259
+ <p>Simple selectors: <code>window.$</code>, <code>window.$$</code>, <code>window.$c</code></p>
260
+ </div>
261
+ <div class="feature">
262
+ <h4>Reactive State</h4>
263
+ <p>Proxy-based state management that automatically updates DOM elements</p>
264
+ </div>
265
+ <div class="feature">
266
+ <h4>Custom Elements</h4>
267
+ <p>Automatic initialization and reactive updates for custom HTML elements</p>
268
+ </div>
269
+ <div class="feature">
270
+ <h4>KV Storage</h4>
271
+ <p>Simple key-value operations: <code>get</code>, <code>set</code>, <code>delete</code></p>
272
+ </div>
273
+ <div class="feature">
274
+ <h4>File Storage</h4>
275
+ <p>Upload, download, list, and delete files via R2/S3-compatible storage</p>
276
+ </div>
277
+ <div class="feature">
278
+ <h4>Authentication</h4>
279
+ <p>Built-in Auth0 OAuth2 integration with login/logout</p>
152
280
  </div>
153
- <p style="margin: 12px 0 0; font-size: 14px;"><hello-world data="message"></hello-world></p>
154
281
  </div>
155
- <div class="reactive-snippet">
156
- <pre>&lt;!-- HTML: data="message" binds to state.message --&gt;
157
- &lt;hello-world data="message"&gt;&lt;/hello-world&gt;
282
+ </section>
158
283
 
159
- // JS: updating state re-renders the element
160
- window.state.message = "World";</pre>
161
- </div>
162
- </div>
163
- </section>
284
+ <section>
285
+ <h2>API Endpoints</h2>
286
+ <table>
287
+ <thead>
288
+ <tr>
289
+ <th>Method</th>
290
+ <th>Path</th>
291
+ <th>Description</th>
292
+ </tr>
293
+ </thead>
294
+ <tbody>
295
+ <tr>
296
+ <td>GET</td>
297
+ <td><code>/</code></td>
298
+ <td>Serves index.html</td>
299
+ </tr>
300
+ <tr>
301
+ <td>GET</td>
302
+ <td><code>/login</code></td>
303
+ <td>Redirects to Auth0 login</td>
304
+ </tr>
305
+ <tr>
306
+ <td>GET</td>
307
+ <td><code>/callback</code></td>
308
+ <td>Auth0 OAuth callback</td>
309
+ </tr>
310
+ <tr>
311
+ <td>GET</td>
312
+ <td><code>/logout</code></td>
313
+ <td>Logs out and clears session</td>
314
+ </tr>
315
+ <tr>
316
+ <td>GET</td>
317
+ <td><code>/me</code></td>
318
+ <td>Current user or 401 (no auth)</td>
319
+ </tr>
320
+ <tr>
321
+ <td>GET</td>
322
+ <td><code>/{key}</code></td>
323
+ <td>KV get (auth required)</td>
324
+ </tr>
325
+ <tr>
326
+ <td>POST</td>
327
+ <td><code>/{key}</code></td>
328
+ <td>KV set (auth required)</td>
329
+ </tr>
330
+ <tr>
331
+ <td>DELETE</td>
332
+ <td><code>/{key}</code></td>
333
+ <td>KV delete (auth required)</td>
334
+ </tr>
335
+ <tr>
336
+ <td>PUT</td>
337
+ <td><code>/{key}</code></td>
338
+ <td>Upload file to R2 (auth required)</td>
339
+ </tr>
340
+ <tr>
341
+ <td>PURGE</td>
342
+ <td><code>/{key}</code></td>
343
+ <td>Delete file from R2 (auth required)</td>
344
+ </tr>
345
+ <tr>
346
+ <td>PROPFIND</td>
347
+ <td><code>/</code></td>
348
+ <td>List R2 files (auth required)</td>
349
+ </tr>
350
+ <tr>
351
+ <td>PATCH</td>
352
+ <td><code>/{key}</code></td>
353
+ <td>Download file from R2 (auth required)</td>
354
+ </tr>
355
+ </tbody>
356
+ </table>
357
+ </section>
164
358
 
165
- <section>
166
- <h2>KV storage</h2>
167
- <div class="row">
168
- <input type="text" id="kv-key" placeholder="Key" value="greeting">
169
- <input type="text" id="kv-value" placeholder="Value" value="Hello from KV">
170
- </div>
171
- <div class="row">
172
- <button onclick="window.get(window.$('#kv-key').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Get</button>
173
- <button onclick="window.set(window.$('#kv-key').value, window.$('#kv-value').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Set</button>
174
- <button onclick="window.delete(window.$('#kv-key').value).then(r => { window.$('#kv-result').textContent = JSON.stringify(r); window.$('#kv-result').classList.remove('error'); }).catch(e => { window.$('#kv-result').textContent = e.message; window.$('#kv-result').classList.add('error'); })">Delete</button>
359
+ <section>
360
+ <h2>Example Usage</h2>
361
+ <h3>Reactive State</h3>
362
+ <pre><code>&lt;hello-world data="message"&gt;&lt;/hello-world&gt;
363
+ &lt;script&gt;
364
+ window.custom['hello-world'] = (data) => `Hello ${data}`;
365
+ window.state.message = "World"; // Automatically updates the element
366
+ &lt;/script&gt;</code></pre>
367
+
368
+ <h3>KV Storage</h3>
369
+ <pre><code>// Get value
370
+ const value = await window.get('my-key');
371
+
372
+ // Set value
373
+ await window.set('my-key', 'my-value');
374
+
375
+ // Delete key
376
+ await window.delete('my-key');</code></pre>
377
+
378
+ <h3>File Operations</h3>
379
+ <pre><code>// Upload file
380
+ await window.put('filename.txt', fileBlob);
381
+
382
+ // List files
383
+ const files = await window.list();
384
+
385
+ // Download file
386
+ await window.download('filename.txt');
387
+
388
+ // Delete file
389
+ await window.purge('filename.txt');</code></pre>
390
+ </section>
391
+
392
+ <section>
393
+ <h2>Environment Variables</h2>
394
+ <p>Create a <code>.env</code> file in the project root:</p>
395
+ <pre><code># Auth0
396
+ AUTH0_DOMAIN=your-tenant.auth0.com
397
+ AUTH0_CLIENT_ID=your-client-id
398
+ AUTH0_CLIENT_SECRET=your-client-secret
399
+
400
+ # Cloudflare R2 (optional, for file storage)
401
+ CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
402
+ CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
403
+ CLOUDFLARE_BUCKET_NAME=your-bucket-name
404
+ CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com</code></pre>
405
+ </section>
406
+ </div>
407
+
408
+ <footer>
409
+ <div class="container">
410
+ <p>MIT License • Built with Bun</p>
175
411
  </div>
176
- <div id="kv-result"></div>
177
- </section>
178
-
179
- <section>
180
- <h2>Files</h2>
181
- <file-widget></file-widget>
182
- </section>
183
-
184
- <script src="client.js"></script>
185
- <script src="custom.js"></script>
186
- <script>
187
- window.api_url = window.api_url || window.location.origin;
188
- window.state.message = window.$('#msg-input').value;
189
-
190
- window.me().then(function(u) {
191
- window.$('#auth-status').textContent = u ? ('Logged in as ' + (u.email || u.sub)) : 'Not logged in';
192
- }).catch(function() {
193
- window.$('#auth-status').textContent = 'Not logged in';
194
- });
195
- </script>
412
+ </footer>
196
413
  </body>
197
414
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigmatic",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "bin": {
5
5
  "enigmatic": "./bin/enigmatic.js"
6
6
  },
@@ -8,9 +8,10 @@ const publicDir = join(dir, "..", "client", "public");
8
8
  const kvDir = join(dir, "kv");
9
9
  const sessions = new Map();
10
10
  const userKv = {};
11
+ let site_origin = "";
11
12
 
12
13
  const kvPath = (sub) => join(kvDir, `${String(sub).replace(/[^a-zA-Z0-9_-]/g, "_")}.jsonl`);
13
- const json = (d, s = 200, h = {}) => new Response(JSON.stringify(d), { status: s, headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json", ...h } });
14
+ const json = (d, s = 200, h = {}, origin = null) => new Response(JSON.stringify(d), { status: s, headers: { "Access-Control-Allow-Origin": origin || "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json", ...h } });
14
15
  const redir = (url, cookie) => new Response(null, { status: 302, headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) } });
15
16
 
16
17
  async function getUserMap(sub) {
@@ -58,10 +59,11 @@ const s3 = new S3Client({
58
59
  export default {
59
60
  async fetch(req) {
60
61
  const url = new URL(req.url), key = url.pathname.slice(1), cb = `${url.origin}/callback`;
62
+ const origin = req.headers.get("Origin") || url.origin;
61
63
  const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
62
64
  const user = (Bun.env.TEST_MODE === "1" && token === Bun.env.TEST_SESSION_ID) ? { sub: "test-user" } : (token ? sessions.get(token) : null);
63
65
 
64
- if (req.method === "OPTIONS") return json(null, 204);
66
+ if (req.method === "OPTIONS") return json(null, 204, {}, origin);
65
67
 
66
68
  if (req.method === "GET") {
67
69
  const p = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
@@ -71,47 +73,58 @@ export default {
71
73
  }
72
74
  }
73
75
 
74
- if (url.pathname === "/login") return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({ response_type: "code", client_id: Bun.env.AUTH0_CLIENT_ID, redirect_uri: cb, scope: "openid email profile" })}`);
76
+ if (url.pathname === "/login") {
77
+ site_origin = req.headers.get("referer");
78
+ return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({ response_type: "code", client_id: Bun.env.AUTH0_CLIENT_ID, redirect_uri: cb, scope: "openid email profile" })}`);
79
+ }
75
80
 
76
81
  if (url.pathname === "/callback") {
77
82
  const code = url.searchParams.get("code");
78
- if (!code) return json({ error: "No code" }, 400);
83
+ if (!code) return json({ error: "No code" }, 400, {}, origin);
79
84
  const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", client_id: Bun.env.AUTH0_CLIENT_ID, client_secret: Bun.env.AUTH0_CLIENT_SECRET, code, redirect_uri: cb }) });
80
- if (!tRes.ok) return json({ error: "Auth error" }, 401);
85
+ if (!tRes.ok) return json({ error: "Auth error" }, 401, {}, origin);
81
86
  const tokens = await tRes.json();
82
87
  const userInfo = await (await fetch(`https://${Bun.env.AUTH0_DOMAIN}/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` } })).json();
83
88
  const sid = crypto.randomUUID();
84
89
  sessions.set(sid, { ...userInfo, login_time: new Date().toISOString(), access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null });
85
- return redir(url.origin, `token=${sid}; HttpOnly; Path=/; Secure; SameSite=Lax; Max-Age=86400`);
90
+ return redir(site_origin || url.origin, `token=${sid}; HttpOnly; Path=/; Secure; SameSite=None; Max-Age=86400`);
86
91
  }
87
92
 
88
- if (url.pathname === "/me") return user ? json(user) : json({ error: "Unauthorized" }, 401);
89
- if (!token || !user) return json({ error: "Unauthorized" }, 401);
90
- if (url.pathname === "/logout") { sessions.delete(token); return redir(url.origin, "token=; Max-Age=0; Path=/"); }
93
+ if (url.pathname === "/me") return user ? json(user, 200, {}, origin) : json({ error: "Unauthorized" }, 401, {}, origin);
94
+ if (!token || !user) return json({ error: "Unauthorized" }, 401, {}, origin);
95
+ if (url.pathname === "/logout") { sessions.delete(token); return redir(url.origin, "token=; Max-Age=0; Path=/; Secure; SameSite=None"); }
91
96
 
92
97
  const m = await getUserMap(user.sub);
93
98
  switch (req.method) {
94
- case "GET": return json(m.get(key) ?? null);
99
+ case "GET": return json(m.get(key) ?? null, 200, {}, origin);
95
100
  case "POST":
96
101
  const val = await req.text();
97
102
  const v = (() => { try { return JSON.parse(val); } catch { return val; } })();
98
103
  m.set(key, v);
99
104
  await saveUserKv(user.sub, "update", key, v);
100
- return json({ key, value: v });
101
- case "DELETE": m.delete(key); await saveUserKv(user.sub, "delete", key); return json({ status: "Deleted" });
102
- case "PUT": await s3.write(`${user.sub}/${key}`, req.body); return json({ status: "Saved to R2" });
103
- case "PURGE": await s3.delete(`${user.sub}/${key}`); return json({ status: "Deleted from R2" });
105
+ return json({ key, value: v }, 200, {}, origin);
106
+ case "DELETE": m.delete(key); await saveUserKv(user.sub, "delete", key); return json({ status: "Deleted" }, 200, {}, origin);
107
+ case "PUT": await s3.write(`${user.sub}/${key}`, req.body); return json({ status: "Saved to R2" }, 200, {}, origin);
108
+ case "PURGE": await s3.delete(`${user.sub}/${key}`); return json({ status: "Deleted from R2" }, 200, {}, origin);
104
109
  case "PROPFIND":
105
110
  const list = await s3.list({ prefix: `${user.sub}/` });
106
111
  const items = Array.isArray(list) ? list : (list?.contents || []);
107
- return json(items.map((i) => ({ name: i.key?.split("/").pop() || i.name || i.Key, lastModified: i.lastModified || i.LastModified, size: i.size || i.Size || 0 })));
112
+ return json(items.map((i) => ({ name: i.key?.split("/").pop() || i.name || i.Key, lastModified: i.lastModified || i.LastModified, size: i.size || i.Size || 0 })), 200, {}, origin);
108
113
  case "PATCH":
109
114
  try {
110
- if (!(await s3.exists(`${user.sub}/${key}`))) return json({ error: "File not found" }, 404);
115
+ if (!(await s3.exists(`${user.sub}/${key}`))) {
116
+ const headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
117
+ return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { ...headers, "Content-Type": "application/json" } });
118
+ }
111
119
  const f = await s3.file(`${user.sub}/${key}`);
112
- return f ? new Response(f.stream(), { headers: f.headers }) : json({ error: "File not found" }, 404);
113
- } catch (e) { return json({ error: "File not found", details: e.message }, 404); }
114
- default: return json({ error: "Method not allowed" }, 405);
120
+ if (f) {
121
+ const headers = { ...f.headers, "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
122
+ return new Response(f.stream(), { headers });
123
+ }
124
+ const headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
125
+ return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { ...headers, "Content-Type": "application/json" } });
126
+ } catch (e) { return json({ error: "File not found", details: e.message }, 404, {}, origin); }
127
+ default: return json({ error: "Method not allowed" }, 405, {}, origin);
115
128
  }
116
129
  },
117
130
  port: 3000,