feat: add compact Toolbox tiles and details overlay
This commit is contained in:
91
src/App.tsx
91
src/App.tsx
@@ -16,8 +16,9 @@ import {
|
||||
ToolboxHeader,
|
||||
type ToolboxHeaderApp,
|
||||
} from '@add-ideas/toolbox-shell-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AppCard } from './components/AppCard';
|
||||
import { AppDetailsPanel } from './components/AppDetailsPanel';
|
||||
import { HelpPanel } from './components/HelpPanel';
|
||||
import { Logo } from './components/Logo';
|
||||
import { PreferencesPanel } from './components/PreferencesPanel';
|
||||
@@ -34,6 +35,9 @@ type LoadState =
|
||||
| { status: 'error'; message: string }
|
||||
| { status: 'ready'; value: LoadedCatalogue };
|
||||
|
||||
type PortalOverlay =
|
||||
{ kind: 'help' } | { kind: 'details'; appId: string } | null;
|
||||
|
||||
function appMatches(
|
||||
app: ResolvedApp,
|
||||
query: string,
|
||||
@@ -50,29 +54,25 @@ function appMatches(
|
||||
}
|
||||
|
||||
interface AppSectionProps {
|
||||
activeTag: string;
|
||||
apps: ResolvedApp[];
|
||||
catalogueUrl: string;
|
||||
hiddenIds: readonly string[];
|
||||
id: string;
|
||||
onCategory: (category: string) => void;
|
||||
onDetails: (id: string) => void;
|
||||
onHide: (id: string) => void;
|
||||
onPin: (id: string) => void;
|
||||
onTag: (tag: string) => void;
|
||||
pinned: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function AppSection({
|
||||
activeTag,
|
||||
apps,
|
||||
catalogueUrl,
|
||||
hiddenIds,
|
||||
id,
|
||||
onCategory,
|
||||
onDetails,
|
||||
onHide,
|
||||
onPin,
|
||||
onTag,
|
||||
pinned,
|
||||
title,
|
||||
}: AppSectionProps) {
|
||||
@@ -97,11 +97,9 @@ function AppSection({
|
||||
contextualUrl={contextualLaunchUrl}
|
||||
pinned={pinned}
|
||||
hidden={hiddenIds.includes(app.id)}
|
||||
activeTag={activeTag}
|
||||
onCategory={onCategory}
|
||||
onDetails={() => onDetails(app.id)}
|
||||
onPin={() => onPin(app.id)}
|
||||
onHide={() => onHide(app.id)}
|
||||
onTag={onTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -119,9 +117,10 @@ export default function App() {
|
||||
const [shownHiddenIds, setShownHiddenIds] = useState<
|
||||
readonly string[] | null
|
||||
>(null);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [overlay, setOverlay] = useState<PortalOverlay>(null);
|
||||
const hiddenToggle = useRef<HTMLInputElement>(null);
|
||||
const [preferences, setPreferences, storageAvailable] = usePreferences();
|
||||
const closeHelp = useCallback(() => setHelpOpen(false), []);
|
||||
const closeOverlay = useCallback(() => setOverlay(null), []);
|
||||
const showHidden =
|
||||
preferences.hidden.length > 0 &&
|
||||
shownHiddenIds?.some((id) => preferences.hidden.includes(id)) === true;
|
||||
@@ -150,6 +149,11 @@ export default function App() {
|
||||
|
||||
const loaded = loadState.status === 'ready' ? loadState.value : undefined;
|
||||
const apps = useMemo(() => loaded?.apps ?? [], [loaded]);
|
||||
const detailsApp =
|
||||
overlay?.kind === 'details'
|
||||
? apps.find((app) => app.id === overlay.appId)
|
||||
: undefined;
|
||||
const overlayOpen = overlay?.kind === 'help' || detailsApp !== undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded) return;
|
||||
@@ -206,6 +210,7 @@ export default function App() {
|
||||
}));
|
||||
|
||||
function toggleListItem(field: 'pinned' | 'hidden', id: string) {
|
||||
const keepCardVisible = field === 'pinned' || showHidden;
|
||||
if (field === 'hidden' && showHidden && !preferences.hidden.includes(id)) {
|
||||
setShownHiddenIds((current) => [...new Set([...(current ?? []), id])]);
|
||||
}
|
||||
@@ -215,6 +220,20 @@ export default function App() {
|
||||
? current[field].filter((item) => item !== id)
|
||||
: [...current[field], id],
|
||||
}));
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!keepCardVisible) {
|
||||
hiddenToggle.current?.focus();
|
||||
return;
|
||||
}
|
||||
const action = field === 'pinned' ? 'pin' : 'visibility';
|
||||
[
|
||||
...document.querySelectorAll<HTMLButtonElement>(
|
||||
`[data-card-action="${action}"]`
|
||||
),
|
||||
]
|
||||
.find((button) => button.dataset.appId === id)
|
||||
?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
@@ -243,8 +262,8 @@ export default function App() {
|
||||
<>
|
||||
<div
|
||||
className="site-shell"
|
||||
inert={helpOpen}
|
||||
aria-hidden={helpOpen || undefined}
|
||||
inert={overlayOpen}
|
||||
aria-hidden={overlayOpen || undefined}
|
||||
>
|
||||
<a className="skip-link" href="#main-content">
|
||||
Skip to tools
|
||||
@@ -257,7 +276,7 @@ export default function App() {
|
||||
homeHref={loaded?.homeUrl ?? './'}
|
||||
homeLabel="add·ideas Toolbox"
|
||||
brandIconUrl="./favicon.svg"
|
||||
helpAction={{ onClick: () => setHelpOpen(true) }}
|
||||
helpAction={{ onClick: () => setOverlay({ kind: 'help' }) }}
|
||||
sourceHref={PORTAL_SOURCE}
|
||||
sourceLabel="Toolbox source on Gitea"
|
||||
personalizeContent={
|
||||
@@ -331,6 +350,7 @@ export default function App() {
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={hiddenToggle}
|
||||
type="checkbox"
|
||||
checked={showHidden}
|
||||
disabled={preferences.hidden.length === 0}
|
||||
@@ -415,30 +435,26 @@ export default function App() {
|
||||
title="Pinned"
|
||||
apps={pinnedApps}
|
||||
pinned
|
||||
activeTag={activeTag}
|
||||
catalogueUrl={loaded.catalogueUrl.href}
|
||||
hiddenIds={preferences.hidden}
|
||||
onCategory={setCategory}
|
||||
onDetails={(id) =>
|
||||
setOverlay({ kind: 'details', appId: id })
|
||||
}
|
||||
onPin={(id) => toggleListItem('pinned', id)}
|
||||
onHide={(id) => toggleListItem('hidden', id)}
|
||||
onTag={(tag) =>
|
||||
setActiveTag((current) => (current === tag ? '' : tag))
|
||||
}
|
||||
/>
|
||||
<AppSection
|
||||
id="regular-apps"
|
||||
title="Tools"
|
||||
apps={regularApps}
|
||||
pinned={false}
|
||||
activeTag={activeTag}
|
||||
catalogueUrl={loaded.catalogueUrl.href}
|
||||
hiddenIds={preferences.hidden}
|
||||
onCategory={setCategory}
|
||||
onDetails={(id) =>
|
||||
setOverlay({ kind: 'details', appId: id })
|
||||
}
|
||||
onPin={(id) => toggleListItem('pinned', id)}
|
||||
onHide={(id) => toggleListItem('hidden', id)}
|
||||
onTag={(tag) =>
|
||||
setActiveTag((current) => (current === tag ? '' : tag))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</DndContext>
|
||||
@@ -473,9 +489,9 @@ export default function App() {
|
||||
</span>
|
||||
</p>
|
||||
<span>
|
||||
Portal v0.2.2 ·{' '}
|
||||
Portal v0.2.3 ·{' '}
|
||||
<a
|
||||
href="https://git.add-ideas.de/zemion/toolbox-portal/src/tag/v0.4.2"
|
||||
href="https://git.add-ideas.de/zemion/toolbox-portal/src/tag/v0.4.3"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -485,7 +501,26 @@ export default function App() {
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<HelpPanel open={helpOpen} onClose={closeHelp} />
|
||||
<HelpPanel open={overlay?.kind === 'help'} onClose={closeOverlay} />
|
||||
{detailsApp && loaded ? (
|
||||
<AppDetailsPanel
|
||||
app={detailsApp}
|
||||
catalogueUrl={loaded.catalogueUrl.href}
|
||||
contextualUrl={contextualLaunchUrl}
|
||||
pinned={preferences.pinned.includes(detailsApp.id)}
|
||||
hidden={preferences.hidden.includes(detailsApp.id)}
|
||||
activeTag={activeTag}
|
||||
onCategory={(nextCategory) => {
|
||||
setCategory(nextCategory);
|
||||
closeOverlay();
|
||||
}}
|
||||
onTag={(tag) => {
|
||||
setActiveTag((current) => (current === tag ? '' : tag));
|
||||
closeOverlay();
|
||||
}}
|
||||
onClose={closeOverlay}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user