@redocly/theme 0.61.0-next.1 → 0.61.0-next.2

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.
@@ -1,6 +1,7 @@
1
1
  import type { JSX } from 'react';
2
+ import type { ColorMode } from '../../core/types';
2
3
  export type ColorModeIconProps = {
3
- mode?: 'dark' | 'light' | string;
4
+ mode?: ColorMode | string;
4
5
  className?: string;
5
6
  };
6
7
  export declare function ColorModeIcon(props: ColorModeIconProps): JSX.Element;
@@ -9,14 +9,15 @@ const styled_components_1 = __importDefault(require("styled-components"));
9
9
  const ContrastIcon_1 = require("../../icons/ContrastIcon/ContrastIcon");
10
10
  const MoonIcon_1 = require("../../icons/MoonIcon/MoonIcon");
11
11
  const SunIcon_1 = require("../../icons/SunIcon/SunIcon");
12
+ const constants_1 = require("../../core/constants");
12
13
  function ColorModeIcon(props) {
13
14
  return (react_1.default.createElement(ColorModeIconComponent, Object.assign({}, props, { "data-component-name": "ColorModeSwitcher/ColorModeIcon" })));
14
15
  }
15
16
  function Icon({ mode, className }) {
16
17
  switch (mode) {
17
- case 'dark':
18
+ case constants_1.DEFAULT_COLOR_MODES.DARK:
18
19
  return react_1.default.createElement(SunIcon_1.SunIcon, { "data-testid": "dark" });
19
- case 'light':
20
+ case constants_1.DEFAULT_COLOR_MODES.LIGHT:
20
21
  return react_1.default.createElement(MoonIcon_1.MoonIcon, { "data-testid": "light" });
21
22
  default:
22
23
  return react_1.default.createElement(ContrastIcon_1.ContrastIcon, { "data-testid": "custom", className: className + (mode ? ' ' + mode : '') });
@@ -9,10 +9,7 @@ const hooks_1 = require("../../core/hooks");
9
9
  const ColorModeIcon_1 = require("../../components/ColorModeSwitcher/ColorModeIcon");
10
10
  const Button_1 = require("../../components/Button/Button");
11
11
  function ColorModeSwitcher({ className }) {
12
- const { isSwitcherHidden, initActiveColorMode, switchColorMode, activeColorMode } = (0, hooks_1.useColorSwitcher)();
13
- (0, hooks_1.useMount)(() => {
14
- initActiveColorMode();
15
- });
12
+ const { isSwitcherHidden, switchColorMode, activeColorMode } = (0, hooks_1.useColorSwitcher)();
16
13
  if (isSwitcherHidden) {
17
14
  return null;
18
15
  }
@@ -0,0 +1 @@
1
+ export declare function getProductClassName(productName: string): string;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getProductClassName = getProductClassName;
4
+ function getProductClassName(productName) {
5
+ return `product-${productName
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, '-')
8
+ .replace(/^-+|-+$/g, '')}`;
9
+ }
10
+ //# sourceMappingURL=utils.js.map
@@ -26,3 +26,7 @@ export declare enum MobileMenuType {
26
26
  PRODUCT = "PRODUCT",
27
27
  PAGE = "PAGE"
28
28
  }
29
+ export declare const DEFAULT_COLOR_MODES: {
30
+ readonly LIGHT: "light";
31
+ readonly DARK: "dark";
32
+ };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MobileMenuType = exports.MenuItemType = exports.DEFAULT_FEEDBACK_TYPE = exports.FEEDBACK_TYPES = exports.ICONS_CDN_URL = exports.CONTENT_ID = exports.DEFAULT_LOCALE_PLACEHOLDER = exports.REDOCLY_TEAMS_RBAC = void 0;
3
+ exports.DEFAULT_COLOR_MODES = exports.MobileMenuType = exports.MenuItemType = exports.DEFAULT_FEEDBACK_TYPE = exports.FEEDBACK_TYPES = exports.ICONS_CDN_URL = exports.CONTENT_ID = exports.DEFAULT_LOCALE_PLACEHOLDER = exports.REDOCLY_TEAMS_RBAC = void 0;
4
4
  var config_1 = require("@redocly/config");
5
5
  Object.defineProperty(exports, "REDOCLY_TEAMS_RBAC", { enumerable: true, get: function () { return config_1.REDOCLY_TEAMS_RBAC; } });
6
6
  exports.DEFAULT_LOCALE_PLACEHOLDER = 'default_locale';
@@ -33,4 +33,8 @@ var MobileMenuType;
33
33
  MobileMenuType["PRODUCT"] = "PRODUCT";
34
34
  MobileMenuType["PAGE"] = "PAGE";
35
35
  })(MobileMenuType || (exports.MobileMenuType = MobileMenuType = {}));
36
+ exports.DEFAULT_COLOR_MODES = {
37
+ LIGHT: 'light',
38
+ DARK: 'dark',
39
+ };
36
40
  //# sourceMappingURL=common.js.map
@@ -48,3 +48,4 @@ export * from './use-connect-mcp-button';
48
48
  export * from './catalog/use-catalog-entity-details';
49
49
  export * from './catalog/use-catalog-entity-schema';
50
50
  export * from './catalog/use-catalog-table-header-cell-actions';
51
+ export * from './use-store';
@@ -64,4 +64,5 @@ __exportStar(require("./use-connect-mcp-button"), exports);
64
64
  __exportStar(require("./catalog/use-catalog-entity-details"), exports);
65
65
  __exportStar(require("./catalog/use-catalog-entity-schema"), exports);
66
66
  __exportStar(require("./catalog/use-catalog-table-header-cell-actions"), exports);
67
+ __exportStar(require("./use-store"), exports);
67
68
  //# sourceMappingURL=index.js.map
@@ -2,31 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useRecentSearches = void 0;
4
4
  const react_1 = require("react");
5
+ const use_store_1 = require("../use-store");
5
6
  const js_utils_1 = require("../../utils/js-utils");
6
7
  const RECENT_SEARCHES_KEY = 'recentSearches';
7
8
  const RECENT_SEARCHES_LIMIT = 5;
8
- const createRecentSearchesStore = () => {
9
- const subscribers = new Set();
10
- let cachedSnapshot;
11
- const getSnapshot = () => {
12
- if (!(0, js_utils_1.isBrowser)())
13
- return [];
14
- if (cachedSnapshot)
15
- return cachedSnapshot;
16
- try {
17
- const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
18
- cachedSnapshot = stored ? JSON.parse(stored) : [];
19
- return cachedSnapshot;
20
- }
21
- catch (e) {
22
- cachedSnapshot = [];
23
- return cachedSnapshot;
24
- }
25
- };
26
- const updateItems = (value, isAdd) => {
9
+ const recentSearchesStore = (0, use_store_1.createStore)({
10
+ storageKey: RECENT_SEARCHES_KEY,
11
+ });
12
+ const useRecentSearches = () => {
13
+ const [items, setItems] = (0, use_store_1.useStore)(recentSearchesStore, []);
14
+ const updateItems = (0, react_1.useCallback)((value, isAdd) => {
27
15
  if (!(0, js_utils_1.isBrowser)())
28
16
  return;
29
- const currentItems = getSnapshot();
17
+ const currentItems = [...items];
30
18
  const valueIndex = currentItems.indexOf(value);
31
19
  if (valueIndex !== -1) {
32
20
  currentItems.splice(valueIndex, 1);
@@ -35,29 +23,14 @@ const createRecentSearchesStore = () => {
35
23
  currentItems.unshift(value);
36
24
  }
37
25
  const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
38
- localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limitedItems));
39
- cachedSnapshot = limitedItems;
40
- subscribers.forEach((callback) => callback());
41
- };
42
- const subscribe = (callback) => {
43
- subscribers.add(callback);
44
- return () => subscribers.delete(callback);
45
- };
46
- return {
47
- getSnapshot,
48
- subscribe,
49
- updateItems,
50
- };
51
- };
52
- const recentSearchesStore = createRecentSearchesStore();
53
- const useRecentSearches = () => {
54
- const items = (0, react_1.useSyncExternalStore)(recentSearchesStore.subscribe, recentSearchesStore.getSnapshot, () => []);
26
+ setItems(limitedItems);
27
+ }, [items, setItems]);
55
28
  const addSearchHistoryItem = (0, react_1.useCallback)((value) => {
56
- recentSearchesStore.updateItems(value, true);
57
- }, []);
29
+ updateItems(value, true);
30
+ }, [updateItems]);
58
31
  const removeSearchHistoryItem = (0, react_1.useCallback)((value) => {
59
- recentSearchesStore.updateItems(value, false);
60
- }, []);
32
+ updateItems(value, false);
33
+ }, [updateItems]);
61
34
  return { items, addSearchHistoryItem, removeSearchHistoryItem };
62
35
  };
63
36
  exports.useRecentSearches = useRecentSearches;
@@ -1,6 +1,5 @@
1
1
  export declare const useColorSwitcher: () => {
2
2
  isSwitcherHidden: boolean | undefined;
3
- initActiveColorMode: () => void;
4
3
  switchColorMode: (mode?: string) => void;
5
4
  activeColorMode: string;
6
5
  };
@@ -4,36 +4,43 @@ exports.useColorSwitcher = void 0;
4
4
  const react_1 = require("react");
5
5
  const use_theme_config_1 = require("./use-theme-config");
6
6
  const use_theme_hooks_1 = require("./use-theme-hooks");
7
+ const use_store_1 = require("./use-store");
8
+ const constants_1 = require("../constants");
9
+ const js_utils_1 = require("../utils/js-utils");
10
+ const COLOR_MODE_KEY = 'colorSchema';
11
+ const colorModeStore = (0, use_store_1.createStore)({
12
+ storageKey: COLOR_MODE_KEY,
13
+ });
7
14
  const useColorSwitcher = () => {
15
+ var _a;
8
16
  const themeSettings = (0, use_theme_config_1.useThemeConfig)();
9
17
  const { useTelemetry } = (0, use_theme_hooks_1.useThemeHooks)();
10
18
  const telemetry = useTelemetry();
11
- const colorMode = themeSettings.colorMode;
12
- const modes = (colorMode === null || colorMode === void 0 ? void 0 : colorMode.modes) || ['light', 'dark'];
13
- const defaultColor = modes[0] || 'light';
14
- const [activeColorMode, setActiveColorMode] = (0, react_1.useState)(defaultColor);
15
- const initActiveColorMode = () => {
16
- const activeMode = Array.from(document.documentElement.classList).find((c) => modes.includes(c));
17
- setActiveColorMode(activeMode || defaultColor);
18
- };
19
+ const themeColorMode = themeSettings.colorMode;
20
+ const modes = (0, react_1.useMemo)(() => (themeColorMode === null || themeColorMode === void 0 ? void 0 : themeColorMode.modes) || [constants_1.DEFAULT_COLOR_MODES.LIGHT, constants_1.DEFAULT_COLOR_MODES.DARK], [themeColorMode]);
21
+ const documentMode = (0, react_1.useMemo)(() => {
22
+ if (!(0, js_utils_1.isBrowser)())
23
+ return;
24
+ return Array.from(document.documentElement.classList).find((c) => modes.includes(c));
25
+ }, [modes]);
26
+ const [activeColorMode, setActiveColorMode] = (0, use_store_1.useStore)(colorModeStore, (_a = documentMode !== null && documentMode !== void 0 ? documentMode : modes[0]) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_COLOR_MODES.LIGHT);
19
27
  const switchColorMode = (mode) => {
20
28
  if (mode && !modes.includes(mode)) {
21
29
  return;
22
30
  }
23
31
  const activeIndex = modes.indexOf(activeColorMode);
24
- // If specific mode is provided, use it, otherwise cycle through modes
25
32
  const newMode = mode || (activeIndex < modes.length - 1 ? modes[activeIndex + 1] : modes[0]);
26
- localStorage.setItem('colorSchema', newMode);
27
- document.documentElement.className = `${newMode} notransition`;
33
+ const root = document.documentElement;
34
+ modes.forEach((mode) => root.classList.remove(mode));
35
+ root.classList.add(newMode, 'notransition');
28
36
  window.requestAnimationFrame(() => {
29
- document.documentElement.classList.remove('notransition');
37
+ root.classList.remove('notransition');
30
38
  });
31
39
  telemetry.sendColorModeSwitchedMessage({ from: activeColorMode, to: newMode });
32
40
  setActiveColorMode(newMode);
33
41
  };
34
42
  return {
35
- isSwitcherHidden: colorMode === null || colorMode === void 0 ? void 0 : colorMode.hide,
36
- initActiveColorMode,
43
+ isSwitcherHidden: themeColorMode === null || themeColorMode === void 0 ? void 0 : themeColorMode.hide,
37
44
  switchColorMode,
38
45
  activeColorMode,
39
46
  };
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useProductPicker = useProductPicker;
4
4
  const react_router_dom_1 = require("react-router-dom");
5
+ const utils_1 = require("../../components/Product/utils");
5
6
  const use_theme_hooks_1 = require("./use-theme-hooks");
6
- const utils_1 = require("../utils");
7
+ const utils_2 = require("../utils");
7
8
  function useProductPicker() {
8
9
  const { useCurrentProduct, useProducts, useTelemetry, useLoadAndNavigate } = (0, use_theme_hooks_1.useThemeHooks)();
9
10
  const currentProduct = useCurrentProduct();
@@ -15,7 +16,16 @@ function useProductPicker() {
15
16
  if (!product)
16
17
  return;
17
18
  telemetry.sendProductPickedMessage({ product: product.slug });
18
- loadAndNavigate({ navigate, to: (0, utils_1.withPathPrefix)(product.link) });
19
+ if (typeof document === 'undefined')
20
+ return;
21
+ if (product.name) {
22
+ const root = document.documentElement;
23
+ Array.from(root.classList)
24
+ .filter((c) => c.startsWith('product-'))
25
+ .forEach((c) => root.classList.remove(c));
26
+ root.classList.add((0, utils_1.getProductClassName)(product.name));
27
+ }
28
+ loadAndNavigate({ navigate, to: (0, utils_2.withPathPrefix)(product.link) });
19
29
  }
20
30
  return {
21
31
  currentProduct,
@@ -0,0 +1,17 @@
1
+ type Store<T> = {
2
+ getValue: (defaultValue: T) => T;
3
+ setValue: (next: T) => void;
4
+ subscribe: (callback: () => void) => Unsubscribe;
5
+ };
6
+ type Unsubscribe = () => void;
7
+ type CreateStoreProps<T> = {
8
+ storageKey: string;
9
+ storageType?: 'localStorage' | 'sessionStorage';
10
+ serializer?: {
11
+ parse: (value: string) => T;
12
+ serialize: (value: T) => string;
13
+ };
14
+ };
15
+ export declare function createStore<T>({ storageKey, storageType, serializer, }: CreateStoreProps<T>): Store<T>;
16
+ export declare function useStore<T>(store: Store<T>, defaultValue: T): readonly [T, (next: T) => void];
17
+ export {};
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createStore = createStore;
4
+ exports.useStore = useStore;
5
+ const react_1 = require("react");
6
+ const js_utils_1 = require("../utils/js-utils");
7
+ function createStore({ storageKey, storageType = 'localStorage', serializer = {
8
+ parse: (value) => JSON.parse(value),
9
+ serialize: (value) => JSON.stringify(value),
10
+ }, }) {
11
+ const subscribers = new Set();
12
+ let cachedValue;
13
+ const shouldSerialize = (value) => typeof value !== 'string';
14
+ const getValue = (defaultValue) => {
15
+ if (!(0, js_utils_1.isBrowser)())
16
+ return defaultValue;
17
+ if (cachedValue !== undefined)
18
+ return cachedValue;
19
+ const value = window[storageType].getItem(storageKey);
20
+ if (!value) {
21
+ cachedValue = defaultValue;
22
+ return cachedValue;
23
+ }
24
+ if (!shouldSerialize(defaultValue)) {
25
+ cachedValue = value;
26
+ return cachedValue;
27
+ }
28
+ try {
29
+ cachedValue = JSON.parse(value);
30
+ }
31
+ catch (_a) {
32
+ cachedValue = defaultValue;
33
+ }
34
+ return cachedValue;
35
+ };
36
+ const setValue = (next) => {
37
+ if (!(0, js_utils_1.isBrowser)())
38
+ return;
39
+ try {
40
+ window[storageType].setItem(storageKey, shouldSerialize(next) ? serializer.serialize(next) : next);
41
+ cachedValue = next;
42
+ subscribers.forEach((callback) => callback());
43
+ }
44
+ catch (_a) {
45
+ return;
46
+ }
47
+ };
48
+ const subscribe = (callback) => {
49
+ subscribers.add(callback);
50
+ return () => {
51
+ subscribers.delete(callback);
52
+ };
53
+ };
54
+ return {
55
+ getValue,
56
+ setValue,
57
+ subscribe,
58
+ };
59
+ }
60
+ function useStore(store, defaultValue) {
61
+ const value = (0, react_1.useSyncExternalStore)(store.subscribe, () => store.getValue(defaultValue), () => defaultValue);
62
+ return [value, store.setValue];
63
+ }
64
+ //# sourceMappingURL=use-store.js.map
@@ -1 +1,3 @@
1
+ import { DEFAULT_COLOR_MODES } from '../constants';
1
2
  export type Plural<T extends string> = `${T}s`;
3
+ export type ColorMode = (typeof DEFAULT_COLOR_MODES)[keyof typeof DEFAULT_COLOR_MODES];
@@ -10,12 +10,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.loadAndNavigate = loadAndNavigate;
13
+ const get_navbar_element_1 = require("./get-navbar-element");
13
14
  const with_load_progress_1 = require("./with-load-progress");
14
15
  let lastNavigatedPath;
15
16
  // this is copy from portal/src/client/app/utils/loadAndNavigate.ts, for case when we need to run Redoc without Realm
16
17
  function loadAndNavigate(_a) {
17
18
  return __awaiter(this, arguments, void 0, function* ({ navigate, to, origin = 'browser', options, }) {
18
- var _b;
19
+ var _b, _c;
19
20
  lastNavigatedPath = to;
20
21
  const { pathname, hash, search } = new URL(to, window.location.origin + window.location.pathname);
21
22
  // use window-shared loader instead of importing to prevent circular import issue
@@ -38,7 +39,11 @@ function loadAndNavigate(_a) {
38
39
  if (hash) {
39
40
  const el = document.getElementById(hash.slice(1));
40
41
  if (el) {
41
- el.scrollIntoView();
42
+ const navbar = (0, get_navbar_element_1.getNavbarElement)();
43
+ const navbarHeight = (_c = navbar === null || navbar === void 0 ? void 0 : navbar.offsetHeight) !== null && _c !== void 0 ? _c : 0;
44
+ const elementTop = el.getBoundingClientRect().top + window.scrollY;
45
+ const scrollPosition = elementTop - navbarHeight;
46
+ window.scrollTo({ top: scrollPosition, behavior: 'smooth' });
42
47
  }
43
48
  }
44
49
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/theme",
3
- "version": "0.61.0-next.1",
3
+ "version": "0.61.0-next.2",
4
4
  "description": "Shared UI components lib",
5
5
  "keywords": [
6
6
  "theme",
@@ -81,7 +81,7 @@
81
81
  "openapi-sampler": "1.6.2",
82
82
  "react-calendar": "5.1.0",
83
83
  "react-date-picker": "11.0.0",
84
- "@redocly/config": "0.41.0"
84
+ "@redocly/config": "0.41.1"
85
85
  },
86
86
  "scripts": {
87
87
  "watch": "tsc -p tsconfig.build.json && (concurrently \"tsc -w -p tsconfig.build.json\" \"tsc-alias -w -p tsconfig.build.json\")",
@@ -2,13 +2,15 @@ import React from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
4
  import type { JSX } from 'react';
5
+ import type { ColorMode } from '@redocly/theme/core/types';
5
6
 
6
7
  import { ContrastIcon } from '@redocly/theme/icons/ContrastIcon/ContrastIcon';
7
8
  import { MoonIcon } from '@redocly/theme/icons/MoonIcon/MoonIcon';
8
9
  import { SunIcon } from '@redocly/theme/icons/SunIcon/SunIcon';
10
+ import { DEFAULT_COLOR_MODES } from '@redocly/theme/core/constants';
9
11
 
10
12
  export type ColorModeIconProps = {
11
- mode?: 'dark' | 'light' | string;
13
+ mode?: ColorMode | string;
12
14
  className?: string;
13
15
  };
14
16
 
@@ -20,9 +22,9 @@ export function ColorModeIcon(props: ColorModeIconProps): JSX.Element {
20
22
 
21
23
  function Icon({ mode, className }: ColorModeIconProps) {
22
24
  switch (mode) {
23
- case 'dark':
25
+ case DEFAULT_COLOR_MODES.DARK:
24
26
  return <SunIcon data-testid="dark" />;
25
- case 'light':
27
+ case DEFAULT_COLOR_MODES.LIGHT:
26
28
  return <MoonIcon data-testid="light" />;
27
29
  default:
28
30
  return <ContrastIcon data-testid="custom" className={className + (mode ? ' ' + mode : '')} />;
@@ -2,7 +2,7 @@ import React from 'react';
2
2
 
3
3
  import type { JSX } from 'react';
4
4
 
5
- import { useMount, useColorSwitcher } from '@redocly/theme/core/hooks';
5
+ import { useColorSwitcher } from '@redocly/theme/core/hooks';
6
6
  import { ColorModeIcon } from '@redocly/theme/components/ColorModeSwitcher/ColorModeIcon';
7
7
  import { Button } from '@redocly/theme/components/Button/Button';
8
8
 
@@ -11,12 +11,7 @@ export type ColorModeSwitcherProps = {
11
11
  };
12
12
 
13
13
  export function ColorModeSwitcher({ className }: ColorModeSwitcherProps): JSX.Element | null {
14
- const { isSwitcherHidden, initActiveColorMode, switchColorMode, activeColorMode } =
15
- useColorSwitcher();
16
-
17
- useMount(() => {
18
- initActiveColorMode();
19
- });
14
+ const { isSwitcherHidden, switchColorMode, activeColorMode } = useColorSwitcher();
20
15
 
21
16
  if (isSwitcherHidden) {
22
17
  return null;
@@ -0,0 +1,6 @@
1
+ export function getProductClassName(productName: string): string {
2
+ return `product-${productName
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-+|-+$/g, '')}`;
6
+ }
@@ -30,3 +30,8 @@ export enum MobileMenuType {
30
30
  PRODUCT = 'PRODUCT',
31
31
  PAGE = 'PAGE',
32
32
  }
33
+
34
+ export const DEFAULT_COLOR_MODES = {
35
+ LIGHT: 'light',
36
+ DARK: 'dark',
37
+ } as const;
@@ -48,3 +48,4 @@ export * from './use-connect-mcp-button';
48
48
  export * from './catalog/use-catalog-entity-details';
49
49
  export * from './catalog/use-catalog-entity-schema';
50
50
  export * from './catalog/use-catalog-table-header-cell-actions';
51
+ export * from './use-store';
@@ -1,83 +1,56 @@
1
- import { useCallback, useSyncExternalStore } from 'react';
1
+ import { useCallback } from 'react';
2
2
 
3
+ import { createStore, useStore } from '../use-store';
3
4
  import { isBrowser } from '../../utils/js-utils';
4
5
 
5
6
  const RECENT_SEARCHES_KEY = 'recentSearches';
6
7
  const RECENT_SEARCHES_LIMIT = 5;
7
8
 
8
- const createRecentSearchesStore = () => {
9
- const subscribers = new Set<() => void>();
10
- let cachedSnapshot: string[];
11
-
12
- const getSnapshot = (): string[] => {
13
- if (!isBrowser()) return [];
14
-
15
- if (cachedSnapshot) return cachedSnapshot;
16
-
17
- try {
18
- const stored = localStorage.getItem(RECENT_SEARCHES_KEY);
19
- cachedSnapshot = stored ? JSON.parse(stored) : [];
20
- return cachedSnapshot;
21
- } catch (e) {
22
- cachedSnapshot = [];
23
- return cachedSnapshot;
24
- }
25
- };
26
-
27
- const updateItems = (value: string, isAdd: boolean) => {
28
- if (!isBrowser()) return;
29
-
30
- const currentItems = getSnapshot();
31
- const valueIndex = currentItems.indexOf(value);
32
-
33
- if (valueIndex !== -1) {
34
- currentItems.splice(valueIndex, 1);
35
- }
36
-
37
- if (isAdd) {
38
- currentItems.unshift(value);
39
- }
40
-
41
- const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
42
-
43
- localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(limitedItems));
44
- cachedSnapshot = limitedItems;
45
-
46
- subscribers.forEach((callback) => callback());
47
- };
48
-
49
- const subscribe = (callback: () => void) => {
50
- subscribers.add(callback);
51
- return () => subscribers.delete(callback);
52
- };
53
-
54
- return {
55
- getSnapshot,
56
- subscribe,
57
- updateItems,
58
- };
59
- };
60
-
61
- const recentSearchesStore = createRecentSearchesStore();
9
+ const recentSearchesStore = createStore<string[]>({
10
+ storageKey: RECENT_SEARCHES_KEY,
11
+ });
62
12
 
63
13
  export const useRecentSearches = (): {
64
14
  items: string[];
65
15
  addSearchHistoryItem: (value: string) => void;
66
16
  removeSearchHistoryItem: (value: string) => void;
67
17
  } => {
68
- const items = useSyncExternalStore(
69
- recentSearchesStore.subscribe,
70
- recentSearchesStore.getSnapshot,
71
- () => [],
18
+ const [items, setItems] = useStore<string[]>(recentSearchesStore, []);
19
+
20
+ const updateItems = useCallback(
21
+ (value: string, isAdd: boolean) => {
22
+ if (!isBrowser()) return;
23
+
24
+ const currentItems = [...items];
25
+ const valueIndex = currentItems.indexOf(value);
26
+ if (valueIndex !== -1) {
27
+ currentItems.splice(valueIndex, 1);
28
+ }
29
+
30
+ if (isAdd) {
31
+ currentItems.unshift(value);
32
+ }
33
+
34
+ const limitedItems = currentItems.slice(0, RECENT_SEARCHES_LIMIT);
35
+
36
+ setItems(limitedItems);
37
+ },
38
+ [items, setItems],
72
39
  );
73
40
 
74
- const addSearchHistoryItem = useCallback((value: string) => {
75
- recentSearchesStore.updateItems(value, true);
76
- }, []);
41
+ const addSearchHistoryItem = useCallback(
42
+ (value: string) => {
43
+ updateItems(value, true);
44
+ },
45
+ [updateItems],
46
+ );
77
47
 
78
- const removeSearchHistoryItem = useCallback((value: string) => {
79
- recentSearchesStore.updateItems(value, false);
80
- }, []);
48
+ const removeSearchHistoryItem = useCallback(
49
+ (value: string) => {
50
+ updateItems(value, false);
51
+ },
52
+ [updateItems],
53
+ );
81
54
 
82
55
  return { items, addSearchHistoryItem, removeSearchHistoryItem };
83
56
  };
@@ -1,23 +1,35 @@
1
- import { useState } from 'react';
1
+ import { useMemo } from 'react';
2
2
 
3
3
  import { useThemeConfig } from './use-theme-config';
4
4
  import { useThemeHooks } from './use-theme-hooks';
5
+ import { createStore, useStore } from './use-store';
6
+ import { DEFAULT_COLOR_MODES } from '../constants';
7
+ import { isBrowser } from '../utils/js-utils';
8
+
9
+ const COLOR_MODE_KEY = 'colorSchema';
10
+ const colorModeStore = createStore<string>({
11
+ storageKey: COLOR_MODE_KEY,
12
+ });
5
13
 
6
14
  export const useColorSwitcher = () => {
7
15
  const themeSettings = useThemeConfig();
8
16
  const { useTelemetry } = useThemeHooks();
9
17
  const telemetry = useTelemetry();
10
- const colorMode = themeSettings.colorMode;
11
- const modes = colorMode?.modes || ['light', 'dark'];
12
- const defaultColor = modes[0] || 'light';
13
- const [activeColorMode, setActiveColorMode] = useState(defaultColor);
14
-
15
- const initActiveColorMode = (): void => {
16
- const activeMode = Array.from(document.documentElement.classList).find((c) =>
17
- modes.includes(c),
18
- );
19
- setActiveColorMode(activeMode || defaultColor);
20
- };
18
+ const themeColorMode = themeSettings.colorMode;
19
+
20
+ const modes = useMemo(
21
+ () => themeColorMode?.modes || [DEFAULT_COLOR_MODES.LIGHT, DEFAULT_COLOR_MODES.DARK],
22
+ [themeColorMode],
23
+ );
24
+ const documentMode = useMemo(() => {
25
+ if (!isBrowser()) return;
26
+ return Array.from(document.documentElement.classList).find((c) => modes.includes(c));
27
+ }, [modes]);
28
+
29
+ const [activeColorMode, setActiveColorMode] = useStore<string>(
30
+ colorModeStore,
31
+ documentMode ?? modes[0] ?? DEFAULT_COLOR_MODES.LIGHT,
32
+ );
21
33
 
22
34
  const switchColorMode = (mode?: string): void => {
23
35
  if (mode && !modes.includes(mode)) {
@@ -25,23 +37,22 @@ export const useColorSwitcher = () => {
25
37
  }
26
38
 
27
39
  const activeIndex = modes.indexOf(activeColorMode);
28
- // If specific mode is provided, use it, otherwise cycle through modes
29
40
  const newMode = mode || (activeIndex < modes.length - 1 ? modes[activeIndex + 1] : modes[0]);
30
41
 
31
- localStorage.setItem('colorSchema', newMode);
32
- document.documentElement.className = `${newMode} notransition`;
42
+ const root = document.documentElement;
43
+
44
+ modes.forEach((mode) => root.classList.remove(mode));
45
+ root.classList.add(newMode, 'notransition');
33
46
 
34
47
  window.requestAnimationFrame(() => {
35
- document.documentElement.classList.remove('notransition');
48
+ root.classList.remove('notransition');
36
49
  });
37
50
  telemetry.sendColorModeSwitchedMessage({ from: activeColorMode, to: newMode });
38
-
39
51
  setActiveColorMode(newMode);
40
52
  };
41
53
 
42
54
  return {
43
- isSwitcherHidden: colorMode?.hide,
44
- initActiveColorMode,
55
+ isSwitcherHidden: themeColorMode?.hide,
45
56
  switchColorMode,
46
57
  activeColorMode,
47
58
  };
@@ -1,5 +1,7 @@
1
1
  import { useNavigate } from 'react-router-dom';
2
2
 
3
+ import { getProductClassName } from '@redocly/theme/components/Product/utils';
4
+
3
5
  import { useThemeHooks } from './use-theme-hooks';
4
6
  import { withPathPrefix } from '../utils';
5
7
 
@@ -13,6 +15,16 @@ export function useProductPicker() {
13
15
  function setProduct(product: typeof currentProduct) {
14
16
  if (!product) return;
15
17
  telemetry.sendProductPickedMessage({ product: product.slug });
18
+ if (typeof document === 'undefined') return;
19
+
20
+ if (product.name) {
21
+ const root = document.documentElement;
22
+ Array.from(root.classList)
23
+ .filter((c) => c.startsWith('product-'))
24
+ .forEach((c) => root.classList.remove(c));
25
+ root.classList.add(getProductClassName(product.name));
26
+ }
27
+
16
28
  loadAndNavigate({ navigate, to: withPathPrefix(product.link) });
17
29
  }
18
30
  return {
@@ -0,0 +1,95 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ import { isBrowser } from '../utils/js-utils';
4
+
5
+ type Store<T> = {
6
+ getValue: (defaultValue: T) => T;
7
+ setValue: (next: T) => void;
8
+ subscribe: (callback: () => void) => Unsubscribe;
9
+ };
10
+
11
+ type Unsubscribe = () => void;
12
+
13
+ type CreateStoreProps<T> = {
14
+ storageKey: string;
15
+ storageType?: 'localStorage' | 'sessionStorage';
16
+ serializer?: {
17
+ parse: (value: string) => T;
18
+ serialize: (value: T) => string;
19
+ };
20
+ };
21
+
22
+ export function createStore<T>({
23
+ storageKey,
24
+ storageType = 'localStorage',
25
+ serializer = {
26
+ parse: (value: string) => JSON.parse(value) as T,
27
+ serialize: (value: T) => JSON.stringify(value),
28
+ },
29
+ }: CreateStoreProps<T>): Store<T> {
30
+ const subscribers = new Set<() => void>();
31
+ let cachedValue: T;
32
+
33
+ const shouldSerialize = (value: unknown) => typeof value !== 'string';
34
+
35
+ const getValue = (defaultValue: T): T => {
36
+ if (!isBrowser()) return defaultValue;
37
+ if (cachedValue !== undefined) return cachedValue;
38
+
39
+ const value = window[storageType].getItem(storageKey);
40
+ if (!value) {
41
+ cachedValue = defaultValue;
42
+ return cachedValue;
43
+ }
44
+
45
+ if (!shouldSerialize(defaultValue)) {
46
+ cachedValue = value as T;
47
+ return cachedValue;
48
+ }
49
+
50
+ try {
51
+ cachedValue = JSON.parse(value) as T;
52
+ } catch {
53
+ cachedValue = defaultValue;
54
+ }
55
+
56
+ return cachedValue;
57
+ };
58
+
59
+ const setValue = (next: T): void => {
60
+ if (!isBrowser()) return;
61
+ try {
62
+ window[storageType].setItem(
63
+ storageKey,
64
+ shouldSerialize(next) ? serializer.serialize(next) : (next as string),
65
+ );
66
+ cachedValue = next;
67
+ subscribers.forEach((callback) => callback());
68
+ } catch {
69
+ return;
70
+ }
71
+ };
72
+
73
+ const subscribe = (callback: () => void): Unsubscribe => {
74
+ subscribers.add(callback);
75
+ return () => {
76
+ subscribers.delete(callback);
77
+ };
78
+ };
79
+
80
+ return {
81
+ getValue,
82
+ setValue,
83
+ subscribe,
84
+ };
85
+ }
86
+
87
+ export function useStore<T>(store: Store<T>, defaultValue: T) {
88
+ const value = useSyncExternalStore(
89
+ store.subscribe,
90
+ () => store.getValue(defaultValue),
91
+ () => defaultValue,
92
+ );
93
+
94
+ return [value, store.setValue] as const;
95
+ }
@@ -1 +1,5 @@
1
+ import { DEFAULT_COLOR_MODES } from '../constants';
2
+
1
3
  export type Plural<T extends string> = `${T}s`;
4
+
5
+ export type ColorMode = (typeof DEFAULT_COLOR_MODES)[keyof typeof DEFAULT_COLOR_MODES];
@@ -1,5 +1,6 @@
1
1
  import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
2
2
 
3
+ import { getNavbarElement } from './get-navbar-element';
3
4
  import { withLoadProgress } from './with-load-progress';
4
5
 
5
6
  export type HistoryOrigin = 'pm' | 'browser';
@@ -45,7 +46,11 @@ export async function loadAndNavigate({
45
46
  if (hash) {
46
47
  const el = document.getElementById(hash.slice(1));
47
48
  if (el) {
48
- el.scrollIntoView();
49
+ const navbar = getNavbarElement();
50
+ const navbarHeight = navbar?.offsetHeight ?? 0;
51
+ const elementTop = el.getBoundingClientRect().top + window.scrollY;
52
+ const scrollPosition = elementTop - navbarHeight;
53
+ window.scrollTo({ top: scrollPosition, behavior: 'smooth' });
49
54
  }
50
55
  } else {
51
56
  window.scrollTo(0, 0);