From 68ebd671fbeb5641f8e66a761ac8600b0c584958 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Fri, 22 May 2026 20:57:36 +0200 Subject: [PATCH] Apply Prioritix design to Rank --- .dockerignore | 7 ++ public/app.js | 300 +++++++--------------------------------------- public/index.html | 200 +++++++++++++++++++------------ public/styles.css | 125 +++---------------- 4 files changed, 189 insertions(+), 443 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ec56d56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.git +.env +*.log +.DS_Store +coverage +playwright-report diff --git a/public/app.js b/public/app.js index bcb469a..3294a18 100644 --- a/public/app.js +++ b/public/app.js @@ -1,259 +1,43 @@ -const state = { - ideas: [], - milestones: [], - activity: [], - filter: 'all', - search: '', - selected: null, -}; - -const $ = (sel, root = document) => root.querySelector(sel); -const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); -const board = $('#board'); -const form = $('#ideaForm'); -const detail = $('#detail'); -const detailForm = $('#detailForm'); -const milestoneSelect = $('#milestoneSelect'); - -async function api(path, options = {}) { - const res = await fetch(path, { - headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, - ...options, - body: options.body && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body, - }); - const text = await res.text(); - const data = text ? JSON.parse(text) : null; - if (!res.ok) throw new Error(data?.error || res.statusText); - return data; -} - -function scoreOf(idea) { return Number(idea.score || 0).toFixed(1); } -function escapeHtml(value) { - return String(value ?? '').replace(/[&<>'"]/g, ch => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[ch])); -} -function short(text, len = 126) { - const value = String(text || '').trim(); - return value.length > len ? `${value.slice(0, len - 1)}…` : value; -} -function milestoneFor(id) { return state.milestones.find(m => m.id === id) || state.milestones[0]; } -function sourceKind(idea) { return /agent|rook|iris|eve|claude|gpt|bot/i.test(`${idea.source} ${idea.sourceName}`) ? 'agent' : 'human'; } -function toast(message) { - const el = document.createElement('div'); - el.className = 'toast'; - el.textContent = message; - document.body.append(el); - setTimeout(() => el.remove(), 2200); -} - -function filteredIdeas() { - const q = state.search.toLowerCase(); - return state.ideas.filter(idea => { - if (state.filter === 'human' && sourceKind(idea) !== 'human') return false; - if (state.filter === 'agent' && sourceKind(idea) !== 'agent') return false; - if (state.filter === 'high' && Number(idea.score) < 4) return false; - if (!q) return true; - return [idea.title, idea.description, idea.sourceName, ...(idea.labels || [])].join(' ').toLowerCase().includes(q); - }); -} - -function renderStats() { - const ideas = state.ideas; - const now = ideas.filter(i => i.milestoneId === 'now').length; - const agent = ideas.filter(i => sourceKind(i) === 'agent').length; - const top = ideas.slice().sort((a, b) => b.score - a.score)[0]; - $('#stats').innerHTML = [ - ['Ideas', ideas.length], - ['Now', now], - ['Agent drops', agent], - ['Top score', top ? scoreOf(top) : '—'], - ].map(([label, value]) => `
${value}${label}
`).join(''); -} - -function renderMilestoneOptions() { - milestoneSelect.innerHTML = state.milestones.map(m => ``).join(''); -} - -function renderActivity() { - $('#activity').innerHTML = state.activity.slice(0, 8).map(item => `${escapeHtml(short(item.message, 52))}`).join(''); -} - -function renderBoard() { - renderStats(); - renderMilestoneOptions(); - renderActivity(); - const ideas = filteredIdeas(); - board.innerHTML = state.milestones.map(milestone => { - const laneIdeas = ideas - .filter(idea => (idea.milestoneId || 'inbox') === milestone.id) - .sort((a, b) => (a.rank - b.rank) || (b.score - a.score)); - const cards = laneIdeas.map(cardHtml).join('') || '
Drop something here
'; - return `
-
-

${escapeHtml(milestone.name)}

${laneIdeas.length}
-

${escapeHtml(milestone.description || milestone.horizon || '')}

-
-
${cards}
-
`; - }).join(''); - bindDrag(); - bindCards(); -} - -function cardHtml(idea) { - const tags = [`${sourceKind(idea)}`, idea.sourceName, ...(idea.labels || [])].filter(Boolean).slice(0, 5); - return `
-

${escapeHtml(idea.title)}

${scoreOf(idea)}
- ${idea.description ? `

${escapeHtml(short(idea.description))}

` : ''} -
${tags.map(t => `${escapeHtml(t)}`).join('')}
-
- I ${idea.impact}E ${idea.effort}C ${idea.confidence}U ${idea.urgency} -
-
`; -} - -function bindCards() { - $$('.card').forEach(card => card.addEventListener('click', () => openDetail(card.dataset.id))); -} - -function bindDrag() { - $$('.card').forEach(card => { - card.addEventListener('dragstart', event => { - card.classList.add('dragging'); - event.dataTransfer.setData('text/plain', card.dataset.id); - }); - card.addEventListener('dragend', () => card.classList.remove('dragging')); - }); - $$('.lane').forEach(lane => { - lane.addEventListener('dragover', event => { event.preventDefault(); lane.classList.add('drag-over'); }); - lane.addEventListener('dragleave', () => lane.classList.remove('drag-over')); - lane.addEventListener('drop', async event => { - event.preventDefault(); - lane.classList.remove('drag-over'); - const id = event.dataTransfer.getData('text/plain'); - const idea = state.ideas.find(i => i.id === id); - const milestoneId = lane.dataset.milestone; - if (!idea || idea.milestoneId === milestoneId) return; - idea.milestoneId = milestoneId; - idea.rank = Date.now() % 100000; - renderBoard(); - try { - const updated = await api(`/api/ideas/${id}`, { method: 'PATCH', body: { milestoneId, rank: idea.rank } }); - replaceIdea(updated); - toast(`Moved to ${milestoneFor(milestoneId).name}`); - } catch (error) { toast(error.message); await load(); } - }); - }); -} - -function replaceIdea(idea) { - const idx = state.ideas.findIndex(i => i.id === idea.id); - if (idx >= 0) state.ideas[idx] = idea; - else state.ideas.unshift(idea); - state.ideas.sort((a, b) => b.score - a.score); -} - -function openDetail(id) { - const idea = state.ideas.find(i => i.id === id); - if (!idea) return; - state.selected = idea.id; - detailForm.title.value = idea.title; - detailForm.description.value = idea.description || ''; - detailForm.impact.value = idea.impact; - detailForm.effort.value = idea.effort; - detailForm.confidence.value = idea.confidence; - detailForm.urgency.value = idea.urgency; - detailForm.notes.value = idea.notes || ''; - detail.classList.add('open'); - detail.setAttribute('aria-hidden', 'false'); -} -function closeDetail() { - detail.classList.remove('open'); - detail.setAttribute('aria-hidden', 'true'); - state.selected = null; -} - -form.addEventListener('submit', async event => { - event.preventDefault(); - const fd = new FormData(form); - const payload = Object.fromEntries(fd.entries()); - payload.labels = String(payload.labels || '').split(',').map(s => s.trim()).filter(Boolean); - payload.source = payload.sourceName ? sourceKind({ sourceName: payload.sourceName }) : 'human'; - payload.status = payload.milestoneId === 'inbox' ? 'inbox' : 'planned'; - try { - const idea = await api('/api/ideas', { method: 'POST', body: payload }); - replaceIdea(idea); - form.reset(); - form.impact.value = 7; form.effort.value = 4; form.confidence.value = 6; form.urgency.value = 5; - renderBoard(); - toast('Captured'); - } catch (error) { toast(error.message); } -}); - -detailForm.addEventListener('submit', async event => { - event.preventDefault(); - if (!state.selected) return; - const payload = Object.fromEntries(new FormData(detailForm).entries()); - try { - const idea = await api(`/api/ideas/${state.selected}`, { method: 'PATCH', body: payload }); - replaceIdea(idea); - closeDetail(); - renderBoard(); - toast('Saved'); - } catch (error) { toast(error.message); } -}); - -$('#archiveIdea').addEventListener('click', async () => { - if (!state.selected) return; - try { - await api(`/api/ideas/${state.selected}`, { method: 'PATCH', body: { archived: true } }); - state.ideas = state.ideas.filter(i => i.id !== state.selected); - closeDetail(); - renderBoard(); - toast('Archived'); - } catch (error) { toast(error.message); } -}); - -$('#closeDetail').addEventListener('click', closeDetail); -$('#refresh').addEventListener('click', load); -$('#search').addEventListener('input', event => { state.search = event.target.value; renderBoard(); }); -$('#filters').addEventListener('click', event => { - const button = event.target.closest('button[data-filter]'); - if (!button) return; - state.filter = button.dataset.filter; - $$('#filters button').forEach(b => b.classList.toggle('active', b === button)); - renderBoard(); -}); -$('#addMilestone').addEventListener('click', async () => { - const name = prompt('Milestone name'); - if (!name) return; - const horizon = prompt('Horizon / timing', 'Custom') || ''; - const colors = ['#8cf7ff', '#f8ff73', '#a78bfa', '#6ee7b7', '#ff5e7a']; - try { - const milestone = await api('/api/milestones', { method: 'POST', body: { name, horizon, color: colors[state.milestones.length % colors.length], position: state.milestones.length * 10 } }); - state.milestones.push(milestone); - renderBoard(); - toast('Milestone added'); - } catch (error) { toast(error.message); } -}); - -document.addEventListener('keydown', event => { - if (event.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) { - event.preventDefault(); - $('#title').focus(); - } - if (event.key === 'Escape') closeDetail(); -}); - -async function load() { - try { - const data = await api('/api/bootstrap'); - state.ideas = data.ideas || []; - state.milestones = data.milestones || []; - state.activity = data.activity || []; - renderBoard(); - } catch (error) { - board.innerHTML = `
Backend is grumpy: ${escapeHtml(error.message)}
`; - } -} - +const state = { ideas: [], milestones: [], activity: [], activeId: null, selected: null }; +const zones = [ + { id:'must', label:'Required / Must Have', copy:'Critical for the solution. Necessary for MVP.', icon:'☆', color:'#16A34A', bg:'rgba(22,163,74,.06)', milestoneId:'now', status:'must' }, + { id:'should', label:'Should Have', copy:'Important but not critical. Adds significant value.', icon:'○', color:'#2563EB', bg:'rgba(37,99,235,.055)', milestoneId:'next', status:'should' }, + { id:'nice', label:'Nice to Have', copy:'Good to include if possible. Not essential.', icon:'♡', color:'#F59E0B', bg:'rgba(245,158,11,.07)', milestoneId:'later', status:'nice' }, + { id:'stretch', label:'Stretch Goal', copy:'Low priority ideas. Consider for the future.', icon:'ϟ', color:'#7C3AED', bg:'rgba(124,58,237,.065)', milestoneId:'later', status:'stretch' }, +]; +const $ = (sel, root=document) => root.querySelector(sel); +const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel)); +const featureDeck = $('#featureDeck'); const sortingGrid = $('#sortingGrid'); const timeline = $('#timeline'); +const detail = $('#detail'); const detailForm = $('#detailForm'); const toastEl = $('#toast'); +function escapeHtml(value){ return String(value ?? '').replace(/[&<>'"]/g, ch => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[ch])); } +function short(text, len=130){ const v=String(text||'').trim(); return v.length>len ? `${v.slice(0,len-1)}…` : v; } +function categoryOf(idea){ return (idea.labels && idea.labels[0]) || idea.sourceName || 'UI / UX'; } +function activeIdeas(){ return state.ideas.filter(i => !i.archived && (!i.status || i.status === 'inbox' || i.milestoneId === 'inbox')); } +function sortedIdeas(){ return state.ideas.filter(i => !i.archived && !activeIdeas().some(a => a.id === i.id)); } +function utilityIdeas(){ return state.ideas.filter(i => ['park','investigate'].includes(i.status)); } +async function api(path, options={}){ const res = await fetch(path,{ headers:{'Content-Type':'application/json',...(options.headers||{})}, ...options, body: options.body && typeof options.body !== 'string' ? JSON.stringify(options.body) : options.body }); const text=await res.text(); const data=text?JSON.parse(text):null; if(!res.ok) throw new Error(data?.error||res.statusText); return data; } +function toast(message){ toastEl.textContent = message; toastEl.hidden = false; setTimeout(()=>{toastEl.hidden=true}, 2400); } +function currentActive(){ const queue = activeIdeas().sort((a,b)=>(a.rank-b.rank)||(new Date(a.createdAt)-new Date(b.createdAt))); if(!state.activeId || !queue.some(i=>i.id===state.activeId)) state.activeId = queue[0]?.id || null; return queue.find(i=>i.id===state.activeId) || null; } +function renderProgress(){ const total = state.ideas.filter(i=>!i.archived).length; const sorted = sortedIdeas().length; const pct = total ? Math.round((sorted/total)*100) : 0; $('#progressCard').innerHTML = `${sorted} / ${total}features sorted
`; } +function renderDeck(){ const idea = currentActive(); if(!idea){ featureDeck.innerHTML = `
No active feature
Add a feature above and it will become the next card in the decision deck.
`; return; } featureDeck.innerHTML = `
${escapeHtml(categoryOf(idea))}

${escapeHtml(idea.title)}

${escapeHtml(short(idea.description || 'No brief yet. Open details to add one.'))}

`; $('.feature-card').addEventListener('dragstart', e => e.dataTransfer.setData('text/plain', idea.id)); $('.open-details').addEventListener('click', () => openDetail(idea.id)); $('.feature-card').addEventListener('dblclick', () => openDetail(idea.id)); } +function renderGrid(){ sortingGrid.innerHTML = zones.map(z => ``).join(''); $$('.zone').forEach(el => { el.addEventListener('click', () => placeActive(el.dataset.zone)); el.addEventListener('dragover', e => { e.preventDefault(); el.classList.add('drag-over'); }); el.addEventListener('dragleave', () => el.classList.remove('drag-over')); el.addEventListener('drop', e => { e.preventDefault(); el.classList.remove('drag-over'); const id=e.dataTransfer.getData('text/plain') || state.activeId; placeFeature(id, el.dataset.zone); }); }); } +function renderTimeline(){ const items = sortedIdeas().filter(i => !['park','investigate','remove'].includes(i.status)); const milestones = [{label:'MVP',left:8,color:'#16A34A'},{label:'Beta',left:42,color:'#2563EB'},{label:'1.0',left:66,color:'#DC2626'},{label:'Stretch Goal',left:88,color:'#7C3AED'}]; const positions = items.map((idea, idx) => { const z = zones.find(z=>z.status===idea.status) || zones[idx % zones.length]; const base = {must:13,should:47,nice:68,stretch:88}[z.status] || 20; return {idea,z,left: Math.min(96, base + (idx%5)*4)}; }); timeline.innerHTML = `
${milestones.map(m=>`${m.label}`).join('')}${positions.map((p,idx)=>``).join('')}${positions.slice(0,1).map(p=>`
${escapeHtml(short(p.idea.title,32))}${escapeHtml(categoryOf(p.idea))} · ${escapeHtml(p.z.label)}
`).join('')}
`; $$('.node', timeline).forEach(n => n.addEventListener('click', () => openDetail(n.dataset.id))); } +function renderBacklog(){ const items = utilityIdeas(); $('#backlogList').innerHTML = items.length ? items.map(i=>``).join('') : '
Clean utility lanesParked and investigate items will appear here.
'; $$('.mini-card[data-id]').forEach(c => c.addEventListener('click',()=>openDetail(c.dataset.id))); } +function render(){ renderProgress(); renderDeck(); renderGrid(); renderTimeline(); renderBacklog(); } +function replaceIdea(idea){ const idx=state.ideas.findIndex(i=>i.id===idea.id); if(idx>=0) state.ideas[idx]=idea; else state.ideas.unshift(idea); } +async function placeActive(zoneId){ const idea = currentActive(); if(!idea) return toast('Capture a feature first'); return placeFeature(idea.id, zoneId); } +async function placeFeature(id, zoneId){ const z = zones.find(z=>z.id===zoneId); const idea = state.ideas.find(i=>i.id===id); if(!z || !idea) return; const previous = {...idea}; Object.assign(idea,{status:z.status,milestoneId:z.milestoneId,rank:Date.now()%100000}); render(); try{ const updated = await api(`/api/ideas/${id}`, {method:'PATCH', body:{status:z.status,milestoneId:z.milestoneId,rank:idea.rank}}); replaceIdea(updated); state.activeId=null; render(); toast(`Placed in ${z.label}`); } catch(error){ Object.assign(idea, previous); render(); toast(error.message); } } +async function setUtility(action){ const idea=currentActive(); if(!idea) return toast('No active feature'); if(action==='remove') return archiveIdea(idea.id); const previous={...idea}; Object.assign(idea,{status:action,milestoneId:'later',rank:Date.now()%100000}); render(); try{ const updated=await api(`/api/ideas/${idea.id}`,{method:'PATCH',body:{status:action,milestoneId:'later',rank:idea.rank}}); replaceIdea(updated); state.activeId=null; render(); toast(action === 'park' ? 'Parked' : 'Sent to investigate'); } catch(error){ Object.assign(idea, previous); render(); toast(error.message); } } +function openDetail(id){ const idea=state.ideas.find(i=>i.id===id); if(!idea) return; state.selected=id; detailForm.title.value=idea.title||''; detailForm.description.value=idea.description||''; detailForm.impact.value=idea.impact??5; detailForm.effort.value=idea.effort??5; detailForm.confidence.value=idea.confidence??5; detailForm.urgency.value=idea.urgency??5; detailForm.notes.value=idea.notes||''; $('#detailCategory').textContent=categoryOf(idea); detail.classList.add('open'); detail.setAttribute('aria-hidden','false'); } +function closeDetail(){ detail.classList.remove('open'); detail.setAttribute('aria-hidden','true'); state.selected=null; } +async function archiveIdea(id=state.selected){ if(!id) return; try{ await api(`/api/ideas/${id}`,{method:'PATCH',body:{archived:true,status:'remove'}}); state.ideas = state.ideas.filter(i=>i.id!==id); closeDetail(); render(); toast('Removed'); } catch(error){ toast(error.message); } } +$('#ideaForm').addEventListener('submit', async e => { e.preventDefault(); const fd=new FormData(e.currentTarget); const payload=Object.fromEntries(fd.entries()); payload.labels=String(payload.labels||'UI / UX').split(',').map(s=>s.trim()).filter(Boolean); payload.source='human'; payload.status='inbox'; try{ const idea=await api('/api/ideas',{method:'POST',body:payload}); replaceIdea(idea); state.activeId=idea.id; e.currentTarget.reset(); $('#title').value=''; $('#description').value=''; $('#labels').value=''; render(); toast('Captured'); }catch(error){ toast(error.message); } }); +$('#skipFeature').addEventListener('click',()=>{ const q=activeIdeas(); if(q.length<2) return toast('No next feature'); const idx=q.findIndex(i=>i.id===state.activeId); state.activeId=q[(idx+1)%q.length].id; renderDeck(); }); +$('#postponeFeature').addEventListener('click',async()=>{ const idea=currentActive(); if(!idea) return; idea.rank=Date.now()%100000; state.activeId=null; render(); try{ const updated=await api(`/api/ideas/${idea.id}`,{method:'PATCH',body:{rank:idea.rank}}); replaceIdea(updated); render(); toast('Postponed'); }catch(error){ toast(error.message); await load(); } }); +$$('[data-utility]').forEach(b=>b.addEventListener('click',()=>setUtility(b.dataset.utility))); +$('#closeDetail').addEventListener('click',closeDetail); $('#archiveIdea').addEventListener('click',()=>archiveIdea()); $('#parkDetail').addEventListener('click',async()=>{ if(!state.selected) return; const id=state.selected; try{ const updated=await api(`/api/ideas/${id}`,{method:'PATCH',body:{status:'park',milestoneId:'later'}}); replaceIdea(updated); closeDetail(); render(); toast('Parked'); }catch(error){toast(error.message)} }); +detailForm.addEventListener('submit', async e => { e.preventDefault(); if(!state.selected) return; const payload=Object.fromEntries(new FormData(detailForm).entries()); try{ const idea=await api(`/api/ideas/${state.selected}`,{method:'PATCH',body:payload}); replaceIdea(idea); closeDetail(); render(); toast('Saved'); }catch(error){ toast(error.message); } }); +$('#profileToggle').addEventListener('click',()=>{ const m=$('#profileMenu'); m.hidden=!m.hidden; }); +document.addEventListener('keydown', e => { if(e.key==='/' && !['INPUT','TEXTAREA'].includes(document.activeElement.tagName)){ e.preventDefault(); $('#title').focus(); } if(e.key==='Escape') closeDetail(); }); +async function load(){ try{ const data=await api('/api/bootstrap'); state.ideas=data.ideas||[]; state.milestones=data.milestones||[]; state.activity=data.activity||[]; render(); } catch(error){ featureDeck.innerHTML=`
Backend is grumpy
${escapeHtml(error.message)}
`; renderGrid(); } } load(); diff --git a/public/index.html b/public/index.html index ff1df5f..1977887 100644 --- a/public/index.html +++ b/public/index.html @@ -3,87 +3,131 @@ - - Rank — Feature Priorities - + + Prioritix — Feature Prioritization + + -
-
-
-
-

rank.friborg.uk · feature triage

-

Drop ideas. Score fast. Drag into reality.

-

A sharp prioritization board for humans and agents. No ceremony, no rounded-corner startup soup.

-
-
-
- -
-
-
- - -
-
- - - - -
-
- - - - - -
-
-
- -
-
- - - - -
-
- - - -
-
- -
- -
- +
+
+ +
+

Rank Prioritization Studio

+

Project goal: capture, sort, and turn rough feature ideas into a visible roadmap.

+
+
+ Sorting profile + MVP (2x2) + + +
+
+
+ +
+
+ + + + + + + + + +
+
+ +
+
+ +
+
+ + +
+
+ +
+ +
+
+
+ +
+ + + +
+ +
+
+
+

Timeline view

+

Sorted features become nodes. Click a node to inspect; drag-and-drop can come later without breaking the model.

+
+
+
+
+
+ +
+ +
+
+
+ + + + + + diff --git a/public/styles.css b/public/styles.css index e6cfe5a..8d07f52 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,108 +1,19 @@ -:root { - color-scheme: dark; - --bg: #050712; - --panel: rgba(9, 14, 31, .74); - --panel-strong: rgba(13, 22, 45, .92); - --line: rgba(159, 231, 255, .22); - --line-hot: rgba(248, 255, 115, .55); - --text: #eff8ff; - --muted: #8fa4b8; - --cyan: #8cf7ff; - --yellow: #f8ff73; - --violet: #a78bfa; - --green: #6ee7b7; - --danger: #ff5e7a; - font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -* { box-sizing: border-box; } -html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--text); } -body { - background: - radial-gradient(circle at 18% 8%, rgba(140, 247, 255, .22), transparent 31rem), - radial-gradient(circle at 76% 6%, rgba(167, 139, 250, .20), transparent 34rem), - linear-gradient(135deg, #050712 0%, #09111f 46%, #03040a 100%); - overflow-x: hidden; -} -button, input, textarea, select { font: inherit; border-radius: 0; } -button { cursor: pointer; color: var(--text); background: #101a31; border: 1px solid var(--line); text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; font-weight: 800; transition: .16s ease; } -button:hover { border-color: var(--yellow); box-shadow: 0 0 28px rgba(248,255,115,.16); transform: translateY(-1px); } -input, textarea, select { width: 100%; background: rgba(1, 4, 11, .72); border: 1px solid var(--line); color: var(--text); padding: .8rem .9rem; outline: none; } -input:focus, textarea:focus, select:focus { border-color: var(--cyan); box-shadow: 0 0 0 1px rgba(140, 247, 255, .16), 0 0 30px rgba(140, 247, 255, .09); } -textarea { resize: vertical; } -label { display: grid; gap: .42rem; color: var(--muted); font-size: .78rem; text-transform: uppercase; letter-spacing: .07em; font-weight: 800; } -kbd { border: 1px solid var(--line); padding: .08rem .32rem; background: rgba(255,255,255,.05); } -.noise { pointer-events: none; position: fixed; inset: 0; opacity: .12; mix-blend-mode: screen; background-image: linear-gradient(rgba(255,255,255,.06) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px); background-size: 32px 32px; mask-image: radial-gradient(circle at 50% 0%, black, transparent 80%); } -.shell { width: min(1720px, calc(100vw - 32px)); margin: 0 auto; padding: 34px 0 60px; position: relative; } -.hero { display: grid; grid-template-columns: 1fr auto; gap: 2rem; align-items: end; margin-bottom: 24px; } -.eyebrow { color: var(--cyan); text-transform: uppercase; letter-spacing: .22em; font-size: .72rem; font-weight: 900; margin: 0 0 .7rem; } -h1 { font-size: clamp(2.6rem, 5vw, 6.8rem); line-height: .86; letter-spacing: -.07em; margin: 0; max-width: 1050px; text-transform: uppercase; } -.subcopy { max-width: 760px; color: var(--muted); font-size: 1.02rem; line-height: 1.55; } -.stats { display: grid; grid-template-columns: repeat(2, minmax(120px, 1fr)); border: 1px solid var(--line); background: var(--panel); backdrop-filter: blur(22px); min-width: 320px; } -.stat { padding: 1rem; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); } -.stat:nth-child(2n) { border-right: 0; } -.stat strong { display: block; font-size: 2.1rem; letter-spacing: -.05em; } -.stat span { color: var(--muted); text-transform: uppercase; font-size: .68rem; letter-spacing: .12em; } -.capture-panel, .toolbar, .lane, .detail, footer { border: 1px solid var(--line); background: var(--panel); backdrop-filter: blur(24px); box-shadow: 0 28px 120px rgba(0,0,0,.22); } -.capture-panel { padding: 16px; margin-bottom: 16px; position: sticky; top: 8px; z-index: 5; } -.capture-main { display: grid; grid-template-columns: 72px 1fr; align-items: center; gap: 12px; } -.capture-main input { font-size: 1.25rem; font-weight: 850; letter-spacing: -.02em; padding: 1rem; } -.capture-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 12px; margin-top: 12px; } -.score-row { display: grid; grid-template-columns: repeat(4, 1fr) 160px; gap: 12px; align-items: end; margin-top: 12px; } -.score-row button { height: 49px; background: linear-gradient(90deg, rgba(140,247,255,.18), rgba(248,255,115,.2)); border-color: var(--line-hot); } -input[type="range"] { accent-color: var(--yellow); padding: 0; height: 32px; } -.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 10px; margin-bottom: 16px; } -.tabs, .tools { display: flex; gap: 8px; align-items: center; } -.tabs button.active { background: var(--yellow); color: #050712; border-color: var(--yellow); } -.tools input { min-width: 260px; } -.board { display: grid; grid-template-columns: repeat(4, minmax(280px, 1fr)); gap: 16px; align-items: start; } -.lane { min-height: 520px; position: relative; overflow: hidden; } -.lane::before { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: var(--lane-color, var(--cyan)); box-shadow: 0 0 32px var(--lane-color, var(--cyan)); } -.lane.drag-over { border-color: var(--yellow); box-shadow: 0 0 0 1px rgba(248,255,115,.3), 0 30px 120px rgba(248,255,115,.10); } -.lane-head { padding: 16px; border-bottom: 1px solid var(--line); display: grid; gap: .45rem; } -.lane-title { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; } -.lane-title h2 { margin: 0; text-transform: uppercase; letter-spacing: -.04em; font-size: 1.55rem; } -.lane-title strong { color: var(--lane-color, var(--cyan)); font-size: 1.8rem; } -.lane-head p { margin: 0; color: var(--muted); font-size: .85rem; min-height: 2.4em; } -.cards { display: grid; gap: 10px; padding: 12px; } -.card { border: 1px solid rgba(255,255,255,.12); background: linear-gradient(145deg, rgba(255,255,255,.07), rgba(255,255,255,.025)); padding: 12px; display: grid; gap: 10px; cursor: grab; position: relative; } -.card:hover { border-color: var(--cyan); background: linear-gradient(145deg, rgba(140,247,255,.11), rgba(255,255,255,.03)); } -.card:active { cursor: grabbing; } -.card.dragging { opacity: .35; } -.card-top { display: flex; justify-content: space-between; gap: 10px; align-items: start; } -.card h3 { margin: 0; font-size: 1rem; line-height: 1.15; letter-spacing: -.02em; } -.score { display: grid; place-items: center; min-width: 48px; height: 38px; border: 1px solid var(--line-hot); color: var(--yellow); font-weight: 950; background: rgba(248,255,115,.06); } -.card p { margin: 0; color: var(--muted); font-size: .84rem; line-height: 1.35; } -.meta { display: flex; flex-wrap: wrap; gap: 6px; } -.pill { border: 1px solid rgba(255,255,255,.13); padding: .2rem .38rem; color: var(--muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; } -.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid rgba(255,255,255,.1); padding-top: 8px; gap: 6px; } -.metrics span { color: var(--muted); font-size: .64rem; text-transform: uppercase; } -.metrics b { display: block; color: var(--text); font-size: .9rem; } -.detail { position: fixed; z-index: 20; top: 0; right: 0; height: 100vh; width: min(520px, 100vw); padding: 18px; transform: translateX(105%); transition: transform .18s ease; } -.detail.open { transform: translateX(0); } -.detail-head { display: flex; justify-content: space-between; align-items: center; } -.detail-head button { width: 40px; height: 40px; font-size: 1.4rem; } -.detail form { display: grid; gap: 12px; } -.detail-title { font-size: 1.55rem; font-weight: 900; letter-spacing: -.04em; } -.detail-sliders { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } -.detail-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } -#archiveIdea { border-color: rgba(255,94,122,.5); color: #ffd5dd; } -footer { margin-top: 16px; padding: 14px; display: grid; grid-template-columns: 1fr auto; gap: 1rem; color: var(--muted); font-size: .82rem; } -#activity { display: flex; gap: 8px; flex-wrap: wrap; } -.activity-item { border: 1px solid rgba(255,255,255,.1); padding: .32rem .48rem; } -.empty { border: 1px dashed rgba(255,255,255,.14); color: var(--muted); padding: 1.2rem; text-align: center; } -.toast { position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%); z-index: 50; background: #f8ff73; color: #050712; padding: .8rem 1rem; border: 1px solid #fff; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; } -@media (max-width: 1180px) { - .hero { grid-template-columns: 1fr; } - .stats { min-width: 0; } - .board { grid-template-columns: repeat(2, minmax(280px, 1fr)); } - .capture-grid, .score-row { grid-template-columns: 1fr 1fr; } -} -@media (max-width: 720px) { - .shell { width: min(100vw - 18px, 100%); padding-top: 16px; } - .board, .capture-grid, .score-row, .capture-main, footer { grid-template-columns: 1fr; } - .toolbar { align-items: stretch; flex-direction: column; } - .tabs, .tools { width: 100%; overflow-x: auto; } - .tools input { min-width: 180px; } - .capture-panel { position: relative; top: auto; } +:root{ + color-scheme: light; + --navy-950:#061B33; --navy-900:#071E3A; --navy-800:#09264b; + --blue-600:#0B63F6; --blue-500:#2563EB; --green-600:#16A34A; + --amber-500:#F59E0B; --purple-600:#7C3AED; --red-600:#DC2626; + --slate-900:#101828; --slate-700:#344054; --slate-600:#667085; --slate-400:#98A2B3; + --page:#F6F8FB; --surface:#FFFFFF; --border:#D9E2EC; --muted:#E6ECF2; + --shadow:0 18px 50px rgba(16,24,40,.10); --shadow-soft:0 6px 18px rgba(16,24,40,.06); + --radius:6px; --font-ui:Inter,"SF Pro Display","SF Pro Text","Segoe UI",system-ui,sans-serif; + font-family:var(--font-ui); } +*{box-sizing:border-box} html,body{margin:0;min-height:100%;background:var(--page);color:var(--slate-900)} +body{background:radial-gradient(circle at 20% 0%,#fff 0,#f4f7fc 36rem,transparent 37rem),linear-gradient(135deg,#F6F8FB 0%,#EEF3FA 100%)} +button,input,textarea{font:inherit} button{cursor:pointer} a{color:inherit;text-decoration:none} +.app-shell{display:grid;grid-template-columns:104px 1fr;min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;background:linear-gradient(180deg,var(--navy-950),#031223);color:white;display:flex;flex-direction:column;align-items:center;padding:20px 8px;border-right:1px solid rgba(255,255,255,.08);box-shadow:16px 0 40px rgba(6,27,51,.12);z-index:10}.brand-mark{width:52px;height:52px;display:grid;place-items:center;margin-bottom:34px;border-radius:10px;background:linear-gradient(135deg,#45B8FF,#6D3BFF);font-weight:900;font-size:28px;letter-spacing:-.12em;box-shadow:0 14px 34px rgba(11,99,246,.32)} +.sidebar nav{display:grid;gap:10px;width:100%}.sidebar a{display:grid;place-items:center;gap:6px;padding:13px 6px;border-radius:6px;color:#D9E8FF;font-size:12px}.sidebar a span{font-size:25px;line-height:1}.sidebar a.active{background:linear-gradient(135deg,#0B63F6,#4E2AD8);color:#fff;box-shadow:0 10px 28px rgba(11,99,246,.30)}.collapse{margin-top:auto;background:transparent;border:0;color:white;font-size:28px;opacity:.9} +.workspace{padding:22px 32px 42px;max-width:1440px;width:100%;margin:0 auto}.topbar{display:grid;grid-template-columns:auto 1fr minmax(220px,270px) 190px;align-items:center;gap:22px;background:rgba(255,255,255,.72);backdrop-filter:blur(18px);border:1px solid var(--border);box-shadow:var(--shadow-soft);border-radius:var(--radius);padding:18px 22px;margin-bottom:18px}.menu-button{width:48px;height:48px;border:1px solid var(--border);border-radius:6px;background:var(--navy-950);color:white;font-size:24px}.project-title h1{font-size:26px;letter-spacing:-.04em;margin:0 0 4px}.project-title p{margin:0;color:var(--slate-700);font-size:14px}.profile-card,.progress-card{position:relative;background:white;border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px;box-shadow:var(--shadow-soft)}.profile-card span,.progress-card span{display:block;color:var(--slate-600);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.profile-card strong,.progress-card strong{font-size:16px}.profile-card>button{position:absolute;right:10px;top:22px;border:0;background:transparent;font-size:20px;color:var(--slate-900)}.profile-menu{position:absolute;top:64px;left:0;right:0;background:#fff;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow);padding:8px;z-index:20}.profile-menu button{display:block;width:100%;text-align:left;border:0;background:white;padding:9px 10px;border-radius:4px}.profile-menu .selected{background:#F0E9FF;color:var(--purple-600);font-weight:800}.progressbar{height:5px;background:var(--muted);border-radius:999px;margin-top:10px;overflow:hidden}.progressbar i{display:block;height:100%;background:linear-gradient(90deg,var(--purple-600),var(--blue-600));border-radius:999px}.capture-strip{background:white;border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow-soft);padding:12px;margin-bottom:18px}.capture-strip form{display:grid;grid-template-columns:1.4fr 1.4fr .8fr 132px;gap:10px;align-items:end}.capture-strip label,.detail label{display:grid;gap:6px;color:var(--slate-600);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em}.capture-strip input,.detail input,.detail textarea{width:100%;border:1px solid var(--border);border-radius:4px;background:#FBFCFF;color:var(--slate-900);padding:12px 13px;outline:none}.capture-strip input:focus,.detail input:focus,.detail textarea:focus{border-color:var(--blue-600);box-shadow:0 0 0 3px rgba(11,99,246,.10)}.capture-strip button,.deck-actions button,.utility-row button,.detail-actions button,.zoom-controls button{border:1px solid var(--border);background:#fff;color:var(--slate-900);border-radius:4px;box-shadow:var(--shadow-soft);padding:12px 16px;font-weight:800}.capture-strip button{background:var(--navy-950);color:white;height:43px}.sorting-layout{display:grid;grid-template-columns:minmax(330px,470px) 1fr;gap:28px;align-items:start;margin-top:18px}.section-label{text-transform:uppercase;letter-spacing:.06em;color:var(--navy-900);font-size:13px;font-weight:900;margin-bottom:12px}.active-column{padding:10px 0}.feature-deck{position:relative;min-height:250px;display:grid;place-items:center}.feature-card{position:relative;width:min(390px,100%);min-height:185px;background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow);padding:22px 22px 18px;display:grid;gap:12px;z-index:3;transition:.18s ease}.feature-card::before,.feature-card::after{content:"";position:absolute;inset:14px -18px -14px 18px;background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);z-index:-1}.feature-card::before{border-right:6px solid var(--amber-500);transform:rotate(-1.4deg)}.feature-card::after{inset:28px -34px -28px 34px;border-right:6px solid var(--green-600);transform:rotate(1.5deg);z-index:-2}.feature-meta,.detail-head>div{display:flex;align-items:center;gap:9px;color:var(--slate-700);font-size:13px;font-weight:700}.category-dot{width:12px;height:12px;border-radius:50%;background:var(--purple-600);box-shadow:0 0 0 4px rgba(124,58,237,.10)}.feature-card h2{margin:0;font-size:22px;letter-spacing:-.03em}.feature-card p{margin:0;color:var(--slate-700);line-height:1.45}.open-details{justify-self:end;width:40px;height:40px;border:1px solid var(--border);border-radius:5px;background:#F8FAFF;color:var(--navy-900);font-size:20px}.empty-deck{background:white;border:1px dashed var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:28px;text-align:center;color:var(--slate-600);max-width:390px}.deck-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:min(390px,100%);margin:4px auto 0}.sorting-grid{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:14px}.zone{min-height:188px;border:1.5px dashed var(--zone-color);background:linear-gradient(135deg,rgba(255,255,255,.94),var(--zone-bg));border-radius:6px;display:grid;place-items:center;text-align:center;padding:22px;transition:.16s ease}.zone:hover,.zone.drag-over{transform:translateY(-2px);box-shadow:var(--shadow);border-style:solid}.zone-icon{font-size:42px;color:var(--zone-color);line-height:1}.zone strong{display:block;color:var(--zone-color);font-size:18px;margin:8px 0}.zone span{display:block;color:var(--slate-700);font-size:14px;line-height:1.35}.drop-hint{margin-top:18px;display:inline-grid;place-items:center;width:42px;height:42px;border:1px dashed var(--border);border-radius:6px;color:var(--slate-600);background:white}.utility-row{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--border);border-radius:6px;background:white;box-shadow:var(--shadow-soft);overflow:hidden;margin:18px 0}.utility-row button{display:grid;grid-template-columns:42px 1fr;grid-template-rows:auto auto;text-align:left;border:0;border-right:1px solid var(--border);border-radius:0;box-shadow:none;background:white;padding:18px}.utility-row button:last-child{border-right:0}.utility-row span{grid-row:1/3;font-size:28px;color:var(--navy-900)}.utility-row strong{font-size:15px}.utility-row small{color:var(--slate-600);font-size:12px;line-height:1.35}.timeline-panel,.backlog-panel{background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:18px;margin-top:18px}.timeline-head{display:flex;justify-content:space-between;gap:16px;align-items:start}.timeline-head h2{font-size:18px;margin:0 0 4px}.timeline-head p{margin:0;color:var(--slate-600);font-size:13px}.zoom-controls{display:flex;gap:6px}.zoom-controls button{padding:9px 13px}.timeline{position:relative;min-height:145px;padding:54px 26px 26px}.timeline-line{position:relative;height:2px;background:#AAB4C0}.milestone-label{position:absolute;top:-30px;transform:translateX(-50%);font-weight:800;font-size:13px;color:var(--milestone-color)}.milestone-tick{position:absolute;top:-17px;height:34px;border-left:3px solid var(--milestone-color)}.node{position:absolute;top:-13px;transform:translateX(-50%);width:28px;height:28px;border-radius:50%;border:3px solid var(--node-color);background:white;display:grid;place-items:center;font-size:11px;font-weight:900;color:var(--node-color);box-shadow:0 3px 10px rgba(16,24,40,.12)}.node-card{position:absolute;top:42px;transform:translateX(-50%);min-width:230px;max-width:300px;background:white;border:1px solid var(--border);border-radius:6px;box-shadow:var(--shadow-soft);padding:11px}.node-card strong{display:block;font-size:13px}.node-card span{color:var(--slate-600);font-size:12px}.backlog-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:10px}.mini-card{border:1px solid var(--border);border-radius:6px;background:#FBFCFF;padding:12px;display:grid;gap:7px}.mini-card strong{font-size:14px}.mini-card span{font-size:12px;color:var(--slate-600)}.chip{background:#EEE6FF;color:var(--purple-600);padding:6px 10px;border-radius:8px;font-size:12px;font-weight:900}.detail{position:fixed;right:20px;top:20px;bottom:20px;width:min(520px,calc(100vw - 40px));z-index:50;background:white;border:1px solid var(--border);border-radius:12px;box-shadow:0 28px 90px rgba(16,24,40,.20);padding:22px;transform:translateX(calc(100% + 40px));transition:.18s ease;overflow:auto}.detail.open{transform:translateX(0)}.detail form{display:grid;gap:14px}.detail-head{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center}.detail-head button{border:0;background:#F5F7FB;border-radius:6px;width:36px;height:36px;font-size:24px}.detail-title{font-size:22px!important;font-weight:850;letter-spacing:-.03em}.detail-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}.detail-actions{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}.detail-actions .danger{border-color:#F8C9C9;color:var(--red-600);background:#FFF6F6}.toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:90;background:var(--navy-950);color:white;border-radius:6px;padding:12px 16px;box-shadow:var(--shadow);font-weight:800}.loading{color:var(--slate-600);padding:20px;text-align:center} +@media(max-width:1050px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}.workspace{padding:14px}.topbar{grid-template-columns:auto 1fr}.profile-card,.progress-card{grid-column:1/-1}.sorting-layout{grid-template-columns:1fr}.capture-strip form{grid-template-columns:1fr 1fr}.capture-strip button{grid-column:1/-1}.utility-row{grid-template-columns:1fr}.utility-row button{border-right:0;border-bottom:1px solid var(--border)}.utility-row button:last-child{border-bottom:0}} +@media(max-width:680px){.workspace{padding:10px}.topbar{padding:12px;gap:12px}.project-title h1{font-size:21px}.sorting-grid{grid-template-columns:1fr 1fr;gap:9px}.zone{min-height:142px;padding:12px}.zone-icon{font-size:30px}.zone strong{font-size:14px}.zone span{font-size:12px}.capture-strip form,.deck-actions,.detail-grid,.detail-actions{grid-template-columns:1fr}.timeline{overflow-x:auto}.timeline-line{min-width:760px}.feature-card{min-height:160px}.feature-card::before,.feature-card::after{display:none}}