Plugin Directory

Changeset 3798740


Ignore:
Timestamp:
11/12/2026 04:08:00 PM (15 hours ago)
Author:
maller
Message:

Version 0.3.16

Location:
enfants-img-performer/trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • enfants-img-performer/trunk/CHANGELOG.md

    r3786650 r3798740  
    22
    33Alle relevanten Änderungen an IMG Performer sind hier dokumentiert.
     4
     5## [0.3.16] – 2026-11-12
     6
     7### Neu
     8- Floating Label als zweites Design für das sichtbare KI-Label: In Ruhe nur eine kleine „AI“-Pille, bei Hover, Fokus oder Tippen wächst sie weich zu „AI generated“ bzw. „AI modified“ auf. Bedienbar per Maus, Tastatur (Enter, Leertaste, Esc) und Touch, mit `aria-expanded` für Screenreader; bei „Bewegung reduzieren“ ohne Animation (IMP-315)
     9- Einstellungen unter „Visible label on the frontend“: Design (Circle oder Floating) und Position (oben links oder unten rechts) (IMP-315)
     10- Labels passen sich der Bildbreite an: Auf Bildern unter 200 px wird das Label kleiner (70 %) und sitzt näher an der Ecke (IMP-316)
     11
     12### Geändert
     13- Das Circle Label ist etwas kleiner (IMP-315)
     14
     15### Behoben
     16- Beitragsbilder bekamen zwei Label-Ebenen übereinander. Bei Circle fiel das nicht auf, beim Floating Label schien die zweite Ebene durch (IMP-315)
     17- Beitragsbild im Cover-Block (Hero): Mit KI-Label wurde das Bild abgeschnitten und der Hero blieb leer. Das Bild füllt den Cover wieder vollständig, und das Label liegt über dem Abdunkeln (Dim) des Cover-Blocks, auch bei Cover-Blöcken mit festem Hintergrund (IMP-315)
    418
    519## [0.3.15] – 2026-11-05
  • enfants-img-performer/trunk/enfants-img-performer.php

    r3786650 r3798740  
    33 * Plugin Name:       Enfants IMG Performer
    44 * Description:       Converts uploaded images to WebP automatically and detects and labels AI-generated images on upload automatically, with manual classification as a complement. Runs on your own server, no cloud, no account. Designed for GDPR.
    5  * Version:           0.3.15
     5 * Version:           0.3.16
    66 * Author:            Enfants Terribles digital GmbH
    77 * Author URI:        https://www.enfants.de
    … …  
    1818define( 'EIMP_PATH', plugin_dir_path( __FILE__ ) );
    1919define( 'EIMP_URL', plugin_dir_url( __FILE__ ) );
    20 define( 'EIMP_VERSION', '0.3.15' );
     20define( 'EIMP_VERSION', '0.3.16' );
    2121
    2222/**
  • enfants-img-performer/trunk/includes/Admin/AiLabelingSettings.php

    r3772693 r3798740  
    4949                        ]
    5050                );
     51                register_setting(
     52                        'eimp_ai_labeling_settings',
     53                        'eimp_ai_label_design',
     54                        [
     55                                'type'              => 'string',
     56                                'sanitize_callback' => static function ( $value ): string {
     57                                        return in_array( $value, [ 'circle', 'floating' ], true ) ? $value : 'circle';
     58                                },
     59                                'default'           => 'circle',
     60                        ]
     61                );
     62                // PRO registers eimp_ai_label_position itself (all four corners).
     63                if ( ! (bool) apply_filters( 'eimp_is_pro_active', false ) ) {
     64                        register_setting(
     65                                'eimp_ai_labeling_settings',
     66                                'eimp_ai_label_position',
     67                                [
     68                                        'type'              => 'string',
     69                                        'sanitize_callback' => static function ( $value ): string {
     70                                                return in_array( $value, [ 'top-left', 'bottom-right' ], true ) ? $value : 'top-left';
     71                                        },
     72                                        'default'           => 'top-left',
     73                                ]
     74                        );
     75                }
     76        }
     77
     78        /**
     79         * Free design + position fields (IMP-315). PRO
     80         * replaces these with its own, richer controls.
     81         */
     82        private function render_free_label_fields() {
     83                $design   = (string) get_option( 'eimp_ai_label_design', 'circle' );
     84                $position = (string) get_option( 'eimp_ai_label_position', 'top-left' );
     85                $design   = in_array( $design, [ 'circle', 'floating' ], true ) ? $design : 'circle';
     86                $position = in_array( $position, [ 'top-left', 'bottom-right' ], true ) ? $position : 'top-left';
     87                ?>
     88                <div style="display:flex; gap:32px; flex-wrap:wrap; margin-top:20px;">
     89                        <div>
     90                                <fieldset style="margin:0 0 16px; border:0; padding:0;">
     91                                        <legend style="font-weight:600; margin-bottom:6px;"><?php esc_html_e( 'Design', 'enfants-img-performer' ); ?></legend>
     92                                        <label style="display:block; margin-bottom:4px;"><input type="radio" name="eimp_ai_label_design" value="circle" <?php checked( 'circle', $design ); ?> /> <?php esc_html_e( 'Circle Label', 'enfants-img-performer' ); ?></label>
     93                                        <label style="display:block;"><input type="radio" name="eimp_ai_label_design" value="floating" <?php checked( 'floating', $design ); ?> /> <?php esc_html_e( 'Floating Label', 'enfants-img-performer' ); ?></label>
     94                                </fieldset>
     95                                <fieldset style="margin:0; border:0; padding:0;">
     96                                        <legend style="font-weight:600; margin-bottom:6px;"><?php esc_html_e( 'Position', 'enfants-img-performer' ); ?></legend>
     97                                        <label style="display:block; margin-bottom:4px;"><input type="radio" name="eimp_ai_label_position" value="top-left" <?php checked( 'top-left', $position ); ?> /> <?php esc_html_e( 'Top left', 'enfants-img-performer' ); ?></label>
     98                                        <label style="display:block; margin-bottom:4px;"><input type="radio" name="eimp_ai_label_position" value="bottom-right" <?php checked( 'bottom-right', $position ); ?> /> <?php esc_html_e( 'Bottom right', 'enfants-img-performer' ); ?></label>
     99                                </fieldset>
     100                        </div>
     101                </div>
     102                <?php
    51103        }
    52104
    … …  
    104156                                 * @param int $enabled Current value of eimp_ai_label_enabled.
    105157                                 */
     158                                if ( ! (bool) apply_filters( 'eimp_is_pro_active', false ) ) {
     159                                        $this->render_free_label_fields();
     160                                }
    106161                                do_action( 'eimp_ai_label_extra_fields', $enabled );
    107162                                ?>
  • enfants-img-performer/trunk/includes/Admin/Settings.php

    r3741830 r3798740  
    116116         */
    117117        private function render_quality_field() {
    118                 $value = (int) get_option( 'eimp_quality', 85 );
    119                 $pct   = (int) round( ( $value - 1 ) / 99 * 100 );
     118                $value  = (int) get_option( 'eimp_quality', 85 );
     119                $pct    = (int) round( ( $value - 1 ) / 99 * 100 );
     120                $is_pro = (bool) apply_filters( 'eimp_is_pro_active', false );
    120121                ?>
    121122                <div style="display:flex; align-items:center; gap:12px; margin-bottom:6px; white-space:nowrap;">
    122                         <span style="font-size:12px; font-weight:600;"><?php esc_html_e( 'Quality', 'enfants-img-performer' ); ?></span>
     123                        <span style="font-size:12px; font-weight:600;"><?php echo $is_pro ? esc_html__( 'WebP quality', 'enfants-img-performer' ) : esc_html__( 'Quality', 'enfants-img-performer' ); ?></span>
    123124                        <span style="font-size:11px; color:#666670;"><?php esc_html_e( 'Recommended: 70–85', 'enfants-img-performer' ); ?></span>
    124125                </div>
    … …  
    490491                                </div>
    491492                                <div style="display:flex; gap:90px; flex-wrap:wrap; margin-bottom:24px;">
    492                                         <div><?php $this->render_quality_field(); ?></div>
     493                                        <div>
     494                                                <?php
     495                                                $this->render_quality_field();
     496                                                /**
     497                                                 * Lets PRO add its AVIF quality slider under the WebP slider, inside
     498                                                 * this form so both share the one Save button (IMP-326).
     499                                                 */
     500                                                do_action( 'eimp_after_quality_field' );
     501                                                ?>
     502                                        </div>
    493503                                        <div style="flex:1 0 0; min-width:240px;"><?php $this->render_stats_opt_in_field(); ?></div>
    494504                                </div>
  • enfants-img-performer/trunk/includes/Core/AiLabelBadge.php

    r3786650 r3798740  
    8383                        // there's nothing for that margin to usefully do here anyway.
    8484                        . '.eimp-ai-badge-wrap img{display:block;margin:0}'
     85                        // Floating Label (IMP-315): small "AI" pill at rest, grows into
     86                        // the full status pill when expanded. Without JS: hover/focus.
     87                        // With JS (see print_floating_label_js) aria-expanded drives it,
     88                        // so Esc/Enter/tap work. prefers-reduced-motion: no animation.
     89                        . '.eimp-ai-badge--floating{cursor:default;outline-offset:2px}'
     90                        . '.eimp-ai-badge--floating svg{display:block}'
     91                        // Soft transition (IMP-315, design by Alex): the pill grows from
     92                        // 35px to full width while the text fades in along a soft mask
     93                        // edge. Only one SVG is visible at a time; the large one is
     94                        // offset by 1.5px/-.156px so "AI" sits exactly on the small one.
     95                        // Pill colour comes from the badge (--imgp-bg), because the mask
     96                        // would otherwise fade the SVG's own pill. Pure CSS, no library.
     97                        . '@property --imgp-p{syntax:"<length>";inherits:false;initial-value:24px}'
     98                        . '.eimp-ai-badge--floating{--imgp-rest:35px;--imgp-full:105.5px;--imgp-bg:#fff;position:absolute;display:block;overflow:hidden;border-radius:10px;height:20px;width:var(--imgp-rest);background:var(--imgp-bg);contain:layout paint;transition:width calc(var(--imgp-k,1)*var(--imgp-dc,460ms)*.85) var(--imgp-ease-c,cubic-bezier(.45,0,.2,1)) calc(var(--imgp-k,1)*var(--imgp-dc,460ms)*.15)}'
     99                        . '.eimp-ai-badge--floating>span{position:absolute;top:0;left:0;display:block}'
     100                        . '.eimp-ai-badge--floating .eimp-ai-badge__rest{opacity:1;transition:opacity 0s calc(var(--imgp-k,1)*var(--imgp-dc,460ms))}'
     101                        . '.eimp-ai-badge--floating .eimp-ai-badge__full{opacity:0;transform:translate(1.5px,-.156px);--imgp-p:24px;'
     102                        . '-webkit-mask-image:linear-gradient(90deg,#000 23.5px,transparent 23.5px),linear-gradient(90deg,#000 calc(var(--imgp-p) - var(--imgp-soft,24px)),transparent var(--imgp-p));'
     103                        . 'mask-image:linear-gradient(90deg,#000 23.5px,transparent 23.5px),linear-gradient(90deg,#000 calc(var(--imgp-p) - var(--imgp-soft,24px)),transparent var(--imgp-p));'
     104                        . 'transition:--imgp-p calc(var(--imgp-k,1)*var(--imgp-dc,460ms)*.8) cubic-bezier(.4,0,.6,1),opacity 0s calc(var(--imgp-k,1)*var(--imgp-dc,460ms))}'
     105                        . '.eimp-ai-badge--floating[aria-expanded="true"],.eimp-ai-badge--floating:not([aria-expanded]):hover,.eimp-ai-badge--floating:not([aria-expanded]):focus{width:var(--imgp-full);transition:width calc(var(--imgp-k,1)*var(--imgp-do,520ms)) var(--imgp-ease-o,cubic-bezier(.2,0,0,1))}'
     106                        . '.eimp-ai-badge--floating[aria-expanded="true"] .eimp-ai-badge__rest,.eimp-ai-badge--floating:not([aria-expanded]):hover .eimp-ai-badge__rest,.eimp-ai-badge--floating:not([aria-expanded]):focus .eimp-ai-badge__rest{opacity:0;transition:none}'
     107                        . '.eimp-ai-badge--floating[aria-expanded="true"] .eimp-ai-badge__full,.eimp-ai-badge--floating:not([aria-expanded]):hover .eimp-ai-badge__full,.eimp-ai-badge--floating:not([aria-expanded]):focus .eimp-ai-badge__full{opacity:1;--imgp-p:calc(var(--imgp-full) + var(--imgp-soft,24px));transition:--imgp-p calc(var(--imgp-k,1)*var(--imgp-do,520ms)*var(--imgp-tf-o,.8)) var(--imgp-te-o,cubic-bezier(.25,0,.1,1)) 0s,opacity 0s}'
     108                        . '@media (prefers-reduced-motion:reduce){.eimp-ai-badge--floating,.eimp-ai-badge--floating>span{transition:none!important}}'
     109                        // PRO Official Label: large pill by default, small Floating Label
     110                        // below 500px image width (IMP-316).
     111                        . '.eimp-ai-badge--floating.eimp-ai-badge--official-small{display:none}'
     112                        . '@container (max-width:499.98px){.eimp-ai-badge--official-large{display:none}.eimp-ai-badge--floating.eimp-ai-badge--official-small{display:inline-block}}'
     113                        // Size-dependent labels (IMP-316): on images narrower than 200px
     114                        // the label is scaled to 70% and sits 4px closer to the corner.
     115                        // Container query on .eimp-ai-badge-box (the image-sized overlay).
     116                        . '@container (max-width:199.98px){'
     117                        . '.eimp-ai-badge--floating,.eimp-ai-badge--circle{transform:scale(.7)}'
     118                        . '.eimp-ai-badge--pos-top-left{transform-origin:top left;margin:-4px 0 0 -4px}'
     119                        . '.eimp-ai-badge--pos-top-right{transform-origin:top right;margin:-4px -4px 0 0}'
     120                        . '.eimp-ai-badge--pos-bottom-left{transform-origin:bottom left;margin:0 0 -4px -4px}'
     121                        . '.eimp-ai-badge--pos-bottom-right{transform-origin:bottom right;margin:0 -4px -4px 0}'
     122                        . '}'
    85123                        . '</style>';
     124                $this->print_floating_label_js();
     125        }
     126
     127        /**
     128         * Progressive enhancement for the Floating Label (IMP-315). The CSS
     129         * hover/focus fallback already works; this adds what CSS can't:
     130         * button semantics + aria-expanded, Enter/Space/click toggle, Esc to
     131         * dismiss (WCAG 1.4.13: dismissible, hoverable, persistent), tap
     132         * outside to close, and no navigation when the label sits inside a link.
     133         */
     134        private function print_floating_label_js(): void {
     135                $is_pro = (bool) apply_filters( 'eimp_is_pro_active', false );
     136                if ( ! (bool) get_option( 'eimp_ai_label_enabled', 1 ) || ( ! $is_pro && 'floating' !== $this->get_design( false ) ) ) {
     137                        return;
     138                }
     139                echo '<script id="eimp-ai-badge-floating-js">'
     140                        . '(function(){var S=".eimp-ai-badge--floating",t=0;'
     141                        . 'function o(b){return b.getAttribute("aria-expanded")==="true"}'
     142                        . 'function set(b,v){b.setAttribute("aria-expanded",v?"true":"false");if(v)t=Date.now()}'
     143                        . 'function close(b){set(b,false)}'
     144                        . 'document.querySelectorAll(S).forEach(function(b){'
     145                        . 'b.setAttribute("role","button");b.setAttribute("aria-expanded","false");'
     146                        . 'b.addEventListener("pointerenter",function(e){if(e.pointerType!=="touch"&&!b.__d)set(b,true)});'
     147                        . 'b.addEventListener("pointerleave",function(e){if(e.pointerType==="touch")return;b.__d=false;if(document.activeElement!==b)close(b)});'
     148                        . 'b.addEventListener("focus",function(){b.__d=false;set(b,true)});'
     149                        . 'b.addEventListener("blur",function(){b.__d=false;close(b)});'
     150                        . 'b.addEventListener("click",function(e){if(b.closest("a")){e.preventDefault();e.stopPropagation()}if(Date.now()-t<400)return;set(b,!o(b))});'
     151                        . 'b.addEventListener("keydown",function(e){if(e.key==="Enter"||e.key===" "){e.preventDefault();set(b,!o(b))}else if(e.key==="Escape"&&o(b)){e.stopPropagation();b.__d=true;close(b)}});'
     152                        . '});'
     153                        . 'document.addEventListener("click",function(e){document.querySelectorAll(S+"[aria-expanded=true]").forEach(function(b){if(!b.contains(e.target))close(b)})});'
     154                        . '})();</script>';
    86155        }
    87156
    … …  
    311380
    312381                return (string) preg_replace_callback(
    313                         '/<div\s[^>]*\bclass="[^"]*\bwp-block-cover__image-background\b[^"]*"[^>]*>\s*<\/div>(?!\s*<span class="eimp-ai-badge")/i',
     382                        '/<div\s[^>]*\bclass="[^"]*\bwp-block-cover__image-background\b[^"]*"[^>]*>\s*<\/div>(?!\s*<span class="eimp-ai-badge-box")/i',
    314383                        function ( array $m ): string {
    315384                                if ( ! preg_match( '/\bwp-image-(\d+)\b/', $m[0], $id_match ) ) {
    … …  
    326395        }
    327396
     397        /**
     398         * Wraps AI-labeled <img> tags with the badge. Images that already sit
     399         * directly inside a badge wrapper (e.g. Featured Images, which
     400         * inject_thumbnail_badge() wrapped before the page scan runs) are hidden
     401         * from the scan, otherwise they get a second wrapper and a second badge:
     402         * invisible with the Circle/Official label (identical, stacked), but with
     403         * the Floating Label the resting twin shows through the opened one.
     404         *
     405         * @param string $html Full page HTML.
     406         * @return string
     407         */
    328408        private function inject_image_badges( string $html ): string {
     409
     410                $protected = [];
     411                $html      = (string) preg_replace_callback(
     412                        '/(<span class="eimp-ai-badge-wrap[^>]*>)(<img\s[^>]*>)/i',
     413                        static function ( array $m ) use ( &$protected ): string {
     414                                $key               = '<!--eimp-wrapped-' . count( $protected ) . '-->';
     415                                $protected[ $key ] = $m[2];
     416                                return $m[1] . $key;
     417                        },
     418                        $html
     419                );
     420
     421                $html = $this->inject_image_badges_scan( $html );
     422
     423                return empty( $protected ) ? $html : str_replace( array_keys( $protected ), array_values( $protected ), $html );
     424        }
     425
     426        private function inject_image_badges_scan( string $html ): string {
    329427
    330428                $upload_baseurl = wp_get_upload_dir()['baseurl'] ?? '';
    … …  
    507605                }
    508606
     607                // Cover block with the Featured Image as <img>: core CSS pins everything
     608                // with .wp-block-cover__image-background absolutely over the whole cover.
     609                // A relative wrapper carrying that class (and a copy of the img style)
     610                // would sit in normal flow with height 0 and, via overflow:hidden, cut
     611                // the photo off – the hero turns empty. Here the wrapper fills the cover
     612                // itself (no z-index, so the <img> keeps core's own layering), does not
     613                // inherit the cover class, and the badge lifts above the dim overlay
     614                // and inner container, same as in the background-image branch.
     615                $is_cover_bg = in_array( 'wp-block-cover__image-background', preg_split( '/\s+/', trim( $img_class ) ), true );
     616                if ( $is_cover_bg ) {
     617                        $wrap_class = 'eimp-ai-badge-wrap' . ( '' !== trim( str_replace( 'wp-block-cover__image-background', '', $img_class ) ) ? ' ' . trim( preg_replace( '/\s+/', ' ', str_replace( 'wp-block-cover__image-background', '', $img_class ) ) ) : '' );
     618                        $wrap_style = 'position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;line-height:0;';
     619                        return '<span class="' . $wrap_class . '" style="' . $wrap_style . '">'
     620                                . $img_tag
     621                                . $this->build_badge_span( $status, 'z-index:2;' )
     622                                . '</span>';
     623                }
     624
    509625                $wrap_class = 'eimp-ai-badge-wrap' . ( '' !== $img_class ? ' ' . $img_class : '' );
    510626                // overflow:hidden is the actual guarantee that an oversized custom
    … …  
    528644
    529645        /**
     646         * Effective label design: 'circle' (Free default / PRO official pill) or
     647         * 'floating'. PRO can steer it via the eimp_ai_badge_design filter.
     648         *
     649         * @param bool $is_pro Whether PRO is active.
     650         * @return string
     651         */
     652        private function get_design( bool $is_pro ): string {
     653                $design = (string) get_option( 'eimp_ai_label_design', 'circle' );
     654                $design = (string) apply_filters( 'eimp_ai_badge_design', $design, $is_pro );
     655                return 'floating' === $design ? 'floating' : 'circle';
     656        }
     657
     658        /**
    530659         * Build the visible badge element itself (label, position, icon incl.
    531660         * PRO filters), shared by the <img> wrapper and the Cover-block pass.
    … …  
    542671
    543672                $is_pro   = (bool) apply_filters( 'eimp_is_pro_active', false );
    544                 $position = $is_pro ? (string) get_option( 'eimp_ai_label_position', 'bottom-right' ) : 'top-left';
     673                $design   = $this->get_design( $is_pro );
     674                // Free offers two corners (IMP-315); the other two stay PRO. A stored
     675                // PRO-only corner (e.g. after a licence lapse) falls back to top-left.
     676                $position = $is_pro
     677                        ? (string) get_option( 'eimp_ai_label_position', 'bottom-right' )
     678                        : (string) get_option( 'eimp_ai_label_position', 'top-left' );
     679                if ( ! $is_pro && ! in_array( $position, [ 'top-left', 'bottom-right' ], true ) ) {
     680                        $position = 'top-left';
     681                }
    545682                $color    = $is_pro ? (string) get_option( 'eimp_ai_label_color', 'white' ) : 'white';
    546683                $contrast = $is_pro ? (string) get_option( 'eimp_ai_label_contrast', 'normal' ) : 'normal';
    … …  
    560697                $offset = apply_filters( 'eimp_ai_badge_offset', $offset, $status, $position, $is_pro );
    561698
    562                 $icon = $is_pro
    563                         ? $this->render_pill_icon( $status, $color, $contrast )
    564                         : $this->render_circle_icon();
     699                $official_pill = null;
     700                if ( 'floating' === $design ) {
     701                        $icon = $this->render_floating_content( $status, $color, $contrast );
     702                } elseif ( $is_pro ) {
     703                        $icon          = $this->render_pill_icon( $status, $color, $contrast );
     704                        $official_pill = $icon;
     705                } else {
     706                        $icon = $this->render_circle_icon();
     707                }
    565708
    566709                // Lets PRO substitute a user-uploaded custom label image for the
    … …  
    572715                // width axis, and to shrink the visible/clipped-to portion
    573716                // gracefully rather than always clipping at an arbitrary point.
    574                 return '<span class="eimp-ai-badge" role="img" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;max-height:100%;pointer-events:none;line-height:0;' . $extra_style . '">'
     717                // Overlay that fills the image/cover and acts as the CSS container
     718                // for the size-dependent rules (IMP-316). Absolutely positioned, so
     719                // size containment can't collapse the shrink-to-fit wrapper.
     720                // container-type makes the box its own stacking context, so a z-index the
     721                // caller wants (Cover block: above dim overlay + inner container) has to
     722                // sit on the box itself – on the pill it would only act inside the box.
     723                $box_z     = preg_match( '/z-index:\s*(\d+)\s*;/', $extra_style, $zm ) ? 'z-index:' . (int) $zm[1] . ';' : '';
     724                $box_open  = '<span class="eimp-ai-badge-box" style="position:absolute;top:0;right:0;bottom:0;left:0;container-type:inline-size;pointer-events:none;' . $box_z . '">';
     725                $box_close = '</span>';
     726                $pos_class = 'eimp-ai-badge--pos-' . ( isset( $position_css[ $position ] ) ? $position : 'top-left' );
     727
     728                if ( 'floating' === $design ) {
     729                        // Focusable + hover-able, so the CSS-only expand works for mouse,
     730                        // keyboard and tap even without JS. The inline script in the
     731                        // footer upgrades it (button role, Enter/Space/Esc, aria-expanded).
     732                        $extra_style .= 'pointer-events:auto;' . $this->floating_vars( $status, $color, $contrast );
     733                        return $box_open . '<span class="eimp-ai-badge eimp-ai-badge--floating ' . $pos_class . '" role="img" tabindex="0" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;' . $extra_style . '">'
     734                                . $icon
     735                                . '</span>' . $box_close;
     736                }
     737
     738                // PRO Official Label (unchanged built-in pill): on images narrower
     739                // than 500px swap to the small Floating Label instead (IMP-316). Both
     740                // variants are rendered; a container query shows only one, and the
     741                // hidden one is display:none (not in the accessibility tree).
     742                if ( null !== $official_pill && $icon === $official_pill ) {
     743                        $small_style = 'position:absolute;' . $offset . 'max-width:100%;pointer-events:auto;' . $this->floating_vars( $status, $color, $contrast );
     744                        return $box_open
     745                                . '<span class="eimp-ai-badge eimp-ai-badge--official-large" role="img" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;max-height:100%;pointer-events:none;line-height:0;' . $extra_style . '">'
     746                                . $icon
     747                                . '</span>'
     748                                . '<span class="eimp-ai-badge eimp-ai-badge--floating eimp-ai-badge--official-small ' . $pos_class . '" role="img" tabindex="0" aria-label="' . esc_attr( $label ) . '" style="' . $small_style . '">'
     749                                . $this->render_floating_content( $status, $color, $contrast )
     750                                . '</span>'
     751                                . $box_close;
     752                }
     753
     754                $circle_class = ( ! $is_pro && 'circle' === $design ) ? ' eimp-ai-badge--circle ' . $pos_class : '';
     755
     756                return $box_open . '<span class="eimp-ai-badge' . $circle_class . '" role="img" aria-label="' . esc_attr( $label ) . '" style="position:absolute;' . $offset . 'max-width:100%;max-height:100%;pointer-events:none;line-height:0;' . $extra_style . '">'
    575757                        . $icon
    576                         . '</span>';
     758                        . '</span>' . $box_close;
     759        }
     760
     761        /**
     762         * Inner markup of the Floating Label: "AI" at rest, expands to
     763         * "AI generated" / "AI modified" on hover/focus (CSS only, see
     764         * print_gallery_crop_fix_css()). English only by design (IMP-315).
     765         *
     766         * @param string $status AiLabelClassifier::STATUS_AI_GENERATED or STATUS_AI_MODIFIED.
     767         * @return string
     768         */
     769        private function render_floating_content( string $status, string $color = 'white', string $contrast = 'normal' ): string {
     770                return '<span class="eimp-ai-badge__rest">' . $this->render_floating_rest_icon( $color, $contrast, false ) . '</span>'
     771                        . '<span class="eimp-ai-badge__full">' . $this->render_pill_icon( $status, $color, $contrast, false ) . '</span>';
     772        }
     773
     774        /**
     775         * Inline CSS variables for the Floating Label transition: full width
     776         * (SVG width + 1.5px offset) and the pill colour, which the badge itself
     777         * paints (the SVGs are rendered without their own pill).
     778         */
     779        private function floating_vars( string $status, string $color, string $contrast ): string {
     780                $full = ( AiLabelClassifier::STATUS_AI_GENERATED === $status ? 104 : 92.4 ) + 1.5;
     781                $rgb  = 'black' === $color ? '0,0,0' : '255,255,255';
     782                $bg   = 'transparent' === $contrast ? 'rgba(' . $rgb . ',.5)' : 'rgb(' . $rgb . ')';
     783                return '--imgp-full:' . $full . 'px;--imgp-bg:' . $bg . ';';
     784        }
     785
     786        /**
     787         * Resting state of the Floating Label: small 35x20 pill with the "AI"
     788         * glyph, traced from the Figma "Ai-Label / white subtle" variant. Public
     789         * so PRO's settings preview can reuse it. Color/contrast work like the
     790         * regular pill (PRO only; Free always passes white/normal).
     791         *
     792         * @param string $color    'white' or 'black'.
     793         * @param string $contrast 'normal' or 'transparent'.
     794         * @return string SVG markup.
     795         */
     796        public function render_floating_rest_icon( string $color, string $contrast, bool $with_bg = true ): string {
     797                $bg_fill    = 'black' === $color ? '#000000' : '#ffffff';
     798                $glyph_fill = 'black' === $color ? '#ffffff' : '#000000';
     799                $bg_opacity = 'transparent' === $contrast ? '0.5' : '1';
     800                return '<svg width="35" height="20" viewBox="0 0 35 20" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">'
     801                        . ( $with_bg ? '<path d="M0 10C0 4.47715 4.47715 0 10 0H25C30.5228 0 35 4.47715 35 10V10C35 15.5228 30.5228 20 25 20H10C4.47715 20 0 15.5228 0 10V10Z" fill="' . esc_attr( $bg_fill ) . '" fill-opacity="' . esc_attr( $bg_opacity ) . '"/>' : '' )
     802                        . '<path d="M15.735 5.03442C15.8908 5.03447 16.0124 5.07277 16.1002 5.15063C16.1873 5.2287 16.2453 5.31958 16.273 5.42017L19.5006 14.2756C19.5185 14.3123 19.5279 14.354 19.5279 14.3997C19.5278 14.473 19.4981 14.5397 19.4391 14.5989C19.3791 14.6588 19.3076 14.6877 19.2252 14.6877H17.5279C17.3899 14.6877 17.2866 14.6533 17.2184 14.5842C17.1502 14.5153 17.1053 14.4534 17.0865 14.3987L16.5484 12.9915H12.8658L12.3414 14.3987C12.3226 14.4534 12.281 14.5153 12.2174 14.5842C12.1528 14.6533 12.0425 14.6877 11.8863 14.6877H10.1891C10.1156 14.6877 10.0491 14.658 9.98984 14.5989C9.92992 14.5389 9.90012 14.473 9.9 14.3997C9.9 14.354 9.90471 14.3123 9.91367 14.2756L13.1549 5.42017C13.1827 5.31883 13.2399 5.2287 13.3277 5.15063C13.4148 5.07264 13.5371 5.03442 13.6939 5.03442H15.735ZM22.8092 5.03149C22.9105 5.0315 22.9936 5.06362 23.0582 5.12817C23.1228 5.19273 23.1549 5.27587 23.1549 5.3772V14.343C23.1548 14.4441 23.1226 14.5266 23.0582 14.5911C22.9936 14.6556 22.9105 14.6877 22.8092 14.6877H20.9615C20.8602 14.6877 20.7771 14.6556 20.7125 14.5911C20.6481 14.5266 20.6159 14.4441 20.6158 14.343V5.3772C20.6158 5.27589 20.648 5.19272 20.7125 5.12817C20.7771 5.06362 20.8602 5.03149 20.9615 5.03149H22.8092ZM13.4312 11.0051H15.9967L14.6998 7.36353L13.4312 11.0051Z" fill="' . esc_attr( $glyph_fill ) . '"/>'
     803                        . '</svg>';
    577804        }
    578805
    … …  
    583810         */
    584811        private function render_circle_icon(): string {
    585                 return '<svg width="50" height="50" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">'
     812                return '<svg width="30" height="30" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">'
    586813                        . '<path fill-rule="evenodd" clip-rule="evenodd" d="M25.0007 0C38.8068 0 50 11.1932 50 25.0007C50 38.8082 38.8068 50 25.0007 50C11.1946 50 0 38.8068 0 25.0007C0 11.1946 11.1932 0 25.0007 0Z" fill="#ffffff"/>'
    587814                        . '<path d="M22.229 15.3242C22.5408 15.3243 22.7844 15.4027 22.9595 15.5586C23.133 15.7145 23.2481 15.8934 23.3042 16.0957L29.7524 33.7871C29.7894 33.861 29.8081 33.9435 29.8081 34.0352C29.808 34.1828 29.7473 34.3157 29.6284 34.4346C29.5095 34.5535 29.3671 34.6133 29.2017 34.6133H25.812C25.5372 34.6133 25.3301 34.5453 25.1919 34.4072C25.0538 34.2691 24.9658 34.1459 24.9302 34.0352L23.855 31.2236H16.4985L15.4517 34.0352C15.4147 34.1459 15.3307 34.2692 15.2036 34.4072C15.075 34.5451 14.855 34.6132 14.5435 34.6133H11.1509C11.0033 34.6133 10.8704 34.5547 10.7515 34.4346C10.6312 34.3157 10.5719 34.1828 10.5718 34.0352C10.5718 33.9435 10.5813 33.861 10.5991 33.7871L17.0757 16.0957C17.1304 15.8933 17.2454 15.7145 17.4204 15.5586C17.5941 15.4026 17.8376 15.3242 18.1509 15.3242H22.229ZM36.3647 15.3232C36.5669 15.3233 36.731 15.3873 36.8608 15.5156C36.9894 15.6442 37.0542 15.8102 37.0542 16.0127V33.9238C37.0542 34.1263 36.9894 34.2923 36.8608 34.4209C36.7323 34.5493 36.567 34.6132 36.3647 34.6133H32.6724C32.4699 34.6133 32.3039 34.5495 32.1753 34.4209C32.0468 34.2923 31.9829 34.1262 31.9829 33.9238V16.0127C31.9829 15.8102 32.0467 15.6456 32.1753 15.5156C32.3039 15.3857 32.4699 15.3232 32.6724 15.3232H36.3647ZM17.6284 27.2559H22.7524L20.1626 19.9814L17.6284 27.2559Z" fill="#000000"/>'
    … …  
    605832         * @return string SVG markup.
    606833         */
    607         public function render_pill_icon( string $status, string $color, string $contrast ): string {
     834        public function render_pill_icon( string $status, string $color, string $contrast, bool $with_bg = true ): string {
    608835
    609836                $generated = AiLabelClassifier::STATUS_AI_GENERATED === $status;
    … …  
    623850
    624851                return '<svg width="' . esc_attr( $width ) . '" height="20" viewBox="0 0 ' . esc_attr( $width ) . ' 20" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false">'
    625                         . '<path d="' . $bg_path . '" fill="' . esc_attr( $bg_fill ) . '" fill-opacity="' . esc_attr( $bg_opacity ) . '"/>'
     852                        . ( $with_bg ? '<path d="' . $bg_path . '" fill="' . esc_attr( $bg_fill ) . '" fill-opacity="' . esc_attr( $bg_opacity ) . '"/>' : '' )
    626853                        . '<path d="' . $text_path . '" fill="' . esc_attr( $text_fill ) . '"/>'
    627854                        . '</svg>';
  • enfants-img-performer/trunk/readme.txt

    r3786650 r3798740  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.3.15
     7Stable tag: 0.3.16
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    238238== Changelog ==
    239239
     240= 0.3.16 =
     241* New: Floating Label as a second design for the visible AI label. At rest it is a small "AI" pill; on hover, focus or tap it grows smoothly into "AI generated" or "AI modified". Works with mouse, keyboard (Enter, Space, Esc) and touch, exposes aria-expanded to screen readers, and skips the animation when "reduce motion" is set.
     242* New: Choose the label design (Circle or Floating) and its position (top left or bottom right) under "Visible label on the frontend".
     243* New: Labels adapt to the image width. On images narrower than 200 px the label is smaller and sits closer to the corner.
     244* Changed: The Circle label is slightly smaller.
     245* Fixed: Featured images received two stacked label layers. With the Circle label this went unnoticed, with the Floating Label the second layer showed through.
     246* Fixed: A featured image inside a Cover block (hero) was cut off when it carried the AI label, leaving the hero empty. The image fills the Cover again, and the label sits above the Cover's dim overlay, also for Covers with a fixed background.
     247
    240248= 0.3.15 =
    241249* Improved: Faster front-end on sites with large media libraries. The plugin no longer looks up the same image in the database over and over on every page view. It now remembers the result (up to 7 days) and clears this memory automatically whenever an image is uploaded, edited, replaced or deleted. On a large test site this cut the plugin's database time on the home page from about 0.7 s to almost zero.
Note: See TracChangeset for help on using the changeset viewer.