From 5e82010ace7548a05fd209dac2c19b6fe0f3a31e Mon Sep 17 00:00:00 2001 From: CRT Date: Fri, 23 Oct 2026 09:08:23 +0800 Subject: [PATCH] Desktop: app page no longer overflows (page grid minmax, wrapping tabs); app ignore/unpair actions move to a ... menu; unique keys in App info --- apps/desktop/src/design.css | 2 +- apps/desktop/src/i18n/en.ts | 1 + apps/desktop/src/i18n/tr.ts | 1 + apps/desktop/src/screens/AppView.tsx | 20 ++++++-------------- apps/desktop/src/screens/tabs/AppInfo.tsx | 6 +++--- apps/desktop/src/styles.css | 2 +- apps/desktop/src/ui/ContextMenu.tsx | 13 +++++++++++++ apps/desktop/src/ui/app-menu.ts | 5 ++++- apps/desktop/src/ui/icons.tsx | 3 ++- 9 files changed, 32 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/design.css b/apps/desktop/src/design.css index ca264d2..ad3e644 100644 --- a/apps/desktop/src/design.css +++ b/apps/desktop/src/design.css @@ -108,7 +108,7 @@ .ds-app-sticky { position: sticky; top: -1px; z-index: 4; margin: 0 -28px; padding: 8px 28px; background: color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } /* ---------- Tabs v2 (pill) ---------- */ -.ds-tabs { display: flex; gap: 4px; padding: 4px; border-radius: var(--ds-r); background: var(--ds-inset); border: 1px solid var(--ds-stroke); overflow-x: auto; scrollbar-width: none; width: max-content; max-width: 100%; } +.ds-tabs { display: flex; gap: 4px; padding: 4px; border-radius: var(--ds-r); background: var(--ds-inset); border: 1px solid var(--ds-stroke); flex-wrap: wrap; width: max-content; max-width: 100%; box-sizing: border-box; } .ds-tabs::-webkit-scrollbar { display: none; } .ds-tab { all: unset; flex: none; display: inline-flex; align-items: center; gap: 7px; height: 34px; padding: 0 14px; border-radius: 10px; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap; transition: background var(--speed) var(--ease), color var(--speed) var(--ease), box-shadow var(--speed) var(--ease); } .ds-tab svg { width: 16px; height: 16px; } diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 4d829c5..ba5b634 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -2041,6 +2041,7 @@ export const en = { 'cm.refreshing': "Reading {name} again", 'cm.ignoredNoReads': "Ignored apps are not read", 'cm.ignore': "Ignore", + 'cm.more': "More actions", 'cm.showIgnored': "Show ignored apps", 'cm.copyVersion': "Copy version", 'cm.copyBuild': "Copy build", diff --git a/apps/desktop/src/i18n/tr.ts b/apps/desktop/src/i18n/tr.ts index 9afc7ad..fd90625 100644 --- a/apps/desktop/src/i18n/tr.ts +++ b/apps/desktop/src/i18n/tr.ts @@ -2043,6 +2043,7 @@ export const tr: Record = { 'cm.refreshing': "{name} yeniden okunuyor", 'cm.ignoredNoReads': "Yok sayılan uygulamalar okunmaz", 'cm.ignore': "Yok say", + 'cm.more': "Diğer işlemler", 'cm.showIgnored': "Yok sayılanları göster", 'cm.copyVersion': "Sürümü kopyala", 'cm.copyBuild': "Build'i kopyala", diff --git a/apps/desktop/src/screens/AppView.tsx b/apps/desktop/src/screens/AppView.tsx index 8debb3a..50e4449 100644 --- a/apps/desktop/src/screens/AppView.tsx +++ b/apps/desktop/src/screens/AppView.tsx @@ -1,13 +1,13 @@ // One app across both stores: header (icon, studio, ids, store state, pairing), then the tabs. A store-only app (no // app folder) shows its store reads (releases, health, reviews, TestFlight) read-only, with "Link a folder" and // "Start an app folder here" to make it a full app. -import { useContextMenu } from '../ui/ContextMenu'; +import { useContextMenu, useMenuButton } from '../ui/ContextMenu'; import { appMenu } from '../ui/app-menu'; import { useShowIgnored } from '../lib/show-ignored'; import { useState } from 'react'; import { api, errorText } from '../api'; import { type Key, t } from '../i18n'; -import { pairCandidates, type UnifiedApp, withIgnored, withLink, withPair, withSideIgnored, withStudio, withUnpair } from '../lib/identity'; +import { pairCandidates, type UnifiedApp, withLink, withPair, withStudio } from '../lib/identity'; import { attention, storeNow } from '../lib/model'; import { Hero, nextStep, Tabs } from '../ui/ds'; import { type AppTab, TAB_ICON, TAB_KEY, tabsFor, useNav } from '../nav'; @@ -43,8 +43,8 @@ function AppControls({ app }: { app: UnifiedApp }) { const d = useData(); const nav = useNav(); const cands = pairCandidates(app, d.apps); - const canUnpair = !app.project && !!app.ios && !!app.android; - const sides = (['ios', 'android'] as const).filter(k => app[k] || app.ignoredStores.includes(k)); + const more = useMenuButton(); + const [showIgnored] = useShowIgnored(); return (
{t(PAIRING[app.pairing])} @@ -65,16 +65,8 @@ function AppControls({ app }: { app: UnifiedApp }) { {cands.slice(0, 30).map(c => )} )} - {canUnpair && } - {sides.length === 2 && sides.map(k => { - const off = app.ignored || app.ignoredStores.includes(k); - const store = k === 'ios' ? 'App Store' : 'Google Play'; - return ; - })} - {app.ignored - ? - : } + +
); } diff --git a/apps/desktop/src/screens/tabs/AppInfo.tsx b/apps/desktop/src/screens/tabs/AppInfo.tsx index be4ce7c..7e6b1c3 100644 --- a/apps/desktop/src/screens/tabs/AppInfo.tsx +++ b/apps/desktop/src/screens/tabs/AppInfo.tsx @@ -107,8 +107,8 @@ function DataSafety({ project, d, onDone }: { project: Project; d: DeclarationsR {ds.types.map(x => (
{typeLabel(x.id)}
{x.category}{x.optional ? ` · ${t('ai.optional')}` : ''}{x.ephemeral ? ` · ${t('ai.ephemeral')}` : ''}
-
{x.collect.map(p => {t(PURPOSE[p] ?? 'ds.functionality')})}
- {x.shared ?
{x.share.map(p => {t(PURPOSE[p] ?? 'ds.functionality')})}
: {t('ai.notShared')}} +
{x.collect.map((p, i) => {t(PURPOSE[p] ?? 'ds.functionality')})}
+ {x.shared ?
{x.share.map((p, i) => {t(PURPOSE[p] ?? 'ds.functionality')})}
: {t('ai.notShared')}} ))} @@ -138,7 +138,7 @@ function AppPrivacy({ project, d, appId }: { project: Project; d: DeclarationsRe {ap.items.map(x => (
{x.apple}
{x.category} · {x.from.map(typeLabel).join(', ')}
-
{x.purposes.map(p => {p})}
+
{x.purposes.map((p, i) => {p})}
{t('ai.ask')} {x.thirdParty ? t('ai.askLikely') : t('ai.ask')} diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index e777cf9..8be9191 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -176,7 +176,7 @@ svg { flex: none; } .reader-line > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .dev-pill { font: 700 10px/1 var(--mono); letter-spacing: .06em; color: var(--warn-ink); background: var(--warn-bg); border: 1px solid color-mix(in srgb, var(--warn-solid) 45%, transparent); padding: 5px 7px; border-radius: 5px; } .content { flex: 1; overflow: auto; min-height: 0; scroll-behavior: smooth; } -.page { padding: 24px 28px 48px; max-width: 1360px; margin: 0 auto; display: grid; gap: 20px; } +.page { padding: 24px 28px 48px; max-width: 1360px; margin: 0 auto; display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; } @media (prefers-reduced-motion: reduce) { .content { scroll-behavior: auto; } } /* ---------- page header ---------- */ diff --git a/apps/desktop/src/ui/ContextMenu.tsx b/apps/desktop/src/ui/ContextMenu.tsx index faab2df..e54b534 100644 --- a/apps/desktop/src/ui/ContextMenu.tsx +++ b/apps/desktop/src/ui/ContextMenu.tsx @@ -39,6 +39,19 @@ export function useContextMenu(): (build: () => MenuInput) => { onContextMenu: ( }), [open]); } +/** Props for a "..." overflow button that opens the same menu under itself (click, Enter or Space). */ +export function useMenuButton(): (build: () => MenuInput) => { onClick: (e: ReactMouseEvent) => void; 'aria-haspopup': 'menu' } { + const open = useContext(Ctx); + return useCallback((build: () => MenuInput) => ({ + 'aria-haspopup': 'menu' as const, + onClick: (e: ReactMouseEvent) => { + if (!open) return; + const r = (e.currentTarget as Element).getBoundingClientRect(); + open(Math.round(r.left), Math.round(r.bottom + 4), build(), { from: e.currentTarget as Element, keyboard: e.detail === 0 }); + }, + }), [open]); +} + /** Copies and says so in a toast-less way (the menu closes; failures are silent like the browser's own Copy). */ export const copyEntry = (label: string, text: string | null | undefined, icon: MenuItem['icon'] = 'copy'): MenuItem | null => text ? { label, icon, onSelect: () => { void copyText(text); } } : null; diff --git a/apps/desktop/src/ui/app-menu.ts b/apps/desktop/src/ui/app-menu.ts index 2ac5541..7c76c80 100644 --- a/apps/desktop/src/ui/app-menu.ts +++ b/apps/desktop/src/ui/app-menu.ts @@ -3,7 +3,7 @@ import { api, errorText } from '../api'; import { t } from '../i18n'; import { copyText } from '../lib/crash'; -import { withIgnored, withSideIgnored, type UnifiedApp } from '../lib/identity'; +import { withIgnored, withSideIgnored, withUnpair, type UnifiedApp } from '../lib/identity'; import { setShowIgnored } from '../lib/show-ignored'; import { type AppTab, type Nav, TAB_ICON, TAB_KEY, tabsFor } from '../nav'; import type { Data } from '../store'; @@ -57,6 +57,9 @@ export function appMenu(app: UnifiedApp, d: Data, nav: Nav, opts: { showIgnored: onSelect: () => { void d.updateOverrides(o => withSideIgnored(o, app, k, !off)); } }; }) : []), ] }, + !p && !!app.ios && !!app.android && { label: t('app.unpair'), icon: 'unlink', onSelect: () => { + void d.updateOverrides(o => withUnpair(o, app.ios!.key, app.android!.key)).then(() => nav.go({ screen: 'portfolio' })); + } }, { label: t('cm.showIgnored'), icon: 'filter', checked: opts.showIgnored, onSelect: () => setShowIgnored(!opts.showIgnored) }, ]; } diff --git a/apps/desktop/src/ui/icons.tsx b/apps/desktop/src/ui/icons.tsx index 1f52e30..f57cc74 100644 --- a/apps/desktop/src/ui/icons.tsx +++ b/apps/desktop/src/ui/icons.tsx @@ -26,6 +26,7 @@ import { DeviceMobileIcon } from '@phosphor-icons/react/dist/csr/DeviceMobile'; import { DotIcon } from '@phosphor-icons/react/dist/csr/Dot'; import { DownloadSimpleIcon } from '@phosphor-icons/react/dist/csr/DownloadSimple'; import { ExportIcon } from '@phosphor-icons/react/dist/csr/Export'; +import { DotsThreeIcon } from '@phosphor-icons/react/dist/csr/DotsThree'; import { EyeIcon } from '@phosphor-icons/react/dist/csr/Eye'; import { EyeSlashIcon } from '@phosphor-icons/react/dist/csr/EyeSlash'; import { FileTextIcon } from '@phosphor-icons/react/dist/csr/FileText'; @@ -84,7 +85,7 @@ const ICONS = { sun: SunIcon, terminal: TerminalWindowIcon, copy: CopyIcon, image: ImageIcon, users: UsersIcon, layers: StackIcon, sparkle: SparkleIcon, pause: PauseIcon, cube: CubeIcon, arrowRight: ArrowRightIcon, arrowLeft: ArrowLeftIcon, dot: DotIcon, phone: DeviceMobileIcon, keyboard: KeyboardIcon, database: DatabaseIcon, wand: MagicWandIcon, building: BuildingsIcon, - eye: EyeIcon, eyeOff: EyeSlashIcon, link: LinkIcon, unlink: LinkBreakIcon, bell: BellIcon, filter: FunnelIcon, play: PlayIcon, + more: DotsThreeIcon, eye: EyeIcon, eyeOff: EyeSlashIcon, link: LinkIcon, unlink: LinkBreakIcon, bell: BellIcon, filter: FunnelIcon, play: PlayIcon, send: PaperPlaneRightIcon, edit: PencilSimpleIcon, upload: UploadSimpleIcon, sliders: SlidersHorizontalIcon, rotate: ArrowCounterClockwiseIcon, cloud: CloudIcon, coins: CoinsIcon, chart: TrendUpIcon, flask: FlaskIcon, grid: GridFourIcon, clipboard: ClipboardTextIcon, share: ExportIcon, lock: LockIcon, question: QuestionIcon,