zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Toolbar customization

Every toolbar, frame header and provider header-action row in the app lets the user hide individual buttons (the Note Tray utility bar pins and orders them instead; see The Note Tray utility bar). One registry describes which buttons are hideable, one settings key stores the choices, and one hook renders both the right-click customize menu and the Settings → Toolbars section. This page is the contributor-facing contract (epic #6562 topic 8); the end-user walkthrough is the Japanese manual's 設定項目リファレンス.

Where the code lives

PieceFile
Settings shape (AppSettings.toolbars), ToolbarSurfaceIdpackages/app-defaults/src/types.ts
Surface registry, read/write helperspackages/app-defaults/src/toolbar-surfaces.ts
Default ({} — everything visible)packages/app-defaults/src/defaults.ts (defaultToolbarSettings)
Validation + the menu-bar-right discardpackages/app-defaults/src/validate-settings.ts
Runtime hooktauri-app/renderer/hooks/use-toolbar-visibility.ts
Right-click menu wrappertauri-app/renderer/components/toolbar-customize-menu.tsx
Settings section (Toolbars)packages/settings-sections/src/sections/toolbars-settings.tsx
Section registration + the open-to-section listenertauri-app/renderer/components/settings/settings-section-registry.ts, settings/settings-dialog.tsx
Palette command / eventtauri-app/renderer/data/app-commands.tsx (open-toolbar-settings, OPEN_TOOLBAR_SETTINGS_EVENT)
Checkbox menu-row primitivepackages/ui-components/src/context-menu.tsx

Settings shape

toolbars: Partial<Record<ToolbarSurfaceId, { hidden: string[] }>>;
  • A missing surface key means everything on that surface is visible, so the default is the empty object {}.

  • withToolbarItemVisible deletes a surface's entry once nothing on it is hidden, keeping the persisted document minimal.

  • toolbars is not part of OverridableSettings: toolbar visibility is a workspace-wide preference, not a per-device one.

  • There is no migration from the pre-#6562 layout.noteTrayToolbar.show* booleans — the app is pre-release, so those keys were deleted and the Note Tray bar starts fully visible again.

The surface registry

TOOLBAR_SURFACES in packages/app-defaults/src/toolbar-surfaces.ts maps every ToolbarSurfaceId to a display label and its list of hideable items:

"efe-tab-strip": {
  label: "External File Editor Tab Strip",
  items: [
    { id: "tab-bar-position", label: "Tab bar position" },
    { id: "view-mode", label: "View mode" },
  ],
},

HEADER_ACTIONS_SURFACE_BY_PROVIDER maps a ViewProvider.id to the *-header-actions surface its HeaderActions component renders, so the frame menu can fold the provider's own actions into its Header buttons submenu. The core.inbox mapping and the note-tray-header-actions surface no longer exist: History, Pile View and Find similar moved onto the utility bar.

Rule: only hideable items are registered

There is no "locked" or "required" flag. A control that must never disappear is simply absent from the registry — close, tabs, navigation, and any escape hatch out of a mode. Live examples:

  • dashboard-header-actions registers only dashboard-reset; Edit widgets is unregistered, because it is the only way out of edit mode.

  • frame-chrome-controls registers collapse, zoom, pop-out and empty — not the close button.

If a reviewer asks "can the user hide themselves into a corner here?", the answer is to drop the item from items, not to add a flag.

Rule: active state beats preference

useToolbarVisibility(surfaceId, { forceVisible }) forces ids on screen regardless of the stored preference, for an item whose current state would otherwise become undiscoverable. This affects rendering only — the menu checkbox keeps showing the stored preference, so toggling it stays meaningful and the item goes back to hidden once its state goes inactive.

  • kanban-header-actions force-shows kanban-filter while a quick filter is active (kanban-board-provider.tsx).

  • frame-chrome-controls force-shows zoom while the frame is zoomed, since a zoomed frame covers everything else and would otherwise strand the user (isChromeControlVisible in packages/frameset/src/frame-chrome.tsx).

The menu-bar.rightIcons storage route

menu-bar-right is the one surface whose visibility does not live in AppSettings.toolbars. menuBar.rightIcons[] already carries both visible and display order, and two representations of the same fact would eventually disagree, so the registry entry declares:

"menu-bar-right": { label: "Menu Bar (Right)", storage: "menuBar.rightIcons", items: … },

Consequences to keep in mind:

  • Read and write only through isToolbarItemVisible / withToolbarItemVisible. They route by storage, so a caller never needs to know which key backs a surface.

  • validateSettings discards any toolbars["menu-bar-right"] entry it finds, so a hand-edited settings file cannot create a second source of truth.

  • The Toolbars settings section renders this surface as a pointer to the Menu Bar section instead of a second editor, and the shared settings-search metadata skips it (its entries already exist under the menu-bar section key).

The Note Tray utility bar

note-tray-utility is the second storage-routed surface. Its items are the pinnable utilities, and it stores them as the ordered list AppSettings.noteTrayUtility.pinned instead of a hidden list. Pinning appends; unpinning keeps the remaining order. An unpinned utility is not removed: it stays in the bar's permanent More menu. note-tray-bar keeps only the tab-rail controls (new-note, position, expand-toggle, note-view).

PieceFile
Settings, defaults, validation, width helperspackages/app-defaults/src/note-tray-utility.ts
One action model feeding the pinned buttons, More, per-action menus and Customizetauri-app/renderer/components/note-tray-utility/utility-model.ts
Bar component (role="toolbar", arrow / Home / End keys, overflow measurement)tauri-app/renderer/components/note-tray-utility/
Persistencetauri-app/renderer/hooks/use-note-tray-utility-settings.ts (updateNoteTrayUtility)
Settings section grouppackages/settings-sections/src/sections/note-tray-utility-settings.tsx

Contracts worth knowing:

  • Placement. Horizontal sits directly under the tray scope row, above the tabs. Vertical is a sibling to the right of the content inside an always-present note-tray-body row, so orientation, label and width changes never remount the editor. The tab rail position is independent.

  • Defaults. Horizontal, labels on, pinned To AI, Sticker, Preview, Pile View.

  • Vertical width per label mode. 144 px (96–240) with labels, 48 px (48–160) icons only, step 8. Rendered width is max(40px × s, min(W px × s, 100% − 160px × s)): the display scale is applied once, and the render clamp is never written back. The settings UI edits one range and number input for the active mode; an invalid typed width stays an editable draft.

  • Ellipsis. Labels use min-w-0 overflow-hidden text-ellipsis whitespace-nowrap; icons are shrink-0.

  • More. Groups Current note, Organize, Tray; unpinned and responsively overflowed actions appear exactly once. Overflow is measured with a ResizeObserver and state is set only on change. Disabled actions stay listed with a real reason (aria-disabled, aria-description, tooltip, menu suffix). Child actions are flattened under a parent state label (for example published Publish becomes Publish again and Unpublish). The utility menus opt into ContextMenu's wrapLabels so long labels wrap.

  • Customize. Right-click the bar or open More → Customize toolbar…. One shared ContextMenu instance serves it (new BarMenuTarget kinds). Entries: Move earlier / Move later, pin checkboxes from the registry, Show labels, Horizontal / Vertical, Reset toolbar, Toolbar settings….

  • Persistence. updateNoteTrayUtility reads, derives and writes inside runSerializedSettingsWrite from fresh base settings, rejects on a failed save (toast), and preserves unrelated preferences. The Settings dialog omits an untouched noteTrayUtility draft from Save & Reload, the same narrowing as toolbars and menuBar (extend the list above only for such popout-writable keys).

  • Commands. Note Tray Utility Bar: Show/Hide Labels and Note Tray Utility Bar: Horizontal/Vertical in the command palette. The help entry is note-tray-utility-bar.

  • Compact mobile hides the bar together with the editor chrome (data-mobile-editor-chrome).

Runtime: the two hooks

useToolbarVisibility(surfaceId, options) returns { isVisible, setVisible, hiddenIds, menuItems }.

  • isVisible(itemId) is what a surface guards its buttons with.

  • setVisible persists immediately. The read-modify-write runs inside runSerializedSettingsWrite against freshly read settings — never a render-time closure — so two quick toggles both land. saveScoped cannot be used: it re-enqueues onto the same queue (deadlock), and its deep merge can never delete a toolbars[surfaceId] entry that the patch drops.

  • hiddenIds is reference-stable while its contents are unchanged, because memoized toolbar chrome consumes it (see tauri-app/CLAUDE.md § Frontend Pitfalls).

  • menuItems is one checkbox row per registered item, a separator, then Toolbar settings….

useToolbarContextMenu(surfaceId, options) wraps that in the shared useContextMenu + ContextMenu pair and returns { onContextMenu, menu, visibility }.

Three contracts that bite if ignored:

  1. Attach onContextMenu to the bar's container, not to individual buttons. The menu has to stay reachable once items disappear — including when every hideable item on the surface is hidden. The preview pane's floating launcher cluster is therefore always mounted: with both launchers hidden it drops its chip chrome and keeps a bare icon-sized hit area in the preview's top-right corner, purely so the right-click target survives.

  2. ContextMenu is a single-instance registry — a second menu evicts the first. A surface that already owns a right-click menu must merge visibility.menuItems into its own item list rather than render menu. The frame header does exactly this, as a Header buttons ▸ submenu built in packages/frameset/src/frame-chrome-menu-items.ts.

  3. Nested menus win by preventDefault(). A descendant with its own context menu (a note pill, a header pin) calls preventDefault(), and the container handler ignores an already-handled event.

Checkbox rows toggle without closing the menu (ContextMenuCheckboxItem, added in #6565), so several buttons can be hidden in one visit.

Touch

No gesture is invented. WebKit's native long-press synthesizes the same contextmenu event the handler receives, and the ContextMenu key / Shift+F10 are handled by use-context-menu.ts. Where a surface swallows the press (a drag sensor claiming it), Settings → Toolbars is the route.

Adding a new surface

  1. Add the id to the ToolbarSurfaceId union in packages/app-defaults/src/types.ts.

  2. Add the registry entry to TOOLBAR_SURFACES, listing only the items a user may hide. If the surface is a provider's HeaderActions, also add the provider id to HEADER_ACTIONS_SURFACE_BY_PROVIDER.

  3. In the component, call useToolbarContextMenu("<surface-id>"), spread onContextMenu on the container, render {menu} inside it, and guard each registered button with visibility.isVisible("<item-id>").

  4. Nothing else. The Toolbars settings section, the settings-search entries and the Toolbar settings… escape hatch are all generated from the registry.

A component in a package that cannot import the renderer (DraftBar in @takazudo/ui-components, TimelineBoard) takes the hidden ids as a plain readonly string[] prop and a onToolbarContextMenu handler instead, and the renderer-side consumer resolves them from settings.

Surfaces deliberately left out

These were considered and rejected, each for a reason that still holds:

SurfaceWhy not
Workspace-switcher railData-driven — it shows the workspaces you have.
Editor status barPassive readout, not launchers.
Spreadsheet merge buttonsSelection-contextual; they only exist while a selection does.
Mind-map overlayOwns its own menu already.
Quick Actions tilesAlready user-configurable.
Mobile header kebabIts contents are the overflow of an already-configurable surface.
Edit key barAlready has its own key catalog and ordering UI.
Header-left pinsAlready user-managed through Manage header pins.
Kanban calendar / now-view toolbarsNavigation whose removal strands the view.

Known limits

  • Settings do not propagate live across windows. No settings subscription exists in the app today, so a popped-out window picks up a toolbar change on its next load. This is out of scope by design — do not "fix" it locally in the hook.

  • OPEN_TOOLBAR_SETTINGS_EVENT does not open a closed Settings dialog. The listener lives inside SettingsDialog, so dispatching the event from outside the palette only navigates an already open dialog to the Toolbars section. The open-toolbar-settings palette command works because it calls openSettings() first. This matches the existing OPEN_LOCAL_AI_AGENT_SETTINGS_EVENT pattern.

  • The Settings dialog's save is narrowed for these keys. A popped-out window can persist a toolbars, menuBar.rightIcons or noteTrayUtility write while this dialog sits open with a stale draft, so settings-dialog.tsx omits each such key from the save payload when the draft is unchanged from the open-time snapshot. Do not extend that narrowing to other fields — unrelated draft edits must still be saved.