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 ( + + ); + } ) } + + { 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; }