react-multi-tab 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # react-multi-tab
2
+
3
+ A headless, accessible, router-agnostic, and fully type-safe multi-tab component library for React.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/react-multi-tab.svg)](https://npmjs.org/package/react-multi-tab)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - **Agnostic & Pluggable:** Works independently of any bundler or router. Includes adapters for memory, URL Search Params, and `react-router-dom`.
11
+ - **Headless & Accessible:** Follows the WAI-ARIA Tabs pattern. Complete keyboard navigation (`Arrow keys`, `Home`, `End`) out of the box. You control the styling.
12
+ - **TypeScript Generics:** Fully type-safe context, components, and hooks. Never use `any` again.
13
+ - **State Preservation:** Keeps tab content mounted when inactive (via `hidden` attribute) to preserve form states and scroll positions.
14
+
15
+ ## 🚀 Performance & Developer Experience (DX)
16
+
17
+ We have built the core state-management of `react-multi-tab` to meet the highest performance and Developer Experience (DX) standards.
18
+
19
+ ### 1. `useSyncExternalStore` (The Performance Boost)
20
+ We stripped the large object out of React Context and replaced it with a custom Vanilla JS store (`createMultiTabStore`).
21
+ By utilizing React 18's `useSyncExternalStore`, components like `useTabData` now strictly subscribe to only the data they care about.
22
+
23
+ > **Why it matters:** Typing in an input field inside a specific tab will **NO LONGER** trigger a re-render across the entire Tab system. Only the component that called `useTabData` for that specific tab will update!
24
+
25
+ ### 2. `TabInstanceContext` (The DX Boost)
26
+ Developers no longer need to manually pass down the cryptic `instanceId` to every page component.
27
+ Because `<TabPanels />` implicitly wraps your components with `<TabContent>`, it invisibly provides the context to all descendants.
28
+
29
+ ```tsx
30
+ // Inside any page (e.g., A-Page.tsx)
31
+ const [data, setData] = useTabData<APageData>(); // Boom! Magic.
32
+ ```
33
+ > `useTabData` automatically finds its own tab's context. If no context is found, it safely falls back to the globally active tab.
34
+
35
+ ### 3. Built for Redux/Zustand Users
36
+ For teams that prefer keeping form data in a global Redux slice instead of our internal data store, we introduced the `useTabInstanceId()` hook.
37
+ ```tsx
38
+ import { useTabInstanceId } from 'react-multi-tab';
39
+
40
+ function MyReduxPage() {
41
+ const tabId = useTabInstanceId(); // Seamlessly gets the ID!
42
+
43
+ const handleChange = () => {
44
+ dispatch(updateFormSlice({ tabId, data: '...' }));
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### 4. Smart Tab History
50
+ When a tab is closed, the library remembers the exact history of your active tabs and gracefully falls back to the **previously active** tab instead of abruptly jumping to the end of the list. Exactly like VS Code!
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ npm install react-multi-tab
56
+ # or
57
+ yarn add react-multi-tab
58
+ ```
59
+
60
+ ## Basic Usage
61
+
62
+ Here is a full example showing how to build a tabbed layout using the vanilla URL `searchParamsAdapter` and the new explicit page registry.
63
+
64
+ ### 1. Define Your Pages & Registry
65
+
66
+ Create your page components and register them explicitly.
67
+
68
+ ```tsx
69
+ // src/registry.ts
70
+ import { createPageRegistry, useTabData } from 'react-multi-tab';
71
+
72
+ interface DashboardData { filter?: string; }
73
+
74
+ function Dashboard({ instanceId }: { instanceId: string }) {
75
+ // 100% Type-safe!
76
+ const [data, setData] = useTabData<DashboardData>();
77
+
78
+ return (
79
+ <div>
80
+ <h2>Dashboard</h2>
81
+ <p>Filter: {data.filter ?? 'None'}</p>
82
+ <button onClick={() => setData({ filter: 'Active' })}>Set Filter</button>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function Settings() {
88
+ return <h2>Settings</h2>;
89
+ }
90
+
91
+ // Create a bundler-agnostic registry
92
+ export const registry = createPageRegistry([
93
+ { id: 'dashboard', label: 'Dashboard', component: Dashboard },
94
+ { id: 'settings', label: 'Settings', component: Settings },
95
+ ]);
96
+ ```
97
+
98
+ ### 2. Wrap with Provider & Build the Layout
99
+
100
+ Use the provided headless components to build your accessible tab interface. They include all necessary ARIA attributes and keyboard events.
101
+
102
+ ```tsx
103
+ // src/App.tsx
104
+ import {
105
+ MultiTabProvider,
106
+ searchParamsAdapter,
107
+ TabList,
108
+ TabTrigger,
109
+ TabCloseButton,
110
+ TabPanels,
111
+ useMultiTab
112
+ } from 'react-multi-tab';
113
+ import { registry } from './registry';
114
+
115
+ function MainLayout() {
116
+ const { tabs, openTab } = useMultiTab();
117
+
118
+ return (
119
+ <div style={{ display: 'flex' }}>
120
+ {/* Sidebar / Menu */}
121
+ <nav style={{ width: 200 }}>
122
+ <button onClick={() => openTab('dashboard')}>Open Dashboard</button>
123
+ <button onClick={() => openTab('settings')}>Open Settings</button>
124
+ </nav>
125
+
126
+ <div style={{ flex: 1 }}>
127
+ {/* Accessible Tab List */}
128
+ <TabList aria-label="My Application Tabs">
129
+ {tabs.map((tab) => (
130
+ <TabTrigger key={tab.instanceId} instanceId={tab.instanceId}>
131
+ {tab.label}
132
+ <TabCloseButton instanceId={tab.instanceId} />
133
+ </TabTrigger>
134
+ ))}
135
+ </TabList>
136
+
137
+ {/* Renders all active and hidden tab panels */}
138
+ <TabPanels />
139
+ </div>
140
+ </div>
141
+ );
142
+ }
143
+
144
+ export default function App() {
145
+ return (
146
+ <MultiTabProvider
147
+ registry={registry}
148
+ adapter={searchParamsAdapter()} // Syncs active tabs to the browser URL
149
+ >
150
+ <MainLayout />
151
+ </MultiTabProvider>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ## Running the Playground
157
+
158
+ You can easily test the components locally using the built-in Playground!
159
+
160
+ 1. Go to the project root directory.
161
+ 2. Run the playground command:
162
+ ```bash
163
+ yarn run dev:demo
164
+ ```
165
+ 3. Open `http://localhost:3000` to interact with the demo.
166
+
167
+ ## Advanced Features
168
+
169
+ ### React Router Integration
170
+
171
+ If you want to sync your tab state with React Router, you can use the optional adapter.
172
+
173
+ ```tsx
174
+ import { MultiTabProvider } from 'react-multi-tab';
175
+ import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';
176
+
177
+ function App() {
178
+ const routerAdapter = useReactRouterAdapter();
179
+
180
+ return (
181
+ <MultiTabProvider registry={registry} adapter={routerAdapter}>
182
+ <MainLayout />
183
+ </MultiTabProvider>
184
+ );
185
+ }
186
+ ```
187
+
188
+ ## License
189
+
190
+ MIT © Arif GEVENCİ
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var reactRouterDom = require('react-router-dom');
5
+
6
+ // src/adapters/react-router.ts
7
+ function useReactRouterAdapter(options) {
8
+ const [, setSearchParams] = reactRouterDom.useSearchParams();
9
+ const setParamsRef = react.useRef(setSearchParams);
10
+ setParamsRef.current = setSearchParams;
11
+ const tabsKey = options?.tabsParam ?? "tabs";
12
+ const activeKey = options?.activeParam ?? "active";
13
+ return react.useMemo(
14
+ () => ({
15
+ read() {
16
+ const params = new URLSearchParams(window.location.search);
17
+ const tabsStr = params.get(tabsKey);
18
+ const activeTab = params.get(activeKey);
19
+ if (!tabsStr) return null;
20
+ return { tabs: tabsStr.split(",").filter(Boolean), activeTab };
21
+ },
22
+ write(tabs, activeTabId) {
23
+ const newParams = {};
24
+ if (tabs.length > 0) {
25
+ newParams[tabsKey] = tabs.map((t) => t.instanceId).join(",");
26
+ if (activeTabId) newParams[activeKey] = activeTabId;
27
+ }
28
+ setParamsRef.current(newParams, { replace: true });
29
+ },
30
+ subscribe(callback) {
31
+ window.addEventListener("popstate", callback);
32
+ return () => window.removeEventListener("popstate", callback);
33
+ }
34
+ }),
35
+ [tabsKey, activeKey]
36
+ );
37
+ }
38
+
39
+ exports.useReactRouterAdapter = useReactRouterAdapter;
40
+ //# sourceMappingURL=react-router.cjs.map
41
+ //# sourceMappingURL=react-router.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/react-router.ts"],"names":["useSearchParams","useRef","useMemo"],"mappings":";;;;;;AAgCO,SAAS,sBACd,OAAA,EACY;AACZ,EAAA,MAAM,GAAG,eAAe,CAAA,GAAIA,8BAAA,EAAgB;AAG5C,EAAA,MAAM,YAAA,GAAeC,aAAO,eAAe,CAAA;AAC3C,EAAA,YAAA,CAAa,OAAA,GAAU,eAAA;AAEvB,EAAA,MAAM,OAAA,GAAU,SAAS,SAAA,IAAa,MAAA;AACtC,EAAA,MAAM,SAAA,GAAY,SAAS,WAAA,IAAe,QAAA;AAE1C,EAAA,OAAOC,aAAA;AAAA,IACL,OAAO;AAAA,MACL,IAAA,GAAO;AACL,QAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAA,CAAO,SAAS,MAAM,CAAA;AACzD,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AAClC,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA;AAEtC,QAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,QAAA,OAAO,EAAE,MAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,EAAG,SAAA,EAAU;AAAA,MAC/D,CAAA;AAAA,MAEA,KAAA,CAAM,MAAqB,WAAA,EAA4B;AACrD,QAAA,MAAM,YAAoC,EAAC;AAC3C,QAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,UAAA,SAAA,CAAU,OAAO,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,UAAU,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC3D,UAAA,IAAI,WAAA,EAAa,SAAA,CAAU,SAAS,CAAA,GAAI,WAAA;AAAA,QAC1C;AACA,QAAA,YAAA,CAAa,OAAA,CAAQ,SAAA,EAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AAAA,MACnD,CAAA;AAAA,MAEA,UAAU,QAAA,EAAsB;AAC9B,QAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,QAAQ,CAAA;AAC5C,QAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,QAAQ,CAAA;AAAA,MAC9D;AAAA,KACF,CAAA;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,GACrB;AACF","file":"react-router.cjs","sourcesContent":["import { useMemo, useRef } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport type { TabInstance, URLAdapter } from \"../types\";\n\n/** Options for {@link useReactRouterAdapter}. */\nexport interface ReactRouterAdapterOptions {\n /** Query-string key for tab instance IDs. @default \"tabs\" */\n tabsParam?: string;\n /** Query-string key for the active tab. @default \"active\" */\n activeParam?: string;\n}\n\n/**\n * React Router v6+ adapter (hook).\n *\n * Must be called **inside** a `<BrowserRouter>` or equivalent.\n * Returns a stable {@link URLAdapter} reference.\n *\n * @example\n * ```tsx\n * import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';\n *\n * function App() {\n * const adapter = useReactRouterAdapter();\n * return (\n * <MultiTabProvider adapter={adapter} registry={registry}>\n * ...\n * </MultiTabProvider>\n * );\n * }\n * ```\n */\nexport function useReactRouterAdapter(\n options?: ReactRouterAdapterOptions\n): URLAdapter {\n const [, setSearchParams] = useSearchParams();\n\n // Keep a stable ref so the adapter object identity doesn't change.\n const setParamsRef = useRef(setSearchParams);\n setParamsRef.current = setSearchParams;\n\n const tabsKey = options?.tabsParam ?? \"tabs\";\n const activeKey = options?.activeParam ?? \"active\";\n\n return useMemo<URLAdapter>(\n () => ({\n read() {\n const params = new URLSearchParams(window.location.search);\n const tabsStr = params.get(tabsKey);\n const activeTab = params.get(activeKey);\n\n if (!tabsStr) return null;\n return { tabs: tabsStr.split(\",\").filter(Boolean), activeTab };\n },\n\n write(tabs: TabInstance[], activeTabId: string | null) {\n const newParams: Record<string, string> = {};\n if (tabs.length > 0) {\n newParams[tabsKey] = tabs.map((t) => t.instanceId).join(\",\");\n if (activeTabId) newParams[activeKey] = activeTabId;\n }\n setParamsRef.current(newParams, { replace: true });\n },\n\n subscribe(callback: () => void) {\n window.addEventListener(\"popstate\", callback);\n return () => window.removeEventListener(\"popstate\", callback);\n },\n }),\n [tabsKey, activeKey]\n );\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { U as URLAdapter } from '../types-Df_5I7eH.cjs';
2
+ import 'react';
3
+
4
+ /** Options for {@link useReactRouterAdapter}. */
5
+ interface ReactRouterAdapterOptions {
6
+ /** Query-string key for tab instance IDs. @default "tabs" */
7
+ tabsParam?: string;
8
+ /** Query-string key for the active tab. @default "active" */
9
+ activeParam?: string;
10
+ }
11
+ /**
12
+ * React Router v6+ adapter (hook).
13
+ *
14
+ * Must be called **inside** a `<BrowserRouter>` or equivalent.
15
+ * Returns a stable {@link URLAdapter} reference.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';
20
+ *
21
+ * function App() {
22
+ * const adapter = useReactRouterAdapter();
23
+ * return (
24
+ * <MultiTabProvider adapter={adapter} registry={registry}>
25
+ * ...
26
+ * </MultiTabProvider>
27
+ * );
28
+ * }
29
+ * ```
30
+ */
31
+ declare function useReactRouterAdapter(options?: ReactRouterAdapterOptions): URLAdapter;
32
+
33
+ export { type ReactRouterAdapterOptions, useReactRouterAdapter };
@@ -0,0 +1,33 @@
1
+ import { U as URLAdapter } from '../types-Df_5I7eH.js';
2
+ import 'react';
3
+
4
+ /** Options for {@link useReactRouterAdapter}. */
5
+ interface ReactRouterAdapterOptions {
6
+ /** Query-string key for tab instance IDs. @default "tabs" */
7
+ tabsParam?: string;
8
+ /** Query-string key for the active tab. @default "active" */
9
+ activeParam?: string;
10
+ }
11
+ /**
12
+ * React Router v6+ adapter (hook).
13
+ *
14
+ * Must be called **inside** a `<BrowserRouter>` or equivalent.
15
+ * Returns a stable {@link URLAdapter} reference.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';
20
+ *
21
+ * function App() {
22
+ * const adapter = useReactRouterAdapter();
23
+ * return (
24
+ * <MultiTabProvider adapter={adapter} registry={registry}>
25
+ * ...
26
+ * </MultiTabProvider>
27
+ * );
28
+ * }
29
+ * ```
30
+ */
31
+ declare function useReactRouterAdapter(options?: ReactRouterAdapterOptions): URLAdapter;
32
+
33
+ export { type ReactRouterAdapterOptions, useReactRouterAdapter };
@@ -0,0 +1,39 @@
1
+ import { useRef, useMemo } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+
4
+ // src/adapters/react-router.ts
5
+ function useReactRouterAdapter(options) {
6
+ const [, setSearchParams] = useSearchParams();
7
+ const setParamsRef = useRef(setSearchParams);
8
+ setParamsRef.current = setSearchParams;
9
+ const tabsKey = options?.tabsParam ?? "tabs";
10
+ const activeKey = options?.activeParam ?? "active";
11
+ return useMemo(
12
+ () => ({
13
+ read() {
14
+ const params = new URLSearchParams(window.location.search);
15
+ const tabsStr = params.get(tabsKey);
16
+ const activeTab = params.get(activeKey);
17
+ if (!tabsStr) return null;
18
+ return { tabs: tabsStr.split(",").filter(Boolean), activeTab };
19
+ },
20
+ write(tabs, activeTabId) {
21
+ const newParams = {};
22
+ if (tabs.length > 0) {
23
+ newParams[tabsKey] = tabs.map((t) => t.instanceId).join(",");
24
+ if (activeTabId) newParams[activeKey] = activeTabId;
25
+ }
26
+ setParamsRef.current(newParams, { replace: true });
27
+ },
28
+ subscribe(callback) {
29
+ window.addEventListener("popstate", callback);
30
+ return () => window.removeEventListener("popstate", callback);
31
+ }
32
+ }),
33
+ [tabsKey, activeKey]
34
+ );
35
+ }
36
+
37
+ export { useReactRouterAdapter };
38
+ //# sourceMappingURL=react-router.mjs.map
39
+ //# sourceMappingURL=react-router.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/react-router.ts"],"names":[],"mappings":";;;;AAgCO,SAAS,sBACd,OAAA,EACY;AACZ,EAAA,MAAM,GAAG,eAAe,CAAA,GAAI,eAAA,EAAgB;AAG5C,EAAA,MAAM,YAAA,GAAe,OAAO,eAAe,CAAA;AAC3C,EAAA,YAAA,CAAa,OAAA,GAAU,eAAA;AAEvB,EAAA,MAAM,OAAA,GAAU,SAAS,SAAA,IAAa,MAAA;AACtC,EAAA,MAAM,SAAA,GAAY,SAAS,WAAA,IAAe,QAAA;AAE1C,EAAA,OAAO,OAAA;AAAA,IACL,OAAO;AAAA,MACL,IAAA,GAAO;AACL,QAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,MAAA,CAAO,SAAS,MAAM,CAAA;AACzD,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,GAAA,CAAI,OAAO,CAAA;AAClC,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,GAAA,CAAI,SAAS,CAAA;AAEtC,QAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,QAAA,OAAO,EAAE,MAAM,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,EAAG,SAAA,EAAU;AAAA,MAC/D,CAAA;AAAA,MAEA,KAAA,CAAM,MAAqB,WAAA,EAA4B;AACrD,QAAA,MAAM,YAAoC,EAAC;AAC3C,QAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG;AACnB,UAAA,SAAA,CAAU,OAAO,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,CAAC,MAAM,CAAA,CAAE,UAAU,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC3D,UAAA,IAAI,WAAA,EAAa,SAAA,CAAU,SAAS,CAAA,GAAI,WAAA;AAAA,QAC1C;AACA,QAAA,YAAA,CAAa,OAAA,CAAQ,SAAA,EAAW,EAAE,OAAA,EAAS,MAAM,CAAA;AAAA,MACnD,CAAA;AAAA,MAEA,UAAU,QAAA,EAAsB;AAC9B,QAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,QAAQ,CAAA;AAC5C,QAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,QAAQ,CAAA;AAAA,MAC9D;AAAA,KACF,CAAA;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,GACrB;AACF","file":"react-router.mjs","sourcesContent":["import { useMemo, useRef } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\nimport type { TabInstance, URLAdapter } from \"../types\";\n\n/** Options for {@link useReactRouterAdapter}. */\nexport interface ReactRouterAdapterOptions {\n /** Query-string key for tab instance IDs. @default \"tabs\" */\n tabsParam?: string;\n /** Query-string key for the active tab. @default \"active\" */\n activeParam?: string;\n}\n\n/**\n * React Router v6+ adapter (hook).\n *\n * Must be called **inside** a `<BrowserRouter>` or equivalent.\n * Returns a stable {@link URLAdapter} reference.\n *\n * @example\n * ```tsx\n * import { useReactRouterAdapter } from 'react-multi-tab/adapters/react-router';\n *\n * function App() {\n * const adapter = useReactRouterAdapter();\n * return (\n * <MultiTabProvider adapter={adapter} registry={registry}>\n * ...\n * </MultiTabProvider>\n * );\n * }\n * ```\n */\nexport function useReactRouterAdapter(\n options?: ReactRouterAdapterOptions\n): URLAdapter {\n const [, setSearchParams] = useSearchParams();\n\n // Keep a stable ref so the adapter object identity doesn't change.\n const setParamsRef = useRef(setSearchParams);\n setParamsRef.current = setSearchParams;\n\n const tabsKey = options?.tabsParam ?? \"tabs\";\n const activeKey = options?.activeParam ?? \"active\";\n\n return useMemo<URLAdapter>(\n () => ({\n read() {\n const params = new URLSearchParams(window.location.search);\n const tabsStr = params.get(tabsKey);\n const activeTab = params.get(activeKey);\n\n if (!tabsStr) return null;\n return { tabs: tabsStr.split(\",\").filter(Boolean), activeTab };\n },\n\n write(tabs: TabInstance[], activeTabId: string | null) {\n const newParams: Record<string, string> = {};\n if (tabs.length > 0) {\n newParams[tabsKey] = tabs.map((t) => t.instanceId).join(\",\");\n if (activeTabId) newParams[activeKey] = activeTabId;\n }\n setParamsRef.current(newParams, { replace: true });\n },\n\n subscribe(callback: () => void) {\n window.addEventListener(\"popstate\", callback);\n return () => window.removeEventListener(\"popstate\", callback);\n },\n }),\n [tabsKey, activeKey]\n );\n}\n"]}