@veiag/payload-enhanced-sidebar 0.1.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.
Files changed (53) hide show
  1. package/README.md +231 -0
  2. package/dist/components/EnhancedSidebar/CustomTabContent.d.ts +6 -0
  3. package/dist/components/EnhancedSidebar/CustomTabContent.js +35 -0
  4. package/dist/components/EnhancedSidebar/CustomTabContent.js.map +1 -0
  5. package/dist/components/EnhancedSidebar/Icon.d.ts +8 -0
  6. package/dist/components/EnhancedSidebar/Icon.js +16 -0
  7. package/dist/components/EnhancedSidebar/Icon.js.map +1 -0
  8. package/dist/components/EnhancedSidebar/NavHamburger/index.d.ts +4 -0
  9. package/dist/components/EnhancedSidebar/NavHamburger/index.js +18 -0
  10. package/dist/components/EnhancedSidebar/NavHamburger/index.js.map +1 -0
  11. package/dist/components/EnhancedSidebar/SidebarContent.d.ts +11 -0
  12. package/dist/components/EnhancedSidebar/SidebarContent.js +140 -0
  13. package/dist/components/EnhancedSidebar/SidebarContent.js.map +1 -0
  14. package/dist/components/EnhancedSidebar/SidebarWrapper/index.d.ts +6 -0
  15. package/dist/components/EnhancedSidebar/SidebarWrapper/index.js +33 -0
  16. package/dist/components/EnhancedSidebar/SidebarWrapper/index.js.map +1 -0
  17. package/dist/components/EnhancedSidebar/SidebarWrapper/index.scss +63 -0
  18. package/dist/components/EnhancedSidebar/TabsBar/index.d.ts +9 -0
  19. package/dist/components/EnhancedSidebar/TabsBar/index.js +85 -0
  20. package/dist/components/EnhancedSidebar/TabsBar/index.js.map +1 -0
  21. package/dist/components/EnhancedSidebar/TabsBar/index.scss +108 -0
  22. package/dist/components/EnhancedSidebar/getNavPrefs.d.ts +2 -0
  23. package/dist/components/EnhancedSidebar/getNavPrefs.js +31 -0
  24. package/dist/components/EnhancedSidebar/getNavPrefs.js.map +1 -0
  25. package/dist/components/EnhancedSidebar/index.client.d.ts +7 -0
  26. package/dist/components/EnhancedSidebar/index.client.js +94 -0
  27. package/dist/components/EnhancedSidebar/index.client.js.map +1 -0
  28. package/dist/components/EnhancedSidebar/index.d.ts +10 -0
  29. package/dist/components/EnhancedSidebar/index.js +65 -0
  30. package/dist/components/EnhancedSidebar/index.js.map +1 -0
  31. package/dist/components/EnhancedSidebar/index.scss +101 -0
  32. package/dist/exports/client.d.ts +0 -0
  33. package/dist/exports/client.js +3 -0
  34. package/dist/exports/client.js.map +1 -0
  35. package/dist/exports/rsc.d.ts +1 -0
  36. package/dist/exports/rsc.js +3 -0
  37. package/dist/exports/rsc.js.map +1 -0
  38. package/dist/hooks/useMutationObserver.d.ts +1 -0
  39. package/dist/hooks/useMutationObserver.js +21 -0
  40. package/dist/hooks/useMutationObserver.js.map +1 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +64 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/translations/index.d.ts +22 -0
  45. package/dist/translations/index.js +30 -0
  46. package/dist/translations/index.js.map +1 -0
  47. package/dist/types.d.ts +132 -0
  48. package/dist/types.js +3 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/utils/index.d.ts +3 -0
  51. package/dist/utils/index.js +14 -0
  52. package/dist/utils/index.js.map +1 -0
  53. package/package.json +95 -0
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # Payload Enhanced Sidebar
2
+
3
+ An enhanced sidebar plugin for [Payload CMS](https://payloadcms.com) that adds a tabbed navigation system to organize collections and globals into logical groups.
4
+
5
+ > **Note:** This plugin is in early development and has not been extensively tested. Use with caution in production environments.
6
+
7
+ ## Features
8
+
9
+ - **Tabbed Navigation** - Organize collections into separate tabs for cleaner navigation
10
+ - **Vertical Tab Bar** - Icon-based tabs on the left side of the sidebar
11
+ - **Link Support** - Add navigation links (like Dashboard) alongside tabs
12
+ - **Custom Items** - Add custom navigation items that can be merged into existing groups
13
+ - **i18n Support** - Full localization support for labels and groups
14
+ - **Lucide Icons** - Use any [Lucide icon](https://lucide.dev/icons) for tabs and links
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @veiag/payload-enhanced-sidebar
20
+ # or
21
+ yarn add @veiag/payload-enhanced-sidebar
22
+ # or
23
+ pnpm add @veiag/payload-enhanced-sidebar
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { payloadEnhancedSidebar } from '@veiag/payload-enhanced-sidebar'
30
+ import { buildConfig } from 'payload'
31
+
32
+ export default buildConfig({
33
+ // ... your config
34
+ plugins: [
35
+ payloadEnhancedSidebar({
36
+ // Works with defaults!
37
+ }),
38
+ ],
39
+ })
40
+ ```
41
+
42
+ This will add:
43
+ - A Dashboard link at the top
44
+ - A default tab showing all collections and globals
45
+ - A logout button at the bottom
46
+
47
+ ## Configuration
48
+
49
+ ### Full Configuration Example
50
+
51
+ ```typescript
52
+ import { payloadEnhancedSidebar } from '@veiag/payload-enhanced-sidebar'
53
+ import { buildConfig } from 'payload'
54
+
55
+ export default buildConfig({
56
+ plugins: [
57
+ payloadEnhancedSidebar({
58
+ // Tabs and links in the sidebar
59
+ tabs: [
60
+ // Dashboard link
61
+ {
62
+ id: 'dashboard',
63
+ type: 'link',
64
+ href: '/',
65
+ icon: 'House',
66
+ label: { en: 'Dashboard', uk: 'Головна' },
67
+ },
68
+ // Content tab - shows specific collections
69
+ {
70
+ id: 'content',
71
+ type: 'tab',
72
+ icon: 'FileText',
73
+ label: { en: 'Content', uk: 'Контент' },
74
+ collections: ['posts', 'pages', 'categories'],
75
+ },
76
+ // E-commerce tab with custom items
77
+ {
78
+ id: 'ecommerce',
79
+ type: 'tab',
80
+ icon: 'ShoppingCart',
81
+ label: { en: 'E-commerce', uk: 'E-commerce' },
82
+ collections: ['products', 'orders', 'customers'],
83
+ customItems: [
84
+ {
85
+ slug: 'analytics',
86
+ href: '/analytics',
87
+ label: { en: 'Analytics', uk: 'Аналітика' },
88
+ group: 'E-commerce', // Merge into existing group
89
+ },
90
+ ],
91
+ },
92
+ // Settings tab with globals
93
+ {
94
+ id: 'settings',
95
+ type: 'tab',
96
+ icon: 'Settings',
97
+ label: { en: 'Settings', uk: 'Налаштування' },
98
+ collections: ['users'],
99
+ globals: ['site-settings', 'footer-settings'],
100
+ customItems: [
101
+ {
102
+ slug: 'api-keys',
103
+ href: '/api-keys',
104
+ label: { en: 'API Keys', uk: 'API Ключі' },
105
+ // No group - will appear at the bottom
106
+ },
107
+ ],
108
+ },
109
+ ],
110
+
111
+ // Show/hide logout button (default: true)
112
+ showLogout: true,
113
+
114
+ // Disable the plugin
115
+ disabled: false,
116
+ }),
117
+ ],
118
+ })
119
+ ```
120
+
121
+ ## Configuration Options
122
+
123
+ ### `tabs`
124
+
125
+ Array of tabs and links to show in the sidebar.
126
+
127
+ **Tab (`type: 'tab'`)**
128
+
129
+ | Property | Type | Required | Description |
130
+ |----------|------|----------|-------------|
131
+ | `id` | `string` | Yes | Unique identifier |
132
+ | `type` | `'tab'` | Yes | Tab type |
133
+ | `icon` | `string` | Yes | Lucide icon name |
134
+ | `label` | `LocalizedString` | Yes | Tab tooltip/label |
135
+ | `collections` | `CollectionSlug[]` | No | Collections to show in this tab |
136
+ | `globals` | `GlobalSlug[]` | No | Globals to show in this tab |
137
+ | `customItems` | `SidebarTabItem[]` | No | Custom navigation items |
138
+
139
+ > If neither `collections` nor `globals` are specified, the tab shows all collections and globals.
140
+
141
+ **Link (`type: 'link'`)**
142
+
143
+ | Property | Type | Required | Description |
144
+ |----------|------|----------|-------------|
145
+ | `id` | `string` | Yes | Unique identifier |
146
+ | `type` | `'link'` | Yes | Link type |
147
+ | `icon` | `string` | Yes | Lucide icon name |
148
+ | `label` | `LocalizedString` | Yes | Link tooltip/label |
149
+ | `href` | `string` | Yes | URL (relative to admin route) |
150
+
151
+ ### `customItems`
152
+
153
+ Custom items can be added to any tab:
154
+
155
+ ```typescript
156
+ {
157
+ slug: 'unique-slug', // Required: unique identifier
158
+ href: '/path', // Required: URL (relative to admin route)
159
+ label: { en: 'Label' }, // Required: display label
160
+ group: { en: 'Group Name' }, // Optional: merge into existing group or create new
161
+ isExternal: true, // Optional: if true, href is absolute URL
162
+ }
163
+ ```
164
+
165
+ **Group behavior:**
166
+ - If `group` matches an existing collection group label, the item is added to that group
167
+ - If `group` doesn't match any existing group, a new group is created
168
+ - If `group` is not specified, the item appears at the bottom as ungrouped
169
+
170
+ ### `showLogout`
171
+
172
+ Show/hide the logout button at the bottom of the tabs bar.
173
+
174
+ - **Type:** `boolean`
175
+ - **Default:** `true`
176
+
177
+ ### `disabled`
178
+
179
+ Completely disable the plugin.
180
+
181
+ - **Type:** `boolean`
182
+ - **Default:** `false`
183
+
184
+ ## Localization
185
+
186
+ All labels support localized strings:
187
+
188
+ ```typescript
189
+ label: 'Simple string'
190
+ // or
191
+ label: {
192
+ en: 'English',
193
+ uk: 'Українська',
194
+ de: 'Deutsch',
195
+ }
196
+ ```
197
+
198
+ ## TODO
199
+
200
+ The following features are planned but not yet implemented:
201
+
202
+ - [ ] **Browse by Folder Button** - Support for the folder view button (requires Payload v3.41.0+)
203
+ - [ ] **Settings Menu Items** - Support for Payload's SettingsMenu items (requires Payload v3.60.0+)
204
+
205
+ ## Contributing
206
+
207
+ Contributions are welcome! Please feel free to submit a Pull Request.
208
+
209
+ 1. Fork the repository
210
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
211
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
212
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
213
+ 5. Open a Pull Request
214
+
215
+ ## Issues
216
+
217
+ Found a bug or have a feature request? Please open an issue on [GitHub](https://github.com/VeiaG/payload-enhanced-sidebar/issues).
218
+
219
+ ## License
220
+
221
+ MIT © [VeiaG](https://github.com/VeiaG)
222
+
223
+ ## Links
224
+
225
+ - [GitHub Repository](https://github.com/VeiaG/payload-enhanced-sidebar)
226
+ - [Payload CMS](https://payloadcms.com)
227
+ - [Lucide Icons](https://lucide.dev/icons)
228
+
229
+ ---
230
+
231
+ ### More plugins and Payload resources at [PayloadCMS Extensions](https://payload.veiag.dev/)
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import type { SidebarTabItem } from '../../types';
3
+ export type CustomTabContentProps = {
4
+ items: SidebarTabItem[];
5
+ };
6
+ export declare const CustomTabContent: React.FC<CustomTabContentProps>;
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { getTranslation } from '@payloadcms/translations';
4
+ import { Link, useConfig, useTranslation } from '@payloadcms/ui';
5
+ import { usePathname } from 'next/navigation.js';
6
+ import { formatAdminURL } from 'payload/shared';
7
+ import React from 'react';
8
+ const baseClass = 'enhanced-sidebar';
9
+ export const CustomTabContent = ({ items })=>{
10
+ const { i18n } = useTranslation();
11
+ const pathname = usePathname();
12
+ const { config: { routes: { admin: adminRoute } } } = useConfig();
13
+ return /*#__PURE__*/ _jsx("div", {
14
+ className: `${baseClass}__custom-items`,
15
+ children: items.map((item)=>{
16
+ const href = item.isExternal ? item.href : formatAdminURL({
17
+ adminRoute,
18
+ path: item.href
19
+ });
20
+ const label = getTranslation(item.label, i18n);
21
+ const isActive = pathname.startsWith(href);
22
+ return /*#__PURE__*/ _jsx(Link, {
23
+ className: `${baseClass}__link ${isActive ? `${baseClass}__link--active` : ''}`,
24
+ href: href,
25
+ prefetch: false,
26
+ children: /*#__PURE__*/ _jsx("span", {
27
+ className: `${baseClass}__link-label`,
28
+ children: label
29
+ })
30
+ }, item.slug);
31
+ })
32
+ });
33
+ };
34
+
35
+ //# sourceMappingURL=CustomTabContent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/EnhancedSidebar/CustomTabContent.tsx"],"sourcesContent":["'use client'\n\nimport { getTranslation } from '@payloadcms/translations'\nimport { Link, useConfig, useTranslation } from '@payloadcms/ui'\nimport { usePathname } from 'next/navigation.js'\nimport { formatAdminURL } from 'payload/shared'\nimport React from 'react'\n\nimport type { SidebarTabItem } from '../../types'\n\nconst baseClass = 'enhanced-sidebar'\n\nexport type CustomTabContentProps = {\n items: SidebarTabItem[]\n}\n\nexport const CustomTabContent: React.FC<CustomTabContentProps> = ({ items }) => {\n const { i18n } = useTranslation()\n const pathname = usePathname()\n\n const {\n config: {\n routes: { admin: adminRoute },\n },\n } = useConfig()\n\n return (\n <div className={`${baseClass}__custom-items`}>\n {items.map((item) => {\n const href = item.isExternal ? item.href : formatAdminURL({ adminRoute, path: item.href })\n const label = getTranslation(item.label, i18n)\n const isActive = pathname.startsWith(href)\n\n return (\n <Link\n className={`${baseClass}__link ${isActive ? `${baseClass}__link--active` : ''}`}\n href={href}\n key={item.slug}\n prefetch={false}\n >\n <span className={`${baseClass}__link-label`}>{label}</span>\n </Link>\n )\n })}\n </div>\n )\n}\n"],"names":["getTranslation","Link","useConfig","useTranslation","usePathname","formatAdminURL","React","baseClass","CustomTabContent","items","i18n","pathname","config","routes","admin","adminRoute","div","className","map","item","href","isExternal","path","label","isActive","startsWith","prefetch","span","slug"],"mappings":"AAAA;;AAEA,SAASA,cAAc,QAAQ,2BAA0B;AACzD,SAASC,IAAI,EAAEC,SAAS,EAAEC,cAAc,QAAQ,iBAAgB;AAChE,SAASC,WAAW,QAAQ,qBAAoB;AAChD,SAASC,cAAc,QAAQ,iBAAgB;AAC/C,OAAOC,WAAW,QAAO;AAIzB,MAAMC,YAAY;AAMlB,OAAO,MAAMC,mBAAoD,CAAC,EAAEC,KAAK,EAAE;IACzE,MAAM,EAAEC,IAAI,EAAE,GAAGP;IACjB,MAAMQ,WAAWP;IAEjB,MAAM,EACJQ,QAAQ,EACNC,QAAQ,EAAEC,OAAOC,UAAU,EAAE,EAC9B,EACF,GAAGb;IAEJ,qBACE,KAACc;QAAIC,WAAW,GAAGV,UAAU,cAAc,CAAC;kBACzCE,MAAMS,GAAG,CAAC,CAACC;YACV,MAAMC,OAAOD,KAAKE,UAAU,GAAGF,KAAKC,IAAI,GAAGf,eAAe;gBAAEU;gBAAYO,MAAMH,KAAKC,IAAI;YAAC;YACxF,MAAMG,QAAQvB,eAAemB,KAAKI,KAAK,EAAEb;YACzC,MAAMc,WAAWb,SAASc,UAAU,CAACL;YAErC,qBACE,KAACnB;gBACCgB,WAAW,GAAGV,UAAU,OAAO,EAAEiB,WAAW,GAAGjB,UAAU,cAAc,CAAC,GAAG,IAAI;gBAC/Ea,MAAMA;gBAENM,UAAU;0BAEV,cAAA,KAACC;oBAAKV,WAAW,GAAGV,UAAU,YAAY,CAAC;8BAAGgB;;eAHzCJ,KAAKS,IAAI;QAMpB;;AAGN,EAAC"}
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { IconName } from '../../types';
3
+ export interface IconProps {
4
+ className?: string;
5
+ name: IconName;
6
+ size?: number;
7
+ }
8
+ export declare const Icon: React.FC<IconProps>;
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { icons } from 'lucide-react';
4
+ import React from 'react';
5
+ export const Icon = ({ name, className, size = 20 })=>{
6
+ const LucideIcon = icons[name];
7
+ if (!LucideIcon) {
8
+ return null;
9
+ }
10
+ return /*#__PURE__*/ _jsx(LucideIcon, {
11
+ className: className,
12
+ size: size
13
+ });
14
+ };
15
+
16
+ //# sourceMappingURL=Icon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/EnhancedSidebar/Icon.tsx"],"sourcesContent":["'use client'\n\nimport { icons } from 'lucide-react'\nimport React from 'react'\n\nimport type { IconName } from '../../types'\n\nexport interface IconProps {\n className?: string\n name: IconName\n size?: number\n}\n\nexport const Icon: React.FC<IconProps> = ({ name, className, size = 20 }) => {\n const LucideIcon = icons[name]\n\n if (!LucideIcon) {\n return null\n }\n\n return <LucideIcon className={className} size={size} />\n}\n"],"names":["icons","React","Icon","name","className","size","LucideIcon"],"mappings":"AAAA;;AAEA,SAASA,KAAK,QAAQ,eAAc;AACpC,OAAOC,WAAW,QAAO;AAUzB,OAAO,MAAMC,OAA4B,CAAC,EAAEC,IAAI,EAAEC,SAAS,EAAEC,OAAO,EAAE,EAAE;IACtE,MAAMC,aAAaN,KAAK,CAACG,KAAK;IAE9B,IAAI,CAACG,YAAY;QACf,OAAO;IACT;IAEA,qBAAO,KAACA;QAAWF,WAAWA;QAAWC,MAAMA;;AACjD,EAAC"}
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare const NavHamburger: React.FC<{
3
+ baseClass: string;
4
+ }>;
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Hamburger, useNav } from '@payloadcms/ui';
4
+ import React from 'react';
5
+ export const NavHamburger = ({ baseClass })=>{
6
+ const { navOpen, setNavOpen } = useNav();
7
+ return /*#__PURE__*/ _jsx("button", {
8
+ className: `${baseClass}__mobile-close`,
9
+ onClick: ()=>setNavOpen(false),
10
+ tabIndex: navOpen ? undefined : -1,
11
+ type: "button",
12
+ children: /*#__PURE__*/ _jsx(Hamburger, {
13
+ isActive: true
14
+ })
15
+ });
16
+ };
17
+
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/EnhancedSidebar/NavHamburger/index.tsx"],"sourcesContent":["'use client'\n\nimport { Hamburger, useNav } from '@payloadcms/ui'\nimport React from 'react'\n\nexport const NavHamburger: React.FC<{\n baseClass: string\n}> = ({ baseClass }) => {\n const { navOpen, setNavOpen } = useNav()\n\n return (\n <button\n className={`${baseClass}__mobile-close`}\n onClick={() => setNavOpen(false)}\n tabIndex={navOpen ? undefined : -1}\n type=\"button\"\n >\n <Hamburger isActive />\n </button>\n )\n}\n"],"names":["Hamburger","useNav","React","NavHamburger","baseClass","navOpen","setNavOpen","button","className","onClick","tabIndex","undefined","type","isActive"],"mappings":"AAAA;;AAEA,SAASA,SAAS,EAAEC,MAAM,QAAQ,iBAAgB;AAClD,OAAOC,WAAW,QAAO;AAEzB,OAAO,MAAMC,eAER,CAAC,EAAEC,SAAS,EAAE;IACjB,MAAM,EAAEC,OAAO,EAAEC,UAAU,EAAE,GAAGL;IAEhC,qBACE,KAACM;QACCC,WAAW,GAAGJ,UAAU,cAAc,CAAC;QACvCK,SAAS,IAAMH,WAAW;QAC1BI,UAAUL,UAAUM,YAAY,CAAC;QACjCC,MAAK;kBAEL,cAAA,KAACZ;YAAUa,QAAQ;;;AAGzB,EAAC"}
@@ -0,0 +1,11 @@
1
+ import type { NavPreferences } from 'payload';
2
+ import React from 'react';
3
+ import type { EnhancedSidebarConfig, ExtendedGroup } from '../../types';
4
+ export type SidebarContentProps = {
5
+ afterNavLinks?: React.ReactNode;
6
+ beforeNavLinks?: React.ReactNode;
7
+ groups: ExtendedGroup[];
8
+ navPreferences: NavPreferences | null;
9
+ sidebarConfig: EnhancedSidebarConfig;
10
+ };
11
+ export declare const SidebarContent: React.FC<SidebarContentProps>;
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from '@payloadcms/ui';
4
+ import React, { useEffect, useMemo, useState } from 'react';
5
+ import { extractLocalizedValue } from '../../utils';
6
+ import { EnhancedSidebarClient } from './index.client';
7
+ import { SidebarWrapper } from './SidebarWrapper';
8
+ import { TabsBar } from './TabsBar';
9
+ const baseClass = 'enhanced-sidebar';
10
+ const STORAGE_KEY = 'payload-enhanced-sidebar-active-tab';
11
+ export const SidebarContent = ({ afterNavLinks, beforeNavLinks, groups, navPreferences, sidebarConfig })=>{
12
+ const { i18n } = useTranslation();
13
+ const currentLang = i18n.language;
14
+ const tabs = sidebarConfig.tabs?.filter((t)=>t.type === 'tab') ?? [];
15
+ const defaultTabId = tabs[0]?.id ?? 'default';
16
+ // Always start with default to match server render
17
+ const [activeTabId, setActiveTabId] = useState(defaultTabId);
18
+ // Read from localStorage only after hydration
19
+ useEffect(()=>{
20
+ const stored = localStorage.getItem(STORAGE_KEY);
21
+ if (stored && tabs.some((t)=>t.id === stored)) {
22
+ setActiveTabId(stored);
23
+ }
24
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
25
+ // Persist to localStorage on change
26
+ useEffect(()=>{
27
+ localStorage.setItem(STORAGE_KEY, activeTabId);
28
+ }, [
29
+ activeTabId
30
+ ]);
31
+ const activeTab = tabs.find((tab)=>tab.id === activeTabId);
32
+ // Build groups for the active tab
33
+ const filteredGroups = useMemo(()=>{
34
+ if (!activeTab) {
35
+ return groups;
36
+ }
37
+ const { collections: tabCollections, customItems, globals: tabGlobals } = activeTab;
38
+ // If no specific collections/globals defined, show all
39
+ const showAll = !tabCollections && !tabGlobals;
40
+ const allowedSlugs = new Set([
41
+ ...tabCollections ?? [],
42
+ ...tabGlobals ?? []
43
+ ]);
44
+ let result = [];
45
+ if (showAll) {
46
+ result = groups.map((g)=>({
47
+ ...g,
48
+ entities: [
49
+ ...g.entities
50
+ ]
51
+ }));
52
+ } else if (allowedSlugs.size > 0) {
53
+ result = groups.map((group)=>({
54
+ ...group,
55
+ entities: group.entities.filter((entity)=>allowedSlugs.has(entity.slug))
56
+ })).filter((group)=>group.entities.length > 0);
57
+ }
58
+ // Merge custom items into groups
59
+ if (customItems && customItems.length > 0) {
60
+ const ungroupedItems = [];
61
+ for (const item of customItems){
62
+ if (item.group) {
63
+ // Get localized group name for comparison
64
+ const itemGroupLabel = extractLocalizedValue(item.group, currentLang);
65
+ // Find existing group by comparing localized labels
66
+ const existingGroup = result.find((g)=>{
67
+ const groupLabel = extractLocalizedValue(g.label, currentLang);
68
+ return groupLabel === itemGroupLabel;
69
+ });
70
+ if (existingGroup) {
71
+ existingGroup.entities.push({
72
+ slug: item.slug,
73
+ type: 'custom',
74
+ href: item.href,
75
+ label: item.label
76
+ });
77
+ } else {
78
+ // Create new group
79
+ result.push({
80
+ entities: [
81
+ {
82
+ slug: item.slug,
83
+ type: 'custom',
84
+ href: item.href,
85
+ label: item.label
86
+ }
87
+ ],
88
+ label: item.group
89
+ });
90
+ }
91
+ } else {
92
+ ungroupedItems.push(item);
93
+ }
94
+ }
95
+ // Add ungrouped items at the end
96
+ if (ungroupedItems.length > 0) {
97
+ result.push({
98
+ entities: ungroupedItems.map((item)=>({
99
+ slug: item.slug,
100
+ type: 'custom',
101
+ href: item.href,
102
+ label: item.label
103
+ })),
104
+ label: ''
105
+ });
106
+ }
107
+ }
108
+ return result;
109
+ }, [
110
+ activeTab,
111
+ groups,
112
+ currentLang
113
+ ]);
114
+ return /*#__PURE__*/ _jsxs(SidebarWrapper, {
115
+ baseClass: baseClass,
116
+ children: [
117
+ /*#__PURE__*/ _jsx(TabsBar, {
118
+ activeTabId: activeTabId,
119
+ onTabChange: setActiveTabId,
120
+ sidebarConfig: sidebarConfig
121
+ }),
122
+ /*#__PURE__*/ _jsx("nav", {
123
+ className: `${baseClass}__content`,
124
+ children: /*#__PURE__*/ _jsxs("div", {
125
+ className: `${baseClass}__content-scroll`,
126
+ children: [
127
+ beforeNavLinks,
128
+ /*#__PURE__*/ _jsx(EnhancedSidebarClient, {
129
+ groups: filteredGroups,
130
+ navPreferences: navPreferences
131
+ }),
132
+ afterNavLinks
133
+ ]
134
+ })
135
+ })
136
+ ]
137
+ });
138
+ };
139
+
140
+ //# sourceMappingURL=SidebarContent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/EnhancedSidebar/SidebarContent.tsx"],"sourcesContent":["'use client'\nimport type { NavPreferences } from 'payload'\n\nimport { useTranslation } from '@payloadcms/ui'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nimport type {\n EnhancedSidebarConfig,\n ExtendedGroup,\n SidebarTabContent as SidebarTabContentType,\n SidebarTabItem,\n} from '../../types'\n\nimport { extractLocalizedValue } from '../../utils'\nimport { EnhancedSidebarClient } from './index.client'\nimport { SidebarWrapper } from './SidebarWrapper'\nimport { TabsBar } from './TabsBar'\n\nconst baseClass = 'enhanced-sidebar'\n\nexport type SidebarContentProps = {\n afterNavLinks?: React.ReactNode\n beforeNavLinks?: React.ReactNode\n groups: ExtendedGroup[]\n navPreferences: NavPreferences | null\n sidebarConfig: EnhancedSidebarConfig\n}\n\nconst STORAGE_KEY = 'payload-enhanced-sidebar-active-tab'\n\nexport const SidebarContent: React.FC<SidebarContentProps> = ({\n afterNavLinks,\n beforeNavLinks,\n groups,\n navPreferences,\n sidebarConfig,\n}) => {\n const { i18n } = useTranslation()\n const currentLang = i18n.language\n\n const tabs = sidebarConfig.tabs?.filter((t): t is SidebarTabContentType => t.type === 'tab') ?? []\n const defaultTabId = tabs[0]?.id ?? 'default'\n\n // Always start with default to match server render\n const [activeTabId, setActiveTabId] = useState(defaultTabId)\n\n // Read from localStorage only after hydration\n useEffect(() => {\n const stored = localStorage.getItem(STORAGE_KEY)\n if (stored && tabs.some((t) => t.id === stored)) {\n setActiveTabId(stored)\n }\n }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n // Persist to localStorage on change\n useEffect(() => {\n localStorage.setItem(STORAGE_KEY, activeTabId)\n }, [activeTabId])\n\n const activeTab = tabs.find((tab) => tab.id === activeTabId)\n\n // Build groups for the active tab\n const filteredGroups = useMemo(() => {\n if (!activeTab) {\n return groups\n }\n\n const { collections: tabCollections, customItems, globals: tabGlobals } = activeTab\n\n // If no specific collections/globals defined, show all\n const showAll = !tabCollections && !tabGlobals\n const allowedSlugs = new Set([...(tabCollections ?? []), ...(tabGlobals ?? [])])\n\n let result: ExtendedGroup[] = []\n\n if (showAll) {\n result = groups.map((g) => ({ ...g, entities: [...g.entities] }))\n } else if (allowedSlugs.size > 0) {\n result = groups\n .map((group) => ({\n ...group,\n entities: group.entities.filter((entity) => allowedSlugs.has(entity.slug)),\n }))\n .filter((group) => group.entities.length > 0)\n }\n\n // Merge custom items into groups\n if (customItems && customItems.length > 0) {\n const ungroupedItems: SidebarTabItem[] = []\n\n for (const item of customItems) {\n if (item.group) {\n // Get localized group name for comparison\n const itemGroupLabel = extractLocalizedValue(item.group, currentLang)\n\n // Find existing group by comparing localized labels\n const existingGroup = result.find((g) => {\n const groupLabel = extractLocalizedValue(g.label, currentLang)\n return groupLabel === itemGroupLabel\n })\n\n if (existingGroup) {\n existingGroup.entities.push({\n slug: item.slug,\n type: 'custom',\n href: item.href,\n label: item.label,\n })\n } else {\n // Create new group\n result.push({\n entities: [\n {\n slug: item.slug,\n type: 'custom',\n href: item.href,\n label: item.label,\n },\n ],\n label: item.group,\n })\n }\n } else {\n ungroupedItems.push(item)\n }\n }\n\n // Add ungrouped items at the end\n if (ungroupedItems.length > 0) {\n result.push({\n entities: ungroupedItems.map((item) => ({\n slug: item.slug,\n type: 'custom',\n href: item.href,\n label: item.label,\n })),\n label: '',\n })\n }\n }\n\n return result\n }, [activeTab, groups, currentLang])\n\n return (\n <SidebarWrapper baseClass={baseClass}>\n <TabsBar\n activeTabId={activeTabId}\n onTabChange={setActiveTabId}\n sidebarConfig={sidebarConfig}\n />\n <nav className={`${baseClass}__content`}>\n <div className={`${baseClass}__content-scroll`}>\n {beforeNavLinks}\n <EnhancedSidebarClient groups={filteredGroups} navPreferences={navPreferences} />\n {afterNavLinks}\n </div>\n </nav>\n </SidebarWrapper>\n )\n}\n"],"names":["useTranslation","React","useEffect","useMemo","useState","extractLocalizedValue","EnhancedSidebarClient","SidebarWrapper","TabsBar","baseClass","STORAGE_KEY","SidebarContent","afterNavLinks","beforeNavLinks","groups","navPreferences","sidebarConfig","i18n","currentLang","language","tabs","filter","t","type","defaultTabId","id","activeTabId","setActiveTabId","stored","localStorage","getItem","some","setItem","activeTab","find","tab","filteredGroups","collections","tabCollections","customItems","globals","tabGlobals","showAll","allowedSlugs","Set","result","map","g","entities","size","group","entity","has","slug","length","ungroupedItems","item","itemGroupLabel","existingGroup","groupLabel","label","push","href","onTabChange","nav","className","div"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import './index.scss';
3
+ export declare const SidebarWrapper: React.FC<{
4
+ baseClass: string;
5
+ children: React.ReactNode;
6
+ }>;
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useNav } from '@payloadcms/ui';
4
+ import React from 'react';
5
+ import { NavHamburger } from '../NavHamburger';
6
+ import './index.scss';
7
+ export const SidebarWrapper = ({ baseClass, children })=>{
8
+ const { hydrated, navOpen, navRef, shouldAnimate } = useNav();
9
+ return /*#__PURE__*/ _jsxs("aside", {
10
+ className: [
11
+ baseClass,
12
+ navOpen && `${baseClass}--nav-open`,
13
+ shouldAnimate && `${baseClass}--nav-animate`,
14
+ hydrated && `${baseClass}--nav-hydrated`
15
+ ].filter(Boolean).join(' '),
16
+ inert: !navOpen ? true : undefined,
17
+ children: [
18
+ /*#__PURE__*/ _jsx("div", {
19
+ className: `${baseClass}__header`,
20
+ children: /*#__PURE__*/ _jsx(NavHamburger, {
21
+ baseClass: baseClass
22
+ })
23
+ }),
24
+ /*#__PURE__*/ _jsx("div", {
25
+ className: `${baseClass}__scroll`,
26
+ ref: navRef,
27
+ children: children
28
+ })
29
+ ]
30
+ });
31
+ };
32
+
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/EnhancedSidebar/SidebarWrapper/index.tsx"],"sourcesContent":["'use client'\n\nimport { useNav } from '@payloadcms/ui'\nimport React from 'react'\n\nimport { NavHamburger } from '../NavHamburger'\nimport './index.scss'\n\nexport const SidebarWrapper: React.FC<{\n baseClass: string\n children: React.ReactNode\n}> = ({ baseClass, children }) => {\n const { hydrated, navOpen, navRef, shouldAnimate } = useNav()\n\n return (\n <aside\n className={[\n baseClass,\n navOpen && `${baseClass}--nav-open`,\n shouldAnimate && `${baseClass}--nav-animate`,\n hydrated && `${baseClass}--nav-hydrated`,\n ]\n .filter(Boolean)\n .join(' ')}\n inert={!navOpen ? true : undefined}\n >\n <div className={`${baseClass}__header`}>\n <NavHamburger baseClass={baseClass} />\n </div>\n <div className={`${baseClass}__scroll`} ref={navRef}>\n {children}\n </div>\n </aside>\n )\n}\n"],"names":["useNav","React","NavHamburger","SidebarWrapper","baseClass","children","hydrated","navOpen","navRef","shouldAnimate","aside","className","filter","Boolean","join","inert","undefined","div","ref"],"mappings":"AAAA;;AAEA,SAASA,MAAM,QAAQ,iBAAgB;AACvC,OAAOC,WAAW,QAAO;AAEzB,SAASC,YAAY,QAAQ,kBAAiB;AAC9C,OAAO,eAAc;AAErB,OAAO,MAAMC,iBAGR,CAAC,EAAEC,SAAS,EAAEC,QAAQ,EAAE;IAC3B,MAAM,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,MAAM,EAAEC,aAAa,EAAE,GAAGT;IAErD,qBACE,MAACU;QACCC,WAAW;YACTP;YACAG,WAAW,GAAGH,UAAU,UAAU,CAAC;YACnCK,iBAAiB,GAAGL,UAAU,aAAa,CAAC;YAC5CE,YAAY,GAAGF,UAAU,cAAc,CAAC;SACzC,CACEQ,MAAM,CAACC,SACPC,IAAI,CAAC;QACRC,OAAO,CAACR,UAAU,OAAOS;;0BAEzB,KAACC;gBAAIN,WAAW,GAAGP,UAAU,QAAQ,CAAC;0BACpC,cAAA,KAACF;oBAAaE,WAAWA;;;0BAE3B,KAACa;gBAAIN,WAAW,GAAGP,UAAU,QAAQ,CAAC;gBAAEc,KAAKV;0BAC1CH;;;;AAIT,EAAC"}
@@ -0,0 +1,63 @@
1
+ @import '~@payloadcms/ui/scss';
2
+
3
+ @layer payload-default {
4
+ .enhanced-sidebar {
5
+ position: sticky;
6
+ top: 0;
7
+ left: 0;
8
+ flex-shrink: 0;
9
+ height: 100vh;
10
+ width: var(--nav-width);
11
+ border-right: 1px solid var(--theme-elevation-100);
12
+ opacity: 0;
13
+
14
+ [dir='rtl'] & {
15
+ border-right: none;
16
+ border-left: 1px solid var(--theme-elevation-100);
17
+ }
18
+
19
+ &--nav-animate {
20
+ transition: opacity var(--nav-trans-time) ease-in-out;
21
+ }
22
+
23
+ &--nav-open {
24
+ opacity: 1;
25
+ }
26
+
27
+ &__header {
28
+ position: absolute;
29
+ top: 0;
30
+ width: 100vw;
31
+ height: var(--app-header-height);
32
+ z-index: 1;
33
+ }
34
+
35
+ &__mobile-close {
36
+ display: none;
37
+ background: none;
38
+ border: 0;
39
+ outline: 0;
40
+ padding: base(0.8) 0;
41
+ cursor: pointer;
42
+ }
43
+
44
+ &__scroll {
45
+ height: 100%;
46
+ display: flex;
47
+ flex-direction: row;
48
+ }
49
+
50
+ @include small-break {
51
+ &__mobile-close {
52
+ display: flex;
53
+ align-items: center;
54
+ padding-left: var(--gutter-h);
55
+
56
+ [dir='rtl'] & {
57
+ padding-left: 0;
58
+ padding-right: var(--gutter-h);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { EnhancedSidebarConfig } from '../../../types';
3
+ import './index.scss';
4
+ export type TabsBarProps = {
5
+ activeTabId: string;
6
+ onTabChange: (tabId: string) => void;
7
+ sidebarConfig: EnhancedSidebarConfig;
8
+ };
9
+ export declare const TabsBar: React.FC<TabsBarProps>;