@orsetra/shared-ui 1.9.1 → 1.10.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/components/layout/index.ts +2 -1
- package/components/layout/layout-container.tsx +15 -1
- package/components/layout/sidebar/data.tsx +14 -3
- package/components/layout/sidebar/main-sidebar-flyout.tsx +57 -29
- package/components/layout/sidebar/main-sidebar.tsx +23 -10
- package/components/layout/sidebar/quick-access-tracker.tsx +36 -0
- package/hooks/index.ts +1 -0
- package/hooks/use-quick-access.ts +74 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Layout components exports
|
|
2
2
|
export { MainSidebar, type SidebarMode } from './sidebar/main-sidebar'
|
|
3
|
-
export { resolveIcon, type MainMenuItem, type SubMenuItem, type SidebarMenus, type MenuApiResponse, type ApiMenuData, type ApiMainMenuItem, type ApiSubMenuItem, type ApiSubMenu, type OrgInfo, type Project } from './sidebar/data'
|
|
3
|
+
export { resolveIcon, type MainMenuItem, type SubMenuItem, type SidebarMenus, type SidebarMenuMeta, type MenuApiResponse, type ApiMenuData, type ApiMainMenuItem, type ApiSubMenuItem, type ApiSubMenu, type OrgInfo, type Project } from './sidebar/data'
|
|
4
|
+
export { QuickAccessTracker } from './sidebar/quick-access-tracker'
|
|
4
5
|
export {
|
|
5
6
|
Sidebar,
|
|
6
7
|
SidebarContent,
|
|
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
|
|
|
4
4
|
import { usePathname, useSearchParams, useParams } from "next/navigation"
|
|
5
5
|
import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
|
|
6
6
|
import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
|
|
7
|
-
import { type SidebarMenus, type MainMenuItem, resolveIcon } from "./sidebar/data"
|
|
7
|
+
import { type SidebarMenus, type MainMenuItem, type SidebarMenuMeta, resolveIcon } from "./sidebar/data"
|
|
8
8
|
import { Button } from "../ui"
|
|
9
9
|
import { Logo } from "../ui/logo"
|
|
10
10
|
import { getMenuFromPath } from "../../lib/menu-utils"
|
|
@@ -50,6 +50,7 @@ function LayoutContent({
|
|
|
50
50
|
const [mainMenuItems, setMainMenuItems] = useState<MainMenuItem[]>([])
|
|
51
51
|
const [fetchedSidebarMenus, setFetchedSidebarMenus] = useState<SidebarMenus>({})
|
|
52
52
|
const [sectionLabels, setSectionLabels] = useState<Record<string, string>>({})
|
|
53
|
+
const [sidebarMenuMeta, setSidebarMenuMeta] = useState<Record<string, SidebarMenuMeta>>({})
|
|
53
54
|
|
|
54
55
|
useEffect(() => {
|
|
55
56
|
if (!fetchMenus) return
|
|
@@ -69,6 +70,7 @@ function LayoutContent({
|
|
|
69
70
|
|
|
70
71
|
const menus: SidebarMenus = {}
|
|
71
72
|
const labels: Record<string, string> = {}
|
|
73
|
+
const meta: Record<string, SidebarMenuMeta> = {}
|
|
72
74
|
Object.entries(subMenus).forEach(([key, subMenu]) => {
|
|
73
75
|
menus[key] = subMenu.items.map((item) => ({
|
|
74
76
|
id: item.id,
|
|
@@ -78,9 +80,20 @@ function LayoutContent({
|
|
|
78
80
|
icon: resolveIcon(item.icon),
|
|
79
81
|
}))
|
|
80
82
|
labels[key] = subMenu.header
|
|
83
|
+
meta[key] = {
|
|
84
|
+
header: subMenu.header,
|
|
85
|
+
description: subMenu.description,
|
|
86
|
+
links: subMenu.links?.map(l => ({
|
|
87
|
+
label: l.label,
|
|
88
|
+
href: l.href,
|
|
89
|
+
description: l.description,
|
|
90
|
+
icon: l.icon,
|
|
91
|
+
})),
|
|
92
|
+
}
|
|
81
93
|
})
|
|
82
94
|
setFetchedSidebarMenus(menus)
|
|
83
95
|
setSectionLabels(labels)
|
|
96
|
+
setSidebarMenuMeta(meta)
|
|
84
97
|
})
|
|
85
98
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
99
|
}, [])
|
|
@@ -148,6 +161,7 @@ function LayoutContent({
|
|
|
148
161
|
mode={isHidden ? "expanded" : mode}
|
|
149
162
|
sidebarMenus={effectiveSidebarMenus}
|
|
150
163
|
mainMenuItems={mainMenuItems}
|
|
164
|
+
sidebarMenuMeta={sidebarMenuMeta}
|
|
151
165
|
/>
|
|
152
166
|
|
|
153
167
|
<main className="flex-1 bg-ibm-gray-10 min-h-0 overflow-auto">
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
ArrowRightLeft,
|
|
37
37
|
ArrowLeftRight,
|
|
38
38
|
Puzzle,
|
|
39
|
+
LifeBuoy,
|
|
39
40
|
} from "lucide-react"
|
|
40
41
|
|
|
41
42
|
export interface MainMenuItem {
|
|
@@ -88,9 +89,18 @@ export interface ApiSubMenuItem {
|
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
export interface ApiSubMenu {
|
|
91
|
-
id:
|
|
92
|
-
header:
|
|
93
|
-
|
|
92
|
+
id: string
|
|
93
|
+
header: string
|
|
94
|
+
description?: string
|
|
95
|
+
/** Resource/doc links shown in the flyout default state (distinct from nav items) */
|
|
96
|
+
links?: ApiSubMenuItem[]
|
|
97
|
+
items: ApiSubMenuItem[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SidebarMenuMeta {
|
|
101
|
+
header?: string
|
|
102
|
+
description?: string
|
|
103
|
+
links?: Array<{ label: string; href: string; description?: string; icon?: string }>
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
export interface ApiMenuData {
|
|
@@ -150,6 +160,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
|
|
150
160
|
FileText,
|
|
151
161
|
Code,
|
|
152
162
|
Cloud,
|
|
163
|
+
LifeBuoy,
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
export function resolveIcon(name: string): LucideIcon {
|
|
@@ -3,24 +3,27 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { cn } from "../../../lib/utils"
|
|
5
5
|
import type { MainMenuItem, SubMenuItem } from "./data"
|
|
6
|
+
import { resolveIcon } from "./data"
|
|
6
7
|
import { Home, ExternalLink, type LucideIcon } from "lucide-react"
|
|
8
|
+
import type { QuickAccessItem } from "../../../hooks/use-quick-access"
|
|
7
9
|
|
|
8
10
|
export interface FlyoutDefaultLink {
|
|
9
11
|
label: string
|
|
10
12
|
href: string
|
|
11
13
|
description?: string
|
|
12
|
-
icon?: LucideIcon
|
|
14
|
+
icon?: LucideIcon | string
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
interface MainSidebarFlyoutProps {
|
|
16
|
-
item:
|
|
17
|
-
subItems:
|
|
18
|
-
onMouseEnter:
|
|
19
|
-
onMouseLeave:
|
|
20
|
-
main_base_url?:
|
|
21
|
-
defaultTitle?:
|
|
22
|
-
defaultDescription?:
|
|
23
|
-
defaultLinks?:
|
|
18
|
+
item: MainMenuItem | null
|
|
19
|
+
subItems: SubMenuItem[]
|
|
20
|
+
onMouseEnter: () => void
|
|
21
|
+
onMouseLeave: () => void
|
|
22
|
+
main_base_url?: string
|
|
23
|
+
defaultTitle?: string
|
|
24
|
+
defaultDescription?: string
|
|
25
|
+
defaultLinks?: FlyoutDefaultLink[]
|
|
26
|
+
quickAccessItems?: QuickAccessItem[]
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function MainSidebarFlyout({
|
|
@@ -32,6 +35,7 @@ export function MainSidebarFlyout({
|
|
|
32
35
|
defaultTitle = "Camel X Platform",
|
|
33
36
|
defaultDescription,
|
|
34
37
|
defaultLinks = [],
|
|
38
|
+
quickAccessItems = [],
|
|
35
39
|
}: MainSidebarFlyoutProps) {
|
|
36
40
|
const homePath = React.useMemo(() => {
|
|
37
41
|
if (!main_base_url) return "/"
|
|
@@ -44,6 +48,8 @@ export function MainSidebarFlyout({
|
|
|
44
48
|
}
|
|
45
49
|
}, [main_base_url])
|
|
46
50
|
|
|
51
|
+
const hasQuickAccess = quickAccessItems.length > 0
|
|
52
|
+
|
|
47
53
|
return (
|
|
48
54
|
<div
|
|
49
55
|
onMouseEnter={onMouseEnter}
|
|
@@ -91,48 +97,70 @@ export function MainSidebarFlyout({
|
|
|
91
97
|
})}
|
|
92
98
|
</nav>
|
|
93
99
|
</>
|
|
100
|
+
) : hasQuickAccess ? (
|
|
101
|
+
/* ── Quick access: pages recently visited ────────────────────────── */
|
|
102
|
+
<div className="flex flex-col h-full items-center justify-center px-6 gap-6">
|
|
103
|
+
<div className="w-full">
|
|
104
|
+
<p className="px-2 pb-1 text-[10px] font-semibold text-text-secondary uppercase tracking-widest">
|
|
105
|
+
Accès rapide
|
|
106
|
+
</p>
|
|
107
|
+
<nav className="space-y-0.5">
|
|
108
|
+
{quickAccessItems.map((qa) => {
|
|
109
|
+
const QAIcon = qa.icon ? resolveIcon(qa.icon) : null
|
|
110
|
+
return (
|
|
111
|
+
<a
|
|
112
|
+
key={qa.id}
|
|
113
|
+
href={qa.href}
|
|
114
|
+
className="flex items-center gap-x-3 px-2 py-2 border-l-4 border-transparent hover:bg-ui-background hover:border-interactive transition-colors"
|
|
115
|
+
>
|
|
116
|
+
{QAIcon
|
|
117
|
+
? <QAIcon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
|
|
118
|
+
: <span className="h-4 w-4 flex-shrink-0" />
|
|
119
|
+
}
|
|
120
|
+
<p className="text-sm font-medium text-text-primary leading-snug truncate">{qa.label}</p>
|
|
121
|
+
</a>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</nav>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
94
127
|
) : (
|
|
95
|
-
/* ── Default state:
|
|
96
|
-
<div className="flex flex-col h-full">
|
|
97
|
-
{/*
|
|
98
|
-
<div className="
|
|
99
|
-
<
|
|
100
|
-
<div className="h-8 w-8 rounded-md bg-interactive flex items-center justify-center flex-shrink-0">
|
|
101
|
-
<Home className="h-4 w-4 text-white" />
|
|
102
|
-
</div>
|
|
103
|
-
<span className="text-sm font-semibold text-text-primary truncate">{defaultTitle}</span>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
128
|
+
/* ── Default state: centré verticalement ────────────────────────── */
|
|
129
|
+
<div className="flex flex-col h-full items-center justify-center px-6 gap-6">
|
|
130
|
+
{/* Titre + texte + CTA */}
|
|
131
|
+
<div className="flex flex-col items-center gap-3 w-full">
|
|
132
|
+
<span className="text-sm font-semibold text-text-primary text-center">{defaultTitle}</span>
|
|
106
133
|
{defaultDescription && (
|
|
107
|
-
<p className="text-xs text-text-secondary leading-relaxed
|
|
108
|
-
{defaultDescription}
|
|
109
|
-
</p>
|
|
134
|
+
<p className="text-xs text-text-secondary text-center leading-relaxed">{defaultDescription}</p>
|
|
110
135
|
)}
|
|
111
|
-
|
|
112
136
|
<a
|
|
113
137
|
href={homePath}
|
|
114
|
-
className="flex items-center justify-center gap-2 w-full px-3 py-2
|
|
138
|
+
className="flex items-center justify-center gap-2 w-full px-3 py-2 border border-interactive text-interactive hover:bg-interactive/10 text-sm font-medium transition-colors"
|
|
115
139
|
>
|
|
116
140
|
<Home className="h-4 w-4 flex-shrink-0" />
|
|
117
141
|
Tableau de bord
|
|
118
142
|
</a>
|
|
119
143
|
</div>
|
|
120
144
|
|
|
121
|
-
{/*
|
|
145
|
+
{/* Liens ressources */}
|
|
122
146
|
{defaultLinks.length > 0 && (
|
|
123
|
-
<nav className="
|
|
147
|
+
<nav className="w-full space-y-0.5">
|
|
124
148
|
<p className="px-2 pb-1 text-[10px] font-semibold text-text-secondary uppercase tracking-widest">
|
|
125
149
|
Ressources
|
|
126
150
|
</p>
|
|
127
151
|
{defaultLinks.map((link, i) => {
|
|
128
|
-
const LinkIcon = link.icon ?? ExternalLink
|
|
129
152
|
const isExternal = link.href.startsWith("http://") || link.href.startsWith("https://")
|
|
153
|
+
const LinkIcon: LucideIcon = !link.icon
|
|
154
|
+
? ExternalLink
|
|
155
|
+
: typeof link.icon === "string"
|
|
156
|
+
? resolveIcon(link.icon)
|
|
157
|
+
: link.icon
|
|
130
158
|
return (
|
|
131
159
|
<a
|
|
132
160
|
key={i}
|
|
133
161
|
href={link.href}
|
|
134
162
|
{...(isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
135
|
-
className="flex items-start gap-x-3 px-2
|
|
163
|
+
className="flex items-start gap-x-3 px-2 py-2 border-l-4 border-transparent hover:bg-ui-background hover:border-interactive transition-colors"
|
|
136
164
|
>
|
|
137
165
|
<LinkIcon className="h-4 w-4 flex-shrink-0 mt-0.5 text-text-secondary" />
|
|
138
166
|
<div className="min-w-0 flex-1">
|
|
@@ -4,10 +4,13 @@ import * as React from "react"
|
|
|
4
4
|
import { cn } from "../../../lib/utils"
|
|
5
5
|
import { Button } from "../../ui/button"
|
|
6
6
|
import { Logo } from "../../ui/logo"
|
|
7
|
-
import { type MainMenuItem, type SidebarMenus } from "./data"
|
|
7
|
+
import { type MainMenuItem, type SidebarMenus, type SidebarMenuMeta } from "./data"
|
|
8
|
+
|
|
9
|
+
export type { SidebarMenuMeta }
|
|
8
10
|
import { X, Menu, ChevronDown, ChevronRight } from "lucide-react"
|
|
9
11
|
import { useIsMobile } from "../../../hooks/use-mobile"
|
|
10
12
|
import { MainSidebarFlyout, type FlyoutDefaultLink } from "./main-sidebar-flyout"
|
|
13
|
+
import { useQuickAccess } from "../../../hooks/use-quick-access"
|
|
11
14
|
|
|
12
15
|
export type { FlyoutDefaultLink }
|
|
13
16
|
|
|
@@ -23,9 +26,8 @@ interface MainSidebarProps {
|
|
|
23
26
|
onSecondarySidebarOpen?: () => void
|
|
24
27
|
sidebarMenus?: SidebarMenus
|
|
25
28
|
mainMenuItems?: MainMenuItem[]
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
flyoutDefaultLinks?: FlyoutDefaultLink[]
|
|
29
|
+
/** Per-submenu metadata (header + description) derived from the API menu response. */
|
|
30
|
+
sidebarMenuMeta?: Record<string, SidebarMenuMeta>
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function MainSidebar({
|
|
@@ -38,12 +40,11 @@ export function MainSidebar({
|
|
|
38
40
|
onSecondarySidebarOpen,
|
|
39
41
|
sidebarMenus = {},
|
|
40
42
|
mainMenuItems = [],
|
|
41
|
-
|
|
42
|
-
flyoutDefaultDescription,
|
|
43
|
-
flyoutDefaultLinks = [],
|
|
43
|
+
sidebarMenuMeta = {},
|
|
44
44
|
}: MainSidebarProps) {
|
|
45
45
|
const isMinimized = mode === 'minimized'
|
|
46
46
|
const isMobile = useIsMobile()
|
|
47
|
+
const { items: quickAccessItems } = useQuickAccess()
|
|
47
48
|
|
|
48
49
|
// Accordion — mobile only
|
|
49
50
|
const [expandedMenu, setExpandedMenu] = React.useState<string | null>(null)
|
|
@@ -153,6 +154,17 @@ export function MainSidebar({
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// Default flyout content derived from the menu metadata
|
|
158
|
+
const mainMeta = sidebarMenuMeta["main"] ?? {}
|
|
159
|
+
const defaultTitle = mainMeta.header ?? "Camel X Platform"
|
|
160
|
+
const defaultDesc = mainMeta.description ?? undefined
|
|
161
|
+
const defaultLinks: FlyoutDefaultLink[] = (mainMeta.links ?? []).map(l => ({
|
|
162
|
+
label: l.label,
|
|
163
|
+
href: l.href,
|
|
164
|
+
description: l.description,
|
|
165
|
+
icon: l.icon,
|
|
166
|
+
}))
|
|
167
|
+
|
|
156
168
|
const handleSubMenuClick = (e: React.MouseEvent, menuId: string, href: string) => {
|
|
157
169
|
e.preventDefault()
|
|
158
170
|
window.location.href = buildSubItemHref(menuId, href)
|
|
@@ -302,9 +314,10 @@ export function MainSidebar({
|
|
|
302
314
|
onMouseEnter={cancelCloseTimer}
|
|
303
315
|
onMouseLeave={startCloseTimer}
|
|
304
316
|
main_base_url={main_base_url}
|
|
305
|
-
defaultTitle={
|
|
306
|
-
defaultDescription={
|
|
307
|
-
defaultLinks={
|
|
317
|
+
defaultTitle={defaultTitle}
|
|
318
|
+
defaultDescription={defaultDesc}
|
|
319
|
+
defaultLinks={defaultLinks}
|
|
320
|
+
quickAccessItems={quickAccessItems}
|
|
308
321
|
/>
|
|
309
322
|
)}
|
|
310
323
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
import { useQuickAccess } from "../../../hooks/use-quick-access"
|
|
6
|
+
|
|
7
|
+
interface QuickAccessTrackerProps {
|
|
8
|
+
/** Human-readable label shown in the quick-access list */
|
|
9
|
+
label: string
|
|
10
|
+
/** Icon name (resolved via resolveIcon). Defaults to "Box". */
|
|
11
|
+
icon?: string
|
|
12
|
+
/** Set to false to skip tracking this page (e.g. list/index pages) */
|
|
13
|
+
enabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drop this inside any page or layout to register that page in the
|
|
18
|
+
* sidebar's quick-access list. Tracks on mount and whenever pathname changes.
|
|
19
|
+
* Renders nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function QuickAccessTracker({ label, icon, enabled = true }: QuickAccessTrackerProps) {
|
|
22
|
+
const pathname = usePathname()
|
|
23
|
+
const { addItem } = useQuickAccess()
|
|
24
|
+
// Stable ref so the effect doesn't re-run when addItem reference changes
|
|
25
|
+
const addRef = useRef(addItem)
|
|
26
|
+
addRef.current = addItem
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!enabled || !label || !pathname) return
|
|
30
|
+
addRef.current({ label, href: pathname, icon })
|
|
31
|
+
// Re-track whenever the actual page changes
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [pathname, label, icon, enabled])
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
package/hooks/index.ts
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react"
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = "sidebar:quick-access"
|
|
6
|
+
const MAX_ITEMS = 4
|
|
7
|
+
|
|
8
|
+
export interface QuickAccessItem {
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
href: string
|
|
12
|
+
icon?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readStorage(): QuickAccessItem[] {
|
|
16
|
+
if (typeof window === "undefined") return []
|
|
17
|
+
try {
|
|
18
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
19
|
+
if (!raw) return []
|
|
20
|
+
const parsed = JSON.parse(raw)
|
|
21
|
+
return Array.isArray(parsed)
|
|
22
|
+
? parsed.filter(
|
|
23
|
+
(i): i is QuickAccessItem =>
|
|
24
|
+
i && typeof i.id === "string" && typeof i.label === "string" && typeof i.href === "string"
|
|
25
|
+
)
|
|
26
|
+
: []
|
|
27
|
+
} catch {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeStorage(items: QuickAccessItem[]): void {
|
|
33
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)) } catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useQuickAccess() {
|
|
37
|
+
const [items, setItems] = useState<QuickAccessItem[]>(readStorage)
|
|
38
|
+
|
|
39
|
+
// Sync across tabs
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const onStorage = (e: StorageEvent) => {
|
|
42
|
+
if (e.key === STORAGE_KEY) setItems(readStorage())
|
|
43
|
+
}
|
|
44
|
+
window.addEventListener("storage", onStorage)
|
|
45
|
+
return () => window.removeEventListener("storage", onStorage)
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const addItem = useCallback((item: Omit<QuickAccessItem, "id"> & { id?: string }) => {
|
|
49
|
+
setItems(prev => {
|
|
50
|
+
const id = item.id ?? item.href
|
|
51
|
+
const next = [
|
|
52
|
+
{ id, label: item.label, href: item.href, icon: item.icon },
|
|
53
|
+
...prev.filter(i => i.href !== item.href),
|
|
54
|
+
].slice(0, MAX_ITEMS)
|
|
55
|
+
writeStorage(next)
|
|
56
|
+
return next
|
|
57
|
+
})
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const removeItem = useCallback((href: string) => {
|
|
61
|
+
setItems(prev => {
|
|
62
|
+
const next = prev.filter(i => i.href !== href)
|
|
63
|
+
writeStorage(next)
|
|
64
|
+
return next
|
|
65
|
+
})
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const clearItems = useCallback(() => {
|
|
69
|
+
setItems([])
|
|
70
|
+
try { localStorage.removeItem(STORAGE_KEY) } catch {}
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
return { items, addItem, removeItem, clearItems }
|
|
74
|
+
}
|