diff --git a/.claude/agents/agent-translation-reviewer.md b/.claude/agents/agent-translation-reviewer.md new file mode 100644 index 00000000..4a317db1 --- /dev/null +++ b/.claude/agents/agent-translation-reviewer.md @@ -0,0 +1,122 @@ +--- +name: translation-reviewer +description: Native-level review of translated documentation against its English source. Reports findings only, never edits. +tools: Read, Grep, Glob +model: opus +--- + +# Translation Reviewer Agent + +Reviews translated documentation as a native speaker of the target language who also +writes React and TypeScript for a living. + +## Role + +**Read-only.** Report findings; never edit a file. The author applies the fixes. +This separation is deliberate — a translation reviewed by whoever wrote it is not reviewed. + +## Input contract + +The dispatching prompt must supply: + +- Target language and its locale directory (e.g. Japanese, `docs/ja/`) +- Each translated file path, paired with its English source path + +If any of these are missing, say so and stop. Do not guess which source a translation came from. + +## Finding categories + +| Category | Meaning | +| ----------- | ---------------------------------------------------------------------------------- | +| `meaning` | The technical claim differs from the English source | +| `glossary` | A term does not match the glossary for this language | +| `fluency` | Grammatical, but not how a native writes it | +| `register` | Politeness or formality inconsistent with the rest of the document | +| `code` | Code, identifier, import path, prop name, or VitePress code marker was altered | +| `link` | Internal link missing the locale prefix, or pointing at a page that does not exist | +| `structure` | Heading level or order diverges from the English source | + +## Output format + +One block per finding, nothing else: + +``` +: [] +source: +current: +suggest: +why: +``` + +Last line, always: + +``` +VERDICT: approve +``` + +or + +``` +VERDICT: revise ( findings) +``` + +No preamble, no document summary, no praise, no closing remarks. + +## Rules + +1. Never report a finding you cannot anchor to a file and a line number. +2. In code blocks, only comments and string literals are translatable. Anything else + that differs from the source is a `code` finding — including `// [!code --]` markers. +3. Do not propose stylistic rewrites of sentences that are already accurate and natural. + Report only what a maintainer would actually change. +4. If the English source is itself ambiguous, report it under `meaning` rather than + inventing a reading. +5. Frontmatter keys are not translatable; their values are. A translated key is a `code` finding. + +## Japanese (`ja`) + +**Register:** です・ます体 for prose. 体言止め is acceptable in parameter descriptions, +return-value descriptions, and table cells. + +| English | 日本語 | +| --------------------- | -------------------------- | +| Hook | フック | +| Component | コンポーネント | +| Utility | ユーティリティ | +| Guide | ガイド | +| Reference | リファレンス | +| Introduction | 紹介 | +| Installation | インストール | +| Design Principles | 設計原則 | +| Contributing | 貢献ガイド | +| Roadmap | ロードマップ | +| Parameters | パラメータ | +| Return Value | 戻り値 | +| Example | 使用例 | +| Bundle size | バンドルサイズ | +| Dependency | 依存関係 | +| Zero dependencies | 依存関係ゼロ | +| Server-side rendering | サーバーサイドレンダリング | +| Callback | コールバック | +| State | 状態 | +| Rendering | レンダリング | +| Cleanup | クリーンアップ | +| Deprecated | 非推奨 | +| Test coverage | テストカバレッジ | +| Type-safe | 型安全 | + +**Keep in the original script:** `react-simplikit`, `React`, `TypeScript`, `npm`, `yarn`, +`pnpm`, `ref`, `props`, `JSDoc`, `SSR`, `MIT`, and every hook/component/util name. + +**Recurring failure modes in JA technical translation — check these explicitly:** + +- 「〜することができます」 where 「〜できます」 reads better +- Overuse of の-chains (「Reactのフックのテストのカバレッジ」) +- English word order preserved through a relative clause that Japanese would front +- 半角/全角 inconsistency around parentheses and colons +- Translating a UI label that the source deliberately left in English + +## Adding a language + +Add a section like the Japanese one above: register, glossary table, do-not-translate list, +and recurring failure modes. The categories, output format, and rules stay language-agnostic. diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index 288a9403..2e6a1af2 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -28,11 +28,14 @@ for (const requiredText of ['release:', 'changesets/action@', 'changeset:publish assert.equal(releaseWorkflow.includes(requiredText), true, `release workflow must contain ${requiredText}`); } -assert.deepEqual(Object.keys(localeDefinitions), ['root', 'ko']); +assert.deepEqual(Object.keys(localeDefinitions), ['root', 'ko', 'ja']); assert.equal(rewrites['docs/index.md'], 'index.md'); assert.equal(rewrites['docs/ko/index.md'], 'ko/index.md'); assert.equal(rewrites['packages/core/src/hooks/:hook/ko/:hook.md'], 'ko/core/hooks/:hook.md'); +assert.equal(rewrites['docs/ja/index.md'], 'ja/index.md'); +assert.equal(rewrites['packages/core/src/hooks/:hook/ja/:hook.md'], 'ja/core/hooks/:hook.md'); assert.equal(generatedRewrites['generated-locales/docs/ko/index.md'], 'ko/index.md'); +assert.equal(generatedRewrites['generated-locales/docs/ja/index.md'], 'ja/index.md'); assert.equal(packageJson.scripts['docs:prepare'], 'tsx .scripts/index.ts prepare-localized-fallbacks'); assert.equal(packageJson.scripts['docs:dev'], 'yarn docs:prepare && vitepress dev'); assert.equal(packageJson.scripts['docs:build'], 'yarn docs:prepare && vitepress build'); @@ -65,8 +68,9 @@ const hookFixtureName = 'useUntranslatedFallbackFixture'; const hookFixtureDirectory = path.join(root, 'packages/core/src/hooks', hookFixtureName); const buildOutputDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'react-simplikit-docs-')); -// The repository currently has a Korean translation for every routed English document, so the -// fallback path only has a route to render on while these English-only fixtures exist. +// Korean translates every routed English document, so its fallback path only has a route to +// render on while these English-only fixtures exist. Japanese ships without translated API +// reference pages, so those routes render from generated fallbacks on every build. await fs.writeFile(guideFixturePath, `# ${guideFixtureTitle}\n`); await fs.mkdir(hookFixtureDirectory, { recursive: true }); await fs.writeFile(path.join(hookFixtureDirectory, `${hookFixtureName}.md`), `# ${hookFixtureName}\n`); @@ -110,49 +114,49 @@ try { await fs.rm(buildOutputDirectory, { force: true, recursive: true }); } -const jaFixtureDefinition = { - label: '日本語', - lang: 'ja', - path: 'ja', - untranslatedNotice: 'このページは翻訳準備中のため、英語の原文を表示しています。', +const unregisteredLocaleFixture = { + label: '简体中文', + lang: 'zh-Hans', + path: 'zh-Hans', + untranslatedNotice: '此页面的翻译正在准备中,暂时显示英文原文。', themeStrings: { - homeNavLabel: 'ホーム', - guideLabel: 'ガイド', - referenceLabel: 'リファレンス', - componentsLabel: 'コンポーネント', - hooksLabel: 'フック', - utilsLabel: 'ユーティリティ', + homeNavLabel: '首页', + guideLabel: '指南', + referenceLabel: '参考', + componentsLabel: '组件', + hooksLabel: 'Hooks', + utilsLabel: '工具函数', guidePages: { core: { - intro: '紹介', - whyReactSimplikitMatters: 'なぜ react-simplikit なのか', - installation: 'インストール', - designPrinciples: '設計原則', - contributing: '貢献ガイド', + intro: '介绍', + whyReactSimplikitMatters: '为什么选择 react-simplikit', + installation: '安装', + designPrinciples: '设计原则', + contributing: '贡献指南', }, mobile: { - intro: '紹介', - roadmap: 'ロードマップ', - installation: 'インストール', - designPrinciples: '設計原則', - contributing: '貢献ガイド', + intro: '介绍', + roadmap: '路线图', + installation: '安装', + designPrinciples: '设计原则', + contributing: '贡献指南', }, }, - editLinkText: 'GitHub で編集する', - footerMessage: 'MIT ライセンスの下で配布されています。', + editLinkText: '在 GitHub 上编辑此页', + footerMessage: '基于 MIT 许可证发布。', }, } satisfies Parameters[0]; -const jaConfig = buildLocaleConfig(jaFixtureDefinition); +const unregisteredConfig = buildLocaleConfig(unregisteredLocaleFixture); -assert.equal(jaConfig.lang, 'ja'); -assert.deepEqual(jaConfig.themeConfig?.nav, [ - { text: 'ホーム', link: '/ja/' }, - { text: 'Mobile', link: '/ja/mobile/intro' }, - { text: 'Core', link: '/ja/core/intro' }, +assert.equal(unregisteredConfig.lang, 'zh-Hans'); +assert.deepEqual(unregisteredConfig.themeConfig?.nav, [ + { text: '首页', link: '/zh-Hans/' }, + { text: 'Mobile', link: '/zh-Hans/mobile/intro' }, + { text: 'Core', link: '/zh-Hans/core/intro' }, ]); -assert.deepEqual(Object.keys(jaConfig.themeConfig?.sidebar ?? {}), ['/ja/core/', '/ja/mobile/']); -assert.equal(jaConfig.themeConfig?.editLink?.text, 'GitHub で編集する'); +assert.deepEqual(Object.keys(unregisteredConfig.themeConfig?.sidebar ?? {}), ['/zh-Hans/core/', '/zh-Hans/mobile/']); +assert.equal(unregisteredConfig.themeConfig?.editLink?.text, '在 GitHub 上编辑此页'); const koConfig = buildLocaleConfig(localeDefinitions.ko); const rootConfig = buildLocaleConfig(localeDefinitions.root); @@ -183,6 +187,21 @@ assert.deepEqual(rootConfig.themeConfig?.nav, [ assert.equal(rootConfig.lang, 'en'); assert.equal(rootConfig.themeConfig?.editLink?.text, 'Edit this page on GitHub'); +const jaConfig = buildLocaleConfig(localeDefinitions.ja); + +assert.equal(jaConfig.lang, 'ja'); +assert.deepEqual(jaConfig.themeConfig?.nav, [ + { text: 'ホーム', link: '/ja/' }, + { text: 'Mobile', link: '/ja/mobile/intro' }, + { text: 'Core', link: '/ja/core/intro' }, +]); +assert.equal(jaConfig.themeConfig?.editLink?.text, 'GitHub で編集する'); +assert.notEqual( + localeDefinitions.ja.themeStrings.search, + undefined, + 'Japanese must ship search translations, or the search UI silently renders in English' +); + for (const retiredLocaleFile of ['.vitepress/en.mts', '.vitepress/ko.mts']) { await assert.rejects( fs.access(path.join(root, retiredLocaleFile)), diff --git a/.vitepress/locales.mts b/.vitepress/locales.mts index 75487660..30b65d6d 100644 --- a/.vitepress/locales.mts +++ b/.vitepress/locales.mts @@ -1,9 +1,10 @@ import { DefaultTheme } from 'vitepress'; import { en } from './locales/en.mts'; +import { ja } from './locales/ja.mts'; import { ko } from './locales/ko.mts'; -export type LocaleCode = 'root' | 'ko'; +export type LocaleCode = 'root' | 'ko' | 'ja'; type GuidePageTitles = { core: { @@ -65,6 +66,13 @@ export const localeDefinitions: Record = { untranslatedNotice: '이 페이지는 번역을 준비하는 동안 영어 원문으로 보여드려요.', themeStrings: ko, }, + ja: { + label: '日本語', + lang: 'ja', + path: 'ja', + untranslatedNotice: 'このページは翻訳の準備中のため、英語の原文を表示しています。', + themeStrings: ja, + }, }; export const localeDirectories = Object.values(localeDefinitions) diff --git a/.vitepress/locales/ja.mts b/.vitepress/locales/ja.mts new file mode 100644 index 00000000..5c9d4d40 --- /dev/null +++ b/.vitepress/locales/ja.mts @@ -0,0 +1,51 @@ +import type { LocaleThemeStrings } from '../locales.mts'; + +export const ja: LocaleThemeStrings = { + homeNavLabel: 'ホーム', + guideLabel: 'ガイド', + referenceLabel: 'リファレンス', + componentsLabel: 'コンポーネント', + hooksLabel: 'フック', + utilsLabel: 'ユーティリティ', + guidePages: { + core: { + intro: '紹介', + whyReactSimplikitMatters: 'なぜ react-simplikit なのか', + installation: 'インストール', + designPrinciples: '設計原則', + contributing: '貢献ガイド', + }, + mobile: { + intro: '紹介', + roadmap: 'ロードマップ', + installation: 'インストール', + designPrinciples: '設計原則', + contributing: '貢献ガイド', + }, + }, + editLinkText: 'GitHub で編集する', + footerMessage: 'MIT ライセンスの下で配布されています。', + search: { + translations: { + button: { + buttonText: '検索', + buttonAriaLabel: '検索', + }, + modal: { + backButtonTitle: '戻る', + displayDetails: '詳細を表示', + footer: { + closeKeyAriaLabel: '閉じる', + closeText: '閉じる', + navigateDownKeyAriaLabel: '下へ', + navigateText: '移動', + navigateUpKeyAriaLabel: '上へ', + selectKeyAriaLabel: '選択', + selectText: '選択', + }, + noResultsText: '検索結果が見つかりませんでした。', + resetButtonTitle: 'すべて消去', + }, + }, + }, +}; diff --git a/README-ja_jp.md b/README-ja_jp.md new file mode 100644 index 00000000..d9286b31 --- /dev/null +++ b/README-ja_jp.md @@ -0,0 +1,117 @@ +![react-simplikit](./public/images/og.png) + +# react-simplikit · [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/react-simplikit/graph/badge.svg?token=RHVOZ3J3TU)](https://codecov.io/gh/toss/react-simplikit) [![Discord Badge](https://discord.com/api/guilds/1281071127052943361/widget.png?style=shield)](https://discord.gg/vGXbVjP2nY) + +[English](./README.md) | [한국어](./README-ko_kr.md) | 日本語 + +堅牢なアプリケーションを構築するための、軽量で依存関係のない React ユーティリティ集です。 + +## パッケージ + +| パッケージ | 説明 | バージョン | +| -------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| [react-simplikit](./packages/core) | Universal hooks - 純粋な状態/ロジック用フック(プラットフォームに依存しない) | [![npm](https://img.shields.io/npm/v/react-simplikit.svg)](https://www.npmjs.com/package/react-simplikit) | +| [@react-simplikit/mobile](./packages/mobile) | モバイル Web ユーティリティ(viewport、keyboard、scroll) | [![npm](https://img.shields.io/npm/v/@react-simplikit/mobile.svg)](https://www.npmjs.com/package/@react-simplikit/mobile) | + +> **注記**: `react-simplikit` は現在、Web とモバイル(React Native)の両方で動作する純粋な状態/ロジック用フックのみを提供する Universal Hook Library として維持されています。ブラウザ/プラットフォームに依存するフックは非推奨です。詳しくは [packages/core/README-ja_jp.md](./packages/core/README-ja_jp.md) を参照してください。 + +## 特長 + +- **依存関係ゼロ** - 非常に軽量 +- **100% TypeScript** - 完全な型安全性 +- **100% テストカバレッジ** - 信頼性と安定性 +- **SSR 安全** - Next.js などの SSR フレームワークで動作 +- **ツリーシェイキング対応** - 使用するものだけがバンドルされる + +## インストール + +```bash +# Core utilities +npm install react-simplikit + +# Mobile web utilities +npm install @react-simplikit/mobile +``` + +## クイックスタート + +### react-simplikit + +```tsx +import { useState } from 'react'; +import { useDebounce } from 'react-simplikit'; + +function SearchInput() { + const [query, setQuery] = useState(''); + + const debouncedSearch = useDebounce((value: string) => { + // 実際の API 呼び出し + searchAPI(value); + }, 300); + + return ( + { + setQuery(e.target.value); + debouncedSearch(e.target.value); + }} + placeholder="検索キーワードを入力" + /> + ); +} +``` + +デバウンスされた関数は `.cancel()` を提供し、コンポーネントがアンマウントされると保留中の呼び出しは自動的にキャンセルされます。 + +### @react-simplikit/mobile + +```tsx +import { useAvoidKeyboard, useBodyScrollLock } from '@react-simplikit/mobile'; + +function ChatInput() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} + +// `useBodyScrollLock` はコンポーネントがマウントされている間 body のスクロールをロックし、 +// アンマウント時に自動的に解除します。モーダルが開いている間だけレンダリングしてください。 +function BodyScrollLock() { + useBodyScrollLock(); + return null; +} +``` + +## ドキュメント + +詳しいドキュメントは [react-simplikit.slash.page](https://react-simplikit.slash.page/ja) をご覧ください。 + +## リポジトリ構成 + +``` +packages/ +├── core/ # react-simplikit (hooks, components, utils) +└── mobile/ # @react-simplikit/mobile (mobile web utilities) +``` + +## 貢献 + +どなたからの貢献も歓迎します!貢献ガイドをご確認ください。 + +[CONTRIBUTING](./.github/CONTRIBUTING.md) + +## ライセンス + +MIT © Viva Republica, Inc. 詳しくは [LICENSE](./LICENSE) を参照してください。 + + + + + Toss + + diff --git a/README-ko_kr.md b/README-ko_kr.md index b35254da..30674968 100644 --- a/README-ko_kr.md +++ b/README-ko_kr.md @@ -2,7 +2,7 @@ # react-simplikit · [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/react-simplikit/graph/badge.svg?token=RHVOZ3J3TU)](https://codecov.io/gh/toss/react-simplikit) [![Discord Badge](https://discord.com/api/guilds/1281071127052943361/widget.png?style=shield)](https://discord.gg/vGXbVjP2nY) -[English](./README.md) | 한국어 +[English](./README.md) | 한국어 | [日本語](./README-ja_jp.md) 견고한 애플리케이션을 만들기 위한 가볍고 의존성 없는 React 유틸리티 모음이에요. diff --git a/README.md b/README.md index 07aaac80..e9b38f87 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # react-simplikit · [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/react-simplikit/graph/badge.svg?token=RHVOZ3J3TU)](https://codecov.io/gh/toss/react-simplikit) [![Discord Badge](https://discord.com/api/guilds/1281071127052943361/widget.png?style=shield)](https://discord.gg/vGXbVjP2nY) -English | [한국어](./README-ko_kr.md) +English | [한국어](./README-ko_kr.md) | [日本語](./README-ja_jp.md) A collection of lightweight, zero-dependency React utilities for building robust applications. diff --git a/docs/ja/core/contributing.md b/docs/ja/core/contributing.md new file mode 100644 index 00000000..a5666603 --- /dev/null +++ b/docs/ja/core/contributing.md @@ -0,0 +1,413 @@ +# react-simplikit への貢献 + +`react-simplikit` は誰でも気軽に貢献できるように設計されています。貢献したい場合は、以下のガイドを参考にしてください。 + +## パッケージのスコープ + +`react-simplikit` は、すべての JavaScript 環境(ブラウザ、サーバー、React Native など)で動作する**プラットフォームに依存しないフック、コンポーネント、ユーティリティ**に焦点を当てています。 + +貢献する前に、実装がどのパッケージに属するかを確認してください。 + +| パッケージ | スコープ | 例 | +| ------------------------- | ------------------------------------------------ | ------------------------------------------------------------ | +| `react-simplikit` | プラットフォームに依存しない純粋な状態・ロジック | `useToggle`, `useAsyncEffect`, `useLoading` | +| `@react-simplikit/mobile` | モバイル Web 特有の課題を解決する | `useAvoidKeyboard`, `useBodyScrollLock`, `useVisualViewport` | + +::: tip +mobile パッケージは、ブラウザ API に依存するすべてのフックのためのものでは**ありません**。**モバイル Web 環境で直面する問題**(ビューポート管理、キーボード処理、iOS Safari や Android Chrome でのレイアウトの問題)に限定して対象とします。たとえば、キーボードショートカットのフックはブラウザ API を使用しますが、mobile パッケージには属しません。 +::: + +## 実装への貢献 + +実装に貢献する際は、その種類(`components`、`hooks`、`utils`)に応じた適切なディレクトリに追加してください。すべての実装には、以下の要素を含める必要があります。 + +- **実装** +- **テストコード** +- **JSDoc** + +::: tip +**ドキュメントは書かなくてもいいですか?** + +はい、ドキュメントを別途書く必要はありません。代わりに、JSDoc コメントを詳しく書いてください。PR がマージされると、JSDoc をもとに英語と韓国語のドキュメントが自動生成され、ドキュメントを追加する PR が自動的に作成されます。 +::: + +### 実装を書く + +`react-simplikit` の [設計原則](./design-principles.md) に従う必要があります。特定のライブラリに依存したり、React のライフサイクルと密接に結びついたりする実装は提供しません。これらの設計原則に沿って実装を書いてください。 + +### JSDoc を書く + +すべての実装には [JSDoc](https://jsdoc.app/) コメントを含める必要があります。JSDoc は実装を使用する際のヒントを提供するだけでなく、ドキュメント生成においても重要な役割を果たします。 +JSDoc コメントには `@description` と `@example` を必ず含める必要があり、パラメータや戻り値がある場合は `@param` と `@returns` も含める必要があります。 + +::: details 正確なドキュメントを生成するために、JSDoc の作成ルールを守る必要があります。JSDoc の検証に失敗すると、CI が失敗することがあります。 + +- JSDoc は英語で書く必要があります。 +- `@description`: 実装の機能や役割を明確に説明する必須タグです。 +- `@example`: 実装の使い方を示すサンプルコードを記述する必須タグです。 +- `@param`: パラメータの名前と説明を書きます。実装にパラメータがある場合は必ず記述してください。 + + - 必須パラメータの場合: `@param {<型>} <パラメータ名> - <パラメータの説明>` + - 任意パラメータの場合: `@param {<型>} [<パラメータ名>] - <パラメータの説明>` + - オブジェクト型のパラメータの場合、オブジェクト自体とそのプロパティの両方に `@param` タグが必要です。 + - 説明の下にリストを書きたい場合は、`-` の代わりに `--` を使用してください。 + + ```ts + type Props = { + name: string; + age: number; + nickname?: string; + company: { + name: string; + address?: string; + }; + paymentMethod?: { + type: 'card' | 'account'; + number?: string; + }; + }; + + /** + * @param {string} name - Name of the user. + * @param {number} age - Age of the user. + * @param {string} [nickname] - Nickname of the user. + * @param {Object} company - Company information of the user. + * @param {string} company.name - Name of the company. + * @param {string} [company.address] - Address of the company. + * @param {Object} [paymentMethod] - Payment information of the user. + * @param {string} [paymentMethod.type] - Payment method. + * @param {string} [paymentMethod.number] - Card or account number. + * -- Card or account number without `-`. + * -- If the number is a card number, it should be 15 or 16 digits. + */ + ``` + + この JSDoc は次のようなドキュメントに変換されます。 + +
+ + + + + +
+ +- `@returns`: 戻り値の名前と説明を書きます。実装に戻り値がある場合は必ず記述してください。 + + - 形式: `@returns {<型>} <戻り値の説明>` + - オブジェクトやタプルの戻り値の場合、各メンバーの説明を含めてください。 + - 各メンバーに追加の説明が必要な場合は、`:` を使用してください。 + + ```ts + type ReturnValue = [Object, () => void]; + + /** + * @returns {[Object, () => void]} A tuple containing: + * - obj `Object` - An object containing: + * : label `string` - The label of the input. + * : value `string` - The value of the input. + * - onChange `() => void` - A function to update the value. + */ + ``` + + この JSDoc は次のようなドキュメントに変換されます。 + +
+ +
+ +
+ + オブジェクト型の戻り値も同様に書けます。 + + ```ts + type ReturnValue = { value: string; onChange: () => void }; + + /** + * @returns {Object} An object containing: + * - value `string` - The value of the input. + * - onChange `() => void` - A function to update the value. + */ + ``` + + この JSDoc は次のようなドキュメントに変換されます。 + +
+ +
+ +::: + +### テストコードを書く + +すべての実装には、実装と同じ名前のテストコードを必ず含める必要があります。テストカバレッジは常に 100% を満たす必要があります。以下のコマンドでカバレッジを確認できます。 + +```bash +yarn test:coverage +``` + +::: details SSR 環境で安全に動作するか確認してください +`react-simplikit` のすべての実装は、SSR 環境で安全に動作することを確認するために特別なレンダリング関数を使ってテストされています。 + +- コンポーネントのテスト + + ```tsx + it('is safe on server side rendering', () => { + // renderSSR.serverOnly はコンポーネントをサーバー環境でレンダリングするメソッドです。 + // この環境では useEffect のようなフックは実行されず、window や document のようなオブジェクトも利用できないため、これらを使うとエラーになります。 + renderSSR.serverOnly(() => ( + +
Test Content
+
+ )); + + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); + + it('should render children correctly', async () => { + // renderSSR はコンポーネントをクライアント環境でレンダリングするメソッドです。 + // ただし、サーバーでレンダリングされた HTML とクライアントでレンダリングされた HTML が異なる場合、ハイドレーションのミスマッチエラーが発生します。 + await renderSSR(() => ( + +
Test Content
+
+ )); + + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); + + it('should hydration mismatch error occurred', async () => { + // このテストコードは、ハイドレーションのミスマッチエラーによって失敗します。 + await renderSSR(() => ( + +
Test Content
+
{Math.random()}
+
+ )); + + expect(screen.getByText('Test Content')).toBeInTheDocument(); + }); + ``` + +- フックのテスト + + ```ts + it('is safe on server side rendering', () => { + // renderHookSSR.serverOnly はフックをサーバー環境でレンダリングするメソッドです。 + // この環境では useEffect のようなフックは実行されず、window や document のようなオブジェクトも利用できないため、これらを使うとエラーになります。 + const result = renderHookSSR.serverOnly(() => useToggle(true)); + const [bool] = result.current; + expect(bool).toBe(true); + }); + + it('should initialize with the default value true', async () => { + const { result } = await renderHookSSR(() => useToggle(true)); + const [bool] = result.current; + expect(bool).toBe(true); + }); + ``` + +::: + +### Changeset を作成する + +コードの変更がパッケージに影響する場合は、changeset を作成する必要があります。Changeset は、バージョン管理と changelog 生成を自動化するツールです。 + +#### Changeset の作成方法 + +1. 変更を実装したら、以下のコマンドを実行してください。 + +```bash +yarn changeset +``` + +2. 変更の種類を選択してください。 + + - `patch`: バグ修正や小さな変更 + - `minor`: 新機能の追加(後方互換性を維持) + - `major`: 破壊的変更(後方互換性が失われる) + +3. 変更内容の簡単な説明を書いてください。 + +::: tip +両パッケージは現在 `0.0.x` の段階です。この段階では、ほとんどの変更に `patch` を使用してください。 +バージョンの種類に迷う場合は、メンテナーに相談してください。 +::: + +4. 生成された changeset ファイルを PR に含めてコミットしてください。 + +::: tip +Changeset ファイルは `.changeset` フォルダに作成され、PR と一緒にコミットする必要があります。PR がマージされると、バージョンが自動的に更新され、changelog が生成されます。 +::: + +### リリース + +変更が `main` ブランチにマージされると、リリースプロセスが自動的に実行されます。 + +1. PR が `main` ブランチにマージされると、GitHub Actions が実行されます。 +2. changeset がある場合、バージョン更新用の PR が自動的に作成されます。 +3. バージョン更新用の PR がマージされると、新しいバージョンが npm に公開されます。 + +リリース結果は [GitHub Actions](https://github.com/toss/react-simplikit/actions) で確認できます。 + +## ドキュメントへの貢献 + +ドキュメントへの貢献に特別な条件はありません。誤った情報や訳の質が良くない箇所を見つけたり、追加したい内容があれば、自由に編集してください。ドキュメントは読者の視点でわかりやすく、簡潔に書いてください。 + +## スキャフォールディング + +貢献のための最小限の骨組みを作成するコマンドがあります。以下のコマンドを使うと、基本的な構造を持つ実装フォルダを作成できます。 + +```bash +yarn run scaffold --type +``` + +- `type`: 実装の種類。`component`、`hook`、`util` のいずれかを指定してください。 +- `name`: 実装の名前。 + +### 使用例 + +```bash +yarn run scaffold Button --type component +``` + +このコマンドは `src/components/Button` フォルダに 3 つのファイルを作成します。 + +::: code-group + +```tsx [Button.tsx] +/** + * @description + * + * + * @param {} - + * @param {} [] - + * + * @returns {} + * - `` - + * + * @example + * + */ +export function Button() { + // TODO: Implement Button +} +``` + +```tsx [Button.spec.ts] +import { describe, expect, it } from 'vitest'; + +import { renderSSR } from '../../_internal/test-utils/renderSSR.tsx'; + +import { Button } from './Button.tsx'; + +describe('Button', () => { + it('is safe on server side rendering', async () => { + const result = renderSSR.serverOnly(() => + + ); +} +``` + +### 特定の区切り要素で配列をレンダリングする + + + + + + + + +## 簡潔な実装で意図しない挙動とバグを最小化する + +`react-simplikit` のすべての実装には隠れたロジックがありません。機能の組み合わせや拡張が必要な場合は、外部から注入できるインターフェースを提供します。また、モダンな実装を通じてクリーンなコードを維持しています。 + +これが、`react-simplikit` を使うことでコードの安定性と信頼性を高められる理由です。 + +```tsx +function Page() { + // useIntersectionObserver は交差の検知に必要な最小限の機能だけを提供し、 + // コールバックと交差判定のオプションは外部から注入して受け取ります + const ref = useIntersectionObserver( + entry => { + if (entry.isIntersecting) { + console.log('Element is in view:', entry.target); + } else { + console.log('Element is out of view:', entry.target); + } + }, + { threshold: 0.5 } + ); + + return
Observe me!
; +} +``` + +## 高い信頼性 + +`react-simplikit` は、すべての実装で 100% のテストカバレッジを維持することで高い信頼性を保証します。 + +## SSR 環境でも安全な動作を保証する + +SSR 環境の積極的な採用に伴い、適切に書かれていないコンポーネントやフックは SSR 環境でエラーを起こしたり、ハイドレーションのミスマッチを引き起こしたりすることがあります。`react-simplikit` はこうした問題を最小化するように設計されており、SSR 環境での 100% テストカバレッジによってそれを保証しています。 + +## React 以外の依存関係なし + +React と React-DOM を除いて [14 個の依存関係](https://www.npmjs.com/package/react-use?activeTab=dependencies)を持つ react-use と比較して、`react-simplikit` は React への peer dependency 以外の依存関係を持ちません。 + +## リンク + +react-simplikit についてさらに詳しく知りたい方は、以下のリンクをご覧ください。 + +- [GitHub](https://github.com/toss/react-simplikit) diff --git a/docs/ja/core/why-react-simplikit-matters.md b/docs/ja/core/why-react-simplikit-matters.md new file mode 100644 index 00000000..c90498c2 --- /dev/null +++ b/docs/ja/core/why-react-simplikit-matters.md @@ -0,0 +1,198 @@ +# なぜ react-simplikit なのか + +数多くの React ベースのライブラリの中で、なぜ `react-simplikit` を選ぶべきなのでしょうか。私たちが大切にしているコアバリューを見ながら、`react-simplikit` を使うことがなぜ「React を React らしく書くこと」と同じなのかを理解していきましょう。 + +## 宣言的インターフェース + +React のコンポーネントは、クラスコンポーネントから関数コンポーネントへと進化してきました。 + +関数コンポーネントと宣言的な API を持つフックの登場によって、これまでクラスコンポーネントで複雑に書かれていた[状態やライフサイクルに関するロジックを抽象化](https://legacy.reactjs.org/docs/hooks-intro.html#its-hard-to-reuse-stateful-logic-between-components)できるようになりました。 + +しかし、React のコンポーネントは依然として複雑です。React は[最小限のインターフェースを提供](https://legacy.reactjs.org/docs/design-principles.html#common-abstraction)しているため、少し複雑な機能を持つコンポーネントでも、数十個の状態、ハンドラー、状態変化に応じた副作用の定義が必要になることがあります。 + +ある時点から、コンポーネントは関心事が混ざり合って命令的に書かれるようになり、そのコンポーネントが何をしているのか、どんなロジックが動いているのかがだんだん把握しづらくなっていきます。 + +`react-simplikit` は、よく使われるものの実装が複雑になりがちな機能に対して、適切な抽象化を提供します。これにより、複雑なロジックを持つコンポーネントを書くときも、直感的な可読性を保つことができます。 + +`react-simplikit` は、実際のサービス開発でよく直面するさまざまな問題を宣言的に解決するインターフェースを提示します。 + +これをもとに、開発者がより宣言的な React コンポーネントを書けるように導きます。 + +::: code-group + +```tsx [without-react-simplikit.tsx] +function AutoCompleteInput() { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [isLoading, setLoading] = useState(false); + const [isOpen, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + setOpen(false); + } + }; + + document.addEventListener('click', handleClickOutside); + return () => document.removeEventListener('click', handleClickOutside); + }, []); + + useEffect(() => { + if (query.trim().length === 0) { + setResults([]); + return; + } + + setLoading(true); + const timeoutId = setTimeout(async () => { + try { + const response = await fetch(`/api/search?q=${query}`); + const data = await response.json(); + setResults(data); + } catch (error) { + console.error('Failed to fetch results:', error); + } finally { + setLoading(false); + } + }, 300); + + return () => clearTimeout(timeoutId); + }, [query]); + + return ( +
+ { + setQuery(e.target.value); + setOpen(true); + }} + onFocus={() => setOpen(true)} + placeholder="検索キーワードを入力" + /> + {isOpen && (isLoading || results.length > 0) && ( +
+ {isLoading ? ( +
検索中...
+ ) : ( + results.map((result, idx) => ( + +
{ + setQuery(result.title); + setOpen(false); + }} + > + {result.title} +
+ {idx !== results.length - 1 && } +
+ )) + )} +
+ )} +
+ ); +} +``` + +```tsx [with-react-simplikit.tsx] +function AutoCompleteInput() { + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [isLoading, startLoading] = useLoading(); + const [isOpen, openSearchBox, closeSearchBox] = useBooleanState(false); + + const searchBoxState = useMemo(() => { + if (!isOpen) return 'CLOSE'; + + if (isLoading) return 'LOADING'; + + if (results.length > 0) return 'RESULT_EXISTS'; + + return 'EMPTY'; + }, [isOpen, isLoading, results]); + + const searchResults = useDebounce(async (searchQuery: string) => { + if (searchQuery.trim().length === 0) { + setResults([]); + return; + } + + const response = await startLoading( + fetch(`/api/search?q=${searchQuery}`) + .then(res => res.json()) + .catch(error => { + console.error('Failed to fetch results:', error); + return []; + }) + ); + + setResults(response); + }, 300); + + const containerRef = useRef(null); + useOutsideClickEffect(containerRef.current, () => closeSearchBox()); + + return ( +
+ { + setQuery(e.target.value); + openSearchBox(); + searchResults(e.target.value); + }} + onFocus={openSearchBox} + placeholder="検索キーワードを入力" + /> +
検索中...
, + EMPTY: () =>
検索結果がありません。
, + RESULT_EXISTS: () => ( + }> + {results.map(result => ( + +
{ + setQuery(result.title); + closeSearchBox(); + }} + > + {result.title} +
+
+ ))} +
+ ), + CLOSE: () => null, + }} + /> +
+ ); +} +``` + +::: + +## 小さいバンドルサイズ + +Web サービスにとって、応答速度の速さは非常に重要です。だからこそ、Web サービスを構成するライブラリである `react-simplikit` にとって、小さいバンドルサイズは非常に重要です。`react-simplikit` は、今もこれからも、できる限り小さいバンドルサイズを提供できるよう努めています。 + +`react-simplikit` は `react-use` と比較して、以下のように最大で約 89% 小さいサイズを実現しています。 + +| | react-simplikit | react-use | 差分 | +| ---------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------ | ------ | +| Unpacked Size | [237 kB](https://www.npmjs.com/package/react-simplikit) | [454 kB](https://www.npmjs.com/package/react-use) | -47.8% | +| Minified Size | [8.7 kB](https://bundlephobia.com/package/react-simplikit@0.0.29) | [78.2 kB](https://bundlephobia.com/package/react-use@17.6.0) | -88.9% | +| Gzipped Size | [2.9 kB](https://bundlephobia.com/package/react-simplikit@0.0.29) | [22 kB](https://bundlephobia.com/package/react-use@17.6.0) | -86.9% | +| 関数 1 つあたりの平均サイズ
(Minified Size 基準) | 318.2 byte | 696.3 byte | -54.3% | diff --git a/docs/ja/index.md b/docs/ja/index.md new file mode 100644 index 00000000..0c90a9c2 --- /dev/null +++ b/docs/ja/index.md @@ -0,0 +1,25 @@ +--- +layout: home + +hero: + name: 'react-simplikit' + text: '軽量で強力な React ユーティリティライブラリ' + image: + src: /images/symbol.svg + alt: react-simplikit + actions: + - theme: brand + text: Mobile (推奨) + link: /ja/mobile/intro + - theme: alt + text: Core + link: /ja/core/intro + +features: + - title: '依存関係ゼロ' + details: 外部ライブラリが不要なので、プロジェクトを軽く、速く、保守しやすく保てます。バンドルサイズを抑えつつ、パフォーマンスも手軽に改善できます。 + - title: テストカバレッジ 100% + details: すべての関数と分岐を厳密にテストしているため、どのようなユースケースでも安定して動作します。 + - title: 充実したドキュメント + details: 各機能に明確な JSDoc、初心者にもわかりやすいガイド、実用的な使用例が揃っています。詳細で追いやすい説明ですぐに始められます。 +--- diff --git a/docs/ja/mobile/contributing.md b/docs/ja/mobile/contributing.md new file mode 100644 index 00000000..b76359bd --- /dev/null +++ b/docs/ja/mobile/contributing.md @@ -0,0 +1,141 @@ +# @react-simplikit/mobile への貢献 + +このガイドは [core の貢献ガイド](/ja/core/contributing) を拡張したものです。 + +## パッケージのスコープ + +`@react-simplikit/mobile` は、**モバイル Web 環境で直面する問題を解決する**ための専用パッケージです。 + +以下のような領域を扱います。 + +- ビューポート管理(ビジュアルビューポート、セーフエリア) +- キーボード処理(キーボードに隠れるコンテンツの回避) +- iOS Safari や Android Chrome 特有のレイアウトの問題 +- モバイルブラウザにおけるスクロールの挙動 + +このパッケージは、ブラウザ API に依存するすべてのユーティリティのためのものでは**ありません**。ブラウザ API を使用していても、デスクトップや汎用的な課題を解決するフック(例: キーボードショートカット、マウス座標)はここには属しません。 + +## 開発ワークフロー + +``` +スキャフォールディング → 実装 → テスト → ドキュメント化 → レビュー → Changeset → マージ +``` + +### 1. スキャフォールディング + +新しいフックの基本構造を作成します。 + +```bash +yarn scaffold useNewHook --type h # フック +``` + +### 2. 実装 + +[設計原則](/ja/mobile/design-principles) に従ってください。 + +- named export のみを使用する +- TypeScript の型推論を最大限活用する +- SSR 安全パターンを適用する + +```typescript +// ✅ SSR 安全パターン +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` + +### 3. ドキュメント化 + +すべての export 対象の関数には、4 つの必須タグを含む JSDoc が必要です。 + +```typescript +/** + * @description 一行の要約。(必須) + * @param {Type} name - 説明。(パラメータがある場合は必須) + * @returns {Type} 説明。(戻り値がある場合は必須) + * @example + * const result = useHook(input); // (必須) + */ +``` + +::: tip +**ドキュメントは書かなくてもいいですか?** + +はい、ドキュメントを別途書く必要はありません。代わりに、JSDoc コメントを詳しく書いてください。PR がマージされると、JSDoc をもとに英語と韓国語のドキュメントが自動生成され、ドキュメントを追加する PR が自動的に作成されます。 +::: + +### 4. テスト + +100% のカバレッジが必須です。 + +```bash +yarn test:spec # 単一テストを実行 +yarn test:coverage # カバレッジを確認 +``` + +#### SSR テスト(必須) + +```typescript +it('is safe on server side rendering', () => { + const result = renderHookSSR.serverOnly(() => useHook()); + expect(result.current).toBeDefined(); +}); +``` + +#### カバレッジチェックリスト + +- [ ] すべての if/else 分岐 +- [ ] すべての switch case +- [ ] すべての早期リターン +- [ ] クリーンアップ関数(useEffect の戻り値) + +### 5. Changeset を作成する + +コードの変更がパッケージに影響する場合は、changeset を作成する必要があります。 + +```bash +yarn changeset +``` + +変更の種類を選択してください。 + +- `patch`: バグ修正や小さな変更 +- `minor`: 新機能の追加(後方互換性を維持) +- `major`: 破壊的変更(後方互換性が失われる) + +::: tip +両パッケージは現在 `0.0.x` の段階です。この段階では、ほとんどの変更に `patch` を使用してください。 +バージョンの種類に迷う場合は、メンテナーに相談してください。 +::: + +## モバイル特有のガイドライン + +### 実機でのテスト + +- iOS Safari と Android Chrome でのテストを推奨します +- Visual Viewport API の挙動は実機で確認する必要があります + +### プラットフォームの違い + +実装時には、以下のプラットフォームの違いを考慮してください。 + +| 機能 | iOS | Android | +| -------------------------- | ------------------------------------ | -------------------------- | +| `visualViewport.offsetTop` | キーボードが表示されると負の値になる | 基本的に 0 のまま | +| キーボードの挙動 | ビューポートが押し上げられる | レイアウトがリサイズされる | + +### window/document へのアクセスパターン + +ブラウザ API にアクセスする際は、常に SSR 安全パターンを使用してください。 + +```typescript +// ✅ SSR 安全パターン +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; + +// これで window/document を安全に使用できます +window.visualViewport?.addEventListener('resize', handler); +``` + +## ドキュメントへの貢献 + +ドキュメントへの貢献に特別な条件はありません。誤った情報や訳の質が良くない箇所を見つけたり、追加したい内容があれば、自由に編集してください。ドキュメントは読者の視点でわかりやすく、簡潔に書いてください。 diff --git a/docs/ja/mobile/design-principles.md b/docs/ja/mobile/design-principles.md new file mode 100644 index 00000000..e9d4ce5f --- /dev/null +++ b/docs/ja/mobile/design-principles.md @@ -0,0 +1,90 @@ +# 設計原則 + +`@react-simplikit/mobile` は `react-simplikit` のコア原則を踏襲しつつ、モバイル特有の課題に合わせて拡張しています。 + +## コア原則 + +### React のライフサイクルを尊重し、干渉しない + +`@react-simplikit/mobile` は、React のライフサイクルに直接干渉する実装を含みません。 +たとえば、`useMount` や `useLifecycles` のようなフックは提供せず、代わりに React のデフォルトの挙動を尊重し、活用するアプローチを採ります。 + +### 依存関係ゼロによる軽量さと高速さ + +`@react-simplikit/mobile` には依存関係が一切ありません。追加のライブラリに依存しないことで、プロジェクトに組み込む際のバンドルサイズを最小化し、パフォーマンス低下への懸念をなくします。 + +### 100% テストカバレッジによる信頼性の確保 + +`@react-simplikit/mobile` は、すべての関数と分岐を徹底的にテストします。 +基本機能だけでなく、各実装の SSR 環境における考慮事項も含めた包括的なテストを書くことで、予期しない挙動による問題を防いでいます。 + +### わかりやすく使いやすい包括的なドキュメント + +`@react-simplikit/mobile` は、ユーザーが各機能を素早く理解し活用できるよう、詳細なドキュメントを提供します。ドキュメントには以下が含まれます。 + +- **JSDoc コメント**: 各関数の挙動、パラメータ、戻り値についての詳しい説明。 +- **使用ガイド**: すぐに始められる、明確でわかりやすい手順。 +- **実践的な使用例**: 実際のシナリオで実装を活用する方法を示す例。 + +### 完全な TypeScript サポートによる型安全性 + +`@react-simplikit/mobile` は、最初から TypeScript で構築されています。すべてのフックとユーティリティには、以下が備わっています。 + +- **厳密な型定義**: すべてのパラメータ、戻り値、オプションが完全に型付けされています +- **IntelliSense サポート**: IDE で自動補完とインラインドキュメントを利用できます +- **ジェネリック型**: 型情報を保持する柔軟な API を提供します +- **`any` 型を使用しない**: 型安全性を損なうエスケープハッチを避けています + +## API 設計基準 + +### フックの戻り値 + +フックの戻り値については、一貫したパターンに従います。 + +- **オブジェクト**: 状態や関連する値を返す場合(例: `useKeyboardHeight(): { keyboardHeight }`、`useVisualViewport(): { viewport }`) +- **void**: 副作用のみを持つフックの場合(例: `useBodyScrollLock(): void`) + +### パラメータ + +- 必須パラメータを先に、任意パラメータを後に配置します +- 任意パラメータが 3 個以上ある場合はオプションオブジェクトを使用します + +### SSR 安全パターン + +すべてのフックは SSR 安全パターンに従います。 + +```typescript +// ✅ SSR 安全 - すべてのフックがこのパターンに従います +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` + +## モバイル特有の原則 + +### プラットフォームを意識した設計 + +実装においては、iOS と Android の挙動の違いを考慮します。 + +- **Visual Viewport API の違い**: + - iOS: キーボードが表示されると `offsetTop` が負の値になります + - Android: `offsetTop` は基本的に 0 のままです +- **キーボードの高さの計算**: 正確な計測のためのプラットフォーム別の処理 + +### SSR 安全性を最優先に + +すべてのフックには、安全なサーバーサイドレンダリングを保証するための SSR テストが含まれます。 + +```typescript +it('is safe on server side rendering', () => { + const result = renderHookSSR.serverOnly(() => useHook()); + expect(result.current).toBeDefined(); +}); +``` + +### パフォーマンス最適化 + +モバイル環境ではパフォーマンスに特別な配慮が必要です。 + +- **イベントのスロットリング/デバウンス**: スクロールやリサイズのような頻発するイベントを最適化します +- **パッシブイベントリスナー**: 適用可能な場合はパッシブリスナーを使用します +- **React トランジション**: 緊急でない更新には `startTransition` を活用します diff --git a/docs/ja/mobile/installation.md b/docs/ja/mobile/installation.md new file mode 100644 index 00000000..da1e4e77 --- /dev/null +++ b/docs/ja/mobile/installation.md @@ -0,0 +1,42 @@ +--- +description: '@react-simplikit/mobile のインストール方法' +--- + +# インストール + +お好みのパッケージマネージャーを使って、[npm](https://npmjs.com/package/@react-simplikit/mobile) から `@react-simplikit/mobile` をインストールできます。 + +::: code-group + +```sh [npm] +npm install @react-simplikit/mobile +``` + +```sh [pnpm] +pnpm add @react-simplikit/mobile +``` + +```sh [yarn] +yarn add @react-simplikit/mobile +``` + +```sh [bun] +bun add @react-simplikit/mobile +``` + +::: + +## 要件 + +- React 18 以上 +- TypeScript 4.7 以上(推奨) + +## 使い方 + +パッケージから直接フックを import してください。 + +```tsx +import { useKeyboardHeight, useAvoidKeyboard } from '@react-simplikit/mobile'; +``` + +すべてのフックはツリーシェイキング対応なので、実際に使用するものだけがバンドルに含まれます。 diff --git a/docs/ja/mobile/intro.md b/docs/ja/mobile/intro.md new file mode 100644 index 00000000..7143888d --- /dev/null +++ b/docs/ja/mobile/intro.md @@ -0,0 +1,123 @@ +# @react-simplikit/mobile + +モバイル Web 環境でよくある UI の課題を解決する React フック集です。 + +## なぜ @react-simplikit/mobile なのか + +モバイル Web 開発には、デスクトップにはない固有の課題があります。 + +- **キーボード回避**: オンスクリーンキーボードが表示されると、下部に固定した要素が隠れてしまいます +- **スクロール方向の検知**: スクロールに応じてヘッダーやナビゲーションバーを表示・非表示にします +- **ネットワーク状態の監視**: 接続速度に応じてコンテンツの品質を調整します +- **ページ可視性の追跡**: アプリがバックグラウンドに移動したときに動画や計測を一時停止します +- **ビジュアルビューポートの変化**: モバイルブラウザでのズーム、キーボード、ビューポートのリサイズに対応します + +`@react-simplikit/mobile` は、これらのシナリオを最小限の設定で扱える実績のあるフックを提供します。 + +## クイックスタート + +```bash +npm install @react-simplikit/mobile +``` + +### CTA ボタンの例 + +もっとも一般的なモバイル UI パターンです。キーボードの上に移動する下部固定ボタンです。 + +```tsx +import { useAvoidKeyboard } from '@react-simplikit/mobile'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### チャット入力欄の例 + +キーボードの上に留まる入力欄を持つチャット UI です。 + +```tsx +import { useState } from 'react'; +import { useAvoidKeyboard } from '@react-simplikit/mobile'; + +function ChatInput() { + const { style } = useAvoidKeyboard(); + const [message, setMessage] = useState(''); + + return ( +
+ setMessage(e.target.value)} + placeholder="Type a message..." + style={{ flex: 1 }} + /> + +
+ ); +} +``` + +### セーフエリアへの対応 + +ホームインジケーターを備えた端末(iPhone など)では、セーフエリアのオフセットを追加できます。 + +```tsx +import { useAvoidKeyboard } from '@react-simplikit/mobile'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); + + return ( +
+ +
+ ); +} +``` + +## 利用可能なフック + +| フック | 説明 | +| --------------------------------------------------------- | -------------------------------------------------------------- | +| [useAvoidKeyboard](/ja/mobile/hooks/useAvoidKeyboard) | 固定要素をオンスクリーンキーボードの上に移動させます | +| [useKeyboardHeight](/ja/mobile/hooks/useKeyboardHeight) | 現在のキーボードの高さを返します | +| [useBodyScrollLock](/ja/mobile/hooks/useBodyScrollLock) | モーダルやオーバーレイのために body のスクロールをロックします | +| [useScrollDirection](/ja/mobile/hooks/useScrollDirection) | スクロール方向(上/下)を検知します | +| [useNetworkStatus](/ja/mobile/hooks/useNetworkStatus) | ネットワーク接続状態を監視します | +| [usePageVisibility](/ja/mobile/hooks/usePageVisibility) | ページの可視性の状態を追跡します | +| [useVisualViewport](/ja/mobile/hooks/useVisualViewport) | ビジュアルビューポートのサイズとオフセットを提供します | diff --git a/docs/ja/mobile/roadmap.md b/docs/ja/mobile/roadmap.md new file mode 100644 index 00000000..637d7f6b --- /dev/null +++ b/docs/ja/mobile/roadmap.md @@ -0,0 +1,41 @@ +# ロードマップ + +モバイル画面は小さく、その小さな空間が驚くほど多くの UI 課題を生み出します。要素がオンスクリーンキーボードに隠れたり、セーフエリアが端末によって異なったり、ユーザーが実際に見ているビューポートがブラウザの報告する値と食い違ったりします。これらはエッジケースではなく、モバイル開発における日常的な現実です。 + +## 課題: モバイル画面での不安定な UI + +モバイル端末では、ユーザーが画面で見るものと開発者が想定するものが必ずしも一致しません。よくあるシナリオをいくつか紹介します。 + +- **キーボードが入力欄を覆う**: ユーザーがテキスト入力欄をタップすると、オンスクリーンキーボードがせり上がり、入力欄や下部に固定された送信ボタンを完全に覆ってしまうことがあります。 +- **セーフエリアの不整合**: ノッチ、丸みを帯びた角、ホームインジケーター(iPhone の下部バーなど)を持つ端末には、コンテンツを配置すべきでない予約領域がありますが、これは端末や OS のバージョンによって異なります。 +- **ビューポートの混乱**: ブラウザのレイアウトビューポートと実際に見える領域(ビジュアルビューポート)は、特にキーボードが開いていたりページがズームされていたりする場合に大きく異なることがあります。固定位置の要素が予期しない場所に配置されてしまうこともあります。 + +これらの課題は特定の OS や端末に固有のものではありません。iOS Safari であれ、Android Chrome であれ、その他どのモバイルブラウザであれ、根本的な課題は同じです。**見える領域は予測不可能であり、標準の CSS だけでは信頼できる形で対処できない**のです。 + +## 私たちのアプローチ: ビジュアルビューポートに焦点を当てる + +`@react-simplikit/mobile` は、これらの問題を解決するために焦点を絞ったアプローチを取ります。もろいハックでブラウザの癖を回避しようとするのではなく、**ビジュアルビューポート** — ユーザーがある瞬間に実際に見ている画面領域 — を中心に設計しています。 + +[Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) をベースに構築することで、以下のようなことができるフックを提供します。 + +- **キーボードの表示を検知して対応する**ことで、下部固定要素が自然にキーボードを避けるようにします。 +- **セーフエリアインセットを読み取る**ことで、ノッチやホームインジケーターなど、端末固有の予約領域を正しく考慮します。 +- **実際に見える領域を追跡する**ことで、ブラウザのレイアウトエンジンが想定するものではなく、ユーザーが実際に見ているものに基づいてレイアウトを決定できます。 + +目標はシンプルです。**ビジュアルビューポート内で、UI が確実かつ予測可能にレンダリングされること**です。 + +## クロスプラットフォーム、クロスデバイス + +特定の OS や端末モデルに限定されないことを目指しています。モバイル Web は本質的にクロスプラットフォームであり、`@react-simplikit/mobile` はそれを受け入れています。 + +私たちのフックは、以下の環境で一貫して動作するように設計されています。 + +- **iOS と Android** — 2 大モバイルプラットフォーム。 +- **さまざまなブラウザ** — Safari、Chrome、Samsung Internet など。 +- **さまざまな端末フォームファクター** — コンパクトな端末から大画面端末まで、ノッチやホームインジケーターの有無を問いません。 + +特定の API が利用できない場合(たとえば古いブラウザの `window.visualViewport`)でも、UI を壊すことなく段階的に劣化する安全なフォールバックを提供します。 + +## 今後の展開 + +`@react-simplikit/mobile` で提供するフックのラインナップを、常に同じ原則に基づいて拡張し続けています。**端末や OS を問わず、モバイル UI 開発を予測可能で信頼できるものにする**という原則です。よくあるモバイル UI の悩みがあれば、私たちはそのためのクリーンで宣言的な解決策に取り組んでいる可能性が高いです。 diff --git a/packages/core/README-ja_jp.md b/packages/core/README-ja_jp.md new file mode 100644 index 00000000..0969ae57 --- /dev/null +++ b/packages/core/README-ja_jp.md @@ -0,0 +1,143 @@ +# react-simplikit + +[![npm version](https://img.shields.io/npm/v/react-simplikit.svg)](https://www.npmjs.com/package/react-simplikit) +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) +[![codecov](https://codecov.io/gh/toss/react-simplikit/graph/badge.svg?token=RHVOZ3J3TU)](https://codecov.io/gh/toss/react-simplikit) + +[English](./README.md) | [한국어](./README-ko_kr.md) | 日本語 + +フック、コンポーネント、ユーティリティを提供する、軽量で依存関係のない React ユーティリティライブラリです。 + +## 特長 + +- **依存関係ゼロ** - 非常に軽量 +- **100% TypeScript** - 完全な型安全性 +- **100% テストカバレッジ** - 信頼性と安定性 +- **SSR 安全** - Next.js などの SSR フレームワークで動作 +- **ツリーシェイキング対応** - 使用するものだけがバンドルされる + +## ライブラリの方針 + +**react-simplikit は現在、純粋な状態/ロジック用フックのみを提供する Universal Hook Library として維持されています。** + +react-simplikit は、Web とモバイル(React Native など)の両方でシームレスに動作する**プラットフォームに依存しないフック**に焦点を絞る方向へと再編を進めています。 + +### 維持されるもの: 純粋な状態/ロジック用フック + +特定のプラットフォーム API に依存しないフックは、引き続き積極的にメンテナンスされます。 + +- `useToggle`、`useBooleanState`、`useCounter` のような状態管理フック +- `usePrevious` のようなライフサイクルフック +- `useDebounce`、`useThrottle` のようなユーティリティフック +- これらの既存の純粋なロジックフックについては、**後方互換性(BC)が維持されます** + +### 非推奨となったもの: ブラウザ/プラットフォームに依存するフック + +ブラウザ固有の API に強く依存する以下のフックは、非推奨となりました。 + +- `useGeolocation` - `navigator.geolocation` に依存 +- `useStorageState` - `localStorage`/`sessionStorage` に依存 +- `useIntersectionObserver` - `IntersectionObserver` API に依存 +- `useImpressionRef` - `IntersectionObserver` + Visibility API に依存 +- `useDoubleClick`、`useLongPress` - DOM イベント + `window.setTimeout` に依存 +- `useOutsideClickEffect` - DOM イベント + `document` に依存 +- `useVisibilityEvent` - `document.visibilityState` に依存 + +これらのフックは: + +- 新機能の追加や大きな改善は行われません +- ドキュメント上で `@deprecated` と明記されます +- 将来のメジャーバージョンで削除される可能性があります + +### パッケージのステータス + +- react-simplikit は**アーカイブされません** +- 純粋な状態/ロジック用フックは引き続きメンテナンスされます +- 重大なバグ修正と最小限のメンテナンスは継続します +- 新しいブラウザ/プラットフォーム依存のフックは追加されません + +## インストール + +```bash +npm install react-simplikit +# or +yarn add react-simplikit +# or +pnpm add react-simplikit +``` + +## クイックスタート + +```tsx +import { useState } from 'react'; +import { useDebounce } from 'react-simplikit'; + +function SearchInput() { + const [query, setQuery] = useState(''); + + const debouncedSearch = useDebounce((value: string) => { + // 実際の API 呼び出し + searchAPI(value); + }, 300); + + return ( + { + setQuery(e.target.value); + debouncedSearch(e.target.value); + }} + placeholder="検索キーワードを入力" + /> + ); +} +``` + +デバウンスされた関数は `.cancel()` を提供し、コンポーネントがアンマウントされると保留中の呼び出しは自動的にキャンセルされます。 + +## 提供している機能 + +### Hooks + +| Hook | 説明 | +| ------------------------- | ------------------------------------------------------------- | +| `useBooleanState` | ハンドラー付きで boolean の状態を管理 | +| `useDebounce` | コールバック関数をデバウンス | +| `useDebouncedCallback` | オプションオブジェクトで `onChange` コールバックをデバウンス | +| `useInterval` | 宣言的にインターバルを設定 | +| `useIntersectionObserver` | 要素の可視性を監視 | +| `usePreservedCallback` | 安定したコールバック参照 | +| `usePreservedReference` | 安定したオブジェクト参照 | +| ... | [すべてのフックを見る](https://react-simplikit.slash.page/ja) | + +### Components + +| Component | 説明 | +| ---------------- | -------------------------------------- | +| `SwitchCase` | 宣言的な switch-case レンダリング | +| `Separated` | 区切り要素付きでアイテムをレンダリング | +| `ImpressionArea` | 要素の表示(インプレッション)を追跡 | + +### Utilities + +| Utility | 説明 | +| -------------- | ------------------------------------------------------- | +| `buildContext` | 定型コードを減らして React Context を定義 | +| `mergeProps` | `className`、`style`、イベントを合成して props をマージ | +| `mergeRefs` | 複数の ref を 1 つの ref にまとめる | + +## ドキュメント + +詳しいドキュメントは [react-simplikit.slash.page](https://react-simplikit.slash.page/ja) をご覧ください。 + +## 関連パッケージ + +- [@react-simplikit/mobile](https://www.npmjs.com/package/@react-simplikit/mobile) - モバイル Web ユーティリティ + +## 貢献 + +貢献を歓迎します![貢献ガイド](https://github.com/toss/react-simplikit/blob/main/CONTRIBUTING.md) をご確認ください。 + +## ライセンス + +MIT © Viva Republica, Inc. 詳しくは [LICENSE](https://github.com/toss/react-simplikit/blob/main/LICENSE) を参照してください。 diff --git a/packages/core/README-ko_kr.md b/packages/core/README-ko_kr.md index 6c484534..bfbbc85c 100644 --- a/packages/core/README-ko_kr.md +++ b/packages/core/README-ko_kr.md @@ -4,7 +4,7 @@ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/react-simplikit/graph/badge.svg?token=RHVOZ3J3TU)](https://codecov.io/gh/toss/react-simplikit) -[English](./README.md) | 한국어 +[English](./README.md) | 한국어 | [日本語](./README-ja_jp.md) React 환경에서 유용하게 사용할 수 있는 다양한 훅, 컴포넌트, 유틸리티를 제공하는 가볍고 강력한 라이브러리예요. diff --git a/packages/core/README.md b/packages/core/README.md index d54cc622..76e86541 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -4,7 +4,7 @@ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/react-simplikit/graph/badge.svg?token=RHVOZ3J3TU)](https://codecov.io/gh/toss/react-simplikit) -English | [한국어](./README-ko_kr.md) +English | [한국어](./README-ko_kr.md) | [日本語](./README-ja_jp.md) A lightweight, zero-dependency React utilities library providing hooks, components, and utilities. diff --git a/packages/mobile/README-ja_jp.md b/packages/mobile/README-ja_jp.md new file mode 100644 index 00000000..e39b76b2 --- /dev/null +++ b/packages/mobile/README-ja_jp.md @@ -0,0 +1,140 @@ +# @react-simplikit/mobile + +[![npm version](https://img.shields.io/npm/v/@react-simplikit/mobile.svg)](https://www.npmjs.com/package/@react-simplikit/mobile) +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) + +[English](./README.md) | [한국어](./README-ko_kr.md) | 日本語 + +iOS Safari と Android Chrome の viewport、キーボード、レイアウトの問題を解決する React 向けモバイル Web ユーティリティです。 + +## なぜ必要なのか + +モバイル Web 開発は難しいものです。iOS Safari と Android Chrome には、以下のような問題を引き起こす癖があります。 + +- キーボードが開いたときの viewport の高さの変化 +- モーダルでの body スクロールの問題 +- Safe area inset の処理 +- Visual viewport の不整合 + +`@react-simplikit/mobile` は、こうしたよくある問題に対する実績のある解決策を提供します。 + +## 特長 + +- **キーボード処理** - 仮想キーボードによってコンテンツが隠れるのを防ぐ +- **Body scroll lock** - モーダルでの背景スクロールを防ぐ +- **Visual viewport** - 実際に見える領域を追跡 +- **Safe area** - ノッチとホームインジケーターを処理 +- **端末の検出** - iOS、Android、ブラウザの種類を検出 +- **SSR 安全** - Next.js などの SSR フレームワークで動作 + +## インストール + +```bash +npm install @react-simplikit/mobile +# or +yarn add @react-simplikit/mobile +# or +pnpm add @react-simplikit/mobile +``` + +## クイックスタート + +### キーボード回避ビュー + +```tsx +import { useAvoidKeyboard } from '@react-simplikit/mobile'; + +function ChatInput() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### Body Scroll Lock + +`useBodyScrollLock` は、コンポーネントがマウントされている間 body のスクロールをロックし、アンマウント時に自動的に解除します。ロックのタイミングを制御するには、このフックを呼び出すコンポーネントを条件付きでレンダリングしてください。 + +```tsx +import { useBodyScrollLock } from '@react-simplikit/mobile'; + +function BodyScrollLock() { + useBodyScrollLock(); + return null; +} + +function ModalContainer({ isOpen, children }) { + return ( + <> + {isOpen && } + {isOpen &&
{children}
} + + ); +} +``` + +### Visual Viewport + +```tsx +import { useVisualViewport } from '@react-simplikit/mobile'; + +function Component() { + const { viewport } = useVisualViewport(); + + // 必ず最初に null チェックを行ってください + if (!viewport) { + return null; + } + + return ( +
+ 実際に見える高さ: {viewport.height}px +
+ ); +} +``` + +## 提供している機能 + +### Hooks + +| Hook | 説明 | +| -------------------- | -------------------------------------------- | +| `useAvoidKeyboard` | キーボードによってコンテンツが隠れるのを防ぐ | +| `useBodyScrollLock` | body スクロールをロック(モーダル用) | +| `useVisualViewport` | visual viewport のサイズを追跡 | +| `useScrollDirection` | スクロール方向を検知 | +| `useNetworkStatus` | ネットワーク接続状態を監視 | +| `usePageVisibility` | ページの可視性の状態を追跡 | + +### Utilities + +| Utility | 説明 | +| ------------------------------------------------ | ------------------------------ | +| `enableBodyScrollLock` / `disableBodyScrollLock` | 命令的なスクロールロックの制御 | +| `getSafeAreaInset` | Safe area inset を取得 | +| `getKeyboardHeight` | キーボードの高さを推定 | +| `isIOS` / `isAndroid` | 端末の検出 | +| `isServer` | SSR 環境かどうかを判定 | + +## 対応ブラウザ + +- iOS Safari 13+ +- Android Chrome 80+ +- デスクトップブラウザ(graceful fallback) + +## 関連パッケージ + +- [react-simplikit](https://www.npmjs.com/package/react-simplikit) - Core hooks & utilities + +## 貢献 + +貢献を歓迎します![貢献ガイド](https://github.com/toss/react-simplikit/blob/main/CONTRIBUTING.md) をご確認ください。 + +## ライセンス + +MIT © Viva Republica, Inc. 詳しくは [LICENSE](https://github.com/toss/react-simplikit/blob/main/LICENSE) を参照してください。 diff --git a/packages/mobile/README-ko_kr.md b/packages/mobile/README-ko_kr.md index 5004989c..4a026bc0 100644 --- a/packages/mobile/README-ko_kr.md +++ b/packages/mobile/README-ko_kr.md @@ -3,7 +3,7 @@ [![npm version](https://img.shields.io/npm/v/@react-simplikit/mobile.svg)](https://www.npmjs.com/package/@react-simplikit/mobile) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) -[English](./README.md) | 한국어 +[English](./README.md) | 한국어 | [日本語](./README-ja_jp.md) 모바일 웹을 위한 React 유틸리티 - iOS Safari와 Android Chrome의 뷰포트, 키보드, 레이아웃 문제를 해결해요. diff --git a/packages/mobile/README.md b/packages/mobile/README.md index f2ea7614..d35e36cf 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -3,7 +3,7 @@ [![npm version](https://img.shields.io/npm/v/@react-simplikit/mobile.svg)](https://www.npmjs.com/package/@react-simplikit/mobile) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/react-simplikit/blob/main/LICENSE) -English | [한국어](./README-ko_kr.md) +English | [한국어](./README-ko_kr.md) | [日本語](./README-ja_jp.md) Mobile web utilities for React - fixing viewport, keyboard, and layout issues on iOS Safari and Android Chrome.