diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index f314c21a..d9549780 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -795,6 +795,7 @@ interface Settings { theme_mode: 'light' | 'dark' | null ui_language: AppLocale | null note_width_mode: 'normal' | 'wide' | null + sidebar_type_pluralization_enabled: boolean | null // null = default true default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | 'gemini' | null default_ai_target: string | null // "agent:codex" or "model:/" ai_model_providers: AiModelProvider[] | null @@ -805,7 +806,7 @@ interface Settings { } ``` -Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette light/dark actions both update that same value. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings. Provider defaults and local/API grouping come from the shared `src/shared/aiModelProviderCatalog.json` catalog used by both renderer settings and the Tauri direct-model runtime. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. +Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette light/dark actions both update that same value. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `sidebar_type_pluralization_enabled` is installation-local and defaults to `true`; when false, type rows use exact type names unless the type document defines an explicit `sidebar_label` override. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings. Provider defaults and local/API grouping come from the shared `src/shared/aiModelProviderCatalog.json` catalog used by both renderer settings and the Tauri direct-model runtime. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. ## Telemetry diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ed7f2af3..805c40ea 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -27,6 +27,7 @@ When deciding where to persist a piece of data, ask: **"Would the user want this | Per-note `_width` rich-editor width override | Default rich-editor note width | | Vault-authored `.gitignore` patterns | Whether this installation hides Gitignored files | | Per-vault All Notes note-list column overrides | All Notes PDF/image/unsupported file visibility | +| Type `_sidebar_label` overrides | Whether this installation auto-pluralizes type labels | | Any user-visible customization of how content is organized or displayed | Any machine-specific or credential-type setting | **Rule:** If the information is about *how the content is structured or presented* and the user would expect it to be consistent wherever they open their vault, store it in the vault (frontmatter of the relevant note, using the `_field` underscore convention for system properties). If it's about *this specific installation of the app*, store it in `~/.config/com.tolaria.app/settings.json` or localStorage. @@ -38,6 +39,7 @@ Examples: - ✅ App settings: `zoom: 1.3` (machine-specific preference) - ✅ App settings: `ui_language: "zh-CN"` (installation-specific UI language) - ✅ App settings: `note_width_mode: "wide"` (installation-specific default for notes without an override) +- ✅ App settings: `sidebar_type_pluralization_enabled: false` (installation-specific sidebar label preference) - ✅ App settings: `all_notes_show_images: true` (installation-specific All Notes file-category visibility) ### No hardcoded exceptions diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index 4195d9d6..c0f6148e 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -323,12 +323,12 @@ tolaria/ | File | Why it matters | |------|---------------| -| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, default AI agent). | +| `src/hooks/useSettings.ts` | App settings (telemetry, release channel, theme mode, UI language, auto-sync interval, default note width, sidebar type pluralization, default AI agent). | | `src/lib/releaseChannel.ts` | Normalizes persisted updater-channel values (`stable` default, optional `alpha`). | | `src/lib/appUpdater.ts` | Frontend wrapper for channel-aware updater commands. | | `src/hooks/useMainWindowSizeConstraints.ts` | Derives the main-window minimum width from the visible panes and asks Tauri to grow back to fit wider layouts. | | `src/hooks/useVaultConfig.ts` | Per-vault local UI preferences (zoom, view mode, colors, Inbox columns, explicit organization workflow, AI permission mode). | -| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, sync interval, UI language, default AI agent, and the vault-level explicit organization toggle. | +| `src/components/SettingsPanel.tsx` | Settings UI for telemetry, release channel, sync interval, UI language, content display preferences, default AI agent, and the vault-level explicit organization toggle. | | `src/hooks/useUpdater.ts` | In-app updates using the selected alpha/stable feed. | ## Architecture Patterns diff --git a/lara.lock b/lara.lock index 4bd2b6c9..322385f6 100644 --- a/lara.lock +++ b/lara.lock @@ -134,6 +134,12 @@ files: settings.titles.autoRenameDescription: cc28c28d8817736e658082a2261d9a6e settings.vaultContent.title: 78b883ce9acb9c25e611158a518d9185 settings.vaultContent.description: a53487bf1c7898a1459dc1510e216f7d + settings.noteWidth.default: 66be0f882801fd19468181f31f58dc20 + settings.noteWidth.defaultDescription: 2b5fb6d7dfa2b1da1e47bcac30f58e8d + settings.noteWidth.normal: 960b44c579bc2f6818d2daaf9e4c16f0 + settings.noteWidth.wide: e7c770a61dbdf81ca922ae0260e327c1 + settings.sidebarTypePluralization.label: 1e5bd615b1abbf39129ef241f5cf2249 + settings.sidebarTypePluralization.description: 8d7bb0649fd7e637552429ea91298de8 settings.vaultContent.hideGitignored: 2c07ee6c8bfe746d356f44275d42f90e settings.vaultContent.hideGitignoredDescription: e8b1ddfa416610f9d6eb299fa123a1d6 settings.allNotesVisibility.title: 0e07c3d48b91e1a4c42c1ff3e5751ec3 diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 82748f42..744862d9 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -78,6 +78,7 @@ pub struct Settings { pub theme_mode: Option, pub ui_language: Option, pub note_width_mode: Option, + pub sidebar_type_pluralization_enabled: Option, pub initial_h1_auto_rename_enabled: Option, pub default_ai_agent: Option, pub default_ai_target: Option, @@ -181,6 +182,7 @@ fn normalize_settings(settings: Settings) -> Settings { theme_mode: normalize_theme_mode(settings.theme_mode.as_deref()), ui_language: normalize_ui_language(settings.ui_language.as_deref()), note_width_mode: normalize_note_width_mode(settings.note_width_mode.as_deref()), + sidebar_type_pluralization_enabled: settings.sidebar_type_pluralization_enabled, initial_h1_auto_rename_enabled: settings.initial_h1_auto_rename_enabled, default_ai_agent: normalize_default_ai_agent(settings.default_ai_agent.as_deref()), default_ai_target: normalize_optional_string(settings.default_ai_target), @@ -330,6 +332,7 @@ mod tests { theme_mode: Some("dark".to_string()), ui_language: Some("zh-Hans".to_string()), note_width_mode: Some("wide".to_string()), + sidebar_type_pluralization_enabled: Some(false), initial_h1_auto_rename_enabled: Some(false), default_ai_agent: Some("codex".to_string()), default_ai_target: Some("agent:codex".to_string()), @@ -364,6 +367,7 @@ mod tests { theme_mode: Some("dark".to_string()), ui_language: Some("zh-Hans".to_string()), note_width_mode: Some("wide".to_string()), + sidebar_type_pluralization_enabled: Some(false), initial_h1_auto_rename_enabled: Some(false), default_ai_agent: Some("codex".to_string()), hide_gitignored_files: Some(false), @@ -381,6 +385,7 @@ mod tests { assert_eq!(loaded.theme_mode.as_deref(), Some("dark")); assert_eq!(loaded.ui_language.as_deref(), Some("zh-CN")); assert_eq!(loaded.note_width_mode.as_deref(), Some("wide")); + assert_eq!(loaded.sidebar_type_pluralization_enabled, Some(false)); assert_eq!(loaded.initial_h1_auto_rename_enabled, Some(false)); assert_eq!(loaded.default_ai_agent.as_deref(), Some("codex")); assert_eq!(loaded.hide_gitignored_files, Some(false)); diff --git a/src/App.tsx b/src/App.tsx index dff8f29f..523498b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1693,7 +1693,7 @@ function App() { {sidebarVisible && ( <>
- +
diff --git a/src/components/SettingsPanel.test.tsx b/src/components/SettingsPanel.test.tsx index 5fccb73b..4e97e82e 100644 --- a/src/components/SettingsPanel.test.tsx +++ b/src/components/SettingsPanel.test.tsx @@ -160,6 +160,8 @@ describe('SettingsPanel', () => { autogit_inactive_threshold_seconds: 30, release_channel: null, theme_mode: 'light', + note_width_mode: 'normal', + sidebar_type_pluralization_enabled: true, hide_gitignored_files: true, all_notes_show_pdfs: false, all_notes_show_images: false, @@ -273,6 +275,53 @@ describe('SettingsPanel', () => { expect(screen.getByText('系统(简体中文)')).toBeInTheDocument() }) + it('defaults note width to normal and sidebar type pluralization to enabled', () => { + render( + + ) + + expect(screen.getByTestId('settings-default-note-width')).toHaveAttribute('data-value', 'normal') + expect( + within(screen.getByTestId('settings-sidebar-type-pluralization')).getByRole('switch') + ).toHaveAttribute('aria-checked', 'true') + }) + + it('preserves saved default note width and sidebar type pluralization preferences', () => { + render( + + ) + + expect(screen.getByTestId('settings-default-note-width')).toHaveAttribute('data-value', 'wide') + expect( + within(screen.getByTestId('settings-sidebar-type-pluralization')).getByRole('switch') + ).toHaveAttribute('aria-checked', 'false') + }) + + it('saves default note width and sidebar type pluralization preferences', () => { + render( + + ) + + fireEvent.pointerDown(screen.getByTestId('settings-default-note-width'), { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: 'Wide' })) + fireEvent.click(within(screen.getByTestId('settings-sidebar-type-pluralization')).getByRole('switch')) + fireEvent.click(screen.getByTestId('settings-save')) + + expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ + note_width_mode: 'wide', + sidebar_type_pluralization_enabled: false, + })) + }) + it('keeps the language selector keyboard accessible', () => { render( @@ -580,6 +629,29 @@ describe('SettingsPanel', () => { expect(screen.getByText(/to open settings/)).toBeInTheDocument() }) + it('keeps Tab focus inside the settings panel', () => { + render( + <> + + + + ) + + const backgroundAction = screen.getByTestId('background-action') + const closeButton = screen.getByTitle('Close settings') + const saveButton = screen.getByTestId('settings-save') + + backgroundAction.focus() + fireEvent.keyDown(document, { key: 'Tab' }) + expect(closeButton).toHaveFocus() + + fireEvent.keyDown(document, { key: 'Tab', shiftKey: true }) + expect(saveButton).toHaveFocus() + + fireEvent.keyDown(document, { key: 'Tab' }) + expect(closeButton).toHaveFocus() + }) + it('copies the MCP config from the AI Agents section', () => { const onCopyMcpConfig = vi.fn() render( diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index e660c876..a00cca50 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -46,7 +46,11 @@ import { import { normalizeReleaseChannel, serializeReleaseChannel, type ReleaseChannel } from '../lib/releaseChannel' import { shouldHideGitignoredFiles } from '../lib/gitignoredVisibility' import { trackEvent } from '../lib/telemetry' -import { trackAllNotesVisibilityChanged } from '../lib/productAnalytics' +import { + trackAllNotesVisibilityChanged, + trackDefaultNoteWidthChanged, + trackSidebarTypePluralizationChanged, +} from '../lib/productAnalytics' import { AiProviderSettings } from './AiProviderSettings' import { PrivacySettingsSection } from './PrivacySettingsSection' import { @@ -59,13 +63,16 @@ import { SettingsSwitchRow, } from './SettingsControls' import { SettingsFooter } from './SettingsFooter' +import { VaultContentSettingsSection } from './VaultContentSettingsSection' import { resolveAllNotesFileVisibility, settingsWithAllNotesFileVisibility, type AllNotesFileVisibility, } from '../utils/allNotesFileVisibility' +import { DEFAULT_NOTE_WIDTH_MODE, normalizeNoteWidthMode } from '../utils/noteWidth' import { Button } from './ui/button' import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs' +import type { NoteWidthMode } from '../types' interface SettingsPanelProps { open: boolean @@ -93,6 +100,8 @@ interface SettingsDraft { releaseChannel: ReleaseChannel themeMode: ThemeMode uiLanguage: UiLanguagePreference + defaultNoteWidth: NoteWidthMode + sidebarTypePluralizationEnabled: boolean initialH1AutoRename: boolean hideGitignoredFiles: boolean allNotesFileVisibility: AllNotesFileVisibility @@ -128,6 +137,10 @@ interface SettingsBodyProps { setThemeMode: (value: ThemeMode) => void uiLanguage: UiLanguagePreference setUiLanguage: (value: UiLanguagePreference) => void + defaultNoteWidth: NoteWidthMode + setDefaultNoteWidth: (value: NoteWidthMode) => void + sidebarTypePluralizationEnabled: boolean + setSidebarTypePluralizationEnabled: (value: boolean) => void locale: AppLocale systemLocale: AppLocale initialH1AutoRename: boolean @@ -158,10 +171,66 @@ const SETTINGS_SECTION_IDS = { } as const type Translate = ReturnType +const SETTINGS_FOCUSABLE_SELECTOR = [ + 'a[href]', + 'button:not([disabled])', + 'input:not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + '[tabindex]:not([tabindex="-1"])', +].join(',') + function isSaveShortcut(event: ReactKeyboardEvent): boolean { return event.key === 'Enter' && (event.metaKey || event.ctrlKey) } +function getSettingsFocusableElements(panel: HTMLElement): HTMLElement[] { + return Array.from(panel.querySelectorAll(SETTINGS_FOCUSABLE_SELECTOR)) + .filter((element) => ( + element.tabIndex >= 0 && + element.getAttribute('aria-disabled') !== 'true' && + !element.closest('[hidden], [aria-hidden="true"]') + )) +} + +function focusSettingsBoundary(focusableElements: HTMLElement[], shiftKey: boolean): void { + const targetIndex = shiftKey ? focusableElements.length - 1 : 0 + focusableElements[targetIndex]?.focus() +} + +function isSettingsPanelElement(panel: HTMLElement, activeElement: Element | null): activeElement is HTMLElement { + return activeElement instanceof HTMLElement && panel.contains(activeElement) +} + +function isSettingsFocusBoundary(activeElement: HTMLElement, focusableElements: HTMLElement[], shiftKey: boolean): boolean { + const boundaryIndex = shiftKey ? 0 : focusableElements.length - 1 + return activeElement === focusableElements[boundaryIndex] +} + +function trapSettingsPanelFocus(event: KeyboardEvent, panel: HTMLElement | null): void { + if (event.key !== 'Tab' || !panel) return + + const focusableElements = getSettingsFocusableElements(panel) + if (focusableElements.length === 0) { + event.preventDefault() + panel.focus() + return + } + + const activeElement = document.activeElement + + if (!isSettingsPanelElement(panel, activeElement)) { + event.preventDefault() + focusSettingsBoundary(focusableElements, event.shiftKey) + return + } + + if (isSettingsFocusBoundary(activeElement, focusableElements, event.shiftKey)) { + event.preventDefault() + focusSettingsBoundary(focusableElements, event.shiftKey) + } +} + function createSettingsDraft( settings: Settings, explicitOrganizationEnabled: boolean, @@ -184,6 +253,8 @@ function createSettingsDraft( releaseChannel: normalizeReleaseChannel(settings.release_channel), themeMode: resolveSettingsDraftThemeMode(settings.theme_mode), uiLanguage: settings.ui_language ?? SYSTEM_UI_LANGUAGE, + defaultNoteWidth: normalizeNoteWidthMode(settings.note_width_mode) ?? DEFAULT_NOTE_WIDTH_MODE, + sidebarTypePluralizationEnabled: settings.sidebar_type_pluralization_enabled ?? true, initialH1AutoRename: settings.initial_h1_auto_rename_enabled ?? true, hideGitignoredFiles: shouldHideGitignoredFiles(settings), allNotesFileVisibility: resolveAllNotesFileVisibility(settings), @@ -226,6 +297,8 @@ function buildSettingsFromDraft(settings: Settings, draft: SettingsDraft): Setti release_channel: serializeReleaseChannel(draft.releaseChannel), theme_mode: draft.themeMode, ui_language: serializeUiLanguagePreference(draft.uiLanguage), + note_width_mode: draft.defaultNoteWidth, + sidebar_type_pluralization_enabled: draft.sidebarTypePluralizationEnabled, initial_h1_auto_rename_enabled: draft.initialH1AutoRename, default_ai_agent: draft.defaultAiAgent, default_ai_target: draft.defaultAiTarget, @@ -240,6 +313,18 @@ function trackTelemetryConsentChange(previousAnalytics: boolean, nextAnalytics: if (previousAnalytics && !nextAnalytics) trackEvent('telemetry_opted_out') } +function trackSettingsPreferenceChanges(settings: Settings, draft: SettingsDraft): void { + const previousNoteWidth = normalizeNoteWidthMode(settings.note_width_mode) ?? DEFAULT_NOTE_WIDTH_MODE + if (previousNoteWidth !== draft.defaultNoteWidth) { + trackDefaultNoteWidthChanged(draft.defaultNoteWidth) + } + + const previousPluralization = settings.sidebar_type_pluralization_enabled ?? true + if (previousPluralization !== draft.sidebarTypePluralizationEnabled) { + trackSidebarTypePluralizationChanged(draft.sidebarTypePluralizationEnabled) + } +} + function sanitizePositiveInteger(value: number | null | undefined, fallback: number): number { if (value === null || value === undefined || !Number.isFinite(value) || value < 1) return fallback return Math.round(value) @@ -260,6 +345,17 @@ function useSettingsPanelAutofocus(panelRef: RefObject): }, [panelRef]) } +function useSettingsPanelFocusTrap(panelRef: RefObject): void { + useEffect(() => { + const handleDocumentKeyDown = (event: KeyboardEvent) => { + trapSettingsPanelFocus(event, panelRef.current) + } + + document.addEventListener('keydown', handleDocumentKeyDown, true) + return () => document.removeEventListener('keydown', handleDocumentKeyDown, true) + }, [panelRef]) +} + export function SettingsPanel({ open, settings, @@ -320,6 +416,7 @@ function SettingsPanelInner({ }, [explicitOrganizationEnabled, settings]) useSettingsPanelAutofocus(panelRef) + useSettingsPanelFocusTrap(panelRef) const updateDraft = useCallback( (key: Key, value: SettingsDraft[Key]) => { @@ -347,6 +444,7 @@ function SettingsPanelInner({ const handleSave = useCallback(() => { trackTelemetryConsentChange(settings.analytics_enabled === true, draft.analytics) + trackSettingsPreferenceChanges(settings, draft) onSave(buildSettingsFromDraft(settings, draft)) onSaveExplicitOrganization?.(draft.explicitOrganization) onClose() @@ -482,6 +580,10 @@ function SettingsBodyFromDraft({ setThemeMode={setThemeMode} uiLanguage={draft.uiLanguage} setUiLanguage={(value) => updateDraft('uiLanguage', value)} + defaultNoteWidth={draft.defaultNoteWidth} + setDefaultNoteWidth={(value) => updateDraft('defaultNoteWidth', value)} + sidebarTypePluralizationEnabled={draft.sidebarTypePluralizationEnabled} + setSidebarTypePluralizationEnabled={(value) => updateDraft('sidebarTypePluralizationEnabled', value)} initialH1AutoRename={draft.initialH1AutoRename} setInitialH1AutoRename={(value) => updateDraft('initialH1AutoRename', value)} hideGitignoredFiles={draft.hideGitignoredFiles} @@ -610,6 +712,10 @@ function SettingsSyncAndAppearanceSections({ function SettingsContentSections({ t, + defaultNoteWidth, + setDefaultNoteWidth, + sidebarTypePluralizationEnabled, + setSidebarTypePluralizationEnabled, initialH1AutoRename, setInitialH1AutoRename, hideGitignoredFiles, @@ -621,6 +727,10 @@ function SettingsContentSections({ ) { - const updateAllNotesFileVisibility = (patch: Partial) => { - setAllNotesFileVisibility({ ...allNotesFileVisibility, ...patch }) - } - - return ( - <> - - - - - - - - updateAllNotesFileVisibility({ pdfs: checked })} - testId="settings-all-notes-show-pdfs" - /> - - updateAllNotesFileVisibility({ images: checked })} - testId="settings-all-notes-show-images" - /> - - updateAllNotesFileVisibility({ unsupported: checked })} - testId="settings-all-notes-show-unsupported" - /> - - - ) -} - function buildDefaultAiTargetOptions( aiAgentsStatus: AiAgentsStatus, providers: AiModelProvider[], diff --git a/src/components/Sidebar.test.ts b/src/components/Sidebar.test.ts index 06b224c6..0fde5da4 100644 --- a/src/components/Sidebar.test.ts +++ b/src/components/Sidebar.test.ts @@ -41,6 +41,11 @@ describe('buildSectionGroup', () => { expect(group.Icon).toBe(FileText) }) + it('uses exact type names when automatic pluralization is disabled', () => { + const group = buildSectionGroup('Widget', {}, false) + expect(group.label).toBe('Widget') + }) + it('overrides built-in type icon/color when type entry has custom values', () => { const typeEntryMap: Record = { Project: { ...baseEntry, title: 'Project', isA: 'Type', icon: 'rocket', color: 'green', sidebarLabel: 'My Projects' }, diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 0bfcd761..19783f1e 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -515,6 +515,22 @@ describe('Sidebar', () => { // Recipe has no sidebarLabel → should auto-pluralize to "Recipes" expect(screen.getByText('Recipes')).toBeInTheDocument() }) + + it('uses exact type names when automatic sidebar pluralization is disabled', () => { + render( + {}} + pluralizeTypeLabels={false} + /> + ) + + expect(screen.getByText('Recipe')).toBeInTheDocument() + expect(screen.getByText('Project')).toBeInTheDocument() + expect(screen.queryByText('Recipes')).not.toBeInTheDocument() + expect(screen.queryByText('Projects')).not.toBeInTheDocument() + }) }) describe('type visibility via visible property', () => { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0928f51e..c9a8d96d 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -66,6 +66,7 @@ interface SidebarProps { showInbox?: boolean inboxCount?: number allNotesFileVisibility?: AllNotesFileVisibility + pluralizeTypeLabels?: boolean locale?: AppLocale onCollapse?: () => void onGoBack?: () => void @@ -485,9 +486,10 @@ function useSidebarRuntime({ onDeleteType, onToggleTypeVisibility, allNotesFileVisibility, + pluralizeTypeLabels = true, locale = 'en', }: SidebarProps) { - const { typeEntryMap, allSectionGroups, visibleSections, sectionIds } = useSidebarSections(entries) + const { typeEntryMap, allSectionGroups, visibleSections, sectionIds } = useSidebarSections(entries, pluralizeTypeLabels) const { activeCount, archivedCount } = useEntryCounts(entries, allNotesFileVisibility) const { collapsed: groupCollapsed, toggle: toggleGroup } = useSidebarCollapsed() const typeInteractions = useSidebarTypeInteractions({ diff --git a/src/components/VaultContentSettingsSection.tsx b/src/components/VaultContentSettingsSection.tsx new file mode 100644 index 00000000..81b1678c --- /dev/null +++ b/src/components/VaultContentSettingsSection.tsx @@ -0,0 +1,128 @@ +import type { TranslationKey, TranslationValues } from '../lib/i18n' +import type { NoteWidthMode } from '../types' +import type { AllNotesFileVisibility } from '../utils/allNotesFileVisibility' +import { + SectionHeading, + SelectControl, + SettingsGroup, + SettingsRow, + SettingsSwitchRow, +} from './SettingsControls' + +type Translate = (key: TranslationKey, values?: TranslationValues) => string + +interface VaultContentSettingsSectionProps { + t: Translate + defaultNoteWidth: NoteWidthMode + setDefaultNoteWidth: (value: NoteWidthMode) => void + sidebarTypePluralizationEnabled: boolean + setSidebarTypePluralizationEnabled: (value: boolean) => void + initialH1AutoRename: boolean + setInitialH1AutoRename: (value: boolean) => void + hideGitignoredFiles: boolean + setHideGitignoredFiles: (value: boolean) => void + allNotesFileVisibility: AllNotesFileVisibility + setAllNotesFileVisibility: (value: AllNotesFileVisibility) => void +} + +const NOTE_WIDTH_OPTIONS: readonly NoteWidthMode[] = ['normal', 'wide'] +const NOTE_WIDTH_LABEL_KEYS: Record = { + normal: 'settings.noteWidth.normal', + wide: 'settings.noteWidth.wide', +} + +function buildNoteWidthOptions(t: Translate): Array<{ value: NoteWidthMode; label: string }> { + return NOTE_WIDTH_OPTIONS.map((value) => ({ + value, + label: t(NOTE_WIDTH_LABEL_KEYS[value]), + })) +} + +export function VaultContentSettingsSection({ + t, + defaultNoteWidth, + setDefaultNoteWidth, + sidebarTypePluralizationEnabled, + setSidebarTypePluralizationEnabled, + initialH1AutoRename, + setInitialH1AutoRename, + hideGitignoredFiles, + setHideGitignoredFiles, + allNotesFileVisibility, + setAllNotesFileVisibility, +}: VaultContentSettingsSectionProps) { + const updateAllNotesFileVisibility = (patch: Partial) => { + setAllNotesFileVisibility({ ...allNotesFileVisibility, ...patch }) + } + + return ( + <> + + + + + setDefaultNoteWidth(value as NoteWidthMode)} + options={buildNoteWidthOptions(t)} + testId="settings-default-note-width" + /> + + + + + + + + + updateAllNotesFileVisibility({ pdfs: checked })} + testId="settings-all-notes-show-pdfs" + /> + + updateAllNotesFileVisibility({ images: checked })} + testId="settings-all-notes-show-images" + /> + + updateAllNotesFileVisibility({ unsupported: checked })} + testId="settings-all-notes-show-unsupported" + /> + + + ) +} diff --git a/src/components/sidebar/sidebarHooks.ts b/src/components/sidebar/sidebarHooks.ts index b3a71e21..39dfb652 100644 --- a/src/components/sidebar/sidebarHooks.ts +++ b/src/components/sidebar/sidebarHooks.ts @@ -157,12 +157,12 @@ export function useSidebarInlineRenameInput({ } } -export function useSidebarSections(entries: VaultEntry[]) { +export function useSidebarSections(entries: VaultEntry[], pluralizeTypeLabels = true) { const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const allSectionGroups = useMemo(() => { - const sections = buildDynamicSections(entries, typeEntryMap) + const sections = buildDynamicSections(entries, typeEntryMap, pluralizeTypeLabels) return sortSections(sections, typeEntryMap) - }, [entries, typeEntryMap]) + }, [entries, pluralizeTypeLabels, typeEntryMap]) const visibleSections = useMemo( () => allSectionGroups.filter((group) => typeEntryMap[group.type]?.visible !== false), [allSectionGroups, typeEntryMap], diff --git a/src/hooks/useSettings.test.ts b/src/hooks/useSettings.test.ts index 4996cf4b..968d0c33 100644 --- a/src/hooks/useSettings.test.ts +++ b/src/hooks/useSettings.test.ts @@ -22,6 +22,7 @@ const defaultSettings: Settings = { theme_mode: null, ui_language: null, note_width_mode: null, + sidebar_type_pluralization_enabled: null, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, @@ -45,6 +46,7 @@ const savedSettings: Settings = { theme_mode: null, ui_language: null, note_width_mode: null, + sidebar_type_pluralization_enabled: null, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, @@ -101,6 +103,7 @@ function changedSettings(): Settings { theme_mode: null, ui_language: 'zh-CN', note_width_mode: 'wide', + sidebar_type_pluralization_enabled: false, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index f8e30a3d..6b30c72c 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -46,6 +46,7 @@ const EMPTY_SETTINGS: Settings = { theme_mode: null, ui_language: null, note_width_mode: null, + sidebar_type_pluralization_enabled: null, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, @@ -66,6 +67,7 @@ function normalizeSettings(settings: Settings): Settings { theme_mode: normalizeThemeMode(settings.theme_mode), ui_language: serializeUiLanguagePreference(settings.ui_language), note_width_mode: normalizeNoteWidthMode(settings.note_width_mode), + sidebar_type_pluralization_enabled: settings.sidebar_type_pluralization_enabled ?? null, default_ai_agent: normalizeStoredAiAgent(settings.default_ai_agent), default_ai_target: settings.default_ai_target?.trim() || null, ai_model_providers: aiModelProviders.length > 0 ? aiModelProviders : null, diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json index cc3998fe..db48f88e 100644 --- a/src/lib/locales/de-DE.json +++ b/src/lib/locales/de-DE.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Wenn diese Option aktiviert ist, benennt Tolaria Dateien mit unbenannten Notizen um, sobald die erste H1-Überschrift zu einem echten Titel wird. Deaktivieren Sie diese Option, um den Dateinamen unverändert zu lassen, bis Sie ihn manuell über die Breadcrumb-Leiste umbenennen.", "settings.vaultContent.title": "Vault-Inhalt", "settings.vaultContent.description": "Legen Sie fest, ob Dateien, die der .gitignore-Datei des Vaults entsprechen, in Tolaria angezeigt werden.", + "settings.noteWidth.default": "Standardbreite für Notizen", + "settings.noteWidth.defaultDescription": "Wählen Sie die Rich-Editor-Breite für Notizen ohne explizite Notizenbreite.", + "settings.noteWidth.normal": "Normal", + "settings.noteWidth.wide": "Breit", + "settings.sidebarTypePluralization.label": "Typnamen in der Seitenleiste pluralisieren", + "settings.sidebarTypePluralization.description": "Wenn diese Option aktiviert ist, pluralisiert Tolaria die Bezeichnungen von Typabschnitten automatisch, es sei denn, ein Typ definiert seine eigene Seitenleistenbezeichnung.", "settings.vaultContent.hideGitignored": "Von Git ignorierte Dateien und Ordner ausblenden", "settings.vaultContent.hideGitignoredDescription": "Hält generierte und rein lokale Vault-Dateien von Notizen, der Suche, dem schnellen Öffnen und Ordnern fern.", "settings.allNotesVisibility.title": "Sichtbarkeit in „Alle Notizen“", diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 62178024..98de22be 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "When enabled, Tolaria renames untitled-note files as soon as the first H1 becomes a real title. Turn this off to keep the filename unchanged until you rename it manually from the breadcrumb bar.", "settings.vaultContent.title": "Vault Content", "settings.vaultContent.description": "Control whether files matched by the vault .gitignore appear in Tolaria.", + "settings.noteWidth.default": "Default note width", + "settings.noteWidth.defaultDescription": "Choose the rich-editor width for notes without an explicit note width.", + "settings.noteWidth.normal": "Normal", + "settings.noteWidth.wide": "Wide", + "settings.sidebarTypePluralization.label": "Pluralize type names in sidebar", + "settings.sidebarTypePluralization.description": "When enabled, Tolaria automatically pluralizes type section labels unless a type defines its own sidebar label.", "settings.vaultContent.hideGitignored": "Hide files and folders ignored by Git", "settings.vaultContent.hideGitignoredDescription": "Keeps generated and local-only vault files out of notes, search, quick open, and folders.", "settings.allNotesVisibility.title": "All Notes visibility", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 1c100da6..1fa5a276 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Cuando esta opción está activada, Tolaria cambia el nombre de los archivos de notas sin título tan pronto como el primer H1 se convierte en un título real. Desactive esta opción para que el nombre del archivo no se modifique hasta que lo cambie manualmente desde la barra de navegación.", "settings.vaultContent.title": "Contenido del repositorio", "settings.vaultContent.description": "Controle si los archivos que coinciden con el archivo .gitignore del repositorio aparecen en Tolaria.", + "settings.noteWidth.default": "Ancho predeterminado de la nota", + "settings.noteWidth.defaultDescription": "Elija el ancho del editor enriquecido para las notas que no tengan un ancho de nota explícito.", + "settings.noteWidth.normal": "Normal", + "settings.noteWidth.wide": "Ancho", + "settings.sidebarTypePluralization.label": "Pluralizar los nombres de los tipos en la barra lateral", + "settings.sidebarTypePluralization.description": "Cuando esta opción está activada, Tolaria pluraliza automáticamente las etiquetas de las secciones de tipos, a menos que un tipo defina su propia etiqueta de barra lateral.", "settings.vaultContent.hideGitignored": "Ocultar archivos y carpetas ignorados por Git", "settings.vaultContent.hideGitignoredDescription": "Mantiene los archivos de la caja fuerte generados y solo locales fuera de las notas, la búsqueda, la apertura rápida y las carpetas.", "settings.allNotesVisibility.title": "Visibilidad de Todas las notas", diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json index 33936a52..79ffcfa0 100644 --- a/src/lib/locales/es-ES.json +++ b/src/lib/locales/es-ES.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Cuando esta opción está activada, Tolaria cambia el nombre de los archivos de notas sin título en cuanto el primer H1 se convierte en un título real. Desactive esta opción para que el nombre del archivo no cambie hasta que lo renombre manualmente desde la barra de navegación.", "settings.vaultContent.title": "Contenido del repositorio", "settings.vaultContent.description": "Controla si los archivos que coinciden con el .gitignore del repositorio aparecen en Tolaria.", + "settings.noteWidth.default": "Ancho predeterminado de la nota", + "settings.noteWidth.defaultDescription": "Elija el ancho del editor enriquecido para las notas que no tengan un ancho de nota explícito.", + "settings.noteWidth.normal": "Normal", + "settings.noteWidth.wide": "Ancho", + "settings.sidebarTypePluralization.label": "Pluralizar los nombres de los tipos en la barra lateral", + "settings.sidebarTypePluralization.description": "Cuando esta opción está activada, Tolaria pluraliza automáticamente las etiquetas de las secciones de tipos, a menos que un tipo defina su propia etiqueta de barra lateral.", "settings.vaultContent.hideGitignored": "Ocultar archivos y carpetas ignorados por Git", "settings.vaultContent.hideGitignoredDescription": "Evita que los archivos del almacén generados y solo locales aparezcan en las notas, la búsqueda, la apertura rápida y las carpetas.", "settings.allNotesVisibility.title": "Visibilidad de Todas las notas", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index cb8763c0..e4c282a5 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Lorsque cette option est activée, Tolaria renomme les fichiers de notes sans titre dès que le premier H1 devient un véritable titre. Désactivez cette option pour que le nom de fichier reste inchangé jusqu'à ce que vous le renommiez manuellement depuis la barre de navigation.", "settings.vaultContent.title": "Contenu du coffre-fort", "settings.vaultContent.description": "Contrôler si les fichiers correspondants au fichier .gitignore du coffre-fort apparaissent dans Tolaria.", + "settings.noteWidth.default": "Largeur de note par défaut", + "settings.noteWidth.defaultDescription": "Choisissez la largeur de l'éditeur enrichi pour les notes sans largeur de note explicitement définie.", + "settings.noteWidth.normal": "Normale", + "settings.noteWidth.wide": "Large", + "settings.sidebarTypePluralization.label": "Pluraliser les noms de types dans la barre latérale", + "settings.sidebarTypePluralization.description": "Lorsque cette option est activée, Tolaria met automatiquement au pluriel les libellés des sections de type, sauf si un type définit son propre libellé de barre latérale.", "settings.vaultContent.hideGitignored": "Masquer les fichiers et les dossiers ignorés par Git", "settings.vaultContent.hideGitignoredDescription": "Exclut les fichiers du coffre-fort générés et locaux des notes, de la recherche, de l'ouverture rapide et des dossiers.", "settings.allNotesVisibility.title": "Visibilité de Toutes les notes", diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json index 09c9e664..78fa8fad 100644 --- a/src/lib/locales/it-IT.json +++ b/src/lib/locales/it-IT.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Se questa opzione è abilitata, Tolaria rinomina i file delle note senza titolo non appena il primo H1 diventa un titolo effettivo. Disattiva questa opzione per mantenere invariato il nome del file finché non lo rinomini manualmente dalla barra di navigazione.", "settings.vaultContent.title": "Contenuto del vault", "settings.vaultContent.description": "Controlla se i file corrispondenti al file .gitignore del vault vengono visualizzati in Tolaria.", + "settings.noteWidth.default": "Larghezza predefinita delle note", + "settings.noteWidth.defaultDescription": "Scegli la larghezza del rich editor per le note che non hanno una larghezza specificata.", + "settings.noteWidth.normal": "Normale", + "settings.noteWidth.wide": "Ampia", + "settings.sidebarTypePluralization.label": "Pluralizza i nomi dei tipi nella barra laterale", + "settings.sidebarTypePluralization.description": "Se l'opzione è abilitata, Tolaria pluralizza automaticamente le etichette delle sezioni dei tipi, a meno che un tipo non definisca la propria etichetta nella barra laterale.", "settings.vaultContent.hideGitignored": "Nascondi file e cartelle ignorati da Git", "settings.vaultContent.hideGitignoredDescription": "Esclude i file del vault generati e quelli solo locali dalle note, dalla ricerca, dall'apertura rapida e dalle cartelle.", "settings.allNotesVisibility.title": "Visibilità di All Notes", diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json index 87d322ce..3eb2d05d 100644 --- a/src/lib/locales/ja-JP.json +++ b/src/lib/locales/ja-JP.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "この設定を有効にすると、Tolariaは最初のH1が実際のタイトルになった時点で、タイトルのないノートファイルの名前を変更します。この設定をオフにすると、ブレッドクラムバーから手動で名前を変更するまで、ファイル名は変更されません。", "settings.vaultContent.title": "Vaultのコンテンツ", "settings.vaultContent.description": "Vaultの.gitignoreで一致したファイルをTolariaに表示するかどうかを制御します。", + "settings.noteWidth.default": "デフォルトのノート幅", + "settings.noteWidth.defaultDescription": "明示的なノート幅が指定されていないノートの場合、リッチエディターの幅を選択します。", + "settings.noteWidth.normal": "通常", + "settings.noteWidth.wide": "ワイド", + "settings.sidebarTypePluralization.label": "サイドバーでのタイプ名の複数形表示", + "settings.sidebarTypePluralization.description": "この設定を有効にすると、Tolaria は、タイプが独自のサイドバーラベルを定義していない限り、タイプセクションのラベルを自動的に複数形にします。", "settings.vaultContent.hideGitignored": "Gitによって無視されるファイルとフォルダーを非表示にする", "settings.vaultContent.hideGitignoredDescription": "生成されたVaultファイルとローカル専用のVaultファイルを、ノート、検索、クイックオープン、フォルダーから除外します。", "settings.allNotesVisibility.title": "「すべてのノート」の表示設定", diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json index 7efa5751..3c0ea19d 100644 --- a/src/lib/locales/ko-KR.json +++ b/src/lib/locales/ko-KR.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "이 옵션을 활성화하면 Tolaria는 첫 번째 H1이 실제 제목이 되는 즉시 제목 없는 노트 파일의 이름을 변경합니다. 이 옵션을 끄면 브레드크럼 바에서 수동으로 파일 이름을 변경할 때까지 파일 이름이 변경되지 않습니다.", "settings.vaultContent.title": "Vault 콘텐츠", "settings.vaultContent.description": "Vault .gitignore와 일치하는 파일이 Tolaria에 표시되는지 여부를 제어합니다.", + "settings.noteWidth.default": "기본 노트 너비", + "settings.noteWidth.defaultDescription": "명시적인 노트 너비가 없는 노트의 경우 리치 편집기 너비를 선택하세요.", + "settings.noteWidth.normal": "일반", + "settings.noteWidth.wide": "넓음", + "settings.sidebarTypePluralization.label": "사이드바에서 타입 이름 복수화", + "settings.sidebarTypePluralization.description": "이 옵션을 활성화하면, Tolaria는 타입이 자체 사이드바 라벨을 정의하지 않는 한 타입 섹션 라벨을 자동으로 복수로 변형합니다.", "settings.vaultContent.hideGitignored": "Git에서 무시하는 파일 및 폴더 숨기기", "settings.vaultContent.hideGitignoredDescription": "생성된 Vault 파일과 로컬 전용 Vault 파일을 노트, 검색, 빠른 열기 기능, 폴더에서 제외합니다.", "settings.allNotesVisibility.title": "모든 노트 표시 여부", diff --git a/src/lib/locales/pl-PL.json b/src/lib/locales/pl-PL.json index 3f0aba9b..9889eff7 100644 --- a/src/lib/locales/pl-PL.json +++ b/src/lib/locales/pl-PL.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Po włączeniu Tolaria zmienia nazwy plików notatek bez tytułu, gdy tylko pierwszy H1 stanie się prawdziwym tytułem. Wyłącz, aby zachować nazwę pliku bez zmian do ręcznej zmiany z paska ścieżki.", "settings.vaultContent.title": "Zawartość sejfu", "settings.vaultContent.description": "Kontroluj, czy pliki pasujące do .gitignore sejfu pojawiają się w Tolaria.", + "settings.noteWidth.default": "Domyślna szerokość notatki", + "settings.noteWidth.defaultDescription": "Wybierz szerokość edytora Rich dla notatek, które nie mają wyraźnie określonej szerokości.", + "settings.noteWidth.normal": "Normalna", + "settings.noteWidth.wide": "Szeroka", + "settings.sidebarTypePluralization.label": "Dodawaj liczbę mnogą do nazw typów na pasku bocznym", + "settings.sidebarTypePluralization.description": "Gdy ta opcja jest włączona, Tolaria automatycznie dodaje liczbę mnogą do etykiet sekcji typów, chyba że typ ma zdefiniowaną własną etykietę paska bocznego.", "settings.vaultContent.hideGitignored": "Ukryj pliki i foldery ignorowane przez Git", "settings.vaultContent.hideGitignoredDescription": "Ukrywa wygenerowane i lokalne pliki sejfu z notatek, wyszukiwania, szybkiego otwierania i folderów.", "settings.allNotesVisibility.title": "Widoczność Wszystkich notatek", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index 5a7b7601..ef0013ab 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Quando ativada, a Tolaria renomeia os arquivos de anotações sem título assim que o primeiro H1 se torna um título de fato. Desative esta opção para manter o nome do arquivo inalterado até que você o renomeie manualmente na barra de navegação.", "settings.vaultContent.title": "Conteúdo do cofre", "settings.vaultContent.description": "Controle se os arquivos correspondentes ao .gitignore do cofre aparecem no Tolaria.", + "settings.noteWidth.default": "Largura padrão da nota", + "settings.noteWidth.defaultDescription": "Escolha a largura do editor rich para notas que não tenham uma largura de nota explícita.", + "settings.noteWidth.normal": "Normal", + "settings.noteWidth.wide": "Largo", + "settings.sidebarTypePluralization.label": "Pluralizar nomes de tipos na barra lateral", + "settings.sidebarTypePluralization.description": "Quando ativada, a Tolaria pluraliza automaticamente os rótulos das seções de tipo, a menos que um tipo defina seu próprio rótulo de barra lateral.", "settings.vaultContent.hideGitignored": "Ocultar arquivos e pastas ignorados pelo Git", "settings.vaultContent.hideGitignoredDescription": "Mantém os arquivos do cofre gerados e apenas locais fora das anotações, da pesquisa, da abertura rápida e das pastas.", "settings.allNotesVisibility.title": "Visibilidade de Todas as anotações", diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json index c1173bb5..13ec5e61 100644 --- a/src/lib/locales/pt-PT.json +++ b/src/lib/locales/pt-PT.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Quando esta opção está ativada, a Tolaria muda o nome dos ficheiros de notas sem título assim que o primeiro H1 se torna um título verdadeiro. Desative esta opção para manter o nome do ficheiro inalterado até o renomear manualmente na barra de navegação.", "settings.vaultContent.title": "Conteúdo do cofre", "settings.vaultContent.description": "Controlar se os ficheiros correspondentes ao .gitignore do cofre aparecem na Tolaria.", + "settings.noteWidth.default": "Largura predefinida da nota", + "settings.noteWidth.defaultDescription": "Escolha a largura do editor rich para notas sem uma largura de nota explícita.", + "settings.noteWidth.normal": "Normal", + "settings.noteWidth.wide": "Larga", + "settings.sidebarTypePluralization.label": "Pluralizar os nomes dos tipos na barra lateral", + "settings.sidebarTypePluralization.description": "Quando ativada, a Tolaria pluraliza automaticamente as etiquetas das secções de tipos, a menos que um tipo defina a sua própria etiqueta de barra lateral.", "settings.vaultContent.hideGitignored": "Ocultar ficheiros e pastas ignorados pelo Git", "settings.vaultContent.hideGitignoredDescription": "Mantém os ficheiros do cofre gerados e apenas locais fora das notas, da pesquisa, da abertura rápida e das pastas.", "settings.allNotesVisibility.title": "Visibilidade de Todas as notas", diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json index 21dfd9fa..87e3bcd0 100644 --- a/src/lib/locales/ru-RU.json +++ b/src/lib/locales/ru-RU.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Если эта функция включена, Tolaria переименовывает файлы заметок без названия, как только первый заголовок H1 становится настоящим заголовком. Отключите эту опцию, чтобы имя файла оставалось неизменным, пока вы не переименуете его вручную в строке навигации.", "settings.vaultContent.title": "Содержимое хранилища", "settings.vaultContent.description": "Управляйте тем, будут ли отображаться в Tolaria файлы, соответствующие списку .gitignore хранилища.", + "settings.noteWidth.default": "Ширина заметки по умолчанию", + "settings.noteWidth.defaultDescription": "Выберите ширину rich-редактора для заметок, для которых не указана явная ширина.", + "settings.noteWidth.normal": "Нормальная", + "settings.noteWidth.wide": "Широкий", + "settings.sidebarTypePluralization.label": "Использовать множественное число для названий типов на боковой панели", + "settings.sidebarTypePluralization.description": "Если эта опция включена, Tolaria автоматически добавляет множественное число к меткам разделов типов, за исключением случаев, когда тип определяет собственную метку на боковой панели.", "settings.vaultContent.hideGitignored": "Скрыть файлы и папки, игнорируемые Git", "settings.vaultContent.hideGitignoredDescription": "Не допускает включения сгенерированных и только локальных файлов хранилища в заметки, поиск, быстрое открытие и папки.", "settings.allNotesVisibility.title": "Видимость в разделе «Все заметки»", diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index 155e2ad5..d1e9be2f 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "Khi bật, Tolaria sẽ đổi tên tệp ghi chú chưa đặt tên ngay khi H1 đầu tiên trở thành tiêu đề thật. Tắt tùy chọn này nếu bạn muốn giữ nguyên tên tệp cho đến khi tự đổi thủ công từ thanh breadcrumb.", "settings.vaultContent.title": "Nội dung kho", "settings.vaultContent.description": "Kiểm soát việc các tệp khớp với .gitignore của kho có xuất hiện trong Tolaria hay không.", + "settings.noteWidth.default": "Chiều rộng ghi chú mặc định", + "settings.noteWidth.defaultDescription": "Chọn độ rộng trình soạn thảo định dạng cho các ghi chú không có độ rộng ghi chú được chỉ định rõ ràng.", + "settings.noteWidth.normal": "Bình thường", + "settings.noteWidth.wide": "Rộng", + "settings.sidebarTypePluralization.label": "Sử dụng dạng số nhiều cho tên loại trong thanh bên", + "settings.sidebarTypePluralization.description": "Khi được bật, Tolaria sẽ tự động biến các nhãn phần loại thành dạng số nhiều, trừ khi một loại xác định nhãn thanh bên của riêng nó.", "settings.vaultContent.hideGitignored": "Ẩn các tệp và thư mục bị Git bỏ qua", "settings.vaultContent.hideGitignoredDescription": "Giữ các tệp kho được tạo sinh và chỉ dùng cục bộ ra khỏi ghi chú, tìm kiếm, mở nhanh và danh sách thư mục.", "settings.allNotesVisibility.title": "All Notes visibility", diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json index 8c0d476f..91838a1c 100644 --- a/src/lib/locales/zh-CN.json +++ b/src/lib/locales/zh-CN.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "启用后,只要第一个 H1 成为真实标题,Tolaria 就会重命名 untitled-note 文件。关闭后,文件名会保持不变,直到你从面包屑栏手动重命名。", "settings.vaultContent.title": "保管库内容", "settings.vaultContent.description": "控制与 Vault .gitignore 文件匹配的文件是否显示在 Tolaria 中。", + "settings.noteWidth.default": "默认笔记宽度", + "settings.noteWidth.defaultDescription": "对于没有明确指定笔记宽度的笔记,选择富文本编辑器宽度。", + "settings.noteWidth.normal": "正常", + "settings.noteWidth.wide": "宽", + "settings.sidebarTypePluralization.label": "在侧边栏中将类型名称复数化", + "settings.sidebarTypePluralization.description": "启用此选项后,除非类型自行定义了侧边栏标签,否则 Tolaria 会自动将类型部分的标签转换为复数形式。", "settings.vaultContent.hideGitignored": "隐藏 Git 忽略的文件和文件夹", "settings.vaultContent.hideGitignoredDescription": "将生成的 Vault 文件和仅限本地的 Vault 文件排除在笔记、搜索、快速打开和文件夹之外。", "settings.allNotesVisibility.title": "所有笔记的可见性", diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json index e2c9a811..bd1c4b6c 100644 --- a/src/lib/locales/zh-TW.json +++ b/src/lib/locales/zh-TW.json @@ -132,6 +132,12 @@ "settings.titles.autoRenameDescription": "啟用後,只要第一個 H1 成為真實標題,Tolaria 就會重新命名 untitled-note 檔案。關閉後,檔名會保持不變,直到你從麵包屑欄手動重新命名。", "settings.vaultContent.title": "保管庫內容", "settings.vaultContent.description": "控制與 Vault .gitignore 相符的檔案是否顯示在 Tolaria 中。", + "settings.noteWidth.default": "預設筆記寬度", + "settings.noteWidth.defaultDescription": "對於沒有明確指定筆記寬度的筆記,請選擇 Rich 編輯器寬度。", + "settings.noteWidth.normal": "正常", + "settings.noteWidth.wide": "寬版", + "settings.sidebarTypePluralization.label": "在側邊欄中將類型名稱加以複數化", + "settings.sidebarTypePluralization.description": "啟用此功能後,除非類型自行定義其側邊欄標籤,否則 Tolaria 會自動將類型區段標籤複數化。", "settings.vaultContent.hideGitignored": "隱藏 Git 忽略的檔案和資料夾", "settings.vaultContent.hideGitignoredDescription": "將已產生的 Vault 檔案和僅限本機的 Vault 檔案排除在筆記、搜尋、快速開啟和資料夾之外。", "settings.allNotesVisibility.title": "「所有筆記」可見性", diff --git a/src/lib/productAnalytics.ts b/src/lib/productAnalytics.ts index f9cbdfa5..839792c5 100644 --- a/src/lib/productAnalytics.ts +++ b/src/lib/productAnalytics.ts @@ -3,6 +3,7 @@ import type { AiAgentPermissionMode } from './aiAgentPermissionMode' import { trackEvent } from './telemetry' import type { AllNotesFileVisibility } from '../utils/allNotesFileVisibility' import type { FilePreviewKind } from '../utils/filePreview' +import type { NoteWidthMode } from '../types' type TrackedPreviewKind = FilePreviewKind | 'unsupported' type FilePreviewAction = 'copy_path' | 'open_external' | 'reveal' @@ -52,6 +53,16 @@ export function trackAllNotesVisibilityChanged( } } +export function trackDefaultNoteWidthChanged(mode: NoteWidthMode): void { + trackEvent('note_width_default_changed', { mode }) +} + +export function trackSidebarTypePluralizationChanged(enabled: boolean): void { + trackEvent('sidebar_type_pluralization_changed', { + enabled: numericFlag(enabled), + }) +} + export function trackInlineImageLightboxOpened(): void { trackEvent('inline_image_lightbox_opened') } diff --git a/src/mock-tauri/mock-handlers.coverage.test.ts b/src/mock-tauri/mock-handlers.coverage.test.ts index a6a56f88..5ef9df12 100644 --- a/src/mock-tauri/mock-handlers.coverage.test.ts +++ b/src/mock-tauri/mock-handlers.coverage.test.ts @@ -169,6 +169,8 @@ describe('mockHandlers coverage', () => { anonymous_id: 'anon-1', release_channel: 'alpha', theme_mode: null, + note_width_mode: null, + sidebar_type_pluralization_enabled: null, ui_language: 'zh-CN', default_ai_agent: 'codex', }) diff --git a/src/mock-tauri/mock-handlers.ts b/src/mock-tauri/mock-handlers.ts index af56b35f..9d1929e4 100644 --- a/src/mock-tauri/mock-handlers.ts +++ b/src/mock-tauri/mock-handlers.ts @@ -113,6 +113,8 @@ let mockSettings: Settings = { release_channel: null, theme_mode: null, ui_language: null, + note_width_mode: null, + sidebar_type_pluralization_enabled: null, default_ai_agent: 'claude_code', } @@ -424,6 +426,8 @@ export const mockHandlers: Record any> = { release_channel: s.release_channel, theme_mode: s.theme_mode ?? null, ui_language: s.ui_language ?? null, + note_width_mode: s.note_width_mode ?? null, + sidebar_type_pluralization_enabled: s.sidebar_type_pluralization_enabled ?? null, default_ai_agent: s.default_ai_agent ?? null, } return null diff --git a/src/types.ts b/src/types.ts index 952961e5..4ffe140b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,6 +98,7 @@ export interface Settings { theme_mode?: ThemeMode | null ui_language?: AppLocale | null note_width_mode?: NoteWidthMode | null + sidebar_type_pluralization_enabled?: boolean | null initial_h1_auto_rename_enabled?: boolean | null default_ai_agent?: AiAgentId | null default_ai_target?: string | null diff --git a/src/utils/sidebarSections.ts b/src/utils/sidebarSections.ts index 3b0061ee..6290e22e 100644 --- a/src/utils/sidebarSections.ts +++ b/src/utils/sidebarSections.ts @@ -80,16 +80,27 @@ export function collectActiveTypes(entries: VaultEntry[]): Set { return types } -function resolveLabel(type: string, typeEntry: VaultEntry | undefined, builtIn: SectionGroup | undefined): string { - return typeEntry?.sidebarLabel || builtIn?.label || pluralizeType(type) +function resolveLabel( + type: string, + typeEntry: VaultEntry | undefined, + builtIn: SectionGroup | undefined, + pluralizeTypeLabel: boolean, +): string { + if (typeEntry?.sidebarLabel) return typeEntry.sidebarLabel + if (!pluralizeTypeLabel) return type + return builtIn?.label || pluralizeType(type) } /** Build a single SectionGroup for a type, using built-in metadata or Type entry for icon/label */ -export function buildSectionGroup(type: string, typeEntryMap: Record): SectionGroup { +export function buildSectionGroup( + type: string, + typeEntryMap: Record, + pluralizeTypeLabel = true, +): SectionGroup { const builtIn = BUILT_IN_TYPE_MAP.get(type) const typeEntry = typeEntryMap[type] const customColor = typeEntry?.color ?? null - const label = resolveLabel(type, typeEntry, builtIn) + const label = resolveLabel(type, typeEntry, builtIn, pluralizeTypeLabel) const icon = resolveIcon(typeEntry?.icon ?? null) if (builtIn) { return { ...builtIn, label, Icon: typeEntry?.icon ? icon : builtIn.Icon, customColor } @@ -98,7 +109,11 @@ export function buildSectionGroup(type: string, typeEntryMap: Record): SectionGroup[] { +export function buildDynamicSections( + entries: VaultEntry[], + typeEntryMap: Record, + pluralizeTypeLabels = true, +): SectionGroup[] { const activeTypes = new Map() for (const type of collectActiveTypes(entries)) { addSectionType(activeTypes, type, typeEntryMap) @@ -109,7 +124,7 @@ export function buildDynamicSections(entries: VaultEntry[], typeEntryMap: Record } return Array.from(activeTypes.values()) .filter((type) => shouldIncludeSectionType(type, typeEntryMap)) - .map((type) => buildSectionGroup(type, typeEntryMap)) + .map((type) => buildSectionGroup(type, typeEntryMap, pluralizeTypeLabels)) } export function sortSections(groups: SectionGroup[], typeEntryMap: Record): SectionGroup[] {