diff --git a/starters/next/components/ui/CTA/CTA.tsx b/starters/next/components/ui/CTA/CTA.tsx index 83c526c..5c3dbd1 100644 --- a/starters/next/components/ui/CTA/CTA.tsx +++ b/starters/next/components/ui/CTA/CTA.tsx @@ -50,7 +50,7 @@ export const CTA = ({ href && ( + ) + )} + + )} + + ) +} + +CTA.displayName = 'CTA' diff --git a/starters/storybook/app/components/ui/CTA/index.ts b/starters/storybook/app/components/ui/CTA/index.ts new file mode 100644 index 0000000..8e67cca --- /dev/null +++ b/starters/storybook/app/components/ui/CTA/index.ts @@ -0,0 +1 @@ +export { CTA } from './CTA' diff --git a/starters/storybook/app/components/ui/CardGroup/CardGroup.tsx b/starters/storybook/app/components/ui/CardGroup/CardGroup.tsx new file mode 100644 index 0000000..f10ecb0 --- /dev/null +++ b/starters/storybook/app/components/ui/CardGroup/CardGroup.tsx @@ -0,0 +1,83 @@ +import { ComponentProps } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '~/components/ui/utils' +import { + Button, + ButtonProps, + SimpleCard, + SimpleCardProps, + TeaserCard, + TeaserCardProps, + LinkProps, +} from '~/components/ui' + +const cardGroupVariants = cva('w-full py-12 md:py-16', { + variants: {}, + defaultVariants: {}, +}) + +type ActionProps = ButtonProps | LinkProps + +type CardItem = SimpleCardProps | TeaserCardProps + +type Props = { + heading?: string | null + subheading?: string | null + description?: string | null + action?: ActionProps | null + cards: CardItem[] +} + +export type CardGroupProps = ComponentProps<'div'> & + VariantProps & + Props + +export const CardGroup = ({ + className, + heading, + subheading, + description, + action, + cards, + ...props +}: CardGroupProps) => { + return ( +
+
+
+ {subheading && ( +
+ {subheading} +
+ )} +

+ {heading} +

+ {description && ( +

+ {description} +

+ )} +
+
+ {cards.map((card, index) => { + return card && card.type === 'simple' ? ( + + ) : ( + + ) + })} +
+ {action && action.href && ( +
+ +
+ )} +
+
+ ) +} + +CardGroup.displayName = 'CardGroup' diff --git a/starters/storybook/app/components/ui/CardGroup/CardGroupSimpleCard.stories.ts b/starters/storybook/app/components/ui/CardGroup/CardGroupSimpleCard.stories.ts new file mode 100644 index 0000000..bacd413 --- /dev/null +++ b/starters/storybook/app/components/ui/CardGroup/CardGroupSimpleCard.stories.ts @@ -0,0 +1,104 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { CardGroup } from '~/components/ui' + +const meta: Meta = { + title: 'Components/Card Group - Simple Card', + component: CardGroup, + tags: ['autodocs'], + argTypes: { + heading: { control: 'text' }, + subheading: { control: 'text' }, + description: { control: 'text' }, + action: { control: 'object' }, + cards: { control: 'object' }, + }, + args: { + heading: 'How it works', + subheading: 'Understand our process', + description: 'Follow these simple steps to get started with our service.', + action: { + text: 'Get Started', + href: '#', + }, + cards: [ + { + type: 'simple', + image: { + src: '/placeholders/icons/drupal-decoupled-hexagon.png', + alt: 'Step 1', + }, + heading: 'Short summary of step one', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.', + }, + { + type: 'simple', + image: { + src: '/placeholders/icons/drupal-decoupled-hexagon.png', + alt: 'Step 2', + }, + heading: 'Short summary of step two', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.', + }, + { + type: 'simple', + image: { + src: '/placeholders/icons/drupal-decoupled-hexagon.png', + alt: 'Step 3', + }, + heading: 'Short summary of step three', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.', + }, + ], + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const WithoutSubheading: Story = { + args: { + subheading: undefined, + }, +} + +export const WithoutDescription: Story = { + args: { + description: undefined, + }, +} + +export const WithoutAction: Story = { + args: { + action: undefined, + }, +} + +export const TwoCards: Story = { + args: { + cards: [ + { + type: 'simple', + image: { + src: '/placeholders/icons/drupal-decoupled-hexagon.png', + alt: 'Step 1', + }, + heading: 'Short summary of step one', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + { + type: 'simple', + image: { + src: '/placeholders/icons/drupal-decoupled-hexagon.png', + alt: 'Step 2', + }, + heading: 'Short summary of step two', + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + }, + ], + }, +} diff --git a/starters/storybook/app/components/ui/CardGroup/CardGroupTeaserCard.stories.ts b/starters/storybook/app/components/ui/CardGroup/CardGroupTeaserCard.stories.ts new file mode 100644 index 0000000..e480311 --- /dev/null +++ b/starters/storybook/app/components/ui/CardGroup/CardGroupTeaserCard.stories.ts @@ -0,0 +1,216 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { CardGroup } from '~/components/ui' + +const meta: Meta = { + title: 'Components/Card Group - Teaser Card', + component: CardGroup, + tags: ['autodocs'], + argTypes: { + heading: { control: 'text' }, + subheading: { control: 'text' }, + description: { control: 'text' }, + action: { control: 'object' }, + cards: { control: 'object' }, + }, + args: { + heading: 'How it works', + subheading: 'Understand our process', + description: 'Follow these simple steps to get started with our service.', + action: { + text: 'Get Started', + href: '#', + }, + cards: [ + { + type: 'teaser', + image: { + src: '/placeholders/drupal-decoupled/landscape-small.png', + alt: 'Blog post 1', + }, + tags: ['Category', 'Featured'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/doc-tahedroid/landscape-small.png', + alt: 'Blog post 2', + }, + tags: ['Category'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/drupal-decoupled/landscape-small.png', + alt: 'Blog post 3', + }, + tags: ['New', 'Popular'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + ], + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const WithoutSubheading: Story = { + args: { + subheading: undefined, + }, +} + +export const WithoutDescriptionAndSubheading: Story = { + args: { + subheading: undefined, + description: undefined, + }, +} + +export const WithoutCategories: Story = { + args: { + heading: 'Headline introducing resources', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.', + cards: [ + { + type: 'teaser', + image: { + src: '/placeholders/doc-tahedroid/landscape-small.png', + alt: 'Blog post 1', + }, + tags: undefined, + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/drupal-decoupled/landscape-small.png', + alt: 'Blog post 2', + }, + tags: undefined, + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/doc-tahedroid/landscape-small.png', + alt: 'Blog post 3', + }, + tags: undefined, + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + ], + }, +} + +export const MixedEmptyCategories: Story = { + args: { + heading: 'Headline introducing resources', + description: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.', + cards: [ + { + type: 'teaser', + image: { + src: '/placeholders/doc-tahedroid/landscape-small.png', + alt: 'Blog post1', + }, + tags: ['Category', 'Featured'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/drupal-decoupled/landscape-small.png', + alt: 'Blog post 1', + }, + tags: undefined, + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/doc-tahedroid/landscape-small.png', + alt: 'Blog post 3', + }, + tags: ['Category', 'Featured'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + ], + }, +} + +export const WithoutAction: Story = { + args: { + action: undefined, + cards: [ + { + type: 'teaser', + image: { + src: '/placeholders/drupal-decoupled/landscape-small.png', + alt: 'Blog post 1', + }, + tags: ['Category', 'Featured'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/doc-tahedroid/landscape-small.png', + alt: 'Blog post 2', + }, + tags: ['Category'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + { + type: 'teaser', + image: { + src: '/placeholders/drupal-decoupled/landscape-small.png', + alt: 'Blog post 3', + }, + tags: ['New', 'Popular'], + heading: 'Blog title heading will go here', + summary: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros.', + link: { href: '#', text: 'Read more' }, + }, + ], + }, +} diff --git a/starters/storybook/app/components/ui/CardGroup/index.ts b/starters/storybook/app/components/ui/CardGroup/index.ts new file mode 100644 index 0000000..fee8034 --- /dev/null +++ b/starters/storybook/app/components/ui/CardGroup/index.ts @@ -0,0 +1 @@ +export { CardGroup } from './CardGroup' diff --git a/starters/storybook/app/components/ui/Colors.mdx b/starters/storybook/app/components/ui/Colors.mdx new file mode 100644 index 0000000..86074ea --- /dev/null +++ b/starters/storybook/app/components/ui/Colors.mdx @@ -0,0 +1,15 @@ +import { Meta, ColorPalette, ColorItem } from '@storybook/blocks' + +import colors from 'tailwindcss/colors' + +# Colors + +This is a list of all the colors available in Tailwind CSS. + + + {Object.entries(colors) + .filter(([, value]) => typeof value !== 'string') + .map(([name, value]) => ( + + ))} + diff --git a/starters/storybook/app/components/ui/FAQ/FAQ.stories.tsx b/starters/storybook/app/components/ui/FAQ/FAQ.stories.tsx new file mode 100644 index 0000000..e17fb37 --- /dev/null +++ b/starters/storybook/app/components/ui/FAQ/FAQ.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { FAQ } from '~/components/ui' + +const meta: Meta = { + title: 'Components/FAQ', + component: FAQ, + tags: ['autodocs'], + args: { + heading: 'Frequently asked questions', + description: + 'Frequently asked questions ordered by popularity. Remember that if the visitor has not committed to the call to action, they may still have questions (doubts) that can be answered.', + questions: [ + { + question: 'What is your return policy?', + answer: + 'We offer a 30-day money-back guarantee for all our products. If you', + }, + { + question: 'How long does shipping take?', + answer: + 'Shipping typically takes 3-5 business days for domestic orders and 7-14 business days for international orders.', + }, + { + question: 'Do you offer international shipping?', + answer: + 'Yes, we ship to most countries worldwide. Shipping costs and delivery times may vary depending on the destination.', + }, + { + question: 'Are your products eco-friendly?', + answer: + 'We strive to use sustainable materials and eco-friendly packaging whenever possible. Many of our products are made from recycled or biodegradable materials.', + }, + { + question: 'How can I contact customer support?', + answer: + 'You can reach our customer support team via email at support@example.com or by phone at 1-800-123-4567, Monday through Friday, 9am to 5pm EST.', + }, + ], + }, + argTypes: { + heading: { control: 'text' }, + description: { control: 'text' }, + questions: { control: 'object' }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const WithoutDescription: Story = { + args: { + description: undefined, + }, +} + +export const FewerQuestions: Story = { + args: { + questions: meta.args!.questions!.slice(0, 3), + }, +} diff --git a/starters/storybook/app/components/ui/FAQ/FAQ.tsx b/starters/storybook/app/components/ui/FAQ/FAQ.tsx new file mode 100644 index 0000000..466b735 --- /dev/null +++ b/starters/storybook/app/components/ui/FAQ/FAQ.tsx @@ -0,0 +1,67 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '~/components/ui/utils' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + RichText, +} from '~/components/ui' +import { ComponentProps } from 'react' + +const faqVariants = cva( + 'w-full max-w-3xl mx-auto px-4 py-8 md:py-12 lg:py-16 text-center', + { + variants: {}, + defaultVariants: {}, + } +) + +type QuestionProps = { + question: string + answer: string +} + +type Props = { + heading: string + description?: string | null + questions: QuestionProps[] +} + +export type FAQProps = ComponentProps<'div'> & + VariantProps & + Props + +export const FAQ = ({ + className, + heading, + description, + questions, + ...props +}: FAQProps) => { + return ( +
+

+ {heading} +

+ {description && ( +

{description}

+ )} + + {questions && + questions.map((item, index) => ( + + + {item.question} + + + + + + ))} + +
+ ) +} + +FAQ.displayName = 'FAQ' diff --git a/starters/storybook/app/components/ui/FAQ/index.ts b/starters/storybook/app/components/ui/FAQ/index.ts new file mode 100644 index 0000000..08eb0e8 --- /dev/null +++ b/starters/storybook/app/components/ui/FAQ/index.ts @@ -0,0 +1 @@ +export { FAQ } from './FAQ' diff --git a/starters/storybook/app/components/ui/Footer/Footer.stories.tsx b/starters/storybook/app/components/ui/Footer/Footer.stories.tsx new file mode 100644 index 0000000..9d912a2 --- /dev/null +++ b/starters/storybook/app/components/ui/Footer/Footer.stories.tsx @@ -0,0 +1,102 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Footer } from '~/components/ui' + +const meta: Meta = { + title: 'Components/Footer', + component: Footer, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + args: { + columns: [ + { + title: 'Column One', + links: [ + { text: 'Link One', href: '#' }, + { text: 'Link Two', href: '#' }, + { text: 'Link Three', href: '#' }, + { text: 'Link Four', href: '#' }, + { text: 'Link Five', href: '#' }, + ], + }, + { + title: 'Column Two', + links: [ + { text: 'Link Six', href: '#' }, + { text: 'Link Seven', href: '#' }, + { text: 'Link Eight', href: '#' }, + { text: 'Link Nine', href: '#' }, + { text: 'Link Ten', href: '#' }, + ], + }, + { + title: 'Column Three', + links: [ + { text: 'Link Eleven', href: '#' }, + { text: 'Link Twelve', href: '#' }, + { text: 'Link Thirteen', href: '#' }, + { text: 'Link Fourteen', href: '#' }, + { text: 'Link Fifteen', href: '#' }, + ], + }, + { + title: 'Column Four', + links: [ + { text: 'Link Sixteen', href: '#' }, + { text: 'Link Seventeen', href: '#' }, + { text: 'Link Eighteen', href: '#' }, + { text: 'Link Nineteen', href: '#' }, + { text: 'Link Twenty', href: '#' }, + ], + }, + { + title: 'Column Five', + links: [ + { text: 'Link Twenty One', href: '#' }, + { text: 'Link Twenty Two', href: '#' }, + { text: 'Link Twenty Three', href: '#' }, + { text: 'Link Twenty Four', href: '#' }, + { text: 'Link Twenty Five', href: '#' }, + ], + }, + { + title: 'Column Six', + links: [ + { text: 'Link Twenty Six', href: '#' }, + { text: 'Link Twenty Seven', href: '#' }, + { text: 'Link Twenty Eight', href: '#' }, + { text: 'Link Twenty Nine', href: '#' }, + { text: 'Link Thirty', href: '#' }, + ], + }, + ], + logo: { + src: '/placeholders/icons/doc-tahedroid.png', + alt: 'Company Logo', + }, + copyrightText: '© 2023 Drupal Decoupled. All rights reserved.', + }, + argTypes: { + columns: { control: 'object' }, + logo: { control: 'object' }, + copyrightText: { control: 'text' }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const FewerColumns: Story = { + args: { + columns: meta.args!.columns!.slice(0, 3), + }, +} + +export const CustomCopyright: Story = { + args: { + copyrightText: '© 2023 Your Company Name. All rights reserved.', + }, +} diff --git a/starters/storybook/app/components/ui/Footer/Footer.tsx b/starters/storybook/app/components/ui/Footer/Footer.tsx new file mode 100644 index 0000000..91429a2 --- /dev/null +++ b/starters/storybook/app/components/ui/Footer/Footer.tsx @@ -0,0 +1,74 @@ +import { ComponentProps } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '~/components/ui/utils' +import { LinkProps, ImageProps } from '~/components/ui' + +const footerVariants = cva('w-full bg-gray-200 ', { + variants: {}, + defaultVariants: {}, +}) + +type FooterColumn = { + title: string + links: LinkProps[] +} + +type Props = { + columns: FooterColumn[] + logo: ImageProps + copyrightText: string +} + +export type FooterProps = ComponentProps<'footer'> & + VariantProps & + Props + +const FooterColumn = ({ title, links }: FooterColumn) => ( +
+
{title}
+
    + {links.map( + (link, index) => + link && + link.href && ( +
  • + + {link.text} + +
  • + ) + )} +
+
+) + +export const Footer = ({ + className, + columns, + logo, + copyrightText, + ...props +}: FooterProps) => { + return ( +
+
+
+ {columns.map((column, index) => ( + + ))} +
+
+
+ {logo.alt} +
+
{copyrightText}
+
+
+
+ ) +} + +Footer.displayName = 'Footer' diff --git a/starters/storybook/app/components/ui/Footer/index.ts b/starters/storybook/app/components/ui/Footer/index.ts new file mode 100644 index 0000000..0302b4a --- /dev/null +++ b/starters/storybook/app/components/ui/Footer/index.ts @@ -0,0 +1 @@ +export { Footer } from './Footer' diff --git a/starters/storybook/app/components/ui/Header/Header.stories.tsx b/starters/storybook/app/components/ui/Header/Header.stories.tsx new file mode 100644 index 0000000..b1408a0 --- /dev/null +++ b/starters/storybook/app/components/ui/Header/Header.stories.tsx @@ -0,0 +1,100 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Header } from '~/components/ui' + +const meta: Meta = { + title: 'Components/Header', + component: Header, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], + args: { + logo: { + src: '/placeholders/icons/drupal-decoupled.png', + alt: 'Company Logo', + }, + navItems: [ + { label: 'Link One', href: '#' }, + { label: 'Link Two', href: '#' }, + { label: 'Link Three', href: '#' }, + { label: 'Link Four', href: '#' }, + ], + actions: [ + { + text: 'Docs', + href: 'https://drupal-decoupled.octahedroid.com/docs', + variant: 'outline', + }, + { + text: 'Get started', + href: 'https://drupal-decoupled.octahedroid.com/', + variant: 'default', + }, + ], + sticky: true, + }, + argTypes: { + logo: { control: 'object' }, + navItems: { control: 'object' }, + actions: { control: 'object' }, + sticky: { control: 'boolean' }, + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const NonStickyHeader: Story = { + args: { + sticky: false, + }, +} + +export const NoActions: Story = { + args: { + actions: undefined, + }, +} + +export const CustomLogo: Story = { + args: { + logo: { + src: '/placeholders/icons/doc-tahedroid.png', + alt: 'Company Logo', + }, + }, +} + +export const SingleAction: Story = { + args: { + actions: [{ text: 'Contact Us', href: '/contact', variant: 'default' }], + }, +} + +export const NestedMenuItems: Story = { + args: { + navItems: [ + { label: 'Home', href: '#' }, + { + label: 'Products', + children: [ + { label: 'Product A', href: '#product-a' }, + { label: 'Product B', href: '#product-b' }, + { label: 'Product C', href: '#product-c' }, + ], + }, + { + label: 'Services', + children: [ + { label: 'Consulting', href: '#consulting' }, + { label: 'Training', href: '#training' }, + { label: 'Support', href: '#support' }, + ], + }, + { label: 'About', href: '#about' }, + { label: 'Contact', href: '#contact' }, + ], + }, +} diff --git a/starters/storybook/app/components/ui/Header/Header.tsx b/starters/storybook/app/components/ui/Header/Header.tsx new file mode 100644 index 0000000..4db15eb --- /dev/null +++ b/starters/storybook/app/components/ui/Header/Header.tsx @@ -0,0 +1,224 @@ +import { ComponentProps, useState } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '~/components/ui/utils' +import { + Button, + ButtonProps, + NavigationMenu, + NavigationMenuList, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuTrigger, + NavigationMenuContent, + ImageProps, +} from '~/components/ui' +import { Menu, X, ChevronDown, ChevronUp } from 'lucide-react' + +const headerVariants = cva('w-full border-b border-border bg-white', { + variants: { + sticky: { + true: 'sticky top-0 z-50', + false: '', + }, + }, + defaultVariants: { + sticky: false, + }, +}) + +type NavItem = { + label: string + href?: string | null + expanded?: boolean + children?: NavItem[] +} + +type Props = { + logo: ImageProps + navItems: NavItem[] + actions: ButtonProps[] +} + +export type HeaderProps = ComponentProps<'header'> & + VariantProps & + Props + +const MobileNavItem = ({ item }: { item: NavItem }) => { + const [isOpen, setIsOpen] = useState(false) + + return ( +
  • + {item.children ? ( +
    + + {isOpen && ( +
      + {item.children.map((child, index) => ( + + ))} +
    + )} +
    + ) : ( + + {item.label} + + )} +
  • + ) +} + +export const Header = ({ + className, + sticky, + logo, + navItems, + actions, + ...props +}: HeaderProps) => { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + + return ( +
    +
    +
    + + {logo.alt} + +
    + + {/* Desktop Navigation */} +
    + + + {navItems?.map((item, index) => ( + + {item.children ? ( + <> + + {item.label} + + +
      + {item.children.map((child, childIndex) => ( +
    • + +
      + {child.label} +
      +
      +
    • + ))} +
    +
    + + ) : ( + + {item.label} + + )} +
    + ))} +
    +
    + {actions && actions.length > 0 && ( +
    + {actions.map( + ({ text, href, internal, variant, ...actionProps }, index) => + href && ( + + ) + )} +
    + )} +
    + + {/* Mobile Menu Button */} +
    + +
    +
    + + {/* Mobile Navigation */} + {mobileMenuOpen && ( +
    + + {actions && actions.length > 0 && ( +
    + {actions.map( + ({ text, href, internal, variant, ...actionProps }, index) => + href && ( + + ) + )} +
    + )} +
    + )} +
    + ) +} + +Header.displayName = 'Header' diff --git a/starters/storybook/app/components/ui/Header/index.ts b/starters/storybook/app/components/ui/Header/index.ts new file mode 100644 index 0000000..e0e2673 --- /dev/null +++ b/starters/storybook/app/components/ui/Header/index.ts @@ -0,0 +1 @@ +export { Header } from './Header' diff --git a/starters/storybook/app/components/ui/Hero/Hero.stories.tsx b/starters/storybook/app/components/ui/Hero/Hero.stories.tsx new file mode 100644 index 0000000..7b74126 --- /dev/null +++ b/starters/storybook/app/components/ui/Hero/Hero.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { Hero } from '~/components/ui' + +const meta: Meta = { + title: 'Components/Hero', + component: Hero, + tags: ['autodocs'], + argTypes: { + heading: { control: 'text' }, + description: { control: 'text' }, + image: { control: 'object' }, + actions: { control: 'object' }, + }, + args: { + heading: 'Welcome to our Hero Section', + description: + 'This is a default description. You can customize it to fit your needs.', + image: { + src: '/placeholders/drupal-decoupled/landscape-large.png', + alt: 'Default hero image', + }, + actions: [ + { text: 'Get Started', href: '#', variant: 'default' }, + { text: 'Learn More', href: '#', variant: 'outline' }, + ], + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const CustomContent: Story = { + args: { + heading: "Resonate with the visitor's problem", + description: + 'Describe exactly what your product or service does to solve this problem. Avoid using verbose words or phrases.', + image: { + src: '/placeholders/doc-tahedroid/landscape-large.png', + alt: 'Custom hero image', + }, + actions: [ + { + text: 'Start Now', + href: '#', + variant: 'default', + className: 'font-bold', + }, + { text: 'Explore', href: '#', variant: 'secondary', internal: false }, + ], + }, +} + +export const SingleAction: Story = { + args: { + heading: 'Hero With One Actions', + actions: [ + { + text: 'Start Now', + href: '#', + variant: 'default', + className: 'font-bold', + }, + ], + }, +} + +export const WithoutActions: Story = { + args: { + heading: 'Hero Without Actions', + actions: undefined, + }, +} diff --git a/starters/storybook/app/components/ui/Hero/Hero.tsx b/starters/storybook/app/components/ui/Hero/Hero.tsx new file mode 100644 index 0000000..63d4e63 --- /dev/null +++ b/starters/storybook/app/components/ui/Hero/Hero.tsx @@ -0,0 +1,74 @@ +import { ComponentProps } from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '~/components/ui/utils' +import { Button, ButtonProps, ImageProps } from '~/components/ui' + +const heroVariants = cva('w-full px-4 py-8 md:py-16 lg:py-24', { + variants: {}, + defaultVariants: {}, +}) + +type Props = { + heading: string + description: string + image?: ImageProps + actions?: ButtonProps[] +} + +export type HeroProps = ComponentProps<'div'> & + VariantProps & + Props + +export const Hero = ({ + className, + heading, + description, + image, + actions, + ...props +}: HeroProps) => { + return ( +
    +
    +
    +

    + {heading} +

    +

    + {description} +

    + {actions && actions.length > 0 && ( +
    + {actions.slice(0, 2).map( + ({ text, href, variant, internal, ...actionProps }, index) => + href && ( + + ) + )} +
    + )} +
    +
    + {image && ( + + )} +
    +
    +
    + ) +} + +Hero.displayName = 'Hero' diff --git a/starters/storybook/app/components/ui/Hero/index.ts b/starters/storybook/app/components/ui/Hero/index.ts new file mode 100644 index 0000000..b8cded6 --- /dev/null +++ b/starters/storybook/app/components/ui/Hero/index.ts @@ -0,0 +1 @@ +export { Hero } from './Hero' diff --git a/starters/storybook/app/components/ui/LogoGroup/LogoGroup.stories.tsx b/starters/storybook/app/components/ui/LogoGroup/LogoGroup.stories.tsx new file mode 100644 index 0000000..b82047f --- /dev/null +++ b/starters/storybook/app/components/ui/LogoGroup/LogoGroup.stories.tsx @@ -0,0 +1,91 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { LogoGroup } from '~/components/ui' + +const meta: Meta = { + title: 'Components/Logo Group', + component: LogoGroup, + tags: ['autodocs'], + argTypes: { + heading: { control: 'text' }, + logos: { control: 'object' }, + }, + args: { + heading: "Trusted by the world's best companies", + logos: [ + { + image: { + src: '/placeholders/icons/doc-tahedroid.png', + alt: 'Octahedroid', + }, + link: { href: 'https://octahedroid.com', internal: false }, + }, + { + image: { + src: '/placeholders/icons/drupal-decoupled.png', + alt: 'Composabase', + }, + link: { href: 'https://composabase.com', internal: false }, + }, + { + image: { + src: '/placeholders/icons/doc-tahedroid.png', + alt: 'Octahedroid', + }, + link: { href: 'https://octahedroid.com', internal: false }, + }, + { + image: { + src: '/placeholders/icons/drupal-decoupled.png', + alt: 'Composabase', + }, + link: { href: 'https://composabase.com', internal: false }, + }, + { + image: { + src: '/placeholders/icons/doc-tahedroid.png', + alt: 'Octahedroid', + }, + link: { href: 'https://octahedroid.com', internal: false }, + }, + { + image: { + src: '/placeholders/icons/drupal-decoupled.png', + alt: 'Composabase', + }, + link: { href: 'https://composabase.com', internal: false }, + }, + ], + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const CustomHeading: Story = { + args: { + heading: 'Our Partners', + }, +} + +export const FewerLogos: Story = { + args: { + logos: [ + { + image: { + src: '/placeholders/icons/doc-tahedroid.png', + alt: 'Octahedroid', + }, + link: { href: 'https://octahedroid.com', internal: false }, + }, + { + image: { + src: '/placeholders/icons/drupal-decoupled.png', + alt: 'Composabase', + }, + link: { href: 'https://composabase.com', internal: false }, + }, + ], + }, +} diff --git a/starters/storybook/app/components/ui/LogoGroup/LogoGroup.tsx b/starters/storybook/app/components/ui/LogoGroup/LogoGroup.tsx new file mode 100644 index 0000000..408670f --- /dev/null +++ b/starters/storybook/app/components/ui/LogoGroup/LogoGroup.tsx @@ -0,0 +1,62 @@ +import { cva, type VariantProps } from 'class-variance-authority' +import { cn } from '~/components/ui/utils' +import { LinkProps, ImageProps } from '~/components/ui' +import { ComponentProps } from 'react' + +const logoGroupVariants = cva('w-full py-8 md:py-12 text-center', { + variants: {}, + defaultVariants: {}, +}) + +type LogoProps = { + image: ImageProps + link: LinkProps +} + +type Props = { + heading: string + logos: LogoProps[] +} + +export type LogoGroupProps = ComponentProps<'div'> & + VariantProps & + Props + +export const LogoGroup = ({ + className, + heading, + logos, + ...props +}: LogoGroupProps) => { + return ( +
    +
    +

    + {heading} +

    +
    + {logos?.map( + ({ link, image }, index) => + link?.href && ( + + {image.alt + + ) + )} +
    +
    +
    + ) +} + +LogoGroup.displayName = 'LogoGroup' diff --git a/starters/storybook/app/components/ui/LogoGroup/index.ts b/starters/storybook/app/components/ui/LogoGroup/index.ts new file mode 100644 index 0000000..79ae597 --- /dev/null +++ b/starters/storybook/app/components/ui/LogoGroup/index.ts @@ -0,0 +1 @@ +export { LogoGroup } from './LogoGroup' diff --git a/starters/storybook/app/components/ui/MainLayout/ArticleLayout.stories.tsx b/starters/storybook/app/components/ui/MainLayout/ArticleLayout.stories.tsx new file mode 100644 index 0000000..4620872 --- /dev/null +++ b/starters/storybook/app/components/ui/MainLayout/ArticleLayout.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { MainLayout, Article, Footer, Header } from '~/components/ui' +import * as ArticleStories from '../Article/Article.stories' +import * as FooterStories from '../Footer/Footer.stories' +import * as HeaderStories from '../Header/Header.stories' +import { HeaderProps } from '../Header/Header' +import { ArticleProps } from '../Article/Article' +import { FooterProps } from '../Footer/Footer' + +const meta: Meta = { + title: 'Layout/Article Layout', + component: MainLayout, + parameters: { + layout: 'fullscreen', + }, +} + +export default meta + +type Story = StoryObj + +console.log(ArticleStories.default.args) + +export const Default: Story = { + args: { + children: ( + <> +
    +
    +