diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a645ae31..63a9ee2d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -96,8 +96,9 @@ jobs: - name: "Install & compile assets" run: | - bin/console importmap:install - bin/console asset-map:compile + bin/console importmap:install + bin/console tailwind:build + bin/console asset-map:compile - name: Run PHPUnit run: vendor/bin/phpunit @@ -141,6 +142,22 @@ jobs: yarn install --frozen-lockfile --non-interactive yarn test + - name: "Restrict packages' versions (Shadcn Admin Ui)" + run: | + (cd src/ShadcnAdminUi/ && composer config --no-plugins allow-plugins.symfony/runtime true) + (cd src/ShadcnAdminUi/ && composer config extra.symfony.require "${{ matrix.symfony }}") + + - name: "Install dependencies (Shadcn Admin Ui)" + run: (cd src/ShadcnAdminUi/ && composer update --no-interaction --no-scripts) + + - name: "Build the assets (Shadcn Admin Ui)" + run: | + (cd src/ShadcnAdminUi/ && APP_ENV=test php tests/Functional/.application/bin/console importmap:install) + (cd src/ShadcnAdminUi/ && APP_ENV=test php tests/Functional/.application/bin/console tailwind:build) + + - name: Run PHPUnit (Shadcn Admin Ui) + run: (cd src/ShadcnAdminUi/ && vendor/bin/phpunit) + - name: "Restrict packages' versions (Twig Hooks)" run: | (cd src/TwigHooks/ && composer config --no-plugins allow-plugins.symfony/runtime true) diff --git a/.github/workflows/split_packages.yaml b/.github/workflows/split_packages.yaml index f68348cc..c93a1888 100644 --- a/.github/workflows/split_packages.yaml +++ b/.github/workflows/split_packages.yaml @@ -26,6 +26,9 @@ jobs: - directory: src/BootstrapAdminUi repository: BootstrapAdminUi + - + directory: src/ShadcnAdminUi + repository: ShadcnAdminUi - directory: src/TwigExtra repository: TwigExtra diff --git a/.gitignore b/.gitignore index 44cac802..f7c027cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ config/reference.php /src/**/.phpunit.result.cache /src/**/vendor +!/src/ShadcnAdminUi/assets/styles/vendor composer.lock ###> symfony/framework-bundle ### diff --git a/README.md b/README.md index 5bf6f8fd..ec8e2a65 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,56 @@ The Sylius stack is a set of tools for your Symfony projects. Documentation is available at [stack.sylius.com](https://stack.sylius.com). +🛠️ Contributing +--------------- + +The repository ships a demo application to try the packages: + +```bash +composer install +docker compose up -d +symfony console doctrine:schema:create +symfony console doctrine:fixtures:load -n +symfony serve -d +``` + +The admin panel is available at `/admin` (login: `admin@example.com` / `admin`). + +### Switching the admin UI + +The demo application can run with either [BootstrapAdminUi](src/BootstrapAdminUi) or [ShadcnAdminUi](src/ShadcnAdminUi): + +```bash +bin/switch-admin-ui # toggles between the two +bin/switch-admin-ui bootstrap +bin/switch-admin-ui shadcn +``` + +The script enables the matching bundle and theme configuration, updates the template namespaces of the demo +application, then clears the cache (and builds the Tailwind stylesheet for `shadcn`). + +When working on the shadcn templates, keep the Tailwind watcher running: + +```bash +symfony console tailwind:build --watch +``` + +### Switching the color themes and the sidebar + +With ShadcnAdminUi, try the [color themes](docs/shadcn-admin-ui/theming.md#color-themes) and the sidebar variants on +the demo application: + +```bash +bin/switch-admin-theme zinc blue # a base color, then an accent +bin/switch-admin-theme --sidebar=floating # sidebar, floating or inset +bin/switch-admin-theme --sidebar=inset rose +bin/switch-admin-theme none # back to the neutral theme +bin/switch-admin-theme # shows the current settings and the available themes +``` + +A setting left out keeps its current value. The script enables ShadcnAdminUi if needed; switching back to Bootstrap +resets both settings. + 🤝 Community ------------ diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 00000000..6cd0ca8e --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,4 @@ +import { startStimulusApp } from '@symfony/stimulus-bundle'; +import './scripts/statistics_chart.js'; + +startStimulusApp(); diff --git a/assets/styles/admin.css b/assets/styles/admin.css new file mode 100644 index 00000000..8a6e086f --- /dev/null +++ b/assets/styles/admin.css @@ -0,0 +1,4 @@ +@import "tailwindcss"; +@import "../../src/ShadcnAdminUi/assets/styles/theme.css"; + +@source "../../templates"; diff --git a/bin/switch-admin-theme b/bin/switch-admin-theme new file mode 100755 index 00000000..349abeda --- /dev/null +++ b/bin/switch-admin-theme @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# Switches the color themes and the sidebar variant of the demo application (ShadcnAdminUi only). +# +# Usage: bin/switch-admin-theme [--sidebar=sidebar|floating|inset] [theme ...|none] +# A setting left out keeps its current value; with no argument at all, prints the current settings. +# +# Examples: +# bin/switch-admin-theme zinc blue +# bin/switch-admin-theme --sidebar=floating +# bin/switch-admin-theme --sidebar=inset slate violet +# bin/switch-admin-theme none + +set -euo pipefail + +cd "$(dirname "$0")/.." + +themes_dir=src/ShadcnAdminUi/assets/styles/themes +available=$(find "$themes_dir" -name '*.css' -exec basename {} .css \; | sort | tr '\n' ' ') +variants='sidebar floating inset' +config=config/packages/sylius_shadcn_admin_ui.yaml + +usage() { + echo "Usage: $0 [--sidebar=sidebar|floating|inset] [theme ...|none]" + echo "Available themes: $available" + echo "Combine a base color (stone, zinc, slate) with an accent, e.g.: $0 zinc blue" + echo "A setting left out keeps its current value; \"none\" goes back to the neutral theme." +} + +# Current settings, read from the configuration written by this script. +current_themes='' +current_variant=sidebar +if [[ -f "$config" ]]; then + current_themes=$(sed -n 's/^ *themes: \[\(.*\)\]$/\1/p' "$config" | tr -d ',') + current_variant=$(sed -n 's/^ *variant: *\([a-z]*\)$/\1/p' "$config") + current_variant=${current_variant:-sidebar} +fi + +if [[ $# -eq 0 ]]; then + if grep -q 'SyliusShadcnAdminUiBundle' config/bundles.php; then + echo "Admin themes: ${current_themes:-none (neutral)}. Sidebar: $current_variant." + else + echo "The demo application runs BootstrapAdminUi: no theme." + fi + echo + usage + exit 0 +fi + +variant=$current_variant +themes=() +themes_given=false +for arg in "$@"; do + case "$arg" in + -h|--help) usage; exit 0 ;; + --sidebar=*) + variant=${arg#--sidebar=} + if [[ " $variants " != *" $variant "* ]]; then + echo "Unknown sidebar variant \"$variant\": use one of $variants." >&2 + exit 1 + fi + ;; + none) themes_given=true ;; + -*) echo "Unknown option \"$arg\"." >&2; usage >&2; exit 1 ;; + *) + if [[ ! -f "$themes_dir/$arg.css" ]]; then + echo "Unknown theme \"$arg\". Available themes: $available" >&2 + exit 1 + fi + themes+=("$arg") + themes_given=true + ;; + esac +done + +if [[ "$themes_given" == false && -n "$current_themes" ]]; then + read -r -a themes <<< "$current_themes" +fi + +# The themes belong to ShadcnAdminUi: enable it first if needed. +if ! grep -q 'SyliusShadcnAdminUiBundle' config/bundles.php; then + bin/switch-admin-ui shadcn +fi + +cat > "$config" <> "$config" +fi +if [[ "$variant" != sidebar ]]; then + printf ' sidebar:\n variant: %s\n' "$variant" >> "$config" +fi +if [[ ${#themes[@]} -gt 0 ]]; then + list=$(printf '%s, ' "${themes[@]}") + printf ' themes: [%s]\n' "${list%, }" >> "$config" +fi + +php bin/console cache:clear --quiet + +# PHP-FPM only revalidates the cached PHP files every 2 seconds (opcache.revalidate_freq): wait for the new container. +sleep 2 + +echo "Admin themes: ${themes[*]:-none (neutral)}. Sidebar: $variant." diff --git a/bin/switch-admin-ui b/bin/switch-admin-ui new file mode 100755 index 00000000..2da4658f --- /dev/null +++ b/bin/switch-admin-ui @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Switches the demo application between BootstrapAdminUi and ShadcnAdminUi. +# +# Usage: bin/switch-admin-ui [bootstrap|shadcn] (no argument: toggles) + +set -euo pipefail + +cd "$(dirname "$0")/.." + +current() { + if grep -q 'SyliusShadcnAdminUiBundle' config/bundles.php; then echo shadcn; else echo bootstrap; fi +} + +from=$(current) +to=${1:-} +if [[ -z "$to" ]]; then + [[ "$from" == shadcn ]] && to=bootstrap || to=shadcn +fi + +case "$to" in + bootstrap) Ns=Bootstrap; prefix=bootstrap; tom_select=true ;; + shadcn) Ns=Shadcn; prefix=shadcn; tom_select=false ;; + *) echo "Usage: $0 [bootstrap|shadcn]" >&2; exit 1 ;; +esac + +if [[ "$from" == "$to" ]]; then + echo "Admin UI is already \"$to\"." + exit 0 +fi + +[[ "$from" == shadcn ]] && OldNs=Shadcn || OldNs=Bootstrap +old_prefix=$(echo "$OldNs" | tr '[:upper:]' '[:lower:]') + +# Kernel: only one of the two bundles can be enabled. +perl -pi -e "s/Sylius\\\\${OldNs}AdminUi\\\\Symfony\\\\Sylius${OldNs}AdminUiBundle/Sylius\\\\${Ns}AdminUi\\\\Symfony\\\\Sylius${Ns}AdminUiBundle/" config/bundles.php + +# Theme configuration (hooks, grid templates, component prefixes). +rm -f "config/packages/sylius_${old_prefix}_admin_ui.yaml" +cat > "config/packages/sylius_${prefix}_admin_ui.yaml" </dev/null \ + | xargs -r perl -pi -e "s/\@Sylius${OldNs}AdminUi/\@Sylius${Ns}AdminUi/g; s/sylius_${old_prefix}_admin_ui:/sylius_${prefix}_admin_ui:/g" + +# The shadcn theme fully replaces the Tom Select stylesheet. +perl -pi -e "s#(\"tom-select/dist/css/tom-select.default.css\": )(true|false)#\${1}${tom_select}#" assets/controllers.json + +php bin/console cache:clear --quiet + +# PHP-FPM only revalidates the cached PHP files every 2 seconds (opcache.revalidate_freq): wait for the new container. +sleep 2 + +if [[ "$to" == shadcn ]]; then + php bin/console tailwind:build +fi + +echo "Admin UI switched from \"$from\" to \"$to\"." diff --git a/composer.json b/composer.json index dae6c206..fcaf134d 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,9 @@ "symfony/ux-icons": "^2.20 || ^3.0", "symfony/ux-live-component": "^2.17 || ^3.0", "symfony/ux-twig-component": "^2.17 || ^3.0", + "symfonycasts/tailwind-bundle": "^0.13 || ^1.0", + "tales-from-a-dev/twig-tailwind-extra": "^1.3", + "twig/extra-bundle": "^3.24", "twig/twig": "^2.15 || ^3.0", "webmozart/assert": "^1.9" }, @@ -63,6 +66,7 @@ "symfony/maker-bundle": "^1.61", "symfony/runtime": "^6.4 || ^7.4 || ^8.0", "symfony/translation": "^6.4 || ^7.4 || ^8.0", + "symfony/ux-toolkit": "v2.36.1 || ^3.7", "symfony/web-profiler-bundle": "^6.4 || ^7.4 || ^8.0", "symfony/workflow": "^6.4 || ^7.4 || ^8.0", "symfony/yaml": "^6.4 || ^7.4 || ^8.0", @@ -74,6 +78,7 @@ "psr-4": { "Sylius\\AdminUi\\": "src/AdminUi/src/", "Sylius\\BootstrapAdminUi\\": "src/BootstrapAdminUi/src/", + "Sylius\\ShadcnAdminUi\\": "src/ShadcnAdminUi/src/", "Sylius\\TwigExtra\\": "src/TwigExtra/src/", "Sylius\\TwigHooks\\": "src/TwigHooks/src/", "Sylius\\UiTranslations\\": "src/UiTranslations/src/" @@ -85,10 +90,12 @@ "MainTests\\Sylius\\": "tests/", "TestApplication\\Sylius\\AdminUi\\": "src/AdminUi/tests/Functional/.application/src/", "TestApplication\\Sylius\\BootstrapAdminUi\\": "src/BootstrapAdminUi/tests/Functional/.application/src/", + "TestApplication\\Sylius\\ShadcnAdminUi\\": "src/ShadcnAdminUi/tests/Functional/.application/src/", "TestApplication\\Sylius\\TwigExtra\\": "src/TwigExtra/tests/Functional/.application/src/", "TestApplication\\Sylius\\TwigHooks\\": "src/TwigHooks/tests/Functional/.application/src/", "TestApplication\\Sylius\\UiTranslations\\": "src/UiTranslations/tests/Functional/.application/src/", "Tests\\Sylius\\AdminUi\\": "src/AdminUi/tests/", + "Tests\\Sylius\\ShadcnAdminUi\\": "src/ShadcnAdminUi/tests/", "Tests\\Sylius\\TwigExtra\\": "src/TwigExtra/tests/", "Tests\\Sylius\\TwigHooks\\": "src/TwigHooks/tests/", "Tests\\Sylius\\UiTranslations\\": "src/UiTranslations/tests/" @@ -97,6 +104,7 @@ "replace": { "sylius/admin-ui": "self.version", "sylius/bootstrap-admin-ui": "self.version", + "sylius/shadcn-admin-ui": "self.version", "sylius/twig-extra": "self.version", "sylius/twig-hooks": "self.version", "sylius/ui-translations": "self.version" diff --git a/config/bundles.php b/config/bundles.php index 3b765680..8521998c 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -25,4 +25,7 @@ Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\UX\Icons\UXIconsBundle::class => ['all' => true], + Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + TalesFromADev\Twig\Extra\Tailwind\Bridge\Symfony\Bundle\TalesFromADevTwigExtraTailwindBundle::class => ['all' => true], ]; diff --git a/config/packages/sylius_bootstrap_admin_ui.yaml b/config/packages/sylius_bootstrap_admin_ui.yaml index 5186cc24..a7f852c1 100644 --- a/config/packages/sylius_bootstrap_admin_ui.yaml +++ b/config/packages/sylius_bootstrap_admin_ui.yaml @@ -1,2 +1,2 @@ imports: - - { resource: '../../src/BootstrapAdminUi/config/app.php' } + - { resource: '../../src/BootstrapAdminUi/config/app.php' } diff --git a/config/packages/symfonycasts_tailwind.yaml b/config/packages/symfonycasts_tailwind.yaml new file mode 100644 index 00000000..41d66f8f --- /dev/null +++ b/config/packages/symfonycasts_tailwind.yaml @@ -0,0 +1,4 @@ +symfonycasts_tailwind: + binary_version: 'v4.3.3' + input_css: + - '%kernel.project_dir%/assets/styles/admin.css' diff --git a/docs/.gitbook/assets/shadcn-admin-ui-dashboard.png b/docs/.gitbook/assets/shadcn-admin-ui-dashboard.png new file mode 100644 index 00000000..142e4695 Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-dashboard.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-form.png b/docs/.gitbook/assets/shadcn-admin-ui-form.png new file mode 100644 index 00000000..a42cb734 Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-form.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-list.png b/docs/.gitbook/assets/shadcn-admin-ui-list.png new file mode 100644 index 00000000..1c6af1de Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-list.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-login.png b/docs/.gitbook/assets/shadcn-admin-ui-login.png new file mode 100644 index 00000000..96541d42 Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-login.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-theme-default.png b/docs/.gitbook/assets/shadcn-admin-ui-theme-default.png new file mode 100644 index 00000000..53a4578e Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-theme-default.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-theme-green.png b/docs/.gitbook/assets/shadcn-admin-ui-theme-green.png new file mode 100644 index 00000000..14832112 Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-theme-green.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-theme-rose.png b/docs/.gitbook/assets/shadcn-admin-ui-theme-rose.png new file mode 100644 index 00000000..e6ba696f Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-theme-rose.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-theme-slate-violet.png b/docs/.gitbook/assets/shadcn-admin-ui-theme-slate-violet.png new file mode 100644 index 00000000..6237acec Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-theme-slate-violet.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-theme-stone-orange.png b/docs/.gitbook/assets/shadcn-admin-ui-theme-stone-orange.png new file mode 100644 index 00000000..2299cc0e Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-theme-stone-orange.png differ diff --git a/docs/.gitbook/assets/shadcn-admin-ui-theme-zinc-blue.png b/docs/.gitbook/assets/shadcn-admin-ui-theme-zinc-blue.png new file mode 100644 index 00000000..928be19d Binary files /dev/null and b/docs/.gitbook/assets/shadcn-admin-ui-theme-zinc-blue.png differ diff --git a/docs/README.md b/docs/README.md index e3deef0b..ebfc67b2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ The Sylius stack is a set of tools for your Symfony projects. It comes with a bu * [**GridBundle:** Amazing grids with support of filters and custom fields integrated into Symfony](grid/index.md) * [**AdminUi:** Minimalist generic templates for your admin panels](admin-ui/getting-started.md) * [**BootstrapAdminUi:** Build your Bootstrap admin panels with Sylius and Symfony UX](bootstrap-admin-ui/getting-started.md) +* [**ShadcnAdminUi:** Build your Tailwind CSS and shadcn/ui admin panels with Sylius and Symfony UX](shadcn-admin-ui/getting-started.md) * [**TwigExtra:** Additional Twig extensions for your Symfony projects](twig-extra/getting-started.md) * [**TwigHooks:** Composable Twig layouts](twig-hooks/getting-started.md) * [**UiTranslations:** Basic UI translations](https://github.com/Sylius/Stack/blob/main/docs/ui-translations/getting-started.md) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c1432c37..bfec56cf 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -71,6 +71,16 @@ * Components * [Copy to clipboard](bootstrap-admin-ui/components/copy-to-clipboard.md) +## 🌑 Shadcn Admin UI + +* [Getting started](shadcn-admin-ui/getting-started.md) +* [Configuration](shadcn-admin-ui/configuration.md) +* [Theming](shadcn-admin-ui/theming.md) +* [Grids](shadcn-admin-ui/grids.md) +* [Menu](shadcn-admin-ui/menu.md) +* [Components](shadcn-admin-ui/components.md) +* [Migrating from Bootstrap Admin UI](shadcn-admin-ui/migration.md) + ## 🍀 Twig Extra * [Getting started](twig-extra/getting-started.md) diff --git a/docs/shadcn-admin-ui/components.md b/docs/shadcn-admin-ui/components.md new file mode 100644 index 00000000..150d767b --- /dev/null +++ b/docs/shadcn-admin-ui/components.md @@ -0,0 +1,212 @@ +# Components + +## shadcn components + +The shadcn kit recipes of Symfony UX Toolkit are bundled under the `shadcn:` prefix and usable in any template: + +```twig + + + Stock + + + 42 parcels + + +``` + +| Category | Components | +|---------------|----------------------------------------------------------------------------------------------| +| Layout | `Card`, `Separator`, `Sidebar`, `Item`, `Empty`, `Skeleton` | +| Forms | `Field`, `Input`, `InputGroup`, `Label`, `NativeSelect`, `Checkbox`, `RadioGroup`, `Switch`, `Textarea` | +| Actions | `Button`, `DropdownMenu`, `Kbd`, `KbdGroup` | +| Overlays | `Dialog`, `AlertDialog`, `Sheet`, `Tooltip` | +| Feedback | `Alert`, `Badge`, `Spinner` | +| Navigation | `Breadcrumb`, `Pagination`, `Collapsible` | +| Data | `Avatar`, `Table` | + +{% hint style="info" %} +The [shadcn kit documentation](https://ux.symfony.com/toolkit/kits/shadcn) describes the props and sub-components +of each recipe. +{% endhint %} + +## Sylius components + +Same components as BootstrapAdminUi, under the `sylius_shadcn_admin_ui:` prefix. + +| Component | Purpose | Main props | +|--------------------------------------|-------------------------------------------------|-------------------------------------------------------------------| +| `sylius_shadcn_admin_ui:delete_modal` | Delete button + confirmation dialog + CSRF form | `id`, `path`, `label`, `icon`, `icon_only`, `disabled`, `form_attr` | +| `sylius_shadcn_admin_ui:confirmation_modal` | Generic confirmation dialog | `modal_id`, `title`, `description`, `size`; blocks `trigger`, `footer` | +| `sylius_shadcn_admin_ui:copyable` | Value followed by a copy button | `value`, `copy_label`, `copied_label`, `error_label` | +| `sylius_shadcn_admin_ui:copy_to_clipboard` | Copy button alone | `value`, `button_class`, `disabled` | +| `sylius_shadcn_admin_ui:badge` | Colored badge | `color`, `light`, `href`, `label` | +| `sylius_shadcn_admin_ui:dot` | Status dot | `color`, `pulse` | + +```twig +Paid + + + + +``` + +`color` accepts every name of the [tone palette](theming.md#the-tone-macro). The `pulse` halo is disabled when the +user prefers reduced motion. + +## Macros + +Same macro APIs as `@SyliusBootstrapAdminUi`: only the namespace changes. + +| Template (`@SyliusShadcnAdminUi/shared/helper/…`) | Macros | +|--------------------------------------------------|---------------------------------------------------------------------| +| `button.html.twig` | `default`, `primary`, `delete`, `create`, `update`, `cancel` | +| `breadcrumbs.html.twig` | `breadcrumbs(items)` | +| `header.html.twig` | `default(level, header, icon, subheader)`, `h1(header, icon, subheader)` | +| `dropdown.html.twig` | `default(params)`, `list(params, items)`, `item(attributes, content, variant)` | +| `table.html.twig` | `headers`, `row` | +| `pagination.html.twig` | `simple(paginator, options)`, `results_count(paginator)` | +| `accordion.html.twig` | `accordion(items, open, auto_close, flush)` | +| `avatar.html.twig` | `default(img, class, initials)`, `small(img, class, initials)` | +| `palette.html.twig` | [`tone(color, kind)`](theming.md#the-tone-macro) | + +The button macros accept an extra `variant` param: `default`, `secondary`, `outline`, `ghost`, `destructive`, `link`. + +```twig +{% import '@SyliusShadcnAdminUi/shared/helper/button.html.twig' as button %} + +{{ button.primary({text: 'Export', icon: 'lucide:download', url: path('app_export')}) }} +{{ button.default({text: 'Archive', variant: 'ghost', size: 'small'}) }} +``` + +## Tabs + +Accessible tabs with the `admin--tabs` controller (`tab` and `panel` targets): + +- keyboard navigation: arrows, Home, End (roving tabindex); +- the selected tab is kept in the URL hash; +- tabs holding a validation error are flagged and opened on submit. + +```twig +
+
+ + +
+
…
+
…
+
+``` + +## Form theme + +The admin forms use the shadcn form theme. Use it anywhere: + +```twig +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} +``` + +Render a checkbox as a switch with `label_attr: {class: 'checkbox-switch'}` or `attr: {role: 'switch'}`: + +```php +$builder->add('enabled', CheckboxType::class, ['attr' => ['role' => 'switch']]); +``` + +## Stimulus controllers + +Registered automatically by the bundle. + +| Identifier | Role | +|---------------------------------------------|------------------------------------------------------------------| +| `admin--tabs` | Accessible tabs ([see above](#tabs)) | +| `admin--grid-selection` | Row selection, "select all", bulk action forms | +| `admin--copy-to-clipboard` | Copy buttons | +| `admin--dismissible` | Closable flash messages and alerts | +| `admin--menu-search` | Sidebar menu search | +| `admin--sidebar-trigger` | Navbar button that toggles the sidebar | +| `admin--theme-switcher` | Light / dark switch | +| `shadcn--sidebar`, `shadcn--dialog`, `shadcn--alert-dialog`, `shadcn--dropdown-menu`, `shadcn--dropdown-menu-sub`, `shadcn--collapsible`, `shadcn--tooltip`, `shadcn--input-group`, `shadcn--input-group-display` | Behaviour of the shadcn recipes | + +## Symfony UX 2 + +The shadcn kit relies on Symfony UX 3.5+ features. Under Symfony UX 2, the package registers a compatibility extension that provides `provide()`, `inject()` and the conflict resolution of `tailwind_classes`. + +{% hint style="warning" %} +Under Symfony UX 2, `tailwind_classes` assumes the classes of the caller apply to the element using the filter. Use it on the root element of your components, through `attributes.defaults()`: on an inner element, default classes conflicting with the caller classes are dropped. +{% endhint %} + +## Updating the shadcn recipes + +The components of `templates/components/shadcn/` and the controllers of `assets/controllers/shadcn/` are recipes of +the [shadcn kit of Symfony UX Toolkit](https://ux.symfony.com/toolkit/kits/shadcn), renamed for the bundle. +`app:shadcn:update`, a command of the test application, installs them again from `symfony/ux-toolkit` and applies the renaming. + +{% hint style="warning" %} +`symfony/ux-toolkit` is a development dependency of the package, enabled in the test application. Its 3.x versions +require PHP 8.4: on an older PHP version, Composer installs v2.36.1, whose kit is outdated. Run this procedure on +PHP 8.4+. +{% endhint %} + +| Recipe | Bundle | +|------------------------------------------|---------------------------------------------------| +| `templates/components/Button.html.twig` | `templates/components/shadcn/Button.html.twig` | +| `assets/controllers/dialog_controller.js`| `assets/controllers/shadcn/dialog_controller.js` | +| ``, `component('Button')` | ``, `component('shadcn:Button')` | +| `data-controller="dialog"`, `click->dialog#open`, `data-dialog-target`, `data-dialog-open-value` | `shadcn--dialog` everywhere | + +### 1. Update the kit + +```bash +composer update symfony/ux-toolkit +``` + +### 2. Install the recipes + +```bash +APP_ENV=dev php tests/Functional/.application/bin/console app:shadcn:update # every bundled recipe +APP_ENV=dev php tests/Functional/.application/bin/console app:shadcn:update button dialog # some of them +``` + +{% hint style="info" %} +Without argument, the bundled recipes are deduced from the components of `templates/components/shadcn/` +(`AlertDialog.html.twig` → `alert-dialog`). Recipe dependencies (e.g. `button` for `dialog`) are installed too. +To add a new recipe, pass its name. +{% endhint %} + +### 3. Review the diff + +The files are **overwritten**: `git diff` shows both the changes of the kit and the local changes of the bundle that +were lost. Keep the former, restore the latter (`git checkout -p`). + +Local changes to restore after an update (as of UX Toolkit 3.6): + +| File | Local change | +|------------------------------------------------------------|------------------------------------------------------------------------------| +| `DropdownMenu/{Item,CheckboxItem,RadioItem,SubTrigger}` | `cursor-pointer` instead of `cursor-default` | +| `DropdownMenu/Content` | `left` / `right` sides combined with `align` (`start`, `center`, `end`) | +| `dropdown_menu_controller.js` | A pointer click focuses the menu, not its first item (no highlighted item) | +| `Sidebar` | Nested sidebar (`sidebar.nested`: `
` instead of `
{% endif %} +
+
+ {{ block('label') }} +
+
    + {% set group_open = true %} + {% else %} + {% if not group_open %} +
    +
      + {% set group_open = true %} + {% endif %} + {{ block('item') }} + {% endif %} + {% endfor %} + {% if group_open %}
    {% endif %} +{% endblock %} + +{% block item %} + {% if item.displayed %} + {%- set button_class = 'peer/menu-button flex h-8 w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:[&>:not(:first-child)]:hidden [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0' -%} + {%- set is_current = matcher.isCurrent(item) -%} + {%- set is_open = is_current or matcher.isAncestor(item, options.matchingDepth) or item.getExtra('always_open', false) -%} + {%- set target = item.linkAttributes and item.linkAttributes['target'] is defined ? item.linkAttributes['target'] : '_self' -%} + {%- set has_children = item.hasChildren and item.displayChildren and options.depth is not same as(0) -%} + +
  • + {% if has_children %} +
    + +
    +
      + {% set parent = item %} + {# Children after a header are indented under it as a tree branch. #} + {% set in_sub_section = false %} + {% for item in parent.children %} + {% if item.displayed and item.getExtra('header', false) %}{% set in_sub_section = true %}{% endif %} + {{ block('child_item') }} + {% endfor %} + {% set item = parent %} +
    +
    +
    + {% else %} + + {{ block('icon') }} + {{ block('label') }} + {% if target == '_blank' %}{{ ux_icon('lucide:external-link', {class: 'ms-auto size-3.5! opacity-60', 'aria-hidden': 'true'}) }}{% endif %} + + {% endif %} +
  • + {% endif %} +{% endblock %} + +{% block child_item %} + {% if item.displayed and item.getExtra('header', false) %} + {# Sub-section title drawn as a node on the sub menu vertical line (border-s of sidebar-menu-sub, px-2.5 away). #} +
  • + + {{ block('label') }} +
  • + {% elseif item.displayed %} + {%- set is_current = matcher.isCurrent(item) or matcher.isAncestor(item, options.matchingDepth) -%} + {%- set target = item.linkAttributes and item.linkAttributes['target'] is defined ? item.linkAttributes['target'] : '_self' -%} + {# The branch line starts under the header label (px-2) and covers the gap-1 above to stay continuous. #} +
  • + + {{ block('label') }} + +
  • + {% endif %} +{% endblock %} + +{% block icon %} + {%- set icon_name = item.labelAttribute('icon') -%} + {%- if icon_name -%} + {{ ux_icon(icon_name, {'aria-hidden': 'true'}) }} + {%- elseif item.level == 1 -%} + {# Keeps something visible when the sidebar is collapsed to icons. #} + + {%- endif -%} +{% endblock %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/common/sidebar/search.html.twig b/src/ShadcnAdminUi/templates/shared/crud/common/sidebar/search.html.twig new file mode 100644 index 00000000..2f488f08 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/common/sidebar/search.html.twig @@ -0,0 +1,13 @@ +
    + + {{ ux_icon('lucide:search', {class: 'pointer-events-none absolute start-4 top-1/2 mt-1 size-4 -translate-y-1/2 text-muted-foreground', 'aria-hidden': 'true'}) }} + +
    diff --git a/src/ShadcnAdminUi/templates/shared/crud/common/title/current.html.twig b/src/ShadcnAdminUi/templates/shared/crud/common/title/current.html.twig new file mode 100644 index 00000000..e86d52ce --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/common/title/current.html.twig @@ -0,0 +1,4 @@ +{% set metadata = hookable_metadata.context.metadata %} +{% set header = hookable_metadata.configuration.header|default(metadata.applicationName ~ '.ui.' ~ metadata.pluralName) %} + +{{ header|trans }} diff --git a/src/ShadcnAdminUi/templates/shared/crud/common/title/parent.html.twig b/src/ShadcnAdminUi/templates/shared/crud/common/title/parent.html.twig new file mode 100644 index 00000000..2139f921 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/common/title/parent.html.twig @@ -0,0 +1,2 @@ +{% set parent = hookable_metadata.context.parent %} +{{ parent }} diff --git a/src/ShadcnAdminUi/templates/shared/crud/common/title/separator.html.twig b/src/ShadcnAdminUi/templates/shared/crud/common/title/separator.html.twig new file mode 100644 index 00000000..948cf947 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/common/title/separator.html.twig @@ -0,0 +1 @@ +| diff --git a/src/ShadcnAdminUi/templates/shared/crud/create/content/header/breadcrumbs.html.twig b/src/ShadcnAdminUi/templates/shared/crud/create/content/header/breadcrumbs.html.twig new file mode 100644 index 00000000..6da95d61 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/create/content/header/breadcrumbs.html.twig @@ -0,0 +1,30 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/breadcrumbs.html.twig' import breadcrumbs %} + +{% if hookable_metadata.context.metadata is defined %} + {% set metadata = hookable_metadata.context.metadata %} + {% set resource_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ metadata.pluralName) %} +{% else %} + {% set resource_name = hookable_metadata.configuration.resource_name|default(null) %} +{% endif %} + +{% set configuration = hookable_metadata.context.configuration|default(null) %} + +{% if configuration is not null %} + {% set index_url = path( + configuration.vars.index.route.name|default(configuration.getRouteName('index')), + configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({})), + ) %} +{% endif %} + +{% if index_url is defined and resource_name is not null %} + {{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + {name: resource_name, url: index_url, active: false}, + {name: 'sylius.ui.new'|trans, active: true}, + ]) }} +{% else %} + {{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + {name: 'sylius.ui.new'|trans, active: true}, + ]) }} +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/create/content/header/title_block/actions.html.twig b/src/ShadcnAdminUi/templates/shared/crud/create/content/header/title_block/actions.html.twig new file mode 100644 index 00000000..b9fe554c --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/create/content/header/title_block/actions.html.twig @@ -0,0 +1,3 @@ +
    + {% hook 'actions' %} +
    diff --git a/src/ShadcnAdminUi/templates/shared/crud/create/content/header/title_block/title.html.twig b/src/ShadcnAdminUi/templates/shared/crud/create/content/header/title_block/title.html.twig new file mode 100644 index 00000000..45b4e59f --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/create/content/header/title_block/title.html.twig @@ -0,0 +1,17 @@ +{% import '@SyliusShadcnAdminUi/shared/helper/header.html.twig' as header %} + +{% if hookable_metadata.context.metadata is defined and hookable_metadata.context.configuration is defined %} + {% set metadata = hookable_metadata.context.metadata %} + {% set singular_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ metadata.name) %} +{% else %} + {% set singular_name = hookable_metadata.configuration.resource_name|default(null) %} +{% endif %} +{% set header = hookable_metadata.configuration.title|default(hookable_metadata.configuration.header|default(null)) %} + +{% set icon = hookable_metadata.configuration.icon|default(null) %} +{% set subheader = hookable_metadata.configuration.subheader|default(null) %} +{% set test_attribute = hookable_metadata.configuration.sylius_test_html_attribute is defined ? sylius_test_html_attribute(hookable_metadata.configuration.sylius_test_html_attribute) : null %} + +
    + {{ header.h1(header is not null ? header|trans : 'sylius.ui.new'|trans ~ ' ' ~ singular_name|trans, icon, subheader|trans) }} +
    diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid.html.twig new file mode 100644 index 00000000..991660e7 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid.html.twig @@ -0,0 +1,3 @@ +
    + {% hook 'grid' %} +
    diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/data_table.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/data_table.html.twig new file mode 100644 index 00000000..e5673e26 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/data_table.html.twig @@ -0,0 +1,48 @@ +{% import '@SyliusShadcnAdminUi/shared/helper/table.html.twig' as table %} +{% import '@SyliusShadcnAdminUi/shared/helper/pagination.html.twig' as pagination %} + +{% set resources = resources|default(hookable_metadata.context.resources|default(null)) %} +{% set data = resources.data|default([]) %} +{% set definition = resources.definition|default(null) %} +{% set has_bulk_actions = definition.actionGroups.bulk is defined and definition.getEnabledActions('bulk')|length > 0 %} + +{% if data|length > 0 %} +
    +
    + {% if has_bulk_actions %} +
    + {% for action in definition.getEnabledActions('bulk')|sylius_sort_by('position') %} + {{ sylius_grid_render_bulk_action(resources, action, null) }} + {% endfor %} +
    + {% endif %} +
    + {{ pagination.number_of_results_selector(data, definition.limits) }} +
    +
    + +
    + + + + {{ table.headers(resources, resources.definition, app.request.attributes) }} + + + + {% for row in resources.data %} + {{ table.row(resources, resources.definition, row) }} + {% endfor %} + +
    +
    + +
    +
    + {{ pagination.results_count(data) }} +
    +
    + {{ pagination.simple(data) }} +
    +
    +
    +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/filters.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/filters.html.twig new file mode 100644 index 00000000..96d50fc5 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/filters.html.twig @@ -0,0 +1,68 @@ +{# + # Collapsible filters panel. Hookable configuration: + # open: true / false, open on load when criteria are set by default + # indicator: false hides the dot of the trigger shown when criteria are set + # Inner hooks: "filters#trigger" (inside the trigger, after its title) and "filters#summary" (between the trigger + # and the form, visible when the panel is collapsed). + #} +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + +{% set resources = resources|default(hookable_metadata.context.resources|default(null)) %} +{% set path = path(app.request.attributes.get('_route'), app.request.attributes.all('_route_params')) %} +{% set criteria = app.request.query.all('criteria') %} +{% set configuration = hookable_metadata.configuration|default({}) %} +{% set open = configuration.open ?? (criteria is not empty) %} +{% set enabled_filters = resources.definition.enabledFilters|default([])|filter(filter => filter.enabled) %} + +{% if enabled_filters is not empty %} +
    + + + {% hook 'filters#summary' %} + +
    +
    + {% if app.request.query.has('limit') %} + + {% endif %} +
    + {% for filter in enabled_filters|sylius_sort_by('position') %} +
    + {{ sylius_grid_render_filter(resources, filter) }} +
    + {% endfor %} +
    +
    + + + {{ ux_icon('lucide:rotate-ccw', {'data-icon': 'inline-start', 'aria-hidden': 'true'}) }} + {{ 'sylius.ui.reset'|trans }} + +
    +
    +
    +
    +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results.html.twig new file mode 100644 index 00000000..5d829bc5 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results.html.twig @@ -0,0 +1,7 @@ +{% set resources = resources|default(hookable_metadata.context.resources|default(null)) %} + +{% if resources.data is defined and resources.data|length == 0 %} +
    + {% hook 'no_results' %} +
    +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/action.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/action.html.twig new file mode 100644 index 00000000..0bf197c2 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/action.html.twig @@ -0,0 +1,8 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + +{% if _context.route is defined and (_context.hidden is not defined or _context.hidden == false) %} + + {{ ux_icon(_context.icon|default('lucide:plus'), {'data-icon': 'inline-start', 'aria-hidden': 'true'}) }} + {{ _context.translation_key|default('sylius.ui.add_new_entry')|trans }} + +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/image.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/image.html.twig new file mode 100644 index 00000000..16a7c1bb --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/image.html.twig @@ -0,0 +1,9 @@ +{% if _context.hidden is not defined or _context.hidden == false %} +
    + {% if _context.image is defined %} + + {% else %} + {{ ux_icon(_context.icon|default('lucide:inbox'), {class: 'size-5', 'aria-hidden': 'true'}) }} + {% endif %} +
    +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/subtitle.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/subtitle.html.twig new file mode 100644 index 00000000..479b5e70 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/subtitle.html.twig @@ -0,0 +1,5 @@ +{% if _context.hidden is not defined or _context.hidden == false %} +

    + {{ _context.translation_key|default('sylius.ui.no_results_adjust_your_search')|trans }} +

    +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/title.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/title.html.twig new file mode 100644 index 00000000..c245cbb4 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/grid/no_results/title.html.twig @@ -0,0 +1,3 @@ +{% if _context.hidden is not defined or _context.hidden == false %} +

    {{ _context.translation_key|default('sylius.ui.no_results')|trans }}

    +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/index/content/header/breadcrumbs.html.twig b/src/ShadcnAdminUi/templates/shared/crud/index/content/header/breadcrumbs.html.twig new file mode 100644 index 00000000..c97a908a --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/index/content/header/breadcrumbs.html.twig @@ -0,0 +1,15 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/breadcrumbs.html.twig' import breadcrumbs %} + +{% set title_from_context = hookable_metadata.context.title|default(null) %} +{% set resource_metadata = hookable_metadata.context.metadata|default(null) %} + +{% if title_from_context is not null or resource_metadata is not null %} + {% set title = title_from_context|default(resource_metadata.applicationName ~ '.ui.' ~ resource_metadata.pluralName) %} +{% else %} + {% set title = hookable_metadata.configuration.title|default(null) %} +{% endif %} + +{{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + {name: title|default('sylius.ui.index'|trans), active: true}, +]) }} diff --git a/src/ShadcnAdminUi/templates/shared/crud/show/content/header/breadcrumbs.html.twig b/src/ShadcnAdminUi/templates/shared/crud/show/content/header/breadcrumbs.html.twig new file mode 100644 index 00000000..3b000b4c --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/show/content/header/breadcrumbs.html.twig @@ -0,0 +1,47 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/breadcrumbs.html.twig' import breadcrumbs %} + +{% set configuration = hookable_metadata.context.configuration|default(null) %} +{% set resource = hookable_metadata.context.resource|default(null) %} +{% set metadata = hookable_metadata.context.metadata|default(null) %} + +{% if configuration and metadata %} + {% set resource_index = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %} + {% set index_url = path( + configuration.vars.index.route.name|default(configuration.getRouteName('index')), + configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({})), + ) %} +{% endif %} + +{% set title = hookable_metadata.configuration.title|default(null) %} +{% set rendered_field_prefix = hookable_metadata.configuration.rendered_field_prefix|default(null) %} +{% set field_name = hookable_metadata.configuration.rendered_field|default('') %} +{% set rendered_field = rendered_field_prefix ~ (attribute(resource, field_name) is defined ? attribute(resource, field_name) : null) %} + +{% set action = hookable_metadata.configuration.action|default(false) + ? {name: hookable_metadata.configuration.action, active: false} + : {} +%} + +{% set resource_show_name = title|default(rendered_field|default(resource.translations[app.locale].name|default(resource.code|default(resource.id|default(null))))) %} + +{% if action is defined and action.name is defined %} + {% set show_url = path(configuration.getRouteName('show'), {id: resource.id}) %} + {% set resource_show = {name: resource_show_name, url: show_url} %} +{% else %} + {% set resource_show = {name: resource_show_name|default('sylius.ui.show'|trans), active: true} %} +{% endif %} + +{% if resource_index is defined and index_url is defined %} + {{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + {name: resource_index, url: index_url, active: false}, + resource_show, + action, + ]) }} +{% else %} + {{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + resource_show, + action, + ]) }} +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/show/content/header/title_block/title.html.twig b/src/ShadcnAdminUi/templates/shared/crud/show/content/header/title_block/title.html.twig new file mode 100644 index 00000000..699f7ec0 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/show/content/header/title_block/title.html.twig @@ -0,0 +1,17 @@ +{% import '@SyliusShadcnAdminUi/shared/helper/header.html.twig' as header %} + +{% if hookable_metadata.context.metadata is defined and hookable_metadata.context.configuration is defined %} + {% set metadata = hookable_metadata.context.metadata %} + {% set singular_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ metadata.name) %} +{% else %} + {% set singular_name = hookable_metadata.configuration.resource_name|default(null) %} +{% endif %} +{% set header = hookable_metadata.configuration.title|default(hookable_metadata.configuration.header|default(null)) %} + +{% set icon = hookable_metadata.configuration.icon|default(null) %} +{% set subheader = hookable_metadata.configuration.subheader|default(null) %} +{% set test_attribute = hookable_metadata.configuration.sylius_test_html_attribute is defined ? sylius_test_html_attribute(hookable_metadata.configuration.sylius_test_html_attribute) : null %} + +
    + {{ header.h1(header is not null ? header|trans : 'sylius.ui.show'|trans ~ ' ' ~ singular_name|trans, icon, subheader|trans) }} +
    diff --git a/src/ShadcnAdminUi/templates/shared/crud/update/content/header/breadcrumbs.html.twig b/src/ShadcnAdminUi/templates/shared/crud/update/content/header/breadcrumbs.html.twig new file mode 100644 index 00000000..684e7d9c --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/update/content/header/breadcrumbs.html.twig @@ -0,0 +1,41 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/breadcrumbs.html.twig' import breadcrumbs %} + +{% set configuration = hookable_metadata.context.configuration|default(null) %} +{% set resource = hookable_metadata.context.resource|default(null) %} +{% set metadata = hookable_metadata.context.metadata|default(null) %} + +{% if metadata is not null and configuration is not null %} + {% set resource_index = metadata.applicationName ~ '.ui.' ~ metadata.pluralName %} + {% set index_url = path( +configuration.vars.index.route.name|default(configuration.getRouteName('index')), +configuration.vars.index.route.parameters|default(configuration.vars.route.parameters|default({})), + ) %} +{% endif %} + +{% set rendered_field_prefix = hookable_metadata.configuration.rendered_field_prefix|default(null) %} +{% set field_name = hookable_metadata.configuration.rendered_field|default('') %} +{% set rendered_field = rendered_field_prefix ~ (attribute(resource, field_name) is defined ? attribute(resource, field_name) : null) %} + +{% set resource_edit_name = rendered_field|default(resource.translations[app.locale].name|default(resource.code|default(resource.id|default(null)))) %} +{% set show_route_name = configuration.vars.index.route.name|default(configuration.getRouteName('show')|default(null)) %} + +{% if show_route_name is not null and sylius_route_exists(show_route_name) %} + {% set show_url = path(show_route_name, {id: resource.id}) %} + {% set resource_edit = {name: resource_edit_name, url: show_url} %} +{% else %} + {% set resource_edit = {name: resource_edit_name, active: true} %} +{% endif %} + +{% if resource_index is defined %} + {{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + {name: resource_index, url: index_url, active: false}, + resource_edit, + {name: 'sylius.ui.edit'|trans, active: false}, + ]) }} +{% else %} + {{ breadcrumbs([ + {name: 'sylius.ui.dashboard', url: path('sylius_admin_ui_dashboard'), active: false}, + {name: 'sylius.ui.edit'|trans, active: false}, + ]) }} +{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/crud/update/content/header/title_block/actions.html.twig b/src/ShadcnAdminUi/templates/shared/crud/update/content/header/title_block/actions.html.twig new file mode 100644 index 00000000..b9fe554c --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/update/content/header/title_block/actions.html.twig @@ -0,0 +1,3 @@ +
    + {% hook 'actions' %} +
    diff --git a/src/ShadcnAdminUi/templates/shared/crud/update/content/header/title_block/title.html.twig b/src/ShadcnAdminUi/templates/shared/crud/update/content/header/title_block/title.html.twig new file mode 100644 index 00000000..ee56104e --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/crud/update/content/header/title_block/title.html.twig @@ -0,0 +1,17 @@ +{% import '@SyliusShadcnAdminUi/shared/helper/header.html.twig' as header %} + +{% if hookable_metadata.context.metadata is defined and hookable_metadata.context.configuration is defined %} + {% set metadata = hookable_metadata.context.metadata %} + {% set singular_name = hookable_metadata.configuration.resource_name|default(metadata.applicationName ~ '.ui.' ~ metadata.name) %} +{% else %} + {% set singular_name = hookable_metadata.configuration.resource_name|default(null) %} +{% endif %} +{% set header = hookable_metadata.configuration.title|default(hookable_metadata.configuration.header|default(null)) %} + +{% set icon = hookable_metadata.configuration.icon|default(null) %} +{% set subheader = hookable_metadata.configuration.subheader|default(null) %} +{% set test_attribute = hookable_metadata.configuration.sylius_test_html_attribute is defined ? sylius_test_html_attribute(hookable_metadata.configuration.sylius_test_html_attribute) : null %} + +
    + {{ header.h1(header is not null ? header|trans : 'sylius.ui.edit'|trans ~ ' ' ~ singular_name|trans, icon, subheader|trans) }} +
    diff --git a/src/ShadcnAdminUi/templates/shared/form_theme.html.twig b/src/ShadcnAdminUi/templates/shared/form_theme.html.twig new file mode 100644 index 00000000..04f35ba6 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/form_theme.html.twig @@ -0,0 +1,338 @@ +{# + # Symfony form theme with the shadcn/ui look (Input, Textarea, NativeSelect, Checkbox, Switch, + # Label, Field). Classes are copied from the UX Toolkit shadcn recipes: keep them in sync when + # you re-install a recipe. + # + # Use it in your own templates with: + # {% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + # + # Switch: add 'checkbox-switch' to the label_attr class (Bootstrap convention) or role="switch" to attr. + #} +{% extends 'form_div_layout.html.twig' %} + +{# ----- Shared class strings ----------------------------------------------------------------- #} + +{%- block shadcn_input_class -%} + h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:[color-scheme:dark] +{%- endblock shadcn_input_class -%} + +{%- block shadcn_textarea_class -%} + flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 +{%- endblock shadcn_textarea_class -%} + +{%- block shadcn_select_class -%} + h-8 w-full min-w-0 appearance-none rounded-lg border border-input bg-transparent py-1 ps-2.5 pe-8 text-sm transition-colors outline-none select-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:[color-scheme:dark] [&_option]:bg-popover [&_option]:text-popover-foreground +{%- endblock shadcn_select_class -%} + +{%- block shadcn_checkbox_class -%} + peer col-start-1 row-start-1 size-4 appearance-none rounded-[4px] border border-input bg-transparent transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 checked:border-primary checked:bg-primary disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:checked:bg-primary dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 forced-colors:appearance-auto +{%- endblock shadcn_checkbox_class -%} + +{%- block shadcn_radio_class -%} + size-4 shrink-0 appearance-none rounded-full border border-input bg-transparent transition-all outline-none checked:border-[5px] checked:border-primary focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-input/30 dark:checked:border-primary +{%- endblock shadcn_radio_class -%} + +{# ----- Form ----------------------------------------------------------------------------------- #} + +{%- block form_start -%} + {%- do form.setMethodRendered() -%} + {% set method = method|upper %} + {%- if method in ['GET', 'POST'] -%} + {% set form_method = method %} + {%- else -%} + {% set form_method = 'POST' %} + {%- endif -%} + + {%- if form_method != method -%} + + {%- endif -%} +{%- endblock form_start -%} + +{%- block form_widget_compound -%} + {%- set attr = attr|merge({class: ('flex flex-col gap-6 ' ~ attr.class|default(''))|trim}) -%} +
    + {%- if form is not rootform -%} + {{- form_errors(form) -}} + {%- endif -%} + {{- block('form_rows') -}} + {{- form_rest(form) -}} +
    +{%- endblock form_widget_compound -%} + +{%- block collection_widget -%} + {%- if prototype is defined and not prototype.rendered -%} + {%- set attr = attr|merge({'data-prototype': form_row(prototype)}) -%} + {%- endif -%} + {{- block('form_widget') -}} +{%- endblock collection_widget -%} + +{# ----- Rows ----------------------------------------------------------------------------------- #} + +{%- block form_row -%} + {%- set widget_attr = {} -%} + {%- set aria_describedby = [] -%} + {%- if help -%} + {%- set aria_describedby = aria_describedby|merge([id ~ '_help']) -%} + {%- endif -%} + {%- if errors|length > 0 -%} + {%- set aria_describedby = aria_describedby|merge([id ~ '_error']) -%} + {%- set widget_attr = widget_attr|merge({'aria-invalid': 'true'}) -%} + {%- endif -%} + {%- if aria_describedby|length > 0 and not compound -%} + {%- set widget_attr = widget_attr|merge({'aria-describedby': aria_describedby|join(' ')}) -%} + {%- endif -%} + {%- set row_attr = row_attr|merge({class: ('group/field flex w-full min-w-0 flex-col gap-2 ' ~ row_attr.class|default(''))|trim}) -%} + + {{- form_label(form) -}} + {{- form_widget(form, {attr: widget_attr}) -}} + {{- form_help(form) -}} + {{- form_errors(form) -}} +
+{%- endblock form_row -%} + +{%- block checkbox_row -%} + {%- set widget_attr = {} -%} + {%- if help -%} + {%- set widget_attr = widget_attr|merge({'aria-describedby': id ~ '_help'}) -%} + {%- endif -%} + {%- if errors|length > 0 -%} + {%- set widget_attr = widget_attr|merge({'aria-invalid': 'true'}) -%} + {%- endif -%} + {%- set row_attr = row_attr|merge({class: ('group/field flex w-full flex-col gap-1.5 ' ~ row_attr.class|default(''))|trim}) -%} + +
+ {{- form_widget(form, {attr: widget_attr}) -}} + {{- form_label(form) -}} +
+
+ {{- form_help(form) -}} + {{- form_errors(form) -}} +
+ +{%- endblock checkbox_row -%} + +{%- block radio_row -%} + {{- block('checkbox_row') -}} +{%- endblock radio_row -%} + +{%- block button_row -%} + + {{- form_widget(form) -}} + +{%- endblock button_row -%} + +{# ----- Labels, help, errors --------------------------------------------------------------- #} + +{%- block form_label -%} + {%- if label is not same as(false) -%} + {%- if not compound -%} + {%- set label_attr = label_attr|merge({for: id}) -%} + {%- endif -%} + {%- set label_attr = label_attr|merge({class: ('flex w-fit items-center gap-1 text-sm leading-snug font-medium select-none group-data-[invalid=true]/field:text-destructive peer-disabled:cursor-not-allowed peer-disabled:opacity-50 ' ~ label_attr.class|default('')|replace({'checkbox-switch': ''}))|trim}) -%} + {%- if label is empty -%} + {%- if label_format is not empty -%} + {%- set label = label_format|replace({'%name%': name, '%id%': id}) -%} + {%- else -%} + {%- set label = name|humanize -%} + {%- endif -%} + {%- endif -%} + <{{ element|default('label') }}{% if label_attr %}{% with {attr: label_attr} %}{{ block('attributes') }}{% endwith %}{% endif %} data-slot="field-label"> + {{- block('form_label_content') -}} + {%- if required and not ('checkbox' in block_prefixes or 'radio' in block_prefixes) -%} + + {%- endif -%} + + {%- endif -%} +{%- endblock form_label -%} + +{%- block form_help -%} + {%- if help is not empty -%} + {%- set help_attr = help_attr|merge({class: ('text-start text-sm leading-normal font-normal text-muted-foreground ' ~ help_attr.class|default(''))|trim}) -%} +

+ {{- block('form_help_content') -}} +

+ {%- endif -%} +{%- endblock form_help -%} + +{%- block form_errors -%} + {%- if errors|length > 0 -%} + {%- if form is rootform -%} + + {%- else -%} + + {%- endif -%} + {%- endif -%} +{%- endblock form_errors -%} + +{# ----- Widgets -------------------------------------------------------------------------------- #} + +{%- block form_widget_simple -%} + {%- if type|default('text') not in ['hidden', 'range', 'color'] -%} + {%- set attr = attr|merge({class: (block('shadcn_input_class') ~ ' ' ~ attr.class|default(''))|tailwind_merge}) -%} + {%- endif -%} + {%- if type|default('text') == 'range' -%} + {%- set attr = attr|merge({class: ('w-full accent-primary ' ~ attr.class|default(''))|trim}) -%} + {%- endif -%} + {{- parent() -}} +{%- endblock form_widget_simple -%} + +{%- block textarea_widget -%} + {%- set attr = attr|merge({class: (block('shadcn_textarea_class') ~ ' ' ~ attr.class|default(''))|tailwind_merge}) -%} + {{- parent() -}} +{%- endblock textarea_widget -%} + +{%- block choice_widget_collapsed -%} + {%- set is_autocomplete = 'autocomplete' in attr['data-controller']|default('') -%} + {%- if is_autocomplete -%} + {#- Tom Select copies the select classes onto its wrapper: the combobox is drawn by theme.css (.ts-*). -#} + {%- set attr = attr|merge({class: ('w-full ' ~ attr.class|default(''))|trim}) -%} + {{- parent() -}} + {%- elseif multiple -%} + {%- set attr = attr|merge({class: (block('shadcn_select_class') ~ ' h-auto min-h-16 pe-2.5 ' ~ attr.class|default(''))|tailwind_merge}) -%} + {{- parent() -}} + {%- else -%} + {%- set attr = attr|merge({class: (block('shadcn_select_class') ~ ' ' ~ attr.class|default(''))|tailwind_merge}) -%} +
+ {{- parent() -}} + {{ ux_icon('lucide:chevron-down', {class: 'pointer-events-none absolute end-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground select-none', 'aria-hidden': 'true'}) }} +
+ {%- endif -%} +{%- endblock choice_widget_collapsed -%} + +{%- block choice_widget_expanded -%} + {%- set attr = attr|merge({class: ('flex flex-col gap-3 ' ~ attr.class|default(''))|trim, role: multiple ? 'group' : 'radiogroup'}) -%} +
+ {%- for child in form -%} +
+ {{- form_widget(child) -}} + {{- form_label(child, null, {translation_domain: choice_translation_domain, label_attr: {class: 'font-normal'}}) -}} +
+ {%- endfor -%} +
+{%- endblock choice_widget_expanded -%} + +{%- block checkbox_widget -%} + {%- set is_switch = 'checkbox-switch' in label_attr.class|default('') or attr.role|default('') == 'switch' -%} + {%- if is_switch -%} + {%- set attr = attr|merge({role: 'switch', class: ('absolute inset-0 cursor-pointer appearance-none rounded-full focus:outline-hidden disabled:cursor-not-allowed ' ~ attr.class|default(''))|trim}) -%} + + + {{- parent() -}} + + {%- else -%} + {%- set attr = attr|merge({class: (block('shadcn_checkbox_class') ~ ' ' ~ attr.class|default(''))|tailwind_merge}) -%} + + {{- parent() -}} + + {{ ux_icon('lucide:check', {class: 'size-3.5', 'aria-hidden': 'true'}) }} + + + {%- endif -%} +{%- endblock checkbox_widget -%} + +{%- block radio_widget -%} + {%- set attr = attr|merge({class: (block('shadcn_radio_class') ~ ' ' ~ attr.class|default(''))|tailwind_merge}) -%} + {{- parent() -}} +{%- endblock radio_widget -%} + +{%- block datetime_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {%- set attr = attr|merge({class: ('flex flex-wrap items-center gap-2 *:min-w-0 *:flex-1 ' ~ attr.class|default(''))|trim}) -%} +
+ {{- form_widget(form.date) -}} + {{- form_widget(form.time) -}} +
+ {{- form_errors(form.date) -}} + {{- form_errors(form.time) -}} + {%- endif -%} +{%- endblock datetime_widget -%} + +{%- block date_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {%- set attr = attr|merge({class: ('flex items-center gap-2 *:min-w-0 *:flex-1 ' ~ attr.class|default(''))|trim}) -%} +
+ {{- date_pattern|replace({ + '{{ year }}': form_widget(form.year), + '{{ month }}': form_widget(form.month), + '{{ day }}': form_widget(form.day), + })|raw -}} +
+ {%- endif -%} +{%- endblock date_widget -%} + +{%- block time_widget -%} + {%- if widget == 'single_text' -%} + {{- block('form_widget_simple') -}} + {%- else -%} + {%- set vars = widget == 'text' ? {attr: {size: 1}} : {} -%} + {%- set attr = attr|merge({class: ('flex items-center gap-2 *:min-w-0 *:flex-1 ' ~ attr.class|default(''))|trim}) -%} +
+ {{ form_widget(form.hour, vars) }}{% if with_minutes %}{{ form_widget(form.minute, vars) }}{% endif %}{% if with_seconds %}{{ form_widget(form.second, vars) }}{% endif %} +
+ {%- endif -%} +{%- endblock time_widget -%} + +{%- block money_widget -%} +
+ {{- money_pattern|form_encode_currency(block('form_widget_simple')) -}} +
+{%- endblock money_widget -%} + +{%- block percent_widget -%} + {%- if symbol -%} +
+ {%- set type = type|default('text') -%} + {{- block('form_widget_simple') -}} + {{ symbol|default('%') }} +
+ {%- else -%} + {{- block('form_widget_simple') -}} + {%- endif -%} +{%- endblock percent_widget -%} + +{%- block button_widget -%} + {%- from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class -%} + {%- set attr = attr|merge({class: button_class('outline', 'default', attr.class|default(''))|trim}) -%} + {{- parent() -}} +{%- endblock button_widget -%} + +{%- block submit_widget -%} + {%- from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class -%} + {%- set type = type|default('submit') -%} + {%- set attr = attr|merge({class: button_class('default', 'default', attr.class|default(''))|trim}) -%} + {{- block('form_widget_button_base') -}} +{%- endblock submit_widget -%} + +{%- block form_widget_button_base -%} + {%- if label is empty -%} + {%- if label_format is not empty -%} + {% set label = label_format|replace({'%name%': name, '%id%': id}) %} + {%- elseif label is not same as(false) -%} + {% set label = name|humanize %} + {%- endif -%} + {%- endif -%} + +{%- endblock form_widget_button_base -%} diff --git a/src/ShadcnAdminUi/templates/shared/grid/action/apply_transition.html.twig b/src/ShadcnAdminUi/templates/shared/grid/action/apply_transition.html.twig new file mode 100644 index 00000000..df4a0381 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/action/apply_transition.html.twig @@ -0,0 +1,21 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + +{% if workflow_can(data, action.name) %} + {% if options.link.url is defined %} + {% set path = options.link.url %} + {% else %} + {% set path = path(options.link.route, options.link.parameters|default({id: data.id})) %} + {% endif %} + + {% set message = action.label|default('sylius.ui.apply_transition') %} + {% set variants = {danger: 'destructive', red: 'destructive', primary: 'default', green: 'secondary', success: 'secondary'} %} + +
+ + + +
+{% endif %} diff --git a/src/ShadcnAdminUi/templates/shared/grid/action/create.html.twig b/src/ShadcnAdminUi/templates/shared/grid/action/create.html.twig new file mode 100644 index 00000000..30dbd938 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/action/create.html.twig @@ -0,0 +1,9 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + +{% set path = options.link.url|default(path(options.link.route|default(grid.requestConfiguration.getRouteName('create')), options.link.parameters|default([]))) %} +{% set message = action.label is empty ? 'sylius.ui.create' : action.label %} + + + {{ ux_icon(action.icon|default('lucide:plus'), {'data-icon': 'inline-start', 'aria-hidden': 'true'}) }} + {{ message|trans }} + diff --git a/src/ShadcnAdminUi/templates/shared/grid/action/delete.html.twig b/src/ShadcnAdminUi/templates/shared/grid/action/delete.html.twig new file mode 100644 index 00000000..0a706852 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/action/delete.html.twig @@ -0,0 +1,16 @@ +{% set id = options.link.parameters['id']|default(data.id) %} + +{% if options.link.url is defined %} + {% set path = options.link.url %} +{% else %} + {% set default_route = options.link.route|default(grid.requestConfiguration is defined ? grid.requestConfiguration.getRouteName('delete') : null) %} + {% set path = default_route ? path(default_route, options.link.parameters|default({id: data.id})) : null %} +{% endif %} + +{{ component('sylius_shadcn_admin_ui:delete_modal', { + id: id, + path: path, + label: action.label|default('sylius.ui.delete'), + icon: action.icon|default('lucide:trash-2'), + disabled: path is empty, +}) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/action/show.html.twig b/src/ShadcnAdminUi/templates/shared/grid/action/show.html.twig new file mode 100644 index 00000000..6b41c883 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/action/show.html.twig @@ -0,0 +1,16 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + +{% if options.link.url is defined %} + {% set path = options.link.url %} +{% else %} + {% set default_route = options.link.route|default(grid.requestConfiguration is defined ? grid.requestConfiguration.getRouteName('show') : null) %} + {% set path = default_route ? path(default_route, options.link.parameters|default({id: data.id})) : null %} +{% endif %} + +{% set message = action.label|default('sylius.ui.show') %} +{% set disabled = path is empty %} + + + {{ ux_icon(action.icon|default('lucide:eye'), {'aria-hidden': 'true'}) }} + {{ message|trans }} + diff --git a/src/ShadcnAdminUi/templates/shared/grid/action/update.html.twig b/src/ShadcnAdminUi/templates/shared/grid/action/update.html.twig new file mode 100644 index 00000000..e1ae42f2 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/action/update.html.twig @@ -0,0 +1,16 @@ +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + +{% if options.link.url is defined %} + {% set path = options.link.url %} +{% else %} + {% set default_route = options.link.route|default(grid.requestConfiguration is defined ? grid.requestConfiguration.getRouteName('update') : null) %} + {% set path = default_route ? path(default_route, options.link.parameters|default({id: data.id})) : null %} +{% endif %} + +{% set message = action.label|default('sylius.ui.edit') %} +{% set disabled = path is empty %} + + + {{ ux_icon(action.icon|default('lucide:pencil'), {'aria-hidden': 'true'}) }} + {{ message|trans }} + diff --git a/src/ShadcnAdminUi/templates/shared/grid/bulk_action/delete.html.twig b/src/ShadcnAdminUi/templates/shared/grid/bulk_action/delete.html.twig new file mode 100644 index 00000000..b663a4db --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/bulk_action/delete.html.twig @@ -0,0 +1,24 @@ +{% set path = options.link.url|default(path(options.link.route|default(grid.requestConfiguration.getRouteName('bulk_delete')), options.link.parameters|default({}))) %} + + + + {% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + + + diff --git a/src/ShadcnAdminUi/templates/shared/grid/field/boolean.html.twig b/src/ShadcnAdminUi/templates/shared/grid/field/boolean.html.twig new file mode 100644 index 00000000..ddae603c --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/field/boolean.html.twig @@ -0,0 +1,3 @@ +{% import '@SyliusShadcnAdminUi/shared/helper/field/boolean.html.twig' as boolean %} + +{{ boolean.default(data) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/boolean.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/boolean.html.twig new file mode 100644 index 00000000..472fdc02 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/boolean.html.twig @@ -0,0 +1,3 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +{{ form_row(form, sylius_test_form_attribute('criterion-enabled')|sylius_merge_recursive({label: filter.label})) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/date.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/date.html.twig new file mode 100644 index 00000000..e46daace --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/date.html.twig @@ -0,0 +1,11 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +
+ {{ filter.label|trans }} + {% for bound in [form.from, form.to] %} +
+ {{ bound.vars.label|trans }} +
{{ form_widget(bound) }}
+
+ {% endfor %} +
diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/entity.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/entity.html.twig new file mode 100644 index 00000000..9c59bb88 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/entity.html.twig @@ -0,0 +1,3 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +{{ form_row(form, {label: filter.label}) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/enum.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/enum.html.twig new file mode 100644 index 00000000..9c59bb88 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/enum.html.twig @@ -0,0 +1,3 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +{{ form_row(form, {label: filter.label}) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/exists.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/exists.html.twig new file mode 100644 index 00000000..9c59bb88 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/exists.html.twig @@ -0,0 +1,3 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +{{ form_row(form, {label: filter.label}) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/select.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/select.html.twig new file mode 100644 index 00000000..9c59bb88 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/select.html.twig @@ -0,0 +1,3 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +{{ form_row(form, {label: filter.label}) }} diff --git a/src/ShadcnAdminUi/templates/shared/grid/filter/string.html.twig b/src/ShadcnAdminUi/templates/shared/grid/filter/string.html.twig new file mode 100644 index 00000000..8e8a2d7b --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/grid/filter/string.html.twig @@ -0,0 +1,13 @@ +{% form_theme form '@SyliusShadcnAdminUi/shared/form_theme.html.twig' %} + +
+ +
+ {% if form.type is defined %} +
{{ form_widget(form.type, {attr: {'aria-label': 'sylius.ui.filter'|trans}}) }}
+ {% endif %} +
+ {{ form_widget(form.value, {attr: {placeholder: form.value.vars.label|trans}}) }} +
+
+
diff --git a/src/ShadcnAdminUi/templates/shared/helper/_checkbox.html.twig b/src/ShadcnAdminUi/templates/shared/helper/_checkbox.html.twig new file mode 100644 index 00000000..350d1629 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/_checkbox.html.twig @@ -0,0 +1,15 @@ +{# Markup of the shadcn Checkbox recipe, usable from macros and includes. #} +{% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import html_attributes %} + + + + {{ ux_icon('lucide:check', {class: 'size-3.5', 'aria-hidden': 'true'}) }} + + + diff --git a/src/ShadcnAdminUi/templates/shared/helper/accordion.html.twig b/src/ShadcnAdminUi/templates/shared/helper/accordion.html.twig new file mode 100644 index 00000000..ed1cea2f --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/accordion.html.twig @@ -0,0 +1,18 @@ +{# Same signature as the BootstrapAdminUi macro, based on the shadcn Collapsible controller. #} +{% macro accordion(items, open = false, auto_close = null, flush = false) %} +
+ {% for item in items %} + {% set item = {open: open, icon: null}|merge(item) %} +
+ +
+ {{ item.content|default('Content') }} +
+
+ {% endfor %} +
+{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/avatar.html.twig b/src/ShadcnAdminUi/templates/shared/helper/avatar.html.twig new file mode 100644 index 00000000..741ff087 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/avatar.html.twig @@ -0,0 +1,15 @@ +{% macro default(img, class, initials) %} + + {% if img %} + + {% else %} + + {% if initials %}{{ initials }}{% else %}{{ ux_icon('lucide:user', {class: 'size-4'}) }}{% endif %} + + {% endif %} + +{% endmacro %} + +{% macro small(img, class, initials) %} + {{ _self.default(img, 'size-6 ' ~ class, initials) }} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/breadcrumbs.html.twig b/src/ShadcnAdminUi/templates/shared/helper/breadcrumbs.html.twig new file mode 100644 index 00000000..657a6304 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/breadcrumbs.html.twig @@ -0,0 +1,22 @@ +{# Same signature as the BootstrapAdminUi macro: items = [{name, url?, active?}, ...] #} +{% macro breadcrumbs(items) %} + {% set items = items|filter(item => item is not empty) %} + +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/button.html.twig b/src/ShadcnAdminUi/templates/shared/helper/button.html.twig new file mode 100644 index 00000000..43a1903b --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/button.html.twig @@ -0,0 +1,77 @@ +{# + # Same macro API as @SyliusBootstrapAdminUi/shared/helper/button.html.twig, so your custom + # templates only need the namespace to be changed. + # + # Extra params: variant ('default'|'secondary'|'outline'|'ghost'|'destructive'|'link') + #} +{% macro default(params = {}) %} + {% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class, html_attributes %} + + {% set params = { + attr: null, + class: null, + text: 'Button', + disabled: false, + dropdown: false, + icon: null, + icon_class: null, + icon_only: false, + id: null, + size: null, + type: 'button', + url: null, + form: null, + variant: 'outline', + }|merge(params) %} + + {% set size = params.icon_only + ? (params.size == 'large' ? 'icon-lg' : (params.size == 'small' ? 'icon-sm' : 'icon')) + : (params.size == 'large' ? 'lg' : (params.size == 'small' ? 'sm' : 'default')) %} + {% set class = button_class(params.variant, size, params.class|default('')) %} + {% set icon_attributes = {class: params.icon_class|default(''), 'aria-hidden': 'true'}|merge(params.icon_only ? {} : {'data-icon': 'inline-start'}) %} + {% set icon = params.icon ? ux_icon(params.icon, icon_attributes) %} + {% set label = params.icon_only ? '' ~ params.text|e ~ '' : params.text|e %} + + {% if params.url %} + + {{- icon|raw }}{{ label|raw -}} + + {% else %} + + {% endif %} +{% endmacro %} + +{% macro primary(params = {}) %} + {{ _self.default({variant: 'default'}|merge(params)) }} +{% endmacro %} + +{% macro delete(params = {}) %} + {{ _self.default({text: 'sylius.ui.delete'|trans, variant: 'destructive'}|merge(params)) }} +{% endmacro %} + +{% macro create(params = {}) %} + {{ _self.primary({type: 'submit', text: 'sylius.ui.create'|trans}|merge(params)) }} +{% endmacro %} + +{% macro update(params = {}) %} + {{ _self.primary({type: 'submit', text: 'sylius.ui.update'|trans}|merge(params)) }} +{% endmacro %} + +{% macro cancel(params = {}) %} + {{ _self.default({text: 'sylius.ui.cancel'|trans, url: app.request.headers.get('referer'), variant: 'outline'}|merge(params)) }} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/dropdown.html.twig b/src/ShadcnAdminUi/templates/shared/helper/dropdown.html.twig new file mode 100644 index 00000000..fbb663bf --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/dropdown.html.twig @@ -0,0 +1,85 @@ +{# + # Same signature as the BootstrapAdminUi macros (default / list), rendered with the shadcn + # DropdownMenu controller. params.id makes the generated ids stable (random otherwise). + #} +{% macro default(params) %} + {% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + + {% set params = { + id: 'dropdown-' ~ random(), + button: {}, + class: null, + content: null, + customTrigger: null, + direction: 'down', + }|merge(params) %} + {% set trigger_id = 'dropdown-menu-' ~ params.id ~ '-trigger' %} + {% set content_id = 'dropdown-menu-' ~ params.id ~ '-content' %} + {% set align_class = params.direction == 'down-end' ? 'end-0 ltr:origin-top-right rtl:origin-top-left' : 'start-0 ltr:origin-top-left rtl:origin-top-right' %} + +
+ {% set trigger_attributes %} + id="{{ trigger_id }}" data-slot="dropdown-menu-trigger" data-shadcn--dropdown-menu-target="trigger" aria-haspopup="menu" aria-controls="{{ content_id }}" aria-expanded="false" data-state="closed" data-action="click->shadcn--dropdown-menu#toggle keydown->shadcn--dropdown-menu#onTriggerKeydown" + {% endset %} + {% if params.customTrigger %} +
{{ params.customTrigger }}
+ {% else %} + {% set button = {text: 'Menu', icon: null, class: null}|merge(params.button) %} + + {% endif %} + +
+{% endmacro %} + +{# content is printed raw: escape user data before passing it. #} +{% macro item(attributes = {}, content = '', variant = 'default') %} + {% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import html_attributes %} + {% set tag = attributes.href is defined ? 'a' : 'button' %} + <{{ tag }} + role="menuitem" + tabindex="-1" + data-slot="dropdown-menu-item" + data-variant="{{ variant }}" + data-shadcn--dropdown-menu-target="item" + data-close-on-select="true" + data-action="click->shadcn--dropdown-menu#closeFromItem" + {% if tag == 'button' %}type="button"{% endif %} + class="relative flex w-full cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden transition-colors select-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4 {{ variant == 'destructive' ? 'text-destructive hover:bg-destructive/10 hover:text-destructive focus:bg-destructive/10 focus:text-destructive' : '' }}" + {{- html_attributes(attributes) }} + >{{ content|raw }} +{% endmacro %} + +{% macro list(params, items) %} + {% set content %} + {% for item in items %} + {% set item = {type: null, class: null, icon: null}|merge(item) %} + {% if item.type == 'header' %} +
{{ item.title|trans }}
+ {% elseif item.type == 'divider' %} + + {% else %} + {% set icon = item.icon ? ux_icon(item.icon, {'aria-hidden': 'true'}) : '' %} + {{ _self.item({href: item.url, class: item.class}, icon ~ (item.title|trans|e)) }} + {% endif %} + {% endfor %} + {% endset %} + + {{ _self.default(params|merge({content: content})) }} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/field/boolean.html.twig b/src/ShadcnAdminUi/templates/shared/helper/field/boolean.html.twig new file mode 100644 index 00000000..46100431 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/field/boolean.html.twig @@ -0,0 +1,13 @@ +{% macro default(data) %} + {% if true == data %} + + {{ ux_icon('lucide:circle-check', {class: 'size-4', 'aria-hidden': 'true'}) }} + {{ 'sylius.ui.yes_label'|trans }} + + {% else %} + + {{ ux_icon('lucide:circle-x', {class: 'size-4', 'aria-hidden': 'true'}) }} + {{ 'sylius.ui.no_label'|trans }} + + {% endif %} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/header.html.twig b/src/ShadcnAdminUi/templates/shared/helper/header.html.twig new file mode 100644 index 00000000..991c645e --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/header.html.twig @@ -0,0 +1,21 @@ +{% macro default(level, header, icon, subheader) %} +
+ {% if icon %} +
+ {{ ux_icon(icon, {class: 'size-5', 'aria-hidden': 'true'}) }} +
+ {% endif %} +
+ + {{ header }} + + {% if subheader %} +

{{ subheader }}

+ {% endif %} +
+
+{% endmacro %} + +{% macro h1(header, icon, subheader) %} + {{ _self.default(1, header, icon, subheader) }} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/pagination.html.twig b/src/ShadcnAdminUi/templates/shared/helper/pagination.html.twig new file mode 100644 index 00000000..8720828c --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/pagination.html.twig @@ -0,0 +1,43 @@ +{% macro simple(paginator, options = {}) %} + {{ pagerfanta(paginator, 'twig', {template: '@SyliusShadcnAdminUi/shared/pagerfanta.html.twig'}|merge(options)) }} +{% endmacro %} + +{% macro results_count(paginator) %} + {% set from = paginator.nbResults == 0 ? 0 : 1 + (paginator.currentPage - 1) * paginator.maxPerPage %} + {% set to = paginator.maxPerPage + (paginator.currentPage - 1) * paginator.maxPerPage %} + {% set to = to < paginator.nbResults ? to : paginator.nbResults %} + + {{ 'sylius.ui.pagination.number_of_results'|trans({'%from%': from, '%to%': to, '%total%': paginator.nbResults}) }} +{% endmacro %} + +{% macro number_of_results_selector(paginator, pagination_limits) %} + {% import '@SyliusShadcnAdminUi/shared/helper/dropdown.html.twig' as dropdown %} + {% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + + {% set other_pagination_limits = pagination_limits|filter(limit => limit != paginator.maxPerPage) %} + + {% if other_pagination_limits is not empty %} + {% set content %} + {% for limit in pagination_limits %} + {% set path = path(app.request.attributes.get('_route'), app.request.attributes.all('_route_params')|merge(app.request.query.all)|merge({limit: limit, page: 1})) %} + {% set label %} + {{ limit }} + {% if limit == paginator.maxPerPage %}{{ ux_icon('lucide:check', {'aria-hidden': 'true'}) }}{% endif %} + {% endset %} + {{ dropdown.item({href: path}, label) }} + {% endfor %} + {% endset %} + + {{ dropdown.default({ + id: 'grid-limit', + button: {text: 'sylius.ui.show'|trans ~ ' ' ~ paginator.maxPerPage, class: 'h-7 text-[0.8rem]'}, + content: content, + direction: 'down-end', + class: 'w-28', + }) }} + {% else %} + + {% endif %} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/palette.html.twig b/src/ShadcnAdminUi/templates/shared/helper/palette.html.twig new file mode 100644 index 00000000..9152493b --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/palette.html.twig @@ -0,0 +1,38 @@ +{# + Palette of tones: turns a color name (the historical Tabler palette, as returned by application + enums) into Tailwind classes. Classes are written in full: Tailwind does not detect a class built + by concatenation (never "bg-" ~ color in a template). Shades come from the --tone-* tokens of + assets/styles/theme.css. + + tone(color, kind) - kind: soft (tinted background + text), solid (filled background), text, dot, + hover (filled background on hover/focus of a "group/tile" parent), + alert (border + tinted background of a shadcn:Alert). + Unknown or empty color: neutral rendering (secondary). +#} +{% macro tone(color, kind = 'soft') -%} + {%- set palette = { + blue: {soft: 'bg-tone-blue/10 text-tone-blue dark:bg-tone-blue/15', solid: 'bg-tone-blue text-background', text: 'text-tone-blue', dot: 'bg-tone-blue', hover: 'group-hover/tile:bg-tone-blue group-hover/tile:text-background group-focus-visible/tile:bg-tone-blue group-focus-visible/tile:text-background', alert: 'border-tone-blue/30 bg-tone-blue/10 text-tone-blue dark:bg-tone-blue/15'}, + azure: {soft: 'bg-tone-azure/10 text-tone-azure dark:bg-tone-azure/15', solid: 'bg-tone-azure text-background', text: 'text-tone-azure', dot: 'bg-tone-azure', hover: 'group-hover/tile:bg-tone-azure group-hover/tile:text-background group-focus-visible/tile:bg-tone-azure group-focus-visible/tile:text-background', alert: 'border-tone-azure/30 bg-tone-azure/10 text-tone-azure dark:bg-tone-azure/15'}, + indigo: {soft: 'bg-tone-indigo/10 text-tone-indigo dark:bg-tone-indigo/15', solid: 'bg-tone-indigo text-background', text: 'text-tone-indigo', dot: 'bg-tone-indigo', hover: 'group-hover/tile:bg-tone-indigo group-hover/tile:text-background group-focus-visible/tile:bg-tone-indigo group-focus-visible/tile:text-background', alert: 'border-tone-indigo/30 bg-tone-indigo/10 text-tone-indigo dark:bg-tone-indigo/15'}, + purple: {soft: 'bg-tone-purple/10 text-tone-purple dark:bg-tone-purple/15', solid: 'bg-tone-purple text-background', text: 'text-tone-purple', dot: 'bg-tone-purple', hover: 'group-hover/tile:bg-tone-purple group-hover/tile:text-background group-focus-visible/tile:bg-tone-purple group-focus-visible/tile:text-background', alert: 'border-tone-purple/30 bg-tone-purple/10 text-tone-purple dark:bg-tone-purple/15'}, + pink: {soft: 'bg-tone-pink/10 text-tone-pink dark:bg-tone-pink/15', solid: 'bg-tone-pink text-background', text: 'text-tone-pink', dot: 'bg-tone-pink', hover: 'group-hover/tile:bg-tone-pink group-hover/tile:text-background group-focus-visible/tile:bg-tone-pink group-focus-visible/tile:text-background', alert: 'border-tone-pink/30 bg-tone-pink/10 text-tone-pink dark:bg-tone-pink/15'}, + red: {soft: 'bg-tone-red/10 text-tone-red dark:bg-tone-red/15', solid: 'bg-tone-red text-background', text: 'text-tone-red', dot: 'bg-tone-red', hover: 'group-hover/tile:bg-tone-red group-hover/tile:text-background group-focus-visible/tile:bg-tone-red group-focus-visible/tile:text-background', alert: 'border-tone-red/30 bg-tone-red/10 text-tone-red dark:bg-tone-red/15'}, + orange: {soft: 'bg-tone-orange/10 text-tone-orange dark:bg-tone-orange/15', solid: 'bg-tone-orange text-background', text: 'text-tone-orange', dot: 'bg-tone-orange', hover: 'group-hover/tile:bg-tone-orange group-hover/tile:text-background group-focus-visible/tile:bg-tone-orange group-focus-visible/tile:text-background', alert: 'border-tone-orange/30 bg-tone-orange/10 text-tone-orange dark:bg-tone-orange/15'}, + yellow: {soft: 'bg-tone-yellow/10 text-tone-yellow dark:bg-tone-yellow/15', solid: 'bg-tone-yellow text-background', text: 'text-tone-yellow', dot: 'bg-tone-yellow', hover: 'group-hover/tile:bg-tone-yellow group-hover/tile:text-background group-focus-visible/tile:bg-tone-yellow group-focus-visible/tile:text-background', alert: 'border-tone-yellow/30 bg-tone-yellow/10 text-tone-yellow dark:bg-tone-yellow/15'}, + lime: {soft: 'bg-tone-lime/10 text-tone-lime dark:bg-tone-lime/15', solid: 'bg-tone-lime text-background', text: 'text-tone-lime', dot: 'bg-tone-lime', hover: 'group-hover/tile:bg-tone-lime group-hover/tile:text-background group-focus-visible/tile:bg-tone-lime group-focus-visible/tile:text-background', alert: 'border-tone-lime/30 bg-tone-lime/10 text-tone-lime dark:bg-tone-lime/15'}, + green: {soft: 'bg-tone-green/10 text-tone-green dark:bg-tone-green/15', solid: 'bg-tone-green text-background', text: 'text-tone-green', dot: 'bg-tone-green', hover: 'group-hover/tile:bg-tone-green group-hover/tile:text-background group-focus-visible/tile:bg-tone-green group-focus-visible/tile:text-background', alert: 'border-tone-green/30 bg-tone-green/10 text-tone-green dark:bg-tone-green/15'}, + teal: {soft: 'bg-tone-teal/10 text-tone-teal dark:bg-tone-teal/15', solid: 'bg-tone-teal text-background', text: 'text-tone-teal', dot: 'bg-tone-teal', hover: 'group-hover/tile:bg-tone-teal group-hover/tile:text-background group-focus-visible/tile:bg-tone-teal group-focus-visible/tile:text-background', alert: 'border-tone-teal/30 bg-tone-teal/10 text-tone-teal dark:bg-tone-teal/15'}, + cyan: {soft: 'bg-tone-cyan/10 text-tone-cyan dark:bg-tone-cyan/15', solid: 'bg-tone-cyan text-background', text: 'text-tone-cyan', dot: 'bg-tone-cyan', hover: 'group-hover/tile:bg-tone-cyan group-hover/tile:text-background group-focus-visible/tile:bg-tone-cyan group-focus-visible/tile:text-background', alert: 'border-tone-cyan/30 bg-tone-cyan/10 text-tone-cyan dark:bg-tone-cyan/15'}, + success: {soft: 'bg-tone-green/10 text-tone-green dark:bg-tone-green/15', solid: 'bg-tone-green text-background', text: 'text-tone-green', dot: 'bg-tone-green', hover: 'group-hover/tile:bg-tone-green group-hover/tile:text-background group-focus-visible/tile:bg-tone-green group-focus-visible/tile:text-background', alert: 'border-tone-green/30 bg-tone-green/10 text-tone-green dark:bg-tone-green/15'}, + danger: {soft: 'bg-tone-red/10 text-tone-red dark:bg-tone-red/15', solid: 'bg-tone-red text-background', text: 'text-tone-red', dot: 'bg-tone-red', hover: 'group-hover/tile:bg-tone-red group-hover/tile:text-background group-focus-visible/tile:bg-tone-red group-focus-visible/tile:text-background', alert: 'border-tone-red/30 bg-tone-red/10 text-tone-red dark:bg-tone-red/15'}, + warning: {soft: 'bg-tone-yellow/10 text-tone-yellow dark:bg-tone-yellow/15', solid: 'bg-tone-yellow text-background', text: 'text-tone-yellow', dot: 'bg-tone-yellow', hover: 'group-hover/tile:bg-tone-yellow group-hover/tile:text-background group-focus-visible/tile:bg-tone-yellow group-focus-visible/tile:text-background', alert: 'border-tone-yellow/30 bg-tone-yellow/10 text-tone-yellow dark:bg-tone-yellow/15'}, + brown: {soft: 'bg-tone-orange/10 text-tone-orange dark:bg-tone-orange/15', solid: 'bg-tone-orange text-background', text: 'text-tone-orange', dot: 'bg-tone-orange', hover: 'group-hover/tile:bg-tone-orange group-hover/tile:text-background group-focus-visible/tile:bg-tone-orange group-focus-visible/tile:text-background', alert: 'border-tone-orange/30 bg-tone-orange/10 text-tone-orange dark:bg-tone-orange/15'}, + info: {soft: 'bg-tone-azure/10 text-tone-azure dark:bg-tone-azure/15', solid: 'bg-tone-azure text-background', text: 'text-tone-azure', dot: 'bg-tone-azure', hover: 'group-hover/tile:bg-tone-azure group-hover/tile:text-background group-focus-visible/tile:bg-tone-azure group-focus-visible/tile:text-background', alert: 'border-tone-azure/30 bg-tone-azure/10 text-tone-azure dark:bg-tone-azure/15'}, + secondary: {soft: 'bg-muted text-muted-foreground', solid: 'bg-muted-foreground text-background', text: 'text-muted-foreground', dot: 'bg-muted-foreground', hover: 'group-hover/tile:bg-muted-foreground group-hover/tile:text-background group-focus-visible/tile:bg-muted-foreground group-focus-visible/tile:text-background', alert: 'bg-muted text-foreground'}, + dark: {soft: 'bg-foreground/10 text-foreground', solid: 'bg-foreground text-background', text: 'text-foreground', dot: 'bg-foreground', hover: 'group-hover/tile:bg-foreground group-hover/tile:text-background group-focus-visible/tile:bg-foreground group-focus-visible/tile:text-background', alert: 'border-foreground/30 bg-foreground/5 text-foreground'}, + primary: {soft: 'bg-primary/10 text-primary', solid: 'bg-primary text-primary-foreground', text: 'text-primary', dot: 'bg-primary', hover: 'group-hover/tile:bg-primary group-hover/tile:text-primary-foreground group-focus-visible/tile:bg-primary group-focus-visible/tile:text-primary-foreground', alert: 'border-primary/30 bg-primary/5 text-primary'}, + } -%} + {%- set aliases = {muted: 'secondary', gray: 'secondary', grey: 'secondary', body: 'dark', black: 'dark'} -%} + {%- set color = aliases[color] ?? color -%} + {{- (palette[color] ?? palette.secondary)[kind] -}} +{%- endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/sorting.html.twig b/src/ShadcnAdminUi/templates/shared/helper/sorting.html.twig new file mode 100644 index 00000000..ef8bafe9 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/sorting.html.twig @@ -0,0 +1,29 @@ +{% macro table_header(grid, field, attributes) %} + {% set sorting_order = grid.getSortingOrder(field.name)|default('asc') %} + {% set is_sorted = grid.isSortedBy(field.name) %} + {% set next_order = is_sorted ? (sorting_order == 'desc' ? 'asc' : 'desc') : sorting_order %} + + + + {{ field.label|trans }} + {% if is_sorted %} + {{ ux_icon(sorting_order == 'desc' ? 'lucide:arrow-down' : 'lucide:arrow-up', {class: 'size-3.5', 'aria-hidden': 'true'}) }} + {% else %} + {{ ux_icon('lucide:chevrons-up-down', {class: 'size-3.5 opacity-60', 'aria-hidden': 'true'}) }} + {% endif %} + + +{% endmacro %} + +{% macro link(field_name, attributes, sorting_order, parameters) %} + {%- set params = attributes.all('_route_params')|merge(parameters) -%} + {%- set sorting = {sorting: {(field_name): (sorting_order)}} -%} + {{- path(attributes.get('_route'), params|merge(sorting)|merge({page: 1})) -}} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/table.html.twig b/src/ShadcnAdminUi/templates/shared/helper/table.html.twig new file mode 100644 index 00000000..ca1a2ec8 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/table.html.twig @@ -0,0 +1,108 @@ +{# Same macro API as BootstrapAdminUi (headers / row), with the shadcn Table classes. #} +{% macro headers(grid, definition, request_attributes) %} + {% import '@SyliusShadcnAdminUi/shared/helper/sorting.html.twig' as sorting %} + + {% if definition.actionGroups.bulk is defined and definition.getEnabledActions('bulk')|length > 0 %} + + {{ include('@SyliusShadcnAdminUi/shared/helper/_checkbox.html.twig', { + attributes: { + 'aria-label': 'sylius.ui.all'|trans, + 'data-admin--grid-selection-target': 'all', + 'data-action': 'admin--grid-selection#toggleAll', + }, + }, false) }} + + {% endif %} + + {% for field in definition.fields|sylius_sort_by('position') %} + {% if field.enabled %} + {% if field.isSortable %} + {{ sorting.table_header(grid, field, request_attributes) }} + {% else %} + {{ field.label|trans }} + {% endif %} + {% endif %} + {% endfor %} + + {% if definition.actionGroups.item is defined and definition.getEnabledActions('item')|length > 0 %} + + {{ 'sylius.ui.actions'|trans }} + + {% endif %} +{% endmacro %} + +{% macro row(grid, definition, row) %} + {% import '@SyliusShadcnAdminUi/shared/helper/dropdown.html.twig' as dropdown %} + {% from '@SyliusShadcnAdminUi/shared/helper/ui.html.twig' import button_class %} + + {# + Below md the row becomes a card (same markup, CSS only, so there is no duplicated markup or ids): + a 3-column grid (bulk checkbox | title | row actions) with the actions top-right and each cell as a "label … value" line. + A field can opt into a richer card with options.vars.mobile: + title (card heading, top-left), badge (short line under the title, e.g. a status), + meta (muted line without label), hidden (not shown on mobile). + #} + + {% if definition.actionGroups.bulk is defined and definition.getEnabledActions('bulk')|length > 0 and row.id is defined %} + + {{ include('@SyliusShadcnAdminUi/shared/helper/_checkbox.html.twig', { + attributes: { + value: row.id, + 'aria-label': row.id, + 'data-admin--grid-selection-target': 'item', + 'data-action': 'admin--grid-selection#refresh', + }, + }, false) }} + + {% endif %} + + {% for field in definition.enabledFields|sylius_sort_by('position') %} + {# vars.wrap: let a long-text column wrap instead of widening the table #} + {% set mobile_class = { + title: 'max-md:col-span-1 max-md:col-start-2 max-md:row-start-1 max-md:block max-md:text-start max-md:text-base max-md:font-medium max-md:before:content-none', + badge: 'max-md:order-1 max-md:col-span-2 max-md:col-start-2 max-md:block max-md:justify-self-start max-md:text-start max-md:before:content-none', + meta: 'max-md:order-2 max-md:col-span-2 max-md:col-start-2 max-md:block max-md:text-start max-md:text-muted-foreground max-md:before:content-none', + hidden: 'max-md:hidden', + }[field.options.vars.mobile|default('')] ?? 'max-md:order-3' %} + + {% if field.name == 'code' %} + {{ sylius_grid_render_field(grid, field, row) }} + {% else %} + {{ sylius_grid_render_field(grid, field, row) }} + {% endif %} + + {% endfor %} + + {% if definition.actionGroups.item is defined and definition.getEnabledActions('item')|length > 0 %} + +
+ {% for action in definition.getEnabledActions('item')|sylius_sort_by('position') %} + {{ sylius_grid_render_action(grid, action, row) }} + {% endfor %} + + {% if definition.actionGroups.subitem is defined and definition.getEnabledActions('subitem')|length > 0 %} + {% set subactions %} + {% for action in definition.getEnabledActions('subitem')|sylius_sort_by('position') %} + {{ sylius_grid_render_action(grid, action, row) }} + {% endfor %} + {% endset %} + {% if subactions|trim|length > 0 %} + {% set more_button %} + + {{ ux_icon('lucide:ellipsis', {'aria-hidden': 'true'}) }} + {{ 'sylius_shadcn_admin_ui.ui.more'|trans }} + + {% endset %} + {# Action templates render icon-only buttons (label in .sr-only): in the menu, stretch each trigger (the element carrying title) into a labelled item. #} + {% set menu_class = 'flex flex-col *:w-full [&_[title]]:h-auto [&_[title]]:w-full [&_[title]]:justify-start [&_[title]]:gap-2 [&_[title]]:px-1.5 [&_[title]]:py-1 [&_[title]]:font-normal [&_[title].text-muted-foreground]:text-popover-foreground [&_[title]_.sr-only]:not-sr-only [&_[title]_.sr-only]:whitespace-nowrap' %} + {{ dropdown.default({id: 'row-' ~ row.id|default(random()), customTrigger: more_button, content: subactions, direction: 'down-end', class: menu_class}) }} + {% else %} + {# Keep the menu slot so the other action icons stay aligned from row to row #} + + {% endif %} + {% endif %} +
+ + {% endif %} + +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/helper/ui.html.twig b/src/ShadcnAdminUi/templates/shared/helper/ui.html.twig new file mode 100644 index 00000000..6ce92730 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/helper/ui.html.twig @@ -0,0 +1,42 @@ +{# + # Class helpers for places where a Twig component cannot be used (KnpMenu / Pagerfanta block + # recursion, legacy macros...). They mirror the shadcn recipes: keep them in sync when you + # re-install a recipe with "ux:install". + #} +{% macro button_class(variant = 'default', size = 'default', extra = '') %} + {%- set style = html_cva( + base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/80', + outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', + secondary: 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', + destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2', + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2', + icon: 'size-8', + 'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)]', + 'icon-lg': 'size-9', + }, + }, + ) -%} + {{- (style.apply({variant: variant, size: size}) ~ ' ' ~ extra)|tailwind_merge -}} +{% endmacro %} + +{% macro html_attributes(attr) %} + {%- if attr is iterable -%} + {%- for name, value in attr -%} + {%- if value is same as(true) %} {{ name }}{% elseif value is not same as(false) and value is not null %} {{ name }}="{{ value }}"{% endif -%} + {%- endfor -%} + {%- elseif attr -%} + {#- Legacy string attributes ("data-foo=bar"), written by template authors. -#} + {{ (' ' ~ attr)|raw }} + {%- endif -%} +{% endmacro %} diff --git a/src/ShadcnAdminUi/templates/shared/layout/javascripts.html.twig b/src/ShadcnAdminUi/templates/shared/layout/javascripts.html.twig new file mode 100644 index 00000000..f625fe39 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/layout/javascripts.html.twig @@ -0,0 +1,2 @@ +{# The importmap entrypoint that starts Stimulus (see README > Installation). #} +{{ importmap(hookable_metadata.configuration.entrypoint|default('admin')) }} diff --git a/src/ShadcnAdminUi/templates/shared/layout/metatags.html.twig b/src/ShadcnAdminUi/templates/shared/layout/metatags.html.twig new file mode 100644 index 00000000..e517c0b1 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/layout/metatags.html.twig @@ -0,0 +1 @@ + diff --git a/src/ShadcnAdminUi/templates/shared/layout/stylesheets.html.twig b/src/ShadcnAdminUi/templates/shared/layout/stylesheets.html.twig new file mode 100644 index 00000000..c4436e51 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/layout/stylesheets.html.twig @@ -0,0 +1,2 @@ +{# The Tailwind entry built by symfonycasts/tailwind-bundle (see README > Installation). #} + diff --git a/src/ShadcnAdminUi/templates/shared/layout/theme.html.twig b/src/ShadcnAdminUi/templates/shared/layout/theme.html.twig new file mode 100644 index 00000000..7d4292b8 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/layout/theme.html.twig @@ -0,0 +1,10 @@ +{# Applied before the stylesheet: no flash of the wrong theme. Same storage key as BootstrapAdminUi. #} + diff --git a/src/ShadcnAdminUi/templates/shared/layout/themes.html.twig b/src/ShadcnAdminUi/templates/shared/layout/themes.html.twig new file mode 100644 index 00000000..b92f6f98 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/layout/themes.html.twig @@ -0,0 +1,4 @@ +{# Color themes enabled with "sylius_shadcn_admin_ui.themes": loaded before the Tailwind entry, so its own overrides still win. #} +{% for theme in hookable_metadata.configuration.themes|default([]) %} + +{% endfor %} diff --git a/src/ShadcnAdminUi/templates/shared/layout/title.html.twig b/src/ShadcnAdminUi/templates/shared/layout/title.html.twig new file mode 100644 index 00000000..55d34523 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/layout/title.html.twig @@ -0,0 +1 @@ +{{ hookable_metadata.configuration.title|default('Sylius') }} diff --git a/src/ShadcnAdminUi/templates/shared/pagerfanta.html.twig b/src/ShadcnAdminUi/templates/shared/pagerfanta.html.twig new file mode 100644 index 00000000..baa6dac8 --- /dev/null +++ b/src/ShadcnAdminUi/templates/shared/pagerfanta.html.twig @@ -0,0 +1,56 @@ +{#- Pagerfanta view with the shadcn Pagination look. Plain HTML: Pagerfanta renders it block by block. -#} +{%- extends '@BabDevPagerfanta/default.html.twig' -%} + +{%- block pager_widget -%} + +{%- endblock pager_widget -%} + +{%- block page_link -%} +
  • {{- page -}}
  • +{%- endblock page_link -%} + +{%- block current_page_link -%} +
  • {{- page -}}
  • +{%- endblock current_page_link -%} + +{%- block previous_page_link -%} +
  • + +
  • +{%- endblock previous_page_link -%} + +{%- block previous_page_link_disabled -%} +
  • + + {{ ux_icon('lucide:chevron-left', {class: 'size-4 rtl:rotate-180', 'aria-hidden': 'true'}) }} + +
  • +{%- endblock previous_page_link_disabled -%} + +{%- block next_page_link -%} +
  • + +
  • +{%- endblock next_page_link -%} + +{%- block next_page_link_disabled -%} +
  • + + {{ ux_icon('lucide:chevron-right', {class: 'size-4 rtl:rotate-180', 'aria-hidden': 'true'}) }} + +
  • +{%- endblock next_page_link_disabled -%} + +{%- block ellipsis -%} + +{%- endblock ellipsis -%} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/.env b/src/ShadcnAdminUi/tests/Functional/.application/.env new file mode 100644 index 00000000..d84e5bf0 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/.env @@ -0,0 +1,24 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=c9570eaa5cbfadf33ad1a93f95c2e42a +###< symfony/framework-bundle ### + +###> doctrine/doctrine-bundle ### +DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" +###< doctrine/doctrine-bundle ### diff --git a/src/ShadcnAdminUi/tests/Functional/.application/.gitignore b/src/ShadcnAdminUi/tests/Functional/.application/.gitignore new file mode 100644 index 00000000..79b02512 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/.gitignore @@ -0,0 +1,17 @@ + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +composer.lock + +###> symfony/asset-mapper ### +/assets/vendor/ +/public/assets/ +###< symfony/asset-mapper ### diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/admin.js b/src/ShadcnAdminUi/tests/Functional/.application/assets/admin.js new file mode 100644 index 00000000..fea4166e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/admin.js @@ -0,0 +1,4 @@ +// Admin entrypoint: starts Stimulus with the controllers of the demo and of the bundle. +import { startStimulusApp } from '@symfony/stimulus-bundle'; + +startStimulusApp(); diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/controllers.json b/src/ShadcnAdminUi/tests/Functional/.application/assets/controllers.json new file mode 100644 index 00000000..a1c6e90c --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/controllers.json @@ -0,0 +1,4 @@ +{ + "controllers": [], + "entrypoints": [] +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/controllers/csrf_protection_controller.js b/src/ShadcnAdminUi/tests/Functional/.application/assets/controllers/csrf_protection_controller.js new file mode 100644 index 00000000..b0849fde --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/controllers/csrf_protection_controller.js @@ -0,0 +1,85 @@ +const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/; +const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/; + +// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager +// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event +// and thus this event-listener will not be executed. +document.addEventListener('submit', function (event) { + generateCsrfToken(event.target); +}, true); + +// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie +// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked +document.addEventListener('turbo:submit-start', function (event) { + const h = generateCsrfHeaders(event.detail.formSubmission.formElement); + Object.keys(h).map(function (k) { + event.detail.formSubmission.fetchRequest.headers[k] = h[k]; + }); +}); + +// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted +document.addEventListener('turbo:submit-end', function (event) { + removeCsrfToken(event.detail.formSubmission.formElement); +}); + +export function generateCsrfToken (formElement) { + const csrfField = getCsrfField(formElement); + + if (!csrfField) { + return; + } + + let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); + let csrfToken = csrfField.value; + + if (!csrfCookie && nameCheck.test(csrfToken)) { + csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken); + csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18)))); + } + csrfField.dispatchEvent(new Event('change', { bubbles: true })); + + if (csrfCookie && tokenCheck.test(csrfToken)) { + const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict'; + document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie; + } +} + +export function generateCsrfHeaders (formElement) { + const headers = {}; + const csrfField = getCsrfField(formElement); + + if (!csrfField) { + return headers; + } + + const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); + + if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { + headers[csrfCookie] = csrfField.value; + } + + return headers; +} + +export function removeCsrfToken (formElement) { + const csrfField = getCsrfField(formElement); + + if (!csrfField) { + return; + } + + const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value'); + + if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) { + const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0'; + + document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie; + } +} + +function getCsrfField (formElement) { + return Array.from(formElement.elements).find((field) => field.matches('input[data-controller="csrf-protection"], input[name="_csrf_token"]')); +} + +/* stimulusFetch: 'lazy' */ +export default 'csrf-protection-controller'; diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-down.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-down.svg new file mode 100644 index 00000000..39ad54dd --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-right-left.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-right-left.svg new file mode 100644 index 00000000..9ae6a934 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-right-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-right.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-right.svg new file mode 100644 index 00000000..059d3940 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-up.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-up.svg new file mode 100644 index 00000000..86bcb102 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/book-copy.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/book-copy.svg new file mode 100644 index 00000000..ffc615b7 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/book-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/book-open.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/book-open.svg new file mode 100644 index 00000000..9edf0b71 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/book-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/check.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/check.svg new file mode 100644 index 00000000..008943df --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-down.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-down.svg new file mode 100644 index 00000000..3182a2a6 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-left.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-left.svg new file mode 100644 index 00000000..93f1e3c1 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-right.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-right.svg new file mode 100644 index 00000000..c2d3c5fd --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevrons-up-down.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevrons-up-down.svg new file mode 100644 index 00000000..de1adf56 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/chevrons-up-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-alert.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-alert.svg new file mode 100644 index 00000000..67c653e0 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-check.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-check.svg new file mode 100644 index 00000000..2c2d48aa --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-x.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-x.svg new file mode 100644 index 00000000..befa4f95 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/circle-x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/copy.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/copy.svg new file mode 100644 index 00000000..a9576857 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/ellipsis.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/ellipsis.svg new file mode 100644 index 00000000..17ef08c0 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/ellipsis.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/external-link.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/external-link.svg new file mode 100644 index 00000000..fe374b16 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/eye.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/eye.svg new file mode 100644 index 00000000..f1c02a33 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/filter.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/filter.svg new file mode 100644 index 00000000..df172fa6 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/inbox.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/inbox.svg new file mode 100644 index 00000000..c0dcec51 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/inbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/info.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/info.svg new file mode 100644 index 00000000..db8abdfc --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/info.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/layout-dashboard.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/layout-dashboard.svg new file mode 100644 index 00000000..e22ea5ef --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/layout-dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/layout-grid.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/layout-grid.svg new file mode 100644 index 00000000..e1dae6cb --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/layout-grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/library.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/library.svg new file mode 100644 index 00000000..6c54487f --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/library.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/loader-2.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/loader-2.svg new file mode 100644 index 00000000..366b4a8a --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/loader-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/log-out.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/log-out.svg new file mode 100644 index 00000000..3fa1f9f7 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/log-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/menu.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/menu.svg new file mode 100644 index 00000000..a3ad7892 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/minus.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/minus.svg new file mode 100644 index 00000000..b41cbe69 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/moon.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/moon.svg new file mode 100644 index 00000000..fcc2a3aa --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/moon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/more-horizontal.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/more-horizontal.svg new file mode 100644 index 00000000..17ef08c0 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/more-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/panel-left.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/panel-left.svg new file mode 100644 index 00000000..d5679692 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/panel-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/pencil.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/pencil.svg new file mode 100644 index 00000000..48ee08b3 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/pencil.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/plus.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/plus.svg new file mode 100644 index 00000000..9b2ee5b2 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/rotate-ccw.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/rotate-ccw.svg new file mode 100644 index 00000000..d02d9d82 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/rotate-ccw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/save.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/save.svg new file mode 100644 index 00000000..504deb25 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/search.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/search.svg new file mode 100644 index 00000000..12725713 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/server.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/server.svg new file mode 100644 index 00000000..56bb1a93 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/sliders-horizontal.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/sliders-horizontal.svg new file mode 100644 index 00000000..68455252 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/sliders-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/sun.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/sun.svg new file mode 100644 index 00000000..3ca22946 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/tags.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/tags.svg new file mode 100644 index 00000000..9147b401 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/tags.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/trash-2.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/trash-2.svg new file mode 100644 index 00000000..bf3d9589 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/trash-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/triangle-alert.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/triangle-alert.svg new file mode 100644 index 00000000..ae83c2ed --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/triangle-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/user.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/user.svg new file mode 100644 index 00000000..b29514ca --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/warehouse.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/warehouse.svg new file mode 100644 index 00000000..39b84966 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/warehouse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/x.svg b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/x.svg new file mode 100644 index 00000000..7e18c3f6 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/icons/lucide/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/assets/styles/admin.css b/src/ShadcnAdminUi/tests/Functional/.application/assets/styles/admin.css new file mode 100644 index 00000000..a127d0db --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/assets/styles/admin.css @@ -0,0 +1,10 @@ +@import "tailwindcss"; +/* Theme of the bundle, from its sources (an application imports it from vendor/sylius/shadcn-admin-ui). */ +@import "../../../../../assets/styles/theme.css"; + +/* Templates of the demo (the theme already scans the bundle templates). */ +@source "../../templates"; + +/* Your own tokens / overrides go here, e.g.: +:root { --primary: oklch(0.55 0.2 260); --radius: 0.5rem; } +*/ diff --git a/src/ShadcnAdminUi/tests/Functional/.application/bin/console b/src/ShadcnAdminUi/tests/Functional/.application/bin/console new file mode 100644 index 00000000..fd85706d --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/bin/console @@ -0,0 +1,21 @@ +#!/usr/bin/env php + dirname(__DIR__), +]; + +if (file_exists(dirname(__DIR__, 4).'/vendor/autoload_runtime.php')) { + require_once dirname(__DIR__, 4).'/vendor/autoload_runtime.php'; +} else { + require_once dirname(__DIR__, 6).'/vendor/autoload_runtime.php'; +} + +return function (array $context) { + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); + + return new Application($kernel); +}; diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/bootstrap.php b/src/ShadcnAdminUi/tests/Functional/.application/config/bootstrap.php new file mode 100644 index 00000000..d9ed1c77 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/bootstrap.php @@ -0,0 +1,36 @@ +=1.2) +if (is_array($env = @include dirname(__DIR__) . '/.env.local.php')) { + $_SERVER += $env; + $_ENV += $env; +} elseif (!class_exists(Dotenv::class)) { + throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); +} else { + // load all the .env files + (new Dotenv())->loadEnv(dirname(__DIR__) . '/.env'); +} + +$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; +$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; +$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], \FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/bundles.php b/src/ShadcnAdminUi/tests/Functional/.application/config/bundles.php new file mode 100644 index 00000000..66b66e48 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/bundles.php @@ -0,0 +1,35 @@ + ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], + Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], + Sylius\TwigHooks\SyliusTwigHooksBundle::class => ['all' => true], + BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true], + Sylius\Bundle\ResourceBundle\SyliusResourceBundle::class => ['all' => true], + Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true], + Sylius\AdminUi\Symfony\SyliusAdminUiBundle::class => ['all' => true], + Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], + Sylius\TwigExtra\Symfony\SyliusTwigExtraBundle::class => ['all' => true], + Sylius\UiTranslations\Symfony\SyliusUiTranslationsBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Symfony\UX\Icons\UXIconsBundle::class => ['all' => true], + Symfonycasts\TailwindBundle\SymfonycastsTailwindBundle::class => ['all' => true], + TalesFromADev\Twig\Extra\Tailwind\Bridge\Symfony\Bundle\TalesFromADevTwigExtraTailwindBundle::class => ['all' => true], + Sylius\ShadcnAdminUi\Symfony\SyliusShadcnAdminUiBundle::class => ['all' => true], + Symfony\UX\Toolkit\UXToolkitBundle::class => ['all' => true], +]; diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/asset_mapper.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/asset_mapper.yaml new file mode 100644 index 00000000..ca42f756 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/asset_mapper.yaml @@ -0,0 +1,6 @@ +framework: + asset_mapper: + # The paths to make available to the asset mapper. + paths: + - assets/ + missing_import_mode: strict diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/csrf.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/csrf.yaml new file mode 100644 index 00000000..c8cbdbd0 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/csrf.yaml @@ -0,0 +1,7 @@ +framework: + form: + csrf_protection: + enabled: true + + csrf_protection: + enabled: true diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/doctrine.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/doctrine.yaml new file mode 100644 index 00000000..ae412739 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/doctrine.yaml @@ -0,0 +1,15 @@ +doctrine: + dbal: + # SQLite: no database server, one file per environment. + url: 'sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db' + profiling_collect_backtrace: '%kernel.debug%' + orm: + validate_xml_mapping: true + naming_strategy: doctrine.orm.naming_strategy.underscore + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Entity' + prefix: 'TestApplication\Sylius\ShadcnAdminUi\Entity' diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/framework.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/framework.yaml new file mode 100644 index 00000000..5f873407 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/framework.yaml @@ -0,0 +1,11 @@ +framework: + secret: 'Three can keep a secret, if two of them are dead.' + + # Note that the session will be started ONLY if you read or write from it. + session: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/routing.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/routing.yaml new file mode 100644 index 00000000..4142bbe6 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/routing.yaml @@ -0,0 +1,4 @@ +framework: + router: + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + default_uri: 'http://localhost:8080' diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/security.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/security.yaml new file mode 100644 index 00000000..bbe01509 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/security.yaml @@ -0,0 +1,29 @@ +security: + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'plaintext' + providers: + users_in_memory: + memory: + users: + admin: { password: 'admin', roles: ['ROLE_ADMIN'] } + firewalls: + dev: + pattern: ^/(_profiler(/|$)|(_wdt|assets|build)/) + security: false + main: + lazy: true + provider: users_in_memory + form_login: + login_path: sylius_admin_ui_login + check_path: sylius_admin_ui_login_check + default_target_path: sylius_admin_ui_dashboard + enable_csrf: true + csrf_parameter: _csrf_admin_security_token + csrf_token_id: admin_authenticate + logout: + path: sylius_admin_ui_logout + target: sylius_admin_ui_login + access_control: + - { path: ^/admin/login, roles: PUBLIC_ACCESS } + - { path: ^/admin/logout, roles: PUBLIC_ACCESS } + - { path: ^/admin, roles: ROLE_ADMIN } diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_resource.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_resource.yaml new file mode 100644 index 00000000..f38fbf89 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_resource.yaml @@ -0,0 +1,15 @@ +# @see https://stack.sylius.com/resource/index +sylius_resource: + # Override default settings + #settings: + + # Configure the mapping for your resources + mapping: + paths: + - '%kernel.project_dir%/src/Entity' + + # Configure your resources + resources: + #app.book: + #classes: + #model: TestApplication\Sylius\ShadcnAdminUi\Entity\Book diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_shadcn_admin_ui.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_shadcn_admin_ui.yaml new file mode 100644 index 00000000..92e30d9c --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_shadcn_admin_ui.yaml @@ -0,0 +1,9 @@ +# Configuration of the theme (hooks, grid templates, component prefixes). +# Loaded before sylius_twig_hooks.yaml (alphabetical order): the hooks of the application win. +imports: + - { resource: '@SyliusShadcnAdminUiBundle/config/app.php' } + +sylius_shadcn_admin_ui: + sidebar: + variant: floating + themes: [zinc, blue] diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_twig_hooks.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_twig_hooks.yaml new file mode 100644 index 00000000..a003729e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/sylius_twig_hooks.yaml @@ -0,0 +1,51 @@ +# Customization of the theme by the application. +# Hook names are the Sylius Stack ones: php tests/Application/bin/console sylius:debug:twig-hooks +sylius_twig_hooks: + hooks: + # Title of the browser tab + 'sylius_admin.base#base_title': + default: + configuration: + title: 'Bibliothèque' + + # Sidebar logo + 'sylius_admin.common.component.sidebar.logo': + image: + configuration: + name: 'Bibliothèque' + subtitle: 'Démo Sylius Stack' + icon: 'lucide:library' + + # Login page logo + 'sylius_admin.security.login.page.logo': + image: + configuration: + name: 'Bibliothèque' + icon: 'lucide:library' + + # Dashboard: subtitle + statistics cards (TestApplication\Sylius\ShadcnAdminUi\Twig\Components\DashboardStats component) + 'sylius_admin.dashboard.index.content.header.title_block': + title: + configuration: + subheader: 'app.ui.dashboard_subtitle' + 'sylius_admin.dashboard.index.content': + stats: + template: 'admin/dashboard/stats.html.twig' + priority: -10 + + # "book" resource: the sylius_admin.book.* hooks complete or override + # the common sylius_admin.common.* hooks provided by the theme. + # To override an existing hookable, reuse its template (see sylius:debug:twig-hooks). + 'sylius_admin.book.show.content': + details: + template: 'admin/book/show/details.html.twig' + 'sylius_admin.book.show.content.header': + breadcrumbs: + template: '@SyliusShadcnAdminUi/shared/crud/show/content/header/breadcrumbs.html.twig' + configuration: + rendered_field: title + 'sylius_admin.book.update.content.header': + breadcrumbs: + template: '@SyliusShadcnAdminUi/shared/crud/update/content/header/breadcrumbs.html.twig' + configuration: + rendered_field: title diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/symfonycasts_tailwind.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/symfonycasts_tailwind.yaml new file mode 100644 index 00000000..957e1a8e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/symfonycasts_tailwind.yaml @@ -0,0 +1,10 @@ +symfonycasts_tailwind: + binary_version: 'v4.3.3' + input_css: + # Stylesheet of the admin panel (imports the theme of the bundle). + - '%kernel.project_dir%/assets/styles/admin.css' + +when@test: + symfonycasts_tailwind: + # Tests render the pages without building the stylesheet. + strict_mode: false diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/translation.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/translation.yaml new file mode 100644 index 00000000..1ca9555e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/translation.yaml @@ -0,0 +1,5 @@ +framework: + default_locale: fr + translator: + default_path: '%kernel.project_dir%/translations' + providers: diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/twig.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/twig.yaml new file mode 100644 index 00000000..3f795d92 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/twig_component.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/twig_component.yaml new file mode 100644 index 00000000..1030f271 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/twig_component.yaml @@ -0,0 +1,5 @@ +twig_component: + anonymous_template_directory: 'components/' + defaults: + # Namespace & directory for components + TestApplication\Sylius\ShadcnAdminUi\Twig\Components\: 'components/' diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/ux_icons.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/ux_icons.yaml new file mode 100644 index 00000000..9abe1c2e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/ux_icons.yaml @@ -0,0 +1,27 @@ +# Full configuration reference: https://symfony.com/bundles/ux-icons/current/index.html#configuration +ux_icons: + + # Default HTML attributes to add to all icons + default_icon_attributes: + + # Use current text color to fill the icon + fill: currentColor + + # Default dimensions + height: '1em' + width: '1em' + + # Throw an exception if an icon is not + ignore_not_found: false + + +when@prod: + ux_icons: + # Suppress exceptions for missing icons in production + ignore_not_found: true + +when@test: + ux_icons: + # No call to the Iconify API from the tests: every icon must be locked in assets/icons/. + iconify: + enabled: false diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/packages/validator.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/validator.yaml new file mode 100644 index 00000000..4015cd6e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/packages/validator.yaml @@ -0,0 +1,11 @@ +framework: + validation: + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # TestApplication\Sylius\ShadcnAdminUi\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/preload.php b/src/ShadcnAdminUi/tests/Functional/.application/config/preload.php new file mode 100644 index 00000000..77c80b9e --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/preload.php @@ -0,0 +1,16 @@ + [ + * 'App\\' => [ + * 'resource' => '../src/', + * ], + * ], + * ]); + * ``` + * + * @psalm-type ImportsConfig = list + * @psalm-type ParametersConfig = array|Param|null>|Param|null> + * @psalm-type ArgumentsType = list|array + * @psalm-type CallType = array|array{0:string, 1?:ArgumentsType, 2?:bool}|array{method:string, arguments?:ArgumentsType, returns_clone?:bool} + * @psalm-type TagsType = list>> // arrays inside the list must have only one element, with the tag name as the key + * @psalm-type CallbackType = string|array{0:string|ReferenceConfigurator,1:string}|\Closure|ReferenceConfigurator|ExpressionConfigurator + * @psalm-type DeprecationType = array{package: string, version: string, message?: string} + * @psalm-type DefaultsType = array{ + * public?: bool, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * } + * @psalm-type InstanceofType = array{ + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type DefinitionType = array{ + * class?: string, + * file?: string, + * parent?: string, + * shared?: bool, + * synthetic?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * configurator?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * decorates?: string, + * decorates_tag?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * from_callable?: CallbackType, + * } + * @psalm-type AliasType = string|array{ + * alias: string, + * public?: bool, + * deprecated?: DeprecationType, + * } + * @psalm-type PrototypeType = array{ + * resource: string, + * namespace?: string, + * exclude?: string|list, + * parent?: string, + * shared?: bool, + * lazy?: bool|string, + * public?: bool, + * abstract?: bool, + * deprecated?: DeprecationType, + * factory?: CallbackType, + * arguments?: ArgumentsType, + * properties?: array, + * configurator?: CallbackType, + * calls?: list, + * tags?: TagsType, + * resource_tags?: TagsType, + * autowire?: bool, + * autoconfigure?: bool, + * bind?: array, + * constructor?: string, + * } + * @psalm-type StackType = array{ + * stack: list>, + * public?: bool, + * deprecated?: DeprecationType, + * decorates?: string, + * decorates_tag?: string, + * decoration_inner_name?: string, + * decoration_priority?: int, + * decoration_on_invalid?: 'exception'|'ignore'|null, + * } + * @psalm-type ServicesConfig = array{ + * _defaults?: DefaultsType, + * _instanceof?: array, + * ... + * } + * @psalm-type ExtensionType = array + * @psalm-type FrameworkConfig = array{ + * secret?: scalar|Param|null, + * http_method_override?: bool|Param, // Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. // Default: false + * allowed_http_method_override?: null|list, + * trust_x_sendfile_type_header?: scalar|Param|null, // Set true to enable support for xsendfile in binary file responses. // Default: "%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%" + * ide?: scalar|Param|null, // Default: "%env(default::SYMFONY_IDE)%" + * test?: bool|Param, + * default_locale?: scalar|Param|null, // Default: "en" + * set_locale_from_accept_language?: bool|Param, // Whether to use the Accept-Language HTTP header to set the Request locale (only when the "_locale" request attribute is not passed). // Default: false + * set_content_language_from_locale?: bool|Param, // Whether to set the Content-Language HTTP header on the Response using the Request locale. // Default: false + * enabled_locales?: list, + * trusted_hosts?: Param|string|list, + * trusted_proxies?: mixed, // Default: ["%env(default::SYMFONY_TRUSTED_PROXIES)%"] + * trusted_headers?: Param|string|list, + * error_controller?: scalar|Param|null, // Default: "error_controller" + * handle_all_throwables?: bool|Param, // HttpKernel will handle all kinds of \Throwable. // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * stateless_token_ids?: list, + * check_header?: scalar|Param|null, // Whether to check the CSRF token in a header in addition to a cookie when using stateless protection. // Default: false + * cookie_name?: scalar|Param|null, // The name of the cookie to use when using stateless protection. // Default: "csrf-token" + * }, + * form?: bool|array{ // Form configuration + * enabled?: bool|Param, // Default: true + * csrf_protection?: bool|array{ + * enabled?: scalar|Param|null, // Default: null + * token_id?: scalar|Param|null, // Default: null + * field_name?: scalar|Param|null, // Default: "_token" + * field_attr?: array, + * }, + * }, + * http_cache?: bool|array{ // HTTP cache configuration + * enabled?: bool|Param, // Default: false + * debug?: bool|Param, // Default: "%kernel.debug%" + * trace_level?: "none"|"short"|"full"|Param, + * trace_header?: scalar|Param|null, + * default_ttl?: int|Param, + * private_headers?: list, + * skip_response_headers?: list, + * allow_reload?: bool|Param, + * allow_revalidate?: bool|Param, + * stale_while_revalidate?: int|Param, + * stale_if_error?: int|Param, + * terminate_on_cache_hit?: bool|Param, // Deprecated: Setting the "framework.http_cache.terminate_on_cache_hit.terminate_on_cache_hit" configuration option is deprecated. It will be removed in version 9.0. + * }, + * esi?: bool|array{ // ESI configuration + * enabled?: bool|Param, // Default: false + * }, + * ssi?: bool|array{ // SSI configuration + * enabled?: bool|Param, // Default: false + * }, + * fragments?: bool|array{ // Fragments configuration + * enabled?: bool|Param, // Default: false + * hinclude_default_template?: scalar|Param|null, // Default: null + * path?: scalar|Param|null, // Default: "/_fragment" + * }, + * profiler?: bool|array{ // Profiler configuration + * enabled?: bool|Param, // Default: false + * collect?: bool|Param, // Default: true + * collect_parameter?: scalar|Param|null, // The name of the parameter to use to enable or disable collection on a per request basis. // Default: null + * only_exceptions?: bool|Param, // Default: false + * only_main_requests?: bool|Param, // Default: false + * dsn?: scalar|Param|null, // Default: "file:%kernel.cache_dir%/profiler" + * collect_serializer_data?: true|Param, // Deprecated: Setting the "framework.profiler.collect_serializer_data.collect_serializer_data" configuration option is deprecated. It will be removed in version 9.0. // Default: true + * }, + * workflows?: bool|array{ + * enabled?: bool|Param, // Default: false + * workflows?: array, + * definition_validators?: list, + * support_strategy?: scalar|Param|null, + * initial_marking?: \BackedEnum|Param|string|list, + * events_to_dispatch?: null|list, + * places?: Param|string|list, + * }>, + * transitions?: list, + * to?: \BackedEnum|Param|string|list, + * weight?: int|Param, // Default: 1 + * metadata?: array, + * }>, + * metadata?: array, + * }>, + * }, + * router?: bool|array{ // Router configuration + * enabled?: bool|Param, // Default: false + * resource?: scalar|Param|null, + * type?: scalar|Param|null, + * default_uri?: scalar|Param|null, // The default URI used to generate URLs in a non-HTTP context. // Default: null + * http_port?: scalar|Param|null, // Default: 80 + * https_port?: scalar|Param|null, // Default: 443 + * strict_requirements?: scalar|Param|null, // set to true to throw an exception when a parameter does not match the requirements set to false to disable exceptions when a parameter does not match the requirements (and return null instead) set to null to disable parameter checks against requirements 'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production // Default: true + * utf8?: bool|Param, // Default: true + * }, + * session?: bool|array{ // Session configuration + * enabled?: bool|Param, // Default: false + * storage_factory_id?: scalar|Param|null, // Default: "session.storage.factory.native" + * handler_id?: scalar|Param|null, // Defaults to using the native session handler, or to the native *file* session handler if "save_path" is not null. + * name?: scalar|Param|null, + * cookie_lifetime?: scalar|Param|null, + * cookie_path?: scalar|Param|null, + * cookie_domain?: scalar|Param|null, + * cookie_secure?: true|false|"auto"|Param, // Default: "auto" + * cookie_httponly?: bool|Param, // Default: true + * cookie_samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax" + * use_cookies?: bool|Param, + * gc_divisor?: scalar|Param|null, + * gc_probability?: scalar|Param|null, + * gc_maxlifetime?: scalar|Param|null, + * save_path?: scalar|Param|null, // Defaults to "%kernel.cache_dir%/sessions" if the "handler_id" option is not null. + * metadata_update_threshold?: int|Param, // Seconds to wait between 2 session metadata updates. // Default: 0 + * }, + * request?: bool|array{ // Request configuration + * enabled?: bool|Param, // Default: false + * formats?: array>, + * }, + * assets?: bool|array{ // Assets configuration + * enabled?: bool|Param, // Default: true + * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false + * version_strategy?: scalar|Param|null, // Default: null + * version?: scalar|Param|null, // Default: null + * version_format?: scalar|Param|null, // Default: "%%s?%%s" + * json_manifest_path?: scalar|Param|null, // Default: null + * base_path?: scalar|Param|null, // Default: "" + * base_urls?: Param|string|list, + * packages?: array, + * }>, + * }, + * asset_mapper?: bool|array{ // Asset Mapper configuration + * enabled?: bool|Param, // Default: true + * paths?: Param|string|array, + * excluded_patterns?: list, + * exclude_dotfiles?: bool|Param, // If true, any files starting with "." will be excluded from the asset mapper. // Default: true + * server?: bool|Param, // If true, a "dev server" will return the assets from the public directory (true in "debug" mode only by default). // Default: true + * public_prefix?: scalar|Param|null, // The public path where the assets will be written to (and served from when "server" is true). // Default: "/assets/" + * missing_import_mode?: "strict"|"warn"|"ignore"|Param, // Behavior if an asset cannot be found when imported from JavaScript or CSS files - e.g. "import './non-existent.js'". "strict" means an exception is thrown, "warn" means a warning is logged, "ignore" means the import is left as-is. // Default: "warn" + * extensions?: array, + * importmap_path?: scalar|Param|null, // The path of the importmap.php file. // Default: "%kernel.project_dir%/importmap.php" + * importmap_polyfill?: scalar|Param|null, // The importmap name that will be used to load the polyfill. Set to false to disable. // Default: "es-module-shims" + * importmap_script_attributes?: array, + * vendor_dir?: scalar|Param|null, // The directory to store JavaScript vendors. // Default: "%kernel.project_dir%/assets/vendor" + * precompress?: bool|array{ // Precompress assets with Brotli, Zstandard and gzip. + * enabled?: bool|Param, // Default: false + * formats?: list, + * extensions?: list, + * }, + * }, + * translator?: bool|array{ // Translator configuration + * enabled?: bool|Param, // Default: true + * fallbacks?: Param|string|list, + * logging?: bool|Param, // Default: false + * formatter?: scalar|Param|null, // Default: "translator.formatter.default" + * cache_dir?: scalar|Param|null, // Default: "%kernel.cache_dir%/translations" + * default_path?: scalar|Param|null, // The default path used to load translations. // Default: "%kernel.project_dir%/translations" + * paths?: list, + * pseudo_localization?: bool|array{ + * enabled?: bool|Param, // Default: false + * accents?: bool|Param, // Default: true + * expansion_factor?: float|Param, // Default: 1.0 + * brackets?: bool|Param, // Default: true + * parse_html?: bool|Param, // Default: false + * localizable_html_attributes?: list, + * }, + * providers?: array, + * locales?: list, + * }>, + * globals?: array, + * domain?: string|Param, + * }>, + * }, + * validation?: bool|array{ // Validation configuration + * enabled?: bool|Param, // Default: true + * enable_attributes?: bool|Param, // Default: true + * static_method?: Param|string|list, + * translation_domain?: scalar|Param|null, // Default: "validators" + * email_validation_mode?: "html5"|"html5-allow-no-tld"|"strict"|Param, // Default: "html5" + * mapping?: array{ + * paths?: list, + * }, + * not_compromised_password?: bool|array{ + * enabled?: bool|Param, // When disabled, compromised passwords will be accepted as valid. // Default: true + * endpoint?: scalar|Param|null, // API endpoint for the NotCompromisedPassword Validator. // Default: null + * }, + * disable_translation?: bool|Param, // Default: false + * property_metadata_existence_check?: bool|Param, // When enabled, validateProperty() and validatePropertyValue() throw an exception if no metadata is found for the given property. // Default: false + * auto_mapping?: array, + * }>, + * }, + * serializer?: bool|array{ // Serializer configuration + * enabled?: bool|Param, // Default: false + * enable_attributes?: bool|Param, // Default: true + * name_converter?: scalar|Param|null, + * circular_reference_handler?: scalar|Param|null, + * max_depth_handler?: scalar|Param|null, + * mapping?: array{ + * paths?: list, + * }, + * default_context?: array, + * named_serializers?: array, + * include_built_in_normalizers?: bool|Param, // Whether to include the built-in normalizers // Default: true + * include_built_in_encoders?: bool|Param, // Whether to include the built-in encoders // Default: true + * }>, + * }, + * property_access?: bool|array{ // Property access configuration + * enabled?: bool|Param, // Default: true + * magic_call?: bool|Param, // Default: false + * magic_get?: bool|Param, // Default: true + * magic_set?: bool|Param, // Default: true + * throw_exception_on_invalid_index?: bool|Param, // Default: false + * throw_exception_on_invalid_property_path?: bool|Param, // Default: true + * }, + * type_info?: bool|array{ // Type info configuration + * enabled?: bool|Param, // Default: true + * aliases?: array, + * }, + * property_info?: bool|array{ // Property info configuration + * enabled?: bool|Param, // Default: true + * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. // Default: true + * }, + * cache?: array{ // Cache configuration + * prefix_seed?: scalar|Param|null, // Used to namespace cache keys when using several apps with the same shared backend. // Default: "_%kernel.project_dir%.%kernel.container_class%" + * app?: scalar|Param|null, // App related cache pools configuration. // Default: "cache.adapter.filesystem" + * system?: scalar|Param|null, // System related cache pools configuration. // Default: "cache.adapter.system" + * directory?: scalar|Param|null, // Default: "%kernel.share_dir%/pools/app" + * default_psr6_provider?: scalar|Param|null, + * default_redis_provider?: scalar|Param|null, // Default: "redis://localhost" + * default_valkey_provider?: scalar|Param|null, // Default: "valkey://localhost" + * default_memcached_provider?: scalar|Param|null, // Default: "memcached://localhost" + * default_doctrine_dbal_provider?: scalar|Param|null, // Default: "database_connection" + * default_pdo_provider?: scalar|Param|null, // Default: null + * pools?: array, + * tags?: scalar|Param|null, // Default: null + * public?: bool|Param, // Default: false + * default_lifetime?: scalar|Param|null, // Default lifetime of the pool. + * provider?: scalar|Param|null, // Overwrite the setting from the default provider for this adapter. + * early_expiration_message_bus?: scalar|Param|null, + * clearer?: scalar|Param|null, + * marshaller?: scalar|Param|null, // The marshaller service to use for this pool. + * }>, + * }, + * php_errors?: array{ // PHP errors handling configuration + * log?: mixed, // Use the application logger instead of the PHP logger for logging PHP errors. // Default: true + * throw?: bool|Param, // Throw PHP errors as \ErrorException instances. // Default: true + * }, + * exceptions?: array, + * web_link?: bool|array{ // Web links configuration + * enabled?: bool|Param, // Default: false + * }, + * lock?: Param|bool|string|array{ // Lock configuration + * enabled?: bool|Param, // Default: false + * resources?: Param|string|array>, + * }, + * semaphore?: Param|bool|string|array{ // Semaphore configuration + * enabled?: bool|Param, // Default: false + * resources?: Param|string|array, + * }, + * messenger?: bool|array{ // Messenger configuration + * enabled?: bool|Param, // Default: false + * routing?: array>, + * serializer?: array{ + * default_serializer?: scalar|Param|null, // Service id to use as the default serializer for the transports. // Default: "messenger.transport.native_php_serializer" + * symfony_serializer?: array{ + * format?: scalar|Param|null, // Serialization format for the messenger.transport.symfony_serializer service (which is not the serializer used by default). // Default: "json" + * context?: array, + * }, + * }, + * transports?: array, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * retry_strategy?: Param|string|array{ + * service?: scalar|Param|null, // Service id to override the retry strategy entirely. // Default: null + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: this delay = (delay * (multiple ^ retries)). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness to apply to the delay (between 0 and 1). // Default: 0.1 + * }, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use when processing messages. // Default: null + * }>, + * failure_transport?: scalar|Param|null, // Transport name to send failed messages to (after all retries have failed). // Default: null + * stop_worker_on_signals?: Param|int|string|list, + * default_bus?: scalar|Param|null, // Default: null + * buses?: array, + * }>, + * }>, + * }, + * scheduler?: bool|array{ // Scheduler configuration + * enabled?: bool|Param, // Default: false + * }, + * disallow_search_engine_index?: bool|Param, // Enabled by default when debug is enabled. // Default: true + * http_client?: bool|array{ // HTTP Client configuration + * enabled?: bool|Param, // Default: true + * max_host_connections?: int|Param, // The maximum number of connections to a single host. + * default_options?: array{ + * headers?: array, + * vars?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...) + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. // Default: 86400 + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: Param|int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }, + * mock_response_factory?: scalar|Param|null, // `true` to always return empty 200 responses, or the id of the service to use to generate mock responses - which should be either an invokable or an iterable. + * scoped_clients?: array, + * headers?: array, + * max_redirects?: int|Param, // The maximum number of redirects to follow. + * http_version?: scalar|Param|null, // The default HTTP version, typically 1.1 or 2.0, leave to null for the best version. + * resolve?: array, + * proxy?: scalar|Param|null, // The URL of the proxy to pass requests through or null for automatic detection. + * no_proxy?: scalar|Param|null, // A comma separated list of hosts that do not require a proxy to be reached. + * timeout?: float|Param, // The idle timeout, defaults to the "default_socket_timeout" ini parameter. + * max_duration?: float|Param, // The maximum execution time for the request+response as a whole. + * bindto?: scalar|Param|null, // A network interface name, IP address, a host name or a UNIX socket to bind to. + * verify_peer?: bool|Param, // Indicates if the peer should be verified in a TLS context. + * verify_host?: bool|Param, // Indicates if the host should exist as a certificate common name. + * cafile?: scalar|Param|null, // A certificate authority file. + * capath?: scalar|Param|null, // A directory that contains multiple certificate authority files. + * local_cert?: scalar|Param|null, // A PEM formatted certificate file. + * local_pk?: scalar|Param|null, // A private key file. + * passphrase?: scalar|Param|null, // The passphrase used to encrypt the "local_pk" file. + * ciphers?: scalar|Param|null, // A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...). + * peer_fingerprint?: array{ // Associative array: hashing algorithm => hash(es). + * sha1?: mixed, + * pin-sha256?: mixed, + * md5?: mixed, + * }, + * crypto_method?: scalar|Param|null, // The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants. + * mock_response_factory?: scalar|Param|null, // `true` to always return empty 200 responses, `false` to disable mocking, or the id of the service to use to generate mock responses (invokable or iterable). + * extra?: array, + * rate_limiter?: scalar|Param|null, // Rate limiter name to use for throttling requests. // Default: null + * caching?: bool|array{ // Caching configuration. + * enabled?: bool|Param, // Default: false + * cache_pool?: string|Param, // The taggable cache pool to use for storing the responses. // Default: "cache.http_client" + * shared?: bool|Param, // Indicates whether the cache is shared (public) or private. // Default: true + * max_ttl?: int|Param, // The maximum TTL (in seconds) allowed for cached responses. // Default: 86400 + * }, + * retry_failed?: bool|array{ + * enabled?: bool|Param, // Default: false + * retry_strategy?: scalar|Param|null, // service id to override the retry strategy. // Default: null + * http_codes?: Param|int|string|array, + * }>, + * max_retries?: int|Param, // Default: 3 + * delay?: int|Param, // Time in ms to delay (or the initial value when multiplier is used). // Default: 1000 + * multiplier?: float|Param, // If greater than 1, delay will grow exponentially for each retry: delay * (multiple ^ retries). // Default: 2 + * max_delay?: int|Param, // Max time in ms that a retry should ever be delayed (0 = infinite). // Default: 0 + * jitter?: float|Param, // Randomness in percent (between 0 and 1) to apply to the delay. // Default: 0.1 + * }, + * }>, + * }, + * mailer?: bool|array{ // Mailer configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * dsn?: scalar|Param|null, // Default: null + * transports?: array, + * envelope?: array{ // Mailer Envelope configuration + * sender?: scalar|Param|null, + * recipients?: Param|string|list, + * allowed_recipients?: Param|string|list, + * }, + * headers?: array, + * dkim_signer?: bool|array{ // DKIM signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Key content, or path to key (in PEM format with the `file://` prefix) // Default: "" + * domain?: scalar|Param|null, // Default: "" + * select?: scalar|Param|null, // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: "" + * options?: array, + * }, + * smime_signer?: bool|array{ // S/MIME signer configuration + * enabled?: bool|Param, // Default: false + * key?: scalar|Param|null, // Path to key (in PEM format) // Default: "" + * certificate?: scalar|Param|null, // Path to certificate (in PEM format without the `file://` prefix) // Default: "" + * passphrase?: scalar|Param|null, // The private key passphrase // Default: null + * extra_certificates?: scalar|Param|null, // Default: null + * sign_options?: int|Param, // Default: null + * }, + * smime_encrypter?: bool|array{ // S/MIME encrypter configuration + * enabled?: bool|Param, // Default: false + * repository?: scalar|Param|null, // S/MIME certificate repository service. This service shall implement the `Symfony\Component\Mailer\EventListener\SmimeCertificateRepositoryInterface`. // Default: "" + * cipher?: int|Param, // A set of algorithms used to encrypt the message // Default: null + * }, + * }, + * secrets?: bool|array{ + * enabled?: bool|Param, // Default: true + * vault_directory?: scalar|Param|null, // Default: "%kernel.project_dir%/config/secrets/%kernel.runtime_environment%" + * local_dotenv_file?: scalar|Param|null, // Default: "%kernel.project_dir%/.env.%kernel.environment%.local" + * decryption_env_var?: scalar|Param|null, // Default: "base64:default::SYMFONY_DECRYPTION_SECRET" + * }, + * notifier?: bool|array{ // Notifier configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. Defaults to the default bus if the Messenger component is installed. // Default: null + * chatter_transports?: array, + * texter_transports?: array, + * notification_on_failed_messages?: bool|Param, // Default: false + * channel_policy?: array>, + * admin_recipients?: list, + * }, + * rate_limiter?: bool|array{ // Rate limiter configuration + * enabled?: bool|Param, // Default: false + * limiters?: array, + * limit?: int|Param, // The maximum allowed hits in a fixed interval or burst. + * interval?: scalar|Param|null, // Configures the fixed interval if "policy" is set to "fixed_window" or "sliding_window". The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * rate?: array{ // Configures the fill rate if "policy" is set to "token_bucket". + * interval?: scalar|Param|null, // Configures the rate interval. The value must be a number followed by "second", "minute", "hour", "day", "week" or "month" (or their plural equivalent). + * amount?: int|Param, // Amount of tokens to add each interval. // Default: 1 + * }, + * anchor_at?: scalar|Param|null, // Aligns the "fixed_window" policy to a calendar (e.g. "2024-01-05 00:00:00 UTC" combined with `interval: 1 month` resets the counter on the 5th of each month). UTC if not specified. // Default: null + * }>, + * }, + * uid?: bool|array{ // Uid configuration + * enabled?: bool|Param, // Default: false + * default_uuid_version?: 7|6|4|1|Param, // Default: 7 + * name_based_uuid_version?: 5|3|Param, // Default: 5 + * name_based_uuid_namespace?: scalar|Param|null, + * time_based_uuid_version?: 7|6|1|Param, // Default: 7 + * time_based_uuid_node?: scalar|Param|null, + * uuid47_secret?: scalar|Param|null, // A high-entropy secret used by the "uuid47_transformer" service. Defaults to "kernel.secret". // Default: null + * }, + * html_sanitizer?: bool|array{ // HtmlSanitizer configuration + * enabled?: bool|Param, // Default: false + * sanitizers?: array, + * block_elements?: Param|string|list, + * drop_elements?: Param|string|list, + * allow_attributes?: array, + * drop_attributes?: array, + * force_attributes?: array>, + * force_https_urls?: bool|Param, // Transforms URLs using the HTTP scheme to use the HTTPS scheme instead. // Default: false + * allowed_link_schemes?: Param|string|list, + * allowed_link_hosts?: Param|null|string|list, + * allow_relative_links?: bool|Param, // Allows relative URLs to be used in links href attributes. // Default: false + * allowed_media_schemes?: Param|string|list, + * allowed_media_hosts?: Param|null|string|list, + * allow_relative_medias?: bool|Param, // Allows relative URLs to be used in media source attributes (img, audio, video, ...). // Default: false + * with_attribute_sanitizers?: Param|string|list, + * without_attribute_sanitizers?: Param|string|list, + * max_input_length?: int|Param, // The maximum length allowed for the sanitized input. // Default: 0 + * }>, + * }, + * webhook?: bool|array{ // Webhook configuration + * enabled?: bool|Param, // Default: false + * message_bus?: scalar|Param|null, // The message bus to use. // Default: "messenger.default_bus" + * event_header_name?: scalar|Param|null, // Default: "Webhook-Event" + * id_header_name?: scalar|Param|null, // Default: "Webhook-Id" + * signature_header_name?: scalar|Param|null, // Default: "Webhook-Signature" + * signing_algorithm?: scalar|Param|null, // Default: "sha256" + * routing?: array, + * }, + * remote_event?: bool|array{ // RemoteEvent configuration + * enabled?: bool|Param, // Default: false + * }, + * json_streamer?: bool|array{ // JSON streamer configuration + * enabled?: bool|Param, // Default: false + * default_options?: array{ + * include_null_properties?: bool|Param, // Encode the properties with null value // Default: false + * ... + * }, + * }, + * } + * @psalm-type TwigConfig = array{ + * form_themes?: list, + * globals?: array + * }>, + * autoescape_service?: scalar|Param|null, // Default: null + * autoescape_service_method?: scalar|Param|null, // Default: null + * cache?: scalar|Param|null, // Default: true + * charset?: scalar|Param|null, // Default: "%kernel.charset%" + * debug?: bool|Param, // Default: "%kernel.debug%" + * strict_variables?: bool|Param, // Default: "%kernel.debug%" + * auto_reload?: scalar|Param|null, + * optimizations?: int|Param, + * default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates" + * file_name_pattern?: Param|string|list, + * paths?: array, + * date?: array{ // The default format options used by the date filter. + * format?: scalar|Param|null, // Default: "F j, Y H:i" + * interval_format?: scalar|Param|null, // Default: "%d days" + * timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null + * }, + * number_format?: array{ // The default format options for the number_format filter. + * decimals?: int|Param, // Default: 0 + * decimal_point?: scalar|Param|null, // Default: "." + * thousands_separator?: scalar|Param|null, // Default: "," + * }, + * mailer?: array{ + * html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null + * }, + * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|Param|null, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Deprecated: Setting the "security.erase_credentials.erase_credentials" configuration option is deprecated. It will be removed in Symfony 9.0, as the "eraseCredentials()" method was removed in Symfony 8.0. // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|Param|null, + * strategy_service?: scalar|Param|null, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|Param|null, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|Param|null, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|Param|null, // Default: null + * time_cost?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * }>, + * providers?: array, + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service?: scalar|Param|null, + * base_dn?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: null + * search_password?: scalar|Param|null, // Default: null + * extra_fields?: list, + * default_roles?: Param|string|list, + * role_fetcher?: scalar|Param|null, // Default: null + * uid_key?: scalar|Param|null, // Default: "sAMAccountName" + * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|Param|null, // Default: null + * }, + * entity?: array{ + * class?: scalar|Param|null, // The full entity class name of your user class. + * property?: scalar|Param|null, // Default: null + * manager_name?: scalar|Param|null, // Default: null + * }, + * }>, + * firewalls?: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|Param|null, + * access_denied_url?: scalar|Param|null, + * access_denied_handler?: scalar|Param|null, + * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|Param|null, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|Param|null, + * logout?: array{ + * enable_csrf?: bool|Param|null, // Default: null + * csrf_token_id?: scalar|Param|null, // Default: "logout" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|Param|null, + * path?: scalar|Param|null, // Default: "/logout" + * target?: scalar|Param|null, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: Param|string|list<"*"|"cache"|"cookies"|"storage"|"clientHints"|"executionContexts"|"prefetchCache"|"prerenderCache"|Param>, + * delete_cookies?: Param|string|array, + * }, + * switch_user?: array{ + * provider?: scalar|Param|null, + * parameter?: scalar|Param|null, // Default: "_switch_user" + * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|Param|null, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|Param|null, // Default: "1 minute" + * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by the login rate limiter ("auto" to use the default one when the Lock component is configured, or null to disable locking). // Default: "auto" + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|Param|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties?: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|Param|null, // The user provider to load users from. + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * login_path?: scalar|Param|null, // Default: "/login" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: null + * token_extractors?: Param|string|list, + * token_handler?: Param|string|array{ + * id?: scalar|Param|null, + * oidc_user_info?: Param|string|array{ + * base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). The latter must end with a slash when it has a path, as the discovery path is resolved against it as a relative URL. + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri?: Param|string|list, + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * enforce_key_usage_verification?: bool|Param, // When enabled (default), only keys explicitly designated for signature (via "use":"sig" or a "key_ops" entry containing "sign"/"verify") are accepted. When disabled, keys without any usage designation are also accepted; keys explicitly restricted to encryption are still rejected. // Default: true + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. + * issuers?: list, + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url?: scalar|Param|null, // CAS server validation URL + * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" + * http_client?: scalar|Param|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|Param|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * service?: scalar|Param|null, + * user_providers?: Param|string|list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: Param|string|array{ + * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|Param|null, // Default: null + * }, + * }, + * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|Param|null, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|Param|null, // Default: "/" + * domain?: scalar|Param|null, // Default: null + * secure?: true|false|"auto"|Param, // Defaults to the value of "framework.session.cookie_secure", or to "auto". + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Defaults to the value of "framework.session.cookie_samesite", or to "lax". + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|Param|null, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|Param|null, // Default: null + * methods?: Param|string|list, + * allow_if?: scalar|Param|null, // Default: null + * roles?: Param|string|list, + * }>, + * role_hierarchy?: array>, + * } + * @psalm-type TwigComponentConfig = array{ + * defaults?: array, + * anonymous_template_directory?: scalar|Param|null, // Defaults to `components` + * profiler?: bool|array{ // Enables the profiler for Twig Component + * enabled?: bool|Param, // Default: "%kernel.debug%" + * collect_components?: bool|Param, // Collect components instances // Default: true + * }, + * } + * @psalm-type StimulusConfig = array{ + * controller_paths?: list, + * controllers_json?: scalar|Param|null, // Default: "%kernel.project_dir%/assets/controllers.json" + * } + * @psalm-type SyliusTwigHooksConfig = array{ + * enable_autoprefixing?: bool|Param, // Default: false + * hook_name_section_separator?: scalar|Param|null, // Default: false + * supported_hookable_types?: array, + * hooks?: array, + * props?: array, + * configuration?: array, + * priority?: int|Param, // Default: null + * condition?: scalar|Param|null, // Default: null + * ... + * }>>, + * } + * @psalm-type BabdevPagerfantaConfig = array{ + * default_view?: scalar|Param|null, // Default: "default" + * default_sequential_view?: scalar|Param|null, // The view to render pagers which the default view cannot render (i.e. cursor pagers with a numbered view), defaults to the "_sequential" view when one exists // Default: null + * default_twig_template?: scalar|Param|null, // Default: "@BabDevPagerfanta/default.html.twig" + * exceptions_strategy?: array{ + * out_of_range_page?: "to_http_not_found"|"custom"|Param, // Default: "to_http_not_found" + * not_valid_current_page?: "to_http_not_found"|"custom"|Param, // Default: "to_http_not_found" + * invalid_cursor?: "to_http_bad_request"|"custom"|Param, // Default: "to_http_bad_request" + * }, + * } + * @psalm-type SyliusResourceConfig = array{ + * resources?: array, + * settings?: array{ + * paginate?: mixed, // Default: null + * limit?: mixed, // Default: null + * allowed_paginate?: list, + * default_page_size?: int|Param, // Default: 10 + * default_templates_dir?: scalar|Param|null, // Default: null + * sortable?: bool|Param, // Default: false + * sorting?: mixed, // Default: null + * filterable?: bool|Param, // Default: false + * criteria?: mixed, // Default: null + * state_machine_component?: scalar|Param|null, // Default: null + * }, + * translation?: bool|array{ + * enabled?: bool|Param, // Default: true + * locale_provider?: scalar|Param|null, // Default: "sylius.translation_locale_provider.immutable" + * }, + * drivers?: list<"doctrine/orm"|"doctrine/mongodb-odm"|"doctrine/phpcr-odm"|Param>, + * mapping?: array{ + * imports?: list, + * paths?: list, + * }, + * authorization_checker?: scalar|Param|null, // Default: "sylius.resource_controller.authorization_checker.disabled" + * routing_path_bc_layer?: bool|Param, + * path_segment_name_generator?: scalar|Param|null, // Specify a path name generator to use. // Default: "sylius.metadata.path_segment_name_generator.dash" + * } + * @psalm-type KnpMenuConfig = array{ + * providers?: array{ + * builder_alias?: bool|Param, // Default: true + * }, + * twig?: array{ + * template?: scalar|Param|null, // Default: "@KnpMenu/menu.html.twig" + * }, + * route_voter?: bool|Param, // Default: true + * default_renderer?: scalar|Param|null, // Default: "twig" + * } + * @psalm-type SyliusAdminUiConfig = array{ + * routing?: array, + * } + * @psalm-type SyliusGridConfig = array{ + * drivers?: list<"doctrine/orm"|"doctrine/phpcr-odm"|Param>, + * templates?: array{ + * filter?: array, + * action?: array, + * bulk_action?: array, + * }, + * grids?: array, + * }, + * sorting?: array, + * limits?: list, + * fields?: array, + * }>, + * filters?: array, + * form_options?: list, + * default_value?: mixed, + * }>, + * actions?: array, + * }>>, + * removals?: array, + * }>, + * } + * @psalm-type SyliusTwigExtraConfig = array{ + * twig_ux?: array{ + * anonymous_component_template_prefixes?: array, + * }, + * } + * @psalm-type DoctrineConfig = array{ + * dbal?: array{ + * default_connection?: scalar|Param|null, + * types?: array, + * driver_schemes?: array, + * connections?: array, + * mapping_types?: array, + * default_table_options?: array, + * schema_manager_factory?: scalar|Param|null, // Default: "doctrine.dbal.default_schema_manager_factory" + * result_cache?: scalar|Param|null, + * replicas?: array + * }>, + * ... + * }>, + * ... + * }, + * orm?: array{ + * default_entity_manager?: scalar|Param|null, + * enable_native_lazy_objects?: bool|Param, // Deprecated: The "enable_native_lazy_objects" option is deprecated and will be removed in DoctrineBundle 4.0, as native lazy objects are now always enabled. // Default: true + * controller_resolver?: bool|array{ + * enabled?: bool|Param, // Default: true + * auto_mapping?: bool|Param, // Deprecated: The "doctrine.orm.controller_resolver.auto_mapping.auto_mapping" option is deprecated and will be removed in DoctrineBundle 4.0, as it only accepts `false` since 3.0. // Set to true to enable using route placeholders as lookup criteria when the primary key doesn't match the argument name // Default: false + * evict_cache?: bool|Param, // Set to true to fetch the entity from the database instead of using the cache, if any // Default: false + * }, + * entity_managers?: array, + * }>, + * }>, + * ... + * }, + * connection?: scalar|Param|null, + * class_metadata_factory_name?: scalar|Param|null, // Default: "Doctrine\\ORM\\Mapping\\ClassMetadataFactory" + * default_repository_class?: scalar|Param|null, // Default: "Doctrine\\ORM\\EntityRepository" + * auto_mapping?: scalar|Param|null, // Default: false + * naming_strategy?: scalar|Param|null, // Default: "doctrine.orm.naming_strategy.default" + * quote_strategy?: scalar|Param|null, // Default: "doctrine.orm.quote_strategy.default" + * typed_field_mapper?: scalar|Param|null, // Default: "doctrine.orm.typed_field_mapper.default" + * entity_listener_resolver?: scalar|Param|null, // Default: null + * fetch_mode_subselect_batch_size?: scalar|Param|null, + * repository_factory?: scalar|Param|null, // Default: "doctrine.orm.container_repository_factory" + * schema_ignore_classes?: list, + * validate_xml_mapping?: bool|Param, // Set to "true" to opt-in to the new mapping driver mode that was added in Doctrine ORM 2.14 and will be mandatory in ORM 3.0. See https://github.com/doctrine/orm/pull/6728. // Default: false + * second_level_cache?: array{ + * region_cache_driver?: Param|string|array{ + * type?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * pool?: scalar|Param|null, + * }, + * region_lock_lifetime?: scalar|Param|null, // Default: 60 + * log_enabled?: bool|Param, // Default: true + * region_lifetime?: scalar|Param|null, // Default: 3600 + * enabled?: bool|Param, // Default: true + * factory?: scalar|Param|null, + * regions?: array, + * loggers?: array, + * }, + * hydrators?: array, + * mappings?: array, + * dql?: array{ + * string_functions?: array, + * numeric_functions?: array, + * datetime_functions?: array, + * }, + * filters?: array, + * ... + * }>, + * identity_generation_preferences?: array, + * }>, + * resolve_target_entities?: array, + * ... + * }, + * } + * @psalm-type TwigExtraConfig = array{ + * cache?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * html?: bool|array{ + * enabled?: bool|Param, // Default: true + * }, + * markdown?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * intl?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * cssinliner?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * inky?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * string?: bool|array{ + * enabled?: bool|Param, // Default: false + * }, + * commonmark?: array{ + * renderer?: array{ // Array of options for rendering HTML. + * block_separator?: scalar|Param|null, + * inner_separator?: scalar|Param|null, + * soft_break?: scalar|Param|null, + * }, + * html_input?: "strip"|"allow"|"escape"|Param, // How to handle HTML input. + * allow_unsafe_links?: bool|Param, // Remove risky link and image URLs by setting this to false. // Default: true + * max_nesting_level?: int|Param, // The maximum nesting level for blocks. // Default: 9223372036854775807 + * max_delimiters_per_line?: int|Param, // The maximum number of strong/emphasis delimiters per line. // Default: 9223372036854775807 + * slug_normalizer?: array{ // Array of options for configuring how URL-safe slugs are created. + * instance?: mixed, + * max_length?: int|Param, // Default: 255 + * unique?: mixed, + * }, + * commonmark?: array{ // Array of options for configuring the CommonMark core extension. + * enable_em?: bool|Param, // Default: true + * enable_strong?: bool|Param, // Default: true + * use_asterisk?: bool|Param, // Default: true + * use_underscore?: bool|Param, // Default: true + * unordered_list_markers?: list, + * }, + * ... + * }, + * } + * @psalm-type UxIconsConfig = array{ + * icon_dir?: scalar|Param|null, // The local directory where icons are stored. // Default: "%kernel.project_dir%/assets/icons" + * default_icon_attributes?: array, + * icon_sets?: array, + * suffixes?: array, + * }>, + * }>, + * aliases?: array, + * iconify?: bool|array{ // Configuration for the remote icon service. + * enabled?: bool|Param, // Default: true + * on_demand?: bool|Param, // Whether to download icons "on demand". // Default: true + * auto_lock?: bool|Param, // Persist "on demand" icons to the local icon directory (see "icon_dir"). Recommended in dev only. Requires "on_demand" to be enabled. // Default: false + * endpoint?: scalar|Param|null, // The endpoint for the Iconify icons API. // Default: "https://api.iconify.design" + * }, + * ignore_not_found?: bool|Param, // Ignore error when an icon is not found. Set to 'true' to fail silently. // Default: false + * } + * @psalm-type SymfonycastsTailwindConfig = array{ + * input_css?: list, + * config_file?: scalar|Param|null, // Path to the tailwind.config.js file // Default: "%kernel.project_dir%/tailwind.config.js" + * binary?: scalar|Param|null, // The tailwind binary to use instead of downloading a new one // Default: null + * binary_version?: scalar|Param|null, // Tailwind CLI version to download - required unless "binary" is set (run "tailwind:init" to configure) // Default: null + * binary_platform?: "auto"|"linux-arm64"|"linux-arm64-musl"|"linux-x64"|"linux-x64-musl"|"macos-arm64"|"macos-x64"|"windows-x64"|Param, // Tailwind CLI platform to download - "auto" will try to detect the platform automatically // Default: "auto" + * postcss_config_file?: scalar|Param|null, // Path to PostCSS config file which is passed to the Tailwind CLI // Default: null + * strict_mode?: bool|Param|null, // When enabled, an exception will be thrown if there are no built assets (default: false in `test` env, true otherwise) // Default: null + * process_timeout?: int|Param, // Timeout in seconds for the Tailwind build process - use "0" to disable // Default: 60 + * } + * @psalm-type TalesFromADevTwigExtraTailwindConfig = array{ + * tailwind_merge?: array{ + * additional_configuration?: mixed, // Default: [] + * }, + * } + * @psalm-type SyliusShadcnAdminUiConfig = array{ + * sidebar?: array{ + * variant?: "sidebar"|"floating"|"inset"|Param, // Default variant of the sidebar on every admin page, overridable per page hook. // Default: "sidebar" + * }, + * themes?: list, + * } + * @psalm-type UxToolkitConfig = array{ + * components?: array{ // Where "ux:install" installs the Twig components of recipes. + * dir?: scalar|Param|null, // The directory TwigComponent loads anonymous components from, relative to the project directory. It must match your configuration: the Twig "default_path" followed by the TwigComponent "anonymous_template_directory". // Default: "templates/components" + * subdirectory?: scalar|Param|null, // A subdirectory of "dir" to install the components in. Their names get it as a prefix, which the installer adds to the copied files, e.g. "" for "ui". // Default: null + * }, + * controllers?: array{ // Where "ux:install" installs the Stimulus controllers of recipes. + * dir?: scalar|Param|null, // The directory your Stimulus setup loads controllers from, relative to the project directory. It must match your configuration: an entry of "stimulus.controller_paths" with AssetMapper, the "controllersDir" option with Reprise, or the directory passed to "require.context()" with Webpack Encore. // Default: "assets/controllers" + * subdirectory?: scalar|Param|null, // A subdirectory of "dir" to install the controllers in. Their identifiers get it as a prefix, which the installer adds to the copied files, e.g. "ui--dialog" for "ui". // Default: null + * }, + * preview?: bool|array{ // Wires the preview assets of kits into AssetMapper, Tailwind and the importmap. + * enabled?: bool|Param, // Default: false + * kits?: list, + * }, + * } + * @psalm-type WhiteOctoberPagerfantaConfig = array{ // Deprecated: The "white_october_pagerfanta" configuration node is deprecated, migrate your configuration to the "babdev_pagerfanta" configuration node. + * exceptions_strategy?: array{ + * out_of_range_page?: scalar|Param|null, // Default: "to_http_not_found" + * not_valid_current_page?: scalar|Param|null, // Default: "to_http_not_found" + * }, + * default_view?: scalar|Param|null, // Default: "default" + * } + * @psalm-type ConfigType = array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * stimulus?: StimulusConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * knp_menu?: KnpMenuConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * doctrine?: DoctrineConfig, + * twig_extra?: TwigExtraConfig, + * ux_icons?: UxIconsConfig, + * symfonycasts_tailwind?: SymfonycastsTailwindConfig, + * tales_from_a_dev_twig_extra_tailwind?: TalesFromADevTwigExtraTailwindConfig, + * sylius_shadcn_admin_ui?: SyliusShadcnAdminUiConfig, + * ux_toolkit?: UxToolkitConfig, + * white_october_pagerfanta?: WhiteOctoberPagerfantaConfig, + * "when@dev"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * stimulus?: StimulusConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * knp_menu?: KnpMenuConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * doctrine?: DoctrineConfig, + * twig_extra?: TwigExtraConfig, + * ux_icons?: UxIconsConfig, + * symfonycasts_tailwind?: SymfonycastsTailwindConfig, + * tales_from_a_dev_twig_extra_tailwind?: TalesFromADevTwigExtraTailwindConfig, + * sylius_shadcn_admin_ui?: SyliusShadcnAdminUiConfig, + * ux_toolkit?: UxToolkitConfig, + * }, + * "when@prod"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * stimulus?: StimulusConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * knp_menu?: KnpMenuConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * doctrine?: DoctrineConfig, + * twig_extra?: TwigExtraConfig, + * ux_icons?: UxIconsConfig, + * symfonycasts_tailwind?: SymfonycastsTailwindConfig, + * tales_from_a_dev_twig_extra_tailwind?: TalesFromADevTwigExtraTailwindConfig, + * sylius_shadcn_admin_ui?: SyliusShadcnAdminUiConfig, + * ux_toolkit?: UxToolkitConfig, + * }, + * "when@test"?: array{ + * imports?: ImportsConfig, + * parameters?: ParametersConfig, + * services?: ServicesConfig, + * framework?: FrameworkConfig, + * twig?: TwigConfig, + * security?: SecurityConfig, + * twig_component?: TwigComponentConfig, + * stimulus?: StimulusConfig, + * sylius_twig_hooks?: SyliusTwigHooksConfig, + * babdev_pagerfanta?: BabdevPagerfantaConfig, + * sylius_resource?: SyliusResourceConfig, + * knp_menu?: KnpMenuConfig, + * sylius_admin_ui?: SyliusAdminUiConfig, + * sylius_grid?: SyliusGridConfig, + * sylius_twig_extra?: SyliusTwigExtraConfig, + * doctrine?: DoctrineConfig, + * twig_extra?: TwigExtraConfig, + * ux_icons?: UxIconsConfig, + * symfonycasts_tailwind?: SymfonycastsTailwindConfig, + * tales_from_a_dev_twig_extra_tailwind?: TalesFromADevTwigExtraTailwindConfig, + * sylius_shadcn_admin_ui?: SyliusShadcnAdminUiConfig, + * ux_toolkit?: UxToolkitConfig, + * }, + * ..., + * }> + * } + */ +final class App +{ + /** + * @param ConfigType $config + * + * @psalm-return ConfigType + */ + public static function config(array $config): array + { + /** @var ConfigType $config */ + $config = AppReference::config($config); + + return $config; + } +} + +namespace Symfony\Component\Routing\Loader\Configurator; + +/** + * This class provides array-shapes for configuring the routes of an application. + * + * Example: + * + * ```php + * // config/routes.php + * namespace Symfony\Component\Routing\Loader\Configurator; + * + * return Routes::config([ + * 'controllers' => [ + * 'resource' => 'routing.controllers', + * ], + * ]); + * ``` + * + * @psalm-type RouteConfig = array{ + * path: string|array, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type ImportConfig = array{ + * resource: string, + * type?: string, + * exclude?: string|list, + * prefix?: string|array, + * name_prefix?: string, + * trailing_slash_on_root?: bool, + * controller?: string, + * methods?: string|list, + * requirements?: array, + * defaults?: array, + * options?: array, + * host?: string|array, + * schemes?: string|list, + * condition?: string, + * locale?: string, + * format?: string, + * utf8?: bool, + * stateless?: bool, + * } + * @psalm-type AliasConfig = array{ + * alias: string, + * deprecated?: array{package:string, version:string, message?:string}, + * } + * @psalm-type RoutesConfig = array{ + * "when@dev"?: array, + * "when@prod"?: array, + * "when@test"?: array, + * ... + * } + */ +final class Routes +{ + /** + * @param RoutesConfig $config + * + * @psalm-return RoutesConfig + */ + public static function config(array $config): array + { + return $config; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/routes.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/routes.yaml new file mode 100644 index 00000000..c779b3eb --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/routes.yaml @@ -0,0 +1,7 @@ +# The demo has no front office: the root redirects to the admin panel. +home: + path: / + controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController + defaults: + route: sylius_admin_ui_dashboard + permanent: false diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/routes/security.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/routes/security.yaml new file mode 100644 index 00000000..f853be15 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/routes/security.yaml @@ -0,0 +1,3 @@ +_security_logout: + resource: security.route_loader.logout + type: service diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/routes/sylius_admin_ui.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/routes/sylius_admin_ui.yaml new file mode 100644 index 00000000..23595fff --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/routes/sylius_admin_ui.yaml @@ -0,0 +1,3 @@ +sylius_admin_ui: + resource: '@SyliusAdminUiBundle/config/routes.php' + prefix: /admin diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml new file mode 100644 index 00000000..6744c22b --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/routes/sylius_resource.yaml @@ -0,0 +1,3 @@ +sylius_resource_loader: + resource: 'sylius.symfony.routing.loader.resource' + type: service diff --git a/src/ShadcnAdminUi/tests/Functional/.application/config/services.yaml b/src/ShadcnAdminUi/tests/Functional/.application/config/services.yaml new file mode 100644 index 00000000..75195946 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/config/services.yaml @@ -0,0 +1,14 @@ +parameters: + locale: 'en' + +services: + _defaults: + autowire: true + autoconfigure: true + + TestApplication\Sylius\ShadcnAdminUi\: + resource: '../src/' + exclude: + - '../src/Entity/' + - '../src/Enum/' + - '../src/Kernel.php' diff --git a/src/ShadcnAdminUi/tests/Functional/.application/importmap.php b/src/ShadcnAdminUi/tests/Functional/.application/importmap.php new file mode 100644 index 00000000..c4039241 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/importmap.php @@ -0,0 +1,21 @@ + ['path' => './assets/admin.js', 'entrypoint' => true], + '@hotwired/stimulus' => ['version' => '3.2.2'], + '@symfony/stimulus-bundle' => ['path' => '../../../vendor/symfony/stimulus-bundle/assets/dist/loader.js'], +]; diff --git a/src/ShadcnAdminUi/tests/Functional/.application/public/index.php b/src/ShadcnAdminUi/tests/Functional/.application/public/index.php new file mode 100644 index 00000000..20222ed9 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/public/index.php @@ -0,0 +1,28 @@ + dirname(__DIR__), +]; + +if (file_exists(dirname(__DIR__, 4) . '/vendor/autoload_runtime.php')) { + require_once dirname(__DIR__, 4) . '/vendor/autoload_runtime.php'; +} else { + require_once dirname(__DIR__, 6) . '/vendor/autoload_runtime.php'; +} + +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Command/LoadBooksCommand.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Command/LoadBooksCommand.php new file mode 100644 index 00000000..8032356b --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Command/LoadBooksCommand.php @@ -0,0 +1,86 @@ +addOption('if-empty', null, InputOption::VALUE_NONE, 'Only loads the data when the table is empty'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $repository = $this->em->getRepository(Book::class); + + if ($input->getOption('if-empty') && $repository->count([]) > 0) { + $io->note('Books already exist: nothing to do.'); + + return Command::SUCCESS; + } + + $this->em->createQuery('DELETE FROM ' . Book::class)->execute(); + + foreach (self::BOOKS as $i => [$title, $author, $category, $available, $summary]) { + $book = new Book(); + $book->setTitle($title); + $book->setAuthorName($author); + $book->setCategory($category); + $book->setAvailable($available); + $book->setSummary($summary); + $book->setCreatedAt(new \DateTimeImmutable(sprintf('-%d days', $i * 3))); + $this->em->persist($book); + } + $this->em->flush(); + + $io->success(sprintf('%d books loaded.', \count(self::BOOKS))); + + return Command::SUCCESS; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Command/UpdateShadcnRecipesCommand.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Command/UpdateShadcnRecipesCommand.php new file mode 100644 index 00000000..1800acde --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Command/UpdateShadcnRecipesCommand.php @@ -0,0 +1,192 @@ + → , component('Button') → component('shadcn:Button'); + * - Stimulus controllers: "dialog" → "shadcn--dialog" (data-controller, actions, targets, values...). + */ +#[AsCommand('app:shadcn:update', 'Updates the shadcn kit recipes bundled in templates/components/shadcn and assets/controllers/shadcn')] +final class UpdateShadcnRecipesCommand extends Command +{ + private const PREFIX = 'shadcn'; + + protected function configure(): void + { + $this->addArgument('recipes', InputArgument::IS_ARRAY, 'Recipes to update (e.g. "button dialog"), every bundled recipe by default'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $recipes = $input->getArgument('recipes'); + + if (!$this->getApplication()?->has('ux:install')) { + $io->error('The "ux:install" command is missing: run this command in the "dev" environment, with symfony/ux-toolkit installed.'); + + return Command::FAILURE; + } + + $bundleDir = \dirname(__DIR__, 5); + $componentsDir = $bundleDir . '/templates/components/' . self::PREFIX; + $controllersDir = $bundleDir . '/assets/controllers/' . self::PREFIX; + + $recipes = $recipes ?: $this->bundledRecipes($componentsDir, $bundleDir . '/vendor/symfony/ux-toolkit/kits/shadcn'); + $filesystem = new Filesystem(); + $tmpDir = $filesystem->tempnam(sys_get_temp_dir(), 'shadcn-recipes-'); + $filesystem->remove($tmpDir); + + try { + foreach ($recipes as $recipe) { + $output = new BufferedOutput(); + $status = $this->getApplication()->find('ux:install')->run(new ArrayInput([ + 'recipe' => $recipe, + '--kit' => 'shadcn', + '--destination' => $tmpDir, + '--force' => true, + ]), $output); + + if (Command::SUCCESS !== $status) { + $io->error(\sprintf('Unable to install the "%s" recipe:', $recipe)); + $io->writeln($output->fetch()); + + return Command::FAILURE; + } + } + + $controllers = $this->controllerIdentifiers($tmpDir . '/assets/controllers'); + $files = [ + ...$this->copy($tmpDir . '/templates/components', $componentsDir, $controllers, $filesystem), + ...$this->copy($tmpDir . '/assets/controllers', $controllersDir, $controllers, $filesystem), + ]; + } finally { + $filesystem->remove($tmpDir); + } + + $io->listing(array_map(static fn (string $file): string => Path::makeRelative($file, $bundleDir), $files)); + $io->success(\sprintf('%d recipe(s) updated. Review the git diff: local changes to the recipes must be restored by hand.', \count($recipes))); + + return Command::SUCCESS; + } + + /** + * Recipes whose main component is bundled: "AlertDialog.html.twig" → "alert-dialog". + * + * @return list + */ + private function bundledRecipes(string $componentsDir, string $kitDir): array + { + $recipes = []; + + foreach ((new Finder())->files()->in($componentsDir)->depth(0)->name('*.html.twig')->sortByName() as $file) { + $recipe = strtolower((string) preg_replace('/(?getBasename('.html.twig'))); + + if (is_dir($kitDir . '/' . $recipe)) { + $recipes[] = $recipe; + } + } + + return $recipes; + } + + /** + * Stimulus identifiers of the installed controllers, longest first ("dropdown-menu-sub" before "dropdown-menu"). + * + * @return list + */ + private function controllerIdentifiers(string $controllersDir): array + { + if (!is_dir($controllersDir)) { + return []; + } + + $identifiers = []; + + foreach ((new Finder())->files()->in($controllersDir)->name('*_controller.js') as $file) { + $identifiers[] = str_replace('_', '-', $file->getBasename('_controller.js')); + } + + usort($identifiers, static fn (string $a, string $b): int => \strlen($b) <=> \strlen($a)); + + return $identifiers; + } + + /** + * @param list $controllers + * + * @return list Written files + */ + private function copy(string $sourceDir, string $targetDir, array $controllers, Filesystem $filesystem): array + { + if (!is_dir($sourceDir)) { + return []; + } + + $written = []; + + foreach ((new Finder())->files()->in($sourceDir)->sortByName() as $file) { + $target = $targetDir . '/' . $file->getRelativePathname(); + $filesystem->dumpFile($target, $this->rewrite($file->getContents(), $controllers)); + $written[] = $target; + } + + return $written; + } + + /** + * @param list $controllers + */ + private function rewrite(string $content, array $controllers): string + { + $content = (string) preg_replace('/<(\/?)twig:(?=[A-Z])/', '<$1twig:' . self::PREFIX . ':', $content); + $content = (string) preg_replace("/\\bcomponent\\((['\"])(?=[A-Z])/", 'component($1' . self::PREFIX . ':', $content); + + foreach ($controllers as $controller) { + $id = preg_quote($controller, '/'); + $prefixed = self::PREFIX . '--' . $controller; + + // Actions ("click->dialog#open", "dialog#open") and Stimulus attributes ("data-dialog-target", "data-dialog-open-value"), + // but not plain data attributes such as "data-collapsible-mode". + $content = (string) preg_replace('/(? $match[1] . preg_replace('/(?createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): void + { + $this->title = $title; + } + + public function getAuthorName(): ?string + { + return $this->authorName; + } + + public function setAuthorName(?string $authorName): void + { + $this->authorName = $authorName; + } + + public function getCategory(): ?BookCategory + { + return $this->category; + } + + public function setCategory(?BookCategory $category): void + { + $this->category = $category; + } + + public function isAvailable(): bool + { + return $this->available; + } + + public function setAvailable(bool $available): void + { + $this->available = $available; + } + + public function getSummary(): ?string + { + return $this->summary; + } + + public function setSummary(?string $summary): void + { + $this->summary = $summary; + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): void + { + $this->createdAt = $createdAt; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Enum/BookCategory.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Enum/BookCategory.php new file mode 100644 index 00000000..abd86083 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Enum/BookCategory.php @@ -0,0 +1,36 @@ + 'blue', + self::MYSTERY => 'green', + self::SCIENCE_FICTION => 'purple', + self::FANTASY => 'orange', + self::HISTORY => 'yellow', + self::TECHNOLOGY => 'red', + }; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Form/BookType.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Form/BookType.php new file mode 100644 index 00000000..a492d99b --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Form/BookType.php @@ -0,0 +1,53 @@ +add('title', null, ['label' => 'app.ui.title', 'help' => 'app.ui.title_help']) + ->add('authorName', null, ['label' => 'app.ui.author_name']) + ->add('category', EnumType::class, [ + 'label' => 'app.ui.category', + 'class' => BookCategory::class, + 'choice_label' => fn (BookCategory $choice) => ucfirst($choice->value), + 'required' => false, + 'placeholder' => 'app.ui.choose_category', + ]) + ->add('summary', TextareaType::class, ['label' => 'app.ui.summary', 'required' => false]) + ->add('available', CheckboxType::class, ['label' => 'app.ui.available', 'required' => false]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults(['data_class' => Book::class]); + } + + public function getBlockPrefix(): string + { + return 'sylius_resource'; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Grid/BookGrid.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Grid/BookGrid.php new file mode 100644 index 00000000..9cdaff54 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Grid/BookGrid.php @@ -0,0 +1,81 @@ +value)] = $category->value; + } + + $gridBuilder + ->orderBy('title') + ->setLimits([10, 25, 50]) + ->addFilter(StringFilter::create('search', ['title', 'authorName'])->setLabel('sylius.ui.search')) + ->addFilter(SelectFilter::create('category', $categories)->setLabel('app.ui.category')) + ->addFilter(BooleanFilter::create('available')->setLabel('app.ui.available')) + ->addFilter(DateFilter::create('createdAt')->setLabel('app.ui.created_at')) + ->addField(StringField::create('title')->setLabel('app.ui.title')->setSortable(true)) + ->addField(StringField::create('authorName')->setLabel('app.ui.author_name')->setSortable(true)) + ->addField(TwigField::create('category', 'book/grid/category.html.twig')->setLabel('app.ui.category')) + ->addField( + TwigField::create('available', '@SyliusShadcnAdminUi/shared/grid/field/boolean.html.twig') + ->setLabel('app.ui.available') + ->withOptions([ + 'vars' => [ + 'th_class' => 'text-center', + 'td_class' => 'text-center', + ], + ]), + ) + ->addField(DateTimeField::create('createdAt', 'd/m/Y')->setLabel('app.ui.created_at')->setSortable(true)) + ->addActionGroup(MainActionGroup::create(CreateAction::create())) + ->addActionGroup(ItemActionGroup::create(ShowAction::create(), UpdateAction::create(), DeleteAction::create())) + ->addActionGroup(BulkActionGroup::create(DeleteAction::create())) + ; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Kernel.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Kernel.php new file mode 100644 index 00000000..679169d9 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Kernel.php @@ -0,0 +1,27 @@ +menuBuilder->createMenu($options); + + $menu->addChild('dashboard', ['route' => 'sylius_admin_ui_dashboard']) + ->setLabel('sylius.ui.dashboard') + ->setLabelAttribute('icon', 'lucide:layout-dashboard') + ; + + $library = $menu->addChild('library') + ->setLabel('app.ui.library') + ->setLabelAttribute('icon', 'lucide:library') + ; + $library->addChild('books', ['route' => 'app_admin_book_index'])->setLabel('app.ui.books'); + $library->addChild('new_book', ['route' => 'app_admin_book_create'])->setLabel('app.ui.new_book'); + + $menu->addChild('section', ['extras' => ['section' => true]]) + ->setLabel('app.ui.section') + ; + $subMenu = $menu->addChild('submenu') + ->setLabel('app.ui.submenu') + ->setLabelAttribute('icon', 'lucide:menu') + ; + $subMenu->addChild('section_1', ['extras' => ['header' => true]])->setLabel('app.ui.section_1'); + $subMenu->addChild('item_1', ['route' => 'app_admin_book_index'])->setLabel('app.ui.item_1'); + + $menu->addChild('docs', ['uri' => 'https://stack.sylius.com', 'linkAttributes' => ['target' => '_blank']]) + ->setLabel('sylius.ui.documentation') + ->setLabelAttribute('icon', 'lucide:book-open') + ; + + return $menu; + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/src/Twig/Components/DashboardStats.php b/src/ShadcnAdminUi/tests/Functional/.application/src/Twig/Components/DashboardStats.php new file mode 100644 index 00000000..eccba567 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/src/Twig/Components/DashboardStats.php @@ -0,0 +1,64 @@ +em->getRepository(Book::class)->count([]); + } + + public function getAvailable(): int + { + return $this->em->getRepository(Book::class)->count(['available' => true]); + } + + /** + * @return list + */ + public function getByCategory(): array + { + return $this->em->createQueryBuilder() + ->select('b.category AS category, COUNT(b.id) AS total') + ->from(Book::class, 'b') + ->groupBy('b.category') + ->orderBy('total', 'DESC') + ->getQuery() + ->getResult() + ; + } + + /** + * @return list + */ + public function getLatest(): array + { + return $this->em->getRepository(Book::class)->findBy([], ['createdAt' => 'DESC'], 5); + } +} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/admin/book/show/details.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/admin/book/show/details.html.twig new file mode 100644 index 00000000..90e46b33 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/admin/book/show/details.html.twig @@ -0,0 +1,48 @@ +{% set book = hookable_metadata.context.resource %} + +
    + + + {{ book.title }} + {{ book.authorName }} + + + + {{ 'sylius.ui.edit'|trans }} + + + + +

    + {{ book.summary ?: 'Aucun résumé.' }} +

    +
    +
    + + + + {{ 'sylius.ui.details'|trans }} + + +
    +
    {{ 'app.ui.category'|trans }}
    +
    {% if book.category %}{{ book.category.value|capitalize }}{% else %}—{% endif %}
    + +
    {{ 'app.ui.available'|trans }}
    +
    + {% if book.available %} + {{ 'sylius.ui.yes_label'|trans }} + {% else %} + {{ 'sylius.ui.no_label'|trans }} + {% endif %} +
    + +
    {{ 'app.ui.created_at'|trans }}
    +
    {{ book.createdAt|date('d/m/Y') }}
    + +
    ID
    +
    +
    +
    +
    +
    diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/admin/dashboard/stats.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/admin/dashboard/stats.html.twig new file mode 100644 index 00000000..fdfdc480 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/admin/dashboard/stats.html.twig @@ -0,0 +1 @@ + diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/book/grid/category.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/book/grid/category.html.twig new file mode 100644 index 00000000..b402af1f --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/book/grid/category.html.twig @@ -0,0 +1 @@ +{% if data %}{{ data.value|capitalize }}{% endif %} diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/components/DashboardStats.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/DashboardStats.html.twig new file mode 100644 index 00000000..02b520b0 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/DashboardStats.html.twig @@ -0,0 +1,88 @@ +{# Data is read here: inside a nested component, "this" is the child component. #} +{% set total = this.total %} +{% set available = this.available %} +{% set by_category = this.byCategory %} +{% set latest = this.latest %} +{# Full class names: Tailwind does not detect dynamically built classes. #} +{% set bar_colors = ['bg-chart-1', 'bg-chart-2', 'bg-chart-3', 'bg-chart-4', 'bg-chart-5'] %} + +
    +
    + {% for stat in [ + {label: 'app.ui.total_books', value: total, icon: 'lucide:book-copy'}, + {label: 'app.ui.available_books', value: available, icon: 'lucide:circle-check', class: 'text-success'}, + {label: 'app.ui.unavailable_books', value: total - available, icon: 'lucide:circle-x', class: 'text-muted-foreground'}, + {label: 'app.ui.categories', value: by_category|length, icon: 'lucide:tags'}, + ] %} + + + {{ stat.label|trans }} + {{ stat.value }} + + + + + + {% endfor %} +
    + +
    + + + {{ 'app.ui.latest_books'|trans }} + {{ 'app.ui.latest_books_description'|trans }} + + + {{ 'app.ui.see_all'|trans }} + + + + + + + + + {{ 'app.ui.title'|trans }} + {{ 'app.ui.category'|trans }} + {{ 'app.ui.created_at'|trans }} + + + + {% for book in latest %} + + + {{ book.title }} +
    {{ book.authorName }}
    +
    + + {% if book.category %}{{ book.category.value|capitalize }}{% endif %} + + {{ book.createdAt|date('d/m/Y') }} +
    + {% endfor %} +
    +
    +
    +
    + + + + {{ 'app.ui.by_category'|trans }} + + + {% for row in by_category %} + {% set percent = total > 0 ? (row.total / total * 100)|round : 0 %} +
    +
    + {{ row.category ? row.category.value|capitalize : '—' }} + {{ row.total }} +
    +
    +
    +
    +
    + {% endfor %} +
    +
    +
    +
    diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ActionButton.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ActionButton.html.twig new file mode 100644 index 00000000..9734fdb2 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ActionButton.html.twig @@ -0,0 +1 @@ + diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ArrayClasses.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ArrayClasses.html.twig new file mode 100644 index 00000000..5f8f763a --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ArrayClasses.html.twig @@ -0,0 +1 @@ +
    diff --git a/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ForwardingCard.html.twig b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ForwardingCard.html.twig new file mode 100644 index 00000000..e132d5b8 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/templates/components/Fixture/ForwardingCard.html.twig @@ -0,0 +1 @@ +Title diff --git a/src/ShadcnAdminUi/tests/Functional/.application/translations/messages.en.yaml b/src/ShadcnAdminUi/tests/Functional/.application/translations/messages.en.yaml new file mode 100644 index 00000000..bdbd3cd8 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/translations/messages.en.yaml @@ -0,0 +1,27 @@ +app: + ui: + book: Book + books: Books + new_book: New book + library: Library + title: Title + title_help: The title as printed on the cover. + author_name: Author + category: Category + choose_category: Choose a category + summary: Summary + available: Available + created_at: Created at + dashboard_subtitle: Library overview + total_books: Books in catalog + available_books: Available + unavailable_books: Unavailable + categories: Categories + latest_books: Latest additions + latest_books_description: The 5 most recently added books + see_all: See all + by_category: Books by category + section: Section + submenu: Submenu + section_1: Section 1 + item_1: Item 1 diff --git a/src/ShadcnAdminUi/tests/Functional/.application/translations/messages.fr.yaml b/src/ShadcnAdminUi/tests/Functional/.application/translations/messages.fr.yaml new file mode 100644 index 00000000..5c4d9ee3 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/.application/translations/messages.fr.yaml @@ -0,0 +1,27 @@ +app: + ui: + book: Livre + books: Livres + new_book: Nouveau livre + library: Bibliothèque + title: Titre + title_help: Le titre tel qu'imprimé sur la couverture. + author_name: Auteur + category: Catégorie + choose_category: Choisir une catégorie + summary: Résumé + available: Disponible + created_at: Ajouté le + dashboard_subtitle: Vue d'ensemble de la bibliothèque + total_books: Livres au catalogue + available_books: Disponibles + unavailable_books: Indisponibles + categories: Catégories + latest_books: Derniers ajouts + latest_books_description: Les 5 livres ajoutés le plus récemment + see_all: Tout voir + by_category: Répartition par catégorie + section: Section + submenu: Sous-menu + section_1: Section 1 + item_1: Élément 1 diff --git a/src/ShadcnAdminUi/tests/Functional/DemoApplicationTest.php b/src/ShadcnAdminUi/tests/Functional/DemoApplicationTest.php new file mode 100644 index 00000000..3a893243 --- /dev/null +++ b/src/ShadcnAdminUi/tests/Functional/DemoApplicationTest.php @@ -0,0 +1,138 @@ +client = self::createClient(); + + $application = new Application(self::$kernel); + $application->setAutoExit(false); + + foreach ([['doctrine:schema:drop', '--force' => true], ['doctrine:schema:create'], ['app:load-books']] as $command) { + $output = new BufferedOutput(); + self::assertSame(0, $application->run(new ArrayInput(['command' => array_shift($command)] + $command), $output), $output->fetch()); + } + } + + public function testTheLoginPageRenders(): void + { + $this->client->request('GET', '/admin/login'); + + self::assertResponseIsSuccessful(); + self::assertSelectorExists('input[name="_csrf_admin_security_token"]'); + self::assertStringContainsString('