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 @@ + + + + + +
StudyU
+
+
+
+
+ Logo gradient +
+
+
+ Primary +
+
+
+ Accent +
+
+
+ Background +
+
+
+
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
+
#0061A4
+
+
+
On Primary
+
#FFFFFF
+
+
+
Primary Container
+
#D1E4FF
+
+
+
On P. Container
+
#001D36
+
+
+

Secondary (Accent - Orange)

+
+
+
Secondary
+
#FF9800
+
+
+
On Secondary
+
#FFFFFF
+
+
+
Sec. Container
+
#FFE0B2
+
+
+
On S. Container
+
#3D1F00
+
+
+

Error & Surface

+
+
+
Error
+
#BA1A1A
+
+
+
Error Container
+
#FFDAD6
+
+
+
Surface
+
#FDFCFF
+
+
+
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

+
+
+
+
local_cafe
+ Willow-Bark tea + info +
check
+
+
Drink a cup of Willow-Bark tea twice a day.  🕐 18:00
+
+
+
+
eco
+ Arnika + info +
+
+
Apply a dime sized amount of Arnica gel to your lower back for 10 mins.  🕐 18:00
+
+
+

Consent Tiles

+
+ + + + +
+ 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

+
+
+ + +
+
+ + +
+
+ + + Minimum 20 characters required +
+
+

Checkboxes & Switches

+
+
Willow-Bark tea
+
Arnika
+
Warming Pad
+
+
Notifications on +
+
+
Off +
+
+ 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

+
4px - space-1
+
8px - space-2
+
12px - space-3
+
16px - space-4
+
24px - space-6
+
32px - space-8
+
48px - space-12
+ +

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