diff --git a/package.json b/package.json index 7873749..891c1a0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "start": "node server.js", "dev": "node --watch server.js", - "test": "node --test test/render/*.test.mjs && node test/overload-detector.test.js && node test/usage-limit.test.js && node test/auth-errors.test.js && node test/env-load-order.test.js && node test/multi-agent-result.test.js && node test/terminal-session.test.js && node test/terminal-schema.test.js && node test/bots.test.js && node test/bot-inbox.test.js && node test/bots-api.test.js && node test/running-sessions.test.js && node test/session-liveness.test.js && node test/terminal-bridge.integration.test.js && node test/tmux-composite.test.js && node test/engine-spawn-cmd.test.js && node test/telegram-format.test.js && node test/telegram-behaviour.test.js && node test/ask-user-question.test.js && node test/delegate-terminal.test.js && node test/update-flow.test.js && node test/kanban-schedule.test.js && node test/kanban-run-badges.test.js && node test/task-backlog.test.js && node test/i18n-completeness.test.js && node test/config-resolve.test.js && node test/chat-defaults.test.js && node test/chat-defaults-api.test.js && node test/run-continuation.test.js && node test/worktree-manager.test.js && node test/setup-gate.test.js && node test/path-guard.test.js && node test/subscribe-nocatchup.test.js && node test/cli-import-remote.test.js && node test/remote-list-parse.test.js && node test/config-migrate.test.js && node test/engine-pane.test.js && node test/global-workspace.test.js && node test/ssh-secret.test.js && node test/agent-dag.test.js && node test/ssh-parser.test.js && node test/ssh-termination.test.js && node test/session-restart.test.js && node test/remote-env.test.js && node test/remote-files.test.js && node test/remote-files-api.test.js && node test/auth-token.test.js && node test/cmd-model.test.js && node test/origin-guard.test.js && node test/interrupted-recovery.test.js && node test/queue-persistence.test.js && node test/interrupt-idle-config.test.js && node test/agents-md.test.js && node test/editor-links.test.js && node test/editor-open-api.test.js && node test/interrupt-delivery.test.js && node test/terminal-pane-guard.integration.test.js && node test/composer-terminal.test.js && node test/claude-cli-status.test.js && node test/asar-helpers.test.js && node test/task-workers.test.js && node test/git-clone.test.js", + "test": "node --test test/render/*.test.mjs && node test/overload-detector.test.js && node test/usage-limit.test.js && node test/auth-errors.test.js && node test/env-load-order.test.js && node test/multi-agent-result.test.js && node test/terminal-session.test.js && node test/terminal-schema.test.js && node test/bots.test.js && node test/bot-inbox.test.js && node test/bots-api.test.js && node test/running-sessions.test.js && node test/session-liveness.test.js && node test/terminal-bridge.integration.test.js && node test/tmux-composite.test.js && node test/engine-spawn-cmd.test.js && node test/telegram-format.test.js && node test/telegram-behaviour.test.js && node test/ask-user-question.test.js && node test/delegate-terminal.test.js && node test/update-flow.test.js && node test/kanban-schedule.test.js && node test/kanban-run-badges.test.js && node test/task-backlog.test.js && node test/i18n-completeness.test.js && node test/config-resolve.test.js && node test/chat-defaults.test.js && node test/chat-defaults-api.test.js && node test/run-continuation.test.js && node test/worktree-manager.test.js && node test/setup-gate.test.js && node test/path-guard.test.js && node test/subscribe-nocatchup.test.js && node test/cli-import-remote.test.js && node test/remote-list-parse.test.js && node test/config-migrate.test.js && node test/engine-pane.test.js && node test/global-workspace.test.js && node test/ssh-secret.test.js && node test/agent-dag.test.js && node test/ssh-parser.test.js && node test/ssh-termination.test.js && node test/session-restart.test.js && node test/remote-env.test.js && node test/remote-files.test.js && node test/remote-files-api.test.js && node test/auth-token.test.js && node test/cmd-model.test.js && node test/origin-guard.test.js && node test/interrupted-recovery.test.js && node test/queue-persistence.test.js && node test/interrupt-idle-config.test.js && node test/agents-md.test.js && node test/editor-links.test.js && node test/editor-open-api.test.js && node test/interrupt-delivery.test.js && node test/terminal-pane-guard.integration.test.js && node test/composer-terminal.test.js && node test/claude-cli-status.test.js && node test/asar-helpers.test.js && node test/task-workers.test.js && node test/git-clone.test.js && node test/responsive-ui.test.js", "postinstall": "node scripts/install-hooks.js", "release": "node scripts/release.js", "electron:dev": "electron .", diff --git a/public/index.html b/public/index.html index 19b17ae..b39e82c 100644 --- a/public/index.html +++ b/public/index.html @@ -2110,7 +2110,7 @@ .lang-sel option { background: var(--s2); } /* ─── Directory browser ─── */ -.dir-modal { width: 520px; } +.dir-modal { width: 520px; max-width: calc(100vw - 24px); } .dir-path-bar { padding: 8px 16px; border-bottom: 1px solid var(--border); background: var(--bg); display: flex; align-items: center; gap: 6px; } .dir-path-input { flex: 1; background: var(--s2); border: 1px solid var(--border); color: var(--accent2); padding: 6px 10px; border-radius: 6px; font-size: 12px; font-family: var(--font-mono); outline: none; min-width: 0; } .dir-path-input:focus { border-color: var(--accent); } @@ -2167,7 +2167,7 @@ .rh-item .cfg { opacity:0; background:none; border:none; color:var(--muted); cursor:pointer; font-size:13px; padding:3px 6px; border-radius:4px; transition:opacity .12s, color .12s; flex-shrink:0; } .rh-item:hover .cfg { opacity:.6; } .rh-item .cfg:hover { opacity:1 !important; color:var(--accent2); background:rgba(124,106,239,.1); } -.ssh-host-modal { width:480px; } +.ssh-host-modal { width: 480px; max-width: calc(100vw - 24px); } .ssh-host-form { padding:16px; display:flex; flex-direction:column; gap:12px; } .ssh-host-form label { font-size:12px; color:var(--muted); display:block; margin-bottom:4px; } .ssh-host-form input { width:100%; background:var(--s2); border:1px solid var(--border); color:var(--text); padding:6px 10px; border-radius:6px; font-size:12px; font-family:inherit; outline:none; box-sizing:border-box; transition:border .12s; } @@ -2657,8 +2657,24 @@ .chain-progress-card { max-width: calc(100vw - 40px); } .cd-btn-ok, .cd-btn-cancel { min-height: 44px; padding: 10px 20px; } .fpv-modal { width: 100vw !important; max-width: 100vw !important; height: 100vh !important; height: 100dvh !important; max-height: 100vh !important; max-height: 100dvh !important; border-radius: 0 !important; } - .modal { max-height: 92vh; max-height: 92dvh; } + .modal { + max-width: calc(100vw - 24px) !important; + min-width: 0 !important; + max-height: 92vh; + max-height: 92dvh; + box-sizing: border-box !important; + } .modal .modal-body textarea { min-height: 200px; } + #cfgModal .modal { + min-width: 0 !important; + width: calc(100vw - 16px) !important; + max-width: calc(100vw - 16px) !important; + resize: none !important; + } + .dir-modal, .ssh-host-modal, .cmd-modal { + max-width: calc(100vw - 24px) !important; + width: 100% !important; + } } @media (max-width: 480px) { .mob-chip { padding: 8px 14px; font-size: 13px; } @@ -15225,6 +15241,8 @@

Delegate to Agent

if (ddWrap && ddName) { if (activeProj) { ddName.textContent = activeProj.name; + ddName.title = activeProj.name; + $i('projDdBtn')?.setAttribute('title', activeProj.name); ddWrap.classList.add('visible'); } else { ddWrap.classList.remove('visible'); diff --git a/public/kanban.html b/public/kanban.html index 538cfb1..6cf81c7 100644 --- a/public/kanban.html +++ b/public/kanban.html @@ -179,7 +179,7 @@ .card-desc{ font-size:12px; color:var(--muted); line-height:1.5; margin-bottom:8px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; word-break:break-word; } .card-foot{ display:flex; align-items:center; gap:5px; flex-wrap:wrap; } .badge{ font-size:11px; padding:2px 7px; border-radius:4px; white-space:nowrap; font-weight:500; } -.badge-muted{ background:var(--s3); color:var(--muted); } +.badge-muted{ background:var(--s3); color:var(--muted); max-width:140px; overflow:hidden; text-overflow:ellipsis; } .badge-accent{ background:var(--abg); color:var(--accent2); border:1px solid rgba(124,106,239,.2); max-width:140px; overflow:hidden; text-overflow:ellipsis; } .badge-green{ background:rgba(63,185,80,.12); color:var(--green); border:1px solid rgba(63,185,80,.2); } .badge-orange{ background:rgba(229,164,53,.12); color:var(--orange); border:1px solid rgba(229,164,53,.2); } @@ -188,7 +188,7 @@ .sched-info{ display:flex; align-items:center; gap:7px; background:rgba(56,139,253,.07); border:1px solid rgba(56,139,253,.2); color:#58a6ff; border-radius:6px; padding:8px 12px; font-size:12px; line-height:1.4; } .sched-info strong{ color:#79b8ff; font-weight:600; } .card-time{ font-size:11px; color:var(--muted); margin-left:auto; } -.card-actions{ display:none; gap:3px; margin-left:auto; } +.card-actions{ display:none; gap:3px; margin-left:auto; flex-wrap:wrap; } .card:hover .card-actions{ display:flex; } .card:hover .card-time{ display:none; } .cbtn{ background:none; border:none; cursor:pointer; color:var(--muted); padding:2px 5px; border-radius:var(--r-sm); font-size:12px; line-height:1; transition:color .1s,background .1s; } @@ -287,12 +287,13 @@ .hb { padding: 5px 8px; min-height: 40px; } .nav-sw-btn { padding: 4px 8px; font-size: 12px; } .nav-sw-btn span { display: none; } - .proj-dd-btn { max-width: 120px; padding: 5px 10px; font-size: 12px; } + .proj-dd-btn { max-width: 170px; min-width: 0; padding: 5px 10px; font-size: 12px; } .board { padding: 10px 8px 16px; gap: 8px; } .col { width: 240px; } - .modal { max-width: 95vw; max-height: 92vh; max-height: 92dvh; } + .ov { padding: 8px; } + .modal { max-width: calc(100vw - 16px) !important; max-height: 92vh; max-height: 92dvh; box-sizing: border-box !important; } .grid2 { grid-template-columns: 1fr; } - .card-actions { opacity: 1; } + .card-actions { opacity: 1; flex-wrap: wrap; justify-content: flex-end; gap: 2px; } /* Prevent iOS auto-zoom on focus (requires ≥ 16px) */ .inp, .sel, .proj-dd-search input { font-size: 16px; } } @@ -302,6 +303,8 @@ .col { scroll-snap-align: start; } .hdr-meta { display: none; } .kb-progress { display: none; } + .ov { padding: 0; align-items: flex-end; } + .modal { max-width: 100vw !important; width: 100% !important; border-radius: 14px 14px 0 0; max-height: 94vh; max-height: 94dvh; } } /* ─── Chain (group) card ─── */ @@ -915,10 +918,16 @@ if (ddWrap && ddName) { if (curWorkdir) { const proj = projects.find(p => p.workdir === curWorkdir || p.id === curWorkdir); - ddName.textContent = proj?.name || curWorkdir; + const fullName = proj?.name || curWorkdir; + ddName.textContent = fullName; + ddName.title = fullName; + $i('projDdBtn')?.setAttribute('title', fullName); ddWrap.classList.add('visible'); } else { - ddName.textContent = t('proj.all').replace('— ', ''); + const allText = t('proj.all').replace('— ', ''); + ddName.textContent = allText; + ddName.title = allText; + $i('projDdBtn')?.setAttribute('title', allText); ddWrap.classList.add('visible'); } } diff --git a/test/responsive-ui.test.js b/test/responsive-ui.test.js new file mode 100644 index 0000000..18fcbf2 --- /dev/null +++ b/test/responsive-ui.test.js @@ -0,0 +1,51 @@ +'use strict'; +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +let failed = 0; +function check(name, fn) { + try { fn(); console.log(` ✓ ${name}`); } + catch (e) { failed++; console.log(` ✗ ${name}\n ${e.message}`); } +} + +console.log('Responsive UI improvements (#106)'); + +const idxPath = path.join(__dirname, '..', 'public', 'index.html'); +const idxSrc = fs.readFileSync(idxPath, 'utf8'); + +const kbPath = path.join(__dirname, '..', 'public', 'kanban.html'); +const kbSrc = fs.readFileSync(kbPath, 'utf8'); + +check('index.html constrains dir-modal and ssh-host-modal with max-width', () => { + assert.ok(/\.dir-modal\s*\{[^}]*max-width:\s*calc\(100vw\s*-\s*24px\)/.test(idxSrc), 'dir-modal lacks max-width constraint'); + assert.ok(/\.ssh-host-modal\s*\{[^}]*max-width:\s*calc\(100vw\s*-\s*24px\)/.test(idxSrc), 'ssh-host-modal lacks max-width constraint'); +}); + +check('index.html mobile media query overrides modal and cfgModal constraints', () => { + const mediaBlock = idxSrc.slice(idxSrc.indexOf('@media (max-width: 800px)')); + assert.ok(/\.modal\s*\{[\s\S]*?max-width:\s*calc\(100vw\s*-\s*24px\)\s*!important/.test(mediaBlock), 'missing modal max-width in mobile media query'); + assert.ok(/#cfgModal\s+\.modal\s*\{[\s\S]*?min-width:\s*0\s*!important/.test(mediaBlock), 'missing cfgModal min-width:0 override in mobile media query'); +}); + +check('kanban.html constrains modal width on tablet and mobile', () => { + const media800 = kbSrc.slice(kbSrc.indexOf('@media (max-width: 800px)')); + assert.ok(/\.modal\s*\{[\s\S]*?max-width:\s*calc\(100vw\s*-\s*16px\)\s*!important/.test(media800), 'missing modal max-width in kanban 800px query'); + + const media480 = kbSrc.slice(kbSrc.indexOf('@media (max-width: 480px)')); + assert.ok(/\.modal\s*\{[\s\S]*?max-width:\s*100vw\s*!important/.test(media480), 'missing bottom-sheet modal in kanban 480px query'); + assert.ok(/\.ov\s*\{[\s\S]*?align-items:\s*flex-end/.test(media480), 'missing bottom-sheet alignment in kanban 480px query'); +}); + +check('kanban.html card actions and badges wrap gracefully on narrow viewports', () => { + assert.ok(/\.card-actions\{[^}]*flex-wrap:\s*wrap/.test(kbSrc), 'card-actions should wrap'); + assert.ok(/\.badge-muted\{[^}]*text-overflow:\s*ellipsis/.test(kbSrc), 'badge-muted should truncate with ellipsis'); +}); + +check('project dropdown buttons set title attribute for full name tooltips', () => { + assert.ok(/ddName\.title\s*=\s*activeProj\.name/.test(idxSrc), 'index.html should set title on project name'); + assert.ok(/ddName\.title\s*=\s*fullName/.test(kbSrc), 'kanban.html should set title on project name'); +}); + +if (failed) { console.log(`\n${failed} test(s) failed`); process.exit(1); } +console.log('\nAll responsive-ui tests passed');