1334 lines
39 KiB
Dart
1334 lines
39 KiB
Dart
// Flutter Styleguide — matches inou.com/styleguide exactly
|
||
import 'package:flutter/material.dart';
|
||
import '../inou_theme.dart';
|
||
import '../widgets/widgets.dart';
|
||
|
||
class StyleguideScreen extends StatefulWidget {
|
||
const StyleguideScreen({super.key});
|
||
|
||
@override
|
||
State<StyleguideScreen> createState() => _StyleguideScreenState();
|
||
}
|
||
|
||
class _StyleguideScreenState extends State<StyleguideScreen> {
|
||
String? _selectedOption = 'Option 1';
|
||
String _selectedSex = 'male';
|
||
bool _checkboxValue = true;
|
||
String _selectedLLM = 'claude';
|
||
String _selectedUnits = 'metric';
|
||
bool _showGeneDetails = true;
|
||
bool _showVitalHistory = false;
|
||
bool _showNoteDetails = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: InouTheme.bg,
|
||
body: SafeArea(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(24),
|
||
child: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Header
|
||
Text('Style Guide', style: InouTheme.pageTitle),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Design system components for inou',
|
||
style: InouTheme.intro,
|
||
),
|
||
const SizedBox(height: 32),
|
||
|
||
// Text Blocks
|
||
_buildTextBlocksSection(),
|
||
|
||
// Typography
|
||
_buildTypographySection(),
|
||
|
||
// Colors
|
||
_buildColorsSection(),
|
||
|
||
// Buttons
|
||
_buildButtonsSection(),
|
||
|
||
// Badges
|
||
_buildBadgesSection(),
|
||
|
||
// Messages
|
||
_buildMessagesSection(),
|
||
|
||
// Form Elements
|
||
_buildFormsSection(),
|
||
|
||
// Settings
|
||
_buildSettingsSection(),
|
||
|
||
// Profile Cards
|
||
_buildProfileCardsSection(),
|
||
|
||
// Data Cards (Imaging, Labs)
|
||
_buildDataCardsSection(),
|
||
|
||
// Genetics
|
||
_buildGeneticsSection(),
|
||
|
||
// Vitals
|
||
_buildVitalsSection(),
|
||
|
||
// Notes
|
||
_buildNotesSection(),
|
||
|
||
// Supplements
|
||
_buildSupplementsSection(),
|
||
|
||
// Peptides
|
||
_buildPeptidesSection(),
|
||
|
||
// Upload Area
|
||
_buildUploadSection(),
|
||
|
||
// Empty State
|
||
_buildEmptyStateSection(),
|
||
|
||
const SizedBox(height: 48),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTextBlocksSection() {
|
||
return InouCard(
|
||
title: 'Text Blocks',
|
||
indicatorColor: InouTheme.indicatorImaging,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(32),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Your data. Your rules.',
|
||
style: InouTheme.pageTitle,
|
||
),
|
||
const SizedBox(height: 16),
|
||
RichText(
|
||
text: TextSpan(
|
||
style: InouTheme.bodyLight,
|
||
children: [
|
||
const TextSpan(text: 'We built '),
|
||
TextSpan(
|
||
text: 'inou',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.w700,
|
||
color: InouTheme.accent,
|
||
),
|
||
),
|
||
const TextSpan(
|
||
text: ' because health data is personal. Not personal like "preferences" — personal like your body, your history, your family. So we made privacy the foundation, not an afterthought.',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 32),
|
||
Text(
|
||
'What we collect',
|
||
style: InouTheme.sectionTitle,
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'Account information.',
|
||
style: InouTheme.subsectionTitle,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Name, email address, date of birth, and sex. Date of birth and sex help provide accurate medical context — an MRI interpretation differs significantly between a 6-year-old and a 16-year-old.',
|
||
style: InouTheme.bodyLight,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTypographySection() {
|
||
return InouCard(
|
||
title: 'Typography Scale',
|
||
indicatorColor: InouTheme.indicatorLabs,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_TypographyRow('Page Title', InouTheme.pageTitle, '2.5rem / 700'),
|
||
_TypographyRow('Section Title', InouTheme.sectionTitle, '1.4rem / 600'),
|
||
_TypographyRow('Subsection Title', InouTheme.subsectionTitle, '1.1rem / 600'),
|
||
_TypographyRow(
|
||
'LABEL / CATEGORY',
|
||
InouTheme.label.copyWith(color: InouTheme.textSubtle),
|
||
'0.75rem / 600 / caps',
|
||
isUppercase: true,
|
||
),
|
||
_TypographyRow(
|
||
'Intro text — larger, lighter',
|
||
InouTheme.intro,
|
||
'1.15rem / 300',
|
||
),
|
||
_TypographyRow(
|
||
'Body light — long-form',
|
||
InouTheme.bodyLight,
|
||
'1rem / 300',
|
||
),
|
||
_TypographyRow(
|
||
'Body regular — UI labels',
|
||
InouTheme.body,
|
||
'1rem / 400',
|
||
),
|
||
_TypographyRow(
|
||
'Mono: 1,234,567.89',
|
||
InouTheme.mono,
|
||
'SF Mono',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildColorsSection() {
|
||
return InouCard(
|
||
title: 'Colors',
|
||
indicatorColor: InouTheme.indicatorUploads,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
children: [
|
||
_ColorRow('Accent', InouTheme.accent, '#B45309'),
|
||
_ColorRow('Text', InouTheme.text, '#1C1917'),
|
||
_ColorRow('Text Muted', InouTheme.textMuted, '#78716C'),
|
||
_ColorRow('Background', InouTheme.bg, '#F8F7F6', hasBorder: true),
|
||
_ColorRow('Success', InouTheme.success, '#059669'),
|
||
_ColorRow('Danger', InouTheme.danger, '#DC2626'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildButtonsSection() {
|
||
return InouCard(
|
||
title: 'Buttons',
|
||
indicatorColor: InouTheme.indicatorVitals,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: [
|
||
InouButton(text: 'Primary', onPressed: () {}),
|
||
InouButton(
|
||
text: 'Secondary',
|
||
variant: ButtonVariant.secondary,
|
||
onPressed: () {},
|
||
),
|
||
InouButton(
|
||
text: 'Danger',
|
||
variant: ButtonVariant.danger,
|
||
onPressed: () {},
|
||
),
|
||
InouButton(
|
||
text: 'Small',
|
||
size: ButtonSize.small,
|
||
onPressed: () {},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildBadgesSection() {
|
||
return InouCard(
|
||
title: 'Badges',
|
||
indicatorColor: InouTheme.indicatorMedications,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: const [
|
||
InouBadge(text: 'default'),
|
||
InouBadge(text: 'care', variant: BadgeVariant.care),
|
||
InouBadge(text: 'COMING SOON', variant: BadgeVariant.comingSoon),
|
||
InouBadge(text: 'processing', variant: BadgeVariant.processing),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildMessagesSection() {
|
||
return InouCard(
|
||
title: 'Messages',
|
||
indicatorColor: InouTheme.indicatorRecords,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Column(
|
||
children: const [
|
||
InouMessage(
|
||
message: 'Error message — something went wrong.',
|
||
type: MessageType.error,
|
||
),
|
||
SizedBox(height: 12),
|
||
InouMessage(
|
||
message: "Info message — here's some useful information.",
|
||
type: MessageType.info,
|
||
),
|
||
SizedBox(height: 12),
|
||
InouMessage(
|
||
message: 'Success message — operation completed.',
|
||
type: MessageType.success,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildFormsSection() {
|
||
return InouCard(
|
||
title: 'Form Elements',
|
||
indicatorColor: InouTheme.indicatorJournal,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const InouTextField(
|
||
label: 'Text Input',
|
||
placeholder: 'Enter text...',
|
||
),
|
||
const SizedBox(height: 16),
|
||
InouSelect<String>(
|
||
label: 'Select',
|
||
value: _selectedOption,
|
||
options: const [
|
||
InouSelectOption(value: 'Option 1', label: 'Option 1'),
|
||
InouSelectOption(value: 'Option 2', label: 'Option 2'),
|
||
InouSelectOption(value: 'Option 3', label: 'Option 3'),
|
||
],
|
||
onChanged: (v) => setState(() => _selectedOption = v),
|
||
),
|
||
const SizedBox(height: 16),
|
||
const InouTextField(
|
||
label: 'Code Input',
|
||
placeholder: '123456',
|
||
isCode: true,
|
||
maxLength: 6,
|
||
keyboardType: TextInputType.number,
|
||
),
|
||
const SizedBox(height: 16),
|
||
InouRadioGroup<String>(
|
||
value: _selectedSex,
|
||
options: const [
|
||
InouRadioOption(value: 'male', label: 'Male'),
|
||
InouRadioOption(value: 'female', label: 'Female'),
|
||
],
|
||
onChanged: (v) => setState(() => _selectedSex = v ?? 'male'),
|
||
),
|
||
const SizedBox(height: 16),
|
||
InouCheckbox(
|
||
value: _checkboxValue,
|
||
label: 'Can add data (supplements, notes, etc.)',
|
||
onChanged: (v) => setState(() => _checkboxValue = v ?? false),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSettingsSection() {
|
||
return InouCard(
|
||
title: 'Settings',
|
||
indicatorColor: InouTheme.indicatorPrivacy,
|
||
child: Column(
|
||
children: [
|
||
// LLM Selector
|
||
_SettingsRow(
|
||
label: 'Primary AI Assistant',
|
||
description: 'Used for "Ask AI" prompts and analysis',
|
||
child: Column(
|
||
children: [
|
||
_LLMOption(
|
||
icon: '🤖',
|
||
name: 'Claude (Anthropic)',
|
||
value: 'claude',
|
||
selected: _selectedLLM == 'claude',
|
||
onTap: () => setState(() => _selectedLLM = 'claude'),
|
||
),
|
||
_LLMOption(
|
||
icon: '💬',
|
||
name: 'ChatGPT (OpenAI)',
|
||
value: 'chatgpt',
|
||
selected: _selectedLLM == 'chatgpt',
|
||
onTap: () => setState(() => _selectedLLM = 'chatgpt'),
|
||
),
|
||
_LLMOption(
|
||
icon: '✖',
|
||
name: 'Grok (xAI)',
|
||
value: 'grok',
|
||
selected: _selectedLLM == 'grok',
|
||
onTap: () => setState(() => _selectedLLM = 'grok'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 1, color: InouTheme.border),
|
||
// Units Selector
|
||
_SettingsRow(
|
||
label: 'Units',
|
||
description: 'Measurement system for vitals',
|
||
child: InouSelect<String>(
|
||
value: _selectedUnits,
|
||
options: const [
|
||
InouSelectOption(value: 'metric', label: 'Metric (kg, cm, °C)'),
|
||
InouSelectOption(value: 'imperial', label: 'Imperial (lb, in, °F)'),
|
||
],
|
||
onChanged: (v) => setState(() => _selectedUnits = v ?? 'metric'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildProfileCardsSection() {
|
||
return InouCard(
|
||
title: 'Profile Cards',
|
||
indicatorColor: InouTheme.indicatorImaging,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final cardWidth = constraints.maxWidth > 700
|
||
? (constraints.maxWidth - 24) / 3
|
||
: constraints.maxWidth;
|
||
return Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: [
|
||
SizedBox(
|
||
width: cardWidth,
|
||
height: 180,
|
||
child: InouProfileCard(
|
||
name: 'Johan Jongsma',
|
||
role: 'you',
|
||
dob: '1985-03-15',
|
||
sex: 'Male',
|
||
stats: const [
|
||
ProfileStat('📷', '3 studies'),
|
||
ProfileStat('🧪', '12 labs'),
|
||
ProfileStat('🧬', 'genome'),
|
||
],
|
||
onTap: () {},
|
||
),
|
||
),
|
||
SizedBox(
|
||
width: cardWidth,
|
||
height: 180,
|
||
child: InouProfileCard(
|
||
name: 'Sophia',
|
||
role: 'my role: Parent',
|
||
dob: '2017-01-01',
|
||
sex: 'Female',
|
||
isCare: true,
|
||
stats: const [
|
||
ProfileStat('📷', '16 studies'),
|
||
ProfileStat('🧪', '0 labs'),
|
||
],
|
||
onTap: () {},
|
||
),
|
||
),
|
||
SizedBox(
|
||
width: cardWidth,
|
||
height: 180,
|
||
child: InouAddCard(
|
||
label: 'Add dossier',
|
||
onTap: () {},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDataCardsSection() {
|
||
return Column(
|
||
children: [
|
||
// Imaging
|
||
InouCard(
|
||
title: 'Imaging',
|
||
subtitle: '16 studies · 4113 slices',
|
||
indicatorColor: InouTheme.indicatorImaging,
|
||
trailing: InouButton(
|
||
text: 'Open viewer',
|
||
variant: ButtonVariant.secondary,
|
||
size: ButtonSize.small,
|
||
onPressed: () {},
|
||
),
|
||
child: Column(
|
||
children: [
|
||
InouDataRow(
|
||
label: 'MRI BRAIN W/WO CONTRAST',
|
||
meta: '13 series',
|
||
date: '5/5/2022',
|
||
isExpandable: true,
|
||
children: [
|
||
InouChildRow(label: 'AX T1', meta: '24 slices'),
|
||
InouChildRow(label: 'AX T2 FLAIR', meta: '24 slices'),
|
||
InouChildRow(label: 'SAG T1', meta: '20 slices'),
|
||
],
|
||
),
|
||
InouDataRow(
|
||
label: 'XR CHEST AP ONLY',
|
||
date: '5/6/2022',
|
||
trailing: GestureDetector(
|
||
onTap: () {},
|
||
child: Text('→', style: TextStyle(
|
||
color: InouTheme.accent,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w500,
|
||
)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Labs
|
||
InouCard(
|
||
title: 'Labs',
|
||
subtitle: '4 panels · 23 results',
|
||
indicatorColor: InouTheme.indicatorLabs,
|
||
child: Column(
|
||
children: [
|
||
InouDataRow(
|
||
label: 'Complete Blood Count (CBC)',
|
||
meta: '8 tests',
|
||
date: '12/15/2024',
|
||
isExpandable: true,
|
||
initiallyExpanded: true,
|
||
children: const [
|
||
InouChildRow(
|
||
label: 'Hemoglobin',
|
||
value: '14.2 g/dL',
|
||
meta: '12.0–16.0',
|
||
),
|
||
InouChildRow(
|
||
label: 'White Blood Cells',
|
||
value: '7.8 K/µL',
|
||
meta: '4.5–11.0',
|
||
),
|
||
InouChildRow(
|
||
label: 'Platelets',
|
||
value: '142 K/µL',
|
||
meta: '150–400',
|
||
valueColor: InouTheme.danger,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildGeneticsSection() {
|
||
return InouCard(
|
||
title: 'Genetics',
|
||
subtitle: 'Medication Response · 47 variants',
|
||
indicatorColor: InouTheme.indicatorGenetics,
|
||
child: Column(
|
||
children: [
|
||
InouDataRow(
|
||
label: 'Medication Response',
|
||
meta: '47 variants',
|
||
isExpandable: true,
|
||
initiallyExpanded: _showGeneDetails,
|
||
onExpandChanged: (v) => setState(() => _showGeneDetails = v),
|
||
children: [
|
||
_GeneVariantRow(
|
||
gene: 'CYP2C19',
|
||
rsid: 'rs4244285',
|
||
allele: 'G;A',
|
||
status: 'intermediate',
|
||
summary: 'Intermediate metabolizer for clopidogrel (Plavix). May need dose adjustment or alternative medication.',
|
||
onAskAI: () {},
|
||
),
|
||
],
|
||
),
|
||
// Show more link
|
||
InkWell(
|
||
onTap: () {},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(top: BorderSide(color: InouTheme.border)),
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'Show all 47 variants in Medication Response →',
|
||
style: InouTheme.small.copyWith(color: InouTheme.accent),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
InouDataRow(
|
||
label: 'Metabolism',
|
||
meta: '23 variants',
|
||
isExpandable: true,
|
||
),
|
||
InouDataRow(
|
||
label: 'Cardiovascular',
|
||
meta: '18 variants',
|
||
isExpandable: true,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildVitalsSection() {
|
||
return InouCard(
|
||
title: 'Vitals',
|
||
subtitle: 'Self-reported measurements',
|
||
indicatorColor: InouTheme.indicatorVitals,
|
||
trailing: InouButton(
|
||
text: '+ Add',
|
||
variant: ButtonVariant.secondary,
|
||
size: ButtonSize.small,
|
||
onPressed: () {},
|
||
),
|
||
child: Column(
|
||
children: [
|
||
InouDataRow(
|
||
label: 'Temperature',
|
||
value: '37.2 °C',
|
||
meta: 'today',
|
||
leading: const InouNoteIcon(emoji: '🌡', color: InouTheme.danger),
|
||
isExpandable: true,
|
||
initiallyExpanded: _showVitalHistory,
|
||
onExpandChanged: (v) => setState(() => _showVitalHistory = v),
|
||
children: [
|
||
_VitalHistoryRow(date: 'Today, 8:30 AM', value: '37.2 °C'),
|
||
_VitalHistoryRow(date: 'Yesterday, 8:15 AM', value: '36.8 °C'),
|
||
_VitalHistoryRow(date: 'Dec 24, 7:45 AM', value: '37.0 °C'),
|
||
],
|
||
),
|
||
InouDataRow(
|
||
label: 'Weight',
|
||
value: '72.4 kg',
|
||
meta: 'today',
|
||
leading: InouNoteIcon(emoji: '⚖', color: Colors.blue.shade600),
|
||
isExpandable: true,
|
||
),
|
||
InouDataRow(
|
||
label: 'Blood Pressure',
|
||
value: '118/76',
|
||
meta: 'yesterday',
|
||
leading: InouNoteIcon(emoji: '❤', color: Colors.pink.shade600),
|
||
isExpandable: true,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildNotesSection() {
|
||
return InouCard(
|
||
title: 'Notes',
|
||
subtitle: 'Health journal entries',
|
||
indicatorColor: InouTheme.indicatorJournal,
|
||
trailing: InouButton(
|
||
text: '+ Add',
|
||
variant: ButtonVariant.secondary,
|
||
size: ButtonSize.small,
|
||
onPressed: () {},
|
||
),
|
||
child: Column(
|
||
children: [
|
||
InouDataRow(
|
||
label: 'Knee injury',
|
||
meta: '3 photos',
|
||
date: 'Dec 20',
|
||
leading: const InouNoteIcon(emoji: '📷', color: Color(0xFF6366F1)),
|
||
trailing: _NoteCategoryBadge(text: 'injury'),
|
||
isExpandable: true,
|
||
initiallyExpanded: _showNoteDetails,
|
||
onExpandChanged: (v) => setState(() => _showNoteDetails = v),
|
||
children: [
|
||
_NotePhotosRow(),
|
||
_NoteTimelineRow(
|
||
date: 'Dec 20, 3:45 PM',
|
||
text: 'Jim fell on his knee at soccer practice. Swelling visible, applied ice.',
|
||
),
|
||
_NoteTimelineRow(
|
||
date: 'Dec 22, 10:20 AM',
|
||
text: 'Swelling reduced. Still some bruising. Can walk without pain.',
|
||
),
|
||
_NoteTimelineRow(
|
||
date: 'Dec 26, 9:15 AM',
|
||
text: 'Almost fully healed. Light bruise remaining.',
|
||
),
|
||
],
|
||
),
|
||
InouDataRow(
|
||
label: 'Mild headache after workout',
|
||
date: 'Dec 25',
|
||
leading: const InouNoteIcon(emoji: '📝', color: InouTheme.accent),
|
||
isExpandable: true,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSupplementsSection() {
|
||
return InouCard(
|
||
title: 'Supplements',
|
||
subtitle: 'Daily routine',
|
||
indicatorColor: InouTheme.indicatorMedications,
|
||
trailing: InouButton(
|
||
text: '+ Add',
|
||
variant: ButtonVariant.secondary,
|
||
size: ButtonSize.small,
|
||
onPressed: () {},
|
||
),
|
||
child: Column(
|
||
children: const [
|
||
_SupplementRow(
|
||
name: 'Vitamin D3',
|
||
dose: '1 capsule',
|
||
amount: '5000 IU',
|
||
timing: 'morning, with food',
|
||
),
|
||
_SupplementRow(
|
||
name: 'Omega-3 Fish Oil',
|
||
dose: '2 capsules',
|
||
amount: '2000 mg EPA/DHA',
|
||
timing: 'morning, with food',
|
||
),
|
||
_SupplementRow(
|
||
name: 'Magnesium Glycinate',
|
||
dose: '2 capsules',
|
||
amount: '400 mg',
|
||
timing: 'evening',
|
||
),
|
||
_SupplementRow(
|
||
name: 'Liquid B12',
|
||
dose: '5 ml',
|
||
amount: '1000 mcg',
|
||
timing: 'morning',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildPeptidesSection() {
|
||
return InouCard(
|
||
title: 'Peptides',
|
||
subtitle: 'Therapeutic protocols',
|
||
indicatorColor: InouTheme.indicatorMedications,
|
||
child: Column(
|
||
children: const [
|
||
_PeptideRow(
|
||
name: 'BPC-157',
|
||
dose: '250 mcg subQ · 2x daily',
|
||
endDate: 'until Jan 23, 2025',
|
||
status: 'active',
|
||
),
|
||
_PeptideRow(
|
||
name: 'TB-500',
|
||
dose: '2.5 mg subQ · 2x weekly',
|
||
endDate: 'until Feb 5, 2025',
|
||
status: 'active',
|
||
),
|
||
_PeptideRow(
|
||
name: 'BPC-157',
|
||
dose: '250 mcg subQ · 2x daily',
|
||
endDate: 'Aug 15 – Sep 7, 2025',
|
||
status: 'completed',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildUploadSection() {
|
||
return InouCard(
|
||
title: 'Upload Area',
|
||
indicatorColor: InouTheme.indicatorUploads,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Container(
|
||
padding: const EdgeInsets.all(40),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(
|
||
color: InouTheme.border,
|
||
width: 2,
|
||
style: BorderStyle.solid,
|
||
),
|
||
borderRadius: InouTheme.borderRadiusLg,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Icon(
|
||
Icons.cloud_upload_outlined,
|
||
size: 32,
|
||
color: InouTheme.accent,
|
||
),
|
||
const SizedBox(height: 12),
|
||
Text(
|
||
'Click or drag files here',
|
||
style: InouTheme.body.copyWith(fontWeight: FontWeight.w500),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'DICOM, PDF, CSV, VCF, and more',
|
||
style: InouTheme.small,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildEmptyStateSection() {
|
||
return InouCard(
|
||
title: 'Empty State',
|
||
indicatorColor: InouTheme.indicatorRecords,
|
||
child: Container(
|
||
padding: const EdgeInsets.all(32),
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.bgCard,
|
||
border: Border.all(color: InouTheme.border),
|
||
borderRadius: InouTheme.borderRadiusLg,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'No lab data',
|
||
style: InouTheme.body.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// Helper widgets
|
||
// ============================================
|
||
|
||
class _TypographyRow extends StatelessWidget {
|
||
final String text;
|
||
final TextStyle style;
|
||
final String spec;
|
||
final bool isUppercase;
|
||
|
||
const _TypographyRow(this.text, this.style, this.spec, {this.isUppercase = false});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
isUppercase ? text.toUpperCase() : text,
|
||
style: style,
|
||
),
|
||
),
|
||
Text(
|
||
spec,
|
||
style: InouTheme.mono.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ColorRow extends StatelessWidget {
|
||
final String name;
|
||
final Color color;
|
||
final String hex;
|
||
final bool hasBorder;
|
||
|
||
const _ColorRow(this.name, this.color, this.hex, {this.hasBorder = false});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 32,
|
||
height: 32,
|
||
decoration: BoxDecoration(
|
||
color: color,
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: hasBorder ? Border.all(color: InouTheme.border) : null,
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Text(name, style: InouTheme.body.copyWith(fontWeight: FontWeight.w500)),
|
||
),
|
||
Text(hex, style: InouTheme.mono.copyWith(color: InouTheme.textMuted)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SettingsRow extends StatelessWidget {
|
||
final String label;
|
||
final String description;
|
||
final Widget child;
|
||
|
||
const _SettingsRow({
|
||
required this.label,
|
||
required this.description,
|
||
required this.child,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
flex: 2,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label, style: InouTheme.body.copyWith(fontWeight: FontWeight.w500)),
|
||
const SizedBox(height: 2),
|
||
Text(description, style: InouTheme.small),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 24),
|
||
Expanded(
|
||
flex: 3,
|
||
child: child,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LLMOption extends StatelessWidget {
|
||
final String icon;
|
||
final String name;
|
||
final String value;
|
||
final bool selected;
|
||
final VoidCallback onTap;
|
||
|
||
const _LLMOption({
|
||
required this.icon,
|
||
required this.name,
|
||
required this.value,
|
||
required this.selected,
|
||
required this.onTap,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
margin: const EdgeInsets.only(bottom: 8),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: selected ? InouTheme.accentLight : InouTheme.bgCard,
|
||
border: Border.all(
|
||
color: selected ? InouTheme.accent : InouTheme.border,
|
||
),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 24,
|
||
height: 24,
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.bg,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(icon, style: const TextStyle(fontSize: 12)),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
name,
|
||
style: InouTheme.body.copyWith(
|
||
color: selected ? InouTheme.accent : InouTheme.text,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _GeneVariantRow extends StatelessWidget {
|
||
final String gene;
|
||
final String rsid;
|
||
final String allele;
|
||
final String status;
|
||
final String summary;
|
||
final VoidCallback onAskAI;
|
||
|
||
const _GeneVariantRow({
|
||
required this.gene,
|
||
required this.rsid,
|
||
required this.allele,
|
||
required this.status,
|
||
required this.summary,
|
||
required this.onAskAI,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(top: BorderSide(color: InouTheme.border)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(gene, style: InouTheme.body.copyWith(fontWeight: FontWeight.w600)),
|
||
const SizedBox(width: 8),
|
||
Text(rsid, style: InouTheme.mono.copyWith(fontSize: 12, color: InouTheme.textMuted)),
|
||
const Spacer(),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.bg,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(allele, style: InouTheme.mono.copyWith(fontWeight: FontWeight.w600)),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(status, style: InouTheme.small.copyWith(color: InouTheme.accent)),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(summary, style: InouTheme.small.copyWith(height: 1.4)),
|
||
const SizedBox(height: 12),
|
||
GestureDetector(
|
||
onTap: onAskAI,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.accentLight,
|
||
border: Border.all(color: InouTheme.accent),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
'Ask AI',
|
||
style: InouTheme.small.copyWith(
|
||
color: InouTheme.accent,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _VitalHistoryRow extends StatelessWidget {
|
||
final String date;
|
||
final String value;
|
||
|
||
const _VitalHistoryRow({required this.date, required this.value});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(date, style: InouTheme.small),
|
||
Text(value, style: InouTheme.mono),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _NoteCategoryBadge extends StatelessWidget {
|
||
final String text;
|
||
|
||
const _NoteCategoryBadge({required this.text});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.bg,
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
child: Text(
|
||
text,
|
||
style: InouTheme.small.copyWith(
|
||
fontSize: 11.25,
|
||
color: InouTheme.textSubtle,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _NotePhotosRow extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: const BoxDecoration(
|
||
color: InouTheme.bg,
|
||
border: Border(top: BorderSide(color: InouTheme.border)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
_PhotoPlaceholder(label: 'Dec 20, 3:45 PM'),
|
||
const SizedBox(width: 12),
|
||
_PhotoPlaceholder(label: 'Dec 22, 10:20 AM'),
|
||
const SizedBox(width: 12),
|
||
_PhotoPlaceholder(label: 'Dec 26, 9:15 AM'),
|
||
const SizedBox(width: 12),
|
||
_AddPhotoPlaceholder(),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PhotoPlaceholder extends StatelessWidget {
|
||
final String label;
|
||
|
||
const _PhotoPlaceholder({required this.label});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
Container(
|
||
width: 64,
|
||
height: 64,
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.border,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: InouTheme.border),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: const Text('🦵', style: TextStyle(fontSize: 24)),
|
||
),
|
||
const SizedBox(height: 4),
|
||
SizedBox(
|
||
width: 64,
|
||
child: Text(
|
||
label.split(', ').first,
|
||
style: InouTheme.small.copyWith(fontSize: 10.5),
|
||
textAlign: TextAlign.center,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _AddPhotoPlaceholder extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
Container(
|
||
width: 64,
|
||
height: 64,
|
||
decoration: BoxDecoration(
|
||
color: InouTheme.bgCard,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: InouTheme.border, style: BorderStyle.solid),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text('+', style: TextStyle(fontSize: 24, color: InouTheme.accent)),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'Add photo',
|
||
style: InouTheme.small.copyWith(fontSize: 10.5, color: InouTheme.accent),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _NoteTimelineRow extends StatelessWidget {
|
||
final String date;
|
||
final String text;
|
||
|
||
const _NoteTimelineRow({required this.date, required this.text});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: InouTheme.border, style: BorderStyle.solid)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(date, style: InouTheme.small),
|
||
const SizedBox(height: 4),
|
||
Text(text, style: InouTheme.body.copyWith(fontSize: 13.5)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SupplementRow extends StatelessWidget {
|
||
final String name;
|
||
final String dose;
|
||
final String amount;
|
||
final String timing;
|
||
|
||
const _SupplementRow({
|
||
required this.name,
|
||
required this.dose,
|
||
required this.amount,
|
||
required this.timing,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: InouTheme.border)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(name, style: InouTheme.body.copyWith(fontWeight: FontWeight.w500)),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'$dose · $amount',
|
||
style: InouTheme.small,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Text(timing, style: InouTheme.small.copyWith(color: InouTheme.textSubtle)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PeptideRow extends StatelessWidget {
|
||
final String name;
|
||
final String dose;
|
||
final String endDate;
|
||
final String status;
|
||
|
||
const _PeptideRow({
|
||
required this.name,
|
||
required this.dose,
|
||
required this.endDate,
|
||
required this.status,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final isActive = status == 'active';
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: InouTheme.border)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text(name, style: InouTheme.body.copyWith(fontWeight: FontWeight.w500)),
|
||
const SizedBox(width: 8),
|
||
Text(dose, style: InouTheme.small),
|
||
],
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(endDate, style: InouTheme.small.copyWith(color: InouTheme.textSubtle)),
|
||
],
|
||
),
|
||
),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: isActive ? InouTheme.successLight : InouTheme.bg,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
status,
|
||
style: InouTheme.small.copyWith(
|
||
color: isActive ? InouTheme.success : InouTheme.textMuted,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|