%3$s

%4$s', '' === $icon ? '' : '' . aafm_icon( $icon ) . '', '' === $title_id ? '' : ' id="' . esc_attr( $title_id ) . '"', esc_html( $title ), $after ); } /** * Echo a settings section wrapper. * * Both shapes share one head: a `.aafm-card-head` holding the icon, the title and an optional * pill or badge. The description never sits in the head; it opens the body as one small, muted * paragraph, so a long explanation cannot stretch the header. Non-collapsible (default) renders a * `
` with a `.aafm-card-pad aafm-section-body` body. * Collapsible renders a `
` whose * `` is the head, honoring `open` only when `collapsible` is true. * * STRUCTURAL ONLY: `body`/`pill` are echoed verbatim - the caller must have * escaped them. `title`/`description` are escaped here with esc_html. * * Recognised $args keys: `id` (wrapper id attribute), `class` (extra wrapper classes, * sanitized per class name), `title` (heading, escaped), `icon` (aafm_icon() key for the * head glyph), `description` (one muted paragraph that opens the body, escaped), `pill` (pre-built pill HTML, echoed * verbatim), `collapsible` (bool, default false), `open` (bool, default true, honored only * when collapsible), `badge` (count-badge text for the summary, escaped), and `body` * (pre-escaped inner HTML, echoed verbatim). * * @param array $args Section args (see the recognised keys above). * @return void */ function aafm_render_section( array $args ): void { $id = isset( $args['id'] ) ? (string) $args['id'] : ''; $extra_class = isset( $args['class'] ) ? (string) $args['class'] : ''; $title = isset( $args['title'] ) ? (string) $args['title'] : ''; $icon = isset( $args['icon'] ) ? (string) $args['icon'] : ''; $description = isset( $args['description'] ) ? (string) $args['description'] : ''; $pill = isset( $args['pill'] ) ? (string) $args['pill'] : ''; $collapsible = ! empty( $args['collapsible'] ); $open = ! isset( $args['open'] ) || ! empty( $args['open'] ); $badge = isset( $args['badge'] ) ? (string) $args['badge'] : ''; $body = isset( $args['body'] ) ? (string) $args['body'] : ''; $id_attr = '' === $id ? '' : sprintf( ' id="%s"', esc_attr( $id ) ); $extra = aafm_sanitize_html_classes( $extra_class ); $desc_html = '' === $description ? '' : sprintf( '

%s

', esc_html( $description ) ); $head = aafm_card_head_html( $icon, $title, '', $pill . ( '' === $badge ? '' : sprintf( ' %s', esc_html( $badge ) ) ) ); if ( $collapsible ) { echo wp_kses( sprintf( '
%4$s
%5$s%6$s
', esc_attr( $extra ), $id_attr, $open ? ' open' : '', $head, $desc_html, $body ), aafm_admin_allowed_html() ); return; } echo wp_kses( sprintf( '
%3$s
%4$s%5$s
', esc_attr( $extra ), $id_attr, $head, $desc_html, $body ), aafm_admin_allowed_html() ); } /** * Echo the toggle control for one ability row: a lock icon when a floor is holding the ability * down, or a checkbox otherwise. * * Shared by aafm_render_ability_row() (the Abilities tab) and * aafm_render_integration_ability_row() (the Integrations tab) so both surfaces render the same * lock treatment from one place. This exists because they didn't: the Integrations renderer never * learned about the lock at all and kept emitting a live checkbox for a locked ability, which is * exactly how a high-risk ability got enabled, saved, and logged from a card whose master switch * was off. * * A locked row emits NO at all, rather than a disabled one. The bulk enable-all controls * query `input[type="checkbox"][name="aafm_abilities[]"]` inside the card, so a greyed-out but * present checkbox would still be swept in by them. That is the 1.5.0 bug and it must not come back. * * STRUCTURAL ONLY, same convention as the rest of this file: `$name` and `$title_id` are escaped * here; the caller owns everything else about the row. * * @param string $name Ability name. * @param string $title_id Id of the row's

, referenced via aria-labelledby. * @param array $enabled Enabled ability names. * @param bool $host_disabled True when the row's host integration is inactive - an * unlocked checkbox then renders disabled so it never * submits. Ignored when the ability is locked, since a * locked row never renders a checkbox at all. * @return string|null The lock reason ('read_only' or 'high_risk') when the row is locked, so the * caller can render the matching note, or null when it is freely toggleable. */ function aafm_render_ability_toggle_control( string $name, string $title_id, array $enabled, bool $host_disabled = false ): ?string { $reason = aafm_ability_lock_reason( $name ); if ( null !== $reason ) { printf( '%2$s', esc_attr__( 'Locked', 'agent-abilities-for-mcp' ), wp_kses( aafm_icon( 'lock' ), aafm_svg_allowed_html() ) ); return $reason; } printf( '', esc_attr( $name ), esc_attr( $title_id ), $host_disabled ? '' : checked( in_array( $name, $enabled, true ), true, false ), $host_disabled ? ' disabled' : '' ); return null; } /** * Echo a labelled set-row: a `.aafm-set-label` (label text + optional `.opt` * sub-label) beside a `.aafm-set-control` (the pre-built control markup), with an * optional `.help` element below. * * STRUCTURAL ONLY: `control` and `pill` are echoed verbatim - the caller owns their * escaping. `label`/`opt`/`help` are escaped here with esc_html. * * Recognised $args keys: `label` (row label, escaped), `opt` (optional sub-label * rendered in a ``, escaped), `class` (extra wrapper classes, * sanitized per class name), `pill` (pre-built pill HTML for the label cell, echoed * verbatim), `control` (pre-built control HTML, echoed verbatim), and `help` (optional * help text in a `.help` element, escaped). * * @param array $args Row args (see the recognised keys above). * @return void */ function aafm_render_set_row( array $args ): void { $label = isset( $args['label'] ) ? (string) $args['label'] : ''; $opt = isset( $args['opt'] ) ? (string) $args['opt'] : ''; $extra_class = isset( $args['class'] ) ? (string) $args['class'] : ''; $pill = isset( $args['pill'] ) ? (string) $args['pill'] : ''; $control = isset( $args['control'] ) ? (string) $args['control'] : ''; $help = isset( $args['help'] ) ? (string) $args['help'] : ''; $extra = aafm_sanitize_html_classes( $extra_class ); $opt_html = '' === $opt ? '' : sprintf( ' %s', esc_html( $opt ) ); $help_html = '' === $help ? '' : sprintf( '

%s

', esc_html( $help ) ); echo wp_kses( sprintf( '
%2$s%3$s%4$s
%5$s%6$s
', esc_attr( $extra ), esc_html( $label ), $opt_html, $pill, $control, $help_html ), aafm_admin_allowed_html() ); } /** * Build a "See more" disclosure for the tail of a long set-row description. * * A native `
`/``, the same affordance the integration cards, the abilities * list, and the Help accordion already use - so it needs no JavaScript, survives a page with * scripts off, and gets its keyboard and screen-reader behaviour from the browser. Only the * three rows whose copy runs much longer than the rest of the tab use it; the point is to * keep the visual rhythm of the tab, not to hide anything, so nothing here is truncated - * every sentence stays in the markup either side of the fold. * * STRUCTURAL ONLY, same convention as the rest of this file: `$body` is echoed verbatim and * the caller owns its escaping. `$label` is the accessible name and IS escaped here. * * @param string $label Accessible name for the summary, e.g. "See more about read-only mode". * The visible text stays the short shared "See more". * @param string $body Pre-escaped HTML for the collapsed remainder. * @return string */ function aafm_get_set_more_html( string $label, string $body ): string { return sprintf( '
%2$s
%3$s
', esc_attr( $label ), esc_html__( 'See more', 'agent-abilities-for-mcp' ), $body ); } /** * Render the search box and All / Read Only / Write buttons shared by the integration cards and * the bridge directory. The caller prints the wrapping .aafm-integration-filter element. * * @param string $input_id Id of the search input, which its visually-hidden label points at. * @return void */ function aafm_render_filter_controls( string $input_id ): void { // Visually-hidden label keeps the search input accessible without adding visible chrome. printf( '', esc_attr( $input_id ), esc_html__( 'Search abilities', 'agent-abilities-for-mcp' ) ); printf( '', esc_attr( $input_id ), esc_attr__( 'Search abilities…', 'agent-abilities-for-mcp' ) ); // All / Read Only / Write toggle group. "All" starts selected. Each button is type="button". echo '
'; $risks = array( 'all' => __( 'All', 'agent-abilities-for-mcp' ), 'read' => __( 'Read Only', 'agent-abilities-for-mcp' ), 'write' => __( 'Write', 'agent-abilities-for-mcp' ), ); foreach ( $risks as $value => $label ) { printf( '', 'all' === $value ? ' is-active' : '', esc_attr( $value ), 'all' === $value ? 'true' : 'false', esc_html( $label ) ); } echo '
'; } /** * Render the head of a stat card: the card's opening element, its label and its icon. The caller * prints the value and any sub line, then closes the card. * * @param string $modifier Modifier class, e.g. 'aafm-stat-abilities'. * @param string $label Translated label, escaped here. * @param string $icon Icon name passed to aafm_icon(). * @return void */ function aafm_render_stat_head( string $modifier, string $label, string $icon ): void { echo '
'; echo '
'; echo '' . esc_html( $label ) . ''; echo ''; echo wp_kses( aafm_icon( $icon ), aafm_svg_allowed_html() ); echo ''; echo '
'; }