Sidebar: pin the four actions as an icon row at the bottom
Graph, new page, import, and trash collapse from scattered text links into one icon row pinned to the sidebar's bottom edge, in that order, each with a hover hint (the trash reads "Papierkorb anzeigen"). The new-page button now toggles the inline form, which still renders above the footer with the same classes; the import trigger becomes an icon whose progress list floats above the row so the icons stay put. All e2e class hooks (.sidebar__new-page, .sidebar__graph-link, .sidebar__import-*) are unchanged. Fixes #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
This commit is contained in:
parent
1781f12f6e
commit
44a90a53ac
@ -1,4 +1,5 @@
|
|||||||
import { IMPORT_EXTENSIONS } from '@dorfteich/shared';
|
import { IMPORT_EXTENSIONS } from '@dorfteich/shared';
|
||||||
|
import { Import } from 'lucide-react';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -28,10 +29,12 @@ export function ImportControl({ pondId, pondSlug }: ImportControlProps): React.J
|
|||||||
<div className="sidebar__import">
|
<div className="sidebar__import">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="linklike sidebar__import-action"
|
className="icon-button sidebar__import-action"
|
||||||
|
title={t('action')}
|
||||||
|
aria-label={t('action')}
|
||||||
onClick={() => inputRef.current?.click()}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{t('action')}
|
<Import aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|||||||
@ -8,7 +8,15 @@ import type {
|
|||||||
} from '@dorfteich/shared';
|
} from '@dorfteich/shared';
|
||||||
import { buildTree, collectSubtreeIds, labelDepth } from '@dorfteich/shared';
|
import { buildTree, collectSubtreeIds, labelDepth } from '@dorfteich/shared';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ChevronRight, FileText, Folder, FolderOpen } from 'lucide-react';
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
FilePlus,
|
||||||
|
FileText,
|
||||||
|
Folder,
|
||||||
|
FolderOpen,
|
||||||
|
Trash2,
|
||||||
|
Waypoints,
|
||||||
|
} from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -329,7 +337,7 @@ function SidebarContent({
|
|||||||
|
|
||||||
{moveError !== null && <FormError error={moveError} />}
|
{moveError !== null && <FormError error={moveError} />}
|
||||||
|
|
||||||
{creating ? (
|
{creating && (
|
||||||
<NewPageForm
|
<NewPageForm
|
||||||
pondId={pond.id}
|
pondId={pond.id}
|
||||||
pondSlug={pondSlug}
|
pondSlug={pondSlug}
|
||||||
@ -341,33 +349,44 @@ function SidebarContent({
|
|||||||
}}
|
}}
|
||||||
onCancel={() => setCreating(false)}
|
onCancel={() => setCreating(false)}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="linklike sidebar__new-page"
|
|
||||||
onClick={() => setCreating(true)}
|
|
||||||
>
|
|
||||||
{t('layout.sidebar.newPage')}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ImportControl pondId={pond.id} pondSlug={pondSlug} />
|
|
||||||
|
|
||||||
{/* Keyboard/drag reordering announcements for screen readers. */}
|
{/* Keyboard/drag reordering announcements for screen readers. */}
|
||||||
<p className="visually-hidden sidebar__announce" role="status" aria-live="polite">
|
<p className="visually-hidden sidebar__announce" role="status" aria-live="polite">
|
||||||
{announcement}
|
{announcement}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Graph + trash stay text links, pinned to the sidebar's bottom
|
{/* All four actions live as an icon row pinned to the sidebar's
|
||||||
(M10 follow-up; pond settings moved to the TopBar gear icon).
|
bottom (#124): graph, new page, import, trash — hover hints via
|
||||||
The graph is for every member (#112); the trash is owner-only. */}
|
title. The graph is for every member (#112); trash is owner-only. */}
|
||||||
<div className="sidebar__footer">
|
<div className="sidebar__footer">
|
||||||
<Link to={`/p/${pondSlug}/graph`} className="linklike sidebar__graph-link">
|
<Link
|
||||||
{t('graph:link')}
|
to={`/p/${pondSlug}/graph`}
|
||||||
|
className="icon-button sidebar__graph-link"
|
||||||
|
title={t('graph:link')}
|
||||||
|
aria-label={t('graph:link')}
|
||||||
|
>
|
||||||
|
<Waypoints aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-button sidebar__new-page"
|
||||||
|
title={t('layout.sidebar.newPageHint')}
|
||||||
|
aria-label={t('layout.sidebar.newPageHint')}
|
||||||
|
aria-expanded={creating}
|
||||||
|
onClick={() => setCreating(!creating)}
|
||||||
|
>
|
||||||
|
<FilePlus aria-hidden />
|
||||||
|
</button>
|
||||||
|
<ImportControl pondId={pond.id} pondSlug={pondSlug} />
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<Link to={`/p/${pondSlug}/trash`} className="linklike sidebar__trash-link">
|
<Link
|
||||||
{t('editor:trash.link')}
|
to={`/p/${pondSlug}/trash`}
|
||||||
|
className="icon-button sidebar__trash-link"
|
||||||
|
title={t('editor:trash.showLink')}
|
||||||
|
aria-label={t('editor:trash.showLink')}
|
||||||
|
>
|
||||||
|
<Trash2 aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -148,10 +148,6 @@ button {
|
|||||||
padding: var(--space-1) var(--space-2);
|
padding: var(--space-1) var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__trash-link {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__pages {
|
.sidebar__pages {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -369,13 +365,6 @@ button {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__new-page {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
/* The sidebar is a flex column now; keep the stretched button reading
|
|
||||||
like the left-aligned link it used to be. */
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__new-page-form {
|
.sidebar__new-page-form {
|
||||||
margin-top: var(--space-2);
|
margin-top: var(--space-2);
|
||||||
}
|
}
|
||||||
@ -390,21 +379,29 @@ button {
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The import control sits in the pinned footer icon row (#124); its
|
||||||
|
progress list floats ABOVE the row so the icons stay put. */
|
||||||
.sidebar__import {
|
.sidebar__import {
|
||||||
margin-top: var(--space-2);
|
position: relative;
|
||||||
}
|
display: inline-flex;
|
||||||
|
|
||||||
.sidebar__import-action {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__import-list {
|
.sidebar__import-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: var(--space-2) 0 0;
|
position: absolute;
|
||||||
padding: 0;
|
bottom: calc(100% + var(--space-1));
|
||||||
|
left: 0;
|
||||||
|
width: 14rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-2);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||||
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__import-item {
|
.sidebar__import-item {
|
||||||
@ -1760,12 +1757,8 @@ button {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: var(--space-4);
|
padding-top: var(--space-4);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-3);
|
align-items: center;
|
||||||
}
|
gap: var(--space-1);
|
||||||
|
|
||||||
.sidebar__graph-link {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Knowledge graph (issue #112) + local graph panel (#113). */
|
/* Knowledge graph (issue #112) + local graph panel (#113). */
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
"reparented": "{{title}} unter {{parent}} eingeordnet."
|
"reparented": "{{title}} unter {{parent}} eingeordnet."
|
||||||
},
|
},
|
||||||
"newPage": "+ Neue Seite",
|
"newPage": "+ Neue Seite",
|
||||||
|
"newPageHint": "Neue Seite",
|
||||||
"newPageTitle": "Titel",
|
"newPageTitle": "Titel",
|
||||||
"newPageUnder": "Wird unter „{{title}}“ angelegt.",
|
"newPageUnder": "Wird unter „{{title}}“ angelegt.",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
|
|||||||
@ -142,6 +142,7 @@
|
|||||||
},
|
},
|
||||||
"trash": {
|
"trash": {
|
||||||
"link": "Papierkorb",
|
"link": "Papierkorb",
|
||||||
|
"showLink": "Papierkorb anzeigen",
|
||||||
"title": "Papierkorb — {{pond}}",
|
"title": "Papierkorb — {{pond}}",
|
||||||
"empty": "Der Papierkorb ist leer.",
|
"empty": "Der Papierkorb ist leer.",
|
||||||
"deletedAt": "Gelöscht am {{date}}",
|
"deletedAt": "Gelöscht am {{date}}",
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
"reparented": "Moved {{title}} under {{parent}}."
|
"reparented": "Moved {{title}} under {{parent}}."
|
||||||
},
|
},
|
||||||
"newPage": "+ New page",
|
"newPage": "+ New page",
|
||||||
|
"newPageHint": "New page",
|
||||||
"newPageTitle": "Title",
|
"newPageTitle": "Title",
|
||||||
"newPageUnder": "Will be created under “{{title}}”.",
|
"newPageUnder": "Will be created under “{{title}}”.",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
|
|||||||
@ -142,6 +142,7 @@
|
|||||||
},
|
},
|
||||||
"trash": {
|
"trash": {
|
||||||
"link": "Trash",
|
"link": "Trash",
|
||||||
|
"showLink": "Show trash",
|
||||||
"title": "Trash — {{pond}}",
|
"title": "Trash — {{pond}}",
|
||||||
"empty": "The trash is empty.",
|
"empty": "The trash is empty.",
|
||||||
"deletedAt": "Deleted {{date}}",
|
"deletedAt": "Deleted {{date}}",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user