@ryuzaki13/react-foundation-lib 2.3.0 → 2.4.0

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.
package/README.md CHANGED
@@ -618,9 +618,9 @@ Pipeline — runtime форматирования ячейки. Он прини
618
618
 
619
619
  | API | Поведение |
620
620
  | -------------------------- | ------------------------------------------------------------------------------------------------- |
621
- | `createNotificationsStore` | Zustand vanilla store, максимум 6 уведомлений, TTL timers, `push/update/upsert/dismiss/clear`. |
622
- | `bindNotifications` | Привязка store к imperative `notify`. |
623
- | `notify` | `push/upsert/update/dismiss/clear`, short methods `success/info/warning/error`, progress pattern. |
621
+ | `createNotificationsStore` | Zustand store: до 6 активных toast, полная session-history и TTL timers. |
622
+ | `bindNotifications` | Привязка store к imperative `notify`. |
623
+ | `notify` | `push/upsert/update/dismiss/clear/clearHistory`, short methods и progress pattern. |
624
624
 
625
625
  `notify` требует bound store; без provider будет ошибка.
626
626
 
package/dist/dom/index.js CHANGED
@@ -89,7 +89,7 @@ function p({ active: n, trapFocus: i = !1, restoreFocus: a = !0, initialFocus: o
89
89
  return;
90
90
  }
91
91
  let n = m(e);
92
- n instanceof HTMLElement && n.isConnected && n.focus(), i && e.addEventListener("keydown", o);
92
+ n instanceof HTMLElement && n.isConnected && n.focus({ preventScroll: !0 }), i && e.addEventListener("keydown", o);
93
93
  }, o = (t) => {
94
94
  if (!i || t.key !== "Tab" || !e) return;
95
95
  let n = p(e);
@@ -107,7 +107,7 @@ function p({ active: n, trapFocus: i = !1, restoreFocus: a = !0, initialFocus: o
107
107
  return r(), () => {
108
108
  if (window.cancelAnimationFrame(t), i && e && e.removeEventListener("keydown", o), !a) return;
109
109
  let n = h();
110
- n?.isConnected && n.focus();
110
+ n?.isConnected && n.focus({ preventScroll: !0 });
111
111
  };
112
112
  }, [
113
113
  n,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/dom/downloadFile.ts","../../src/dom/getOrCreatePortalRoot.ts","../../src/dom/useClickOutside.ts","../../src/dom/useElementHeightObserver.ts","../../src/dom/useEscapeDismiss.ts","../../src/dom/useOverlayFocus.ts","../../src/dom/useFocusTrap.ts","../../src/dom/useIntersectionObserver.ts","../../src/dom/useIsTouchDevice.ts"],"sourcesContent":["export function downloadFileFromObjectURL(filename: string, objectUrl: string) {\n\tconst link = document.createElement(\"a\");\n\n\tlink.href = objectUrl;\n\tlink.download = filename;\n\tlink.click();\n\n\twindow.setTimeout(() => {\n\t\twindow.URL.revokeObjectURL(objectUrl);\n\t}, 0);\n}\n\nexport function downloadFileFromBlob(filename: string, blob: Blob) {\n\tconst objectUrl = window.URL.createObjectURL(blob);\n\tdownloadFileFromObjectURL(filename, objectUrl);\n}\n\nexport function downloadFileFromJson(filename: string, payload: unknown) {\n\tconst blob = new Blob([JSON.stringify(payload, null, 2)], { type: \"application/json;charset=utf-8\" });\n\tdownloadFileFromBlob(filename, blob);\n}\n","export function getOrCreatePortalRoot(id: string): HTMLElement | null {\n\tif (typeof document === \"undefined\") {\n\t\treturn null;\n\t}\n\n\tconst existingElement = document.getElementById(id);\n\tif (existingElement instanceof HTMLElement) {\n\t\treturn existingElement;\n\t}\n\n\tconst portalRoot = document.createElement(\"div\");\n\tportalRoot.id = id;\n\tdocument.body.append(portalRoot);\n\n\treturn portalRoot;\n}\n","import { RefObject, useEffect } from \"react\";\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[], handler: () => void) {\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (Array.isArray(ref)) {\n\t\t\t\tlet isOutside = true;\n\t\t\t\tfor (const r of ref) {\n\t\t\t\t\tif (r.current && r.current.contains(event.target as Node)) {\n\t\t\t\t\t\tisOutside = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isOutside) {\n\t\t\t\t\thandler();\n\t\t\t\t}\n\t\t\t} else if (ref.current && !ref.current.contains(event.target as Node)) {\n\t\t\t\thandler();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\treturn () => document.removeEventListener(\"mousedown\", handleClickOutside);\n\t}, [ref, handler]);\n}\n","import { type RefObject, useLayoutEffect, useState } from \"react\";\n\nexport function useElementHeightObserver<TElement extends HTMLElement>(elementRef: RefObject<TElement | null>) {\n\tconst [height, setHeight] = useState(0);\n\n\tuseLayoutEffect(() => {\n\t\tconst element = elementRef.current;\n\t\tif (!element) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst nextHeight = entries[0]?.contentRect.height ?? element.getBoundingClientRect().height;\n\t\t\tsetHeight(nextHeight);\n\t\t});\n\t\tobserver.observe(element);\n\n\t\treturn () => observer.disconnect();\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/refs\n\t}, [elementRef.current]);\n\n\treturn height;\n}\n","import { RefObject, useEffect, useEffectEvent } from \"react\";\n\ninterface UseEscapeDismissOptions {\n\tactive: boolean;\n\tonDismiss: () => void;\n\tenabled?: boolean;\n\tdocumentTarget?: Document | null;\n\tcontainerRef?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Подписывает overlay на закрытие по клавише Escape.\n */\nexport function useEscapeDismiss({ active, onDismiss, enabled = true, documentTarget, containerRef }: UseEscapeDismissOptions) {\n\tconst dismiss = useEffectEvent(() => onDismiss());\n\n\tuseEffect(() => {\n\t\tif (!active || !enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst targetDocument = documentTarget ?? (typeof document === \"undefined\" ? null : document);\n\t\tif (!targetDocument) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key !== \"Escape\" /*|| event.defaultPrevented*/) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst activeElement = targetDocument.activeElement;\n\t\t\tconst container = containerRef?.current;\n\t\t\tif (container && activeElement instanceof Node && activeElement !== container && !container.contains(activeElement)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tevent.preventDefault();\n\t\t\tdismiss();\n\t\t};\n\n\t\ttargetDocument.addEventListener(\"keydown\", handleKeyDown, true);\n\t\treturn () => targetDocument.removeEventListener(\"keydown\", handleKeyDown, true);\n\t}, [active, enabled, documentTarget, containerRef]);\n}\n","import { RefObject, useEffect, useEffectEvent, useRef } from \"react\";\n\nconst focusableElementsSelector =\n\t'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])';\n\nexport interface UseOverlayFocusOptions<T extends HTMLElement> {\n\tactive: boolean;\n\ttrapFocus?: boolean;\n\trestoreFocus?: boolean;\n\tinitialFocus?: \"auto\" | \"container\" | ((container: T) => HTMLElement | null | undefined);\n\trestoreFocusTarget?: () => HTMLElement | null | undefined;\n\tcontainerRef?: RefObject<T | null>;\n}\n\n/**\n * Возвращает ref overlay-контейнера и управляет начальными/возвратными переходами фокуса.\n */\nexport function useOverlayFocus<T extends HTMLElement>({\n\tactive,\n\ttrapFocus = false,\n\trestoreFocus = true,\n\tinitialFocus = \"auto\",\n\trestoreFocusTarget,\n\tcontainerRef\n}: UseOverlayFocusOptions<T>) {\n\tconst internalOverlayRef = useRef<T | null>(null);\n\tconst overlayRef = containerRef ?? internalOverlayRef;\n\tconst previousActiveElementRef = useRef<HTMLElement | null>(null);\n\n\t/**\n\t * Собирает фокусируемые элементы overlay в актуальном DOM-порядке.\n\t */\n\tconst getFocusableElements = useEffectEvent((container: T) =>\n\t\tArray.from(container.querySelectorAll<HTMLElement>(focusableElementsSelector)).filter(\n\t\t\t(element) => !element.hasAttribute(\"aria-hidden\")\n\t\t)\n\t);\n\n\t/**\n\t * Вычисляет стартовый фокус при открытии overlay.\n\t */\n\tconst resolveInitialFocus = useEffectEvent((container: T) => {\n\t\tif (initialFocus === \"container\") {\n\t\t\treturn container;\n\t\t}\n\n\t\tif (typeof initialFocus === \"function\") {\n\t\t\treturn initialFocus(container) ?? container;\n\t\t}\n\n\t\tconst focusableElements = getFocusableElements(container);\n\t\treturn (\n\t\t\tfocusableElements.find((element) => element.hasAttribute(\"autofocus\") || element.hasAttribute(\"autoFocus\")) ??\n\t\t\tfocusableElements[0] ??\n\t\t\tcontainer\n\t\t);\n\t});\n\n\t/**\n\t * Вычисляет элемент, которому нужно вернуть фокус после закрытия overlay.\n\t */\n\tconst resolveRestoreFocusTarget = useEffectEvent(() => restoreFocusTarget?.() ?? previousActiveElementRef.current);\n\n\tuseEffect(() => {\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreviousActiveElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n\t\tlet overlayElement: T | null = null;\n\t\tlet rafId = 0;\n\n\t\t/**\n\t\t * Цикл откладывает инициализацию, пока ref контейнера не будет привязан к DOM.\n\t\t */\n\t\tconst attachOverlay = () => {\n\t\t\toverlayElement = overlayRef.current;\n\t\t\tif (!overlayElement) {\n\t\t\t\trafId = window.requestAnimationFrame(attachOverlay);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusTarget = resolveInitialFocus(overlayElement);\n\t\t\tif (focusTarget instanceof HTMLElement && focusTarget.isConnected) {\n\t\t\t\tfocusTarget.focus();\n\t\t\t}\n\n\t\t\tif (trapFocus) {\n\t\t\t\toverlayElement.addEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Зацикливает Tab внутри overlay, пока он открыт как модальный слой.\n\t\t */\n\t\tconst handleTabKey = (event: KeyboardEvent) => {\n\t\t\tif (!trapFocus || event.key !== \"Tab\" || !overlayElement) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusableElements = getFocusableElements(overlayElement);\n\t\t\tif (focusableElements.length === 0) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\toverlayElement.focus();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst firstElement = focusableElements[0];\n\t\t\tconst lastElement = focusableElements[focusableElements.length - 1];\n\t\t\tconst activeElement = document.activeElement;\n\n\t\t\tif (event.shiftKey) {\n\t\t\t\tif (activeElement === firstElement || activeElement === overlayElement) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlastElement.focus();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (activeElement === lastElement) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfirstElement.focus();\n\t\t\t}\n\t\t};\n\n\t\tattachOverlay();\n\n\t\treturn () => {\n\t\t\twindow.cancelAnimationFrame(rafId);\n\t\t\tif (trapFocus && overlayElement) {\n\t\t\t\toverlayElement.removeEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\n\t\t\tif (!restoreFocus) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst restoreTarget = resolveRestoreFocusTarget();\n\t\t\tif (restoreTarget?.isConnected) {\n\t\t\t\trestoreTarget.focus();\n\t\t\t}\n\t\t};\n\t}, [active, trapFocus, restoreFocus, overlayRef]);\n\n\treturn overlayRef;\n}\n","import { useOverlayFocus } from \"./useOverlayFocus\";\n\n/**\n * Совместимый wrapper для overlay с фокус-ловушкой.\n */\nexport const useFocusTrap = (isOpen: boolean) => {\n\treturn useOverlayFocus<HTMLDivElement>({ active: isOpen, trapFocus: true, restoreFocus: true, initialFocus: \"auto\" });\n};\n","import { useEffect, useState } from \"react\";\n\nexport function useIntersectionObserver(ref: React.RefObject<Element | null>, options?: IntersectionObserverInit) {\n\tconst [isIntersecting, setIsIntersecting] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tconst observer = new IntersectionObserver(([entry]) => {\n\t\t\tsetIsIntersecting(entry.isIntersecting);\n\t\t}, options);\n\n\t\tobserver.observe(el);\n\n\t\treturn () => {\n\t\t\tif (el) {\n\t\t\t\tobserver.unobserve(el);\n\t\t\t}\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [options, ref]); // не забыть, что ref.current может меняться\n\n\treturn { isIntersecting };\n}\n","import { useEffect, useState } from \"react\";\n\nexport const useIsTouchDevice = () => {\n\tconst [isTouchDevice, setIsTouchDevice] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst checkTouchDevice = () => {\n\t\t\tconst isTouch = window.matchMedia(\"(pointer: coarse)\").matches || \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\n\t\t\tsetIsTouchDevice(isTouch);\n\t\t};\n\n\t\tcheckTouchDevice();\n\n\t\t// Обработчик для изменений в типе устройства\n\t\twindow.addEventListener(\"resize\", checkTouchDevice);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"resize\", checkTouchDevice);\n\t\t};\n\t}, []);\n\n\treturn isTouchDevice;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/dom/downloadFile.ts","../../src/dom/getOrCreatePortalRoot.ts","../../src/dom/useClickOutside.ts","../../src/dom/useElementHeightObserver.ts","../../src/dom/useEscapeDismiss.ts","../../src/dom/useOverlayFocus.ts","../../src/dom/useFocusTrap.ts","../../src/dom/useIntersectionObserver.ts","../../src/dom/useIsTouchDevice.ts"],"sourcesContent":["export function downloadFileFromObjectURL(filename: string, objectUrl: string) {\n\tconst link = document.createElement(\"a\");\n\n\tlink.href = objectUrl;\n\tlink.download = filename;\n\tlink.click();\n\n\twindow.setTimeout(() => {\n\t\twindow.URL.revokeObjectURL(objectUrl);\n\t}, 0);\n}\n\nexport function downloadFileFromBlob(filename: string, blob: Blob) {\n\tconst objectUrl = window.URL.createObjectURL(blob);\n\tdownloadFileFromObjectURL(filename, objectUrl);\n}\n\nexport function downloadFileFromJson(filename: string, payload: unknown) {\n\tconst blob = new Blob([JSON.stringify(payload, null, 2)], { type: \"application/json;charset=utf-8\" });\n\tdownloadFileFromBlob(filename, blob);\n}\n","export function getOrCreatePortalRoot(id: string): HTMLElement | null {\n\tif (typeof document === \"undefined\") {\n\t\treturn null;\n\t}\n\n\tconst existingElement = document.getElementById(id);\n\tif (existingElement instanceof HTMLElement) {\n\t\treturn existingElement;\n\t}\n\n\tconst portalRoot = document.createElement(\"div\");\n\tportalRoot.id = id;\n\tdocument.body.append(portalRoot);\n\n\treturn portalRoot;\n}\n","import { RefObject, useEffect } from \"react\";\n\nexport function useClickOutside(ref: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[], handler: () => void) {\n\tuseEffect(() => {\n\t\tconst handleClickOutside = (event: MouseEvent) => {\n\t\t\tif (Array.isArray(ref)) {\n\t\t\t\tlet isOutside = true;\n\t\t\t\tfor (const r of ref) {\n\t\t\t\t\tif (r.current && r.current.contains(event.target as Node)) {\n\t\t\t\t\t\tisOutside = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (isOutside) {\n\t\t\t\t\thandler();\n\t\t\t\t}\n\t\t\t} else if (ref.current && !ref.current.contains(event.target as Node)) {\n\t\t\t\thandler();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener(\"mousedown\", handleClickOutside);\n\t\treturn () => document.removeEventListener(\"mousedown\", handleClickOutside);\n\t}, [ref, handler]);\n}\n","import { type RefObject, useLayoutEffect, useState } from \"react\";\n\nexport function useElementHeightObserver<TElement extends HTMLElement>(elementRef: RefObject<TElement | null>) {\n\tconst [height, setHeight] = useState(0);\n\n\tuseLayoutEffect(() => {\n\t\tconst element = elementRef.current;\n\t\tif (!element) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst nextHeight = entries[0]?.contentRect.height ?? element.getBoundingClientRect().height;\n\t\t\tsetHeight(nextHeight);\n\t\t});\n\t\tobserver.observe(element);\n\n\t\treturn () => observer.disconnect();\n\t\t// eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/refs\n\t}, [elementRef.current]);\n\n\treturn height;\n}\n","import { RefObject, useEffect, useEffectEvent } from \"react\";\n\ninterface UseEscapeDismissOptions {\n\tactive: boolean;\n\tonDismiss: () => void;\n\tenabled?: boolean;\n\tdocumentTarget?: Document | null;\n\tcontainerRef?: RefObject<HTMLElement | null>;\n}\n\n/**\n * Подписывает overlay на закрытие по клавише Escape.\n */\nexport function useEscapeDismiss({ active, onDismiss, enabled = true, documentTarget, containerRef }: UseEscapeDismissOptions) {\n\tconst dismiss = useEffectEvent(() => onDismiss());\n\n\tuseEffect(() => {\n\t\tif (!active || !enabled) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst targetDocument = documentTarget ?? (typeof document === \"undefined\" ? null : document);\n\t\tif (!targetDocument) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key !== \"Escape\" /*|| event.defaultPrevented*/) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst activeElement = targetDocument.activeElement;\n\t\t\tconst container = containerRef?.current;\n\t\t\tif (container && activeElement instanceof Node && activeElement !== container && !container.contains(activeElement)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tevent.preventDefault();\n\t\t\tdismiss();\n\t\t};\n\n\t\ttargetDocument.addEventListener(\"keydown\", handleKeyDown, true);\n\t\treturn () => targetDocument.removeEventListener(\"keydown\", handleKeyDown, true);\n\t}, [active, enabled, documentTarget, containerRef]);\n}\n","import { RefObject, useEffect, useEffectEvent, useRef } from \"react\";\n\nconst focusableElementsSelector =\n\t'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]):not([disabled])';\n\nexport interface UseOverlayFocusOptions<T extends HTMLElement> {\n\tactive: boolean;\n\ttrapFocus?: boolean;\n\trestoreFocus?: boolean;\n\tinitialFocus?: \"auto\" | \"container\" | ((container: T) => HTMLElement | null | undefined);\n\trestoreFocusTarget?: () => HTMLElement | null | undefined;\n\tcontainerRef?: RefObject<T | null>;\n}\n\n/**\n * Возвращает ref overlay-контейнера и управляет начальными/возвратными переходами фокуса.\n * Открытие и закрытие не прокручивают документ: портал может получить фокус\n * до завершения позиционирования. Пользовательский Tab по-прежнему раскрывает\n * выбранный элемент в прокручиваемом содержимом overlay.\n */\nexport function useOverlayFocus<T extends HTMLElement>({\n\tactive,\n\ttrapFocus = false,\n\trestoreFocus = true,\n\tinitialFocus = \"auto\",\n\trestoreFocusTarget,\n\tcontainerRef\n}: UseOverlayFocusOptions<T>) {\n\tconst internalOverlayRef = useRef<T | null>(null);\n\tconst overlayRef = containerRef ?? internalOverlayRef;\n\tconst previousActiveElementRef = useRef<HTMLElement | null>(null);\n\n\t/**\n\t * Собирает фокусируемые элементы overlay в актуальном DOM-порядке.\n\t */\n\tconst getFocusableElements = useEffectEvent((container: T) =>\n\t\tArray.from(container.querySelectorAll<HTMLElement>(focusableElementsSelector)).filter(\n\t\t\t(element) => !element.hasAttribute(\"aria-hidden\")\n\t\t)\n\t);\n\n\t/**\n\t * Вычисляет стартовый фокус при открытии overlay.\n\t */\n\tconst resolveInitialFocus = useEffectEvent((container: T) => {\n\t\tif (initialFocus === \"container\") {\n\t\t\treturn container;\n\t\t}\n\n\t\tif (typeof initialFocus === \"function\") {\n\t\t\treturn initialFocus(container) ?? container;\n\t\t}\n\n\t\tconst focusableElements = getFocusableElements(container);\n\t\treturn (\n\t\t\tfocusableElements.find((element) => element.hasAttribute(\"autofocus\") || element.hasAttribute(\"autoFocus\")) ??\n\t\t\tfocusableElements[0] ??\n\t\t\tcontainer\n\t\t);\n\t});\n\n\t/**\n\t * Вычисляет элемент, которому нужно вернуть фокус после закрытия overlay.\n\t */\n\tconst resolveRestoreFocusTarget = useEffectEvent(() => restoreFocusTarget?.() ?? previousActiveElementRef.current);\n\n\tuseEffect(() => {\n\t\tif (!active) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreviousActiveElementRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;\n\t\tlet overlayElement: T | null = null;\n\t\tlet rafId = 0;\n\n\t\t/**\n\t\t * Цикл откладывает инициализацию, пока ref контейнера не будет привязан к DOM.\n\t\t */\n\t\tconst attachOverlay = () => {\n\t\t\toverlayElement = overlayRef.current;\n\t\t\tif (!overlayElement) {\n\t\t\t\trafId = window.requestAnimationFrame(attachOverlay);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusTarget = resolveInitialFocus(overlayElement);\n\t\t\tif (focusTarget instanceof HTMLElement && focusTarget.isConnected) {\n\t\t\t\tfocusTarget.focus({ preventScroll: true });\n\t\t\t}\n\n\t\t\tif (trapFocus) {\n\t\t\t\toverlayElement.addEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Зацикливает Tab внутри overlay, пока он открыт как модальный слой.\n\t\t */\n\t\tconst handleTabKey = (event: KeyboardEvent) => {\n\t\t\tif (!trapFocus || event.key !== \"Tab\" || !overlayElement) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst focusableElements = getFocusableElements(overlayElement);\n\t\t\tif (focusableElements.length === 0) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\toverlayElement.focus();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst firstElement = focusableElements[0];\n\t\t\tconst lastElement = focusableElements[focusableElements.length - 1];\n\t\t\tconst activeElement = document.activeElement;\n\n\t\t\tif (event.shiftKey) {\n\t\t\t\tif (activeElement === firstElement || activeElement === overlayElement) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tlastElement.focus();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (activeElement === lastElement) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tfirstElement.focus();\n\t\t\t}\n\t\t};\n\n\t\tattachOverlay();\n\n\t\treturn () => {\n\t\t\twindow.cancelAnimationFrame(rafId);\n\t\t\tif (trapFocus && overlayElement) {\n\t\t\t\toverlayElement.removeEventListener(\"keydown\", handleTabKey);\n\t\t\t}\n\n\t\t\tif (!restoreFocus) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst restoreTarget = resolveRestoreFocusTarget();\n\t\t\tif (restoreTarget?.isConnected) {\n\t\t\t\trestoreTarget.focus({ preventScroll: true });\n\t\t\t}\n\t\t};\n\t}, [active, trapFocus, restoreFocus, overlayRef]);\n\n\treturn overlayRef;\n}\n","import { useOverlayFocus } from \"./useOverlayFocus\";\n\n/**\n * Совместимый wrapper для overlay с фокус-ловушкой.\n */\nexport const useFocusTrap = (isOpen: boolean) => {\n\treturn useOverlayFocus<HTMLDivElement>({ active: isOpen, trapFocus: true, restoreFocus: true, initialFocus: \"auto\" });\n};\n","import { useEffect, useState } from \"react\";\n\nexport function useIntersectionObserver(ref: React.RefObject<Element | null>, options?: IntersectionObserverInit) {\n\tconst [isIntersecting, setIsIntersecting] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst el = ref.current;\n\t\tif (!el) return;\n\n\t\tconst observer = new IntersectionObserver(([entry]) => {\n\t\t\tsetIsIntersecting(entry.isIntersecting);\n\t\t}, options);\n\n\t\tobserver.observe(el);\n\n\t\treturn () => {\n\t\t\tif (el) {\n\t\t\t\tobserver.unobserve(el);\n\t\t\t}\n\t\t\tobserver.disconnect();\n\t\t};\n\t}, [options, ref]); // не забыть, что ref.current может меняться\n\n\treturn { isIntersecting };\n}\n","import { useEffect, useState } from \"react\";\n\nexport const useIsTouchDevice = () => {\n\tconst [isTouchDevice, setIsTouchDevice] = useState(false);\n\n\tuseEffect(() => {\n\t\tconst checkTouchDevice = () => {\n\t\t\tconst isTouch = window.matchMedia(\"(pointer: coarse)\").matches || \"ontouchstart\" in window || navigator.maxTouchPoints > 0;\n\t\t\tsetIsTouchDevice(isTouch);\n\t\t};\n\n\t\tcheckTouchDevice();\n\n\t\t// Обработчик для изменений в типе устройства\n\t\twindow.addEventListener(\"resize\", checkTouchDevice);\n\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"resize\", checkTouchDevice);\n\t\t};\n\t}, []);\n\n\treturn isTouchDevice;\n};\n"],"mappings":"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"}
@@ -9,6 +9,9 @@ export interface UseOverlayFocusOptions<T extends HTMLElement> {
9
9
  }
10
10
  /**
11
11
  * Возвращает ref overlay-контейнера и управляет начальными/возвратными переходами фокуса.
12
+ * Открытие и закрытие не прокручивают документ: портал может получить фокус
13
+ * до завершения позиционирования. Пользовательский Tab по-прежнему раскрывает
14
+ * выбранный элемент в прокручиваемом содержимом overlay.
12
15
  */
13
16
  export declare function useOverlayFocus<T extends HTMLElement>({ active, trapFocus, restoreFocus, initialFocus, restoreFocusTarget, containerRef }: UseOverlayFocusOptions<T>): RefObject<T | null>;
14
17
  //# sourceMappingURL=useOverlayFocus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useOverlayFocus.d.ts","sourceRoot":"","sources":["../../src/dom/useOverlayFocus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAqC,MAAM,OAAO,CAAC;AAKrE,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,WAAW;IAC5D,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,KAAK,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzF,kBAAkB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1D,YAAY,CAAC,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,EACtD,MAAM,EACN,SAAiB,EACjB,YAAmB,EACnB,YAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,EAAE,sBAAsB,CAAC,CAAC,CAAC,uBAyH3B"}
1
+ {"version":3,"file":"useOverlayFocus.d.ts","sourceRoot":"","sources":["../../src/dom/useOverlayFocus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAqC,MAAM,OAAO,CAAC;AAKrE,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,WAAW;IAC5D,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,KAAK,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzF,kBAAkB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1D,YAAY,CAAC,EAAE,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,EACtD,MAAM,EACN,SAAiB,EACjB,YAAmB,EACnB,YAAqB,EACrB,kBAAkB,EAClB,YAAY,EACZ,EAAE,sBAAsB,CAAC,CAAC,CAAC,uBAyH3B"}
@@ -11,6 +11,7 @@ var t = null, n = (e) => (t = e, () => {
11
11
  update: (e, t) => r().getState().actions.update(e, t),
12
12
  dismiss: (e) => r().getState().actions.dismiss(e),
13
13
  clear: () => r().getState().actions.clear(),
14
+ clearHistory: () => r().getState().actions.clearHistory(),
14
15
  success: (e, t) => r().getState().actions.push({
15
16
  type: "success",
16
17
  message: e,
@@ -64,7 +65,8 @@ var t = null, n = (e) => (t = e, () => {
64
65
  dismiss: () => i.dismiss(o)
65
66
  };
66
67
  }
67
- }), a = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`, o = 6, s = () => {
68
+ }), a = () => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`, o = 6;
69
+ function s() {
68
70
  let t = /* @__PURE__ */ new Map(), n = (e) => {
69
71
  let n = t.get(e);
70
72
  n && (window.clearTimeout(n), t.delete(e));
@@ -76,30 +78,39 @@ var t = null, n = (e) => (t = e, () => {
76
78
  t.set(r, a);
77
79
  }, i = e()((e, s) => ({
78
80
  items: [],
81
+ history: [],
79
82
  actions: {
80
83
  push: (t) => {
81
- let n = t.id ?? a(), s = Date.now(), c = {
82
- id: n,
84
+ let c = t.id ?? a(), l = Date.now(), u = {
85
+ id: c,
83
86
  type: t.type,
84
87
  title: t.title,
85
88
  message: t.message,
86
- createdAt: s,
89
+ createdAt: l,
87
90
  ttlMs: t.ttlMs,
88
91
  dismissible: t.dismissible ?? !0,
89
92
  actions: t.actions
90
- };
91
- return e((e) => ({ items: [c, ...e.items.filter((e) => e.id !== n)].slice(0, o) })), r(i, n, c.ttlMs), n;
93
+ }, d = s().items, f = [u, ...d.filter((e) => e.id !== c)].slice(0, o), p = new Set(f.map((e) => e.id));
94
+ return d.forEach((e) => {
95
+ p.has(e.id) || n(e.id);
96
+ }), e((e) => ({
97
+ items: f,
98
+ history: [u, ...e.history.filter((e) => e.id !== c)]
99
+ })), r(i, c, u.ttlMs), c;
92
100
  },
93
101
  update: (t, n) => {
94
- let { items: a } = s(), o = a.findIndex((e) => e.id === t);
95
- if (o === -1) return !1;
96
- let c = a[o], l = {
97
- ...c,
102
+ let a = s().items.find((e) => e.id === t);
103
+ if (!a) return !1;
104
+ let o = {
105
+ ...a,
98
106
  ...n,
99
- id: c.id,
100
- createdAt: c.createdAt
107
+ id: a.id,
108
+ createdAt: a.createdAt
101
109
  };
102
- return e((e) => ({ items: e.items.map((e) => e.id === t ? l : e) })), "ttlMs" in n && r(i, t, l.ttlMs), !0;
110
+ return e((e) => ({
111
+ items: e.items.map((e) => e.id === t ? o : e),
112
+ history: e.history.map((e) => e.id === t ? o : e)
113
+ })), "ttlMs" in n && r(i, t, o.ttlMs), !0;
103
114
  },
104
115
  upsert: (e) => s().actions.update(e.id, {
105
116
  type: e.type,
@@ -113,11 +124,14 @@ var t = null, n = (e) => (t = e, () => {
113
124
  },
114
125
  clear: () => {
115
126
  t.forEach((e) => window.clearTimeout(e)), t.clear(), e({ items: [] });
127
+ },
128
+ clearHistory: () => {
129
+ e({ history: [] });
116
130
  }
117
131
  }
118
132
  }));
119
133
  return i;
120
- };
134
+ }
121
135
  //#endregion
122
136
  export { n as bindNotifications, s as createNotificationsStore, i as notify };
123
137
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/notifications/notify.ts","../../src/notifications/store.ts"],"sourcesContent":["import type { NotificationPushInput, NotificationsStoreApi, NotificationUpdatePatch } from \"./store\";\nimport type { NotificationId } from \"./types\";\n\nlet boundStore: NotificationsStoreApi | null = null;\n\nexport const bindNotifications = (store: NotificationsStoreApi) => {\n\tboundStore = store;\n\n\treturn () => {\n\t\tif (boundStore === store) {\n\t\t\tboundStore = null;\n\t\t}\n\t};\n};\n\nconst requireStore = (): NotificationsStoreApi => {\n\tif (!boundStore) throw new Error(\"Notifications store is not bound. Wrap app with NotificationsProvider.\");\n\treturn boundStore;\n};\n\nexport const notify = Object.freeze({\n\tpush: (input: NotificationPushInput) => requireStore().getState().actions.push(input),\n\tupsert: (input: NotificationPushInput & { id: NotificationId }) => requireStore().getState().actions.upsert(input),\n\tupdate: (id: NotificationId, patch: NotificationUpdatePatch) => requireStore().getState().actions.update(id, patch),\n\n\tdismiss: (id: NotificationId) => requireStore().getState().actions.dismiss(id),\n\tclear: () => requireStore().getState().actions.clear(),\n\n\tsuccess: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"success\", message, ...opts }),\n\tinfo: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"info\", message, ...opts }),\n\twarning: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"warning\", message, ...opts }),\n\terror: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"error\", message, ...opts }),\n\n\t/**\n\t * Удобный паттерн \"долгой операции\":\n\t * const op = notify.progress(\"Сохраняю...\");\n\t * ... op.success(\"Готово\") или op.error(\"Ошибка\")\n\t */\n\tprogress: (message: string, opts?: { id?: NotificationId; title?: string }) => {\n\t\tconst store = requireStore();\n\t\tconst id = opts?.id;\n\n\t\tif (id) {\n\t\t\tstore.getState().actions.upsert({\n\t\t\t\tid,\n\t\t\t\ttype: \"info\",\n\t\t\t\ttitle: opts?.title,\n\t\t\t\tmessage,\n\t\t\t\tdismissible: false\n\t\t\t});\n\t\t}\n\n\t\tconst notificationId =\n\t\t\tid ??\n\t\t\tstore.getState().actions.push({\n\t\t\t\ttype: \"info\",\n\t\t\t\ttitle: opts?.title,\n\t\t\t\tmessage,\n\t\t\t\tdismissible: false\n\t\t\t});\n\n\t\treturn {\n\t\t\tid: notificationId,\n\t\t\tupdate: (nextMessage: string) => notify.update(notificationId, { message: nextMessage }),\n\t\t\tsuccess: (nextMessage: string, ttlMs = 2500) =>\n\t\t\t\tnotify.update(notificationId, { type: \"success\", message: nextMessage, dismissible: true, ttlMs }),\n\t\t\terror: (nextMessage: string, ttlMs = 5000) =>\n\t\t\t\tnotify.update(notificationId, { type: \"error\", message: nextMessage, dismissible: true, ttlMs }),\n\t\t\tdismiss: () => notify.dismiss(notificationId)\n\t\t};\n\t}\n});\n","import { createStore, StoreApi } from \"zustand\";\n\nimport { Notification, NotificationAction, NotificationId, NotificationType } from \"./types\";\n\nexport type NotificationPushInput = {\n\tid?: NotificationId;\n\ttype: NotificationType;\n\ttitle?: string;\n\tmessage: string;\n\tttlMs?: number;\n\tdismissible?: boolean;\n\tactions?: NotificationAction[];\n};\n\nexport type NotificationUpdatePatch = Partial<Omit<Notification, \"id\" | \"createdAt\">> & {\n\t// ttlMs можно менять; createdAt — нет\n};\n\ntype NotificationsState = {\n\titems: Notification[];\n};\n\ntype NotificationsActions = {\n\tpush: (input: NotificationPushInput) => NotificationId;\n\tupdate: (id: NotificationId, patch: NotificationUpdatePatch) => boolean;\n\tupsert: (input: NotificationPushInput & { id: NotificationId }) => NotificationId;\n\tdismiss: (id: NotificationId) => void;\n\tclear: () => void;\n};\n\nexport type NotificationsStore = NotificationsState & {\n\tactions: NotificationsActions;\n};\n\nconst genId = (): NotificationId => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;\n\nconst MAX_ITEMS = 6;\n\nexport const createNotificationsStore = () => {\n\tconst timers = new Map<NotificationId, number>();\n\n\tconst clearTimer = (id: NotificationId) => {\n\t\tconst t = timers.get(id);\n\t\tif (t) {\n\t\t\twindow.clearTimeout(t);\n\t\t\ttimers.delete(id);\n\t\t}\n\t};\n\n\tconst armTimer = (store: StoreApi<NotificationsStore>, id: NotificationId, ttlMs: number = 10_000) => {\n\t\tclearTimer(id);\n\t\tif (ttlMs <= 0) return;\n\n\t\tconst t = window.setTimeout(() => {\n\t\t\tstore.getState().actions.dismiss(id);\n\t\t}, ttlMs);\n\n\t\ttimers.set(id, t);\n\t};\n\n\tconst store = createStore<NotificationsStore>()((set, get) => ({\n\t\titems: [],\n\n\t\tactions: {\n\t\t\tpush: (input) => {\n\t\t\t\tconst id = input.id ?? genId();\n\t\t\t\tconst now = Date.now();\n\n\t\t\t\tconst notif: Notification = {\n\t\t\t\t\tid,\n\t\t\t\t\ttype: input.type,\n\t\t\t\t\ttitle: input.title,\n\t\t\t\t\tmessage: input.message,\n\t\t\t\t\tcreatedAt: now,\n\t\t\t\t\tttlMs: input.ttlMs,\n\t\t\t\t\tdismissible: input.dismissible ?? true,\n\t\t\t\t\tactions: input.actions\n\t\t\t\t};\n\n\t\t\t\tset((s) => ({\n\t\t\t\t\titems: [notif, ...s.items.filter((n) => n.id !== id)].slice(0, MAX_ITEMS)\n\t\t\t\t}));\n\n\t\t\t\tarmTimer(store, id, notif.ttlMs);\n\t\t\t\treturn id;\n\t\t\t},\n\n\t\t\tupdate: (id, patch) => {\n\t\t\t\tconst { items } = get();\n\t\t\t\tconst idx = items.findIndex((n) => n.id === id);\n\t\t\t\tif (idx === -1) return false;\n\n\t\t\t\tconst prev = items[idx];\n\t\t\t\tconst next: Notification = {\n\t\t\t\t\t...prev,\n\t\t\t\t\t...patch,\n\t\t\t\t\tid: prev.id,\n\t\t\t\t\tcreatedAt: prev.createdAt\n\t\t\t\t};\n\n\t\t\t\tset((s) => ({\n\t\t\t\t\titems: s.items.map((n) => (n.id === id ? next : n))\n\t\t\t\t}));\n\n\t\t\t\t// если ttlMs изменили — пере-армим таймер\n\t\t\t\tif (\"ttlMs\" in patch) {\n\t\t\t\t\tarmTimer(store, id, next.ttlMs);\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\tupsert: (input) => {\n\t\t\t\tconst ok = get().actions.update(input.id, {\n\t\t\t\t\ttype: input.type,\n\t\t\t\t\ttitle: input.title,\n\t\t\t\t\tmessage: input.message,\n\t\t\t\t\tttlMs: input.ttlMs,\n\t\t\t\t\tdismissible: input.dismissible\n\t\t\t\t});\n\n\t\t\t\tif (ok) return input.id;\n\t\t\t\treturn get().actions.push(input);\n\t\t\t},\n\n\t\t\tdismiss: (id) => {\n\t\t\t\tclearTimer(id);\n\t\t\t\tset((s) => ({ items: s.items.filter((n) => n.id !== id) }));\n\t\t\t},\n\n\t\t\tclear: () => {\n\t\t\t\ttimers.forEach((t) => window.clearTimeout(t));\n\t\t\t\ttimers.clear();\n\t\t\t\tset({ items: [] });\n\t\t\t}\n\t\t}\n\t}));\n\n\treturn store;\n};\n\nexport type NotificationsStoreApi = StoreApi<NotificationsStore>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/notifications/notify.ts","../../src/notifications/store.ts"],"sourcesContent":["import { type NotificationPushInput, type NotificationsStoreApi, type NotificationUpdatePatch } from \"./store\";\nimport { type NotificationId } from \"./types\";\n\nlet boundStore: NotificationsStoreApi | null = null;\n\nexport const bindNotifications = (store: NotificationsStoreApi) => {\n\tboundStore = store;\n\n\treturn () => {\n\t\tif (boundStore === store) {\n\t\t\tboundStore = null;\n\t\t}\n\t};\n};\n\nconst requireStore = (): NotificationsStoreApi => {\n\tif (!boundStore) throw new Error(\"Notifications store is not bound. Wrap app with NotificationsProvider.\");\n\treturn boundStore;\n};\n\nexport const notify = Object.freeze({\n\tpush: (input: NotificationPushInput) => requireStore().getState().actions.push(input),\n\tupsert: (input: NotificationPushInput & { id: NotificationId }) => requireStore().getState().actions.upsert(input),\n\tupdate: (id: NotificationId, patch: NotificationUpdatePatch) => requireStore().getState().actions.update(id, patch),\n\n\tdismiss: (id: NotificationId) => requireStore().getState().actions.dismiss(id),\n\tclear: () => requireStore().getState().actions.clear(),\n\tclearHistory: () => requireStore().getState().actions.clearHistory(),\n\n\tsuccess: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"success\", message, ...opts }),\n\tinfo: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"info\", message, ...opts }),\n\twarning: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"warning\", message, ...opts }),\n\terror: (message: string, opts?: Omit<NotificationPushInput, \"type\" | \"message\">) =>\n\t\trequireStore()\n\t\t\t.getState()\n\t\t\t.actions.push({ type: \"error\", message, ...opts }),\n\n\t/**\n\t * Удобный паттерн \"долгой операции\":\n\t * const op = notify.progress(\"Сохраняю...\");\n\t * ... op.success(\"Готово\") или op.error(\"Ошибка\")\n\t */\n\tprogress: (message: string, opts?: { id?: NotificationId; title?: string }) => {\n\t\tconst store = requireStore();\n\t\tconst id = opts?.id;\n\n\t\tif (id) {\n\t\t\tstore.getState().actions.upsert({\n\t\t\t\tid,\n\t\t\t\ttype: \"info\",\n\t\t\t\ttitle: opts?.title,\n\t\t\t\tmessage,\n\t\t\t\tdismissible: false\n\t\t\t});\n\t\t}\n\n\t\tconst notificationId =\n\t\t\tid ??\n\t\t\tstore.getState().actions.push({\n\t\t\t\ttype: \"info\",\n\t\t\t\ttitle: opts?.title,\n\t\t\t\tmessage,\n\t\t\t\tdismissible: false\n\t\t\t});\n\n\t\treturn {\n\t\t\tid: notificationId,\n\t\t\tupdate: (nextMessage: string) => notify.update(notificationId, { message: nextMessage }),\n\t\t\tsuccess: (nextMessage: string, ttlMs = 2500) =>\n\t\t\t\tnotify.update(notificationId, { type: \"success\", message: nextMessage, dismissible: true, ttlMs }),\n\t\t\terror: (nextMessage: string, ttlMs = 5000) =>\n\t\t\t\tnotify.update(notificationId, { type: \"error\", message: nextMessage, dismissible: true, ttlMs }),\n\t\t\tdismiss: () => notify.dismiss(notificationId)\n\t\t};\n\t}\n});\n","import { createStore, type StoreApi } from \"zustand\";\n\nimport { type Notification, type NotificationAction, type NotificationId, type NotificationType } from \"./types\";\n\nexport type NotificationPushInput = {\n\tid?: NotificationId;\n\ttype: NotificationType;\n\ttitle?: string;\n\tmessage: string;\n\tttlMs?: number;\n\tdismissible?: boolean;\n\tactions?: NotificationAction[];\n};\n\nexport type NotificationUpdatePatch = Partial<Omit<Notification, \"id\" | \"createdAt\">> & {\n\t// ttlMs можно менять; createdAt — нет\n};\n\nexport type NotificationsState = {\n\t/** Активные toast-уведомления, которые должен отображать краткоживущий host. */\n\titems: Notification[];\n\t/** Полная история за время жизни store; TTL, dismiss и переполнение toast-стека её не сокращают. */\n\thistory: Notification[];\n};\n\nexport type NotificationsActions = {\n\tpush: (input: NotificationPushInput) => NotificationId;\n\tupdate: (id: NotificationId, patch: NotificationUpdatePatch) => boolean;\n\tupsert: (input: NotificationPushInput & { id: NotificationId }) => NotificationId;\n\tdismiss: (id: NotificationId) => void;\n\tclear: () => void;\n\tclearHistory: () => void;\n};\n\nexport type NotificationsStore = NotificationsState & {\n\tactions: NotificationsActions;\n};\n\nconst genId = (): NotificationId => `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;\n\nconst MAX_VISIBLE_ITEMS = 6;\n\nexport function createNotificationsStore() {\n\tconst timers = new Map<NotificationId, number>();\n\n\tconst clearTimer = (id: NotificationId) => {\n\t\tconst t = timers.get(id);\n\t\tif (t) {\n\t\t\twindow.clearTimeout(t);\n\t\t\ttimers.delete(id);\n\t\t}\n\t};\n\n\tconst armTimer = (store: StoreApi<NotificationsStore>, id: NotificationId, ttlMs: number = 10_000) => {\n\t\tclearTimer(id);\n\t\tif (ttlMs <= 0) return;\n\n\t\tconst t = window.setTimeout(() => {\n\t\t\tstore.getState().actions.dismiss(id);\n\t\t}, ttlMs);\n\n\t\ttimers.set(id, t);\n\t};\n\n\tconst store = createStore<NotificationsStore>()((set, get) => ({\n\t\titems: [],\n\t\thistory: [],\n\n\t\tactions: {\n\t\t\tpush: (input) => {\n\t\t\t\tconst id = input.id ?? genId();\n\t\t\t\tconst now = Date.now();\n\n\t\t\t\tconst notif: Notification = {\n\t\t\t\t\tid,\n\t\t\t\t\ttype: input.type,\n\t\t\t\t\ttitle: input.title,\n\t\t\t\t\tmessage: input.message,\n\t\t\t\t\tcreatedAt: now,\n\t\t\t\t\tttlMs: input.ttlMs,\n\t\t\t\t\tdismissible: input.dismissible ?? true,\n\t\t\t\t\tactions: input.actions\n\t\t\t\t};\n\n\t\t\t\tconst currentItems = get().items;\n\t\t\t\tconst nextItems = [notif, ...currentItems.filter((notification) => notification.id !== id)].slice(0, MAX_VISIBLE_ITEMS);\n\t\t\t\tconst visibleIds = new Set(nextItems.map((notification) => notification.id));\n\n\t\t\t\t// Для вытесненного toast таймер больше не нужен: запись уже останется в history.\n\t\t\t\tcurrentItems.forEach((notification) => {\n\t\t\t\t\tif (!visibleIds.has(notification.id)) clearTimer(notification.id);\n\t\t\t\t});\n\n\t\t\t\tset((state) => ({\n\t\t\t\t\titems: nextItems,\n\t\t\t\t\thistory: [notif, ...state.history.filter((notification) => notification.id !== id)]\n\t\t\t\t}));\n\n\t\t\t\tarmTimer(store, id, notif.ttlMs);\n\t\t\t\treturn id;\n\t\t\t},\n\n\t\t\tupdate: (id, patch) => {\n\t\t\t\tconst previous = get().items.find((notification) => notification.id === id);\n\t\t\t\tif (!previous) return false;\n\n\t\t\t\tconst next: Notification = {\n\t\t\t\t\t...previous,\n\t\t\t\t\t...patch,\n\t\t\t\t\tid: previous.id,\n\t\t\t\t\tcreatedAt: previous.createdAt\n\t\t\t\t};\n\n\t\t\t\tset((state) => ({\n\t\t\t\t\titems: state.items.map((notification) => (notification.id === id ? next : notification)),\n\t\t\t\t\thistory: state.history.map((notification) => (notification.id === id ? next : notification))\n\t\t\t\t}));\n\n\t\t\t\t// если ttlMs изменили — пере-армим таймер\n\t\t\t\tif (\"ttlMs\" in patch) {\n\t\t\t\t\tarmTimer(store, id, next.ttlMs);\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\t\t\t},\n\n\t\t\tupsert: (input) => {\n\t\t\t\tconst ok = get().actions.update(input.id, {\n\t\t\t\t\ttype: input.type,\n\t\t\t\t\ttitle: input.title,\n\t\t\t\t\tmessage: input.message,\n\t\t\t\t\tttlMs: input.ttlMs,\n\t\t\t\t\tdismissible: input.dismissible\n\t\t\t\t});\n\n\t\t\t\tif (ok) return input.id;\n\t\t\t\treturn get().actions.push(input);\n\t\t\t},\n\n\t\t\tdismiss: (id) => {\n\t\t\t\tclearTimer(id);\n\t\t\t\tset((s) => ({ items: s.items.filter((n) => n.id !== id) }));\n\t\t\t},\n\n\t\t\tclear: () => {\n\t\t\t\ttimers.forEach((t) => window.clearTimeout(t));\n\t\t\t\ttimers.clear();\n\t\t\t\tset({ items: [] });\n\t\t\t},\n\n\t\t\tclearHistory: () => {\n\t\t\t\tset({ history: [] });\n\t\t\t}\n\t\t}\n\t}));\n\n\treturn store;\n}\n\nexport type NotificationsStoreApi = StoreApi<NotificationsStore>;\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import type { NotificationPushInput, NotificationsStoreApi, NotificationUpdatePatch } from "./store";
2
- import type { NotificationId } from "./types";
1
+ import { type NotificationPushInput, type NotificationsStoreApi, type NotificationUpdatePatch } from "./store";
2
+ import { type NotificationId } from "./types";
3
3
  export declare const bindNotifications: (store: NotificationsStoreApi) => () => void;
4
4
  export declare const notify: Readonly<{
5
5
  push: (input: NotificationPushInput) => string;
@@ -9,6 +9,7 @@ export declare const notify: Readonly<{
9
9
  update: (id: NotificationId, patch: NotificationUpdatePatch) => boolean;
10
10
  dismiss: (id: NotificationId) => void;
11
11
  clear: () => void;
12
+ clearHistory: () => void;
12
13
  success: (message: string, opts?: Omit<NotificationPushInput, "type" | "message">) => string;
13
14
  info: (message: string, opts?: Omit<NotificationPushInput, "type" | "message">) => string;
14
15
  warning: (message: string, opts?: Omit<NotificationPushInput, "type" | "message">) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/notifications/notify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AACrG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAI9C,eAAO,MAAM,iBAAiB,GAAI,OAAO,qBAAqB,eAQ7D,CAAC;AAOF,eAAO,MAAM,MAAM;kBACJ,qBAAqB;oBACnB,qBAAqB,GAAG;QAAE,EAAE,EAAE,cAAc,CAAA;KAAE;iBACjD,cAAc,SAAS,uBAAuB;kBAE7C,cAAc;;uBAGT,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;oBAIjE,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;uBAI3D,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;qBAIhE,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;IAK/E;;;;OAIG;wBACiB,MAAM,SAAS;QAAE,EAAE,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;;8BAyBlD,MAAM;+BACL,MAAM;6BAER,MAAM;;;EAK5B,CAAC"}
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/notifications/notify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qBAAqB,EAAE,KAAK,qBAAqB,EAAE,KAAK,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAC/G,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAI9C,eAAO,MAAM,iBAAiB,GAAI,OAAO,qBAAqB,eAQ7D,CAAC;AAOF,eAAO,MAAM,MAAM;kBACJ,qBAAqB;oBACnB,qBAAqB,GAAG;QAAE,EAAE,EAAE,cAAc,CAAA;KAAE;iBACjD,cAAc,SAAS,uBAAuB;kBAE7C,cAAc;;;uBAIT,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;oBAIjE,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;uBAI3D,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;qBAIhE,MAAM,SAAS,IAAI,CAAC,qBAAqB,EAAE,MAAM,GAAG,SAAS,CAAC;IAK/E;;;;OAIG;wBACiB,MAAM,SAAS;QAAE,EAAE,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;;8BAyBlD,MAAM;+BACL,MAAM;6BAER,MAAM;;;EAK5B,CAAC"}
@@ -1,5 +1,5 @@
1
- import { StoreApi } from "zustand";
2
- import { Notification, NotificationAction, NotificationId, NotificationType } from "./types";
1
+ import { type StoreApi } from "zustand";
2
+ import { type Notification, type NotificationAction, type NotificationId, type NotificationType } from "./types";
3
3
  export type NotificationPushInput = {
4
4
  id?: NotificationId;
5
5
  type: NotificationType;
@@ -10,10 +10,13 @@ export type NotificationPushInput = {
10
10
  actions?: NotificationAction[];
11
11
  };
12
12
  export type NotificationUpdatePatch = Partial<Omit<Notification, "id" | "createdAt">> & {};
13
- type NotificationsState = {
13
+ export type NotificationsState = {
14
+ /** Активные toast-уведомления, которые должен отображать краткоживущий host. */
14
15
  items: Notification[];
16
+ /** Полная история за время жизни store; TTL, dismiss и переполнение toast-стека её не сокращают. */
17
+ history: Notification[];
15
18
  };
16
- type NotificationsActions = {
19
+ export type NotificationsActions = {
17
20
  push: (input: NotificationPushInput) => NotificationId;
18
21
  update: (id: NotificationId, patch: NotificationUpdatePatch) => boolean;
19
22
  upsert: (input: NotificationPushInput & {
@@ -21,11 +24,11 @@ type NotificationsActions = {
21
24
  }) => NotificationId;
22
25
  dismiss: (id: NotificationId) => void;
23
26
  clear: () => void;
27
+ clearHistory: () => void;
24
28
  };
25
29
  export type NotificationsStore = NotificationsState & {
26
30
  actions: NotificationsActions;
27
31
  };
28
- export declare const createNotificationsStore: () => StoreApi<NotificationsStore>;
32
+ export declare function createNotificationsStore(): StoreApi<NotificationsStore>;
29
33
  export type NotificationsStoreApi = StoreApi<NotificationsStore>;
30
- export {};
31
34
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/notifications/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAE7F,MAAM,MAAM,qBAAqB,GAAG;IACnC,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,WAAW,CAAC,CAAC,GAAG,EAEvF,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACzB,KAAK,EAAE,YAAY,EAAE,CAAC;CACtB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC3B,IAAI,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,cAAc,CAAC;IACvD,MAAM,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC;IACxE,MAAM,EAAE,CAAC,KAAK,EAAE,qBAAqB,GAAG;QAAE,EAAE,EAAE,cAAc,CAAA;KAAE,KAAK,cAAc,CAAC;IAClF,OAAO,EAAE,CAAC,EAAE,EAAE,cAAc,KAAK,IAAI,CAAC;IACtC,KAAK,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,kBAAkB,GAAG;IACrD,OAAO,EAAE,oBAAoB,CAAC;CAC9B,CAAC;AAMF,eAAO,MAAM,wBAAwB,oCAqGpC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/notifications/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAErD,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,cAAc,EAAE,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEjH,MAAM,MAAM,qBAAqB,GAAG;IACnC,EAAE,CAAC,EAAE,cAAc,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,WAAW,CAAC,CAAC,GAAG,EAEvF,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAChC,gFAAgF;IAChF,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,oGAAoG;IACpG,OAAO,EAAE,YAAY,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAClC,IAAI,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,cAAc,CAAC;IACvD,MAAM,EAAE,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,uBAAuB,KAAK,OAAO,CAAC;IACxE,MAAM,EAAE,CAAC,KAAK,EAAE,qBAAqB,GAAG;QAAE,EAAE,EAAE,cAAc,CAAA;KAAE,KAAK,cAAc,CAAC;IAClF,OAAO,EAAE,CAAC,EAAE,EAAE,cAAc,KAAK,IAAI,CAAC;IACtC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,YAAY,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,kBAAkB,GAAG;IACrD,OAAO,EAAE,oBAAoB,CAAC;CAC9B,CAAC;AAMF,wBAAgB,wBAAwB,iCAmHvC;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC,kBAAkB,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-lib",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Reusable React and TypeScript foundation utilities, hooks, and primitives.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -1279,7 +1279,7 @@ console.log(overlayRef === existingRef); // true.
1279
1279
  2. пытается прочитать `overlayRef.current`;
1280
1280
  3. если ref ещё пустой, повторяет попытку через `requestAnimationFrame`;
1281
1281
  4. когда container найден, вычисляет initial focus;
1282
- 5. вызывает `.focus()`, только если target — подключённый `HTMLElement`;
1282
+ 5. вызывает `.focus({ preventScroll: true })`, только если target — подключённый `HTMLElement`;
1283
1283
  6. при `trapFocus: true` добавляет `keydown` listener на container.
1284
1284
 
1285
1285
  Если `active: true`, но container никогда не монтируется, requestAnimationFrame retry продолжается до cleanup. Всегда согласовывайте `active` с реальным mount overlay.
@@ -1421,7 +1421,10 @@ Container должен быть программно focusable.
1421
1421
  restoreFocusTarget?.() ?? previousActiveElement;
1422
1422
  ```
1423
1423
 
1424
- Focus возвращается, только если выбранный element всё ещё `isConnected`.
1424
+ Focus возвращается, только если выбранный element всё ещё `isConnected`,
1425
+ через `.focus({ preventScroll: true })`. Открытие и закрытие overlay не меняют
1426
+ прокрутку документа, в том числе пока портал ещё позиционируется. Переходы
1427
+ по Tab сохраняют обычную прокрутку к выбранному элементу внутри длинного overlay.
1425
1428
 
1426
1429
  Типичный сценарий:
1427
1430
 
@@ -4,7 +4,7 @@ import { Meta } from "@storybook/addon-docs/blocks";
4
4
 
5
5
  # Notifications
6
6
 
7
- Модуль `@ryuzaki13/react-foundation-lib/notifications` хранит очередь коротких уведомлений и предоставляет два способа управления ею:
7
+ Модуль `@ryuzaki13/react-foundation-lib/notifications` хранит активный toast-стек и полную историю уведомлений, а также предоставляет два способа управления ими:
8
8
 
9
9
  - Zustand-store — для React-интерфейса, который отображает уведомления;
10
10
  - объект `notify` — для императивных вызовов из обработчиков, сервисов и `try/catch`.
@@ -88,6 +88,8 @@ export function NotificationsViewport() {
88
88
  }
89
89
  ```
90
90
 
91
+ `items` содержит только активные toast-уведомления и ограничен шестью элементами. Для отдельного центра уведомлений, страницы или содержимого диалога подписывайтесь на `state.history`: эта коллекция не сокращается при TTL, ручном закрытии и переполнении toast-стека.
92
+
91
93
  ### 3. Показывайте уведомления
92
94
 
93
95
  ```ts
@@ -137,17 +139,21 @@ type Notification = {
137
139
  ```ts
138
140
  type NotificationsStore = {
139
141
  items: Notification[];
142
+ history: Notification[];
140
143
  actions: {
141
144
  push(input): NotificationId;
142
145
  update(id, patch): boolean;
143
146
  upsert(inputWithId): NotificationId;
144
147
  dismiss(id): void;
145
148
  clear(): void;
149
+ clearHistory(): void;
146
150
  };
147
151
  };
148
152
  ```
149
153
 
150
- Очередь ограничена шестью элементами. Новое уведомление помещается в начало. При добавлении с уже существующим `id` старый элемент удаляется, а новый получает новое `createdAt`.
154
+ `items` — стек видимых toast-уведомлений, ограниченный шестью элементами. `history` — полная история за время жизни store. Новое уведомление помещается в начало обеих коллекций. При добавлении с уже существующим `id` старый элемент заменяется, а новый получает новое `createdAt`.
155
+
156
+ История живёт в памяти и не является журналом аудита: она сбрасывается при пересоздании store или явном `clearHistory()`. Поскольку `actions` уведомлений содержат функции, модуль намеренно не сохраняет историю в `localStorage` и не сериализует её.
151
157
 
152
158
  ### `push(input)`
153
159
 
@@ -196,9 +202,11 @@ notificationsStore.getState().actions.upsert({
196
202
 
197
203
  Важная деталь: при обновлении существующего элемента `upsert` меняет `type`, `title`, `message`, `ttlMs` и `dismissible`, но не заменяет `actions`. Для явной смены действий используйте `update(id, { actions })`.
198
204
 
199
- ### `dismiss(id)` и `clear()`
205
+ ### `dismiss(id)`, `clear()` и `clearHistory()`
206
+
207
+ `dismiss(id)` скрывает один toast и очищает его таймер. `clear()` скрывает весь toast-стек и очищает все таймеры. Обе операции сохраняют записи в `history`.
200
208
 
201
- `dismiss(id)` удаляет один элемент и очищает его таймер. `clear()` удаляет всю очередь и все таймеры.
209
+ `clearHistory()` явно очищает историю, не закрывая активные toast-уведомления. Чтобы полностью сбросить обе коллекции, последовательно вызовите `clear()` и `clearHistory()`.
202
210
 
203
211
  ## Императивный API `notify`
204
212
 
@@ -223,6 +231,7 @@ notify.upsert(inputWithId);
223
231
  notify.update(id, patch);
224
232
  notify.dismiss(id);
225
233
  notify.clear();
234
+ notify.clearHistory();
226
235
 
227
236
  notify.success(message, options);
228
237
  notify.info(message, options);
@@ -258,7 +267,7 @@ async function importFile(file: File) {
258
267
  - `update(message)` — меняет текст, не перезапуская таймер;
259
268
  - `success(message, ttlMs = 2500)` — делает уведомление успешным и закрываемым;
260
269
  - `error(message, ttlMs = 5000)` — делает уведомление ошибкой и закрываемым;
261
- - `dismiss()` — удаляет уведомление.
270
+ - `dismiss()` — скрывает toast, сохраняя уведомление в истории.
262
271
 
263
272
  При переданном стабильном `id` повторный `progress` продолжит существующий элемент. Это удобно для операции, которая может стартовать из нескольких мест.
264
273
 
@@ -307,17 +316,19 @@ function App() {
307
316
 
308
317
  ### Использование уведомления вместо состояния операции
309
318
 
310
- Очередь ограничена шестью элементами и живёт в памяти страницы. Не используйте её как журнал аудита, источник истины о фоновой задаче или долговременное хранилище ошибок.
319
+ История живёт в памяти страницы. Не используйте её как журнал аудита, источник истины о фоновой задаче или долговременное хранилище ошибок.
311
320
 
312
321
  ## Краткая карта API
313
322
 
314
323
  | Экспорт | Назначение |
315
324
  | ---------------------------- | -------------------------------------------------------- |
316
- | `createNotificationsStore()` | Создать независимый Zustand-store уведомлений |
317
- | `bindNotifications(store)` | Привязать store к глобальному фасаду `notify` |
318
- | `notify` | Императивно добавлять, обновлять и закрывать уведомления |
319
- | `Notification` | Полная модель сохранённого элемента |
320
- | `NotificationPushInput` | Параметры добавления элемента |
321
- | `NotificationUpdatePatch` | Допустимые поля частичного обновления |
322
- | `NotificationsStoreApi` | Тип vanilla Zustand-store |
323
- | `NotificationAction` | Кнопка-действие уведомления |
325
+ | `createNotificationsStore()` | Создать независимый Zustand-store toast-стека и истории |
326
+ | `bindNotifications(store)` | Привязать store к глобальному фасаду `notify` |
327
+ | `notify` | Императивно добавлять, обновлять и закрывать уведомления |
328
+ | `Notification` | Полная модель сохранённого элемента |
329
+ | `NotificationPushInput` | Параметры добавления элемента |
330
+ | `NotificationUpdatePatch` | Допустимые поля частичного обновления |
331
+ | `NotificationsState` | Состояние с активными `items` и полной `history` |
332
+ | `NotificationsActions` | Действия toast-стека и явная очистка истории |
333
+ | `NotificationsStoreApi` | Тип vanilla Zustand-store |
334
+ | `NotificationAction` | Кнопка-действие уведомления |