@mugwork/mug 0.1.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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/explorer.js +3 -0
  4. package/dist/packages/email-template/src/email-template.d.ts +18 -0
  5. package/dist/packages/email-template/src/email-template.js +74 -0
  6. package/dist/packages/email-template/src/index.d.ts +1 -0
  7. package/dist/packages/email-template/src/index.js +1 -0
  8. package/dist/packages/surface-renderer/src/form-renderer.d.ts +117 -0
  9. package/dist/packages/surface-renderer/src/form-renderer.js +719 -0
  10. package/dist/packages/surface-renderer/src/index.d.ts +4 -0
  11. package/dist/packages/surface-renderer/src/index.js +2 -0
  12. package/dist/packages/surface-renderer/src/portal-renderer.d.ts +177 -0
  13. package/dist/packages/surface-renderer/src/portal-renderer.js +1089 -0
  14. package/dist/packages/surface-renderer/src/workspace-home.d.ts +46 -0
  15. package/dist/packages/surface-renderer/src/workspace-home.js +345 -0
  16. package/dist/runtime/agent-types.d.ts +48 -0
  17. package/dist/runtime/agent-types.js +3 -0
  18. package/dist/runtime/ai-router.d.ts +32 -0
  19. package/dist/runtime/ai-router.js +112 -0
  20. package/dist/runtime/app.d.ts +6 -0
  21. package/dist/runtime/app.js +399 -0
  22. package/dist/runtime/chunker.d.ts +6 -0
  23. package/dist/runtime/chunker.js +30 -0
  24. package/dist/runtime/context.d.ts +115 -0
  25. package/dist/runtime/context.js +440 -0
  26. package/dist/runtime/do/workspace-database.d.ts +10 -0
  27. package/dist/runtime/do/workspace-database.js +199 -0
  28. package/dist/runtime/form-types.d.ts +143 -0
  29. package/dist/runtime/form-types.js +1 -0
  30. package/dist/runtime/runtime.d.ts +9 -0
  31. package/dist/runtime/runtime.js +7 -0
  32. package/dist/runtime/source-types.d.ts +15 -0
  33. package/dist/runtime/source-types.js +1 -0
  34. package/dist/runtime/source.d.ts +70 -0
  35. package/dist/runtime/source.js +21 -0
  36. package/dist/runtime/sync-runtime.d.ts +10 -0
  37. package/dist/runtime/sync-runtime.js +185 -0
  38. package/dist/runtime/types.d.ts +21 -0
  39. package/dist/runtime/types.js +1 -0
  40. package/dist/runtime/workflow-entrypoint.d.ts +31 -0
  41. package/dist/runtime/workflow-entrypoint.js +1297 -0
  42. package/dist/runtime/workflow.d.ts +285 -0
  43. package/dist/runtime/workflow.js +1008 -0
  44. package/dist/src/cli.d.ts +2 -0
  45. package/dist/src/cli.js +44116 -0
  46. package/dist/src/commands/ai-gateway-route.d.ts +24 -0
  47. package/dist/src/commands/ai-gateway-route.js +192 -0
  48. package/dist/src/commands/auth.d.ts +1 -0
  49. package/dist/src/commands/auth.js +42 -0
  50. package/dist/src/commands/billing.d.ts +6 -0
  51. package/dist/src/commands/billing.js +76 -0
  52. package/dist/src/commands/brain.d.ts +1 -0
  53. package/dist/src/commands/brain.js +194 -0
  54. package/dist/src/commands/demo.d.ts +12 -0
  55. package/dist/src/commands/demo.js +147 -0
  56. package/dist/src/commands/deploy.d.ts +1 -0
  57. package/dist/src/commands/deploy.js +1052 -0
  58. package/dist/src/commands/dev.d.ts +14 -0
  59. package/dist/src/commands/dev.js +2818 -0
  60. package/dist/src/commands/form.d.ts +8 -0
  61. package/dist/src/commands/form.js +396 -0
  62. package/dist/src/commands/init.d.ts +1 -0
  63. package/dist/src/commands/init.js +139 -0
  64. package/dist/src/commands/issue.d.ts +7 -0
  65. package/dist/src/commands/issue.js +191 -0
  66. package/dist/src/commands/login.d.ts +9 -0
  67. package/dist/src/commands/login.js +163 -0
  68. package/dist/src/commands/logs.d.ts +8 -0
  69. package/dist/src/commands/logs.js +113 -0
  70. package/dist/src/commands/portal.d.ts +2 -0
  71. package/dist/src/commands/portal.js +111 -0
  72. package/dist/src/commands/pull.d.ts +3 -0
  73. package/dist/src/commands/pull.js +184 -0
  74. package/dist/src/commands/push.d.ts +4 -0
  75. package/dist/src/commands/push.js +183 -0
  76. package/dist/src/commands/run.d.ts +6 -0
  77. package/dist/src/commands/run.js +91 -0
  78. package/dist/src/commands/secret.d.ts +7 -0
  79. package/dist/src/commands/secret.js +105 -0
  80. package/dist/src/commands/shutdown.d.ts +1 -0
  81. package/dist/src/commands/shutdown.js +46 -0
  82. package/dist/src/commands/sql.d.ts +8 -0
  83. package/dist/src/commands/sql.js +142 -0
  84. package/dist/src/commands/status.d.ts +5 -0
  85. package/dist/src/commands/status.js +39 -0
  86. package/dist/src/commands/sync.d.ts +7 -0
  87. package/dist/src/commands/sync.js +991 -0
  88. package/dist/src/commands/usage.d.ts +6 -0
  89. package/dist/src/commands/usage.js +78 -0
  90. package/dist/src/commands/webhooks.d.ts +1 -0
  91. package/dist/src/commands/webhooks.js +102 -0
  92. package/dist/src/commands/workspace.d.ts +23 -0
  93. package/dist/src/commands/workspace.js +590 -0
  94. package/dist/src/connector-migration.d.ts +20 -0
  95. package/dist/src/connector-migration.js +43 -0
  96. package/dist/src/connector-parser.d.ts +14 -0
  97. package/dist/src/connector-parser.js +94 -0
  98. package/dist/src/connector-service/discover.d.ts +37 -0
  99. package/dist/src/connector-service/discover.js +79 -0
  100. package/dist/src/connector-service/gather.d.ts +22 -0
  101. package/dist/src/connector-service/gather.js +89 -0
  102. package/dist/src/connector-service/init.d.ts +14 -0
  103. package/dist/src/connector-service/init.js +109 -0
  104. package/dist/src/connector-service/scaffold.d.ts +17 -0
  105. package/dist/src/connector-service/scaffold.js +194 -0
  106. package/dist/src/connector-service/spec-storage.d.ts +8 -0
  107. package/dist/src/connector-service/spec-storage.js +48 -0
  108. package/dist/src/connector-service/types.d.ts +57 -0
  109. package/dist/src/connector-service/types.js +2 -0
  110. package/dist/src/connector-service/verify.d.ts +24 -0
  111. package/dist/src/connector-service/verify.js +575 -0
  112. package/dist/src/email-template.d.ts +2 -0
  113. package/dist/src/email-template.js +1 -0
  114. package/dist/src/manifest.d.ts +31 -0
  115. package/dist/src/manifest.js +25 -0
  116. package/dist/src/mug-icon.d.ts +1 -0
  117. package/dist/src/mug-icon.js +12 -0
  118. package/dist/src/slack-manifest.d.ts +119 -0
  119. package/dist/src/slack-manifest.js +163 -0
  120. package/dist/src/source-migration.d.ts +20 -0
  121. package/dist/src/source-migration.js +43 -0
  122. package/dist/src/surface-renderer.d.ts +5 -0
  123. package/dist/src/surface-renderer.js +3 -0
  124. package/dist/src/templates.d.ts +3 -0
  125. package/dist/src/templates.js +48 -0
  126. package/dist/src/version-check.d.ts +1 -0
  127. package/dist/src/version-check.js +28 -0
  128. package/dist/src/workflow-parser.d.ts +95 -0
  129. package/dist/src/workflow-parser.js +526 -0
  130. package/dist/worker/src/agent-types.d.ts +27 -0
  131. package/dist/worker/src/agent-types.js +3 -0
  132. package/dist/worker/src/source-types.d.ts +14 -0
  133. package/dist/worker/src/source-types.js +1 -0
  134. package/package.json +90 -0
  135. package/src/data/model-capabilities.json +171 -0
@@ -0,0 +1,719 @@
1
+ export function renderPwaTags(opts) {
2
+ const color = opts.accentColor ?? "#71B7FB";
3
+ return `<link rel="manifest" href="/manifest.json?surface=${esc(opts.surfaceId)}">
4
+ <meta name="theme-color" content="${esc(color)}">
5
+ <meta name="apple-mobile-web-app-capable" content="yes">
6
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
7
+ <link rel="apple-touch-icon" href="/icon-192.png">`;
8
+ }
9
+ export function renderInstallBanner(opts) {
10
+ const color = opts.accentColor ?? "#71B7FB";
11
+ const id = esc(opts.surfaceId);
12
+ return `<div id="mug-pwa-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;z-index:9999;padding:12px 16px;background:${esc(color)};color:#fff;font-family:system-ui,sans-serif;font-size:14px;align-items:center;gap:12px;box-shadow:0 -2px 8px rgba(0,0,0,.15)">
13
+ <img src="/icon-192.png" width="40" height="40" style="border-radius:8px">
14
+ <span style="flex:1">Add "<strong>${esc(opts.title)}</strong>" to your home screen</span>
15
+ <button id="mug-pwa-dismiss" style="background:none;border:none;color:#fff;font-size:22px;cursor:pointer;padding:0 4px" aria-label="Dismiss">&times;</button>
16
+ </div>
17
+ <style>@media(min-width:769px){#mug-pwa-banner{display:none!important}}</style>
18
+ <script>
19
+ (function(){
20
+ if(navigator.serviceWorker)navigator.serviceWorker.register('/sw.js');
21
+ var s=window.matchMedia('(display-mode:standalone)').matches||navigator.standalone;
22
+ if(s)return;
23
+ var k='mug-pwa-dismissed-${id}';
24
+ if(localStorage.getItem(k))return;
25
+ var b=document.getElementById('mug-pwa-banner');
26
+ if(b)b.style.display='flex';
27
+ var d=document.getElementById('mug-pwa-dismiss');
28
+ if(d)d.onclick=function(){localStorage.setItem(k,'1');if(b)b.style.display='none';};
29
+ })();
30
+ </script>`;
31
+ }
32
+ export function renderMetaTags(opts) {
33
+ const desc = opts.description ?? "Powered by Mug";
34
+ let tags = `<meta property="og:title" content="${esc(opts.title)}">
35
+ <meta property="og:description" content="${esc(desc)}">
36
+ <meta property="og:image" content="${esc(opts.ogImageUrl)}">
37
+ <meta property="og:type" content="website">
38
+ <meta name="twitter:card" content="summary_large_image">
39
+ <meta name="twitter:title" content="${esc(opts.title)}">
40
+ <meta name="twitter:description" content="${esc(desc)}">
41
+ <meta name="twitter:image" content="${esc(opts.ogImageUrl)}">
42
+ <meta name="description" content="${esc(desc)}">`;
43
+ if (opts.canonicalUrl)
44
+ tags += `\n<meta property="og:url" content="${esc(opts.canonicalUrl)}">`;
45
+ if (opts.siteName)
46
+ tags += `\n<meta property="og:site_name" content="${esc(opts.siteName)}">`;
47
+ return tags;
48
+ }
49
+ export function renderForm(config, session, editRecord, basePath, prefillValues, breadcrumb, embed) {
50
+ const { title, description, submitText, pages, access, branding } = config;
51
+ const needsAuth = access.mode !== "public" && !session;
52
+ const pageCount = pages.length;
53
+ const base = basePath ?? `/surface/${config.workspace}/${config.surfaceId}`;
54
+ const logoSrc = branding?.logo ?? branding?.logoSquare;
55
+ const accentVar = branding?.accentColor ? `<style>:root { --accent: ${esc(branding.accentColor)}; }</style>` : "";
56
+ const ogImageUrl = branding?.ogImage ?? "/_og-image.png";
57
+ const metaTags = renderMetaTags({ title, description, ogImageUrl });
58
+ return `<!DOCTYPE html>
59
+ <html lang="en">
60
+ <head>
61
+ <meta charset="utf-8">
62
+ <meta name="viewport" content="width=device-width, initial-scale=1">
63
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
64
+ <title>${esc(title)}</title>
65
+ ${metaTags}
66
+ <style>${CSS}</style>
67
+ ${accentVar}
68
+ </head>
69
+ <body>
70
+ <div class="container">
71
+ ${breadcrumb ? `<a href="${esc(breadcrumb.href)}" class="back-link">&larr; ${esc(breadcrumb.label)}</a>` : ""}
72
+ <header>
73
+ ${logoSrc ? (embed ? `<img src="${esc(logoSrc)}" alt="" class="brand-logo">` : `<a href="/"><img src="${esc(logoSrc)}" alt="" class="brand-logo"></a>`) : ""}
74
+ <h1>${esc(title)}</h1>
75
+ ${description ? `<p class="desc">${esc(description)}</p>` : ""}
76
+ ${session ? `<div class="header-meta">
77
+ <div class="session">${esc(session.email ?? session.phone ?? "")}</div>
78
+ ${!session.isDemo ? `<form method="POST" action="/logout"><button type="submit" class="logout-btn"><i data-lucide="log-out"></i> Log out</button></form>` : ""}
79
+ </div>` : ""}
80
+ </header>
81
+
82
+ ${needsAuth ? renderAuthGate(config) : renderFormBody(config, pageCount, prefillValues, session?.authRow)}
83
+ </div>
84
+ ${editRecord ? `<script>var __editRecord = ${JSON.stringify(editRecord)};</script>` : ""}
85
+ ${session?.authRow ? `<script>var __authRow = ${JSON.stringify(session.authRow)};</script>` : ""}
86
+ ${prefillValues && Object.keys(prefillValues).length > 0 ? `<script>var __prefill = ${JSON.stringify(prefillValues)};</script>` : ""}
87
+ <script>${needsAuth ? AUTH_JS(config, base) : FORM_JS(config, pageCount, base)}</script>
88
+ <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
89
+ <script>lucide.createIcons();</script>
90
+ </body>
91
+ </html>`;
92
+ }
93
+ export function esc(s) {
94
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
95
+ }
96
+ function renderAuthGate(config) {
97
+ const method = config.access.method ?? "email";
98
+ const label = method === "email" ? "Enter your email" : "Enter your phone number";
99
+ const inputType = method === "email" ? "email" : "tel";
100
+ const placeholder = method === "email" ? "you@example.com" : "+1 555 123 4567";
101
+ return `
102
+ <div id="auth-gate">
103
+ <div class="auth-box">
104
+ <label for="auth-input">${label}</label>
105
+ <input type="${inputType}" id="auth-input" placeholder="${placeholder}" required autocomplete="${method}">
106
+ <button type="button" id="auth-submit" class="btn btn-primary">Send verification</button>
107
+ <div id="auth-msg" class="msg"></div>
108
+ </div>
109
+ <div id="code-box" class="auth-box" style="display:none">
110
+ <label for="code-input">Enter verification code</label>
111
+ <input type="text" id="code-input" placeholder="123456" maxlength="6" inputmode="numeric" pattern="[0-9]*">
112
+ <button type="button" id="code-submit" class="btn btn-primary">Verify</button>
113
+ <div id="code-msg" class="msg"></div>
114
+ </div>
115
+ </div>`;
116
+ }
117
+ function renderFormBody(config, pageCount, prefillValues, authRow) {
118
+ const { pages, submitText } = config;
119
+ let html = `<form id="mug-form" novalidate>`;
120
+ for (let i = 0; i < pages.length; i++) {
121
+ const page = pages[i];
122
+ const condAttr = page.showWhen ? ` data-page-show='${JSON.stringify(page.showWhen)}'` : "";
123
+ html += `<div class="page" data-page-id="${esc(page.id)}" data-page-idx="${i}"${condAttr} ${i > 0 ? 'style="display:none"' : ""}>`;
124
+ if (page.title)
125
+ html += `<h2>${esc(page.title)}</h2>`;
126
+ if (page.description)
127
+ html += `<p class="page-desc">${esc(page.description)}</p>`;
128
+ for (const field of page.fields) {
129
+ html += renderField(field, prefillValues, authRow);
130
+ }
131
+ html += `<div class="nav">`;
132
+ if (i > 0)
133
+ html += `<button type="button" class="btn btn-back" data-nav="back">Back</button>`;
134
+ if (i < pageCount - 1) {
135
+ html += `<button type="button" class="btn btn-primary" data-nav="next">Next</button>`;
136
+ }
137
+ else {
138
+ html += `<button type="submit" class="btn btn-primary">${esc(submitText ?? "Submit")}</button>`;
139
+ }
140
+ html += `</div></div>`;
141
+ }
142
+ html += `</form><div id="form-msg" class="msg"></div>`;
143
+ if (pageCount > 1) {
144
+ html += `<div class="progress"><div class="progress-bar" id="progress-bar"></div></div>`;
145
+ }
146
+ return html;
147
+ }
148
+ function resolveTemplates(text, authRow) {
149
+ if (!authRow)
150
+ return text;
151
+ return text.replace(/\{\{(\w+)\}\}/g, (_, key) => {
152
+ const val = authRow[key];
153
+ return val != null ? String(val) : `{{${key}}}`;
154
+ });
155
+ }
156
+ function resolveValidationRules(rules, authRow) {
157
+ if (!rules || !authRow)
158
+ return rules;
159
+ return rules.map(r => ({
160
+ ...r,
161
+ value: typeof r.value === "string" && r.value.includes("{{")
162
+ ? (() => { const resolved = resolveTemplates(String(r.value), authRow); const n = Number(resolved); return isNaN(n) ? resolved : n; })()
163
+ : r.value,
164
+ message: resolveTemplates(r.message, authRow),
165
+ }));
166
+ }
167
+ function renderField(field, prefillValues, authRow) {
168
+ if (field.type === "hidden") {
169
+ const val = prefillValues?.[field.name] ?? field.default ?? "";
170
+ return `<input type="hidden" name="${esc(field.name)}" value="${esc(String(val))}">`;
171
+ }
172
+ if (field.type === "calculated") {
173
+ return renderCalculated(field);
174
+ }
175
+ const condAttr = field.showWhen ? ` data-show='${JSON.stringify(field.showWhen)}'` : "";
176
+ const req = field.required ? " required" : "";
177
+ const ph = field.placeholder ? ` placeholder="${esc(field.placeholder)}"` : "";
178
+ const pf = prefillValues?.[field.name];
179
+ const effectiveVal = pf != null ? String(pf) : (field.default != null ? String(field.default) : "");
180
+ const def = field.default != null ? String(field.default) : "";
181
+ const defAttr = def ? ` data-default="${esc(def)}"` : "";
182
+ const valAttr = effectiveVal ? ` value="${esc(effectiveVal)}"` : "";
183
+ const isLocked = field.locked === true;
184
+ const lockAttr = isLocked ? " readonly" : "";
185
+ const lockClass = isLocked ? " locked" : "";
186
+ const resolvedRules = resolveValidationRules(field.validate, authRow);
187
+ const validateAttr = resolvedRules?.length ? ` data-validate='${JSON.stringify(resolvedRules).replace(/'/g, "&#39;")}'` : "";
188
+ const resolvedHelp = field.helpText ? resolveTemplates(field.helpText, authRow) : undefined;
189
+ let html = `<div class="field${lockClass}"${condAttr}${validateAttr}>`;
190
+ html += `<label for="f-${esc(field.name)}">${esc(field.label)}${field.required ? ' <span class="req">*</span>' : ""}</label>`;
191
+ if (resolvedHelp)
192
+ html += `<div class="help-text">${esc(resolvedHelp)}</div>`;
193
+ switch (field.type) {
194
+ case "text":
195
+ case "email":
196
+ case "phone": {
197
+ const t = field.type === "phone" ? "tel" : field.type;
198
+ const pat = field.pattern ? ` pattern="${esc(field.pattern)}"` : "";
199
+ html += `<input type="${t}" id="f-${esc(field.name)}" name="${esc(field.name)}"${ph}${req}${pat}${valAttr}${defAttr}${lockAttr}>`;
200
+ break;
201
+ }
202
+ case "number": {
203
+ const min = field.min != null ? ` min="${field.min}"` : "";
204
+ const max = field.max != null ? ` max="${field.max}"` : "";
205
+ const step = field.step != null ? ` step="${field.step}"` : "";
206
+ html += `<input type="number" id="f-${esc(field.name)}" name="${esc(field.name)}"${ph}${req}${min}${max}${step}${valAttr}${defAttr}${lockAttr} inputmode="decimal">`;
207
+ break;
208
+ }
209
+ case "date": {
210
+ const min = field.min ? ` min="${esc(String(field.min))}"` : "";
211
+ const max = field.max ? ` max="${esc(String(field.max))}"` : "";
212
+ html += `<input type="date" id="f-${esc(field.name)}" name="${esc(field.name)}"${req}${min}${max}${valAttr}${defAttr}${lockAttr}>`;
213
+ break;
214
+ }
215
+ case "select": {
216
+ const disabledAttr = isLocked ? " disabled" : "";
217
+ html += `<select id="f-${esc(field.name)}" name="${esc(field.name)}"${req}${defAttr}${disabledAttr}>`;
218
+ html += `<option value="">Select...</option>`;
219
+ for (const opt of field.options ?? []) {
220
+ const sel = effectiveVal && opt.value === effectiveVal ? " selected" : "";
221
+ html += `<option value="${esc(opt.value)}"${sel}>${esc(opt.label)}</option>`;
222
+ }
223
+ html += `</select>`;
224
+ if (isLocked && effectiveVal) {
225
+ html += `<input type="hidden" name="${esc(field.name)}" value="${esc(effectiveVal)}">`;
226
+ }
227
+ break;
228
+ }
229
+ case "multiselect": {
230
+ const selVals = effectiveVal ? effectiveVal.split(",").map(s => s.trim()) : [];
231
+ html += `<div class="multi" id="f-${esc(field.name)}"${defAttr}>`;
232
+ for (const opt of field.options ?? []) {
233
+ const chk = selVals.includes(opt.value) ? " checked" : "";
234
+ const dis = isLocked ? " disabled" : "";
235
+ html += `<label class="check-label"><input type="checkbox" name="${esc(field.name)}" value="${esc(opt.value)}"${chk}${dis}> ${esc(opt.label)}</label>`;
236
+ }
237
+ if (isLocked) {
238
+ for (const v of selVals) {
239
+ html += `<input type="hidden" name="${esc(field.name)}" value="${esc(v)}">`;
240
+ }
241
+ }
242
+ html += `</div>`;
243
+ break;
244
+ }
245
+ case "textarea": {
246
+ const rows = field.rows ?? 3;
247
+ const ml = field.maxLength ? ` maxlength="${field.maxLength}"` : "";
248
+ html += `<textarea id="f-${esc(field.name)}" name="${esc(field.name)}" rows="${rows}"${ph}${req}${ml}${defAttr}${lockAttr}>${effectiveVal ? esc(effectiveVal) : ""}</textarea>`;
249
+ break;
250
+ }
251
+ case "file": {
252
+ const accept = field.accept ? ` accept="${esc(field.accept)}"` : "";
253
+ html += `<input type="file" id="f-${esc(field.name)}" name="${esc(field.name)}"${req}${accept} data-max-mb="${field.maxSizeMb ?? 10}">`;
254
+ html += `<div class="file-info" id="fi-${esc(field.name)}"></div>`;
255
+ break;
256
+ }
257
+ }
258
+ html += `<div class="err" id="e-${esc(field.name)}"></div></div>`;
259
+ return html;
260
+ }
261
+ function renderCalculated(field) {
262
+ const condAttr = field.showWhen ? ` data-show='${JSON.stringify(field.showWhen)}'` : "";
263
+ return `<div class="field calc"${condAttr}>
264
+ <label>${esc(field.label)}</label>
265
+ <div class="calc-value" id="f-${esc(field.name)}" data-expr="${esc(field.expression ?? "")}" data-fmt="${field.format ?? "number"}">—</div>
266
+ </div>`;
267
+ }
268
+ function AUTH_JS(config, base) {
269
+ const method = config.access.method ?? "email";
270
+ return `
271
+ (function() {
272
+ var input = document.getElementById('auth-input');
273
+ var btn = document.getElementById('auth-submit');
274
+ var msg = document.getElementById('auth-msg');
275
+ var codeBox = document.getElementById('code-box');
276
+ var codeInput = document.getElementById('code-input');
277
+ var codeBtn = document.getElementById('code-submit');
278
+ var codeMsg = document.getElementById('code-msg');
279
+ var identifier = '';
280
+
281
+ btn.addEventListener('click', function() {
282
+ identifier = input.value.trim();
283
+ if (!identifier) return;
284
+ btn.disabled = true;
285
+ msg.textContent = 'Sending...';
286
+ fetch('${base}/auth', {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({ identifier: identifier }),
290
+ credentials: 'include'
291
+ }).then(function(r) { return r.json(); }).then(function(d) {
292
+ btn.disabled = false;
293
+ if (d.status === 'sent') {
294
+ ${method === "email"
295
+ ? "msg.textContent = 'Check your email for a verification link.';"
296
+ : "msg.textContent = 'Code sent.'; codeBox.style.display = ''; codeInput.focus();"}
297
+ }
298
+ }).catch(function() { btn.disabled = false; msg.textContent = 'Error. Try again.'; });
299
+ });
300
+
301
+ ${method === "phone" ? `
302
+ codeBtn.addEventListener('click', function() {
303
+ var code = codeInput.value.trim();
304
+ if (!code) return;
305
+ codeBtn.disabled = true;
306
+ codeMsg.textContent = 'Verifying...';
307
+ fetch('${base}/verify-code', {
308
+ method: 'POST',
309
+ headers: { 'Content-Type': 'application/json' },
310
+ body: JSON.stringify({ identifier: identifier, code: code }),
311
+ credentials: 'include'
312
+ }).then(function(r) {
313
+ if (r.ok) { location.reload(); }
314
+ else { return r.json().then(function(d) { codeBtn.disabled = false; codeMsg.textContent = d.error || 'Invalid code.'; }); }
315
+ }).catch(function() { codeBtn.disabled = false; codeMsg.textContent = 'Error. Try again.'; });
316
+ });` : ""}
317
+ })();`;
318
+ }
319
+ function FORM_JS(config, pageCount, base) {
320
+ const pagesJson = JSON.stringify(config.pages.map(p => ({
321
+ id: p.id,
322
+ nextPage: p.nextPage,
323
+ showWhen: p.showWhen,
324
+ })));
325
+ return `
326
+ (function() {
327
+ var pages = ${pagesJson};
328
+ var currentPage = 0;
329
+ var form = document.getElementById('mug-form');
330
+ var formMsg = document.getElementById('form-msg');
331
+ var allPages = document.querySelectorAll('.page');
332
+ var progressBar = document.getElementById('progress-bar');
333
+
334
+ function getValues() {
335
+ var vals = {};
336
+ var inputs = form.querySelectorAll('input, select, textarea');
337
+ for (var i = 0; i < inputs.length; i++) {
338
+ var el = inputs[i];
339
+ if (el.type === 'checkbox') {
340
+ if (!vals[el.name]) vals[el.name] = [];
341
+ if (el.checked) vals[el.name].push(el.value);
342
+ } else if (el.type === 'file') {
343
+ continue;
344
+ } else {
345
+ vals[el.name] = el.value;
346
+ }
347
+ }
348
+ return vals;
349
+ }
350
+
351
+ function evalCondition(c, vals) {
352
+ var v = vals[c.field];
353
+ switch (c.op) {
354
+ case 'eq': return v == c.value;
355
+ case 'neq': return v != c.value;
356
+ case 'in': return Array.isArray(c.value) && c.value.indexOf(v) >= 0;
357
+ case 'gt': return Number(v) > Number(c.value);
358
+ case 'lt': return Number(v) < Number(c.value);
359
+ case 'filled': return v !== '' && v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0);
360
+ case 'empty': return v === '' || v === undefined || v === null || (Array.isArray(v) && v.length === 0);
361
+ }
362
+ return true;
363
+ }
364
+
365
+ function evalConditions(conds, vals) {
366
+ if (!conds || conds.length === 0) return true;
367
+ for (var i = 0; i < conds.length; i++) {
368
+ if (!evalCondition(conds[i], vals)) return false;
369
+ }
370
+ return true;
371
+ }
372
+
373
+ function updateVisibility() {
374
+ var vals = getValues();
375
+ var fields = form.querySelectorAll('[data-show]');
376
+ for (var i = 0; i < fields.length; i++) {
377
+ var conds = JSON.parse(fields[i].getAttribute('data-show'));
378
+ var show = evalConditions(conds, vals);
379
+ fields[i].style.display = show ? '' : 'none';
380
+ var inputs = fields[i].querySelectorAll('input, select, textarea');
381
+ for (var j = 0; j < inputs.length; j++) {
382
+ if (!show) inputs[j].removeAttribute('required');
383
+ }
384
+ }
385
+ }
386
+
387
+ function updateCalculated() {
388
+ var vals = getValues();
389
+ var calcs = form.querySelectorAll('.calc-value');
390
+ for (var i = 0; i < calcs.length; i++) {
391
+ var expr = calcs[i].getAttribute('data-expr');
392
+ var fmt = calcs[i].getAttribute('data-fmt');
393
+ try {
394
+ var result = evalExpr(expr, vals);
395
+ if (isNaN(result) || result === null) { calcs[i].textContent = '\\u2014'; continue; }
396
+ if (fmt === 'currency') calcs[i].textContent = '$' + result.toFixed(2);
397
+ else if (fmt === 'percent') calcs[i].textContent = result.toFixed(1) + '%';
398
+ else calcs[i].textContent = String(result);
399
+ } catch(e) { calcs[i].textContent = '\\u2014'; }
400
+ }
401
+ }
402
+
403
+ function evalExpr(expr, vals) {
404
+ var tokens = expr.replace(/([+\\-*\\/()%])/g, ' $1 ').split(/\\s+/).filter(Boolean);
405
+ var out = [];
406
+ for (var i = 0; i < tokens.length; i++) {
407
+ var t = tokens[i];
408
+ if (/^[+\\-*\\/()%]$/.test(t)) { out.push(t); }
409
+ else if (/^\\d+(\\.\\d+)?$/.test(t)) { out.push(t); }
410
+ else { var v = parseFloat(vals[t]); if (isNaN(v)) return NaN; out.push(v); }
411
+ }
412
+ return Function('"use strict"; return (' + out.join(' ') + ')')();
413
+ }
414
+
415
+ function validateField(fieldEl, requireRequired) {
416
+ var input = fieldEl.querySelector('input, select, textarea');
417
+ if (!input) return true;
418
+ var errEl = fieldEl.querySelector('.err');
419
+ if (errEl) errEl.textContent = '';
420
+ if (requireRequired && input.hasAttribute('required') && !input.value.trim()) {
421
+ if (errEl) errEl.textContent = 'Required';
422
+ return false;
423
+ }
424
+ if (input.value.trim() && !input.checkValidity()) {
425
+ if (errEl) errEl.textContent = input.validationMessage;
426
+ return false;
427
+ }
428
+ var rulesJson = fieldEl.getAttribute('data-validate');
429
+ if (rulesJson && input.value.trim()) {
430
+ var rules = JSON.parse(rulesJson);
431
+ for (var r = 0; r < rules.length; r++) {
432
+ var rule = rules[r];
433
+ var val = input.value;
434
+ var fail = false;
435
+ if (rule.rule === 'min' && Number(val) < Number(rule.value)) fail = true;
436
+ if (rule.rule === 'max' && Number(val) > Number(rule.value)) fail = true;
437
+ if (rule.rule === 'minLength' && val.length < Number(rule.value)) fail = true;
438
+ if (rule.rule === 'maxLength' && val.length > Number(rule.value)) fail = true;
439
+ if (rule.rule === 'pattern' && !new RegExp(rule.value).test(val)) fail = true;
440
+ if (fail) {
441
+ if (errEl) errEl.textContent = rule.message;
442
+ return false;
443
+ }
444
+ }
445
+ }
446
+ return true;
447
+ }
448
+
449
+ function validatePage(pageIdx) {
450
+ var page = allPages[pageIdx];
451
+ var valid = true;
452
+ var fields = page.querySelectorAll('.field:not([style*="display: none"]):not([style*="display:none"])');
453
+ for (var i = 0; i < fields.length; i++) {
454
+ if (!validateField(fields[i], true)) valid = false;
455
+ }
456
+ return valid;
457
+ }
458
+
459
+ function resolveNextPage(pageIdx) {
460
+ var p = pages[pageIdx];
461
+ if (!p.nextPage) return pageIdx + 1;
462
+ if (typeof p.nextPage === 'string') {
463
+ for (var i = 0; i < pages.length; i++) { if (pages[i].id === p.nextPage) return i; }
464
+ return pageIdx + 1;
465
+ }
466
+ var vals = getValues();
467
+ var conds = p.nextPage.conditions || [];
468
+ for (var i = 0; i < conds.length; i++) {
469
+ if (evalConditions(conds[i].when, vals)) {
470
+ for (var j = 0; j < pages.length; j++) { if (pages[j].id === conds[i].goto) return j; }
471
+ }
472
+ }
473
+ var def = p.nextPage['default'];
474
+ if (def) { for (var j = 0; j < pages.length; j++) { if (pages[j].id === def) return j; } }
475
+ return pageIdx + 1;
476
+ }
477
+
478
+ function showPage(idx) {
479
+ for (var i = 0; i < allPages.length; i++) allPages[i].style.display = 'none';
480
+ allPages[idx].style.display = '';
481
+ currentPage = idx;
482
+ if (progressBar) progressBar.style.width = ((idx + 1) / ${pageCount} * 100) + '%';
483
+ window.scrollTo(0, 0);
484
+ }
485
+
486
+ form.addEventListener('keydown', function(e) {
487
+ if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
488
+ var lastPage = ${pageCount} - 1;
489
+ if (currentPage < lastPage) {
490
+ e.preventDefault();
491
+ var nextBtn = allPages[currentPage].querySelector('[data-nav="next"]');
492
+ if (nextBtn) nextBtn.click();
493
+ }
494
+ }
495
+ });
496
+
497
+ form.addEventListener('input', function(e) {
498
+ updateVisibility(); updateCalculated();
499
+ var fieldEl = e.target.closest('.field');
500
+ if (fieldEl) validateField(fieldEl, false);
501
+ });
502
+ form.addEventListener('change', function(e) {
503
+ updateVisibility(); updateCalculated();
504
+ var fieldEl = e.target.closest('.field');
505
+ if (fieldEl) validateField(fieldEl, false);
506
+ });
507
+ form.addEventListener('blur', function(e) {
508
+ var fieldEl = e.target.closest('.field');
509
+ if (fieldEl) validateField(fieldEl, false);
510
+ }, true);
511
+
512
+ form.addEventListener('click', function(e) {
513
+ var btn = e.target.closest('[data-nav]');
514
+ if (!btn) return;
515
+ if (btn.getAttribute('data-nav') === 'next') {
516
+ if (!validatePage(currentPage)) return;
517
+ var next = resolveNextPage(currentPage);
518
+ if (next < ${pageCount}) showPage(next);
519
+ } else if (btn.getAttribute('data-nav') === 'back') {
520
+ if (currentPage > 0) showPage(currentPage - 1);
521
+ }
522
+ });
523
+
524
+ form.addEventListener('submit', function(e) {
525
+ e.preventDefault();
526
+ if (!validatePage(currentPage)) return;
527
+ var vals = getValues();
528
+
529
+ var calcs = form.querySelectorAll('.calc-value');
530
+ for (var i = 0; i < calcs.length; i++) {
531
+ var name = calcs[i].id.replace('f-', '');
532
+ var text = calcs[i].textContent;
533
+ if (text !== '\\u2014') vals[name] = text;
534
+ }
535
+
536
+ for (var fk in fileUrls) { vals[fk] = fileUrls[fk]; }
537
+ if (window.__editRecord) { vals._edit = true; vals._editRecord = window.__editRecord; }
538
+
539
+ var submitBtn = form.querySelector('[type="submit"]');
540
+ submitBtn.disabled = true;
541
+ form.style.display = 'none';
542
+ if (progressBar) progressBar.parentElement.style.display = 'none';
543
+ formMsg.className = 'msg success';
544
+ formMsg.textContent = 'Submitted successfully!';
545
+ confetti();
546
+
547
+ fetch('${base}/submit', {
548
+ method: 'POST',
549
+ headers: { 'Content-Type': 'application/json' },
550
+ body: JSON.stringify(vals),
551
+ credentials: 'include'
552
+ }).then(function(r) { return r.json(); }).then(function(d) {
553
+ if (d.status !== 'ok' && d.status !== 'created') {
554
+ form.style.display = '';
555
+ if (progressBar) progressBar.parentElement.style.display = '';
556
+ submitBtn.disabled = false;
557
+ formMsg.className = 'msg';
558
+ formMsg.textContent = d.error || 'Submission failed.';
559
+ }
560
+ }).catch(function() {
561
+ form.style.display = '';
562
+ if (progressBar) progressBar.parentElement.style.display = '';
563
+ submitBtn.disabled = false;
564
+ formMsg.className = 'msg';
565
+ formMsg.textContent = 'Network error. Try again.';
566
+ });
567
+ });
568
+
569
+ function confetti() {
570
+ var colors = ['#4a9','#e44','#fa0','#48f','#f4a','#8e4'];
571
+ var container = document.querySelector('.container');
572
+ for (var i = 0; i < 80; i++) {
573
+ var el = document.createElement('div');
574
+ el.className = 'confetti-piece';
575
+ el.style.left = Math.random() * 100 + '%';
576
+ el.style.background = colors[Math.floor(Math.random() * colors.length)];
577
+ el.style.animationDelay = Math.random() * 0.6 + 's';
578
+ el.style.animationDuration = (1.2 + Math.random() * 1.2) + 's';
579
+ container.appendChild(el);
580
+ }
581
+ setTimeout(function() {
582
+ var pieces = document.querySelectorAll('.confetti-piece');
583
+ for (var i = 0; i < pieces.length; i++) pieces[i].remove();
584
+ }, 3000);
585
+ }
586
+
587
+ function setFieldValue(el, val) {
588
+ if (!el) return;
589
+ if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
590
+ el.value = String(val ?? '');
591
+ }
592
+ }
593
+
594
+ function prefill() {
595
+ var record = window.__editRecord || {};
596
+ for (var key in record) { setFieldValue(document.getElementById('f-' + key), record[key]); }
597
+
598
+ var params = new URLSearchParams(window.location.search);
599
+ params.forEach(function(val, key) { setFieldValue(document.getElementById('f-' + key), val); });
600
+
601
+ var pf = window.__prefill || {};
602
+ for (var key in pf) { setFieldValue(document.getElementById('f-' + key), pf[key]); }
603
+ }
604
+
605
+ var fileUrls = {};
606
+ form.addEventListener('change', function(e) {
607
+ var input = e.target;
608
+ if (input.type !== 'file' || !input.files || !input.files[0]) return;
609
+ var file = input.files[0];
610
+ var maxMb = parseInt(input.getAttribute('data-max-mb') || '10');
611
+ var info = document.getElementById('fi-' + input.name);
612
+ if (file.size > maxMb * 1024 * 1024) {
613
+ if (info) info.textContent = 'File too large (max ' + maxMb + 'MB)';
614
+ input.value = '';
615
+ return;
616
+ }
617
+ if (info) info.textContent = 'Uploading...';
618
+ var fd = new FormData();
619
+ fd.append('file', file);
620
+ fd.append('field', input.name);
621
+ fetch('${base}/upload', { method: 'POST', body: fd, credentials: 'include' })
622
+ .then(function(r) { return r.json(); })
623
+ .then(function(d) {
624
+ if (d.url) {
625
+ fileUrls[input.name] = d.url;
626
+ if (info) info.textContent = file.name + ' uploaded';
627
+ } else {
628
+ if (info) info.textContent = 'Upload failed';
629
+ }
630
+ }).catch(function() { if (info) info.textContent = 'Upload failed'; });
631
+ });
632
+
633
+ prefill();
634
+ updateVisibility();
635
+ updateCalculated();
636
+ if (progressBar) progressBar.style.width = (1 / ${pageCount} * 100) + '%';
637
+ })();`;
638
+ }
639
+ const CSS = `
640
+ :root { --accent: #1a1a1a; }
641
+ * { box-sizing: border-box; margin: 0; padding: 0; }
642
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #1a1a1a; line-height: 1.5; }
643
+ .container { max-width: 560px; margin: 0 auto; padding: 24px 16px; }
644
+ .back-link { display: inline-block; margin-bottom: 16px; color: #666; text-decoration: none; font-size: 14px; transition: color 0.15s; }
645
+ .back-link:hover { color: var(--accent); }
646
+ header { margin-bottom: 24px; position: relative; }
647
+ .brand-logo { display: block; max-height: 72px; max-width: 240px; margin-bottom: 12px; }
648
+ h1 { font-size: 24px; font-weight: 600; margin-bottom: 4px; }
649
+ h2 { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
650
+ .desc, .page-desc { color: #666; font-size: 14px; margin-bottom: 12px; }
651
+ .header-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
652
+ .session { font-size: 13px; color: #666; background: #f0f0f0; padding: 6px 12px; border-radius: 6px; display: inline-block; }
653
+ .logout-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: none; background: #f0f0f0; border-radius: 6px; cursor: pointer; color: #666; font-size: 13px; font-family: inherit; transition: background 0.15s, color 0.15s; }
654
+ .logout-btn:hover { background: #e0e0e0; color: #333; }
655
+ .logout-btn svg { width: 16px; height: 16px; }
656
+
657
+ .field { margin-bottom: 16px; }
658
+ label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; }
659
+ .req { color: #e44; }
660
+ input[type="text"], input[type="email"], input[type="tel"], input[type="number"], input[type="date"],
661
+ select, textarea {
662
+ width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px;
663
+ font-size: 16px; font-family: inherit; background: #fff; transition: border-color 0.15s;
664
+ }
665
+ input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
666
+ textarea { resize: vertical; }
667
+ .multi { display: flex; flex-direction: column; gap: 6px; }
668
+ .check-label { font-size: 14px; display: flex; align-items: center; gap: 8px; cursor: pointer; }
669
+ .check-label input { width: 18px; height: 18px; }
670
+ input[type="file"] { padding: 8px; }
671
+ .file-info { font-size: 12px; color: #888; margin-top: 4px; }
672
+
673
+ .field.locked input, .field.locked select, .field.locked textarea { background: #f5f5f5; color: #666; cursor: not-allowed; border-color: #e0e0e0; }
674
+ .calc { background: #f9f9f9; padding: 12px; border-radius: 8px; }
675
+ .calc-value { font-size: 20px; font-weight: 600; color: #1a1a1a; }
676
+
677
+ .help-text { color: #888; font-size: 13px; margin-bottom: 6px; }
678
+ .err { color: #e44; font-size: 13px; margin-top: 4px; min-height: 0; }
679
+ .msg { font-size: 14px; color: #666; margin-top: 12px; }
680
+ .msg.success { color: var(--accent); font-size: 18px; text-align: center; padding: 32px 0; }
681
+
682
+ .nav { display: flex; gap: 12px; margin-top: 20px; }
683
+ .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 16px; font-weight: 500; cursor: pointer; transition: background 0.15s; }
684
+ .btn-primary { background: var(--accent); color: #fff; flex: 1; }
685
+ .btn-primary:hover { background: color-mix(in srgb, var(--accent) 80%, #000); }
686
+ .btn-primary:disabled { background: #999; cursor: not-allowed; }
687
+ .btn-back { background: #e8e8e8; color: #333; }
688
+ .btn-back:hover { background: #ddd; }
689
+
690
+ .progress { height: 3px; background: #e8e8e8; border-radius: 2px; margin-top: 24px; }
691
+ .progress-bar { height: 100%; background: var(--accent); border-radius: 2px; transition: width 0.3s; }
692
+
693
+ .auth-box { background: #fff; padding: 24px; border-radius: 12px; margin-bottom: 16px; }
694
+ .auth-box label { margin-bottom: 8px; }
695
+ .auth-box input { margin-bottom: 12px; }
696
+ .auth-box .btn { width: 100%; }
697
+
698
+ @media (max-width: 480px) {
699
+ .container { padding: 16px 12px; }
700
+ h1 { font-size: 20px; }
701
+ .btn { padding: 14px 20px; }
702
+ }
703
+
704
+ .confetti-piece {
705
+ position: fixed;
706
+ top: -10px;
707
+ width: 8px;
708
+ height: 12px;
709
+ border-radius: 2px;
710
+ opacity: 0.9;
711
+ animation: confetti-fall linear forwards;
712
+ pointer-events: none;
713
+ z-index: 1000;
714
+ }
715
+ @keyframes confetti-fall {
716
+ 0% { transform: translateY(0) rotate(0deg); opacity: 1; }
717
+ 100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
718
+ }
719
+ `;