Skip to content

Commit 8b07642

Browse files
committed
feat: redesign dashboard and config editor
Replace the monolithic dashboard modal form with a component-based architecture and full-page editor. Dashboard: - 2-column card grid with staggered fade-in animations - ConfigCard: subtle per-card accent color with ambient top glow, terminal-style install command with blinking cursor, large stat numbers - ContextMenu: three-dot dropdown replaces 6-button row per card - Gradient text page title, cleaner empty state Config Editor (new route /dashboard/edit/[slug]): - Full-page layout with 4 visual sections (Identity/Stack/System/Script) - Identity: gradient background + glassmorphism card, 2.2rem inline title - Stack: unified Homebrew+npm search with colored type badges (formula/ cask/npm/tap), preset pills, brewfile import inline - System: dotfiles input above macOS prefs, catalog auto-opens when empty - Script: terminal-styled code editor - SectionNav: floating dot navigation with IntersectionObserver - PackageDna: SVG dot-matrix visualization in editor header Dashboard page reduced from 2200 lines to ~350 lines by extracting 6 focused components.
1 parent 23a3891 commit 8b07642

File tree

10 files changed

+3508
-1495
lines changed

10 files changed

+3508
-1495
lines changed

src/lib/components/ConfigCard.svelte

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

src/lib/components/ConfigEditor.svelte

Lines changed: 1404 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<script lang="ts">
2+
interface MenuItem {
3+
label: string;
4+
action: string;
5+
danger?: boolean;
6+
}
7+
8+
let { items, onselect }: {
9+
items: MenuItem[];
10+
onselect: (action: string) => void;
11+
} = $props();
12+
13+
let open = $state(false);
14+
let menuEl: HTMLDivElement | undefined = $state(undefined);
15+
16+
function toggle(e: MouseEvent) {
17+
e.stopPropagation();
18+
e.preventDefault();
19+
open = !open;
20+
}
21+
22+
function select(action: string, e: MouseEvent) {
23+
e.stopPropagation();
24+
e.preventDefault();
25+
open = false;
26+
onselect(action);
27+
}
28+
29+
function handleClickOutside(e: MouseEvent) {
30+
if (open && menuEl && !menuEl.contains(e.target as Node)) {
31+
open = false;
32+
}
33+
}
34+
</script>
35+
36+
<svelte:window onclick={handleClickOutside} />
37+
38+
<div class="ctx" bind:this={menuEl}>
39+
<button class="trigger" onclick={toggle} aria-label="More actions">
40+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
41+
<circle cx="8" cy="3" r="1.5" />
42+
<circle cx="8" cy="8" r="1.5" />
43+
<circle cx="8" cy="13" r="1.5" />
44+
</svg>
45+
</button>
46+
{#if open}
47+
<div class="dropdown">
48+
{#each items as item, i}
49+
{#if i > 0 && item.danger && !items[i - 1]?.danger}
50+
<div class="divider"></div>
51+
{/if}
52+
<button
53+
class="menu-item"
54+
class:danger={item.danger}
55+
onclick={(e) => select(item.action, e)}
56+
>
57+
{item.label}
58+
</button>
59+
{/each}
60+
</div>
61+
{/if}
62+
</div>
63+
64+
<style>
65+
.ctx {
66+
position: relative;
67+
}
68+
69+
.trigger {
70+
display: flex;
71+
align-items: center;
72+
justify-content: center;
73+
width: 32px;
74+
height: 32px;
75+
background: none;
76+
border: 1px solid transparent;
77+
border-radius: 8px;
78+
color: var(--text-muted);
79+
cursor: pointer;
80+
transition: all 0.15s;
81+
}
82+
83+
.trigger:hover {
84+
background: var(--bg-tertiary);
85+
border-color: var(--border);
86+
color: var(--text-secondary);
87+
}
88+
89+
.dropdown {
90+
position: absolute;
91+
top: calc(100% + 4px);
92+
right: 0;
93+
min-width: 170px;
94+
background: var(--bg-secondary);
95+
border: 1px solid var(--border);
96+
border-radius: 12px;
97+
padding: 4px;
98+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.03);
99+
z-index: 100;
100+
animation: menuIn 0.15s ease-out;
101+
}
102+
103+
@keyframes menuIn {
104+
from { opacity: 0; transform: translateY(-4px) scale(0.97); }
105+
to { opacity: 1; transform: translateY(0) scale(1); }
106+
}
107+
108+
.divider {
109+
height: 1px;
110+
background: var(--border);
111+
margin: 4px 8px;
112+
}
113+
114+
.menu-item {
115+
display: block;
116+
width: 100%;
117+
padding: 10px 14px;
118+
background: none;
119+
border: none;
120+
border-radius: 8px;
121+
color: var(--text-primary);
122+
font-size: 0.85rem;
123+
font-weight: 500;
124+
font-family: inherit;
125+
text-align: left;
126+
cursor: pointer;
127+
transition: background 0.12s;
128+
}
129+
130+
.menu-item:hover {
131+
background: var(--bg-tertiary);
132+
}
133+
134+
.menu-item.danger {
135+
color: var(--danger);
136+
}
137+
138+
.menu-item.danger:hover {
139+
background: rgba(239, 68, 68, 0.1);
140+
}
141+
</style>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script lang="ts">
2+
interface Package {
3+
name: string;
4+
type: string;
5+
}
6+
7+
let { packages = [] }: { packages: Package[] } = $props();
8+
9+
const cols = 10;
10+
const rows = 4;
11+
const size = 7;
12+
const gap = 3;
13+
const totalCells = cols * rows;
14+
const w = cols * (size + gap) - gap;
15+
const h = rows * (size + gap) - gap;
16+
17+
function getColor(type: string): string {
18+
if (type === 'cask') return '#3b82f6';
19+
if (type === 'npm') return '#f97316';
20+
return '#22c55e';
21+
}
22+
23+
// Group by type for visual clustering
24+
const sorted = $derived([
25+
...packages.filter(p => p.type === 'cask'),
26+
...packages.filter(p => p.type !== 'cask' && p.type !== 'npm'),
27+
...packages.filter(p => p.type === 'npm'),
28+
]);
29+
</script>
30+
31+
<svg class="dna" viewBox="0 0 {w} {h}" width={w} height={h} aria-label="Package DNA visualization">
32+
{#each Array(totalCells) as _, i}
33+
{@const x = (i % cols) * (size + gap)}
34+
{@const y = Math.floor(i / cols) * (size + gap)}
35+
{@const pkg = sorted[i]}
36+
<rect
37+
{x} {y}
38+
width={size} height={size}
39+
rx="1.5"
40+
fill={pkg ? getColor(pkg.type) : 'currentColor'}
41+
opacity={pkg ? 0.9 : 0.07}
42+
>
43+
{#if pkg}
44+
<title>{pkg.name} ({pkg.type})</title>
45+
{/if}
46+
</rect>
47+
{/each}
48+
</svg>
49+
50+
<style>
51+
.dna {
52+
display: block;
53+
color: var(--text-muted);
54+
}
55+
</style>

0 commit comments

Comments
 (0)