diff --git a/plugins/voyage/playground/voyage-playground.html b/plugins/voyage/playground/voyage-playground.html
index 65e8b90..6ad4fcb 100644
--- a/plugins/voyage/playground/voyage-playground.html
+++ b/plugins/voyage/playground/voyage-playground.html
@@ -76,6 +76,40 @@
outline-offset: 2px;
}
+ /* v4.3 Step 12 — drag-drop overlay (hidden until dragenter). */
+ .voyage-dropzone {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ background: rgba(0, 0, 0, 0.55);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ }
+ .voyage-dropzone[hidden] { display: none; }
+ .voyage-dropzone--active {
+ pointer-events: auto;
+ }
+ .voyage-dropzone__inner {
+ background: var(--color-surface);
+ color: var(--color-text-primary);
+ border: 2px dashed var(--color-primary-500);
+ border-radius: var(--radius-md);
+ padding: var(--space-6) var(--space-8, 32px);
+ text-align: center;
+ max-width: 520px;
+ }
+ .voyage-dropzone__title {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-semibold);
+ margin-bottom: var(--space-2);
+ }
+ .voyage-dropzone__hint {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+ }
+
/* Render-pipeline layout (left 70% viewport / right 30% sidebar reserved
for Step 10). The viewport panel mounts the rendered markdown. */
.voyage-layout {
@@ -518,6 +552,23 @@
aria-hidden="true"
tabindex="-1"
/>
+
+
+
+
+
Slipp prosjektmappe her
+
Dra direkte fra OS-filutforsker for korrekt path-info.
+
+
Drag-drop deaktivert' +
+ '' + escapeHtml(msg) + '
';
+ var main = document.getElementById('main-content');
+ if (main) main.insertBefore(warn, main.firstChild);
+ }
+
+ function showOverlay() {
+ dropzone.hidden = false;
+ dropzone.classList.add('voyage-dropzone--active');
+ dropzone.setAttribute('aria-hidden', 'false');
+ }
+ function hideOverlay() {
+ dropzone.hidden = true;
+ dropzone.classList.remove('voyage-dropzone--active');
+ dropzone.setAttribute('aria-hidden', 'true');
+ }
+
+ document.addEventListener('dragenter', function (e) {
+ if (e.dataTransfer && Array.from(e.dataTransfer.types || []).indexOf('Files') !== -1) {
+ showOverlay();
+ }
+ });
+ dropzone.addEventListener('dragover', function (e) {
+ e.preventDefault();
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
+ });
+ dropzone.addEventListener('dragleave', function (e) {
+ // Only hide when leaving the overlay entirely (relatedTarget outside).
+ if (!e.relatedTarget || e.relatedTarget === document.documentElement) {
+ hideOverlay();
+ }
+ });
+ dropzone.addEventListener('drop', function (e) {
+ e.preventDefault();
+ hideOverlay();
+
+ if (isFirefox150Windows) {
+ showWarnPanel('Drag-drop deaktivert i Firefox 150 på Windows pga kjent crash-bug. Bruk knappen «Velg prosjektmappe» i stedet.');
+ return;
+ }
+
+ var items = e.dataTransfer && e.dataTransfer.items;
+ if (!items || !items.length) return;
+ var collected = [];
+ var basePath = '';
+ var pending = 0;
+ var allDone = false;
+
+ function maybeFinish() {
+ if (allDone && pending === 0) {
+ if (typeof loadProjectDirectory === 'function') {
+ loadProjectDirectory(collected, basePath);
+ } else {
+ try { console.log('[voyage] drag-drop: ' + collected.length + ' files in ' + basePath); } catch (_) {}
+ }
+ }
+ }
+
+ // Recursive walker — readEntries() returns max 100 entries per call,
+ // so loop until it returns an empty array (Chromium spec).
+ function walkEntry(entry, prefix) {
+ if (!entry) return;
+ if (entry.isFile) {
+ pending++;
+ entry.file(function (file) {
+ try {
+ Object.defineProperty(file, 'webkitRelativePath', {
+ value: prefix + entry.name,
+ configurable: true
+ });
+ } catch (_) { /* property may already exist */ }
+ collected.push(file);
+ pending--;
+ maybeFinish();
+ }, function () {
+ pending--;
+ maybeFinish();
+ });
+ } else if (entry.isDirectory) {
+ if (!basePath) basePath = entry.name;
+ var reader = entry.createReader();
+ function readBatch() {
+ pending++;
+ reader.readEntries(function (entries) {
+ pending--;
+ if (!entries.length) {
+ maybeFinish();
+ return;
+ }
+ for (var i = 0; i < entries.length; i++) {
+ walkEntry(entries[i], prefix + entry.name + '/');
+ }
+ readBatch();
+ }, function () {
+ pending--;
+ maybeFinish();
+ });
+ }
+ readBatch();
+ }
+ }
+
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i];
+ var entry = item.webkitGetAsEntry && item.webkitGetAsEntry();
+ if (entry) walkEntry(entry, '');
+ }
+ allDone = true;
+ maybeFinish();
+ });
+ }
+
// ---- v4.3 Step 9 — renderPageShell --------------------------------
// Universal page-header for dashboard + artifact-detail flater.
// Returns an HTML string wrapping body content with DS Tier 3
@@ -1650,6 +1833,10 @@ playground first-run shows a complete round-trip-able artifact.
// Step 11 (v4.3) — webkitdirectory project-loader wiring (button +
// hidden file-input + browser-support detection).
wireProjectLoader();
+
+ // Step 12 (v4.3) — drag-drop overlay with webkitGetAsEntry recursive
+ // walker + Firefox 150 Windows UA-guard.
+ wireDragDrop();
}
function setThemeLabel(theme) {