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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"require": "./dist/themes/index.js"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"import": "./dist/utils/index.js",
"require": "./dist/utils/index.js"
"types": "./dist/lib/utils.d.ts",
"import": "./dist/lib/utils.js",
"require": "./dist/lib/utils.js"
},
"./settings": {
"types": "./dist/components/settings/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/settings-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function SettingsSidebar({
value={search}
onChange={(e) => setSearch(e.target.value)}
className="h-8 pl-8"
placeholder={searchPlaceholder}
placeholder={searchPlaceholder || 'Search...'}
aria-label="Search settings"
data-testid="settings-search"
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/alert-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function AlertDialogContent({
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
return (
<AlertDialogPortal className={ cn('pui-root', mode) } style={cssVariables}>
<AlertDialogPortal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function ComboboxContent({
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
return (
<ComboboxPrimitive.Portal className={ cn('pui-root', mode) } style={cssVariables}>
<ComboboxPrimitive.Portal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<ComboboxPrimitive.Positioner
side={side}
sideOffset={sideOffset}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function DialogContent({
const mode = theme?.mode ?? "light"
const cssVariables = theme?.cssVariables ?? defaultCssVariables
return (
<DialogPortal className={cn("pui-root", mode)} style={cssVariables}>
<DialogPortal className={cn("pui-root", mode, theme?.className)} style={cssVariables}>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function DropdownMenuContent({
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
return (
<MenuPrimitive.Portal className={ cn('pui-root', mode) } style={cssVariables}>
<MenuPrimitive.Portal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<MenuPrimitive.Positioner
className="isolate z-50 outline-none"
align={align}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export {
FieldContent,
FieldTitle,
} from "./field";
export { DataViews, type DataViewAction, type DataViewField, type DataViewFilterField, type DataViewFilterProps, type DataViewLayouts, type DataViewsProps, type DataViewState } from '../wordpress/dataviews';
export { DataViews, type ConfirmActionTone, type DataViewAction, type DataViewField, type DataViewFilterField, type DataViewFilterProps, type DataViewLayouts, type DataViewsProps, type DataViewState, type DestructiveActionConfig } from '../wordpress/dataviews';
export { DataForm, useFormValidity } from '@wordpress/dataviews/wp';
export type {
DataFormProps,
Expand Down
8 changes: 4 additions & 4 deletions src/components/ui/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ export function Modal({

const container = document.createElement("div");
container.setAttribute("data-pui-modal-root", "true");
container.className = cn("pui-root", mode);
container.className = cn("pui-root", mode, theme?.className);
Object.assign(container.style, cssVariables);

document.body.appendChild(container);
Expand All @@ -331,15 +331,15 @@ export function Modal({
previousActiveElement.current.focus();
}
};
}, [open]);
}, [open, mode, cssVariables, theme?.className]);

// Keep portal container class and CSS variables in sync with theme
useEffect(() => {
if (portalContainer) {
portalContainer.className = cn("pui-root", mode);
portalContainer.className = cn("pui-root", mode, theme?.className);
Object.assign(portalContainer.style, cssVariables);
}
}, [mode, cssVariables, portalContainer]);
}, [mode, cssVariables, portalContainer, theme?.className]);

// Handle escape key
useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function PopoverContent({
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
return (
<PopoverPrimitive.Portal className={cn("pui-root", mode)} style={cssVariables}>
<PopoverPrimitive.Portal className={cn("pui-root", mode, theme?.className)} style={cssVariables}>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function SelectContent({
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
return (
<SelectPrimitive.Portal className={ cn('pui-root', mode) } style={cssVariables}>
<SelectPrimitive.Portal className={ cn('pui-root', mode, theme?.className) } style={cssVariables}>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function SheetContent({
const cssVariables = theme?.cssVariables ?? defaultCssVariables;

return (
<SheetPortal className={cn("pui-root", mode)} style={cssVariables}>
<SheetPortal className={cn("pui-root", mode, theme?.className)} style={cssVariables}>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function TooltipContent({
const mode = theme?.mode ?? 'light';
const cssVariables = theme?.cssVariables ?? defaultCssVariables;
return (
<TooltipPrimitive.Portal className={ cn('pui-root', mode, portalClassName) } style={cssVariables}>
<TooltipPrimitive.Portal className={ cn('pui-root', mode, portalClassName, theme?.className) } style={cssVariables}>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
Expand Down
99 changes: 99 additions & 0 deletions src/components/wordpress/DataViews.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,105 @@ Both support bulk selection. Try selecting multiple rows and using the bulk tool
},
};

/**
* Confirm-button color controlled by `confirmTone`:
* - `destructive` (default) — red button for delete/remove
* - `positive` — green button for approve/publish
* - `default` — neutral primary button
*/
export const ConfirmActionTones: StoryFn = () => {
const [view, setView] = useState<DataViewState>(createDefaultView());
const [selection, setSelection] = useState<string[]>([]);
const [users, setUsers] = useState(allUsers);

const paginatedData = paginateData(users, view);

const tonedActions: DataViewAction<User>[] = [
{
id: "approve",
label: "Approve",
icon: <CheckCircle size={16} />,
isDestructive: true,
confirmTone: "positive",
confirmTitle: "Approve users?",
confirmMessage: "Approved users gain access immediately.",
confirmButtonLabel: "Approve",
supportsBulk: true,
callback: async (items) => {
await new Promise((r) => setTimeout(r, 1000));
setUsers((prev) =>
prev.map((u) =>
items.some((i) => i.id === u.id) ? { ...u, status: "active" } : u
)
);
setSelection([]);
},
},
{
id: "mark-reviewed",
label: "Mark Reviewed",
icon: <ShieldCheck size={16} />,
isDestructive: true,
confirmTone: "default",
confirmTitle: "Mark as reviewed?",
confirmMessage: "Logs the review timestamp. Reversible.",
confirmButtonLabel: "Mark Reviewed",
supportsBulk: true,
callback: async (items) => {
await new Promise((r) => setTimeout(r, 800));
alert(`Marked reviewed: ${items.map((i) => i.name).join(", ")}`);
},
},
{
id: "delete",
label: "Delete",
icon: <Trash2 size={16} />,
isDestructive: true,
supportsBulk: true,
callback: async (items) => {
await new Promise((r) => setTimeout(r, 1000));
const ids = new Set(items.map((i) => i.id));
setUsers((prev) => prev.filter((u) => !ids.has(u.id)));
setSelection([]);
},
},
];

return (
<div className="p-4">
<DataViews<User>
namespace="dataviews-demo"
data={paginatedData}
fields={fields}
view={view}
onChangeView={setView}
actions={tonedActions}
selection={selection}
onChangeSelection={setSelection}
paginationInfo={{
totalItems: users.length,
totalPages: getTotalPages(users.length, view.perPage),
}}
getItemId={(item) => item.id}
/>
</div>
);
};
ConfirmActionTones.storyName = "Confirm Action Tones";
ConfirmActionTones.parameters = {
docs: {
description: {
story: `Set \`confirmTone\` on any \`isDestructive\` action to color the confirm button.

- **Approve** → \`confirmTone: "positive"\` (green / success)
- **Mark Reviewed** → \`confirmTone: "default"\` (neutral primary)
- **Delete** → omitted, defaults to \`"destructive"\` (red)

Useful for non-destructive actions that still need a confirmation step.`,
},
},
};

/** DataViews with dynamic filters. Click "Add Filter" to add filters. */
export const TabAndFilters: StoryFn = () => {
const [view, setView] = useState<DataViewState>(createDefaultView());
Expand Down
20 changes: 19 additions & 1 deletion src/components/wordpress/dataviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ function applyFiltersToTableElements<Item>(
}

// Extended action type with automatic destructive confirmation support
export type ConfirmActionTone = 'destructive' | 'positive' | 'default';

export type DestructiveActionConfig = {
/** When true, shows an AlertDialog confirmation before executing the action callback. */
isDestructive?: boolean;
/** Tone of the confirm button. Defaults to 'destructive'. Use 'positive' for approvals, 'default' for neutral confirmations. */
confirmTone?: ConfirmActionTone;
/** Custom title for the confirmation dialog. Defaults to the action label. */
confirmTitle?: string;
/** Custom message for the confirmation dialog. */
Expand All @@ -71,6 +75,15 @@ export type DestructiveActionConfig = {
cancelButtonLabel?: string;
};

const CONFIRM_TONE_VARIANT: Record<
ConfirmActionTone,
React.ComponentProps<typeof Button>['variant']
> = {
destructive: 'destructive',
positive: 'success',
default: 'default'
};

// Re-export types from @wordpress/dataviews with prefixed names to avoid conflicts
export type DataViewAction<Item> = Action<Item> & DestructiveActionConfig;
export type { Field as DataViewField, SupportedLayouts as DataViewLayouts, View as DataViewState };
Expand Down Expand Up @@ -1017,7 +1030,12 @@ export function DataViews<Item>(props: DataViewsProps<Item>) {
__('Cancel', 'default')}
</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
variant={
CONFIRM_TONE_VARIANT[
pendingDestructiveAction.action.confirmTone ??
'destructive'
]
}
onClick={handleDestructiveConfirm}
disabled={isConfirming}>
{isConfirming && <Spinner className="mr-2" />}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,9 @@ export {
type CurrencyInputProps,
type CurrencyOption,
type CheckboxProps,
type ConfirmActionTone,
type DataViewAction,
type DestructiveActionConfig,
type DataViewField,
type DataViewFilterField,
type DataViewFilterProps,
Expand Down
4 changes: 3 additions & 1 deletion src/providers/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export interface ThemeContextValue {
tokens: ThemeTokens;
resolvedMode: "light" | "dark";
cssVariables: Record<string, string>;
className?: string;
}

const ThemeContext = createContext<ThemeContextValue | null>(null);
Expand Down Expand Up @@ -497,8 +498,9 @@ export function ThemeProvider({
tokens: mergedTokens,
resolvedMode,
cssVariables,
className,
}),
[pluginId, mode, setMode, mergedTokens, resolvedMode, cssVariables],
[pluginId, mode, setMode, mergedTokens, resolvedMode, cssVariables, className],
);

return (
Expand Down
5 changes: 5 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ module.exports = {
...defaultConfig,
entry: {
index: './src/index.ts',
'components/ui/index': './src/components/ui/index.ts',
'components/settings/index': './src/components/settings/index.tsx',
'providers/index': './src/providers/index.ts',
'themes/index': './src/themes/index.ts',
'lib/utils': './src/lib/utils.ts',
},
output: {
...defaultConfig.output,
Expand Down
Loading