Skip to content

Commit 87a27a5

Browse files
committed
feat(mdviewer): progressive toolbar collapse based on available width
Collapse groups into dropdowns progressively as width shrinks: - Level 1 (<480px): block elements (quote, hr, table, codeblock) - Level 2 (<390px): + lists (bullet, ordered, task) - Level 3 (<300px): + text formatting (bold, italic, etc.) Most-used tools stay visible longest.
1 parent 6199697 commit 87a27a5

1 file changed

Lines changed: 54 additions & 41 deletions

File tree

src-mdviewer/src/components/embedded-toolbar.js

Lines changed: 54 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
* Minimal embedded toolbar for Phoenix live preview.
33
* Read mode: "Edit" button
44
* Edit mode: Format row + "Done" button
5-
* Responsive: collapses into dropdown groups when narrow.
5+
* Responsive: progressively collapses groups into dropdowns as width shrinks.
6+
* Level 0: all expanded
7+
* Level 1: block elements collapse
8+
* Level 2: block elements + lists collapse
9+
* Level 3: all groups collapse
610
*/
711
import {
812
createIcons,
@@ -30,10 +34,12 @@ import { t, tp } from "../core/i18n.js";
3034

3135
let toolbar = null;
3236
let resizeObserver = null;
33-
let isCollapsed = false;
37+
let collapseLevel = 0; // 0=expanded, 1=blocks, 2=blocks+lists, 3=all
3438

35-
// Minimum width needed for the expanded toolbar (block-type-select ~90 + 15 buttons*24 + dividers + done ~50)
36-
const COLLAPSE_WIDTH = 520;
39+
// Width thresholds for progressive collapse
40+
const THRESHOLD_BLOCKS = 480; // collapse block elements first
41+
const THRESHOLD_LISTS = 390; // then lists
42+
const THRESHOLD_TEXT = 300; // finally text formatting
3743

3844
const allIcons = { Bold, Italic, Strikethrough, Underline, Code, Link, List, ListOrdered,
3945
ListChecks, Quote, Minus, Table, FileCode, ChevronDown, Type, MoreHorizontal, Pencil };
@@ -59,7 +65,7 @@ function render() {
5965
}
6066

6167
if (state.editMode) {
62-
renderEditMode(isCollapsed);
68+
renderEditMode(collapseLevel);
6369
setupResponsiveToggle();
6470
} else {
6571
renderReadMode();
@@ -85,7 +91,18 @@ function renderReadMode() {
8591
}
8692
}
8793

88-
function renderEditMode(collapsed) {
94+
function btn(id, icon, tooltip) {
95+
return `<button class="toolbar-btn format-btn" id="${id}" data-tooltip="${tooltip}" aria-pressed="false"><i data-lucide="${icon}"></i></button>`;
96+
}
97+
98+
function dropdown(group, triggerIcon, tooltip, content) {
99+
return `<div class="toolbar-dropdown" data-group="${group}">
100+
<button class="toolbar-btn toolbar-dropdown-trigger" data-tooltip="${tooltip}"><i data-lucide="${triggerIcon}"></i><i data-lucide="chevron-down" class="dropdown-chevron"></i></button>
101+
<div class="toolbar-dropdown-panel">${content}</div>
102+
</div>`;
103+
}
104+
105+
function renderEditMode(level) {
89106
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
90107
const mod = isMac ? "\u2318" : "Ctrl";
91108

@@ -97,9 +114,6 @@ function renderEditMode(collapsed) {
97114
<option value="<h3>">${t("slash.heading3") || "Heading 3"}</option>
98115
</select>`;
99116

100-
const btn = (id, icon, tooltip) =>
101-
`<button class="toolbar-btn format-btn" id="${id}" data-tooltip="${tooltip}" aria-pressed="false"><i data-lucide="${icon}"></i></button>`;
102-
103117
const textBtns = [
104118
btn("emb-bold", "bold", tp("format.bold", { mod }) || "Bold"),
105119
btn("emb-italic", "italic", tp("format.italic", { mod }) || "Italic"),
@@ -122,37 +136,31 @@ function renderEditMode(collapsed) {
122136
btn("emb-codeblock", "file-code", t("format.code_block") || "Code block")
123137
].join("");
124138

125-
let formatRow;
126-
if (collapsed) {
127-
formatRow = `
128-
<div class="format-row">
129-
${blockTypeSelect}
130-
<div class="toolbar-divider"></div>
131-
<div class="toolbar-dropdown" data-group="text">
132-
<button class="toolbar-btn toolbar-dropdown-trigger" data-tooltip="${t("format.text_formatting") || "Text formatting"}"><i data-lucide="type"></i><i data-lucide="chevron-down" class="dropdown-chevron"></i></button>
133-
<div class="toolbar-dropdown-panel">${textBtns}</div>
134-
</div>
135-
<div class="toolbar-dropdown" data-group="lists">
136-
<button class="toolbar-btn toolbar-dropdown-trigger" data-tooltip="${t("format.lists") || "Lists"}"><i data-lucide="list"></i><i data-lucide="chevron-down" class="dropdown-chevron"></i></button>
137-
<div class="toolbar-dropdown-panel">${listBtns}</div>
138-
</div>
139-
<div class="toolbar-dropdown" data-group="blocks">
140-
<button class="toolbar-btn toolbar-dropdown-trigger" data-tooltip="${t("format.more_elements") || "More"}"><i data-lucide="more-horizontal"></i><i data-lucide="chevron-down" class="dropdown-chevron"></i></button>
141-
<div class="toolbar-dropdown-panel">${blockBtns}</div>
142-
</div>
143-
</div>`;
144-
} else {
145-
formatRow = `
139+
// Build the text section (inline or dropdown)
140+
const textSection = level >= 3
141+
? dropdown("text", "type", t("format.text_formatting") || "Text formatting", textBtns)
142+
: textBtns;
143+
144+
// Build the list section (inline or dropdown)
145+
const listSection = level >= 2
146+
? dropdown("lists", "list", t("format.lists") || "Lists", listBtns)
147+
: listBtns;
148+
149+
// Build the block section (inline or dropdown)
150+
const blockSection = level >= 1
151+
? dropdown("blocks", "more-horizontal", t("format.more_elements") || "More", blockBtns)
152+
: blockBtns;
153+
154+
const formatRow = `
146155
<div class="format-row">
147156
${blockTypeSelect}
148157
<div class="toolbar-divider"></div>
149-
${textBtns}
158+
${textSection}
150159
<div class="toolbar-divider"></div>
151-
${listBtns}
160+
${listSection}
152161
<div class="toolbar-divider"></div>
153-
${blockBtns}
162+
${blockSection}
154163
</div>`;
155-
}
156164

157165
toolbar.innerHTML = `<div class="embedded-toolbar">
158166
${formatRow}
@@ -166,7 +174,7 @@ function renderEditMode(collapsed) {
166174

167175
wireFormatButtons();
168176
wireBlockTypeSelect();
169-
if (collapsed) {
177+
if (level > 0) {
170178
wireDropdowns();
171179
}
172180
wireDoneButton();
@@ -250,15 +258,20 @@ function wireDoneButton() {
250258
}
251259
}
252260

261+
function widthToCollapseLevel(width) {
262+
if (width < THRESHOLD_TEXT) return 3;
263+
if (width < THRESHOLD_LISTS) return 2;
264+
if (width < THRESHOLD_BLOCKS) return 1;
265+
return 0;
266+
}
267+
253268
function setupResponsiveToggle() {
254-
// Observe the #toolbar element (grid-constrained) not .embedded-toolbar (can overflow)
255269
function checkWidth() {
256270
const width = toolbar.offsetWidth;
257-
const shouldCollapse = width < COLLAPSE_WIDTH;
258-
if (shouldCollapse !== isCollapsed) {
259-
isCollapsed = shouldCollapse;
260-
renderEditMode(isCollapsed);
261-
// Re-attach observer after re-render
271+
const newLevel = widthToCollapseLevel(width);
272+
if (newLevel !== collapseLevel) {
273+
collapseLevel = newLevel;
274+
renderEditMode(collapseLevel);
262275
resizeObserver.observe(toolbar);
263276
}
264277
}

0 commit comments

Comments
 (0)