diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0d8f00bc..caa8a2f5 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -21,6 +21,10 @@ "resizable": true, "fullscreen": false, "titleBarStyle": "Overlay", + "trafficLightPosition": { + "x": 18, + "y": 24 + }, "hiddenTitle": true, "backgroundColor": "#F7F6F3", "dragDropEnabled": true diff --git a/src/components/BreadcrumbBar.test.tsx b/src/components/BreadcrumbBar.test.tsx index e1f48a06..1e9c35b8 100644 --- a/src/components/BreadcrumbBar.test.tsx +++ b/src/components/BreadcrumbBar.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, act } from '@testing-library/react' +import { render, screen, fireEvent, act, within } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { BreadcrumbBar } from './BreadcrumbBar' import { formatShortcutDisplay } from '../hooks/appCommandCatalog' @@ -284,6 +284,33 @@ describe('BreadcrumbBar — action buttons always right-aligned', () => { const actions = container.querySelector('.breadcrumb-bar__actions') expect(actions).toBeInTheDocument() expect(actions).toHaveClass('ml-auto') + expect(actions).toHaveStyle({ gap: '8px' }) + }) + + it('keeps grouped action buttons evenly spaced', () => { + render( + , + ) + + const fileActionsGroup = screen.getByTestId('breadcrumb-reveal-file').closest('.breadcrumb-bar__overflowable-action') + expect(fileActionsGroup).toHaveClass('gap-2') + const widthActionGroup = screen.getByRole('button', { name: 'Switch to wide note width' }).closest('.breadcrumb-bar__overflowable-action') + expect(widthActionGroup).toHaveClass('gap-2') + }) + + it('lets the title use the free space before the fixed drag gap', () => { + const { container } = render() + + expect(container.querySelector('.breadcrumb-bar__title')).toHaveClass('flex-1') + expect(container.querySelector('.breadcrumb-bar__drag-spacer')).toHaveClass('w-6', 'shrink-0') + expect(container.querySelector('.breadcrumb-bar__drag-spacer')).not.toHaveClass('flex-1') }) it('does not render the unused backlinks or more-actions placeholders', () => { @@ -291,6 +318,35 @@ describe('BreadcrumbBar — action buttons always right-aligned', () => { expect(screen.queryByRole('button', { name: 'Backlinks are coming soon' })).not.toBeInTheDocument() expect(screen.queryByRole('button', { name: 'More note actions are coming soon' })).not.toBeInTheDocument() }) + + it('exposes lower-priority actions through the overflow menu', async () => { + render( + , + ) + + fireEvent.pointerDown(screen.getByRole('button', { name: 'More note actions' }), { + button: 0, + ctrlKey: false, + }) + + const menu = await screen.findByRole('menu') + expect(within(menu).getByRole('menuitem', { name: 'Show the current diff' })).toBeInTheDocument() + expect(within(menu).getByRole('menuitem', { name: 'Switch to wide note width' })).toBeInTheDocument() + expect(within(menu).getByRole('menuitem', { name: 'Reveal in Finder' })).toBeInTheDocument() + expect(within(menu).getByRole('menuitem', { name: 'Copy file path' })).toBeInTheDocument() + expect(within(menu).getByRole('menuitem', { name: 'Archive this note' })).toBeInTheDocument() + expect(within(menu).getByRole('menuitem', { name: 'Delete this note' })).toBeInTheDocument() + }) }) describe('BreadcrumbBar — raw editor toggle', () => { diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index 8d9c3808..b3946b4a 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -1,4 +1,4 @@ -import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from 'react' +import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode } from 'react' import type { NoteWidthMode, VaultEntry } from '../types' import { cn } from '@/lib/utils' import { translate, type AppLocale } from '../lib/i18n' @@ -7,6 +7,12 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { ActionTooltip, type ActionTooltipCopy } from '@/components/ui/action-tooltip' import { TooltipProvider } from '@/components/ui/tooltip' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { GitBranch, Code, @@ -22,6 +28,7 @@ import { ArrowsClockwise, ArrowsInLineHorizontal, ArrowsOutLineHorizontal, + DotsThree, } from '@phosphor-icons/react' import { NoteTitleIcon } from './NoteTitleIcon' import { slugify } from '../hooks/useNoteCreation' @@ -60,6 +67,7 @@ interface BreadcrumbBarProps { const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const const BREADCRUMB_ICON_CLASS = 'size-[16px]' +const TITLE_ACTION_GAP_PX = 24 function focusFilenameInput( isEditing: boolean, @@ -433,6 +441,159 @@ function InspectorAction({ ) } +function OverflowToolbarAction({ children }: { children: ReactNode }) { + return {children} +} + +function diffMenuLabelKey({ + showDiffToggle, + diffMode, + diffLoading, +}: Pick): Parameters[1] { + if (diffLoading) return 'editor.toolbar.loadingDiff' + if (diffMode) return 'editor.toolbar.rawReturn' + if (showDiffToggle) return 'editor.toolbar.showDiff' + return 'editor.toolbar.noDiff' +} + +function availableDiffAction(showDiffToggle: boolean, onToggleDiff: () => void): (() => void) | undefined { + return showDiffToggle ? onToggleDiff : undefined +} + +function noteWidthLabelKey(noteWidth: NoteWidthMode = 'normal'): Parameters[1] { + return noteWidth === 'wide' ? 'editor.toolbar.noteWidthNormal' : 'editor.toolbar.noteWidthWide' +} + +function NoteWidthMenuIcon({ noteWidth = 'normal' }: { noteWidth?: NoteWidthMode }) { + return noteWidth === 'wide' ? : +} + +function archiveLabelKey(archived: boolean): Parameters[1] { + return archived ? 'editor.toolbar.restoreArchived' : 'editor.toolbar.archive' +} + +function archiveAction( + archived: boolean, + onArchive?: () => void, + onUnarchive?: () => void, +): (() => void) | undefined { + return archived ? onUnarchive : onArchive +} + +function pathAction(action: ((path: string) => void) | undefined, path: string): (() => void) | undefined { + return action ? () => action(path) : undefined +} + +function ArchiveMenuIcon({ archived }: { archived: boolean }) { + return archived ? : +} + +function measureExpandedActionsWidth( + actions: HTMLDivElement, + collapsed: boolean, + cachedExpandedActionsWidth: number, +) { + return collapsed ? cachedExpandedActionsWidth || actions.scrollWidth : actions.scrollWidth +} + +function readElementWidth(element: HTMLElement): number { + return element.getBoundingClientRect().width || element.scrollWidth || element.clientWidth +} + +function prepareTitleMeasurementClone(clone: HTMLElement) { + clone.setAttribute('aria-hidden', 'true') + clone.style.position = 'absolute' + clone.style.visibility = 'hidden' + clone.style.pointerEvents = 'none' + clone.style.width = 'max-content' + clone.style.minWidth = 'max-content' + clone.style.maxWidth = 'none' + clone.style.overflow = 'visible' + clone.style.whiteSpace = 'nowrap' +} + +function removeCloneTruncation(clone: HTMLElement) { + for (const node of clone.querySelectorAll('.truncate')) { + node.style.overflow = 'visible' + node.style.textOverflow = 'clip' + node.style.whiteSpace = 'nowrap' + node.style.width = 'max-content' + node.style.minWidth = 'max-content' + node.style.maxWidth = 'none' + } +} + +function measureNaturalTitleWidth(title: HTMLDivElement): number { + const titleContent = title.querySelector('.breadcrumb-bar__title-content') + if (!(titleContent instanceof HTMLElement)) return readElementWidth(title) + + const clone = titleContent.cloneNode(true) as HTMLElement + prepareTitleMeasurementClone(clone) + removeCloneTruncation(clone) + title.appendChild(clone) + const width = readElementWidth(clone) + clone.remove() + return width +} + +function expandedActionsLeft(actions: HTMLDivElement, expandedActionsWidth: number): number { + const actionsRight = actions.getBoundingClientRect().right + return actionsRight - expandedActionsWidth +} + +function shouldCollapseBreadcrumbOverflow( + title: HTMLDivElement, + actions: HTMLDivElement, + expandedActionsWidth: number, +) { + const titleLeft = title.getBoundingClientRect().left + const availableTitleWidth = expandedActionsLeft(actions, expandedActionsWidth) - titleLeft - TITLE_ACTION_GAP_PX + return measureNaturalTitleWidth(title) > availableTitleWidth +} + +function useBreadcrumbOverflow( + titleRef: React.RefObject, + actionsRef: React.RefObject, +) { + const [collapsed, setCollapsed] = useState(false) + const expandedActionsWidthRef = useRef(0) + + useLayoutEffect(() => { + const title = titleRef.current + const actions = actionsRef.current + const bar = title?.closest('.breadcrumb-bar') as HTMLDivElement | null + if (!title || !actions || !bar) return undefined + + let frame = 0 + const measure = () => { + const expandedActionsWidth = measureExpandedActionsWidth(actions, collapsed, expandedActionsWidthRef.current) + if (!collapsed) expandedActionsWidthRef.current = expandedActionsWidth + setCollapsed(shouldCollapseBreadcrumbOverflow(title, actions, expandedActionsWidth)) + } + const scheduleMeasure = () => { + cancelAnimationFrame(frame) + frame = requestAnimationFrame(measure) + } + + scheduleMeasure() + if (typeof ResizeObserver === 'undefined') { + return () => cancelAnimationFrame(frame) + } + + const resizeObserver = new ResizeObserver(scheduleMeasure) + resizeObserver.observe(bar) + resizeObserver.observe(title) + resizeObserver.observe(actions) + + return () => { + cancelAnimationFrame(frame) + resizeObserver.disconnect() + } + }) + + return collapsed +} + function normalizeFilenameStemInput(value: string): string { const trimmed = value.trim() return trimmed.replace(/\.md$/i, '').trim() @@ -503,7 +664,7 @@ function FilenameTrigger({ aria-label={translate(locale, 'editor.filename.trigger', { filename: filenameStem })} > - {filenameStem} + {filenameStem} ) } @@ -648,30 +809,149 @@ function BreadcrumbActions({ onDelete, onArchive, onUnarchive, + actionsRef, + overflowCollapsed, locale = 'en', -}: Omit) { +}: Omit & { + actionsRef: React.RefObject + overflowCollapsed: boolean +}) { return ( -
+
- + + + {!forceRawMode && } + + + + + + + + + + + + + + - {!forceRawMode && } - - - - -
) } +function BreadcrumbOverflowMenu({ + entry, + showDiffToggle, + diffMode, + diffLoading, + onToggleDiff, + noteWidth, + onToggleNoteWidth, + onRevealFile, + onCopyFilePath, + onArchive, + onUnarchive, + onDelete, + locale = 'en', +}: Pick< + BreadcrumbBarProps, + | 'entry' + | 'showDiffToggle' + | 'diffMode' + | 'diffLoading' + | 'onToggleDiff' + | 'noteWidth' + | 'onToggleNoteWidth' + | 'onRevealFile' + | 'onCopyFilePath' + | 'onArchive' + | 'onUnarchive' + | 'onDelete' + | 'locale' +>) { + const runDiffAction = availableDiffAction(showDiffToggle, onToggleDiff) + const runRevealAction = pathAction(onRevealFile, entry.path) + const runCopyPathAction = pathAction(onCopyFilePath, entry.path) + const runArchiveAction = archiveAction(entry.archived, onArchive, onUnarchive) + const diffLabel = translate(locale, diffMenuLabelKey({ showDiffToggle, diffMode, diffLoading })) + const noteWidthLabel = translate(locale, noteWidthLabelKey(noteWidth)) + const archiveLabel = translate(locale, archiveLabelKey(entry.archived)) + + return ( + + + + + + + + + + {diffLabel} + + + + {noteWidthLabel} + + + + {translate(locale, 'editor.toolbar.revealFile')} + + + + {translate(locale, 'editor.toolbar.copyFilePath')} + + + + {archiveLabel} + + + + {translate(locale, 'editor.toolbar.delete')} + + + + ) +} + function BreadcrumbTitle({ entry, locale, @@ -680,7 +960,7 @@ function BreadcrumbTitle({ }: Pick) { const typeLabel = entry.isA ?? 'Note' return ( -
+
{typeLabel}
@@ -701,6 +981,9 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({ ...actionProps }: BreadcrumbBarProps) { const { onMouseDown } = useDragRegion() + const actionsRef = useRef(null) + const titleRef = useRef(null) + const overflowCollapsed = useBreadcrumbOverflow(titleRef, actionsRef) return ( @@ -713,11 +996,11 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({ style={{ height: 52, background: 'var(--background)', - padding: '6px 16px', + padding: '6px 16px 6px var(--breadcrumb-bar-left-padding, 16px)', boxSizing: 'border-box', }} > -
+
) diff --git a/src/components/BreadcrumbBar.visibility.test.tsx b/src/components/BreadcrumbBar.visibility.test.tsx index b74776a4..d435ccec 100644 --- a/src/components/BreadcrumbBar.visibility.test.tsx +++ b/src/components/BreadcrumbBar.visibility.test.tsx @@ -67,4 +67,22 @@ describe('BreadcrumbBar filename visibility', () => { expect(editorCss).toContain('padding: 0;') expect(editorCss).toContain('border-radius: 0;') }) + + it('offsets the editor-only breadcrumb title past the macOS traffic lights', () => { + const editorCss = readFileSync(`${process.cwd()}/src/components/Editor.css`, 'utf8') + + expect(editorCss).toContain('.app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadcrumb-bar') + expect(editorCss).toContain('--breadcrumb-bar-left-padding: 90px;') + }) + + it('moves lower-priority breadcrumb actions into the overflow menu from measured overflow state', () => { + const editorCss = readFileSync(`${process.cwd()}/src/components/Editor.css`, 'utf8') + + expect(editorCss).not.toContain('@container (max-width:') + expect(editorCss).toContain(".breadcrumb-bar__actions[data-overflow-collapsed='true']") + expect(editorCss).toContain('.breadcrumb-bar__overflowable-action') + expect(editorCss).toContain('display: none;') + expect(editorCss).toContain('.breadcrumb-bar__overflow-menu') + expect(editorCss).toContain('display: flex;') + }) }) diff --git a/src/components/Editor.css b/src/components/Editor.css index 40e71a42..0bb0ad48 100644 --- a/src/components/Editor.css +++ b/src/components/Editor.css @@ -25,9 +25,15 @@ /* Breadcrumb bar: border can still react to the data attribute, but the breadcrumb filename/title stays visible at all times. */ .breadcrumb-bar { + container-type: inline-size; + --breadcrumb-bar-left-padding: 16px; transition: border-color 0.2s ease; } +.app:not(:has(.app__sidebar)):not(:has(.app__note-list)) .breadcrumb-bar { + --breadcrumb-bar-left-padding: 90px; +} + .breadcrumb-bar[data-title-hidden] { border-bottom-color: var(--border); } @@ -48,6 +54,18 @@ background: transparent; } +.breadcrumb-bar__overflow-menu { + display: none; +} + +.breadcrumb-bar__actions[data-overflow-collapsed='true'] .breadcrumb-bar__overflowable-action { + display: none; +} + +.breadcrumb-bar__actions[data-overflow-collapsed='true'] .breadcrumb-bar__overflow-menu { + display: flex; +} + /* Scroll area wrapping title + editor — single scroll context for alignment */ .editor-scroll-area { flex: 1; diff --git a/src/components/sidebar/FavoritesSection.tsx b/src/components/sidebar/FavoritesSection.tsx index 098f7abe..bcde6600 100644 --- a/src/components/sidebar/FavoritesSection.tsx +++ b/src/components/sidebar/FavoritesSection.tsx @@ -11,7 +11,7 @@ import { buildTypeEntryMap, getTypeColor, getTypeLightColor } from '../../utils/ import { NoteTitleIcon } from '../NoteTitleIcon' import { isSelectionActive } from '../SidebarParts' import { SidebarGroupHeader } from './SidebarGroupHeader' -import { SIDEBAR_ITEM_PADDING } from './sidebarStyles' +import { SIDEBAR_ITEM_PADDING, SIDEBAR_SECTION_CONTENT_PADDING_BOTTOM } from './sidebarStyles' import { translate, type AppLocale } from '../../lib/i18n' const FAVORITE_TYPE_ICON_MAP: Record = { @@ -141,7 +141,7 @@ export function FavoritesSection({ {!collapsed && ( -
+
{favorites.map((entry) => ( {!collapsed && ( -
+
{onReorderViews ? ( @@ -303,13 +304,15 @@ export function TypesSection({ )}
{!collapsed && ( - - - {visibleSections.map((group) => ( - - ))} - - +
+ + + {visibleSections.map((group) => ( + + ))} + + +
)}
) diff --git a/src/components/sidebar/sidebarStyles.ts b/src/components/sidebar/sidebarStyles.ts index af469a6d..ec79e2b7 100644 --- a/src/components/sidebar/sidebarStyles.ts +++ b/src/components/sidebar/sidebarStyles.ts @@ -9,3 +9,5 @@ export const SIDEBAR_GROUP_HEADER_PADDING = { regular: '8px 14px 8px 16px', withCount: '8px 8px 8px 16px', } as const + +export const SIDEBAR_SECTION_CONTENT_PADDING_BOTTOM = 8 diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json index 4edf6428..843a6393 100644 --- a/src/lib/locales/de-DE.json +++ b/src/lib/locales/de-DE.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Diese Notiz löschen", "editor.toolbar.revealFile": "Im Finder anzeigen", "editor.toolbar.copyFilePath": "Dateipfad kopieren", + "editor.toolbar.moreActions": "Weitere Notizaktionen", "editor.toolbar.openProperties": "Eigenschaften-Panel öffnen", "editor.imageLightbox.title": "Bildvorschau", "editor.filename.rename": "Dateinamen umbenennen", diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 37ed6656..259bb5d5 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Delete this note", "editor.toolbar.revealFile": "Reveal in Finder", "editor.toolbar.copyFilePath": "Copy file path", + "editor.toolbar.moreActions": "More note actions", "editor.toolbar.openProperties": "Open the properties panel", "editor.imageLightbox.title": "Image preview", "editor.filename.rename": "Rename filename", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 79c09e79..72069e33 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Eliminar esta nota", "editor.toolbar.revealFile": "Mostrar en el Finder", "editor.toolbar.copyFilePath": "Copiar la ruta del archivo", + "editor.toolbar.moreActions": "Más acciones de nota", "editor.toolbar.openProperties": "Abrir el panel de propiedades", "editor.imageLightbox.title": "Vista previa de la imagen", "editor.filename.rename": "Cambiar el nombre del archivo", diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json index 0a920095..a23e4939 100644 --- a/src/lib/locales/es-ES.json +++ b/src/lib/locales/es-ES.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Eliminar esta nota", "editor.toolbar.revealFile": "Mostrar en el Finder", "editor.toolbar.copyFilePath": "Copiar la ruta del archivo", + "editor.toolbar.moreActions": "Más acciones de nota", "editor.toolbar.openProperties": "Abrir el panel de propiedades", "editor.imageLightbox.title": "Vista previa de la imagen", "editor.filename.rename": "Cambiar el nombre del archivo", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index 7fbbc6b2..0787ddf0 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Supprimer cette note", "editor.toolbar.revealFile": "Afficher dans le Finder", "editor.toolbar.copyFilePath": "Copier le chemin du fichier", + "editor.toolbar.moreActions": "Autres actions de note", "editor.toolbar.openProperties": "Ouvrir le panneau des propriétés", "editor.imageLightbox.title": "Aperçu de l'image", "editor.filename.rename": "Renommer le fichier", diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json index ae07ef1b..df4b50e9 100644 --- a/src/lib/locales/it-IT.json +++ b/src/lib/locales/it-IT.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Elimina questa nota", "editor.toolbar.revealFile": "Mostra nel Finder", "editor.toolbar.copyFilePath": "Copia il percorso del file", + "editor.toolbar.moreActions": "Altre azioni della nota", "editor.toolbar.openProperties": "Apri il pannello delle proprietà", "editor.imageLightbox.title": "Anteprima immagine", "editor.filename.rename": "Rinomina nome file", diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json index 7aa0172c..9eeb521f 100644 --- a/src/lib/locales/ja-JP.json +++ b/src/lib/locales/ja-JP.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "このノートを削除", "editor.toolbar.revealFile": "Finderで表示", "editor.toolbar.copyFilePath": "ファイルパスをコピー", + "editor.toolbar.moreActions": "その他のノート操作", "editor.toolbar.openProperties": "プロパティパネルを開く", "editor.imageLightbox.title": "画像プレビュー", "editor.filename.rename": "ファイル名を変更", diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json index 7c102608..75968ed7 100644 --- a/src/lib/locales/ko-KR.json +++ b/src/lib/locales/ko-KR.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "이 노트 삭제", "editor.toolbar.revealFile": "Finder에서 표시", "editor.toolbar.copyFilePath": "파일 경로 복사", + "editor.toolbar.moreActions": "추가 노트 작업", "editor.toolbar.openProperties": "속성 패널 열기", "editor.imageLightbox.title": "이미지 미리 보기", "editor.filename.rename": "파일 이름 변경", diff --git a/src/lib/locales/pl-PL.json b/src/lib/locales/pl-PL.json index 48f0fc6b..d6c3dd35 100644 --- a/src/lib/locales/pl-PL.json +++ b/src/lib/locales/pl-PL.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Usuń tę notatkę", "editor.toolbar.revealFile": "Pokaż w Finderze", "editor.toolbar.copyFilePath": "Kopiuj ścieżkę pliku", + "editor.toolbar.moreActions": "Więcej działań notatki", "editor.toolbar.openProperties": "Otwórz panel właściwości", "editor.imageLightbox.title": "Podgląd obrazu", "editor.filename.rename": "Zmień nazwę pliku", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index 0b99a1cb..28542068 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Excluir esta nota", "editor.toolbar.revealFile": "Exibir no Finder", "editor.toolbar.copyFilePath": "Copiar caminho do arquivo", + "editor.toolbar.moreActions": "Mais ações da nota", "editor.toolbar.openProperties": "Abrir o painel de propriedades", "editor.imageLightbox.title": "Pré-visualização da imagem", "editor.filename.rename": "Renomear nome do arquivo", diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json index f2bdfd7d..4c16dda8 100644 --- a/src/lib/locales/pt-PT.json +++ b/src/lib/locales/pt-PT.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Eliminar esta nota", "editor.toolbar.revealFile": "Mostrar no Finder", "editor.toolbar.copyFilePath": "Copiar caminho do ficheiro", + "editor.toolbar.moreActions": "Mais ações da nota", "editor.toolbar.openProperties": "Abrir o painel de propriedades", "editor.imageLightbox.title": "Pré-visualização da imagem", "editor.filename.rename": "Mudar o nome do ficheiro", diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json index c6e7a10f..d32dc5eb 100644 --- a/src/lib/locales/ru-RU.json +++ b/src/lib/locales/ru-RU.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "Удалить эту заметку", "editor.toolbar.revealFile": "Показать в Finder", "editor.toolbar.copyFilePath": "Копировать путь к файлу", + "editor.toolbar.moreActions": "Другие действия с заметкой", "editor.toolbar.openProperties": "Открыть панель свойств", "editor.imageLightbox.title": "Предварительный просмотр изображения", "editor.filename.rename": "Переименовать файл", diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index 77715f1e..ead90c3f 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -264,6 +264,7 @@ "editor.toolbar.delete": "Xóa ghi chú này", "editor.toolbar.revealFile": "Hiển thị trong Finder", "editor.toolbar.copyFilePath": "Sao chép đường dẫn tệp", + "editor.toolbar.moreActions": "Thêm hành động ghi chú", "editor.toolbar.openProperties": "Mở bảng thuộc tính", "editor.filename.rename": "Đổi tên tệp", "editor.filename.renameToTitle": "Đổi tên tệp cho khớp với tiêu đề", diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json index fdab96f8..b1c5d16d 100644 --- a/src/lib/locales/zh-CN.json +++ b/src/lib/locales/zh-CN.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "删除这条笔记", "editor.toolbar.revealFile": "在访达中显示", "editor.toolbar.copyFilePath": "复制文件路径", + "editor.toolbar.moreActions": "更多笔记操作", "editor.toolbar.openProperties": "打开属性面板", "editor.imageLightbox.title": "图像预览", "editor.filename.rename": "重命名文件名", diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json index dad615bf..8fdb70fc 100644 --- a/src/lib/locales/zh-TW.json +++ b/src/lib/locales/zh-TW.json @@ -368,6 +368,7 @@ "editor.toolbar.delete": "刪除這條筆記", "editor.toolbar.revealFile": "在訪達中顯示", "editor.toolbar.copyFilePath": "複製檔案路徑", + "editor.toolbar.moreActions": "更多筆記操作", "editor.toolbar.openProperties": "開啟屬性面板", "editor.imageLightbox.title": "圖片預覽", "editor.filename.rename": "重新命名檔名", diff --git a/src/utils/openNoteWindow.test.ts b/src/utils/openNoteWindow.test.ts index b7c516c7..7977ae0f 100644 --- a/src/utils/openNoteWindow.test.ts +++ b/src/utils/openNoteWindow.test.ts @@ -32,6 +32,12 @@ vi.mock('@tauri-apps/api/webviewWindow', () => ({ }, })) +vi.mock('@tauri-apps/api/dpi', () => ({ + LogicalPosition: class MockLogicalPosition { + constructor(public x: number, public y: number) {} + }, +})) + describe('openNoteWindow', () => { beforeEach(() => { vi.clearAllMocks() @@ -73,6 +79,7 @@ describe('openNoteWindow', () => { height: 700, resizable: true, titleBarStyle: 'overlay', + trafficLightPosition: expect.objectContaining({ x: 18, y: 24 }), hiddenTitle: true, decorations: true, }), diff --git a/src/utils/openNoteWindow.ts b/src/utils/openNoteWindow.ts index 2b56c3f7..12710182 100644 --- a/src/utils/openNoteWindow.ts +++ b/src/utils/openNoteWindow.ts @@ -2,6 +2,8 @@ import { isTauri } from '../mock-tauri' import { shouldUseLinuxWindowChrome } from './platform' import { rememberNoteWindowParams } from './windowMode' +const MACOS_TRAFFIC_LIGHT_POSITION = { x: 18, y: 24 } as const + export function buildNoteWindowUrl(notePath: string, vaultPath: string, noteTitle: string, windowLabel?: string): string { const params = new URLSearchParams({ window: 'note', @@ -24,6 +26,7 @@ export function buildNoteWindowUrl(notePath: string, vaultPath: string, noteTitl export async function openNoteInNewWindow(notePath: string, vaultPath: string, noteTitle: string): Promise { if (!isTauri()) return + const { LogicalPosition } = await import('@tauri-apps/api/dpi') const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow') const label = `note-${Date.now()}` rememberNoteWindowParams(label, { notePath, vaultPath, noteTitle }) @@ -35,6 +38,7 @@ export async function openNoteInNewWindow(notePath: string, vaultPath: string, n height: 700, resizable: true, titleBarStyle: 'overlay', + trafficLightPosition: new LogicalPosition(MACOS_TRAFFIC_LIGHT_POSITION.x, MACOS_TRAFFIC_LIGHT_POSITION.y), hiddenTitle: true, decorations: !shouldUseLinuxWindowChrome(), })