diff --git a/.gitignore b/.gitignore index b46bf3a..d74f56f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ .idea .vscode +.cursor node_modules docs/.vitepress/cache/ docs/.vitepress/**/deps_temp_*/ dist .DS_Store .temp +.env +.env.* *.log diff --git a/docs/.vitepress/data/homeElementsDemos.ts b/docs/.vitepress/data/homeElementsDemos.ts new file mode 100644 index 0000000..e1c78e6 --- /dev/null +++ b/docs/.vitepress/data/homeElementsDemos.ts @@ -0,0 +1,14 @@ +/** Live demos for the home "Elements" section (same order as /functions/elements). */ +export const homeElementsDemos: ReadonlyArray<{ demo: string; title: string }> = [ + { demo: 'useTextareaAutoSize/basic', title: 'useTextareaAutoSize' }, + { demo: 'useClickOutside/basic', title: 'useClickOutside' }, + { demo: 'useDraggable/basic', title: 'useDraggable' }, + { demo: 'useElementSize/basic', title: 'useElementSize' }, + { demo: 'useElementBounding/basic', title: 'useElementBounding' }, + { demo: 'useElementVisibility/basic', title: 'useElementVisibility' }, + { demo: 'useParentElement/basic', title: 'useParentElement' }, + { demo: 'useWindowFocus/basic', title: 'useWindowFocus' }, + { demo: 'useWindowScroll/basic', title: 'useWindowScroll' }, + { demo: 'useActiveElement/basic', title: 'useActiveElement' }, + { demo: 'useDropZone/basic', title: 'useDropZone' }, +] diff --git a/docs/.vitepress/data/hookDemoSubtitles.ts b/docs/.vitepress/data/hookDemoSubtitles.ts index e76c800..9d9289e 100644 --- a/docs/.vitepress/data/hookDemoSubtitles.ts +++ b/docs/.vitepress/data/hookDemoSubtitles.ts @@ -3,6 +3,25 @@ * Keys are `useX/basic` as passed to the `demo` prop. */ export const hookDemoSubtitles: Record = { + 'useActiveElement/basic': + 'Mirror document.activeElement and inspect focus transitions across inputs, textarea, buttons, and links.', + 'useClickOutside/basic': + 'Close a panel when clicks happen outside multiple protected refs (toggle button + content area).', + '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.', + 'useElementBounding/basic': + 'Track full DOMRect values (x/y/edges/size) while a target moves inside a scrollable container.', + 'useElementSize/basic': + 'Observe a panel size with ResizeObserver while changing width, height, and inner padding live.', + 'useElementVisibility/basic': + 'Observe when a sentinel becomes visible inside a scroll root with configurable threshold.', + 'useParentElement/basic': + 'Resolve and inspect a target node parentElement, and interact with the parent wrapper through the returned node.', + 'useWindowFocus/basic': 'Reflect current window focus/blur state and demonstrate focus-aware background ticking.', + 'useWindowScroll/basic': 'Track live window scrollX/scrollY and expose quick actions for top/middle/end navigation.', + 'useTextareaAutoSize/basic': + 'Autosize a textarea to content height; optionally sync the measured height to a wrapper via styleTarget.', 'useToggle/basic': 'Boolean (or set) state with a flip/toggle, optional custom setters, and a stable toggler function.', 'useCounter/basic': 'Increment, decrement, or set a number with optional min/max so values stay in range.', diff --git a/docs/.vitepress/theme/components/HomeHookShowcase.vue b/docs/.vitepress/theme/components/HomeHookShowcase.vue index 11d10e1..7b14ddf 100644 --- a/docs/.vitepress/theme/components/HomeHookShowcase.vue +++ b/docs/.vitepress/theme/components/HomeHookShowcase.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue' import { withBase } from 'vitepress' import { homeStateDemos } from '../../data/homeStateDemos' +import { homeElementsDemos } from '../../data/homeElementsDemos' /** Open card ids - several demos can stay open at once. */ const expandedDemos = ref([]) @@ -158,7 +159,42 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) { - +
+
+

Elements

+
+

+ DOM-focused helpers: textarea autosize, outside click handling, drag/drop, element measurement, focus and + scroll state. Browse + Elements in the function list → +

+ +
+
+ +
+
+
@@ -240,6 +276,10 @@ function onDemoItemKeydown(demo: string, ev: KeyboardEvent) { background: transparent; } +.home-showcase__section--spaced { + margin-top: 2.2rem; +} + .home-showcase__section-head { display: flex; flex-wrap: wrap; diff --git a/docs/.vitepress/theme/components/HookLiveDemo.vue b/docs/.vitepress/theme/components/HookLiveDemo.vue index a972d18..5c83a19 100644 --- a/docs/.vitepress/theme/components/HookLiveDemo.vue +++ b/docs/.vitepress/theme/components/HookLiveDemo.vue @@ -40,9 +40,20 @@ const activeSource = computed(() => sourceJsx.value) const sourceOpen = ref(false) const demoLoaders: Record Promise> = { + 'useActiveElement/basic': () => import('../react-demos/useActiveElement.basic'), 'useAsyncState/basic': () => import('../react-demos/useAsyncState.basic'), 'useCounter/basic': () => import('../react-demos/useCounter.basic'), + 'useClickOutside/basic': () => import('../react-demos/useClickOutside.basic'), + 'useDraggable/basic': () => import('../react-demos/useDraggable.basic'), + 'useElementBounding/basic': () => import('../react-demos/useElementBounding.basic'), + 'useElementSize/basic': () => import('../react-demos/useElementSize.basic'), + 'useElementVisibility/basic': () => import('../react-demos/useElementVisibility.basic'), + 'useParentElement/basic': () => import('../react-demos/useParentElement.basic'), + 'useTextareaAutoSize/basic': () => import('../react-demos/useTextareaAutoSize.basic'), + 'useWindowFocus/basic': () => import('../react-demos/useWindowFocus.basic'), + 'useWindowScroll/basic': () => import('../react-demos/useWindowScroll.basic'), 'useDebouncedRefHistory/basic': () => import('../react-demos/useDebouncedRefHistory.basic'), + 'useDropZone/basic': () => import('../react-demos/useDropZone.basic'), 'useToggle/basic': () => import('../react-demos/useToggle.basic'), 'useDebounce/basic': () => import('../react-demos/useDebounce.basic'), 'useEventCallback/basic': () => import('../react-demos/useEventCallback.basic'), diff --git a/docs/.vitepress/theme/react-demos/useActiveElement.basic.ts b/docs/.vitepress/theme/react-demos/useActiveElement.basic.ts new file mode 100644 index 0000000..92e665b --- /dev/null +++ b/docs/.vitepress/theme/react-demos/useActiveElement.basic.ts @@ -0,0 +1,160 @@ +import React from 'react' +import useActiveElement from '@dedalik/use-react/useActiveElement' + +function describeElement(el: Element | null): string { + if (!el || !(el instanceof HTMLElement)) return 'none' + const id = el.id ? `#${el.id}` : '' + const name = 'name' in el && typeof (el as HTMLInputElement).name === 'string' ? (el as HTMLInputElement).name : '' + return `${el.tagName.toLowerCase()}${id}${name ? `[name=${name}]` : ''}` +} + +function ActiveElementDemo() { + const active = useActiveElement() + + return React.createElement( + 'div', + { className: 'hook-demo-surface' }, + React.createElement( + 'p', + { className: 'hook-demo-hint' }, + 'Move focus with mouse, Tab, or programmatic focus to see document.activeElement update in real time.', + ), + React.createElement( + 'p', + { style: { margin: '0 0 10px' } }, + 'Active element: ', + React.createElement('strong', null, describeElement(active)), + ), + React.createElement( + 'div', + { style: { display: 'grid', gap: 8, maxWidth: 460 } }, + React.createElement( + 'label', + { htmlFor: 'ae-email', style: { display: 'grid', gap: 4 } }, + React.createElement('span', null, 'Email'), + React.createElement('input', { + id: 'ae-email', + name: 'email', + type: 'email', + placeholder: 'you@example.com', + }), + ), + React.createElement( + 'label', + { htmlFor: 'ae-notes', style: { display: 'grid', gap: 4 } }, + React.createElement('span', null, 'Notes'), + React.createElement('textarea', { + id: 'ae-notes', + name: 'notes', + rows: 3, + placeholder: 'Type here...', + }), + ), + React.createElement( + 'div', + { className: 'hook-demo-toolbar', style: { gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' } }, + React.createElement( + 'button', + { + type: 'button', + onClick: () => { + const el = document.getElementById('ae-email') as HTMLInputElement | null + el?.focus() + }, + }, + 'Focus email', + ), + React.createElement( + 'button', + { + type: 'button', + onClick: () => { + const el = document.getElementById('ae-notes') as HTMLTextAreaElement | null + el?.focus() + }, + }, + 'Focus notes', + ), + React.createElement( + 'button', + { + type: 'button', + onClick: () => { + const activeEl = document.activeElement as HTMLElement | null + activeEl?.blur() + }, + }, + 'Blur active', + ), + ), + ), + ) +} + +export const sourceJsx = `import useActiveElement from '@dedalik/use-react/useActiveElement' + +function describeElement(el: Element | null): string { + if (!el || !(el instanceof HTMLElement)) return 'none' + const id = el.id ? '#' + el.id : '' + const name = 'name' in el && typeof (el as HTMLInputElement).name === 'string' ? (el as HTMLInputElement).name : '' + return el.tagName.toLowerCase() + id + (name ? '[name=' + name + ']' : '') +} + +export default function ActiveElementDemo() { + const active = useActiveElement() + + return ( +
+

+ Move focus with mouse, Tab, or programmatic focus to see document.activeElement update in real time. +

+

+ Active element: {describeElement(active)} +

+ +
+ +