Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ui/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -545,3 +545,7 @@ select{appearance:none; -webkit-appearance:none; background-image:url("data:imag
.cer-kv{display:flex; gap:10px; align-items:baseline; font-size:12px; margin-top:3px}
.cer-k{min-width:70px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.04em; font-size:10px; font-family:var(--font-mono)}
.cer-kv code{font-family:var(--font-mono); font-size:11.5px; color:var(--silver); word-break:break-all}

/* Exit link on the demo banner (#60 runtime demo) */
.demo-banner-exit{margin-left:12px; text-decoration:underline; color:inherit; opacity:.85; font-weight:600}
.demo-banner-exit:hover{opacity:1}
29 changes: 20 additions & 9 deletions ui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,30 @@ export type Balance = {
const ENV = import.meta.env as Record<string, string | undefined>
const BASE: string = ENV.VITE_API_BASE ?? ''

// Hosted-demo mode (Vercel, no backend). When set, reads that fail (they all do without a
// bridge) fall back to a coherent mock dataset so every screen renders fully populated.
// `health()` is deliberately NOT affected, so the "demo/offline" pill still shows.
const DEMO = ENV.VITE_DEMO === '1'
/** True in the hosted demo build: screens should load api data (which falls back to
* the coherent mock) even though `health()` is false. */
// Demo mode, decided at RUNTIME (issue #60) so ONE build serves both the demo (mock data) and the
// real app — no build-time VITE_DEMO / separate deploy needed. `?demo=1` enters demo mode and
// persists it (localStorage); `?demo=0` exits. VITE_DEMO stays as a build-time fallback so the
// existing demo project keeps working. When set, reads that fail fall back to a coherent mock
// dataset so every screen renders fully populated; `health()` is NOT affected, so the demo pill shows.
const DEMO = (() => {
if (typeof window === 'undefined') return ENV.VITE_DEMO === '1'
try {
const q = new URLSearchParams(window.location.search)
if (q.get('demo') === '1') { localStorage.setItem('konclave.demo', '1'); return true }
if (q.get('demo') === '0') { localStorage.removeItem('konclave.demo'); return false }
if (localStorage.getItem('konclave.demo') === '1') return true
} catch { /* storage unavailable — fall through to the build flag */ }
return ENV.VITE_DEMO === '1'
})()
/** True in demo mode (runtime `?demo=1` or the VITE_DEMO fallback): screens load api data (which
* falls back to the coherent mock) even though `health()` is false. */
export const IS_DEMO = DEMO

// Browser-native mode (Etapa 3 convergence): when a hosted blind helper is configured, the PWA
// screens (Dashboard / Proposals / Ledger) read the SELECTED /net vault from the helper instead of
// the local bridge, so the same polished app operates the browser-born vault. Gated on
// `helperConfigured()`, so the local-first path and the submission demo (no helper) are unchanged.
const NET = helperConfigured()
// the local bridge, so the same polished app operates the browser-born vault. Never in demo mode —
// demo always uses mock, so the same canonical build can point at a helper AND still show the demo.
const NET = helperConfigured() && !DEMO
/** True when the app operates a browser-native (/net) vault via the hosted helper. Screens use it
* to route signing to /net (where the share lives) instead of a server-side ceremony. */
export const IS_NET = NET
Expand Down
6 changes: 4 additions & 2 deletions ui/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import { useReveal } from './reveal'
import { useI18n, useT } from './i18n'
import { IS_DEMO } from './api'

/** A thin, always-visible strip on the hosted demo build (VITE_DEMO=1) so a visitor never
* mistakes the sample data for a real vault. Renders nothing outside the demo build. */
/** A thin, always-visible strip in demo mode (runtime `?demo=1` or the VITE_DEMO fallback) so a
* visitor never mistakes the sample data for a real vault, and can leave demo mode (`?demo=0`).
* Renders nothing outside demo mode. */
export function DemoBanner() {
const t = useT()
if (!IS_DEMO) return null
return (
<div className="demo-banner" role="note">
<span className="demo-banner-tag">DEMO</span>
{t('demo.bannerNote')}
<a className="demo-banner-exit" href="?demo=0">{t('demo.bannerExit')}</a>
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export const en: Record<string, string> = {
'demo.live': 'Live demo',
'demo.note': 'This is the cryptography itself, running live, not the everyday vault screens.',
'demo.bannerNote': 'Demonstration with sample data. No real funds, no real transactions.',
'demo.bannerExit': 'Exit demo',
'settings.eyebrow': 'SETTINGS',
'settings.title': 'Settings',
'settings.vault': 'Vault',
Expand Down
1 change: 1 addition & 0 deletions ui/src/i18n/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export const ptBR: Record<string, string> = {
'demo.live': 'Demonstração ao vivo',
'demo.note': 'Isto é a própria criptografia, rodando ao vivo, não as telas do dia a dia do cofre.',
'demo.bannerNote': 'Demonstração com dados de exemplo. Nenhum fundo real, nenhuma transação de verdade.',
'demo.bannerExit': 'Sair da demo',
'settings.eyebrow': 'AJUSTES',
'settings.title': 'Ajustes',
'settings.vault': 'Cofre',
Expand Down
5 changes: 3 additions & 2 deletions ui/src/screens/Intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ export default function Intro() {
<h1>{t('landing.h1')}</h1>
<p className="sub">{tr('landing.sub')}</p>
<div className="lp-ctas">
<Link className="lp-btn primary" to="/demo">
{/* Enter demo MODE (mock data everywhere) at runtime — one build, no VITE_DEMO (#60). */}
<a className="lp-btn primary" href="?demo=1#/vaults">
{t('demo.watchCta')}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 12h14M13 6l6 6-6 6" /></svg>
</Link>
</a>
<Link className="lp-btn" to="/docs">{t('landing.ctaHow')}</Link>
<Link className="lp-btn" to="/vaults">{t('landing.ctaVaults')}</Link>
<a className="lp-btn" href="https://github.com/deegalabs/konclave/releases" target="_blank" rel="noreferrer">
Expand Down
Loading