diff --git a/.gitignore b/.gitignore
index 22a8969..6aea623 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/node_modules
/dist
storybook-static
-debug-storybook.log
\ No newline at end of file
+debug-storybook.log
+.vscode
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 70e5a6a..09e382c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@dnd-kit/react": "^0.3.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+ "@wordpress/api-fetch": "^7.43.0",
"@wordpress/components": "^32.5.0",
"@wordpress/dataviews": "^14.0.0",
"@wordpress/hooks": "^4.43.0",
@@ -8251,6 +8252,20 @@
"npm": ">=8.19.2"
}
},
+ "node_modules/@wordpress/api-fetch": {
+ "version": "7.44.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-7.44.0.tgz",
+ "integrity": "sha512-KZP5Y0AzUVPRbwCsp2MUNEjIyYPJdaa7ojzYyc/IVlaAlbXVdd0Ofk8UDf4l8PjtXkyyPs9pX9sFy5iNcrF2cQ==",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@wordpress/i18n": "^6.17.0",
+ "@wordpress/url": "^4.44.0"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
"node_modules/@wordpress/babel-preset-default": {
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.39.0.tgz",
@@ -8875,9 +8890,9 @@
}
},
"node_modules/@wordpress/hooks": {
- "version": "4.43.0",
- "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.43.0.tgz",
- "integrity": "sha512-BY7GPjEwhOlgkavVak40E3RtA8Z9ehydqTZckRoesMRjXYfxKSzr1C1FT4wAPS5uXM1pNlWivfofMaJjVNQu5w==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-4.44.0.tgz",
+ "integrity": "sha512-6p2vFvoFaovqnKFnIoy6Kib2XJhTwaJ1VhMXp4tM2PhSLnFMXVm1TpcHeX/kH7E6sWKJACBrDR6FH2nGYMk5dA==",
"license": "GPL-2.0-or-later",
"engines": {
"node": ">=18.12.0",
@@ -8895,13 +8910,13 @@
}
},
"node_modules/@wordpress/i18n": {
- "version": "6.16.0",
- "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.16.0.tgz",
- "integrity": "sha512-D8yiDLzOrs9Aa4Cc1nm7m2OMilZeG9Qd7zHauMIDQujwHOe9xrOyH9ppDDko6AAWb+GeUYsf5zf2Efu5saLq0w==",
+ "version": "6.17.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-6.17.0.tgz",
+ "integrity": "sha512-v1SLBweg7CRzQ+5+WSC1U93i8h9d3AoB0YBvMsd6gWI5vO8Zh4YKlEMexvrHQC++WN83egwqux84fWEdeU0MUA==",
"license": "GPL-2.0-or-later",
"dependencies": {
"@tannin/sprintf": "^1.3.2",
- "@wordpress/hooks": "^4.43.0",
+ "@wordpress/hooks": "^4.44.0",
"gettext-parser": "^1.3.1",
"memize": "^2.1.0",
"tannin": "^1.2.0"
@@ -9468,6 +9483,19 @@
"npm": ">=8.19.2"
}
},
+ "node_modules/@wordpress/url": {
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.44.0.tgz",
+ "integrity": "sha512-kWalXttgtRwFy4szBPX9dJcqHErRC0V9JuZ7uxdrxxdXl6WNv+lx8SYpLx12q3Zk6zNIw73M8E5wHON7eyXZZw==",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "remove-accents": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
"node_modules/@wordpress/warning": {
"version": "3.43.0",
"resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.43.0.tgz",
diff --git a/package.json b/package.json
index bde37bb..fdfd6cb 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"@dnd-kit/react": "^0.3.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+ "@wordpress/api-fetch": "^7.43.0",
"@wordpress/components": "^32.5.0",
"@wordpress/dataviews": "^14.0.0",
"@wordpress/hooks": "^4.43.0",
diff --git a/src/components/settings/Settings.stories.tsx b/src/components/settings/Settings.stories.tsx
index 81006e5..05b66ba 100644
--- a/src/components/settings/Settings.stories.tsx
+++ b/src/components/settings/Settings.stories.tsx
@@ -3576,7 +3576,7 @@ const dokanSettingsSchema: SettingsElement[] = [
"dependencies": [],
"validations": [],
"variant": "notice",
- "value": null,
+ "value": '',
"notice_type": "warning",
"notice_icon": "Info",
"notice_title": "",
@@ -3600,7 +3600,7 @@ const dokanSettingsSchema: SettingsElement[] = [
"dependencies": [],
"validations": [],
"variant": "notice",
- "value": null,
+ "value": '',
"notice_type": "warning",
"notice_icon": "Info",
"notice_title": "Size Guide Title",
@@ -5121,7 +5121,6 @@ const dokanSettingsSchema: SettingsElement[] = [
"doc_link": ""
}
],
- "description": "Set up AI to elevate your platform with enhanced capabilities.",
"dependency_key": "product_generation",
"dependencies": [],
"validations": [],
diff --git a/src/components/settings/index.tsx b/src/components/settings/index.tsx
index 179c7e2..fc1dc8a 100644
--- a/src/components/settings/index.tsx
+++ b/src/components/settings/index.tsx
@@ -27,6 +27,8 @@ export function Settings({
applyFilters,
initialPage,
onNavigate,
+ searchPlaceholder,
+ searchable = true,
}: SettingsProps) {
return (
);
@@ -56,9 +60,13 @@ export function Settings({
function SettingsInner({
title,
className,
+ searchPlaceholder,
+ searchable,
}: {
title?: string;
className?: string;
+ searchPlaceholder?: string;
+ searchable?: boolean;
}) {
const { loading, activeSubpage, isSidebarVisible } = useSettings();
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
@@ -78,7 +86,7 @@ function SettingsInner({
return (
)}
-
+
)}
@@ -184,7 +196,8 @@ function usePrevious
(value: T): T | undefined {
// Re-exports
// ============================================
-export { useSettings } from './settings-context';
+export { SettingsProvider, useSettings } from './settings-context';
export type { ApplyFiltersFunction } from './settings-context';
+export { FieldRenderer } from './field-renderer';
export { formatSettingsData, extractValues } from './settings-formatter';
export type { SettingsElement, SettingsProps, FieldComponentProps, SaveButtonRenderProps } from './settings-types';
diff --git a/src/components/settings/settings-content.tsx b/src/components/settings/settings-content.tsx
index 38a69f0..1bd92ef 100644
--- a/src/components/settings/settings-content.tsx
+++ b/src/components/settings/settings-content.tsx
@@ -1,8 +1,9 @@
+import { useState } from 'react';
import type { SettingsElement as SettingsElementType } from './settings-types';
import { useSettings } from './settings-context';
import { FieldRenderer } from './field-renderer';
import { cn } from '@/lib/utils';
-import { FileText, Info } from "lucide-react";
+import { ChevronDown, FileText, Info } from "lucide-react";
import { ScrollArea, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui";
import { Button } from "@/components/ui/button";
import { RawHTML } from "@wordpress/element";
@@ -43,8 +44,12 @@ export function SettingsContent({ className }: { className?: string }) {
};
// Determine whether to show a save area
- // Hidden when the active page/subpage sets hide_save: true (e.g. License page)
- const showSaveArea = Boolean(save) && !contentSource?.hide_save;
+ // Hidden when the active page/subpage — or the currently-active tab — sets
+ // hide_save: true. Tab-level hide_save lets a single tab under a shared
+ // scope (e.g. an action-only tab) opt out without affecting siblings.
+ const activeTabElement = tabs.find((t) => t.id === activeTab);
+ const showSaveArea =
+ Boolean(save) && !contentSource?.hide_save && !activeTabElement?.hide_save;
if (!contentSource) {
return (
@@ -209,6 +214,8 @@ function ContentBlock({ element }: { element: SettingsElementType }) {
function SettingsSection({ section }: { section: SettingsElementType }) {
const { shouldDisplay } = useSettings();
+ const collapsible = section.collapsible === true;
+ const [open, setOpen] = useState(!section.collapsed);
if (!shouldDisplay(section)) {
return null;
@@ -217,11 +224,44 @@ function SettingsSection({ section }: { section: SettingsElementType }) {
const sectionLabel = section.label || section.title || '';
const hasHeading = Boolean(sectionLabel || section.description);
const tooltip = section?.tooltip || '';
+ const hasChildren = (section.children?.length ?? 0) > 0;
+ const contentId = `settings-section-content-${section.id}`;
+ const bodyVisible = !collapsible || open;
+
+ const toggle = () => setOpen((o) => !o);
+ const onHeadingKeyDown = (e: React.KeyboardEvent) => {
+ if (!collapsible) return;
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggle();
+ }
+ };
+
+ // Always a so nested interactive children (doc links, tooltip triggers,
+ // field controls) keep their native behavior — a