From 3cd90f5af565592c6e442ec8978506950e6aa7d4 Mon Sep 17 00:00:00 2001
From: net <96362337+netqo@users.noreply.github.com>
Date: Wed, 2 Sep 2026 17:17:33 -0300
Subject: [PATCH] feat: add hover controls to add/remove table rows and columns
Editing tables in the live editor was hard: rows could only be added via
Enter/Tab inside a cell, and the column insert/delete primitive
(_tableColumnResult) existed but had no caller, so columns could not be
changed at all from the UI.
Add hover-revealed controls anchored to the live table:
- a "+" bar on the right edge appends a column, and one on the bottom
edge appends a row;
- a small "x" above each column and to the left of each body row deletes
that specific column/row.
Controls are rendered as part of the table markup, so they are recreated
on every re-render and never touch the markdown source: they are
contenteditable="false", carry no data-editable/data-from, and are
ignored by the DOM-to-source mapping. Handles are positioned by anchoring
to real th/td boxes, so they track column/row geometry with no JS
measurement. Clicks are dispatched through the existing delegated live
click handler to the existing row/column mutation primitives, so undo,
redo, and screen-reader announcements work unchanged. Controls are hidden
when the editor is read-only or disabled, the delete-column handle is
hidden at one column, and delete-row handles are absent when the body is
empty.
---
templates/writemark.js | 77 +++++++++++++++++++++++++++++++++++++++---
1 file changed, 72 insertions(+), 5 deletions(-)
diff --git a/templates/writemark.js b/templates/writemark.js
index ae99fef..b7f18f5 100644
--- a/templates/writemark.js
+++ b/templates/writemark.js
@@ -1606,6 +1606,25 @@ class WritemarkEditorElement extends HTMLElement {
.md-table th, .md-table td { border: 1px solid var(--md-editor-border); padding: 6px 8px; vertical-align: top; }
.md-table th { background: color-mix(in srgb, CanvasText 7%, Canvas 93%); font-weight: 700; text-align: left; }
.md-cell { min-height: 1.35em; outline: none; white-space: pre-wrap; overflow-wrap: anywhere; }
+ /* Hover controls (add/remove rows and columns): identically sized,
+ 2px-corner tabs. The block adds no padding, so table spacing matches
+ the base rule (margin-block: 0.5em). Each tab is centered on a table
+ edge: column-delete on the top, row-add on the bottom, and column-add
+ and row-delete on the right (over the cells' right padding). Nothing
+ extends horizontally, so there is no scrollbar; overflow is shown so
+ the top/bottom tabs are not clipped. */
+ .md-has-controls { position: relative; overflow: visible; }
+ .md-has-controls .md-table th, .md-has-controls .md-table td { position: relative; }
+ .md-tbl-ctl { position: absolute; z-index: 3; box-sizing: border-box; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 1px solid var(--md-editor-border); border-radius: 2px; background: var(--md-editor-code-bg, Canvas); color: var(--md-editor-fg); cursor: pointer; opacity: 0; transition: opacity 0.12s ease; user-select: none; }
+ .md-tbl-ctl svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
+ .md-tbl-ctl:focus-visible { opacity: 1; outline: 2px solid var(--md-editor-accent, Highlight); outline-offset: 1px; }
+ [data-table-control="add-col"] { right: 0; top: 50%; transform: translateY(-50%); }
+ [data-table-control="add-row"] { left: 50%; bottom: 0; transform: translate(-50%, 50%); }
+ [data-table-control="del-col"] { left: 50%; top: 0; transform: translate(-50%, -50%); }
+ [data-table-control="del-row"] { right: 0; top: 50%; transform: translateY(-50%); }
+ .md-has-controls:hover [data-table-control^="add-"], .md-has-controls:focus-within [data-table-control^="add-"] { opacity: 0.5; }
+ .md-table th:hover [data-table-control="del-col"], .md-table tr:hover [data-table-control="del-row"] { opacity: 0.6; }
+ .md-has-controls .md-tbl-ctl:hover { opacity: 1; background: color-mix(in srgb, CanvasText 12%, Canvas 88%); }
.completion-popup { position: absolute; z-index: 20; min-inline-size: 240px; max-inline-size: min(420px, 90vw); max-block-size: min(320px, 50vh); overflow: auto; border: 1px solid var(--md-editor-popup-border); border-radius: var(--md-editor-radius); background: var(--md-editor-popup-bg); color: var(--md-editor-popup-fg); box-shadow: var(--md-editor-popup-shadow); padding: 4px; }
.completion-popup[hidden] { display: none; }
/* Floating selection toolbar (bubble menu) shown above a text selection.
@@ -2249,14 +2268,33 @@ class WritemarkEditorElement extends HTMLElement {
_renderTable(block) {
const cols = Math.max(block.header.cells.length, ...block.rows.map(r => r.cells.length), 1);
const alignments = Array.from({ length: cols }, (_, i) => tableAlignmentFromDelimiter(block.delimiter.cells[i]?.text));
- const renderCell = (cell, tag, row, col) => `<${tag}${tableAlignmentStyle(alignments[col])}>