Skip to content

Commit 4246fa3

Browse files
committed
feat(docs): redesign CodeFirst as VS Code-style editor with file tree
Shows the full VSA feature-folder structure as a real-feeling IDE pane: - Title bar: mac traffic lights, breadcrumb path (Modules.Identity / Features / v1 / Users / RegisterUser / <active>.cs), C# lang badge - Left column (file tree): the Users/ folder with RegisterUser/ expanded showing its three files, and five other collapsed feature folders (LoginUser, AssignUserRole, ChangePassword, DeleteUser, RefreshToken) — visually demonstrates that each feature is its own folder with its own files - Active file: green left-border accent + tinted background + primary text color. Inactive: muted with hover row tint - Right column: code panel with EC frame=none (chrome stripped, just the syntax-highlighted body) - Status footer: project name, encoding, line count updates with selection - Breadcrumb's filename and line count both swap on click - Hover gradient ring behind the editor (mask-composite trick) - Mobile: tree stacks above code with bottom border instead of right - Indentation guide line (1px vertical) inside the RegisterUser folder - Chevrons + folder icons via inline SVGs (lucide-shaped) — no astro-icon dep needed for the section
1 parent 9846e0c commit 4246fa3

1 file changed

Lines changed: 185 additions & 79 deletions

File tree

docs/src/components/landing/CodeFirst.astro

Lines changed: 185 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,43 @@ const validatorCode = `public sealed class RegisterUserCommandValidator
3232
}
3333
}`;
3434
35+
// Active files (clickable). Order matters for indices.
3536
const files = [
36-
{ name: 'RegisterUserEndpoint.cs', code: endpointCode, lines: 9 },
37-
{ name: 'RegisterUserCommandHandler.cs', code: handlerCode, lines: 7 },
38-
{ name: 'RegisterUserCommandValidator.cs', code: validatorCode, lines: 10 },
37+
{
38+
name: 'RegisterUserEndpoint.cs',
39+
short: 'Endpoint.cs',
40+
lines: 9,
41+
code: endpointCode,
42+
},
43+
{
44+
name: 'RegisterUserCommandHandler.cs',
45+
short: 'Handler.cs',
46+
lines: 7,
47+
code: handlerCode,
48+
},
49+
{
50+
name: 'RegisterUserCommandValidator.cs',
51+
short: 'Validator.cs',
52+
lines: 10,
53+
code: validatorCode,
54+
},
55+
];
56+
57+
// Collapsed feature folders (visual-only — show the breadth of VSA)
58+
const otherFeatures = [
59+
'LoginUser',
60+
'AssignUserRole',
61+
'ChangePassword',
62+
'DeleteUser',
63+
'RefreshToken',
3964
];
4065
---
4166
<section class="relative border-t border-border bg-subtle/40">
4267
<div class="section-dots absolute inset-0 -z-10 opacity-60" aria-hidden="true"></div>
4368

44-
<div class="mx-auto max-w-6xl px-6 py-24 sm:py-32">
69+
<div class="mx-auto max-w-7xl px-6 py-24 sm:py-32">
4570
<div class="grid items-start gap-12 lg:grid-cols-[minmax(0,_5fr)_minmax(0,_7fr)] lg:gap-16">
46-
<!-- Left: editorial copy -->
71+
<!-- LEFT: editorial copy -->
4772
<div class="lg:sticky lg:top-24">
4873
<p class="text-[11px] font-medium uppercase tracking-[0.18em] text-primary">Vertical Slice Architecture</p>
4974
<h2 class="mt-4 font-display text-4xl font-semibold leading-[1.05] tracking-[-0.03em] text-foreground sm:text-5xl">
@@ -82,60 +107,128 @@ const files = [
82107
</a>
83108
</div>
84109

85-
<!-- Right: custom-framed editor window -->
86-
<div class="code-window relative">
87-
<!-- Glow behind window (only intense on hover) -->
88-
<div class="code-window-glow" aria-hidden="true"></div>
110+
<!-- RIGHT: VS-Code-style editor with file tree + code -->
111+
<div class="editor relative">
112+
<!-- Glow behind the window (visible on hover) -->
113+
<div class="editor-glow" aria-hidden="true"></div>
89114

90-
<div class="code-window-frame relative overflow-hidden rounded-xl border border-border bg-card shadow-[0_30px_60px_-30px_rgba(21,128,61,0.18),0_8px_24px_-12px_rgba(0,0,0,0.08)] dark:shadow-[0_30px_60px_-30px_rgba(74,222,128,0.12),0_8px_24px_-12px_rgba(0,0,0,0.5)]">
91-
<!-- Title bar with traffic lights + path + langauge badge -->
92-
<div class="flex items-center gap-3 border-b border-border bg-subtle/50 px-4 py-2.5">
115+
<div class="editor-frame relative overflow-hidden rounded-xl border border-border bg-card shadow-[0_30px_60px_-30px_rgba(21,128,61,0.18),0_8px_24px_-12px_rgba(0,0,0,0.08)] dark:shadow-[0_30px_60px_-30px_rgba(74,222,128,0.12),0_8px_24px_-12px_rgba(0,0,0,0.5)]">
116+
<!-- Title bar -->
117+
<div class="flex items-center gap-3 border-b border-border bg-subtle/60 px-4 py-2.5">
93118
<!-- Traffic lights -->
94119
<div class="flex items-center gap-1.5" aria-hidden="true">
95120
<span class="size-3 rounded-full bg-[#ff5f57]"></span>
96121
<span class="size-3 rounded-full bg-[#febc2e]"></span>
97122
<span class="size-3 rounded-full bg-[#28c840]"></span>
98123
</div>
99-
<!-- Path -->
100-
<p class="hidden flex-1 truncate font-mono text-[11.5px] text-muted-foreground sm:block">
101-
Modules.Identity / Features / v1 / Users / <span class="text-foreground">RegisterUser</span> /
124+
<!-- Breadcrumb path (updates with active file) -->
125+
<p class="hidden min-w-0 flex-1 truncate font-mono text-[11.5px] text-muted-foreground sm:block">
126+
<span>Modules.Identity</span>
127+
<span class="text-border">/</span>
128+
<span>Features</span>
129+
<span class="text-border">/</span>
130+
<span>v1</span>
131+
<span class="text-border">/</span>
132+
<span>Users</span>
133+
<span class="text-border">/</span>
134+
<span class="text-foreground/80">RegisterUser</span>
135+
<span class="text-border">/</span>
136+
<span class="text-foreground" data-vsa-path>RegisterUserEndpoint.cs</span>
102137
</p>
103-
<!-- C# language badge -->
104-
<span class="inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-2 py-0.5 font-mono text-[10px] font-semibold tracking-wide text-muted-foreground">
138+
<!-- Language badge -->
139+
<span class="ml-auto inline-flex items-center gap-1.5 rounded-full border border-border bg-card px-2 py-0.5 font-mono text-[10px] font-semibold tracking-wide text-muted-foreground">
105140
<span class="size-1.5 rounded-full bg-primary"></span>
106141
C#
107142
</span>
108143
</div>
109144

110-
<!-- File tabs -->
111-
<div role="tablist" class="flex overflow-x-auto border-b border-border bg-subtle/30 px-1.5 pt-1.5 no-scrollbar">
112-
{files.map((f, i) => (
113-
<button
114-
type="button"
115-
role="tab"
116-
data-cf-tab={i}
117-
aria-selected={i === 0 ? 'true' : 'false'}
118-
class="cf-tab relative shrink-0 rounded-t-md px-3.5 py-2 font-mono text-[11.5px] text-muted-foreground transition-colors aria-selected:text-foreground"
119-
>
120-
<span class="aria-selected:text-foreground">{f.name}</span>
121-
</button>
122-
))}
123-
</div>
145+
<!-- Editor body: file tree + code -->
146+
<div class="grid grid-cols-1 md:grid-cols-[200px_minmax(0,_1fr)]">
147+
<!-- File tree -->
148+
<aside class="file-tree border-r-0 border-b border-border bg-subtle/30 py-3 font-mono text-[12px] md:border-b-0 md:border-r">
149+
<ul class="space-y-0.5">
150+
<!-- Users folder header -->
151+
<li>
152+
<div class="flex items-center gap-1.5 px-3 py-1 text-muted-foreground">
153+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
154+
<polyline points="6 9 12 15 18 9"></polyline>
155+
</svg>
156+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="text-primary/70">
157+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
158+
</svg>
159+
<span class="text-foreground">Users</span>
160+
</div>
161+
</li>
162+
163+
<!-- RegisterUser/ — expanded, active feature folder -->
164+
<li class="relative">
165+
<!-- Vertical indent guide -->
166+
<span aria-hidden="true" class="pointer-events-none absolute left-[26px] top-7 bottom-1 w-px bg-border"></span>
167+
168+
<div class="flex items-center gap-1.5 px-3 py-1 pl-5 text-foreground">
169+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
170+
<polyline points="6 9 12 15 18 9"></polyline>
171+
</svg>
172+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="text-primary">
173+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
174+
</svg>
175+
<span class="font-medium">RegisterUser</span>
176+
</div>
124177

125-
<!-- Code panels (no EC frame) -->
126-
{files.map((f, i) => (
127-
<div data-cf-panel={i} class={`cf-panel ${i === 0 ? '' : 'hidden'}`}>
128-
<Code code={f.code} lang="csharp" frame="none" />
178+
<ul class="ml-2">
179+
{files.map((f, i) => (
180+
<li>
181+
<button
182+
type="button"
183+
data-vsa-tab={i}
184+
aria-selected={i === 0 ? 'true' : 'false'}
185+
class="vsa-file group flex w-full items-center gap-2 rounded-md px-3 py-1 pl-9 text-left text-muted-foreground transition-colors aria-selected:bg-primary/10 aria-selected:text-primary hover:bg-foreground/5"
186+
>
187+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="opacity-70">
188+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
189+
<polyline points="14 2 14 8 20 8"></polyline>
190+
</svg>
191+
<span class="truncate">{f.short}</span>
192+
</button>
193+
</li>
194+
))}
195+
</ul>
196+
</li>
197+
198+
<!-- Other feature folders (collapsed, visual only) -->
199+
{otherFeatures.map((feature) => (
200+
<li>
201+
<div class="flex items-center gap-1.5 rounded-md px-3 py-1 pl-5 text-muted-foreground/80 transition-colors hover:bg-foreground/[0.04]">
202+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="opacity-60">
203+
<polyline points="9 18 15 12 9 6"></polyline>
204+
</svg>
205+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="opacity-70">
206+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
207+
</svg>
208+
<span class="truncate">{feature}</span>
209+
</div>
210+
</li>
211+
))}
212+
</ul>
213+
</aside>
214+
215+
<!-- Code panels -->
216+
<div class="relative">
217+
{files.map((f, i) => (
218+
<div data-vsa-panel={i} class={`vsa-panel ${i === 0 ? '' : 'hidden'}`}>
219+
<Code code={f.code} lang="csharp" frame="none" />
220+
</div>
221+
))}
129222
</div>
130-
))}
223+
</div>
131224

132225
<!-- Status footer -->
133-
<div class="flex items-center justify-between border-t border-border bg-subtle/30 px-4 py-2 font-mono text-[10.5px] text-muted-foreground">
226+
<div class="flex items-center justify-between border-t border-border bg-subtle/40 px-4 py-2 font-mono text-[10.5px] text-muted-foreground">
134227
<span class="inline-flex items-center gap-3">
135228
<span class="inline-flex items-center gap-1.5"><span class="size-1.5 rounded-full bg-primary"></span>vsa</span>
136229
<span class="hidden sm:inline">utf-8 · LF · spaces: 4</span>
137230
</span>
138-
<span data-cf-lineinfo>{files[0].lines} lines</span>
231+
<span data-vsa-lineinfo>{files[0].lines} lines</span>
139232
</div>
140233
</div>
141234
</div>
@@ -144,74 +237,87 @@ const files = [
144237
</section>
145238

146239
<style>
147-
/* Tab active-state — green underline that animates between tabs. We use
148-
box-shadow as the underline so we can transition it smoothly, and add
149-
a subtle background tint on the active tab. */
150-
.cf-tab[aria-selected='true'] {
151-
background-color: var(--card);
152-
box-shadow: inset 0 -2px 0 0 var(--primary);
153-
}
154-
.cf-tab[aria-selected='false']:hover {
155-
color: var(--foreground);
156-
background-color: color-mix(in oklab, var(--foreground) 4%, transparent);
157-
}
158-
159-
/* Window glow — appears on hover. Sits behind the frame and uses the brand
160-
gradient. Designed to feel subtle, not the cliché "neon glow" trope. */
161-
.code-window-glow {
240+
/* Hover glow behind the editor — same idiom as the rest of the site */
241+
.editor-glow {
162242
position: absolute;
163243
inset: -1px;
164244
border-radius: 0.85rem;
165245
padding: 1px;
166-
background: linear-gradient(135deg, transparent, color-mix(in oklab, var(--primary) 28%, transparent), transparent);
246+
background: linear-gradient(135deg, transparent, color-mix(in oklab, var(--primary) 30%, transparent), transparent);
167247
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
168248
-webkit-mask-composite: xor;
169249
mask-composite: exclude;
170250
opacity: 0;
171251
transition: opacity 320ms ease;
172252
pointer-events: none;
173253
}
174-
.code-window:hover .code-window-glow {
254+
.editor:hover .editor-glow {
175255
opacity: 1;
176256
}
177257

178-
/* Tame Expressive Code's default outer frame so it integrates with our
179-
custom chrome cleanly. Targeting only inside this section to avoid
180-
bleeding into docs pages. */
181-
.code-window-frame :global(.expressive-code) {
258+
/* Active file row: subtle left-border accent that animates in */
259+
.vsa-file[aria-selected='true'] {
260+
position: relative;
261+
}
262+
.vsa-file[aria-selected='true']::before {
263+
content: '';
264+
position: absolute;
265+
left: 0;
266+
top: 4px;
267+
bottom: 4px;
268+
width: 2px;
269+
border-radius: 0 2px 2px 0;
270+
background: var(--primary);
271+
}
272+
273+
/* Tame Expressive Code inside the panel (no chrome, just code body) */
274+
.vsa-panel :global(.expressive-code) {
182275
margin: 0;
183-
border-radius: 0;
184276
}
185-
.code-window-frame :global(.expressive-code pre) {
186-
border: 0;
187-
border-radius: 0;
277+
.vsa-panel :global(.expressive-code pre) {
278+
border: 0 !important;
279+
border-radius: 0 !important;
188280
background-color: var(--card) !important;
189281
}
190-
.code-window-frame :global(.expressive-code .frame.has-title) {
191-
--code-background: var(--card);
282+
.vsa-panel :global(.expressive-code pre code) {
283+
padding-block: 1rem !important;
284+
padding-inline: 1.25rem !important;
192285
}
193-
/* Reduced motion */
286+
194287
@media (prefers-reduced-motion: reduce) {
195-
.code-window-glow { transition: none; }
288+
.editor-glow { transition: none; }
196289
}
197290
</style>
198291

199-
<script is:inline define:vars={{ lineCounts: files.map(f => f.lines) }}>
200-
(() => {
201-
const tabs = document.querySelectorAll('[data-cf-tab]');
202-
const panels = document.querySelectorAll('[data-cf-panel]');
203-
const info = document.querySelector('[data-cf-lineinfo]');
292+
<script is:inline>
293+
(function () {
294+
var lineCounts = [9, 7, 10];
295+
var fileNames = [
296+
'RegisterUserEndpoint.cs',
297+
'RegisterUserCommandHandler.cs',
298+
'RegisterUserCommandValidator.cs',
299+
];
300+
204301
function select(idx) {
205-
tabs.forEach((t) => t.setAttribute('aria-selected', t.getAttribute('data-cf-tab') === String(idx) ? 'true' : 'false'));
206-
panels.forEach((p) => {
207-
if (p.getAttribute('data-cf-panel') === String(idx)) p.classList.remove('hidden');
302+
document.querySelectorAll('[data-vsa-tab]').forEach(function (t) {
303+
t.setAttribute(
304+
'aria-selected',
305+
t.getAttribute('data-vsa-tab') === String(idx) ? 'true' : 'false',
306+
);
307+
});
308+
document.querySelectorAll('[data-vsa-panel]').forEach(function (p) {
309+
if (p.getAttribute('data-vsa-panel') === String(idx)) p.classList.remove('hidden');
208310
else p.classList.add('hidden');
209311
});
210-
if (info) info.textContent = `${lineCounts[idx]} lines`;
312+
var info = document.querySelector('[data-vsa-lineinfo]');
313+
if (info) info.textContent = lineCounts[idx] + ' lines';
314+
var path = document.querySelector('[data-vsa-path]');
315+
if (path) path.textContent = fileNames[idx];
211316
}
212-
tabs.forEach((t) => {
213-
const idx = t.getAttribute('data-cf-tab');
214-
t.addEventListener('click', () => select(Number(idx)));
317+
318+
document.querySelectorAll('[data-vsa-tab]').forEach(function (t) {
319+
var idx = t.getAttribute('data-vsa-tab');
320+
t.addEventListener('click', function () { select(Number(idx)); });
215321
});
216322
})();
217323
</script>

0 commit comments

Comments
 (0)