Repository: FilecoinFoundationWeb/filecoin-foundation
Pull request #1178: [FF][FFDW] Make Card shareable [skip percy]
Author: login: CharlyMartin
Created: 2025-03-04T17:23:01Z | Merged: 2025-03-05T11:21:07Z

## 📝 Description

This PR moves `Card` & `Meta` in our shared UI package. The implementation on FFDW will be done in a second PR.

## 🛠️ Key Changes

- Moves `Card` & `Meta` in `packages/ui`
- Creates `Card` wrapper in `apps/ff-site/src/app/_components/` to set `BASE_DOMAIN` locally

## 📸 Screenshots

No UI changes.


Diff:
diff --git a/apps/ff-site/src/app/(homepage)/components/FeaturedBlogPosts.tsx b/apps/ff-site/src/app/(homepage)/components/FeaturedBlogPosts.tsx
index 308f489dc4..23b02885da 100644
--- a/apps/ff-site/src/app/(homepage)/components/FeaturedBlogPosts.tsx
+++ b/apps/ff-site/src/app/(homepage)/components/FeaturedBlogPosts.tsx
@@ -7,8 +7,6 @@ import { PATHS } from '@/constants/paths'
 import configData from '@/data/cmsConfigSchema.json'
 import { graphicsData } from '@/data/graphicsData'
 
-
-
 import { Card } from '@/components/Card'
 import { CardGrid } from '@/components/CardGrid'
 
@@ -37,8 +35,7 @@ export function FeaturedBlogPosts({
               metaData={[formatDate(publishedOn)]}
               tags={[{ text: categoryLabel }]}
               title={title}
-              description={description}
-              textIsClamped={true}
+              description={{ text: description, isClamped: true }}
               cta={{
                 href: `${PATHS.BLOG.path}/${slug}`,
                 text: 'Learn More',
diff --git a/apps/ff-site/src/app/(homepage)/components/FeaturedEcosystemProjects.tsx b/apps/ff-site/src/app/(homepage)/components/FeaturedEcosystemProjects.tsx
index eae57aedc6..ca4b55d450 100644
--- a/apps/ff-site/src/app/(homepage)/components/FeaturedEcosystemProjects.tsx
+++ b/apps/ff-site/src/app/(homepage)/components/FeaturedEcosystemProjects.tsx
@@ -23,9 +23,8 @@ export function FeaturedEcosystemProjects({
         <Card
           key={slug}
           title={title}
-          description={description}
-          borderColor="brand-300"
-          textIsClamped={true}
+          description={{ text: description, isClamped: true }}
+          borderColor="lighter"
           cta={{
             href: `${PATHS.ECOSYSTEM_EXPLORER.path}/${slug}`,
             text: 'Learn More',
diff --git a/apps/ff-site/src/app/_components/Card.tsx b/apps/ff-site/src/app/_components/Card.tsx
index 4a0da8bfbf..723c525e39 100644
--- a/apps/ff-site/src/app/_components/Card.tsx
+++ b/apps/ff-site/src/app/_components/Card.tsx
@@ -1,212 +1,26 @@
-import Image, { type ImageProps } from 'next/image'
-
-import {
-  AvatarGroup,
-  type AvatarGroupProps,
-} from '@filecoin-foundation/ui/AvatarGroup'
-import { BaseLink } from '@filecoin-foundation/ui/BaseLink'
-import { Heading } from '@filecoin-foundation/ui/Heading'
-import { Icon } from '@filecoin-foundation/ui/Icon'
-import {
-  type TagGroupProps,
-  TagGroup,
-} from '@filecoin-foundation/ui/TagComponents'
-import { buildImageSizeProp } from '@filecoin-foundation/utils/buildImageSizeProp'
-import { isExternalLink } from '@filecoin-foundation/utils/linkUtils'
-import { type CTAProps } from '@filecoin-foundation/utils/types/ctaType'
-import type {
-  ImageObjectFit,
-  StaticImageProps,
-} from '@filecoin-foundation/utils/types/imageType'
 import {
-  type BreakpointValue,
-  type SpacingValue,
-} from '@filecoin-foundation/utils/types/tailwindTypes'
-import { ArrowUpRight } from '@phosphor-icons/react/dist/ssr'
-import { clsx } from 'clsx'
+  Card as SharedCard,
+  type ExtendedCTAProps,
+  type CardProps as SharedCardProps,
+} from '@filecoin-foundation/ui/Card'
 
 import { BASE_DOMAIN } from '@/constants/siteMetadata'
 
-import { Meta, type MetaDataType } from '@/components/Meta'
-
-type CardImageProps = (StaticImageProps | ImageProps) & {
-  objectFit?: ImageObjectFit
-  padding?: boolean
-  priority?: boolean
-  sizes?: string
-}
-
-type CardProps = {
-  title: string | React.ReactNode
-  tags?: TagGroupProps['tags']
-  metaData?: MetaDataType
-  description?: string
-  cta?: CTAPropsWithSpacing
-  image?: CardImageProps
-  borderColor?: 'brand-300' | 'brand-400' | 'brand-500' | 'brand-600'
-  textIsClamped?: boolean
-  as?: React.ElementType
-  avatars?: AvatarGroupProps['authors']
-}
-
-type LeftProperty = `left-${SpacingValue}`
-type ResponsiveLeftProperty = `${BreakpointValue}:${LeftProperty}`
-
-type CTAPropsWithSpacing = {
-  left?: LeftProperty | [LeftProperty, ResponsiveLeftProperty]
-} & CTAProps
-
-const borderStyles = {
-  'brand-300': 'border-brand-300',
-  'brand-400': 'border-brand-400',
-  'brand-500': 'border-brand-500',
-  'brand-600': 'border-brand-600',
-}
+type CTAWithoutBaseDomain = Omit<ExtendedCTAProps, 'baseDomain'>
 
-export function Card({
-  title,
-  tags,
-  metaData,
-  description,
-  cta,
-  image,
-  borderColor = 'brand-500',
-  textIsClamped = false,
-  as: Tag = 'li',
-  avatars,
-}: CardProps) {
-  return (
-    <Tag
-      className={clsx(
-        'relative h-full rounded-lg border bg-brand-700/30 backdrop-blur-xl',
-        borderStyles[borderColor],
-      )}
-    >
-      {image && <Card.Image image={image} />}
-      <div className="flex flex-col gap-3 p-4">
-        {tags && <TagGroup tags={tags} />}
-        {metaData && <Meta metaData={metaData} />}
-        <Card.Title title={title} />
-        <div className={clsx(cta && 'mb-10')}>
-          {description && (
-            <Card.Description
-              description={description}
-              textIsClamped={textIsClamped}
-            />
-          )}
-          {avatars && <Card.Avatars avatars={avatars} />}
-          {cta && <Card.Link {...cta} />}
-        </div>
-      </div>
-    </Tag>
-  )
+type CardProps = Omit<SharedCardProps, 'cta'> & {
+  cta?: CTAWithoutBaseDomain
 }
 
-Card.Image = function ImageComponent({
-  image,
-}: Required<Pick<CardProps, 'image'>>) {
-  const isStaticImage = 'data' in image
-
-  const commonProps = {
-    alt: image.alt,
-    priority: image.priority,
-    quality: 100,
-    sizes:
-      image.sizes || buildImageSizeProp({ startSize: '100vw', lg: '490px' }),
-    className: clsx(
-      'rounded-lg px-1 pt-1',
-      image.objectFit === 'cover' && 'object-cover',
-      image.objectFit === 'contain' && 'object-contain',
-      image.padding && 'px-6 pt-4',
-    ),
-  }
-
-  if (isStaticImage) {
-    return (
-      <Image
-        {...commonProps}
-        className={clsx(commonProps.className, 'aspect-video')}
-        src={image.data}
-        alt={commonProps.alt}
-      />
-    )
+export function Card({ cta, ...rest }: CardProps) {
+  const ctaWithBaseDomain = cta && {
+    ...cta,
+    baseDomain: BASE_DOMAIN,
   }
 
-  return (
-    <div className="relative aspect-video">
-      <Image
-        fill
-        {...commonProps}
-        className={clsx(commonProps.className, 'h-full w-full')}
-        src={image.src}
-        alt={commonProps.alt}
-      />
-    </div>
-  )
+  return <SharedCard cta={ctaWithBaseDomain} {...rest} />
 }
 
-Card.Title = function Title({ title }: Pick<CardProps, 'title'>) {
-  return typeof title === 'string' ? (
-    <Heading isClamped tag="h3" variant="lg">
-      {title}
-    </Heading>
-  ) : (
-    title
-  )
-}
-
-Card.Description = function Description({
-  description,
-  textIsClamped,
-}: Required<Pick<CardProps, 'description' | 'textIsClamped'>>) {
-  return (
-    <p className={clsx(textIsClamped && 'line-clamp-3 text-ellipsis')}>
-      {description}
-    </p>
-  )
-}
-
-Card.Avatars = function Avatars({
-  avatars,
-}: Required<Pick<CardProps, 'avatars'>>) {
-  return (
-    <div className="mt-6">
-      <AvatarGroup authors={avatars} />
-    </div>
-  )
-}
-
-Card.Link = function Link({
-  href,
-  ariaLabel,
-  icon,
-  text,
-  left = 'left-4',
-}: NonNullable<CardProps['cta']>) {
-  const isExternal = isExternalLink(href, BASE_DOMAIN)
-  const textElement = <span key="text">{text}</span>
-
-  const textIcon = icon
-    ? [<Icon key="custom" component={icon} />, textElement]
-    : isExternal
-      ? [textElement, <Icon key="arrow" component={ArrowUpRight} />]
-      : textElement
-
-  return (
-    <BaseLink
-      href={href}
-      baseDomain={BASE_DOMAIN}
-      aria-label={ariaLabel}
-      className="absolute inset-0 rounded-lg focus:brand-outline"
-    >
-      <span
-        className={clsx(
-          'absolute bottom-4 inline-flex items-center gap-2 text-brand-300',
-          left,
-        )}
-      >
-        {textIcon}
-      </span>
-    </BaseLink>
-  )
+Card.Link = function CardLink({ ...rest }: CTAWithoutBaseDomain) {
+  return <SharedCard.Link {...rest} baseDomain={BASE_DOMAIN} />
 }
diff --git a/apps/ff-site/src/app/_components/PageHeader.tsx b/apps/ff-site/src/app/_components/PageHeader.tsx
index 3707ca4962..7dea7d96a9 100644
--- a/apps/ff-site/src/app/_components/PageHeader.tsx
+++ b/apps/ff-site/src/app/_components/PageHeader.tsx
@@ -5,6 +5,7 @@ import {
   type DescriptionTextType,
 } from '@filecoin-foundation/ui/DescriptionText'
 import { Heading } from '@filecoin-foundation/ui/Heading'
+import { Meta, type MetaDataType } from '@filecoin-foundation/ui/Meta'
 import { buildImageSizeProp } from '@filecoin-foundation/utils/buildImageSizeProp'
 import type {
   ImageObjectFit,
@@ -16,7 +17,6 @@ import {
   type CTAButtonGroupProps,
   CTAButtonGroup,
 } from '@/components/CTAButtonGroup'
-import { Meta, type MetaDataType } from '@/components/Meta'
 import { SectionDivider } from '@/components/SectionDivider'
 
 type TitleProps = {
diff --git a/apps/ff-site/src/app/_styles/globals.css b/apps/ff-site/src/app/_styles/globals.css
index 82338e646a..6a69e5749a 100644
--- a/apps/ff-site/src/app/_styles/globals.css
+++ b/apps/ff-site/src/app/_styles/globals.css
@@ -219,4 +219,38 @@
   .slide-over-panel {
     @apply bg-brand-800;
   }
+
+  /* CARD */
+  .card {
+    @apply rounded-lg bg-brand-700/30;
+
+    &.card-border-lighter {
+      @apply border-brand-300;
+    }
+
+    &.card-border-light {
+      @apply border-brand-400;
+    }
+
+    &.card-border-base {
+      @apply border-brand-500;
+    }
+
+    &.card-border-dark {
+      @apply border-brand-600;
+    }
+  }
+
+  .card-image {
+    @apply rounded-lg px-1 pt-1;
+  }
+
+  .card-link {
+    @apply text-brand-300;
+  }
+
+  /* META */
+  .meta {
+    @apply text-brand-300;
+  }
 }
diff --git a/apps/ff-site/src/app/blog/components/BlogContent.tsx b/apps/ff-site/src/app/blog/components/BlogContent.tsx
index b31c38dda1..450655dbe5 100644
--- a/apps/ff-site/src/app/blog/components/BlogContent.tsx
+++ b/apps/ff-site/src/app/blog/components/BlogContent.tsx
@@ -139,8 +139,7 @@ export function BlogContent({ posts }: BlogContentProps) {
                     <Card
                       key={slug}
                       title={title}
-                      description={description}
-                      textIsClamped={true}
+                      description={{ text: description, isClamped: true }}
                       metaData={[formatDate(publishedOn)]}
                       tags={[{ text: categoryLabel }]}
                       cta={{
diff --git a/apps/ff-site/src/app/digest/page.tsx b/apps/ff-site/src/app/digest/page.tsx
index 3a20d3221b..ea80bcc761 100644
--- a/apps/ff-site/src/app/digest/page.tsx
+++ b/apps/ff-site/src/app/digest/page.tsx
@@ -70,8 +70,7 @@ export default async function Digest() {
                 key={slug}
                 title={title}
                 avatars={authors}
-                description={description}
-                textIsClamped={true}
+                description={{ text: description, isClamped: true }}
                 tags={[
                   { text: `Issue ${issueNumber}` },
                   { text: `Article ${articleNumber}` },
diff --git a/apps/ff-site/src/app/events/[slug]/components/ProgramSection.tsx b/apps/ff-site/src/app/events/[slug]/components/ProgramSection.tsx
index 6f515a526d..33402d8efb 100644
--- a/apps/ff-site/src/app/events/[slug]/components/ProgramSection.tsx
+++ b/apps/ff-site/src/app/events/[slug]/components/ProgramSection.tsx
@@ -37,7 +37,7 @@ export function ProgramSection({ title, kicker, events }: ProgramSectionProps) {
               key={title}
               title={title}
               description={description}
-              borderColor="brand-400"
+              borderColor="light"
               metaData={getMetaData({
                 startDate,
                 endDate,
diff --git a/apps/ff-site/src/app/events/components/EventsContent.tsx b/apps/ff-site/src/app/events/components/EventsContent.tsx
index 016d1c79ca..87387bea1f 100644
--- a/apps/ff-site/src/app/events/components/EventsContent.tsx
+++ b/apps/ff-site/src/app/events/components/EventsContent.tsx
@@ -1,4 +1,3 @@
-
 import { NoSearchResultsMessage } from '@filecoin-foundation/ui/NoSearchResultsMessage'
 import { Pagination, usePagination } from '@filecoin-foundation/ui/Pagination'
 import { Search, useSearch } from '@filecoin-foundation/ui/Search'
@@ -147,9 +146,11 @@ export default function EventsContent({
                     <Card
                       key={slug}
                       title={title}
-                      borderColor="brand-400"
-                      textIsClamped={true}
+                      borderColor="light"
                       tags={[{ text: tagLabel }]}
+                      description={
+                        description && { text: description, isClamped: true }
+                      }
                       metaData={getMetaData({
                         startDate,
                         endDate,
diff --git a/apps/ff-site/src/app/grants/components/FeaturedGrantGraduates.tsx b/apps/ff-site/src/app/grants/components/FeaturedGrantGraduates.tsx
index 69cdde351c..666595b864 100644
--- a/apps/ff-site/src/app/grants/components/FeaturedGrantGraduates.tsx
+++ b/apps/ff-site/src/app/grants/components/FeaturedGrantGraduates.tsx
@@ -23,8 +23,7 @@ export function FeaturedGrantGraduates({
         <Card
           key={slug}
           title={title}
-          description={description}
-          textIsClamped={true}
+          description={{ text: description, isClamped: true }}
           cta={{
             href: `${PATHS.ECOSYSTEM_EXPLORER.path}/${slug}`,
             text: 'Read More',
diff --git a/apps/ff-site/src/app/orbit/components/EventsSection.tsx b/apps/ff-site/src/app/orbit/components/EventsSection.tsx
index 7158bc3e3a..6e84a239cd 100644
--- a/apps/ff-site/src/app/orbit/components/EventsSection.tsx
+++ b/apps/ff-site/src/app/orbit/components/EventsSection.tsx
@@ -100,8 +100,7 @@ function OrbitEvents({ events, searchParams }: OrbitEventsProps) {
                   key={index}
                   title={title}
                   metaData={[formatDate(startDate), city]}
-                  borderColor="brand-400"
-                  textIsClamped={true}
+                  borderColor="light"
                   cta={ctaProps}
                 />
               )
diff --git a/apps/ffdweb-site/src/app/_styles/globals.css b/apps/ffdweb-site/src/app/_styles/globals.css
index c200e96352..7f4842d107 100644
--- a/apps/ffdweb-site/src/app/_styles/globals.css
+++ b/apps/ffdweb-site/src/app/_styles/globals.css
@@ -231,4 +231,9 @@
   .slide-over-panel {
     @apply bg-neutral-950;
   }
+
+  /* META */
+  .meta {
+    @apply text-neutral-50;
+  }
 }
diff --git a/packages/ui/package.json b/packages/ui/package.json
index c4f4ef8b51..4a82df8292 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -17,6 +17,7 @@
     "./BreadCrumbs": "./src/BreadCrumbs.tsx",
     "./BreadCrumbsLayout": "./src/BreadCrumbsLayout.tsx",
     "./BreakpointDebugger": "./src/BreakpointDebugger.tsx",
+    "./Card": "./src/Card.tsx",
     "./DescriptionText": "./src/DescriptionText.tsx",
     "./DigestArticleHeader": "./src/DigestArticleHeader.tsx",
     "./ErrorMessage": "./src/ErrorMessage.tsx",
@@ -25,6 +26,7 @@
     "./Icon": "./src/Icon.tsx",
     "./IconButton": "./src/IconButton.tsx",
     "./MarkdownPage": "./src/MarkdownPage.tsx",
+    "./Meta": "./src/Meta.tsx",
     "./NoResultsMessage": "./src/NoResultsMessage.tsx",
     "./NoSearchResultsMessage": "./src/NoSearchResultsMessage.tsx",
     "./PageLayout": "./src/PageLayout.tsx",
diff --git a/packages/ui/src/Card.tsx b/packages/ui/src/Card.tsx
new file mode 100644
index 0000000000..e76ee39308
--- /dev/null
+++ b/packages/ui/src/Card.tsx
@@ -0,0 +1,213 @@
+import Image, { type ImageProps } from 'next/image'
+
+import {
+  AvatarGroup,
+  type AvatarGroupProps,
+} from '@filecoin-foundation/ui/AvatarGroup'
+import { BaseLink } from '@filecoin-foundation/ui/BaseLink'
+import { Heading } from '@filecoin-foundation/ui/Heading'
+import { Icon } from '@filecoin-foundation/ui/Icon'
+import {
+  type TagGroupProps,
+  TagGroup,
+} from '@filecoin-foundation/ui/TagComponents'
+import { buildImageSizeProp } from '@filecoin-foundation/utils/buildImageSizeProp'
+import { isExternalLink } from '@filecoin-foundation/utils/linkUtils'
+import { type CTAProps } from '@filecoin-foundation/utils/types/ctaType'
+import type {
+  ImageObjectFit,
+  StaticImageProps,
+} from '@filecoin-foundation/utils/types/imageType'
+import {
+  type BreakpointValue,
+  type SpacingValue,
+} from '@filecoin-foundation/utils/types/tailwindTypes'
+import { ArrowUpRight } from '@phosphor-icons/react/dist/ssr'
+import { clsx } from 'clsx'
+
+import { Meta, type MetaDataType } from './Meta'
+
+type CardDescriptionProps = {
+  text: string
+  isClamped?: boolean
+}
+
+type CardImageProps = (StaticImageProps | ImageProps) & {
+  objectFit?: ImageObjectFit
+  padding?: boolean
+  priority?: boolean
+  sizes?: string
+}
+
+type LeftProperty = `left-${SpacingValue}`
+type ResponsiveLeftProperty = `${BreakpointValue}:${LeftProperty}`
+
+export type ExtendedCTAProps = {
+  baseDomain: string
+  left?: LeftProperty | [LeftProperty, ResponsiveLeftProperty]
+} & CTAProps
+
+export type CardProps = {
+  title: string | React.ReactNode
+  tags?: TagGroupProps['tags']
+  metaData?: MetaDataType
+  description?: string | CardDescriptionProps
+  cta?: ExtendedCTAProps
+  image?: CardImageProps
+  borderColor?: keyof typeof borderStyles
+  as?: React.ElementType
+  avatars?: AvatarGroupProps['authors']
+}
+
+const borderStyles = {
+  lighter: 'card-border-lighter',
+  light: 'card-border-light',
+  base: 'card-border-base',
+  dark: 'card-border-dark',
+}
+
+export function Card({
+  title,
+  tags,
+  metaData,
+  description,
+  cta,
+  image,
+  borderColor = 'base',
+  as: Tag = 'li',
+  avatars,
+}: CardProps) {
+  return (
+    <Tag
+      className={clsx(
+        'card relative h-full border backdrop-blur-xl',
+        borderStyles[borderColor],
+      )}
+    >
+      {image && <Card.Image image={image} />}
+      <div className="flex flex-col gap-3 p-4">
+        {tags && <TagGroup tags={tags} />}
+        {metaData && <Meta metaData={metaData} />}
+        <Card.Title title={title} />
+        <div className={clsx(cta && 'mb-10')}>
+          {description && typeof description === 'string' && (
+            <Card.Description text={description} />
+          )}
+          {description && typeof description === 'object' && (
+            <Card.Description {...description} />
+          )}
+          {avatars && <Card.Avatars avatars={avatars} />}
+          {cta && <Card.Link {...cta} />}
+        </div>
+      </div>
+    </Tag>
+  )
+}
+
+Card.Image = function ImageComponent({
+  image,
+}: Required<Pick<CardProps, 'image'>>) {
+  const isStaticImage = 'data' in image
+
+  const commonProps = {
+    alt: image.alt,
+    priority: image.priority,
+    quality: 100,
+    sizes:
+      image.sizes || buildImageSizeProp({ startSize: '100vw', lg: '490px' }),
+    className: clsx(
+      'card-image',
+      image.objectFit === 'cover' && 'object-cover',
+      image.objectFit === 'contain' && 'object-contain',
+      image.padding && 'px-6 pt-4',
+    ),
+  }
+
+  if (isStaticImage) {
+    return (
+      <Image
+        {...commonProps}
+        className={clsx(commonProps.className, 'aspect-video')}
+        src={image.data}
+        alt={commonProps.alt}
+      />
+    )
+  }
+
+  return (
+    <div className="relative aspect-video">
+      <Image
+        fill
+        {...commonProps}
+        className={clsx(commonProps.className, 'h-full w-full')}
+        src={image.src}
+        alt={commonProps.alt}
+      />
+    </div>
+  )
+}
+
+Card.Title = function Title({ title }: Pick<CardProps, 'title'>) {
+  return typeof title === 'string' ? (
+    <Heading isClamped tag="h3" variant="lg">
+      {title}
+    </Heading>
+  ) : (
+    title
+  )
+}
+
+Card.Description = function Description({
+  text,
+  isClamped,
+}: CardDescriptionProps) {
+  return (
+    <p className={clsx(isClamped && 'line-clamp-3 text-ellipsis')}>{text}</p>
+  )
+}
+
+Card.Avatars = function Avatars({
+  avatars,
+}: Required<Pick<CardProps, 'avatars'>>) {
+  return (
+    <div className="mt-6">
+      <AvatarGroup authors={avatars} />
+    </div>
+  )
+}
+
+Card.Link = function Link({
+  href,
+  ariaLabel,
+  icon,
+  text,
+  baseDomain,
+  left = 'left-4',
+}: NonNullable<CardProps['cta']>) {
+  const isExternal = isExternalLink(href, baseDomain)
+  const textElement = <span key="text">{text}</span>
+
+  const textIcon = icon
+    ? [<Icon key="custom" component={icon} />, textElement]
+    : isExternal
+      ? [textElement, <Icon key="arrow" component={ArrowUpRight} />]
+      : textElement
+
+  return (
+    <BaseLink
+      href={href}
+      baseDomain={baseDomain}
+      aria-label={ariaLabel}
+      className="focus:brand-outline absolute inset-0 rounded-lg"
+    >
+      <span
+        className={clsx(
+          'card-link absolute bottom-4 inline-flex items-center gap-2',
+          left,
+        )}
+      >
+        {textIcon}
+      </span>
+    </BaseLink>
+  )
+}
diff --git a/apps/ff-site/src/app/_components/Meta.tsx b/packages/ui/src/Meta.tsx
similarity index 90%
rename from apps/ff-site/src/app/_components/Meta.tsx
rename to packages/ui/src/Meta.tsx
index ad82ed6958..71ca55fd48 100644
--- a/apps/ff-site/src/app/_components/Meta.tsx
+++ b/packages/ui/src/Meta.tsx
@@ -14,7 +14,7 @@ export function Meta({ metaData }: MetaProps) {
   }
 
   return (
-    <div className="flex flex-wrap gap-x-2 gap-y-1 text-brand-300">
+    <div className="meta flex flex-wrap gap-x-2 gap-y-1">
       {metaData.filter(Boolean).map((data, index) => {
         const isNotLastItem = index < metaData.length - 1
         const isNotFirstItem = index > 0


Reviews:
user: login: CharlyMartin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T17:38:21Z
user: login: mirhamasala
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:33:54Z
user: login: mirhamasala
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:34:58Z
user: login: mirhamasala
body: Wonderful! 🙌🏼 Two nits. I changed `AppSiteLayout` to also using `Shared` prefix already. Adding a note to refactor to use a wrapped for `BASE_DOMAIN` for the other components. Love this solution. ❤️
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:36:03Z
user: login: CharlyMartin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T10:09:29Z
user: login: CharlyMartin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T10:12:42Z
user: login: mirhamasala
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T10:14:49Z

Review comments:
diff_hunk: @@ -37,8 +35,7 @@ export function FeaturedBlogPosts({
               metaData={[formatDate(publishedOn)]}
               tags={[{ text: categoryLabel }]}
               title={title}
-              description={description}
-              textIsClamped={true}
+              description={{ text: description, isClamped: true }}
path: apps/ff-site/src/app/(homepage)/components/FeaturedBlogPosts.tsx
user: login: CharlyMartin
body: I grouped `textIsClamped` and `description` in one prop to follow the same logic as the rest of the props.
created_at: 2025-03-04T17:38:21Z
author_association: COLLABORATOR
diff_hunk: @@ -219,4 +219,33 @@
   .slide-over-panel {
     @apply bg-brand-800;
   }
+
+  /* CARD */
+  .card {
+    @apply rounded-lg bg-brand-700/30;
+
+    &.card-border-lighter {
+      @apply border-brand-300;
+    }
+
+    &.card-border-light {
+      @apply border-brand-400;
+    }
+
+    &.card-border-base {
+      @apply border-brand-500;
+    }
+
+    &.card-border-dark {
+      @apply border-brand-600;
+    }
+  }
+
+  .card-image {
+    @apply rounded-lg px-1 pt-1;
+  }
+
+  .card-link {
+    @apply text-brand-300;
+  }
path: apps/ff-site/src/app/_styles/globals.css
user: login: mirhamasala
body: Why are these not grouped under `.card`?
created_at: 2025-03-05T09:33:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,7 +14,7 @@ export function Meta({ metaData }: MetaProps) {
   }
 
   return (
-    <div className="flex flex-wrap gap-x-2 gap-y-1 text-brand-300">
+    <div className="text-brand-300 flex flex-wrap gap-x-2 gap-y-1">
path: packages/ui/src/Meta.tsx
user: login: mirhamasala
body: Should this style be extracted for both apps? `text-brand-300`?
created_at: 2025-03-05T09:34:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,7 +14,7 @@ export function Meta({ metaData }: MetaProps) {
   }
 
   return (
-    <div className="flex flex-wrap gap-x-2 gap-y-1 text-brand-300">
+    <div className="text-brand-300 flex flex-wrap gap-x-2 gap-y-1">
path: packages/ui/src/Meta.tsx
user: login: CharlyMartin
body: Ah yes! Good catch :)
created_at: 2025-03-05T10:09:28Z
author_association: COLLABORATOR
diff_hunk: @@ -219,4 +219,33 @@
   .slide-over-panel {
     @apply bg-brand-800;
   }
+
+  /* CARD */
+  .card {
+    @apply rounded-lg bg-brand-700/30;
+
+    &.card-border-lighter {
+      @apply border-brand-300;
+    }
+
+    &.card-border-light {
+      @apply border-brand-400;
+    }
+
+    &.card-border-base {
+      @apply border-brand-500;
+    }
+
+    &.card-border-dark {
+      @apply border-brand-600;
+    }
+  }
+
+  .card-image {
+    @apply rounded-lg px-1 pt-1;
+  }
+
+  .card-link {
+    @apply text-brand-300;
+  }
path: apps/ff-site/src/app/_styles/globals.css
user: login: CharlyMartin
body: @mirhamasala `Card.Link` is used as a standalone component in some places.

If we nest `.card-link` inside `.card`, then it won't work for standalone components? Right?
created_at: 2025-03-05T10:12:42Z
author_association: COLLABORATOR
diff_hunk: @@ -219,4 +219,33 @@
   .slide-over-panel {
     @apply bg-brand-800;
   }
+
+  /* CARD */
+  .card {
+    @apply rounded-lg bg-brand-700/30;
+
+    &.card-border-lighter {
+      @apply border-brand-300;
+    }
+
+    &.card-border-light {
+      @apply border-brand-400;
+    }
+
+    &.card-border-base {
+      @apply border-brand-500;
+    }
+
+    &.card-border-dark {
+      @apply border-brand-600;
+    }
+  }
+
+  .card-image {
+    @apply rounded-lg px-1 pt-1;
+  }
+
+  .card-link {
+    @apply text-brand-300;
+  }
path: apps/ff-site/src/app/_styles/globals.css
user: login: mirhamasala
body: Ah, I didn't realize. Thank you!
created_at: 2025-03-05T10:14:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-04T17:23:07Z
body: [vc]: #7RsnPKIvHtlWSbZIVQJHGWCzqynGY7Ytg3zdMokwlDI=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJmaWxlY29pbi1mb3VuZGF0aW9uLXNpdGUiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9mZi1zaXRlIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2ZpbGVjb2luLWZvdW5kYXRpb25zLXByb2plY3RzL2ZpbGVjb2luLWZvdW5kYXRpb24tc2l0ZS9EcXNBaGV2UTFyS2JnbU5XSmZEam05VUV5blp6IiwicHJldmlld1VybCI6ImZpbGVjb2luLWZvdW5kYXRpb24tc2ktZ2l0LWMyYWYxNy1maWxlY29pbi1mb3VuZGF0aW9ucy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImZpbGVjb2luLWZvdW5kYXRpb24tc2ktZ2l0LWMyYWYxNy1maWxlY29pbi1mb3VuZGF0aW9ucy1wcm9qZWN0cy52ZXJjZWwuYXBwIn19LHsibmFtZSI6ImZmZHdlYi1zaXRlIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvZmZkd2ViLXNpdGUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZmlsZWNvaW4tZm91bmRhdGlvbnMtcHJvamVjdHMvZmZkd2ViLXNpdGUvMzdTZ2d4UEplaW5uendBY3AxY3VOa2ZLVTZQYSIsInByZXZpZXdVcmwiOiJmZmR3ZWItc2l0ZS1naXQtdXhpdC0yMjk0LWZjMDliOC1maWxlY29pbi1mb3VuZGF0aW9ucy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImZmZHdlYi1zaXRlLWdpdC11eGl0LTIyOTQtZmMwOWI4LWZpbGVjb2luLWZvdW5kYXRpb25zLXByb2plY3RzLnZlcmNlbC5hcHAifX1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **ffdweb-site** | ✅ Ready ([Inspect](https://vercel.com/filecoin-foundations-projects/ffdweb-site/37SggxPJeinnzwAcp1cuNkfKU6Pa)) | [Visit Preview](https://ffdweb-site-git-uxit-2294-fc09b8-filecoin-foundations-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/ffdweb-site-git-uxit-2294-fc09b8-filecoin-foundations-projects.vercel.app?via=pr-comment-feedback-link) | Mar 5, 2025 11:15am |
| **filecoin-foundation-site** | ✅ Ready ([Inspect](https://vercel.com/filecoin-foundations-projects/filecoin-foundation-site/DqsAhevQ1rKbgmNWJfDjm9UEynZz)) | [Visit Preview](https://filecoin-foundation-si-git-c2af17-filecoin-foundations-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/filecoin-foundation-si-git-c2af17-filecoin-foundations-projects.vercel.app?via=pr-comment-feedback-link) | Mar 5, 2025 11:15am |



author_association: NONE
user: login: notion-workspace[bot]
created_at: 2025-03-04T17:23:10Z
body: <a href="https://www.notion.so/FFDW-Create-Adapt-Card-Components-1ac7631f2825809e932fd545fba7a99b?pvs=8&n=github_linkback">[FFDW] Create/Adapt Card Components</a>
author_association: NONE