Skip to content

Commit 53447cd

Browse files
committed
feat: add macOS preferences catalog module
Include the macos-prefs-catalog.ts and updated ConfigDetail.svelte that were created in a prior session but not committed.
1 parent 8b07642 commit 53447cd

File tree

2 files changed

+180
-33
lines changed

2 files changed

+180
-33
lines changed

src/lib/components/ConfigDetail.svelte

Lines changed: 119 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import { goto } from '$app/navigation';
3+
import { getCatalogItem } from '$lib/macos-prefs-catalog';
34
45
let {
56
configUser,
@@ -143,6 +144,24 @@
143144
const snapshot = $derived(config.snapshot || {});
144145
const snapshotPkgs = $derived(snapshot.packages || {});
145146
const macosPrefs = $derived(snapshot.macos_prefs || []);
147+
interface PrefGroup {
148+
category: string;
149+
items: { pref: any; catalogItem: ReturnType<typeof getCatalogItem> }[];
150+
}
151+
const groupedMacosPrefs = $derived.by((): PrefGroup[] => {
152+
const result: PrefGroup[] = [];
153+
for (const pref of macosPrefs) {
154+
const catalogItem = getCatalogItem(pref.domain, pref.key);
155+
const category = catalogItem?.category ?? 'Custom';
156+
const existing = result.find((g: PrefGroup) => g.category === category);
157+
if (existing) {
158+
existing.items = [...existing.items, { pref, catalogItem }];
159+
} else {
160+
result.push({ category, items: [{ pref, catalogItem }] });
161+
}
162+
}
163+
return result;
164+
});
146165
const shell = $derived(snapshot.shell || {});
147166
const git = $derived(snapshot.git || {});
148167
const devToolsRaw = $derived(snapshot.dev_tools || []);
@@ -417,17 +436,38 @@
417436
{#if macosPrefs.length > 0}
418437
<section class="section">
419438
<h2 class="section-title">🍎 macOS Preferences</h2>
420-
<div class="prefs">
421-
{#each macosPrefs as pref}
422-
<div class="pref">
423-
<div class="pref-header">
424-
<span class="pref-key">{pref.key}</span>
425-
<span class="pref-domain">{pref.domain}</span>
439+
<div class="prefs-groups">
440+
{#each groupedMacosPrefs as group}
441+
<div class="prefs-category-block">
442+
<div class="prefs-category-name">{group.category}</div>
443+
<div class="prefs-category-items">
444+
{#each group.items as { pref, catalogItem }}
445+
<div class="pref-card">
446+
<div class="pref-card-body">
447+
<div class="pref-card-label">{catalogItem?.label ?? pref.key}</div>
448+
{#if catalogItem?.description}
449+
<div class="pref-card-desc">{catalogItem.description}</div>
450+
{:else if pref.desc}
451+
<div class="pref-card-desc">{pref.desc}</div>
452+
{:else}
453+
<div class="pref-card-desc pref-card-raw">{pref.domain} · {pref.key}</div>
454+
{/if}
455+
</div>
456+
<div class="pref-card-value">
457+
{#if (catalogItem?.type ?? pref.type) === 'bool'}
458+
<span class="pref-bool {pref.value === 'true' ? 'pref-bool-on' : 'pref-bool-off'}">
459+
{pref.value === 'true' ? 'ON' : 'OFF'}
460+
</span>
461+
{:else if catalogItem?.options}
462+
{@const opt = catalogItem.options.find((o: { value: string; label: string }) => o.value === pref.value)}
463+
<span class="pref-option-val">{opt?.label ?? pref.value}</span>
464+
{:else}
465+
<span class="pref-raw-val">{pref.value}</span>
466+
{/if}
467+
</div>
468+
</div>
469+
{/each}
426470
</div>
427-
{#if pref.desc}
428-
<p class="pref-desc">{pref.desc}</p>
429-
{/if}
430-
<div class="pref-val">{pref.value}</div>
431471
</div>
432472
{/each}
433473
</div>
@@ -1086,56 +1126,102 @@
10861126
color: var(--text-primary);
10871127
}
10881128
1089-
.prefs {
1129+
.prefs-groups {
10901130
display: flex;
10911131
flex-direction: column;
1092-
gap: 16px;
1132+
gap: 28px;
10931133
}
10941134
1095-
.pref {
1096-
padding: 20px;
1097-
background: var(--bg-tertiary);
1098-
border: 1px solid var(--border);
1099-
border-radius: 12px;
1135+
.prefs-category-block {
1136+
display: flex;
1137+
flex-direction: column;
1138+
gap: 10px;
1139+
}
1140+
1141+
.prefs-category-name {
1142+
font-size: 0.75rem;
1143+
font-weight: 700;
1144+
text-transform: uppercase;
1145+
letter-spacing: 0.1em;
1146+
color: var(--text-muted);
1147+
padding-bottom: 8px;
1148+
border-bottom: 1px solid var(--border);
11001149
}
11011150
1102-
.pref-header {
1151+
.prefs-category-items {
1152+
display: flex;
1153+
flex-direction: column;
1154+
gap: 8px;
1155+
}
1156+
1157+
.pref-card {
11031158
display: flex;
1104-
justify-content: space-between;
11051159
align-items: center;
1106-
margin-bottom: 10px;
1160+
justify-content: space-between;
1161+
gap: 16px;
1162+
padding: 14px 18px;
1163+
background: var(--bg-tertiary);
1164+
border: 1px solid var(--border);
1165+
border-radius: 10px;
11071166
}
11081167
1109-
.pref-key {
1110-
font-family: 'JetBrains Mono', monospace;
1168+
.pref-card-body {
1169+
flex: 1;
1170+
min-width: 0;
1171+
}
1172+
1173+
.pref-card-label {
11111174
font-size: 0.95rem;
11121175
font-weight: 600;
11131176
color: var(--text-primary);
1177+
margin-bottom: 3px;
11141178
}
11151179
1116-
.pref-domain {
1117-
font-size: 0.8rem;
1180+
.pref-card-desc {
1181+
font-size: 0.82rem;
11181182
color: var(--text-muted);
1119-
background: var(--bg-secondary);
1183+
line-height: 1.4;
1184+
}
1185+
1186+
.pref-card-raw {
1187+
font-family: 'JetBrains Mono', monospace;
1188+
font-size: 0.78rem;
1189+
}
1190+
1191+
.pref-card-value {
1192+
flex-shrink: 0;
1193+
}
1194+
1195+
.pref-bool {
1196+
display: inline-block;
11201197
padding: 4px 10px;
11211198
border-radius: 6px;
1199+
font-size: 0.78rem;
1200+
font-weight: 700;
1201+
letter-spacing: 0.05em;
11221202
}
11231203
1124-
.pref-desc {
1125-
font-size: 0.9rem;
1126-
color: var(--text-secondary);
1127-
margin: 0 0 10px;
1128-
line-height: 1.5;
1204+
.pref-bool-on {
1205+
background: rgba(34, 197, 94, 0.15);
1206+
color: var(--accent);
1207+
border: 1px solid rgba(34, 197, 94, 0.3);
11291208
}
11301209
1131-
.pref-val {
1132-
padding: 12px 16px;
1210+
.pref-bool-off {
11331211
background: var(--bg-secondary);
1212+
color: var(--text-muted);
11341213
border: 1px solid var(--border);
1135-
border-radius: 8px;
1214+
}
1215+
1216+
.pref-option-val,
1217+
.pref-raw-val {
11361218
font-family: 'JetBrains Mono', monospace;
11371219
font-size: 0.85rem;
11381220
color: var(--accent);
1221+
background: var(--bg-secondary);
1222+
padding: 4px 10px;
1223+
border-radius: 6px;
1224+
border: 1px solid var(--border);
11391225
}
11401226
11411227
.cta {

src/lib/macos-prefs-catalog.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export interface MacOSPrefCatalogItem {
2+
id: string;
3+
category: string;
4+
label: string;
5+
description: string;
6+
domain: string;
7+
key: string;
8+
type: 'bool' | 'int' | 'float' | 'string';
9+
defaultValue: string;
10+
options?: { value: string; label: string }[];
11+
min?: number;
12+
max?: number;
13+
}
14+
15+
export const MACOS_PREF_CATALOG: MacOSPrefCatalogItem[] = [
16+
// Dock
17+
{ id: 'dock-autohide', category: 'Dock', label: 'Auto-hide Dock', description: 'Automatically hide and show the Dock when not in use', domain: 'com.apple.dock', key: 'autohide', type: 'bool', defaultValue: 'true' },
18+
{ id: 'dock-autohide-delay', category: 'Dock', label: 'Auto-hide delay (s)', description: 'Delay before the Dock appears when hovering — 0 for instant', domain: 'com.apple.dock', key: 'autohide-delay', type: 'float', defaultValue: '0', min: 0, max: 5 },
19+
{ id: 'dock-tilesize', category: 'Dock', label: 'Icon size', description: 'Size of Dock icons in pixels', domain: 'com.apple.dock', key: 'tilesize', type: 'int', defaultValue: '48', min: 16, max: 128 },
20+
{ id: 'dock-show-recents', category: 'Dock', label: 'Show recent apps', description: 'Show recently used apps in a separate section of the Dock', domain: 'com.apple.dock', key: 'show-recents', type: 'bool', defaultValue: 'false' },
21+
{ id: 'dock-orientation', category: 'Dock', label: 'Position on screen', description: 'Which edge of the screen the Dock appears on', domain: 'com.apple.dock', key: 'orientation', type: 'string', defaultValue: 'bottom', options: [{ value: 'bottom', label: 'Bottom' }, { value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }] },
22+
{ id: 'dock-mineffect', category: 'Dock', label: 'Minimize animation', description: 'Visual effect when minimizing windows to the Dock', domain: 'com.apple.dock', key: 'mineffect', type: 'string', defaultValue: 'genie', options: [{ value: 'genie', label: 'Genie' }, { value: 'scale', label: 'Scale' }] },
23+
{ id: 'dock-static-only', category: 'Dock', label: 'Show only active apps', description: 'Hide non-running apps from the Dock (cleaner, minimalist look)', domain: 'com.apple.dock', key: 'static-only', type: 'bool', defaultValue: 'false' },
24+
{ id: 'dock-launchanim', category: 'Dock', label: 'Animate app launches', description: 'Bounce icons in the Dock when launching an app', domain: 'com.apple.dock', key: 'launchanim', type: 'bool', defaultValue: 'true' },
25+
26+
// Finder
27+
{ id: 'finder-view-style', category: 'Finder', label: 'Default view style', description: 'How files are displayed in new Finder windows', domain: 'com.apple.finder', key: 'FXPreferredViewStyle', type: 'string', defaultValue: 'Nlsv', options: [{ value: 'icnv', label: 'Icon' }, { value: 'Nlsv', label: 'List' }, { value: 'clmv', label: 'Column' }, { value: 'glyv', label: 'Gallery' }] },
28+
{ id: 'finder-new-window', category: 'Finder', label: 'New window opens', description: 'Default location when opening a new Finder window', domain: 'com.apple.finder', key: 'NewWindowTarget', type: 'string', defaultValue: 'PfHm', options: [{ value: 'PfHm', label: 'Home folder' }, { value: 'PfDo', label: 'Documents' }, { value: 'PfDe', label: 'Desktop' }, { value: 'PfCm', label: 'Computer' }] },
29+
{ id: 'finder-search-scope', category: 'Finder', label: 'Default search scope', description: 'Where Finder searches by default when you use the search bar', domain: 'com.apple.finder', key: 'FXDefaultSearchScope', type: 'string', defaultValue: 'SCcf', options: [{ value: 'SCev', label: 'This Mac' }, { value: 'SCcf', label: 'Current folder' }, { value: 'SCsp', label: 'Previous scope' }] },
30+
{ id: 'finder-show-pathbar', category: 'Finder', label: 'Show path bar', description: 'Display the full folder path at the bottom of Finder windows', domain: 'com.apple.finder', key: 'ShowPathbar', type: 'bool', defaultValue: 'true' },
31+
{ id: 'finder-show-statusbar', category: 'Finder', label: 'Show status bar', description: 'Display item count and available disk space at the bottom of windows', domain: 'com.apple.finder', key: 'ShowStatusBar', type: 'bool', defaultValue: 'true' },
32+
{ id: 'finder-show-hidden', category: 'Finder', label: 'Show hidden files', description: 'Show files and folders whose names start with a dot', domain: 'com.apple.finder', key: 'AppleShowAllFiles', type: 'bool', defaultValue: 'false' },
33+
{ id: 'finder-show-extensions', category: 'Finder', label: 'Always show file extensions', description: 'Show filename extensions for all files, even when macOS would hide them', domain: 'NSGlobalDomain', key: 'AppleShowAllExtensions', type: 'bool', defaultValue: 'true' },
34+
{ id: 'finder-posix-path', category: 'Finder', label: 'Show full path in title bar', description: 'Display the complete POSIX path in the Finder window title', domain: 'com.apple.finder', key: '_FXShowPosixPathInTitle', type: 'bool', defaultValue: 'false' },
35+
36+
// Trackpad
37+
{ id: 'trackpad-tap-click', category: 'Trackpad', label: 'Tap to click', description: 'Tap the trackpad with one finger to click, instead of pressing down', domain: 'com.apple.AppleMultitouchTrackpad', key: 'Clicking', type: 'bool', defaultValue: 'true' },
38+
{ id: 'trackpad-natural-scroll', category: 'Trackpad', label: 'Natural scrolling', description: 'Scroll content in the direction your fingers move (iOS-style)', domain: 'NSGlobalDomain', key: 'com.apple.swipescrolldirection', type: 'bool', defaultValue: 'true' },
39+
{ id: 'trackpad-three-finger-drag', category: 'Trackpad', label: 'Three-finger drag', description: 'Drag windows and items using three fingers on the trackpad', domain: 'com.apple.AppleMultitouchTrackpad', key: 'TrackpadThreeFingerDrag', type: 'bool', defaultValue: 'false' },
40+
41+
// Desktop
42+
{ id: 'desktop-click-to-show', category: 'Desktop', label: 'Click desktop to show it', description: 'Click the desktop wallpaper to bring it to the front and hide all windows', domain: 'com.apple.WindowManager', key: 'EnableStandardClickToShowDesktop', type: 'bool', defaultValue: 'false' },
43+
44+
// Keyboard
45+
{ id: 'keyboard-key-repeat', category: 'Keyboard', label: 'Key repeat rate', description: 'How fast keys repeat when held down — lower number = faster (min 1)', domain: 'NSGlobalDomain', key: 'KeyRepeat', type: 'int', defaultValue: '2', min: 1, max: 15 },
46+
{ id: 'keyboard-initial-repeat', category: 'Keyboard', label: 'Delay until key repeat', description: 'How long to hold a key before it starts repeating — lower = shorter delay', domain: 'NSGlobalDomain', key: 'InitialKeyRepeat', type: 'int', defaultValue: '15', min: 10, max: 120 },
47+
{ id: 'keyboard-press-hold', category: 'Keyboard', label: 'Press and hold for accents', description: 'Show accent character menu when holding a key (disable for faster key repeat)', domain: 'NSGlobalDomain', key: 'ApplePressAndHoldEnabled', type: 'bool', defaultValue: 'false' },
48+
49+
// Screenshots
50+
{ id: 'screenshot-type', category: 'Screenshots', label: 'Screenshot format', description: 'File format used when saving screenshots', domain: 'com.apple.screencapture', key: 'type', type: 'string', defaultValue: 'png', options: [{ value: 'png', label: 'PNG' }, { value: 'jpg', label: 'JPEG' }, { value: 'pdf', label: 'PDF' }, { value: 'tiff', label: 'TIFF' }] },
51+
{ id: 'screenshot-no-shadow', category: 'Screenshots', label: 'Disable window shadows', description: 'Remove drop shadows from screenshots of individual windows', domain: 'com.apple.screencapture', key: 'disable-shadow', type: 'bool', defaultValue: 'true' },
52+
53+
// Menu Bar
54+
{ id: 'menubar-sound', category: 'Menu Bar', label: 'Show Sound in menu bar', description: 'Always show the volume/sound control in the menu bar', domain: 'com.apple.controlcenter', key: 'NSStatusItem Visible Sound', type: 'bool', defaultValue: 'true' },
55+
];
56+
57+
export const CATALOG_CATEGORIES = [...new Set(MACOS_PREF_CATALOG.map(p => p.category))];
58+
59+
export function getCatalogItem(domain: string, key: string): MacOSPrefCatalogItem | undefined {
60+
return MACOS_PREF_CATALOG.find(p => p.domain === domain && p.key === key);
61+
}

0 commit comments

Comments
 (0)