desktop tray: card as tall as its content, card and menu open from the tray icon's edge (never over the taskbar); menu permission files

This commit is contained in:
CRTkafa 2026-10-24 19:53:06 +08:00
parent 6782ac6078
commit da7ca48ab4
17 changed files with 185 additions and 26 deletions

View file

@ -18,7 +18,7 @@ export interface CardHost {
get: () => Promise<CardSnapshot | null>;
onSnapshot: (fn: (s: CardSnapshot) => void) => () => void;
onShown: (fn: () => void) => () => void;
action: (action: 'hide' | 'open' | 'route' | 'refresh' | 'mute' | 'quit' | 'external' | 'ready', arg?: string) => void;
action: (action: 'hide' | 'open' | 'route' | 'refresh' | 'mute' | 'quit' | 'external' | 'ready' | 'size', arg?: string) => void;
}
const route = (app: string, tab: 'releases' | 'reviews' | 'health') => JSON.stringify({ screen: 'app', appId: app, tab });
@ -39,6 +39,18 @@ export function Card({ host, lang }: { host: CardHost; lang: CardLang }) {
const tm = setInterval(() => setNow(Date.now()), 30_000);
return () => { a(); b(); clearInterval(tm); };
}, [host]);
// the window is as tall as the card: report the height whenever it changes (the host re-anchors it above the icon)
useEffect(() => {
const el = root.current;
if (!el || typeof ResizeObserver === 'undefined') return;
let last = 0;
const ro = new ResizeObserver(() => {
const h = Math.ceil(el.getBoundingClientRect().height);
if (h > 0 && h !== last) { last = h; host.action('size', String(h)); }
});
ro.observe(el);
return () => ro.disconnect();
}, [host]);
// every show: fade in, focus the first control (keyboard users start at Refresh)
useEffect(() => { (root.current?.querySelector<HTMLElement>('button:not([disabled])') ?? root.current)?.focus(); }, [shown, !!s]);

View file

@ -4,7 +4,7 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import type { CardSnapshot } from './model';
export type CardAction = 'hide' | 'open' | 'route' | 'refresh' | 'mute' | 'quit' | 'external' | 'ready';
export type CardAction = 'hide' | 'open' | 'route' | 'refresh' | 'mute' | 'quit' | 'external' | 'ready' | 'size';
export const inTauri = (): boolean => typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;

View file

@ -1,12 +1,12 @@
/* The tray card (card.html): v2 language at 360 x 480, rounded, its own surface; fades in over 120 ms on every show. */
/* The tray card (card.html): v2 language, 360 wide and as tall as its content (at most 480: the body scrolls), rounded, its own surface; fades in over 120 ms on every show. */
html:has(body.tc-page), body.tc-page { margin: 0; height: 100%; background: transparent; overflow: hidden; }
body.tc-page { font: var(--ds-t-body)/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--ink); }
body.tc-page #card { height: 100%; }
body.tc-page #card { height: auto; }
body.tc-page.tc-in .tc { animation: tc-in 120ms ease-out; }
:root[data-motion="reduce"] body.tc-page.tc-in .tc { animation: none; }
@keyframes tc-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
.tc { box-sizing: border-box; height: 100%; max-height: 480px; display: flex; flex-direction: column; border-radius: var(--ds-r-lg);
.tc { box-sizing: border-box; height: auto; max-height: 480px; display: flex; flex-direction: column; border-radius: var(--ds-r-lg);
border: 1px solid var(--ds-stroke); background: radial-gradient(120% 60% at 100% 0%, color-mix(in srgb, var(--accent-solid) 10%, transparent), transparent 60%), var(--ds-raised); overflow: hidden; outline: none; }
.tc-head { position: relative; display: flex; align-items: center; gap: 8px; padding: 14px 12px 12px 16px; border-bottom: 1px solid var(--ds-stroke); }
.tc-title { flex: 1; min-width: 0; display: flex; flex-direction: column; }
@ -25,7 +25,7 @@ body.tc-page.tc-in .tc { animation: tc-in 120ms ease-out; }
.tc-menu button:hover, .tc-menu button:focus-visible { background: var(--hover); outline: none; }
.tc-menu hr { border: 0; border-top: 1px solid var(--ds-stroke); margin: 4px 2px; }
.tc-body { flex: 1; overflow-y: auto; padding: 6px 10px 12px; scrollbar-width: thin; }
.tc-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 6px 10px 12px; scrollbar-width: thin; }
.tc-empty { margin: 28px 16px; text-align: center; color: var(--muted); }
.tc-sec { margin-top: 8px; }
.tc-sec h2 { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin: 6px 6px 4px; font-size: var(--ds-t-micro); font-weight: var(--ds-w-strong); text-transform: uppercase; letter-spacing: .07em; color: var(--muted); }