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..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 @@ -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_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 142f2dd85955c1..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`. * @@ -49,6 +56,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 {@see self::PRESENTATION_VALUES} (first entry is the + * default). 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, ) ); } diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index 0beb8f82267658..0ae6c549245942 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -1952,7 +1952,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 ) => { @@ -1973,6 +1973,7 @@ function collectWidgets() { dirName: widgetName, hasRender: widgetFiles.hasRender, hasWidget: widgetFiles.hasWidget, + presentation: metadata.presentation ?? null, }, ]; } ); @@ -1998,11 +1999,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. */ /** 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 + ) } diff --git a/routes/dashboard/widget-dashboard/types.ts b/routes/dashboard/widget-dashboard/types.ts index 763403f2dd3c39..ca5d404bb50a82 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,139 +17,21 @@ import type { ComponentType, ReactNode } from 'react'; /** * WordPress dependencies */ -import type { IconType } from '@wordpress/components'; -import type { Field } from '@wordpress/dataviews'; import type { DashboardGridLayoutItem, DashboardLanesLayoutItem, } 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. - */ -export type WidgetName = `${ string }/${ string }`; - /** - * 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. + * Internal dependencies */ -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; - - /** - * 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 >; - }; -} +import type { + WidgetName, + WidgetTypeMetadata, + WidgetType, +} from '../widget-types/types'; -/** - * 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' >; export type MasonryTilePlacement = Omit< DashboardLanesLayoutItem, 'key' >; diff --git a/routes/dashboard/widget-types/hooks/use-widget-types.ts b/routes/dashboard/widget-types/hooks/use-widget-types.ts index 9970c8d1961085..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,6 +39,7 @@ interface WidgetModuleRecord { name: string; render_module?: string | null; widget_module?: string | null; + presentation?: WidgetTypeMetadata[ 'presentation' ] | 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; diff --git a/routes/dashboard/widget-types/types.ts b/routes/dashboard/widget-types/types.ts index 065201f2d70bd4..cc4ed681bf916c 100644 --- a/routes/dashboard/widget-types/types.ts +++ b/routes/dashboard/widget-types/types.ts @@ -68,6 +68,17 @@ export interface WidgetTypeMetadata< Item = unknown > { */ 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. */ @@ -78,6 +89,11 @@ export interface WidgetTypeMetadata< Item = unknown > { */ version?: string; + /** + * Gettext text domain for translations. + */ + textdomain?: string; + /** * Experiment gate; boolean `true`, or a specific experiment name. */ 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" }