feat(web): manual escape hatch on the skipped-onboarding board (TASK-1856) (#747)

A user who clicks "Skip setup" on an empty workspace landed on a board with
no collections and no surfaced way to create one (collections lived behind
Settings) — declining an agent was effectively a dead end.

When onboarding is dismissed and the workspace has no user collections, owners
now see an "Your workspace is empty" card with a "+ Create a collection"
button (opens the existing CreateCollectionModal) and a "Show setup guide"
link back to the launchpad. Owner-gated to match the server's
create-collection boundary; hidden the moment any user collection exists.
Non-owners and already-populated boards keep the existing slim reshow link.

Parent: PLAN-1847 (Phase 3).

Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
This commit is contained in:
xarmian
2026-06-21 12:29:09 -04:00
committed by GitHub
parent fa8064e2b9
commit 4a99f5f213
@@ -205,6 +205,10 @@
let totalItems = $derived(dashboard?.summary.total_items ?? 0);
let firstCollection = $derived(collections.filter(c => !c.is_system && c.slug !== 'tasks').sort((a, b) => a.sort_order - b.sort_order)[0]);
let hasTasksCollection = $derived(collections.some(c => c.slug === 'tasks'));
// A workspace with no user-facing collections (only system ones, or none)
// has no manual path to create anything from the board. Drives the
// web-only escape hatch on the skipped-onboarding board (TASK-1856).
let hasUserCollections = $derived(collections.some(c => !c.is_system));
function statusColor(status: string): string {
const s = status.toLowerCase().replace(/-/g, '_');
@@ -344,13 +348,39 @@
and ConnectBanner use.
-->
{#if needsOnboarding && onboardingDismissed}
<!--
Reshow affordance for users who skipped the launchpad. Clicking
restores the launchpad render-mode (clears the dismissed flag).
-->
<div class="onboarding-reshow">
<button class="reshow-btn" onclick={showOnboarding}>Show setup guide</button>
</div>
{#if isOwner && !hasUserCollections}
<!--
Web-only escape hatch (TASK-1856). A user who skipped the
launchpad on a still-empty workspace has no manual path to
create anything from the board (collections live behind
Settings). Offer a direct "create a collection" affordance so
declining an agent isn't a dead end — owner-gated to match the
server's create-collection boundary, with a reshow link back to
the launchpad. Hidden once any user collection exists.
-->
<div class="empty-escape">
<span class="empty-escape-icon" aria-hidden="true">📋</span>
<div class="empty-escape-body">
<p class="empty-escape-title">Your workspace is empty</p>
<p class="empty-escape-text">
Connect an agent and say <strong>set up my workspace</strong> to
have it set up for you — or create your first collection by hand.
</p>
<div class="empty-escape-actions">
<button class="btn btn-primary" onclick={() => (showCreateCollection = true)}>+ Create a collection</button>
<button class="btn btn-secondary" onclick={showOnboarding}>Show setup guide</button>
</div>
</div>
</div>
{:else}
<!--
Reshow affordance for users who skipped the launchpad. Clicking
restores the launchpad render-mode (clears the dismissed flag).
-->
<div class="onboarding-reshow">
<button class="reshow-btn" onclick={showOnboarding}>Show setup guide</button>
</div>
{/if}
{/if}
<!-- 3. Active Work -->
@@ -710,6 +740,49 @@
border-color: var(--text-muted);
}
/* Web-only escape hatch (TASK-1856) — manual start for users who skip the
agent flow on an empty workspace. */
.empty-escape {
display: flex;
align-items: flex-start;
gap: var(--space-3);
margin-bottom: var(--space-4);
padding: var(--space-4);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.empty-escape-icon {
font-size: 1.4em;
line-height: 1.2;
flex-shrink: 0;
}
.empty-escape-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.empty-escape-title {
margin: 0;
font-size: 0.95em;
font-weight: 600;
color: var(--text-primary);
}
.empty-escape-text {
margin: 0;
font-size: 0.85em;
line-height: 1.45;
color: var(--text-secondary);
}
.empty-escape-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-1);
}
/* ── Section headers ────────────────────────────────────────────────── */
.section {
margin-bottom: var(--space-8);