@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,1089 @@
|
|
|
1
|
+
import { esc, renderForm, renderMetaTags } from "./form-renderer.js";
|
|
2
|
+
export function bindUserParam(sql, session) {
|
|
3
|
+
const user = session?.email ?? session?.phone ?? "";
|
|
4
|
+
const params = [];
|
|
5
|
+
const bound = sql.replace(/:auth\.(\w+)|:user/g, (match, col) => {
|
|
6
|
+
if (col) {
|
|
7
|
+
params.push(String(session?.authRow?.[col] ?? ""));
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
params.push(user);
|
|
11
|
+
}
|
|
12
|
+
return "?";
|
|
13
|
+
});
|
|
14
|
+
return { sql: bound, userParams: params };
|
|
15
|
+
}
|
|
16
|
+
export function paginateQuery(sql, userParams, page, pageSize) {
|
|
17
|
+
const offset = (page - 1) * pageSize;
|
|
18
|
+
return {
|
|
19
|
+
countSql: `SELECT COUNT(*) as total FROM (${sql})`,
|
|
20
|
+
dataSql: `SELECT * FROM (${sql}) LIMIT ? OFFSET ?`,
|
|
21
|
+
countParams: [...userParams],
|
|
22
|
+
dataParams: [...userParams, pageSize, offset],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function detailQuery(sql, userParams, primaryKey, rowId) {
|
|
26
|
+
return {
|
|
27
|
+
sql: `SELECT * FROM (${sql}) WHERE ${primaryKey} = ?`,
|
|
28
|
+
params: [...userParams, rowId],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
let _tz;
|
|
32
|
+
let _eq = ""; // embed query suffix: "&embed=true" or ""
|
|
33
|
+
export function formatDateTime(value, style = "long", tz) {
|
|
34
|
+
if (value == null || value === "")
|
|
35
|
+
return "—";
|
|
36
|
+
const d = new Date(String(value));
|
|
37
|
+
if (isNaN(d.getTime()))
|
|
38
|
+
return String(value);
|
|
39
|
+
const t = tz ?? _tz;
|
|
40
|
+
if (style === "short") {
|
|
41
|
+
return d.toLocaleString("en-US", { month: "numeric", day: "numeric", year: "2-digit", hour: "numeric", minute: "2-digit", timeZone: t });
|
|
42
|
+
}
|
|
43
|
+
return d.toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit", timeZone: t });
|
|
44
|
+
}
|
|
45
|
+
function formatCurrency(n, style) {
|
|
46
|
+
if (style === "short") {
|
|
47
|
+
const abs = Math.abs(n);
|
|
48
|
+
if (abs >= 1_000_000)
|
|
49
|
+
return (n < 0 ? "-" : "") + "$" + (abs / 1_000_000).toLocaleString("en-US", { maximumFractionDigits: 1 }) + "M";
|
|
50
|
+
if (abs >= 1_000)
|
|
51
|
+
return (n < 0 ? "-" : "") + "$" + (abs / 1_000).toLocaleString("en-US", { maximumFractionDigits: 1 }) + "K";
|
|
52
|
+
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
53
|
+
}
|
|
54
|
+
if (style === "whole")
|
|
55
|
+
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
56
|
+
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
57
|
+
}
|
|
58
|
+
function formatValue(value, format, dateFormat) {
|
|
59
|
+
if (value == null || value === "")
|
|
60
|
+
return "—";
|
|
61
|
+
const s = String(value);
|
|
62
|
+
switch (format) {
|
|
63
|
+
case "date": {
|
|
64
|
+
const d = new Date(s);
|
|
65
|
+
return isNaN(d.getTime()) ? s : d.toLocaleDateString("en-US", { month: dateFormat === "short" ? "numeric" : "short", day: "numeric", year: dateFormat === "short" ? "2-digit" : "numeric", timeZone: _tz });
|
|
66
|
+
}
|
|
67
|
+
case "datetime":
|
|
68
|
+
return formatDateTime(value, dateFormat ?? "long");
|
|
69
|
+
case "currency":
|
|
70
|
+
return formatCurrency(Number(s), "full");
|
|
71
|
+
case "currency-whole":
|
|
72
|
+
return formatCurrency(Number(s), "whole");
|
|
73
|
+
case "currency-short":
|
|
74
|
+
return formatCurrency(Number(s), "short");
|
|
75
|
+
case "number":
|
|
76
|
+
return Number(s).toLocaleString("en-US");
|
|
77
|
+
case "percent":
|
|
78
|
+
return Number(s).toLocaleString("en-US", { maximumFractionDigits: 1 }) + "%";
|
|
79
|
+
default:
|
|
80
|
+
return s;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const BADGE_COLORS = {
|
|
84
|
+
pending: { bg: "#fef3c7", fg: "#92400e" },
|
|
85
|
+
waiting: { bg: "#fef3c7", fg: "#92400e" },
|
|
86
|
+
draft: { bg: "#fef3c7", fg: "#92400e" },
|
|
87
|
+
review: { bg: "#fef3c7", fg: "#92400e" },
|
|
88
|
+
approved: { bg: "#d1fae5", fg: "#065f46" },
|
|
89
|
+
active: { bg: "#d1fae5", fg: "#065f46" },
|
|
90
|
+
done: { bg: "#d1fae5", fg: "#065f46" },
|
|
91
|
+
success: { bg: "#d1fae5", fg: "#065f46" },
|
|
92
|
+
completed: { bg: "#d1fae5", fg: "#065f46" },
|
|
93
|
+
denied: { bg: "#fee2e2", fg: "#991b1b" },
|
|
94
|
+
rejected: { bg: "#fee2e2", fg: "#991b1b" },
|
|
95
|
+
failed: { bg: "#fee2e2", fg: "#991b1b" },
|
|
96
|
+
error: { bg: "#fee2e2", fg: "#991b1b" },
|
|
97
|
+
cancelled: { bg: "#f3f4f6", fg: "#4b5563" },
|
|
98
|
+
expired: { bg: "#f3f4f6", fg: "#4b5563" },
|
|
99
|
+
archived: { bg: "#f3f4f6", fg: "#4b5563" },
|
|
100
|
+
};
|
|
101
|
+
function badgeColorsFor(value, custom) {
|
|
102
|
+
if (custom) {
|
|
103
|
+
const hex = custom[value] ?? custom[value.toLowerCase()];
|
|
104
|
+
if (hex)
|
|
105
|
+
return { bg: hex + "22", fg: hex };
|
|
106
|
+
}
|
|
107
|
+
return BADGE_COLORS[value.toLowerCase()] ?? { bg: "#f3f4f6", fg: "#4b5563" };
|
|
108
|
+
}
|
|
109
|
+
function renderBadge(value, custom) {
|
|
110
|
+
const colors = badgeColorsFor(value, custom);
|
|
111
|
+
return `<span class="badge" style="background:${colors.bg};color:${colors.fg}">${esc(value)}</span>`;
|
|
112
|
+
}
|
|
113
|
+
function renderCell(value, col) {
|
|
114
|
+
if (col.badge && value != null && value !== "")
|
|
115
|
+
return renderBadge(String(value), col.badgeColors);
|
|
116
|
+
return esc(formatValue(value, col.format, col.dateFormat));
|
|
117
|
+
}
|
|
118
|
+
function renderDetailValue(value, field) {
|
|
119
|
+
if (field.badge && value != null && value !== "")
|
|
120
|
+
return renderBadge(String(value), field.badgeColors);
|
|
121
|
+
if (field.format === "multiline" && value != null) {
|
|
122
|
+
return `<div class="multiline">${esc(String(value)).replace(/\n/g, "<br>")}</div>`;
|
|
123
|
+
}
|
|
124
|
+
return esc(formatValue(value, field.format));
|
|
125
|
+
}
|
|
126
|
+
function interpolateTitle(template, row) {
|
|
127
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
128
|
+
const val = row[key];
|
|
129
|
+
return val != null ? String(val) : "";
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function evalShowWhen(condition, row) {
|
|
133
|
+
const v = row[condition.field];
|
|
134
|
+
switch (condition.op) {
|
|
135
|
+
case "eq": return v == condition.value;
|
|
136
|
+
case "neq": return v != condition.value;
|
|
137
|
+
case "in": return Array.isArray(condition.value) && condition.value.includes(String(v));
|
|
138
|
+
case "gt": return Number(v) > Number(condition.value);
|
|
139
|
+
case "lt": return Number(v) < Number(condition.value);
|
|
140
|
+
case "filled": return v !== "" && v !== undefined && v !== null;
|
|
141
|
+
case "empty": return v === "" || v === undefined || v === null;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
// --- Section renderers (return HTML fragments, not full pages) ---
|
|
146
|
+
function abbreviateNumber(n) {
|
|
147
|
+
const abs = Math.abs(n);
|
|
148
|
+
if (abs >= 1_000_000)
|
|
149
|
+
return (n < 0 ? "-" : "") + (abs / 1_000_000).toLocaleString("en-US", { maximumFractionDigits: 1 }) + "M";
|
|
150
|
+
if (abs >= 10_000)
|
|
151
|
+
return (n < 0 ? "-" : "") + (abs / 1_000).toLocaleString("en-US", { maximumFractionDigits: 1 }) + "K";
|
|
152
|
+
return n.toLocaleString("en-US");
|
|
153
|
+
}
|
|
154
|
+
function formatStatValue(value, format) {
|
|
155
|
+
if (value == null || value === "")
|
|
156
|
+
return "—";
|
|
157
|
+
const n = Number(value);
|
|
158
|
+
if (isNaN(n))
|
|
159
|
+
return esc(String(value));
|
|
160
|
+
switch (format) {
|
|
161
|
+
case "currency": return formatCurrency(n, "whole");
|
|
162
|
+
case "currency-whole": return formatCurrency(n, "whole");
|
|
163
|
+
case "currency-short": return formatCurrency(n, "short");
|
|
164
|
+
case "percent": return n.toLocaleString("en-US", { maximumFractionDigits: 1 }) + "%";
|
|
165
|
+
case "number":
|
|
166
|
+
default: return n.toLocaleString("en-US");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export function renderStatsSection(section, rows) {
|
|
170
|
+
const row = rows[0] ?? {};
|
|
171
|
+
let html = `<div class="section-stats">`;
|
|
172
|
+
for (const item of section.items) {
|
|
173
|
+
const borderStyle = item.color ? `border-top: 3px solid ${esc(item.color)}` : "";
|
|
174
|
+
const vc = item.valueColor;
|
|
175
|
+
const resolvedValueColor = vc === "neutral" ? "#1a1a1a" : vc === "match" ? item.color : vc;
|
|
176
|
+
const valStyle = (resolvedValueColor ?? (item.color ? item.color : "")) ? `color: ${esc(resolvedValueColor ?? item.color)}` : "";
|
|
177
|
+
const cardStyle = borderStyle;
|
|
178
|
+
const tag = item.href ? "a" : "div";
|
|
179
|
+
const hrefAttr = item.href ? ` href="${esc(item.href)}"` : "";
|
|
180
|
+
const rawNum = Number(row[item.column]);
|
|
181
|
+
const fmt = item.format;
|
|
182
|
+
const isCurrency = fmt === "currency" || fmt === "currency-whole" || fmt === "currency-short";
|
|
183
|
+
const shortVal = !isNaN(rawNum) ? (isCurrency ? formatCurrency(rawNum, "short") : abbreviateNumber(rawNum)) : "";
|
|
184
|
+
const dataShort = shortVal ? ` data-short="${esc(shortVal)}"` : "";
|
|
185
|
+
html += `<${tag} class="stat-card"${hrefAttr}${cardStyle ? ` style="${cardStyle}"` : ""}>
|
|
186
|
+
<div class="stat-label">${esc(item.label)}</div>
|
|
187
|
+
<div class="stat-value"${valStyle ? ` style="${valStyle}"` : ""}${dataShort}>${formatStatValue(row[item.column], item.format)}</div>
|
|
188
|
+
</${tag}>`;
|
|
189
|
+
}
|
|
190
|
+
html += `</div>`;
|
|
191
|
+
return html;
|
|
192
|
+
}
|
|
193
|
+
function resolveProgressColor(section, row, pct) {
|
|
194
|
+
if (section.colorColumn) {
|
|
195
|
+
const v = row[section.colorColumn];
|
|
196
|
+
if (v != null && String(v).trim())
|
|
197
|
+
return esc(String(v));
|
|
198
|
+
}
|
|
199
|
+
if (section.colorThresholds?.length) {
|
|
200
|
+
const sorted = [...section.colorThresholds].sort((a, b) => a.percent - b.percent);
|
|
201
|
+
for (const t of sorted) {
|
|
202
|
+
if (pct <= t.percent)
|
|
203
|
+
return esc(t.color);
|
|
204
|
+
}
|
|
205
|
+
return esc(sorted[sorted.length - 1].color);
|
|
206
|
+
}
|
|
207
|
+
return section.color ? esc(section.color) : "var(--accent)";
|
|
208
|
+
}
|
|
209
|
+
export function renderProgressSection(section, rows) {
|
|
210
|
+
let html = `<div class="section-progress">`;
|
|
211
|
+
for (const row of rows) {
|
|
212
|
+
const label = String(row[section.labelColumn] ?? "");
|
|
213
|
+
const value = Number(row[section.valueColumn] ?? 0);
|
|
214
|
+
const max = Number(row[section.maxColumn] ?? 1);
|
|
215
|
+
const pct = max > 0 ? Math.min(100, Math.round((value / max) * 100)) : 0;
|
|
216
|
+
const barColor = resolveProgressColor(section, row, pct);
|
|
217
|
+
let subtitle = "";
|
|
218
|
+
if (section.subtitleTemplate) {
|
|
219
|
+
subtitle = section.subtitleTemplate.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
220
|
+
const v = row[key];
|
|
221
|
+
return v != null ? String(v) : "";
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
html += `<div class="progress-item">
|
|
225
|
+
<div class="progress-header">
|
|
226
|
+
<span class="progress-label">${esc(label)}</span>
|
|
227
|
+
<span class="progress-pct">${pct}%</span>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="progress-track">
|
|
230
|
+
<div class="progress-fill" style="width:${pct}%;background:${barColor}"></div>
|
|
231
|
+
</div>
|
|
232
|
+
${subtitle ? `<div class="progress-subtitle">${esc(subtitle)}</div>` : ""}
|
|
233
|
+
</div>`;
|
|
234
|
+
}
|
|
235
|
+
html += `</div>`;
|
|
236
|
+
return html;
|
|
237
|
+
}
|
|
238
|
+
function simpleMarkdown(text) {
|
|
239
|
+
return text
|
|
240
|
+
.split("\n\n").map(block => {
|
|
241
|
+
block = block.trim();
|
|
242
|
+
if (!block)
|
|
243
|
+
return "";
|
|
244
|
+
if (block.startsWith("### "))
|
|
245
|
+
return `<h4>${esc(block.slice(4))}</h4>`;
|
|
246
|
+
if (block.startsWith("## "))
|
|
247
|
+
return `<h3>${esc(block.slice(3))}</h3>`;
|
|
248
|
+
if (block.startsWith("# "))
|
|
249
|
+
return `<h2>${esc(block.slice(2))}</h2>`;
|
|
250
|
+
const lines = block.split("\n");
|
|
251
|
+
if (lines.every(l => l.startsWith("- "))) {
|
|
252
|
+
return `<ul>${lines.map(l => `<li>${inlineFormat(l.slice(2))}</li>`).join("")}</ul>`;
|
|
253
|
+
}
|
|
254
|
+
return `<p>${inlineFormat(lines.join("<br>"))}</p>`;
|
|
255
|
+
}).join("\n");
|
|
256
|
+
}
|
|
257
|
+
function inlineFormat(text) {
|
|
258
|
+
return esc(text)
|
|
259
|
+
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
|
260
|
+
.replace(/\*(.+?)\*/g, "<em>$1</em>");
|
|
261
|
+
}
|
|
262
|
+
export function renderTextSection(section) {
|
|
263
|
+
const content = section.content.trim().startsWith("<") ? section.content : simpleMarkdown(section.content);
|
|
264
|
+
return `<div class="section-text">${content}</div>`;
|
|
265
|
+
}
|
|
266
|
+
const CHART_PALETTE = ["#3b82f6", "#22c55e", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899"];
|
|
267
|
+
export function renderChartSection(section, rows) {
|
|
268
|
+
const maxItems = section.chartType === "donut" ? 8 : 20;
|
|
269
|
+
const truncated = rows.length > maxItems;
|
|
270
|
+
const items = rows.slice(0, maxItems);
|
|
271
|
+
if (section.chartType === "donut")
|
|
272
|
+
return renderDonutChart(section, items, truncated ? rows.length - maxItems : 0);
|
|
273
|
+
return renderBarChart(section, items, truncated ? rows.length - maxItems : 0);
|
|
274
|
+
}
|
|
275
|
+
function renderBarChart(section, rows, othersCount) {
|
|
276
|
+
const lc = section.labelColumn ?? "label";
|
|
277
|
+
const vc = section.valueColumn ?? "value";
|
|
278
|
+
const values = rows.map(r => ({ label: String(r[lc] ?? ""), value: Number(r[vc] ?? 0) }));
|
|
279
|
+
values.sort((a, b) => b.value - a.value);
|
|
280
|
+
const maxVal = Math.max(...values.map(v => v.value), 1);
|
|
281
|
+
const barColor = section.color ?? "var(--accent)";
|
|
282
|
+
const barHeight = 28;
|
|
283
|
+
const labelWidth = 120;
|
|
284
|
+
const chartWidth = 600;
|
|
285
|
+
const svgHeight = values.length * (barHeight + 8) + 8;
|
|
286
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${chartWidth} ${svgHeight}" class="chart-bar-svg">`;
|
|
287
|
+
values.forEach((v, i) => {
|
|
288
|
+
const y = i * (barHeight + 8) + 4;
|
|
289
|
+
const barW = maxVal > 0 ? ((v.value / maxVal) * (chartWidth - labelWidth - 60)) : 0;
|
|
290
|
+
const color = section.colors?.[v.label] ?? (section.colorColumn ? CHART_PALETTE[i % CHART_PALETTE.length] : barColor);
|
|
291
|
+
svg += `<text x="${labelWidth - 8}" y="${y + barHeight / 2 + 5}" text-anchor="end" font-size="13" fill="#666">${esc(v.label.length > 14 ? v.label.slice(0, 13) + "…" : v.label)}</text>`;
|
|
292
|
+
svg += `<rect x="${labelWidth}" y="${y}" width="${barW}" height="${barHeight}" rx="4" fill="${esc(color)}"/>`;
|
|
293
|
+
svg += `<text x="${labelWidth + barW + 8}" y="${y + barHeight / 2 + 5}" font-size="13" fill="#666">${v.value.toLocaleString("en-US")}</text>`;
|
|
294
|
+
});
|
|
295
|
+
svg += `</svg>`;
|
|
296
|
+
let html = `<div class="section-chart">`;
|
|
297
|
+
if (section.title)
|
|
298
|
+
html += `<div class="chart-title">${esc(section.title)}</div>`;
|
|
299
|
+
html += svg;
|
|
300
|
+
if (othersCount > 0)
|
|
301
|
+
html += `<div class="chart-others">+${othersCount} others</div>`;
|
|
302
|
+
html += `</div>`;
|
|
303
|
+
return html;
|
|
304
|
+
}
|
|
305
|
+
function renderDonutChart(section, rows, othersCount) {
|
|
306
|
+
const lc = section.labelColumn ?? "label";
|
|
307
|
+
const vc = section.valueColumn ?? "value";
|
|
308
|
+
const items = rows.map((r, i) => {
|
|
309
|
+
const label = String(r[lc] ?? "");
|
|
310
|
+
return {
|
|
311
|
+
label,
|
|
312
|
+
value: Math.max(0, Number(r[vc] ?? 0)),
|
|
313
|
+
color: section.colors?.[label] ?? CHART_PALETTE[i % CHART_PALETTE.length],
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
const total = items.reduce((s, v) => s + v.value, 0) || 1;
|
|
317
|
+
const size = 160;
|
|
318
|
+
const cx = size / 2, cy = size / 2, r = 60;
|
|
319
|
+
const circumference = 2 * Math.PI * r;
|
|
320
|
+
let offset = 0;
|
|
321
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" class="chart-donut-svg">`;
|
|
322
|
+
for (const item of items) {
|
|
323
|
+
const pct = item.value / total;
|
|
324
|
+
const dash = pct * circumference;
|
|
325
|
+
svg += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${esc(item.color)}" stroke-width="24" stroke-dasharray="${dash} ${circumference - dash}" stroke-dashoffset="${-offset}" transform="rotate(-90 ${cx} ${cy})"/>`;
|
|
326
|
+
offset += dash;
|
|
327
|
+
}
|
|
328
|
+
svg += `</svg>`;
|
|
329
|
+
let legend = `<div class="chart-legend">`;
|
|
330
|
+
for (const item of items) {
|
|
331
|
+
const pct = Math.round((item.value / total) * 100);
|
|
332
|
+
legend += `<div class="legend-item"><span class="legend-dot" style="background:${esc(item.color)}"></span>${esc(item.label)} — ${item.value.toLocaleString("en-US")} (${pct}%)</div>`;
|
|
333
|
+
}
|
|
334
|
+
if (othersCount > 0)
|
|
335
|
+
legend += `<div class="legend-item" style="color:#888">+${othersCount} others</div>`;
|
|
336
|
+
legend += `</div>`;
|
|
337
|
+
let html = `<div class="section-chart section-chart-donut">`;
|
|
338
|
+
if (section.title)
|
|
339
|
+
html += `<div class="chart-title">${esc(section.title)}</div>`;
|
|
340
|
+
html += `<div class="chart-donut-layout">${svg}${legend}</div>`;
|
|
341
|
+
html += `</div>`;
|
|
342
|
+
return html;
|
|
343
|
+
}
|
|
344
|
+
export function renderGallerySection(section, rows) {
|
|
345
|
+
const cols = section.columns ?? 3;
|
|
346
|
+
let html = `<div class="section-gallery" style="grid-template-columns: repeat(${cols}, 1fr)">`;
|
|
347
|
+
for (const row of rows) {
|
|
348
|
+
const imgUrl = row[section.imageColumn] ? String(row[section.imageColumn]) : "";
|
|
349
|
+
const title = section.titleColumn ? String(row[section.titleColumn] ?? "") : "";
|
|
350
|
+
html += `<div class="gallery-card">`;
|
|
351
|
+
if (imgUrl) {
|
|
352
|
+
html += `<div class="gallery-img"><img src="${esc(imgUrl)}" alt="${esc(title)}" loading="lazy"></div>`;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
html += `<div class="gallery-img gallery-placeholder"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg></div>`;
|
|
356
|
+
}
|
|
357
|
+
if (title)
|
|
358
|
+
html += `<div class="gallery-title">${esc(title)}</div>`;
|
|
359
|
+
if (section.fields) {
|
|
360
|
+
for (const field of section.fields) {
|
|
361
|
+
html += `<div class="gallery-field"><span class="gallery-field-label">${esc(field.label)}</span> ${renderDetailValue(row[field.key], field)}</div>`;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
html += `</div>`;
|
|
365
|
+
}
|
|
366
|
+
html += `</div>`;
|
|
367
|
+
return html;
|
|
368
|
+
}
|
|
369
|
+
export function renderAccordionSection(section, renderedChildren) {
|
|
370
|
+
return `<details class="section-accordion"${section.open ? " open" : ""}>
|
|
371
|
+
<summary class="accordion-summary">${esc(section.label)}</summary>
|
|
372
|
+
<div class="accordion-body">${renderedChildren}</div>
|
|
373
|
+
</details>`;
|
|
374
|
+
}
|
|
375
|
+
export function renderSection(section, queryResults, tableCounts, pages, sectionKey, sectionIndex, base, activeTabId) {
|
|
376
|
+
switch (section.type) {
|
|
377
|
+
case "stats": return renderStatsSection(section, queryResults.get(sectionKey) ?? []);
|
|
378
|
+
case "progress": return renderProgressSection(section, queryResults.get(sectionKey) ?? []);
|
|
379
|
+
case "text": return renderTextSection(section);
|
|
380
|
+
case "chart": return renderChartSection(section, queryResults.get(sectionKey) ?? []);
|
|
381
|
+
case "gallery": return renderGallerySection(section, queryResults.get(sectionKey) ?? []);
|
|
382
|
+
case "accordion": {
|
|
383
|
+
let childHtml = "";
|
|
384
|
+
section.sections.forEach((child, i) => {
|
|
385
|
+
childHtml += renderSection(child, queryResults, tableCounts, pages, `${sectionKey}:${i}`, sectionIndex, base, activeTabId);
|
|
386
|
+
});
|
|
387
|
+
return renderAccordionSection(section, childHtml);
|
|
388
|
+
}
|
|
389
|
+
case "table": {
|
|
390
|
+
const rows = queryResults.get(sectionKey) ?? [];
|
|
391
|
+
const count = tableCounts.get(sectionKey) ?? rows.length;
|
|
392
|
+
return renderTableSection(section, rows, count, pages, sectionIndex, base, activeTabId);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
export function collectSectionQueries(sections, keyPrefix) {
|
|
397
|
+
const queries = [];
|
|
398
|
+
sections.forEach((section, i) => {
|
|
399
|
+
const key = `${keyPrefix}:${i}`;
|
|
400
|
+
if (section.type === "accordion") {
|
|
401
|
+
queries.push(...collectSectionQueries(section.sections, key));
|
|
402
|
+
}
|
|
403
|
+
else if ("query" in section && section.query) {
|
|
404
|
+
queries.push({ key, query: section.query });
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
return queries;
|
|
408
|
+
}
|
|
409
|
+
function renderTableSection(section, rows, totalCount, pages, sectionIndex, base, activeTabId) {
|
|
410
|
+
const pageSize = section.pageSize ?? 25;
|
|
411
|
+
const pageKey = `page_${sectionIndex}`;
|
|
412
|
+
const page = pages[pageKey] ?? 1;
|
|
413
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
414
|
+
const pk = section.primaryKey ?? "id";
|
|
415
|
+
if (rows.length === 0) {
|
|
416
|
+
return `<div class="empty">${esc(section.emptyMessage ?? "No records found.")}</div>`;
|
|
417
|
+
}
|
|
418
|
+
let html = `<div class="table-wrap"><table>
|
|
419
|
+
<thead><tr>${section.columns.map(c => `<th>${esc(c.label)}</th>`).join("")}</tr></thead>
|
|
420
|
+
<tbody>`;
|
|
421
|
+
const hasDetail = !!section.detail;
|
|
422
|
+
for (const row of rows) {
|
|
423
|
+
if (hasDetail) {
|
|
424
|
+
const rowId = row[pk] != null ? String(row[pk]) : "";
|
|
425
|
+
html += `<tr class="clickable" data-row-id="${esc(rowId)}" onclick="location.href='${base}/row/${esc(rowId)}?tab=${esc(activeTabId)}§ion=${sectionIndex}${_eq}'">`;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
html += `<tr>`;
|
|
429
|
+
}
|
|
430
|
+
for (const col of section.columns) {
|
|
431
|
+
html += `<td>${renderCell(row[col.key], col)}</td>`;
|
|
432
|
+
}
|
|
433
|
+
html += `</tr>`;
|
|
434
|
+
}
|
|
435
|
+
html += `</tbody></table></div>`;
|
|
436
|
+
if (totalPages > 1) {
|
|
437
|
+
const tabParam = `tab=${esc(activeTabId)}`;
|
|
438
|
+
html += `<div class="pagination">`;
|
|
439
|
+
if (page > 1)
|
|
440
|
+
html += `<a href="${base}?${tabParam}&${pageKey}=${page - 1}${_eq}" class="page-btn">← Previous</a>`;
|
|
441
|
+
html += `<span class="page-info">Page ${page} of ${totalPages}</span>`;
|
|
442
|
+
if (page < totalPages)
|
|
443
|
+
html += `<a href="${base}?${tabParam}&${pageKey}=${page + 1}${_eq}" class="page-btn">Next →</a>`;
|
|
444
|
+
html += `</div>`;
|
|
445
|
+
}
|
|
446
|
+
return html;
|
|
447
|
+
}
|
|
448
|
+
export function renderPortal(config, queryResults, tableCounts, pages, activeTabId, session, basePath, breadcrumb, tabCounts, embed) {
|
|
449
|
+
_tz = config.timezone;
|
|
450
|
+
_eq = embed ? "&embed=true" : "";
|
|
451
|
+
const { title, description, access, branding, tabs } = config;
|
|
452
|
+
const needsAuth = access.mode !== "public" && !session;
|
|
453
|
+
const base = basePath ?? `/${config.surfaceId}`;
|
|
454
|
+
const logoSrc = branding?.logo ?? branding?.logoSquare;
|
|
455
|
+
const accentVar = branding?.accentColor ? `<style>:root { --accent: ${esc(branding.accentColor)}; }</style>` : "";
|
|
456
|
+
if (needsAuth) {
|
|
457
|
+
const authConfig = { workspace: config.workspace, surfaceId: config.surfaceId, title, access, pages: [], workflow: "", branding };
|
|
458
|
+
return renderForm(authConfig, null, null, base, undefined, undefined, embed);
|
|
459
|
+
}
|
|
460
|
+
const activeTab = tabs.find(t => t.id === activeTabId) ?? tabs[0];
|
|
461
|
+
if (!activeTab) {
|
|
462
|
+
return `<!DOCTYPE html><html><body><div style="max-width:600px;margin:40px auto;font-family:sans-serif"><h1>No tabs configured</h1></div></body></html>`;
|
|
463
|
+
}
|
|
464
|
+
const links = activeTab.links;
|
|
465
|
+
const ogImageUrl = branding?.ogImage ?? "/_og-image.png";
|
|
466
|
+
const metaTags = renderMetaTags({ title, description, ogImageUrl });
|
|
467
|
+
let html = `<!DOCTYPE html>
|
|
468
|
+
<html lang="en">
|
|
469
|
+
<head>
|
|
470
|
+
<meta charset="utf-8">
|
|
471
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
472
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
473
|
+
<title>${esc(title)}</title>
|
|
474
|
+
${metaTags}
|
|
475
|
+
<style>${PORTAL_CSS}</style>
|
|
476
|
+
${accentVar}
|
|
477
|
+
</head>
|
|
478
|
+
<body>
|
|
479
|
+
<div class="container">
|
|
480
|
+
${breadcrumb ? `<a href="${esc(breadcrumb.href)}" class="back-link">← ${esc(breadcrumb.label)}</a>` : ""}
|
|
481
|
+
<header>
|
|
482
|
+
${logoSrc ? (embed ? `<img src="${esc(logoSrc)}" alt="" class="brand-logo">` : `<a href="/"><img src="${esc(logoSrc)}" alt="" class="brand-logo"></a>`) : ""}
|
|
483
|
+
<h1>${esc(title)}</h1>
|
|
484
|
+
${description ? `<p class="desc">${esc(description)}</p>` : ""}
|
|
485
|
+
${session ? `<div class="header-meta">
|
|
486
|
+
<div class="session">${esc(session.email ?? session.phone ?? "")}</div>
|
|
487
|
+
${!session.isDemo ? `<form method="POST" action="/logout"><button type="submit" class="logout-btn"><i data-lucide="log-out"></i> Log out</button></form>` : ""}
|
|
488
|
+
</div>` : ""}
|
|
489
|
+
</header>`;
|
|
490
|
+
if (config.sections && config.sections.length > 0) {
|
|
491
|
+
html += `<div class="portal-top-sections">`;
|
|
492
|
+
config.sections.forEach((section, i) => {
|
|
493
|
+
const sectionKey = `_top:${i}`;
|
|
494
|
+
html += renderSection(section, queryResults, tableCounts, pages, sectionKey, i, base, activeTab.id);
|
|
495
|
+
});
|
|
496
|
+
html += `</div>`;
|
|
497
|
+
}
|
|
498
|
+
if (tabs.length > 1) {
|
|
499
|
+
html += `<nav class="tab-bar">`;
|
|
500
|
+
for (const tab of tabs) {
|
|
501
|
+
const isActive = tab.id === activeTab.id;
|
|
502
|
+
const href = `${base}?tab=${esc(tab.id)}${_eq}`;
|
|
503
|
+
const tabStyle = tab.color ? ` style="--tab-color: ${esc(tab.color)}"` : "";
|
|
504
|
+
const count = tabCounts?.get(tab.id);
|
|
505
|
+
const countBadge = count != null ? ` <span class="tab-count">${count.toLocaleString("en-US")}</span>` : "";
|
|
506
|
+
html += `<a href="${href}" class="tab-link${isActive ? " tab-active" : ""}${tab.color ? " tab-colored" : ""}"${tabStyle}>${esc(tab.label)}${countBadge}</a>`;
|
|
507
|
+
}
|
|
508
|
+
html += `</nav>`;
|
|
509
|
+
}
|
|
510
|
+
if (links && links.length > 0) {
|
|
511
|
+
html += `<nav class="portal-links">${links.map(l => `<a href="${esc(l.href)}" class="link-btn">${esc(l.label)}</a>`).join("")}</nav>`;
|
|
512
|
+
}
|
|
513
|
+
html += `<div class="tab-content">`;
|
|
514
|
+
activeTab.sections.forEach((section, i) => {
|
|
515
|
+
const sectionKey = `${activeTab.id}:${i}`;
|
|
516
|
+
html += renderSection(section, queryResults, tableCounts, pages, sectionKey, i, base, activeTab.id);
|
|
517
|
+
});
|
|
518
|
+
html += `</div>
|
|
519
|
+
</div>
|
|
520
|
+
<div id="tl-toast-container"></div>
|
|
521
|
+
<div id="tl-preview-container"></div>
|
|
522
|
+
${config.autoplay ? `<script type="application/json" id="tl-autoplay">${JSON.stringify(config.autoplay)}</script>` : ""}
|
|
523
|
+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
524
|
+
<script>lucide.createIcons();</script>
|
|
525
|
+
<script>${STAT_ABBREV_JS}</script>
|
|
526
|
+
<script>${TIMELINE_JS}</script>
|
|
527
|
+
</body>
|
|
528
|
+
</html>`;
|
|
529
|
+
return html;
|
|
530
|
+
}
|
|
531
|
+
export function findTableSection(config, tabId, sectionIndex) {
|
|
532
|
+
const tab = config.tabs.find(t => t.id === tabId);
|
|
533
|
+
if (!tab)
|
|
534
|
+
return null;
|
|
535
|
+
const section = tab.sections[sectionIndex];
|
|
536
|
+
if (!section || section.type !== "table")
|
|
537
|
+
return null;
|
|
538
|
+
return { tab, section };
|
|
539
|
+
}
|
|
540
|
+
export function renderPortalDetail(config, tableSection, row, session, tabId, basePath, embed) {
|
|
541
|
+
_tz = config.timezone;
|
|
542
|
+
_eq = embed ? "&embed=true" : "";
|
|
543
|
+
const { title, access, branding } = config;
|
|
544
|
+
const base = basePath ?? `/${config.surfaceId}`;
|
|
545
|
+
const needsAuth = access.mode !== "public" && !session;
|
|
546
|
+
const logoSrc = branding?.logo ?? branding?.logoSquare;
|
|
547
|
+
const accentVar = branding?.accentColor ? `<style>:root { --accent: ${esc(branding.accentColor)}; }</style>` : "";
|
|
548
|
+
if (needsAuth) {
|
|
549
|
+
const authConfig = { workspace: config.workspace, surfaceId: config.surfaceId, title, access, pages: [], workflow: "", branding };
|
|
550
|
+
return renderForm(authConfig, null, null, base, undefined, undefined, embed);
|
|
551
|
+
}
|
|
552
|
+
const detail = tableSection.detail;
|
|
553
|
+
const detailTitle = detail?.title ? interpolateTitle(detail.title, row) : esc(title);
|
|
554
|
+
const ogImageUrl = branding?.ogImage ?? "/_og-image.png";
|
|
555
|
+
const detailMetaTags = renderMetaTags({ title: `${detailTitle} — ${title}`, ogImageUrl });
|
|
556
|
+
let html = `<!DOCTYPE html>
|
|
557
|
+
<html lang="en">
|
|
558
|
+
<head>
|
|
559
|
+
<meta charset="utf-8">
|
|
560
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
561
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
562
|
+
<title>${esc(detailTitle)} — ${esc(title)}</title>
|
|
563
|
+
${detailMetaTags}
|
|
564
|
+
<style>${PORTAL_CSS}</style>
|
|
565
|
+
${accentVar}
|
|
566
|
+
</head>
|
|
567
|
+
<body>
|
|
568
|
+
<div class="container">
|
|
569
|
+
${`<a href="${esc(base)}?tab=${esc(tabId)}${_eq}" class="back-link">← Back to ${esc(title)}</a>`}
|
|
570
|
+
<header>
|
|
571
|
+
${logoSrc ? (embed ? `<img src="${esc(logoSrc)}" alt="" class="brand-logo">` : `<a href="/"><img src="${esc(logoSrc)}" alt="" class="brand-logo"></a>`) : ""}
|
|
572
|
+
<h1>${esc(detailTitle)}</h1>
|
|
573
|
+
${session ? `<div class="header-meta">
|
|
574
|
+
<div class="session">${esc(session.email ?? session.phone ?? "")}</div>
|
|
575
|
+
${!session.isDemo ? `<form method="POST" action="/logout"><button type="submit" class="logout-btn"><i data-lucide="log-out"></i> Log out</button></form>` : ""}
|
|
576
|
+
</div>` : ""}
|
|
577
|
+
</header>`;
|
|
578
|
+
if (detail?.fields) {
|
|
579
|
+
html += `<div class="detail-fields">`;
|
|
580
|
+
for (const field of detail.fields) {
|
|
581
|
+
html += `<div class="detail-field">
|
|
582
|
+
<div class="detail-label">${esc(field.label)}</div>
|
|
583
|
+
<div class="detail-value">${renderDetailValue(row[field.key], field)}</div>
|
|
584
|
+
</div>`;
|
|
585
|
+
}
|
|
586
|
+
html += `</div>`;
|
|
587
|
+
}
|
|
588
|
+
const actions = tableSection.actions ?? [];
|
|
589
|
+
const visibleActions = actions.filter(a => !a.showWhen || evalShowWhen(a.showWhen, row));
|
|
590
|
+
if (visibleActions.length > 0) {
|
|
591
|
+
const pk = tableSection.primaryKey ?? "id";
|
|
592
|
+
const rowId = row[pk] != null ? String(row[pk]) : "";
|
|
593
|
+
const rowDataJson = JSON.stringify(row);
|
|
594
|
+
html += `<div class="detail-actions" id="action-buttons">`;
|
|
595
|
+
for (const action of visibleActions) {
|
|
596
|
+
const s = action.style ?? "default";
|
|
597
|
+
const btnClass = `btn btn-${s === "primary" || s === "danger" || s === "success" || s === "warning" ? s : "default"}`;
|
|
598
|
+
const colorStyle = action.color ? ` style="--btn-bg: ${esc(action.color)}"` : "";
|
|
599
|
+
const confirmAttr = action.confirm ? ` data-confirm="${esc(action.confirm)}"` : "";
|
|
600
|
+
const afterAttr = action.afterActions ? ` data-after='${JSON.stringify(action.afterActions).replace(/'/g, "'")}'` : "";
|
|
601
|
+
const afterMsgAttr = action.afterMessage ? ` data-after-msg="${esc(action.afterMessage)}"` : "";
|
|
602
|
+
const afterColorAttr = action.afterColor ? ` data-after-color="${esc(action.afterColor)}"` : "";
|
|
603
|
+
const timelineAttr = action.timeline ? ` data-timeline='${JSON.stringify(action.timeline).replace(/'/g, "'")}'` : "";
|
|
604
|
+
const workflowAttr = action.workflow ? ` data-workflow="${esc(action.workflow)}"` : "";
|
|
605
|
+
html += `<button class="${btnClass}"${colorStyle} data-action="${esc(action.name)}"${workflowAttr}${timelineAttr}${confirmAttr}${afterAttr}${afterMsgAttr}${afterColorAttr}>${esc(action.label)}</button>`;
|
|
606
|
+
}
|
|
607
|
+
html += `</div>
|
|
608
|
+
<div id="action-msg" class="msg"></div>`;
|
|
609
|
+
const streamTargets = new Set();
|
|
610
|
+
for (const action of visibleActions) {
|
|
611
|
+
for (const ev of action.timeline ?? []) {
|
|
612
|
+
if (ev.event === "stream" && ev.target)
|
|
613
|
+
streamTargets.add(ev.target.replace(/^#/, ""));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const id of streamTargets) {
|
|
617
|
+
html += `<div id="${esc(id)}" class="section-text" style="display:none; white-space:pre-wrap;"></div>`;
|
|
618
|
+
}
|
|
619
|
+
const sectionIdx = config.tabs.find(t => t.id === tabId)?.sections.indexOf(tableSection) ?? 0;
|
|
620
|
+
html += `<script>${ACTION_JS(base, "?tab=" + encodeURIComponent(tabId) + "§ion=" + sectionIdx + (_eq ? "&embed=true" : ""), rowId, rowDataJson)}</script>`;
|
|
621
|
+
}
|
|
622
|
+
html += `</div>
|
|
623
|
+
<div id="tl-toast-container"></div>
|
|
624
|
+
<div id="tl-preview-container"></div>
|
|
625
|
+
${config.autoplay ? `<script type="application/json" id="tl-autoplay">${JSON.stringify(config.autoplay)}</script>` : ""}
|
|
626
|
+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
627
|
+
<script>lucide.createIcons();</script>
|
|
628
|
+
<script>${TIMELINE_JS}</script>
|
|
629
|
+
</body>
|
|
630
|
+
</html>`;
|
|
631
|
+
return html;
|
|
632
|
+
}
|
|
633
|
+
function ACTION_JS(base, query, rowId, rowDataJson) {
|
|
634
|
+
return `
|
|
635
|
+
(function() {
|
|
636
|
+
var actionsDiv = document.getElementById('action-buttons');
|
|
637
|
+
var msg = document.getElementById('action-msg');
|
|
638
|
+
var rowData = ${rowDataJson};
|
|
639
|
+
var savedHtml = actionsDiv.innerHTML;
|
|
640
|
+
|
|
641
|
+
function bindButtons() {
|
|
642
|
+
var buttons = actionsDiv.querySelectorAll('[data-action]');
|
|
643
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
644
|
+
buttons[i].addEventListener('click', handleClick);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function handleClick(e) {
|
|
649
|
+
var btn = e.currentTarget;
|
|
650
|
+
var confirmText = btn.getAttribute('data-confirm');
|
|
651
|
+
if (confirmText && !confirm(confirmText)) return;
|
|
652
|
+
|
|
653
|
+
var timelineRaw = btn.getAttribute('data-timeline');
|
|
654
|
+
if (timelineRaw) {
|
|
655
|
+
var events = JSON.parse(timelineRaw);
|
|
656
|
+
if (window.__mugTimeline) window.__mugTimeline.play(events, btn);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
var actionName = btn.getAttribute('data-action');
|
|
661
|
+
var workflow = btn.getAttribute('data-workflow');
|
|
662
|
+
var afterRaw = btn.getAttribute('data-after');
|
|
663
|
+
var afterActions = afterRaw ? JSON.parse(afterRaw) : null;
|
|
664
|
+
var afterMsg = btn.getAttribute('data-after-msg');
|
|
665
|
+
var afterColor = btn.getAttribute('data-after-color');
|
|
666
|
+
var label = btn.textContent || 'Done';
|
|
667
|
+
var now = new Date();
|
|
668
|
+
var tzOpt = ${_tz ? "'" + _tz + "'" : "undefined"};
|
|
669
|
+
var ts = now.toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: tzOpt }) + ' ' + now.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', timeZone: tzOpt });
|
|
670
|
+
var statusText = afterMsg ? afterMsg.replace('{{timestamp}}', ts) : '\\u2713 ' + label + ' \\u00b7 ' + ts;
|
|
671
|
+
|
|
672
|
+
var statusStyle = afterColor ? ' style="color:' + afterColor + '"' : '';
|
|
673
|
+
var statusHtml = '<span class="action-status"' + statusStyle + '>' + statusText + '</span>';
|
|
674
|
+
if (afterActions) {
|
|
675
|
+
for (var k = 0; k < afterActions.length; k++) {
|
|
676
|
+
var aa = afterActions[k];
|
|
677
|
+
var s = aa.style || 'default';
|
|
678
|
+
var cls = 'btn btn-' + (s === 'primary' || s === 'danger' || s === 'success' || s === 'warning' ? s : 'default') + ' btn-sm';
|
|
679
|
+
statusHtml += '<button class="' + cls + '" data-action="' + aa.action + '" data-workflow="' + workflow + '">' + aa.label + '</button>';
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
actionsDiv.innerHTML = statusHtml;
|
|
683
|
+
msg.textContent = '';
|
|
684
|
+
msg.className = 'msg';
|
|
685
|
+
bindButtons();
|
|
686
|
+
|
|
687
|
+
fetch('${base}/action${query}', {
|
|
688
|
+
method: 'POST',
|
|
689
|
+
headers: { 'Content-Type': 'application/json' },
|
|
690
|
+
body: JSON.stringify({
|
|
691
|
+
action: actionName,
|
|
692
|
+
workflow: workflow,
|
|
693
|
+
rowId: '${rowId}',
|
|
694
|
+
rowData: rowData
|
|
695
|
+
})
|
|
696
|
+
}).then(function(r) { return r.json(); }).then(function(d) {
|
|
697
|
+
if (d.status !== 'ok') {
|
|
698
|
+
actionsDiv.innerHTML = savedHtml;
|
|
699
|
+
bindButtons();
|
|
700
|
+
msg.className = 'msg';
|
|
701
|
+
msg.textContent = d.error || 'Action failed.';
|
|
702
|
+
}
|
|
703
|
+
}).catch(function() {
|
|
704
|
+
actionsDiv.innerHTML = savedHtml;
|
|
705
|
+
bindButtons();
|
|
706
|
+
msg.className = 'msg';
|
|
707
|
+
msg.textContent = 'Network error. Try again.';
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
bindButtons();
|
|
712
|
+
})();`;
|
|
713
|
+
}
|
|
714
|
+
const STAT_ABBREV_JS = `
|
|
715
|
+
(function() {
|
|
716
|
+
var els = document.querySelectorAll('.stat-value[data-short]');
|
|
717
|
+
for (var i = 0; i < els.length; i++) {
|
|
718
|
+
var el = els[i];
|
|
719
|
+
if (el.scrollWidth > el.parentElement.clientWidth - 32) {
|
|
720
|
+
el.textContent = el.getAttribute('data-short');
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
`;
|
|
725
|
+
const TIMELINE_JS = `
|
|
726
|
+
(function() {
|
|
727
|
+
var toastContainer = document.getElementById('tl-toast-container');
|
|
728
|
+
var previewContainer = document.getElementById('tl-preview-container');
|
|
729
|
+
|
|
730
|
+
function showToast(msg) {
|
|
731
|
+
var el = document.createElement('div');
|
|
732
|
+
el.className = 'tl-toast';
|
|
733
|
+
el.textContent = msg;
|
|
734
|
+
toastContainer.appendChild(el);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function showPreview(ev) {
|
|
738
|
+
previewContainer.innerHTML = '';
|
|
739
|
+
var card = document.createElement('div');
|
|
740
|
+
card.className = 'tl-preview';
|
|
741
|
+
var ch = ev.channel || 'email';
|
|
742
|
+
card.innerHTML =
|
|
743
|
+
'<div class="tl-preview-channel">' + ch.toUpperCase() + '</div>' +
|
|
744
|
+
(ev.to ? '<div class="tl-preview-to">To: ' + esc(ev.to) + '</div>' : '') +
|
|
745
|
+
(ev.subject ? '<div class="tl-preview-subject">' + esc(ev.subject) + '</div>' : '') +
|
|
746
|
+
'<div class="tl-preview-body">' + esc(ev.body || '') + '</div>';
|
|
747
|
+
previewContainer.appendChild(card);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function streamText(ev) {
|
|
751
|
+
var target = ev.target ? document.querySelector(ev.target) : null;
|
|
752
|
+
if (!target) return;
|
|
753
|
+
target.style.display = '';
|
|
754
|
+
var text = ev.text || '';
|
|
755
|
+
var i = 0;
|
|
756
|
+
target.innerHTML = '';
|
|
757
|
+
var cursor = document.createElement('span');
|
|
758
|
+
cursor.className = 'tl-stream-cursor';
|
|
759
|
+
target.appendChild(cursor);
|
|
760
|
+
var interval = setInterval(function() {
|
|
761
|
+
if (i >= text.length) {
|
|
762
|
+
clearInterval(interval);
|
|
763
|
+
cursor.remove();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
cursor.before(document.createTextNode(text[i]));
|
|
767
|
+
i++;
|
|
768
|
+
}, 30);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function updateField(ev) {
|
|
772
|
+
var row = ev.row ? document.querySelector('[data-row-id="' + ev.row + '"]') : null;
|
|
773
|
+
if (!row) return;
|
|
774
|
+
var cells = row.querySelectorAll('td');
|
|
775
|
+
var headers = row.closest('table') ? row.closest('table').querySelectorAll('th') : [];
|
|
776
|
+
for (var j = 0; j < headers.length; j++) {
|
|
777
|
+
if (headers[j].textContent.trim().toLowerCase() === (ev.field || '').toLowerCase()) {
|
|
778
|
+
if (cells[j]) {
|
|
779
|
+
cells[j].textContent = ev.value || '';
|
|
780
|
+
cells[j].classList.add('tl-update-flash');
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function makeBadge(message, className, bg, fg) {
|
|
788
|
+
var badge = document.createElement('span');
|
|
789
|
+
badge.className = className;
|
|
790
|
+
if (bg) { badge.style.background = bg; badge.style.color = fg || '#fff'; }
|
|
791
|
+
badge.innerHTML = '<i data-lucide="pointer" style="width:12px;height:12px;display:inline-block;vertical-align:-1px;margin-right:4px"></i>' + esc(message);
|
|
792
|
+
return badge;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function highlightRow(ev) {
|
|
796
|
+
if (ev.action) {
|
|
797
|
+
var btn = document.querySelector('[data-action="' + ev.action + '"]');
|
|
798
|
+
if (!btn) return;
|
|
799
|
+
var bg = getComputedStyle(btn).backgroundColor;
|
|
800
|
+
btn.style.setProperty('--hl-color', bg);
|
|
801
|
+
btn.classList.add('tl-highlight-btn');
|
|
802
|
+
if (ev.message) {
|
|
803
|
+
btn.parentNode.insertBefore(makeBadge(ev.message, 'tl-highlight-btn-badge', bg, '#fff'), btn.nextSibling);
|
|
804
|
+
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
var row = ev.row ? document.querySelector('[data-row-id="' + ev.row + '"]') : null;
|
|
809
|
+
if (!row) return;
|
|
810
|
+
row.classList.add('tl-highlight');
|
|
811
|
+
if (ev.message) {
|
|
812
|
+
var lastCell = row.querySelector('td:last-child');
|
|
813
|
+
if (lastCell) {
|
|
814
|
+
lastCell.appendChild(makeBadge(ev.message, 'tl-highlight-badge', null, null));
|
|
815
|
+
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function esc(s) {
|
|
821
|
+
var d = document.createElement('div');
|
|
822
|
+
d.textContent = s;
|
|
823
|
+
return d.innerHTML;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function playEvents(events, triggerBtn) {
|
|
827
|
+
if (triggerBtn) triggerBtn.disabled = true;
|
|
828
|
+
var maxDelay = 0;
|
|
829
|
+
for (var i = 0; i < events.length; i++) {
|
|
830
|
+
if (events[i].delay > maxDelay) maxDelay = events[i].delay;
|
|
831
|
+
}
|
|
832
|
+
for (var i = 0; i < events.length; i++) {
|
|
833
|
+
(function(ev) {
|
|
834
|
+
setTimeout(function() {
|
|
835
|
+
if (ev.event === 'toast') showToast(ev.message || '');
|
|
836
|
+
else if (ev.event === 'preview') showPreview(ev);
|
|
837
|
+
else if (ev.event === 'stream') streamText(ev);
|
|
838
|
+
else if (ev.event === 'update') updateField(ev);
|
|
839
|
+
else if (ev.event === 'highlight') highlightRow(ev);
|
|
840
|
+
}, ev.delay * 1000);
|
|
841
|
+
})(events[i]);
|
|
842
|
+
}
|
|
843
|
+
if (triggerBtn) {
|
|
844
|
+
setTimeout(function() { triggerBtn.disabled = false; }, (maxDelay + 2) * 1000);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
window.__mugTimeline = { play: playEvents };
|
|
849
|
+
|
|
850
|
+
var autoplayEl = document.getElementById('tl-autoplay');
|
|
851
|
+
if (autoplayEl) {
|
|
852
|
+
try {
|
|
853
|
+
var events = JSON.parse(autoplayEl.textContent);
|
|
854
|
+
if (events && events.length) playEvents(events, null);
|
|
855
|
+
} catch(e) {}
|
|
856
|
+
}
|
|
857
|
+
})();
|
|
858
|
+
`;
|
|
859
|
+
export function renderDevBanner(currentIdentity, registeredUsers, accentColor, workspaceName, badge) {
|
|
860
|
+
const inputStyle = "flex:1;max-width:300px;padding:3px 8px;border:1px solid #1E2430;border-radius:4px;font-size:13px;background:#13161A;color:#E9E9EA";
|
|
861
|
+
let identityControl;
|
|
862
|
+
if (registeredUsers && registeredUsers.length > 0) {
|
|
863
|
+
const options = registeredUsers.map(u => `<option value="${esc(u)}"${u === currentIdentity ? " selected" : ""}>${esc(u)}</option>`).join("");
|
|
864
|
+
identityControl = `<select id="mug-dev-identity" style="${inputStyle}"><option value="">— select user —</option>${options}</select>`;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
identityControl = `<input type="email" id="mug-dev-identity" value="${esc(currentIdentity)}" placeholder="user@example.com" style="${inputStyle}">`;
|
|
868
|
+
}
|
|
869
|
+
const isDropdown = registeredUsers && registeredUsers.length > 0;
|
|
870
|
+
const applyBtn = isDropdown ? "" : `<button id="mug-dev-apply" style="padding:3px 12px;border:1px solid #1E2430;border-radius:4px;background:#13161A;color:#A3A6A7;cursor:pointer;font-size:13px;font-weight:500;white-space:nowrap;transition:all 0.15s">Apply</button>`;
|
|
871
|
+
const iconColor = accentColor || "#71B7FB";
|
|
872
|
+
const displayName = workspaceName ? esc(workspaceName.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")) : "";
|
|
873
|
+
const badgeText = badge || "Surface Preview";
|
|
874
|
+
return `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@500;600;700&display=swap" media="print" onload="this.media='all'">
|
|
875
|
+
<div id="mug-dev-banner" style="position:sticky;top:0;z-index:9999;background:#08090C;color:#E9E9EA;height:56px;padding:0 24px;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:13px;border-bottom:1px solid #1E2430"><div style="max-width:1200px;margin:0 auto;display:flex;align-items:center;gap:24px;height:100%">
|
|
876
|
+
<a href="/explorer" style="display:flex;align-items:center;gap:10px;text-decoration:none;color:inherit"><svg width="28" height="36" viewBox="0 0 291 371" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><path d="M134.129 122.279L140.915 122.8L141.155 124.478L141.394 126.157L151.534 126.674L161.674 127.191L167.245 128.392L172.817 129.593L176.743 130.899L180.668 132.206L179.403 132.915L178.139 133.624L170.306 134.807L162.472 135.99L156.284 135.995L150.097 136V138.406V140.813L103.591 140.606L57.0848 140.4L56.8325 138.2L56.5794 136L51.2438 135.984L45.9073 135.968L40.3186 135.187L34.7299 134.406L30.9376 133.523L27.1452 132.641V131.987V131.334L34.1311 129.714L41.117 128.094L47.362 127.247L53.6078 126.4H59.5374H65.4678V124.438V122.476L70.4577 122.192L75.4477 121.908L101.395 121.834L127.343 121.759L134.129 122.279Z" fill="${iconColor}"/><path fill-rule="evenodd" clip-rule="evenodd" d="M3.07464 136.078L5.58875 136.957V137.573V138.189L10.5787 139.553L15.5686 140.917L19.6268 142.398L23.6858 143.878L29.8645 144.739L36.0433 145.6H43.9161H51.7891L52.0422 147.8L52.2944 150H103.391H154.488L154.729 147.918L154.97 145.836L167.503 145.398L180.036 144.96L184.827 143.222L189.617 141.485L195.206 139.871L200.795 138.258L201.623 137.208L202.452 136.158L204.418 135.879L206.383 135.6L206.407 231.8L206.431 328H204.315H202.198L201.636 335.2L201.074 342.4H199.138H197.202V344.8V347.2H194.807H192.411V349.6V352H190.016H187.621V354.4V356.8H182.831H178.04V359.2V361.6H173.25H168.46L168.461 363.8L168.461 366H157.084H145.706L145.454 368.2L145.2 370.4H103.391H61.5821L61.329 368.2L61.0767 366L49.3005 365.778L37.5243 365.558V363.578V361.6H33.1331H28.742V359.2V356.8H23.9517H19.1613V354.453V352.106L16.9658 351.853L14.7702 351.6L14.5179 349.4L14.2648 347.2H11.9232H9.58069V342.4V337.6H7.18553H4.79036V332.8V328H2.3952H3.8147e-05V231.6V135.2H0.280282H0.560526L3.07464 136.078ZM19.1613 323.094V243.547V164H23.9517H28.742V239.2V314.4H31.1372H33.5323V321.6V328.8H35.5283H37.5243V330.8V332.8H39.9194H42.3146V335.2V337.6H44.7097H47.1049V342.4V347.2H42.7665H38.4288L38.1757 345L37.9235 342.8L33.4948 342.565L29.0654 342.33L28.1751 339.965L27.2841 337.6H25.649H24.0139L23.7832 330.6L23.5525 323.6L21.3569 323.347L19.1613 323.094Z" fill="${iconColor}"/><path d="M237.972 155.228L239.516 156.056V157.586V159.116L243.708 159.358L247.899 159.6L248.687 161.4L249.475 163.2H251.182H252.889L255.384 163.7L257.879 164.2V166.5V168.8H260.189H262.499L262.961 170.007L263.423 171.214L265.442 172.135L267.46 173.057V175.275V177.494L269.655 177.747L271.851 178L272.101 180.6L272.352 183.2H274.585H276.819L276.438 185.2L276.056 187.2H278.544H281.033V192V196.8H283.428H285.823V204V211.2H288.218H290.613V230V248.8H288.218H285.823V255.547V262.294L283.627 262.547L281.432 262.8L281.192 267.4L280.953 272H278.977H277.001L276.522 274.4L276.043 276.8H274.146H272.25V278.508V280.217L270.454 282.033L268.658 283.85L267.628 285.125L266.598 286.4H264.634H262.67V288.8V291.2H260.274H257.879V293.2V295.2H255.484H253.089V297.6V300H246.303H239.516V302.4V304.8H227.541H215.565V295.622V286.442L227.341 286.222L239.117 286L239.086 283.8L239.054 281.6H243.677H248.299V279.2V276.8H250.694H253.089V274.4V272H255.484H257.879V269.6V267.2H260.274H262.67V264.8V262.4H265.065H267.46V257.492V252.583L269.456 250.73L271.452 248.876V230.038V211.2H269.487H267.522L267.291 204.2L267.061 197.2L264.865 196.947L262.67 196.694V194.347V192H260.274H257.879V190.063V188.127L255.484 187.6L253.089 187.073V185.19V183.306L250.893 183.053L248.698 182.8L248.445 180.6L248.192 178.4H243.907H239.623L239.369 176.2L239.117 174L227.341 173.778L215.565 173.558V163.978V154.4H225.996H236.427L237.972 155.228Z" fill="${iconColor}"/><path fill-rule="evenodd" clip-rule="evenodd" d="M107.782 6.8V13.6H110.073H112.364L112.858 16.6L113.354 19.6V23.2V26.8L112.866 29.756L112.378 32.7128L110.28 32.956L108.182 33.2L107.929 35.4L107.676 37.6H105.786H103.897L103.644 39.8L103.391 42L101.196 42.2528L99.0001 42.5064V44.0792V45.6512L97.8433 48.4256L96.6864 51.2H95.5527H94.419L93.924 54.2L93.4289 57.2L93.4904 64.4L93.5511 71.6L94.0805 75.6L94.609 79.6L96.8045 79.8528L99.0001 80.1064V82.0528V84H103.391H107.782V79.6V75.2H105.88H103.976L103.495 73.4L103.014 71.6L103.003 65.852L102.992 60.104L103.77 58.052L104.549 56H106.113H107.676L107.929 53.8L108.182 51.6L110.328 51.3512L112.476 51.1032L112.724 48.9512L112.972 46.8L115.167 46.5472L117.363 46.2936V44.4V42.5064L119.559 42.2528L121.754 42L121.992 37.4496L122.23 32.8992L124.387 32.6496L126.544 32.4L126.303 26L126.062 19.6L123.583 12.2392L121.105 4.87841L119.433 4.6392L117.762 4.4L117.51 2.2L117.257 0H112.52H107.782V6.8ZM66.2662 37.548V47.096L65.4878 49.148L64.7093 51.2H63.0926H61.4759V53.6V56H59.0807H56.6856V58.4V60.8H54.2904H51.8952V69.9472V79.0936L54.0908 79.3472L56.2864 79.6L56.5386 81.8L56.7917 84H61.1294H65.4678V79.6V75.2H63.4719H61.4759V70.4V65.6H63.4088H65.3409L65.867 63.2L66.3932 60.8H68.2726H70.152L70.4051 58.6L70.6573 56.4L72.8042 56.1512L74.9519 55.9032L75.1994 53.7512L75.4477 51.6L77.6432 51.3472L79.8388 51.0936V41.9472V32.8H77.4436H75.0485V30.4V28H70.6573H66.2662V37.548ZM146.096 36.6L146.087 45.2L145.592 48.2L145.097 51.2H143.206H141.315V53.6V56H138.919H136.524V58.4V60.8H134.129H131.734V69.9368V79.0728L134.129 79.6L136.524 80.1272V82.0632V84H140.915H145.307V79.6V75.2H143.359H141.413L140.876 71.6096L140.338 68.02L140.801 66.8096L141.264 65.6H143.286H145.307V63.2V60.8H147.649H149.991L150.244 58.6L150.496 56.4L152.692 56.1472L154.887 55.8936V53.5472V51.2H156.883H158.879V42V32.8H156.883H154.887V30.4V28H150.496H146.105L146.096 36.6Z" fill="${iconColor}"/><path d="M145.307 105.2V107.2L147.901 107.222L150.496 107.243L161.274 107.678L172.053 108.113L173.051 108.431L174.049 108.75V110.261V111.772L178.719 112.262L183.39 112.75L189.697 114.747L196.004 116.744L199.797 119.154L203.589 121.565V123.164V124.763L201.78 125.951L199.97 127.139L196.544 128.35L193.118 129.562L190.538 126.981L187.958 124.4L182.8 122.609L177.641 120.818L167.262 118.471L156.883 116.125L149.698 115.261L142.512 114.396L132.217 113.598L121.922 112.8H104.641H87.362L73.0214 113.988L58.6815 115.176L53.0928 116.069L47.5041 116.962L35.7965 119.63L24.0882 122.298L21.0264 123.871L17.9638 125.444L16.0987 127.446L14.2345 129.449L12.0844 128.831L9.93437 128.214L6.51408 126.465L3.09379 124.716L3.3429 122.962L3.59278 121.209L7.73164 118.734L11.8705 116.26L17.2644 114.486L22.6575 112.711L27.637 112.246L32.6158 111.78L33.0892 109.89L33.5627 108H40.2108H46.8598L54.1675 107.48L61.4759 106.961V105.08V103.2H103.391H145.307V105.2Z" fill="${iconColor}"/></svg>
|
|
877
|
+
${displayName ? `<strong style="white-space:nowrap;font-family:'Outfit',sans-serif;font-size:18px;font-weight:700;letter-spacing:-0.5px">${displayName}</strong>` : ""}</a>
|
|
878
|
+
<span style="font-size:11px;padding:2px 8px;border-radius:4px;background:#13161A;border:1px solid #1E2430;color:#A3A6A7;font-weight:500;white-space:nowrap">${esc(badgeText)}</span>
|
|
879
|
+
<span style="flex:1"></span>
|
|
880
|
+
<span style="white-space:nowrap;color:#A3A6A7">View as:</span>
|
|
881
|
+
${identityControl}
|
|
882
|
+
${applyBtn}
|
|
883
|
+
</div></div>
|
|
884
|
+
<script>
|
|
885
|
+
(function() {
|
|
886
|
+
var el = document.getElementById('mug-dev-identity');
|
|
887
|
+
function apply() {
|
|
888
|
+
var val = (el.value || '').trim();
|
|
889
|
+
document.cookie = 'mug_dev_identity=' + encodeURIComponent(val) + ';path=/;max-age=31536000';
|
|
890
|
+
location.reload();
|
|
891
|
+
}
|
|
892
|
+
if (el.tagName === 'INPUT') {
|
|
893
|
+
var btn = document.getElementById('mug-dev-apply');
|
|
894
|
+
var orig = el.value;
|
|
895
|
+
function updateBtn() {
|
|
896
|
+
var dirty = el.value.trim() !== orig.trim();
|
|
897
|
+
btn.style.background = dirty ? '#71B7FB' : '#13161A';
|
|
898
|
+
btn.style.color = dirty ? '#08090C' : '#A3A6A7';
|
|
899
|
+
btn.style.borderColor = dirty ? '#71B7FB' : '#1E2430';
|
|
900
|
+
}
|
|
901
|
+
el.addEventListener('input', updateBtn);
|
|
902
|
+
btn.addEventListener('click', apply);
|
|
903
|
+
el.addEventListener('keydown', function(e) { if (e.key === 'Enter') apply(); });
|
|
904
|
+
} else {
|
|
905
|
+
el.addEventListener('change', apply);
|
|
906
|
+
}
|
|
907
|
+
})();
|
|
908
|
+
</script>`;
|
|
909
|
+
}
|
|
910
|
+
const PORTAL_CSS = `
|
|
911
|
+
:root { --accent: #1a1a1a; }
|
|
912
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
913
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #1a1a1a; line-height: 1.5; }
|
|
914
|
+
.container { max-width: 800px; margin: 0 auto; padding: 24px 16px; }
|
|
915
|
+
header { margin-bottom: 24px; position: relative; }
|
|
916
|
+
.brand-logo { display: block; max-height: 72px; max-width: 240px; margin-bottom: 12px; }
|
|
917
|
+
h1 { font-size: 24px; font-weight: 600; margin-bottom: 4px; }
|
|
918
|
+
.desc { color: #666; font-size: 14px; margin-bottom: 12px; }
|
|
919
|
+
.header-meta { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
|
|
920
|
+
.session { font-size: 13px; color: #666; background: #f0f0f0; padding: 6px 12px; border-radius: 6px; display: inline-block; }
|
|
921
|
+
.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; }
|
|
922
|
+
.logout-btn:hover { background: #e0e0e0; color: #333; }
|
|
923
|
+
.logout-btn svg { width: 16px; height: 16px; }
|
|
924
|
+
|
|
925
|
+
.tab-bar { display: flex; gap: 0; border-bottom: 2px solid #e0e0e0; margin-bottom: 20px; overflow-x: auto; overflow-y: hidden; }
|
|
926
|
+
.tab-link { padding: 10px 20px; font-size: 14px; font-weight: 500; color: #666; text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -2px; white-space: nowrap; transition: color 0.15s, border-color 0.15s; }
|
|
927
|
+
.tab-link:hover { color: var(--accent); }
|
|
928
|
+
.tab-active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
|
|
929
|
+
.tab-colored { color: var(--tab-color); }
|
|
930
|
+
.tab-colored:hover { color: var(--tab-color); }
|
|
931
|
+
.tab-colored.tab-active { color: var(--tab-color); border-bottom-color: var(--tab-color); }
|
|
932
|
+
.tab-count { font-size: 12px; font-weight: 600; background: #e8e8e8; color: #666; padding: 1px 7px; border-radius: 10px; margin-left: 4px; }
|
|
933
|
+
.tab-active .tab-count { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
|
|
934
|
+
.tab-colored .tab-count { background: color-mix(in srgb, var(--tab-color) 15%, transparent); color: var(--tab-color); }
|
|
935
|
+
.tab-content { }
|
|
936
|
+
|
|
937
|
+
.portal-links { display: flex; gap: 8px; margin-top: 0; margin-bottom: 20px; flex-wrap: wrap; }
|
|
938
|
+
.link-btn { display: inline-block; padding: 8px 16px; background: var(--accent); color: #fff; text-decoration: none; border-radius: 8px; font-size: 14px; font-weight: 500; transition: background 0.15s; }
|
|
939
|
+
.link-btn:hover { background: color-mix(in srgb, var(--accent) 80%, #000); }
|
|
940
|
+
|
|
941
|
+
.table-wrap { overflow-x: auto; border-radius: 12px; border: 1px solid #e0e0e0; background: #fff; }
|
|
942
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
943
|
+
th { padding: 10px 14px; text-align: left; background: #fafafa; color: #666; font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #e0e0e0; }
|
|
944
|
+
td { padding: 10px 14px; border-bottom: 1px solid #f0f0f0; }
|
|
945
|
+
tr.clickable { cursor: pointer; transition: background 0.1s; }
|
|
946
|
+
tr.clickable:hover { background: #f8f8f8; }
|
|
947
|
+
tr:last-child td { border-bottom: none; }
|
|
948
|
+
|
|
949
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: capitalize; }
|
|
950
|
+
|
|
951
|
+
.empty { text-align: center; padding: 48px 24px; color: #999; font-size: 15px; background: #fff; border-radius: 12px; border: 1px solid #e0e0e0; }
|
|
952
|
+
|
|
953
|
+
.pagination { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 16px; }
|
|
954
|
+
.page-btn { padding: 8px 16px; background: #fff; border: 1px solid #ddd; border-radius: 8px; text-decoration: none; color: var(--accent); font-size: 14px; transition: background 0.15s; }
|
|
955
|
+
.page-btn:hover { background: #f0f0f0; }
|
|
956
|
+
.page-info { font-size: 14px; color: #666; }
|
|
957
|
+
|
|
958
|
+
.back-link { display: inline-block; margin-bottom: 16px; color: #666; text-decoration: none; font-size: 14px; transition: color 0.15s; }
|
|
959
|
+
.back-link:hover { color: var(--accent); }
|
|
960
|
+
|
|
961
|
+
.detail-fields { background: #fff; border-radius: 12px; border: 1px solid #e0e0e0; overflow: hidden; }
|
|
962
|
+
.detail-field { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
|
|
963
|
+
.detail-field:last-child { border-bottom: none; }
|
|
964
|
+
.detail-label { font-size: 12px; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
|
|
965
|
+
.detail-value { font-size: 15px; }
|
|
966
|
+
.multiline { white-space: pre-wrap; }
|
|
967
|
+
|
|
968
|
+
.detail-actions { display: flex; gap: 12px; margin-top: 20px; flex-wrap: wrap; }
|
|
969
|
+
.btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 16px; font-weight: 500; cursor: pointer; transition: background 0.15s; }
|
|
970
|
+
.btn-primary { background: var(--accent); color: #fff; }
|
|
971
|
+
.btn-primary:hover { background: color-mix(in srgb, var(--accent) 80%, #000); }
|
|
972
|
+
.btn-danger { background: #dc2626; color: #fff; }
|
|
973
|
+
.btn-danger:hover { background: #b91c1c; }
|
|
974
|
+
.btn-success { background: #16a34a; color: #fff; }
|
|
975
|
+
.btn-success:hover { background: #15803d; }
|
|
976
|
+
.btn-warning { background: #d97706; color: #fff; }
|
|
977
|
+
.btn-warning:hover { background: #b45309; }
|
|
978
|
+
.btn-default { background: #e8e8e8; color: #333; }
|
|
979
|
+
.btn-default:hover { background: #ddd; }
|
|
980
|
+
.btn[style*="--btn-bg"] { background: var(--btn-bg); color: #fff; }
|
|
981
|
+
.btn[style*="--btn-bg"]:hover { background: color-mix(in srgb, var(--btn-bg) 80%, #000); }
|
|
982
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
983
|
+
|
|
984
|
+
.msg { font-size: 14px; color: #666; margin-top: 12px; }
|
|
985
|
+
.msg.success { color: var(--accent); font-weight: 500; }
|
|
986
|
+
.action-status { color: var(--accent); font-weight: 500; font-size: 14px; }
|
|
987
|
+
.btn-sm { font-size: 13px; padding: 6px 14px; }
|
|
988
|
+
.detail-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
989
|
+
|
|
990
|
+
.section-stats { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; }
|
|
991
|
+
.stat-card { flex: 1; min-width: 120px; background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 16px; text-decoration: none; color: inherit; }
|
|
992
|
+
a.stat-card { cursor: pointer; transition: border-color 0.15s, box-shadow 0.15s; }
|
|
993
|
+
a.stat-card:hover { border-color: #bbb; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
|
994
|
+
.stat-label { font-size: 12px; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
995
|
+
.stat-value { font-size: 28px; font-weight: 700; color: var(--accent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
996
|
+
|
|
997
|
+
.section-progress { display: flex; flex-direction: column; gap: 16px; margin-bottom: 20px; background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 16px; }
|
|
998
|
+
.progress-item { }
|
|
999
|
+
.progress-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
|
|
1000
|
+
.progress-label { font-size: 14px; font-weight: 500; }
|
|
1001
|
+
.progress-pct { font-size: 13px; color: #666; font-weight: 600; }
|
|
1002
|
+
.progress-track { height: 8px; background: #e8e8e8; border-radius: 4px; overflow: hidden; }
|
|
1003
|
+
.progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
|
|
1004
|
+
.progress-subtitle { font-size: 12px; color: #888; margin-top: 4px; }
|
|
1005
|
+
|
|
1006
|
+
.section-text { background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 20px; margin-bottom: 20px; font-size: 14px; line-height: 1.6; }
|
|
1007
|
+
.section-text h2 { font-size: 20px; margin-bottom: 8px; }
|
|
1008
|
+
.section-text h3 { font-size: 17px; margin-bottom: 6px; }
|
|
1009
|
+
.section-text h4 { font-size: 15px; margin-bottom: 4px; }
|
|
1010
|
+
.section-text p { margin-bottom: 12px; }
|
|
1011
|
+
.section-text p:last-child { margin-bottom: 0; }
|
|
1012
|
+
.section-text ul { margin: 0 0 12px 20px; }
|
|
1013
|
+
.section-text li { margin-bottom: 4px; }
|
|
1014
|
+
|
|
1015
|
+
.section-chart { background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 20px; margin-bottom: 20px; }
|
|
1016
|
+
.chart-title { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
|
|
1017
|
+
.chart-bar-svg { width: 100%; height: auto; }
|
|
1018
|
+
.chart-donut-layout { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
|
|
1019
|
+
.chart-donut-svg { width: 160px; height: 160px; flex-shrink: 0; }
|
|
1020
|
+
.chart-legend { display: flex; flex-direction: column; gap: 6px; }
|
|
1021
|
+
.legend-item { font-size: 13px; display: flex; align-items: center; gap: 8px; }
|
|
1022
|
+
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
1023
|
+
.chart-others { font-size: 12px; color: #888; margin-top: 8px; }
|
|
1024
|
+
|
|
1025
|
+
.section-gallery { display: grid; gap: 16px; margin-bottom: 20px; }
|
|
1026
|
+
.gallery-card { background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; overflow: hidden; }
|
|
1027
|
+
.gallery-img { aspect-ratio: 4/3; overflow: hidden; background: #f5f5f5; }
|
|
1028
|
+
.gallery-img img { width: 100%; height: 100%; object-fit: cover; }
|
|
1029
|
+
.gallery-placeholder { display: flex; align-items: center; justify-content: center; }
|
|
1030
|
+
.gallery-title { font-size: 15px; font-weight: 600; padding: 12px 14px 4px; }
|
|
1031
|
+
.gallery-field { font-size: 13px; padding: 2px 14px; color: #666; }
|
|
1032
|
+
.gallery-field:last-child { padding-bottom: 12px; }
|
|
1033
|
+
.gallery-field-label { font-weight: 600; color: #999; font-size: 11px; text-transform: uppercase; letter-spacing: 0.3px; }
|
|
1034
|
+
|
|
1035
|
+
.section-accordion { margin-bottom: 20px; border: 1px solid #e0e0e0; border-radius: 12px; overflow: hidden; }
|
|
1036
|
+
.accordion-summary { padding: 14px 16px; font-size: 15px; font-weight: 600; cursor: pointer; background: #fafafa; list-style: none; display: flex; align-items: center; gap: 8px; }
|
|
1037
|
+
.accordion-summary::-webkit-details-marker { display: none; }
|
|
1038
|
+
.accordion-summary::before { content: ''; display: inline-block; width: 8px; height: 8px; border-right: 2px solid #666; border-bottom: 2px solid #666; transform: rotate(-45deg); transition: transform 0.2s; flex-shrink: 0; }
|
|
1039
|
+
details[open] > .accordion-summary::before { transform: rotate(45deg); }
|
|
1040
|
+
.accordion-body { padding: 16px; }
|
|
1041
|
+
.accordion-body > :last-child { margin-bottom: 0; }
|
|
1042
|
+
|
|
1043
|
+
#tl-toast-container { position: fixed; top: 16px; right: 16px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; max-width: 380px; max-height: calc(100vh - 32px); overflow-y: auto; pointer-events: none; }
|
|
1044
|
+
.tl-toast { background: #1a1a1a; color: #fff; padding: 12px 16px; border-radius: 10px; font-size: 14px; line-height: 1.4; box-shadow: 0 4px 16px rgba(0,0,0,0.2); animation: tl-slide-in 0.3s ease-out; pointer-events: auto; }
|
|
1045
|
+
.tl-toast.tl-out { animation: tl-slide-out 0.3s ease-in forwards; }
|
|
1046
|
+
@keyframes tl-slide-in { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: none; } }
|
|
1047
|
+
@keyframes tl-slide-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(40px); } }
|
|
1048
|
+
|
|
1049
|
+
#tl-preview-container { position: fixed; bottom: 16px; right: 16px; z-index: 999; max-width: 400px; pointer-events: none; }
|
|
1050
|
+
.tl-preview { background: #fff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.12); animation: tl-preview-in 0.4s ease-out; pointer-events: auto; }
|
|
1051
|
+
.tl-preview.tl-out { animation: tl-preview-out 0.3s ease-in forwards; }
|
|
1052
|
+
@keyframes tl-preview-in { from { opacity: 0; transform: translateX(60px); } to { opacity: 1; transform: none; } }
|
|
1053
|
+
@keyframes tl-preview-out { from { opacity: 1; } to { opacity: 0; } }
|
|
1054
|
+
.tl-preview-channel { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 4px; }
|
|
1055
|
+
.tl-preview-to { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 2px; }
|
|
1056
|
+
.tl-preview-subject { font-size: 13px; color: #666; margin-bottom: 8px; }
|
|
1057
|
+
.tl-preview-body { font-size: 14px; color: #1a1a1a; line-height: 1.5; white-space: pre-wrap; }
|
|
1058
|
+
|
|
1059
|
+
.tl-stream-cursor { display: inline-block; width: 2px; height: 1.1em; background: var(--accent, #1a1a1a); vertical-align: text-bottom; animation: tl-blink 0.6s step-end infinite; }
|
|
1060
|
+
@keyframes tl-blink { 50% { opacity: 0; } }
|
|
1061
|
+
|
|
1062
|
+
.tl-update-flash { animation: tl-flash 0.6s ease-out; }
|
|
1063
|
+
@keyframes tl-flash { 0% { background: #fef3c7; } 100% { background: transparent; } }
|
|
1064
|
+
|
|
1065
|
+
tr.tl-highlight { background: rgba(234, 179, 8, 0.06); animation: tl-row-pulse 2s ease-in-out infinite; }
|
|
1066
|
+
tr.tl-highlight td { background: rgba(234, 179, 8, 0.06); }
|
|
1067
|
+
@keyframes tl-row-pulse { 0%, 100% { outline: 1.5px solid rgba(234, 179, 8, 0.4); box-shadow: 0 0 0 0 rgba(234, 179, 8, 0); } 50% { outline: 1.5px solid rgba(234, 179, 8, 0.9); box-shadow: 0 0 4px 3px rgba(234, 179, 8, 0.15); } }
|
|
1068
|
+
.tl-highlight-badge { display: inline-flex; align-items: center; margin-left: 8px; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; background: #fef3c7; color: #92400e; white-space: nowrap; vertical-align: middle; animation: tl-badge-in 0.3s ease-out; }
|
|
1069
|
+
@keyframes tl-badge-in { from { opacity: 0; transform: scale(0.85); } to { opacity: 1; transform: none; } }
|
|
1070
|
+
|
|
1071
|
+
.tl-highlight-btn { animation: tl-btn-pulse 2s ease-in-out infinite; }
|
|
1072
|
+
@keyframes tl-btn-pulse { 0%, 100% { box-shadow: 0 0 0 0 var(--hl-color, rgba(0,0,0,0.2)); } 50% { box-shadow: 0 0 0 8px color-mix(in srgb, var(--hl-color, rgba(0,0,0,0.2)) 35%, transparent); } }
|
|
1073
|
+
.tl-highlight-btn-badge { display: inline-flex; align-items: center; margin-left: 10px; padding: 3px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; white-space: nowrap; vertical-align: middle; animation: tl-badge-in 0.3s ease-out; }
|
|
1074
|
+
|
|
1075
|
+
@media (max-width: 480px) {
|
|
1076
|
+
.container { padding: 16px 12px; }
|
|
1077
|
+
h1 { font-size: 20px; }
|
|
1078
|
+
th, td { padding: 8px 10px; font-size: 13px; }
|
|
1079
|
+
.btn { padding: 14px 20px; }
|
|
1080
|
+
.stat-value { font-size: 22px; }
|
|
1081
|
+
.section-stats { gap: 8px; }
|
|
1082
|
+
.stat-card { min-width: 100px; padding: 12px; }
|
|
1083
|
+
.chart-donut-layout { flex-direction: column; align-items: flex-start; }
|
|
1084
|
+
.section-gallery { grid-template-columns: 1fr !important; }
|
|
1085
|
+
#tl-toast-container { top: auto; bottom: 16px; right: 8px; left: 8px; max-width: none; }
|
|
1086
|
+
.tl-toast { border-radius: 8px; }
|
|
1087
|
+
#tl-preview-container { right: 8px; left: 8px; bottom: 8px; max-width: none; }
|
|
1088
|
+
}
|
|
1089
|
+
`;
|