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:
@@ -9,6 +9,7 @@
|
||||
import FormRecommendation from './components/FormRecommendation.svelte';
|
||||
import AnalysisTransitionBanner from './components/AnalysisTransitionBanner.svelte';
|
||||
import HamburgerMenu from './components/HamburgerMenu.svelte';
|
||||
import Landing from './components/Landing.svelte';
|
||||
import { BicorderClassifier } from './bicorder-classifier';
|
||||
|
||||
// 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 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
|
||||
type Screen =
|
||||
| { type: 'metadata' }
|
||||
@@ -432,6 +459,9 @@
|
||||
|
||||
<HelpModal bind:isOpen={isHelpOpen} />
|
||||
|
||||
{#if !started}
|
||||
<Landing on:begin={startReading} on:about={openHelp} />
|
||||
{:else}
|
||||
<main>
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
@@ -519,12 +549,6 @@
|
||||
|
||||
{:else}
|
||||
<!-- 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">
|
||||
{#if currentScreenData.type === 'metadata'}
|
||||
<div class="focused-screen">
|
||||
@@ -679,6 +703,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
main {
|
||||
|
||||
@@ -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 →
|
||||
</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>
|
||||
Reference in New Issue
Block a user