Skip to content

Instantly share code, notes, and snippets.

@theklr
Created July 28, 2025 19:21
Show Gist options
  • Select an option

  • Save theklr/19c04e107c97c259f51fab2004828838 to your computer and use it in GitHub Desktop.

Select an option

Save theklr/19c04e107c97c259f51fab2004828838 to your computer and use it in GitHub Desktop.
GS Unified Theme Standards Proposal

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Changed

  • PricingOptions: Removed unnecessary variant prop - component now only supports card layout as per Figma design
  • BREAKING: Removed ESP theme from design system
  • Consolidated roadside theme to use identical styling as default theme
  • Updated TypeScript types to remove "esp" from ThemeName union type
  • Updated Storybook configuration to remove ESP theme option
  • Simplified theme stories to reflect current theme options

Fixed

  • Icon handling: Fixed TypeScript errors with dynamic icon casting in composite components
  • Storybook: Corrected minimum column values from 1 to 2 across all story controls
  • Fixed roadside theme not applying proper CSS classes by defining .theme-roadside class
  • Resolved Storybook theme switching issues where roadside theme wasn't visually different from default

Removed

  • Test files: Removed unused *.test.tsx files in favor of Storybook-based testing

Documentation

  • LIBRARY_STANDARDS.md: Added comprehensive development guidelines for multi-team collaboration
  • Changelog standards: Established required changelog documentation practices
  • shadcn/ui guidelines: Documented standards for primitive component development
  • Modern React patterns: Updated standards to use automatic ref forwarding as default

Migration Guide

Applications using the ESP theme (theme-esp) should update to use either:

  • theme-default - Good Sam's primary brand theme
  • theme-roadside - Identical styling to default (maintained for compatibility)

Both themes now provide identical visual styling using GS Green primary colors and consistent semantic tokens.

Previous Releases

See the Git history for details on previous changes.

GS Unified Components - Library Standards & Development Guidelines

Overview

This document outlines the standards and guidelines for multi-team development of the GS Unified Components library, derived from patterns established during the VR-555 cleanup and enhancement work. The library is built on a foundation of shadcn/ui primitives with GS-specific theming and composite components.

πŸ—οΈ Architecture Standards

Component Hierarchy

src/
β”œβ”€β”€ primitives/           # shadcn/ui-based foundational components
β”œβ”€β”€ composites/          # Business-specific components built from primitives
β”œβ”€β”€ types/              # Shared TypeScript interfaces
β”œβ”€β”€ lib/                # Core utilities (format, styles)
β”œβ”€β”€ utils/              # Helper functions (cn, constants, shared data)
└── styles/             # CSS/theme architecture with GS branding

shadcn/ui Foundation

All primitive components MUST be derived from shadcn/ui patterns and maintain compatibility with the shadcn/ui ecosystem while incorporating GS brand theming.

🎨 Primitive Component Standards

Required shadcn/ui Patterns

1. Class Variance Authority (CVA)

For components with variants, use CVA for type-safe styling:

import { cva, type VariantProps } from "class-variance-authority";

const buttonVariants = cva(
  // Base classes
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

2. Component Pattern (Automatic Ref Forwarding)

Modern components use automatic ref forwarding:

import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/utils/cn";

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean;
}

function Button({ 
  className, 
  variant, 
  size, 
  asChild = false, 
  children, 
  ...props 
}: ButtonProps) {
  const Comp = asChild ? Slot : "button";
  
  // Accessibility check for icon buttons without aria-label
  if (process.env.NODE_ENV !== "production") {
    if (size === "icon") {
      // ... accessibility validation logic
    }
  }
  
  return (
    <Comp
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    >
      {children}
    </Comp>
  );
}

// Export with proper typing
export { Button, buttonVariants, type ButtonProps };

3. When to Use forwardRef

Only use explicit forwardRef when component logic needs direct ref access:

import * as React from "react";

const CustomInput = React.forwardRef<HTMLInputElement, InputProps>(
  ({ onFocus, ...props }, ref) => {
    // Only when you need to use the ref in component logic
    const handleFocus = () => {
      if (ref && 'current' in ref && ref.current) {
        ref.current.select(); // Direct ref manipulation
      }
      onFocus?.();
    };
    
    return <input ref={ref} onFocus={handleFocus} {...props} />;
  }
);

4. Radix UI Integration

When applicable, use Radix UI primitives as the foundation:

import { Slot } from "@radix-ui/react-slot";
import * as React from "react";

5. CN Utility Usage

Always use the cn utility for class merging:

import { cn } from "@/utils/cn";

// Usage
className={cn(baseClasses, variantClasses, className)}

Required Interface Patterns

Component Props Interface

interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean; // For Slot compatibility
}

// Export the interface for external use
export { Button, buttonVariants, type ButtonProps };

Compound Component Pattern

For multi-part components like Card:

function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn(
        "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
        className
      )}
      {...props}
    />
  );
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card-header"
      className={cn(
        "@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
        className
      )}
      {...props}
    />
  );
}

// Export all parts
export {
  Card,
  CardHeader,
  CardFooter,
  CardTitle,
  CardAction,
  CardDescription,
  CardContent,
};

Accessibility Standards

All primitives must include appropriate accessibility features:

// Development-time accessibility warnings
if (process.env.NODE_ENV !== "production") {
  if (size === "icon" && !hasVisibleText && !hasAriaLabel) {
    console.warn(
      "Accessibility issue: Icon button is missing an aria-label. " +
      "Please add an aria-label attribute to describe the button purpose for screen readers."
    );
  }
}

🧩 Composite Component Standards

Built on Primitives

Composites must be built using primitives from the library:

import { Button } from "@/primitives/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/primitives/card";

interface PricingCardProps {
  plan: PricingPlan;
  onSelect: (plan: PricingPlan) => void;
}

function PricingCard({ plan, onSelect }: PricingCardProps) {
  return (
    <Card className="relative">
      <CardHeader>
        <CardTitle>{plan.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <Button onClick={() => onSelect(plan)}>
          {plan.buttonText}
        </Button>
      </CardContent>
    </Card>
  );
}

export { PricingCard, type PricingCardProps };

TypeScript Standards

  • Strict typing: All components must use proper TypeScript interfaces
  • Export interfaces: All component props interfaces must be exported
  • No any types: Use proper type assertions with ESLint disable comments when necessary
// βœ… Good - Proper interface with export
export interface PricingOptionsProps {
  plans: PricingPlan[];
  columns?: 2 | 3 | 4;
}

// ❌ Bad - Using any
const IconComponent = (Icons as any)[iconName];

// βœ… Good - Proper type assertion with ESLint disable
const IconComponent = 
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  (Icons as unknown as Record<string, React.ComponentType<LucideProps>>)[iconName] || Icons.Check;

Icon Handling Standards

For dynamic icon usage in composites:

import * as Icons from "lucide-react";
import type { LucideProps } from "lucide-react";

function ComponentWithIcon({ icon }: { icon: string }) {
  const IconComponent =
    (Icons as unknown as Record<string, React.ComponentType<LucideProps>>)[
      icon
    ] || Icons.Check;
    
  return <IconComponent className="h-5 w-5" />;
}

🎨 Styling Standards

Theme Integration

All components must use semantic color tokens that work with GS themes:

// βœ… Use semantic tokens
"bg-primary text-primary-foreground"
"border-input bg-background"
"text-muted-foreground"

// ❌ Avoid hardcoded colors
"bg-green-500 text-white"
"border-gray-300"

CSS Architecture

src/styles/
β”œβ”€β”€ index.css         # Main entry - includes all themes
β”œβ”€β”€ base.css         # Base Tailwind + fonts, variables, utilities
β”œβ”€β”€ default.css      # Default theme + base
β”œβ”€β”€ roadside.css     # Roadside theme + base
└── themes/
    β”œβ”€β”€ colors.css   # GS brand colors
    β”œβ”€β”€ default.css  # Default theme variables
    └── roadside.css # Roadside theme variables

Import Standards

/* βœ… Recommended - All themes */
@import "@goodsamenterprises/gs-unified-components/styles";

/* βœ… Specific theme only */
@import "@goodsamenterprises/gs-unified-components/styles/roadside";

πŸ“– Storybook Standards

Story Structure for Primitives

import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { Button } from "./button";

const meta = {
  title: "Primitives/Button",
  component: Button,
  parameters: {
    layout: "centered",
  },
  tags: ["autodocs"],
  argTypes: {
    variant: {
      control: { type: "select" },
      options: ["default", "destructive", "outline", "secondary", "ghost", "link"],
    },
    size: {
      control: { type: "select" },
      options: ["default", "sm", "lg", "icon"],
    },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  args: {
    children: "Button",
  },
};

export const IconButton: Story = {
  args: {
    size: "icon",
    "aria-label": "Settings", // Required for accessibility
    children: "βš™οΈ",
  },
};

Story Structure for Composites

import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { PricingOptions } from "./pricing-options";

const meta = {
  title: "Composites/PricingOptions",
  component: PricingOptions,
  parameters: {
    layout: "fullscreen",
  },
  tags: ["autodocs"],
  argTypes: {
    columns: {
      control: { type: "select" },
      options: [2, 3, 4], // Minimum 2 columns
    },
  },
} satisfies Meta<typeof PricingOptions>;

export default meta;
type Story = StoryObj<typeof meta>;

export const RoadsideAssistance: Story = {
  args: {
    plans: rvPlans, // Use realistic, detailed data
    columns: 3,
  },
};

πŸ”§ Development Workflow

shadcn/ui Integration Guidelines

Adding New Primitives

  1. Source from shadcn/ui: Start with official shadcn/ui component code
  2. Remove unnecessary forwardRef: Use automatic ref forwarding when possible
  3. Adapt for GS: Integrate GS theme variables and brand requirements
  4. Maintain compatibility: Ensure props and API remain compatible
  5. Add accessibility: Include GS-specific accessibility enhancements

Migration Pattern from forwardRef

// OLD (explicit forwardRef)
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, ...props }, ref) => (
    <button className={cn("...", className)} ref={ref} {...props} />
  )
);
Button.displayName = "Button";

// NEW (automatic ref forwarding)
function Button({ className, ...props }: ButtonProps) {
  return <button className={cn("...", className)} {...props} />;
}

Quality Gates

Pre-commit hooks run:

pnpm lint-staged  # Formatting, linting, and related tests

Pre-push hooks run:

pnpm lint          # ESLint checks
pnpm typecheck     # TypeScript compilation
pnpm test          # Vitest test runner
pnpm build         # Library build
pnpm build:storybook # Storybook build

File Naming Conventions

  • Primitives: kebab-case.tsx (e.g., button.tsx, dropdown-menu.tsx)
  • Composites: kebab-case.tsx (e.g., pricing-options.tsx)
  • Stories: component-name.stories.tsx
  • Types: kebab-case.ts (e.g., navigation.ts)

πŸ“¦ Package Standards

Export Structure

{
  "exports": {
    ".": "./dist/index.js",
    "./primitives/*": "./dist/primitives/*.js",
    "./composites/*": "./dist/composites/*.js",
    "./styles": "./dist/index.css",
    "./styles/roadside": "./dist/roadside.css"
  }
}

Dependencies

Core Dependencies

{
  "dependencies": {
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "tailwind-merge": "^2.6.0"
  }
}

Peer Dependencies (shadcn/ui ecosystem)

{
  "peerDependencies": {
    "@radix-ui/react-accordion": "^1.2.8",
    "@radix-ui/react-alert-dialog": "^1.1.14",
    "@radix-ui/react-avatar": "^1.1.3",
    "@radix-ui/react-checkbox": "^1.1.4",
    "@radix-ui/react-dialog": "^1.1.11",
    "@radix-ui/react-dropdown-menu": "^2.1.2",
    "@radix-ui/react-slot": "^1.1.2",
    "lucide-react": "^0.479.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

πŸ§ͺ Testing Standards

Test Strategy

  • Storybook-based testing: Primary testing through Storybook addon
  • Browser testing: Playwright integration via Storybook
  • Accessibility testing: Built-in accessibility checks in development
  • Visual regression: Chromatic integration for visual testing

Testing Configuration

// vitest.config.ts
export default defineConfig({
  test: {
    projects: [
      {
        plugins: [
          storybookTest({
            configDir: path.join(dirname, ".storybook"),
            tags: {
              exclude: ["skip-test"],
            },
          }),
        ],
        test: {
          name: "storybook",
          browser: {
            enabled: true,
            headless: true,
            provider: "playwright",
            instances: [{ browser: "chromium" }],
          },
        },
      },
    ],
  },
});

🚨 Migration & Maintenance

shadcn/ui Update Process

  1. Monitor upstream: Track shadcn/ui component updates
  2. Apply modern patterns: Remove unnecessary forwardRef usage
  3. Test compatibility: Ensure GS themes still work
  4. Update dependencies: Sync Radix UI versions
  5. Regression test: Run full test suite
  6. Update documentation: Reflect any API changes

Adding New shadcn/ui Components

  1. Copy base component: Start with official shadcn/ui code
  2. Apply GS theming: Integrate brand colors and spacing
  3. Modernize patterns: Use automatic ref forwarding
  4. Add accessibility: Include GS-specific a11y requirements
  5. Create stories: Document all variants and states
  6. Export properly: Add to main index and package exports

Implementation Checklist

For New Primitives (shadcn/ui based)

  • Sourced from official shadcn/ui component
  • Uses modern function component pattern
  • Uses cn utility for class merging
  • Uses CVA for variants (if applicable)
  • Integrates with Radix UI (if applicable)
  • Uses semantic color tokens
  • Includes accessibility features
  • Has comprehensive Storybook stories
  • Exports TypeScript interfaces
  • Passes all quality gates
  • Ref forwarding works automatically

For New Composites

  • Built using library primitives
  • Follows naming conventions (kebab-case.tsx)
  • Props interface is exported and properly typed
  • Icons use string type with proper casting
  • Storybook story exists with realistic data
  • Story includes proper controls and args
  • Component passes all quality gates
  • Uses semantic design tokens
  • No TypeScript errors or ESLint warnings

Team Guidelines

Code Reviews

  • Modern patterns: Ensure components use current React patterns
  • Primitive changes: Must maintain shadcn/ui compatibility
  • forwardRef usage: Only when component logic requires ref access
  • Breaking changes: Require architectural discussion
  • Theme integration: Must work with all GS themes
  • Accessibility: Required for all interactive components
  • Changelog updates: All changes must be documented in CHANGELOG.md

Documentation Requirements

  • shadcn/ui attribution: Credit upstream components
  • GS customizations: Document brand-specific changes
  • API compatibility: Note any deviations from shadcn/ui
  • Theme usage: Document semantic token usage
  • Changelog maintenance: Keep CHANGELOG.md up to date with all changes

Changelog Standards

All changes must be documented in CHANGELOG.md following these guidelines:

Format

## [Version] - YYYY-MM-DD

### Added
- New features and components

### Changed  
- Changes to existing functionality
- API improvements and simplifications

### Fixed
- Bug fixes and corrections

### Removed
- Deprecated features and breaking changes

Change Categories

  • Added: New features, components, or capabilities
  • Changed: Modifications to existing functionality, API improvements
  • Fixed: Bug fixes, accessibility improvements, type corrections
  • Removed: Deprecated features, breaking changes, unused code removal

Change Description Guidelines

  • Be specific: Describe what changed and why
  • Include component names: Reference specific components affected
  • Note breaking changes: Clearly mark any breaking changes
  • Provide context: Explain the benefit or reason for the change

Examples

### Changed
- **PricingOptions**: Removed unnecessary `variant` prop - component now only supports card layout as per Figma design
- **Button**: Updated to use automatic ref forwarding instead of forwardRef for better React 19 compatibility

### Fixed
- **Icon handling**: Fixed TypeScript errors with dynamic icon casting in composite components
- **Storybook**: Corrected minimum column values from 1 to 2 across all story controls

### Removed
- **Test files**: Removed unused `*.test.tsx` files in favor of Storybook-based testing
- **ESP theme**: Consolidated ESP theme with default theme to reduce complexity

This standards document ensures consistency with modern React and the shadcn/ui ecosystem while maintaining GS brand requirements and multi-team development practices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment