Quote Block storybook implemented

This commit is contained in:
adilallo
2025-08-26 10:40:51 -06:00
parent 28b788c584
commit ac05f33705
4 changed files with 218 additions and 26 deletions
+15 -4
View File
@@ -1,18 +1,27 @@
import "../app/globals.css";
// 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({
subsets: ["latin"],
weight: ["400", "500"],
weight: ["400", "500", "600", "700"],
variable: "--font-inter",
display: "swap",
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ["latin"],
weight: ["400", "500"],
weight: ["400", "500", "700", "800"],
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 } */
@@ -28,7 +37,9 @@ const preview = {
},
decorators: [
(Story) => (
<div className={`${inter.variable} ${bricolageGrotesque.variable}`}>
<div
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
>
<Story />
</div>
),
+15 -4
View File
@@ -1,18 +1,27 @@
import "../app/globals.css";
// 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({
subsets: ["latin"],
weight: ["400", "500"],
weight: ["400", "500", "600", "700"],
variable: "--font-inter",
display: "swap",
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ["latin"],
weight: ["400", "500"],
weight: ["400", "500", "700", "800"],
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 } */
@@ -28,7 +37,9 @@ const preview = {
},
decorators: [
(Story) => (
<div className={`${inter.variable} ${bricolageGrotesque.variable}`}>
<div
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
>
<Story />
</div>
),
+42 -18
View File
@@ -140,30 +140,32 @@ const QuoteBlock = ({
<div className={`flex flex-col ${config.avatarGap}`}>
{/* Avatar with error handling */}
<div className="relative">
<Image
src={currentAvatarSrc}
alt={`Portrait of ${author}`}
width={64}
height={64}
className={`filter sepia ${
config.avatar
} transition-opacity duration-300 ${
imageLoading ? "opacity-0" : "opacity-100"
}`}
loading="lazy"
onError={handleImageError}
onLoad={handleImageLoad}
/>
{!imageError ? (
<Image
src={avatarSrc}
alt={`Portrait of ${author}`}
width={64}
height={64}
className={`filter sepia ${
config.avatar
} transition-opacity duration-300 ${
imageLoading ? "opacity-0" : "opacity-100"
}`}
loading="lazy"
onError={handleImageError}
onLoad={handleImageLoad}
/>
) : null}
{/* Loading state */}
{imageLoading && (
{imageLoading && !imageError && (
<div
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
/>
)}
{/* Error state - show initials */}
{imageError && !imageLoading && (
{imageError && (
<div
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"
>
<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="&ldquo;"
data-qclose="&rdquo;"
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}
</p>
@@ -199,7 +214,16 @@ const QuoteBlock = ({
</cite>
{source && (
<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="&ldquo;"
data-qclose="&rdquo;"
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}
</p>
+146
View File
@@ -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.",
},
},
},
};