@flamingo-stack/openframe-frontend-core 0.0.263 → 0.0.264
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/{chunk-C3FAOQAH.cjs → chunk-3LWUHLPK.cjs} +28 -28
- package/dist/{chunk-C3FAOQAH.cjs.map → chunk-3LWUHLPK.cjs.map} +1 -1
- package/dist/{chunk-U7UDESAU.js → chunk-6ZEKKAAL.js} +2 -2
- package/dist/{chunk-YC357BPF.js → chunk-JOUI6TQY.js} +3 -3
- package/dist/{chunk-2XUWR7AH.cjs → chunk-P67D545X.cjs} +11 -11
- package/dist/{chunk-2XUWR7AH.cjs.map → chunk-P67D545X.cjs.map} +1 -1
- package/dist/{chunk-3DC6MHW6.cjs → chunk-QLWBBCI5.cjs} +491 -391
- package/dist/chunk-QLWBBCI5.cjs.map +1 -0
- package/dist/{chunk-36HLBLDK.cjs → chunk-SIYHXLFH.cjs} +462 -454
- package/dist/chunk-SIYHXLFH.cjs.map +1 -0
- package/dist/{chunk-ALEKX56R.js → chunk-SSUUMPAQ.js} +2 -2
- package/dist/{chunk-6AIXHYBZ.js → chunk-VYTU7JZR.js} +64 -56
- package/dist/chunk-VYTU7JZR.js.map +1 -0
- package/dist/{chunk-6GBA6YJN.js → chunk-ZMFT5XOB.js} +576 -476
- package/dist/chunk-ZMFT5XOB.js.map +1 -0
- package/dist/{chunk-XKA7UFED.cjs → chunk-ZOOTBYZO.cjs} +30 -30
- package/dist/{chunk-XKA7UFED.cjs.map → chunk-ZOOTBYZO.cjs.map} +1 -1
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/features/notifications/notification-drawer.d.ts.map +1 -1
- package/dist/components/features/notifications/notifications-context.d.ts +9 -1
- package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
- package/dist/components/index.cjs +50 -50
- package/dist/components/index.js +4 -4
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/navigation/sliding-sidebar.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +4 -4
- package/dist/components/related-content/index.js +3 -3
- package/dist/components/tickets/index.cjs +58 -58
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/drawer.d.ts +5 -0
- package/dist/components/ui/drawer.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/hooks/index.cjs +6 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +5 -1
- package/dist/hooks/ui/index.d.ts +6 -4
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-header-height.d.ts +8 -0
- package/dist/hooks/ui/use-header-height.d.ts.map +1 -0
- package/dist/hooks/ui/use-horizontal-scrollbar.d.ts.map +1 -1
- package/dist/hooks/ui/use-notification-permission.d.ts +14 -0
- package/dist/hooks/ui/use-notification-permission.d.ts.map +1 -0
- package/dist/index.cjs +7 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -2
- package/package.json +1 -1
- package/src/components/features/notifications/notification-drawer.tsx +51 -2
- package/src/components/features/notifications/notifications-context.tsx +26 -0
- package/src/components/navigation/sliding-sidebar.tsx +3 -82
- package/src/components/ui/drawer.tsx +17 -6
- package/src/hooks/ui/index.ts +6 -4
- package/src/hooks/ui/use-header-height.ts +55 -0
- package/src/hooks/ui/use-horizontal-scrollbar.ts +19 -2
- package/src/hooks/ui/use-notification-permission.ts +64 -0
- package/dist/chunk-36HLBLDK.cjs.map +0 -1
- package/dist/chunk-3DC6MHW6.cjs.map +0 -1
- package/dist/chunk-6AIXHYBZ.js.map +0 -1
- package/dist/chunk-6GBA6YJN.js.map +0 -1
- /package/dist/{chunk-U7UDESAU.js.map → chunk-6ZEKKAAL.js.map} +0 -0
- /package/dist/{chunk-YC357BPF.js.map → chunk-JOUI6TQY.js.map} +0 -0
- /package/dist/{chunk-ALEKX56R.js.map → chunk-SSUUMPAQ.js.map} +0 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useState, useEffect } from 'react'
|
|
4
4
|
import { motion, AnimatePresence } from 'framer-motion'
|
|
5
5
|
import { cn } from '../../utils'
|
|
6
|
+
import { useHeaderHeight } from '../../hooks/ui'
|
|
6
7
|
import { SlidingSidebarConfig, NavigationItem } from '../../types/navigation'
|
|
7
8
|
import { Button } from '../ui/button'
|
|
8
9
|
|
|
@@ -13,89 +14,9 @@ export interface SlidingSidebarProps {
|
|
|
13
14
|
export function SlidingSidebar({ config }: SlidingSidebarProps) {
|
|
14
15
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set())
|
|
15
16
|
const [mounted, setMounted] = useState(false)
|
|
16
|
-
const
|
|
17
|
+
const headerHeight = useHeaderHeight()
|
|
17
18
|
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
setMounted(true)
|
|
20
|
-
|
|
21
|
-
// Function to calculate total header height
|
|
22
|
-
const calculateHeaderHeight = () => {
|
|
23
|
-
let totalHeight = 0
|
|
24
|
-
|
|
25
|
-
// Find and measure the header
|
|
26
|
-
const header = document.querySelector('header')
|
|
27
|
-
if (header) {
|
|
28
|
-
totalHeight += header.offsetHeight
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Find and measure the announcement bar (if exists)
|
|
32
|
-
const announcementBar = document.querySelector('[data-announcement-bar]')
|
|
33
|
-
if (announcementBar && announcementBar instanceof HTMLElement) {
|
|
34
|
-
totalHeight += announcementBar.offsetHeight
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Update the header height if we found elements
|
|
38
|
-
if (totalHeight > 0) {
|
|
39
|
-
setHeaderHeight(totalHeight)
|
|
40
|
-
} else {
|
|
41
|
-
// If no header found, default to 64px
|
|
42
|
-
setHeaderHeight(64)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Calculate initial height with a small delay to ensure elements are rendered
|
|
47
|
-
setTimeout(calculateHeaderHeight, 100)
|
|
48
|
-
|
|
49
|
-
// Also calculate immediately in case elements are already there
|
|
50
|
-
calculateHeaderHeight()
|
|
51
|
-
|
|
52
|
-
// Recalculate on window resize
|
|
53
|
-
window.addEventListener('resize', calculateHeaderHeight)
|
|
54
|
-
|
|
55
|
-
// Use ResizeObserver to detect header size changes
|
|
56
|
-
const resizeObserver = new ResizeObserver(calculateHeaderHeight)
|
|
57
|
-
|
|
58
|
-
// Observe header
|
|
59
|
-
const header = document.querySelector('header')
|
|
60
|
-
if (header) {
|
|
61
|
-
resizeObserver.observe(header)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Observe announcement bar
|
|
65
|
-
const announcementBar = document.querySelector('[data-announcement-bar]')
|
|
66
|
-
if (announcementBar) {
|
|
67
|
-
resizeObserver.observe(announcementBar)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Use MutationObserver to detect when announcement bar is added/removed
|
|
71
|
-
const mutationObserver = new MutationObserver((mutations) => {
|
|
72
|
-
// Check if announcement bar was added or removed
|
|
73
|
-
for (const mutation of mutations) {
|
|
74
|
-
if (mutation.type === 'childList') {
|
|
75
|
-
// Recalculate height when DOM changes
|
|
76
|
-
calculateHeaderHeight()
|
|
77
|
-
|
|
78
|
-
// If announcement bar was added, start observing it
|
|
79
|
-
const newAnnouncementBar = document.querySelector('[data-announcement-bar]')
|
|
80
|
-
if (newAnnouncementBar) {
|
|
81
|
-
resizeObserver.observe(newAnnouncementBar)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// Observe the body for changes (announcement bar might be added/removed)
|
|
88
|
-
mutationObserver.observe(document.body, {
|
|
89
|
-
childList: true,
|
|
90
|
-
subtree: true
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
window.removeEventListener('resize', calculateHeaderHeight)
|
|
95
|
-
resizeObserver.disconnect()
|
|
96
|
-
mutationObserver.disconnect()
|
|
97
|
-
}
|
|
98
|
-
}, [])
|
|
19
|
+
useEffect(() => { setMounted(true) }, [])
|
|
99
20
|
|
|
100
21
|
const toggleExpanded = (itemId: string) => {
|
|
101
22
|
const newExpanded = new Set(expandedItems)
|
|
@@ -6,6 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
6
6
|
import { X } from "lucide-react"
|
|
7
7
|
|
|
8
8
|
import { cn } from "../../utils/cn"
|
|
9
|
+
import { useHeaderHeight } from "../../hooks/ui/use-header-height"
|
|
9
10
|
|
|
10
11
|
const Drawer = DialogPrimitive.Root
|
|
11
12
|
|
|
@@ -354,6 +355,9 @@ interface DrawerContentProps
|
|
|
354
355
|
panelStyle?: React.CSSProperties
|
|
355
356
|
/** Optional className applied to the inner panel. (Alias for `className`.) */
|
|
356
357
|
panelClassName?: string
|
|
358
|
+
/** Offset the drawer's top edge below the app header + announcement bar.
|
|
359
|
+
* Measured live via ResizeObserver so it tracks height changes. */
|
|
360
|
+
offsetHeader?: boolean
|
|
357
361
|
}
|
|
358
362
|
|
|
359
363
|
const DrawerContent = React.forwardRef<
|
|
@@ -376,6 +380,7 @@ const DrawerContent = React.forwardRef<
|
|
|
376
380
|
style,
|
|
377
381
|
panelStyle,
|
|
378
382
|
panelClassName,
|
|
383
|
+
offsetHeader = false,
|
|
379
384
|
children,
|
|
380
385
|
...props
|
|
381
386
|
},
|
|
@@ -385,6 +390,8 @@ const DrawerContent = React.forwardRef<
|
|
|
385
390
|
const isHorizontal = HORIZONTAL_SIDES.has(resolvedSide)
|
|
386
391
|
const initialSize = defaultSize ?? (isHorizontal ? 560 : 480)
|
|
387
392
|
|
|
393
|
+
const headerHeight = useHeaderHeight()
|
|
394
|
+
|
|
388
395
|
const [isMobile, setIsMobile] = React.useState(false)
|
|
389
396
|
React.useEffect(() => {
|
|
390
397
|
if (typeof window === "undefined") return
|
|
@@ -417,7 +424,7 @@ const DrawerContent = React.forwardRef<
|
|
|
417
424
|
<DialogPrimitive.Content
|
|
418
425
|
ref={ref}
|
|
419
426
|
className={cn(drawerVariants({ side, flush }))}
|
|
420
|
-
style={style}
|
|
427
|
+
style={{ ...(offsetHeader ? { top: headerHeight } : {}), ...style }}
|
|
421
428
|
{...props}
|
|
422
429
|
>
|
|
423
430
|
{/* Resize handle is a sibling of the panel — outside the panel's
|
|
@@ -468,12 +475,14 @@ interface DrawerTitleProps
|
|
|
468
475
|
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> {
|
|
469
476
|
/** Optional header actions rendered between the title and the close button. */
|
|
470
477
|
actions?: React.ReactNode
|
|
478
|
+
/** Hide the close (X) button. */
|
|
479
|
+
hideClose?: boolean
|
|
471
480
|
}
|
|
472
481
|
|
|
473
482
|
const DrawerTitle = React.forwardRef<
|
|
474
483
|
React.ComponentRef<typeof DialogPrimitive.Title>,
|
|
475
484
|
DrawerTitleProps
|
|
476
|
-
>(({ className, children, actions, ...props }, ref) => (
|
|
485
|
+
>(({ className, children, actions, hideClose, ...props }, ref) => (
|
|
477
486
|
<div className="flex items-start gap-4">
|
|
478
487
|
<DialogPrimitive.Title
|
|
479
488
|
ref={ref}
|
|
@@ -486,10 +495,12 @@ const DrawerTitle = React.forwardRef<
|
|
|
486
495
|
{children}
|
|
487
496
|
</DialogPrimitive.Title>
|
|
488
497
|
{actions}
|
|
489
|
-
|
|
490
|
-
<
|
|
491
|
-
|
|
492
|
-
|
|
498
|
+
{!hideClose && (
|
|
499
|
+
<DialogPrimitive.Close className="shrink-0 rounded-sm text-ods-text-secondary transition-colors hover:text-ods-text-primary outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0">
|
|
500
|
+
<X className="size-6" />
|
|
501
|
+
<span className="sr-only">Close</span>
|
|
502
|
+
</DialogPrimitive.Close>
|
|
503
|
+
)}
|
|
493
504
|
</div>
|
|
494
505
|
))
|
|
495
506
|
DrawerTitle.displayName = "DrawerTitle"
|
package/src/hooks/ui/index.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// UI Hooks exports
|
|
2
|
+
export * from './use-auto-limit-tags'
|
|
2
3
|
export * from './use-debounce'
|
|
4
|
+
export * from './use-header-height'
|
|
5
|
+
export * from './use-horizontal-scrollbar'
|
|
6
|
+
export * from './use-image-edge-color'
|
|
3
7
|
export * from './use-local-storage'
|
|
4
8
|
export * from './use-media-query'
|
|
5
9
|
export * from './use-memoized-callback'
|
|
10
|
+
export * from './use-notification-permission'
|
|
6
11
|
export * from './use-onboarding-state'
|
|
12
|
+
export * from './use-search'
|
|
7
13
|
export * from './use-table-pagination'
|
|
8
14
|
export * from './use-throttle'
|
|
9
15
|
export * from './use-window-size'
|
|
10
|
-
export * from './use-horizontal-scrollbar'
|
|
11
|
-
export * from './use-image-edge-color'
|
|
12
|
-
export * from './use-search'
|
|
13
|
-
export * from './use-auto-limit-tags'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the combined height (px) of the sticky page header and announcement
|
|
7
|
+
* bar (if present), updated live via ResizeObserver / MutationObserver.
|
|
8
|
+
* Useful for offsetting fixed/absolute-positioned panels so they don't
|
|
9
|
+
* overlap the header.
|
|
10
|
+
*/
|
|
11
|
+
export function useHeaderHeight(defaultHeight = 64): number {
|
|
12
|
+
const [height, setHeight] = useState(defaultHeight)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const measure = () => {
|
|
16
|
+
let total = 0
|
|
17
|
+
const header = document.querySelector('header')
|
|
18
|
+
if (header) total += header.offsetHeight
|
|
19
|
+
const bar = document.querySelector('[data-announcement-bar]')
|
|
20
|
+
if (bar instanceof HTMLElement) total += bar.offsetHeight
|
|
21
|
+
setHeight(total > 0 ? total : defaultHeight)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
measure()
|
|
25
|
+
|
|
26
|
+
const resizeObserver = new ResizeObserver(measure)
|
|
27
|
+
const header = document.querySelector('header')
|
|
28
|
+
if (header) resizeObserver.observe(header)
|
|
29
|
+
const bar = document.querySelector('[data-announcement-bar]')
|
|
30
|
+
if (bar) resizeObserver.observe(bar)
|
|
31
|
+
|
|
32
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
33
|
+
for (const mutation of mutations) {
|
|
34
|
+
if (mutation.type === 'childList' || mutation.type === 'attributes') {
|
|
35
|
+
measure()
|
|
36
|
+
const newBar = document.querySelector('[data-announcement-bar]')
|
|
37
|
+
if (newBar) resizeObserver.observe(newBar)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
mutationObserver.observe(document.body, {
|
|
42
|
+
childList: true,
|
|
43
|
+
subtree: true,
|
|
44
|
+
attributes: true,
|
|
45
|
+
attributeFilter: ['data-announcement-bar'],
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
resizeObserver.disconnect()
|
|
50
|
+
mutationObserver.disconnect()
|
|
51
|
+
}
|
|
52
|
+
}, [defaultHeight])
|
|
53
|
+
|
|
54
|
+
return height
|
|
55
|
+
}
|
|
@@ -17,6 +17,7 @@ export function useHorizontalScrollbar() {
|
|
|
17
17
|
const trackRef = useRef<HTMLDivElement>(null)
|
|
18
18
|
const thumbRef = useRef<HTMLDivElement>(null)
|
|
19
19
|
const roRef = useRef<ResizeObserver | null>(null)
|
|
20
|
+
const moRef = useRef<MutationObserver | null>(null)
|
|
20
21
|
const [thumbRatio, setThumbRatio] = useState(0)
|
|
21
22
|
|
|
22
23
|
// Edge-fade state
|
|
@@ -94,6 +95,10 @@ export function useHorizontalScrollbar() {
|
|
|
94
95
|
roRef.current.disconnect()
|
|
95
96
|
roRef.current = null
|
|
96
97
|
}
|
|
98
|
+
if (moRef.current) {
|
|
99
|
+
moRef.current.disconnect()
|
|
100
|
+
moRef.current = null
|
|
101
|
+
}
|
|
97
102
|
if (rafIdRef.current) {
|
|
98
103
|
cancelAnimationFrame(rafIdRef.current)
|
|
99
104
|
rafIdRef.current = 0
|
|
@@ -105,8 +110,20 @@ export function useHorizontalScrollbar() {
|
|
|
105
110
|
const ratio = node.clientWidth / node.scrollWidth
|
|
106
111
|
setThumbRatio(ratio >= 1 ? 0 : ratio)
|
|
107
112
|
|
|
108
|
-
|
|
109
|
-
roRef.current
|
|
113
|
+
const ro = new ResizeObserver(measure)
|
|
114
|
+
roRef.current = ro
|
|
115
|
+
const observeAll = () => {
|
|
116
|
+
ro.disconnect()
|
|
117
|
+
ro.observe(node)
|
|
118
|
+
for (const child of Array.from(node.children)) ro.observe(child)
|
|
119
|
+
}
|
|
120
|
+
observeAll()
|
|
121
|
+
|
|
122
|
+
moRef.current = new MutationObserver(() => {
|
|
123
|
+
observeAll()
|
|
124
|
+
measure()
|
|
125
|
+
})
|
|
126
|
+
moRef.current.observe(node, { childList: true })
|
|
110
127
|
|
|
111
128
|
requestAnimationFrame(() => {
|
|
112
129
|
syncThumbToDOM()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface UseNotificationPermissionResult {
|
|
6
|
+
/** Whether the page-context Notification API exists in this browser (false during SSR and on iOS Safari). */
|
|
7
|
+
supported: boolean
|
|
8
|
+
permission: NotificationPermission
|
|
9
|
+
/** Prompt the user for permission. Must be called from a user gesture or browsers will auto-deny. */
|
|
10
|
+
request: () => Promise<NotificationPermission>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tracks the browser's desktop-notification permission, staying in sync when
|
|
15
|
+
* the user changes the site setting externally (Permissions API change event
|
|
16
|
+
* where available, visibility/focus re-read as the Safari fallback).
|
|
17
|
+
*/
|
|
18
|
+
export function useNotificationPermission(): UseNotificationPermissionResult {
|
|
19
|
+
const [supported, setSupported] = useState(false)
|
|
20
|
+
const [permission, setPermission] = useState<NotificationPermission>('default')
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (typeof window === 'undefined' || !('Notification' in window)) return
|
|
24
|
+
setSupported(true)
|
|
25
|
+
const sync = () => setPermission(Notification.permission)
|
|
26
|
+
sync()
|
|
27
|
+
|
|
28
|
+
// The query resolves async; without the flag it would attach the listener after unmount.
|
|
29
|
+
let cancelled = false
|
|
30
|
+
let status: PermissionStatus | undefined
|
|
31
|
+
navigator.permissions
|
|
32
|
+
?.query({ name: 'notifications' as PermissionName })
|
|
33
|
+
.then((s) => {
|
|
34
|
+
if (cancelled) return
|
|
35
|
+
status = s
|
|
36
|
+
s.addEventListener('change', sync)
|
|
37
|
+
})
|
|
38
|
+
.catch(() => undefined)
|
|
39
|
+
|
|
40
|
+
document.addEventListener('visibilitychange', sync)
|
|
41
|
+
window.addEventListener('focus', sync)
|
|
42
|
+
return () => {
|
|
43
|
+
cancelled = true
|
|
44
|
+
status?.removeEventListener('change', sync)
|
|
45
|
+
document.removeEventListener('visibilitychange', sync)
|
|
46
|
+
window.removeEventListener('focus', sync)
|
|
47
|
+
}
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const request = useCallback(async () => {
|
|
51
|
+
if (typeof window === 'undefined' || !('Notification' in window)) {
|
|
52
|
+
return 'denied' as NotificationPermission
|
|
53
|
+
}
|
|
54
|
+
// Older Safari only implements the callback form; resolving from both is harmless.
|
|
55
|
+
const result = await new Promise<NotificationPermission>((resolve) => {
|
|
56
|
+
const maybePromise = Notification.requestPermission(resolve)
|
|
57
|
+
if (maybePromise && typeof maybePromise.then === 'function') void maybePromise.then(resolve)
|
|
58
|
+
})
|
|
59
|
+
setPermission(Notification.permission)
|
|
60
|
+
return result
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
return { supported, permission, request }
|
|
64
|
+
}
|