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: 2 additions & 0 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ jobs:

env:
SYMFONY_REQUIRE: ${{ matrix.symfony-version || '>=5.4' }} # TODO: To change to '>=6.4' in 3.x
# https://github.com/spatie/phpunit-snapshot-assertions#usage-in-ci
CREATE_SNAPSHOTS: false
steps:
- uses: actions/checkout@v4

Expand Down
47 changes: 47 additions & 0 deletions src/Toolkit/kits/shadcn/AlertDialog/EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Examples

## Default

```twig {"preview":true,"height":"500px"}
<twig:AlertDialog id="delete_account">
<twig:AlertDialog:Trigger>
<twig:Button {{ ...trigger_attrs }}>Open</twig:Button>
</twig:AlertDialog:Trigger>
<twig:AlertDialog:Content>
<twig:AlertDialog:Header>
<twig:AlertDialog:Title>Are you absolutely sure?</twig:AlertDialog:Title>
<twig:AlertDialog:Description>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</twig:AlertDialog:Description>
</twig:AlertDialog:Header>
<twig:AlertDialog:Footer>
<twig:AlertDialog:Cancel>Cancel</twig:AlertDialog:Cancel>
<twig:AlertDialog:Action>Continue</twig:AlertDialog:Action>
</twig:AlertDialog:Footer>
</twig:AlertDialog:Content>
</twig:AlertDialog>
```

## Opened by default

```twig {"preview":true,"height":"500px"}
<twig:AlertDialog id="delete_account" open>
<twig:AlertDialog:Trigger>
<twig:Button {{ ...trigger_attrs }}>Open</twig:Button>
</twig:AlertDialog:Trigger>
<twig:AlertDialog:Content>
<twig:AlertDialog:Header>
<twig:AlertDialog:Title>Are you absolutely sure?</twig:AlertDialog:Title>
<twig:AlertDialog:Description>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</twig:AlertDialog:Description>
</twig:AlertDialog:Header>
<twig:AlertDialog:Footer>
<twig:AlertDialog:Cancel>Cancel</twig:AlertDialog:Cancel>
<twig:AlertDialog:Action>Continue</twig:AlertDialog:Action>
</twig:AlertDialog:Footer>
</twig:AlertDialog:Content>
</twig:AlertDialog>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Controller } from '@hotwired/stimulus';
import { enter, leave } from 'el-transition';

export default class extends Controller {

static targets = ['trigger', 'overlay', 'dialog', 'content'];

async open() {
this.dialogTarget.showModal();

await Promise.all([enter(this.overlayTarget), enter(this.contentTarget)]);

if (this.hasTriggerTarget) {
this.triggerTarget.setAttribute('aria-expanded', 'true');
}
}

async close() {
await Promise.all([leave(this.overlayTarget), leave(this.contentTarget)]);

this.dialogTarget.close();

if (this.hasTriggerTarget) {
this.triggerTarget.setAttribute('aria-expanded', 'false');
}
}
}
36 changes: 36 additions & 0 deletions src/Toolkit/kits/shadcn/AlertDialog/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "../../../schema-kit-recipe-v1.json",
"type": "component",
"name": "AlertDialog",
"description": "A modal dialog that interrupts the user with important content and expects a response.",
"copy-files": {
"templates/": "templates/"
},
"dependencies": [
{
"type": "php",
"name": "twig/extra-bundle"
},
{
"type": "php",
"name": "twig/html-extra",
"version": "^3.12.0"
},
{
"type": "php",
"name": "tales-from-a-dev/twig-tailwind-extra"
},
{
"type": "npm",
"name": "el-transition"
},
{
"type": "importmap",
"package": "el-transition"
},
{
"type": "recipe",
"name": "Button"
}
]
Comment on lines +9 to +35
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 don't like this schema structure anymore, I will refactor it in another PR

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{%- props open = false, id -%}

{%- set _alert_dialog_open = open %}
{%- set _alert_dialog_id = 'alert-dialog-' ~ id -%}
{%- set _alert_dialog_title_id = _alert_dialog_id ~ '-title' -%}
{%- set _alert_dialog_description_id = _alert_dialog_id ~ '-description' -%}
<div {{ attributes.defaults({
'data-controller': 'alert-dialog',
'aria-labelledby': _alert_dialog_title_id,
'aria-describedby': _alert_dialog_description_id,
}) }}>
{% block content %}{% endblock %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% props variant = 'default' %}
<twig:Button variant="{{ variant }}" {{ ...attributes }}>
{{ block(outerBlocks.content) }}
</twig:Button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<twig:Button
variant="outline"
data-action="click->alert-dialog#close"
{{ ...attributes }}
>
{{- block(outerBlocks.content) -}}
</twig:Button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<dialog
id="{{ _alert_dialog_id }}"
{{ _alert_dialog_open ? 'open' }}
class="{{ 'fixed inset-0 size-auto max-h-none max-w-none overflow-y-auto bg-transparent backdrop:bg-transparent ' ~ attributes.render('class')|tailwind_merge }}"
data-alert-dialog-target="dialog"
data-action="keydown.esc->alert-dialog#close:prevent"
{{ attributes.without('id') }}
>
<div
data-alert-dialog-target="overlay"
data-transition-enter="transition ease-out duration-100"
data-transition-enter-start="transform opacity-0"
data-transition-enter-end="transform opacity-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100"
data-transition-leave-end="transform opacity-0"
Comment on lines +12 to +16
Copy link
Member Author

Choose a reason for hiding this comment

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

This data-transition-* syntax from Vue/Alpine.js is do-able thanks to https://github.com/mmccall10/el-transition

FYI it's not possible to use https://stimulus-use.github.io/stimulus-use/#/use-transition because it supports only one target, but we need to transition the overlay and content targets.

class="{{ _alert_dialog_open ? '' : 'hidden' }} fixed inset-0 z-50 bg-black/50"
></div>

<section
tabindex="0"
class="flex min-h-full items-end justify-center p-4 text-center focus:outline-none sm:items-center sm:p-0"
>
<div
data-transition-enter="transition ease-out duration-200"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-enter-end="transform opacity-100 scale-100"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-0 scale-95"
class="{{ _alert_dialog_open ? '' : 'hidden' }} bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg sm:max-w-lg"
data-alert-dialog-target="content"
>
{%- block content %}{% endblock -%}
</div>
</section>
</dialog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p
id="{{ _alert_dialog_description_id }}"
class="{{ 'text-muted-foreground text-sm ' ~ attributes.render('class')|tailwind_merge }}"
{{ attributes.without('id') }}
>
{%- block content %}{% endblock -%}
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<footer
class="{{ 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end ' ~ attributes.render('class')|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</footer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<header
class="{{ 'flex flex-col gap-2 text-center sm:text-left ' ~ attributes.render('class')|tailwind_merge }}"
{{ attributes }}
>
{%- block content %}{% endblock -%}
</header>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<h2
id="{{ _alert_dialog_title_id }}"
class="{{ 'text-lg font-semibold ' ~ attributes.render('class')|tailwind_merge }}"
{{ attributes.without('id') }}
>
{%- block content %}{% endblock -%}
</h2>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{%- set trigger_attrs = {
'data-action': 'click->alert-dialog#open',
'data-alert-dialog-target': 'trigger',
'aria-haspopup': 'dialog',
'aria-expanded': _alert_dialog_open ? 'true' : 'false',
} -%}
{%- block content %}{% endblock -%}
2 changes: 1 addition & 1 deletion src/Toolkit/kits/shadcn/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"name": "Shadcn UI",
"description": "Component based on the Shadcn UI library, one of the most popular design systems in JavaScript world.",
"license": "MIT",
"homepage": "https://ux.symfony.com/components"
"homepage": "https://ux.symfony.com/toolkit/kits/shadcn"
}
8 changes: 5 additions & 3 deletions src/Toolkit/src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
*/
private function getAlternativeRecipes(Kit $kit, string $recipeName): array
{
$alternative = [];
$alternativeRecipes = [];

foreach ($kit->getRecipes() as $recipe) {
$lev = levenshtein($recipeName, $recipe->manifest->name, 2, 5, 10);
if ($lev <= 8 || str_contains($recipe->manifest->name, $recipeName)) {
$alternative[] = $recipe;
$alternativeRecipes[] = $recipe;
}
}

return $alternative;
usort($alternativeRecipes, fn (Recipe $recipeA, Recipe $recipeB) => strcmp($recipeA->manifest->name, $recipeB->manifest->name));

return $alternativeRecipes;
}
}
53 changes: 33 additions & 20 deletions src/Toolkit/src/Kit/KitContextRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
*/
final class KitContextRunner
{
/**
* @var array<string, ComponentTemplateFinderInterface>
*/
private static $componentTemplateFinders = [];

public function __construct(
private readonly \Twig\Environment $twig,
private readonly ComponentFactory $componentFactory,
Expand All @@ -39,51 +44,59 @@ public function __construct(
*/
public function runForKit(Kit $kit, callable $callback): mixed
{
$resetServices = $this->contextualizeServicesForKit($kit);
$resetTwig = $this->contextualizeTwig($kit);
$resetComponentFactory = $this->contextualizeComponentFactory($kit);

try {
return $callback($kit);
} finally {
$resetServices();
$resetTwig();
$resetComponentFactory();
}
}

/**
* @return callable(): void Reset the services when called
* @return callable(): void
*/
private function contextualizeServicesForKit(Kit $kit): callable
private function contextualizeTwig(Kit $kit): callable
{
// Configure Twig
$initialTwigLoader = $this->twig->getLoader();

$loaders = [];
foreach ($kit->getRecipes(type: RecipeType::Component) as $recipe) {
$loaders[] = new FilesystemLoader($recipe->absolutePath);
}
$this->twig->setLoader(new ChainLoader([...$loaders, $initialTwigLoader]));
$loaders[] = $initialTwigLoader;

$this->twig->setLoader(new ChainLoader($loaders));

// Configure Twig Components
return fn () => $this->twig->setLoader($initialTwigLoader);
}

/**
* @return callable(): void
*/
private function contextualizeComponentFactory(Kit $kit): callable
{
$reflComponentFactory = new \ReflectionClass($this->componentFactory);

$reflComponentFactoryConfig = $reflComponentFactory->getProperty('config');
$initialComponentFactoryConfig = $reflComponentFactoryConfig->getValue($this->componentFactory);
$reflComponentFactoryConfig->setValue($this->componentFactory, []);
$reflConfig = $reflComponentFactory->getProperty('config');
$initialConfig = $reflConfig->getValue($this->componentFactory);
$reflConfig->setValue($this->componentFactory, []);

$reflComponentFactoryComponentTemplateFinder = $reflComponentFactory->getProperty('componentTemplateFinder');
$initialComponentFactoryComponentTemplateFinder = $reflComponentFactoryComponentTemplateFinder->getValue($this->componentFactory);
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $this->createComponentTemplateFinder($kit));
$reflComponentTemplateFinder = $reflComponentFactory->getProperty('componentTemplateFinder');
$initialComponentTemplateFinder = $reflComponentTemplateFinder->getValue($this->componentFactory);
$reflComponentTemplateFinder->setValue($this->componentFactory, $this->createComponentTemplateFinder($kit));

return function () use ($initialTwigLoader, $reflComponentFactoryConfig, $initialComponentFactoryConfig, $reflComponentFactoryComponentTemplateFinder, $initialComponentFactoryComponentTemplateFinder) {
$this->twig->setLoader($initialTwigLoader);
$reflComponentFactoryConfig->setValue($this->componentFactory, $initialComponentFactoryConfig);
$reflComponentFactoryComponentTemplateFinder->setValue($this->componentFactory, $initialComponentFactoryComponentTemplateFinder);
return function () use ($reflConfig, $initialConfig, $reflComponentTemplateFinder, $initialComponentTemplateFinder): void {
$reflConfig->setValue($this->componentFactory, $initialConfig);
$reflComponentTemplateFinder->setValue($this->componentFactory, $initialComponentTemplateFinder);
};
}

private function createComponentTemplateFinder(Kit $kit): ComponentTemplateFinderInterface
{
static $instances = [];

return $instances[$kit->manifest->name] ?? new class($kit) implements ComponentTemplateFinderInterface {
return self::$componentTemplateFinders[$kit->manifest->name] ??= new class($kit) implements ComponentTemplateFinderInterface {
public function __construct(private readonly Kit $kit)
{
}
Expand Down
2 changes: 1 addition & 1 deletion src/Toolkit/tests/Command/DebugKitCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function testShouldBeAbleToDebugShadcnKit()
->assertSuccessful()
// Kit details
->assertOutputContains('Name Shadcn')
->assertOutputContains('Homepage https://ux.symfony.com/components')
->assertOutputContains('Homepage https://ux.symfony.com/toolkit/kits/shadcn')
->assertOutputContains('License MIT')
// Components details
->assertOutputContains(implode(\PHP_EOL, [
Expand Down
2 changes: 1 addition & 1 deletion src/Toolkit/tests/Command/InstallCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function testShouldFailAndSuggestAlternativeRecipesWhenKitIsExplicit()
->execute()
->assertFaulty()
->assertOutputContains('[WARNING] The recipe "A" does not exist')
->assertOutputContains('Possible alternatives: "Alert", "AspectRatio", "Avatar"')
->assertOutputContains('Possible alternatives: "Alert", "AlertDialog", "AspectRatio"')
;
}

Expand Down
Loading
Loading