Skip to content

Commit c5b635b

Browse files
committed
feature #3464 [Toolkit][Shadcn] Add radio-group, collapsible, typography and toggle-group recipes (Amoifr)
This PR was squashed before being merged into the 3.x branch. Discussion ---------- [Toolkit][Shadcn] Add radio-group, collapsible, typography and toggle-group recipes | Q | A | -------------- | --- | Bug fix? | no | New feature? | yes | Deprecations? | no | Documentation? | no | Issues | Part of #3233 | License | MIT Hi 👋 This PR adds four recipes from the unchecked list in #3233: - **radio-group** — form control mirroring the Checkbox pattern (native `<input type=radio>` + `accent-primary`), with a `RadioGroup:Item` sub-component. - **collapsible** — native `<details>` / `<summary>` based, no JS. - **typography** — H1–H4, P, Blockquote, List, InlineCode sub-components. - **toggle-group** — container for Toggle items, with `variant` / `size` propagated to items through outer-context variables (`_toggle_group_variant` / `_toggle_group_size`). Each recipe has a `Usage.html.twig` and a `Demo.html.twig` example and passing snapshot tests. I initially planned to send these as separate PRs and asked for a sanity check on #3233 — happy to split them back up if that is easier to review. Each recipe is a single commit so rebasing into 4 PRs is straightforward. ### Note on sub-component context propagation While working on `radio-group`, I noticed that variables set in a parent component (`{% set _radio_group_name = ... %}`) do not propagate to **self-closing** children, because `<twig:RadioGroup:Item .../>` compiles to a `component()` function call that does not carry outer context. It works fine for non-self-closing children (e.g. `<twig:ToggleGroup:Item>...</twig:ToggleGroup:Item>`, which uses the `{% component %}` tag — this pattern is used in `toggle-group` to inherit `variant`/`size`). For `radio-group`, `Item` is always self-closing, so I kept `name` as an explicit prop on each `Item`. Curious if you would prefer a different pattern here (e.g. `outerScope`, forcing a block body, a helper). Thanks a lot for the Toolkit work! 🙏 Commits ------- 57f8a1d Minor fixes f30d83a [Toolkit][Shadcn] Add toggle-group recipe 6559742 [Toolkit][Shadcn] Add typography recipe 4450e50 [Toolkit][Shadcn] Add collapsible recipe 0ccd0db [Toolkit][Shadcn] Add radio-group recipe
2 parents c406c5e + 57f8a1d commit c5b635b

File tree

94 files changed

+2398
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+2398
-0
lines changed

src/Toolkit/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
- Minimum required Symfony version is now 7.4
1111
- Minimum required PHP version is now 8.4
12+
- [Shadcn] Add `radio-group` recipe
13+
- [Shadcn] Add `collapsible` recipe
14+
- [Shadcn] Add `typography` recipe
15+
- [Shadcn] Add `toggle-group` recipe
1216

1317
## 2.35
1418

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
export default class extends Controller {
4+
static targets = ['trigger', 'content'];
5+
6+
static values = {
7+
open: { type: Boolean, default: false },
8+
};
9+
10+
connect() {
11+
this.updateState();
12+
}
13+
14+
toggle() {
15+
this.openValue = !this.openValue;
16+
}
17+
18+
openValueChanged() {
19+
this.updateState();
20+
}
21+
22+
updateState() {
23+
const open = this.openValue;
24+
const state = open ? 'open' : 'closed';
25+
26+
this.element.dataset.state = state;
27+
28+
for (const trigger of this.triggerTargets) {
29+
trigger.setAttribute('aria-expanded', String(open));
30+
trigger.dataset.state = state;
31+
}
32+
33+
for (const content of this.contentTargets) {
34+
content.dataset.state = state;
35+
content.setAttribute('aria-hidden', String(!open));
36+
if (open) {
37+
content.removeAttribute('hidden');
38+
} else {
39+
content.setAttribute('hidden', '');
40+
}
41+
}
42+
}
43+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<twig:Card class="mx-auto w-full max-w-sm self-start">
2+
<twig:Card:Content class="p-3">
3+
<twig:Collapsible class="rounded-md data-[state=open]:bg-muted">
4+
<twig:Collapsible:Trigger>
5+
<twig:Button variant="ghost" class="group w-full" {{ ...collapsible_trigger_attrs }}>
6+
Product details
7+
<twig:ux:icon name="lucide:chevron-down" class="ml-auto size-4 transition-transform group-data-[state=open]:rotate-180" />
8+
</twig:Button>
9+
</twig:Collapsible:Trigger>
10+
<twig:Collapsible:Content class="flex flex-col items-start gap-2 p-2.5 pt-0 text-sm">
11+
<div>
12+
This panel can be expanded or collapsed to reveal additional content.
13+
</div>
14+
<twig:Button size="xs">Learn More</twig:Button>
15+
</twig:Collapsible:Content>
16+
</twig:Collapsible>
17+
</twig:Card:Content>
18+
</twig:Card>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<twig:Collapsible class="flex w-[350px] flex-col gap-2 self-start">
2+
<div class="flex items-center justify-between gap-4 px-4">
3+
<h4 class="text-sm font-semibold">Order #4189</h4>
4+
<twig:Collapsible:Trigger>
5+
<twig:Button variant="ghost" size="icon" class="size-8" {{ ...collapsible_trigger_attrs }}>
6+
<twig:ux:icon name="lucide:chevrons-up-down" class="size-4" />
7+
<span class="sr-only">Toggle details</span>
8+
</twig:Button>
9+
</twig:Collapsible:Trigger>
10+
</div>
11+
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
12+
<span class="text-muted-foreground">Status</span>
13+
<span class="font-medium">Shipped</span>
14+
</div>
15+
<twig:Collapsible:Content class="flex flex-col gap-2">
16+
<div class="rounded-md border px-4 py-2 text-sm">
17+
<p class="font-medium">Shipping address</p>
18+
<p class="text-muted-foreground">100 Market St, San Francisco</p>
19+
</div>
20+
<div class="rounded-md border px-4 py-2 text-sm">
21+
<p class="font-medium">Items</p>
22+
<p class="text-muted-foreground">2x Studio Headphones</p>
23+
</div>
24+
</twig:Collapsible:Content>
25+
</twig:Collapsible>
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<twig:Card class="mx-auto w-full max-w-sm">
2+
<twig:Card:Header class="p-2.5">
3+
<div class="flex items-center gap-1.5 text-sm font-medium">
4+
<twig:ux:icon name="lucide:panel-left" class="size-4" />
5+
Explorer
6+
</div>
7+
</twig:Card:Header>
8+
<twig:Card:Content class="p-2.5 pt-0">
9+
<div class="flex flex-col gap-0.5">
10+
{# Folder: components #}
11+
<twig:Collapsible>
12+
<twig:Collapsible:Trigger>
13+
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
14+
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
15+
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
16+
components
17+
</twig:Button>
18+
</twig:Collapsible:Trigger>
19+
<twig:Collapsible:Content>
20+
<div class="ml-4 flex flex-col gap-0.5">
21+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
22+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
23+
button.tsx
24+
</twig:Button>
25+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
26+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
27+
card.tsx
28+
</twig:Button>
29+
</div>
30+
</twig:Collapsible:Content>
31+
</twig:Collapsible>
32+
33+
{# Folder: lib #}
34+
<twig:Collapsible>
35+
<twig:Collapsible:Trigger>
36+
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
37+
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
38+
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
39+
lib
40+
</twig:Button>
41+
</twig:Collapsible:Trigger>
42+
<twig:Collapsible:Content>
43+
<div class="ml-4 flex flex-col gap-0.5">
44+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
45+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
46+
utils.ts
47+
</twig:Button>
48+
</div>
49+
</twig:Collapsible:Content>
50+
</twig:Collapsible>
51+
52+
{# Folder: hooks #}
53+
<twig:Collapsible>
54+
<twig:Collapsible:Trigger>
55+
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
56+
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
57+
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
58+
hooks
59+
</twig:Button>
60+
</twig:Collapsible:Trigger>
61+
<twig:Collapsible:Content>
62+
<div class="ml-4 flex flex-col gap-0.5">
63+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
64+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
65+
use-toast.ts
66+
</twig:Button>
67+
</div>
68+
</twig:Collapsible:Content>
69+
</twig:Collapsible>
70+
71+
{# Folder: types #}
72+
<twig:Collapsible>
73+
<twig:Collapsible:Trigger>
74+
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
75+
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
76+
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
77+
types
78+
</twig:Button>
79+
</twig:Collapsible:Trigger>
80+
<twig:Collapsible:Content>
81+
<div class="ml-4 flex flex-col gap-0.5">
82+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
83+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
84+
index.d.ts
85+
</twig:Button>
86+
</div>
87+
</twig:Collapsible:Content>
88+
</twig:Collapsible>
89+
90+
{# Folder: public #}
91+
<twig:Collapsible>
92+
<twig:Collapsible:Trigger>
93+
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
94+
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
95+
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
96+
public
97+
</twig:Button>
98+
</twig:Collapsible:Trigger>
99+
<twig:Collapsible:Content>
100+
<div class="ml-4 flex flex-col gap-0.5">
101+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
102+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
103+
favicon.ico
104+
</twig:Button>
105+
</div>
106+
</twig:Collapsible:Content>
107+
</twig:Collapsible>
108+
109+
{# Root files #}
110+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
111+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
112+
app.tsx
113+
</twig:Button>
114+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
115+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
116+
layout.tsx
117+
</twig:Button>
118+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
119+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
120+
globals.css
121+
</twig:Button>
122+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
123+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
124+
package.json
125+
</twig:Button>
126+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
127+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
128+
tsconfig.json
129+
</twig:Button>
130+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
131+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
132+
README.md
133+
</twig:Button>
134+
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
135+
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
136+
.gitignore
137+
</twig:Button>
138+
</div>
139+
</twig:Card:Content>
140+
</twig:Card>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<twig:Collapsible class="flex w-[350px] flex-col gap-2" dir="rtl">
2+
<div class="flex items-center justify-between gap-4 px-4">
3+
<h4 class="text-sm font-semibold">الطلب #4189</h4>
4+
<twig:Collapsible:Trigger>
5+
<twig:Button variant="ghost" size="icon" class="size-8" {{ ...collapsible_trigger_attrs }}>
6+
<twig:ux:icon name="lucide:chevrons-up-down" class="size-4" />
7+
<span class="sr-only">Toggle details</span>
8+
</twig:Button>
9+
</twig:Collapsible:Trigger>
10+
</div>
11+
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
12+
<span class="text-muted-foreground">الحالة</span>
13+
<span class="font-medium">تم الشحن</span>
14+
</div>
15+
<twig:Collapsible:Content class="flex flex-col gap-2">
16+
<div class="rounded-md border px-4 py-2 text-sm">
17+
<p class="font-medium">عنوان الشحن</p>
18+
<p class="text-muted-foreground">100 Market St, San Francisco</p>
19+
</div>
20+
<div class="rounded-md border px-4 py-2 text-sm">
21+
<p class="font-medium">العناصر</p>
22+
<p class="text-muted-foreground">2x سماعات الاستوديو</p>
23+
</div>
24+
</twig:Collapsible:Content>
25+
</twig:Collapsible>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<twig:Card class="mx-auto w-full max-w-xs">
2+
<twig:Card:Header>
3+
<twig:Card:Title>Radius</twig:Card:Title>
4+
<twig:Card:Description>Set the corner radius of the element.</twig:Card:Description>
5+
</twig:Card:Header>
6+
<twig:Card:Content>
7+
<twig:Collapsible class="flex items-start gap-2">
8+
<div class="w-full space-y-2">
9+
<div class="grid grid-cols-2 gap-2">
10+
<twig:Field>
11+
<twig:Field:Label for="radius-1" class="sr-only">Radius X</twig:Field:Label>
12+
<twig:Input id="radius-1" placeholder="0" value="0" />
13+
</twig:Field>
14+
<twig:Field>
15+
<twig:Field:Label for="radius-2" class="sr-only">Radius Y</twig:Field:Label>
16+
<twig:Input id="radius-2" placeholder="0" value="0" />
17+
</twig:Field>
18+
</div>
19+
<twig:Collapsible:Content>
20+
<div class="grid grid-cols-2 gap-2">
21+
<twig:Field>
22+
<twig:Field:Label for="radius-3" class="sr-only">Radius X</twig:Field:Label>
23+
<twig:Input id="radius-3" placeholder="0" value="0" />
24+
</twig:Field>
25+
<twig:Field>
26+
<twig:Field:Label for="radius-4" class="sr-only">Radius Y</twig:Field:Label>
27+
<twig:Input id="radius-4" placeholder="0" value="0" />
28+
</twig:Field>
29+
</div>
30+
</twig:Collapsible:Content>
31+
</div>
32+
<twig:Collapsible:Trigger>
33+
<twig:Button variant="outline" size="icon" class="group" {{ ...collapsible_trigger_attrs }}>
34+
<twig:ux:icon name="lucide:maximize" class="size-4 group-data-[state=open]:hidden" />
35+
<twig:ux:icon name="lucide:minimize" class="size-4 hidden group-data-[state=open]:block" />
36+
</twig:Button>
37+
</twig:Collapsible:Trigger>
38+
</twig:Collapsible>
39+
</twig:Card:Content>
40+
</twig:Card>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<twig:Collapsible>
2+
<twig:Collapsible:Trigger>
3+
<twig:Button variant="ghost" {{ ...collapsible_trigger_attrs }}>Show details</twig:Button>
4+
</twig:Collapsible:Trigger>
5+
<twig:Collapsible:Content>
6+
The content is now visible.
7+
</twig:Collapsible:Content>
8+
</twig:Collapsible>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "../../../schema-kit-recipe-v1.json",
3+
"type": "component",
4+
"name": "Collapsible",
5+
"description": "An interactive component which expands and collapses a section of content.",
6+
"copy-files": {
7+
"assets/": "assets/",
8+
"templates/": "templates/"
9+
},
10+
"dependencies": {
11+
"composer": ["tales-from-a-dev/twig-tailwind-extra:^1.0.0"]
12+
}
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{# @prop open boolean Whether the collapsible is open by default. Defaults to `false` #}
2+
{# @block content The collapsible content, typically a `Collapsible:Trigger` and a `Collapsible:Content` #}
3+
{%- props open = false -%}
4+
{%- set _collapsible_open = open -%}
5+
<div
6+
class="{{ ('group ' ~ attributes.render('class'))|tailwind_merge }}"
7+
{{ attributes.defaults({
8+
'data-slot': 'collapsible',
9+
'data-controller': 'collapsible',
10+
'data-collapsible-open-value': open ? 'true' : 'false',
11+
'data-state': open ? 'open' : 'closed',
12+
}) }}
13+
>
14+
{%- block content %}{% endblock -%}
15+
</div>

0 commit comments

Comments
 (0)