Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/renderer-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@
"artisanpack/grid",
"artisanpack/grid-item",
"artisanpack/next-post",
"artisanpack/previous-post"
"artisanpack/previous-post",
"artisanpack/copyright",
"artisanpack/marquee",
"artisanpack/comments-number"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Front-end styles for the marquee block (artisanpack/marquee — #500).
*
* Mirrors the editor preview styles in
* `resources/js/visual-editor/blocks/marquee/marquee.css`. Host apps
* load this stylesheet on any page that renders a marquee block — the
* `<x-ve-blocks-styles />` component emits the `<link>`.
*
* The block's `marqueeSpeed` attribute lands as an inline
* `animation: ap-marquee-scroll Ns linear infinite` on the inner `<p>`,
* so the keyframe definition here is all the frontend needs.
*/

.ap-marquee {
margin-left: auto;
margin-right: auto;
overflow: hidden;
}

.ap-marquee__text {
margin: 0;
transform: translateX(100%);
white-space: nowrap;
will-change: transform;
}

@keyframes ap-marquee-scroll {
from {
transform: translateX(100%);
}
to {
transform: translateX(-100%);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/* Respect prefers-reduced-motion. Visitors who've opted out of
animations get the text pinned in place inside the wrapper instead
of an endless horizontal scroll — the inline `animation` declaration
on `.ap-marquee__text` is overridden and the base
`translateX(100%)` (which hides the text off-screen) is reset. */
@media (prefers-reduced-motion: reduce) {
.ap-marquee__text {
animation: none !important;
transform: none;
white-space: normal;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@php
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;

$rawCount = $attributes['_resolvedCommentCount'] ?? 0;
$count = is_numeric( $rawCount ) ? (int) $rawCount : 0;

if ( $count < 0 ) {
$count = 0;
}

$singular = (string) ( $attributes['singularCommentText'] ?? 'Comment' );
$plural = (string) ( $attributes['pluralCommentText'] ?? 'Comments' );
$label = 1 === $count ? $singular : $plural;

$line = trim( $count . ' ' . $label );

$baseClasses = [ 'ap-comments-number' ];
@endphp
<p{!! BlockSupports::wrapperAttrs( $attributes, $baseClasses ) !!}>{{ $line }}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@php
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;

$validTypes = [ 'icon-text', 'icon-only', 'text-only' ];

$copyrightType = (string) ( $attributes['copyrightType'] ?? 'icon-text' );

if ( ! in_array( $copyrightType, $validTypes, true ) ) {
$copyrightType = 'icon-text';
}

$copyrightText = trim( (string) ( $attributes['copyrightText'] ?? 'Copyright' ) );

$currentYear = gmdate( 'Y' );

if ( 'icon-only' === $copyrightType ) {
$line = '© ' . $currentYear;
} elseif ( 'text-only' === $copyrightType ) {
$line = '' === $copyrightText ? $currentYear : $copyrightText . ' ' . $currentYear;
} else {
$line = '' === $copyrightText
? '© ' . $currentYear
: '© ' . $copyrightText . ' ' . $currentYear;
}

$baseClasses = [ 'ap-copyright' ];
@endphp
<p{!! BlockSupports::wrapperAttrs( $attributes, $baseClasses ) !!}>{{ $line }}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@php
use ArtisanPackUI\VisualEditorRendererBlade\Support\BlockSupports;

$rawWidth = $attributes['marqueeWidth'] ?? 100;
$width = is_numeric( $rawWidth ) ? (int) round( (float) $rawWidth ) : 100;
$width = max( 1, min( 100, $width ) );

$rawSpeed = $attributes['marqueeSpeed'] ?? 5;
$speed = is_numeric( $rawSpeed ) ? (int) round( (float) $rawSpeed ) : 5;
$speed = max( 1, min( 100, $speed ) );

$content = (string) ( $attributes['marqueeContent'] ?? '' );

$baseClasses = [ 'ap-marquee' ];

// The wrapper width is a per-instance inline declaration that has
// to live alongside any block-supports style (background, padding,
// border) on the same `style="…"` attribute — emit them as one
// declaration so a second `style="…"` doesn't override the first.
$compiled = BlockSupports::compile( $attributes );
$classes = array_values( array_unique( array_merge( $baseClasses, $compiled['classes'] ) ) );
$styles = '' !== $compiled['style'] ? rtrim( $compiled['style'], ';' ) : '';
$styles = ( '' !== $styles ? $styles . '; ' : '' ) . 'width: ' . $width . '%';

$classAttr = sprintf( ' class="%s"', e( implode( ' ', $classes ) ) );
$styleAttr = sprintf( ' style="%s"', e( $styles . ';' ) );
$idAttr = null !== $compiled['id'] ? sprintf( ' id="%s"', e( $compiled['id'] ) ) : '';

$textStyle = 'animation: ap-marquee-scroll ' . $speed . 's linear infinite;';
@endphp
<div{!! $classAttr . $styleAttr . $idAttr !!}>
<p class="ap-marquee__text" style="{{ $textStyle }}">{!! $content !!}</p>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
loads independently of $emitInteractive. Hosts that opt out of
accordion/tabs interactivity still want grid blocks to render. --}}
<link rel="stylesheet" href="{{ $gridStyleHref }}" data-ve-grid>
{{-- Marquee CSS ships the keyframes the inline `animation` declaration
on the inner `<p>` references. Layout-only, same story as grid. --}}
<link rel="stylesheet" href="{{ $marqueeStyleHref }}" data-ve-marquee>
@if( '' !== $themeTokensCss )
<style data-ve-theme-tokens>{!! $themeTokensCss !!}</style>
@endif
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class BlocksStylesComponent extends Component

public string $gridStyleHref;

public string $marqueeStyleHref;

public string $interactivityScriptSrc;

public bool $emitBlockLibrary;
Expand Down Expand Up @@ -96,6 +98,7 @@ public function __construct(
$this->accordionStyleHref = $base . '/frontend/accordion.css';
$this->tabsStyleHref = $base . '/frontend/tabs.css';
$this->gridStyleHref = $base . '/frontend/grid.css';
$this->marqueeStyleHref = $base . '/frontend/marquee.css';
$this->interactivityScriptSrc = $base . '/frontend/interactivity.js';
$this->emitBlockLibrary = $bundle;
$this->emitInteractive = $interactive;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@
->not->toContain( 'data-ve-block-library' );
} );

it( 'emits the grid + marquee frontend stylesheet links independently of $emitInteractive', function () {
$rendered = Blade::render( '<x-ve-blocks-styles :interactive="false" />' );

expect( $rendered )
->toContain( '/vendor/visual-editor-renderer-blade/frontend/grid.css' )
->toContain( 'data-ve-grid' )
->toContain( '/vendor/visual-editor-renderer-blade/frontend/marquee.css' )
->toContain( 'data-ve-marquee' )
->not->toContain( 'data-ve-accordion' )
->not->toContain( 'data-ve-tabs' );
} );

it( 'rebases the link href when assetBase is supplied', function () {
$rendered = Blade::render(
'<x-ve-blocks-styles asset-base="https://cdn.example.com/ve" />'
Expand Down
126 changes: 126 additions & 0 deletions packages/visual-editor-renderer-blade/tests/Unit/BlockRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,132 @@ public function render( array $attrs )
->and( $html )->not->toContain( '<li class="ap-breadcrumbs__item' );
} );

it( 'renders the artisanpack/copyright block with the © + text + current year (icon-text default)', function () {
$tree = [
makeBlock( 'artisanpack/copyright', [
'copyrightType' => 'icon-text',
'copyrightText' => 'Acme, Inc.',
] ),
];

$year = gmdate( 'Y' );
$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'class="ap-copyright"' )
->and( $html )->toContain( '© Acme, Inc. ' . $year )
->and( $html )->toContain( '<p' );
} );

it( 'omits the text when copyright type is icon-only and drops the icon when text-only', function () {
$iconOnly = makeRenderer()->render( [
makeBlock( 'artisanpack/copyright', [
'copyrightType' => 'icon-only',
'copyrightText' => 'Should not appear',
] ),
] );

$textOnly = makeRenderer()->render( [
makeBlock( 'artisanpack/copyright', [
'copyrightType' => 'text-only',
'copyrightText' => 'Plain',
] ),
] );

$year = gmdate( 'Y' );

expect( $iconOnly )->toContain( '© ' . $year )
->and( $iconOnly )->not->toContain( 'Should not appear' )
->and( $textOnly )->toContain( 'Plain ' . $year )
->and( $textOnly )->not->toContain( '©' );
} );

it( 'falls back to icon-text when copyright type is invalid', function () {
$tree = [
makeBlock( 'artisanpack/copyright', [
'copyrightType' => 'made-up-mode',
'copyrightText' => 'Fallback',
] ),
];

$year = gmdate( 'Y' );
$html = makeRenderer()->render( $tree );

expect( $html )->toContain( '© Fallback ' . $year );
} );

it( 'renders the artisanpack/marquee block with width + animation styles applied', function () {
$tree = [
makeBlock( 'artisanpack/marquee', [
'marqueeContent' => 'Breaking news',
'marqueeWidth' => 60,
'marqueeSpeed' => 10,
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'class="ap-marquee"' )
->and( $html )->toContain( 'width: 60%' )
->and( $html )->toContain( 'animation: ap-marquee-scroll 10s linear infinite' )
->and( $html )->toContain( 'class="ap-marquee__text"' )
->and( $html )->toContain( 'Breaking news' );
} );

it( 'clamps marquee width and speed to their valid range and falls back on non-numeric input', function () {
$tree = [
makeBlock( 'artisanpack/marquee', [
'marqueeContent' => 'x',
'marqueeWidth' => 999,
'marqueeSpeed' => 'fast',
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'width: 100%' )
->and( $html )->toContain( 'animation: ap-marquee-scroll 5s linear infinite' );
} );

it( 'renders the artisanpack/comments-number block with the resolved count and plural label', function () {
$tree = [
makeBlock( 'artisanpack/comments-number', [
'_resolvedCommentCount' => 5,
'singularCommentText' => 'Reply',
'pluralCommentText' => 'Replies',
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( 'class="ap-comments-number"' )
->and( $html )->toContain( '5 Replies' )
->and( $html )->not->toContain( 'Reply<' );
} );

it( 'uses the singular comments-number label when the resolved count is exactly one', function () {
$tree = [
makeBlock( 'artisanpack/comments-number', [
'_resolvedCommentCount' => 1,
'singularCommentText' => 'Reply',
'pluralCommentText' => 'Replies',
] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( '1 Reply' );
} );

it( 'falls back to zero comments and default labels when the resolved count is missing', function () {
$tree = [
makeBlock( 'artisanpack/comments-number', [] ),
];

$html = makeRenderer()->render( $tree );

expect( $html )->toContain( '0 Comments' );
} );

it( 'renders an artisanpack/accordions tree with its nested panel + title/body grandchildren', function () {
$tree = [
makeBlock( 'artisanpack/accordions', [], [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* React renderer for the `artisanpack/comments-number` block (#500).
*
* Mirrors the Blade partial and the Vue renderer so every environment
* emits identical markup. The count is server-resolved (PostResolver
* stamps `_resolvedCommentCount`) and combined with the saved
* singular / plural labels at render time.
*/

import type { JSX } from 'react';

import { attrInt, attrString, classList } from '../../support/attributes';
import type { BlockRendererProps } from '../../types';

export function CommentsNumberBlock({
attributes,
}: BlockRendererProps): JSX.Element {
const count = Math.max(0, attrInt(attributes._resolvedCommentCount, 0));
const singular = attrString(attributes.singularCommentText, 'Comment');
const plural = attrString(attributes.pluralCommentText, 'Comments');
const label = count === 1 ? singular : plural;
const className = attrString(attributes.className);

const classes = classList(['ap-comments-number', className]);
const line = `${count} ${label}`.trim();

return <p className={classes}>{line}</p>;
}
Loading