@caratlogic/design-system 0.1.10 → 0.1.12

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 (42) hide show
  1. package/dist/components/layouts/AppShell/AppShell.types.d.ts +14 -1
  2. package/dist/components/layouts/AppShell/AppShell.types.d.ts.map +1 -1
  3. package/dist/components/layouts/AppShell/DashboardShell.d.ts.map +1 -1
  4. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.accessibility.d.ts +4 -0
  5. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.accessibility.d.ts.map +1 -0
  6. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.d.ts +8 -0
  7. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.d.ts.map +1 -0
  8. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.types.d.ts +22 -0
  9. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.types.d.ts.map +1 -0
  10. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.utils.d.ts +4 -0
  11. package/dist/components/molecules/InAppNotificationCard/InAppNotificationCard.utils.d.ts.map +1 -0
  12. package/dist/components/molecules/InAppNotificationCard/index.d.ts +4 -0
  13. package/dist/components/molecules/InAppNotificationCard/index.d.ts.map +1 -0
  14. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.accessibility.d.ts +9 -0
  15. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.accessibility.d.ts.map +1 -0
  16. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.d.ts +9 -0
  17. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.d.ts.map +1 -0
  18. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.hooks.d.ts +12 -0
  19. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.hooks.d.ts.map +1 -0
  20. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.types.d.ts +41 -0
  21. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.types.d.ts.map +1 -0
  22. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.utils.d.ts +4 -0
  23. package/dist/components/organisms/InAppNotificationsPanel/InAppNotificationsPanel.utils.d.ts.map +1 -0
  24. package/dist/components/organisms/InAppNotificationsPanel/index.d.ts +5 -0
  25. package/dist/components/organisms/InAppNotificationsPanel/index.d.ts.map +1 -0
  26. package/dist/components/organisms/MediaGallery/MediaGallery.d.ts +1 -1
  27. package/dist/components/organisms/MediaGallery/MediaGallery.d.ts.map +1 -1
  28. package/dist/components/organisms/MediaGallery/MediaGallery.utils.d.ts +6 -0
  29. package/dist/components/organisms/MediaGallery/MediaGallery.utils.d.ts.map +1 -1
  30. package/dist/components/organisms/Navbar/Navbar.d.ts +1 -1
  31. package/dist/components/organisms/Navbar/Navbar.d.ts.map +1 -1
  32. package/dist/constants/filePatterns.d.ts +3 -0
  33. package/dist/constants/filePatterns.d.ts.map +1 -1
  34. package/dist/index.cjs.js +184 -19
  35. package/dist/index.cjs.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.esm.js +180 -20
  39. package/dist/index.esm.js.map +1 -1
  40. package/dist/utils/relativeTime.d.ts +6 -0
  41. package/dist/utils/relativeTime.d.ts.map +1 -0
  42. package/package.json +1 -1
package/dist/index.cjs.js CHANGED
@@ -2698,12 +2698,12 @@ const createLucideIcon = (iconName, iconNode) => {
2698
2698
  */
2699
2699
 
2700
2700
 
2701
- const __iconNode$17 = [
2701
+ const __iconNode$18 = [
2702
2702
  ["rect", { width: "20", height: "5", x: "2", y: "3", rx: "1", key: "1wp1u1" }],
2703
2703
  ["path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8", key: "1s80jp" }],
2704
2704
  ["path", { d: "M10 12h4", key: "a56b0p" }]
2705
2705
  ];
2706
- const Archive = createLucideIcon("archive", __iconNode$17);
2706
+ const Archive = createLucideIcon("archive", __iconNode$18);
2707
2707
 
2708
2708
  /**
2709
2709
  * @license lucide-react v0.539.0 - ISC
@@ -2713,11 +2713,11 @@ const Archive = createLucideIcon("archive", __iconNode$17);
2713
2713
  */
2714
2714
 
2715
2715
 
2716
- const __iconNode$16 = [
2716
+ const __iconNode$17 = [
2717
2717
  ["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
2718
2718
  ["path", { d: "M19 12H5", key: "x3x0zl" }]
2719
2719
  ];
2720
- const ArrowLeft = createLucideIcon("arrow-left", __iconNode$16);
2720
+ const ArrowLeft = createLucideIcon("arrow-left", __iconNode$17);
2721
2721
 
2722
2722
  /**
2723
2723
  * @license lucide-react v0.539.0 - ISC
@@ -2727,11 +2727,11 @@ const ArrowLeft = createLucideIcon("arrow-left", __iconNode$16);
2727
2727
  */
2728
2728
 
2729
2729
 
2730
- const __iconNode$15 = [
2730
+ const __iconNode$16 = [
2731
2731
  ["path", { d: "M5 12h14", key: "1ays0h" }],
2732
2732
  ["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
2733
2733
  ];
2734
- const ArrowRight = createLucideIcon("arrow-right", __iconNode$15);
2734
+ const ArrowRight = createLucideIcon("arrow-right", __iconNode$16);
2735
2735
 
2736
2736
  /**
2737
2737
  * @license lucide-react v0.539.0 - ISC
@@ -2741,11 +2741,32 @@ const ArrowRight = createLucideIcon("arrow-right", __iconNode$15);
2741
2741
  */
2742
2742
 
2743
2743
 
2744
- const __iconNode$14 = [
2744
+ const __iconNode$15 = [
2745
2745
  ["path", { d: "M4.929 4.929 19.07 19.071", key: "196cmz" }],
2746
2746
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
2747
2747
  ];
2748
- const Ban = createLucideIcon("ban", __iconNode$14);
2748
+ const Ban = createLucideIcon("ban", __iconNode$15);
2749
+
2750
+ /**
2751
+ * @license lucide-react v0.539.0 - ISC
2752
+ *
2753
+ * This source code is licensed under the ISC license.
2754
+ * See the LICENSE file in the root directory of this source tree.
2755
+ */
2756
+
2757
+
2758
+ const __iconNode$14 = [
2759
+ ["path", { d: "M10.268 21a2 2 0 0 0 3.464 0", key: "vwvbt9" }],
2760
+ [
2761
+ "path",
2762
+ {
2763
+ d: "M13.916 2.314A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.74 7.327A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673 9 9 0 0 1-.585-.665",
2764
+ key: "1tip0g"
2765
+ }
2766
+ ],
2767
+ ["circle", { cx: "18", cy: "8", r: "3", key: "1g0gzu" }]
2768
+ ];
2769
+ const BellDot = createLucideIcon("bell-dot", __iconNode$14);
2749
2770
 
2750
2771
  /**
2751
2772
  * @license lucide-react v0.539.0 - ISC
@@ -28257,6 +28278,53 @@ const FilterSelect = ({ label, value, onValueChange, options, resetValue = 'all'
28257
28278
  return (jsxRuntime.jsxs(Select, { value: value, onValueChange: onValueChange, keepOpenOnSelect: true, children: [jsxRuntime.jsxs(SelectTrigger, { className: cn(TRIGGER_BASE, active && TRIGGER_ACTIVE, triggerClassName), children: [jsxRuntime.jsx(Funnel, { className: cn('h-4 w-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(SelectValue, { placeholder: label }), active && (jsxRuntime.jsx("span", { className: "ml-1 inline-flex h-4 min-w-4 items-center justify-center rounded bg-primary/10 px-1 text-[10px] font-semibold leading-none text-primary", children: activeCount }))] })] }), jsxRuntime.jsxs(SelectContent, { className: cn(CONTENT_BASE, contentClassName), searchable: searchable, children: [jsxRuntime.jsx(SelectItem, { value: resetValue, withCheckbox: true, children: label }), options.map((opt) => (jsxRuntime.jsx(SelectItem, { value: opt.value, searchValue: opt.searchValue, withCheckbox: true, children: opt.label }, opt.value)))] })] }));
28258
28279
  };
28259
28280
 
28281
+ /**
28282
+ * Compact "time ago" label, e.g. "5m ago". Falls back to a locale date past a
28283
+ * week so older entries stay scannable.
28284
+ */
28285
+ function formatRelativeTime(value) {
28286
+ if (!value)
28287
+ return '';
28288
+ const then = value instanceof Date ? value.getTime() : new Date(value).getTime();
28289
+ if (Number.isNaN(then))
28290
+ return '';
28291
+ const minutes = Math.floor((Date.now() - then) / 60000);
28292
+ if (minutes < 1)
28293
+ return 'Just now';
28294
+ if (minutes < 60)
28295
+ return `${minutes}m ago`;
28296
+ const hours = Math.floor(minutes / 60);
28297
+ if (hours < 24)
28298
+ return `${hours}h ago`;
28299
+ const days = Math.floor(hours / 24);
28300
+ if (days < 7)
28301
+ return `${days}d ago`;
28302
+ return new Date(then).toLocaleDateString();
28303
+ }
28304
+
28305
+ /** Read entries sit flat on the surface; unread ones get a primary wash. */
28306
+ function getInAppNotificationCardClasses(isRead, className) {
28307
+ return cn('flex h-[5.125rem] w-full cursor-pointer gap-2.5 rounded-lg border px-3 py-3 text-left transition-colors', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50', isRead
28308
+ ? 'border-border bg-card hover:bg-muted/40'
28309
+ : 'border-primary/20 bg-primary/5 hover:bg-primary/10', className);
28310
+ }
28311
+ function getInAppNotificationIconClasses(isRead) {
28312
+ return cn('mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full', isRead ? 'bg-muted text-muted-foreground' : 'bg-primary/10 text-primary');
28313
+ }
28314
+
28315
+ const inAppNotificationCardA11y = {
28316
+ unreadLabel: 'Unread',
28317
+ };
28318
+
28319
+ /**
28320
+ * One row in a notification list. Unread state is signalled by a dot, a tinted
28321
+ * surface and a heavier title, so it never relies on colour alone.
28322
+ */
28323
+ const InAppNotificationCard = ({ notification, onSelect, defaultIcon, className, }) => {
28324
+ const { title, message, createdAt, isRead, subject, icon } = notification;
28325
+ return (jsxRuntime.jsxs("button", { type: "button", onClick: () => onSelect(notification), className: getInAppNotificationCardClasses(isRead, className), children: [jsxRuntime.jsx("span", { className: getInAppNotificationIconClasses(isRead), children: icon ?? defaultIcon ?? (jsxRuntime.jsx(BellDot, { className: "size-4", "aria-hidden": "true" })) }), jsxRuntime.jsxs("span", { className: "min-w-0 flex-1", children: [jsxRuntime.jsxs("span", { className: "flex items-baseline gap-2", children: [!isRead && (jsxRuntime.jsx("span", { role: "img", "aria-label": inAppNotificationCardA11y.unreadLabel, className: "size-1.5 shrink-0 rounded-full bg-primary" })), jsxRuntime.jsx("span", { className: cn('min-w-0 flex-1 truncate text-sm text-foreground', isRead ? 'font-medium' : 'font-semibold'), children: title }), jsxRuntime.jsx("time", { className: "shrink-0 text-[0.6875rem] text-muted-foreground tabular-nums", children: formatRelativeTime(createdAt) })] }), jsxRuntime.jsx("span", { className: "mt-1 line-clamp-1 text-xs text-secondary-foreground", children: message }), subject && (jsxRuntime.jsx("span", { className: "mt-0.5 block truncate text-[0.6875rem] font-medium text-muted-foreground", children: subject }))] })] }));
28326
+ };
28327
+
28260
28328
  const HEADER_TONE_CLASSES = {
28261
28329
  neutral: 'bg-[var(--section-header)]',
28262
28330
  gem: 'bg-primary/5 dark:bg-[#173626]',
@@ -64671,6 +64739,8 @@ const FILE_TYPE_PATTERNS = {
64671
64739
  DOCUMENT: /\.(doc|docx|xls|xlsx|ppt|pptx)($|\?)/i,
64672
64740
  VIDEO: /\.(mp4|m4v|avi|mov|wmv|flv|webm|mkv|ogv|mpe?g|3gp)($|\?)/i,
64673
64741
  AUDIO: /\.(mp3|wav|flac|aac|ogg)($|\?)/i,
64742
+ /** Interactive viewer pages (supplier 360° spinners), not media files. */
64743
+ EMBED: /\.html?($|\?|#)/i,
64674
64744
  };
64675
64745
  const isFileType = {
64676
64746
  pdf: (url) => FILE_TYPE_PATTERNS.PDF.test(url),
@@ -64678,6 +64748,7 @@ const isFileType = {
64678
64748
  document: (url) => FILE_TYPE_PATTERNS.DOCUMENT.test(url),
64679
64749
  video: (url) => FILE_TYPE_PATTERNS.VIDEO.test(url),
64680
64750
  audio: (url) => FILE_TYPE_PATTERNS.AUDIO.test(url),
64751
+ embed: (url) => FILE_TYPE_PATTERNS.EMBED.test(url),
64681
64752
  };
64682
64753
 
64683
64754
  /** Seek to ~1st frame for static video thumbnails (HTML5 media fragment). */
@@ -64693,6 +64764,12 @@ const createVideoMediaItem = (src, index, idPrefix = 'vid') => ({
64693
64764
  src,
64694
64765
  thumbSrc: videoThumbnailSrc(src),
64695
64766
  });
64767
+ /**
64768
+ * True when a "video" is really an interactive viewer page. Supplier 360°
64769
+ * spinners (Vision360, Varni) serve `text/html`, so `<video>` renders nothing
64770
+ * and there is no frame to seek for a thumbnail — they need an iframe.
64771
+ */
64772
+ const isEmbedMediaItem = (item) => item.kind === 'video' && isFileType.embed(item.src);
64696
64773
  const isPdfMediaItem = (item) => item.isPdf === true ||
64697
64774
  isFileType.pdf(item.src) ||
64698
64775
  (item.kind === 'certificate' && !isFileType.image(item.src));
@@ -65646,6 +65723,71 @@ const FileUploader = ({ accept, multiple = true, maxSize = 50, maxFiles = 5, lab
65646
65723
  return (jsxRuntime.jsxs("div", { className: cn('flex flex-col gap-4', className), children: [label && (jsxRuntime.jsx("label", { className: "text-sm font-medium text-foreground", children: label })), jsxRuntime.jsx(DropZone, { uploadSubject: uploadSubject, supportedFormats: supportedFormats, maxSize: maxSize, maxFiles: maxFiles, accept: accept, multiple: multiple, disabled: disabled, onFiles: handlePickerFiles }), files.length > 0 && (jsxRuntime.jsxs("div", { className: "space-y-2", children: [jsxRuntime.jsx("p", { className: "text-[13px] text-label", children: "Uploaded File" }), jsxRuntime.jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: onDragEnd, children: jsxRuntime.jsx(SortableContext, { items: files.map((f) => f.id), strategy: rectSortingStrategy, children: jsxRuntime.jsx("div", { className: "flex flex-wrap gap-3", children: files.map((file) => (jsxRuntime.jsx(FileCard, { file: file, sortable: sortable, disabled: disabled, onRemove: handleRemove, onPreview: previewFile }, file.id))) }) }) })] })), enableUrlInput && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-sm font-normal text-label", children: [jsxRuntime.jsx("span", { className: "h-px flex-1 bg-border" }), jsxRuntime.jsx("span", { children: "or" }), jsxRuntime.jsx("span", { className: "h-px flex-1 bg-border" })] }), jsxRuntime.jsx(UrlImportRow, { disabled: disabled, onAdd: handleAddUrl })] })), cropImages && currentCropFile && cropPreviewUrl && (jsxRuntime.jsx(ImageCropModal, { open: true, imageUrl: cropPreviewUrl, fileName: currentCropFile.name, fileType: currentCropFile.type, onCancel: handleCropCancel, onConfirm: handleCropConfirm }))] }));
65647
65724
  };
65648
65725
 
65726
+ /** Falls back to counting the current page when the caller has no total. */
65727
+ function resolveUnreadCount(notifications, unreadCount) {
65728
+ if (typeof unreadCount === 'number')
65729
+ return Math.max(0, unreadCount);
65730
+ return notifications.filter((notification) => !notification.isRead).length;
65731
+ }
65732
+
65733
+ // 1. React
65734
+ /** Derived display state for the panel header and body. */
65735
+ function useInAppNotificationsPanel(notifications, unreadCount) {
65736
+ return React.useMemo(() => {
65737
+ const resolved = resolveUnreadCount(notifications, unreadCount);
65738
+ return {
65739
+ unreadCount: resolved,
65740
+ hasUnread: resolved > 0,
65741
+ isEmpty: notifications.length === 0,
65742
+ };
65743
+ }, [notifications, unreadCount]);
65744
+ }
65745
+ /**
65746
+ * Fires `onLoadMore` when the sentinel scrolls into view inside `rootRef`.
65747
+ * No-ops where IntersectionObserver is unavailable (jsdom, older browsers), so
65748
+ * the list simply stops auto-loading rather than breaking.
65749
+ */
65750
+ function useInfiniteScrollSentinel(rootRef, onLoadMore, enabled) {
65751
+ const sentinelRef = React.useRef(null);
65752
+ React.useEffect(() => {
65753
+ const sentinel = sentinelRef.current;
65754
+ if (!sentinel || !enabled || !onLoadMore)
65755
+ return;
65756
+ if (typeof IntersectionObserver === 'undefined')
65757
+ return;
65758
+ const observer = new IntersectionObserver((entries) => {
65759
+ if (entries.some((entry) => entry.isIntersecting))
65760
+ onLoadMore();
65761
+ },
65762
+ // Start fetching slightly before the sentinel is actually visible.
65763
+ { root: rootRef.current, rootMargin: '80px' });
65764
+ observer.observe(sentinel);
65765
+ return () => observer.disconnect();
65766
+ }, [rootRef, onLoadMore, enabled]);
65767
+ return sentinelRef;
65768
+ }
65769
+
65770
+ const inAppNotificationsPanelA11y = {
65771
+ panelLabel: 'Notifications',
65772
+ listLabel: 'Notification list',
65773
+ markAllReadLabel: 'Mark all notifications as read',
65774
+ retryLabel: 'Retry loading notifications',
65775
+ };
65776
+ /** Non-modal dropdown, so `dialog` rather than `alertdialog`. */
65777
+ const IN_APP_NOTIFICATIONS_PANEL_ROLE = 'dialog';
65778
+
65779
+ /**
65780
+ * Notification list with header actions and loading / error / empty states.
65781
+ * Positioning is the caller's concern — render it inside a popover anchored to
65782
+ * whatever opens it (see `DashboardShell`'s `notificationsPanel` slot).
65783
+ */
65784
+ const InAppNotificationsPanel = ({ notifications, onSelect, unreadCount, totalCount, isLoading = false, hasNextPage = false, isFetchingNextPage = false, onLoadMore, isError = false, onRetry, onMarkAllRead, isMarkingAllRead = false, title = inAppNotificationsPanelA11y.panelLabel, emptyTitle = 'No notifications yet', emptyDescription, emptyIcon, emptyState, errorMessage = "Couldn't load notifications.", defaultIcon, className, }) => {
65785
+ const { unreadCount: resolvedUnread, hasUnread, isEmpty, } = useInAppNotificationsPanel(notifications, unreadCount);
65786
+ const listRef = React.useRef(null);
65787
+ const sentinelRef = useInfiniteScrollSentinel(listRef, onLoadMore, hasNextPage && !isLoading);
65788
+ return (jsxRuntime.jsxs("div", { role: IN_APP_NOTIFICATIONS_PANEL_ROLE, "aria-label": inAppNotificationsPanelA11y.panelLabel, className: cn('flex w-full flex-col overflow-hidden', className), children: [jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-border px-2 py-3", children: [jsxRuntime.jsx("div", { className: "min-w-0", children: jsxRuntime.jsxs("h2", { className: "flex min-w-0 items-center gap-2 text-sm font-semibold text-foreground", children: [jsxRuntime.jsx("span", { className: "truncate", children: title }), typeof totalCount === 'number' && (jsxRuntime.jsx("span", { className: "shrink-0 rounded-full bg-muted px-2 py-0.5 text-[0.6875rem] font-semibold text-muted-foreground tabular-nums", children: totalCount }))] }) }), jsxRuntime.jsx("span", { className: "flex shrink-0 items-center gap-2", children: hasUnread && onMarkAllRead && (jsxRuntime.jsx(Button$1, { variant: "ghost", size: "sm", onClick: onMarkAllRead, disabled: isMarkingAllRead, "aria-label": inAppNotificationsPanelA11y.markAllReadLabel, className: "h-7 px-2 text-xs font-medium text-primary hover:text-primary", children: "Mark all as read" })) })] }), jsxRuntime.jsx("div", { ref: listRef, className: "min-h-0 max-h-[16.75rem] flex-1 overflow-y-auto p-2", children: isLoading ? (jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground", children: [jsxRuntime.jsx(LoaderCircle, { className: "size-4 animate-spin", "aria-hidden": "true" }), "Loading notifications..."] })) : isError ? (jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2 py-10 text-center", children: [jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: errorMessage }), onRetry && (jsxRuntime.jsx(Button$1, { variant: "outline", size: "sm", onClick: onRetry, "aria-label": inAppNotificationsPanelA11y.retryLabel, className: "h-7 px-3 text-xs", children: "Try again" }))] })) : isEmpty ? ((emptyState ?? (jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-2 px-6 py-10 text-center", children: [jsxRuntime.jsx("span", { className: "flex size-10 items-center justify-center rounded-full bg-muted", children: emptyIcon ?? (jsxRuntime.jsx(Bell, { className: "size-5 text-muted-foreground", "aria-hidden": "true" })) }), jsxRuntime.jsx("p", { className: "text-sm font-medium text-foreground", children: emptyTitle }), emptyDescription && (jsxRuntime.jsx("p", { className: "max-w-[16rem] text-xs text-muted-foreground", children: emptyDescription }))] })))) : (jsxRuntime.jsxs("div", { role: "list", "aria-label": inAppNotificationsPanelA11y.listLabel, className: "flex flex-col gap-1.5", children: [notifications.map((notification) => (jsxRuntime.jsx(InAppNotificationCard, { notification: notification, onSelect: onSelect, defaultIcon: defaultIcon }, notification.id))), hasNextPage && (jsxRuntime.jsx("div", { ref: sentinelRef, "aria-hidden": "true", className: "h-px" })), isFetchingNextPage && (jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2 py-3 text-xs text-muted-foreground", children: [jsxRuntime.jsx(LoaderCircle, { className: "size-3.5 animate-spin", "aria-hidden": "true" }), "Loading more..."] }))] })) })] }));
65789
+ };
65790
+
65649
65791
  const stop = (fn) => (e) => {
65650
65792
  e.stopPropagation();
65651
65793
  fn?.();
@@ -65800,9 +65942,22 @@ const PdfPreview = ({ item }) => {
65800
65942
  const docsViewer = `https://docs.google.com/gview?embedded=1&url=${encodeURIComponent(item.src)}`;
65801
65943
  return (jsxRuntime.jsx("object", { data: item.src, type: "application/pdf", className: "h-full w-full bg-card", "aria-label": item.name ?? 'Certificate PDF', children: jsxRuntime.jsx("iframe", { title: item.name ?? 'Certificate PDF', className: "h-full w-full border-0 bg-card", src: docsViewer }) }));
65802
65944
  };
65945
+ /**
65946
+ * Videos arrive as either a real file or a supplier's 360° viewer page. The URL
65947
+ * settles most cases, but not every viewer ends in `.html` (v360, loupe360),
65948
+ * so a `<video>` that fails to load falls back to the iframe as well. Remounted
65949
+ * per selection via `key`, which resets that fallback.
65950
+ */
65951
+ const VideoOrEmbed = ({ item }) => {
65952
+ const [useEmbed, setUseEmbed] = React.useState(() => isEmbedMediaItem(item));
65953
+ if (useEmbed) {
65954
+ return (jsxRuntime.jsx("iframe", { src: item.src, title: item.name ?? '360\u00B0 viewer', className: "h-full w-full border-0 bg-card", allowFullScreen: true }));
65955
+ }
65956
+ return (jsxRuntime.jsx("video", { src: item.src, controls: true, onError: () => setUseEmbed(true), className: "h-full w-full object-cover", "aria-label": item.name ?? 'Video' }));
65957
+ };
65803
65958
  const MainMedia = ({ item }) => {
65804
65959
  if (item.kind === 'video') {
65805
- return (jsxRuntime.jsx("video", { src: item.src, controls: true, className: "h-full w-full object-cover", "aria-label": item.name ?? 'Video' }));
65960
+ return jsxRuntime.jsx(VideoOrEmbed, { item: item });
65806
65961
  }
65807
65962
  if (isPdfMediaItem(item)) {
65808
65963
  return jsxRuntime.jsx(PdfPreview, { item: item });
@@ -65812,7 +65967,7 @@ const MainMedia = ({ item }) => {
65812
65967
  const Thumbnail = ({ item, isActive, onSelect }) => {
65813
65968
  const Icon = KIND_ICON[item.kind];
65814
65969
  const isPdf = isPdfMediaItem(item);
65815
- const isVideoWithoutThumb = item.kind === 'video' && !item.thumbSrc;
65970
+ const isVideoWithoutThumb = item.kind === 'video' && (!item.thumbSrc || isEmbedMediaItem(item));
65816
65971
  return (jsxRuntime.jsxs("button", { type: "button", onClick: onSelect, "aria-label": `Select ${item.name ?? item.kind}`, "aria-pressed": isActive, className: cn('group relative flex aspect-square w-full cursor-pointer overflow-hidden rounded-lg border transition-colors', isActive
65817
65972
  ? 'border-primary ring-2 ring-primary/30'
65818
65973
  : 'border-border hover:border-primary/40'), children: [isPdf ? (jsxRuntime.jsxs("span", { className: "flex h-full w-full flex-col items-center justify-center gap-1 bg-muted p-2 text-muted-foreground", children: [jsxRuntime.jsx(Icon, { size: 20 }), jsxRuntime.jsx("span", { className: "text-[10px] font-medium uppercase", children: "PDF" })] })) : isVideoWithoutThumb ? (jsxRuntime.jsx("div", { className: "h-full w-full flex items-center justify-center bg-muted", children: jsxRuntime.jsx(Icon, { size: 24, className: "text-muted-foreground" }) })) : item.kind === 'video' ? (jsxRuntime.jsx("video", { src: item.thumbSrc ?? item.src, muted: true, playsInline: true, preload: "metadata", className: "h-full w-full object-cover", "aria-hidden": "true" })) : (jsxRuntime.jsx("img", { src: item.thumbSrc ?? item.src, alt: "", className: "h-full w-full object-cover" })), !isPdf && item.kind !== 'image' && !isVideoWithoutThumb && (jsxRuntime.jsx("span", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/30 text-white", children: jsxRuntime.jsx(Icon, { size: 20 }) })), isVideoWithoutThumb && (jsxRuntime.jsx("span", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/30" }))] }));
@@ -65833,7 +65988,7 @@ const MediaGallery = ({ items, thumbsVisible = 4, hideTabs = false, mainAspectCl
65833
65988
  (kinds.length > 0 ? (jsxRuntime.jsx(Tabs, { value: activeKind ?? undefined, onValueChange: (v) => setActiveKind(v), tabStyle: "boxed", size: "sm", children: jsxRuntime.jsx(TabsList, { children: kinds.map((kind) => {
65834
65989
  const Icon = KIND_ICON[kind];
65835
65990
  return (jsxRuntime.jsx(TabsTrigger, { value: kind, icon: jsxRuntime.jsx(Icon, { size: 14 }), children: MEDIA_KIND_LABEL[kind] }, kind));
65836
- }) }) })) : (jsxRuntime.jsx("div", { className: "h-9", "aria-hidden": "true" }))), selected && isZoomableImage ? (jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(ZoomSplitViewer, { src: selected.src, alt: selected.name ?? 'Media' }), !hideDownload && (jsxRuntime.jsx("div", { className: "absolute right-3 top-3 z-20", children: jsxRuntime.jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsxRuntime.jsx(Download, { className: "h-4 w-4" }) }) }))] })) : selected ? (jsxRuntime.jsxs("div", { className: cn('relative overflow-hidden rounded-xl border border-border bg-muted', mainAspectClassName), children: [jsxRuntime.jsx(MainMedia, { item: selected }), !hideDownload && (jsxRuntime.jsx("div", { className: "absolute right-3 top-3", children: jsxRuntime.jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsxRuntime.jsx(Download, { className: "h-4 w-4" }) }) }))] })) : (jsxRuntime.jsx("div", { className: cn('flex items-center justify-center rounded-xl border border-dashed border-border bg-muted/40 text-sm text-muted-foreground', mainAspectClassName), children: emptyText })), visibleItems.length > 1 && (jsxRuntime.jsx("div", { className: "grid gap-3", style: {
65991
+ }) }) })) : (jsxRuntime.jsx("div", { className: "h-9", "aria-hidden": "true" }))), selected && isZoomableImage ? (jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(ZoomSplitViewer, { src: selected.src, alt: selected.name ?? 'Media' }), !hideDownload && (jsxRuntime.jsx("div", { className: "absolute right-3 top-3 z-20", children: jsxRuntime.jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsxRuntime.jsx(Download, { className: "h-4 w-4" }) }) }))] })) : selected ? (jsxRuntime.jsxs("div", { className: cn('relative overflow-hidden rounded-xl border border-border bg-muted', mainAspectClassName), children: [jsxRuntime.jsx(MainMedia, { item: selected }, selected.id), !hideDownload && !isEmbedMediaItem(selected) && (jsxRuntime.jsx("div", { className: "absolute right-3 top-3", children: jsxRuntime.jsx("button", { type: "button", onClick: handleDownload, "aria-label": "Download media", className: "inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-border bg-card text-foreground transition-all hover:border-primary hover:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:border-primary focus-visible:shadow-[0_0_0_4px_rgba(49,122,81,0.10)] focus-visible:outline-none", children: jsxRuntime.jsx(Download, { className: "h-4 w-4" }) }) }))] })) : (jsxRuntime.jsx("div", { className: cn('flex items-center justify-center rounded-xl border border-dashed border-border bg-muted/40 text-sm text-muted-foreground', mainAspectClassName), children: emptyText })), visibleItems.length > 1 && (jsxRuntime.jsx("div", { className: "grid gap-3", style: {
65837
65992
  gridTemplateColumns: `repeat(${Math.min(visibleItems.length, thumbsVisible)}, minmax(0, 1fr))`,
65838
65993
  }, children: visibleItems.slice(0, thumbsVisible).map((item) => (jsxRuntime.jsx(Thumbnail, { item: item, isActive: selected?.id === item.id, onSelect: () => setSelected(item) }, item.id))) }))] }));
65839
65994
  };
@@ -65896,7 +66051,7 @@ const navbarIconButtonVariants = cva([
65896
66051
  });
65897
66052
  /** Tiny solid-pill badge ("8") absolutely positioned on the icon button. */
65898
66053
  const navbarIndicatorCountVariants = cva([
65899
- 'absolute -top-1.5 -right-1.5 inline-flex items-center justify-center',
66054
+ 'absolute -top-2.5 -right-2.5 inline-flex items-center justify-center',
65900
66055
  'min-w-4 h-4 px-1 rounded-full ring-2 ring-card',
65901
66056
  'text-[10px] font-medium leading-none tabular-nums text-white',
65902
66057
  ].join(' '), {
@@ -65983,10 +66138,11 @@ const NavbarTitle = ({ title, items, level = 1, className, ...props }) => {
65983
66138
  // ─── Right-side actions container ────────────────────────────────────────────
65984
66139
  const NavbarActions = ({ gap = 'md', className, children, ...props }) => (jsxRuntime.jsx("div", { className: cn(navbarActionsVariants({ gap }), className), ...props, children: children }));
65985
66140
  // ─── Icon button (with optional dot or count badge) ──────────────────────────
65986
- const NavbarIconButton = ({ appearance = 'plain', size = 'md', indicator = false, className, children, type = 'button', ...props }) => {
66141
+ const NavbarIconButton = React.forwardRef(({ appearance = 'plain', size = 'md', indicator = false, className, children, type = 'button', ...props }, ref) => {
65987
66142
  const resolved = resolveIndicator(indicator);
65988
- return (jsxRuntime.jsxs("button", { type: type, className: cn(navbarIconButtonVariants({ appearance, size }), className), ...props, children: [jsxRuntime.jsx("span", { "aria-hidden": "true", className: "inline-flex", children: children }), resolved && resolved.count !== null && (jsxRuntime.jsx("span", { role: "img", "aria-label": navbarA11y.notificationIndicatorLabel, className: cn(navbarIndicatorCountVariants({ tone: resolved.tone })), children: resolved.count })), resolved && resolved.count === null && (jsxRuntime.jsx("span", { role: "img", "aria-label": navbarA11y.notificationIndicatorLabel, className: cn(navbarIndicatorDotVariants({ tone: resolved.tone })) }))] }));
65989
- };
66143
+ return (jsxRuntime.jsxs("button", { ref: ref, type: type, className: cn(navbarIconButtonVariants({ appearance, size }), className), ...props, children: [jsxRuntime.jsx("span", { "aria-hidden": "true", className: "inline-flex", children: children }), resolved && resolved.count !== null && (jsxRuntime.jsx("span", { role: "img", "aria-label": navbarA11y.notificationIndicatorLabel, className: cn(navbarIndicatorCountVariants({ tone: resolved.tone })), children: resolved.count })), resolved && resolved.count === null && (jsxRuntime.jsx("span", { role: "img", "aria-label": navbarA11y.notificationIndicatorLabel, className: cn(navbarIndicatorDotVariants({ tone: resolved.tone })) }))] }));
66144
+ });
66145
+ NavbarIconButton.displayName = 'NavbarIconButton';
65990
66146
  // ─── Search input (matches Figma 288×36 muted pill) ──────────────────────────
65991
66147
  const NavbarSearch = ({ placeholder = 'Search', value, onChange, onClear, className, containerClassName, ...rest }) => {
65992
66148
  const isControlled = value !== undefined;
@@ -66070,7 +66226,8 @@ const NavLeaf = ({ item, currentPath, onNavigate }) => {
66070
66226
  const Icon = item.icon;
66071
66227
  const active = isPathActive(currentPath, item.path);
66072
66228
  const { collapsed, expandSidebar } = useCollapsedSidebar();
66073
- return (jsxRuntime.jsxs(SidebarMenuItem, { className: cn(active && 'flex items-center gap-1.5 group-data-[collapsible=icon]:block'), children: [active && jsxRuntime.jsx(ActiveRail, {}), jsxRuntime.jsxs(SidebarMenuButton, { isActive: active, tooltip: item.label, onClick: () => {
66229
+ return (jsxRuntime.jsxs(SidebarMenuItem, { className: cn(active &&
66230
+ 'flex items-center gap-1.5 group-data-[collapsible=icon]:block'), children: [active && jsxRuntime.jsx(ActiveRail, {}), jsxRuntime.jsxs(SidebarMenuButton, { isActive: active, tooltip: item.label, onClick: () => {
66074
66231
  if (collapsed)
66075
66232
  expandSidebar();
66076
66233
  if (item.path)
@@ -66120,12 +66277,15 @@ function resolveBreadcrumb(pageMeta, leafOverride) {
66120
66277
  return crumbs;
66121
66278
  return crumbs.map((crumb, index) => index === crumbs.length - 1 ? { ...crumb, label: leafOverride } : crumb);
66122
66279
  }
66123
- const ShellHeader = ({ pageMeta, breadcrumbLeafOverride, user, profileMenuItems, onLogout, showThemeToggle, isDark, onToggleTheme, showNotifications, notificationsIndicator, onNotificationsClick, headerActionsSlot, }) => {
66280
+ const ShellHeader = ({ pageMeta, breadcrumbLeafOverride, user, profileMenuItems, onLogout, showThemeToggle, isDark, onToggleTheme, showNotifications, notificationsIndicator, onNotificationsClick, notificationsPanel, notificationsOpen, onNotificationsOpenChange, headerActionsSlot, }) => {
66124
66281
  const { isMobile } = useSidebar();
66125
66282
  const breadcrumb = resolveBreadcrumb(pageMeta, breadcrumbLeafOverride);
66126
- return (jsxRuntime.jsxs(Navbar, { "aria-label": "Top navigation", children: [jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [isMobile && jsxRuntime.jsx(SidebarTrigger, {}), breadcrumb ? (jsxRuntime.jsx(NavbarTitle, { items: breadcrumb })) : (pageMeta?.title && jsxRuntime.jsx(NavbarTitle, { title: pageMeta.title }))] }), jsxRuntime.jsxs(NavbarActions, { gap: "lg", children: [headerActionsSlot, showThemeToggle && (jsxRuntime.jsx(NavbarIconButton, { "aria-label": isDark ? 'Switch to light theme' : 'Switch to dark theme', size: "md", onClick: onToggleTheme, children: isDark ? jsxRuntime.jsx(Sun, { size: 20 }) : jsxRuntime.jsx(Moon, { size: 20 }) })), showNotifications && (jsxRuntime.jsx(NavbarIconButton, { "aria-label": "Notifications", size: "md", indicator: notificationsIndicator || undefined, onClick: onNotificationsClick, children: jsxRuntime.jsx(Bell, { size: 20 }) })), jsxRuntime.jsx(ProfileMenu, { user: user, profileMenuItems: profileMenuItems, onLogout: onLogout })] })] }));
66283
+ return (jsxRuntime.jsxs(Navbar, { "aria-label": "Top navigation", children: [jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [isMobile && jsxRuntime.jsx(SidebarTrigger, {}), breadcrumb ? (jsxRuntime.jsx(NavbarTitle, { items: breadcrumb })) : (pageMeta?.title && jsxRuntime.jsx(NavbarTitle, { title: pageMeta.title }))] }), jsxRuntime.jsxs(NavbarActions, { gap: "lg", children: [headerActionsSlot, showThemeToggle && (jsxRuntime.jsx(NavbarIconButton, { "aria-label": isDark ? 'Switch to light theme' : 'Switch to dark theme', size: "md", onClick: onToggleTheme, children: isDark ? jsxRuntime.jsx(Sun, { size: 20 }) : jsxRuntime.jsx(Moon, { size: 20 }) })), showNotifications &&
66284
+ (notificationsPanel ? (jsxRuntime.jsxs(Popover, { open: notificationsOpen, onOpenChange: onNotificationsOpenChange, children: [jsxRuntime.jsx(PopoverTrigger, { asChild: true, children: jsxRuntime.jsx(NavbarIconButton, { "aria-label": "Notifications", size: "md", indicator: notificationsIndicator || undefined, onClick: onNotificationsClick, children: jsxRuntime.jsx(Bell, { size: 20 }) }) }), jsxRuntime.jsx(PopoverContent, { align: "end", sideOffset: 8, className: "w-[min(24rem,calc(100vw-2rem))] p-0", children: notificationsPanel })] })) : (jsxRuntime.jsx(NavbarIconButton, { "aria-label": "Notifications", size: "md", indicator: notificationsIndicator || undefined, onClick: onNotificationsClick, children: jsxRuntime.jsx(Bell, { size: 20 }) }))), jsxRuntime.jsx(ProfileMenu, { user: user, profileMenuItems: profileMenuItems, onLogout: onLogout })] })] }));
66127
66285
  };
66128
- const DashboardShell = ({ brand, navGroups, currentPath, onNavigate, pageMeta, breadcrumbLeafOverride, user, profileMenuItems, onLogout, showThemeToggle, isDark, onToggleTheme, showNotifications, notificationsIndicator, onNotificationsClick, headerActionsSlot, sidebarWidth, children, }) => (jsxRuntime.jsxs(SidebarProvider, { className: "h-screen min-h-0", style: { '--sidebar-width': sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH }, children: [jsxRuntime.jsx(ShellSidebar, { brand: brand, navGroups: navGroups, currentPath: currentPath, onNavigate: onNavigate }), jsxRuntime.jsxs(SidebarInset, { className: "flex h-screen flex-col overflow-hidden bg-background", children: [jsxRuntime.jsx(ShellHeader, { pageMeta: pageMeta, breadcrumbLeafOverride: breadcrumbLeafOverride, user: user, profileMenuItems: profileMenuItems, onLogout: onLogout, showThemeToggle: showThemeToggle, isDark: isDark, onToggleTheme: onToggleTheme, showNotifications: showNotifications, notificationsIndicator: notificationsIndicator, onNotificationsClick: onNotificationsClick, headerActionsSlot: headerActionsSlot }), jsxRuntime.jsx("main", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: children })] })] }));
66286
+ const DashboardShell = ({ brand, navGroups, currentPath, onNavigate, pageMeta, breadcrumbLeafOverride, user, profileMenuItems, onLogout, showThemeToggle, isDark, onToggleTheme, showNotifications, notificationsIndicator, onNotificationsClick, notificationsPanel, notificationsOpen, onNotificationsOpenChange, headerActionsSlot, sidebarWidth, children, }) => (jsxRuntime.jsxs(SidebarProvider, { className: "h-screen min-h-0", style: {
66287
+ '--sidebar-width': sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH,
66288
+ }, children: [jsxRuntime.jsx(ShellSidebar, { brand: brand, navGroups: navGroups, currentPath: currentPath, onNavigate: onNavigate }), jsxRuntime.jsxs(SidebarInset, { className: "flex h-screen flex-col overflow-hidden bg-background", children: [jsxRuntime.jsx(ShellHeader, { pageMeta: pageMeta, breadcrumbLeafOverride: breadcrumbLeafOverride, user: user, profileMenuItems: profileMenuItems, onLogout: onLogout, showThemeToggle: showThemeToggle, isDark: isDark, onToggleTheme: onToggleTheme, showNotifications: showNotifications, notificationsIndicator: notificationsIndicator, onNotificationsClick: onNotificationsClick, notificationsPanel: notificationsPanel, notificationsOpen: notificationsOpen, onNotificationsOpenChange: onNotificationsOpenChange, headerActionsSlot: headerActionsSlot }), jsxRuntime.jsx("main", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto", children: children })] })] }));
66129
66289
 
66130
66290
  function CreateFormPageHeader({ title, onBack, backLabel = 'Go back', }) {
66131
66291
  return (jsxRuntime.jsxs("div", { className: "mb-4 flex items-center justify-between gap-4", children: [jsxRuntime.jsx("h1", { className: "min-w-0 text-xl font-semibold leading-7 text-foreground", children: title }), jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-2", children: jsxRuntime.jsxs(Button$1, { type: "button", variant: "ghost", onClick: onBack, className: "h-8 shrink-0 gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-secondary-foreground hover:text-secondary-foreground", children: [jsxRuntime.jsx(ChevronLeft, { className: "h-4 w-4", "aria-hidden": true }), backLabel] }) })] }));
@@ -68206,6 +68366,8 @@ exports.HistoryTimelineShimmer = HistoryTimelineShimmer;
68206
68366
  exports.INVOICE_AUDIT_CONFIG = INVOICE_AUDIT_CONFIG;
68207
68367
  exports.IconButton = IconButton;
68208
68368
  exports.IdHyperlinkText = IdHyperlinkText;
68369
+ exports.InAppNotificationCard = InAppNotificationCard;
68370
+ exports.InAppNotificationsPanel = InAppNotificationsPanel;
68209
68371
  exports.InfoBanner = InfoBanner;
68210
68372
  exports.InventoryComboBox = InventoryComboBox;
68211
68373
  exports.InventoryItemCard = InventoryItemCard;
@@ -68327,6 +68489,8 @@ exports.getBadgeTypeForStatus = getBadgeTypeForStatus;
68327
68489
  exports.getChangePills = getChangePills;
68328
68490
  exports.getInitials = getInitials;
68329
68491
  exports.iconButtonVariants = iconButtonVariants;
68492
+ exports.inAppNotificationCardA11y = inAppNotificationCardA11y;
68493
+ exports.inAppNotificationsPanelA11y = inAppNotificationsPanelA11y;
68330
68494
  exports.isPathActive = isPathActive;
68331
68495
  exports.notify = notify;
68332
68496
  exports.releaseStaleOverlayBodyLock = releaseStaleOverlayBodyLock;
@@ -68335,6 +68499,7 @@ exports.useAuditShowMore = useAuditShowMore;
68335
68499
  exports.useBadge = useBadge;
68336
68500
  exports.useButton = useButton;
68337
68501
  exports.useDropdownMenu = useDropdownMenu;
68502
+ exports.useInAppNotificationsPanel = useInAppNotificationsPanel;
68338
68503
  exports.useNavbar = useNavbar;
68339
68504
  exports.useSidebar = useSidebar;
68340
68505
  exports.videoThumbnailSrc = videoThumbnailSrc;