+
+ {/* ── Section header: Active ── */}
+
+
+ {/* ── Active list ── */}
+
{ e.preventDefault(); setActiveDrop(entries.length); }}
+ onDrop={e => onActiveDrop(e as DragEvent, entries.length)}
+ >
+ {entries.length === 0 && (
+
+ {t("toolbar_customization.drag_here")}
+
+ )}
+
+ {entries.map((entry, i) => (
+
+
+
+ {entry.kind === "separator" ? (
+
startDrag(e as DragEvent, { from: "active", idx: i })}
+ onDragEnd={clearDrag}
+ onDragOver={e => onActiveRowOver(e as DragEvent, i)}
+ onDrop={e => onActiveDrop(e as DragEvent, rowInsertIdx(e as DragEvent, i))}
+ onRemove={() => removeAt(i)}
+ />
+ ) : entry.kind === "group" ? (
+ <>
+ setExpanded(v => v === entry.id ? null : entry.id)}
+ onDragStart={e => startDrag(e as DragEvent, { from: "active", idx: i })}
+ onDragEnd={clearDrag}
+ onDragOver={e => onActiveRowOver(e as DragEvent, i)}
+ onDrop={e => onGroupRowDrop(e as DragEvent, i)}
+ onRemove={() => removeAt(i)}
+ />
+ {expandedGroup === entry.id && (
+
+ {entry.items.length === 0 && (
+
+ {t("toolbar_customization.group_empty")}
+
+ )}
+ {entry.items.map((c, ci) => (
+
+
+ {c.kind === "separator" ? (
+ startDrag(e as DragEvent, { from: "child", groupIdx: i, childIdx: ci })}
+ onDragEnd={clearDrag}
+ onDragOver={e => onChildRowOver(e as DragEvent, i, ci)}
+ onDrop={e => onChildDrop(e as DragEvent, i, rowInsertIdx(e as DragEvent, ci))}
+ onRemove={() => removeFromGroup(i, ci)}
+ />
+ ) : (
+ startDrag(e as DragEvent, { from: "child", groupIdx: i, childIdx: ci })}
+ onDragEnd={clearDrag}
+ onDragOver={e => onChildRowOver(e as DragEvent, i, ci)}
+ onDrop={e => onChildDrop(e as DragEvent, i, rowInsertIdx(e as DragEvent, ci))}
+ onRemove={() => removeFromGroup(i, ci)}
+ />
+ )}
+
+ ))}
+
+
+ )}
+ >
+ ) : (
+ startDrag(e as DragEvent, { from: "active", idx: i })}
+ onDragEnd={clearDrag}
+ onDragOver={e => onActiveRowOver(e as DragEvent, i)}
+ onDrop={e => onActiveDrop(e as DragEvent, rowInsertIdx(e as DragEvent, i))}
+ onRemove={() => removeAt(i)}
+ />
+ )}
+
+ ))}
+
+
+ {/* Sticky delete zone — sticks to bottom of scroll area while dragging */}
+ {isDraggingToPool && (
+
{ e.preventDefault(); e.stopPropagation(); setPoolOver(true); setActiveDrop(null); }}
+ onDragLeave={e => {
+ if (!(e.currentTarget as Element).contains(e.relatedTarget as Node)) setPoolOver(false);
+ }}
+ onDrop={e => {
+ e.preventDefault(); e.stopPropagation();
+ if (drag?.from === "active") removeAt(drag.idx);
+ else if (drag?.from === "child") removeFromGroup(drag.groupIdx, drag.childIdx);
+ clearDrag();
+ }}
+ >
+ {poolOver ? `⬇ ${t("toolbar_customization.release_to_remove_from_toolbar")}` : `🗑 ${t("toolbar_customization.drag_to_remove_from_toolbar")}`}
+
+ )}
+
+
+ {/* ── Add buttons ── */}
+
+
+
+
+
+ {/* ── Section header: Available ── */}
+
+
+ {/* ── Pool chips — always rendered so active items can be dragged here to remove ── */}
+
{ e.preventDefault(); setPoolOver(true); }}
+ onDragLeave={e => {
+ // Only clear when truly leaving the container (not entering a child)
+ if (!(e.currentTarget as Element).contains(e.relatedTarget as Node)) {
+ setPoolOver(false);
+ }
+ }}
+ onDrop={onPoolDrop}
+ >
+ {/* Remove hint — shown while dragging an active item */}
+ {isDraggingToPool && (
+
+ {poolOver ? `⬇ ${t("toolbar_customization.release_to_remove")}` : t("toolbar_customization.drop_to_remove")}
+
+ )}
+
+ {/* "All in toolbar" notice when pool is empty and not dragging */}
+ {!isDraggingToPool && pool.length === 0 && (
+
+ {t("toolbar_customization.all_active")}
+
+ )}
+
+ {pool.map(id => (
+
appendItem(id)}
+ onDragStart={e => startDrag(e as DragEvent, { from: "pool", id })}
+ onDragEnd={clearDrag}
+ />
+ ))}
+
+
+ {pool.length > 0 && !isDraggingToPool && (
+
+ {t("toolbar_customization.pool_hint")}
+
+ )}
+
+ );
+}
+
+// ─── Row components ───────────────────────────────────────────────────────────
+
+interface RowBase {
+ faded: boolean;
+ indent?: boolean;
+ onDragStart: (e: Event) => void;
+ onDragEnd: () => void;
+ onDragOver: (e: Event) => void;
+ onDrop: (e: Event) => void;
+ onRemove: () => void;
+}
+
+const ROW_H = "32px";
+
+function rowBase(faded: boolean, extra?: preact.JSX.CSSProperties): preact.JSX.CSSProperties {
+ return {
+ display: "flex",
+ alignItems: "center",
+ height: ROW_H,
+ padding: "0 8px",
+ cursor: "grab",
+ opacity: faded ? 0.3 : 1,
+ userSelect: "none",
+ color: "var(--bs-body-color)", // explicit: prevents dark-mode white-on-light issue
+ borderBottom: `1px solid ${COLOR.rowSep}`,
+ transition: "background .1s, opacity .1s",
+ ...extra,
+ };
+}
+
+function ItemRow({ id, faded, indent, onDragStart, onDragEnd, onDragOver, onDrop, onRemove }: RowBase & { id: string }) {
+ return (
+ (e.currentTarget as HTMLElement).style.boxShadow = `inset 0 0 0 999px ${COLOR.hoverOverlay}`}
+ onMouseLeave={e => (e.currentTarget as HTMLElement).style.boxShadow = ""}
+ >
+
+
+
+
+
+ {getItemLabel(id)}
+
+
+
+ );
+}
+
+function SepRow({ faded, indent, onDragStart, onDragEnd, onDragOver, onDrop, onRemove }: RowBase) {
+ return (
+ (e.currentTarget as HTMLElement).style.boxShadow = `inset 0 0 0 999px ${COLOR.hoverOverlay}`}
+ onMouseLeave={e => (e.currentTarget as HTMLElement).style.boxShadow = ""}
+ >
+
+ {/* currentColor adapts to --bs-body-color in any theme; opacity dims it slightly */}
+
+
+
+ │ {t("toolbar_customization.separator")}
+
+
+
+
+
+ );
+}
+
+interface GroupRowProps extends RowBase {
+ group: ToolbarGroup;
+ expanded: boolean;
+ onToggle: () => void;
+}
+
+function GroupRow({ group, faded, expanded, onToggle, onDragStart, onDragEnd, onDragOver, onDrop, onRemove }: GroupRowProps) {
+ return (
+