From a07ab07a11b84e4a1af53ef5798ef56eaabe536e Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Tue, 10 Jun 2025 10:42:42 -0400 Subject: [PATCH 01/45] Add plasmic code component integration to component library --- lib/plasmic-init-client.tsx | 56 ++++++++++++++++ lib/plasmic-init.ts | 13 ++++ pages/plasmic-host.tsx | 9 +++ pages/plasmic/[[...catchall]].tsx | 103 ++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 lib/plasmic-init-client.tsx create mode 100644 lib/plasmic-init.ts create mode 100644 pages/plasmic-host.tsx create mode 100644 pages/plasmic/[[...catchall]].tsx diff --git a/lib/plasmic-init-client.tsx b/lib/plasmic-init-client.tsx new file mode 100644 index 00000000..1b00c691 --- /dev/null +++ b/lib/plasmic-init-client.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { PlasmicRootProvider } from "@plasmicapp/loader-nextjs"; +import { Testimonial01 } from "@/registry/new-york/marketing/testimonial-01"; +// import "../app/globals.css"; +import { PLASMIC } from "./plasmic-init"; + +// You can register any code components that you want to use here; see +// https://docs.plasmic.app/learn/code-components-ref/ +// And configure your Plasmic project to use the host url pointing at +// the /plasmic-host page of your nextjs app (for example, +// http://localhost:3000/plasmic-host). See +// https://docs.plasmic.app/learn/app-hosting/#set-a-plasmic-project-to-use-your-app-host + +// PLASMIC.registerComponent(...); + +function HelloWorld() { + return ( + <> +

Hello World

+ + ); +} + +PLASMIC.registerComponent(HelloWorld, { + name: "HelloWorld", + props: {}, +}); + +PLASMIC.registerComponent(Testimonial01, { + name: "Testimonial01", + + props: { + // verbose: "boolean", + // children: "slot", + content: "string", + name: "string", + role: "string", + companyName: "string", + className: "string", + }, +}); +/** + * PlasmicClientRootProvider is a Client Component that passes in the loader for you. + * + * Why? Props passed from Server to Client Components must be serializable. + * https://beta.nextjs.org/docs/rendering/server-and-client-components#passing-props-from-server-to-client-components-serialization + * However, PlasmicRootProvider requires a loader, but the loader is NOT serializable. + */ +export function PlasmicClientRootProvider( + props: Omit, "loader"> +) { + return ( + + ); +} diff --git a/lib/plasmic-init.ts b/lib/plasmic-init.ts new file mode 100644 index 00000000..e429e96b --- /dev/null +++ b/lib/plasmic-init.ts @@ -0,0 +1,13 @@ +import { initPlasmicLoader } from "@plasmicapp/loader-nextjs"; + +export const PLASMIC = initPlasmicLoader({ + projects: [ + { + id: process.env.PLASMIC_PROJECT_ID!, // ID of a project you are using + token: process.env.PLASMIC_API_TOKEN!, // API token for that project + }, + ], + // Fetches the latest revisions, whether or not they were unpublished! + // Disable for production to ensure you render only published changes. + preview: true, +}); diff --git a/pages/plasmic-host.tsx b/pages/plasmic-host.tsx new file mode 100644 index 00000000..6cd0cf25 --- /dev/null +++ b/pages/plasmic-host.tsx @@ -0,0 +1,9 @@ +import { PlasmicCanvasHost } from "@plasmicapp/loader-nextjs"; +import * as React from "react"; +import { PLASMIC } from "@/lib/plasmic-init"; +import "@/lib/plasmic-init-client"; +import "../app/globals.css"; + +export default function PlasmicHost() { + return PLASMIC && ; +} diff --git a/pages/plasmic/[[...catchall]].tsx b/pages/plasmic/[[...catchall]].tsx new file mode 100644 index 00000000..dfdff0ec --- /dev/null +++ b/pages/plasmic/[[...catchall]].tsx @@ -0,0 +1,103 @@ +import { + ComponentRenderData, + extractPlasmicQueryData, + PlasmicComponent, + PlasmicRootProvider, +} from "@plasmicapp/loader-nextjs"; +import * as React from "react"; +import { GetStaticPaths, GetStaticProps } from "next"; +import Error from "next/error"; +import { useRouter } from "next/router"; +import "@/lib/plasmic-init-client"; +import { PLASMIC } from "@/lib/plasmic-init"; +import "../../app/globals.css"; + +/** + * Use fetchPages() to fetch list of pages that have been created in Plasmic + */ +export const getStaticPaths: GetStaticPaths = async () => { + const pages = await PLASMIC.fetchPages(); + return { + paths: pages.map((page) => ({ + params: { catchall: page.path.substring(1).split("/") }, + })), + fallback: "blocking", + }; +}; + +/** + * For each page, pre-fetch the data we need to render it + */ +export const getStaticProps: GetStaticProps = async (context) => { + const { catchall } = context.params ?? {}; + + // Convert the catchall param into a path string + const plasmicPath = + typeof catchall === "string" + ? catchall + : Array.isArray(catchall) + ? `/${catchall.join("/")}` + : "/"; + const plasmicData = await PLASMIC.maybeFetchComponentData(plasmicPath); + if (!plasmicData) { + // This is some non-Plasmic catch-all page + return { + props: {}, + }; + } + + // This is a path that Plasmic knows about. + const pageMeta = plasmicData.entryCompMetas[0]; + + // Cache the necessary data fetched for the page. + const queryCache = await extractPlasmicQueryData( + + + + ); + + // Pass the data in as props. + return { + props: { plasmicData, queryCache }, + + // Using incremental static regeneration, will invalidate this page + // after 300s (no deploy webhooks needed) + revalidate: 300, + }; +}; + +/** + * Actually render the page! + */ +export default function CatchallPage(props: { + plasmicData?: ComponentRenderData; + queryCache?: Record; +}) { + const { plasmicData, queryCache } = props; + const router = useRouter(); + if (!plasmicData || plasmicData.entryCompMetas.length === 0) { + return ; + } + const pageMeta = plasmicData.entryCompMetas[0]; + return ( + // Pass in the data fetched in getStaticProps as prefetchedData + + { + // pageMeta.displayName contains the name of the component you fetched. + } + + + ); +} From bc8415172e693a7c84c54b538a7317969b005927 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Tue, 10 Jun 2025 20:57:34 -0400 Subject: [PATCH 02/45] Update plasmic init client --- lib/plasmic-init-client.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/plasmic-init-client.tsx b/lib/plasmic-init-client.tsx index 1b00c691..a8d1be00 100644 --- a/lib/plasmic-init-client.tsx +++ b/lib/plasmic-init-client.tsx @@ -4,6 +4,7 @@ import { PlasmicRootProvider } from "@plasmicapp/loader-nextjs"; import { Testimonial01 } from "@/registry/new-york/marketing/testimonial-01"; // import "../app/globals.css"; import { PLASMIC } from "./plasmic-init"; +import { cn } from "./utils"; // You can register any code components that you want to use here; see // https://docs.plasmic.app/learn/code-components-ref/ @@ -14,17 +15,21 @@ import { PLASMIC } from "./plasmic-init"; // PLASMIC.registerComponent(...); -function HelloWorld() { +function HelloWorld({ className }: { className: string }) { return ( <> -

Hello World

+

+ Hello World +

); } PLASMIC.registerComponent(HelloWorld, { name: "HelloWorld", - props: {}, + props: { className: "string" }, + section: "TestHelloSection", + styleSections: ["visibility"], }); PLASMIC.registerComponent(Testimonial01, { From e57cd449372b91edc2acf02013b17c65b6571535 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 12:18:55 -0400 Subject: [PATCH 03/45] Add plasmic CLI --- package.json | 1 + pnpm-lock.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/package.json b/package.json index c36d0d9e..2d034231 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@ianvs/prettier-plugin-sort-imports": "^4.4.1", + "@plasmicapp/cli": "^0.1.338", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "19.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 954dc7b7..3fe8d7b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@ianvs/prettier-plugin-sort-imports': specifier: ^4.4.1 version: 4.4.1(prettier@3.5.3) + '@plasmicapp/cli': + specifier: ^0.1.338 + version: 0.1.338 '@tailwindcss/postcss': specifier: ^4 version: 4.1.3 @@ -881,6 +884,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@plasmicapp/cli@0.1.338': + resolution: {integrity: sha512-dxE07MHlPxwj4Y/hLDIkUK1JT2oL0mbXTmIKHzNQoy1j8u60BlgLziW/A+4RgjcHns19eusimZxPBEVT43lo2A==} + engines: {node: '>=12'} + hasBin: true + '@radix-ui/number@1.0.1': resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} @@ -5760,6 +5768,8 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@plasmicapp/cli@0.1.338': {} + '@radix-ui/number@1.0.1': dependencies: '@babel/runtime': 7.27.1 From d675fda7177561caab13a124e33af2191235437c Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 13:18:47 -0400 Subject: [PATCH 04/45] Add notes to the readme on plasmic integration --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index df191aa9..8b855b70 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,71 @@ This project supports integration with Figma's Dev Mode MCP (Model Context Proto For more detailed instructions, refer to [Figma's official Dev Mode MCP Server guide](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Dev-Mode-MCP-Server). +# Plasmic integration + +- http://localhost:3002/plasmic/showcase + Here you can see the rendered shocase page. you can edit this page in the [shared plasmic project](https://studio.plasmic.app/projects/qkC7iPT4FekKxstnmsJETj) + The page is rendered via plasmic's [catchall renderer](./pages/plasmic/[[...catchall]].tsx). + +Issue 1: Note that as more pages are created in the Plasmic Studio editor, those get synced into the codebase directly under `/pages` under +corresponding global routes. For some reason these are broken and I have not spent time trying to fix them + +Issue 2: Note: please do not create a page with root URL ("/"). This causes a duplicate route issue in the componend library code project as plasmic pages under /plasmic/[PAGE] are rendered via the page router, while +the rest of the components are rendered via the app router and app router already has a route for root URL. + +### How to add new code components + +Follow existing examples in [./lib/plasmic-init-client.tsx](./lib/plasmic-init-client.tsx). + +### Dev workflow with Plasmic UI + +- Have `pnpm run dev` in one terminal. +- Open http://localhost:3002/ for list of components +- Open http://localhost:3002/plasmic-host and confirm "Your app is ready to host Plasmic Studio!" message +- Open http://localhost:3002/plasmic/showcase for latest Plasmic-studio designed homepage +- (done once or when switching Plasmic studio projects) + - Update [./lib/plasmic-init.ts](./lib/plasmic-init.ts) with my new project ID and token. + - Delete [./components/plasmic-autogen/](./components/plasmic-autogen/) and [./plasmic.json](./plasmic.json) + - `npx plasmic sync --projects [PROJECT_ID]` + - Prepare corresponding project in Plasmic Studio UI. + - Open the project, under kebab menu next to project name on the top right, click on "Configure custom app host" + - Put `http://localhost:3002/plasmic-host` as app host + - Hard-refresh the Plasmic Studio page. You'll see requests in terminal and will see registered code components in the plasmic UI + +## Using Plasmic Components + +For each component, Plasmic generates two React components +in two files. For example, for component Homepage, there +are: + +- A blackbox component at plasmic-autogen/website_starter/PlasmicHomepage.tsx + This is a blackbox, purely-presentational library component + that you can use to render your designs. This file is owned + by Plasmic, and you should not edit it -- it will be + overwritten when the component design is updated. This + component should only be used by the "wrapper" component + (below). + +- A wrapper component at ../pages/index.tsx + This component is owned and edited by you to instantiate the + PlasmicHomepage component with desired variants, states, + event handlers, and data. You have complete control over + this file, and this is the actual component that should be + used by the rest of the codebase. + +Learn more at https://www.plasmic.app/learn/codegen-guide/ + +## Using Icons + +For each SVG icon, Plasmic also generates a React component. +The component takes in all the usual props that you can pass +to an svg element, and defaults to width/height of 1em. + +For example, for the CircleIcon icon at plasmic-autogen/website_starter/icons/PlasmicIcon\_\_Circle.tsx, +instantiate it like: + + + ## TODO Coming up: From fb3a930395f3d8bb5662f286a8d6fc9ede543bb7 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 13:18:55 -0400 Subject: [PATCH 05/45] Add plasmic watch script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d034231..7772a521 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "test": "vitest", "test:watch": "vitest watch", "test:coverage": "vitest run --coverage", - "type:test": "tsd" + "type:test": "tsd", + "plasmic:watch": "plasmic watch" }, "dependencies": { "-": "^0.0.1", From 36471fd95cd8b01ca64f3a1b801afcae029b0492 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 13:20:09 -0400 Subject: [PATCH 06/45] Commit generated showcase root component file --- pages/showcase.tsx | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 pages/showcase.tsx diff --git a/pages/showcase.tsx b/pages/showcase.tsx new file mode 100644 index 00000000..e68dec4a --- /dev/null +++ b/pages/showcase.tsx @@ -0,0 +1,37 @@ +// This is a skeleton starter React page generated by Plasmic. +// This file is owned by you, feel free to edit as you see fit. +import { PageParamsProvider as PageParamsProvider__ } from "@plasmicapp/react-web/lib/host"; +import * as React from "react"; +import { useRouter } from "next/router"; +import { PlasmicShowcase } from "../components/plasmic-autogen/component_library_2/PlasmicShowcase"; + +function Showcase() { + // Use PlasmicShowcase to render this component as it was + // designed in Plasmic, by activating the appropriate variants, + // attaching the appropriate event handlers, etc. You + // can also install whatever React hooks you need here to manage state or + // fetch data. + // + // Props you can pass into PlasmicShowcase are: + // 1. Variants you want to activate, + // 2. Contents for slots you want to fill, + // 3. Overrides for any named node in the component to attach behavior and data, + // 4. Props to set on the root node. + // + // By default, PlasmicShowcase is wrapped by your project's global + // variant context providers. These wrappers may be moved to + // Next.js Custom App component + // (https://nextjs.org/docs/advanced-features/custom-app). + + return ( + + + + ); +} + +export default Showcase; From 8c36b6e92da6cea23345806d6649699bceeaab60 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 13:23:26 -0400 Subject: [PATCH 07/45] Move back to hardcoding token and project id these are already hardcoded in plasmic.json, so no need to hide them from here --- lib/plasmic-init.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/plasmic-init.ts b/lib/plasmic-init.ts index e429e96b..1d8bbf55 100644 --- a/lib/plasmic-init.ts +++ b/lib/plasmic-init.ts @@ -1,10 +1,13 @@ import { initPlasmicLoader } from "@plasmicapp/loader-nextjs"; +import * as NextNavigation from "next/navigation"; export const PLASMIC = initPlasmicLoader({ + nextNavigation: NextNavigation, projects: [ { - id: process.env.PLASMIC_PROJECT_ID!, // ID of a project you are using - token: process.env.PLASMIC_API_TOKEN!, // API token for that project + id: "qkC7iPT4FekKxstnmsJETj", // ID of a project you are using + token: + "vmtH2Z3Rp0OnR9TF3vU3SC3PRMyM2L3PYLJJrpl063nCqxlQLYUyC1U7tWcLnZmCMRzGQd5bhfsqUPiusIyA", // API token for that project }, ], // Fetches the latest revisions, whether or not they were unpublished! From 56553c4239f9a9746dbbd6240cd09d96cbd0babd Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 13:23:44 -0400 Subject: [PATCH 08/45] Register some component library components --- lib/plasmic-init-client.tsx | 116 ++++++++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 17 deletions(-) diff --git a/lib/plasmic-init-client.tsx b/lib/plasmic-init-client.tsx index a8d1be00..fdb9a940 100644 --- a/lib/plasmic-init-client.tsx +++ b/lib/plasmic-init-client.tsx @@ -1,10 +1,14 @@ "use client"; import { PlasmicRootProvider } from "@plasmicapp/loader-nextjs"; +import { + Ticker, + TickerContent, + TickerItem, +} from "@/registry/new-york/layout/ticker"; import { Testimonial01 } from "@/registry/new-york/marketing/testimonial-01"; // import "../app/globals.css"; import { PLASMIC } from "./plasmic-init"; -import { cn } from "./utils"; // You can register any code components that you want to use here; see // https://docs.plasmic.app/learn/code-components-ref/ @@ -15,21 +19,84 @@ import { cn } from "./utils"; // PLASMIC.registerComponent(...); -function HelloWorld({ className }: { className: string }) { - return ( - <> -

- Hello World -

- - ); -} +PLASMIC.registerComponent(TickerItem, { + name: "TickerItem", + importPath: "@/registry/new-york/layout/ticker", + props: { + className: "string", + children: { + type: "slot", + defaultValue: { + type: "text", + value: "Logo Example", + }, + }, + }, +}); + +PLASMIC.registerComponent(TickerContent, { + name: "TickerContent", + importPath: "@/registry/new-york/layout/ticker", + props: { + className: "string", + children: { + type: "slot", + defaultValue: ["#ff0000", "#00ff00", "#0000ff"].map((c, i) => ({ + type: "component", + name: "TickerItem", + props: { + children: [ + { type: "text", value: "Logo Example " + i, styles: { color: c } }, + ], + }, + })), + }, + }, + actions: [ + { + type: "button-action", + label: "Add Logo", + onClick: ({ studioOps, ...props }) => { + studioOps.appendToSlot( + { + type: "component", + name: "TickerItem", + props: { + children: [{ type: "text", value: "Added Logo" }], + }, + }, + "children" + ); + }, + }, + ], +}); -PLASMIC.registerComponent(HelloWorld, { - name: "HelloWorld", - props: { className: "string" }, - section: "TestHelloSection", - styleSections: ["visibility"], +PLASMIC.registerComponent(Ticker, { + name: "Ticker", + importPath: "@/registry/new-york/layout/ticker", + props: { + className: "string", + speed: { type: "number", defaultValue: 15 }, + direction: { + type: "choice", + options: ["left", "right", "up", "down"], + defaultValue: "left", + }, + children: { + type: "slot", + defaultValue: { + type: "component", + name: "TickerContent", + }, + }, + }, + actions: [ + { + type: "custom-action", + control: () => <>Select TickerContent to add new Logos., + }, + ], }); PLASMIC.registerComponent(Testimonial01, { @@ -37,14 +104,29 @@ PLASMIC.registerComponent(Testimonial01, { props: { // verbose: "boolean", - // children: "slot", - content: "string", + children: { + type: "slot", + defaultValue: [ + { + type: "text", + value: "first child text", + }, + { + type: "text", + value: "— second child text in span, author", + tag: "figcaption", + }, + ], + }, + content: { type: "string" }, name: "string", role: "string", companyName: "string", className: "string", }, + importPath: "@/registry/new-york/marketing/testimonial-01", }); + /** * PlasmicClientRootProvider is a Client Component that passes in the loader for you. * From 64fddde5aa1a0f1d6ebd00e3c280ae3ff1bc46ad Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 13:27:00 -0400 Subject: [PATCH 09/45] readme --- README.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 8b855b70..72330c4a 100644 --- a/README.md +++ b/README.md @@ -157,35 +157,45 @@ For more detailed instructions, refer to [Figma's official Dev Mode MCP Server g # Plasmic integration -- http://localhost:3002/plasmic/showcase - Here you can see the rendered shocase page. you can edit this page in the [shared plasmic project](https://studio.plasmic.app/projects/qkC7iPT4FekKxstnmsJETj) - The page is rendered via plasmic's [catchall renderer](./pages/plasmic/[[...catchall]].tsx). +Plasmic integration adds a two-way sync to and from the attached Plasmic Studio project. +Project ref is hard-coded in [./lib/plasmic-init.ts](./lib/plasmic-init.ts) and in [./plasmic.json](./plasmic.json) -Issue 1: Note that as more pages are created in the Plasmic Studio editor, those get synced into the codebase directly under `/pages` under -corresponding global routes. For some reason these are broken and I have not spent time trying to fix them +Plasmic integration adds `/plasmic-host` and `/plasmic/*` routes via a page router, along existing app router (the two are able to co-exist so long as they do not define overlapping routes, which is something we have to be careful about here). +Generally, the first of the two routes above is responsible for localdev->PlasmicStudio sync direction, while the second is responsible for PlasmicStudio->localdev direction of sync. -Issue 2: Note: please do not create a page with root URL ("/"). This causes a duplicate route issue in the componend library code project as plasmic pages under /plasmic/[PAGE] are rendered via the page router, while -the rest of the components are rendered via the app router and app router already has a route for root URL. +The main PlasmicUI built page lives [http://localhost:3000/plasmic/showcase](http://localhost:3000/plasmic/showcase). +You can edit this page in the [shared plasmic project](https://studio.plasmic.app/projects/qkC7iPT4FekKxstnmsJETj) +The page is rendered via plasmic's [catchall renderer](./pages/plasmic/[[...catchall]].tsx). -### How to add new code components - -Follow existing examples in [./lib/plasmic-init-client.tsx](./lib/plasmic-init-client.tsx). - -### Dev workflow with Plasmic UI +## Dev workflow with Plasmic UI - Have `pnpm run dev` in one terminal. -- Open http://localhost:3002/ for list of components -- Open http://localhost:3002/plasmic-host and confirm "Your app is ready to host Plasmic Studio!" message -- Open http://localhost:3002/plasmic/showcase for latest Plasmic-studio designed homepage +- Open http://localhost:3000/ for list of components +- Open http://localhost:3000/plasmic-host and confirm "Your app is ready to host Plasmic Studio!" message +- Open http://localhost:3000/plasmic/showcase for latest Plasmic-studio designed homepage - (done once or when switching Plasmic studio projects) - Update [./lib/plasmic-init.ts](./lib/plasmic-init.ts) with my new project ID and token. - Delete [./components/plasmic-autogen/](./components/plasmic-autogen/) and [./plasmic.json](./plasmic.json) - `npx plasmic sync --projects [PROJECT_ID]` - Prepare corresponding project in Plasmic Studio UI. - Open the project, under kebab menu next to project name on the top right, click on "Configure custom app host" - - Put `http://localhost:3002/plasmic-host` as app host + - Put `http://localhost:3000/plasmic-host` as app host - Hard-refresh the Plasmic Studio page. You'll see requests in terminal and will see registered code components in the plasmic UI +## Issues + +Issue 1: Note that as more pages are created in the Plasmic Studio editor, those get synced into the codebase directly under `/pages` under +corresponding global routes. For some reason these are broken and I have not spent time trying to fix them + +Issue 2: Note: please do not create a page with root URL ("/"). This causes a duplicate route issue in the componend library code project as plasmic pages under /plasmic/[PAGE] are rendered via the page router, while +the rest of the components are rendered via the app router and app router already has a route for root URL. + +Issue 3: Plasmic editor Actions API still uses ReactDOM.render API which was removed in React 19, that is why I downgraded the project to React 18, so integration does not break until this gets fixed in he upstream + +## How to add new code components + +Follow existing examples in [./lib/plasmic-init-client.tsx](./lib/plasmic-init-client.tsx). + ## Using Plasmic Components For each component, Plasmic generates two React components From a673e1b106c0f72ae9a3d1d794bfb08da2b505b6 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 12 Jun 2025 14:18:57 -0400 Subject: [PATCH 10/45] Use the new testimonial --- lib/plasmic-init-client.tsx | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/plasmic-init-client.tsx b/lib/plasmic-init-client.tsx index fdb9a940..e7f8a3be 100644 --- a/lib/plasmic-init-client.tsx +++ b/lib/plasmic-init-client.tsx @@ -78,6 +78,8 @@ PLASMIC.registerComponent(Ticker, { props: { className: "string", speed: { type: "number", defaultValue: 15 }, + pauseOnHover: { type: "boolean", defaultValue: true }, + fade: { type: "boolean", defaultValue: true }, direction: { type: "choice", options: ["left", "right", "up", "down"], @@ -101,30 +103,30 @@ PLASMIC.registerComponent(Ticker, { PLASMIC.registerComponent(Testimonial01, { name: "Testimonial01", - + importPath: "@/registry/new-york/marketing/testimonial-01", props: { - // verbose: "boolean", - children: { + content: { type: "slot", - defaultValue: [ - { - type: "text", - value: "first child text", - }, - { - type: "text", - value: "— second child text in span, author", - tag: "figcaption", - }, - ], + defaultValue: { + type: "text", + value: + "Lantern's component library made our product launch seamless. The design and accessibility are top-notch.", + }, + }, + attribution: { + type: "slot", + hidePlaceholder: true, + defaultValue: { + type: "text", + tag: "span", + value: "— Alex Kim, Lead Designer at PixelPerfect Inc.", + }, }, - content: { type: "string" }, name: "string", role: "string", companyName: "string", className: "string", }, - importPath: "@/registry/new-york/marketing/testimonial-01", }); /** From 6d82008a17236f96dca665298c5bf36139d78806 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Wed, 18 Jun 2025 16:14:36 -0400 Subject: [PATCH 11/45] Add plasmic dependencies --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 7772a521..bee8692e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "dependencies": { "-": "^0.0.1", "@phosphor-icons/react": "^2.1.10", + "@plasmicapp/loader-nextjs": "^1.0.432", + "@plasmicapp/react-web": "^0.2.393", + "@plasmicpkgs/react-parallax-tilt": "^0.0.222", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-collapsible": "^1.1.10", From 9c4af1d329dae75b44ac75b957c1e27ab85326ef Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Wed, 25 Jun 2025 16:33:49 -0400 Subject: [PATCH 12/45] Add support libraries --- lib/plasmic-init-client.tsx | 35 +++++------------------------------ lib/plasmic-init.ts | 1 + lib/utils-client.ts | 20 ++++++++++++++++++++ lib/utils-server.ts | 16 ++++++++++++++++ 4 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 lib/utils-client.ts create mode 100644 lib/utils-server.ts diff --git a/lib/plasmic-init-client.tsx b/lib/plasmic-init-client.tsx index e7f8a3be..b7a725a2 100644 --- a/lib/plasmic-init-client.tsx +++ b/lib/plasmic-init-client.tsx @@ -6,9 +6,12 @@ import { TickerContent, TickerItem, } from "@/registry/new-york/layout/ticker"; -import { Testimonial01 } from "@/registry/new-york/marketing/testimonial-01"; -// import "../app/globals.css"; +// this line ensures that tailwind classes are part of the loaded page in the editor +import "../app/globals.css"; import { PLASMIC } from "./plasmic-init"; +// +// import component registrations +import "./plasmic-component-registrations/testimonials.tsx"; // You can register any code components that you want to use here; see // https://docs.plasmic.app/learn/code-components-ref/ @@ -101,34 +104,6 @@ PLASMIC.registerComponent(Ticker, { ], }); -PLASMIC.registerComponent(Testimonial01, { - name: "Testimonial01", - importPath: "@/registry/new-york/marketing/testimonial-01", - props: { - content: { - type: "slot", - defaultValue: { - type: "text", - value: - "Lantern's component library made our product launch seamless. The design and accessibility are top-notch.", - }, - }, - attribution: { - type: "slot", - hidePlaceholder: true, - defaultValue: { - type: "text", - tag: "span", - value: "— Alex Kim, Lead Designer at PixelPerfect Inc.", - }, - }, - name: "string", - role: "string", - companyName: "string", - className: "string", - }, -}); - /** * PlasmicClientRootProvider is a Client Component that passes in the loader for you. * diff --git a/lib/plasmic-init.ts b/lib/plasmic-init.ts index 1d8bbf55..ffa7a1e6 100644 --- a/lib/plasmic-init.ts +++ b/lib/plasmic-init.ts @@ -1,5 +1,6 @@ import { initPlasmicLoader } from "@plasmicapp/loader-nextjs"; import * as NextNavigation from "next/navigation"; +import "../app/globals.css"; export const PLASMIC = initPlasmicLoader({ nextNavigation: NextNavigation, diff --git a/lib/utils-client.ts b/lib/utils-client.ts new file mode 100644 index 00000000..b63add25 --- /dev/null +++ b/lib/utils-client.ts @@ -0,0 +1,20 @@ +import { isValidElement } from "react"; +import { flushSync } from "react-dom"; +import { createRoot } from "react-dom/client"; + +// ideally, if the text content of the passed component was passed in as part of prop chain, we could extract +// it without having to render. However, Plasmic stores all content separately and places references in the props +// and reaches out to the DB to collect content during rendering. +// So, this is the only simple way to extract the content. Alternatively, we would have to reimplement a bunch of things that Plasmic handles +export function innerText(component: string | undefined | React.ReactElement) { + if (!component || typeof component == "string") return component; + if (isValidElement(component)) { + const el = document.createElement("div"); + const root = createRoot(el); + flushSync(() => { + root.render(component); + }); + return el.innerText; + } + throw new Error("unknown input type. Expected string or ReactElement"); +} diff --git a/lib/utils-server.ts b/lib/utils-server.ts new file mode 100644 index 00000000..6b11d477 --- /dev/null +++ b/lib/utils-server.ts @@ -0,0 +1,16 @@ +import { isValidElement } from "react"; +import { renderToString } from "react-dom/server"; + +// The following would work on the server but we cannot use react - dom / server on the client + +export function innerTextServer( + component: string | undefined | React.ReactElement +) { + if (!component || typeof component == "string") return component; + if (isValidElement(component)) { + const el = document.createElement("div"); + el.innerHTML = renderToString(component); + return el.innerText; + } + throw new Error("unknown input type. Expected string or ReactElement"); +} From 5f6272b28cb1c017e72423c612c68325db4ba4b2 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Wed, 25 Jun 2025 16:34:17 -0400 Subject: [PATCH 13/45] Modify testimonial components for integration --- .../testimonials.tsx | 234 ++++++++++++++++++ .../new-york/marketing/profile-avatar.tsx | 8 +- .../new-york/marketing/testimonial-01.tsx | 22 +- .../new-york/marketing/testimonial-02.tsx | 14 +- .../new-york/marketing/testimonial-03.tsx | 69 +++--- .../new-york/marketing/testimonial-04.tsx | 36 +-- .../new-york/marketing/testimonial-05.tsx | 53 ++-- .../new-york/marketing/testimonial-06.tsx | 44 ++-- .../new-york/marketing/testimonial-07.tsx | 40 ++- .../new-york/marketing/testimonial-08.tsx | 43 ++-- .../new-york/marketing/testimonial-09.tsx | 27 +- .../new-york/marketing/testimonial-10.tsx | 48 ++-- .../new-york/marketing/testimonial-base.tsx | 16 ++ registry/new-york/marketing/testimonial.tsx | 46 ++++ 14 files changed, 456 insertions(+), 244 deletions(-) create mode 100644 lib/plasmic-component-registrations/testimonials.tsx create mode 100644 registry/new-york/marketing/testimonial-base.tsx create mode 100644 registry/new-york/marketing/testimonial.tsx diff --git a/lib/plasmic-component-registrations/testimonials.tsx b/lib/plasmic-component-registrations/testimonials.tsx new file mode 100644 index 00000000..d3580af9 --- /dev/null +++ b/lib/plasmic-component-registrations/testimonials.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { useRef, useState } from "react"; +import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; +import { + Testimonial, + TESTIMONIALS, +} from "@/registry/new-york/marketing/testimonial"; +import { PLASMIC } from "../plasmic-init"; + +type TKind = keyof typeof TESTIMONIALS; + +const testimonialControl = ({ studioOps, componentProps }) => { + const [hoveredKind, setHoveredKind] = useState(null); + const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); + const containerRef = useRef(null); + + const kinds = Object.keys(TESTIMONIALS) as TKind[]; + kinds.sort(); + + const onSelect = (kind: string) => { + studioOps.updateProps({ kind }); + }; + + const onMouseMove = (e: React.MouseEvent) => { + setMousePos({ x: e.clientX + 20, y: e.clientY + 20 }); + }; + + const Preview = hoveredKind ? TESTIMONIALS[hoveredKind] : null; + const PREVIEW_WIDTH = 600; + const PREVIEW_HEIGHT = 200; + + const clampedX = Math.min(mousePos.x, window.innerWidth - PREVIEW_WIDTH - 90); // 90px buffer + + return ( +
+ {kinds.map((kind) => ( + + ))} + + {Preview && ( +
+ {/* tailwind currently does not work in preview since this runs in */} + {/* another iframe. */} + {/* TODO: fix tailwind here */} + +
+ )} +
+ ); +}; + +PLASMIC.registerComponent(ProfileAvatar, { + name: "ProfileAvatar", + importPath: "@/registry/new-york/marketing/profile-avatar", + props: { + name: { + type: "string", + defaultValue: "Emily Watson", + }, + image: { + type: "slot", + hidePlaceholder: true, + defaultValue: { + type: "img", + src: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200&h=200&fit=crop&crop=face", + styles: { display: "inline" }, + }, + }, + size: { + type: "choice", + options: ["sm", "md", "lg", "xl"], + defaultValue: "sm", + }, + fallbackType: { + type: "choice", + options: ["initials", "first-char"], + defaultValue: "initials", + }, + className: "string", + }, +}); + +PLASMIC.registerComponent(Testimonial, { + name: "Testimonial", + importPath: "@/registry/new-york/marketing/testimonial", + props: { + kind: { + type: "choice", + options: ["01", "02", "03", "04", "05", "06"], + defaultValue: "02", + }, + content: { + type: "slot", + defaultValue: { + type: "text", + value: + "Lantern's component library made our product launch seamless. The design and accessibility are top-notch.", + }, + }, + attribution: { + type: "slot", + hidePlaceholder: true, + defaultValue: { + type: "text", + tag: "span", + value: "— Alex Kim, Lead Designer at PixelPerfect Inc.", + }, + }, + name: { + type: "slot", + mergeWithParent: true, + defaultValue: { + type: "text", + tag: "span", + value: "Emily Watson", + }, + }, + avatar: { + type: "slot", + hidden: (props) => props["kind"] == "08", + displayName: "ProfileAvatar", + hidePlaceholder: true, + defaultValue: { + type: "component", + name: "ProfileAvatar", + props: { + size: "lg", + }, + }, + }, + image: { + type: "slot", + hidden: (props) => props["kind"] != "08", + defaultValue: { + type: "img", + src: "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200&h=200&fit=crop&crop=face", + styles: { + width: "100%", + height: "100%", + }, + }, + }, + role: { + type: "slot", + mergeWithParent: true, + defaultValue: { + type: "text", + tag: "span", + value: "Senior Developer", + }, + }, + companyName: { + type: "slot", + mergeWithParent: true, + defaultValue: { + type: "text", + tag: "span", + value: "IBM", + }, + }, + align: { + type: "choice", + options: ["left", "center", "right"], + hidden: (props, ctx) => props["kind"] != "02", + defaultValue: "left", + }, + companyLogo: { + type: "slot", + defaultValue: { + type: "img", + src: "https://placehold.co/96x48?text=Logo&font=roboto", + }, + }, + link: { + type: "object", + defaultValue: { + href: "http://example.com", + children: "Read Full Case Study", + }, + }, + className: "string", + }, + actions: [ + { + type: "custom-action", + control: testimonialControl, + }, + ], +}); diff --git a/registry/new-york/marketing/profile-avatar.tsx b/registry/new-york/marketing/profile-avatar.tsx index a34db98e..ebb14751 100644 --- a/registry/new-york/marketing/profile-avatar.tsx +++ b/registry/new-york/marketing/profile-avatar.tsx @@ -9,7 +9,7 @@ import { export interface ProfileAvatarProps { name: string; - image?: string; + image?: string | React.ReactElement; size?: "sm" | "md" | "lg" | "xl"; fallbackType?: "initials" | "first-char"; className?: string; @@ -56,7 +56,11 @@ export function ProfileAvatar({ return ( {image ? ( - + typeof image === "string" ? ( + + ) : ( + image + ) ) : ( {getFallbackText()} diff --git a/registry/new-york/marketing/testimonial-01.tsx b/registry/new-york/marketing/testimonial-01.tsx index 0dbcc8df..da490f15 100644 --- a/registry/new-york/marketing/testimonial-01.tsx +++ b/registry/new-york/marketing/testimonial-01.tsx @@ -2,14 +2,8 @@ import { BiSolidQuoteAltLeft } from "react-icons/bi"; import { cn } from "@/lib/utils"; - -export interface TestimonialProps extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - className?: string; -} +import { innerTextServer as innerText } from "@/lib/utils-server"; +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial01: Compact, icon-based testimonial for non-inline use (e.g., in grids or sidebars). @@ -24,13 +18,13 @@ export function Testimonial01({ ...props }: TestimonialProps) { // Build the attribution text - let attribution = name; + let attribution = innerText(name); if (role && companyName) { - attribution += `, ${role} at ${companyName}`; + attribution += `, ${innerText(role)} at ${innerText(companyName)}`; } else if (role) { - attribution += `, ${role}`; + attribution += `, ${innerText(role)}`; } else if (companyName) { - attribution += `, ${companyName}`; + attribution += `, ${innerText(companyName)}`; } return ( @@ -46,7 +40,9 @@ export function Testimonial01({ {content}
- — {attribution} + — {name} + {role && <>, {role}} + {companyName && <> at {companyName}}
); diff --git a/registry/new-york/marketing/testimonial-02.tsx b/registry/new-york/marketing/testimonial-02.tsx index 0a7f6761..daca3ae8 100644 --- a/registry/new-york/marketing/testimonial-02.tsx +++ b/registry/new-york/marketing/testimonial-02.tsx @@ -1,15 +1,7 @@ "use client"; import { cn } from "@/lib/utils"; - -export interface TestimonialQuoteProps - extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - align?: "left" | "center" | "right"; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial02: Pull-quote style testimonial for inline content emphasis. @@ -23,7 +15,7 @@ export function Testimonial02({ align = "center", className, ...props -}: TestimonialQuoteProps) { +}: TestimonialProps) { // Build the attribution text for screen readers const attributionParts = []; if (role) attributionParts.push(role); @@ -76,7 +68,7 @@ export function Testimonial02({ {name} {(role || companyName) && ( - {role && `, ${role}`} + {role && <>, {role}} {companyName && role && ` at `} {companyName} diff --git a/registry/new-york/marketing/testimonial-03.tsx b/registry/new-york/marketing/testimonial-03.tsx index 5dce2799..f377cd27 100644 --- a/registry/new-york/marketing/testimonial-03.tsx +++ b/registry/new-york/marketing/testimonial-03.tsx @@ -1,18 +1,9 @@ "use client"; import { cn } from "@/lib/utils"; +import { innerTextServer as innerText } from "@/lib/utils-server"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; - -export interface TestimonialCardProps - extends React.HTMLAttributes { - name: string; - role?: string; - username?: string; - image?: string; - content: string; - companyLogo?: string; - companyName?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial03: Card-style testimonial for non-inline use (e.g., in grids). @@ -21,20 +12,19 @@ export interface TestimonialCardProps export function Testimonial03({ name, role, - username, - image, + avatar, content, companyLogo, companyName, className, ...props -}: TestimonialCardProps) { +}: TestimonialProps) { // Determine secondary text (username takes precedence over role) - const secondaryText = username ? `@${username}` : role; - const attribution = [name]; + const secondaryText = innerText(role); + const nameText = innerText(name); + const attribution = [nameText]; if (secondaryText) attribution.push(secondaryText); - if (companyName && !secondaryText) attribution.push(companyName); - + if (companyName && !secondaryText) attribution.push(innerText(companyName)); return (
- + {nameText && avatar}
- + {name} - {secondaryText && ( - - {secondaryText} - + {role} +
)}
{companyLogo && (
- {companyName + {typeof companyLogo == "string" ? ( + { + ) : ( + companyLogo + )}
)}
-

{content}

+
{content}
diff --git a/registry/new-york/marketing/testimonial-04.tsx b/registry/new-york/marketing/testimonial-04.tsx index 72344a68..75076079 100644 --- a/registry/new-york/marketing/testimonial-04.tsx +++ b/registry/new-york/marketing/testimonial-04.tsx @@ -1,18 +1,9 @@ import { BiSolidQuoteAltLeft } from "react-icons/bi"; import { cn } from "@/lib/utils"; +import { innerTextServer as innerText } from "@/lib/utils-server"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; import { LinkButton } from "@/registry/new-york/ui/link-button"; -import type { LinkButtonProps } from "@/registry/new-york/ui/link-button"; - -export interface TestimonialFeaturedProps - extends React.HTMLAttributes { - name: string; - role?: string; - image?: string; - content: string; - link?: LinkButtonProps; - companyName?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial04: Large, featured testimonial for non-inline use (e.g., hero sections). @@ -21,23 +12,24 @@ export interface TestimonialFeaturedProps export function Testimonial04({ name, role, - image, + avatar, content, link, companyName, className, ...props -}: TestimonialFeaturedProps) { +}: TestimonialProps) { // Build the attribution text const attributionParts = []; if (role) attributionParts.push(role); if (companyName) { attributionParts.push(role ? `at ${companyName}` : companyName); } + const nameText = innerText(name); const attribution = attributionParts.length > 0 - ? `${name}, ${attributionParts.join(" ")}` - : name; + ? `${nameText}, ${attributionParts.join(" ")}` + : nameText; return (
- - + {nameText && avatar}
- {name} +
{name}
{name && role ? ", " : ""} - {role} +
{role}
{companyName && !role && ( - {companyName} +
{companyName}
)} {companyName && role && ( at {companyName} diff --git a/registry/new-york/marketing/testimonial-05.tsx b/registry/new-york/marketing/testimonial-05.tsx index c9c0932d..14ab276e 100644 --- a/registry/new-york/marketing/testimonial-05.tsx +++ b/registry/new-york/marketing/testimonial-05.tsx @@ -1,16 +1,8 @@ +import { isValidElement } from "react"; import { cn } from "@/lib/utils"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; import { H6 } from "../theme/typography"; - -export interface TestimonialFeaturedProps - extends React.HTMLAttributes { - name: string; - role?: string; - image?: string; - content: string; - companyName?: string; - companyLogo?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial05: Large, featured testimonial for non-inline use (e.g., hero sections). @@ -19,13 +11,13 @@ export interface TestimonialFeaturedProps export function Testimonial05({ name, role, - image, + avatar, content, companyName, companyLogo, className, ...props -}: TestimonialFeaturedProps) { +}: TestimonialProps) { // Build the attribution text const attributionParts = []; if (role) attributionParts.push(role); @@ -37,6 +29,22 @@ export function Testimonial05({ ? `${name}, ${attributionParts.join(" ")}` : name; + if (companyLogo) console.log("got company logo", companyLogo); + + // client can pass either a string URL or a component to be rendered as logo + const companyLogoComp = isValidElement(companyLogo) ? ( + companyLogo + ) : typeof companyLogo == "string" ? ( + {companyName + ) : ( + companyLogo + ); + return (
{/* 1. Logo container */} -
- {companyLogo && ( - {companyName - )} -
+
{companyLogoComp}
{/* 2. Testimonial text container */}
@@ -65,15 +64,7 @@ export function Testimonial05({
{/* 3. Avatar + attribution container */}
- + {avatar}
{name} diff --git a/registry/new-york/marketing/testimonial-06.tsx b/registry/new-york/marketing/testimonial-06.tsx index 7ac8ac78..3fce4f11 100644 --- a/registry/new-york/marketing/testimonial-06.tsx +++ b/registry/new-york/marketing/testimonial-06.tsx @@ -5,17 +5,7 @@ import { FaStar } from "react-icons/fa"; import { cn } from "@/lib/utils"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; import { H6 } from "../theme/typography"; - -export interface Testimonial06Props - extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - companyLogo?: string; - image?: string; - className?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial06: 5-star testimonial with bold quote, avatar, attribution, and company logo. @@ -26,10 +16,10 @@ export function Testimonial06({ role, companyName, companyLogo, - image, + avatar, className, ...props -}: Testimonial06Props) { +}: TestimonialProps) { return (
{/* Avatar + Name/Role/Company */}
- + {avatar}
{name} {(role || companyName) && ( - {[role, companyName].filter(Boolean).join(", ")} + {role} + {role && companyName && ", "} + {companyName} )}
@@ -74,12 +60,16 @@ export function Testimonial06({ {/* Company Logo */} {companyLogo && (
- {companyName + {typeof companyLogo == "string" ? ( + {companyName + ) : ( + companyLogo + )}
)}
diff --git a/registry/new-york/marketing/testimonial-07.tsx b/registry/new-york/marketing/testimonial-07.tsx index dbe8e756..ccc583b7 100644 --- a/registry/new-york/marketing/testimonial-07.tsx +++ b/registry/new-york/marketing/testimonial-07.tsx @@ -5,17 +5,7 @@ import { FaStar } from "react-icons/fa"; import { cn } from "@/lib/utils"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; import { H6 } from "../theme/typography"; - -export interface Testimonial07Props - extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - companyLogo?: string; - image?: string; - className?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial07: Center-aligned 5-star testimonial with bold quote, avatar, attribution, and company logo. @@ -24,12 +14,12 @@ export function Testimonial07({ content, name, role, + avatar, companyName, companyLogo, - image, className, ...props -}: Testimonial07Props) { +}: TestimonialProps) { return (
{/* Avatar */} - + {avatar} {/* Name/Role/Company */}
{name} @@ -73,12 +57,16 @@ export function Testimonial07({ {/* Company Logo */} {companyLogo && (
- {companyName + {typeof companyLogo == "string" ? ( + {companyName + ) : ( + companyLogo + )}
)}
diff --git a/registry/new-york/marketing/testimonial-08.tsx b/registry/new-york/marketing/testimonial-08.tsx index 6b6a5ac3..6b5e2687 100644 --- a/registry/new-york/marketing/testimonial-08.tsx +++ b/registry/new-york/marketing/testimonial-08.tsx @@ -3,17 +3,7 @@ import { FaStar } from "react-icons/fa"; import { cn } from "@/lib/utils"; import { H6 } from "../theme/typography"; - -export interface Testimonial08Props - extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - companyLogo?: string; - image?: string; - className?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial08: Side-by-side horizontal testimonial with 5-star rating, bold quote, avatar, attribution, and company logo. Matches screenshot. @@ -22,12 +12,13 @@ export function Testimonial08({ content, name, role, + avatar, // unused. we use image here + image, companyName, companyLogo, - image, className, ...props -}: Testimonial08Props) { +}: TestimonialProps & { image?: string | React.ReactElement }) { return (
{/* Left: Image */}
- {name} + {typeof image != "string" ? ( + image + ) : ( + {name} + )}
{/* Right: Testimonial Content */}
@@ -64,7 +59,9 @@ export function Testimonial08({ {(role || companyName) && ( - {[role, companyName].filter(Boolean).join(", ")} + {role} + {role && companyName && ", "} + {companyName} )}
@@ -73,12 +70,16 @@ export function Testimonial08({ {/* Company Logo */} {companyLogo && (
- {companyName + {typeof companyLogo == "string" ? ( + {companyName + ) : ( + companyLogo + )}
)}
diff --git a/registry/new-york/marketing/testimonial-09.tsx b/registry/new-york/marketing/testimonial-09.tsx index 40ace677..92459289 100644 --- a/registry/new-york/marketing/testimonial-09.tsx +++ b/registry/new-york/marketing/testimonial-09.tsx @@ -4,16 +4,7 @@ import * as React from "react"; import { FaStar } from "react-icons/fa"; import { cn } from "@/lib/utils"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; - -export interface Testimonial09Props - extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - image?: string; - className?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial09: Card-style testimonial matching screenshot visual details. @@ -24,10 +15,10 @@ export function Testimonial09({ name, role, companyName, - image, + avatar, className, ...props -}: Testimonial09Props) { +}: TestimonialProps) { return (
- + {avatar}
{name} {(role || companyName) && ( - {[role, companyName].filter(Boolean).join(", ")} + {role} + {role && companyName && ", "} + {companyName} )}
diff --git a/registry/new-york/marketing/testimonial-10.tsx b/registry/new-york/marketing/testimonial-10.tsx index 071ae731..c66e062d 100644 --- a/registry/new-york/marketing/testimonial-10.tsx +++ b/registry/new-york/marketing/testimonial-10.tsx @@ -3,17 +3,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; - -export interface Testimonial10Props - extends React.HTMLAttributes { - content: string; - name: string; - role?: string; - companyName?: string; - image?: string; - companyLogo?: string; - className?: string; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial10: Card-style testimonial matching testimonial-09, but uses a logo image instead of stars. @@ -23,11 +13,11 @@ export function Testimonial10({ name, role, companyName, - image, + avatar, companyLogo, className, ...props -}: Testimonial10Props) { +}: TestimonialProps) { return (
- {companyName + {typeof companyLogo == "string" ? ( + {companyName + ) : ( + companyLogo + )}
)} {/* Quote */}
-

+

{content} -

+
{/* Attribution Row */}
- + {avatar}
{name} {(role || companyName) && ( - {[role, companyName].filter(Boolean).join(", ")} + {role} + {role && companyName && ", "} + {companyName} )}
diff --git a/registry/new-york/marketing/testimonial-base.tsx b/registry/new-york/marketing/testimonial-base.tsx new file mode 100644 index 00000000..c5626e7e --- /dev/null +++ b/registry/new-york/marketing/testimonial-base.tsx @@ -0,0 +1,16 @@ +import type { LinkButtonProps } from "@/registry/new-york/ui/link-button"; + +export interface TestimonialProps extends React.HTMLAttributes { + content: string; + // kind: "01" | "02" | "03" | "04" | "05" | "06"; + attribution: React.ReactElement; + name: string; + role?: string; + avatar?: React.ReactElement; + companyName?: string | React.ReactElement; + companyLogo?: string | React.ReactElement; // <-- needed in testimonial-05 + className?: string; + align?: "left" | "center" | "right"; // <-- neeed in testimonial-02 + link?: LinkButtonProps; // needed in testimonial-04 + linkText?: string; // needed in testimonial-04 +} diff --git a/registry/new-york/marketing/testimonial.tsx b/registry/new-york/marketing/testimonial.tsx new file mode 100644 index 00000000..c1b3b8ba --- /dev/null +++ b/registry/new-york/marketing/testimonial.tsx @@ -0,0 +1,46 @@ +import { Testimonial01 } from "./testimonial-01"; +import { Testimonial02 } from "./testimonial-02"; +import { Testimonial03 } from "./testimonial-03"; +import { Testimonial04 } from "./testimonial-04"; +import { Testimonial05 } from "./testimonial-05"; +import { Testimonial06 } from "./testimonial-06"; +import { Testimonial07 } from "./testimonial-07"; +import { Testimonial08 } from "./testimonial-08"; +import { Testimonial09 } from "./testimonial-09"; +import { Testimonial10 } from "./testimonial-10"; +import { TestimonialProps } from "./testimonial-base"; + +export const TESTIMONIALS = { + "01": Testimonial01, + "02": Testimonial02, + "03": Testimonial03, + "04": Testimonial04, + "05": Testimonial05, + "06": Testimonial06, + "07": Testimonial07, + "08": Testimonial08, + "09": Testimonial09, + "10": Testimonial10, +} as const; + +type TestimonialKind = keyof typeof TESTIMONIALS; // "01" | … | "06" | … + +// 2. Helper type (optional, handy elsewhere) +type TestimonialOf = (typeof TESTIMONIALS)[K]; + +// 3. Generic accessor: returns the *precise* component type +function getTestimonial(kind: K): TestimonialOf { + return TESTIMONIALS[kind]; +} + +export function Testimonial({ + kind, + ...props +}: TestimonialProps & { + kind: K; +}) { + const comp = TESTIMONIALS[kind]; + // const comp2 = getTestimonial(kind); + // return comp2(props); + return comp(props); +} From 0f31e6dedf2370cb765a56e8c7b6a50955334ee6 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 10:12:50 -0400 Subject: [PATCH 14/45] ignore autogen components --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5ef6a520..ff6a4308 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +components-autogen From e0eea1813d384822d469498b0342a2277ce01671 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 10:13:03 -0400 Subject: [PATCH 15/45] Remove kinds from TestimonialProps --- registry/new-york/marketing/testimonial-base.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/registry/new-york/marketing/testimonial-base.tsx b/registry/new-york/marketing/testimonial-base.tsx index c5626e7e..a8fe3900 100644 --- a/registry/new-york/marketing/testimonial-base.tsx +++ b/registry/new-york/marketing/testimonial-base.tsx @@ -2,7 +2,6 @@ import type { LinkButtonProps } from "@/registry/new-york/ui/link-button"; export interface TestimonialProps extends React.HTMLAttributes { content: string; - // kind: "01" | "02" | "03" | "04" | "05" | "06"; attribution: React.ReactElement; name: string; role?: string; From eb8758709b41ed324cce60a5c7a0d0bd6db18e14 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 10:16:39 -0400 Subject: [PATCH 16/45] Ignore aider files --- .gitignore | 1 + eslint.config.mjs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ff6a4308..c59e31c6 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.aider* components-autogen diff --git a/eslint.config.mjs b/eslint.config.mjs index 3b910158..0c8385ad 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,10 +10,10 @@ const compat = new FlatCompat({ }); const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + // ...compat.extends("next/typescript"), { rules: { - "@next/next/no-img-element": "off", + // "@next/next/no-img-element": "off", }, }, ]; From af925f4b3137be957da5262b5ee411988d75c217 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 15:39:33 -0400 Subject: [PATCH 17/45] Consolidate plasmic code related to testimonials to a single file --- .../testimonials.tsx | 69 ++++++++++++++++--- registry/new-york/marketing/testimonial.tsx | 46 ------------- 2 files changed, 61 insertions(+), 54 deletions(-) delete mode 100644 registry/new-york/marketing/testimonial.tsx diff --git a/lib/plasmic-component-registrations/testimonials.tsx b/lib/plasmic-component-registrations/testimonials.tsx index d3580af9..4b6f5b95 100644 --- a/lib/plasmic-component-registrations/testimonials.tsx +++ b/lib/plasmic-component-registrations/testimonials.tsx @@ -1,16 +1,69 @@ "use client"; +import { + ActionProps, + usePlasmicCanvasComponentInfo, +} from "@plasmicapp/react-web/lib/host"; import { useRef, useState } from "react"; +import { Testimonial01 } from "@/components/marketing/testimonial-01"; +import { Testimonial02 } from "@/components/marketing/testimonial-02"; +import { Testimonial03 } from "@/components/marketing/testimonial-03"; +import { Testimonial04 } from "@/components/marketing/testimonial-04"; +import { Testimonial05 } from "@/components/marketing/testimonial-05"; +import { Testimonial06 } from "@/components/marketing/testimonial-06"; +import { Testimonial07 } from "@/components/marketing/testimonial-07"; +import { Testimonial08 } from "@/components/marketing/testimonial-08"; +import { Testimonial09 } from "@/components/marketing/testimonial-09"; +import { Testimonial10 } from "@/components/marketing/testimonial-10"; +import { Testimonial11 } from "@/components/marketing/testimonial-11"; import { ProfileAvatar } from "@/registry/new-york/marketing/profile-avatar"; -import { - Testimonial, - TESTIMONIALS, -} from "@/registry/new-york/marketing/testimonial"; +import { TestimonialProps } from "@/registry/new-york/marketing/testimonial-base"; import { PLASMIC } from "../plasmic-init"; +export const TESTIMONIALS = { + "01": Testimonial01, + "02": Testimonial02, + "03": Testimonial03, + "04": Testimonial04, + "05": Testimonial05, + "06": Testimonial06, + "07": Testimonial07, + "08": Testimonial08, + "09": Testimonial09, + "10": Testimonial10, + "11": Testimonial11, +} as const; + +type TestimonialKind = keyof typeof TESTIMONIALS; // "01" | … | "06" | … + +// 2. Helper type (optional, handy elsewhere) +type TestimonialOf = (typeof TESTIMONIALS)[K]; + +// 3. Generic accessor: returns the *precise* component type +function getTestimonial(kind: K): TestimonialOf { + return TESTIMONIALS[kind]; +} + +export function Testimonial({ + kind, + ...props +}: TestimonialProps & { + kind: K; +}) { + const comp = TESTIMONIALS[kind]; + const { isSelected, ...rest } = usePlasmicCanvasComponentInfo(props) ?? {}; + console.log("plasmic hook", isSelected, rest); + props.selectedInEditor = isSelected; + // const comp2 = getTestimonial(kind); + // return comp2(props); + return comp(props); +} + type TKind = keyof typeof TESTIMONIALS; -const testimonialControl = ({ studioOps, componentProps }) => { +const testimonialControl: React.ComponentType< + ActionProps +> = ({ studioOps, componentProps }) => { const [hoveredKind, setHoveredKind] = useState(null); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const containerRef = useRef(null); @@ -87,7 +140,7 @@ const testimonialControl = ({ studioOps, componentProps }) => { {/* tailwind currently does not work in preview since this runs in */} {/* another iframe. */} {/* TODO: fix tailwind here */} - +
)}
@@ -127,11 +180,11 @@ PLASMIC.registerComponent(ProfileAvatar, { PLASMIC.registerComponent(Testimonial, { name: "Testimonial", - importPath: "@/registry/new-york/marketing/testimonial", + importPath: "@/lib/plasmic-component-registrations/testimonials", props: { kind: { type: "choice", - options: ["01", "02", "03", "04", "05", "06"], + options: Object.keys(TESTIMONIALS), defaultValue: "02", }, content: { diff --git a/registry/new-york/marketing/testimonial.tsx b/registry/new-york/marketing/testimonial.tsx deleted file mode 100644 index c1b3b8ba..00000000 --- a/registry/new-york/marketing/testimonial.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Testimonial01 } from "./testimonial-01"; -import { Testimonial02 } from "./testimonial-02"; -import { Testimonial03 } from "./testimonial-03"; -import { Testimonial04 } from "./testimonial-04"; -import { Testimonial05 } from "./testimonial-05"; -import { Testimonial06 } from "./testimonial-06"; -import { Testimonial07 } from "./testimonial-07"; -import { Testimonial08 } from "./testimonial-08"; -import { Testimonial09 } from "./testimonial-09"; -import { Testimonial10 } from "./testimonial-10"; -import { TestimonialProps } from "./testimonial-base"; - -export const TESTIMONIALS = { - "01": Testimonial01, - "02": Testimonial02, - "03": Testimonial03, - "04": Testimonial04, - "05": Testimonial05, - "06": Testimonial06, - "07": Testimonial07, - "08": Testimonial08, - "09": Testimonial09, - "10": Testimonial10, -} as const; - -type TestimonialKind = keyof typeof TESTIMONIALS; // "01" | … | "06" | … - -// 2. Helper type (optional, handy elsewhere) -type TestimonialOf = (typeof TESTIMONIALS)[K]; - -// 3. Generic accessor: returns the *precise* component type -function getTestimonial(kind: K): TestimonialOf { - return TESTIMONIALS[kind]; -} - -export function Testimonial({ - kind, - ...props -}: TestimonialProps & { - kind: K; -}) { - const comp = TESTIMONIALS[kind]; - // const comp2 = getTestimonial(kind); - // return comp2(props); - return comp(props); -} From de2145488a59f4a99a08ed389867870311eeb951 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 15:40:14 -0400 Subject: [PATCH 18/45] Downgrade to react 18 --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bee8692e..e23d74cd 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ "next": "15.3.1", "next-themes": "^0.4.6", "pnpm": "^10.12.1", - "react": "19.1.0", - "react-dom": "19.1.0", + "react": "18.2.0", + "react-dom": "18.2.0", "react-icons": "^5.5.0", "shadcn": "2.6.0-canary.2", "shiki": "^3.4.2", @@ -64,8 +64,8 @@ "@plasmicapp/cli": "^0.1.338", "@tailwindcss/postcss": "^4", "@types/node": "^20", - "@types/react": "19.1.2", - "@types/react-dom": "19.1.2", + "@types/react": "18", + "@types/react-dom": "18", "@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/utils": "^7.9.0", "@vitest/coverage-v8": "^3.1.3", @@ -84,8 +84,8 @@ }, "pnpm": { "overrides": { - "@types/react": "19.1.2", - "@types/react-dom": "19.1.2" + "@types/react": "18", + "@types/react-dom": "18" } }, "lint-staged": { From ddb2661eca8b5b4b483fb5e1750f8bde5e5ae052 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 15:41:03 -0400 Subject: [PATCH 19/45] Add an animated testimonial component and support for different editor view --- .../new-york/marketing/testimonial-11.tsx | 28 +++----- .../new-york/marketing/testimonial-base.tsx | 9 +++ .../new-york/motion/scroll-reveal-heading.tsx | 68 +++++++++++++------ 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/registry/new-york/marketing/testimonial-11.tsx b/registry/new-york/marketing/testimonial-11.tsx index 0bf0204e..60c8eacc 100644 --- a/registry/new-york/marketing/testimonial-11.tsx +++ b/registry/new-york/marketing/testimonial-11.tsx @@ -3,21 +3,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; import { ScrollRevealHeading } from "@/registry/new-york/motion/scroll-reveal-heading"; - -export interface Testimonial11Props extends React.HTMLAttributes { - /** Full quote as a single string *or* an array of lines */ - content: string; - /** Person quoted */ - name: string; - /** Author role (optional) */ - role?: string; - /** Company or organization (optional) */ - companyName?: string; - /** Total scroll length in vh. If not provided, will be calculated based on word count */ - scrollLength?: number; - /** Render as element/component (defaults to
) */ - as?: React.ElementType; -} +import { TestimonialProps } from "./testimonial-base"; /** * Testimonial11: Full-screen parallax testimonial section. Lines move at various speeds @@ -30,13 +16,15 @@ export function Testimonial11({ role, companyName, scrollLength: scrollLengthProp, + selectedInEditor, className, ...props -}: Testimonial11Props) { +}: TestimonialProps) { return ( @@ -45,9 +33,11 @@ export function Testimonial11({ {name}

{(role || companyName) && ( -

- {[role, companyName].filter(Boolean).join(" · ")} -

+ + {role} + {role && companyName && ", "} + {companyName} + )}
diff --git a/registry/new-york/marketing/testimonial-base.tsx b/registry/new-york/marketing/testimonial-base.tsx index a8fe3900..3df48594 100644 --- a/registry/new-york/marketing/testimonial-base.tsx +++ b/registry/new-york/marketing/testimonial-base.tsx @@ -12,4 +12,13 @@ export interface TestimonialProps extends React.HTMLAttributes { align?: "left" | "center" | "right"; // <-- neeed in testimonial-02 link?: LinkButtonProps; // needed in testimonial-04 linkText?: string; // needed in testimonial-04 + /** Total scroll length in vh. If not provided, will be calculated based on word count */ + scrollLength?: number; // needed in testimonial-11 + /** Render as element/component (defaults to
) */ + as?: React.ElementType; // needed in testimonial-11 + + /** optional argument passed in all components in the editor. This allows components change + * the way they render text for + * in-editor view (e.g. show animated-reveal text or show hidden modals) */ + selectedInEditor?: boolean; } diff --git a/registry/new-york/motion/scroll-reveal-heading.tsx b/registry/new-york/motion/scroll-reveal-heading.tsx index 43405209..b59b0f4b 100644 --- a/registry/new-york/motion/scroll-reveal-heading.tsx +++ b/registry/new-york/motion/scroll-reveal-heading.tsx @@ -3,6 +3,7 @@ import { motion, MotionValue, useScroll, useTransform } from "motion/react"; import * as React from "react"; import { cn } from "@/lib/utils"; +import { innerTextServer as innerText } from "@/lib/utils-server"; interface AnimatedWordProps { word: string; @@ -78,13 +79,17 @@ AnimatedLine.displayName = "AnimatedLine"; export interface ScrollRevealHeadingProps extends React.HTMLAttributes { /** Heading / quote text: separate lines with \n */ - text: string; + text: string | React.ReactElement; /** Total scroll length in vh; calculated automatically if omitted */ scrollLength?: number; /** Render as element/component (defaults to
) */ as?: React.ElementType; /** Optional content rendered beneath the heading (e.g., attribution) */ children?: React.ReactNode; + /** Special editor hook, used here to know when we are in editor and should render + * animation differently for editing + */ + selectedInEditor: boolean; } /** @@ -96,6 +101,7 @@ export function ScrollRevealHeading({ text, scrollLength: scrollLengthProp, className, + selectedInEditor, children, ...props }: ScrollRevealHeadingProps) { @@ -108,7 +114,7 @@ export function ScrollRevealHeading({ const lines = React.useMemo( () => - text + (innerText(text) ?? "") .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean), @@ -120,33 +126,53 @@ export function ScrollRevealHeading({ ); const scrollLength = scrollLengthProp ?? Math.max(200, totalWords * 20); + console.log("selectedInEditor", selectedInEditor); return ( -
+
- {lines.map((line, i) => { - const prefixWords = lines - .slice(0, i) - .join(" ") - .split(/\s+/) - .filter(Boolean); - const indexWordOffset = prefixWords.length; - return ( - - ); - })} + {selectedInEditor != null && React.isValidElement(text) && ( + + {text} + + )} + {selectedInEditor == null && + lines.map((line, i) => { + const prefixWords = lines + .slice(0, i) + .join(" ") + .split(/\s+/) + .filter(Boolean); + const indexWordOffset = prefixWords.length; + return ( + + ); + })}
{children}
From 469e28e4dad1ff3d3e7b0d2c5e462ab79f003f4d Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 15:41:15 -0400 Subject: [PATCH 20/45] Testimonial plasmic integration fixes --- registry/new-york/marketing/testimonial-02.tsx | 4 ++-- registry/new-york/marketing/testimonial-07.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/registry/new-york/marketing/testimonial-02.tsx b/registry/new-york/marketing/testimonial-02.tsx index daca3ae8..f1748269 100644 --- a/registry/new-york/marketing/testimonial-02.tsx +++ b/registry/new-york/marketing/testimonial-02.tsx @@ -51,9 +51,9 @@ export function Testimonial02({ > “ -

+

{content} -

+
diff --git a/registry/new-york/marketing/testimonial-07.tsx b/registry/new-york/marketing/testimonial-07.tsx index ccc583b7..91c04d2a 100644 --- a/registry/new-york/marketing/testimonial-07.tsx +++ b/registry/new-york/marketing/testimonial-07.tsx @@ -49,8 +49,10 @@ export function Testimonial07({
{name} {(role || companyName) && ( - - {[role, companyName].filter(Boolean).join(", ")} + + {role} + {role && companyName && ", "} + {companyName} )}
From 9b5293102cb82d68ff127e953e3007affdc84b78 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 15:44:42 -0400 Subject: [PATCH 21/45] reveal heading typecheck --- registry/new-york/motion/scroll-reveal-heading.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/new-york/motion/scroll-reveal-heading.tsx b/registry/new-york/motion/scroll-reveal-heading.tsx index b59b0f4b..f0087cfc 100644 --- a/registry/new-york/motion/scroll-reveal-heading.tsx +++ b/registry/new-york/motion/scroll-reveal-heading.tsx @@ -89,7 +89,7 @@ export interface ScrollRevealHeadingProps /** Special editor hook, used here to know when we are in editor and should render * animation differently for editing */ - selectedInEditor: boolean; + selectedInEditor?: boolean; } /** From fce98cfb3a2f836112470d229d399a3e6f14eca7 Mon Sep 17 00:00:00 2001 From: Narek Galstyan Date: Thu, 26 Jun 2025 15:45:19 -0400 Subject: [PATCH 22/45] Use a heuristic to handle encoded strings in html --- lib/utils-server.ts | 54 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/utils-server.ts b/lib/utils-server.ts index 6b11d477..3a79c0ec 100644 --- a/lib/utils-server.ts +++ b/lib/utils-server.ts @@ -8,9 +8,57 @@ export function innerTextServer( ) { if (!component || typeof component == "string") return component; if (isValidElement(component)) { - const el = document.createElement("div"); - el.innerHTML = renderToString(component); - return el.innerText; + const html = renderToString(component); + return stripHtml(html); } throw new Error("unknown input type. Expected string or ReactElement"); } + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/** Very small “poor-man’s” HTML-to-text converter. */ +function stripHtml(html: string): string { + const text = html + // remove comments,