mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
fix(cloudcp): remove Pulse Account CSP-blocked styles
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "f26a39d5beef5f6fc0649b14f45a48c68df81e299b579087430cd05e42c810bd",
|
||||
"source_hash": "07cc157c9e51171f54b181dc953c685e2687e14af66407867c678e0bcd370540",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+40
-1
@@ -213,6 +213,13 @@ header .logout-btn:hover {
|
||||
.btn-danger:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
.btn-compact {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.link-button {
|
||||
text-decoration: none;
|
||||
}
|
||||
.account-actions {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
@@ -285,6 +292,14 @@ header .logout-btn:hover {
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.team-message-cell {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.team-message-cell.error {
|
||||
color: #991b1b;
|
||||
}
|
||||
.team-table select {
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
@@ -498,6 +513,9 @@ header .logout-btn:hover {
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
}
|
||||
.service-result {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.service-panel .subsection {
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
@@ -572,8 +590,26 @@ header .logout-btn:hover {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
.empty-state-spaced {
|
||||
margin-top: 48px;
|
||||
}
|
||||
.support-copy {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.support-link {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.badge-role {
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
.workspace-state-label {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.spinner {
|
||||
display: none;
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #93c5fd;
|
||||
@@ -581,6 +617,9 @@ header .logout-btn:hover {
|
||||
border-radius: 50%;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
|
||||
+16
-20
@@ -15,7 +15,7 @@
|
||||
var tr = document.createElement("tr");
|
||||
var td = document.createElement("td");
|
||||
td.setAttribute("colspan", "3");
|
||||
td.style.cssText = "text-align:center;padding:16px;color:" + (isError ? "#991b1b" : "#94a3b8");
|
||||
td.className = "team-message-cell" + (isError ? " error" : "");
|
||||
td.textContent = msg;
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
@@ -63,7 +63,7 @@
|
||||
if (!form) return;
|
||||
form.classList.toggle("visible", entry.addWorkspaceOpen);
|
||||
if (spinner) {
|
||||
spinner.style.display = entry.createWorkspace.pending ? "block" : "none";
|
||||
spinner.hidden = !entry.createWorkspace.pending;
|
||||
}
|
||||
}
|
||||
function renderTeamSection(accountID, entry) {
|
||||
@@ -832,7 +832,7 @@
|
||||
function setVisible(id, visible) {
|
||||
var el = getElement3(id);
|
||||
if (el) {
|
||||
el.style.display = visible ? "block" : "none";
|
||||
el.hidden = !visible;
|
||||
}
|
||||
}
|
||||
function setValue(id, value) {
|
||||
@@ -876,23 +876,19 @@
|
||||
function renderManagePanel(flowState) {
|
||||
var root = getElement3("manage-service-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<h3>Manage subscriptions</h3><p>Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.</p><div id="manage-inline-step1"><div class="form-group"><label for="manage-inline-email">Email address</label><input type="email" id="manage-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="manage-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button></div></div><div id="manage-inline-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="manage-inline-code">Verification code</label><input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-confirm" data-account-service-action="manage-inline-confirm">Open Customer Portal</button></div><div class="helper-text">Need a new code? <a href="#" id="manage-inline-resend" data-account-service-action="manage-inline-resend">Send again</a></div></div><div class="service-status" id="manage-inline-status"></div>';
|
||||
root.innerHTML = '<h3>Manage subscriptions</h3><p>Request a verification code for the commercial email, then open the Stripe customer portal for billing changes, invoices, and subscription actions.</p><div id="manage-inline-step1"><div class="form-group"><label for="manage-inline-email">Email address</label><input type="email" id="manage-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="manage-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button></div></div><div id="manage-inline-step2"' + (flowState.step2Visible ? "" : " hidden") + '><div class="form-group"><label for="manage-inline-code">Verification code</label><input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="manage-inline-confirm" data-account-service-action="manage-inline-confirm">Open Customer Portal</button></div><div class="helper-text">Need a new code? <a href="#" id="manage-inline-resend" data-account-service-action="manage-inline-resend">Send again</a></div></div><div class="service-status" id="manage-inline-status"></div>';
|
||||
}
|
||||
function renderRetrievePanel(flowState) {
|
||||
var root = getElement3("retrieve-service-root");
|
||||
if (!root) return;
|
||||
var result = flowState.result;
|
||||
var invoiceURL = result && result.invoice_url ? result.invoice_url : "#";
|
||||
var invoiceDisplay = result && result.invoice_url ? "inline-block" : "none";
|
||||
var copyDisplay = result ? "inline-block" : "none";
|
||||
var resultDisplay = result ? "block" : "none";
|
||||
root.innerHTML = '<h3>Retrieve licenses</h3><p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p><div id="retrieve-inline-step1"><div class="form-group"><label for="retrieve-inline-email">Email address</label><input type="email" id="retrieve-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="retrieve-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button></div></div><div id="retrieve-inline-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="retrieve-inline-code">Verification code</label><input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button><button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy" style="display:' + copyDisplay + '">Copy License Key</button><a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener" style="display:' + invoiceDisplay + '">View Invoice</a></div><div class="helper-text">Use the latest active self-hosted license for this commercial email.</div></div><div class="service-status" id="retrieve-inline-status"></div><div id="retrieve-inline-result" style="display:' + resultDisplay + '; margin-top:14px"><label for="retrieve-inline-token">License key</label><textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : "") + '</textarea><div class="result-grid"><div><div class="result-meta-label">Plan</div><div class="result-meta-value" id="retrieve-inline-tier">' + escapeText(result ? result.tier : "") + '</div></div><div><div class="result-meta-label">Issued</div><div class="result-meta-value" id="retrieve-inline-issued">' + escapeText(result ? new Date(result.issued_at).toLocaleString() : "") + '</div></div><div><div class="result-meta-label">Expires</div><div class="result-meta-value" id="retrieve-inline-expires">' + escapeText(result ? result.expires_at ? new Date(result.expires_at).toLocaleString() : "Does not expire" : "") + '</div></div><div><div class="result-meta-label">Purchase Email</div><div class="result-meta-value" id="retrieve-inline-email-value">' + escapeText(result ? result.email : "") + "</div></div></div></div>";
|
||||
root.innerHTML = '<h3>Retrieve licenses</h3><p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p><div id="retrieve-inline-step1"><div class="form-group"><label for="retrieve-inline-email">Email address</label><input type="email" id="retrieve-inline-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="retrieve-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button></div></div><div id="retrieve-inline-step2"' + (flowState.step2Visible ? "" : " hidden") + '><div class="form-group"><label for="retrieve-inline-code">Verification code</label><input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button><button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy"' + (result ? "" : " hidden") + '>Copy License Key</button><a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener"' + (result && result.invoice_url ? "" : " hidden") + '>View Invoice</a></div><div class="helper-text">Use the latest active self-hosted license for this commercial email.</div></div><div class="service-status" id="retrieve-inline-status"></div><div id="retrieve-inline-result" class="service-result"' + (result ? "" : " hidden") + '><label for="retrieve-inline-token">License key</label><textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : "") + '</textarea><div class="result-grid"><div><div class="result-meta-label">Plan</div><div class="result-meta-value" id="retrieve-inline-tier">' + escapeText(result ? result.tier : "") + '</div></div><div><div class="result-meta-label">Issued</div><div class="result-meta-value" id="retrieve-inline-issued">' + escapeText(result ? new Date(result.issued_at).toLocaleString() : "") + '</div></div><div><div class="result-meta-label">Expires</div><div class="result-meta-value" id="retrieve-inline-expires">' + escapeText(result ? result.expires_at ? new Date(result.expires_at).toLocaleString() : "Does not expire" : "") + '</div></div><div><div class="result-meta-label">Purchase Email</div><div class="result-meta-value" id="retrieve-inline-email-value">' + escapeText(result ? result.email : "") + "</div></div></div></div>";
|
||||
}
|
||||
function renderExportPanel(flowState) {
|
||||
var root = getElement3("data-export-root");
|
||||
if (!root) return;
|
||||
var resultDisplay = flowState.result ? "block" : "none";
|
||||
root.innerHTML = '<h4>Export My Data</h4><div id="data-export-step1"><div class="form-group"><label for="data-export-email">Email address</label><input type="email" id="data-export-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-export-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button></div></div><div id="data-export-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="data-export-code">Verification code</label><input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-confirm" data-account-service-action="data-export-confirm">Export My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div></div><div class="service-status" id="data-export-status"></div><div id="data-export-result" style="display:' + resultDisplay + '; margin-top:14px"><label for="data-export-payload">Export payload</label><textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : "") + "</textarea></div>";
|
||||
root.innerHTML = '<h4>Export My Data</h4><div id="data-export-step1"><div class="form-group"><label for="data-export-email">Email address</label><input type="email" id="data-export-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-export-email"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button></div></div><div id="data-export-step2"' + (flowState.step2Visible ? "" : " hidden") + '><div class="form-group"><label for="data-export-code">Verification code</label><input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code"></div><div class="form-actions"><button class="btn-primary" type="button" id="data-export-confirm" data-account-service-action="data-export-confirm">Export My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div></div><div class="service-status" id="data-export-status"></div><div id="data-export-result" class="service-result"' + (flowState.result ? "" : " hidden") + '><label for="data-export-payload">Export payload</label><textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : "") + "</textarea></div>";
|
||||
}
|
||||
function renderExportResult(result) {
|
||||
setVisible("data-export-result", !!result);
|
||||
@@ -901,7 +897,7 @@
|
||||
function renderDeletePanel(flowState) {
|
||||
var root = getElement3("data-delete-root");
|
||||
if (!root) return;
|
||||
root.innerHTML = '<h4>Delete My Data</h4><div class="warning"><strong>Warning:</strong> deleting commercial data also revokes license records and cannot be undone.</div><div id="data-delete-step1"><div class="form-group"><label for="data-delete-email">Email address</label><input type="email" id="data-delete-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-delete-email"></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button></div></div><div id="data-delete-step2" style="display:' + (flowState.step2Visible ? "block" : "none") + '"><div class="form-group"><label for="data-delete-code">Verification code</label><input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code"></div><div class="checkbox-row"><input type="checkbox" id="data-delete-confirm-check"' + (flowState.checkboxChecked ? " checked" : "") + '><span>I understand this permanently deletes my commercial data and revokes associated licenses.</span></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-confirm" data-account-service-action="data-delete-confirm">Delete My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-delete-resend" data-account-service-action="data-delete-resend">Send again</a></div></div><div class="service-status" id="data-delete-status"></div>';
|
||||
root.innerHTML = '<h4>Delete My Data</h4><div class="warning"><strong>Warning:</strong> deleting commercial data also revokes license records and cannot be undone.</div><div id="data-delete-step1"><div class="form-group"><label for="data-delete-email">Email address</label><input type="email" id="data-delete-email" value="' + escapeAttribute(flowState.emailValue || "") + '" autocomplete="email" data-account-service-input="data-delete-email"></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button></div></div><div id="data-delete-step2"' + (flowState.step2Visible ? "" : " hidden") + '><div class="form-group"><label for="data-delete-code">Verification code</label><input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || "") + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code"></div><div class="checkbox-row"><input type="checkbox" id="data-delete-confirm-check"' + (flowState.checkboxChecked ? " checked" : "") + '><span>I understand this permanently deletes my commercial data and revokes associated licenses.</span></div><div class="form-actions"><button class="btn-danger" type="button" id="data-delete-confirm" data-account-service-action="data-delete-confirm">Delete My Data</button></div><div class="helper-text">Need a new code? <a href="#" id="data-delete-resend" data-account-service-action="data-delete-resend">Send again</a></div></div><div class="service-status" id="data-delete-status"></div>';
|
||||
}
|
||||
|
||||
// src/services_controller.ts
|
||||
@@ -1378,9 +1374,9 @@
|
||||
return date.toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
|
||||
}
|
||||
function roleBadgeHTML(role) {
|
||||
if (role === "owner") return '<span class="badge" style="background:#f1f5f9;color:#64748b">Owner</span>';
|
||||
if (role === "admin") return '<span class="badge" style="background:#f1f5f9;color:#64748b">Admin</span>';
|
||||
if (role === "tech") return '<span class="badge" style="background:#f1f5f9;color:#64748b">Tech</span>';
|
||||
if (role === "owner") return '<span class="badge badge-role">Owner</span>';
|
||||
if (role === "admin") return '<span class="badge badge-role">Admin</span>';
|
||||
if (role === "tech") return '<span class="badge badge-role">Tech</span>';
|
||||
return "";
|
||||
}
|
||||
function renderWorkspaceCard(account, workspace, accountAPIBasePath) {
|
||||
@@ -1391,7 +1387,7 @@
|
||||
if (state === "active") {
|
||||
openAction = '<form method="POST" action="' + escapeAttr(accountAPIBasePath + "/" + account.id + "/tenants/" + workspace.id + "/handoff") + '"><button type="submit" class="btn-primary">Open \u2192</button></form>';
|
||||
} else {
|
||||
openAction = '<span style="font-size:13px;color:#94a3b8">' + safeState + "</span>";
|
||||
openAction = '<span class="workspace-state-label">' + safeState + "</span>";
|
||||
}
|
||||
var manageAction = "";
|
||||
if (account.can_manage && (state === "active" || state === "suspended" || state === "failed")) {
|
||||
@@ -1415,9 +1411,9 @@
|
||||
var addWorkspaceForm = "";
|
||||
if (account.can_manage) {
|
||||
actions = '<div class="account-actions">' + (account.kind === "msp" ? '<button type="button" class="btn-secondary" id="add-ws-btn-' + escapeAttr(account.id) + '" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">+ Add workspace</button>' : "") + (account.has_billing ? '<button type="button" class="btn-secondary" data-action="open-billing" data-account-id="' + escapeAttr(account.id) + '">Manage billing</button>' : "") + '<button type="button" class="btn-secondary" id="team-btn-' + escapeAttr(account.id) + '" data-action="toggle-team" data-account-id="' + escapeAttr(account.id) + '">Manage team</button></div>';
|
||||
teamSection = '<div class="team-section" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><h3>Team members</h3><table class="team-table"><thead><tr><th>Email</th><th>Role</th><th></th></tr></thead><tbody id="team-list-' + escapeAttr(account.id) + '"><tr><td colspan="3" style="color:#94a3b8;text-align:center;padding:16px">Loading\u2026</td></tr></tbody></table><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><button type="button" class="btn-primary" style="padding:8px 14px;font-size:13px" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div>';
|
||||
teamSection = '<div class="team-section" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><h3>Team members</h3><table class="team-table"><thead><tr><th>Email</th><th>Role</th><th></th></tr></thead><tbody id="team-list-' + escapeAttr(account.id) + '"><tr><td colspan="3" class="team-message-cell">Loading\u2026</td></tr></tbody></table><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div>';
|
||||
if (account.kind === "msp") {
|
||||
addWorkspaceForm = '<div class="add-workspace-form" id="add-ws-form-' + escapeAttr(account.id) + '"><label for="ws-name-' + escapeAttr(account.id) + '">Workspace name (e.g. client name)</label><input type="text" id="ws-name-' + escapeAttr(account.id) + '" placeholder="Acme Corp" maxlength="80" autocomplete="off"><div class="form-actions"><button type="button" class="btn-primary" data-action="create-workspace" data-account-id="' + escapeAttr(account.id) + '">Create workspace</button><button type="button" class="btn-secondary" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">Cancel</button><div class="spinner" id="ws-spinner-' + escapeAttr(account.id) + '"></div></div></div>';
|
||||
addWorkspaceForm = '<div class="add-workspace-form" id="add-ws-form-' + escapeAttr(account.id) + '"><label for="ws-name-' + escapeAttr(account.id) + '">Workspace name (e.g. client name)</label><input type="text" id="ws-name-' + escapeAttr(account.id) + '" placeholder="Acme Corp" maxlength="80" autocomplete="off"><div class="form-actions"><button type="button" class="btn-primary" data-action="create-workspace" data-account-id="' + escapeAttr(account.id) + '">Create workspace</button><button type="button" class="btn-secondary" data-action="toggle-add-workspace" data-account-id="' + escapeAttr(account.id) + '">Cancel</button><div class="spinner" id="ws-spinner-' + escapeAttr(account.id) + '" hidden></div></div></div>';
|
||||
}
|
||||
}
|
||||
return '<section class="account-section"><div class="account-header"><h2>' + escapeHTML(account.name) + '</h2><span class="badge badge-' + escapeHTML(account.kind) + '">' + escapeHTML(account.kind_label) + "</span>" + roleBadgeHTML(account.role) + "</div>" + workspaceHTML + actions + teamSection + addWorkspaceForm + "</section>";
|
||||
@@ -1426,12 +1422,12 @@
|
||||
if (context.bootstrap.authenticated) {
|
||||
return "<span>" + escapeHTML(context.bootstrap.email || "") + '</span><button class="logout-btn" id="logout-btn" type="button">Sign out</button>';
|
||||
}
|
||||
return '<a class="logout-btn" href="' + escapeAttr(context.signupPath) + '" style="text-decoration:none">Create account</a>';
|
||||
return '<a class="logout-btn link-button" href="' + escapeAttr(context.signupPath) + '">Create account</a>';
|
||||
}
|
||||
function renderAccountsHTML(context) {
|
||||
var safeAccounts = Array.isArray(context.bootstrap.accounts) ? context.bootstrap.accounts : [];
|
||||
if (safeAccounts.length === 0) {
|
||||
return '<div class="empty-state" style="margin-top:48px"><p>No workspaces found. If you just signed up, check your email for setup instructions.</p><p style="margin-top:12px;font-size:13px">Need help? Contact <a href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '" style="color:#1d4ed8">' + escapeHTML(context.bootstrap.support_email || "") + "</a></p></div>";
|
||||
return '<div class="empty-state empty-state-spaced"><p>No workspaces found. If you just signed up, check your email for setup instructions.</p><p class="support-copy">Need help? Contact <a href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '" class="support-link">' + escapeHTML(context.bootstrap.support_email || "") + "</a></p></div>";
|
||||
}
|
||||
return safeAccounts.map(function(account) {
|
||||
return renderAccountSection(account, context.accountAPIBasePath);
|
||||
@@ -1447,7 +1443,7 @@
|
||||
} else if (context.loginState.success) {
|
||||
statusHTML = `<div class="service-status visible success">Magic link sent. Check your inbox and click the link to sign in.<br><br><strong>Don't see it?</strong> <a href="#" data-portal-action="resend-magic-link">Send a new link</a>.</div>`;
|
||||
}
|
||||
return '<section class="intro-card"><h1>Pulse Account</h1><p>Sign in to manage Cloud workspaces, MSP access, and commercial account services from one account surface.</p></section><section class="service-section"><div class="service-panel visible"><h3>Sign in</h3><p>Enter the commercial email address for your Pulse account. I will send a magic link so you can open Pulse Account without managing a password.</p><div class="form-group"><label for="portal-login-email">Email address</label><input id="portal-login-email" type="email" autocomplete="email" placeholder="you@example.com" value="' + escapeAttr(context.loginState.emailValue || "") + '" data-portal-input="login-email"></div><div class="form-actions"><button class="btn-primary" id="portal-login-send" type="button" data-portal-action="send-magic-link">' + (context.loginState.request.pending ? "Sending\u2026" : "Send magic link") + '</button><a class="btn-secondary" href="' + escapeAttr(context.signupPath) + '" style="text-decoration:none">Create an account</a></div>' + statusHTML + "</div></section>";
|
||||
return '<section class="intro-card"><h1>Pulse Account</h1><p>Sign in to manage Cloud workspaces, MSP access, and commercial account services from one account surface.</p></section><section class="service-section"><div class="service-panel visible"><h3>Sign in</h3><p>Enter the commercial email address for your Pulse account. I will send a magic link so you can open Pulse Account without managing a password.</p><div class="form-group"><label for="portal-login-email">Email address</label><input id="portal-login-email" type="email" autocomplete="email" placeholder="you@example.com" value="' + escapeAttr(context.loginState.emailValue || "") + '" data-portal-input="login-email"></div><div class="form-actions"><button class="btn-primary" id="portal-login-send" type="button" data-portal-action="send-magic-link">' + (context.loginState.request.pending ? "Sending\u2026" : "Send magic link") + '</button><a class="btn-secondary link-button" href="' + escapeAttr(context.signupPath) + '">Create an account</a></div>' + statusHTML + "</div></section>";
|
||||
}
|
||||
|
||||
// src/shell.ts
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('account runtime', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="add-ws-form-acct_1" class="add-workspace-form">' +
|
||||
'<input id="ws-name-acct_1" value="Acme Corp">' +
|
||||
'<div id="ws-spinner-acct_1" style="display:none"></div>' +
|
||||
'<div id="ws-spinner-acct_1" hidden></div>' +
|
||||
'</div>';
|
||||
|
||||
runtime.toggleAddWorkspace('acct_1');
|
||||
@@ -103,7 +103,7 @@ describe('account runtime', function() {
|
||||
expect(deps.showToast).toHaveBeenCalledWith('Workspace created!');
|
||||
expect(deps.store.getAccountState().byAccountID.acct_1.addWorkspaceOpen).toBe(false);
|
||||
expect(deps.store.getAccountState().byAccountID.acct_1.createWorkspace.pending).toBe(false);
|
||||
expect((document.getElementById('ws-spinner-acct_1') as HTMLElement).style.display).toBe('none');
|
||||
expect((document.getElementById('ws-spinner-acct_1') as HTMLElement).hidden).toBe(true);
|
||||
});
|
||||
|
||||
it('loads and updates team membership from runtime actions', async function() {
|
||||
|
||||
@@ -28,17 +28,17 @@ describe('account view', function() {
|
||||
it('renders add-workspace visibility from account UI state', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="add-ws-form-acct_1" class="add-workspace-form"></div>' +
|
||||
'<div id="ws-spinner-acct_1" style="display:none"></div>';
|
||||
'<div id="ws-spinner-acct_1" hidden></div>';
|
||||
|
||||
renderAddWorkspaceSection('acct_1', createEntry({ addWorkspaceOpen: true }));
|
||||
expect(document.getElementById('add-ws-form-acct_1')?.classList.contains('visible')).toBe(true);
|
||||
expect((document.getElementById('ws-spinner-acct_1') as HTMLElement).style.display).toBe('none');
|
||||
expect((document.getElementById('ws-spinner-acct_1') as HTMLElement).hidden).toBe(true);
|
||||
|
||||
renderAddWorkspaceSection('acct_1', createEntry({
|
||||
addWorkspaceOpen: true,
|
||||
createWorkspace: { pending: true, error: '' },
|
||||
}));
|
||||
expect((document.getElementById('ws-spinner-acct_1') as HTMLElement).style.display).toBe('block');
|
||||
expect((document.getElementById('ws-spinner-acct_1') as HTMLElement).hidden).toBe(false);
|
||||
|
||||
renderAddWorkspaceSection('acct_1', createEntry({ addWorkspaceOpen: false }));
|
||||
expect(document.getElementById('add-ws-form-acct_1')?.classList.contains('visible')).toBe(false);
|
||||
|
||||
@@ -20,7 +20,7 @@ function setTbodyMessage(tbody: HTMLElement, msg: string, isError: boolean): voi
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.setAttribute('colspan', '3');
|
||||
td.style.cssText = 'text-align:center;padding:16px;color:' + (isError ? '#991b1b' : '#94a3b8');
|
||||
td.className = 'team-message-cell' + (isError ? ' error' : '');
|
||||
td.textContent = msg;
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
@@ -73,7 +73,7 @@ export function renderAddWorkspaceSection(accountID: string, entry: PortalAccoun
|
||||
if (!form) return;
|
||||
form.classList.toggle('visible', entry.addWorkspaceOpen);
|
||||
if (spinner) {
|
||||
spinner.style.display = entry.createWorkspace.pending ? 'block' : 'none';
|
||||
spinner.hidden = !entry.createWorkspace.pending;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -295,8 +295,8 @@ describe('portal app', function() {
|
||||
}),
|
||||
]);
|
||||
expect((store.getServiceState().flows.retrieve.result as { token?: string } | null)?.token).toBe('pulse_token_123');
|
||||
expect((document.getElementById('retrieve-inline-result') as HTMLElement | null)?.style.display).toBe('block');
|
||||
expect((document.getElementById('retrieve-inline-copy') as HTMLButtonElement | null)?.style.display).toBe('inline-block');
|
||||
expect((document.getElementById('retrieve-inline-result') as HTMLElement | null)?.hidden).toBe(false);
|
||||
expect((document.getElementById('retrieve-inline-copy') as HTMLButtonElement | null)?.hidden).toBe(false);
|
||||
expect(document.getElementById('retrieve-inline-status')?.textContent).toContain('License retrieved successfully.');
|
||||
});
|
||||
|
||||
|
||||
@@ -115,12 +115,12 @@ describe('services view', function() {
|
||||
var invoiceLink = document.getElementById('retrieve-inline-invoice') as HTMLAnchorElement;
|
||||
var tokenArea = document.getElementById('retrieve-inline-token') as HTMLTextAreaElement;
|
||||
|
||||
expect(copyButton.style.display).toBe('inline-block');
|
||||
expect(copyButton.hidden).toBe(false);
|
||||
expect(invoiceLink.href).toBe('https://license.pulserelay.pro/invoices/inv_123');
|
||||
expect(invoiceLink.style.display).toBe('inline-block');
|
||||
expect(invoiceLink.hidden).toBe(false);
|
||||
expect(tokenArea.value).toBe('pulse_key_123');
|
||||
expect(document.getElementById('retrieve-inline-email-value')?.textContent).toBe('buyer@example.com');
|
||||
expect(document.getElementById('retrieve-inline-result')?.style.display).toBe('block');
|
||||
expect((document.getElementById('retrieve-inline-result') as HTMLElement).hidden).toBe(false);
|
||||
});
|
||||
|
||||
it('renders refund and delete panels from owned state', function() {
|
||||
@@ -152,13 +152,13 @@ describe('services view', function() {
|
||||
expect(refundToken.value).toBe('pulse_token');
|
||||
expect(document.getElementById('refund-service-root')?.innerHTML).toContain('/refund.html?email=owner%40example.com');
|
||||
expect(deleteCheck.checked).toBe(true);
|
||||
expect(document.getElementById('data-delete-step2')?.style.display).toBe('block');
|
||||
expect((document.getElementById('data-delete-step2') as HTMLElement).hidden).toBe(false);
|
||||
});
|
||||
|
||||
it('renders export panel and updates export result payload visibility', function() {
|
||||
document.body.innerHTML =
|
||||
'<div id="data-export-root"></div>' +
|
||||
'<div id="data-export-result" style="display:none"></div>' +
|
||||
'<div id="data-export-result" hidden></div>' +
|
||||
'<textarea id="data-export-payload"></textarea>';
|
||||
|
||||
renderExportPanel(
|
||||
@@ -175,8 +175,8 @@ describe('services view', function() {
|
||||
renderExportResult(payload);
|
||||
|
||||
expect((document.getElementById('data-export-email') as HTMLInputElement).value).toBe('owner@example.com');
|
||||
expect(document.getElementById('data-export-step2')?.style.display).toBe('block');
|
||||
expect(document.getElementById('data-export-result')?.style.display).toBe('block');
|
||||
expect((document.getElementById('data-export-step2') as HTMLElement).hidden).toBe(false);
|
||||
expect((document.getElementById('data-export-result') as HTMLElement).hidden).toBe(false);
|
||||
expect((document.getElementById('data-export-payload') as HTMLTextAreaElement).value).toBe(
|
||||
JSON.stringify(payload, null, 2)
|
||||
);
|
||||
@@ -202,7 +202,7 @@ describe('services view', function() {
|
||||
|
||||
expect((document.getElementById('manage-inline-email') as HTMLInputElement).value).toBe('owner@example.com');
|
||||
expect((document.getElementById('manage-inline-code') as HTMLInputElement).value).toBe('123456');
|
||||
expect(document.getElementById('manage-inline-step2')?.style.display).toBe('block');
|
||||
expect((document.getElementById('manage-inline-step2') as HTMLElement).hidden).toBe(false);
|
||||
expect(document.getElementById('manage-inline-status')?.className).toContain('success');
|
||||
expect(document.getElementById('manage-inline-status')?.textContent).toBe('Code sent.');
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ export function focusElement(id: string): void {
|
||||
export function setVisible(id: string, visible: boolean): void {
|
||||
var el = getElement(id);
|
||||
if (el) {
|
||||
el.style.display = visible ? 'block' : 'none';
|
||||
el.hidden = !visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export function renderManagePanel(flowState: VerificationFlowState): void {
|
||||
'<button class="btn-primary" type="button" id="manage-inline-request" data-account-service-action="manage-inline-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="manage-inline-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div id="manage-inline-step2"' + (flowState.step2Visible ? '' : ' hidden') + '>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="manage-inline-code">Verification code</label>' +
|
||||
'<input type="text" id="manage-inline-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="manage-code">' +
|
||||
@@ -139,9 +139,6 @@ export function renderRetrievePanel(flowState: VerificationFlowState): void {
|
||||
email?: string;
|
||||
} | null;
|
||||
var invoiceURL = result && result.invoice_url ? result.invoice_url : '#';
|
||||
var invoiceDisplay = result && result.invoice_url ? 'inline-block' : 'none';
|
||||
var copyDisplay = result ? 'inline-block' : 'none';
|
||||
var resultDisplay = result ? 'block' : 'none';
|
||||
root.innerHTML = '' +
|
||||
'<h3>Retrieve licenses</h3>' +
|
||||
'<p>Request a verification code for the commercial email, then reveal the current active self-hosted license without leaving Pulse Account.</p>' +
|
||||
@@ -154,20 +151,20 @@ export function renderRetrievePanel(flowState: VerificationFlowState): void {
|
||||
'<button class="btn-primary" type="button" id="retrieve-inline-request" data-account-service-action="retrieve-inline-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="retrieve-inline-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div id="retrieve-inline-step2"' + (flowState.step2Visible ? '' : ' hidden') + '>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="retrieve-inline-code">Verification code</label>' +
|
||||
'<input type="text" id="retrieve-inline-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="retrieve-code">' +
|
||||
'</div>' +
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn-primary" type="button" id="retrieve-inline-confirm" data-account-service-action="retrieve-inline-confirm">Show License</button>' +
|
||||
'<button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy" style="display:' + copyDisplay + '">Copy License Key</button>' +
|
||||
'<a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener" style="display:' + invoiceDisplay + '">View Invoice</a>' +
|
||||
'<button class="btn-secondary" type="button" id="retrieve-inline-copy" data-account-service-action="retrieve-inline-copy"' + (result ? '' : ' hidden') + '>Copy License Key</button>' +
|
||||
'<a class="btn-secondary" id="retrieve-inline-invoice" href="' + escapeAttribute(invoiceURL) + '" target="_blank" rel="noopener"' + (result && result.invoice_url ? '' : ' hidden') + '>View Invoice</a>' +
|
||||
'</div>' +
|
||||
'<div class="helper-text">Use the latest active self-hosted license for this commercial email.</div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="retrieve-inline-status"></div>' +
|
||||
'<div id="retrieve-inline-result" style="display:' + resultDisplay + '; margin-top:14px">' +
|
||||
'<div id="retrieve-inline-result" class="service-result"' + (result ? '' : ' hidden') + '>' +
|
||||
'<label for="retrieve-inline-token">License key</label>' +
|
||||
'<textarea id="retrieve-inline-token" readonly>' + escapeText(result ? result.token : '') + '</textarea>' +
|
||||
'<div class="result-grid">' +
|
||||
@@ -182,7 +179,6 @@ export function renderRetrievePanel(flowState: VerificationFlowState): void {
|
||||
export function renderExportPanel(flowState: VerificationFlowState): void {
|
||||
var root = getElement('data-export-root');
|
||||
if (!root) return;
|
||||
var resultDisplay = flowState.result ? 'block' : 'none';
|
||||
root.innerHTML = '' +
|
||||
'<h4>Export My Data</h4>' +
|
||||
'<div id="data-export-step1">' +
|
||||
@@ -194,7 +190,7 @@ export function renderExportPanel(flowState: VerificationFlowState): void {
|
||||
'<button class="btn-primary" type="button" id="data-export-request" data-account-service-action="data-export-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="data-export-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div id="data-export-step2"' + (flowState.step2Visible ? '' : ' hidden') + '>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-export-code">Verification code</label>' +
|
||||
'<input type="text" id="data-export-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-export-code">' +
|
||||
@@ -205,7 +201,7 @@ export function renderExportPanel(flowState: VerificationFlowState): void {
|
||||
'<div class="helper-text">Need a new code? <a href="#" id="data-export-resend" data-account-service-action="data-export-resend">Send again</a></div>' +
|
||||
'</div>' +
|
||||
'<div class="service-status" id="data-export-status"></div>' +
|
||||
'<div id="data-export-result" style="display:' + resultDisplay + '; margin-top:14px">' +
|
||||
'<div id="data-export-result" class="service-result"' + (flowState.result ? '' : ' hidden') + '>' +
|
||||
'<label for="data-export-payload">Export payload</label>' +
|
||||
'<textarea id="data-export-payload" readonly>' + escapeText(flowState.result ? JSON.stringify(flowState.result, null, 2) : '') + '</textarea>' +
|
||||
'</div>';
|
||||
@@ -231,7 +227,7 @@ export function renderDeletePanel(flowState: VerificationFlowState): void {
|
||||
'<button class="btn-danger" type="button" id="data-delete-request" data-account-service-action="data-delete-request">Send Verification Code</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="data-delete-step2" style="display:' + (flowState.step2Visible ? 'block' : 'none') + '">' +
|
||||
'<div id="data-delete-step2"' + (flowState.step2Visible ? '' : ' hidden') + '>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="data-delete-code">Verification code</label>' +
|
||||
'<input type="text" id="data-delete-code" value="' + escapeAttribute(flowState.codeValue || '') + '" inputmode="numeric" pattern="[0-9]{6}" placeholder="123456" data-account-service-input="data-delete-code">' +
|
||||
|
||||
@@ -33,9 +33,9 @@ function formatWorkspaceDate(value: unknown): string {
|
||||
}
|
||||
|
||||
function roleBadgeHTML(role: string): string {
|
||||
if (role === 'owner') return '<span class="badge" style="background:#f1f5f9;color:#64748b">Owner</span>';
|
||||
if (role === 'admin') return '<span class="badge" style="background:#f1f5f9;color:#64748b">Admin</span>';
|
||||
if (role === 'tech') return '<span class="badge" style="background:#f1f5f9;color:#64748b">Tech</span>';
|
||||
if (role === 'owner') return '<span class="badge badge-role">Owner</span>';
|
||||
if (role === 'admin') return '<span class="badge badge-role">Admin</span>';
|
||||
if (role === 'tech') return '<span class="badge badge-role">Tech</span>';
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ function renderWorkspaceCard(account: PortalAccountSummary, workspace: PortalWor
|
||||
'<button type="submit" class="btn-primary">Open →</button>' +
|
||||
'</form>';
|
||||
} else {
|
||||
openAction = '<span style="font-size:13px;color:#94a3b8">' + safeState + '</span>';
|
||||
openAction = '<span class="workspace-state-label">' + safeState + '</span>';
|
||||
}
|
||||
|
||||
var manageAction = '';
|
||||
@@ -141,7 +141,7 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
|
||||
'<tbody id="team-list-' +
|
||||
escapeAttr(account.id) +
|
||||
'">' +
|
||||
'<tr><td colspan="3" style="color:#94a3b8;text-align:center;padding:16px">Loading…</td></tr>' +
|
||||
'<tr><td colspan="3" class="team-message-cell">Loading…</td></tr>' +
|
||||
'</tbody>' +
|
||||
'</table>' +
|
||||
'<div class="team-invite">' +
|
||||
@@ -155,7 +155,7 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
|
||||
'">Role</label><select id="invite-role-' +
|
||||
escapeAttr(account.id) +
|
||||
'"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div>' +
|
||||
'<button type="button" class="btn-primary" style="padding:8px 14px;font-size:13px" data-action="invite-member" data-account-id="' +
|
||||
'<button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' +
|
||||
escapeAttr(account.id) +
|
||||
'">Invite</button>' +
|
||||
'</div>' +
|
||||
@@ -181,7 +181,7 @@ function renderAccountSection(account: PortalAccountSummary, accountAPIBasePath:
|
||||
'">Cancel</button>' +
|
||||
'<div class="spinner" id="ws-spinner-' +
|
||||
escapeAttr(account.id) +
|
||||
'"></div>' +
|
||||
'" hidden></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
@@ -209,18 +209,18 @@ export function renderHeaderHTML(context: ShellViewContext): string {
|
||||
'<button class="logout-btn" id="logout-btn" type="button">Sign out</button>'
|
||||
);
|
||||
}
|
||||
return '<a class="logout-btn" href="' + escapeAttr(context.signupPath) + '" style="text-decoration:none">Create account</a>';
|
||||
return '<a class="logout-btn link-button" href="' + escapeAttr(context.signupPath) + '">Create account</a>';
|
||||
}
|
||||
|
||||
export function renderAccountsHTML(context: ShellViewContext): string {
|
||||
var safeAccounts = Array.isArray(context.bootstrap.accounts) ? context.bootstrap.accounts : [];
|
||||
if (safeAccounts.length === 0) {
|
||||
return (
|
||||
'<div class="empty-state" style="margin-top:48px">' +
|
||||
'<div class="empty-state empty-state-spaced">' +
|
||||
'<p>No workspaces found. If you just signed up, check your email for setup instructions.</p>' +
|
||||
'<p style="margin-top:12px;font-size:13px">Need help? Contact <a href="mailto:' +
|
||||
'<p class="support-copy">Need help? Contact <a href="mailto:' +
|
||||
escapeAttr(context.bootstrap.support_email || '') +
|
||||
'" style="color:#1d4ed8">' +
|
||||
'" class="support-link">' +
|
||||
escapeHTML(context.bootstrap.support_email || '') +
|
||||
'</a></p>' +
|
||||
'</div>'
|
||||
@@ -309,7 +309,7 @@ export function renderSignedOutPortalHTML(context: ShellViewContext): string {
|
||||
'<button class="btn-primary" id="portal-login-send" type="button" data-portal-action="send-magic-link">' +
|
||||
(context.loginState.request.pending ? 'Sending…' : 'Send magic link') +
|
||||
'</button>' +
|
||||
'<a class="btn-secondary" href="' + escapeAttr(context.signupPath) + '" style="text-decoration:none">Create an account</a>' +
|
||||
'<a class="btn-secondary link-button" href="' + escapeAttr(context.signupPath) + '">Create an account</a>' +
|
||||
'</div>' +
|
||||
statusHTML +
|
||||
'</div>' +
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
.btn-secondary:hover { background: #f8fafc; border-color: #94a3b8; }
|
||||
.btn-danger { background: #fff; color: #dc2626; border: 1px solid #fca5a5; border-radius: 8px; padding: 7px 14px; font-size: 13px; cursor: pointer; }
|
||||
.btn-danger:hover { background: #fef2f2; }
|
||||
.btn-compact { padding: 8px 14px; font-size: 13px; }
|
||||
.link-button { text-decoration: none; }
|
||||
.account-actions { margin-top: 14px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.add-workspace-form { margin-top: 12px; display: none; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 16px; }
|
||||
.add-workspace-form.visible { display: block; }
|
||||
@@ -48,6 +50,8 @@
|
||||
.team-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
.team-table th { text-align: left; font-size: 12px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 8px; border-bottom: 1px solid #e2e8f0; }
|
||||
.team-table td { padding: 8px; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
|
||||
.team-message-cell { text-align: center; padding: 16px; color: #94a3b8; }
|
||||
.team-message-cell.error { color: #991b1b; }
|
||||
.team-table select { border: 1px solid #cbd5e1; border-radius: 6px; padding: 4px 8px; font-size: 13px; background: #fff; }
|
||||
.team-table .btn-remove { background: none; border: none; color: #dc2626; cursor: pointer; font-size: 13px; padding: 4px 8px; }
|
||||
.team-table .btn-remove:hover { text-decoration: underline; }
|
||||
@@ -83,6 +87,7 @@
|
||||
.service-panel .result-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-top: 14px; }
|
||||
.service-panel .result-meta-label { font-size: 12px; color: #64748b; margin-bottom: 4px; }
|
||||
.service-panel .result-meta-value { font-size: 14px; color: #0f172a; word-break: break-word; }
|
||||
.service-result { margin-top: 14px; }
|
||||
.service-panel .subsection { margin-top: 18px; padding-top: 18px; border-top: 1px solid #e2e8f0; }
|
||||
.service-panel .subsection:first-of-type { margin-top: 0; padding-top: 0; border-top: none; }
|
||||
.service-panel .subsection h4 { margin: 0 0 8px; font-size: 15px; font-weight: 700; }
|
||||
@@ -96,7 +101,13 @@
|
||||
.service-status.success { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
|
||||
.empty-state { background: #fff; border: 1px dashed #cbd5e1; border-radius: 10px; padding: 32px; text-align: center; color: #64748b; }
|
||||
.empty-state p { margin: 0; font-size: 15px; }
|
||||
.spinner { display: none; width: 18px; height: 18px; border: 2px solid #93c5fd; border-top-color: #1d4ed8; border-radius: 50%; animation: spin 0.6s linear infinite; }
|
||||
.empty-state-spaced { margin-top: 48px; }
|
||||
.support-copy { margin-top: 12px; font-size: 13px; }
|
||||
.support-link { color: #1d4ed8; }
|
||||
.badge-role { background: #f1f5f9; color: #64748b; }
|
||||
.workspace-state-label { font-size: 13px; color: #94a3b8; }
|
||||
.spinner { display: inline-block; width: 18px; height: 18px; border: 2px solid #93c5fd; border-top-color: #1d4ed8; border-radius: 50%; animation: spin 0.6s linear infinite; }
|
||||
[hidden] { display: none !important; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.toast { position: fixed; bottom: 24px; right: 24px; background: #1e293b; color: #f8fafc; border-radius: 8px; padding: 12px 20px; font-size: 14px; display: none; z-index: 100; }
|
||||
.toast.visible { display: block; animation: fadein 0.2s; }
|
||||
|
||||
Reference in New Issue
Block a user