current_product() ) { return ''; } $entry = $registry->visible( null )[ $id ] ?? null; return null === $entry ? '' : \Rooster\Admin\Chrome::pin_button( $entry ); } } if ( ! function_exists( 'rooster_admin_card_open_html' ) ) { /** * The opening of a card in the chrome's look (the one card pattern): the section, the header with the icon, the * title, an optional meta chip and the entry's pin, and the body's opening with an optional description. The * entry id gives the card its section colour and its pin; '' for a card without an entry. * * @param string $id Catalogue entry id of the card ('' for none). * @param string $title Title (raw). * @param array $opts dom_id, description (raw), icon (Lucide name), class, meta (escaped HTML * beside the title, such as a `` chip); since 0.47.0: tools (escaped * HTML at the head's end: buttons, a switch), pin (escaped HTML in place of the * entry's pin; false for none), level (3: an h3), attrs (more attributes on * the section: a product's data hooks, `hidden` for a card that starts hidden; * an option that sets the same attribute wins), flush (the body without padding: a * table), hosted (another plugin's output in the body), body_class; since * 0.58.0, its place on a page board (Board, read back from data attributes): * col (1 to 3: its default column), span ('all': it needs the board's width and * stands as a band of its own), min (rem: the width under which the board goes * to one column rather than squeeze it), movable (false: it keeps its place), * and essential (its description stays in the Compact guidance: a legal duty, * data loss, money; UI-Final-Review FR5); since 0.64.3, dismissible (true, * with a dom_id: the dismiss button at the head's end; a person who dismissed * it gets it printed hidden; Reset layout brings it back). */ function rooster_admin_card_open_html( string $id, string $title, array $opts = array() ): string { $registry = \Rooster\Admin\Library::registry(); $entry = null !== $registry && '' !== $id ? $registry->get( $id ) : null; $dom = (string) ( $opts['dom_id'] ?? ( '' !== $id ? 'rooster-admin-card-' . sanitize_html_class( str_replace( '/', '-', $id ) ) : '' ) ); $sec = ''; // the card wears its entry's section; a card without an entry wears the screen's, as the page head does if ( null !== $entry ) { $sec = \Rooster\Admin\Chrome::sec_class( (string) $entry['product'], (string) $entry['section'] ); } elseif ( null !== $registry && '' !== $registry->current_product() ) { $slug = $registry->current_product(); $section = $registry->current_section( $slug, $registry->visible( $slug ) ); $sec = '' !== $section ? \Rooster\Admin\Chrome::sec_class( $slug, $section ) : ''; } // The pin: the option's, else its entry's; with neither (null), Ui::card_open() takes the one its anchor names (0.87.0). $pin = array_key_exists( 'pin', $opts ) ? ( false === $opts['pin'] ? '' : (string) $opts['pin'] ) : ( null !== $entry ? \Rooster\Admin\Chrome::pin_button( $entry ) : null ); $place = array( // the card's place on a board (0.58.0) 'data-ra-col' => isset( $opts['col'] ) && (int) $opts['col'] >= 1 ? (string) min( 3, (int) $opts['col'] ) : null, 'data-ra-span' => 'all' === ( $opts['span'] ?? '' ) ? 'all' : null, 'data-ra-min' => isset( $opts['min'] ) && (float) $opts['min'] > 0 ? (string) (float) $opts['min'] : null, 'data-ra-movable' => array_key_exists( 'movable', $opts ) && false === $opts['movable'] ? 'false' : null, ); $key = ! empty( $opts['dismissible'] ) && class_exists( '\Rooster\Admin\Chrome', false ) ? \Rooster\Admin\Chrome::card_key( $dom ) : ''; // 0.64.3 $gone = '' !== $key && \Rooster\Admin\Chrome::card_dismissed( $key ); $place += array( 'data-ra-dismissible' => '' !== $key ? $key : null, 'data-ra-dismissed' => $gone, 'hidden' => $gone, ); // What the options leave unset gives way to the caller's attrs (0.83.1): a card that starts hidden, a dependent whose // master is off (States::depends_attrs(), FR4), kept its `hidden` only when it was dismissed. $place = array_filter( $place, static fn( $value ): bool => null !== $value && false !== $value ); // The anatomy is Ui::card_open()'s (0.75.0): the registry's and the chrome's part resolved above, as options. return \Rooster\Admin\Ui::card_open( $title, array( 'id' => $dom, 'entry' => null !== $entry ? $id : '', 'section' => $sec, 'flush' => ! empty( $opts['flush'] ), 'class' => (string) ( $opts['class'] ?? '' ), 'icon' => (string) ( $opts['icon'] ?? ( null !== $entry ? $entry['icon'] : '' ) ), 'meta' => (string) ( $opts['meta'] ?? '' ), 'tools' => (string) ( $opts['tools'] ?? '' ), 'dismiss' => '' !== $key ? \Rooster\Admin\Chrome::dismiss_button( $title ) : '', 'pin' => $pin, 'level' => (int) ( $opts['level'] ?? 2 ), 'hosted' => ! empty( $opts['hosted'] ), 'body_class' => (string) ( $opts['body_class'] ?? '' ), 'attrs' => $place + ( is_array( $opts['attrs'] ?? null ) ? $opts['attrs'] : array() ), 'description' => (string) ( $opts['description'] ?? '' ), 'essential' => ! empty( $opts['essential'] ), ) ); } } if ( ! function_exists( 'rooster_admin_card_section_html' ) ) { /** * A titled section of a card (0.58.0, the Metronic note's F4): closes the body and opens another under a hairline, * its h3 title first; rooster_admin_card_close_html() or rooster_admin_card_footer_html() still closes it. A topic of * a long form at no cost in width, where a sub-card would box it. A section has no pin and does not move: a part * worth either is a card of the board. * * @param string $title The section's title (raw; '' for an untitled part). * @param array $opts id (the section's anchor, for a deep link), description (raw: one sentence). */ function rooster_admin_card_section_html( string $title = '', array $opts = array() ): string { return \Rooster\Admin\Ui::card_section( $title, $opts ); } } /* * The page board (0.58.0, Board): bands of cards in columns a person rearranges, as the Home's blocks. */ if ( ! function_exists( 'rooster_admin_board' ) ) { /** * A board from its bands (Board::render()), escaped: each band's cards as the HTML strings the card helpers return. * * @param string $key The board's key: the screen, unique in the product. * @param array> $bands title, description, key, cols (1, 2, 3, '2:1', '1:2'), rows, * fixed, min, and cards, columns or full. * @param array $opts movable (false: nothing moves), class. */ function rooster_admin_board( string $key, array $bands, array $opts = array() ): string { return \Rooster\Admin\Board::render( $key, $bands, $opts ); } } if ( ! function_exists( 'rooster_admin_board_capture' ) ) { /** * Prints a board whose bands and cards $render echoes (0.69.0, Board::capture()): rooster_admin_band_open(), the * cards, rooster_admin_band_close(), as many bands as the screen has. The board's buffer starts and ends inside. * * @param string $key The board's key. * @param array $opts See rooster_admin_board(). * @param callable $render Echoes the bands and cards. */ function rooster_admin_board_capture( string $key, array $opts, callable $render ): void { \Rooster\Admin\Board::capture( $key, $opts, $render ); } } if ( ! function_exists( 'rooster_admin_board_html' ) ) { /** * The board of a screen's cards built as a string (0.83.0, Board::from_html()), escaped: a leading tab strip is its * head, the cards are split; anything else between them keeps the whole as it came. * * @param string $key The board's key. * @param string $html The cards, as built (escaped). * @param array $opts See rooster_admin_board(). */ function rooster_admin_board_html( string $key, string $html, array $opts = array() ): string { return \Rooster\Admin\Board::from_html( $key, $html, $opts ); } } if ( ! function_exists( 'rooster_admin_board_open' ) ) { /** * Starts a printed board the old way: since 0.69.0 nothing is buffered, the cards print where they come, one under * the other (Board::open()); use rooster_admin_board_capture(). * * @param string $key The board's key. * @param array $opts See rooster_admin_board(). */ function rooster_admin_board_open( string $key, array $opts = array() ): void { \Rooster\Admin\Board::open( $key, $opts ); } } if ( ! function_exists( 'rooster_admin_band_open' ) ) { /** * Starts a band of the board being printed; echo its cards, nothing between them, then rooster_admin_band_close(). * * @param array $opts title, description, key, cols, rows, fixed, min. */ function rooster_admin_band_open( array $opts = array() ): void { \Rooster\Admin\Board::band_open( $opts ); } } if ( ! function_exists( 'rooster_admin_band_close' ) ) { /** Ends the band being printed. */ function rooster_admin_band_close(): void { \Rooster\Admin\Board::band_close(); } } if ( ! function_exists( 'rooster_admin_board_close' ) ) { /** Ends a board rooster_admin_board_open() started (since 0.69.0 it printed as it came). */ function rooster_admin_board_close(): void { \Rooster\Admin\Board::close(); } } if ( ! function_exists( 'rooster_admin_card_close_html' ) ) { /** * The closing of a card. */ function rooster_admin_card_close_html(): string { return \Rooster\Admin\Ui::card_close(); } } if ( ! function_exists( 'rooster_admin_table_assets' ) ) { /** * A library table's assets on an admin screen without the chrome (0.65.1, CONTRACT §8.2 "A table off the chrome"): * call it on `admin_enqueue_scripts` for the screen that prints one (a WooCommerce order, a post's editor), so the * stylesheet comes in the head; a screen that receives a table later from a product's script needs it too. A table * that finds them missing enqueues them itself (with the page's footer). A screen whose script draws tables with the * twin (0.74.0, roosterAdmin.ui.table()) asks for `twin` and names the icons its rows draw. * * @param array $parts What the tables ask for: local, edit, move, filters, counts, totals, cards (0.73.0: * a list shown as a card per row, a stylesheet alone), twin (0.74.0: the table's * script twin). * @param array $icons 0.74.0: the Lucide names a script's table draws, into the page's sprite. */ function rooster_admin_table_assets( array $parts = array(), array $icons = array() ): void { \Rooster\Admin\Library::table_assets( $parts, $icons ); } } if ( ! function_exists( 'rooster_admin_card' ) ) { /** * A whole card: rooster_admin_card_open_html() + the body + rooster_admin_card_close_html(). * * @param string $id Catalogue entry id of the card ('' for none). * @param string $title Title (raw). * @param string $body Escaped HTML. * @param array $opts See rooster_admin_card_open_html(); footer (escaped HTML), footer_opts * (rooster_admin_card_footer_html()'s: sticky, text; 0.85.2). */ function rooster_admin_card( string $id, string $title, string $body, array $opts = array() ): string { $footer = isset( $opts['footer'] ) && '' !== (string) $opts['footer'] ? rooster_admin_card_footer_html( is_array( $opts['footer_opts'] ?? null ) ? $opts['footer_opts'] : array() ) . (string) $opts['footer'] : ''; return rooster_admin_card_open_html( $id, $title, $opts ) . $body . $footer . rooster_admin_card_close_html(); } } if ( ! function_exists( 'rooster_admin_card_footer_html' ) ) { /** * Closes a card's body and opens its footer (0.47.0): the actions under a form (Save), a note about the whole * card (`

`). rooster_admin_card_close_html() closes the footer as it closes a * body. In rooster_admin_card(), the `footer` option (escaped HTML). * * @param array $opts sticky (0.85.2: a long form's footer, shown while the form is unsaved and kept at * the window's foot), text; see Ui::card_footer(). */ function rooster_admin_card_footer_html( array $opts = array() ): string { return \Rooster\Admin\Ui::card_footer( $opts ); } } if ( ! function_exists( 'rooster_admin_card_open' ) ) { /** * Echoes a card's opening (through the chrome's list); the page prints its own body, escaped as it always did, * then rooster_admin_card_close(). The pattern for an existing page that keeps its markup. * * @param string $id Catalogue entry id of the card ('' for none). * @param string $title Title (raw). * @param array $opts See rooster_admin_card_open_html(). */ function rooster_admin_card_open( string $id, string $title, array $opts = array() ): void { \Rooster\Admin\Markup::out( rooster_admin_card_open_html( $id, $title, $opts ) ); } } if ( ! function_exists( 'rooster_admin_card_close' ) ) { /** * Echoes a card's closing. */ function rooster_admin_card_close(): void { \Rooster\Admin\Markup::out( rooster_admin_card_close_html() ); } } if ( ! function_exists( 'rooster_admin_page_open' ) ) { /** * Echoes the opening of a product's own page in the chrome's look: the `.wrap` with the adoption layer * (`rooster-admin-adopt`: WordPress's own markup inside takes the chrome's look) and the page head card (icon, * title, a description). Close with rooster_admin_page_close(). * * @param string $title Title (raw). * @param array $opts icon (Lucide name), description (raw), class, adopt (false: no adoption layer); * since 0.58.0 board (the key of the page's board: its Reset layout among the * head's actions) and essential (the description stays in Compact: FR5). */ function rooster_admin_page_open( string $title, array $opts = array() ): void { $registry = \Rooster\Admin\Library::registry(); $slug = null !== $registry ? $registry->current_product() : ''; $section = '' !== $slug ? $registry->current_section( $slug, $registry->visible( $slug ) ) : ''; // the head wears its section's colour, as its cards do $icon = (string) ( $opts['icon'] ?? 'layout-grid' ); $adopt = ! isset( $opts['adopt'] ) || ! empty( $opts['adopt'] ); $actions = ''; // the head's links as buttons: {label, url, icon (Lucide), external (a new tab)} foreach ( is_array( $opts['actions'] ?? null ) ? $opts['actions'] : array() as $action ) { if ( ! is_array( $action ) || '' === (string) ( $action['label'] ?? '' ) || '' === (string) ( $action['url'] ?? '' ) ) { continue; } $external = ! empty( $action['external'] ); $actions .= '' . ( '' !== (string) ( $action['icon'] ?? '' ) ? \Rooster\Admin\Icons::svg( (string) $action['icon'], 15 ) : '' ) . esc_html( (string) $action['label'] ) . ( $external ? \Rooster\Admin\Icons::ui( 'external', 13 ) . ' ' . esc_html__( '(opens in a new tab)', 'ai-eshop-optimizer' ) . '' : '' ) . ''; } $html = '


' . \Rooster\Admin\Ui::pagehead( $title, array( 'icon' => $icon, 'actions' => $actions, 'description' => (string) ( $opts['description'] ?? '' ), 'class' => '' !== $section ? \Rooster\Admin\Chrome::sec_class( $slug, $section ) : '', 'board' => (string) ( $opts['board'] ?? '' ), 'essential' => ! empty( $opts['essential'] ), ) ); \Rooster\Admin\Markup::out( $html ); } } if ( ! function_exists( 'rooster_admin_page_close' ) ) { /** * Echoes the closing of a page opened with rooster_admin_page_open(). */ function rooster_admin_page_close(): void { echo '
'; } } if ( ! function_exists( 'rooster_admin_icon' ) ) { /** * An inline Lucide icon of the bundled set ('' for an unknown name). * * @param string $name Lucide name. * @param int $size Size in px. */ function rooster_admin_icon( string $name, int $size = 16 ): string { return \Rooster\Admin\Icons::svg( $name, $size ); } } if ( ! function_exists( 'rooster_admin_kses' ) ) { /** * Echoes escaped chrome HTML through the chrome's allowed list. * * @param string $html Escaped HTML. */ function rooster_admin_kses( string $html ): void { \Rooster\Admin\Markup::out( $html ); } } /* * The kit's builders (0.22.0, docs/UI-PLAN.md §2.3): one function per piece, each returning escaped HTML in the * chrome's recipes (Rooster\Admin\Ui). A product's own builder delegates here and keeps its signature. */ if ( ! function_exists( 'rooster_admin_button' ) ) { /** A button or a link that looks like one (Ui::button()). */ function rooster_admin_button( string $label, array $opts = array() ): string { return \Rooster\Admin\Ui::button( $label, $opts ); } } if ( ! function_exists( 'rooster_admin_badge' ) ) { /** A badge or a chip (Ui::badge()). */ function rooster_admin_badge( string $label, string $tone = 'mono', array $opts = array() ): string { return \Rooster\Admin\Ui::badge( $label, $tone, $opts ); } } if ( ! function_exists( 'rooster_admin_notice' ) ) { /** A page or inline notice (Ui::notice()). */ function rooster_admin_notice( string $html, string $tone = 'info', array $opts = array() ): string { return \Rooster\Admin\Ui::notice( $html, $tone, $opts ); } } if ( ! function_exists( 'rooster_admin_tabs' ) ) { /** A tab strip (Ui::tabs()). */ function rooster_admin_tabs( array $tabs, string $active, array $opts = array() ): string { return \Rooster\Admin\Ui::tabs( $tabs, $active, $opts ); } } if ( ! function_exists( 'rooster_admin_field_row' ) ) { /** A field row (Ui::field_row()). */ function rooster_admin_field_row( array $field ): string { return \Rooster\Admin\Ui::field_row( $field ); } } if ( ! function_exists( 'rooster_admin_input' ) ) { /** A text-like input (Ui::input()). */ function rooster_admin_input( string $name, string $value = '', array $opts = array() ): string { return \Rooster\Admin\Ui::input( $name, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_select' ) ) { /** A select (Ui::select()). */ function rooster_admin_select( string $name, array $options, string $value = '', array $opts = array() ): string { return \Rooster\Admin\Ui::select( $name, $options, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_textarea' ) ) { /** A textarea (Ui::textarea()). */ function rooster_admin_textarea( string $name, string $value = '', array $opts = array() ): string { return \Rooster\Admin\Ui::textarea( $name, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_checks' ) ) { /** Checkboxes or radios (Ui::checks()). */ function rooster_admin_checks( string $name, array $options, array $values = array(), array $opts = array() ): string { return \Rooster\Admin\Ui::checks( $name, $options, $values, $opts ); } } if ( ! function_exists( 'rooster_admin_toggle' ) ) { /** A switch (Ui::toggle()). */ function rooster_admin_toggle( string $name, bool $on, array $opts = array() ): string { return \Rooster\Admin\Ui::toggle( $name, $on, $opts ); } } if ( ! function_exists( 'rooster_admin_combo' ) ) { /** A combobox (Ui::combo()). */ function rooster_admin_combo( string $name, array $options, string $value = '', array $opts = array() ): string { return \Rooster\Admin\Ui::combo( $name, $options, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_dialog' ) ) { /** A dialog (Ui::dialog()). */ function rooster_admin_dialog( string $id, string $title, string $body, array $opts = array() ): string { return \Rooster\Admin\Ui::dialog( $id, $title, $body, $opts ); } } if ( ! function_exists( 'rooster_admin_empty' ) ) { /** An empty state (Ui::empty_state()). */ function rooster_admin_empty( string $title, string $text = '', array $opts = array() ): string { return \Rooster\Admin\Ui::empty_state( $title, $text, $opts ); } } if ( ! function_exists( 'rooster_admin_pager' ) ) { /** A pager (Ui::pager()). */ function rooster_admin_pager( int $page, int $pages, string $base, array $opts = array() ): string { return \Rooster\Admin\Ui::pager( $page, $pages, $base, $opts ); } } if ( ! function_exists( 'rooster_admin_table' ) ) { /** The table skin (Ui::table()). */ function rooster_admin_table( array $columns, array $rows, array $opts = array() ): string { return \Rooster\Admin\Ui::table( $columns, $rows, $opts ); } } if ( ! function_exists( 'rooster_admin_progress' ) ) { /** A progress bar (Ui::progress()). */ function rooster_admin_progress( float $value, float $max = 100, array $opts = array() ): string { return \Rooster\Admin\Ui::progress( $value, $max, $opts ); } } if ( ! function_exists( 'rooster_admin_steps' ) ) { /** Steps (Ui::steps()). */ function rooster_admin_steps( array $steps, int $current, array $opts = array() ): string { return \Rooster\Admin\Ui::steps( $steps, $current, $opts ); } } if ( ! function_exists( 'rooster_admin_stars' ) ) { /** Stars (Ui::stars()). */ function rooster_admin_stars( int $value, array $opts = array() ): string { return \Rooster\Admin\Ui::stars( $value, $opts ); } } if ( ! function_exists( 'rooster_admin_stat' ) ) { /** A stat tile (Ui::stat()). */ function rooster_admin_stat( string $label, string $value, array $opts = array() ): string { return \Rooster\Admin\Ui::stat( $label, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_chart' ) ) { /** A chart's container, its configuration as JSON data for the consumer's charting library (Ui::chart(), 0.75.0). */ function rooster_admin_chart( string $kind, array $series, array $opts = array() ): string { return \Rooster\Admin\Ui::chart( $kind, $series, $opts ); } } if ( ! function_exists( 'rooster_admin_chart_svg' ) ) { /** A chart as inline SVG, no script (Ui::chart_svg(); rooster_admin_chart() until 0.75.0). */ function rooster_admin_chart_svg( array $spec ): string { return \Rooster\Admin\Ui::chart_svg( $spec ); } } if ( ! function_exists( 'rooster_admin_meter' ) ) { /** A meter, a measurement within a range (Ui::meter()). */ function rooster_admin_meter( float $value, array $opts = array() ): string { return \Rooster\Admin\Ui::meter( $value, $opts ); } } if ( ! function_exists( 'rooster_admin_wizard' ) ) { /** A wizard: steps, one panel per step, Back and Next (Ui::wizard()). */ function rooster_admin_wizard( string $id, array $steps, int $current, array $opts = array() ): string { return \Rooster\Admin\Ui::wizard( $id, $steps, $current, $opts ); } } /* * R2, fields (0.59.0, Rooster\Admin\Fields; CONTRACT §8.3): the field patterns of the UI recommendations, one wrapper * each. The widths (`xs`, and `short` and `medium` on a select or a combobox), a button's and a field row's `reason`, a * compound row (`parts`) and the help tied to its control are options of the builders above. */ if ( ! function_exists( 'rooster_admin_color' ) ) { /** A colour field: the swatch and its hex (Fields::color()). */ function rooster_admin_color( string $name, string $value = '', array $opts = array() ): string { return \Rooster\Admin\Fields::color( $name, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_segmented' ) ) { /** A segmented switch: two to four short options as radios (Fields::segmented()). */ function rooster_admin_segmented( string $name, array $options, string $value = '', array $opts = array() ): string { return \Rooster\Admin\Fields::segmented( $name, $options, $value, $opts ); } } if ( ! function_exists( 'rooster_admin_compound' ) ) { /** A compound row's control: its parts side by side, each named (Fields::compound()). */ function rooster_admin_compound( array $parts, array $opts = array() ): string { return \Rooster\Admin\Fields::compound( $parts, $opts ); } } if ( ! function_exists( 'rooster_admin_media_picker' ) ) { /** A media picker: thumbnails that sort and remove, and Choose (Fields::media_picker()). */ function rooster_admin_media_picker( string $name, array $items, array $opts = array() ): string { return \Rooster\Admin\Fields::media_picker( $name, $items, $opts ); } } if ( ! function_exists( 'rooster_admin_ordered_list' ) ) { /** An ordered list: rows moved by their grip, each with its controls (Fields::ordered_list()). */ function rooster_admin_ordered_list( array $rows, array $opts = array() ): string { return \Rooster\Admin\Fields::ordered_list( $rows, $opts ); } } if ( ! function_exists( 'rooster_admin_windows' ) ) { /** A page's windows: docked in the user's order, floating, folded, each with its tools (Ui::windows(), 0.68.13). */ function rooster_admin_windows( string $key, array $windows ): string { return \Rooster\Admin\Ui::windows( $key, $windows ); } } if ( ! function_exists( 'rooster_admin_change_row' ) ) { /** A value another card owns: " · Change in " (Fields::change_row()). */ function rooster_admin_change_row( string $value_html, array $opts = array() ): string { return \Rooster\Admin\Fields::change_row( $value_html, $opts ); } } if ( ! function_exists( 'rooster_admin_secret' ) ) { /** A secret field: masked with Replace once saved, never revealed (Fields::secret()). */ function rooster_admin_secret( string $name, bool $saved, array $opts = array() ): string { return \Rooster\Admin\Fields::secret( $name, $saved, $opts ); } }