@goodfellow-cms/registry 0.0.0-stage → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Goodfellow contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 shadcn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,107 @@
1
- # Temporary Holding Version
1
+ # @goodfellow-cms/registry
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Goodfellow's block registry: blocks built with [shadcn/ui](https://ui.shadcn.com), which a site's admins add and remove on the admin panel's **Blocks** screen, with no developer. It's also the reference for anyone publishing a registry of Goodfellow blocks built with another component library.
4
+
5
+ | Block | Category | Recommended |
6
+ |---|---|---|
7
+ | Hero | Page sections | Yes |
8
+ | Cards | Page sections | Yes |
9
+ | Call to action | Page sections | Yes |
10
+ | Testimonials | Page sections | Yes |
11
+ | Notice | Page sections | Yes |
12
+ | Pricing table | Page sections | |
13
+ | FAQ | Interactive | Yes |
14
+ | Tabs | Interactive | Yes |
15
+ | Image carousel | Interactive | |
16
+ | Custom HTML | Advanced | |
17
+
18
+ New sites made with `create-goodfellow` start with the recommended blocks unless the admin chooses "Built-in blocks only".
19
+
20
+ ## How a block gets into a site
21
+
22
+ Adding a block copies its code into the site, as `npx shadcn add` would, in one publish:
23
+
24
+ - the block's own files to `blocks/installed/<name>/`;
25
+ - the shadcn components it uses to `components/ui/`, and any other shared code to `components/`, `lib/` or `hooks/`, unless the site already has them;
26
+ - a record of what was added, `blocks/installed/installed.json`, with a hash of every file written;
27
+ - `blocks/installed/index.ts`, which the site's `goodfellow.config.tsx` imports and which lists every installed block.
28
+
29
+ The editor offers the block once the site has been rebuilt with it, which the host does after the publish (and `goodfellow dev` or `next dev` do straight away). Removing a block is refused while any page, collection page design, header or footer uses it. Removing deletes the files it added, except those someone has changed since or that another installed block uses.
30
+
31
+ The admin panel installs a release of this registry that matches its own: `https://cdn.jsdelivr.net/npm/@goodfellow-cms/registry@<version>/r/{name}.json`. In development, `goodfellow dev` and `next dev` serve the copy in the site's `node_modules`.
32
+
33
+ ## Publishing your own registry
34
+
35
+ A Goodfellow block registry is a [shadcn registry](https://ui.shadcn.com/docs/registry) whose block items follow a few extra rules. Build it with `npx shadcn build` and host the files anywhere that allows requests from browsers (CORS), such as GitHub Pages, npm through jsDelivr, or your own site.
36
+
37
+ ### Items
38
+
39
+ A block is a `registry:block` item with `meta.goodfellow`:
40
+
41
+ ```json
42
+ {
43
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
44
+ "name": "acme-gallery",
45
+ "type": "registry:block",
46
+ "title": "Gallery",
47
+ "description": "Pictures in a grid that open larger when clicked.",
48
+ "registryDependencies": ["@acme/dialog"],
49
+ "dependencies": ["radix-ui"],
50
+ "files": [
51
+ { "path": "blocks/acme-gallery/block.tsx", "type": "registry:block", "content": "…" },
52
+ { "path": "blocks/acme-gallery/gallery-grid.tsx", "type": "registry:component", "content": "…" }
53
+ ],
54
+ "meta": {
55
+ "goodfellow": {
56
+ "category": "Pictures",
57
+ "recommended": false,
58
+ "image": "images/acme-gallery.png",
59
+ "version": "1.2.0"
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ - **`name`** becomes the block's key in the site's content, so never change it once published. Use lowercase letters, numbers and dashes, and start names with your registry's own prefix so they don't clash with other registries' blocks.
66
+ - **`title`** and **`description`** are what admins see on the Blocks screen. Write them for people who aren't developers.
67
+ - **`meta.goodfellow.category`** is the group the block is listed under, on the Blocks screen and in the editor. `recommended` marks blocks a new site should start with. `image` is a picture of the block, relative to the item's address. `version` is recorded in the site when the block is added.
68
+ - **Files:** the item has exactly one `registry:block` file, whose default export is the block's Puck `ComponentConfig`. Every other file of the item goes into the block's folder too (`blocks/installed/<name>/`), so import them with relative paths (`./gallery-grid`). Items without `meta.goodfellow`, such as shared components, are dependencies: their files go to `components/ui/` (`registry:ui`), `components/` (`registry:component`), `lib/` (`registry:lib`) or `hooks/` (`registry:hook`), and blocks import them as `@/components/ui/dialog` and so on. A file's `target` can name another place within those folders. Other file types, and files elsewhere in the site, are refused.
69
+ - **`registryDependencies`** name items as `@namespace/name`, or as a plain name for shadcn/ui's own components (`"button"`), as with the shadcn CLI.
70
+ - **`registry.json`**, at the same address as the items (`…/r/registry.json` for `…/r/{name}.json`), lists them, so the Blocks screen can show them without loading every item.
71
+
72
+ ### Packages
73
+
74
+ The admin panel can't install npm packages, so every site includes the same set and blocks can use only those. Besides `react`, `@goodfellow-cms/react` and `@puckeditor/core`, which every site has, they are `class-variance-authority`, `clsx`, `cn`, `embla-carousel-react`, `lucide-react`, `radix-ui`, `tailwind-merge` and `tw-animate-css`. An item whose `dependencies` name anything else can't be added. To suggest a package for the list, open an issue.
75
+
76
+ ### Writing the blocks
77
+
78
+ Registry blocks follow the same rules as any Goodfellow block (see [AGENTS.md](https://github.com/goodfellow-cms/goodfellow-cms/blob/master/AGENTS.md#blocks)):
79
+
80
+ - Every block has a `className` prop (`className: classNameField`), applied to its outermost element and combined with `cx()` from `@goodfellow-cms/react`, last.
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
+ - 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
+ - Labels and options are for non-technical people: "Space above and below", not "padding-y".
84
+ - `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.
85
+ - **One Client Component per interactive part.** Sites built with `goodfellow build` run each Client Component a block renders as its own small React app (an island), and islands don't share React context. Components whose parts talk to each other through context, as Radix's `Accordion`, `AccordionItem` and `AccordionContent` do, must all be put together inside one `"use client"` file, which the block renders with its content as props. See `site/blocks/installed/shadcn-faq/` here.
86
+ - **Keep content in the page.** Hidden content, such as closed answers or other tabs, should still be in the HTML (Radix's `forceMount`, hidden with CSS), so search engines and visitors without JavaScript see it.
87
+ - Prefer HTML and CSS where they're enough: a `<details>` element rather than a Client Component, since a page with a Client Component loads React.
88
+
89
+ ### Letting a site use it
90
+
91
+ Admins can only add blocks from registries the site's developer trusts, since a block's code runs in the admin panel with the editor's access to the site. Add yours to the site's `goodfellow.config.tsx`, and its blocks appear on the Blocks screen beside Goodfellow's:
92
+
93
+ ```tsx
94
+ export default defineConfig({
95
+ blocks: { ...blocks, ...installedBlocks },
96
+ registries: { "@acme": "https://acme.example/r/{name}.json" },
97
+ });
98
+ ```
99
+
100
+ ## Working on this registry
101
+
102
+ - `registry.json` lists the blocks, whose files are in `site/blocks/installed/`, laid out as they'll be in a site.
103
+ - `site/components/ui/` and `shadcn.json` are a snapshot of the shadcn/ui components the blocks use, so a release always installs the same code. `pnpm --filter @goodfellow-cms/registry update-shadcn` (`scripts/snapshot.mjs`) fetches them again; review and commit what it writes. Don't edit the snapshot by hand.
104
+ - `pnpm build` (`scripts/build.mjs`) writes `r/`, which is what's published: an item per block and component, with its files, and `r/registry.json`.
105
+ - The tests check that every item is valid, uses only the allowed packages, installs with every file it imports, and renders.
106
+
107
+ The shadcn/ui components are © shadcn, under the MIT license in `LICENSE-shadcn.md`.
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@goodfellow-cms/registry",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Goodfellow's block registry: blocks built with shadcn/ui, which sites install from the admin panel.",
5
+ "keywords": [
6
+ "goodfellow",
7
+ "cms",
8
+ "website-builder",
9
+ "puck",
10
+ "shadcn",
11
+ "shadcn-registry",
12
+ "blocks"
13
+ ],
14
+ "homepage": "https://goodfellow-cms.github.io/goodfellow-cms/docs/block-registries",
15
+ "bugs": "https://github.com/goodfellow-cms/goodfellow-cms/issues",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/goodfellow-cms/goodfellow-cms.git",
20
+ "directory": "packages/registry"
21
+ },
22
+ "type": "module",
23
+ "files": [
24
+ "r",
25
+ "LICENSE-shadcn.md"
26
+ ],
27
+ "exports": {
28
+ "./package.json": "./package.json"
29
+ },
30
+ "devDependencies": {
31
+ "@puckeditor/core": "0.23.0",
32
+ "@types/react": "^19.2.0",
33
+ "@types/react-dom": "^19.2.0",
34
+ "class-variance-authority": "0.7.1",
35
+ "cn": "0.4.0",
36
+ "embla-carousel-react": "8.6.0",
37
+ "lucide-react": "1.52.0",
38
+ "radix-ui": "1.7.0",
39
+ "react": "19.3.0",
40
+ "react-dom": "19.3.0",
41
+ "@goodfellow-cms/core": "0.2.0",
42
+ "@goodfellow-cms/react": "0.2.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=22"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "node scripts/build.mjs",
52
+ "typecheck": "tsc -p tsconfig.json",
53
+ "test": "vitest run",
54
+ "update-shadcn": "node scripts/snapshot.mjs"
55
+ }
6
56
  }
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "accordion",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn",
7
+ "radix-ui"
8
+ ],
9
+ "registryDependencies": [],
10
+ "files": [
11
+ {
12
+ "path": "components/ui/accordion.tsx",
13
+ "type": "registry:ui",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport { ChevronDownIcon } from \"lucide-react\"\nimport { Accordion as AccordionPrimitive } from \"radix-ui\"\n\nfunction Accordion({\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />\n}\n\nfunction AccordionItem({\n className,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n return (\n <AccordionPrimitive.Item\n data-slot=\"accordion-item\"\n className={cn(\"border-b last:border-b-0\", className)}\n {...props}\n />\n )\n}\n\nfunction AccordionTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n data-slot=\"accordion-trigger\"\n className={cn(\n \"flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronDownIcon className=\"pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n )\n}\n\nfunction AccordionContent({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n return (\n <AccordionPrimitive.Content\n data-slot=\"accordion-content\"\n className=\"overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\"\n {...props}\n >\n <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n )\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n"
15
+ }
16
+ ]
17
+ }
package/r/alert.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "alert",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn"
7
+ ],
8
+ "registryDependencies": [],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/alert.tsx",
12
+ "type": "registry:ui",
13
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\n\nconst alertVariants = cva(\n \"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n destructive:\n \"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Alert({\n className,\n variant,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n return (\n <div\n data-slot=\"alert\"\n role=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\n \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Alert, AlertTitle, AlertDescription }\n"
14
+ }
15
+ ]
16
+ }
package/r/badge.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "badge",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn",
7
+ "radix-ui"
8
+ ],
9
+ "registryDependencies": [],
10
+ "files": [
11
+ {
12
+ "path": "components/ui/badge.tsx",
13
+ "type": "registry:ui",
14
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Slot } from \"radix-ui\"\n\nconst badgeVariants = cva(\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90\",\n outline:\n \"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n ghost: \"[a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 [a&]:hover:underline\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot.Root : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n data-variant={variant}\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n"
15
+ }
16
+ ]
17
+ }
package/r/button.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "button",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn",
7
+ "radix-ui"
8
+ ],
9
+ "registryDependencies": [],
10
+ "files": [
11
+ {
12
+ "path": "components/ui/button.tsx",
13
+ "type": "registry:ui",
14
+ "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Slot } from \"radix-ui\"\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot.Root : \"button\"\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"
15
+ }
16
+ ]
17
+ }
package/r/card.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "card",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn"
7
+ ],
8
+ "registryDependencies": [],
9
+ "files": [
10
+ {
11
+ "path": "components/ui/card.tsx",
12
+ "type": "registry:ui",
13
+ "content": "import * as React from \"react\"\nimport { cn } from \"cn\"\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card\"\n className={cn(\n \"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"leading-none font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\n \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-content\"\n className={cn(\"px-6\", className)}\n {...props}\n />\n )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardAction,\n CardDescription,\n CardContent,\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "carousel",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn",
7
+ "embla-carousel-react"
8
+ ],
9
+ "registryDependencies": [
10
+ "@goodfellow/button"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "components/ui/carousel.tsx",
15
+ "type": "registry:ui",
16
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"cn\"\nimport useEmblaCarousel, {\n type UseEmblaCarouselType,\n} from \"embla-carousel-react\"\nimport { ArrowLeft, ArrowRight } from \"lucide-react\"\n\nimport { Button } from \"@/components/ui/button\"\n\ntype CarouselApi = UseEmblaCarouselType[1]\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>\ntype CarouselOptions = UseCarouselParameters[0]\ntype CarouselPlugin = UseCarouselParameters[1]\n\ntype CarouselProps = {\n opts?: CarouselOptions\n plugins?: CarouselPlugin\n orientation?: \"horizontal\" | \"vertical\"\n setApi?: (api: CarouselApi) => void\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof useEmblaCarousel>[0]\n api: ReturnType<typeof useEmblaCarousel>[1]\n scrollPrev: () => void\n scrollNext: () => void\n canScrollPrev: boolean\n canScrollNext: boolean\n} & CarouselProps\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null)\n\nfunction useCarousel() {\n const context = React.useContext(CarouselContext)\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\")\n }\n\n return context\n}\n\nfunction Carousel({\n orientation = \"horizontal\",\n opts,\n setApi,\n plugins,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n const [carouselRef, api] = useEmblaCarousel(\n {\n ...opts,\n axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n },\n plugins\n )\n const [canScrollPrev, setCanScrollPrev] = React.useState(false)\n const [canScrollNext, setCanScrollNext] = React.useState(false)\n\n const onSelect = React.useCallback((api: CarouselApi) => {\n if (!api) return\n setCanScrollPrev(api.canScrollPrev())\n setCanScrollNext(api.canScrollNext())\n }, [])\n\n const scrollPrev = React.useCallback(() => {\n api?.scrollPrev()\n }, [api])\n\n const scrollNext = React.useCallback(() => {\n api?.scrollNext()\n }, [api])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault()\n scrollPrev()\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault()\n scrollNext()\n }\n },\n [scrollPrev, scrollNext]\n )\n\n React.useEffect(() => {\n if (!api || !setApi) return\n setApi(api)\n }, [api, setApi])\n\n React.useEffect(() => {\n if (!api) return\n onSelect(api)\n api.on(\"reInit\", onSelect)\n api.on(\"select\", onSelect)\n\n return () => {\n api?.off(\"select\", onSelect)\n }\n }, [api, onSelect])\n\n return (\n <CarouselContext.Provider\n value={{\n carouselRef,\n api: api,\n opts,\n orientation:\n orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext,\n }}\n >\n <div\n onKeyDownCapture={handleKeyDown}\n className={cn(\"relative\", className)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n data-slot=\"carousel\"\n {...props}\n >\n {children}\n </div>\n </CarouselContext.Provider>\n )\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n const { carouselRef, orientation } = useCarousel()\n\n return (\n <div\n ref={carouselRef}\n className=\"overflow-hidden\"\n data-slot=\"carousel-content\"\n >\n <div\n className={cn(\n \"flex\",\n orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const { orientation } = useCarousel()\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n data-slot=\"carousel-item\"\n className={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CarouselPrevious({\n className,\n variant = \"outline\",\n size = \"icon\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { orientation, scrollPrev, canScrollPrev } = useCarousel()\n\n return (\n <Button\n data-slot=\"carousel-previous\"\n variant={variant}\n size={size}\n className={cn(\n \"absolute size-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"top-1/2 -left-12 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollPrev}\n onClick={scrollPrev}\n {...props}\n >\n <ArrowLeft />\n <span className=\"sr-only\">Previous slide</span>\n </Button>\n )\n}\n\nfunction CarouselNext({\n className,\n variant = \"outline\",\n size = \"icon\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { orientation, scrollNext, canScrollNext } = useCarousel()\n\n return (\n <Button\n data-slot=\"carousel-next\"\n variant={variant}\n size={size}\n className={cn(\n \"absolute size-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"top-1/2 -right-12 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollNext}\n onClick={scrollNext}\n {...props}\n >\n <ArrowRight />\n <span className=\"sr-only\">Next slide</span>\n </Button>\n )\n}\n\nexport {\n type CarouselApi,\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselPrevious,\n CarouselNext,\n}\n"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "custom-html",
4
+ "type": "registry:block",
5
+ "title": "Custom HTML",
6
+ "description": "HTML written by hand, such as a widget's embed code. Made safe unless you choose to use it exactly as written.",
7
+ "files": [
8
+ {
9
+ "path": "blocks/installed/custom-html/block.tsx",
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 only inside a\n * sandboxed frame, where it can't reach the admin panel. Its scripts run\n * whenever its page is shown, also 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"
12
+ },
13
+ {
14
+ "path": "blocks/installed/custom-html/html-preview.tsx",
15
+ "type": "registry:component",
16
+ "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\n/** Tells the editor how tall the HTML is, so the frame fits it. */\nconst REPORT_HEIGHT =\n \"<script>new ResizeObserver(function(){parent.postMessage({goodfellowHtmlHeight:document.documentElement.scrollHeight},'*')}).observe(document.documentElement)</script>\";\n\n/**\n * Shows HTML as it was written, in the editor only: inside a frame sandboxed\n * without `allow-same-origin`, so its scripts run on their own and can't reach\n * the admin panel or the editor's sign-in. It has the page's styles.\n */\nexport function HtmlPreview({ html, className }: { html: string; className: string }) {\n const frame = useRef<HTMLIFrameElement>(null);\n const [height, setHeight] = useState(120);\n const [styles, setStyles] = useState(\"\");\n\n useEffect(() => {\n const document = frame.current?.ownerDocument;\n const view = document?.defaultView;\n if (!document || !view) return;\n setStyles(\n [...document.querySelectorAll('link[rel=\"stylesheet\"], style')].map((element) => element.outerHTML).join(\"\"),\n );\n const onMessage = (event: MessageEvent) => {\n const reported = (event.data as { goodfellowHtmlHeight?: unknown } | null)?.goodfellowHtmlHeight;\n if (event.source === frame.current?.contentWindow && typeof reported === \"number\") setHeight(Math.ceil(reported));\n };\n view.addEventListener(\"message\", onMessage);\n return () => view.removeEventListener(\"message\", onMessage);\n }, []);\n\n return (\n <iframe\n ref={frame}\n title=\"Custom HTML\"\n sandbox=\"allow-scripts\"\n srcDoc={`<!doctype html><html><head><meta charset=\"utf-8\">${styles}</head><body style=\"margin:0\">${html}${REPORT_HEIGHT}</body></html>`}\n className={className || undefined}\n style={{ display: \"block\", width: \"100%\", height, border: 0 }}\n />\n );\n}\n"
17
+ },
18
+ {
19
+ "path": "blocks/installed/custom-html/html-scripts.tsx",
20
+ "type": "registry:component",
21
+ "content": "\"use client\";\n\nimport { useEffect, useRef, useSyncExternalStore } from \"react\";\n\n/** Scripts that have already run, so a second pass (React's Strict Mode runs effects twice) doesn't run them again. */\nconst ran = new WeakSet<Element>();\n\nconst noSubscription = () => () => {};\n\n/** Runs a script that was put in the page as HTML, which browsers never run on their own. */\nfunction run(original: HTMLScriptElement): Promise<void> {\n const script = document.createElement(\"script\");\n for (const { name, value } of original.attributes) script.setAttribute(name, value);\n script.text = original.text;\n ran.add(script);\n // An external script runs once it has loaded; the next one waits for it, as when the page is first opened.\n const waits = Boolean(original.src) && !original.async && original.type !== \"module\";\n const loaded = waits\n ? new Promise<void>((resolve) => {\n script.addEventListener(\"load\", () => resolve());\n script.addEventListener(\"error\", () => resolve());\n })\n : Promise.resolve();\n if (waits) script.async = false;\n original.replaceWith(script);\n return loaded;\n}\n\n/**\n * HTML with scripts, used as it was written. When the page is first opened\n * the browser runs its scripts; when it's shown without a new page load (moving\n * between pages on a Next.js site), this runs them, in order.\n */\nexport function HtmlWithScripts({ html, className }: { html: string; className: string }) {\n const container = useRef<HTMLDivElement>(null);\n // True when React takes over HTML the browser loaded with the page, whose scripts have already run.\n const hydrating = useSyncExternalStore(\n noSubscription,\n () => false,\n () => true,\n );\n const fromPage = useRef(hydrating);\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: runs the scripts again whenever the HTML changes\n useEffect(() => {\n const scripts = [...(container.current?.querySelectorAll(\"script\") ?? [])].filter((script) => !ran.has(script));\n if (fromPage.current) {\n fromPage.current = false;\n for (const script of scripts) ran.add(script);\n return;\n }\n let cancelled = false;\n (async () => {\n for (const script of scripts) {\n if (cancelled) return;\n await run(script);\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [html]);\n\n return (\n <div\n ref={container}\n className={className || undefined}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: the editor chose to use this HTML as written\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n"
22
+ }
23
+ ],
24
+ "meta": {
25
+ "goodfellow": {
26
+ "category": "Advanced",
27
+ "version": "0.2.0"
28
+ }
29
+ },
30
+ "dependencies": []
31
+ }
@@ -0,0 +1,364 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry.json",
3
+ "name": "goodfellow",
4
+ "homepage": "https://github.com/goodfellow-cms/goodfellow-cms",
5
+ "items": [
6
+ {
7
+ "name": "shadcn-hero",
8
+ "type": "registry:block",
9
+ "title": "Hero",
10
+ "description": "A large heading at the top of a page, with text, buttons and a picture.",
11
+ "registryDependencies": [
12
+ "@goodfellow/button"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "blocks/installed/shadcn-hero/block.tsx",
17
+ "type": "registry:block"
18
+ }
19
+ ],
20
+ "meta": {
21
+ "goodfellow": {
22
+ "category": "Page sections",
23
+ "recommended": true,
24
+ "version": "0.2.0"
25
+ }
26
+ },
27
+ "dependencies": []
28
+ },
29
+ {
30
+ "name": "shadcn-cards",
31
+ "type": "registry:block",
32
+ "title": "Cards",
33
+ "description": "A grid of cards, each with a picture, a heading, text and a link.",
34
+ "registryDependencies": [
35
+ "@goodfellow/card",
36
+ "@goodfellow/button"
37
+ ],
38
+ "files": [
39
+ {
40
+ "path": "blocks/installed/shadcn-cards/block.tsx",
41
+ "type": "registry:block"
42
+ }
43
+ ],
44
+ "meta": {
45
+ "goodfellow": {
46
+ "category": "Page sections",
47
+ "recommended": true,
48
+ "version": "0.2.0"
49
+ }
50
+ },
51
+ "dependencies": []
52
+ },
53
+ {
54
+ "name": "shadcn-call-to-action",
55
+ "type": "registry:block",
56
+ "title": "Call to action",
57
+ "description": "A highlighted box asking visitors to do something, with a button.",
58
+ "registryDependencies": [
59
+ "@goodfellow/card",
60
+ "@goodfellow/button"
61
+ ],
62
+ "files": [
63
+ {
64
+ "path": "blocks/installed/shadcn-call-to-action/block.tsx",
65
+ "type": "registry:block"
66
+ }
67
+ ],
68
+ "meta": {
69
+ "goodfellow": {
70
+ "category": "Page sections",
71
+ "recommended": true,
72
+ "version": "0.2.0"
73
+ }
74
+ },
75
+ "dependencies": []
76
+ },
77
+ {
78
+ "name": "shadcn-testimonials",
79
+ "type": "registry:block",
80
+ "title": "Testimonials",
81
+ "description": "Quotes from people, with their names and photos.",
82
+ "registryDependencies": [
83
+ "@goodfellow/card"
84
+ ],
85
+ "files": [
86
+ {
87
+ "path": "blocks/installed/shadcn-testimonials/block.tsx",
88
+ "type": "registry:block"
89
+ }
90
+ ],
91
+ "meta": {
92
+ "goodfellow": {
93
+ "category": "Page sections",
94
+ "recommended": true,
95
+ "version": "0.2.0"
96
+ }
97
+ },
98
+ "dependencies": []
99
+ },
100
+ {
101
+ "name": "shadcn-pricing",
102
+ "type": "registry:block",
103
+ "title": "Pricing table",
104
+ "description": "Plans side by side, with prices, what each includes and a button.",
105
+ "registryDependencies": [
106
+ "@goodfellow/card",
107
+ "@goodfellow/badge",
108
+ "@goodfellow/button"
109
+ ],
110
+ "dependencies": [
111
+ "lucide-react"
112
+ ],
113
+ "files": [
114
+ {
115
+ "path": "blocks/installed/shadcn-pricing/block.tsx",
116
+ "type": "registry:block"
117
+ }
118
+ ],
119
+ "meta": {
120
+ "goodfellow": {
121
+ "category": "Page sections",
122
+ "version": "0.2.0"
123
+ }
124
+ }
125
+ },
126
+ {
127
+ "name": "shadcn-notice",
128
+ "type": "registry:block",
129
+ "title": "Notice",
130
+ "description": "A short message that stands out, such as a closure or a change of times.",
131
+ "registryDependencies": [
132
+ "@goodfellow/alert"
133
+ ],
134
+ "dependencies": [
135
+ "lucide-react"
136
+ ],
137
+ "files": [
138
+ {
139
+ "path": "blocks/installed/shadcn-notice/block.tsx",
140
+ "type": "registry:block"
141
+ }
142
+ ],
143
+ "meta": {
144
+ "goodfellow": {
145
+ "category": "Page sections",
146
+ "recommended": true,
147
+ "version": "0.2.0"
148
+ }
149
+ }
150
+ },
151
+ {
152
+ "name": "custom-html",
153
+ "type": "registry:block",
154
+ "title": "Custom HTML",
155
+ "description": "HTML written by hand, such as a widget's embed code. Made safe unless you choose to use it exactly as written.",
156
+ "files": [
157
+ {
158
+ "path": "blocks/installed/custom-html/block.tsx",
159
+ "type": "registry:block"
160
+ },
161
+ {
162
+ "path": "blocks/installed/custom-html/html-preview.tsx",
163
+ "type": "registry:component"
164
+ },
165
+ {
166
+ "path": "blocks/installed/custom-html/html-scripts.tsx",
167
+ "type": "registry:component"
168
+ }
169
+ ],
170
+ "meta": {
171
+ "goodfellow": {
172
+ "category": "Advanced",
173
+ "version": "0.2.0"
174
+ }
175
+ },
176
+ "dependencies": []
177
+ },
178
+ {
179
+ "name": "shadcn-faq",
180
+ "type": "registry:block",
181
+ "title": "FAQ",
182
+ "description": "Questions that open to show their answers.",
183
+ "registryDependencies": [
184
+ "@goodfellow/accordion"
185
+ ],
186
+ "files": [
187
+ {
188
+ "path": "blocks/installed/shadcn-faq/block.tsx",
189
+ "type": "registry:block"
190
+ },
191
+ {
192
+ "path": "blocks/installed/shadcn-faq/faq-list.tsx",
193
+ "type": "registry:component"
194
+ }
195
+ ],
196
+ "meta": {
197
+ "goodfellow": {
198
+ "category": "Interactive",
199
+ "recommended": true,
200
+ "version": "0.2.0"
201
+ }
202
+ },
203
+ "dependencies": []
204
+ },
205
+ {
206
+ "name": "shadcn-tabs",
207
+ "type": "registry:block",
208
+ "title": "Tabs",
209
+ "description": "Content split into tabs, one shown at a time.",
210
+ "registryDependencies": [
211
+ "@goodfellow/tabs"
212
+ ],
213
+ "files": [
214
+ {
215
+ "path": "blocks/installed/shadcn-tabs/block.tsx",
216
+ "type": "registry:block"
217
+ },
218
+ {
219
+ "path": "blocks/installed/shadcn-tabs/tabs-view.tsx",
220
+ "type": "registry:component"
221
+ }
222
+ ],
223
+ "meta": {
224
+ "goodfellow": {
225
+ "category": "Interactive",
226
+ "recommended": true,
227
+ "version": "0.2.0"
228
+ }
229
+ },
230
+ "dependencies": []
231
+ },
232
+ {
233
+ "name": "shadcn-carousel",
234
+ "type": "registry:block",
235
+ "title": "Image carousel",
236
+ "description": "Pictures shown one at a time, with buttons to move between them.",
237
+ "registryDependencies": [
238
+ "@goodfellow/carousel"
239
+ ],
240
+ "files": [
241
+ {
242
+ "path": "blocks/installed/shadcn-carousel/block.tsx",
243
+ "type": "registry:block"
244
+ },
245
+ {
246
+ "path": "blocks/installed/shadcn-carousel/image-carousel.tsx",
247
+ "type": "registry:component"
248
+ }
249
+ ],
250
+ "meta": {
251
+ "goodfellow": {
252
+ "category": "Interactive",
253
+ "version": "0.2.0"
254
+ }
255
+ },
256
+ "dependencies": []
257
+ },
258
+ {
259
+ "name": "accordion",
260
+ "type": "registry:ui",
261
+ "dependencies": [
262
+ "cn",
263
+ "radix-ui"
264
+ ],
265
+ "registryDependencies": [],
266
+ "files": [
267
+ {
268
+ "path": "components/ui/accordion.tsx",
269
+ "type": "registry:ui"
270
+ }
271
+ ]
272
+ },
273
+ {
274
+ "name": "alert",
275
+ "type": "registry:ui",
276
+ "dependencies": [
277
+ "cn"
278
+ ],
279
+ "registryDependencies": [],
280
+ "files": [
281
+ {
282
+ "path": "components/ui/alert.tsx",
283
+ "type": "registry:ui"
284
+ }
285
+ ]
286
+ },
287
+ {
288
+ "name": "badge",
289
+ "type": "registry:ui",
290
+ "dependencies": [
291
+ "cn",
292
+ "radix-ui"
293
+ ],
294
+ "registryDependencies": [],
295
+ "files": [
296
+ {
297
+ "path": "components/ui/badge.tsx",
298
+ "type": "registry:ui"
299
+ }
300
+ ]
301
+ },
302
+ {
303
+ "name": "button",
304
+ "type": "registry:ui",
305
+ "dependencies": [
306
+ "cn",
307
+ "radix-ui"
308
+ ],
309
+ "registryDependencies": [],
310
+ "files": [
311
+ {
312
+ "path": "components/ui/button.tsx",
313
+ "type": "registry:ui"
314
+ }
315
+ ]
316
+ },
317
+ {
318
+ "name": "card",
319
+ "type": "registry:ui",
320
+ "dependencies": [
321
+ "cn"
322
+ ],
323
+ "registryDependencies": [],
324
+ "files": [
325
+ {
326
+ "path": "components/ui/card.tsx",
327
+ "type": "registry:ui"
328
+ }
329
+ ]
330
+ },
331
+ {
332
+ "name": "carousel",
333
+ "type": "registry:ui",
334
+ "dependencies": [
335
+ "cn",
336
+ "embla-carousel-react"
337
+ ],
338
+ "registryDependencies": [
339
+ "@goodfellow/button"
340
+ ],
341
+ "files": [
342
+ {
343
+ "path": "components/ui/carousel.tsx",
344
+ "type": "registry:ui"
345
+ }
346
+ ]
347
+ },
348
+ {
349
+ "name": "tabs",
350
+ "type": "registry:ui",
351
+ "dependencies": [
352
+ "cn",
353
+ "radix-ui"
354
+ ],
355
+ "registryDependencies": [],
356
+ "files": [
357
+ {
358
+ "path": "components/ui/tabs.tsx",
359
+ "type": "registry:ui"
360
+ }
361
+ ]
362
+ }
363
+ ]
364
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-call-to-action",
4
+ "type": "registry:block",
5
+ "title": "Call to action",
6
+ "description": "A highlighted box asking visitors to do something, with a button.",
7
+ "registryDependencies": [
8
+ "@goodfellow/card",
9
+ "@goodfellow/button"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "blocks/installed/shadcn-call-to-action/block.tsx",
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"
16
+ }
17
+ ],
18
+ "meta": {
19
+ "goodfellow": {
20
+ "category": "Page sections",
21
+ "recommended": true,
22
+ "version": "0.2.0"
23
+ }
24
+ },
25
+ "dependencies": []
26
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-cards",
4
+ "type": "registry:block",
5
+ "title": "Cards",
6
+ "description": "A grid of cards, each with a picture, a heading, text and a link.",
7
+ "registryDependencies": [
8
+ "@goodfellow/card",
9
+ "@goodfellow/button"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "blocks/installed/shadcn-cards/block.tsx",
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"
16
+ }
17
+ ],
18
+ "meta": {
19
+ "goodfellow": {
20
+ "category": "Page sections",
21
+ "recommended": true,
22
+ "version": "0.2.0"
23
+ }
24
+ },
25
+ "dependencies": []
26
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-carousel",
4
+ "type": "registry:block",
5
+ "title": "Image carousel",
6
+ "description": "Pictures shown one at a time, with buttons to move between them.",
7
+ "registryDependencies": [
8
+ "@goodfellow/carousel"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "blocks/installed/shadcn-carousel/block.tsx",
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"
15
+ },
16
+ {
17
+ "path": "blocks/installed/shadcn-carousel/image-carousel.tsx",
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"
20
+ }
21
+ ],
22
+ "meta": {
23
+ "goodfellow": {
24
+ "category": "Interactive",
25
+ "version": "0.2.0"
26
+ }
27
+ },
28
+ "dependencies": []
29
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-faq",
4
+ "type": "registry:block",
5
+ "title": "FAQ",
6
+ "description": "Questions that open to show their answers.",
7
+ "registryDependencies": [
8
+ "@goodfellow/accordion"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "blocks/installed/shadcn-faq/block.tsx",
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"
15
+ },
16
+ {
17
+ "path": "blocks/installed/shadcn-faq/faq-list.tsx",
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"
20
+ }
21
+ ],
22
+ "meta": {
23
+ "goodfellow": {
24
+ "category": "Interactive",
25
+ "recommended": true,
26
+ "version": "0.2.0"
27
+ }
28
+ },
29
+ "dependencies": []
30
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-hero",
4
+ "type": "registry:block",
5
+ "title": "Hero",
6
+ "description": "A large heading at the top of a page, with text, buttons and a picture.",
7
+ "registryDependencies": [
8
+ "@goodfellow/button"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "blocks/installed/shadcn-hero/block.tsx",
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"
15
+ }
16
+ ],
17
+ "meta": {
18
+ "goodfellow": {
19
+ "category": "Page sections",
20
+ "recommended": true,
21
+ "version": "0.2.0"
22
+ }
23
+ },
24
+ "dependencies": []
25
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-notice",
4
+ "type": "registry:block",
5
+ "title": "Notice",
6
+ "description": "A short message that stands out, such as a closure or a change of times.",
7
+ "registryDependencies": [
8
+ "@goodfellow/alert"
9
+ ],
10
+ "dependencies": [
11
+ "lucide-react"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "blocks/installed/shadcn-notice/block.tsx",
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"
18
+ }
19
+ ],
20
+ "meta": {
21
+ "goodfellow": {
22
+ "category": "Page sections",
23
+ "recommended": true,
24
+ "version": "0.2.0"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-pricing",
4
+ "type": "registry:block",
5
+ "title": "Pricing table",
6
+ "description": "Plans side by side, with prices, what each includes and a button.",
7
+ "registryDependencies": [
8
+ "@goodfellow/card",
9
+ "@goodfellow/badge",
10
+ "@goodfellow/button"
11
+ ],
12
+ "dependencies": [
13
+ "lucide-react"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "blocks/installed/shadcn-pricing/block.tsx",
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"
20
+ }
21
+ ],
22
+ "meta": {
23
+ "goodfellow": {
24
+ "category": "Page sections",
25
+ "version": "0.2.0"
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-tabs",
4
+ "type": "registry:block",
5
+ "title": "Tabs",
6
+ "description": "Content split into tabs, one shown at a time.",
7
+ "registryDependencies": [
8
+ "@goodfellow/tabs"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "blocks/installed/shadcn-tabs/block.tsx",
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"
15
+ },
16
+ {
17
+ "path": "blocks/installed/shadcn-tabs/tabs-view.tsx",
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"
20
+ }
21
+ ],
22
+ "meta": {
23
+ "goodfellow": {
24
+ "category": "Interactive",
25
+ "recommended": true,
26
+ "version": "0.2.0"
27
+ }
28
+ },
29
+ "dependencies": []
30
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "shadcn-testimonials",
4
+ "type": "registry:block",
5
+ "title": "Testimonials",
6
+ "description": "Quotes from people, with their names and photos.",
7
+ "registryDependencies": [
8
+ "@goodfellow/card"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "blocks/installed/shadcn-testimonials/block.tsx",
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"
15
+ }
16
+ ],
17
+ "meta": {
18
+ "goodfellow": {
19
+ "category": "Page sections",
20
+ "recommended": true,
21
+ "version": "0.2.0"
22
+ }
23
+ },
24
+ "dependencies": []
25
+ }
package/r/tabs.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "tabs",
4
+ "type": "registry:ui",
5
+ "dependencies": [
6
+ "cn",
7
+ "radix-ui"
8
+ ],
9
+ "registryDependencies": [],
10
+ "files": [
11
+ {
12
+ "path": "components/ui/tabs.tsx",
13
+ "type": "registry:ui",
14
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\nimport { Tabs as TabsPrimitive } from \"radix-ui\"\n\nfunction Tabs({\n className,\n orientation = \"horizontal\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n data-orientation={orientation}\n orientation={orientation}\n className={cn(\n \"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst tabsListVariants = cva(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none\",\n {\n variants: {\n variant: {\n default: \"bg-muted\",\n line: \"gap-1 bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction TabsList({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List> &\n VariantProps<typeof tabsListVariants>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n data-variant={variant}\n className={cn(tabsListVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n \"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent\",\n \"data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground\",\n \"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }\n"
15
+ }
16
+ ]
17
+ }