-
Notifications
You must be signed in to change notification settings - Fork 0
feat(frontend): add release links and mobile board layout #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -57,9 +57,10 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const setSelectedLabelIds = (ids: string[]) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLabelFilter({ projectId: selectedProjectId, ids }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // URL-derived project key (slug or id), captured once on mount and resolved | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // against the project list as soon as it loads. Cleared after resolution. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // against the project list as soon as it loads. Release links use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // /projects/:key/releases/:release and apply the board release filter. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [urlProjectKey, setUrlProjectKey] = useState<string | null>(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m = window.location.pathname.match(/^\/projects\/([^/]+)$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m = window.location.pathname.match(/^\/projects\/([^/]+)(?:\/releases\/([^/]+))?$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!m) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decodeURIComponent(m[1]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -69,6 +70,15 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return m[1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [urlReleaseTarget, setUrlReleaseTarget] = useState<string | null>(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m = window.location.pathname.match(/^\/projects\/([^/]+)\/releases\/([^/]+)$/); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!m) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return decodeURIComponent(m[2]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return m[2]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [settingsProjectId, setSettingsProjectId] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [search, setSearch] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [syncing, setSyncing] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -82,6 +92,7 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [viewPreset, setViewPreset] = usePersistedState<BoardViewPreset>('baton.board.viewPreset', 'all'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [releaseTarget, setReleaseTarget] = usePersistedState<string>('baton.board.releaseTarget', ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [paletteOpen, setPaletteOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [showArchived, setShowArchived] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [registryOpen, setRegistryOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [viewsOpen, setViewsOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -122,6 +133,11 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUrlProjectKey(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (found) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedProjectId(found.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (urlReleaseTarget !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setReleaseTarget(urlReleaseTarget); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setBoardGrouping('release'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUrlReleaseTarget(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -130,9 +146,9 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (persistedStillExists) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const first = projects.find(p => p.status === 'active') ?? projects[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSelectedProjectId(first ? first.id : null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [projects, selectedProjectId, setSelectedProjectId, urlProjectKey]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [projects, selectedProjectId, setSelectedProjectId, setReleaseTarget, urlProjectKey, urlReleaseTarget]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Keep the URL in sync with the selected project, except while a /tasks, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Keep the URL in sync with the selected project/release, except while a /tasks, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // /users, or /agents deep link is active or still loading. Use replaceState | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so the back button still goes back to wherever the user came from. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| React.useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -143,11 +159,15 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (/^\/(tasks|users|agents)\//.test(window.location.pathname)) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const project = projects.find(p => p.id === selectedProjectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!project) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const href = `/projects/${project.slug || project.id}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const projectHref = `/projects/${project.slug || project.id}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const release = releaseTarget.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const href = release | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? `${projectHref}/releases/${encodeURIComponent(release)}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : projectHref; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.location.pathname !== href) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.history.replaceState(null, '', href); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [selectedProjectId, projects, linkedTask, linkedResource, urlProjectKey]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [selectedProjectId, projects, linkedTask, linkedResource, urlProjectKey, releaseTarget]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global Cmd+K / Ctrl+K to open the command palette. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| React.useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -232,6 +252,7 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenRegistry={() => setRegistryOpen(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenViews={() => setViewsOpen(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenPalette={() => setPaletteOpen(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenSidebar={() => setMobileSidebarOpen(true)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| density={density} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onToggleDensity={() => setDensity(d => d === 'comfortable' ? 'compact' : 'comfortable')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rootOnly={rootOnly} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -245,21 +266,44 @@ function AppShell() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-1 overflow-hidden"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {sidebarOpen && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Sidebar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projects={visibleProjects} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| archivedCount={projects.length - projects.filter(p => p.status === 'active').length} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showArchived={showArchived} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onToggleArchived={setShowArchived} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedId={selectedProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSelect={setSelectedProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedLabelIds={selectedLabelIds} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onLabelIdsChange={setSelectedLabelIds} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCreate={async (data) => { await createProject(data); }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onDelete={deleteProject} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenSettings={setSettingsProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width={sidebarWidth} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onResize={setSidebarWidth} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="hidden md:flex flex-shrink-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Sidebar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projects={visibleProjects} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| archivedCount={projects.length - projects.filter(p => p.status === 'active').length} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showArchived={showArchived} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onToggleArchived={setShowArchived} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedId={selectedProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSelect={setSelectedProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedLabelIds={selectedLabelIds} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onLabelIdsChange={setSelectedLabelIds} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCreate={async (data) => { await createProject(data); }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onDelete={deleteProject} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenSettings={setSettingsProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width={sidebarWidth} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onResize={setSidebarWidth} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {mobileSidebarOpen && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="md:hidden fixed inset-0 z-50 bg-black/60" onClick={() => setMobileSidebarOpen(false)}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="h-full w-[88vw] max-w-[360px]" onClick={e => e.stopPropagation()}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Sidebar | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projects={visibleProjects} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| archivedCount={projects.length - projects.filter(p => p.status === 'active').length} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showArchived={showArchived} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onToggleArchived={setShowArchived} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedId={selectedProjectId} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSelect={(id) => { setSelectedProjectId(id); setMobileSidebarOpen(false); }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedLabelIds={selectedLabelIds} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onLabelIdsChange={setSelectedLabelIds} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onCreate={async (data) => { await createProject(data); }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onDelete={deleteProject} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onOpenSettings={(id) => { setSettingsProjectId(id); setMobileSidebarOpen(false); }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width={360} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onResize={() => {}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+288
to
+306
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Fixed The drawer wrapper is 🐛 Suggested fix- <div className="h-full w-[88vw] max-w-[360px]" onClick={e => e.stopPropagation()}>
+ <div className="h-full w-[88vw] max-w-[360px] overflow-hidden" onClick={e => e.stopPropagation()}>
<Sidebar
...
- width={360}
+ width={Math.min(360, Math.round(window.innerWidth * 0.88))}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <main className="flex-1 overflow-hidden flex flex-col"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <KanbanBoard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ interface Props { | |
| onOpenRegistry: () => void; | ||
| onOpenViews: () => void; | ||
| onOpenPalette: () => void; | ||
| onOpenSidebar: () => void; | ||
| density: 'comfortable' | 'compact'; | ||
| onToggleDensity: () => void; | ||
| rootOnly: boolean; | ||
|
|
@@ -40,6 +41,7 @@ export function Header({ | |
| onOpenRegistry, | ||
| onOpenViews, | ||
| onOpenPalette, | ||
| onOpenSidebar, | ||
| density, | ||
| onToggleDensity, | ||
| rootOnly, | ||
|
|
@@ -65,6 +67,9 @@ export function Header({ | |
| const projectHref = selectedProject | ||
| ? `/projects/${selectedProject.slug || selectedProject.id}` | ||
| : null; | ||
| const releaseHref = projectHref && releaseTarget.trim() | ||
| ? `${projectHref}/releases/${encodeURIComponent(releaseTarget.trim())}` | ||
| : null; | ||
|
|
||
| const copyProjectLink = async () => { | ||
| if (!projectHref) return; | ||
|
|
@@ -76,6 +81,16 @@ export function Header({ | |
| } | ||
| }; | ||
|
|
||
| const copyReleaseLink = async () => { | ||
| if (!releaseHref) return; | ||
| try { | ||
| await navigator.clipboard.writeText(`${window.location.origin}${releaseHref}`); | ||
| showToast('Release link copied', 'success'); | ||
| } catch { | ||
| showToast('Copy failed', 'error'); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (!accountOpen) return; | ||
| const handler = (e: MouseEvent) => { | ||
|
|
@@ -99,15 +114,25 @@ export function Header({ | |
| }, [pickerOpen]); | ||
|
|
||
| return ( | ||
| <header className="h-12 border-b border-[#1a1a1a] flex items-center gap-3 px-4 bg-[#080808] flex-shrink-0"> | ||
| <div className="flex items-center gap-2 mr-2"> | ||
| <header className="min-h-12 md:h-12 border-b border-[#1a1a1a] flex flex-wrap md:flex-nowrap items-center gap-2 md:gap-3 px-3 md:px-4 py-2 md:py-0 bg-[#080808] flex-shrink-0"> | ||
| <button | ||
| type="button" | ||
| onClick={onOpenSidebar} | ||
| aria-label="Open projects" | ||
| title="Projects" | ||
| className="md:hidden text-zinc-500 hover:text-amber-400 text-lg px-1" | ||
| > | ||
| ☰ | ||
| </button> | ||
|
|
||
| <div className="flex items-center gap-2 mr-1 md:mr-2"> | ||
| <span className="text-amber-500 text-lg">🔥</span> | ||
| <span className="text-sm font-semibold text-zinc-200 tracking-tight">baton</span> | ||
| <span className="hidden sm:inline text-sm font-semibold text-zinc-200 tracking-tight">baton</span> | ||
| </div> | ||
|
|
||
| <div className="w-px h-4 bg-[#2a2a2a]" /> | ||
| <div className="hidden md:block w-px h-4 bg-[#2a2a2a]" /> | ||
|
|
||
| <div ref={pickerRef} className="relative flex items-center"> | ||
| <div ref={pickerRef} className="relative flex items-center min-w-0 flex-1 md:flex-none"> | ||
| {selectedProject && projectHref ? ( | ||
| <a | ||
| href={projectHref} | ||
|
|
@@ -119,7 +144,7 @@ export function Header({ | |
| // clicking just keeps the user on the same view. | ||
| }} | ||
| title={`Copy or open ${selectedProject.name}`} | ||
| className="text-sm text-zinc-300 hover:text-amber-400 transition-colors px-1 truncate max-w-[16rem]" | ||
| className="text-sm text-zinc-300 hover:text-amber-400 transition-colors px-1 truncate max-w-[11rem] sm:max-w-[16rem]" | ||
| > | ||
| {selectedProject.icon && <span className="mr-1">{selectedProject.icon}</span>} | ||
| {selectedProject.name} | ||
|
|
@@ -172,7 +197,7 @@ export function Header({ | |
| )} | ||
| </div> | ||
|
|
||
| <div className="flex-1 max-w-xs ml-2"> | ||
| <div className="order-2 md:order-none basis-full md:basis-auto flex-1 md:max-w-xs md:ml-2"> | ||
| <input | ||
| value={search} | ||
| onChange={e => onSearch(e.target.value)} | ||
|
|
@@ -191,13 +216,13 @@ export function Header({ | |
| <span aria-hidden="true">⌘</span>K | ||
| </button> | ||
|
|
||
| <div className="flex-1" /> | ||
| <div className="hidden md:block flex-1" /> | ||
|
|
||
| <button | ||
| onClick={onToggleRootOnly} | ||
| title={rootOnly ? 'Show all tasks' : 'Show root tasks with children nested'} | ||
| aria-pressed={rootOnly} | ||
| className={`text-xs px-2 py-1.5 rounded transition-colors ${ | ||
| className={`hidden sm:inline-flex text-xs px-2 py-1.5 rounded transition-colors ${ | ||
| rootOnly ? 'text-amber-400 bg-amber-500/10' : 'text-zinc-500 hover:text-amber-400 hover:bg-amber-500/5' | ||
| }`} | ||
| > | ||
|
|
@@ -209,7 +234,7 @@ export function Header({ | |
| onChange={e => onGroupingChange(e.target.value as BoardGrouping)} | ||
| title="Board grouping" | ||
| aria-label="Board grouping" | ||
| className="bg-[#111] border border-[#1f1f1f] rounded text-xs text-zinc-400 px-2 py-1.5 outline-none hover:border-amber-500/30" | ||
| className="hidden md:block bg-[#111] border border-[#1f1f1f] rounded text-xs text-zinc-400 px-2 py-1.5 outline-none hover:border-amber-500/30" | ||
| > | ||
| <option value="priority">Priority</option> | ||
| <option value="tags">Tags</option> | ||
|
|
@@ -221,7 +246,7 @@ export function Header({ | |
| onChange={e => onViewPresetChange(e.target.value as BoardViewPreset)} | ||
| title="Saved view" | ||
| aria-label="Saved view" | ||
| className="bg-[#111] border border-[#1f1f1f] rounded text-xs text-zinc-400 px-2 py-1.5 outline-none hover:border-amber-500/30" | ||
| className="hidden md:block bg-[#111] border border-[#1f1f1f] rounded text-xs text-zinc-400 px-2 py-1.5 outline-none hover:border-amber-500/30" | ||
| > | ||
| <option value="all">All</option> | ||
| <option value="open-high">Open high</option> | ||
|
|
@@ -236,18 +261,26 @@ export function Header({ | |
| placeholder="release" | ||
| title="Filter by release label, e.g. 1.2 or v1.2" | ||
| aria-label="Release target" | ||
| className="w-24 bg-[#111] border border-[#1f1f1f] rounded text-xs text-zinc-300 placeholder-zinc-600 px-2 py-1.5 outline-none focus:border-amber-500/30" | ||
| className="w-20 sm:w-24 bg-[#111] border border-[#1f1f1f] rounded text-xs text-zinc-300 placeholder-zinc-600 px-2 py-1.5 outline-none focus:border-amber-500/30" | ||
| /> | ||
| <button | ||
| type="button" | ||
| onClick={copyReleaseLink} | ||
| disabled={!releaseHref} | ||
| title={releaseHref ? 'Copy release link' : 'Enter a release to copy a link'} | ||
| aria-label="Copy release link" | ||
| className="text-zinc-600 hover:text-amber-400 disabled:opacity-30 disabled:hover:text-zinc-600 px-1 text-xs" | ||
| >🔗</button> | ||
|
|
||
| <button | ||
| onClick={onOpenRegistry} | ||
| className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| className="hidden md:flex items-center gap-1.5 text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| > | ||
| Registry | ||
| </button> | ||
| <button | ||
| onClick={onOpenViews} | ||
| className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| className="hidden md:flex items-center gap-1.5 text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| > | ||
| Views | ||
| </button> | ||
|
|
@@ -256,7 +289,7 @@ export function Header({ | |
| <button | ||
| onClick={onSync} | ||
| disabled={syncing} | ||
| className="flex items-center gap-1.5 text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors disabled:opacity-40" | ||
| className="hidden md:flex items-center gap-1.5 text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors disabled:opacity-40" | ||
| > | ||
| <span className={syncing ? 'animate-spin' : ''}>⟳</span> | ||
| {syncing ? 'Syncing…' : 'Sync YAML'} | ||
|
|
@@ -267,15 +300,15 @@ export function Header({ | |
| onClick={onToggleDensity} | ||
| title={density === 'comfortable' ? 'Switch to compact density' : 'Switch to comfortable density'} | ||
| aria-label="Toggle density" | ||
| className="text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| className="hidden md:block text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| > | ||
| {density === 'comfortable' ? '≡' : '☰'} | ||
| </button> | ||
| <button | ||
| onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} | ||
| title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'} | ||
| aria-label="Toggle theme" | ||
| className="text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| className="hidden md:block text-xs text-zinc-500 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors" | ||
| > | ||
| {theme === 'dark' ? '☼' : '☾'} | ||
| </button> | ||
|
|
@@ -284,7 +317,7 @@ export function Header({ | |
| <div ref={accountRef} className="relative"> | ||
| <button | ||
| onClick={() => setAccountOpen(o => !o)} | ||
| className="text-xs text-zinc-400 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors max-w-[10rem] truncate" | ||
| className="hidden md:block text-xs text-zinc-400 hover:text-amber-400 px-2 py-1.5 rounded hover:bg-amber-500/5 transition-colors max-w-[10rem] truncate" | ||
|
Comment on lines
318
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When auth is enabled (or claims are present), this hidden button is the only control that opens the account menu and reaches the Useful? React with 👍 / 👎. |
||
| > | ||
| {accountLabel} | ||
| </button> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Opening a copied
/projects/:project/releases/:releaseURL only overwritesreleaseTarget/grouping here; the persistedbaton.board.viewPresetremains active. If the recipient previously selectedBlocked,Open high, etc., the shared release link shows only that subset (and on mobile the preset selector is hidden), so copied release URLs are not reliably shareable unless the preset is reset or encoded in the URL.Useful? React with 👍 / 👎.