@nikala-ui/hooks 0.10.1 → 0.11.0-nightly.b01fa01

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/hooks",
3
- "version": "0.10.1",
3
+ "version": "0.11.0-nightly.b01fa01",
4
4
  "description": "Reactive SolidJS primitives and primitives for Nikala UI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,272 @@
1
+ import { createSignal, onMount, type Accessor } from "solid-js";
2
+ import { isTauriEnvironment } from "./create-tauri-window";
3
+
4
+ export type UpdaterStatus =
5
+ | "idle"
6
+ | "checking"
7
+ | "available"
8
+ | "up-to-date"
9
+ | "downloading"
10
+ | "downloaded"
11
+ | "error";
12
+
13
+ export interface UpdateManifestInfo {
14
+ version: string;
15
+ currentVersion?: string;
16
+ date?: string;
17
+ body?: string;
18
+ }
19
+
20
+ export interface UpdateProgressInfo {
21
+ downloaded: number;
22
+ total: number;
23
+ percentage: number;
24
+ }
25
+
26
+ export interface CreateAppUpdaterOptions {
27
+ /** Automatically check for updates on mount. Defaults to false. */
28
+ autoCheck?: boolean;
29
+ /** Current application version fallback if not detected. */
30
+ currentVersion?: string;
31
+ /** Callback fired when an update is available. */
32
+ onUpdateAvailable?: (info: UpdateManifestInfo) => void;
33
+ /** Callback fired when download finishes. */
34
+ onDownloadFinished?: () => void;
35
+ /** Callback fired on update check or download error. */
36
+ onError?: (error: Error | string) => void;
37
+ }
38
+
39
+ export interface CreateAppUpdaterReturn {
40
+ /** Current lifecycle status of the updater. */
41
+ status: Accessor<UpdaterStatus>;
42
+ /** Update metadata (new version, release notes, release date). */
43
+ updateInfo: Accessor<UpdateManifestInfo | null>;
44
+ /** Download progress (bytes downloaded, total bytes, percentage). */
45
+ progress: Accessor<UpdateProgressInfo>;
46
+ /** Error message if an operation failed. */
47
+ error: Accessor<string | null>;
48
+ /** Whether an update is currently being checked or downloaded. */
49
+ isLoading: Accessor<boolean>;
50
+ /** Check update endpoint for a new release. */
51
+ checkForUpdates: () => Promise<boolean>;
52
+ /** Download and install the available update. */
53
+ downloadAndInstall: () => Promise<void>;
54
+ /** Restart application to apply the downloaded update. */
55
+ relaunch: () => Promise<void>;
56
+ /** Dismiss current update prompt or clear error state. */
57
+ dismiss: () => void;
58
+ /** Simulate an update cycle in web / playground preview environments. */
59
+ simulateUpdate: (mockInfo?: Partial<UpdateManifestInfo>) => void;
60
+ }
61
+
62
+ /**
63
+ * SolidJS reactive primitive for controlling and observing Tauri v2 application updates.
64
+ * Integrates with @tauri-apps/plugin-updater with full web simulation support.
65
+ */
66
+ export function createAppUpdater(
67
+ options: CreateAppUpdaterOptions = {}
68
+ ): CreateAppUpdaterReturn {
69
+ const [status, setStatus] = createSignal<UpdaterStatus>("idle");
70
+ const [updateInfo, setUpdateInfo] = createSignal<UpdateManifestInfo | null>(null);
71
+ const [progress, setProgress] = createSignal<UpdateProgressInfo>({
72
+ downloaded: 0,
73
+ total: 0,
74
+ percentage: 0,
75
+ });
76
+ const [error, setError] = createSignal<string | null>(null);
77
+
78
+ let activeTauriUpdate: any = null;
79
+ let simulatedTimer: any = null;
80
+
81
+ const getTauriUpdaterPlugin = async () => {
82
+ if (typeof window === "undefined") return null;
83
+ try {
84
+ if ((window as any).__TAURI__?.updater) {
85
+ return (window as any).__TAURI__.updater;
86
+ }
87
+ const moduleName = "@tauri-apps/plugin-updater";
88
+ // @ts-ignore - Optional runtime dependency in Tauri apps
89
+ const plugin = await import(/* @vite-ignore */ moduleName).catch(() => null);
90
+ return plugin;
91
+ } catch {
92
+ return null;
93
+ }
94
+ };
95
+
96
+ const getTauriProcessPlugin = async () => {
97
+ if (typeof window === "undefined") return null;
98
+ try {
99
+ if ((window as any).__TAURI__?.process) {
100
+ return (window as any).__TAURI__.process;
101
+ }
102
+ const moduleName = "@tauri-apps/plugin-process";
103
+ // @ts-ignore - Optional runtime dependency in Tauri apps
104
+ const plugin = await import(/* @vite-ignore */ moduleName).catch(() => null);
105
+ return plugin;
106
+ } catch {
107
+ return null;
108
+ }
109
+ };
110
+
111
+ const checkForUpdates = async (): Promise<boolean> => {
112
+ setStatus("checking");
113
+ setError(null);
114
+
115
+ if (isTauriEnvironment()) {
116
+ try {
117
+ const updaterPlugin = await getTauriUpdaterPlugin();
118
+ if (updaterPlugin?.check) {
119
+ const update = await updaterPlugin.check();
120
+ if (update?.available) {
121
+ activeTauriUpdate = update;
122
+ const info: UpdateManifestInfo = {
123
+ version: update.version,
124
+ currentVersion: update.currentVersion || options.currentVersion || "v1.0.0",
125
+ date: update.date,
126
+ body: update.body || "Bug fixes and performance enhancements.",
127
+ };
128
+ setUpdateInfo(info);
129
+ setStatus("available");
130
+ options.onUpdateAvailable?.(info);
131
+ return true;
132
+ } else {
133
+ setStatus("up-to-date");
134
+ return false;
135
+ }
136
+ }
137
+ } catch (err: any) {
138
+ const errMsg = err?.message || String(err);
139
+ setError(errMsg);
140
+ setStatus("error");
141
+ options.onError?.(errMsg);
142
+ return false;
143
+ }
144
+ }
145
+
146
+ // In web preview / non-tauri mode, simulate checking
147
+ await new Promise((resolve) => setTimeout(resolve, 800));
148
+ setStatus("up-to-date");
149
+ return false;
150
+ };
151
+
152
+ const downloadAndInstall = async (): Promise<void> => {
153
+ if (status() !== "available" && status() !== "error") return;
154
+ setStatus("downloading");
155
+ setError(null);
156
+ setProgress({ downloaded: 0, total: 100, percentage: 0 });
157
+
158
+ if (isTauriEnvironment() && activeTauriUpdate?.downloadAndInstall) {
159
+ try {
160
+ let downloadedBytes = 0;
161
+ let contentLength = 0;
162
+
163
+ await activeTauriUpdate.downloadAndInstall((event: any) => {
164
+ if (event.event === "Started") {
165
+ contentLength = event.data?.contentLength || 100;
166
+ } else if (event.event === "Progress") {
167
+ downloadedBytes += event.data?.chunkLength || 0;
168
+ const pct = contentLength > 0 ? Math.min(100, Math.round((downloadedBytes / contentLength) * 100)) : 50;
169
+ setProgress({
170
+ downloaded: downloadedBytes,
171
+ total: contentLength,
172
+ percentage: pct,
173
+ });
174
+ } else if (event.event === "Finished") {
175
+ setProgress({
176
+ downloaded: contentLength,
177
+ total: contentLength,
178
+ percentage: 100,
179
+ });
180
+ }
181
+ });
182
+
183
+ setStatus("downloaded");
184
+ options.onDownloadFinished?.();
185
+ return;
186
+ } catch (err: any) {
187
+ const errMsg = err?.message || String(err);
188
+ setError(errMsg);
189
+ setStatus("error");
190
+ options.onError?.(errMsg);
191
+ return;
192
+ }
193
+ }
194
+
195
+ // Web simulation mode with smooth progress step
196
+ let currentPct = 0;
197
+ simulatedTimer = setInterval(() => {
198
+ currentPct += 15;
199
+ if (currentPct >= 100) {
200
+ clearInterval(simulatedTimer);
201
+ setProgress({ downloaded: 45.8 * 1024 * 1024, total: 45.8 * 1024 * 1024, percentage: 100 });
202
+ setStatus("downloaded");
203
+ options.onDownloadFinished?.();
204
+ } else {
205
+ const downloaded = (45.8 * 1024 * 1024 * currentPct) / 100;
206
+ setProgress({ downloaded, total: 45.8 * 1024 * 1024, percentage: currentPct });
207
+ }
208
+ }, 250);
209
+ };
210
+
211
+ const relaunch = async (): Promise<void> => {
212
+ if (isTauriEnvironment()) {
213
+ try {
214
+ const processPlugin = await getTauriProcessPlugin();
215
+ if (processPlugin?.relaunch) {
216
+ await processPlugin.relaunch();
217
+ return;
218
+ }
219
+ } catch {
220
+ // Fallback
221
+ }
222
+ }
223
+
224
+ if (typeof window !== "undefined") {
225
+ window.location.reload();
226
+ }
227
+ };
228
+
229
+ const dismiss = (): void => {
230
+ if (simulatedTimer) clearInterval(simulatedTimer);
231
+ setStatus("idle");
232
+ setError(null);
233
+ };
234
+
235
+ const simulateUpdate = (mockInfo?: Partial<UpdateManifestInfo>): void => {
236
+ if (simulatedTimer) clearInterval(simulatedTimer);
237
+ const info: UpdateManifestInfo = {
238
+ version: mockInfo?.version || "v1.2.0",
239
+ currentVersion: mockInfo?.currentVersion || options.currentVersion || "v1.0.0",
240
+ date: mockInfo?.date || new Date().toISOString().split("T")[0],
241
+ body:
242
+ mockInfo?.body ||
243
+ "### What's New in v1.2.0\n- Added native Window Titlebar tabs\n- Enhanced Tauri v2 capability security\n- Optimized SolidJS signal reactivity\n- Fixed titlebar drag region jitter on Linux",
244
+ };
245
+ setUpdateInfo(info);
246
+ setStatus("available");
247
+ setError(null);
248
+ setProgress({ downloaded: 0, total: 100, percentage: 0 });
249
+ options.onUpdateAvailable?.(info);
250
+ };
251
+
252
+ onMount(() => {
253
+ if (options.autoCheck) {
254
+ checkForUpdates();
255
+ }
256
+ });
257
+
258
+ const isLoading = () => status() === "checking" || status() === "downloading";
259
+
260
+ return {
261
+ status,
262
+ updateInfo,
263
+ progress,
264
+ error,
265
+ isLoading,
266
+ checkForUpdates,
267
+ downloadAndInstall,
268
+ relaunch,
269
+ dismiss,
270
+ simulateUpdate,
271
+ };
272
+ }
@@ -0,0 +1,113 @@
1
+ import { createSignal, createEffect, onCleanup, onMount, type Accessor } from "solid-js";
2
+
3
+ export interface CreateChatScrollOptions {
4
+ /** Target scrollable container element or accessor */
5
+ target: HTMLElement | Accessor<HTMLElement | undefined>;
6
+ /** Dependency accessor (e.g. messages length or content signal) that triggers auto-scroll when changed */
7
+ trigger?: Accessor<any>;
8
+ /** Threshold in pixels from bottom to consider the container "at bottom". Defaults to 40. */
9
+ threshold?: number;
10
+ /** Whether auto-scroll is enabled. Defaults to true. */
11
+ enabled?: boolean | Accessor<boolean>;
12
+ /** Scroll behavior: "smooth" or "auto". Defaults to "smooth". */
13
+ behavior?: ScrollBehavior;
14
+ }
15
+
16
+ export interface CreateChatScrollReturn {
17
+ /** Accessor indicating whether the container is currently scrolled to the bottom */
18
+ isAtBottom: Accessor<boolean>;
19
+ /** Accessor indicating whether user has manually scrolled up away from bottom */
20
+ isScrolledUp: Accessor<boolean>;
21
+ /** Programmatically scroll container directly to the bottom */
22
+ scrollToBottom: (options?: { smooth?: boolean }) => void;
23
+ }
24
+
25
+ /**
26
+ * SolidJS reactive primitive for chat and streaming message auto-scrolling with user scroll detection.
27
+ *
28
+ * @param options Chat scroll configuration options.
29
+ */
30
+ export function createChatScroll(options: CreateChatScrollOptions): CreateChatScrollReturn {
31
+ const [isAtBottom, setIsAtBottom] = createSignal<boolean>(true);
32
+ const isScrolledUp = () => !isAtBottom();
33
+
34
+ const getElement = (): HTMLElement | undefined => {
35
+ if (typeof options.target === "function") {
36
+ return (options.target as Accessor<HTMLElement | undefined>)();
37
+ }
38
+ return options.target;
39
+ };
40
+
41
+ const isEnabled = () => {
42
+ if (typeof options.enabled === "function") {
43
+ return (options.enabled as Accessor<boolean>)();
44
+ }
45
+ return options.enabled ?? true;
46
+ };
47
+
48
+ const threshold = options.threshold ?? 40;
49
+
50
+ const checkIfAtBottom = () => {
51
+ const el = getElement();
52
+ if (!el) return true;
53
+ const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
54
+ return distanceToBottom <= threshold;
55
+ };
56
+
57
+ const scrollToBottom = (opts?: { smooth?: boolean }) => {
58
+ const el = getElement();
59
+ if (!el) return;
60
+
61
+ const useSmooth = opts?.smooth ?? (options.behavior === "smooth" || options.behavior === undefined);
62
+
63
+ el.scrollTo({
64
+ top: el.scrollHeight,
65
+ behavior: useSmooth ? "smooth" : "auto",
66
+ });
67
+ setIsAtBottom(true);
68
+ };
69
+
70
+ const handleScroll = () => {
71
+ const atBottom = checkIfAtBottom();
72
+ setIsAtBottom(atBottom);
73
+ };
74
+
75
+ onMount(() => {
76
+ if (typeof window === "undefined") return;
77
+
78
+ const el = getElement();
79
+ if (el) {
80
+ el.addEventListener("scroll", handleScroll, { passive: true });
81
+ setIsAtBottom(checkIfAtBottom());
82
+ }
83
+ });
84
+
85
+ onCleanup(() => {
86
+ if (typeof window === "undefined") return;
87
+ const el = getElement();
88
+ if (el) {
89
+ el.removeEventListener("scroll", handleScroll);
90
+ }
91
+ });
92
+
93
+ // Watch trigger dependencies (e.g. messages length or stream tokens)
94
+ if (options.trigger) {
95
+ createEffect(() => {
96
+ // Track trigger dependency
97
+ options.trigger!();
98
+
99
+ if (isEnabled() && isAtBottom()) {
100
+ // Run after microtask/DOM paint
101
+ setTimeout(() => {
102
+ scrollToBottom({ smooth: true });
103
+ }, 10);
104
+ }
105
+ });
106
+ }
107
+
108
+ return {
109
+ isAtBottom,
110
+ isScrolledUp,
111
+ scrollToBottom,
112
+ };
113
+ }
@@ -0,0 +1,267 @@
1
+ import {
2
+ createSignal,
3
+ createMemo,
4
+ onMount,
5
+ onCleanup,
6
+ type Accessor,
7
+ } from "solid-js";
8
+
9
+ export interface TabItem<T = any> {
10
+ id: string;
11
+ title: string;
12
+ icon?: any;
13
+ isDirty?: boolean;
14
+ isPinned?: boolean;
15
+ closable?: boolean;
16
+ data?: T;
17
+ }
18
+
19
+ export interface CreateDocumentTabsOptions<T = any> {
20
+ /** Initial array of tab items. */
21
+ initialTabs?: TabItem<T>[];
22
+ /** Default active tab id. Defaults to first tab if available. */
23
+ defaultActiveId?: string;
24
+ /** Whether to enable keyboard shortcuts (Ctrl/Cmd+W to close active, Ctrl/Cmd+T for new tab). */
25
+ enableKeybindings?: boolean;
26
+ /** Callback triggered when the active tab changes. */
27
+ onTabChange?: (tab: TabItem<T> | undefined) => void;
28
+ /** Callback triggered before or when a tab closes. Return false to prevent closing. */
29
+ onTabClose?: (tab: TabItem<T>) => boolean | void;
30
+ /** Callback triggered when a new tab is created. */
31
+ onTabAdd?: (tab: TabItem<T>) => void;
32
+ }
33
+
34
+ export interface CreateDocumentTabsReturn<T = any> {
35
+ /** All tabs list. */
36
+ tabs: Accessor<TabItem<T>[]>;
37
+ /** Currently active tab ID. */
38
+ activeTabId: Accessor<string>;
39
+ /** Currently active tab object. */
40
+ activeTab: Accessor<TabItem<T> | undefined>;
41
+ /** Pinned tabs only. */
42
+ pinnedTabs: Accessor<TabItem<T>[]>;
43
+ /** Standard unpinned tabs. */
44
+ unpinnedTabs: Accessor<TabItem<T>[]>;
45
+ /** Whether any open tab has unsaved changes (isDirty). */
46
+ hasDirtyTabs: Accessor<boolean>;
47
+ /** Count of total open tabs. */
48
+ count: Accessor<number>;
49
+ /** Add a new tab to the list. */
50
+ addTab: (tab: TabItem<T>, activate?: boolean) => void;
51
+ /** Close a tab by ID with auto-switching to adjacent tab. */
52
+ closeTab: (id: string) => boolean;
53
+ /** Set the active tab. */
54
+ setActiveTab: (id: string) => void;
55
+ /** Mark or unmark a tab as dirty (unsaved changes). */
56
+ markDirty: (id: string, isDirty?: boolean) => void;
57
+ /** Toggle pinned state of a tab. */
58
+ togglePin: (id: string) => void;
59
+ /** Close all tabs except the specified one. */
60
+ closeOthers: (id: string) => void;
61
+ /** Close all open unpinned tabs. */
62
+ closeAll: (includePinned?: boolean) => void;
63
+ /** Switch to the next tab in sequence. */
64
+ nextTab: () => void;
65
+ /** Switch to the previous tab in sequence. */
66
+ prevTab: () => void;
67
+ /** Reorder tabs from one position to another. */
68
+ reorderTabs: (fromIndex: number, toIndex: number) => void;
69
+ }
70
+
71
+ function findLastPinnedIndex<T>(items: TabItem<T>[]): number {
72
+ for (let i = items.length - 1; i >= 0; i--) {
73
+ if (items[i].isPinned) return i;
74
+ }
75
+ return -1;
76
+ }
77
+
78
+ /**
79
+ * SolidJS reactive primitive for managing multi-document tabs, editor buffers, and browser tab states.
80
+ */
81
+ export function createDocumentTabs<T = any>(
82
+ options: CreateDocumentTabsOptions<T> = {}
83
+ ): CreateDocumentTabsReturn<T> {
84
+ const initial = options.initialTabs || [];
85
+ const [tabs, setTabs] = createSignal<TabItem<T>[]>(initial);
86
+ const [activeTabId, setActiveTabId] = createSignal<string>(
87
+ options.defaultActiveId || (initial.length > 0 ? initial[0].id : "")
88
+ );
89
+
90
+ const activeTab = createMemo(() => tabs().find((t) => t.id === activeTabId()));
91
+ const pinnedTabs = createMemo(() => tabs().filter((t) => t.isPinned));
92
+ const unpinnedTabs = createMemo(() => tabs().filter((t) => !t.isPinned));
93
+ const hasDirtyTabs = createMemo(() => tabs().some((t) => t.isDirty));
94
+ const count = createMemo(() => tabs().length);
95
+
96
+ const setActive = (id: string) => {
97
+ const target = tabs().find((t) => t.id === id);
98
+ if (target) {
99
+ setActiveTabId(id);
100
+ options.onTabChange?.(target);
101
+ }
102
+ };
103
+
104
+ const addTab = (tab: TabItem<T>, activate = true) => {
105
+ const existing = tabs().find((t) => t.id === tab.id);
106
+ if (existing) {
107
+ if (activate) setActive(tab.id);
108
+ return;
109
+ }
110
+
111
+ setTabs((prev) => {
112
+ if (tab.isPinned) {
113
+ const lastPinnedIdx = findLastPinnedIndex(prev);
114
+ if (lastPinnedIdx === -1) return [tab, ...prev];
115
+ const next = [...prev];
116
+ next.splice(lastPinnedIdx + 1, 0, tab);
117
+ return next;
118
+ }
119
+ return [...prev, tab];
120
+ });
121
+
122
+ if (activate) {
123
+ setActiveTabId(tab.id);
124
+ options.onTabChange?.(tab);
125
+ }
126
+ options.onTabAdd?.(tab);
127
+ };
128
+
129
+ const closeTab = (id: string): boolean => {
130
+ const currentTabs = tabs();
131
+ const tabToClose = currentTabs.find((t) => t.id === id);
132
+ if (!tabToClose) return false;
133
+
134
+ if (options.onTabClose && options.onTabClose(tabToClose) === false) {
135
+ return false;
136
+ }
137
+
138
+ const closeIdx = currentTabs.findIndex((t) => t.id === id);
139
+ const nextTabs = currentTabs.filter((t) => t.id !== id);
140
+ setTabs(nextTabs);
141
+
142
+ if (activeTabId() === id) {
143
+ if (nextTabs.length === 0) {
144
+ setActiveTabId("");
145
+ options.onTabChange?.(undefined);
146
+ } else {
147
+ const nextIdx = Math.min(closeIdx, nextTabs.length - 1);
148
+ const nextActive = nextTabs[nextIdx];
149
+ setActiveTabId(nextActive.id);
150
+ options.onTabChange?.(nextActive);
151
+ }
152
+ }
153
+ return true;
154
+ };
155
+
156
+ const markDirty = (id: string, isDirty = true) => {
157
+ setTabs((prev) =>
158
+ prev.map((t) => (t.id === id ? { ...t, isDirty } : t))
159
+ );
160
+ };
161
+
162
+ const togglePin = (id: string) => {
163
+ setTabs((prev) => {
164
+ const target = prev.find((t) => t.id === id);
165
+ if (!target) return prev;
166
+ const updated = { ...target, isPinned: !target.isPinned };
167
+ const remaining = prev.filter((t) => t.id !== id);
168
+
169
+ if (updated.isPinned) {
170
+ const lastPinnedIdx = findLastPinnedIndex(remaining);
171
+ if (lastPinnedIdx === -1) return [updated, ...remaining];
172
+ remaining.splice(lastPinnedIdx + 1, 0, updated);
173
+ return remaining;
174
+ }
175
+ return [...remaining, updated];
176
+ });
177
+ };
178
+
179
+ const closeOthers = (id: string) => {
180
+ setTabs((prev) => prev.filter((t) => t.id === id || t.isPinned));
181
+ setActive(id);
182
+ };
183
+
184
+ const closeAll = (includePinned = false) => {
185
+ if (includePinned) {
186
+ setTabs([]);
187
+ setActiveTabId("");
188
+ options.onTabChange?.(undefined);
189
+ } else {
190
+ const pinned = tabs().filter((t) => t.isPinned);
191
+ setTabs(pinned);
192
+ if (pinned.length > 0) {
193
+ setActiveTabId(pinned[0].id);
194
+ options.onTabChange?.(pinned[0]);
195
+ } else {
196
+ setActiveTabId("");
197
+ options.onTabChange?.(undefined);
198
+ }
199
+ }
200
+ };
201
+
202
+ const nextTab = () => {
203
+ const list = tabs();
204
+ if (list.length <= 1) return;
205
+ const currentIdx = list.findIndex((t) => t.id === activeTabId());
206
+ const nextIdx = (currentIdx + 1) % list.length;
207
+ setActive(list[nextIdx].id);
208
+ };
209
+
210
+ const prevTab = () => {
211
+ const list = tabs();
212
+ if (list.length <= 1) return;
213
+ const currentIdx = list.findIndex((t) => t.id === activeTabId());
214
+ const prevIdx = (currentIdx - 1 + list.length) % list.length;
215
+ setActive(list[prevIdx].id);
216
+ };
217
+
218
+ const reorderTabs = (fromIndex: number, toIndex: number) => {
219
+ setTabs((prev) => {
220
+ if (fromIndex < 0 || fromIndex >= prev.length || toIndex < 0 || toIndex >= prev.length) {
221
+ return prev;
222
+ }
223
+ const clone = [...prev];
224
+ const [moved] = clone.splice(fromIndex, 1);
225
+ clone.splice(toIndex, 0, moved);
226
+ return clone;
227
+ });
228
+ };
229
+
230
+ onMount(() => {
231
+ if (!options.enableKeybindings || typeof window === "undefined") return;
232
+
233
+ const handleKeyDown = (e: KeyboardEvent) => {
234
+ const isCmdOrCtrl = e.metaKey || e.ctrlKey;
235
+ if (isCmdOrCtrl && (e.key === "w" || e.key === "W")) {
236
+ const active = activeTabId();
237
+ if (active) {
238
+ e.preventDefault();
239
+ closeTab(active);
240
+ }
241
+ }
242
+ };
243
+
244
+ window.addEventListener("keydown", handleKeyDown);
245
+ onCleanup(() => window.removeEventListener("keydown", handleKeyDown));
246
+ });
247
+
248
+ return {
249
+ tabs,
250
+ activeTabId,
251
+ activeTab,
252
+ pinnedTabs,
253
+ unpinnedTabs,
254
+ hasDirtyTabs,
255
+ count,
256
+ addTab,
257
+ closeTab,
258
+ setActiveTab: setActive,
259
+ markDirty,
260
+ togglePin,
261
+ closeOthers,
262
+ closeAll,
263
+ nextTab,
264
+ prevTab,
265
+ reorderTabs,
266
+ };
267
+ }