Yuki Takei vor 1 Monat
Ursprung
Commit
198fe6e3c6

+ 3 - 1
.kiro/specs/hotkeys/design.md

@@ -24,7 +24,7 @@ BasicLayout / AdminLayout
 | File | Role |
 |------|------|
 | `src/client/components/Hotkeys/HotkeysManager.tsx` | Core orchestrator — binds all keys via tinykeys, renders subscribers |
-| `src/client/components/Hotkeys/Subscribers/*.{tsx,jsx}` | Individual action handlers rendered when their hotkey fires |
+| `src/client/components/Hotkeys/Subscribers/*.tsx` | Individual action handlers rendered when their hotkey fires |
 | `src/components/Layout/BasicLayout.tsx` | Mounts HotkeysManager via `next/dynamic({ ssr: false })` |
 | `src/components/Layout/AdminLayout.tsx` | Mounts HotkeysManager via `next/dynamic({ ssr: false })` |
 
@@ -86,6 +86,8 @@ BasicLayout / AdminLayout
 | Req 7 AC 2: "define hotkey without modifying core detection logic" | Adding a new hotkey requires editing HotkeysManager.tsx's binding map | Binding map is a trivial object literal; the simplification from removing HotkeysDetector + getHotkeyStrokes outweighs the minor editing cost |
 | Req 8 AC 2: "export typed interfaces for hotkey definitions" | `SubscriberComponent` type is internal only, not exported | No external consumers need the type; exporting it would be unnecessary API surface |
 
+> **Note (task 5)**: Req 8 AC 1 is now fully satisfied — all 6 subscriber components converted from `.jsx` to `.tsx` with TypeScript `Props` types and named exports.
+
 ## Key Binding Format (tinykeys)
 
 | Category | Format | Example |

+ 1 - 1
.kiro/specs/hotkeys/spec.json

@@ -1,7 +1,7 @@
 {
   "feature_name": "hotkeys",
   "created_at": "2026-02-20T00:00:00.000Z",
-  "updated_at": "2026-02-24T00:00:00.000Z",
+  "updated_at": "2026-02-24T05:35:00.000Z",
   "language": "en",
   "phase": "implementation-complete",
   "approvals": {

+ 10 - 0
.kiro/specs/hotkeys/tasks.md

@@ -31,3 +31,13 @@ All tasks completed as part of `reduce-modules-loaded` spec (iteration 8.7).
   - Tests: 6/6 pass
   - ChunkModuleStats: async-only 4,608 → 4,516 (-92 modules)
   - _Requirements: 1_
+
+- [x] 5. Refactor subscriber components to match ideal patterns
+  - Converted 4 JSX files to TypeScript: CreatePage, FocusToGlobalSearch, ShowStaffCredit, SwitchToMirrorMode
+  - Fixed `onDeleteRender(this)` bug in 3 files — `this` is undefined in functional components; changed to `onDeleteRender()`
+  - Replaced PropTypes with TypeScript `Props` type in all subscribers
+  - Removed unnecessary `React.memo` wrapper from CreatePage
+  - Unified return values: `return null` for logic-only components (ShowShortcutsModal also updated)
+  - Converted all 6 subscribers from default exports to named exports; updated HotkeysManager imports
+  - Tests: 6/6 pass, lint:typecheck: pass, lint:biome: pass
+  - _Requirements: 7, 8_

+ 10 - 12
apps/app/src/client/components/Hotkeys/HotkeysManager.spec.tsx

@@ -2,30 +2,28 @@ import { act, cleanup, render } from '@testing-library/react';
 import { afterEach, describe, expect, it, vi } from 'vitest';
 
 // Mock all subscriber components as simple render trackers
-vi.mock('./Subscribers/EditPage', () => ({ default: vi.fn(() => null) }));
-vi.mock('./Subscribers/CreatePage', () => ({ default: vi.fn(() => null) }));
+vi.mock('./Subscribers/EditPage', () => ({ EditPage: vi.fn(() => null) }));
+vi.mock('./Subscribers/CreatePage', () => ({ CreatePage: vi.fn(() => null) }));
 vi.mock('./Subscribers/FocusToGlobalSearch', () => ({
-  default: vi.fn(() => null),
+  FocusToGlobalSearch: vi.fn(() => null),
 }));
 vi.mock('./Subscribers/ShowShortcutsModal', () => ({
-  default: vi.fn(() => null),
+  ShowShortcutsModal: vi.fn(() => null),
 }));
 vi.mock('./Subscribers/ShowStaffCredit', () => ({
-  default: vi.fn(() => null),
+  ShowStaffCredit: vi.fn(() => null),
 }));
 vi.mock('./Subscribers/SwitchToMirrorMode', () => ({
-  default: vi.fn(() => null),
+  SwitchToMirrorMode: vi.fn(() => null),
 }));
 
 const { default: HotkeysManager } = await import('./HotkeysManager');
-const { default: EditPage } = await import('./Subscribers/EditPage');
-const { default: CreatePage } = await import('./Subscribers/CreatePage');
-const { default: FocusToGlobalSearch } = await import(
+const { EditPage } = await import('./Subscribers/EditPage');
+const { CreatePage } = await import('./Subscribers/CreatePage');
+const { FocusToGlobalSearch } = await import(
   './Subscribers/FocusToGlobalSearch'
 );
-const { default: ShowShortcutsModal } = await import(
-  './Subscribers/ShowShortcutsModal'
-);
+const { ShowShortcutsModal } = await import('./Subscribers/ShowShortcutsModal');
 
 afterEach(() => {
   cleanup();

+ 6 - 6
apps/app/src/client/components/Hotkeys/HotkeysManager.tsx

@@ -2,12 +2,12 @@ import type { JSX } from 'react';
 import { useCallback, useEffect, useRef, useState } from 'react';
 import { tinykeys } from 'tinykeys';
 
-import CreatePage from './Subscribers/CreatePage';
-import EditPage from './Subscribers/EditPage';
-import FocusToGlobalSearch from './Subscribers/FocusToGlobalSearch';
-import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
-import ShowStaffCredit from './Subscribers/ShowStaffCredit';
-import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
+import { CreatePage } from './Subscribers/CreatePage';
+import { EditPage } from './Subscribers/EditPage';
+import { FocusToGlobalSearch } from './Subscribers/FocusToGlobalSearch';
+import { ShowShortcutsModal } from './Subscribers/ShowShortcutsModal';
+import { ShowStaffCredit } from './Subscribers/ShowStaffCredit';
+import { SwitchToMirrorMode } from './Subscribers/SwitchToMirrorMode';
 
 type SubscriberComponent = React.ComponentType<{ onDeleteRender: () => void }>;
 

+ 10 - 16
apps/app/src/client/components/Hotkeys/Subscribers/CreatePage.tsx

@@ -1,28 +1,22 @@
-import React, { useEffect } from 'react';
-import PropTypes from 'prop-types';
+import { useEffect } from 'react';
 
 import { useCurrentPagePath } from '~/states/page';
 import { usePageCreateModalActions } from '~/states/ui/modal/page-create';
 
-const CreatePage = React.memo((props) => {
+type Props = {
+  onDeleteRender: () => void;
+};
+
+const CreatePage = ({ onDeleteRender }: Props): null => {
   const { open: openCreateModal } = usePageCreateModalActions();
   const currentPath = useCurrentPagePath();
 
-  // setup effect
   useEffect(() => {
     openCreateModal(currentPath ?? '');
+    onDeleteRender();
+  }, [currentPath, openCreateModal, onDeleteRender]);
 
-    // remove this
-    props.onDeleteRender(this);
-  }, [currentPath, openCreateModal, props]);
-
-  return <></>;
-});
-
-CreatePage.propTypes = {
-  onDeleteRender: PropTypes.func.isRequired,
+  return null;
 };
 
-CreatePage.displayName = 'CreatePage';
-
-export default CreatePage;
+export { CreatePage };

+ 1 - 1
apps/app/src/client/components/Hotkeys/Subscribers/EditPage.tsx

@@ -68,4 +68,4 @@ const EditPage = (props: Props): null => {
   return null;
 };
 
-export default EditPage;
+export { EditPage };

+ 8 - 6
apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.jsx → apps/app/src/client/components/Hotkeys/Subscribers/FocusToGlobalSearch.tsx

@@ -6,12 +6,15 @@ import {
 } from '~/features/search/client/states/modal/search';
 import { useIsEditable } from '~/states/page';
 
-const FocusToGlobalSearch = (props) => {
+type Props = {
+  onDeleteRender: () => void;
+};
+
+const FocusToGlobalSearch = ({ onDeleteRender }: Props): null => {
   const isEditable = useIsEditable();
   const searchModalData = useSearchModalStatus();
   const { open: openSearchModal } = useSearchModalActions();
 
-  // setup effect
   useEffect(() => {
     if (!isEditable) {
       return;
@@ -19,12 +22,11 @@ const FocusToGlobalSearch = (props) => {
 
     if (!searchModalData.isOpened) {
       openSearchModal();
-      // remove this
-      props.onDeleteRender();
+      onDeleteRender();
     }
-  }, [isEditable, openSearchModal, props, searchModalData.isOpened]);
+  }, [isEditable, openSearchModal, onDeleteRender, searchModalData.isOpened]);
 
   return null;
 };
 
-export default FocusToGlobalSearch;
+export { FocusToGlobalSearch };

+ 5 - 8
apps/app/src/client/components/Hotkeys/Subscribers/ShowShortcutsModal.tsx

@@ -1,4 +1,4 @@
-import React, { type JSX, useEffect } from 'react';
+import { useEffect } from 'react';
 
 import {
   useShortcutsModalActions,
@@ -8,13 +8,11 @@ import {
 type Props = {
   onDeleteRender: () => void;
 };
-const ShowShortcutsModal = (props: Props): JSX.Element => {
+
+const ShowShortcutsModal = ({ onDeleteRender }: Props): null => {
   const status = useShortcutsModalStatus();
   const { open } = useShortcutsModalActions();
 
-  const { onDeleteRender } = props;
-
-  // setup effect
   useEffect(() => {
     if (status == null) {
       return;
@@ -22,12 +20,11 @@ const ShowShortcutsModal = (props: Props): JSX.Element => {
 
     if (!status.isOpened) {
       open();
-      // remove this
       onDeleteRender();
     }
   }, [onDeleteRender, open, status]);
 
-  return <></>;
+  return null;
 };
 
-export default ShowShortcutsModal;
+export { ShowShortcutsModal };

+ 6 - 6
apps/app/src/client/components/Hotkeys/Subscribers/ShowStaffCredit.tsx

@@ -1,13 +1,13 @@
-import PropTypes from 'prop-types';
+import type { JSX } from 'react';
 
 import StaffCredit from '../../StaffCredit/StaffCredit';
 
-const ShowStaffCredit = (props) => {
-  return <StaffCredit onClosed={() => props.onDeleteRender(this)} />;
+type Props = {
+  onDeleteRender: () => void;
 };
 
-ShowStaffCredit.propTypes = {
-  onDeleteRender: PropTypes.func.isRequired,
+const ShowStaffCredit = ({ onDeleteRender }: Props): JSX.Element => {
+  return <StaffCredit onClosed={onDeleteRender} />;
 };
 
-export default ShowStaffCredit;
+export { ShowStaffCredit };

+ 10 - 14
apps/app/src/client/components/Hotkeys/Subscribers/SwitchToMirrorMode.tsx

@@ -1,20 +1,16 @@
-import React, { useEffect } from 'react';
-import PropTypes from 'prop-types';
+import { useEffect } from 'react';
 
-const SwitchToMirrorMode = (props) => {
-  // setup effect
+type Props = {
+  onDeleteRender: () => void;
+};
+
+const SwitchToMirrorMode = ({ onDeleteRender }: Props): null => {
   useEffect(() => {
     document.body.classList.add('mirror');
+    onDeleteRender();
+  }, [onDeleteRender]);
 
-    // remove this
-    props.onDeleteRender(this);
-  }, [props]);
-
-  return <></>;
-};
-
-SwitchToMirrorMode.propTypes = {
-  onDeleteRender: PropTypes.func.isRequired,
+  return null;
 };
 
-export default SwitchToMirrorMode;
+export { SwitchToMirrorMode };