Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions components/ClearableList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { ReactNode } from 'react';

interface ClearableListProps<T> {
items: T[];
keyOf: (item: T) => string | number | undefined;
emptyLabel: string;
renderRow: (item: T) => ReactNode;
buttonLabel: (item: T) => string;
buttonDisabled: (item: T) => boolean;
onClear: (item: T) => void;
}

export function ClearableList<T>({
items,
keyOf,
emptyLabel,
renderRow,
buttonLabel,
buttonDisabled,
onClear,
}: ClearableListProps<T>) {
return (
<ul className="divide-y divide-stone-200 rounded-md border border-stone-200 dark:divide-stone-700 dark:border-stone-700 max-h-80 overflow-y-auto">
{items.length === 0 && (
<li className="px-3 py-4 text-sm text-stone-500 text-center">{emptyLabel}</li>
)}
{items.map((item) => (
<li key={keyOf(item)} className="flex items-center justify-between px-3 py-2 gap-3">
{renderRow(item)}
<button
onClick={() => onClear(item)}
disabled={buttonDisabled(item)}
className="shrink-0 rounded-md border border-stone-300 hover:bg-stone-100 dark:border-stone-600 dark:hover:bg-stone-700 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 px-3 py-1 text-xs whitespace-nowrap"
>
{buttonLabel(item)}
</button>
</li>
))}
</ul>
);
}
23 changes: 23 additions & 0 deletions components/FilterRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ReactNode } from 'react';

interface FilterRowProps {
value: string;
onChange: (value: string) => void;
placeholder: string;
children: ReactNode;
}

export function FilterRow({ value, onChange, placeholder, children }: FilterRowProps) {
return (
<div className="flex gap-2 mb-2">
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="flex-1 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500"
/>
{children}
</div>
);
}
26 changes: 10 additions & 16 deletions components/HistoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Browser } from 'wxt/browser';
import { useTranslation } from '../hooks/useTranslation';
import { ClearableList } from './ClearableList';

interface HistoryListProps {
items: Browser.history.HistoryItem[];
Expand All @@ -10,21 +11,14 @@ export function HistoryList({ items, onDelete }: HistoryListProps) {
const t = useTranslation();

return (
<ul className="divide-y divide-stone-200 rounded-md border border-stone-200 dark:divide-stone-700 dark:border-stone-700 max-h-80 overflow-y-auto">
{items.length === 0 && (
<li className="px-3 py-4 text-sm text-stone-500 text-center">{t('noMatchingHistory')}</li>
)}
{items.map((item) => (
<li key={item.id} className="flex items-center justify-between px-3 py-2">
<span className="text-sm truncate max-w-md">{item.title || item.url}</span>
<button
onClick={() => item.url && onDelete(item.url)}
className="rounded-md border border-stone-300 hover:bg-stone-100 dark:border-stone-600 dark:hover:bg-stone-700 cursor-pointer px-3 py-1 text-xs"
>
{t('remove')}
</button>
</li>
))}
</ul>
<ClearableList
items={items}
keyOf={(item) => item.id}
emptyLabel={t('noMatchingHistory')}
renderRow={(item) => <span className="text-sm truncate max-w-md">{item.title || item.url}</span>}
buttonLabel={() => t('remove')}
buttonDisabled={() => false}
onClear={(item) => item.url && onDelete(item.url)}
/>
);
}
14 changes: 14 additions & 0 deletions components/SectionHeading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
interface SectionHeadingProps {
as?: 'h2' | 'h3';
title: string;
description?: string;
}

export function SectionHeading({ as: Tag = 'h2', title, description }: SectionHeadingProps) {
return (
<>
<Tag className="text-sm uppercase tracking-wide text-stone-500 dark:text-stone-400 mb-2">{title}</Tag>
{description && <p className="text-xs text-stone-500 mb-2">{description}</p>}
</>
);
}
64 changes: 25 additions & 39 deletions components/TabClearList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Browser } from 'wxt/browser';
import { useTranslation } from '../hooks/useTranslation';
import { tabDomain } from '../utils/clearing';
import { tabHostname } from '../utils/clearing';
import { ClearableList } from './ClearableList';

interface TabClearListProps {
tabs: Browser.tabs.Tab[];
Expand All @@ -10,47 +11,32 @@ interface TabClearListProps {
onClear: (tab: Browser.tabs.Tab) => void;
}

export function TabClearList({
tabs,
busyTabIds,
failedTabIds,
reloadingTabIds,
onClear,
}: TabClearListProps) {
export function TabClearList({ tabs, busyTabIds, failedTabIds, reloadingTabIds, onClear }: TabClearListProps) {
const t = useTranslation();

return (
<ul className="divide-y divide-stone-200 rounded-md border border-stone-200 dark:divide-stone-700 dark:border-stone-700 max-h-80 overflow-y-auto">
{tabs.length === 0 && (
<li className="px-3 py-4 text-sm text-stone-500 text-center">{t('noMatchingTabs')}</li>
<ClearableList
items={tabs}
keyOf={(tab) => tab.id}
emptyLabel={t('noMatchingTabs')}
renderRow={(tab) => (
<span className="min-w-0">
{tabHostname(tab) && (
<span className="block text-xs text-stone-500 truncate">[{tabHostname(tab)}]</span>
)}
<span className="block text-sm truncate">{tab.title ?? tab.url}</span>
</span>
)}
{tabs.map((tab) => {
const busy = tab.id != null && busyTabIds.has(tab.id);
const reloading = tab.id != null && reloadingTabIds?.has(tab.id);
return (
<li key={tab.id} className="flex items-center justify-between px-3 py-2 gap-3">
<span className="min-w-0">
{tabDomain(tab) && (
<span className="block text-xs text-stone-500 truncate">[{tabDomain(tab)}]</span>
)}
<span className="block text-sm truncate">{tab.title ?? tab.url}</span>
</span>
<button
onClick={() => onClear(tab)}
disabled={busy || reloading}
className="rounded-md border border-stone-300 hover:bg-stone-100 dark:border-stone-600 dark:hover:bg-stone-700 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 px-3 py-1 text-xs whitespace-nowrap"
>
{busy
? t('clearing')
: reloading
? t('reloading')
: tab.id != null && failedTabIds.has(tab.id)
? t('failed')
: t('clear')}
</button>
</li>
);
})}
</ul>
buttonLabel={(tab) => {
if (tab.id != null && busyTabIds.has(tab.id)) return t('clearing');
if (tab.id != null && reloadingTabIds?.has(tab.id)) return t('reloading');
if (tab.id != null && failedTabIds.has(tab.id)) return t('failed');
return t('clear');
}}
buttonDisabled={(tab) =>
(tab.id != null && busyTabIds.has(tab.id)) || (tab.id != null && (reloadingTabIds?.has(tab.id) ?? false))
}
onClear={onClear}
/>
);
}
34 changes: 14 additions & 20 deletions components/VisitedSiteList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslation } from '../hooks/useTranslation';
import type { VisitedSite } from '../utils/clearing';
import { ClearableList } from './ClearableList';

interface VisitedSiteListProps {
sites: VisitedSite[];
Expand All @@ -12,25 +13,18 @@ export function VisitedSiteList({ sites, busyHostnames, failedHostnames, onClear
const t = useTranslation();

return (
<ul className="divide-y divide-stone-200 rounded-md border border-stone-200 dark:divide-stone-700 dark:border-stone-700 max-h-80 overflow-y-auto">
{sites.length === 0 && (
<li className="px-3 py-4 text-sm text-stone-500 text-center">{t('noMatchingSites')}</li>
)}
{sites.map((site) => {
const busy = busyHostnames.has(site.hostname);
return (
<li key={site.hostname} className="flex items-center justify-between px-3 py-2 gap-3">
<span className="text-sm truncate min-w-0">{site.hostname}</span>
<button
onClick={() => onClear(site)}
disabled={busy}
className="rounded-md border border-stone-300 hover:bg-stone-100 dark:border-stone-600 dark:hover:bg-stone-700 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 px-3 py-1 text-xs whitespace-nowrap"
>
{busy ? t('clearing') : failedHostnames.has(site.hostname) ? t('failed') : t('clear')}
</button>
</li>
);
})}
</ul>
<ClearableList
items={sites}
keyOf={(site) => site.hostname}
emptyLabel={t('noMatchingSites')}
renderRow={(site) => <span className="text-sm truncate min-w-0">{site.hostname}</span>}
buttonLabel={(site) => {
if (busyHostnames.has(site.hostname)) return t('clearing');
if (failedHostnames.has(site.hostname)) return t('failed');
return t('clear');
}}
buttonDisabled={(site) => busyHostnames.has(site.hostname)}
onClear={onClear}
/>
);
}
6 changes: 2 additions & 4 deletions components/options/GlobalSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from '../../hooks/useTranslation';
import { DATA_TYPES, type DataTypeId } from '../../utils/data-types';
import { DangerConfirmButton } from '../DangerConfirmButton';
import { DataTypeGrid } from '../DataTypeGrid';
import { SectionHeading } from '../SectionHeading';
import { type ClearStatus } from '../StatusButton';
import { HistorySection } from './HistorySection';

Expand Down Expand Up @@ -41,10 +42,7 @@ export function GlobalSection({

return (
<section className="mb-8">
<h2 className="text-sm uppercase tracking-wide text-stone-500 dark:text-stone-400 mb-2">
{t('scopeGlobal')}
</h2>
<p className="text-xs text-stone-500 mb-2">{t('scopeGlobalDescription')}</p>
<SectionHeading title={t('scopeGlobal')} description={t('scopeGlobalDescription')} />
<DataTypeGrid types={DATA_TYPES} selected={selectedTypes} onToggle={onToggleType} className="mb-2" />
<p className="text-xs text-stone-500 mb-3">{t('storageKeyShareNote')}</p>
<div className="max-w-xs">
Expand Down
19 changes: 6 additions & 13 deletions components/options/HistorySection.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Browser } from 'wxt/browser';
import { useTranslation } from '../../hooks/useTranslation';
import { clearAllLabel } from '../../utils/clear-status';
import { FilterRow } from '../FilterRow';
import { HistoryList } from '../HistoryList';
import { SectionHeading } from '../SectionHeading';
import { type ClearStatus } from '../StatusButton';
import { clearAllLabel } from '../../utils/clear-status';

interface HistorySectionProps {
items: Browser.history.HistoryItem[];
Expand All @@ -18,25 +20,16 @@ export function HistorySection({ items, filter, onFilterChange, status, onClearA

return (
<div className="mt-4">
<h3 className="text-sm uppercase tracking-wide text-stone-500 dark:text-stone-400 mb-2">
{t('recentHistory')}
</h3>
<div className="flex gap-2 mb-2">
<input
type="text"
value={filter}
onChange={(e) => onFilterChange(e.target.value)}
placeholder={t('filterHistoryPlaceholder')}
className="flex-1 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500"
/>
<SectionHeading as="h3" title={t('recentHistory')} />
<FilterRow value={filter} onChange={onFilterChange} placeholder={t('filterHistoryPlaceholder')}>
<button
onClick={onClearAll}
disabled={status === 'clearing' || items.length === 0}
className="rounded-md bg-blue-600 hover:bg-blue-500 text-white cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 px-3 py-1.5 text-xs font-medium whitespace-nowrap"
>
{clearAllLabel(t, status, items.length)}
</button>
</div>
</FilterRow>
<HistoryList items={items} onDelete={onDelete} />
</div>
);
Expand Down
Loading
Loading