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 src/Toolkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 3.5.0

- Add support for blocks: ready-to-use page sections built from a kit's components, installed and previewed as a unit
- [Shadcn] Add the `login-01` and `login-02` login blocks
- [Shadcn][Flowbite] Merge component classes with `attributes.defaults({ class: '...'|tailwind_classes })` instead of `tailwind_merge` (consumer classes now override component variants)
- [Shadcn] Fix Field/Label, InputGroup and Pagination silently dropping their own base classes when forwarding to a child component (their checked/disabled/nested/dark styles now apply)
- Remove the now-obsolete `ClassMergeSpacingChecker` linter
Expand Down
15 changes: 15 additions & 0 deletions src/Toolkit/kits/shadcn/login-01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# LoginForm

A simple login form centered in a card.

```twig {"preview":true}
<div class="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div class="w-full max-w-sm">
<twig:LoginForm />
</div>
</div>
```

## Installation

::: installation
17 changes: 17 additions & 0 deletions src/Toolkit/kits/shadcn/login-01/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "../../../schema-kit-recipe-v1.json",
"type": "block",
"name": "LoginForm",
"description": "A simple login form centered in a card.",
"copy-files": {
"templates/": "templates/"
},
"dependencies": {
"recipe": ["button", "card", "input", "label"],
"composer": [
"twig/html-extra:^3.24.0",
"symfony/ux-twig-component:^3.5",
"tales-from-a-dev/twig-tailwind-extra:^1.3.0"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{# @block content Optional content to override the entire form. #}
<div
{{ attributes.defaults({
class: 'flex flex-col gap-6'|tailwind_classes,
}) }}
>
{%- block content -%}
<twig:Card>
<twig:Card:Header>
<twig:Card:Title class="text-2xl">Login</twig:Card:Title>
<twig:Card:Description>
Enter your email below to login to your account
</twig:Card:Description>
</twig:Card:Header>
<twig:Card:Content>
<form>
<div class="flex flex-col gap-6">
<div class="grid gap-2">
<twig:Label for="email">Email</twig:Label>
<twig:Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<twig:Label for="password">Password</twig:Label>
<a
href="#"
class="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<twig:Input id="password" type="password" required />
</div>
<twig:Button type="submit" class="w-full">
Login
</twig:Button>
<twig:Button variant="outline" class="w-full">
Login with Google
</twig:Button>
</div>
<div class="mt-4 text-center text-sm">
Don't have an account?
<a href="#" class="underline underline-offset-4">
Sign up
</a>
</div>
</form>
</twig:Card:Content>
</twig:Card>
{%- endblock -%}
</div>
34 changes: 34 additions & 0 deletions src/Toolkit/kits/shadcn/login-02/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# LoginForm

A two-column login page with a cover image.

```twig {"preview":true}
<div class="grid min-h-svh lg:grid-cols-2">
<div class="flex flex-col gap-4 p-6 md:p-10">
<div class="flex justify-center gap-2 md:justify-start">
<a href="#" class="flex items-center gap-2 font-medium">
<div class="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2h18"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M3 16h18"/></svg>
</div>
Acme Inc.
</a>
</div>
<div class="flex flex-1 items-center justify-center">
<div class="w-full max-w-xs">
<twig:LoginForm />
</div>
</div>
</div>
<div class="relative hidden bg-muted lg:block">
<img
src="https://ui.shadcn.com/placeholder.svg"
alt="Image"
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
/>
</div>
</div>
```

## Installation

::: installation
17 changes: 17 additions & 0 deletions src/Toolkit/kits/shadcn/login-02/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "../../../schema-kit-recipe-v1.json",
"type": "block",
"name": "LoginForm",
"description": "A two-column login page with a cover image.",
"copy-files": {
"templates/": "templates/"
},
"dependencies": {
"recipe": ["button", "field", "input"],
"composer": [
"twig/html-extra:^3.24.0",
"symfony/ux-twig-component:^3.5",
"tales-from-a-dev/twig-tailwind-extra:^1.3.0"
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{# @block content Optional content to override the entire form. #}
{%- block content -%}
<form
{{ attributes.defaults({
class: 'flex flex-col gap-6'|tailwind_classes,
}) }}
>
<twig:Field:Group>
<div class="flex flex-col items-center gap-1 text-center">
<h1 class="text-2xl font-bold">Login to your account</h1>
<p class="text-sm text-balance text-muted-foreground">
Enter your email below to login to your account
</p>
</div>
<twig:Field>
<twig:Field:Label for="email">Email</twig:Field:Label>
<twig:Input
id="email"
type="email"
placeholder="m@example.com"
required
/>
</twig:Field>
<twig:Field>
<div class="flex items-center">
<twig:Field:Label for="password">Password</twig:Field:Label>
<a
href="#"
class="ml-auto text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<twig:Input id="password" type="password" required />
</twig:Field>
<twig:Field>
<twig:Button type="submit">Login</twig:Button>
</twig:Field>
<twig:Field:Separator>Or continue with</twig:Field:Separator>
<twig:Field>
<twig:Button variant="outline" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="size-4">
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
fill="currentColor"
/>
</svg>
Login with GitHub
</twig:Button>
<twig:Field:Description class="text-center">
Don't have an account?
<a href="#" class="underline underline-offset-4">
Sign up
</a>
</twig:Field:Description>
</twig:Field>
</twig:Field:Group>
</form>
{%- endblock -%}
51 changes: 37 additions & 14 deletions src/Toolkit/src/Kit/KitContextRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Symfony\UX\Toolkit\Kit;

use Symfony\UX\Toolkit\Recipe\RecipeType;
use Symfony\UX\Toolkit\Recipe\Recipe;
use Symfony\UX\TwigComponent\ComponentFactory;
use Symfony\UX\TwigComponent\ComponentTemplateFinderInterface;
use Twig\Loader\ChainLoader;
Expand Down Expand Up @@ -42,10 +42,10 @@ public function __construct(
*
* @return TResult
*/
public function runForKit(Kit $kit, callable $callback): mixed
public function runForKit(Kit $kit, callable $callback, ?Recipe $prioritizedRecipe = null): mixed
{
$resetTwig = $this->contextualizeTwig($kit);
$resetComponentFactory = $this->contextualizeComponentFactory($kit);
$resetTwig = $this->contextualizeTwig($kit, $prioritizedRecipe);
$resetComponentFactory = $this->contextualizeComponentFactory($kit, $prioritizedRecipe);

try {
return $callback($kit);
Expand All @@ -58,12 +58,18 @@ public function runForKit(Kit $kit, callable $callback): mixed
/**
* @return callable(): void
*/
private function contextualizeTwig(Kit $kit): callable
private function contextualizeTwig(Kit $kit, ?Recipe $prioritizedRecipe): callable
{
$initialTwigLoader = $this->twig->getLoader();

$loaders = [];
foreach ($kit->getRecipes(type: RecipeType::Component) as $recipe) {
if (null !== $prioritizedRecipe) {
$loaders[] = new FilesystemLoader($prioritizedRecipe->absolutePath);
}
foreach ($kit->getRecipes() as $recipe) {
if (null !== $prioritizedRecipe && $recipe->name === $prioritizedRecipe->name) {
continue;
}
$loaders[] = new FilesystemLoader($recipe->absolutePath);
}
$loaders[] = $initialTwigLoader;
Expand All @@ -76,7 +82,7 @@ private function contextualizeTwig(Kit $kit): callable
/**
* @return callable(): void
*/
private function contextualizeComponentFactory(Kit $kit): callable
private function contextualizeComponentFactory(Kit $kit, ?Recipe $prioritizedRecipe): callable
{
$reflComponentFactory = new \ReflectionClass($this->componentFactory);

Expand All @@ -86,26 +92,43 @@ private function contextualizeComponentFactory(Kit $kit): callable

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

return function () use ($reflConfig, $initialConfig, $reflComponentTemplateFinder, $initialComponentTemplateFinder): void {
$reflConfig->setValue($this->componentFactory, $initialConfig);
$reflComponentTemplateFinder->setValue($this->componentFactory, $initialComponentTemplateFinder);
};
}

private function createComponentTemplateFinder(Kit $kit): ComponentTemplateFinderInterface
private function createComponentTemplateFinder(Kit $kit, ?Recipe $prioritizedRecipe): ComponentTemplateFinderInterface
{
return self::$componentTemplateFinders[$kit->manifest->name] ??= new class($kit) implements ComponentTemplateFinderInterface {
public function __construct(private readonly Kit $kit)
{
$cacheKey = $kit->manifest->name.($prioritizedRecipe ? '/'.$prioritizedRecipe->name : '');

return self::$componentTemplateFinders[$cacheKey] ??= new class($kit, $prioritizedRecipe) implements ComponentTemplateFinderInterface {
public function __construct(
private readonly Kit $kit,
private readonly ?Recipe $prioritizedRecipe,
) {
}

public function findAnonymousComponentTemplate(string $name): ?string
{
foreach ($this->kit->getRecipes(type: RecipeType::Component) as $recipe) {
$templateSuffix = 'templates/components/'.str_replace(':', '/', $name).'.html.twig';

if (null !== $this->prioritizedRecipe) {
foreach ($this->prioritizedRecipe->getFiles() as $file) {
if (str_ends_with($file->sourceRelativePathName, $templateSuffix)) {
return $file->sourceRelativePathName;
}
}
}

foreach ($this->kit->getRecipes() as $recipe) {
if (null !== $this->prioritizedRecipe && $recipe->name === $this->prioritizedRecipe->name) {
continue;
}
foreach ($recipe->getFiles() as $file) {
if (str_ends_with($file->sourceRelativePathName, 'templates/components/'.str_replace(':', '/', $name).'.html.twig')) {
if (str_ends_with($file->sourceRelativePathName, $templateSuffix)) {
return $file->sourceRelativePathName;
}
}
Expand Down
Loading