/* global React, ReactDOM, lucide */ /* ============================================================ App — router + tweaks + page dispatch + per-route metadata ============================================================ */ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "beamIntensity": 0.55, "spacingDensity": "regular", "typeScale": "regular", "atmosphere": "regular", "motion": "restrained", "asymmetry": "asymmetric", "diagnosticPacing": "measured", "imageOpacity": 0.95, "editorialDensity": "regular" }/*EDITMODE-END*/; /* ============================================================ Per-route metadata ============================================================ */ const SITE_NAME = 'The Daniel Pledger'; const SITE_BASE_URL = 'https://thedanielpledger.com'; // placeholder canonical origin const ROUTE_META = { '/': { title: 'The Daniel Pledger — Identity Holding Under Pressure', desc: 'Private executive authority advisory. For senior leaders who have outgrown the performance and need their signal to carry when the room is loudest.', }, '/audit': { title: 'The Executive Authority Signal Audit — The Daniel Pledger', desc: 'A private diagnostic for senior leaders. Ten questions. A short reading of where authority leaks under consequence.', }, '/audit/flow': { title: 'The Audit — In progress — The Daniel Pledger', desc: 'The Executive Authority Signal Audit. Held in confidence.', }, '/audit/results': { title: 'The Reading — The Executive Authority Signal Audit', desc: 'Your private reading. One dominant signature. One secondary. Held in confidence.', }, '/conversation': { title: 'Request a Private Conversation — The Daniel Pledger', desc: 'A short note about the room you are trying to hold is usually enough. Daniel responds personally within a week.', }, '/insights': { title: 'Field Notes — The Daniel Pledger', desc: 'Short, deliberate writing on identity, authority, and what the senior room actually reads.', }, '/perspective': { title: 'Perspective — The Daniel Pledger', desc: 'I do not coach performance. I work on identity. A position on what private executive advisory is, and is not.', }, '/speaking': { title: 'Speaking — The Daniel Pledger', desc: 'Closed-room conversations on authority under consequence. Founder cohorts, board offsites, leadership intensives.', }, '/privacy': { title: 'Privacy Policy — The Daniel Pledger', desc: 'What is collected when you use this site, why, and how it is held.', }, '/terms': { title: 'Terms of Service — The Daniel Pledger', desc: 'The terms that apply to this site, its writing, and the Executive Authority Signal Audit.', }, }; function getRouteMeta(path) { if (ROUTE_META[path]) return ROUTE_META[path]; // /insights/:slug const m = matchRoute(path, '/insights/:slug'); if (m) { const i = (window.INSIGHTS || []).find((x) => x.slug === m.slug); if (i) { return { title: `${i.title} — The Daniel Pledger`, desc: i.excerpt, }; } } return { title: SITE_NAME, desc: ROUTE_META['/'].desc, }; } /* Update and meta tags imperatively for SPA route changes */ function applyRouteMeta(path) { const meta = getRouteMeta(path); const canonical = SITE_BASE_URL + path; document.title = meta.title; setMeta('description', meta.desc); // Open Graph setMetaProperty('og:title', meta.title); setMetaProperty('og:description', meta.desc); setMetaProperty('og:type', 'website'); setMetaProperty('og:site_name', SITE_NAME); setMetaProperty('og:url', canonical); setMetaProperty('og:image', SITE_BASE_URL + '/og-default.jpg'); // Twitter setMeta('twitter:card', 'summary_large_image'); setMeta('twitter:title', meta.title); setMeta('twitter:description', meta.desc); setMeta('twitter:image', SITE_BASE_URL + '/og-default.jpg'); // Canonical setLink('canonical', canonical); } function setMeta(name, content) { let el = document.querySelector(`meta[name="${name}"]`); if (!el) { el = document.createElement('meta'); el.setAttribute('name', name); document.head.appendChild(el); } el.setAttribute('content', content || ''); } function setMetaProperty(prop, content) { let el = document.querySelector(`meta[property="${prop}"]`); if (!el) { el = document.createElement('meta'); el.setAttribute('property', prop); document.head.appendChild(el); } el.setAttribute('content', content || ''); } function setLink(rel, href) { let el = document.querySelector(`link[rel="${rel}"]`); if (!el) { el = document.createElement('link'); el.setAttribute('rel', rel); document.head.appendChild(el); } el.setAttribute('href', href); } /* ============================================================ Map tweak values to CSS variables on :root ============================================================ */ function applyTweaksToRoot(t) { const r = document.documentElement; r.style.setProperty('--tdp-beam', String(t.beamIntensity)); const dens = { compact: 0.72, regular: 1.0, generous: 1.35 }[t.spacingDensity] || 1; r.style.setProperty('--tdp-density', String(dens)); const ts = { smaller: 0.88, regular: 1.0, larger: 1.12 }[t.typeScale] || 1; r.style.setProperty('--tdp-type-scale', String(ts)); const mo = { still: 0, restrained: 1.0, lively: 1.6 }[t.motion] || 1; r.style.setProperty('--tdp-motion', String(mo)); r.style.setProperty('--tdp-img-opacity', String(t.imageOpacity)); const ed = { sparse: 0.86, regular: 1.0, dense: 1.16 }[t.editorialDensity] || 1; r.style.setProperty('--tdp-edit-density', String(ed)); const atm = { lighter: { bg: 'var(--c-admiral-ink)', deep: 'var(--c-imperial-indigo)' }, regular: { bg: 'var(--c-imperial-indigo)', deep: 'var(--c-nightfall)' }, deeper: { bg: 'var(--c-nightfall)', deep: '#06080d' }, }[t.atmosphere] || { bg: 'var(--c-imperial-indigo)', deep: 'var(--c-nightfall)' }; document.body.style.background = atm.bg; r.style.setProperty('--bg', atm.bg); r.style.setProperty('--bg-deep', atm.deep); } /* ============================================================ Page dispatcher — reads router path, applies metadata ============================================================ */ function Page() { const { path } = useRouter(); // Apply per-route metadata on every path change React.useEffect(() => { applyRouteMeta(path); }, [path]); if (path === '/' || path === '') return <HomePage />; if (path === '/audit') return <AuditLandingPage />; if (path === '/audit/flow') return <AuditFlowPage />; if (path === '/audit/results') return <AuditResultsPage />; if (path === '/conversation') return <ConversationPage />; if (path === '/insights') return <InsightsIndexPage />; const m = matchRoute(path, '/insights/:slug'); if (m) return <InsightArticlePage slug={m.slug} />; if (path === '/perspective') return <PerspectivePage />; if (path === '/speaking') return <SpeakingPage />; if (path === '/privacy') return <PrivacyPage />; if (path === '/terms') return <TermsPage />; return ( <div className="page-fade"> <section style={{ paddingTop: 200, minHeight: '80vh' }}> <div className="container-narrow" style={{ textAlign: 'center' }}> <Eyebrow>Off the map</Eyebrow> <h1 style={{ fontSize: 'clamp(36px, 4vw, 64px)', color: '#fff', fontWeight: 500, margin: '24px 0 24px', letterSpacing: '-0.028em' }}> This path does not lead anywhere. </h1> <Button variant="primary" href="/">Return home</Button> </div> </section> </div> ); } /* ============================================================ Tweaks UI ============================================================ */ function TweaksPanelTDP({ tweaks, setTweak }) { return ( <TweaksPanel title="Tweaks"> <TweakSection label="Atmosphere"> <TweakSlider label="Beam intensity" value={tweaks.beamIntensity} min={0.1} max={1.0} step={0.05} onChange={(v) => setTweak('beamIntensity', v)} /> <TweakSelect label="Atmospheric contrast" value={tweaks.atmosphere} options={[ { value: 'lighter', label: 'Lighter' }, { value: 'regular', label: 'Regular' }, { value: 'deeper', label: 'Deeper' }, ]} onChange={(v) => setTweak('atmosphere', v)} /> <TweakSlider label="Image luminance" value={tweaks.imageOpacity} min={0.3} max={1.0} step={0.05} onChange={(v) => setTweak('imageOpacity', v)} /> </TweakSection> <TweakSection label="Layout"> <TweakRadio label="Density" value={tweaks.spacingDensity} options={['compact', 'regular', 'generous']} onChange={(v) => setTweak('spacingDensity', v)} /> <TweakRadio label="Asymmetry" value={tweaks.asymmetry} options={['centered', 'balanced', 'asymmetric']} onChange={(v) => setTweak('asymmetry', v)} /> <TweakRadio label="Editorial density" value={tweaks.editorialDensity} options={['sparse', 'regular', 'dense']} onChange={(v) => setTweak('editorialDensity', v)} /> </TweakSection> <TweakSection label="Type"> <TweakRadio label="Type scale" value={tweaks.typeScale} options={['smaller', 'regular', 'larger']} onChange={(v) => setTweak('typeScale', v)} /> </TweakSection> <TweakSection label="Motion"> <TweakRadio label="Motion restraint" value={tweaks.motion} options={['still', 'restrained', 'lively']} onChange={(v) => setTweak('motion', v)} /> <TweakRadio label="Diagnostic pacing" value={tweaks.diagnosticPacing} options={['quick', 'measured', 'slow']} onChange={(v) => setTweak('diagnosticPacing', v)} /> </TweakSection> </TweaksPanel> ); } /* ============================================================ Root ============================================================ */ function App() { const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); // Apply tweaks to :root variables React.useEffect(() => { applyTweaksToRoot(tweaks); }, [tweaks]); // Lucide icon refresh React.useEffect(() => { const id = setTimeout(() => window.lucide && window.lucide.createIcons(), 60); return () => clearTimeout(id); }); React.useEffect(() => { const i = setInterval(() => window.lucide && window.lucide.createIcons(), 800); return () => clearInterval(i); }, []); return ( <RouterProvider> <TweaksContext.Provider value={tweaks}> <a href="#main-content" className="skip-link">Skip to content</a> <Nav /> <main id="main-content" tabIndex={-1} style={{ position: 'relative', overflow: 'hidden' }}> <Page /> </main> <Footer /> <TweaksPanelTDP tweaks={tweaks} setTweak={setTweak} /> </TweaksContext.Provider> </RouterProvider> ); } ReactDOM.createRoot(document.getElementById('root')).render(<App />); // expose INSIGHTS to window so getRouteMeta can resolve article titles // (pages-insights.jsx attaches INSIGHTS to window already)