@opencosmos/ui 1.3.1 → 1.3.2
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/.claude/CLAUDE.md +47 -47
- package/LICENSE +21 -0
- package/README.md +21 -21
- package/dist/{hooks-CKW8vE9H.d.ts → hooks-CFPKFXhH.d.ts} +1 -1
- package/dist/{hooks-1b8WaQf1.d.mts → hooks-CeAuZ0i5.d.mts} +1 -1
- package/dist/hooks.d.mts +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js.map +1 -1
- package/dist/hooks.mjs.map +1 -1
- package/dist/index.d.mts +78 -11
- package/dist/index.d.ts +78 -11
- package/dist/index.js +585 -342
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +548 -312
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-CXPDMsl7.d.mts → providers-CzKisd2T.d.mts} +1 -1
- package/dist/{providers-Dn_Msjvz.d.ts → providers-D39-kwai.d.ts} +1 -1
- package/dist/providers.d.mts +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js.map +1 -1
- package/dist/providers.mjs.map +1 -1
- package/dist/tokens.js.map +1 -1
- package/dist/{utils-Cs04sxth.d.mts → utils-CkatYLG4.d.mts} +1 -1
- package/dist/{utils-CIIM7dAC.d.ts → utils-Y1Zi7biA.d.ts} +1 -1
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/utils.mjs.map +1 -1
- package/package.json +256 -256
- package/src/component-registry.ts +4 -4
- package/src/components/data-display/CollapsibleCodeBlock.tsx +1 -1
- package/src/components/data-display/OpenCosmosIcon.tsx +39 -0
- package/src/components/data-display/index.ts +1 -0
- package/src/components/layout/AppSidebar.tsx +277 -0
- package/src/components/layout/CustomizerPanel.tsx +2 -2
- package/src/components/layout/index.ts +1 -0
- package/src/hooks/useTheme.ts +1 -1
- package/src/hooks.ts +1 -1
- package/src/index.ts +6 -4
- package/src/lib/store/customizer.ts +1 -1
- package/src/lib/store/theme.ts +1 -1
- package/src/lib/syntax-parser/index.ts +1 -1
- package/src/providers/ThemeProvider.tsx +2 -2
- package/src/providers.ts +1 -1
- package/src/tokens.ts +3 -3
- package/src/utils.ts +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { useMotionPreference } from '../../hooks/useMotionPreference';
|
|
6
|
+
import { PanelLeftClose } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'appsidebar:state';
|
|
11
|
+
export const APP_SIDEBAR_WIDTH = 280;
|
|
12
|
+
export const APP_SIDEBAR_WIDTH_COLLAPSED = 60;
|
|
13
|
+
|
|
14
|
+
// ── Context ───────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface AppSidebarContextValue {
|
|
17
|
+
isOpen: boolean;
|
|
18
|
+
toggle: () => void;
|
|
19
|
+
open: () => void;
|
|
20
|
+
close: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const AppSidebarContext = createContext<AppSidebarContextValue | null>(null);
|
|
24
|
+
|
|
25
|
+
// Safe default used when no provider is in scope.
|
|
26
|
+
//
|
|
27
|
+
// WHY this exists — do not change back to a throw:
|
|
28
|
+
//
|
|
29
|
+
// `next.config.mjs` sets `transpilePackages: ['@opencosmos/ui']`, which tells
|
|
30
|
+
// Next.js/webpack to bundle this package from SOURCE instead of dist. Webpack can
|
|
31
|
+
// then split this file and its consumer (e.g. the docs playground) into separate
|
|
32
|
+
// chunks. Each chunk gets its own module execution scope, so `createContext()` can
|
|
33
|
+
// run twice — producing two distinct AppSidebarContext objects. The Provider writes
|
|
34
|
+
// to instance A; AppSidebar reads from instance B → useContext returns null even
|
|
35
|
+
// though a Provider is present in the React tree.
|
|
36
|
+
//
|
|
37
|
+
// Throwing on null ctx (the common pattern) causes the entire page to 500.
|
|
38
|
+
// Returning a safe default lets the component render correctly in isolation.
|
|
39
|
+
// Consuming apps should still wrap with AppSidebarProvider for state persistence.
|
|
40
|
+
const DEFAULT_CONTEXT: AppSidebarContextValue = {
|
|
41
|
+
isOpen: true,
|
|
42
|
+
toggle: () => {},
|
|
43
|
+
open: () => {},
|
|
44
|
+
close: () => {},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function useAppSidebar(): AppSidebarContextValue {
|
|
48
|
+
return useContext(AppSidebarContext) ?? DEFAULT_CONTEXT;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── AppSidebarProvider ────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export interface AppSidebarProviderProps {
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
/** Initial open state used on server and first render @default true */
|
|
56
|
+
defaultOpen?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function AppSidebarProvider({ children, defaultOpen = true }: AppSidebarProviderProps) {
|
|
60
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
64
|
+
if (stored !== null) setIsOpen(stored === 'true');
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const persist = (value: boolean) => localStorage.setItem(STORAGE_KEY, String(value));
|
|
68
|
+
|
|
69
|
+
const toggle = () => setIsOpen(prev => { const next = !prev; persist(next); return next; });
|
|
70
|
+
const open = () => { setIsOpen(true); persist(true); };
|
|
71
|
+
const close = () => { setIsOpen(false); persist(false); };
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<AppSidebarContext.Provider value={{ isOpen, toggle, open, close }}>
|
|
75
|
+
{children}
|
|
76
|
+
</AppSidebarContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── AppSidebarInset ───────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function AppSidebarInset({
|
|
83
|
+
children,
|
|
84
|
+
className,
|
|
85
|
+
}: {
|
|
86
|
+
children: React.ReactNode;
|
|
87
|
+
className?: string;
|
|
88
|
+
}) {
|
|
89
|
+
const { isOpen } = useAppSidebar();
|
|
90
|
+
const { shouldAnimate, scale } = useMotionPreference();
|
|
91
|
+
const duration = shouldAnimate ? Math.round(300 * (5 / Math.max(scale, 0.1))) : 0;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div
|
|
95
|
+
className={cn('min-h-screen', className)}
|
|
96
|
+
style={{
|
|
97
|
+
marginLeft: isOpen ? APP_SIDEBAR_WIDTH : APP_SIDEBAR_WIDTH_COLLAPSED,
|
|
98
|
+
transition: shouldAnimate ? `margin-left ${duration}ms ease-out` : 'none',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Nav item type ─────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export interface AppSidebarNavItem {
|
|
109
|
+
icon: React.ReactNode;
|
|
110
|
+
label: string;
|
|
111
|
+
href: string;
|
|
112
|
+
active?: boolean;
|
|
113
|
+
external?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── AppSidebar ────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export interface AppSidebarProps {
|
|
119
|
+
/** Icon element always visible (32×32). Clicking it toggles open/closed. */
|
|
120
|
+
logo?: React.ReactNode;
|
|
121
|
+
/** Wordmark shown next to the logo when expanded */
|
|
122
|
+
title?: string;
|
|
123
|
+
/** Navigation items */
|
|
124
|
+
items?: AppSidebarNavItem[];
|
|
125
|
+
/** Body slot — rendered in the scrollable mid-section (e.g. conversation history). Only visible when expanded. */
|
|
126
|
+
children?: React.ReactNode;
|
|
127
|
+
/** Footer slot — auth section, user avatar, sign-in prompt, etc. */
|
|
128
|
+
footer?: React.ReactNode;
|
|
129
|
+
/** Additional className for the <aside> */
|
|
130
|
+
className?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function AppSidebar({
|
|
134
|
+
logo,
|
|
135
|
+
title,
|
|
136
|
+
items = [],
|
|
137
|
+
children,
|
|
138
|
+
footer,
|
|
139
|
+
className,
|
|
140
|
+
}: AppSidebarProps) {
|
|
141
|
+
const { isOpen, toggle } = useAppSidebar();
|
|
142
|
+
const { shouldAnimate, scale } = useMotionPreference();
|
|
143
|
+
const duration = shouldAnimate ? Math.round(300 * (5 / Math.max(scale, 0.1))) : 0;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<aside
|
|
147
|
+
className={cn(
|
|
148
|
+
'fixed left-0 top-0 bottom-0 z-40 flex flex-col',
|
|
149
|
+
'bg-background border-r border-foreground/8 overflow-hidden',
|
|
150
|
+
className
|
|
151
|
+
)}
|
|
152
|
+
style={{
|
|
153
|
+
width: isOpen ? APP_SIDEBAR_WIDTH : APP_SIDEBAR_WIDTH_COLLAPSED,
|
|
154
|
+
transition: shouldAnimate ? `width ${duration}ms ease-out` : 'none',
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
{/* ── Header ─────────────────────────────────────────────────────── */}
|
|
158
|
+
<div className="flex items-center h-16 px-[10px] shrink-0">
|
|
159
|
+
{/* Logo + wordmark — clicking toggles in both states */}
|
|
160
|
+
<button
|
|
161
|
+
onClick={toggle}
|
|
162
|
+
className={cn(
|
|
163
|
+
'flex items-center gap-2.5 flex-1 min-w-0',
|
|
164
|
+
'rounded-lg p-1.5',
|
|
165
|
+
'hover:bg-foreground/5 transition-colors duration-150',
|
|
166
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus)]'
|
|
167
|
+
)}
|
|
168
|
+
aria-label={isOpen ? (title ?? 'OpenCosmos') : 'Expand sidebar'}
|
|
169
|
+
>
|
|
170
|
+
<span className="w-8 h-8 shrink-0 flex items-center justify-center">
|
|
171
|
+
{logo}
|
|
172
|
+
</span>
|
|
173
|
+
<span
|
|
174
|
+
className="font-semibold text-sm text-foreground whitespace-nowrap"
|
|
175
|
+
style={{
|
|
176
|
+
opacity: isOpen ? 1 : 0,
|
|
177
|
+
width: isOpen ? 'auto' : 0,
|
|
178
|
+
overflow: 'hidden',
|
|
179
|
+
pointerEvents: isOpen ? 'auto' : 'none',
|
|
180
|
+
transition: shouldAnimate
|
|
181
|
+
? `opacity ${Math.round(duration * 0.6)}ms ease-out`
|
|
182
|
+
: 'none',
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{title}
|
|
186
|
+
</span>
|
|
187
|
+
</button>
|
|
188
|
+
|
|
189
|
+
{/* Collapse icon — only shown/accessible when open */}
|
|
190
|
+
<button
|
|
191
|
+
onClick={toggle}
|
|
192
|
+
tabIndex={isOpen ? 0 : -1}
|
|
193
|
+
className={cn(
|
|
194
|
+
'shrink-0 w-8 h-8 flex items-center justify-center rounded-lg',
|
|
195
|
+
'text-foreground/35 hover:text-foreground/65 hover:bg-foreground/5',
|
|
196
|
+
'transition-colors duration-150',
|
|
197
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus)]'
|
|
198
|
+
)}
|
|
199
|
+
style={{
|
|
200
|
+
opacity: isOpen ? 1 : 0,
|
|
201
|
+
pointerEvents: isOpen ? 'auto' : 'none',
|
|
202
|
+
transition: shouldAnimate
|
|
203
|
+
? `opacity ${Math.round(duration * 0.5)}ms ease-out`
|
|
204
|
+
: 'none',
|
|
205
|
+
}}
|
|
206
|
+
aria-label="Collapse sidebar"
|
|
207
|
+
>
|
|
208
|
+
<PanelLeftClose className="w-4 h-4" />
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* ── Nav items ──────────────────────────────────────────────────── */}
|
|
213
|
+
{items.length > 0 && (
|
|
214
|
+
<nav className="px-2 py-2 space-y-0.5 shrink-0" aria-label="Main navigation">
|
|
215
|
+
{items.map((item) => (
|
|
216
|
+
<a
|
|
217
|
+
key={item.label}
|
|
218
|
+
href={item.href}
|
|
219
|
+
target={item.external ? '_blank' : undefined}
|
|
220
|
+
rel={item.external ? 'noopener noreferrer' : undefined}
|
|
221
|
+
title={!isOpen ? item.label : undefined}
|
|
222
|
+
aria-label={!isOpen ? item.label : undefined}
|
|
223
|
+
className={cn(
|
|
224
|
+
'flex items-center rounded-lg transition-colors duration-150',
|
|
225
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus)]',
|
|
226
|
+
isOpen
|
|
227
|
+
? 'gap-3 px-3 py-2.5'
|
|
228
|
+
: 'justify-center w-9 h-9 mx-auto',
|
|
229
|
+
item.active
|
|
230
|
+
? 'bg-foreground/8 text-foreground font-medium'
|
|
231
|
+
: 'text-[var(--color-text-secondary)] hover:bg-foreground/5 hover:text-[var(--color-text-primary)]'
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
<span className="shrink-0 flex items-center justify-center w-4 h-4">
|
|
235
|
+
{item.icon}
|
|
236
|
+
</span>
|
|
237
|
+
<span
|
|
238
|
+
className="text-sm whitespace-nowrap"
|
|
239
|
+
style={{
|
|
240
|
+
opacity: isOpen ? 1 : 0,
|
|
241
|
+
width: isOpen ? 'auto' : 0,
|
|
242
|
+
overflow: 'hidden',
|
|
243
|
+
pointerEvents: isOpen ? 'auto' : 'none',
|
|
244
|
+
transition: shouldAnimate
|
|
245
|
+
? `opacity ${Math.round(duration * 0.55)}ms ease-out`
|
|
246
|
+
: 'none',
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
{item.label}
|
|
250
|
+
</span>
|
|
251
|
+
</a>
|
|
252
|
+
))}
|
|
253
|
+
</nav>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* ── Body (conversation history, etc.) ──────────────────────────── */}
|
|
257
|
+
<div
|
|
258
|
+
className="flex-1 overflow-y-auto overflow-x-hidden min-h-0"
|
|
259
|
+
style={{ opacity: isOpen ? 1 : 0, pointerEvents: isOpen ? 'auto' : 'none' }}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* ── Footer ─────────────────────────────────────────────────────── */}
|
|
265
|
+
{footer && (
|
|
266
|
+
<div
|
|
267
|
+
className={cn(
|
|
268
|
+
'shrink-0 border-t border-foreground/8',
|
|
269
|
+
isOpen ? 'p-3' : 'px-2 py-3 flex justify-center'
|
|
270
|
+
)}
|
|
271
|
+
>
|
|
272
|
+
{footer}
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</aside>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { SlidersHorizontal, Sun, Moon, SunMoon, Building2, Leaf, Zap, Rocket, X, Palette } from 'lucide-react';
|
|
4
|
-
import { studioTokens, terraTokens, voltTokens, speedboatTokens, PUBLIC_THEME_NAMES } from '@
|
|
5
|
-
import type { ThemeName } from '@
|
|
4
|
+
import { studioTokens, terraTokens, voltTokens, speedboatTokens, PUBLIC_THEME_NAMES } from '@opencosmos/tokens';
|
|
5
|
+
import type { ThemeName } from '@opencosmos/tokens';
|
|
6
6
|
import { useCustomizer } from '../../lib/store/customizer';
|
|
7
7
|
import { useThemeStore } from '../../lib/store/theme';
|
|
8
8
|
import { ColorPicker } from '../forms/ColorPicker';
|
package/src/hooks/useTheme.ts
CHANGED
package/src/hooks.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
2
|
// MAIN BARREL EXPORTS - Backward Compatible
|
|
3
3
|
// ============================================================================
|
|
4
|
-
// All components remain importable from '@
|
|
5
|
-
// Example: import { Button, Card, Dialog } from '@
|
|
4
|
+
// All components remain importable from '@opencosmos/ui' root
|
|
5
|
+
// Example: import { Button, Card, Dialog } from '@opencosmos/ui'
|
|
6
6
|
|
|
7
7
|
// Actions
|
|
8
8
|
export * from './components/actions/Button';
|
|
@@ -81,6 +81,7 @@ export * from './components/data-display/CollapsibleCodeBlock';
|
|
|
81
81
|
export * from './components/data-display/DataTable';
|
|
82
82
|
export * from './components/data-display/DescriptionList';
|
|
83
83
|
export * from './components/data-display/GitHubIcon';
|
|
84
|
+
export * from './components/data-display/OpenCosmosIcon';
|
|
84
85
|
export * from './components/data-display/Heading';
|
|
85
86
|
export * from './components/data-display/Table';
|
|
86
87
|
export * from './components/data-display/Text';
|
|
@@ -108,6 +109,7 @@ export * from './components/layout/Resizable';
|
|
|
108
109
|
export * from './components/layout/ScrollArea';
|
|
109
110
|
export * from './components/layout/Separator';
|
|
110
111
|
export * from './components/layout/Sidebar';
|
|
112
|
+
export * from './components/layout/AppSidebar';
|
|
111
113
|
export * from './components/layout/Stack';
|
|
112
114
|
export * from './components/layout/GlassSurface';
|
|
113
115
|
|
|
@@ -142,7 +144,7 @@ export * from './lib/validation';
|
|
|
142
144
|
export * from './lib/syntax-parser';
|
|
143
145
|
|
|
144
146
|
// Tokens (selective re-exports)
|
|
145
|
-
export { typographySystem } from '@
|
|
147
|
+
export { typographySystem } from '@opencosmos/tokens';
|
|
146
148
|
|
|
147
149
|
// Component Registry (metadata)
|
|
148
150
|
export * from './component-registry';
|
|
@@ -151,7 +153,7 @@ export * from './component-registry';
|
|
|
151
153
|
// OPTIONAL CATEGORY-BASED EXPORTS (Future Use)
|
|
152
154
|
// ============================================================================
|
|
153
155
|
// These allow category-specific imports for better code organization
|
|
154
|
-
// Example: import { Button } from '@
|
|
156
|
+
// Example: import { Button } from '@opencosmos/ui/actions'
|
|
155
157
|
// Note: These require package.json exports configuration
|
|
156
158
|
|
|
157
159
|
// Re-export categories for convenience
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { persist } from 'zustand/middleware';
|
|
3
|
-
import { computeDerivedTokens, type FontTheme } from '@
|
|
3
|
+
import { computeDerivedTokens, type FontTheme } from '@opencosmos/tokens';
|
|
4
4
|
import {
|
|
5
5
|
generateColorScale,
|
|
6
6
|
getOptimalForeground,
|
package/src/lib/store/theme.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { create } from 'zustand';
|
|
7
7
|
import { persist } from 'zustand/middleware';
|
|
8
|
-
import type { ThemeName, ColorMode } from '@
|
|
8
|
+
import type { ThemeName, ColorMode } from '@opencosmos/tokens';
|
|
9
9
|
|
|
10
10
|
// Re-export types for convenience
|
|
11
11
|
export type { ThemeName, ColorMode };
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import { useEffect, useState } from 'react';
|
|
9
9
|
import { useThemeStore } from '../lib/store/theme';
|
|
10
10
|
import { useCustomizer, type ColorPalette } from '../lib/store/customizer';
|
|
11
|
-
import { studioTokens, terraTokens, voltTokens, speedboatTokens, syntaxColors, codeColors } from '@
|
|
12
|
-
import type { ThemeName, ColorMode } from '@
|
|
11
|
+
import { studioTokens, terraTokens, voltTokens, speedboatTokens, syntaxColors, codeColors } from '@opencosmos/tokens';
|
|
12
|
+
import type { ThemeName, ColorMode } from '@opencosmos/tokens';
|
|
13
13
|
|
|
14
14
|
// ── Type-safe token access ──────────────────────────────────────────────────
|
|
15
15
|
|
package/src/providers.ts
CHANGED
package/src/tokens.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tokens subpath export
|
|
3
|
-
* Allows: import { ... } from '@
|
|
3
|
+
* Allows: import { ... } from '@opencosmos/ui/tokens'
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
// Re-export all tokens from @
|
|
7
|
-
export * from '@
|
|
6
|
+
// Re-export all tokens from @opencosmos/tokens
|
|
7
|
+
export * from '@opencosmos/tokens';
|