chromium-tabs 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { Tab, TabGroup, TabStripModel, TabStripModelOptions } from './core/index.js';
2
- export { AddTabFlags, AddTabOptions, CanDiscardDecision, CanDiscardResult, CannotDiscardReason, CloseAllStoppedReason, CloseTabFlags, DiscardReason, IndexRange, ListSelectionModel, NO_TAB, ReconcileOptions, ReconcileTab, TAB_GROUP_COLORS, TabGroupChange, TabGroupColor, TabGroupId, TabGroupVisualData, TabId, TabLifecycleManager, TabLifecycleOptions, TabOpenCause, TabStripModelChange, TabStripModelObserver, TabStripSelectionChange } from './core/index.js';
1
+ import { T as TabStripModel } from './tab-strip-model-JBpyUyRs.js';
2
+ export { C as CloseAllStoppedReason, I as IndexRange, L as ListSelectionModel, a as TabGroupChange, b as TabStripModelChange, c as TabStripModelObserver, d as TabStripSelectionChange } from './tab-strip-model-JBpyUyRs.js';
3
+ export { CanDiscardDecision, CanDiscardResult, CannotDiscardReason, DiscardReason, TabLifecycleManager, TabLifecycleOptions } from './core/index.js';
4
+ import { b as Tab, e as TabGroup, c as TabStripModelOptions } from './types-CYa7ouKw.js';
5
+ export { h as AddTabFlags, A as AddTabOptions, C as CloseTabFlags, N as NO_TAB, f as ReconcileOptions, R as ReconcileTab, i as TAB_GROUP_COLORS, j as TabGroupColor, T as TabGroupId, a as TabGroupVisualData, d as TabId, g as TabOpenCause } from './types-CYa7ouKw.js';
3
6
  import * as react from 'react';
4
7
  import { RefObject, PointerEvent, ReactNode, MouseEvent } from 'react';
5
8
 
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import {
2
- AddTabFlags,
3
- CloseTabFlags,
4
2
  ListSelectionModel,
5
- NO_TAB,
6
- TAB_GROUP_COLORS,
7
3
  TabLifecycleManager,
8
4
  TabStripModel
9
- } from "./chunk-2DTGNBUT.js";
5
+ } from "./chunk-XUPBZES2.js";
6
+ import {
7
+ AddTabFlags,
8
+ CloseTabFlags,
9
+ NO_TAB,
10
+ TAB_GROUP_COLORS
11
+ } from "./chunk-NDM7JYY6.js";
10
12
 
11
13
  // src/react/use-tab-strip.ts
12
14
  import { useMemo, useRef, useSyncExternalStore } from "react";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/use-tab-strip.ts","../src/react/use-tab-drag.ts","../src/react/tab-panels.tsx","../src/react/tab-strip.tsx","../src/react/group-header.tsx","../src/react/tab-item.tsx","../src/react/tabs.tsx"],"sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport type { Tab, TabGroup, TabStripModelOptions } from '../core/types'\nimport { TabStripModel } from '../core/tab-strip-model'\n\nexport interface TabStripSnapshot<T> {\n tabs: ReadonlyArray<Tab<T>>\n activeTab: Tab<T> | null\n activeIndex: number\n selectedIndices: ReadonlyArray<number>\n groups: ReadonlyArray<TabGroup>\n}\n\n/** Creates a TabStripModel once for the component's lifetime. */\nexport function useTabStripModel<T>(\n init?: (model: TabStripModel<T>) => void,\n options?: TabStripModelOptions<T>,\n): TabStripModel<T> {\n const ref = useRef<TabStripModel<T> | null>(null)\n if (ref.current === null) {\n ref.current = new TabStripModel<T>(options)\n init?.(ref.current)\n }\n return ref.current\n}\n\n/**\n * Subscribes a component to a TabStripModel. Re-renders on any model change\n * (the model batches per-operation, so one operation is one render).\n */\nexport function useTabStrip<T>(model: TabStripModel<T>): TabStripSnapshot<T> {\n const store = useMemo(() => {\n let version = 0\n let snapshotVersion = -1\n let snapshot: TabStripSnapshot<T> | null = null\n return {\n subscribe(onStoreChange: () => void): () => void {\n return model.addObserver({\n onTabStripModelChanged: () => {\n version++\n onStoreChange()\n },\n onTabPinnedStateChanged: () => {\n version++\n onStoreChange()\n },\n onTabGroupedStateChanged: () => {\n version++\n onStoreChange()\n },\n onTabGroupChanged: () => {\n version++\n onStoreChange()\n },\n onTabChanged: () => {\n version++\n onStoreChange()\n },\n onTabDiscardedStateChanged: () => {\n version++\n onStoreChange()\n },\n })\n },\n getSnapshot(): TabStripSnapshot<T> {\n if (snapshot === null || snapshotVersion !== version) {\n snapshot = {\n tabs: model.getTabs(),\n activeTab: model.activeTab,\n activeIndex: model.activeIndex,\n selectedIndices: model.selectionModel().selectedIndices(),\n groups: model.getGroups(),\n }\n snapshotVersion = version\n }\n return snapshot\n },\n }\n }, [model])\n\n return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\n}\n","import { useCallback, useRef, useState, type PointerEvent, type RefObject } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\n\nconst DRAG_THRESHOLD_PX = 4\n\n/**\n * Pointer-based drag-to-reorder. On drag, the hovered insertion position is\n * computed from the midpoints of the rendered tabs and the model's moveTabTo\n * applies Chrome's clamping (pinned boundary) and group-assignment rules.\n */\nexport function useTabDrag<T>(\n model: TabStripModel<T>,\n containerRef: RefObject<HTMLElement | null>,\n): {\n draggingTabId: string | null\n onTabPointerDown: (event: PointerEvent, tabId: string) => void\n} {\n const [draggingTabId, setDraggingTabId] = useState<string | null>(null)\n const drag = useRef<{ tabId: string; startX: number; started: boolean } | null>(null)\n\n const onTabPointerDown = useCallback(\n (event: PointerEvent, tabId: string) => {\n if (event.button !== 0) return\n drag.current = { tabId, startX: event.clientX, started: false }\n const target = event.currentTarget as HTMLElement\n target.setPointerCapture(event.pointerId)\n\n const onMove = (e: globalThis.PointerEvent) => {\n const state = drag.current\n if (!state) return\n if (!state.started) {\n if (Math.abs(e.clientX - state.startX) < DRAG_THRESHOLD_PX) return\n state.started = true\n setDraggingTabId(state.tabId)\n }\n const container = containerRef.current\n if (!container) return\n\n const tab = model.getTabById(state.tabId)\n if (!tab) return\n const currentIndex = model.indexOfTab(tab)\n\n // Insertion position: how many other tabs' midpoints are left of the\n // pointer.\n const elements = [...container.querySelectorAll<HTMLElement>('[data-tab-id]')]\n let targetIndex = 0\n for (const el of elements) {\n if (el.dataset['tabId'] === state.tabId) continue\n const rect = el.getBoundingClientRect()\n if (e.clientX > rect.left + rect.width / 2) targetIndex++\n }\n if (targetIndex !== currentIndex) {\n model.moveTabTo(currentIndex, targetIndex)\n }\n }\n\n const onUp = () => {\n drag.current = null\n setDraggingTabId(null)\n target.removeEventListener('pointermove', onMove)\n target.removeEventListener('pointerup', onUp)\n target.removeEventListener('pointercancel', onUp)\n }\n\n target.addEventListener('pointermove', onMove)\n target.addEventListener('pointerup', onUp)\n target.addEventListener('pointercancel', onUp)\n },\n [model, containerRef],\n )\n\n return { draggingTabId, onTabPointerDown }\n}\n","import { createContext, useContext, type CSSProperties, type ReactNode } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\nimport type { Tab } from '../core/types'\nimport { useTabStrip } from './use-tab-strip'\n\nexport type TabVisibility = 'visible' | 'hidden'\n\nconst TabVisibilityContext = createContext<TabVisibility>('visible')\n\n/**\n * Visibility of the enclosing tab panel. The React analogue of the page\n * visibility signal Chrome sends background tabs (WasShown/WasHidden): use it\n * to pause polling, animations, or media while the tab is in the background.\n */\nexport function useTabVisibility(): TabVisibility {\n return useContext(TabVisibilityContext)\n}\n\nexport interface TabPanelsProps<T> {\n model: TabStripModel<T>\n /** Renders a tab's content. Mounted once, kept alive while hidden. */\n children: (tab: Tab<T>) => ReactNode\n className?: string\n /**\n * Hiding strategy for inactive panels. 'display-none' (default) removes\n * hidden panels from layout. 'visibility' keeps layout (useful when\n * content measures itself and must not collapse to zero size).\n */\n hideMode?: 'display-none' | 'visibility'\n}\n\n/**\n * The content host: the React analogue of Chrome keeping background tabs'\n * pages alive. Every non-discarded tab's content stays mounted (component\n * state survives tab switches); only the active tab is visible. Discarded\n * tabs render nothing, and remount fresh when activated, which is exactly\n * Chrome's discard + reload-on-focus lifecycle.\n *\n * Panels are keyed by tab id, so reordering tabs never remounts content.\n */\nexport function TabPanels<T>({ model, children, className, hideMode = 'display-none' }: TabPanelsProps<T>) {\n const snapshot = useTabStrip(model)\n\n return (\n <div className={['ctabs-panels', className].filter(Boolean).join(' ')}>\n {snapshot.tabs.map((tab) => {\n if (tab.discarded) return null\n const visible = tab === snapshot.activeTab\n const style: CSSProperties =\n hideMode === 'display-none'\n ? { display: visible ? undefined : 'none' }\n : {\n visibility: visible ? undefined : 'hidden',\n position: visible ? undefined : 'absolute',\n inset: visible ? undefined : 0,\n }\n return (\n <div\n key={tab.id}\n role=\"tabpanel\"\n data-tab-panel-id={tab.id}\n className=\"ctabs-panel\"\n style={style}\n >\n <TabVisibilityContext.Provider value={visible ? 'visible' : 'hidden'}>\n {children(tab)}\n </TabVisibilityContext.Provider>\n </div>\n )\n })}\n </div>\n )\n}\n","import { useRef, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\nimport type { Tab } from '../core/types'\nimport { GroupHeader, GROUP_COLOR_VALUES } from './group-header'\nimport { TabItem } from './tab-item'\nimport { useTabDrag } from './use-tab-drag'\nimport { useTabStrip } from './use-tab-strip'\n\nexport interface TabStripProps<T> {\n model: TabStripModel<T>\n /** Renders a tab's content. Defaults to String(tab.data). */\n renderTab?: (tab: Tab<T>) => ReactNode\n /** Shows the new-tab button and handles clicks on it. */\n onNewTab?: () => void\n onTabContextMenu?: (index: number, event: MouseEvent) => void\n className?: string\n}\n\n/**\n * A Chrome-style tab strip bound to a TabStripModel. Click activates,\n * ctrl/cmd-click toggles selection, shift-click extends from the anchor,\n * middle-click closes, dragging reorders, arrow keys switch tabs and\n * ctrl/cmd+arrows move the active tab (hopping group boundaries like Chrome).\n */\nexport function TabStrip<T>({\n model,\n renderTab = (tab) => String(tab.data),\n onNewTab,\n onTabContextMenu,\n className,\n}: TabStripProps<T>) {\n const snapshot = useTabStrip(model)\n const containerRef = useRef<HTMLDivElement | null>(null)\n const { draggingTabId, onTabPointerDown } = useTabDrag(model, containerRef)\n\n const onActivate = (index: number, event: MouseEvent) => {\n if (event.shiftKey) {\n model.extendSelectionTo(index)\n } else if (event.metaKey || event.ctrlKey) {\n if (model.isTabSelected(index)) model.deselectTabAt(index)\n else model.selectTabAt(index)\n } else {\n model.activateTabAt(index, { userGesture: true })\n }\n }\n\n const onKeyDown = (event: KeyboardEvent) => {\n const move = event.metaKey || event.ctrlKey\n if (event.key === 'ArrowRight') {\n move ? model.moveTabNext() : model.selectNextTab({ userGesture: true })\n event.preventDefault()\n } else if (event.key === 'ArrowLeft') {\n move ? model.moveTabPrevious() : model.selectPreviousTab({ userGesture: true })\n event.preventDefault()\n }\n }\n\n const selected = new Set(snapshot.selectedIndices)\n const groupById = new Map(snapshot.groups.map((g) => [g.id, g]))\n const items: ReactNode[] = []\n let previousGroup: string | null = null\n\n snapshot.tabs.forEach((tab, index) => {\n if (tab.group !== null && tab.group !== previousGroup) {\n const group = groupById.get(tab.group)\n if (group) {\n items.push(\n <GroupHeader\n key={`group-${group.id}`}\n group={group}\n onToggleCollapsed={(id) => model.setGroupCollapsed(id, !model.isGroupCollapsed(id))}\n />,\n )\n }\n }\n previousGroup = tab.group\n\n if (tab.group !== null && model.isGroupCollapsed(tab.group)) return\n\n const groupColor = tab.group\n ? (GROUP_COLOR_VALUES[groupById.get(tab.group)?.visualData.color ?? 'grey'] ?? null)\n : null\n\n items.push(\n <TabItem\n key={tab.id}\n tab={tab}\n index={index}\n active={index === snapshot.activeIndex}\n selected={selected.has(index)}\n dragging={tab.id === draggingTabId}\n groupColor={groupColor}\n renderContent={renderTab}\n onPointerDown={onTabPointerDown}\n onActivate={onActivate}\n onClose={(i) => model.closeTabAt(i)}\n onContextMenu={onTabContextMenu}\n />,\n )\n })\n\n return (\n <div\n ref={containerRef}\n role=\"tablist\"\n tabIndex={0}\n className={['ctabs-strip', className].filter(Boolean).join(' ')}\n onKeyDown={onKeyDown}\n >\n {items}\n {onNewTab && (\n <button\n type=\"button\"\n className=\"ctabs-new-tab\"\n aria-label=\"New tab\"\n onClick={onNewTab}\n >\n +\n </button>\n )}\n </div>\n )\n}\n","import type { TabGroup } from '../core/types'\n\nexport const GROUP_COLOR_VALUES: Record<string, string> = {\n grey: '#5f6368',\n blue: '#1a73e8',\n red: '#d93025',\n yellow: '#f9ab00',\n green: '#188038',\n pink: '#d01884',\n purple: '#a142f4',\n cyan: '#007b83',\n orange: '#fa903e',\n}\n\nexport interface GroupHeaderProps {\n group: TabGroup\n onToggleCollapsed: (groupId: string) => void\n}\n\n/** The colored group chip shown before a group's tabs, like Chrome's. */\nexport function GroupHeader({ group, onToggleCollapsed }: GroupHeaderProps) {\n const color = GROUP_COLOR_VALUES[group.visualData.color] ?? GROUP_COLOR_VALUES['grey']!\n return (\n <button\n type=\"button\"\n className={[\n 'ctabs-group-header',\n group.visualData.isCollapsed && 'ctabs-group-header--collapsed',\n ]\n .filter(Boolean)\n .join(' ')}\n style={{ ['--ctabs-group-color' as string]: color }}\n onClick={() => onToggleCollapsed(group.id)}\n title={group.visualData.isCollapsed ? 'Expand group' : 'Collapse group'}\n >\n {group.visualData.title || ' '}\n </button>\n )\n}\n","import type { MouseEvent, PointerEvent, ReactNode } from 'react'\nimport type { Tab } from '../core/types'\n\nexport interface TabItemProps<T> {\n tab: Tab<T>\n index: number\n active: boolean\n selected: boolean\n dragging: boolean\n groupColor: string | null\n renderContent: (tab: Tab<T>) => ReactNode\n onPointerDown: (event: PointerEvent, tabId: string) => void\n onActivate: (index: number, event: MouseEvent) => void\n onClose: (index: number) => void\n onContextMenu?: (index: number, event: MouseEvent) => void\n}\n\nexport function TabItem<T>({\n tab,\n index,\n active,\n selected,\n dragging,\n groupColor,\n renderContent,\n onPointerDown,\n onActivate,\n onClose,\n onContextMenu,\n}: TabItemProps<T>) {\n return (\n <div\n role=\"tab\"\n aria-selected={active}\n data-tab-id={tab.id}\n className={[\n 'ctabs-tab',\n active && 'ctabs-tab--active',\n selected && !active && 'ctabs-tab--selected',\n tab.pinned && 'ctabs-tab--pinned',\n tab.discarded && 'ctabs-tab--discarded',\n dragging && 'ctabs-tab--dragging',\n groupColor && 'ctabs-tab--grouped',\n ]\n .filter(Boolean)\n .join(' ')}\n style={groupColor ? { ['--ctabs-group-color' as string]: groupColor } : undefined}\n onPointerDown={(e) => onPointerDown(e, tab.id)}\n onMouseDown={(e) => {\n // Middle click closes, like Chrome.\n if (e.button === 1) {\n e.preventDefault()\n onClose(index)\n }\n }}\n onClick={(e) => onActivate(index, e)}\n onContextMenu={(e) => onContextMenu?.(index, e)}\n >\n <span className=\"ctabs-tab__content\">{renderContent(tab)}</span>\n {!tab.pinned && (\n <button\n type=\"button\"\n className=\"ctabs-tab__close\"\n aria-label=\"Close tab\"\n onClick={(e) => {\n e.stopPropagation()\n onClose(index)\n }}\n onPointerDown={(e) => e.stopPropagation()}\n >\n ×\n </button>\n )}\n </div>\n )\n}\n","import type { MouseEvent, ReactNode } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\nimport type { Tab } from '../core/types'\nimport { TabPanels } from './tab-panels'\nimport { TabStrip } from './tab-strip'\n\nexport interface TabsProps<T> {\n model: TabStripModel<T>\n /** Renders a tab's strip label. Defaults to String(tab.data). */\n renderTab?: (tab: Tab<T>) => ReactNode\n /**\n * Renders a tab's content. Hosted in TabPanels: mounted once, kept alive\n * while the tab is in the background, unmounted only on discard.\n */\n children: (tab: Tab<T>) => ReactNode\n onNewTab?: () => void\n onTabContextMenu?: (index: number, event: MouseEvent) => void\n /** Panel hiding strategy, see TabPanels. */\n hideMode?: 'display-none' | 'visibility'\n className?: string\n}\n\n/**\n * The batteries-included layout: strip on top, keep-alive content below.\n * This is the recommended entry point — content state survives tab switches\n * by construction, because the panels host every loaded tab's tree like\n * Chrome keeps background pages alive.\n *\n * Use the composable pieces (TabStrip, TabPanels) directly only when you\n * need a custom layout, and keep content inside TabPanels unless you\n * specifically want remount-on-switch semantics.\n */\nexport function Tabs<T>({\n model,\n renderTab,\n children,\n onNewTab,\n onTabContextMenu,\n hideMode,\n className,\n}: TabsProps<T>) {\n return (\n <div className={['ctabs', className].filter(Boolean).join(' ')}>\n <TabStrip\n model={model}\n renderTab={renderTab}\n onNewTab={onNewTab}\n onTabContextMenu={onTabContextMenu}\n />\n <TabPanels model={model} hideMode={hideMode}>\n {children}\n </TabPanels>\n </div>\n )\n}\n"],"mappings":"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","names":["useRef","useRef","jsx","jsx","jsx","jsxs","useRef","jsx","jsxs"]}
1
+ {"version":3,"sources":["../src/react/use-tab-strip.ts","../src/react/use-tab-drag.ts","../src/react/tab-panels.tsx","../src/react/tab-strip.tsx","../src/react/group-header.tsx","../src/react/tab-item.tsx","../src/react/tabs.tsx"],"sourcesContent":["import { useMemo, useRef, useSyncExternalStore } from 'react'\nimport type { Tab, TabGroup, TabStripModelOptions } from '../core/types'\nimport { TabStripModel } from '../core/tab-strip-model'\n\nexport interface TabStripSnapshot<T> {\n tabs: ReadonlyArray<Tab<T>>\n activeTab: Tab<T> | null\n activeIndex: number\n selectedIndices: ReadonlyArray<number>\n groups: ReadonlyArray<TabGroup>\n}\n\n/** Creates a TabStripModel once for the component's lifetime. */\nexport function useTabStripModel<T>(\n init?: (model: TabStripModel<T>) => void,\n options?: TabStripModelOptions<T>,\n): TabStripModel<T> {\n const ref = useRef<TabStripModel<T> | null>(null)\n if (ref.current === null) {\n ref.current = new TabStripModel<T>(options)\n init?.(ref.current)\n }\n return ref.current\n}\n\n/**\n * Subscribes a component to a TabStripModel. Re-renders on any model change\n * (the model batches per-operation, so one operation is one render).\n */\nexport function useTabStrip<T>(model: TabStripModel<T>): TabStripSnapshot<T> {\n const store = useMemo(() => {\n let version = 0\n let snapshotVersion = -1\n let snapshot: TabStripSnapshot<T> | null = null\n return {\n subscribe(onStoreChange: () => void): () => void {\n return model.addObserver({\n onTabStripModelChanged: () => {\n version++\n onStoreChange()\n },\n onTabPinnedStateChanged: () => {\n version++\n onStoreChange()\n },\n onTabGroupedStateChanged: () => {\n version++\n onStoreChange()\n },\n onTabGroupChanged: () => {\n version++\n onStoreChange()\n },\n onTabChanged: () => {\n version++\n onStoreChange()\n },\n onTabDiscardedStateChanged: () => {\n version++\n onStoreChange()\n },\n })\n },\n getSnapshot(): TabStripSnapshot<T> {\n if (snapshot === null || snapshotVersion !== version) {\n snapshot = {\n tabs: model.getTabs(),\n activeTab: model.activeTab,\n activeIndex: model.activeIndex,\n selectedIndices: model.selectionModel().selectedIndices(),\n groups: model.getGroups(),\n }\n snapshotVersion = version\n }\n return snapshot\n },\n }\n }, [model])\n\n return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)\n}\n","import { useCallback, useRef, useState, type PointerEvent, type RefObject } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\n\nconst DRAG_THRESHOLD_PX = 4\n\n/**\n * Pointer-based drag-to-reorder. On drag, the hovered insertion position is\n * computed from the midpoints of the rendered tabs and the model's moveTabTo\n * applies Chrome's clamping (pinned boundary) and group-assignment rules.\n */\nexport function useTabDrag<T>(\n model: TabStripModel<T>,\n containerRef: RefObject<HTMLElement | null>,\n): {\n draggingTabId: string | null\n onTabPointerDown: (event: PointerEvent, tabId: string) => void\n} {\n const [draggingTabId, setDraggingTabId] = useState<string | null>(null)\n const drag = useRef<{ tabId: string; startX: number; started: boolean } | null>(null)\n\n const onTabPointerDown = useCallback(\n (event: PointerEvent, tabId: string) => {\n if (event.button !== 0) return\n drag.current = { tabId, startX: event.clientX, started: false }\n const target = event.currentTarget as HTMLElement\n target.setPointerCapture(event.pointerId)\n\n const onMove = (e: globalThis.PointerEvent) => {\n const state = drag.current\n if (!state) return\n if (!state.started) {\n if (Math.abs(e.clientX - state.startX) < DRAG_THRESHOLD_PX) return\n state.started = true\n setDraggingTabId(state.tabId)\n }\n const container = containerRef.current\n if (!container) return\n\n const tab = model.getTabById(state.tabId)\n if (!tab) return\n const currentIndex = model.indexOfTab(tab)\n\n // Insertion position: how many other tabs' midpoints are left of the\n // pointer.\n const elements = [...container.querySelectorAll<HTMLElement>('[data-tab-id]')]\n let targetIndex = 0\n for (const el of elements) {\n if (el.dataset['tabId'] === state.tabId) continue\n const rect = el.getBoundingClientRect()\n if (e.clientX > rect.left + rect.width / 2) targetIndex++\n }\n if (targetIndex !== currentIndex) {\n model.moveTabTo(currentIndex, targetIndex)\n }\n }\n\n const onUp = () => {\n drag.current = null\n setDraggingTabId(null)\n target.removeEventListener('pointermove', onMove)\n target.removeEventListener('pointerup', onUp)\n target.removeEventListener('pointercancel', onUp)\n }\n\n target.addEventListener('pointermove', onMove)\n target.addEventListener('pointerup', onUp)\n target.addEventListener('pointercancel', onUp)\n },\n [model, containerRef],\n )\n\n return { draggingTabId, onTabPointerDown }\n}\n","import { createContext, useContext, type CSSProperties, type ReactNode } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\nimport type { Tab } from '../core/types'\nimport { useTabStrip } from './use-tab-strip'\n\nexport type TabVisibility = 'visible' | 'hidden'\n\nconst TabVisibilityContext = createContext<TabVisibility>('visible')\n\n/**\n * Visibility of the enclosing tab panel. The React analogue of the page\n * visibility signal Chrome sends background tabs (WasShown/WasHidden): use it\n * to pause polling, animations, or media while the tab is in the background.\n */\nexport function useTabVisibility(): TabVisibility {\n return useContext(TabVisibilityContext)\n}\n\nexport interface TabPanelsProps<T> {\n model: TabStripModel<T>\n /** Renders a tab's content. Mounted once, kept alive while hidden. */\n children: (tab: Tab<T>) => ReactNode\n className?: string\n /**\n * Hiding strategy for inactive panels. 'display-none' (default) removes\n * hidden panels from layout. 'visibility' keeps layout (useful when\n * content measures itself and must not collapse to zero size).\n */\n hideMode?: 'display-none' | 'visibility'\n}\n\n/**\n * The content host: the React analogue of Chrome keeping background tabs'\n * pages alive. Every non-discarded tab's content stays mounted (component\n * state survives tab switches); only the active tab is visible. Discarded\n * tabs render nothing, and remount fresh when activated, which is exactly\n * Chrome's discard + reload-on-focus lifecycle.\n *\n * Panels are keyed by tab id, so reordering tabs never remounts content.\n */\nexport function TabPanels<T>({ model, children, className, hideMode = 'display-none' }: TabPanelsProps<T>) {\n const snapshot = useTabStrip(model)\n\n return (\n <div className={['ctabs-panels', className].filter(Boolean).join(' ')}>\n {snapshot.tabs.map((tab) => {\n if (tab.discarded) return null\n const visible = tab === snapshot.activeTab\n const style: CSSProperties =\n hideMode === 'display-none'\n ? { display: visible ? undefined : 'none' }\n : {\n visibility: visible ? undefined : 'hidden',\n position: visible ? undefined : 'absolute',\n inset: visible ? undefined : 0,\n }\n return (\n <div\n key={tab.id}\n role=\"tabpanel\"\n data-tab-panel-id={tab.id}\n className=\"ctabs-panel\"\n style={style}\n >\n <TabVisibilityContext.Provider value={visible ? 'visible' : 'hidden'}>\n {children(tab)}\n </TabVisibilityContext.Provider>\n </div>\n )\n })}\n </div>\n )\n}\n","import { useRef, type KeyboardEvent, type MouseEvent, type ReactNode } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\nimport type { Tab } from '../core/types'\nimport { GroupHeader, GROUP_COLOR_VALUES } from './group-header'\nimport { TabItem } from './tab-item'\nimport { useTabDrag } from './use-tab-drag'\nimport { useTabStrip } from './use-tab-strip'\n\nexport interface TabStripProps<T> {\n model: TabStripModel<T>\n /** Renders a tab's content. Defaults to String(tab.data). */\n renderTab?: (tab: Tab<T>) => ReactNode\n /** Shows the new-tab button and handles clicks on it. */\n onNewTab?: () => void\n onTabContextMenu?: (index: number, event: MouseEvent) => void\n className?: string\n}\n\n/**\n * A Chrome-style tab strip bound to a TabStripModel. Click activates,\n * ctrl/cmd-click toggles selection, shift-click extends from the anchor,\n * middle-click closes, dragging reorders, arrow keys switch tabs and\n * ctrl/cmd+arrows move the active tab (hopping group boundaries like Chrome).\n */\nexport function TabStrip<T>({\n model,\n renderTab = (tab) => String(tab.data),\n onNewTab,\n onTabContextMenu,\n className,\n}: TabStripProps<T>) {\n const snapshot = useTabStrip(model)\n const containerRef = useRef<HTMLDivElement | null>(null)\n const { draggingTabId, onTabPointerDown } = useTabDrag(model, containerRef)\n\n const onActivate = (index: number, event: MouseEvent) => {\n if (event.shiftKey) {\n model.extendSelectionTo(index)\n } else if (event.metaKey || event.ctrlKey) {\n if (model.isTabSelected(index)) model.deselectTabAt(index)\n else model.selectTabAt(index)\n } else {\n model.activateTabAt(index, { userGesture: true })\n }\n }\n\n const onKeyDown = (event: KeyboardEvent) => {\n const move = event.metaKey || event.ctrlKey\n if (event.key === 'ArrowRight') {\n move ? model.moveTabNext() : model.selectNextTab({ userGesture: true })\n event.preventDefault()\n } else if (event.key === 'ArrowLeft') {\n move ? model.moveTabPrevious() : model.selectPreviousTab({ userGesture: true })\n event.preventDefault()\n }\n }\n\n const selected = new Set(snapshot.selectedIndices)\n const groupById = new Map(snapshot.groups.map((g) => [g.id, g]))\n const items: ReactNode[] = []\n let previousGroup: string | null = null\n\n snapshot.tabs.forEach((tab, index) => {\n if (tab.group !== null && tab.group !== previousGroup) {\n const group = groupById.get(tab.group)\n if (group) {\n items.push(\n <GroupHeader\n key={`group-${group.id}`}\n group={group}\n onToggleCollapsed={(id) => model.setGroupCollapsed(id, !model.isGroupCollapsed(id))}\n />,\n )\n }\n }\n previousGroup = tab.group\n\n if (tab.group !== null && model.isGroupCollapsed(tab.group)) return\n\n const groupColor = tab.group\n ? (GROUP_COLOR_VALUES[groupById.get(tab.group)?.visualData.color ?? 'grey'] ?? null)\n : null\n\n items.push(\n <TabItem\n key={tab.id}\n tab={tab}\n index={index}\n active={index === snapshot.activeIndex}\n selected={selected.has(index)}\n dragging={tab.id === draggingTabId}\n groupColor={groupColor}\n renderContent={renderTab}\n onPointerDown={onTabPointerDown}\n onActivate={onActivate}\n onClose={(i) => model.closeTabAt(i)}\n onContextMenu={onTabContextMenu}\n />,\n )\n })\n\n return (\n <div\n ref={containerRef}\n role=\"tablist\"\n tabIndex={0}\n className={['ctabs-strip', className].filter(Boolean).join(' ')}\n onKeyDown={onKeyDown}\n >\n {items}\n {onNewTab && (\n <button\n type=\"button\"\n className=\"ctabs-new-tab\"\n aria-label=\"New tab\"\n onClick={onNewTab}\n >\n +\n </button>\n )}\n </div>\n )\n}\n","import type { TabGroup } from '../core/types'\n\nexport const GROUP_COLOR_VALUES: Record<string, string> = {\n grey: '#5f6368',\n blue: '#1a73e8',\n red: '#d93025',\n yellow: '#f9ab00',\n green: '#188038',\n pink: '#d01884',\n purple: '#a142f4',\n cyan: '#007b83',\n orange: '#fa903e',\n}\n\nexport interface GroupHeaderProps {\n group: TabGroup\n onToggleCollapsed: (groupId: string) => void\n}\n\n/** The colored group chip shown before a group's tabs, like Chrome's. */\nexport function GroupHeader({ group, onToggleCollapsed }: GroupHeaderProps) {\n const color = GROUP_COLOR_VALUES[group.visualData.color] ?? GROUP_COLOR_VALUES['grey']!\n return (\n <button\n type=\"button\"\n className={[\n 'ctabs-group-header',\n group.visualData.isCollapsed && 'ctabs-group-header--collapsed',\n ]\n .filter(Boolean)\n .join(' ')}\n style={{ ['--ctabs-group-color' as string]: color }}\n onClick={() => onToggleCollapsed(group.id)}\n title={group.visualData.isCollapsed ? 'Expand group' : 'Collapse group'}\n >\n {group.visualData.title || ' '}\n </button>\n )\n}\n","import type { MouseEvent, PointerEvent, ReactNode } from 'react'\nimport type { Tab } from '../core/types'\n\nexport interface TabItemProps<T> {\n tab: Tab<T>\n index: number\n active: boolean\n selected: boolean\n dragging: boolean\n groupColor: string | null\n renderContent: (tab: Tab<T>) => ReactNode\n onPointerDown: (event: PointerEvent, tabId: string) => void\n onActivate: (index: number, event: MouseEvent) => void\n onClose: (index: number) => void\n onContextMenu?: (index: number, event: MouseEvent) => void\n}\n\nexport function TabItem<T>({\n tab,\n index,\n active,\n selected,\n dragging,\n groupColor,\n renderContent,\n onPointerDown,\n onActivate,\n onClose,\n onContextMenu,\n}: TabItemProps<T>) {\n return (\n <div\n role=\"tab\"\n aria-selected={active}\n data-tab-id={tab.id}\n className={[\n 'ctabs-tab',\n active && 'ctabs-tab--active',\n selected && !active && 'ctabs-tab--selected',\n tab.pinned && 'ctabs-tab--pinned',\n tab.discarded && 'ctabs-tab--discarded',\n dragging && 'ctabs-tab--dragging',\n groupColor && 'ctabs-tab--grouped',\n ]\n .filter(Boolean)\n .join(' ')}\n style={groupColor ? { ['--ctabs-group-color' as string]: groupColor } : undefined}\n onPointerDown={(e) => onPointerDown(e, tab.id)}\n onMouseDown={(e) => {\n // Middle click closes, like Chrome.\n if (e.button === 1) {\n e.preventDefault()\n onClose(index)\n }\n }}\n onClick={(e) => onActivate(index, e)}\n onContextMenu={(e) => onContextMenu?.(index, e)}\n >\n <span className=\"ctabs-tab__content\">{renderContent(tab)}</span>\n {!tab.pinned && (\n <button\n type=\"button\"\n className=\"ctabs-tab__close\"\n aria-label=\"Close tab\"\n onClick={(e) => {\n e.stopPropagation()\n onClose(index)\n }}\n onPointerDown={(e) => e.stopPropagation()}\n >\n ×\n </button>\n )}\n </div>\n )\n}\n","import type { MouseEvent, ReactNode } from 'react'\nimport type { TabStripModel } from '../core/tab-strip-model'\nimport type { Tab } from '../core/types'\nimport { TabPanels } from './tab-panels'\nimport { TabStrip } from './tab-strip'\n\nexport interface TabsProps<T> {\n model: TabStripModel<T>\n /** Renders a tab's strip label. Defaults to String(tab.data). */\n renderTab?: (tab: Tab<T>) => ReactNode\n /**\n * Renders a tab's content. Hosted in TabPanels: mounted once, kept alive\n * while the tab is in the background, unmounted only on discard.\n */\n children: (tab: Tab<T>) => ReactNode\n onNewTab?: () => void\n onTabContextMenu?: (index: number, event: MouseEvent) => void\n /** Panel hiding strategy, see TabPanels. */\n hideMode?: 'display-none' | 'visibility'\n className?: string\n}\n\n/**\n * The batteries-included layout: strip on top, keep-alive content below.\n * This is the recommended entry point — content state survives tab switches\n * by construction, because the panels host every loaded tab's tree like\n * Chrome keeps background pages alive.\n *\n * Use the composable pieces (TabStrip, TabPanels) directly only when you\n * need a custom layout, and keep content inside TabPanels unless you\n * specifically want remount-on-switch semantics.\n */\nexport function Tabs<T>({\n model,\n renderTab,\n children,\n onNewTab,\n onTabContextMenu,\n hideMode,\n className,\n}: TabsProps<T>) {\n return (\n <div className={['ctabs', className].filter(Boolean).join(' ')}>\n <TabStrip\n model={model}\n renderTab={renderTab}\n onNewTab={onNewTab}\n onTabContextMenu={onTabContextMenu}\n />\n <TabPanels model={model} hideMode={hideMode}>\n {children}\n </TabPanels>\n </div>\n )\n}\n"],"mappings":"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","names":["useRef","useRef","jsx","jsx","jsx","jsxs","useRef","jsx","jsxs"]}