@goodfellow-cms/registry 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,7 @@ Registry blocks follow the same rules as any Goodfellow block (see [AGENTS.md](h
79
79
 
80
80
  - Every block has a `className` prop (`className: classNameField`), applied to its outermost element and combined with `cx()` from `@goodfellow-cms/react`, last.
81
81
  - Use the theme's classes (`bg-primary`, `text-muted-foreground`, `rounded-lg`), which shadcn's components use too, so blocks follow the site's colors and fonts.
82
+ - **Name the block's parts**, so a site's theme can restyle them: `const part = useParts()` at the start of `render`, then `className={part("gf-my-block-title", "text-xl font-bold", className)}` on each part (the block's own classes, then anything an editor chose, `className` last), and declare them with what owners call them: `export default withParts(block, { "gf-my-block-title": "Title" })`. Start every name with `gf-` and the item's name, so they never clash with another block's, and never rename or remove one once it's published, since themes refer to them. Client Components get their parts' classes as plain props from the block, as `shadcn-faq` does.
82
83
  - Render links and images with `SiteLink` and `SiteImage` from `@goodfellow-cms/react`, and image fields with `mediaField()`. To style a link as a button, give `SiteLink` the classes from shadcn's `buttonVariants()`.
83
84
  - Labels and options are for non-technical people: "Space above and below", not "padding-y".
84
85
  - `render` uses no hooks besides `useSite()`. Interactive parts go in a file starting with `"use client"`, which gets plain values and content (`children`, rich text) only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/registry",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Goodfellow's block registry: blocks built with shadcn/ui, which sites install from the admin panel.",
5
5
  "keywords": [
6
6
  "goodfellow",
@@ -28,8 +28,8 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "devDependencies": {
31
- "@goodfellow-cms/core": "0.5.0",
32
- "@goodfellow-cms/react": "0.5.0",
31
+ "@goodfellow-cms/core": "0.6.0",
32
+ "@goodfellow-cms/react": "0.6.0",
33
33
  "@puckeditor/core": "0.23.0",
34
34
  "@types/react": "^19.2.0",
35
35
  "@types/react-dom": "^19.2.0",
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "path": "blocks/installed/custom-html/block.tsx",
10
10
  "type": "registry:block",
11
- "content": "import { classNameField, sanitizeHtml } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { HtmlPreview } from \"./html-preview\";\nimport { HtmlWithScripts } from \"./html-scripts\";\n\nexport interface CustomHtmlProps {\n html: string;\n sanitize: boolean;\n className: string;\n}\n\n/**\n * HTML written by hand, such as a widget's embed code. It's made safe by\n * default: scripts, styles, frames, forms and inline styles are left out. Used\n * as it's written, it runs on the site, but in the editor it's only shown,\n * inside a sandboxed frame, where it can't reach the admin panel, and its\n * scripts don't run. On the site they run whenever its page is shown, also\n * after moving to it on a Next.js site.\n */\nconst CustomHtml: ComponentConfig<CustomHtmlProps> = {\n label: \"Custom HTML\",\n fields: {\n html: { type: \"textarea\", label: \"HTML\" },\n sanitize: {\n type: \"radio\",\n label: \"Make it safe\",\n options: [\n { label: \"Yes: leave out scripts, styles and frames\", value: true },\n { label: \"No: use it exactly as written\", value: false },\n ],\n },\n className: classNameField,\n },\n defaultProps: { html: \"<p>Your HTML goes here.</p>\", sanitize: true, className: \"\" },\n render: ({ html, sanitize, className, puck }) => {\n if (!sanitize && puck.isEditing) return <HtmlPreview html={html} className={className} />;\n // Only HTML with scripts needs the browser's help, which loads React on the page.\n if (!sanitize && /<script[\\s>]/i.test(html)) return <HtmlWithScripts html={html} className={className} />;\n return (\n <div\n className={className || undefined}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: sanitized unless the editor chose to use it as written\n dangerouslySetInnerHTML={{ __html: sanitize ? sanitizeHtml(html) : html }}\n />\n );\n },\n};\n\nexport default CustomHtml;\n"
11
+ "content": "import { classNameField, sanitizeHtml, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { HtmlPreview } from \"./html-preview\";\nimport { HtmlWithScripts } from \"./html-scripts\";\n\nexport interface CustomHtmlProps {\n html: string;\n sanitize: boolean;\n className: string;\n}\n\n/**\n * HTML written by hand, such as a widget's embed code. It's made safe by\n * default: scripts, styles, frames, forms and inline styles are left out. Used\n * as it's written, it runs on the site, but in the editor it's only shown,\n * inside a sandboxed frame, where it can't reach the admin panel, and its\n * scripts don't run. On the site they run whenever its page is shown, also\n * after moving to it on a Next.js site.\n */\nconst CustomHtml: ComponentConfig<CustomHtmlProps> = withParts(\n {\n label: \"Custom HTML\",\n fields: {\n html: { type: \"textarea\", label: \"HTML\" },\n sanitize: {\n type: \"radio\",\n label: \"Make it safe\",\n options: [\n { label: \"Yes: leave out scripts, styles and frames\", value: true },\n { label: \"No: use it exactly as written\", value: false },\n ],\n },\n className: classNameField,\n },\n defaultProps: { html: \"<p>Your HTML goes here.</p>\", sanitize: true, className: \"\" },\n render: ({ html, sanitize, className: chosen, puck }) => {\n const part = useParts();\n const className = part(\"gf-custom-html\", undefined, chosen);\n if (!sanitize && puck.isEditing) return <HtmlPreview html={html} className={className} />;\n // Only HTML with scripts needs the browser's help, which loads React on the page.\n if (!sanitize && /<script[\\s>]/i.test(html)) return <HtmlWithScripts html={html} className={className} />;\n return (\n <div\n className={className}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: sanitized unless the editor chose to use it as written\n dangerouslySetInnerHTML={{ __html: sanitize ? sanitizeHtml(html) : html }}\n />\n );\n },\n },\n {\n \"gf-custom-html\": \"Custom HTML\",\n },\n);\n\nexport default CustomHtml;\n"
12
12
  },
13
13
  {
14
14
  "path": "blocks/installed/custom-html/html-preview.tsx",
@@ -24,7 +24,7 @@
24
24
  "meta": {
25
25
  "goodfellow": {
26
26
  "category": "Advanced",
27
- "version": "0.5.0"
27
+ "version": "0.6.0"
28
28
  }
29
29
  },
30
30
  "dependencies": []
package/r/registry.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "goodfellow": {
22
22
  "category": "Page sections",
23
23
  "recommended": true,
24
- "version": "0.5.0"
24
+ "version": "0.6.0"
25
25
  }
26
26
  },
27
27
  "dependencies": []
@@ -45,7 +45,7 @@
45
45
  "goodfellow": {
46
46
  "category": "Page sections",
47
47
  "recommended": true,
48
- "version": "0.5.0"
48
+ "version": "0.6.0"
49
49
  }
50
50
  },
51
51
  "dependencies": []
@@ -69,7 +69,7 @@
69
69
  "goodfellow": {
70
70
  "category": "Page sections",
71
71
  "recommended": true,
72
- "version": "0.5.0"
72
+ "version": "0.6.0"
73
73
  }
74
74
  },
75
75
  "dependencies": []
@@ -92,7 +92,7 @@
92
92
  "goodfellow": {
93
93
  "category": "Page sections",
94
94
  "recommended": true,
95
- "version": "0.5.0"
95
+ "version": "0.6.0"
96
96
  }
97
97
  },
98
98
  "dependencies": []
@@ -119,7 +119,7 @@
119
119
  "meta": {
120
120
  "goodfellow": {
121
121
  "category": "Page sections",
122
- "version": "0.5.0"
122
+ "version": "0.6.0"
123
123
  }
124
124
  }
125
125
  },
@@ -144,7 +144,7 @@
144
144
  "goodfellow": {
145
145
  "category": "Page sections",
146
146
  "recommended": true,
147
- "version": "0.5.0"
147
+ "version": "0.6.0"
148
148
  }
149
149
  }
150
150
  },
@@ -170,7 +170,7 @@
170
170
  "meta": {
171
171
  "goodfellow": {
172
172
  "category": "Advanced",
173
- "version": "0.5.0"
173
+ "version": "0.6.0"
174
174
  }
175
175
  },
176
176
  "dependencies": []
@@ -197,7 +197,7 @@
197
197
  "goodfellow": {
198
198
  "category": "Interactive",
199
199
  "recommended": true,
200
- "version": "0.5.0"
200
+ "version": "0.6.0"
201
201
  }
202
202
  },
203
203
  "dependencies": []
@@ -224,7 +224,7 @@
224
224
  "goodfellow": {
225
225
  "category": "Interactive",
226
226
  "recommended": true,
227
- "version": "0.5.0"
227
+ "version": "0.6.0"
228
228
  }
229
229
  },
230
230
  "dependencies": []
@@ -250,7 +250,7 @@
250
250
  "meta": {
251
251
  "goodfellow": {
252
252
  "category": "Interactive",
253
- "version": "0.5.0"
253
+ "version": "0.6.0"
254
254
  }
255
255
  },
256
256
  "dependencies": []
@@ -12,14 +12,14 @@
12
12
  {
13
13
  "path": "blocks/installed/shadcn-call-to-action/block.tsx",
14
14
  "type": "registry:block",
15
- "content": "import { classNameField, cx, SiteLink } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\n\ntype Style = \"highlight\" | \"subtle\";\n\nexport interface CallToActionProps {\n title: string;\n text: string;\n buttonLabel: string;\n buttonLink: string;\n style: Style;\n className: string;\n}\n\n/** A highlighted box asking visitors to do something, with a button. */\nconst CallToAction: ComponentConfig<CallToActionProps> = {\n label: \"Call to action\",\n fields: {\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"textarea\", label: \"Text\", contentEditable: true },\n buttonLabel: { type: \"text\", label: \"Button\" },\n buttonLink: { type: \"text\", label: \"Button links to\" },\n style: {\n type: \"radio\",\n label: \"Style\",\n options: [\n { label: \"In the main color\", value: \"highlight\" },\n { label: \"Subtle\", value: \"subtle\" },\n ],\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Ready to get started?\",\n text: \"Tell visitors what to do next.\",\n buttonLabel: \"Get in touch\",\n buttonLink: \"/contact\",\n style: \"highlight\",\n className: \"\",\n },\n render: ({ title, text, buttonLabel, buttonLink, style, className }) => {\n const highlight = style === \"highlight\";\n return (\n <section className={cx(\"mx-auto w-full max-w-5xl px-4 py-12\", className)}>\n <Card className={cx(\"border-0\", highlight ? \"bg-primary text-primary-foreground\" : \"bg-muted\")}>\n <CardContent className=\"flex flex-col items-start gap-6 p-8 md:flex-row md:items-center md:justify-between md:p-12\">\n <div className=\"flex flex-col gap-2\">\n <h2 className=\"text-2xl font-bold tracking-tight md:text-3xl\">{title}</h2>\n {text && <p className={cx(\"text-lg\", highlight ? \"opacity-90\" : \"text-muted-foreground\")}>{text}</p>}\n </div>\n {buttonLabel && (\n <SiteLink\n href={buttonLink || \"/\"}\n className={buttonVariants({ size: \"lg\", variant: highlight ? \"secondary\" : \"default\" })}\n >\n {buttonLabel}\n </SiteLink>\n )}\n </CardContent>\n </Card>\n </section>\n );\n },\n};\n\nexport default CallToAction;\n"
15
+ "content": "import { classNameField, SiteLink, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\n\ntype Style = \"highlight\" | \"subtle\";\n\nexport interface CallToActionProps {\n title: string;\n text: string;\n buttonLabel: string;\n buttonLink: string;\n style: Style;\n className: string;\n}\n\n/** A highlighted box asking visitors to do something, with a button. */\nconst CallToAction: ComponentConfig<CallToActionProps> = withParts(\n {\n label: \"Call to action\",\n fields: {\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"textarea\", label: \"Text\", contentEditable: true },\n buttonLabel: { type: \"text\", label: \"Button\" },\n buttonLink: { type: \"text\", label: \"Button links to\" },\n style: {\n type: \"radio\",\n label: \"Style\",\n options: [\n { label: \"In the main color\", value: \"highlight\" },\n { label: \"Subtle\", value: \"subtle\" },\n ],\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Ready to get started?\",\n text: \"Tell visitors what to do next.\",\n buttonLabel: \"Get in touch\",\n buttonLink: \"/contact\",\n style: \"highlight\",\n className: \"\",\n },\n render: ({ title, text, buttonLabel, buttonLink, style, className }) => {\n const part = useParts();\n const highlight = style === \"highlight\";\n return (\n <section className={part(\"gf-shadcn-call-to-action\", \"mx-auto w-full max-w-5xl px-4 py-12\", className)}>\n <Card\n className={part(\"gf-shadcn-call-to-action-box\", [\n \"border-0\",\n highlight ? \"bg-primary text-primary-foreground\" : \"bg-muted\",\n ])}\n >\n <CardContent className=\"flex flex-col items-start gap-6 p-8 md:flex-row md:items-center md:justify-between md:p-12\">\n <div className=\"flex flex-col gap-2\">\n <h2\n className={part(\"gf-shadcn-call-to-action-heading\", \"text-2xl font-bold tracking-tight md:text-3xl\")}\n >\n {title}\n </h2>\n {text && (\n <p\n className={part(\"gf-shadcn-call-to-action-text\", [\n \"text-lg\",\n highlight ? \"opacity-90\" : \"text-muted-foreground\",\n ])}\n >\n {text}\n </p>\n )}\n </div>\n {buttonLabel && (\n <SiteLink\n href={buttonLink || \"/\"}\n className={part(\n \"gf-shadcn-call-to-action-button\",\n buttonVariants({ size: \"lg\", variant: highlight ? \"secondary\" : \"default\" }),\n )}\n >\n {buttonLabel}\n </SiteLink>\n )}\n </CardContent>\n </Card>\n </section>\n );\n },\n },\n {\n \"gf-shadcn-call-to-action\": \"Call to action\",\n \"gf-shadcn-call-to-action-box\": \"Box\",\n \"gf-shadcn-call-to-action-heading\": \"Heading\",\n \"gf-shadcn-call-to-action-text\": \"Text\",\n \"gf-shadcn-call-to-action-button\": \"Button\",\n },\n);\n\nexport default CallToAction;\n"
16
16
  }
17
17
  ],
18
18
  "meta": {
19
19
  "goodfellow": {
20
20
  "category": "Page sections",
21
21
  "recommended": true,
22
- "version": "0.5.0"
22
+ "version": "0.6.0"
23
23
  }
24
24
  },
25
25
  "dependencies": []
@@ -12,14 +12,14 @@
12
12
  {
13
13
  "path": "blocks/installed/shadcn-cards/block.tsx",
14
14
  "type": "registry:block",
15
- "content": "import { classNameField, cx, mediaField, SiteImage, SiteLink } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\n\ntype Columns = \"2\" | \"3\" | \"4\";\n\ninterface CardItem {\n title: string;\n text: string;\n image: string;\n imageAlt: string;\n linkLabel: string;\n link: string;\n}\n\nexport interface CardsProps {\n title: string;\n intro: string;\n columns: Columns;\n cards: CardItem[];\n className: string;\n}\n\nconst columnClasses: Record<Columns, string> = {\n \"2\": \"sm:grid-cols-2\",\n \"3\": \"sm:grid-cols-2 lg:grid-cols-3\",\n \"4\": \"sm:grid-cols-2 lg:grid-cols-4\",\n};\n\n/** A grid of cards, each with a picture, a heading, text and a link. */\nconst Cards: ComponentConfig<CardsProps> = {\n label: \"Cards\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n intro: { type: \"textarea\", label: \"Introduction\" },\n columns: {\n type: \"radio\",\n label: \"Cards in a row\",\n options: [\n { label: \"2\", value: \"2\" },\n { label: \"3\", value: \"3\" },\n { label: \"4\", value: \"4\" },\n ],\n },\n cards: {\n type: \"array\",\n label: \"Cards\",\n arrayFields: {\n title: { type: \"text\", label: \"Heading\" },\n text: { type: \"textarea\", label: \"Text\" },\n image: mediaField(\"Picture\"),\n imageAlt: { type: \"text\", label: \"Description of the picture for screen readers\" },\n linkLabel: { type: \"text\", label: \"Link text\" },\n link: { type: \"text\", label: \"Links to\" },\n },\n defaultItemProps: {\n title: \"A card\",\n text: \"Say something about it.\",\n image: \"\",\n imageAlt: \"\",\n linkLabel: \"\",\n link: \"\",\n },\n getItemSummary: (card) => card.title || \"Card\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"\",\n intro: \"\",\n columns: \"3\",\n cards: [\n { title: \"First\", text: \"Say something about it.\", image: \"\", imageAlt: \"\", linkLabel: \"\", link: \"\" },\n { title: \"Second\", text: \"Say something about it.\", image: \"\", imageAlt: \"\", linkLabel: \"\", link: \"\" },\n { title: \"Third\", text: \"Say something about it.\", image: \"\", imageAlt: \"\", linkLabel: \"\", link: \"\" },\n ],\n className: \"\",\n },\n render: ({ title, intro, columns, cards, className }) => (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {(title || intro) && (\n <div className=\"mb-8 flex max-w-3xl flex-col gap-3\">\n {title && <h2 className=\"text-3xl font-bold tracking-tight\">{title}</h2>}\n {intro && <p className=\"text-lg text-muted-foreground\">{intro}</p>}\n </div>\n )}\n <div className={cx(\"grid gap-6\", columnClasses[columns])}>\n {cards.map((card, index) => (\n // Cards have no ids of their own, and their order is all that tells them apart.\n // biome-ignore lint/suspicious/noArrayIndexKey: see above\n <Card key={index} className=\"overflow-hidden pt-0\">\n {card.image ? (\n <SiteImage src={card.image} alt={card.imageAlt} className=\"aspect-video w-full object-cover\" />\n ) : (\n <div className=\"h-0\" />\n )}\n <CardHeader>\n <CardTitle className=\"text-xl\">{card.title}</CardTitle>\n {card.text && <CardDescription className=\"text-base\">{card.text}</CardDescription>}\n </CardHeader>\n <CardContent className=\"flex-1\" />\n {card.linkLabel && (\n <CardFooter>\n <SiteLink href={card.link || \"/\"} className={buttonVariants({ variant: \"outline\" })}>\n {card.linkLabel}\n </SiteLink>\n </CardFooter>\n )}\n </Card>\n ))}\n </div>\n </section>\n ),\n};\n\nexport default Cards;\n"
15
+ "content": "import { classNameField, mediaField, SiteImage, SiteLink, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\n\ntype Columns = \"2\" | \"3\" | \"4\";\n\ninterface CardItem {\n title: string;\n text: string;\n image: string;\n imageAlt: string;\n linkLabel: string;\n link: string;\n}\n\nexport interface CardsProps {\n title: string;\n intro: string;\n columns: Columns;\n cards: CardItem[];\n className: string;\n}\n\nconst columnClasses: Record<Columns, string> = {\n \"2\": \"sm:grid-cols-2\",\n \"3\": \"sm:grid-cols-2 lg:grid-cols-3\",\n \"4\": \"sm:grid-cols-2 lg:grid-cols-4\",\n};\n\n/** A grid of cards, each with a picture, a heading, text and a link. */\nconst Cards: ComponentConfig<CardsProps> = withParts(\n {\n label: \"Cards\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n intro: { type: \"textarea\", label: \"Introduction\" },\n columns: {\n type: \"radio\",\n label: \"Cards in a row\",\n options: [\n { label: \"2\", value: \"2\" },\n { label: \"3\", value: \"3\" },\n { label: \"4\", value: \"4\" },\n ],\n },\n cards: {\n type: \"array\",\n label: \"Cards\",\n arrayFields: {\n title: { type: \"text\", label: \"Heading\" },\n text: { type: \"textarea\", label: \"Text\" },\n image: mediaField(\"Picture\"),\n imageAlt: { type: \"text\", label: \"Description of the picture for screen readers\" },\n linkLabel: { type: \"text\", label: \"Link text\" },\n link: { type: \"text\", label: \"Links to\" },\n },\n defaultItemProps: {\n title: \"A card\",\n text: \"Say something about it.\",\n image: \"\",\n imageAlt: \"\",\n linkLabel: \"\",\n link: \"\",\n },\n getItemSummary: (card) => card.title || \"Card\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"\",\n intro: \"\",\n columns: \"3\",\n cards: [\n { title: \"First\", text: \"Say something about it.\", image: \"\", imageAlt: \"\", linkLabel: \"\", link: \"\" },\n { title: \"Second\", text: \"Say something about it.\", image: \"\", imageAlt: \"\", linkLabel: \"\", link: \"\" },\n { title: \"Third\", text: \"Say something about it.\", image: \"\", imageAlt: \"\", linkLabel: \"\", link: \"\" },\n ],\n className: \"\",\n },\n render: ({ title, intro, columns, cards, className }) => {\n const part = useParts();\n return (\n <section className={part(\"gf-shadcn-cards\", \"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {(title || intro) && (\n <div className={part(\"gf-shadcn-cards-header\", \"mb-8 flex max-w-3xl flex-col gap-3\")}>\n {title && (\n <h2 className={part(\"gf-shadcn-cards-heading\", \"text-3xl font-bold tracking-tight\")}>{title}</h2>\n )}\n {intro && <p className={part(\"gf-shadcn-cards-intro\", \"text-lg text-muted-foreground\")}>{intro}</p>}\n </div>\n )}\n <div className={part(\"gf-shadcn-cards-grid\", \"grid gap-6\", columnClasses[columns])}>\n {cards.map((card, index) => (\n // Cards have no ids of their own, and their order is all that tells them apart.\n // biome-ignore lint/suspicious/noArrayIndexKey: see above\n <Card key={index} className={part(\"gf-shadcn-cards-card\", \"overflow-hidden pt-0\")}>\n {card.image ? (\n <SiteImage\n src={card.image}\n alt={card.imageAlt}\n className={part(\"gf-shadcn-cards-image\", \"aspect-video w-full object-cover\")}\n />\n ) : (\n <div className=\"h-0\" />\n )}\n <CardHeader>\n <CardTitle className={part(\"gf-shadcn-cards-title\", \"text-xl\")}>{card.title}</CardTitle>\n {card.text && (\n <CardDescription className={part(\"gf-shadcn-cards-text\", \"text-base\")}>{card.text}</CardDescription>\n )}\n </CardHeader>\n <CardContent className=\"flex-1\" />\n {card.linkLabel && (\n <CardFooter>\n <SiteLink\n href={card.link || \"/\"}\n className={part(\"gf-shadcn-cards-link\", buttonVariants({ variant: \"outline\" }))}\n >\n {card.linkLabel}\n </SiteLink>\n </CardFooter>\n )}\n </Card>\n ))}\n </div>\n </section>\n );\n },\n },\n {\n \"gf-shadcn-cards\": \"Cards\",\n \"gf-shadcn-cards-header\": \"Heading and introduction\",\n \"gf-shadcn-cards-heading\": \"Heading\",\n \"gf-shadcn-cards-intro\": \"Introduction\",\n \"gf-shadcn-cards-grid\": \"Grid of cards\",\n \"gf-shadcn-cards-card\": \"Card\",\n \"gf-shadcn-cards-image\": \"Card's picture\",\n \"gf-shadcn-cards-title\": \"Card's heading\",\n \"gf-shadcn-cards-text\": \"Card's text\",\n \"gf-shadcn-cards-link\": \"Card's link\",\n },\n);\n\nexport default Cards;\n"
16
16
  }
17
17
  ],
18
18
  "meta": {
19
19
  "goodfellow": {
20
20
  "category": "Page sections",
21
21
  "recommended": true,
22
- "version": "0.5.0"
22
+ "version": "0.6.0"
23
23
  }
24
24
  },
25
25
  "dependencies": []
@@ -11,18 +11,18 @@
11
11
  {
12
12
  "path": "blocks/installed/shadcn-carousel/block.tsx",
13
13
  "type": "registry:block",
14
- "content": "import { classNameField, cx, mediaField } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { ImageCarousel } from \"./image-carousel\";\n\ninterface Slide {\n image: string;\n alt: string;\n caption: string;\n}\n\nexport interface CarouselProps {\n slides: Slide[];\n loop: boolean;\n className: string;\n}\n\n/** Pictures shown one at a time, with buttons to move between them. */\nconst Carousel: ComponentConfig<CarouselProps> = {\n label: \"Image carousel\",\n fields: {\n slides: {\n type: \"array\",\n label: \"Pictures\",\n arrayFields: {\n image: mediaField(\"Picture\"),\n alt: { type: \"text\", label: \"Description for screen readers\" },\n caption: { type: \"text\", label: \"Caption\" },\n },\n defaultItemProps: { image: \"\", alt: \"\", caption: \"\" },\n getItemSummary: (slide, index) => slide.caption || slide.alt || `Picture ${(index ?? 0) + 1}`,\n },\n loop: {\n type: \"radio\",\n label: \"Start again after the last picture\",\n options: [\n { label: \"Yes\", value: true },\n { label: \"No\", value: false },\n ],\n },\n className: classNameField,\n },\n defaultProps: { slides: [], loop: true, className: \"\" },\n render: ({ slides, loop, className }) => (\n <section className={cx(\"mx-auto w-full max-w-4xl px-14 py-8\", className)}>\n {slides.length > 0 ? (\n <ImageCarousel slides={slides.filter((slide) => slide.image)} loop={loop} />\n ) : (\n <div className=\"aspect-video w-full rounded-xl bg-muted\" />\n )}\n </section>\n ),\n};\n\nexport default Carousel;\n"
14
+ "content": "import { classNameField, mediaField, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { ImageCarousel } from \"./image-carousel\";\n\ninterface Slide {\n image: string;\n alt: string;\n caption: string;\n}\n\nexport interface CarouselProps {\n slides: Slide[];\n loop: boolean;\n className: string;\n}\n\n/** Pictures shown one at a time, with buttons to move between them. */\nconst Carousel: ComponentConfig<CarouselProps> = withParts(\n {\n label: \"Image carousel\",\n fields: {\n slides: {\n type: \"array\",\n label: \"Pictures\",\n arrayFields: {\n image: mediaField(\"Picture\"),\n alt: { type: \"text\", label: \"Description for screen readers\" },\n caption: { type: \"text\", label: \"Caption\" },\n },\n defaultItemProps: { image: \"\", alt: \"\", caption: \"\" },\n getItemSummary: (slide, index) => slide.caption || slide.alt || `Picture ${(index ?? 0) + 1}`,\n },\n loop: {\n type: \"radio\",\n label: \"Start again after the last picture\",\n options: [\n { label: \"Yes\", value: true },\n { label: \"No\", value: false },\n ],\n },\n className: classNameField,\n },\n defaultProps: { slides: [], loop: true, className: \"\" },\n render: ({ slides, loop, className }) => {\n const part = useParts();\n const image = part(\"gf-shadcn-carousel-image\", \"aspect-video w-full rounded-xl object-cover\");\n return (\n <section className={part(\"gf-shadcn-carousel\", \"mx-auto w-full max-w-4xl px-14 py-8\", className)}>\n {slides.length > 0 ? (\n <ImageCarousel\n slides={slides.filter((slide) => slide.image)}\n loop={loop}\n classes={{\n image,\n caption: part(\"gf-shadcn-carousel-caption\", \"mt-2 text-center text-sm text-muted-foreground\"),\n }}\n />\n ) : (\n <div className={part(\"gf-shadcn-carousel-image\", \"aspect-video w-full rounded-xl bg-muted\")} />\n )}\n </section>\n );\n },\n },\n {\n \"gf-shadcn-carousel\": \"Carousel\",\n \"gf-shadcn-carousel-image\": \"Picture\",\n \"gf-shadcn-carousel-caption\": \"Caption\",\n },\n);\n\nexport default Carousel;\n"
15
15
  },
16
16
  {
17
17
  "path": "blocks/installed/shadcn-carousel/image-carousel.tsx",
18
18
  "type": "registry:component",
19
- "content": "\"use client\";\n\nimport { SiteImage } from \"@goodfellow-cms/react\";\nimport { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from \"@/components/ui/carousel\";\n\n/** The pictures, one at a time, with previous and next buttons. */\nexport function ImageCarousel({\n slides,\n loop,\n}: {\n slides: Array<{ image: string; alt: string; caption: string }>;\n loop: boolean;\n}) {\n return (\n <Carousel opts={{ loop }}>\n <CarouselContent>\n {slides.map((slide, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: pictures have no ids of their own\n <CarouselItem key={index}>\n <figure>\n <SiteImage src={slide.image} alt={slide.alt} className=\"aspect-video w-full rounded-xl object-cover\" />\n {slide.caption && (\n <figcaption className=\"mt-2 text-center text-sm text-muted-foreground\">{slide.caption}</figcaption>\n )}\n </figure>\n </CarouselItem>\n ))}\n </CarouselContent>\n <CarouselPrevious />\n <CarouselNext />\n </Carousel>\n );\n}\n"
19
+ "content": "\"use client\";\n\nimport { SiteImage } from \"@goodfellow-cms/react\";\nimport { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from \"@/components/ui/carousel\";\n\n/** The pictures, one at a time, with previous and next buttons, and their parts' classes from the block. */\nexport function ImageCarousel({\n slides,\n loop,\n classes,\n}: {\n slides: Array<{ image: string; alt: string; caption: string }>;\n loop: boolean;\n classes: { image: string; caption: string };\n}) {\n return (\n <Carousel opts={{ loop }}>\n <CarouselContent>\n {slides.map((slide, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: pictures have no ids of their own\n <CarouselItem key={index}>\n <figure>\n <SiteImage src={slide.image} alt={slide.alt} className={classes.image} />\n {slide.caption && <figcaption className={classes.caption}>{slide.caption}</figcaption>}\n </figure>\n </CarouselItem>\n ))}\n </CarouselContent>\n <CarouselPrevious />\n <CarouselNext />\n </Carousel>\n );\n}\n"
20
20
  }
21
21
  ],
22
22
  "meta": {
23
23
  "goodfellow": {
24
24
  "category": "Interactive",
25
- "version": "0.5.0"
25
+ "version": "0.6.0"
26
26
  }
27
27
  },
28
28
  "dependencies": []
package/r/shadcn-faq.json CHANGED
@@ -11,19 +11,19 @@
11
11
  {
12
12
  "path": "blocks/installed/shadcn-faq/block.tsx",
13
13
  "type": "registry:block",
14
- "content": "import { classNameField, cx } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { FaqList } from \"./faq-list\";\n\ninterface Question {\n question: string;\n answer: RichText;\n}\n\nexport interface FaqProps {\n title: string;\n questions: Question[];\n className: string;\n}\n\n/** Questions that open to show their answers. Every answer is in the page, so search engines see them. */\nconst Faq: ComponentConfig<FaqProps> = {\n label: \"FAQ\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n questions: {\n type: \"array\",\n label: \"Questions\",\n arrayFields: {\n question: { type: \"text\", label: \"Question\" },\n answer: { type: \"richtext\", label: \"Answer\" },\n },\n defaultItemProps: { question: \"A question\", answer: \"<p>Its answer.</p>\" },\n getItemSummary: (item) => item.question || \"Question\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Questions and answers\",\n questions: [\n { question: \"A question people ask\", answer: \"<p>Its answer.</p>\" },\n { question: \"Another question\", answer: \"<p>Its answer.</p>\" },\n ],\n className: \"\",\n },\n render: ({ title, questions, className }) => (\n <section className={cx(\"mx-auto w-full max-w-3xl px-4 py-12\", className)}>\n {title && <h2 className=\"mb-6 text-3xl font-bold tracking-tight\">{title}</h2>}\n <FaqList questions={questions.map(({ question, answer }) => ({ question, answer }))} />\n </section>\n ),\n};\n\nexport default Faq;\n"
14
+ "content": "import { classNameField, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { FaqList } from \"./faq-list\";\n\ninterface Question {\n question: string;\n answer: RichText;\n}\n\nexport interface FaqProps {\n title: string;\n questions: Question[];\n className: string;\n}\n\n/** Questions that open to show their answers. Every answer is in the page, so search engines see them. */\nconst Faq: ComponentConfig<FaqProps> = withParts(\n {\n label: \"FAQ\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n questions: {\n type: \"array\",\n label: \"Questions\",\n arrayFields: {\n question: { type: \"text\", label: \"Question\" },\n answer: { type: \"richtext\", label: \"Answer\" },\n },\n defaultItemProps: { question: \"A question\", answer: \"<p>Its answer.</p>\" },\n getItemSummary: (item) => item.question || \"Question\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Questions and answers\",\n questions: [\n { question: \"A question people ask\", answer: \"<p>Its answer.</p>\" },\n { question: \"Another question\", answer: \"<p>Its answer.</p>\" },\n ],\n className: \"\",\n },\n render: ({ title, questions, className }) => {\n const part = useParts();\n return (\n <section className={part(\"gf-shadcn-faq\", \"mx-auto w-full max-w-3xl px-4 py-12\", className)}>\n {title && (\n <h2 className={part(\"gf-shadcn-faq-heading\", \"mb-6 text-3xl font-bold tracking-tight\")}>{title}</h2>\n )}\n <FaqList\n questions={questions.map(({ question, answer }) => ({ question, answer }))}\n classes={{\n question: part(\n \"gf-shadcn-faq-question\",\n undefined,\n \"[&>[data-slot=accordion-content][data-state=closed]]:hidden\",\n ),\n trigger: part(\"gf-shadcn-faq-trigger\", \"text-base\"),\n answer: part(\"gf-shadcn-faq-answer\", \"gf-prose\"),\n }}\n />\n </section>\n );\n },\n },\n {\n \"gf-shadcn-faq\": \"Questions and answers\",\n \"gf-shadcn-faq-heading\": \"Heading\",\n \"gf-shadcn-faq-question\": \"Question and answer\",\n \"gf-shadcn-faq-trigger\": \"Question\",\n \"gf-shadcn-faq-answer\": \"Answer\",\n },\n);\n\nexport default Faq;\n"
15
15
  },
16
16
  {
17
17
  "path": "blocks/installed/shadcn-faq/faq-list.tsx",
18
18
  "type": "registry:component",
19
- "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from \"@/components/ui/accordion\";\n\n/** The questions, one open at a time. Closed answers stay in the page, hidden, rather than being left out. */\nexport function FaqList({ questions }: { questions: Array<{ question: string; answer: ReactNode }> }) {\n return (\n <Accordion type=\"single\" collapsible className=\"w-full\">\n {questions.map((item, index) => (\n <AccordionItem\n // biome-ignore lint/suspicious/noArrayIndexKey: questions have no ids of their own\n key={index}\n value={`question-${index}`}\n className=\"[&>[data-slot=accordion-content][data-state=closed]]:hidden\"\n >\n <AccordionTrigger className=\"text-base\">{item.question}</AccordionTrigger>\n <AccordionContent forceMount>\n <div className=\"gf-prose\">{item.answer}</div>\n </AccordionContent>\n </AccordionItem>\n ))}\n </Accordion>\n );\n}\n"
19
+ "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from \"@/components/ui/accordion\";\n\n/**\n * The questions, one open at a time, with their parts' classes from the block.\n * Closed answers stay in the page, hidden, rather than being left out.\n */\nexport function FaqList({\n questions,\n classes,\n}: {\n questions: Array<{ question: string; answer: ReactNode }>;\n classes: { question: string; trigger: string; answer: string };\n}) {\n return (\n <Accordion type=\"single\" collapsible className=\"w-full\">\n {questions.map((item, index) => (\n <AccordionItem\n // biome-ignore lint/suspicious/noArrayIndexKey: questions have no ids of their own\n key={index}\n value={`question-${index}`}\n className={classes.question}\n >\n <AccordionTrigger className={classes.trigger}>{item.question}</AccordionTrigger>\n <AccordionContent forceMount>\n <div className={classes.answer}>{item.answer}</div>\n </AccordionContent>\n </AccordionItem>\n ))}\n </Accordion>\n );\n}\n"
20
20
  }
21
21
  ],
22
22
  "meta": {
23
23
  "goodfellow": {
24
24
  "category": "Interactive",
25
25
  "recommended": true,
26
- "version": "0.5.0"
26
+ "version": "0.6.0"
27
27
  }
28
28
  },
29
29
  "dependencies": []
@@ -11,14 +11,14 @@
11
11
  {
12
12
  "path": "blocks/installed/shadcn-hero/block.tsx",
13
13
  "type": "registry:block",
14
- "content": "import { classNameField, cx, mediaField, SiteImage, SiteLink } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\ntype Layout = \"centered\" | \"beside\";\n\nexport interface HeroProps {\n title: string;\n text: string;\n image: string;\n imageAlt: string;\n layout: Layout;\n primaryLabel: string;\n primaryLink: string;\n secondaryLabel: string;\n secondaryLink: string;\n className: string;\n}\n\n/** A large heading at the top of a page, with text, up to two buttons and a picture. */\nconst Hero: ComponentConfig<HeroProps> = {\n label: \"Hero\",\n fields: {\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"textarea\", label: \"Text\", contentEditable: true },\n image: mediaField(\"Picture\"),\n imageAlt: { type: \"text\", label: \"Description of the picture for screen readers\" },\n layout: {\n type: \"radio\",\n label: \"Layout\",\n options: [\n { label: \"Centered, picture below\", value: \"centered\" },\n { label: \"Picture beside the text\", value: \"beside\" },\n ],\n },\n primaryLabel: { type: \"text\", label: \"Main button\" },\n primaryLink: { type: \"text\", label: \"Main button links to\" },\n secondaryLabel: { type: \"text\", label: \"Second button (optional)\" },\n secondaryLink: { type: \"text\", label: \"Second button links to\" },\n className: classNameField,\n },\n defaultProps: {\n title: \"Welcome\",\n text: \"Say in a sentence or two what this site is about.\",\n image: \"\",\n imageAlt: \"\",\n layout: \"centered\",\n primaryLabel: \"Find out more\",\n primaryLink: \"/about\",\n secondaryLabel: \"\",\n secondaryLink: \"\",\n className: \"\",\n },\n render: ({\n title,\n text,\n image,\n imageAlt,\n layout,\n primaryLabel,\n primaryLink,\n secondaryLabel,\n secondaryLink,\n className,\n }) => {\n const beside = layout === \"beside\";\n return (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-16 md:py-24\", className)}>\n <div\n className={cx(\"flex flex-col gap-10\", beside ? \"md:flex-row md:items-center\" : \"items-center text-center\")}\n >\n <div className={cx(\"flex flex-col gap-6\", beside ? \"md:flex-1\" : \"max-w-3xl items-center\")}>\n <h1 className=\"text-4xl font-bold tracking-tight text-balance md:text-6xl\">{title}</h1>\n {text && <p className=\"text-lg text-muted-foreground md:text-xl\">{text}</p>}\n {(primaryLabel || secondaryLabel) && (\n <div className=\"flex flex-wrap gap-3\">\n {primaryLabel && (\n <SiteLink href={primaryLink || \"/\"} className={buttonVariants({ size: \"lg\" })}>\n {primaryLabel}\n </SiteLink>\n )}\n {secondaryLabel && (\n <SiteLink href={secondaryLink || \"/\"} className={buttonVariants({ size: \"lg\", variant: \"outline\" })}>\n {secondaryLabel}\n </SiteLink>\n )}\n </div>\n )}\n </div>\n {image && (\n <SiteImage\n src={image}\n alt={imageAlt}\n className={cx(\"w-full rounded-xl object-cover\", beside ? \"md:w-1/2\" : \"max-w-4xl\")}\n />\n )}\n </div>\n </section>\n );\n },\n};\n\nexport default Hero;\n"
14
+ "content": "import { classNameField, mediaField, SiteImage, SiteLink, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { buttonVariants } from \"@/components/ui/button\";\n\ntype Layout = \"centered\" | \"beside\";\n\nexport interface HeroProps {\n title: string;\n text: string;\n image: string;\n imageAlt: string;\n layout: Layout;\n primaryLabel: string;\n primaryLink: string;\n secondaryLabel: string;\n secondaryLink: string;\n className: string;\n}\n\n/** A large heading at the top of a page, with text, up to two buttons and a picture. */\nconst Hero: ComponentConfig<HeroProps> = withParts(\n {\n label: \"Hero\",\n fields: {\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"textarea\", label: \"Text\", contentEditable: true },\n image: mediaField(\"Picture\"),\n imageAlt: { type: \"text\", label: \"Description of the picture for screen readers\" },\n layout: {\n type: \"radio\",\n label: \"Layout\",\n options: [\n { label: \"Centered, picture below\", value: \"centered\" },\n { label: \"Picture beside the text\", value: \"beside\" },\n ],\n },\n primaryLabel: { type: \"text\", label: \"Main button\" },\n primaryLink: { type: \"text\", label: \"Main button links to\" },\n secondaryLabel: { type: \"text\", label: \"Second button (optional)\" },\n secondaryLink: { type: \"text\", label: \"Second button links to\" },\n className: classNameField,\n },\n defaultProps: {\n title: \"Welcome\",\n text: \"Say in a sentence or two what this site is about.\",\n image: \"\",\n imageAlt: \"\",\n layout: \"centered\",\n primaryLabel: \"Find out more\",\n primaryLink: \"/about\",\n secondaryLabel: \"\",\n secondaryLink: \"\",\n className: \"\",\n },\n render: ({\n title,\n text,\n image,\n imageAlt,\n layout,\n primaryLabel,\n primaryLink,\n secondaryLabel,\n secondaryLink,\n className,\n }) => {\n const part = useParts();\n const beside = layout === \"beside\";\n return (\n <section className={part(\"gf-shadcn-hero\", \"mx-auto w-full max-w-6xl px-4 py-16 md:py-24\", className)}>\n <div\n className={part(\n \"gf-shadcn-hero-layout\",\n \"flex flex-col gap-10\",\n beside ? \"md:flex-row md:items-center\" : \"items-center text-center\",\n )}\n >\n <div\n className={part(\n \"gf-shadcn-hero-text\",\n \"flex flex-col gap-6\",\n beside ? \"md:flex-1\" : \"max-w-3xl items-center\",\n )}\n >\n <h1\n className={part(\"gf-shadcn-hero-heading\", \"text-4xl font-bold tracking-tight text-balance md:text-6xl\")}\n >\n {title}\n </h1>\n {text && (\n <p className={part(\"gf-shadcn-hero-intro\", \"text-lg text-muted-foreground md:text-xl\")}>{text}</p>\n )}\n {(primaryLabel || secondaryLabel) && (\n <div className={part(\"gf-shadcn-hero-buttons\", \"flex flex-wrap gap-3\")}>\n {primaryLabel && (\n <SiteLink\n href={primaryLink || \"/\"}\n className={part(\"gf-shadcn-hero-button\", buttonVariants({ size: \"lg\" }))}\n >\n {primaryLabel}\n </SiteLink>\n )}\n {secondaryLabel && (\n <SiteLink\n href={secondaryLink || \"/\"}\n className={part(\"gf-shadcn-hero-button\", buttonVariants({ size: \"lg\", variant: \"outline\" }))}\n >\n {secondaryLabel}\n </SiteLink>\n )}\n </div>\n )}\n </div>\n {image && (\n <SiteImage\n src={image}\n alt={imageAlt}\n className={part(\n \"gf-shadcn-hero-image\",\n \"w-full rounded-xl object-cover\",\n beside ? \"md:w-1/2\" : \"max-w-4xl\",\n )}\n />\n )}\n </div>\n </section>\n );\n },\n },\n {\n \"gf-shadcn-hero\": \"Hero\",\n \"gf-shadcn-hero-layout\": \"Text and picture\",\n \"gf-shadcn-hero-text\": \"Text\",\n \"gf-shadcn-hero-heading\": \"Heading\",\n \"gf-shadcn-hero-intro\": \"Introduction\",\n \"gf-shadcn-hero-buttons\": \"Buttons\",\n \"gf-shadcn-hero-button\": \"Button\",\n \"gf-shadcn-hero-image\": \"Picture\",\n },\n);\n\nexport default Hero;\n"
15
15
  }
16
16
  ],
17
17
  "meta": {
18
18
  "goodfellow": {
19
19
  "category": "Page sections",
20
20
  "recommended": true,
21
- "version": "0.5.0"
21
+ "version": "0.6.0"
22
22
  }
23
23
  },
24
24
  "dependencies": []
@@ -14,14 +14,14 @@
14
14
  {
15
15
  "path": "blocks/installed/shadcn-notice/block.tsx",
16
16
  "type": "registry:block",
17
- "content": "import { classNameField, cx } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { CircleCheckIcon, InfoIcon, TriangleAlertIcon } from \"lucide-react\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\n\ntype Kind = \"info\" | \"warning\" | \"success\" | \"urgent\";\n\nexport interface NoticeProps {\n kind: Kind;\n title: string;\n text: RichText;\n className: string;\n}\n\nconst icons = { info: InfoIcon, warning: TriangleAlertIcon, success: CircleCheckIcon, urgent: TriangleAlertIcon };\n\n/** A short message that stands out, such as a closure or a change of times. */\nconst Notice: ComponentConfig<NoticeProps> = {\n label: \"Notice\",\n fields: {\n kind: {\n type: \"select\",\n label: \"Kind\",\n options: [\n { label: \"Information\", value: \"info\" },\n { label: \"Warning\", value: \"warning\" },\n { label: \"Good news\", value: \"success\" },\n { label: \"Urgent\", value: \"urgent\" },\n ],\n },\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"richtext\", label: \"Text\", contentEditable: true },\n className: classNameField,\n },\n defaultProps: { kind: \"info\", title: \"Please note\", text: \"<p>Say what's changed.</p>\", className: \"\" },\n render: ({ kind, title, text, className }) => {\n const Icon = icons[kind];\n return (\n <div className={cx(\"mx-auto w-full max-w-3xl px-4 py-4\", className)}>\n <Alert variant={kind === \"urgent\" ? \"destructive\" : \"default\"}>\n <Icon aria-hidden=\"true\" />\n {title && <AlertTitle>{title}</AlertTitle>}\n <AlertDescription>\n <div className=\"gf-prose\">{text}</div>\n </AlertDescription>\n </Alert>\n </div>\n );\n },\n};\n\nexport default Notice;\n"
17
+ "content": "import { classNameField, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { CircleCheckIcon, InfoIcon, TriangleAlertIcon } from \"lucide-react\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\n\ntype Kind = \"info\" | \"warning\" | \"success\" | \"urgent\";\n\nexport interface NoticeProps {\n kind: Kind;\n title: string;\n text: RichText;\n className: string;\n}\n\nconst icons = { info: InfoIcon, warning: TriangleAlertIcon, success: CircleCheckIcon, urgent: TriangleAlertIcon };\n\n/** A short message that stands out, such as a closure or a change of times. */\nconst Notice: ComponentConfig<NoticeProps> = withParts(\n {\n label: \"Notice\",\n fields: {\n kind: {\n type: \"select\",\n label: \"Kind\",\n options: [\n { label: \"Information\", value: \"info\" },\n { label: \"Warning\", value: \"warning\" },\n { label: \"Good news\", value: \"success\" },\n { label: \"Urgent\", value: \"urgent\" },\n ],\n },\n title: { type: \"text\", label: \"Heading\", contentEditable: true },\n text: { type: \"richtext\", label: \"Text\", contentEditable: true },\n className: classNameField,\n },\n defaultProps: { kind: \"info\", title: \"Please note\", text: \"<p>Say what's changed.</p>\", className: \"\" },\n render: ({ kind, title, text, className }) => {\n const part = useParts();\n const Icon = icons[kind];\n return (\n <div className={part(\"gf-shadcn-notice\", \"mx-auto w-full max-w-3xl px-4 py-4\", className)}>\n <Alert variant={kind === \"urgent\" ? \"destructive\" : \"default\"} className={part(\"gf-shadcn-notice-box\")}>\n <Icon aria-hidden=\"true\" />\n {title && <AlertTitle className={part(\"gf-shadcn-notice-title\")}>{title}</AlertTitle>}\n <AlertDescription>\n <div className={part(\"gf-shadcn-notice-text\", \"gf-prose\")}>{text}</div>\n </AlertDescription>\n </Alert>\n </div>\n );\n },\n },\n {\n \"gf-shadcn-notice\": \"Notice\",\n \"gf-shadcn-notice-box\": \"Notice's box\",\n \"gf-shadcn-notice-title\": \"Notice's title\",\n \"gf-shadcn-notice-text\": \"Notice's text\",\n },\n);\n\nexport default Notice;\n"
18
18
  }
19
19
  ],
20
20
  "meta": {
21
21
  "goodfellow": {
22
22
  "category": "Page sections",
23
23
  "recommended": true,
24
- "version": "0.5.0"
24
+ "version": "0.6.0"
25
25
  }
26
26
  }
27
27
  }
@@ -16,13 +16,13 @@
16
16
  {
17
17
  "path": "blocks/installed/shadcn-pricing/block.tsx",
18
18
  "type": "registry:block",
19
- "content": "import { classNameField, cx, SiteLink } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\n\ninterface Plan {\n name: string;\n price: string;\n period: string;\n description: string;\n features: string;\n buttonLabel: string;\n buttonLink: string;\n badge: string;\n}\n\nexport interface PricingProps {\n title: string;\n intro: string;\n plans: Plan[];\n className: string;\n}\n\nconst emptyPlan: Plan = {\n name: \"Plan\",\n price: \"$10\",\n period: \"a month\",\n description: \"\",\n features: \"Something included\\nSomething else included\",\n buttonLabel: \"Choose\",\n buttonLink: \"/contact\",\n badge: \"\",\n};\n\n/** Plans side by side, with prices, what each includes and a button. */\nconst Pricing: ComponentConfig<PricingProps> = {\n label: \"Pricing table\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n intro: { type: \"textarea\", label: \"Introduction\" },\n plans: {\n type: \"array\",\n label: \"Plans\",\n arrayFields: {\n name: { type: \"text\", label: \"Name\" },\n price: { type: \"text\", label: \"Price, such as $10\" },\n period: { type: \"text\", label: \"Per, such as a month\" },\n description: { type: \"textarea\", label: \"Description\" },\n features: { type: \"textarea\", label: \"What's included, one per line\" },\n buttonLabel: { type: \"text\", label: \"Button\" },\n buttonLink: { type: \"text\", label: \"Button links to\" },\n badge: { type: \"text\", label: \"Label at the top, such as Most popular (optional)\" },\n },\n defaultItemProps: emptyPlan,\n getItemSummary: (plan) => plan.name || \"Plan\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Prices\",\n intro: \"\",\n plans: [\n { ...emptyPlan, name: \"Basic\" },\n { ...emptyPlan, name: \"Standard\", price: \"$25\", badge: \"Most popular\" },\n { ...emptyPlan, name: \"Premium\", price: \"$50\" },\n ],\n className: \"\",\n },\n render: ({ title, intro, plans, className }) => (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {(title || intro) && (\n <div className=\"mb-8 flex flex-col items-center gap-3 text-center\">\n {title && <h2 className=\"text-3xl font-bold tracking-tight\">{title}</h2>}\n {intro && <p className=\"max-w-2xl text-lg text-muted-foreground\">{intro}</p>}\n </div>\n )}\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {plans.map((plan, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: plans have no ids of their own\n <Card key={index} className={cx(plan.badge && \"border-primary border-2\")}>\n <CardHeader>\n <div className=\"flex items-center justify-between gap-2\">\n <CardTitle className=\"text-xl\">{plan.name}</CardTitle>\n {plan.badge && <Badge>{plan.badge}</Badge>}\n </div>\n {plan.description && <CardDescription>{plan.description}</CardDescription>}\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-6\">\n <p>\n <span className=\"text-4xl font-bold\">{plan.price}</span>\n {plan.period && <span className=\"text-muted-foreground\"> {plan.period}</span>}\n </p>\n <ul className=\"flex flex-col gap-2\">\n {plan.features\n .split(\"\\n\")\n .map((feature) => feature.trim())\n .filter(Boolean)\n .map((feature) => (\n <li key={feature} className=\"flex items-start gap-2\">\n <CheckIcon aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-primary\" />\n {feature}\n </li>\n ))}\n </ul>\n </CardContent>\n {plan.buttonLabel && (\n <CardFooter>\n <SiteLink\n href={plan.buttonLink || \"/\"}\n className={cx(buttonVariants({ variant: plan.badge ? \"default\" : \"outline\" }), \"w-full\")}\n >\n {plan.buttonLabel}\n </SiteLink>\n </CardFooter>\n )}\n </Card>\n ))}\n </div>\n </section>\n ),\n};\n\nexport default Pricing;\n"
19
+ "content": "import { classNameField, SiteLink, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { CheckIcon } from \"lucide-react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\n\ninterface Plan {\n name: string;\n price: string;\n period: string;\n description: string;\n features: string;\n buttonLabel: string;\n buttonLink: string;\n badge: string;\n}\n\nexport interface PricingProps {\n title: string;\n intro: string;\n plans: Plan[];\n className: string;\n}\n\nconst emptyPlan: Plan = {\n name: \"Plan\",\n price: \"$10\",\n period: \"a month\",\n description: \"\",\n features: \"Something included\\nSomething else included\",\n buttonLabel: \"Choose\",\n buttonLink: \"/contact\",\n badge: \"\",\n};\n\n/** Plans side by side, with prices, what each includes and a button. */\nconst Pricing: ComponentConfig<PricingProps> = withParts(\n {\n label: \"Pricing table\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n intro: { type: \"textarea\", label: \"Introduction\" },\n plans: {\n type: \"array\",\n label: \"Plans\",\n arrayFields: {\n name: { type: \"text\", label: \"Name\" },\n price: { type: \"text\", label: \"Price, such as $10\" },\n period: { type: \"text\", label: \"Per, such as a month\" },\n description: { type: \"textarea\", label: \"Description\" },\n features: { type: \"textarea\", label: \"What's included, one per line\" },\n buttonLabel: { type: \"text\", label: \"Button\" },\n buttonLink: { type: \"text\", label: \"Button links to\" },\n badge: { type: \"text\", label: \"Label at the top, such as Most popular (optional)\" },\n },\n defaultItemProps: emptyPlan,\n getItemSummary: (plan) => plan.name || \"Plan\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"Prices\",\n intro: \"\",\n plans: [\n { ...emptyPlan, name: \"Basic\" },\n { ...emptyPlan, name: \"Standard\", price: \"$25\", badge: \"Most popular\" },\n { ...emptyPlan, name: \"Premium\", price: \"$50\" },\n ],\n className: \"\",\n },\n render: ({ title, intro, plans, className }) => {\n const part = useParts();\n return (\n <section className={part(\"gf-shadcn-pricing\", \"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {(title || intro) && (\n <div className={part(\"gf-shadcn-pricing-header\", \"mb-8 flex flex-col items-center gap-3 text-center\")}>\n {title && (\n <h2 className={part(\"gf-shadcn-pricing-heading\", \"text-3xl font-bold tracking-tight\")}>{title}</h2>\n )}\n {intro && (\n <p className={part(\"gf-shadcn-pricing-intro\", \"max-w-2xl text-lg text-muted-foreground\")}>{intro}</p>\n )}\n </div>\n )}\n <div className={part(\"gf-shadcn-pricing-grid\", \"grid gap-6 md:grid-cols-2 lg:grid-cols-3\")}>\n {plans.map((plan, index) => (\n <Card\n // biome-ignore lint/suspicious/noArrayIndexKey: plans have no ids of their own\n key={index}\n className={part(\"gf-shadcn-pricing-plan\", undefined, plan.badge && \"border-primary border-2\")}\n >\n <CardHeader>\n <div className=\"flex items-center justify-between gap-2\">\n <CardTitle className={part(\"gf-shadcn-pricing-name\", \"text-xl\")}>{plan.name}</CardTitle>\n {plan.badge && <Badge className={part(\"gf-shadcn-pricing-badge\")}>{plan.badge}</Badge>}\n </div>\n {plan.description && (\n <CardDescription className={part(\"gf-shadcn-pricing-description\")}>\n {plan.description}\n </CardDescription>\n )}\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-6\">\n <p>\n <span className={part(\"gf-shadcn-pricing-price\", \"text-4xl font-bold\")}>{plan.price}</span>\n {plan.period && (\n <span className={part(\"gf-shadcn-pricing-period\", \"text-muted-foreground\")}> {plan.period}</span>\n )}\n </p>\n <ul className={part(\"gf-shadcn-pricing-features\", \"flex flex-col gap-2\")}>\n {plan.features\n .split(\"\\n\")\n .map((feature) => feature.trim())\n .filter(Boolean)\n .map((feature) => (\n <li key={feature} className={part(\"gf-shadcn-pricing-feature\", \"flex items-start gap-2\")}>\n <CheckIcon aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-primary\" />\n {feature}\n </li>\n ))}\n </ul>\n </CardContent>\n {plan.buttonLabel && (\n <CardFooter>\n <SiteLink\n href={plan.buttonLink || \"/\"}\n className={part(\"gf-shadcn-pricing-button\", [\n buttonVariants({ variant: plan.badge ? \"default\" : \"outline\" }),\n \"w-full\",\n ])}\n >\n {plan.buttonLabel}\n </SiteLink>\n </CardFooter>\n )}\n </Card>\n ))}\n </div>\n </section>\n );\n },\n },\n {\n \"gf-shadcn-pricing\": \"Pricing\",\n \"gf-shadcn-pricing-header\": \"Heading and introduction\",\n \"gf-shadcn-pricing-heading\": \"Heading\",\n \"gf-shadcn-pricing-intro\": \"Introduction\",\n \"gf-shadcn-pricing-grid\": \"Grid of plans\",\n \"gf-shadcn-pricing-plan\": \"Plan\",\n \"gf-shadcn-pricing-name\": \"Plan's name\",\n \"gf-shadcn-pricing-badge\": \"Badge\",\n \"gf-shadcn-pricing-description\": \"Plan's description\",\n \"gf-shadcn-pricing-price\": \"Price\",\n \"gf-shadcn-pricing-period\": \"Period\",\n \"gf-shadcn-pricing-features\": \"Features\",\n \"gf-shadcn-pricing-feature\": \"Feature\",\n \"gf-shadcn-pricing-button\": \"Button\",\n },\n);\n\nexport default Pricing;\n"
20
20
  }
21
21
  ],
22
22
  "meta": {
23
23
  "goodfellow": {
24
24
  "category": "Page sections",
25
- "version": "0.5.0"
25
+ "version": "0.6.0"
26
26
  }
27
27
  }
28
28
  }
@@ -11,19 +11,19 @@
11
11
  {
12
12
  "path": "blocks/installed/shadcn-tabs/block.tsx",
13
13
  "type": "registry:block",
14
- "content": "import { classNameField, cx } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { TabsView } from \"./tabs-view\";\n\ninterface Tab {\n label: string;\n content: RichText;\n}\n\nexport interface TabsProps {\n tabs: Tab[];\n className: string;\n}\n\n/** Content split into tabs, one shown at a time. Every tab's content is in the page, so search engines see it. */\nconst Tabs: ComponentConfig<TabsProps> = {\n label: \"Tabs\",\n fields: {\n tabs: {\n type: \"array\",\n label: \"Tabs\",\n arrayFields: {\n label: { type: \"text\", label: \"Tab name\" },\n content: { type: \"richtext\", label: \"Content\" },\n },\n defaultItemProps: { label: \"Tab\", content: \"<p>What this tab shows.</p>\" },\n getItemSummary: (tab) => tab.label || \"Tab\",\n },\n className: classNameField,\n },\n defaultProps: {\n tabs: [\n { label: \"First\", content: \"<p>What the first tab shows.</p>\" },\n { label: \"Second\", content: \"<p>What the second tab shows.</p>\" },\n ],\n className: \"\",\n },\n render: ({ tabs, className }) => (\n <section className={cx(\"mx-auto w-full max-w-4xl px-4 py-8\", className)}>\n <TabsView tabs={tabs.map(({ label, content }) => ({ label, content }))} />\n </section>\n ),\n};\n\nexport default Tabs;\n"
14
+ "content": "import { classNameField, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig, RichText } from \"@puckeditor/core\";\nimport { TabsView } from \"./tabs-view\";\n\ninterface Tab {\n label: string;\n content: RichText;\n}\n\nexport interface TabsProps {\n tabs: Tab[];\n className: string;\n}\n\n/** Content split into tabs, one shown at a time. Every tab's content is in the page, so search engines see it. */\nconst Tabs: ComponentConfig<TabsProps> = withParts(\n {\n label: \"Tabs\",\n fields: {\n tabs: {\n type: \"array\",\n label: \"Tabs\",\n arrayFields: {\n label: { type: \"text\", label: \"Tab name\" },\n content: { type: \"richtext\", label: \"Content\" },\n },\n defaultItemProps: { label: \"Tab\", content: \"<p>What this tab shows.</p>\" },\n getItemSummary: (tab) => tab.label || \"Tab\",\n },\n className: classNameField,\n },\n defaultProps: {\n tabs: [\n { label: \"First\", content: \"<p>What the first tab shows.</p>\" },\n { label: \"Second\", content: \"<p>What the second tab shows.</p>\" },\n ],\n className: \"\",\n },\n render: ({ tabs, className }) => {\n const part = useParts();\n return (\n <section className={part(\"gf-shadcn-tabs\", \"mx-auto w-full max-w-4xl px-4 py-8\", className)}>\n <TabsView\n tabs={tabs.map(({ label, content }) => ({ label, content }))}\n classes={{\n list: part(\"gf-shadcn-tabs-list\", \"flex-wrap\"),\n tab: part(\"gf-shadcn-tabs-tab\"),\n content: part(\"gf-shadcn-tabs-content\", \"pt-4\", \"data-[state=inactive]:hidden\"),\n }}\n />\n </section>\n );\n },\n },\n {\n \"gf-shadcn-tabs\": \"Tabs\",\n \"gf-shadcn-tabs-list\": \"Tabs' names\",\n \"gf-shadcn-tabs-tab\": \"Tab's name\",\n \"gf-shadcn-tabs-content\": \"Tab's content\",\n },\n);\n\nexport default Tabs;\n"
15
15
  },
16
16
  {
17
17
  "path": "blocks/installed/shadcn-tabs/tabs-view.tsx",
18
18
  "type": "registry:component",
19
- "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\n\n/** The tabs, with every tab's content in the page and the others hidden. */\nexport function TabsView({ tabs }: { tabs: Array<{ label: string; content: ReactNode }> }) {\n if (tabs.length === 0) return null;\n return (\n <Tabs defaultValue=\"tab-0\">\n <TabsList className=\"flex-wrap\">\n {tabs.map((tab, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabs have no ids of their own\n <TabsTrigger key={index} value={`tab-${index}`}>\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n {tabs.map((tab, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabs have no ids of their own\n <TabsContent key={index} value={`tab-${index}`} forceMount className=\"pt-4 data-[state=inactive]:hidden\">\n <div className=\"gf-prose\">{tab.content}</div>\n </TabsContent>\n ))}\n </Tabs>\n );\n}\n"
19
+ "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\n\n/** The tabs, with every tab's content in the page and the others hidden, and their parts' classes from the block. */\nexport function TabsView({\n tabs,\n classes,\n}: {\n tabs: Array<{ label: string; content: ReactNode }>;\n classes: { list: string; tab: string; content: string };\n}) {\n if (tabs.length === 0) return null;\n return (\n <Tabs defaultValue=\"tab-0\">\n <TabsList className={classes.list}>\n {tabs.map((tab, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabs have no ids of their own\n <TabsTrigger key={index} value={`tab-${index}`} className={classes.tab}>\n {tab.label}\n </TabsTrigger>\n ))}\n </TabsList>\n {tabs.map((tab, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: tabs have no ids of their own\n <TabsContent key={index} value={`tab-${index}`} forceMount className={classes.content}>\n <div className=\"gf-prose\">{tab.content}</div>\n </TabsContent>\n ))}\n </Tabs>\n );\n}\n"
20
20
  }
21
21
  ],
22
22
  "meta": {
23
23
  "goodfellow": {
24
24
  "category": "Interactive",
25
25
  "recommended": true,
26
- "version": "0.5.0"
26
+ "version": "0.6.0"
27
27
  }
28
28
  },
29
29
  "dependencies": []
@@ -11,14 +11,14 @@
11
11
  {
12
12
  "path": "blocks/installed/shadcn-testimonials/block.tsx",
13
13
  "type": "registry:block",
14
- "content": "import { classNameField, cx, mediaField, SiteImage } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\";\n\ninterface Testimonial {\n quote: string;\n name: string;\n role: string;\n photo: string;\n}\n\nexport interface TestimonialsProps {\n title: string;\n testimonials: Testimonial[];\n className: string;\n}\n\n/** Quotes from people, with their names and photos. */\nconst Testimonials: ComponentConfig<TestimonialsProps> = {\n label: \"Testimonials\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n testimonials: {\n type: \"array\",\n label: \"Quotes\",\n arrayFields: {\n quote: { type: \"textarea\", label: \"What they said\" },\n name: { type: \"text\", label: \"Name\" },\n role: { type: \"text\", label: \"Who they are, such as a member since 2020\" },\n photo: mediaField(\"Photo\"),\n },\n defaultItemProps: { quote: \"Something kind they said.\", name: \"A name\", role: \"\", photo: \"\" },\n getItemSummary: (item) => item.name || \"Quote\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"What people say\",\n testimonials: [\n { quote: \"Something kind they said.\", name: \"A name\", role: \"\", photo: \"\" },\n { quote: \"Something else kind they said.\", name: \"Another name\", role: \"\", photo: \"\" },\n ],\n className: \"\",\n },\n render: ({ title, testimonials, className }) => (\n <section className={cx(\"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {title && <h2 className=\"mb-8 text-3xl font-bold tracking-tight\">{title}</h2>}\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {testimonials.map((item, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: quotes have no ids of their own\n <Card key={index}>\n <CardContent className=\"flex-1\">\n <blockquote className=\"text-lg leading-relaxed\">“{item.quote}”</blockquote>\n </CardContent>\n <CardFooter className=\"gap-3\">\n {item.photo && (\n <SiteImage src={item.photo} alt=\"\" className=\"size-10 shrink-0 rounded-full object-cover\" />\n )}\n <div className=\"flex flex-col\">\n <span className=\"font-semibold\">{item.name}</span>\n {item.role && <span className=\"text-sm text-muted-foreground\">{item.role}</span>}\n </div>\n </CardFooter>\n </Card>\n ))}\n </div>\n </section>\n ),\n};\n\nexport default Testimonials;\n"
14
+ "content": "import { classNameField, mediaField, SiteImage, useParts, withParts } from \"@goodfellow-cms/react\";\nimport type { ComponentConfig } from \"@puckeditor/core\";\nimport { Card, CardContent, CardFooter } from \"@/components/ui/card\";\n\ninterface Testimonial {\n quote: string;\n name: string;\n role: string;\n photo: string;\n}\n\nexport interface TestimonialsProps {\n title: string;\n testimonials: Testimonial[];\n className: string;\n}\n\n/** Quotes from people, with their names and photos. */\nconst Testimonials: ComponentConfig<TestimonialsProps> = withParts(\n {\n label: \"Testimonials\",\n fields: {\n title: { type: \"text\", label: \"Heading\" },\n testimonials: {\n type: \"array\",\n label: \"Quotes\",\n arrayFields: {\n quote: { type: \"textarea\", label: \"What they said\" },\n name: { type: \"text\", label: \"Name\" },\n role: { type: \"text\", label: \"Who they are, such as a member since 2020\" },\n photo: mediaField(\"Photo\"),\n },\n defaultItemProps: { quote: \"Something kind they said.\", name: \"A name\", role: \"\", photo: \"\" },\n getItemSummary: (item) => item.name || \"Quote\",\n },\n className: classNameField,\n },\n defaultProps: {\n title: \"What people say\",\n testimonials: [\n { quote: \"Something kind they said.\", name: \"A name\", role: \"\", photo: \"\" },\n { quote: \"Something else kind they said.\", name: \"Another name\", role: \"\", photo: \"\" },\n ],\n className: \"\",\n },\n render: ({ title, testimonials, className }) => {\n const part = useParts();\n return (\n <section className={part(\"gf-shadcn-testimonials\", \"mx-auto w-full max-w-6xl px-4 py-12\", className)}>\n {title && (\n <h2 className={part(\"gf-shadcn-testimonials-heading\", \"mb-8 text-3xl font-bold tracking-tight\")}>\n {title}\n </h2>\n )}\n <div className={part(\"gf-shadcn-testimonials-grid\", \"grid gap-6 md:grid-cols-2 lg:grid-cols-3\")}>\n {testimonials.map((item, index) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: quotes have no ids of their own\n <Card key={index} className={part(\"gf-shadcn-testimonials-card\")}>\n <CardContent className=\"flex-1\">\n <blockquote className={part(\"gf-shadcn-testimonials-quote\", \"text-lg leading-relaxed\")}>\n “{item.quote}”\n </blockquote>\n </CardContent>\n <CardFooter className=\"gap-3\">\n {item.photo && (\n <SiteImage\n src={item.photo}\n alt=\"\"\n className={part(\"gf-shadcn-testimonials-photo\", \"size-10 shrink-0 rounded-full object-cover\")}\n />\n )}\n <div className=\"flex flex-col\">\n <span className={part(\"gf-shadcn-testimonials-name\", \"font-semibold\")}>{item.name}</span>\n {item.role && (\n <span className={part(\"gf-shadcn-testimonials-role\", \"text-sm text-muted-foreground\")}>\n {item.role}\n </span>\n )}\n </div>\n </CardFooter>\n </Card>\n ))}\n </div>\n </section>\n );\n },\n },\n {\n \"gf-shadcn-testimonials\": \"Testimonials\",\n \"gf-shadcn-testimonials-heading\": \"Heading\",\n \"gf-shadcn-testimonials-grid\": \"Grid of quotes\",\n \"gf-shadcn-testimonials-card\": \"Quote's card\",\n \"gf-shadcn-testimonials-quote\": \"Quote\",\n \"gf-shadcn-testimonials-photo\": \"Photo\",\n \"gf-shadcn-testimonials-name\": \"Name\",\n \"gf-shadcn-testimonials-role\": \"Role\",\n },\n);\n\nexport default Testimonials;\n"
15
15
  }
16
16
  ],
17
17
  "meta": {
18
18
  "goodfellow": {
19
19
  "category": "Page sections",
20
20
  "recommended": true,
21
- "version": "0.5.0"
21
+ "version": "0.6.0"
22
22
  }
23
23
  },
24
24
  "dependencies": []