fix(electrobun): XDG dirs, sanitized errors, themed dropdowns + checkboxes
- PathBrowser: dynamic XDG shortcuts (only shows dirs that exist) - Errors sanitized: "Directory not found" instead of raw ENOENT - Select dropdowns: custom arrow, themed options, appearance:none - Checkboxes: custom styled with --ctp-blue check, themed border
This commit is contained in:
parent
f71ca2e1ca
commit
d8b831089a
2 changed files with 81 additions and 7 deletions
|
|
@ -10,12 +10,34 @@
|
||||||
let { onSelect, onClose }: Props = $props();
|
let { onSelect, onClose }: Props = $props();
|
||||||
|
|
||||||
let HOME = $state('/home');
|
let HOME = $state('/home');
|
||||||
const SHORTCUTS = [
|
let shortcuts = $state([
|
||||||
{ label: 'Home', path: '~' },
|
{ label: 'Home', path: '~' },
|
||||||
{ label: 'Desktop', path: '~/Desktop' },
|
|
||||||
{ label: 'Documents', path: '~/Documents' },
|
|
||||||
{ label: '/tmp', path: '/tmp' },
|
{ label: '/tmp', path: '/tmp' },
|
||||||
];
|
]);
|
||||||
|
|
||||||
|
// Load XDG user directories from backend
|
||||||
|
async function loadXdgDirs() {
|
||||||
|
try {
|
||||||
|
const r = await appRpc.request['files.homeDir']({});
|
||||||
|
if (r?.path) HOME = r.path;
|
||||||
|
// Check which XDG dirs actually exist
|
||||||
|
const candidates = [
|
||||||
|
{ label: 'Desktop', env: 'XDG_DESKTOP_DIR', fallback: `${HOME}/Desktop` },
|
||||||
|
{ label: 'Documents', env: 'XDG_DOCUMENTS_DIR', fallback: `${HOME}/Documents` },
|
||||||
|
{ label: 'Downloads', env: 'XDG_DOWNLOAD_DIR', fallback: `${HOME}/Downloads` },
|
||||||
|
{ label: 'Projects', env: 'XDG_PROJECTS_DIR', fallback: `${HOME}/Projects` },
|
||||||
|
];
|
||||||
|
// Validate each via files.browse (only add if dir exists)
|
||||||
|
const existing: typeof shortcuts = [{ label: 'Home', path: '~' }];
|
||||||
|
for (const c of candidates) {
|
||||||
|
const path = c.fallback.replace(HOME, '~');
|
||||||
|
const res = await appRpc.request['files.browse']({ path: c.fallback }).catch(() => null);
|
||||||
|
if (res && !res.error) existing.push({ label: c.label, path });
|
||||||
|
}
|
||||||
|
existing.push({ label: '/tmp', path: '/tmp' });
|
||||||
|
shortcuts = existing;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
let currentPath = $state('~');
|
let currentPath = $state('~');
|
||||||
let entries = $state<Array<{ name: string; type: 'file' | 'dir'; size: number }>>([]);
|
let entries = $state<Array<{ name: string; type: 'file' | 'dir'; size: number }>>([]);
|
||||||
|
|
@ -65,7 +87,15 @@
|
||||||
}
|
}
|
||||||
currentPath = dirPath;
|
currentPath = dirPath;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err instanceof Error ? err.message : String(err);
|
const raw = err instanceof Error ? err.message : String(err);
|
||||||
|
// Sanitize: show user-friendly message, not raw ENOENT
|
||||||
|
if (raw.includes('ENOENT') || raw.includes('no such file')) {
|
||||||
|
error = 'Directory not found';
|
||||||
|
} else if (raw.includes('EACCES') || raw.includes('permission')) {
|
||||||
|
error = 'Permission denied';
|
||||||
|
} else {
|
||||||
|
error = 'Cannot access this directory';
|
||||||
|
}
|
||||||
entries = [];
|
entries = [];
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
@ -89,6 +119,7 @@
|
||||||
|
|
||||||
// Load initial directory
|
// Load initial directory
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
loadXdgDirs();
|
||||||
loadDir('~');
|
loadDir('~');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -102,7 +133,7 @@
|
||||||
|
|
||||||
<!-- Shortcuts -->
|
<!-- Shortcuts -->
|
||||||
<div class="pb-shortcuts">
|
<div class="pb-shortcuts">
|
||||||
{#each SHORTCUTS as sc}
|
{#each shortcuts as sc}
|
||||||
<button class="pb-shortcut" onclick={() => navigateTo(sc.path)}>{sc.label}</button>
|
<button class="pb-shortcut" onclick={() => navigateTo(sc.path)}>{sc.label}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,50 @@
|
||||||
.wz-input:focus, .wz-select:focus, .wz-textarea:focus { outline: none; border-color: var(--ctp-blue); }
|
.wz-input:focus, .wz-select:focus, .wz-textarea:focus { outline: none; border-color: var(--ctp-blue); }
|
||||||
.wz-input::placeholder, .wz-textarea::placeholder { color: var(--ctp-overlay0); }
|
.wz-input::placeholder, .wz-textarea::placeholder { color: var(--ctp-overlay0); }
|
||||||
.wz-textarea { resize: vertical; min-height: 3rem; }
|
.wz-textarea { resize: vertical; min-height: 3rem; }
|
||||||
.wz-select { cursor: pointer; }
|
.wz-select {
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a6adc8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
padding-right: 1.75rem;
|
||||||
|
}
|
||||||
|
.wz-select option {
|
||||||
|
background: var(--ctp-surface0);
|
||||||
|
color: var(--ctp-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Themed checkboxes */
|
||||||
|
input[type="checkbox"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 1rem; height: 1rem;
|
||||||
|
border: 1px solid var(--ctp-surface2);
|
||||||
|
border-radius: 0.1875rem;
|
||||||
|
background: var(--ctp-surface0);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
input[type="checkbox"]:checked {
|
||||||
|
background: var(--ctp-blue);
|
||||||
|
border-color: var(--ctp-blue);
|
||||||
|
}
|
||||||
|
input[type="checkbox"]:checked::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0.25rem; top: 0.0625rem;
|
||||||
|
width: 0.3125rem; height: 0.5625rem;
|
||||||
|
border: solid var(--ctp-base);
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
input[type="checkbox"]:focus-visible {
|
||||||
|
outline: 2px solid var(--ctp-blue);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.wz-radios { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
.wz-radios { display: flex; flex-wrap: wrap; gap: 0.375rem; }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue