Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,34 @@
/**
* 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
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>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* React renderer for the `artisanpack/copyright` block (#500).
*
* Mirrors the Blade partial at
* `packages/visual-editor-renderer-blade/resources/views/blocks/artisanpack/copyright.blade.php`
* and the Vue renderer so every environment emits identical markup. The
* current year is read at render time (not stamped server-side) so the
* line stays accurate whenever the page is rendered.
*/

import type { JSX } from 'react';

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

type CopyrightType = 'icon-text' | 'icon-only' | 'text-only';

const VALID_TYPES: ReadonlyArray<CopyrightType> = [
'icon-text',
'icon-only',
'text-only',
];

function normalizeType(value: unknown): CopyrightType {
const raw = attrString(value, 'icon-text');
return (VALID_TYPES as ReadonlyArray<string>).includes(raw)
? (raw as CopyrightType)
: 'icon-text';
}

function buildLine(type: CopyrightType, text: string, year: number): string {
const trimmed = text.trim();
if (type === 'icon-only') {
return `© ${year}`;
}
if (type === 'text-only') {
return trimmed === '' ? `${year}` : `${trimmed} ${year}`;
}
return trimmed === '' ? `© ${year}` : `© ${trimmed} ${year}`;
}

export function CopyrightBlock({ attributes }: BlockRendererProps): JSX.Element {
const copyrightType = normalizeType(attributes.copyrightType);
const copyrightText = attrString(attributes.copyrightText, 'Copyright');
const className = attrString(attributes.className);

const classes = classList(['ap-copyright', className]);
const year = new Date().getUTCFullYear();
const line = buildLine(copyrightType, copyrightText, year);

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