From 4799fcff55713594fd19a1987081bccf13920ca7 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:01:29 +0900 Subject: [PATCH 1/7] feat(docs): flatten reference URLs and emit legacy redirect stubs --- .scripts/utils/assertLlmsOutput.ts | 10 ++- .scripts/verifyDocsI18n.ts | 112 +++++++++++++++++--------- .vitepress/config.mts | 6 ++ .vitepress/libs/buildLocaleConfig.mts | 61 ++++---------- .vitepress/libs/legacyRedirects.mts | 80 ++++++++++++++++++ .vitepress/locales.mts | 50 +++++++++--- .vitepress/locales/en.mts | 1 + .vitepress/locales/es.mts | 1 + .vitepress/locales/ja.mts | 1 + .vitepress/locales/ko.mts | 1 + .vitepress/locales/zh-Hans.mts | 1 + docs/es/mobile/intro.md | 18 ++--- docs/ja/mobile/intro.md | 18 ++--- docs/ko/mobile/intro.md | 18 ++--- docs/mobile/intro.md | 18 ++--- docs/zh-Hans/mobile/intro.md | 18 ++--- 16 files changed, 271 insertions(+), 143 deletions(-) create mode 100644 .vitepress/libs/legacyRedirects.mts diff --git a/.scripts/utils/assertLlmsOutput.ts b/.scripts/utils/assertLlmsOutput.ts index cd3b2f16..77308bc7 100644 --- a/.scripts/utils/assertLlmsOutput.ts +++ b/.scripts/utils/assertLlmsOutput.ts @@ -14,7 +14,7 @@ const PACKAGE_INDEX_FILE = 'packages/react-simplikit/src/index.ts'; // The generated links are absolute (the plugin's `domain` option), and every documentation page // lives under core/ or mobile/. A ko/ or ja/ link means the localized copies leaked into the // listing, which would make an agent read the same page several times in different languages. -const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(core|mobile)\//; +const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(?:hooks|components|utils|core|mobile)\/[^/]+\.md$/; /** * Checks the llms outputs vitepress-plugin-llms wrote into a docs build: @@ -28,7 +28,11 @@ export async function assertLlmsOutput({ buildOutputDirectory, root }: AssertLlm assert.notEqual(links.length, 0, 'llms.txt must list the documentation pages'); for (const link of links) { - assert.match(link, ALLOWED_LINK, `llms.txt must only link English pages under core/ or mobile/: ${link}`); + assert.match( + link, + ALLOWED_LINK, + `llms.txt must only link English pages in the flat reference namespaces or the guides: ${link}` + ); } for (const name of await collectPublicExports(path.join(root, PACKAGE_INDEX_FILE))) { @@ -42,7 +46,7 @@ export async function assertLlmsOutput({ buildOutputDirectory, root }: AssertLlm const llmsFullTxt = await fs.readFile(path.join(buildOutputDirectory, 'llms-full.txt'), 'utf8'); assert.equal(llmsFullTxt.includes('# useDebounce'), true, 'llms-full.txt must inline the page contents'); - const pageMarkdown = await fs.readFile(path.join(buildOutputDirectory, 'core/hooks/useDebounce.md'), 'utf8'); + const pageMarkdown = await fs.readFile(path.join(buildOutputDirectory, 'hooks/useDebounce.md'), 'utf8'); assert.equal(pageMarkdown.includes('# useDebounce'), true, 'each page must be served as Markdown'); assert.equal(pageMarkdown.includes(' item.text === hookFixtureName), - { text: hookFixtureName, link: `/ko/core/hooks/${hookFixtureName}` }, + getSidebarItems(corePackageRoot, 'hooks', '', 'ko').find(item => item.text === hookFixtureName), + { text: hookFixtureName, link: `/ko/hooks/${hookFixtureName}` }, 'the Korean sidebar must link the fallback page so it is not reachable by URL only' ); } finally { @@ -138,6 +138,7 @@ const unregisteredLocaleFixture = { componentsLabel: 'Componentes', hooksLabel: 'Hooks', utilsLabel: 'Utilitários', + mobileWebLabel: 'Web móvel', guidePages: { core: { intro: 'Introdução', @@ -163,23 +164,33 @@ const unregisteredLocaleFixture = { const unregisteredConfig = buildLocaleConfig(unregisteredLocaleFixture); assert.equal(unregisteredConfig.lang, 'pt-BR'); -assert.deepEqual(unregisteredConfig.themeConfig?.nav, [ - { text: 'Início', link: '/pt-BR/' }, - { text: 'Guide', link: '/pt-BR/core/intro' }, - { text: 'Mobile Utilities', link: '/pt-BR/mobile/intro' }, -]); -assert.deepEqual(Object.keys(unregisteredConfig.themeConfig?.sidebar ?? {}), ['/pt-BR/core/', '/pt-BR/mobile/']); +const unregisteredConfigNav = unregisteredConfig.themeConfig?.nav ?? []; +assert.deepEqual(unregisteredConfigNav[0], { text: 'Início', link: '/pt-BR/' }); +assert.deepEqual(unregisteredConfigNav[1], { text: 'Guide', link: '/pt-BR/core/intro' }); +assert.equal((unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Referência'); +assert.equal( + (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/pt-BR/hooks/') || + (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/pt-BR/core/intro', + true, + 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' +); +assert.deepEqual(Object.keys(unregisteredConfig.themeConfig?.sidebar ?? {}), ['/pt-BR/']); assert.equal(unregisteredConfig.themeConfig?.editLink?.text, 'Editar esta página no GitHub'); const koConfig = buildLocaleConfig(localeDefinitions.ko); const rootConfig = buildLocaleConfig(localeDefinitions.root); -assert.deepEqual(koConfig.themeConfig?.nav, [ - { text: '홈', link: '/ko/' }, - { text: 'Guide', link: '/ko/core/intro' }, - { text: 'Mobile Utilities', link: '/ko/mobile/intro' }, -]); -assert.deepEqual((koConfig.themeConfig?.sidebar as Record)['/ko/core/'][0], { +const koConfigNav = koConfig.themeConfig?.nav ?? []; +assert.deepEqual(koConfigNav[0], { text: '홈', link: '/ko/' }); +assert.deepEqual(koConfigNav[1], { text: 'Guide', link: '/ko/core/intro' }); +assert.equal((koConfigNav[2] as DefaultTheme.NavItemWithLink).text, '레퍼런스'); +assert.equal( + (koConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/ko/hooks/') || + (koConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ko/core/intro', + true, + 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' +); +assert.deepEqual((koConfig.themeConfig?.sidebar as Record)['/ko/'][0], { text: '가이드', items: [ { text: '소개', link: '/ko/core/intro' }, @@ -187,28 +198,39 @@ assert.deepEqual((koConfig.themeConfig?.sidebar as Record [ @@ -76,6 +77,11 @@ Guidelines for AI agents: ], }, rewrites: { ...rewrites, ...generatedRewrites }, + sitemap: { hostname: 'https://react-simplikit.slash.page' }, + buildEnd: async siteConfig => { + const count = writeLegacyRedirectStubs(siteConfig.outDir); + console.log(`legacy redirect stubs: ${count}`); + }, head: [ ['link', { rel: 'stylesheet', href: 'https://static.toss.im/tps/main.css' }], ['link', { rel: 'stylesheet', href: 'https://static.toss.im/tps/others.css' }], diff --git a/.vitepress/libs/buildLocaleConfig.mts b/.vitepress/libs/buildLocaleConfig.mts index e51c2e97..4ec0afc1 100644 --- a/.vitepress/libs/buildLocaleConfig.mts +++ b/.vitepress/libs/buildLocaleConfig.mts @@ -16,16 +16,25 @@ export function buildLocaleConfig( const sidebarLocale = definition.path === '' ? undefined : definition.path; const strings = definition.themeStrings; + // Reference URLs are flat: category segment only, no core/mobile namespace. + const hooks = getSidebarItems(corePackageRoot, 'hooks', '', sidebarLocale); + const components = getSidebarItems(corePackageRoot, 'components', '', sidebarLocale); + const utils = getSidebarItems(corePackageRoot, 'utils', '', sidebarLocale); + const mobileWeb = [ + ...getSidebarItems(mobilePackageRoot, 'hooks', '', sidebarLocale), + ...getSidebarItems(mobilePackageRoot, 'utils', '', sidebarLocale), + ].sort((a, b) => (a.text ?? '').localeCompare(b.text ?? '')); + return { lang: definition.lang, themeConfig: { nav: [ { text: strings.homeNavLabel, link: `${prefix}/` }, { text: 'Guide', link: `${prefix}/core/intro` }, - { text: 'Mobile Utilities', link: `${prefix}/mobile/intro` }, + { text: strings.referenceLabel, link: hooks[0]?.link ?? `${prefix}/core/intro` }, ], sidebar: { - [`${prefix}/core/`]: [ + [`${prefix}/`]: [ { text: strings.guideLabel, items: [ @@ -37,55 +46,17 @@ export function buildLocaleConfig( { text: strings.guidePages.core.installation, link: `${prefix}/core/installation` }, { text: strings.guidePages.core.aiIntegration, link: `${prefix}/core/ai-integration` }, { text: strings.guidePages.core.designPrinciples, link: `${prefix}/core/design-principles` }, + { text: strings.mobileWebLabel, link: `${prefix}/mobile/intro` }, { text: strings.guidePages.core.contributing, link: `${prefix}/core/contributing` }, ], }, { text: strings.referenceLabel, items: sortByText([ - { - text: strings.componentsLabel, - collapsed: false, - items: getSidebarItems(corePackageRoot, 'components', '/core', sidebarLocale), - }, - { - text: strings.hooksLabel, - collapsed: false, - items: getSidebarItems(corePackageRoot, 'hooks', '/core', sidebarLocale), - }, - { - text: strings.utilsLabel, - collapsed: false, - items: getSidebarItems(corePackageRoot, 'utils', '/core', sidebarLocale), - }, - ]), - }, - ], - [`${prefix}/mobile/`]: [ - { - text: strings.guideLabel, - items: [ - { text: strings.guidePages.mobile.intro, link: `${prefix}/mobile/intro` }, - { text: strings.guidePages.mobile.roadmap, link: `${prefix}/mobile/roadmap` }, - { text: strings.guidePages.mobile.installation, link: `${prefix}/mobile/installation` }, - { text: strings.guidePages.mobile.designPrinciples, link: `${prefix}/mobile/design-principles` }, - { text: strings.guidePages.mobile.contributing, link: `${prefix}/mobile/contributing` }, - ], - }, - { - text: strings.referenceLabel, - items: [ - { - text: strings.hooksLabel, - collapsed: false, - items: getSidebarItems(mobilePackageRoot, 'hooks', '/mobile', sidebarLocale), - }, - { - text: strings.utilsLabel, - collapsed: false, - items: getSidebarItems(mobilePackageRoot, 'utils', '/mobile', sidebarLocale), - }, - ], + { text: strings.componentsLabel, collapsed: false, items: components }, + { text: strings.hooksLabel, collapsed: false, items: hooks }, + { text: strings.utilsLabel, collapsed: false, items: utils }, + ]).concat([{ text: strings.mobileWebLabel, collapsed: false, items: mobileWeb }]), }, ], }, diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts new file mode 100644 index 00000000..20adc77b --- /dev/null +++ b/.vitepress/libs/legacyRedirects.mts @@ -0,0 +1,80 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { legacyRoutePatterns } from '../locales.mts'; +import { projectRoot } from '../shared.mts'; + +const SITE_ORIGIN = 'https://react-simplikit.slash.page'; + +type RedirectPair = { from: string; to: string }; + +/** + * Expands the parameterized legacy routes against the actual source tree. + * A pattern like `packages/react-simplikit/src/hooks/:hook/:hook.md` with the + * legacy destination `core/hooks/:hook.md` yields one pair per hook directory. + */ +export function collectLegacyRedirects(): RedirectPair[] { + const pairs: RedirectPair[] = []; + + for (const route of legacyRoutePatterns) { + const parameter = route.from.match(/:([A-Za-z]+)\.md$/)?.[1]; + + if (parameter === undefined) { + continue; + } + + const itemsRoot = path.join(projectRoot, route.source.split(`/:${parameter}/`)[0]); + + if (!fs.existsSync(itemsRoot)) { + continue; + } + + for (const entry of fs.readdirSync(itemsRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + + pairs.push({ + from: route.from.replace(`:${parameter}`, entry.name).replace(/\.md$/, '.html'), + to: route.to.replace(`:${parameter}`, entry.name).replace(/\.md$/, '.html'), + }); + } + } + + return pairs; +} + +/** + * Writes one redirect stub per legacy URL into the build output. An instant + * meta refresh is treated as a permanent redirect by search engines, and the + * canonical link points them at the new URL, so the stubs work on any static + * host with no server configuration. + */ +export function writeLegacyRedirectStubs(outDir: string): number { + const pairs = collectLegacyRedirects(); + + for (const { from, to } of pairs) { + const target = `/${to}`; + const stubPath = path.join(outDir, from); + + fs.mkdirSync(path.dirname(stubPath), { recursive: true }); + fs.writeFileSync( + stubPath, + [ + '', + '', + '', + '', + ``, + ``, + '', + `Redirecting to ${target}`, + '', + `

This page moved to ${target}.

`, + '', + ].join('\n') + ); + } + + return pairs.length; +} diff --git a/.vitepress/locales.mts b/.vitepress/locales.mts index c77b2db7..51f27711 100644 --- a/.vitepress/locales.mts +++ b/.vitepress/locales.mts @@ -33,6 +33,7 @@ export type LocaleThemeStrings = { componentsLabel: string; hooksLabel: string; utilsLabel: string; + mobileWebLabel: string; guidePages: GuidePageTitles; editLinkText: string; footerMessage: string; @@ -52,6 +53,9 @@ type RouteDefinition = { destination: string; localizedSource: string; localizedDestination: string; + /** Pre-flattening URL kept only to emit redirect stubs. */ + legacyDestination?: string; + localizedLegacyDestination?: string; }; export const localeDefinitions: Record = { @@ -119,33 +123,43 @@ const routeDefinitions: RouteDefinition[] = [ }, { source: 'packages/react-simplikit/src/hooks/:hook/:hook.md', - destination: 'core/hooks/:hook.md', + destination: 'hooks/:hook.md', + legacyDestination: 'core/hooks/:hook.md', localizedSource: 'packages/react-simplikit/src/hooks/:hook/:locale/:hook.md', - localizedDestination: ':locale/core/hooks/:hook.md', + localizedDestination: ':locale/hooks/:hook.md', + localizedLegacyDestination: ':locale/core/hooks/:hook.md', }, { source: 'packages/react-simplikit/src/components/:component/:component.md', - destination: 'core/components/:component.md', + destination: 'components/:component.md', + legacyDestination: 'core/components/:component.md', localizedSource: 'packages/react-simplikit/src/components/:component/:locale/:component.md', - localizedDestination: ':locale/core/components/:component.md', + localizedDestination: ':locale/components/:component.md', + localizedLegacyDestination: ':locale/core/components/:component.md', }, { source: 'packages/react-simplikit/src/utils/:util/:util.md', - destination: 'core/utils/:util.md', + destination: 'utils/:util.md', + legacyDestination: 'core/utils/:util.md', localizedSource: 'packages/react-simplikit/src/utils/:util/:locale/:util.md', - localizedDestination: ':locale/core/utils/:util.md', + localizedDestination: ':locale/utils/:util.md', + localizedLegacyDestination: ':locale/core/utils/:util.md', }, { source: 'packages/react-simplikit/src/mobile/hooks/:hook/:hook.md', - destination: 'mobile/hooks/:hook.md', + destination: 'hooks/:hook.md', + legacyDestination: 'mobile/hooks/:hook.md', localizedSource: 'packages/react-simplikit/src/mobile/hooks/:hook/:locale/:hook.md', - localizedDestination: ':locale/mobile/hooks/:hook.md', + localizedDestination: ':locale/hooks/:hook.md', + localizedLegacyDestination: ':locale/mobile/hooks/:hook.md', }, { source: 'packages/react-simplikit/src/mobile/utils/:util/:util.md', - destination: 'mobile/utils/:util.md', + destination: 'utils/:util.md', + legacyDestination: 'mobile/utils/:util.md', localizedSource: 'packages/react-simplikit/src/mobile/utils/:util/:locale/:util.md', - localizedDestination: ':locale/mobile/utils/:util.md', + localizedDestination: ':locale/utils/:util.md', + localizedLegacyDestination: ':locale/mobile/utils/:util.md', }, ]; @@ -161,6 +175,22 @@ export const rewrites = Object.fromEntries([ ...localizedRewriteEntries, ]); +/** + * Old-to-new URL pairs (still parameterized with :hook etc.) for every route that + * moved in the flattening. buildEnd expands them against the source tree and writes + * redirect stubs so pre-flattening links keep working on any static host. + */ +export const legacyRoutePatterns = routeDefinitions + .filter(route => route.legacyDestination !== undefined) + .flatMap(route => [ + { source: route.source, from: route.legacyDestination as string, to: route.destination }, + ...localeDirectories.map(locale => ({ + source: route.source, + from: (route.localizedLegacyDestination as string).replace(':locale', locale), + to: route.localizedDestination.replace(':locale', locale), + })), + ]); + export const generatedRewrites = Object.fromEntries( localizedRewriteEntries.map(([source, destination]) => [`${generatedLocalesDirectory}/${source}`, destination]) ); diff --git a/.vitepress/locales/en.mts b/.vitepress/locales/en.mts index 9be30db0..aa411a80 100644 --- a/.vitepress/locales/en.mts +++ b/.vitepress/locales/en.mts @@ -7,6 +7,7 @@ export const en: LocaleThemeStrings = { componentsLabel: 'Components', hooksLabel: 'Hooks', utilsLabel: 'Utils', + mobileWebLabel: 'Mobile Web', guidePages: { core: { intro: 'Introduction', diff --git a/.vitepress/locales/es.mts b/.vitepress/locales/es.mts index dc58acc8..366794b5 100644 --- a/.vitepress/locales/es.mts +++ b/.vitepress/locales/es.mts @@ -7,6 +7,7 @@ export const es: LocaleThemeStrings = { componentsLabel: 'Componentes', hooksLabel: 'Hooks', utilsLabel: 'Utilidades', + mobileWebLabel: 'Web móvil', guidePages: { core: { intro: 'Introducción', diff --git a/.vitepress/locales/ja.mts b/.vitepress/locales/ja.mts index e590d4e1..318f2f87 100644 --- a/.vitepress/locales/ja.mts +++ b/.vitepress/locales/ja.mts @@ -7,6 +7,7 @@ export const ja: LocaleThemeStrings = { componentsLabel: 'コンポーネント', hooksLabel: 'フック', utilsLabel: 'ユーティリティ', + mobileWebLabel: 'モバイル Web', guidePages: { core: { intro: '紹介', diff --git a/.vitepress/locales/ko.mts b/.vitepress/locales/ko.mts index 0edcd600..5df7b2e6 100644 --- a/.vitepress/locales/ko.mts +++ b/.vitepress/locales/ko.mts @@ -7,6 +7,7 @@ export const ko: LocaleThemeStrings = { componentsLabel: '컴포넌트', hooksLabel: '훅', utilsLabel: '유틸리티', + mobileWebLabel: '모바일 웹', guidePages: { core: { intro: '소개', diff --git a/.vitepress/locales/zh-Hans.mts b/.vitepress/locales/zh-Hans.mts index 66e9360d..0d2aa007 100644 --- a/.vitepress/locales/zh-Hans.mts +++ b/.vitepress/locales/zh-Hans.mts @@ -7,6 +7,7 @@ export const zhHans: LocaleThemeStrings = { componentsLabel: '组件', hooksLabel: 'Hooks', utilsLabel: '工具函数', + mobileWebLabel: '移动端 Web', guidePages: { core: { intro: '简介', diff --git a/docs/es/mobile/intro.md b/docs/es/mobile/intro.md index 0ed24d07..3e718360 100644 --- a/docs/es/mobile/intro.md +++ b/docs/es/mobile/intro.md @@ -112,12 +112,12 @@ function FixedBottomCTA() { ## Hooks disponibles -| Hook | Descripción | -| --------------------------------------------------------- | ----------------------------------------------------------------- | -| [useAvoidKeyboard](/es/mobile/hooks/useAvoidKeyboard) | Mueve los elementos fijos por encima del teclado en pantalla | -| [useKeyboardHeight](/es/mobile/hooks/useKeyboardHeight) | Devuelve la altura actual del teclado | -| [useBodyScrollLock](/es/mobile/hooks/useBodyScrollLock) | Bloquea el desplazamiento del body para modales y superposiciones | -| [useScrollDirection](/es/mobile/hooks/useScrollDirection) | Detecta la dirección del desplazamiento (arriba/abajo) | -| [useNetworkStatus](/es/mobile/hooks/useNetworkStatus) | Supervisa el estado de la conexión de red | -| [usePageVisibility](/es/mobile/hooks/usePageVisibility) | Sigue el estado de visibilidad de la página | -| [useVisualViewport](/es/mobile/hooks/useVisualViewport) | Proporciona las dimensiones y la posición del viewport visual | +| Hook | Descripción | +| -------------------------------------------------- | ----------------------------------------------------------------- | +| [useAvoidKeyboard](/es/hooks/useAvoidKeyboard) | Mueve los elementos fijos por encima del teclado en pantalla | +| [useKeyboardHeight](/es/hooks/useKeyboardHeight) | Devuelve la altura actual del teclado | +| [useBodyScrollLock](/es/hooks/useBodyScrollLock) | Bloquea el desplazamiento del body para modales y superposiciones | +| [useScrollDirection](/es/hooks/useScrollDirection) | Detecta la dirección del desplazamiento (arriba/abajo) | +| [useNetworkStatus](/es/hooks/useNetworkStatus) | Supervisa el estado de la conexión de red | +| [usePageVisibility](/es/hooks/usePageVisibility) | Sigue el estado de visibilidad de la página | +| [useVisualViewport](/es/hooks/useVisualViewport) | Proporciona las dimensiones y la posición del viewport visual | diff --git a/docs/ja/mobile/intro.md b/docs/ja/mobile/intro.md index b07d3df8..aa4194c5 100644 --- a/docs/ja/mobile/intro.md +++ b/docs/ja/mobile/intro.md @@ -112,12 +112,12 @@ function FixedBottomCTA() { ## 利用可能なフック -| フック | 説明 | -| --------------------------------------------------------- | -------------------------------------------------------------- | -| [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) | ビジュアルビューポートのサイズとオフセットを提供します | +| フック | 説明 | +| -------------------------------------------------- | -------------------------------------------------------------- | +| [useAvoidKeyboard](/ja/hooks/useAvoidKeyboard) | 固定要素をオンスクリーンキーボードの上に移動させます | +| [useKeyboardHeight](/ja/hooks/useKeyboardHeight) | 現在のキーボードの高さを返します | +| [useBodyScrollLock](/ja/hooks/useBodyScrollLock) | モーダルやオーバーレイのために body のスクロールをロックします | +| [useScrollDirection](/ja/hooks/useScrollDirection) | スクロール方向(上/下)を検知します | +| [useNetworkStatus](/ja/hooks/useNetworkStatus) | ネットワーク接続状態を監視します | +| [usePageVisibility](/ja/hooks/usePageVisibility) | ページの可視性の状態を追跡します | +| [useVisualViewport](/ja/hooks/useVisualViewport) | ビジュアルビューポートのサイズとオフセットを提供します | diff --git a/docs/ko/mobile/intro.md b/docs/ko/mobile/intro.md index 6fe05e2c..6392bc21 100644 --- a/docs/ko/mobile/intro.md +++ b/docs/ko/mobile/intro.md @@ -112,12 +112,12 @@ function FixedBottomCTA() { ## 제공하는 훅 -| 훅 | 설명 | -| --------------------------------------------------------- | ------------------------------------------- | -| [useAvoidKeyboard](/ko/mobile/hooks/useAvoidKeyboard) | 고정 요소를 온스크린 키보드 위로 이동시켜요 | -| [useKeyboardHeight](/ko/mobile/hooks/useKeyboardHeight) | 현재 키보드 높이를 반환해요 | -| [useBodyScrollLock](/ko/mobile/hooks/useBodyScrollLock) | 모달과 오버레이를 위해 body 스크롤을 잠가요 | -| [useScrollDirection](/ko/mobile/hooks/useScrollDirection) | 스크롤 방향(위/아래)을 감지해요 | -| [useNetworkStatus](/ko/mobile/hooks/useNetworkStatus) | 네트워크 연결 상태를 모니터링해요 | -| [usePageVisibility](/ko/mobile/hooks/usePageVisibility) | 페이지 가시성 상태를 추적해요 | -| [useVisualViewport](/ko/mobile/hooks/useVisualViewport) | Visual Viewport 크기와 오프셋을 제공해요 | +| 훅 | 설명 | +| -------------------------------------------------- | ------------------------------------------- | +| [useAvoidKeyboard](/ko/hooks/useAvoidKeyboard) | 고정 요소를 온스크린 키보드 위로 이동시켜요 | +| [useKeyboardHeight](/ko/hooks/useKeyboardHeight) | 현재 키보드 높이를 반환해요 | +| [useBodyScrollLock](/ko/hooks/useBodyScrollLock) | 모달과 오버레이를 위해 body 스크롤을 잠가요 | +| [useScrollDirection](/ko/hooks/useScrollDirection) | 스크롤 방향(위/아래)을 감지해요 | +| [useNetworkStatus](/ko/hooks/useNetworkStatus) | 네트워크 연결 상태를 모니터링해요 | +| [usePageVisibility](/ko/hooks/usePageVisibility) | 페이지 가시성 상태를 추적해요 | +| [useVisualViewport](/ko/hooks/useVisualViewport) | Visual Viewport 크기와 오프셋을 제공해요 | diff --git a/docs/mobile/intro.md b/docs/mobile/intro.md index 22d5264c..204b72fe 100644 --- a/docs/mobile/intro.md +++ b/docs/mobile/intro.md @@ -112,12 +112,12 @@ function FixedBottomCTA() { ## Available Hooks -| Hook | Description | -| ------------------------------------------------------ | ------------------------------------------------- | -| [useAvoidKeyboard](/mobile/hooks/useAvoidKeyboard) | Moves fixed elements above the on-screen keyboard | -| [useKeyboardHeight](/mobile/hooks/useKeyboardHeight) | Returns the current keyboard height | -| [useBodyScrollLock](/mobile/hooks/useBodyScrollLock) | Locks body scroll for modals and overlays | -| [useScrollDirection](/mobile/hooks/useScrollDirection) | Detects scroll direction (up/down) | -| [useNetworkStatus](/mobile/hooks/useNetworkStatus) | Monitors network connection status | -| [usePageVisibility](/mobile/hooks/usePageVisibility) | Tracks page visibility state | -| [useVisualViewport](/mobile/hooks/useVisualViewport) | Provides visual viewport dimensions and offset | +| Hook | Description | +| ----------------------------------------------- | ------------------------------------------------- | +| [useAvoidKeyboard](/hooks/useAvoidKeyboard) | Moves fixed elements above the on-screen keyboard | +| [useKeyboardHeight](/hooks/useKeyboardHeight) | Returns the current keyboard height | +| [useBodyScrollLock](/hooks/useBodyScrollLock) | Locks body scroll for modals and overlays | +| [useScrollDirection](/hooks/useScrollDirection) | Detects scroll direction (up/down) | +| [useNetworkStatus](/hooks/useNetworkStatus) | Monitors network connection status | +| [usePageVisibility](/hooks/usePageVisibility) | Tracks page visibility state | +| [useVisualViewport](/hooks/useVisualViewport) | Provides visual viewport dimensions and offset | diff --git a/docs/zh-Hans/mobile/intro.md b/docs/zh-Hans/mobile/intro.md index 39871e16..451ba32c 100644 --- a/docs/zh-Hans/mobile/intro.md +++ b/docs/zh-Hans/mobile/intro.md @@ -112,12 +112,12 @@ function FixedBottomCTA() { ## 可用的 Hook -| Hook | 说明 | -| -------------------------------------------------------------- | ---------------------------- | -| [useAvoidKeyboard](/zh-Hans/mobile/hooks/useAvoidKeyboard) | 把固定元素移动到软键盘上方 | -| [useKeyboardHeight](/zh-Hans/mobile/hooks/useKeyboardHeight) | 返回当前的键盘高度 | -| [useBodyScrollLock](/zh-Hans/mobile/hooks/useBodyScrollLock) | 为模态框和浮层锁定 body 滚动 | -| [useScrollDirection](/zh-Hans/mobile/hooks/useScrollDirection) | 检测滚动方向(向上/向下) | -| [useNetworkStatus](/zh-Hans/mobile/hooks/useNetworkStatus) | 监控网络连接状态 | -| [usePageVisibility](/zh-Hans/mobile/hooks/usePageVisibility) | 跟踪页面可见性状态 | -| [useVisualViewport](/zh-Hans/mobile/hooks/useVisualViewport) | 提供视觉视口的尺寸和偏移量 | +| Hook | 说明 | +| ------------------------------------------------------- | ---------------------------- | +| [useAvoidKeyboard](/zh-Hans/hooks/useAvoidKeyboard) | 把固定元素移动到软键盘上方 | +| [useKeyboardHeight](/zh-Hans/hooks/useKeyboardHeight) | 返回当前的键盘高度 | +| [useBodyScrollLock](/zh-Hans/hooks/useBodyScrollLock) | 为模态框和浮层锁定 body 滚动 | +| [useScrollDirection](/zh-Hans/hooks/useScrollDirection) | 检测滚动方向(向上/向下) | +| [useNetworkStatus](/zh-Hans/hooks/useNetworkStatus) | 监控网络连接状态 | +| [usePageVisibility](/zh-Hans/hooks/usePageVisibility) | 跟踪页面可见性状态 | +| [useVisualViewport](/zh-Hans/hooks/useVisualViewport) | 提供视觉视口的尺寸和偏移量 | From eb63c7a8fa3839d6ad83efc9e1472cb987a01c58 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:19:54 +0900 Subject: [PATCH 2/7] feat(docs): merge the split guides into one root-level set --- .../prepareLocalizedFallbacks/index.spec.ts | 4 +- .scripts/utils/assertLlmsOutput.ts | 2 +- .scripts/verifyDocsI18n.ts | 65 +++-- .vitepress/libs/buildLocaleConfig.mts | 20 +- .vitepress/libs/legacyRedirects.mts | 25 +- .vitepress/locales.mts | 35 +-- .vitepress/locales/en.mts | 21 +- .vitepress/locales/es.mts | 21 +- .vitepress/locales/ja.mts | 21 +- .vitepress/locales/ko.mts | 21 +- .vitepress/locales/zh-Hans.mts | 21 +- docs/{core => }/ai-integration.md | 0 docs/{core => }/contributing.md | 0 docs/core/installation.md | 27 --- docs/{core => }/design-principles.md | 0 docs/es/{core => }/contributing.md | 0 docs/es/core/installation.md | 27 --- docs/es/{core => }/design-principles.md | 0 docs/es/index.md | 7 +- docs/es/{mobile => }/installation.md | 7 +- docs/es/{core => }/intro.md | 0 docs/es/mobile-web.md | 224 ++++++++++++++++++ docs/es/mobile/contributing.md | 141 ----------- docs/es/mobile/design-principles.md | 90 ------- docs/es/mobile/intro.md | 123 ---------- docs/es/mobile/roadmap.md | 41 ---- .../{core => }/why-react-simplikit-matters.md | 0 docs/index.md | 5 +- docs/{mobile => }/installation.md | 7 +- docs/{core => }/intro.md | 0 docs/ja/{core => }/contributing.md | 0 docs/ja/core/installation.md | 27 --- docs/ja/{core => }/design-principles.md | 0 docs/ja/index.md | 5 +- docs/ja/{mobile => }/installation.md | 9 +- docs/ja/{core => }/intro.md | 0 docs/ja/mobile-web.md | 224 ++++++++++++++++++ docs/ja/mobile/contributing.md | 141 ----------- docs/ja/mobile/design-principles.md | 90 ------- docs/ja/mobile/intro.md | 123 ---------- docs/ja/mobile/roadmap.md | 41 ---- .../{core => }/why-react-simplikit-matters.md | 0 docs/ko/{core => }/ai-integration.md | 0 docs/ko/{core => }/contributing.md | 0 docs/ko/core/installation.md | 27 --- docs/ko/{core => }/design-principles.md | 0 docs/ko/index.md | 5 +- docs/ko/{mobile => }/installation.md | 9 +- docs/ko/{core => }/intro.md | 0 docs/ko/mobile-web.md | 224 ++++++++++++++++++ docs/ko/mobile/contributing.md | 136 ----------- docs/ko/mobile/design-principles.md | 90 ------- docs/ko/mobile/intro.md | 123 ---------- docs/ko/mobile/roadmap.md | 41 ---- .../{core => }/why-react-simplikit-matters.md | 0 docs/mobile-web.md | 224 ++++++++++++++++++ docs/mobile/contributing.md | 141 ----------- docs/mobile/design-principles.md | 90 ------- docs/mobile/intro.md | 123 ---------- docs/mobile/roadmap.md | 41 ---- .../{core => }/why-react-simplikit-matters.md | 0 docs/zh-Hans/{core => }/contributing.md | 0 docs/zh-Hans/core/installation.md | 27 --- docs/zh-Hans/{core => }/design-principles.md | 0 docs/zh-Hans/index.md | 7 +- docs/zh-Hans/{mobile => }/installation.md | 7 +- docs/zh-Hans/{core => }/intro.md | 0 docs/zh-Hans/mobile-web.md | 224 ++++++++++++++++++ docs/zh-Hans/mobile/contributing.md | 141 ----------- docs/zh-Hans/mobile/design-principles.md | 90 ------- docs/zh-Hans/mobile/intro.md | 123 ---------- docs/zh-Hans/mobile/roadmap.md | 41 ---- .../{core => }/why-react-simplikit-matters.md | 0 73 files changed, 1238 insertions(+), 2311 deletions(-) rename docs/{core => }/ai-integration.md (100%) rename docs/{core => }/contributing.md (100%) delete mode 100644 docs/core/installation.md rename docs/{core => }/design-principles.md (100%) rename docs/es/{core => }/contributing.md (100%) delete mode 100644 docs/es/core/installation.md rename docs/es/{core => }/design-principles.md (100%) rename docs/es/{mobile => }/installation.md (80%) rename docs/es/{core => }/intro.md (100%) create mode 100644 docs/es/mobile-web.md delete mode 100644 docs/es/mobile/contributing.md delete mode 100644 docs/es/mobile/design-principles.md delete mode 100644 docs/es/mobile/intro.md delete mode 100644 docs/es/mobile/roadmap.md rename docs/es/{core => }/why-react-simplikit-matters.md (100%) rename docs/{mobile => }/installation.md (80%) rename docs/{core => }/intro.md (100%) rename docs/ja/{core => }/contributing.md (100%) delete mode 100644 docs/ja/core/installation.md rename docs/ja/{core => }/design-principles.md (100%) rename docs/ja/{mobile => }/installation.md (70%) rename docs/ja/{core => }/intro.md (100%) create mode 100644 docs/ja/mobile-web.md delete mode 100644 docs/ja/mobile/contributing.md delete mode 100644 docs/ja/mobile/design-principles.md delete mode 100644 docs/ja/mobile/intro.md delete mode 100644 docs/ja/mobile/roadmap.md rename docs/ja/{core => }/why-react-simplikit-matters.md (100%) rename docs/ko/{core => }/ai-integration.md (100%) rename docs/ko/{core => }/contributing.md (100%) delete mode 100644 docs/ko/core/installation.md rename docs/ko/{core => }/design-principles.md (100%) rename docs/ko/{mobile => }/installation.md (72%) rename docs/ko/{core => }/intro.md (100%) create mode 100644 docs/ko/mobile-web.md delete mode 100644 docs/ko/mobile/contributing.md delete mode 100644 docs/ko/mobile/design-principles.md delete mode 100644 docs/ko/mobile/intro.md delete mode 100644 docs/ko/mobile/roadmap.md rename docs/ko/{core => }/why-react-simplikit-matters.md (100%) create mode 100644 docs/mobile-web.md delete mode 100644 docs/mobile/contributing.md delete mode 100644 docs/mobile/design-principles.md delete mode 100644 docs/mobile/intro.md delete mode 100644 docs/mobile/roadmap.md rename docs/{core => }/why-react-simplikit-matters.md (100%) rename docs/zh-Hans/{core => }/contributing.md (100%) delete mode 100644 docs/zh-Hans/core/installation.md rename docs/zh-Hans/{core => }/design-principles.md (100%) rename docs/zh-Hans/{mobile => }/installation.md (79%) rename docs/zh-Hans/{core => }/intro.md (100%) create mode 100644 docs/zh-Hans/mobile-web.md delete mode 100644 docs/zh-Hans/mobile/contributing.md delete mode 100644 docs/zh-Hans/mobile/design-principles.md delete mode 100644 docs/zh-Hans/mobile/intro.md delete mode 100644 docs/zh-Hans/mobile/roadmap.md rename docs/zh-Hans/{core => }/why-react-simplikit-matters.md (100%) diff --git a/.scripts/commands/prepareLocalizedFallbacks/index.spec.ts b/.scripts/commands/prepareLocalizedFallbacks/index.spec.ts index 74fd45ef..b0fbf51d 100644 --- a/.scripts/commands/prepareLocalizedFallbacks/index.spec.ts +++ b/.scripts/commands/prepareLocalizedFallbacks/index.spec.ts @@ -15,13 +15,13 @@ afterEach(async () => { describe('prepareLocalizedFallbacks', () => { it('creates a marked English fallback when a localized document is missing', async () => { const root = await createFixtureDirectory(); - await writeFile(root, 'docs/core/intro.md', '# Introduction\n'); + await writeFile(root, 'docs/intro.md', '# Introduction\n'); await prepareLocalizedFallbacks({ localeDirectories: ['ja'], root }); await expectFile( root, - 'generated-locales/docs/ja/core/intro.md', + 'generated-locales/docs/ja/intro.md', `---\nuntranslated: true\nsourceLocale: en\n---\n# Introduction\n` ); }); diff --git a/.scripts/utils/assertLlmsOutput.ts b/.scripts/utils/assertLlmsOutput.ts index 77308bc7..a8a5820b 100644 --- a/.scripts/utils/assertLlmsOutput.ts +++ b/.scripts/utils/assertLlmsOutput.ts @@ -14,7 +14,7 @@ const PACKAGE_INDEX_FILE = 'packages/react-simplikit/src/index.ts'; // The generated links are absolute (the plugin's `domain` option), and every documentation page // lives under core/ or mobile/. A ko/ or ja/ link means the localized copies leaked into the // listing, which would make an agent read the same page several times in different languages. -const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(?:hooks|components|utils|core|mobile)\/[^/]+\.md$/; +const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(?:(?:hooks|components|utils)\/)?[^/]+\.md$/; /** * Checks the llms outputs vitepress-plugin-llms wrote into a docs build: diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index e2503d33..6d2336a3 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -70,7 +70,7 @@ try { } const guideFixtureTitle = 'Untranslated Fallback Fixture'; -const guideFixturePath = path.join(root, 'docs/core/untranslated-fallback-fixture.md'); +const guideFixturePath = path.join(root, 'docs/untranslated-fallback-fixture.md'); const hookFixtureName = 'useUntranslatedFallbackFixture'; const hookFixtureDirectory = path.join(root, 'packages/react-simplikit/src/hooks', hookFixtureName); const buildOutputDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'react-simplikit-docs-')); @@ -88,7 +88,7 @@ try { await assertLlmsOutput({ buildOutputDirectory, root }); const fallbackPage = await fs.readFile( - path.join(buildOutputDirectory, 'ko/core/untranslated-fallback-fixture.html'), + path.join(buildOutputDirectory, 'ko/untranslated-fallback-fixture.html'), 'utf8' ); @@ -103,7 +103,7 @@ try { 'the fallback route must show the untranslated banner' ); - const translatedPage = await fs.readFile(path.join(buildOutputDirectory, 'ko/core/intro.html'), 'utf8'); + const translatedPage = await fs.readFile(path.join(buildOutputDirectory, 'ko/intro.html'), 'utf8'); assert.equal( translatedPage.includes(localeDefinitions.ko.untranslatedNotice), @@ -140,21 +140,12 @@ const unregisteredLocaleFixture = { utilsLabel: 'Utilitários', mobileWebLabel: 'Web móvel', guidePages: { - core: { - intro: 'Introdução', - whyReactSimplikitMatters: 'Por que o react-simplikit importa', - installation: 'Instalação', - aiIntegration: 'Integração com IA', - designPrinciples: 'Princípios de design', - contributing: 'Contribuir', - }, - mobile: { - intro: 'Introdução', - roadmap: 'Roteiro', - installation: 'Instalação', - designPrinciples: 'Princípios de design', - contributing: 'Contribuir', - }, + intro: 'Introdução', + whyReactSimplikitMatters: 'Por que o react-simplikit importa', + installation: 'Instalação', + aiIntegration: 'Integração com IA', + designPrinciples: 'Princípios de design', + contributing: 'Contribuir', }, editLinkText: 'Editar esta página no GitHub', footerMessage: 'Distribuído sob a licença MIT.', @@ -166,11 +157,11 @@ const unregisteredConfig = buildLocaleConfig(unregisteredLocaleFixture); assert.equal(unregisteredConfig.lang, 'pt-BR'); const unregisteredConfigNav = unregisteredConfig.themeConfig?.nav ?? []; assert.deepEqual(unregisteredConfigNav[0], { text: 'Início', link: '/pt-BR/' }); -assert.deepEqual(unregisteredConfigNav[1], { text: 'Guide', link: '/pt-BR/core/intro' }); +assert.deepEqual(unregisteredConfigNav[1], { text: 'Guide', link: '/pt-BR/intro' }); assert.equal((unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Referência'); assert.equal( (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/pt-BR/hooks/') || - (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/pt-BR/core/intro', + (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/pt-BR/intro', true, 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' ); @@ -182,24 +173,24 @@ const rootConfig = buildLocaleConfig(localeDefinitions.root); const koConfigNav = koConfig.themeConfig?.nav ?? []; assert.deepEqual(koConfigNav[0], { text: '홈', link: '/ko/' }); -assert.deepEqual(koConfigNav[1], { text: 'Guide', link: '/ko/core/intro' }); +assert.deepEqual(koConfigNav[1], { text: 'Guide', link: '/ko/intro' }); assert.equal((koConfigNav[2] as DefaultTheme.NavItemWithLink).text, '레퍼런스'); assert.equal( (koConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/ko/hooks/') || - (koConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ko/core/intro', + (koConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ko/intro', true, 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' ); assert.deepEqual((koConfig.themeConfig?.sidebar as Record)['/ko/'][0], { text: '가이드', items: [ - { text: '소개', link: '/ko/core/intro' }, - { text: 'react-simplikit, 선택의 이유', link: '/ko/core/why-react-simplikit-matters' }, - { text: '설치하기', link: '/ko/core/installation' }, - { text: 'AI 연동', link: '/ko/core/ai-integration' }, - { text: '설계 원칙', link: '/ko/core/design-principles' }, - { text: '모바일 웹', link: '/ko/mobile/intro' }, - { text: '기여하기', link: '/ko/core/contributing' }, + { text: '소개', link: '/ko/intro' }, + { text: 'react-simplikit, 선택의 이유', link: '/ko/why-react-simplikit-matters' }, + { text: '설치하기', link: '/ko/installation' }, + { text: 'AI 연동', link: '/ko/ai-integration' }, + { text: '설계 원칙', link: '/ko/design-principles' }, + { text: '모바일 웹', link: '/ko/mobile-web' }, + { text: '기여하기', link: '/ko/contributing' }, ], }); assert.equal(koConfig.themeConfig?.editLink?.text, 'GitHub에서 수정하기'); @@ -207,11 +198,11 @@ assert.equal(koConfig.themeConfig?.footer?.message, 'MIT 라이선스에 따라 const rootConfigNav = rootConfig.themeConfig?.nav ?? []; assert.deepEqual(rootConfigNav[0], { text: 'Home', link: '/' }); -assert.deepEqual(rootConfigNav[1], { text: 'Guide', link: '/core/intro' }); +assert.deepEqual(rootConfigNav[1], { text: 'Guide', link: '/intro' }); assert.equal((rootConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Reference'); assert.equal( (rootConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/hooks/') || - (rootConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/core/intro', + (rootConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/intro', true, 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' ); @@ -223,11 +214,11 @@ const jaConfig = buildLocaleConfig(localeDefinitions.ja); assert.equal(jaConfig.lang, 'ja'); const jaConfigNav = jaConfig.themeConfig?.nav ?? []; assert.deepEqual(jaConfigNav[0], { text: 'ホーム', link: '/ja/' }); -assert.deepEqual(jaConfigNav[1], { text: 'Guide', link: '/ja/core/intro' }); +assert.deepEqual(jaConfigNav[1], { text: 'Guide', link: '/ja/intro' }); assert.equal((jaConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'リファレンス'); assert.equal( (jaConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/ja/hooks/') || - (jaConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ja/core/intro', + (jaConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ja/intro', true, 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' ); @@ -243,11 +234,11 @@ const zhHansConfig = buildLocaleConfig(localeDefinitions['zh-Hans']); assert.equal(zhHansConfig.lang, 'zh-Hans'); const zhHansConfigNav = zhHansConfig.themeConfig?.nav ?? []; assert.deepEqual(zhHansConfigNav[0], { text: '首页', link: '/zh-Hans/' }); -assert.deepEqual(zhHansConfigNav[1], { text: 'Guide', link: '/zh-Hans/core/intro' }); +assert.deepEqual(zhHansConfigNav[1], { text: 'Guide', link: '/zh-Hans/intro' }); assert.equal((zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).text, '参考'); assert.equal( (zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/zh-Hans/hooks/') || - (zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/zh-Hans/core/intro', + (zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/zh-Hans/intro', true, 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' ); @@ -263,11 +254,11 @@ const esConfig = buildLocaleConfig(localeDefinitions.es); assert.equal(esConfig.lang, 'es'); const esConfigNav = esConfig.themeConfig?.nav ?? []; assert.deepEqual(esConfigNav[0], { text: 'Inicio', link: '/es/' }); -assert.deepEqual(esConfigNav[1], { text: 'Guide', link: '/es/core/intro' }); +assert.deepEqual(esConfigNav[1], { text: 'Guide', link: '/es/intro' }); assert.equal((esConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Referencia'); assert.equal( (esConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/es/hooks/') || - (esConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/es/core/intro', + (esConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/es/intro', true, 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' ); diff --git a/.vitepress/libs/buildLocaleConfig.mts b/.vitepress/libs/buildLocaleConfig.mts index 4ec0afc1..4220fc96 100644 --- a/.vitepress/libs/buildLocaleConfig.mts +++ b/.vitepress/libs/buildLocaleConfig.mts @@ -30,24 +30,24 @@ export function buildLocaleConfig( themeConfig: { nav: [ { text: strings.homeNavLabel, link: `${prefix}/` }, - { text: 'Guide', link: `${prefix}/core/intro` }, - { text: strings.referenceLabel, link: hooks[0]?.link ?? `${prefix}/core/intro` }, + { text: 'Guide', link: `${prefix}/intro` }, + { text: strings.referenceLabel, link: hooks[0]?.link ?? `${prefix}/intro` }, ], sidebar: { [`${prefix}/`]: [ { text: strings.guideLabel, items: [ - { text: strings.guidePages.core.intro, link: `${prefix}/core/intro` }, + { text: strings.guidePages.intro, link: `${prefix}/intro` }, { - text: strings.guidePages.core.whyReactSimplikitMatters, - link: `${prefix}/core/why-react-simplikit-matters`, + text: strings.guidePages.whyReactSimplikitMatters, + link: `${prefix}/why-react-simplikit-matters`, }, - { text: strings.guidePages.core.installation, link: `${prefix}/core/installation` }, - { text: strings.guidePages.core.aiIntegration, link: `${prefix}/core/ai-integration` }, - { text: strings.guidePages.core.designPrinciples, link: `${prefix}/core/design-principles` }, - { text: strings.mobileWebLabel, link: `${prefix}/mobile/intro` }, - { text: strings.guidePages.core.contributing, link: `${prefix}/core/contributing` }, + { text: strings.guidePages.installation, link: `${prefix}/installation` }, + { text: strings.guidePages.aiIntegration, link: `${prefix}/ai-integration` }, + { text: strings.guidePages.designPrinciples, link: `${prefix}/design-principles` }, + { text: strings.mobileWebLabel, link: `${prefix}/mobile-web` }, + { text: strings.guidePages.contributing, link: `${prefix}/contributing` }, ], }, { diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts index 20adc77b..1c6a687a 100644 --- a/.vitepress/libs/legacyRedirects.mts +++ b/.vitepress/libs/legacyRedirects.mts @@ -1,20 +1,41 @@ import fs from 'node:fs'; import path from 'node:path'; -import { legacyRoutePatterns } from '../locales.mts'; +import { legacyRoutePatterns, localeDirectories } from '../locales.mts'; import { projectRoot } from '../shared.mts'; const SITE_ORIGIN = 'https://react-simplikit.slash.page'; type RedirectPair = { from: string; to: string }; +/** + * Guide pages moved with per-page targets (the merge folded eleven pages into + * seven), so they are listed explicitly instead of derived from a pattern. + */ +const GUIDE_LEGACY: RedirectPair[] = [ + { from: 'core/intro.html', to: 'intro.html' }, + { from: 'core/why-react-simplikit-matters.html', to: 'why-react-simplikit-matters.html' }, + { from: 'core/installation.html', to: 'installation.html' }, + { from: 'core/ai-integration.html', to: 'ai-integration.html' }, + { from: 'core/design-principles.html', to: 'design-principles.html' }, + { from: 'core/contributing.html', to: 'contributing.html' }, + { from: 'mobile/intro.html', to: 'mobile-web.html' }, + { from: 'mobile/roadmap.html', to: 'mobile-web.html' }, + { from: 'mobile/installation.html', to: 'installation.html' }, + { from: 'mobile/design-principles.html', to: 'design-principles.html' }, + { from: 'mobile/contributing.html', to: 'contributing.html' }, +]; + /** * Expands the parameterized legacy routes against the actual source tree. * A pattern like `packages/react-simplikit/src/hooks/:hook/:hook.md` with the * legacy destination `core/hooks/:hook.md` yields one pair per hook directory. */ export function collectLegacyRedirects(): RedirectPair[] { - const pairs: RedirectPair[] = []; + const pairs: RedirectPair[] = GUIDE_LEGACY.flatMap(pair => [ + pair, + ...localeDirectories.map(locale => ({ from: `${locale}/${pair.from}`, to: `${locale}/${pair.to}` })), + ]); for (const route of legacyRoutePatterns) { const parameter = route.from.match(/:([A-Za-z]+)\.md$/)?.[1]; diff --git a/.vitepress/locales.mts b/.vitepress/locales.mts index 51f27711..92d8d57d 100644 --- a/.vitepress/locales.mts +++ b/.vitepress/locales.mts @@ -9,21 +9,12 @@ import { zhHans } from './locales/zh-Hans.mts'; export type LocaleCode = 'root' | 'ko' | 'ja' | 'zh-Hans' | 'es'; type GuidePageTitles = { - core: { - intro: string; - whyReactSimplikitMatters: string; - installation: string; - aiIntegration: string; - designPrinciples: string; - contributing: string; - }; - mobile: { - intro: string; - roadmap: string; - installation: string; - designPrinciples: string; - contributing: string; - }; + intro: string; + whyReactSimplikitMatters: string; + installation: string; + aiIntegration: string; + designPrinciples: string; + contributing: string; }; export type LocaleThemeStrings = { @@ -110,16 +101,10 @@ const routeDefinitions: RouteDefinition[] = [ localizedDestination: ':locale/index.md', }, { - source: 'docs/core/:doc.md', - destination: 'core/:doc.md', - localizedSource: 'docs/:locale/core/:doc.md', - localizedDestination: ':locale/core/:doc.md', - }, - { - source: 'docs/mobile/:doc.md', - destination: 'mobile/:doc.md', - localizedSource: 'docs/:locale/mobile/:doc.md', - localizedDestination: ':locale/mobile/:doc.md', + source: 'docs/:doc.md', + destination: ':doc.md', + localizedSource: 'docs/:locale/:doc.md', + localizedDestination: ':locale/:doc.md', }, { source: 'packages/react-simplikit/src/hooks/:hook/:hook.md', diff --git a/.vitepress/locales/en.mts b/.vitepress/locales/en.mts index aa411a80..d7f23158 100644 --- a/.vitepress/locales/en.mts +++ b/.vitepress/locales/en.mts @@ -9,21 +9,12 @@ export const en: LocaleThemeStrings = { utilsLabel: 'Utils', mobileWebLabel: 'Mobile Web', guidePages: { - core: { - intro: 'Introduction', - whyReactSimplikitMatters: 'Why react-simplikit matters', - installation: 'Installation', - aiIntegration: 'AI Integration', - designPrinciples: 'Design Principles', - contributing: 'Contributing', - }, - mobile: { - intro: 'Introduction', - roadmap: 'Roadmap', - installation: 'Installation', - designPrinciples: 'Design Principles', - contributing: 'Contributing', - }, + intro: 'Introduction', + whyReactSimplikitMatters: 'Why react-simplikit matters', + installation: 'Installation', + aiIntegration: 'AI Integration', + designPrinciples: 'Design Principles', + contributing: 'Contributing', }, editLinkText: 'Edit this page on GitHub', footerMessage: 'Released under the MIT License.', diff --git a/.vitepress/locales/es.mts b/.vitepress/locales/es.mts index 366794b5..adade77b 100644 --- a/.vitepress/locales/es.mts +++ b/.vitepress/locales/es.mts @@ -9,21 +9,12 @@ export const es: LocaleThemeStrings = { utilsLabel: 'Utilidades', mobileWebLabel: 'Web móvil', guidePages: { - core: { - intro: 'Introducción', - whyReactSimplikitMatters: 'Por qué importa react-simplikit', - installation: 'Instalación', - aiIntegration: 'Integración con IA', - designPrinciples: 'Principios de diseño', - contributing: 'Contribuir', - }, - mobile: { - intro: 'Introducción', - roadmap: 'Hoja de ruta', - installation: 'Instalación', - designPrinciples: 'Principios de diseño', - contributing: 'Contribuir', - }, + intro: 'Introducción', + whyReactSimplikitMatters: 'Por qué importa react-simplikit', + installation: 'Instalación', + aiIntegration: 'Integración con IA', + designPrinciples: 'Principios de diseño', + contributing: 'Contribuir', }, editLinkText: 'Editar esta página en GitHub', footerMessage: 'Publicado bajo la licencia MIT.', diff --git a/.vitepress/locales/ja.mts b/.vitepress/locales/ja.mts index 318f2f87..9b799f29 100644 --- a/.vitepress/locales/ja.mts +++ b/.vitepress/locales/ja.mts @@ -9,21 +9,12 @@ export const ja: LocaleThemeStrings = { utilsLabel: 'ユーティリティ', mobileWebLabel: 'モバイル Web', guidePages: { - core: { - intro: '紹介', - whyReactSimplikitMatters: 'なぜ react-simplikit なのか', - installation: 'インストール', - aiIntegration: 'AI 連携', - designPrinciples: '設計原則', - contributing: '貢献ガイド', - }, - mobile: { - intro: '紹介', - roadmap: 'ロードマップ', - installation: 'インストール', - designPrinciples: '設計原則', - contributing: '貢献ガイド', - }, + intro: '紹介', + whyReactSimplikitMatters: 'なぜ react-simplikit なのか', + installation: 'インストール', + aiIntegration: 'AI 連携', + designPrinciples: '設計原則', + contributing: '貢献ガイド', }, editLinkText: 'GitHub で編集する', footerMessage: 'MIT ライセンスの下で配布されています。', diff --git a/.vitepress/locales/ko.mts b/.vitepress/locales/ko.mts index 5df7b2e6..f06fb32b 100644 --- a/.vitepress/locales/ko.mts +++ b/.vitepress/locales/ko.mts @@ -9,21 +9,12 @@ export const ko: LocaleThemeStrings = { utilsLabel: '유틸리티', mobileWebLabel: '모바일 웹', guidePages: { - core: { - intro: '소개', - whyReactSimplikitMatters: 'react-simplikit, 선택의 이유', - installation: '설치하기', - aiIntegration: 'AI 연동', - designPrinciples: '설계 원칙', - contributing: '기여하기', - }, - mobile: { - intro: '소개', - roadmap: '앞으로의 방향', - installation: '설치하기', - designPrinciples: '설계 원칙', - contributing: '기여하기', - }, + intro: '소개', + whyReactSimplikitMatters: 'react-simplikit, 선택의 이유', + installation: '설치하기', + aiIntegration: 'AI 연동', + designPrinciples: '설계 원칙', + contributing: '기여하기', }, editLinkText: 'GitHub에서 수정하기', footerMessage: 'MIT 라이선스에 따라 배포됩니다.', diff --git a/.vitepress/locales/zh-Hans.mts b/.vitepress/locales/zh-Hans.mts index 0d2aa007..cb8ccaa9 100644 --- a/.vitepress/locales/zh-Hans.mts +++ b/.vitepress/locales/zh-Hans.mts @@ -9,21 +9,12 @@ export const zhHans: LocaleThemeStrings = { utilsLabel: '工具函数', mobileWebLabel: '移动端 Web', guidePages: { - core: { - intro: '简介', - whyReactSimplikitMatters: '为什么选择 react-simplikit', - installation: '安装', - aiIntegration: 'AI 集成', - designPrinciples: '设计原则', - contributing: '贡献指南', - }, - mobile: { - intro: '简介', - roadmap: '路线图', - installation: '安装', - designPrinciples: '设计原则', - contributing: '贡献指南', - }, + intro: '简介', + whyReactSimplikitMatters: '为什么选择 react-simplikit', + installation: '安装', + aiIntegration: 'AI 集成', + designPrinciples: '设计原则', + contributing: '贡献指南', }, editLinkText: '在 GitHub 上编辑此页', footerMessage: '基于 MIT 许可证发布。', diff --git a/docs/core/ai-integration.md b/docs/ai-integration.md similarity index 100% rename from docs/core/ai-integration.md rename to docs/ai-integration.md diff --git a/docs/core/contributing.md b/docs/contributing.md similarity index 100% rename from docs/core/contributing.md rename to docs/contributing.md diff --git a/docs/core/installation.md b/docs/core/installation.md deleted file mode 100644 index 6bfdc3f2..00000000 --- a/docs/core/installation.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: How to install react-simplikit ---- - -# Installation - -You can install `react-simplikit` from [npm](https://npmjs.com/package/react-simplikit) using your favorite package manager. - -::: code-group - -```sh [npm] -npm install react-simplikit -``` - -```sh [pnpm] -pnpm add react-simplikit -``` - -```sh [yarn] -yarn add react-simplikit -``` - -```sh [bun] -bun add react-simplikit -``` - -::: diff --git a/docs/core/design-principles.md b/docs/design-principles.md similarity index 100% rename from docs/core/design-principles.md rename to docs/design-principles.md diff --git a/docs/es/core/contributing.md b/docs/es/contributing.md similarity index 100% rename from docs/es/core/contributing.md rename to docs/es/contributing.md diff --git a/docs/es/core/installation.md b/docs/es/core/installation.md deleted file mode 100644 index a0e43b36..00000000 --- a/docs/es/core/installation.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: Cómo instalar react-simplikit ---- - -# Instalación - -Puedes instalar `react-simplikit` desde [npm](https://npmjs.com/package/react-simplikit) con el gestor de paquetes que prefieras. - -::: code-group - -```sh [npm] -npm install react-simplikit -``` - -```sh [pnpm] -pnpm add react-simplikit -``` - -```sh [yarn] -yarn add react-simplikit -``` - -```sh [bun] -bun add react-simplikit -``` - -::: diff --git a/docs/es/core/design-principles.md b/docs/es/design-principles.md similarity index 100% rename from docs/es/core/design-principles.md rename to docs/es/design-principles.md diff --git a/docs/es/index.md b/docs/es/index.md index 8a3191e6..5ebfa448 100644 --- a/docs/es/index.md +++ b/docs/es/index.md @@ -9,11 +9,8 @@ hero: alt: react-simplikit actions: - theme: brand - text: Empezar - link: /es/core/intro - - theme: alt - text: Utilidades para móvil - link: /es/mobile/intro + text: Comenzar + link: /es/intro features: - title: 'Cero dependencias' diff --git a/docs/es/mobile/installation.md b/docs/es/installation.md similarity index 80% rename from docs/es/mobile/installation.md rename to docs/es/installation.md index e10e0d47..c550d711 100644 --- a/docs/es/mobile/installation.md +++ b/docs/es/installation.md @@ -1,5 +1,5 @@ --- -description: Cómo instalar react-simplikit para la web móvil +description: Cómo instalar react-simplikit --- # Instalación @@ -26,11 +26,6 @@ bun add react-simplikit ::: -## Requisitos - -- React 18 o superior -- TypeScript 4.7 o superior (recomendado) - ## Uso Importa los Hooks directamente desde el paquete: diff --git a/docs/es/core/intro.md b/docs/es/intro.md similarity index 100% rename from docs/es/core/intro.md rename to docs/es/intro.md diff --git a/docs/es/mobile-web.md b/docs/es/mobile-web.md new file mode 100644 index 00000000..1fae1859 --- /dev/null +++ b/docs/es/mobile-web.md @@ -0,0 +1,224 @@ +# Utilidades para móvil + +Una colección de Hooks de React que resuelven los retos de interfaz más habituales en entornos de web móvil. + +## ¿Por qué utilidades para móvil? + +El desarrollo web para móvil trae consigo retos propios que no existen en escritorio: + +- **Evitar el teclado**: los elementos fijados abajo quedan ocultos cuando aparece el teclado en pantalla +- **Detección de la dirección del desplazamiento**: cabeceras y barras de navegación que se muestran u ocultan según el desplazamiento +- **Supervisión del estado de la red**: adaptar la calidad del contenido a la velocidad de la conexión +- **Seguimiento de la visibilidad de la página**: pausar los videos o la analítica cuando la aplicación pasa a segundo plano +- **Cambios en el viewport visual**: gestionar el zoom, el teclado y el redimensionado del viewport en los navegadores móviles + +`react-simplikit` ofrece Hooks para móvil probados en producción que resuelven estos escenarios con una configuración mínima. + +## Inicio rápido + +```bash +npm install react-simplikit +``` + +### Ejemplo de botón CTA + +El patrón de interfaz móvil más habitual: un botón fijado abajo que se mueve por encima del teclado. + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### Ejemplo de campo de chat + +Una interfaz de chat con un campo de entrada que se mantiene por encima del teclado. + +```tsx +import { useState } from 'react'; +import { useAvoidKeyboard } from 'react-simplikit'; + +function ChatInput() { + const { style } = useAvoidKeyboard(); + const [message, setMessage] = useState(''); + + return ( +
+ setMessage(e.target.value)} + placeholder="Type a message..." + style={{ flex: 1 }} + /> + +
+ ); +} +``` + +### Con área segura + +En los dispositivos con indicador de inicio (como el iPhone), puedes añadir un margen para el área segura. + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); + + return ( +
+ +
+ ); +} +``` + +## Hooks disponibles + +| Hook | Descripción | +| -------------------------------------------------- | ----------------------------------------------------------------- | +| [useAvoidKeyboard](/es/hooks/useAvoidKeyboard) | Mueve los elementos fijos por encima del teclado en pantalla | +| [useKeyboardHeight](/es/hooks/useKeyboardHeight) | Devuelve la altura actual del teclado | +| [useBodyScrollLock](/es/hooks/useBodyScrollLock) | Bloquea el desplazamiento del body para modales y superposiciones | +| [useScrollDirection](/es/hooks/useScrollDirection) | Detecta la dirección del desplazamiento (arriba/abajo) | +| [useNetworkStatus](/es/hooks/useNetworkStatus) | Supervisa el estado de la conexión de red | +| [usePageVisibility](/es/hooks/usePageVisibility) | Sigue el estado de visibilidad de la página | +| [useVisualViewport](/es/hooks/useVisualViewport) | Proporciona las dimensiones y la posición del viewport visual | + +## Hoja de ruta + +Las pantallas de los móviles son pequeñas, y ese espacio reducido genera una cantidad sorprendente de retos de interfaz. Los elementos quedan ocultos tras el teclado en pantalla, las áreas seguras varían según el dispositivo y el viewport que el usuario ve de verdad suele diferir del que informa el navegador. No son casos límite: son la realidad diaria del desarrollo para móvil. + +### El problema: una interfaz poco fiable en las pantallas de los móviles + +En los dispositivos móviles, lo que los usuarios ven en su pantalla no siempre coincide con lo que esperan los desarrolladores. Estos son algunos escenarios habituales: + +- **El teclado tapa los campos de entrada**: cuando el usuario toca un campo de texto, el teclado en pantalla sube y puede ocultar por completo el campo o el botón de envío fijado abajo. +- **Inconsistencias en las áreas seguras**: los dispositivos con muescas, esquinas redondeadas o indicadores de inicio (como la barra inferior del iPhone) tienen zonas reservadas donde no debe colocarse contenido, pero esas zonas varían entre dispositivos y versiones del sistema operativo. +- **Confusión con el viewport**: el viewport de diseño del navegador y el área visible real (el viewport visual) pueden diferir bastante, sobre todo cuando el teclado está abierto o la página tiene zoom aplicado. Los elementos de posición fija pueden acabar en lugares inesperados. + +Estos problemas no son exclusivos de un sistema operativo ni de un dispositivo concreto. Ya sea iOS Safari, Android Chrome o cualquier otro navegador móvil, el reto de fondo es el mismo: **el área visible es impredecible y el CSS estándar por sí solo no puede tenerla en cuenta de forma fiable**. + +### Nuestro enfoque: centrarnos en el viewport visual + +Las utilidades para móvil de `react-simplikit` abordan estos problemas con un enfoque muy concreto. En lugar de sortear las rarezas de cada navegador con trucos frágiles, centramos el diseño en el **viewport visual**: el área de la pantalla que el usuario puede ver realmente en cada momento. + +Al apoyarnos en la [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API), ofrecemos Hooks que te permiten: + +- **Detectar la aparición del teclado y reaccionar a ella** para que los elementos fijados abajo se aparten con naturalidad. +- **Leer los márgenes del área segura** para tener en cuenta correctamente las muescas, los indicadores de inicio y otras zonas reservadas propias de cada dispositivo. +- **Seguir el área visible real** para que tus decisiones de maquetación se basen en lo que el usuario ve de verdad y no en lo que supone el motor de maquetación del navegador. + +El objetivo es sencillo: **dentro del viewport visual, la interfaz debe renderizarse de forma fiable y predecible**. + +### Multiplataforma y multidispositivo + +No queremos limitarnos a un sistema operativo ni a un modelo de dispositivo concreto. La web móvil es multiplataforma por naturaleza, y `react-simplikit` lo asume como punto de partida. + +Nuestros Hooks están diseñados para funcionar de forma consistente en: + +- **iOS y Android**: las dos plataformas móviles dominantes. +- **Distintos navegadores**: Safari, Chrome, Samsung Internet y más. +- **Distintos formatos de dispositivo**: desde teléfonos compactos hasta dispositivos de pantalla grande, con o sin muescas e indicadores de inicio. + +Cuando una API concreta no está disponible (por ejemplo, `window.visualViewport` en navegadores antiguos), ofrecemos alternativas seguras que degradan el comportamiento con elegancia sin romper tu interfaz. + +### Próximos pasos + +Seguimos ampliando el conjunto de Hooks para móvil disponibles en `react-simplikit`, siempre guiados por el mismo principio: **hacer que el desarrollo de interfaces móviles sea predecible y fiable, sea cual sea el dispositivo o el sistema operativo**. Si existe un problema habitual de interfaz en móvil, lo más probable es que estemos trabajando en una solución limpia y declarativa para él. + +## Principios específicos para móvil + +### Diseño consciente de la plataforma + +En nuestras implementaciones tenemos en cuenta las diferencias de comportamiento entre iOS y Android: + +- **Diferencias en la Visual Viewport API**: + - iOS: `offsetTop` se vuelve negativo cuando aparece el teclado + - Android: `offsetTop` suele mantenerse en 0 +- **Cálculo de la altura del teclado**: tratamiento específico por plataforma para obtener medidas precisas + +### La seguridad en SSR es lo primero + +Cada Hook incluye pruebas de SSR para garantizar un renderizado en el servidor seguro: + +```typescript +it('is safe on server side rendering', () => { + const result = renderHookSSR.serverOnly(() => useHook()); + expect(result.current).toBeDefined(); +}); +``` + +### Optimización del rendimiento + +Los entornos móviles exigen una atención especial al rendimiento: + +- **Throttling y debouncing de eventos**: optimiza los eventos frecuentes como el desplazamiento y el redimensionado +- **Detectores de eventos pasivos**: usa detectores pasivos cuando sea aplicable +- **Transiciones de React**: aprovecha `startTransition` para las actualizaciones no urgentes + +## Directrices específicas para móvil + +### Probar en dispositivos reales + +- Se recomienda probar en iOS Safari y Android Chrome +- El comportamiento de la Visual Viewport API debe verificarse en dispositivos reales + +### Diferencias entre plataformas + +Ten en cuenta estas diferencias entre plataformas al implementar: + +| Característica | iOS | Android | +| -------------------------- | -------------------------------------------- | --------------------------- | +| `visualViewport.offsetTop` | Se vuelve negativo cuando aparece el teclado | Suele mantenerse en 0 | +| Comportamiento del teclado | El viewport se desplaza hacia arriba | Redimensiona la maquetación | + +### Patrón de acceso a window/document + +Usa siempre el patrón seguro para SSR cuando accedas a las APIs del navegador: + +```typescript +// ✅ Patrón seguro para SSR +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; + +// Ahora es seguro usar window/document +window.visualViewport?.addEventListener('resize', handler); +``` diff --git a/docs/es/mobile/contributing.md b/docs/es/mobile/contributing.md deleted file mode 100644 index 8368110b..00000000 --- a/docs/es/mobile/contributing.md +++ /dev/null @@ -1,141 +0,0 @@ -# Contribuir a las utilidades para móvil - -Esta guía amplía la [guía de contribución de core](/es/core/contributing). - -## Alcance del paquete - -Las utilidades para móvil de `react-simplikit` se centran en **resolver los problemas que aparecen en entornos de web móvil**. - -Esto incluye: - -- Gestión del viewport (viewport visual, área segura) -- Manejo del teclado (evitar que el teclado oculte el contenido) -- Problemas de maquetación propios de iOS Safari y Android Chrome -- Comportamiento del desplazamiento en los navegadores móviles - -Este paquete **no** está pensado para todas las utilidades que dependen de las APIs del navegador. Un Hook que usa APIs del navegador pero resuelve un problema de escritorio o de propósito general (por ejemplo, atajos de teclado o coordenadas del ratón) no encaja aquí. - -## Flujo de trabajo de desarrollo - -``` -Generación de esqueletos → Implementación → Pruebas → Documentación → Revisión → Changeset → Fusión -``` - -### 1. Generación de esqueletos - -Crea la estructura básica de un Hook nuevo: - -```bash -yarn scaffold useNewHook --type h # Hook -``` - -### 2. Implementación - -Sigue los [principios de diseño](/es/mobile/design-principles): - -- Solo exportaciones con nombre -- Aprovecha al máximo la inferencia de TypeScript -- Aplica el patrón de seguridad para SSR - -```typescript -// ✅ Patrón seguro para SSR -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` - -### 3. Documentación - -Todas las funciones exportadas deben incluir JSDoc con 4 etiquetas obligatorias: - -```typescript -/** - * @description Resumen en una línea. (obligatorio) - * @param {Type} name - Descripción. (obligatorio si tiene parámetros) - * @returns {Type} Descripción. (obligatorio si tiene valor de retorno) - * @example - * const result = useHook(input); // (obligatorio) - */ -``` - -::: tip -**¿Necesito escribir documentación?** - -No, no hace falta que escribas documentación aparte. En su lugar, escribe comentarios JSDoc detallados y luego ejecuta `yarn docs:gen ` para generar la documentación en inglés a partir de ellos; incluye el resultado en el commit de tu PR. Las traducciones se mantienen por separado; hasta que exista una, la página se muestra en inglés con un aviso. -::: - -### 4. Pruebas - -Se exige un 100% de cobertura: - -```bash -yarn test:spec # Ejecuta una sola prueba -yarn test:coverage # Comprueba la cobertura -``` - -#### Pruebas de SSR (obligatorias) - -```typescript -it('is safe on server side rendering', () => { - const result = renderHookSSR.serverOnly(() => useHook()); - expect(result.current).toBeDefined(); -}); -``` - -#### Lista de comprobación de la cobertura - -- [ ] Todas las ramas if/else -- [ ] Todos los casos de switch -- [ ] Todos los returns anticipados -- [ ] Las funciones de limpieza (el return de useEffect) - -### 5. Crear un changeset - -Cuando tus cambios de código afecten al paquete, tienes que crear un changeset: - -```bash -yarn changeset -``` - -Elige el tipo de cambio: - -- `patch`: correcciones de errores o cambios menores -- `minor`: nuevas funcionalidades (se mantiene la compatibilidad con versiones anteriores) -- `major`: cambios incompatibles (se rompe la compatibilidad con versiones anteriores) - -::: tip -Ambos paquetes están actualmente en la etapa `0.0.x`. Durante esta fase, la mayoría de los cambios deberían usar `patch`. -Si no tienes claro qué tipo de versión corresponde, coméntalo con los mantenedores del proyecto. -::: - -## Directrices específicas para móvil - -### Probar en dispositivos reales - -- Se recomienda probar en iOS Safari y Android Chrome -- El comportamiento de la Visual Viewport API debe verificarse en dispositivos reales - -### Diferencias entre plataformas - -Ten en cuenta estas diferencias entre plataformas al implementar: - -| Característica | iOS | Android | -| -------------------------- | -------------------------------------------- | --------------------------- | -| `visualViewport.offsetTop` | Se vuelve negativo cuando aparece el teclado | Suele mantenerse en 0 | -| Comportamiento del teclado | El viewport se desplaza hacia arriba | Redimensiona la maquetación | - -### Patrón de acceso a window/document - -Usa siempre el patrón seguro para SSR cuando accedas a las APIs del navegador: - -```typescript -// ✅ Patrón seguro para SSR -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; - -// Ahora es seguro usar window/document -window.visualViewport?.addEventListener('resize', handler); -``` - -## Contribuciones a la documentación - -No hay condiciones especiales para contribuir a la documentación. Si encuentras información incorrecta o traducciones de baja calidad, o si tienes contenido que añadir, edita el texto con total libertad. Escribe la documentación de forma clara y concisa, desde el punto de vista de quien la lee. diff --git a/docs/es/mobile/design-principles.md b/docs/es/mobile/design-principles.md deleted file mode 100644 index 68d21097..00000000 --- a/docs/es/mobile/design-principles.md +++ /dev/null @@ -1,90 +0,0 @@ -# Principios de diseño - -Las utilidades para móvil siguen los principios fundamentales de `react-simplikit`, ampliados para los retos propios del móvil. - -## Principios fundamentales - -### Respetar el ciclo de vida de React sin interferir en él - -`react-simplikit` no incluye implementaciones que interfieran directamente en el ciclo de vida de React. -Por ejemplo, no ofrece Hooks como `useMount` o `useLifecycles`; en su lugar, prefiere enfoques que respetan y aprovechan el comportamiento predeterminado de React. - -### Ligero y rápido gracias a cero dependencias - -`react-simplikit` no tiene absolutamente ninguna dependencia. Al no depender de bibliotecas adicionales, minimiza el tamaño del bundle cuando lo integras en un proyecto y elimina la preocupación por una posible pérdida de rendimiento. - -### Fiabilidad garantizada con un 100% de cobertura de pruebas - -`react-simplikit` prueba a fondo cada función y cada rama. -Escribimos pruebas completas que cubren no solo la funcionalidad básica, sino también las consideraciones de los entornos SSR de cada implementación, y así evitamos los problemas causados por comportamientos inesperados. - -### Documentación completa para entenderla y usarla con facilidad - -`react-simplikit` ofrece documentación detallada para que puedas entender y aprovechar rápidamente cada funcionalidad. La documentación incluye: - -- **Comentarios JSDoc**: explicaciones detalladas del comportamiento, los parámetros y los valores de retorno de cada función. -- **Guías de uso**: instrucciones claras y fáciles de seguir para empezar de inmediato. -- **Ejemplos prácticos**: ejemplos que muestran cómo aprovechar las implementaciones en situaciones reales. - -### Seguridad de tipos con compatibilidad total con TypeScript - -`react-simplikit` está construido con TypeScript desde cero. Cada Hook y cada utilidad viene con: - -- **Definiciones de tipos estrictas**: todos los parámetros, valores de retorno y opciones están completamente tipados -- **Compatibilidad con IntelliSense**: obtén autocompletado y documentación integrada en tu IDE -- **Tipos genéricos**: APIs flexibles que preservan tu información de tipos -- **Sin tipos `any`**: evitamos las vías de escape que comprometen la seguridad de tipos - -## Estándares de diseño de la API - -### Valores de retorno de los Hooks - -Seguimos patrones consistentes para los valores de retorno de los Hooks: - -- **Objeto**: para el estado y los valores relacionados (por ejemplo, `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) -- **void**: para los Hooks que solo producen efectos secundarios (por ejemplo, `useBodyScrollLock(): void`) - -### Parámetros - -- Los parámetros obligatorios van primero y los opcionales al final -- Usa un objeto de opciones cuando haya 3 o más parámetros opcionales - -### Patrón de seguridad para SSR - -Todos los Hooks siguen el patrón seguro para SSR: - -```typescript -// ✅ Seguro para SSR: todos los Hooks siguen este patrón -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` - -## Principios específicos para móvil - -### Diseño consciente de la plataforma - -En nuestras implementaciones tenemos en cuenta las diferencias de comportamiento entre iOS y Android: - -- **Diferencias en la Visual Viewport API**: - - iOS: `offsetTop` se vuelve negativo cuando aparece el teclado - - Android: `offsetTop` suele mantenerse en 0 -- **Cálculo de la altura del teclado**: tratamiento específico por plataforma para obtener medidas precisas - -### La seguridad en SSR es lo primero - -Cada Hook incluye pruebas de SSR para garantizar un renderizado en el servidor seguro: - -```typescript -it('is safe on server side rendering', () => { - const result = renderHookSSR.serverOnly(() => useHook()); - expect(result.current).toBeDefined(); -}); -``` - -### Optimización del rendimiento - -Los entornos móviles exigen una atención especial al rendimiento: - -- **Throttling y debouncing de eventos**: optimiza los eventos frecuentes como el desplazamiento y el redimensionado -- **Detectores de eventos pasivos**: usa detectores pasivos cuando sea aplicable -- **Transiciones de React**: aprovecha `startTransition` para las actualizaciones no urgentes diff --git a/docs/es/mobile/intro.md b/docs/es/mobile/intro.md deleted file mode 100644 index 3e718360..00000000 --- a/docs/es/mobile/intro.md +++ /dev/null @@ -1,123 +0,0 @@ -# Utilidades para móvil - -Una colección de Hooks de React que resuelven los retos de interfaz más habituales en entornos de web móvil. - -## ¿Por qué utilidades para móvil? - -El desarrollo web para móvil trae consigo retos propios que no existen en escritorio: - -- **Evitar el teclado**: los elementos fijados abajo quedan ocultos cuando aparece el teclado en pantalla -- **Detección de la dirección del desplazamiento**: cabeceras y barras de navegación que se muestran u ocultan según el desplazamiento -- **Supervisión del estado de la red**: adaptar la calidad del contenido a la velocidad de la conexión -- **Seguimiento de la visibilidad de la página**: pausar los videos o la analítica cuando la aplicación pasa a segundo plano -- **Cambios en el viewport visual**: gestionar el zoom, el teclado y el redimensionado del viewport en los navegadores móviles - -`react-simplikit` ofrece Hooks para móvil probados en producción que resuelven estos escenarios con una configuración mínima. - -## Inicio rápido - -```bash -npm install react-simplikit -``` - -### Ejemplo de botón CTA - -El patrón de interfaz móvil más habitual: un botón fijado abajo que se mueve por encima del teclado. - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard(); - - return ( -
- -
- ); -} -``` - -### Ejemplo de campo de chat - -Una interfaz de chat con un campo de entrada que se mantiene por encima del teclado. - -```tsx -import { useState } from 'react'; -import { useAvoidKeyboard } from 'react-simplikit'; - -function ChatInput() { - const { style } = useAvoidKeyboard(); - const [message, setMessage] = useState(''); - - return ( -
- setMessage(e.target.value)} - placeholder="Type a message..." - style={{ flex: 1 }} - /> - -
- ); -} -``` - -### Con área segura - -En los dispositivos con indicador de inicio (como el iPhone), puedes añadir un margen para el área segura. - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); - - return ( -
- -
- ); -} -``` - -## Hooks disponibles - -| Hook | Descripción | -| -------------------------------------------------- | ----------------------------------------------------------------- | -| [useAvoidKeyboard](/es/hooks/useAvoidKeyboard) | Mueve los elementos fijos por encima del teclado en pantalla | -| [useKeyboardHeight](/es/hooks/useKeyboardHeight) | Devuelve la altura actual del teclado | -| [useBodyScrollLock](/es/hooks/useBodyScrollLock) | Bloquea el desplazamiento del body para modales y superposiciones | -| [useScrollDirection](/es/hooks/useScrollDirection) | Detecta la dirección del desplazamiento (arriba/abajo) | -| [useNetworkStatus](/es/hooks/useNetworkStatus) | Supervisa el estado de la conexión de red | -| [usePageVisibility](/es/hooks/usePageVisibility) | Sigue el estado de visibilidad de la página | -| [useVisualViewport](/es/hooks/useVisualViewport) | Proporciona las dimensiones y la posición del viewport visual | diff --git a/docs/es/mobile/roadmap.md b/docs/es/mobile/roadmap.md deleted file mode 100644 index 209a7dd3..00000000 --- a/docs/es/mobile/roadmap.md +++ /dev/null @@ -1,41 +0,0 @@ -# Hoja de ruta - -Las pantallas de los móviles son pequeñas, y ese espacio reducido genera una cantidad sorprendente de retos de interfaz. Los elementos quedan ocultos tras el teclado en pantalla, las áreas seguras varían según el dispositivo y el viewport que el usuario ve de verdad suele diferir del que informa el navegador. No son casos límite: son la realidad diaria del desarrollo para móvil. - -## El problema: una interfaz poco fiable en las pantallas de los móviles - -En los dispositivos móviles, lo que los usuarios ven en su pantalla no siempre coincide con lo que esperan los desarrolladores. Estos son algunos escenarios habituales: - -- **El teclado tapa los campos de entrada**: cuando el usuario toca un campo de texto, el teclado en pantalla sube y puede ocultar por completo el campo o el botón de envío fijado abajo. -- **Inconsistencias en las áreas seguras**: los dispositivos con muescas, esquinas redondeadas o indicadores de inicio (como la barra inferior del iPhone) tienen zonas reservadas donde no debe colocarse contenido, pero esas zonas varían entre dispositivos y versiones del sistema operativo. -- **Confusión con el viewport**: el viewport de diseño del navegador y el área visible real (el viewport visual) pueden diferir bastante, sobre todo cuando el teclado está abierto o la página tiene zoom aplicado. Los elementos de posición fija pueden acabar en lugares inesperados. - -Estos problemas no son exclusivos de un sistema operativo ni de un dispositivo concreto. Ya sea iOS Safari, Android Chrome o cualquier otro navegador móvil, el reto de fondo es el mismo: **el área visible es impredecible y el CSS estándar por sí solo no puede tenerla en cuenta de forma fiable**. - -## Nuestro enfoque: centrarnos en el viewport visual - -Las utilidades para móvil de `react-simplikit` abordan estos problemas con un enfoque muy concreto. En lugar de sortear las rarezas de cada navegador con trucos frágiles, centramos el diseño en el **viewport visual**: el área de la pantalla que el usuario puede ver realmente en cada momento. - -Al apoyarnos en la [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API), ofrecemos Hooks que te permiten: - -- **Detectar la aparición del teclado y reaccionar a ella** para que los elementos fijados abajo se aparten con naturalidad. -- **Leer los márgenes del área segura** para tener en cuenta correctamente las muescas, los indicadores de inicio y otras zonas reservadas propias de cada dispositivo. -- **Seguir el área visible real** para que tus decisiones de maquetación se basen en lo que el usuario ve de verdad y no en lo que supone el motor de maquetación del navegador. - -El objetivo es sencillo: **dentro del viewport visual, la interfaz debe renderizarse de forma fiable y predecible**. - -## Multiplataforma y multidispositivo - -No queremos limitarnos a un sistema operativo ni a un modelo de dispositivo concreto. La web móvil es multiplataforma por naturaleza, y `react-simplikit` lo asume como punto de partida. - -Nuestros Hooks están diseñados para funcionar de forma consistente en: - -- **iOS y Android**: las dos plataformas móviles dominantes. -- **Distintos navegadores**: Safari, Chrome, Samsung Internet y más. -- **Distintos formatos de dispositivo**: desde teléfonos compactos hasta dispositivos de pantalla grande, con o sin muescas e indicadores de inicio. - -Cuando una API concreta no está disponible (por ejemplo, `window.visualViewport` en navegadores antiguos), ofrecemos alternativas seguras que degradan el comportamiento con elegancia sin romper tu interfaz. - -## Próximos pasos - -Seguimos ampliando el conjunto de Hooks para móvil disponibles en `react-simplikit`, siempre guiados por el mismo principio: **hacer que el desarrollo de interfaces móviles sea predecible y fiable, sea cual sea el dispositivo o el sistema operativo**. Si existe un problema habitual de interfaz en móvil, lo más probable es que estemos trabajando en una solución limpia y declarativa para él. diff --git a/docs/es/core/why-react-simplikit-matters.md b/docs/es/why-react-simplikit-matters.md similarity index 100% rename from docs/es/core/why-react-simplikit-matters.md rename to docs/es/why-react-simplikit-matters.md diff --git a/docs/index.md b/docs/index.md index b96b2842..82eb3686 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,10 +10,7 @@ hero: actions: - theme: brand text: Get Started - link: /core/intro - - theme: alt - text: Mobile Utilities - link: /mobile/intro + link: /intro features: - title: 'Zero dependencies' diff --git a/docs/mobile/installation.md b/docs/installation.md similarity index 80% rename from docs/mobile/installation.md rename to docs/installation.md index aa51ab57..de178121 100644 --- a/docs/mobile/installation.md +++ b/docs/installation.md @@ -1,5 +1,5 @@ --- -description: How to install react-simplikit for mobile web +description: How to install react-simplikit --- # Installation @@ -26,11 +26,6 @@ bun add react-simplikit ::: -## Requirements - -- React 18 or higher -- TypeScript 4.7 or higher (recommended) - ## Usage Import hooks directly from the package: diff --git a/docs/core/intro.md b/docs/intro.md similarity index 100% rename from docs/core/intro.md rename to docs/intro.md diff --git a/docs/ja/core/contributing.md b/docs/ja/contributing.md similarity index 100% rename from docs/ja/core/contributing.md rename to docs/ja/contributing.md diff --git a/docs/ja/core/installation.md b/docs/ja/core/installation.md deleted file mode 100644 index 3a401dc7..00000000 --- a/docs/ja/core/installation.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: react-simplikit のインストール方法 ---- - -# インストール - -お好みのパッケージマネージャーを使って、[npm](https://npmjs.com/package/react-simplikit) から `react-simplikit` をインストールできます。 - -::: code-group - -```sh [npm] -npm install react-simplikit -``` - -```sh [pnpm] -pnpm add react-simplikit -``` - -```sh [yarn] -yarn add react-simplikit -``` - -```sh [bun] -bun add react-simplikit -``` - -::: diff --git a/docs/ja/core/design-principles.md b/docs/ja/design-principles.md similarity index 100% rename from docs/ja/core/design-principles.md rename to docs/ja/design-principles.md diff --git a/docs/ja/index.md b/docs/ja/index.md index 1826f503..a06b6f66 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -10,10 +10,7 @@ hero: actions: - theme: brand text: はじめる - link: /ja/core/intro - - theme: alt - text: モバイルユーティリティ - link: /ja/mobile/intro + link: /ja/intro features: - title: '依存関係ゼロ' diff --git a/docs/ja/mobile/installation.md b/docs/ja/installation.md similarity index 70% rename from docs/ja/mobile/installation.md rename to docs/ja/installation.md index 6339566a..44ef8404 100644 --- a/docs/ja/mobile/installation.md +++ b/docs/ja/installation.md @@ -1,10 +1,10 @@ --- -description: 'モバイル Web 向け react-simplikit のインストール方法' +description: react-simplikit のインストール方法 --- # インストール -お好みのパッケージマネージャーを使って、[npm](https://npmjs.com/package/react-simplikit) から `react-simplikit` をインストールできます。モバイルユーティリティも同じパッケージに含まれています。 +お好みのパッケージマネージャーを使って、[npm](https://npmjs.com/package/react-simplikit) から `react-simplikit` をインストールできます。 ::: code-group @@ -26,11 +26,6 @@ bun add react-simplikit ::: -## 要件 - -- React 18 以上 -- TypeScript 4.7 以上(推奨) - ## 使い方 パッケージから直接フックを import してください。 diff --git a/docs/ja/core/intro.md b/docs/ja/intro.md similarity index 100% rename from docs/ja/core/intro.md rename to docs/ja/intro.md diff --git a/docs/ja/mobile-web.md b/docs/ja/mobile-web.md new file mode 100644 index 00000000..852c8ebe --- /dev/null +++ b/docs/ja/mobile-web.md @@ -0,0 +1,224 @@ +# モバイルユーティリティ + +モバイル Web 環境でよくある UI の課題を解決する React フック集です。 + +## なぜモバイルユーティリティなのか + +モバイル Web 開発には、デスクトップにはない固有の課題があります。 + +- **キーボード回避**: オンスクリーンキーボードが表示されると、下部に固定した要素が隠れてしまいます +- **スクロール方向の検知**: スクロールに応じてヘッダーやナビゲーションバーを表示・非表示にします +- **ネットワーク状態の監視**: 接続速度に応じてコンテンツの品質を調整します +- **ページ可視性の追跡**: アプリがバックグラウンドに移動したときに動画や計測を一時停止します +- **ビジュアルビューポートの変化**: モバイルブラウザでのズーム、キーボード、ビューポートのリサイズに対応します + +`react-simplikit` は、これらのシナリオを最小限の設定で扱える実績のあるフックを提供します。 + +## クイックスタート + +```bash +npm install react-simplikit +``` + +### CTA ボタンの例 + +もっとも一般的なモバイル UI パターンです。キーボードの上に移動する下部固定ボタンです。 + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### チャット入力欄の例 + +キーボードの上に留まる入力欄を持つチャット UI です。 + +```tsx +import { useState } from 'react'; +import { useAvoidKeyboard } from 'react-simplikit'; + +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'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); + + return ( +
+ +
+ ); +} +``` + +## 利用可能なフック + +| フック | 説明 | +| -------------------------------------------------- | -------------------------------------------------------------- | +| [useAvoidKeyboard](/ja/hooks/useAvoidKeyboard) | 固定要素をオンスクリーンキーボードの上に移動させます | +| [useKeyboardHeight](/ja/hooks/useKeyboardHeight) | 現在のキーボードの高さを返します | +| [useBodyScrollLock](/ja/hooks/useBodyScrollLock) | モーダルやオーバーレイのために body のスクロールをロックします | +| [useScrollDirection](/ja/hooks/useScrollDirection) | スクロール方向(上/下)を検知します | +| [useNetworkStatus](/ja/hooks/useNetworkStatus) | ネットワーク接続状態を監視します | +| [usePageVisibility](/ja/hooks/usePageVisibility) | ページの可視性の状態を追跡します | +| [useVisualViewport](/ja/hooks/useVisualViewport) | ビジュアルビューポートのサイズとオフセットを提供します | + +## ロードマップ + +モバイル画面は小さく、その小さな空間が驚くほど多くの UI 課題を生み出します。要素がオンスクリーンキーボードに隠れたり、セーフエリアが端末によって異なったり、ユーザーが実際に見ているビューポートがブラウザの報告する値と食い違ったりします。これらはエッジケースではなく、モバイル開発における日常的な現実です。 + +### 課題: モバイル画面での不安定な UI + +モバイル端末では、ユーザーが画面で見るものと開発者が想定するものが必ずしも一致しません。よくあるシナリオをいくつか紹介します。 + +- **キーボードが入力欄を覆う**: ユーザーがテキスト入力欄をタップすると、オンスクリーンキーボードがせり上がり、入力欄や下部に固定された送信ボタンを完全に覆ってしまうことがあります。 +- **セーフエリアの不整合**: ノッチ、丸みを帯びた角、ホームインジケーター(iPhone の下部バーなど)を持つ端末には、コンテンツを配置すべきでない予約領域がありますが、これは端末や OS のバージョンによって異なります。 +- **ビューポートの混乱**: ブラウザのレイアウトビューポートと実際に見える領域(ビジュアルビューポート)は、特にキーボードが開いていたりページがズームされていたりする場合に大きく異なることがあります。固定位置の要素が予期しない場所に配置されてしまうこともあります。 + +これらの課題は特定の OS や端末に固有のものではありません。iOS Safari であれ、Android Chrome であれ、その他どのモバイルブラウザであれ、根本的な課題は同じです。**見える領域は予測不可能であり、標準の CSS だけでは信頼できる形で対処できない**のです。 + +### 私たちのアプローチ: ビジュアルビューポートに焦点を当てる + +`react-simplikit` のモバイルユーティリティは、これらの問題を解決するために焦点を絞ったアプローチを取ります。もろいハックでブラウザの癖を回避しようとするのではなく、**ビジュアルビューポート** — ユーザーがある瞬間に実際に見ている画面領域 — を中心に設計しています。 + +[Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) をベースに構築することで、以下のようなことができるフックを提供します。 + +- **キーボードの表示を検知して対応する**ことで、下部固定要素が自然にキーボードを避けるようにします。 +- **セーフエリアインセットを読み取る**ことで、ノッチやホームインジケーターなど、端末固有の予約領域を正しく考慮します。 +- **実際に見える領域を追跡する**ことで、ブラウザのレイアウトエンジンが想定するものではなく、ユーザーが実際に見ているものに基づいてレイアウトを決定できます。 + +目標はシンプルです。**ビジュアルビューポート内で、UI が確実かつ予測可能にレンダリングされること**です。 + +### クロスプラットフォーム、クロスデバイス + +特定の OS や端末モデルに限定されないことを目指しています。モバイル Web は本質的にクロスプラットフォームであり、`react-simplikit` はそれを受け入れています。 + +私たちのフックは、以下の環境で一貫して動作するように設計されています。 + +- **iOS と Android** — 2 大モバイルプラットフォーム。 +- **さまざまなブラウザ** — Safari、Chrome、Samsung Internet など。 +- **さまざまな端末フォームファクター** — コンパクトな端末から大画面端末まで、ノッチやホームインジケーターの有無を問いません。 + +特定の API が利用できない場合(たとえば古いブラウザの `window.visualViewport`)でも、UI を壊すことなく段階的に劣化する安全なフォールバックを提供します。 + +### 今後の展開 + +`react-simplikit` で提供するモバイルフックのラインナップを、常に同じ原則に基づいて拡張し続けています。**端末や OS を問わず、モバイル UI 開発を予測可能で信頼できるものにする**という原則です。よくあるモバイル UI の悩みがあれば、私たちはそのためのクリーンで宣言的な解決策に取り組んでいる可能性が高いです。 + +## モバイル特有の原則 + +### プラットフォームを意識した設計 + +実装においては、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` を活用します + +## モバイル特有のガイドライン + +### 実機でのテスト + +- 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/contributing.md b/docs/ja/mobile/contributing.md deleted file mode 100644 index 653b6042..00000000 --- a/docs/ja/mobile/contributing.md +++ /dev/null @@ -1,141 +0,0 @@ -# モバイルユーティリティへの貢献 - -このガイドは [core の貢献ガイド](/ja/core/contributing) を拡張したものです。 - -## パッケージのスコープ - -`react-simplikit` のモバイルユーティリティは、**モバイル 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 コメントを詳しく書いたうえで `yarn docs:gen ` を実行すると、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 deleted file mode 100644 index b2e57895..00000000 --- a/docs/ja/mobile/design-principles.md +++ /dev/null @@ -1,90 +0,0 @@ -# 設計原則 - -モバイルユーティリティは `react-simplikit` のコア原則を踏襲しつつ、モバイル特有の課題に合わせて拡張しています。 - -## コア原則 - -### React のライフサイクルを尊重し、干渉しない - -`react-simplikit` は、React のライフサイクルに直接干渉する実装を含みません。 -たとえば、`useMount` や `useLifecycles` のようなフックは提供せず、代わりに React のデフォルトの挙動を尊重し、活用するアプローチを採ります。 - -### 依存関係ゼロによる軽量さと高速さ - -`react-simplikit` には依存関係が一切ありません。追加のライブラリに依存しないことで、プロジェクトに組み込む際のバンドルサイズを最小化し、パフォーマンス低下への懸念をなくします。 - -### 100% テストカバレッジによる信頼性の確保 - -`react-simplikit` は、すべての関数と分岐を徹底的にテストします。 -基本機能だけでなく、各実装の SSR 環境における考慮事項も含めた包括的なテストを書くことで、予期しない挙動による問題を防いでいます。 - -### わかりやすく使いやすい包括的なドキュメント - -`react-simplikit` は、ユーザーが各機能を素早く理解し活用できるよう、詳細なドキュメントを提供します。ドキュメントには以下が含まれます。 - -- **JSDoc コメント**: 各関数の挙動、パラメータ、戻り値についての詳しい説明。 -- **使用ガイド**: すぐに始められる、明確でわかりやすい手順。 -- **実践的な使用例**: 実際のシナリオで実装を活用する方法を示す例。 - -### 完全な TypeScript サポートによる型安全性 - -`react-simplikit` は、最初から 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/intro.md b/docs/ja/mobile/intro.md deleted file mode 100644 index aa4194c5..00000000 --- a/docs/ja/mobile/intro.md +++ /dev/null @@ -1,123 +0,0 @@ -# モバイルユーティリティ - -モバイル Web 環境でよくある UI の課題を解決する React フック集です。 - -## なぜモバイルユーティリティなのか - -モバイル Web 開発には、デスクトップにはない固有の課題があります。 - -- **キーボード回避**: オンスクリーンキーボードが表示されると、下部に固定した要素が隠れてしまいます -- **スクロール方向の検知**: スクロールに応じてヘッダーやナビゲーションバーを表示・非表示にします -- **ネットワーク状態の監視**: 接続速度に応じてコンテンツの品質を調整します -- **ページ可視性の追跡**: アプリがバックグラウンドに移動したときに動画や計測を一時停止します -- **ビジュアルビューポートの変化**: モバイルブラウザでのズーム、キーボード、ビューポートのリサイズに対応します - -`react-simplikit` は、これらのシナリオを最小限の設定で扱える実績のあるフックを提供します。 - -## クイックスタート - -```bash -npm install react-simplikit -``` - -### CTA ボタンの例 - -もっとも一般的なモバイル UI パターンです。キーボードの上に移動する下部固定ボタンです。 - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard(); - - return ( -
- -
- ); -} -``` - -### チャット入力欄の例 - -キーボードの上に留まる入力欄を持つチャット UI です。 - -```tsx -import { useState } from 'react'; -import { useAvoidKeyboard } from 'react-simplikit'; - -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'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); - - return ( -
- -
- ); -} -``` - -## 利用可能なフック - -| フック | 説明 | -| -------------------------------------------------- | -------------------------------------------------------------- | -| [useAvoidKeyboard](/ja/hooks/useAvoidKeyboard) | 固定要素をオンスクリーンキーボードの上に移動させます | -| [useKeyboardHeight](/ja/hooks/useKeyboardHeight) | 現在のキーボードの高さを返します | -| [useBodyScrollLock](/ja/hooks/useBodyScrollLock) | モーダルやオーバーレイのために body のスクロールをロックします | -| [useScrollDirection](/ja/hooks/useScrollDirection) | スクロール方向(上/下)を検知します | -| [useNetworkStatus](/ja/hooks/useNetworkStatus) | ネットワーク接続状態を監視します | -| [usePageVisibility](/ja/hooks/usePageVisibility) | ページの可視性の状態を追跡します | -| [useVisualViewport](/ja/hooks/useVisualViewport) | ビジュアルビューポートのサイズとオフセットを提供します | diff --git a/docs/ja/mobile/roadmap.md b/docs/ja/mobile/roadmap.md deleted file mode 100644 index bc33c44f..00000000 --- a/docs/ja/mobile/roadmap.md +++ /dev/null @@ -1,41 +0,0 @@ -# ロードマップ - -モバイル画面は小さく、その小さな空間が驚くほど多くの UI 課題を生み出します。要素がオンスクリーンキーボードに隠れたり、セーフエリアが端末によって異なったり、ユーザーが実際に見ているビューポートがブラウザの報告する値と食い違ったりします。これらはエッジケースではなく、モバイル開発における日常的な現実です。 - -## 課題: モバイル画面での不安定な UI - -モバイル端末では、ユーザーが画面で見るものと開発者が想定するものが必ずしも一致しません。よくあるシナリオをいくつか紹介します。 - -- **キーボードが入力欄を覆う**: ユーザーがテキスト入力欄をタップすると、オンスクリーンキーボードがせり上がり、入力欄や下部に固定された送信ボタンを完全に覆ってしまうことがあります。 -- **セーフエリアの不整合**: ノッチ、丸みを帯びた角、ホームインジケーター(iPhone の下部バーなど)を持つ端末には、コンテンツを配置すべきでない予約領域がありますが、これは端末や OS のバージョンによって異なります。 -- **ビューポートの混乱**: ブラウザのレイアウトビューポートと実際に見える領域(ビジュアルビューポート)は、特にキーボードが開いていたりページがズームされていたりする場合に大きく異なることがあります。固定位置の要素が予期しない場所に配置されてしまうこともあります。 - -これらの課題は特定の OS や端末に固有のものではありません。iOS Safari であれ、Android Chrome であれ、その他どのモバイルブラウザであれ、根本的な課題は同じです。**見える領域は予測不可能であり、標準の CSS だけでは信頼できる形で対処できない**のです。 - -## 私たちのアプローチ: ビジュアルビューポートに焦点を当てる - -`react-simplikit` のモバイルユーティリティは、これらの問題を解決するために焦点を絞ったアプローチを取ります。もろいハックでブラウザの癖を回避しようとするのではなく、**ビジュアルビューポート** — ユーザーがある瞬間に実際に見ている画面領域 — を中心に設計しています。 - -[Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) をベースに構築することで、以下のようなことができるフックを提供します。 - -- **キーボードの表示を検知して対応する**ことで、下部固定要素が自然にキーボードを避けるようにします。 -- **セーフエリアインセットを読み取る**ことで、ノッチやホームインジケーターなど、端末固有の予約領域を正しく考慮します。 -- **実際に見える領域を追跡する**ことで、ブラウザのレイアウトエンジンが想定するものではなく、ユーザーが実際に見ているものに基づいてレイアウトを決定できます。 - -目標はシンプルです。**ビジュアルビューポート内で、UI が確実かつ予測可能にレンダリングされること**です。 - -## クロスプラットフォーム、クロスデバイス - -特定の OS や端末モデルに限定されないことを目指しています。モバイル Web は本質的にクロスプラットフォームであり、`react-simplikit` はそれを受け入れています。 - -私たちのフックは、以下の環境で一貫して動作するように設計されています。 - -- **iOS と Android** — 2 大モバイルプラットフォーム。 -- **さまざまなブラウザ** — Safari、Chrome、Samsung Internet など。 -- **さまざまな端末フォームファクター** — コンパクトな端末から大画面端末まで、ノッチやホームインジケーターの有無を問いません。 - -特定の API が利用できない場合(たとえば古いブラウザの `window.visualViewport`)でも、UI を壊すことなく段階的に劣化する安全なフォールバックを提供します。 - -## 今後の展開 - -`react-simplikit` で提供するモバイルフックのラインナップを、常に同じ原則に基づいて拡張し続けています。**端末や OS を問わず、モバイル UI 開発を予測可能で信頼できるものにする**という原則です。よくあるモバイル UI の悩みがあれば、私たちはそのためのクリーンで宣言的な解決策に取り組んでいる可能性が高いです。 diff --git a/docs/ja/core/why-react-simplikit-matters.md b/docs/ja/why-react-simplikit-matters.md similarity index 100% rename from docs/ja/core/why-react-simplikit-matters.md rename to docs/ja/why-react-simplikit-matters.md diff --git a/docs/ko/core/ai-integration.md b/docs/ko/ai-integration.md similarity index 100% rename from docs/ko/core/ai-integration.md rename to docs/ko/ai-integration.md diff --git a/docs/ko/core/contributing.md b/docs/ko/contributing.md similarity index 100% rename from docs/ko/core/contributing.md rename to docs/ko/contributing.md diff --git a/docs/ko/core/installation.md b/docs/ko/core/installation.md deleted file mode 100644 index f672a3c7..00000000 --- a/docs/ko/core/installation.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: react-simplikit 설치 방법 ---- - -# 설치하기 - -좋아하는 패키지 매니저를 사용하여 [npm](https://npmjs.com/package/react-simplikit)에서 `react-simplikit`을 설치할 수 있어요. - -::: code-group - -```sh [npm] -npm install react-simplikit -``` - -```sh [pnpm] -pnpm add react-simplikit -``` - -```sh [yarn] -yarn add react-simplikit -``` - -```sh [bun] -bun add react-simplikit -``` - -::: diff --git a/docs/ko/core/design-principles.md b/docs/ko/design-principles.md similarity index 100% rename from docs/ko/core/design-principles.md rename to docs/ko/design-principles.md diff --git a/docs/ko/index.md b/docs/ko/index.md index e1d1455e..7d70ddbc 100644 --- a/docs/ko/index.md +++ b/docs/ko/index.md @@ -10,10 +10,7 @@ hero: actions: - theme: brand text: 시작하기 - link: /ko/core/intro - - theme: alt - text: 모바일 유틸리티 - link: /ko/mobile/intro + link: /ko/intro features: - title: '의존성이 전혀 없어요' diff --git a/docs/ko/mobile/installation.md b/docs/ko/installation.md similarity index 72% rename from docs/ko/mobile/installation.md rename to docs/ko/installation.md index 98021804..f1bad6ac 100644 --- a/docs/ko/mobile/installation.md +++ b/docs/ko/installation.md @@ -1,10 +1,10 @@ --- -description: '모바일 웹을 위한 react-simplikit 설치 방법' +description: react-simplikit 설치 방법 --- # 설치하기 -좋아하는 패키지 매니저를 사용하여 [npm](https://npmjs.com/package/react-simplikit)에서 `react-simplikit`을 설치할 수 있어요. 모바일 유틸리티도 같은 패키지에 포함돼 있어요. +좋아하는 패키지 매니저를 사용하여 [npm](https://npmjs.com/package/react-simplikit)에서 `react-simplikit`을 설치할 수 있어요. ::: code-group @@ -26,11 +26,6 @@ bun add react-simplikit ::: -## 요구사항 - -- React 18 이상 -- TypeScript 4.7 이상 (권장) - ## 사용법 패키지에서 직접 훅을 import하세요: diff --git a/docs/ko/core/intro.md b/docs/ko/intro.md similarity index 100% rename from docs/ko/core/intro.md rename to docs/ko/intro.md diff --git a/docs/ko/mobile-web.md b/docs/ko/mobile-web.md new file mode 100644 index 00000000..3c0c9139 --- /dev/null +++ b/docs/ko/mobile-web.md @@ -0,0 +1,224 @@ +# 모바일 유틸리티 + +모바일 웹 환경에서 발생하는 다양한 UI 문제를 해결하는 React 훅 모음이에요. + +## 왜 모바일 유틸리티인가요? + +모바일 웹 개발에는 데스크톱에서는 없는 고유한 문제들이 있어요: + +- **키보드 회피**: 온스크린 키보드가 올라오면 하단 고정 요소가 가려지는 문제 +- **스크롤 방향 감지**: 스크롤에 따라 헤더나 네비게이션 바를 숨기거나 보여주기 +- **네트워크 상태 모니터링**: 연결 속도에 따라 콘텐츠 품질 조절하기 +- **페이지 가시성 추적**: 앱이 백그라운드로 갈 때 비디오나 분석 일시정지하기 +- **Visual Viewport 변화**: 모바일 브라우저에서 줌, 키보드, 뷰포트 리사이즈 처리하기 + +`react-simplikit`은 이러한 시나리오를 최소한의 설정으로 처리할 수 있는 검증된 훅들을 제공해요. + +## Quick Start + +```bash +npm install react-simplikit +``` + +### Button CTA 예제 + +가장 흔한 모바일 UI 패턴 - 키보드 위로 이동하는 하단 고정 버튼: + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### 채팅 입력창 예제 + +키보드 위에 위치하는 입력창이 있는 채팅 인터페이스: + +```tsx +import { useState } from 'react'; +import { useAvoidKeyboard } from 'react-simplikit'; + +function ChatInput() { + const { style } = useAvoidKeyboard(); + const [message, setMessage] = useState(''); + + return ( +
+ setMessage(e.target.value)} + placeholder="Type a message..." + style={{ flex: 1 }} + /> + +
+ ); +} +``` + +### Safe Area 적용 + +홈 인디케이터가 있는 기기(예: iPhone)의 경우 safe area 오프셋을 추가할 수 있어요: + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); + + return ( +
+ +
+ ); +} +``` + +## 제공하는 훅 + +| 훅 | 설명 | +| -------------------------------------------------- | ------------------------------------------- | +| [useAvoidKeyboard](/ko/hooks/useAvoidKeyboard) | 고정 요소를 온스크린 키보드 위로 이동시켜요 | +| [useKeyboardHeight](/ko/hooks/useKeyboardHeight) | 현재 키보드 높이를 반환해요 | +| [useBodyScrollLock](/ko/hooks/useBodyScrollLock) | 모달과 오버레이를 위해 body 스크롤을 잠가요 | +| [useScrollDirection](/ko/hooks/useScrollDirection) | 스크롤 방향(위/아래)을 감지해요 | +| [useNetworkStatus](/ko/hooks/useNetworkStatus) | 네트워크 연결 상태를 모니터링해요 | +| [usePageVisibility](/ko/hooks/usePageVisibility) | 페이지 가시성 상태를 추적해요 | +| [useVisualViewport](/ko/hooks/useVisualViewport) | Visual Viewport 크기와 오프셋을 제공해요 | + +## 앞으로의 방향 + +모바일 화면은 작고 그 작은 공간 안에서 UI가 의도대로 보이지 않는 경우가 많아요. 키보드에 요소가 가려지고, 기기마다 다른 SafeArea가 다르고, 브라우저가 보여주는 viewport와 사용자가 실제로 보는 영역의 차이가 빈번하게 발생해요. + +### 문제: 모바일 화면에서 불안정한 UI + +모바일 기기에서 사용자가 화면에서 보는 것과 개발자가 기대하는 것이 항상 일치하지는 않아요. 흔히 마주치는 상황들을 살펴볼게요: + +- **키보드가 입력 필드를 가리는 경우**: 사용자가 텍스트 입력 필드를 탭하면 온스크린 키보드가 올라오면서, 입력 필드나 하단에 고정된 제출 버튼을 완전히 가릴 수 있어요. +- **안전 영역의 불일치**: 노치, 둥근 모서리, 홈 인디케이터(아이폰 하단 바 등)가 있는 기기에는 콘텐츠를 배치하면 안 되는 예약 영역이 있어요. 하지만 이 영역은 기기와 OS 버전마다 달라요. +- **뷰포트 혼란**: 브라우저의 레이아웃 뷰포트와 실제 보이는 영역(비주얼 뷰포트)은 크게 다를 수 있어요. 특히 키보드가 열려 있거나 페이지가 확대된 경우에 `position: fixed` 요소가 예상치 못한 위치에 나타날 수 있어요. + +이런 문제들은 특정 OS나 기기에 국한되지 않아요. iOS Safari든 Android Chrome이든, 어떤 모바일 브라우저든 근본적인 문제는 같아요: **보이는 영역이 예측 불가능하고, 표준 CSS만으로는 이를 안정적으로 처리할 수 없다**는 것이에요. + +### 우리의 접근: 비주얼 뷰포트에 집중 + +`react-simplikit`의 모바일 유틸리티는 이러한 문제들을 해결하기 위해 명확한 접근 방식을 취해요. 브라우저의 특이한 동작을 불안정한 우회 방법으로 처리하는 대신, **비주얼 뷰포트(Visual Viewport)** — 사용자가 특정 순간에 실제로 볼 수 있는 화면 영역 — 를 중심으로 설계해요. + +[Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API)를 기반으로, 다음과 같은 훅들을 제공해요: + +- **키보드 출현을 감지하고 대응**하여 하단 고정 요소가 자연스럽게 비켜나도록 해요. +- **안전 영역 인셋을 읽어** 노치, 홈 인디케이터 등 기기별 예약 영역을 올바르게 처리해요. +- **실제 보이는 영역을 추적**하여 브라우저의 레이아웃 엔진이 가정하는 것이 아닌, 사용자가 실제로 보는 것을 기반으로 레이아웃을 결정할 수 있게 해요. + +목표는 단순해요: **비주얼 뷰포트 안에서 UI가 안정적이고 예측 가능하게 렌더링되도록 하는 것**이에요. + +### 크로스 플랫폼, 크로스 디바이스 + +특정 OS나 기기 모델에 국한되고 싶지 않아요. 모바일 웹은 본질적으로 크로스 플랫폼이고, `react-simplikit`은 이를 지향해요. + +우리의 훅들은 다음 환경에서 일관되게 동작하도록 설계되었어요: + +- **iOS와 Android** — 두 가지 주요 모바일 플랫폼. +- **다양한 브라우저** — Safari, Chrome, Samsung Internet 등. +- **다양한 기기 폼 팩터** — 소형 폰부터 대형 화면 기기까지, 노치나 홈 인디케이터의 유무에 관계없이. + +특정 API를 사용할 수 없는 환경(예: 구형 브라우저의 `window.visualViewport`)에서는 UI가 깨지지 않도록 안전한 폴백을 제공해요. + +### 앞으로의 방향 + +`react-simplikit`에서 제공하는 모바일 훅들을 계속 확장해 나갈 예정이에요. 항상 같은 원칙에 따라: **기기나 OS에 관계없이 모바일 UI 개발을 예측 가능하고 안정적으로 만드는 것**이에요. 모바일 UI에서 흔히 겪는 불편함이 있다면, 우리는 그것에 대한 깔끔하고 선언적인 해결책을 만들고 있을 거예요. + +## 모바일 특화 원칙 + +### 플랫폼 인식 설계 + +구현에서 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` 활용 + +## 모바일 특화 가이드라인 + +### 실제 기기 테스트 + +- 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/ko/mobile/contributing.md b/docs/ko/mobile/contributing.md deleted file mode 100644 index 9deee03a..00000000 --- a/docs/ko/mobile/contributing.md +++ /dev/null @@ -1,136 +0,0 @@ -# 기여하기 - -이 가이드는 [core 기여 가이드](/ko/core/contributing)를 확장한 것이에요. - -## 패키지 범위 - -`react-simplikit`의 모바일 유틸리티는 **모바일 웹 환경에서 겪는 문제 해결**에 집중해요. - -다음과 같은 영역을 다뤄요: - -- 뷰포트 관리 (visual viewport, safe area) -- 키보드 처리 (키보드에 가려지는 콘텐츠 방지) -- iOS Safari와 Android Chrome에서 발생하는 레이아웃 이슈 -- 모바일 브라우저의 스크롤 동작 - -이 패키지는 모든 브라우저 API 의존 유틸리티를 위한 것이 **아니에요**. 브라우저 API를 사용하더라도 데스크톱이나 범용적인 문제를 해결하는 훅(예: 키보드 단축키, 마우스 좌표)은 여기에 속하지 않아요. - -## 개발 워크플로우 - -``` -스캐폴딩 → 구현 → 테스트 → 문서화 → 리뷰 → Changeset → 병합 -``` - -### 1. 스캐폴딩 - -새로운 훅을 위한 기본 구조를 생성해요: - -```bash -yarn scaffold useNewHook --type h # 훅 -``` - -### 2. 구현 - -[설계 원칙](/ko/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을 꼼꼼하게 작성한 뒤 `yarn docs:gen `을 실행하면 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 -- [ ] 모든 early return -- [ ] cleanup 함수 (useEffect return) - -### 5. Changeset 작성 - -코드 변경 사항이 패키지에 영향을 미치는 경우 changeset을 작성해야 해요: - -```bash -yarn changeset -``` - -변경 유형을 선택하세요: - -- `patch`: 버그 수정이나 작은 변경사항 -- `minor`: 새로운 기능 추가 (하위 호환성 유지) -- `major`: 주요 변경사항 (하위 호환성 깨짐) - -## 모바일 특화 가이드라인 - -### 실제 기기 테스트 - -- 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/ko/mobile/design-principles.md b/docs/ko/mobile/design-principles.md deleted file mode 100644 index 1fbb23f7..00000000 --- a/docs/ko/mobile/design-principles.md +++ /dev/null @@ -1,90 +0,0 @@ -# 설계 원칙 - -모바일 유틸리티는 `react-simplikit`의 핵심 원칙을 따르면서, 모바일 환경에 특화된 고려사항을 추가로 다루고 있어요. - -## 핵심 원칙 - -### React의 생명주기를 존중하고 간섭하지 않기 - -`react-simplikit`은 React의 생명주기에 직접적으로 간섭하는 구현체를 포함하지 않아요. -예를 들어, `useMount`나 `useLifecycles`와 같은 훅을 제공하지 않고, React의 기본 동작을 존중하고 활용하는 접근 방식을 선호해요. - -### 의존성 없음을 통한 가볍고 빠른 성능 - -`react-simplikit`은 의존성이 전혀 없어요. 추가 라이브러리에 의존하지 않음으로써 프로젝트에 통합할 때 번들 크기를 최소화하고 성능 저하에 대한 우려를 없애요. - -### 100% 테스트 커버리지를 통한 신뢰성 보장 - -`react-simplikit`은 모든 함수와 분기를 철저하게 테스트해요. -기본 기능뿐만 아니라 각 구현체의 SSR 환경 고려사항도 포함하는 포괄적인 테스트를 작성하여, 예상치 못한 동작으로 인한 문제를 방지해요. - -### 쉬운 이해와 사용을 위한 포괄적인 문서 - -`react-simplikit`은 사용자가 각 기능을 빠르게 이해하고 활용할 수 있도록 상세한 문서를 제공해요. 문서에는 다음이 포함돼요: - -- **JSDoc 주석**: 각 함수의 동작, 매개변수, 반환 값에 대한 자세한 설명. -- **사용 가이드**: 즉시 시작할 수 있는 명확하고 따라하기 쉬운 지침. -- **실용적인 예제**: 실제 시나리오에서 구현체를 활용하는 방법을 보여주는 예제. - -### 완전한 TypeScript 지원을 통한 타입 안전성 - -`react-simplikit`은 처음부터 TypeScript로 구축되었어요. 모든 훅과 유틸리티는 다음을 제공해요: - -- **엄격한 타입 정의**: 모든 매개변수, 반환 값, 옵션이 완전히 타입화되어 있어요 -- **IntelliSense 지원**: IDE에서 자동완성과 인라인 문서를 제공받을 수 있어요 -- **제네릭 타입**: 타입 정보를 보존하는 유연한 API를 제공해요 -- **`any` 타입 없음**: 타입 안전성을 손상시키는 escape hatch를 사용하지 않아요 - -## 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/ko/mobile/intro.md b/docs/ko/mobile/intro.md deleted file mode 100644 index 6392bc21..00000000 --- a/docs/ko/mobile/intro.md +++ /dev/null @@ -1,123 +0,0 @@ -# 모바일 유틸리티 - -모바일 웹 환경에서 발생하는 다양한 UI 문제를 해결하는 React 훅 모음이에요. - -## 왜 모바일 유틸리티인가요? - -모바일 웹 개발에는 데스크톱에서는 없는 고유한 문제들이 있어요: - -- **키보드 회피**: 온스크린 키보드가 올라오면 하단 고정 요소가 가려지는 문제 -- **스크롤 방향 감지**: 스크롤에 따라 헤더나 네비게이션 바를 숨기거나 보여주기 -- **네트워크 상태 모니터링**: 연결 속도에 따라 콘텐츠 품질 조절하기 -- **페이지 가시성 추적**: 앱이 백그라운드로 갈 때 비디오나 분석 일시정지하기 -- **Visual Viewport 변화**: 모바일 브라우저에서 줌, 키보드, 뷰포트 리사이즈 처리하기 - -`react-simplikit`은 이러한 시나리오를 최소한의 설정으로 처리할 수 있는 검증된 훅들을 제공해요. - -## Quick Start - -```bash -npm install react-simplikit -``` - -### Button CTA 예제 - -가장 흔한 모바일 UI 패턴 - 키보드 위로 이동하는 하단 고정 버튼: - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard(); - - return ( -
- -
- ); -} -``` - -### 채팅 입력창 예제 - -키보드 위에 위치하는 입력창이 있는 채팅 인터페이스: - -```tsx -import { useState } from 'react'; -import { useAvoidKeyboard } from 'react-simplikit'; - -function ChatInput() { - const { style } = useAvoidKeyboard(); - const [message, setMessage] = useState(''); - - return ( -
- setMessage(e.target.value)} - placeholder="Type a message..." - style={{ flex: 1 }} - /> - -
- ); -} -``` - -### Safe Area 적용 - -홈 인디케이터가 있는 기기(예: iPhone)의 경우 safe area 오프셋을 추가할 수 있어요: - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); - - return ( -
- -
- ); -} -``` - -## 제공하는 훅 - -| 훅 | 설명 | -| -------------------------------------------------- | ------------------------------------------- | -| [useAvoidKeyboard](/ko/hooks/useAvoidKeyboard) | 고정 요소를 온스크린 키보드 위로 이동시켜요 | -| [useKeyboardHeight](/ko/hooks/useKeyboardHeight) | 현재 키보드 높이를 반환해요 | -| [useBodyScrollLock](/ko/hooks/useBodyScrollLock) | 모달과 오버레이를 위해 body 스크롤을 잠가요 | -| [useScrollDirection](/ko/hooks/useScrollDirection) | 스크롤 방향(위/아래)을 감지해요 | -| [useNetworkStatus](/ko/hooks/useNetworkStatus) | 네트워크 연결 상태를 모니터링해요 | -| [usePageVisibility](/ko/hooks/usePageVisibility) | 페이지 가시성 상태를 추적해요 | -| [useVisualViewport](/ko/hooks/useVisualViewport) | Visual Viewport 크기와 오프셋을 제공해요 | diff --git a/docs/ko/mobile/roadmap.md b/docs/ko/mobile/roadmap.md deleted file mode 100644 index 0bb052de..00000000 --- a/docs/ko/mobile/roadmap.md +++ /dev/null @@ -1,41 +0,0 @@ -# 앞으로의 방향 - -모바일 화면은 작고 그 작은 공간 안에서 UI가 의도대로 보이지 않는 경우가 많아요. 키보드에 요소가 가려지고, 기기마다 다른 SafeArea가 다르고, 브라우저가 보여주는 viewport와 사용자가 실제로 보는 영역의 차이가 빈번하게 발생해요. - -## 문제: 모바일 화면에서 불안정한 UI - -모바일 기기에서 사용자가 화면에서 보는 것과 개발자가 기대하는 것이 항상 일치하지는 않아요. 흔히 마주치는 상황들을 살펴볼게요: - -- **키보드가 입력 필드를 가리는 경우**: 사용자가 텍스트 입력 필드를 탭하면 온스크린 키보드가 올라오면서, 입력 필드나 하단에 고정된 제출 버튼을 완전히 가릴 수 있어요. -- **안전 영역의 불일치**: 노치, 둥근 모서리, 홈 인디케이터(아이폰 하단 바 등)가 있는 기기에는 콘텐츠를 배치하면 안 되는 예약 영역이 있어요. 하지만 이 영역은 기기와 OS 버전마다 달라요. -- **뷰포트 혼란**: 브라우저의 레이아웃 뷰포트와 실제 보이는 영역(비주얼 뷰포트)은 크게 다를 수 있어요. 특히 키보드가 열려 있거나 페이지가 확대된 경우에 `position: fixed` 요소가 예상치 못한 위치에 나타날 수 있어요. - -이런 문제들은 특정 OS나 기기에 국한되지 않아요. iOS Safari든 Android Chrome이든, 어떤 모바일 브라우저든 근본적인 문제는 같아요: **보이는 영역이 예측 불가능하고, 표준 CSS만으로는 이를 안정적으로 처리할 수 없다**는 것이에요. - -## 우리의 접근: 비주얼 뷰포트에 집중 - -`react-simplikit`의 모바일 유틸리티는 이러한 문제들을 해결하기 위해 명확한 접근 방식을 취해요. 브라우저의 특이한 동작을 불안정한 우회 방법으로 처리하는 대신, **비주얼 뷰포트(Visual Viewport)** — 사용자가 특정 순간에 실제로 볼 수 있는 화면 영역 — 를 중심으로 설계해요. - -[Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API)를 기반으로, 다음과 같은 훅들을 제공해요: - -- **키보드 출현을 감지하고 대응**하여 하단 고정 요소가 자연스럽게 비켜나도록 해요. -- **안전 영역 인셋을 읽어** 노치, 홈 인디케이터 등 기기별 예약 영역을 올바르게 처리해요. -- **실제 보이는 영역을 추적**하여 브라우저의 레이아웃 엔진이 가정하는 것이 아닌, 사용자가 실제로 보는 것을 기반으로 레이아웃을 결정할 수 있게 해요. - -목표는 단순해요: **비주얼 뷰포트 안에서 UI가 안정적이고 예측 가능하게 렌더링되도록 하는 것**이에요. - -## 크로스 플랫폼, 크로스 디바이스 - -특정 OS나 기기 모델에 국한되고 싶지 않아요. 모바일 웹은 본질적으로 크로스 플랫폼이고, `react-simplikit`은 이를 지향해요. - -우리의 훅들은 다음 환경에서 일관되게 동작하도록 설계되었어요: - -- **iOS와 Android** — 두 가지 주요 모바일 플랫폼. -- **다양한 브라우저** — Safari, Chrome, Samsung Internet 등. -- **다양한 기기 폼 팩터** — 소형 폰부터 대형 화면 기기까지, 노치나 홈 인디케이터의 유무에 관계없이. - -특정 API를 사용할 수 없는 환경(예: 구형 브라우저의 `window.visualViewport`)에서는 UI가 깨지지 않도록 안전한 폴백을 제공해요. - -## 앞으로의 방향 - -`react-simplikit`에서 제공하는 모바일 훅들을 계속 확장해 나갈 예정이에요. 항상 같은 원칙에 따라: **기기나 OS에 관계없이 모바일 UI 개발을 예측 가능하고 안정적으로 만드는 것**이에요. 모바일 UI에서 흔히 겪는 불편함이 있다면, 우리는 그것에 대한 깔끔하고 선언적인 해결책을 만들고 있을 거예요. diff --git a/docs/ko/core/why-react-simplikit-matters.md b/docs/ko/why-react-simplikit-matters.md similarity index 100% rename from docs/ko/core/why-react-simplikit-matters.md rename to docs/ko/why-react-simplikit-matters.md diff --git a/docs/mobile-web.md b/docs/mobile-web.md new file mode 100644 index 00000000..293dd3c4 --- /dev/null +++ b/docs/mobile-web.md @@ -0,0 +1,224 @@ +# Mobile Utilities + +A collection of React hooks that solve common UI challenges in mobile web environments. + +## Why mobile utilities? + +Mobile web development comes with unique challenges that don't exist on desktop: + +- **Keyboard avoidance**: Fixed bottom elements get hidden when the on-screen keyboard appears +- **Scroll direction detection**: Headers and navigation bars that show/hide based on scroll +- **Network status monitoring**: Adapting content quality based on connection speed +- **Page visibility tracking**: Pausing videos or analytics when the app goes to background +- **Visual viewport changes**: Handling zoom, keyboard, and viewport resize on mobile browsers + +`react-simplikit` provides battle-tested mobile hooks to handle these scenarios with minimal configuration. + +## Quick Start + +```bash +npm install react-simplikit +``` + +### Button CTA Example + +The most common mobile UI pattern - a fixed bottom button that moves above the keyboard: + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### Chat Input Example + +A chat interface with an input field that stays above the keyboard: + +```tsx +import { useState } from 'react'; +import { useAvoidKeyboard } from 'react-simplikit'; + +function ChatInput() { + const { style } = useAvoidKeyboard(); + const [message, setMessage] = useState(''); + + return ( +
+ setMessage(e.target.value)} + placeholder="Type a message..." + style={{ flex: 1 }} + /> + +
+ ); +} +``` + +### With Safe Area + +For devices with home indicators (like iPhone), you can add a safe area offset: + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); + + return ( +
+ +
+ ); +} +``` + +## Available Hooks + +| Hook | Description | +| ----------------------------------------------- | ------------------------------------------------- | +| [useAvoidKeyboard](/hooks/useAvoidKeyboard) | Moves fixed elements above the on-screen keyboard | +| [useKeyboardHeight](/hooks/useKeyboardHeight) | Returns the current keyboard height | +| [useBodyScrollLock](/hooks/useBodyScrollLock) | Locks body scroll for modals and overlays | +| [useScrollDirection](/hooks/useScrollDirection) | Detects scroll direction (up/down) | +| [useNetworkStatus](/hooks/useNetworkStatus) | Monitors network connection status | +| [usePageVisibility](/hooks/usePageVisibility) | Tracks page visibility state | +| [useVisualViewport](/hooks/useVisualViewport) | Provides visual viewport dimensions and offset | + +## Roadmap + +Mobile screens are small, and that small space creates a surprising number of UI challenges. Elements get hidden behind on-screen keyboards, safe areas vary by device, and the viewport the user actually sees often differs from what the browser reports. These are not edge cases — they are everyday realities of mobile development. + +### The Problem: Unreliable UI on Mobile Screens + +On mobile devices, what users see on their screen doesn't always match what developers expect. Here are a few common scenarios: + +- **Keyboard covering input fields**: When a user taps on a text input, the on-screen keyboard slides up and can completely obscure the input field or a submit button fixed at the bottom. +- **Safe area inconsistencies**: Devices with notches, rounded corners, or home indicators (like the iPhone's bottom bar) have reserved areas where content shouldn't be placed — but these vary across devices and OS versions. +- **Viewport confusion**: The browser's layout viewport and the actual visible area (the visual viewport) can differ significantly, especially when the keyboard is open or the page is zoomed. Fixed-position elements may end up in unexpected places. + +These issues are not specific to any single OS or device. Whether it's iOS Safari, Android Chrome, or any other mobile browser, the underlying challenge is the same: **the visible area is unpredictable, and standard CSS alone can't reliably account for it**. + +### Our Approach: Focus on the Visual Viewport + +The mobile utilities in `react-simplikit` take a focused approach to solving these problems. Rather than trying to work around browser quirks with brittle hacks, we center our design around the **visual viewport** — the area of the screen that the user can actually see at any given moment. + +By building on the [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API), we provide hooks that let you: + +- **Detect and respond to keyboard appearance** so that fixed-bottom elements move out of the way naturally. +- **Read safe area insets** to properly account for notches, home indicators, and other device-specific reserved areas. +- **Track the real visible area** so your layout decisions are based on what the user actually sees, not what the browser's layout engine assumes. + +The goal is simple: **within the visual viewport, UI should render reliably and predictably**. + +### Cross-Platform, Cross-Device + +We don't want to be limited to a specific OS or device model. Mobile web is inherently cross-platform, and `react-simplikit` embraces that. + +Our hooks are designed to work consistently across: + +- **iOS and Android** — the two dominant mobile platforms. +- **Various browsers** — Safari, Chrome, Samsung Internet, and more. +- **Different device form factors** — from compact phones to large-screen devices, with or without notches and home indicators. + +Where a specific API is unavailable (e.g., `window.visualViewport` in older browsers), we provide safe fallbacks that degrade gracefully without breaking your UI. + +### What's Next + +We're continuing to expand the set of mobile hooks available in `react-simplikit`, always guided by the same principle: **make mobile UI development predictable and reliable, regardless of device or OS**. If there's a common mobile UI pain point, chances are we're working on a clean, declarative solution for it. + +## Mobile-Specific Principles + +### Platform-Aware Design + +We consider the behavioral differences between iOS and Android in our implementations: + +- **Visual Viewport API differences**: + - iOS: `offsetTop` becomes negative when the keyboard appears + - Android: `offsetTop` typically remains 0 +- **Keyboard height calculation**: Platform-specific handling for accurate measurements + +### SSR Safety First + +Every hook includes SSR testing to ensure safe server-side rendering: + +```typescript +it('is safe on server side rendering', () => { + const result = renderHookSSR.serverOnly(() => useHook()); + expect(result.current).toBeDefined(); +}); +``` + +### Performance Optimization + +Mobile environments require special attention to performance: + +- **Event throttling/debouncing**: Optimize frequent events like scroll and resize +- **Passive event listeners**: Use passive listeners where applicable +- **React transitions**: Leverage `startTransition` for non-urgent updates + +## Mobile-Specific Guidelines + +### Testing on Real Devices + +- Testing on iOS Safari and Android Chrome is recommended +- Visual Viewport API behavior should be verified on real devices + +### Platform Differences + +Consider these platform differences when implementing: + +| Feature | iOS | Android | +| -------------------------- | -------------------------------------- | ------------------- | +| `visualViewport.offsetTop` | Becomes negative when keyboard appears | Typically remains 0 | +| Keyboard behavior | Viewport is pushed up | Resizes the layout | + +### window/document Access Pattern + +Always use the SSR-safe pattern when accessing browser APIs: + +```typescript +// ✅ SSR-safe pattern +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; + +// Now safe to use window/document +window.visualViewport?.addEventListener('resize', handler); +``` diff --git a/docs/mobile/contributing.md b/docs/mobile/contributing.md deleted file mode 100644 index b57817fa..00000000 --- a/docs/mobile/contributing.md +++ /dev/null @@ -1,141 +0,0 @@ -# Contributing to the Mobile Utilities - -This guide extends the [core contributing guide](/core/contributing). - -## Package Scope - -The mobile utilities in `react-simplikit` focus on **solving problems encountered in mobile web environments**. - -This includes: - -- Viewport management (visual viewport, safe area) -- Keyboard handling (avoiding keyboard-hidden content) -- Layout issues specific to iOS Safari and Android Chrome -- Scroll behavior in mobile browsers - -This package is **not** for all browser API-dependent utilities. A hook that uses browser APIs but solves a desktop or general-purpose concern (e.g., keyboard shortcuts, mouse coordinates) does not belong here. - -## Development Workflow - -``` -Scaffold → Implementation → Testing → Documentation → Review → Changeset → Merge -``` - -### 1. Scaffold - -Create the basic structure for a new hook: - -```bash -yarn scaffold useNewHook --type h # Hook -``` - -### 2. Implementation - -Follow the [Design Principles](/mobile/design-principles): - -- Named exports only -- Maximize TypeScript inference -- Apply the SSR safety pattern - -```typescript -// ✅ SSR-safe pattern -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` - -### 3. Documentation - -All exported functions must include JSDoc with 4 required tags: - -```typescript -/** - * @description One-line summary. (required) - * @param {Type} name - Description. (required if has params) - * @returns {Type} Description. (required if has return) - * @example - * const result = useHook(input); // (required) - */ -``` - -::: tip -**Do I need to write documentation?** - -No, you don't need to write documentation separately. Instead, please write detailed JSDoc comments, then run `yarn docs:gen ` to generate the English documentation from them and commit the result with your PR. Translations are maintained separately; until one exists, the page is shown in English with a notice. -::: - -### 4. Testing - -100% coverage is required: - -```bash -yarn test:spec # Run single test -yarn test:coverage # Check coverage -``` - -#### SSR Testing (Required) - -```typescript -it('is safe on server side rendering', () => { - const result = renderHookSSR.serverOnly(() => useHook()); - expect(result.current).toBeDefined(); -}); -``` - -#### Coverage Checklist - -- [ ] All if/else branches -- [ ] All switch cases -- [ ] All early returns -- [ ] Cleanup functions (useEffect return) - -### 5. Creating a Changeset - -When your code changes affect the package, you need to create a changeset: - -```bash -yarn changeset -``` - -Select the type of change: - -- `patch`: Bug fixes or minor changes -- `minor`: New features (maintaining backward compatibility) -- `major`: Breaking changes (breaking backward compatibility) - -::: tip -Both packages are currently in the `0.0.x` stage. During this phase, most changes should use `patch`. -If you're unsure about the version type, please discuss with the maintainers. -::: - -## Mobile-Specific Guidelines - -### Testing on Real Devices - -- Testing on iOS Safari and Android Chrome is recommended -- Visual Viewport API behavior should be verified on real devices - -### Platform Differences - -Consider these platform differences when implementing: - -| Feature | iOS | Android | -| -------------------------- | -------------------------------------- | ------------------- | -| `visualViewport.offsetTop` | Becomes negative when keyboard appears | Typically remains 0 | -| Keyboard behavior | Viewport is pushed up | Resizes the layout | - -### window/document Access Pattern - -Always use the SSR-safe pattern when accessing browser APIs: - -```typescript -// ✅ SSR-safe pattern -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; - -// Now safe to use window/document -window.visualViewport?.addEventListener('resize', handler); -``` - -## Documentation Contribution - -There are no specific conditions for contributing to documentation. If you find incorrect information, poor translations, or have additional content to add, feel free to make edits. Please write documentation clearly and concisely from the reader's perspective. diff --git a/docs/mobile/design-principles.md b/docs/mobile/design-principles.md deleted file mode 100644 index 5f5e72ae..00000000 --- a/docs/mobile/design-principles.md +++ /dev/null @@ -1,90 +0,0 @@ -# Design Principles - -The mobile utilities follow the core principles of `react-simplikit`, extended for mobile-specific challenges. - -## Core Principles - -### Respect React's Lifecycle Without Interference - -`react-simplikit` does not include implementations that directly interfere with React's lifecycle. -For example, it doesn't provide hooks like `useMount` or `useLifecycles`, instead favoring approaches that respect and utilize React's default behaviors. - -### Lightweight and Fast Through Zero Dependencies - -`react-simplikit` has absolutely no dependencies. By not relying on additional libraries, it minimizes bundle size when integrated into projects and eliminates concerns about performance degradation. - -### Ensures Reliability Through 100% Test Coverage - -`react-simplikit` thoroughly tests every function and branch. -We write comprehensive tests that include not only basic functionality but also SSR environment considerations for each implementation, preventing issues caused by unexpected behavior. - -### Comprehensive Documentation for Easy Understanding and Use - -`react-simplikit` provides detailed documentation to help users quickly understand and utilize each feature. The documentation includes: - -- **JSDoc Comments**: Detailed explanations of each function's behavior, parameters, and return values. -- **Usage Guides**: Clear and easy-to-follow instructions to get started immediately. -- **Practical Examples**: Examples demonstrating how to utilize implementations in real-world scenarios. - -### Type Safe with Full TypeScript Support - -`react-simplikit` is built with TypeScript from the ground up. Every hook and utility comes with: - -- **Strict Type Definitions**: All parameters, return values, and options are fully typed -- **IntelliSense Support**: Get autocompletion and inline documentation in your IDE -- **Generic Types**: Flexible APIs that preserve your type information -- **No `any` Types**: We avoid escape hatches that compromise type safety - -## API Design Standards - -### Hook Return Values - -We follow consistent patterns for hook return values: - -- **Object**: For state and related values (e.g., `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) -- **void**: For side-effect only hooks (e.g., `useBodyScrollLock(): void`) - -### Parameters - -- Required parameters come first, optional parameters last -- Use an options object for 3+ optional parameters - -### SSR Safety Pattern - -All hooks follow the SSR-safe pattern: - -```typescript -// ✅ SSR-safe - All hooks follow this pattern -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` - -## Mobile-Specific Principles - -### Platform-Aware Design - -We consider the behavioral differences between iOS and Android in our implementations: - -- **Visual Viewport API differences**: - - iOS: `offsetTop` becomes negative when the keyboard appears - - Android: `offsetTop` typically remains 0 -- **Keyboard height calculation**: Platform-specific handling for accurate measurements - -### SSR Safety First - -Every hook includes SSR testing to ensure safe server-side rendering: - -```typescript -it('is safe on server side rendering', () => { - const result = renderHookSSR.serverOnly(() => useHook()); - expect(result.current).toBeDefined(); -}); -``` - -### Performance Optimization - -Mobile environments require special attention to performance: - -- **Event throttling/debouncing**: Optimize frequent events like scroll and resize -- **Passive event listeners**: Use passive listeners where applicable -- **React transitions**: Leverage `startTransition` for non-urgent updates diff --git a/docs/mobile/intro.md b/docs/mobile/intro.md deleted file mode 100644 index 204b72fe..00000000 --- a/docs/mobile/intro.md +++ /dev/null @@ -1,123 +0,0 @@ -# Mobile Utilities - -A collection of React hooks that solve common UI challenges in mobile web environments. - -## Why mobile utilities? - -Mobile web development comes with unique challenges that don't exist on desktop: - -- **Keyboard avoidance**: Fixed bottom elements get hidden when the on-screen keyboard appears -- **Scroll direction detection**: Headers and navigation bars that show/hide based on scroll -- **Network status monitoring**: Adapting content quality based on connection speed -- **Page visibility tracking**: Pausing videos or analytics when the app goes to background -- **Visual viewport changes**: Handling zoom, keyboard, and viewport resize on mobile browsers - -`react-simplikit` provides battle-tested mobile hooks to handle these scenarios with minimal configuration. - -## Quick Start - -```bash -npm install react-simplikit -``` - -### Button CTA Example - -The most common mobile UI pattern - a fixed bottom button that moves above the keyboard: - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard(); - - return ( -
- -
- ); -} -``` - -### Chat Input Example - -A chat interface with an input field that stays above the keyboard: - -```tsx -import { useState } from 'react'; -import { useAvoidKeyboard } from 'react-simplikit'; - -function ChatInput() { - const { style } = useAvoidKeyboard(); - const [message, setMessage] = useState(''); - - return ( -
- setMessage(e.target.value)} - placeholder="Type a message..." - style={{ flex: 1 }} - /> - -
- ); -} -``` - -### With Safe Area - -For devices with home indicators (like iPhone), you can add a safe area offset: - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); - - return ( -
- -
- ); -} -``` - -## Available Hooks - -| Hook | Description | -| ----------------------------------------------- | ------------------------------------------------- | -| [useAvoidKeyboard](/hooks/useAvoidKeyboard) | Moves fixed elements above the on-screen keyboard | -| [useKeyboardHeight](/hooks/useKeyboardHeight) | Returns the current keyboard height | -| [useBodyScrollLock](/hooks/useBodyScrollLock) | Locks body scroll for modals and overlays | -| [useScrollDirection](/hooks/useScrollDirection) | Detects scroll direction (up/down) | -| [useNetworkStatus](/hooks/useNetworkStatus) | Monitors network connection status | -| [usePageVisibility](/hooks/usePageVisibility) | Tracks page visibility state | -| [useVisualViewport](/hooks/useVisualViewport) | Provides visual viewport dimensions and offset | diff --git a/docs/mobile/roadmap.md b/docs/mobile/roadmap.md deleted file mode 100644 index 681ec7ad..00000000 --- a/docs/mobile/roadmap.md +++ /dev/null @@ -1,41 +0,0 @@ -# Roadmap - -Mobile screens are small, and that small space creates a surprising number of UI challenges. Elements get hidden behind on-screen keyboards, safe areas vary by device, and the viewport the user actually sees often differs from what the browser reports. These are not edge cases — they are everyday realities of mobile development. - -## The Problem: Unreliable UI on Mobile Screens - -On mobile devices, what users see on their screen doesn't always match what developers expect. Here are a few common scenarios: - -- **Keyboard covering input fields**: When a user taps on a text input, the on-screen keyboard slides up and can completely obscure the input field or a submit button fixed at the bottom. -- **Safe area inconsistencies**: Devices with notches, rounded corners, or home indicators (like the iPhone's bottom bar) have reserved areas where content shouldn't be placed — but these vary across devices and OS versions. -- **Viewport confusion**: The browser's layout viewport and the actual visible area (the visual viewport) can differ significantly, especially when the keyboard is open or the page is zoomed. Fixed-position elements may end up in unexpected places. - -These issues are not specific to any single OS or device. Whether it's iOS Safari, Android Chrome, or any other mobile browser, the underlying challenge is the same: **the visible area is unpredictable, and standard CSS alone can't reliably account for it**. - -## Our Approach: Focus on the Visual Viewport - -The mobile utilities in `react-simplikit` take a focused approach to solving these problems. Rather than trying to work around browser quirks with brittle hacks, we center our design around the **visual viewport** — the area of the screen that the user can actually see at any given moment. - -By building on the [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API), we provide hooks that let you: - -- **Detect and respond to keyboard appearance** so that fixed-bottom elements move out of the way naturally. -- **Read safe area insets** to properly account for notches, home indicators, and other device-specific reserved areas. -- **Track the real visible area** so your layout decisions are based on what the user actually sees, not what the browser's layout engine assumes. - -The goal is simple: **within the visual viewport, UI should render reliably and predictably**. - -## Cross-Platform, Cross-Device - -We don't want to be limited to a specific OS or device model. Mobile web is inherently cross-platform, and `react-simplikit` embraces that. - -Our hooks are designed to work consistently across: - -- **iOS and Android** — the two dominant mobile platforms. -- **Various browsers** — Safari, Chrome, Samsung Internet, and more. -- **Different device form factors** — from compact phones to large-screen devices, with or without notches and home indicators. - -Where a specific API is unavailable (e.g., `window.visualViewport` in older browsers), we provide safe fallbacks that degrade gracefully without breaking your UI. - -## What's Next - -We're continuing to expand the set of mobile hooks available in `react-simplikit`, always guided by the same principle: **make mobile UI development predictable and reliable, regardless of device or OS**. If there's a common mobile UI pain point, chances are we're working on a clean, declarative solution for it. diff --git a/docs/core/why-react-simplikit-matters.md b/docs/why-react-simplikit-matters.md similarity index 100% rename from docs/core/why-react-simplikit-matters.md rename to docs/why-react-simplikit-matters.md diff --git a/docs/zh-Hans/core/contributing.md b/docs/zh-Hans/contributing.md similarity index 100% rename from docs/zh-Hans/core/contributing.md rename to docs/zh-Hans/contributing.md diff --git a/docs/zh-Hans/core/installation.md b/docs/zh-Hans/core/installation.md deleted file mode 100644 index c28354e6..00000000 --- a/docs/zh-Hans/core/installation.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: 如何安装 react-simplikit ---- - -# 安装 - -你可以使用自己喜欢的包管理器,从 [npm](https://npmjs.com/package/react-simplikit) 安装 `react-simplikit`。 - -::: code-group - -```sh [npm] -npm install react-simplikit -``` - -```sh [pnpm] -pnpm add react-simplikit -``` - -```sh [yarn] -yarn add react-simplikit -``` - -```sh [bun] -bun add react-simplikit -``` - -::: diff --git a/docs/zh-Hans/core/design-principles.md b/docs/zh-Hans/design-principles.md similarity index 100% rename from docs/zh-Hans/core/design-principles.md rename to docs/zh-Hans/design-principles.md diff --git a/docs/zh-Hans/index.md b/docs/zh-Hans/index.md index 4e76a99e..097440b9 100644 --- a/docs/zh-Hans/index.md +++ b/docs/zh-Hans/index.md @@ -9,11 +9,8 @@ hero: alt: react-simplikit actions: - theme: brand - text: 开始使用 - link: /zh-Hans/core/intro - - theme: alt - text: 移动端工具函数 - link: /zh-Hans/mobile/intro + text: 快速开始 + link: /zh-Hans/intro features: - title: '零依赖' diff --git a/docs/zh-Hans/mobile/installation.md b/docs/zh-Hans/installation.md similarity index 79% rename from docs/zh-Hans/mobile/installation.md rename to docs/zh-Hans/installation.md index 1b64dbf0..bfddebf0 100644 --- a/docs/zh-Hans/mobile/installation.md +++ b/docs/zh-Hans/installation.md @@ -1,5 +1,5 @@ --- -description: 如何为移动端 Web 安装 react-simplikit +description: 如何安装 react-simplikit --- # 安装 @@ -26,11 +26,6 @@ bun add react-simplikit ::: -## 环境要求 - -- React 18 或更高版本 -- TypeScript 4.7 或更高版本(推荐) - ## 用法 直接从这个包中导入 Hook: diff --git a/docs/zh-Hans/core/intro.md b/docs/zh-Hans/intro.md similarity index 100% rename from docs/zh-Hans/core/intro.md rename to docs/zh-Hans/intro.md diff --git a/docs/zh-Hans/mobile-web.md b/docs/zh-Hans/mobile-web.md new file mode 100644 index 00000000..e1be9b37 --- /dev/null +++ b/docs/zh-Hans/mobile-web.md @@ -0,0 +1,224 @@ +# 移动端工具函数 + +一组用于解决移动端 Web 环境中常见 UI 难题的 React Hook。 + +## 为什么需要移动端工具函数? + +移动端 Web 开发有一些桌面端不存在的独特挑战: + +- **避让键盘**:软键盘出现时,固定在底部的元素会被遮住 +- **滚动方向检测**:让页头和导航栏随滚动显示或隐藏 +- **网络状态监控**:根据连接速度调整内容质量 +- **页面可见性跟踪**:应用切到后台时暂停视频或数据统计 +- **视觉视口变化**:处理移动端浏览器上的缩放、键盘和视口尺寸变化 + +`react-simplikit` 提供经过实战检验的移动端 Hook,只需极少的配置就能应对这些场景。 + +## 快速开始 + +```bash +npm install react-simplikit +``` + +### CTA 按钮示例 + +最常见的移动端 UI 模式,就是固定在底部、会移动到键盘上方的按钮: + +```tsx +import { useAvoidKeyboard } from 'react-simplikit'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard(); + + return ( +
+ +
+ ); +} +``` + +### 聊天输入框示例 + +一个聊天界面,其中的输入框会始终停留在键盘上方: + +```tsx +import { useState } from 'react'; +import { useAvoidKeyboard } from 'react-simplikit'; + +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'; + +function FixedBottomCTA() { + const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); + + return ( +
+ +
+ ); +} +``` + +## 可用的 Hook + +| Hook | 说明 | +| ------------------------------------------------------- | ---------------------------- | +| [useAvoidKeyboard](/zh-Hans/hooks/useAvoidKeyboard) | 把固定元素移动到软键盘上方 | +| [useKeyboardHeight](/zh-Hans/hooks/useKeyboardHeight) | 返回当前的键盘高度 | +| [useBodyScrollLock](/zh-Hans/hooks/useBodyScrollLock) | 为模态框和浮层锁定 body 滚动 | +| [useScrollDirection](/zh-Hans/hooks/useScrollDirection) | 检测滚动方向(向上/向下) | +| [useNetworkStatus](/zh-Hans/hooks/useNetworkStatus) | 监控网络连接状态 | +| [usePageVisibility](/zh-Hans/hooks/usePageVisibility) | 跟踪页面可见性状态 | +| [useVisualViewport](/zh-Hans/hooks/useVisualViewport) | 提供视觉视口的尺寸和偏移量 | + +## 路线图 + +移动端的屏幕很小,而这块小小的空间会带来数量惊人的 UI 难题。元素被软键盘遮住,安全区域因设备而异,用户实际看到的视口也常常和浏览器报告的不一样。这些都不是边缘情况,而是移动端开发每天都要面对的现实。 + +### 问题:移动端屏幕上不可靠的 UI + +在移动设备上,用户在屏幕上看到的内容并不总是和开发者预期的一致。下面是几个常见的场景: + +- **键盘遮挡输入框**:用户点击文本输入框时,软键盘会滑出来,可能把输入框或固定在底部的提交按钮完全挡住。 +- **安全区域不一致**:带刘海、圆角或主屏幕指示条(比如 iPhone 底部的横条)的设备会预留出不应放置内容的区域,而这些区域在不同设备和不同系统版本上各不相同。 +- **视口混乱**:浏览器的布局视口和实际可见区域(视觉视口)可能相差很大,键盘弹出或页面被缩放时尤其明显。固定定位的元素可能会跑到意想不到的位置。 + +这些问题并不局限于某一个操作系统或某一款设备。无论是 iOS Safari、Android Chrome 还是其他移动端浏览器,底层的难题都是一样的:**可见区域难以预测,仅靠标准 CSS 无法可靠地应对**。 + +### 我们的思路:聚焦视觉视口 + +`react-simplikit` 中的移动端工具函数用一种聚焦的方式来解决这些问题。我们不去用脆弱的 hack 绕开浏览器的各种怪癖,而是把设计围绕**视觉视口**展开,也就是用户在任一时刻真正能看到的那部分屏幕区域。 + +我们基于 [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) 构建,提供的 Hook 可以让你: + +- **检测并响应键盘的出现**,让固定在底部的元素自然地让开位置。 +- **读取安全区域内边距**,从而正确处理刘海、主屏幕指示条以及其他设备特有的预留区域。 +- **跟踪真实的可见区域**,让你的布局决策基于用户实际看到的内容,而不是浏览器布局引擎的假设。 + +目标很简单:**在视觉视口内,UI 应该可靠且可预测地渲染出来**。 + +### 跨平台、跨设备 + +我们不希望被某个特定的操作系统或设备型号限制住。移动端 Web 本身就是跨平台的,`react-simplikit` 也拥抱这一点。 + +我们的 Hook 在设计上要在以下环境中表现一致: + +- **iOS 和 Android**,两大主流移动平台。 +- **各种浏览器**,包括 Safari、Chrome、Samsung Internet 等。 +- **不同的设备形态**,从小尺寸手机到大屏设备,无论有没有刘海和主屏幕指示条。 + +当某个 API 不可用时(例如旧版浏览器中的 `window.visualViewport`),我们会提供安全的回退方案,优雅降级而不会破坏你的 UI。 + +### 接下来的计划 + +我们会继续扩充 `react-simplikit` 中的移动端 Hook,并始终遵循同一条原则:**让移动端 UI 开发变得可预测、可靠,无论设备和操作系统是什么**。如果存在某个常见的移动端 UI 痛点,我们很可能正在为它准备一套简洁、声明式的解决方案。 + +## 移动端专属原则 + +### 感知平台差异的设计 + +在实现中,我们会考虑 iOS 和 Android 之间的行为差异: + +- **Visual Viewport API 的差异**: + - iOS:键盘出现时 `offsetTop` 会变成负数 + - Android:`offsetTop` 通常保持为 0 +- **键盘高度计算**:针对各平台分别处理,以获得准确的测量结果 + +### SSR 安全优先 + +每个 Hook 都包含 SSR 测试,以确保服务端渲染时的安全性: + +```typescript +it('is safe on server side rendering', () => { + const result = renderHookSSR.serverOnly(() => useHook()); + expect(result.current).toBeDefined(); +}); +``` + +### 性能优化 + +移动端环境需要特别关注性能: + +- **事件节流/防抖**:优化 scroll、resize 这类高频事件 +- **被动事件监听器**:在适用的场景中使用 passive 监听器 +- **React transition**:对非紧急的更新使用 `startTransition` + +## 移动端专属准则 + +### 在真机上测试 + +- 建议在 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/zh-Hans/mobile/contributing.md b/docs/zh-Hans/mobile/contributing.md deleted file mode 100644 index a11848ef..00000000 --- a/docs/zh-Hans/mobile/contributing.md +++ /dev/null @@ -1,141 +0,0 @@ -# 为移动端工具函数做贡献 - -本指南是在 [core 贡献指南](/zh-Hans/core/contributing) 的基础上扩展的。 - -## 包的范围 - -`react-simplikit` 中的移动端工具函数专注于**解决移动端 Web 环境中遇到的问题**。 - -其中包括: - -- 视口管理(视觉视口、安全区域) -- 键盘处理(避免内容被键盘遮挡) -- iOS Safari 和 Android Chrome 上特有的布局问题 -- 移动端浏览器中的滚动行为 - -这个包**并不**收录所有依赖浏览器 API 的工具函数。如果一个 Hook 用到了浏览器 API,但解决的是桌面端或通用场景的问题(例如快捷键、鼠标坐标),它就不属于这里。 - -## 开发流程 - -``` -脚手架 → 实现 → 测试 → 文档 → 评审 → Changeset → 合并 -``` - -### 1. 脚手架 - -为新的 Hook 创建基本结构: - -```bash -yarn scaffold useNewHook --type h # Hook -``` - -### 2. 实现 - -请遵循[设计原则](/zh-Hans/mobile/design-principles): - -- 只使用具名导出 -- 最大限度地利用 TypeScript 的类型推断 -- 应用 SSR 安全模式 - -```typescript -// ✅ SSR 安全模式 -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` - -### 3. 文档 - -所有导出的函数都必须包含带 4 个必填标签的 JSDoc: - -```typescript -/** - * @description 一句话摘要。(必填) - * @param {Type} name - 说明。(有参数时必填) - * @returns {Type} 说明。(有返回值时必填) - * @example - * const result = useHook(input); // (必填) - */ -``` - -::: tip -**我需要自己写文档吗?** - -不需要,你不用另外写文档。请改为写详细的 JSDoc 注释,然后运行 `yarn docs:gen `,它会根据 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 -- [ ] 所有提前 return -- [ ] 清理函数(useEffect 返回的函数) - -### 5. 创建 Changeset - -当你的代码改动会影响这个包时,就需要创建一个 changeset: - -```bash -yarn changeset -``` - -选择改动的类型: - -- `patch`:修复 bug 或小改动 -- `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/zh-Hans/mobile/design-principles.md b/docs/zh-Hans/mobile/design-principles.md deleted file mode 100644 index a6e664b1..00000000 --- a/docs/zh-Hans/mobile/design-principles.md +++ /dev/null @@ -1,90 +0,0 @@ -# 设计原则 - -移动端工具函数遵循 `react-simplikit` 的核心原则,并针对移动端特有的问题做了扩展。 - -## 核心原则 - -### 尊重 React 的生命周期,不加干涉 - -`react-simplikit` 不包含直接干涉 React 生命周期的实现。 -例如,它不提供 `useMount` 或 `useLifecycles` 这样的 Hook,而是采用尊重并利用 React 默认行为的方式。 - -### 通过零依赖做到轻量与快速 - -`react-simplikit` 完全没有依赖。由于不依赖任何额外的库,它把接入项目时的包体积降到最低,也让你不必担心性能下降。 - -### 通过 100% 测试覆盖率保证可靠性 - -`react-simplikit` 会彻底测试每个函数和每个分支。 -我们为每个实现编写全面的测试,不仅覆盖基本功能,还考虑到 SSR 环境的情况,从而避免非预期行为引发的问题。 - -### 完善的文档,易于理解和使用 - -`react-simplikit` 提供详细的文档,帮助用户快速理解并用好每个功能。文档包括: - -- **JSDoc 注释**:详细说明每个函数的行为、参数和返回值。 -- **使用指南**:清晰易懂的步骤,让你立刻上手。 -- **实用示例**:展示如何在真实场景中运用这些实现的示例。 - -### 完整的 TypeScript 支持带来类型安全 - -`react-simplikit` 从一开始就用 TypeScript 构建。每个 Hook 和工具函数都具备: - -- **严格的类型定义**:所有参数、返回值和选项都有完整的类型 -- **IntelliSense 支持**:在 IDE 中获得自动补全和内联文档 -- **泛型**:灵活的 API,保留你的类型信息 -- **不使用 `any` 类型**:我们避免使用会破坏类型安全的脱围机制 - -## API 设计规范 - -### Hook 的返回值 - -对于 Hook 的返回值,我们遵循一致的模式: - -- **对象**:用于状态及相关的值(例如 `useKeyboardHeight(): { keyboardHeight }`、`useVisualViewport(): { viewport }`) -- **void**:用于只有副作用的 Hook(例如 `useBodyScrollLock(): void`) - -### 参数 - -- 必填参数放在前面,可选参数放在最后 -- 可选参数达到 3 个或更多时,请使用选项对象 - -### SSR 安全模式 - -所有 Hook 都遵循 SSR 安全模式: - -```typescript -// ✅ SSR 安全:所有 Hook 都遵循这个模式 -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` - -## 移动端专属原则 - -### 感知平台差异的设计 - -在实现中,我们会考虑 iOS 和 Android 之间的行为差异: - -- **Visual Viewport API 的差异**: - - iOS:键盘出现时 `offsetTop` 会变成负数 - - Android:`offsetTop` 通常保持为 0 -- **键盘高度计算**:针对各平台分别处理,以获得准确的测量结果 - -### SSR 安全优先 - -每个 Hook 都包含 SSR 测试,以确保服务端渲染时的安全性: - -```typescript -it('is safe on server side rendering', () => { - const result = renderHookSSR.serverOnly(() => useHook()); - expect(result.current).toBeDefined(); -}); -``` - -### 性能优化 - -移动端环境需要特别关注性能: - -- **事件节流/防抖**:优化 scroll、resize 这类高频事件 -- **被动事件监听器**:在适用的场景中使用 passive 监听器 -- **React transition**:对非紧急的更新使用 `startTransition` diff --git a/docs/zh-Hans/mobile/intro.md b/docs/zh-Hans/mobile/intro.md deleted file mode 100644 index 451ba32c..00000000 --- a/docs/zh-Hans/mobile/intro.md +++ /dev/null @@ -1,123 +0,0 @@ -# 移动端工具函数 - -一组用于解决移动端 Web 环境中常见 UI 难题的 React Hook。 - -## 为什么需要移动端工具函数? - -移动端 Web 开发有一些桌面端不存在的独特挑战: - -- **避让键盘**:软键盘出现时,固定在底部的元素会被遮住 -- **滚动方向检测**:让页头和导航栏随滚动显示或隐藏 -- **网络状态监控**:根据连接速度调整内容质量 -- **页面可见性跟踪**:应用切到后台时暂停视频或数据统计 -- **视觉视口变化**:处理移动端浏览器上的缩放、键盘和视口尺寸变化 - -`react-simplikit` 提供经过实战检验的移动端 Hook,只需极少的配置就能应对这些场景。 - -## 快速开始 - -```bash -npm install react-simplikit -``` - -### CTA 按钮示例 - -最常见的移动端 UI 模式,就是固定在底部、会移动到键盘上方的按钮: - -```tsx -import { useAvoidKeyboard } from 'react-simplikit'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard(); - - return ( -
- -
- ); -} -``` - -### 聊天输入框示例 - -一个聊天界面,其中的输入框会始终停留在键盘上方: - -```tsx -import { useState } from 'react'; -import { useAvoidKeyboard } from 'react-simplikit'; - -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'; - -function FixedBottomCTA() { - const { style } = useAvoidKeyboard({ safeAreaBottom: 34 }); - - return ( -
- -
- ); -} -``` - -## 可用的 Hook - -| Hook | 说明 | -| ------------------------------------------------------- | ---------------------------- | -| [useAvoidKeyboard](/zh-Hans/hooks/useAvoidKeyboard) | 把固定元素移动到软键盘上方 | -| [useKeyboardHeight](/zh-Hans/hooks/useKeyboardHeight) | 返回当前的键盘高度 | -| [useBodyScrollLock](/zh-Hans/hooks/useBodyScrollLock) | 为模态框和浮层锁定 body 滚动 | -| [useScrollDirection](/zh-Hans/hooks/useScrollDirection) | 检测滚动方向(向上/向下) | -| [useNetworkStatus](/zh-Hans/hooks/useNetworkStatus) | 监控网络连接状态 | -| [usePageVisibility](/zh-Hans/hooks/usePageVisibility) | 跟踪页面可见性状态 | -| [useVisualViewport](/zh-Hans/hooks/useVisualViewport) | 提供视觉视口的尺寸和偏移量 | diff --git a/docs/zh-Hans/mobile/roadmap.md b/docs/zh-Hans/mobile/roadmap.md deleted file mode 100644 index 7a126083..00000000 --- a/docs/zh-Hans/mobile/roadmap.md +++ /dev/null @@ -1,41 +0,0 @@ -# 路线图 - -移动端的屏幕很小,而这块小小的空间会带来数量惊人的 UI 难题。元素被软键盘遮住,安全区域因设备而异,用户实际看到的视口也常常和浏览器报告的不一样。这些都不是边缘情况,而是移动端开发每天都要面对的现实。 - -## 问题:移动端屏幕上不可靠的 UI - -在移动设备上,用户在屏幕上看到的内容并不总是和开发者预期的一致。下面是几个常见的场景: - -- **键盘遮挡输入框**:用户点击文本输入框时,软键盘会滑出来,可能把输入框或固定在底部的提交按钮完全挡住。 -- **安全区域不一致**:带刘海、圆角或主屏幕指示条(比如 iPhone 底部的横条)的设备会预留出不应放置内容的区域,而这些区域在不同设备和不同系统版本上各不相同。 -- **视口混乱**:浏览器的布局视口和实际可见区域(视觉视口)可能相差很大,键盘弹出或页面被缩放时尤其明显。固定定位的元素可能会跑到意想不到的位置。 - -这些问题并不局限于某一个操作系统或某一款设备。无论是 iOS Safari、Android Chrome 还是其他移动端浏览器,底层的难题都是一样的:**可见区域难以预测,仅靠标准 CSS 无法可靠地应对**。 - -## 我们的思路:聚焦视觉视口 - -`react-simplikit` 中的移动端工具函数用一种聚焦的方式来解决这些问题。我们不去用脆弱的 hack 绕开浏览器的各种怪癖,而是把设计围绕**视觉视口**展开,也就是用户在任一时刻真正能看到的那部分屏幕区域。 - -我们基于 [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) 构建,提供的 Hook 可以让你: - -- **检测并响应键盘的出现**,让固定在底部的元素自然地让开位置。 -- **读取安全区域内边距**,从而正确处理刘海、主屏幕指示条以及其他设备特有的预留区域。 -- **跟踪真实的可见区域**,让你的布局决策基于用户实际看到的内容,而不是浏览器布局引擎的假设。 - -目标很简单:**在视觉视口内,UI 应该可靠且可预测地渲染出来**。 - -## 跨平台、跨设备 - -我们不希望被某个特定的操作系统或设备型号限制住。移动端 Web 本身就是跨平台的,`react-simplikit` 也拥抱这一点。 - -我们的 Hook 在设计上要在以下环境中表现一致: - -- **iOS 和 Android**,两大主流移动平台。 -- **各种浏览器**,包括 Safari、Chrome、Samsung Internet 等。 -- **不同的设备形态**,从小尺寸手机到大屏设备,无论有没有刘海和主屏幕指示条。 - -当某个 API 不可用时(例如旧版浏览器中的 `window.visualViewport`),我们会提供安全的回退方案,优雅降级而不会破坏你的 UI。 - -## 接下来的计划 - -我们会继续扩充 `react-simplikit` 中的移动端 Hook,并始终遵循同一条原则:**让移动端 UI 开发变得可预测、可靠,无论设备和操作系统是什么**。如果存在某个常见的移动端 UI 痛点,我们很可能正在为它准备一套简洁、声明式的解决方案。 diff --git a/docs/zh-Hans/core/why-react-simplikit-matters.md b/docs/zh-Hans/why-react-simplikit-matters.md similarity index 100% rename from docs/zh-Hans/core/why-react-simplikit-matters.md rename to docs/zh-Hans/why-react-simplikit-matters.md From 2cf0c9268361d394dd358a4ea2775a158ceb1229 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:23:02 +0900 Subject: [PATCH 3/7] feat(docs): add a generated reference index, single-button hero, and robots.txt --- .gitignore | 4 + .../commands/generateReferenceIndex/index.ts | 109 ++++++++++++++++++ .scripts/index.ts | 8 ++ .scripts/verifyDocsI18n.ts | 47 ++------ .vitepress/libs/buildLocaleConfig.mts | 2 +- package.json | 2 +- public/robots.txt | 3 + 7 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 .scripts/commands/generateReferenceIndex/index.ts create mode 100644 public/robots.txt diff --git a/.gitignore b/.gitignore index 74b7d081..754b41ce 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,7 @@ packages/react-simplikit/src/hooks/useUntranslatedFallbackFixture/ context/ .omc/ .omx/ + +# generated per-locale reference index +docs/reference.md +docs/*/reference.md diff --git a/.scripts/commands/generateReferenceIndex/index.ts b/.scripts/commands/generateReferenceIndex/index.ts new file mode 100644 index 00000000..ce9843c1 --- /dev/null +++ b/.scripts/commands/generateReferenceIndex/index.ts @@ -0,0 +1,109 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; + +import { localeDefinitions } from '../../../.vitepress/locales.mts'; +import { getRootPath } from '../../utils/getRootPath.ts'; + +const PACKAGE_SRC = 'packages/react-simplikit/src'; + +type Group = { labelKey: 'hooksLabel' | 'componentsLabel' | 'utilsLabel' | 'mobileWebLabel'; directories: string[] }; + +// Mirrors the sidebar grouping: the mobile trees share the flat hooks/utils URLs +// but stay a separate group so mobile web stays discoverable as a category. +const GROUPS: Group[] = [ + { labelKey: 'hooksLabel', directories: ['hooks'] }, + { labelKey: 'componentsLabel', directories: ['components'] }, + { labelKey: 'utilsLabel', directories: ['utils'] }, + { labelKey: 'mobileWebLabel', directories: ['mobile/hooks', 'mobile/utils'] }, +]; + +/** + * Extracts the first descriptive sentence from a co-located document: the first + * paragraph line after the title, cut at the end of its first sentence. + */ +async function firstSentence(markdownPath: string): Promise { + let text: string; + + try { + text = await fs.readFile(markdownPath, 'utf8'); + } catch { + return undefined; + } + + const lines = text.split('\n'); + let inFrontmatter = false; + + for (const [index, line] of lines.entries()) { + if (index === 0 && line.trim() === '---') { + inFrontmatter = true; + continue; + } + + if (inFrontmatter) { + if (line.trim() === '---') { + inFrontmatter = false; + } + continue; + } + + const trimmed = line.trim(); + + if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('<') || trimmed.startsWith(':::')) { + continue; + } + + const sentenceEnd = trimmed.search(/(?<=[.!?。])\s|(?<=[.!?。])$/); + return sentenceEnd === -1 ? trimmed : trimmed.slice(0, sentenceEnd); + } + + return undefined; +} + +/** + * Generates one reference index page per locale: every export as + * "name - first sentence of its document", grouped the same way as the sidebar. + * The output is untracked; `docs:prepare` recreates it before every build. + */ +export async function generateReferenceIndex(): Promise { + const root = getRootPath(); + + for (const definition of Object.values(localeDefinitions)) { + const localeSegment = definition.path === '' ? '' : `${definition.path}/`; + const urlPrefix = definition.path === '' ? '' : `/${definition.path}`; + const strings = definition.themeStrings; + const sections: string[] = [`# ${strings.referenceLabel}`]; + + for (const group of GROUPS) { + const items: string[] = []; + + for (const directory of group.directories) { + const category = directory.split('/').pop() as string; + const base = path.join(root, PACKAGE_SRC, directory); + const entries = await fs.readdir(base, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + const name = entry.name; + const localized = path.join(base, name, definition.path, `${name}.md`); + const english = path.join(base, name, `${name}.md`); + const description = + (definition.path === '' ? undefined : await firstSentence(localized)) ?? (await firstSentence(english)); + + items.push( + `- [${name}](${urlPrefix}/${category}/${name})${description === undefined ? '' : ` — ${description}`}` + ); + } + } + + items.sort((a, b) => a.localeCompare(b)); + sections.push(`## ${strings[group.labelKey]}`, items.join('\n')); + } + + const target = path.join(root, 'docs', localeSegment, 'reference.md'); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(target, `${sections.join('\n\n')}\n`); + } +} diff --git a/.scripts/index.ts b/.scripts/index.ts index 8ac82a36..c085d934 100644 --- a/.scripts/index.ts +++ b/.scripts/index.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { generateDocs } from './commands/generateDocs/index.ts'; +import { generateReferenceIndex } from './commands/generateReferenceIndex/index.ts'; import { generateSkill } from './commands/generateSkill/index.ts'; import { prepareLocalizedFallbacks } from './commands/prepareLocalizedFallbacks/index.ts'; import { scaffold } from './commands/scaffold/index.ts'; @@ -26,6 +27,13 @@ export function cli(args: string[]) { await prepareLocalizedFallbacks(); }); + program + .command('generate-reference-index') + .description('Generate the per-locale reference index page from the source tree') + .action(async () => { + await generateReferenceIndex(); + }); + program .command('generate-skill') .description('Generate the react-simplikit agent skill (SKILL.md + references) from the documentation pages') diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index 6d2336a3..d77a660c 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -43,7 +43,10 @@ assert.equal(generatedRewrites['generated-locales/docs/ko/index.md'], 'ko/index. assert.equal(generatedRewrites['generated-locales/docs/ja/index.md'], 'ja/index.md'); assert.equal(generatedRewrites['generated-locales/docs/zh-Hans/index.md'], 'zh-Hans/index.md'); assert.equal(generatedRewrites['generated-locales/docs/es/index.md'], 'es/index.md'); -assert.equal(packageJson.scripts['docs:prepare'], 'tsx .scripts/index.ts prepare-localized-fallbacks'); +assert.equal( + packageJson.scripts['docs:prepare'], + 'tsx .scripts/index.ts prepare-localized-fallbacks && tsx .scripts/index.ts generate-reference-index' +); assert.equal(packageJson.scripts['docs:dev'], 'yarn docs:prepare && vitepress dev'); assert.equal(packageJson.scripts['docs:build'], 'yarn docs:prepare && vitepress build'); assert.equal(gitignore.includes('generated-locales'), true); @@ -159,12 +162,7 @@ const unregisteredConfigNav = unregisteredConfig.themeConfig?.nav ?? []; assert.deepEqual(unregisteredConfigNav[0], { text: 'Início', link: '/pt-BR/' }); assert.deepEqual(unregisteredConfigNav[1], { text: 'Guide', link: '/pt-BR/intro' }); assert.equal((unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Referência'); -assert.equal( - (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/pt-BR/hooks/') || - (unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/pt-BR/intro', - true, - 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' -); +assert.equal((unregisteredConfigNav[2] as DefaultTheme.NavItemWithLink).link, '/pt-BR/reference'); assert.deepEqual(Object.keys(unregisteredConfig.themeConfig?.sidebar ?? {}), ['/pt-BR/']); assert.equal(unregisteredConfig.themeConfig?.editLink?.text, 'Editar esta página no GitHub'); @@ -175,12 +173,7 @@ const koConfigNav = koConfig.themeConfig?.nav ?? []; assert.deepEqual(koConfigNav[0], { text: '홈', link: '/ko/' }); assert.deepEqual(koConfigNav[1], { text: 'Guide', link: '/ko/intro' }); assert.equal((koConfigNav[2] as DefaultTheme.NavItemWithLink).text, '레퍼런스'); -assert.equal( - (koConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/ko/hooks/') || - (koConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ko/intro', - true, - 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' -); +assert.equal((koConfigNav[2] as DefaultTheme.NavItemWithLink).link, '/ko/reference'); assert.deepEqual((koConfig.themeConfig?.sidebar as Record)['/ko/'][0], { text: '가이드', items: [ @@ -200,12 +193,7 @@ const rootConfigNav = rootConfig.themeConfig?.nav ?? []; assert.deepEqual(rootConfigNav[0], { text: 'Home', link: '/' }); assert.deepEqual(rootConfigNav[1], { text: 'Guide', link: '/intro' }); assert.equal((rootConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Reference'); -assert.equal( - (rootConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/hooks/') || - (rootConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/intro', - true, - 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' -); +assert.equal((rootConfigNav[2] as DefaultTheme.NavItemWithLink).link, '/reference'); assert.equal(rootConfig.lang, 'en'); assert.equal(rootConfig.themeConfig?.editLink?.text, 'Edit this page on GitHub'); @@ -216,12 +204,7 @@ const jaConfigNav = jaConfig.themeConfig?.nav ?? []; assert.deepEqual(jaConfigNav[0], { text: 'ホーム', link: '/ja/' }); assert.deepEqual(jaConfigNav[1], { text: 'Guide', link: '/ja/intro' }); assert.equal((jaConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'リファレンス'); -assert.equal( - (jaConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/ja/hooks/') || - (jaConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/ja/intro', - true, - 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' -); +assert.equal((jaConfigNav[2] as DefaultTheme.NavItemWithLink).link, '/ja/reference'); assert.equal(jaConfig.themeConfig?.editLink?.text, 'GitHub で編集する'); assert.notEqual( localeDefinitions.ja.themeStrings.search, @@ -236,12 +219,7 @@ const zhHansConfigNav = zhHansConfig.themeConfig?.nav ?? []; assert.deepEqual(zhHansConfigNav[0], { text: '首页', link: '/zh-Hans/' }); assert.deepEqual(zhHansConfigNav[1], { text: 'Guide', link: '/zh-Hans/intro' }); assert.equal((zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).text, '参考'); -assert.equal( - (zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/zh-Hans/hooks/') || - (zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/zh-Hans/intro', - true, - 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' -); +assert.equal((zhHansConfigNav[2] as DefaultTheme.NavItemWithLink).link, '/zh-Hans/reference'); assert.equal(zhHansConfig.themeConfig?.editLink?.text, '在 GitHub 上编辑此页'); assert.notEqual( localeDefinitions['zh-Hans'].themeStrings.search, @@ -256,12 +234,7 @@ const esConfigNav = esConfig.themeConfig?.nav ?? []; assert.deepEqual(esConfigNav[0], { text: 'Inicio', link: '/es/' }); assert.deepEqual(esConfigNav[1], { text: 'Guide', link: '/es/intro' }); assert.equal((esConfigNav[2] as DefaultTheme.NavItemWithLink).text, 'Referencia'); -assert.equal( - (esConfigNav[2] as DefaultTheme.NavItemWithLink).link.startsWith('/es/hooks/') || - (esConfigNav[2] as DefaultTheme.NavItemWithLink).link === '/es/intro', - true, - 'the reference nav item must point at the flat hooks namespace, or fall back to the guide when a locale has no items' -); +assert.equal((esConfigNav[2] as DefaultTheme.NavItemWithLink).link, '/es/reference'); assert.equal(esConfig.themeConfig?.editLink?.text, 'Editar esta página en GitHub'); assert.notEqual( localeDefinitions.es.themeStrings.search, diff --git a/.vitepress/libs/buildLocaleConfig.mts b/.vitepress/libs/buildLocaleConfig.mts index 4220fc96..77cf3b57 100644 --- a/.vitepress/libs/buildLocaleConfig.mts +++ b/.vitepress/libs/buildLocaleConfig.mts @@ -31,7 +31,7 @@ export function buildLocaleConfig( nav: [ { text: strings.homeNavLabel, link: `${prefix}/` }, { text: 'Guide', link: `${prefix}/intro` }, - { text: strings.referenceLabel, link: hooks[0]?.link ?? `${prefix}/intro` }, + { text: strings.referenceLabel, link: `${prefix}/reference` }, ], sidebar: { [`${prefix}/`]: [ diff --git a/package.json b/package.json index 3c4efbee..4903a75b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "changeset:publish": "changeset publish", "docs:gen": "tsx .scripts/index.ts generate-docs", "skill:gen": "tsx .scripts/index.ts generate-skill", - "docs:prepare": "tsx .scripts/index.ts prepare-localized-fallbacks", + "docs:prepare": "tsx .scripts/index.ts prepare-localized-fallbacks && tsx .scripts/index.ts generate-reference-index", "docs:dev": "yarn docs:prepare && vitepress dev", "docs:build": "yarn docs:prepare && vitepress build", "docs:preview": "vitepress preview", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..922cc4fb --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Allow: / +Sitemap: https://react-simplikit.slash.page/sitemap.xml From dab3e6e6e1adad9dd85847fe34ccbc6bd2dba4d4 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:57:12 +0900 Subject: [PATCH 4/7] fix(docs): restore merged-away sections and harden the redirect stubs --- .github/CONTRIBUTING.md | 4 ++-- .gitignore | 2 +- .vitepress/libs/legacyRedirects.mts | 16 +++++++++++++++- docs/ai-integration.md | 2 +- docs/design-principles.md | 24 ++++++++++++++++++++++++ docs/es/design-principles.md | 24 ++++++++++++++++++++++++ docs/es/installation.md | 5 +++++ docs/installation.md | 5 +++++ docs/ja/design-principles.md | 24 ++++++++++++++++++++++++ docs/ja/installation.md | 5 +++++ docs/ko/ai-integration.md | 2 +- docs/ko/design-principles.md | 24 ++++++++++++++++++++++++ docs/ko/installation.md | 5 +++++ docs/zh-Hans/design-principles.md | 24 ++++++++++++++++++++++++ docs/zh-Hans/installation.md | 5 +++++ 15 files changed, 165 insertions(+), 6 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a1a3a6c4..605ff12e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -12,8 +12,8 @@ Each contribution requires: For detailed instructions, see the package-specific guides: -- [Core Package Contributing Guide](../docs/core/contributing.md) -- [Mobile Package Contributing Guide](../docs/mobile/contributing.md) +- [Contributing Guide](../docs/contributing.md) +- [Mobile Web](../docs/mobile-web.md) ## Scaffolding diff --git a/.gitignore b/.gitignore index 754b41ce..a74e65a3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ coverage generated-locales/ # transient fixtures written by `yarn test:docs`, which a killed run cannot clean up -docs/core/untranslated-fallback-fixture.md +docs/untranslated-fallback-fixture.md packages/react-simplikit/src/hooks/useUntranslatedFallbackFixture/ # next diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts index 1c6a687a..b1742952 100644 --- a/.vitepress/libs/legacyRedirects.mts +++ b/.vitepress/libs/legacyRedirects.mts @@ -22,7 +22,7 @@ const GUIDE_LEGACY: RedirectPair[] = [ { from: 'mobile/intro.html', to: 'mobile-web.html' }, { from: 'mobile/roadmap.html', to: 'mobile-web.html' }, { from: 'mobile/installation.html', to: 'installation.html' }, - { from: 'mobile/design-principles.html', to: 'design-principles.html' }, + { from: 'mobile/design-principles.html', to: 'mobile-web.html#mobile-specific-principles' }, { from: 'mobile/contributing.html', to: 'contributing.html' }, ]; @@ -78,6 +78,17 @@ export function writeLegacyRedirectStubs(outDir: string): number { const target = `/${to}`; const stubPath = path.join(outDir, from); + // The llms plugin emits a raw Markdown twin of every page. A meta refresh is + // useless to whatever fetches those, so the old path gets a copy of the new + // file instead of a stub. + const markdownSource = path.join(outDir, to.replace(/\.html(#.*)?$/, '.md')); + const markdownTarget = path.join(outDir, from.replace(/\.html$/, '.md')); + + if (fs.existsSync(markdownSource)) { + fs.mkdirSync(path.dirname(markdownTarget), { recursive: true }); + fs.copyFileSync(markdownSource, markdownTarget); + } + fs.mkdirSync(path.dirname(stubPath), { recursive: true }); fs.writeFileSync( stubPath, @@ -88,6 +99,9 @@ export function writeLegacyRedirectStubs(outDir: string): number { '', ``, ``, + // Carries the fragment and query the meta refresh would drop; the meta + // tag above stays as the no-JS fallback. + ``, '', `Redirecting to ${target}`, '', diff --git a/docs/ai-integration.md b/docs/ai-integration.md index 8f20393f..858720da 100644 --- a/docs/ai-integration.md +++ b/docs/ai-integration.md @@ -36,7 +36,7 @@ The documentation is also published in the formats agents read directly: - [`/llms.txt`](https://react-simplikit.slash.page/llms.txt) — an index of every page with a one-line summary - [`/llms-full.txt`](https://react-simplikit.slash.page/llms-full.txt) — the whole documentation in one file -- Any page with a `.md` suffix returns raw Markdown, for example [`/core/hooks/useDebounce.md`](https://react-simplikit.slash.page/core/hooks/useDebounce.md) +- Any page with a `.md` suffix returns raw Markdown, for example [`/hooks/useDebounce.md`](https://react-simplikit.slash.page/hooks/useDebounce.md) ## Context7 diff --git a/docs/design-principles.md b/docs/design-principles.md index 164cd63c..306816ec 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -35,3 +35,27 @@ While the primary documentation is in English, Korean documentation is also supp - **IntelliSense Support**: Get autocompletion and inline documentation in your IDE - **Generic Types**: Flexible APIs that preserve your type information - **No `any` Types**: We avoid escape hatches that compromise type safety + +## API Design Standards + +### Hook Return Values + +We follow consistent patterns for hook return values: + +- **Object**: For state and related values (e.g., `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) +- **void**: For side-effect only hooks (e.g., `useBodyScrollLock(): void`) + +### Parameters + +- Required parameters come first, optional parameters last +- Use an options object for 3+ optional parameters + +### SSR Safety Pattern + +All hooks follow the SSR-safe pattern: + +```typescript +// ✅ SSR-safe - All hooks follow this pattern +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/es/design-principles.md b/docs/es/design-principles.md index 2a984bd3..e4382a1b 100644 --- a/docs/es/design-principles.md +++ b/docs/es/design-principles.md @@ -35,3 +35,27 @@ Aunque la documentación principal está en inglés, también ofrecemos document - **Compatibilidad con IntelliSense**: obtén autocompletado y documentación integrada en tu IDE - **Tipos genéricos**: APIs flexibles que preservan tu información de tipos - **Sin tipos `any`**: evitamos las vías de escape que comprometen la seguridad de tipos + +## Estándares de diseño de la API + +### Valores de retorno de los Hooks + +Seguimos patrones consistentes para los valores de retorno de los Hooks: + +- **Objeto**: para el estado y los valores relacionados (por ejemplo, `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) +- **void**: para los Hooks que solo producen efectos secundarios (por ejemplo, `useBodyScrollLock(): void`) + +### Parámetros + +- Los parámetros obligatorios van primero y los opcionales al final +- Usa un objeto de opciones cuando haya 3 o más parámetros opcionales + +### Patrón de seguridad para SSR + +Todos los Hooks siguen el patrón seguro para SSR: + +```typescript +// ✅ Seguro para SSR: todos los Hooks siguen este patrón +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/es/installation.md b/docs/es/installation.md index c550d711..ec1a5552 100644 --- a/docs/es/installation.md +++ b/docs/es/installation.md @@ -26,6 +26,11 @@ bun add react-simplikit ::: +## Requisitos + +- React 18 o superior +- TypeScript 4.7 o superior (recomendado) + ## Uso Importa los Hooks directamente desde el paquete: diff --git a/docs/installation.md b/docs/installation.md index de178121..e8566c82 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -26,6 +26,11 @@ bun add react-simplikit ::: +## Requirements + +- React 18 or higher +- TypeScript 4.7 or higher (recommended) + ## Usage Import hooks directly from the package: diff --git a/docs/ja/design-principles.md b/docs/ja/design-principles.md index ee601454..e6bae379 100644 --- a/docs/ja/design-principles.md +++ b/docs/ja/design-principles.md @@ -35,3 +35,27 @@ - **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; +``` diff --git a/docs/ja/installation.md b/docs/ja/installation.md index 44ef8404..e9284391 100644 --- a/docs/ja/installation.md +++ b/docs/ja/installation.md @@ -26,6 +26,11 @@ bun add react-simplikit ::: +## 要件 + +- React 18 以上 +- TypeScript 4.7 以上(推奨) + ## 使い方 パッケージから直接フックを import してください。 diff --git a/docs/ko/ai-integration.md b/docs/ko/ai-integration.md index 955530ee..0585f6fe 100644 --- a/docs/ko/ai-integration.md +++ b/docs/ko/ai-integration.md @@ -36,7 +36,7 @@ codex plugin marketplace add https://github.com/toss/react-simplikit - [`/llms.txt`](https://react-simplikit.slash.page/llms.txt) — 모든 페이지의 목록과 한 줄 요약 - [`/llms-full.txt`](https://react-simplikit.slash.page/llms-full.txt) — 전체 문서를 하나로 합친 파일 -- 어떤 페이지든 주소 끝에 `.md`를 붙이면 원본 Markdown을 반환해요. 예: [`/core/hooks/useDebounce.md`](https://react-simplikit.slash.page/core/hooks/useDebounce.md) +- 어떤 페이지든 주소 끝에 `.md`를 붙이면 원본 Markdown을 반환해요. 예: [`/hooks/useDebounce.md`](https://react-simplikit.slash.page/hooks/useDebounce.md) ## Context7 diff --git a/docs/ko/design-principles.md b/docs/ko/design-principles.md index 58da2adc..05391a29 100644 --- a/docs/ko/design-principles.md +++ b/docs/ko/design-principles.md @@ -35,3 +35,27 @@ - **IntelliSense 지원**: IDE에서 자동완성과 인라인 문서를 제공받을 수 있어요 - **제네릭 타입**: 타입 정보를 보존하는 유연한 API를 제공해요 - **`any` 타입 없음**: 타입 안전성을 손상시키는 escape hatch를 사용하지 않아요 + +## API 설계 표준 + +### 훅 반환 값 + +훅 반환 값에 대해 일관된 패턴을 따르고 있어요: + +- **객체**: 상태와 관련 값들에 사용 (예: `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) +- **void**: 사이드 이펙트 전용 훅에 사용 (예: `useBodyScrollLock(): void`) + +### 파라미터 + +- 필수 파라미터가 먼저 오고, 선택 파라미터가 뒤에 와요 +- 3개 이상의 선택 파라미터가 있는 경우 옵션 객체를 사용해요 + +### SSR 안전 패턴 + +모든 훅은 SSR 안전 패턴을 따라요: + +```typescript +// ✅ SSR 안전 - 모든 훅이 이 패턴을 따라요 +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/ko/installation.md b/docs/ko/installation.md index f1bad6ac..45670741 100644 --- a/docs/ko/installation.md +++ b/docs/ko/installation.md @@ -26,6 +26,11 @@ bun add react-simplikit ::: +## 요구사항 + +- React 18 이상 +- TypeScript 4.7 이상 (권장) + ## 사용법 패키지에서 직접 훅을 import하세요: diff --git a/docs/zh-Hans/design-principles.md b/docs/zh-Hans/design-principles.md index 8959a66b..c519e572 100644 --- a/docs/zh-Hans/design-principles.md +++ b/docs/zh-Hans/design-principles.md @@ -35,3 +35,27 @@ - **IntelliSense 支持**:在 IDE 中获得自动补全和内联文档 - **泛型**:灵活的 API,保留你的类型信息 - **不使用 `any` 类型**:我们避免使用会破坏类型安全的脱围机制 + +## API 设计规范 + +### Hook 的返回值 + +对于 Hook 的返回值,我们遵循一致的模式: + +- **对象**:用于状态及相关的值(例如 `useKeyboardHeight(): { keyboardHeight }`、`useVisualViewport(): { viewport }`) +- **void**:用于只有副作用的 Hook(例如 `useBodyScrollLock(): void`) + +### 参数 + +- 必填参数放在前面,可选参数放在最后 +- 可选参数达到 3 个或更多时,请使用选项对象 + +### SSR 安全模式 + +所有 Hook 都遵循 SSR 安全模式: + +```typescript +// ✅ SSR 安全:所有 Hook 都遵循这个模式 +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/zh-Hans/installation.md b/docs/zh-Hans/installation.md index bfddebf0..18096f0a 100644 --- a/docs/zh-Hans/installation.md +++ b/docs/zh-Hans/installation.md @@ -26,6 +26,11 @@ bun add react-simplikit ::: +## 环境要求 + +- React 18 或更高版本 +- TypeScript 4.7 或更高版本(推荐) + ## 用法 直接从这个包中导入 Hook: From b4ec6fe4b793e39621b6dc076e3656a55d0436ed Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:38:46 +0900 Subject: [PATCH 5/7] fix(docs): scope the mobile API standards, fix redirect signals, and lock the stubs with tests --- .github/CONTRIBUTING.md | 4 +- .../commands/generateReferenceIndex/index.ts | 3 +- .scripts/utils/assertLlmsOutput.ts | 2 +- .scripts/verifyDocsI18n.ts | 45 ++++++++++++++++++- .vitepress/libs/legacyRedirects.mts | 18 ++++++-- docs/design-principles.md | 24 ---------- docs/es/design-principles.md | 24 ---------- docs/es/index.md | 2 +- docs/es/mobile-web.md | 28 +++++++++++- docs/ja/design-principles.md | 24 ---------- docs/ja/mobile-web.md | 28 +++++++++++- docs/ko/design-principles.md | 24 ---------- docs/ko/mobile-web.md | 28 +++++++++++- docs/mobile-web.md | 28 +++++++++++- docs/zh-Hans/design-principles.md | 24 ---------- docs/zh-Hans/index.md | 2 +- docs/zh-Hans/mobile-web.md | 28 +++++++++++- packages/react-simplikit/README-es.md | 2 +- packages/react-simplikit/README-ja_jp.md | 2 +- packages/react-simplikit/README-ko_kr.md | 2 +- packages/react-simplikit/README-zh_hans.md | 2 +- packages/react-simplikit/README.md | 2 +- 22 files changed, 201 insertions(+), 145 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 605ff12e..3585f971 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -6,7 +6,7 @@ Welcome! We appreciate your interest in contributing to react-simplikit. This do Each contribution requires: -- **Implementation** — following our [Design Principles](https://react-simplikit.slash.page/core/design-principles.html) +- **Implementation** — following our [Design Principles](https://react-simplikit.slash.page/design-principles.html) - **Test Code** — 100% coverage required - **JSDoc** — documentation is auto-generated from JSDoc, so no separate docs needed @@ -41,5 +41,5 @@ Select the version bump type (`patch`, `minor`, or `major`). ## Useful Links - [Documentation Site](https://react-simplikit.slash.page) -- [Design Principles](https://react-simplikit.slash.page/core/design-principles.html) +- [Design Principles](https://react-simplikit.slash.page/design-principles.html) - [Discord](https://discord.gg/vGXbVjP2nY) — Community chat for questions and discussions diff --git a/.scripts/commands/generateReferenceIndex/index.ts b/.scripts/commands/generateReferenceIndex/index.ts index ce9843c1..dbc4e5c3 100644 --- a/.scripts/commands/generateReferenceIndex/index.ts +++ b/.scripts/commands/generateReferenceIndex/index.ts @@ -71,7 +71,8 @@ export async function generateReferenceIndex(): Promise { const localeSegment = definition.path === '' ? '' : `${definition.path}/`; const urlPrefix = definition.path === '' ? '' : `/${definition.path}`; const strings = definition.themeStrings; - const sections: string[] = [`# ${strings.referenceLabel}`]; + // The page is generated and gitignored, so an edit link would 404. + const sections: string[] = ['---', 'editLink: false', '---', '', `# ${strings.referenceLabel}`]; for (const group of GROUPS) { const items: string[] = []; diff --git a/.scripts/utils/assertLlmsOutput.ts b/.scripts/utils/assertLlmsOutput.ts index a8a5820b..9e39f66b 100644 --- a/.scripts/utils/assertLlmsOutput.ts +++ b/.scripts/utils/assertLlmsOutput.ts @@ -12,7 +12,7 @@ type AssertLlmsOutputOptions = { const PACKAGE_INDEX_FILE = 'packages/react-simplikit/src/index.ts'; // The generated links are absolute (the plugin's `domain` option), and every documentation page -// lives under core/ or mobile/. A ko/ or ja/ link means the localized copies leaked into the +// lives in a flat reference namespace (hooks/components/utils) or at the root as a guide. // listing, which would make an agent read the same page several times in different languages. const ALLOWED_LINK = /^https:\/\/react-simplikit\.slash\.page\/(?:(?:hooks|components|utils)\/)?[^/]+\.md$/; diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index d77a660c..d45a105e 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -6,7 +6,14 @@ import { DefaultTheme } from 'vitepress'; import { buildLocaleConfig } from '../.vitepress/libs/buildLocaleConfig.mts'; import { getSidebarItems } from '../.vitepress/libs/getSidebarItems.mts'; -import { generatedLocalesDirectory, generatedRewrites, localeDefinitions, rewrites } from '../.vitepress/locales.mts'; +import { collectLegacyRedirects } from '../.vitepress/libs/legacyRedirects.mts'; +import { + generatedLocalesDirectory, + generatedRewrites, + localeDefinitions, + localeDirectories, + rewrites, +} from '../.vitepress/locales.mts'; import { corePackageRoot } from '../.vitepress/shared.mts'; import { assertLlmsOutput } from './utils/assertLlmsOutput.ts'; @@ -90,6 +97,42 @@ try { await assertLlmsOutput({ buildOutputDirectory, root }); + // The redirect stubs are the only thing keeping pre-flattening URLs alive, and a + // broken route filter would silently emit none of them. + const stubs = collectLegacyRedirects(); + const localeCount = localeDirectories.length + 1; + const referenceItemCount = ( + await Promise.all( + ['hooks', 'components', 'utils', 'mobile/hooks', 'mobile/utils'].map( + async directory => + ( + await fs.readdir(path.join(root, 'packages/react-simplikit/src', directory), { withFileTypes: true }) + ).filter(entry => entry.isDirectory()).length + ) + ) + ).reduce((total, count) => total + count, 0); + const guidePageCount = 11; + + assert.equal( + stubs.length, + (guidePageCount + referenceItemCount) * localeCount, + 'the legacy redirect set must cover every pre-flattening URL across all locales' + ); + + for (const { from, to } of stubs) { + const stub = await fs.readFile(path.join(buildOutputDirectory, from), 'utf8'); + assert.match(stub, /http-equiv="refresh"/, `${from} must redirect`); + assert.equal(stub.includes('noindex'), false, `${from} must stay indexable so canonical can consolidate signals`); + await fs.access(path.join(buildOutputDirectory, to.split('#')[0])); + } + + // The reference index is generated, so a broken generator would leave the nav + // pointing at a page that does not exist. + for (const locale of ['', ...localeDirectories]) { + const referencePage = await fs.readFile(path.join(buildOutputDirectory, locale, 'reference.html'), 'utf8'); + assert.match(referencePage, /\/hooks\/useToggle/, `${locale || 'root'} reference index must list the exports`); + } + const fallbackPage = await fs.readFile( path.join(buildOutputDirectory, 'ko/untranslated-fallback-fixture.html'), 'utf8' diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts index b1742952..1d41119e 100644 --- a/.vitepress/libs/legacyRedirects.mts +++ b/.vitepress/libs/legacyRedirects.mts @@ -8,6 +8,19 @@ const SITE_ORIGIN = 'https://react-simplikit.slash.page'; type RedirectPair = { from: string; to: string }; +/** + * Rebuilds the URL from its parts so the query and fragment a meta refresh would + * drop survive. A target that already carries a fragment keeps it unless the + * incoming URL has one of its own, and the query always lands before the hash. + */ +function REDIRECT_SCRIPT(target: string): string { + const [pathname, fragment] = target.split('#'); + return ( + `location.replace(${JSON.stringify(pathname)} + location.search + ` + + `(location.hash || ${JSON.stringify(fragment === undefined ? '' : `#${fragment}`)}))` + ); +} + /** * Guide pages moved with per-page targets (the merge folded eleven pages into * seven), so they are listed explicitly instead of derived from a pattern. @@ -98,11 +111,10 @@ export function writeLegacyRedirectStubs(outDir: string): number { '', '', ``, - ``, + ``, // Carries the fragment and query the meta refresh would drop; the meta // tag above stays as the no-JS fallback. - ``, - '', + ``, `Redirecting to ${target}`, '', `

This page moved to ${target}.

`, diff --git a/docs/design-principles.md b/docs/design-principles.md index 306816ec..164cd63c 100644 --- a/docs/design-principles.md +++ b/docs/design-principles.md @@ -35,27 +35,3 @@ While the primary documentation is in English, Korean documentation is also supp - **IntelliSense Support**: Get autocompletion and inline documentation in your IDE - **Generic Types**: Flexible APIs that preserve your type information - **No `any` Types**: We avoid escape hatches that compromise type safety - -## API Design Standards - -### Hook Return Values - -We follow consistent patterns for hook return values: - -- **Object**: For state and related values (e.g., `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) -- **void**: For side-effect only hooks (e.g., `useBodyScrollLock(): void`) - -### Parameters - -- Required parameters come first, optional parameters last -- Use an options object for 3+ optional parameters - -### SSR Safety Pattern - -All hooks follow the SSR-safe pattern: - -```typescript -// ✅ SSR-safe - All hooks follow this pattern -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` diff --git a/docs/es/design-principles.md b/docs/es/design-principles.md index e4382a1b..2a984bd3 100644 --- a/docs/es/design-principles.md +++ b/docs/es/design-principles.md @@ -35,27 +35,3 @@ Aunque la documentación principal está en inglés, también ofrecemos document - **Compatibilidad con IntelliSense**: obtén autocompletado y documentación integrada en tu IDE - **Tipos genéricos**: APIs flexibles que preservan tu información de tipos - **Sin tipos `any`**: evitamos las vías de escape que comprometen la seguridad de tipos - -## Estándares de diseño de la API - -### Valores de retorno de los Hooks - -Seguimos patrones consistentes para los valores de retorno de los Hooks: - -- **Objeto**: para el estado y los valores relacionados (por ejemplo, `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) -- **void**: para los Hooks que solo producen efectos secundarios (por ejemplo, `useBodyScrollLock(): void`) - -### Parámetros - -- Los parámetros obligatorios van primero y los opcionales al final -- Usa un objeto de opciones cuando haya 3 o más parámetros opcionales - -### Patrón de seguridad para SSR - -Todos los Hooks siguen el patrón seguro para SSR: - -```typescript -// ✅ Seguro para SSR: todos los Hooks siguen este patrón -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` diff --git a/docs/es/index.md b/docs/es/index.md index 5ebfa448..40d6853f 100644 --- a/docs/es/index.md +++ b/docs/es/index.md @@ -9,7 +9,7 @@ hero: alt: react-simplikit actions: - theme: brand - text: Comenzar + text: Empezar link: /es/intro features: diff --git a/docs/es/mobile-web.md b/docs/es/mobile-web.md index 1fae1859..2d50254d 100644 --- a/docs/es/mobile-web.md +++ b/docs/es/mobile-web.md @@ -1,4 +1,4 @@ -# Utilidades para móvil +# Web móvil Una colección de Hooks de React que resuelven los retos de interfaz más habituales en entornos de web móvil. @@ -164,7 +164,7 @@ Cuando una API concreta no está disponible (por ejemplo, `window.visualViewport Seguimos ampliando el conjunto de Hooks para móvil disponibles en `react-simplikit`, siempre guiados por el mismo principio: **hacer que el desarrollo de interfaces móviles sea predecible y fiable, sea cual sea el dispositivo o el sistema operativo**. Si existe un problema habitual de interfaz en móvil, lo más probable es que estemos trabajando en una solución limpia y declarativa para él. -## Principios específicos para móvil +## Principios específicos para móvil {#mobile-specific-principles} ### Diseño consciente de la plataforma @@ -222,3 +222,27 @@ if (!isClient) return defaultValue; // Ahora es seguro usar window/document window.visualViewport?.addEventListener('resize', handler); ``` + +## Estándares de diseño de la API + +### Valores de retorno de los Hooks + +Seguimos patrones consistentes para los valores de retorno de los Hooks: + +- **Objeto**: para el estado y los valores relacionados (por ejemplo, `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) +- **void**: para los Hooks que solo producen efectos secundarios (por ejemplo, `useBodyScrollLock(): void`) + +### Parámetros + +- Los parámetros obligatorios van primero y los opcionales al final +- Usa un objeto de opciones cuando haya 3 o más parámetros opcionales + +### Patrón de seguridad para SSR + +Todos los Hooks siguen el patrón seguro para SSR: + +```typescript +// ✅ Seguro para SSR: todos los Hooks siguen este patrón +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/ja/design-principles.md b/docs/ja/design-principles.md index e6bae379..ee601454 100644 --- a/docs/ja/design-principles.md +++ b/docs/ja/design-principles.md @@ -35,27 +35,3 @@ - **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; -``` diff --git a/docs/ja/mobile-web.md b/docs/ja/mobile-web.md index 852c8ebe..490860e5 100644 --- a/docs/ja/mobile-web.md +++ b/docs/ja/mobile-web.md @@ -1,4 +1,4 @@ -# モバイルユーティリティ +# モバイル Web モバイル Web 環境でよくある UI の課題を解決する React フック集です。 @@ -164,7 +164,7 @@ function FixedBottomCTA() { `react-simplikit` で提供するモバイルフックのラインナップを、常に同じ原則に基づいて拡張し続けています。**端末や OS を問わず、モバイル UI 開発を予測可能で信頼できるものにする**という原則です。よくあるモバイル UI の悩みがあれば、私たちはそのためのクリーンで宣言的な解決策に取り組んでいる可能性が高いです。 -## モバイル特有の原則 +## モバイル特有の原則 {#mobile-specific-principles} ### プラットフォームを意識した設計 @@ -222,3 +222,27 @@ if (!isClient) return defaultValue; // これで window/document を安全に使用できます window.visualViewport?.addEventListener('resize', handler); ``` + +## API 設計基準 + +### フックの戻り値 + +フックの戻り値については、一貫したパターンに従います。 + +- **オブジェクト**: 状態や関連する値を返す場合(例: `useKeyboardHeight(): { keyboardHeight }`、`useVisualViewport(): { viewport }`) +- **void**: 副作用のみを持つフックの場合(例: `useBodyScrollLock(): void`) + +### パラメータ + +- 必須パラメータを先に、任意パラメータを後に配置します +- 任意パラメータが 3 個以上ある場合はオプションオブジェクトを使用します + +### SSR 安全パターン + +すべてのフックは SSR 安全パターンに従います。 + +```typescript +// ✅ SSR 安全 - すべてのフックがこのパターンに従います +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/ko/design-principles.md b/docs/ko/design-principles.md index 05391a29..58da2adc 100644 --- a/docs/ko/design-principles.md +++ b/docs/ko/design-principles.md @@ -35,27 +35,3 @@ - **IntelliSense 지원**: IDE에서 자동완성과 인라인 문서를 제공받을 수 있어요 - **제네릭 타입**: 타입 정보를 보존하는 유연한 API를 제공해요 - **`any` 타입 없음**: 타입 안전성을 손상시키는 escape hatch를 사용하지 않아요 - -## API 설계 표준 - -### 훅 반환 값 - -훅 반환 값에 대해 일관된 패턴을 따르고 있어요: - -- **객체**: 상태와 관련 값들에 사용 (예: `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) -- **void**: 사이드 이펙트 전용 훅에 사용 (예: `useBodyScrollLock(): void`) - -### 파라미터 - -- 필수 파라미터가 먼저 오고, 선택 파라미터가 뒤에 와요 -- 3개 이상의 선택 파라미터가 있는 경우 옵션 객체를 사용해요 - -### SSR 안전 패턴 - -모든 훅은 SSR 안전 패턴을 따라요: - -```typescript -// ✅ SSR 안전 - 모든 훅이 이 패턴을 따라요 -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` diff --git a/docs/ko/mobile-web.md b/docs/ko/mobile-web.md index 3c0c9139..4ae043d0 100644 --- a/docs/ko/mobile-web.md +++ b/docs/ko/mobile-web.md @@ -1,4 +1,4 @@ -# 모바일 유틸리티 +# 모바일 웹 모바일 웹 환경에서 발생하는 다양한 UI 문제를 해결하는 React 훅 모음이에요. @@ -164,7 +164,7 @@ function FixedBottomCTA() { `react-simplikit`에서 제공하는 모바일 훅들을 계속 확장해 나갈 예정이에요. 항상 같은 원칙에 따라: **기기나 OS에 관계없이 모바일 UI 개발을 예측 가능하고 안정적으로 만드는 것**이에요. 모바일 UI에서 흔히 겪는 불편함이 있다면, 우리는 그것에 대한 깔끔하고 선언적인 해결책을 만들고 있을 거예요. -## 모바일 특화 원칙 +## 모바일 특화 원칙 {#mobile-specific-principles} ### 플랫폼 인식 설계 @@ -222,3 +222,27 @@ if (!isClient) return defaultValue; // 이제 window/document를 안전하게 사용할 수 있어요 window.visualViewport?.addEventListener('resize', handler); ``` + +## API 설계 표준 + +### 훅 반환 값 + +훅 반환 값에 대해 일관된 패턴을 따르고 있어요: + +- **객체**: 상태와 관련 값들에 사용 (예: `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) +- **void**: 사이드 이펙트 전용 훅에 사용 (예: `useBodyScrollLock(): void`) + +### 파라미터 + +- 필수 파라미터가 먼저 오고, 선택 파라미터가 뒤에 와요 +- 3개 이상의 선택 파라미터가 있는 경우 옵션 객체를 사용해요 + +### SSR 안전 패턴 + +모든 훅은 SSR 안전 패턴을 따라요: + +```typescript +// ✅ SSR 안전 - 모든 훅이 이 패턴을 따라요 +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/mobile-web.md b/docs/mobile-web.md index 293dd3c4..138e5b15 100644 --- a/docs/mobile-web.md +++ b/docs/mobile-web.md @@ -1,4 +1,4 @@ -# Mobile Utilities +# Mobile Web A collection of React hooks that solve common UI challenges in mobile web environments. @@ -164,7 +164,7 @@ Where a specific API is unavailable (e.g., `window.visualViewport` in older brow We're continuing to expand the set of mobile hooks available in `react-simplikit`, always guided by the same principle: **make mobile UI development predictable and reliable, regardless of device or OS**. If there's a common mobile UI pain point, chances are we're working on a clean, declarative solution for it. -## Mobile-Specific Principles +## Mobile-Specific Principles {#mobile-specific-principles} ### Platform-Aware Design @@ -222,3 +222,27 @@ if (!isClient) return defaultValue; // Now safe to use window/document window.visualViewport?.addEventListener('resize', handler); ``` + +## API Design Standards + +### Hook Return Values + +We follow consistent patterns for hook return values: + +- **Object**: For state and related values (e.g., `useKeyboardHeight(): { keyboardHeight }`, `useVisualViewport(): { viewport }`) +- **void**: For side-effect only hooks (e.g., `useBodyScrollLock(): void`) + +### Parameters + +- Required parameters come first, optional parameters last +- Use an options object for 3+ optional parameters + +### SSR Safety Pattern + +All hooks follow the SSR-safe pattern: + +```typescript +// ✅ SSR-safe - All hooks follow this pattern +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/docs/zh-Hans/design-principles.md b/docs/zh-Hans/design-principles.md index c519e572..8959a66b 100644 --- a/docs/zh-Hans/design-principles.md +++ b/docs/zh-Hans/design-principles.md @@ -35,27 +35,3 @@ - **IntelliSense 支持**:在 IDE 中获得自动补全和内联文档 - **泛型**:灵活的 API,保留你的类型信息 - **不使用 `any` 类型**:我们避免使用会破坏类型安全的脱围机制 - -## API 设计规范 - -### Hook 的返回值 - -对于 Hook 的返回值,我们遵循一致的模式: - -- **对象**:用于状态及相关的值(例如 `useKeyboardHeight(): { keyboardHeight }`、`useVisualViewport(): { viewport }`) -- **void**:用于只有副作用的 Hook(例如 `useBodyScrollLock(): void`) - -### 参数 - -- 必填参数放在前面,可选参数放在最后 -- 可选参数达到 3 个或更多时,请使用选项对象 - -### SSR 安全模式 - -所有 Hook 都遵循 SSR 安全模式: - -```typescript -// ✅ SSR 安全:所有 Hook 都遵循这个模式 -const isClient = typeof window !== 'undefined'; -if (!isClient) return defaultValue; -``` diff --git a/docs/zh-Hans/index.md b/docs/zh-Hans/index.md index 097440b9..6bc0b27e 100644 --- a/docs/zh-Hans/index.md +++ b/docs/zh-Hans/index.md @@ -9,7 +9,7 @@ hero: alt: react-simplikit actions: - theme: brand - text: 快速开始 + text: 开始使用 link: /zh-Hans/intro features: diff --git a/docs/zh-Hans/mobile-web.md b/docs/zh-Hans/mobile-web.md index e1be9b37..9ac68b58 100644 --- a/docs/zh-Hans/mobile-web.md +++ b/docs/zh-Hans/mobile-web.md @@ -1,4 +1,4 @@ -# 移动端工具函数 +# 移动端 Web 一组用于解决移动端 Web 环境中常见 UI 难题的 React Hook。 @@ -164,7 +164,7 @@ function FixedBottomCTA() { 我们会继续扩充 `react-simplikit` 中的移动端 Hook,并始终遵循同一条原则:**让移动端 UI 开发变得可预测、可靠,无论设备和操作系统是什么**。如果存在某个常见的移动端 UI 痛点,我们很可能正在为它准备一套简洁、声明式的解决方案。 -## 移动端专属原则 +## 移动端专属原则 {#mobile-specific-principles} ### 感知平台差异的设计 @@ -222,3 +222,27 @@ if (!isClient) return defaultValue; // 现在可以安全地使用 window/document 了 window.visualViewport?.addEventListener('resize', handler); ``` + +## API 设计规范 + +### Hook 的返回值 + +对于 Hook 的返回值,我们遵循一致的模式: + +- **对象**:用于状态及相关的值(例如 `useKeyboardHeight(): { keyboardHeight }`、`useVisualViewport(): { viewport }`) +- **void**:用于只有副作用的 Hook(例如 `useBodyScrollLock(): void`) + +### 参数 + +- 必填参数放在前面,可选参数放在最后 +- 可选参数达到 3 个或更多时,请使用选项对象 + +### SSR 安全模式 + +所有 Hook 都遵循 SSR 安全模式: + +```typescript +// ✅ SSR 安全:所有 Hook 都遵循这个模式 +const isClient = typeof window !== 'undefined'; +if (!isClient) return defaultValue; +``` diff --git a/packages/react-simplikit/README-es.md b/packages/react-simplikit/README-es.md index fdd76b1f..2c75572f 100644 --- a/packages/react-simplikit/README-es.md +++ b/packages/react-simplikit/README-es.md @@ -92,7 +92,7 @@ Consulta la documentación completa en [react-simplikit.slash.page](https://reac ## Paquetes relacionados -- [Utilidades para la web móvil](https://react-simplikit.slash.page/es/mobile/intro.html) - se incluyen en `react-simplikit` +- [Utilidades para la web móvil](https://react-simplikit.slash.page/es/mobile-web.html) - se incluyen en `react-simplikit` ## Contribuir diff --git a/packages/react-simplikit/README-ja_jp.md b/packages/react-simplikit/README-ja_jp.md index 6d665ad6..98157392 100644 --- a/packages/react-simplikit/README-ja_jp.md +++ b/packages/react-simplikit/README-ja_jp.md @@ -92,7 +92,7 @@ function SearchInput() { ## 関連パッケージ -- [モバイル Web ユーティリティ](https://react-simplikit.slash.page/ja/mobile/intro.html) - `react-simplikit` に含まれます +- [モバイル Web ユーティリティ](https://react-simplikit.slash.page/ja/mobile-web.html) - `react-simplikit` に含まれます ## 貢献 diff --git a/packages/react-simplikit/README-ko_kr.md b/packages/react-simplikit/README-ko_kr.md index 46e994bd..70de1d85 100644 --- a/packages/react-simplikit/README-ko_kr.md +++ b/packages/react-simplikit/README-ko_kr.md @@ -92,7 +92,7 @@ function SearchInput() { ## 관련 패키지 -- [모바일 웹 유틸리티](https://react-simplikit.slash.page/ko/mobile/intro.html) - `react-simplikit`에 포함 +- [모바일 웹 유틸리티](https://react-simplikit.slash.page/ko/mobile-web.html) - `react-simplikit`에 포함 ## 기여하기 diff --git a/packages/react-simplikit/README-zh_hans.md b/packages/react-simplikit/README-zh_hans.md index f75c97c1..3188cf4b 100644 --- a/packages/react-simplikit/README-zh_hans.md +++ b/packages/react-simplikit/README-zh_hans.md @@ -92,7 +92,7 @@ function SearchInput() { ## 相关包 -- [移动端 Web 工具函数](https://react-simplikit.slash.page/zh-Hans/mobile/intro.html) - 已包含在 `react-simplikit` 中 +- [移动端 Web 工具函数](https://react-simplikit.slash.page/zh-Hans/mobile-web.html) - 已包含在 `react-simplikit` 中 ## 贡献 diff --git a/packages/react-simplikit/README.md b/packages/react-simplikit/README.md index d26c7f74..75fc56cf 100644 --- a/packages/react-simplikit/README.md +++ b/packages/react-simplikit/README.md @@ -92,7 +92,7 @@ Visit [react-simplikit.slash.page](https://react-simplikit.slash.page) for full ## Related Packages -- [Mobile web utilities](https://react-simplikit.slash.page/mobile/intro.html) - included in `react-simplikit` +- [Mobile web utilities](https://react-simplikit.slash.page/mobile-web.html) - included in `react-simplikit` ## Contributing From b745d48eaa93d2dca7c2a3e33d9a5de095153f72 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:10:49 +0900 Subject: [PATCH 6/7] fix(docs): order the docs pipeline, anchor moved sections, and restore contribution rules --- .../commands/generateReferenceIndex/index.ts | 24 +++++++++++++++---- .scripts/verifyDocsI18n.ts | 22 +++++++++++++++-- .vitepress/libs/legacyRedirects.mts | 16 ++++++++++--- docs/contributing.md | 17 +++++++++++++ docs/es/contributing.md | 17 +++++++++++++ docs/es/installation.md | 2 +- docs/es/mobile-web.md | 4 ++-- docs/installation.md | 2 +- docs/ja/contributing.md | 17 +++++++++++++ docs/ja/installation.md | 2 +- docs/ja/mobile-web.md | 4 ++-- docs/ko/contributing.md | 17 +++++++++++++ docs/ko/installation.md | 2 +- docs/ko/mobile-web.md | 4 ++-- docs/mobile-web.md | 4 ++-- docs/zh-Hans/contributing.md | 17 +++++++++++++ docs/zh-Hans/installation.md | 2 +- docs/zh-Hans/mobile-web.md | 4 ++-- package.json | 2 +- 19 files changed, 153 insertions(+), 26 deletions(-) diff --git a/.scripts/commands/generateReferenceIndex/index.ts b/.scripts/commands/generateReferenceIndex/index.ts index dbc4e5c3..63f18673 100644 --- a/.scripts/commands/generateReferenceIndex/index.ts +++ b/.scripts/commands/generateReferenceIndex/index.ts @@ -71,8 +71,8 @@ export async function generateReferenceIndex(): Promise { const localeSegment = definition.path === '' ? '' : `${definition.path}/`; const urlPrefix = definition.path === '' ? '' : `/${definition.path}`; const strings = definition.themeStrings; - // The page is generated and gitignored, so an edit link would 404. - const sections: string[] = ['---', 'editLink: false', '---', '', `# ${strings.referenceLabel}`]; + let untranslatedCount = 0; + const sections: string[] = [`# ${strings.referenceLabel}`]; for (const group of GROUPS) { const items: string[] = []; @@ -90,8 +90,12 @@ export async function generateReferenceIndex(): Promise { const name = entry.name; const localized = path.join(base, name, definition.path, `${name}.md`); const english = path.join(base, name, `${name}.md`); - const description = - (definition.path === '' ? undefined : await firstSentence(localized)) ?? (await firstSentence(english)); + const localizedDescription = definition.path === '' ? undefined : await firstSentence(localized); + const description = localizedDescription ?? (await firstSentence(english)); + + if (definition.path !== '' && localizedDescription === undefined) { + untranslatedCount += 1; + } items.push( `- [${name}](${urlPrefix}/${category}/${name})${description === undefined ? '' : ` — ${description}`}` @@ -103,8 +107,18 @@ export async function generateReferenceIndex(): Promise { sections.push(`## ${strings[group.labelKey]}`, items.join('\n')); } + // Descriptions fall back to the English documents whenever a locale has none, + // so the page carries the same untranslated banner an individual fallback gets. + const frontmatter = ['---', 'editLink: false']; + + if (definition.path !== '' && untranslatedCount > 0) { + frontmatter.push('untranslated: true', 'sourceLocale: en'); + } + + frontmatter.push('---'); + const target = path.join(root, 'docs', localeSegment, 'reference.md'); await fs.mkdir(path.dirname(target), { recursive: true }); - await fs.writeFile(target, `${sections.join('\n\n')}\n`); + await fs.writeFile(target, `${frontmatter.join('\n')}\n\n${sections.join('\n\n')}\n`); } } diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index d45a105e..1ef621db 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -52,7 +52,7 @@ assert.equal(generatedRewrites['generated-locales/docs/zh-Hans/index.md'], 'zh-H assert.equal(generatedRewrites['generated-locales/docs/es/index.md'], 'es/index.md'); assert.equal( packageJson.scripts['docs:prepare'], - 'tsx .scripts/index.ts prepare-localized-fallbacks && tsx .scripts/index.ts generate-reference-index' + 'tsx .scripts/index.ts generate-reference-index && 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'); @@ -119,6 +119,19 @@ try { 'the legacy redirect set must cover every pre-flattening URL across all locales' ); + // Spot-check the shape itself: a renamed `from` would keep the count intact and + // still write a file, so the count alone cannot catch it. + const stubPaths = new Set(stubs.map(stub => stub.from)); + for (const expected of [ + 'core/hooks/useToggle.html', + 'mobile/hooks/useKeyboardHeight.html', + 'mobile/roadmap.html', + 'ko/core/utils/mergeRefs.html', + 'ja/mobile/utils/isServer.html', + ]) { + assert.equal(stubPaths.has(expected), true, `the legacy URL ${expected} must keep a redirect`); + } + for (const { from, to } of stubs) { const stub = await fs.readFile(path.join(buildOutputDirectory, from), 'utf8'); assert.match(stub, /http-equiv="refresh"/, `${from} must redirect`); @@ -130,7 +143,12 @@ try { // pointing at a page that does not exist. for (const locale of ['', ...localeDirectories]) { const referencePage = await fs.readFile(path.join(buildOutputDirectory, locale, 'reference.html'), 'utf8'); - assert.match(referencePage, /\/hooks\/useToggle/, `${locale || 'root'} reference index must list the exports`); + const renderedLinks = [...referencePage.matchAll(/
  • = { + '#core-principles': '#mobile-specific-principles', +}; + function REDIRECT_SCRIPT(target: string): string { const [pathname, fragment] = target.split('#'); + const fallback = fragment === undefined ? '' : `#${fragment}`; return ( + `var r=${JSON.stringify(RETIRED_ANCHORS)};` + `location.replace(${JSON.stringify(pathname)} + location.search + ` + - `(location.hash || ${JSON.stringify(fragment === undefined ? '' : `#${fragment}`)}))` + `(r[location.hash] || location.hash || ${JSON.stringify(fallback)}))` ); } @@ -33,10 +43,10 @@ const GUIDE_LEGACY: RedirectPair[] = [ { from: 'core/design-principles.html', to: 'design-principles.html' }, { from: 'core/contributing.html', to: 'contributing.html' }, { from: 'mobile/intro.html', to: 'mobile-web.html' }, - { from: 'mobile/roadmap.html', to: 'mobile-web.html' }, + { from: 'mobile/roadmap.html', to: 'mobile-web.html#roadmap' }, { from: 'mobile/installation.html', to: 'installation.html' }, { from: 'mobile/design-principles.html', to: 'mobile-web.html#mobile-specific-principles' }, - { from: 'mobile/contributing.html', to: 'contributing.html' }, + { from: 'mobile/contributing.html', to: 'mobile-web.html#mobile-specific-guidelines' }, ]; /** diff --git a/docs/contributing.md b/docs/contributing.md index 1b3cec21..53ff94d4 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -411,3 +411,20 @@ yarn run scaffold getButton --t u // Create util ``` ::: + +## Contribution Workflow + +Scaffold → Implementation → Testing → Documentation → Review → Changeset → Merge + +### Coverage Checklist + +- [ ] All if/else branches +- [ ] All switch cases +- [ ] All early returns +- [ ] Cleanup functions (useEffect return) + +### Implementation Rules + +- Named exports only +- Maximize TypeScript inference +- Apply the SSR safety pattern diff --git a/docs/es/contributing.md b/docs/es/contributing.md index cbbc3c89..66fd5b0b 100644 --- a/docs/es/contributing.md +++ b/docs/es/contributing.md @@ -411,3 +411,20 @@ yarn run scaffold getButton --t u // Crear una utilidad ``` ::: + +## Flujo de contribución + +Generación de esqueletos → Implementación → Pruebas → Documentación → Revisión → Changeset → Fusión + +### Lista de comprobación de la cobertura + +- [ ] Todas las ramas if/else +- [ ] Todos los casos de switch +- [ ] Todos los returns anticipados +- [ ] Las funciones de limpieza (el return de useEffect) + +### Reglas de implementación + +- Solo exportaciones con nombre +- Aprovecha al máximo la inferencia de TypeScript +- Aplica el patrón de seguridad para SSR diff --git a/docs/es/installation.md b/docs/es/installation.md index ec1a5552..b5de5206 100644 --- a/docs/es/installation.md +++ b/docs/es/installation.md @@ -36,7 +36,7 @@ bun add react-simplikit Importa los Hooks directamente desde el paquete: ```tsx -import { useKeyboardHeight, useAvoidKeyboard } from 'react-simplikit'; +import { useToggle } from 'react-simplikit'; ``` Todos los Hooks admiten tree shaking, así que en tu bundle solo se incluye lo que realmente usas. diff --git a/docs/es/mobile-web.md b/docs/es/mobile-web.md index 2d50254d..cfb36960 100644 --- a/docs/es/mobile-web.md +++ b/docs/es/mobile-web.md @@ -122,7 +122,7 @@ function FixedBottomCTA() { | [usePageVisibility](/es/hooks/usePageVisibility) | Sigue el estado de visibilidad de la página | | [useVisualViewport](/es/hooks/useVisualViewport) | Proporciona las dimensiones y la posición del viewport visual | -## Hoja de ruta +## Hoja de ruta {#roadmap} Las pantallas de los móviles son pequeñas, y ese espacio reducido genera una cantidad sorprendente de retos de interfaz. Los elementos quedan ocultos tras el teclado en pantalla, las áreas seguras varían según el dispositivo y el viewport que el usuario ve de verdad suele diferir del que informa el navegador. No son casos límite: son la realidad diaria del desarrollo para móvil. @@ -194,7 +194,7 @@ Los entornos móviles exigen una atención especial al rendimiento: - **Detectores de eventos pasivos**: usa detectores pasivos cuando sea aplicable - **Transiciones de React**: aprovecha `startTransition` para las actualizaciones no urgentes -## Directrices específicas para móvil +## Directrices específicas para móvil {#mobile-specific-guidelines} ### Probar en dispositivos reales diff --git a/docs/installation.md b/docs/installation.md index e8566c82..31ff0425 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -36,7 +36,7 @@ bun add react-simplikit Import hooks directly from the package: ```tsx -import { useKeyboardHeight, useAvoidKeyboard } from 'react-simplikit'; +import { useToggle } from 'react-simplikit'; ``` All hooks are tree-shakeable, so you only include what you use in your bundle. diff --git a/docs/ja/contributing.md b/docs/ja/contributing.md index 6d3d5fa2..583442bb 100644 --- a/docs/ja/contributing.md +++ b/docs/ja/contributing.md @@ -411,3 +411,20 @@ yarn run scaffold getButton --t u // ユーティリティを作成 ``` ::: + +## コントリビューションの流れ + +スキャフォールディング → 実装 → テスト → ドキュメント化 → レビュー → Changeset → マージ + +### カバレッジチェックリスト + +- [ ] すべての if/else 分岐 +- [ ] すべての switch case +- [ ] すべての早期リターン +- [ ] クリーンアップ関数(useEffect の戻り値) + +### 実装ルール + +- named export のみを使用する +- TypeScript の型推論を最大限活用する +- SSR 安全パターンを適用する diff --git a/docs/ja/installation.md b/docs/ja/installation.md index e9284391..8e96c137 100644 --- a/docs/ja/installation.md +++ b/docs/ja/installation.md @@ -36,7 +36,7 @@ bun add react-simplikit パッケージから直接フックを import してください。 ```tsx -import { useKeyboardHeight, useAvoidKeyboard } from 'react-simplikit'; +import { useToggle } from 'react-simplikit'; ``` すべてのフックはツリーシェイキング対応なので、実際に使用するものだけがバンドルに含まれます。 diff --git a/docs/ja/mobile-web.md b/docs/ja/mobile-web.md index 490860e5..12c71a2d 100644 --- a/docs/ja/mobile-web.md +++ b/docs/ja/mobile-web.md @@ -122,7 +122,7 @@ function FixedBottomCTA() { | [usePageVisibility](/ja/hooks/usePageVisibility) | ページの可視性の状態を追跡します | | [useVisualViewport](/ja/hooks/useVisualViewport) | ビジュアルビューポートのサイズとオフセットを提供します | -## ロードマップ +## ロードマップ {#roadmap} モバイル画面は小さく、その小さな空間が驚くほど多くの UI 課題を生み出します。要素がオンスクリーンキーボードに隠れたり、セーフエリアが端末によって異なったり、ユーザーが実際に見ているビューポートがブラウザの報告する値と食い違ったりします。これらはエッジケースではなく、モバイル開発における日常的な現実です。 @@ -194,7 +194,7 @@ it('is safe on server side rendering', () => { - **パッシブイベントリスナー**: 適用可能な場合はパッシブリスナーを使用します - **React トランジション**: 緊急でない更新には `startTransition` を活用します -## モバイル特有のガイドライン +## モバイル特有のガイドライン {#mobile-specific-guidelines} ### 実機でのテスト diff --git a/docs/ko/contributing.md b/docs/ko/contributing.md index d1ddaa96..824eacd2 100644 --- a/docs/ko/contributing.md +++ b/docs/ko/contributing.md @@ -402,3 +402,20 @@ yarn run scaffold getButton --t u // 유틸 생성 ``` ::: + +## 기여 워크플로우 + +스캐폴딩 → 구현 → 테스트 → 문서화 → 리뷰 → Changeset → 병합 + +### 커버리지 체크리스트 + +- [ ] 모든 if/else 브랜치 +- [ ] 모든 switch case +- [ ] 모든 early return +- [ ] cleanup 함수 (useEffect return) + +### 구현 규칙 + +- named export만 사용 +- TypeScript 추론 최대화 +- SSR 안전 패턴 적용 diff --git a/docs/ko/installation.md b/docs/ko/installation.md index 45670741..0e666e89 100644 --- a/docs/ko/installation.md +++ b/docs/ko/installation.md @@ -36,7 +36,7 @@ bun add react-simplikit 패키지에서 직접 훅을 import하세요: ```tsx -import { useKeyboardHeight, useAvoidKeyboard } from 'react-simplikit'; +import { useToggle } from 'react-simplikit'; ``` 모든 훅은 트리 쉐이킹이 가능하므로, 번들에는 사용하는 것만 포함돼요. diff --git a/docs/ko/mobile-web.md b/docs/ko/mobile-web.md index 4ae043d0..6543a3a8 100644 --- a/docs/ko/mobile-web.md +++ b/docs/ko/mobile-web.md @@ -122,7 +122,7 @@ function FixedBottomCTA() { | [usePageVisibility](/ko/hooks/usePageVisibility) | 페이지 가시성 상태를 추적해요 | | [useVisualViewport](/ko/hooks/useVisualViewport) | Visual Viewport 크기와 오프셋을 제공해요 | -## 앞으로의 방향 +## 앞으로의 방향 {#roadmap} 모바일 화면은 작고 그 작은 공간 안에서 UI가 의도대로 보이지 않는 경우가 많아요. 키보드에 요소가 가려지고, 기기마다 다른 SafeArea가 다르고, 브라우저가 보여주는 viewport와 사용자가 실제로 보는 영역의 차이가 빈번하게 발생해요. @@ -194,7 +194,7 @@ it('is safe on server side rendering', () => { - **패시브 이벤트 리스너**: 해당하는 경우 패시브 리스너 사용 - **React 트랜지션**: 급하지 않은 업데이트에 `startTransition` 활용 -## 모바일 특화 가이드라인 +## 모바일 특화 가이드라인 {#mobile-specific-guidelines} ### 실제 기기 테스트 diff --git a/docs/mobile-web.md b/docs/mobile-web.md index 138e5b15..4f90fc05 100644 --- a/docs/mobile-web.md +++ b/docs/mobile-web.md @@ -122,7 +122,7 @@ function FixedBottomCTA() { | [usePageVisibility](/hooks/usePageVisibility) | Tracks page visibility state | | [useVisualViewport](/hooks/useVisualViewport) | Provides visual viewport dimensions and offset | -## Roadmap +## Roadmap {#roadmap} Mobile screens are small, and that small space creates a surprising number of UI challenges. Elements get hidden behind on-screen keyboards, safe areas vary by device, and the viewport the user actually sees often differs from what the browser reports. These are not edge cases — they are everyday realities of mobile development. @@ -194,7 +194,7 @@ Mobile environments require special attention to performance: - **Passive event listeners**: Use passive listeners where applicable - **React transitions**: Leverage `startTransition` for non-urgent updates -## Mobile-Specific Guidelines +## Mobile-Specific Guidelines {#mobile-specific-guidelines} ### Testing on Real Devices diff --git a/docs/zh-Hans/contributing.md b/docs/zh-Hans/contributing.md index 11c1dd91..be5bfe02 100644 --- a/docs/zh-Hans/contributing.md +++ b/docs/zh-Hans/contributing.md @@ -411,3 +411,20 @@ yarn run scaffold getButton --t u // 创建工具函数 ``` ::: + +## 贡献流程 + +脚手架 → 实现 → 测试 → 文档 → 评审 → Changeset → 合并 + +### 覆盖率检查清单 + +- [ ] 所有 if/else 分支 +- [ ] 所有 switch case +- [ ] 所有提前 return +- [ ] 清理函数(useEffect 返回的函数) + +### 实现规范 + +- 只使用具名导出 +- 最大限度地利用 TypeScript 的类型推断 +- 应用 SSR 安全模式 diff --git a/docs/zh-Hans/installation.md b/docs/zh-Hans/installation.md index 18096f0a..07e5f36e 100644 --- a/docs/zh-Hans/installation.md +++ b/docs/zh-Hans/installation.md @@ -36,7 +36,7 @@ bun add react-simplikit 直接从这个包中导入 Hook: ```tsx -import { useKeyboardHeight, useAvoidKeyboard } from 'react-simplikit'; +import { useToggle } from 'react-simplikit'; ``` 所有 Hook 都支持 tree shaking,因此只有你真正用到的部分才会被打进包里。 diff --git a/docs/zh-Hans/mobile-web.md b/docs/zh-Hans/mobile-web.md index 9ac68b58..d660bea7 100644 --- a/docs/zh-Hans/mobile-web.md +++ b/docs/zh-Hans/mobile-web.md @@ -122,7 +122,7 @@ function FixedBottomCTA() { | [usePageVisibility](/zh-Hans/hooks/usePageVisibility) | 跟踪页面可见性状态 | | [useVisualViewport](/zh-Hans/hooks/useVisualViewport) | 提供视觉视口的尺寸和偏移量 | -## 路线图 +## 路线图 {#roadmap} 移动端的屏幕很小,而这块小小的空间会带来数量惊人的 UI 难题。元素被软键盘遮住,安全区域因设备而异,用户实际看到的视口也常常和浏览器报告的不一样。这些都不是边缘情况,而是移动端开发每天都要面对的现实。 @@ -194,7 +194,7 @@ it('is safe on server side rendering', () => { - **被动事件监听器**:在适用的场景中使用 passive 监听器 - **React transition**:对非紧急的更新使用 `startTransition` -## 移动端专属准则 +## 移动端专属准则 {#mobile-specific-guidelines} ### 在真机上测试 diff --git a/package.json b/package.json index 4903a75b..3077fdb6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "changeset:publish": "changeset publish", "docs:gen": "tsx .scripts/index.ts generate-docs", "skill:gen": "tsx .scripts/index.ts generate-skill", - "docs:prepare": "tsx .scripts/index.ts prepare-localized-fallbacks && tsx .scripts/index.ts generate-reference-index", + "docs:prepare": "tsx .scripts/index.ts generate-reference-index && tsx .scripts/index.ts prepare-localized-fallbacks", "docs:dev": "yarn docs:prepare && vitepress dev", "docs:build": "yarn docs:prepare && vitepress build", "docs:preview": "vitepress preview", From 432481a0a90632483967a9d551c91fbd2d7573d9 Mon Sep 17 00:00:00 2001 From: mnxmnz <48766355+mnxmnz@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:55:47 +0900 Subject: [PATCH 7/7] fix(docs): route retired anchors to their new pages and assert the anchor pins --- .scripts/verifyDocsI18n.ts | 29 +++++++++++++++++++++++-- .vitepress/libs/legacyRedirects.mts | 33 ++++++++++++++++++----------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/.scripts/verifyDocsI18n.ts b/.scripts/verifyDocsI18n.ts index 1ef621db..993d4d3c 100644 --- a/.scripts/verifyDocsI18n.ts +++ b/.scripts/verifyDocsI18n.ts @@ -6,7 +6,7 @@ import { DefaultTheme } from 'vitepress'; import { buildLocaleConfig } from '../.vitepress/libs/buildLocaleConfig.mts'; import { getSidebarItems } from '../.vitepress/libs/getSidebarItems.mts'; -import { collectLegacyRedirects } from '../.vitepress/libs/legacyRedirects.mts'; +import { collectLegacyRedirects, RETIRED_ANCHORS } from '../.vitepress/libs/legacyRedirects.mts'; import { generatedLocalesDirectory, generatedRewrites, @@ -119,6 +119,19 @@ try { 'the legacy redirect set must cover every pre-flattening URL across all locales' ); + // Sections that moved to a different page than their stub's target are redirected + // by the stub script instead, so their destinations need the same anchor check. + for (const [anchor, destination] of Object.entries(RETIRED_ANCHORS)) { + const [file, id] = destination.replace(/^\//, '').split('#'); + // VitePress emits NFD ids for some locales, so both sides are normalized. + const page = (await fs.readFile(path.join(buildOutputDirectory, file), 'utf8')).normalize('NFC'); + assert.equal( + page.includes(`id="${id.normalize('NFC')}"`), + true, + `${anchor} must land on an existing anchor in ${file}` + ); + } + // Spot-check the shape itself: a renamed `from` would keep the count intact and // still write a file, so the count alone cannot catch it. const stubPaths = new Set(stubs.map(stub => stub.from)); @@ -136,7 +149,19 @@ try { const stub = await fs.readFile(path.join(buildOutputDirectory, from), 'utf8'); assert.match(stub, /http-equiv="refresh"/, `${from} must redirect`); assert.equal(stub.includes('noindex'), false, `${from} must stay indexable so canonical can consolidate signals`); - await fs.access(path.join(buildOutputDirectory, to.split('#')[0])); + const [targetFile, targetAnchor] = to.split('#'); + await fs.access(path.join(buildOutputDirectory, targetFile)); + + // The merge strategy hangs on explicit {#...} pins in the merged pages: drop one + // and the id silently becomes the translated heading slug, with no other signal. + if (targetAnchor !== undefined) { + const targetPage = (await fs.readFile(path.join(buildOutputDirectory, targetFile), 'utf8')).normalize('NFC'); + assert.equal( + targetPage.includes(`id="${targetAnchor.normalize('NFC')}"`), + true, + `${from} points at a missing anchor` + ); + } } // The reference index is generated, so a broken generator would leave the nav diff --git a/.vitepress/libs/legacyRedirects.mts b/.vitepress/libs/legacyRedirects.mts index 2021dab4..3ee505ae 100644 --- a/.vitepress/libs/legacyRedirects.mts +++ b/.vitepress/libs/legacyRedirects.mts @@ -9,25 +9,34 @@ const SITE_ORIGIN = 'https://react-simplikit.slash.page'; type RedirectPair = { from: string; to: string }; /** - * Rebuilds the URL from its parts so the query and fragment a meta refresh would - * drop survive. A target that already carries a fragment keeps it unless the - * incoming URL has one of its own, and the query always lands before the hash. - */ -/** - * Headings that moved into a differently named section during the guide merge. - * Without this the incoming hash wins and lands on an id that no longer exists. + * Sections that the guide merge moved to a *different page* than their stub's + * target. The stub only knows one destination, so these incoming fragments carry + * their own path. Keys are the ids the old pages published, which are localized. */ -const RETIRED_ANCHORS: Record = { - '#core-principles': '#mobile-specific-principles', +export const RETIRED_ANCHORS: Record = { + // The old mobile design-principles page opened with a verbatim copy of the core + // principles; those live on the merged design-principles page, not mobile-web. + '#core-principles': '/design-principles.html#design-principles', + '#핵심-원칙': '/ko/design-principles.html#설계-원칙', + '#コア原則': '/ja/design-principles.html#設計原則', + '#核心原则': '/zh-Hans/design-principles.html#设计原则', + '#principios-fundamentales': '/es/design-principles.html#principios-de-diseno', }; +/** + * Rebuilds the URL from its parts so the query and fragment a meta refresh would + * drop survive. A retired anchor overrides the whole destination, since the + * section it names now lives on another page; otherwise the incoming fragment + * wins over the stub's own default. + */ function REDIRECT_SCRIPT(target: string): string { const [pathname, fragment] = target.split('#'); const fallback = fragment === undefined ? '' : `#${fragment}`; return ( - `var r=${JSON.stringify(RETIRED_ANCHORS)};` + - `location.replace(${JSON.stringify(pathname)} + location.search + ` + - `(r[location.hash] || location.hash || ${JSON.stringify(fallback)}))` + `var r=${JSON.stringify(RETIRED_ANCHORS)},h=decodeURIComponent(location.hash).normalize('NFC'),` + + `t=r[h],p=t?t.split('#')[0]:${JSON.stringify(pathname)},` + + `f=t?'#'+t.split('#')[1]:(h||${JSON.stringify(fallback)});` + + `location.replace(p + location.search + f)` ); }