diff --git a/.claude/skills/studyu-design/SKILL.md b/.claude/skills/studyu-design/SKILL.md
new file mode 100644
index 000000000..f1e4c61fa
--- /dev/null
+++ b/.claude/skills/studyu-design/SKILL.md
@@ -0,0 +1,40 @@
+---
+name: studyu-design
+description: Create, review, or update StudyU-branded interfaces, assets, copy, and production UI across the participant mobile app, researcher designer dashboard, marketing/docs site, and prototypes. Use when work involves StudyU visual design, Material 3 styling, brand colors, Work Sans typography, logos, UI kits, Flutter UI, HTML mockups, CSS tokens, or design-system consistency.
+---
+
+# StudyU Design
+
+Use the bundled design system before producing StudyU UI, prototypes, visual assets, or design-system changes.
+
+## Workflow
+
+1. Read `references/design-system.md` for brand, voice, colors, typography, shape, icon, and component guidance.
+2. Use `colors_and_type.css` for concrete CSS variables and token values when building HTML/CSS artifacts.
+3. Inspect only the relevant preview or UI kit files:
+ - `preview/*.html` for token and component specimens.
+ - `ui_kits/app/index.html` for participant mobile app patterns.
+ - `ui_kits/designer/index.html` for researcher dashboard patterns.
+4. Reuse assets from `assets/` and `fonts/` when making mockups, previews, or production assets.
+5. For production code, adapt the guidance to the repository's existing Flutter, Docusaurus, or web patterns instead of copying throwaway HTML directly.
+
+## Quick Facts
+
+- Primary: #2196F3 (Material Blue)
+- Accent: #FF9800 (Material Orange)
+- Logo gradient: #00E5CC to #2196F3
+- Font: Work Sans (`fonts/WorkSans-VariableFont_wght.ttf`) loaded with `@font-face`
+- Icons: Material Icons (filled)
+- Card radius: 8px; input radius: 5px; elevation: flat (shadow-sm only)
+- Background: #EBF4FD (light blue tint)
+- CSS vars: colors_and_type.css
+- Mobile UI kit: ui_kits/app/index.html
+- Designer UI kit: ui_kits/designer/index.html
+
+## Guardrails
+
+- Keep StudyU UI calm, trustworthy, clear, and science-grounded.
+- Use Material Design 3 conventions and the repository's existing component patterns.
+- Avoid emoji in UI; prefer Material Icons.
+- Use title case for screen headings and sentence case for body/supporting copy.
+- Keep cards and controls restrained: 8px radius for cards/list tiles/buttons, 5px for inputs, minimal shadows.
diff --git a/.claude/skills/studyu-design/agents/openai.yaml b/.claude/skills/studyu-design/agents/openai.yaml
new file mode 100644
index 000000000..d1350bdb7
--- /dev/null
+++ b/.claude/skills/studyu-design/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "StudyU Design"
+ short_description: "Apply the StudyU design system, tokens, assets, and UI kits."
+ default_prompt: "Use the StudyU design system to create or review a branded interface."
diff --git a/.claude/skills/studyu-design/assets/studyulogo.png b/.claude/skills/studyu-design/assets/studyulogo.png
new file mode 100644
index 000000000..44571b3c2
Binary files /dev/null and b/.claude/skills/studyu-design/assets/studyulogo.png differ
diff --git a/.claude/skills/studyu-design/colors_and_type.css b/.claude/skills/studyu-design/colors_and_type.css
new file mode 100644
index 000000000..6d9b3fcc7
--- /dev/null
+++ b/.claude/skills/studyu-design/colors_and_type.css
@@ -0,0 +1,265 @@
+/* ============================================================
+ StudyU Design System - Colors & Typography
+ Source: hpi-studyu/studyu (branch: dev)
+ Font: Work Sans variable
+ ============================================================ */
+
+@font-face {
+ font-family: 'Work Sans';
+ src: url('fonts/WorkSans-VariableFont_wght.ttf') format('truetype');
+ font-weight: 100 900;
+ font-style: normal;
+ font-display: swap;
+}
+@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
+
+/* -- M3 Color Roles (seed: #2196F3, SchemeFidelity / light) -- */
+/* Generated from material_color_utilities matching designer_v2/lib/theme.dart */
+:root {
+ /* Primary */
+ --md-primary: #0061A4;
+ --md-on-primary: #FFFFFF;
+ --md-primary-container: #D1E4FF;
+ --md-on-primary-container: #001D36;
+
+ /* Secondary (Orange accent - app theme) */
+ --md-secondary: #FF9800;
+ --md-on-secondary: #FFFFFF;
+ --md-secondary-container: #FFE0B2;
+ --md-on-secondary-container: #3D1F00;
+
+ /* Tertiary */
+ --md-tertiary: #006874;
+ --md-on-tertiary: #FFFFFF;
+ --md-tertiary-container: #97F0FF;
+ --md-on-tertiary-container: #001F24;
+
+ /* Error */
+ --md-error: #BA1A1A;
+ --md-on-error: #FFFFFF;
+ --md-error-container: #FFDAD6;
+ --md-on-error-container: #410002;
+
+ /* Surface tones */
+ --md-surface: #FDFCFF;
+ --md-on-surface: #1A1C1E;
+ --md-surface-variant: #DFE2EB;
+ --md-on-surface-variant: #43474E;
+ --md-surface-container-lowest: #FFFFFF;
+ --md-surface-container-low: #F7F9FF; /* scaffold bg (@15% primaryContainer) */
+ --md-surface-container: #F1F3FA;
+ --md-surface-container-high: #E9ECF4;
+ --md-surface-container-highest: #E3E5ED;
+
+ /* Outline */
+ --md-outline: #73777F;
+ --md-outline-variant: #C3C7CF;
+
+ /* Inverse */
+ --md-inverse-surface: #2F3033;
+ --md-inverse-on-surface: #F1F0F4;
+ --md-inverse-primary: #9ECAFF;
+
+ /* Legacy / brand extras */
+ --color-logo-start: #00E5CC;
+ --color-logo-end: #2196F3;
+ --color-link: #00B0FF;
+ --color-primary-500: #2196F3; /* M2 blue - used in app appbar */
+
+ /* State layers (M3 spec) */
+ --md-state-hover-opacity: 0.08;
+ --md-state-focus-opacity: 0.12;
+ --md-state-pressed-opacity: 0.12;
+ --md-state-dragged-opacity: 0.16;
+}
+
+/* -- M3 Shape Scale ------------------------------------------ */
+:root {
+ --md-shape-none: 0px;
+ --md-shape-extra-small: 4px;
+ --md-shape-small: 8px; /* cards in designer */
+ --md-shape-medium: 12px;
+ --md-shape-large: 16px;
+ --md-shape-extra-large: 28px;
+ --md-shape-full: 9999px;
+
+ /* Aliases used in codebase */
+ --radius-tooltip: 2px; /* custom - not standard M3 */
+ --radius-input: 5px; /* custom */
+ --radius-card: var(--md-shape-small);
+ --radius-button: var(--md-shape-full);
+}
+
+/* -- M3 Elevation (surface tint, not drop-shadow) ----------- */
+:root {
+ --md-elevation-0: transparent;
+ --md-elevation-1: rgba(0,97,164, 0.05); /* tinted with primary */
+ --md-elevation-2: rgba(0,97,164, 0.08);
+ --md-elevation-3: rgba(0,97,164, 0.11);
+ --md-elevation-4: rgba(0,97,164, 0.12);
+ --md-elevation-5: rgba(0,97,164, 0.14);
+
+ /* App-specific shadows (non-M3, used in appbar) */
+ --shadow-appbar: 0 2px 6px rgba(187,222,251,0.30);
+ --shadow-card: none;
+}
+
+/* -- Semantic Role Aliases (M3-aligned) ---------------------- */
+:root {
+ --fg1: var(--md-on-surface); /* primary body text */
+ --fg2: var(--md-on-surface-variant); /* secondary / captions */
+ --fg3: var(--md-outline); /* muted / helper */
+ --fg4: var(--md-outline-variant); /* placeholder */
+ --fgHeading: var(--md-on-surface-variant);
+
+ --bgPage: var(--md-surface-container-low);
+ --bgCard: var(--md-surface-container-lowest);
+ --bgInput: var(--md-surface-container-lowest);
+
+ --interactive: var(--md-primary);
+ --interactiveAccent: var(--md-secondary);
+}
+
+/* -- Typography Base (M3 Type Scale) ------------------------- */
+:root {
+ /* Font families */
+ --font-sans: 'Work Sans', -apple-system, BlinkMacSystemFont, sans-serif;
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
+
+ /* M3 standard type scale */
+ --md-display-large-size: 57px; --md-display-large-lh: 1.12; --md-display-large-weight: 400;
+ --md-display-medium-size: 45px; --md-display-medium-lh: 1.15; --md-display-medium-weight: 400;
+ --md-display-small-size: 36px; --md-display-small-lh: 1.22; --md-display-small-weight: 400;
+ --md-headline-large-size: 32px; --md-headline-large-lh: 1.25; --md-headline-large-weight: 400;
+ --md-headline-medium-size: 28px; --md-headline-medium-lh: 1.28; --md-headline-medium-weight:400;
+ --md-headline-small-size: 24px; --md-headline-small-lh: 1.33; --md-headline-small-weight: 400;
+ --md-title-large-size: 22px; --md-title-large-lh: 1.27; --md-title-large-weight: 400;
+ --md-title-medium-size: 16px; --md-title-medium-lh: 1.5; --md-title-medium-weight: 500;
+ --md-title-small-size: 14px; --md-title-small-lh: 1.43; --md-title-small-weight: 500;
+ --md-body-large-size: 16px; --md-body-large-lh: 1.5; --md-body-large-weight: 400;
+ --md-body-medium-size: 14px; --md-body-medium-lh: 1.43; --md-body-medium-weight: 400;
+ --md-body-small-size: 12px; --md-body-small-lh: 1.33; --md-body-small-weight: 400;
+ --md-label-large-size: 14px; --md-label-large-lh: 1.43; --md-label-large-weight: 500;
+ --md-label-medium-size: 12px; --md-label-medium-lh: 1.33; --md-label-medium-weight: 500;
+ --md-label-small-size: 11px; --md-label-small-lh: 1.45; --md-label-small-weight: 500;
+
+ /* App-custom overrides (from designer_v2/lib/theme.dart) */
+ --app-body: 14px; /* all body roles use 14px in app */
+ --app-title-large: 15px;
+ --app-headline-sm: 18px;
+ --app-headline-md: 22px;
+ --app-display-sm: 26px;
+ --app-display-md: 36px;
+ --app-display-lg: 48px;
+
+ /* Line heights */
+ --lh-body: 1.35;
+ --lh-heading: 1.2;
+
+ /* Font weights */
+ --fw-regular: 400;
+ --fw-medium: 500;
+ --fw-semibold:600;
+ --fw-bold: 700;
+}
+
+/* -- Semantic Typography Classes ----------------------------- */
+.text-display-lg {
+ font-family: var(--font-sans);
+ font-size: var(--text-display-lg);
+ font-weight: var(--fw-bold);
+ line-height: var(--lh-heading);
+ color: var(--fgHeading);
+}
+.text-display-md {
+ font-family: var(--font-sans);
+ font-size: var(--text-display-md);
+ font-weight: var(--fw-bold);
+ line-height: var(--lh-heading);
+ color: var(--fgHeading);
+}
+.text-display-sm {
+ font-family: var(--font-sans);
+ font-size: var(--text-display-sm);
+ font-weight: var(--fw-bold);
+ line-height: var(--lh-heading);
+ color: var(--fgHeading);
+}
+.text-headline-md {
+ font-family: var(--font-sans);
+ font-size: var(--text-headline-md);
+ font-weight: var(--fw-bold);
+ line-height: var(--lh-heading);
+ color: var(--fgHeading);
+}
+.text-headline-sm {
+ font-family: var(--font-sans);
+ font-size: var(--text-headline-sm);
+ font-weight: var(--fw-bold);
+ line-height: var(--lh-heading);
+ color: var(--fgHeading);
+}
+.text-title-lg {
+ font-family: var(--font-sans);
+ font-size: var(--text-title-lg);
+ font-weight: var(--fw-bold);
+ line-height: var(--lh-heading);
+ color: var(--fgHeading);
+}
+.text-body {
+ font-family: var(--font-sans);
+ font-size: var(--text-body-sm);
+ font-weight: var(--fw-regular);
+ line-height: var(--lh-body);
+ color: var(--fg1);
+}
+.text-body-muted {
+ font-family: var(--font-sans);
+ font-size: var(--text-body-sm);
+ font-weight: var(--fw-regular);
+ line-height: var(--lh-body);
+ color: var(--fg3);
+}
+.text-label {
+ font-family: var(--font-sans);
+ font-size: var(--text-body-sm);
+ font-weight: var(--fw-semibold);
+ line-height: var(--lh-body);
+ color: var(--fg1);
+}
+.text-link {
+ font-family: var(--font-sans);
+ font-size: var(--text-body-sm);
+ color: var(--color-link);
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+/* -- Spacing Tokens ------------------------------------------ */
+:root {
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-8: 32px;
+ --space-10: 40px;
+ --space-12: 48px;
+ --space-16: 64px;
+
+ /* M3 shape aliases */
+ --radius-sm: var(--md-shape-extra-small); /* 4px - tooltips */
+ --radius-md: 5px; /* inputs (app custom) */
+ --radius-lg: var(--md-shape-small); /* 8px - cards */
+ --radius-full: var(--md-shape-full); /* buttons, chips */
+
+ --content-min: 600px;
+ --content-max: 1264px;
+
+ /* Elevation / Shadow (M3: surface tint preferred; shadow kept for appbar) */
+ --shadow-none: none;
+ --shadow-sm: 0 1px 3px rgba(0,97,164,0.10), 0 1px 2px rgba(0,97,164,0.07);
+ --shadow-md: 0 2px 8px rgba(0,97,164,0.12), 0 1px 4px rgba(0,97,164,0.08);
+ --shadow-appbar: 0 2px 6px rgba(209,228,255,0.40);
+}
diff --git a/.claude/skills/studyu-design/copilot.instructions.md b/.claude/skills/studyu-design/copilot.instructions.md
new file mode 100644
index 000000000..c52bc1d5c
--- /dev/null
+++ b/.claude/skills/studyu-design/copilot.instructions.md
@@ -0,0 +1,26 @@
+---
+applyTo: "**/*.{dart,css,html,md,mdx,js,jsx,ts,tsx}"
+---
+
+# StudyU Design System Instructions
+
+Use the StudyU design system whenever generating or editing user-facing UI, visual assets, prototypes, or product copy in this repository.
+
+Reference source:
+- Full design system: `.claude/skills/studyu-design/references/design-system.md`
+- CSS tokens and type: `.claude/skills/studyu-design/colors_and_type.css`
+- Logo: `.claude/skills/studyu-design/assets/studyulogo.png`
+- Fonts: `.claude/skills/studyu-design/fonts/`
+- Participant app UI kit: `.claude/skills/studyu-design/ui_kits/app/index.html`
+- Researcher designer UI kit: `.claude/skills/studyu-design/ui_kits/designer/index.html`
+- Component previews: `.claude/skills/studyu-design/preview/`
+
+Core rules:
+- Use StudyU primary blue `#2196F3`, accent orange `#FF9800`, light blue background `#EBF4FD`, and logo gradient `#00E5CC` to `#2196F3`.
+- Prefer Work Sans where available; otherwise preserve the repository's existing fallback font behavior.
+- Follow Material Design 3 patterns and the existing Flutter/Docusaurus/UI conventions in the touched code.
+- Keep UI calm, trustworthy, clear, and science-grounded.
+- Use Material Icons instead of emoji.
+- Use title case for screen headings and sentence case for body/supporting copy.
+- Keep cards, list tiles, and buttons at 8px radius; inputs at 5px radius; avoid decorative shadows except established app bar behavior.
+- Avoid gradients outside the logo, decorative backgrounds, and noisy animation.
diff --git a/.claude/skills/studyu-design/fonts/WorkSans-VariableFont_wght.ttf b/.claude/skills/studyu-design/fonts/WorkSans-VariableFont_wght.ttf
new file mode 100644
index 000000000..16293f140
Binary files /dev/null and b/.claude/skills/studyu-design/fonts/WorkSans-VariableFont_wght.ttf differ
diff --git a/.claude/skills/studyu-design/preview/brand-logo.html b/.claude/skills/studyu-design/preview/brand-logo.html
new file mode 100644
index 000000000..0a883fe5e
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/brand-logo.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ 
+
+
+
+
Rounded sans-serif type
+
Material Icons
+
Flat cards ; 8px radius
+
+
diff --git a/.claude/skills/studyu-design/preview/colors-m3-roles.html b/.claude/skills/studyu-design/preview/colors-m3-roles.html
new file mode 100644
index 000000000..a76e830c4
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/colors-m3-roles.html
@@ -0,0 +1,71 @@
+
+
+
+
+M3 Color Roles - Primary
+
+
+
+
+
Primary Container
+
#D1E4FF
+
+
+
On P. Container
+
#001D36
+
+
+Secondary (Accent - Orange)
+
+
+
+
On Secondary
+
#FFFFFF
+
+
+
Sec. Container
+
#FFE0B2
+
+
+
On S. Container
+
#3D1F00
+
+
+Error & Surface
+
+
+
+
Error Container
+
#FFDAD6
+
+
+
+
Surface Variant
+
#DFE2EB
+
+
+
diff --git a/.claude/skills/studyu-design/preview/colors-primary.html b/.claude/skills/studyu-design/preview/colors-primary.html
new file mode 100644
index 000000000..7a30a4cc6
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/colors-primary.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+Primary - Blue
+
+
800 #1565C0
+
500 #2196F3 ★
+
300 #64B5F6
+
100 #BBDEFB
+
50 #E3F2FD
+
+Accent - Orange
+
+
900 #E65100
+
500 #FF9800 ★
+
300 #FFB74D
+
100 #FFE0B2
+
50 #FFF3E0
+
+Logo Gradient
+
+
Cyan #00E5CC → Blue #2196F3
+
+
diff --git a/.claude/skills/studyu-design/preview/colors-semantic.html b/.claude/skills/studyu-design/preview/colors-semantic.html
new file mode 100644
index 000000000..4b4257067
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/colors-semantic.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+Surfaces & Background
+
+
Page bg
+
Surface
+
Surface variant
+
Surface high
+
Link #00B0FF
+
+Status
+
+
Error
+
Error bg
+
Success
+
Success bg
+
Warning
+
+Text Ramp
+
+
fg1 - 90%
+
fg2 - 80%
+
fg3 - 65%
+
fg4 - 40%
+
Heading
+
+
diff --git a/.claude/skills/studyu-design/preview/components-badges.html b/.claude/skills/studyu-design/preview/components-badges.html
new file mode 100644
index 000000000..1d0921150
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/components-badges.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+Status Badges
+
+ Active
+ Completed
+ Pending
+ Dropout
+ Draft
+
+Phase Chips
+
+ ― Baseline
+ ☕ Willow-Bark tea
+ 🏁 Results available
+
+Timeline Nodes
+
+Progress Bar
+
+
diff --git a/.claude/skills/studyu-design/preview/components-buttons.html b/.claude/skills/studyu-design/preview/components-buttons.html
new file mode 100644
index 000000000..a53b95ef3
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/components-buttons.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+Filled ; Tonal ; Elevated ; Outlined ; Text
+
+
+
+
+
+
+
+
+Secondary / Accent & Danger
+
+
+
+
+
+Icon Buttons & FAB
+
+
+
+
+
+
+
+
diff --git a/.claude/skills/studyu-design/preview/components-cards.html b/.claude/skills/studyu-design/preview/components-cards.html
new file mode 100644
index 000000000..9873c60fe
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/components-cards.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+Study List Card
+
+
+
people
+
Lower back pain
This study helps you find out which treatment is more effective for you.
+
+
+
biotech
+
Leg Cramps
This study helps you find out what reduces the frequency of leg cramps most.
+
+
+Intervention Card
+
+
+
+
Drink a cup of Willow-Bark tea twice a day. 🕐 18:00
+
+
+
+
Apply a dime sized amount of Arnica gel to your lower back for 10 mins. 🕐 18:00
+
+
+Consent Tiles
+
+
manage_searchWhy Consent Is Needed
+
safety_dividerRisks & Benefits
+
storageData Handling & Use
+
gavelParticipant Rights
+
+
diff --git a/.claude/skills/studyu-design/preview/components-inputs.html b/.claude/skills/studyu-design/preview/components-inputs.html
new file mode 100644
index 000000000..3cf619a61
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/components-inputs.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+Text Inputs
+
+Checkboxes & Switches
+
+
diff --git a/.claude/skills/studyu-design/preview/spacing-tokens.html b/.claude/skills/studyu-design/preview/spacing-tokens.html
new file mode 100644
index 000000000..ff10215ab
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/spacing-tokens.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+Spacing Scale
+
+
+
+
+
+
+
+
+Border Radius
+
+
2px
tooltip
+
5px
input
+
8px
card
+
pill
badge
+
+
+Elevation / Shadow
+
+
none
+
sm
+
md
+
appbar
+
+
diff --git a/.claude/skills/studyu-design/preview/type-scale.html b/.claude/skills/studyu-design/preview/type-scale.html
new file mode 100644
index 000000000..d6714ebf5
--- /dev/null
+++ b/.claude/skills/studyu-design/preview/type-scale.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+ Display Large
+ StudyU
+ 57sp ; 400 (app: 48px)
+
+
+ Display Medium
+ Your Health Journey
+ 45sp ; 400 (app: 36px)
+
+
+ Display Small
+ Please select a study.
+ 36sp ; 400 (app: 26px)
+
+
+ Headline Large
+ Lower back pain
+ 32sp ; 400 (app: 22px)
+
+
+ Headline Medium
+ Today's tasks
+ 28sp ; 400 (app: 18px)
+
+
+ Headline Small
+ Current intervention
+ 24sp ; 400 (app: 15px)
+
+
+ Title Large
+ Willow-Bark tea
+ 22sp ; medium
+
+
+ Body Medium
+ This study helps you find out which treatment is more effective for you.
+ 14sp ; regular
+
+
+ Label Large
+ Get started
+ 14sp ; medium
+
+
+ Label Medium
+ Active ; 24 participants
+ 12sp ; medium
+
+
+ Bold (700)
+ Lower back pain
+ Headlines ; bold
+
+
diff --git a/.claude/skills/studyu-design/references/design-system.md b/.claude/skills/studyu-design/references/design-system.md
new file mode 100644
index 000000000..35e3bdc97
--- /dev/null
+++ b/.claude/skills/studyu-design/references/design-system.md
@@ -0,0 +1,154 @@
+# StudyU Design System
+
+## Overview
+
+**StudyU** is a fully-functional platform for personalized N-of-1 treatment advice. It enables researchers to design clinical studies and patients to participate in them - tracking interventions, logging outcomes, and receiving personalized recommendations based on their own data.
+
+### Products / Surfaces
+1. **Mobile App** (`app/`) - Flutter mobile app for study participants (iOS & Android). Patients browse studies, enroll, complete daily tasks, and track their journey.
+2. **Designer / Dashboard** (`designer_v2/`) - Flutter Web app for researchers to design, monitor, and analyze studies.
+3. **Marketing Site** (`studyu.health`) - Docusaurus-based documentation + marketing site.
+4. **Email** - Transactional notifications (not yet fully templated in repo).
+
+### Sources
+- GitHub: https://github.com/hpi-studyu/studyu (branch: `dev`)
+- Website: https://studyu.health/
+- Logo: provided as PNG (assets/studyulogo.png)
+
+---
+
+## CONTENT FUNDAMENTALS
+
+- **Voice**: Clear, empathetic, science-grounded. Speaks to both researchers (precise, technical) and patients (friendly, encouraging).
+- **Tone**: Calm and trustworthy. Never alarmist. Avoids jargon where possible in patient-facing copy.
+- **Person**: Second person ("you", "your") - e.g. "Your Journey", "Today's tasks".
+- **Casing**: Title Case for screen headings ("Your Journey", "Please select a study."); Sentence case for body/supporting copy.
+- **Emoji**: Not used anywhere in the UI - icon-first approach.
+- **CTAs**: Action-oriented, imperative - "Get started", "Accept", "Decline", "Next", "Back".
+- **Link style**: Blue underlined inline links, used for contextual help ("Why?").
+- **Numbers/dates**: DD-MM-YYYY format used in journey timeline.
+
+---
+
+## VISUAL FOUNDATIONS
+
+### Colors
+- **Primary**: `#2196F3` (Material Blue 500) - app bars, interactive elements, CTAs
+- **Accent / Secondary**: `#FF9800` (Material Orange 500) - active state indicators, checkboxes, current-step circles
+- **Logo gradient**: Cyan `#00E5CC` to Blue `#2196F3` (left to right)
+- **Link**: `#00B0FF` (light blue)
+- **Surface**: `#FFFFFF`
+- **Background**: very light blue tint (`primaryContainer` at about 15% opacity, approximately `#EBF4FD`)
+- **Error**: Material Red `#F44336`
+- **On-surface text**: near-black at 90% opacity; secondary text at 65-80%
+
+### Typography
+- **Primary font**: Work Sans variable (`fonts/WorkSans-VariableFont_wght.ttf`), loaded locally with `@font-face`.
+- **Scale** (from designer_v2 theme): 14px body, 15px titleLarge (bold), 18px headlineSmall (bold), 22px headlineMedium (bold), 26px displaySmall (bold), 36px displayMedium (bold), 48px displayLarge (bold)
+- **Line height**: 1.35 for body text
+- **Heading color**: `onSurfaceVariant` (muted dark, not pure black)
+
+### Spacing & Layout
+- Min content width: 600px; Max: 1264px (designer)
+- Border radius: 8px (cards, list tiles, buttons); 5px (inputs); 2px (tooltips)
+- Content padding: 16px (inputs), 14px (dropdown inputs)
+
+### Cards
+- Elevation: 0 (flat, border-only or background-differentiated)
+- Shape: 8px rounded rectangle
+- Clip: antiAlias
+- Background: white or very light blue
+
+### Backgrounds
+- App: near-white light blue tint
+- Designer: `primaryContainer` at 15% opacity (subtle wash)
+- No full-bleed images, no repeating patterns, no gradients in UI (gradient only in logo)
+
+### Animation
+- Web (designer): fade in/out transitions (opacity ease-in, both old and new routes)
+- Mobile (app): platform-native (Cupertino on iOS, FadeUpwards on Android)
+- No bounces, no spring animations in web
+
+### Hover / Interaction States
+- Hover: 70% opacity fade (`kHoverFadeFactor = 0.7`)
+- Mute: 80% opacity (`kMuteFadeFactor = 0.8`)
+- Press: platform ripple (Material)
+
+### Borders & Dividers
+- Dividers: 0.5px, `onPrimaryContainer` at 15% opacity (very subtle)
+- Input borders: outlined, `surfaceContainerHighest` at 80% opacity; primary color when focused
+- Checkbox border: secondary at 20% opacity when unselected
+
+### Shadows
+- App bar: elevation 2, primaryContainer shadow at 30% opacity
+- Tooltip: layered shadows (primaryContainer + secondary)
+- Cards: no shadow (elevation 0)
+
+### Icons
+- System: **Material Icons** (filled style)
+- Size: 17px in UI; larger (48-64px) for consent/info grid tiles
+- Opacity: 80% default
+- Color: onSurface (dark) or primary blue for interactive
+
+### Corner Radii
+- Cards / list tiles / buttons: 8px
+- Inputs: 5px
+- Tooltips: 2px
+
+---
+
+## ICONOGRAPHY
+
+Material Icons (filled) are the primary icon system, used throughout both the app and designer. They are loaded via CDN in web contexts. The app uses Flutter's built-in Material Icons font.
+
+No custom icon font or SVG sprite exists - all icons are standard Material Icons references.
+Emoji are **never** used as icons.
+Unicode characters are not used as icons.
+
+Key icon usage patterns:
+- Navigation/action icons in app bars (chart icon for analytics, person icon for account)
+- Category icons in study lists (stethoscope, stomach, etc. - sourced from Material Icons)
+- Consent tile icons (search, barrier, database, clock, gavel, binoculars)
+- Task-type icons (checklist for "Rate your day")
+- Timeline node icons (flag for results, minus for baseline)
+
+## MATERIAL DESIGN 3
+
+StudyU uses **Material Design 3** throughout both the mobile app and the designer dashboard. Key implementation details:
+
+- **Color system**: `material_color_utilities` package - `SchemeFidelity` + `Blend.harmonize()` generates the full M3 tonal palette from seed color `#2196F3` (app) / `#0061A4` (designer M3 primary)
+- **Type scale**: M3 naming (`displayLarge` to `labelSmall`). The app uses a compressed custom scale (14-48px); the M3 standard scale (12-57sp) is documented in CSS vars
+- **Shape system**: M3 shape tokens (`extraSmall: 4dp` to `extraLarge: 28dp`) - the app uses `small (8dp)` for cards/list tiles. Inputs use a custom `5px` not from the M3 spec
+- **Elevation**: M3 surface tint model (no drop shadows on cards). App bar uses a light shadow as an exception (`designer_v2` keeps `elevation: 2` on AppBar)
+- **State layers**: M3 spec (`hover: 8%`, `focus: 12%`, `press: 12%`, `drag: 16%` opacity over role color)
+- **Components**: `CardTheme` with `elevation: 0`, `RoundedRectangleBorder(8dp)`. `InputDecorationTheme` filled + outlined. `SwitchTheme`, `CheckboxTheme`, `RadioTheme` all wired to `ColorScheme.primary`
+- **Transitions**: Web uses fade-in/fade-out; mobile uses platform-native (Cupertino on iOS)
+- **Icons**: Material Symbols / Material Icons (filled) - referenced by name in Flutter, loaded via CDN in web
+
+---
+
+## FILE INDEX
+
+```
+references/design-system.md - This file
+SKILL.md - Skill trigger and workflow
+copilot.instructions.md - Shared Copilot custom instructions
+colors_and_type.css - CSS variables: colors, type, spacing
+assets/
+ studyulogo.png - Official StudyU logo (PNG)
+fonts/
+ WorkSans-VariableFont_wght.ttf - Work Sans variable font, weights 100-900
+preview/
+ colors-primary.html - Primary & accent palette
+ colors-semantic.html - Semantic colors (surface, bg, error, text)
+ type-scale.html - Typography scale specimen
+ spacing-tokens.html - Spacing, radius, elevation tokens
+ components-buttons.html - Button variants
+ components-inputs.html - Form inputs
+ components-cards.html - Card & list tile patterns
+ components-badges.html - Status badges & chips
+ brand-logo.html - Logo & gradient usage
+ui_kits/
+ app/index.html - Mobile app UI kit (participant)
+ designer/index.html - Designer/dashboard UI kit (researcher)
+```
diff --git a/.claude/skills/studyu-design/ui_kits/app/index.html b/.claude/skills/studyu-design/ui_kits/app/index.html
new file mode 100644
index 000000000..22c3b1897
--- /dev/null
+++ b/.claude/skills/studyu-design/ui_kits/app/index.html
@@ -0,0 +1,416 @@
+
+
+
+
+
+StudyU - Mobile App UI Kit
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.claude/skills/studyu-design/ui_kits/designer/index.html b/.claude/skills/studyu-design/ui_kits/designer/index.html
new file mode 100644
index 000000000..a0b058028
--- /dev/null
+++ b/.claude/skills/studyu-design/ui_kits/designer/index.html
@@ -0,0 +1,514 @@
+
+
+
+
+
+StudyU - Designer UI Kit
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 120000
index 000000000..cb23bccf4
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1 @@
+../.claude/skills/studyu-design/copilot.instructions.md
\ No newline at end of file
diff --git a/.github/instructions/studyu-design.instructions.md b/.github/instructions/studyu-design.instructions.md
new file mode 120000
index 000000000..388f3418a
--- /dev/null
+++ b/.github/instructions/studyu-design.instructions.md
@@ -0,0 +1 @@
+../../.claude/skills/studyu-design/copilot.instructions.md
\ No newline at end of file
diff --git a/app/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf b/app/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf
new file mode 100644
index 000000000..9c57fbdb0
Binary files /dev/null and b/app/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf differ
diff --git a/app/lib/l10n/app_localizations_de.dart b/app/lib/l10n/app_localizations_de.dart
index b058323e0..7b1e9da25 100644
--- a/app/lib/l10n/app_localizations_de.dart
+++ b/app/lib/l10n/app_localizations_de.dart
@@ -1,6 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
-
import 'app_localizations.dart';
// ignore_for_file: type=lint
diff --git a/app/lib/l10n/app_localizations_en.dart b/app/lib/l10n/app_localizations_en.dart
index ab65f3ba8..da0511e6e 100644
--- a/app/lib/l10n/app_localizations_en.dart
+++ b/app/lib/l10n/app_localizations_en.dart
@@ -1,6 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
-
import 'app_localizations.dart';
// ignore_for_file: type=lint
diff --git a/app/lib/routes.dart b/app/lib/routes.dart
index 547227a84..d1ce4456a 100644
--- a/app/lib/routes.dart
+++ b/app/lib/routes.dart
@@ -19,6 +19,7 @@ import 'package:studyu_app/screens/study/onboarding/kickoff.dart';
import 'package:studyu_app/screens/study/onboarding/study_overview.dart';
import 'package:studyu_app/screens/study/onboarding/study_selection.dart';
import 'package:studyu_app/screens/study/report/report_history.dart';
+import 'package:studyu_app/spacing.dart';
class Routes {
static const String loading = '/loading';
@@ -51,7 +52,7 @@ class Routes {
body: SafeArea(
child: Center(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Text(
'No route defined for ${settings.name}.\nThe developers should fix this 👩💻',
),
diff --git a/app/lib/screens/app_onboarding/about.dart b/app/lib/screens/app_onboarding/about.dart
index 4cd1bd442..a3e9362c5 100644
--- a/app/lib/screens/app_onboarding/about.dart
+++ b/app/lib/screens/app_onboarding/about.dart
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
class AboutScreen extends StatelessWidget {
const AboutScreen({super.key});
@@ -16,12 +17,12 @@ class AboutScreen extends StatelessWidget {
scrollDirection: Axis.vertical,
children: [
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -43,7 +44,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 100),
+ const SizedBox(height: StudyUSpacing.space16),
Text(
AppLocalizations.of(context)!.description_part1,
textAlign: TextAlign.center,
@@ -53,7 +54,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -66,12 +67,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -83,7 +84,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 100),
+ const SizedBox(height: StudyUSpacing.space16),
Text(
AppLocalizations.of(context)!.description_part2,
textAlign: TextAlign.center,
@@ -93,7 +94,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -106,12 +107,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -123,7 +124,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part3,
textAlign: TextAlign.justify,
@@ -133,7 +134,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -146,12 +147,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -163,7 +164,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part4,
textAlign: TextAlign.justify,
@@ -173,7 +174,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -186,12 +187,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -217,7 +218,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part5,
textAlign: TextAlign.justify,
@@ -227,7 +228,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -240,12 +241,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -257,7 +258,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part6,
textAlign: TextAlign.justify,
@@ -267,7 +268,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -280,12 +281,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -297,7 +298,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part7,
textAlign: TextAlign.justify,
@@ -307,7 +308,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -320,12 +321,12 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Row(
children: [
Expanded(
@@ -337,7 +338,7 @@ class AboutScreen extends StatelessWidget {
),
],
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part8,
textAlign: TextAlign.justify,
@@ -347,7 +348,7 @@ class AboutScreen extends StatelessWidget {
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(
- padding: EdgeInsets.only(bottom: 10),
+ padding: EdgeInsets.only(bottom: StudyUSpacing.space3),
child: Icon(
Icons.arrow_drop_down,
color: Colors.blue,
@@ -360,7 +361,7 @@ class AboutScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
@@ -369,13 +370,13 @@ class AboutScreen extends StatelessWidget {
image: AssetImage('assets/icon/logo.png'),
height: 200,
),
- const SizedBox(height: 50),
+ const SizedBox(height: StudyUSpacing.space12),
Text(
AppLocalizations.of(context)!.description_part9,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
- const SizedBox(height: 40),
+ const SizedBox(height: StudyUSpacing.space10),
if (context.read().activeSubject == null)
OutlinedButton.icon(
icon: Icon(MdiIcons.rocket),
diff --git a/app/lib/screens/app_onboarding/app_error_screen.dart b/app/lib/screens/app_onboarding/app_error_screen.dart
index 09eb0df61..c97363604 100644
--- a/app/lib/screens/app_onboarding/app_error_screen.dart
+++ b/app/lib/screens/app_onboarding/app_error_screen.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/cache.dart';
import 'package:studyu_app/util/schedule_notifications.dart';
import 'package:studyu_core/core.dart';
@@ -50,51 +51,51 @@ class _AppErrorScreenState extends State {
body: SafeArea(
child: SingleChildScrollView(
child: Padding(
- padding: const EdgeInsets.all(16.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
children: [
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
const Image(
image: AssetImage('assets/icon/logo.png'),
height: 200,
),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
Text(
loc.loading_error_title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
Text(
loc.loading_error_description,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
- const SizedBox(height: 24),
+ const SizedBox(height: StudyUSpacing.space6),
// Debug information section
Card(
child: Padding(
- padding: const EdgeInsets.all(16.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(MdiIcons.informationOutline),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Text(
'Debug Information',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
- const SizedBox(height: 12),
+ const SizedBox(height: StudyUSpacing.space3),
if (isLoadingData)
const Center(child: CircularProgressIndicator())
else
Container(
width: double.infinity,
- padding: const EdgeInsets.all(12),
+ padding: const EdgeInsets.all(StudyUSpacing.space3),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
@@ -112,7 +113,7 @@ class _AppErrorScreenState extends State {
),
),
),
- const SizedBox(height: 24),
+ const SizedBox(height: StudyUSpacing.space6),
// Action buttons
Row(
children: [
@@ -129,7 +130,7 @@ class _AppErrorScreenState extends State {
label: Text(loc.contact_support),
),
),
- const SizedBox(width: 16),
+ const SizedBox(width: StudyUSpacing.space4),
Expanded(
child: TextButton.icon(
icon: Icon(MdiIcons.deleteOutline),
@@ -142,7 +143,7 @@ class _AppErrorScreenState extends State {
),
],
),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
],
),
),
diff --git a/app/lib/screens/app_onboarding/app_outdated_screen.dart b/app/lib/screens/app_onboarding/app_outdated_screen.dart
index 17b2b496a..f59753629 100644
--- a/app/lib/screens/app_onboarding/app_outdated_screen.dart
+++ b/app/lib/screens/app_onboarding/app_outdated_screen.dart
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/env.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -33,9 +34,9 @@ class AppOutdatedScreen extends StatelessWidget {
image: AssetImage('assets/icon/logo.png'),
height: 200,
),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
Padding(
- padding: const EdgeInsets.all(20),
+ padding: const EdgeInsets.all(StudyUSpacing.space5),
child: Text(
loc.app_outdated_message,
textAlign: TextAlign.center,
diff --git a/app/lib/screens/app_onboarding/onboarding_screen.dart b/app/lib/screens/app_onboarding/onboarding_screen.dart
index 3e688340d..9a96699df 100644
--- a/app/lib/screens/app_onboarding/onboarding_screen.dart
+++ b/app/lib/screens/app_onboarding/onboarding_screen.dart
@@ -3,6 +3,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:introduction_screen/introduction_screen.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
class OnboardingScreen extends StatelessWidget {
@@ -71,8 +72,8 @@ class OnboardingScreen extends StatelessWidget {
decoration: const PageDecoration(
titleTextStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
bodyTextStyle: TextStyle(fontSize: 16),
- imagePadding: EdgeInsets.only(top: 40),
- contentMargin: EdgeInsets.symmetric(horizontal: 16),
+ imagePadding: EdgeInsets.only(top: StudyUSpacing.space10),
+ contentMargin: EdgeInsets.symmetric(horizontal: StudyUSpacing.space4),
),
);
}
diff --git a/app/lib/screens/app_onboarding/terms.dart b/app/lib/screens/app_onboarding/terms.dart
index bbd017f12..af21bdcca 100644
--- a/app/lib/screens/app_onboarding/terms.dart
+++ b/app/lib/screens/app_onboarding/terms.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
@@ -54,7 +55,7 @@ class _TermsScreenState extends State {
final appLocale = Localizations.localeOf(context);
return SingleChildScrollView(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -68,7 +69,7 @@ class _TermsScreenState extends State {
pdfUrl: appConfig!.appTerms[appLocale.languageCode],
pdfUrlLabel: AppLocalizations.of(context)!.terms_read,
),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
LegalSection(
title: AppLocalizations.of(context)!.privacy,
description: AppLocalizations.of(context)!.privacy_content,
@@ -79,7 +80,7 @@ class _TermsScreenState extends State {
pdfUrl: appConfig.appPrivacy[appLocale.languageCode],
pdfUrlLabel: AppLocalizations.of(context)!.privacy_read,
),
- const SizedBox(height: 30),
+ const SizedBox(height: StudyUSpacing.space8),
OutlinedButton.icon(
icon: Icon(MdiIcons.scaleBalance),
onPressed: () async {
@@ -132,9 +133,9 @@ class LegalSection extends StatelessWidget {
color: theme.primaryColor,
),
),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
Text(description!),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
OutlinedButton.icon(
icon: icon,
onPressed: () async {
diff --git a/app/lib/screens/app_onboarding/welcome.dart b/app/lib/screens/app_onboarding/welcome.dart
index a072057e2..892816612 100644
--- a/app/lib/screens/app_onboarding/welcome.dart
+++ b/app/lib/screens/app_onboarding/welcome.dart
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
-import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/debug_screen.dart';
+import 'package:studyu_app/widgets/welcome_button.dart';
class WelcomeScreen extends StatelessWidget {
const WelcomeScreen({super.key});
@@ -11,58 +12,48 @@ class WelcomeScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
- child: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- const Spacer(),
- GestureDetector(
- onDoubleTap: () {
- DebugScreen.showDebugScreen(context);
- },
- child: const Image(
- image: AssetImage('assets/icon/logo.png'),
- height: 200,
+ child: Padding(
+ padding: const EdgeInsets.all(StudyUSpacing.space8),
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ GestureDetector(
+ onDoubleTap: () {
+ DebugScreen.showDebugScreen(context);
+ },
+ child: const Image(
+ image: AssetImage('assets/icon/logo.png'),
+ height: 70,
+ ),
),
- ),
- const SizedBox(height: 20),
- OutlinedButton.icon(
- icon: const Icon(Icons.info),
- onPressed: () => Navigator.pushNamed(context, Routes.about),
- label: Text(
- AppLocalizations.of(context)!.what_is_studyu,
- style: const TextStyle(fontSize: 20),
+ const SizedBox(height: StudyUSpacing.space5),
+ WelcomeButton(
+ icon: Icons.info,
+ label: AppLocalizations.of(context)!.what_is_studyu,
+ onPressed: () => Navigator.pushNamed(context, Routes.about),
),
- ),
- const SizedBox(height: 20),
- OutlinedButton.icon(
- icon: Icon(MdiIcons.accountBox),
- onPressed: () => Navigator.pushNamed(context, Routes.contact),
- label: Text(
- AppLocalizations.of(context)!.contact,
- style: const TextStyle(fontSize: 20),
+ const SizedBox(height: StudyUSpacing.space5),
+ WelcomeButton(
+ icon: Icons.person,
+ label: AppLocalizations.of(context)!.contact,
+ onPressed: () => Navigator.pushNamed(context, Routes.contact),
),
- ),
- const SizedBox(height: 20),
- OutlinedButton.icon(
- icon: Icon(MdiIcons.frequentlyAskedQuestions),
- onPressed: () => Navigator.pushNamed(context, Routes.faq),
- label: Text(
- AppLocalizations.of(context)!.faq,
- style: const TextStyle(fontSize: 20),
+ const SizedBox(height: StudyUSpacing.space5),
+ WelcomeButton(
+ icon: Icons.quiz,
+ label: AppLocalizations.of(context)!.faq,
+ onPressed: () => Navigator.pushNamed(context, Routes.faq),
),
- ),
- const Spacer(),
- OutlinedButton.icon(
- icon: Icon(MdiIcons.rocket, size: 30),
- onPressed: () => Navigator.pushNamed(context, Routes.terms),
- label: Text(
- AppLocalizations.of(context)!.get_started,
- style: const TextStyle(fontSize: 20),
+ const SizedBox(height: StudyUSpacing.space5),
+ WelcomeButton(
+ icon: Icons.rocket_launch,
+ label: AppLocalizations.of(context)!.get_started,
+ onPressed: () => Navigator.pushNamed(context, Routes.terms),
+ isPrimary: true,
),
- ),
- const Spacer(),
- ],
+ ],
+ ),
),
),
),
diff --git a/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart b/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart
index 9136eaf40..d9fba0823 100644
--- a/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart
+++ b/app/lib/screens/study/dashboard/contact_tab/contact_screen.dart
@@ -3,6 +3,7 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart
import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -29,32 +30,49 @@ class _ContactScreenState extends State {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.contact)),
body: ListView(
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
children: [
- Container(
- alignment: Alignment.topCenter,
- child: const Image(
- image: AssetImage('assets/icon/logo.png'),
- height: 80,
+ const Card(
+ child: Padding(
+ padding: EdgeInsets.all(StudyUSpacing.space6),
+ child: Image(
+ image: AssetImage('assets/icon/logo.png'),
+ height: 80,
+ ),
),
),
+ const SizedBox(height: StudyUSpacing.space4),
RetryFutureBuilder(
tryFunction: AppConfig.getAppContact,
- successBuilder:
- (BuildContext context, Contact? appSupportContact) =>
- ContactWidget(
- contact: appSupportContact,
- title: AppLocalizations.of(context)!.app_support,
- subtitle: AppLocalizations.of(context)!.app_support_text,
- color: theme.primaryColor,
- ),
- ),
- const SizedBox(height: 20),
- ContactWidget(
- contact: studyContact,
- title: AppLocalizations.of(context)!.study_support,
- subtitle: AppLocalizations.of(context)!.study_support_text,
- color: theme.colorScheme.secondary,
+ successBuilder: (BuildContext context, Contact? appSupportContact) {
+ if (appSupportContact == null) return const SizedBox.shrink();
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
+ child: ContactWidget(
+ contact: appSupportContact,
+ title: AppLocalizations.of(context)!.app_support,
+ subtitle: AppLocalizations.of(context)!.app_support_text,
+ color: theme.colorScheme.onSurface,
+ ),
+ ),
+ );
+ },
),
+ if (studyContact != null) ...[
+ const SizedBox(height: StudyUSpacing.space4),
+ Card(
+ child: Padding(
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
+ child: ContactWidget(
+ contact: studyContact,
+ title: AppLocalizations.of(context)!.study_support,
+ subtitle: AppLocalizations.of(context)!.study_support_text,
+ color: theme.colorScheme.onSurface,
+ ),
+ ),
+ ),
+ ],
],
),
);
@@ -79,76 +97,96 @@ class ContactWidget extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (contact == null) {
- return Container();
+ return const SizedBox.shrink();
}
- final titles = [
+ final children = [
Text(title, style: theme.textTheme.titleLarge!.copyWith(color: color)),
+ if (subtitle != null && subtitle!.isNotEmpty)
+ Padding(
+ padding: const EdgeInsets.only(top: StudyUSpacing.space1),
+ child: Text(
+ subtitle!,
+ style: theme.textTheme.bodyMedium!.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ const SizedBox(height: StudyUSpacing.space3),
];
- if (subtitle != null && subtitle!.isNotEmpty) {
- titles.add(
- Text(
- subtitle!,
- style: theme.textTheme.titleMedium!.copyWith(fontSize: 14),
+
+ final items = [];
+
+ void addItem({
+ required String itemName,
+ required String? itemValue,
+ required IconData iconData,
+ ContactItemType? type,
+ }) {
+ if (itemValue == null || itemValue.isEmpty) return;
+ if (items.isNotEmpty) {
+ items.add(const Divider(height: 1));
+ }
+ items.add(
+ ContactItem(
+ itemName: itemName,
+ itemValue: itemValue,
+ iconData: iconData,
+ type: type,
),
);
}
+ addItem(
+ itemName: AppLocalizations.of(context)!.organization,
+ itemValue: contact?.organization,
+ iconData: MdiIcons.hospitalBuilding,
+ );
+ if (contact?.institutionalReviewBoard != null) {
+ addItem(
+ itemName: AppLocalizations.of(context)!.irb,
+ itemValue:
+ contact!.institutionalReviewBoard! +
+ (contact?.institutionalReviewBoardNumber != null
+ ? ': ${contact?.institutionalReviewBoardNumber}'
+ : ''),
+ iconData: MdiIcons.clipboardCheck,
+ );
+ }
+ addItem(
+ itemName: AppLocalizations.of(context)!.researchers,
+ itemValue: contact?.researchers,
+ iconData: MdiIcons.doctor,
+ );
+ addItem(
+ itemName: AppLocalizations.of(context)!.website,
+ itemValue: contact?.website,
+ iconData: MdiIcons.web,
+ type: ContactItemType.website,
+ );
+ addItem(
+ itemName: AppLocalizations.of(context)!.email,
+ itemValue: contact?.email,
+ iconData: MdiIcons.email,
+ type: ContactItemType.email,
+ );
+ addItem(
+ itemName: AppLocalizations.of(context)!.phone,
+ itemValue: contact?.phone,
+ iconData: MdiIcons.phone,
+ type: ContactItemType.phone,
+ );
+ if (contact?.additionalInfo != null) {
+ addItem(
+ itemName: AppLocalizations.of(context)!.additionalInfo,
+ itemValue: contact!.additionalInfo,
+ iconData: MdiIcons.information,
+ );
+ }
+
return Column(
- children: [
- Column(mainAxisSize: MainAxisSize.min, children: titles),
- ContactItem(
- itemName: AppLocalizations.of(context)!.organization,
- itemValue: contact?.organization,
- iconData: MdiIcons.hospitalBuilding,
- iconColor: color,
- ),
- if (contact?.institutionalReviewBoard != null)
- ContactItem(
- itemName: AppLocalizations.of(context)!.irb,
- itemValue:
- contact!.institutionalReviewBoard! +
- (contact?.institutionalReviewBoardNumber != null
- ? ': ${contact?.institutionalReviewBoardNumber}'
- : ''),
- iconData: MdiIcons.clipboardCheck,
- iconColor: color,
- ),
- ContactItem(
- itemName: AppLocalizations.of(context)!.researchers,
- itemValue: contact?.researchers,
- iconData: MdiIcons.doctor,
- iconColor: color,
- ),
- ContactItem(
- itemName: AppLocalizations.of(context)!.website,
- itemValue: contact?.website,
- iconData: MdiIcons.web,
- type: ContactItemType.website,
- iconColor: color,
- ),
- ContactItem(
- itemName: AppLocalizations.of(context)!.email,
- itemValue: contact?.email,
- iconData: MdiIcons.email,
- type: ContactItemType.email,
- iconColor: color,
- ),
- ContactItem(
- itemName: AppLocalizations.of(context)!.phone,
- itemValue: contact?.phone,
- iconData: MdiIcons.phone,
- type: ContactItemType.phone,
- iconColor: color,
- ),
- if (contact?.additionalInfo != null)
- ContactItem(
- itemName: AppLocalizations.of(context)!.additionalInfo,
- itemValue: contact!.additionalInfo,
- iconData: MdiIcons.information,
- iconColor: color,
- ),
- ],
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [...children, ...items],
);
}
}
@@ -172,45 +210,77 @@ class ContactItem extends StatelessWidget {
});
Future launchContact() async {
- {
- Uri uri;
- switch (type) {
- case ContactItemType.website:
- if (!itemValue!.startsWith('http://') &&
- !itemValue!.startsWith('https://')) {
- uri = Uri.parse('http://$itemValue');
- } else {
- uri = Uri.parse(itemValue!);
- }
- case ContactItemType.email:
- uri = Uri.parse('mailto:$itemValue');
- case ContactItemType.phone:
- uri = Uri.parse('tel:$itemValue');
- default:
+ Uri uri;
+ switch (type) {
+ case ContactItemType.website:
+ if (!itemValue!.startsWith('http://') &&
+ !itemValue!.startsWith('https://')) {
+ uri = Uri.parse('http://$itemValue');
+ } else {
uri = Uri.parse(itemValue!);
- }
- if (await canLaunchUrl(uri)) {
- launchUrl(uri);
- } else {
- StudyULogger.warning("Cannot launch Url: $uri");
- }
+ }
+ case ContactItemType.email:
+ uri = Uri.parse('mailto:$itemValue');
+ case ContactItemType.phone:
+ uri = Uri.parse('tel:$itemValue');
+ default:
+ uri = Uri.parse(itemValue!);
+ }
+ if (await canLaunchUrl(uri)) {
+ launchUrl(uri);
+ } else {
+ StudyULogger.warning("Cannot launch Url: $uri");
}
}
@override
Widget build(BuildContext context) {
- if (itemValue == null || itemValue!.isEmpty) return Container();
-
- const iconSize = 40.0;
- return ListTile(
- title: Text(itemName),
- subtitle: SelectableText(itemValue!),
- leading: Icon(
- iconData,
- color: iconColor ?? Theme.of(context).primaryColor,
- size: iconSize,
- ),
+ if (itemValue == null || itemValue!.isEmpty) return const SizedBox.shrink();
+
+ final theme = Theme.of(context);
+ return InkWell(
onTap: type != null ? launchContact : null,
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: StudyUSpacing.space3),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(
+ iconData,
+ color: iconColor ?? theme.colorScheme.onSurfaceVariant,
+ size: 24,
+ ),
+ const SizedBox(width: StudyUSpacing.space4),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ itemName,
+ style: theme.textTheme.labelMedium?.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const SizedBox(height: StudyUSpacing.space1),
+ SelectableText(
+ itemValue!,
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: theme.colorScheme.onSurface,
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (type != null)
+ Icon(
+ Icons.chevron_right,
+ color: theme.colorScheme.outline,
+ size: 20,
+ ),
+ ],
+ ),
+ ),
);
}
}
diff --git a/app/lib/screens/study/dashboard/contact_tab/faq.dart b/app/lib/screens/study/dashboard/contact_tab/faq.dart
index 2a28153f1..8b6e7f871 100644
--- a/app/lib/screens/study/dashboard/contact_tab/faq.dart
+++ b/app/lib/screens/study/dashboard/contact_tab/faq.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
class FAQ extends StatelessWidget {
const FAQ({super.key});
@@ -7,26 +8,22 @@ class FAQ extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO(Manisha): Transfer strings to translation files
- if (AppLocalizations.of(context)!.faq_full ==
- 'Frequently Asked Questions') {
- return Scaffold(
- appBar: AppBar(title: Text(AppLocalizations.of(context)!.faq_full)),
- body: ListView.builder(
- padding: const EdgeInsets.all(20),
- itemBuilder: (context, index) => EntryItem(data_en[index]),
- itemCount: data_en.length,
- ),
- );
- } else {
- return Scaffold(
- appBar: AppBar(title: Text(AppLocalizations.of(context)!.faq_full)),
- body: ListView.builder(
- padding: const EdgeInsets.all(20),
- itemBuilder: (context, index) => EntryItem(data_de[index]),
- itemCount: data_de.length,
- ),
- );
- }
+ final isEnglish =
+ AppLocalizations.of(context)!.faq_full == 'Frequently Asked Questions';
+ return Scaffold(
+ appBar: AppBar(title: Text(AppLocalizations.of(context)!.faq_full)),
+ body: ListView.builder(
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
+ itemBuilder: (context, index) {
+ final entry = isEnglish ? data_en[index] : data_de[index];
+ return Padding(
+ padding: const EdgeInsets.only(bottom: StudyUSpacing.space3),
+ child: Card(child: EntryItem(entry)),
+ );
+ },
+ itemCount: isEnglish ? data_en.length : data_de.length,
+ ),
+ );
}
}
@@ -203,17 +200,48 @@ class EntryItem extends StatelessWidget {
final Entry entry;
- Widget _buildTiles(Entry root) {
- if (root.children.isEmpty) return ListTile(title: Text(root.title));
- return ExpansionTile(
- key: PageStorageKey(root),
- title: Text(root.title),
- children: root.children.map(_buildTiles).toList(),
+ Widget _buildTiles(BuildContext context, Entry root) {
+ if (root.children.isEmpty) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space3,
+ ),
+ child: Text(
+ root.title,
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ height: 1.35,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ );
+ }
+ return Theme(
+ data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
+ child: ExpansionTile(
+ key: PageStorageKey(root),
+ title: Text(
+ root.title,
+ style: Theme.of(context).textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ ),
+ collapsedIconColor: Theme.of(context).colorScheme.onSurfaceVariant,
+ iconColor: Theme.of(context).colorScheme.primary,
+ childrenPadding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ ),
+ expandedCrossAxisAlignment: CrossAxisAlignment.start,
+ children: root.children
+ .map((e) => _buildTiles(context, e))
+ .toList(),
+ ),
);
}
@override
Widget build(BuildContext context) {
- return _buildTiles(entry);
+ return _buildTiles(context, entry);
}
}
diff --git a/app/lib/screens/study/dashboard/dashboard.dart b/app/lib/screens/study/dashboard/dashboard.dart
index 61310805f..7f59fbd83 100644
--- a/app/lib/screens/study/dashboard/dashboard.dart
+++ b/app/lib/screens/study/dashboard/dashboard.dart
@@ -12,6 +12,7 @@ import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/study/dashboard/task_overview_tab/task_overview.dart';
import 'package:studyu_app/screens/study/report/report_details.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/debug_screen.dart';
import 'package:studyu_core/core.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -109,24 +110,24 @@ class _DashboardScreenState extends State
// Removes back button. We currently keep navigation stack to make developing easier
automaticallyImplyLeading: false,
title: Text(AppLocalizations.of(context)!.dashboard),
- forceMaterialTransparency: true,
actions: [
IconButton(
tooltip: AppLocalizations.of(context)!.contact,
- icon: Icon(MdiIcons.faceAgent),
+ icon: const Icon(Icons.account_circle),
onPressed: () {
Navigator.pushNamed(context, Routes.contact);
},
),
IconButton(
tooltip: AppLocalizations.of(context)!.current_report,
- icon: Icon(MdiIcons.chartBar),
+ icon: const Icon(Icons.bar_chart),
onPressed: () => Navigator.push(
context,
ReportDetailsScreen.routeFor(subject: subject!),
),
),
PopupMenuButton(
+ icon: const Icon(Icons.more_horiz),
onSelected: (value) {
if (value.routeName != null) {
Navigator.pushNamed(context, value.routeName!);
@@ -197,7 +198,7 @@ class _DashboardScreenState extends State
],
),
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
Column(
children: iconAuthors
.map(
@@ -227,7 +228,7 @@ class _DashboardScreenState extends State
child: Row(
children: [
Icon(choice.icon, color: Colors.black),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Text(choice.name),
],
),
@@ -247,8 +248,8 @@ class _DashboardScreenState extends State
width: double.infinity,
color: Colors.orange.shade100,
padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 8,
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space2,
),
child: Row(
children: [
@@ -257,7 +258,7 @@ class _DashboardScreenState extends State
color: Colors.orange.shade800,
size: 20,
),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Expanded(
child: Text(
AppLocalizations.of(context)!.preview_mode_active,
@@ -280,9 +281,7 @@ class _DashboardScreenState extends State
Expanded(
child: Padding(
padding: showNextDay
- ? EdgeInsets.only(
- bottom: MediaQuery.of(context).size.height / 10,
- )
+ ? const EdgeInsets.only(bottom: StudyUSpacing.space12)
: EdgeInsets.zero,
child: _buildBody(),
),
@@ -291,9 +290,18 @@ class _DashboardScreenState extends State
),
bottomSheet: showNextDay
? Container(
- margin: const EdgeInsets.only(left: 16, bottom: 8),
- child: ElevatedButton.icon(
- icon: const Icon(Icons.fast_forward_rounded),
+ width: double.infinity,
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space2,
+ ),
+ decoration: BoxDecoration(
+ color: const Color(0xFFF5F7FA),
+ border: Border(
+ top: BorderSide(color: Colors.black.withValues(alpha: 0.08)),
+ ),
+ ),
+ child: TextButton(
onPressed: () async {
try {
await subject!.setStartDateBackBy(days: 1);
@@ -302,11 +310,16 @@ class _DashboardScreenState extends State
});
} on SocketException catch (_) {}
},
- label: Text(AppLocalizations.of(context)!.next_day),
- style: ElevatedButton.styleFrom(
- side: const BorderSide(color: Colors.orange, width: 2.0),
- foregroundColor: Theme.of(context).colorScheme.primary,
+ style: TextButton.styleFrom(
+ foregroundColor: const Color(0xFF2196F3),
+ padding: EdgeInsets.zero,
+ alignment: Alignment.centerLeft,
+ textStyle: const TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w700,
+ ),
),
+ child: Text('${AppLocalizations.of(context)!.next_day} ›'),
),
)
: null,
@@ -321,7 +334,7 @@ class _DashboardScreenState extends State
title: Row(
children: [
const Icon(Icons.preview, color: Colors.orange),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Text(AppLocalizations.of(context)!.preview_mode),
],
),
@@ -346,7 +359,12 @@ class _DashboardScreenState extends State
final theme = Theme.of(context);
return Center(
child: Padding(
- padding: const EdgeInsets.fromLTRB(32, 32, 32, 32),
+ padding: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space8,
+ StudyUSpacing.space8,
+ StudyUSpacing.space8,
+ StudyUSpacing.space8,
+ ),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -373,7 +391,7 @@ class _DashboardScreenState extends State
}
class StudyFinishedPlaceholder extends StatelessWidget {
- static const space = SizedBox(height: 80);
+ static const space = SizedBox(height: StudyUSpacing.space16);
const StudyFinishedPlaceholder({super.key});
@@ -382,7 +400,12 @@ class StudyFinishedPlaceholder extends StatelessWidget {
final theme = Theme.of(context);
return SingleChildScrollView(
child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 32, 16, 32),
+ padding: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space4,
+ StudyUSpacing.space8,
+ StudyUSpacing.space4,
+ StudyUSpacing.space8,
+ ),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
diff --git a/app/lib/screens/study/dashboard/settings.dart b/app/lib/screens/study/dashboard/settings.dart
index 262552054..26ed0f097 100644
--- a/app/lib/screens/study/dashboard/settings.dart
+++ b/app/lib/screens/study/dashboard/settings.dart
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/app_analytics.dart';
import 'package:studyu_app/util/fitbit_handler.dart';
import 'package:studyu_app/util/localization.dart';
@@ -53,7 +54,7 @@ class _SettingsState extends State {
mainAxisSize: MainAxisSize.min,
children: [
Text('${AppLocalizations.of(context)!.language}:'),
- const SizedBox(width: 5),
+ const SizedBox(width: StudyUSpacing.space1),
DropdownButton(
value: _selectedValue,
items: dropDownItems,
@@ -73,11 +74,16 @@ class _SettingsState extends State {
Tooltip(
triggerMode: TooltipTriggerMode.tap,
showDuration: const Duration(milliseconds: 10000),
- margin: const EdgeInsets.fromLTRB(30, 0, 30, 0),
+ margin: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space8,
+ 0,
+ StudyUSpacing.space8,
+ 0,
+ ),
message: AppLocalizations.of(context)!.allow_analytics_desc,
child: const Icon(Icons.info),
),
- const SizedBox(width: 5),
+ const SizedBox(width: StudyUSpacing.space1),
Switch(
value: _analyticsValue!,
onChanged: (value) {
@@ -104,12 +110,12 @@ class _SettingsState extends State {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
getDropdownRow(context),
- const SizedBox(height: 24),
+ const SizedBox(height: StudyUSpacing.space6),
Text(
'${AppLocalizations.of(context)!.study_current} ${subject!.study.title}',
style: theme.textTheme.titleLarge,
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
ElevatedButton.icon(
icon: Icon(MdiIcons.exitToApp),
label: Text(AppLocalizations.of(context)!.opt_out),
@@ -123,7 +129,7 @@ class _SettingsState extends State {
);
},
),
- const SizedBox(height: 24),
+ const SizedBox(height: StudyUSpacing.space6),
ElevatedButton.icon(
icon: const Icon(Icons.delete),
label: Text(AppLocalizations.of(context)!.delete_data),
diff --git a/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart b/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart
index 7b90d8e36..869c3e2df 100644
--- a/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart
+++ b/app/lib/screens/study/dashboard/task_overview_tab/progress_row.dart
@@ -1,199 +1,137 @@
-import 'dart:math';
-
import 'package:flutter/material.dart';
-import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/intervention.dart';
-import 'package:studyu_app/widgets/intervention_card.dart';
import 'package:studyu_core/core.dart';
-class ProgressRow extends StatefulWidget {
+class ProgressRow extends StatelessWidget {
final StudySubject? subject;
const ProgressRow({super.key, this.subject});
- @override
- State createState() => _ProgressRowState();
-}
-
-class _ProgressRowState extends State {
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
-
- final currentPhase = widget.subject!.getInterventionIndexForDate(
- DateTime.now(),
- );
+ final currentPhase = subject!.getInterventionIndexForDate(DateTime.now());
+ final interventions = subject!.getInterventionsInOrder();
+ final phaseDuration = subject!.study.schedule.phaseDuration;
+ final phaseDay = DateTime.now()
+ .differenceInDays(subject!.startOfPhase(currentPhase))
+ .clamp(0, phaseDuration - 1);
+ final phaseDayProgress = (phaseDay + 1) / phaseDuration;
- return Padding(
- padding: const EdgeInsets.all(8),
- child: Stack(
- alignment: Alignment.center,
+ return Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space3,
+ vertical: StudyUSpacing.space3,
+ ),
+ child: Row(
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(MdiIcons.run, size: 30),
- const SizedBox(width: 8),
- ...intersperseIndexed(
- (index) => Expanded(
- child: Divider(
- indent: 5,
- endIndent: 5,
- thickness: 3,
- color: currentPhase > index
- ? theme.primaryColor
- : theme.disabledColor,
- ),
- ),
- widget.subject!.getInterventionsInOrder().asMap().entries.map((
- entry,
- ) {
- return InterventionSegment(
- intervention: entry.value,
- isCurrent: currentPhase == entry.key,
- isFuture: currentPhase < entry.key,
- phaseDuration: widget.subject!.study.schedule.phaseDuration,
- percentCompleted: widget.subject!.percentCompletedForPhase(
- entry.key,
- ),
- percentMissed: widget.subject!.percentMissedForPhase(
- entry.key,
- DateTime.now(),
- ),
- );
- }),
- ),
- const SizedBox(width: 8),
- Icon(MdiIcons.flagCheckered, size: 30),
- ],
- ),
+ const Icon(Icons.directions_run, color: Color(0xFF666666), size: 20),
+ const SizedBox(width: StudyUSpacing.space1),
+ ..._buildNodesAndLines(interventions, currentPhase, phaseDayProgress),
+ const SizedBox(width: StudyUSpacing.space1),
+ const Icon(Icons.flag, color: Color(0xFF666666), size: 20),
],
),
);
}
+
+ List _buildNodesAndLines(
+ List interventions,
+ int currentPhase,
+ double phaseDayProgress,
+ ) {
+ final List widgets = [];
+ for (var i = 0; i < interventions.length; i++) {
+ final intervention = interventions[i];
+ final isCurrent = i == currentPhase;
+ final isPast = i < currentPhase;
+
+ widgets.add(
+ _TimelineNode(
+ intervention: intervention,
+ isCurrent: isCurrent,
+ isPast: isPast,
+ phaseDayProgress: phaseDayProgress,
+ ),
+ );
+
+ if (i < interventions.length - 1) {
+ widgets.add(
+ Expanded(
+ child: Container(
+ height: 2,
+ margin: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space1,
+ ),
+ color: isCurrent
+ ? const Color(0xFF2196F3)
+ : const Color(0xFFCCCCCC),
+ ),
+ ),
+ );
+ }
+ }
+ return widgets;
+ }
}
-class InterventionSegment extends StatelessWidget {
+class _TimelineNode extends StatelessWidget {
final Intervention intervention;
- final double percentCompleted;
- final double percentMissed;
final bool isCurrent;
- final bool isFuture;
- final int phaseDuration;
+ final bool isPast;
+ final double phaseDayProgress;
- const InterventionSegment({
+ const _TimelineNode({
required this.intervention,
- required this.percentCompleted,
- required this.percentMissed,
required this.isCurrent,
- required this.isFuture,
- required this.phaseDuration,
- super.key,
+ required this.isPast,
+ required this.phaseDayProgress,
});
- List buildSeparators(int nbSeparators) {
- final sep = [];
- for (var i = 0; i < nbSeparators; i++) {
- sep.add(
- Transform.rotate(
- angle: i * 1 / nbSeparators * 2 * pi,
- child: SizedBox(
- width: 2,
- height: 40,
- child: Column(
- children: [
- Container(width: 8, height: 10, color: Colors.white),
- ],
- ),
- ),
- ),
- );
- }
- return sep;
- }
+ static const _activeColor = Color(0xFFFF9800);
+ static const _pastColor = Color(0xFF9E9E9E);
+ static const _futureColor = Color(0xFFCCCCCC);
+ static const _progressColor = Color(0xFF2196F3);
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
- final color = isFuture
- ? Colors.grey
- : (isCurrent ? theme.colorScheme.secondary : theme.primaryColor);
-
- final emptyColor = Color.alphaBlend(theme.dividerColor, Colors.white);
- final activeColor = Color.alphaBlend(
- theme.colorScheme.secondary,
- Colors.white,
+ final fillColor = isCurrent
+ ? _activeColor
+ : (isPast ? _pastColor : _futureColor);
+ final node = Container(
+ width: 36,
+ height: 36,
+ decoration: BoxDecoration(color: fillColor, shape: BoxShape.circle),
+ child: Center(child: interventionIcon(intervention, color: Colors.white)),
);
- final completedColor = Color.alphaBlend(theme.primaryColor, Colors.white);
- return Expanded(
+ if (!isCurrent) return node;
+
+ return SizedBox.square(
+ dimension: 42,
child: Stack(
alignment: Alignment.center,
children: [
- AspectRatio(
- aspectRatio: 1,
+ SizedBox.square(
+ dimension: 42,
child: CircularProgressIndicator(
value: 1,
- valueColor: AlwaysStoppedAnimation(emptyColor),
- ),
- ),
- if (isCurrent)
- AspectRatio(
- aspectRatio: 1,
- child: CircularProgressIndicator(
- value: percentMissed + percentCompleted + (1 / phaseDuration),
- valueColor: AlwaysStoppedAnimation(activeColor),
- ),
- ),
- AspectRatio(
- aspectRatio: 1,
- child: CircularProgressIndicator(
- value: percentMissed + percentCompleted,
- valueColor: AlwaysStoppedAnimation(emptyColor),
+ strokeWidth: 3,
+ color: _activeColor.withValues(alpha: 0.24),
),
),
- AspectRatio(
- aspectRatio: 1,
+ SizedBox.square(
+ dimension: 42,
child: CircularProgressIndicator(
- value: percentCompleted,
- valueColor: AlwaysStoppedAnimation(completedColor),
- ),
- ),
- Stack(children: buildSeparators(phaseDuration)),
- RawMaterialButton(
- onPressed: () {
- showDialog(
- context: context,
- builder: (context) => AlertDialog(
- contentPadding: EdgeInsets.zero,
- content: InterventionCard(intervention),
- ),
- );
- },
- elevation: 0,
- fillColor: color,
- shape: const CircleBorder(
- side: BorderSide(color: Colors.white, width: 2),
+ value: phaseDayProgress,
+ strokeWidth: 3,
+ strokeCap: StrokeCap.round,
+ color: _progressColor,
),
- child: interventionIcon(intervention),
),
+ node,
],
),
);
}
}
-
-Iterable intersperseIndexed(
- T Function(int) generator,
- Iterable iterable,
-) sync* {
- final iterator = iterable.iterator;
- var index = 0;
- if (iterator.moveNext()) {
- yield iterator.current;
- while (iterator.moveNext()) {
- yield generator(index++);
- yield iterator.current;
- }
- }
-}
diff --git a/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart b/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart
index 6c187080a..edac12c41 100644
--- a/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart
+++ b/app/lib/screens/study/dashboard/task_overview_tab/task_box.dart
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/screens/study/tasks/task_screen.dart';
-import 'package:studyu_app/theme.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/schedule_notifications.dart';
-import 'package:studyu_app/widgets/round_checkbox.dart';
import 'package:studyu_core/core.dart';
class TaskBox extends StatefulWidget {
@@ -54,31 +53,72 @@ class _TaskBoxState extends State {
);
final isTaskOpen = !completed && isInsidePeriod || isPreview || kDebugMode;
return Card(
- elevation: 2,
+ margin: const EdgeInsets.only(
+ top: StudyUSpacing.space2,
+ bottom: StudyUSpacing.space2,
+ ),
+ color: Colors.white,
+ surfaceTintColor: Colors.transparent,
child: InkWell(
onTap: isTaskOpen ? _navigateToTaskScreen : () {},
- child: Row(
- children: [
- Expanded(
- child: ListTile(
- leading: widget.icon,
- title: Text(widget.taskInstance.task.title ?? ''),
- onTap: isTaskOpen ? _navigateToTaskScreen : () {},
- ),
- ),
- if (isInsidePeriod || isPreview || completed)
- RoundCheckbox(
- value: completed, //_isCompleted,
- onChanged: (value) =>
- isTaskOpen ? _navigateToTaskScreen() : () {},
- )
- else
- Padding(
- padding: const EdgeInsets.fromLTRB(0, 0, 8, 0),
- child: Icon(Icons.lock, color: theme.colorScheme.secondary),
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space3,
+ ),
+ child: Row(
+ children: [
+ widget.icon,
+ const SizedBox(width: StudyUSpacing.space3),
+ Expanded(
+ child: Text(
+ widget.taskInstance.task.title ?? '',
+ style: const TextStyle(
+ fontSize: 14,
+ color: Color(0xFF333333),
+ ),
+ ),
),
- ],
+ if (isInsidePeriod || isPreview || completed)
+ _CheckCircle(
+ completed: completed,
+ onTap: isTaskOpen ? _navigateToTaskScreen : null,
+ )
+ else
+ Icon(
+ Icons.lock,
+ color: Theme.of(context).colorScheme.secondary,
+ size: 20,
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _CheckCircle extends StatelessWidget {
+ final bool completed;
+ final VoidCallback? onTap;
+
+ const _CheckCircle({required this.completed, this.onTap});
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ width: 28,
+ height: 28,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ border: Border.all(color: const Color(0xFFFF9800), width: 2.5),
),
+ child: completed
+ ? const Icon(Icons.check, color: Color(0xFFFF9800), size: 18)
+ : const SizedBox.shrink(),
),
);
}
diff --git a/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart b/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart
index a2786d51a..9f6e6c6b5 100644
--- a/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart
+++ b/app/lib/screens/study/dashboard/task_overview_tab/task_overview.dart
@@ -4,8 +4,7 @@ import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/study/dashboard/task_overview_tab/progress_row.dart';
import 'package:studyu_app/screens/study/dashboard/task_overview_tab/task_box.dart';
-import 'package:studyu_app/theme.dart';
-import 'package:studyu_app/widgets/intervention_card.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
class TaskOverview extends StatefulWidget {
@@ -39,19 +38,26 @@ class _TaskOverviewState extends State {
List buildScheduleToday(BuildContext context) {
final theme = Theme.of(context);
final List list = [];
+ if (widget.scheduleToday == null || widget.scheduleToday!.isEmpty) {
+ return list;
+ }
for (final taskInstance in widget.scheduleToday!) {
list
..add(
Padding(
- padding: const EdgeInsets.only(top: 16, left: 16, right: 16),
+ padding: const EdgeInsets.only(
+ top: StudyUSpacing.space2,
+ bottom: StudyUSpacing.space1,
+ ),
child: Row(
children: [
- Icon(Icons.access_time, color: theme.primaryColor),
- const SizedBox(width: 8),
+ Icon(Icons.schedule, color: theme.primaryColor, size: 16),
+ const SizedBox(width: StudyUSpacing.space1),
Text(
taskInstance.completionPeriod.formatted(),
- style: theme.textTheme.titleSmall!.copyWith(
- fontSize: 16,
+ style: TextStyle(
+ fontSize: 14,
+ fontWeight: FontWeight.w700,
color: theme.primaryColor,
),
),
@@ -66,7 +72,9 @@ class _TaskOverviewState extends State {
icon: Icon(
taskInstance.task is Observation
? MdiIcons.orderBoolAscendingVariant
- : MdiIcons.fromString(widget.interventionIcon!),
+ : MdiIcons.fromString(widget.interventionIcon ?? 'help'),
+ color: Colors.black.withValues(alpha: 0.4),
+ size: 20,
),
),
);
@@ -76,51 +84,124 @@ class _TaskOverviewState extends State {
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
+ final currentIntervention = widget.subject!.getInterventionForDate(
+ DateTime.now(),
+ );
+
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const SizedBox(height: 8),
ProgressRow(subject: widget.subject),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ Expanded(
+ child: ListView(
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ ),
children: [
- const SizedBox(height: 16),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Flexible(
- child: Text(
- AppLocalizations.of(context)!.intervention_current,
- style: theme.textTheme.titleLarge,
- ),
- ),
- const SizedBox(width: 5),
- Text(
- '${widget.subject!.daysLeftForPhase(widget.subject!.getInterventionIndexForDate(DateTime.now()))} ${AppLocalizations.of(context)!.days_left}',
- style: const TextStyle(color: primaryColor),
- ),
- ],
- ),
- const SizedBox(height: 8),
- InterventionCardTitle(
- intervention: widget.subject!.getInterventionForDate(
- DateTime.now(),
+ const SizedBox(height: StudyUSpacing.space4),
+ Text(
+ AppLocalizations.of(context)!.intervention_current,
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF333333),
),
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
+ _buildInterventionCard(currentIntervention),
+ const SizedBox(height: StudyUSpacing.space4),
Text(
AppLocalizations.of(context)!.today_tasks,
- style: theme.textTheme.titleLarge,
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF333333),
+ ),
),
+ const SizedBox(height: StudyUSpacing.space2),
+ ...buildScheduleToday(context),
+ const SizedBox(height: StudyUSpacing.space4),
],
),
),
- // Todo: find good way to calculate duration of intervention and display it
- Expanded(child: ListView(children: [...buildScheduleToday(context)])),
],
);
}
+
+ Widget _buildInterventionCard(Intervention? intervention) {
+ if (intervention == null) {
+ return const SizedBox.shrink();
+ }
+
+ return Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space3,
+ ),
+ child: Row(
+ children: [
+ Container(
+ width: 24,
+ height: 3,
+ decoration: BoxDecoration(
+ color: const Color(0xFFFF9800),
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ const SizedBox(width: StudyUSpacing.space3),
+ Expanded(
+ child: Text(
+ intervention.name ?? '',
+ style: const TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w700,
+ color: Color(0xFF333333),
+ ),
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.info, color: Color(0xFF999999), size: 20),
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ onPressed: () => _showInterventionInfo(intervention),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showInterventionInfo(Intervention intervention) {
+ showDialog(
+ context: context,
+ builder: (context) {
+ final description = intervention.isBaseline()
+ ? AppLocalizations.of(context)!.baseline_description
+ : intervention.description;
+ return AlertDialog(
+ title: ListTile(
+ leading: Icon(
+ MdiIcons.fromString(intervention.icon),
+ color: Theme.of(context).colorScheme.secondary,
+ ),
+ dense: true,
+ title: Text(
+ intervention.name ?? '',
+ style: Theme.of(context).textTheme.titleLarge,
+ ),
+ ),
+ content: SingleChildScrollView(child: Text(description ?? '')),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(AppLocalizations.of(context)!.ok),
+ ),
+ ],
+ );
+ },
+ );
+ }
}
diff --git a/app/lib/screens/study/multimodal/capture_picture_screen.dart b/app/lib/screens/study/multimodal/capture_picture_screen.dart
index 8a025e8c2..a0a234aca 100644
--- a/app/lib/screens/study/multimodal/capture_picture_screen.dart
+++ b/app/lib/screens/study/multimodal/capture_picture_screen.dart
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/temporary_storage_handler.dart';
import 'package:studyu_core/core.dart';
@@ -187,12 +188,12 @@ class _CapturePictureScreenState extends State
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(10),
),
- padding: const EdgeInsets.all(20.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space5),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
Text(AppLocalizations.of(context)!.take_a_photo),
],
),
@@ -205,7 +206,7 @@ class _CapturePictureScreenState extends State
floatingActionButton: Wrap(
children: [
Container(
- margin: const EdgeInsets.all(10),
+ margin: const EdgeInsets.all(StudyUSpacing.space3),
child: FloatingActionButton(
heroTag: "captureImage",
onPressed: cameraController != null && !_isTakingPicture
@@ -217,7 +218,7 @@ class _CapturePictureScreenState extends State
),
),
Container(
- margin: const EdgeInsets.all(10),
+ margin: const EdgeInsets.all(StudyUSpacing.space3),
child: FloatingActionButton(
heroTag: "jumpToNextCamera",
onPressed: cameraController != null && !_isTakingPicture
diff --git a/app/lib/screens/study/onboarding/consent.dart b/app/lib/screens/study/onboarding/consent.dart
index 97fb95520..70ae71607 100644
--- a/app/lib/screens/study/onboarding/consent.dart
+++ b/app/lib/screens/study/onboarding/consent.dart
@@ -9,6 +9,7 @@ import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/save_pdf.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_app/widgets/html_text.dart';
@@ -118,7 +119,7 @@ class _ConsentScreenState extends State {
body: SingleChildScrollView(
child: Center(
child: Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -171,7 +172,7 @@ class _ConsentScreenState extends State {
);
},
primary: false,
- padding: const EdgeInsets.all(20),
+ padding: const EdgeInsets.all(StudyUSpacing.space5),
),
),
],
@@ -237,7 +238,7 @@ class ConsentCard extends StatelessWidget {
else
const SizedBox.shrink(),
if (consent!.iconName.isNotEmpty)
- const SizedBox(width: 8)
+ const SizedBox(width: StudyUSpacing.space2)
else
const SizedBox.shrink(),
Expanded(child: Text(consent!.title!)),
@@ -253,7 +254,7 @@ class ConsentCard extends StatelessWidget {
onTapped(index!);
},
child: Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
@@ -267,7 +268,7 @@ class ConsentCard extends StatelessWidget {
else
const SizedBox.shrink(),
if (consent!.iconName.isNotEmpty)
- const SizedBox(height: 10)
+ const SizedBox(height: StudyUSpacing.space3)
else
const SizedBox.shrink(),
Flexible(
diff --git a/app/lib/screens/study/onboarding/eligibility_screen.dart b/app/lib/screens/study/onboarding/eligibility_screen.dart
index 1a29f9ddb..becec7159 100644
--- a/app/lib/screens/study/onboarding/eligibility_screen.dart
+++ b/app/lib/screens/study/onboarding/eligibility_screen.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_app/widgets/questionnaire/questionnaire_widget.dart';
import 'package:studyu_core/core.dart';
@@ -144,13 +145,13 @@ class _EligibilityScreenState extends State {
AppLocalizations.of(context)!.eligible_no,
style: Theme.of(context).textTheme.titleMedium,
),
- const SizedBox(height: 4),
+ const SizedBox(height: StudyUSpacing.space1),
if (activeResult?.firstFailed?.reason != null)
Text(activeResult!.firstFailed!.reason!)
else
const SizedBox.shrink(),
if (activeResult?.firstFailed?.reason != null)
- const SizedBox(height: 4)
+ const SizedBox(height: StudyUSpacing.space1)
else
const SizedBox.shrink(),
Text(AppLocalizations.of(context)!.eligible_mistake),
@@ -182,7 +183,7 @@ class _EligibilityScreenState extends State {
body: Column(
children: [
Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Text(
AppLocalizations.of(context)!.please_answer_eligibility,
style: theme.textTheme.titleMedium,
diff --git a/app/lib/screens/study/onboarding/intervention_selection.dart b/app/lib/screens/study/onboarding/intervention_selection.dart
index 1340c8675..50b232241 100644
--- a/app/lib/screens/study/onboarding/intervention_selection.dart
+++ b/app/lib/screens/study/onboarding/intervention_selection.dart
@@ -5,6 +5,7 @@ import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_app/widgets/intervention_card.dart';
import 'package:studyu_core/core.dart';
@@ -31,14 +32,14 @@ class _InterventionSelectionScreenState
Widget _buildInterventionSelectionExplanation(ThemeData theme) {
return Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Column(
children: [
Text(
AppLocalizations.of(context)!.please_select_interventions,
style: theme.textTheme.titleMedium,
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
Text(
AppLocalizations.of(
context,
@@ -111,13 +112,13 @@ class _InterventionSelectionScreenState
body: SingleChildScrollView(
child: Center(
child: Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildInterventionSelectionExplanation(theme),
_buildInterventionSelectionList(),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
],
),
),
diff --git a/app/lib/screens/study/onboarding/journey_overview.dart b/app/lib/screens/study/onboarding/journey_overview.dart
index 918ff4928..21f0af206 100644
--- a/app/lib/screens/study/onboarding/journey_overview.dart
+++ b/app/lib/screens/study/onboarding/journey_overview.dart
@@ -6,6 +6,7 @@ import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/study/onboarding/onboarding_progress.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_core/core.dart';
import 'package:timeline_tile/timeline_tile.dart';
@@ -58,7 +59,7 @@ class _JourneyOverviewScreen extends State {
body: Center(
child: SingleChildScrollView(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
children: [
//StudyTile.fromUserStudy(study: study),
@@ -96,7 +97,7 @@ class Timeline extends StatelessWidget {
title: intervention.name,
iconName: intervention.icon,
color: intervention.isBaseline()
- ? Colors.grey
+ ? theme.disabledColor
: theme.colorScheme.secondary,
date: now.add(
Duration(days: index * subject!.study.schedule.phaseDuration),
@@ -145,7 +146,7 @@ class InterventionTile extends StatelessWidget {
indicatorStyle: IndicatorStyle(
width: 40,
height: 40,
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
indicator: IconIndicator(iconName: iconName, color: color),
),
beforeLineStyle: LineStyle(color: theme.primaryColor),
@@ -182,7 +183,10 @@ class IconIndicator extends StatelessWidget {
color: color ?? Theme.of(context).colorScheme.secondary,
),
child: Center(
- child: Icon(MdiIcons.fromString(iconName), color: Colors.white),
+ child: Icon(
+ MdiIcons.fromString(iconName),
+ color: Theme.of(context).colorScheme.onPrimary,
+ ),
),
);
}
@@ -196,7 +200,7 @@ class TimelineChild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
constraints: const BoxConstraints(minHeight: 100),
child: Center(child: child),
);
diff --git a/app/lib/screens/study/onboarding/kickoff.dart b/app/lib/screens/study/onboarding/kickoff.dart
index 308d02661..bc375328d 100644
--- a/app/lib/screens/study/onboarding/kickoff.dart
+++ b/app/lib/screens/study/onboarding/kickoff.dart
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/cache.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
@@ -87,16 +88,16 @@ class _KickoffScreen extends State {
return Center(
child: SingleChildScrollView(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
children: [
_constructStatusIcon(context),
- const SizedBox(height: 32),
+ const SizedBox(height: StudyUSpacing.space8),
Text(
_getStatusText(context),
style: Theme.of(context).textTheme.titleLarge,
),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
/*OutlinedButton(
onPressed: () => _storeUserStudy(context),
child: Text(AppLocalizations.of(context)!.start_study),
diff --git a/app/lib/screens/study/onboarding/onboarding_progress.dart b/app/lib/screens/study/onboarding/onboarding_progress.dart
index 12bfb89fa..7a7901b67 100644
--- a/app/lib/screens/study/onboarding/onboarding_progress.dart
+++ b/app/lib/screens/study/onboarding/onboarding_progress.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:studyu_app/spacing.dart';
class OnboardingProgress extends StatelessWidget {
final int stage;
@@ -26,14 +27,14 @@ class OnboardingProgress extends StatelessWidget {
child: LinearProgressIndicator(value: _getProgressForStage(0)),
),
),
- const SizedBox(width: 4),
+ const SizedBox(width: StudyUSpacing.space1),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(value: _getProgressForStage(1)),
),
),
- const SizedBox(width: 4),
+ const SizedBox(width: StudyUSpacing.space1),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
diff --git a/app/lib/screens/study/onboarding/study_overview.dart b/app/lib/screens/study/onboarding/study_overview.dart
index 7f920653f..b320e76fc 100644
--- a/app/lib/screens/study/onboarding/study_overview.dart
+++ b/app/lib/screens/study/onboarding/study_overview.dart
@@ -6,6 +6,7 @@ import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
import 'package:studyu_app/screens/study/dashboard/contact_tab/contact_screen.dart';
import 'package:studyu_app/screens/study/onboarding/eligibility_screen.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_app/widgets/study_tile.dart';
import 'package:studyu_core/core.dart';
@@ -75,15 +76,18 @@ class _StudyOverviewScreen extends State {
title: Text(AppLocalizations.of(context)!.study_overview_title),
),
body: SingleChildScrollView(
- child: Column(
- children: [
- Hero(
- tag: 'study_tile_${study!.id}',
- child: Material(child: StudyTile.fromStudy(study: study!)),
- ),
- const SizedBox(height: 16),
- StudyDetailsView(study: study),
- ],
+ child: Padding(
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
+ child: Column(
+ children: [
+ Hero(
+ tag: 'study_tile_${study!.id}',
+ child: Material(child: StudyTile.fromStudy(study: study!)),
+ ),
+ const SizedBox(height: StudyUSpacing.space4),
+ StudyDetailsView(study: study),
+ ],
+ ),
),
),
bottomNavigationBar: BottomOnboardingNavigation(
@@ -130,7 +134,7 @@ class StudyDetailsView extends StatelessWidget {
size: iconSize,
),
),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
ContactWidget(
contact: study!.contact,
title: AppLocalizations.of(context)!.study_publisher,
diff --git a/app/lib/screens/study/onboarding/study_selection.dart b/app/lib/screens/study/onboarding/study_selection.dart
index 0f60e8ad7..12cef2dca 100644
--- a/app/lib/screens/study/onboarding/study_selection.dart
+++ b/app/lib/screens/study/onboarding/study_selection.dart
@@ -2,13 +2,14 @@ import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
-import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/bottom_onboarding_navigation.dart';
import 'package:studyu_app/widgets/study_tile.dart';
+import 'package:studyu_app/widgets/welcome_button.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
@@ -79,76 +80,80 @@ class _StudySelectionScreenState extends State {
child: Column(
children: [
Padding(
- padding: const EdgeInsets.all(16),
- child: Column(
- children: [
- Text(
- AppLocalizations.of(context)!.study_selection_description,
- style: theme.textTheme.headlineSmall,
- ),
- const SizedBox(height: 8),
- RichText(
- text: TextSpan(
- children: [
- TextSpan(
- text: AppLocalizations.of(
- context,
- )!.study_selection_single,
- style: theme.textTheme.titleSmall,
- ),
- TextSpan(
- text: ' ',
- style: theme.textTheme.titleSmall,
- ),
- TextSpan(
- text: AppLocalizations.of(
- context,
- )!.study_selection_single_why,
- style: theme.textTheme.titleSmall!.copyWith(
- color: theme.primaryColor,
- ),
- recognizer: TapGestureRecognizer()
- ..onTap = () => showDialog(
- context: context,
- builder: (context) => AlertDialog(
- content: Text(
- AppLocalizations.of(
- context,
- )!.study_selection_single_reason,
- ),
- ),
+ padding: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space6,
+ StudyUSpacing.space6,
+ StudyUSpacing.space6,
+ StudyUSpacing.space2,
+ ),
+ child: Text(
+ AppLocalizations.of(context)!.study_selection_description,
+ style: theme.textTheme.headlineMedium,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space6,
+ 0,
+ StudyUSpacing.space6,
+ StudyUSpacing.space5,
+ ),
+ child: RichText(
+ textAlign: TextAlign.center,
+ text: TextSpan(
+ children: [
+ TextSpan(
+ text: AppLocalizations.of(
+ context,
+ )!.study_selection_single,
+ style: theme.textTheme.titleSmall!.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ const TextSpan(text: ' '),
+ TextSpan(
+ text: AppLocalizations.of(
+ context,
+ )!.study_selection_single_why,
+ style: theme.textTheme.titleSmall!.copyWith(
+ color: theme.primaryColor,
+ decoration: TextDecoration.underline,
+ decorationColor: theme.primaryColor,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () => showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ content: Text(
+ AppLocalizations.of(
+ context,
+ )!.study_selection_single_reason,
),
+ ),
),
- ],
),
- ),
- ],
+ ],
+ ),
),
),
if (_hiddenStudies)
- Column(
- children: [
- MaterialBanner(
- padding: const EdgeInsets.all(8),
- leading: Icon(
- MdiIcons.exclamationThick,
- color: Colors.orange,
- size: 32,
- ),
- content: Text(
- AppLocalizations.of(
- context,
- )!.study_selection_hidden_studies,
- style: Theme.of(context).textTheme.titleSmall,
- ),
- actions: const [SizedBox.shrink()],
- backgroundColor: Colors.yellow[100],
- ),
- const SizedBox(height: 16),
- ],
- )
- else
- const SizedBox.shrink(),
+ MaterialBanner(
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
+ leading: Icon(
+ Icons.warning,
+ color: theme.colorScheme.secondary,
+ size: 32,
+ ),
+ content: Text(
+ AppLocalizations.of(
+ context,
+ )!.study_selection_hidden_studies,
+ style: theme.textTheme.titleSmall,
+ ),
+ actions: const [SizedBox.shrink()],
+ backgroundColor: theme.colorScheme.secondaryContainer,
+ ),
Expanded(
child: RetryFutureBuilder>(
tryFunction: () => publishedStudies,
@@ -171,12 +176,39 @@ class _StudySelectionScreenState extends State {
});
}
return ListView.builder(
- itemCount: studies.length,
+ padding: const EdgeInsets.only(
+ bottom: StudyUSpacing.space2,
+ ),
+ itemCount: studies.length + 1,
itemBuilder: (context, index) {
+ if (index == studies.length) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space3,
+ ),
+ child: Center(
+ child: WelcomeButton(
+ icon: Icons.vpn_key,
+ label: AppLocalizations.of(
+ context,
+ )!.invite_code_button,
+ onPressed: () async {
+ await showDialog(
+ context: context,
+ builder: (_) =>
+ const InviteCodeDialog(),
+ );
+ },
+ ),
+ ),
+ );
+ }
final study = studies[index];
return Hero(
tag: 'study_tile_${studies[index].id}',
child: Material(
+ color: Colors.transparent,
child: StudyTile.fromStudy(
study: study,
onTap: () async {
@@ -193,19 +225,6 @@ class _StudySelectionScreenState extends State {
},
),
),
- Padding(
- padding: const EdgeInsets.all(8),
- child: OutlinedButton.icon(
- icon: Icon(MdiIcons.key),
- onPressed: () async {
- await showDialog(
- context: context,
- builder: (_) => const InviteCodeDialog(),
- );
- },
- label: Text(AppLocalizations.of(context)!.invite_code_button),
- ),
- ),
],
),
),
diff --git a/app/lib/screens/study/report/generic_section.dart b/app/lib/screens/study/report/generic_section.dart
index 966ecb0af..29403ad14 100644
--- a/app/lib/screens/study/report/generic_section.dart
+++ b/app/lib/screens/study/report/generic_section.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
abstract class GenericSection extends StatelessWidget {
@@ -14,7 +15,7 @@ abstract class GenericSection extends StatelessWidget {
child: InkWell(
onTap: onTap,
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: buildContent(context),
),
),
diff --git a/app/lib/screens/study/report/performance/performance_details.dart b/app/lib/screens/study/report/performance/performance_details.dart
index 2c045050c..78c9f5e5f 100644
--- a/app/lib/screens/study/report/performance/performance_details.dart
+++ b/app/lib/screens/study/report/performance/performance_details.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/routes.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/intervention_card.dart';
import 'package:studyu_core/core.dart';
@@ -30,19 +31,19 @@ class PerformanceDetailsScreen extends StatelessWidget {
body: SingleChildScrollView(
child: Center(
child: Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Text(
AppLocalizations.of(context)!.performance_overview,
style: theme.textTheme.titleMedium,
),
),
Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
@@ -65,7 +66,7 @@ class PerformanceDetailsScreen extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
@@ -110,7 +111,7 @@ class InterventionPerformanceBar extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
children: [
InterventionCard(
@@ -118,7 +119,7 @@ class InterventionPerformanceBar extends StatelessWidget {
showTasks: false,
showDescription: false,
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
...intervention.tasks.map(
(task) => PerformanceBar(
task: task,
@@ -147,7 +148,7 @@ class ObservationPerformanceBar extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: PerformanceBar(
task: observation,
completed: subject!.completedTasksFor(observation),
@@ -181,7 +182,7 @@ class PerformanceBar extends StatelessWidget {
Text('$completed/$total'),
],
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
Stack(
alignment: Alignment.center,
children: [
diff --git a/app/lib/screens/study/report/performance/performance_section.dart b/app/lib/screens/study/report/performance/performance_section.dart
index d45d88bb0..67490db79 100644
--- a/app/lib/screens/study/report/performance/performance_section.dart
+++ b/app/lib/screens/study/report/performance/performance_section.dart
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:rainbow_color/rainbow_color.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/screens/study/report/generic_section.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
class PerformanceSection extends GenericSection {
@@ -16,6 +17,7 @@ class PerformanceSection extends GenericSection {
@override
Widget buildContent(BuildContext context) {
+ final theme = Theme.of(context);
final interventions = subject!.selectedInterventions
.where((intervention) => intervention.id != Study.baselineID)
.toList();
@@ -34,10 +36,10 @@ class PerformanceSection extends GenericSection {
: Column(
children: [
Padding(
- padding: const EdgeInsets.only(bottom: 16),
+ padding: const EdgeInsets.only(bottom: StudyUSpacing.space4),
child: Text(
'${AppLocalizations.of(context)!.current_power_level}: ${getPowerLevelDescription(context, interventionProgress)}',
- style: Theme.of(context).textTheme.titleLarge,
+ style: theme.textTheme.titleLarge,
),
),
ListView.builder(
@@ -48,12 +50,16 @@ class PerformanceSection extends GenericSection {
final i = (index / 2).floor();
if (index.isEven) {
return Padding(
- padding: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.only(
+ bottom: StudyUSpacing.space2,
+ ),
child: Text(interventions[i].name!),
);
} else {
return Padding(
- padding: const EdgeInsets.only(bottom: 8),
+ padding: const EdgeInsets.only(
+ bottom: StudyUSpacing.space2,
+ ),
child: PerformanceBar(
progress: interventionProgress[i],
minimum: minimumRatio,
@@ -141,8 +147,9 @@ class PerformanceBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
final rainbow = Rainbow(
- spectrum: [Colors.red, Colors.yellow, Colors.green],
+ spectrum: [colorScheme.error, colorScheme.tertiary, colorScheme.primary],
rangeStart: 0,
rangeEnd: 1,
);
@@ -161,7 +168,7 @@ class PerformanceBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
- height: 20,
+ height: StudyUSpacing.space5,
width: double.infinity,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
@@ -187,7 +194,7 @@ class PerformanceBar extends StatelessWidget {
),
),
),
- Container(width: 2, color: Colors.grey[600]),
+ Container(width: 2, color: colorScheme.onSurfaceVariant),
],
),
],
diff --git a/app/lib/screens/study/report/report_history.dart b/app/lib/screens/study/report/report_history.dart
index 8ca37b196..262a3c372 100644
--- a/app/lib/screens/study/report/report_history.dart
+++ b/app/lib/screens/study/report/report_history.dart
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/screens/study/report/report_details.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
import 'package:studyu_flutter_common/studyu_flutter_common.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
@@ -47,10 +48,11 @@ class ReportHistoryItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
final model = context.watch();
- final isActiveStudy = model.activeSubject!.studyId == subject.studyId;
+ final isActiveStudy = model.activeSubject?.studyId == subject.studyId;
return Card(
- color: isActiveStudy ? Colors.green[600] : theme.cardColor,
+ color: isActiveStudy ? colorScheme.primary : theme.cardColor,
child: InkWell(
onTap: () {
Navigator.push(
@@ -59,7 +61,7 @@ class ReportHistoryItem extends StatelessWidget {
);
},
child: Padding(
- padding: const EdgeInsets.all(20),
+ padding: const EdgeInsets.all(StudyUSpacing.space5),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -67,14 +69,18 @@ class ReportHistoryItem extends StatelessWidget {
Icon(
MdiIcons.fromString(subject.study.iconName) ??
MdiIcons.accountHeart,
- color: isActiveStudy ? Colors.white : Colors.black,
+ color: isActiveStudy
+ ? colorScheme.onPrimary
+ : colorScheme.onSurface,
),
- const SizedBox(width: 16),
+ const SizedBox(width: StudyUSpacing.space4),
Expanded(
child: Text(
subject.study.title!,
style: theme.textTheme.headlineSmall!.copyWith(
- color: isActiveStudy ? Colors.white : Colors.black,
+ color: isActiveStudy
+ ? colorScheme.onPrimary
+ : colorScheme.onSurface,
),
),
),
diff --git a/app/lib/screens/study/report/report_section_container.dart b/app/lib/screens/study/report/report_section_container.dart
index 5bcfd9877..ed3043314 100644
--- a/app/lib/screens/study/report/report_section_container.dart
+++ b/app/lib/screens/study/report/report_section_container.dart
@@ -6,6 +6,7 @@ import 'package:studyu_app/screens/study/report/sections/descriptive_stats_secti
import 'package:studyu_app/screens/study/report/sections/gauge_comparison_section_widget.dart';
import 'package:studyu_app/screens/study/report/sections/linear_regression_section_widget.dart';
import 'package:studyu_app/screens/study/report/sections/textual_summary_section_widget.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
typedef SectionBuilder =
@@ -48,7 +49,7 @@ class ReportSectionContainer extends StatelessWidget {
color: theme.colorScheme.secondary,
),
),
- const SizedBox(height: 4),
+ const SizedBox(height: StudyUSpacing.space1),
];
@override
@@ -58,18 +59,18 @@ class ReportSectionContainer extends StatelessWidget {
child: InkWell(
onTap: onTap,
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (primary) ...buildPrimaryHeader(context, theme),
Text(section.title ?? '', style: theme.textTheme.headlineSmall),
- const SizedBox(height: 4),
+ const SizedBox(height: StudyUSpacing.space1),
Text(
section.description ?? '',
style: theme.textTheme.bodyMedium,
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
buildContents(context),
],
),
diff --git a/app/lib/screens/study/report/sections/average_section_widget.dart b/app/lib/screens/study/report/sections/average_section_widget.dart
index 28ad210aa..11edc53de 100644
--- a/app/lib/screens/study/report/sections/average_section_widget.dart
+++ b/app/lib/screens/study/report/sections/average_section_widget.dart
@@ -115,7 +115,7 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
for (final entry in data)
interventionNames[entry.intervention]!: Legend(
interventionNames[entry.intervention]!,
- getColor(entry),
+ getColor(context, entry),
),
};
return LegendsListWidget(legends: legends.values.toList());
@@ -134,6 +134,7 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
}
BarChartData getChartData(BuildContext context, List data) {
+ final colorScheme = Theme.of(context).colorScheme;
final barGroups = getBarGroups(context, data);
final maxY =
((data.sortedBy((entry) => entry.value).toList().lastOrNull?.value ??
@@ -161,12 +162,12 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
rod.toY.toString(),
- const TextStyle(color: Colors.white),
+ TextStyle(color: colorScheme.surface),
);
},
),
),
- backgroundColor: getBackgroundColor(data),
+ backgroundColor: getBackgroundColor(context, data),
maxY: maxY,
);
}
@@ -175,6 +176,7 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
BuildContext context,
List data,
) {
+ final colorScheme = Theme.of(context).colorScheme;
// Sort data by x value to ensure proper line plotting
data.sort((a, b) => a.x.compareTo(b.x));
@@ -191,13 +193,14 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
];
// Determine the background color based on the intervention
+ final datumColor = getColor(context, datum);
Color backgroundColor;
- if (getColor(datum) == Colors.blue) {
- backgroundColor = Colors.blue.withValues(alpha: 0.6);
- } else if (getColor(datum) == Colors.orange) {
- backgroundColor = Colors.orange.withValues(alpha: 0.6);
- } else if (getColor(datum) == Colors.grey) {
- backgroundColor = Colors.grey.withValues(alpha: 0.6);
+ if (datumColor == colorScheme.primary) {
+ backgroundColor = colorScheme.primary.withValues(alpha: 0.6);
+ } else if (datumColor == colorScheme.secondary) {
+ backgroundColor = colorScheme.secondary.withValues(alpha: 0.6);
+ } else if (datumColor == colorScheme.outline) {
+ backgroundColor = colorScheme.outline.withValues(alpha: 0.6);
} else {
backgroundColor = Colors.transparent;
}
@@ -215,13 +218,13 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
- color: Colors.black,
+ color: colorScheme.onSurface,
strokeWidth: 2,
- strokeColor: Colors.white,
+ strokeColor: colorScheme.surface,
);
},
),
- color: Colors.black, // Line color
+ color: colorScheme.onSurface, // Line color
),
);
}
@@ -279,7 +282,10 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
topTitles: const AxisTitles(axisNameWidget: SizedBox.shrink()),
rightTitles: const AxisTitles(axisNameWidget: SizedBox.shrink()),
),
- borderData: FlBorderData(show: true, border: Border.all()),
+ borderData: FlBorderData(
+ show: true,
+ border: Border.all(color: colorScheme.outline),
+ ),
lineBarsData: lineBarsData,
minX: minX,
maxX: maxX,
@@ -288,23 +294,29 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
);
}
- List getBackgroundColors(List data) {
+ List getBackgroundColors(
+ BuildContext context,
+ List data,
+ ) {
+ final colorScheme = Theme.of(context).colorScheme;
final List colors = [];
for (final datum in data) {
- if (getColor(datum) == Colors.blue) {
- colors.add(Colors.blue.withValues(alpha: 0.2));
- } else if (getColor(datum) == Colors.orange) {
- colors.add(Colors.orange.withValues(alpha: 0.2));
+ final datumColor = getColor(context, datum);
+ if (datumColor == colorScheme.primary) {
+ colors.add(colorScheme.primary.withValues(alpha: 0.2));
+ } else if (datumColor == colorScheme.secondary) {
+ colors.add(colorScheme.secondary.withValues(alpha: 0.2));
}
}
return colors;
}
- Color getBackgroundColor(List data) {
+ Color getBackgroundColor(BuildContext context, List data) {
+ final colorScheme = Theme.of(context).colorScheme;
if (data.any((datum) => datum.intervention == 'intervention_a')) {
- return Colors.lightBlue.withValues(alpha: 0.3);
+ return colorScheme.primaryContainer;
} else if (data.any((datum) => datum.intervention == 'intervention_b')) {
- return Colors.orange.withValues(alpha: 0.3);
+ return colorScheme.secondaryContainer;
}
return Colors.transparent;
}
@@ -371,7 +383,7 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
starter[entry.x.round()] = barGenerator(
entry.x.round(),
y: entry.value.toDouble(),
- color: getColor(entry),
+ color: getColor(context, entry),
);
}
return starter;
@@ -406,10 +418,11 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
);
}
- MaterialColor getColor(DiagramDatum diagram) {
- const baselineColor = Colors.grey;
- final colors = [Colors.blue, Colors.orange];
- MaterialColor? c = Colors.teal;
+ Color getColor(BuildContext context, DiagramDatum diagram) {
+ final colorScheme = Theme.of(context).colorScheme;
+ final baselineColor = colorScheme.outline;
+ final colors = [colorScheme.primary, colorScheme.secondary];
+ Color c = colorScheme.primary;
switch (widget.section.aggregate) {
case TemporalAggregation.day:
@@ -439,9 +452,9 @@ class _AverageSectionWidgetState extends State<_AverageSectionStatefulWidget> {
if (widget.subject.study.schedule.includeBaseline && diagram.x == 2) {
c = baselineColor;
} else if (diagram.x == 0) {
- c = Colors.blue;
+ c = colorScheme.primary;
} else if (diagram.x == 1) {
- c = Colors.orange;
+ c = colorScheme.secondary;
}
default:
}
diff --git a/app/lib/screens/study/tasks/task_screen.dart b/app/lib/screens/study/tasks/task_screen.dart
index 6e3b0ec54..911938ca8 100644
--- a/app/lib/screens/study/tasks/task_screen.dart
+++ b/app/lib/screens/study/tasks/task_screen.dart
@@ -4,6 +4,7 @@ import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/screens/study/tasks/intervention/checkmark_task_widget.dart';
import 'package:studyu_app/screens/study/tasks/observation/questionnaire_task_widget.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/cache.dart';
import 'package:studyu_app/widgets/html_text.dart';
import 'package:studyu_core/core.dart';
@@ -45,7 +46,7 @@ class _TaskScreenState extends State {
child: Column(
children: [
HtmlText(taskInstance.task.header, centered: true),
- const SizedBox(height: 20),
+ const SizedBox(height: StudyUSpacing.space5),
CheckmarkTaskWidget(
task: checkmarkTask,
key: UniqueKey(),
@@ -70,7 +71,10 @@ class _TaskScreenState extends State {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(taskInstance.task.title ?? '')),
- body: Padding(padding: const EdgeInsets.all(16), child: _buildTask()),
+ body: Padding(
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
+ child: _buildTask(),
+ ),
);
}
}
diff --git a/app/lib/spacing.dart b/app/lib/spacing.dart
new file mode 100644
index 000000000..c37c3d27b
--- /dev/null
+++ b/app/lib/spacing.dart
@@ -0,0 +1,52 @@
+/// StudyU Design System Spacing Scale
+///
+/// 4px base grid, aligned with CSS design tokens in colors_and_type.css.
+/// Use these constants instead of hardcoded numeric values for consistent
+/// padding, margin, and spacing across the app.
+///
+/// ```dart
+/// // Preferred
+/// padding: EdgeInsets.all(StudyUSpacing.space4),
+/// SizedBox(height: StudyUSpacing.space2),
+///
+/// // Avoid
+/// padding: EdgeInsets.all(16),
+/// SizedBox(height: 8),
+/// ```
+class StudyUSpacing {
+ StudyUSpacing._();
+
+ // Core spacing scale
+ static const double space1 = 4.0;
+ static const double space2 = 8.0;
+ static const double space3 = 12.0;
+ static const double space4 = 16.0;
+ static const double space5 = 20.0;
+ static const double space6 = 24.0;
+ static const double space8 = 32.0;
+ static const double space10 = 40.0;
+ static const double space12 = 48.0;
+ static const double space16 = 64.0;
+
+ // Semantic aliases for common usage patterns
+ /// Standard screen horizontal padding
+ static const double screenHorizontal = space4;
+
+ /// Standard card padding
+ static const double cardPadding = space4;
+
+ /// Compact spacing within card/list tile content
+ static const double cardCompact = space2;
+
+ /// Vertical gap between sections
+ static const double sectionGap = space6;
+
+ /// Smallest visible gap (between inline elements)
+ static const double inlineGap = space1;
+
+ /// Standard gap between related items
+ static const double itemGap = space2;
+
+ /// Gap between unrelated items/groups
+ static const double groupGap = space4;
+}
diff --git a/app/lib/theme.dart b/app/lib/theme.dart
index 028e11b9c..d357502fd 100644
--- a/app/lib/theme.dart
+++ b/app/lib/theme.dart
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
-const primaryColor = Colors.blue;
-const accentColor = Colors.orange;
+const primaryColor = Color(0xFF2196F3);
+const accentColor = Color(0xFFFF9800);
+const surfaceColor = Colors.white;
+const scaffoldColor = Color(0xFFF5F7FA);
class ThemeConfig {
static SliderThemeData coloredSliderTheme(ThemeData theme) => SliderThemeData(
@@ -11,14 +13,91 @@ class ThemeConfig {
}
ThemeData get theme => ThemeData(
+ useMaterial3: true,
brightness: Brightness.light,
primaryColor: primaryColor,
- colorScheme: ThemeData().colorScheme.copyWith(
- secondary: accentColor,
- primary: primaryColor,
+ scaffoldBackgroundColor: scaffoldColor,
+ appBarTheme: const AppBarTheme(
+ backgroundColor: primaryColor,
+ foregroundColor: Colors.white,
+ elevation: 2,
+ titleTextStyle: TextStyle(
+ color: Colors.white,
+ fontSize: 20,
+ fontWeight: FontWeight.w700,
+ ),
),
+ cardTheme: const CardThemeData(
+ color: surfaceColor,
+ elevation: 0,
+ surfaceTintColor: Colors.transparent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ ),
+ ),
+ colorScheme:
+ ColorScheme.fromSeed(
+ seedColor: primaryColor,
+ primary: primaryColor,
+ secondary: accentColor,
+ surface: surfaceColor,
+ ).copyWith(
+ onPrimary: Colors.white,
+ onSecondary: Colors.white,
+ onSurface: const Color(0xFF333333),
+ surfaceContainerLowest: surfaceColor,
+ surfaceContainerLow: scaffoldColor,
+ surfaceContainer: scaffoldColor,
+ surfaceContainerHigh: scaffoldColor,
+ surfaceContainerHighest: const Color(0xFFE9EEF5),
+ surfaceTint: Colors.transparent,
+ ),
elevatedButtonTheme: ElevatedButtonThemeData(
- style: ButtonStyle(foregroundColor: WidgetStateProperty.all(Colors.white)),
+ style: ButtonStyle(
+ foregroundColor: WidgetStateProperty.all(Colors.white),
+ backgroundColor: WidgetStateProperty.all(primaryColor),
+ surfaceTintColor: WidgetStateProperty.all(Colors.transparent),
+ overlayColor: WidgetStateProperty.all(
+ primaryColor.withValues(alpha: 0.08),
+ ),
+ shape: WidgetStateProperty.all(
+ const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ ),
+ ),
+ ),
+ ),
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: ButtonStyle(
+ foregroundColor: WidgetStateProperty.all(primaryColor),
+ backgroundColor: WidgetStateProperty.all(Colors.transparent),
+ surfaceTintColor: WidgetStateProperty.all(Colors.transparent),
+ overlayColor: WidgetStateProperty.all(
+ primaryColor.withValues(alpha: 0.08),
+ ),
+ side: WidgetStateProperty.all(
+ const BorderSide(color: primaryColor, width: 1.5),
+ ),
+ shape: WidgetStateProperty.all(
+ const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ ),
+ ),
+ ),
+ ),
+ textButtonTheme: TextButtonThemeData(
+ style: ButtonStyle(
+ foregroundColor: WidgetStateProperty.all(primaryColor),
+ surfaceTintColor: WidgetStateProperty.all(Colors.transparent),
+ overlayColor: WidgetStateProperty.all(
+ primaryColor.withValues(alpha: 0.08),
+ ),
+ shape: WidgetStateProperty.all(
+ const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ ),
+ ),
+ ),
),
visualDensity: VisualDensity.adaptivePlatformDensity,
);
diff --git a/app/lib/util/debug_screen.dart b/app/lib/util/debug_screen.dart
index e33e42786..bec5ee573 100644
--- a/app/lib/util/debug_screen.dart
+++ b/app/lib/util/debug_screen.dart
@@ -7,6 +7,7 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/notifications.dart';
import 'package:studyu_app/util/schedule_notifications.dart';
import 'package:studyu_core/core.dart';
@@ -140,21 +141,21 @@ class __DebugDialogState extends State<_DebugDialog> {
content: Column(
children: [
_buildVersionInfo(),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
_buildEmailButton(),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
_buildTestNotificationButton(),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
_buildResetAppButton(),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
_buildPreviewModeSwitch(),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
_buildBatteryOptimizationInfo(),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
_buildPendingNotificationsInfo(),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
_buildPendingNotificationsPluginInfo(),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
_buildScheduledNotificationsInfo(),
],
),
diff --git a/app/lib/widgets/bottom_onboarding_navigation.dart b/app/lib/widgets/bottom_onboarding_navigation.dart
index 6ea37f00e..d9e6258d3 100644
--- a/app/lib/widgets/bottom_onboarding_navigation.dart
+++ b/app/lib/widgets/bottom_onboarding_navigation.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
class BottomOnboardingNavigation extends StatelessWidget {
final VoidCallback? onBack;
@@ -31,7 +32,7 @@ class BottomOnboardingNavigation extends StatelessWidget {
Widget build(BuildContext context) {
return BottomAppBar(
child: Padding(
- padding: const EdgeInsets.all(8),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: Row(
children: [
Visibility(
@@ -52,9 +53,9 @@ class BottomOnboardingNavigation extends StatelessWidget {
),
),
if (progress != null) ...[
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Expanded(child: progress!),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
] else
const Spacer(),
Visibility(
diff --git a/app/lib/widgets/intervention_card.dart b/app/lib/widgets/intervention_card.dart
index ffc0fc1f2..20a8409b5 100644
--- a/app/lib/widgets/intervention_card.dart
+++ b/app/lib/widgets/intervention_card.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/html_text.dart';
import 'package:studyu_core/core.dart';
@@ -129,7 +130,12 @@ class InterventionCardDescription extends StatelessWidget {
if (description == null) return Container();
return Padding(
- padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
+ padding: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space4,
+ StudyUSpacing.space1,
+ StudyUSpacing.space4,
+ StudyUSpacing.space2,
+ ),
child: Text(
description,
style: theme.textTheme.bodyMedium!.copyWith(
@@ -160,7 +166,10 @@ class _TaskList extends StatelessWidget {
return Column(
children: [
Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ padding: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space2,
+ ),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -178,8 +187,8 @@ class _TaskList extends StatelessWidget {
.map(
(task) => Padding(
padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 8,
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space2,
),
child: Row(
children: [
@@ -196,7 +205,7 @@ class _TaskList extends StatelessWidget {
size: 16,
color: theme.textTheme.bodySmall!.color,
),
- const SizedBox(width: 4),
+ const SizedBox(width: StudyUSpacing.space1),
Text(
scheduleString(task.schedule.completionPeriods),
style: theme.textTheme.bodyMedium!.copyWith(
diff --git a/app/lib/widgets/questionnaire/audio_recording_question_widget.dart b/app/lib/widgets/questionnaire/audio_recording_question_widget.dart
index a987dbea7..46b35abfe 100644
--- a/app/lib/widgets/questionnaire/audio_recording_question_widget.dart
+++ b/app/lib/widgets/questionnaire/audio_recording_question_widget.dart
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import 'package:record/record.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/temporary_storage_handler.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
import 'package:studyu_core/core.dart';
@@ -86,13 +87,13 @@ class _AudioRecordingQuestionWidgetState
: null,
child: Padding(
padding: const EdgeInsets.symmetric(
- vertical: 8.0,
- horizontal: 2.0,
+ vertical: StudyUSpacing.space2,
+ horizontal: StudyUSpacing.space1,
),
child: Row(
children: [
Padding(
- padding: const EdgeInsets.all(4.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space1),
child: Icon(
_hasRecorded
? MdiIcons.checkCircleOutline
@@ -122,18 +123,18 @@ class _AudioRecordingQuestionWidgetState
),
),
),
- const SizedBox(width: 16.0),
+ const SizedBox(width: StudyUSpacing.space4),
Text(
'${_formatNumber(_recordDurationSeconds ~/ 60)}:${_formatNumber(_recordDurationSeconds % 60)}',
style: const TextStyle(fontSize: 16),
),
- const SizedBox(width: 8.0),
+ const SizedBox(width: StudyUSpacing.space2),
if (_isRecording &&
_recordDurationSeconds > 0 &&
_recordDurationSeconds < maxRecordingDurationSeconds)
SizedBox(
- width: 16,
- height: 16,
+ width: StudyUSpacing.space4,
+ height: StudyUSpacing.space4,
child: CircularProgressIndicator(
value:
1.0 - (_recordDurationSeconds / maxRecordingDurationSeconds),
diff --git a/app/lib/widgets/questionnaire/image_capturing_question_widget.dart b/app/lib/widgets/questionnaire/image_capturing_question_widget.dart
index bb62c5a69..5599d0a3f 100644
--- a/app/lib/widgets/questionnaire/image_capturing_question_widget.dart
+++ b/app/lib/widgets/questionnaire/image_capturing_question_widget.dart
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
import 'package:studyu_app/screens/study/multimodal/capture_picture_screen.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
import 'package:studyu_core/core.dart';
@@ -49,11 +50,14 @@ class _ImageCapturingQuestionWidgetState
}
: null,
child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 2.0),
+ padding: const EdgeInsets.symmetric(
+ vertical: StudyUSpacing.space2,
+ horizontal: StudyUSpacing.space1,
+ ),
child: Row(
children: [
Padding(
- padding: const EdgeInsets.all(4.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space1),
child: Icon(
_hasCaptured ? MdiIcons.checkCircleOutline : MdiIcons.camera,
color: _hasCaptured
diff --git a/app/lib/widgets/questionnaire/pain_selection/body_part_selector.dart b/app/lib/widgets/questionnaire/pain_selection/body_part_selector.dart
index 34e0cdcfc..e4b9f29ac 100644
--- a/app/lib/widgets/questionnaire/pain_selection/body_part_selector.dart
+++ b/app/lib/widgets/questionnaire/pain_selection/body_part_selector.dart
@@ -7,6 +7,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/pain_selection/svg_service.dart';
import 'package:studyu_core/core.dart';
import 'package:touchable/touchable.dart';
@@ -326,8 +327,8 @@ class _PainEditDialogState extends State {
duration: const Duration(milliseconds: 150),
width: 90,
padding: const EdgeInsets.symmetric(
- vertical: 12,
- horizontal: 8,
+ vertical: StudyUSpacing.space3,
+ horizontal: StudyUSpacing.space2,
),
decoration: BoxDecoration(
color: selectedLevel == level
@@ -356,7 +357,7 @@ class _PainEditDialogState extends State {
style.face,
style: const TextStyle(fontSize: 36),
),
- const SizedBox(height: 6),
+ const SizedBox(height: StudyUSpacing.space2),
Text(
style.description,
textAlign: TextAlign.center,
@@ -428,7 +429,7 @@ class _PainEditDialogState extends State {
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Expanded(child: Text(loc.painTypeLabel)),
],
),
@@ -439,7 +440,7 @@ class _PainEditDialogState extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- const SizedBox(height: 12),
+ const SizedBox(height: StudyUSpacing.space3),
Flexible(
child: SizedBox(
width: 320,
@@ -507,7 +508,7 @@ class _PainEditDialogState extends State {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- const SizedBox(height: 12),
+ const SizedBox(height: StudyUSpacing.space3),
Flexible(
child: SizedBox(
width: 320,
diff --git a/app/lib/widgets/questionnaire/pain_selection/body_part_selector_turnable.dart b/app/lib/widgets/questionnaire/pain_selection/body_part_selector_turnable.dart
index 149428ca0..762d52c5c 100644
--- a/app/lib/widgets/questionnaire/pain_selection/body_part_selector_turnable.dart
+++ b/app/lib/widgets/questionnaire/pain_selection/body_part_selector_turnable.dart
@@ -3,6 +3,7 @@
// Licensed under the MIT License.
import 'package:flutter/material.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/pain_selection/body_part_selector.dart';
import 'package:studyu_core/core.dart';
@@ -17,7 +18,7 @@ class BodyPartSelectorTurnable extends StatefulWidget {
this.scale = PainScale.english,
this.unselectedColor,
this.unselectedOutlineColor,
- this.padding = const EdgeInsets.all(16),
+ this.padding = const EdgeInsets.all(StudyUSpacing.space4),
this.frontButtonText = 'Front',
this.backButtonText = 'Back',
this.frontButtonIcon = const Icon(Icons.face),
@@ -73,7 +74,7 @@ class _BodyPartSelectorTurnableState extends State {
mainAxisSize: MainAxisSize.min,
children: [
Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
+ padding: const EdgeInsets.symmetric(vertical: StudyUSpacing.space2),
child: SegmentedButton(
showSelectedIcon: false,
segments: [
@@ -96,7 +97,7 @@ class _BodyPartSelectorTurnableState extends State {
},
),
),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
Padding(
padding: widget.padding,
child: BodyPartSelector(
diff --git a/app/lib/widgets/questionnaire/question_container.dart b/app/lib/widgets/questionnaire/question_container.dart
index 95b1f0fc7..0a46690eb 100644
--- a/app/lib/widgets/questionnaire/question_container.dart
+++ b/app/lib/widgets/questionnaire/question_container.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/audio_recording_question_widget.dart';
import 'package:studyu_app/widgets/questionnaire/image_capturing_question_widget.dart';
import 'package:studyu_app/widgets/questionnaire/question_header.dart';
@@ -107,7 +108,12 @@ class _QuestionContainerState extends State
return Card(
key: widget.containerKey,
child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
+ padding: const EdgeInsets.fromLTRB(
+ StudyUSpacing.space4,
+ StudyUSpacing.space4,
+ StudyUSpacing.space4,
+ StudyUSpacing.space2,
+ ),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -117,7 +123,7 @@ class _QuestionContainerState extends State
subtitle: questionBody.subtitle,
rationale: widget.question.rationale,
),
- const SizedBox(height: 24),
+ const SizedBox(height: StudyUSpacing.space6),
questionBody,
],
),
diff --git a/app/lib/widgets/questionnaire/question_header.dart b/app/lib/widgets/questionnaire/question_header.dart
index 797c45d49..b5501f171 100644
--- a/app/lib/widgets/questionnaire/question_header.dart
+++ b/app/lib/widgets/questionnaire/question_header.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/html_text.dart';
class QuestionHeader extends StatelessWidget {
@@ -11,7 +12,7 @@ class QuestionHeader extends StatelessWidget {
List _buildSubtitle(BuildContext context) {
if (subtitle == null || subtitle!.isEmpty) return [];
return [
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
Text(subtitle!, style: Theme.of(context).textTheme.bodySmall),
];
}
@@ -19,7 +20,7 @@ class QuestionHeader extends StatelessWidget {
List _buildRationaleButton(BuildContext context) {
if (rationale == null || rationale!.isEmpty) return [];
return [
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
IconButton(
icon: const Icon(Icons.info_outline),
color: Theme.of(context).primaryColor,
diff --git a/app/lib/widgets/questionnaire/questionnaire_widget.dart b/app/lib/widgets/questionnaire/questionnaire_widget.dart
index 1e02a2d39..721e507aa 100644
--- a/app/lib/widgets/questionnaire/questionnaire_widget.dart
+++ b/app/lib/widgets/questionnaire/questionnaire_widget.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/html_text.dart';
import 'package:studyu_app/widgets/questionnaire/question_container.dart';
import 'package:studyu_core/core.dart';
@@ -397,7 +398,7 @@ class HtmlTextBox extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
- padding: const EdgeInsets.all(16),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
diff --git a/app/lib/widgets/questionnaire/questions/boolean_question_widget.dart b/app/lib/widgets/questionnaire/questions/boolean_question_widget.dart
index febea344a..68ad73a68 100644
--- a/app/lib/widgets/questionnaire/questions/boolean_question_widget.dart
+++ b/app/lib/widgets/questionnaire/questions/boolean_question_widget.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
import 'package:studyu_app/widgets/selectable_button.dart';
import 'package:studyu_core/core.dart';
@@ -40,7 +41,7 @@ class _BooleanQuestionWidgetState extends State {
onTap: () => tapped(choice: true),
child: Text(AppLocalizations.of(context)!.yes),
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
SelectableButton(
selected: selected == false,
onTap: () => tapped(choice: false),
diff --git a/app/lib/widgets/questionnaire/questions/choice_question_widget.dart b/app/lib/widgets/questionnaire/questions/choice_question_widget.dart
index 6b2803ef1..c65b491a1 100644
--- a/app/lib/widgets/questionnaire/questions/choice_question_widget.dart
+++ b/app/lib/widgets/questionnaire/questions/choice_question_widget.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
import 'package:studyu_app/widgets/selectable_button.dart';
import 'package:studyu_core/core.dart';
@@ -93,7 +94,8 @@ class _ChoiceQuestionWidgetState extends State {
physics: const NeverScrollableScrollPhysics(),
itemCount: choiceWidgets.length,
itemBuilder: (context, index) => choiceWidgets[index],
- separatorBuilder: (context, index) => const SizedBox(height: 8),
+ separatorBuilder: (context, index) =>
+ const SizedBox(height: StudyUSpacing.space2),
);
}
}
diff --git a/app/lib/widgets/questionnaire/questions/fitbit_question_widget.dart b/app/lib/widgets/questionnaire/questions/fitbit_question_widget.dart
index 6230b40fe..96c8bc7f8 100644
--- a/app/lib/widgets/questionnaire/questions/fitbit_question_widget.dart
+++ b/app/lib/widgets/questionnaire/questions/fitbit_question_widget.dart
@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/models/app_state.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/fitbit_handler.dart';
import 'package:studyu_app/util/string_extensions.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
@@ -118,7 +119,10 @@ class _FitbitQuestionWidgetState extends State {
Text(AppLocalizations.of(context)!.fitbit_data_synced_info),
for (final type in earliestDates.keys)
Padding(
- padding: const EdgeInsets.only(top: 5.0, left: 10.0),
+ padding: const EdgeInsets.only(
+ top: StudyUSpacing.space1,
+ left: StudyUSpacing.space3,
+ ),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -185,7 +189,7 @@ class _FitbitQuestionWidgetState extends State {
Icons.check_circle,
color: Theme.of(context).colorScheme.onSecondary,
),
- const SizedBox(width: 8),
+ const SizedBox(width: StudyUSpacing.space2),
Expanded(
child: Text(
AppLocalizations.of(context)!.fitbit_data_synced,
@@ -220,7 +224,7 @@ class _FitbitQuestionWidgetState extends State {
onPressed: _isLoading ? null : _syncFitbitData,
child: _isLoading
? Padding(
- padding: const EdgeInsets.all(8.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
child: CircularProgressIndicator(
color: Theme.of(context).colorScheme.onSecondary,
),
diff --git a/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart b/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart
index be16a4cc2..c31507fe1 100644
--- a/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart
+++ b/app/lib/widgets/questionnaire/questions/free_text_question_widget.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
import 'package:studyu_core/core.dart';
@@ -209,7 +210,7 @@ class _FreeTextQuestionWidgetState extends State {
}
},
),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
diff --git a/app/lib/widgets/questionnaire/questions/pain_question_widget.dart b/app/lib/widgets/questionnaire/questions/pain_question_widget.dart
index 6fa517457..bd7a67891 100644
--- a/app/lib/widgets/questionnaire/questions/pain_question_widget.dart
+++ b/app/lib/widgets/questionnaire/questions/pain_question_widget.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/widgets/questionnaire/pain_selection/body_part_selector.dart';
import 'package:studyu_app/widgets/questionnaire/pain_selection/body_part_selector_turnable.dart';
import 'package:studyu_app/widgets/questionnaire/questions/question_widget.dart';
@@ -91,7 +92,7 @@ class _PainQuestionWidgetState extends State {
frontButtonIcon: const Icon(Icons.face_outlined),
backButtonIcon: const Icon(Icons.accessibility_new_outlined),
),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
OutlinedButton(
onPressed: _onDone,
child: Text(AppLocalizations.of(context)!.done),
diff --git a/app/lib/widgets/report/descriptive_statistics_widget.dart b/app/lib/widgets/report/descriptive_statistics_widget.dart
index f98ccc55d..eb074a2ee 100644
--- a/app/lib/widgets/report/descriptive_statistics_widget.dart
+++ b/app/lib/widgets/report/descriptive_statistics_widget.dart
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:statistics/statistics.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
class DescriptiveStats {
@@ -64,11 +65,13 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
return Card(
- elevation: 3,
- margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
+ margin: const EdgeInsets.symmetric(
+ vertical: StudyUSpacing.space2,
+ horizontal: StudyUSpacing.space1,
+ ),
child: ExpansionTile(
title: Text(
'Descriptive Statistics',
@@ -83,21 +86,21 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
initiallyExpanded: initiallyExpanded,
children: [
Padding(
- padding: const EdgeInsets.all(16.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummary(context),
- const SizedBox(height: 16),
+ const SizedBox(height: StudyUSpacing.space4),
_buildStatsTable(context),
if (statsA.missing > 0 || statsB.missing > 0)
Padding(
- padding: const EdgeInsets.only(top: 8.0),
+ padding: const EdgeInsets.only(top: StudyUSpacing.space2),
child: Text(
AppLocalizations.of(context)!.missing_observations_note,
style: theme.textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
- color: Colors.grey[600],
+ color: colorScheme.onSurfaceVariant,
),
),
),
@@ -111,14 +114,15 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
Widget _buildSummary(BuildContext context) {
final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
final headingStyle = theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
);
return Container(
- padding: const EdgeInsets.all(12),
+ padding: const EdgeInsets.all(StudyUSpacing.space3),
decoration: BoxDecoration(
- color: Colors.blue.withValues(alpha: 0.1),
+ color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Column(
@@ -128,7 +132,7 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
AppLocalizations.of(context)!.quick_summary,
style: headingStyle,
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
Row(
children: [
Expanded(
@@ -142,7 +146,7 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
Expanded(child: _valueLabel(statsB.avgString, statsB.name)),
],
),
- const SizedBox(height: 4),
+ const SizedBox(height: StudyUSpacing.space1),
Row(
children: [
Expanded(
@@ -179,7 +183,9 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
1: FlexColumnWidth(),
2: FlexColumnWidth(),
},
- border: TableBorder.all(color: Colors.grey.shade300),
+ border: TableBorder.all(
+ color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
+ ),
children: [
_buildTableRow(
[AppLocalizations.of(context)!.statistic, statsA.name, statsB.name],
@@ -226,17 +232,21 @@ class DescriptiveStatisticsWidget extends StatelessWidget {
bool highlight = false,
}) {
final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
return TableRow(
decoration: BoxDecoration(
color: isHeader
- ? Colors.grey.shade200
+ ? colorScheme.surfaceContainerHighest
: highlight
- ? Colors.amber.withValues(alpha: 0.1)
+ ? colorScheme.secondaryContainer
: null,
),
children: cells.map((cell) {
return Padding(
- padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
+ padding: const EdgeInsets.symmetric(
+ vertical: StudyUSpacing.space3,
+ horizontal: StudyUSpacing.space2,
+ ),
child: Text(
cell,
softWrap: true,
diff --git a/app/lib/widgets/report/gauges_widget.dart b/app/lib/widgets/report/gauges_widget.dart
index 014cff683..4f0074b40 100644
--- a/app/lib/widgets/report/gauges_widget.dart
+++ b/app/lib/widgets/report/gauges_widget.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:statistics/statistics.dart';
+import 'package:studyu_app/spacing.dart';
class GaugesWidget extends StatelessWidget {
final String nameInterventionA;
@@ -21,22 +22,24 @@ class GaugesWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
return Row(
children: [
Expanded(
child: Column(
children: [
Container(
- padding: const EdgeInsets.all(8.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
decoration: BoxDecoration(
- color: Colors.white,
- border: Border.all(color: Colors.grey),
+ color: colorScheme.surface,
+ border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
width: 160,
height: 160,
child: createGauge(
+ context,
0,
10,
meanInterventionA,
@@ -51,16 +54,17 @@ class GaugesWidget extends StatelessWidget {
child: Column(
children: [
Container(
- padding: const EdgeInsets.all(8.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
decoration: BoxDecoration(
- color: Colors.white,
- border: Border.all(color: Colors.grey),
+ color: colorScheme.surface,
+ border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
width: 160,
height: 160,
child: createGauge(
+ context,
0,
10,
meanInterventionB,
@@ -76,12 +80,15 @@ class GaugesWidget extends StatelessWidget {
}
Widget createGauge(
+ BuildContext context,
double min,
double max,
num value,
String nameIntervention,
) {
- const Color gaugeBackgroundColor = Color(0xFFDFE2EC);
+ final colorScheme = Theme.of(context).colorScheme;
+ final gaugeBackgroundColor = colorScheme.surfaceContainerHighest;
+ final needleColor = colorScheme.onSurface;
// Create a gauge axis based on whether colors should be shown
GaugeAxis gaugeAxis;
@@ -91,15 +98,15 @@ class GaugesWidget extends StatelessWidget {
min: min,
max: max,
degrees: 240, // Set to 240 degrees for a 3/4 circular gauge
- style: const GaugeAxisStyle(
+ style: GaugeAxisStyle(
background: gaugeBackgroundColor,
segmentSpacing: 4,
),
- pointer: const GaugePointer.needle(
+ pointer: GaugePointer.needle(
width: 10,
height: 50,
borderRadius: 8,
- color: Color(0xFF193663),
+ color: needleColor,
),
progressBar: null, // Disable the progress bar
segments: [
@@ -120,15 +127,15 @@ class GaugesWidget extends StatelessWidget {
min: min,
max: max,
degrees: 240, // Set to 240 degrees for a 3/4 circular gauge
- style: const GaugeAxisStyle(
+ style: GaugeAxisStyle(
background: gaugeBackgroundColor,
segmentSpacing: 4,
),
- pointer: const GaugePointer.needle(
+ pointer: GaugePointer.needle(
width: 10,
height: 50,
borderRadius: 8,
- color: Color(0xFF193663),
+ color: needleColor,
),
);
}
@@ -152,15 +159,15 @@ class GaugesWidget extends StatelessWidget {
children: [
TextSpan(
text: value.toStringAsFixed(1),
- style: const TextStyle(
+ style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
- color: Colors.black87,
+ color: colorScheme.onSurface,
),
),
- const TextSpan(
+ TextSpan(
text: '/10',
- style: TextStyle(fontSize: 12, color: Colors.black),
+ style: TextStyle(fontSize: 12, color: colorScheme.onSurface),
),
],
),
@@ -173,7 +180,7 @@ class GaugesWidget extends StatelessWidget {
children: [
TextSpan(
text: nameIntervention,
- style: const TextStyle(fontSize: 15, color: Colors.black),
+ style: TextStyle(fontSize: 15, color: colorScheme.onSurface),
),
],
),
diff --git a/app/lib/widgets/report/textual_summary_widget.dart b/app/lib/widgets/report/textual_summary_widget.dart
index 561d3e668..86957b061 100644
--- a/app/lib/widgets/report/textual_summary_widget.dart
+++ b/app/lib/widgets/report/textual_summary_widget.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:statistics/statistics.dart';
import 'package:studyu_app/l10n/app_localizations.dart';
import 'package:studyu_app/screens/study/report/sections/t_test.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_app/util/string_extensions.dart';
import 'package:studyu_core/core.dart';
@@ -25,6 +26,9 @@ class TextualSummaryWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
if (valuesInterventionA.length < 2 || valuesInterventionB.length < 2) {
return SizedBox(
width: double.infinity,
@@ -46,14 +50,14 @@ class TextualSummaryWidget extends StatelessWidget {
child: Column(
children: [
Container(
- padding: const EdgeInsets.all(8.0),
+ padding: const EdgeInsets.all(StudyUSpacing.space2),
decoration: BoxDecoration(
- border: Border.all(color: Colors.grey),
+ border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(12.0),
),
child: Column(
children: [
- const SizedBox(height: 4),
+ const SizedBox(height: StudyUSpacing.space1),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -81,6 +85,8 @@ class TextualSummaryWidget extends StatelessWidget {
}
void _showStatisticalInfoDialog(BuildContext context, TTest tTest) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
showDialog(
context: context,
builder: (BuildContext context) {
@@ -92,42 +98,38 @@ class TextualSummaryWidget extends StatelessWidget {
children: [
Text(
AppLocalizations.of(context)!.t_test_outcome_based_on,
- style: const TextStyle(fontSize: 16),
+ style: theme.textTheme.bodyLarge,
),
- const SizedBox(height: 8),
+ const SizedBox(height: StudyUSpacing.space2),
Text(
'${AppLocalizations.of(context)!.level_of_significance} α = 0.05',
- style: const TextStyle(
- fontSize: 15,
+ style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
- color: Colors.black87,
+ color: colorScheme.onSurface,
),
),
- const SizedBox(height: 2),
+ const SizedBox(height: StudyUSpacing.space1),
Text(
'${AppLocalizations.of(context)!.p_value.toPascalCase()} ${tTest.pValue.toStringAsFixed(4)}',
- style: const TextStyle(
- fontSize: 15,
+ style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
- color: Colors.black87,
+ color: colorScheme.onSurface,
),
),
- const SizedBox(height: 2),
+ const SizedBox(height: StudyUSpacing.space1),
Text(
'${AppLocalizations.of(context)!.t_statistic} ${tTest.tStatistic.toStringAsFixed(4)}',
- style: const TextStyle(
- fontSize: 15,
+ style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
- color: Colors.black87,
+ color: colorScheme.onSurface,
),
),
- const SizedBox(height: 2),
+ const SizedBox(height: StudyUSpacing.space1),
Text(
'${AppLocalizations.of(context)!.degrees_of_freedom} ${tTest.degreesOfFreedom.toStringAsFixed(2)}',
- style: const TextStyle(
- fontSize: 15,
+ style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
- color: Colors.black87,
+ color: colorScheme.onSurface,
),
),
],
@@ -147,6 +149,7 @@ class TextualSummaryWidget extends StatelessWidget {
RichText getTextualSummaryRich(BuildContext context, bool isDifferent) {
final loc = AppLocalizations.of(context)!;
+ final colorScheme = Theme.of(context).colorScheme;
List spans;
if (isDifferent) {
@@ -197,7 +200,7 @@ class TextualSummaryWidget extends StatelessWidget {
return RichText(
text: TextSpan(
- style: const TextStyle(color: Colors.black),
+ style: TextStyle(color: colorScheme.onSurface),
children: spans,
),
);
diff --git a/app/lib/widgets/selectable_button.dart b/app/lib/widgets/selectable_button.dart
index 78b2d26c4..1cd330e54 100644
--- a/app/lib/widgets/selectable_button.dart
+++ b/app/lib/widgets/selectable_button.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:studyu_app/spacing.dart';
class SelectableButton extends StatelessWidget {
final Widget child;
@@ -13,9 +14,10 @@ class SelectableButton extends StatelessWidget {
});
Color _getFillColor(ThemeData theme) =>
- selected ? theme.primaryColor : theme.cardColor;
+ selected ? theme.colorScheme.primary : theme.colorScheme.surface;
- Color _getTextColor(ThemeData theme) => selected ? Colors.white : Colors.blue;
+ Color _getTextColor(ThemeData theme) =>
+ selected ? theme.colorScheme.onPrimary : theme.colorScheme.primary;
@override
Widget build(BuildContext context) {
@@ -28,7 +30,10 @@ class SelectableButton extends StatelessWidget {
foregroundColor: _getTextColor(theme),
backgroundColor: _getFillColor(theme),
minimumSize: const Size(double.infinity, 48),
- padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
+ padding: const EdgeInsets.symmetric(
+ vertical: StudyUSpacing.space4,
+ horizontal: StudyUSpacing.space4,
+ ),
),
onPressed: onTap,
child: child,
diff --git a/app/lib/widgets/study_tile.dart b/app/lib/widgets/study_tile.dart
index c128c1dfe..dd0980ff5 100644
--- a/app/lib/widgets/study_tile.dart
+++ b/app/lib/widgets/study_tile.dart
@@ -1,29 +1,28 @@
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
+import 'package:studyu_app/spacing.dart';
import 'package:studyu_core/core.dart';
class StudyTile extends StatelessWidget {
final String? title;
final String? description;
final String iconName;
-
final Future Function()? onTap;
-
- final EdgeInsetsGeometry contentPadding;
+ final EdgeInsetsGeometry? contentPadding;
const StudyTile({
required this.title,
required this.description,
required this.iconName,
this.onTap,
- this.contentPadding = const EdgeInsets.all(16),
+ this.contentPadding,
super.key,
});
StudyTile.fromStudy({
required Study study,
this.onTap,
- this.contentPadding = const EdgeInsets.all(16),
+ this.contentPadding,
super.key,
}) : title = study.title,
description = study.description,
@@ -32,7 +31,7 @@ class StudyTile extends StatelessWidget {
StudyTile.fromUserStudy({
required StudySubject subject,
this.onTap,
- this.contentPadding = const EdgeInsets.all(16),
+ this.contentPadding,
super.key,
}) : title = subject.study.title,
description = subject.study.description,
@@ -41,27 +40,71 @@ class StudyTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- ListTile(
- contentPadding: contentPadding,
- onTap: onTap,
- title: Center(
- child: Text(
- title!,
- style: theme.textTheme.titleLarge!.copyWith(
- color: theme.primaryColor,
+ return Card(
+ margin: const EdgeInsets.symmetric(
+ horizontal: StudyUSpacing.space4,
+ vertical: StudyUSpacing.space2,
+ ),
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(8),
+ child: Padding(
+ padding: contentPadding ?? const EdgeInsets.all(StudyUSpacing.space4),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _StudyIconCircle(iconName: iconName),
+ const SizedBox(width: StudyUSpacing.space4),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ title ?? '',
+ style: theme.textTheme.titleLarge!.copyWith(
+ color: theme.primaryColor,
+ ),
+ ),
+ if (description != null && description!.isNotEmpty) ...[
+ const SizedBox(height: StudyUSpacing.space1),
+ Text(
+ description!,
+ style: theme.textTheme.bodySmall!.copyWith(
+ color: theme.colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ],
+ ),
),
- ),
- ),
- subtitle: Center(child: Text(description ?? '')),
- leading: Icon(
- MdiIcons.fromString(iconName),
- color: theme.primaryColor,
+ ],
),
),
- ],
+ ),
+ );
+ }
+}
+
+class _StudyIconCircle extends StatelessWidget {
+ final String iconName;
+
+ const _StudyIconCircle({required this.iconName});
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return Container(
+ width: 40,
+ height: 40,
+ decoration: BoxDecoration(
+ color: theme.colorScheme.primary,
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ MdiIcons.fromString(iconName),
+ color: theme.colorScheme.onPrimary,
+ size: 20,
+ ),
);
}
}
diff --git a/app/lib/widgets/welcome_button.dart b/app/lib/widgets/welcome_button.dart
new file mode 100644
index 000000000..562c15a37
--- /dev/null
+++ b/app/lib/widgets/welcome_button.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+
+class WelcomeButton extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final VoidCallback? onPressed;
+ final bool isPrimary;
+
+ const WelcomeButton({
+ super.key,
+ required this.icon,
+ required this.label,
+ required this.onPressed,
+ this.isPrimary = false,
+ });
+
+ static const double _width = 240;
+ static const double _height = 48;
+ static const double _iconSize = 18;
+
+ @override
+ Widget build(BuildContext context) {
+ final textStyle = Theme.of(context).textTheme.titleLarge;
+ final iconWidget = Icon(icon, size: _iconSize);
+ final labelWidget = Text(label, style: textStyle);
+
+ return SizedBox.fromSize(
+ size: const Size(_width, _height),
+ child: isPrimary
+ ? ElevatedButton.icon(
+ onPressed: onPressed,
+ icon: iconWidget,
+ label: labelWidget,
+ )
+ : OutlinedButton.icon(
+ onPressed: onPressed,
+ icon: iconWidget,
+ label: labelWidget,
+ ),
+ );
+ }
+}
diff --git a/app/pubspec.yaml b/app/pubspec.yaml
index 002f86efe..d6dcebdc5 100644
--- a/app/pubspec.yaml
+++ b/app/pubspec.yaml
@@ -79,6 +79,10 @@ flutter:
- assets/fonts/
- assets/images/
- assets/images/onboarding/
+ fonts:
+ - family: OpenSans
+ fonts:
+ - asset: assets/fonts/OpenSans-VariableFont_wdth,wght.ttf
flutter_launcher_icons:
android: true