Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ For the full tag dictionary, operational playbook (direct merge / duplicate-titl

## i18n keys

- `apps/web/src/i18n/types.ts` is the typed `Dict`; every key must be defined in all 18 locale files under `apps/web/src/i18n/locales/*.ts` (`ar`, `de`, `en`, `es-ES`, `fa`, `fr`, `hu`, `id`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `th`, `tr`, `uk`, `zh-CN`, `zh-TW`). Add the key to `types.ts` first; missing translations produce a typecheck error.
- `apps/web/src/i18n/types.ts` is the typed `Dict`; every key must be defined in all 19 locale files under `apps/web/src/i18n/locales/*.ts` (`ar`, `de`, `en`, `es-ES`, `fa`, `fr`, `hu`, `id`, `it`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `th`, `tr`, `uk`, `zh-CN`, `zh-TW`). Add the key to `types.ts` first; missing translations produce a typecheck error.

## UI animation philosophy

Expand Down
78 changes: 54 additions & 24 deletions apps/web/src/components/pet/PetOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { useT } from '../../i18n';
import type { PetConfig } from '../../types';
import {
Expand Down Expand Up @@ -45,13 +45,33 @@ const EMPTY_TASK_CENTER: PetTaskCenter = {
};

interface Position {
// Distances from the right/bottom of the viewport so the overlay
// sticks to the corner across resizes. Saved in localStorage.
right: number;
bottom: number;
// Distance from the horizontal edge of the anchor corner (left for
// left-anchored corners, right for right-anchored corners).
x: number;
// Distance from the vertical edge of the anchor corner (top for
// top-anchored corners, bottom for bottom-anchored corners).
y: number;
}

const DEFAULT_POSITION: Position = { right: 24, bottom: 24 };
const DEFAULT_POSITION: Position = { x: 24, y: 24 };

type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

// Converts (corner, position) into a React CSS style object with exactly two
// positional properties set and the other two undefined so the browser ignores
// them. The overlay is position:fixed so any unset axis property resolves to
// 'auto' rather than a pixel value, which is what the tests assert for the
// "empty string" checks (jsdom reports '' for unset inline styles).
function cornerStyle(corner: Corner, pos: Position): React.CSSProperties {
const isTop = corner === 'top-left' || corner === 'top-right';
const isLeft = corner === 'top-left' || corner === 'bottom-left';
return {
top: isTop ? pos.y : undefined,
bottom: isTop ? undefined : pos.y,
left: isLeft ? pos.x : undefined,
right: isLeft ? undefined : pos.x,
};
}

// How long the pet has to sit untouched before the overlay flips to
// the "waiting" animation row. Sized to sit comfortably past a few
Expand Down Expand Up @@ -93,11 +113,15 @@ function loadPosition(): Position {
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULT_POSITION;
const parsed = JSON.parse(raw) as Partial<Position>;
return {
right: typeof parsed.right === 'number' ? parsed.right : DEFAULT_POSITION.right,
bottom: typeof parsed.bottom === 'number' ? parsed.bottom : DEFAULT_POSITION.bottom,
};
const parsed = JSON.parse(raw) as Partial<Position & { right?: number; bottom?: number }>;
// Migrate legacy { right, bottom } shape to { x, y }.
const x = typeof parsed.x === 'number' ? parsed.x
: typeof parsed.right === 'number' ? parsed.right
: DEFAULT_POSITION.x;
const y = typeof parsed.y === 'number' ? parsed.y
: typeof parsed.bottom === 'number' ? parsed.bottom
: DEFAULT_POSITION.y;
return { x, y };
} catch {
return DEFAULT_POSITION;
}
Expand Down Expand Up @@ -139,8 +163,8 @@ export function PetOverlay({
const dragRef = useRef<{
startX: number;
startY: number;
startRight: number;
startBottom: number;
startPosX: number;
startPosY: number;
moved: boolean;
// Last classified gesture direction. Kept on the ref so we don't
// trigger a state update + render on every pointermove tick.
Expand Down Expand Up @@ -319,15 +343,19 @@ export function PetOverlay({

if (!active) return null;

const activeCorner: Corner = pet?.corner ?? 'bottom-right';
const isLeftAnchored = activeCorner === 'top-left' || activeCorner === 'bottom-left';
const isTopAnchored = activeCorner === 'top-left' || activeCorner === 'top-right';

const onPointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
const target = event.currentTarget;
target.setPointerCapture(event.pointerId);
dragRef.current = {
startX: event.clientX,
startY: event.clientY,
startRight: position.right,
startBottom: position.bottom,
startPosX: position.x,
startPosY: position.y,
moved: false,
direction: null,
};
Expand All @@ -341,13 +369,15 @@ export function PetOverlay({
const dy = event.clientY - drag.startY;
if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 4) return;
drag.moved = true;
// Convert pointer movement into right/bottom offsets so the sprite
// tracks the cursor while staying anchored to the corner system.
// The clamp budget (~120px) keeps the 96px sprite plus its drop
// shadow on-screen even when dragged toward the opposite edge.
const nextRight = Math.max(8, Math.min(window.innerWidth - 120, drag.startRight - dx));
const nextBottom = Math.max(8, Math.min(window.innerHeight - 120, drag.startBottom - dy));
setPosition({ right: nextRight, bottom: nextBottom });
// For left-anchored corners, moving right increases x offset.
// For right-anchored corners, moving right decreases x offset.
// For top-anchored corners, moving down increases y offset.
// For bottom-anchored corners, moving down decreases y offset.
const nextX = Math.max(8, Math.min(window.innerWidth - 120,
isLeftAnchored ? drag.startPosX + dx : drag.startPosX - dx));
const nextY = Math.max(8, Math.min(window.innerHeight - 120,
isTopAnchored ? drag.startPosY + dy : drag.startPosY - dy));
setPosition({ x: nextX, y: nextY });

// Classify the gesture direction once it clears the jitter floor
// and one axis clearly dominates the other. The animation then
Expand Down Expand Up @@ -429,9 +459,9 @@ export function PetOverlay({
className="pet-overlay"
role="complementary"
aria-label={t('pet.overlayAria')}
data-corner={activeCorner}
style={{
right: position.right,
bottom: position.bottom,
...cornerStyle(activeCorner, position),
// The accent drives the halo, the bubble border, and the focus
// ring on the action buttons via CSS custom property cascade.
['--pet-accent' as string]: active.accent,
Expand Down
22 changes: 22 additions & 0 deletions apps/web/src/components/pet/PetSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,28 @@ export function PetSettings({ cfg, setCfg }: Props) {
</p>
</div>

<div className="field pet-corner-field">
<span className="field-label">{t('pet.fieldCorner')}</span>
<div
className="pet-corner-picker"
role="radiogroup"
aria-label={t('pet.fieldCorner')}
>
{(['top-left', 'top-right', 'bottom-left', 'bottom-right'] as const).map((c) => (
<button
key={c}
type="button"
role="radio"
aria-checked={pet.corner === c}
className={`seg-btn small${pet.corner === c ? ' active' : ''}`}
onClick={() => update({ corner: c })}
>
{t(`pet.corner.${c}` as const)}
</button>
))}
</div>
</div>

{activeTab === 'builtIn' ? (
<div className="pet-built-in">
{bundledPets.length === 0 ? (
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,11 @@ export const ar: Dict = {
'pet.communitySyncFailed': 'فشلت المزامنة: {error}',
'pet.codexBundled': 'مدمج',
'pet.codexBundledTitle': 'يأتي مع Open Design - لا حاجة للتحميل.',
'pet.fieldCorner': 'الزاوية',
'pet.corner.top-left': 'أعلى يسار',
'pet.corner.top-right': 'أعلى يمين',
'pet.corner.bottom-left': 'أسفل يسار',
'pet.corner.bottom-right': 'أسفل يمين',

'settings.notifications': 'الإشعارات',
'settings.notificationsHint': 'صوت وإشعار سطح المكتب عند اكتمال المهمة',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,11 @@ export const de: Dict = {
'pet.communitySyncFailed': 'Sync fehlgeschlagen: {error}',
'pet.codexBundled': 'Mitgeliefert',
'pet.codexBundledTitle': 'Wird mit Open Design ausgeliefert — kein Download nötig.',
'pet.fieldCorner': 'Ecke',
'pet.corner.top-left': 'Oben links',
'pet.corner.top-right': 'Oben rechts',
'pet.corner.bottom-left': 'Unten links',
'pet.corner.bottom-right': 'Unten rechts',

'settings.notifications': 'Benachrichtigungen',
'settings.notificationsHint': 'Ton und Desktop-Benachrichtigung beim Abschluss von Aufgaben',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1922,6 +1922,11 @@ export const en: Dict = {
'pet.communitySyncFailed': 'Sync failed: {error}',
'pet.codexBundled': 'Bundled',
'pet.codexBundledTitle': 'Ships with Open Design — no download needed.',
'pet.fieldCorner': 'Corner',
'pet.corner.top-left': 'Top left',
'pet.corner.top-right': 'Top right',
'pet.corner.bottom-left': 'Bottom left',
'pet.corner.bottom-right': 'Bottom right',

'settings.notifications': 'Notifications',
'settings.notificationsHint': 'Sound and desktop notification on task completion',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,11 @@ export const esES: Dict = {
'pet.communitySyncFailed': 'Error al sincronizar: {error}',
'pet.codexBundled': 'Incluida',
'pet.codexBundledTitle': 'Viene con Open Design — sin descarga.',
'pet.fieldCorner': 'Esquina',
'pet.corner.top-left': 'Arriba izquierda',
'pet.corner.top-right': 'Arriba derecha',
'pet.corner.bottom-left': 'Abajo izquierda',
'pet.corner.bottom-right': 'Abajo derecha',

'settings.notifications': 'Notificaciones',
'settings.notificationsHint': 'Sonido y notificación al completar la tarea',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,11 @@ export const fa: Dict = {
'pet.communitySyncFailed': 'خطا در همگام‌سازی: {error}',
'pet.codexBundled': 'همراه',
'pet.codexBundledTitle': 'همراه Open Design ارائه می‌شود — نیازی به دانلود نیست.',
'pet.fieldCorner': 'گوشه',
'pet.corner.top-left': 'بالا چپ',
'pet.corner.top-right': 'بالا راست',
'pet.corner.bottom-left': 'پایین چپ',
'pet.corner.bottom-right': 'پایین راست',

'settings.notifications': 'اعلان‌ها',
'settings.notificationsHint': 'صدا و اعلان دسکتاپ هنگام تکمیل وظیفه',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,11 @@ export const fr: Dict = {
'pet.communitySyncFailed': 'Échec de la synchronisation : {error}',
'pet.codexBundled': 'Fourni',
'pet.codexBundledTitle': 'Livré avec Open Design — aucun téléchargement nécessaire.',
'pet.fieldCorner': 'Coin',
'pet.corner.top-left': 'Haut gauche',
'pet.corner.top-right': 'Haut droite',
'pet.corner.bottom-left': 'Bas gauche',
'pet.corner.bottom-right': 'Bas droite',

'settings.notifications': 'Notifications',
'settings.notificationsHint': 'Son et notification bureau à la fin d\'une tâche',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/hu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,11 @@ export const hu: Dict = {
'pet.communitySyncFailed': 'A szinkronizálás sikertelen: {error}',
'pet.codexBundled': 'Beépített',
'pet.codexBundledTitle': 'Az Open Designgal érkezik — letöltés nem szükséges.',
'pet.fieldCorner': 'Sarok',
'pet.corner.top-left': 'Bal felső',
'pet.corner.top-right': 'Jobb felső',
'pet.corner.bottom-left': 'Bal alsó',
'pet.corner.bottom-right': 'Jobb alsó',

'settings.notifications': 'Értesítések',
'settings.notificationsHint': 'Hang és asztali értesítés a feladat befejezésekor',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,11 @@ export const id: Dict = {
'pet.communitySyncFailed': 'Sinkronisasi gagal: {error}',
'pet.codexBundled': 'Bawaan',
'pet.codexBundledTitle': 'Disertakan dengan Open Design - tidak perlu diunduh.',
'pet.fieldCorner': 'Sudut',
'pet.corner.top-left': 'Kiri atas',
'pet.corner.top-right': 'Kanan atas',
'pet.corner.bottom-left': 'Kiri bawah',
'pet.corner.bottom-right': 'Kanan bawah',

'settings.notifications': 'Notifikasi',
'settings.notificationsHint': 'Atur suara dan notifikasi desktop.',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,11 @@ export const it: Dict = {
'pet.communitySyncFailed': 'Sincronizzazione fallita: {error}',
'pet.codexBundled': 'Fornito',
'pet.codexBundledTitle': 'Fornito con Open Design — nessun download necessario.',
'pet.fieldCorner': 'Angolo',
'pet.corner.top-left': 'In alto a sinistra',
'pet.corner.top-right': 'In alto a destra',
'pet.corner.bottom-left': 'In basso a sinistra',
'pet.corner.bottom-right': 'In basso a destra',

'settings.notifications': 'Notifiche',
'settings.notificationsHint': 'Suono e notifica desktop alla fine di un\'attività',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,11 @@ export const ja: Dict = {
'pet.communitySyncFailed': '同期に失敗しました: {error}',
'pet.codexBundled': '同梱',
'pet.codexBundledTitle': 'Open Design に同梱 — ダウンロード不要。',
'pet.fieldCorner': 'コーナー',
'pet.corner.top-left': '左上',
'pet.corner.top-right': '右上',
'pet.corner.bottom-left': '左下',
'pet.corner.bottom-right': '右下',

'settings.notifications': '通知',
'settings.notificationsHint': 'タスク完了時の効果音とデスクトップ通知',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,11 @@ export const ko: Dict = {
'pet.communitySyncFailed': '동기화 실패: {error}',
'pet.codexBundled': '내장',
'pet.codexBundledTitle': 'Open Design 에 내장 — 다운로드 불필요.',
'pet.fieldCorner': '모서리',
'pet.corner.top-left': '왼쪽 상단',
'pet.corner.top-right': '오른쪽 상단',
'pet.corner.bottom-left': '왼쪽 하단',
'pet.corner.bottom-right': '오른쪽 하단',

'settings.notifications': '알림',
'settings.notificationsHint': '작업 완료 시 효과음 및 데스크톱 알림',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,11 @@ export const pl: Dict = {
'pet.communitySyncFailed': 'Błąd synchronizacji: {error}',
'pet.codexBundled': 'W zestawie',
'pet.codexBundledTitle': 'Dostarczany z Open Design — bez pobierania.',
'pet.fieldCorner': 'Narożnik',
'pet.corner.top-left': 'Lewy górny',
'pet.corner.top-right': 'Prawy górny',
'pet.corner.bottom-left': 'Lewy dolny',
'pet.corner.bottom-right': 'Prawy dolny',

'settings.notifications': 'Powiadomienia',
'settings.notificationsHint': 'Dźwięk i powiadomienie pulpitu po zakończeniu zadania',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,11 @@ export const ptBR: Dict = {
'pet.communitySyncFailed': 'Falha na sincronização: {error}',
'pet.codexBundled': 'Incluído',
'pet.codexBundledTitle': 'Já vem com o Open Design — sem download.',
'pet.fieldCorner': 'Canto',
'pet.corner.top-left': 'Superior esquerdo',
'pet.corner.top-right': 'Superior direito',
'pet.corner.bottom-left': 'Inferior esquerdo',
'pet.corner.bottom-right': 'Inferior direito',

'settings.notifications': 'Notificações',
'settings.notificationsHint': 'Som e notificação na conclusão da tarefa',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,11 @@ export const ru: Dict = {
'pet.communitySyncFailed': 'Ошибка синхронизации: {error}',
'pet.codexBundled': 'Встроен',
'pet.codexBundledTitle': 'Поставляется с Open Design — загрузка не нужна.',
'pet.fieldCorner': 'Угол',
'pet.corner.top-left': 'Верхний левый',
'pet.corner.top-right': 'Верхний правый',
'pet.corner.bottom-left': 'Нижний левый',
'pet.corner.bottom-right': 'Нижний правый',

'settings.notifications': 'Уведомления',
'settings.notificationsHint': 'Звук и уведомление при завершении задачи',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,11 @@ export const th: Dict = {
'pet.communitySyncFailed': 'ซิงค์เข้าล้มซะแล้ว มีเออเร่อแบบนี้ขึ้นเลย: {error}',
'pet.codexBundled': 'รวมมามัดชุดใหญ่',
'pet.codexBundledTitle': 'เตรียมมาให้กับ Open Design แล้ว — ไม่ต้องดาวน์โหลดใหม่',
'pet.fieldCorner': 'มุม',
'pet.corner.top-left': 'บนซ้าย',
'pet.corner.top-right': 'บนขวา',
'pet.corner.bottom-left': 'ล่างซ้าย',
'pet.corner.bottom-right': 'ล่างขวา',

'settings.notifications': 'การรับส่งข้อมูลการแจ้งเตือน',
'settings.notificationsHint': 'มีระบบใช้บอกทั้งส่งเตือนเดสก์ท็อป และเปิดเสียงที่รันรับใช้เข้าแบบทำงานลุล่วง',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,11 @@ export const tr: Dict = {
'pet.communitySyncFailed': 'Eşitleme başarısız: {error}',
'pet.codexBundled': 'Yerleşik',
'pet.codexBundledTitle': 'Open Design ile birlikte gelir — indirme gerekmez.',
'pet.fieldCorner': 'Köşe',
'pet.corner.top-left': 'Sol üst',
'pet.corner.top-right': 'Sağ üst',
'pet.corner.bottom-left': 'Sol alt',
'pet.corner.bottom-right': 'Sağ alt',

'settings.notifications': 'Bildirimler',
'settings.notificationsHint': 'Görev tamamlandığında ses ve masaüstü bildirimi',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,11 @@ export const uk: Dict = {
'pet.communitySyncFailed': 'Синхронізація не вдалася: {error}',
'pet.codexBundled': 'Упаковано',
'pet.codexBundledTitle': 'Поставляється з Open Design — завантаження не потрібне.',
'pet.fieldCorner': 'Кут',
'pet.corner.top-left': 'Верхній лівий',
'pet.corner.top-right': 'Верхній правий',
'pet.corner.bottom-left': 'Нижній лівий',
'pet.corner.bottom-right': 'Нижній правий',

'settings.notifications': 'Сповіщення',
'settings.notificationsHint': 'Звук та сповіщення робочого столу при завершенні завдання',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,11 @@ export const zhCN: Dict = {
'pet.communitySyncFailed': '同步失败:{error}',
'pet.codexBundled': '内置',
'pet.codexBundledTitle': 'Open Design 内置宠物,无需下载。',
'pet.fieldCorner': '角落',
'pet.corner.top-left': '左上',
'pet.corner.top-right': '右上',
'pet.corner.bottom-left': '左下',
'pet.corner.bottom-right': '右下',

'settings.notifications': '通知',
'settings.notificationsHint': '任务完成时的声音和桌面通知',
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1459,6 +1459,11 @@ export const zhTW: Dict = {
'pet.communitySyncFailed': '同步失敗:{error}',
'pet.codexBundled': '內建',
'pet.codexBundledTitle': 'Open Design 內建寵物,無需下載。',
'pet.fieldCorner': '角落',
'pet.corner.top-left': '左上',
'pet.corner.top-right': '右上',
'pet.corner.bottom-left': '左下',
'pet.corner.bottom-right': '右下',

'settings.notifications': '通知',
'settings.notificationsHint': '任務完成時的音效和桌面通知',
Expand Down
Loading