Skip to content

add toolbar customization over drag & drop#8864

Open
tuuuni-cell wants to merge 17 commits intoTriliumNext:mainfrom
tuuuni-cell:main
Open

add toolbar customization over drag & drop#8864
tuuuni-cell wants to merge 17 commits intoTriliumNext:mainfrom
tuuuni-cell:main

Conversation

@tuuuni-cell
Copy link
Copy Markdown

@tuuuni-cell tuuuni-cell commented Mar 1, 2026

Adds a new "Toolbar" tab to the Text Notes options panel, allowing users to
fully customize the toolbar.

  • Drag & drop reordering of toolbar items
  • Click-to-add chips from a pool of all available items
  • Groups — collapsible sections that map to CKEditor toolbar groups, with their own nested drag & drop
  • Remove any item by clicking ✕ or dragging it to the sticky delete zone at the bottom
  • Drop indicator — a glowing blue line shows exactly between which two items the dragged element will land

Its my first time contributing do you need something more?

image

tuuuni147 and others added 13 commits February 28, 2026 19:00
Users can now reorder, hide and group toolbar buttons
directly from the text note settings.
…ble pool chips

- Layout: active items now shown as a full-width vertical list (top = leftmost in editor)
- Available items shown as compact chips below — click to append OR drag to insert
- Removed 'Block Toolbar' tab (internal CKEditor detail, not user-facing)
- Group rows now have a blue left border and bold label for clear visual hierarchy
- Expanded group children shown with blue left indent line
- Improved translation strings with clearer descriptions
- Pool chips: click-to-add support in addition to drag-and-drop
…l chip hover

- Replace braille ⠿ character with a proper inline SVG 2×3 dot grid (no font rendering issues)
- Add border-bottom on every row for clear visual separation between items
- Group rows: blue left border (4px) + primary-bg-subtle background + bold label
- Expanded group children: shown on a distinct secondary-bg background
- Pool chips: white background + border, blue hover (matches group color)
- Drop indicator line: 3px rounded blue bar (was 2px, harder to see)
- Extract design token constants (COLOR) for consistent theming
- Dark mode text: add explicit color:var(--bs-body-color) to all rows so text is
  always readable regardless of theme background
- Group row bg: replace --bs-primary-bg-subtle (broken in dark mode) with
  rgba(13,110,253,0.13) — adds a blue tint to whatever body-bg is, works in both themes
- Pool chip hover: replace background change with box-shadow ring — no background
  change means text stays readable in dark mode
- DropLine: height is now 0 when inactive (no invisible 3px gaps between rows);
  expands to 4px bright blue when dragging between two specific rows
- Drag active→pool to remove: pool section now has dragOver/drop handlers;
  when dragging an active item a dashed border + "drop to remove" hint appears;
  on hover turns red so the action is obvious
…e, better separator/dropline

- Remove className="btn btn-link btn-sm p-0" from RemoveBtn and GroupRow toggle:
  these were being styled by Trilium's theme as wide gray rectangles, making rows look
  cluttered. Now use purely inline styles (background:none, border:none).
- Remove className="btn btn-sm" from PoolChip for the same reason.
- Active list now scrollable: maxHeight 380px + overflowY auto (was overflow:hidden).
- Sticky delete zone: a "🗑 Drag here to remove" bar sticks to the bottom of the
  scrollable list area while dragging, turning red on hover. Always reachable regardless
  of list scroll position.
- Separator row: replace near-invisible dashed border-top with solid 1px lines +
  a small badge label — clearly visible in both light and dark mode.
- DropLine: replaced with VS Code-style indicator — 2px blue horizontal line with
  end-cap circles at both sides. Only rendered when active (no height when inactive).
- Group bg opacity increased from 0.13 to 0.18 for more visible distinction.
…header contrast

- Separator row: remove badge (background was resolving to wrong color in some themes);
  now uses "currentColor" lines at opacity 0.55 — automatically adapts to body text
  color in any theme, no background variables involved
- Drop indicator (DropLine): replace clipped position:absolute circles with a solid
  3px blue line + box-shadow glow; glow makes it unmissable in both light and dark mode
- Row hover: replace background change (var(--bs-tertiary-bg) was inconsistent) with
  "inset 0 0 0 999px rgba(255,255,255,0.06)" box-shadow overlay — in dark mode adds
  a subtle lightness, reliable in any theme
- Section headers: use var(--bs-body-color) at opacity 0.65 instead of var(--bs-secondary-color)
  which was too faint in some dark themes
- rowSep: use solid border-color instead of translucent variant (more visible in dark mode)
The container's onDragOver was firing after every row's onDragOver due
to event bubbling and always overwriting activeDrop with entries.length,
so the blue line appeared only at the very bottom instead of between the
two items the cursor was positioned between.

Fix: add e.stopPropagation() in onActiveRowOver so the container handler
only fires when the cursor is in empty space below all rows (which is the
one remaining valid case for the container-level fallback).

onChildRowOver already had stopPropagation — only the top-level row
handler was missing it.
…ar-TzrEM

Claude/customize trilium toolbar tzr em
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Mar 1, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a highly anticipated feature: interactive customization of the text editor's toolbar. Users can now personalize their editing environment by arranging, adding, and removing toolbar buttons and groups through an intuitive drag-and-drop interface. This enhancement significantly improves user control over the editor's layout, adapting it to individual workflows and preferences, and ensures a more efficient and tailored writing experience.

Highlights

  • Interactive Toolbar Customization: Introduced a new 'Toolbar' tab within the Text Notes options panel, enabling comprehensive customization of the text editor's toolbar.
  • Drag & Drop Functionality: Implemented drag & drop for reordering toolbar items, allowing users to intuitively arrange buttons and groups.
  • Flexible Item Management: Users can now click to add items from a pool of available options, create collapsible groups for nested items, and remove any item by dragging it to a dedicated delete zone or clicking a remove button.
  • Dynamic Toolbar Configuration: The editor's toolbar configuration is now dynamically generated based on user preferences stored in a new textNoteToolbarConfig option, replacing previous hardcoded layouts.
  • Enhanced User Experience: Visual drop indicators provide clear feedback during drag & drop operations, improving the usability of the customization interface.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • apps/client/src/translations/en/translation.json
    • Added numerous new translation keys to support the user interface for toolbar customization, including titles, descriptions, button labels, and hints for various actions like adding separators, groups, and removing items.
  • apps/client/src/widgets/type_widgets/options/text_notes.tsx
    • Imported and rendered the new ToolbarCustomization component, integrating the interactive toolbar settings into the existing Text Notes options panel.
  • apps/client/src/widgets/type_widgets/options/toolbar_customization.tsx
    • Added a new Preact component that provides the interactive UI for toolbar customization, featuring drag & drop logic, state management for toolbar entries, and rendering of active items, available items pool, separators, and groups.
  • apps/client/src/widgets/type_widgets/text/toolbar.ts
    • Removed hardcoded toolbar item arrays and replaced them with calls to new resolveClassicItems, resolveFloatingItems, and resolveBlockToolbarItems functions, which dynamically build the toolbar based on user-defined configurations.
    • Updated type assertions for toolbar items to explicitly handle string[] within grouped items.
  • apps/client/src/widgets/type_widgets/text/toolbar_config.ts
    • Created a new module defining data models (ToolbarItem, ToolbarSeparator, ToolbarGroup, ToolbarEntry, ToolbarCustomConfig) for toolbar customization.
    • Implemented entriesToCKItems function to convert the custom toolbar configuration into the format expected by CKEditor5, including logic for cleaning up separators and handling hidden items/empty groups.
    • Defined TOOLBAR_ITEM_LABELS for human-readable names of CKEditor commands and getItemLabel utility.
    • Provided DEFAULT_CLASSIC_TOOLBAR, DEFAULT_FLOATING_TOOLBAR, and DEFAULT_BLOCK_TOOLBAR constants to ensure backward compatibility and default behavior.
  • apps/server/src/routes/api/options.ts
    • Whitelisted the new textNoteToolbarConfig option, allowing it to be managed via the API.
  • apps/server/src/services/options_init.ts
    • Initialized the textNoteToolbarConfig option with an empty string as its default value, indicating that built-in defaults should be used initially.
  • packages/commons/src/lib/options_interface.ts
    • Added textNoteToolbarConfig to the OptionDefinitions interface, specifying it as a string type for JSON-serialized custom toolbar configurations.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a powerful and well-designed feature for customizing the editor toolbar. The implementation is comprehensive, covering drag-and-drop reordering, adding/removing items, and grouping. The code is split into a configuration model, a UI component for customization, and integration into the existing toolbar logic, which is a good separation of concerns. I've identified a few areas for improvement, mainly concerning type safety, performance, and internationalization in the new UI component. Overall, this is a great addition.

Comment on lines 27 to 33
if (typeof item === "object" && "items" in item) {
for (const subitem of item.items) {
for (const subitem of (item as { items: string[] }).items) {
items.push(subitem);
}
} else {
items.push(item);
items.push(item as string);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The type assertion (item as { items: string[] }) is incorrect. The items property of a toolbar group object is of type (string | object)[], not string[]. This could lead to runtime errors if a sub-item is an object and is pushed into the items array, which is of type string[]. The loop should be more type-safe.

        if (typeof item === "object" && "items" in item) {
            for (const subitem of (item as { items: (string|object)[] }).items) {
                if (typeof subitem === 'string') {
                    items.push(subitem);
                }
            }
        } else if (typeof item === 'string') {
            items.push(item);
        }

const [local, setLocal] = useState<ToolbarCustomConfig>(() => parseConfig(savedRaw));
const [tab, setTab] = useState<TabKey>("classic");

const isDirty = JSON.stringify(local) !== JSON.stringify(parseConfig(savedRaw));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For performance optimization, parseConfig(savedRaw) is called on every render. This can be memoized using useMemo to avoid re-parsing the JSON string unnecessarily.

const savedConfig = useMemo(() => parseConfig(savedRaw), [savedRaw]);
const isDirty = JSON.stringify(local) !== JSON.stringify(savedConfig);

function addSeparator() { commit([...entries, { kind: "separator" } as ToolbarSeparator]); }

function addGroup() {
const id = `group_${Date.now()}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Date.now() does not guarantee unique IDs, especially if a user adds items very quickly. This could lead to key conflicts in React. Using crypto.randomUUID() is a more robust way to generate unique IDs.

Suggested change
const id = `group_${Date.now()}`;
const id = `group_${crypto.randomUUID()}`;

clearDrag();
}}
>
{poolOver ? "⬇ Release to remove from toolbar" : "🗑 Drag here to remove from toolbar"}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The strings "⬇ Release to remove from toolbar" and "🗑 Drag here to remove from toolbar" are hardcoded. They should be internationalized using the t() function. Similarly, on line 566, "⬇ Release to remove" is also hardcoded.

Please add these strings to apps/client/src/translations/en/translation.json and use t() to render them.

tuuuni147 and others added 3 commits March 2, 2026 06:03
…strings

- Replace Date.now() with crypto.randomUUID() to guarantee unique group
  IDs even when adding items rapidly
- Add i18n keys for the three hardcoded drag/drop removal strings and
  use t() calls instead of raw English text

https://claude.ai/code/session_017f29o9ho9X2q69LiyLj1nj
fix: use crypto.randomUUID() for group IDs and internationalize drag …
@tuuuni-cell
Copy link
Copy Markdown
Author

tuuuni-cell commented Mar 2, 2026

@gemini-code-assist review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new feature for interactive toolbar customization with drag and drop functionality. While a great addition, it introduces a stored Cross-Site Scripting (XSS) vulnerability. The custom toolbar configuration, stored in a user-controlled option, contains properties that are rendered unsanitized as HTML/SVG by the underlying editor component, potentially allowing arbitrary JavaScript execution if an attacker manipulates the configuration. It is crucial to add sanitization or strict validation for the toolbar group labels and icons to mitigate this risk. Additionally, the code review suggests improvements for the new ToolbarCustomization component, including using CSS classes instead of inline styles, enhancing usability by allowing items to be dragged out of groups, and following React best practices for conditional rendering.

Comment on lines +100 to +101
label: entry.label,
icon: resolveIcon(entry.icon),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The textNoteToolbarConfig option stores a JSON-serialized configuration for the text note toolbar. This configuration includes ToolbarGroup objects, which have label and icon properties. These properties are passed unsanitized to CKEditor 5 when building the toolbar. CKEditor 5 renders the icon property as an SVG string, which can contain malicious JavaScript (e.g., <svg/onload=alert(1)>). Since the textNoteToolbarConfig option is user-controlled and added to the ALLOWED_OPTIONS list in the server-side API, an authenticated user (or an attacker via CSRF) can set a malicious configuration that will execute JavaScript whenever the user opens a text note.

To remediate this, sanitize the label and icon properties of ToolbarGroup objects before passing them to CKEditor. Alternatively, restrict the icon property to a set of allowed built-in icon names or validate that it is a safe SVG string.

Comment on lines +320 to +326
function onActiveDrop(e: DragEvent, at: number) {
e.preventDefault();
if (!drag) return;
if (drag.from === "pool") insertAt(drag.id, at);
else if (drag.from === "active") moveActive(drag.idx, at);
clearDrag();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation doesn't allow dragging an item from a group to the main list of active items. This can be inconvenient for users who want to reorganize their toolbar by moving items out of groups. Consider enhancing this drop handler to support items dragged from a group (drag.from === 'child'). A similar enhancement could be made to onGroupRowDrop to allow moving items between groups.

Comment on lines +627 to +646
function ItemRow({ id, faded, indent, onDragStart, onDragEnd, onDragOver, onDrop, onRemove }: RowBase & { id: string }) {
return (
<div
draggable
style={rowBase(faded, { paddingLeft: indent ? "28px" : "8px" })}
onDragStart={onDragStart} onDragEnd={onDragEnd}
onDragOver={onDragOver} onDrop={onDrop}
onMouseEnter={e => (e.currentTarget as HTMLElement).style.boxShadow = `inset 0 0 0 999px ${COLOR.hoverOverlay}`}
onMouseLeave={e => (e.currentTarget as HTMLElement).style.boxShadow = ""}
>
<DragDots />
<span style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: "18px", flexShrink: 0 }}>
<ToolbarIcon id={id} size={14} />
</span>
<span style={{ marginLeft: "8px", flex: 1, fontSize: "0.86em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{getItemLabel(id)}
</span>
<RemoveBtn onClick={onRemove} />
</div>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This component, along with SepRow, GroupRow, and PoolChip, uses inline styles and direct DOM manipulation for hover effects (onMouseEnter, onMouseLeave). This is generally considered an anti-pattern in React as it mixes presentation with logic and can be less performant. It would be better to use CSS classes and the :hover pseudo-selector for these effects. This would improve maintainability and readability.

For example:

.item-row:hover {
  box-shadow: inset 0 0 0 999px rgba(255,255,255,0.06);
}

Additionally, the event handler types in the RowBase interface could be more specific (e.g., onDrop: (e: DragEvent) => void;) to avoid repeated type casting (e as DragEvent) in the component implementations.

* When active: a solid 3px blue line with a bright glow so it's unmissable in any theme.
*/
function DropLine({ active, indent }: { active: boolean; indent?: boolean }) {
if (!active) return <div style={{ height: 0 }} />;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of rendering a div with height: 0, it's more idiomatic in React to return null when a component should not render anything. This avoids adding an unnecessary element to the DOM.

Suggested change
if (!active) return <div style={{ height: 0 }} />;
if (!active) return null;

@tuuuni-cell tuuuni-cell changed the title add interactive toolbar customization with drag & drop add toolbar customization over drag & drop Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflicts size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants