Add landing screen as the app's entry point

Introduce a simple, show-don't-tell entry screen that lowers the "what
counts as a protocol" barrier and frames the activity as a short journey:

- Lead line "Examine the protocols around you" over a rotating, cross-fading
  list of deliberately diverse examples (traffic, code, kitchens, ritual,
  governance, play)
- Prominent "Begin" CTA above a three-step path: Describe / Understand / Share
- "About the Bicorder" link opens the existing help modal

Shown on first arrival; returning users with a reading in progress skip
straight to the diagnostic (computed synchronously, no flash). Remove the
now-redundant inline description from the focused flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Schneider
2026-06-30 17:37:42 -06:00
parent fd556d967f
commit c571bf1c01
2 changed files with 247 additions and 6 deletions
+31 -6
View File
@@ -9,6 +9,7 @@
import FormRecommendation from './components/FormRecommendation.svelte'; import FormRecommendation from './components/FormRecommendation.svelte';
import AnalysisTransitionBanner from './components/AnalysisTransitionBanner.svelte'; import AnalysisTransitionBanner from './components/AnalysisTransitionBanner.svelte';
import HamburgerMenu from './components/HamburgerMenu.svelte'; import HamburgerMenu from './components/HamburgerMenu.svelte';
import Landing from './components/Landing.svelte';
import { BicorderClassifier } from './bicorder-classifier'; import { BicorderClassifier } from './bicorder-classifier';
// Load bicorder data and model from build-time constants // Load bicorder data and model from build-time constants
@@ -30,6 +31,32 @@
let refreshKey = 0; // Used to force component refresh in focused mode let refreshKey = 0; // Used to force component refresh in focused mode
let isHelpOpen = false; let isHelpOpen = false;
// Show the landing screen on first arrival. Returning users who already have
// a reading in progress skip straight to the diagnostic. Computed
// synchronously (not in onMount) so the landing never flashes for them.
function hasReadingInProgress(): boolean {
if (typeof localStorage === 'undefined') return false;
try {
const saved = localStorage.getItem('bicorder-state');
if (!saved) return false;
const s = JSON.parse(saved);
const hasProtocol = !!s?.metadata?.protocol;
const hasValue = s?.diagnostic?.some(
(set: any) => set?.gradients?.some((g: any) => g?.value !== null && g?.value !== undefined)
);
return hasProtocol || hasValue;
} catch {
return false;
}
}
let started = hasReadingInProgress();
function startReading() {
started = true;
currentScreen = 0;
}
// Screen types // Screen types
type Screen = type Screen =
| { type: 'metadata' } | { type: 'metadata' }
@@ -432,6 +459,9 @@
<HelpModal bind:isOpen={isHelpOpen} /> <HelpModal bind:isOpen={isHelpOpen} />
{#if !started}
<Landing on:begin={startReading} on:about={openHelp} />
{:else}
<main> <main>
<div class="header"> <div class="header">
<div class="header-left"> <div class="header-left">
@@ -519,12 +549,6 @@
{:else} {:else}
<!-- FOCUSED MODE: Show one screen at a time --> <!-- FOCUSED MODE: Show one screen at a time -->
{#if currentScreen === 0}
<div class="description">
<p>A diagnostic tool for the study of protocols</p>
</div>
{/if}
<div class="focused-container"> <div class="focused-container">
{#if currentScreenData.type === 'metadata'} {#if currentScreenData.type === 'metadata'}
<div class="focused-screen"> <div class="focused-screen">
@@ -679,6 +703,7 @@
</div> </div>
{/if} {/if}
</main> </main>
{/if}
<style> <style>
main { main {
+216
View File
@@ -0,0 +1,216 @@
<script lang="ts">
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { fade } from 'svelte/transition';
const dispatch = createEventDispatcher<{ begin: void; about: void }>();
// A deliberately wide-ranging list — traffic, code, kitchens, ritual,
// governance, play — to show how capacious "protocol" really is.
const examples = [
'a four-way stop',
'tipping at a restaurant',
"Robert's Rules of Order",
'a sourdough starter passed between neighbors',
'code review on a pull request',
'a Quaker meeting for worship',
'queuing for the bus',
'how Wikipedia settles an edit war',
'a family holiday gift exchange',
'the handshake',
'academic peer review',
'trick-or-treating',
'a potluck dinner',
'jury deliberation',
"a city's recycling rules",
'an open-source contributor guide',
];
let idx = 0;
let timer: ReturnType<typeof setInterval>;
onMount(() => {
timer = setInterval(() => {
idx = (idx + 1) % examples.length;
}, 2400);
});
onDestroy(() => clearInterval(timer));
</script>
<div class="landing">
<div class="title-block">
<div class="title-sm">Protocol</div>
<div class="title-lg">BICORDER</div>
<div class="motif" aria-hidden="true">[----#----]</div>
</div>
<div class="scan">
<span class="scan-label">Examine the protocols around you</span>
<span class="scan-example">
{#key idx}
<span in:fade={{ duration: 350 }}>{examples[idx]}</span>
{/key}
</span>
</div>
<button class="begin-btn" on:click={() => dispatch('begin')}>
Begin &rarr;
</button>
<ol class="steps">
<li>
<span class="step-n">1</span>
<span class="step-text"><strong>Describe</strong> a protocol around you</span>
</li>
<li>
<span class="step-n">2</span>
<span class="step-text"><strong>Understand</strong> how it works</span>
</li>
<li>
<span class="step-n">3</span>
<span class="step-text"><strong>Share</strong> your findings</span>
</li>
</ol>
<button class="about-link" on:click={() => dispatch('about')}>
About the Bicorder
</button>
</div>
<style>
.landing {
min-height: 100vh;
max-width: 640px;
margin: 0 auto;
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 2rem;
}
.title-block {
display: flex;
flex-direction: column;
align-items: center;
}
.title-sm {
font-size: 1.1rem;
letter-spacing: 0.3rem;
opacity: 0.7;
}
.title-lg {
font-size: 2.4rem;
font-weight: bold;
letter-spacing: 0.4rem;
}
.motif {
font-family: 'Courier New', Courier, monospace;
font-size: 1.2rem;
letter-spacing: 0.15rem;
opacity: 0.4;
margin-top: 0.75rem;
}
.scan {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.scan-label {
font-size: 1.4rem;
font-weight: bold;
line-height: 1.4;
}
.scan-example {
/* Reserve space for up to two wrapped lines so the rotating example
never shifts the steps below it. */
display: flex;
align-items: flex-start;
justify-content: center;
min-height: 3.2rem;
font-size: 1.2rem;
opacity: 0.85;
}
.steps {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.85rem;
text-align: left;
}
.steps li {
display: flex;
align-items: center;
gap: 0.85rem;
font-size: 1.05rem;
}
.step-n {
flex-shrink: 0;
width: 1.9rem;
height: 1.9rem;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--border-color);
border-radius: 50%;
font-weight: bold;
font-size: 0.95rem;
}
.step-text {
opacity: 0.9;
}
.begin-btn {
font-size: 1.15rem;
font-weight: bold;
letter-spacing: 0.1rem;
padding: 0.85rem 2.5rem;
}
.about-link {
background: none;
border: none;
min-height: auto;
min-width: auto;
padding: 0;
font-size: 0.85rem;
text-decoration: underline;
opacity: 0.55;
cursor: pointer;
}
.about-link:hover {
opacity: 0.9;
background: none;
color: var(--fg-color);
}
@media (max-width: 768px) {
.landing {
gap: 1.5rem;
padding: 1.5rem 1.25rem;
}
.title-lg {
font-size: 2rem;
}
.scan-label {
font-size: 1.2rem;
}
}
</style>