render_body( $server ); } // ============================================================ // Declarative body — the "extend + fields, get a rendered tab // for free" surface (D35 self-contained-subsystem pattern). // ============================================================ /** * Renders the tab's body. Concrete default = declarative dispatch to the * `get_*` overrides below. Non-`final` on purpose: subclasses whose body * doesn't fit the standard scaffold (form-heavy tabs like NpmTab / * DangerZoneTab / Overview) can still override this method directly and * emit whatever HTML they need. React-mount tabs (Embeds / Abilities / * Tools / AI Connectors) should prefer the declarative getters below. * * Standard scaffold order: * *
*

{@see get_heading()}

*

{@see get_description()}

* [optional] {@see render_disabled_server_notice()} when * {@see requires_enabled_server()} && ! $server['is_enabled'] * {@see render_content($server)} ← default renders React mount * {@see render_noscript_fallback($server)} ← default renders nothing *
* * @since 0.0.6 * @param array $server Server row data. * @return void */ protected function render_body( array $server ): void { echo '
'; $this->render_heading(); $this->render_description(); if ( $this->requires_enabled_server() && empty( $server['is_enabled'] ) ) { $this->render_disabled_server_notice(); } $this->render_content( $server ); $this->render_noscript_fallback( $server ); echo '
'; } /** * Tab-body heading (rendered as `

`). Defaults to `label()` so a * subclass that only declares slug/label gets a sensible heading for free. * Override when the tab title in the nav should differ from the body * heading (e.g. label "Embeds" vs heading "Frontend Embeds"). * * @since 0.1.11 * @return string Translated heading; empty string suppresses the `

`. */ public function get_heading(): string { return $this->label(); } /** * Optional one-liner description shown under the heading. Empty string * suppresses the `

`. * * @since 0.1.11 * @return string */ public function get_description(): string { return ''; } /** * DOM id of the React mount div. Empty string = no mount emitted (the * default; only React-mount tabs override). * * @since 0.1.11 * @return string */ public function get_react_root_id(): string { return ''; } /** * Data attributes to emit on the React mount `

`. Keys are the * attribute *suffix* — e.g. returning `['server-id' => 42]` emits * `data-server-id="42"`. Default: empty array (no data attrs; React * reads state from `wp_localize_script` bootstrap payload instead). * * @since 0.1.11 * @param array $server Server row data. * @return array */ public function get_react_root_data( array $server ): array { unset( $server ); // Subclasses override to derive from $server. return array(); } /** * Text shown inside the React mount before the app hydrates. Empty * string = no placeholder (mount `
` renders empty until React * takes over). * * @since 0.1.11 * @return string */ public function get_loading_placeholder(): string { return ''; } /** * When true and `! $server['is_enabled']`, the base emits a "server * disabled" warning notice above the content. Default false — enable * only for tabs whose contents depend on the server being on. * * @since 0.1.11 * @return bool */ public function requires_enabled_server(): bool { return false; } /** * Emits the standard `

` heading. Override * {@see get_heading()} to change the text. * * @since 0.1.11 * @return void */ protected function render_heading(): void { $heading = $this->get_heading(); if ( '' === $heading ) { return; } printf( '

%s

', esc_html( $heading ) ); } /** * Emits the standard `

`. Override * {@see get_description()} to change the text. * * @since 0.1.11 * @return void */ protected function render_description(): void { $description = $this->get_description(); if ( '' === $description ) { return; } printf( '

%s

', esc_html( $description ) ); } /** * Default content — the React mount div, driven by * {@see get_react_root_id()}, {@see get_react_root_data()}, and * {@see get_loading_placeholder()}. Emits nothing when * `get_react_root_id()` returns empty (the default). Non-React tabs can * override this method directly to emit form / table / custom markup * without touching `render_body()`. * * @since 0.1.11 * @param array $server Server row data. * @return void */ protected function render_content( array $server ): void { $root_id = $this->get_react_root_id(); if ( '' === $root_id ) { return; } $attrs = ''; foreach ( $this->get_react_root_data( $server ) as $key => $value ) { $attrs .= sprintf( ' data-%s="%s"', esc_attr( (string) $key ), esc_attr( (string) $value ) ); } $placeholder = $this->get_loading_placeholder(); $inner = '' === $placeholder ? '' : '

' . esc_html( $placeholder ) . '

'; printf( '
%s
', esc_attr( $root_id ), $attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled from pre-escaped fragments above. $inner // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Assembled from pre-escaped fragments above. ); } /** * Optional `