@veiag/payload-enhanced-sidebar 0.1.1 → 0.2.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 (45) hide show
  1. package/README.md +182 -5
  2. package/dist/components/EnhancedSidebar/Badge/index.d.ts +22 -0
  3. package/dist/components/EnhancedSidebar/Badge/index.js +30 -0
  4. package/dist/components/EnhancedSidebar/Badge/index.js.map +1 -0
  5. package/dist/components/EnhancedSidebar/Badge/index.scss +65 -0
  6. package/dist/components/EnhancedSidebar/BadgeProvider/index.d.ts +64 -0
  7. package/dist/components/EnhancedSidebar/BadgeProvider/index.js +66 -0
  8. package/dist/components/EnhancedSidebar/BadgeProvider/index.js.map +1 -0
  9. package/dist/components/EnhancedSidebar/InternalBadgeProvider/index.d.ts +15 -0
  10. package/dist/components/EnhancedSidebar/InternalBadgeProvider/index.js +132 -0
  11. package/dist/components/EnhancedSidebar/InternalBadgeProvider/index.js.map +1 -0
  12. package/dist/components/EnhancedSidebar/NavItem/index.d.ts +11 -0
  13. package/dist/components/EnhancedSidebar/NavItem/index.js +46 -0
  14. package/dist/components/EnhancedSidebar/NavItem/index.js.map +1 -0
  15. package/dist/components/EnhancedSidebar/SettingsMenuButton/index.d.ts +6 -0
  16. package/dist/components/EnhancedSidebar/SettingsMenuButton/index.js +33 -0
  17. package/dist/components/EnhancedSidebar/SettingsMenuButton/index.js.map +1 -0
  18. package/dist/components/EnhancedSidebar/SettingsMenuButton/index.scss +25 -0
  19. package/dist/components/EnhancedSidebar/SidebarContent.d.ts +2 -0
  20. package/dist/components/EnhancedSidebar/SidebarContent.js +117 -99
  21. package/dist/components/EnhancedSidebar/SidebarContent.js.map +1 -1
  22. package/dist/components/EnhancedSidebar/TabsBar/TabItem.d.ts +15 -0
  23. package/dist/components/EnhancedSidebar/TabsBar/TabItem.js +56 -0
  24. package/dist/components/EnhancedSidebar/TabsBar/TabItem.js.map +1 -0
  25. package/dist/components/EnhancedSidebar/TabsBar/index.d.ts +1 -0
  26. package/dist/components/EnhancedSidebar/TabsBar/index.js +58 -52
  27. package/dist/components/EnhancedSidebar/TabsBar/index.js.map +1 -1
  28. package/dist/components/EnhancedSidebar/TabsBar/index.scss +2 -2
  29. package/dist/components/EnhancedSidebar/hooks/useBadge.d.ts +11 -0
  30. package/dist/components/EnhancedSidebar/hooks/useBadge.js +31 -0
  31. package/dist/components/EnhancedSidebar/hooks/useBadge.js.map +1 -0
  32. package/dist/components/EnhancedSidebar/index.client.d.ts +2 -1
  33. package/dist/components/EnhancedSidebar/index.client.js +18 -31
  34. package/dist/components/EnhancedSidebar/index.client.js.map +1 -1
  35. package/dist/components/EnhancedSidebar/index.js +17 -1
  36. package/dist/components/EnhancedSidebar/index.js.map +1 -1
  37. package/dist/exports/client.d.ts +1 -0
  38. package/dist/exports/client.js +2 -1
  39. package/dist/exports/client.js.map +1 -1
  40. package/dist/index.d.ts +2 -1
  41. package/dist/index.js +16 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/types.d.ts +104 -1
  44. package/dist/types.js.map +1 -1
  45. package/package.json +10 -10
package/README.md CHANGED
@@ -10,9 +10,12 @@ An enhanced sidebar plugin for [Payload CMS](https://payloadcms.com) that adds a
10
10
  - **Vertical Tab Bar** - Icon-based tabs on the left side of the sidebar
11
11
  - **Link Support** - Add navigation links (like Dashboard) alongside tabs
12
12
  - **Custom Items** - Add custom navigation items that can be merged into existing groups
13
+ - **Badges** - Show notification badges on tabs and navigation items (API-based or reactive provider)
13
14
  - **i18n Support** - Full localization support for labels and groups
14
15
  - **Lucide Icons** - Use any [Lucide icon](https://lucide.dev/icons) for tabs and links
15
16
 
17
+ ![Showcase](docs/showcase.gif)
18
+
16
19
  ## Installation
17
20
 
18
21
  ```bash
@@ -44,6 +47,8 @@ This will add:
44
47
  - A default tab showing all collections and globals
45
48
  - A logout button at the bottom
46
49
 
50
+ ![Default Config](docs/default-config.png)
51
+
47
52
  ## Configuration
48
53
 
49
54
  ### Full Configuration Example
@@ -150,9 +155,11 @@ Array of tabs and links to show in the sidebar.
150
155
  | `collections` | `CollectionSlug[]` | No | Collections to show in this tab |
151
156
  | `globals` | `GlobalSlug[]` | No | Globals to show in this tab |
152
157
  | `customItems` | `SidebarTabItem[]` | No | Custom navigation items |
158
+ | `badge` | `BadgeConfig` | No | Badge configuration for the tab icon |
153
159
 
154
160
  > If neither `collections` nor `globals` are specified, the tab shows all collections and globals.
155
161
 
162
+
156
163
  **Link (`type: 'link'`)**
157
164
 
158
165
  | Property | Type | Required | Description |
@@ -163,6 +170,10 @@ Array of tabs and links to show in the sidebar.
163
170
  | `label` | `LocalizedString` | Yes | Link tooltip/label |
164
171
  | `href` | `string` | Yes | URL |
165
172
  | `isExternal` | `boolean` | No | If true, `href` is absolute URL, if not, `href` is relative to admin route |
173
+ | `badge` | `BadgeConfig` | No | Badge configuration for the link icon |
174
+
175
+
176
+ ![Tab and Link active difference](docs/tab-link-active.png)
166
177
 
167
178
  ### `customItems`
168
179
 
@@ -183,6 +194,174 @@ Custom items can be added to any tab:
183
194
  - If `group` doesn't match any existing group, a new group is created
184
195
  - If `group` is not specified, the item appears at the bottom as ungrouped
185
196
 
197
+
198
+ ## Badges
199
+
200
+ Badges allow you to show notification counts on tabs and navigation items. There are three ways to configure badges:
201
+
202
+ <!-- [screenshot - Badges showcase: show sidebar with multiple badges - on tab icon (red "5"), on nav item (blue "12"), maybe one with "99+". Show different colors: error (red), primary (blue), warning (yellow)] -->
203
+
204
+ ### Badge on Tabs/Links
205
+
206
+ Add a `badge` property to any tab or link in the `tabs` array:
207
+
208
+ ```typescript
209
+ tabs: [
210
+ {
211
+ id: 'orders',
212
+ type: 'tab',
213
+ icon: 'ShoppingCart',
214
+ label: 'Orders',
215
+ collections: ['orders'],
216
+ // Badge on the tab icon
217
+ badge: {
218
+ type: 'collection-count',
219
+ collectionSlug: 'orders',
220
+ color: 'error',
221
+ },
222
+ },
223
+ ]
224
+ ```
225
+
226
+ ### Badges on Navigation Items
227
+
228
+ Use the `badges` configuration to add badges to any sidebar item (collections, globals, or custom items):
229
+
230
+ ```typescript
231
+ payloadEnhancedSidebar({
232
+ badges: {
233
+ // Show document count for posts collection
234
+ posts: { type: 'collection-count', color: 'primary' },
235
+ // Custom API endpoint
236
+ orders: {
237
+ type: 'api',
238
+ endpoint: '/api/orders/pending',
239
+ responseKey: 'count',
240
+ color: 'error',
241
+ },
242
+ // Provider-based (reactive)
243
+ notifications: { type: 'provider', color: 'warning' },
244
+ },
245
+ })
246
+ ```
247
+
248
+ ### Badge Types
249
+
250
+ #### `collection-count`
251
+
252
+ Automatically fetches document count from a collection.
253
+
254
+ ```typescript
255
+ {
256
+ type: 'collection-count',
257
+ collectionSlug?: string, // Defaults to item's slug
258
+ color?: BadgeColor, // 'default' | 'primary' | 'success' | 'warning' | 'error'
259
+ where?: object, // Optional filter query
260
+ }
261
+ ```
262
+
263
+ #### `api`
264
+
265
+ Fetches badge value from a custom API endpoint.
266
+
267
+ ```typescript
268
+ {
269
+ type: 'api',
270
+ endpoint: string, // API URL (relative or absolute)
271
+ method?: 'GET' | 'POST', // Default: 'GET'
272
+ responseKey?: string, // Key to extract from response. Default: 'count'
273
+ color?: BadgeColor,
274
+ }
275
+ ```
276
+
277
+ #### `provider`
278
+
279
+ Uses reactive values from `BadgeProvider` context. Values update automatically when the provider changes.
280
+
281
+ ```typescript
282
+ {
283
+ type: 'provider',
284
+ slug?: string, // Key in provider values. Defaults to item's slug/id
285
+ color?: BadgeColor,
286
+ }
287
+ ```
288
+
289
+ ### Using BadgeProvider
290
+
291
+ For reactive badges (real-time updates, websockets, etc.), use the `BadgeProvider`:
292
+
293
+ 1. Create a provider component:
294
+
295
+ ```typescript
296
+ // components/MyBadgeProvider.tsx
297
+ 'use client'
298
+
299
+ import { BadgeProvider } from '@veiag/payload-enhanced-sidebar'
300
+ import { useEffect, useState } from 'react'
301
+
302
+ export const MyBadgeProvider = ({ children }) => {
303
+ const [counts, setCounts] = useState({
304
+ orders: 0,
305
+ notifications: 0,
306
+ })
307
+
308
+ useEffect(() => {
309
+ // Fetch initial counts, subscribe to websocket, etc.
310
+ const ws = new WebSocket('wss://your-api/counts')
311
+ ws.onmessage = (e) => setCounts(JSON.parse(e.data))
312
+ return () => ws.close()
313
+ }, [])
314
+
315
+ return <BadgeProvider values={counts}>{children}</BadgeProvider>
316
+ }
317
+ ```
318
+
319
+ 2. Add it to Payload's providers:
320
+
321
+ ```typescript
322
+ // payload.config.ts
323
+ export default buildConfig({
324
+ admin: {
325
+ components: {
326
+ providers: ['./components/MyBadgeProvider#MyBadgeProvider'],
327
+ },
328
+ },
329
+ })
330
+ ```
331
+
332
+ 3. Configure badges to use the provider:
333
+
334
+ ```typescript
335
+ payloadEnhancedSidebar({
336
+ badges: {
337
+ orders: { type: 'provider', color: 'error' },
338
+ },
339
+ tabs: [
340
+ {
341
+ id: 'notifications',
342
+ type: 'link',
343
+ href: '/notifications',
344
+ icon: 'Bell',
345
+ label: 'Notifications',
346
+ badge: { type: 'provider', slug: 'notifications', color: 'warning' },
347
+ },
348
+ ],
349
+ })
350
+ ```
351
+
352
+ ### Badge Colors
353
+
354
+ Available colors: `default`, `primary`, `success`, `warning`, `error`
355
+
356
+ ![Badge Colors](docs/badge-colors.png)
357
+
358
+ ### Badge Display
359
+
360
+ - Numbers up to 99 are shown as-is
361
+ - Numbers > 99 are shown as "99+"
362
+ - Zero or undefined values hide the badge
363
+ - Provider values can also be React nodes for custom rendering
364
+
186
365
  ### `showLogout`
187
366
 
188
367
  Show/hide the logout button at the bottom of the tabs bar.
@@ -211,12 +390,10 @@ label: {
211
390
  }
212
391
  ```
213
392
 
214
- ## TODO
215
-
216
- The following features are planned but not yet implemented:
393
+ ## Payload Features Support
217
394
 
218
- - [ ] **Browse by Folder Button** - Support for the folder view button (requires Payload v3.41.0+)
219
- - [ ] **Settings Menu Items** - Support for Payload's SettingsMenu items (requires Payload v3.60.0+)
395
+ - **Browse by Folder Button** - Automatically shows folder view button when Payload folders are enabled (requires Payload v3.41.0+)
396
+ - **Settings Menu Items** - Integrates with Payload's SettingsMenu components (requires Payload v3.60.0+)
220
397
 
221
398
  ## Contributing
222
399
 
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import type { BadgeColor } from '../../../types';
3
+ import './index.scss';
4
+ export type BadgeProps = {
5
+ /**
6
+ * Badge color variant
7
+ * @default 'default'
8
+ */
9
+ color?: BadgeColor;
10
+ /**
11
+ * Position modifier
12
+ * - 'absolute': positioned at top-right corner (for icons)
13
+ * - 'inline': inline with margin (for nav items)
14
+ */
15
+ position?: 'absolute' | 'inline';
16
+ /**
17
+ * Badge value - number or React node
18
+ * Numbers > 99 will be displayed as "99+"
19
+ */
20
+ value: number | React.ReactNode;
21
+ };
22
+ export declare const Badge: React.FC<BadgeProps>;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import './index.scss';
5
+ const baseClass = 'sidebar-badge';
6
+ /**
7
+ * Format badge value - show up to 2 digits, 99+ for larger numbers
8
+ */ const formatValue = (value)=>{
9
+ if (typeof value === 'number') {
10
+ return value > 99 ? '99+' : value;
11
+ }
12
+ return value;
13
+ };
14
+ export const Badge = ({ color = 'default', position, value })=>{
15
+ // Don't render if value is 0 or falsy (except for React nodes)
16
+ if (value === 0 || value === null || typeof value !== 'object' && !value) {
17
+ return null;
18
+ }
19
+ const classes = [
20
+ baseClass,
21
+ `${baseClass}--${color}`,
22
+ position && `${baseClass}--${position}`
23
+ ].filter(Boolean).join(' ');
24
+ return /*#__PURE__*/ _jsx("span", {
25
+ className: classes,
26
+ children: formatValue(value)
27
+ });
28
+ };
29
+
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/EnhancedSidebar/Badge/index.tsx"],"sourcesContent":["'use client'\n\nimport React from 'react'\n\nimport type { BadgeColor } from '../../../types'\n\nimport './index.scss'\n\nconst baseClass = 'sidebar-badge'\n\nexport type BadgeProps = {\n /**\n * Badge color variant\n * @default 'default'\n */\n color?: BadgeColor\n /**\n * Position modifier\n * - 'absolute': positioned at top-right corner (for icons)\n * - 'inline': inline with margin (for nav items)\n */\n position?: 'absolute' | 'inline'\n /**\n * Badge value - number or React node\n * Numbers > 99 will be displayed as \"99+\"\n */\n value: number | React.ReactNode\n}\n\n/**\n * Format badge value - show up to 2 digits, 99+ for larger numbers\n */\nconst formatValue = (value: number | React.ReactNode): React.ReactNode => {\n if (typeof value === 'number') {\n return value > 99 ? '99+' : value\n }\n return value\n}\n\nexport const Badge: React.FC<BadgeProps> = ({ color = 'default', position, value }) => {\n // Don't render if value is 0 or falsy (except for React nodes)\n if (value === 0 || value === null || (typeof value !== 'object' && !value)) {\n return null\n }\n\n const classes = [baseClass, `${baseClass}--${color}`, position && `${baseClass}--${position}`]\n .filter(Boolean)\n .join(' ')\n\n return <span className={classes}>{formatValue(value)}</span>\n}\n"],"names":["React","baseClass","formatValue","value","Badge","color","position","classes","filter","Boolean","join","span","className"],"mappings":"AAAA;;AAEA,OAAOA,WAAW,QAAO;AAIzB,OAAO,eAAc;AAErB,MAAMC,YAAY;AAqBlB;;CAEC,GACD,MAAMC,cAAc,CAACC;IACnB,IAAI,OAAOA,UAAU,UAAU;QAC7B,OAAOA,QAAQ,KAAK,QAAQA;IAC9B;IACA,OAAOA;AACT;AAEA,OAAO,MAAMC,QAA8B,CAAC,EAAEC,QAAQ,SAAS,EAAEC,QAAQ,EAAEH,KAAK,EAAE;IAChF,+DAA+D;IAC/D,IAAIA,UAAU,KAAKA,UAAU,QAAS,OAAOA,UAAU,YAAY,CAACA,OAAQ;QAC1E,OAAO;IACT;IAEA,MAAMI,UAAU;QAACN;QAAW,GAAGA,UAAU,EAAE,EAAEI,OAAO;QAAEC,YAAY,GAAGL,UAAU,EAAE,EAAEK,UAAU;KAAC,CAC3FE,MAAM,CAACC,SACPC,IAAI,CAAC;IAER,qBAAO,KAACC;QAAKC,WAAWL;kBAAUL,YAAYC;;AAChD,EAAC"}
@@ -0,0 +1,65 @@
1
+ @import '~@payloadcms/ui/scss';
2
+
3
+ @layer payload-default {
4
+ .sidebar-badge {
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ min-width: 18px;
9
+ height: 18px;
10
+ padding: 0 base(0.25);
11
+ border-radius: 9px;
12
+ font-size: 11px;
13
+ font-weight: 600;
14
+ line-height: 1;
15
+ white-space: nowrap;
16
+
17
+ // Color variants - using fixed colors for consistent contrast in both themes
18
+ &--default {
19
+ background: var(--theme-elevation-200);
20
+ color: var(--theme-elevation-800);
21
+ }
22
+
23
+ &--primary {
24
+ background: #3b82f6; // Blue
25
+ color: #fff;
26
+ }
27
+
28
+ &--success {
29
+ background: #22c55e; // Green
30
+ color: #fff;
31
+ }
32
+
33
+ &--warning {
34
+ background: #f59e0b; // Amber
35
+ color: #000;
36
+ }
37
+
38
+ &--error {
39
+ background: #ef4444; // Red
40
+ color: #fff;
41
+ }
42
+
43
+ // Position modifier for absolute positioning (used on icons)
44
+ &--absolute {
45
+ position: absolute;
46
+ top: -4px;
47
+ right: -4px;
48
+
49
+ [dir='rtl'] & {
50
+ right: auto;
51
+ left: -4px;
52
+ }
53
+ }
54
+
55
+ // Inline modifier (used in nav items)
56
+ &--inline {
57
+ margin-left: base(0.5);
58
+
59
+ [dir='rtl'] & {
60
+ margin-left: 0;
61
+ margin-right: base(0.5);
62
+ }
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import type { BadgeValues } from '../../../types';
3
+ type BadgeContextValue = {
4
+ values: BadgeValues;
5
+ };
6
+ export type BadgeProviderProps = {
7
+ children: React.ReactNode;
8
+ /**
9
+ * Badge values object.
10
+ * Keys are slugs (collection slug, global slug, or custom item slug).
11
+ * Values can be numbers or React nodes.
12
+ *
13
+ * Values are merged with any parent BadgeProvider values.
14
+ * This provider's values take priority over parent values.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <BadgeProvider values={{ orders: 5, notifications: <CustomBadge /> }}>
19
+ * {children}
20
+ * </BadgeProvider>
21
+ * ```
22
+ */
23
+ values: BadgeValues;
24
+ };
25
+ /**
26
+ * Provider for badge values.
27
+ * Wrap your app with this provider and pass badge values.
28
+ * Values are reactive - changes will update badges automatically.
29
+ *
30
+ * Values are merged with any parent BadgeProvider (e.g., InternalBadgeProvider).
31
+ * This provider's values take priority, allowing you to override internal values.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * // In your admin.components.providers
36
+ * import { BadgeProvider } from '@veiag/payload-enhanced-sidebar'
37
+ *
38
+ * export const MyProvider = ({ children }) => {
39
+ * const [counts, setCounts] = useState({ orders: 0 })
40
+ *
41
+ * useEffect(() => {
42
+ * // Fetch counts, subscribe to realtime updates, etc.
43
+ * }, [])
44
+ *
45
+ * return (
46
+ * <BadgeProvider values={counts}>
47
+ * {children}
48
+ * </BadgeProvider>
49
+ * )
50
+ * }
51
+ * ```
52
+ */
53
+ export declare const BadgeProvider: React.FC<BadgeProviderProps>;
54
+ /**
55
+ * Hook to access badge values from context.
56
+ * Returns the full values object from BadgeProvider.
57
+ */
58
+ export declare const useBadgeContext: () => BadgeContextValue;
59
+ /**
60
+ * Hook to get a specific badge value by slug.
61
+ * Returns the value for the given slug, or undefined if not found.
62
+ */
63
+ export declare const useBadgeValue: (slug: string) => number | React.ReactNode | undefined;
64
+ export {};
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React, { createContext, useContext, useMemo } from 'react';
4
+ const BadgeContext = /*#__PURE__*/ createContext({
5
+ values: {}
6
+ });
7
+ /**
8
+ * Provider for badge values.
9
+ * Wrap your app with this provider and pass badge values.
10
+ * Values are reactive - changes will update badges automatically.
11
+ *
12
+ * Values are merged with any parent BadgeProvider (e.g., InternalBadgeProvider).
13
+ * This provider's values take priority, allowing you to override internal values.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // In your admin.components.providers
18
+ * import { BadgeProvider } from '@veiag/payload-enhanced-sidebar'
19
+ *
20
+ * export const MyProvider = ({ children }) => {
21
+ * const [counts, setCounts] = useState({ orders: 0 })
22
+ *
23
+ * useEffect(() => {
24
+ * // Fetch counts, subscribe to realtime updates, etc.
25
+ * }, [])
26
+ *
27
+ * return (
28
+ * <BadgeProvider values={counts}>
29
+ * {children}
30
+ * </BadgeProvider>
31
+ * )
32
+ * }
33
+ * ```
34
+ */ export const BadgeProvider = ({ children, values })=>{
35
+ // Get parent context values (if any)
36
+ const parentContext = useContext(BadgeContext);
37
+ // Merge parent values with this provider's values (this provider wins)
38
+ const mergedValues = useMemo(()=>({
39
+ ...parentContext.values,
40
+ ...values
41
+ }), [
42
+ parentContext.values,
43
+ values
44
+ ]);
45
+ return /*#__PURE__*/ _jsx(BadgeContext.Provider, {
46
+ value: {
47
+ values: mergedValues
48
+ },
49
+ children: children
50
+ });
51
+ };
52
+ /**
53
+ * Hook to access badge values from context.
54
+ * Returns the full values object from BadgeProvider.
55
+ */ export const useBadgeContext = ()=>{
56
+ return useContext(BadgeContext);
57
+ };
58
+ /**
59
+ * Hook to get a specific badge value by slug.
60
+ * Returns the value for the given slug, or undefined if not found.
61
+ */ export const useBadgeValue = (slug)=>{
62
+ const { values } = useBadgeContext();
63
+ return values[slug];
64
+ };
65
+
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/EnhancedSidebar/BadgeProvider/index.tsx"],"sourcesContent":["'use client'\n\nimport React, { createContext, useContext, useMemo } from 'react'\n\nimport type { BadgeValues } from '../../../types'\n\ntype BadgeContextValue = {\n values: BadgeValues\n}\n\nconst BadgeContext = createContext<BadgeContextValue>({ values: {} })\n\nexport type BadgeProviderProps = {\n children: React.ReactNode\n /**\n * Badge values object.\n * Keys are slugs (collection slug, global slug, or custom item slug).\n * Values can be numbers or React nodes.\n *\n * Values are merged with any parent BadgeProvider values.\n * This provider's values take priority over parent values.\n *\n * @example\n * ```tsx\n * <BadgeProvider values={{ orders: 5, notifications: <CustomBadge /> }}>\n * {children}\n * </BadgeProvider>\n * ```\n */\n values: BadgeValues\n}\n\n/**\n * Provider for badge values.\n * Wrap your app with this provider and pass badge values.\n * Values are reactive - changes will update badges automatically.\n *\n * Values are merged with any parent BadgeProvider (e.g., InternalBadgeProvider).\n * This provider's values take priority, allowing you to override internal values.\n *\n * @example\n * ```tsx\n * // In your admin.components.providers\n * import { BadgeProvider } from '@veiag/payload-enhanced-sidebar'\n *\n * export const MyProvider = ({ children }) => {\n * const [counts, setCounts] = useState({ orders: 0 })\n *\n * useEffect(() => {\n * // Fetch counts, subscribe to realtime updates, etc.\n * }, [])\n *\n * return (\n * <BadgeProvider values={counts}>\n * {children}\n * </BadgeProvider>\n * )\n * }\n * ```\n */\nexport const BadgeProvider: React.FC<BadgeProviderProps> = ({ children, values }) => {\n // Get parent context values (if any)\n const parentContext = useContext(BadgeContext)\n\n // Merge parent values with this provider's values (this provider wins)\n const mergedValues = useMemo(\n () => ({\n ...parentContext.values,\n ...values,\n }),\n [parentContext.values, values],\n )\n\n return <BadgeContext.Provider value={{ values: mergedValues }}>{children}</BadgeContext.Provider>\n}\n\n/**\n * Hook to access badge values from context.\n * Returns the full values object from BadgeProvider.\n */\nexport const useBadgeContext = (): BadgeContextValue => {\n return useContext(BadgeContext)\n}\n\n/**\n * Hook to get a specific badge value by slug.\n * Returns the value for the given slug, or undefined if not found.\n */\nexport const useBadgeValue = (slug: string): number | React.ReactNode | undefined => {\n const { values } = useBadgeContext()\n return values[slug]\n}\n"],"names":["React","createContext","useContext","useMemo","BadgeContext","values","BadgeProvider","children","parentContext","mergedValues","Provider","value","useBadgeContext","useBadgeValue","slug"],"mappings":"AAAA;;AAEA,OAAOA,SAASC,aAAa,EAAEC,UAAU,EAAEC,OAAO,QAAQ,QAAO;AAQjE,MAAMC,6BAAeH,cAAiC;IAAEI,QAAQ,CAAC;AAAE;AAsBnE;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GACD,OAAO,MAAMC,gBAA8C,CAAC,EAAEC,QAAQ,EAAEF,MAAM,EAAE;IAC9E,qCAAqC;IACrC,MAAMG,gBAAgBN,WAAWE;IAEjC,uEAAuE;IACvE,MAAMK,eAAeN,QACnB,IAAO,CAAA;YACL,GAAGK,cAAcH,MAAM;YACvB,GAAGA,MAAM;QACX,CAAA,GACA;QAACG,cAAcH,MAAM;QAAEA;KAAO;IAGhC,qBAAO,KAACD,aAAaM,QAAQ;QAACC,OAAO;YAAEN,QAAQI;QAAa;kBAAIF;;AAClE,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMK,kBAAkB;IAC7B,OAAOV,WAAWE;AACpB,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMS,gBAAgB,CAACC;IAC5B,MAAM,EAAET,MAAM,EAAE,GAAGO;IACnB,OAAOP,MAAM,CAACS,KAAK;AACrB,EAAC"}
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import type { EnhancedSidebarConfig } from '../../../types';
3
+ export type InternalBadgeProviderProps = {
4
+ children: React.ReactNode;
5
+ /**
6
+ * Sidebar configuration containing badge configs
7
+ */
8
+ sidebarConfig: EnhancedSidebarConfig;
9
+ };
10
+ /**
11
+ * Internal provider that fetches all API-based badges once on mount.
12
+ * This provider is automatically injected by the plugin.
13
+ * Values are stored in context and don't refetch on navigation.
14
+ */
15
+ export declare const InternalBadgeProvider: React.FC<InternalBadgeProviderProps>;
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useConfig } from '@payloadcms/ui';
4
+ import { stringify } from 'qs-esm';
5
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { BadgeProvider } from '../BadgeProvider';
7
+ /**
8
+ * Internal provider that fetches all API-based badges once on mount.
9
+ * This provider is automatically injected by the plugin.
10
+ * Values are stored in context and don't refetch on navigation.
11
+ */ export const InternalBadgeProvider = ({ children, sidebarConfig })=>{
12
+ const [values, setValues] = useState({});
13
+ const { config: { routes: { api: apiRoute }, serverURL } } = useConfig();
14
+ // Collect all badges that need to be fetched (api and collection-count types)
15
+ const badgesToFetch = useMemo(()=>{
16
+ const badges = [];
17
+ // From tabs
18
+ if (sidebarConfig.tabs) {
19
+ for (const tab of sidebarConfig.tabs){
20
+ if (tab.badge && tab.badge.type !== 'provider') {
21
+ badges.push({
22
+ slug: tab.id,
23
+ config: tab.badge
24
+ });
25
+ }
26
+ }
27
+ }
28
+ // From badges config
29
+ if (sidebarConfig.badges) {
30
+ for (const [slug, config] of Object.entries(sidebarConfig.badges)){
31
+ if (config.type !== 'provider') {
32
+ badges.push({
33
+ slug,
34
+ config
35
+ });
36
+ }
37
+ }
38
+ }
39
+ return badges;
40
+ }, [
41
+ sidebarConfig
42
+ ]);
43
+ // Fetch a single badge value
44
+ const fetchBadge = useCallback(async (badge)=>{
45
+ const { slug, config } = badge;
46
+ try {
47
+ let url;
48
+ let responseKey;
49
+ if (config.type === 'api') {
50
+ url = config.endpoint;
51
+ responseKey = config.responseKey ?? 'count';
52
+ // If endpoint is relative, prepend serverURL
53
+ if (!url.startsWith('http')) {
54
+ url = `${serverURL || ''}${url}`;
55
+ }
56
+ } else if (config.type === 'collection-count') {
57
+ const collectionSlug = config.collectionSlug ?? slug;
58
+ const baseUrl = `${serverURL || ''}${apiRoute}/${collectionSlug}`;
59
+ if (config.where) {
60
+ const whereParams = stringify({
61
+ where: config.where
62
+ });
63
+ url = `${baseUrl}?${whereParams}`;
64
+ } else {
65
+ url = `${baseUrl}`;
66
+ }
67
+ responseKey = 'totalDocs';
68
+ } else {
69
+ return {
70
+ slug,
71
+ value: undefined
72
+ };
73
+ }
74
+ const response = await fetch(url, {
75
+ credentials: 'include',
76
+ method: config.type === 'api' ? config.method ?? 'GET' : 'GET'
77
+ });
78
+ if (response.ok) {
79
+ const data = await response.json();
80
+ // Extract value from nested key (e.g., "data.count")
81
+ const keys = responseKey.split('.');
82
+ let value = data;
83
+ for (const key of keys){
84
+ value = value?.[key];
85
+ }
86
+ return {
87
+ slug,
88
+ value: typeof value === 'number' ? value : undefined
89
+ };
90
+ }
91
+ } catch (error) {
92
+ //eslint-disable-next-line no-console
93
+ console.error(`Failed to fetch badge data for ${slug}:`, error);
94
+ }
95
+ return {
96
+ slug,
97
+ value: undefined
98
+ };
99
+ }, [
100
+ apiRoute,
101
+ serverURL
102
+ ]);
103
+ // Fetch all badges on mount
104
+ useEffect(()=>{
105
+ if (badgesToFetch.length === 0) {
106
+ return;
107
+ }
108
+ const fetchAll = async ()=>{
109
+ const results = await Promise.all(badgesToFetch.map(fetchBadge));
110
+ const newValues = {};
111
+ for (const result of results){
112
+ if (result.value !== undefined) {
113
+ newValues[result.slug] = result.value;
114
+ }
115
+ }
116
+ setValues(newValues);
117
+ };
118
+ fetchAll().catch((err)=>{
119
+ //eslint-disable-next-line no-console
120
+ console.error('Error fetching badge data:', err);
121
+ });
122
+ }, [
123
+ badgesToFetch,
124
+ fetchBadge
125
+ ]);
126
+ return /*#__PURE__*/ _jsx(BadgeProvider, {
127
+ values: values,
128
+ children: children
129
+ });
130
+ };
131
+
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/EnhancedSidebar/InternalBadgeProvider/index.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig } from '@payloadcms/ui'\nimport { stringify } from 'qs-esm'\nimport React, { useCallback, useEffect, useMemo, useState } from 'react'\n\nimport type { BadgeConfig, BadgeValues, EnhancedSidebarConfig } from '../../../types'\n\nimport { BadgeProvider } from '../BadgeProvider'\n\ntype BadgeToFetch = {\n config: BadgeConfig\n slug: string\n}\n\nexport type InternalBadgeProviderProps = {\n children: React.ReactNode\n /**\n * Sidebar configuration containing badge configs\n */\n sidebarConfig: EnhancedSidebarConfig\n}\n\n/**\n * Internal provider that fetches all API-based badges once on mount.\n * This provider is automatically injected by the plugin.\n * Values are stored in context and don't refetch on navigation.\n */\nexport const InternalBadgeProvider: React.FC<InternalBadgeProviderProps> = ({\n children,\n sidebarConfig,\n}) => {\n const [values, setValues] = useState<BadgeValues>({})\n\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n // Collect all badges that need to be fetched (api and collection-count types)\n const badgesToFetch = useMemo(() => {\n const badges: BadgeToFetch[] = []\n\n // From tabs\n if (sidebarConfig.tabs) {\n for (const tab of sidebarConfig.tabs) {\n if (tab.badge && tab.badge.type !== 'provider') {\n badges.push({ slug: tab.id, config: tab.badge })\n }\n }\n }\n\n // From badges config\n if (sidebarConfig.badges) {\n for (const [slug, config] of Object.entries(sidebarConfig.badges)) {\n if (config.type !== 'provider') {\n badges.push({ slug, config })\n }\n }\n }\n\n return badges\n }, [sidebarConfig])\n\n // Fetch a single badge value\n const fetchBadge = useCallback(\n async (badge: BadgeToFetch): Promise<{ slug: string; value: number | undefined }> => {\n const { slug, config } = badge\n\n try {\n let url: string\n let responseKey: string\n\n if (config.type === 'api') {\n url = config.endpoint\n responseKey = config.responseKey ?? 'count'\n\n // If endpoint is relative, prepend serverURL\n if (!url.startsWith('http')) {\n url = `${serverURL || ''}${url}`\n }\n } else if (config.type === 'collection-count') {\n const collectionSlug = config.collectionSlug ?? slug\n const baseUrl = `${serverURL || ''}${apiRoute}/${collectionSlug}`\n\n if (config.where) {\n const whereParams = stringify({\n where: config.where,\n })\n url = `${baseUrl}?${whereParams}`\n } else {\n url = `${baseUrl}`\n }\n\n responseKey = 'totalDocs'\n } else {\n return { slug, value: undefined }\n }\n\n const response = await fetch(url, {\n credentials: 'include',\n method: config.type === 'api' ? (config.method ?? 'GET') : 'GET',\n })\n\n if (response.ok) {\n const data = await response.json()\n // Extract value from nested key (e.g., \"data.count\")\n const keys = responseKey.split('.')\n let value = data\n for (const key of keys) {\n value = value?.[key]\n }\n return { slug, value: typeof value === 'number' ? value : undefined }\n }\n } catch (error) {\n //eslint-disable-next-line no-console\n console.error(`Failed to fetch badge data for ${slug}:`, error)\n }\n\n return { slug, value: undefined }\n },\n [apiRoute, serverURL],\n )\n\n // Fetch all badges on mount\n useEffect(() => {\n if (badgesToFetch.length === 0) {\n return\n }\n\n const fetchAll = async () => {\n const results = await Promise.all(badgesToFetch.map(fetchBadge))\n\n const newValues: BadgeValues = {}\n for (const result of results) {\n if (result.value !== undefined) {\n newValues[result.slug] = result.value\n }\n }\n\n setValues(newValues)\n }\n\n fetchAll().catch((err) => {\n //eslint-disable-next-line no-console\n console.error('Error fetching badge data:', err)\n })\n }, [badgesToFetch, fetchBadge])\n\n return <BadgeProvider values={values}>{children}</BadgeProvider>\n}\n"],"names":["useConfig","stringify","React","useCallback","useEffect","useMemo","useState","BadgeProvider","InternalBadgeProvider","children","sidebarConfig","values","setValues","config","routes","api","apiRoute","serverURL","badgesToFetch","badges","tabs","tab","badge","type","push","slug","id","Object","entries","fetchBadge","url","responseKey","endpoint","startsWith","collectionSlug","baseUrl","where","whereParams","value","undefined","response","fetch","credentials","method","ok","data","json","keys","split","key","error","console","length","fetchAll","results","Promise","all","map","newValues","result","catch","err"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { BadgeConfig, ExtendedEntity } from '../../../types';
3
+ export type NavItemProps = {
4
+ badgeConfig?: BadgeConfig;
5
+ entity: ExtendedEntity;
6
+ href: string;
7
+ id: string;
8
+ isActive: boolean;
9
+ isCurrentPage: boolean;
10
+ };
11
+ export declare const NavItem: React.FC<NavItemProps>;