Skip to content

Commit 1099347

Browse files
ndemiancclaude
andcommitted
feat(ai): floating-chevron hero + docs link on the account modal
Elevates the signed-in Manage-account popover from clean-but-plain to the landing page's signature moment. - Hero: the levelcode.ai "portal" mark — three colored chevrons rising over four soft circles that drift (portalDrift 7s, staggered -1.4s per layer), replacing the plain SD initials. Decorative + aria-hidden; honors prefers-reduced-motion. - The modal is now a proper premium dialog: full-bleed hero, 14px radius, soft shadow, entrance pop. - Documentation link -> https://levelcode.ai/ai/docs, in a tidy footer beside the Synced line. Routed through the SAME fixed allow-list as Terms/Privacy (target 'docs' -> /ai/docs); never a webview-supplied URL, so the open-redirect guard holds. Still model-agnostic; light/One Dark keep the brand palette; high-contrast still defers to editor tokens (the hero is decorative). extension.js: openLegal() gains the 'docs' target. Verified: both files parse; full gate 24 suites; structure present. Matches the approved mockup; a live in-editor render needs the VS Code host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 502cd75 commit 1099347

2 files changed

Lines changed: 59 additions & 23 deletions

File tree

extensions/levelcode-ai/extension.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,10 +1161,10 @@ async function openUpgrade() {
11611161
await vscode.env.openExternal(vscode.Uri.parse(base + '/ai/pricing'));
11621162
}
11631163

1164-
/** Open a LevelCode legal page (Terms / Privacy) from the sign-in modal. Targets are a fixed
1165-
* allow-list — never a URL from the webview — so this can't be turned into an open-redirect. */
1164+
/** Open a fixed LevelCode site page (Terms / Privacy / Docs). Targets are a fixed allow-list — never a
1165+
* URL from the webview — so this can't be turned into an open-redirect. */
11661166
async function openLegal(target) {
1167-
const path = target === 'privacy' ? '/privacy' : target === 'terms' ? '/terms' : null;
1167+
const path = target === 'privacy' ? '/privacy' : target === 'terms' ? '/terms' : target === 'docs' ? '/ai/docs' : null;
11681168
if (!path) { return; }
11691169
await vscode.env.openExternal(vscode.Uri.parse('https://levelcode.ai' + path));
11701170
}

extensions/levelcode-ai/media/chat.html

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -849,9 +849,9 @@
849849
--cc-line: #3e4451; --cc-accent: #7d6bff;
850850
--cc-mono: var(--vscode-editor-font-family, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
851851
font-variant-numeric: tabular-nums; /* align digits across the card (email, any future counters) */
852-
max-width: 300px; padding: 24px 22px 20px; border-radius: 6px;
852+
max-width: 304px; padding: 0; border-radius: 14px; overflow: hidden;
853853
background: var(--cc-surface); color: var(--cc-text); border: 1px solid var(--cc-line);
854-
box-shadow: 0 1px 2px rgba(0,0,0,.05);
854+
box-shadow: 0 18px 50px rgba(0,0,0,.34); animation: lcPop .4s cubic-bezier(.2,.9,.25,1.2) both;
855855
}
856856
body.vscode-light .acctcard.classic {
857857
--cc-bg: #ffffff; --cc-surface: #fafaf9; --cc-text: #333333; --cc-text2: #555555; --cc-text3: #777777;
@@ -876,12 +876,18 @@
876876
}
877877
.acctcard.classic .ovclose { color: var(--cc-text3); }
878878
.acctcard.classic .ovclose:hover { color: var(--cc-text); background: color-mix(in srgb, var(--cc-text3) 18%, transparent); }
879-
.acctcard.classic .bigavatar {
880-
width: 56px; height: 56px; border-radius: 50%; margin: 2px auto 14px; display: grid; place-items: center;
881-
border: 1px solid color-mix(in srgb, var(--cc-accent) 42%, var(--cc-line));
882-
background: color-mix(in srgb, var(--cc-accent) 13%, transparent); color: var(--cc-accent);
883-
font-weight: 600; font-size: 18px; letter-spacing: .02em;
884-
}
879+
/* Hero — the landing page's floating chevron over four drifting "portal" circles. Decorative (aria-hidden),
880+
full-bleed at the top of the modal; the motion respects prefers-reduced-motion (guard below). */
881+
.acctcard.classic .hero { position: relative; height: 150px; display: grid; place-items: center; overflow: hidden; }
882+
.acctcard.classic .hero .portalmark { width: 212px; height: auto; display: block; }
883+
.acctcard.classic .portal-layer { transform-origin: 50% 60%; animation: portalDrift 7s ease-in-out infinite; }
884+
.acctcard.classic .portal-layer:nth-child(2) { animation-delay: -1.4s; }
885+
.acctcard.classic .portal-layer:nth-child(3) { animation-delay: -2.8s; }
886+
.acctcard.classic .portal-layer:nth-child(4) { animation-delay: -4.2s; }
887+
.acctcard.classic .portal-layer:nth-child(5) { animation-delay: -5.6s; }
888+
@keyframes portalDrift { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-6px) scale(1.02); } }
889+
/* Everything under the hero carries the padding, since the hero is full-bleed. */
890+
.acctcard.classic .acctmain { padding: 2px 24px 22px; }
885891
.acctcard.classic h2 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -.01em; color: var(--cc-text); }
886892
.acctcard.classic .em { margin: 5px 0 0; font-family: var(--cc-mono); font-size: 12.5px; color: var(--cc-text3); }
887893
.acctcard.classic .planpill {
@@ -902,14 +908,27 @@
902908
}
903909
.acctcard.classic .signout:hover { text-decoration: underline; }
904910
.acctcard.classic .signout:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }
905-
.acctcard.classic .synced {
911+
/* Footer: the Documentation link + the muted "Synced" line, under one divider. */
912+
.acctcard.classic .acctfoot {
906913
margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--cc-line);
907-
font-family: var(--cc-mono); font-size: 11.5px; color: var(--cc-text3); line-height: 1.5;
914+
display: flex; flex-direction: column; align-items: center; gap: 9px;
915+
}
916+
.acctcard.classic .docs {
917+
display: inline-flex; align-items: center; gap: 6px; padding: 1px 4px; border-radius: 4px;
918+
color: var(--cc-accent); font-size: 13px; font-weight: 500; text-decoration: none; cursor: pointer;
908919
}
920+
.acctcard.classic .docs:hover { text-decoration: underline; }
921+
.acctcard.classic .docs:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }
922+
.acctcard.classic .docs .darrow { font-size: 11px; opacity: .85; }
923+
.acctcard.classic .synced { font-family: var(--cc-mono); font-size: 11px; color: var(--cc-text3); line-height: 1.5; }
909924
.acctcard.classic .note {
910925
margin-top: 12px; font-family: var(--cc-mono); font-size: 11.5px; color: var(--cc-text3);
911926
line-height: 1.55; text-align: left;
912927
}
928+
@media (prefers-reduced-motion: reduce) {
929+
.acctcard.classic { animation: none; }
930+
.acctcard.classic .portal-layer { animation: none; }
931+
}
913932

914933
/* ── LevelCode sign-in modal — calm, soft, THEME-FOLLOWING (matches the Manage-account card) ────
915934
Scoped to .acctcard.lc-modal. Uses the editor theme tokens (--vscode-*, --border, --muted, --field-bg)
@@ -2740,6 +2759,20 @@
27402759
'<svg viewBox="0 0 120 120" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
27412760
+ '<path d="M29,92 L60,70 L91,92" opacity=".5"/><path d="M29,71 L60,49 L91,71" opacity=".78"/>'
27422761
+ '<path d="M29,50 L60,28 L91,50"/></svg>';
2762+
// The full COLORED "portal" mark from the levelcode.ai landing hero: three chevrons over four soft
2763+
// drifting circles (the .portal-layer drift lives in CSS). Decorative; aria-hidden. First circle takes
2764+
// --cc-accent (theme-aware); the rest are the fixed brand gradient.
2765+
const LC_PORTAL_SVG =
2766+
'<svg class="portalmark" viewBox="0 0 300 280" aria-hidden="true">'
2767+
+ '<g class="portal-layer"><circle cx="118" cy="128" r="104" fill="var(--cc-accent)" opacity=".14"/></g>'
2768+
+ '<g class="portal-layer"><circle cx="186" cy="104" r="86" fill="#7d6bff" opacity=".16"/></g>'
2769+
+ '<g class="portal-layer"><circle cx="120" cy="180" r="72" fill="#5fb4ff" opacity=".18"/></g>'
2770+
+ '<g class="portal-layer"><circle cx="204" cy="182" r="58" fill="#a8ecff" opacity=".28"/></g>'
2771+
+ '<g class="portal-layer" fill="none" stroke-linecap="round" stroke-linejoin="round">'
2772+
+ '<path d="M96 196 L152 152 L208 196" stroke="#5b3fd6" stroke-width="21" opacity=".55"/>'
2773+
+ '<path d="M96 152 L152 108 L208 152" stroke="#7d6bff" stroke-width="21" opacity=".8"/>'
2774+
+ '<path d="M96 108 L152 64 L208 108" stroke="#5fb4ff" stroke-width="21"/>'
2775+
+ '</g></svg>';
27432776
const LC_GH_SVG =
27442777
'<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.42 7.42 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/></svg>';
27452778
const LC_GOOGLE_SVG =
@@ -2782,16 +2815,19 @@
27822815
// Gateway needs no explainer (the "how the gateway works" blurb was removed). BYOK keeps a one-line
27832816
// privacy reassurance, which is genuinely useful there.
27842817
const note = gateway ? '' : '<div class="note">Your keys never leave your machine — requests go direct to your provider.</div>';
2818+
const docsLink = '<a class="docs" data-act="docs" role="link" tabindex="0">Documentation <span class="darrow" aria-hidden="true">↗</span></a>';
27852819
acctBody.innerHTML =
2786-
'<div class="bigavatar">' + esc(initials(account.name || account.email)) + '</div>'
2787-
+ '<h2 id="lcTitle">' + esc(account.name || 'Signed in') + '</h2>'
2788-
+ emailLine
2789-
+ planPill
2790-
+ upgradeBtn
2791-
+ '<button class="abtn" data-act="manage">Manage account</button>'
2792-
+ '<button class="signout" data-act="signout">Sign out</button>'
2793-
+ '<div class="synced">Synced · settings · skills · keymaps</div>'
2794-
+ note;
2820+
'<div class="hero">' + LC_PORTAL_SVG + '</div>'
2821+
+ '<div class="acctmain">'
2822+
+ '<h2 id="lcTitle">' + esc(account.name || 'Signed in') + '</h2>'
2823+
+ emailLine
2824+
+ planPill
2825+
+ upgradeBtn
2826+
+ '<button class="abtn" data-act="manage">Manage account</button>'
2827+
+ '<button class="signout" data-act="signout">Sign out</button>'
2828+
+ '<div class="acctfoot">' + docsLink + '<div class="synced">Synced · settings · skills · keymaps</div></div>'
2829+
+ note
2830+
+ '</div>';
27952831
} else {
27962832
acctCard.classList.remove('classic');
27972833
acctCard.classList.add('lc-modal'); // signed-out → the calm LevelCode sign-in modal
@@ -2866,7 +2902,7 @@
28662902
else if (act === 'manage') { vscode.postMessage({ type: 'accountManage' }); }
28672903
else if (act === 'github' || act === 'google') { vscode.postMessage({ type: 'accountSignIn', provider: act }); }
28682904
else if (act === 'create') { vscode.postMessage({ type: 'accountSignIn', create: true }); }
2869-
else if (act === 'terms' || act === 'privacy') { vscode.postMessage({ type: 'openExternal', target: act }); }
2905+
else if (act === 'terms' || act === 'privacy' || act === 'docs') { vscode.postMessage({ type: 'openExternal', target: act }); }
28702906
else { vscode.postMessage({ type: 'accountSignIn' }); }
28712907
}
28722908
acctBody.addEventListener('click', (e) => {

0 commit comments

Comments
 (0)