diff --git a/website/snap/src/App.tsx b/website/snap/src/App.tsx index b5acd02be9..b253e28744 100644 --- a/website/snap/src/App.tsx +++ b/website/snap/src/App.tsx @@ -1,11 +1,12 @@ import { useEffect, useMemo, useState } from 'react'; +import { createPortal } from 'react-dom'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { - AppWindow, ArrowLeft, ArrowRight, BadgeCheck, Boxes, Check, ChevronDown, ChevronRight, - CircleGauge, Cloud, Database, Download, ExternalLink, GripVertical, HardDriveDownload, - LayoutGrid, Link2, LockKeyhole, LogOut, Menu, Package, Play, Plus, Search, Server, Settings, ShieldCheck, - Sparkles, Star, Store, UploadCloud, Users, X + AppWindow, ArrowLeft, ArrowRight, BadgeCheck, BookOpen, Boxes, Bug, Check, ChevronDown, ChevronRight, + CircleDollarSign, CircleGauge, Cloud, Database, Download, ExternalLink, FileCode2, Globe2, GripVertical, + HardDriveDownload, LayoutGrid, Link2, LockKeyhole, LogOut, Mail, Menu, MessageCircle, Package, Play, Plus, + Search, Server, Settings, ShieldCheck, Sparkles, Star, Store, UploadCloud, Users, X } from 'lucide-react'; import { abortPackageUpload, createUpstream, createVersion, finalizePackage, getAdminState, getAppDetails, getCatalog, getStorefront, requestPackageUpload, saveUpstreams, searchApps, uploadPackagePart } from './api'; import type { AdminState, StoreApp, StorefrontData, Upstream } from './types'; @@ -157,12 +158,28 @@ function linkDestination(value: string) { } } +function PublisherLinkIcon({ label }: { label: string }) { + switch (label.trim().toLowerCase()) { + case 'website': return ; + case 'contact': return ; + case 'documentation': return ; + case 'report a bug': return ; + case 'source code': return ; + case 'donate': return ; + case 'video': return ; + case 'community': return ; + case 'chat': return ; + default: return ; + } +} + function MediaGallery({ app }: { app: StoreApp }) { const items = [ - ...(app.screenshots || []).map(url => ({ kind: 'image' as const, url })), ...(app.videos || []).map(url => ({ kind: 'video' as const, url })), + ...(app.screenshots || []).map(url => ({ kind: 'image' as const, url })), ]; const [active, setActive] = useState(0); + const [expandedImage, setExpandedImage] = useState(null); useEffect(() => { if (active >= items.length) setActive(0); }, [active, items.length]); if (!items.length) return null; const current = items[active] || items[0]; @@ -170,9 +187,10 @@ function MediaGallery({ app }: { app: StoreApp }) { return Preview{items.length} {items.length === 1 ? 'item' : 'items'} - {current.kind === 'image' ? : embed ? : Watch app videoOpen the publisher's video } + {current.kind === 'image' ? setExpandedImage(current.url)} aria-label="Enlarge screenshot"> : embed ? : Watch app videoOpen the publisher's video } {items.length > 1 && {items.map((item, index) => setActive(index)} aria-label={`Show ${item.kind} ${index + 1}`}>{item.kind === 'image' ? : })}} + {expandedImage && createPortal( setExpandedImage(null)}> setExpandedImage(null)} aria-label="Close enlarged screenshot"> event.stopPropagation()} />, document.body)} ; } @@ -198,7 +216,7 @@ function AppDetail({ app, loading, onClose, action, onAction }: { app: StoreApp; About{loading ? Loading details… : {app.description || app.summary}} - {(links.length > 0 || app.storeUrl) && LinksFrom publisher{links.map(link => {link.label}{linkDestination(link.url)})}{app.storeUrl && Snap StoreOfficial listing}} + {(links.length > 0 || app.storeUrl) && LinksFrom publisher{links.map(link => {link.label}{linkDestination(link.url)})}{app.storeUrl && Snap StoreOfficial listing}} CapOS integration{app.webdesktop === 'native' ? 'Native WebDesktop app' : app.webdesktop === 'web' ? 'Web service integration' : app.webdesktop === 'gui' ? 'GUI bridge ready' : app.webdesktop === 'service' ? 'Service integration' : 'Snap managed by CapOS'}CapOS can install, update and manage this app from WebDesktop. InformationPublisher{app.publisher}{app.license && License{app.license}}{app.confinement && Confinement{app.confinement}}Category{app.category}Updated{releasedAt || app.updated || 'Recently'}Package{app.name} diff --git a/website/snap/src/styles.css b/website/snap/src/styles.css index 574e2fb5e6..306f8f4190 100644 --- a/website/snap/src/styles.css +++ b/website/snap/src/styles.css @@ -22,5 +22,5 @@ @media(max-width:760px){.store-shelves{gap:10px}.carousel-controls>span{display:none}} /* Rich Snap listing detail */ -.detail-panel{width:min(820px,100%);padding:0 48px 44px;scrollbar-gutter:stable}.close-detail{background:rgba(255,255,255,.9);box-shadow:0 4px 16px rgba(0,0,0,.12);backdrop-filter:blur(12px)}.detail-banner{height:232px;margin:0 -48px;position:relative;overflow:hidden;background:#eef1f5}.detail-banner img{width:100%;height:100%;object-fit:cover;display:block}.detail-banner>span{position:absolute;inset:0;background:linear-gradient(180deg,transparent 50%,rgba(0,0,0,.12))}.detail-hero{margin-top:58px}.detail-hero.with-banner{position:relative;margin-top:-48px;align-items:end}.detail-hero.with-banner .app-icon{border:5px solid #fff;box-shadow:0 12px 32px rgba(0,0,0,.18)}.detail-identity{min-width:0;padding-bottom:3px}.detail-identity>p{line-height:1.4}.detail-identity .publisher{display:flex;align-items:center;gap:7px}.detail-identity .publisher>span{color:#919198}.detail-stats{margin:30px 0 10px}.detail-stats strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.detail-section{padding:20px 0 26px}.detail-section-heading{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px}.detail-section-heading h3{margin:0}.detail-section-heading>span{font-size:10px;color:#99999f;font-weight:600;letter-spacing:.02em}.media-stage{width:100%;aspect-ratio:16/9;max-height:410px;border-radius:18px;overflow:hidden;background:#111318;border:1px solid #e1e1e4;display:grid;place-items:center}.media-stage>img{width:100%;height:100%;object-fit:contain;display:block;background:#f4f4f6}.media-stage>iframe{width:100%;height:100%;border:0;background:#111}.media-strip{display:flex;gap:9px;margin-top:10px;overflow-x:auto;padding:2px 1px 7px;scrollbar-width:thin}.media-strip>button{width:108px;height:68px;flex:none;padding:0;border:2px solid transparent;border-radius:11px;overflow:hidden;background:#eceef1}.media-strip>button.active{border-color:#1685ee;box-shadow:0 0 0 2px rgba(22,133,238,.12)}.media-strip img{width:100%;height:100%;object-fit:cover;display:block}.video-thumb{width:100%;height:100%;display:grid;place-items:center;background:linear-gradient(145deg,#242932,#11151b);color:#fff}.video-thumb svg{width:21px;fill:currentColor}.video-link-preview{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;background:radial-gradient(circle at 50% 42%,#343b47,#151920 65%);gap:8px}.video-link-preview>span{width:58px;height:58px;border-radius:50%;background:#fff;color:#111820;display:grid;place-items:center;box-shadow:0 12px 35px rgba(0,0,0,.25)}.video-link-preview>span svg{width:23px;fill:currentColor;margin-left:3px}.video-link-preview strong{font-size:15px}.video-link-preview small{display:flex;align-items:center;gap:4px;color:#b9c0ca}.video-link-preview small svg{width:12px}.app-link-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.app-link-grid>a{min-width:0;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;border:1px solid #e5e5e8;border-radius:13px;padding:11px 12px;background:#fbfbfc}.app-link-grid>a:hover{border-color:#cbdcf0;background:#f7fbff;transform:translateY(-1px)}.app-link-icon{width:34px;height:34px;border-radius:9px;background:#edf4fc;color:#147ce5;display:grid;place-items:center}.app-link-icon svg{width:16px}.app-link-grid>a>span:nth-child(2){min-width:0;display:flex;flex-direction:column}.app-link-grid strong{font-size:12px;color:#303035}.app-link-grid small{font-size:9px;color:#929298;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.app-link-grid>a>svg{width:14px;color:#a1a1a7}.info-list dd code{font:600 11px "SFMono-Regular",Consolas,"Liberation Mono",monospace;background:#f2f2f4;border-radius:5px;padding:3px 5px}.info-capitalize{text-transform:capitalize} +.detail-panel{width:min(820px,100%);padding:0 48px 44px;scrollbar-gutter:stable}.close-detail{background:rgba(255,255,255,.9);box-shadow:0 4px 16px rgba(0,0,0,.12);backdrop-filter:blur(12px)}.detail-banner{height:232px;margin:0 -48px;position:relative;overflow:hidden;background:#eef1f5}.detail-banner img{width:100%;height:100%;object-fit:cover;display:block}.detail-banner>span{position:absolute;inset:0;background:linear-gradient(180deg,transparent 38%,rgba(255,255,255,.18) 58%,rgba(255,255,255,.82) 84%,var(--panel) 100%)}.detail-hero{margin-top:58px}.detail-hero.with-banner{position:relative;margin-top:-48px;align-items:end}.detail-hero.with-banner .app-icon{border:5px solid #fff;box-shadow:0 12px 32px rgba(0,0,0,.18)}.detail-identity{min-width:0;padding-bottom:3px}.detail-identity>p{line-height:1.4}.detail-identity .publisher{display:flex;align-items:center;gap:7px}.detail-identity .publisher>span{color:#919198}.detail-stats{margin:30px 0 10px}.detail-stats strong{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.detail-section{padding:20px 0 26px}.detail-section-heading{display:flex;align-items:center;justify-content:space-between;margin:0 0 12px}.detail-section-heading h3{margin:0}.detail-section-heading>span{font-size:10px;color:#99999f;font-weight:600;letter-spacing:.02em}.media-stage{width:100%;aspect-ratio:16/9;max-height:410px;border-radius:18px;overflow:hidden;background:#111318;border:1px solid #e1e1e4;display:grid;place-items:center}.media-image-button{width:100%;height:100%;padding:0;border:0;background:#f4f4f6;display:block;cursor:zoom-in}.media-image-button img{width:100%;height:100%;object-fit:contain;display:block}.media-stage>iframe{width:100%;height:100%;border:0;background:#111}.media-lightbox{position:fixed;z-index:120;inset:0;background:rgba(8,10,14,.88);backdrop-filter:blur(12px);display:grid;place-items:center;padding:34px;cursor:zoom-out}.media-lightbox>img{max-width:min(94vw,1800px);max-height:92vh;width:auto;height:auto;object-fit:contain;border-radius:12px;box-shadow:0 24px 80px rgba(0,0,0,.45);cursor:default}.media-lightbox-close{position:fixed;right:22px;top:20px;width:38px;height:38px;border:0;border-radius:50%;background:rgba(255,255,255,.94);color:#17171a;display:grid;place-items:center;box-shadow:0 8px 28px rgba(0,0,0,.28)}.media-lightbox-close svg{width:18px}.media-strip{display:flex;gap:9px;margin-top:10px;overflow-x:auto;padding:2px 1px 7px;scrollbar-width:thin}.media-strip>button{width:108px;height:68px;flex:none;padding:0;border:2px solid transparent;border-radius:11px;overflow:hidden;background:#eceef1}.media-strip>button.active{border-color:#1685ee;box-shadow:0 0 0 2px rgba(22,133,238,.12)}.media-strip img{width:100%;height:100%;object-fit:cover;display:block}.video-thumb{width:100%;height:100%;display:grid;place-items:center;background:linear-gradient(145deg,#242932,#11151b);color:#fff}.video-thumb svg{width:21px;fill:currentColor}.video-link-preview{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;background:radial-gradient(circle at 50% 42%,#343b47,#151920 65%);gap:8px}.video-link-preview>span{width:58px;height:58px;border-radius:50%;background:#fff;color:#111820;display:grid;place-items:center;box-shadow:0 12px 35px rgba(0,0,0,.25)}.video-link-preview>span svg{width:23px;fill:currentColor;margin-left:3px}.video-link-preview strong{font-size:15px}.video-link-preview small{display:flex;align-items:center;gap:4px;color:#b9c0ca}.video-link-preview small svg{width:12px}.app-link-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.app-link-grid>a{min-width:0;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;border:1px solid #e5e5e8;border-radius:13px;padding:11px 12px;background:#fbfbfc}.app-link-grid>a:hover{border-color:#cbdcf0;background:#f7fbff;transform:translateY(-1px)}.app-link-icon{width:34px;height:34px;border-radius:9px;background:#edf4fc;color:#147ce5;display:grid;place-items:center}.app-link-icon svg{width:16px}.app-link-grid>a>span:nth-child(2){min-width:0;display:flex;flex-direction:column}.app-link-grid strong{font-size:12px;color:#303035}.app-link-grid small{font-size:9px;color:#929298;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.app-link-grid>a>svg{width:14px;color:#a1a1a7}.info-list dd code{font:600 11px "SFMono-Regular",Consolas,"Liberation Mono",monospace;background:#f2f2f4;border-radius:5px;padding:3px 5px}.info-capitalize{text-transform:capitalize} @media(max-width:650px){.detail-panel{padding:0 22px 36px}.detail-banner{height:176px;margin:0 -22px}.detail-hero{margin-top:54px}.detail-hero.with-banner{margin-top:-38px}.detail-hero.with-banner .app-icon{border-width:4px}.detail-hero>.install-progress{grid-column:auto}.media-stage{border-radius:14px}.media-strip>button{width:88px;height:56px}.app-link-grid{grid-template-columns:1fr}.detail-stats{margin-top:24px}}
CapOS can install, update and manage this app from WebDesktop.
{app.name}