settings = $settings; } /** * REST only — the tab itself is composed (rendered, styled, scripted) by * {@see EditorPanel} like every other panel section. */ public function register() { add_action( 'rest_api_init', array( $this, 'register_rest' ) ); } /** * Code-level override for the Share tab, layered ON TOP of the * enable_share_copy setting (see {@see is_enabled()}). * * @return bool */ public static function enabled() { /** * Filter whether the Share tab is offered. * * @param bool $enabled Default true. */ return (bool) apply_filters( 'agentimus_share_copy_enabled', true ); } /** * Section-API alias of {@see enabled()}, matching the other panel sections. * * @return bool */ public function is_enabled() { return $this->settings->enabled( 'enable_share_copy' ) && self::enabled(); } /* ---------------------------------------------------------------------- * * The networks * ---------------------------------------------------------------------- */ /** * The networks a card is drafted for — the general-public set. `limit` 0 * means no counter. The intent `href` is a template the JS fills at click * time — {text} is the card's CURRENT textarea value (the author may have * edited it), {url} the permalink — so what opens in the composer is what * the author approved. * * `intent.carries` says what the composer link can pre-fill: 'text' (the * whole post travels), 'title' (Reddit: title + URL as separate fields), * or 'url' (LinkedIn/Facebook: URL only — the Copy button is the way the * text travels). Telegram's own share page carries the URL separately from * the text, so its draft (and brief) deliberately excludes the link. * * @return array */ public static function networks() { $networks = array( 'x' => array( 'label' => 'X (Twitter)', // Not bare "X" — as a card heading that reads like a close button. 'limit' => 280, 'brief' => __( 'punchy and specific, at most 260 characters including the link, at most two hashtags', 'agentimus' ), 'intent' => array( 'carries' => 'text', 'href' => 'https://x.com/intent/post?text={text}', 'label' => __( 'Open X', 'agentimus' ), ), ), 'linkedin' => array( 'label' => 'LinkedIn', 'limit' => 0, 'brief' => __( 'two or three short paragraphs, professional but personal, at most three hashtags, the link on its own last line', 'agentimus' ), 'hint' => __( 'LinkedIn won’t accept text from outside — Open copies your draft, so just paste it into the composer.', 'agentimus' ), 'intent' => array( 'carries' => 'url', 'href' => 'https://www.linkedin.com/sharing/share-offsite/?url={url}', 'label' => __( 'Open LinkedIn', 'agentimus' ), ), ), 'telegram' => array( 'label' => 'Telegram', 'limit' => 0, 'brief' => __( 'one or two sentences for a channel post; the link is attached separately by Telegram, so do NOT include it', 'agentimus' ), 'hint' => __( 'Telegram attaches the link by itself, so it isn’t in the text.', 'agentimus' ), 'intent' => array( 'carries' => 'text', 'href' => 'https://t.me/share/url?url={url}&text={text}', 'label' => __( 'Open Telegram', 'agentimus' ), ), ), 'facebook' => array( 'label' => 'Facebook', 'limit' => 0, 'brief' => __( 'one or two friendly sentences, then the link', 'agentimus' ), 'hint' => __( 'Facebook won’t accept text from outside — Open copies your draft, so just paste it into the composer.', 'agentimus' ), 'intent' => array( 'carries' => 'url', 'href' => 'https://www.facebook.com/sharer/sharer.php?u={url}', 'label' => __( 'Open Facebook', 'agentimus' ), ), ), 'whatsapp' => array( 'label' => 'WhatsApp', 'limit' => 0, 'brief' => __( 'a short friendly message you would send to a group chat — one or two sentences, then the link', 'agentimus' ), 'intent' => array( 'carries' => 'text', 'href' => 'https://api.whatsapp.com/send?text={text}', 'label' => __( 'Open WhatsApp', 'agentimus' ), ), ), 'reddit' => array( 'label' => 'Reddit', 'limit' => 0, 'brief' => __( 'the BODY text of a link submission — one or two honest first-person sentences on what the post covers and why it might interest the community; no marketing tone, no hashtags, no link (Reddit attaches it)', 'agentimus' ), 'hint' => __( 'Reddit takes the post title and link by itself — this text goes in as the body.', 'agentimus' ), 'intent' => array( 'carries' => 'text', // {title} = the post's own title; proven live: the new composer // fills title, link AND body from these three parameters. 'href' => 'https://www.reddit.com/submit?url={url}&title={title}&text={text}', 'label' => __( 'Open Reddit', 'agentimus' ), ), ), ); /** * Adjust the share-copy networks — reorder, drop, or add one (Bluesky, * Mastodon, …). An added entry needs label, limit, brief and an intent * with carries/href/label. * * @param array $networks */ return apply_filters( 'agentimus_share_copy_networks', $networks ); } /* ---------------------------------------------------------------------- * * Drafting — format templates by default, ONE AI call per card on request * ---------------------------------------------------------------------- */ /** * Format drafts for every offered network from what the site already * knows — no AI involved. * * @param \WP_Post $post The post. * @return array Network key → draft text. */ private function local_drafts( \WP_Post $post ) { $title = self::plain( get_the_title( $post ) ); $url = (string) get_permalink( $post ); $desc = self::plain( Description::for_post( $post ) ); $tags = self::hashtags( $post, 2 ); $short = trim( $title . ( '' !== $tags ? "\n\n" . $tags : '' ) . "\n\n" . $url ); $long = trim( sprintf( /* translators: %s: post title. */ __( 'New on the blog: %s', 'agentimus' ), $title ) . ( '' !== $desc ? "\n\n" . $desc : '' ) . "\n\n" . $url ); $titled = trim( $title . ( '' !== $desc ? ' — ' . $desc : '' ) ); $drafts = array( 'x' => $short, 'facebook' => $titled . "\n\n" . $url, 'linkedin' => $long, 'whatsapp' => $titled . "\n\n" . $url, 'telegram' => $titled, // Telegram's share page attaches the URL itself. // Reddit's card is the submission BODY — the title and link travel as // their own composer fields (see the intent template above). 'reddit' => '' !== $desc ? $desc : $title, ); // Only keys that are actually offered (the networks filter may trim the // list); a filter-added network starts from the plain short template. $out = array(); foreach ( self::networks() as $key => $network ) { $out[ $key ] = isset( $drafts[ $key ] ) ? $drafts[ $key ] : $short; } return $out; } /** * ONE AI request writes ONE network's copy — the per-card button's server * half. The reply is the post text itself (no list format to parse), and * it is data, not gospel: empty or errored means '' and the caller keeps * the template draft. * * @param \WP_Post $post The post. * @param string $key Network key (must exist in {@see networks()}). * @return string The drafted text, '' on any failure. */ private function draft_one_ai( \WP_Post $post, $key ) { $networks = self::networks(); if ( ! isset( $networks[ $key ] ) ) { return ''; } $network = $networks[ $key ]; $body = (string) Content::markdown_source( $post ); if ( '' === trim( $body ) ) { $body = wp_strip_all_tags( (string) $post->post_content ); } $body = function_exists( 'mb_substr' ) ? mb_substr( $body, 0, self::MAX_BODY_CHARS ) : substr( $body, 0, self::MAX_BODY_CHARS ); $system = __( 'You write a ready-to-post social media announcement of an article, in the article\'s own language, for one named network. Match the network\'s brief. Use the article link EXACTLY as given when the brief calls for it, never a shortened or altered one. State only what the article supports — no invented claims, no engagement bait. Reply with the post text only — no preamble, no surrounding quotes, no commentary.', 'agentimus' ); $prompt = sprintf( "Article title: %s\nLink: %s\nSummary: %s\nTopics: %s\n\nArticle text (excerpt):\n%s\n\nNetwork: %s\nBrief: %s", self::plain( get_the_title( $post ) ), get_permalink( $post ), self::plain( Description::for_post( $post ) ), implode( ', ', Topics::for_post( $post ) ), $body, $network['label'], isset( $network['brief'] ) ? $network['brief'] : __( 'a short announcement including the link', 'agentimus' ) ); // 4000, not ~1200: a thinking provider's reasoning tokens come out of // the same budget (proven live — a tight budget came back truncated). $reply = ( new Assist( $this->settings ) )->generate( $system, $prompt, 4000 ); if ( is_wp_error( $reply ) ) { return ''; } $text = trim( (string) $reply ); $text = (string) preg_replace( '/^```[a-z]*\s*|\s*```$/i', '', $text ); // A stray code fence, if the model added one. $text = trim( $text, "\"'“”‘’ \t\r\n" ); return sanitize_textarea_field( $text ); } /** * Text as a person reads it. A draft is plain text bound for a social * network, never HTML, so `&`, `’` and ` ` — what * WordPress hands titles and excerpts out with — become the characters * they stand for, and a no-break space a plain one. (Found in the * 2026-10-23 audit: drafts posted "Getter & Setter".) * * ⛔ Only for these plain-text drafts. Every HTML output escapes on its own; * decoding there would turn `<script>` into markup. * * @param string $text Text from WordPress. * @return string */ private static function plain( $text ) { $text = html_entity_decode( (string) $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); return trim( (string) preg_replace( '/[\s\x{00A0}]+/u', ' ', $text ) ); } /** * Up to $max topics as #CamelCase hashtags ('' when the post has none). * * @param \WP_Post $post Post. * @param int $max Max hashtags. * @return string Space-separated hashtags. */ private static function hashtags( \WP_Post $post, $max ) { $tags = array(); foreach ( array_slice( Topics::for_post( $post ), 0, max( 0, (int) $max ) ) as $topic ) { $tag = preg_replace( '/[^A-Za-z0-9]/', '', ucwords( (string) $topic ) ); if ( '' !== $tag ) { $tags[] = '#' . $tag; } } return implode( ' ', $tags ); } /** * What a network's scraper will build a link card from — image via the one * shared resolver ({@see Seo::social_image()}: featured image → site-wide * default → entity image), text via the same sources the drafts use. * * @param \WP_Post $post The post. * @return array{image:string,title:string,description:string,host:string} */ private function preview( \WP_Post $post ) { $image = ( new Seo( $this->settings ) )->social_image( $post ); $host = (string) wp_parse_url( (string) get_permalink( $post ), PHP_URL_HOST ); return array( 'image' => null !== $image ? (string) $image['url'] : '', 'title' => (string) get_the_title( $post ), 'description' => (string) Description::for_post( $post ), 'host' => $host, ); } /* ---------------------------------------------------------------------- * * REST * ---------------------------------------------------------------------- */ /** * POST /share-copy/generate — gated per post by edit_post, exactly like * the editor the tab sits in. Without `network`: the full set of format * drafts plus the link preview (the tab's first load — never AI). With * `network` (+ use_ai): that one card redrafted. * * The network value is validated in the CALLBACK, not via an args enum — * an enum rejects before permission_callback runs (400-not-403; the * 1.27.1 lesson). */ public function register_rest() { register_rest_route( self::NS, self::ROUTE, array( 'methods' => \WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_generate' ), // Off means off everywhere: a disabled feature must not keep a live, // invisible REST surface (the enable_agent_writes doctrine). 'permission_callback' => function ( $request ) { return $this->is_enabled() && current_user_can( 'edit_post', absint( $request['post'] ) ); }, 'args' => array( 'post' => array( 'type' => 'integer', 'required' => true, 'sanitize_callback' => 'absint', ), 'network' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key', ), 'use_ai' => array( 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', ), // "Just the standing, please." The panel asks for this after // it queues something and whenever it comes back to the // screen — no drafting, no preview, one option read. 'lines' => array( 'type' => 'boolean', 'default' => false, 'sanitize_callback' => 'rest_sanitize_boolean', ), ), ) ); } /** * The route handler. AI runs only for a single named card, only when the * author asked AND a provider is there; anything less (or an AI failure) * means the format draft, never an error. * * @param \WP_REST_Request $request Request. * @return \WP_REST_Response|\WP_Error */ public function rest_generate( \WP_REST_Request $request ) { if ( ! self::enabled() ) { return new \WP_Error( 'agentimus_field_off', __( 'The Share tab is turned off.', 'agentimus' ), array( 'status' => 400 ) ); } $post = get_post( absint( $request['post'] ) ); if ( ! $post || ! in_array( $post->post_type, Content::post_types(), true ) ) { return new \WP_Error( 'agentimus_not_found', __( 'That content is not available.', 'agentimus' ), array( 'status' => 404 ) ); } // The standing alone: the panel's cheapest question, and the one it // asks most often. A queued promise becomes a sent one minutes after // the tab was drawn, and an editor left open must not keep saying // "queued" for a post that already went out. if ( rest_sanitize_boolean( $request['lines'] ) ) { return rest_ensure_response( $this->announce_standing( $post->ID ) ); } $networks = self::networks(); $key = (string) $request['network']; // One card asked for — redraft just that network. if ( '' !== $key ) { if ( ! isset( $networks[ $key ] ) ) { return new \WP_Error( 'agentimus_unknown_network', __( 'That network is not offered here.', 'agentimus' ), array( 'status' => 404 ) ); } $drafts = $this->local_drafts( $post ); $text = ''; $source = 'local'; if ( rest_sanitize_boolean( $request['use_ai'] ) && Assist::ai_available() ) { $text = $this->draft_one_ai( $post, $key ); } if ( '' !== $text ) { $source = 'ai'; } else { $text = $drafts[ $key ]; } return rest_ensure_response( array( 'source' => $source, 'network' => array_merge( $this->card( $key, $networks[ $key ] ), array( 'text' => $text ) ), ) ); } // The tab's first load: every card as a format draft, plus the preview. $drafts = $this->local_drafts( $post ); $cards = array(); foreach ( $networks as $k => $network ) { $cards[] = array_merge( $this->card( $k, $network ), array( 'text' => $drafts[ $k ] ) ); } return rest_ensure_response( array( 'source' => 'local', 'permalink' => (string) get_permalink( $post ), 'published' => 'publish' === $post->post_status, 'preview' => $this->preview( $post ), 'networks' => $cards, ) + $this->announce_standing( $post->ID ) ); } /** * This post's announcing standing: a sentence per network, plus the one * number the panel needs to decide whether to watch — the soonest queued * moment across every network, or 0 when nothing is owed. * * The number is what keeps the watching honest: a promise due in a minute * is worth re-reading (and each read wakes wp-cron, so the watching helps * the sending); a promise due next Tuesday is a calendar, and nobody polls * a calendar. * * @param int $post_id Post id. * @return array{announceLines:array,announceNextAt:int} */ private function announce_standing( $post_id ) { $engine = new Integrations\Announcements( $this->settings ); $next = 0; foreach ( Integrations\Announcements::NETWORKS as $network ) { $queued = (int) $engine->post_network_state( $post_id, $network )['queuedAt']; if ( $queued && ( 0 === $next || $queued < $next ) ) { $next = $queued; } } return array( 'announceLines' => $this->announce_lines( $post_id ), 'announceNextAt' => $next, ); } /** * THIS post's standing with each schedulable network, as one ready * sentence per network — the card's memory beside its schedule row. One * line, chosen by what the owner can still act on: a queued promise * outranks a parked failure outranks the last success. Dates are * formatted HERE, in the site's own formats — never the browser's. * * @param int $post_id Post id. * @return array Network key → line ('' = nothing to say). */ private function announce_lines( $post_id ) { $engine = new Integrations\Announcements( $this->settings ); $lines = array(); // Every network the queue knows — the card's memory must not lag the roster. foreach ( Integrations\Announcements::NETWORKS as $network ) { $state = $engine->post_network_state( $post_id, $network ); if ( $state['queuedAt'] ) { $lines[ $network ] = sprintf( /* translators: %s: the scheduled date and time, in the site's formats. */ __( 'Queued for %s — manage it under Agentimus → More → Announcements.', 'agentimus' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $state['queuedAt'] ) ); } elseif ( $state['failed'] ) { $lines[ $network ] = __( 'The last announcement didn’t go — see Agentimus → More → Announcements.', 'agentimus' ); } elseif ( $state['lastSentAt'] ) { $lines[ $network ] = sprintf( /* translators: %s: how long ago, e.g. "10 mins". */ __( 'Last announcement sent %s ago.', 'agentimus' ), human_time_diff( $state['lastSentAt'], time() ) ); } else { $lines[ $network ] = ''; } } return $lines; } /** * The client-facing shape of one network (everything but the text). * * @param string $key Network key. * @param array $network Its {@see networks()} entry. * @return array */ private function card( $key, array $network ) { return array( 'key' => $key, 'label' => $network['label'], 'limit' => (int) $network['limit'], 'hint' => isset( $network['hint'] ) ? (string) $network['hint'] : '', 'intent' => $network['intent'], ); } /* ---------------------------------------------------------------------- * * The panel section (rendered by EditorPanel) * ---------------------------------------------------------------------- */ /** * The tab's shell — the drafts load themselves the first time the tab is * opened; the JS does the rest. * * @param \WP_Post $post The post being edited. */ public function render_meta_box( $post ) { ?>

esc_url_raw( rest_url( self::NS . self::ROUTE ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'aiAvailable' => Assist::ai_available(), // The scheduling door: which networks the announcements queue can // post to (their sharing use is on), and whether THIS user may // queue (the announcements route is admin-gated). A network absent // from the map has no scheduling story yet — its card stays as it // was, no dead promises. 'announceEndpoint' => esc_url_raw( rest_url( self::NS . '/announcements' ) ), 'canSchedule' => current_user_can( 'manage_options' ), 'schedulable' => array( 'telegram' => Integrations\Services\Telegram::sharing_active( new Settings() ), 'x' => Integrations\Services\Twitter::sharing_active( new Settings() ), 'linkedin' => Integrations\Services\LinkedIn::sharing_active( new Settings() ), ), 'sparkSvg' => '', 'i18n' => array( 'working' => __( 'Drafting…', 'agentimus' ), 'error' => __( 'Couldn’t write the drafts — try again, and if it keeps happening, check the provider key under Settings → AI.', 'agentimus' ), 'localNote' => __( 'Drafted from the post’s title, description and topics — no AI involved.', 'agentimus' ), 'aiNote' => __( 'Rewritten by your AI provider — edit to taste before posting.', 'agentimus' ), 'aiFellBack' => __( 'The AI rewrite didn’t come back — this is the format draft.', 'agentimus' ), 'rewrite' => __( 'Rewrite with AI', 'agentimus' ), 'rewriting' => __( 'Rewriting…', 'agentimus' ), 'copy' => __( 'Copy', 'agentimus' ), 'copied' => __( 'Copied ✓', 'agentimus' ), 'chars' => __( 'characters', 'agentimus' ), 'previewLabel' => __( 'Link Preview', 'agentimus' ), 'previewOpen' => __( 'See the link preview', 'agentimus' ), 'closeLabel' => __( 'Close', 'agentimus' ), 'previewHint' => __( 'The image and text networks build this link’s card from.', 'agentimus' ), 'pasteHint' => __( 'Text copied — paste it into the composer that just opened (this network can’t be pre-filled).', 'agentimus' ), 'noImage' => __( 'No image to share — set a featured image (or a default social image in Settings) so this link gets a picture.', 'agentimus' ), 'unpublished' => __( 'This post isn’t published yet — the link in these drafts is its current address and can change at publish time.', 'agentimus' ), 'sendLater' => __( 'Send it later', 'agentimus' ), 'queueIt' => __( 'Queue it', 'agentimus' ), 'queuedTick' => __( 'Queued ✓', 'agentimus' ), 'queuedNote' => __( 'Queued. Watch it under Agentimus → More → Announcements.', 'agentimus' ), 'pickTime' => __( 'Pick a time first.', 'agentimus' ), /* translators: %s: the network's name, e.g. Telegram. */ 'connectToSchedule' => __( 'Connect %s under Agentimus → Integrations → Sharing, and this card can post by itself at a time you pick.', 'agentimus' ), 'fbNoSchedule' => __( 'Can’t be scheduled: Facebook doesn’t let apps post to a personal profile. The draft above stays ready to paste.', 'agentimus' ), ), ); return 'window.agentimusSc=' . wp_json_encode( $config ) . ';' . <<<'JS' (function(){ var CFG = window.agentimusSc || {}; if (!CFG.endpoint) { return; } function esc(s){ return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/ (the modal law — never inside a scrolling metabox), // closed by its button, the backdrop or Escape; focus walks back to the // eye that asked. function openPreview(p, opener){ if (document.querySelector('.agentimus-sc-pop')) { return; } var media = p.image ? '
' : '
' + esc(CFG.i18n.noImage) + '
'; var pop = document.createElement('div'); pop.className = 'agentimus-sc-pop'; pop.setAttribute('role', 'dialog'); pop.setAttribute('aria-modal', 'true'); pop.setAttribute('aria-label', CFG.i18n.previewLabel); pop.innerHTML = '
' + '
' + esc(CFG.i18n.previewLabel) + '' + '' + '
' + '
' + media + '
' + '' + esc(p.host || '') + '' + '' + esc(p.title || '') + '' + '' + esc(p.description || '') + '' + '
' + '
' + '

' + esc(CFG.i18n.previewHint) + '

' + '
'; function close(){ document.removeEventListener('keydown', onKey); if (pop.parentNode) { pop.parentNode.removeChild(pop); } if (opener && opener.focus) { opener.focus(); } } function onKey(e){ if (e.key === 'Escape') { close(); } } pop.addEventListener('click', function(e){ if (e.target === pop) { close(); } }); pop.querySelector('.agentimus-sc-pop__close').addEventListener('click', close); document.addEventListener('keydown', onKey); // The dialog is body-mounted, outside the measured box — it inherits the // box's verdict rather than measuring , so the pair always match. var host = document.querySelector('.agentimus-sc'); if (host && host.classList.contains('agentimus-sc--dark')) { pop.classList.add('agentimus-sc--dark'); } document.body.appendChild(pop); pop.querySelector('.agentimus-sc-pop__close').focus(); } function card(net, ctx){ var item = document.createElement('div'); item.className = 'agentimus-sc__card'; var ai = CFG.aiAvailable ? '' : ''; // The schedule row: only a network the announcements queue can actually // post to gets the controls; Facebook states why it never will; a network // with no scheduling story yet shows nothing — no dead promises. var sched = ''; if (net.key === 'facebook') { sched = '

' + esc(CFG.i18n.fbNoSchedule) + '

'; } else if (CFG.schedulable && Object.prototype.hasOwnProperty.call(CFG.schedulable, net.key)) { if (CFG.schedulable[net.key] && CFG.canSchedule) { // Picker and verb ride in one group: on a narrow card the pair wraps // below the label TOGETHER — the button never orphans onto its own line. sched = '
' + '' + esc(CFG.i18n.sendLater) + '' + '' + '' + '' + '' + '
'; } else { sched = '

' + esc(CFG.i18n.connectToSchedule.replace('%s', net.label)) + '

'; } } // The card's mark — the Sharing tab's grammar, carried here so the two // surfaces read as one feature. // ⛔⛔ THE REAL VENDOR MARK WHERE WE HOLD ONE, A MONOGRAM ONLY WHERE WE DO // NOT. This said "Letters, never logos" and drew "X"/"In"/"Tg" for the three // networks Integrations → Sharing renders with — so one feature // wore two different marks depending on which screen you opened, and the // comment justified it with a claim about the Sharing tab that was not true // of the Sharing tab. // ⭐ Paths are the SAME ones resources/admin/js/brandMarks.js holds, viewBox // included — copied, not redrawn, so the two surfaces cannot drift. ⛔ If a // mark is corrected there, correct it here in the same commit. // ⭐ facebook / whatsapp / reddit keep their letters HONESTLY: brandMarks.js // holds no faithful mark for them, and an invented lookalike is the one thing // that is never allowed. Adding a mark there means adding it in both places. var LOGOS = { x: { vb: '-4.44 -4.44 32.88 32.88', d: ['M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z'] }, linkedin: { vb: '-0.88 -0.78 24.76 24.76', d: ['M3.6 1.2a2.4 2.4 0 1 1 0 4.8 2.4 2.4 0 0 1 0-4.8Z','M1.6 8h4v14h-4z','M8.2 8H12v1.9h.05c.6-1.15 2.05-2.2 4.15-2.2 3.5 0 5.6 2.15 5.6 6.25V22h-4v-6.9c0-1.95-.75-3.15-2.4-3.15-1.35 0-2.1.9-2.45 1.8-.13.32-.15.75-.15 1.2V22h-4V8Z'] }, telegram: { vb: '-1.95 -1.95 27.91 27.91', d: ['M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z'] }, facebook: { vb: '-1.95 -1.95 27.91 27.91', d: ['M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z'] }, whatsapp: { vb: '-1.95 -1.95 27.91 27.91', d: ['M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z'] }, reddit: { vb: '-1.95 -1.95 27.91 27.91', d: ['M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z'] } }; // ⛔ EMPTY, and that is the point: every network this card offers now has a // faithful mark, so nothing falls back to letters. A monogram stays the // honest answer for a brand we hold no mark for — leave this here so the next // network added has somewhere to land rather than getting an invented logo. var MARKS = {}; function markHtml(key, label){ var L = LOGOS[key]; if (L) { return ''; } return esc(MARKS[key] || label.charAt(0)); } // The card's memory: this post's standing with the network, one server- // written sentence (queued promise > parked failure > last success). // Always drawn, hidden while empty: this line APPEARS the moment the // owner queues something, and a node that has to be created on refresh is // a node the refresh has to know where to put. var aLine = ctx.announceLine(net.key); var announce = '

' + esc(aLine) + '

'; item.innerHTML = '
' + '' + '' + esc(net.label) + '' + '' + '' + '
' + '' + (net.hint ? '

' + esc(net.hint) + '

' : '') + '
' + '' + ai + '' + '
' + sched + announce; var area = item.querySelector('.agentimus-sc__text'); var count = item.querySelector('.agentimus-sc__count'); var copyBtn = item.querySelector('.agentimus-sc__copy'); item.querySelector('.agentimus-sc__eye').addEventListener('click', function(e){ ctx.preview(e.currentTarget); }); area.value = net.text; function recount(){ if (!net.limit) { count.textContent = ''; return; } var n = area.value.length; count.textContent = n + '/' + net.limit + ' ' + CFG.i18n.chars; count.classList.toggle('agentimus-sc__count--over', n > net.limit); } area.addEventListener('input', recount); recount(); copyBtn.addEventListener('click', function(){ copyText(area, this); }); item.querySelector('.agentimus-sc__open').addEventListener('click', function(){ // A URL-only composer (Facebook/LinkedIn) can't carry the text — put it // on the clipboard on the way out, so the author just pastes on arrival. if (net.intent.carries === 'url') { copyText(area, copyBtn); ctx.note(CFG.i18n.pasteHint); } window.open(intentHref(net.intent.href, area.value, ctx.permalink(), ctx.title()), '_blank', 'noopener'); }); var queueBtn = item.querySelector('.agentimus-sc__queue'); if (queueBtn) { var timeInput = item.querySelector('.agentimus-sc__schedtime'); // Prefill an hour out, floored to the minute — a suggestion the owner // adjusts, in the browser's local clock. The queue stores the absolute // moment; the Announcements screen replays it in the site's format. var seed = new Date(Date.now() + 3600000); seed.setSeconds(0, 0); var pad = function(n){ return (n < 10 ? '0' : '') + n; }; var toLocal = function(d){ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()); }; timeInput.value = toLocal(seed); timeInput.min = toLocal(new Date()); queueBtn.addEventListener('click', function(){ var when = new Date(timeInput.value); if (!timeInput.value || isNaN(when.getTime())) { ctx.note(CFG.i18n.pickTime); return; } queueBtn.disabled = true; post( { action: 'queue', network: net.key, post_id: ctx.id(), body: area.value, at: Math.floor(when.getTime() / 1000) }, CFG.announceEndpoint ) .then(function(res){ queueBtn.disabled = false; if (!res.ok) { ctx.note((res.body && res.body.message) || CFG.i18n.error); return; } queueBtn.textContent = CFG.i18n.queuedTick; setTimeout(function(){ queueBtn.textContent = CFG.i18n.queueIt; }, 2400); ctx.note(CFG.i18n.queuedNote); // The act must ring where it lands: the card's own standing line // states the promise, without a reload. ctx.refreshLines(); }) .catch(function(){ queueBtn.disabled = false; ctx.note(CFG.i18n.error); }); }); } var aiBtn = item.querySelector('.agentimus-sc__aione'); if (aiBtn) { aiBtn.addEventListener('click', function(){ aiBtn.disabled = true; aiBtn.innerHTML = CFG.sparkSvg + esc(CFG.i18n.rewriting); post({ post: ctx.id(), network: net.key, use_ai: true }) .then(function(res){ aiBtn.disabled = false; aiBtn.innerHTML = CFG.sparkSvg + esc(CFG.i18n.rewrite); if (!res.ok) { ctx.note((res.body && res.body.message) || CFG.i18n.error); return; } area.value = (res.body.network && res.body.network.text) || area.value; recount(); ctx.note(res.body.source === 'ai' ? CFG.i18n.aiNote : CFG.i18n.aiFellBack); }) .catch(function(){ aiBtn.disabled = false; aiBtn.innerHTML = CFG.sparkSvg + esc(CFG.i18n.rewrite); ctx.note(CFG.i18n.error); }); }); } return item; } function boot(){ var box = document.querySelector('.agentimus-sc'); if (!box) { return false; } // One measurement, at boot: walk up to the first painted ancestor and // read its lightness. The metabox wears whatever world it sits in — a // light admin keeps it light at midnight; a genuinely darkened admin // takes it along. (The OS's own mode is nobody here.) if (surroundingsAreDark(box)) { box.classList.add('agentimus-sc--dark'); } var note = box.querySelector('.agentimus-sc__note'); var grid = box.querySelector('.agentimus-sc__grid'); var foot = box.querySelector('.agentimus-sc__foot'); var loaded = false, busy = false, permalink = '', postTitle = ''; var previewData = {}; var announceLines = {}, announceNextAt = 0, lineTimer = null, lastLineRead = 0; // Re-read the standing and repaint each card's line in place. Cheap by // design: no drafting, no preview, one option read on the server. function refreshLines(force){ if (!loaded || !ctx.id()) { return; } // A window regaining focus can fire in bursts; the standing does not // change that fast. var now = Date.now(); if (!force && now - lastLineRead < 10000) { return; } lastLineRead = now; post({ post: ctx.id(), lines: true }) .then(function(res){ if (!res.ok || !res.body) { return; } announceLines = res.body.announceLines || {}; announceNextAt = parseInt(res.body.announceNextAt, 10) || 0; var nodes = grid.querySelectorAll('[data-announce]'); for (var i = 0; i < nodes.length; i++) { var line = announceLines[nodes[i].getAttribute('data-announce')] || ''; nodes[i].textContent = line; nodes[i].hidden = !line; } armLineWatch(); }) .catch(function(){}); } // Watch ONLY while a promise is due (or about to be): the send happens on // a cron tick, an idle editor makes no requests, so this re-read is also // what wakes the tick. A promise days out is a calendar — nobody polls a // calendar. function armLineWatch(){ if (lineTimer) { clearTimeout(lineTimer); lineTimer = null; } if (!announceNextAt) { return; } if (announceNextAt * 1000 - Date.now() > 60000) { return; } lineTimer = setTimeout(function(){ refreshLines(true); }, 15000); } var ctx = { id: function(){ return parseInt(box.getAttribute('data-post'), 10); }, permalink: function(){ return permalink; }, title: function(){ return postTitle; }, announceLine: function(key){ return announceLines[key] || ''; }, refreshLines: function(){ refreshLines(true); }, preview: function(opener){ openPreview(previewData, opener); }, note: function(t){ note.textContent = t; } }; function load(){ if (loaded || busy || !ctx.id()) { return; } busy = true; note.textContent = CFG.i18n.working; post({ post: ctx.id() }) .then(function(res){ busy = false; if (!res.ok) { note.textContent = (res.body && res.body.message) || CFG.i18n.error; return; } loaded = true; permalink = res.body.permalink || ''; postTitle = (res.body.preview && res.body.preview.title) || ''; note.textContent = CFG.i18n.localNote; grid.innerHTML = ''; announceLines = res.body.announceLines || {}; announceNextAt = parseInt(res.body.announceNextAt, 10) || 0; lastLineRead = Date.now(); previewData = res.body.preview || {}; (res.body.networks || []).forEach(function(net){ grid.appendChild(card(net, ctx)); }); if (res.body.published === false) { foot.textContent = CFG.i18n.unpublished; foot.hidden = false; } else { foot.hidden = true; } armLineWatch(); }) .catch(function(){ busy = false; note.textContent = CFG.i18n.error; }); } // Coming back to the screen is the other moment the standing can be // stale: the editor sat open while the queue drained. One read on return, // none while away — the freshness rule the rest of the plugin keeps. document.addEventListener('visibilitychange', function(){ if (!document.hidden) { refreshLines(false); } }); window.addEventListener('focus', function(){ refreshLines(false); }); if (window.IntersectionObserver) { var io = new IntersectionObserver(function(entries){ for (var i = 0; i < entries.length; i++) { if (entries[i].isIntersecting) { io.disconnect(); load(); return; } } }); io.observe(box); } else { load(); } return true; } function run(tries){ if (!boot() && tries > 0) { setTimeout(function(){ run(tries - 1); }, 300); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function(){ run(20); }); } else { run(20); } })(); JS; } /** * Tab styles — admin palette, scoped. Every card is the same shape: a * flex column in a uniform grid, textarea filling the middle, actions * pinned to the bottom — so the row reads as one organized set. The * preview card mimics a link card: cover image on a 1.91:1 frame (the og * ratio), then host/title/description. * * @return string */ public static function css() { // One local palette, declared twice: the whole surface paints from its // own variables, so a dark OS re-inks everything in one media block. // The sheet ground is what separates this box from the editor content // above it — warm paper, warm cards on it, white fields on top: three // planes the eye can tell apart in either mode. return '.agentimus-sc,.agentimus-sc-pop{--asc-ground:#f6f5f2;--asc-card:#fdfcfa;--asc-line:#d5d3cf;--asc-line-soft:#e7e5e0;--asc-ink:#1d2327;--asc-soft:#646970;--asc-faint:#8c8f94;--asc-field:#fff;--asc-accent:#2271b1;--asc-bad:#b32d2e;' . 'background:var(--asc-ground);border:1px solid var(--asc-line-soft);border-radius:10px;padding:16px;margin-top:6px}' // Not the OS's clock — the metabox paints by the ground it actually // sits on. wp-admin's metaboxes panel is light whatever the hour, so // this class is granted by JS only when a MEASURED surrounding is // dark (a dark-admin plugin, some future dark wp-admin) — never by // prefers-color-scheme, which turned this card into a dark island // inside a light editor the evening his Mac hit sunset. . '.agentimus-sc--dark{--asc-ground:#1e1d1b;--asc-card:#282725;--asc-line:#403e3a;--asc-line-soft:#343330;--asc-ink:#e3e1de;--asc-soft:#a6a39e;--asc-faint:#8b8985;--asc-field:#191817;--asc-accent:#5b9bd5;--asc-bad:#e0705f}' // The intro as an alert-shaped card: accent on the left edge, the // lead, the how, and the drafting-source pill in one quiet callout. . '.agentimus-sc__intro-card{background:var(--asc-card);border:1px solid var(--asc-line-soft);border-left:3px solid var(--asc-accent);border-radius:8px;padding:14px 16px 12px;margin:2px 0 14px;box-shadow:0 1px 2px rgba(16,24,40,.04)}' // ⭐ The same measure the Readability list takes {@see \Agentimus\PageCheckMetaBox::css()}. // These two tabs share one box, so a reader switching between them must // not find the line length doubling: capped at 557px there and running // 1170px here was one panel that reads and one that does not. . '.agentimus-sc__lead{font-size:14px;font-weight:600;color:var(--asc-ink);letter-spacing:-.01em;margin:0 0 3px;max-width:68ch}' . '.agentimus-sc__intro{color:var(--asc-soft);font-size:12.5px;line-height:1.6;margin:0 0 9px;max-width:68ch}' . '.agentimus-sc__note{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--asc-soft);background:var(--asc-field);border:1px solid var(--asc-line);border-radius:999px;padding:5px 14px;margin:0}' . '.agentimus-sc__note:empty{display:none}' . '.agentimus-sc__note::before{content:"";flex:none;width:6px;height:6px;border-radius:50%;background:var(--asc-accent)}' . '.agentimus-sc__body{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-start;container-type:inline-size}' // auto-rows 1fr: every card the size of the tallest, across ALL // rows (his call) — the set reads as one sheet of identical cards. . '.agentimus-sc__grid{flex:1 1 620px;display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));grid-auto-rows:1fr;gap:14px;align-items:stretch}' // Three across at most (his number) — the auto-fill above is only // the fallback for browsers without container queries. . '@container (max-width:659px){.agentimus-sc__grid{grid-template-columns:1fr}}' . '@container (min-width:660px){.agentimus-sc__grid{grid-template-columns:repeat(2,1fr)}}' . '@container (min-width:1000px){.agentimus-sc__grid{grid-template-columns:repeat(3,1fr)}}' // The card's own mild warm ground (his call) — the FIELD is the // white thing; the card is the paper it sits on. . '.agentimus-sc__card{display:flex;flex-direction:column;border:1px solid var(--asc-line-soft);border-radius:8px;padding:16px;background:var(--asc-card);min-height:240px;box-sizing:border-box;box-shadow:0 1px 2px rgba(16,24,40,.04);transition:border-color .15s ease,box-shadow .15s ease}' . '.agentimus-sc__card:hover{border-color:var(--asc-line);box-shadow:0 4px 12px rgba(16,24,40,.07)}' . '.agentimus-sc__head{display:flex;align-items:center;gap:8px;margin-bottom:10px}' . '.agentimus-sc__mark{flex:none;width:26px;height:26px;border-radius:7px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;background:var(--asc-ground);color:var(--asc-soft);border:1px solid var(--asc-line-soft)}' // A real vendor mark is a SHAPE, not a letter: it takes the card's ink // rather than the monogram's soft grey, or the logo reads as disabled // beside the network name it belongs to. . '.agentimus-sc__mark.is-brand{color:var(--asc-ink)}' . '.agentimus-sc__mark svg{display:block}' . '.agentimus-sc__network{font-weight:600;font-size:13px;color:var(--asc-ink)}' . '.agentimus-sc__count{margin-left:auto;font-size:11px;color:var(--asc-faint);font-variant-numeric:tabular-nums}' . '.agentimus-sc__count--over{color:var(--asc-bad);font-weight:600}' // One height for every draft box (his call): the card's leftover // space collects between hint and actions instead of stretching // whichever textarea happens to sit in the emptiest card. . '.agentimus-sc__text{flex:none;height:160px;width:100%;font-size:13px;line-height:1.5;resize:none;box-sizing:border-box;border:1px solid var(--asc-line);border-radius:6px;padding:10px 12px;background:var(--asc-field);color:var(--asc-ink)}' . '.agentimus-sc__cardhint{font-size:11px;color:var(--asc-soft);font-style:italic;margin:4px 0 0}' . '.agentimus-sc__row{display:flex;gap:8px;align-items:center;margin-top:auto;padding-top:8px}' . '.agentimus-sc__row .agentimus-sc__open{margin-left:auto}' // The outward mark: these buttons leave the site — say so at a glance. . '.agentimus-sc__open{display:inline-flex;align-items:center;gap:4px}' // Under core's 782px admin breakpoint every .button swells to a 40px // touch target. A drafting card is dense desk furniture, not a // toolbar — the metabox opts out, keeping the same compact rhythm at // every width. (The composer link keeps its own auto left margin.) . '@media screen and (max-width:782px){' . '.wp-core-ui .agentimus-sc .button:not(.button-link){min-height:24px;line-height:22px;font-size:11px;padding:0 8px;margin:0;vertical-align:middle}' . '.wp-core-ui .agentimus-sc .button.button-link{font-size:11px;min-height:24px;line-height:22px;margin:0 0 0 auto}' . '}' . '.agentimus-sc__aione{display:inline-flex;align-items:center;gap:4px}' . '.agentimus-sc__open{display:inline-flex;align-items:center;gap:4px}' . '.agentimus-sc__foot{color:var(--asc-soft);font-size:12px;font-style:italic;margin:10px 0 0}' . '.agentimus-sc__prevframe{flex:1;min-height:0;border:1px solid var(--asc-line);border-radius:6px;overflow:hidden;display:flex;flex-direction:column;background:var(--asc-field)}' . '.agentimus-sc__previmg{flex:none;aspect-ratio:1.91/1;background-size:cover;background-position:center;background-color:var(--asc-line-soft)}' . '.agentimus-sc__previmg--none{display:flex;align-items:center;justify-content:center;text-align:center;color:var(--asc-soft);font-size:12px;padding:12px;box-sizing:border-box;background:repeating-linear-gradient(45deg,var(--asc-ground),var(--asc-ground) 8px,var(--asc-line-soft) 8px,var(--asc-line-soft) 16px)}' . '.agentimus-sc__prevbody{display:flex;flex-direction:column;gap:2px;padding:8px 10px;overflow:hidden}' . '.agentimus-sc__prevhost{font-size:11px;color:var(--asc-soft);text-transform:uppercase}' . '.agentimus-sc__prevtitle{font-weight:600;font-size:13px;color:var(--asc-ink);overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}' . '.agentimus-sc__prevdesc{font-size:12px;color:var(--asc-soft);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}' . '.agentimus-sc__prevhint{color:var(--asc-soft);font-size:11px;margin:8px 0 0}' . '.agentimus-sc__sched{display:flex;flex-wrap:wrap;gap:8px;align-items:center;border-top:1px dashed var(--asc-line);margin-top:10px;padding-top:10px}' . '.agentimus-sc__sched--plain{display:block;font-size:11px;color:var(--asc-soft);font-style:italic}' . '.agentimus-sc__schedlabel{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--asc-faint)}' . '.agentimus-sc__schedgroup{display:flex;gap:8px;align-items:center;flex:1 1 auto;min-width:0}' // Slim, to the button's own height (his call) — the row reads as // one line of controls, not a field towering over its verb. The // doubled class outranks core's input[type=datetime-local] rule, // whose min-height:30px was quietly winning. 24px is button-small's // REAL height (measured, not assumed — 26 left the verb 2px short). // The field may flex down a little; its text is 12px, so 176px still // shows the full stamp without clipping. . '.agentimus-sc .agentimus-sc__schedtime{flex:1 1 176px;min-width:0;font-size:12px;background:var(--asc-field);color:var(--asc-ink);border:1px solid var(--asc-line);border-radius:4px;padding:0 6px;height:24px;min-height:24px;line-height:22px;box-sizing:border-box}' . '.agentimus-sc__queue{flex:0 0 auto}' // The card's memory as a small notice (his call) — the intro // card's accent grammar in miniature, never bare floating text. . '.agentimus-sc__announce{margin:10px 0 0;padding:7px 12px;background:var(--asc-ground);border-left:3px solid var(--asc-accent);border-radius:0 6px 6px 0;font-size:12px;line-height:1.5;color:var(--asc-soft)}' // The eye: quiet beside the title, the accent only when asked. . '.agentimus-sc__eye{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;background:transparent;color:var(--asc-faint);cursor:pointer;border-radius:5px}' . '.agentimus-sc__eye:hover,.agentimus-sc__eye:focus-visible{color:var(--asc-accent);background:var(--asc-ground)}' // The preview dialog — body-mounted, so it carries the token set // itself (see the :root-like selector above). . '.agentimus-sc-pop{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.55)}' . '.agentimus-sc-pop__panel{width:min(400px,100%);max-height:min(80vh,640px);overflow:auto;background:var(--asc-card);border:1px solid var(--asc-line-soft);border-radius:10px;padding:16px;box-sizing:border-box;box-shadow:0 12px 40px rgba(0,0,0,.3)}' . '.agentimus-sc-pop__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;font-weight:600;font-size:13px;color:var(--asc-ink)}' . '.agentimus-sc-pop__close{flex:none;width:26px;height:26px;padding:0;border:0;background:transparent;color:var(--asc-faint);cursor:pointer;font-size:19px;line-height:1;border-radius:5px}' . '.agentimus-sc-pop__close:hover,.agentimus-sc-pop__close:focus-visible{color:var(--asc-ink);background:var(--asc-ground)}' // Under core's 782px admin breakpoint every .button and input // swells to a 40px touch target. A drafting card is dense desk // furniture, not a toolbar — the metabox opts out, keeping the // same compact rhythm at every width. (.wp-core-ui outranks // core's own mobile rule; the composer link keeps its own dress.) . '@media screen and (max-width:782px){' . '.wp-core-ui .agentimus-sc .button:not(.button-link){min-height:24px;line-height:22px;font-size:11px;padding:0 8px;margin:0;vertical-align:middle}' . '.wp-core-ui .agentimus-sc .button.button-link{font-size:11px;min-height:24px;line-height:22px;margin:0 0 0 auto}' . '.agentimus-sc .agentimus-sc__schedtime{height:24px;min-height:24px;font-size:12px;line-height:22px;padding:0 6px}' . '}'; } }