feat(apps): publish button in app toolbar + published links in detail

- toolbar (local apps): 'Publish' button; turns into a green 'Published'
  badge-check once published (click opens the detail panel)
- detail Source section: clickable repo + release links for published apps
This commit is contained in:
Gagan 2026-07-06 20:12:25 +05:30
parent 8e4f234d69
commit 5e0fc059e7
2 changed files with 58 additions and 4 deletions

View file

@ -180,8 +180,20 @@ export function AppDetail({ folder, onClose }: { folder: string; onClose: () =>
<InfoRow k="Installed" v={new Date(app.install.installedAt).toLocaleString()} />
{app.install.updatedAt && <InfoRow k="Updated" v={new Date(app.install.updatedAt).toLocaleString()} />}
</>
) : app.publish ? (
<>
<p className="text-muted-foreground">Local app published to the Rowboat catalog.</p>
<InfoRow k="Repository" v={app.publish.repo} mono link={`https://github.com/${app.publish.repo}`} />
{app.publish.lastPublishedVersion && (
<InfoRow
k="Published"
v={`v${app.publish.lastPublishedVersion}`}
link={`https://github.com/${app.publish.repo}/releases/tag/v${app.publish.lastPublishedVersion}`}
/>
)}
</>
) : (
<p className="text-muted-foreground">Local app created on this machine{app.publish ? `, published as ${app.publish.repo}` : ''}.</p>
<p className="text-muted-foreground">Local app created on this machine.</p>
)}
<div className="flex flex-wrap gap-2 pt-1">
{app.kind === 'installed' && app.install?.repo && (
@ -268,11 +280,22 @@ export function AppDetail({ folder, onClose }: { folder: string; onClose: () =>
)
}
function InfoRow({ k, v, mono }: { k: string; v: string; mono?: boolean }) {
function InfoRow({ k, v, mono, link }: { k: string; v: string; mono?: boolean; link?: string }) {
return (
<div className="flex items-baseline gap-2">
<span className="w-28 shrink-0 text-xs text-muted-foreground">{k}</span>
<span className={`min-w-0 truncate ${mono ? 'font-mono text-xs' : ''}`}>{v}</span>
{link ? (
<a
href={link}
target="_blank"
rel="noreferrer"
className={`min-w-0 truncate text-primary hover:underline ${mono ? 'font-mono text-xs' : ''}`}
>
{v}
</a>
) : (
<span className={`min-w-0 truncate ${mono ? 'font-mono text-xs' : ''}`}>{v}</span>
)}
</div>
)
}

View file

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'
import { ArrowLeft, ExternalLink, Info, RotateCw } from 'lucide-react'
import { ArrowLeft, BadgeCheck, ExternalLink, Info, RotateCw, UploadCloud } from 'lucide-react'
import type { rowboatApp } from '@x/shared'
import { appOpened } from '@/lib/analytics'
import { AppDetail } from '@/components/apps/app-detail'
import { PublishDialog } from '@/components/apps/publish-dialog'
// Full-height iframe on the app's own origin (spec §6.6). No sandbox attr —
// per-app browser origins are the isolation boundary. Toolbar: back, reload,
@ -11,6 +12,7 @@ import { AppDetail } from '@/components/apps/app-detail'
export function AppFrame({ app, onBack }: { app: rowboatApp.AppSummary; onBack: () => void }) {
const [reloadNonce, setReloadNonce] = useState(0)
const [showDetail, setShowDetail] = useState(false)
const [showPublish, setShowPublish] = useState(false)
// Load watchdog: if the iframe hasn't fired `load` within the deadline,
// surface a visible retry state instead of a silent blank pane.
const [loadState, setLoadState] = useState<'loading' | 'ok' | 'stuck'>('loading')
@ -62,6 +64,27 @@ export function AppFrame({ app, onBack }: { app: rowboatApp.AppSummary; onBack:
>
<ExternalLink className="size-4" />
</button>
{app.kind === 'local' && (
app.publish ? (
<button
type="button"
title={`Published as ${app.publish.repo} — view details`}
onClick={() => setShowDetail(true)}
className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium text-green-600 hover:bg-green-500/10 dark:text-green-500"
>
<BadgeCheck className="size-4" /> Published
</button>
) : (
<button
type="button"
title="Publish this app"
onClick={() => setShowPublish(true)}
className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
>
<UploadCloud className="size-4" /> Publish
</button>
)
)}
<button
type="button"
title="App details"
@ -100,6 +123,14 @@ export function AppFrame({ app, onBack }: { app: rowboatApp.AppSummary; onBack:
</div>
)}
</div>
{showPublish && (
<PublishDialog
folder={app.folder}
appName={title}
onClose={() => setShowPublish(false)}
onPublished={() => setShowDetail(true)}
/>
)}
</div>
)
}