Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/node_modules
/dist
storybook-static
debug-storybook.log
debug-storybook.log
.vscode
42 changes: 35 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
kzamanbd marked this conversation as resolved.
"@wordpress/components": "^32.5.0",
"@wordpress/dataviews": "^14.0.0",
"@wordpress/hooks": "^4.43.0",
Expand Down
5 changes: 2 additions & 3 deletions src/components/settings/Settings.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3576,7 +3576,7 @@ const dokanSettingsSchema: SettingsElement[] = [
"dependencies": [],
"validations": [],
"variant": "notice",
"value": null,
"value": '',
"notice_type": "warning",
"notice_icon": "Info",
"notice_title": "",
Expand All @@ -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",
Expand Down Expand Up @@ -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": [],
Expand Down
19 changes: 16 additions & 3 deletions src/components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function Settings({
applyFilters,
initialPage,
onNavigate,
searchPlaceholder,
searchable = true,
}: SettingsProps) {
return (
<SettingsProvider
Expand All @@ -44,6 +46,8 @@ export function Settings({
<SettingsInner
title={title}
className={className}
searchPlaceholder={searchPlaceholder}
searchable={searchable}
/>
</SettingsProvider>
);
Expand All @@ -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);
Expand All @@ -78,7 +86,7 @@ function SettingsInner({
return (
<div
className={cn(
'relative flex min-h-[500px] rounded-lg border border-border bg-background overflow-hidden',
'relative flex min-h-125 rounded-lg border border-border bg-background overflow-hidden',
className
)}
data-testid="settings-root"
Expand Down Expand Up @@ -133,7 +141,11 @@ function SettingsInner({
</div>
)}

<SettingsSidebar className="flex-1 overflow-y-auto" />
<SettingsSidebar
className="flex-1 overflow-y-auto"
searchPlaceholder={searchPlaceholder}
searchable={searchable}
/>
</aside>
)}

Expand Down Expand Up @@ -184,7 +196,8 @@ function usePrevious<T>(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';
102 changes: 79 additions & 23 deletions src/components/settings/settings-content.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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;
Expand All @@ -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 <div> so nested interactive children (doc links, tooltip triggers,
// field controls) keep their native behavior — a <button> would invalidate
// them. When collapsible, we add role="button" + keyboard handlers for a11y.
const HeadingTag: keyof JSX.IntrinsicElements = 'div';

return (
<div className={ cn( 'rounded-lg border overflow-hidden', section.is_danger ? 'bg-destructive/10 border-destructive/20' : 'border-border bg-card' ) } data-testid={`settings-section-${section.id}`}>
{hasHeading && (
<div className={ cn( 'px-5 pt-5 pb-3 flex justify-between items-start', section?.children?.length > 0 && 'border-b border-border' ) }>
<HeadingTag
{...(collapsible
? {
role: 'button' as const,
tabIndex: 0,
onClick: toggle,
onKeyDown: onHeadingKeyDown,
'aria-expanded': open,
'aria-controls': contentId,
}
: {})}
className={ cn(
'px-5 pt-5 pb-3 flex justify-between items-start w-full text-left',
hasChildren && bodyVisible && 'border-b border-border',
collapsible && 'cursor-pointer select-none hover:bg-muted/30 transition-colors'
) }
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
{sectionLabel && (
Expand All @@ -234,9 +274,12 @@ function SettingsSection({ section }: { section: SettingsElementType }) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<button type="button" className="inline-flex">
<span
className="inline-flex"
onClick={(e) => e.stopPropagation()}
>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</button>
</span>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs text-xs">{tooltip}</p>
Expand All @@ -252,25 +295,38 @@ function SettingsSection({ section }: { section: SettingsElementType }) {
</p>
)}
</div>
{section.doc_link && (
<a
href={section.doc_link}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground flex gap-1 items-center text-sm hover:text-foreground transition-colors shrink-0"
>
<FileText className="size-4" />
{ section.doc_link_text ?? '' }
</a>
)}
</div>
<div className="flex items-center gap-3 shrink-0 self-center">
{section.doc_link && (
<a
href={section.doc_link}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-muted-foreground flex gap-1 items-center text-sm hover:text-foreground transition-colors"
>
<FileText className="size-4" />
{ section.doc_link_text ?? '' }
</a>
)}
{collapsible && (
<ChevronDown
className={ cn(
'size-5 text-muted-foreground transition-transform duration-200',
open && 'rotate-180'
) }
/>
)}
</div>
</HeadingTag>
)}

<div className="divide-y divide-border">
{section.children?.map((child) => (
<ElementRenderer key={child.id} element={child} />
))}
</div>
{hasChildren && bodyVisible && (
<div id={contentId} className="divide-y divide-border">
{section.children?.map((child) => (
<ElementRenderer key={child.id} element={child} />
))}
</div>
)}
</div>
);
}
Expand Down
38 changes: 24 additions & 14 deletions src/components/settings/settings-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ function collectSearchableText(element: SettingsElement): string {
return texts.join(' ').toLowerCase();
}

export function SettingsSidebar({ className }: { className?: string }) {
export function SettingsSidebar({
className,
searchPlaceholder,
searchable = true,
}: {
className?: string;
searchPlaceholder?: string;
searchable?: boolean;
}) {
const {
schema,
activePage,
Expand Down Expand Up @@ -152,23 +160,25 @@ export function SettingsSidebar({ className }: { className?: string }) {
return (
<div className={cn('flex flex-col h-full', className)} data-testid="settings-sidebar">
{/* Deep-search input */}
<div className="shrink-0 p-2">
<div className="relative">
<Search className="text-muted-foreground pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
<Input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-8 pl-8"
aria-label="Search settings"
data-testid="settings-search"
/>
{searchable && (
<div className="shrink-0 p-2 px-4 mt-2">
<div className="relative">
<Search className="text-muted-foreground pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
<Input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-8 pl-8"
placeholder={searchPlaceholder}
aria-label="Search settings"
data-testid="settings-search"
/>
</div>
</div>
</div>
)}
Comment thread
kzamanbd marked this conversation as resolved.

<LayoutMenu
items={filteredItems}
searchable={false}
activeItemId={activeItemId}
onItemClick={(item) => {
const isPage = schema.some((p) => p.id === item.id);
Expand Down
Loading
Loading