@pennyfarthing/core 11.3.7 → 11.4.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.
Files changed (405) hide show
  1. package/README.md +1 -1
  2. package/package.json +17 -16
  3. package/packages/core/dist/public/css/react.css +1 -1
  4. package/packages/core/dist/public/js/react/react.js +24 -24
  5. package/packages/core/src/public/App.tsx +356 -0
  6. package/packages/core/src/public/components/AgentLoadDialog.tsx +202 -0
  7. package/packages/core/src/public/components/AgentPopup.tsx +308 -0
  8. package/packages/core/src/public/components/ApprovalModal/ApprovalModal.css +35 -0
  9. package/packages/core/src/public/components/ApprovalModal/index.tsx +632 -0
  10. package/packages/core/src/public/components/BikeRackIndex.tsx +53 -0
  11. package/packages/core/src/public/components/BikeRackWorkspace.tsx +217 -0
  12. package/packages/core/src/public/components/CommandPalette.tsx +554 -0
  13. package/packages/core/src/public/components/ConfirmDialog.tsx +168 -0
  14. package/packages/core/src/public/components/ContextIndicator/ContextIndicator.css +85 -0
  15. package/packages/core/src/public/components/ContextIndicator/index.tsx +330 -0
  16. package/packages/core/src/public/components/ContextSparkline.tsx +56 -0
  17. package/packages/core/src/public/components/ControlBar.tsx +636 -0
  18. package/packages/core/src/public/components/DeadCodeDialog.tsx +169 -0
  19. package/packages/core/src/public/components/DiffViewer.tsx +585 -0
  20. package/packages/core/src/public/components/DockviewWorkspace.tsx +749 -0
  21. package/packages/core/src/public/components/Editor.tsx +630 -0
  22. package/packages/core/src/public/components/ErrorBoundary.tsx +67 -0
  23. package/packages/core/src/public/components/FileTree.tsx +379 -0
  24. package/packages/core/src/public/components/FontPicker/FontPicker.css +276 -0
  25. package/packages/core/src/public/components/FontPicker/index.tsx +430 -0
  26. package/packages/core/src/public/components/FullFileTree.tsx +237 -0
  27. package/packages/core/src/public/components/HealthGauge.tsx +181 -0
  28. package/packages/core/src/public/components/Message.tsx +225 -0
  29. package/packages/core/src/public/components/MessageList.tsx +98 -0
  30. package/packages/core/src/public/components/MessageView.tsx +400 -0
  31. package/packages/core/src/public/components/ModeSwitch/ModeSwitch.css +165 -0
  32. package/packages/core/src/public/components/ModeSwitch/index.tsx +372 -0
  33. package/packages/core/src/public/components/PersonaHeader.tsx +242 -0
  34. package/packages/core/src/public/components/ProjectInfoBar.tsx +45 -0
  35. package/packages/core/src/public/components/QuickActions.tsx +267 -0
  36. package/packages/core/src/public/components/SpanTimeline.tsx +352 -0
  37. package/packages/core/src/public/components/StandalonePanel.tsx +82 -0
  38. package/packages/core/src/public/components/StatsStrip.tsx +162 -0
  39. package/packages/core/src/public/components/StreamingContent.tsx +77 -0
  40. package/packages/core/src/public/components/SubagentSpan.tsx +180 -0
  41. package/packages/core/src/public/components/TandemPortrait.tsx +72 -0
  42. package/packages/core/src/public/components/ThemePalette/ThemePalette.css +179 -0
  43. package/packages/core/src/public/components/ThemePalette/index.tsx +326 -0
  44. package/packages/core/src/public/components/ToolCallBlock.tsx +252 -0
  45. package/packages/core/src/public/components/ToolStack.tsx +209 -0
  46. package/packages/core/src/public/components/ToolStatus.tsx +57 -0
  47. package/packages/core/src/public/components/dialogs/CodeMarkersDialog.tsx +169 -0
  48. package/packages/core/src/public/components/dialogs/ComplexityDialog.tsx +163 -0
  49. package/packages/core/src/public/components/dialogs/DependenciesDialog.tsx +120 -0
  50. package/packages/core/src/public/components/dialogs/HotspotsDialog.tsx +451 -0
  51. package/packages/core/src/public/components/dialogs/ToolDialog.tsx +43 -0
  52. package/packages/core/src/public/components/panel-registry.ts +13 -0
  53. package/packages/core/src/public/components/panels/ACPanel.tsx +93 -0
  54. package/packages/core/src/public/components/panels/AcceptanceCriteriaPanel.tsx +104 -0
  55. package/packages/core/src/public/components/panels/AuditLogPanel.tsx +489 -0
  56. package/packages/core/src/public/components/panels/BackgroundPanel.tsx +115 -0
  57. package/packages/core/src/public/components/panels/BikeLanePanel.tsx +214 -0
  58. package/packages/core/src/public/components/panels/DebugPanel.tsx +344 -0
  59. package/packages/core/src/public/components/panels/DiffView.tsx +109 -0
  60. package/packages/core/src/public/components/panels/DiffsPanel.tsx +56 -0
  61. package/packages/core/src/public/components/panels/GitPanel.tsx +260 -0
  62. package/packages/core/src/public/components/panels/HotspotsPanel.tsx +365 -0
  63. package/packages/core/src/public/components/panels/MessageFeed.tsx +39 -0
  64. package/packages/core/src/public/components/panels/MessagePanel.tsx +497 -0
  65. package/packages/core/src/public/components/panels/ProgressPanel.tsx +189 -0
  66. package/packages/core/src/public/components/panels/SettingsPanel.tsx +361 -0
  67. package/packages/core/src/public/components/panels/SprintPanel.tsx +723 -0
  68. package/packages/core/src/public/components/panels/TandemPanel.tsx +104 -0
  69. package/packages/core/src/public/components/panels/TaskTracker.tsx +48 -0
  70. package/packages/core/src/public/components/panels/TeamPanel.tsx +64 -0
  71. package/packages/core/src/public/components/panels/TeamRoster.tsx +67 -0
  72. package/packages/core/src/public/components/panels/TodoPanel.tsx +142 -0
  73. package/packages/core/src/public/components/panels/WorkflowPanel.tsx +224 -0
  74. package/packages/core/src/public/components/panels/index.ts +24 -0
  75. package/packages/core/src/public/components/ui/alert-dialog.tsx +139 -0
  76. package/packages/core/src/public/components/ui/badge.tsx +36 -0
  77. package/packages/core/src/public/components/ui/button.tsx +57 -0
  78. package/packages/core/src/public/components/ui/checkbox.tsx +28 -0
  79. package/packages/core/src/public/components/ui/collapsible.tsx +9 -0
  80. package/packages/core/src/public/components/ui/command.tsx +151 -0
  81. package/packages/core/src/public/components/ui/dialog.tsx +120 -0
  82. package/packages/core/src/public/components/ui/popover.tsx +31 -0
  83. package/packages/core/src/public/components/ui/progress.tsx +28 -0
  84. package/packages/core/src/public/components/ui/scroll-area.tsx +46 -0
  85. package/packages/core/src/public/components/ui/select.tsx +157 -0
  86. package/packages/core/src/public/components/ui/separator.tsx +29 -0
  87. package/packages/core/src/public/components/ui/skeleton.tsx +15 -0
  88. package/packages/core/src/public/components/ui/switch.tsx +27 -0
  89. package/packages/core/src/public/components/ui/toggle-group.tsx +59 -0
  90. package/packages/core/src/public/components/ui/toggle.tsx +43 -0
  91. package/packages/core/src/public/components/ui/tooltip.tsx +30 -0
  92. package/packages/core/src/public/contexts/ClaudeContext.tsx +311 -0
  93. package/packages/core/src/public/contexts/MessageQueueContext.tsx +143 -0
  94. package/packages/core/src/public/css/theme-browser.css +550 -0
  95. package/packages/core/src/public/css/theme-system.css +630 -0
  96. package/packages/core/src/public/hooks/index.ts +49 -0
  97. package/packages/core/src/public/hooks/useAgentLoad.ts +105 -0
  98. package/packages/core/src/public/hooks/useBackgroundTasks.ts +131 -0
  99. package/packages/core/src/public/hooks/useClaude.ts +234 -0
  100. package/packages/core/src/public/hooks/useCodeMarkers.ts +101 -0
  101. package/packages/core/src/public/hooks/useColorScheme.ts +42 -0
  102. package/packages/core/src/public/hooks/useCommandHistory.ts +99 -0
  103. package/packages/core/src/public/hooks/useComplexity.ts +80 -0
  104. package/packages/core/src/public/hooks/useDeadCode.ts +99 -0
  105. package/packages/core/src/public/hooks/useDependencies.ts +82 -0
  106. package/packages/core/src/public/hooks/useDiffs.ts +143 -0
  107. package/packages/core/src/public/hooks/useFileBrowser.ts +73 -0
  108. package/packages/core/src/public/hooks/useFocusPanel.ts +137 -0
  109. package/packages/core/src/public/hooks/useGitStatus.ts +233 -0
  110. package/packages/core/src/public/hooks/useHealthScore.ts +71 -0
  111. package/packages/core/src/public/hooks/useHotspots.ts +123 -0
  112. package/packages/core/src/public/hooks/useLayoutPersistence.ts +141 -0
  113. package/packages/core/src/public/hooks/useMarkdownParser.ts +36 -0
  114. package/packages/core/src/public/hooks/useMarkerActions.ts +234 -0
  115. package/packages/core/src/public/hooks/useMessageQueue.ts +380 -0
  116. package/packages/core/src/public/hooks/useMessageStream.ts +131 -0
  117. package/packages/core/src/public/hooks/usePersona.ts +112 -0
  118. package/packages/core/src/public/hooks/usePlanModeExit.ts +105 -0
  119. package/packages/core/src/public/hooks/useResponsiveLayout.ts +173 -0
  120. package/packages/core/src/public/hooks/useSprint.ts +157 -0
  121. package/packages/core/src/public/hooks/useStatsStrip.ts +204 -0
  122. package/packages/core/src/public/hooks/useStory.ts +135 -0
  123. package/packages/core/src/public/hooks/useSubagentHelper.ts +64 -0
  124. package/packages/core/src/public/hooks/useSyntaxHighlighter.ts +52 -0
  125. package/packages/core/src/public/hooks/useTabCompletion.ts +124 -0
  126. package/packages/core/src/public/hooks/useTandemObservations.ts +165 -0
  127. package/packages/core/src/public/hooks/useTeamMembers.ts +273 -0
  128. package/packages/core/src/public/hooks/useTodos.ts +93 -0
  129. package/packages/core/src/public/hooks/useUserAvatar.ts +54 -0
  130. package/packages/core/src/public/images/cyclist-dark.png +0 -0
  131. package/packages/core/src/public/images/cyclist-light.png +0 -0
  132. package/packages/core/src/public/index.html +14 -0
  133. package/packages/core/src/public/index.tsx +10 -0
  134. package/packages/core/src/public/lib/utils.ts +6 -0
  135. package/packages/core/src/public/styles/dockview-theme.css +376 -0
  136. package/packages/core/src/public/styles/tailwind.css +4454 -0
  137. package/packages/core/src/public/types/message.ts +51 -0
  138. package/packages/core/src/public/utils/avatar-service.ts +73 -0
  139. package/packages/core/src/public/utils/color-presets.ts +940 -0
  140. package/packages/core/src/public/utils/font-presets.ts +362 -0
  141. package/packages/core/src/public/utils/formatDuration.ts +14 -0
  142. package/packages/core/src/public/utils/markdown.ts +249 -0
  143. package/packages/core/src/public/utils/messageFilters.ts +128 -0
  144. package/packages/core/src/public/utils/slash-commands.ts +341 -0
  145. package/packages/core/src/public/utils/subagent-display.ts +146 -0
  146. package/packages/core/src/public/utils/syntax.ts +219 -0
  147. package/packages/core/src/public/utils/toolIntentSummarizer.ts +199 -0
  148. package/packages/core/src/public/utils/toolStackGrouper.ts +106 -0
  149. package/packages/core/src/public/utils/toolTypeColors.ts +45 -0
  150. package/pennyfarthing-dist/pf/__pycache__/__init__.cpython-314.pyc +0 -0
  151. package/pennyfarthing-dist/pf/__pycache__/cli.cpython-314.pyc +0 -0
  152. package/pennyfarthing-dist/pf/__pycache__/context.cpython-314.pyc +0 -0
  153. package/pennyfarthing-dist/pf/bc/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/pennyfarthing-dist/pf/bc/__pycache__/cli.cpython-314.pyc +0 -0
  155. package/pennyfarthing-dist/pf/bc/__pycache__/focus.cpython-314.pyc +0 -0
  156. package/pennyfarthing-dist/pf/bc/__pycache__/split.cpython-314.pyc +0 -0
  157. package/pennyfarthing-dist/pf/bc/cli.py +0 -1
  158. package/pennyfarthing-dist/pf/bc/focus.py +0 -1
  159. package/pennyfarthing-dist/pf/bikerack/__pycache__/__init__.cpython-314.pyc +0 -0
  160. package/pennyfarthing-dist/pf/bikerack/__pycache__/base_panel.cpython-314.pyc +0 -0
  161. package/pennyfarthing-dist/pf/bikerack/__pycache__/cli.cpython-314.pyc +0 -0
  162. package/pennyfarthing-dist/pf/bikerack/__pycache__/git_panel.cpython-314.pyc +0 -0
  163. package/pennyfarthing-dist/pf/bikerack/__pycache__/launcher.cpython-314.pyc +0 -0
  164. package/pennyfarthing-dist/pf/bikerack/__pycache__/portrait_resolver.cpython-314.pyc +0 -0
  165. package/pennyfarthing-dist/pf/bikerack/__pycache__/sprint_panel.cpython-314.pyc +0 -0
  166. package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_data.cpython-314.pyc +0 -0
  167. package/pennyfarthing-dist/pf/bikerack/__pycache__/story_detail_screen.cpython-314.pyc +0 -0
  168. package/pennyfarthing-dist/pf/bikerack/__pycache__/tui.cpython-314.pyc +0 -0
  169. package/pennyfarthing-dist/pf/bikerack/base_panel.py +0 -1
  170. package/pennyfarthing-dist/pf/bikerack/events.py +1 -7
  171. package/pennyfarthing-dist/pf/bikerack/git_panel.py +273 -10
  172. package/pennyfarthing-dist/pf/bikerack/portrait_resolver.py +21 -0
  173. package/pennyfarthing-dist/pf/bikerack/sprint_panel.py +58 -1
  174. package/pennyfarthing-dist/pf/bikerack/tui.py +5 -20
  175. package/pennyfarthing-dist/pf/bmad/__pycache__/__init__.cpython-314.pyc +0 -0
  176. package/pennyfarthing-dist/pf/bmad/__pycache__/cli.cpython-314.pyc +0 -0
  177. package/pennyfarthing-dist/pf/bmad/__pycache__/parser.cpython-314.pyc +0 -0
  178. package/pennyfarthing-dist/pf/bmad/parser.py +15 -9
  179. package/pennyfarthing-dist/pf/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
  180. package/pennyfarthing-dist/pf/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
  181. package/pennyfarthing-dist/pf/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
  182. package/pennyfarthing-dist/pf/common/__pycache__/__init__.cpython-314.pyc +0 -0
  183. package/pennyfarthing-dist/pf/common/__pycache__/config.cpython-314.pyc +0 -0
  184. package/pennyfarthing-dist/pf/common/__pycache__/output.cpython-314.pyc +0 -0
  185. package/pennyfarthing-dist/pf/common/__pycache__/pr_config.cpython-314.pyc +0 -0
  186. package/pennyfarthing-dist/pf/common/__pycache__/themes.cpython-314.pyc +0 -0
  187. package/pennyfarthing-dist/pf/common/pr_config.py +27 -2
  188. package/pennyfarthing-dist/pf/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
  189. package/pennyfarthing-dist/pf/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
  190. package/pennyfarthing-dist/pf/complexity/__pycache__/models.cpython-314.pyc +0 -0
  191. package/pennyfarthing-dist/pf/consultation/__pycache__/__init__.cpython-314.pyc +0 -0
  192. package/pennyfarthing-dist/pf/consultation/__pycache__/cli.cpython-314.pyc +0 -0
  193. package/pennyfarthing-dist/pf/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
  194. package/pennyfarthing-dist/pf/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
  195. package/pennyfarthing-dist/pf/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
  196. package/pennyfarthing-dist/pf/deadcode/__pycache__/models.cpython-314.pyc +0 -0
  197. package/pennyfarthing-dist/pf/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
  198. package/pennyfarthing-dist/pf/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
  199. package/pennyfarthing-dist/pf/dependencies/__pycache__/models.cpython-314.pyc +0 -0
  200. package/pennyfarthing-dist/pf/epic/__pycache__/__init__.cpython-314.pyc +0 -0
  201. package/pennyfarthing-dist/pf/epic/__pycache__/cli.cpython-314.pyc +0 -0
  202. package/pennyfarthing-dist/pf/git_group/__pycache__/__init__.cpython-314.pyc +0 -0
  203. package/pennyfarthing-dist/pf/git_group/__pycache__/cli.cpython-314.pyc +0 -0
  204. package/pennyfarthing-dist/pf/handoff/__pycache__/__init__.cpython-314.pyc +0 -0
  205. package/pennyfarthing-dist/pf/handoff/__pycache__/cli.cpython-314.pyc +0 -0
  206. package/pennyfarthing-dist/pf/handoff/__pycache__/complete_phase.cpython-314.pyc +0 -0
  207. package/pennyfarthing-dist/pf/handoff/__pycache__/marker.cpython-314.pyc +0 -0
  208. package/pennyfarthing-dist/pf/handoff/__pycache__/phase_check.cpython-314.pyc +0 -0
  209. package/pennyfarthing-dist/pf/handoff/__pycache__/resolve_gate.cpython-314.pyc +0 -0
  210. package/pennyfarthing-dist/pf/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
  211. package/pennyfarthing-dist/pf/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
  212. package/pennyfarthing-dist/pf/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
  213. package/pennyfarthing-dist/pf/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
  214. package/pennyfarthing-dist/pf/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
  215. package/pennyfarthing-dist/pf/healthscore/__pycache__/models.cpython-314.pyc +0 -0
  216. package/pennyfarthing-dist/pf/hooks/__pycache__/__init__.cpython-314.pyc +0 -0
  217. package/pennyfarthing-dist/pf/hooks/__pycache__/bell_mode.cpython-314.pyc +0 -0
  218. package/pennyfarthing-dist/pf/hooks/__pycache__/cli.cpython-314.pyc +0 -0
  219. package/pennyfarthing-dist/pf/hooks/__pycache__/context_breaker.cpython-314.pyc +0 -0
  220. package/pennyfarthing-dist/pf/hooks/__pycache__/context_warning.cpython-314.pyc +0 -0
  221. package/pennyfarthing-dist/pf/hooks/__pycache__/cyclist_pretooluse.cpython-314.pyc +0 -0
  222. package/pennyfarthing-dist/pf/hooks/__pycache__/pre_edit_check.cpython-314.pyc +0 -0
  223. package/pennyfarthing-dist/pf/hooks/__pycache__/reflector_check.cpython-314.pyc +0 -0
  224. package/pennyfarthing-dist/pf/hooks/__pycache__/schema_validation.cpython-314.pyc +0 -0
  225. package/pennyfarthing-dist/pf/hooks/__pycache__/session_start.cpython-314.pyc +0 -0
  226. package/pennyfarthing-dist/pf/hooks/__pycache__/session_stop.cpython-314.pyc +0 -0
  227. package/pennyfarthing-dist/pf/hooks/__pycache__/sprint_yaml_validation.cpython-314.pyc +0 -0
  228. package/pennyfarthing-dist/pf/hooks/__pycache__/statusline.cpython-314.pyc +0 -0
  229. package/pennyfarthing-dist/pf/hooks/cyclist-pretooluse-hook.sh +0 -0
  230. package/pennyfarthing-dist/pf/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
  231. package/pennyfarthing-dist/pf/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
  232. package/pennyfarthing-dist/pf/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
  233. package/pennyfarthing-dist/pf/hotspots/__pycache__/models.cpython-314.pyc +0 -0
  234. package/pennyfarthing-dist/pf/jira/__pycache__/__init__.cpython-314.pyc +0 -0
  235. package/pennyfarthing-dist/pf/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
  236. package/pennyfarthing-dist/pf/jira/__pycache__/claim.cpython-314.pyc +0 -0
  237. package/pennyfarthing-dist/pf/jira/__pycache__/cli.cpython-314.pyc +0 -0
  238. package/pennyfarthing-dist/pf/jira/__pycache__/client.cpython-314.pyc +0 -0
  239. package/pennyfarthing-dist/pf/jira/__pycache__/create.cpython-314.pyc +0 -0
  240. package/pennyfarthing-dist/pf/jira/__pycache__/epic.cpython-314.pyc +0 -0
  241. package/pennyfarthing-dist/pf/jira/__pycache__/operations.cpython-314.pyc +0 -0
  242. package/pennyfarthing-dist/pf/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
  243. package/pennyfarthing-dist/pf/jira/__pycache__/story.cpython-314.pyc +0 -0
  244. package/pennyfarthing-dist/pf/jira/__pycache__/sync.cpython-314.pyc +0 -0
  245. package/pennyfarthing-dist/pf/launch/__pycache__/__init__.cpython-314.pyc +0 -0
  246. package/pennyfarthing-dist/pf/launch/__pycache__/cli.cpython-314.pyc +0 -0
  247. package/pennyfarthing-dist/pf/prime/__pycache__/__init__.cpython-314.pyc +0 -0
  248. package/pennyfarthing-dist/pf/prime/__pycache__/cli.cpython-314.pyc +0 -0
  249. package/pennyfarthing-dist/pf/prime/__pycache__/loader.cpython-314.pyc +0 -0
  250. package/pennyfarthing-dist/pf/prime/__pycache__/models.cpython-314.pyc +0 -0
  251. package/pennyfarthing-dist/pf/prime/__pycache__/persona.cpython-314.pyc +0 -0
  252. package/pennyfarthing-dist/pf/prime/__pycache__/session.cpython-314.pyc +0 -0
  253. package/pennyfarthing-dist/pf/prime/__pycache__/tiers.cpython-314.pyc +0 -0
  254. package/pennyfarthing-dist/pf/prime/__pycache__/workflow.cpython-314.pyc +0 -0
  255. package/pennyfarthing-dist/pf/session/__pycache__/__init__.cpython-314.pyc +0 -0
  256. package/pennyfarthing-dist/pf/session/__pycache__/cli.cpython-314.pyc +0 -0
  257. package/pennyfarthing-dist/pf/settings/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/pennyfarthing-dist/pf/settings/__pycache__/cli.cpython-314.pyc +0 -0
  259. package/pennyfarthing-dist/pf/settings/__pycache__/settings.cpython-314.pyc +0 -0
  260. package/pennyfarthing-dist/pf/settings/settings.py +44 -8
  261. package/pennyfarthing-dist/pf/sprint/__pycache__/__init__.cpython-314.pyc +0 -0
  262. package/pennyfarthing-dist/pf/sprint/__pycache__/archive.cpython-314.pyc +0 -0
  263. package/pennyfarthing-dist/pf/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
  264. package/pennyfarthing-dist/pf/sprint/__pycache__/cli.cpython-314.pyc +0 -0
  265. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
  266. package/pennyfarthing-dist/pf/sprint/__pycache__/epic_update.cpython-314.pyc +0 -0
  267. package/pennyfarthing-dist/pf/sprint/__pycache__/loader.cpython-314.pyc +0 -0
  268. package/pennyfarthing-dist/pf/sprint/__pycache__/status.cpython-314.pyc +0 -0
  269. package/pennyfarthing-dist/pf/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
  270. package/pennyfarthing-dist/pf/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
  271. package/pennyfarthing-dist/pf/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
  272. package/pennyfarthing-dist/pf/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
  273. package/pennyfarthing-dist/pf/sprint/__pycache__/validator.cpython-314.pyc +0 -0
  274. package/pennyfarthing-dist/pf/sprint/__pycache__/work.cpython-314.pyc +0 -0
  275. package/pennyfarthing-dist/pf/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
  276. package/pennyfarthing-dist/pf/sprint/story_finish.py +14 -2
  277. package/pennyfarthing-dist/pf/sprint/validator.py +7 -7
  278. package/pennyfarthing-dist/pf/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  279. package/pennyfarthing-dist/pf/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  280. package/pennyfarthing-dist/pf/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
  281. package/pennyfarthing-dist/pf/tests/test_sprint_validator.py +44 -0
  282. package/pennyfarthing-dist/pf/theme/__pycache__/__init__.cpython-314.pyc +0 -0
  283. package/pennyfarthing-dist/pf/theme/__pycache__/cli.cpython-314.pyc +0 -0
  284. package/pennyfarthing-dist/pf/validate/__pycache__/__init__.cpython-314.pyc +0 -0
  285. package/pennyfarthing-dist/pf/validate/__pycache__/cli.cpython-314.pyc +0 -0
  286. package/pennyfarthing-dist/pf/workflow/__pycache__/__init__.cpython-314.pyc +0 -0
  287. package/pennyfarthing-dist/pf/workflow/__pycache__/cli.cpython-314.pyc +0 -0
  288. package/pennyfarthing-dist/pf/workflow/__pycache__/helpers.cpython-314.pyc +0 -0
  289. package/pennyfarthing-dist/pf/workflow/__pycache__/scale.cpython-314.pyc +0 -0
  290. package/pennyfarthing-dist/pf/workflow/__pycache__/state.cpython-314.pyc +0 -0
  291. package/pennyfarthing-dist/scripts/core/agent-session.sh +0 -0
  292. package/pennyfarthing-dist/scripts/core/check-context.sh +0 -0
  293. package/pennyfarthing-dist/scripts/core/dialogue-manager.sh +0 -0
  294. package/pennyfarthing-dist/scripts/core/pf.sh +0 -0
  295. package/pennyfarthing-dist/scripts/core/phase-check-start.sh +0 -0
  296. package/pennyfarthing-dist/scripts/core/prime.sh +0 -0
  297. package/pennyfarthing-dist/scripts/cyclist/is-cyclist.sh +0 -0
  298. package/pennyfarthing-dist/scripts/git/create-feature-branches.sh +0 -0
  299. package/pennyfarthing-dist/scripts/git/git-status-all.sh +0 -0
  300. package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +0 -0
  301. package/pennyfarthing-dist/scripts/git/release.sh +0 -0
  302. package/pennyfarthing-dist/scripts/git/worktree-manager.sh +0 -0
  303. package/pennyfarthing-dist/scripts/health/drift-detection.sh +0 -0
  304. package/pennyfarthing-dist/scripts/hooks/bell-mode-hook.sh +0 -0
  305. package/pennyfarthing-dist/scripts/hooks/context-circuit-breaker.sh +0 -0
  306. package/pennyfarthing-dist/scripts/hooks/context-warning.sh +0 -0
  307. package/pennyfarthing-dist/scripts/hooks/cyclist-pretooluse-hook.sh +0 -0
  308. package/pennyfarthing-dist/scripts/hooks/dispatcher-template.sh +0 -0
  309. package/pennyfarthing-dist/scripts/hooks/otel-auto-config.sh +0 -0
  310. package/pennyfarthing-dist/scripts/hooks/post-merge.sh +0 -0
  311. package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +0 -0
  312. package/pennyfarthing-dist/scripts/hooks/pre-edit-check.sh +0 -0
  313. package/pennyfarthing-dist/scripts/hooks/pre-push.sh +0 -0
  314. package/pennyfarthing-dist/scripts/hooks/question-reflector-check.sh +0 -0
  315. package/pennyfarthing-dist/scripts/hooks/question_reflector_check.py +0 -0
  316. package/pennyfarthing-dist/scripts/hooks/schema-validation.sh +0 -0
  317. package/pennyfarthing-dist/scripts/hooks/session-start.sh +0 -0
  318. package/pennyfarthing-dist/scripts/hooks/session-stop.sh +0 -0
  319. package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +0 -0
  320. package/pennyfarthing-dist/scripts/hooks/welcome-hook.sh +0 -0
  321. package/pennyfarthing-dist/scripts/jira/create-jira-epic.sh +0 -0
  322. package/pennyfarthing-dist/scripts/jira/create-jira-story.sh +0 -0
  323. package/pennyfarthing-dist/scripts/jira/jira-claim-story.sh +0 -0
  324. package/pennyfarthing-dist/scripts/jira/jira-reconcile.sh +0 -0
  325. package/pennyfarthing-dist/scripts/jira/jira-sync-story.sh +0 -0
  326. package/pennyfarthing-dist/scripts/jira/sync-epic-jira.sh +0 -0
  327. package/pennyfarthing-dist/scripts/lib/background-tasks.sh +0 -0
  328. package/pennyfarthing-dist/scripts/lib/checkpoint.sh +0 -0
  329. package/pennyfarthing-dist/scripts/lib/common.sh +0 -0
  330. package/pennyfarthing-dist/scripts/lib/env.sh +0 -0
  331. package/pennyfarthing-dist/scripts/lib/file-lock.sh +0 -0
  332. package/pennyfarthing-dist/scripts/lib/find-root.sh +1 -1
  333. package/pennyfarthing-dist/scripts/lib/logging.sh +0 -0
  334. package/pennyfarthing-dist/scripts/lib/retry.sh +0 -0
  335. package/pennyfarthing-dist/scripts/lib/run-pf.sh +0 -0
  336. package/pennyfarthing-dist/scripts/maintenance/migrate-theme-schema.mjs +0 -0
  337. package/pennyfarthing-dist/scripts/maintenance/sidecar-health.sh +0 -0
  338. package/pennyfarthing-dist/scripts/misc/add-short-names.sh +0 -0
  339. package/pennyfarthing-dist/scripts/misc/add_short_names.py +0 -0
  340. package/pennyfarthing-dist/scripts/misc/backlog.sh +0 -0
  341. package/pennyfarthing-dist/scripts/misc/check-status.sh +0 -0
  342. package/pennyfarthing-dist/scripts/misc/find-related-work.sh +0 -0
  343. package/pennyfarthing-dist/scripts/misc/generate-skill-docs.sh +0 -0
  344. package/pennyfarthing-dist/scripts/misc/log-skill-usage.sh +0 -0
  345. package/pennyfarthing-dist/scripts/misc/migrate-bmad-workflow.sh +0 -0
  346. package/pennyfarthing-dist/scripts/misc/migrate_bmad_workflow.py +0 -1
  347. package/pennyfarthing-dist/scripts/misc/repo-scan.sh +0 -0
  348. package/pennyfarthing-dist/scripts/misc/repo-utils.sh +0 -0
  349. package/pennyfarthing-dist/scripts/misc/run-ci.sh +0 -0
  350. package/pennyfarthing-dist/scripts/misc/run-timestamp.sh +0 -0
  351. package/pennyfarthing-dist/scripts/misc/session-cleanup.sh +0 -0
  352. package/pennyfarthing-dist/scripts/misc/skill-usage-report.sh +0 -0
  353. package/pennyfarthing-dist/scripts/misc/statusline.sh +0 -0
  354. package/pennyfarthing-dist/scripts/misc/uninstall.sh +0 -0
  355. package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +0 -0
  356. package/pennyfarthing-dist/scripts/portraits/generate-portraits.py +13 -13
  357. package/pennyfarthing-dist/scripts/portraits/generate-portraits.sh +0 -0
  358. package/pennyfarthing-dist/scripts/portraits/generate-tandem-portraits.sh +0 -0
  359. package/pennyfarthing-dist/scripts/story/create-story.sh +0 -0
  360. package/pennyfarthing-dist/scripts/story/size-story.sh +0 -0
  361. package/pennyfarthing-dist/scripts/story/story-template.sh +0 -0
  362. package/pennyfarthing-dist/scripts/tests/check.test.sh +0 -0
  363. package/pennyfarthing-dist/scripts/tests/dev-story-workflow-import.test.sh +0 -0
  364. package/pennyfarthing-dist/scripts/tests/epics-and-stories-workflow-import.test.sh +0 -0
  365. package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +0 -0
  366. package/pennyfarthing-dist/scripts/tests/implementation-readiness-workflow-import.test.sh +0 -0
  367. package/pennyfarthing-dist/scripts/tests/migrate-bmad-workflow.test.sh +0 -0
  368. package/pennyfarthing-dist/scripts/tests/prd-workflow-import.test.sh +0 -0
  369. package/pennyfarthing-dist/scripts/tests/project-context-workflow-import.test.sh +0 -0
  370. package/pennyfarthing-dist/scripts/tests/test-character-voice.sh +0 -0
  371. package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +0 -0
  372. package/pennyfarthing-dist/scripts/tests/test-post-merge-hook.sh +0 -0
  373. package/pennyfarthing-dist/scripts/tests/test-session-checkpoint.sh +0 -0
  374. package/pennyfarthing-dist/scripts/tests/test-solo-command.sh +0 -0
  375. package/pennyfarthing-dist/scripts/tests/ux-design-workflow-import.test.sh +0 -0
  376. package/pennyfarthing-dist/scripts/theme/list-themes.sh +0 -0
  377. package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -0
  378. package/pennyfarthing-dist/scripts/workflow/check.py +4 -6
  379. package/pennyfarthing-dist/scripts/workflow/check.sh +0 -0
  380. package/pennyfarthing-dist/scripts/workflow/complete-step.py +2 -2
  381. package/pennyfarthing-dist/scripts/workflow/finish-story.sh +0 -0
  382. package/pennyfarthing-dist/scripts/workflow/fix-session-phase.sh +0 -0
  383. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.py +0 -0
  384. package/pennyfarthing-dist/scripts/workflow/get-workflow-type.sh +0 -0
  385. package/pennyfarthing-dist/scripts/workflow/list-workflows.sh +0 -0
  386. package/pennyfarthing-dist/scripts/workflow/phase-owner.sh +0 -0
  387. package/pennyfarthing-dist/scripts/workflow/resume-workflow.sh +0 -0
  388. package/pennyfarthing-dist/scripts/workflow/show-workflow.sh +0 -0
  389. package/pennyfarthing-dist/scripts/workflow/start-workflow.sh +0 -0
  390. package/pennyfarthing-dist/scripts/workflow/workflow-status.sh +0 -0
  391. package/pennyfarthing-dist/skills/pf-story/scripts/create-story.sh +0 -0
  392. package/pennyfarthing-dist/skills/pf-story/scripts/size-story.sh +0 -0
  393. package/pennyfarthing-dist/skills/pf-story/scripts/story-template.sh +0 -0
  394. package/pennyfarthing-dist/skills/skill-registry.yaml +19 -0
  395. package/pennyfarthing-dist/workflows/release/steps/step-10-publish.md +41 -9
  396. package/pennyfarthing-dist/workflows/tdd-tandem.yaml +15 -2
  397. package/packages/core/dist/workflow/__test_context_watch__/.session/.tandem-turn-counter +0 -1
  398. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-session.md +0 -3
  399. package/packages/core/dist/workflow/__test_context_watch__/.session/95-6-tandem-architect.md +0 -6
  400. package/packages/core/dist/workflow/__test_file_watch__/.session/95-4-tandem-architect.md +0 -6
  401. package/packages/core/dist/workflow/__test_file_watch__/workdir/trigger.ts +0 -1
  402. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-architect.md +0 -6
  403. package/packages/core/dist/workflow/__test_tool_watch__/.session/95-5-tandem-toolcalls.jsonl +0 -1
  404. package/pennyfarthing-dist/pf/bikerack/changed_panel.py +0 -201
  405. package/scripts/README.md +0 -41
@@ -0,0 +1,430 @@
1
+ /**
2
+ * FontPicker Component
3
+ *
4
+ * Font selection with system font browser via queryLocalFonts() API.
5
+ * Story MSSCI-12769 - Font Customization
6
+ *
7
+ * Features:
8
+ * - shadcn Select-based dropdown with system font discovery
9
+ * - Live font preview in each option
10
+ * - Monospace detection for code font filtering
11
+ * - Graceful fallback when queryLocalFonts() unavailable
12
+ * - Font size picker (segmented control)
13
+ * - ARIA accessibility via Radix Select
14
+ */
15
+
16
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectGroup,
23
+ SelectItem,
24
+ SelectLabel,
25
+ SelectTrigger,
26
+ SelectValue,
27
+ } from '@/components/ui/select';
28
+ import {
29
+ UI_FONT_PRESETS,
30
+ CODE_FONT_PRESETS,
31
+ FONT_SIZE_SCALE,
32
+ FontSize,
33
+ } from '../../utils/font-presets';
34
+ import './FontPicker.css';
35
+
36
+ // =============================================================================
37
+ // Types
38
+ // =============================================================================
39
+
40
+ export interface FontPickerProps {
41
+ type: 'ui' | 'code';
42
+ currentFont: string;
43
+ customFont?: string;
44
+ onSelect: (presetId: string, customFamily?: string) => void;
45
+ onCustomFontChange?: (family: string) => void;
46
+ className?: string;
47
+ }
48
+
49
+ export interface FontSizePickerProps {
50
+ currentSize: FontSize;
51
+ onSelect: (size: FontSize) => void;
52
+ className?: string;
53
+ }
54
+
55
+ interface SystemFont {
56
+ family: string;
57
+ isMonospace: boolean;
58
+ }
59
+
60
+ // =============================================================================
61
+ // Monospace Detection via OpenType `post` table
62
+ // =============================================================================
63
+
64
+ /**
65
+ * Parse SFNT table directory to find a table's offset and length.
66
+ * SFNT header: version(4) + numTables(2) + searchRange(2) + entrySelector(2) + rangeShift(2) = 12
67
+ * Each table record: tag(4) + checksum(4) + offset(4) + length(4) = 16
68
+ */
69
+ function findSfntTable(view: DataView, tag: string): { offset: number; length: number } | null {
70
+ const numTables = view.getUint16(4);
71
+ for (let i = 0; i < numTables; i++) {
72
+ const recordOffset = 12 + i * 16;
73
+ const tableTag = String.fromCharCode(
74
+ view.getUint8(recordOffset),
75
+ view.getUint8(recordOffset + 1),
76
+ view.getUint8(recordOffset + 2),
77
+ view.getUint8(recordOffset + 3),
78
+ );
79
+ if (tableTag === tag) {
80
+ return {
81
+ offset: view.getUint32(recordOffset + 8),
82
+ length: view.getUint32(recordOffset + 12),
83
+ };
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Read `isFixedPitch` from the `post` table.
91
+ * post layout: version(4) + italicAngle(4) + underlinePosition(2) + underlineThickness(2) = offset 12
92
+ * isFixedPitch is uint32 at offset 12: 0 = proportional, non-zero = monospace.
93
+ */
94
+ async function detectMonospaceFromBlob(fontData: { blob: () => Promise<Blob> }): Promise<boolean> {
95
+ try {
96
+ const blob = await fontData.blob();
97
+ const buffer = await blob.arrayBuffer();
98
+ const view = new DataView(buffer);
99
+
100
+ const post = findSfntTable(view, 'post');
101
+ if (post) {
102
+ const isFixedPitch = view.getUint32(post.offset + 12);
103
+ return isFixedPitch !== 0;
104
+ }
105
+ return false;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ // =============================================================================
112
+ // System Font Discovery
113
+ // =============================================================================
114
+
115
+ interface FontDataEntry {
116
+ family: string;
117
+ fullName: string;
118
+ postscriptName: string;
119
+ style: string;
120
+ blob: () => Promise<Blob>;
121
+ }
122
+
123
+ const FONT_CACHE_KEY = 'cyclist-system-fonts';
124
+
125
+ interface FontCacheData {
126
+ fonts: SystemFont[];
127
+ count: number; // number of font families — if it changes, fonts were installed/removed
128
+ }
129
+
130
+ function loadCachedFonts(): SystemFont[] | null {
131
+ try {
132
+ const raw = localStorage.getItem(FONT_CACHE_KEY);
133
+ if (!raw) return null;
134
+ const data: FontCacheData = JSON.parse(raw);
135
+ if (data.fonts?.length > 0) return data.fonts;
136
+ return null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function saveCachedFonts(fonts: SystemFont[], count: number): void {
143
+ try {
144
+ const data: FontCacheData = { fonts, count };
145
+ localStorage.setItem(FONT_CACHE_KEY, JSON.stringify(data));
146
+ } catch {
147
+ // localStorage full or unavailable — not critical
148
+ }
149
+ }
150
+
151
+ let systemFontsPromise: Promise<SystemFont[]> | null = null;
152
+
153
+ async function getSystemFonts(): Promise<SystemFont[]> {
154
+ if (systemFontsPromise) return systemFontsPromise;
155
+
156
+ systemFontsPromise = (async () => {
157
+ if (!('queryLocalFonts' in window)) {
158
+ return [];
159
+ }
160
+
161
+ try {
162
+ const fonts: FontDataEntry[] = await (window as unknown as { queryLocalFonts: () => Promise<FontDataEntry[]> }).queryLocalFonts();
163
+
164
+ // Deduplicate by family name, keep one FontData per family for mono detection
165
+ const familyMap = new Map<string, FontDataEntry>();
166
+ for (const font of fonts) {
167
+ if (!familyMap.has(font.family)) {
168
+ familyMap.set(font.family, font);
169
+ }
170
+ }
171
+
172
+ const familyCount = familyMap.size;
173
+
174
+ // Check localStorage cache — reuse if font count hasn't changed
175
+ const cached = loadCachedFonts();
176
+ if (cached && cached.length > 0) {
177
+ // Load raw cached data to check count
178
+ try {
179
+ const raw = localStorage.getItem(FONT_CACHE_KEY);
180
+ if (raw) {
181
+ const data: FontCacheData = JSON.parse(raw);
182
+ if (data.count === familyCount) {
183
+ return cached;
184
+ }
185
+ }
186
+ } catch {
187
+ // Fall through to re-detect
188
+ }
189
+ }
190
+
191
+ // Detect monospace via post table in parallel
192
+ const entries = Array.from(familyMap.entries());
193
+ const monoResults = await Promise.all(
194
+ entries.map(([, fontData]) => detectMonospaceFromBlob(fontData))
195
+ );
196
+
197
+ const result: SystemFont[] = entries.map(([family], i) => ({
198
+ family,
199
+ isMonospace: monoResults[i],
200
+ }));
201
+
202
+ result.sort((a, b) => a.family.localeCompare(b.family));
203
+ saveCachedFonts(result, familyCount);
204
+ return result;
205
+ } catch {
206
+ // Permission denied or API error
207
+ return [];
208
+ }
209
+ })();
210
+
211
+ return systemFontsPromise;
212
+ }
213
+
214
+ // Prefix for system font values to distinguish from preset IDs
215
+ const SYSTEM_FONT_PREFIX = 'system-font:';
216
+
217
+ // =============================================================================
218
+ // FontPicker Component
219
+ // =============================================================================
220
+
221
+ export function FontPicker({
222
+ type,
223
+ currentFont,
224
+ customFont,
225
+ onSelect,
226
+ onCustomFontChange,
227
+ className = '',
228
+ }: FontPickerProps): React.ReactElement {
229
+ const [customValue, setCustomValue] = useState(customFont || '');
230
+ const [systemFonts, setSystemFonts] = useState<SystemFont[]>([]);
231
+ const [fontsLoaded, setFontsLoaded] = useState(false);
232
+
233
+ const presets = type === 'ui' ? UI_FONT_PRESETS : CODE_FONT_PRESETS;
234
+ const currentPreset = presets.find(p => p.id === currentFont);
235
+
236
+ // Load system fonts eagerly
237
+ useEffect(() => {
238
+ if (!fontsLoaded) {
239
+ getSystemFonts().then(fonts => {
240
+ setSystemFonts(fonts);
241
+ setFontsLoaded(true);
242
+ });
243
+ }
244
+ }, [fontsLoaded]);
245
+
246
+ // Filter system fonts: English-only (Latin names), monospace-only for code
247
+ const filteredSystemFonts = useMemo(() => {
248
+ let fonts = systemFonts;
249
+
250
+ // Filter to fonts with Latin-script names (excludes CJK, Arabic, Devanagari, etc.)
251
+ fonts = fonts.filter(f => /^[\x20-\x7E\u00C0-\u024F]+$/.test(f.family));
252
+
253
+ // For code fonts, only show monospace
254
+ if (type === 'code') {
255
+ fonts = fonts.filter(f => f.isMonospace);
256
+ }
257
+
258
+ return fonts;
259
+ }, [systemFonts, type]);
260
+
261
+ // Non-custom presets for display
262
+ const displayPresets = useMemo(() => {
263
+ return presets.filter(p => !p.isCustom);
264
+ }, [presets]);
265
+
266
+ // Update custom value when prop changes
267
+ useEffect(() => {
268
+ if (customFont !== undefined) {
269
+ setCustomValue(customFont);
270
+ }
271
+ }, [customFont]);
272
+
273
+ // Compute the Select value: for system fonts we encode as "system-font:FamilyName"
274
+ const selectValue = useMemo(() => {
275
+ if (currentFont === 'custom' && customValue) {
276
+ // Check if this matches a system font
277
+ const isSystemFont = systemFonts.some(f => f.family === customValue);
278
+ if (isSystemFont) {
279
+ return `${SYSTEM_FONT_PREFIX}${customValue}`;
280
+ }
281
+ return 'custom';
282
+ }
283
+ return currentFont;
284
+ }, [currentFont, customValue, systemFonts]);
285
+
286
+ const handleValueChange = useCallback(
287
+ (value: string) => {
288
+ if (value.startsWith(SYSTEM_FONT_PREFIX)) {
289
+ const family = value.slice(SYSTEM_FONT_PREFIX.length);
290
+ onSelect('custom', family);
291
+ } else if (value === 'custom') {
292
+ onSelect('custom');
293
+ } else {
294
+ onSelect(value);
295
+ }
296
+ },
297
+ [onSelect]
298
+ );
299
+
300
+ const handleCustomChange = useCallback(
301
+ (e: React.ChangeEvent<HTMLInputElement>) => {
302
+ const value = e.target.value;
303
+ setCustomValue(value);
304
+ onCustomFontChange?.(value);
305
+ if (currentFont === 'custom') {
306
+ onSelect('custom', value);
307
+ }
308
+ },
309
+ [currentFont, onSelect, onCustomFontChange]
310
+ );
311
+
312
+ const hasSystemFonts = systemFonts.length > 0;
313
+ const showCustomInput = currentFont === 'custom' && !hasSystemFonts;
314
+
315
+ return (
316
+ <div className={`font-picker ${className}`}>
317
+ <Select value={selectValue} onValueChange={handleValueChange}>
318
+ <SelectTrigger
319
+ className="font-picker-trigger"
320
+ aria-label={`Select ${type} font`}
321
+ style={{ fontFamily: currentPreset?.fontFamily || (customValue ? `"${customValue}"` : 'inherit') }}
322
+ >
323
+ <SelectValue placeholder="Select font..." />
324
+ </SelectTrigger>
325
+ <SelectContent className="max-h-[300px]">
326
+ {/* Presets section */}
327
+ {displayPresets.length > 0 && (
328
+ <SelectGroup>
329
+ <SelectLabel>Presets</SelectLabel>
330
+ {displayPresets.map((preset) => (
331
+ <SelectItem
332
+ key={preset.id}
333
+ value={preset.id}
334
+ style={{ fontFamily: preset.fontFamily || 'inherit' }}
335
+ >
336
+ <span className="font-picker-item-content">
337
+ <span className="font-name">{preset.name}</span>
338
+ <span className="font-preview" style={{ fontFamily: preset.fontFamily }}>
339
+ Aa
340
+ </span>
341
+ </span>
342
+ </SelectItem>
343
+ ))}
344
+ </SelectGroup>
345
+ )}
346
+
347
+ {/* System fonts section */}
348
+ {hasSystemFonts && filteredSystemFonts.length > 0 && (
349
+ <SelectGroup>
350
+ <SelectLabel>System Fonts ({filteredSystemFonts.length})</SelectLabel>
351
+ {filteredSystemFonts.map((font) => (
352
+ <SelectItem
353
+ key={font.family}
354
+ value={`${SYSTEM_FONT_PREFIX}${font.family}`}
355
+ style={{ fontFamily: `"${font.family}", inherit` }}
356
+ >
357
+ <span className="font-picker-item-content">
358
+ <span className="font-name">{font.family}</span>
359
+ <span className="font-preview" style={{ fontFamily: `"${font.family}"` }}>
360
+ Aa
361
+ </span>
362
+ </span>
363
+ </SelectItem>
364
+ ))}
365
+ </SelectGroup>
366
+ )}
367
+
368
+ {/* Custom option (fallback when no system fonts) */}
369
+ {!hasSystemFonts && fontsLoaded && (
370
+ <SelectGroup>
371
+ <SelectLabel>Custom</SelectLabel>
372
+ <SelectItem value="custom">
373
+ Custom...
374
+ </SelectItem>
375
+ </SelectGroup>
376
+ )}
377
+ </SelectContent>
378
+ </Select>
379
+
380
+ {showCustomInput && (
381
+ <input
382
+ type="text"
383
+ className="font-picker-custom-input"
384
+ value={customValue}
385
+ onChange={handleCustomChange}
386
+ placeholder="Enter font family..."
387
+ aria-label="Custom font family"
388
+ />
389
+ )}
390
+ </div>
391
+ );
392
+ }
393
+
394
+ // =============================================================================
395
+ // FontSizePicker Component
396
+ // =============================================================================
397
+
398
+ const SIZES: FontSize[] = ['xs', 'sm', 'base', 'lg', 'xl'];
399
+
400
+ export function FontSizePicker({
401
+ currentSize,
402
+ onSelect,
403
+ className = '',
404
+ }: FontSizePickerProps): React.ReactElement {
405
+ return (
406
+ <TooltipProvider delayDuration={300}>
407
+ <div className={`font-size-picker ${className}`} role="group" aria-label="Font size">
408
+ {SIZES.map((size) => (
409
+ <Tooltip key={size}>
410
+ <TooltipTrigger asChild>
411
+ <Button
412
+ variant={size === currentSize ? 'secondary' : 'ghost'}
413
+ size="sm"
414
+ className={`font-size-option ${size === currentSize ? 'active' : ''}`}
415
+ data-size={size}
416
+ onClick={() => onSelect(size)}
417
+ aria-pressed={size === currentSize}
418
+ >
419
+ {size.toUpperCase()}
420
+ </Button>
421
+ </TooltipTrigger>
422
+ <TooltipContent>{`${FONT_SIZE_SCALE[size]} (${size})`}</TooltipContent>
423
+ </Tooltip>
424
+ ))}
425
+ </div>
426
+ </TooltipProvider>
427
+ );
428
+ }
429
+
430
+ export default FontPicker;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * FullFileTree - Complete directory tree with changed file highlighting
3
+ *
4
+ * Displays the full project file tree with lazy-loaded directories.
5
+ * Changed files are highlighted with git status colors (created/modified/deleted).
6
+ * Uses /api/files for directory listing and /ws/git for change status.
7
+ */
8
+
9
+ import React, { useState, useCallback, useEffect } from 'react';
10
+ import { Badge } from '@/components/ui/badge';
11
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
12
+ import { ScrollArea } from '@/components/ui/scroll-area';
13
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
14
+ import { useFileBrowser, DirectoryEntry } from '../hooks/useFileBrowser';
15
+ import type { FileStatus } from './FileTree';
16
+
17
+ // =============================================================================
18
+ // Types
19
+ // =============================================================================
20
+
21
+ export interface FullFileTreeProps {
22
+ /** Map of file path → git status for highlighting */
23
+ changedFiles: Map<string, FileStatus>;
24
+ /** Callback when a file is clicked */
25
+ onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
26
+ }
27
+
28
+ // =============================================================================
29
+ // File Item Component
30
+ // =============================================================================
31
+
32
+ function TreeFileItem({
33
+ entry,
34
+ status,
35
+ depth,
36
+ onFileClick,
37
+ }: {
38
+ entry: DirectoryEntry;
39
+ status?: FileStatus;
40
+ depth: number;
41
+ onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
42
+ }): React.ReactElement {
43
+ const statusIcon = status === 'created' ? '+' : status === 'modified' ? '~' : status === 'deleted' ? '-' : null;
44
+
45
+ return (
46
+ <Tooltip>
47
+ <TooltipTrigger asChild>
48
+ <div
49
+ role="treeitem"
50
+ className={`file-item${status ? ` file-${status}` : ''}`}
51
+ style={{ paddingLeft: `${12 + depth * 16}px` }}
52
+ tabIndex={0}
53
+ aria-label={`${entry.name}${status ? `, ${status}` : ''}`}
54
+ onClick={() => onFileClick?.(entry, status)}
55
+ onKeyDown={(e) => {
56
+ if (e.key === 'Enter' || e.key === ' ') {
57
+ e.preventDefault();
58
+ onFileClick?.(entry, status);
59
+ }
60
+ }}
61
+ >
62
+ {statusIcon && (
63
+ <span className={`status-icon status-${status}`} aria-hidden="true">
64
+ {statusIcon}
65
+ </span>
66
+ )}
67
+ <span className={`file-name${status === 'deleted' ? '' : ''}`}>{entry.name}</span>
68
+ </div>
69
+ </TooltipTrigger>
70
+ <TooltipContent>{entry.path}</TooltipContent>
71
+ </Tooltip>
72
+ );
73
+ }
74
+
75
+ // =============================================================================
76
+ // Directory Node Component (recursive)
77
+ // =============================================================================
78
+
79
+ function TreeDirectoryNode({
80
+ entry,
81
+ depth,
82
+ changedFiles,
83
+ onFileClick,
84
+ fetchDirectory,
85
+ cache,
86
+ loading,
87
+ }: {
88
+ entry: DirectoryEntry;
89
+ depth: number;
90
+ changedFiles: Map<string, FileStatus>;
91
+ onFileClick?: (entry: DirectoryEntry, status?: FileStatus) => void;
92
+ fetchDirectory: (path: string) => Promise<void>;
93
+ cache: Record<string, DirectoryEntry[]>;
94
+ loading: Set<string>;
95
+ }): React.ReactElement {
96
+ // Check if this directory contains any changed files
97
+ const hasChanges = Array.from(changedFiles.keys()).some(
98
+ (filePath) => filePath.startsWith(entry.path + '/')
99
+ );
100
+
101
+ const [isOpen, setIsOpen] = useState(hasChanges);
102
+ const children = cache[entry.path];
103
+ const isLoading = loading.has(entry.path);
104
+
105
+ // Auto-fetch children when directory has changes and is opened by default
106
+ useEffect(() => {
107
+ if (hasChanges && !children && !loading.has(entry.path)) {
108
+ fetchDirectory(entry.path);
109
+ }
110
+ }, [hasChanges, children, entry.path, fetchDirectory, loading]);
111
+
112
+ // Auto-open when changes appear in this directory
113
+ useEffect(() => {
114
+ if (hasChanges) {
115
+ setIsOpen(true);
116
+ }
117
+ }, [hasChanges]);
118
+
119
+ const handleToggle = useCallback(() => {
120
+ const willOpen = !isOpen;
121
+ setIsOpen(willOpen);
122
+ if (willOpen && !children) {
123
+ fetchDirectory(entry.path);
124
+ }
125
+ }, [isOpen, children, entry.path, fetchDirectory]);
126
+
127
+ return (
128
+ <Collapsible open={isOpen} onOpenChange={handleToggle}>
129
+ <CollapsibleTrigger asChild>
130
+ <div
131
+ className={`directory-header${hasChanges ? ' has-changes' : ''}`}
132
+ style={{ paddingLeft: `${4 + depth * 16}px` }}
133
+ >
134
+ <span className="directory-toggle">
135
+ <span className="toggle-icon">{isOpen ? '▼' : '▶'}</span>
136
+ </span>
137
+ <span className="directory-name">{entry.name}</span>
138
+ </div>
139
+ </CollapsibleTrigger>
140
+ <CollapsibleContent>
141
+ {isLoading && (
142
+ <div
143
+ className="tree-loading"
144
+ style={{ paddingLeft: `${12 + (depth + 1) * 16}px` }}
145
+ >
146
+ Loading...
147
+ </div>
148
+ )}
149
+ {children?.map((child) =>
150
+ child.type === 'directory' ? (
151
+ <TreeDirectoryNode
152
+ key={child.path}
153
+ entry={child}
154
+ depth={depth + 1}
155
+ changedFiles={changedFiles}
156
+ onFileClick={onFileClick}
157
+ fetchDirectory={fetchDirectory}
158
+ cache={cache}
159
+ loading={loading}
160
+ />
161
+ ) : (
162
+ <TreeFileItem
163
+ key={child.path}
164
+ entry={child}
165
+ status={changedFiles.get(child.path)}
166
+ depth={depth + 1}
167
+ onFileClick={onFileClick}
168
+ />
169
+ )
170
+ )}
171
+ </CollapsibleContent>
172
+ </Collapsible>
173
+ );
174
+ }
175
+
176
+ // =============================================================================
177
+ // FullFileTree Component
178
+ // =============================================================================
179
+
180
+ export function FullFileTree({ changedFiles, onFileClick }: FullFileTreeProps): React.ReactElement {
181
+ const { cache, loading, error, fetchDirectory } = useFileBrowser();
182
+
183
+ // Load root directory on mount
184
+ useEffect(() => {
185
+ fetchDirectory('');
186
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
187
+
188
+ const rootEntries = cache['__root__'];
189
+ const changedCount = changedFiles.size;
190
+
191
+ return (
192
+ <TooltipProvider delayDuration={300}>
193
+ <div role="tree" aria-label="Project files" className="filetree full-filetree">
194
+ {changedCount > 0 && (
195
+ <Badge
196
+ variant="secondary"
197
+ data-testid="file-count-badge"
198
+ className="file-count-badge"
199
+ aria-label={`${changedCount} files changed`}
200
+ >
201
+ {changedCount}
202
+ </Badge>
203
+ )}
204
+ <ScrollArea className="filetree-scroll">
205
+ {error && <div className="tree-error">{error}</div>}
206
+ {!rootEntries && !error && (
207
+ <div className="tree-loading">Loading project files...</div>
208
+ )}
209
+ {rootEntries?.map((entry) =>
210
+ entry.type === 'directory' ? (
211
+ <TreeDirectoryNode
212
+ key={entry.path}
213
+ entry={entry}
214
+ depth={0}
215
+ changedFiles={changedFiles}
216
+ onFileClick={onFileClick}
217
+ fetchDirectory={fetchDirectory}
218
+ cache={cache}
219
+ loading={loading}
220
+ />
221
+ ) : (
222
+ <TreeFileItem
223
+ key={entry.path}
224
+ entry={entry}
225
+ status={changedFiles.get(entry.path)}
226
+ depth={0}
227
+ onFileClick={onFileClick}
228
+ />
229
+ )
230
+ )}
231
+ </ScrollArea>
232
+ </div>
233
+ </TooltipProvider>
234
+ );
235
+ }
236
+
237
+ export default FullFileTree;