fix: frontend UI polish — tooltips, planner checkbox, settings layout (#228)
- Fix mobile tooltip positioning (above icon instead of centered) - Place planner checkbox and send-now button on same row - Move settings tooltips beside input fields instead of overlapping - Fix input-with-tooltip layout for narrow screens - Add daily/everyNDays i18n keys for dose frequency display - Fix lint formatting in page components Closes #225
This commit is contained in:
@@ -301,6 +301,12 @@ function AppContent() {
|
||||
closeAllTooltips();
|
||||
// Toggle this one
|
||||
target.classList.add("tooltip-active");
|
||||
// Position tooltip above the icon on mobile
|
||||
if (window.innerWidth <= 640) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
// Place tooltip bottom edge just above the icon
|
||||
target.style.setProperty("--tooltip-bottom", `${window.innerHeight - rect.top + 8}px`);
|
||||
}
|
||||
} else {
|
||||
closeAllTooltips();
|
||||
}
|
||||
|
||||
@@ -356,8 +356,7 @@ export function MedDetailModal({
|
||||
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
|
||||
</span>
|
||||
<span className="med-schedule-freq">
|
||||
{t("form.blisters.every")} {blister.every}{" "}
|
||||
{blister.every !== 1 ? t("common.days") : t("common.day")}
|
||||
{blister.every === 1 ? t("common.daily") : t("common.everyNDays", { count: blister.every })}
|
||||
</span>
|
||||
<span className="med-schedule-time">
|
||||
{t("modal.at")}{" "}
|
||||
|
||||
@@ -92,8 +92,8 @@ export function UserFilterModal({
|
||||
{intake.usage} {intake.usage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{med.pillWeightMg != null &&
|
||||
` (${intake.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}{" "}
|
||||
{t("form.blisters.every")} {intake.every}{" "}
|
||||
{intake.every !== 1 ? t("common.days") : t("common.day")} {t("modal.at")} {timeStr}
|
||||
{intake.every === 1 ? t("common.daily") : t("common.everyNDays", { count: intake.every })}{" "}
|
||||
{t("modal.at")} {timeStr}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -434,6 +434,8 @@
|
||||
"of": "von",
|
||||
"loose": "lose",
|
||||
"none": "Kein",
|
||||
"daily": "täglich",
|
||||
"everyNDays": "alle {{count}} Tage",
|
||||
"day": "Tag",
|
||||
"days": "Tage",
|
||||
"blister": "Blister",
|
||||
|
||||
@@ -434,6 +434,8 @@
|
||||
"of": "of",
|
||||
"loose": "loose",
|
||||
"none": "None",
|
||||
"daily": "daily",
|
||||
"everyNDays": "every {{count}} days",
|
||||
"day": "day",
|
||||
"days": "days",
|
||||
"blister": "blister",
|
||||
|
||||
@@ -608,7 +608,17 @@ export function DashboardPage() {
|
||||
<span className="med-name-line">
|
||||
<MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />
|
||||
<span className="med-name-block-dash">
|
||||
<span className="med-name-text">{row.name}</span>
|
||||
<span className="med-name-text">
|
||||
{row.name}
|
||||
{med?.notes && (
|
||||
<>
|
||||
{" "}
|
||||
<span className="notes-icon info-tooltip" data-tooltip={t("tooltips.hasNotes")}>
|
||||
📝
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{med?.takenBy && med.takenBy.length > 0 && (
|
||||
<span className="med-taken-by-line">
|
||||
{med.takenBy.map((person) => (
|
||||
@@ -628,13 +638,6 @@ export function DashboardPage() {
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
{med?.notes && (
|
||||
<span className="med-icons">
|
||||
<span className="notes-icon info-tooltip" data-tooltip={t("tooltips.hasNotes")}>
|
||||
📝
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span data-label={t("table.stock")} className={textClass}>
|
||||
{med?.packageType === "bottle"
|
||||
|
||||
@@ -697,9 +697,9 @@ export function MedicationsPage() {
|
||||
<div className="blister-list">
|
||||
{(med.intakes ?? med.blisters).map((s, idx) => (
|
||||
<div key={`${med.id}-${idx}`} className="blister-row-simple">
|
||||
{s.usage} {s.usage === 1 ? t("common.pill") : t("common.pills")} · {t("form.blisters.every")}{" "}
|
||||
{s.every} {s.every === 1 ? t("common.day") : t("common.days")} · {t("form.blisters.from")}{" "}
|
||||
{formatDateTime(s.start)}
|
||||
{s.usage} {s.usage === 1 ? t("common.pill") : t("common.pills")} ·{" "}
|
||||
{s.every === 1 ? t("common.daily") : t("common.everyNDays", { count: s.every })} ·{" "}
|
||||
{t("form.blisters.from")} {formatDateTime(s.start)}
|
||||
{"takenBy" in s && s.takenBy && <span className="blister-taken-by"> · {s.takenBy}</span>}
|
||||
{"intakeRemindersEnabled" in s && s.intakeRemindersEnabled && (
|
||||
<span className="blister-reminder-icon" title={t("form.blisters.remindTooltip")}>
|
||||
|
||||
@@ -168,17 +168,19 @@ export function PlannerPage() {
|
||||
{t("planner.until")}
|
||||
<DateTimeInput step="60" value={range.end} onChange={(e) => setRange({ ...range, end: e.target.value })} />
|
||||
</label>
|
||||
<label className="planner-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeUntilStart}
|
||||
onChange={(e) => setIncludeUntilStart(e.target.checked)}
|
||||
/>
|
||||
{t("planner.includeUntilStart")}
|
||||
<div className="planner-checkbox-row">
|
||||
<label className="planner-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeUntilStart}
|
||||
onChange={(e) => setIncludeUntilStart(e.target.checked)}
|
||||
/>
|
||||
{t("planner.includeUntilStart")}
|
||||
</label>
|
||||
<span className="info-tooltip small" data-tooltip={t("planner.includeUntilStartTooltip")}>
|
||||
ⓘ
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="planner-actions">
|
||||
<button type="button" className="ghost" onClick={resetRange}>
|
||||
{t("common.reset")}
|
||||
@@ -210,8 +212,10 @@ export function PlannerPage() {
|
||||
{row.medicationName}
|
||||
</span>
|
||||
<span data-label={t("planner.table.usage")}>
|
||||
<strong>{row.plannerUsage}</strong>
|
||||
{row.plannerUsage === 1 ? t("common.pill") : t("common.pills")}
|
||||
<span>
|
||||
<strong>{row.plannerUsage}</strong>
|
||||
{row.plannerUsage === 1 ? t("common.pill") : t("common.pills")}
|
||||
</span>
|
||||
</span>
|
||||
<span data-label={t("planner.table.blisters")}>
|
||||
{row.packageType === "bottle" ? "–" : `${row.blistersNeeded} × ${row.blisterSize}`}
|
||||
|
||||
@@ -43,28 +43,26 @@ export function SettingsPage() {
|
||||
<div className="card-head">
|
||||
<h2>{t("settings.language.title")}</h2>
|
||||
</div>
|
||||
<div className="setting-section">
|
||||
<label className="setting-row language-row">
|
||||
<span className="setting-label">{t("settings.language.select")}</span>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={(e) => {
|
||||
const lang = e.target.value;
|
||||
i18n.changeLanguage(lang);
|
||||
fetch("/api/settings/language", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ language: lang }),
|
||||
});
|
||||
}}
|
||||
className="language-select"
|
||||
>
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label className="setting-row language-row">
|
||||
<span className="setting-label">{t("settings.language.select")}</span>
|
||||
<select
|
||||
value={i18n.language}
|
||||
onChange={(e) => {
|
||||
const lang = e.target.value;
|
||||
i18n.changeLanguage(lang);
|
||||
fetch("/api/settings/language", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ language: lang }),
|
||||
});
|
||||
}}
|
||||
className="language-select"
|
||||
>
|
||||
<option value="en">🇬🇧 English</option>
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
</select>
|
||||
</label>
|
||||
</article>
|
||||
|
||||
{/* Notifications */}
|
||||
@@ -373,25 +371,25 @@ export function SettingsPage() {
|
||||
{settings.emailEnabled && (
|
||||
<>
|
||||
<div className="setting-group">
|
||||
<label className="full">
|
||||
<span className="field-label">{t("settings.email.recipient")}</span>
|
||||
<div className="input-with-tooltip">
|
||||
<input
|
||||
type="email"
|
||||
value={settings.notificationEmail}
|
||||
onChange={(e) => setSettings({ ...settings, notificationEmail: e.target.value })}
|
||||
placeholder="your@email.com"
|
||||
pattern="[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$"
|
||||
autoComplete="email"
|
||||
/>
|
||||
<div className="full">
|
||||
<span className="field-label">
|
||||
{t("settings.email.recipient")}
|
||||
<span
|
||||
className="info-tooltip"
|
||||
data-tooltip={`SMTP: ${settings.smtpHost || t("settings.email.notConfigured")}:${settings.smtpPort}${settings.hasSmtpPassword ? "\nPassword: ✓" : ""}`}
|
||||
>
|
||||
ⓘ
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
<input
|
||||
type="email"
|
||||
value={settings.notificationEmail}
|
||||
onChange={(e) => setSettings({ ...settings, notificationEmail: e.target.value })}
|
||||
placeholder="your@email.com"
|
||||
pattern="[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}$"
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-actions">
|
||||
<button
|
||||
@@ -442,23 +440,23 @@ export function SettingsPage() {
|
||||
{settings.shoutrrrEnabled && (
|
||||
<>
|
||||
<div className="setting-group">
|
||||
<label className="full">
|
||||
<span className="field-label">{t("settings.push.url")}</span>
|
||||
<div className="input-with-tooltip">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.shoutrrrUrl}
|
||||
onChange={(e) => setSettings({ ...settings, shoutrrrUrl: e.target.value })}
|
||||
placeholder={t("settings.push.urlPlaceholder")}
|
||||
/>
|
||||
<div className="full">
|
||||
<span className="field-label">
|
||||
{t("settings.push.url")}
|
||||
<span
|
||||
className="info-tooltip"
|
||||
data-tooltip={`${t("settings.push.supports")}\n\n${t("settings.push.docsLink")}`}
|
||||
>
|
||||
ⓘ
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.shoutrrrUrl}
|
||||
onChange={(e) => setSettings({ ...settings, shoutrrrUrl: e.target.value })}
|
||||
placeholder={t("settings.push.urlPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="setting-actions">
|
||||
<button
|
||||
@@ -606,7 +604,7 @@ export function SettingsPage() {
|
||||
<h3>{t("settings.stock.thresholds")}</h3>
|
||||
</div>
|
||||
<div className="setting-group threshold-chips-group">
|
||||
<label className={settings.reminderDaysBefore >= settings.lowStockDays ? "threshold-invalid" : ""}>
|
||||
<div className={settings.reminderDaysBefore >= settings.lowStockDays ? "threshold-invalid" : ""}>
|
||||
<span className="field-label threshold-chip-label">
|
||||
<span className="status-chip small danger">{t("status.criticalStock")}</span>
|
||||
<span
|
||||
@@ -616,17 +614,15 @@ export function SettingsPage() {
|
||||
ⓘ
|
||||
</span>
|
||||
</span>
|
||||
<div className="input-with-tooltip">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={settings.reminderDaysBefore}
|
||||
onChange={(e) => setSettings({ ...settings, reminderDaysBefore: Number(e.target.value) || 7 })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={settings.reminderDaysBefore}
|
||||
onChange={(e) => setSettings({ ...settings, reminderDaysBefore: Number(e.target.value) || 7 })}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
settings.lowStockDays <= settings.reminderDaysBefore ||
|
||||
settings.lowStockDays >= settings.highStockDays
|
||||
@@ -643,17 +639,15 @@ export function SettingsPage() {
|
||||
ⓘ
|
||||
</span>
|
||||
</span>
|
||||
<div className="input-with-tooltip">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={settings.lowStockDays}
|
||||
onChange={(e) => setSettings({ ...settings, lowStockDays: Number(e.target.value) || 30 })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label className={settings.highStockDays <= settings.lowStockDays ? "threshold-invalid" : ""}>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={settings.lowStockDays}
|
||||
onChange={(e) => setSettings({ ...settings, lowStockDays: Number(e.target.value) || 30 })}
|
||||
/>
|
||||
</div>
|
||||
<div className={settings.highStockDays <= settings.lowStockDays ? "threshold-invalid" : ""}>
|
||||
<span className="field-label threshold-chip-label">
|
||||
<span className="status-chip small high">{t("status.highStock")}</span>
|
||||
<span
|
||||
@@ -663,16 +657,14 @@ export function SettingsPage() {
|
||||
ⓘ
|
||||
</span>
|
||||
</span>
|
||||
<div className="input-with-tooltip">
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={settings.highStockDays}
|
||||
onChange={(e) => setSettings({ ...settings, highStockDays: Number(e.target.value) || 180 })}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={settings.highStockDays}
|
||||
onChange={(e) => setSettings({ ...settings, highStockDays: Number(e.target.value) || 180 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(settings.reminderDaysBefore >= settings.lowStockDays ||
|
||||
settings.lowStockDays >= settings.highStockDays) && (
|
||||
@@ -734,6 +726,7 @@ export function SettingsPage() {
|
||||
{t("exportImport.importSuccessDetails", {
|
||||
medications: importResult.medications,
|
||||
doses: importResult.doses,
|
||||
refills: importResult.refills,
|
||||
shares: importResult.shares,
|
||||
})}
|
||||
</span>
|
||||
|
||||
+34
-21
@@ -2503,8 +2503,14 @@ button.has-validation-error {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.planner label.planner-checkbox {
|
||||
.planner .planner-checkbox-row {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.planner label.planner-checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -2911,16 +2917,20 @@ button.has-validation-error {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: calc(100vw - 32px);
|
||||
width: max-content;
|
||||
top: auto;
|
||||
bottom: var(--tooltip-bottom, 50%);
|
||||
left: 16px !important;
|
||||
right: 16px !important;
|
||||
transform: none !important;
|
||||
width: auto !important;
|
||||
max-width: none !important;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.info-tooltip.tooltip-active::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.info-tooltip::before {
|
||||
display: none;
|
||||
}
|
||||
@@ -3456,23 +3466,22 @@ button.has-validation-error {
|
||||
font-family: "SF Mono", "Fira Code", monospace;
|
||||
}
|
||||
|
||||
/* Input with tooltip inside */
|
||||
/* Input with tooltip beside */
|
||||
.input-with-tooltip {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.input-with-tooltip input {
|
||||
width: 100%;
|
||||
padding-right: 2.5rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.input-with-tooltip .info-tooltip {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
/* SMTP Info */
|
||||
@@ -3671,7 +3680,7 @@ button.has-validation-error {
|
||||
|
||||
.cell-with-avatar .med-name-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -4015,14 +4024,14 @@ button.has-validation-error {
|
||||
|
||||
.user-med-info {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.user-med-name {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-med-generic {
|
||||
@@ -4199,7 +4208,8 @@ button.has-validation-error {
|
||||
.med-schedule-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
justify-content: flex-start;
|
||||
gap: 1rem;
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
@@ -4216,7 +4226,6 @@ button.has-validation-error {
|
||||
}
|
||||
|
||||
.med-schedule-time {
|
||||
margin-left: auto;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -6078,6 +6087,10 @@ a.about-version-link:hover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-edit-form .date-input-display {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.mobile-edit-form .blister-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
Reference in New Issue
Block a user