Development
Hook
Hooks
What piyaz runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add FrkAk/piyaz > /plugin install piyaz@piyaz
Ships with piyaz. Installing the plugin gets these hooks.
Where it lives
- hooks/useCopyToClipboard.tsGitHub
Read the script
import { useCallback, useEffect, useRef, useState } from "react"; /** Copy-to-clipboard lifecycle state. */ export type CopyStatus = "idle" | "copied" | "error"; /** * Copy-to-clipboard state with auto-reset and error surfacing. * * Catches clipboard write failures (insecure context, Permissions-Policy, * unfocused document) and exposes them via the `'error'` status. Clears any * pending reset timer on unmount or rapid re-copy to avoid stale updates. * * @param resetMs - How long a non-idle status persists (default 1200ms). * @returns `{ status, copy }` — call `copy(text)` per invocation. */ export function useCopyToClipboard(resetMs = 1200) { const [status, setStatus] = useState<CopyStatus>("idle"); const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); useEffect(() => { return () => { if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; }, []); /** * Write `text` to the clipboard and update status. * @param text - String to write to the clipboard. * @returns Resolves once status has transitioned. */ const copy = useCallback( async (text: string) => { if (timeoutRef.current !== null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } try { await navigator.clipboard.writeText(text); setStatus("copied"); } catch { setStatus("error"); } timeoutRef.current = setTimeout(() => { setStatus("idle"); timeoutRef.current = null; }, resetMs); }, [resetMs], ); return { status, copy }; } - hooks/useInlineEdit.tsGitHub
Read the script
"use client"; import { useRef } from "react"; import type { FocusEvent, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, } from "react"; import { caretOffsetFromPoint, EDIT_HINT_LABEL, placeCaret, } from "@/components/shared/inlineEdit"; /** Caret placement when entering edit mode. */ type CaretMode = "point" | "end"; /** Props spread onto the display element to make it an inline-edit trigger. */ interface InlineEditTriggerProps { tabIndex: number; title: string; onDoubleClick: (event: ReactMouseEvent<HTMLElement>) => void; onKeyDown: (event: ReactKeyboardEvent<HTMLElement>) => void; } /** Handlers returned by `useInlineEdit` for wiring an inline-edit field. */ interface InlineEditHandlers { /** Spread on the display element: double-click or Enter/Space enters edit mode. Keyboard activation only fires on the element itself, so focusable descendants (markdown links) stay reachable. */ triggerProps: InlineEditTriggerProps; /** Bind to the touch-only edit button's `onClick`; enters edit mode with the caret at the end. */ onActivate: () => void; /** Bind to the editor's `onFocus` to position the caret. */ onEditorFocus: ( event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>, ) => void; } /** * Wire double-click and keyboard activation to edit, with caret positioning, for an inline field. * @param startEditing - Enters edit mode (sets the field's local editing state). * @param caret - `"point"` places the caret at the double-clicked character, for plain-text fields whose rendered text mirrors the editable value; `"end"` focuses at the end, for markdown fields and any activation that carries no pointer coordinate (keyboard, touch button). * @returns Trigger props for the display element, the touch-button activate handler, and the editor focus handler. */ export function useInlineEdit( startEditing: () => void, caret: CaretMode = "end", ): InlineEditHandlers { const pendingCaret = useRef<number | null>(null); return { triggerProps: { tabIndex: 0, title: EDIT_HINT_LABEL, onDoubleClick: (event) => { pendingCaret.current = caret === "point" ? caretOffsetFromPoint( event.currentTarget, event.clientX, event.clientY, ) : null; startEditing(); }, onKeyDown: (event) => { if (event.target !== event.currentTarget) return; if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); pendingCaret.current = null; startEditing(); }, }, onActivate: () => { pendingCaret.current = null; startEditing(); }, onEditorFocus: (event) => { placeCaret(event.currentTarget, pendingCaret.current); pendingCaret.current = null; }, }; } - hooks/useMediaQuery.tsGitHub
Read the script
"use client"; import { useCallback, useSyncExternalStore } from "react"; /** No-op subscriber used on the server where `matchMedia` doesn't exist. */ const noopSubscribe = () => () => {}; /** * Track a CSS media query as boolean state. SSR returns the `defaultValue` * (defaults to `false`) and the real value lands on the first client paint. * * @param query - Standard CSS media query string (e.g. `(min-width: 1280px)`). * @param defaultValue - SSR fallback. Defaults to `false`. * @returns `true` when the query currently matches. */ export function useMediaQuery(query: string, defaultValue = false): boolean { // `useSyncExternalStore` re-subscribes on identity change — memoise per `query`. const subscribe = useCallback( (callback: () => void) => { if (typeof window === "undefined") return noopSubscribe(); const mql = window.matchMedia(query); mql.addEventListener("change", callback); return () => mql.removeEventListener("change", callback); }, [query], ); return useSyncExternalStore( subscribe, () => typeof window === "undefined" ? defaultValue : window.matchMedia(query).matches, () => defaultValue, ); } - hooks/useModalChrome.tsGitHub
Read the script
"use client"; import { useEffect, useRef } from "react"; import type React from "react"; /** CSS selector matching tabbable descendants inside the dialog panel. */ const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; /** * Keep keyboard focus inside the dialog panel while Tab is pressed. * @param event - The Tab keydown event. * @param panel - The panel element scoping focusable descendants. * @returns Nothing. */ function trapTabFocus(event: KeyboardEvent, panel: HTMLElement | null): void { if (!panel) return; const focusables = panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR); if (focusables.length === 0) return; const first = focusables[0]; const last = focusables[focusables.length - 1]; const active = document.activeElement as HTMLElement | null; if (event.shiftKey && active === first) { event.preventDefault(); last.focus(); return; } if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); } } /** * Active modal handle tracked by the global stack — capturing onClose * and panel bounds so the topmost dialog can handle Escape and trap Tab * focus, even when modals are nested. */ interface ModalHandle { panelRef: React.RefObject<HTMLElement | null>; onClose: () => void; } const modalStack: ModalHandle[] = []; let globalListenerInstalled = false; let lockedBodyOverflow: string | null = null; /** * Lock background scrolling while any modal is open. Applies when the * first handle pushes onto the stack and restores the body's previous * overflow when the last one pops, so nested modals share one lock. */ function syncScrollLock(): void { if (modalStack.length > 0 && lockedBodyOverflow === null) { lockedBodyOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return; } if (modalStack.length === 0 && lockedBodyOverflow !== null) { document.body.style.overflow = lockedBodyOverflow; lockedBodyOverflow = null; } } /** * Whether any dialog wired through {@link useModalChrome} is currently * open. Page-level Escape handlers (e.g. the detail header's * deselect-task listener) call this to yield to the open dialog instead * of racing it on listener registration order. * * @returns `true` while at least one modal is on the stack. */ export function isModalOpen(): boolean { return modalStack.length > 0; } /** * Install the single document-level keydown listener responsible for * dispatching to the topmost active modal. Idempotent — installs once * and stays registered for the lifetime of the page so subsequent * modal mounts only push/pop the stack. */ function ensureGlobalListenerInstalled(): void { if (globalListenerInstalled) return; globalListenerInstalled = true; document.addEventListener("keydown", (e) => { const top = modalStack[modalStack.length - 1]; if (!top) return; if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); top.onClose(); return; } if (e.key === "Tab") { trapTabFocus(e, top.panelRef.current); } }); } /** * Wires modal chrome behavior: Escape to close, Tab focus trap, focus * restore on unmount, and a background scroll lock while open — the * behavior `aria-modal` promises. Stack-aware so nested modals (e.g. a * destructive confirm dialog opened from inside a settings modal) are * each handled by the topmost dialog only — outer modals stay open * until the inner one dismisses. * * @param open - Whether the modal is currently open. * @param onClose - Callback invoked when Escape pops this modal off * the stack. Closure identity may change across renders; the hook * always dispatches the latest `onClose`. * @param panelRef - Ref to the modal panel — used to bound the focus * trap and to seed initial focus. */ export function useModalChrome( open: boolean, onClose: () => void, panelRef: React.RefObject<HTMLElement | null>, ): void { const previousFocusRef = useRef<HTMLElement | null>(null); const onCloseRef = useRef(onClose); useEffect(() => { onCloseRef.current = onClose; }, [onClose]); useEffect(() => { if (!open) return; ensureGlobalListenerInstalled(); previousFocusRef.current = (document.activeElement as HTMLElement | null) ?? null; const handle: ModalHandle = { panelRef, onClose: () => onCloseRef.current(), }; modalStack.push(handle); syncScrollLock(); const frame = window.requestAnimationFrame(() => { const panel = panelRef.current; if (!panel) return; const focusable = panel.querySelector<HTMLElement>( 'input, textarea, select, button, [tabindex]:not([tabindex="-1"])', ); focusable?.focus(); }); return () => { window.cancelAnimationFrame(frame); const idx = modalStack.indexOf(handle); if (idx !== -1) modalStack.splice(idx, 1); syncScrollLock(); previousFocusRef.current?.focus?.(); }; }, [open, panelRef]); } - hooks/useMounted.tsGitHub
Read the script
"use client"; import { useSyncExternalStore } from "react"; /** No-op subscribe: the mount snapshot never changes after hydration. */ const subscribe = () => () => {}; /** * Detect the first client render. Returns `false` on the server and during * the hydration paint, then `true` once mounted on the client, without a * setState-in-effect. Use to defer viewport-dependent layout past hydration * so it never flashes an SSR-default layout. * * @returns `true` after the component has mounted on the client. */ export function useMounted(): boolean { return useSyncExternalStore( subscribe, () => true, () => false, ); } - hooks/useNotesCollapse.tsGitHub
Read the script
"use client"; import { useCallback, useSyncExternalStore } from "react"; /** Cookie max-age in seconds (1 year). */ const COOKIE_MAX_AGE = 60 * 60 * 24 * 365; interface CookieCollapse { /** Whether the surface is currently collapsed (hidden). */ collapsed: boolean; /** Flip the collapsed state and persist it. */ toggle: () => void; } /** * Build a cookie-persisted collapse hook around one cookie name. Each * call owns its module-level store (listener set plus lazily-read cached * value), so repeated `getSnapshot` calls stay referentially stable. * Server-renders expanded ({@link useSyncExternalStore} server snapshot), * then reconciles to the cookie value after hydration. * * @param cookieName - Cookie persisting the preference. * @returns Hook exposing the collapse state and its toggle. */ function createCookieCollapse(cookieName: string): () => CookieCollapse { const listeners = new Set<() => void>(); let cachedValue: boolean | null = null; /** * Read the persisted value from `document.cookie`. Browser-only. * * @returns `true` when the cookie marks the surface as collapsed. */ function readCookie(): boolean { try { const match = document.cookie.match( new RegExp(`(?:^|; )${cookieName}=([^;]*)`), ); return match?.[1] === "1"; } catch { return false; } } /** * Write the persisted value to `document.cookie`. Browser-only. * * @param next - The new collapse state. */ function writeCookie(next: boolean): void { try { document.cookie = `${cookieName}=${next ? "1" : "0"}; path=/; max-age=${COOKIE_MAX_AGE}; samesite=lax`; } catch { /* swallow cookie errors; preference is non-critical */ } } /** * Subscribe to in-tab collapse changes. * * @param onStoreChange - Notification callback from {@link useSyncExternalStore}. * @returns Unsubscribe function. */ function subscribe(onStoreChange: () => void): () => void { listeners.add(onStoreChange); return () => { listeners.delete(onStoreChange); }; } /** * Read the cached collapse state, lazily loading from the cookie on * first access. * * @returns `true` when the surface should render collapsed. */ function getClientSnapshot(): boolean { if (cachedValue !== null) return cachedValue; cachedValue = readCookie(); return cachedValue; } return function useCookieCollapse(): CookieCollapse { const collapsed = useSyncExternalStore( subscribe, getClientSnapshot, () => false, ); const toggle = useCallback(() => { cachedValue = !getClientSnapshot(); writeCookie(cachedValue); listeners.forEach((l) => l()); }, []); return { collapsed, toggle }; }; } /** Cookie-persisted toggle for hiding the notes tree rail at `lg` and up. */ export const useNotesRailCollapse = createCookieCollapse( "piyaz-notes-rail-collapsed", ); /** Cookie-persisted toggle for hiding the notes settings ribbon at `xl` and up. */ export const useNotesSettingsCollapse = createCookieCollapse( "piyaz-notes-settings-collapsed", ); - hooks/usePopoverAnchor.tsGitHub
- hooks/useSkeletonVisibility.tsGitHub
All 8 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withpiyaz
The agentic workspace where people and agents work together in the loop.
Get the whole plugin
Stats
191
Stars
18
Forks
Active
Maintenance
TypeScript
Language
AGPL-3.0
License
5d ago
Last commit
5mo ago
Created
Repo: FrkAk/piyaz

