Skip to content

[Toolkit] Make kits self-describing so ux.symfony.com renders them with zero per-kit code #3711

Description

@Kocal

Problem

A kit lives in symfony/ux (src/Toolkit/kits/<kit>/), but ux.symfony.com hardcodes every kit. Adding one means 8 hand-edits, plus 1-2 per recipe:

ToolkitKitId enum (case + color()), assets/icons/toolkit/<kit>.svg, assets/styles/toolkit-<kit>.css (Tailwind entry + theme tokens, 238 lines for Flowbite), assets/toolkit-<kit>.js (import + app.register() each controller), importmap.php, symfonycasts_tailwind.yaml + composer.json tailwind list (duplicated), composer.lock, and one templates/toolkit/docs/<kit>/<recipe>.md.twig per recipe.

Kit identity is declared twice, controllers listed three times, docs written 1:1 by hand. Community kits get none of this: no preview, no rendered docs.

Goal

Make a kit fully self-describing: metadata, theme, docs, and rendering assets all ship inside the kit (or inside Toolkit). ux.symfony.com becomes a generic renderer with zero per-kit code. Community-kit preview is out of scope here, but this is the first step toward it (a future bin/ux-toolkit-kit-preview).

Key insight

renderComponentDoc() renders .md.twig with Twig into a markdown string, then parses it with CommonMark. The .md.twig tangles three things:

  • directives (::: tabs, > [!NOTE], preview) -> portable, host-agnostic
  • scaffolding (_base_component.md.twig: install tabs, API table) -> logic over the Kit/Recipe model
  • host rendering (the signed preview iframe) -> the only genuinely site-specific bit

toolkit_code_example() is just sugar expanding to a ::: tabs block with a Preview and a Code tab. The fix is to separate these three.

Design: 3 layers

1. Markdown extensions inside Toolkit (src/Toolkit/src/Markdown/)

For a first iteration these live in Toolkit (Symfony\UX\Toolkit\Markdown\), not a separate package; the site consumes them from the vendored symfony/ux-toolkit. league/commonmark is a dev-only Toolkit dependency (guarded by class_exists), so installing Toolkit to scaffold components never pulls it in; hosts that render docs provide it. Standalone League CommonMark extensions, added à la carte to your own converter (no all-in-one factory):

$env->addExtension(new AlertExtension())
    ->addExtension(new TabsExtension())
    ->addExtension(new CodePreviewExtension(previewController: 'app_toolkit_component_preview'));
  • CodePreview is the live-render primitive (code + options, no kit knowledge). Its renderer points at a controller that accepts code and returns a Response; the host implements it (site = signed iframe; a future bin/ux-toolkit-kit-preview = local). No endpoint configured -> static highlighted block.
  • Rendered via overridable Twig templates (@UXToolkit/markdown/{alert,tabs,popover,code_preview}.html.twig, semantic HTML with Toolkit classes, not Shadcn). A host restyles by overriding a template.
  • Generic inline preview = a fenced code block flagged {"preview": true}.
  • Ships the portable frontend under src/Toolkit/assets/: a base CSS the templates reference + Stimulus controllers tabs / popover / clipboard (plain ES modules served through AssetMapper, no build). Rendering works standalone.

2. Toolkit: docs move into the kit

Each recipe gets a doc.md (narrative + example refs, no Twig):

### Borders
Add `border` to the items.

::: example Borders {"height": "300px"}
  • ::: example <Name> is a single-line leaf directive (no closing :::). Toolkit reads examples/<Name>.html.twig and builds Tabs(CodePreview, FencedCode) directly in the AST (no markdown re-parse, so code containing ``` or ::: never breaks it).
  • The site's _base_component.md.twig moves into Toolkit as @UXToolkit/doc/recipe.md.twig. RecipeDocRenderer exposes renderAsHtml($kit, $recipe, $previewUrlGenerator) (Toolkit assembles the CommonMark converter from its own extensions plus the host's PreviewUrlGenerator) and renderAsMarkdown($kit, $recipe) (portable Markdown for .md/LLM consumption, with ::: example resolved to code fences).
  • The manifest gains color / icon (the theme is not a manifest field: theme.css is pulled in through the kit.js -> kit.css import chain). Controllers are auto-discovered by scanning assets/controllers/.
{ "name": "shadcn", "color": "#000", "icon": "icon.svg" }

The kit also ships, at its root, kit.css (the Tailwind entry @import "tailwindcss"; @source "."; @import "./theme.css";) and kit.js (the preview entrypoint: import './kit.css'; then boots Stimulus and registers the kit's controllers via relative imports). These two are preview/demo artifacts, not shipped to end users by ux:install.

3. ux.symfony.com becomes a generic renderer

  • Consume Toolkit's Symfony\UX\Toolkit\Markdown\ extensions; delete the site's App\Service\CommonMark\* copies. Build a per-render converter (Toolkit extensions + ExampleExtension($recipe) + the site's signed PreviewUrlGenerator); optionally override @UXToolkit/markdown/* to keep the Shadcn look.

  • Drop ToolkitKitId -> auto-discover kits by scanning vendor/symfony/ux-toolkit/kits/*/manifest.json.

  • Drop the per-recipe .md.twig -> render via RecipeDocRenderer, the site keeps its shell (TOC, sidebar).

  • Wire front assets dynamically, nothing generated or committed. A KitDiscovery service + a single KitAssetsPass compiler pass (plus one decorator):

    • AssetMapper: append kit dirs to AssetMapperRepository's $paths (makes kit.js/kit.css/controllers servable).
    • Tailwind: append each kit's kit.css to symfonycasts/tailwind's input_css.
    • importmap: decorate ImportMapConfigReader to ->add() one entrypoint per kit pointing at kits/<kit>/kit.js. AssetMapper traces kit.js's relative imports (./kit.css + the controllers), so no per-controller entry and no ControllersMapGenerator hook.

    Add a kit -> cache:clear -> it's wired. No importmap.php, toolkit-<kit>.{js,css}, or Tailwind-config edits.

For kit authors

Before: kit source in ux, plus up to 8 files on ux.symfony.com and a .md.twig per recipe.

After: the kit ships everything (manifest metadata, theme.css, doc.md, controllers), and the site picks it up automatically. No site edit to add, update, or theme a kit.

Implementation (2 PRs)

  1. symfony/ux: Toolkit markdown extensions + overridable templates/CSS/JS (src/Toolkit/assets/), the CodePreview preview-controller contract, ::: example, RecipeDocRenderer (the relocated @UXToolkit/doc/recipe.md.twig), manifest color/icon, doc.md per recipe, migrate common/shadcn/flowbite-4.
  2. symfony/ux.symfony.com: consume Toolkit's extensions, kit auto-discovery, KitAssetsPass + ImportMapConfigReader decorator, render via RecipeDocRenderer, delete the hand-written per-kit files.

Open questions

  • Preview transport. code reaches the preview controller. Signed GET (simple, caps size at the URL limit) vs POST (no cap, more signing/caching). Which default does Toolkit ship?
  • doc.md scope. Narrative-only, or allow a full-layout override for recipes the scaffolding can't express?
  • Tailwind on a vendored input. Confirm symfonycasts/tailwind compiles an input_css under vendor/ with a relative @source (Tailwind v4).

Full implementation plan (2 PRs, task-by-task)

Goal: make a UX Toolkit kit fully self-describing (docs + metadata + theme + rendering assets in the kit/Toolkit) so ux.symfony.com renders any kit with zero per-kit code.

Architecture: Two PRs. PR A (symfony/ux) puts the markdown extensions, their default rendering (overridable Twig templates + CSS + Stimulus JS), the doc-markdown generator, and the kit metadata into src/Toolkit. PR B (symfony/ux.symfony.com) makes the site a generic renderer that converts Toolkit's doc-markdown with Toolkit's extensions and wires kit assets dynamically via DI.

Tech Stack: PHP 8.4, league/commonmark ^2.4 (Toolkit dev-only, provided by hosts that render), Twig, Symfony FrameworkBundle, AssetMapper + importmap, symfony/stimulus-bundle, symfonycasts/tailwind, PHPUnit + spatie/phpunit-snapshot-assertions. Toolkit ships no-build (plain-JS) Stimulus controllers served via AssetMapper.

Global Constraints

  • PHP >=8.4. Classes final, promoted readonly where it fits, typed everything.
  • PHP CS @Symfony + @Symfony:risky; Symfony license header on every PHP file. PSR-4 Symfony\UX\Toolkit\ -> src/Toolkit/src.
  • league/commonmark is require-dev in src/Toolkit/composer.json, never require. Any Toolkit code path that instantiates a Markdown extension guards with class_exists(\League\CommonMark\Environment\Environment::class) and throws RuntimeException('Install league/commonmark to render Toolkit docs as HTML.').
  • New manifest.json fields are optional (no BC break).
  • Toolkit ships plain-JS Stimulus controllers (no build, no dist/), served via AssetMapper. oxfmt + oxlint clean on JS.
  • Toolkit tests: cd src/Toolkit && php vendor/bin/phpunit. Snapshots: php vendor/bin/simple-phpunit -d --update-snapshots.
  • Specific exceptions only. Commit per task. PR A ships before PR B.

PR A: symfony/ux (Toolkit markdown + rendering assets + doc model + metadata)

New PHP under src/Toolkit/src/Markdown/ and src/Toolkit/src/Doc/; new frontend under src/Toolkit/assets/; default templates under src/Toolkit/templates/markdown/. Port PHP from ux.symfony.com/src/Service/CommonMark/ (generalize, drop ToolkitKitId/site coupling). Toolkit renders each node through an overridable Twig template and ships base CSS + Stimulus controllers, so rendering is portable and hosts can override to restyle.

Task A1: league/commonmark dev dep + Markdown skeleton + guard

Files:

  • Modify: src/Toolkit/composer.json (require-dev: "league/commonmark": "^2.4")
  • Modify: src/Toolkit/src/Assert.php (add Assert::commonMarkAvailable(): void)
  • Test: src/Toolkit/tests/Markdown/AssertCommonMarkTest.php

Interfaces:

  • Produces: Assert::commonMarkAvailable() throws RuntimeException when league/commonmark is absent. Called by every extension constructor.

  • No MarkdownConverterFactory (dropped: hosts build their own converter and add Toolkit's extensions).

  • Test: guard throws with a clear message when the class is missing (simulate via a wrapper). Implement, add dep, green, commit.

Task A2: Alert / Tabs / Popover extensions (render via overridable Twig templates)

Files:

  • Create: src/Toolkit/src/Markdown/Extension/{Alert,Tabs,Popover}/{Node,Parser,Renderer,Extension}.php
  • Create: src/Toolkit/templates/markdown/{alert,tabs,popover}.html.twig (semantic HTML + Toolkit classes, not Shadcn)
  • Test: src/Toolkit/tests/Markdown/{Alert,Tabs,Popover}Test.php

Interfaces:

  • Produces: AlertExtension, TabsExtension, PopoverExtension (ExtensionInterface). Each renderer renders through its Twig template (@UXToolkit/markdown/*.html.twig), so a host overrides the template to restyle. Constructor guards via Assert::commonMarkAvailable().
  • Consumes: Twig (Environment), already a Toolkit dep.

What: port node/parser/renderer trios from ux.symfony.com/src/Service/CommonMark/Extension/{Alert,Tabs,Popover}; the site's renderers already delegate to Twig, keep that but ship the templates in Toolkit under a @UXToolkit namespace. Syntax unchanged (> [!NOTE], ::: tabs / :: tab).

  • Per extension: snapshot test (directive -> HTML from the default template), red, port, green, commit.

Task A3: FencedCode renderer + {"preview": true} inline form

Files:

  • Create: src/Toolkit/src/Markdown/Extension/FencedCode/FencedCodeRenderer.php
  • Test: src/Toolkit/tests/Markdown/FencedCodeTest.php

Interfaces:

  • Produces: FencedCodeRenderer reading the fence info-string JSON tail; on {"preview": true} it builds Tabs(Preview: CodePreview(code), Code: FencedCode(code)) (A5 nodes). Renders via @UXToolkit/markdown/fenced_code.html.twig.

  • Test: {"preview": true} fence -> Preview+Code tabs; plain fence -> normal code block. Red, implement, green, commit. (Sequence with A5.)

Task A4: CodePreview node + PreviewUrlGenerator contract

Files:

  • Create: src/Toolkit/src/Markdown/Extension/CodePreview/{CodePreview,CodePreviewParser,CodePreviewRenderer,CodePreviewExtension}.php
  • Create: src/Toolkit/src/Markdown/PreviewUrlGenerator.php (interface)
  • Create: src/Toolkit/templates/markdown/code_preview.html.twig
  • Test: src/Toolkit/tests/Markdown/CodePreviewTest.php

Interfaces:

  • Produces:
    • interface PreviewUrlGenerator { public function generate(string $code, array $options): ?string; } (host-implemented).
    • final class CodePreview extends AbstractBlock carrying string $code, array $options.
    • CodePreviewExtension(?PreviewUrlGenerator $urlGenerator = null). Renderer: URL present -> iframe via template; null -> static highlighted block.

What: port .../ToolkitPreview/*, decouple from ToolkitKitId/UriSigner/routes (all behind PreviewUrlGenerator). Node has no kit knowledge.

  • Test: stub generator -> iframe HTML; null generator -> static block. Red, implement, green, commit.

Task A5: ::: example <Name> leaf directive (recipe-aware)

Files:

  • Create: src/Toolkit/src/Markdown/Extension/Example/{ExampleParser,ExampleExtension}.php
  • Test: src/Toolkit/tests/Markdown/ExampleDirectiveTest.php

Interfaces:

  • Produces: ExampleExtension(Recipe $recipe), instantiated per render. Parser matches a single line ::: example <Name> {json} (no closing :::), reads <recipe>/examples/<Name>.html.twig, builds Tabs(CodePreview($code,$opts), FencedCode($code)) directly in the AST in closeBlock() (reuse A2/A3/A4 nodes; no re-serialize).

  • Consumes: Recipe, A2/A3/A4 nodes.

  • Test against a fixture recipe: builds Tabs with the example code; code with ``` does not break; missing example throws. Red, implement, green, commit.

Task A6: Toolkit frontend assets (no-build Stimulus controllers + base CSS)

No build step (per decision): plain ES modules a host serves through AssetMapper. Consistent with the kit.js no-build approach.

Files:

  • Create: src/Toolkit/assets/controllers/{tabs,popover,clipboard}_controller.js (plain ESM, import { Controller } from '@hotwired/stimulus')
  • Create: src/Toolkit/assets/styles/toolkit.css (base stylesheet the default templates reference, via CSS custom properties for theming)
  • Modify: @UXToolkit/markdown/{tabs,popover,code_preview}.html.twig to wire the controllers (data-controller / -target / data-action, identifiers toolkit-tabs / toolkit-popover / toolkit-clipboard); update the A2 exact-HTML tests

Interfaces:

  • Produces: three Stimulus controllers (tab switching, popover toggle, copy-to-clipboard). No package.json / tsdown / dist. The host (PR B) adds src/Toolkit/assets/controllers to its Stimulus/AssetMapper paths and imports the CSS.

What: the portable interactivity for the default rendering. PHP tests already assert the wired HTML structure; browser tests can follow.

  • Wire templates + controllers; run PHP --filter 'Markdown|Doc', oxfmt/oxlint, twig-cs-fixer green; commit.

Task A7: Manifest color / icon

Files:

  • Modify: src/Toolkit/schema-kit-v1.json, src/Toolkit/src/Kit/KitManifest.php, src/Toolkit/src/Kit/Kit.php (if it re-exposes)
  • Test: src/Toolkit/tests/Unit/Kit/KitManifestTest.php

Interfaces:

  • Produces: KitManifest::$color/$icon (?string), parsed optionally. (No theme field: theme.css is loaded via the kit.js -> kit.css import chain, so the manifest never needs its path.)

  • Test: manifest with/without the fields parses. Red, implement, green, commit.

Task A8: RecipeDocRenderer (renderAsHtml / renderAsMarkdown) + relocated scaffolding template

Files:

  • Create: src/Toolkit/src/Doc/RecipeDocRenderer.php
  • Create: src/Toolkit/templates/doc/recipe.md.twig (the relocated _base_component.md.twig, de-coupled from the site)
  • Test: src/Toolkit/tests/Functional/RecipeDocRendererTest.php (snapshot)

Interfaces:

  • Produces: RecipeDocRenderer::renderAsHtml(Kit, Recipe, PreviewUrlGenerator): string (Toolkit assembles the CommonMark converter from its own extensions + ExampleExtension($recipe) + the host's PreviewUrlGenerator; guarded by Assert::commonMarkAvailable()) and renderAsMarkdown(Kit, Recipe): string (portable Markdown, ::: example resolved to fences, no league/commonmark needed). Both render the @UXToolkit/doc/recipe.md.twig template (with a format flag) as the intermediate Markdown.
  • Consumes: Kit, Recipe, Installer/InstallationReport (files + deps), ComponentDocParser/ComponentDoc, the recipe doc.md (A9), Twig.

What: move ux.symfony.com/templates/toolkit/docs/_base_component.md.twig into Toolkit as @UXToolkit/doc/recipe.md.twig, keeping its section order (title/description, Demo, Installation Automatic/Manual, narrative, API-reference tables). Replace the site-only bits: toolkit_code_demo/usage/example(...) -> ::: example Demo / ::: example <Name> directives; the usage/examples blocks -> inject {{ doc }} (the recipe's doc.md); is_llm branch -> a format ('html' | 'markdown') flag. In markdown mode the template resolves ::: example to a plain ```twig fence (final output, not re-parsed; long-fence-escape code containing backticks).

Note: the site's _base_component.md.twig and every <kit>/<recipe>.md.twig are deleted in PR B (B4); their prose lives in each recipe's doc.md.

  • Snapshot test: markdown for a fixture recipe (llm true + false). Red, implement, green, update snapshots, commit.

Task A9: doc.md loading + linter check

Files:

  • Modify: src/Toolkit/src/Recipe/Recipe.php (?string $doc), src/Toolkit/src/Recipe/RecipeSynchronizer.php (read doc.md)
  • Create: src/Toolkit/src/Kit/Lint/Checker/DocChecker.php; register in KitLinter.php
  • Test: src/Toolkit/tests/Unit/Recipe/RecipeSynchronizerTest.php, .../Lint/DocCheckerTest.php

Interfaces:

  • Produces: Recipe::$doc; DocChecker (flags missing doc.md). Follow src/Toolkit/src/Kit/Lint/Checker/StimulusControllerChecker.php.

  • Tests: recipe loads doc.md; linter flags a missing one. Red, implement, green, commit.

Task A10: One-shot .md.twig -> doc.md conversion script

Files:

  • Create: src/Toolkit/bin/convert-mdtwig-to-doc.php
  • Test: src/Toolkit/tests/Unit/ConvertMdTwigTest.php

Interfaces:

  • Produces: convertMdTwig(string $mdTwig): string: strip {% extends/block/endblock %}; toolkit_code_example(kit, name, 'X', {opts}) -> ::: example X {json}; toolkit_code_demo(...) -> ::: example Demo {json}; toolkit_code_usage(...) -> ::: example Usage; {height:'300px'} -> {"height":"300px"}; keep prose/headings/alerts.

  • Unit-test the transform on button/alert/post-link snippets. Red, implement, green, commit.

Task A11: Migrate common, shadcn, flowbite-4

Files:

  • Create per recipe: src/Toolkit/kits/<kit>/<recipe>/doc.md
  • Create per kit: src/Toolkit/kits/<kit>/{theme.css,kit.css,kit.js,icon.svg} (kit.css = @import "tailwindcss"; @source "."; @import "./theme.css";; kit.js = import './kit.css'; + boots Stimulus + registers the kit's controllers via relative imports)
  • Modify per kit: src/Toolkit/kits/<kit>/manifest.json (color/icon); snapshots

What: run A10 over the site templates; extract theme.css from the site assets/styles/toolkit-<kit>.css @theme{} + INSTALL.md; copy icon.svg from ux.symfony.com/assets/icons/toolkit/<kit>.svg; color from ToolkitKitId::color(). Hand-review each doc.md.

  • Per kit: add files, update snapshots, review diffs, commit.

Task A12: PR A wrap-up

  • php-cs-fixer, twig-cs-fixer, oxfmt/oxlint, full phpunit + Vitest green; dist/ built.
  • src/Toolkit/CHANGELOG.md; open PR against symfony/ux (maintained branch; push upstream).

PR B: symfony/ux.symfony.com (generic renderer + dynamic wiring)

Depends on PR A available in the vendored symfony/ux-toolkit.

Task B1: Convert docs with Toolkit's extensions; delete site copies

Files:

  • Modify/Create: a dedicated toolkit-doc converter builder that, per render, assembles a CommonMarkConverter with Toolkit's Alert/Tabs/Popover/FencedCode/CodePreview extensions + ExampleExtension($recipe) + the site SignedPreviewUrlGenerator (B2)
  • Delete: src/Service/CommonMark/Extension/{Alert,Tabs,Popover,ToolkitPreview}/**, its FencedCode copy
  • Optional: override @UXToolkit/markdown/*.html.twig in templates/bundles/ to keep the Shadcn look (else use Toolkit defaults + Toolkit CSS)
  • Test: blog/general markdown still renders; a toolkit-doc string renders

Interfaces:

  • Consumes: RecipeDocRenderer::render() markdown (B4), Toolkit extensions, ExampleExtension (needs the Recipe).

  • Test: a doc-markdown with ::: example, ::: tabs, > [!NOTE] renders to HTML. Delete copies, commit.

Task B2: Site SignedPreviewUrlGenerator

Files:

  • Create: src/Service/Toolkit/SignedPreviewUrlGenerator.php implements Symfony\UX\Toolkit\Markdown\PreviewUrlGenerator
  • Modify: config/services.yaml

Interfaces:

  • Consumes: UriSigner, UrlGeneratorInterface (app_toolkit_component_preview), current kit id (via render context).

  • Produces: generate($code,$options): string signed URL (port ToolkitPreviewRenderer logic).

  • Test: URL is signed + points at the preview route. Commit.

Task B3: KitDiscovery; drop ToolkitKitId

Files:

  • Create: src/Service/Toolkit/KitDiscovery.php (scan vendor/symfony/ux-toolkit/kits/*/manifest.json)

  • Delete: src/Enum/ToolkitKitId.php; update all usages (ToolkitService, ToolkitRuntime::kitColor, ComponentsController, GenerateLlmsFilesCommand, listing templates) to string ids + manifest color

  • Test: tests/.../KitDiscoveryTest.php

  • Test discovery returns the three kits + colors; replace call sites; suite green; commit.

Task B4: Render docs via RecipeDocRenderer; delete .md.twig

Files:

  • Modify: src/Service/Toolkit/ToolkitService.php (renderComponentDoc -> RecipeDocRenderer::render() then convert with the B1 converter; .md LLM -> render(llm: true))

  • Delete: templates/toolkit/docs/**, src/Twig/Extension/{ToolkitExtension,ToolkitRuntime}.php toolkit_code_*

  • Test: functional test on a component page + a .md page

  • Functional: /toolkit/shadcn/button renders (docs + preview); .md renders markdown. Delete templates/functions, commit.

Task B5: KitAssetsPass + ImportMapConfigReader decorator (dynamic wiring)

Files:

  • Create: src/DependencyInjection/Compiler/KitAssetsPass.php, src/AssetMapper/KitImportMapConfigReader.php
  • Modify: src/Kernel.php (register pass + GlobResource on vendor/symfony/ux-toolkit/kits/*)
  • Test: tests/.../KitAssetsPassTest.php

Hooks (verified in vendor):

  • Append kit dirs to AssetMapperRepository $paths (makes kit.js/kit.css/controllers servable).

  • Append each kit kit.css to symfonycasts/tailwind input_css (compiled).

  • Decorate ImportMapConfigReader::getEntries() -> parent + ->add(ImportMapEntry::createLocal('toolkit-<kit>', ..., 'kits/<kit>/kit.js', isEntrypoint: true)), one entry per kit. AssetMapper traces kit.js's relative imports (./kit.css + controllers), so no per-controller entry and no ControllersMapGenerator hook.

  • One-time (not per kit): import Toolkit's own markdown assets (@symfony/ux-toolkit tabs/popover/clipboard + CSS from A6).

  • Test: pass mutates the three args + decorator adds entries for a fixture kit. Commit.

Task B6: Delete hand-written per-kit files

Files:

  • Delete: assets/toolkit-<kit>.js, assets/styles/toolkit-<kit>.css, assets/icons/toolkit/<kit>.svg

  • Modify: drop per-kit lines from importmap.php, config/packages/symfonycasts_tailwind.yaml, composer.json tailwind:build

  • Boot, load each kit page, styles + controllers resolve; commit.

Task B7: End-to-end verification

  • pnpm run fmt/lint clean; site suite green.
  • Manual: /toolkit/{common,shadcn,flowbite-4}/<recipe> render docs, API tables, live previews, tabs/alerts.
  • Throwaway-kit: drop a minimal kit into vendored kits/, cache:clear, confirm end-to-end with zero site edits and no committed generated files. Remove it.
  • Open PR against symfony/ux.symfony.com.

Self-review notes

  • Spec coverage: Layer 1 -> A1-A6; Layer 2 -> A7-A11; Layer 3 -> B1-B6. Migration -> A10-A11.
  • Remarks folded in: (1) league/commonmark dev-only + Assert::commonMarkAvailable() guard; (2) no MarkdownConverterFactory; RecipeDocRenderer exposes renderAsHtml (Toolkit assembles the converter from its extensions + the host PreviewUrlGenerator) and renderAsMarkdown (portable Markdown); (3) Toolkit ships overridable Twig templates + base CSS + no-build Stimulus controllers (A2/A4/A6), the site optionally overrides templates to keep Shadcn.
  • Sequencing: A4 before/with A3 and A5; A6 pairs with A2/A4 templates. B depends on A shipped.
  • Open questions: preview transport (signed GET vs POST) decided in A4/B2; Tailwind vendored-input @source validated early in A11/B5.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions