Skip to content

Commit db7776e

Browse files
preact 03: add Settings view + light/dark theme
Settings has three sections: - Appearance: light/dark toggle. CSS variables + [data-theme="dark"] overrides. Persisted to localStorage and applied before first render so there's no flash. - Identity: WebID input placeholder (stage 4 wires real auth). - Data islands: enumerates loaded ld+json islands with @type, @id, and property count — a live introspection panel. Nav grows to four entries. Same useHashRoute; adding a view is literally one line in the VIEWS map.
1 parent b5ef9ec commit db7776e

1 file changed

Lines changed: 141 additions & 11 deletions

File tree

preact/03-routing/index.html

Lines changed: 141 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,33 @@
77
<style>
88
* { margin: 0; box-sizing: border-box; }
99
html, body { height: 100%; }
10+
:root {
11+
--bg: #f5f5f9;
12+
--text: #222;
13+
--muted: #888;
14+
--card: #fff;
15+
--border: #e8e8ee;
16+
--shadow: 0 1px 2px rgba(0,0,0,.03);
17+
--rail-bg: #1e1b2e;
18+
--rail-text: #e9e8f3;
19+
--rail-muted: #c9c6dc;
20+
--rail-hover: #2a2640;
21+
}
22+
html[data-theme="dark"] {
23+
--bg: #15131f;
24+
--text: #e9e8f3;
25+
--muted: #8d8aa4;
26+
--card: #1e1b2e;
27+
--border: #2a2640;
28+
--shadow: 0 1px 2px rgba(0,0,0,.35);
29+
--rail-bg: #0f0d18;
30+
--rail-muted: #8d8aa4;
31+
--rail-hover: #1e1b2e;
32+
}
1033
body {
1134
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
12-
background: #f5f5f9; color: #222;
35+
background: var(--bg); color: var(--text);
36+
transition: background .2s, color .2s;
1337
}
1438

1539
/* Shell */
@@ -19,10 +43,10 @@
1943
min-height: 100vh;
2044
}
2145
.rail {
22-
background: #1e1b2e;
23-
color: #e9e8f3;
46+
background: var(--rail-bg);
47+
color: var(--rail-text);
2448
padding: 28px 18px;
25-
border-right: 1px solid #15131f;
49+
border-right: 1px solid rgba(0,0,0,.4);
2650
position: sticky; top: 0; align-self: start; min-height: 100vh;
2751
}
2852
.brand {
@@ -40,11 +64,11 @@
4064
.nav a {
4165
display: flex; align-items: center; gap: 10px;
4266
padding: 9px 12px; border-radius: 8px;
43-
color: #c9c6dc; text-decoration: none;
67+
color: var(--rail-muted); text-decoration: none;
4468
font-size: 14px; font-weight: 500;
4569
transition: background .14s, color .14s;
4670
}
47-
.nav a:hover { background: #2a2640; color: #fff; }
71+
.nav a:hover { background: var(--rail-hover); color: #fff; }
4872
.nav a.active { background: linear-gradient(135deg, #7c3aed, #6366f1); color: #fff; }
4973
.nav .ic { font-size: 16px; }
5074

@@ -63,11 +87,11 @@
6387
}
6488

6589
.card {
66-
background: #fff;
67-
border: 1px solid #e8e8ee;
90+
background: var(--card);
91+
border: 1px solid var(--border);
6892
border-radius: 12px;
6993
padding: 28px;
70-
box-shadow: 0 1px 2px rgba(0,0,0,.03);
94+
box-shadow: var(--shadow);
7195
}
7296
.card + .card { margin-top: 18px; }
7397

@@ -110,7 +134,7 @@
110134
.page-count { font: 600 11px/1 'Inter'; letter-spacing: .12em; text-transform: uppercase; color: #999; margin-bottom: 22px; }
111135
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; }
112136
.contact-card {
113-
background: #fff; border: 1px solid #e8e8ee; border-radius: 12px;
137+
background: var(--card); border: 1px solid var(--border); border-radius: 12px;
114138
padding: 18px; display: flex; gap: 12px; align-items: center;
115139
text-decoration: none; color: inherit; transition: transform .16s, border-color .16s, box-shadow .16s;
116140
}
@@ -148,6 +172,39 @@
148172
color: #888; letter-spacing: 0.05em;
149173
}
150174
.footer-tag code { padding: 2px 7px; border-radius: 4px; background: #eef0fe; color: #4338ca; }
175+
176+
/* Settings view */
177+
.settings-row {
178+
display: flex; align-items: center; justify-content: space-between;
179+
padding: 14px 0; gap: 16px;
180+
}
181+
.settings-row + .settings-row { border-top: 1px solid var(--border); }
182+
.settings-label { font: 500 14px/1.4 'Inter'; color: var(--text); }
183+
.settings-help { font: 400 12.5px/1.4 'Inter'; color: var(--muted); margin-top: 2px; }
184+
.seg {
185+
display: inline-flex; padding: 3px; border-radius: 999px;
186+
background: var(--bg); border: 1px solid var(--border);
187+
}
188+
.seg button {
189+
padding: 6px 14px; border: 0; background: transparent;
190+
font: 500 13px/1 'Inter'; color: var(--muted);
191+
border-radius: 999px; cursor: pointer;
192+
}
193+
.seg button.on { background: var(--card); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.08); }
194+
.settings-input {
195+
padding: 8px 12px; border-radius: 8px;
196+
border: 1px solid var(--border); background: var(--card); color: var(--text);
197+
font: 400 13px/1.4 ui-monospace, monospace;
198+
width: 320px; max-width: 60%; outline: none;
199+
}
200+
.settings-input:focus { border-color: #6366f1; box-shadow: 0 0 0 2px rgba(99,102,241,.18); }
201+
.island-row {
202+
display: flex; align-items: center; gap: 14px;
203+
padding: 10px 0; font: 400 13px/1.4 ui-monospace, monospace;
204+
color: var(--muted);
205+
}
206+
.island-row + .island-row { border-top: 1px solid var(--border); }
207+
.island-badge { padding: 2px 9px; border-radius: 6px; background: #eef0fe; color: #4338ca; font-weight: 600; font-size: 12px; }
151208
</style>
152209
</head>
153210
<body>
@@ -377,12 +434,85 @@ <h1 class="ev-name">${name || 'Untitled Event'}</h1>
377434
`
378435
}
379436

437+
// --- view: Settings --------------------------------------------------------
438+
439+
const THEME_KEY = 'solid-preact.theme'
440+
const WEBID_KEY = 'solid-preact.webid'
441+
442+
function applyTheme(theme) {
443+
document.documentElement.dataset.theme = theme
444+
}
445+
446+
// Restore on load before any render so there's no light→dark flash.
447+
applyTheme(localStorage.getItem(THEME_KEY) || 'light')
448+
449+
function SettingsView() {
450+
const [theme, setTheme] = useState(() => localStorage.getItem(THEME_KEY) || 'light')
451+
const [webid, setWebid] = useState(() => localStorage.getItem(WEBID_KEY) || '')
452+
453+
useEffect(() => { localStorage.setItem(THEME_KEY, theme); applyTheme(theme) }, [theme])
454+
useEffect(() => { if (webid) localStorage.setItem(WEBID_KEY, webid); else localStorage.removeItem(WEBID_KEY) }, [webid])
455+
456+
const islands = Array.from(document.querySelectorAll('script[type="application/ld+json"][data-view]')).map(el => {
457+
let type = '?', id = '?', count = 0
458+
try { const d = JSON.parse(el.textContent); type = d['@type'] || '?'; id = d['@id'] || '?'; count = Object.keys(d).filter(k => !k.startsWith('@')).length } catch {}
459+
return { view: el.dataset.view, type, id, count }
460+
})
461+
462+
return html`
463+
<div>
464+
<h1 class="page-title">Settings</h1>
465+
<p class="page-sub">Appearance, identity, and the data islands this SPA has loaded.</p>
466+
467+
<div class="card" style=${{ marginTop: '18px' }}>
468+
<h3 class="side-title">Appearance</h3>
469+
<div class="settings-row">
470+
<div>
471+
<div class="settings-label">Theme</div>
472+
<div class="settings-help">Saved to <code>localStorage</code>. Applied before render so there's no flash.</div>
473+
</div>
474+
<div class="seg">
475+
<button class=${theme === 'light' ? 'on' : ''} onClick=${() => setTheme('light')}>\u2600\uFE0F Light</button>
476+
<button class=${theme === 'dark' ? 'on' : ''} onClick=${() => setTheme('dark')}>\u{1F319} Dark</button>
477+
</div>
478+
</div>
479+
</div>
480+
481+
<div class="card">
482+
<h3 class="side-title">Identity</h3>
483+
<div class="settings-row">
484+
<div>
485+
<div class="settings-label">WebID</div>
486+
<div class="settings-help">Placeholder — stage 4 will wire this up to xlogin for real authFetch.</div>
487+
</div>
488+
<input class="settings-input" type="url" placeholder="https://you.example.org/profile/card#me" value=${webid} onInput=${(e) => setWebid(e.target.value)} />
489+
</div>
490+
</div>
491+
492+
<div class="card">
493+
<h3 class="side-title">Data islands</h3>
494+
<div>
495+
${islands.map(isl => html`
496+
<div class="island-row">
497+
<span class="island-badge">${isl.view}</span>
498+
<span><strong style=${{ color: 'var(--text)' }}>@type</strong>: ${isl.type}</span>
499+
<span><strong style=${{ color: 'var(--text)' }}>@id</strong>: ${isl.id}</span>
500+
<span style=${{ marginLeft: 'auto' }}>${isl.count} props</span>
501+
</div>
502+
`)}
503+
</div>
504+
</div>
505+
</div>
506+
`
507+
}
508+
380509
// --- shell -----------------------------------------------------------------
381510

382511
const VIEWS = {
383512
profile: { label: 'Profile', icon: '\u{1F464}', Component: ProfileView },
384513
contacts: { label: 'Contacts', icon: '\u{1F4D2}', Component: ContactsView },
385514
calendar: { label: 'Calendar', icon: '\u{1F4C5}', Component: CalendarView },
515+
settings: { label: 'Settings', icon: '\u{2699}\uFE0F', Component: SettingsView },
386516
}
387517

388518
function App() {
@@ -406,7 +536,7 @@ <h1 class="ev-name">${name || 'Untitled Event'}</h1>
406536
</aside>
407537
<main>
408538
<${ViewComponent} />
409-
<div class="footer-tag">stage 03 \u2014 hash routes \u2014 <code>#/profile</code> <code>#/contacts</code> <code>#/calendar</code></div>
539+
<div class="footer-tag">stage 03 \u2014 hash routes \u2014 <code>#/profile</code> <code>#/contacts</code> <code>#/calendar</code> <code>#/settings</code></div>
410540
</main>
411541
</div>
412542
`

0 commit comments

Comments
 (0)