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 */
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 {
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
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 1 px 2 px rgba ( 0 , 0 , 0 , .03 );
94+ box-shadow : var ( --shadow );
7195 }
7296 .card + .card { margin-top : 18px ; }
7397
110134 .page-count { font : 600 11 px/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 }
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 14 px/1.4 'Inter' ; color : var (--text ); }
183+ .settings-help { font : 400 12.5 px/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 13 px/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 13 px/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 13 px/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
382511const 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
388518function 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