497 lines
12 KiB
Dart
497 lines
12 KiB
Dart
/// inou Text Styles & Typography Widgets
|
|
///
|
|
/// RULES:
|
|
/// - Pages MUST use InouText.* styles or widgets
|
|
/// - NO raw TextStyle() in page code
|
|
/// - NO fontSize: or fontWeight: in page code
|
|
///
|
|
/// Usage:
|
|
/// Text('Hello', style: InouText.pageTitle)
|
|
/// InouText.pageTitle('Hello')
|
|
/// InouText.body('Paragraph', color: InouTheme.textMuted)
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'inou_theme.dart';
|
|
|
|
/// Typography system for inou
|
|
///
|
|
/// Base: 16px (1rem), Sora font, line-height 1.5
|
|
class InouText {
|
|
InouText._();
|
|
|
|
// ===========================================
|
|
// FONT FAMILY
|
|
// ===========================================
|
|
static const String fontFamily = 'Sora';
|
|
|
|
// ===========================================
|
|
// TEXT STYLES
|
|
// ===========================================
|
|
|
|
/// Page title: 2.5rem (40px), weight 800 (ExtraBold)
|
|
/// Use for: Main page headings like "Style Guide", "Privacy Policy"
|
|
static const TextStyle pageTitle = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 40.0, // 2.5 * 16
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: -0.5,
|
|
height: 1.2,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Hero title: 2.25rem (36px), weight 300 (Light)
|
|
/// Use for: Large hero text like "Your data. Your rules."
|
|
static const TextStyle heroTitle = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 36.0, // 2.25 * 16
|
|
fontWeight: FontWeight.w300,
|
|
height: 1.2,
|
|
letterSpacing: -1.08, // -0.03em
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Section title: 1.4rem (22.4px), weight 600 (SemiBold)
|
|
/// Use for: Section headings like "What we collect"
|
|
static const TextStyle sectionTitle = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 22.4, // 1.4 * 16
|
|
fontWeight: FontWeight.w600,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Subsection title: 1.1rem (17.6px), weight 600 (SemiBold)
|
|
/// Use for: Subsection headings like "Account information"
|
|
static const TextStyle subsectionTitle = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 17.6, // 1.1 * 16
|
|
fontWeight: FontWeight.w600,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// H3: 1.125rem (18px), weight 500 (Medium)
|
|
/// Use for: Tertiary headings
|
|
static const TextStyle h3 = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 18.0, // 1.125 * 16
|
|
fontWeight: FontWeight.w500,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Intro text: 1.15rem (18.4px), weight 300 (Light)
|
|
/// Use for: Introduction paragraphs, larger body text
|
|
static const TextStyle intro = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 18.4,
|
|
fontWeight: FontWeight.w300,
|
|
height: 1.8,
|
|
color: InouTheme.textMuted,
|
|
);
|
|
|
|
/// Body light: 1rem (16px), weight 300 (Light)
|
|
/// Use for: Long-form content, articles, descriptions
|
|
static const TextStyle bodyLight = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w300,
|
|
height: 1.5,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Body regular: 1rem (16px), weight 400 (Regular)
|
|
/// Use for: UI labels, default text, buttons
|
|
static const TextStyle body = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w400,
|
|
height: 1.5,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Body small: 0.85rem (13.6px), weight 400 (Regular)
|
|
/// Use for: Secondary text, captions, helper text
|
|
static const TextStyle bodySmall = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 13.6, // 0.85 * 16
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Label: 1rem (16px), weight 500 (Medium)
|
|
/// Use for: Form labels, button text
|
|
static const TextStyle label = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w500,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Label caps: 0.75rem (12px), weight 600, uppercase
|
|
/// Use for: Category labels like "TEXT BLOCKS", "TYPOGRAPHY SCALE"
|
|
static const TextStyle labelCaps = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 12.0, // 0.75 * 16
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 1.2, // 0.1em
|
|
color: InouTheme.textSubtle,
|
|
);
|
|
|
|
/// Logo: 1.75rem (28px), weight 700 (Bold)
|
|
/// Use for: "inou" in header
|
|
static const TextStyle logo = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 28.0, // 1.75 * 16
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.56, // -0.02em
|
|
);
|
|
|
|
/// Logo light: 1.75rem (28px), weight 300 (Light)
|
|
/// Use for: "health" in header
|
|
static const TextStyle logoLight = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 28.0, // 1.75 * 16
|
|
fontWeight: FontWeight.w300,
|
|
letterSpacing: -0.56, // -0.02em
|
|
);
|
|
|
|
/// Logo tagline: 0.95rem (15.2px), weight 300 (Light)
|
|
/// Use for: "ai answers for you" tagline
|
|
static const TextStyle logoTagline = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 15.2, // 0.95 * 16
|
|
fontWeight: FontWeight.w300,
|
|
letterSpacing: 0.608, // 0.04em
|
|
color: InouTheme.textMuted,
|
|
);
|
|
|
|
/// Nav item: 1rem (16px), weight 400 (Regular)
|
|
/// Use for: Navigation menu items
|
|
static const TextStyle nav = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Nav item active: 1rem (16px), weight 600 (SemiBold)
|
|
/// Use for: Active navigation menu items
|
|
static const TextStyle navActive = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w600,
|
|
color: InouTheme.accent,
|
|
);
|
|
|
|
/// Mono: SF Mono, 0.85rem (13.6px)
|
|
/// Use for: Code, technical data, IDs
|
|
static const TextStyle mono = TextStyle(
|
|
fontFamily: 'SF Mono',
|
|
fontFamilyFallback: ['Monaco', 'Consolas', 'monospace'],
|
|
fontSize: 13.6, // 0.85 * 16
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Profile name: 1.25rem (20px), weight 600 (SemiBold)
|
|
/// Use for: User names in profile cards
|
|
static const TextStyle profileName = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 20.0, // 1.25 * 16
|
|
fontWeight: FontWeight.w600,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Badge: 1rem (16px), weight 500 (Medium)
|
|
/// Use for: Badge/pill text
|
|
static const TextStyle badge = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w500,
|
|
);
|
|
|
|
/// Button: 1rem (16px), weight 500 (Medium)
|
|
/// Use for: Button text
|
|
static const TextStyle button = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w500,
|
|
);
|
|
|
|
/// Input: 1rem (16px), weight 400 (Regular)
|
|
/// Use for: Text input fields
|
|
static const TextStyle input = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.text,
|
|
);
|
|
|
|
/// Input placeholder: 1rem (16px), weight 400 (Regular)
|
|
/// Use for: Placeholder text in inputs
|
|
static const TextStyle inputPlaceholder = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 16.0,
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.textSubtle,
|
|
);
|
|
|
|
/// Error text: 0.85rem (13.6px), weight 400 (Regular)
|
|
/// Use for: Form validation errors
|
|
static const TextStyle error = TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: 13.6,
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.danger,
|
|
);
|
|
|
|
/// Link: inherits size, weight 400, accent color
|
|
/// Use for: Inline links
|
|
static TextStyle link({double? fontSize}) => TextStyle(
|
|
fontFamily: fontFamily,
|
|
fontSize: fontSize ?? 16.0,
|
|
fontWeight: FontWeight.w400,
|
|
color: InouTheme.accent,
|
|
decoration: TextDecoration.underline,
|
|
decorationColor: InouTheme.accent,
|
|
);
|
|
|
|
// ===========================================
|
|
// CONVENIENCE WIDGETS
|
|
// ===========================================
|
|
|
|
/// Page title widget
|
|
static Widget pageTitleText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: pageTitle.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Hero title widget
|
|
static Widget heroTitleText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: heroTitle.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Section title widget
|
|
static Widget sectionTitleText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: sectionTitle.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Subsection title widget
|
|
static Widget subsectionTitleText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: subsectionTitle.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Body text widget
|
|
static Widget bodyText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
int? maxLines,
|
|
TextOverflow? overflow,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: body.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
maxLines: maxLines,
|
|
overflow: overflow,
|
|
);
|
|
}
|
|
|
|
/// Body light text widget (for long-form)
|
|
static Widget bodyLightText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: bodyLight.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Intro text widget
|
|
static Widget introText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: intro.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Label caps widget (auto-uppercases)
|
|
static Widget labelCapsText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text.toUpperCase(),
|
|
style: labelCaps.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Mono text widget
|
|
static Widget monoText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: mono.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Small text widget
|
|
static Widget smallText(
|
|
String text, {
|
|
Color? color,
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: bodySmall.copyWith(color: color),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
/// Error text widget
|
|
static Widget errorText(
|
|
String text, {
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text(
|
|
text,
|
|
style: error,
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
|
|
// ===========================================
|
|
// RICH TEXT HELPERS
|
|
// ===========================================
|
|
|
|
/// Build rich text with multiple styled spans
|
|
static Widget rich(
|
|
List<InlineSpan> children, {
|
|
TextAlign? textAlign,
|
|
}) {
|
|
return Text.rich(
|
|
TextSpan(children: children),
|
|
textAlign: textAlign,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Styled text spans for use with InouText.rich()
|
|
class InouSpan {
|
|
InouSpan._();
|
|
|
|
/// Accent colored text
|
|
static TextSpan accent(String text, {TextStyle? baseStyle}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.accent),
|
|
);
|
|
}
|
|
|
|
/// Muted colored text
|
|
static TextSpan muted(String text, {TextStyle? baseStyle}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.textMuted),
|
|
);
|
|
}
|
|
|
|
/// Subtle colored text
|
|
static TextSpan subtle(String text, {TextStyle? baseStyle}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.textSubtle),
|
|
);
|
|
}
|
|
|
|
/// Bold text
|
|
static TextSpan bold(String text, {TextStyle? baseStyle}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w700),
|
|
);
|
|
}
|
|
|
|
/// SemiBold text
|
|
static TextSpan semiBold(String text, {TextStyle? baseStyle}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w600),
|
|
);
|
|
}
|
|
|
|
/// Light text
|
|
static TextSpan light(String text, {TextStyle? baseStyle}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w300),
|
|
);
|
|
}
|
|
|
|
/// Plain text (default style)
|
|
static TextSpan plain(String text, {TextStyle? style}) {
|
|
return TextSpan(text: text, style: style);
|
|
}
|
|
|
|
/// Link text
|
|
static TextSpan link(
|
|
String text, {
|
|
VoidCallback? onTap,
|
|
TextStyle? baseStyle,
|
|
}) {
|
|
return TextSpan(
|
|
text: text,
|
|
style: (baseStyle ?? InouText.body).copyWith(
|
|
color: InouTheme.accent,
|
|
decoration: TextDecoration.underline,
|
|
decorationColor: InouTheme.accent,
|
|
),
|
|
// Note: For tap handling, wrap in GestureDetector or use url_launcher
|
|
);
|
|
}
|
|
}
|