chromium-tabs 0.1.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.js ADDED
@@ -0,0 +1,394 @@
1
+ import {
2
+ AddTabFlags,
3
+ CloseTabFlags,
4
+ ListSelectionModel,
5
+ NO_TAB,
6
+ TAB_GROUP_COLORS,
7
+ TabLifecycleManager,
8
+ TabStripModel
9
+ } from "./chunk-2DTGNBUT.js";
10
+
11
+ // src/react/use-tab-strip.ts
12
+ import { useMemo, useRef, useSyncExternalStore } from "react";
13
+ function useTabStripModel(init, options) {
14
+ const ref = useRef(null);
15
+ if (ref.current === null) {
16
+ ref.current = new TabStripModel(options);
17
+ init?.(ref.current);
18
+ }
19
+ return ref.current;
20
+ }
21
+ function useTabStrip(model) {
22
+ const store = useMemo(() => {
23
+ let version = 0;
24
+ let snapshotVersion = -1;
25
+ let snapshot = null;
26
+ return {
27
+ subscribe(onStoreChange) {
28
+ return model.addObserver({
29
+ onTabStripModelChanged: () => {
30
+ version++;
31
+ onStoreChange();
32
+ },
33
+ onTabPinnedStateChanged: () => {
34
+ version++;
35
+ onStoreChange();
36
+ },
37
+ onTabGroupedStateChanged: () => {
38
+ version++;
39
+ onStoreChange();
40
+ },
41
+ onTabGroupChanged: () => {
42
+ version++;
43
+ onStoreChange();
44
+ },
45
+ onTabChanged: () => {
46
+ version++;
47
+ onStoreChange();
48
+ },
49
+ onTabDiscardedStateChanged: () => {
50
+ version++;
51
+ onStoreChange();
52
+ }
53
+ });
54
+ },
55
+ getSnapshot() {
56
+ if (snapshot === null || snapshotVersion !== version) {
57
+ snapshot = {
58
+ tabs: model.getTabs(),
59
+ activeTab: model.activeTab,
60
+ activeIndex: model.activeIndex,
61
+ selectedIndices: model.selectionModel().selectedIndices(),
62
+ groups: model.getGroups()
63
+ };
64
+ snapshotVersion = version;
65
+ }
66
+ return snapshot;
67
+ }
68
+ };
69
+ }, [model]);
70
+ return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
71
+ }
72
+
73
+ // src/react/use-tab-drag.ts
74
+ import { useCallback, useRef as useRef2, useState } from "react";
75
+ var DRAG_THRESHOLD_PX = 4;
76
+ function useTabDrag(model, containerRef) {
77
+ const [draggingTabId, setDraggingTabId] = useState(null);
78
+ const drag = useRef2(null);
79
+ const onTabPointerDown = useCallback(
80
+ (event, tabId) => {
81
+ if (event.button !== 0) return;
82
+ drag.current = { tabId, startX: event.clientX, started: false };
83
+ const target = event.currentTarget;
84
+ target.setPointerCapture(event.pointerId);
85
+ const onMove = (e) => {
86
+ const state = drag.current;
87
+ if (!state) return;
88
+ if (!state.started) {
89
+ if (Math.abs(e.clientX - state.startX) < DRAG_THRESHOLD_PX) return;
90
+ state.started = true;
91
+ setDraggingTabId(state.tabId);
92
+ }
93
+ const container = containerRef.current;
94
+ if (!container) return;
95
+ const tab = model.getTabById(state.tabId);
96
+ if (!tab) return;
97
+ const currentIndex = model.indexOfTab(tab);
98
+ const elements = [...container.querySelectorAll("[data-tab-id]")];
99
+ let targetIndex = 0;
100
+ for (const el of elements) {
101
+ if (el.dataset["tabId"] === state.tabId) continue;
102
+ const rect = el.getBoundingClientRect();
103
+ if (e.clientX > rect.left + rect.width / 2) targetIndex++;
104
+ }
105
+ if (targetIndex !== currentIndex) {
106
+ model.moveTabTo(currentIndex, targetIndex);
107
+ }
108
+ };
109
+ const onUp = () => {
110
+ drag.current = null;
111
+ setDraggingTabId(null);
112
+ target.removeEventListener("pointermove", onMove);
113
+ target.removeEventListener("pointerup", onUp);
114
+ target.removeEventListener("pointercancel", onUp);
115
+ };
116
+ target.addEventListener("pointermove", onMove);
117
+ target.addEventListener("pointerup", onUp);
118
+ target.addEventListener("pointercancel", onUp);
119
+ },
120
+ [model, containerRef]
121
+ );
122
+ return { draggingTabId, onTabPointerDown };
123
+ }
124
+
125
+ // src/react/tab-panels.tsx
126
+ import { createContext, useContext } from "react";
127
+ import { jsx } from "react/jsx-runtime";
128
+ var TabVisibilityContext = createContext("visible");
129
+ function useTabVisibility() {
130
+ return useContext(TabVisibilityContext);
131
+ }
132
+ function TabPanels({ model, children, className, hideMode = "display-none" }) {
133
+ const snapshot = useTabStrip(model);
134
+ return /* @__PURE__ */ jsx("div", { className: ["ctabs-panels", className].filter(Boolean).join(" "), children: snapshot.tabs.map((tab) => {
135
+ if (tab.discarded) return null;
136
+ const visible = tab === snapshot.activeTab;
137
+ const style = hideMode === "display-none" ? { display: visible ? void 0 : "none" } : {
138
+ visibility: visible ? void 0 : "hidden",
139
+ position: visible ? void 0 : "absolute",
140
+ inset: visible ? void 0 : 0
141
+ };
142
+ return /* @__PURE__ */ jsx(
143
+ "div",
144
+ {
145
+ role: "tabpanel",
146
+ "data-tab-panel-id": tab.id,
147
+ className: "ctabs-panel",
148
+ style,
149
+ children: /* @__PURE__ */ jsx(TabVisibilityContext.Provider, { value: visible ? "visible" : "hidden", children: children(tab) })
150
+ },
151
+ tab.id
152
+ );
153
+ }) });
154
+ }
155
+
156
+ // src/react/tab-strip.tsx
157
+ import { useRef as useRef3 } from "react";
158
+
159
+ // src/react/group-header.tsx
160
+ import { jsx as jsx2 } from "react/jsx-runtime";
161
+ var GROUP_COLOR_VALUES = {
162
+ grey: "#5f6368",
163
+ blue: "#1a73e8",
164
+ red: "#d93025",
165
+ yellow: "#f9ab00",
166
+ green: "#188038",
167
+ pink: "#d01884",
168
+ purple: "#a142f4",
169
+ cyan: "#007b83",
170
+ orange: "#fa903e"
171
+ };
172
+ function GroupHeader({ group, onToggleCollapsed }) {
173
+ const color = GROUP_COLOR_VALUES[group.visualData.color] ?? GROUP_COLOR_VALUES["grey"];
174
+ return /* @__PURE__ */ jsx2(
175
+ "button",
176
+ {
177
+ type: "button",
178
+ className: [
179
+ "ctabs-group-header",
180
+ group.visualData.isCollapsed && "ctabs-group-header--collapsed"
181
+ ].filter(Boolean).join(" "),
182
+ style: { ["--ctabs-group-color"]: color },
183
+ onClick: () => onToggleCollapsed(group.id),
184
+ title: group.visualData.isCollapsed ? "Expand group" : "Collapse group",
185
+ children: group.visualData.title || "\xA0"
186
+ }
187
+ );
188
+ }
189
+
190
+ // src/react/tab-item.tsx
191
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
192
+ function TabItem({
193
+ tab,
194
+ index,
195
+ active,
196
+ selected,
197
+ dragging,
198
+ groupColor,
199
+ renderContent,
200
+ onPointerDown,
201
+ onActivate,
202
+ onClose,
203
+ onContextMenu
204
+ }) {
205
+ return /* @__PURE__ */ jsxs(
206
+ "div",
207
+ {
208
+ role: "tab",
209
+ "aria-selected": active,
210
+ "data-tab-id": tab.id,
211
+ className: [
212
+ "ctabs-tab",
213
+ active && "ctabs-tab--active",
214
+ selected && !active && "ctabs-tab--selected",
215
+ tab.pinned && "ctabs-tab--pinned",
216
+ tab.discarded && "ctabs-tab--discarded",
217
+ dragging && "ctabs-tab--dragging",
218
+ groupColor && "ctabs-tab--grouped"
219
+ ].filter(Boolean).join(" "),
220
+ style: groupColor ? { ["--ctabs-group-color"]: groupColor } : void 0,
221
+ onPointerDown: (e) => onPointerDown(e, tab.id),
222
+ onMouseDown: (e) => {
223
+ if (e.button === 1) {
224
+ e.preventDefault();
225
+ onClose(index);
226
+ }
227
+ },
228
+ onClick: (e) => onActivate(index, e),
229
+ onContextMenu: (e) => onContextMenu?.(index, e),
230
+ children: [
231
+ /* @__PURE__ */ jsx3("span", { className: "ctabs-tab__content", children: renderContent(tab) }),
232
+ !tab.pinned && /* @__PURE__ */ jsx3(
233
+ "button",
234
+ {
235
+ type: "button",
236
+ className: "ctabs-tab__close",
237
+ "aria-label": "Close tab",
238
+ onClick: (e) => {
239
+ e.stopPropagation();
240
+ onClose(index);
241
+ },
242
+ onPointerDown: (e) => e.stopPropagation(),
243
+ children: "\xD7"
244
+ }
245
+ )
246
+ ]
247
+ }
248
+ );
249
+ }
250
+
251
+ // src/react/tab-strip.tsx
252
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
253
+ function TabStrip({
254
+ model,
255
+ renderTab = (tab) => String(tab.data),
256
+ onNewTab,
257
+ onTabContextMenu,
258
+ className
259
+ }) {
260
+ const snapshot = useTabStrip(model);
261
+ const containerRef = useRef3(null);
262
+ const { draggingTabId, onTabPointerDown } = useTabDrag(model, containerRef);
263
+ const onActivate = (index, event) => {
264
+ if (event.shiftKey) {
265
+ model.extendSelectionTo(index);
266
+ } else if (event.metaKey || event.ctrlKey) {
267
+ if (model.isTabSelected(index)) model.deselectTabAt(index);
268
+ else model.selectTabAt(index);
269
+ } else {
270
+ model.activateTabAt(index, { userGesture: true });
271
+ }
272
+ };
273
+ const onKeyDown = (event) => {
274
+ const move = event.metaKey || event.ctrlKey;
275
+ if (event.key === "ArrowRight") {
276
+ move ? model.moveTabNext() : model.selectNextTab({ userGesture: true });
277
+ event.preventDefault();
278
+ } else if (event.key === "ArrowLeft") {
279
+ move ? model.moveTabPrevious() : model.selectPreviousTab({ userGesture: true });
280
+ event.preventDefault();
281
+ }
282
+ };
283
+ const selected = new Set(snapshot.selectedIndices);
284
+ const groupById = new Map(snapshot.groups.map((g) => [g.id, g]));
285
+ const items = [];
286
+ let previousGroup = null;
287
+ snapshot.tabs.forEach((tab, index) => {
288
+ if (tab.group !== null && tab.group !== previousGroup) {
289
+ const group = groupById.get(tab.group);
290
+ if (group) {
291
+ items.push(
292
+ /* @__PURE__ */ jsx4(
293
+ GroupHeader,
294
+ {
295
+ group,
296
+ onToggleCollapsed: (id) => model.setGroupCollapsed(id, !model.isGroupCollapsed(id))
297
+ },
298
+ `group-${group.id}`
299
+ )
300
+ );
301
+ }
302
+ }
303
+ previousGroup = tab.group;
304
+ if (tab.group !== null && model.isGroupCollapsed(tab.group)) return;
305
+ const groupColor = tab.group ? GROUP_COLOR_VALUES[groupById.get(tab.group)?.visualData.color ?? "grey"] ?? null : null;
306
+ items.push(
307
+ /* @__PURE__ */ jsx4(
308
+ TabItem,
309
+ {
310
+ tab,
311
+ index,
312
+ active: index === snapshot.activeIndex,
313
+ selected: selected.has(index),
314
+ dragging: tab.id === draggingTabId,
315
+ groupColor,
316
+ renderContent: renderTab,
317
+ onPointerDown: onTabPointerDown,
318
+ onActivate,
319
+ onClose: (i) => model.closeTabAt(i),
320
+ onContextMenu: onTabContextMenu
321
+ },
322
+ tab.id
323
+ )
324
+ );
325
+ });
326
+ return /* @__PURE__ */ jsxs2(
327
+ "div",
328
+ {
329
+ ref: containerRef,
330
+ role: "tablist",
331
+ tabIndex: 0,
332
+ className: ["ctabs-strip", className].filter(Boolean).join(" "),
333
+ onKeyDown,
334
+ children: [
335
+ items,
336
+ onNewTab && /* @__PURE__ */ jsx4(
337
+ "button",
338
+ {
339
+ type: "button",
340
+ className: "ctabs-new-tab",
341
+ "aria-label": "New tab",
342
+ onClick: onNewTab,
343
+ children: "+"
344
+ }
345
+ )
346
+ ]
347
+ }
348
+ );
349
+ }
350
+
351
+ // src/react/tabs.tsx
352
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
353
+ function Tabs({
354
+ model,
355
+ renderTab,
356
+ children,
357
+ onNewTab,
358
+ onTabContextMenu,
359
+ hideMode,
360
+ className
361
+ }) {
362
+ return /* @__PURE__ */ jsxs3("div", { className: ["ctabs", className].filter(Boolean).join(" "), children: [
363
+ /* @__PURE__ */ jsx5(
364
+ TabStrip,
365
+ {
366
+ model,
367
+ renderTab,
368
+ onNewTab,
369
+ onTabContextMenu
370
+ }
371
+ ),
372
+ /* @__PURE__ */ jsx5(TabPanels, { model, hideMode, children })
373
+ ] });
374
+ }
375
+ export {
376
+ AddTabFlags,
377
+ CloseTabFlags,
378
+ GROUP_COLOR_VALUES,
379
+ GroupHeader,
380
+ ListSelectionModel,
381
+ NO_TAB,
382
+ TAB_GROUP_COLORS,
383
+ TabItem,
384
+ TabLifecycleManager,
385
+ TabPanels,
386
+ TabStrip,
387
+ TabStripModel,
388
+ Tabs,
389
+ useTabDrag,
390
+ useTabStrip,
391
+ useTabStripModel,
392
+ useTabVisibility
393
+ };
394
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,165 @@
1
+ /* Chrome-inspired tab strip. Override the variables to theme. */
2
+ .ctabs-strip {
3
+ --ctabs-bg: #dee1e6;
4
+ --ctabs-tab-bg: transparent;
5
+ --ctabs-tab-active-bg: #ffffff;
6
+ --ctabs-tab-hover-bg: rgba(255, 255, 255, 0.55);
7
+ --ctabs-text: #3c4043;
8
+ --ctabs-radius: 8px;
9
+ --ctabs-height: 36px;
10
+
11
+ display: flex;
12
+ align-items: flex-end;
13
+ gap: 1px;
14
+ padding: 6px 8px 0;
15
+ background: var(--ctabs-bg);
16
+ overflow-x: auto;
17
+ scrollbar-width: none;
18
+ user-select: none;
19
+ -webkit-user-select: none;
20
+ }
21
+
22
+ .ctabs-strip::-webkit-scrollbar {
23
+ display: none;
24
+ }
25
+
26
+ .ctabs-tab {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 6px;
30
+ height: var(--ctabs-height);
31
+ min-width: 0;
32
+ max-width: 240px;
33
+ flex: 1 1 160px;
34
+ padding: 0 8px 0 12px;
35
+ border-radius: var(--ctabs-radius) var(--ctabs-radius) 0 0;
36
+ background: var(--ctabs-tab-bg);
37
+ color: var(--ctabs-text);
38
+ font: 12px/1.2 system-ui, sans-serif;
39
+ cursor: default;
40
+ position: relative;
41
+ touch-action: none;
42
+ }
43
+
44
+ .ctabs-tab:hover {
45
+ background: var(--ctabs-tab-hover-bg);
46
+ }
47
+
48
+ .ctabs-tab--active {
49
+ background: var(--ctabs-tab-active-bg);
50
+ }
51
+
52
+ .ctabs-tab--selected {
53
+ background: rgba(255, 255, 255, 0.75);
54
+ }
55
+
56
+ .ctabs-tab--pinned {
57
+ flex: 0 0 auto;
58
+ max-width: var(--ctabs-height);
59
+ padding: 0 10px;
60
+ }
61
+
62
+ /* Discarded tabs stay in the strip but their content is unloaded, shown
63
+ faded like Chrome's Memory Saver. */
64
+ .ctabs-tab--discarded .ctabs-tab__content {
65
+ opacity: 0.55;
66
+ }
67
+
68
+ .ctabs-tab--dragging {
69
+ opacity: 0.85;
70
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
71
+ z-index: 1;
72
+ }
73
+
74
+ .ctabs-tab--grouped::before {
75
+ content: '';
76
+ position: absolute;
77
+ left: 0;
78
+ right: 0;
79
+ bottom: 0;
80
+ height: 2px;
81
+ background: var(--ctabs-group-color);
82
+ }
83
+
84
+ .ctabs-tab__content {
85
+ flex: 1;
86
+ min-width: 0;
87
+ overflow: hidden;
88
+ text-overflow: ellipsis;
89
+ white-space: nowrap;
90
+ }
91
+
92
+ .ctabs-tab__close {
93
+ flex: 0 0 auto;
94
+ width: 16px;
95
+ height: 16px;
96
+ border: 0;
97
+ border-radius: 50%;
98
+ background: transparent;
99
+ color: inherit;
100
+ font-size: 12px;
101
+ line-height: 1;
102
+ cursor: pointer;
103
+ display: grid;
104
+ place-items: center;
105
+ }
106
+
107
+ .ctabs-tab__close:hover {
108
+ background: rgba(0, 0, 0, 0.12);
109
+ }
110
+
111
+ .ctabs-group-header {
112
+ flex: 0 0 auto;
113
+ align-self: center;
114
+ margin: 0 2px;
115
+ padding: 2px 8px;
116
+ border: 0;
117
+ border-radius: 6px;
118
+ background: var(--ctabs-group-color);
119
+ color: #fff;
120
+ font: 600 11px/1.4 system-ui, sans-serif;
121
+ min-width: 14px;
122
+ min-height: 14px;
123
+ cursor: pointer;
124
+ }
125
+
126
+ .ctabs-group-header--collapsed {
127
+ opacity: 0.9;
128
+ }
129
+
130
+ .ctabs-new-tab {
131
+ flex: 0 0 auto;
132
+ align-self: center;
133
+ width: 26px;
134
+ height: 26px;
135
+ margin-left: 6px;
136
+ border: 0;
137
+ border-radius: 50%;
138
+ background: transparent;
139
+ color: var(--ctabs-text);
140
+ font-size: 16px;
141
+ cursor: pointer;
142
+ display: grid;
143
+ place-items: center;
144
+ }
145
+
146
+ .ctabs-new-tab:hover {
147
+ background: rgba(0, 0, 0, 0.08);
148
+ }
149
+
150
+ .ctabs {
151
+ display: flex;
152
+ flex-direction: column;
153
+ height: 100%;
154
+ min-height: 0;
155
+ }
156
+
157
+ .ctabs-panels {
158
+ position: relative;
159
+ flex: 1;
160
+ min-height: 0;
161
+ }
162
+
163
+ .ctabs-panel {
164
+ height: 100%;
165
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "chromium-tabs",
3
+ "version": "0.1.0",
4
+ "description": "Chrome's TabStripModel, ported to TypeScript with React bindings. Pinning, tab groups, opener-aware activation, multi-select, drag-reorder.",
5
+ "license": "BSD-3-Clause",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/willwearing/chromium-tabs.git"
9
+ },
10
+ "homepage": "https://github.com/willwearing/chromium-tabs#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/willwearing/chromium-tabs/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./core": {
25
+ "types": "./dist/core/index.d.ts",
26
+ "import": "./dist/core/index.js",
27
+ "require": "./dist/core/index.cjs"
28
+ },
29
+ "./styles.css": "./dist/styles.css"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "sideEffects": [
35
+ "*.css"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup && cp src/react/styles.css dist/styles.css",
39
+ "dev": "tsup --watch",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint": "eslint src"
44
+ },
45
+ "peerDependencies": {
46
+ "react": ">=18",
47
+ "react-dom": ">=18"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "react": {
51
+ "optional": true
52
+ },
53
+ "react-dom": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@testing-library/react": "^16.1.0",
59
+ "@types/react": "^19.0.0",
60
+ "@types/react-dom": "^19.0.0",
61
+ "jsdom": "^25.0.1",
62
+ "react": "^19.0.0",
63
+ "react-dom": "^19.0.0",
64
+ "tsup": "^8.3.5",
65
+ "typescript": "^5.7.2",
66
+ "vitest": "^2.1.8"
67
+ },
68
+ "keywords": [
69
+ "tabs",
70
+ "tab-strip",
71
+ "chromium",
72
+ "chrome",
73
+ "react",
74
+ "tab-groups",
75
+ "headless"
76
+ ]
77
+ }