1190 lines
37 KiB
Dart
1190 lines
37 KiB
Dart
// Flutter Styleguide — matches inou.com/styleguide
|
||
import 'package:flutter/material.dart';
|
||
import 'package:inou_app/design/inou_theme.dart';
|
||
import 'package:inou_app/design/inou_text.dart';
|
||
import 'package:inou_app/design/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;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: InouTheme.bg,
|
||
body: Column(
|
||
children: [
|
||
// Header component
|
||
const InouHeader(),
|
||
|
||
// Content
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Center(
|
||
child: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Page title
|
||
Text('Style Guide', style: InouText.pageTitle),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Design system components for inou',
|
||
style: InouText.bodyLight.copyWith(
|
||
color: InouTheme.textMuted,
|
||
),
|
||
),
|
||
const SizedBox(height: 32),
|
||
|
||
// Header Component Preview
|
||
_buildHeaderSection(),
|
||
|
||
// Text Blocks
|
||
_buildTextBlocksSection(),
|
||
|
||
// Typography
|
||
_buildTypographySection(),
|
||
|
||
// Colors
|
||
_buildColorsSection(),
|
||
|
||
// Buttons
|
||
_buildButtonsSection(),
|
||
|
||
// Badges
|
||
_buildBadgesSection(),
|
||
|
||
// Messages
|
||
_buildMessagesSection(),
|
||
|
||
// Form Elements
|
||
_buildFormsSection(),
|
||
|
||
// Profile Cards
|
||
_buildProfileCardsSection(),
|
||
|
||
// Data Cards (Imaging, Labs)
|
||
_buildDataCardsSection(),
|
||
|
||
// Settings
|
||
_buildSettingsSection(),
|
||
|
||
// Genetics
|
||
_buildGeneticsSection(),
|
||
|
||
// Notes
|
||
_buildNotesSection(),
|
||
|
||
// Supplements
|
||
_buildSupplementsSection(),
|
||
|
||
// Peptides
|
||
_buildPeptidesSection(),
|
||
|
||
// Upload Area
|
||
_buildUploadSection(),
|
||
|
||
// Empty State
|
||
_buildEmptyStateSection(),
|
||
|
||
const SizedBox(height: 48),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
String _selectedLLM = 'claude';
|
||
String _selectedUnits = 'metric';
|
||
|
||
Widget _buildSettingsSection() {
|
||
return InouCard(
|
||
title: 'Settings',
|
||
indicatorColor: InouTheme.indicatorPrivacy,
|
||
child: Column(
|
||
children: [
|
||
// LLM Selector
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
flex: 2,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('Primary AI Assistant', style: InouText.label),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'Used for "Ask AI" prompts and analysis',
|
||
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 24),
|
||
Expanded(
|
||
flex: 3,
|
||
child: Column(
|
||
children: [
|
||
_LLMOption(
|
||
icon: '🤖',
|
||
name: 'Claude (Anthropic)',
|
||
selected: _selectedLLM == 'claude',
|
||
onTap: () => setState(() => _selectedLLM = 'claude'),
|
||
),
|
||
_LLMOption(
|
||
icon: '💬',
|
||
name: 'ChatGPT (OpenAI)',
|
||
selected: _selectedLLM == 'chatgpt',
|
||
onTap: () => setState(() => _selectedLLM = 'chatgpt'),
|
||
),
|
||
_LLMOption(
|
||
icon: '✖',
|
||
name: 'Grok (xAI)',
|
||
selected: _selectedLLM == 'grok',
|
||
onTap: () => setState(() => _selectedLLM = 'grok'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 1, color: InouTheme.border),
|
||
// Units
|
||
Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
flex: 2,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('Units', style: InouText.label),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'Measurement system for vitals',
|
||
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 24),
|
||
Expanded(
|
||
flex: 3,
|
||
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 _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: true,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Text('CYP2C19', style: InouText.label),
|
||
const SizedBox(width: 8),
|
||
Text('rs4244285', style: InouText.mono.copyWith(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('G;A', style: InouText.mono.copyWith(fontWeight: FontWeight.w600)),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text('intermediate', style: InouText.bodySmall.copyWith(color: InouTheme.accent)),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'Intermediate metabolizer for clopidogrel (Plavix). May need dose adjustment or alternative medication.',
|
||
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted, height: 1.4),
|
||
),
|
||
const SizedBox(height: 12),
|
||
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: InouText.bodySmall.copyWith(color: InouTheme.accent, fontWeight: FontWeight.w500),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
InkWell(
|
||
onTap: () {},
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
decoration: const BoxDecoration(
|
||
border: Border(top: BorderSide(color: InouTheme.border)),
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'Show all 47 variants in Medication Response →',
|
||
style: InouText.bodySmall.copyWith(color: InouTheme.accent),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const InouDataRow(label: 'Metabolism', meta: '23 variants', isExpandable: true),
|
||
const InouDataRow(label: 'Cardiovascular', meta: '18 variants', 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)),
|
||
isExpandable: true,
|
||
initiallyExpanded: true,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(16),
|
||
color: InouTheme.bg,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Photos
|
||
Row(
|
||
children: [
|
||
_PhotoPlaceholder(label: 'Dec 20'),
|
||
const SizedBox(width: 12),
|
||
_PhotoPlaceholder(label: 'Dec 22'),
|
||
const SizedBox(width: 12),
|
||
_PhotoPlaceholder(label: 'Dec 26'),
|
||
const SizedBox(width: 12),
|
||
_AddPhotoPlaceholder(),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
// Timeline
|
||
_NoteTimelineEntry(date: 'Dec 20, 3:45 PM', text: 'Jim fell on his knee at soccer practice. Swelling visible, applied ice.'),
|
||
_NoteTimelineEntry(date: 'Dec 22, 10:20 AM', text: 'Swelling reduced. Still some bruising. Can walk without pain.'),
|
||
_NoteTimelineEntry(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 _buildEmptyStateSection() {
|
||
return InouCard(
|
||
title: 'Empty State',
|
||
indicatorColor: InouTheme.indicatorRecords,
|
||
child: Container(
|
||
padding: const EdgeInsets.all(32),
|
||
child: Center(
|
||
child: Text(
|
||
'No lab data',
|
||
style: InouText.body.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHeaderSection() {
|
||
return InouCard(
|
||
title: 'Header Component',
|
||
indicatorColor: InouTheme.accent,
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'The InouHeader widget is shown at the top of this page.',
|
||
style: InouText.body.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: InouTheme.border),
|
||
borderRadius: InouTheme.borderRadiusLg,
|
||
),
|
||
child: ClipRRect(
|
||
borderRadius: InouTheme.borderRadiusLg,
|
||
child: const InouHeader(),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'Logo: "inou" (accent, 700) + "health" (muted, 300) • Font: Sora 1.75rem',
|
||
style: InouText.mono.copyWith(
|
||
color: InouTheme.textMuted,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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: InouText.heroTitle,
|
||
),
|
||
const SizedBox(height: 16),
|
||
InouText.rich([
|
||
InouSpan.plain('We built ', style: InouText.intro),
|
||
InouSpan.accent('inou', baseStyle: InouText.intro.copyWith(fontWeight: FontWeight.w700)),
|
||
InouSpan.plain(
|
||
' 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.',
|
||
style: InouText.intro,
|
||
),
|
||
]),
|
||
const SizedBox(height: 32),
|
||
Text(
|
||
'What we collect',
|
||
style: InouText.sectionTitle,
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
'Account information.',
|
||
style: InouText.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: InouText.intro,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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', InouText.pageTitle, '2.5rem / 800'),
|
||
_TypographyRow('Section Title', InouText.sectionTitle, '1.4rem / 600'),
|
||
_TypographyRow('Subsection Title', InouText.subsectionTitle, '1.1rem / 600'),
|
||
_TypographyRow(
|
||
'LABEL / CATEGORY',
|
||
InouText.labelCaps,
|
||
'0.75rem / 600 / caps',
|
||
),
|
||
_TypographyRow(
|
||
'Intro text — larger, lighter',
|
||
InouText.intro,
|
||
'1.15rem / 300',
|
||
),
|
||
_TypographyRow(
|
||
'Body light — long-form',
|
||
InouText.bodyLight,
|
||
'1rem / 300',
|
||
),
|
||
_TypographyRow(
|
||
'Body regular — UI labels',
|
||
InouText.body,
|
||
'1rem / 400',
|
||
),
|
||
_TypographyRow(
|
||
'Mono: 1,234,567.89',
|
||
InouText.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 — 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 _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: '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'),
|
||
],
|
||
),
|
||
const InouDataRow(
|
||
label: 'XR CHEST AP ONLY',
|
||
date: '5/6/2022',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 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,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Vitals
|
||
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,
|
||
),
|
||
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 _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,
|
||
),
|
||
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: InouText.label,
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'DICOM, PDF, CSV, VCF, and more',
|
||
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// Helper widgets
|
||
class _TypographyRow extends StatelessWidget {
|
||
final String text;
|
||
final TextStyle style;
|
||
final String spec;
|
||
|
||
const _TypographyRow(this.text, this.style, this.spec);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
Expanded(child: Text(text, style: style)),
|
||
Text(
|
||
spec,
|
||
style: InouText.mono.copyWith(
|
||
fontSize: 12,
|
||
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: InouText.label),
|
||
),
|
||
Text(
|
||
hex,
|
||
style: InouText.mono.copyWith(
|
||
fontSize: 12,
|
||
color: InouTheme.textMuted,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _LLMOption extends StatelessWidget {
|
||
final String icon;
|
||
final String name;
|
||
final bool selected;
|
||
final VoidCallback onTap;
|
||
|
||
const _LLMOption({
|
||
required this.icon,
|
||
required this.name,
|
||
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: InouText.body.copyWith(color: selected ? InouTheme.accent : InouTheme.text),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: const Text('🦵', style: TextStyle(fontSize: 24)),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(label, style: InouText.bodySmall.copyWith(fontSize: 11)),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
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),
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text('+', style: TextStyle(fontSize: 24, color: InouTheme.accent)),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text('Add photo', style: InouText.bodySmall.copyWith(fontSize: 11, color: InouTheme.accent)),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _NoteTimelineEntry extends StatelessWidget {
|
||
final String date;
|
||
final String text;
|
||
|
||
const _NoteTimelineEntry({required this.date, required this.text});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(date, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted)),
|
||
const SizedBox(height: 4),
|
||
Text(text, style: InouText.bodySmall),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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: InouText.label),
|
||
const SizedBox(height: 2),
|
||
Text('$dose · $amount', style: InouText.bodySmall.copyWith(color: InouTheme.textMuted)),
|
||
],
|
||
),
|
||
),
|
||
Text(timing, style: InouText.bodySmall.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: InouText.label),
|
||
const SizedBox(width: 8),
|
||
Text(dose, style: InouText.bodySmall.copyWith(color: InouTheme.textMuted)),
|
||
],
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(endDate, style: InouText.bodySmall.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: InouText.bodySmall.copyWith(
|
||
color: isActive ? InouTheme.success : InouTheme.textMuted,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|