create-goodfellow 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 +21 -0
- package/README.md +15 -2
- package/bin/create-goodfellow.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +220 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +2 -0
- package/dist/scaffold-BGxzPdN1.js +150 -0
- package/package.json +55 -3
- package/templates/next/.github/workflows/deploy.yml +86 -0
- package/templates/next/.gitlab-ci.yml +28 -0
- package/templates/next/README.md +127 -0
- package/templates/next/_gitignore +5 -0
- package/templates/next/app/(site)/[[...path]]/page.tsx +8 -0
- package/templates/next/app/(site)/layout.tsx +5 -0
- package/templates/next/app/admin/demo-content.json/route.ts +5 -0
- package/templates/next/app/admin/layout.tsx +8 -0
- package/templates/next/app/admin/page.tsx +8 -0
- package/templates/next/app/layout.tsx +11 -0
- package/templates/next/app/not-found.tsx +4 -0
- package/templates/next/app/robots.ts +4 -0
- package/templates/next/app/site.css +11 -0
- package/templates/next/app/sitemap.ts +4 -0
- package/templates/next/blocks/installed/index.ts +7 -0
- package/templates/next/components.json +24 -0
- package/templates/next/content/collections/news/_collection.json +113 -0
- package/templates/next/content/collections/news/open-house.json +9 -0
- package/templates/next/content/collections/news/welcome.json +10 -0
- package/templates/next/content/layout/footer.json +55 -0
- package/templates/next/content/layout/header.json +54 -0
- package/templates/next/content/menus.json +25 -0
- package/templates/next/content/pages/404.json +73 -0
- package/templates/next/content/pages/about.json +59 -0
- package/templates/next/content/pages/index.json +215 -0
- package/templates/next/content/pages/news.json +55 -0
- package/templates/next/content/site.json +25 -0
- package/templates/next/content/styles/custom.css +4 -0
- package/templates/next/goodfellow.config.tsx +17 -0
- package/templates/next/lib/site.ts +5 -0
- package/templates/next/next.config.ts +7 -0
- package/templates/next/package.json +40 -0
- package/templates/next/postcss.config.mjs +3 -0
- package/templates/next/public/media/favicon.svg +1 -0
- package/templates/next/public/media/logo.svg +1 -0
- package/templates/next/public/media/placeholder.svg +1 -0
- package/templates/next/tsconfig.json +27 -0
- package/templates/parish/.github/workflows/deploy.yml +84 -0
- package/templates/parish/.gitlab-ci.yml +28 -0
- package/templates/parish/README.md +128 -0
- package/templates/parish/_gitignore +2 -0
- package/templates/parish/blocks/installed/index.ts +7 -0
- package/templates/parish/blocks/mass-times.tsx +59 -0
- package/templates/parish/blocks/notice.tsx +46 -0
- package/templates/parish/blocks/scripture.tsx +40 -0
- package/templates/parish/components.json +24 -0
- package/templates/parish/content/collections/bulletins/2026-09-27.json +8 -0
- package/templates/parish/content/collections/bulletins/2026-10-04.json +8 -0
- package/templates/parish/content/collections/bulletins/_collection.json +113 -0
- package/templates/parish/content/collections/events/_collection.json +156 -0
- package/templates/parish/content/collections/events/advent-penance-service.json +14 -0
- package/templates/parish/content/collections/events/all-souls.json +14 -0
- package/templates/parish/content/collections/events/christmas-concert.json +14 -0
- package/templates/parish/content/collections/events/fall-festival.json +14 -0
- package/templates/parish/content/collections/events/youth-group.json +23 -0
- package/templates/parish/content/collections/news/_collection.json +137 -0
- package/templates/parish/content/collections/news/bell-tower-repairs.json +13 -0
- package/templates/parish/content/collections/news/choir-new-voices.json +14 -0
- package/templates/parish/content/collections/news/food-drive.json +13 -0
- package/templates/parish/content/collections/news/new-website.json +13 -0
- package/templates/parish/content/collections/news/winter-coats.json +13 -0
- package/templates/parish/content/collections/news/youth-group.json +13 -0
- package/templates/parish/content/collections/news/youth-service-day.json +14 -0
- package/templates/parish/content/collections/staff/_collection.json +34 -0
- package/templates/parish/content/collections/staff/david-okafor.json +9 -0
- package/templates/parish/content/collections/staff/fr-thomas-reed.json +9 -0
- package/templates/parish/content/collections/staff/maria-chen.json +9 -0
- package/templates/parish/content/collections/staff/sarah-lindqvist.json +9 -0
- package/templates/parish/content/layout/footer.json +142 -0
- package/templates/parish/content/layout/header.json +54 -0
- package/templates/parish/content/menus.json +67 -0
- package/templates/parish/content/pages/404.json +85 -0
- package/templates/parish/content/pages/about.json +153 -0
- package/templates/parish/content/pages/bulletins.json +64 -0
- package/templates/parish/content/pages/calendar.json +59 -0
- package/templates/parish/content/pages/contact.json +180 -0
- package/templates/parish/content/pages/events.json +107 -0
- package/templates/parish/content/pages/index.json +390 -0
- package/templates/parish/content/pages/mass-times.json +168 -0
- package/templates/parish/content/pages/news.json +68 -0
- package/templates/parish/content/pages/sacraments.json +259 -0
- package/templates/parish/content/site.json +36 -0
- package/templates/parish/content/styles/custom.css +4 -0
- package/templates/parish/goodfellow.config.tsx +24 -0
- package/templates/parish/package.json +36 -0
- package/templates/parish/public/media/bulletin-example.pdf +32 -0
- package/templates/parish/public/media/candles.svg +22 -0
- package/templates/parish/public/media/church.svg +33 -0
- package/templates/parish/public/media/favicon.svg +5 -0
- package/templates/parish/public/media/gathering.svg +18 -0
- package/templates/parish/public/media/logo.svg +5 -0
- package/templates/parish/public/media/person.svg +5 -0
- package/templates/parish/public/media/placeholder.svg +5 -0
- package/templates/parish/src/styles.css +8 -0
- package/templates/parish/tsconfig.json +14 -0
- package/templates/parish/vercel.ts +65 -0
- package/templates/registry/accordion.json +17 -0
- package/templates/registry/alert.json +16 -0
- package/templates/registry/badge.json +17 -0
- package/templates/registry/button.json +17 -0
- package/templates/registry/card.json +16 -0
- package/templates/registry/carousel.json +19 -0
- package/templates/registry/custom-html.json +31 -0
- package/templates/registry/registry.json +364 -0
- package/templates/registry/shadcn-call-to-action.json +26 -0
- package/templates/registry/shadcn-cards.json +26 -0
- package/templates/registry/shadcn-carousel.json +29 -0
- package/templates/registry/shadcn-faq.json +30 -0
- package/templates/registry/shadcn-hero.json +25 -0
- package/templates/registry/shadcn-notice.json +27 -0
- package/templates/registry/shadcn-pricing.json +28 -0
- package/templates/registry/shadcn-tabs.json +30 -0
- package/templates/registry/shadcn-testimonials.json +25 -0
- package/templates/registry/tabs.json +17 -0
- package/templates/starter/.github/workflows/deploy.yml +84 -0
- package/templates/starter/.gitlab-ci.yml +28 -0
- package/templates/starter/README.md +108 -0
- package/templates/starter/_gitignore +2 -0
- package/templates/starter/blocks/installed/index.ts +7 -0
- package/templates/starter/components.json +24 -0
- package/templates/starter/content/collections/news/_collection.json +113 -0
- package/templates/starter/content/collections/news/open-house.json +9 -0
- package/templates/starter/content/collections/news/welcome.json +10 -0
- package/templates/starter/content/layout/footer.json +55 -0
- package/templates/starter/content/layout/header.json +54 -0
- package/templates/starter/content/menus.json +25 -0
- package/templates/starter/content/pages/404.json +73 -0
- package/templates/starter/content/pages/about.json +59 -0
- package/templates/starter/content/pages/index.json +215 -0
- package/templates/starter/content/pages/news.json +55 -0
- package/templates/starter/content/site.json +25 -0
- package/templates/starter/content/styles/custom.css +4 -0
- package/templates/starter/goodfellow.config.tsx +17 -0
- package/templates/starter/package.json +36 -0
- package/templates/starter/public/media/favicon.svg +1 -0
- package/templates/starter/public/media/logo.svg +1 -0
- package/templates/starter/public/media/placeholder.svg +1 -0
- package/templates/starter/src/styles.css +8 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/vercel.ts +65 -0
- package/templates/versions.json +13 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|