From 773f2d369aea3a8260ac74e63ad4675be203f6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 11 May 2026 12:19:23 -0300 Subject: [PATCH 1/8] add presentation field to widget type metadata --- routes/dashboard/widget-dashboard/types.ts | 11 +++++++++++ routes/dashboard/widget-types/types.ts | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/routes/dashboard/widget-dashboard/types.ts b/routes/dashboard/widget-dashboard/types.ts index a3f5913f6d831b..65676e47674c62 100644 --- a/routes/dashboard/widget-dashboard/types.ts +++ b/routes/dashboard/widget-dashboard/types.ts @@ -82,6 +82,17 @@ export interface WidgetTypeMetadata { */ category?: string; + /** + * Authoring intent about how the widget wants to render. Static + * and declarative; not a user-editable attribute. + * + * - `'framed'` (default when absent): the widget renders its + * content only. + * - `'full-bleed'`: the widget renders edge-to-edge with no + * surrounding chrome. + */ + presentation?: 'framed' | 'full-bleed'; + /** * Search aliases used to surface the widget from the inserter. */ diff --git a/routes/dashboard/widget-types/types.ts b/routes/dashboard/widget-types/types.ts index bbec6e2086fa2f..fb12db81a066ee 100644 --- a/routes/dashboard/widget-types/types.ts +++ b/routes/dashboard/widget-types/types.ts @@ -64,6 +64,17 @@ export interface WidgetTypeMetadata { */ category?: string; + /** + * Authoring intent about how the widget wants to render. Static + * and declarative; not a user-editable attribute. + * + * - `'framed'` (default when absent): the widget renders its + * content only. + * - `'full-bleed'`: the widget renders edge-to-edge with no + * surrounding chrome. + */ + presentation?: 'framed' | 'full-bleed'; + /** * Search aliases used to surface the widget from the inserter. */ From 48c77ce58892c2af35f6ae3af3096cc95c83b683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 11 May 2026 12:19:35 -0300 Subject: [PATCH 2/8] expose presentation through php widget registry --- .../class-wp-rest-widget-modules-controller.php | 16 ++++++++++++++++ .../dashboard-widgets/class-wp-widget-type.php | 11 +++++++++++ .../dashboard-widgets/widget-types.php | 1 + 3 files changed, 28 insertions(+) diff --git a/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php b/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php index af0954d9f6388c..852aa8001ddb92 100644 --- a/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php +++ b/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php @@ -182,13 +182,19 @@ public function prepare_item_for_response( $item, $request ) { if ( rest_is_field_included( 'name', $fields ) ) { $data['name'] = $widget_type->name; } + if ( rest_is_field_included( 'render_module', $fields ) ) { $data['render_module'] = $widget_type->render_module; } + if ( rest_is_field_included( 'widget_module', $fields ) ) { $data['widget_module'] = $widget_type->widget_module; } + if ( rest_is_field_included( 'presentation', $fields ) ) { + $data['presentation'] = $widget_type->presentation; + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -217,18 +223,28 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), + 'render_module' => array( 'description' => __( 'Script-module handle for the widget render entry point.', 'gutenberg' ), 'type' => array( 'string', 'null' ), 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), + 'widget_module' => array( 'description' => __( 'Script-module handle for the widget metadata entry point.', 'gutenberg' ), 'type' => array( 'string', 'null' ), 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), + + 'presentation' => array( + 'description' => __( 'Authoring intent about how the widget wants to render.', 'gutenberg' ), + 'type' => array( 'string', 'null' ), + 'enum' => array( 'framed', 'full-bleed', null ), + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), ), ); diff --git a/lib/experimental/dashboard-widgets/class-wp-widget-type.php b/lib/experimental/dashboard-widgets/class-wp-widget-type.php index 142f2dd85955c1..586e75e9f14a61 100644 --- a/lib/experimental/dashboard-widgets/class-wp-widget-type.php +++ b/lib/experimental/dashboard-widgets/class-wp-widget-type.php @@ -49,6 +49,17 @@ class WP_Widget_Type { */ public $widget_module = null; + /** + * Authoring intent about how the widget wants to render. Static + * and declarative; not a user-editable attribute. + * + * One of `'framed'` (default) or `'full-bleed'`. Null when the + * widget did not declare the field. + * + * @var string|null + */ + public $presentation = null; + /** * Constructor. * diff --git a/lib/experimental/dashboard-widgets/widget-types.php b/lib/experimental/dashboard-widgets/widget-types.php index f7dea34843de6c..ad3b50a381923d 100644 --- a/lib/experimental/dashboard-widgets/widget-types.php +++ b/lib/experimental/dashboard-widgets/widget-types.php @@ -41,6 +41,7 @@ function gutenberg_register_widget_types() { array( 'render_module' => $widget['render_module'] ?? null, 'widget_module' => $widget['widget_module'] ?? null, + 'presentation' => $widget['presentation'] ?? null, ) ); } From 0efdb2e177ca30555f2d18469c0013887863f0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 11 May 2026 12:19:44 -0300 Subject: [PATCH 3/8] pass presentation through wp-build manifest --- packages/wp-build/lib/build.mjs | 15 ++++++++++----- packages/wp-build/lib/widget-utils.mjs | 9 +++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index 39e746630bec46..10488b9e45d9fa 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -1928,7 +1928,7 @@ async function buildAllWidgets() { * Discover all widgets and collect their registry-facing data. * Widgets without a valid widget.json are skipped. * - * @return {Array<{ name: string, dirName: string, hasRender: boolean, hasWidget: boolean }>} Array of widget objects. + * @return {Array<{ name: string, dirName: string, hasRender: boolean, hasWidget: boolean, presentation: string | null }>} Array of widget objects. */ function collectWidgets() { return getAllWidgets( ROOT_DIR ).flatMap( ( widgetName ) => { @@ -1949,6 +1949,7 @@ function collectWidgets() { dirName: widgetName, hasRender: widgetFiles.hasRender, hasWidget: widgetFiles.hasWidget, + presentation: metadata.presentation ?? null, }, ]; } ); @@ -1974,11 +1975,15 @@ async function generateWidgetRegistry( widgets, replacements ) { .map( ( widget ) => { const hasRenderStr = widget.hasRender ? 'true' : 'false'; const hasWidgetStr = widget.hasWidget ? 'true' : 'false'; + const presentationStr = widget.presentation + ? `'${ widget.presentation }'` + : 'null'; return `\tarray( - 'name' => '${ widget.name }', - 'dir_name' => '${ widget.dirName }', - 'has_render' => ${ hasRenderStr }, - 'has_widget' => ${ hasWidgetStr }, + 'name' => '${ widget.name }', + 'dir_name' => '${ widget.dirName }', + 'has_render' => ${ hasRenderStr }, + 'has_widget' => ${ hasWidgetStr }, + 'presentation' => ${ presentationStr }, )`; } ) .join( ',\n' ); diff --git a/packages/wp-build/lib/widget-utils.mjs b/packages/wp-build/lib/widget-utils.mjs index 998eaee554ab3e..364a27932bf39c 100644 --- a/packages/wp-build/lib/widget-utils.mjs +++ b/packages/wp-build/lib/widget-utils.mjs @@ -25,10 +25,11 @@ export function getAllWidgets( rootDir ) { /** * @typedef {Object} WidgetMetadata - * @property {string} name Widget namespaced identifier. - * @property {string} [title] Human-readable title. - * @property {string} [description] Short description. - * @property {string} [category] Grouping category. + * @property {string} name Widget namespaced identifier. + * @property {string} [title] Human-readable title. + * @property {string} [description] Short description. + * @property {string} [category] Grouping category. + * @property {'framed' | 'full-bleed'} [presentation] Authoring intent about how the widget wants to render. */ /** From 9e02c75c1c25938c0c426e237099310609e8b084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 11 May 2026 12:19:54 -0300 Subject: [PATCH 4/8] map presentation in widget-types resolver --- routes/dashboard/widget-types/hooks/use-widget-types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routes/dashboard/widget-types/hooks/use-widget-types.ts b/routes/dashboard/widget-types/hooks/use-widget-types.ts index 9970c8d1961085..4b8a14ccdf5206 100644 --- a/routes/dashboard/widget-types/hooks/use-widget-types.ts +++ b/routes/dashboard/widget-types/hooks/use-widget-types.ts @@ -39,6 +39,7 @@ interface WidgetModuleRecord { name: string; render_module?: string | null; widget_module?: string | null; + presentation?: 'framed' | 'full-bleed' | null; } /** @@ -92,6 +93,9 @@ export function useWidgetTypes(): WidgetType[] { ...( module.default as Partial< WidgetType > ), name: record.name as WidgetName, renderModule: record.render_module ?? '', + ...( record.presentation + ? { presentation: record.presentation } + : {} ), } as WidgetType; } catch { return null; From 13fa997f996c902e354c19b5ad045cdbae5d6f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 11 May 2026 12:20:04 -0300 Subject: [PATCH 5/8] honor full-bleed presentation in widget chrome --- .../widget-chrome/widget-chrome.module.css | 18 +++++++++ .../widget-chrome/widget-chrome.tsx | 38 ++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.module.css b/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.module.css index 39d6b6d98c1920..bce449d18b1dbd 100644 --- a/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.module.css +++ b/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.module.css @@ -9,6 +9,24 @@ .widgetChromeContent { flex: 1; + height: 100%; + + /* + * Probably remove the following padding + * once #77856 lands. + * @see https://github.com/WordPress/gutenberg/pull/77856 + */ + padding-top: 0; + padding-bottom: 0; +} + +/** + * Full-bleed widgets are rendered in a full-bleed container. + * Probably remove the following CSS once #77586 lands. + * @see https://github.com/WordPress/gutenberg/pull/77586 + */ +.widgetChromeContentFullBleed { + height: 100%; } .loading { diff --git a/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.tsx b/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.tsx index e6eb5708eaab4c..98daa0c97cfc48 100644 --- a/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.tsx +++ b/routes/dashboard/widget-dashboard/components/widget-chrome/widget-chrome.tsx @@ -17,7 +17,7 @@ import { import { __ } from '@wordpress/i18n'; // Dashboard is still experimental. // eslint-disable-next-line @wordpress/use-recommended-components -import { Card, Stack, Notice } from '@wordpress/ui'; +import { Card, Stack, Notice, VisuallyHidden } from '@wordpress/ui'; /** * Internal dependencies @@ -126,6 +126,16 @@ export const WidgetChrome = forwardRef< HTMLDivElement, WidgetChromeProps >( return null; } + const isFullBleed = widgetType.presentation === 'full-bleed'; + const header =
; + const body = ( + + }> + + + + ); + return ( ( aria-labelledby={ widgetType.title ? titleId : undefined } { ...( editMode ? { inert: '' } : {} ) } > -
+ { isFullBleed ? ( + { header } + ) : ( + header + ) } + - - }> - - - + { isFullBleed ? ( + + { body } + + ) : ( + body + ) } From 4f31f490e0f7c7cb8140943f64576f901bfd9f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 11 May 2026 12:20:15 -0300 Subject: [PATCH 6/8] update hello-world to showcase full-bleed --- widgets/hello-world/render.tsx | 17 ++++++++++++++++- widgets/hello-world/widget.json | 3 ++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/widgets/hello-world/render.tsx b/widgets/hello-world/render.tsx index 9d5dcd6259e033..592cd452a7293f 100644 --- a/widgets/hello-world/render.tsx +++ b/widgets/hello-world/render.tsx @@ -1,3 +1,18 @@ +import { Stack, Text } from '@wordpress/ui'; + export default function HelloWorld() { - return
Hello World
; + return ( + + Hello World + + ); } diff --git a/widgets/hello-world/widget.json b/widgets/hello-world/widget.json index f94cc461931ff0..86df48a7a01ccc 100644 --- a/widgets/hello-world/widget.json +++ b/widgets/hello-world/widget.json @@ -2,5 +2,6 @@ "name": "core/hello-world", "title": "Hello World", "description": "A minimal example widget.", - "category": "demo" + "category": "demo", + "presentation": "full-bleed" } From 9ce6e35c23bc6bd5d6ca99b803d50a7dd6f687f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Tue, 12 May 2026 14:41:00 -0300 Subject: [PATCH 7/8] unify widget type identity in single source --- routes/dashboard/widget-dashboard/types.ts | 152 ++------------------- routes/dashboard/widget-types/types.ts | 5 + 2 files changed, 20 insertions(+), 137 deletions(-) diff --git a/routes/dashboard/widget-dashboard/types.ts b/routes/dashboard/widget-dashboard/types.ts index 65676e47674c62..812b763edb133b 100644 --- a/routes/dashboard/widget-dashboard/types.ts +++ b/routes/dashboard/widget-dashboard/types.ts @@ -1,5 +1,12 @@ /** - * Widget type definitions. + * Widget type definitions for the dashboard engine. + * + * The widget identity types (`WidgetName`, `WidgetTypeMetadata`, + * `WidgetType`) live in `routes/dashboard/widget-types/types` and are + * re-exported here so dashboard internals can pull every type they need + * from a single module. The local declarations below cover the + * dashboard-specific surface area: `DashboardWidget`, render props, + * module resolver, grid settings, and the `WidgetDashboard` prop bag. */ /** @@ -10,147 +17,18 @@ import type { ComponentType, ReactNode } from 'react'; /** * WordPress dependencies */ -import type { IconType } from '@wordpress/components'; -import type { Field } from '@wordpress/dataviews'; import type { DashboardGridLayoutItem } from '@wordpress/grid'; -/* - * MIGRATION: `WidgetName`, `WidgetTypeMetadata`, and `WidgetType` below - * are also defined in `@wordpress/widget-types` (currently on its own - * branch). When that package lands in trunk, replace the three - * declarations with: - * - * export type { - * WidgetName, - * WidgetTypeMetadata, - * WidgetType, - * } from '@wordpress/widget-types'; - * - * The shapes are kept identical on purpose so the swap is mechanical — - * any change to the fields here must land in lockstep on the - * `@wordpress/widget-types` package to keep the cutover trivial. - */ - /** - * Widget type identifier, structured as `/`. - * Both segments are lowercase, kebab-case; the full character pattern is - * enforced by the `widget.json` schema at authoring time. + * Internal dependencies */ -export type WidgetName = `${ string }/${ string }`; +import type { + WidgetName, + WidgetTypeMetadata, + WidgetType, +} from '../widget-types/types'; -/** - * Literal contents of a widget's `widget.json` metadata file. - * - * Captures the *authoring* shape only — module entry points and style - * assets are discovered by convention from the widget directory - * (`render.*`, `widget.*`, `render.scss`), not declared here. - * - * Consumed by tooling (IDE autocomplete, validation, the build pipeline). - * The dashboard engine consumes the richer `WidgetType` below, which - * extends this shape with runtime-only fields produced by the build - * manifest. - */ -export interface WidgetTypeMetadata { - /** - * Version of the Widget API used by the widget. - */ - apiVersion: number; - - /** - * Stable type identifier. See `WidgetName` for the shape. - */ - name: WidgetName; - - /** - * Display title; shown in the inserter. - */ - title: string; - - /** - * Short description shown in the widget inspector. - */ - description?: string; - - /** - * Visual identifier shown in the widget header; dashicon string, React node, or SVG component. - */ - icon?: IconType; - - /** - * Grouping category. Core provides `dashboard`; plugins and themes may - * register custom categories. - */ - category?: string; - - /** - * Authoring intent about how the widget wants to render. Static - * and declarative; not a user-editable attribute. - * - * - `'framed'` (default when absent): the widget renders its - * content only. - * - `'full-bleed'`: the widget renders edge-to-edge with no - * surrounding chrome. - */ - presentation?: 'framed' | 'full-bleed'; - - /** - * Search aliases used to surface the widget from the inserter. - */ - keywords?: string[]; - - /** - * Widget version — used for asset cache invalidation. - */ - version?: string; - - /** - * Gettext text domain for translations. - */ - textdomain?: string; - - /** - * Experiment gate — boolean `true`, or a specific experiment name. - */ - __experimental?: string | boolean; - - /** - * Declarative attribute schema. Surfaces render forms straight from - * this list via `DataForm`, with no per-widget form wiring. `any` is - * used here because the array is heterogeneous — each widget narrows - * `Item` to its own attribute type at the point of registration. - */ - attributes?: Field< any >[]; - - /** - * Structured example data for the Inspector Help Panel preview, and - * the default attributes applied by `createDashboardWidget` when no - * initial attributes are supplied. - */ - example?: { - attributes?: Record< string, unknown >; - }; -} - -/** - * Runtime widget type consumed by the dashboard engine. - * - * Extends `WidgetTypeMetadata` (the authoring shape of `widget.json`) with - * runtime-only fields produced by the build pipeline — notably - * `renderModule`, which maps each widget to its discovered script-module - * entry point. - * - * Surfaces consume `WidgetType[]` via the `widgetTypes` prop; the - * dashboard never reads the widget-types store directly. - */ -export interface WidgetType extends WidgetTypeMetadata { - /** - * Script-module identifier resolved to a React component at render - * time by `ResolveWidgetModule`. Produced by the build pipeline from - * the conventional `render.*` / `widget.*` entry points; not declared - * in `widget.json`. - */ - renderModule: string; -} +export type { WidgetName, WidgetTypeMetadata, WidgetType }; export type GridTilePlacement = Omit< DashboardGridLayoutItem, 'key' >; diff --git a/routes/dashboard/widget-types/types.ts b/routes/dashboard/widget-types/types.ts index fb12db81a066ee..380e87865ebe0a 100644 --- a/routes/dashboard/widget-types/types.ts +++ b/routes/dashboard/widget-types/types.ts @@ -85,6 +85,11 @@ export interface WidgetTypeMetadata { */ version?: string; + /** + * Gettext text domain for translations. + */ + textdomain?: string; + /** * Experiment gate — boolean `true`, or a specific experiment name. */ From 72a54fdfb1c19a66367fb294b3fcc195dc1a3d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Tue, 12 May 2026 14:41:10 -0300 Subject: [PATCH 8/8] dedupe presentation declarations per layer --- .../class-wp-rest-widget-modules-controller.php | 2 +- .../dashboard-widgets/class-wp-widget-type.php | 11 +++++++++-- .../dashboard/widget-types/hooks/use-widget-types.ts | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php b/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php index 852aa8001ddb92..a0a7d3dc154b8f 100644 --- a/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php +++ b/lib/experimental/dashboard-widgets/class-wp-rest-widget-modules-controller.php @@ -241,7 +241,7 @@ public function get_item_schema() { 'presentation' => array( 'description' => __( 'Authoring intent about how the widget wants to render.', 'gutenberg' ), 'type' => array( 'string', 'null' ), - 'enum' => array( 'framed', 'full-bleed', null ), + 'enum' => array_merge( WP_Widget_Type::PRESENTATION_VALUES, array( null ) ), 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), diff --git a/lib/experimental/dashboard-widgets/class-wp-widget-type.php b/lib/experimental/dashboard-widgets/class-wp-widget-type.php index 586e75e9f14a61..df2f8aa2e66a40 100644 --- a/lib/experimental/dashboard-widgets/class-wp-widget-type.php +++ b/lib/experimental/dashboard-widgets/class-wp-widget-type.php @@ -22,6 +22,13 @@ #[AllowDynamicProperties] class WP_Widget_Type { + /** + * Allowed values for the `presentation` field. Treated as the + * single source of truth across the registry, REST schema, and + * any consumer that needs to validate or enumerate the set. + */ + const PRESENTATION_VALUES = array( 'framed', 'full-bleed' ); + /** * Widget type key. Namespaced identifier, e.g. `core/hello-world`. * @@ -53,8 +60,8 @@ class WP_Widget_Type { * Authoring intent about how the widget wants to render. Static * and declarative; not a user-editable attribute. * - * One of `'framed'` (default) or `'full-bleed'`. Null when the - * widget did not declare the field. + * One of {@see self::PRESENTATION_VALUES} (first entry is the + * default). Null when the widget did not declare the field. * * @var string|null */ diff --git a/routes/dashboard/widget-types/hooks/use-widget-types.ts b/routes/dashboard/widget-types/hooks/use-widget-types.ts index 4b8a14ccdf5206..2cd17fc4fe728a 100644 --- a/routes/dashboard/widget-types/hooks/use-widget-types.ts +++ b/routes/dashboard/widget-types/hooks/use-widget-types.ts @@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import type { WidgetName, WidgetType } from '../types'; +import type { WidgetName, WidgetType, WidgetTypeMetadata } from '../types'; /** * Registers the `widgetModule` core-data entity at module load. @@ -39,7 +39,7 @@ interface WidgetModuleRecord { name: string; render_module?: string | null; widget_module?: string | null; - presentation?: 'framed' | 'full-bleed' | null; + presentation?: WidgetTypeMetadata[ 'presentation' ] | null; } /**