diff --git a/client/.storybook/preview.tsx b/client/.storybook/preview.tsx index 4ead0f9dff..1abf4c4d7d 100644 --- a/client/.storybook/preview.tsx +++ b/client/.storybook/preview.tsx @@ -35,7 +35,9 @@ export const decorators = [ - +
+ +
@@ -56,6 +58,7 @@ const preview: Preview = { viewMode: "docs", docs: { toc: true, + inlineStories: true, }, options: { storySort: { diff --git a/client/src/features/dashboardV2/LazyHelpV2.tsx b/client/src/features/dashboardV2/LazyHelpV2.tsx index d1f00e10a3..c8855edb12 100644 --- a/client/src/features/dashboardV2/LazyHelpV2.tsx +++ b/client/src/features/dashboardV2/LazyHelpV2.tsx @@ -19,7 +19,6 @@ import { Suspense, lazy } from "react"; import PageLoader from "../../components/PageLoader"; const HelpV2 = lazy(() => import("./HelpV2")); - export default function LazyHelpV2() { return ( }> diff --git a/client/src/storybook/bootstrap/DesignTokens/BorderShadows.stories.tsx b/client/src/storybook/bootstrap/DesignTokens/BorderShadows.stories.tsx new file mode 100644 index 0000000000..e0e3ab6f83 --- /dev/null +++ b/client/src/storybook/bootstrap/DesignTokens/BorderShadows.stories.tsx @@ -0,0 +1,378 @@ +import cx from "classnames"; +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Copy, CopyIcon } from "~/storybook/bootstrap/utils.tsx"; + +const tokenData = { + border: { + "border-width": { value: "1px" }, + "border-style": { value: "solid" }, + }, + borderRadius: { + "rounded-0": { + value: "0rem", + type: "borderRadius", + description: "No border radius", + extensions: { + px: "0px", + }, + }, + "rounded-1": { + value: "0.2rem", + type: "borderRadius", + description: "Extra small border radius", + extensions: { + px: "3.2px", + }, + }, + "rounded-2": { + value: "0.25rem", + type: "borderRadius", + description: "Small border radius", + extensions: { + px: "4px", + }, + }, + "rounded-3": { + value: "0.3rem", + type: "borderRadius", + description: "Medium border radius", + extensions: { + px: "4.8px", + }, + }, + "rounded-circle": { + value: "50%", + type: "borderRadius", + description: "Perfect circle border radius", + extensions: { + px: "", + }, + }, + "rounded-pill": { + value: "50rem", + type: "borderRadius", + description: "Fully rounded pill shape", + extensions: { + px: "800px", + }, + }, + }, + shadow: { + shadow: { + value: "0 .5rem 1rem rgba(0, 0, 0, .15)", + description: "Default box shadow", + }, + "shadow-sm": { + value: "0 .125rem .25rem rgba(0, 0, 0, .075)", + description: "Small box shadow", + }, + "shadow-lg": { + value: "0 1rem 3rem rgba(0, 0, 0, .175)", + description: "Large box shadow", + }, + "shadow-inset": { + value: "inset 0 1px 2px rgba(0, 0, 0, .075)", + description: "Inset box shadow", + }, + }, +}; + +interface PropertyCardProps { + token: string; + value: string; + px?: string; + notes?: string; +} + +const PropertyCard: React.FC = ({ + token, + value, + px, + notes, +}) => ( +
+
+
+ {token} +
+
+ Value: {value} +
+ {px && ( +
+ PX: {px} +
+ )} +
+ {notes && ( +
+ {notes} +
+ )} +
+); + +interface BorderRadiusExampleCardProps { + token: string; + value: string; + px: string; +} + +const BorderRadiusExampleCard: React.FC = ({ + token, + value, + px, +}) => { + const [copied, setCopied] = useState(""); + const isCircle = token === "rounded-circle"; + + return ( +
+
{token}
+
+ {value} ({px}) +
+
token && Copy(token, setCopied)} + > + {token} + {copied === token && ( + + )} + {copied !== token && } +
+
+ ); +}; + +interface ShadowExampleCardProps { + token: string; + value: string; + description: string; + cssClass?: string; +} + +const ShadowExampleCard: React.FC = ({ + token, + value, + description, +}) => { + const [copied, setCopied] = useState(""); + + return ( +
+
{token}
+
+ {description} +
+
token && Copy(token, setCopied)} + > + {token || "Custom Shadow"} + {copied === token && ( + + )} + {copied !== token && } +
+
+ ); +}; + +const SectionHeader: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( +

+ {children} +

+); + +const SectionDescription: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( +

+ {children} +

+); + +const meta: Meta = { + title: "Design Tokens/Borders & Shadows", + component: () =>
, + parameters: { + docs: { + description: { + component: + "This section defines our visual treatments for **borders** and **shadows**, crucial for defining element boundaries and conveying depth in our UI. These tokens are designed to align with **Bootstrap's border and shadow utilities** (e.g., `.border-radius-*`, `.shadow-*`), ensuring consistent visual presentation and seamless integration across all components. They empower designers and developers to create clear, layered, and modern interfaces.", + }, + }, + layout: "centered", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const BordersAndShadows: Story = { + render: () => ( +
+
+ 1. Core Borders + + Fundamental properties defining the default appearance of borders. + +
+ {Object.entries(tokenData.border).map(([key, data]) => ( + + ))} +
+
+ +
+ 2. Border Radius + + Defines the roundness of element corners, aligning with + Bootstrap's `.rounded-*` classes for consistent visual softness. + +
+ {Object.entries(tokenData.borderRadius).map(([key, data]) => ( + + ))} +
+
+ +
+ 3. Box Shadows + + Adds depth and visual hierarchy using predefined shadow values, + directly corresponding to Bootstrap's `.shadow-*` classes. + +
+ + + + +
+
+
+ ), +}; diff --git a/client/src/storybook/bootstrap/DesignTokens/Colors.stories.tsx b/client/src/storybook/bootstrap/DesignTokens/Colors.stories.tsx new file mode 100644 index 0000000000..3cfff13cca --- /dev/null +++ b/client/src/storybook/bootstrap/DesignTokens/Colors.stories.tsx @@ -0,0 +1,749 @@ +import cx from "classnames"; +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Copy, CopyIcon } from "~/storybook/bootstrap/utils.tsx"; // Assuming this path is correct + +const allColorTokens = { + "brand colors": { + primary: { value: "#006e58", notes: "Main brand color" }, + secondary: { value: "#6c757d", notes: "Secondary UI color" }, + navy: { value: "#01192d", notes: "Custom dark navy" }, + }, + "feedback colors": { + success: { value: "#65a30d", notes: "Success color" }, + info: { value: "#17a2b8", notes: "Info color" }, + warning: { value: "#ffc107", notes: "Warning color" }, + danger: { value: "#dc3545", notes: "Danger color" }, + }, + "body colors": { + "body-color": { value: "#212529", notes: "Default text color" }, + "body-bg": { value: "#ffffff", notes: "Default background color" }, + }, + grayscale: { + white: { value: "#ffffff", notes: "" }, + light: { value: "#f8f9fa", notes: "Light grayscale" }, + dark: { value: "#212529", notes: "Dark grayscale" }, + gray: { value: "#6c757d", notes: "Gray base" }, + "gray-dark": { value: "#343a40", notes: "Dark gray" }, + "gray-100": { value: "#f8f9fa", notes: "Lightest gray" }, + "gray-200": { value: "#e9ecef", notes: "" }, + "gray-300": { value: "#dee2e6", notes: "" }, + "gray-400": { value: "#ced4da", notes: "" }, + "gray-500": { value: "#adb5bd", notes: "" }, + "gray-600": { value: "#6c757d", notes: "" }, + "gray-700": { value: "#495057", notes: "" }, + "gray-800": { value: "#343a40", notes: "" }, + "gray-900": { value: "#212529", notes: "Darkest gray" }, + black: { value: "#000000", notes: "" }, + }, + text: { + "text-primary": { value: "#006e58" }, + "text-secondary": { value: "#6c757d" }, + "text-success": { value: "#65a30d" }, + "text-info": { value: "#17a2b8" }, + "text-warning": { value: "#ffc107" }, + "text-danger": { value: "#dc3545" }, + "text-light": { value: "#f8f9fa" }, + "text-dark": { value: "#212529" }, + "text-body": { value: "#212529" }, + "text-muted": { value: "#6c757d" }, + "text-white": { value: "#ffffff" }, + "text-black-50": { value: "rgba(0, 0, 0, 0.5)" }, + "text-white-50": { value: "rgba(255, 255, 255, 0.5)" }, + "text-primary-emphasis": { value: "#002c23" }, + "text-secondary-emphasis": { value: "#2b2f32" }, + "text-success-emphasis": { value: "#284105" }, + "text-info-emphasis": { value: "#09414a" }, + "text-warning-emphasis": { value: "#664d03" }, + "text-danger-emphasis": { value: "#58151c" }, + "text-light-emphasis": { value: "#495057" }, + "text-dark-emphasis": { value: "#495057" }, + }, + background: { + "bg-primary-subtle": { value: "#cce2de", notes: "" }, + "bg-secondary-subtle": { value: "#e2e3e5", notes: "" }, + "bg-success-subtle": { value: "#e0edcf", notes: "" }, + "bg-info-subtle": { value: "#d1ecf1", notes: "" }, + "bg-warning-subtle": { value: "#fff3cd", notes: "" }, + "bg-danger-subtle": { value: "#f8d7da", notes: "" }, + "bg-light-subtle": { value: "#fcfcfd", notes: "" }, + "bg-dark-subtle": { value: "#ced4da", notes: "" }, + }, + border: { + "border-primary-subtle": { value: "#99c5bc", notes: "" }, + "border-secondary-subtle": { value: "#c4c8cb", notes: "" }, + "border-success-subtle": { value: "#c1da9e", notes: "" }, + "border-info-subtle": { value: "#a2dae3", notes: "" }, + "border-warning-subtle": { value: "#ffe69c", notes: "" }, + "border-danger-subtle": { value: "#f1aeb5", notes: "" }, + "border-light-subtle": { value: "#e9ecef", notes: "" }, + "border-dark-subtle": { value: "#adb5bd", notes: "" }, + }, + "link colors": { + "link-color": { value: "#006e58", notes: "Link color" }, + "link-decoration": { value: "underline", notes: "Default link decoration" }, + "link-hover-color": { value: "#005846", notes: "Link hover color" }, + }, + "border colors": { + "border-color": { value: "#dee2e6", notes: "Default border color" }, + "border-color-translucent": { + value: "rgba(0, 0, 0, 0.175)", + notes: "Translucent border for shadows/dividers", + }, + }, + "form colors": { + "valid-color": { value: "#65a30d", notes: "Valid state color" }, + "valid-border-color": { value: "#65a30d", notes: "Valid border color" }, + "invalid-color": { value: "#dc3545", notes: "Invalid state color" }, + "invalid-border-color": { value: "#dc3545", notes: "Invalid border color" }, + }, + "other colors": { + "code-color": { value: "#d63384", notes: "Code syntax color" }, + "highlight-color": { value: "#16192c", notes: "Highlight text color" }, + "highlight-bg": { value: "#fff3cd", notes: "Highlight background" }, + }, +}; + +// Helper function to convert hex to RGB (simplified, for common hex formats) +const hexToRgb = (hex: string): string | undefined => { + const bigint = parseInt(hex.slice(1), 16); + if (isNaN(bigint)) return undefined; // Handle invalid hex + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return `rgb(${r}, ${g}, ${b})`; +}; + +type ColorSection = { + title: string; + tokens: { token: string; hex: string; rgb?: string; notes?: string }[]; +}; + +const transformedSections: ColorSection[] = [ + { + title: "1. Core Brand Colors", + tokens: Object.entries(allColorTokens["brand colors"]).map( + ([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + }) + ), + }, + { + title: "2. Feedback Colors", + tokens: Object.entries(allColorTokens["feedback colors"]).map( + ([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + }) + ), + }, + { + title: "3. Body Colors", + tokens: Object.entries(allColorTokens["body colors"]).map( + ([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + }) + ), + }, + { + title: "4. Grayscale", + tokens: Object.entries(allColorTokens.grayscale).map(([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + })), + }, + { + title: "5. Text Colors", + tokens: Object.entries(allColorTokens.text).map(([token, data]) => ({ + token, + hex: data.value, + })), + }, + { + title: "6. Subtle Background Colors", // Renaming from original to match new structure + tokens: Object.entries(allColorTokens.background).map(([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + })), + }, + { + title: "7. Subtle Border Colors", // Renaming from original to match new structure + tokens: Object.entries(allColorTokens.border).map(([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes ?? "", + })), + }, + { + title: "8. Link Colors", // Reordered based on new JSON + tokens: Object.entries(allColorTokens["link colors"]) + .filter(([key]) => key !== "link-decoration") + .map(([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + })), + }, + { + title: "9. Border Colors", // Reordered based on new JSON + tokens: Object.entries(allColorTokens["border colors"]).map( + ([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + }) + ), + }, + { + title: "10. Form Validation Colors", // Reordered based on new JSON + tokens: Object.entries(allColorTokens["form colors"]).map( + ([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + }) + ), + }, + { + title: "11. Other Colors", // Reordered based on new JSON + tokens: Object.entries(allColorTokens["other colors"]).map( + ([token, data]) => ({ + token, + hex: data.value, + rgb: hexToRgb(data.value), + notes: data.notes, + }) + ), + }, +]; + +const ColorCard: React.FC<{ + token: string; + hex: string; + rgb?: string; + notes?: string; +}> = ({ token, hex, rgb, notes }) => { + const [copied, setCopied] = useState(""); + const bg = hex || rgb || "#fff"; + return ( +
+ {/* Top section: Color display */} +
+ {/* Bottom section: Color details */} +
+
token && Copy(token, setCopied)} + > + Token: + {token} + {copied === token && ( + + )} + {copied !== token && } +
+
hex && Copy(hex, setCopied)} + > + {hex} + {copied === hex && ( + + )} + {copied !== hex && } +
+ {rgb && ( +
Copy(rgb, setCopied)} + > + {rgb} + {copied === rgb && ( + + )} + {copied !== rgb && } +
+ )} + {notes && ( +
{notes}
+ )} +
+
+ ); +}; + +const TextColorCard: React.FC<{ + token: string; + color: string; + bgColor: string; + notes?: string; +}> = ({ token, color, bgColor }) => { + const [copied, setCopied] = useState(""); + return ( +
+
+

+ The quick brown fox jumps over the lazy dog. +

+
+
+
+ Token: + {token} + token && Copy(token, setCopied)} + > + {copied === token && ( + + )} + {copied !== token && } + +
+
token && Copy(color, setCopied)} + > + {color} + {copied === color && ( + + )} + {copied !== color && } +
+
+
+ ); +}; + +const BorderColorCard: React.FC<{ + token: string; + hex: string; + rgb?: string; + borderSize: string; +}> = ({ token, hex, rgb, borderSize }) => { + const [copied, setCopied] = useState(""); + const borderColor = hex || rgb || "#fff"; // Use the color for the border + const displayBg = + borderColor === "#ffffff" || borderColor === "rgb(255, 255, 255)" + ? "#f8f9fa" + : "#ffffff"; // A subtle background for white borders + + return ( +
+
+
token && Copy(token, setCopied)} + > + Token: + {token} + {copied === token && ( + + )} + {copied !== token && } +
+
hex && Copy(hex, setCopied)} + > + {hex} + {copied === hex && ( + + )} + {copied !== hex && } +
+ {rgb && ( +
Copy(rgb, setCopied)} + > + {rgb} + {copied === rgb && ( + + )} + {copied !== rgb && } +
+ )} +
+
+ ); +}; + +const Section: React.FC<{ + title: string; + tokens: { token: string; hex: string; rgb?: string; notes?: string }[]; +}> = ({ title, tokens }) => ( +
+

+ {title} +

+
+ {tokens.map((c) => ( + + ))} +
+
+); + +const SectionBorder: React.FC<{ + title: string; + tokens: { token: string; hex: string; rgb?: string; notes?: string }[]; + borderSize: string; +}> = ({ title, tokens, borderSize }) => ( +
+

+ {title} +

+
+ {tokens.map((c) => ( + + ))} +
+
+); + +const SectionText: React.FC<{ title: string; tokens: { token: string }[] }> = ({ + title, +}) => ( +
+

+ {title} +

+

+ They directly correspond to Bootstrap's `.text-*` utility classes. +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+); + +const meta: Meta = { + title: "Design Tokens/Colors", + parameters: { + docs: { + description: { + component: + "Each color shows hex and RGB (if available), with copy-to-clipboard functionality This palette is designed to work seamlessly with **Bootstrap's extensive color utility classes**.", + }, + }, + layout: "centered", + }, + argTypes: { + borderSize: { + options: ["border-1", "border-2", "border-3", "border-4", "border-5"], + control: { type: "select" }, + description: "Sets the thickness of the border using Bootstrap classes.", + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const ColorPalette: Story = { + args: { + borderSize: "border-1", // Default value for the control + }, + render: (args) => { + //eslint-disable-next-line @typescript-eslint/no-explicit-any + const renderSection = (section: { title: string; tokens: any[] }) => { + switch (section.title) { + case "5. Text Colors": + return ( + + ); + case "9. Border Colors": + case "7. Subtle Border Colors": + return ( + + ); + default: + return ( +
+ ); + } + }; + return ( +
+ {transformedSections.map(renderSection)} +
+ ); + }, +}; diff --git a/client/src/storybook/bootstrap/DesignTokens/Fonts.stories.tsx b/client/src/storybook/bootstrap/DesignTokens/Fonts.stories.tsx new file mode 100644 index 0000000000..c068f02420 --- /dev/null +++ b/client/src/storybook/bootstrap/DesignTokens/Fonts.stories.tsx @@ -0,0 +1,352 @@ +import cx from "classnames"; +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Copy, CopyIcon } from "~/storybook/bootstrap/utils.tsx"; + +const fontTokens = { + typography: { + "body-font-family": { value: '"Inter", sans-serif', px: "" }, + "body-font-size": { value: "1rem", px: "16px" }, + "body-font-weight": { value: "400", px: "" }, + "body-line-height": { value: "1.5", px: "" }, + }, + fontSizes: { + "fs-1": { value: "2.5rem", px: "40px", type: "fontSizes" }, + "fs-2": { value: "2rem", px: "32px", type: "fontSizes" }, + "fs-3": { value: "1.75rem", px: "28px", type: "fontSizes" }, + "fs-4": { value: "1.5rem", px: "24px", type: "fontSizes" }, + "fs-5": { value: "1.25rem", px: "20px", type: "fontSizes" }, + "fs-6": { value: "1rem", px: "16px", type: "fontSizes" }, + }, + lineHeight: { + "1": { + value: 1, + type: "lineHeight", + description: "Tight line height", + extensions: { + px: "16px", + rem: "1rem", + }, + }, + sm: { + value: 1.25, + type: "lineHeight", + description: "Small line height", + extensions: { + px: "20px", + rem: "1.25rem", + }, + }, + base: { + value: 1.5, + type: "lineHeight", + description: "Base/default line height", + extensions: { + px: "24px", + rem: "1.5rem", + }, + }, + lg: { + value: 2, + type: "lineHeight", + description: "Large line height", + extensions: { + px: "32px", + rem: "2rem", + }, + }, + }, +}; + +interface FontPropertyCardProps { + token: string; + value: string | number; + px?: string; + notes?: string; +} + +const FontPropertyCard: React.FC = ({ + token, + value, + px, + notes, +}) => ( +
+
+
+ {token} +
+
+ Value: {value} +
+ {px && ( +
+ PX: {px} +
+ )} +
+ {notes && ( +
+ {notes} +
+ )} +
+); + +interface FontSizeExampleCardProps { + token: string; + value: string; + px: string; +} + +const FontSizeExampleCard: React.FC = ({ + token, + value, + px, +}) => { + const [copied, setCopied] = useState(""); + + return ( +
+
+ The quick brown fox jumps over the lazy dog. +
+
+ Token: + Copy(token, setCopied)} + > + {token} + {copied === token && ( + + )} + {copied !== token && } | Value: {value} ( + {px}) + +
+
+ Bootstrap class: .fs-{token.split("-")[1]} +
+
+ ); +}; + +interface LineHeightExampleCardProps { + token: string; + value: number; + description: string; + px?: string; + rem: string; +} + +const LineHeightExampleCard: React.FC = ({ + token, + value, + description, + px, + rem, +}) => { + const [copied, setCopied] = useState(""); + + return ( +
+
+ This is an example of text with line height {value}.
+ It demonstrates how spacing between lines changes.
+ Readability is key to a great user experience. +
+
+ Token: + Copy(token, setCopied)} + > + lh-{token} + {copied === token && ( + + )} + {copied !== token && } | Value: {value} ( + {px}) + {" "} + | PX: {px} | REM: {rem} +
+
+ {description} (Bootstrap class: .lh-{token}) +
+
+ ); +}; + +const SectionHeader: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( +

+ {children} +

+); + +const SectionDescription: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => ( +

+ {children} +

+); + +const meta: Meta = { + title: "Design Tokens/Fonts", + component: () =>
, + parameters: { + docs: { + description: { + component: + "This section defines our core **typographic system**, including font families, sizes, weights, and line heights. These tokens are seamlessly integrated with **Bootstrap's native font and line-height utilities** (e.g., `.fs-1` to `.fs-6`, and `.lh-1` to `.lh-lg`), ensuring a consistent and responsive typographic hierarchy across all applications. Designers and developers can leverage these tokens to maintain visual harmony and readability.", + }, + }, + layout: "centered", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const FontSystem: Story = { + render: () => ( +
+
+ 1. Core Typography +
+ {Object.entries(fontTokens.typography).map(([key, data]) => ( + + ))} +
+
+ +
+ 2. Font Sizes (Headings & Display) + + These tokens define our scalable font sizes, directly mapping to + Bootstrap's `.fs-` classes for easy application. + +
+ {Object.entries(fontTokens.fontSizes) + .sort(([, a], [, b]) => parseFloat(b.px) - parseFloat(a.px)) + .map(([key, data]) => ( + + ))} +
+
+ +
+ 3. Line Heights + + These tokens map directly to Bootstrap's `.lh-` classes. + +
+ {Object.entries(fontTokens.lineHeight).map(([key, data]) => ( + + ))} +
+
+
+ ), +}; diff --git a/client/src/storybook/bootstrap/DesignTokens/Opacity.stories.tsx b/client/src/storybook/bootstrap/DesignTokens/Opacity.stories.tsx new file mode 100644 index 0000000000..a78b571ede --- /dev/null +++ b/client/src/storybook/bootstrap/DesignTokens/Opacity.stories.tsx @@ -0,0 +1,229 @@ +import cx from "classnames"; +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Copy, CopyIcon } from "~/storybook/bootstrap/utils.tsx"; + +const opacityTokens = { + "bg-opacity-0": { + value: "0", + type: "opacity", + description: "Fully transparent (0% opacity)", + }, + "bg-opacity-25": { + value: "0.25", + type: "opacity", + description: "25% opaque", + }, + "bg-opacity-50": { + value: "0.5", + type: "opacity", + description: "50% opaque", + }, + "bg-opacity-75": { + value: "0.75", + type: "opacity", + description: "75% opaque", + }, + "bg-opacity-100": { + value: "1", + type: "opacity", + description: "Fully opaque (100% opacity)", + }, +}; + +const OpacityExampleCard: React.FC<{ + token: string; + value: string; + description: string; + backgroundColor: string; +}> = ({ token, value, backgroundColor }) => { + const [copied, setCopied] = useState(""); + return ( +
+
+ +
+ {`${parseFloat(value) * 100}%`} +
+ +
+
{token}
+
token && Copy(token, setCopied)} + > + Token: {token} + {copied === token && ( + + )} + {copied !== token && } +
+
Value: {value}
+
+
+ ); +}; + +const meta: Meta = { + title: "Design Tokens/Opacity", + component: () =>
, // Dummy component as we're rendering complex structure + parameters: { + docs: { + description: { + component: + "This section defines our **opacity scale** for controlling the transparency of elements. These tokens are designed to integrate seamlessly with **Bootstrap's `bg-opacity-*` utility classes**, providing a standardized approach to creating subtle visual effects, overlays, and layered designs. They ensure consistency in visual depth and interaction states across the user interface.", + }, + }, + layout: "centered", + }, + argTypes: { + backgroundColor: { + options: [ + "bg-primary", + "bg-secondary", + "bg-navy", + "bg-success", + "bg-info", + "bg-warning", + "bg-danger", + "bg-primary-subtle", + "bg-secondary-subtle", + "bg-success-subtle", + "bg-info-subtle", + "bg-warning-subtle", + "bg-danger-subtle", + "bg-light-subtle", + "bg-dark-subtle", + ], + control: { + type: "select", + }, + description: "Sets the base background color for the opaque layer.", + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const OpacityLevels: Story = { + render: (args) => ( +
+
+

+ Background Opacity (bg-opacity-*) +

+

+ Visualize the effect of different opacity levels on a solid + background, showcasing how elements can become more or less + transparent. +

+
+ {Object.entries(opacityTokens).map(([key, data]) => ( + + ))} +
+
+
+ ), +}; diff --git a/client/src/storybook/bootstrap/DesignTokens/Spacing.stories.tsx b/client/src/storybook/bootstrap/DesignTokens/Spacing.stories.tsx new file mode 100644 index 0000000000..770585e6f2 --- /dev/null +++ b/client/src/storybook/bootstrap/DesignTokens/Spacing.stories.tsx @@ -0,0 +1,258 @@ +import cx from "classnames"; +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Copy, CopyIcon } from "~/storybook/bootstrap/utils.tsx"; + +interface SpacingBoxProps { + token: string; + spacingClass: string; + type: "padding" | "margin"; + value: string; +} + +const SpacingBox: React.FC = ({ + token, + spacingClass, + type, + value, +}) => { + const [copied, setCopied] = useState(""); + + return ( +
+ {type === "margin" && ( +
+
+
+ {token.split("(")[0].trim()} +
+
+
+ )} + {type === "padding" && ( +
+
+ {token.split("(")[0].trim()} +
+
+ )} + +
+
token && Copy(token, setCopied)} + > + Token: {token} + {copied === token && ( + + )} + {copied !== token && } +
+
Value: {value}
+
+ Bootstrap class: {spacingClass} +
+
+
+ ); +}; + +interface SectionHeaderProps { + title: string; +} + +const SectionHeader: React.FC = ({ title }) => ( +

+ {title} +

+); + +interface SpacingGridProps { + children: React.ReactNode; +} + +const SpacingGrid: React.FC = ({ children }) => ( +
+ {children} +
+); + +const meta: Meta = { + title: "Design Tokens/Spacing", + component: SpacingBox, + parameters: { + docs: { + description: { + component: + "Defines a spacing system using design tokens, integrated with Bootstrap's utility classes. This system ensures consistent layouts and responsive designs across all components. Each example demonstrates **padding** (`p-*`) and **margin** (`m-*`) ", + }, + }, + layout: "centered", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const SpacingExamples: Story = { + render: () => ( +
+
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+
+ ), +}; diff --git a/client/src/storybook/bootstrap/Molecules/SectioncontainerCard.stories.tsx b/client/src/storybook/bootstrap/Molecules/SectioncontainerCard.stories.tsx new file mode 100644 index 0000000000..48429577f6 --- /dev/null +++ b/client/src/storybook/bootstrap/Molecules/SectioncontainerCard.stories.tsx @@ -0,0 +1,839 @@ +import { ReactNode } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Controls } from "@storybook/addon-docs"; +import { Canvas, Unstyled } from "@storybook/blocks"; +import { + Card, + CardHeader, + CardBody, + Badge, + Button, + ListGroupItem, + ListGroup, +} from "reactstrap"; +import cx from "classnames"; +import { + Folder, + PlayCircle, + People, + ThreeDotsVertical, + PlusLg, + CheckCircleFill, + XCircleFill, +} from "react-bootstrap-icons"; + +interface ContainerSectionCardProps { + title: string; + icon: "folder" | "play-circle" | "people"; + action?: "add" | "three-dots-vertical" | "none"; + badgeValue: number; + children: ReactNode | string; + description?: string; +} + +const contentOptions = { + Text: "This is an example of the content within the card body. It provides more detailed information about the section and what users can expect to find here.", + Lists: ( + + + Session 1 + + + Session 2 + + + Session 3 + + + ), +}; + +const SectionCardDocsPage = () => ( +
+

+ Container Section Card +

+ +
+

+ The Container Section Card is a reusable component used + throughout the Renku UI to group content visually. It is composed of a + header (with optional icons, buttons, or badges) and a body area for + structured content. This card brings visual consistency to dashboards, + summaries, and complex content blocks. +

+
+ +
+

+ When to use it +

+

+ Use the Section Container Card to visually group related content within + a page. It's ideal for structuring forms, settings, or dashboard + sections where a clear separation between different content blocks + enhances readability and organization. +

+
+ +
+

+ Anatomy +

+
    +
  • + + Container (<Card>) + +
  • +
  • + + Header (<CardHeader>) + +
      +
    • Title with an Icon
    • +
    • Optional badge
    • +
    • Optional button with only icon
    • +
    +
  • +
  • + + Body (<CardBody>) + +
      +
    • + Custom content (list as My projects Section, forms as General + Settings section, other cards as Sessions Section or any other + components or info as Documentation Section.) +
    • +
    +
  • +
+
+ +
+

+ Interactive Component +

+

+ Use the controls below to interact with the Container Section Card and + see how different props affect its appearance. +

+
+ + + + +
+
+ +
+

+ Usage Guidelines +

+ +

+ Do's +

+
    +
  • Use Bootstrap utility classes for spacing and sizing
  • +
  • Keep header structure consistent across all cards
  • +
  • + Use **only** the primary-outline button in the card + header +
  • +
+ +

+ + Don'ts{" "} +

+
    +
  • + Don’t place descriptions in the header — include them in the body + instead +
  • +
  • Don’t hardcode custom spacing unless absolutely necessary
  • +
  • Don’t mix multiple heading levels in the card title
  • +
  • + Don’t include other elements like images in the header — only + predefined variations are allowed +
  • +
  • Don’t place more than one primary button on a card header
  • +
  • + Don’t overload the card with content — if it requires scrolling, split + it into multiple cards or sections +
  • +
  • + Don’t apply custom styles for state changes like focus or selected +
  • +
+
+ +
+

+ Variants +

+
    +
  1. +

    1. Basic With Icon

    +
      +
    • Icon + title in header
    • +
    +
    + +
    +
  2. +
  3. +

    2. With Badged

    +
      +
    • gray badge for counter in the header
    • +
    +
    + +
    +
  4. +
  5. +

    3. With Actions

    +
      +
    • Right-aligned Icon button(s) in the header
    • +
    +
    + +
    +
  6. +
  7. +

    3. With Description

    +
      +
    • + When a section card includes a description, it is placed in the + card body with the following styles: +
    • +
    +
    + +
    +
  8. +
+
+ +
+

+ Layout & Spacing +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementBootstrap Class(es)Notes
Card Header Spacing + .px-3 .pt-3 .pb-0 + + Applies 1rem 16px padding on top, left + and right side and 0px on the bottom +
Body Spacing + .p-3 + + padding 16px all sides +
Header Icon title Spacing + .me-1 + + Margin between icon and title 4px +
Header Title Badged Spacing + .me-2 + + Margin between title and badge 8px +
Header Icon Title Badged - Action Button Spacing + .d-flex justify-content-between + + Main action must be aligned to the right without any additional + space +
Border Radius + .border-radius + + Use Token border-radius 6px +
Border + .border,.border-1 + + Light border style (stroke 1px) +
Shadows + .box-shadow + + 0 .5rem 1rem rgba(0, 0, 0, .15) +
+
+ +
+

+ Typography +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementBootstrap Class(es)Notes
Header Title + .h4, .fw-semibold, .mb-0 + + Section title, no margin font-weight 600,{" "} + 23.322px +
Icon + .bi + + height (1em, 14px) width ( + 1em, 14px) +
Icon button + .btn .btn-outline-primary{" "} + .btn-sm + + height (1em, 14px) width ( + 1em, 23.322px) +
Badge + .badge .badge-secondary + + font size: 12px +
Description + .text-body-secondary p .m-0 + + font size: 16px, margin 0px +
+
+ +
+

+ Colors +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ElementBootstrap ClassNotes
Background + .bg-white + Standard white card background
Text (primary) + .body-color-rgb + + Font color: rgb(22, 25, 44) +
Subtitle + .text-body-secondary + + Font color: rgba(22, 25, 44, .75) +
Border + .border + + Border color: #dee2e6 +
Icon Color + .body-color-rgb + + Font color: rgb(22, 25, 44) +
Badge + .badge-secondary + + background color: rgb(108, 117, 125), font color:{" "} + white +
Main action button + .btn-outline-primary + + icon color: #006e58; +
+ border color: #006e58; +
+ hover icon color: #fff; +
+ hover bg: #006e58; +
+ hover border color: #006e58; +
+ active icon color: #fff; +
+ active bg: #006e58; +
+ active border color: #006e58; +
+ disabled color: #006e58; +
+ disabled bg: transparent; +
+ disabled border color: #006e58; +
+
+
+); + +export default { + title: "Molecules/Card/Container Section Card", + args: { + title: "Example Section", + icon: "info-circle", + action: "none", + badgeValue: 0, + children: "This is an example of the content within the card body.", + }, + argTypes: { + title: { + control: "text", + description: "Header title text", + }, + icon: { + control: { type: "select" }, + options: ["folder", "play-circle", "people"], + description: + "SVG icon, predefined options for testing purposes: 'folder', 'play-circle', 'people'.", + }, + action: { + control: { type: "select" }, + options: ["add", "three-dots-vertical", "none"], + description: + "Predefined options for testing purposes. Use 'none' to remove the button.", + }, + badgeValue: { + control: "number", + description: "Value for the badge (if applicable). Set to -1 to hide.", + }, + description: { + control: "text", + description: "Description text for the card body. ", + }, + children: { + control: { type: "select" }, + options: Object.keys(contentOptions), + mapping: contentOptions, + description: "Card content (ReactNode) - Select from predefined options", + table: { + type: { summary: "ReactNode" }, + }, + }, + }, + parameters: { + layout: "centered", + docs: { + page: SectionCardDocsPage, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const BasicCard_: StoryObj = { + render: ({ title, icon, action, badgeValue, description, children }) => { + const IconToRender = { + folder: , + "play-circle": , + people: , + }; + const buttonIcon = + action === "none" ? null : action === "add" ? ( + + ) : ( + + ); + + return ( + + +
+
+

+ {IconToRender[icon]} + {title} +

+ {badgeValue >= 0 && {badgeValue}} +
+ {buttonIcon && ( +
+ +
+ )} +
+
+ +

{description}

+ {children} +
+
+ ); + }, + args: { + title: "My Projects", + icon: "folder", + action: "add", + description: "List of your recent projects will appear here.", + children: contentOptions["Text"], + }, + parameters: { + docs: { + source: { + type: "auto", + language: "tsx", + }, + }, + }, +}; + +export const Basic: Story = { + render: BasicCard_.render, + args: { + title: "Groups", + icon: "people", + badgeValue: -1, + }, +}; +export const WithBadge: Story = { + render: BasicCard_.render, + args: { + title: "Sessions", + icon: "play-circle", + badgeValue: 5, + children: "You have 5 unread notifications.", + }, +}; + +export const WithAction: Story = { + render: BasicCard_.render, + args: { + title: "Groups", + icon: "people", + action: "three-dots-vertical", + children: "You don't have groups yet", + badgeValue: -1, + }, +}; + +export const WithDescription: Story = { + render: BasicCard_.render, + args: { + title: "Groups", + icon: "people", + action: "three-dots-vertical", + description: "Manage your groups from this section.", + children: "", + }, +}; diff --git a/client/src/storybook/bootstrap/utils.tsx b/client/src/storybook/bootstrap/utils.tsx new file mode 100644 index 0000000000..54b658606d --- /dev/null +++ b/client/src/storybook/bootstrap/utils.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +export async function Copy( + text: string, + set: React.Dispatch> +) { + await navigator.clipboard.writeText(text); + set(text); + setTimeout(() => set(""), 1000); +} + +export function CopyIcon() { + return ( + + + + + + ); +}