Skip to content

Commit e4f1fcb

Browse files
markcclaude
andcommitted
Adopt OKLCH color space and refine app shell
site.css: Complete rewrite using OKLCH perceptually uniform colors - Systematic lightness structure (L%) consistent across all schemes - Color schemes via hue rotation: Stone(60), Ocean(220), Forest(150), Sunset(45) - Added --accent-fg for proper button text contrast in light/dark modes - Shadow overrides for dark mode visibility base.css: - Added preload class to disable transitions during initial load - Fixed sidebar nav link hover jump (transparent border by default) - Added margin-block-start to h3/h4 for better spacing after lists base.js: - Added preload class removal after state restore (prevents reflow) - Added responsive listener to unpin sidebars when viewport shrinks - Hamburger now unpins pinned sidebars when clicked 04-Themes: - Reorganized content: Home=Theme System, About=App Shell - Added page title with back arrow - Updated inline script for base-state localStorage format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c2c7f56 commit e4f1fcb

5 files changed

Lines changed: 275 additions & 97 deletions

File tree

04-Themes/public/index.php

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
public function __construct(
99
public string $email = 'mc@netserva.org',
1010
array $in = ['o' => 'Home', 'm' => 'list', 'x' => ''],
11-
public array $out = ['doc' => 'SPE::04', 'head' => '', 'main' => '', 'foot' => ''],
11+
public array $out = ['doc' => 'SPE::04', 'page' => '← 04 Themes', 'head' => '', 'main' => '', 'foot' => ''],
1212
public array $nav = [
1313
['home', 'Home', 'Home'],
1414
['book-open','About', 'About'],
@@ -56,12 +56,12 @@ public function list(): array { return ['head' => 'List', 'main' => 'Not imple
5656

5757
final class HomeModel extends Plugin {
5858
#[\Override] public function list(): array {
59-
return ['head' => 'App Shell', 'main' => 'Dual sidebar interface with <b>LHS navigation</b> and <b>RHS settings</b>.'];
59+
return ['head' => 'Theme System', 'main' => 'A modern CSS architecture using <b>OKLCH color space</b> and <b>mobile-first</b> responsive design.'];
6060
}
6161
}
6262
final class AboutModel extends Plugin {
6363
#[\Override] public function list(): array {
64-
return ['head' => 'About', 'main' => 'This chapter introduces the <b>App Shell</b> pattern with pinnable sidebars.'];
64+
return ['head' => 'App Shell', 'main' => 'Dual sidebar interface with <b>LHS</b> and <b>RHS</b> navigation panels.'];
6565
}
6666
}
6767
final class ContactModel extends Plugin {
@@ -80,13 +80,49 @@ public function list(): string {
8080
final class HomeView extends View {
8181
#[\Override] public function list(): string {
8282
return <<<HTML
83+
<div class="card">
84+
<h2>{$this->ary['head']}</h2>
85+
<p>{$this->ary['main']}</p>
86+
<h3>CSS Architecture</h3>
87+
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem">
88+
<li><b>base.css</b> - Color-agnostic framework (layout, components, utilities)</li>
89+
<li><b>site.css</b> - All color definitions using OKLCH color space</li>
90+
</ul>
91+
<h3>OKLCH Color Space</h3>
92+
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem">
93+
<li><b>Perceptually uniform</b> - Equal changes look equal visually</li>
94+
<li><b>L</b> (Lightness) - Consistent across all hues (0-100%)</li>
95+
<li><b>C</b> (Chroma) - Color intensity (0 = gray, 0.4 = vivid)</li>
96+
<li><b>H</b> (Hue) - Color angle (0-360°)</li>
97+
</ul>
98+
<h3>Color Schemes</h3>
99+
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem">
100+
<li><b>Stone</b> (H=60) - Warm neutral, default theme</li>
101+
<li><b>Ocean</b> (H=220) - Cool cyan-blue tones</li>
102+
<li><b>Forest</b> (H=150) - Natural green palette</li>
103+
<li><b>Sunset</b> (H=45) - Warm orange-amber</li>
104+
</ul>
105+
<h3>Mobile-First Breakpoints</h3>
106+
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem">
107+
<li><b>Mobile</b> (0-767px) - Base styles, full width</li>
108+
<li><b>Tablet</b> (768px+) - Cards gain borders/shadows</li>
109+
<li><b>Desktop</b> (1280px+) - Sidebar pinning enabled</li>
110+
</ul>
111+
</div>
112+
HTML;
113+
}
114+
}
115+
116+
final class AboutView extends View {
117+
#[\Override] public function list(): string {
118+
return <<<HTML
83119
<div class="card">
84120
<h2>{$this->ary['head']}</h2>
85121
<p>{$this->ary['main']}</p>
86122
<h3>Features</h3>
87123
<ul style="list-style:disc;padding-left:1.5rem;margin-top:0.5rem">
88-
<li><b>LHS Sidebar</b> - Navigation (pages, posts)</li>
89-
<li><b>RHS Sidebar</b> - Settings (theme, colors)</li>
124+
<li><b>LHS Panel</b> - Primary navigation (pages, posts)</li>
125+
<li><b>RHS Panel</b> - Secondary navigation, suited to settings</li>
90126
<li><b>Flyover</b> - Sidebars overlay content by default</li>
91127
<li><b>Pin</b> - Lock sidebars open on desktop (1280px+)</li>
92128
</ul>
@@ -101,8 +137,6 @@ final class HomeView extends View {
101137
}
102138
}
103139

104-
final class AboutView extends View {}
105-
106140
final class ContactView extends View {
107141
#[\Override] public function list(): string {
108142
return <<<HTML
@@ -153,7 +187,7 @@ private function topnav(): string {
153187
return <<<HTML
154188
<nav class="topnav">
155189
<button class="menu-toggle" data-sidebar="left"><i data-lucide="menu"></i></button>
156-
<h1><a class="brand" href="/"><span>{$this->out['doc']}</span></a></h1>
190+
<h1><a class="brand" href="/"><span>{$this->out['page']}</span></a></h1>
157191
<button class="menu-toggle" data-sidebar="right"><i data-lucide="menu"></i></button>
158192
</nav>
159193
HTML;
@@ -162,8 +196,9 @@ private function topnav(): string {
162196
private function sidebar(string $side): string {
163197
$nav = $side === 'left'
164198
? $this->links($this->ctx->nav, 'o', $this->ctx->in['o'])
165-
: '<a href="#" onclick="Base.toggleTheme();return false" data-icon="moon"><i data-lucide="moon"></i> Toggle Theme</a>'
166-
. $this->colorLinks();
199+
: $this->colorLinks()
200+
. '<div class="sidebar-divider"></div>'
201+
. '<a href="#" onclick="Base.toggleTheme();return false" data-icon="moon"><i data-lucide="moon"></i> Toggle Theme</a>';
167202
$title = $side === 'left' ? 'Navigation' : 'Settings';
168203
$icon = $side === 'left' ? 'compass' : 'sliders-horizontal';
169204
return <<<HTML
@@ -193,7 +228,7 @@ private function html(string $body): string {
193228
<link rel="stylesheet" href="/base.css">
194229
<link rel="stylesheet" href="/site.css">
195230
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
196-
<script>(function(){var t=localStorage.getItem('theme'),c=localStorage.getItem('scheme'),h=document.documentElement;h.className=(t||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'))+(c&&c!=='default'?' scheme-'+c:'');})()</script>
231+
<script>(function(){var s=JSON.parse(localStorage.getItem('base-state')||'{}'),t=s.theme,c=s.scheme,h=document.documentElement;h.className='preload '+(t||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'))+(c&&c!=='default'?' scheme-'+c:'');})()</script>
197232
</head>
198233
<body>
199234
{$body}

09-Blog/src/Core/Theme.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,49 @@ protected function authNav(): string
9494
return '<a href="?o=Auth&m=login"><i data-lucide="lock"></i> Login</a>';
9595
}
9696

97+
protected function userDropdown(): string
98+
{
99+
if (!Util::is_usr()) {
100+
return '<a href="?o=Auth&m=login"><i data-lucide="lock"></i> Login</a>';
101+
}
102+
103+
$usr = $_SESSION['usr'];
104+
$name = htmlspecialchars($usr['fname'] ?: $usr['login']);
105+
$t = $this->ctx->in['t'];
106+
107+
// Profile link
108+
$links = '<a href="?o=Auth&m=profile"><i data-lucide="user"></i> Profile</a>';
109+
$links .= '<a href="?o=Auth&m=changepw"><i data-lucide="key"></i> Password</a>';
110+
111+
// Admin links
112+
if (Util::is_adm()) {
113+
$links .= '<div class="dropdown-divider"></div>';
114+
$links .= '<a href="?o=Users"><i data-lucide="users"></i> Users</a>';
115+
$links .= '<a href="?o=Posts"><i data-lucide="file-text"></i> Posts</a>';
116+
$links .= '<a href="?o=Categories"><i data-lucide="tags"></i> Categories</a>';
117+
}
118+
119+
// Layout options
120+
$links .= '<div class="dropdown-divider"></div>';
121+
foreach ($this->ctx->themes as $p) {
122+
$active = $t === $p[2] ? ' class="active"' : '';
123+
$links .= "<a href=\"?t={$p[2]}\"$active><i data-lucide=\"{$p[0]}\"></i> {$p[1]}</a>";
124+
}
125+
126+
// Color options
127+
$links .= '<div class="dropdown-divider"></div>';
128+
$links .= '<a href="#" data-scheme="default"><i data-lucide="circle"></i> Stone</a>';
129+
$links .= '<a href="#" data-scheme="ocean"><i data-lucide="waves"></i> Ocean</a>';
130+
$links .= '<a href="#" data-scheme="forest"><i data-lucide="trees"></i> Forest</a>';
131+
$links .= '<a href="#" data-scheme="sunset"><i data-lucide="sunset"></i> Sunset</a>';
132+
133+
// Logout
134+
$links .= '<div class="dropdown-divider"></div>';
135+
$links .= '<a href="?o=Auth&m=logout"><i data-lucide="log-out"></i> Logout</a>';
136+
137+
return "<div class=\"dropdown dropdown-end\"><span class=\"dropdown-toggle\"><i data-lucide=\"circle-user\"></i> $name</span><div class=\"dropdown-menu\">$links</div></div>";
138+
}
139+
97140
protected function html(string $theme, string $body): string
98141
{
99142
$flash = $this->flash();
@@ -110,7 +153,7 @@ protected function html(string $theme, string $body): string
110153
<link rel="stylesheet" href="/base.css">
111154
<link rel="stylesheet" href="/site.css">
112155
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
113-
<script>(function(){const t=localStorage.getItem("base-theme"),s=localStorage.getItem("base-scheme"),c=t||(matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");document.documentElement.className=c+(s&&s!=="default"?" scheme-"+s:"")})();</script>
156+
<script>(function(){const o=JSON.parse(localStorage.getItem("base-state")||"{}"),c=o.theme||(matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light");document.documentElement.className=c+(o.scheme&&o.scheme!=="default"?" scheme-"+o.scheme:"")})();</script>
114157
$css
115158
</head>
116159
<body>

docs/base.css

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
/* === CSS Reset (~50 lines) === */
7777
@layer reset {
7878
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
79+
html.preload, html.preload *, html.preload *::before, html.preload *::after { transition: none !important; }
7980

8081
html {
8182
font-size: 100%;
@@ -123,8 +124,9 @@
123124
h1, h2, h3, h4 { line-height: var(--leading-tight); font-weight: 600; text-wrap: balance; }
124125
h1 { font-size: var(--text-2xl); margin-block-end: var(--space-4); }
125126
h2 { font-size: var(--text-xl); margin-block-end: var(--space-3); }
126-
h3 { font-size: var(--text-lg); margin-block-end: var(--space-2); }
127-
h4 { font-size: var(--text-base); margin-block-end: var(--space-2); }
127+
h3 { font-size: var(--text-lg); margin-block: var(--space-4) var(--space-2); }
128+
h4 { font-size: var(--text-base); margin-block: var(--space-3) var(--space-2); }
129+
:is(h2, p) + h3 { margin-block-start: var(--space-2); }
128130

129131
p { margin-block-end: var(--space-4); text-wrap: pretty; }
130132

@@ -191,7 +193,7 @@
191193
cursor: pointer;
192194
transition: transform var(--transition-slow), box-shadow var(--transition-slow), background-color var(--transition-slow);
193195
background-color: var(--accent);
194-
color: white;
196+
color: var(--accent-fg);
195197
}
196198

197199
.btn:hover { background-color: var(--accent-hover); text-decoration: none; }
@@ -388,12 +390,13 @@
388390
.sidebar nav { padding: var(--space-2) 0; display: flex; flex-direction: column; align-items: stretch; border: none; }
389391
.sidebar nav a {
390392
display: flex; align-items: center; gap: var(--space-3);
391-
padding: var(--space-2) var(--space-3);
393+
padding: var(--space-2) var(--space-3); padding-inline-start: calc(var(--space-3) - 3px);
394+
border-inline-start: 3px solid transparent;
392395
color: var(--fg-primary); font-weight: normal;
393-
transition: background var(--transition-fast), color var(--transition-fast);
396+
transition: background var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast);
394397
}
395-
.sidebar nav a:hover { background: var(--bg-primary); border-inline-start: 3px solid var(--fg-muted); padding-inline-start: calc(var(--space-3) - 3px); text-decoration: none; }
396-
.sidebar nav a.active { background: var(--bg-primary); border-inline-start: 3px solid var(--accent); padding-inline-start: calc(var(--space-3) - 3px); color: var(--accent); }
398+
.sidebar nav a:hover { background: var(--bg-primary); border-color: var(--fg-muted); text-decoration: none; }
399+
.sidebar nav a.active { background: var(--bg-primary); border-color: var(--accent); color: var(--accent); }
397400

398401
.sidebar-divider { height: 1px; background: var(--border); margin-block: var(--space-2); }
399402

docs/base.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,11 @@ const Base = {
7575
document.body.classList.add('sidebar-open');
7676
this.state({ [side + 'Open']: true });
7777
} else {
78-
sb.classList.remove('open');
78+
// Also unpin if pinned
79+
sb.classList.remove('open', 'pinned');
80+
document.body.classList.remove(side + '-pinned');
7981
if (!document.querySelector('.sidebar.open')) document.body.classList.remove('sidebar-open');
80-
this.state({ [side + 'Open']: false });
82+
this.state({ [side + 'Open']: false, [side + 'Pinned']: false });
8183
}
8284
},
8385

@@ -200,8 +202,25 @@ const Base = {
200202
}
201203
});
202204

205+
// Responsive: hide pinned sidebars when viewport shrinks to mobile
206+
matchMedia('(min-width: 1280px)').addEventListener('change', e => {
207+
if (!e.matches) {
208+
// Viewport went below desktop - close all sidebars
209+
document.querySelectorAll('.sidebar.open').forEach(sb => {
210+
sb.classList.remove('open', 'pinned');
211+
});
212+
document.body.classList.remove('left-pinned', 'right-pinned', 'sidebar-open');
213+
} else {
214+
// Viewport went to desktop - restore pinned state
215+
this.restore();
216+
}
217+
});
218+
203219
// Lucide icons
204220
if (typeof lucide !== 'undefined') lucide.createIcons();
221+
222+
// Remove preload class to enable transitions (after state restored)
223+
requestAnimationFrame(() => document.documentElement.classList.remove('preload'));
205224
}
206225
};
207226

0 commit comments

Comments
 (0)