feat(frontend): mobile-friendly headers across detail and list pages

Roll the truncate-chain + mobile-navigation pattern introduced for the
/flows/:id page out to every other detail and list page so a long title
never pushes the action buttons off-screen on a ~390px viewport.

Detail pages
- `templates/template.tsx` and `features/knowledges/knowledge-header.tsx`
  now mirror `pages/flows/flow.tsx`: left container becomes
  `flex min-w-0 flex-1` with `shrink-0` on the SidebarTrigger/Separator,
  Breadcrumb/BreadcrumbList/BreadcrumbItem get the `min-w-0` /
  `flex-nowrap` chain, BreadcrumbPage gets `truncate`, the right action
  area becomes `flex shrink-0`. InlineEditInput in both pages now uses
  `w-64 min-w-0 max-w-full flex-1` so the rename input shrinks with the
  parent instead of forcing it to ≥256px.
- On `isMobile`, the DetailNavigationToolbar is hidden and re-surfaced
  inside the existing actions dropdown as a single row matching
  `pages/flows/flow.tsx` (icon + label + Prev/Position/Next button
  group with shared borders; middle button doubles as the sheet
  trigger). DetailNavigationSheet is mounted separately, controlled by
  `isMobileNavSheetOpen` state.

List pages
- `pages/flows/flows.tsx`, `pages/knowledges/knowledges.tsx`,
  `pages/templates/templates.tsx`, `pages/resources/resources.tsx`,
  and `pages/flows/new-flow.tsx` get the same header truncate chain
  preventively — most page titles are short, but the structure stays
  consistent across pages and protects against future longer labels.
- `pages/templates/templates.tsx` Title cell gains `max-w-[380px]
  truncate` (matched to the existing Text-cell cap) so a long template
  title can't blow up the row.
- Settings pages (`settings-providers.tsx`, `settings-api-tokens.tsx`)
  don't render a breadcrumb header — their `SettingsLayout` already
  shows the sidebar trigger — but their inline header rows still leaked
  on narrow screens. Add `min-w-0 flex-1 truncate` to the description
  text and `shrink-0` to the Create button so the right-edge CTA stays
  visible while the left text degrades gracefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-05-15 19:21:25 +07:00
co-authored by Claude Opus 4.7
parent 7a2f882cea
commit c196e55d5d
9 changed files with 430 additions and 183 deletions
@@ -1,14 +1,14 @@
import type { ReactNode } from 'react';
import { Ellipsis, LibraryBig, Loader2, Pencil, Trash } from 'lucide-react';
import { useCallback, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronLeft, ChevronRight, Ellipsis, LibraryBig, Loader2, Pencil, Trash } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import type { KnowledgeDocumentFragmentFragment } from '@/graphql/types';
import ConfirmationDialog from '@/components/shared/confirmation-dialog';
import { DetailNavigationToolbar } from '@/components/shared/detail-navigation';
import { DetailNavigationSheet, DetailNavigationToolbar, useNavigation } from '@/components/shared/detail-navigation';
import { InlineEditInput, useInlineEdit } from '@/components/shared/inline-edit';
import { Badge } from '@/components/ui/badge';
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb';
@@ -23,6 +23,8 @@ import {
import { Separator } from '@/components/ui/separator';
import { SidebarTrigger } from '@/components/ui/sidebar';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { mergeHrefWithSearchParams } from '@/lib/url-params';
import { type Knowledge, useKnowledges } from '@/providers/knowledges-provider';
import { useKnowledgeDetailNavigation } from './use-knowledge-detail-navigation';
@@ -42,6 +44,8 @@ interface KnowledgeHeaderProps {
export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveButton }: KnowledgeHeaderProps) => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { isMobile } = useBreakpoint();
const { deleteKnowledge, updateKnowledge } = useKnowledges();
const [isRenaming, setIsRenaming] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -51,6 +55,56 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu
const { toolbarProps: knowledgeToolbarProps } = useKnowledgeDetailNavigation(knowledgeId);
// Mirror what `<DetailNavigationToolbar>` computes internally so the
// mobile menu items share the same filtered subset as the desktop toolbar.
const mobileNav = useNavigation<Knowledge>({
currentId: knowledgeToolbarProps.currentId,
getId: knowledgeToolbarProps.getId,
getSearchableText: knowledgeToolbarProps.getSearchableText ?? knowledgeToolbarProps.getLabel,
items: knowledgeToolbarProps.items,
query: knowledgeToolbarProps.filter,
});
const [isMobileNavSheetOpen, setIsMobileNavSheetOpen] = useState(false);
const mobileNavGoTo = useCallback(
(id: null | string) => {
if (!id) {
return;
}
const target = mobileNav.filteredItems.find(
(item) => String(knowledgeToolbarProps.getId(item)) === id,
);
if (!target) {
return;
}
navigate(mergeHrefWithSearchParams(knowledgeToolbarProps.getHref(target), searchParams), {
replace: true,
});
},
[knowledgeToolbarProps, mobileNav.filteredItems, navigate, searchParams],
);
const mobileNavSelectItem = useCallback(
(item: Knowledge) => {
setIsMobileNavSheetOpen(false);
navigate(mergeHrefWithSearchParams(knowledgeToolbarProps.getHref(item), searchParams), {
replace: true,
});
},
[knowledgeToolbarProps, navigate, searchParams],
);
const mobilePositionLabel = useMemo(
() =>
mobileNav.total === 0 || mobileNav.currentIndex === -1
? `/${mobileNav.total}`
: `${mobileNav.currentIndex + 1}/${mobileNav.total}`,
[mobileNav.currentIndex, mobileNav.total],
);
// Title source-of-truth is the server-side `question`. We intentionally do
// not read it from the form draft below — the inline rename flow in this
// header writes through `updateKnowledge`, which refreshes `knowledge` via
@@ -121,47 +175,49 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu
return (
<>
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator
className="mr-2 h-4"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="gap-2">
<LibraryBig className="size-4 shrink-0" />
{isEditingTitle && canShowActions ? (
<InlineEditInput
busy={isRenaming}
className="w-64 max-w-full"
defaultValue={knowledgeName ?? ''}
inputRef={editingInputRef}
onCancel={handleRenameCancel}
onSave={handleRenameSave}
placeholder="Knowledge question"
/>
) : canShowActions ? (
<Tooltip>
<TooltipTrigger asChild>
<BreadcrumbPage
className="max-w-[240px] cursor-text truncate select-none"
onDoubleClick={handleRenameStart}
>
{knowledgeName ?? 'Knowledge'}
</BreadcrumbPage>
</TooltipTrigger>
<TooltipContent>Double-click to rename</TooltipContent>
</Tooltip>
) : (
<BreadcrumbPage className="max-w-[240px] truncate">
{isNew ? 'New knowledge' : (knowledgeName ?? 'Knowledge')}
</BreadcrumbPage>
)}
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="ml-auto flex items-center gap-2">
{canShowActions && (
<div className="flex min-w-0 flex-1 items-center gap-2">
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="mr-2 h-4 shrink-0"
orientation="vertical"
/>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0 gap-2">
<LibraryBig className="size-4 shrink-0" />
{isEditingTitle && canShowActions ? (
<InlineEditInput
busy={isRenaming}
className="w-64 min-w-0 max-w-full flex-1"
defaultValue={knowledgeName ?? ''}
inputRef={editingInputRef}
onCancel={handleRenameCancel}
onSave={handleRenameSave}
placeholder="Knowledge question"
/>
) : canShowActions ? (
<Tooltip>
<TooltipTrigger asChild>
<BreadcrumbPage
className="min-w-0 cursor-text select-none truncate"
onDoubleClick={handleRenameStart}
>
{knowledgeName ?? 'Knowledge'}
</BreadcrumbPage>
</TooltipTrigger>
<TooltipContent>Double-click to rename</TooltipContent>
</Tooltip>
) : (
<BreadcrumbPage className="min-w-0 truncate">
{isNew ? 'New knowledge' : (knowledgeName ?? 'Knowledge')}
</BreadcrumbPage>
)}
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex shrink-0 items-center gap-2">
{canShowActions && !isMobile && (
<DetailNavigationToolbar<Knowledge>
{...knowledgeToolbarProps}
renderItem={(item, isCurrent) => (
@@ -199,6 +255,49 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu
className="min-w-24"
onCloseAutoFocus={handleDropdownCloseAutoFocus}
>
{isMobile && mobileNav.total > 0 && (
<>
<DropdownMenuItem
className="cursor-default hover:bg-transparent focus:bg-transparent"
onSelect={(event) => event.preventDefault()}
>
<LibraryBig className="size-4" />
Knowledges
<div className="-my-1.5 -mr-2 ml-auto flex items-center">
<Button
aria-label="Previous"
className="size-7 rounded-r-none border-r-0 p-0"
disabled={!mobileNav.prevId}
onClick={() => mobileNavGoTo(mobileNav.prevId)}
size="icon"
variant="outline"
>
<ChevronLeft />
</Button>
<Button
aria-label="Open knowledges list"
className="h-7 min-w-12 rounded-none border-x px-2 font-mono text-xs tabular-nums"
disabled={mobileNav.currentIndex === -1}
onClick={() => setIsMobileNavSheetOpen(true)}
variant="outline"
>
{mobilePositionLabel}
</Button>
<Button
aria-label="Next"
className="size-7 rounded-l-none border-l-0 p-0"
disabled={!mobileNav.nextId}
onClick={() => mobileNavGoTo(mobileNav.nextId)}
size="icon"
variant="outline"
>
<ChevronRight />
</Button>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={handleRenameStart}>
<Pencil className="size-3" />
Rename
@@ -225,6 +324,32 @@ export const KnowledgeHeader = ({ isNew, knowledge, onBeforeNavigateAway, saveBu
)}
</div>
</header>
{isMobile && canShowActions && (
<DetailNavigationSheet<Knowledge>
currentId={knowledgeToolbarProps.currentId}
currentIndex={mobileNav.currentIndex}
getId={knowledgeToolbarProps.getId}
getLabel={knowledgeToolbarProps.getLabel}
items={mobileNav.filteredItems}
onItemSelect={mobileNavSelectItem}
onOpenChange={setIsMobileNavSheetOpen}
open={isMobileNavSheetOpen}
renderItem={(item, isCurrent) => (
<>
<Badge
className="shrink-0 text-[10px] whitespace-nowrap"
variant="outline"
>
{item.docType}
</Badge>
<span className={isCurrent ? 'truncate font-medium' : 'truncate'}>{item.question}</span>
</>
)}
sheetIcon={<LibraryBig className="size-4" />}
sheetTitle="Knowledges"
total={mobileNav.total}
/>
)}
<ConfirmationDialog
cancelText="Cancel"
confirmText="Delete"
+9 -9
View File
@@ -585,22 +585,22 @@ const Flows = () => {
const pageHeader = (
<header className="bg-background sticky top-0 z-10 flex h-12 w-full shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<div className="flex min-w-0 flex-1 items-center gap-2 px-4">
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="h-4"
className="h-4 shrink-0"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<GitFork className="size-4" />
<BreadcrumbPage>Flows</BreadcrumbPage>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0">
<GitFork className="size-4 shrink-0" />
<BreadcrumbPage className="min-w-0 truncate">Flows</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto flex items-center gap-2 px-4">
<div className="flex shrink-0 items-center gap-2 px-4">
<HeaderButton
icon={<Plus />}
label="New Flow"
+6 -6
View File
@@ -48,15 +48,15 @@ const NewFlow = () => {
return (
<>
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="mr-2 h-4"
className="mr-2 h-4 shrink-0"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage>New flow</BreadcrumbPage>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="min-w-0 truncate">New flow</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
+9 -9
View File
@@ -365,22 +365,22 @@ const Knowledges = () => {
const pageHeader = (
<header className="bg-background sticky top-0 z-10 flex h-12 w-full shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<div className="flex min-w-0 flex-1 items-center gap-2 px-4">
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="h-4"
className="h-4 shrink-0"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<LibraryBig className="size-4" />
<BreadcrumbPage>Knowledges</BreadcrumbPage>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0">
<LibraryBig className="size-4 shrink-0" />
<BreadcrumbPage className="min-w-0 truncate">Knowledges</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto flex items-center gap-2 px-4">
<div className="flex shrink-0 items-center gap-2 px-4">
<HeaderButton
icon={<Plus />}
label="New Knowledge"
+9 -9
View File
@@ -383,22 +383,22 @@ const Resources = () => {
const pageHeader = (
<header className="bg-background sticky top-0 z-10 flex h-12 w-full shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<div className="flex min-w-0 flex-1 items-center gap-2 px-4">
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="h-4"
className="h-4 shrink-0"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<Folder className="size-4" />
<BreadcrumbPage>Resources</BreadcrumbPage>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0">
<Folder className="size-4 shrink-0" />
<BreadcrumbPage className="min-w-0 truncate">Resources</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto flex items-center gap-2 px-4">
<div className="flex shrink-0 items-center gap-2 px-4">
<HeaderButton
disabled={upload.isUploading}
icon={<FolderPlus />}
@@ -142,8 +142,8 @@ const copyToClipboard = async (text: string): Promise<boolean> => {
const SettingsAPITokensHeader = ({ onCreateClick }: { onCreateClick: () => void }) => {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-2">
<p className="text-muted-foreground">Manage API tokens for programmatic access</p>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<p className="text-muted-foreground truncate">Manage API tokens for programmatic access</p>
<div className="flex gap-4 text-sm">
<a
className="text-primary inline-flex items-center gap-1 underline hover:no-underline"
@@ -167,6 +167,7 @@ const SettingsAPITokensHeader = ({ onCreateClick }: { onCreateClick: () => void
</div>
<Button
className="shrink-0"
onClick={onCreateClick}
variant="secondary"
>
@@ -89,11 +89,14 @@ const SettingsProvidersHeader = () => {
return (
<div className="flex items-center justify-between gap-4">
<p className="text-muted-foreground">Manage language model providers</p>
<p className="text-muted-foreground min-w-0 flex-1 truncate">Manage language model providers</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary">
<Button
className="shrink-0"
variant="secondary"
>
Create Provider
<ChevronDown className="size-4" />
</Button>
+209 -91
View File
@@ -1,6 +1,8 @@
import { zodResolver } from '@hookform/resolvers/zod';
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Ellipsis,
FileSymlink,
FileText,
@@ -13,12 +15,12 @@ import {
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import { z } from 'zod';
import ConfirmationDialog from '@/components/shared/confirmation-dialog';
import { DetailNavigationToolbar } from '@/components/shared/detail-navigation';
import { DetailNavigationSheet, DetailNavigationToolbar, useNavigation } from '@/components/shared/detail-navigation';
import { InlineEditInput, useInlineEdit } from '@/components/shared/inline-edit';
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage } from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
@@ -42,6 +44,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { useTemplateDetailNavigation } from '@/features/templates/use-template-detail-navigation';
import { useFlowTemplateQuery } from '@/graphql/types';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { mergeHrefWithSearchParams } from '@/lib/url-params';
import { cn } from '@/lib/utils';
import { type Template, useTemplates } from '@/providers/templates-provider';
@@ -231,6 +234,7 @@ Action plan:
const Template = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { templateId } = useParams<{ templateId?: string }>();
const { createTemplate, deleteTemplate, updateTemplate } = useTemplates();
@@ -242,6 +246,56 @@ const Template = () => {
// below by `canShowActions`).
const { toolbarProps: templateToolbarProps } = useTemplateDetailNavigation(isNew ? null : templateId);
// Mirror what `<DetailNavigationToolbar>` computes internally so the
// mobile menu items share the same filtered subset as the desktop toolbar.
const mobileNav = useNavigation<Template>({
currentId: templateToolbarProps.currentId,
getId: templateToolbarProps.getId,
getSearchableText: templateToolbarProps.getSearchableText ?? templateToolbarProps.getLabel,
items: templateToolbarProps.items,
query: templateToolbarProps.filter,
});
const [isMobileNavSheetOpen, setIsMobileNavSheetOpen] = useState(false);
const mobileNavGoTo = useCallback(
(id: null | string) => {
if (!id) {
return;
}
const target = mobileNav.filteredItems.find(
(item) => String(templateToolbarProps.getId(item)) === id,
);
if (!target) {
return;
}
navigate(mergeHrefWithSearchParams(templateToolbarProps.getHref(target), searchParams), {
replace: true,
});
},
[mobileNav.filteredItems, navigate, searchParams, templateToolbarProps],
);
const mobileNavSelectItem = useCallback(
(item: Template) => {
setIsMobileNavSheetOpen(false);
navigate(mergeHrefWithSearchParams(templateToolbarProps.getHref(item), searchParams), {
replace: true,
});
},
[navigate, searchParams, templateToolbarProps],
);
const mobilePositionLabel = useMemo(
() =>
mobileNav.total === 0 || mobileNav.currentIndex === -1
? `/${mobileNav.total}`
: `${mobileNav.currentIndex + 1}/${mobileNav.total}`,
[mobileNav.currentIndex, mobileNav.total],
);
const [isAsideOpen, setIsAsideOpen] = useState(false);
const [expandedPresetIndex, setExpandedPresetIndex] = useState<null | number>(null);
const [isReplaceConfirmOpen, setIsReplaceConfirmOpen] = useState(false);
@@ -393,100 +447,164 @@ const Template = () => {
const canShowActions = !isNew && !!templateData?.flowTemplate;
const pageHeader = (
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator
className="mr-2 h-4"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="gap-2">
{isEditingTitle && canShowActions ? (
<InlineEditInput
busy={isRenaming}
className="w-64 max-w-full"
defaultValue={templateName ?? ''}
inputRef={editingInputRef}
onCancel={handleTemplateRenameCancel}
onSave={handleTemplateRenameSave}
placeholder="Template title"
/>
) : canShowActions ? (
<Tooltip>
<TooltipTrigger asChild>
<BreadcrumbPage
className="cursor-text select-none"
onDoubleClick={handleTemplateRenameStart}
>
{templateName ?? 'Template'}
</BreadcrumbPage>
</TooltipTrigger>
<TooltipContent>Double-click to rename</TooltipContent>
</Tooltip>
) : (
<BreadcrumbPage>{isNew ? 'New template' : (templateName ?? 'Template')}</BreadcrumbPage>
)}
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="ml-auto flex items-center gap-2">
{canShowActions && (
<DetailNavigationToolbar<Template>
{...templateToolbarProps}
sheetIcon={<FileText className="size-4" />}
sheetTitle="Templates"
<>
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4">
<div className="flex min-w-0 flex-1 items-center gap-2">
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="mr-2 h-4 shrink-0"
orientation="vertical"
/>
)}
<Button
onClick={() => setIsAsideOpen((open) => !open)}
size="icon"
variant="ghost"
>
{isAsideOpen ? <PanelRightClose /> : <PanelRightOpen />}
</Button>
{canShowActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Template actions"
className="size-8 p-0"
variant="ghost"
>
<Ellipsis />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="min-w-24"
onCloseAutoFocus={handleDropdownCloseAutoFocus}
>
<DropdownMenuItem onClick={handleTemplateRenameStart}>
<Pencil className="size-3" />
Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={isDeleting}
onClick={() => setIsDeleteDialogOpen(true)}
>
{isDeleting ? (
<>
<Loader2 className="size-4 animate-spin" />
Deleting...
</>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0 gap-2">
{isEditingTitle && canShowActions ? (
<InlineEditInput
busy={isRenaming}
className="w-64 min-w-0 max-w-full flex-1"
defaultValue={templateName ?? ''}
inputRef={editingInputRef}
onCancel={handleTemplateRenameCancel}
onSave={handleTemplateRenameSave}
placeholder="Template title"
/>
) : canShowActions ? (
<Tooltip>
<TooltipTrigger asChild>
<BreadcrumbPage
className="min-w-0 cursor-text select-none truncate"
onDoubleClick={handleTemplateRenameStart}
>
{templateName ?? 'Template'}
</BreadcrumbPage>
</TooltipTrigger>
<TooltipContent>Double-click to rename</TooltipContent>
</Tooltip>
) : (
<BreadcrumbPage className="min-w-0 truncate">
{isNew ? 'New template' : (templateName ?? 'Template')}
</BreadcrumbPage>
)}
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="flex shrink-0 items-center gap-2">
{canShowActions && !isMobile && (
<DetailNavigationToolbar<Template>
{...templateToolbarProps}
sheetIcon={<FileText className="size-4" />}
sheetTitle="Templates"
/>
)}
<Button
onClick={() => setIsAsideOpen((open) => !open)}
size="icon"
variant="ghost"
>
{isAsideOpen ? <PanelRightClose /> : <PanelRightOpen />}
</Button>
{canShowActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="Template actions"
className="size-8 p-0"
variant="ghost"
>
<Ellipsis />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="min-w-24"
onCloseAutoFocus={handleDropdownCloseAutoFocus}
>
{isMobile && mobileNav.total > 0 && (
<>
<Trash className="size-4" />
Delete
<DropdownMenuItem
className="cursor-default hover:bg-transparent focus:bg-transparent"
onSelect={(event) => event.preventDefault()}
>
<FileText className="size-4" />
Templates
<div className="-my-1.5 -mr-2 ml-auto flex items-center">
<Button
aria-label="Previous"
className="size-7 rounded-r-none border-r-0 p-0"
disabled={!mobileNav.prevId}
onClick={() => mobileNavGoTo(mobileNav.prevId)}
size="icon"
variant="outline"
>
<ChevronLeft />
</Button>
<Button
aria-label="Open templates list"
className="h-7 min-w-12 rounded-none border-x px-2 font-mono text-xs tabular-nums"
disabled={mobileNav.currentIndex === -1}
onClick={() => setIsMobileNavSheetOpen(true)}
variant="outline"
>
{mobilePositionLabel}
</Button>
<Button
aria-label="Next"
className="size-7 rounded-l-none border-l-0 p-0"
disabled={!mobileNav.nextId}
onClick={() => mobileNavGoTo(mobileNav.nextId)}
size="icon"
variant="outline"
>
<ChevronRight />
</Button>
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</header>
<DropdownMenuItem onClick={handleTemplateRenameStart}>
<Pencil className="size-3" />
Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
disabled={isDeleting}
onClick={() => setIsDeleteDialogOpen(true)}
>
{isDeleting ? (
<>
<Loader2 className="size-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash className="size-4" />
Delete
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</header>
{isMobile && canShowActions && (
<DetailNavigationSheet<Template>
currentId={templateToolbarProps.currentId}
currentIndex={mobileNav.currentIndex}
getId={templateToolbarProps.getId}
getLabel={templateToolbarProps.getLabel}
items={mobileNav.filteredItems}
onItemSelect={mobileNavSelectItem}
onOpenChange={setIsMobileNavSheetOpen}
open={isMobileNavSheetOpen}
sheetIcon={<FileText className="size-4" />}
sheetTitle="Templates"
total={mobileNav.total}
/>
)}
</>
);
const asideContent = useMemo(
+10 -10
View File
@@ -137,7 +137,7 @@ const Templates = () => {
);
}
return <div className="font-medium">{title}</div>;
return <div className="max-w-[380px] truncate font-medium">{title}</div>;
},
header: ({ column }) => (
<DataTableColumnHeader
@@ -242,22 +242,22 @@ const Templates = () => {
const pageHeader = (
<header className="bg-background sticky top-0 z-10 flex h-12 w-full shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<div className="flex min-w-0 flex-1 items-center gap-2 px-4">
<SidebarTrigger className="-ml-1 shrink-0" />
<Separator
className="h-4"
className="h-4 shrink-0"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<FileText className="size-4" />
<BreadcrumbPage>Templates</BreadcrumbPage>
<Breadcrumb className="min-w-0 flex-1">
<BreadcrumbList className="min-w-0 flex-nowrap">
<BreadcrumbItem className="min-w-0">
<FileText className="size-4 shrink-0" />
<BreadcrumbPage className="min-w-0 truncate">Templates</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="ml-auto flex items-center gap-2 px-4">
<div className="flex shrink-0 items-center gap-2 px-4">
<HeaderButton
icon={<Plus />}
label="New Template"