diff --git a/Dockerfile b/Dockerfile index 5aa1ac0..050c660 100644 --- a/Dockerfile +++ b/Dockerfile @@ -107,6 +107,11 @@ RUN set -eux; \ composer dump-autoload --classmap-authoritative --no-dev; \ composer dump-env prod; \ rm -f .env; \ + # Strip deployment secrets from the compiled .env.local.php so they cannot be read from the + # image layers. APP_SECRET, DATABASE_URL, and MYSQL_* passwords must be injected as real + # environment variables at runtime. If they are absent at runtime Symfony will raise an error + # rather than silently using the public .env.dist defaults. + php -r '$e=include(".env.local.php"); unset($e["APP_SECRET"],$e["DATABASE_URL"],$e["MYSQL_USER"],$e["MYSQL_PASSWORD"],$e["MYSQL_ROOT_PASSWORD"]); file_put_contents(".env.local.php"," { + if (this._dragRow === row) { + row.dataset.dragging = '1'; + } + }); + } + + /** + * @param {DragEvent} ev + */ + _onDragOver(ev) { + if (!this._dragRow) { + return; + } + const row = ev.target instanceof Element ? ev.target.closest('[data-mag-a-row]') : null; + if (!row || row === this._dragRow) { + this._clearDragOver(); + return; + } + // Only allow reordering within the same `[data-mag-a-list]` container. + const dragList = this._dragRow.closest('[data-mag-a-list]'); + if (!dragList || dragList !== row.closest('[data-mag-a-list]')) { + this._clearDragOver(); + return; + } + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'move'; + // Re-evaluate insert position as the cursor moves within the same row. + this._clearDragOver(); + this._dragOverRow = row; + const rect = row.getBoundingClientRect(); + row.dataset[ev.clientY < rect.top + rect.height / 2 ? 'dragBefore' : 'dragAfter'] = '1'; + } + + /** + * @param {DragEvent} ev + */ + _onDrop(ev) { + ev.preventDefault(); + const overRow = this._dragOverRow; + const dragRow = this._dragRow; + if (!overRow || !dragRow || overRow === dragRow) { + this._clearDrag(); + return; + } + const dragList = dragRow.closest('[data-mag-a-list]'); + if (!dragList || dragList !== overRow.closest('[data-mag-a-list]')) { + this._clearDrag(); + return; + } + const insertBefore = 'dragBefore' in overRow.dataset; + this._clearDrag(); + if (insertBefore) { + dragList.insertBefore(dragRow, overRow); + } else { + overRow.insertAdjacentElement('afterend', dragRow); + } + } + + _onDragEnd() { + this._clearDrag(); + } + + _clearDragOver() { + if (this._dragOverRow) { + delete this._dragOverRow.dataset.dragBefore; + delete this._dragOverRow.dataset.dragAfter; + this._dragOverRow = null; + } + } + + _clearDrag() { + if (this._dragRow) { + delete this._dragRow.dataset.dragging; + this._dragRow = null; + } + this._clearDragOver(); + } + captureBaselines() { if (!this.hasNodesTarget) { return; diff --git a/assets/controllers/user_highlight_tooltip_controller.js b/assets/controllers/user_highlight_tooltip_controller.js index 31214cb..a2b94e7 100644 --- a/assets/controllers/user_highlight_tooltip_controller.js +++ b/assets/controllers/user_highlight_tooltip_controller.js @@ -109,7 +109,10 @@ export default class extends Controller { if (!(t instanceof Node)) { return; } - const m = t.nodeType === 1 ? t.closest('mark.user-highlight__marker[data-hl]') : null; + const m = + t.nodeType === 1 + ? /** @type {Element} */ (t).closest('mark.user-highlight__marker[data-hl]') + : t.parentElement?.closest('mark.user-highlight__marker[data-hl]') ?? null; if (m) { const to = e.relatedTarget; if (to && (m.contains(to) || (to instanceof Node && this.tip.contains(/** @type {Node} */ (to))))) { diff --git a/assets/styles/magazine-editor.css b/assets/styles/magazine-editor.css index 9f50456..b52f3e1 100644 --- a/assets/styles/magazine-editor.css +++ b/assets/styles/magazine-editor.css @@ -228,16 +228,66 @@ gap: 0.5rem; } -/* Grid: two columns regardless of flex quirks inside fieldsets / % widths on inputs */ +/* Grid: drag-handle | input | remove-btn */ .magazine-editor__a-row { display: grid; - grid-template-columns: minmax(0, 1fr) auto; + grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 0.35rem; width: 100%; min-width: 0; } +.magazine-editor__a-drag-handle { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.1rem 0.15rem; + margin: 0; + background: none; + border: none; + color: var(--color-text-mid, rgba(160, 160, 160, 0.55)); + cursor: grab; + touch-action: none; + line-height: 0; + font-size: 0; + text-transform: none; + font-weight: normal; + border-radius: 2px; + box-shadow: none; +} + +.magazine-editor__a-drag-handle:active { + cursor: grabbing; +} + +.magazine-editor__a-drag-handle:hover { + color: var(--color-text, inherit); +} + +.magazine-editor__a-drag-handle:focus-visible { + outline: 2px solid var(--color-focus, currentColor); + outline-offset: 2px; +} + +.magazine-editor__a-drag-handle svg { + display: block; +} + +/* Ghost image: fade the row being dragged */ +.magazine-editor__a-row[data-dragging] { + opacity: 0.35; +} + +/* Drop-target indicator: a coloured line above or below the target row */ +.magazine-editor__a-row[data-drag-before] { + box-shadow: 0 -2px 0 0 var(--color-accent, #5b9) inset; +} + +.magazine-editor__a-row[data-drag-after] { + box-shadow: 0 2px 0 0 var(--color-accent, #5b9) inset; +} + .magazine-editor__a-line-field { min-width: 0; max-width: 100%; diff --git a/templates/components/Footer.html.twig b/templates/components/Footer.html.twig index 72bd3a2..99956a5 100644 --- a/templates/components/Footer.html.twig +++ b/templates/components/Footer.html.twig @@ -10,13 +10,6 @@