tray card: per-icon Phosphor imports; teaser pins the current app-privacy step 5 text

This commit is contained in:
CRTkafa 2026-10-23 14:07:44 +08:00
parent e2ff7efcda
commit 987f0775b3
2 changed files with 21 additions and 12 deletions

View file

@ -1,6 +1,15 @@
// The tray card: store status at a glance next to the tray icon. It only draws the main window's snapshot (card_get /
// `card-snapshot`) and sends its buttons back (card_action); Refresh is the main window's own Refresh all.
import { ArrowsClockwise, ArrowSquareOut, BellSlash, Bell, DotsThree, Power, Star, WarningOctagon, Pulse, AppWindow } from '@phosphor-icons/react';
import { ArrowsClockwiseIcon } from '@phosphor-icons/react/dist/csr/ArrowsClockwise';
import { ArrowSquareOutIcon } from '@phosphor-icons/react/dist/csr/ArrowSquareOut';
import { BellSlashIcon } from '@phosphor-icons/react/dist/csr/BellSlash';
import { BellIcon } from '@phosphor-icons/react/dist/csr/Bell';
import { DotsThreeIcon } from '@phosphor-icons/react/dist/csr/DotsThree';
import { PowerIcon } from '@phosphor-icons/react/dist/csr/Power';
import { StarIcon } from '@phosphor-icons/react/dist/csr/Star';
import { WarningOctagonIcon } from '@phosphor-icons/react/dist/csr/WarningOctagon';
import { PulseIcon } from '@phosphor-icons/react/dist/csr/Pulse';
import { AppWindowIcon } from '@phosphor-icons/react/dist/csr/AppWindow';
import { useCallback, useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
import type { CardApp, CardRelease, CardSnapshot, CardStore } from './model';
import { ago, cardT, type CardKey, type CardLang } from './text';
@ -54,18 +63,18 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
<header className="tc-head">
<div className="tc-title">
<strong>{T('title')}</strong>
<span className="tc-meta">{s?.muted && <span className="tc-muted"><BellSlash size={12} weight="bold" />{T('muted')}</span>}{T('lastRead', { ago: ago(lang, s?.at ?? null, now) })}</span>
<span className="tc-meta">{s?.muted && <span className="tc-muted"><BellSlashIcon size={12} weight="bold" />{T('muted')}</span>}{T('lastRead', { ago: ago(lang, s?.at ?? null, now) })}</span>
</div>
<button className="tc-btn tc-primary" data-first onClick={() => host.action('refresh')} disabled={busy} aria-busy={busy} data-testid="card-refresh">
<ArrowsClockwise size={14} weight="bold" className={busy ? 'spin' : undefined} />{busy ? T('refreshing') : T('refresh')}
<ArrowsClockwiseIcon size={14} weight="bold" className={busy ? 'spin' : undefined} />{busy ? T('refreshing') : T('refresh')}
</button>
<button className="tc-icon" onClick={() => setMenu(m => !m)} aria-haspopup="menu" aria-expanded={menu} aria-label={T('more')} title={T('more')}><DotsThree size={18} weight="bold" /></button>
<button className="tc-icon" onClick={() => setMenu(m => !m)} aria-haspopup="menu" aria-expanded={menu} aria-label={T('more')} title={T('more')}><DotsThreeIcon size={18} weight="bold" /></button>
{menu && (
<div className="tc-menu" role="menu">
<button role="menuitem" onClick={() => host.action('open')}><AppWindow size={15} />{T('openMain')}</button>
<button role="menuitem" onClick={() => { host.action('mute'); setMenu(false); }}>{s?.muted ? <Bell size={15} /> : <BellSlash size={15} />}{s?.muted ? T('unmute') : T('mute')}</button>
<button role="menuitem" onClick={() => host.action('open')}><AppWindowIcon size={15} />{T('openMain')}</button>
<button role="menuitem" onClick={() => { host.action('mute'); setMenu(false); }}>{s?.muted ? <BellIcon size={15} /> : <BellSlashIcon size={15} />}{s?.muted ? T('unmute') : T('mute')}</button>
<hr />
<button role="menuitem" onClick={() => host.action('quit')}><Power size={15} />{T('quit')}</button>
<button role="menuitem" onClick={() => host.action('quit')}><PowerIcon size={15} />{T('quit')}</button>
</div>
)}
</header>
@ -78,7 +87,7 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
<Section title={T('rejections')} tone="bad">
{s.rejections.map(r => (
<Row key={`${r.app}:${r.store}:${r.version}`} hue={r.hue} name={r.name} tone="bad" onOpen={() => host.action('route', route(r.app, 'releases'))} openLabel={T('openApp', { name: r.name })}
line={<><WarningOctagon size={13} weight="fill" />{T('rejected')} · {store(r.store)}{r.version ? ` ${r.version}` : ''}</>} sub={r.reason}
line={<><WarningOctagonIcon size={13} weight="fill" />{T('rejected')} · {store(r.store)}{r.version ? ` ${r.version}` : ''}</>} sub={r.reason}
consoles={consoles(link(r.app), r.store, host, T)} />
))}
</Section>
@ -100,7 +109,7 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
<Section title={T('health')} tone="warn">
{s.health.map(h => (
<Row key={h.app} hue={h.hue} name={h.name} tone="warn" onOpen={() => host.action('route', route(h.app, 'health'))} openLabel={T('openApp', { name: h.name })}
line={<><Pulse size={13} weight="bold" />{h.kinds.length > 1 ? T('crashAnr') : T(h.kinds[0]!)}</>} consoles={consoles(link(h.app), 'android', host, T)} />
line={<><PulseIcon size={13} weight="bold" />{h.kinds.length > 1 ? T('crashAnr') : T(h.kinds[0]!)}</>} consoles={consoles(link(h.app), 'android', host, T)} />
))}
</Section>
)}
@ -109,7 +118,7 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
<Section title={T('reviews')} aside={s.reviews.recent ? T('recent', { n: s.reviews.recent }) : T('recentNone')}>
{s.reviews.low.map(r => (
<button key={`${r.app}:${r.date}:${r.text}`} className="tc-review" onClick={() => host.action('route', route(r.app, 'reviews'))}>
<span className="tc-stars" aria-label={T('stars', { n: r.rating })}>{[1, 2, 3, 4, 5].map(i => <Star key={i} size={11} weight={i <= r.rating ? 'fill' : 'regular'} />)}</span>
<span className="tc-stars" aria-label={T('stars', { n: r.rating })}>{[1, 2, 3, 4, 5].map(i => <StarIcon key={i} size={11} weight={i <= r.rating ? 'fill' : 'regular'} />)}</span>
<span className="tc-rname">{r.name}</span>
<span className="tc-rtext">{r.text}</span>
</button>
@ -124,7 +133,7 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
function consoles(l: CardApp | undefined, st: CardStore, host: CardHost, T: (k: CardKey) => string): ReactNode {
const url = st === 'ios' ? l?.asc : l?.play;
if (!url) return null;
return <button className="tc-ext" onClick={e => { e.stopPropagation(); host.action('external', url); }} title={T(st === 'ios' ? 'asc' : 'play')} aria-label={T(st === 'ios' ? 'asc' : 'play')}><ArrowSquareOut size={14} /></button>;
return <button className="tc-ext" onClick={e => { e.stopPropagation(); host.action('external', url); }} title={T(st === 'ios' ? 'asc' : 'play')} aria-label={T(st === 'ios' ? 'asc' : 'play')}><ArrowSquareOutIcon size={14} /></button>;
}
function Section({ title, tone, aside, children }: { title: string; tone?: 'bad' | 'warn'; aside?: string; children: ReactNode }) {

View file

@ -138,7 +138,7 @@ export const PRIVACY_STEPS = [
' 2. First time: click "Get Started" and answer the Data Collection question; then Next. [click: "Get Started"]',
' 6. "Email Address" step 1, purposes: tick and untick the 6 checkboxes as answered, then Next. [click: "Next"]',
' 7. "Email Address" step 2, linked to the user: pick the radio, then Next. [click: "Next"]',
' 9. "Email Address" step 5, tracking: pick the radio, then Save (a draft). Never "Definitions and Examples". The dialog closes; the row shows Edit and the summary. [click: "Save"]',
' 9. "Email Address" step 5, tracking: pick the radio, then the last button: "Save" (first setup: a draft) or "Publish" (answers published before: this type goes live at once, verified 2026-10-22). Publish is the owner\'s button unless bypass mode is on (storebound settings bypass status). Never "Definitions and Examples". The dialog closes; the row shows Edit and the summary. [click: "Save"]',
];
export const DATA_SAFETY_CSV = [
'PSL_DATA_COLLECTION_COLLECTS_PERSONAL_DATA,,TRUE,REQUIRED,Does your app collect or share any of the required user data types?',