feat: add sidebar navigation arrows
This commit is contained in:
@@ -203,7 +203,7 @@ flowchart TD
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types: pointer users can drag the existing view row, while keyboard users can use command-palette move actions. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root.
|
||||
- **Sidebar** (150-400px, resizable): Top-level filters (All Notes, Changes, Pulse), saved Views, collapsible type-based section groups, and a dedicated folder tree. The title bar exposes Back/Forward buttons wired to the same note navigation history as the command palette, menu accelerators, and mouse navigation buttons. The folder tree starts with a vault-root row labeled from the opened vault path, shows root-level files when selected, and nests user-created folders plus default vault folders such as `attachments/` and `views/` underneath it; only the dedicated `type/` directory stays hidden because note types already have their own sidebar section. Saved Views persist a top-level YAML `order` field in each view file and use the same ordered-list mental model as Types: pointer users can drag the existing view row, while keyboard users can use command-palette move actions. The folder tree supports inline folder creation and rename, exposes a right-click menu for rename/delete plus filesystem reveal/copy-path actions on mutable folders, and auto-expands ancestor folders when the current selection or rename target is nested. Type sections and folder rows also act as note drop targets: dropping a note on a type updates its `type:` frontmatter, while dropping it on a folder runs the same crash-safe move path as the command palette flow. Each type can have a custom icon, color, sort, and visibility set via its `type: Type` document; new type documents created by Tolaria are written at the vault root.
|
||||
- **Note List / Pulse View** (200-500px, resizable): When a section group, filter, or saved view is selected, shows filtered notes with snippets, modified dates, status indicators, and per-context note-list controls. When `selection.kind === 'entity'`, the same pane enters **Neighborhood** mode: the source note is pinned at the top as a normal active row, outgoing relationship groups render first, inverse/backlink groups follow, empty groups stay visible with `0`, and duplicates across groups are allowed when multiple relationships are true. Plain click / `Enter` open the focused note without replacing the current Neighborhood, while Cmd/Ctrl-click and Cmd/Ctrl-`Enter` pivot the pane into the clicked note's Neighborhood. Folder-backed lists also show non-Markdown files: previewable image and PDF binaries get file indicators and open in the editor pane, while unsupported binaries remain muted instead of auto-launching an external app. Saved views reuse the same sort and visible-column controls as the built-in lists, and those changes persist back into the view `.yml` definition (`sort`, `listPropertiesDisplay`). When Pulse filter is active, shows `PulseView` — a chronological git activity feed grouped by day.
|
||||
- **Editor** (flex, fills remaining space): Single note open at a time (no tabs — see ADR-0003). Breadcrumb bar with word count and rich-editor width toggle, BlockNote rich text editor with wikilink support, Markdown-compatible inline/display math rendering, first-class Mermaid diagram blocks, markdown-safe formatting controls, and schema-backed fenced code block highlighting via `@blocknote/code-block`. Can toggle to diff view (modified files), raw CodeMirror view, or a wide rich-editor reading surface with preserved side margins; raw CodeMirror remains full-width and unaffected by note width mode. Binary image and PDF files render through `FilePreview` as ordinary vault files using Tauri asset URLs; external-open actions call `open_vault_file_external` so the target is validated against the active vault before the native default app opens it. Unsupported/broken binaries show explicit fallback states and keyboard focus returns to the note list on `Escape`. Decomposed into `Editor` (orchestrator), `EditorContent`, `FilePreview`, `EditorRightPanel`, `SingleEditorView`, with hooks `useDiffMode`, `useEditorFocus`, and `useEditorSave`, plus the `useRawMode`/`RawEditorView` pair for markdown source editing. Rich BlockNote input and raw CodeMirror input both route typed `->`, `<-`, and `<->` through the shared `src/utils/arrowLigatures.ts` resolver so arrow ligatures stay consistent across mode switches while escaped ASCII sequences remain literal. Navigation history (Cmd+[/]) replaces tabs.
|
||||
- **Inspector / AI Agent** (200-500px or 40px collapsed): Toggles between Inspector (frontmatter, relationships, instances, backlinks, git history) and AI Agent panel (the selected CLI agent with tool execution). The Sparkle icon in the breadcrumb bar toggles between them. Per-note `icon` is a suggested Inspector property and the command palette's "Set Note Icon" action opens that field directly. When viewing a Type note, the Inspector shows an **Instances** section listing all notes of that type (sorted by modified_at desc, capped at 50).
|
||||
|
||||
@@ -247,6 +247,8 @@ files:
|
||||
sidebar.nav.inbox: 3882d32c66e7e768145ecd8f104b0c08
|
||||
sidebar.nav.allNotes: cd76cf851b08a9d2feafaf255bc1f4d5
|
||||
sidebar.nav.archive: e727b00944f81e1d0a95c12886ac4641
|
||||
sidebar.nav.back: 0557fa923dcee4d0f86b1409f5c2167f
|
||||
sidebar.nav.forward: 67d2f6740a8eaebf4d5c6f79be8da481
|
||||
sidebar.group.favorites: 953bf4d5e8a1807c15dec4e255384b1d
|
||||
sidebar.group.views: 8f56a72923a6ca8ff53462533d89e567
|
||||
sidebar.group.types: 6b20155008db90cf7589c07baa7334e9
|
||||
|
||||
@@ -1632,7 +1632,7 @@ function App() {
|
||||
{sidebarVisible && (
|
||||
<>
|
||||
<div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
|
||||
<Sidebar entries={vault.entries} folders={vault.folders} views={vault.views} selection={effectiveSelection} onSelect={handleSetSelection} onSelectNote={notes.handleSelectNote} onSelectFavorite={handleOpenFavorite} onReorderFavorites={entryActions.handleReorderFavorites} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} onToggleTypeVisibility={entryActions.handleToggleTypeVisibility} onCreateFolder={handleCreateFolder} onRenameFolder={folderActions.renameFolder} onDeleteFolder={folderActions.requestDeleteFolder} folderFileActions={fileActions.folderActions} renamingFolderPath={folderActions.renamingFolderPath} onStartRenameFolder={folderActions.startFolderRename} onCancelRenameFolder={folderActions.cancelFolderRename} onCreateView={dialogs.openCreateView} onEditView={handleEditView} onDeleteView={handleDeleteView} onReorderViews={viewOrdering.onReorderViews} showInbox={explicitOrganizationEnabled} inboxCount={inboxCount} allNotesFileVisibility={allNotesFileVisibility} locale={appLocale} loading={isVaultContentLoading} vaultRootPath={resolvedPath} />
|
||||
<Sidebar entries={vault.entries} folders={vault.folders} views={vault.views} selection={effectiveSelection} onSelect={handleSetSelection} onSelectNote={notes.handleSelectNote} onSelectFavorite={handleOpenFavorite} onReorderFavorites={entryActions.handleReorderFavorites} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} onToggleTypeVisibility={entryActions.handleToggleTypeVisibility} onCreateFolder={handleCreateFolder} onRenameFolder={folderActions.renameFolder} onDeleteFolder={folderActions.requestDeleteFolder} folderFileActions={fileActions.folderActions} renamingFolderPath={folderActions.renamingFolderPath} onStartRenameFolder={folderActions.startFolderRename} onCancelRenameFolder={folderActions.cancelFolderRename} onCreateView={dialogs.openCreateView} onEditView={handleEditView} onDeleteView={handleDeleteView} onReorderViews={viewOrdering.onReorderViews} showInbox={explicitOrganizationEnabled} inboxCount={inboxCount} allNotesFileVisibility={allNotesFileVisibility} locale={appLocale} loading={isVaultContentLoading} vaultRootPath={resolvedPath} canGoBack={canGoBack} canGoForward={canGoForward} onGoBack={handleGoBack} onGoForward={handleGoForward} />
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleSidebarResize} />
|
||||
</>
|
||||
|
||||
@@ -65,6 +65,10 @@ interface SidebarProps {
|
||||
locale?: AppLocale
|
||||
onCollapse?: () => void
|
||||
loading?: boolean
|
||||
canGoBack?: boolean
|
||||
canGoForward?: boolean
|
||||
onGoBack?: () => void
|
||||
onGoForward?: () => void
|
||||
}
|
||||
|
||||
interface SidebarNavigationProps extends Pick<
|
||||
@@ -633,7 +637,14 @@ export const Sidebar = memo(function Sidebar(props: SidebarProps) {
|
||||
|
||||
return (
|
||||
<aside className="flex h-full flex-col overflow-hidden border-r border-[var(--sidebar-border)] bg-sidebar text-sidebar-foreground">
|
||||
<SidebarTitleBar locale={locale} onCollapse={props.onCollapse} />
|
||||
<SidebarTitleBar
|
||||
locale={locale}
|
||||
onCollapse={props.onCollapse}
|
||||
canGoBack={props.canGoBack}
|
||||
canGoForward={props.canGoForward}
|
||||
onGoBack={props.onGoBack}
|
||||
onGoForward={props.onGoForward}
|
||||
/>
|
||||
<SidebarRuntimeNavigation props={props} runtime={runtime} />
|
||||
<SidebarInteractionOverlays locale={locale} runtime={runtime} />
|
||||
</aside>
|
||||
|
||||
87
src/components/sidebar/SidebarNavArrows.test.tsx
Normal file
87
src/components/sidebar/SidebarNavArrows.test.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { SidebarNavArrows } from './SidebarNavArrows'
|
||||
|
||||
const { trackNavigationHistoryButtonClickedMock } = vi.hoisted(() => ({
|
||||
trackNavigationHistoryButtonClickedMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../../lib/productAnalytics', () => ({
|
||||
trackNavigationHistoryButtonClicked: trackNavigationHistoryButtonClickedMock,
|
||||
}))
|
||||
|
||||
function renderArrows(props: Partial<ComponentProps<typeof SidebarNavArrows>> = {}) {
|
||||
const defaults = {
|
||||
canGoBack: true,
|
||||
canGoForward: true,
|
||||
onGoBack: vi.fn(),
|
||||
onGoForward: vi.fn(),
|
||||
}
|
||||
const merged = { ...defaults, ...props }
|
||||
return {
|
||||
...render(
|
||||
<TooltipProvider>
|
||||
<SidebarNavArrows {...merged} />
|
||||
</TooltipProvider>,
|
||||
),
|
||||
props: merged,
|
||||
}
|
||||
}
|
||||
|
||||
describe('SidebarNavArrows', () => {
|
||||
beforeEach(() => {
|
||||
trackNavigationHistoryButtonClickedMock.mockClear()
|
||||
})
|
||||
|
||||
it('renders back and forward buttons with default English labels', () => {
|
||||
renderArrows()
|
||||
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Forward' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('localizes button labels', () => {
|
||||
renderArrows({ locale: 'zh-CN' })
|
||||
expect(screen.getByRole('button', { name: '返回' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '前进' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls and tracks back navigation when back is clicked', () => {
|
||||
const { props } = renderArrows()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back' }))
|
||||
expect(props.onGoBack).toHaveBeenCalledOnce()
|
||||
expect(trackNavigationHistoryButtonClickedMock).toHaveBeenCalledWith('back')
|
||||
})
|
||||
|
||||
it('calls and tracks forward navigation when forward is clicked', () => {
|
||||
const { props } = renderArrows()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Forward' }))
|
||||
expect(props.onGoForward).toHaveBeenCalledOnce()
|
||||
expect(trackNavigationHistoryButtonClickedMock).toHaveBeenCalledWith('forward')
|
||||
})
|
||||
|
||||
it('disables back navigation when history is unavailable', () => {
|
||||
const { props } = renderArrows({ canGoBack: false })
|
||||
const back = screen.getByRole('button', { name: 'Back' })
|
||||
expect(back).toBeDisabled()
|
||||
fireEvent.click(back)
|
||||
expect(props.onGoBack).not.toHaveBeenCalled()
|
||||
expect(trackNavigationHistoryButtonClickedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('disables forward navigation when future history is unavailable', () => {
|
||||
const { props } = renderArrows({ canGoForward: false })
|
||||
const forward = screen.getByRole('button', { name: 'Forward' })
|
||||
expect(forward).toBeDisabled()
|
||||
fireEvent.click(forward)
|
||||
expect(props.onGoForward).not.toHaveBeenCalled()
|
||||
expect(trackNavigationHistoryButtonClickedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('marks both buttons as non-drag controls', () => {
|
||||
renderArrows()
|
||||
expect(screen.getByRole('button', { name: 'Back' })).toHaveAttribute('data-no-drag')
|
||||
expect(screen.getByRole('button', { name: 'Forward' })).toHaveAttribute('data-no-drag')
|
||||
})
|
||||
})
|
||||
73
src/components/sidebar/SidebarNavArrows.tsx
Normal file
73
src/components/sidebar/SidebarNavArrows.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { CaretLeft, CaretRight } from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ActionTooltip } from '@/components/ui/action-tooltip'
|
||||
import { APP_COMMAND_IDS, getAppCommandShortcutDisplay } from '../../hooks/appCommandCatalog'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
import { trackNavigationHistoryButtonClicked } from '../../lib/productAnalytics'
|
||||
|
||||
export interface SidebarNavArrowsProps {
|
||||
canGoBack: boolean
|
||||
canGoForward: boolean
|
||||
onGoBack: () => void
|
||||
onGoForward: () => void
|
||||
locale?: AppLocale
|
||||
}
|
||||
|
||||
const navButtonClass = 'h-6 w-6 rounded text-muted-foreground hover:text-foreground disabled:opacity-40'
|
||||
|
||||
export function SidebarNavArrows({
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
onGoBack,
|
||||
onGoForward,
|
||||
locale = 'en',
|
||||
}: SidebarNavArrowsProps) {
|
||||
const backLabel = translate(locale, 'sidebar.nav.back')
|
||||
const forwardLabel = translate(locale, 'sidebar.nav.forward')
|
||||
const backShortcut = getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewGoBack)
|
||||
const forwardShortcut = getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewGoForward)
|
||||
|
||||
const handleBackClick = () => {
|
||||
trackNavigationHistoryButtonClicked('back')
|
||||
onGoBack()
|
||||
}
|
||||
const handleForwardClick = () => {
|
||||
trackNavigationHistoryButtonClicked('forward')
|
||||
onGoForward()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1" data-testid="sidebar-nav-arrows">
|
||||
<ActionTooltip copy={{ label: backLabel, shortcut: backShortcut }} side="bottom">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className={navButtonClass}
|
||||
disabled={!canGoBack}
|
||||
onClick={handleBackClick}
|
||||
aria-label={backLabel}
|
||||
data-no-drag
|
||||
data-testid="sidebar-nav-back"
|
||||
>
|
||||
<CaretLeft size={14} weight="bold" />
|
||||
</Button>
|
||||
</ActionTooltip>
|
||||
<ActionTooltip copy={{ label: forwardLabel, shortcut: forwardShortcut }} side="bottom">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className={navButtonClass}
|
||||
disabled={!canGoForward}
|
||||
onClick={handleForwardClick}
|
||||
aria-label={forwardLabel}
|
||||
data-no-drag
|
||||
data-testid="sidebar-nav-forward"
|
||||
>
|
||||
<CaretRight size={14} weight="bold" />
|
||||
</Button>
|
||||
</ActionTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
CaretLeft, Plus,
|
||||
} from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import {
|
||||
type SectionGroup, isSelectionActive, SectionContent, VisibilityPopover,
|
||||
} from '../SidebarParts'
|
||||
@@ -21,6 +22,7 @@ import { TypeCustomizePopover } from '../TypeCustomizePopover'
|
||||
import { useDragRegion } from '../../hooks/useDragRegion'
|
||||
import { SidebarGroupHeader } from './SidebarGroupHeader'
|
||||
import { SidebarViewItem } from './SidebarViewItem'
|
||||
import { SidebarNavArrows } from './SidebarNavArrows'
|
||||
import { computeReorder } from './sidebarHooks'
|
||||
import { countByFilter } from '../../utils/noteListHelpers'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
@@ -302,27 +304,55 @@ export function TypesSection({
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarTitleBar({ locale = 'en', onCollapse }: { locale?: AppLocale; onCollapse?: () => void }) {
|
||||
const noopNavigation = () => {}
|
||||
|
||||
export function SidebarTitleBar({
|
||||
locale = 'en',
|
||||
onCollapse,
|
||||
canGoBack = false,
|
||||
canGoForward = false,
|
||||
onGoBack = noopNavigation,
|
||||
onGoForward = noopNavigation,
|
||||
}: {
|
||||
locale?: AppLocale
|
||||
onCollapse?: () => void
|
||||
canGoBack?: boolean
|
||||
canGoForward?: boolean
|
||||
onGoBack?: () => void
|
||||
onGoForward?: () => void
|
||||
}) {
|
||||
const { onMouseDown } = useDragRegion()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="shrink-0 flex items-center justify-end border-b border-border"
|
||||
style={{ height: 52, padding: '0 8px', paddingLeft: 80, cursor: 'default' }}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{onCollapse && (
|
||||
<button
|
||||
className="flex shrink-0 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
style={{ width: 24, height: 24 }}
|
||||
onClick={onCollapse}
|
||||
aria-label={translate(locale, 'sidebar.action.collapse')}
|
||||
title={translate(locale, 'sidebar.action.collapse')}
|
||||
>
|
||||
<CaretLeft size={14} weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<div
|
||||
className="flex shrink-0 items-center justify-between gap-2 border-b border-border"
|
||||
style={{ height: 52, padding: '0 8px', paddingLeft: 80, cursor: 'default' }}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
<SidebarNavArrows
|
||||
canGoBack={canGoBack}
|
||||
canGoForward={canGoForward}
|
||||
onGoBack={onGoBack}
|
||||
onGoForward={onGoForward}
|
||||
locale={locale}
|
||||
/>
|
||||
{onCollapse && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="h-6 w-6 rounded text-muted-foreground hover:text-foreground"
|
||||
onClick={onCollapse}
|
||||
aria-label={translate(locale, 'sidebar.action.collapse')}
|
||||
title={translate(locale, 'sidebar.action.collapse')}
|
||||
data-no-drag
|
||||
>
|
||||
<CaretLeft size={14} weight="bold" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
51
src/components/sidebar/SidebarTitleBar.test.tsx
Normal file
51
src/components/sidebar/SidebarTitleBar.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { SidebarTitleBar } from './SidebarSections'
|
||||
|
||||
function renderTitleBar(overrides: Partial<ComponentProps<typeof SidebarTitleBar>> = {}) {
|
||||
const props = {
|
||||
canGoBack: true,
|
||||
canGoForward: true,
|
||||
onGoBack: vi.fn(),
|
||||
onGoForward: vi.fn(),
|
||||
onCollapse: vi.fn(),
|
||||
...overrides,
|
||||
}
|
||||
return {
|
||||
...render(<SidebarTitleBar {...props} />),
|
||||
props,
|
||||
}
|
||||
}
|
||||
|
||||
describe('SidebarTitleBar', () => {
|
||||
it('renders navigation arrows and the collapse button together', () => {
|
||||
renderTitleBar()
|
||||
expect(screen.getByTestId('sidebar-nav-arrows')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Forward' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Collapse sidebar' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('forwards back and forward clicks', () => {
|
||||
const { props } = renderTitleBar()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Back' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Forward' }))
|
||||
expect(props.onGoBack).toHaveBeenCalledOnce()
|
||||
expect(props.onGoForward).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps the collapse button clickable and outside the drag region', () => {
|
||||
const { props } = renderTitleBar()
|
||||
const collapse = screen.getByRole('button', { name: 'Collapse sidebar' })
|
||||
fireEvent.click(collapse)
|
||||
expect(props.onCollapse).toHaveBeenCalledOnce()
|
||||
expect(collapse).toHaveAttribute('data-no-drag')
|
||||
})
|
||||
|
||||
it('hides collapse when unavailable but keeps history controls visible', () => {
|
||||
renderTitleBar({ onCollapse: undefined })
|
||||
expect(screen.getByTestId('sidebar-nav-arrows')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Collapse sidebar' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Posteingang",
|
||||
"sidebar.nav.allNotes": "Alle Notizen",
|
||||
"sidebar.nav.archive": "Archivieren",
|
||||
"sidebar.nav.back": "Zurück",
|
||||
"sidebar.nav.forward": "Weiter",
|
||||
"sidebar.group.favorites": "FAVORITEN",
|
||||
"sidebar.group.views": "AUFRUFE",
|
||||
"sidebar.group.types": "TYPEN",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Inbox",
|
||||
"sidebar.nav.allNotes": "All Notes",
|
||||
"sidebar.nav.archive": "Archive",
|
||||
"sidebar.nav.back": "Back",
|
||||
"sidebar.nav.forward": "Forward",
|
||||
"sidebar.group.favorites": "FAVORITES",
|
||||
"sidebar.group.views": "VIEWS",
|
||||
"sidebar.group.types": "TYPES",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Bandeja de entrada",
|
||||
"sidebar.nav.allNotes": "Todas las notas",
|
||||
"sidebar.nav.archive": "Archivar",
|
||||
"sidebar.nav.back": "Atrás",
|
||||
"sidebar.nav.forward": "Siguiente",
|
||||
"sidebar.group.favorites": "FAVORITOS",
|
||||
"sidebar.group.views": "VISTAS",
|
||||
"sidebar.group.types": "TIPOS",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Bandeja de entrada",
|
||||
"sidebar.nav.allNotes": "Todas las notas",
|
||||
"sidebar.nav.archive": "Archivar",
|
||||
"sidebar.nav.back": "Atrás",
|
||||
"sidebar.nav.forward": "Siguiente",
|
||||
"sidebar.group.favorites": "FAVORITOS",
|
||||
"sidebar.group.views": "VISUALIZACIONES",
|
||||
"sidebar.group.types": "TIPOS",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Boîte de réception",
|
||||
"sidebar.nav.allNotes": "Toutes les notes",
|
||||
"sidebar.nav.archive": "Archiver",
|
||||
"sidebar.nav.back": "Retour",
|
||||
"sidebar.nav.forward": "Suivant",
|
||||
"sidebar.group.favorites": "FAVORIS",
|
||||
"sidebar.group.views": "VUES",
|
||||
"sidebar.group.types": "TYPES",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Inbox",
|
||||
"sidebar.nav.allNotes": "Tutte le note",
|
||||
"sidebar.nav.archive": "Archiviate",
|
||||
"sidebar.nav.back": "Indietro",
|
||||
"sidebar.nav.forward": "Avanti",
|
||||
"sidebar.group.favorites": "PREFERITI",
|
||||
"sidebar.group.views": "VISTE",
|
||||
"sidebar.group.types": "TIPI",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "受信トレイ",
|
||||
"sidebar.nav.allNotes": "すべてのノート",
|
||||
"sidebar.nav.archive": "アーカイブ",
|
||||
"sidebar.nav.back": "戻る",
|
||||
"sidebar.nav.forward": "進む",
|
||||
"sidebar.group.favorites": "お気に入り",
|
||||
"sidebar.group.views": "閲覧数",
|
||||
"sidebar.group.types": "タイプ",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "받은 편지함",
|
||||
"sidebar.nav.allNotes": "모든 노트",
|
||||
"sidebar.nav.archive": "보관",
|
||||
"sidebar.nav.back": "뒤로",
|
||||
"sidebar.nav.forward": "다음",
|
||||
"sidebar.group.favorites": "즐겨찾기",
|
||||
"sidebar.group.views": "조회수",
|
||||
"sidebar.group.types": "유형",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Caixa de entrada",
|
||||
"sidebar.nav.allNotes": "Todas as anotações",
|
||||
"sidebar.nav.archive": "Arquivar",
|
||||
"sidebar.nav.back": "Voltar",
|
||||
"sidebar.nav.forward": "Avançar",
|
||||
"sidebar.group.favorites": "FAVORITOS",
|
||||
"sidebar.group.views": "VISUALIZAÇÕES",
|
||||
"sidebar.group.types": "TIPOS",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Caixa de entrada",
|
||||
"sidebar.nav.allNotes": "Todas as notas",
|
||||
"sidebar.nav.archive": "Arquivar",
|
||||
"sidebar.nav.back": "Voltar",
|
||||
"sidebar.nav.forward": "Avançar",
|
||||
"sidebar.group.favorites": "FAVORITOS",
|
||||
"sidebar.group.views": "VISUALIZAÇÕES",
|
||||
"sidebar.group.types": "TIPOS",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "Входящие",
|
||||
"sidebar.nav.allNotes": "Все заметки",
|
||||
"sidebar.nav.archive": "Архив",
|
||||
"sidebar.nav.back": "Назад",
|
||||
"sidebar.nav.forward": "Вперед",
|
||||
"sidebar.group.favorites": "ИЗБРАННОЕ",
|
||||
"sidebar.group.views": "ПРОСМОТРЫ",
|
||||
"sidebar.group.types": "ТИПЫ",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "收件箱",
|
||||
"sidebar.nav.allNotes": "全部笔记",
|
||||
"sidebar.nav.archive": "归档",
|
||||
"sidebar.nav.back": "返回",
|
||||
"sidebar.nav.forward": "前进",
|
||||
"sidebar.group.favorites": "收藏",
|
||||
"sidebar.group.views": "视图",
|
||||
"sidebar.group.types": "类型",
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
"sidebar.nav.inbox": "收件箱",
|
||||
"sidebar.nav.allNotes": "全部筆記",
|
||||
"sidebar.nav.archive": "歸檔",
|
||||
"sidebar.nav.back": "返回",
|
||||
"sidebar.nav.forward": "下一頁",
|
||||
"sidebar.group.favorites": "收藏",
|
||||
"sidebar.group.views": "檢視",
|
||||
"sidebar.group.types": "型別",
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { FilePreviewKind } from '../utils/filePreview'
|
||||
type TrackedPreviewKind = FilePreviewKind | 'unsupported'
|
||||
type FilePreviewAction = 'copy_path' | 'open_external' | 'reveal'
|
||||
type AgentBlockedReason = 'agent_unavailable' | 'missing_vault'
|
||||
type NavigationHistoryDirection = 'back' | 'forward'
|
||||
|
||||
const ALL_NOTES_VISIBILITY_CATEGORIES: ReadonlyArray<keyof AllNotesFileVisibility> = [
|
||||
'pdfs',
|
||||
@@ -52,6 +53,10 @@ export function trackAllNotesVisibilityChanged(
|
||||
}
|
||||
}
|
||||
|
||||
export function trackNavigationHistoryButtonClicked(direction: NavigationHistoryDirection): void {
|
||||
trackEvent('navigation_history_button_clicked', { direction })
|
||||
}
|
||||
|
||||
export function trackAiAgentMessageBlocked(agent: AiAgentId, reason: AgentBlockedReason): void {
|
||||
trackEvent('ai_agent_message_blocked', { agent, reason })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user