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:
*
*
`). 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
? ''
: '