Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.idea
.vscode
.cursor
node_modules
docs/.vitepress/cache/
docs/.vitepress/**/deps_temp_*/
Expand All @@ -9,4 +8,17 @@ dist
.temp
.env
.env.*
.envrc
.direnv/
*.log

# Local deployment / tooling artifacts
.vercel/
.netlify/
.turbo/
.cache/
.npmrc.local

# Local keys / certificates
*.pem
*.key
16 changes: 14 additions & 2 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,22 @@ function editUrlForPage(page: PageData): string {
export default defineConfig({
lang: 'en-US',
title: 'useReact',
titleTemplate: ':title | useReact',
titleTemplate: false,
description: 'Collection of React Hooks',
cleanUrls: true,
lastUpdated: true,
transformPageData(pageData) {
const rawTitle = String(pageData.title || '').trim()
if (!rawTitle) return

const hookMatch = pageData.relativePath.match(/^functions\/(use[A-Za-z0-9_]+)\.md$/)
if (hookMatch) {
pageData.title = `${rawTitle} | ${hookMatch[1]}() | use react`
return
}

pageData.title = `${rawTitle} | use react`
},
async transformHead(ctx) {
return seoTransformHead({
siteData: ctx.siteData,
Expand Down Expand Up @@ -123,8 +135,8 @@ export default defineConfig({
},
head: [
['meta', { name: 'theme-color', content: '#ffffff' }],
['link', { rel: 'icon', href: '/favicon-32x32.png', type: 'image/png' }],
['link', { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' }],
['link', { rel: 'shortcut icon', href: '/favicon.svg' }],
['meta', { property: 'og:title', content: 'UseReact' }],
['meta', { property: 'og:image', content: 'https://usereact.org/logo.png' }],
[
Expand Down
44 changes: 44 additions & 0 deletions docs/.vitepress/data/homeBrowserDemos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** Live demos for the home "Browser" section (same order as /functions/browser). */
export const homeBrowserDemos: ReadonlyArray<{ demo: string; title: string }> = [
{ demo: 'useHash/basic', title: 'useHash' },
{ demo: 'useBrowserLocation/basic', title: 'useBrowserLocation' },
{ demo: 'useBroadcastChannel/basic', title: 'useBroadcastChannel' },
{ demo: 'useFavicon/basic', title: 'useFavicon' },
{ demo: 'useFileDialog/basic', title: 'useFileDialog' },
{ demo: 'useFileSystemAccess/basic', title: 'useFileSystemAccess' },
{ demo: 'useFullscreen/basic', title: 'useFullscreen' },
{ demo: 'useMediaQuery/basic', title: 'useMediaQuery' },
{ demo: 'useMediaControls/basic', title: 'useMediaControls' },
{ demo: 'useMemory/basic', title: 'useMemory' },
{ demo: 'useCssSupports/basic', title: 'useCssSupports' },
{ demo: 'useCssVar/basic', title: 'useCssVar' },
{ demo: 'useBreakpoints/basic', title: 'useBreakpoints' },
{ demo: 'useBluetooth/basic', title: 'useBluetooth' },
{ demo: 'usePreferredColorScheme/basic', title: 'usePreferredColorScheme' },
{ demo: 'usePreferredDark/basic', title: 'usePreferredDark' },
{ demo: 'useDark/basic', title: 'useDark' },
{ demo: 'usePreferredLanguages/basic', title: 'usePreferredLanguages' },
{ demo: 'usePreferredReducedMotion/basic', title: 'usePreferredReducedMotion' },
{ demo: 'usePreferredContrast/basic', title: 'usePreferredContrast' },
{ demo: 'usePreferredReducedTransparency/basic', title: 'usePreferredReducedTransparency' },
{ demo: 'useTextDirection/basic', title: 'useTextDirection' },
{ demo: 'useColorMode/basic', title: 'useColorMode' },
{ demo: 'useUrlSearchParams/basic', title: 'useUrlSearchParams' },
{ demo: 'useSSRWidth/basic', title: 'useSSRWidth' },
{ demo: 'useScreenOrientation/basic', title: 'useScreenOrientation' },
{ demo: 'useShare/basic', title: 'useShare' },
{ demo: 'useVibrate/basic', title: 'useVibrate' },
{ demo: 'useWakeLock/basic', title: 'useWakeLock' },
{ demo: 'useStyleTag/basic', title: 'useStyleTag' },
{ demo: 'useWebNotification/basic', title: 'useWebNotification' },
{ demo: 'useScreenSafeArea/basic', title: 'useScreenSafeArea' },
{ demo: 'useWindowSize/basic', title: 'useWindowSize' },
{ demo: 'useTitle/basic', title: 'useTitle' },
{ demo: 'useLockBodyScroll/basic', title: 'useLockBodyScroll' },
{ demo: 'useScript/basic', title: 'useScript' },
{ demo: 'usePageVisibility/basic', title: 'usePageVisibility' },
{ demo: 'useCopyToClipboard/basic', title: 'useCopyToClipboard' },
{ demo: 'useClipboardItems/basic', title: 'useClipboardItems' },
{ demo: 'usePermission/basic', title: 'usePermission' },
{ demo: 'usePerformanceObserver/basic', title: 'usePerformanceObserver' },
]
75 changes: 75 additions & 0 deletions docs/.vitepress/data/hookDemoSubtitles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,81 @@ export const hookDemoSubtitles: Record<string, string> = {
'useDraggable/basic':
'Drag a card by its handle and keep it inside a container while tracking position and drag state.',
'useDropZone/basic': 'Track drag-over state and capture dropped files with hover feedback and recent-drop logging.',
'useBroadcastChannel/basic':
'Broadcast structured messages across tabs on one channel and inspect the latest received payload.',
'useBrowserLocation/basic':
'Watch href/path/search/hash updates from popstate/hashchange and try quick hash navigation actions.',
'useFileDialog/basic': 'Open the native file chooser, capture selected files, and reset selection programmatically.',
'useFileSystemAccess/basic':
'Open a text file with native picker, edit content, and save it back through the File System Access API.',
'useBluetooth/basic': 'Trigger the Web Bluetooth device picker and track selected device name and request errors.',
'useFullscreen/basic':
'Enter, exit, and toggle fullscreen for a target element while tracking real-time fullscreen state.',
'useMediaControls/basic':
'Control a media element with custom play/pause, mute, volume, and seek interactions synced from events.',
'useMemory/basic': 'Poll performance.memory in Chromium and display live used/total/limit JS heap snapshots.',
'useMediaQuery/basic':
'Subscribe to multiple media queries and watch match booleans react to viewport and system preference changes.',
'useCssSupports/basic':
'Check CSS.supports in both property/value and condition-string forms with live user-entered input.',
'useCssVar/basic': 'Read and update a CSS custom property on a target element, keeping UI state in sync with styles.',
'useBreakpoints/basic':
'Resolve active breakpoint names from window width and inspect current, active list, boolean flags, and >= checks.',
'usePreferredColorScheme/basic':
'Read prefers-color-scheme changes and map them to light/dark/no-preference UI behavior.',
'usePreferredDark/basic':
'Return a boolean from prefers-color-scheme: dark for straightforward dark-mode branching in UI.',
'useDark/basic':
'Persist an explicit dark/light choice and synchronize dark/light classes on the chosen DOM element.',
'usePreferredLanguages/basic':
'Expose navigator language priority order and refresh when the browser locale preference changes.',
'usePreferredReducedMotion/basic':
'Observe prefers-reduced-motion and conditionally disable non-essential animations in UI.',
'usePreferredContrast/basic':
'Read prefers-contrast (more/less/custom/no-preference) and map it to stronger or softer visual emphasis.',
'usePreferredReducedTransparency/basic':
'Observe prefers-reduced-transparency and replace frosted/translucent UI surfaces with solid backgrounds.',
'useColorMode/basic':
'Persist light/dark/auto mode and expose resolved dark state while syncing theme attribute/class on the root.',
'useUrlSearchParams/basic':
'Read current URL query values and replace search params through record or URLSearchParams updates.',
'useSSRWidth/basic':
'Return fallback width on server and initial window width on client for SSR-safe layout branching.',
'useScreenOrientation/basic':
'Track screen orientation type and angle, then derive portrait/landscape oriented UI hints.',
'useShare/basic':
'Open the native share sheet with title, text, and URL, then reflect whether share succeeded or was dismissed.',
'useVibrate/basic':
'Invoke short and patterned device vibrations from user gestures, and report whether the browser accepted calls.',
'useWakeLock/basic':
'Acquire and release a screen wake lock, tracking active state and request outcomes in real time.',
'useStyleTag/basic':
'Inject CSS into a dynamic style tag, swap style variants live, and observe id/loaded/error state.',
'useWebNotification/basic':
'Request notification permission and send a browser notification while tracking permission/action outcomes.',
'useScreenSafeArea/basic':
'Read top/right/bottom/left safe-area insets and preview layout padding adjusted with current values.',
'useWindowSize/basic':
'Track live viewport width/height from resize events and derive orientation/aspect display hints.',
'useTitle/basic': 'Set document.title from component state and optionally restore the previous title on unmount.',
'useLockBodyScroll/basic':
'Toggle body overflow lock with overlay state so background page scrolling is disabled while open.',
'useScript/basic':
'Load and track an external script URL with idle/loading/ready/error status and global availability checks.',
'usePageVisibility/basic':
'Mirror document visibility state and pause visible-only work while the tab or window is hidden.',
'useCopyToClipboard/basic':
'Copy text to navigator.clipboard, track success/failure, and expose the hook stored copied value.',
'useClipboardItems/basic':
'Read ClipboardItem entries from the async clipboard API and inspect returned MIME type groups.',
'usePermission/basic':
'Observe Permissions API state for selected names and watch updates when browser settings change.',
'usePerformanceObserver/basic':
'Subscribe to PerformanceObserver entry types and inspect buffered navigation/resource timeline events.',
'useTextDirection/basic':
'Watch document dir changes and switch UI direction between ltr and rtl with a simple state value.',
'useFavicon/basic': 'Swap the tab icon dynamically using preset and custom favicon URLs.',
'useHash/basic': 'Read and update window.location.hash with quick presets and custom hash input.',
'useElementBounding/basic':
'Track full DOMRect values (x/y/edges/size) while a target moves inside a scrollable container.',
'useElementSize/basic':
Expand Down
14 changes: 13 additions & 1 deletion docs/.vitepress/seo/transformHead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,25 @@ export interface TransformHeadContext {
description: string
}

function buildSeoTitle(pageData: PageData, title: string): string {
const hookMatch = pageData.relativePath.match(/^functions\/(use[A-Za-z0-9_]+)\.md$/)
if (hookMatch) {
return `${title} | ${hookMatch[1]}() | use react`
}
return `${title} | use react`
}

export function transformHead(ctx: TransformHeadContext): HeadConfig[] {
const { siteData, pageData, title, description } = ctx
if (pageData.isNotFound) return []

const seoTitle = buildSeoTitle(pageData, title)
const canonical = buildCanonical(siteData, pageData)
const lang = siteData.lang || 'en-US'
const ld = buildJsonLd({ canonical, title, description, lang })
const ld = buildJsonLd({ canonical, title: seoTitle, description, lang })

const head: HeadConfig[] = [
['title', seoTitle],
['link', { rel: 'canonical', href: canonical }],
[
'meta',
Expand All @@ -97,6 +107,8 @@ export function transformHead(ctx: TransformHeadContext): HeadConfig[] {
['meta', { property: 'article:publisher:url', content: `${SITE}/` }],
['meta', { name: 'publisher', content: PUBLISHER_NAME }],
['meta', { property: 'og:url', content: canonical }],
['meta', { property: 'og:title', content: seoTitle }],
['meta', { name: 'twitter:title', content: seoTitle }],
['script', { type: 'application/ld+json' }, ld],
]

Expand Down
81 changes: 74 additions & 7 deletions docs/.vitepress/theme/components/HomeHookShowcase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { computed, ref } from 'vue'
import { withBase } from 'vitepress'
import { homeStateDemos } from '../../data/homeStateDemos'
import { homeElementsDemos } from '../../data/homeElementsDemos'
import { homeBrowserDemos } from '../../data/homeBrowserDemos'

/** Open card ids - several demos can stay open at once. */
const expandedDemos = ref<string[]>([])

/** All State cards show full preview (overrides per-card list for display). */
const showAllDemos = ref(true)
/** Start collapsed by default. */
const showAllDemos = ref(false)
const allHomeDemos = [...homeStateDemos, ...homeElementsDemos, ...homeBrowserDemos]

const anyDemosOpen = computed(() => showAllDemos.value || expandedDemos.value.length > 0)

Expand Down Expand Up @@ -37,7 +39,7 @@ function onDemoItemClick(demo: string, ev: MouseEvent) {
if (!el.closest('.hook-live-demo__header')) return
if (showAllDemos.value) {
showAllDemos.value = false
expandedDemos.value = homeStateDemos.map((i) => i.demo).filter((d) => d !== demo)
expandedDemos.value = allHomeDemos.map((i) => i.demo).filter((d) => d !== demo)
} else {
expandedDemos.value = expandedDemos.value.filter((d) => d !== demo)
}
Expand All @@ -57,7 +59,7 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) {
if (open) {
if (showAllDemos.value) {
showAllDemos.value = false
expandedDemos.value = homeStateDemos.map((i) => i.demo).filter((d) => d !== demo)
expandedDemos.value = allHomeDemos.map((i) => i.demo).filter((d) => d !== demo)
} else {
expandedDemos.value = expandedDemos.value.filter((d) => d !== demo)
}
Expand All @@ -74,7 +76,7 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) {
</div>

<div class="home-showcase__inner">
<h2 id="home-showcase-title" class="home-showcase__title">Live component examples</h2>
<h2 id="home-showcase-title" class="home-showcase__title">Demo component examples</h2>
<p class="home-showcase__intro">
These are real in-page previews: each card links to the hook’s full reference. More categories will land here
next.
Expand Down Expand Up @@ -150,6 +152,9 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) {
@click="onDemoItemClick(item.demo, $event)"
@keydown="onDemoItemKeydown(item.demo, $event)"
>
<div class="home-state-demos__chevron" aria-hidden="true">
{{ isCardExpanded(item.demo) ? '▲' : '▼' }}
</div>
<HookLiveDemo
:demo="item.demo"
:title="item.title"
Expand Down Expand Up @@ -187,6 +192,47 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) {
@click="onDemoItemClick(item.demo, $event)"
@keydown="onDemoItemKeydown(item.demo, $event)"
>
<div class="home-state-demos__chevron" aria-hidden="true">
{{ isCardExpanded(item.demo) ? '▲' : '▼' }}
</div>
<HookLiveDemo
:demo="item.demo"
:title="item.title"
:title-href="withBase(`/functions/${item.demo.split('/')[0]}`)"
/>
</article>
</div>
</section>

<section
class="home-showcase__section home-showcase__section--spaced"
aria-labelledby="home-showcase-browser-title"
>
<div class="home-showcase__section-head">
<h3 id="home-showcase-browser-title" class="home-showcase__section-title">Browser</h3>
</div>
<p class="home-showcase__section-lead">
Browser APIs and environment hooks: location/hash, media, permissions, clipboard, notifications, orientation,
scripts, and more. Browse
<a class="home-showcase__state-link" :href="withBase('/functions/browser')">Browser in the function list →</a>
</p>

<div class="home-state-demos" role="list">
<article
v-for="(item, index) in homeBrowserDemos"
:key="item.demo"
class="home-state-demos__item"
:class="{ 'home-state-demos__item--expanded': isCardExpanded(item.demo) }"
:style="{ '--stagger': String(index) }"
role="listitem"
tabindex="0"
:aria-expanded="isCardExpanded(item.demo)"
@click="onDemoItemClick(item.demo, $event)"
@keydown="onDemoItemKeydown(item.demo, $event)"
>
<div class="home-state-demos__chevron" aria-hidden="true">
{{ isCardExpanded(item.demo) ? '▲' : '▼' }}
</div>
<HookLiveDemo
:demo="item.demo"
:title="item.title"
Expand Down Expand Up @@ -363,11 +409,16 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) {
}

.home-showcase__section-lead {
max-width: 52rem;
margin: 0 0 1.5rem;
max-width: 56rem;
margin: 0.7rem 0 1.7rem;
padding: 0.75rem 0.9rem;
font-size: 0.95rem;
line-height: 1.6;
color: var(--vp-c-text-2);
background: color-mix(in srgb, var(--vp-c-bg-soft) 86%, var(--vp-c-bg));
border: 1px solid color-mix(in srgb, var(--vp-c-divider) 70%, transparent);
border-left: 3px solid color-mix(in srgb, var(--vp-c-brand-1) 60%, var(--vp-c-divider));
border-radius: 10px;
}

.home-showcase__state-link {
Expand Down Expand Up @@ -408,6 +459,22 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) {
animation-delay: calc(0.04s + (var(--stagger) * 0.02s));
border-radius: 16px;
outline-offset: 2px;
position: relative;
}

.home-state-demos__chevron {
position: absolute;
top: 12px;
right: 12px;
z-index: 2;
font-size: 11px;
line-height: 1;
color: var(--vp-c-text-3);
pointer-events: none;
}

.home-state-demos__item--expanded .home-state-demos__chevron {
color: var(--vp-c-brand-1);
}

.home-state-demos__item:not(.home-state-demos__item--expanded) {
Expand Down
Loading