@rspress-theme-anatole/theme-default 0.7.22-alpha4 → 0.7.23

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 (2) hide show
  1. package/dist/bundle.js +387 -200
  2. package/package.json +69 -69
package/dist/bundle.js CHANGED
@@ -339,48 +339,97 @@ function normalizeRoleList(value) {
339
339
  const list = Array.isArray(value) ? value : String(value).split(',');
340
340
  return list.map((role) => String(role).trim().toLowerCase()).filter(Boolean);
341
341
  }
342
+ // Cache for user roles fetched from userContext endpoint
343
+ let _cachedUserRoles = null;
344
+ let _userRolesFetchPromise = null;
345
+
342
346
  function getUserRoles() {
343
- if (typeof getUserFromStorage !== 'function') return [];
344
- const user = getUserFromStorage();
345
- const roles = user?.roles ?? user?.role ?? user?.groups ?? user?.group ?? [];
346
- return normalizeRoleList(roles);
347
+ // Return cached roles if available
348
+ if (_cachedUserRoles !== null) return _cachedUserRoles;
349
+ return [];
347
350
  }
348
- function hasRoleAccess(item, userRoles) {
349
- const requiredRoles = normalizeRoleList(item?.roles);
350
- if (!requiredRoles.length) return true;
351
- if (!userRoles.length) return false;
352
- return requiredRoles.some((role) => userRoles.includes(role));
351
+
352
+ async function fetchUserRoles(userContextUrl) {
353
+ if (!userContextUrl) return [];
354
+ if (_cachedUserRoles !== null) return _cachedUserRoles;
355
+ if (_userRolesFetchPromise) return _userRolesFetchPromise;
356
+
357
+ _userRolesFetchPromise = (async () => {
358
+ try {
359
+ const response = await fetch(userContextUrl, { credentials: 'include' });
360
+ if (!response.ok) {
361
+ console.warn('Failed to fetch user context:', response.status);
362
+ _cachedUserRoles = [];
363
+ return [];
364
+ }
365
+ const data = await response.json();
366
+ const roles = data?.permissions?.roles || [];
367
+ _cachedUserRoles = normalizeRoleList(roles);
368
+ // Dispatch event to trigger sidebar re-render
369
+ if (typeof window !== 'undefined') {
370
+ window.dispatchEvent(new Event('UserRolesReady'));
371
+ }
372
+ return _cachedUserRoles;
373
+ } catch (e) {
374
+ console.warn('Error fetching user context:', e);
375
+ _cachedUserRoles = [];
376
+ return [];
377
+ } finally {
378
+ _userRolesFetchPromise = null;
379
+ }
380
+ })();
381
+
382
+ return _userRolesFetchPromise;
353
383
  }
354
- function filterSidebarItems(items, userRoles) {
384
+ function isItemLocked(item, userRoles) {
385
+ const requiredRoles = normalizeRoleList(item?.roles);
386
+ // No roles required = public content (not locked)
387
+ if (!requiredRoles.length) return false;
388
+ // Has roles required but user has matching role = unlocked
389
+ if (userRoles.length && requiredRoles.some((role) => userRoles.includes(role))) return false;
390
+ // Has roles required but user doesn't have matching role = locked
391
+ return true;
392
+ }
393
+ function markLockedSidebarItems(items, userRoles) {
355
394
  if (!Array.isArray(items)) return items;
356
395
  return items.map((item) => {
357
396
  if (!item) return null;
358
397
  let nextItem = item;
398
+ const locked = isItemLocked(item, userRoles);
399
+ if (locked) {
400
+ nextItem = { ...nextItem, locked: true };
401
+ }
359
402
  if ('items' in item && Array.isArray(item.items)) {
360
- const filteredItems = filterSidebarItems(item.items, userRoles);
361
- if (filteredItems.length !== item.items.length) {
362
- nextItem = {
363
- ...item,
364
- items: filteredItems
365
- };
366
- }
367
- if (!nextItem.link && filteredItems.length === 0 && !('dividerType' in nextItem) && !('sectionHeaderText' in nextItem)) {
368
- return null;
369
- }
403
+ const markedItems = markLockedSidebarItems(item.items, userRoles);
404
+ nextItem = { ...nextItem, items: markedItems };
370
405
  }
371
- return hasRoleAccess(nextItem, userRoles) ? nextItem : null;
406
+ return nextItem;
372
407
  }).filter(Boolean);
373
408
  }
374
409
  function useSidebarData() {
375
410
  const { sidebar } = useLocaleSiteData();
411
+ const { siteData } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.usePageData)();
376
412
  const { pathname: rawPathname } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.useLocation)();
377
413
  const pathname = decodeURIComponent(rawPathname);
378
414
  const [roleVersion, setRoleVersion] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(0);
415
+
416
+ // Fetch user roles from userContext endpoint
417
+ (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
418
+ const userContextUrl = siteData?.auth?.endpoints?.userContext;
419
+ if (userContextUrl) {
420
+ fetchUserRoles(userContextUrl);
421
+ }
422
+ }, [siteData?.auth?.endpoints?.userContext]);
423
+
379
424
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
380
425
  if ('undefined' == typeof window) return;
381
426
  const handler = () => setRoleVersion((v) => v + 1);
382
427
  window.addEventListener('MetaRolesReady', handler);
383
- return () => window.removeEventListener('MetaRolesReady', handler);
428
+ window.addEventListener('UserRolesReady', handler);
429
+ return () => {
430
+ window.removeEventListener('MetaRolesReady', handler);
431
+ window.removeEventListener('UserRolesReady', handler);
432
+ };
384
433
  }, []);
385
434
  const sidebarData = (0, __WEBPACK_EXTERNAL_MODULE_react__.useMemo)(() => {
386
435
  const data = getSidebarDataGroup(sidebar, pathname);
@@ -417,8 +466,7 @@ function useSidebarData() {
417
466
  }
418
467
  } catch (e) {
419
468
  }
420
- console.log('[sidebar] data:', dataWithRoles);
421
- return filterSidebarItems(dataWithRoles, userRoles);
469
+ return markLockedSidebarItems(dataWithRoles, userRoles);
422
470
  }, [
423
471
  sidebar,
424
472
  pathname,
@@ -426,11 +474,83 @@ function useSidebarData() {
426
474
  ]);
427
475
  return sidebarData;
428
476
  }
477
+
478
+ // Get first accessible (not locked) page from sidebar items
479
+ function getFirstAccessiblePage(items) {
480
+ if (!Array.isArray(items)) return null;
481
+
482
+ for (const item of items) {
483
+ if (!item) continue;
484
+ if ('dividerType' in item) continue;
485
+
486
+ // If item is locked, skip it
487
+ if (item.locked === true) continue;
488
+
489
+ // If item has a link and is not locked, return it
490
+ if ('link' in item && item.link) {
491
+ return { text: item.text, link: item.link };
492
+ }
493
+
494
+ // If item has children, search recursively
495
+ if ('items' in item && Array.isArray(item.items)) {
496
+ const found = getFirstAccessiblePage(item.items);
497
+ if (found) return found;
498
+ }
499
+ }
500
+
501
+ return null;
502
+ }
503
+
504
+ // Hook to redirect to first accessible page if current page is locked
505
+ function useRedirectToAccessiblePage() {
506
+ const { pathname } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.useLocation)();
507
+ const navigate = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.useNavigate)();
508
+ const items = useSidebarData();
509
+
510
+ (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
511
+ if (!items || items.length === 0) return;
512
+
513
+ // Find current page in sidebar
514
+ const findCurrentPage = (items, targetPath) => {
515
+ for (const item of items) {
516
+ if (!item) continue;
517
+ if ('dividerType' in item) continue;
518
+
519
+ if ('link' in item && item.link) {
520
+ const itemPath = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.withBase)(item.link);
521
+ if ((0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.isEqualPath)(itemPath, targetPath)) {
522
+ return item;
523
+ }
524
+ }
525
+
526
+ if ('items' in item && Array.isArray(item.items)) {
527
+ const found = findCurrentPage(item.items, targetPath);
528
+ if (found) return found;
529
+ }
530
+ }
531
+ return null;
532
+ };
533
+
534
+ const currentPage = findCurrentPage(items, pathname);
535
+
536
+ // If current page is locked, redirect to first accessible page
537
+ if (currentPage && currentPage.locked === true) {
538
+ const firstAccessible = getFirstAccessiblePage(items);
539
+ if (firstAccessible && firstAccessible.link) {
540
+ navigate((0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.withBase)(firstAccessible.link), { replace: true });
541
+ }
542
+ }
543
+ }, [items, pathname, navigate]);
544
+ }
545
+
429
546
  function usePrevNextPage() {
430
547
  const { pathname } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.useLocation)();
431
548
  const items = useSidebarData();
432
549
  const flattenTitles = [];
433
550
  const walk = (sidebarItem) => {
551
+ // Skip locked items
552
+ if (sidebarItem.locked === true) return;
553
+
434
554
  if ('items' in sidebarItem) {
435
555
  if (sidebarItem.link) flattenTitles.push({
436
556
  text: sidebarItem.text,
@@ -635,9 +755,29 @@ function SidebarGroup(props) {
635
755
  const initialState = (0, __WEBPACK_EXTERNAL_MODULE_react__.useRef)('collapsed' in item && item.collapsed);
636
756
  const active = item.link && activeMatcher(item.link);
637
757
  const { collapsed, collapsible = true } = item;
758
+ const isLocked = item.locked === true;
759
+
760
+ // Lock icon SVG for groups
761
+ const GroupLockIcon = () => (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("svg", {
762
+ xmlns: "http://www.w3.org/2000/svg",
763
+ width: "14",
764
+ height: "14",
765
+ viewBox: "0 0 24 24",
766
+ fill: "none",
767
+ stroke: "currentColor",
768
+ strokeWidth: "2",
769
+ strokeLinecap: "round",
770
+ strokeLinejoin: "round",
771
+ style: { marginLeft: '6px', opacity: 0.6, flexShrink: 0 },
772
+ children: [
773
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
774
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })
775
+ ]
776
+ });
777
+
638
778
  const collapsibleIcon = (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
639
779
  style: {
640
- cursor: 'pointer',
780
+ cursor: isLocked ? 'not-allowed' : 'pointer',
641
781
  transition: 'transform 0.2s ease-out',
642
782
  transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)'
643
783
  },
@@ -675,6 +815,7 @@ function SidebarGroup(props) {
675
815
  }, []);
676
816
  const toggleCollapse = (e) => {
677
817
  e.stopPropagation();
818
+ if (isLocked) return; // Prevent collapse toggle for locked items
678
819
  setSidebarData((sidebarData) => {
679
820
  const newSidebarData = [
680
821
  ...sidebarData
@@ -692,23 +833,29 @@ function SidebarGroup(props) {
692
833
  className: "rspress-sidebar-section mt-0.5 block",
693
834
  "data-context": item.context,
694
835
  style: {
695
- marginLeft: 0 === depth ? 0 : '18px'
836
+ marginLeft: 0 === depth ? 0 : '18px',
837
+ opacity: isLocked ? 0.6 : 1
696
838
  },
697
839
  children: [
698
840
  (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsxs)("div", {
699
841
  className: `rspress-sidebar-collapse flex items-center ${active ? Sidebar_index_module.menuItemActive : Sidebar_index_module.menuItem}`,
700
842
  "data-context": item.context,
701
- onMouseEnter: () => item.link && preloadLink(item.link),
843
+ onMouseEnter: () => !isLocked && item.link && preloadLink(item.link),
702
844
  onClick: (e) => {
845
+ if (isLocked) {
846
+ e.preventDefault();
847
+ e.stopPropagation();
848
+ return;
849
+ }
703
850
  if (item.link) navigate((0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.withBase)((0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.normalizeHrefInRuntime)(item.link)));
704
851
  collapsible && toggleCollapse(e);
705
852
  },
706
853
  style: {
707
854
  // borderRadius: 0 === depth ? '0 var(--rp-radius) var(--rp-radius) 0' : void 0,
708
- cursor: collapsible || item.link ? 'pointer' : 'normal'
855
+ cursor: isLocked ? 'not-allowed' : (collapsible || item.link ? 'pointer' : 'normal')
709
856
  },
710
857
  children: [
711
- collapsible && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
858
+ collapsible && !isLocked && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
712
859
  className: `${Sidebar_index_module.collapseContainer} p-2 rounded-xl`,
713
860
  style: {
714
861
  paddingRight: 0
@@ -717,7 +864,7 @@ function SidebarGroup(props) {
717
864
  children: collapsibleIcon
718
865
  }),
719
866
  (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsxs)("h2", {
720
- className: "py-2 px-2 text-sm font-medium flex",
867
+ className: "py-2 px-2 text-sm font-medium flex items-center",
721
868
  style: {
722
869
  ...highlightTitleStyle
723
870
  },
@@ -733,12 +880,13 @@ function SidebarGroup(props) {
733
880
  letterSpacing: '-1%'
734
881
  },
735
882
  children: renderInlineMarkdown(item.text)
736
- })
883
+ }),
884
+ isLocked && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)(GroupLockIcon, {})
737
885
  ]
738
886
  })
739
887
  ]
740
888
  }),
741
- (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
889
+ !isLocked && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
742
890
  ref: containerRef,
743
891
  className: "transition-all duration-300 ease-in-out",
744
892
  style: {
@@ -774,6 +922,7 @@ function SidebarItem(props) {
774
922
  const { item, depth = 0, activeMatcher, id, setSidebarData } = props;
775
923
  const active = 'link' in item && item.link && activeMatcher(item.link);
776
924
  const ref = (0, __WEBPACK_EXTERNAL_MODULE_react__.useRef)(null);
925
+ const isLocked = item.locked === true;
777
926
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
778
927
  if (active) ref.current?.scrollIntoView({
779
928
  block: 'center'
@@ -787,6 +936,65 @@ function SidebarItem(props) {
787
936
  collapsed: item.collapsed,
788
937
  setSidebarData: setSidebarData
789
938
  }, `${item.text}-${id}`);
939
+
940
+ // Lock icon SVG
941
+ const LockIcon = () => (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("svg", {
942
+ xmlns: "http://www.w3.org/2000/svg",
943
+ width: "14",
944
+ height: "14",
945
+ viewBox: "0 0 24 24",
946
+ fill: "none",
947
+ stroke: "currentColor",
948
+ strokeWidth: "2",
949
+ strokeLinecap: "round",
950
+ strokeLinejoin: "round",
951
+ style: { marginLeft: '6px', opacity: 0.6, flexShrink: 0 },
952
+ children: [
953
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
954
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })
955
+ ]
956
+ });
957
+
958
+ if (isLocked) {
959
+ // Locked item - show lock icon and prevent navigation
960
+ return (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
961
+ className: Sidebar_index_module.menuLink,
962
+ onClick: (e) => {
963
+ e.preventDefault();
964
+ e.stopPropagation();
965
+ },
966
+ style: { cursor: 'not-allowed' },
967
+ children: (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsxs)("div", {
968
+ ref: ref,
969
+ className: `${Sidebar_index_module.menuItem} mt-0.5 py-2 px-3 font-medium flex items-center justify-between`,
970
+ style: {
971
+ fontSize: 0 === depth ? '14px' : '13px',
972
+ marginLeft: 0 === depth ? 0 : '18px',
973
+ opacity: 0.6,
974
+ ...0 === depth ? highlightTitleStyle : {}
975
+ },
976
+ children: [
977
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsxs)("span", {
978
+ className: "flex items-center",
979
+ children: [
980
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)(__WEBPACK_EXTERNAL_MODULE__theme_75e53063__.Tag, {
981
+ tag: item.tag
982
+ }),
983
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("span", {
984
+ style: {
985
+ fontSize: 0 === depth ? '14px' : '13px',
986
+ fontWeight: 400
987
+ },
988
+ children: renderInlineMarkdown(item.text)
989
+ })
990
+ ]
991
+ }),
992
+ (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)(LockIcon, {})
993
+ ]
994
+ })
995
+ });
996
+ }
997
+
790
998
  return (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)(__WEBPACK_EXTERNAL_MODULE__theme_75e53063__.Link, {
791
999
  href: (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.normalizeHrefInRuntime)(item.link),
792
1000
  className: Sidebar_index_module.menuLink,
@@ -1741,6 +1949,7 @@ const Layout = (props) => {
1741
1949
  const { pageType, lang: currentLang, title: articleTitle, frontmatter = {} } = page;
1742
1950
  const localesData = useLocaleSiteData();
1743
1951
  useRedirect4FirstVisit();
1952
+ useRedirectToAccessiblePage();
1744
1953
  let title = frontmatter.title ?? articleTitle;
1745
1954
  const mainTitle = siteData.title || localesData.title || '';
1746
1955
  title = title &&
@@ -4663,11 +4872,6 @@ function NavVersions() {
4663
4872
  });
4664
4873
  }
4665
4874
  // Utility functions for cookie management
4666
- function setCookie(name, value, days = 7) {
4667
- const expires = new Date();
4668
- expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
4669
- document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};expires=${expires.toUTCString()};path=/`;
4670
- }
4671
4875
  function getCookie(name) {
4672
4876
  const nameEQ = name + "=";
4673
4877
  const ca = document.cookie.split(';');
@@ -4687,9 +4891,6 @@ function getCookie(name) {
4687
4891
  }
4688
4892
  return null;
4689
4893
  }
4690
- function deleteCookie(name) {
4691
- document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/`;
4692
- }
4693
4894
  // Decode Base64 string (supports Unicode)
4694
4895
  function decodeBase64(base64String) {
4695
4896
  try {
@@ -4718,6 +4919,87 @@ function decodeBase64(base64String) {
4718
4919
  return null;
4719
4920
  }
4720
4921
  }
4922
+ // Generate initials from username (e.g., "Jake Thai" -> "JT")
4923
+ function getInitialsFromName(name) {
4924
+ if (!name) return '?';
4925
+ const parts = name.trim().split(/\s+/);
4926
+ if (parts.length === 1) {
4927
+ return parts[0].charAt(0).toUpperCase();
4928
+ }
4929
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
4930
+ }
4931
+
4932
+ // Generate a consistent color based on the username
4933
+ function getAvatarColor(name) {
4934
+ // Microsoft Fluent UI persona colors
4935
+ const colors = [
4936
+ '#0078D4', // Microsoft Blue
4937
+ '#107C10', // Green
4938
+ '#5C2D91', // Purple
4939
+ '#B4009E', // Magenta
4940
+ '#D83B01', // Orange
4941
+ '#E74856', // Red
4942
+ '#00B7C3', // Teal
4943
+ '#0099BC', // Cyan
4944
+ '#7A7574', // Gray
4945
+ '#69797E', // Dark Gray
4946
+ '#498205', // Lime Green
4947
+ '#847545', // Gold
4948
+ '#C239B3', // Pink
4949
+ '#038387', // Dark Teal
4950
+ '#8764B8', // Light Purple
4951
+ '#CA5010', // Burnt Orange
4952
+ ];
4953
+ // Generate a hash from the name for consistent color
4954
+ let hash = 0;
4955
+ const str = name || 'User';
4956
+ for (let i = 0; i < str.length; i++) {
4957
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
4958
+ }
4959
+ return colors[Math.abs(hash) % colors.length];
4960
+ }
4961
+
4962
+ // Generate avatar as base64 PNG image
4963
+ function generateAvatarBase64(username, size = 64) {
4964
+ if (typeof document === 'undefined') return null;
4965
+
4966
+ const initials = getInitialsFromName(username);
4967
+ const bgColor = getAvatarColor(username);
4968
+
4969
+ // Use higher resolution for crisp rendering
4970
+ const scale = 2;
4971
+ const actualSize = size * scale;
4972
+
4973
+ // Create canvas
4974
+ const canvas = document.createElement('canvas');
4975
+ canvas.width = actualSize;
4976
+ canvas.height = actualSize;
4977
+ const ctx = canvas.getContext('2d');
4978
+
4979
+ // Enable anti-aliasing
4980
+ ctx.imageSmoothingEnabled = true;
4981
+ ctx.imageSmoothingQuality = 'high';
4982
+
4983
+ // Draw circle background
4984
+ ctx.beginPath();
4985
+ ctx.arc(actualSize / 2, actualSize / 2, actualSize / 2, 0, Math.PI * 2);
4986
+ ctx.fillStyle = bgColor;
4987
+ ctx.fill();
4988
+
4989
+ // Draw initials text
4990
+ ctx.fillStyle = '#FFFFFF';
4991
+
4992
+ const fontSize = actualSize * 0.42;
4993
+ ctx.font = `400 ${fontSize}px "Segoe UI", "Segoe UI Web", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif`;
4994
+ ctx.textAlign = 'center';
4995
+ ctx.textBaseline = 'middle';
4996
+
4997
+ // Slight vertical adjustment for better visual centering
4998
+ ctx.fillText(initials, actualSize / 2, actualSize / 2 + (fontSize * 0.04));
4999
+
5000
+ // Return as base64
5001
+ return canvas.toDataURL('image/png');
5002
+ }
4721
5003
  // Parse KBGateway-User-Context cookie (Base64 encoded)
4722
5004
  function parseKBGatewayUserContext(cookieValue) {
4723
5005
  if (!cookieValue) return null;
@@ -4743,8 +5025,7 @@ function parseKBGatewayUserContext(cookieValue) {
4743
5025
  return {
4744
5026
  username: username,
4745
5027
  email: parsed.email || null,
4746
- roles: parsed.roles || parsed.role || parsed.groups || parsed.group || null,
4747
- avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAYCAYAAAAPtVbGAAAAAXNSR0IArs4c6QAAAsJJREFUSEullU1IFHEYxp9ndpc+KHLzAyyDIIjO+TEzqWDtbFYXK/Bi0Cm61Ck6iEFsFEhFtyKiY2SBh0AIyZ1VQ2P/q3hMKIgOkUJahgRG6+4bo7kfzs7OSnPb+b/v8/s/78csscVHLP0T7dShUmkSNa5BoCMTvMexyZmNGG6FIRE9CnIElE7GUyMbuXLSPIKsvAegFeh9pa0anN9bg0SNHxCEQSwhrqoJiDgalpH1uOwAbXWhYohEmpvBwFReTEzaKSXRliaINu1VEdqKlUPWb1sYL7SVJifMFmiS+m+IWGYPIM9dQtQuomr/AJa+rHpAhmirroqciNtFTtMph0T04yBHi0H8iXBDDQcHM74QscxeQPo9p1DYx0Sy/98AvATQCOABbfW44hEWy3BKESgz6hnaKlhuFco6Ecu4A+CG7y4J7jKhej2b73UgjY0hhEMrPi420jOYW97J2dk/pfQ8nUjUvAWRm74ucoXnbcaTJeNLQny22JtrqwAB1/aXhkTMh6BcqdhF3s0jxpNXN+e5INLdHSixXI65Z9DQj2PJj3jXehTI9EFwznWR8IGgsxuF790Qy3gN4ExB0AraTu1iLOYqg7S27saOzHcAofx2YphxVZhf/BWWGDRMru1FHh6SPRxOLXtOYYdxEEF8LjgXtKkgY/neFDkRS58G2JRLELxiQp33641YxgcAhwvyZphQOZ0cRE631yKd/lYkGEjv45uZeX+I3gZwoiguFKrj8MSC8y4PsYwxAB1FgZKtYWLKqXnZRzrb65FJz20KektbremtQSTSUg1qiy4lTZo4ksr9V3v2JWpcguCp63x1ey3HxxfXIZaRBGC4gsgFxpN1vk4sw/mc5Ccsn6BoK5NytqMKv34veQoRXYyrIU8XEf0yyCee+duCeymWPgewvuxts+zhaPLF5hixzOuA3Pfp2PxfA8n5ENhQazEAAAAASUVORK5CYII="
5028
+ avatar: generateAvatarBase64(username)
4748
5029
  };
4749
5030
  } catch (e) {
4750
5031
  console.warn('Failed to parse KBGateway-User-Context:', e);
@@ -4761,8 +5042,7 @@ function parseKBGatewayUserContext(cookieValue) {
4761
5042
  return {
4762
5043
  username: username,
4763
5044
  email: parsed.email || null,
4764
- roles: parsed.roles || parsed.role || parsed.groups || parsed.group || null,
4765
- avatar: parsed.Avatar || parsed.avatar || null
5045
+ avatar: generateAvatarBase64(username)
4766
5046
  };
4767
5047
  } catch (e2) {
4768
5048
  console.warn('Failed to parse as JSON:', e2);
@@ -4772,54 +5052,12 @@ function parseKBGatewayUserContext(cookieValue) {
4772
5052
  }
4773
5053
  // LocalStorage key for user data
4774
5054
  const USER_STORAGE_KEY = 'kb_user_data';
4775
- // Get user data from localStorage
4776
- function getUserFromStorage() {
4777
- if (typeof localStorage === 'undefined') return null;
4778
-
4779
- try {
4780
- const storedData = localStorage.getItem(USER_STORAGE_KEY);
4781
- if (storedData) {
4782
- return JSON.parse(storedData);
4783
- }
4784
- } catch (e) {
4785
- console.warn('Failed to get user from localStorage:', e);
4786
- localStorage.removeItem(USER_STORAGE_KEY);
4787
- }
4788
- return null;
4789
- }
4790
- // Save user data to localStorage
4791
- function saveUserToStorage(userData) {
4792
- if (typeof localStorage === 'undefined') return;
4793
-
4794
- try {
4795
- if (userData) {
4796
- localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userData));
4797
- } else {
4798
- localStorage.removeItem(USER_STORAGE_KEY);
4799
- }
4800
- } catch (e) {
4801
- console.warn('Failed to save user to localStorage:', e);
4802
- }
4803
- }
4804
- // Remove user data from localStorage
4805
- function removeUserFromStorage() {
4806
- if (typeof localStorage === 'undefined') return;
4807
- localStorage.removeItem(USER_STORAGE_KEY);
4808
- }
4809
- // Sync cookie to localStorage and return user data
4810
- function syncCookieToStorage(cookieName) {
4811
- const cookieValue = getCookie(cookieName);
4812
- const userData = parseKBGatewayUserContext(cookieValue);
4813
-
4814
- if (userData) {
4815
- saveUserToStorage(userData);
4816
- } else {
4817
- removeUserFromStorage();
4818
- }
4819
-
4820
- return userData;
4821
- }
5055
+
4822
5056
  // User Authentication Component
5057
+ // Cache user data globally to prevent flicker between page navigations
5058
+ let cachedUserData = null;
5059
+ let isClientInitialized = false;
5060
+
4823
5061
  function UserAuth() {
4824
5062
  const { siteData } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.usePageData)();
4825
5063
  const dropdownRef = (0, __WEBPACK_EXTERNAL_MODULE_react__.useRef)(null);
@@ -4827,52 +5065,54 @@ function UserAuth() {
4827
5065
  const authEnabled = siteData?.auth?.enabled !== false;
4828
5066
  const showSignInButton = siteData?.auth?.showSignInButton !== false;
4829
5067
 
4830
- // Get sign in URL from site config
5068
+ // Get sign in /sign out URL from site config
4831
5069
  const signInUrl = siteData?.auth?.endpoints?.signIn;
5070
+ const signOutUrl = siteData?.auth?.endpoints?.signOut;
4832
5071
 
4833
5072
  // Cookie name for KBGateway
4834
5073
  const KB_COOKIE_NAME = 'KBGateway-User-Context';
4835
5074
 
4836
- const [user, setUser] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(() => {
5075
+ // Helper function to get user from cookie directly
5076
+ const getUserFromCookie = () => {
4837
5077
  if (typeof document === 'undefined') return null;
4838
-
4839
- // 1. First try to get from localStorage (instant, no flicker)
4840
- const storedUser = getUserFromStorage();
4841
- if (storedUser) {
4842
- return storedUser;
5078
+ const cookieValue = getCookie(KB_COOKIE_NAME);
5079
+ return parseKBGatewayUserContext(cookieValue);
5080
+ };
5081
+
5082
+ // Initialize from cache first, then cookie - prevents flicker on navigation
5083
+ const [user, setUser] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(() => {
5084
+ // Use cached data first for instant render
5085
+ if (cachedUserData) return cachedUserData;
5086
+ // Fall back to cookie (only works on client)
5087
+ if (typeof document !== 'undefined') {
5088
+ const cookieUser = getUserFromCookie();
5089
+ if (cookieUser) {
5090
+ cachedUserData = cookieUser;
5091
+ isClientInitialized = true;
5092
+ }
5093
+ return cookieUser;
4843
5094
  }
4844
-
4845
- // 2. If not in localStorage, check cookie and sync
4846
- return syncCookieToStorage(KB_COOKIE_NAME);
5095
+ return null;
4847
5096
  });
4848
5097
 
5098
+ // Track if component has mounted (for SSR hydration)
5099
+ const [isMounted, setIsMounted] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(isClientInitialized);
5100
+
4849
5101
  const [showDropdown, setShowDropdown] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(false);
4850
5102
 
4851
- // Sync cookie to localStorage on mount and periodically check for changes
5103
+ // Check cookie on mount and update cache
4852
5104
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
4853
- // Sync cookie to localStorage on mount
4854
- const userData = syncCookieToStorage(KB_COOKIE_NAME);
5105
+ const userData = getUserFromCookie();
4855
5106
  if (userData) {
5107
+ cachedUserData = userData;
4856
5108
  setUser(userData);
5109
+ } else if (isClientInitialized) {
5110
+ // Cookie was removed (signed out) - only clear if we were initialized
5111
+ cachedUserData = null;
5112
+ setUser(null);
4857
5113
  }
4858
-
4859
- // Listen for storage events to sync across tabs
4860
- const handleStorageChange = (e) => {
4861
- if (e.key === USER_STORAGE_KEY) {
4862
- if (e.newValue) {
4863
- try {
4864
- setUser(JSON.parse(e.newValue));
4865
- } catch (err) {
4866
- setUser(null);
4867
- }
4868
- } else {
4869
- setUser(null);
4870
- }
4871
- }
4872
- };
4873
-
4874
- window.addEventListener('storage', handleStorageChange);
4875
- return () => window.removeEventListener('storage', handleStorageChange);
5114
+ isClientInitialized = true;
5115
+ setIsMounted(true);
4876
5116
  }, []);
4877
5117
 
4878
5118
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
@@ -4887,96 +5127,43 @@ function UserAuth() {
4887
5127
  return () => document.removeEventListener('mousedown', handleClickOutside);
4888
5128
  }, []);
4889
5129
 
4890
- const handleSignIn = async () => {
5130
+ const handleSignIn = () => {
4891
5131
  if (!signInUrl) {
4892
5132
  console.warn('Sign in URL not configured');
4893
5133
  return;
4894
5134
  }
4895
5135
 
4896
- try {
4897
- // Center the popup window
4898
- const width = 600;
4899
- const height = 600;
4900
- const left = window.top.outerWidth / 2 + window.top.screenX - width / 2;
4901
- const top = window.top.outerHeight / 2 + window.top.screenY - height / 2;
4902
-
4903
- // Open sign in URL in new window/tab
4904
- const authWindow = window.open(signInUrl, '_blank', `width=${width},height=${height},left=${left},top=${top}`);
4905
-
4906
- // Listen for messages from auth window
4907
- const handleMessage = (event) => {
4908
- if (event.origin !== new URL(signInUrl).origin) return;
4909
-
4910
- if (event.data && event.data.type === 'AUTH_SUCCESS') {
4911
- const userData = {
4912
- username: event.data.username,
4913
- email: event.data.email,
4914
- avatar: event.data.avatar
4915
- };
4916
-
4917
- // Save to localStorage
4918
- saveUserToStorage(userData);
4919
- setUser(userData);
4920
- authWindow.close();
4921
-
4922
- // Remove event listener
4923
- window.removeEventListener('message', handleMessage);
4924
- }
4925
- };
4926
-
4927
- window.addEventListener('message', handleMessage);
4928
-
4929
- // Poll for KBGateway-User-Context cookie
4930
- const pollInterval = setInterval(() => {
4931
- // Sync cookie to localStorage and get user data
4932
- const userData = syncCookieToStorage(KB_COOKIE_NAME);
4933
-
4934
- if (userData) {
4935
- setUser(userData);
4936
- clearInterval(pollInterval);
4937
- window.removeEventListener('message', handleMessage);
4938
-
4939
- // Close popup if still open
4940
- if (authWindow && !authWindow.closed) {
4941
- authWindow.close();
4942
- }
4943
- }
4944
-
4945
- // Also check if popup was closed manually
4946
- if (authWindow && authWindow.closed) {
4947
- clearInterval(pollInterval);
4948
- window.removeEventListener('message', handleMessage);
4949
-
4950
- // Final sync cookie to localStorage
4951
- const finalUserData = syncCookieToStorage(KB_COOKIE_NAME);
4952
- if (finalUserData) {
4953
- setUser(finalUserData);
4954
- }
4955
- }
4956
- }, 500); // Check every 500ms
4957
-
4958
- // Timeout after 5 minutes
4959
- setTimeout(() => {
4960
- clearInterval(pollInterval);
4961
- window.removeEventListener('message', handleMessage);
4962
- }, 300000);
4963
-
4964
-
4965
- } catch (error) {
4966
- console.error('Sign in error:', error);
4967
- }
5136
+ // Redirect directly to sign in URL
5137
+ window.location.href = signInUrl;
4968
5138
  };
4969
5139
 
4970
5140
  const handleSignOut = () => {
4971
- deleteCookie(KB_COOKIE_NAME);
4972
- removeUserFromStorage();
4973
- setUser(null);
4974
- setShowDropdown(false);
5141
+ if (!signOutUrl) {
5142
+ console.warn('Sign out URL not configured');
5143
+ return;
5144
+ }
5145
+ fetch(signOutUrl)
5146
+ .then(() => {
5147
+ cachedUserData = null; // Clear cache on sign out
5148
+ setUser(null);
5149
+ setShowDropdown(false);
5150
+ })
5151
+ .catch((error) => {
5152
+ console.error('Error signing out:', error);
5153
+ });
4975
5154
  };
4976
5155
 
4977
5156
  if (!authEnabled) {
4978
5157
  return null;
4979
5158
  }
5159
+
5160
+ // Don't render anything until client is mounted to prevent hydration mismatch
5161
+ if (!isMounted) {
5162
+ // Return a placeholder with same dimensions to prevent layout shift
5163
+ return (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
5164
+ style: { width: '28px', height: '28px' }
5165
+ });
5166
+ }
4980
5167
 
4981
5168
  if (!user) {
4982
5169
  if (!showSignInButton) {
@@ -4993,7 +5180,7 @@ function UserAuth() {
4993
5180
  });
4994
5181
  }
4995
5182
 
4996
- const userAvatar = user.avatar ?? "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAYCAYAAAAPtVbGAAAAAXNSR0IArs4c6QAAAsJJREFUSEullU1IFHEYxp9ndpc+KHLzAyyDIIjO+TEzqWDtbFYXK/Bi0Cm61Ck6iEFsFEhFtyKiY2SBh0AIyZ1VQ2P/q3hMKIgOkUJahgRG6+4bo7kfzs7OSnPb+b/v8/s/78csscVHLP0T7dShUmkSNa5BoCMTvMexyZmNGG6FIRE9CnIElE7GUyMbuXLSPIKsvAegFeh9pa0anN9bg0SNHxCEQSwhrqoJiDgalpH1uOwAbXWhYohEmpvBwFReTEzaKSXRliaINu1VEdqKlUPWb1sYL7SVJifMFmiS+m+IWGYPIM9dQtQuomr/AJa+rHpAhmirroqciNtFTtMph0T04yBHi0H8iXBDDQcHM74QscxeQPo9p1DYx0Sy/98AvATQCOABbfW44hEWy3BKESgz6hnaKlhuFco6Ecu4A+CG7y4J7jKhej2b73UgjY0hhEMrPi420jOYW97J2dk/pfQ8nUjUvAWRm74ucoXnbcaTJeNLQny22JtrqwAB1/aXhkTMh6BcqdhF3s0jxpNXN+e5INLdHSixXI65Z9DQj2PJj3jXehTI9EFwznWR8IGgsxuF790Qy3gN4ExB0AraTu1iLOYqg7S27saOzHcAofx2YphxVZhf/BWWGDRMru1FHh6SPRxOLXtOYYdxEEF8LjgXtKkgY/neFDkRS58G2JRLELxiQp33641YxgcAhwvyZphQOZ0cRE631yKd/lYkGEjv45uZeX+I3gZwoiguFKrj8MSC8y4PsYwxAB1FgZKtYWLKqXnZRzrb65FJz20KektbremtQSTSUg1qiy4lTZo4ksr9V3v2JWpcguCp63x1ey3HxxfXIZaRBGC4gsgFxpN1vk4sw/mc5Ccsn6BoK5NytqMKv34veQoRXYyrIU8XEf0yyCee+duCeymWPgewvuxts+zhaPLF5hixzOuA3Pfp2PxfA8n5ENhQazEAAAAASUVORK5CYII=";
5183
+ const userAvatar = user.avatar;
4997
5184
 
4998
5185
  return (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
4999
5186
  className: "relative",
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "@rspress-theme-anatole/theme-default",
3
- "author": "Anatole Tong",
4
- "version": "0.7.22-alpha4",
5
- "license": "MIT",
6
- "sideEffects": [
7
- "*.css",
8
- "*.less",
9
- "*.sass",
10
- "*.scss",
11
- "**/virtual-global-styles.js",
12
- "virtual-global-styles",
13
- "./src/styles/index.ts",
14
- "./dist/index.js"
15
- ],
16
- "type": "module",
17
- "imports": {
18
- "#theme/*": "./src/*"
19
- },
20
- "main": "./dist/index.js",
21
- "types": "./dist/bundle.d.ts",
22
- "dependencies": {
23
- "@mdx-js/react": "2.3.0",
24
- "@rspress-theme-anatole/rspress-plugin-mermaid": "0.7.22-alpha4",
25
- "@rspress-theme-anatole/shared": "0.7.22-alpha4",
26
- "@rspress/runtime": "1.43.8",
27
- "body-scroll-lock": "4.0.0-beta.0",
28
- "copy-to-clipboard": "^3.3.3",
29
- "flexsearch": "0.7.43",
30
- "github-slugger": "^2.0.0",
31
- "html2pdf.js": "^0.10.3",
32
- "htmr": "^1.0.2",
33
- "lodash-es": "^4.17.21",
34
- "nprogress": "^0.2.0",
35
- "react": "^18.3.1",
36
- "react-dom": "^18.3.1",
37
- "react-helmet-async": "^1.3.0",
38
- "react-router-dom": "6.29.0",
39
- "react-syntax-highlighter": "^15.6.1"
40
- },
41
- "devDependencies": {
42
- "@microsoft/api-extractor": "^7.49.2",
43
- "@modern-js/tsconfig": "2.64.0",
44
- "@rsbuild/plugin-react": "~1.1.0",
45
- "@rsbuild/plugin-sass": "~1.2.0",
46
- "@rsbuild/plugin-svgr": "^1.0.6",
47
- "@rslib/core": "0.5.2",
48
- "@types/body-scroll-lock": "^3.1.2",
49
- "@types/hast": "^2.3.10",
50
- "@types/jest": "~29.5.14",
51
- "@types/lodash-es": "^4.17.12",
52
- "@types/mdast": "^3.0.15",
53
- "@types/nprogress": "^0.2.3",
54
- "@types/react": "^18.3.18",
55
- "@types/react-dom": "^18.3.5",
56
- "@types/react-syntax-highlighter": "^15.5.13",
57
- "gray-matter": "4.0.3",
58
- "tailwindcss": "^3.4.17",
59
- "typescript": "^5.5.3"
60
- },
61
- "keywords": [
62
- "rspress",
63
- "theme",
64
- "rspress-theme"
65
- ],
66
- "publishConfig": {
67
- "access": "public"
68
- }
69
- }
1
+ {
2
+ "name": "@rspress-theme-anatole/theme-default",
3
+ "author": "Anatole Tong",
4
+ "version": "0.7.23",
5
+ "license": "MIT",
6
+ "sideEffects": [
7
+ "*.css",
8
+ "*.less",
9
+ "*.sass",
10
+ "*.scss",
11
+ "**/virtual-global-styles.js",
12
+ "virtual-global-styles",
13
+ "./src/styles/index.ts",
14
+ "./dist/index.js"
15
+ ],
16
+ "type": "module",
17
+ "imports": {
18
+ "#theme/*": "./src/*"
19
+ },
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/bundle.d.ts",
22
+ "dependencies": {
23
+ "@mdx-js/react": "2.3.0",
24
+ "@rspress-theme-anatole/rspress-plugin-mermaid": "0.7.23",
25
+ "@rspress-theme-anatole/shared": "0.7.23",
26
+ "@rspress/runtime": "1.43.8",
27
+ "body-scroll-lock": "4.0.0-beta.0",
28
+ "copy-to-clipboard": "^3.3.3",
29
+ "flexsearch": "0.7.43",
30
+ "github-slugger": "^2.0.0",
31
+ "html2pdf.js": "^0.10.3",
32
+ "htmr": "^1.0.2",
33
+ "lodash-es": "^4.17.21",
34
+ "nprogress": "^0.2.0",
35
+ "react": "^18.3.1",
36
+ "react-dom": "^18.3.1",
37
+ "react-helmet-async": "^1.3.0",
38
+ "react-router-dom": "6.29.0",
39
+ "react-syntax-highlighter": "^15.6.1"
40
+ },
41
+ "devDependencies": {
42
+ "@microsoft/api-extractor": "^7.49.2",
43
+ "@modern-js/tsconfig": "2.64.0",
44
+ "@rsbuild/plugin-react": "~1.1.0",
45
+ "@rsbuild/plugin-sass": "~1.2.0",
46
+ "@rsbuild/plugin-svgr": "^1.0.6",
47
+ "@rslib/core": "0.5.2",
48
+ "@types/body-scroll-lock": "^3.1.2",
49
+ "@types/hast": "^2.3.10",
50
+ "@types/jest": "~29.5.14",
51
+ "@types/lodash-es": "^4.17.12",
52
+ "@types/mdast": "^3.0.15",
53
+ "@types/nprogress": "^0.2.3",
54
+ "@types/react": "^18.3.18",
55
+ "@types/react-dom": "^18.3.5",
56
+ "@types/react-syntax-highlighter": "^15.5.13",
57
+ "gray-matter": "4.0.3",
58
+ "tailwindcss": "^3.4.17",
59
+ "typescript": "^5.5.3"
60
+ },
61
+ "keywords": [
62
+ "rspress",
63
+ "theme",
64
+ "rspress-theme"
65
+ ],
66
+ "publishConfig": {
67
+ "access": "public"
68
+ }
69
+ }