enigmatic 0.34.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 +62 -96
- package/bin/enigmatic.js +3 -0
- package/client/public/AGENTS.md +314 -0
- package/client/public/api.html +202 -0
- package/client/public/client.js +106 -0
- package/client/public/index.html +414 -0
- package/clientserver.png +0 -0
- package/package.json +7 -9
- package/server/bun-server.js +132 -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/client.js +0 -118
- package/public/index.html +0 -48
- package/public/index2.html +0 -10
- /package/{public → client/public}/custom.js +0 -0
|
@@ -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><!-- HTML: data="message" binds to state.message -->
|
|
162
|
+
<hello-world data="message"></hello-world>
|
|
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>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const ren = async (el, v) => {
|
|
2
|
+
const f = window.custom?.[el.tagName.toLowerCase()];
|
|
3
|
+
if (f) {
|
|
4
|
+
const dataAttr = el.getAttribute('data');
|
|
5
|
+
const val = v !== undefined ? v : (dataAttr ? window.state[dataAttr] : undefined);
|
|
6
|
+
try {
|
|
7
|
+
if (f.render) {
|
|
8
|
+
el.innerHTML = await f.render.call(f, val);
|
|
9
|
+
} else if (typeof f === 'function') {
|
|
10
|
+
el.innerHTML = await f(val);
|
|
11
|
+
}
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error(e);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
window.custom = {};
|
|
19
|
+
|
|
20
|
+
// 2. State proxy
|
|
21
|
+
const sProx = new Proxy({}, {
|
|
22
|
+
set(o, p, v) {
|
|
23
|
+
o[p] = v;
|
|
24
|
+
window.$$(`[data="${p}"]`).forEach(el => ren(el, v));
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
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
|
+
});
|
|
36
|
+
|
|
37
|
+
const toJson = (r) => {
|
|
38
|
+
const ct = (r.headers.get('content-type') || '').toLowerCase();
|
|
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
|
+
}
|
|
42
|
+
return r.json();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
Object.assign(window, {
|
|
46
|
+
$: (s) => document.querySelector(s),
|
|
47
|
+
$$: (s) => document.querySelectorAll(s),
|
|
48
|
+
$c: (s) => $0.closest(s),
|
|
49
|
+
state: sProx,
|
|
50
|
+
get: (k) => req('GET', k).then(toJson),
|
|
51
|
+
set: (k, v) => req('POST', k, v).then(toJson),
|
|
52
|
+
put: (k, v) => req('PUT', k, v).then(toJson),
|
|
53
|
+
delete: (k) => req('DELETE', k).then(toJson),
|
|
54
|
+
purge: (k) => req('PURGE', k).then(toJson),
|
|
55
|
+
list: () => req('PROPFIND').then(toJson),
|
|
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`,
|
|
59
|
+
download: async (k) => {
|
|
60
|
+
const r = await req('PATCH', k);
|
|
61
|
+
if (!r.ok) throw new Error('Download failed');
|
|
62
|
+
const a = document.createElement('a');
|
|
63
|
+
a.href = URL.createObjectURL(await r.blob());
|
|
64
|
+
a.download = k;
|
|
65
|
+
a.click();
|
|
66
|
+
URL.revokeObjectURL(a.href);
|
|
67
|
+
},
|
|
68
|
+
initCustomElements: () => {
|
|
69
|
+
if (!document.body) return;
|
|
70
|
+
Object.keys(window.custom || {}).forEach((t) => {
|
|
71
|
+
const elements = window.$$(t);
|
|
72
|
+
if (elements.length > 0) {
|
|
73
|
+
elements.forEach(el => ren(el));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 5. Boot
|
|
80
|
+
const boot = () => {
|
|
81
|
+
if (window.initCustomElements) {
|
|
82
|
+
window.initCustomElements();
|
|
83
|
+
setTimeout(() => window.initCustomElements(), 10);
|
|
84
|
+
}
|
|
85
|
+
if (document.body) {
|
|
86
|
+
new MutationObserver((mutations) => {
|
|
87
|
+
mutations.forEach((m) => {
|
|
88
|
+
m.addedNodes.forEach((node) => {
|
|
89
|
+
if (node.nodeType === 1) {
|
|
90
|
+
const tag = node.tagName?.toLowerCase();
|
|
91
|
+
if (tag && window.custom?.[tag]) ren(node);
|
|
92
|
+
node.querySelectorAll && Array.from(node.querySelectorAll('*')).forEach((child) => {
|
|
93
|
+
const childTag = child.tagName?.toLowerCase();
|
|
94
|
+
if (childTag && window.custom?.[childTag]) ren(child);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
if (document.readyState === 'loading') {
|
|
103
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
104
|
+
} else {
|
|
105
|
+
setTimeout(boot, 0);
|
|
106
|
+
}
|