refactor frontend

This commit is contained in:
Julian Freeman
2026-04-16 16:26:21 -04:00
parent 6cc694754f
commit dabd8789f4
14 changed files with 1574 additions and 1357 deletions

View File

@@ -0,0 +1,69 @@
<script setup lang="ts">
import SortDropdown from "../SortDropdown.vue";
import type { BookmarkSortKey, BookmarkSummary } from "../../types/browser";
defineProps<{
bookmarks: BookmarkSummary[];
sortKey: BookmarkSortKey;
domainFromUrl: (url: string) => string;
bookmarkProfilesExpanded: (url: string) => boolean;
}>();
const emit = defineEmits<{
"update:sortKey": [value: BookmarkSortKey];
toggleProfiles: [url: string];
}>();
</script>
<template>
<section class="content-section">
<div class="sort-bar">
<SortDropdown
:model-value="sortKey"
label="Sort by"
:options="[
{ label: 'Name', value: 'title' },
{ label: 'URL', value: 'url' },
]"
@update:model-value="emit('update:sortKey', $event as BookmarkSortKey)"
/>
</div>
<div v-if="bookmarks.length" class="bookmark-list">
<article v-for="bookmark in bookmarks" :key="bookmark.url" class="bookmark-card">
<div class="bookmark-body">
<div class="bookmark-topline">
<h4>{{ bookmark.title }}</h4>
<span class="badge neutral">{{ domainFromUrl(bookmark.url) }}</span>
</div>
<p class="bookmark-url" :title="bookmark.url">{{ bookmark.url }}</p>
<div class="source-disclosure">
<button
class="disclosure-button"
type="button"
@click="emit('toggleProfiles', bookmark.url)"
>
<span>Profiles</span>
<span class="badge neutral">{{ bookmark.profileIds.length }}</span>
</button>
<div
v-if="bookmarkProfilesExpanded(bookmark.url)"
class="disclosure-panel"
>
<span
v-for="profileId in bookmark.profileIds"
:key="`${bookmark.url}-${profileId}`"
class="badge"
>
{{ profileId }}
</span>
</div>
</div>
</div>
</article>
</div>
<div v-else class="empty-card">
<p>No bookmarks were discovered for this browser.</p>
</div>
</section>
</template>

View File

@@ -0,0 +1,105 @@
<script setup lang="ts">
import type {
ActiveSection,
BookmarkSortKey,
BrowserView,
ExtensionSortKey,
ProfileSortKey,
} from "../../types/browser";
import BookmarksList from "./BookmarksList.vue";
import ExtensionsList from "./ExtensionsList.vue";
import ProfilesList from "./ProfilesList.vue";
defineProps<{
currentBrowser: BrowserView;
activeSection: ActiveSection;
profileSortKey: ProfileSortKey;
extensionSortKey: ExtensionSortKey;
bookmarkSortKey: BookmarkSortKey;
sortedProfiles: BrowserView["profiles"];
sortedExtensions: BrowserView["extensions"];
sortedBookmarks: BrowserView["bookmarks"];
openProfileError: string;
sectionCount: (section: ActiveSection) => number;
isOpeningProfile: (browserId: string, profileId: string) => boolean;
extensionMonogram: (name: string) => string;
extensionProfilesExpanded: (extensionId: string) => boolean;
bookmarkProfilesExpanded: (url: string) => boolean;
domainFromUrl: (url: string) => string;
}>();
const emit = defineEmits<{
"update:activeSection": [value: ActiveSection];
"update:profileSortKey": [value: ProfileSortKey];
"update:extensionSortKey": [value: ExtensionSortKey];
"update:bookmarkSortKey": [value: BookmarkSortKey];
openProfile: [browserId: string, profileId: string];
toggleExtensionProfiles: [extensionId: string];
toggleBookmarkProfiles: [url: string];
}>();
</script>
<template>
<section class="section-tabs">
<button
class="section-tab"
:class="{ active: activeSection === 'profiles' }"
type="button"
@click="emit('update:activeSection', 'profiles')"
>
<span>Profiles</span>
<span class="count-pill">{{ sectionCount("profiles") }}</span>
</button>
<button
class="section-tab"
:class="{ active: activeSection === 'extensions' }"
type="button"
@click="emit('update:activeSection', 'extensions')"
>
<span>Extensions</span>
<span class="count-pill">{{ sectionCount("extensions") }}</span>
</button>
<button
class="section-tab"
:class="{ active: activeSection === 'bookmarks' }"
type="button"
@click="emit('update:activeSection', 'bookmarks')"
>
<span>Bookmarks</span>
<span class="count-pill">{{ sectionCount("bookmarks") }}</span>
</button>
</section>
<div class="content-scroll-area">
<ProfilesList
v-if="activeSection === 'profiles'"
:profiles="sortedProfiles"
:sort-key="profileSortKey"
:open-profile-error="openProfileError"
:browser-id="currentBrowser.browserId"
:is-opening-profile="isOpeningProfile"
@update:sort-key="emit('update:profileSortKey', $event)"
@open-profile="(browserId, profileId) => emit('openProfile', browserId, profileId)"
/>
<ExtensionsList
v-else-if="activeSection === 'extensions'"
:extensions="sortedExtensions"
:sort-key="extensionSortKey"
:extension-monogram="extensionMonogram"
:extension-profiles-expanded="extensionProfilesExpanded"
@update:sort-key="emit('update:extensionSortKey', $event)"
@toggle-profiles="emit('toggleExtensionProfiles', $event)"
/>
<BookmarksList
v-else
:bookmarks="sortedBookmarks"
:sort-key="bookmarkSortKey"
:domain-from-url="domainFromUrl"
:bookmark-profiles-expanded="bookmarkProfilesExpanded"
@update:sort-key="emit('update:bookmarkSortKey', $event)"
@toggle-profiles="emit('toggleBookmarkProfiles', $event)"
/>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import SortDropdown from "../SortDropdown.vue";
import type { ExtensionSortKey, ExtensionSummary } from "../../types/browser";
defineProps<{
extensions: ExtensionSummary[];
sortKey: ExtensionSortKey;
extensionMonogram: (name: string) => string;
extensionProfilesExpanded: (extensionId: string) => boolean;
}>();
const emit = defineEmits<{
"update:sortKey": [value: ExtensionSortKey];
toggleProfiles: [extensionId: string];
}>();
</script>
<template>
<section class="content-section">
<div class="sort-bar">
<SortDropdown
:model-value="sortKey"
label="Sort by"
:options="[
{ label: 'Name', value: 'name' },
{ label: 'Extension ID', value: 'id' },
]"
@update:model-value="emit('update:sortKey', $event as ExtensionSortKey)"
/>
</div>
<div v-if="extensions.length" class="stack-list">
<article
v-for="extension in extensions"
:key="extension.id"
class="extension-card"
>
<div class="extension-icon">
<img
v-if="extension.iconDataUrl"
:src="extension.iconDataUrl"
:alt="`${extension.name} icon`"
/>
<span v-else>{{ extensionMonogram(extension.name) }}</span>
</div>
<div class="extension-body">
<div class="extension-topline">
<h4>{{ extension.name }}</h4>
<span v-if="extension.version" class="badge neutral">
v{{ extension.version }}
</span>
</div>
<p class="meta-line">{{ extension.id }}</p>
<div class="source-disclosure">
<button
class="disclosure-button"
type="button"
@click="emit('toggleProfiles', extension.id)"
>
<span>Profiles</span>
<span class="badge neutral">{{ extension.profileIds.length }}</span>
</button>
<div
v-if="extensionProfilesExpanded(extension.id)"
class="disclosure-panel"
>
<span
v-for="profileId in extension.profileIds"
:key="`${extension.id}-${profileId}`"
class="badge"
>
{{ profileId }}
</span>
</div>
</div>
</div>
</article>
</div>
<div v-else class="empty-card">
<p>No extensions were discovered for this browser.</p>
</div>
</section>
</template>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import SortDropdown from "../SortDropdown.vue";
import type { ProfileSortKey, ProfileSummary } from "../../types/browser";
defineProps<{
profiles: ProfileSummary[];
sortKey: ProfileSortKey;
openProfileError: string;
browserId: string;
isOpeningProfile: (browserId: string, profileId: string) => boolean;
}>();
const emit = defineEmits<{
"update:sortKey": [value: ProfileSortKey];
openProfile: [browserId: string, profileId: string];
}>();
</script>
<template>
<section class="content-section">
<div v-if="openProfileError" class="inline-error">
{{ openProfileError }}
</div>
<div class="sort-bar">
<SortDropdown
:model-value="sortKey"
label="Sort by"
:options="[
{ label: 'Name', value: 'name' },
{ label: 'Email', value: 'email' },
{ label: 'Profile ID', value: 'id' },
]"
@update:model-value="emit('update:sortKey', $event as ProfileSortKey)"
/>
</div>
<div v-if="profiles.length" class="stack-list">
<article v-for="profile in profiles" :key="profile.id" class="profile-card">
<div class="profile-avatar">
<img
v-if="profile.avatarDataUrl"
:src="profile.avatarDataUrl"
:alt="`${profile.name} avatar`"
/>
<span v-else>{{ profile.avatarLabel }}</span>
</div>
<div class="profile-body">
<div class="profile-topline">
<h4>{{ profile.name }}</h4>
<div class="profile-actions">
<button
class="card-action-button"
:disabled="isOpeningProfile(browserId, profile.id)"
type="button"
@click="emit('openProfile', browserId, profile.id)"
>
{{ isOpeningProfile(browserId, profile.id) ? "Opening..." : "Open" }}
</button>
<span class="badge neutral">{{ profile.id }}</span>
</div>
</div>
<p class="profile-email">{{ profile.email || "No email found" }}</p>
</div>
</article>
</div>
<div v-else class="empty-card">
<p>No profile directories were found for this browser.</p>
</div>
</section>
</template>