feat: add FormNumberStepper to medication edit forms (#274)
Replace plain numeric inputs with a reusable +/− stepper component in both desktop (MedicationsPage) and mobile (MobileEditModal) edit forms. Applied to Stock, Schedule, and Prescription tab fields. Reorder tabs so Schedule appears before Prescription. Add responsive grid overrides for narrow sidebar and compact schedule rows. Fix label-hover ghost activation by placing <input> first in DOM (CSS order restores visual [−] [value] [+] layout). Closes #273
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import { Minus, Plus } from "lucide-react";
|
||||
|
||||
interface FormNumberStepperProps {
|
||||
value: string;
|
||||
onChange: (nextValue: string) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
allowDecimal?: boolean;
|
||||
decrementLabel: string;
|
||||
incrementLabel: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DECIMAL_ROUNDING_FACTOR = 1000;
|
||||
|
||||
function clamp(value: number, min: number, max?: number): number {
|
||||
const clampedMin = Math.max(min, value);
|
||||
if (max == null) return clampedMin;
|
||||
return Math.min(max, clampedMin);
|
||||
}
|
||||
|
||||
function normalizeDecimal(value: number): number {
|
||||
return Math.round(value * DECIMAL_ROUNDING_FACTOR) / DECIMAL_ROUNDING_FACTOR;
|
||||
}
|
||||
|
||||
function toDisplayValue(value: number, allowDecimal: boolean): string {
|
||||
if (!allowDecimal) return String(Math.max(0, Math.trunc(value)));
|
||||
const normalized = normalizeDecimal(value);
|
||||
return normalized.toString();
|
||||
}
|
||||
|
||||
function sanitizeRawInput(raw: string, allowDecimal: boolean): string {
|
||||
const normalizedRaw = raw.replace(",", ".");
|
||||
if (allowDecimal) {
|
||||
const cleaned = normalizedRaw.replace(/[^\d.]/g, "");
|
||||
const [integerPart = "", ...fractionalParts] = cleaned.split(".");
|
||||
if (fractionalParts.length === 0) return integerPart;
|
||||
return `${integerPart}.${fractionalParts.join("")}`;
|
||||
}
|
||||
return normalizedRaw.replace(/\D/g, "");
|
||||
}
|
||||
|
||||
function parseInputValue(raw: string, allowDecimal: boolean): number | null {
|
||||
if (raw.trim() === "") return null;
|
||||
const parsed = allowDecimal ? Number.parseFloat(raw) : Number.parseInt(raw, 10);
|
||||
if (Number.isNaN(parsed)) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function FormNumberStepper({
|
||||
value,
|
||||
onChange,
|
||||
min = 0,
|
||||
max,
|
||||
step = 1,
|
||||
allowDecimal = false,
|
||||
decrementLabel,
|
||||
incrementLabel,
|
||||
className = "",
|
||||
}: FormNumberStepperProps) {
|
||||
const parsed = parseInputValue(value, allowDecimal);
|
||||
const baseValue = parsed ?? min;
|
||||
const canDecrement = baseValue > min;
|
||||
const canIncrement = max == null || baseValue < max;
|
||||
|
||||
const normalizedClassName = ["number-stepper", "form-number-stepper", className].filter(Boolean).join(" ");
|
||||
|
||||
const handleStep = (direction: -1 | 1) => {
|
||||
const nextRaw = clamp(baseValue + direction * step, min, max);
|
||||
onChange(toDisplayValue(nextRaw, allowDecimal));
|
||||
};
|
||||
|
||||
const handleInputChange = (nextRaw: string) => {
|
||||
onChange(sanitizeRawInput(nextRaw, allowDecimal));
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const nextParsed = parseInputValue(value, allowDecimal);
|
||||
if (nextParsed == null) return;
|
||||
const clamped = clamp(nextParsed, min, max);
|
||||
onChange(toDisplayValue(clamped, allowDecimal));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={normalizedClassName}>
|
||||
{/* Input first in DOM so <label> associates with it, not the decrement button.
|
||||
CSS order restores the visual layout: [−] [input] [+]. */}
|
||||
<input
|
||||
type="text"
|
||||
inputMode={allowDecimal ? "decimal" : "numeric"}
|
||||
pattern={allowDecimal ? "[0-9]*\\.?[0-9]*" : "[0-9]*"}
|
||||
value={value}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="stepper-btn decrement"
|
||||
onClick={() => handleStep(-1)}
|
||||
disabled={!canDecrement}
|
||||
aria-label={decrementLabel}
|
||||
>
|
||||
<Minus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="stepper-btn increment"
|
||||
onClick={() => handleStep(1)}
|
||||
disabled={!canIncrement}
|
||||
aria-label={incrementLabel}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type { DoseUnit, FieldErrors, FormBlister, FormIntake, FormState, Medicat
|
||||
import { DOSE_UNITS } from "../types";
|
||||
import { deriveTotal } from "../utils";
|
||||
import { DateInput } from "./DateInput";
|
||||
import { FormNumberStepper } from "./FormNumberStepper";
|
||||
|
||||
// Field limits for validation
|
||||
const FIELD_LIMITS = {
|
||||
@@ -107,6 +108,8 @@ export function MobileEditModal({
|
||||
onSaveMedication,
|
||||
}: MobileEditModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const decrementValueLabel = t("editStock.decreaseValue");
|
||||
const incrementValueLabel = t("editStock.increaseValue");
|
||||
const [activeTab, setActiveTab] = useState<MobileTab>("general");
|
||||
const fieldsetRef = useRef<HTMLFieldSetElement | null>(null);
|
||||
const tabStripRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -499,32 +502,32 @@ export function MobileEditModal({
|
||||
<>
|
||||
<label>
|
||||
{t("form.packs")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.packCount}
|
||||
onChange={(e) => onHandleValueChange("packCount", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("packCount", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.blistersPerPack")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.blistersPerPack}
|
||||
onChange={(e) => onHandleValueChange("blistersPerPack", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("blistersPerPack", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.pillsPerBlister")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.pillsPerBlister}
|
||||
onChange={(e) => onHandleValueChange("pillsPerBlister", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("pillsPerBlister", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
@@ -536,22 +539,22 @@ export function MobileEditModal({
|
||||
<>
|
||||
<label>
|
||||
{t("form.totalCapacity")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.totalPills}
|
||||
onChange={(e) => onHandleValueChange("totalPills", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("totalPills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.currentPills")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.looseTablets}
|
||||
onChange={(e) => onHandleValueChange("looseTablets", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("looseTablets", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
@@ -646,22 +649,24 @@ export function MobileEditModal({
|
||||
>
|
||||
<label className="compact">
|
||||
<span>{t("form.blisters.usage")}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*\.?[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={intake.usage}
|
||||
onChange={(e) => onSetIntakeValue(idx, "usage", e.target.value)}
|
||||
onChange={(nextValue) => onSetIntakeValue(idx, "usage", nextValue)}
|
||||
min={0.5}
|
||||
step={0.5}
|
||||
allowDecimal={true}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="compact">
|
||||
<span>{t("form.blisters.everyDays")}</span>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={intake.every}
|
||||
onChange={(e) => onSetIntakeValue(idx, "every", e.target.value)}
|
||||
onChange={(nextValue) => onSetIntakeValue(idx, "every", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="compact full-row">
|
||||
@@ -740,32 +745,32 @@ export function MobileEditModal({
|
||||
<>
|
||||
<label className="prescription-field">
|
||||
{t("prescription.authorizedRefills")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.prescriptionAuthorizedRefills}
|
||||
onChange={(e) => onHandleValueChange("prescriptionAuthorizedRefills", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("prescriptionAuthorizedRefills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="prescription-field">
|
||||
{t("prescription.remainingRefills")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.prescriptionRemainingRefills}
|
||||
onChange={(e) => onHandleValueChange("prescriptionRemainingRefills", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("prescriptionRemainingRefills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="prescription-field">
|
||||
{t("prescription.lowThreshold")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.prescriptionLowRefillThreshold}
|
||||
onChange={(e) => onHandleValueChange("prescriptionLowRefillThreshold", e.target.value)}
|
||||
onChange={(nextValue) => onHandleValueChange("prescriptionLowRefillThreshold", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="prescription-field">
|
||||
|
||||
@@ -6,6 +6,7 @@ export { ConfirmModal } from "./ConfirmModal";
|
||||
export { DateInput } from "./DateInput";
|
||||
export { DateTimeInput } from "./DateTimeInput";
|
||||
export { default as ExportModal } from "./ExportModal";
|
||||
export { FormNumberStepper } from "./FormNumberStepper";
|
||||
export type { LightboxProps } from "./Lightbox";
|
||||
|
||||
export { Lightbox } from "./Lightbox";
|
||||
|
||||
@@ -5,7 +5,15 @@ import { Bell, Eye, Minus, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { ConfirmModal, DateInput, Lightbox, MedicationAvatar, MobileEditModal, ReportModal } from "../components";
|
||||
import {
|
||||
ConfirmModal,
|
||||
DateInput,
|
||||
FormNumberStepper,
|
||||
Lightbox,
|
||||
MedicationAvatar,
|
||||
MobileEditModal,
|
||||
ReportModal,
|
||||
} from "../components";
|
||||
import { useAuth } from "../components/Auth";
|
||||
import { useAppContext, useUnsavedChanges } from "../context";
|
||||
import { useMedicationForm, useModalHistory, useUnsavedChangesWarning } from "../hooks";
|
||||
@@ -175,6 +183,8 @@ export function MedicationsPage() {
|
||||
const pillsPerBlister = Number(form.pillsPerBlister) || 1;
|
||||
return packCount * blistersPerPack * pillsPerBlister;
|
||||
}, [form.packageType, form.packCount, form.blistersPerPack, form.pillsPerBlister, form.looseTablets]);
|
||||
const decrementValueLabel = t("editStock.decreaseValue");
|
||||
const incrementValueLabel = t("editStock.increaseValue");
|
||||
|
||||
const dateConsistencyError = useMemo(() => {
|
||||
const medicationStartDate = form.medicationStartDate;
|
||||
@@ -966,15 +976,6 @@ export function MedicationsPage() {
|
||||
>
|
||||
{t("form.sections.stock")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === "prescription"}
|
||||
className={`form-tab${activeTab === "prescription" ? " active" : ""}`}
|
||||
onClick={() => setActiveTab("prescription")}
|
||||
>
|
||||
{t("form.sections.prescription")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -984,6 +985,15 @@ export function MedicationsPage() {
|
||||
>
|
||||
{t("form.sections.schedule")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === "prescription"}
|
||||
className={`form-tab${activeTab === "prescription" ? " active" : ""}`}
|
||||
onClick={() => setActiveTab("prescription")}
|
||||
>
|
||||
{t("form.sections.prescription")}
|
||||
</button>
|
||||
</div>
|
||||
<fieldset className="readonly-fieldset" disabled={readOnlyView}>
|
||||
<div className={`form-tab-panel${activeTab === "general" ? " active" : ""}`}>
|
||||
@@ -1157,32 +1167,32 @@ export function MedicationsPage() {
|
||||
<>
|
||||
<label>
|
||||
{t("form.packs")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.packCount}
|
||||
onChange={(e) => handleValueChange("packCount", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("packCount", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.blistersPerPack")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.blistersPerPack}
|
||||
onChange={(e) => handleValueChange("blistersPerPack", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("blistersPerPack", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.pillsPerBlister")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.pillsPerBlister}
|
||||
onChange={(e) => handleValueChange("pillsPerBlister", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("pillsPerBlister", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
@@ -1194,22 +1204,22 @@ export function MedicationsPage() {
|
||||
<>
|
||||
<label>
|
||||
{t("form.totalCapacity")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.totalPills}
|
||||
onChange={(e) => handleValueChange("totalPills", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("totalPills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.currentPills")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.looseTablets}
|
||||
onChange={(e) => handleValueChange("looseTablets", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("looseTablets", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
@@ -1300,32 +1310,32 @@ export function MedicationsPage() {
|
||||
<>
|
||||
<label className="prescription-field">
|
||||
{t("prescription.authorizedRefills")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.prescriptionAuthorizedRefills}
|
||||
onChange={(e) => handleValueChange("prescriptionAuthorizedRefills", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("prescriptionAuthorizedRefills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="prescription-field">
|
||||
{t("prescription.remainingRefills")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.prescriptionRemainingRefills}
|
||||
onChange={(e) => handleValueChange("prescriptionRemainingRefills", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("prescriptionRemainingRefills", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="prescription-field">
|
||||
{t("prescription.lowThreshold")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={form.prescriptionLowRefillThreshold}
|
||||
onChange={(e) => handleValueChange("prescriptionLowRefillThreshold", e.target.value)}
|
||||
onChange={(nextValue) => handleValueChange("prescriptionLowRefillThreshold", nextValue)}
|
||||
min={0}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label className="prescription-field">
|
||||
@@ -1362,22 +1372,24 @@ export function MedicationsPage() {
|
||||
<div className="blister-inputs">
|
||||
<label>
|
||||
{t("form.blisters.usage")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
pattern="[0-9]*\.?[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={intake.usage}
|
||||
onChange={(e) => setIntakeValue(idx, "usage", e.target.value)}
|
||||
onChange={(nextValue) => setIntakeValue(idx, "usage", nextValue)}
|
||||
min={0.5}
|
||||
step={0.5}
|
||||
allowDecimal={true}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t("form.blisters.everyDays")}
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
<FormNumberStepper
|
||||
value={intake.every}
|
||||
onChange={(e) => setIntakeValue(idx, "every", e.target.value)}
|
||||
onChange={(nextValue) => setIntakeValue(idx, "every", nextValue)}
|
||||
min={1}
|
||||
decrementLabel={decrementValueLabel}
|
||||
incrementLabel={incrementValueLabel}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
|
||||
+20
-5
@@ -1127,11 +1127,15 @@ body.modal-open {
|
||||
}
|
||||
.blister-row .blister-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(0, 1.05fr) minmax(10.75rem, 1fr) minmax(7.25rem, 0.8fr);
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.blister-row .blister-inputs > label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.blister-row .blister-inputs label.taken-by-field {
|
||||
grid-column: span 2;
|
||||
}
|
||||
@@ -1154,6 +1158,17 @@ body.modal-open {
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop edit sidebar can be narrow; avoid clipping right-side controls. */
|
||||
@media (min-width: 769px) {
|
||||
.edit-sidebar .blister-row .blister-inputs {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.edit-sidebar .blister-row .blister-inputs label.taken-by-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.gap {
|
||||
gap: 0.6rem;
|
||||
}
|
||||
@@ -3376,7 +3391,7 @@ button.has-validation-error {
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
visibility 0.15s;
|
||||
z-index: 100;
|
||||
z-index: 1100;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -3394,7 +3409,7 @@ button.has-validation-error {
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
visibility 0.15s;
|
||||
z-index: 101;
|
||||
z-index: 1101;
|
||||
}
|
||||
|
||||
/* Tooltip aligned to left edge of icon (prevents clipping inside modals) */
|
||||
@@ -4335,7 +4350,7 @@ button.has-validation-error {
|
||||
overscroll-behavior: contain;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.med-detail-modal .med-detail-body {
|
||||
@@ -4701,7 +4716,7 @@ button.has-validation-error {
|
||||
background: var(--bg-primary);
|
||||
border-radius: 0 0 12px 12px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
@@ -461,6 +461,7 @@
|
||||
|
||||
.refill-number-stepper .stepper-btn.decrement {
|
||||
order: initial;
|
||||
background: color-mix(in srgb, var(--danger) 22%, var(--bg-tertiary));
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@@ -468,13 +469,20 @@
|
||||
order: initial;
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--border-primary);
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 85%, transparent);
|
||||
background: color-mix(in srgb, var(--success) 22%, var(--bg-tertiary));
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.refill-number-stepper .stepper-btn:hover:not(:disabled) {
|
||||
filter: none;
|
||||
background: color-mix(in srgb, var(--accent) 14%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
.refill-number-stepper .stepper-btn.decrement:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--danger) 36%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
.refill-number-stepper .stepper-btn.increment:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--success) 36%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
@@ -488,12 +496,12 @@
|
||||
}
|
||||
|
||||
[data-theme="light"] .refill-number-stepper .stepper-btn.decrement {
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 90%, transparent);
|
||||
background: color-mix(in srgb, #dc2626 18%, white);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
[data-theme="light"] .refill-number-stepper .stepper-btn.increment {
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 90%, transparent);
|
||||
background: color-mix(in srgb, #0f766e 18%, white);
|
||||
color: #0f766e;
|
||||
}
|
||||
}
|
||||
@@ -504,6 +512,111 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Form stepper keeps symmetric - value + layout in all contexts (desktop/mobile). */
|
||||
.form-number-stepper {
|
||||
display: grid;
|
||||
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
||||
}
|
||||
|
||||
.form-number-stepper input {
|
||||
order: 0;
|
||||
text-align: center;
|
||||
padding: 0.75rem 0.5rem;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-number-stepper .stepper-btn {
|
||||
flex: 0 0 auto;
|
||||
border-right: 1px solid var(--border-primary);
|
||||
border-left: none;
|
||||
background: color-mix(in srgb, var(--bg-tertiary) 85%, transparent);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-number-stepper .stepper-btn.decrement {
|
||||
order: -1;
|
||||
background: color-mix(in srgb, var(--danger) 22%, var(--bg-tertiary));
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.form-number-stepper .stepper-btn.increment {
|
||||
order: 1;
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--border-primary);
|
||||
background: color-mix(in srgb, var(--success) 22%, var(--bg-tertiary));
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.form-number-stepper .stepper-btn:hover:not(:disabled) {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.form-number-stepper .stepper-btn.decrement:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--danger) 36%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
.form-number-stepper .stepper-btn.increment:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--success) 36%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
/* Highlight both controls when the center value field is focused (keyboard/click). */
|
||||
.form-number-stepper:has(input:focus) .stepper-btn.decrement:not(:disabled),
|
||||
.form-number-stepper:has(input:focus-visible) .stepper-btn.decrement:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--danger) 36%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
.form-number-stepper:has(input:focus) .stepper-btn.increment:not(:disabled),
|
||||
.form-number-stepper:has(input:focus-visible) .stepper-btn.increment:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--success) 36%, var(--bg-tertiary));
|
||||
}
|
||||
|
||||
/* Dense schedule grids need a compact variant so the middle value stays visible. */
|
||||
.blister-inputs .form-number-stepper,
|
||||
.mobile-edit-form .blister-row .form-number-stepper {
|
||||
grid-template-columns: 2.35rem minmax(2rem, 1fr) 2.35rem;
|
||||
}
|
||||
|
||||
.blister-inputs .form-number-stepper input,
|
||||
.mobile-edit-form .blister-row .form-number-stepper input {
|
||||
min-height: 2.35rem;
|
||||
padding: 0.5rem 0.35rem;
|
||||
}
|
||||
|
||||
.blister-inputs .form-number-stepper .stepper-btn,
|
||||
.mobile-edit-form .blister-row .form-number-stepper .stepper-btn {
|
||||
min-width: 2.35rem;
|
||||
min-height: 2.35rem;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.form-number-stepper {
|
||||
display: grid;
|
||||
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
||||
}
|
||||
|
||||
.form-number-stepper input {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
[data-theme="light"] .form-number-stepper .stepper-btn.decrement {
|
||||
background: color-mix(in srgb, #dc2626 18%, white);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
[data-theme="light"] .form-number-stepper .stepper-btn.increment {
|
||||
background: color-mix(in srgb, #0f766e 18%, white);
|
||||
color: #0f766e;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.form-number-stepper {
|
||||
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-stock-summary {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
Reference in New Issue
Block a user