claude-pet 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-pet might be problematic. Click here for more details.

Files changed (133) hide show
  1. package/.claude/commands/feed.md +28 -0
  2. package/.claude/commands/name.md +28 -0
  3. package/.claude/commands/pet.md +29 -0
  4. package/.claude/commands/play.md +29 -0
  5. package/.claude/settings.local.json +41 -0
  6. package/.github/workflows/AGENTS.md +60 -0
  7. package/.github/workflows/build.yml +87 -0
  8. package/AGENTS.md +66 -0
  9. package/LICENSE +15 -0
  10. package/README.md +292 -0
  11. package/bin/claude-pet.js +42 -0
  12. package/build/AGENTS.md +50 -0
  13. package/build/dmg-background.png +0 -0
  14. package/build/entitlements.mac.plist +14 -0
  15. package/build/icon.ico +0 -0
  16. package/build/icon.png +0 -0
  17. package/build/installerHeader.bmp +0 -0
  18. package/build/installerSidebar.bmp +0 -0
  19. package/build/tray-icon.png +0 -0
  20. package/dist/main/core/badge-manager.js +49 -0
  21. package/dist/main/core/badge-registry.js +72 -0
  22. package/dist/main/core/badge-triggers.js +45 -0
  23. package/dist/main/core/contextual-messages.js +372 -0
  24. package/dist/main/core/messages.js +440 -0
  25. package/dist/main/core/mood-engine.js +145 -0
  26. package/dist/main/core/pet-messages.js +612 -0
  27. package/dist/main/core/pet-state-engine.js +232 -0
  28. package/dist/main/core/quote-collection.js +60 -0
  29. package/dist/main/core/quote-registry.js +175 -0
  30. package/dist/main/core/quote-triggers.js +62 -0
  31. package/dist/main/core/usage-tracker.js +625 -0
  32. package/dist/main/main/auto-launch.js +39 -0
  33. package/dist/main/main/auto-updater.js +98 -0
  34. package/dist/main/main/event-watcher.js +174 -0
  35. package/dist/main/main/ipc-handlers.js +89 -0
  36. package/dist/main/main/main.js +422 -0
  37. package/dist/main/main/preload.js +93 -0
  38. package/dist/main/main/settings-window.js +49 -0
  39. package/dist/main/main/share-card.js +139 -0
  40. package/dist/main/main/skin-manager.js +118 -0
  41. package/dist/main/main/tray.js +88 -0
  42. package/dist/main/shared/i18n.js +392 -0
  43. package/dist/main/shared/types.js +25 -0
  44. package/dist/main/shared/utils.js +9 -0
  45. package/dist/renderer/assets/claude-pet.png +0 -0
  46. package/dist/renderer/assets/index-BMnMEuOf.js +9 -0
  47. package/dist/renderer/assets/index-qzlrlqpX.css +1 -0
  48. package/dist/renderer/index.html +30 -0
  49. package/dist/renderer/share-card-template/card.html +148 -0
  50. package/docs/AGENTS.md +42 -0
  51. package/docs/images/angry.png +0 -0
  52. package/docs/images/character.webp +0 -0
  53. package/docs/images/claude-mama.png +0 -0
  54. package/docs/images/happy.png +0 -0
  55. package/docs/images/proud.png +0 -0
  56. package/docs/images/share-card-example.png +0 -0
  57. package/docs/images/worried_1.png +0 -0
  58. package/docs/images/worried_2.png +0 -0
  59. package/docs/spritesheet-bugs.md +240 -0
  60. package/docs/superpowers/plans/2026-03-10-compact-widget.md +888 -0
  61. package/docs/superpowers/plans/2026-03-10-viral-features.md +1874 -0
  62. package/docs/superpowers/plans/2026-03-14-update-ux.md +362 -0
  63. package/docs/superpowers/plans/2026-03-14-v1.1-features.md +2139 -0
  64. package/docs/superpowers/specs/2026-03-10-compact-widget-design.md +150 -0
  65. package/docs/superpowers/specs/2026-03-10-viral-features-design.md +217 -0
  66. package/docs/superpowers/specs/2026-03-14-streak-calendar-design.md +26 -0
  67. package/docs/superpowers/specs/2026-03-14-update-ux-design.md +172 -0
  68. package/docs/superpowers/specs/2026-03-14-v1.1-features-design.md +342 -0
  69. package/electron-builder.yml +75 -0
  70. package/package.json +48 -0
  71. package/scripts/AGENTS.md +60 -0
  72. package/scripts/install.ps1 +47 -0
  73. package/scripts/install.sh +98 -0
  74. package/scripts/make-icon.js +119 -0
  75. package/scripts/notarize.js +18 -0
  76. package/src/AGENTS.md +47 -0
  77. package/src/core/AGENTS.md +58 -0
  78. package/src/core/__tests__/AGENTS.md +60 -0
  79. package/src/core/__tests__/badge-triggers.test.ts +83 -0
  80. package/src/core/__tests__/contextual-messages.test.ts +87 -0
  81. package/src/core/__tests__/pet-state-engine.test.ts +350 -0
  82. package/src/core/__tests__/quote-collection.test.ts +62 -0
  83. package/src/core/__tests__/quote-triggers.test.ts +110 -0
  84. package/src/core/badge-manager.ts +50 -0
  85. package/src/core/badge-registry.ts +71 -0
  86. package/src/core/badge-triggers.ts +41 -0
  87. package/src/core/contextual-messages.ts +381 -0
  88. package/src/core/pet-messages.ts +615 -0
  89. package/src/core/pet-state-engine.ts +272 -0
  90. package/src/core/quote-collection.ts +63 -0
  91. package/src/core/quote-registry.ts +181 -0
  92. package/src/core/quote-triggers.ts +64 -0
  93. package/src/core/usage-tracker.ts +680 -0
  94. package/src/main/AGENTS.md +70 -0
  95. package/src/main/auto-launch.ts +38 -0
  96. package/src/main/auto-updater.ts +106 -0
  97. package/src/main/event-watcher.ts +159 -0
  98. package/src/main/ipc-handlers.ts +107 -0
  99. package/src/main/main.ts +425 -0
  100. package/src/main/preload.ts +111 -0
  101. package/src/main/settings-window.ts +50 -0
  102. package/src/main/share-card.ts +153 -0
  103. package/src/main/skin-manager.ts +119 -0
  104. package/src/main/tray.ts +94 -0
  105. package/src/renderer/AGENTS.md +62 -0
  106. package/src/renderer/App.tsx +270 -0
  107. package/src/renderer/assets/claude-mama.png +0 -0
  108. package/src/renderer/assets/claude-pet.png +0 -0
  109. package/src/renderer/components/AGENTS.md +50 -0
  110. package/src/renderer/components/Character.tsx +327 -0
  111. package/src/renderer/components/SpeechBubble.tsx +182 -0
  112. package/src/renderer/components/UsageIndicator.tsx +268 -0
  113. package/src/renderer/electron.d.ts +34 -0
  114. package/src/renderer/hooks/AGENTS.md +55 -0
  115. package/src/renderer/hooks/usePetState.ts +59 -0
  116. package/src/renderer/hooks/useWidgetMode.ts +18 -0
  117. package/src/renderer/index.html +29 -0
  118. package/src/renderer/main.tsx +13 -0
  119. package/src/renderer/pages/AGENTS.md +53 -0
  120. package/src/renderer/pages/Collection.tsx +252 -0
  121. package/src/renderer/pages/Settings.tsx +815 -0
  122. package/src/renderer/share-card-template/card.html +148 -0
  123. package/src/renderer/styles/AGENTS.md +50 -0
  124. package/src/renderer/styles/styles.css +166 -0
  125. package/src/shared/AGENTS.md +48 -0
  126. package/src/shared/i18n.ts +395 -0
  127. package/src/shared/types.ts +163 -0
  128. package/src/shared/utils.ts +6 -0
  129. package/tsconfig.json +16 -0
  130. package/tsconfig.main.json +12 -0
  131. package/tsconfig.renderer.json +12 -0
  132. package/vite.config.ts +47 -0
  133. package/vitest.config.ts +9 -0
@@ -0,0 +1,150 @@
1
+ # Compact Widget with Drag & Mini/Expand Modes
2
+
3
+ ## Summary
4
+
5
+ Redesign the Claude Mama widget from a fixed 250x300 always-expanded window to a compact mini mode (~120x100) that expands on hover or message rotation. Add direct drag-to-move via hit-test based pointer event switching.
6
+
7
+ ## Current State
8
+
9
+ - Window: 250x300, frameless, transparent, always-on-top, bottom-right
10
+ - `setIgnoreMouseEvents(true, { forward: true })` — fully click-through
11
+ - Position changeable only via Settings dropdown (topLeft/topRight/bottomLeft/bottomRight)
12
+ - Character: 100x100, speech bubble up to 200px wide, two usage bars below
13
+
14
+ ## Design
15
+
16
+ ### Mini Mode (Default)
17
+
18
+ Dimensions: ~120x100 content area within a fixed-size transparent window.
19
+
20
+ ```
21
+ ┌───────────┐
22
+ │ 🧑 (60px) │
23
+ │ [██░] 55% │
24
+ └───────────┘
25
+ ```
26
+
27
+ - Character scaled to 60x60 (pixelated rendering preserved)
28
+ - Hit area: 80x80 (10px invisible padding around 60px character for Fitts's law)
29
+ - Single 7-day usage mini bar with percent text, one line
30
+ - Mood animations continue at reduced scale
31
+ - No speech bubble, no 5-hour bar, no countdown
32
+ - Click-through except on character hit area
33
+ - New message indicator: when message rotates, show a pulsing dot on character instead of auto-expanding
34
+ - Cursor: `grab` when hovering character, `grabbing` while dragging
35
+
36
+ ### Expanded Mode
37
+
38
+ Dimensions: ~200x250 content area, triggers:
39
+
40
+ 1. **Mouse hover on character area**: expand immediately, collapse 1s after mouse leaves
41
+ 2. **Click on character** (non-drag): toggle expand/collapse
42
+ 3. **Right-click on character**: show context menu (Settings, Hide Mama, Quit)
43
+
44
+ Note: Message rotation does NOT auto-expand. Instead, a pulsing dot indicator appears on the character in mini mode. The user must hover or click to see the new message.
45
+
46
+ ```
47
+ ┌─────────────────┐
48
+ │ Speech Bubble │
49
+ │ │
50
+ │ 🧑 (60px) │
51
+ │ │
52
+ │ [7d ████] 55% │ with countdown
53
+ │ [5h ██░░] 38% │ with countdown
54
+ └─────────────────┘
55
+ ```
56
+
57
+ ### Expansion Direction
58
+
59
+ Determined by window position on screen:
60
+ - `windowCenterY > screenHeight / 2` → expand upward (bubble above character)
61
+ - Otherwise → expand downward (bubble below character)
62
+ - Recalculated after each drag-move
63
+
64
+ ### Mouse Interaction & Drag
65
+
66
+ **Dynamic pointer event switching (hit-test pattern):**
67
+
68
+ The renderer tracks `mousemove` and determines if the cursor is over the character area:
69
+ - Over character: IPC to main → `setIgnoreMouseEvents(false)` — clickable/draggable
70
+ - Outside character: IPC to main → `setIgnoreMouseEvents(true, { forward: true })` — click-through
71
+
72
+ **Cursor affordance:**
73
+ - Hover over character: `cursor: grab`
74
+ - During drag: `cursor: grabbing`
75
+ - First launch only: show a brief tooltip hint ("Drag me to move!") that fades after 3s, stored in electron-store `firstRunHintShown`
76
+
77
+ **Drag implementation:**
78
+ - Apply `-webkit-app-region: drag` to character element when hover is active
79
+ - On drag end, read `window.screenX / window.screenY`, send via IPC to main
80
+ - Main process saves position to electron-store
81
+ - Next launch restores saved position
82
+
83
+ **Click (non-drag) behavior:**
84
+ - Short click (no drag movement) on character → toggle expand/collapse
85
+ - Detected by comparing mousedown/mouseup positions (threshold: 5px)
86
+
87
+ **Right-click context menu:**
88
+ - Right-click on character → show Electron context menu with: Settings, Hide Mama, Quit
89
+ - Implemented via IPC: renderer sends `mama:show-context-menu`, main builds and shows Menu
90
+
91
+ ### Window Strategy
92
+
93
+ **Fixed-size transparent window** (no dynamic resizing):
94
+ - Window always created at expanded size (200x250)
95
+ - Mini/expanded is pure CSS transition (300ms ease) within the transparent window
96
+ - Invisible areas remain click-through due to transparency + `setIgnoreMouseEvents` forwarding
97
+ - Eliminates flickering from repeated `win.setBounds()` calls
98
+
99
+ ### Position Management
100
+
101
+ **Initial position:**
102
+ - First launch: bottom-right corner with 16px margin
103
+ - Subsequent: restored from electron-store `{ x, y }`
104
+
105
+ **Screen bounds enforcement:**
106
+ - After drag end, use `screen.getDisplayNearestPoint({ x, y })` to find the correct display
107
+ - Clamp position within that display's `workArea`
108
+ - Supports multi-monitor setups
109
+
110
+ **Settings change:**
111
+ - Remove the existing position dropdown from Settings (replaced by drag)
112
+ - Store format changes from preset string to `{ x: number, y: number }`
113
+
114
+ ### Timing Details
115
+
116
+ | Event | Action | Duration |
117
+ |-------|--------|----------|
118
+ | Hover enter character | Expand immediately | 300ms transition |
119
+ | Hover leave | Start collapse timer | 1000ms delay, then 300ms transition |
120
+ | Hover re-enter during delay | Cancel collapse | — |
121
+ | Click (non-drag) | Toggle expand/collapse | 300ms transition |
122
+ | Right-click | Show context menu | Immediate |
123
+ | Message rotation tick | Show pulsing dot on character | Dot pulses until hover/click |
124
+ | Speech bubble typing | Show during expand | ~50ms/char |
125
+ | Speech bubble visible | Hold expanded | ~4 seconds |
126
+ | Speech bubble fade-out | Start collapse timer | 400ms fade, then 1000ms delay |
127
+ | Drag start | Lock expanded | — |
128
+ | Drag end | Save position, keep expanded | Collapse on hover leave |
129
+ | First launch | Show drag hint tooltip | 3s, then fade out |
130
+
131
+ ### Files to Modify
132
+
133
+ - `src/main/main.ts` — window size (200x250), remove fixed position presets, load saved position, context menu IPC
134
+ - `src/main/ipc-handlers.ts` — add IPC for `set-ignore-mouse-events`, `save-position`, `show-context-menu`
135
+ - `src/main/preload.ts` — expose new IPC channels
136
+ - `src/renderer/App.tsx` — mini/expand state management, expansion direction logic, click toggle, right-click, first-run hint
137
+ - `src/renderer/components/Character.tsx` — reduce to 60x60 (80x80 hit area), add drag region, hit-test mousemove, cursor affordance
138
+ - `src/renderer/components/SpeechBubble.tsx` — only render when expanded
139
+ - `src/renderer/components/UsageIndicator.tsx` — mini bar variant (7d only, compact) vs expanded (both bars + countdown)
140
+ - `src/renderer/components/NewMessageDot.tsx` — pulsing dot indicator for new messages in mini mode
141
+ - `src/renderer/styles/styles.css` — mini/expand transitions, new compact layout, pulsing dot animation
142
+ - `src/renderer/hooks/useMamaState.ts` — no changes needed
143
+ - `src/renderer/hooks/useWidgetMode.ts` — add `hasNewMessage` state, click toggle
144
+ - `src/renderer/pages/Settings.tsx` — remove position dropdown
145
+
146
+ ### Out of Scope
147
+
148
+ - Resize handle or user-configurable widget size
149
+ - Snap-to-edge magnetism during drag
150
+ - Keyboard shortcuts / accessibility (future iteration)
@@ -0,0 +1,217 @@
1
+ # claude-mama Viral Features Design
2
+
3
+ **Date**: 2026-03-10
4
+ **Status**: Approved
5
+
6
+ ## Goal
7
+
8
+ claude-mama 앱의 바이럴 확산을 위한 두 가지 핵심 기능 추가.
9
+
10
+ - **타겟 채널**: Twitter/X, Reddit
11
+ - **공유 동기**: 재미 (엄마 반응) + 유틸리티 (실용적 추천)
12
+ - **브랜딩**: 한국 엄마 특화 — "Korean Mom Simulator"
13
+ - **제약**: 서버 없음, 100% 클라이언트, 최소 소셜
14
+
15
+ ---
16
+
17
+ ## Feature 1: 엄마 성적표 카드 (Share Card)
18
+
19
+ ### 개요
20
+
21
+ 현재 엄마 상태를 예쁜 카드 이미지로 생성하여 클립보드에 복사. Twitter/Reddit에 바로 붙여넣기 가능.
22
+
23
+ ### 카드 레이아웃 (~600x400px)
24
+
25
+ ```
26
+ ┌──────────────────────────────────┐
27
+ │ Claude Mama Report Card │
28
+ │ │
29
+ │ [엄마 캐릭터] 이번 주 무드: │
30
+ │ (현재 무드) 😤 분노 │
31
+ │ │
32
+ │ "옆집 아들은 Opus 다 썼다더라" │
33
+ │ │
34
+ │ ████████░░ 72% (7일) │
35
+ │ ██░░░░░░░░ 18% (5시간) │
36
+ │ │
37
+ │ claude-mama · github.com/... │
38
+ └──────────────────────────────────┘
39
+ ```
40
+
41
+ ### 기술 구현
42
+
43
+ - Electron `BrowserWindow({ show: false, offscreen: true })` → `webContents.capturePage()` → PNG
44
+ - 별도 HTML 템플릿을 숨겨진 윈도우에서 렌더링
45
+ - **2x DPI 렌더링** (1200x800 실제 픽셀, 600x400 표시) — HiDPI/소셜미디어 선명도 확보
46
+ - 오프스크린 윈도우는 **lazy singleton** — 첫 공유 시 생성, 이후 재사용
47
+ - 생성 이미지 **클립보드 자동 복사** + 선택적 파일 저장
48
+ - 순수 로컬, 외부 의존성 없음
49
+
50
+ ### 공유 플로우
51
+
52
+ 1. 트레이 메뉴 → "성적표 공유" 클릭
53
+ 2. 현재 상태 기반 카드 이미지 생성 (~500ms-1s)
54
+ 3. 클립보드 복사 완료 알림 (Electron notification)
55
+ 4. 유저가 Twitter/Reddit에 바로 붙여넣기
56
+
57
+ ### 에러 처리
58
+
59
+ - `MamaState`가 null (앱 방금 시작) → "아직 데이터 수집 중..." 알림, 공유 스킵
60
+ - 클립보드 쓰기 실패 → 파일 저장 폴백 (바탕화면) + 실패 알림
61
+ - 연속 공유 요청 → mutex로 이전 렌더링 완료까지 대기 (debounce)
62
+ - `capturePage()` 빈 버퍼 반환 시 → rect 지정 재시도 1회
63
+
64
+ ### 워터마크
65
+
66
+ - 카드 하단에 작고 세련되게 `claude-mama` + GitHub 링크
67
+ - GitHub star/다운로드로 이어지는 유입 경로
68
+
69
+ ---
70
+
71
+ ## Feature 2: 레어 멘트 도감 (Mom's Quote Collection)
72
+
73
+ ### 등급 시스템
74
+
75
+ | 등급 | 출현 조건 | 예시 |
76
+ |------|----------|------|
77
+ | **Common** | 기존 무드별 멘트 | "밥은 먹고 코딩하니?" |
78
+ | **Rare** | 특정 사용량 구간 | 사용량 정확히 50%: "딱 반이네... 반만 하는 게 어딨어" |
79
+ | **Legendary** | 극한 조건 | 99%+: "엄마가 아들 잘못 봤다... 진짜 대단하다" |
80
+ | **Secret** | 시간/날짜 이스터에그 | 새벽 3시: "이 시간에 아직도?! 엄마도 못 자잖아" |
81
+
82
+ ### 트리거 조건
83
+
84
+ **Rare:**
85
+ - 사용량 0% → "한 번도 안 쓴 거야? 비싼 돈 내고?"
86
+ - 5시간 사용량 100% → "쉬어!! 손목 부러진다!!"
87
+ - 사용량 급등 (전날 대비 +30%) → "갑자기 열심히 하니까 더 무섭다"
88
+
89
+ **Legendary:**
90
+ - 7일 연속 80%+ 유지 → "우리 아들이 달라졌어... (눈물)"
91
+ - 첫 설치 후 첫 API 호출 감지 → "첫 걸음마 뗐구나~ 엄마가 봤어"
92
+
93
+ **Secret:**
94
+ - 설날/추석 → "명절에도 코딩하냐... 세뱃돈이나 받아라"
95
+ - 크리스마스 → "산타 말고 엄마가 치킨 시켜줄게"
96
+ - 앱 설치 100일 → "벌써 100일... 엄마랑 동거 100일째네"
97
+
98
+ ### 도감 UI
99
+
100
+ 설정 창에 "도감" 탭 추가:
101
+
102
+ ```
103
+ ┌─ 엄마 어록 도감 ─────────────────┐
104
+ │ 수집 현황: 23/48 (47%) │
105
+ │ ████████████░░░░░░░░░ │
106
+ │ │
107
+ │ [일반] ████████████ 15/15 ✓ │
108
+ │ [희귀] ██████░░░░░ 6/10 │
109
+ │ [전설] ██░░░░░░░░░ 2/8 │
110
+ │ [비밀] ░░░░░░░░░░░ 0/15 🔒 │
111
+ │ │
112
+ │ 최근 획득: │
113
+ │ ⭐ "우리 아들이 달라졌어..." │
114
+ │ 2026-03-08 획득 │
115
+ │ │
116
+ │ [미발견 멘트는 "???" 로 표시] │
117
+ └──────────────────────────────────┘
118
+ ```
119
+
120
+ ### 바이럴 연결
121
+
122
+ - 레어/전설 멘트 획득 시 → "이 멘트를 성적표로 공유" 버튼
123
+ - 성적표 카드에 등급 배지 표시
124
+ - 미발견 멘트 힌트 → 커뮤니티 토론 유발
125
+
126
+ ### 데이터 저장
127
+
128
+ - `electron-store`에 저장할 새 필드:
129
+ - `unlockedQuotes: { id: string, unlockedAt: string }[]` — 해금된 멘트 목록
130
+ - `installDate: string` — 앱 설치 시각 (ISO)
131
+ - `firstApiCallSeen: boolean` — 첫 API 호출 감지 여부
132
+ - `dailyUtilization: { date: string, percent: number }[]` — 일별 사용량 (14일 보관)
133
+ - `collectionVersion: number` — 스키마 버전 (마이그레이션용)
134
+ - 각 멘트에 고유 ID (예: `rare_exact50`, `legendary_7day_streak`)
135
+ - 총 멘트 수는 quote registry에서 동적 계산 (하드코딩 X)
136
+
137
+ ### 트리거 평가
138
+
139
+ - `evaluateQuoteTriggers(input, history): string[]` — **새 pure function** (side-effect 없음)
140
+ - `computeMood()`와 별도로 `main.ts`에서 호출
141
+ - 평가 시점: 매 API 폴링(5분) + 매 메시지 로테이션(2분)
142
+ - Secret(시간 기반) 트리거: 시스템 로컬 타임존 사용
143
+ - 음력 명절(설날/추석): 향후 2-3년 날짜 상수로 하드코딩
144
+
145
+ ### Common 멘트 정의
146
+
147
+ - Common = 기존 무드별 멘트. **보이면 자동 해금** (첫 등장 시 기록)
148
+ - 기존 메시지 풀 전체가 대상 (무드별 15개 × 4무드 + confused/sleeping/warning)
149
+ - 시간 기반 시드로 인한 편향은 허용 — 장기 사용으로 자연 수집
150
+
151
+ ### i18n 전략
152
+
153
+ - Rare/Legendary/Secret 멘트도 기존 패턴대로 **ko/en/ja/zh 4개 언어 모두 번역**
154
+ - 기존 `messages.ts`의 `Record<Locale, string[]>` 구조 확장
155
+ - 한국 문화 코드(설날, 치킨 등)는 다른 언어에서도 유지 — "Korean Mom" 브랜딩 의도
156
+ - 트레이 메뉴 라벨도 i18n: ko="성적표 공유", en="Share Report Card", ja="成績表を共有", zh="分享成绩单"
157
+
158
+ ---
159
+
160
+ ## IPC 설계
161
+
162
+ ### 새 IPC 채널
163
+
164
+ | 채널 | 방향 | 용도 |
165
+ |------|------|------|
166
+ | `mama:collection-get` | renderer → main | 도감 전체 상태 조회 |
167
+ | `mama:collection-updated` | main → renderer | 새 멘트 해금 알림 |
168
+ | `mama:share-card` | renderer → main | 성적표 카드 생성 요청 (도감에서 특정 멘트 공유 시) |
169
+
170
+ ### 도감 UI 라우팅
171
+
172
+ - 기존 Settings 창에 **탭으로 추가** (`#settings` → 설정 탭 / 도감 탭)
173
+ - 별도 창 불필요 — 기존 `settings-window.ts` + `App.tsx` 해시 라우팅 활용
174
+
175
+ ### Preload 확장
176
+
177
+ ```typescript
178
+ electronAPI.getCollection(): Promise<CollectionState>
179
+ electronAPI.onCollectionUpdated(callback): void
180
+ electronAPI.shareCard(quoteId?: string): Promise<boolean>
181
+ ```
182
+
183
+ ---
184
+
185
+ ## 테스트 전략
186
+
187
+ - `src/core/__tests__/quote-collection.test.ts` — 해금 로직, 직렬화, 중복 방지
188
+ - `src/core/__tests__/quote-triggers.test.ts` — 경계값 (정확히 50%, 99%, 0%), 연속 기록 감지
189
+ - 성적표 카드: 수동 테스트 (오프스크린 렌더링은 자동화 어려움)
190
+
191
+ ---
192
+
193
+ ## 변경 범위
194
+
195
+ | 파일 | 변경 내용 |
196
+ |------|----------|
197
+ | `src/core/messages.ts` | 등급별 멘트 추가 (Rare/Legendary/Secret, 4개 언어) |
198
+ | `src/core/quote-triggers.ts` | **새 파일** — `evaluateQuoteTriggers()` pure function |
199
+ | `src/core/quote-collection.ts` | **새 파일** — 도감 상태 관리 (해금, 조회, 직렬화) |
200
+ | `src/core/__tests__/quote-collection.test.ts` | **새 파일** — 도감 단위 테스트 |
201
+ | `src/core/__tests__/quote-triggers.test.ts` | **새 파일** — 트리거 경계값 테스트 |
202
+ | `src/main/main.ts` | broadcastState에서 트리거 평가 + 도감 기록 호출 |
203
+ | `src/main/share-card.ts` | **새 파일** — 오프스크린 렌더링 + 클립보드 |
204
+ | `src/main/ipc-handlers.ts` | 새 IPC 채널 등록 (collection-get, share-card) |
205
+ | `src/main/preload.js` | 새 API 노출 (getCollection, onCollectionUpdated, shareCard) |
206
+ | `src/main/tray.ts` | "성적표 공유" 메뉴 항목 추가 (i18n) |
207
+ | `src/renderer/pages/Collection.tsx` | **새 파일** — 도감 UI (Settings 창 탭) |
208
+ | `src/renderer/pages/Settings.tsx` | 탭 네비게이션 추가 (설정/도감) |
209
+ | `src/renderer/share-card-template/` | **새 디렉토리** — 카드 HTML/CSS 템플릿 |
210
+ | `src/shared/types.ts` | QuoteRarity, CollectionState, 새 IPC 채널 타입 추가 |
211
+ | `src/shared/i18n.ts` | 트레이 메뉴 라벨 i18n 추가 |
212
+
213
+ ## 아키텍처 원칙
214
+
215
+ - 기존 pure core + Electron main + React renderer 구조 유지
216
+ - 도감 로직(`quote-collection.ts`)은 Electron 의존성 없는 pure core
217
+ - 성적표 카드 생성(`share-card.ts`)만 Electron main에 위치
@@ -0,0 +1,26 @@
1
+ # Streak Calendar Design Spec
2
+
3
+ **Date:** 2026-03-14
4
+
5
+ ## Goal
6
+ Settings 페이지의 현재 상태 카드에 GitHub 스타일 잔디밭 캘린더 + 스트릭 카운터를 추가한다. 각 셀은 해당 일의 무드 색상으로 표시.
7
+
8
+ ## Design
9
+ - **위치:** Settings → Current Status 카드, 상태 정보 아래
10
+ - **스트릭 카운터:** "🔥 N일 연속" (dailyHistory에서 최근 연속 percent > 0 일수)
11
+ - **잔디밭 그리드:** 최근 30일, 6열 × 5행, 최신이 우측 하단
12
+ - **색상:** angry=#ef4444, worried=#eab308, happy=#22c55e, proud=#f59e0b, 미사용=#e5e7eb, 없음=#f3f4f6
13
+ - **무드 계산:** 일별 percent로 단일축 fallback (< 25% angry, 25-60% worried, 60-85% happy, 85%+ proud)
14
+ - **범례:** 색상 점 4개 + 라벨
15
+
16
+ ## Data Flow
17
+ - dailyHistory를 renderer에 전달 (IPC: DAILY_HISTORY_GET)
18
+ - Settings에서 마운트 시 조회
19
+
20
+ ## Files
21
+ - `src/shared/types.ts` — IPC channel 추가
22
+ - `src/main/ipc-handlers.ts` — dailyHistory 핸들러
23
+ - `src/main/preload.ts` — bridge
24
+ - `src/renderer/electron.d.ts` — type
25
+ - `src/renderer/pages/Settings.tsx` — 잔디밭 렌더링
26
+ - `src/shared/i18n.ts` — streak 관련 문자열
@@ -0,0 +1,172 @@
1
+ # Update UX Flow Improvement
2
+
3
+ ## Problem
4
+
5
+ When the user clicks "Check for Updates":
6
+ 1. If an update exists: **no feedback at all** until download completes (could be minutes)
7
+ 2. No download progress indication
8
+ 3. "You are up to date!" message lacks version info
9
+ 4. Update check logic is **duplicated** between `tray.ts` and `main.ts`
10
+ 5. Existing `update-downloaded` handler strings are hardcoded English (not localized)
11
+
12
+ ## Solution
13
+
14
+ Event-driven dialog chain providing feedback at every stage of the update lifecycle, with explicit download control to avoid race conditions.
15
+
16
+ ## Architecture
17
+
18
+ ### Key design decision: `autoDownload = false` for manual checks
19
+
20
+ The existing `initAutoUpdater()` sets `autoDownload = true` for background auto-updates. For manual checks, we temporarily disable `autoDownload` and call `downloadUpdate()` explicitly after showing the "update found" dialog. This prevents the race condition where download starts before the user sees any feedback.
21
+
22
+ ### New: `checkForUpdatesManual()` in `auto-updater.ts`
23
+
24
+ Single exported function that handles the entire manual update check flow. Both `tray.ts` and `main.ts` call this instead of implementing their own logic.
25
+
26
+ **Flow:**
27
+
28
+ ```
29
+ User clicks "Check for Updates"
30
+ → Guard: if already checking, return (concurrency guard)
31
+ → Guard: if !app.isPackaged, show dev mode message, return
32
+ → Set autoDownload = false (prevent race)
33
+ → result = autoUpdater.checkForUpdates()
34
+ → Branch:
35
+ A) No update → Dialog: "최신 버전입니다! (v1.1.0)" → restore autoDownload = true
36
+ B) Update found → Dialog: "v1.2.0 업데이트를 다운로드합니다" [OK / Cancel]
37
+ → User clicks OK → autoUpdater.downloadUpdate()
38
+ → Download completes → update-downloaded handler shows restart prompt
39
+ → restore autoDownload = true
40
+ C) Error → Dialog: "업데이트 확인 실패: {error}" → restore autoDownload = true
41
+ ```
42
+
43
+ ### Localized `update-downloaded` handler
44
+
45
+ The existing `update-downloaded` handler in `initAutoUpdater()` uses hardcoded English strings. It will be updated to read the current locale from the store and use translation keys.
46
+
47
+ ## Changes
48
+
49
+ ### 1. `src/main/auto-updater.ts`
50
+
51
+ **Modify `initAutoUpdater()`:**
52
+ - Localize the `update-downloaded` dialog using `t()` and `getStore().get('locale')`
53
+ - Use `update_ready_title` and a new `update_ready_message` key
54
+
55
+ **Add `checkForUpdatesManual()`:**
56
+
57
+ ```typescript
58
+ import { autoUpdater } from 'electron-updater';
59
+ import { app, BrowserWindow, dialog } from 'electron';
60
+ import { getStore } from './ipc-handlers';
61
+ import { t, DEFAULT_LOCALE } from '../shared/i18n';
62
+ import { Locale } from '../shared/types';
63
+
64
+ let isManualChecking = false;
65
+
66
+ export async function checkForUpdatesManual(): Promise<void> {
67
+ if (isManualChecking) return;
68
+ if (!app.isPackaged) {
69
+ dialog.showMessageBox({ type: 'info', title: 'Claude Mama', message: 'Auto-update is not available in dev mode.' });
70
+ return;
71
+ }
72
+
73
+ isManualChecking = true;
74
+ const locale = getStore().get('locale', DEFAULT_LOCALE) as Locale;
75
+
76
+ // Temporarily disable autoDownload to control timing
77
+ autoUpdater.autoDownload = false;
78
+
79
+ try {
80
+ const result = await autoUpdater.checkForUpdates();
81
+
82
+ if (!result || !result.updateInfo || result.updateInfo.version === app.getVersion()) {
83
+ await dialog.showMessageBox({
84
+ type: 'info',
85
+ title: 'Claude Mama',
86
+ message: t(locale, 'update_up_to_date'),
87
+ detail: `v${app.getVersion()}`,
88
+ });
89
+ return;
90
+ }
91
+
92
+ // Update found — ask user to download
93
+ const newVersion = result.updateInfo.version;
94
+ const { response } = await dialog.showMessageBox({
95
+ type: 'info',
96
+ title: 'Claude Mama',
97
+ message: t(locale, 'update_downloading'),
98
+ detail: `v${app.getVersion()} → v${newVersion}`,
99
+ buttons: ['OK', 'Cancel'],
100
+ defaultId: 0,
101
+ });
102
+
103
+ if (response === 0) {
104
+ // User confirmed — start download
105
+ // update-downloaded handler (in initAutoUpdater) will show restart prompt
106
+ await autoUpdater.downloadUpdate();
107
+ }
108
+ } catch (err: any) {
109
+ await dialog.showMessageBox({
110
+ type: 'error',
111
+ title: 'Claude Mama',
112
+ message: t(locale, 'update_check_failed'),
113
+ detail: err.message,
114
+ });
115
+ } finally {
116
+ autoUpdater.autoDownload = true; // restore for background updates
117
+ isManualChecking = false;
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### 2. `src/main/tray.ts` (lines 83-98)
123
+
124
+ Replace inline update logic with:
125
+ ```typescript
126
+ click: () => { void checkForUpdatesManual(); },
127
+ ```
128
+
129
+ Remove `autoUpdater` import (no longer needed directly).
130
+
131
+ ### 3. `src/main/main.ts` (lines 291-306)
132
+
133
+ Replace inline update logic with:
134
+ ```typescript
135
+ click: () => { void checkForUpdatesManual(); },
136
+ ```
137
+
138
+ Remove `autoUpdater` import from main.ts (no longer needed directly).
139
+
140
+ ### 4. `src/shared/i18n.ts`
141
+
142
+ Add new translation keys (all 4 locales):
143
+
144
+ | Key | EN | KO | JA | ZH |
145
+ |-----|----|----|----|----|
146
+ | `update_downloading` | Downloading update... | 업데이트 다운로드 중... | アップデートをダウンロード中... | 正在下载更新... |
147
+ | `update_check_failed` | Update check failed | 업데이트 확인 실패 | アップデートの確認に失敗しました | 检查更新失败 |
148
+ | `update_ready_title` | Update Ready | 업데이트 준비 완료 | アップデート準備完了 | 更新已准备就绪 |
149
+ | `update_ready_message` | v{version} is ready to install. Restart now? | v{version} 설치 준비 완료. 지금 재시작할까요? | v{version}のインストール準備ができました。今すぐ再起動しますか? | v{version}已准备好安装。现在重启吗? |
150
+
151
+ Existing keys kept: `update_up_to_date`, `tray_check_update`
152
+
153
+ ## Non-Goals
154
+
155
+ - Custom progress bar window (over-engineering for this use case)
156
+ - In-app toast notifications (widget is transparent, complex to implement)
157
+ - Auto-update settings UI (not requested)
158
+ - Linux deb-specific update handling (electron-updater handles AppImage only on Linux)
159
+
160
+ ## Acceptance Criteria
161
+
162
+ - [ ] Clicking "Check for Updates" immediately begins check (no stale UI)
163
+ - [ ] When up to date, dialog shows current version number
164
+ - [ ] When update found, user sees download confirmation with version transition info
165
+ - [ ] User can cancel the download at the confirmation dialog
166
+ - [ ] When download completes, localized restart prompt appears
167
+ - [ ] Error case shows localized error dialog
168
+ - [ ] All update-related strings translated in EN/KO/JA/ZH
169
+ - [ ] No duplicate update logic in tray.ts or main.ts
170
+ - [ ] Dev mode guard preserved (no update check in dev)
171
+ - [ ] Concurrent manual checks are prevented (isManualChecking guard)
172
+ - [ ] autoDownload restored to true after manual check completes