feat(home): Getting Started moves first, collapsible, in-page anchor (#296)

Three tweaks to the post-PR-#291 Getting Started card:

1. Chronologically first. Moved from below the install-hero (where
   it sat as a static white card) to ABOVE it, inside the same
   `{% if not onboarded %}` guard. The blue hero is now the actual
   install flow that the card points at, not a peer that competes
   for attention.

2. Collapsed by default. Switched from <section> to <details> with
   no `open` attribute, so the page lands with just a quiet pill
   (`Getting Started — Two quick next steps — click to expand ›`).
   Expand to reveal the two rows. Chevron rotates 90deg when open
   via the `[open]` selector. Per-device dismiss X stays — generic
   `.home-card-close[data-dismiss-key]` handler now uses
   `closest('section, details')` so it works on both container types.

3. First row → #install-hero in-page anchor. Was `/setup` (which
   would round-trip to the same hero via a redirect through /setup).
   Anchored directly to the blue hero on the same page; copy reads
   "One-time install — walkthrough in the section below" so the
   user knows it's a scroll-to, not a navigation. Install-hero <div>
   gained `id="install-hero"`. `.install-hero { scroll-margin-top:
   88px }` keeps the hero's eyebrow clear of the 72px sticky header
   on the jump.

Second row link to /setup-advanced and the dismiss key unchanged.
GS disappears alongside the install-hero when the user is onboarded,
so the in-page anchor never dangles. Tests updated to assert the new
markup + onboarded-state hiding.
This commit is contained in:
Vojtech 2026-05-14 11:02:23 +04:00 committed by GitHub
parent 4501c9c3dd
commit 3d244038b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 129 additions and 52 deletions

View file

@ -120,7 +120,53 @@
margin-bottom: 18px; margin-bottom: 18px;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04); box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
} }
.home-mock .home-getting-started > header h2, /* Getting Started uses <details> for native collapsed-by-default
behaviour. The summary owns the padding when collapsed; row layout
reveals on expand. Chevron rotates 90deg when open. */
.home-mock details.home-getting-started {
padding: 0;
}
.home-mock .home-gs-summary {
list-style: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
user-select: none;
}
.home-mock .home-gs-summary::-webkit-details-marker { display: none; }
.home-mock .home-gs-summary-title {
font-size: 16px;
font-weight: 600;
color: var(--hp-text-primary);
}
.home-mock .home-gs-summary-hint {
flex: 1;
font-size: 13px;
color: var(--hp-text-secondary);
}
.home-mock .home-gs-summary-chev {
font-size: 18px;
color: var(--hp-text-muted);
transition: transform 0.15s;
}
.home-mock details.home-getting-started[open] .home-gs-summary-chev {
transform: rotate(90deg);
}
.home-mock details.home-getting-started[open] {
padding-bottom: 18px;
}
.home-mock details.home-getting-started[open] .home-gs-item {
margin-left: 24px;
margin-right: 24px;
}
/* Install-hero is the scroll target for Getting Started's first row.
Offset the anchor jump by the 72px sticky .app-header height + a
bit of breathing room so the hero's eyebrow lands cleanly under
the header bar. */
.home-mock .install-hero { scroll-margin-top: 88px; }
.home-mock .home-overview > h2, .home-mock .home-overview > h2,
.home-mock .home-usage > header h2 { .home-mock .home-usage > header h2 {
font-size: 18px; font-size: 18px;
@ -128,7 +174,6 @@
margin: 0 0 6px; margin: 0 0 6px;
color: var(--hp-text-primary); color: var(--hp-text-primary);
} }
.home-mock .home-getting-started > header p,
.home-mock .home-usage > header p { .home-mock .home-usage > header p {
font-size: 13px; font-size: 13px;
color: var(--hp-text-secondary); color: var(--hp-text-secondary);
@ -1355,7 +1400,42 @@
etc.) stays. Offboarding escape hatch moved to a discrete strip etc.) stays. Offboarding escape hatch moved to a discrete strip
below; see `.offboard-strip`. #} below; see `.offboard-strip`. #}
{% if not onboarded %} {% if not onboarded %}
<div class="install-hero"> {# Getting Started renders FIRST in the not-onboarded flow as a
collapsed-by-default <details>. Click the summary to expand the
two-row map. First row anchors back to the install hero just
below (#install-hero); second row leaves the page for
/setup-advanced. Per-device dismiss X (data-dismiss-key) survives
on the generic .home-card-close handler — selector widened in JS
to accept <details> containers too. Disappears post-onboarding
alongside the hero so the in-page anchor never dangles. #}
<details class="home-getting-started" id="homeGettingStarted">
<summary class="home-gs-summary">
<span class="home-gs-summary-title">Getting Started</span>
<span class="home-gs-summary-hint">Two quick next steps — click to expand</span>
<span class="home-gs-summary-chev" aria-hidden="true">&rsaquo;</span>
</summary>
<button type="button" class="home-card-close"
data-dismiss-key="agnes_home_gs_dismissed"
aria-label="Dismiss Getting Started">&times;</button>
<a class="home-gs-item" href="#install-hero">
<span class="ico" aria-hidden="true">&#x1F680;</span>
<div class="text">
<strong>Setup {{ instance_brand }} in your Claude Code</strong>
<span class="desc">One-time install — walkthrough in the section below.</span>
</div>
<span class="arrow" aria-hidden="true">&darr;</span>
</a>
<a class="home-gs-item" href="/setup-advanced">
<span class="ico" aria-hidden="true">&#x1F6E0;&#xFE0F;</span>
<div class="text">
<strong>Go deeper into your AI workspace</strong>
<span class="desc">VS Code layout, recommended plugins, multi-model second opinions, custom skills + rules + hooks, project workflows.</span>
</div>
<span class="arrow" aria-hidden="true">&rarr;</span>
</a>
</details>
<div class="install-hero" id="install-hero">
<button type="button" class="install-hero-close" id="installHeroClose" <button type="button" class="install-hero-close" id="installHeroClose"
data-target-source="self_acknowledged" data-target-source="self_acknowledged"
aria-label="I'm already set up — close this setup hero">&times;</button> aria-label="I'm already set up — close this setup hero">&times;</button>
@ -1514,35 +1594,10 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
BEFORE Step 3's install runs ~20 commands. Gated by the same BEFORE Step 3's install runs ~20 commands. Gated by the same
`home_automode.show` flag at the call site. #} `home_automode.show` flag at the call site. #}
{# Getting Started card — dismissible per-device via localStorage. {# Getting Started was previously rendered HERE (between the offboard
Two clickable rows pointing at the install flow (/setup) and the strip and Overview) as a full-card <section>. Moved up to render
deeper reference (/setup-advanced). Subsumes the legacy BEFORE the install-hero as a collapsed-by-default <details> — see
`.advanced-pointer` row that used to sit above the news section. #} the block right after `{% if not onboarded %}` near line ~1357. #}
<section class="home-getting-started" id="homeGettingStarted">
<button type="button" class="home-card-close"
data-dismiss-key="agnes_home_gs_dismissed"
aria-label="Dismiss Getting Started">&times;</button>
<header>
<h2>Getting Started</h2>
<p>Two quick next steps to get the most out of {{ instance_brand }}.</p>
</header>
<a class="home-gs-item" href="/setup">
<span class="ico" aria-hidden="true">&#x1F680;</span>
<div class="text">
<strong>Setup {{ instance_brand }} in your Claude Code</strong>
<span class="desc">One-time install: copies a setup script to your clipboard, paste into Claude Code, done in ~10 minutes.</span>
</div>
<span class="arrow" aria-hidden="true">&rarr;</span>
</a>
<a class="home-gs-item" href="/setup-advanced">
<span class="ico" aria-hidden="true">&#x1F6E0;&#xFE0F;</span>
<div class="text">
<strong>Go deeper into your AI workspace</strong>
<span class="desc">VS Code layout, recommended plugins, multi-model second opinions, custom skills + rules + hooks, project workflows.</span>
</div>
<span class="arrow" aria-hidden="true">&rarr;</span>
</a>
</section>
{# Overview section — operator-owned, opt-in. Body comes from the {# Overview section — operator-owned, opt-in. Body comes from the
`instance.overview` yaml field via get_instance_overview() `instance.overview` yaml field via get_instance_overview()
@ -2015,7 +2070,7 @@ Set-Location "$HOME\{{ workspace_dir }}"</span>
// zero per-card JS. // zero per-card JS.
document.querySelectorAll('.home-card-close[data-dismiss-key]').forEach(function (btn) { document.querySelectorAll('.home-card-close[data-dismiss-key]').forEach(function (btn) {
var key = btn.getAttribute('data-dismiss-key'); var key = btn.getAttribute('data-dismiss-key');
var section = btn.closest('section'); var section = btn.closest('section, details');
if (!section || !key) return; if (!section || !key) return;
try { try {
if (localStorage.getItem(key) === '1') { if (localStorage.getItem(key) === '1') {

View file

@ -335,27 +335,49 @@ def test_home_renders_connector_prompts_from_shared_module(fresh_db):
def test_getting_started_card_renders_on_home(fresh_db): def test_getting_started_card_renders_on_home(fresh_db):
"""The dismissible Getting Started card sits between the install """The dismissible Getting Started card now renders BEFORE the
hero and the connector tiles. Both rows must be present and point install-hero (chronologically first in the not-onboarded flow) as
at /setup and /setup-advanced respectively. State-independent: a <details> element collapsed by default so the install hero
renders for both onboarded and not-onboarded users (per-device stays visible on first paint. Disappears when the user is
localStorage dismiss is the only off switch).""" onboarded (no `<details class="home-getting-started">`) so the
in-page #install-hero anchor on the first row never points at
nothing. First row links to #install-hero (same-page jump to the
blue setup hero); second row still leaves the page for
/setup-advanced."""
from src.db import get_system_db, close_system_db from src.db import get_system_db, close_system_db
for onboarded in (False, True): # Not-onboarded: GS is rendered + install-hero anchor target exists.
conn = get_system_db() conn = get_system_db()
try: try:
_, sess = _make_user_and_session( _, sess = _make_user_and_session(
conn, email=f"gs-{onboarded}@example.com", onboarded=onboarded conn, email="gs-not-onboarded@example.com", onboarded=False
) )
finally: finally:
conn.close() conn.close()
close_system_db() close_system_db()
body = _client().get("/home", cookies={"access_token": sess}).text body = _client().get("/home", cookies={"access_token": sess}).text
assert '<section class="home-getting-started"' in body assert '<details class="home-getting-started"' in body
assert 'data-dismiss-key="agnes_home_gs_dismissed"' in body assert 'data-dismiss-key="agnes_home_gs_dismissed"' in body
assert 'class="home-gs-item" href="/setup"' in body assert 'class="home-gs-item" href="#install-hero"' in body
assert 'class="home-gs-item" href="/setup-advanced"' in body assert 'class="home-gs-item" href="/setup-advanced"' in body
# Install-hero must carry the matching id so the first-row anchor
# resolves. Co-asserted with the GS markup so a refactor that drops
# one but not the other breaks here, not in the browser.
assert '<div class="install-hero" id="install-hero">' in body
# Onboarded: install-hero is gone, GS rides alongside it — neither
# renders. Prevents a dangling #install-hero anchor.
conn = get_system_db()
try:
_, sess2 = _make_user_and_session(
conn, email="gs-onboarded@example.com", onboarded=True
)
finally:
conn.close()
close_system_db()
body2 = _client().get("/home", cookies={"access_token": sess2}).text
assert '<details class="home-getting-started"' not in body2
assert '<div class="install-hero"' not in body2
def test_overview_section_renders_when_yaml_set(fresh_db, monkeypatch): def test_overview_section_renders_when_yaml_set(fresh_db, monkeypatch):