feat: improve medication detail modal layout and display (#258)

Widen detail modal on desktop (711px, up from 500px) with max-width
override to beat modals-base.css specificity. Limit fullscreen mode
to actual phones (<=500px) instead of all screens <=900px. Move intake
schedule section before prescription details. Show per-intake takenBy
person and bell icon with proper warning color. Right-align time in
schedule rows. Move notes icon after label text. Replace emoji bell
icons with Lucide Bell component in SchedulePage and MobileEditModal.
Add common.on/common.off i18n keys.

Closes #254
This commit is contained in:
Daniel Volz
2026-02-21 18:00:23 +01:00
committed by GitHub
parent 943148fb49
commit 8708f0c32a
5 changed files with 88 additions and 56 deletions
+56 -48
View File
@@ -626,7 +626,7 @@ export function MedDetailModal({
<div className="modal-footer">
<button className="ghost" onClick={onCloseEditStockModal}>
{t("common.cancel")}
{t("common.close")}
</button>
<button className="info" onClick={() => onSubmitStockCorrection(selectedMed.id)} disabled={editStockSaving}>
{editStockSaving ? t("editStock.saving") : t("editStock.save")}
@@ -803,6 +803,58 @@ export function MedDetailModal({
</div>
</div>
{/* Intake Schedule Section */}
{selectedMed.blisters.length > 0 && (
<div className="med-detail-section">
<h3>
{t("modal.intakeSchedule")}{" "}
{selectedMed.intakeRemindersEnabled && (
<span className="reminder-icon info-tooltip" data-tooltip={t("tooltips.intakeReminders")}>
<Bell size={14} aria-hidden="true" />
</span>
)}
</h3>
<div className="med-detail-schedules">
{selectedMed.blisters.map((blister, idx) => {
// When using new intakes format with per-intake takenBy,
// each intake already represents one person's dose — don't multiply.
// For legacy intakes (no per-intake takenBy), multiply by personCount.
const intake = selectedMed.intakes?.[idx];
const hasPerIntakeTakenBy = !!intake?.takenBy;
const personCount = hasPerIntakeTakenBy ? 1 : Math.max(1, selectedMed.takenBy?.length || 1);
const totalUsage = blister.usage * personCount;
return (
<div key={idx} className="med-schedule-item">
<span className="med-schedule-usage">
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")}
{selectedMed.pillWeightMg &&
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
</span>
<span className="med-schedule-freq">
{blister.every === 1 ? t("common.daily") : t("common.everyNDays", { count: blister.every })}
</span>
{hasPerIntakeTakenBy && intake.takenBy && (
<span className="med-schedule-person">{intake.takenBy}</span>
)}
{intake?.intakeRemindersEnabled && (
<span className="med-schedule-bell" role="img" aria-label={t("tooltips.intakeReminders")}>
<Bell size={13} aria-hidden="true" />
</span>
)}
<span className="med-schedule-time">
{t("modal.at")}{" "}
{new Date(blister.start).toLocaleTimeString(getSystemLocale(i18n.language), {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
);
})}
</div>
</div>
)}
{/* Prescription Details Section */}
{selectedMed.prescriptionEnabled && (
<div className="med-detail-section">
@@ -839,50 +891,6 @@ export function MedDetailModal({
</div>
)}
{/* Intake Schedule Section */}
{selectedMed.blisters.length > 0 && (
<div className="med-detail-section">
<h3>
{t("modal.intakeSchedule")}{" "}
{selectedMed.intakeRemindersEnabled && (
<span className="reminder-icon info-tooltip" data-tooltip={t("tooltips.intakeReminders")}>
<Bell size={14} aria-hidden="true" />
</span>
)}
</h3>
<div className="med-detail-schedules">
{selectedMed.blisters.map((blister, idx) => {
// When using new intakes format with per-intake takenBy,
// each intake already represents one person's dose — don't multiply.
// For legacy intakes (no per-intake takenBy), multiply by personCount.
const intake = selectedMed.intakes?.[idx];
const hasPerIntakeTakenBy = !!intake?.takenBy;
const personCount = hasPerIntakeTakenBy ? 1 : Math.max(1, selectedMed.takenBy?.length || 1);
const totalUsage = blister.usage * personCount;
return (
<div key={idx} className="med-schedule-item">
<span className="med-schedule-usage">
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")}
{selectedMed.pillWeightMg &&
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
</span>
<span className="med-schedule-freq">
{blister.every === 1 ? t("common.daily") : t("common.everyNDays", { count: blister.every })}
</span>
<span className="med-schedule-time">
{t("modal.at")}{" "}
{new Date(blister.start).toLocaleTimeString(getSystemLocale(i18n.language), {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
);
})}
</div>
</div>
)}
{/* Coverage Status Section */}
{medCoverage && status && (
<div className="med-detail-section">
@@ -909,10 +917,10 @@ export function MedDetailModal({
{selectedMed.notes && (
<div className="med-detail-section">
<h3>
{t("modal.notes")}{" "}
<span className="notes-icon notes-icon-static" aria-hidden="true">
<NotebookPen size={14} />
</span>{" "}
{t("modal.notes")}
</span>
</h3>
<div className="med-notes-content">{selectedMed.notes}</div>
</div>
@@ -1111,7 +1119,7 @@ export function MedDetailModal({
<div className="modal-footer">
<button className="ghost" onClick={onCloseRefillModal}>
{t("common.cancel")}
{t("common.close")}
</button>
<div className="refill-footer-right">
<button
+4 -1
View File
@@ -153,6 +153,7 @@
},
"form": {
"editEntry": "Bearbeiten",
"editEntryWithName": "Bearbeiten: {{name}}",
"viewEntry": "Ansehen",
"newEntry": "Neues Medikament",
"badge": "Packungen + lose Tabletten",
@@ -462,7 +463,9 @@
"pillsTotal": "{{count}} Tabletten gesamt",
"pillsTotal_one": "{{count}} Tablette gesamt",
"pillsTotal_other": "{{count}} Tabletten gesamt",
"max": "max"
"max": "max",
"on": "An",
"off": "Aus"
},
"share": {
"button": "Teilen",
+4 -1
View File
@@ -153,6 +153,7 @@
},
"form": {
"editEntry": "Edit",
"editEntryWithName": "Edit: {{name}}",
"viewEntry": "View",
"newEntry": "New medication",
"badge": "Packs + loose pills",
@@ -462,7 +463,9 @@
"pillsTotal": "{{count}} pills total",
"pillsTotal_one": "{{count}} pill total",
"pillsTotal_other": "{{count}} pills total",
"max": "max"
"max": "max",
"on": "On",
"off": "Off"
},
"share": {
"button": "Share",
+3 -2
View File
@@ -1,3 +1,4 @@
import { Bell } from "lucide-react";
import { useTranslation } from "react-i18next";
import { MedicationAvatar } from "../components";
import { useAuth } from "../components/Auth";
@@ -204,7 +205,7 @@ export function SchedulePage() {
className="reminder-icon info-tooltip"
data-tooltip={t("tooltips.intakeReminders")}
>
🔔
<Bell size={14} aria-hidden="true" />
</span>
)}{" "}
<div className="dose-checks">
@@ -365,7 +366,7 @@ export function SchedulePage() {
className="reminder-icon info-tooltip"
data-tooltip={t("tooltips.intakeReminders")}
>
🔔
<Bell size={14} aria-hidden="true" />
</span>
)}
<div className="dose-checks">
+21 -4
View File
@@ -4323,9 +4323,10 @@ button.has-validation-error {
/* Modal base styles moved to styles/modals-base.css */
/* Medication Detail Modal */
.med-detail-modal {
.modal-content.med-detail-modal {
padding: 0;
width: min(100vw - 1rem, 520px);
width: min(100vw - 1rem, 711px);
max-width: 711px;
max-height: 90vh;
background: var(--bg-primary);
overscroll-behavior: contain;
@@ -4668,6 +4669,22 @@ button.has-validation-error {
.med-schedule-time {
font-weight: 500;
margin-left: auto;
}
.med-schedule-person {
color: var(--text-secondary);
font-size: 0.85rem;
}
.med-schedule-bell {
color: var(--warning);
display: inline-flex;
align-items: center;
}
[data-theme="light"] .med-schedule-bell {
color: #b45309;
}
.med-detail-footer {
@@ -4689,7 +4706,7 @@ button.has-validation-error {
/* Mobile devices can report wide CSS viewports (e.g., 768px in device emulation).
Use input modality instead of width-only breakpoints so the modal still fills the handset viewport. */
@media (hover: none) and (pointer: coarse) {
@media (hover: none) and (pointer: coarse) and (max-width: 500px) {
.med-detail-overlay {
padding: 0.4rem;
align-items: stretch;
@@ -4938,7 +4955,7 @@ button.has-validation-error {
}
/* Hard mobile override for MedDetailModal: remove side frame and use full handset viewport. */
@media (max-width: 900px) {
@media (max-width: 500px) {
.modal-overlay.med-detail-overlay {
padding: 0 !important;
align-items: stretch;