tray card: per-icon Phosphor imports; teaser pins the current app-privacy step 5 text
This commit is contained in:
parent
e2ff7efcda
commit
987f0775b3
2 changed files with 21 additions and 12 deletions
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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?',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue