{"version":3,"file":"index.cjs","sources":["../src/types.ts","../src/useMangaViewer.ts","../src/MangaViewer.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport type { HtmlPage, ImagePage, Manga } from \"@yui540/comimi\";\n\n/**\n * An HTML page whose body is a React node instead of an HTML string.\n *\n * The node is rendered into a stable host element via a React portal, so its\n * contents update reactively as `content` changes — without recreating the\n * viewer or resetting the current page.\n */\nexport interface ReactHtmlPage extends Omit<HtmlPage, \"html\" | \"element\"> {\n  type: \"html\";\n  /** React content rendered into the page. Takes precedence over `html`. */\n  content?: ReactNode;\n  /** Raw HTML markup, used only when `content` is not provided. */\n  html?: string;\n}\n\n/** A page that may carry React content in place of an HTML string. */\nexport type ReactMangaPage = ImagePage | ReactHtmlPage;\n\n/** A manga whose pages may carry React content. */\nexport interface ReactManga extends Omit<Manga, \"pages\"> {\n  pages: ReactMangaPage[];\n}\n\n/** Type guard for pages that should be rendered through a React portal. */\nexport function isReactContentPage(\n  page: ReactMangaPage,\n): page is ReactHtmlPage & { content: ReactNode } {\n  return page.type === \"html\" && page.content !== undefined;\n}\n","import { useEffect, useRef, useState, type ReactPortal } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  createMangaViewer,\n  type HtmlPage,\n  type Manga,\n  type MangaPage,\n  type MangaViewerInstance,\n  type MangaViewerOptions,\n  type ViewerEventMap,\n  type ViewerEventName,\n} from \"@yui540/comimi\";\nimport { isReactContentPage, type ReactManga } from \"./types\";\n\n/**\n * comimi の各イベントを `on<EventName>` 形式の props にしたもの。\n * 例: `pageChange` → `onPageChange`、`favoritesChange` → `onFavoritesChange`。\n */\nexport type ViewerEventProps = {\n  [K in ViewerEventName as `on${Capitalize<K>}`]?: ViewerEventMap[K] extends void\n    ? () => void\n    : (event: ViewerEventMap[K]) => void;\n};\n\nconst EVENT_NAMES = [\n  \"ready\",\n  \"mangaChange\",\n  \"pageChange\",\n  \"settingsChange\",\n  \"layoutChange\",\n  \"overlayChange\",\n  \"panelChange\",\n  \"autoPageTurnChange\",\n  \"zoomChange\",\n  \"favoritesChange\",\n  \"notification\",\n  \"pageLoadError\",\n  \"destroy\",\n] as const satisfies readonly ViewerEventName[];\n\ntype EventPropName = keyof ViewerEventProps;\n\nfunction toPropName(eventName: ViewerEventName): EventPropName {\n  return `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}` as EventPropName;\n}\n\nexport const VIEWER_EVENT_PROP_NAMES: readonly EventPropName[] =\n  EVENT_NAMES.map(toPropName);\n\n/** props からイベントハンドラだけを取り出す。 */\nexport function pickViewerEventProps<T extends ViewerEventProps>(\n  props: T,\n): ViewerEventProps {\n  const picked: Record<string, unknown> = {};\n  for (const name of VIEWER_EVENT_PROP_NAMES) {\n    if (props[name] !== undefined) picked[name] = props[name];\n  }\n  return picked as ViewerEventProps;\n}\n\nexport interface UseMangaViewerOptions\n  extends Omit<MangaViewerOptions, \"events\" | \"className\" | \"manga\">,\n    ViewerEventProps {\n  manga: ReactManga;\n}\n\nexport interface UseMangaViewerResult {\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  viewer: MangaViewerInstance | null;\n  /** Portals that render React page content. Must be rendered by the consumer. */\n  portals: ReactPortal[];\n}\n\nexport function useMangaViewer(\n  options: UseMangaViewerOptions,\n): UseMangaViewerResult {\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const [viewer, setViewer] = useState<MangaViewerInstance | null>(null);\n\n  // ハンドラは ref に保持し、毎レンダーで関数が変わってもビューワーを再生成しない。\n  const handlersRef = useRef<ViewerEventProps>(pickViewerEventProps(options));\n  handlersRef.current = pickViewerEventProps(options);\n\n  // Stable host elements that React portals render into, keyed by page id.\n  // Reusing the same element across renders lets the viewer cache the page\n  // slot while React keeps full control over its contents.\n  const portalContainersRef = useRef<Map<string, HTMLElement>>(new Map());\n  const getPortalContainer = (id: string): HTMLElement => {\n    let container = portalContainersRef.current.get(id);\n    if (!container) {\n      container = document.createElement(\"div\");\n      container.className = \"comimi-react-page\";\n      container.style.width = \"100%\";\n      container.style.height = \"100%\";\n      portalContainersRef.current.set(id, container);\n    }\n    return container;\n  };\n\n  // Convert a ReactManga into a core Manga, swapping React-content pages for\n  // HTML pages backed by their stable host element.\n  const toCoreManga = (manga: ReactManga): Manga => {\n    const pages: MangaPage[] = manga.pages.map((page) => {\n      if (isReactContentPage(page)) {\n        const { content: _content, html: _html, ...rest } = page;\n        return { ...rest, element: getPortalContainer(page.id) } as HtmlPage;\n      }\n      return page as MangaPage;\n    });\n    return { ...manga, pages };\n  };\n\n  const initialOptionsRef = useRef<\n    Omit<MangaViewerOptions, \"manga\" | \"events\">\n  >({\n    initialPageIndex: options.initialPageIndex,\n    initialPageQueryParam: options.initialPageQueryParam,\n    locale: options.locale,\n    translations: options.translations,\n    settings: options.settings,\n    storage: options.storage,\n    resolvePageSrc: options.resolvePageSrc,\n    lockLayoutMode: options.lockLayoutMode,\n    mascot: options.mascot,\n    hiddenSettings: options.hiddenSettings,\n    forceSettings: options.forceSettings,\n  });\n  const mangaRef = useRef<ReactManga>(options.manga);\n  mangaRef.current = options.manga;\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const events: NonNullable<MangaViewerOptions[\"events\"]> = {};\n    for (const eventName of EVENT_NAMES) {\n      const propName = toPropName(eventName);\n      (events as Record<string, (payload: unknown) => void>)[eventName] = (\n        payload: unknown,\n      ) => {\n        const handler = handlersRef.current[propName] as\n          | ((payload: unknown) => void)\n          | undefined;\n        handler?.(payload);\n      };\n    }\n\n    const instance = createMangaViewer(container, {\n      ...initialOptionsRef.current,\n      manga: toCoreManga(mangaRef.current),\n      events,\n    });\n\n    setViewer(instance);\n\n    return () => {\n      instance.destroy();\n      setViewer(null);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Re-run setManga only when the page *structure* changes (ids, order, image\n  // sources, raw html, etc.). React content changes are reflected through the\n  // portals below, so they must not trigger a viewer reset.\n  const signature = pageStructureSignature(options.manga);\n  const signatureRef = useRef(signature);\n  useEffect(() => {\n    if (!viewer) return;\n    if (signatureRef.current === signature) return;\n    signatureRef.current = signature;\n    void viewer.setManga(toCoreManga(mangaRef.current));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [viewer, signature]);\n\n  const settingsRef = useRef(options.settings);\n  useEffect(() => {\n    if (!viewer) return;\n    if (settingsRef.current === options.settings) return;\n    settingsRef.current = options.settings;\n    if (options.settings) {\n      void viewer.updateSettings(options.settings);\n    }\n  }, [viewer, options.settings]);\n\n  // Drop host elements for pages that no longer exist.\n  useEffect(() => {\n    const liveIds = new Set(options.manga.pages.map((page) => page.id));\n    for (const id of portalContainersRef.current.keys()) {\n      if (!liveIds.has(id)) {\n        portalContainersRef.current.delete(id);\n      }\n    }\n  });\n\n  const portals = options.manga.pages\n    .filter(isReactContentPage)\n    .map((page) =>\n      createPortal(page.content, getPortalContainer(page.id), page.id),\n    );\n\n  return { containerRef, viewer, portals };\n}\n\nfunction pageStructureSignature(manga: ReactManga): string {\n  return JSON.stringify([\n    manga.id,\n    manga.pages.map((page) => {\n      if (page.type === \"image\") {\n        return [\"image\", page.id, page.src, page.alt ?? null];\n      }\n      return [\n        \"html\",\n        page.id,\n        page.content !== undefined ? \"react\" : \"string\",\n        page.html ?? null,\n        page.aspectRatio ?? null,\n        page.sandbox ?? null,\n      ];\n    }),\n  ]);\n}\n","import {\n  forwardRef,\n  useImperativeHandle,\n  type HTMLAttributes,\n} from \"react\";\nimport type { MangaViewerInstance } from \"@yui540/comimi\";\nimport {\n  pickViewerEventProps,\n  useMangaViewer,\n  VIEWER_EVENT_PROP_NAMES,\n  type UseMangaViewerOptions,\n} from \"./useMangaViewer\";\n\nexport interface MangaViewerProps\n  extends UseMangaViewerOptions,\n    Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {}\n\nexport type MangaViewerHandle = MangaViewerInstance;\n\nexport const MangaViewer = forwardRef<MangaViewerHandle, MangaViewerProps>(\n  function MangaViewer(props, ref) {\n    const {\n      manga,\n      initialPageIndex,\n      initialPageQueryParam,\n      locale,\n      translations,\n      settings,\n      storage,\n      resolvePageSrc,\n      lockLayoutMode,\n      mascot,\n      hiddenSettings,\n      forceSettings,\n      ...rest\n    } = props;\n\n    // イベント props は div に渡さない。\n    const divProps: Record<string, unknown> = { ...rest };\n    for (const name of VIEWER_EVENT_PROP_NAMES) {\n      delete divProps[name];\n    }\n\n    const { containerRef, viewer, portals } = useMangaViewer({\n      manga,\n      initialPageIndex,\n      initialPageQueryParam,\n      locale,\n      translations,\n      settings,\n      storage,\n      resolvePageSrc,\n      lockLayoutMode,\n      mascot,\n      hiddenSettings,\n      forceSettings,\n      ...pickViewerEventProps(rest),\n    });\n\n    useImperativeHandle(ref, () => viewer as MangaViewerHandle, [viewer]);\n\n    return (\n      <>\n        <div\n          ref={containerRef}\n          {...(divProps as Omit<HTMLAttributes<HTMLDivElement>, \"children\">)}\n        />\n        {portals}\n      </>\n    );\n  },\n);\n"],"names":["isReactContentPage","page","EVENT_NAMES","toPropName","eventName","VIEWER_EVENT_PROP_NAMES","pickViewerEventProps","props","picked","name","useMangaViewer","options","containerRef","useRef","viewer","setViewer","useState","handlersRef","portalContainersRef","getPortalContainer","id","container","toCoreManga","manga","pages","_content","_html","rest","initialOptionsRef","mangaRef","useEffect","events","propName","payload","handler","instance","createMangaViewer","signature","pageStructureSignature","signatureRef","settingsRef","liveIds","portals","createPortal","MangaViewer","forwardRef","ref","initialPageIndex","initialPageQueryParam","locale","translations","settings","storage","resolvePageSrc","lockLayoutMode","mascot","hiddenSettings","forceSettings","divProps","useImperativeHandle","jsxs","Fragment","jsx"],"mappings":"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"}