- history({
- search: `?tab=${search.tab}&subtab=${key}`
- })
- }
- items={tabItems}
- />
+
+
+ history({
+ search: `?tab=${search.tab}&subtab=${key}`
+ })
+ }
+ items={tabItems}
+ />
+
);
}
diff --git a/client/src/components/shop-info/shop-info.section-navigator.component.jsx b/client/src/components/shop-info/shop-info.section-navigator.component.jsx
new file mode 100644
index 000000000..69fd36e85
--- /dev/null
+++ b/client/src/components/shop-info/shop-info.section-navigator.component.jsx
@@ -0,0 +1,137 @@
+import { Select } from "antd";
+import { useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import "./shop-info.section-navigator.styles.scss";
+
+const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active";
+
+export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) {
+ const { t } = useTranslation();
+ const targetMapRef = useRef(new Map());
+ const highlightedTargetRef = useRef(null);
+ const [options, setOptions] = useState([]);
+ const [selectedSection, setSelectedSection] = useState(undefined);
+
+ useEffect(() => {
+ const tabsContainer = tabsRef.current;
+ if (!tabsContainer) return undefined;
+
+ let animationFrameId = 0;
+
+ const refreshOptions = () => {
+ const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active");
+ if (!activePane) {
+ targetMapRef.current = new Map();
+ setOptions([]);
+ return;
+ }
+
+ const nextTargetMap = new Map();
+ const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row"))
+ .filter((card) => {
+ const titleNode = getOwnCardTitleNode(card);
+ if (!titleNode?.textContent?.trim()) return false;
+
+ const ancestorCard = card.parentElement?.closest(".imex-form-row");
+ return !ancestorCard || !activePane.contains(ancestorCard);
+ })
+ .map((card, index) => {
+ const label = getOwnCardTitleNode(card)?.textContent?.trim();
+ const value = `${activeTabKey}-shop-info-section-${index}`;
+
+ nextTargetMap.set(value, card);
+
+ return {
+ label,
+ value
+ };
+ });
+
+ targetMapRef.current = nextTargetMap;
+ setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions));
+ };
+
+ const scheduleRefresh = () => {
+ cancelAnimationFrame(animationFrameId);
+ animationFrameId = requestAnimationFrame(refreshOptions);
+ };
+
+ scheduleRefresh();
+
+ const observer = new MutationObserver(scheduleRefresh);
+ observer.observe(tabsContainer, {
+ childList: true,
+ subtree: true,
+ characterData: true,
+ attributes: true,
+ attributeFilter: ["class"]
+ });
+
+ return () => {
+ cancelAnimationFrame(animationFrameId);
+ observer.disconnect();
+ };
+ }, [activeTabKey, tabsRef]);
+
+ useEffect(() => {
+ clearHighlightedTarget(highlightedTargetRef);
+ setSelectedSection(undefined);
+ }, [activeTabKey]);
+
+ const handleSectionChange = (value) => {
+ setSelectedSection(value);
+
+ clearHighlightedTarget(highlightedTargetRef);
+ if (!value) return;
+
+ const target = targetMapRef.current.get(value);
+ if (target) {
+ target.classList.add(HIGHLIGHT_CLASS);
+ highlightedTargetRef.current = target;
+ target.scrollIntoView({
+ behavior: "smooth",
+ block: "start"
+ });
+ }
+
+ window.setTimeout(() => {
+ setSelectedSection(undefined);
+ }, 0);
+ };
+
+ return (
+
+
+
+ );
+}
+
+function getOwnCardTitleNode(card) {
+ const headNode = Array.from(card.children).find((child) => child.classList?.contains("ant-card-head"));
+ return headNode?.querySelector(".ant-card-head-title");
+}
+
+function clearHighlightedTarget(highlightedTargetRef) {
+ if (highlightedTargetRef.current) {
+ highlightedTargetRef.current.classList.remove(HIGHLIGHT_CLASS);
+ highlightedTargetRef.current = null;
+ }
+}
+
+function areOptionsEqual(currentOptions, nextOptions) {
+ if (currentOptions.length !== nextOptions.length) return false;
+
+ return currentOptions.every((option, index) => {
+ const nextOption = nextOptions[index];
+ return option.label === nextOption.label && option.value === nextOption.value;
+ });
+}
diff --git a/client/src/components/shop-info/shop-info.section-navigator.styles.scss b/client/src/components/shop-info/shop-info.section-navigator.styles.scss
new file mode 100644
index 000000000..eff4d1a0f
--- /dev/null
+++ b/client/src/components/shop-info/shop-info.section-navigator.styles.scss
@@ -0,0 +1,29 @@
+.shop-info-section-navigator {
+ max-width: 360px;
+ width: min(360px, 100%);
+
+ .ant-select {
+ width: 100%;
+ }
+}
+
+.imex-form-row.shop-info-section-navigator__target--active.ant-card {
+ border-color: color-mix(
+ in srgb,
+ var(--ant-colorPrimary, #1890ff) 65%,
+ var(--imex-form-surface-border)
+ );
+ background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 7%, var(--imex-form-surface));
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 24%, transparent);
+ transition: border-color 0.2s ease,
+ background-color 0.2s ease,
+ box-shadow 0.2s ease;
+
+ .ant-card-head {
+ background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 12%, var(--imex-form-surface-head));
+ }
+
+ .ant-card-body {
+ background: color-mix(in srgb, var(--ant-colorPrimary, #1890ff) 7%, var(--imex-form-surface));
+ }
+}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index d394b9c69..03e9072d9 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -765,6 +765,7 @@
"notification_options": "Notification Options",
"notemplatesavailable": "No templates available to add.",
"notespresets": "Notes Presets",
+ "jump_to_section": "Jump to section",
"notifications": {
"followers": "Notifications"
},
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 77288adf9..b696bc1c6 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -765,6 +765,7 @@
"notification_options": "",
"notemplatesavailable": "",
"notespresets": "",
+ "jump_to_section": "",
"notifications": {
"followers": ""
},
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 5b0a327e8..cf5f12e5b 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -765,6 +765,7 @@
"notification_options": "",
"notemplatesavailable": "",
"notespresets": "",
+ "jump_to_section": "",
"notifications": {
"followers": ""
},