From bc9accc41ecdd3466ab0715e20c1a5d88b7acce9 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Wed, 3 Sep 2025 23:01:27 +0200 Subject: [PATCH] [Toolkit][Shadcn] Add AlertDialog recipe --- .github/workflows/unit-tests.yaml | 2 + .../kits/shadcn/AlertDialog/EXAMPLES.md | 47 ++++++++++++++++ .../controllers/alert_dialog_controller.js | 27 ++++++++++ .../kits/shadcn/AlertDialog/manifest.json | 36 +++++++++++++ .../components/AlertDialog.html.twig | 13 +++++ .../components/AlertDialog/Action.html.twig | 4 ++ .../components/AlertDialog/Cancel.html.twig | 7 +++ .../components/AlertDialog/Content.html.twig | 37 +++++++++++++ .../AlertDialog/Description.html.twig | 7 +++ .../components/AlertDialog/Footer.html.twig | 6 +++ .../components/AlertDialog/Header.html.twig | 6 +++ .../components/AlertDialog/Title.html.twig | 7 +++ .../components/AlertDialog/Trigger.html.twig | 7 +++ src/Toolkit/kits/shadcn/manifest.json | 2 +- src/Toolkit/src/Command/InstallCommand.php | 8 +-- src/Toolkit/src/Kit/KitContextRunner.php | 53 ++++++++++++------- .../tests/Command/DebugKitCommandTest.php | 2 +- .../tests/Command/InstallCommandTest.php | 2 +- ...dcn, component AlertDialog, code 1__1.html | 45 ++++++++++++++++ ...dcn, component AlertDialog, code 2__1.html | 45 ++++++++++++++++ ux.symfony.com/assets/toolkit-shadcn.js | 5 ++ .../config/packages/asset_mapper.yaml | 6 ++- ux.symfony.com/importmap.php | 6 +++ .../src/Service/Toolkit/ToolkitService.php | 2 +- 24 files changed, 354 insertions(+), 28 deletions(-) create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/EXAMPLES.md create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/manifest.json create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Action.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Cancel.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Content.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Description.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Footer.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Header.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Title.html.twig create mode 100644 src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Trigger.html.twig create mode 100644 src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 1__1.html create mode 100644 src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 2__1.html diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 63699864767..dcf0b812712 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -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 diff --git a/src/Toolkit/kits/shadcn/AlertDialog/EXAMPLES.md b/src/Toolkit/kits/shadcn/AlertDialog/EXAMPLES.md new file mode 100644 index 00000000000..68ac76447fb --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/EXAMPLES.md @@ -0,0 +1,47 @@ +# Examples + +## Default + +```twig {"preview":true,"height":"500px"} + + + Open + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account + and remove your data from our servers. + + + + Cancel + Continue + + + +``` + +## Opened by default + +```twig {"preview":true,"height":"500px"} + + + Open + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account + and remove your data from our servers. + + + + Cancel + Continue + + + +``` diff --git a/src/Toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js b/src/Toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js new file mode 100644 index 00000000000..5624ec54c13 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js @@ -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'); + } + } +} diff --git a/src/Toolkit/kits/shadcn/AlertDialog/manifest.json b/src/Toolkit/kits/shadcn/AlertDialog/manifest.json new file mode 100644 index 00000000000..4333819bb77 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/manifest.json @@ -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" + } + ] +} diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog.html.twig new file mode 100644 index 00000000000..bf31fcc8993 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog.html.twig @@ -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' -%} +
+ {% block content %}{% endblock %} +
diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Action.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Action.html.twig new file mode 100644 index 00000000000..560ae0a0abf --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Action.html.twig @@ -0,0 +1,4 @@ +{% props variant = 'default' %} + + {{ block(outerBlocks.content) }} + diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Cancel.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Cancel.html.twig new file mode 100644 index 00000000000..d4a9dd77ca8 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Cancel.html.twig @@ -0,0 +1,7 @@ + + {{- block(outerBlocks.content) -}} + diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Content.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Content.html.twig new file mode 100644 index 00000000000..c0d6833bcfe --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Content.html.twig @@ -0,0 +1,37 @@ + +
+ +
+
+ {%- block content %}{% endblock -%} +
+
+
diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Description.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Description.html.twig new file mode 100644 index 00000000000..af494c4c007 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Description.html.twig @@ -0,0 +1,7 @@ +

+ {%- block content %}{% endblock -%} +

diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Footer.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Footer.html.twig new file mode 100644 index 00000000000..2d05b4f3159 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Footer.html.twig @@ -0,0 +1,6 @@ + diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Header.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Header.html.twig new file mode 100644 index 00000000000..d4b754f064f --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Header.html.twig @@ -0,0 +1,6 @@ +
+ {%- block content %}{% endblock -%} +
diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Title.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Title.html.twig new file mode 100644 index 00000000000..c1a7cdb9e11 --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Title.html.twig @@ -0,0 +1,7 @@ +

+ {%- block content %}{% endblock -%} +

diff --git a/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Trigger.html.twig b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Trigger.html.twig new file mode 100644 index 00000000000..a52777db61a --- /dev/null +++ b/src/Toolkit/kits/shadcn/AlertDialog/templates/components/AlertDialog/Trigger.html.twig @@ -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 -%} diff --git a/src/Toolkit/kits/shadcn/manifest.json b/src/Toolkit/kits/shadcn/manifest.json index 03571bdbcc3..928f5511527 100644 --- a/src/Toolkit/kits/shadcn/manifest.json +++ b/src/Toolkit/kits/shadcn/manifest.json @@ -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" } diff --git a/src/Toolkit/src/Command/InstallCommand.php b/src/Toolkit/src/Command/InstallCommand.php index 3400348a524..0467532e609 100644 --- a/src/Toolkit/src/Command/InstallCommand.php +++ b/src/Toolkit/src/Command/InstallCommand.php @@ -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; } } diff --git a/src/Toolkit/src/Kit/KitContextRunner.php b/src/Toolkit/src/Kit/KitContextRunner.php index 20813b2a270..e052380f221 100644 --- a/src/Toolkit/src/Kit/KitContextRunner.php +++ b/src/Toolkit/src/Kit/KitContextRunner.php @@ -24,6 +24,11 @@ */ final class KitContextRunner { + /** + * @var array + */ + private static $componentTemplateFinders = []; + public function __construct( private readonly \Twig\Environment $twig, private readonly ComponentFactory $componentFactory, @@ -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) { } diff --git a/src/Toolkit/tests/Command/DebugKitCommandTest.php b/src/Toolkit/tests/Command/DebugKitCommandTest.php index 78878d2a791..f5e65e3f25a 100644 --- a/src/Toolkit/tests/Command/DebugKitCommandTest.php +++ b/src/Toolkit/tests/Command/DebugKitCommandTest.php @@ -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, [ diff --git a/src/Toolkit/tests/Command/InstallCommandTest.php b/src/Toolkit/tests/Command/InstallCommandTest.php index afecc0b77c6..5da5ee8e6bb 100644 --- a/src/Toolkit/tests/Command/InstallCommandTest.php +++ b/src/Toolkit/tests/Command/InstallCommandTest.php @@ -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"') ; } diff --git a/src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 1__1.html b/src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 1__1.html new file mode 100644 index 00000000000..8b2110a2a35 --- /dev/null +++ b/src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 1__1.html @@ -0,0 +1,45 @@ + +
+ + + + +
+ +
+
+
\ No newline at end of file diff --git a/src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 2__1.html b/src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 2__1.html new file mode 100644 index 00000000000..29d9d235f6d --- /dev/null +++ b/src/Toolkit/tests/Functional/__snapshots__/ComponentsRenderingTest__testComponentRendering with data set Kit shadcn, component AlertDialog, code 2__1.html @@ -0,0 +1,45 @@ + +
+ + +
+ +
+
+

Are you absolutely sure?

+

This action cannot be undone. This will permanently delete your account + and remove your data from our servers. +

+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/ux.symfony.com/assets/toolkit-shadcn.js b/ux.symfony.com/assets/toolkit-shadcn.js index 263378a5c3b..8a1d16de416 100644 --- a/ux.symfony.com/assets/toolkit-shadcn.js +++ b/ux.symfony.com/assets/toolkit-shadcn.js @@ -1 +1,6 @@ import './styles/toolkit-shadcn.css'; +import { startStimulusApp } from '@symfony/stimulus-bundle'; +import AlertDialog from '@symfony/ux-toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js'; + +const app = startStimulusApp(); +app.register('alert-dialog', AlertDialog); diff --git a/ux.symfony.com/config/packages/asset_mapper.yaml b/ux.symfony.com/config/packages/asset_mapper.yaml index 730e2f482a3..38497f7eadc 100644 --- a/ux.symfony.com/config/packages/asset_mapper.yaml +++ b/ux.symfony.com/config/packages/asset_mapper.yaml @@ -2,7 +2,8 @@ framework: asset_mapper: # The paths to make available to the asset mapper. paths: - - assets/ + assets/: '' + vendor/symfony/ux-toolkit/kits/: '@symfony/ux-toolkit/kits' excluded_patterns: - '*/assets/styles/_*.scss' - '*/assets/styles/**/_*.scss' @@ -11,6 +12,9 @@ framework: - '*/assets/react/src**' # React sources - '*/assets/svelte/build**' # ESvelte build dir - '*/assets/svelte/src**' # Svelte source files + - '*/kits/**/*.html.twig' # UX Toolkit kit Twig templates + - '*/kits/**/*.md' # UX Toolkit kit documentation + - '*/kits/**/manifest.json' # UX Toolkit kit manifest files importmap_polyfill: false react: diff --git a/ux.symfony.com/importmap.php b/ux.symfony.com/importmap.php index 3b6259f2d12..3bcd7d58ca8 100644 --- a/ux.symfony.com/importmap.php +++ b/ux.symfony.com/importmap.php @@ -204,4 +204,10 @@ '@symfony/ux-leaflet-map' => [ 'path' => './vendor/symfony/ux-leaflet-map/assets/dist/map_controller.js', ], + '@symfony/ux-toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js' => [ + 'path' => './vendor/symfony/ux-toolkit/kits/shadcn/AlertDialog/assets/controllers/alert_dialog_controller.js', + ], + 'el-transition' => [ + 'version' => '0.0.7', + ], ]; diff --git a/ux.symfony.com/src/Service/Toolkit/ToolkitService.php b/ux.symfony.com/src/Service/Toolkit/ToolkitService.php index 7d766145af6..c2e710b63ec 100644 --- a/ux.symfony.com/src/Service/Toolkit/ToolkitService.php +++ b/ux.symfony.com/src/Service/Toolkit/ToolkitService.php @@ -132,7 +132,7 @@ public function renderInstallationSteps(ToolkitKitId $kitId, Recipe $component): $manual .= '
  • And the most important, enjoy!
  • '; $manual .= ''; - return $this->generateTabs([ + return self::generateTabs([ 'Automatic' => \sprintf( '

    Ensure the Symfony UX Toolkit is installed in your Symfony app:

    %s

    Then, run the following command to install the component and its dependencies:

    %s', CodeBlockRenderer::highlightCode('shell', '$ composer require --dev symfony/ux-toolkit'),