feat(notifications): add overlay notifications with position config

- Add Catppuccin Macchiato overlay notification stack with 3s transient timeout
- Add `notifications.overlayPosition` config (top-left | top | top-right)
- Route startup tokenization and subtitle annotation status through configured surfaces
- Deduplicate rapid subtitle mode toggle notifications
- Change `both` to mean overlay + system; add `osd-system` as legacy alias for old behavior
- Keep `osd`/`osd-system` as config-file-only legacy values; Settings UI offers overlay/system/both/none
This commit is contained in:
2026-06-04 21:56:51 -07:00
parent c09d009a3e
commit 144373db52
82 changed files with 2290 additions and 243 deletions
+37 -1
View File
@@ -1,9 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
beginUpdateProgress,
createUiFeedbackState,
showProgressTick,
showStatusNotification,
showUpdateResult,
} from './ui-feedback';
@@ -65,3 +66,38 @@ test('showUpdateResult renders failed updates with an x marker', () => {
'x Sentence card failed: deck missing',
]);
});
test('showStatusNotification falls back to system when overlay delivery is unavailable', () => {
const calls: string[] = [];
showStatusNotification('Waiting for card update', {
getNotificationType: () => 'overlay',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showSystemNotification: (title, options) => {
calls.push(`system:${title}:${options.body}`);
},
});
assert.deepEqual(calls, ['system:SubMiner:Waiting for card update']);
});
test('showStatusNotification does not duplicate system notifications for both', () => {
const calls: string[] = [];
showStatusNotification('Card updated', {
getNotificationType: () => 'both',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showOverlayNotification: (payload) => {
calls.push(`overlay:${payload.body}`);
},
showSystemNotification: (title, options) => {
calls.push(`system:${title}:${options.body}`);
},
});
assert.deepEqual(calls, ['overlay:Card updated', 'system:SubMiner:Card updated']);
});
+23 -5
View File
@@ -1,4 +1,5 @@
import { NotificationOptions } from '../types/anki';
import type { NotificationOptions } from '../types/anki';
import type { NotificationType, OverlayNotificationPayload } from '../types/notification';
export interface UiFeedbackState {
progressDepth: number;
@@ -13,8 +14,9 @@ export interface UiFeedbackResult {
}
export interface UiFeedbackNotificationContext {
getNotificationType: () => string | undefined;
getNotificationType: () => NotificationType | undefined;
showOsd: (text: string) => void;
showOverlayNotification?: (payload: OverlayNotificationPayload) => void;
showSystemNotification: (title: string, options: NotificationOptions) => void;
}
@@ -36,13 +38,29 @@ export function showStatusNotification(
message: string,
context: UiFeedbackNotificationContext,
): void {
const type = context.getNotificationType() || 'osd';
const type = context.getNotificationType() ?? 'overlay';
if (type === 'osd' || type === 'both') {
if (type === 'none') {
return;
}
if (type === 'overlay' || type === 'both') {
if (context.showOverlayNotification) {
context.showOverlayNotification({
title: 'SubMiner',
body: message,
variant: 'info',
});
} else if (type === 'overlay') {
context.showSystemNotification('SubMiner', { body: message });
}
}
if (type === 'osd' || type === 'osd-system') {
context.showOsd(message);
}
if (type === 'system' || type === 'both') {
if (type === 'system' || type === 'both' || type === 'osd-system') {
context.showSystemNotification('SubMiner', { body: message });
}
}