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
2 changes: 1 addition & 1 deletion ux.symfony.com/assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ $utilities: map-remove(
@import "components/Button";
@import "components/Browser";
@import "components/Changelog";
@import "components/CodePreview_Tabs";
@import "components/DataList";
@import "components/DemoContainer";
@import "components/DemoCard";
Expand All @@ -153,6 +152,7 @@ $utilities: map-remove(
@import "components/Terminal";
@import "components/TerminalCommand";
@import "components/ThemeSwitcher";
@import "components/Toolkit_Tabs";
@import "components/Wysiwyg";

// Utilities
Expand Down
20 changes: 11 additions & 9 deletions ux.symfony.com/assets/styles/components/_Terminal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
border-radius: .75rem;
position: relative;
font-size: 12px;
display: grid; // Ensure the Terminal overflow its parent if "pre" contains a very-long-line of the same highlighted element (e.g.: a long string)
}

.Terminal_light {
Expand Down Expand Up @@ -162,6 +163,7 @@
overflow: visible;
}
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, .8) transparent;
pre {
background: none;
}
Expand All @@ -170,15 +172,15 @@
}
}

@media screen and (min-width: 768px) {
.Terminal_content::-webkit-scrollbar {
display: none;
}
.Terminal_content {
--webkit-scrollbar-width: none;
scrollbar-width: none;
}
}
// @media screen and (min-width: 768px) {
// .Terminal_content::-webkit-scrollbar {
// display: none;
// }
// .Terminal_content {
// --webkit-scrollbar-width: none;
// scrollbar-width: none;
// }
// }
Comment on lines +175 to +183
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this one, but I don't really see why you would want to hide a scrollbar, as it reduce accessibility and UX

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to Toolkit ? Any page i can see the problem ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no, I wanted to display a scroll bar when necessary, but I couldn't understand why this code exists.
You can check the "Alert" component page, by unfolding the files from "manual installation" section, it contains a Terminal with a veryyy long and unbreakable line.

You can also check other parts of the website where a Terminal is displayed, to confirm it didn't break things. 🙏

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it broke things at a time, two scrollbars were displayed. Testing on mac often gives a false impression, as macOS hide scrollbars per default.

But let's see and we will be able to fix if there were any pb

👍 :


.Terminal_expand {
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
.CodePreview_Tabs {
.Toolkit_Tabs {
}

.CodePreview_TabHead {
.Toolkit_TabHead {
display: flex;
flex-direction: row;
margin-bottom: 1rem
}

.CodePreview_TabControl {
.Toolkit_TabControl {
border-bottom: 3px solid transparent;
color: var(--bs-primary-color);
padding: 0 1rem;
Expand All @@ -18,24 +18,24 @@
margin-bottom: -1px;
}

.CodePreview_TabControl.active {
.Toolkit_TabControl.active {
border-color: var(--bs-secondary-color);
}

.CodePreview_TabPanel {
.Toolkit_TabPanel {
position: relative;
}

.CodePreview_TabPanel:not(.active) {
.Toolkit_TabPanel:not(.active) {
display: none;
}

.CodePreview_TabPanel:has(.CodePreview_Preview) {
.Toolkit_TabPanel:has(.Toolkit_Preview) {
border: 1px solid var(--bs-border-color);
border-radius: .75rem
}

.CodePreview_Loader {
.Toolkit_Loader {
width: 100%;
display: flex;
justify-content: center;
Expand All @@ -48,7 +48,7 @@
}
}

.CodePreview_Preview {
.Toolkit_Preview {
width: 100%;
transition: opacity .250s linear;
border-radius: .75rem;
Expand Down
8 changes: 3 additions & 5 deletions ux.symfony.com/src/Service/CommonMark/ConverterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@
namespace App\Service\CommonMark;

use App\Service\CommonMark\Extension\CodeBlockRenderer\CodeBlockRenderer;
use App\Service\Toolkit\ToolkitService;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\Mention\MentionExtension;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
* @author Kevin Bond <[email protected]>
Expand All @@ -28,8 +27,7 @@
final class ConverterFactory
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly UriSigner $uriSigner,
private readonly ToolkitService $toolkitService,
) {
}

Expand Down Expand Up @@ -57,7 +55,7 @@ public function __invoke(): CommonMarkConverter
->addExtension(new ExternalLinkExtension())
->addExtension(new MentionExtension())
->addExtension(new FrontMatterExtension())
->addRenderer(FencedCode::class, new CodeBlockRenderer($this->urlGenerator, $this->uriSigner))
->addRenderer(FencedCode::class, new CodeBlockRenderer($this->toolkitService))
;

return $converter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,19 @@

namespace App\Service\CommonMark\Extension\CodeBlockRenderer;

use App\Enum\ToolkitKitId;
use App\Service\Toolkit\ToolkitService;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Tempest\Highlight\Highlighter;
use Tempest\Highlight\WebTheme;

final readonly class CodeBlockRenderer implements NodeRendererInterface
{
public function __construct(
private UrlGeneratorInterface $urlGenerator,
private UriSigner $uriSigner,
private ToolkitService $toolkitService,
) {
}

Expand All @@ -38,45 +37,19 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \
$infoWords = $node->getInfoWords();
$language = $infoWords[0] ?? 'txt';
$options = isset($infoWords[1]) && json_validate($infoWords[1]) ? json_decode($infoWords[1], true) : [];
$kitId = ToolkitKitId::tryFrom($options['kit'] ?? null);
$preview = $options['preview'] ?? false;
$kit = $options['kit'] ?? null;
$height = $options['height'] ?? '150px';

$code = $node->getLiteral();
$output = $this->highlightCode($language, $code = $node->getLiteral());

$output = $this->highlightCode($code, $language);

if ($preview && $kit) {
$previewUrl = $this->uriSigner->sign($this->urlGenerator->generate('app_toolkit_component_preview', [
'kitId' => $kit,
'code' => $code,
'height' => $height,
], UrlGeneratorInterface::ABSOLUTE_URL));

$output = <<<HTML
<div class="CodePreview_Tabs" data-controller="tabs" data-tabs-tab-value="preview" data-tabs-active-class="active">
<nav class="CodePreview_TabHead" role="tablist" style="border-bottom: 1px solid var(--bs-border-color)">
<button class="CodePreview_TabControl" data-action="tabs#show" data-tabs-target="control" data-tabs-tab-param="preview" role="tab" aria-selected="true">Preview</button>
<button class="CodePreview_TabControl" data-action="tabs#show" data-tabs-target="control" data-tabs-tab-param="code" role="tab" aria-selected="false">Code</button>
</nav>
<div class="CodePreview_TabBody">
<div class="CodePreview_TabPanel active" data-tabs-target="tab" data-tab="preview" role="tabpanel">
<div class="CodePreview_Loader" style="height: {$height};">
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
<span>Loading...</span>
</div>
<iframe class="CodePreview_Preview loading" src="{$previewUrl}" style="height: {$height};" loading="lazy" onload="this.previousElementSibling.style.display = 'none'; this.classList.remove('loading')"></iframe>
</div>
<div class="CodePreview_TabPanel" data-tabs-target="tab" data-tab="code" role="tabpanel">{$output}</div>
</div>
</div>
HTML;
if ($kitId && $preview) {
$output = $this->toolkitService->renderComponentPreviewCodeTabs($kitId, $code, $output, $options['height'] ?? '150px');
}

return $output;
}

private function highlightCode(string $code, string $language): string
public static function highlightCode(string $language, string $code, string $style = 'margin-bottom: 1rem'): string
{
$highlighter = new Highlighter();

Expand All @@ -87,11 +60,9 @@ private function highlightCode(string $code, string $language): string
: '<pre data-lang="'.$language.'" class="notranslate">'.$parsed.'</pre>';

return <<<HTML
<div class="Terminal terminal-code" style="margin-bottom: 1rem;">
<div class="Terminal terminal-code" style="$style">
<div class="Terminal_body">
<div class="Terminal_content" style="max-height: 450px;">
{$output}
</div>
<div class="Terminal_content" style="max-height: 450px;">{$output}</div>
</div>
</div>
HTML;
Expand Down
88 changes: 87 additions & 1 deletion ux.symfony.com/src/Service/Toolkit/ToolkitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@
namespace App\Service\Toolkit;

use App\Enum\ToolkitKitId;
use App\Service\CommonMark\Extension\CodeBlockRenderer\CodeBlockRenderer;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Filesystem\Path;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\UX\Toolkit\Asset\Component;
use Symfony\UX\Toolkit\Installer\PoolResolver;
use Symfony\UX\Toolkit\Kit\Kit;
use Symfony\UX\Toolkit\Registry\RegistryFactory;

class ToolkitService
{
public function __construct(
#[Autowire(service: 'ux_toolkit.registry.registry_factory')]
private RegistryFactory $registryFactory,
private readonly RegistryFactory $registryFactory,
private readonly UriSigner $uriSigner,
private readonly UrlGeneratorInterface $urlGenerator,
) {
}

Expand Down Expand Up @@ -54,4 +61,83 @@ public function getDocumentableComponents(Kit $kit): array
{
return array_filter($kit->getComponents(), fn (Component $component) => $component->doc);
}

public function renderComponentPreviewCodeTabs(ToolkitKitId $kitId, string $code, string $highlightedCode, string $height): string
{
$previewUrl = $this->urlGenerator->generate('app_toolkit_component_preview', ['kitId' => $kitId->value, 'code' => $code, 'height' => $height], UrlGeneratorInterface::ABSOLUTE_URL);
$previewUrl = $this->uriSigner->sign($previewUrl);

return self::generateTabs([
'Preview' => <<<HTML
<div class="Toolkit_Loader" style="height: {$height};">
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
<span>Loading...</span>
</div>
<iframe class="Toolkit_Preview loading" src="{$previewUrl}" style="height: {$height};" loading="lazy" onload="this.previousElementSibling.style.display = 'none'; this.classList.remove('loading')"></iframe>
HTML,
'Code' => $highlightedCode,
]);
}

public function renderInstallationSteps(ToolkitKitId $kitId, Component $component): string
{
$kit = $this->getKit($kitId);
$pool = (new PoolResolver())->resolveForComponent($kit, $component);

$manual = '<p>The UX Toolkit is not mandatory to install a component. You can install it manually by following the next steps:</p>';
$manual .= '<ol style="display: grid; gap: 1rem;">';
$manual .= '<li><strong>Copy the following file(s) into your Symfony app:</strong>';
foreach ($pool->getFiles() as $file) {
$manual .= \sprintf(
"<details><summary><code>%s</code></summary>\n%s\n</details>",
$file->relativePathNameToKit,
\sprintf("\n```%s\n%s\n```", pathinfo($file->relativePathNameToKit, \PATHINFO_EXTENSION), trim(file_get_contents(Path::join($kit->path, $file->relativePathNameToKit))))
);
}
$manual .= '</li>';

if ($phpPackageDependencies = $pool->getPhpPackageDependencies()) {
$manual .= '<li><strong>If necessary, install the following Composer dependencies:</strong>';
$manual .= CodeBlockRenderer::highlightCode('shell', '$ composer require '.implode(' ', $phpPackageDependencies), 'margin-bottom: 0');
$manual .= '</li>';
}

$manual .= '<li><strong>And the most important, enjoy!</strong></li>';
$manual .= '</ol>';

return $this->generateTabs([
'Automatic' => \sprintf(
'<p>Ensure the Symfony UX Toolkit is installed in your Symfony app:</p>%s<p>Then, run the following command to install the component and its dependencies:</p>%s',
CodeBlockRenderer::highlightCode('shell', '$ composer require --dev symfony/ux-toolkit'),
CodeBlockRenderer::highlightCode('shell', "$ bin/console ux:toolkit:install-component {$component->name} --kit {$kitId->value}"),
),
'Manual' => $manual,
]);
}

/**
* @param non-empty-array<string, string> $tabs
*/
private static function generateTabs(array $tabs): string
{
$activeTabId = null;
$tabsControls = '';
$tabsPanels = '';

foreach ($tabs as $tabText => $tabContent) {
$tabId = hash('xxh3', $tabText);
$activeTabId ??= $tabId;
$isActive = $activeTabId === $tabId;

$tabsControls .= \sprintf('<button class="Toolkit_TabControl" data-action="tabs#show" data-tabs-target="control" data-tabs-tab-param="%s" role="tab" aria-selected="%s">%s</button>', $tabId, $isActive ? 'true' : 'false', trim($tabText));
$tabsPanels .= \sprintf('<div class="Toolkit_TabPanel %s" data-tabs-target="tab" data-tab="%s" role="tabpanel">%s</div>', $isActive ? 'active' : '', $tabId, $tabContent);
}

return <<<HTML
<div class="Toolkit_Tabs" data-controller="tabs" data-tabs-tab-value="{$activeTabId}" data-tabs-active-class="active">
<nav class="Toolkit_TabHead" role="tablist" style="border-bottom: 1px solid var(--bs-border-color)">{$tabsControls}</nav>
<div class="Toolkit_TabBody">{$tabsPanels}</div>
</div>
HTML;
}
}
Loading