@ttt-productions/ui-core 0.11.12 → 0.11.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ export interface ReturnScrollEntry {
2
+ /** The saved `window.scrollY`. */
3
+ y: number;
4
+ /** Epoch ms at save time (diagnostic; the caller decides eligibility, not the hook). */
5
+ savedAt: number;
6
+ }
7
+ export interface UseReturnScrollOptions {
8
+ /** The caller's CANONICAL list URL — equivalent list states must give the same key. */
9
+ key: string;
10
+ /** Decided by the caller ONCE at mount (e.g. "the list's data was already cached on first render"). */
11
+ eligible: boolean;
12
+ /** The list is displayed at its real height: data present, no error surface in its place. */
13
+ ready: boolean;
14
+ }
15
+ export interface UseReturnScrollResult {
16
+ /** Record the current window offset for `key`. Call right before navigating away. */
17
+ save: () => void;
18
+ }
19
+ /** The stored entry for `key`, or null when absent, unreadable, or malformed. */
20
+ export declare function readReturnScroll(key: string): ReturnScrollEntry | null;
21
+ /** Record `y` (default: the current `window.scrollY`) for `key`. No-op without storage. */
22
+ export declare function saveReturnScroll(key: string, y?: number): void;
23
+ /** Drop the entry for `key` (e.g. the list changed underneath the saved offset). */
24
+ export declare function clearReturnScroll(key: string): void;
25
+ export declare function useReturnScroll({ key, eligible, ready }: UseReturnScrollOptions): UseReturnScrollResult;
26
+ //# sourceMappingURL=use-return-scroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-return-scroll.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-return-scroll.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,iBAAiB;IAChC,kCAAkC;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ,uGAAuG;IACvG,QAAQ,EAAE,OAAO,CAAC;IAClB,6FAA6F;IAC7F,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAeD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CActE;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAU9D;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAQnD;AASD,wBAAgB,eAAe,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,sBAAsB,GAAG,qBAAqB,CAuCvG"}
@@ -0,0 +1,123 @@
1
+ "use client";
2
+ // Return-scroll: bring a list back to where the user left it after a REAL route change.
3
+ //
4
+ // A same-page view swap keeps its component mounted and can restore from a ref; a list
5
+ // whose detail pages have their own URLs unmounts, so the offset has to survive in
6
+ // per-tab storage and the restore has to wait until the list is displayed at its real
7
+ // height again. This hook is the ONE owner of that mechanism. It knows nothing about
8
+ // what the list is — the caller supplies a canonical key, decides eligibility once at
9
+ // mount, and reports readiness.
10
+ //
11
+ // Contract (every rule below is tested):
12
+ // - `save()` writes { y, savedAt } for `key` — call it immediately before navigating
13
+ // away (inside any guarded-navigation callback, so a cancelled leave writes nothing).
14
+ // - `eligible` is latched per key the first time the hook sees that key. Not eligible
15
+ // ⇒ the entry is deleted immediately and permanently for that key; a later flip
16
+ // of `eligible` or `ready` never re-arms the restore.
17
+ // - `ready` ⇒ the restore fires ONCE, in a requestAnimationFrame, `behavior: "instant"`,
18
+ // then consumes the entry. The scheduled frame is cancelled on unmount or key change.
19
+ // React Strict Mode's mount/unmount/mount cancels and reschedules, so exactly one
20
+ // scroll happens and the entry is consumed exactly once.
21
+ // - Blocked, unavailable, or malformed storage is a silent no-op.
22
+ import * as React from "react";
23
+ const STORAGE_PREFIX = "return-scroll:";
24
+ function storageKey(key) {
25
+ return `${STORAGE_PREFIX}${key}`;
26
+ }
27
+ function sessionStorageOrNull() {
28
+ try {
29
+ if (typeof window === "undefined")
30
+ return null;
31
+ return window.sessionStorage ?? null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** The stored entry for `key`, or null when absent, unreadable, or malformed. */
38
+ export function readReturnScroll(key) {
39
+ const storage = sessionStorageOrNull();
40
+ if (!storage)
41
+ return null;
42
+ try {
43
+ const raw = storage.getItem(storageKey(key));
44
+ if (!raw)
45
+ return null;
46
+ const parsed = JSON.parse(raw);
47
+ if (typeof parsed !== "object" || parsed === null)
48
+ return null;
49
+ const { y, savedAt } = parsed;
50
+ if (typeof y !== "number" || !Number.isFinite(y) || y < 0)
51
+ return null;
52
+ return { y, savedAt: typeof savedAt === "number" ? savedAt : 0 };
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ /** Record `y` (default: the current `window.scrollY`) for `key`. No-op without storage. */
59
+ export function saveReturnScroll(key, y) {
60
+ const storage = sessionStorageOrNull();
61
+ if (!storage)
62
+ return;
63
+ try {
64
+ const offset = typeof y === "number" ? y : window.scrollY;
65
+ const entry = { y: offset, savedAt: Date.now() };
66
+ storage.setItem(storageKey(key), JSON.stringify(entry));
67
+ }
68
+ catch {
69
+ /* storage blocked or full — nothing to restore later, which is the honest outcome */
70
+ }
71
+ }
72
+ /** Drop the entry for `key` (e.g. the list changed underneath the saved offset). */
73
+ export function clearReturnScroll(key) {
74
+ const storage = sessionStorageOrNull();
75
+ if (!storage)
76
+ return;
77
+ try {
78
+ storage.removeItem(storageKey(key));
79
+ }
80
+ catch {
81
+ /* nothing to clear */
82
+ }
83
+ }
84
+ export function useReturnScroll({ key, eligible, ready }) {
85
+ const decisionRef = React.useRef(null);
86
+ // Decide ONCE per key, the first time the hook sees it. Effects may read and write refs;
87
+ // the decision is latched, so a later `eligible` flip finds it already made and returns.
88
+ React.useEffect(() => {
89
+ if (decisionRef.current?.key === key)
90
+ return;
91
+ const entry = readReturnScroll(key);
92
+ if (!entry) {
93
+ decisionRef.current = { key, y: null, done: true };
94
+ return;
95
+ }
96
+ if (!eligible) {
97
+ // A cold return: nothing cached to restore against. Discard permanently for this visit.
98
+ clearReturnScroll(key);
99
+ decisionRef.current = { key, y: null, done: true };
100
+ return;
101
+ }
102
+ decisionRef.current = { key, y: entry.y, done: false };
103
+ }, [key, eligible]);
104
+ // Restore once the list is displayed at its real height. The frame is cancelled on unmount
105
+ // or key change; Strict Mode's re-run reschedules it, so exactly one scroll happens.
106
+ React.useEffect(() => {
107
+ const decision = decisionRef.current;
108
+ if (!decision || decision.key !== key || decision.y === null || decision.done || !ready)
109
+ return;
110
+ if (typeof window === "undefined")
111
+ return;
112
+ const y = decision.y;
113
+ const frame = window.requestAnimationFrame(() => {
114
+ window.scrollTo({ top: y, left: 0, behavior: "instant" });
115
+ clearReturnScroll(key);
116
+ decision.done = true;
117
+ });
118
+ return () => window.cancelAnimationFrame(frame);
119
+ }, [key, ready]);
120
+ const save = React.useCallback(() => saveReturnScroll(key), [key]);
121
+ return { save };
122
+ }
123
+ //# sourceMappingURL=use-return-scroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-return-scroll.js","sourceRoot":"","sources":["../../../src/react/hooks/use-return-scroll.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n// Return-scroll: bring a list back to where the user left it after a REAL route change.\n//\n// A same-page view swap keeps its component mounted and can restore from a ref; a list\n// whose detail pages have their own URLs unmounts, so the offset has to survive in\n// per-tab storage and the restore has to wait until the list is displayed at its real\n// height again. This hook is the ONE owner of that mechanism. It knows nothing about\n// what the list is — the caller supplies a canonical key, decides eligibility once at\n// mount, and reports readiness.\n//\n// Contract (every rule below is tested):\n// - `save()` writes { y, savedAt } for `key` — call it immediately before navigating\n// away (inside any guarded-navigation callback, so a cancelled leave writes nothing).\n// - `eligible` is latched per key the first time the hook sees that key. Not eligible\n// ⇒ the entry is deleted immediately and permanently for that key; a later flip\n// of `eligible` or `ready` never re-arms the restore.\n// - `ready` ⇒ the restore fires ONCE, in a requestAnimationFrame, `behavior: \"instant\"`,\n// then consumes the entry. The scheduled frame is cancelled on unmount or key change.\n// React Strict Mode's mount/unmount/mount cancels and reschedules, so exactly one\n// scroll happens and the entry is consumed exactly once.\n// - Blocked, unavailable, or malformed storage is a silent no-op.\n\nimport * as React from \"react\";\n\nconst STORAGE_PREFIX = \"return-scroll:\";\n\nexport interface ReturnScrollEntry {\n /** The saved `window.scrollY`. */\n y: number;\n /** Epoch ms at save time (diagnostic; the caller decides eligibility, not the hook). */\n savedAt: number;\n}\n\nexport interface UseReturnScrollOptions {\n /** The caller's CANONICAL list URL — equivalent list states must give the same key. */\n key: string;\n /** Decided by the caller ONCE at mount (e.g. \"the list's data was already cached on first render\"). */\n eligible: boolean;\n /** The list is displayed at its real height: data present, no error surface in its place. */\n ready: boolean;\n}\n\nexport interface UseReturnScrollResult {\n /** Record the current window offset for `key`. Call right before navigating away. */\n save: () => void;\n}\n\nfunction storageKey(key: string): string {\n return `${STORAGE_PREFIX}${key}`;\n}\n\nfunction sessionStorageOrNull(): Storage | null {\n try {\n if (typeof window === \"undefined\") return null;\n return window.sessionStorage ?? null;\n } catch {\n return null;\n }\n}\n\n/** The stored entry for `key`, or null when absent, unreadable, or malformed. */\nexport function readReturnScroll(key: string): ReturnScrollEntry | null {\n const storage = sessionStorageOrNull();\n if (!storage) return null;\n try {\n const raw = storage.getItem(storageKey(key));\n if (!raw) return null;\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== \"object\" || parsed === null) return null;\n const { y, savedAt } = parsed as { y?: unknown; savedAt?: unknown };\n if (typeof y !== \"number\" || !Number.isFinite(y) || y < 0) return null;\n return { y, savedAt: typeof savedAt === \"number\" ? savedAt : 0 };\n } catch {\n return null;\n }\n}\n\n/** Record `y` (default: the current `window.scrollY`) for `key`. No-op without storage. */\nexport function saveReturnScroll(key: string, y?: number): void {\n const storage = sessionStorageOrNull();\n if (!storage) return;\n try {\n const offset = typeof y === \"number\" ? y : window.scrollY;\n const entry: ReturnScrollEntry = { y: offset, savedAt: Date.now() };\n storage.setItem(storageKey(key), JSON.stringify(entry));\n } catch {\n /* storage blocked or full — nothing to restore later, which is the honest outcome */\n }\n}\n\n/** Drop the entry for `key` (e.g. the list changed underneath the saved offset). */\nexport function clearReturnScroll(key: string): void {\n const storage = sessionStorageOrNull();\n if (!storage) return;\n try {\n storage.removeItem(storageKey(key));\n } catch {\n /* nothing to clear */\n }\n}\n\ninterface Decision {\n key: string;\n /** The offset to restore, or null when this key's return is not restorable. */\n y: number | null;\n done: boolean;\n}\n\nexport function useReturnScroll({ key, eligible, ready }: UseReturnScrollOptions): UseReturnScrollResult {\n const decisionRef = React.useRef<Decision | null>(null);\n\n // Decide ONCE per key, the first time the hook sees it. Effects may read and write refs;\n // the decision is latched, so a later `eligible` flip finds it already made and returns.\n React.useEffect(() => {\n if (decisionRef.current?.key === key) return;\n const entry = readReturnScroll(key);\n if (!entry) {\n decisionRef.current = { key, y: null, done: true };\n return;\n }\n if (!eligible) {\n // A cold return: nothing cached to restore against. Discard permanently for this visit.\n clearReturnScroll(key);\n decisionRef.current = { key, y: null, done: true };\n return;\n }\n decisionRef.current = { key, y: entry.y, done: false };\n }, [key, eligible]);\n\n // Restore once the list is displayed at its real height. The frame is cancelled on unmount\n // or key change; Strict Mode's re-run reschedules it, so exactly one scroll happens.\n React.useEffect(() => {\n const decision = decisionRef.current;\n if (!decision || decision.key !== key || decision.y === null || decision.done || !ready) return;\n if (typeof window === \"undefined\") return;\n const y = decision.y;\n const frame = window.requestAnimationFrame(() => {\n window.scrollTo({ top: y, left: 0, behavior: \"instant\" as ScrollBehavior });\n clearReturnScroll(key);\n decision.done = true;\n });\n return () => window.cancelAnimationFrame(frame);\n }, [key, ready]);\n\n const save = React.useCallback(() => saveReturnScroll(key), [key]);\n\n return { save };\n}\n"]}
@@ -57,4 +57,6 @@ export { usePagedList } from "./hooks/use-paged-list.js";
57
57
  export type { ListPaginationState, PagedList, UsePagedListOptions } from "./hooks/use-paged-list.js";
58
58
  export { useCursorPage } from "./hooks/use-cursor-page.js";
59
59
  export type { CursorPage, CursorPager, UseCursorPageOptions } from "./hooks/use-cursor-page.js";
60
+ export { useReturnScroll, saveReturnScroll, clearReturnScroll, readReturnScroll } from "./hooks/use-return-scroll.js";
61
+ export type { ReturnScrollEntry, UseReturnScrollOptions, UseReturnScrollResult } from "./hooks/use-return-scroll.js";
60
62
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
@@ -46,4 +46,8 @@ export { ChunkErrorRecovery } from "./components/chunk-error-recovery.js";
46
46
  export { ListPagination } from "./components/list-pagination.js";
47
47
  export { usePagedList } from "./hooks/use-paged-list.js";
48
48
  export { useCursorPage } from "./hooks/use-cursor-page.js";
49
+ // Return-scroll: restore a list's window offset when the user comes back to it after a
50
+ // route change (save on leave, restore once on an eligible + ready return, keyed by the
51
+ // caller's canonical list URL).
52
+ export { useReturnScroll, saveReturnScroll, clearReturnScroll, readReturnScroll } from "./hooks/use-return-scroll.js";
49
53
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nexport type { ButtonProps } from \"./components/button.js\";\nexport { Button, buttonVariants } from \"./components/button.js\";\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from \"./components/card.js\";\nexport { Label } from \"./components/label.js\";\nexport { Textarea } from \"./components/textarea.js\";\nexport { Skeleton } from \"./components/skeleton.js\";\nexport { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from \"./components/dialog.js\";\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } from \"./components/popover.js\";\nexport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } from \"./components/dropdown-menu.js\";\nexport { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut } from \"./components/menubar.js\";\nexport { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from \"./components/alert-dialog.js\";\nexport { Alert, AlertTitle, AlertDescription } from \"./components/alert.js\";\nexport type { ToastProps, ToastActionElement } from \"./components/toast.js\";\nexport { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction } from \"./components/toast.js\";\nexport { Toaster } from \"./components/toaster.js\";\nexport type { ToastVariant, ToasterToast } from \"./hooks/use-toast.js\";\nexport { toast, useToast } from \"./hooks/use-toast.js\";\nexport { Input } from \"./components/input.js\";\nexport { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton } from \"./components/select.js\";\nexport { Tabs, TabsList, TabsTrigger, TabsContent } from \"./components/tabs.js\";\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from \"./components/tooltip.js\";\nexport { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField } from \"./components/form.js\";\nexport { Checkbox } from \"./components/checkbox.js\";\nexport { Switch } from \"./components/switch.js\";\nexport type { BadgeProps } from \"./components/badge.js\";\nexport { Badge, badgeVariants } from \"./components/badge.js\";\nexport { Avatar, AvatarImage, AvatarFallback } from \"./components/avatar.js\";\nexport { Separator } from \"./components/separator.js\";\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"./components/accordion.js\";\nexport { RadioGroup, RadioGroupItem } from \"./components/radio-group.js\";\nexport { Progress } from \"./components/progress.js\";\nexport { Slider } from \"./components/slider.js\";\nexport type { DatePickerProps } from \"./components/date-picker.js\";\nexport { DatePicker } from \"./components/date-picker.js\";\nexport { ScrollArea, ScrollBar } from \"./components/scroll-area.js\";\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } from \"./components/table.js\";\nexport { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from \"./components/sheet.js\";\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent } from \"./components/collapsible.js\";\nexport type { SearchDropdownProps } from \"./components/search-dropdown.js\";\nexport { SearchDropdown } from \"./components/search-dropdown.js\";\n\nexport type { ConsequenceDialogProps, ConsequenceDialogReason, ConsequenceDialogTypedConfirmation } from \"./components/consequence-dialog.js\";\nexport { ConsequenceDialog } from \"./components/consequence-dialog.js\";\n\nexport type { MaxWidthOption, ScreenAdaptiveViewProps } from \"./components/layout/screen-adaptive-view.js\";\nexport { ScreenAdaptiveView } from \"./components/layout/screen-adaptive-view.js\";\n\nexport { useMediaQuery } from \"./hooks/use-media-query.js\";\n\n// Shared building blocks\nexport { RelativeTime } from \"./components/relative-time.js\";\nexport type { RelativeTimeProps } from \"./components/relative-time.js\";\n\nexport { EndOfListIndicator } from \"./components/end-of-list-indicator.js\";\nexport type { EndOfListIndicatorProps } from \"./components/end-of-list-indicator.js\";\n\nexport { ScrollToTopButton } from \"./components/scroll-to-top-button.js\";\nexport type { ScrollToTopButtonProps } from \"./components/scroll-to-top-button.js\";\n\nexport { ChunkErrorRecovery } from \"./components/chunk-error-recovery.js\";\nexport type { ChunkErrorRecoveryProps } from \"./components/chunk-error-recovery.js\";\n\n// List pagination — the one Previous / counter / Next control plus the page-state\n// hooks for its two flavors (known total, and cursor/`hasMore`).\nexport { ListPagination } from \"./components/list-pagination.js\";\nexport type { ListPaginationProps } from \"./components/list-pagination.js\";\n\nexport { usePagedList } from \"./hooks/use-paged-list.js\";\nexport type { ListPaginationState, PagedList, UsePagedListOptions } from \"./hooks/use-paged-list.js\";\n\nexport { useCursorPage } from \"./hooks/use-cursor-page.js\";\nexport type { CursorPage, CursorPager, UseCursorPageOptions } from \"./hooks/use-cursor-page.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nexport type { ButtonProps } from \"./components/button.js\";\nexport { Button, buttonVariants } from \"./components/button.js\";\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from \"./components/card.js\";\nexport { Label } from \"./components/label.js\";\nexport { Textarea } from \"./components/textarea.js\";\nexport { Skeleton } from \"./components/skeleton.js\";\nexport { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from \"./components/dialog.js\";\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } from \"./components/popover.js\";\nexport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuRadioGroup } from \"./components/dropdown-menu.js\";\nexport { Menubar, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem, MenubarSeparator, MenubarLabel, MenubarCheckboxItem, MenubarRadioGroup, MenubarRadioItem, MenubarPortal, MenubarSubContent, MenubarSubTrigger, MenubarGroup, MenubarSub, MenubarShortcut } from \"./components/menubar.js\";\nexport { AlertDialog, AlertDialogPortal, AlertDialogOverlay, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from \"./components/alert-dialog.js\";\nexport { Alert, AlertTitle, AlertDescription } from \"./components/alert.js\";\nexport type { ToastProps, ToastActionElement } from \"./components/toast.js\";\nexport { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction } from \"./components/toast.js\";\nexport { Toaster } from \"./components/toaster.js\";\nexport type { ToastVariant, ToasterToast } from \"./hooks/use-toast.js\";\nexport { toast, useToast } from \"./hooks/use-toast.js\";\nexport { Input } from \"./components/input.js\";\nexport { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton } from \"./components/select.js\";\nexport { Tabs, TabsList, TabsTrigger, TabsContent } from \"./components/tabs.js\";\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from \"./components/tooltip.js\";\nexport { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField } from \"./components/form.js\";\nexport { Checkbox } from \"./components/checkbox.js\";\nexport { Switch } from \"./components/switch.js\";\nexport type { BadgeProps } from \"./components/badge.js\";\nexport { Badge, badgeVariants } from \"./components/badge.js\";\nexport { Avatar, AvatarImage, AvatarFallback } from \"./components/avatar.js\";\nexport { Separator } from \"./components/separator.js\";\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from \"./components/accordion.js\";\nexport { RadioGroup, RadioGroupItem } from \"./components/radio-group.js\";\nexport { Progress } from \"./components/progress.js\";\nexport { Slider } from \"./components/slider.js\";\nexport type { DatePickerProps } from \"./components/date-picker.js\";\nexport { DatePicker } from \"./components/date-picker.js\";\nexport { ScrollArea, ScrollBar } from \"./components/scroll-area.js\";\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption } from \"./components/table.js\";\nexport { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription } from \"./components/sheet.js\";\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent } from \"./components/collapsible.js\";\nexport type { SearchDropdownProps } from \"./components/search-dropdown.js\";\nexport { SearchDropdown } from \"./components/search-dropdown.js\";\n\nexport type { ConsequenceDialogProps, ConsequenceDialogReason, ConsequenceDialogTypedConfirmation } from \"./components/consequence-dialog.js\";\nexport { ConsequenceDialog } from \"./components/consequence-dialog.js\";\n\nexport type { MaxWidthOption, ScreenAdaptiveViewProps } from \"./components/layout/screen-adaptive-view.js\";\nexport { ScreenAdaptiveView } from \"./components/layout/screen-adaptive-view.js\";\n\nexport { useMediaQuery } from \"./hooks/use-media-query.js\";\n\n// Shared building blocks\nexport { RelativeTime } from \"./components/relative-time.js\";\nexport type { RelativeTimeProps } from \"./components/relative-time.js\";\n\nexport { EndOfListIndicator } from \"./components/end-of-list-indicator.js\";\nexport type { EndOfListIndicatorProps } from \"./components/end-of-list-indicator.js\";\n\nexport { ScrollToTopButton } from \"./components/scroll-to-top-button.js\";\nexport type { ScrollToTopButtonProps } from \"./components/scroll-to-top-button.js\";\n\nexport { ChunkErrorRecovery } from \"./components/chunk-error-recovery.js\";\nexport type { ChunkErrorRecoveryProps } from \"./components/chunk-error-recovery.js\";\n\n// List pagination — the one Previous / counter / Next control plus the page-state\n// hooks for its two flavors (known total, and cursor/`hasMore`).\nexport { ListPagination } from \"./components/list-pagination.js\";\nexport type { ListPaginationProps } from \"./components/list-pagination.js\";\n\nexport { usePagedList } from \"./hooks/use-paged-list.js\";\nexport type { ListPaginationState, PagedList, UsePagedListOptions } from \"./hooks/use-paged-list.js\";\n\nexport { useCursorPage } from \"./hooks/use-cursor-page.js\";\nexport type { CursorPage, CursorPager, UseCursorPageOptions } from \"./hooks/use-cursor-page.js\";\n\n// Return-scroll: restore a list's window offset when the user comes back to it after a\n// route change (save on leave, restore once on an eligible + ready return, keyed by the\n// caller's canonical list URL).\nexport { useReturnScroll, saveReturnScroll, clearReturnScroll, readReturnScroll } from \"./hooks/use-return-scroll.js\";\nexport type { ReturnScrollEntry, UseReturnScrollOptions, UseReturnScrollResult } from \"./hooks/use-return-scroll.js\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/ui-core",
3
- "version": "0.11.12",
3
+ "version": "0.11.14",
4
4
  "description": "Shared UI components for TTT Productions projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,7 +49,7 @@
49
49
  "react-dom": ">=19.0.0",
50
50
  "react-hook-form": ">=7.0.0",
51
51
  "@hookform/resolvers": ">=3.0.0",
52
- "@ttt-productions/firebase-helpers": "^0.12.4"
52
+ "@ttt-productions/firebase-helpers": "^0.12.7"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "react": {
@@ -99,7 +99,7 @@
99
99
  "typescript": "^5.8.3",
100
100
  "react-hook-form": "^7.69.0",
101
101
  "@hookform/resolvers": "^5.2.2",
102
- "@ttt-productions/firebase-helpers": "^0.12.4"
102
+ "@ttt-productions/firebase-helpers": "^0.12.7"
103
103
  },
104
104
  "author": "DJ (TTT Productions)",
105
105
  "license": "MIT"
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ // Return-scroll: bring a list back to where the user left it after a REAL route change.
4
+ //
5
+ // A same-page view swap keeps its component mounted and can restore from a ref; a list
6
+ // whose detail pages have their own URLs unmounts, so the offset has to survive in
7
+ // per-tab storage and the restore has to wait until the list is displayed at its real
8
+ // height again. This hook is the ONE owner of that mechanism. It knows nothing about
9
+ // what the list is — the caller supplies a canonical key, decides eligibility once at
10
+ // mount, and reports readiness.
11
+ //
12
+ // Contract (every rule below is tested):
13
+ // - `save()` writes { y, savedAt } for `key` — call it immediately before navigating
14
+ // away (inside any guarded-navigation callback, so a cancelled leave writes nothing).
15
+ // - `eligible` is latched per key the first time the hook sees that key. Not eligible
16
+ // ⇒ the entry is deleted immediately and permanently for that key; a later flip
17
+ // of `eligible` or `ready` never re-arms the restore.
18
+ // - `ready` ⇒ the restore fires ONCE, in a requestAnimationFrame, `behavior: "instant"`,
19
+ // then consumes the entry. The scheduled frame is cancelled on unmount or key change.
20
+ // React Strict Mode's mount/unmount/mount cancels and reschedules, so exactly one
21
+ // scroll happens and the entry is consumed exactly once.
22
+ // - Blocked, unavailable, or malformed storage is a silent no-op.
23
+
24
+ import * as React from "react";
25
+
26
+ const STORAGE_PREFIX = "return-scroll:";
27
+
28
+ export interface ReturnScrollEntry {
29
+ /** The saved `window.scrollY`. */
30
+ y: number;
31
+ /** Epoch ms at save time (diagnostic; the caller decides eligibility, not the hook). */
32
+ savedAt: number;
33
+ }
34
+
35
+ export interface UseReturnScrollOptions {
36
+ /** The caller's CANONICAL list URL — equivalent list states must give the same key. */
37
+ key: string;
38
+ /** Decided by the caller ONCE at mount (e.g. "the list's data was already cached on first render"). */
39
+ eligible: boolean;
40
+ /** The list is displayed at its real height: data present, no error surface in its place. */
41
+ ready: boolean;
42
+ }
43
+
44
+ export interface UseReturnScrollResult {
45
+ /** Record the current window offset for `key`. Call right before navigating away. */
46
+ save: () => void;
47
+ }
48
+
49
+ function storageKey(key: string): string {
50
+ return `${STORAGE_PREFIX}${key}`;
51
+ }
52
+
53
+ function sessionStorageOrNull(): Storage | null {
54
+ try {
55
+ if (typeof window === "undefined") return null;
56
+ return window.sessionStorage ?? null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /** The stored entry for `key`, or null when absent, unreadable, or malformed. */
63
+ export function readReturnScroll(key: string): ReturnScrollEntry | null {
64
+ const storage = sessionStorageOrNull();
65
+ if (!storage) return null;
66
+ try {
67
+ const raw = storage.getItem(storageKey(key));
68
+ if (!raw) return null;
69
+ const parsed: unknown = JSON.parse(raw);
70
+ if (typeof parsed !== "object" || parsed === null) return null;
71
+ const { y, savedAt } = parsed as { y?: unknown; savedAt?: unknown };
72
+ if (typeof y !== "number" || !Number.isFinite(y) || y < 0) return null;
73
+ return { y, savedAt: typeof savedAt === "number" ? savedAt : 0 };
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /** Record `y` (default: the current `window.scrollY`) for `key`. No-op without storage. */
80
+ export function saveReturnScroll(key: string, y?: number): void {
81
+ const storage = sessionStorageOrNull();
82
+ if (!storage) return;
83
+ try {
84
+ const offset = typeof y === "number" ? y : window.scrollY;
85
+ const entry: ReturnScrollEntry = { y: offset, savedAt: Date.now() };
86
+ storage.setItem(storageKey(key), JSON.stringify(entry));
87
+ } catch {
88
+ /* storage blocked or full — nothing to restore later, which is the honest outcome */
89
+ }
90
+ }
91
+
92
+ /** Drop the entry for `key` (e.g. the list changed underneath the saved offset). */
93
+ export function clearReturnScroll(key: string): void {
94
+ const storage = sessionStorageOrNull();
95
+ if (!storage) return;
96
+ try {
97
+ storage.removeItem(storageKey(key));
98
+ } catch {
99
+ /* nothing to clear */
100
+ }
101
+ }
102
+
103
+ interface Decision {
104
+ key: string;
105
+ /** The offset to restore, or null when this key's return is not restorable. */
106
+ y: number | null;
107
+ done: boolean;
108
+ }
109
+
110
+ export function useReturnScroll({ key, eligible, ready }: UseReturnScrollOptions): UseReturnScrollResult {
111
+ const decisionRef = React.useRef<Decision | null>(null);
112
+
113
+ // Decide ONCE per key, the first time the hook sees it. Effects may read and write refs;
114
+ // the decision is latched, so a later `eligible` flip finds it already made and returns.
115
+ React.useEffect(() => {
116
+ if (decisionRef.current?.key === key) return;
117
+ const entry = readReturnScroll(key);
118
+ if (!entry) {
119
+ decisionRef.current = { key, y: null, done: true };
120
+ return;
121
+ }
122
+ if (!eligible) {
123
+ // A cold return: nothing cached to restore against. Discard permanently for this visit.
124
+ clearReturnScroll(key);
125
+ decisionRef.current = { key, y: null, done: true };
126
+ return;
127
+ }
128
+ decisionRef.current = { key, y: entry.y, done: false };
129
+ }, [key, eligible]);
130
+
131
+ // Restore once the list is displayed at its real height. The frame is cancelled on unmount
132
+ // or key change; Strict Mode's re-run reschedules it, so exactly one scroll happens.
133
+ React.useEffect(() => {
134
+ const decision = decisionRef.current;
135
+ if (!decision || decision.key !== key || decision.y === null || decision.done || !ready) return;
136
+ if (typeof window === "undefined") return;
137
+ const y = decision.y;
138
+ const frame = window.requestAnimationFrame(() => {
139
+ window.scrollTo({ top: y, left: 0, behavior: "instant" as ScrollBehavior });
140
+ clearReturnScroll(key);
141
+ decision.done = true;
142
+ });
143
+ return () => window.cancelAnimationFrame(frame);
144
+ }, [key, ready]);
145
+
146
+ const save = React.useCallback(() => saveReturnScroll(key), [key]);
147
+
148
+ return { save };
149
+ }
@@ -72,3 +72,9 @@ export type { ListPaginationState, PagedList, UsePagedListOptions } from "./hook
72
72
 
73
73
  export { useCursorPage } from "./hooks/use-cursor-page.js";
74
74
  export type { CursorPage, CursorPager, UseCursorPageOptions } from "./hooks/use-cursor-page.js";
75
+
76
+ // Return-scroll: restore a list's window offset when the user comes back to it after a
77
+ // route change (save on leave, restore once on an eligible + ready return, keyed by the
78
+ // caller's canonical list URL).
79
+ export { useReturnScroll, saveReturnScroll, clearReturnScroll, readReturnScroll } from "./hooks/use-return-scroll.js";
80
+ export type { ReturnScrollEntry, UseReturnScrollOptions, UseReturnScrollResult } from "./hooks/use-return-scroll.js";