Initial commit from dev
|
|
@ -0,0 +1,6 @@
|
|||
node_modules/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
api/api
|
||||
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Consultation Prompt
|
||||
|
||||
Handle user input related to medical consultations, appointments, and follow-ups.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
Category: {{CATEGORY}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
For consultations, use `input_config.fields`. If creating a record, use the `entries` array.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "snake_case_identifier",
|
||||
"input_type": "form|checkbox",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [{"key": "...", "type": "...", "label": "..."}]},
|
||||
"entries": [{"value": "...", "data": {...}}]
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Create a `schedule` for future appointments mentioned with a day or date. Default time is "09:00".
|
||||
- Create a follow-up `question` for the scheduled appointment.
|
||||
- A simple past event with no follow-up should have `schedule: null`.
|
||||
- `question` and UI labels must be in {{LANGUAGE}}.
|
||||
- If the user provides data for a new event, populate the `entries` array with a single entry.
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "I went to the dentist"
|
||||
Language: en
|
||||
Category: consultation
|
||||
```json
|
||||
{
|
||||
"question": null,
|
||||
"type": "dentist_visit",
|
||||
"input_type": null,
|
||||
"schedule": null,
|
||||
"input_config": null,
|
||||
"entries": [{"value": "Dentist visit", "data": {"provider": "dentist"}}]
|
||||
}
|
||||
```
|
||||
|
||||
Input: "I have an appointment with my PCP for Friday"
|
||||
Language: en
|
||||
Category: consultation
|
||||
```json
|
||||
{
|
||||
"question": "Did you attend your PCP appointment today?",
|
||||
"type": "pcp_appointment",
|
||||
"input_type": "checkbox",
|
||||
"schedule": [{"days": ["fri"], "time": "09:00"}],
|
||||
"input_config": {"fields": [{"key": "completed", "type": "checkbox", "label": "Yes"}]},
|
||||
"entries": [{"value": "PCP Appointment", "data": {"provider": "PCP", "date": "Friday"}}]
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# Default Prompt
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
Category: {{CATEGORY}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
For complex activities, use `input_config.groups`. For simple forms, use `input_config.fields`.
|
||||
If creating a record, use the `entries` array.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "snake_case_identifier",
|
||||
"input_type": "form|checkbox",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {
|
||||
"fields": [{"key": "...", "type": "...", "label": "..."}],
|
||||
"groups": [{"title": "...", "fields": [{"key": "...", "type": "...", "label": "..."}]}]
|
||||
},
|
||||
"entries": [{"value": "...", "data": {...}}]
|
||||
}
|
||||
```
|
||||
|
||||
## Rules
|
||||
- If the user provides data for a new event, populate the `entries` array.
|
||||
- If the input contains multiple distinct items, create a separate object for each in the `entries` array.
|
||||
- `schedule` should be `null` for one-time past events with no follow-up.
|
||||
- `question` and all UI labels must be in {{LANGUAGE}}.
|
||||
|
||||
## Example (Supplement)
|
||||
|
||||
Input: "I take 2 capsules of Vit D and 500mg of Vit C everyday"
|
||||
Language: en
|
||||
Category: supplement
|
||||
```json
|
||||
{
|
||||
"question": "Did you take your supplements today?",
|
||||
"category": "supplement",
|
||||
"type": "daily_supplements",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {
|
||||
"fields": [
|
||||
{"key": "vitamin_d", "label": "Vitamin D", "type": "checkbox"},
|
||||
{"key": "vitamin_c", "label": "Vitamin C", "type": "checkbox"}
|
||||
]
|
||||
},
|
||||
"entries": [
|
||||
{"value": "2 capsules Vit D", "data": {"supplement": "vitamin d", "amount": 2, "unit": "capsules"}},
|
||||
{"value": "500mg Vit C", "data": {"supplement": "vitamin c", "amount": 500, "unit": "mg"}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# Exercise Prompt
|
||||
|
||||
Extract exercise/activity details.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
For complex activities with multiple parts, use the `input_config.groups` structure to group related fields.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "exercise_type",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Group Title",
|
||||
"fields": [{"key": "...", "type": "...", "label": "..."}]
|
||||
}
|
||||
]
|
||||
},
|
||||
"entries": [{"value": "...", "data": {...}}]
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule format
|
||||
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
|
||||
- `time`: 24h format "08:00", "12:00", "18:00" etc.
|
||||
- Daily = all 7 days
|
||||
|
||||
## Standard fields
|
||||
- `duration`: number (minutes)
|
||||
- `distance`: number (km, miles)
|
||||
- `intensity`: select [Light, Moderate, Vigorous]
|
||||
|
||||
## Rules
|
||||
- If the activity has multiple distinct parts (e.g. left and right leg), create a `group` in `input_config` for each part.
|
||||
- `entries` should be used for recording initial data. If multiple parts, create multiple entries.
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "walked 30 minutes"
|
||||
```json
|
||||
{
|
||||
"question": "Did you walk today?",
|
||||
"type": "walk",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [{"key": "duration", "type": "number", "label": "Duration", "unit": "min"}]},
|
||||
"entries": [{"value": "30 min walk", "data": {"duration": 30, "unit": "min"}}]
|
||||
}
|
||||
```
|
||||
|
||||
Input: "For my daughter, we have a leg trainer exercise we do twice a day. The right leg is for 20 minutes at 40 degrees and speed 4. The left leg is for 20 minutes at 30 degrees and speed 4."
|
||||
```json
|
||||
{
|
||||
"question": "Did you do the leg trainer exercises today?",
|
||||
"category": "exercise",
|
||||
"type": "leg_trainer",
|
||||
"input_type": "form",
|
||||
"schedule": [
|
||||
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "09:00"},
|
||||
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "17:00"}
|
||||
],
|
||||
"input_config": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Right Leg",
|
||||
"fields": [
|
||||
{ "key": "right_leg_duration", "type": "number", "label": "Duration (min)" },
|
||||
{ "key": "right_leg_angle", "type": "number", "label": "Angle (°)" },
|
||||
{ "key": "right_leg_speed", "type": "number", "label": "Speed" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Left Leg",
|
||||
"fields": [
|
||||
{ "key": "left_leg_duration", "type": "number", "label": "Duration (min)" },
|
||||
{ "key": "left_leg_angle", "type": "number", "label": "Angle (°)" },
|
||||
{ "key": "left_leg_speed", "type": "number", "label": "Speed" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Notes",
|
||||
"fields": [
|
||||
{ "key": "notes", "type": "text", "label": "Session Notes" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"value": "Leg trainer session",
|
||||
"data": {
|
||||
"right_leg": { "duration_min": 20, "degrees": 40, "speed": 4 },
|
||||
"left_leg": { "duration_min": 20, "degrees": 30, "speed": 4 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# Family History Prompt
|
||||
|
||||
Extract details about a relative's health condition.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "confirmation question in {{LANGUAGE}}",
|
||||
"type": "relation_type (mother, father, etc)",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [...]},
|
||||
"entry": {"value": "human readable", "data": {...}}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule
|
||||
|
||||
Always `null` for family history — these are one-time records, not recurring prompts.
|
||||
|
||||
## Type mapping (by relation)
|
||||
|
||||
| Relation | type |
|
||||
|----------|------|
|
||||
| mother, mom | mother |
|
||||
| father, dad | father |
|
||||
| grandmother, grandma | grandmother |
|
||||
| grandfather, grandpa | grandfather |
|
||||
| sister | sister |
|
||||
| brother | brother |
|
||||
| aunt | aunt |
|
||||
| uncle | uncle |
|
||||
| son | son |
|
||||
| daughter | daughter |
|
||||
| spouse, husband, wife | spouse |
|
||||
|
||||
## Entry data structure
|
||||
|
||||
```json
|
||||
{
|
||||
"relation": "mother",
|
||||
"relation_detail": "maternal grandmother",
|
||||
"condition": "breast cancer",
|
||||
"age_at_diagnosis": 58,
|
||||
"outcome": "survived",
|
||||
"treatment": "mastectomy + chemo",
|
||||
"current_status": "in remission",
|
||||
"notes": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Include only fields that can be inferred from input.
|
||||
|
||||
## Rules
|
||||
|
||||
- entry.data must be a JSON OBJECT, not a string
|
||||
- schedule is always null (no recurring prompt)
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "My mother had breast cancer at 58, survived"
|
||||
```json
|
||||
{
|
||||
"question": "Mother's breast cancer recorded. Is she still in remission?",
|
||||
"type": "mother",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [{"key": "current_status", "type": "select", "label": "Current status", "options": ["In remission", "Cancer-free", "Recurrence", "Deceased"]}]},
|
||||
"entry": {"value": "Mother: breast cancer at 58, survived", "data": {"relation": "mother", "condition": "breast cancer", "age_at_diagnosis": 58, "outcome": "survived"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "Father has type 2 diabetes"
|
||||
```json
|
||||
{
|
||||
"question": "Father's diabetes recorded. How is it managed?",
|
||||
"type": "father",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [{"key": "management", "type": "select", "label": "Management", "options": ["Diet controlled", "Oral medication", "Insulin", "Unknown"]}]},
|
||||
"entry": {"value": "Father: type 2 diabetes", "data": {"relation": "father", "condition": "type 2 diabetes"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "Maternal grandmother had Alzheimer's"
|
||||
```json
|
||||
{
|
||||
"question": "Grandmother's Alzheimer's recorded. Age of onset?",
|
||||
"type": "grandmother",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [{"key": "age_onset", "type": "number", "label": "Age of onset"}, {"key": "outcome", "type": "select", "label": "Outcome", "options": ["Living with condition", "Deceased"]}]},
|
||||
"entry": {"value": "Maternal grandmother: Alzheimer's", "data": {"relation": "grandmother", "relation_detail": "maternal", "condition": "Alzheimer's"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "Brother is healthy"
|
||||
```json
|
||||
{
|
||||
"question": "Brother recorded as healthy.",
|
||||
"type": "brother",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": []},
|
||||
"entry": {"value": "Brother: healthy", "data": {"relation": "brother", "condition": "healthy"}}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
# Fertility Prompt
|
||||
|
||||
Extract menstrual/fertility cycle details. Also handles new pregnancy announcements.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
Existing Types: {{EXISTING_TYPES}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "fertility_type",
|
||||
"input_type": "form|checkbox",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [...]},
|
||||
"entries": [{"value": "...", "data": {...}}]
|
||||
}
|
||||
```
|
||||
|
||||
## Special Handling for Pregnancy
|
||||
If the user announces a pregnancy (e.g., "I'm pregnant"), do NOT create an `entries` array immediately. The goal is to ask a follow-up question to get more details.
|
||||
- Set `schedule` to `null`.
|
||||
- Set `question` to ask for the estimated due date.
|
||||
- The `input_config` should be a form with a single `date` field.
|
||||
|
||||
## Handling Contradictions (e.g., Period during Pregnancy)
|
||||
If the user reports a period and "pregnancy" is listed in the `Existing Types`, respond with care and offer gentle options.
|
||||
- Set `question` to "I'm so sorry, I know this can be a difficult time. I've noted you're tracking a pregnancy and have logged a new period. How would you like me to update your status?".
|
||||
- Set `category` to `fertility`.
|
||||
- Set `type` to `pregnancy_inconsistency`.
|
||||
- Set `input_type` to `form`.
|
||||
- Set `input_config` to offer choices to update pregnancy status.
|
||||
- Set `schedule` to `null`.
|
||||
- `entries` should be null.
|
||||
|
||||
## Schedule format
|
||||
- Period/cycle tracking is typically daily.
|
||||
- A new pregnancy announcement is a one-time event (`schedule: null`).
|
||||
|
||||
## Fertility types
|
||||
period, menstruation, ovulation, cycle, fertility, pms, cramps, pregnancy
|
||||
|
||||
## Standard fields
|
||||
- flow: select [Spotting, Light, Medium, Heavy]
|
||||
- pain: scale 0-5
|
||||
- mood: select [Good, Okay, Low, Anxious, Irritable]
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "My period started today"
|
||||
Existing Types: (none yet)
|
||||
```json
|
||||
{
|
||||
"question": "How is your period today?",
|
||||
"type": "period",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "flow", "type": "select", "label": "Flow", "options": ["Spotting", "Light", "Medium", "Heavy"]},
|
||||
{"key": "pain", "type": "scale", "label": "Pain (0-5)"}
|
||||
]},
|
||||
"entries": [{"value": "Period started", "data": {"started": true}}]
|
||||
}
|
||||
```
|
||||
|
||||
Input: "I'm pregnant!"
|
||||
Existing Types: (none yet)
|
||||
```json
|
||||
{
|
||||
"question": "Congratulations! What is your estimated due date?",
|
||||
"category": "fertility",
|
||||
"type": "pregnancy",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {
|
||||
"fields": [
|
||||
{
|
||||
"key": "estimated_due_date",
|
||||
"type": "date",
|
||||
"label": "Estimated Due Date"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "I just had my period"
|
||||
Existing Types: - fertility: [pregnancy]
|
||||
```json
|
||||
{
|
||||
"question": "I'm so sorry, I know this can be a difficult time. I've noted you're tracking a pregnancy and have logged a new period. How would you like me to update your status?",
|
||||
"category": "fertility",
|
||||
"type": "pregnancy_inconsistency",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {
|
||||
"fields": [
|
||||
{
|
||||
"key": "pregnancy_status",
|
||||
"type": "select",
|
||||
"label": "Update my status",
|
||||
"options": [
|
||||
"End pregnancy tracking",
|
||||
"This was spotting or something else",
|
||||
"Keep tracking pregnancy"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# History Prompt
|
||||
|
||||
Extract details from a past medical event (user's own history, not documented).
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "friendly confirmation question in {{LANGUAGE}}",
|
||||
"type": "snake_case_identifier",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [...]},
|
||||
"entry": {"value": "human readable", "data": {...}}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule
|
||||
|
||||
Always `null` for history — these are one-time records, not recurring prompts.
|
||||
|
||||
## Type mapping
|
||||
|
||||
| Input pattern | type |
|
||||
|---------------|------|
|
||||
| appendix, appendectomy | appendectomy |
|
||||
| tonsils, tonsillectomy | tonsillectomy |
|
||||
| broke, fracture, broken | fracture |
|
||||
| chickenpox, measles, mumps | childhood_illness |
|
||||
| surgery (general) | past_surgery |
|
||||
| allergy | allergy |
|
||||
| injury | past_injury |
|
||||
|
||||
## Entry data structure
|
||||
|
||||
```json
|
||||
{
|
||||
"condition": "appendectomy",
|
||||
"age": 12,
|
||||
"year": 2005,
|
||||
"body_part": "left arm",
|
||||
"severity": "normal",
|
||||
"complications": "none",
|
||||
"notes": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Include only fields that can be inferred from input.
|
||||
|
||||
## Rules
|
||||
|
||||
- entry.data must be a JSON OBJECT, not a string
|
||||
- schedule is always null (no recurring prompt)
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "I had my appendix removed when I was 12"
|
||||
```json
|
||||
{
|
||||
"question": "Appendectomy recorded. Any complications?",
|
||||
"type": "appendectomy",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [{"key": "complications", "type": "text", "label": "Complications"}]},
|
||||
"entry": {"value": "Appendectomy at age 12", "data": {"condition": "appendectomy", "age": 12}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "Broke my left arm in 2015"
|
||||
```json
|
||||
{
|
||||
"question": "Left arm fracture recorded. How did it heal?",
|
||||
"type": "fracture",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [{"key": "healing", "type": "select", "label": "Healing outcome", "options": ["Full recovery", "Some limitations", "Ongoing issues"]}]},
|
||||
"entry": {"value": "Left arm fracture, 2015", "data": {"condition": "fracture", "body_part": "left arm", "year": 2015}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "had chickenpox as a child"
|
||||
```json
|
||||
{
|
||||
"question": "Chickenpox recorded. Any complications?",
|
||||
"type": "childhood_illness",
|
||||
"input_type": "form",
|
||||
"schedule": null,
|
||||
"input_config": {"fields": [{"key": "complications", "type": "text", "label": "Complications (if any)"}]},
|
||||
"entry": {"value": "Chickenpox in childhood", "data": {"condition": "chickenpox", "age_period": "childhood"}}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Medication Prompt
|
||||
|
||||
Extract medication tracking details.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
**If the user mentions multiple medications, create one entry for each in an `entries` array. The `input_config` for the follow-up prompt should also contain a separate checkbox field for each medication identified.**
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "medication_name_snake_case",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [{"key": "med_name", "type": "checkbox", "label": "Med Name"}]},
|
||||
"entries": [{"value": "...", "data": {...}}]
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule format
|
||||
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
|
||||
- `time`: 24h format "08:00", "12:00", "18:00" etc.
|
||||
- Daily = all 7 days
|
||||
- "once" or one-time event = null (no schedule, no recurring prompt)
|
||||
|
||||
## Rules
|
||||
- `entry.data` must be a JSON OBJECT, not a string.
|
||||
- If multiple medications are mentioned, create a separate entry for each in the `entries` array.
|
||||
- The `input_config.fields` should contain a checkbox for each medication found. The `key` should be the snake_case name of the medication, and the `label` should be the proper name.
|
||||
- If only one medication is mentioned, use the `entries` array with a single element and a single checkbox in `input_config`.
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "took ibuprofen 400mg"
|
||||
```json
|
||||
{
|
||||
"question": "Did you take your ibuprofen?",
|
||||
"type": "ibuprofen",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "ibuprofen", "type": "checkbox", "label": "Ibuprofen"}
|
||||
]},
|
||||
"entries": [{"value": "Ibuprofen 400mg", "data": {"medication": "ibuprofen", "dose": 400, "unit": "mg"}}]
|
||||
}
|
||||
```
|
||||
|
||||
Input: "I took my morning pills: 10mg of Lisinopril and a multivitamin."
|
||||
```json
|
||||
{
|
||||
"question": "Did you take your morning pills?",
|
||||
"type": "morning_pills",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {
|
||||
"fields": [
|
||||
{"key": "lisinopril", "type": "checkbox", "label": "Lisinopril"},
|
||||
{"key": "multivitamin", "type": "checkbox", "label": "Multivitamin"}
|
||||
]
|
||||
},
|
||||
"entries": [
|
||||
{"value": "10mg Lisinopril", "data": {"medication": "lisinopril", "dose": 10, "unit": "mg"}},
|
||||
{"value": "Multivitamin", "data": {"medication": "multivitamin"}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# Nutrition Prompt
|
||||
|
||||
Extract nutrition/dietary details.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
**If the user mentions multiple foods or meals, create one entry for each in an `entries` array.**
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "nutrition_type",
|
||||
"input_type": "form|checkbox",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [...]},
|
||||
"entries": [{"value": "...", "data": {...}}]
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule format
|
||||
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
|
||||
- `time`: 24h format "08:00", "12:00", "18:00" etc.
|
||||
- Daily = all 7 days
|
||||
- "once" or one-time event = null (no schedule, no recurring prompt)
|
||||
|
||||
## Nutrition types
|
||||
coffee, alcohol, water, caffeine, sugar, breakfast, lunch, dinner, snack, fasting
|
||||
|
||||
## Rules
|
||||
- entry.data must be a JSON OBJECT, not a string
|
||||
- If multiple items are mentioned, create a separate entry for each in the `entries` array.
|
||||
- If only one item is mentioned, use the `entries` array with a single element.
|
||||
- For abstinence tracking (e.g., "stopped coffee"), create a prompt to track if they avoided it.
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "I stopped drinking coffee"
|
||||
```json
|
||||
{
|
||||
"question": "Did you have coffee today?",
|
||||
"type": "coffee_abstinence",
|
||||
"input_type": "checkbox",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
|
||||
"input_config": {"fields": [{"key": "had_coffee", "type": "checkbox", "label": "Yes"}]},
|
||||
"entries": [{"value": "Tracking coffee abstinence", "data": {"abstinence": true}}]
|
||||
}
|
||||
```
|
||||
|
||||
Input: "Today for lunch I had a chicken salad and a glass of water, and for dinner I had a steak with a side of potatoes."
|
||||
```json
|
||||
{
|
||||
"question": "What did you eat today?",
|
||||
"type": "daily_food_log",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
|
||||
"input_config": {"fields": [{"key": "food", "type": "text", "label": "Food"}]},
|
||||
"entries": [
|
||||
{"value": "Chicken salad and water for lunch", "data": {"meal": "lunch", "food": "chicken salad", "drink": "water"}},
|
||||
{"value": "Steak and potatoes for dinner", "data": {"meal": "dinner", "food": "steak", "side": "potatoes"}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# Supplement Prompt
|
||||
|
||||
Extract supplement tracking details.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "supplement_name_snake_case",
|
||||
"input_type": "form|checkbox",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [...]},
|
||||
"entry": {"value": "...", "data": {...}}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule format
|
||||
|
||||
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
|
||||
- `time`: 24h format "08:00", "12:00", "18:00" etc.
|
||||
- Daily = all 7 days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
- "with meals" = 3 entries: 08:00, 12:00, 18:00
|
||||
- "twice a day" = 2 entries: 08:00, 20:00
|
||||
- "once" or one-time event = null (no schedule, no recurring prompt)
|
||||
|
||||
## Common supplements
|
||||
|
||||
vitamin_d, vitamin_c, omega_3, fish_oil, magnesium, zinc, iron, probiotics, multivitamin, calcium, b12, folate, melatonin, collagen
|
||||
|
||||
## Standard fields
|
||||
|
||||
- `amount`: number with unit (mg, IU, mcg, tbsp, capsules)
|
||||
|
||||
## Rules
|
||||
|
||||
- entry.data must be a JSON OBJECT, not a string
|
||||
- Only include fields that have values from user input
|
||||
- If one-time (e.g. "got flu shot"), set schedule to null
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "taking omega-3 fish oil"
|
||||
```json
|
||||
{
|
||||
"question": "Did you take your Omega-3 today?",
|
||||
"type": "omega_3",
|
||||
"input_type": "checkbox",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [{"key": "taken", "type": "checkbox", "label": "Taken"}]}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "I take 1 tablespoon of fish oil daily"
|
||||
```json
|
||||
{
|
||||
"question": "Did you take your fish oil today?",
|
||||
"type": "fish_oil",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "amount", "type": "number", "label": "Amount", "unit": "tbsp"}
|
||||
]},
|
||||
"entry": {"value": "1 tbsp fish oil", "data": {"amount": 1}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "vitamin D 2000 IU with breakfast and dinner"
|
||||
```json
|
||||
{
|
||||
"question": "Did you take your Vitamin D?",
|
||||
"type": "vitamin_d",
|
||||
"input_type": "form",
|
||||
"schedule": [
|
||||
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"},
|
||||
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "18:00"}
|
||||
],
|
||||
"input_config": {"fields": [
|
||||
{"key": "amount", "type": "number", "label": "Amount", "unit": "IU"}
|
||||
]},
|
||||
"entry": {"value": "Vitamin D 2000 IU", "data": {"amount": 2000}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "got my flu shot today"
|
||||
```json
|
||||
{
|
||||
"question": null,
|
||||
"type": "flu_shot",
|
||||
"input_type": null,
|
||||
"schedule": null,
|
||||
"input_config": null,
|
||||
"entry": {"value": "Flu shot", "data": {"type": "flu_shot"}}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Symptom Prompt
|
||||
|
||||
Extract symptom details for tracking.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "snake_case_symptom",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [...]},
|
||||
"entry": {"value": "...", "data": {...}}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule format
|
||||
|
||||
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
|
||||
- `time`: 24h format "08:00", "12:00", "18:00" etc.
|
||||
- Daily = all 7 days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
- Symptoms typically track daily until resolved
|
||||
|
||||
## Common symptom types
|
||||
|
||||
headache, migraine, back_pain, knee_pain, joint_pain, fatigue, nausea, dizziness, insomnia, anxiety, cough, congestion, fever, rash, swelling
|
||||
|
||||
## Standard fields for symptoms
|
||||
|
||||
- `severity`: scale 0-5
|
||||
- `duration`: number with unit (hours, days)
|
||||
- `location`: text or select
|
||||
- `triggers`: text
|
||||
- `notes`: text
|
||||
|
||||
## Rules
|
||||
|
||||
- entry.data must be a JSON OBJECT, not a string
|
||||
- Only include fields that have values from user input
|
||||
- IMPORTANT: For injuries (cut finger, twisted ankle), track HEALING not the injury event
|
||||
- Use scale (0-5) for severity/pain, not 1-10
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "headache today"
|
||||
```json
|
||||
{
|
||||
"question": "How is your headache?",
|
||||
"type": "headache",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "severity", "type": "scale", "label": "Severity (0-5)"},
|
||||
{"key": "location", "type": "select", "label": "Location", "options": ["Forehead", "Temples", "Back of head", "One side", "All over"]}
|
||||
]},
|
||||
"entry": {"value": "Headache reported", "data": {"symptom": "headache"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "my knee hurts when climbing stairs"
|
||||
```json
|
||||
{
|
||||
"question": "How is your knee pain today?",
|
||||
"type": "knee_pain",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "severity", "type": "scale", "label": "Pain level (0-5)"},
|
||||
{"key": "trigger", "type": "select", "label": "Worse with", "options": ["Stairs", "Walking", "Standing", "Sitting", "All activities"]}
|
||||
]},
|
||||
"entry": {"value": "Knee pain, worse on stairs", "data": {"symptom": "knee_pain", "trigger": "stairs"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "twisted my ankle yesterday"
|
||||
```json
|
||||
{
|
||||
"question": "How is your ankle healing?",
|
||||
"type": "ankle_injury",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "pain", "type": "scale", "label": "Pain (0-5)"},
|
||||
{"key": "swelling", "type": "select", "label": "Swelling", "options": ["None", "Mild", "Moderate", "Severe"]}
|
||||
]},
|
||||
"entry": {"value": "Twisted ankle", "data": {"injury": "ankle", "type": "twist"}}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# Triage Prompt
|
||||
|
||||
Your most important task is to first determine if the user input is a valid health entry or an invalid query.
|
||||
|
||||
**DO NOT categorize requests for medical advice as 'symptom' or 'question'. They are invalid queries.**
|
||||
|
||||
- If the input is not health-related, respond with: `{"error": "not_health_related"}`
|
||||
- If the input asks for medical advice (e.g., contains "what should I do?", "how do I treat?"), respond with: `{"error": "no_medical_advice"}`
|
||||
|
||||
---
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
|
||||
If, and only if, the input is a valid health entry, classify it into a category and extract basic metadata.
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object below. No explanations, no markdown, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"category": "one of the categories below",
|
||||
"language": "en|nl|de|hi|...",
|
||||
"has_data": true/false,
|
||||
"summary": "brief normalized summary"
|
||||
}
|
||||
```
|
||||
|
||||
## Invalid Query Examples
|
||||
|
||||
Input: "What is the capital of Mexico?"
|
||||
```json
|
||||
{"error": "not_health_related"}
|
||||
```
|
||||
|
||||
Input: "I have a headache, what should I do?"
|
||||
```json
|
||||
{"error": "no_medical_advice"}
|
||||
```
|
||||
|
||||
## Categories
|
||||
|
||||
| Category | Use when | Examples |
|
||||
|----------|----------|----------|
|
||||
| vital | Measurements: weight, blood pressure, temperature, heart rate | "I weigh 80kg", "BP 120/80" |
|
||||
| exercise | Physical activity, workouts, steps | "walked 30 min", "did yoga" |
|
||||
| medication | Taking/tracking medicines | "took ibuprofen", "started metformin" |
|
||||
| supplement | Vitamins, supplements | "taking omega-3", "vitamin D" |
|
||||
| nutrition | Food, drink, dietary habits, abstinence | "stopped coffee", "no alcohol", "ate breakfast" |
|
||||
| fertility | Menstrual cycle, period, ovulation, pregnancy | "period started", "ovulating", "I'm pregnant" |
|
||||
| symptom | Current symptoms, pain, discomfort | "headache today", "knee hurts" |
|
||||
| note | General health notes | "feeling good", "stressed" |
|
||||
| history | YOUR past medical events (no documentation) | "appendectomy at 12", "had chickenpox", "broke arm 2015" |
|
||||
| family_history | Relatives' health conditions | "mother: breast cancer", "father: diabetes" |
|
||||
| surgery | Documented surgical procedures | "scheduled for knee surgery" |
|
||||
| hospitalization | Hospital stays | "admitted for pneumonia" |
|
||||
| consultation | Doctor visits, appointments | "saw cardiologist today" |
|
||||
| diagnosis | Active conditions being managed | "diagnosed with epilepsy" |
|
||||
| device | Medical devices, implants | "VP shunt placed", "pacemaker settings" |
|
||||
| therapy | Therapeutic treatments | "started physical therapy" |
|
||||
| assessment | Evaluations, tests | "developmental assessment scheduled" |
|
||||
| birth | Birth-related data | "born at 32 weeks" |
|
||||
| imaging_finding | Imaging results | "MRI showed..." |
|
||||
| eeg_finding | EEG results | "EEG normal" |
|
||||
| provider | Doctors, hospitals, clinics | "Dr. Smith is my neurologist" |
|
||||
| question | Open questions | "should I worry about..." |
|
||||
|
||||
## Rules
|
||||
|
||||
- `has_data`: true if user provided actual values/facts, false if just describing intent
|
||||
- `summary`: normalize to third person, e.g. "walked 30 minutes" → "30 min walk"
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# Vital Prompt
|
||||
|
||||
Extract vital sign measurements.
|
||||
|
||||
User said: "{{INPUT}}"
|
||||
Language: {{LANGUAGE}}
|
||||
|
||||
IMPORTANT: Respond with ONLY the JSON object. No explanations, no markdown fences, no text before or after.
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "tracking question in {{LANGUAGE}}",
|
||||
"type": "vital_type",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": [...], "time": "HH:MM"}],
|
||||
"input_config": {"fields": [...]},
|
||||
"entry": {"value": "...", "data": {...}}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule format
|
||||
|
||||
- `days`: array of "mon", "tue", "wed", "thu", "fri", "sat", "sun"
|
||||
- `time`: 24h format "08:00", "12:00", "18:00" etc.
|
||||
- Daily = all 7 days: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
|
||||
- "once" or one-time measurement = null (no schedule, no recurring prompt)
|
||||
|
||||
## Vital types and units
|
||||
|
||||
| Type | Fields | Units |
|
||||
|------|--------|-------|
|
||||
| weight | weight | kg, lbs |
|
||||
| blood_pressure | systolic, diastolic | mmHg |
|
||||
| heart_rate | bpm | bpm |
|
||||
| temperature | temp | °C, °F |
|
||||
| blood_glucose | glucose | mg/dL, mmol/L |
|
||||
| oxygen_saturation | spo2 | % |
|
||||
| sleep | hours | hours |
|
||||
| steps | steps | steps |
|
||||
|
||||
## Rules
|
||||
|
||||
- entry.data must be a JSON OBJECT, not a string
|
||||
- Only include fields that have values from user input
|
||||
|
||||
## Examples
|
||||
|
||||
Input: "I weigh 80kg"
|
||||
```json
|
||||
{
|
||||
"question": "What is your weight today?",
|
||||
"type": "weight",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [{"key": "weight", "type": "number", "label": "Weight", "unit": "kg"}]},
|
||||
"entry": {"value": "80 kg", "data": {"weight": 80, "unit": "kg"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "blood pressure 120/80"
|
||||
```json
|
||||
{
|
||||
"question": "What is your blood pressure?",
|
||||
"type": "blood_pressure",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "systolic", "type": "number", "label": "Systolic", "unit": "mmHg"},
|
||||
{"key": "diastolic", "type": "number", "label": "Diastolic", "unit": "mmHg"}
|
||||
]},
|
||||
"entry": {"value": "120/80 mmHg", "data": {"systolic": 120, "diastolic": 80, "unit": "mmHg"}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "slept 7 hours last night"
|
||||
```json
|
||||
{
|
||||
"question": "How many hours did you sleep?",
|
||||
"type": "sleep",
|
||||
"input_type": "form",
|
||||
"schedule": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"}],
|
||||
"input_config": {"fields": [
|
||||
{"key": "hours", "type": "number", "label": "Hours", "unit": "hrs", "step": 0.5},
|
||||
{"key": "quality", "type": "scale", "label": "Quality (0-5)"}
|
||||
]},
|
||||
"entry": {"value": "7 hours sleep", "data": {"hours": 7}}
|
||||
}
|
||||
```
|
||||
|
||||
Input: "check BP morning and evening"
|
||||
```json
|
||||
{
|
||||
"question": "What is your blood pressure?",
|
||||
"type": "blood_pressure",
|
||||
"input_type": "form",
|
||||
"schedule": [
|
||||
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "08:00"},
|
||||
{"days": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"], "time": "20:00"}
|
||||
],
|
||||
"input_config": {"fields": [
|
||||
{"key": "systolic", "type": "number", "label": "Systolic", "unit": "mmHg"},
|
||||
{"key": "diastolic", "type": "number", "label": "Diastolic", "unit": "mmHg"}
|
||||
]}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: android
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: ios
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: linux
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: macos
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: web
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
- platform: windows
|
||||
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
# Flutter Landing Page + i18n Task
|
||||
|
||||
## Goal
|
||||
Match Go version landing page, add i18n (EN, NL, RU first)
|
||||
|
||||
## Visual Changes Needed
|
||||
|
||||
### Layout (match Go version)
|
||||
- [ ] Wrap sections in `.landing-card` style (white bg, border, rounded, padding 48px)
|
||||
- [ ] Hero: centered text, not left-aligned
|
||||
- [ ] Hero text: "inou organizes and shares your health dossier with your AI — securely and privately."
|
||||
- [ ] Hero tagline: "Your health, understood."
|
||||
- [ ] CTA button: "Sign in" (or "Invite a friend" if logged in)
|
||||
|
||||
### Content Sections (3 cards in Go)
|
||||
1. **Hero card** - tagline + CTA
|
||||
2. **"You need AI for your health"** - warm prose style
|
||||
3. **"The challenge"** - Data/Reality pairs format:
|
||||
- "Your MRI has 4,000 slices." / "It was read in 10 minutes."
|
||||
- "Your genome has millions of variants." / "All you learned was your eye color..."
|
||||
- etc.
|
||||
4. **"Why we built this"** - prose paragraphs
|
||||
5. **Trust section** - 4-column grid:
|
||||
- Never used for training
|
||||
- Never shared
|
||||
- Military-grade encryption
|
||||
- Delete anytime
|
||||
|
||||
### CSS Values (from Go)
|
||||
```
|
||||
--bg: #F8F7F6
|
||||
--bg-card: #FFFFFF
|
||||
--border: #E5E2DE
|
||||
--text: #1C1917
|
||||
--text-muted: #78716C
|
||||
--accent: #B45309
|
||||
--accent-hover: #92400E
|
||||
|
||||
.landing-card {
|
||||
padding: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.trust-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 32px;
|
||||
}
|
||||
```
|
||||
|
||||
## i18n Setup
|
||||
|
||||
### Packages
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: ^0.18.0
|
||||
```
|
||||
|
||||
### Structure
|
||||
```
|
||||
lib/
|
||||
├── l10n/
|
||||
│ ├── app_en.arb # English
|
||||
│ ├── app_nl.arb # Dutch
|
||||
│ └── app_ru.arb # Russian
|
||||
└── main.dart # Add localization delegates
|
||||
```
|
||||
|
||||
### Key Strings to Port (landing only)
|
||||
From `lang/en.yaml`:
|
||||
- data_yours: "Your data stays yours"
|
||||
- never_training: "Never used for training"
|
||||
- never_training_desc: "Your images are never used to train AI models."
|
||||
- never_shared: "Never shared"
|
||||
- never_shared_desc: "We never share your data with anyone."
|
||||
- encrypted: "Military-grade encryption"
|
||||
- encrypted_desc: "At rest and in transit..."
|
||||
- delete: "Delete anytime"
|
||||
- delete_desc: "Your data, your control."
|
||||
- get_started: "Get started"
|
||||
|
||||
### Language Switcher
|
||||
- Add to InouHeader
|
||||
- Dropdown with: English, Nederlands, Русский
|
||||
- Store preference (SharedPreferences)
|
||||
|
||||
## Source Files
|
||||
- Go templates: `~/dev/inou/templates/`
|
||||
- Go CSS: `~/dev/inou/static/style.css`
|
||||
- Go translations: `~/dev/inou/lang/*.yaml`
|
||||
|
||||
## Translation Strategy
|
||||
- Port EN strings first (manual)
|
||||
- Use cheap model to translate EN → NL, RU
|
||||
- Human review later
|
||||
|
||||
## Priority
|
||||
1. Match visual layout first
|
||||
2. Add i18n scaffolding
|
||||
3. Port EN strings
|
||||
4. Generate NL, RU translations
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# inou_app
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# Instead of disabling a lint rule for the entire project in the
|
||||
# section below, it can also be suppressed for a single line of code
|
||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.inou.inou_app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.inou.inou_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.debug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="inou_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.inou.inou_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = "../build"
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
**/dgph
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
*.perspectivev3
|
||||
**/*sync/
|
||||
.sconsign.dblite
|
||||
.tags*
|
||||
**/.vagrant/
|
||||
**/DerivedData/
|
||||
Icon?
|
||||
**/Pods/
|
||||
**/.symlinks/
|
||||
profile
|
||||
xcuserdata
|
||||
**/.generated/
|
||||
Flutter/App.framework
|
||||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
||||
Flutter/flutter_export_environment.sh
|
||||
ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -0,0 +1 @@
|
|||
#include "Generated.xcconfig"
|
||||
|
|
@ -0,0 +1,616 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run Script";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
97C147061CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147071CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.inou.inouApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147031CF9000F007C117D /* Debug */,
|
||||
97C147041CF9000F007C117D /* Release */,
|
||||
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
97C147061CF9000F007C117D /* Debug */,
|
||||
97C147071CF9000F007C117D /* Release */,
|
||||
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Icon-App-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Icon-App-1024x1024@1x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchImage@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
|
@ -0,0 +1,5 @@
|
|||
# Launch Screen Assets
|
||||
|
||||
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||
|
||||
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Inou App</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>inou_app</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
output-class: AppLocalizations
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Manages app locale with persistence
|
||||
class LocaleProvider extends ChangeNotifier {
|
||||
static const String _localeKey = 'app_locale';
|
||||
|
||||
Locale _locale = const Locale('en');
|
||||
|
||||
Locale get locale => _locale;
|
||||
|
||||
/// Supported locales
|
||||
static const List<Locale> supportedLocales = [
|
||||
Locale('en'), // English
|
||||
Locale('nl'), // Dutch
|
||||
Locale('ru'), // Russian
|
||||
];
|
||||
|
||||
/// Locale display names
|
||||
static const Map<String, String> localeNames = {
|
||||
'en': 'English',
|
||||
'nl': 'Nederlands',
|
||||
'ru': 'Русский',
|
||||
};
|
||||
|
||||
/// Short codes for display in header
|
||||
static const Map<String, String> localeCodes = {
|
||||
'en': 'EN',
|
||||
'nl': 'NL',
|
||||
'ru': 'RU',
|
||||
};
|
||||
|
||||
LocaleProvider() {
|
||||
_loadLocale();
|
||||
}
|
||||
|
||||
/// Load saved locale from preferences
|
||||
Future<void> _loadLocale() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final localeCode = prefs.getString(_localeKey);
|
||||
if (localeCode != null && supportedLocales.any((l) => l.languageCode == localeCode)) {
|
||||
_locale = Locale(localeCode);
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to default if loading fails
|
||||
debugPrint('Failed to load locale: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Set and persist locale
|
||||
Future<void> setLocale(Locale locale) async {
|
||||
if (!supportedLocales.contains(locale)) return;
|
||||
|
||||
_locale = locale;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_localeKey, locale.languageCode);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to save locale: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current locale name for display
|
||||
String get currentLocaleName => localeNames[_locale.languageCode] ?? 'English';
|
||||
|
||||
/// Get current locale code for header display
|
||||
String get currentLocaleCode => localeCodes[_locale.languageCode] ?? 'EN';
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// Feature screens
|
||||
import 'package:inou_app/features/static/landing_page.dart';
|
||||
import 'package:inou_app/features/static/security_page.dart';
|
||||
import 'package:inou_app/features/static/privacy_page.dart';
|
||||
import 'package:inou_app/features/static/faq_page.dart';
|
||||
import 'package:inou_app/features/static/dpa_page.dart';
|
||||
import 'package:inou_app/features/static/connect_page.dart';
|
||||
import 'package:inou_app/features/static/invite_page.dart';
|
||||
import 'package:inou_app/features/auth/login_page.dart';
|
||||
import 'package:inou_app/features/auth/signup_page.dart';
|
||||
import 'package:inou_app/features/dashboard/dashboard.dart';
|
||||
import 'package:inou_app/design/screens/styleguide_screen.dart';
|
||||
|
||||
/// App route definitions
|
||||
class AppRoutes {
|
||||
// Static pages
|
||||
static const String landing = '/';
|
||||
static const String security = '/security';
|
||||
static const String privacy = '/privacy';
|
||||
static const String faq = '/faq';
|
||||
static const String dpa = '/dpa';
|
||||
static const String connect = '/connect';
|
||||
static const String invite = '/invite';
|
||||
|
||||
// Auth pages
|
||||
static const String login = '/login';
|
||||
static const String signup = '/signup';
|
||||
static const String forgotPassword = '/forgot-password';
|
||||
|
||||
// Authenticated pages (deep pages)
|
||||
static const String dashboard = '/dashboard';
|
||||
static const String dossier = '/dossier/:id';
|
||||
static const String profile = '/profile';
|
||||
|
||||
// Dev
|
||||
static const String styleguide = '/styleguide';
|
||||
}
|
||||
|
||||
/// GoRouter configuration
|
||||
final GoRouter appRouter = GoRouter(
|
||||
initialLocation: '/',
|
||||
routes: [
|
||||
// Static pages
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const LandingPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/security',
|
||||
builder: (context, state) => const SecurityPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/privacy',
|
||||
builder: (context, state) => const PrivacyPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/faq',
|
||||
builder: (context, state) => const FaqPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/dpa',
|
||||
builder: (context, state) => const DpaPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/connect',
|
||||
builder: (context, state) => const ConnectPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/invite',
|
||||
builder: (context, state) => const InvitePage(),
|
||||
),
|
||||
|
||||
// Auth pages
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/signup',
|
||||
builder: (context, state) => const SignupPage(),
|
||||
),
|
||||
|
||||
// Authenticated pages
|
||||
GoRoute(
|
||||
path: '/dashboard',
|
||||
builder: (context, state) => const DashboardPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/dossier/:id',
|
||||
builder: (context, state) {
|
||||
final dossierId = state.pathParameters['id']!;
|
||||
return DossierPage(dossierId: dossierId);
|
||||
},
|
||||
),
|
||||
|
||||
// Dev
|
||||
GoRoute(
|
||||
path: '/styleguide',
|
||||
builder: (context, state) => const StyleguideScreen(),
|
||||
),
|
||||
],
|
||||
errorBuilder: (context, state) => const LandingPage(),
|
||||
);
|
||||
|
||||
/// Legacy route generator for MaterialApp (deprecated, use GoRouter)
|
||||
@Deprecated('Use appRouter with GoRouter instead')
|
||||
Route<dynamic>? generateRoute(RouteSettings settings) {
|
||||
// Keep for backwards compatibility if needed
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => const LandingPage(),
|
||||
settings: settings,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,496 @@
|
|||
/// inou Text Styles & Typography Widgets
|
||||
///
|
||||
/// RULES:
|
||||
/// - Pages MUST use InouText.* styles or widgets
|
||||
/// - NO raw TextStyle() in page code
|
||||
/// - NO fontSize: or fontWeight: in page code
|
||||
///
|
||||
/// Usage:
|
||||
/// Text('Hello', style: InouText.pageTitle)
|
||||
/// InouText.pageTitle('Hello')
|
||||
/// InouText.body('Paragraph', color: InouTheme.textMuted)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'inou_theme.dart';
|
||||
|
||||
/// Typography system for inou
|
||||
///
|
||||
/// Base: 16px (1rem), Sora font, line-height 1.5
|
||||
class InouText {
|
||||
InouText._();
|
||||
|
||||
// ===========================================
|
||||
// FONT FAMILY
|
||||
// ===========================================
|
||||
static const String fontFamily = 'Sora';
|
||||
|
||||
// ===========================================
|
||||
// TEXT STYLES
|
||||
// ===========================================
|
||||
|
||||
/// Page title: 2.5rem (40px), weight 800 (ExtraBold)
|
||||
/// Use for: Main page headings like "Style Guide", "Privacy Policy"
|
||||
static const TextStyle pageTitle = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 40.0, // 2.5 * 16
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: -0.5,
|
||||
height: 1.2,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Hero title: 2.25rem (36px), weight 300 (Light)
|
||||
/// Use for: Large hero text like "Your data. Your rules."
|
||||
static const TextStyle heroTitle = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 36.0, // 2.25 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.2,
|
||||
letterSpacing: -1.08, // -0.03em
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Section title: 1.4rem (22.4px), weight 600 (SemiBold)
|
||||
/// Use for: Section headings like "What we collect"
|
||||
static const TextStyle sectionTitle = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 22.4, // 1.4 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Subsection title: 1.1rem (17.6px), weight 600 (SemiBold)
|
||||
/// Use for: Subsection headings like "Account information"
|
||||
static const TextStyle subsectionTitle = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 17.6, // 1.1 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// H3: 1.125rem (18px), weight 500 (Medium)
|
||||
/// Use for: Tertiary headings
|
||||
static const TextStyle h3 = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 18.0, // 1.125 * 16
|
||||
fontWeight: FontWeight.w500,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Intro text: 1.15rem (18.4px), weight 300 (Light)
|
||||
/// Use for: Introduction paragraphs, larger body text
|
||||
static const TextStyle intro = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 18.4,
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.8,
|
||||
color: InouTheme.textMuted,
|
||||
);
|
||||
|
||||
/// Body light: 1rem (16px), weight 300 (Light)
|
||||
/// Use for: Long-form content, articles, descriptions
|
||||
static const TextStyle bodyLight = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.5,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Body regular: 1rem (16px), weight 400 (Regular)
|
||||
/// Use for: UI labels, default text, buttons
|
||||
static const TextStyle body = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Body small: 0.85rem (13.6px), weight 400 (Regular)
|
||||
/// Use for: Secondary text, captions, helper text
|
||||
static const TextStyle bodySmall = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 13.6, // 0.85 * 16
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Label: 1rem (16px), weight 500 (Medium)
|
||||
/// Use for: Form labels, button text
|
||||
static const TextStyle label = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Label caps: 0.75rem (12px), weight 600, uppercase
|
||||
/// Use for: Category labels like "TEXT BLOCKS", "TYPOGRAPHY SCALE"
|
||||
static const TextStyle labelCaps = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 12.0, // 0.75 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.2, // 0.1em
|
||||
color: InouTheme.textSubtle,
|
||||
);
|
||||
|
||||
/// Logo: 1.75rem (28px), weight 700 (Bold)
|
||||
/// Use for: "inou" in header
|
||||
static const TextStyle logo = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 28.0, // 1.75 * 16
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.56, // -0.02em
|
||||
);
|
||||
|
||||
/// Logo light: 1.75rem (28px), weight 300 (Light)
|
||||
/// Use for: "health" in header
|
||||
static const TextStyle logoLight = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 28.0, // 1.75 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: -0.56, // -0.02em
|
||||
);
|
||||
|
||||
/// Logo tagline: 0.95rem (15.2px), weight 300 (Light)
|
||||
/// Use for: "ai answers for you" tagline
|
||||
static const TextStyle logoTagline = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 15.2, // 0.95 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 0.608, // 0.04em
|
||||
color: InouTheme.textMuted,
|
||||
);
|
||||
|
||||
/// Nav item: 1rem (16px), weight 400 (Regular)
|
||||
/// Use for: Navigation menu items
|
||||
static const TextStyle nav = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Nav item active: 1rem (16px), weight 600 (SemiBold)
|
||||
/// Use for: Active navigation menu items
|
||||
static const TextStyle navActive = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: InouTheme.accent,
|
||||
);
|
||||
|
||||
/// Mono: SF Mono, 0.85rem (13.6px)
|
||||
/// Use for: Code, technical data, IDs
|
||||
static const TextStyle mono = TextStyle(
|
||||
fontFamily: 'SF Mono',
|
||||
fontFamilyFallback: ['Monaco', 'Consolas', 'monospace'],
|
||||
fontSize: 13.6, // 0.85 * 16
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Profile name: 1.25rem (20px), weight 600 (SemiBold)
|
||||
/// Use for: User names in profile cards
|
||||
static const TextStyle profileName = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 20.0, // 1.25 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Badge: 1rem (16px), weight 500 (Medium)
|
||||
/// Use for: Badge/pill text
|
||||
static const TextStyle badge = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
/// Button: 1rem (16px), weight 500 (Medium)
|
||||
/// Use for: Button text
|
||||
static const TextStyle button = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
/// Input: 1rem (16px), weight 400 (Regular)
|
||||
/// Use for: Text input fields
|
||||
static const TextStyle input = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.text,
|
||||
);
|
||||
|
||||
/// Input placeholder: 1rem (16px), weight 400 (Regular)
|
||||
/// Use for: Placeholder text in inputs
|
||||
static const TextStyle inputPlaceholder = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.textSubtle,
|
||||
);
|
||||
|
||||
/// Error text: 0.85rem (13.6px), weight 400 (Regular)
|
||||
/// Use for: Form validation errors
|
||||
static const TextStyle error = TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: 13.6,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.danger,
|
||||
);
|
||||
|
||||
/// Link: inherits size, weight 400, accent color
|
||||
/// Use for: Inline links
|
||||
static TextStyle link({double? fontSize}) => TextStyle(
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize ?? 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: InouTheme.accent,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: InouTheme.accent,
|
||||
);
|
||||
|
||||
// ===========================================
|
||||
// CONVENIENCE WIDGETS
|
||||
// ===========================================
|
||||
|
||||
/// Page title widget
|
||||
static Widget pageTitleText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: pageTitle.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Hero title widget
|
||||
static Widget heroTitleText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: heroTitle.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section title widget
|
||||
static Widget sectionTitleText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: sectionTitle.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Subsection title widget
|
||||
static Widget subsectionTitleText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: subsectionTitle.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Body text widget
|
||||
static Widget bodyText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
int? maxLines,
|
||||
TextOverflow? overflow,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: body.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
maxLines: maxLines,
|
||||
overflow: overflow,
|
||||
);
|
||||
}
|
||||
|
||||
/// Body light text widget (for long-form)
|
||||
static Widget bodyLightText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: bodyLight.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Intro text widget
|
||||
static Widget introText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: intro.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Label caps widget (auto-uppercases)
|
||||
static Widget labelCapsText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text.toUpperCase(),
|
||||
style: labelCaps.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Mono text widget
|
||||
static Widget monoText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: mono.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Small text widget
|
||||
static Widget smallText(
|
||||
String text, {
|
||||
Color? color,
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: bodySmall.copyWith(color: color),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
/// Error text widget
|
||||
static Widget errorText(
|
||||
String text, {
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text(
|
||||
text,
|
||||
style: error,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// RICH TEXT HELPERS
|
||||
// ===========================================
|
||||
|
||||
/// Build rich text with multiple styled spans
|
||||
static Widget rich(
|
||||
List<InlineSpan> children, {
|
||||
TextAlign? textAlign,
|
||||
}) {
|
||||
return Text.rich(
|
||||
TextSpan(children: children),
|
||||
textAlign: textAlign,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Styled text spans for use with InouText.rich()
|
||||
class InouSpan {
|
||||
InouSpan._();
|
||||
|
||||
/// Accent colored text
|
||||
static TextSpan accent(String text, {TextStyle? baseStyle}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.accent),
|
||||
);
|
||||
}
|
||||
|
||||
/// Muted colored text
|
||||
static TextSpan muted(String text, {TextStyle? baseStyle}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.textMuted),
|
||||
);
|
||||
}
|
||||
|
||||
/// Subtle colored text
|
||||
static TextSpan subtle(String text, {TextStyle? baseStyle}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(color: InouTheme.textSubtle),
|
||||
);
|
||||
}
|
||||
|
||||
/// Bold text
|
||||
static TextSpan bold(String text, {TextStyle? baseStyle}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w700),
|
||||
);
|
||||
}
|
||||
|
||||
/// SemiBold text
|
||||
static TextSpan semiBold(String text, {TextStyle? baseStyle}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w600),
|
||||
);
|
||||
}
|
||||
|
||||
/// Light text
|
||||
static TextSpan light(String text, {TextStyle? baseStyle}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(fontWeight: FontWeight.w300),
|
||||
);
|
||||
}
|
||||
|
||||
/// Plain text (default style)
|
||||
static TextSpan plain(String text, {TextStyle? style}) {
|
||||
return TextSpan(text: text, style: style);
|
||||
}
|
||||
|
||||
/// Link text
|
||||
static TextSpan link(
|
||||
String text, {
|
||||
VoidCallback? onTap,
|
||||
TextStyle? baseStyle,
|
||||
}) {
|
||||
return TextSpan(
|
||||
text: text,
|
||||
style: (baseStyle ?? InouText.body).copyWith(
|
||||
color: InouTheme.accent,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: InouTheme.accent,
|
||||
),
|
||||
// Note: For tap handling, wrap in GestureDetector or use url_launcher
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
/// inou Design System
|
||||
/// Source: https://inou.com/static/style.css
|
||||
/// Base: 16px, Sora font, line-height 1.5
|
||||
///
|
||||
/// Using LOCAL Sora font (fonts/Sora-*.ttf), not google_fonts package.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InouTheme {
|
||||
InouTheme._();
|
||||
|
||||
// Font family - local asset
|
||||
static const String _fontFamily = 'Sora';
|
||||
|
||||
// ===========================================
|
||||
// COLORS (from :root CSS variables)
|
||||
// ===========================================
|
||||
static const Color bg = Color(0xFFF8F7F6); // --bg
|
||||
static const Color bgCard = Color(0xFFFFFFFF); // --bg-card
|
||||
static const Color border = Color(0xFFE5E2DE); // --border
|
||||
static const Color borderHover = Color(0xFFC4BFB8); // --border-hover
|
||||
static const Color text = Color(0xFF1C1917); // --text
|
||||
static const Color textMuted = Color(0xFF78716C); // --text-muted
|
||||
static const Color textSubtle = Color(0xFFA8A29E); // --text-subtle
|
||||
static const Color accent = Color(0xFFB45309); // --accent
|
||||
static const Color accentHover = Color(0xFF92400E); // --accent-hover
|
||||
static const Color accentLight = Color(0xFFFEF3C7); // --accent-light
|
||||
static const Color danger = Color(0xFFDC2626); // --danger
|
||||
static const Color dangerLight = Color(0xFFFEF2F2); // --danger-light
|
||||
static const Color success = Color(0xFF059669); // --success
|
||||
static const Color successLight = Color(0xFFECFDF5); // --success-light
|
||||
|
||||
// Indicator colors (data sections)
|
||||
static const Color indicatorImaging = Color(0xFFB45309);
|
||||
static const Color indicatorLabs = Color(0xFF059669);
|
||||
static const Color indicatorUploads = Color(0xFF6366F1);
|
||||
static const Color indicatorVitals = Color(0xFFEC4899);
|
||||
static const Color indicatorMedications = Color(0xFF8B5CF6);
|
||||
static const Color indicatorRecords = Color(0xFF06B6D4);
|
||||
static const Color indicatorJournal = Color(0xFFF59E0B);
|
||||
static const Color indicatorPrivacy = Color(0xFF64748B);
|
||||
static const Color indicatorGenetics = Color(0xFF10B981);
|
||||
|
||||
// Message border colors
|
||||
static const Color errorBorder = Color(0xFFFECACA); // #FECACA
|
||||
static const Color infoBorder = Color(0xFFFDE68A); // #FDE68A
|
||||
static const Color successBorder = Color(0xFFA7F3D0); // #A7F3D0
|
||||
|
||||
// ===========================================
|
||||
// SPACING
|
||||
// ===========================================
|
||||
static const double spaceXs = 4.0;
|
||||
static const double spaceSm = 8.0;
|
||||
static const double spaceMd = 12.0;
|
||||
static const double spaceLg = 16.0;
|
||||
static const double spaceXl = 24.0;
|
||||
static const double spaceXxl = 32.0;
|
||||
static const double spaceXxxl = 48.0;
|
||||
|
||||
// ===========================================
|
||||
// BORDER RADIUS
|
||||
// ===========================================
|
||||
static const double radiusSm = 4.0;
|
||||
static const double radiusMd = 6.0;
|
||||
static const double radiusLg = 8.0;
|
||||
static const double radiusXl = 12.0;
|
||||
static BorderRadius get borderRadiusSm => BorderRadius.circular(radiusSm);
|
||||
static BorderRadius get borderRadiusMd => BorderRadius.circular(radiusMd);
|
||||
static BorderRadius get borderRadiusLg => BorderRadius.circular(radiusLg);
|
||||
|
||||
// ===========================================
|
||||
// LAYOUT
|
||||
// ===========================================
|
||||
static const double maxWidth = 1200.0;
|
||||
static const double maxWidthNarrow = 800.0;
|
||||
static const double maxWidthForm = 360.0;
|
||||
|
||||
// ===========================================
|
||||
// TYPOGRAPHY
|
||||
// CSS base: body { font-size: 16px; line-height: 1.5; font-weight: 400; }
|
||||
// All rem values calculated as: rem * 16
|
||||
// ===========================================
|
||||
|
||||
// h1: 2.25rem (36px), weight 300, line-height 1.2, letter-spacing -0.03em
|
||||
static TextStyle get h1 => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 36.0, // 2.25 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.2,
|
||||
letterSpacing: -0.03 * 36.0, // -0.03em
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Page title (styleguide): 2.5rem (40px), weight 700
|
||||
static TextStyle get pageTitle => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 40.0, // 2.5 * 16
|
||||
fontWeight: FontWeight.w800, // ExtraBold
|
||||
letterSpacing: -0.5, // Tighter tracking to match CSS
|
||||
height: 1.2, // Line height to match CSS defaults
|
||||
color: text,
|
||||
);
|
||||
|
||||
// h2: 1.5rem (24px), weight 300, letter-spacing -0.02em
|
||||
static TextStyle get h2 => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 24.0, // 1.5 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: -0.02 * 24.0, // -0.02em
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Section title (styleguide): 1.4rem (22.4px), weight 600
|
||||
static TextStyle get sectionTitle => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 22.4, // 1.4 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// h3: 1.125rem (18px), weight 500
|
||||
static TextStyle get h3 => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 18.0, // 1.125 * 16
|
||||
fontWeight: FontWeight.w500,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Subsection title (styleguide): 1.1rem (17.6px), weight 600
|
||||
static TextStyle get subsectionTitle => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 17.6, // 1.1 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Intro text: 1.15rem (18.4px), weight 300
|
||||
static TextStyle get intro => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 18.4, // 1.15 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.8,
|
||||
color: textMuted,
|
||||
);
|
||||
|
||||
// Body light (long-form): 1rem (16px), weight 300
|
||||
static TextStyle get bodyLight => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w300,
|
||||
height: 1.5,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Body regular (UI labels): 1rem (16px), weight 400
|
||||
static TextStyle get body => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.5,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Small text: 0.85rem (13.6px), weight 400
|
||||
static TextStyle get bodySmall => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 13.6, // 0.85 * 16
|
||||
fontWeight: FontWeight.w400,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Label/Category: 0.75rem (12px), weight 600, uppercase, letter-spacing 0.1em
|
||||
static TextStyle get labelCaps => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 12.0, // 0.75 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.1 * 12.0, // 0.1em
|
||||
color: textSubtle,
|
||||
);
|
||||
|
||||
// Button/label: 1rem (16px), weight 500
|
||||
static TextStyle get label => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: text,
|
||||
);
|
||||
static TextStyle get labelLarge => label; // alias
|
||||
|
||||
// Badge text: 1rem (16px), weight 500
|
||||
static TextStyle get badge => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
static TextStyle get badgeText => badge; // alias
|
||||
|
||||
// Logo: 1.75rem (28px), weight 700, letter-spacing -0.02em
|
||||
static TextStyle get logo => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 28.0, // 1.75 * 16
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: -0.02 * 28.0, // -0.02em
|
||||
);
|
||||
|
||||
// Logo tagline: 0.95rem (15.2px), weight 300, letter-spacing 0.04em
|
||||
static TextStyle get logoTagline => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 15.2, // 0.95 * 16
|
||||
fontWeight: FontWeight.w300,
|
||||
letterSpacing: 0.04 * 15.2, // 0.04em
|
||||
color: textMuted,
|
||||
);
|
||||
|
||||
// Mono: SF Mono, 0.85rem (13.6px)
|
||||
static TextStyle get mono => const TextStyle(
|
||||
fontFamily: 'SF Mono',
|
||||
fontFamilyFallback: ['Monaco', 'Consolas', 'monospace'],
|
||||
fontSize: 13.6, // 0.85 * 16
|
||||
fontWeight: FontWeight.w400,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// Profile card h3: 1.25rem (20px)
|
||||
static TextStyle get profileName => const TextStyle(
|
||||
fontFamily: _fontFamily,
|
||||
fontSize: 20.0, // 1.25 * 16
|
||||
fontWeight: FontWeight.w600,
|
||||
color: text,
|
||||
);
|
||||
|
||||
// ===========================================
|
||||
// THEME DATA
|
||||
// ===========================================
|
||||
static ThemeData get light => ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
fontFamily: _fontFamily,
|
||||
scaffoldBackgroundColor: bg,
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: accent,
|
||||
onPrimary: Colors.white,
|
||||
secondary: accentLight,
|
||||
onSecondary: accent,
|
||||
surface: bgCard,
|
||||
onSurface: text,
|
||||
error: danger,
|
||||
onError: Colors.white,
|
||||
outline: border,
|
||||
),
|
||||
textTheme: TextTheme(
|
||||
displayLarge: pageTitle,
|
||||
displayMedium: h1,
|
||||
headlineMedium: sectionTitle,
|
||||
headlineSmall: subsectionTitle,
|
||||
bodyLarge: body,
|
||||
bodyMedium: body,
|
||||
bodySmall: bodySmall,
|
||||
labelLarge: label,
|
||||
labelSmall: labelCaps,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: bg,
|
||||
foregroundColor: text,
|
||||
elevation: 0,
|
||||
centerTitle: false,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
color: bgCard,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadiusLg,
|
||||
side: BorderSide(color: border),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: accent,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: EdgeInsets.symmetric(horizontal: spaceLg, vertical: spaceMd),
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadiusMd),
|
||||
textStyle: label,
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: text,
|
||||
side: BorderSide(color: border),
|
||||
padding: EdgeInsets.symmetric(horizontal: spaceLg, vertical: spaceMd),
|
||||
shape: RoundedRectangleBorder(borderRadius: borderRadiusMd),
|
||||
textStyle: label,
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: bgCard,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: borderRadiusMd,
|
||||
borderSide: BorderSide(color: border),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadiusMd,
|
||||
borderSide: BorderSide(color: border),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: borderRadiusMd,
|
||||
borderSide: BorderSide(color: accent, width: 1),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: spaceMd, vertical: spaceMd),
|
||||
),
|
||||
dividerTheme: DividerThemeData(
|
||||
color: border,
|
||||
thickness: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// AUTO-GENERATED widget — matches web .badge
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inou_app/design/inou_theme.dart';
|
||||
import 'package:inou_app/design/inou_text.dart';
|
||||
|
||||
enum BadgeVariant { normal, care, comingSoon, processing }
|
||||
|
||||
class InouBadge extends StatelessWidget {
|
||||
final String text;
|
||||
final BadgeVariant variant;
|
||||
|
||||
const InouBadge({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.variant = BadgeVariant.normal,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = _getStyle();
|
||||
final isUppercase = variant == BadgeVariant.comingSoon;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), // 2v, 8h per styleguide
|
||||
decoration: BoxDecoration(
|
||||
color: style.background,
|
||||
borderRadius: BorderRadius.circular(InouTheme.radiusSm),
|
||||
),
|
||||
child: Text(
|
||||
isUppercase ? text.toUpperCase() : text,
|
||||
style: InouTheme.badgeText.copyWith(
|
||||
fontSize: variant == BadgeVariant.comingSoon ? 10 : 15, // 15px (1rem) per styleguide
|
||||
color: style.foreground,
|
||||
letterSpacing: isUppercase ? 0.5 : 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_BadgeStyle _getStyle() {
|
||||
switch (variant) {
|
||||
case BadgeVariant.normal:
|
||||
return _BadgeStyle(
|
||||
background: InouTheme.accentLight,
|
||||
foreground: InouTheme.accent,
|
||||
);
|
||||
case BadgeVariant.care:
|
||||
return _BadgeStyle(
|
||||
background: InouTheme.successLight,
|
||||
foreground: InouTheme.success,
|
||||
);
|
||||
case BadgeVariant.comingSoon:
|
||||
return _BadgeStyle(
|
||||
background: InouTheme.bg,
|
||||
foreground: InouTheme.textMuted,
|
||||
);
|
||||
case BadgeVariant.processing:
|
||||
return _BadgeStyle(
|
||||
background: InouTheme.accentLight,
|
||||
foreground: InouTheme.accent,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BadgeStyle {
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
|
||||
_BadgeStyle({required this.background, required this.foreground});
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// AUTO-GENERATED widget — matches web .btn
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inou_app/design/inou_theme.dart';
|
||||
import 'package:inou_app/design/inou_text.dart';
|
||||
|
||||
enum ButtonVariant { primary, secondary, danger }
|
||||
enum ButtonSize { regular, small }
|
||||
|
||||
class InouButton extends StatelessWidget {
|
||||
final String text;
|
||||
final ButtonVariant variant;
|
||||
final ButtonSize size;
|
||||
final bool fullWidth;
|
||||
final VoidCallback? onPressed;
|
||||
final Widget? icon;
|
||||
|
||||
const InouButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.variant = ButtonVariant.primary,
|
||||
this.size = ButtonSize.regular,
|
||||
this.fullWidth = false,
|
||||
this.onPressed,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSmall = size == ButtonSize.small;
|
||||
final padding = isSmall
|
||||
? const EdgeInsets.symmetric(horizontal: 12, vertical: 6)
|
||||
: const EdgeInsets.symmetric(horizontal: 18, vertical: 10);
|
||||
|
||||
final style = _getStyle();
|
||||
|
||||
Widget button = TextButton(
|
||||
onPressed: onPressed,
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: style.background,
|
||||
foregroundColor: style.foreground,
|
||||
padding: padding,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: InouTheme.borderRadiusMd,
|
||||
side: style.border,
|
||||
),
|
||||
textStyle: InouText.label.copyWith(
|
||||
fontSize: 15, // Always 15px (1rem) for both sizes per styleguide
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
icon!,
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Text(text),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (fullWidth) {
|
||||
button = SizedBox(width: double.infinity, child: button);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
_ButtonStyle _getStyle() {
|
||||
switch (variant) {
|
||||
case ButtonVariant.primary:
|
||||
return _ButtonStyle(
|
||||
background: InouTheme.accent,
|
||||
foreground: Colors.white,
|
||||
border: BorderSide.none,
|
||||
);
|
||||
case ButtonVariant.secondary:
|
||||
return _ButtonStyle(
|
||||
background: InouTheme.bgCard,
|
||||
foreground: InouTheme.text,
|
||||
border: const BorderSide(color: InouTheme.border),
|
||||
);
|
||||
case ButtonVariant.danger:
|
||||
return _ButtonStyle(
|
||||
background: InouTheme.dangerLight,
|
||||
foreground: InouTheme.danger,
|
||||
border: BorderSide(color: InouTheme.danger.withOpacity(0.3)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ButtonStyle {
|
||||
final Color background;
|
||||
final Color foreground;
|
||||
final BorderSide border;
|
||||
|
||||
_ButtonStyle({
|
||||
required this.background,
|
||||
required this.foreground,
|
||||
required this.border,
|
||||
});
|
||||
}
|
||||
|
||||
/// Icon button (matches .btn-icon)
|
||||
class InouIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
final Color? color;
|
||||
|
||||
const InouIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: onPressed,
|
||||
color: color ?? InouTheme.textSubtle,
|
||||
iconSize: 20,
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
// AUTO-GENERATED widget — matches web .data-card
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inou_app/design/inou_theme.dart';
|
||||
import 'package:inou_app/design/inou_text.dart';
|
||||
import 'package:inou_app/design/widgets/inou_badge.dart';
|
||||
import 'package:inou_app/design/widgets/inou_button.dart';
|
||||
|
||||
/// Data card with colored indicator bar
|
||||
class InouCard extends StatelessWidget {
|
||||
final String? title;
|
||||
final String? subtitle;
|
||||
final Color indicatorColor;
|
||||
final Widget? trailing;
|
||||
final Widget? child;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const InouCard({
|
||||
super.key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.indicatorColor = InouTheme.accent,
|
||||
this.trailing,
|
||||
this.child,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: InouTheme.spaceLg),
|
||||
decoration: BoxDecoration(
|
||||
color: InouTheme.bgCard,
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
border: Border.all(color: InouTheme.border),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (title != null)
|
||||
_Header(
|
||||
title: title!,
|
||||
subtitle: subtitle,
|
||||
indicatorColor: indicatorColor,
|
||||
trailing: trailing,
|
||||
),
|
||||
if (child != null) child!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Header extends StatelessWidget {
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Color indicatorColor;
|
||||
final Widget? trailing;
|
||||
|
||||
const _Header({
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
required this.indicatorColor,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(InouTheme.spaceLg),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: InouTheme.border),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: indicatorColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: InouTheme.spaceMd),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: InouText.labelCaps,
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple card without indicator
|
||||
class InouSimpleCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final EdgeInsets? padding;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const InouSimpleCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.padding,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final card = Container(
|
||||
padding: padding ?? const EdgeInsets.all(InouTheme.spaceLg),
|
||||
decoration: BoxDecoration(
|
||||
color: InouTheme.bgCard,
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
border: Border.all(color: InouTheme.border),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
child: card,
|
||||
);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
||||
/// Profile card for dashboard
|
||||
class InouProfileCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String? role;
|
||||
final String? dob;
|
||||
final String? sex;
|
||||
final List<ProfileStat> stats;
|
||||
final bool isCare;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onEdit;
|
||||
|
||||
const InouProfileCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
this.role,
|
||||
this.dob,
|
||||
this.sex,
|
||||
this.stats = const [],
|
||||
this.isCare = false,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: InouTheme.bgCard,
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
border: Border.all(color: InouTheme.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(name, style: InouText.h3),
|
||||
),
|
||||
if (onEdit != null)
|
||||
GestureDetector(
|
||||
onTap: onEdit,
|
||||
child: Text('✎', style: TextStyle(color: InouTheme.textMuted)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
role ?? 'you',
|
||||
style: InouText.bodySmall.copyWith(color: InouTheme.textSubtle),
|
||||
),
|
||||
if (isCare) ...[
|
||||
const SizedBox(width: 8),
|
||||
const InouBadge(text: 'care', variant: BadgeVariant.care),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (dob != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Born: $dob${sex != null ? ' · $sex' : ''}',
|
||||
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
|
||||
),
|
||||
],
|
||||
if (stats.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: stats.map((s) => _StatChip(stat: s)).toList(),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
InouButton(
|
||||
text: 'View',
|
||||
size: ButtonSize.small,
|
||||
onPressed: onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileStat {
|
||||
final String emoji;
|
||||
final String label;
|
||||
|
||||
const ProfileStat(this.emoji, this.label);
|
||||
}
|
||||
|
||||
class _StatChip extends StatelessWidget {
|
||||
final ProfileStat stat;
|
||||
|
||||
const _StatChip({required this.stat});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
'${stat.emoji} ${stat.label}',
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
fontSize: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add card (dashed border)
|
||||
class InouAddCard extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const InouAddCard({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: InouTheme.borderRadiusLg,
|
||||
border: Border.all(
|
||||
color: InouTheme.border,
|
||||
width: 2,
|
||||
style: BorderStyle.solid, // Note: Flutter doesn't support dashed directly
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'+',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
color: InouTheme.accent,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: InouText.body.copyWith(color: InouTheme.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
// AUTO-GENERATED widget — matches web .data-row
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:inou_app/design/inou_theme.dart';
|
||||
import 'package:inou_app/design/inou_text.dart';
|
||||
|
||||
/// Expandable data row (for imaging, labs, etc.)
|
||||
class InouDataRow extends StatefulWidget {
|
||||
final String label;
|
||||
final String? meta;
|
||||
final String? date;
|
||||
final String? value;
|
||||
final bool isExpandable;
|
||||
final List<Widget>? children;
|
||||
final Widget? leading;
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onTap;
|
||||
final bool initiallyExpanded;
|
||||
|
||||
const InouDataRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.meta,
|
||||
this.date,
|
||||
this.value,
|
||||
this.isExpandable = false,
|
||||
this.children,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.initiallyExpanded = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InouDataRow> createState() => _InouDataRowState();
|
||||
}
|
||||
|
||||
class _InouDataRowState extends State<InouDataRow> {
|
||||
late bool _expanded;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_expanded = widget.initiallyExpanded;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: widget.isExpandable
|
||||
? () => setState(() => _expanded = !_expanded)
|
||||
: widget.onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: InouTheme.spaceLg,
|
||||
vertical: InouTheme.spaceMd,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: InouTheme.border,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.isExpandable)
|
||||
SizedBox(
|
||||
width: 20,
|
||||
child: Text(
|
||||
_expanded ? '−' : '+',
|
||||
style: TextStyle(
|
||||
color: InouTheme.textMuted,
|
||||
fontSize: 14,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (widget.leading == null)
|
||||
const SizedBox(width: 32),
|
||||
if (widget.leading != null) ...[
|
||||
widget.leading!,
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.label,
|
||||
style: InouText.body.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.value != null)
|
||||
Text(
|
||||
widget.value!,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SF Mono',
|
||||
fontSize: 13,
|
||||
color: InouTheme.text,
|
||||
),
|
||||
),
|
||||
if (widget.meta != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
widget.meta!,
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.date != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
widget.date!,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SF Mono',
|
||||
fontSize: 12,
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
widget.trailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_expanded && widget.children != null)
|
||||
Container(
|
||||
color: InouTheme.bg,
|
||||
child: Column(children: widget.children!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Child row (indented)
|
||||
class InouChildRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String? value;
|
||||
final String? meta;
|
||||
final Widget? trailing;
|
||||
final Color? valueColor;
|
||||
|
||||
const InouChildRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
this.value,
|
||||
this.meta,
|
||||
this.trailing,
|
||||
this.valueColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: InouTheme.spaceLg,
|
||||
vertical: InouTheme.spaceMd,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: InouTheme.border,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: InouTheme.spaceXxxl), // 48px indent per styleguide
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: InouText.body,
|
||||
),
|
||||
),
|
||||
if (value != null)
|
||||
Text(
|
||||
value!,
|
||||
style: TextStyle(
|
||||
fontFamily: 'SF Mono',
|
||||
fontSize: 13,
|
||||
color: valueColor ?? InouTheme.text,
|
||||
),
|
||||
),
|
||||
if (meta != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
meta!,
|
||||
style: InouText.bodySmall.copyWith(color: InouTheme.textMuted),
|
||||
),
|
||||
],
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
trailing!,
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon for notes/vitals
|
||||
class InouNoteIcon extends StatelessWidget {
|
||||
final String emoji;
|
||||
final Color color;
|
||||
|
||||
const InouNoteIcon({
|
||||
super.key,
|
||||
required this.emoji,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(emoji, style: const TextStyle(fontSize: 16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:inou_app/design/inou_theme.dart';
|
||||
import 'package:inou_app/design/inou_text.dart';
|
||||
|
||||
/// Footer link group
|
||||
class FooterLinkGroup {
|
||||
final String title;
|
||||
final List<FooterLink> links;
|
||||
|
||||
const FooterLinkGroup({
|
||||
required this.title,
|
||||
required this.links,
|
||||
});
|
||||
}
|
||||
|
||||
/// Individual footer link
|
||||
class FooterLink {
|
||||
final String label;
|
||||
final String route;
|
||||
final bool isExternal;
|
||||
|
||||
const FooterLink({
|
||||
required this.label,
|
||||
required this.route,
|
||||
this.isExternal = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// inou Footer - responsive, matches web design
|
||||
class InouFooter extends StatelessWidget {
|
||||
final List<FooterLinkGroup> linkGroups;
|
||||
final Function(FooterLink)? onLinkTap;
|
||||
final String? copyrightText;
|
||||
|
||||
const InouFooter({
|
||||
super.key,
|
||||
this.linkGroups = const [],
|
||||
this.onLinkTap,
|
||||
this.copyrightText,
|
||||
});
|
||||
|
||||
static final defaultLinkGroups = [
|
||||
const FooterLinkGroup(
|
||||
title: 'Product',
|
||||
links: [
|
||||
FooterLink(label: 'Features', route: '/features'),
|
||||
FooterLink(label: 'Security', route: '/security'),
|
||||
FooterLink(label: 'FAQ', route: '/faq'),
|
||||
],
|
||||
),
|
||||
const FooterLinkGroup(
|
||||
title: 'Legal',
|
||||
links: [
|
||||
FooterLink(label: 'Privacy', route: '/privacy'),
|
||||
FooterLink(label: 'Terms', route: '/terms'),
|
||||
FooterLink(label: 'DPA', route: '/dpa'),
|
||||
],
|
||||
),
|
||||
const FooterLinkGroup(
|
||||
title: 'Connect',
|
||||
links: [
|
||||
FooterLink(label: 'Contact', route: '/contact'),
|
||||
FooterLink(label: 'Invite a friend', route: '/invite'),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isMobile = screenWidth < 768;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: InouTheme.bgCard,
|
||||
border: Border(
|
||||
top: BorderSide(color: InouTheme.border, width: 1),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isMobile ? 16 : 24,
|
||||
vertical: isMobile ? 32 : 48,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
isMobile
|
||||
? _buildMobileLinks(context)
|
||||
: _buildDesktopLinks(context),
|
||||
const SizedBox(height: 32),
|
||||
_buildBottomBar(context, isMobile),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopLinks(BuildContext context) {
|
||||
final groups = linkGroups.isEmpty ? defaultLinkGroups : linkGroups;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo and tagline
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'inou',
|
||||
style: InouText.h3.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: InouTheme.accent,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Your health, understood.',
|
||||
style: InouText.body.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Link groups
|
||||
for (final group in groups) ...[
|
||||
const SizedBox(width: 48),
|
||||
Expanded(
|
||||
child: _buildLinkGroup(context, group),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileLinks(BuildContext context) {
|
||||
final groups = linkGroups.isEmpty ? defaultLinkGroups : linkGroups;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
Text(
|
||||
'inou',
|
||||
style: InouText.h3.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: InouTheme.accent,
|
||||
letterSpacing: -0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Your health, understood.',
|
||||
style: InouText.body.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Links in 2-column grid
|
||||
Wrap(
|
||||
spacing: 32,
|
||||
runSpacing: 24,
|
||||
children: [
|
||||
for (final group in groups)
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: _buildLinkGroup(context, group),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLinkGroup(BuildContext context, FooterLinkGroup group) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
group.title.toUpperCase(),
|
||||
style: InouText.labelCaps.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
for (final link in group.links)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: () => _handleLinkTap(context, link),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
link.label,
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.text,
|
||||
),
|
||||
),
|
||||
if (link.isExternal) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.open_in_new,
|
||||
size: 12,
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(BuildContext context, bool isMobile) {
|
||||
final year = DateTime.now().year;
|
||||
final copyright = copyrightText ?? '© $year inou health. All rights reserved.';
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: InouTheme.border, width: 1),
|
||||
),
|
||||
),
|
||||
child: isMobile
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
copyright,
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildSocialLinks(),
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Text(
|
||||
copyright,
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
_buildSocialLinks(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSocialLinks() {
|
||||
// Placeholder for social links if needed
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
void _handleLinkTap(BuildContext context, FooterLink link) {
|
||||
if (onLinkTap != null) {
|
||||
onLinkTap!(link);
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.isExternal) {
|
||||
// Handle external links (url_launcher)
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.pushNamed(context, link.route);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:inou_app/design/inou_theme.dart';
|
||||
import 'package:inou_app/design/inou_text.dart';
|
||||
import 'package:inou_app/main.dart';
|
||||
import 'package:inou_app/core/locale_provider.dart';
|
||||
|
||||
/// Navigation item for header
|
||||
class NavItem {
|
||||
final String label;
|
||||
final String route;
|
||||
final bool isExternal;
|
||||
|
||||
const NavItem({
|
||||
required this.label,
|
||||
required this.route,
|
||||
this.isExternal = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// inou Header - responsive, matches web design with language switcher
|
||||
class InouHeader extends StatelessWidget {
|
||||
final VoidCallback? onLogoTap;
|
||||
final List<NavItem> navItems;
|
||||
final String? currentRoute;
|
||||
final VoidCallback? onLoginTap;
|
||||
final VoidCallback? onSignupTap;
|
||||
final bool isLoggedIn;
|
||||
final String? userName;
|
||||
final VoidCallback? onProfileTap;
|
||||
final VoidCallback? onLogoutTap;
|
||||
|
||||
const InouHeader({
|
||||
super.key,
|
||||
this.onLogoTap,
|
||||
this.navItems = const [],
|
||||
this.currentRoute,
|
||||
this.onLoginTap,
|
||||
this.onSignupTap,
|
||||
this.isLoggedIn = false,
|
||||
this.userName,
|
||||
this.onProfileTap,
|
||||
this.onLogoutTap,
|
||||
});
|
||||
|
||||
static const defaultNavItems = [
|
||||
NavItem(label: 'Dossiers', route: '/dossiers'),
|
||||
NavItem(label: 'Privacy', route: '/privacy'),
|
||||
NavItem(label: 'Connect', route: '/connect'),
|
||||
NavItem(label: 'Invite a friend', route: '/invite'),
|
||||
NavItem(label: 'Demo', route: '/demo'),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isMobile = screenWidth < 768;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: InouTheme.bg,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: InouTheme.border, width: 1),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
bottom: false,
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: InouTheme.maxWidth),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isMobile ? 16 : 24,
|
||||
vertical: 12,
|
||||
),
|
||||
child: isMobile ? _buildMobileHeader(context) : _buildDesktopHeader(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// Logo
|
||||
_buildLogo(context),
|
||||
|
||||
const SizedBox(width: 48),
|
||||
|
||||
// Navigation
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
for (final item in navItems.isEmpty ? defaultNavItems : navItems)
|
||||
_buildNavItem(context, item),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Language switcher
|
||||
_LanguageSwitcher(),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Auth buttons
|
||||
_buildAuthSection(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
_buildLogo(context),
|
||||
const Spacer(),
|
||||
_LanguageSwitcher(),
|
||||
const SizedBox(width: 8),
|
||||
_buildMobileMenuButton(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogo(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onLogoTap,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
'inou',
|
||||
style: InouText.logo.copyWith(
|
||||
color: InouTheme.accent,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'health',
|
||||
style: InouText.logoLight.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n?.appTagline ?? 'ai answers for you',
|
||||
style: InouText.logoTagline,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(BuildContext context, NavItem item) {
|
||||
final isActive = currentRoute == item.route;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: InkWell(
|
||||
onTap: () => _navigateTo(context, item),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Text(
|
||||
item.label,
|
||||
style: isActive ? InouText.navActive : InouText.nav,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAuthSection(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
if (isLoggedIn) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onProfileTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: InouTheme.accentLight,
|
||||
child: Text(
|
||||
(userName ?? 'U')[0].toUpperCase(),
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
userName ?? 'Account',
|
||||
style: InouText.body,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: onLoginTap,
|
||||
child: Text(
|
||||
l10n?.signIn ?? 'Log in',
|
||||
style: InouText.nav,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: onSignupTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: InouTheme.accent,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: InouTheme.borderRadiusMd,
|
||||
),
|
||||
),
|
||||
child: Text(l10n?.getStarted ?? 'Get started', style: InouText.button.copyWith(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileMenuButton(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.menu, color: InouTheme.text),
|
||||
onPressed: () => _showMobileMenu(context),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMobileMenu(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: InouTheme.bgCard,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final item in navItems.isEmpty ? defaultNavItems : navItems)
|
||||
ListTile(
|
||||
title: Text(item.label, style: InouText.body),
|
||||
trailing: item.isExternal
|
||||
? Icon(Icons.open_in_new, size: 18, color: InouTheme.textMuted)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_navigateTo(context, item);
|
||||
},
|
||||
),
|
||||
const Divider(height: 32),
|
||||
if (!isLoggedIn) ...[
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onLoginTap?.call();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: Text(l10n?.signIn ?? 'Log in', style: InouText.button),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onSignupTap?.call();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: InouTheme.accent,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
child: Text(l10n?.getStarted ?? 'Get started', style: InouText.button.copyWith(color: Colors.white)),
|
||||
),
|
||||
] else ...[
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: InouTheme.accentLight,
|
||||
child: Text(
|
||||
(userName ?? 'U')[0].toUpperCase(),
|
||||
style: InouText.bodySmall.copyWith(color: InouTheme.accent),
|
||||
),
|
||||
),
|
||||
title: Text(userName ?? 'Account', style: InouText.body),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onProfileTap?.call();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text('Log out', style: InouText.body),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
onLogoutTap?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateTo(BuildContext context, NavItem item) {
|
||||
if (item.isExternal) {
|
||||
// Handle external links (url_launcher would be needed)
|
||||
return;
|
||||
}
|
||||
Navigator.pushNamed(context, item.route);
|
||||
}
|
||||
}
|
||||
|
||||
/// Language switcher dropdown matching Go version .lang-menu
|
||||
class _LanguageSwitcher extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<Locale>(
|
||||
valueListenable: localeNotifier,
|
||||
builder: (context, locale, _) {
|
||||
final currentCode = LocaleProvider.localeCodes[locale.languageCode] ?? 'EN';
|
||||
|
||||
return PopupMenuButton<Locale>(
|
||||
offset: const Offset(0, 40),
|
||||
tooltip: 'Change language',
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: InouTheme.borderRadiusMd,
|
||||
),
|
||||
color: InouTheme.bgCard,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: InouTheme.border),
|
||||
borderRadius: InouTheme.borderRadiusSm,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
currentCode,
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: InouTheme.textMuted,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
size: 16,
|
||||
color: InouTheme.textMuted,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onSelected: (selectedLocale) {
|
||||
InouApp.setLocale(context, selectedLocale);
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
for (final supportedLocale in LocaleProvider.supportedLocales)
|
||||
PopupMenuItem<Locale>(
|
||||
value: supportedLocale,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
LocaleProvider.localeNames[supportedLocale.languageCode] ?? '',
|
||||
style: InouText.bodySmall.copyWith(
|
||||
color: locale.languageCode == supportedLocale.languageCode
|
||||
? InouTheme.accent
|
||||
: InouTheme.text,
|
||||
fontWeight: locale.languageCode == supportedLocale.languageCode
|
||||
? FontWeight.w600
|
||||
: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
if (locale.languageCode == supportedLocale.languageCode) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(Icons.check, size: 16, color: InouTheme.accent),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||