diff --git a/package-lock.json b/package-lock.json
index 440df02b7068c5..feec1d42eccf53 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -64380,6 +64380,7 @@
"@wordpress/icons": "file:../../packages/icons",
"@wordpress/notices": "file:../../packages/notices",
"@wordpress/preferences": "file:../../packages/preferences",
+ "@wordpress/primitives": "file:../../packages/primitives",
"@wordpress/private-apis": "file:../../packages/private-apis",
"@wordpress/ui": "file:../../packages/ui",
"@wordpress/warning": "file:../../packages/warning",
diff --git a/packages/grid/src/dashboard-grid/index.tsx b/packages/grid/src/dashboard-grid/index.tsx
index 1ae8eeff67e909..7313db6e0e18e9 100644
--- a/packages/grid/src/dashboard-grid/index.tsx
+++ b/packages/grid/src/dashboard-grid/index.tsx
@@ -48,6 +48,12 @@ import styles from './grid.module.css';
// effect overwrites this with the actual computed value.
const FALLBACK_GAP_PX = 12;
+// Default column cap when no explicit `columns` or `minColumnWidth` is
+// supplied. Layered semantics: `columns` acts as a cap and
+// `minColumnWidth` as a per-tile floor; if neither is set we still
+// need a finite count to render against.
+const DEFAULT_COLUMNS = 6;
+
// Reorder is driven by `temporaryLayout` + CSS Grid, not by dnd-kit
// transforms. Hoist the no-op strategy outside the component so its
// reference is stable across renders — passing a fresh `() => null`
@@ -91,7 +97,7 @@ export const DashboardGrid = forwardRef< HTMLDivElement, DashboardGridProps >(
function DashboardGrid( props, ref ) {
const {
layout,
- columns = 6,
+ columns,
children,
className,
style,
@@ -172,14 +178,15 @@ export const DashboardGrid = forwardRef< HTMLDivElement, DashboardGridProps >(
}, [] );
const effectiveColumns = useMemo( () => {
if ( ! minColumnWidth ) {
- return columns;
+ return columns ?? DEFAULT_COLUMNS;
}
const totalWidthPerColumn = minColumnWidth + gapPx;
- const maxColumns = Math.floor(
- ( containerWidth + gapPx ) / totalWidthPerColumn
+ const maxFit = Math.max(
+ 1,
+ Math.floor( ( containerWidth + gapPx ) / totalWidthPerColumn )
);
- return Math.max( 1, maxColumns );
+ return columns !== undefined ? Math.min( columns, maxFit ) : maxFit;
}, [ minColumnWidth, gapPx, containerWidth, columns ] );
const columnWidth =
( containerWidth - ( effectiveColumns - 1 ) * gapPx ) /
diff --git a/packages/grid/src/dashboard-grid/stories/index.story.tsx b/packages/grid/src/dashboard-grid/stories/index.story.tsx
index 33e8a39f245124..fc8898114d8426 100644
--- a/packages/grid/src/dashboard-grid/stories/index.story.tsx
+++ b/packages/grid/src/dashboard-grid/stories/index.story.tsx
@@ -331,6 +331,50 @@ export const Responsive: Story = {
},
};
+/**
+ * Layered configuration: `columns` caps the count and
+ * `minColumnWidth` enforces a per-tile width floor. The grid renders
+ * up to `columns` columns on wide containers and reduces the count
+ * on narrow ones whenever fitting all of them would push tiles
+ * below `minColumnWidth`. Resize the preview to see the cap apply
+ * on wide widths and the floor reduce the count on narrow widths.
+ */
+export const Layered: Story = {
+ args: {
+ layout: [
+ { key: 'a', width: 1, order: 1 },
+ { key: 'b', width: 2, order: 2 },
+ { key: 'c', width: 2, order: 3 },
+ { key: 'd', width: 1, order: 4 },
+ { key: 'e', width: 2, order: 5 },
+ { key: 'f', width: 2, order: 6 },
+ ],
+ rowHeight: 96,
+ columns: 6,
+ minColumnWidth: 240,
+ children: [
+
+ width: 1
+ ,
+
+ width: 2
+ ,
+
+ width: 2
+ ,
+
+ width: 1
+ ,
+
+ width: 2
+ ,
+
+ width: 2
+ ,
+ ],
+ },
+};
+
/**
* A `width: 'fill'` item expands to cover the remaining columns in
* its row. Mix it with fixed-width items on either side to build
diff --git a/packages/grid/src/dashboard-grid/types.ts b/packages/grid/src/dashboard-grid/types.ts
index 6f98e54b0f243c..19e574e4dcd236 100644
--- a/packages/grid/src/dashboard-grid/types.ts
+++ b/packages/grid/src/dashboard-grid/types.ts
@@ -115,11 +115,19 @@ export type GridItemProps = {
};
/**
- * Props shared by fixed and responsive DashboardGrid variants. Extends
- * the standard div props so consumers can pass `id`, `aria-*`, `data-*`,
- * event handlers, etc., directly on the grid root.
+ * Props for `DashboardGrid`. Extends the standard div props so consumers
+ * can pass `id`, `aria-*`, `data-*`, event handlers, etc., directly on
+ * the grid root.
+ *
+ * `columns` and `minColumnWidth` compose as a layered model:
+ * - `columns` alone: fixed N columns; each tile scales with the container.
+ * - `minColumnWidth` alone: column count derives from container width,
+ * floored by the per-tile minimum, down to 1 column.
+ * - Both together: `columns` caps the count, `minColumnWidth` enforces a
+ * per-tile width floor that can reduce the count below the cap on
+ * narrow containers ("up to N columns, but never narrower than W px").
*/
-interface BaseDashboardGridProps
+export interface DashboardGridProps
extends Omit<
React.ComponentPropsWithoutRef< 'div' >,
'children' | 'className' | 'style'
@@ -215,30 +223,25 @@ interface BaseDashboardGridProps
* the package's default visual is used.
*/
renderGridOverlay?: React.ComponentType< GridOverlayRenderProps >;
-}
-interface FixedDashboardGridProps extends BaseDashboardGridProps {
/**
- * Total number of columns in the grid.
+ * Target column count (cap). When set alone, the grid renders this
+ * many columns and tiles scale with the container.
*
- * @default 6
+ * Composes with `minColumnWidth`: if both are set, the effective
+ * column count is `min( columns, fitsAtMinWidth )`. When omitted
+ * but `minColumnWidth` is set, the count is uncapped and derives
+ * purely from the container width. When both are omitted, the
+ * grid renders six columns.
*/
- columns: number;
-
- minColumnWidth?: never;
-}
+ columns?: number;
-interface ResponsiveDashboardGridProps extends BaseDashboardGridProps {
/**
- * Minimum width in pixels per column. Enables responsive mode:
- * the column count is derived from container width, down to a
- * minimum of 1 column. Mutually exclusive with `columns`.
+ * Per-tile minimum width in pixels. The effective column count is
+ * derived from container width, floored by this value, down to 1.
+ *
+ * Composes with `columns`: when both are set, this acts as a floor
+ * that can reduce the count below `columns` on narrow containers.
*/
minColumnWidth?: number;
-
- columns?: never;
}
-
-export type DashboardGridProps =
- | FixedDashboardGridProps
- | ResponsiveDashboardGridProps;
diff --git a/packages/grid/src/dashboard-lanes/index.tsx b/packages/grid/src/dashboard-lanes/index.tsx
index e0b45323760d9d..66439d07746782 100644
--- a/packages/grid/src/dashboard-lanes/index.tsx
+++ b/packages/grid/src/dashboard-lanes/index.tsx
@@ -48,6 +48,12 @@ import styles from './lanes.module.css';
// effect overwrites this with the actual computed value.
const FALLBACK_GAP_PX = 12;
+// Default lane cap when no explicit `columns` or `minColumnWidth` is
+// supplied. Layered semantics: `columns` acts as a cap and
+// `minColumnWidth` as a per-tile floor; if neither is set we still
+// need a finite count to render against.
+const DEFAULT_COLUMNS = 6;
+
const NO_SORT_STRATEGY = () => null;
/**
@@ -91,7 +97,7 @@ export const DashboardLanes = forwardRef< HTMLDivElement, DashboardLanesProps >(
function DashboardLanes( props, ref ) {
const {
layout,
- columns = 6,
+ columns,
children,
className,
style,
@@ -158,13 +164,14 @@ export const DashboardLanes = forwardRef< HTMLDivElement, DashboardLanesProps >(
}, [ container ] );
const effectiveColumns = useMemo( () => {
if ( ! minColumnWidth ) {
- return columns;
+ return columns ?? DEFAULT_COLUMNS;
}
const totalWidthPerColumn = minColumnWidth + gapPx;
- const maxColumns = Math.floor(
- ( containerWidth + gapPx ) / totalWidthPerColumn
+ const maxFit = Math.max(
+ 1,
+ Math.floor( ( containerWidth + gapPx ) / totalWidthPerColumn )
);
- return Math.max( 1, maxColumns );
+ return columns !== undefined ? Math.min( columns, maxFit ) : maxFit;
}, [ minColumnWidth, gapPx, containerWidth, columns ] );
const columnWidth =
( containerWidth - ( effectiveColumns - 1 ) * gapPx ) /
diff --git a/packages/grid/src/dashboard-lanes/stories/index.story.tsx b/packages/grid/src/dashboard-lanes/stories/index.story.tsx
index fa5fd760e90228..f3130175ab9add 100644
--- a/packages/grid/src/dashboard-lanes/stories/index.story.tsx
+++ b/packages/grid/src/dashboard-lanes/stories/index.story.tsx
@@ -219,6 +219,36 @@ export const Responsive: Story = {
},
};
+/**
+ * Layered configuration: `columns` caps the lane count and
+ * `minColumnWidth` enforces a per-tile width floor. The surface
+ * renders up to `columns` lanes on wide containers and reduces the
+ * count on narrow ones whenever fitting all of them would push
+ * tiles below `minColumnWidth`.
+ */
+export const Layered: Story = {
+ args: {
+ columns: 4,
+ minColumnWidth: 200,
+ layout: [
+ { key: 'a' },
+ { key: 'b' },
+ { key: 'c' },
+ { key: 'd' },
+ { key: 'e' },
+ { key: 'f' },
+ ],
+ children: [
+ ,
+ ,
+ ,
+ ,
+ ,
+ ,
+ ],
+ },
+};
+
/**
* Items with `width: 2` span two lanes. The skyline picks a span
* position that minimizes the resulting baseline across spanned
diff --git a/packages/grid/src/dashboard-lanes/types.ts b/packages/grid/src/dashboard-lanes/types.ts
index 11e51e7a3b5217..90670560e798a5 100644
--- a/packages/grid/src/dashboard-lanes/types.ts
+++ b/packages/grid/src/dashboard-lanes/types.ts
@@ -46,9 +46,16 @@ export type DashboardLanesLayoutItem = {
};
/**
- * Props shared by fixed and responsive `DashboardLanes` variants.
+ * Props for `DashboardLanes`.
+ *
+ * `columns` and `minColumnWidth` compose as a layered model:
+ * - `columns` alone: fixed N lanes; tiles scale with the container.
+ * - `minColumnWidth` alone: lane count derives from container width,
+ * floored by the per-tile minimum, down to 1.
+ * - Both together: `columns` caps the count, `minColumnWidth` enforces
+ * a per-tile width floor that can reduce the count below the cap.
*/
-interface BaseDashboardLanesProps
+export interface DashboardLanesProps
extends Omit<
React.ComponentPropsWithoutRef< 'div' >,
'children' | 'className' | 'style'
@@ -152,29 +159,25 @@ interface BaseDashboardLanesProps
* the package's default visual is used.
*/
renderGridOverlay?: React.ComponentType< GridOverlayRenderProps >;
-}
-interface FixedDashboardLanesProps extends BaseDashboardLanesProps {
/**
- * Total number of lanes in the surface.
+ * Target lane count (cap). When set alone, the surface renders
+ * this many lanes and tiles scale with the container.
*
- * @default 6
+ * Composes with `minColumnWidth`: if both are set, the effective
+ * lane count is `min( columns, fitsAtMinWidth )`. When omitted but
+ * `minColumnWidth` is set, the count is uncapped and derives purely
+ * from container width. When both are omitted, the surface
+ * renders six lanes.
*/
- columns: number;
-
- minColumnWidth?: never;
-}
+ columns?: number;
-interface ResponsiveDashboardLanesProps extends BaseDashboardLanesProps {
/**
- * Minimum width in pixels per lane. Enables responsive mode: the
- * lane count is derived from container width, down to 1.
+ * Per-tile minimum width in pixels. The effective lane count is
+ * derived from container width, floored by this value, down to 1.
+ *
+ * Composes with `columns`: when both are set, this acts as a floor
+ * that can reduce the count below `columns` on narrow containers.
*/
minColumnWidth?: number;
-
- columns?: never;
}
-
-export type DashboardLanesProps =
- | FixedDashboardLanesProps
- | ResponsiveDashboardLanesProps;
diff --git a/routes/dashboard/hooks/use-dashboard-grid-settings/use-dashboard-grid-settings.ts b/routes/dashboard/hooks/use-dashboard-grid-settings/use-dashboard-grid-settings.ts
index 98aaf70d8ff552..b15d150d3e792a 100644
--- a/routes/dashboard/hooks/use-dashboard-grid-settings/use-dashboard-grid-settings.ts
+++ b/routes/dashboard/hooks/use-dashboard-grid-settings/use-dashboard-grid-settings.ts
@@ -21,6 +21,7 @@ const KEY = 'dashboardGridSettings';
*/
const DEFAULT_GRID_SETTINGS: WidgetGridSettings = {
model: 'grid',
+ columns: 6,
minColumnWidth: 350,
rowHeight: 200,
};
diff --git a/routes/dashboard/package.json b/routes/dashboard/package.json
index ccd8b5921dff3f..2b9c4132cfbcf1 100644
--- a/routes/dashboard/package.json
+++ b/routes/dashboard/package.json
@@ -24,6 +24,7 @@
"@wordpress/icons": "file:../../packages/icons",
"@wordpress/notices": "file:../../packages/notices",
"@wordpress/preferences": "file:../../packages/preferences",
+ "@wordpress/primitives": "file:../../packages/primitives",
"@wordpress/private-apis": "file:../../packages/private-apis",
"@wordpress/ui": "file:../../packages/ui",
"@wordpress/warning": "file:../../packages/warning",
diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/index.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/index.tsx
new file mode 100644
index 00000000000000..66ada23daaf386
--- /dev/null
+++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/index.tsx
@@ -0,0 +1,103 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+
+/**
+ * WordPress dependencies
+ */
+import type { DataFormControlProps } from '@wordpress/dataviews';
+import { useCallback } from '@wordpress/element';
+/* eslint-disable @wordpress/use-recommended-components */
+import { Button, Fieldset, Stack, Text } from '@wordpress/ui';
+/* eslint-enable @wordpress/use-recommended-components */
+
+/**
+ * Internal dependencies
+ */
+import type { WidgetGridSettings } from '../../../types';
+import { GridThumbnail } from './thumbnail-grid';
+import { MasonryThumbnail } from './thumbnail-masonry';
+import styles from './style.module.css';
+
+function ModelThumbnail( {
+ model,
+ className,
+}: {
+ model: string;
+ className?: string;
+} ): React.ReactNode {
+ return model === 'masonry' ? (
+
+ ) : (
+
+ );
+}
+
+/**
+ * Card-style picker for the dashboard layout model. Renders each option
+ * (`grid`, `masonry`) as a visual card with an inline-SVG thumbnail and a
+ * label, exposed as a radio group for assistive tech.
+ *
+ * Used as the `Edit` control for the `model` field of the layout
+ * settings DataForm.
+ *
+ * @param props DataForm control props supplied by ` `.
+ * @param props.data Current form data for the dashboard grid settings.
+ * @param props.field Field descriptor with label, description, and options.
+ * @param props.onChange Commits an edit to the form's staging buffer.
+ * @return The model picker.
+ */
+export function LayoutModelEditField( {
+ data,
+ field,
+ onChange,
+}: DataFormControlProps< WidgetGridSettings > ): React.ReactNode {
+ const { getValue, setValue, elements = [] } = field;
+ const value = getValue( { item: data } );
+ const disabled = field.isDisabled( { item: data, field } );
+
+ const onSelect = useCallback(
+ ( nextValue: string | number | undefined ) =>
+ onChange( setValue( { item: data, value: nextValue } ) ),
+ [ data, onChange, setValue ]
+ );
+
+ return (
+
+ { field.label }
+
+ { elements.map( ( option ) => {
+ const isSelected = value === option.value;
+
+ return (
+ onSelect( option.value ) }
+ >
+
+
+ { option.label }
+
+
+ );
+ } ) }
+
+ { typeof field.description === 'string' && (
+
+ { field.description }
+
+ ) }
+
+ );
+}
diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/style.module.css b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/style.module.css
new file mode 100644
index 00000000000000..d920348e1c6de3
--- /dev/null
+++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/style.module.css
@@ -0,0 +1,34 @@
+.option {
+ flex: 1;
+ padding: var(--wpds-dimension-padding-md);
+ border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral);
+ border-radius: var(--wpds-border-radius-md);
+ background: transparent;
+ color: inherit;
+ cursor: var(--wpds-cursor-control);
+ text-align: center;
+ transition:
+ border-color var(--wpds-motion-duration-sm) var(--wpds-motion-easing-subtle),
+ background-color var(--wpds-motion-duration-sm) var(--wpds-motion-easing-subtle);
+}
+
+.option:hover {
+ border-color: var(--wpds-color-stroke-surface-neutral-strong);
+}
+
+.optionSelected,
+.optionSelected:hover {
+ border-color: var(--wpds-color-stroke-interactive-brand);
+ background-color: color-mix(in srgb, var(--wpds-color-bg-surface-brand) 6%, transparent);
+}
+
+.thumbnail {
+ display: block;
+ inline-size: 100%;
+ block-size: auto;
+ color: var(--wpds-color-fg-content-neutral-weak);
+}
+
+.optionSelected .thumbnail {
+ color: var(--wpds-color-fg-interactive-brand);
+}
diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/thumbnail-grid.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/thumbnail-grid.tsx
new file mode 100644
index 00000000000000..8018fd481b832d
--- /dev/null
+++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/thumbnail-grid.tsx
@@ -0,0 +1,28 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Rect } from '@wordpress/primitives';
+
+interface GridThumbnailProps {
+ className?: string;
+}
+
+export function GridThumbnail( {
+ className,
+}: GridThumbnailProps ): React.ReactNode {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/thumbnail-masonry.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/thumbnail-masonry.tsx
new file mode 100644
index 00000000000000..ca1239656b1089
--- /dev/null
+++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-model-edit-field/thumbnail-masonry.tsx
@@ -0,0 +1,28 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Rect } from '@wordpress/primitives';
+
+interface MasonryThumbnailProps {
+ className?: string;
+}
+
+export function MasonryThumbnail( {
+ className,
+}: MasonryThumbnailProps ): React.ReactNode {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx
index e794b8f708df73..73e0351f56e57f 100644
--- a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx
+++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx
@@ -6,9 +6,7 @@ import { DataForm } from '@wordpress/dataviews';
import type { DataFormControlProps, Field, Form } from '@wordpress/dataviews';
import { useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-/* eslint-disable @wordpress/use-recommended-components */
-import { Button, Drawer } from '@wordpress/ui';
-/* eslint-enable @wordpress/use-recommended-components */
+import { Button, Drawer } from '@wordpress/ui'; // eslint-disable-line @wordpress/use-recommended-components
/**
* Internal dependencies
@@ -20,6 +18,7 @@ import type {
WidgetGridModel,
WidgetGridSettings,
} from '../../types';
+import { LayoutModelEditField } from './layout-model-edit-field';
const DEFAULT_FIXED_COLUMNS = 6;
const DEFAULT_MIN_COLUMN_WIDTH = 350;
@@ -34,10 +33,6 @@ function isMasonry( item: WidgetGridSettings ): boolean {
return getModel( item ) === 'masonry';
}
-function isFixedColumns( item: WidgetGridSettings ): boolean {
- return item.columns !== undefined;
-}
-
function getRowHeight(
item: WidgetGridSettings
): WidgetGridLayoutSettings[ 'rowHeight' ] {
@@ -94,10 +89,10 @@ const fields: Field< WidgetGridSettings >[] = [
{
id: 'model',
type: 'text',
- Edit: 'select',
+ Edit: LayoutModelEditField,
label: __( 'Layout model' ),
description: __(
- 'Standard grid uses explicit widths and heights. Masonry packs items by content height.'
+ 'Grid keeps every tile the same height. Masonry lets tiles flow at their own height.'
),
elements: [
{ value: 'grid', label: __( 'Standard grid' ) },
@@ -105,42 +100,48 @@ const fields: Field< WidgetGridSettings >[] = [
],
getValue: ( { item } ) => getModel( item ),
},
- {
- id: 'fixedColumns',
- type: 'boolean',
- Edit: 'toggle',
- label: __( 'Fixed column count' ),
- getValue: ( { item } ) => isFixedColumns( item ),
- setValue: ( { item, value } ) =>
- value
- ? {
- columns: item.columns ?? DEFAULT_FIXED_COLUMNS,
- minColumnWidth: undefined,
- }
- : {
- columns: undefined,
- minColumnWidth:
- item.minColumnWidth ?? DEFAULT_MIN_COLUMN_WIDTH,
- },
- },
{
id: 'columns',
type: 'integer',
Edit: StepperIntegerEdit,
label: __( 'Columns' ),
+ description: __(
+ 'How many columns to show when the dashboard has enough space.'
+ ),
isValid: { min: 1, max: 12 },
- isVisible: ( item ) => isFixedColumns( item ),
+ },
+ {
+ id: 'adaptiveColumns',
+ type: 'boolean',
+ Edit: 'toggle',
+ label: __( 'Adjust on narrow screens' ),
+ description: __(
+ 'Show fewer columns when the dashboard gets too narrow to keep tiles readable.'
+ ),
+ getValue: ( { item } ) => item.minColumnWidth !== 0,
+ setValue: ( { item, value } ) => {
+ if ( ! value ) {
+ return { minColumnWidth: 0 };
+ }
+ const previous = item.minColumnWidth;
+ return {
+ minColumnWidth:
+ previous && previous > 0
+ ? previous
+ : DEFAULT_MIN_COLUMN_WIDTH,
+ };
+ },
},
{
id: 'minColumnWidth',
type: 'integer',
Edit: StepperIntegerEdit,
- label: __( 'Min column width (px)' ),
+ label: __( 'Minimum tile width' ),
description: __(
- 'Minimum width of each column. The number of columns adapts to the container width.'
+ 'The smallest tile width before a column is removed.'
),
- isValid: { min: 48, max: 1024 },
- isVisible: ( item ) => ! isFixedColumns( item ),
+ isValid: { min: 48, max: 600 },
+ isVisible: ( item ) => item.minColumnWidth !== 0,
},
{
id: 'autoRowHeight',
@@ -151,7 +152,7 @@ const fields: Field< WidgetGridSettings >[] = [
setValue: ( { value } ) => ( {
rowHeight: value ? ROW_HEIGHT_AUTO : DEFAULT_ROW_HEIGHT,
} ),
- isDisabled: ( { item } ) => isMasonry( item ),
+ isVisible: ( item ) => ! isMasonry( item ),
},
{
id: 'rowHeight',
@@ -173,8 +174,8 @@ const form: Form = {
layout: { type: 'regular', labelPosition: 'top' },
fields: [
'model',
- 'fixedColumns',
'columns',
+ 'adaptiveColumns',
'minColumnWidth',
'autoRowHeight',
'rowHeight',
diff --git a/routes/dashboard/widget-dashboard/components/widgets/widgets.tsx b/routes/dashboard/widget-dashboard/components/widgets/widgets.tsx
index bd7c54333a482d..28a534a36dc81d 100644
--- a/routes/dashboard/widget-dashboard/components/widgets/widgets.tsx
+++ b/routes/dashboard/widget-dashboard/components/widgets/widgets.tsx
@@ -29,6 +29,13 @@ import type {
WidgetName,
} from '../../types';
+// Floor applied as `minColumnWidth` on every surface render. Acts as a
+// safety net for stored settings that predate the layered model (where
+// `minColumnWidth` was XOR with `columns` and could be persisted as
+// `undefined`), and keeps tiles legible on narrow viewports without
+// requiring the consumer to wire the floor up themselves.
+const DASHBOARD_MIN_COLUMN_WIDTH = 350;
+
function toGridLayout( widgets: DashboardWidget[] ): DashboardGridLayoutItem[] {
return widgets.map( ( w ) => ( {
key: w.uuid,
@@ -149,54 +156,32 @@ export const Widgets = forwardRef< HTMLDivElement, WidgetsProps >(
WidgetResizeHandle as React.ComponentType< ResizeHandleRenderProps >,
};
- let surface: React.ReactNode;
- if ( isMasonry ) {
- surface =
- gridSettings.columns !== undefined ? (
-
- { children }
-
- ) : (
-
- { children }
-
- );
- } else {
- surface =
- gridSettings.columns !== undefined ? (
-
- { children }
-
- ) : (
-
- { children }
-
- );
- }
+ const minColumnWidth =
+ gridSettings.minColumnWidth ?? DASHBOARD_MIN_COLUMN_WIDTH;
+
+ const surface: React.ReactNode = isMasonry ? (
+
+ { children }
+
+ ) : (
+
+ { children }
+
+ );
return (
diff --git a/routes/dashboard/widget-dashboard/context/dashboard-context.tsx b/routes/dashboard/widget-dashboard/context/dashboard-context.tsx
index cf0eb25b428f22..34555afcc59dd3 100644
--- a/routes/dashboard/widget-dashboard/context/dashboard-context.tsx
+++ b/routes/dashboard/widget-dashboard/context/dashboard-context.tsx
@@ -31,8 +31,13 @@ import type {
* `gridSettings` entirely; if `gridSettings` is provided, the consumer's
* shape passes through untouched and missing fields fall back to whatever
* defaults the grid model itself supplies.
+ *
+ * `widgets.tsx` also applies a hard-coded floor when `minColumnWidth`
+ * resolves to `undefined`, to keep legibility intact for stored settings
+ * that predate the layered model.
*/
const DEFAULT_GRID: WidgetGridSettings = {
+ columns: 6,
minColumnWidth: 350,
rowHeight: 200,
};
diff --git a/routes/dashboard/widget-dashboard/types.ts b/routes/dashboard/widget-dashboard/types.ts
index 1d4cf65fa96622..763403f2dd3c39 100644
--- a/routes/dashboard/widget-dashboard/types.ts
+++ b/routes/dashboard/widget-dashboard/types.ts
@@ -266,9 +266,9 @@ export type WidgetGridModel = 'grid' | 'masonry';
/**
* Settings common to every grid model. `columns` and `minColumnWidth`
- * are mutually exclusive at runtime; the underlying grid component
- * handles the conflict so the dashboard does not enforce the xor at
- * the type level (keeps `react-docgen-typescript` serialization clean).
+ * compose as a layered model at runtime: `columns` caps the count and
+ * `minColumnWidth` enforces a per-tile width floor that can reduce the
+ * count on narrow containers. See `@wordpress/grid` for the resolution.
*
* `spacing` is intentionally absent: the gap between tiles is
* presentational and lives with the design-system theme/density, not
@@ -277,13 +277,15 @@ export type WidgetGridModel = 'grid' | 'masonry';
*/
interface BaseWidgetGridSettings {
/**
- * Fixed column count. Mutually exclusive with `minColumnWidth`.
+ * Target column count (cap). When omitted alongside
+ * `minColumnWidth`, the grid renders six columns.
*/
columns?: number;
/**
- * Responsive minimum column width in pixels. Mutually exclusive
- * with `columns`.
+ * Per-tile minimum width in pixels. Acts as a floor that can
+ * reduce the effective column count below `columns` on narrow
+ * containers.
*/
minColumnWidth?: number;
}