Rule Stack #11

Merged
an.di merged 29 commits from adilallo/component/RuleStack into main 2025-08-26 21:31:42 +00:00
28 changed files with 2086 additions and 209 deletions
-4
View File
@@ -16,11 +16,7 @@ const config = {
options: {}, options: {},
}, },
staticDirs: ["../public"], staticDirs: ["../public"],
managerHead: (head) => `${head}<base href="/communityrulestorybook/">`,
previewHead: (head) => `${head}<base href="/communityrulestorybook/">`,
async viteFinal(cfg) { async viteFinal(cfg) {
// IMPORTANT: Set base path for GitHub Pages sub-path hosting
cfg.base = "/communityrulestorybook/";
// Ensure esbuild treats .js as JSX during dep pre-bundling // Ensure esbuild treats .js as JSX during dep pre-bundling
cfg.optimizeDeps ??= {}; cfg.optimizeDeps ??= {};
cfg.optimizeDeps.esbuildOptions ??= {}; cfg.optimizeDeps.esbuildOptions ??= {};
+33
View File
@@ -1,5 +1,29 @@
import "../app/globals.css"; import "../app/globals.css";
// Import Google Fonts for Storybook
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-inter",
display: "swap",
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ["latin"],
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 } */ /** @type { import('@storybook/react').Preview } */
const preview = { const preview = {
parameters: { parameters: {
@@ -11,6 +35,15 @@ const preview = {
}, },
}, },
}, },
decorators: [
(Story) => (
<div
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
>
<Story />
</div>
),
],
}; };
export default preview; export default preview;
+33
View File
@@ -1,5 +1,29 @@
import "../app/globals.css"; import "../app/globals.css";
// Import Google Fonts for Storybook
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-inter",
display: "swap",
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ["latin"],
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 } */ /** @type { import('@storybook/react').Preview } */
const preview = { const preview = {
parameters: { parameters: {
@@ -11,6 +35,15 @@ const preview = {
}, },
}, },
}, },
decorators: [
(Story) => (
<div
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
>
<Story />
</div>
),
],
}; };
export default preview; export default preview;
+33
View File
@@ -1,5 +1,29 @@
import "../app/globals.css"; import "../app/globals.css";
// Import Google Fonts for Storybook
import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-inter",
display: "swap",
});
const bricolageGrotesque = Bricolage_Grotesque({
subsets: ["latin"],
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 } */ /** @type { import('@storybook/react').Preview } */
const preview = { const preview = {
parameters: { parameters: {
@@ -11,6 +35,15 @@ const preview = {
}, },
}, },
}, },
decorators: [
(Story) => (
<div
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable} font-sans`}
>
<Story />
</div>
),
],
}; };
export default preview; export default preview;
+3 -3
View File
@@ -6,10 +6,10 @@ export default function Avatar({
...props ...props
}) { }) {
const sizeStyles = { const sizeStyles = {
small: "w-[16px] h-[16px]", small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]",
medium: "w-[18px] h-[18px]", medium: "w-[18px] h-[18px]",
large: "w-[24px] h-[24px]", large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
xlarge: "w-[32px] h-[32px]", xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]",
}; };
const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`; const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`;
+2 -2
View File
@@ -5,9 +5,9 @@ export default function AvatarContainer({
...props ...props
}) { }) {
const sizeStyles = { const sizeStyles = {
small: "flex -space-x-2", small: "flex -space-x-[var(--spacing-scale-008)]",
medium: "flex -space-x-[9px]", medium: "flex -space-x-[9px]",
large: "flex -space-x-[10px]", large: "flex -space-x-[var(--spacing-scale-010)]",
xlarge: "flex -space-x-[13px]", xlarge: "flex -space-x-[13px]",
}; };
+1 -1
View File
@@ -10,7 +10,7 @@ export default function HeaderTab({
return ( return (
<div <div
className={`HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-default-brand-primary)] rounded-t-[32px] sm:rounded-t-[32px] md:rounded-t-[32px] lg:rounded-t-[32px] xl:rounded-t-[32px] pl-[var(--spacing-measures-spacing-012)] h-[40px] sm:h-[52px] md:h-[52px] lg:h-[52px] xl:h-[64px] sm:pr-[var(--spacing-scale-006)] md:pl-[var(--spacing-scale-024)] lg:pl-[var(--spacing-scale-024)] xl:pl-[var(--spacing-scale-032)] md:pr-[var(--spacing-scale-012)] lg:pr-[var(--spacing-scale-048)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] ${stretchClasses} ${className}`} className={`HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-t-[32px] sm:rounded-t-[32px] md:rounded-t-[32px] lg:rounded-t-[32px] xl:rounded-t-[32px] pl-[var(--spacing-scale-012)] h-[40px] sm:h-[52px] md:h-[52px] lg:h-[52px] xl:h-[64px] sm:pr-[var(--spacing-scale-006)] md:pl-[var(--spacing-scale-024)] lg:pl-[var(--spacing-scale-024)] xl:pl-[var(--spacing-scale-032)] md:pr-[var(--spacing-scale-012)] lg:pr-[var(--spacing-scale-048)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] ${stretchClasses} ${className}`}
{...props} {...props}
> >
{children} {children}
+2 -2
View File
@@ -8,7 +8,7 @@ const HeroBanner = ({ title, subtitle, description, ctaText, ctaHref }) => {
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]"> <section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
<div className="flex flex-col gap-[var(--spacing-scale-010)]"> <div className="flex flex-col gap-[var(--spacing-scale-010)]">
{/* Frame container for content */} {/* Frame container for content */}
<div className="bg-[var(--color-surface-default-brand-primary)] p-[var(--spacing-scale-012)] sm:p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-096)] lg:px-[var(--spacing-scale-064)] rounded-tl-none rounded-tr-[16px] rounded-br-[16px] rounded-bl-[16px] flex flex-col gap-[var(--spacing-scale-024)] sm:gap-[var(--spacing-scale-024)] md:flex-row md:gap-[var(--spacing-scale-048)] relative overflow-hidden"> <div className="bg-[var(--color-surface-inverse-brand-primary)] p-[var(--spacing-scale-012)] sm:p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-096)] lg:px-[var(--spacing-scale-064)] rounded-tl-none rounded-tr-[var(--radius-measures-radius-medium)] rounded-br-[var(--radius-measures-radius-medium)] rounded-bl-[var(--radius-measures-radius-medium)] flex flex-col gap-[var(--spacing-scale-024)] sm:gap-[var(--spacing-scale-024)] md:flex-row md:gap-[var(--spacing-scale-048)] relative overflow-hidden">
{/* DECORATIONS (behind content) */} {/* DECORATIONS (behind content) */}
<HeroDecor <HeroDecor
className="pointer-events-none absolute z-0 className="pointer-events-none absolute z-0
@@ -30,7 +30,7 @@ const HeroBanner = ({ title, subtitle, description, ctaText, ctaHref }) => {
</div> </div>
{/* Hero Image Container */} {/* Hero Image Container */}
<div className="w-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center"> <div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
<img <img
src="assets/HeroImage.png" src="assets/HeroImage.png"
alt="Hero illustration" alt="Hero illustration"
+1 -1
View File
@@ -3,7 +3,7 @@
const HeroDecor = ({ className = "" }) => { const HeroDecor = ({ className = "" }) => {
return ( return (
<svg <svg
className={`text-[#FDFAA8] opacity-50 ${className}`} className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
viewBox="0 0 1540 645" viewBox="0 0 1540 645"
aria-hidden="true" aria-hidden="true"
overflow="visible" overflow="visible"
+1 -1
View File
@@ -16,7 +16,7 @@ export default function NavigationItem({
// Size styles // Size styles
const sizeStyles = { const sizeStyles = {
default: default:
"px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-016)] gap-[var(--spacing-scale-004)]", "px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] gap-[var(--spacing-scale-004)]",
xsmall: xsmall:
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]", "px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
}; };
+238
View File
@@ -0,0 +1,238 @@
"use client";
import React, { useState } from "react";
import Image from "next/image";
import QuoteDecor from "./QuoteDecor";
const QuoteBlock = ({
variant = "standard",
className = "",
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",
id,
fallbackAvatarSrc = "assets/Quote_Avatar.svg", // Fallback avatar
onError, // Error callback
}) => {
const [imageError, setImageError] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
// Variant configurations
const variants = {
compact: {
container: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)]",
card: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-040)] md:px-[var(--spacing-scale-024)] rounded-[var(--radius-measures-radius-small)]",
gap: "gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]",
avatarGap: "gap-[var(--spacing-scale-012)]",
avatar: "w-[48px] h-[48px] md:w-[64px] md:h-[64px]",
quote: "text-[16px] leading-[120%] md:text-[20px] md:leading-[110%]",
author: "text-[10px] leading-[120%] md:text-[12px]",
source: "text-[10px] leading-[120%] md:text-[12px]",
showDecor: false,
},
standard: {
container:
"md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-016)] lg:p-[var(--spacing-scale-064)]",
card: "py-[var(--spacing-scale-064)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-048)] md:rounded-[var(--radius-measures-radius-medium)] lg:py-[var(--spacing-scale-064)] lg:pl-[120px] lg:pr-[320px]",
gap: "gap-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-048)] lg:gap-[var(--spacing-scale-064)] xl:gap-[105px]",
avatarGap:
"gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-018)] xl:gap-[var(--spacing-scale-032)]",
avatar:
"md:w-[120px] md:h-[120px] lg:w-[150px] lg:h-[150px] xl:w-[200px] xl:h-[200px]",
quote:
"text-[18px] leading-[120%] md:text-[36px] md:leading-[110%] md:tracking-[0px] lg:text-[52px] xl:text-[64px]",
author:
"text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]",
source:
"text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]",
showDecor: true,
},
extended: {
container:
"py-[var(--spacing-scale-048)] px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-080)] lg:px-[var(--spacing-scale-048)]",
card: "py-[var(--spacing-scale-080)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)] md:rounded-[var(--radius-measures-radius-large)] lg:py-[var(--spacing-scale-112)] lg:pl-[160px] lg:pr-[400px]",
gap: "gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-064)] lg:gap-[var(--spacing-scale-080)] xl:gap-[140px]",
avatarGap:
"gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] xl:gap-[var(--spacing-scale-048)]",
avatar:
"w-[80px] h-[80px] md:w-[140px] md:h-[140px] lg:w-[180px] lg:h-[180px] xl:w-[240px] xl:h-[240px]",
quote:
"text-[20px] leading-[120%] md:text-[40px] md:leading-[110%] md:tracking-[0px] lg:text-[60px] xl:text-[72px]",
author:
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
source:
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
showDecor: true,
},
};
const config = variants[variant] || variants.standard;
// Use provided ID or generate a stable one based on content
const baseId = id || `quote-${author.toLowerCase().replace(/\s+/g, "-")}`;
const quoteId = `${baseId}-content`;
const authorId = `${baseId}-author`;
// Error handling functions
const handleImageError = (error) => {
console.warn(
`QuoteBlock: Failed to load avatar image for ${author}:`,
error
);
setImageError(true);
setImageLoading(false);
// Call error callback if provided
if (onError) {
onError({
type: "image_load_error",
message: `Failed to load avatar for ${author}`,
author,
avatarSrc,
error,
});
}
};
const handleImageLoad = () => {
setImageLoading(false);
setImageError(false);
};
// Validate required props
if (!quote || !author) {
console.error("QuoteBlock: Missing required props (quote or author)");
if (onError) {
onError({
type: "missing_props",
message: "QuoteBlock requires quote and author props",
quote: !!quote,
author: !!author,
});
}
return null; // Don't render if missing required props
}
// Determine which avatar to use
const currentAvatarSrc = imageError ? fallbackAvatarSrc : avatarSrc;
return (
<section
className={`${config.container} ${className}`}
aria-labelledby={quoteId}
role="region"
>
<div
className={`${config.card} bg-[var(--color-surface-default-brand-darker-accent)] relative overflow-hidden`}
>
{/* DECORATIONS (behind content) */}
{config.showDecor && (
<QuoteDecor
className="pointer-events-none absolute z-0
left-0 top-0
w-full h-full"
aria-hidden="true"
/>
)}
<div className={`flex flex-col ${config.gap} relative z-10`}>
<div className={`flex flex-col ${config.avatarGap}`}>
{/* Avatar with error handling */}
<div className="relative">
{!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 && !imageError && (
<div
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
/>
)}
{/* Error state - show initials */}
{imageError && (
<div
className={`flex items-center justify-center bg-gray-300 rounded-full ${config.avatar} text-gray-600 font-bold`}
>
<span className="text-sm md:text-base lg:text-lg xl:text-xl">
{author
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</span>
</div>
)}
</div>
<blockquote
id={quoteId}
aria-labelledby={authorId}
className="relative"
>
<p
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>
</blockquote>
</div>
<footer className="flex flex-col gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-012)] xl:gap-[var(--spacing-scale-020)]">
<cite
id={authorId}
className={`font-inter font-normal ${config.author} text-[var(--color-content-inverse-primary)] uppercase not-italic`}
>
{author}
</cite>
{source && (
<p
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>
)}
</footer>
</div>
</div>
</section>
);
};
export default QuoteBlock;
+73
View File
@@ -0,0 +1,73 @@
"use client";
const QuoteDecor = ({ className = "" }) => {
return (
<svg
className={`text-[var(--color-surface-inverse-brand-primary)] opacity-100 w-full h-full md:max-w-[640px] lg:max-w-[850px] xl:max-w-[1100px] ${className}`}
viewBox="400 0 442 163"
aria-hidden="true"
overflow="visible"
preserveAspectRatio="xMinYMin meet"
>
<g fill="currentColor">
{/* Mobile ellipses */}
<g className="md:hidden">
{/* First ellipse - top left */}
<ellipse
cx="490"
cy="80"
rx="300"
ry="100"
transform="rotate(-20 600 90)"
/>
{/* Second ellipse - middle */}
<ellipse
cx="508"
cy="250"
rx="300"
ry="110"
transform="rotate(-25 600 90)"
/>
{/* Third ellipse - bottom right */}
<ellipse
cx="550"
cy="420"
rx="300"
ry="120"
transform="rotate(-25 600 90)"
/>
</g>
{/* MD+ ellipses */}
<g className="hidden md:block">
{/* First ellipse - top left */}
<ellipse
cx="590"
cy="70"
rx="300"
ry="110"
transform="rotate(-30 600 90)"
/>
{/* Second ellipse - middle */}
<ellipse
cx="680"
cy="250"
rx="300"
ry="110"
transform="rotate(-30 600 90)"
/>
{/* Third ellipse - bottom right */}
<ellipse
cx="670"
cy="400"
rx="300"
ry="120"
transform="rotate(-30 600 90)"
/>
</g>
</g>
</svg>
);
};
export default QuoteDecor;
+73
View File
@@ -0,0 +1,73 @@
"use client";
const RuleCard = ({
title,
description,
icon,
backgroundColor = "bg-[var(--color-community-teal-100)]",
className = "",
onClick,
}) => {
const handleClick = () => {
// Basic analytics event tracking
if (typeof window !== "undefined" && window.gtag) {
window.gtag("event", "template_selected", {
template_name: title,
template_type: "governance_pattern",
});
}
// Custom analytics event for other tracking systems
if (typeof window !== "undefined" && window.analytics) {
window.analytics.track("Template Selected", {
templateName: title,
templateType: "governance_pattern",
});
}
if (onClick) onClick();
};
const handleKeyDown = (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleClick();
}
};
return (
<div
className={`${backgroundColor} rounded-[var(--radius-measures-radius-small)] pt-[var(--spacing-scale-012)] pr-[var(--spacing-scale-012)] pl-[var(--spacing-scale-012)] pb-[var(--spacing-scale-024)] md:p-[var(--spacing-scale-024)] md:h-[210px] lg:h-[277px] flex flex-col gap-[18px] shadow-lg backdrop-blur-sm transition-all duration-500 ease-in-out hover:shadow-xl hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-[var(--color-community-teal-500)] focus:ring-offset-2 cursor-pointer min-h-[44px] min-w-[44px] ${className}`}
tabIndex={0}
role="button"
aria-label={`Learn more about ${title} governance pattern`}
onClick={handleClick}
onKeyDown={handleKeyDown}
>
{/* Header Container */}
<div className="grid grid-cols-[auto_1fr] h-[72px] md:h-[80px] lg:h-[138px] border-b border-[var(--color-surface-default-primary)]">
{/* Icon Container */}
{icon && (
<div className="p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-012)] lg:p-[var(--spacing-scale-024)] border-r border-[var(--color-surface-default-primary)] w-fit flex items-center justify-center">
{icon}
</div>
)}
{/* Title Container */}
{title && (
<div className="pl-[var(--spacing-scale-008)] md:pl-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)] flex items-center gap-[var(--spacing-scale-004)]">
<h3 className="font-space-grotesk font-bold text-[20px] md:text-[28px] lg:text-[36px] leading-[28px] md:leading-[36px] lg:leading-[44px] text-[--color-content-inverse-primary]">
{title}
</h3>
</div>
)}
</div>
{description && (
<p className="font-inter font-medium text-[12px] md:text-[14px] lg:text-[18px] leading-[14px] md:leading-[16px] lg:leading-[24px] text-[var(--color-content-inverse-primary)]">
{description}
</p>
)}
</div>
);
};
export default RuleCard;
+103
View File
@@ -0,0 +1,103 @@
"use client";
import React from "react";
import Image from "next/image";
import RuleCard from "./RuleCard";
import Button from "./Button";
const RuleStack = ({ className = "" }) => {
const handleTemplateClick = (templateName) => {
// Basic analytics tracking
if (typeof window !== "undefined") {
if (window.gtag) {
window.gtag("event", "template_click", {
template_name: templateName,
});
}
if (window.analytics) {
window.analytics.track("Template Clicked", {
templateName: templateName,
});
}
}
console.log(`${templateName} template clicked`);
};
return (
<section
className={`w-full bg-transparent py-[var(--spacing-scale-032)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-048)] md:px-[var(--spacing-scale-032)] xmd:py-[var(--spacing-scale-056)] xmd:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)] lg:px-[var(--spacing-scale-064)] xl:py-[var(--spacing-scale-064)] xl:px-[var(--spacing-scale-096)] flex flex-col gap-[var(--spacing-scale-024)] xmd:gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] ${className}`}
>
<div className="flex flex-col gap-[18px] xmd:grid xmd:grid-cols-2 lg:gap-[var(--spacing-scale-024)]">
<RuleCard
title="Consensus clusters"
description="Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council."
icon={
<Image
src="assets/Icon_Sociocracy.svg"
alt="Sociocracy"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
backgroundColor="bg-[var(--color-surface-default-brand-lime)]"
onClick={() => handleTemplateClick("Consensus clusters")}
/>
<RuleCard
title="Consensus"
description="Decisions that affect the group collectively should involve participation of all participants."
icon={
<Image
src="assets/Icon_Consensus.svg"
alt="Consensus"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
backgroundColor="bg-[var(--color-surface-default-brand-rust)]"
onClick={() => handleTemplateClick("Consensus")}
/>
<RuleCard
title="Elected Board"
description="An elected board determines policies and organizes their implementation."
icon={
<Image
src="assets/Icon_ElectedBoard.svg"
alt="Elected Board"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
backgroundColor="bg-[var(--color-surface-default-brand-red)]"
onClick={() => handleTemplateClick("Elected Board")}
/>
<RuleCard
title="Petition"
description="All participants can propose and vote on proposals for the group."
icon={
<Image
src="assets/Icon_Petition.svg"
alt="Petition"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
backgroundColor="bg-[var(--color-surface-default-brand-teal)]"
onClick={() => handleTemplateClick("Petition")}
/>
</div>
{/* See all templates button */}
<div className="flex justify-center">
<Button variant="outlined" size="large">
See all templates
</Button>
</div>
</section>
);
};
export default RuleStack;
+36 -6
View File
@@ -1,19 +1,49 @@
"use client"; "use client";
const SectionHeader = ({ title, subtitle, titleLg }) => { const SectionHeader = ({ title, subtitle, titleLg, variant = "default" }) => {
return ( return (
<div className="flex flex-col gap-1 w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"> <div
className={
variant === "multi-line"
? "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
: "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
}
>
{/* Title Container - Left side (lg breakpoint) */} {/* Title Container - Left side (lg breakpoint) */}
<div className="lg:w-[369px] lg:h-[120px] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"> <div
<h2 className="font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-24 xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-24 text-[var(--color-content-default-primary)]"> className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
: "lg:w-[369px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"
}
>
<h2
className={
variant === "multi-line"
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:font-bold md:text-[32px] md:leading-[40px] lg:w-[410px] lg:text-left xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
}
>
<span className="block lg:hidden">{title}</span> <span className="block lg:hidden">{title}</span>
<span className="hidden lg:block">{titleLg || title}</span> <span className="hidden lg:block">{titleLg || title}</span>
</h2> </h2>
</div> </div>
{/* Subtitle Container */} {/* Subtitle Container */}
<div className="lg:w-[928px] lg:h-[120px] lg:flex lg:items-center lg:justify-end xl:w-[763px] xl:h-[156px] xl:flex xl:items-center xl:justify-end"> <div
<p className="font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"> className={
variant === "multi-line"
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end lg:ml-[var(--spacing-scale-016)] xl:ml-[0px] xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-end"
: "lg:w-[928px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end xl:h-[156px] xl:flex xl:items-center xl:justify-end"
}
>
<p
className={
variant === "multi-line"
? "font-inter font-normal text-[14px] leading-[20px] md:font-normal md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-default-tertiary)]"
: "font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"
}
>
{subtitle} {subtitle}
</p> </p>
</div> </div>
+5
View File
@@ -12,6 +12,11 @@
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
} }
.font-space-grotesk {
font-family: var(--font-space-grotesk), ui-sans-serif, system-ui,
-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
/* Set default body text face */ /* Set default body text face */
html, html,
body { body {
+16 -5
View File
@@ -1,24 +1,35 @@
import { Inter, Bricolage_Grotesque } from "next/font/google"; import { Inter, Bricolage_Grotesque, Space_Grotesk } from "next/font/google";
import "./globals.css"; import "./globals.css";
import HomeHeader from "./components/HomeHeader"; import HomeHeader from "./components/HomeHeader";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
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",
}); });
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en" className="font-sans">
<body className={`${inter.variable} ${bricolageGrotesque.variable}`}> <body
className={`${inter.variable} ${bricolageGrotesque.variable} ${spaceGrotesk.variable}`}
>
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<HomeHeader /> <HomeHeader />
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
+4
View File
@@ -1,6 +1,8 @@
import NumberedCards from "./components/NumberedCards"; import NumberedCards from "./components/NumberedCards";
import HeroBanner from "./components/HeroBanner"; import HeroBanner from "./components/HeroBanner";
import LogoWall from "./components/LogoWall"; import LogoWall from "./components/LogoWall";
import RuleStack from "./components/RuleStack";
import QuoteBlock from "./components/QuoteBlock";
export default function Page() { export default function Page() {
const heroBannerData = { const heroBannerData = {
@@ -39,6 +41,8 @@ export default function Page() {
<HeroBanner {...heroBannerData} /> <HeroBanner {...heroBannerData} />
<LogoWall /> <LogoWall />
<NumberedCards {...numberedCardsData} /> <NumberedCards {...numberedCardsData} />
<RuleStack />
<QuoteBlock />
</div> </div>
); );
} }
+978 -183
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.9511 24.94C50.2744 24.94 54.5897 20.4763 54.5897 14.97C54.5897 9.46372 50.2744 5 44.9511 5C39.6279 5 35.3125 9.46372 35.3125 14.97C35.3125 20.4763 39.6279 24.94 44.9511 24.94Z" fill="black"/>
<path d="M16.0449 54.8404C21.3681 54.8404 25.6835 50.3767 25.6835 44.8704C25.6835 39.3641 21.3681 34.9004 16.0449 34.9004C10.7216 34.9004 6.40625 39.3641 6.40625 44.8704C6.40625 50.3767 10.7216 54.8404 16.0449 54.8404Z" fill="black"/>
<path d="M73.873 54.8404C79.1962 54.8404 83.5116 50.3767 83.5116 44.8704C83.5116 39.3641 79.1962 34.9004 73.873 34.9004C68.5497 34.9004 64.2344 39.3641 64.2344 44.8704C64.2344 50.3767 68.5497 54.8404 73.873 54.8404Z" fill="black"/>
<path d="M18.9667 27.94C24.29 27.94 28.6054 23.4763 28.6054 17.97C28.6054 12.4637 24.29 8 18.9667 8C13.6435 8 9.32812 12.4637 9.32812 17.97C9.32812 23.4763 13.6435 27.94 18.9667 27.94Z" fill="black"/>
<path d="M71.0449 27.8599C76.3681 27.8599 80.6835 23.3962 80.6835 17.8899C80.6835 12.3836 76.3681 7.91992 71.0449 7.91992C65.7216 7.91992 61.4062 12.3836 61.4062 17.8899C61.4062 23.3962 65.7216 27.8599 71.0449 27.8599Z" fill="black"/>
<path d="M44.9511 84.7505C50.2744 84.7505 54.5897 80.2868 54.5897 74.7805C54.5897 69.2743 50.2744 64.8105 44.9511 64.8105C39.6279 64.8105 35.3125 69.2743 35.3125 74.7805C35.3125 80.2868 39.6279 84.7505 44.9511 84.7505Z" fill="black"/>
<path d="M18.873 81.8297C24.1962 81.8297 28.5116 77.3659 28.5116 71.8596C28.5116 66.3534 24.1962 61.8896 18.873 61.8896C13.5497 61.8896 9.23438 66.3534 9.23438 71.8596C9.23438 77.3659 13.5497 81.8297 18.873 81.8297Z" fill="black"/>
<path d="M71.0449 81.8297C76.3681 81.8297 80.6835 77.3659 80.6835 71.8596C80.6835 66.3534 76.3681 61.8896 71.0449 61.8896C65.7216 61.8896 61.4062 66.3534 61.4062 71.8596C61.4062 77.3659 65.7216 81.8297 71.0449 81.8297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.4518 25.2C50.8445 25.2 55.2161 20.6781 55.2161 15.1C55.2161 9.52192 50.8445 5 45.4518 5C40.0591 5 35.6875 9.52192 35.6875 15.1C35.6875 20.6781 40.0591 25.2 45.4518 25.2Z" fill="black"/>
<path d="M19.0299 28.16C24.4226 28.16 28.7942 23.638 28.7942 18.06C28.7942 12.4819 24.4226 7.95996 19.0299 7.95996C13.6372 7.95996 9.26562 12.4819 9.26562 18.06C9.26562 23.638 13.6372 28.16 19.0299 28.16Z" fill="black"/>
<path d="M71.8893 28.16C77.282 28.16 81.6536 23.638 81.6536 18.06C81.6536 12.4819 77.282 7.95996 71.8893 7.95996C66.4966 7.95996 62.125 12.4819 62.125 18.06C62.125 23.638 66.4966 28.16 71.8893 28.16Z" fill="black"/>
<path d="M45.4518 85.7898C50.8445 85.7898 55.2161 81.2679 55.2161 75.6898C55.2161 70.1118 50.8445 65.5898 45.4518 65.5898C40.0591 65.5898 35.6875 70.1118 35.6875 75.6898C35.6875 81.2679 40.0591 85.7898 45.4518 85.7898Z" fill="black"/>
<path d="M19.0299 82.8299C24.4226 82.8299 28.7942 78.308 28.7942 72.7299C28.7942 67.1518 24.4226 62.6299 19.0299 62.6299C13.6372 62.6299 9.26562 67.1518 9.26562 72.7299C9.26562 78.308 13.6372 82.8299 19.0299 82.8299Z" fill="black"/>
<path d="M71.8893 82.8299C77.282 82.8299 81.6536 78.308 81.6536 72.7299C81.6536 67.1518 77.282 62.6299 71.8893 62.6299C66.4966 62.6299 62.125 67.1518 62.125 72.7299C62.125 78.308 66.4966 82.8299 71.8893 82.8299Z" fill="black"/>
<path d="M6.40625 32.3398V58.4498H32.8375L45.4634 45.3998L58.0797 58.4498H84.5109V32.3398H6.40625Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg width="91" height="90" viewBox="0 0 91 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_17413_8953)">
<path d="M45.4518 24.2C50.8445 24.2 55.2161 19.6781 55.2161 14.1C55.2161 8.52192 50.8445 4 45.4518 4C40.0591 4 35.6875 8.52192 35.6875 14.1C35.6875 19.6781 40.0591 24.2 45.4518 24.2Z" fill="black"/>
<path d="M25.9362 44.4002C31.3289 44.4002 35.7005 39.8783 35.7005 34.3002C35.7005 28.7221 31.3289 24.2002 25.9362 24.2002C20.5435 24.2002 16.1719 28.7221 16.1719 34.3002C16.1719 39.8783 20.5435 44.4002 25.9362 44.4002Z" fill="black"/>
<path d="M45.4538 36L6.40625 84.79H84.511L45.4538 36ZM45.4538 74.66C43.5226 74.66 41.6348 74.0676 40.029 72.9578C38.4233 71.848 37.1718 70.2706 36.4328 68.4251C35.6937 66.5796 35.5004 64.5488 35.8771 62.5896C36.2539 60.6304 37.1838 58.8307 38.5494 57.4182C39.915 56.0057 41.6548 55.0438 43.5489 54.6541C45.443 54.2644 47.4062 54.4644 49.1904 55.2288C50.9746 55.9933 52.4996 57.2878 53.5725 58.9487C54.6454 60.6097 55.2181 62.5624 55.2181 64.56C55.2219 65.8889 54.9722 67.2055 54.4832 68.4343C53.9942 69.6632 53.2756 70.7801 52.3685 71.7212C51.4614 72.6622 50.3837 73.4089 49.1972 73.9183C48.0106 74.4278 46.7385 74.69 45.4538 74.69V74.66Z" fill="black"/>
<path d="M64.9831 44.4002C70.3757 44.4002 74.7474 39.8783 74.7474 34.3002C74.7474 28.7221 70.3757 24.2002 64.9831 24.2002C59.5904 24.2002 55.2188 28.7221 55.2188 34.3002C55.2188 39.8783 59.5904 44.4002 64.9831 44.4002Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_17413_8953">
<rect width="78.1048" height="80.79" fill="white" transform="translate(6.40625 4)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="90" height="90" viewBox="0 0 90 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.0071 5C37.0944 4.99804 29.3589 7.34263 22.7788 11.7372C16.1987 16.1318 11.0696 22.3791 8.04025 29.6889C5.01085 36.9986 4.2172 45.0426 5.75968 52.8035C7.30216 60.5643 11.1115 67.6934 16.7059 73.2892C22.3003 78.885 29.4285 82.6961 37.1889 84.2405C44.9494 85.7849 52.9936 84.9932 60.3041 81.9657C67.6146 78.9381 73.8631 73.8105 78.2594 67.2315C82.6556 60.6525 85.0021 52.9176 85.0021 45.005C85.0021 34.3967 80.7887 24.2228 73.2884 16.7207C65.7882 9.21858 55.6153 5.00263 45.0071 5ZM37.6875 69.519C35.4631 71.3261 32.7528 72.4331 29.8993 72.6999C27.0458 72.9666 24.1772 72.3813 21.6564 71.0177C19.1356 69.6542 17.0757 67.5738 15.7372 65.0396C14.3988 62.5054 13.8419 59.6312 14.1369 56.7805C14.432 53.9297 15.5657 51.2305 17.3948 49.0242C19.2239 46.8178 21.6662 45.2033 24.4129 44.385C27.1595 43.5667 30.0871 43.5812 32.8255 44.4267C35.5639 45.2722 37.9901 46.9108 39.7972 49.1352C42.2205 52.118 43.3596 55.9413 42.964 59.764C42.5683 63.5867 40.6703 67.0957 37.6875 69.519ZM30.5562 24.532C30.5542 21.6656 31.4024 18.8631 32.9934 16.4788C34.5845 14.0945 36.8469 12.2357 39.4945 11.1374C42.1422 10.0392 45.056 9.7508 47.8676 10.3088C50.6791 10.8668 53.262 12.2461 55.2895 14.2722C57.3171 16.2984 58.6981 18.8803 59.258 21.6915C59.818 24.5027 59.5316 27.4167 58.4351 30.0651C57.3387 32.7135 55.4814 34.9772 53.0982 36.5698C50.715 38.1625 47.9131 39.0126 45.0467 39.0126C41.2053 39.0126 37.521 37.4873 34.8038 34.772C32.0866 32.0566 30.5588 28.3734 30.5562 24.532ZM72.7005 67.3994C70.8942 69.6252 68.4683 71.2653 65.7296 72.1121C62.991 72.9589 60.0628 72.9745 57.3153 72.1568C54.5679 71.3391 52.1247 69.7248 50.2948 67.5183C48.465 65.3118 47.3307 62.6121 47.0354 59.7608C46.7402 56.9095 47.2973 54.0347 48.6362 51.5001C49.9751 48.9654 52.0357 46.8848 54.5573 45.5215C57.0789 44.1582 59.9482 43.5734 62.8022 43.8411C65.6562 44.1088 68.3667 45.217 70.5908 47.0255C73.5707 49.4486 75.4668 52.9555 75.8624 56.7758C76.258 60.5961 75.1207 64.4172 72.7005 67.3994Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 41 KiB

+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.",
},
},
},
};
+176
View File
@@ -0,0 +1,176 @@
import RuleCard from "../app/components/RuleCard";
import Image from "next/image";
export default {
title: "Components/RuleCard",
component: RuleCard,
parameters: {
layout: "centered",
docs: {
description: {
component:
"An interactive card component that displays governance templates and decision-making patterns. Features hover states, keyboard navigation, analytics tracking, and accessibility support. Use Tab key to test focus indicators and Enter/Space to activate.",
},
},
},
argTypes: {
title: {
control: { type: "text" },
description: "The title of the governance template",
},
description: {
control: { type: "text" },
description: "The description of the governance pattern",
},
backgroundColor: {
control: { type: "select" },
options: [
"bg-[var(--color-surface-default-brand-lime)]",
"bg-[var(--color-surface-default-brand-rust)]",
"bg-[var(--color-surface-default-brand-red)]",
"bg-[var(--color-surface-default-brand-teal)]",
"bg-[var(--color-community-teal-100)]",
],
description: "The background color variant for the card",
},
onClick: { action: "clicked" },
},
tags: ["autodocs"],
};
export const Default = {
args: {
title: "Consensus clusters",
description:
"Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council.",
backgroundColor: "bg-[var(--color-surface-default-brand-lime)]",
icon: (
<Image
src="assets/Icon_Sociocracy.svg"
alt="Sociocracy"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
),
},
};
export const AllVariants = {
render: (args) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl">
<RuleCard
title="Consensus clusters"
description="Units called Circles have the ability to decide and act on matters in their domains, which their members agree on through a Council."
backgroundColor="bg-[var(--color-surface-default-brand-lime)]"
icon={
<Image
src="assets/Icon_Sociocracy.svg"
alt="Sociocracy"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
onClick={() => console.log("Consensus clusters selected")}
/>
<RuleCard
title="Consensus"
description="Decisions that affect the group collectively should involve participation of all participants."
backgroundColor="bg-[var(--color-surface-default-brand-rust)]"
icon={
<Image
src="assets/Icon_Consensus.svg"
alt="Consensus"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
onClick={() => console.log("Consensus selected")}
/>
<RuleCard
title="Elected Board"
description="An elected board determines policies and organizes their implementation."
backgroundColor="bg-[var(--color-surface-default-brand-red)]"
icon={
<Image
src="assets/Icon_ElectedBoard.svg"
alt="Elected Board"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
onClick={() => console.log("Elected Board selected")}
/>
<RuleCard
title="Petition"
description="All participants can propose and vote on proposals for the group."
backgroundColor="bg-[var(--color-surface-default-brand-teal)]"
icon={
<Image
src="assets/Icon_Petition.svg"
alt="Petition"
width={40}
height={40}
className="md:w-[56px] md:h-[56px] lg:w-[90px] lg:h-[90px]"
/>
}
onClick={() => console.log("Petition selected")}
/>
</div>
),
parameters: {
docs: {
description: {
story:
"All four governance template variants with their respective colors and icons. Test hover states, focus indicators, and click interactions.",
},
},
},
};
export const InteractiveStates = {
args: {
title: "Interactive Demo",
description:
"Hover over this card to see the scale and shadow effects. Use Tab to focus and Enter/Space to activate.",
backgroundColor: "bg-[var(--color-community-teal-100)]",
icon: (
<div className="w-10 h-10 md:w-14 md:h-14 lg:w-[90px] lg:h-[90px] bg-white rounded-full flex items-center justify-center">
<span className="text-lg font-bold text-gray-800">?</span>
</div>
),
},
parameters: {
docs: {
description: {
story:
"Demonstrates interactive states including hover effects, focus indicators, and keyboard navigation. Test with mouse hover and keyboard Tab/Enter/Space.",
},
},
},
};
export const AccessibilityTest = {
args: {
title: "Accessibility Demo",
description:
"This card is designed for accessibility testing. Use Tab to focus, Enter/Space to activate, and screen readers to test ARIA labels.",
backgroundColor: "bg-[var(--color-surface-default-brand-teal)]",
icon: (
<div className="w-10 h-10 md:w-14 md:h-14 lg:w-[90px] lg:h-[90px] bg-white rounded-full flex items-center justify-center">
<span className="text-lg font-bold text-gray-800"></span>
</div>
),
},
parameters: {
docs: {
description: {
story:
"Specifically designed for testing accessibility features including keyboard navigation, screen reader support, and focus management.",
},
},
},
};
+39
View File
@@ -0,0 +1,39 @@
import RuleStack from "../app/components/RuleStack";
export default {
title: "Components/RuleStack",
component: RuleStack,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"A complete template library component that displays governance patterns in a responsive grid layout. Includes SectionHeader with multi-line variant, interactive RuleCard components, and a call-to-action button. Features comprehensive accessibility, analytics tracking, and responsive design across all breakpoints.\n\n" +
"**Testing Scenarios:**\n" +
"- **Responsive Testing**: Resize browser window to test layout adaptation from single column on mobile to 2x2 grid on larger screens\n" +
"- **Interactive Testing**: Hover over cards to see effects, use Tab to navigate between cards, and click to see analytics events in console\n" +
"- **Accessibility Testing**: Use screen readers to test ARIA labels, keyboard navigation to move between cards, and verify focus indicators\n" +
"- **Custom Styling**: Add className prop to customize background or other styling",
},
},
},
argTypes: {
className: {
control: { type: "text" },
description: "Additional CSS classes for custom styling",
},
},
tags: ["autodocs"],
};
export const Default = {
args: {},
parameters: {
docs: {
description: {
story:
"The complete RuleStack component with all four governance templates, responsive grid layout, and interactive features. Test hover states, keyboard navigation, and responsive behavior across different screen sizes.",
},
},
},
};
+46 -1
View File
@@ -8,7 +8,7 @@ export default {
docs: { docs: {
description: { description: {
component: component:
"A section header component that displays a title and subtitle with responsive typography and layout. Supports different title text for large breakpoints and maintains consistent spacing across all screen sizes.", "A section header component that displays a title and subtitle with responsive typography and layout. Supports different title text for large breakpoints and maintains consistent spacing across all screen sizes. Includes 'default' and 'multi-line' variants with different layout behaviors.",
}, },
}, },
}, },
@@ -26,6 +26,12 @@ export default {
description: description:
"The title text for lg and xl breakpoints (optional, falls back to title)", "The title text for lg and xl breakpoints (optional, falls back to title)",
}, },
variant: {
control: { type: "select" },
options: ["default", "multi-line"],
description:
"The layout variant - 'default' for traditional layout, 'multi-line' for 50/50 split layout",
},
}, },
tags: ["autodocs"], tags: ["autodocs"],
}; };
@@ -35,6 +41,24 @@ export const Default = {
title: "How CommunityRule works", title: "How CommunityRule works",
subtitle: "Here's a quick overview of the process, from start to finish.", subtitle: "Here's a quick overview of the process, from start to finish.",
titleLg: "How CommunityRule helps", titleLg: "How CommunityRule helps",
variant: "default",
},
};
export const MultiLine = {
args: {
title: "Popular templates",
subtitle:
"These are popular patterns for making decisions in mutual aid and open source communities. You can use them as they are or as a starting place for customizing your own CommunityRule.",
variant: "multi-line",
},
parameters: {
docs: {
description: {
story:
"The multi-line variant creates a 50/50 split layout at lg and xl breakpoints, with the title on the left and subtitle on the right. This variant is used in the RuleStack component.",
},
},
}, },
}; };
@@ -44,6 +68,7 @@ export const CustomContent = {
subtitle: subtitle:
"We're dedicated to helping communities thrive through better decision-making processes and transparent governance structures.", "We're dedicated to helping communities thrive through better decision-making processes and transparent governance structures.",
titleLg: "Building Better Communities", titleLg: "Building Better Communities",
variant: "default",
}, },
parameters: { parameters: {
docs: { docs: {
@@ -61,6 +86,7 @@ export const LongSubtitle = {
subtitle: subtitle:
"This is a much longer subtitle that demonstrates how the component handles extended text content across different breakpoints and layout configurations.", "This is a much longer subtitle that demonstrates how the component handles extended text content across different breakpoints and layout configurations.",
titleLg: "Complex Process Simplified", titleLg: "Complex Process Simplified",
variant: "default",
}, },
parameters: { parameters: {
docs: { docs: {
@@ -78,6 +104,7 @@ export const ResponsiveTest = {
subtitle: subtitle:
"Test the responsive behavior by resizing your browser window or using the viewport controls in Storybook.", "Test the responsive behavior by resizing your browser window or using the viewport controls in Storybook.",
titleLg: "Responsive Design Test", titleLg: "Responsive Design Test",
variant: "default",
}, },
parameters: { parameters: {
docs: { docs: {
@@ -94,6 +121,7 @@ export const WithoutTitleLg = {
title: "Simple Header", title: "Simple Header",
subtitle: subtitle:
"This example doesn't specify a titleLg prop, so it will use the same title text across all breakpoints.", "This example doesn't specify a titleLg prop, so it will use the same title text across all breakpoints.",
variant: "default",
}, },
parameters: { parameters: {
docs: { docs: {
@@ -104,3 +132,20 @@ export const WithoutTitleLg = {
}, },
}, },
}; };
export const MultiLineResponsive = {
args: {
title: "Multi-line Responsive Test",
subtitle:
"This multi-line variant demonstrates the 50/50 split layout at larger breakpoints. Resize your browser to see how the layout adapts from stacked on mobile to side-by-side on desktop.",
variant: "multi-line",
},
parameters: {
docs: {
description: {
story:
"Test the responsive behavior of the multi-line variant. The layout changes from stacked on mobile to 50/50 split on lg and xl breakpoints.",
},
},
},
};