@rspress-theme-anatole/theme-default 0.7.22 → 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 +447 -176
  2. package/package.json +4 -4
package/dist/bundle.js CHANGED
@@ -334,21 +334,223 @@ function useLocaleSiteData() {
334
334
  langRoutePrefix: lang === defaultLang ? '/' : (0, __WEBPACK_EXTERNAL_MODULE__rspress_shared_baa012d0__.addTrailingSlash)(lang)
335
335
  };
336
336
  }
337
+ function normalizeRoleList(value) {
338
+ if (!value) return [];
339
+ const list = Array.isArray(value) ? value : String(value).split(',');
340
+ return list.map((role) => String(role).trim().toLowerCase()).filter(Boolean);
341
+ }
342
+ // Cache for user roles fetched from userContext endpoint
343
+ let _cachedUserRoles = null;
344
+ let _userRolesFetchPromise = null;
345
+
346
+ function getUserRoles() {
347
+ // Return cached roles if available
348
+ if (_cachedUserRoles !== null) return _cachedUserRoles;
349
+ return [];
350
+ }
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;
383
+ }
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) {
394
+ if (!Array.isArray(items)) return items;
395
+ return items.map((item) => {
396
+ if (!item) return null;
397
+ let nextItem = item;
398
+ const locked = isItemLocked(item, userRoles);
399
+ if (locked) {
400
+ nextItem = { ...nextItem, locked: true };
401
+ }
402
+ if ('items' in item && Array.isArray(item.items)) {
403
+ const markedItems = markLockedSidebarItems(item.items, userRoles);
404
+ nextItem = { ...nextItem, items: markedItems };
405
+ }
406
+ return nextItem;
407
+ }).filter(Boolean);
408
+ }
337
409
  function useSidebarData() {
338
410
  const { sidebar } = useLocaleSiteData();
411
+ const { siteData } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.usePageData)();
339
412
  const { pathname: rawPathname } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.useLocation)();
340
413
  const pathname = decodeURIComponent(rawPathname);
341
- const sidebarData = (0, __WEBPACK_EXTERNAL_MODULE_react__.useMemo)(() => getSidebarDataGroup(sidebar, pathname), [
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
+
424
+ (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
425
+ if ('undefined' == typeof window) return;
426
+ const handler = () => setRoleVersion((v) => v + 1);
427
+ window.addEventListener('MetaRolesReady', handler);
428
+ window.addEventListener('UserRolesReady', handler);
429
+ return () => {
430
+ window.removeEventListener('MetaRolesReady', handler);
431
+ window.removeEventListener('UserRolesReady', handler);
432
+ };
433
+ }, []);
434
+ const sidebarData = (0, __WEBPACK_EXTERNAL_MODULE_react__.useMemo)(() => {
435
+ const data = getSidebarDataGroup(sidebar, pathname);
436
+ const userRoles = getUserRoles();
437
+ let dataWithRoles = data;
438
+ try {
439
+ const roleMap = 'undefined' != typeof window ? window.__META_ROLE_MAP__ : null;
440
+ if (roleMap) {
441
+ const applyRoles = (items) => {
442
+ if (!Array.isArray(items)) return items;
443
+ return items.map((item) => {
444
+ if (!item) return item;
445
+ let nextItem = item;
446
+ const key = item._fileKey;
447
+ if (key && roleMap[key]) {
448
+ nextItem = {
449
+ ...nextItem,
450
+ roles: roleMap[key]
451
+ };
452
+ }
453
+ if ('items' in nextItem && Array.isArray(nextItem.items)) {
454
+ const nextItems = applyRoles(nextItem.items);
455
+ if (nextItems !== nextItem.items) {
456
+ nextItem = {
457
+ ...nextItem,
458
+ items: nextItems
459
+ };
460
+ }
461
+ }
462
+ return nextItem;
463
+ });
464
+ };
465
+ dataWithRoles = applyRoles(data);
466
+ }
467
+ } catch (e) {
468
+ }
469
+ return markLockedSidebarItems(dataWithRoles, userRoles);
470
+ }, [
342
471
  sidebar,
343
- pathname
472
+ pathname,
473
+ roleVersion
344
474
  ]);
345
475
  return sidebarData;
346
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
+
347
546
  function usePrevNextPage() {
348
547
  const { pathname } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.useLocation)();
349
548
  const items = useSidebarData();
350
549
  const flattenTitles = [];
351
550
  const walk = (sidebarItem) => {
551
+ // Skip locked items
552
+ if (sidebarItem.locked === true) return;
553
+
352
554
  if ('items' in sidebarItem) {
353
555
  if (sidebarItem.link) flattenTitles.push({
354
556
  text: sidebarItem.text,
@@ -553,9 +755,29 @@ function SidebarGroup(props) {
553
755
  const initialState = (0, __WEBPACK_EXTERNAL_MODULE_react__.useRef)('collapsed' in item && item.collapsed);
554
756
  const active = item.link && activeMatcher(item.link);
555
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
+
556
778
  const collapsibleIcon = (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
557
779
  style: {
558
- cursor: 'pointer',
780
+ cursor: isLocked ? 'not-allowed' : 'pointer',
559
781
  transition: 'transform 0.2s ease-out',
560
782
  transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)'
561
783
  },
@@ -593,6 +815,7 @@ function SidebarGroup(props) {
593
815
  }, []);
594
816
  const toggleCollapse = (e) => {
595
817
  e.stopPropagation();
818
+ if (isLocked) return; // Prevent collapse toggle for locked items
596
819
  setSidebarData((sidebarData) => {
597
820
  const newSidebarData = [
598
821
  ...sidebarData
@@ -610,23 +833,29 @@ function SidebarGroup(props) {
610
833
  className: "rspress-sidebar-section mt-0.5 block",
611
834
  "data-context": item.context,
612
835
  style: {
613
- marginLeft: 0 === depth ? 0 : '18px'
836
+ marginLeft: 0 === depth ? 0 : '18px',
837
+ opacity: isLocked ? 0.6 : 1
614
838
  },
615
839
  children: [
616
840
  (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsxs)("div", {
617
841
  className: `rspress-sidebar-collapse flex items-center ${active ? Sidebar_index_module.menuItemActive : Sidebar_index_module.menuItem}`,
618
842
  "data-context": item.context,
619
- onMouseEnter: () => item.link && preloadLink(item.link),
843
+ onMouseEnter: () => !isLocked && item.link && preloadLink(item.link),
620
844
  onClick: (e) => {
845
+ if (isLocked) {
846
+ e.preventDefault();
847
+ e.stopPropagation();
848
+ return;
849
+ }
621
850
  if (item.link) navigate((0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.withBase)((0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.normalizeHrefInRuntime)(item.link)));
622
851
  collapsible && toggleCollapse(e);
623
852
  },
624
853
  style: {
625
854
  // borderRadius: 0 === depth ? '0 var(--rp-radius) var(--rp-radius) 0' : void 0,
626
- cursor: collapsible || item.link ? 'pointer' : 'normal'
855
+ cursor: isLocked ? 'not-allowed' : (collapsible || item.link ? 'pointer' : 'normal')
627
856
  },
628
857
  children: [
629
- collapsible && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
858
+ collapsible && !isLocked && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
630
859
  className: `${Sidebar_index_module.collapseContainer} p-2 rounded-xl`,
631
860
  style: {
632
861
  paddingRight: 0
@@ -635,7 +864,7 @@ function SidebarGroup(props) {
635
864
  children: collapsibleIcon
636
865
  }),
637
866
  (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsxs)("h2", {
638
- className: "py-2 px-2 text-sm font-medium flex",
867
+ className: "py-2 px-2 text-sm font-medium flex items-center",
639
868
  style: {
640
869
  ...highlightTitleStyle
641
870
  },
@@ -651,12 +880,13 @@ function SidebarGroup(props) {
651
880
  letterSpacing: '-1%'
652
881
  },
653
882
  children: renderInlineMarkdown(item.text)
654
- })
883
+ }),
884
+ isLocked && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)(GroupLockIcon, {})
655
885
  ]
656
886
  })
657
887
  ]
658
888
  }),
659
- (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
889
+ !isLocked && (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
660
890
  ref: containerRef,
661
891
  className: "transition-all duration-300 ease-in-out",
662
892
  style: {
@@ -692,6 +922,7 @@ function SidebarItem(props) {
692
922
  const { item, depth = 0, activeMatcher, id, setSidebarData } = props;
693
923
  const active = 'link' in item && item.link && activeMatcher(item.link);
694
924
  const ref = (0, __WEBPACK_EXTERNAL_MODULE_react__.useRef)(null);
925
+ const isLocked = item.locked === true;
695
926
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
696
927
  if (active) ref.current?.scrollIntoView({
697
928
  block: 'center'
@@ -705,6 +936,65 @@ function SidebarItem(props) {
705
936
  collapsed: item.collapsed,
706
937
  setSidebarData: setSidebarData
707
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
+
708
998
  return (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)(__WEBPACK_EXTERNAL_MODULE__theme_75e53063__.Link, {
709
999
  href: (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.normalizeHrefInRuntime)(item.link),
710
1000
  className: Sidebar_index_module.menuLink,
@@ -1659,6 +1949,7 @@ const Layout = (props) => {
1659
1949
  const { pageType, lang: currentLang, title: articleTitle, frontmatter = {} } = page;
1660
1950
  const localesData = useLocaleSiteData();
1661
1951
  useRedirect4FirstVisit();
1952
+ useRedirectToAccessiblePage();
1662
1953
  let title = frontmatter.title ?? articleTitle;
1663
1954
  const mainTitle = siteData.title || localesData.title || '';
1664
1955
  title = title &&
@@ -4581,11 +4872,6 @@ function NavVersions() {
4581
4872
  });
4582
4873
  }
4583
4874
  // Utility functions for cookie management
4584
- function setCookie(name, value, days = 7) {
4585
- const expires = new Date();
4586
- expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
4587
- document.cookie = `${name}=${encodeURIComponent(JSON.stringify(value))};expires=${expires.toUTCString()};path=/`;
4588
- }
4589
4875
  function getCookie(name) {
4590
4876
  const nameEQ = name + "=";
4591
4877
  const ca = document.cookie.split(';');
@@ -4605,9 +4891,6 @@ function getCookie(name) {
4605
4891
  }
4606
4892
  return null;
4607
4893
  }
4608
- function deleteCookie(name) {
4609
- document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/`;
4610
- }
4611
4894
  // Decode Base64 string (supports Unicode)
4612
4895
  function decodeBase64(base64String) {
4613
4896
  try {
@@ -4636,6 +4919,87 @@ function decodeBase64(base64String) {
4636
4919
  return null;
4637
4920
  }
4638
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
+ }
4639
5003
  // Parse KBGateway-User-Context cookie (Base64 encoded)
4640
5004
  function parseKBGatewayUserContext(cookieValue) {
4641
5005
  if (!cookieValue) return null;
@@ -4661,7 +5025,7 @@ function parseKBGatewayUserContext(cookieValue) {
4661
5025
  return {
4662
5026
  username: username,
4663
5027
  email: parsed.email || null,
4664
- 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)
4665
5029
  };
4666
5030
  } catch (e) {
4667
5031
  console.warn('Failed to parse KBGateway-User-Context:', e);
@@ -4678,7 +5042,7 @@ function parseKBGatewayUserContext(cookieValue) {
4678
5042
  return {
4679
5043
  username: username,
4680
5044
  email: parsed.email || null,
4681
- avatar: parsed.Avatar || parsed.avatar || null
5045
+ avatar: generateAvatarBase64(username)
4682
5046
  };
4683
5047
  } catch (e2) {
4684
5048
  console.warn('Failed to parse as JSON:', e2);
@@ -4688,54 +5052,12 @@ function parseKBGatewayUserContext(cookieValue) {
4688
5052
  }
4689
5053
  // LocalStorage key for user data
4690
5054
  const USER_STORAGE_KEY = 'kb_user_data';
4691
- // Get user data from localStorage
4692
- function getUserFromStorage() {
4693
- if (typeof localStorage === 'undefined') return null;
4694
-
4695
- try {
4696
- const storedData = localStorage.getItem(USER_STORAGE_KEY);
4697
- if (storedData) {
4698
- return JSON.parse(storedData);
4699
- }
4700
- } catch (e) {
4701
- console.warn('Failed to get user from localStorage:', e);
4702
- localStorage.removeItem(USER_STORAGE_KEY);
4703
- }
4704
- return null;
4705
- }
4706
- // Save user data to localStorage
4707
- function saveUserToStorage(userData) {
4708
- if (typeof localStorage === 'undefined') return;
4709
-
4710
- try {
4711
- if (userData) {
4712
- localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userData));
4713
- } else {
4714
- localStorage.removeItem(USER_STORAGE_KEY);
4715
- }
4716
- } catch (e) {
4717
- console.warn('Failed to save user to localStorage:', e);
4718
- }
4719
- }
4720
- // Remove user data from localStorage
4721
- function removeUserFromStorage() {
4722
- if (typeof localStorage === 'undefined') return;
4723
- localStorage.removeItem(USER_STORAGE_KEY);
4724
- }
4725
- // Sync cookie to localStorage and return user data
4726
- function syncCookieToStorage(cookieName) {
4727
- const cookieValue = getCookie(cookieName);
4728
- const userData = parseKBGatewayUserContext(cookieValue);
4729
-
4730
- if (userData) {
4731
- saveUserToStorage(userData);
4732
- } else {
4733
- removeUserFromStorage();
4734
- }
4735
-
4736
- return userData;
4737
- }
5055
+
4738
5056
  // User Authentication Component
5057
+ // Cache user data globally to prevent flicker between page navigations
5058
+ let cachedUserData = null;
5059
+ let isClientInitialized = false;
5060
+
4739
5061
  function UserAuth() {
4740
5062
  const { siteData } = (0, __WEBPACK_EXTERNAL_MODULE__rspress_runtime_0abd3046__.usePageData)();
4741
5063
  const dropdownRef = (0, __WEBPACK_EXTERNAL_MODULE_react__.useRef)(null);
@@ -4743,52 +5065,54 @@ function UserAuth() {
4743
5065
  const authEnabled = siteData?.auth?.enabled !== false;
4744
5066
  const showSignInButton = siteData?.auth?.showSignInButton !== false;
4745
5067
 
4746
- // Get sign in URL from site config
5068
+ // Get sign in /sign out URL from site config
4747
5069
  const signInUrl = siteData?.auth?.endpoints?.signIn;
5070
+ const signOutUrl = siteData?.auth?.endpoints?.signOut;
4748
5071
 
4749
5072
  // Cookie name for KBGateway
4750
5073
  const KB_COOKIE_NAME = 'KBGateway-User-Context';
4751
5074
 
4752
- const [user, setUser] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(() => {
5075
+ // Helper function to get user from cookie directly
5076
+ const getUserFromCookie = () => {
4753
5077
  if (typeof document === 'undefined') return null;
4754
-
4755
- // 1. First try to get from localStorage (instant, no flicker)
4756
- const storedUser = getUserFromStorage();
4757
- if (storedUser) {
4758
- 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;
4759
5094
  }
4760
-
4761
- // 2. If not in localStorage, check cookie and sync
4762
- return syncCookieToStorage(KB_COOKIE_NAME);
5095
+ return null;
4763
5096
  });
4764
5097
 
5098
+ // Track if component has mounted (for SSR hydration)
5099
+ const [isMounted, setIsMounted] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(isClientInitialized);
5100
+
4765
5101
  const [showDropdown, setShowDropdown] = (0, __WEBPACK_EXTERNAL_MODULE_react__.useState)(false);
4766
5102
 
4767
- // Sync cookie to localStorage on mount and periodically check for changes
5103
+ // Check cookie on mount and update cache
4768
5104
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
4769
- // Sync cookie to localStorage on mount
4770
- const userData = syncCookieToStorage(KB_COOKIE_NAME);
5105
+ const userData = getUserFromCookie();
4771
5106
  if (userData) {
5107
+ cachedUserData = userData;
4772
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);
4773
5113
  }
4774
-
4775
- // Listen for storage events to sync across tabs
4776
- const handleStorageChange = (e) => {
4777
- if (e.key === USER_STORAGE_KEY) {
4778
- if (e.newValue) {
4779
- try {
4780
- setUser(JSON.parse(e.newValue));
4781
- } catch (err) {
4782
- setUser(null);
4783
- }
4784
- } else {
4785
- setUser(null);
4786
- }
4787
- }
4788
- };
4789
-
4790
- window.addEventListener('storage', handleStorageChange);
4791
- return () => window.removeEventListener('storage', handleStorageChange);
5114
+ isClientInitialized = true;
5115
+ setIsMounted(true);
4792
5116
  }, []);
4793
5117
 
4794
5118
  (0, __WEBPACK_EXTERNAL_MODULE_react__.useEffect)(() => {
@@ -4803,96 +5127,43 @@ function UserAuth() {
4803
5127
  return () => document.removeEventListener('mousedown', handleClickOutside);
4804
5128
  }, []);
4805
5129
 
4806
- const handleSignIn = async () => {
5130
+ const handleSignIn = () => {
4807
5131
  if (!signInUrl) {
4808
5132
  console.warn('Sign in URL not configured');
4809
5133
  return;
4810
5134
  }
4811
5135
 
4812
- try {
4813
- // Center the popup window
4814
- const width = 600;
4815
- const height = 600;
4816
- const left = window.top.outerWidth / 2 + window.top.screenX - width / 2;
4817
- const top = window.top.outerHeight / 2 + window.top.screenY - height / 2;
4818
-
4819
- // Open sign in URL in new window/tab
4820
- const authWindow = window.open(signInUrl, '_blank', `width=${width},height=${height},left=${left},top=${top}`);
4821
-
4822
- // Listen for messages from auth window
4823
- const handleMessage = (event) => {
4824
- if (event.origin !== new URL(signInUrl).origin) return;
4825
-
4826
- if (event.data && event.data.type === 'AUTH_SUCCESS') {
4827
- const userData = {
4828
- username: event.data.username,
4829
- email: event.data.email,
4830
- avatar: event.data.avatar
4831
- };
4832
-
4833
- // Save to localStorage
4834
- saveUserToStorage(userData);
4835
- setUser(userData);
4836
- authWindow.close();
4837
-
4838
- // Remove event listener
4839
- window.removeEventListener('message', handleMessage);
4840
- }
4841
- };
4842
-
4843
- window.addEventListener('message', handleMessage);
4844
-
4845
- // Poll for KBGateway-User-Context cookie
4846
- const pollInterval = setInterval(() => {
4847
- // Sync cookie to localStorage and get user data
4848
- const userData = syncCookieToStorage(KB_COOKIE_NAME);
4849
-
4850
- if (userData) {
4851
- setUser(userData);
4852
- clearInterval(pollInterval);
4853
- window.removeEventListener('message', handleMessage);
4854
-
4855
- // Close popup if still open
4856
- if (authWindow && !authWindow.closed) {
4857
- authWindow.close();
4858
- }
4859
- }
4860
-
4861
- // Also check if popup was closed manually
4862
- if (authWindow && authWindow.closed) {
4863
- clearInterval(pollInterval);
4864
- window.removeEventListener('message', handleMessage);
4865
-
4866
- // Final sync cookie to localStorage
4867
- const finalUserData = syncCookieToStorage(KB_COOKIE_NAME);
4868
- if (finalUserData) {
4869
- setUser(finalUserData);
4870
- }
4871
- }
4872
- }, 500); // Check every 500ms
4873
-
4874
- // Timeout after 5 minutes
4875
- setTimeout(() => {
4876
- clearInterval(pollInterval);
4877
- window.removeEventListener('message', handleMessage);
4878
- }, 300000);
4879
-
4880
-
4881
- } catch (error) {
4882
- console.error('Sign in error:', error);
4883
- }
5136
+ // Redirect directly to sign in URL
5137
+ window.location.href = signInUrl;
4884
5138
  };
4885
5139
 
4886
5140
  const handleSignOut = () => {
4887
- deleteCookie(KB_COOKIE_NAME);
4888
- removeUserFromStorage();
4889
- setUser(null);
4890
- 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
+ });
4891
5154
  };
4892
5155
 
4893
5156
  if (!authEnabled) {
4894
5157
  return null;
4895
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
+ }
4896
5167
 
4897
5168
  if (!user) {
4898
5169
  if (!showSignInButton) {
@@ -4909,7 +5180,7 @@ function UserAuth() {
4909
5180
  });
4910
5181
  }
4911
5182
 
4912
- 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;
4913
5184
 
4914
5185
  return (0, __WEBPACK_EXTERNAL_MODULE_react_jsx_runtime_225474f2__.jsx)("div", {
4915
5186
  className: "relative",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
- {
1
+ {
2
2
  "name": "@rspress-theme-anatole/theme-default",
3
3
  "author": "Anatole Tong",
4
- "version": "0.7.22",
4
+ "version": "0.7.23",
5
5
  "license": "MIT",
6
6
  "sideEffects": [
7
7
  "*.css",
@@ -21,8 +21,8 @@
21
21
  "types": "./dist/bundle.d.ts",
22
22
  "dependencies": {
23
23
  "@mdx-js/react": "2.3.0",
24
- "@rspress-theme-anatole/rspress-plugin-mermaid": "0.7.22",
25
- "@rspress-theme-anatole/shared": "0.7.22",
24
+ "@rspress-theme-anatole/rspress-plugin-mermaid": "0.7.23",
25
+ "@rspress-theme-anatole/shared": "0.7.23",
26
26
  "@rspress/runtime": "1.43.8",
27
27
  "body-scroll-lock": "4.0.0-beta.0",
28
28
  "copy-to-clipboard": "^3.3.3",