SVRNTY Design System

A sophisticated Flutter design system with 20+ reusable components and a comprehensive server-driven token system. Build beautiful, consistent iOS-style applications with runtime theme switching across multiple brand identities.

Flutter Dart License

Features

  • 20+ Production-Ready Widgets - Organized by atomic design principles (atoms, molecules, organisms, layouts)
  • 🎨 Server-Driven Design Tokens - Dynamic styling that fetches from .NET CQRS backend at runtime
  • 🌓 Automatic Dark Mode - All colors and shadows adapt to light/dark themes
  • 📱 Cupertino-Only - Pure iOS-style components, no Material widgets
  • 🎯 8 Token Categories - Colors, spacing, typography, radii, shadows, gradients, animations, custom
  • 🏢 Multi-Brand Support - Apple HIG, Svrnty, and PlanB brand identities
  • 📐 Responsive Design - Built-in breakpoints and adaptive utilities
  • 🔒 Type-Safe - Full null-safety with immutable data structures
  • 💾 Offline Fallback - Static tokens keep your app running when server is unavailable

Table of Contents


Installation

1. Add Dependency

Add this to your Flutter app's pubspec.yaml:

dependencies:
  svrnty_design_system:
    path: ../svrnty_design_system  # Adjust path as needed

2. Install Packages

flutter pub get

3. Import Library

import 'package:svrnty_design_system/svrnty_design_system.dart';

Quick Start

Basic Setup

import 'package:flutter/cupertino.dart';
import 'package:svrnty_design_system/svrnty_design_system.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Load style model from server
  final loader = StyleModelLoader(
    config: StyleModelApiConfig(baseUrl: 'http://localhost:6001'),
  );

  StyleModel styleModel;
  try {
    // Seed the model (idempotent)
    await loader.seedAppleModel();

    // Load the model
    styleModel = await loader.loadModel('apple');
  } catch (e) {
    // Fallback to static tokens if server unavailable
    styleModel = AppleTokens.appleModel;
  }

  runApp(MyApp(styleModel: styleModel));
}

class MyApp extends StatelessWidget {
  final StyleModel styleModel;

  const MyApp({required this.styleModel, super.key});

  @override
  Widget build(BuildContext context) {
    return StyleModelProvider(
      model: styleModel,
      child: const CupertinoApp(
        title: 'My App',
        home: HomePage(),
      ),
    );
  }
}

Using Components

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final model = StyleModelProvider.of(context);

    return SvScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Home'),
      ),
      child: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(model.getSpacing('lg')),
          child: Column(
            children: [
              // Use design system button
              SvButton(
                label: 'Continue',
                onPressed: () => print('Pressed!'),
                variant: SvButtonVariant.filled,
                size: SvButtonSize.large,
              ),

              SizedBox(height: model.getSpacing('md')),

              // Use design system card
              SvCard(
                title: 'Welcome',
                subtitle: 'Get started with SVRNTY Design System',
                onTap: () => print('Card tapped'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Architecture

Server-Driven Tokens

The design system uses a unique server-driven architecture:

┌─────────────────────────────────────────────────┐
│            Flutter App                           │
│  ┌───────────────────────────────────────────┐  │
│  │ StyleModelLoader                          │  │
│  │ ↓                                         │  │
│  │ POST /api/query/getStyleModel            │  │
│  └───────────────────────────────────────────┘  │
└──────────────────┬──────────────────────────────┘
                   │ HTTP/JSON
                   ↓
┌─────────────────────────────────────────────────┐
│          .NET CQRS Backend                      │
│  ┌───────────────────────────────────────────┐  │
│  │ PostgreSQL Database                       │  │
│  │ - StyleModel (id, name, version)          │  │
│  │ - StyleToken (category, key, valueJson)   │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
                   │
                   ↓
┌─────────────────────────────────────────────────┐
│          Flutter App (Runtime)                   │
│  ┌───────────────────────────────────────────┐  │
│  │ StyleModelProvider                        │  │
│  │ - Makes tokens available to widget tree   │  │
│  └───────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────┐  │
│  │ Widgets (SvButton, SvCard, etc.)          │  │
│  │ - Access tokens by KEY, not value         │  │
│  │ - model.getColor('systemBlue', context)   │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

Benefits:

  • 🔄 Update styling without recompiling apps
  • 🎨 Support multiple brands with same widgets
  • 💾 Automatic fallback to static tokens when offline
  • 🌓 Seamless light/dark mode switching

Components

Overview

20 production-ready components organized by atomic design:

Category Count Components
Atoms 3 Button, TextField, Chip
Molecules 8 Card, ListTile, Dropdown, ExpansionTile, FormRow, ColorPicker, WizardProgress, ReorderableList
Organisms 7 Dialog, EmptyState, GlassContainer, PageHeader, Pagination, DataTable, TagCard
Layouts 1 Scaffold
Utils 1 Responsive (Breakpoints, Builder, Layout)

Atoms

Simple, reusable building blocks:

SvButton

SvButton(
  label: 'Continue',
  onPressed: () {},
  variant: SvButtonVariant.filled,    // filled, outlined, plain, destructive
  size: SvButtonSize.large,            // small, medium, large
  isExpanded: true,                    // Full width
  isLoading: false,                    // Loading state
  icon: CupertinoIcons.add,            // Optional icon
)

SvTextField

SvTextField(
  label: 'Email',
  placeholder: 'Enter your email',
  helperText: 'We\'ll never share your email',
  errorText: isError ? 'Invalid email' : null,
  leadingIcon: CupertinoIcons.mail,
  onChanged: (value) => print(value),
)

SvChip

SvChip(
  label: 'Active',
  isSelected: true,
  variant: SvChipVariant.filter,      // filter, choice
  onSelected: (selected) => print(selected),
)

Molecules

Composite components:

SvCard

SvCard(
  title: 'Card Title',
  subtitle: 'Card subtitle text',
  trailing: Icon(CupertinoIcons.chevron_right),
  onTap: () => print('Tapped'),
)

SvListTile

SvListTile(
  title: 'Settings',
  subtitle: 'Configure your preferences',
  leading: Icon(CupertinoIcons.settings),
  trailing: Icon(CupertinoIcons.chevron_right),
  showDisclosure: true,
  onTap: () {},
)

SvDropdown

SvDropdown<String>(
  value: selectedValue,
  items: ['Option 1', 'Option 2', 'Option 3'],
  onChanged: (value) => setState(() => selectedValue = value),
  itemBuilder: (item) => Text(item),
)

Organisms

Complex, feature-rich components:

SvConfirmDialog

showCupertinoDialog(
  context: context,
  builder: (context) => SvConfirmDialog(
    title: 'Delete Account',
    message: 'This action cannot be undone.',
    confirmText: 'Delete',
    cancelText: 'Cancel',
    isDestructive: true,
    onConfirm: () => deleteAccount(),
  ),
)

SvEmptyState

SvEmptyState(
  icon: CupertinoIcons.folder,
  title: 'No Items',
  message: 'You haven\'t added any items yet.',
  actionLabel: 'Add Item',
  onAction: () => addItem(),
)

SvDataTable

SvDataTable(
  columns: ['Name', 'Email', 'Role'],
  rows: [
    ['John Doe', 'john@example.com', 'Admin'],
    ['Jane Smith', 'jane@example.com', 'User'],
  ],
  sortColumnIndex: 0,
  sortAscending: true,
  onSort: (columnIndex, ascending) {},
)

Layouts

SvScaffold

SvScaffold(
  navigationBar: CupertinoNavigationBar(
    middle: Text('Page Title'),
  ),
  useSliverNavigationBar: false,      // Use sliver for large titles
  bottomBar: Container(/* ... */),    // Optional bottom bar
  child: /* Your content */,
)

Full Component List

See TOKENS.md for complete component documentation.


Design Tokens

Token Categories

The design system provides 8 token categories:

  1. Colors - 100+ adaptive color tokens (light/dark variants)
  2. Spacing - 8 spacing values (2pt → 48pt)
  3. Typography - 27 text styles (Apple HIG + Material Design 3)
  4. Radii - 9 border radius values (2pt → 9999pt)
  5. Shadows - 4 elevation levels (sm → xl)
  6. Gradients - Linear gradients for backgrounds
  7. Animations - 4 duration presets (fast → verySlow)
  8. Custom - Extensible custom token categories

Accessing Tokens

// Get StyleModel
final model = StyleModelProvider.of(context);

// Colors (adaptive to light/dark mode)
final color = model.getColor('systemBlue', context);

// Spacing
final spacing = model.getSpacing('md');              // 12.0 or 16.0
final padding = model.getPaddingAll('lg');           // EdgeInsets.all(16 or 20)

// Typography
final textStyle = model.getTextStyle('body');        // TextStyle(...)

// Radii
final radius = model.getRadius('md');                // 10.0 or 12.0
final borderRadius = model.getBorderRadius('lg');    // BorderRadius.circular(...)

// Shadows (adaptive)
final shadow = model.getShadow('md', context);       // List<BoxShadow>

// Gradients
final gradient = model.getLinearGradient('brandPrimary');

// Animations
final duration = model.getAnimationDuration('normal'); // Duration(milliseconds: 250)

Context Extensions (Shorthand)

// Instead of StyleModelProvider.of(context)
final color = context.styleColor('systemBlue');
final spacing = context.styleSpacing('md');
final textStyle = context.styleTextStyle('body');
final padding = context.stylePaddingAll('lg');
final shadow = context.styleShadow('md');

// Responsive
final isMobile = context.isMobile;
final isTablet = context.isTablet;
final padding = context.adaptivePadding;  // 16/24/32 based on screen size

Complete Token Reference

See TOKENS.md for a comprehensive list of all available tokens with values and usage examples.


Usage Examples

Example 1: Custom Button with Tokens

class CustomButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;
  final bool isPrimary;

  const CustomButton({
    required this.label,
    this.onPressed,
    this.isPrimary = true,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final model = StyleModelProvider.of(context);

    return GestureDetector(
      onTap: onPressed,
      child: Container(
        padding: model.getPaddingSymmetric(
          horizontal: 'xl',
          vertical: 'md',
        ),
        decoration: BoxDecoration(
          color: isPrimary
              ? model.getColor('systemBlue', context)
              : model.getColor('systemGray5', context),
          borderRadius: model.getBorderRadius('md'),
          boxShadow: model.getShadow('sm', context),
        ),
        child: Text(
          label,
          style: model.getTextStyle('body').copyWith(
            color: isPrimary
                ? CupertinoColors.white
                : model.getColor('label', context),
          ),
        ),
      ),
    );
  }
}

Example 2: Form with Validation

class LoginForm extends StatefulWidget {
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  String? _emailError;
  String? _passwordError;

  void _submit() {
    setState(() {
      _emailError = _validateEmail(_emailController.text);
      _passwordError = _validatePassword(_passwordController.text);
    });

    if (_emailError == null && _passwordError == null) {
      // Proceed with login
    }
  }

  String? _validateEmail(String value) {
    if (value.isEmpty) return 'Email is required';
    if (!value.contains('@')) return 'Invalid email';
    return null;
  }

  String? _validatePassword(String value) {
    if (value.isEmpty) return 'Password is required';
    if (value.length < 6) return 'Password must be at least 6 characters';
    return null;
  }

  @override
  Widget build(BuildContext context) {
    final model = StyleModelProvider.of(context);

    return SvScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Login'),
      ),
      child: SafeArea(
        child: Padding(
          padding: EdgeInsets.all(model.getSpacing('lg')),
          child: Column(
            children: [
              SvTextField(
                label: 'Email',
                placeholder: 'Enter your email',
                controller: _emailController,
                errorText: _emailError,
                leadingIcon: CupertinoIcons.mail,
              ),
              SizedBox(height: model.getSpacing('md')),
              SvTextField(
                label: 'Password',
                placeholder: 'Enter your password',
                controller: _passwordController,
                errorText: _passwordError,
                obscureText: true,
                leadingIcon: CupertinoIcons.lock,
              ),
              SizedBox(height: model.getSpacing('xl')),
              SvButton(
                label: 'Login',
                onPressed: _submit,
                variant: SvButtonVariant.filled,
                size: SvButtonSize.large,
                isExpanded: true,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Example 3: Responsive Layout

class ResponsivePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SvResponsiveLayout(
      mobile: MobileLayout(),
      tablet: TabletLayout(),
      desktop: DesktopLayout(),
    );
  }
}

class MobileLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SvScaffold(
      navigationBar: const CupertinoNavigationBar(
        middle: Text('Mobile'),
      ),
      child: SafeArea(
        child: Padding(
          padding: context.adaptivePadding,  // 16pt
          child: Column(
            spacing: context.styleSpacing('md'),
            children: [
              Text('Mobile View', style: context.styleTextStyle('largeTitle')),
              SvCard(title: 'Card 1'),
              SvCard(title: 'Card 2'),
            ],
          ),
        ),
      ),
    );
  }
}

Example 4: Animated Transitions

class AnimatedCard extends StatefulWidget {
  @override
  State<AnimatedCard> createState() => _AnimatedCardState();
}

class _AnimatedCardState extends State<AnimatedCard> {
  bool isExpanded = false;

  @override
  Widget build(BuildContext context) {
    final model = StyleModelProvider.of(context);

    return GestureDetector(
      onTap: () => setState(() => isExpanded = !isExpanded),
      child: AnimatedContainer(
        duration: model.getAnimationDuration('normal'),
        curve: Curves.easeInOut,
        height: isExpanded ? 200 : 100,
        padding: model.getPaddingAll('lg'),
        decoration: BoxDecoration(
          color: model.getColor('secondarySystemBackground', context),
          borderRadius: model.getBorderRadius('lg'),
          boxShadow: model.getShadow(isExpanded ? 'lg' : 'sm', context),
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Tap to expand', style: model.getTextStyle('headline')),
            if (isExpanded) ...[
              SizedBox(height: model.getSpacing('md')),
              Text(
                'Additional content appears here',
                style: model.getTextStyle('body'),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

Responsive Design

Breakpoints

Built-in responsive breakpoints:

Breakpoint Width Usage
mobile < 600px Phones
mobileSmall < 360px Small phones
mobileLarge 480px+ Large phones
tablet 600px - 1024px Tablets
tabletLarge 840px+ Large tablets
desktop 1024px+ Desktops
desktopLarge 1920px+ Large monitors
desktopUltra 2560px+ Ultra-wide monitors

Responsive Utilities

// Check device type
if (context.isMobile) {
  // Mobile-specific layout
}

// Get screen dimensions
final width = context.screenWidth;
final height = context.screenHeight;

// Adaptive values
final columns = context.gridColumns;  // 1-12 columns based on width
final padding = context.adaptivePadding;  // 16/24/32

// Adaptive value selection
final fontSize = context.adaptive(
  mobile: 14.0,
  tablet: 16.0,
  desktop: 18.0,
);

// Responsive builder
SvResponsiveBuilder(
  builder: (context, deviceType) {
    if (deviceType == DeviceType.mobile) {
      return MobileWidget();
    } else if (deviceType == DeviceType.tablet) {
      return TabletWidget();
    } else {
      return DesktopWidget();
    }
  },
);

// Responsive layout switcher
SvResponsiveLayout(
  mobile: MobileWidget(),
  tablet: TabletWidget(),
  desktop: DesktopWidget(),
);

API Reference

StyleModel

Core token container with comprehensive token access methods. See TOKENS.md for complete API documentation.

StyleModelProvider

InheritedWidget for providing tokens:

class StyleModelProvider extends InheritedWidget {
  final StyleModel model;

  static StyleModel of(BuildContext context);
  static StyleModel? maybeOf(BuildContext context);
}

// Context extensions
extension StyleModelContext on BuildContext {
  StyleModel get styleModel;
  StyleModel? get maybeStyleModel;
  Color styleColor(String key);
  double styleSpacing(String key);
  TextStyle styleTextStyle(String key);
  // ... and more
}

StyleModelLoader

Server token loader:

class StyleModelLoader {
  StyleModelLoader({required StyleModelApiConfig config});

  Future<StyleModel> loadModel(String name);
  Future<List<StyleModelSummary>> listModels();

  // Seed commands (for development)
  Future<void> seedAppleModel();
  Future<void> seedSvrntyModel();
  Future<void> seedPlanBModel();
}

class StyleModelApiConfig {
  final String baseUrl;
  final Map<String, String>? headers;
}

Best Practices

✅ DO

  1. Use tokens instead of hardcoded values

    // Good
    color: model.getColor('systemBlue', context)
    padding: model.getPaddingAll('md')
    
    // Bad
    color: Color(0xFF007AFF)
    padding: EdgeInsets.all(12)
    
  2. Pass context for adaptive tokens

    // Good - adapts to light/dark mode
    color: model.getColor('label', context)
    
    // Bad - doesn't adapt
    color: model.getColorLight('label')
    
  3. Use semantic colors for states

    // Good
    color: model.getColor('error', context)
    
    // Avoid (less semantic)
    color: model.getColor('systemRed', context)
    
  4. Wrap app with StyleModelProvider

    CupertinoApp(
      builder: (context, child) {
        return StyleModelProvider(
          model: styleModel,
          child: child!,
        );
      },
    );
    
  5. Use context extensions for cleaner code

    // Cleaner
    Text('Hello', style: context.styleTextStyle('body'))
    
    // More verbose
    Text('Hello', style: StyleModelProvider.of(context).getTextStyle('body'))
    

❌ DON'T

  1. Don't hardcode values
  2. Don't skip context for adaptive tokens
  3. Don't mix brand token sets (use one consistently)
  4. Don't access tokens before wrapping with StyleModelProvider
  5. Don't import Material widgets (use Cupertino instead)

Contributing

Development Setup

  1. Clone the repository
  2. Install dependencies: flutter pub get
  3. Run theme browser: cd ../theme_browser_app && flutter run

Adding a New Widget

  1. Create widget in appropriate category:

    lib/src/atoms|molecules|organisms/{widget_name}/
    ├── sv_{widget_name}.dart
    └── README.md (optional)
    
  2. Export from main library file:

    // lib/svrnty_design_system.dart
    export 'src/atoms/{widget_name}/sv_{widget_name}.dart';
    
  3. Use tokens, never hardcode:

    final model = StyleModelProvider.of(context);
    color: model.getColor('systemBlue', context)
    
  4. Test in theme browser app

  5. Update version in pubspec.yaml

  6. Document in README

Adding a New Token

Server approach (recommended):

  1. Add to database via backend API
  2. Apps fetch at startup

Fallback approach:

  1. Add to lib/src/tokens/apple_tokens.dart (or svrnty/planb)
  2. Update TOKENS.md

Project Structure

svrnty_design_system/
├── lib/
│   ├── svrnty_design_system.dart          # Main export
│   └── src/
│       ├── core/                           # StyleModel, Provider, Loader
│       ├── tokens/                         # Fallback token sets
│       ├── atoms/                          # Simple components (3)
│       ├── molecules/                      # Composite components (8)
│       ├── organisms/                      # Complex components (7)
│       ├── layouts/                        # Layout components (1)
│       └── utils/                          # Utilities
├── test/                                    # Unit tests
├── pubspec.yaml
├── README.md                                # This file
├── TOKENS.md                                # Token reference
└── CHANGELOG.md                             # Version history

Troubleshooting

"No StyleModel found in context"

Problem: Widget trying to access tokens before StyleModelProvider is initialized.

Solution: Ensure your app is wrapped with StyleModelProvider:

StyleModelProvider(
  model: styleModel,
  child: CupertinoApp(...),
)

"Failed to load style model from server"

Problem: Backend server is unavailable or not running.

Solutions:

  1. Start the backend: cd backend-server/dotnet-cqrs && dotnet run --project Svrnty.Sample
  2. Use fallback tokens: styleModel = AppleTokens.appleModel
  3. Check baseUrl in StyleModelApiConfig

"Token key not found"

Problem: Accessing a token that doesn't exist.

Solution: Check available tokens:

print(model.colorKeys);      // List all color tokens
print(model.spacingKeys);    // List all spacing tokens
print(model.categories);     // List all categories

See TOKENS.md for complete token reference.

Colors not adapting to dark mode

Problem: Not passing context to getColor().

Solution:

// Wrong
model.getColor('systemBlue')

// Correct
model.getColor('systemBlue', context)

Versioning

This package follows Semantic Versioning:

  • MAJOR version for breaking changes
  • MINOR version for new features (backward compatible)
  • PATCH version for bug fixes

Current version: 1.0.0

See CHANGELOG.md for version history.


License

Proprietary - SVRNTY/PlanB Ecosystem

Copyright © 2025 Svrnty. All rights reserved.


Support

For issues, questions, or contributions:

  • Issues: Create an issue in the repository
  • Documentation: See TOKENS.md for token reference
  • Example: Check theme_browser_app directory
  • Architecture: See main repository CLAUDE.md

Acknowledgments


Last Updated: 2025-12-29 Design System Version: 1.0.0

Libraries

svrnty_design_system
SVRNTY Design System