diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.scss b/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.scss index e2ed84afde..33c31b956c 100644 --- a/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.scss +++ b/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.scss @@ -1,5 +1,9 @@ @use '../../../component-library/styles/tokens' as *; +// Gutter aligned with GalleryLayout ($gutter) so the tab strip lines up +// with the page header content of every tab pane. +$scene-gutter: clamp(28px, 4vw, 64px); + .miniapp-gallery-scene { display: flex; flex-direction: column; @@ -10,56 +14,36 @@ height: 100%; overflow: hidden; + // Layout glue: make the component-library Tabs fill the scene so each + // pane's GalleryLayout keeps its own internal scroll. &__tabs { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: 3px; - min-height: 42px; - padding: 6px clamp(24px, 4vw, 56px); - border-bottom: 1px solid var(--border-subtle); - background: var(--color-bg-primary); - - button { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 30px; - padding: 5px 10px; - border: 1px solid transparent; - border-radius: $size-radius-base; - background: transparent; - color: var(--color-text-secondary); - font-size: var(--font-size-sm); - cursor: pointer; - - &:hover { - background: var(--element-bg-soft); - color: var(--color-text-primary); - } + flex: 1 1 auto; + min-height: 0; + + > .bitfun-tabs__nav { + flex: 0 0 auto; + padding: $size-gap-1 $scene-gutter 0; + background: var(--color-bg-primary); + border-bottom-color: var(--border-subtle); + } - &:active { - transform: translateY(1px); - } + > .bitfun-tabs__content { + display: flex; + flex: 1 1 auto; + min-height: 0; + } - &.is-active { - border-color: var(--border-subtle); - background: var(--element-bg-medium); - color: var(--color-text-primary); - font-weight: 500; - } + .bitfun-tab-pane { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; } } } @media (max-width: 560px) { - .miniapp-gallery-scene__tabs { - justify-content: stretch; - padding-inline: 12px; - - button { - justify-content: center; - flex: 1; - } + .miniapp-gallery-scene__tabs > .bitfun-tabs__nav { + padding-inline: $size-gap-3; } } diff --git a/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx b/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx index 0f3c2f8fe2..db56a04766 100644 --- a/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx +++ b/src/web-ui/src/app/scenes/miniapps/MiniAppGalleryScene.tsx @@ -4,6 +4,7 @@ */ import React, { Suspense, lazy, useState } from 'react'; import { Download, Store, UploadCloud } from 'lucide-react'; +import { Tabs, TabPane } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import './MiniAppGalleryScene.scss'; @@ -19,40 +20,41 @@ const MiniAppGalleryScene: React.FC = () => { return (
- - - {activeTab === 'installed' ? : null} - {activeTab === 'market' ? : null} - {activeTab === 'submissions' ? : null} - + + + + +
); }; diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.scss b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.scss index 15ce99cefa..0f67f3205f 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.scss +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.scss @@ -1,13 +1,6 @@ @use '../../../../component-library/styles/tokens' as *; .miniapp-market-native { - .gallery-page-header, - &__controls, - &__catalog { - width: min(100%, 1240px); - margin-inline: auto; - } - &__header-actions { display: flex; align-items: center; @@ -24,63 +17,10 @@ gap: $size-gap-1; color: var(--color-text-secondary); font-size: var(--font-size-sm); - - img { - width: 24px; - height: 24px; - border-radius: $size-radius-full; - } } - &__controls { - display: flex; - justify-content: space-between; - gap: $size-gap-3; - padding: $size-gap-3 clamp(24px, 4vw, 56px) $size-gap-2; - - select { - min-width: 130px; - height: 32px; - padding-inline: $size-gap-2; - border: 1px solid var(--border-subtle); - border-radius: $size-radius-base; - background: var(--element-bg-medium); - color: var(--color-text-primary); - } - } - - &__categories { - display: flex; - flex-wrap: wrap; - gap: $size-gap-1; - - button { - border: 1px solid transparent; - border-radius: $size-radius-full; - padding: 5px 10px; - background: transparent; - color: var(--color-text-secondary); - cursor: pointer; - - &:hover { - background: var(--element-bg-soft); - color: var(--color-text-primary); - } - - &.is-active { - border-color: var(--border-base); - background: var(--element-bg-medium); - color: var(--color-text-primary); - } - } - } - - &__catalog { - padding: $size-gap-2 clamp(24px, 4vw, 56px) 48px; - } - - &__grid { - --gallery-grid-min: 285px; + &__sort { + min-width: 132px; } &__load-more { @@ -100,7 +40,10 @@ color: inherit; text-align: left; cursor: pointer; - transition: transform 160ms ease, border-color 160ms ease, background 160ms ease; + transition: + transform $motion-fast $easing-standard, + border-color $motion-fast $easing-standard, + background $motion-fast $easing-standard; &:hover { transform: translateY(-2px); @@ -108,6 +51,11 @@ background: var(--element-bg-soft); } + &:focus-visible { + outline: 2px solid var(--color-accent-500); + outline-offset: 2px; + } + &__visual { display: grid; place-items: center; @@ -115,7 +63,6 @@ overflow: hidden; border-bottom: 1px solid var(--border-subtle); background: var(--color-bg-tertiary); - font-size: 34px; img { width: 100%; @@ -124,10 +71,18 @@ } } + &__fallback { + display: grid; + place-items: center; + width: 100%; + height: 100%; + color: var(--color-text-primary); + } + &__body { display: grid; - gap: 8px; - padding: 14px 15px 15px; + gap: $size-gap-2; + padding: $size-gap-3; strong { overflow: hidden; @@ -138,7 +93,7 @@ p { display: -webkit-box; - min-height: 38px; + min-height: 2.9em; margin: 0; overflow: hidden; color: var(--color-text-secondary); @@ -162,13 +117,13 @@ } &__stats { - gap: 13px; - padding-top: 3px; + gap: $size-gap-3; + padding-top: $size-gap-1; span { display: inline-flex; align-items: center; - gap: 4px; + gap: $size-gap-1; } } } @@ -229,7 +184,7 @@ &__rating { display: flex; align-items: center; - gap: 4px; + gap: $size-gap-1; button { display: grid; @@ -250,13 +205,13 @@ &__releases { display: grid; - gap: 6px; + gap: $size-gap-2; > div { display: grid; grid-template-columns: 72px 1fr auto; align-items: center; - padding: 8px 10px; + padding: $size-gap-2 $size-gap-3; border: 1px solid var(--border-subtle); border-radius: $size-radius-base; color: var(--color-text-secondary); @@ -265,20 +220,13 @@ } } -@media (max-width: 760px) { - .miniapp-market-native { - &__header-actions, - &__controls { - width: 100%; - align-items: stretch; - flex-direction: column; - } +@media (max-width: 720px) { + .miniapp-market-native__header-actions { + width: 100%; + align-items: stretch; + flex-direction: column; - &__header-actions > :first-child { - width: 100%; - } - - &__controls select { + > :first-child { width: 100%; } } diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx index 69b9406884..8f3e8cc80d 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx @@ -12,10 +12,13 @@ import { Star, } from 'lucide-react'; import { + Avatar, Badge, Button, ConfirmDialog, Search, + Select, + type SelectOption, } from '@/component-library'; import { GalleryDetailModal, @@ -24,6 +27,7 @@ import { GalleryLayout, GalleryPageHeader, GallerySkeleton, + GalleryZone, } from '@/app/components'; import { useSceneManager } from '@/app/hooks/useSceneManager'; import type { SceneTabId } from '@/app/components/SceneBar/types'; @@ -42,6 +46,7 @@ import { } from '@/infrastructure/api/service-api/MiniAppMarketAPI'; import { createLogger } from '@/shared/utils/logger'; import { useNotification } from '@/shared/notification-system'; +import { getMiniAppIconGradient, renderMiniAppIcon } from '../utils/miniAppIcons'; import { pickLocalizedString } from '../utils/pickLocalizedString'; import './MiniAppMarketView.scss'; @@ -268,6 +273,11 @@ const MiniAppMarketView: React.FC = () => { const detailDescription = detail ? pickLocalizedString(detail, currentLanguage, 'description') : ''; + const sortOptions = useMemo(() => [ + { value: 'newest', label: t('market.sort.newest') }, + { value: 'downloads', label: t('market.sort.downloads') }, + { value: 'rating', label: t('market.sort.rating') }, + ], [t]); return ( @@ -284,7 +294,7 @@ const MiniAppMarketView: React.FC = () => { /> {me ? (
- + @{me.user.login}
) : ( @@ -297,96 +307,110 @@ const MiniAppMarketView: React.FC = () => { )} /> -
-
- {CATEGORIES.map((value) => ( - - ))} -
- -
- -
- {loading ? : null} - {!loading && error ? ( - } - isError - message={t('market.messages.catalogFailed', { error })} - action={( - - )} - /> - ) : null} - {!loading && !error && items.length === 0 ? ( - } message={t('market.empty')} /> - ) : null} - {!loading && items.length > 0 ? ( - <> - - {items.map((item) => { - const name = pickLocalizedString(item, currentLanguage, 'name'); - const description = pickLocalizedString(item, currentLanguage, 'description'); - return ( - + ))} +
+ {loading ? : null} + {!loading && error ? ( + } + isError + message={t('market.messages.catalogFailed', { error })} + action={( + + )} + /> + ) : null} + {!loading && !error && items.length === 0 ? ( + } message={t('market.empty')} /> + ) : null} + {!loading && items.length > 0 ? ( + <> + + {items.map((item) => { + const name = pickLocalizedString(item, currentLanguage, 'name'); + const description = pickLocalizedString(item, currentLanguage, 'description'); + return ( + - ); - })} - - {nextCursor ? ( -
- -
- ) : null} - - ) : null} + + ); + })} + + {nextCursor ? ( +
+ +
+ ) : null} + + ) : null} + { setDetail(undefined); setInstalled(null); }} - icon={detailLoading ? : {detail?.icon || '◫'}} + icon={detailLoading + ? + : renderMiniAppIcon(detail?.icon || 'box', 24)} + iconGradient={detail ? getMiniAppIconGradient(detail.icon || 'box') : undefined} title={detailName || t('market.detail.loading')} badges={detail ? ( <> diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.scss b/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.scss index 747c260d42..a823ac74d3 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.scss +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.scss @@ -1,10 +1,11 @@ @use '../../../../component-library/styles/tokens' as *; .miniapp-submissions { - .gallery-page-header, + // Match GalleryLayout's $content-max / $gutter so switching tabs inside the + // MiniApp scene doesn't shift content horizontally. &__workspace, &__remote-warning { - width: min(100%, 1180px); + width: min(100%, 1360px); margin-inline: auto; } @@ -19,7 +20,7 @@ display: flex; align-items: center; gap: $size-gap-2; - padding: 0 clamp(24px, 4vw, 56px) $size-gap-3; + padding: 0 clamp(28px, 4vw, 64px) $size-gap-3; color: var(--color-warning); font-size: var(--font-size-sm); } @@ -28,7 +29,7 @@ display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(310px, 0.75fr); gap: $size-gap-5; - padding: $size-gap-2 clamp(24px, 4vw, 56px) 56px; + padding: $size-gap-2 clamp(28px, 4vw, 64px) 56px; } &__form, @@ -87,14 +88,17 @@ font-weight: 500; } - select, textarea { width: 100%; + min-height: 92px; + padding: 9px 10px; border: 1px solid var(--border-subtle); border-radius: $size-radius-base; outline: none; background: var(--element-bg-soft); color: var(--color-text-primary); + line-height: 1.5; + resize: vertical; &:focus { border-color: var(--color-accent-500); @@ -105,18 +109,6 @@ opacity: 0.55; } } - - select { - height: 36px; - padding-inline: 10px; - } - - textarea { - min-height: 92px; - resize: vertical; - padding: 9px 10px; - line-height: 1.5; - } } &__screenshots { @@ -181,6 +173,44 @@ } } + &__advanced-toggle { + display: flex; + align-items: center; + gap: $size-gap-1; + padding: 0; + border: 0; + background: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: 500; + cursor: pointer; + justify-self: start; + max-width: 100%; + + &:hover { + color: var(--color-text-primary); + } + + svg { + flex: 0 0 auto; + } + + small { + overflow: hidden; + color: var(--color-text-muted); + font-weight: 400; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &__advanced { + display: grid; + gap: $size-gap-3; + padding-left: $size-gap-3; + border-left: 2px solid var(--border-subtle); + } + &__progress { display: grid; gap: 7px; diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx index 46e2f319d5..4a0c27bfff 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppSubmissionsView.tsx @@ -3,6 +3,8 @@ import { open } from '@tauri-apps/plugin-dialog'; import { AlertTriangle, Camera, + ChevronDown, + ChevronRight, FileImage, Github, Loader2, @@ -11,7 +13,7 @@ import { Send, X, } from 'lucide-react'; -import { Badge, Button, Input } from '@/component-library'; +import { Badge, Button, Input, Select } from '@/component-library'; import { GalleryEmpty, GalleryLayout, GalleryPageHeader } from '@/app/components'; import { useI18n } from '@/infrastructure/i18n'; import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI'; @@ -71,6 +73,7 @@ const MiniAppSubmissionsView: React.FC = () => { const [selectedAppId, setSelectedAppId] = useState(''); const [draft, setDraft] = useState(emptyDraft); const [screenshotPaths, setScreenshotPaths] = useState([]); + const [showAdvanced, setShowAdvanced] = useState(false); const [busy, setBusy] = useState(false); const [loading, setLoading] = useState(true); const [progress, setProgress] = useState(); @@ -203,6 +206,13 @@ const MiniAppSubmissionsView: React.FC = () => { setBusy(true); setProgress({ phase: 'validating', completed: 0, total: 1 }); try { + // The marketplace rejects submissions declaring both license forms, so + // an SPDX expression wins over a custom URL when both are filled in. + const spdxExpression = draft.license.spdxExpression?.trim() || undefined; + const customUrl = draft.license.customUrl?.trim() || undefined; + const changelogFallback = draft.releaseNumber > 1 + ? t('market.submissions.changelogFallbackUpdate') + : t('market.submissions.changelogFallbackFirst'); const created = await miniAppMarketAPI.submitInstalled( selectedApp.id, { @@ -210,13 +220,10 @@ const MiniAppSubmissionsView: React.FC = () => { slug: draft.slug.trim().toLowerCase(), name: draft.name.trim(), description: draft.description.trim(), - changelog: draft.changelog.trim(), + changelog: draft.changelog.trim() || changelogFallback, tags: draft.tags.map((tag) => tag.trim()).filter(Boolean), repositoryUrl: draft.repositoryUrl?.trim() || undefined, - license: { - spdxExpression: draft.license.spdxExpression?.trim() || undefined, - customUrl: draft.license.customUrl?.trim() || undefined, - }, + license: spdxExpression ? { spdxExpression } : { customUrl }, }, screenshotPaths, ); @@ -312,41 +319,16 @@ const MiniAppSubmissionsView: React.FC = () => { - -
- setDraft({ ...draft, slug: event.target.value })} - hint={t('market.submissions.slugHint')} + options={apps.map((app) => ({ value: app.id, label: app.name }))} /> - - setDraft({ ...draft, releaseNumber: Number(event.target.value) }) - } - /> -
+ { /> -
- - setDraft({ ...draft, minBitfunVersion: event.target.value })} - /> -
- - - setDraft({ ...draft, tags: event.target.value.split(',').slice(0, 10) }) - } - hint={t('market.submissions.tagsHint')} - /> - -