diff --git a/package-lock.json b/package-lock.json index df049bd..af6afb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,7 @@ "name": "openboot-web", "version": "0.0.1", "dependencies": { - "@cf-wasm/resvg": "^0.3.3", - "@codemirror/commands": "^6.10.3", - "@codemirror/language": "^6.12.2", - "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/state": "^6.6.0", - "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.0" + "@cf-wasm/resvg": "^0.3.3" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.16.6", @@ -252,74 +246,6 @@ "dev": true, "license": "MIT OR Apache-2.0" }, - "node_modules/@codemirror/commands": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", - "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.6.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", - "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/legacy-modes": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz", - "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0" - } - }, - "node_modules/@codemirror/state": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", - "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", - "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.43.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.0.tgz", - "integrity": "sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.6.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1568,36 +1494,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lezer/common": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", - "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", - "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -3060,12 +2956,6 @@ "node": ">= 0.6" } }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5108,12 +4998,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", - "license": "MIT" - }, "node_modules/supports-color": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", @@ -5718,12 +5602,6 @@ } } }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", diff --git a/package.json b/package.json index 4161c1e..e82bb9d 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,6 @@ "wrangler": "^4.61.1" }, "dependencies": { - "@cf-wasm/resvg": "^0.3.3", - "@codemirror/commands": "^6.10.3", - "@codemirror/language": "^6.12.2", - "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/state": "^6.6.0", - "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.43.0" + "@cf-wasm/resvg": "^0.3.3" } } diff --git a/src/lib/components/ConfigDetail.svelte b/src/lib/components/ConfigDetail.svelte index 2afabdc..d72a9a7 100644 --- a/src/lib/components/ConfigDetail.svelte +++ b/src/lib/components/ConfigDetail.svelte @@ -1,6 +1,6 @@
-
- - -
- {#if configUser.avatar_url} - {configUser.username} - {:else} -
{configUser.username.charAt(0).toUpperCase()}
- {/if} -
- @{configUser.username} - / - {config.slug} -
+
+
+ + + Back to explore + +
-

{config.name}

- {#if config.description} -

{config.description}

- {/if} - -
-
- $ {getCurlCommand()} - -
-
- Already have OpenBoot? - {getInstallCommand()} -
+ +

{config.name}

+ {#if config.description} +

{config.description}

+ {/if} + +
+ + + {config.install_count || 0} installs + + {#if formatDate(config.updated_at)} + updated {formatDate(config.updated_at)} {/if} - -
-
- - -
- {#if forkError} -
{forkError}
- {/if} -
+ {#if config.base_preset} + base preset {config.base_preset} + {/if} +
-
-
-
{casks.length}
-
Apps
-
-
-
{formulae.length}
-
CLI
+ {#if dna.length > 0} +
+ {#each dna as seg} + + {/each} +
+
+ {#each dnaLegend as l} + {l.n} {l.label} + {/each} +
+ {/if}
- {#if devTools.length > 0} -
-
{devTools.length}
-
Dev
+ + +
+
+
Install this setup
+ + + {#if forkError} +

{forkError}

+ {/if} +

+ Already have OpenBoot?
{getInstallCommand()} +

- {/if} - {#if macosPrefs.length > 0} -
-
{macosPrefs.length}
-
Prefs
+ +
+
Visibility{visibility}
+ {#if config.base_preset} +
Base preset{config.base_preset}
+ {/if} + {#if config.dotfiles_repo} + + {/if} +
macOS prefs{macosPrefs.length} applied
- {/if} -
-
{config.install_count || 0}
-
Installs
-
- -
- {#if casks.length > 0} -
-

📱 Applications

- - {#if casks.length > 12} - - {/if} -
- {/if} - - {#if devTools.length > 0} -
-

⚙️ Development Tools

-
- {#each devTools as tool, i} -
-
- {tool.charAt(0).toUpperCase()} -
-
{tool}
-
- {/each} -
-
- {/if} - - {#if formulae.length > 0} -
-

⌘ CLI Tools

-
- {#each displayedCli as pkg} - {displayName(pkg)} - {/each} -
- {#if formulae.length > 24} - - {/if} -
- {/if} - - {#if configNpm.length > 0} -
-

📦 NPM Packages

-
- {#each configNpm as pkg} - {pkg.name} - {/each} -
-
- {/if} - -
-

🔧 Configuration

- - {#if config.dotfiles_repo} -
- - 📁 - Dotfiles Repository - -
- - {config.dotfiles_repo} - - -

Deployed automatically via GNU Stow

+ +
+ {#if configCli.length > 0} +
+

Formulae {configCli.length}

+ -
+
{/if} - {#if shell.default || shell.oh_my_zsh} -
- - 🐚 - Shell Setup - -
-
- {#if shell.default} -
- Shell - {shell.default} -
- {/if} - {#if shell.oh_my_zsh} -
- Oh My Zsh - Installed -
- {/if} - {#if shell.theme} -
- Theme - {shell.theme} -
- {/if} -
- {#if shell.plugins && shell.plugins.length > 0} -
-
Plugins ({shell.plugins.length})
-
- {#each shell.plugins as plugin} - {plugin} - {/each} -
-
- {/if} + {#if configApps.length > 0} +
+

Casks {configApps.length}

+ -
+ {/if} - {#if Object.keys(git).length > 0} -
- - 🔀 - Git Configuration - -
-
- {#each Object.entries(git) as [key, value]} -
- {key} - {value} -
- {/each} -
+ {#if configNpm.length > 0} +
+

NPM globals {configNpm.length}

+
+ {#each configNpm as p} + {p.name} + {/each}
-
+ {/if} - {#if config.custom_script} -
- - - Custom Installation Script - -
-
- custom-script.sh - -
-
{@html highlightBash(config.custom_script)}
+ {#if configTaps.length > 0} +
+

Homebrew taps {configTaps.length}

+
+ {#each configTaps as p} + {p.name} + {/each}
-
+ {/if} - - - {#if macosPrefs.length > 0} -
-

🍎 macOS Preferences

- -
- {/if} - - {#if taps.length > 0} -
-

📋 Additional

-
- - 🚰 - Homebrew Taps ({taps.length}) - -
-
- {#each taps as tap} -
{tap}
+ + {#if config.custom_script} +
+

Custom script

+
+
runs after packages install
+
+ {#each config.custom_script.split('\n') as line} +
{@html scriptLine(line)}
{/each}
-
-
- {/if} - -
-
-
-

Create Your Own Configuration

-

Build and share your perfect dev environment in minutes

-
- - Get Started - - -
-
+ + {/if} + + {#if macosPrefs.length > 0} +
+

macOS preferences {macosPrefs.length}

+ +
+ {/if} + + {#if shell.default || shell.oh_my_zsh || Object.keys(git).length > 0} +
+

Shell & Git

+
+ {#if shell.default}
Shell{shell.default}
{/if} + {#if shell.oh_my_zsh}
Oh My Zshinstalled
{/if} + {#if shell.theme}
Theme{shell.theme}
{/if} + {#each Object.entries(git) as [key, value]} +
git.{key}{value}
+ {/each} +
+
+ {/if} + + {#if devTools.length > 0} +
+

Dev tools {devTools.length}

+
+ {#each devTools as tool} + {tool} + {/each} +
+
+ {/if} +
+ diff --git a/src/lib/components/ConfigEditor.svelte b/src/lib/components/ConfigEditor.svelte index bf0fc05..20200c1 100644 --- a/src/lib/components/ConfigEditor.svelte +++ b/src/lib/components/ConfigEditor.svelte @@ -3,17 +3,8 @@ import { goto } from '$app/navigation'; import { auth } from '$lib/stores/auth'; import PackageManager from './PackageManager.svelte'; - import PackageDna from './PackageDna.svelte'; - import SectionNav from './SectionNav.svelte'; - import { - MACOS_PREF_CATALOG, - CATALOG_CATEGORIES, - getCatalogItem, - type MacOSPrefCatalogItem, - } from '$lib/macos-prefs-catalog'; - import { PRESET_PACKAGES } from '$lib/presets'; - import ShellEditor from './ShellEditor.svelte'; import MacOSPreferencesEditor from './MacOSPreferencesEditor.svelte'; + import { PRESET_PACKAGES } from '$lib/presets'; let { slug, skipAuth = false }: { slug?: string; skipAuth?: boolean } = $props(); const isNew = !slug; @@ -23,22 +14,10 @@ let error = $state(''); let editingConfig = $state(null); let initialSnapshot = $state(''); + let initialPayload: any = null; let rawMode = $state(false); let rawJson = $state(''); - function captureSnapshot(): string { - return JSON.stringify({ - formData, - packages: [...selectedPackages.entries()].sort(), - packageDescs: [...packageDescs.entries()].sort(), - macosPrefs, - }); - } - - const hasChanges = $derived(isNew || initialSnapshot === '' || rawMode || captureSnapshot() !== initialSnapshot); - let showScriptModal = $state(false); - let scriptDraft = $state(''); - let formData = $state({ name: '', description: '', @@ -46,7 +25,7 @@ visibility: 'unlisted' as string, alias: '', custom_script: '', - dotfiles_repo: '', + dotfiles_repo: '' }); let selectedPackages = $state(new Map()); @@ -65,80 +44,22 @@ host?: string; } let macosPrefs = $state([]); - let expandedPrefCats = $state>(new Set()); - - const packages = $derived( - Array.from(selectedPackages.entries()).map(([key, type]) => { - const name = key.slice(type.length + 1); - return { name, type, desc: packageDescs.get(key) || '' }; - }) - ); - - const sections = [ - { id: 'identity', label: 'Identity' }, - { id: 'stack', label: 'Stack' }, - { id: 'system', label: 'System' }, - { id: 'script', label: 'Script' }, - ]; - - // Gradient from config name (matches ConfigCard) - const GRADIENTS = [ - 'linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%)', - 'linear-gradient(135deg, #f43f5e 0%, #fb923c 50%, #fbbf24 100%)', - 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 50%, #8b5cf6 100%)', - 'linear-gradient(135deg, #10b981 0%, #06b6d4 50%, #3b82f6 100%)', - 'linear-gradient(135deg, #f97316 0%, #ef4444 50%, #ec4899 100%)', - 'linear-gradient(135deg, #8b5cf6 0%, #d946ef 50%, #f43f5e 100%)', - 'linear-gradient(135deg, #14b8a6 0%, #22c55e 50%, #84cc16 100%)', - 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 50%, #a855f7 100%)', - ]; - function hashStr(s: string): number { - let h = 0; - for (let i = 0; i < s.length; i++) { - h = ((h << 5) - h) + s.charCodeAt(i); - h |= 0; - } - return Math.abs(h); - } - const editorGradient = $derived( - formData.name - ? GRADIENTS[hashStr(formData.name) % GRADIENTS.length] - : GRADIENTS[0] - ); - - const catalogByCategory = $derived.by(() => { - const map: Record = {}; - for (const cat of CATALOG_CATEGORIES) { - map[cat] = MACOS_PREF_CATALOG.filter((i) => i.category === cat); - } - return map; - }); - const addedCountByCategory = $derived.by(() => { - const counts: Record = {}; - for (const pref of macosPrefs) { - const ci = getCatalogItem(pref.domain, pref.key); - const cat = ci?.category ?? 'Custom'; - counts[cat] = (counts[cat] || 0) + 1; - } - return counts; - }); + const scriptPlaceholder = + '#!/usr/bin/env bash\n# Runs after packages, shell, dotfiles, and macOS prefs.\n\n# npm install -g pnpm vercel\n# git config --global pull.rebase true'; - const customPrefs = $derived( - macosPrefs - .map((p, i) => ({ pref: p, index: i })) - .filter(({ pref }) => !getCatalogItem(pref.domain, pref.key)) - ); - - function initExpandedCats() { - const cats = new Set(); - for (const pref of macosPrefs) { - const ci = getCatalogItem(pref.domain, pref.key); - if (ci) cats.add(ci.category); - } - expandedPrefCats = cats.size > 0 ? cats : new Set([CATALOG_CATEGORIES[0]]); + function captureSnapshot(): string { + return JSON.stringify({ + formData, + packages: [...selectedPackages.entries()].sort(), + packageDescs: [...packageDescs.entries()].sort(), + macosPrefs + }); } + const dirty = $derived(initialSnapshot !== '' && captureSnapshot() !== initialSnapshot); + const canSave = $derived(isNew || dirty || rawMode); + function initPackagesForPreset(preset: string) { const p = PRESET_PACKAGES[preset]; if (!p) return; @@ -151,7 +72,12 @@ function handlePresetChange(preset: string) { formData.base_preset = preset; - initPackagesForPreset(preset); + if (preset === 'scratch') { + selectedPackages = new Map(); + packageDescs = new Map(); + } else { + initPackagesForPreset(preset); + } } function togglePackage(name: string, type: string, desc: string = '') { @@ -170,12 +96,25 @@ } function normalizePrefValue(type: string, value: string): string { - if (type === 'bool') { - return (value === '1' || value === 'true') ? 'true' : 'false'; - } + if (type === 'bool') return value === '1' || value === 'true' ? 'true' : 'false'; return value; } + function mapPrefs(raw: any[]): MacOSPref[] { + return raw.map((p: any) => { + const type = p.type || ''; + const out: MacOSPref = { + domain: p.domain || '', + key: p.key || '', + type, + value: normalizePrefValue(type, String(p.value ?? '')), + desc: p.desc || '' + }; + if (typeof p.host === 'string' && p.host !== '') out.host = p.host; + return out; + }); + } + onMount(async () => { if (!skipAuth) { await auth.check(); @@ -198,7 +137,7 @@ visibility: data.visibility || 'unlisted', alias: '', custom_script: data.custom_script || '', - dotfiles_repo: data.dotfiles_repo || '', + dotfiles_repo: data.dotfiles_repo || '' }; if (data.packages?.length) { const map = new Map(); @@ -218,21 +157,7 @@ } else { initPackagesForPreset(formData.base_preset); } - if (data.snapshot?.macos_prefs) { - macosPrefs = data.snapshot.macos_prefs.map((p: any) => { - const type = p.type || ''; - const out: MacOSPref = { - domain: p.domain || '', - key: p.key || '', - type, - value: normalizePrefValue(type, String(p.value ?? '')), - desc: p.desc || '', - }; - if (typeof p.host === 'string' && p.host !== '') out.host = p.host; - return out; - }); - } - initExpandedCats(); + if (data.snapshot?.macos_prefs) macosPrefs = mapPrefs(data.snapshot.macos_prefs); } catch { initPackagesForPreset('developer'); } @@ -253,23 +178,11 @@ visibility: config.visibility || 'unlisted', alias: config.alias || '', custom_script: config.custom_script || '', - dotfiles_repo: config.dotfiles_repo || '', + dotfiles_repo: config.dotfiles_repo || '' }; macosPrefs = Array.isArray(config.snapshot?.macos_prefs) - ? config.snapshot.macos_prefs.map((p: any) => { - const type = p.type || ''; - const out: MacOSPref = { - domain: p.domain || '', - key: p.key || '', - type, - value: normalizePrefValue(type, String(p.value ?? '')), - desc: p.desc || '', - }; - if (typeof p.host === 'string' && p.host !== '') out.host = p.host; - return out; - }) + ? mapPrefs(config.snapshot.macos_prefs) : []; - initExpandedCats(); const savedPkgs = config.packages || []; if (savedPkgs.length > 0) { const newMap = new Map(); @@ -292,9 +205,11 @@ } catch { error = 'Failed to load configuration'; } - initialSnapshot = captureSnapshot(); loading = false; } + + initialPayload = JSON.parse(JSON.stringify(buildPayload())); + initialSnapshot = captureSnapshot(); }); function buildPayload() { @@ -310,7 +225,7 @@ const name = key.slice(type.length + 1); return { name, type, desc: packageDescs.get(key) || '' }; }), - snapshot: updatedSnapshot, + snapshot: updatedSnapshot }; } @@ -322,7 +237,7 @@ visibility: parsed.visibility || 'unlisted', alias: parsed.alias || '', custom_script: parsed.custom_script || '', - dotfiles_repo: parsed.dotfiles_repo || '', + dotfiles_repo: parsed.dotfiles_repo || '' }; const newMap = new Map(); const newDescs = new Map(); @@ -338,21 +253,7 @@ } selectedPackages = newMap; packageDescs = newDescs; - macosPrefs = Array.isArray(parsed.snapshot?.macos_prefs) - ? parsed.snapshot.macos_prefs.map((p: any) => { - const type = p.type || ''; - const out: MacOSPref = { - domain: p.domain || '', - key: p.key || '', - type, - value: normalizePrefValue(type, String(p.value ?? '')), - desc: p.desc || '', - }; - if (typeof p.host === 'string' && p.host !== '') out.host = p.host; - return out; - }) - : []; - initExpandedCats(); + macosPrefs = Array.isArray(parsed.snapshot?.macos_prefs) ? mapPrefs(parsed.snapshot.macos_prefs) : []; } function enterRawMode() { @@ -363,8 +264,7 @@ function exitRawMode() { try { - const parsed = JSON.parse(rawJson); - applyParsed(parsed); + applyParsed(JSON.parse(rawJson)); rawMode = false; error = ''; } catch { @@ -372,8 +272,18 @@ } } + function toggleRaw() { + if (rawMode) exitRawMode(); + else enterRawMode(); + } + + function discard() { + if (initialPayload) applyParsed(JSON.parse(JSON.stringify(initialPayload))); + error = ''; + } + async function save() { - let payload: ReturnType | any; + let payload: any; if (rawMode) { try { payload = JSON.parse(rawJson); @@ -400,7 +310,7 @@ const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), + body: JSON.stringify(payload) }); const text = await response.text(); let result; @@ -428,204 +338,115 @@ {#if loading} -
-
-
+
{:else}
- {#if !rawMode} - - {/if} - -
- -
- {#if !rawMode} -
- -
+
+ {#if error} + {error} + {:else if rawMode} + editing config.json + {:else if dirty} + Unsaved changes + {:else if !isNew} + ✓ All changes saved {/if} - - {#if error} - {error} + {#if dirty && !rawMode} + {/if} -
-
+
{#if rawMode} -
-
Edit the config JSON directly. Switch back to Visual to see your changes reflected in the form.
- +
+
+
+ + config.json + editable · consumed by the CLI +
+ +
{:else} - -
- -
-
-
- - -
+
+ +
+

> IDENTITY

+
+ + +
- {#each ['public', 'unlisted', 'private'] as v} - + {#each ['public', 'unlisted', 'private'] as v (v)} + {/each}
openboot.dev/ - +
-
-
+ - -
-
- - {selectedPackages.size} packages -
- -
- - -
- - -
- Dotfiles Repository - +
+
+

> STACK

+ {selectedPackages.size} packages +
+ -

Cloned & deployed via GNU Stow after install.

-
- - -
- - -
- -
-
- post-install.sh - +
+ + +
+

> SYSTEM

+
+
Dotfiles repository
+ +

Cloned & deployed via GNU Stow after install.

+
macOS preferences
+
- - - {#if formData.custom_script} -
 { scriptDraft = formData.custom_script; showScriptModal = true; }}>{formData.custom_script}
- {:else} -
{ scriptDraft = ''; showScriptModal = true; }}>No post-install script — click to add
- {/if} -
- -
- {/if} - -{/if} - -{#if showScriptModal} - -
{ if (e.key === 'Escape') showScriptModal = false; }}> - -
showScriptModal = false}>
-
-
- post-install.sh -
- - -
-
-
- -
-
- Commands run sequentially in your home directory after packages, shell, dotfiles, and macOS preferences are applied. + + + +
+

> SCRIPT

+
+
+ + post-install.sh — runs after packages +
+ +
+
-
+ {/if}
{/if} @@ -653,879 +474,451 @@ } .editor { - max-width: 820px; + max-width: 1160px; margin: 0 auto; - padding: 0 24px 100px; + padding: 0 36px 96px; } - /* Header */ - .editor-header { + /* ---------- save bar ---------- */ + .savebar { position: sticky; - top: 53px; + top: 56px; z-index: 40; display: flex; - justify-content: space-between; align-items: center; - padding: 16px 0; - margin-bottom: 40px; - background: var(--bg-primary); + justify-content: space-between; + gap: 16px; + padding: 22px 0 16px; + background: color-mix(in srgb, var(--bg-primary) 90%, transparent); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); } - .back-btn { - display: flex; + .back { + display: inline-flex; align-items: center; gap: 8px; background: none; border: none; - color: var(--text-muted); - font-size: 0.88rem; - font-weight: 600; - font-family: inherit; cursor: pointer; - transition: color 0.15s; - padding: 8px 0; + font-family: inherit; + font-size: 0.85rem; + color: var(--text-secondary); + padding: 0; + transition: color 0.15s ease; } - .back-btn:hover { + .back:hover { color: var(--text-primary); } - .header-right { + .savebar-right { display: flex; align-items: center; - gap: 16px; - } - - .dna-preview { - opacity: 0.6; - transition: opacity 0.3s; + gap: 14px; } - .dna-preview:hover { - opacity: 1; + .status { + display: flex; + align-items: center; + gap: 7px; + font-size: 0.78rem; + animation: ob-fade 0.25s ease; } - .header-error { - color: var(--danger); - font-size: 0.82rem; - font-weight: 500; + .status.unsaved { + color: var(--amber); } - .save-btn { - padding: 10px 24px; - background: var(--accent); - color: var(--bg-primary); - border: none; - border-radius: 10px; - font-size: 0.88rem; - font-weight: 600; - font-family: inherit; - cursor: pointer; - transition: all 0.2s; + .status.saved { + color: var(--accent); } - .save-btn:hover:not(:disabled) { - background: var(--accent-hover); - transform: translateY(-1px); + .status.raw { + color: var(--text-muted); } - .save-btn:disabled { - opacity: 0.6; - cursor: not-allowed; + .status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--amber); + animation: ob-pulse 1.5s ease infinite; } - .save-btn-inactive { - background: var(--border); - color: var(--text-muted); + .savebar-error { + font-size: 0.78rem; + color: var(--danger); } .raw-btn { - display: flex; + display: inline-flex; align-items: center; - gap: 6px; - padding: 8px 14px; - background: none; - border: 1px solid var(--border); + gap: 7px; + padding: 9px 15px; + background: var(--bg-secondary); + border: 1px solid var(--border-hover); border-radius: 8px; - color: var(--text-muted); - font-size: 0.82rem; - font-weight: 600; font-family: inherit; + font-size: 0.8rem; + color: var(--text-secondary); cursor: pointer; - transition: all 0.15s; + transition: all 0.15s ease; } .raw-btn:hover { - border-color: var(--text-muted); color: var(--text-primary); } - .raw-btn-active { + .raw-btn.active { + background: var(--accent-glow); border-color: var(--accent); color: var(--accent); } - .raw-editor { - display: flex; - flex-direction: column; - gap: 12px; - padding-bottom: 100px; + .discard { + padding: 9px 16px; + background: none; + border: 1px solid var(--border-hover); + border-radius: 8px; + font-family: inherit; + font-size: 0.82rem; + color: var(--text-secondary); + cursor: pointer; + animation: ob-fade 0.25s ease; + transition: color 0.15s ease; } - .raw-hint { - font-size: 0.80rem; - color: var(--text-muted); + .discard:hover { + color: var(--text-primary); } - .raw-textarea { - width: 100%; - min-height: 60vh; - padding: 16px; - background: var(--bg-secondary, #111); - color: var(--text-primary); + .save { + padding: 10px 22px; + background: var(--accent); + color: var(--bg-primary); + border: none; + border-radius: 8px; + font-family: inherit; + font-size: 0.84rem; + font-weight: 500; + cursor: pointer; + transition: filter 0.15s ease, transform 0.15s ease; + } + + .save:hover:not(:disabled) { + filter: brightness(1.08); + transform: translateY(-1px); + } + + .save.inactive { + background: var(--bg-tertiary); + color: var(--text-muted); border: 1px solid var(--border); - border-radius: 10px; - font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 0.82rem; - line-height: 1.6; - resize: vertical; - outline: none; - box-sizing: border-box; - transition: border-color 0.15s; + cursor: default; } - .raw-textarea:focus { - border-color: var(--accent); + .save:disabled { + cursor: default; } - /* Sections */ - .editor-body { - display: flex; - flex-direction: column; - gap: 56px; + /* ---------- form grid ---------- */ + .form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px 24px; + align-items: start; + padding-top: 30px; } - .section { - scroll-margin-top: 80px; + .col-full { + grid-column: 1 / -1; } - .section-tag { - font-size: 0.68rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.14em; + .eyebrow { + font-size: 0.72rem; + letter-spacing: 0.1em; color: var(--text-muted); - margin-bottom: 16px; - display: flex; - align-items: center; - gap: 12px; + margin: 0 0 16px; } - .section-tag::after { - content: ''; - flex: 1; - height: 1px; - background: var(--border); + .eyebrow .gt { + color: var(--accent); } - .section-top { + .eyebrow-row { display: flex; + align-items: baseline; justify-content: space-between; - align-items: center; margin-bottom: 16px; } - .section-top .section-tag { - margin-bottom: 0; + .eyebrow-row .eyebrow { + margin: 0; } - .section-count { - font-size: 0.78rem; + .eyebrow-count { + font-size: 0.74rem; color: var(--text-muted); - font-weight: 600; - font-variant-numeric: tabular-nums; } - /* Identity */ - .identity-hero { - border-radius: 20px; - padding: 4px; - transition: background 0.5s; + .card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 14px; + padding: 22px; } - .identity-glass { - background: color-mix(in srgb, var(--bg-primary) 80%, transparent); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 17px; - padding: 32px; - display: flex; - flex-direction: column; - gap: 12px; + .identity-card { + padding: 24px; } - .name-input { - background: none; - border: none; - color: var(--text-primary); - font-size: 2.2rem; - font-weight: 600; + .field-label { + font-size: 0.74rem; + color: var(--text-secondary); + margin-bottom: 11px; + } + + .field-label-spaced { + margin-top: 22px; + } + + .field-hint { + font-size: 0.74rem; + color: var(--text-muted); + margin: 6px 0 0; + } + + .text-input { + width: 100%; + background: var(--bg-tertiary); + border: 1px solid var(--border-hover); + border-radius: 8px; + padding: 11px 13px; font-family: inherit; + font-size: 0.84rem; + color: var(--text-primary); outline: none; - padding: 0; - line-height: 1.2; - width: 100%; + transition: border-color 0.15s ease; } - .name-input::placeholder { - color: var(--text-muted); - opacity: 0.5; + .text-input:focus { + border-color: var(--accent); } - .desc-input { - background: none; + /* ---------- identity ---------- */ + .name-input { + width: 100%; + background: transparent; border: none; - color: var(--text-secondary); - font-size: 1.05rem; - font-family: inherit; outline: none; - padding: 0; + font-family: inherit; + font-size: 1.5rem; + font-weight: 500; + letter-spacing: -0.02em; + color: var(--text-primary); + margin-bottom: 8px; + } + + .desc-input { width: 100%; + background: transparent; + border: none; + outline: none; + font-family: inherit; + font-size: 0.88rem; + color: var(--text-secondary); + margin-bottom: 20px; } + .name-input::placeholder, .desc-input::placeholder { color: var(--text-muted); - opacity: 0.5; } - .identity-row { + .identity-foot { display: flex; - justify-content: space-between; align-items: center; - gap: 16px; - margin-top: 12px; + justify-content: space-between; + gap: 20px; flex-wrap: wrap; + padding-top: 18px; + border-top: 1px solid var(--border); } .vis-pills { display: flex; - gap: 0; - border: 1px solid var(--border); - border-radius: 10px; - overflow: hidden; + gap: 6px; } .vis-pill { - padding: 8px 16px; - background: var(--bg-tertiary); - border: none; - color: var(--text-muted); - font-size: 0.78rem; - font-weight: 600; + padding: 7px 15px; + border-radius: 7px; font-family: inherit; + font-size: 0.8rem; cursor: pointer; + background: var(--bg-tertiary); + color: var(--text-secondary); + border: 1px solid var(--border-hover); text-transform: capitalize; - transition: all 0.15s; - border-right: 1px solid var(--border); - } - - .vis-pill:last-child { - border-right: none; + transition: all 0.15s ease; } .vis-pill:hover { - color: var(--text-secondary); - background: var(--bg-hover); + color: var(--text-primary); } .vis-pill.active { background: var(--accent); color: var(--bg-primary); + border-color: var(--accent); } .alias-field { display: flex; align-items: center; - gap: 4px; + background: var(--bg-tertiary); + border: 1px solid var(--border-hover); + border-radius: 8px; + overflow: hidden; } .alias-prefix { - color: var(--text-muted); font-size: 0.82rem; + color: var(--text-muted); + padding: 9px 4px 9px 12px; white-space: nowrap; } .alias-input { - width: 120px; - padding: 8px 10px; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-primary); - font-size: 0.82rem; - font-family: 'JetBrains Mono', monospace; + background: transparent; + border: none; outline: none; - transition: border-color 0.2s; + font-family: inherit; + font-size: 0.82rem; + color: var(--accent); + padding: 9px 12px 9px 0; + width: 130px; } - .alias-input:focus { - border-color: var(--accent); + .alias-input::placeholder { + color: var(--text-muted); } - /* System */ - .system-prefs { - display: flex; - flex-direction: column; - gap: 14px; + /* ---------- terminal (script + raw) ---------- */ + .term { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 14px; + overflow: hidden; } - .prefs-top { + .term-head { display: flex; - justify-content: space-between; align-items: center; + gap: 7px; + padding: 11px 15px; + border-bottom: 1px solid var(--border); } - .prefs-title { - font-size: 0.95rem; - font-weight: 600; - color: var(--text-primary); + .dot { + width: 9px; + height: 9px; + border-radius: 50%; } - .prefs-count { - font-size: 0.78rem; - color: var(--text-muted); - font-weight: 500; + .dot-red { + background: var(--danger); } - /* Accordion */ - .pref-accordion { - display: flex; - flex-direction: column; - border: 1px solid var(--border); - border-radius: 12px; - overflow: hidden; + .dot-amber { + background: var(--amber); } - .pref-acc-group { - border-bottom: 1px solid var(--border); + .dot-green { + background: var(--accent); } - .pref-acc-group:last-child { - border-bottom: none; + .term-name { + margin-left: 6px; + font-size: 0.72rem; + color: var(--text-muted); } - .pref-acc-header { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 10px 14px; - background: var(--bg-tertiary); - border: none; - font-family: inherit; - cursor: pointer; - text-align: left; - transition: background 0.1s; - } - - .pref-acc-header:hover { - background: var(--bg-secondary); - } - - .pref-acc-header-static { - cursor: default; - } - - .pref-acc-header-static:hover { - background: var(--bg-tertiary); - } - - .pref-acc-chevron { - color: var(--text-muted); - transition: transform 0.15s; - flex-shrink: 0; - } - - .pref-acc-chevron-open { - transform: rotate(90deg); - } - - .pref-acc-name { - font-size: 0.84rem; - font-weight: 600; - color: var(--text-primary); - flex: 1; - } - - .pref-acc-badge { - padding: 2px 7px; - background: var(--accent); - border-radius: 8px; - font-size: 0.65rem; - font-weight: 600; - color: var(--bg-primary); - line-height: 1.3; - } - - .pref-acc-total { - font-size: 0.72rem; - color: var(--text-muted); - } - - .pref-acc-body { - display: flex; - flex-direction: column; - background: var(--bg-primary); - } - - /* Pref rows */ - .pref-row { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 14px; - border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); - transition: background 0.1s; - } - - .pref-row-active { - background: color-mix(in srgb, var(--accent) 4%, transparent); - } - - .pref-toggle-btn { - background: none; - border: none; - padding: 0; - cursor: pointer; - flex-shrink: 0; - } - - .pref-check { - width: 18px; - height: 18px; - display: flex; - align-items: center; - justify-content: center; - border: 1.5px solid var(--border); - border-radius: 4px; - font-size: 0.65rem; - font-weight: 600; - color: transparent; - background: var(--bg-secondary); - transition: all 0.15s; - } - - .pref-check-on { - background: var(--accent); - border-color: var(--accent); - color: var(--bg-primary); - } - - .pref-info { - flex: 1; - min-width: 0; - } - - .pref-label { - font-size: 0.84rem; - font-weight: 500; - color: var(--text-primary); - display: block; - } - - .pref-meta { + .term-note { + margin-left: auto; font-size: 0.7rem; color: var(--text-muted); - display: block; - line-height: 1.3; - } - - .pref-control { - flex-shrink: 0; - } - - .pref-bool { - padding: 4px 10px; - border-radius: 6px; - font-size: 0.72rem; - font-weight: 600; - font-family: inherit; - cursor: pointer; - transition: all 0.15s; - background: var(--bg-secondary); - border: 1px solid var(--border); - color: var(--text-muted); - min-width: 44px; - } - - .pref-bool.on { - background: color-mix(in srgb, var(--accent) 15%, transparent); - border-color: var(--accent); - color: var(--accent); - } - - .pref-sel { - padding: 4px 8px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text-primary); - font-size: 0.78rem; - font-family: inherit; - cursor: pointer; - outline: none; - } - - .pref-sel:focus { - border-color: var(--accent); - } - - .pref-val { - width: 72px; - padding: 4px 8px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text-primary); - font-size: 0.78rem; - font-family: 'JetBrains Mono', monospace; - outline: none; - } - - .pref-val:focus { - border-color: var(--accent); - } - - .pref-rm { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - font-size: 1.1rem; - padding: 2px 4px; - border-radius: 4px; - transition: color 0.15s; - line-height: 1; - flex-shrink: 0; } - .pref-rm:hover { - color: var(--danger); - } - - .custom-pref-toggle { - background: none; + .script-textarea { + width: 100%; + min-height: 200px; + padding: 18px 20px; + background: transparent; border: none; - padding: 0; - font-size: 0.78rem; - color: var(--text-muted); - cursor: pointer; - font-family: inherit; - text-align: left; - transition: color 0.15s; - } - - .custom-pref-toggle:hover { - color: var(--text-secondary); - } - - .custom-pref-row { - display: flex; - gap: 8px; - align-items: center; - } - - .custom-pref-input { - flex: 1; - padding: 10px 12px; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-primary); - font-size: 0.82rem; - font-family: 'JetBrains Mono', monospace; outline: none; - } - - .custom-pref-input:focus { - border-color: var(--accent); - } - - .custom-pref-type { - padding: 10px 8px; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-secondary); - font-size: 0.78rem; - font-family: inherit; - cursor: pointer; - outline: none; - } - - .custom-pref-add { - padding: 10px 16px; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-secondary); - font-size: 0.82rem; - font-family: inherit; - cursor: pointer; - transition: all 0.15s; - } - - .custom-pref-add:hover { - border-color: var(--accent); - color: var(--accent); - } - - .pref-error { - font-size: 0.78rem; - color: var(--danger); - } - - .pref-hint { - font-size: 0.75rem; - color: var(--text-muted); - margin: 0; - } - - /* Dotfiles */ - .system-dotfiles { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 14px; - padding: 20px 24px; - display: flex; - align-items: center; - gap: 16px; - flex-wrap: wrap; - margin-bottom: 20px; - } - - .dotfiles-label { - font-size: 0.88rem; - font-weight: 600; - color: var(--text-primary); - } - - .dotfiles-input { - flex: 1; - min-width: 200px; - padding: 10px 12px; - background: var(--bg-tertiary); - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-primary); - font-size: 0.85rem; + resize: vertical; font-family: inherit; - outline: none; - } - - .dotfiles-input:focus { - border-color: var(--accent); - } - - .dotfiles-hint { - font-size: 0.75rem; - color: var(--text-muted); - margin: 0; - line-height: 1.4; - } - - /* Script */ - .script-card { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 14px; - overflow: hidden; - } - - .script-bar { - padding: 12px 18px; - background: var(--bg-tertiary); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - } - - .script-filename { - font-family: 'JetBrains Mono', monospace; - font-size: 0.82rem; - font-weight: 600; - color: var(--text-secondary); - } - - .script-edit-btn { - padding: 5px 14px; - background: none; - border: 1px solid var(--border); - border-radius: 6px; - color: var(--accent); font-size: 0.8rem; - font-family: inherit; - cursor: pointer; - transition: all 0.15s; - } - - .script-edit-btn:hover { - border-color: var(--accent); - background: var(--accent-glow); - } - - .script-preview { - padding: 16px 20px; - margin: 0; + line-height: 1.9; color: var(--text-secondary); - font-family: 'JetBrains Mono', monospace; - font-size: 0.82rem; - line-height: 1.7; - white-space: pre-wrap; - word-break: break-all; - max-height: 160px; - overflow: hidden; - cursor: pointer; } - .script-empty { - padding: 24px 20px; + .script-textarea::placeholder { color: var(--text-muted); - font-size: 0.85rem; - text-align: center; - cursor: pointer; } - .script-empty:hover { - color: var(--text-secondary); + .raw-wrap { + padding: 30px 0 0; } - /* Script modal */ - .script-modal-overlay { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - padding: 32px; - } - - .script-modal-backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); - } - - .script-modal { - position: relative; + .raw-textarea { + display: block; width: 100%; - max-width: 860px; - height: 80vh; - max-height: 700px; - background: var(--bg-primary); - border: 1px solid var(--border); - border-radius: 16px; - display: flex; - flex-direction: column; - overflow: hidden; - box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4); - } - - .script-modal-bar { - padding: 14px 20px; - background: var(--bg-tertiary); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - justify-content: space-between; - flex-shrink: 0; - } - - .script-modal-filename { - font-family: 'JetBrains Mono', monospace; - font-size: 0.85rem; - font-weight: 600; - color: var(--text-secondary); - } - - .script-modal-actions { - display: flex; - gap: 8px; - } - - .script-modal-cancel { - padding: 6px 16px; - background: none; - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-secondary); - font-size: 0.82rem; - font-family: inherit; - cursor: pointer; - transition: all 0.15s; - } - - .script-modal-cancel:hover { - border-color: var(--text-muted); - } - - .script-modal-save { - padding: 6px 16px; - background: var(--accent); + min-height: 58vh; + padding: 20px 22px; + background: transparent; border: none; - border-radius: 8px; - color: var(--bg-primary); - font-size: 0.82rem; - font-weight: 600; + outline: none; + resize: vertical; font-family: inherit; - cursor: pointer; - transition: all 0.15s; - } - - .script-modal-save:hover { - opacity: 0.9; - } - - .script-modal-body { - flex: 1; - overflow: hidden; - } - - - .script-modal-hint { - padding: 10px 20px; - background: var(--bg-tertiary); - border-top: 1px solid var(--border); - color: var(--text-muted); - font-size: 0.75rem; - flex-shrink: 0; + font-size: 0.8rem; + line-height: 1.75; + color: var(--text-secondary); } - @media (max-width: 768px) { - .editor { - padding: 0 16px 80px; + @keyframes ob-fade { + from { + opacity: 0; + transform: translateY(4px); } - - .name-input { - font-size: 1.4rem; + to { + opacity: 1; + transform: none; } + } - .identity-row { - flex-direction: column; - align-items: flex-start; + @keyframes ob-pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.45; + transform: scale(1.5); } + } - .editor-header { - flex-wrap: wrap; - gap: 12px; + @media (max-width: 880px) { + .editor { + padding: 0 22px 80px; } - .dna-preview { - display: none; + .form-grid { + grid-template-columns: 1fr; } } diff --git a/src/lib/components/ConfigRow.svelte b/src/lib/components/ConfigRow.svelte index 907e115..ae6805c 100644 --- a/src/lib/components/ConfigRow.svelte +++ b/src/lib/components/ConfigRow.svelte @@ -107,11 +107,14 @@
- - {#if expanded} -
- {#each items as item} - {@const added = isPrefAdded(item)} - {@const prefIdx = getAddedPrefIndex(item)} -
- -
- {item.label} - {item.description} -
- {#if added && prefIdx >= 0} -
- {#if item.options} - - {:else if item.type === 'bool'} - - {:else} - updatePrefValue(prefIdx, e.currentTarget.value)} - /> - {/if} -
- {/if} -
- {/each} -
- {/if}
{/each} - - - {#if customPrefs.length > 0} -
-
- Custom - {customPrefs.length} -
-
- {#each customPrefs as { pref, index }} -
- -
- {pref.domain}.{pref.key} - {#if pref.desc} - {pref.desc} - {/if} -
-
- updatePrefValue(index, e.currentTarget.value)} - /> -
- -
- {/each} + {/each} + + {#if customPrefs.length > 0} +
+
Custom
+ {#each customPrefs as pref (pref.domain + ':' + pref.key)} +
+
+
{pref.desc || pref.key}
+
{pref.domain} {pref.key} = {pref.value}
+
+
-
- {/if} - - - {#if showCustomPref} -
- e.key === 'Enter' && (e.preventDefault(), addPref())} - /> - - -
- {#if prefInputError} -

{prefInputError}

- {:else} -

Format: domain.key=value

- {/if} - {/if} -
+ {/each} +
+ {/if} + diff --git a/src/lib/components/PackageDna.svelte b/src/lib/components/PackageDna.svelte deleted file mode 100644 index f7adbcf..0000000 --- a/src/lib/components/PackageDna.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - - {#each Array(totalCells) as _, i} - {@const x = (i % cols) * (size + gap)} - {@const y = Math.floor(i / cols) * (size + gap)} - {@const pkg = sorted[i]} - - {#if pkg} - {pkg.name} ({pkg.type}) - {/if} - - {/each} - - - diff --git a/src/lib/components/PackageManager.svelte b/src/lib/components/PackageManager.svelte index 84ac252..9992492 100644 --- a/src/lib/components/PackageManager.svelte +++ b/src/lib/components/PackageManager.svelte @@ -1,6 +1,4 @@ -
- +
+
Base preset
- {#each presets as p} - + {#each presets as p (p)} + {/each}
- - - - - {#if searchLoading} -
Searching...
- {:else if searchQuery.length >= 2 && searchResults.length === 0} -
No results for "{searchQuery}"
- {:else if searchResults.length > 0} -
- {#each searchResults as result} - - {/each} -
- {/if} - - - {#if showImport} -
- {#if importError} -
{importError}
- {/if} - -
- - -
-
- {/if} - - -
- {#if grouped.apps.length > 0} -
-
- - APPS - {grouped.apps.length} -
-
- {#each grouped.apps as pkg} - - {/each} -
-
- {/if} - - {#if grouped.cli.length > 0} -
-
- - CLI - {grouped.cli.length} -
-
- {#each grouped.cli as pkg} - - {/each} -
-
+
Packages
+
+ {#each packageList as pk (pk.type + ':' + pk.name)} + + {displayName(pk.name)} + + + {/each} + {#if packageList.length === 0} + No packages — pick a preset or add one below. {/if} +
- {#if grouped.npm.length > 0} -
-
- - NPM - {grouped.npm.length} -
-
- {#each grouped.npm as pkg} - - {/each} -
+ {#if !searchOpen} + + {:else} + + {/if} +
+ {/if}
diff --git a/src/lib/components/SectionNav.svelte b/src/lib/components/SectionNav.svelte deleted file mode 100644 index 21866e2..0000000 --- a/src/lib/components/SectionNav.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - - - - diff --git a/src/lib/components/ShellEditor.svelte b/src/lib/components/ShellEditor.svelte deleted file mode 100644 index 7d5fcf1..0000000 --- a/src/lib/components/ShellEditor.svelte +++ /dev/null @@ -1,183 +0,0 @@ - - -
- - diff --git a/src/lib/components/SiteHeader.svelte b/src/lib/components/SiteHeader.svelte index 9a17b76..855dc12 100644 --- a/src/lib/components/SiteHeader.svelte +++ b/src/lib/components/SiteHeader.svelte @@ -18,21 +18,21 @@ Explore Docs -
- {#if children}{@render children()}{/if} - - {#if $auth.loading} - - {:else if $auth.user} +
+
+ {#if children}{@render children()}{/if} + + {#if $auth.loading} + + {:else if $auth.user} + Dashboard /
- {:else} - - {/if} -
-
-
+ + {:else} + + {/if}
@@ -64,7 +64,7 @@ .header-left { display: flex; align-items: center; - gap: 26px; + gap: 28px; } .header-logo { @@ -124,23 +124,25 @@ transform: scaleX(1); } - .header-actions { + .header-right { display: flex; align-items: center; gap: 16px; } - .header-right { + .header-auth { display: flex; align-items: center; + gap: 11px; } .header-dashboard-link, .header-login-link, .header-logout-link { color: var(--text-secondary); - font-size: 0.9rem; + font-size: 0.85rem; font-weight: 500; + text-transform: lowercase; text-decoration: none; transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1); } @@ -161,7 +163,7 @@ .header-separator { color: var(--text-muted); - font-size: 0.9rem; + font-size: 0.85rem; user-select: none; } diff --git a/src/lib/components/ThemeToggle.svelte b/src/lib/components/ThemeToggle.svelte index d85db87..7733b5d 100644 --- a/src/lib/components/ThemeToggle.svelte +++ b/src/lib/components/ThemeToggle.svelte @@ -18,24 +18,27 @@ diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3c34a1b..16e6b4d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,4 @@ @@ -51,13 +67,13 @@ Fresh Mac?
Set it up in
one command.

- One command installs Git, Node, Docker, VS Code, shell config, dotfiles—everything you'd normally spend your first day installing. Then snapshot your setup and share it with your team so they get the same thing. + One command installs Git, Node, Docker, VS Code, shell config, dotfiles — everything you'd normally spend your first day installing. Then snapshot your setup and share it with your team.

  • Installs Homebrew packages, apps, configures shell + macOS preferences
  • Snapshot your current Mac and recreate it on another machine
  • -
  • Share your exact setup with teammates via a one-line install URL
  • +
  • Share your exact setup via a one-line install URL
@@ -86,17 +102,61 @@
-
Checksum verified · Added to PATH
-
=== OpenBoot Installer v0.20.0 ===
-
[DRY-RUN MODE — no changes will be made]
-
=== Step 1: Git configuration ===
-
Already configured: jam <jam@hey.com>
-
=== Step 2: Preset selection ===
-
Choose your preset
-
developer — Node, Go, Docker, VS Code
-
full — languages, DevOps, databases, AI
-
minimal — CLI essentials only
-
↑ up · ↓ down · / filter · ⏎ submit
+ +
+
$ curl -fsSL openboot.dev/install | bash
+
OpenBoot Installer
+
==================
+
Xcode Command Line Tools ready
+
Homebrew ready
+
Detected: darwin/arm64
+
Downloading OpenBoot v0.44.0...
+
Downloaded to ~/.openboot/bin/openboot
+
Checksum verified
+
Added to PATH
+
+
+ + +
+
+ Formulae + Casks + NPM + developer preset +
+
+ {#each demoPackages as pkg, i (pkg.name)} +
+ {#if i === 0}{/if} + [✓] + {pkg.name} + {pkg.desc} +
+ {/each} +
+
Selected: 48 packages
+
+ Tab/←→ switch ↑↓ navigate + Space toggle / search + a all ⏎ Enter +
+
+ + +
+
=== Installation Complete! ===
+
OpenBoot has successfully configured your Mac.
+
What was installed:
+
– Git configured with your identity
+
28 CLI packages
+
14 GUI applications
+
5 npm global packages
+
Next steps:
+
– Restart your terminal to apply changes
+
– Run 'brew doctor' to verify Homebrew health
+
$
+
@@ -135,26 +195,11 @@
-
-

openboot $

- - +
@@ -393,80 +438,160 @@ } .terminal-body { + position: relative; + height: 486px; + overflow: hidden; background: var(--bg-secondary); - padding: 18px 18px 20px; - font-size: 0.76rem; - line-height: 1.85; + font-size: 0.72rem; + line-height: 1.6; color: var(--text-secondary); } - .t-line { - white-space: nowrap; + /* three demo screens cross-fade on a loop: bootstrap → picker → done */ + .t-screen { + position: absolute; + inset: 0; + opacity: 0; } - .t-accent { - color: var(--accent); + .t-screen-a { + padding: 16px 18px; + animation: ob-scrA 15s ease infinite; } - .t-muted { - color: var(--text-muted); + .t-screen-b { + display: flex; + flex-direction: column; + animation: ob-scrB 15s ease infinite; } - .t-strong { - color: var(--text-primary); + .t-screen-c { + padding: 16px 18px; + animation: ob-scrC 15s ease infinite; } + .t-accent { color: var(--accent); } + .t-muted { color: var(--text-muted); } + .t-strong { color: var(--text-primary); } + .t-amber { color: var(--amber); } + .t-head { color: var(--accent); - margin-top: 10px; + font-weight: 600; + } + + .t-mt10 { margin-top: 10px; } + .t-mt13 { margin-top: 13px; } + .t-mt14 { margin-top: 14px; } + .t-mt15 { margin-top: 15px; } + .t-pl16 { padding-left: 16px; } + + .t-caret { + display: inline-block; + width: 7px; + height: 1em; + background: var(--text-secondary); + translate: 0 2px; + animation: ob-blink 1s infinite; + } + + /* ── package-picker TUI (screen B) ── */ + .tui-tabs { + display: flex; + align-items: center; + gap: 18px; + padding: 14px 16px 9px; + border-bottom: 1px solid var(--border); + font-size: 0.71rem; + white-space: nowrap; + } + + .tui-tab { color: var(--text-muted); } + + .tui-tab.active { + color: var(--accent); + font-weight: 600; + border-bottom: 2px solid var(--accent); + padding-bottom: 9px; + margin-bottom: -11px; } - .t-note { + .tui-preset { + margin-left: auto; color: var(--text-muted); - margin-top: 8px; + font-size: 0.68rem; + } + + .tui-list { padding: 9px 14px 4px; } + + .tui-row { + position: relative; + height: 20px; + display: flex; + align-items: center; + gap: 7px; + padding-left: 18px; + white-space: nowrap; + overflow: hidden; } - .t-ok { - margin-top: 4px; + .tui-row.selected { + border-radius: 5px; + background: var(--accent-glow); + box-shadow: inset 2px 0 0 var(--accent); } - .t-prompt-label { - color: var(--amber); - margin-top: 6px; + .tui-arrow { + position: absolute; + left: 5px; + color: var(--accent); } - .t-sel { - margin-top: 2px; + .tui-check { flex-shrink: 0; } + + .tui-name { + width: 74px; + flex-shrink: 0; } - .t-opt { - color: var(--text-muted); - padding-left: 14px; + .tui-selected { + padding: 6px 16px; + color: var(--text-secondary); } - .t-keys { + .tui-keys { + margin-top: auto; + padding: 9px 16px; + border-top: 1px solid var(--border); color: var(--text-muted); - margin-top: 10px; + font-size: 0.66rem; } - .t-caret { - display: inline-block; - width: 7px; - height: 1em; - background: var(--accent); - translate: 0 2px; - animation: ob-blink 1s infinite; - } + .tui-sep { color: var(--border-hover); } @keyframes ob-blink { 0%, - 49% { - opacity: 1; - } + 49% { opacity: 1; } 50%, - 100% { - opacity: 0; - } + 100% { opacity: 0; } + } + + @keyframes ob-scrA { + 0%, 1% { opacity: 0; } + 4%, 19% { opacity: 1; } + 23%, 100% { opacity: 0; } + } + + @keyframes ob-scrB { + 0%, 25% { opacity: 0; } + 28%, 70% { opacity: 1; } + 73%, 100% { opacity: 0; } + } + + @keyframes ob-scrC { + 0%, 75% { opacity: 0; } + 78%, 97% { opacity: 1; } + 99.5%, 100% { opacity: 0; } } /* ---------- how it works ---------- */ @@ -542,61 +667,36 @@ /* ---------- footer ---------- */ footer { border-top: 1px solid var(--border); - padding: 66px 0 76px; - } - - .foot-prompt { - color: var(--text-muted); - font-size: 0.88rem; - margin: 0 0 46px; - } - - .foot-prompt .accent { - color: var(--accent); - } - - .caret { - display: inline-block; - width: 7px; - height: 1.02em; - background: var(--accent); - translate: 0 2px; + margin-top: 8px; } - .footer-cols { + .footer-inner { display: flex; - gap: 90px; + align-items: center; + justify-content: space-between; + gap: 24px; flex-wrap: wrap; - margin-bottom: 50px; + padding-top: 26px; + padding-bottom: 26px; } - .footer-col h4 { - font-size: 0.72rem; + .foot-tagline { + font-size: 0.82rem; color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.1em; - margin: 0 0 18px; - font-weight: 500; + margin: 0; } - .footer-col a { - display: block; - color: var(--text-secondary); - font-size: 0.85rem; - margin-bottom: 12px; - transition: color 0.15s ease; + .foot-tagline .accent { + color: var(--accent); } - .footer-col a:hover { - color: var(--accent); + .foot-tagline-text { + color: var(--text-secondary); } .footer-legal { color: var(--text-muted); font-size: 0.78rem; - border-top: 1px solid var(--border); - padding-top: 28px; - letter-spacing: 0.01em; margin: 0; } @@ -636,9 +736,5 @@ .install-command code { font-size: 0.8rem; } - - .footer-cols { - gap: 48px; - } } diff --git a/src/routes/[username]/+page.svelte b/src/routes/[username]/+page.svelte index 2ec05d4..e4ffbf7 100644 --- a/src/routes/[username]/+page.svelte +++ b/src/routes/[username]/+page.svelte @@ -332,20 +332,13 @@ display: flex; flex-direction: column; gap: 12px; - transition: - border-color 0.18s ease, - box-shadow 0.18s ease; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; } .config-card:hover { + transform: translateY(-4px); border-color: var(--border-hover); - box-shadow: - 0 0 0 1px var(--accent-glow), - 0 14px 40px -22px var(--shadow); - } - - .config-card:hover .config-name { - color: var(--accent); + box-shadow: 0 16px 34px -16px var(--shadow); } .card-header { @@ -549,14 +542,12 @@ text-decoration: none; cursor: pointer; white-space: nowrap; - transition: - background 0.18s ease, - box-shadow 0.18s ease; + transition: filter 0.16s ease, transform 0.16s ease; } .cta-button:hover { - background: var(--accent-hover); - box-shadow: 0 0 0 3px var(--accent-glow); + filter: brightness(1.08); + transform: translateY(-1px); } /* ---------- responsive ---------- */ diff --git a/src/routes/dashboard/+page.svelte b/src/routes/dashboard/+page.svelte index a1de88a..efd3312 100644 --- a/src/routes/dashboard/+page.svelte +++ b/src/routes/dashboard/+page.svelte @@ -266,10 +266,8 @@

My configurations

{configs.length} config{configs.length !== 1 ? 's' : ''} - {#if totalInstalls > 0} - · - {totalInstalls} install{totalInstalls !== 1 ? 's' : ''} - {/if} + · + {totalInstalls} install{totalInstalls !== 1 ? 's' : ''}

@@ -465,7 +463,6 @@ .page-stats { color: var(--text-secondary); font-size: 0.85rem; - margin-top: 6px; font-variant-numeric: tabular-nums; } @@ -498,8 +495,7 @@ } .btn-import:hover { - border-color: var(--accent-deep); - box-shadow: 0 0 0 3px var(--accent-glow); + border-color: var(--border-hover); } .btn-new { @@ -515,14 +511,12 @@ font-size: 0.84rem; font-weight: 500; cursor: pointer; - transition: - background 0.18s ease, - box-shadow 0.18s ease; + transition: filter 0.16s ease, transform 0.16s ease; } .btn-new:hover { - background: var(--accent-hover); - box-shadow: 0 0 0 3px var(--accent-glow); + filter: brightness(1.08); + transform: translateY(-1px); } .plus { diff --git a/src/routes/docs/+layout.svelte b/src/routes/docs/+layout.svelte index ffbfd7b..9f2da9d 100644 --- a/src/routes/docs/+layout.svelte +++ b/src/routes/docs/+layout.svelte @@ -435,16 +435,19 @@ .docs-layout { display: flex; - margin-top: 56px; + align-items: flex-start; + max-width: 1160px; + margin: 56px auto 0; + padding: 0 36px; min-height: calc(100vh - 56px); } /* ── Left Sidebar ─────────────────────────────────── */ .sidebar { - width: 240px; + width: 228px; flex-shrink: 0; - background: var(--bg-secondary); + background: transparent; border-right: 1px solid var(--border); padding: 28px 0; position: sticky; @@ -461,12 +464,12 @@ align-items: center; justify-content: space-between; width: 100%; - font-size: 0.68rem; + font-size: 0.66rem; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.1em; color: var(--text-muted); - padding: 0 24px; + padding: 0 16px; margin-bottom: 8px; margin-top: 24px; background: none; @@ -503,13 +506,13 @@ display: block; width: 100%; text-align: left; - padding: 7px 24px; - font-size: 0.85rem; + padding: 7px 16px; + font-size: 0.82rem; font-weight: 450; color: var(--text-secondary); background: none; border: none; - border-left: 3px solid transparent; + border-left: 2px solid transparent; cursor: pointer; transition: all 0.15s; font-family: inherit; @@ -525,12 +528,13 @@ .sidebar-item.active { color: var(--accent); border-left-color: var(--accent); + border-radius: 0 5px 5px 0; background: var(--accent-glow); font-weight: 600; } .sidebar-footer { - padding: 20px 24px 0; + padding: 20px 16px 0; border-top: 1px solid var(--border); margin-top: 20px; display: flex; @@ -554,21 +558,21 @@ /* ── Right TOC ────────────────────────────────────── */ .toc { - width: 220px; + width: 196px; flex-shrink: 0; position: sticky; top: 56px; height: calc(100vh - 56px); overflow-y: auto; - padding: 32px 16px 32px 0; + padding: 40px 16px 32px 0; border-left: 1px solid var(--border); } .toc-label { - font-size: 0.68rem; + font-size: 0.66rem; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 12px; padding-left: 16px; @@ -589,7 +593,7 @@ .toc-item { display: block; padding: 4px 16px; - font-size: 0.78rem; + font-size: 0.76rem; color: var(--text-muted); text-decoration: none; border-left: 2px solid transparent; @@ -609,7 +613,7 @@ .toc-item.toc-h3 { padding-left: 28px; - font-size: 0.75rem; + font-size: 0.72rem; } /* ── Content Area ─────────────────────────────────── */ @@ -617,8 +621,8 @@ .docs-content { flex: 1; min-width: 0; - padding: 40px 56px 80px; - max-width: 800px; + padding: 40px 44px 96px; + max-width: 780px; view-transition-name: docs-content; } @@ -783,24 +787,22 @@ .prose :global(.copy-btn) { position: absolute; - top: 8px; - right: 8px; - padding: 4px 10px; - font-size: 0.72rem; + top: 9px; + right: 10px; + padding: 3px 9px; + font-size: 0.68rem; font-family: 'JetBrains Mono', monospace; font-weight: 500; color: var(--text-muted); - background: var(--bg-tertiary); + background: transparent; border: 1px solid var(--border); - border-radius: 6px; + border-radius: 5px; cursor: pointer; - opacity: 0; + opacity: 1; transition: all 0.15s; z-index: 5; } - .prose :global(.code-block-wrapper:hover .copy-btn) { opacity: 1; } - .prose :global(.copy-btn:hover) { color: var(--text-primary); border-color: var(--border-hover); @@ -815,7 +817,7 @@ /* ── Prose ─────────────────────────────────────────── */ .prose :global(h1) { - font-size: 1.9rem; + font-size: 2rem; font-weight: 600; letter-spacing: -0.035em; margin-bottom: 12px; @@ -827,14 +829,14 @@ font-size: 1.02rem; color: var(--text-secondary); line-height: 1.7; - margin-bottom: 36px; + margin-bottom: 40px; } .prose :global(h2) { font-size: 1.3rem; font-weight: 600; letter-spacing: -0.025em; - margin-top: 48px; + margin-top: 44px; margin-bottom: 16px; color: var(--text-primary); padding-bottom: 8px; @@ -842,9 +844,9 @@ } .prose :global(h3) { - font-size: 1.08rem; + font-size: 1.05rem; font-weight: 600; - margin-top: 32px; + margin-top: 28px; margin-bottom: 12px; color: var(--text-primary); } @@ -902,10 +904,10 @@ } .prose :global(pre) { - background: var(--code-bg); + background: var(--bg-secondary); border: 1px solid var(--border); - border-radius: 8px; - padding: 20px 24px; + border-radius: 9px; + padding: 16px 18px; margin-bottom: 24px; overflow-x: auto; position: relative; @@ -996,6 +998,8 @@ left: 0; bottom: 0; z-index: 80; + height: auto; + background: var(--bg-primary); transform: translateX(-100%); transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: none; diff --git a/src/routes/docs/[slug]/+page.svelte b/src/routes/docs/[slug]/+page.svelte index 4c04a09..fa0ba8c 100644 --- a/src/routes/docs/[slug]/+page.svelte +++ b/src/routes/docs/[slug]/+page.svelte @@ -52,6 +52,10 @@ })} +

+ > docs{#if meta?.group} / {meta.group.toLowerCase()}{/if} +

+
{#key slug} @@ -61,54 +65,51 @@ diff --git a/src/routes/explore/+page.svelte b/src/routes/explore/+page.svelte index 71a8329..de182da 100644 --- a/src/routes/explore/+page.svelte +++ b/src/routes/explore/+page.svelte @@ -23,8 +23,6 @@ let total = $state(0); let limit = 24; - const cardColors = ['#22c55e', '#3b82f6', '#a855f7', '#f97316', '#ec4899', '#06b6d4', '#eab308', '#f43f5e']; - async function loadConfigs(reset = false) { if (reset) { offset = 0; @@ -168,24 +166,19 @@

No configurations yet

Be the first to share your setup with openboot snapshot

- Get Started + Get started
{:else}
- {#each configs as config, i} - -
+ {#each configs as config} +

{config.name}

- {#if config.description} -

{config.description}

- {/if} +

{config.description}