@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.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/explorer.js +3 -0
- package/dist/packages/email-template/src/email-template.d.ts +18 -0
- package/dist/packages/email-template/src/email-template.js +74 -0
- package/dist/packages/email-template/src/index.d.ts +1 -0
- package/dist/packages/email-template/src/index.js +1 -0
- package/dist/packages/surface-renderer/src/form-renderer.d.ts +117 -0
- package/dist/packages/surface-renderer/src/form-renderer.js +719 -0
- package/dist/packages/surface-renderer/src/index.d.ts +4 -0
- package/dist/packages/surface-renderer/src/index.js +2 -0
- package/dist/packages/surface-renderer/src/portal-renderer.d.ts +177 -0
- package/dist/packages/surface-renderer/src/portal-renderer.js +1089 -0
- package/dist/packages/surface-renderer/src/workspace-home.d.ts +46 -0
- package/dist/packages/surface-renderer/src/workspace-home.js +345 -0
- package/dist/runtime/agent-types.d.ts +48 -0
- package/dist/runtime/agent-types.js +3 -0
- package/dist/runtime/ai-router.d.ts +32 -0
- package/dist/runtime/ai-router.js +112 -0
- package/dist/runtime/app.d.ts +6 -0
- package/dist/runtime/app.js +399 -0
- package/dist/runtime/chunker.d.ts +6 -0
- package/dist/runtime/chunker.js +30 -0
- package/dist/runtime/context.d.ts +115 -0
- package/dist/runtime/context.js +440 -0
- package/dist/runtime/do/workspace-database.d.ts +10 -0
- package/dist/runtime/do/workspace-database.js +199 -0
- package/dist/runtime/form-types.d.ts +143 -0
- package/dist/runtime/form-types.js +1 -0
- package/dist/runtime/runtime.d.ts +9 -0
- package/dist/runtime/runtime.js +7 -0
- package/dist/runtime/source-types.d.ts +15 -0
- package/dist/runtime/source-types.js +1 -0
- package/dist/runtime/source.d.ts +70 -0
- package/dist/runtime/source.js +21 -0
- package/dist/runtime/sync-runtime.d.ts +10 -0
- package/dist/runtime/sync-runtime.js +185 -0
- package/dist/runtime/types.d.ts +21 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/workflow-entrypoint.d.ts +31 -0
- package/dist/runtime/workflow-entrypoint.js +1297 -0
- package/dist/runtime/workflow.d.ts +285 -0
- package/dist/runtime/workflow.js +1008 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +44116 -0
- package/dist/src/commands/ai-gateway-route.d.ts +24 -0
- package/dist/src/commands/ai-gateway-route.js +192 -0
- package/dist/src/commands/auth.d.ts +1 -0
- package/dist/src/commands/auth.js +42 -0
- package/dist/src/commands/billing.d.ts +6 -0
- package/dist/src/commands/billing.js +76 -0
- package/dist/src/commands/brain.d.ts +1 -0
- package/dist/src/commands/brain.js +194 -0
- package/dist/src/commands/demo.d.ts +12 -0
- package/dist/src/commands/demo.js +147 -0
- package/dist/src/commands/deploy.d.ts +1 -0
- package/dist/src/commands/deploy.js +1052 -0
- package/dist/src/commands/dev.d.ts +14 -0
- package/dist/src/commands/dev.js +2818 -0
- package/dist/src/commands/form.d.ts +8 -0
- package/dist/src/commands/form.js +396 -0
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +139 -0
- package/dist/src/commands/issue.d.ts +7 -0
- package/dist/src/commands/issue.js +191 -0
- package/dist/src/commands/login.d.ts +9 -0
- package/dist/src/commands/login.js +163 -0
- package/dist/src/commands/logs.d.ts +8 -0
- package/dist/src/commands/logs.js +113 -0
- package/dist/src/commands/portal.d.ts +2 -0
- package/dist/src/commands/portal.js +111 -0
- package/dist/src/commands/pull.d.ts +3 -0
- package/dist/src/commands/pull.js +184 -0
- package/dist/src/commands/push.d.ts +4 -0
- package/dist/src/commands/push.js +183 -0
- package/dist/src/commands/run.d.ts +6 -0
- package/dist/src/commands/run.js +91 -0
- package/dist/src/commands/secret.d.ts +7 -0
- package/dist/src/commands/secret.js +105 -0
- package/dist/src/commands/shutdown.d.ts +1 -0
- package/dist/src/commands/shutdown.js +46 -0
- package/dist/src/commands/sql.d.ts +8 -0
- package/dist/src/commands/sql.js +142 -0
- package/dist/src/commands/status.d.ts +5 -0
- package/dist/src/commands/status.js +39 -0
- package/dist/src/commands/sync.d.ts +7 -0
- package/dist/src/commands/sync.js +991 -0
- package/dist/src/commands/usage.d.ts +6 -0
- package/dist/src/commands/usage.js +78 -0
- package/dist/src/commands/webhooks.d.ts +1 -0
- package/dist/src/commands/webhooks.js +102 -0
- package/dist/src/commands/workspace.d.ts +23 -0
- package/dist/src/commands/workspace.js +590 -0
- package/dist/src/connector-migration.d.ts +20 -0
- package/dist/src/connector-migration.js +43 -0
- package/dist/src/connector-parser.d.ts +14 -0
- package/dist/src/connector-parser.js +94 -0
- package/dist/src/connector-service/discover.d.ts +37 -0
- package/dist/src/connector-service/discover.js +79 -0
- package/dist/src/connector-service/gather.d.ts +22 -0
- package/dist/src/connector-service/gather.js +89 -0
- package/dist/src/connector-service/init.d.ts +14 -0
- package/dist/src/connector-service/init.js +109 -0
- package/dist/src/connector-service/scaffold.d.ts +17 -0
- package/dist/src/connector-service/scaffold.js +194 -0
- package/dist/src/connector-service/spec-storage.d.ts +8 -0
- package/dist/src/connector-service/spec-storage.js +48 -0
- package/dist/src/connector-service/types.d.ts +57 -0
- package/dist/src/connector-service/types.js +2 -0
- package/dist/src/connector-service/verify.d.ts +24 -0
- package/dist/src/connector-service/verify.js +575 -0
- package/dist/src/email-template.d.ts +2 -0
- package/dist/src/email-template.js +1 -0
- package/dist/src/manifest.d.ts +31 -0
- package/dist/src/manifest.js +25 -0
- package/dist/src/mug-icon.d.ts +1 -0
- package/dist/src/mug-icon.js +12 -0
- package/dist/src/slack-manifest.d.ts +119 -0
- package/dist/src/slack-manifest.js +163 -0
- package/dist/src/source-migration.d.ts +20 -0
- package/dist/src/source-migration.js +43 -0
- package/dist/src/surface-renderer.d.ts +5 -0
- package/dist/src/surface-renderer.js +3 -0
- package/dist/src/templates.d.ts +3 -0
- package/dist/src/templates.js +48 -0
- package/dist/src/version-check.d.ts +1 -0
- package/dist/src/version-check.js +28 -0
- package/dist/src/workflow-parser.d.ts +95 -0
- package/dist/src/workflow-parser.js +526 -0
- package/dist/worker/src/agent-types.d.ts +27 -0
- package/dist/worker/src/agent-types.js +3 -0
- package/dist/worker/src/source-types.d.ts +14 -0
- package/dist/worker/src/source-types.js +1 -0
- package/package.json +90 -0
- 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">×</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">← ${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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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, "'")}'` : "";
|
|
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
|
+
`;
|