Quote Block storybook implemented
This commit is contained in:
+15
-4
@@ -1,18 +1,27 @@
|
|||||||
import "../app/globals.css";
|
import "../app/globals.css";
|
||||||
|
|
||||||
// Import Google Fonts for Storybook
|
// Import Google Fonts for Storybook
|
||||||
import { Inter, Bricolage_Grotesque } from "next/font/google";
|
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500"],
|
weight: ["400", "500", "600", "700"],
|
||||||
variable: "--font-inter",
|
variable: "--font-inter",
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bricolageGrotesque = Bricolage_Grotesque({
|
const bricolageGrotesque = Bricolage_Grotesque({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500"],
|
weight: ["400", "500", "700", "800"],
|
||||||
variable: "--font-bricolage-grotesque",
|
variable: "--font-bricolage-grotesque",
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const spaceGrotesk = Space_Grotesk({
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "700"],
|
||||||
|
variable: "--font-space-grotesk",
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @type { import('@storybook/react').Preview } */
|
/** @type { import('@storybook/react').Preview } */
|
||||||
@@ -28,7 +37,9 @@ const preview = {
|
|||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<div className={`${inter.variable} ${bricolageGrotesque.variable}`}>
|
<div
|
||||||
|
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
|
||||||
|
>
|
||||||
<Story />
|
<Story />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import "../app/globals.css";
|
import "../app/globals.css";
|
||||||
|
|
||||||
// Import Google Fonts for Storybook
|
// Import Google Fonts for Storybook
|
||||||
import { Inter, Bricolage_Grotesque } from "next/font/google";
|
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500"],
|
weight: ["400", "500", "600", "700"],
|
||||||
variable: "--font-inter",
|
variable: "--font-inter",
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const bricolageGrotesque = Bricolage_Grotesque({
|
const bricolageGrotesque = Bricolage_Grotesque({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
weight: ["400", "500"],
|
weight: ["400", "500", "700", "800"],
|
||||||
variable: "--font-bricolage-grotesque",
|
variable: "--font-bricolage-grotesque",
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
const spaceGrotesk = Space_Grotesk({
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "700"],
|
||||||
|
variable: "--font-space-grotesk",
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @type { import('@storybook/react').Preview } */
|
/** @type { import('@storybook/react').Preview } */
|
||||||
@@ -28,7 +37,9 @@ const preview = {
|
|||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
<div className={`${inter.variable} ${bricolageGrotesque.variable}`}>
|
<div
|
||||||
|
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
|
||||||
|
>
|
||||||
<Story />
|
<Story />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -140,30 +140,32 @@ const QuoteBlock = ({
|
|||||||
<div className={`flex flex-col ${config.avatarGap}`}>
|
<div className={`flex flex-col ${config.avatarGap}`}>
|
||||||
{/* Avatar with error handling */}
|
{/* Avatar with error handling */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Image
|
{!imageError ? (
|
||||||
src={currentAvatarSrc}
|
<Image
|
||||||
alt={`Portrait of ${author}`}
|
src={avatarSrc}
|
||||||
width={64}
|
alt={`Portrait of ${author}`}
|
||||||
height={64}
|
width={64}
|
||||||
className={`filter sepia ${
|
height={64}
|
||||||
config.avatar
|
className={`filter sepia ${
|
||||||
} transition-opacity duration-300 ${
|
config.avatar
|
||||||
imageLoading ? "opacity-0" : "opacity-100"
|
} transition-opacity duration-300 ${
|
||||||
}`}
|
imageLoading ? "opacity-0" : "opacity-100"
|
||||||
loading="lazy"
|
}`}
|
||||||
onError={handleImageError}
|
loading="lazy"
|
||||||
onLoad={handleImageLoad}
|
onError={handleImageError}
|
||||||
/>
|
onLoad={handleImageLoad}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{imageLoading && (
|
{imageLoading && !imageError && (
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
|
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error state - show initials */}
|
{/* Error state - show initials */}
|
||||||
{imageError && !imageLoading && (
|
{imageError && (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center bg-gray-300 rounded-full ${config.avatar} text-gray-600 font-bold`}
|
className={`flex items-center justify-center bg-gray-300 rounded-full ${config.avatar} text-gray-600 font-bold`}
|
||||||
>
|
>
|
||||||
@@ -184,7 +186,20 @@ const QuoteBlock = ({
|
|||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
className={`font-bricolage-grotesque font-normal ${config.quote} text-[var(--color-content-inverse-primary)] -indent-[0.5em] relative before:content-['\\201C'] after:content-['\\201D'] before:mr-[0.05em] after:ml-[0.05em] before:[font-family:var(--font-bricolage-grotesque)] after:[font-family:var(--font-bricolage-grotesque)]`}
|
data-qopen="“"
|
||||||
|
data-qclose="”"
|
||||||
|
className={[
|
||||||
|
"font-bricolage-grotesque font-normal",
|
||||||
|
config.quote,
|
||||||
|
"text-[var(--color-content-inverse-primary)]",
|
||||||
|
// give space for the hanging open-quote so it's not clipped:
|
||||||
|
"pl-[0.6em] -indent-[0.6em]",
|
||||||
|
// inject quotes
|
||||||
|
"relative before:content-[attr(data-qopen)] after:content-[attr(data-qclose)]",
|
||||||
|
// lock quote glyphs to your display face
|
||||||
|
"before:[font-family:var(--font-bricolage-grotesque)]",
|
||||||
|
"after:[font-family:var(--font-bricolage-grotesque)]",
|
||||||
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{quote}
|
{quote}
|
||||||
</p>
|
</p>
|
||||||
@@ -199,7 +214,16 @@ const QuoteBlock = ({
|
|||||||
</cite>
|
</cite>
|
||||||
{source && (
|
{source && (
|
||||||
<p
|
<p
|
||||||
className={`font-inter font-normal ${config.source} text-[var(--color-content-inverse-primary)] uppercase -indent-[0.5em] relative before:content-['\\201C'] after:content-['\\201D'] before:mr-[0.05em] after:ml-[0.05em] before:[font-family:var(--font-inter)] after:[font-family:var(--font-inter)]`}
|
data-qopen="“"
|
||||||
|
data-qclose="”"
|
||||||
|
className={[
|
||||||
|
"font-inter font-normal",
|
||||||
|
config.source,
|
||||||
|
"text-[var(--color-content-inverse-primary)] uppercase",
|
||||||
|
"pl-[0.6em] -indent-[0.6em]",
|
||||||
|
"relative before:content-[attr(data-qopen)] after:content-[attr(data-qclose)]",
|
||||||
|
"before:[font-family:var(--font-inter)] after:[font-family:var(--font-inter)]",
|
||||||
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{source}
|
{source}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import QuoteBlock from "../app/components/QuoteBlock";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Components/QuoteBlock",
|
||||||
|
component: QuoteBlock,
|
||||||
|
parameters: {
|
||||||
|
layout: "fullscreen",
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component: `
|
||||||
|
A responsive quote section component that displays inspirational governance quotes with author attribution and decorative geometric elements.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Three variants**: compact, standard, and extended layouts
|
||||||
|
- **Responsive design**: Adapts across all breakpoints
|
||||||
|
- **Error handling**: Graceful fallbacks for image loading failures
|
||||||
|
- **Accessibility**: WCAG 2.1 AA compliant with proper ARIA labels
|
||||||
|
- **Design system integration**: Uses design tokens for consistent styling
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
\`\`\`jsx
|
||||||
|
<QuoteBlock
|
||||||
|
variant="standard"
|
||||||
|
quote="Your quote text here..."
|
||||||
|
author="Author Name"
|
||||||
|
source="Source Title"
|
||||||
|
avatarSrc="path/to/avatar.jpg"
|
||||||
|
/>
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
variant: {
|
||||||
|
control: { type: "select" },
|
||||||
|
options: ["compact", "standard", "extended"],
|
||||||
|
description: "Layout variant for different use cases",
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "The quote text to display",
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "Author name for attribution",
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "Source title (book, article, etc.)",
|
||||||
|
},
|
||||||
|
avatarSrc: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "Path to author avatar image",
|
||||||
|
},
|
||||||
|
fallbackAvatarSrc: {
|
||||||
|
control: { type: "text" },
|
||||||
|
description: "Fallback avatar image path",
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
action: "error",
|
||||||
|
description: "Error callback function",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default story
|
||||||
|
export const Default = {
|
||||||
|
args: {
|
||||||
|
variant: "standard",
|
||||||
|
quote:
|
||||||
|
"The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.",
|
||||||
|
author: "Jo Freeman",
|
||||||
|
source: "The Tyranny of Structurelessness",
|
||||||
|
avatarSrc: "assets/Quote_Avatar.svg",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// All variants comparison
|
||||||
|
export const AllVariants = {
|
||||||
|
render: () => (
|
||||||
|
<div className="space-y-8 p-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4">Compact Variant</h3>
|
||||||
|
<QuoteBlock
|
||||||
|
variant="compact"
|
||||||
|
quote="The rules of decision-making must be open and available to everyone."
|
||||||
|
author="Jo Freeman"
|
||||||
|
source="The Tyranny of Structurelessness"
|
||||||
|
avatarSrc="assets/Quote_Avatar.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4">Standard Variant</h3>
|
||||||
|
<QuoteBlock
|
||||||
|
variant="standard"
|
||||||
|
quote="The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized."
|
||||||
|
author="Jo Freeman"
|
||||||
|
source="The Tyranny of Structurelessness"
|
||||||
|
avatarSrc="assets/Quote_Avatar.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold mb-4">Extended Variant</h3>
|
||||||
|
<QuoteBlock
|
||||||
|
variant="extended"
|
||||||
|
quote="The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized."
|
||||||
|
author="Jo Freeman"
|
||||||
|
source="The Tyranny of Structurelessness"
|
||||||
|
avatarSrc="assets/Quote_Avatar.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Side-by-side comparison of all three variants to show the differences in layout, typography, and spacing.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error state simulation
|
||||||
|
export const ErrorState = {
|
||||||
|
args: {
|
||||||
|
variant: "standard",
|
||||||
|
quote:
|
||||||
|
"The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.",
|
||||||
|
author: "Jo Freeman",
|
||||||
|
source: "The Tyranny of Structurelessness",
|
||||||
|
avatarSrc: "invalid-image-path.jpg", // This will trigger error state
|
||||||
|
onError: (error) => console.log("QuoteBlock error:", error),
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
"Error state when avatar image fails to load. Shows initials fallback and error handling.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user