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 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 {
|
||||||
|
|||||||
@@ -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