@jjlmoya/utils-pets 1.26.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/data.ts +2 -0
- package/src/entries.ts +4 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +12 -10
- package/src/tool/petAge/component.astro +1 -1
- package/src/tool/petMedicationSchedulePlanner/bibliography.astro +6 -0
- package/src/tool/petMedicationSchedulePlanner/bibliography.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/component.astro +40 -0
- package/src/tool/petMedicationSchedulePlanner/controller.ts +95 -0
- package/src/tool/petMedicationSchedulePlanner/dom-views.ts +57 -0
- package/src/tool/petMedicationSchedulePlanner/entry.ts +26 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/content.ts +58 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/de.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/en.ts +7 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/es.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/fr.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/id.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/it.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/ja.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/ko.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/nl.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/pl.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/pt.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/ru.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/sv.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/tr.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/i18n/zh.ts +6 -0
- package/src/tool/petMedicationSchedulePlanner/index.ts +12 -0
- package/src/tool/petMedicationSchedulePlanner/logic.test.ts +24 -0
- package/src/tool/petMedicationSchedulePlanner/logic.ts +118 -0
- package/src/tool/petMedicationSchedulePlanner/pet-medication-schedule-planner.css +368 -0
- package/src/tool/petMedicationSchedulePlanner/seo.astro +11 -0
- package/src/tool/petMedicationSchedulePlanner/storage.ts +32 -0
- package/src/tool/petMedicationSchedulePlanner/ui.ts +37 -0
- package/src/tools.ts +3 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'harmonogram-lekow-dla-zwierzat', title: 'Harmonogram leków dla zwierząt', description: 'Zamień zalecenia weterynarza w czytelny lokalny harmonogram z kolejnymi dawkami do odhaczenia.', dateLocale: 'pl', medicationNameLabel: 'Nazwa leku', medicationNamePlaceholder: 'Na przykład: przepisany antybiotyk', startDateLabel: 'Data pierwszego podania', startTimeLabel: 'Godzina pierwszego podania', scheduleModeLabel: 'Jak określono częstotliwość?', intervalMode: 'Co kilka godzin', timesMode: 'Stałe godziny każdego dnia', intervalHoursLabel: 'Godziny między podaniami', timesLabel: 'Godziny dzienne', timesHint: 'Wpisz godziny w formacie 24-godzinnym, oddzielając je przecinkami: 08:00, 16:00, 00:00.', durationLabel: 'Czas leczenia', durationUnit: 'dni', instructionsLabel: 'Uwagi z zaleceń', instructionsPlaceholder: 'Karma, sposób podania lub inne wskazówki weterynarza', reset: 'Wyczyść harmonogram', scheduleTitle: 'Tablica podań', nextDoseLabel: 'Następne nieoznaczone podanie', noNextDose: 'Wszystkie podania są oznaczone', completedCount: '{done} z {total} oznaczonych', markDone: 'Oznacz jako podane', markUndone: 'Oznacz jako niepodane', completed: 'Podano', upcoming: 'Nadchodzące', due: 'Sprawdź', emptySchedule: 'Uzupełnij pola, aby zobaczyć tablicę podań.', invalidInput: 'Sprawdź nazwę, datę, godzinę, czas trwania i częstotliwość.', safetyTitle: 'Stosuj dokładnie przepisane zalecenia', safetyText: 'To pomoc do przypominania i zapisu. Nie wybiera leku, nie zmienia dawki i nie zastępuje weterynarza. Przy spóźnionym, pominiętym lub zwymiotowanym podaniu skontaktuj się z lecznicą.', methodText: 'Plan interwałowy zaczyna się od pierwszej daty i godziny, a następnie dodaje wybraną liczbę godzin. Godziny dzienne powtarzają się w każdym dniu leczenia. Dane pozostają na tym urządzeniu, dopóki ich nie usuniesz.', scheduleIllustration: 'Ręcznie rysowany plan dnia obok spokojnego kota i psa', summary: ['Uporządkuj istniejące zalecenia w czytelny plan.', 'Wybierz odstępy albo stałe godziny.', 'Odhacz podania bez wysyłania danych zwierzęcia.', 'Miej nazwę leku i uwagi przy codziennej rutynie.'], seoTitle1: 'Praktyczne przypomnienie dla istniejących zaleceń weterynaryjnych', seoIntro: 'Po wizycie opiekun często musi połączyć etykietę, kartę wypisu i kilka podań w domu. Decyzję o leczeniu podejmuje weterynarz; ten planer tylko porządkuje otrzymane instrukcje. Nazwa, pierwszy lokalny termin, rytm, liczba dni i własne notatki tworzą chronologiczną listę przydatną rano, w nocy i podczas przekazywania opieki.', seoTitle2: 'Dwa sposoby wpisania godzin', seoMethod: 'Wybierz interwał, gdy etykieta mówi na przykład co osiem godzin. Lista przejdzie wtedy przez północ. Wybierz godziny dzienne, gdy zalecono rano i wieczorem albo konkretne pory. Pierwszy dzień respektuje pierwsze podanie i nie dodaje wcześniejszej godziny. Czas trwania wyznacza tylko okno planowania, a wynik pokazuje pory, nie ilości leku.', seoTitle3: 'Lista kontrolna nie jest poradą medyczną', seoSafety: 'Zachowaj opakowanie, kartę z lecznicy i jej numer telefonu. Strona nie zna zwierzęcia, stężenia, drogi podania, interakcji ani reakcji organizmu. Przy pominiętym podaniu zapytaj lecznicę przed decyzją. Odznaczanie może jednak zapobiec podwójnemu podaniu i ułatwić przekazanie kolejnego zadania. Pamięć lokalną można w każdej chwili wyczyścić.', tipTitle: 'Przed rozpoczęciem', tipText: 'Przepisz częstotliwość i czas trwania z zaleceń, bez zgadywania. Porównaj pierwszy wygenerowany dzień z etykietą.', faq: [{ question: 'Czy planer oblicza ilość leku?', answer: 'Nie. Układa tylko godziny wpisane z istniejących zaleceń i nie oblicza, nie zmienia ani nie zaleca ilości.' }, { question: 'Czy obsługuje podania po północy?', answer: 'Tak. Interwał przechodzi na kolejny lokalny dzień, a godziny dzienne mogą zawierać 00:00.' }, { question: 'Co zrobić przy spóźnionym lub pominiętym podaniu?', answer: 'Skontaktuj się z weterynarzem lub lecznicą. Lista zapisuje stan, ale nie decyduje o nadrobieniu, pominięciu ani przesunięciu podania.' }], howTo: [{ name: 'Przepisz zalecenia', text: 'Wpisz nazwę, pierwszy termin i czas trwania dokładnie według zaleceń weterynarza.' }, { name: 'Wybierz rytm', text: 'Ustaw interwał godzinowy albo wpisz przepisane godziny dzienne.' }, { name: 'Sprawdź tablicę', text: 'Zweryfikuj pierwszy dzień i następne nieoznaczone podanie.' }, { name: 'Odhacz podania', text: 'Dotknij wiersza po podaniu i miej przy sobie oryginalne zalecenia.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'agenda-medicacao-animais', title: 'Agenda de medicação para animais', description: 'Transforme uma receita veterinária numa agenda local clara, com próximas doses e marcação de acompanhamento.', dateLocale: 'pt', medicationNameLabel: 'Nome do medicamento', medicationNamePlaceholder: 'Por exemplo: antibiótico prescrito', startDateLabel: 'Data da primeira toma', startTimeLabel: 'Hora da primeira toma', scheduleModeLabel: 'Como está indicada a frequência?', intervalMode: 'A cada algumas horas', timesMode: 'Horas fixas todos os dias', intervalHoursLabel: 'Horas entre tomas', timesLabel: 'Horas diárias', timesHint: 'Use horas de 24 horas separadas por vírgulas: 08:00, 16:00, 00:00.', durationLabel: 'Duração do tratamento', durationUnit: 'dias', instructionsLabel: 'Notas da receita', instructionsPlaceholder: 'Comida, administração ou outras instruções do veterinário', reset: 'Apagar esta agenda', scheduleTitle: 'Quadro de tomas', nextDoseLabel: 'Próxima toma por marcar', noNextDose: 'Todas as tomas estão marcadas', completedCount: '{done} de {total} marcadas', markDone: 'Marcar como dada', markUndone: 'Marcar como pendente', completed: 'Feita', upcoming: 'Próxima', due: 'Verificar', emptySchedule: 'Preencha os campos para ver o quadro.', invalidInput: 'Verifique nome, data, hora, duração e frequência.', safetyTitle: 'Use a receita exatamente como foi indicada', safetyText: 'É uma ajuda de memória e registo. Não escolhe medicamentos, altera doses nem substitui o veterinário. Se uma toma atrasar, for esquecida ou for vomitada, contacte a clínica.', methodText: 'Um intervalo começa na primeira data e hora e soma as horas escolhidas. As horas diárias repetem-se em cada dia do tratamento. Os dados ficam neste dispositivo até serem apagados.', scheduleIllustration: 'Calendário desenhado à mão junto de um gato e um cão tranquilos', summary: ['Passe uma receita existente para um horário legível.', 'Escolha intervalos ou horas fixas do dia.', 'Marque cada toma sem enviar dados do animal.', 'Mantenha o nome e as notas visíveis.'], seoTitle1: 'Um lembrete prático para uma receita veterinária existente', seoIntro: 'Depois da consulta, é comum ter de coordenar a etiqueta, a folha da clínica e várias tomas em casa. A decisão sobre o tratamento pertence ao veterinário; esta agenda apenas organiza instruções que já recebeu. O nome, o primeiro momento, o ritmo, a duração e notas opcionais tornam-se numa lista cronológica útil de manhã, durante a noite ou na troca entre cuidadores.', seoTitle2: 'Duas formas de indicar os horários', seoMethod: 'Escolha um intervalo quando a receita disser, por exemplo, a cada oito horas. A sequência atravessa a meia-noite sem perder a data local. Escolha horas diárias quando a indicação for manhã e noite ou horários concretos. No primeiro dia, uma hora anterior à primeira toma não é acrescentada. A duração é apenas uma janela de planeamento; o resultado mostra horários, não quantidades.', seoTitle3: 'Uma lista de controlo não é aconselhamento médico', seoSafety: 'Guarde a embalagem, a folha da clínica e os contactos. A página não conhece o animal, a concentração, a via, as interações ou a resposta. Em caso de toma esquecida, fale com a clínica antes de decidir. Marcar as linhas pode evitar duplicações e esclarecer a próxima ação entre cuidadores. O armazenamento local é opcional e pode ser limpo.', tipTitle: 'Antes de começar', tipText: 'Copie a frequência e a duração da receita, sem as calcular. Confirme o primeiro dia criado comparando-o com a etiqueta.', faq: [{ question: 'A agenda calcula a quantidade do medicamento?', answer: 'Não. Apenas organiza horários introduzidos a partir de uma receita existente. Não calcula, altera nem recomenda quantidades.' }, { question: 'Pode mostrar tomas depois da meia-noite?', answer: 'Sim. Os intervalos continuam no dia local seguinte e as horas diárias podem incluir 00:00.' }, { question: 'O que faço se uma toma atrasar ou for esquecida?', answer: 'Contacte o veterinário ou a clínica que indicou o tratamento. A lista regista o estado, mas não decide se deve compensar, saltar ou mudar a toma.' }], howTo: [{ name: 'Transcreva a receita', text: 'Introduza o nome, o primeiro momento e a duração como foram indicados pela equipa veterinária.' }, { name: 'Escolha o ritmo', text: 'Selecione um intervalo em horas ou escreva as horas diárias prescritas.' }, { name: 'Reveja o quadro', text: 'Confira o primeiro dia e a próxima toma por marcar antes de o usar como lembrete.' }, { name: 'Marque cada toma', text: 'Toque numa linha depois de administrar e mantenha as instruções originais por perto.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'raspisanie-lekarstv-dlya-pitomca', title: 'Расписание лекарств для питомца', description: 'Преобразуйте назначение ветеринара в понятное локальное расписание с ближайшими приёмами и отметками.', dateLocale: 'ru', medicationNameLabel: 'Название лекарства', medicationNamePlaceholder: 'Например: назначенный антибиотик', startDateLabel: 'Дата первого приёма', startTimeLabel: 'Время первого приёма', scheduleModeLabel: 'Как задан режим?', intervalMode: 'Через несколько часов', timesMode: 'Фиксированное время каждый день', intervalHoursLabel: 'Часов между приёмами', timesLabel: 'Ежедневное время', timesHint: 'Введите время в 24-часовом формате через запятую: 08:00, 16:00, 00:00.', durationLabel: 'Длительность лечения', durationUnit: 'дней', instructionsLabel: 'Примечания к назначению', instructionsPlaceholder: 'Кормление, способ приёма или другие указания врача', reset: 'Очистить расписание', scheduleTitle: 'Таблица приёмов', nextDoseLabel: 'Следующий непомеченный приём', noNextDose: 'Все показанные приёмы отмечены', completedCount: '{done} из {total} отмечено', markDone: 'Отметить как данное', markUndone: 'Вернуть в список', completed: 'Дано', upcoming: 'Предстоит', due: 'Проверить', emptySchedule: 'Заполните поля, чтобы увидеть таблицу.', invalidInput: 'Проверьте название, дату, время, длительность и режим.', safetyTitle: 'Точно следуйте назначению', safetyText: 'Это только напоминание и журнал. Инструмент не выбирает лекарство, не меняет дозу и не заменяет ветеринара. При задержке, пропуске или рвоте после приёма свяжитесь с клиникой.', methodText: 'Интервальный план начинается с первой даты и времени и добавляет выбранное число часов. Ежедневные часы повторяются в каждый день лечения. Данные остаются на устройстве, пока вы их не удалите.', scheduleIllustration: 'Нарисованный от руки распорядок рядом со спокойными кошкой и собакой', summary: ['Сделайте понятное расписание из готового назначения.', 'Выберите интервалы или фиксированные часы.', 'Отмечайте приёмы без отправки данных о питомце.', 'Держите название и примечания рядом с распорядком.'], seoTitle1: 'Практичное напоминание для уже назначенного лечения', seoIntro: 'После визита в клинику владельцу часто нужно совместить этикетку, выписку и несколько приёмов дома. Решение о лечении принимает ветеринар; этот планировщик лишь превращает полученные указания в удобный список. Название, первый местный момент, режим, длительность и заметки образуют хронологическую памятку для утра, ночи и передачи ухода другому человеку.', seoTitle2: 'Два способа задать время', seoMethod: 'Выберите интервал, если на этикетке написано, например, каждые восемь часов. Последовательность продолжится после полуночи. Выберите ежедневные часы, если указаны утро и вечер или точные часы. В первый день более раннее время не добавляется автоматически. Длительность ограничивает окно планирования, а результат показывает время, а не количество лекарства.', seoTitle3: 'Список не заменяет медицинский совет', seoSafety: 'Храните упаковку, выписку и номер клиники рядом. Страница не знает животное, концентрацию, путь введения, взаимодействия и реакцию. При пропущенном приёме спросите врача до принятия решения. Отметки всё же помогают избежать повторной дачи и показывают следующую задачу. Локальные данные можно удалить в любой момент.', tipTitle: 'Перед началом', tipText: 'Перепишите частоту и длительность из назначения, не угадывая. Сверьте первый день с этикеткой.', faq: [{ question: 'Рассчитывает ли планировщик дозу?', answer: 'Нет. Он только раскладывает время из существующего назначения и не рассчитывает, не меняет и не рекомендует количество.' }, { question: 'Учитывает ли он приёмы после полуночи?', answer: 'Да. Интервал переходит на следующий местный день, а среди ежедневных часов можно указать 00:00.' }, { question: 'Что делать при пропуске или задержке?', answer: 'Свяжитесь с назначившим лечение ветеринаром или клиникой. Список показывает состояние, но не решает, нужно ли наверстать, пропустить или перенести приём.' }], howTo: [{ name: 'Перенесите назначение', text: 'Укажите название, первый момент и длительность точно по инструкции врача.' }, { name: 'Выберите режим', text: 'Задайте интервал в часах или ежедневные часы из назначения.' }, { name: 'Проверьте таблицу', text: 'Сверьте первый день и следующий непомеченный приём.' }, { name: 'Отмечайте приёмы', text: 'Нажимайте строку после дачи и храните рядом исходную инструкцию.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'lakemedelsschema-for-husdjur', title: 'Läkemedelsschema för husdjur', description: 'Gör ett tydligt lokalt schema av en veterinärförskriven behandling, med kommande doser och avprickning.', dateLocale: 'sv', medicationNameLabel: 'Läkemedlets namn', medicationNamePlaceholder: 'Till exempel: förskrivet antibiotikum', startDateLabel: 'Datum för första dosen', startTimeLabel: 'Tid för första dosen', scheduleModeLabel: 'Hur anges tidsintervallet?', intervalMode: 'Med några timmars mellanrum', timesMode: 'Fasta tider varje dag', intervalHoursLabel: 'Timmar mellan doserna', timesLabel: 'Dagliga tider', timesHint: 'Använd 24-timmarsformat och separera tider med kommatecken: 08:00, 16:00, 00:00.', durationLabel: 'Behandlingens längd', durationUnit: 'dagar', instructionsLabel: 'Anteckningar från ordinationen', instructionsPlaceholder: 'Mat, hantering eller andra instruktioner från veterinären', reset: 'Rensa schemat', scheduleTitle: 'Dostavla', nextDoseLabel: 'Nästa ej avprickade dos', noNextDose: 'Alla visade doser är avprickade', completedCount: '{done} av {total} avprickade', markDone: 'Markera som given', markUndone: 'Markera som ogiven', completed: 'Klar', upcoming: 'Kommande', due: 'Kontrollera', emptySchedule: 'Fyll i fälten för att se dostavlan.', invalidInput: 'Kontrollera namn, datum, tid, längd och tidsregel.', safetyTitle: 'Följ ordinationen exakt', safetyText: 'Detta är ett hjälpmedel för påminnelse och dokumentation. Det väljer inget läkemedel, ändrar ingen dos och ersätter inte veterinären. Kontakta kliniken vid en försenad, glömd eller uppkräkt dos.', methodText: 'Ett intervallschema börjar vid första datum och tid och lägger till valda timmar. Dagliga tider upprepas under varje behandlingsdag. Allt sparas på enheten tills du raderar det.', scheduleIllustration: 'Handritat dagschema bredvid en lugn katt och hund', summary: ['Gör ett befintligt recept till ett lättläst schema.', 'Välj intervall eller fasta tider för dagen.', 'Pricka av doser utan att skicka djuruppgifter.', 'Ha namn och anteckningar nära rutinen.'], seoTitle1: 'En praktisk påminnelse för en befintlig veterinärordination', seoIntro: 'Efter ett klinikbesök behöver djurägaren ofta samordna etikett, utskrivningsblad och flera doser hemma. Veterinären avgör behandlingen; planeraren ordnar bara instruktioner som redan finns. Namn, första lokala tid, rytm, behandlingslängd och valfria anteckningar blir en kronologisk lista för morgnar, nätter och överlämning mellan personer.', seoTitle2: 'Två sätt att beskriva tiderna', seoMethod: 'Välj intervall när etiketten säger exempelvis var åttonde timme. Schemat fortsätter då över midnatt. Välj dagliga tider när ordinationen anger morgon och kväll eller bestämda klockslag. Den första dagen respekterar första dosen och lägger inte till en tidigare tid. Längden är ett planeringsfönster, inte en tolkning av receptet. Resultatet visar tider, inte mängder.', seoTitle3: 'En checklista är inte medicinsk rådgivning', seoSafety: 'Ha förpackning, klinikens papper och telefonnummer till hands. Sidan känner inte till djuret, koncentrationen, administreringsvägen, interaktioner eller reaktioner. Fråga kliniken om en dos missats. Avprickning kan ändå minska risken för dubbla doser och visa nästa uppgift tydligt. Lokal lagring är valfri och kan raderas när som helst.', tipTitle: 'Innan du börjar', tipText: 'Kopiera frekvens och längd från ordinationen utan att uppskatta. Kontrollera den första dagen mot etiketten.', faq: [{ question: 'Räknar planeraren ut dosen?', answer: 'Nej. Den ordnar endast tider som du skriver in från en befintlig ordination och räknar inte ut eller ändrar mängder.' }, { question: 'Fungerar tider efter midnatt?', answer: 'Ja. Intervall fortsätter på nästa lokala dag och dagliga tider kan innehålla 00:00.' }, { question: 'Vad gör jag om en dos missas?', answer: 'Kontakta veterinären eller kliniken som skrev ordinationen. Listan visar status men avgör inte om dosen ska tas igen, hoppas över eller flyttas.' }], howTo: [{ name: 'Skriv av ordinationen', text: 'Ange namn, första tid och längd precis som veterinärteamet angav.' }, { name: 'Välj rytm', text: 'Välj ett timintervall eller skriv de ordinerade dagliga tiderna.' }, { name: 'Granska tavlan', text: 'Kontrollera första dagen och nästa ej avprickade dos.' }, { name: 'Pricka av', text: 'Tryck på raden efter dosen och ha originalinstruktionen nära.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'evcil-hayvan-ilac-programi', title: 'Evcil Hayvan İlaç Programı', description: 'Veterinerin yazdığı tedavi planını, yaklaşan dozları ve işaretleme listesini içeren anlaşılır bir yerel programa dönüştürün.', dateLocale: 'tr', medicationNameLabel: 'İlaç adı', medicationNamePlaceholder: 'Örneğin: reçete edilen antibiyotik', startDateLabel: 'İlk doz tarihi', startTimeLabel: 'İlk doz saati', scheduleModeLabel: 'Reçetede zamanlama nasıl belirtiliyor?', intervalMode: 'Birkaç saatte bir', timesMode: 'Her gün sabit saatler', intervalHoursLabel: 'Dozlar arasındaki saat', timesLabel: 'Günlük saatler', timesHint: '24 saat biçimindeki saatleri virgülle ayırın: 08:00, 16:00, 00:00.', durationLabel: 'Tedavi süresi', durationUnit: 'gün', instructionsLabel: 'Reçete notları', instructionsPlaceholder: 'Mama, uygulama veya veterinerin diğer talimatları', reset: 'Programı temizle', scheduleTitle: 'Doz panosu', nextDoseLabel: 'İşaretlenmemiş sonraki doz', noNextDose: 'Listelenen tüm dozlar işaretlendi', completedCount: '{done} / {total} işaretlendi', markDone: 'Verildi olarak işaretle', markUndone: 'Verilmedi olarak işaretle', completed: 'Tamamlandı', upcoming: 'Sıradaki', due: 'Kontrol et', emptySchedule: 'Doz panosunu görmek için alanları doldurun.', invalidInput: 'Adı, tarihi, saati, süreyi ve zamanlama kuralını kontrol edin.', safetyTitle: 'Reçeteyi aynen uygulayın', safetyText: 'Bu araç yalnızca hatırlatma ve kayıt içindir. İlaç seçmez, dozu değiştirmez ve veterinerin yerini tutmaz. Geciken, unutulan veya kusulan bir dozda kliniği arayın.', methodText: 'Aralıklı plan ilk tarih ve saatten başlar, seçilen saatleri ekler. Günlük saatler tedavinin her gününde tekrarlanır. Bilgiler siz silene kadar bu cihazda kalır.', scheduleIllustration: 'Sakin bir kedi ve köpeğin yanında elle çizilmiş günlük program', summary: ['Mevcut reçeteyi okunabilir bir programa çevirin.', 'Saat aralığı veya günlük sabit saat seçin.', 'Evcil hayvan bilgisi göndermeden dozları işaretleyin.', 'İlaç adını ve notları rutinin yanında tutun.'], seoTitle1: 'Mevcut veteriner reçetesi için pratik hatırlatıcı', seoIntro: 'Klinik ziyaretinden sonra bakıcıların etiketi, taburcu kâğıdını ve evdeki birden fazla dozu bir araya getirmesi gerekir. Tedavi kararını veteriner verir; bu planlayıcı yalnızca elinizdeki talimatları sıralar. İlaç adı, ilk yerel tarih ve saat, zamanlama, gün sayısı ve notlar; sabah, gece veya bakıcı değişiminde okunabilecek kronolojik bir liste oluşturur.', seoTitle2: 'Saatleri belirtmenin iki yolu', seoMethod: 'Etiket her sekiz saatte bir diyorsa aralık seçin; liste gece yarısından sonra da devam eder. Reçete sabah-akşam ya da belirli saatler söylüyorsa günlük saatleri seçin. İlk gün ilk doza uyar ve daha erken bir saati kendiliğinden eklemez. Süre yalnızca planlama aralığıdır; sonuç ilaç miktarı değil, saatleri gösterir.', seoTitle3: 'Kontrol listesi tıbbi tavsiye değildir', seoSafety: 'Kutuyu, klinik belgesini ve telefon numarasını yanınızda tutun. Sayfa hayvanı, yoğunluğu, uygulama yolunu, etkileşimleri veya yanıtı bilmez. Kaçırılan bir dozda karar vermeden önce kliniğe danışın. İşaretlemek yine de iki kez verme riskini azaltabilir ve sıradaki görevi görünür kılabilir. Yerel kayıt her zaman silinebilir.', tipTitle: 'Başlamadan önce', tipText: 'Sıklığı ve süreyi reçeteden aynen kopyalayın, tahmin etmeyin. Oluşan ilk günü etiketle karşılaştırın.', faq: [{ question: 'Program doz miktarını hesaplar mı?', answer: 'Hayır. Yalnızca mevcut reçeteden girdiğiniz saatleri düzenler; miktar hesaplamaz, değiştirmez veya önermez.' }, { question: 'Gece yarısından sonraki dozları gösterir mi?', answer: 'Evet. Aralık bir sonraki yerel güne geçer ve günlük saatler içinde 00:00 kullanılabilir.' }, { question: 'Bir doz gecikirse veya unutulursa ne yapmalıyım?', answer: 'Veterinerle veya reçeteyi yazan klinikle iletişime geçin. Liste durumu kaydeder ama dozu telafi etme, atlama veya taşıma kararı vermez.' }], howTo: [{ name: 'Reçeteyi aktarın', text: 'Adı, ilk zamanı ve süreyi veteriner ekibinin verdiği şekilde yazın.' }, { name: 'Zamanlamayı seçin', text: 'Saat aralığını veya reçetedeki günlük saatleri kullanın.' }, { name: 'Panoyu inceleyin', text: 'İlk günü ve işaretlenmemiş sonraki dozu kontrol edin.' }, { name: 'Dozları işaretleyin', text: 'Verdikten sonra satıra dokunun ve asıl talimatları yakında tutun.' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createContent, type MedicationCopy } from './content';
|
|
2
|
+
import { copy as en } from './en';
|
|
3
|
+
|
|
4
|
+
const copy: MedicationCopy = { ...en, slug: 'chong-wu-yong-yao-ri-cheng', title: '宠物用药日程表', description: '把兽医已经开具的用药安排整理成清晰的本地日程,显示下一次用药并支持完成标记。', dateLocale: 'zh-CN', medicationNameLabel: '药物名称', medicationNamePlaceholder: '例如:处方抗生素', startDateLabel: '第一次用药日期', startTimeLabel: '第一次用药时间', scheduleModeLabel: '处方如何规定时间?', intervalMode: '每隔几小时', timesMode: '每天固定时间', intervalHoursLabel: '两次用药间隔(小时)', timesLabel: '每天的时间', timesHint: '使用24小时制,用逗号分隔,例如 08:00、16:00、00:00。', durationLabel: '治疗时长', durationUnit: '天', instructionsLabel: '处方备注', instructionsPlaceholder: '进食、喂药方式或兽医的其他说明', reset: '清除日程', scheduleTitle: '用药看板', nextDoseLabel: '下一次未确认的用药', noNextDose: '显示的用药都已确认', completedCount: '已确认 {done} / {total}', markDone: '标记为已用药', markUndone: '标记为未用药', completed: '已完成', upcoming: '即将到来', due: '查看', emptySchedule: '填写上方内容后即可查看用药看板。', invalidInput: '请检查药名、日期、时间、时长和时间规则。', safetyTitle: '严格按照处方执行', safetyText: '这是用于提醒和记录的辅助工具,不会选择药物、修改剂量,也不能替代兽医建议。如果用药迟了、漏服或发生呕吐,请联系开方的诊所。', methodText: '间隔模式从第一次日期和时间开始,按指定小时数生成后续时间。每日时间模式会在治疗期间重复输入的时刻。除非主动清除,信息只保存在本设备上。', scheduleIllustration: '平静的猫和狗旁边的一张手绘每日用药表', summary: ['把现有处方整理成容易阅读的时间表。', '选择时间间隔或每天固定的时刻。', '不发送宠物信息,也能标记每次用药。', '在日程旁边保留药名和备注。'], seoTitle1: '为现有兽医处方准备的实用提醒', seoIntro: '看完兽医后,照护者通常需要在家里同时参考药品标签、出院说明和多次用药安排。治疗方案由兽医决定,本工具只整理已经收到的说明。输入药名、第一次用药时间、重复方式、天数和备注,就能得到一份按时间排列的清单,适合忙碌的早晨、夜间照护以及不同照护者之间交接。', seoTitle2: '两种填写时间的方法', seoMethod: '如果标签写着每八小时一次,请选择间隔模式,日程会自然跨过午夜。如果处方写着早晚各一次或列出具体时间,请选择每天固定时间。第一天会尊重第一次用药时间,不会悄悄加入更早的时间。治疗时长只是日程范围,并不是对处方的解释;结果显示时间,不显示药量。', seoTitle3: '检查清单不是医疗建议', seoSafety: '请把药盒、诊所说明和联系电话放在手边。本页面不知道宠物情况、浓度、给药途径、相互作用或实际反应。如果漏服或不确定,请先咨询诊所。完成标记可以减少重复用药,并让下一位照护者清楚下一步。设备内保存的数据可以随时清除。', tipTitle: '开始前', tipText: '从处方原样抄录频率和时长,不要自行估计。使用前把生成的第一天与药品标签核对。', faq: [{ question: '这个工具会计算药量吗?', answer: '不会。它只整理根据现有处方输入的时间,不会计算、修改或推荐用量。' }, { question: '可以处理跨午夜的用药吗?', answer: '可以。间隔模式会进入下一个本地日期,每日时间也可以填写00:00。' }, { question: '如果用药晚了或漏了怎么办?', answer: '请联系开具处方的兽医或诊所。清单可以记录状态,但不会决定是否补用、跳过或调整时间。' }], howTo: [{ name: '录入处方信息', text: '按照兽医说明填写药名、第一次用药时间和治疗时长。' }, { name: '选择时间模式', text: '选择小时间隔,或填写处方中列出的每日时间。' }, { name: '检查日程', text: '开始使用前核对第一天和下一次未确认的用药。' }, { name: '标记每次用药', text: '完成用药后点击对应行,并把原始说明放在附近。' }] };
|
|
5
|
+
|
|
6
|
+
export const content = createContent(copy);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../types';
|
|
2
|
+
import { petMedicationSchedulePlanner } from './entry';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
export * from './logic';
|
|
6
|
+
|
|
7
|
+
export const PET_MEDICATION_SCHEDULE_PLANNER_TOOL: ToolDefinition = {
|
|
8
|
+
entry: petMedicationSchedulePlanner,
|
|
9
|
+
Component: () => import('./component.astro'),
|
|
10
|
+
SEOComponent: () => import('./seo.astro'),
|
|
11
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateSchedule, getDateKey, type MedicationScheduleInput } from './logic';
|
|
3
|
+
|
|
4
|
+
const base: MedicationScheduleInput = {
|
|
5
|
+
medicationName: 'Prescribed medicine', startDate: '2026-09-05', startTime: '22:00', mode: 'interval', intervalHours: 8, dailyTimes: '', durationDays: 2, instructions: '',
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
describe('pet medication schedule logic', () => {
|
|
9
|
+
it('creates interval doses across midnight without changing the local date incorrectly', () => {
|
|
10
|
+
const doses = generateSchedule(base);
|
|
11
|
+
expect(doses.map((dose) => `${getDateKey(dose.date)} ${dose.time}`)).toEqual(['2026-09-05 22:00', '2026-09-06 06:00', '2026-09-06 14:00', '2026-09-06 22:00', '2026-09-07 06:00', '2026-09-07 14:00']);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('creates sorted daily-time doses and removes duplicates', () => {
|
|
15
|
+
const doses = generateSchedule({ ...base, startTime: '07:00', mode: 'times', dailyTimes: '20:00, 08:00, 08:00', durationDays: 2 });
|
|
16
|
+
expect(doses.map((dose) => dose.time)).toEqual(['08:00', '20:00', '08:00', '20:00']);
|
|
17
|
+
expect(doses[2]?.dateKey).toBe('2026-09-06');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('rejects a schedule without a medication or usable timing', () => {
|
|
21
|
+
expect(() => generateSchedule({ ...base, medicationName: '' })).toThrow('Missing medication name');
|
|
22
|
+
expect(() => generateSchedule({ ...base, mode: 'times', dailyTimes: 'later' })).toThrow('Missing times');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type ScheduleMode = 'interval' | 'times';
|
|
2
|
+
|
|
3
|
+
export interface MedicationScheduleInput {
|
|
4
|
+
medicationName: string;
|
|
5
|
+
startDate: string;
|
|
6
|
+
startTime: string;
|
|
7
|
+
mode: ScheduleMode;
|
|
8
|
+
intervalHours: number;
|
|
9
|
+
dailyTimes: string;
|
|
10
|
+
durationDays: number;
|
|
11
|
+
instructions: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ScheduledDose {
|
|
15
|
+
id: string;
|
|
16
|
+
date: Date;
|
|
17
|
+
dateKey: string;
|
|
18
|
+
time: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getDateKey(date: Date): string {
|
|
22
|
+
return [date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0')].join('-');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseClock(value: string): number | null {
|
|
26
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const hours = Number(match[1]);
|
|
29
|
+
const minutes = Number(match[2]);
|
|
30
|
+
return hours <= 23 && minutes <= 59 ? hours * 60 + minutes : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseStart(input: MedicationScheduleInput): Date {
|
|
34
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(input.startDate)) throw new Error('Invalid date');
|
|
35
|
+
const minutes = parseClock(input.startTime);
|
|
36
|
+
if (minutes === null) throw new Error('Invalid time');
|
|
37
|
+
const parts = input.startDate.split('-').map(Number);
|
|
38
|
+
const year = parts[0] ?? 0;
|
|
39
|
+
const month = parts[1] ?? 0;
|
|
40
|
+
const day = parts[2] ?? 0;
|
|
41
|
+
const date = new Date(year, month - 1, day, Math.floor(minutes / 60), minutes % 60, 0, 0);
|
|
42
|
+
if (Number.isNaN(date.getTime()) || getDateKey(date) !== input.startDate) throw new Error('Invalid date');
|
|
43
|
+
return date;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatClock(minutes: number): string {
|
|
47
|
+
return `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createDose(date: Date): ScheduledDose {
|
|
51
|
+
const copy = new Date(date);
|
|
52
|
+
const time = formatClock(copy.getHours() * 60 + copy.getMinutes());
|
|
53
|
+
return { id: `${getDateKey(copy)}T${time}`, date: copy, dateKey: getDateKey(copy), time };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readDailyTimes(value: string): number[] {
|
|
57
|
+
const values = value.split(/[,\s]+/).map((part) => parseClock(part)).filter((part): part is number => part !== null);
|
|
58
|
+
return [...new Set(values)].sort((left, right) => left - right);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateScheduleInput(input: MedicationScheduleInput): void {
|
|
62
|
+
if (!input.medicationName.trim()) throw new Error('Missing medication name');
|
|
63
|
+
parseStart(input);
|
|
64
|
+
validateDuration(input.durationDays);
|
|
65
|
+
validateTiming(input);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateDuration(durationDays: number): void {
|
|
69
|
+
if (!Number.isInteger(durationDays) || durationDays < 1 || durationDays > 30) throw new Error('Invalid duration');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validateTiming(input: MedicationScheduleInput): void {
|
|
73
|
+
if (input.mode === 'interval') {
|
|
74
|
+
if (!Number.isFinite(input.intervalHours) || input.intervalHours < 1 || input.intervalHours > 24) throw new Error('Invalid interval');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (readDailyTimes(input.dailyTimes).length === 0) throw new Error('Missing times');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function generateDailySchedule(input: MedicationScheduleInput, start: Date, end: Date): ScheduledDose[] {
|
|
81
|
+
const doses: ScheduledDose[] = [];
|
|
82
|
+
const times = readDailyTimes(input.dailyTimes);
|
|
83
|
+
for (let day = 0; day < input.durationDays; day += 1) {
|
|
84
|
+
for (const minutes of times) {
|
|
85
|
+
const dose = new Date(start);
|
|
86
|
+
dose.setDate(start.getDate() + day);
|
|
87
|
+
dose.setHours(Math.floor(minutes / 60), minutes % 60, 0, 0);
|
|
88
|
+
if (dose >= start && dose < end) doses.push(createDose(dose));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return doses;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function generateSchedule(input: MedicationScheduleInput): ScheduledDose[] {
|
|
95
|
+
validateScheduleInput(input);
|
|
96
|
+
const start = parseStart(input);
|
|
97
|
+
const end = new Date(start);
|
|
98
|
+
end.setDate(end.getDate() + input.durationDays);
|
|
99
|
+
const doses: ScheduledDose[] = [];
|
|
100
|
+
|
|
101
|
+
if (input.mode === 'interval') {
|
|
102
|
+
const cursor = new Date(start);
|
|
103
|
+
while (cursor < end && doses.length < 200) {
|
|
104
|
+
doses.push(createDose(cursor));
|
|
105
|
+
cursor.setTime(cursor.getTime() + input.intervalHours * 60 * 60 * 1000);
|
|
106
|
+
}
|
|
107
|
+
} else doses.push(...generateDailySchedule(input, start, end));
|
|
108
|
+
|
|
109
|
+
return doses.sort((left, right) => left.date.getTime() - right.date.getTime());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function formatTime(date: Date, locale: string): string {
|
|
113
|
+
return new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' }).format(date);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatDay(date: Date, locale: string): string {
|
|
117
|
+
return new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'short', day: 'numeric' }).format(date);
|
|
118
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
.pet-medication-schedule-planner {
|
|
2
|
+
--med-ink: #17343b;
|
|
3
|
+
--med-muted: #5c7477;
|
|
4
|
+
--med-paper: #f7f0e6;
|
|
5
|
+
--med-card: #fffdf8;
|
|
6
|
+
--med-line: #d2ddd8;
|
|
7
|
+
--med-teal: #087c78;
|
|
8
|
+
--med-teal-dark: #073f43;
|
|
9
|
+
--med-coral: #d9624b;
|
|
10
|
+
--med-coral-soft: #f4d7ca;
|
|
11
|
+
--med-yellow: #efc66e;
|
|
12
|
+
--med-focus: #e99882;
|
|
13
|
+
--med-result: #123840;
|
|
14
|
+
--med-result-muted: #b8d4d0;
|
|
15
|
+
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
max-width: 70rem;
|
|
18
|
+
margin: 2rem auto;
|
|
19
|
+
padding: clamp(1rem, 3vw, 2rem);
|
|
20
|
+
border: 1px solid var(--med-line);
|
|
21
|
+
border-radius: 1.5rem;
|
|
22
|
+
color: var(--med-ink);
|
|
23
|
+
background: var(--med-paper);
|
|
24
|
+
box-shadow: 0.45rem 0.45rem 0 rgba(20, 62, 58, 0.12);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.pet-medication-schedule-planner .medication-workbench,
|
|
28
|
+
.pet-medication-schedule-planner .result-top,
|
|
29
|
+
.pet-medication-schedule-planner .two-fields {
|
|
30
|
+
display: grid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.pet-medication-schedule-planner p,
|
|
34
|
+
.pet-medication-schedule-planner h2,
|
|
35
|
+
.pet-medication-schedule-planner h3 {
|
|
36
|
+
margin: 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.pet-medication-schedule-planner .form-heading p,
|
|
40
|
+
.pet-medication-schedule-planner .result-kicker {
|
|
41
|
+
color: var(--med-coral);
|
|
42
|
+
font-size: 0.7rem;
|
|
43
|
+
font-weight: 900;
|
|
44
|
+
letter-spacing: 0.13em;
|
|
45
|
+
line-height: 1.3;
|
|
46
|
+
text-transform: uppercase;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.pet-medication-schedule-planner .medication-workbench {
|
|
50
|
+
grid-template-columns: minmax(17rem, 0.78fr) minmax(0, 1.22fr);
|
|
51
|
+
gap: 1rem;
|
|
52
|
+
margin-top: 1rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.pet-medication-schedule-planner .medication-form,
|
|
56
|
+
.pet-medication-schedule-planner .medication-result {
|
|
57
|
+
min-width: 0;
|
|
58
|
+
padding: clamp(1rem, 2.5vw, 1.5rem);
|
|
59
|
+
border: 1px solid var(--med-line);
|
|
60
|
+
border-radius: 1.15rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.theme-dark .pet-medication-schedule-planner {
|
|
64
|
+
--med-ink: #eaf6f3;
|
|
65
|
+
--med-muted: #a6c2c2;
|
|
66
|
+
--med-paper: #10242c;
|
|
67
|
+
--med-card: #183a43;
|
|
68
|
+
--med-line: #3b6269;
|
|
69
|
+
--med-teal: #49cabb;
|
|
70
|
+
--med-teal-dark: #0a2d34;
|
|
71
|
+
--med-coral: #ff9a80;
|
|
72
|
+
--med-coral-soft: #5a3435;
|
|
73
|
+
--med-yellow: #f2c86d;
|
|
74
|
+
--med-focus: #ffb39d;
|
|
75
|
+
--med-result: #0a252d;
|
|
76
|
+
--med-result-muted: #c1dbd6;
|
|
77
|
+
|
|
78
|
+
border-color: var(--med-line);
|
|
79
|
+
background: var(--med-paper);
|
|
80
|
+
box-shadow: 0.45rem 0.45rem 0 rgba(0, 0, 0, 0.28);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.pet-medication-schedule-planner .medication-form {
|
|
84
|
+
background: var(--med-card);
|
|
85
|
+
}
|
|
86
|
+
.pet-medication-schedule-planner .medication-result {
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
color: #fffdf8;
|
|
89
|
+
background: var(--med-result);
|
|
90
|
+
border-color: var(--med-result);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.pet-medication-schedule-planner .form-heading {
|
|
94
|
+
display: flex;
|
|
95
|
+
gap: 0.75rem;
|
|
96
|
+
align-items: flex-start;
|
|
97
|
+
margin-bottom: 1.3rem;
|
|
98
|
+
padding-bottom: 1rem;
|
|
99
|
+
border-bottom: 1px solid var(--med-line);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.pet-medication-schedule-planner .form-heading > span {
|
|
103
|
+
display: grid;
|
|
104
|
+
flex: 0 0 2.2rem;
|
|
105
|
+
place-items: center;
|
|
106
|
+
width: 2.2rem;
|
|
107
|
+
height: 2.2rem;
|
|
108
|
+
border-radius: 0.65rem;
|
|
109
|
+
color: var(--med-card);
|
|
110
|
+
background: var(--med-coral);
|
|
111
|
+
font-size: 0.75rem;
|
|
112
|
+
font-weight: 900;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.pet-medication-schedule-planner .form-heading h2,
|
|
116
|
+
.pet-medication-schedule-planner .result-top h2 {
|
|
117
|
+
margin-top: 0.15rem;
|
|
118
|
+
font-size: 1.35rem;
|
|
119
|
+
line-height: 1.1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.pet-medication-schedule-planner .form-heading p {
|
|
123
|
+
color: var(--med-muted);
|
|
124
|
+
font-size: 0.68rem;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.pet-medication-schedule-planner label,
|
|
128
|
+
.pet-medication-schedule-planner fieldset {
|
|
129
|
+
display: grid;
|
|
130
|
+
gap: 0.4rem;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
margin: 0 0 1rem;
|
|
133
|
+
padding: 0;
|
|
134
|
+
border: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.pet-medication-schedule-planner label > span,
|
|
138
|
+
.pet-medication-schedule-planner legend {
|
|
139
|
+
color: var(--med-ink);
|
|
140
|
+
font-size: 0.85rem;
|
|
141
|
+
font-weight: 800;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.pet-medication-schedule-planner .two-fields {
|
|
145
|
+
grid-template-columns: 1fr 1fr;
|
|
146
|
+
gap: 0.65rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.pet-medication-schedule-planner input,
|
|
150
|
+
.pet-medication-schedule-planner textarea {
|
|
151
|
+
box-sizing: border-box;
|
|
152
|
+
width: 100%;
|
|
153
|
+
border: 1px solid var(--med-line);
|
|
154
|
+
border-radius: 0.65rem;
|
|
155
|
+
padding: 0.65rem 0.7rem;
|
|
156
|
+
color: var(--med-ink);
|
|
157
|
+
background: var(--med-card);
|
|
158
|
+
font: inherit;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.pet-medication-schedule-planner textarea {
|
|
162
|
+
resize: vertical;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.pet-medication-schedule-planner input:focus-visible,
|
|
166
|
+
.pet-medication-schedule-planner textarea:focus-visible,
|
|
167
|
+
.pet-medication-schedule-planner button:focus-visible {
|
|
168
|
+
outline: 3px solid var(--med-focus);
|
|
169
|
+
outline-offset: 2px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.pet-medication-schedule-planner small {
|
|
173
|
+
display: block;
|
|
174
|
+
color: var(--med-muted);
|
|
175
|
+
font-size: 0.72rem;
|
|
176
|
+
font-weight: 400;
|
|
177
|
+
line-height: 1.3;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.pet-medication-schedule-planner .mode-buttons {
|
|
181
|
+
display: grid;
|
|
182
|
+
grid-template-columns: 1fr 1fr;
|
|
183
|
+
gap: 0.5rem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.pet-medication-schedule-planner button {
|
|
187
|
+
min-height: 2.65rem;
|
|
188
|
+
border: 1px solid var(--med-line);
|
|
189
|
+
border-radius: 0.7rem;
|
|
190
|
+
padding: 0.55rem 0.75rem;
|
|
191
|
+
color: var(--med-ink);
|
|
192
|
+
background: var(--med-card);
|
|
193
|
+
font: inherit;
|
|
194
|
+
font-weight: 700;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
transition:
|
|
197
|
+
transform 160ms ease,
|
|
198
|
+
border-color 160ms ease,
|
|
199
|
+
background 160ms ease;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.pet-medication-schedule-planner button:hover,
|
|
203
|
+
.pet-medication-schedule-planner button:focus-visible {
|
|
204
|
+
border-color: var(--med-teal);
|
|
205
|
+
transform: translateY(-1px);
|
|
206
|
+
}
|
|
207
|
+
.pet-medication-schedule-planner .mode-buttons button.is-active {
|
|
208
|
+
color: var(--med-card);
|
|
209
|
+
background: var(--med-teal);
|
|
210
|
+
border-color: var(--med-teal);
|
|
211
|
+
}
|
|
212
|
+
.pet-medication-schedule-planner .with-suffix {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
gap: 0.55rem;
|
|
216
|
+
}
|
|
217
|
+
.pet-medication-schedule-planner .with-suffix input {
|
|
218
|
+
max-width: 7rem;
|
|
219
|
+
}
|
|
220
|
+
.pet-medication-schedule-planner .with-suffix b {
|
|
221
|
+
color: var(--med-muted);
|
|
222
|
+
font-size: 0.8rem;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.pet-medication-schedule-planner .form-error {
|
|
226
|
+
min-height: 1.3rem;
|
|
227
|
+
color: var(--med-coral);
|
|
228
|
+
font-size: 0.82rem;
|
|
229
|
+
font-weight: 700;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.pet-medication-schedule-planner .reset-button {
|
|
233
|
+
width: 100%;
|
|
234
|
+
color: var(--med-muted);
|
|
235
|
+
}
|
|
236
|
+
.pet-medication-schedule-planner .result-top {
|
|
237
|
+
grid-template-columns: minmax(0, 1fr) minmax(9rem, 0.75fr);
|
|
238
|
+
gap: 1rem;
|
|
239
|
+
align-items: start;
|
|
240
|
+
}
|
|
241
|
+
.pet-medication-schedule-planner .result-top h2 {
|
|
242
|
+
color: #fffdf8;
|
|
243
|
+
}
|
|
244
|
+
.pet-medication-schedule-planner .next-dose {
|
|
245
|
+
padding: 0.75rem;
|
|
246
|
+
border: 1px solid rgba(184, 212, 208, 0.35);
|
|
247
|
+
border-radius: 0.8rem;
|
|
248
|
+
background: rgba(255, 255, 255, 0.07);
|
|
249
|
+
}
|
|
250
|
+
.pet-medication-schedule-planner .next-dose span,
|
|
251
|
+
.pet-medication-schedule-planner .next-dose strong {
|
|
252
|
+
display: block;
|
|
253
|
+
}
|
|
254
|
+
.pet-medication-schedule-planner .next-dose span {
|
|
255
|
+
color: var(--med-result-muted);
|
|
256
|
+
font-size: 0.7rem;
|
|
257
|
+
}
|
|
258
|
+
.pet-medication-schedule-planner .next-dose strong {
|
|
259
|
+
margin-top: 0.25rem;
|
|
260
|
+
line-height: 1.3;
|
|
261
|
+
}
|
|
262
|
+
.pet-medication-schedule-planner .progress-line {
|
|
263
|
+
display: flex;
|
|
264
|
+
gap: 0.7rem;
|
|
265
|
+
align-items: center;
|
|
266
|
+
margin: 1.2rem 0 0.8rem;
|
|
267
|
+
color: var(--med-result-muted);
|
|
268
|
+
font-size: 0.78rem;
|
|
269
|
+
}
|
|
270
|
+
.pet-medication-schedule-planner .progress-rule {
|
|
271
|
+
flex: 1;
|
|
272
|
+
height: 1px;
|
|
273
|
+
background: rgba(184, 212, 208, 0.35);
|
|
274
|
+
}
|
|
275
|
+
.pet-medication-schedule-planner .schedule-list {
|
|
276
|
+
display: grid;
|
|
277
|
+
gap: 0.95rem;
|
|
278
|
+
max-height: 35rem;
|
|
279
|
+
overflow: auto;
|
|
280
|
+
padding-right: 0.25rem;
|
|
281
|
+
}
|
|
282
|
+
.pet-medication-schedule-planner .dose-day {
|
|
283
|
+
display: grid;
|
|
284
|
+
gap: 0.45rem;
|
|
285
|
+
}
|
|
286
|
+
.pet-medication-schedule-planner .dose-day h3 {
|
|
287
|
+
color: var(--med-yellow);
|
|
288
|
+
font-size: 0.85rem;
|
|
289
|
+
text-transform: capitalize;
|
|
290
|
+
}
|
|
291
|
+
.pet-medication-schedule-planner .dose-row {
|
|
292
|
+
display: grid;
|
|
293
|
+
grid-template-columns: 1.25rem minmax(0, 1fr) auto;
|
|
294
|
+
gap: 0.65rem;
|
|
295
|
+
align-items: center;
|
|
296
|
+
min-height: 3rem;
|
|
297
|
+
text-align: left;
|
|
298
|
+
color: #fffdf8;
|
|
299
|
+
background: rgba(255, 255, 255, 0.08);
|
|
300
|
+
border-color: rgba(184, 212, 208, 0.25);
|
|
301
|
+
}
|
|
302
|
+
.pet-medication-schedule-planner .dose-row.is-complete {
|
|
303
|
+
color: var(--med-result-muted);
|
|
304
|
+
background: rgba(59, 195, 181, 0.16);
|
|
305
|
+
}
|
|
306
|
+
.pet-medication-schedule-planner .dose-check {
|
|
307
|
+
width: 0.95rem;
|
|
308
|
+
height: 0.95rem;
|
|
309
|
+
border: 2px solid var(--med-result-muted);
|
|
310
|
+
border-radius: 0.3rem;
|
|
311
|
+
}
|
|
312
|
+
.pet-medication-schedule-planner .dose-row.is-complete .dose-check {
|
|
313
|
+
border-color: var(--med-yellow);
|
|
314
|
+
background: var(--med-yellow);
|
|
315
|
+
box-shadow: inset 0 0 0 3px var(--med-result);
|
|
316
|
+
}
|
|
317
|
+
.pet-medication-schedule-planner .dose-time {
|
|
318
|
+
font-size: 1.05rem;
|
|
319
|
+
font-variant-numeric: tabular-nums;
|
|
320
|
+
}
|
|
321
|
+
.pet-medication-schedule-planner .dose-state {
|
|
322
|
+
color: var(--med-result-muted);
|
|
323
|
+
font-size: 0.72rem;
|
|
324
|
+
}
|
|
325
|
+
.pet-medication-schedule-planner .empty-schedule {
|
|
326
|
+
color: var(--med-result-muted);
|
|
327
|
+
font-size: 0.9rem;
|
|
328
|
+
}
|
|
329
|
+
.pet-medication-schedule-planner .medication-safety,
|
|
330
|
+
.pet-medication-schedule-planner .medication-method {
|
|
331
|
+
display: flex;
|
|
332
|
+
gap: 0.6rem;
|
|
333
|
+
margin: 1rem 0 0;
|
|
334
|
+
color: var(--med-muted);
|
|
335
|
+
font-size: 0.82rem;
|
|
336
|
+
line-height: 1.5;
|
|
337
|
+
}
|
|
338
|
+
.pet-medication-schedule-planner .medication-safety {
|
|
339
|
+
padding: 0.9rem 1rem;
|
|
340
|
+
border-left: 0.25rem solid var(--med-coral);
|
|
341
|
+
background: color-mix(in srgb, var(--med-coral-soft) 60%, transparent);
|
|
342
|
+
}
|
|
343
|
+
.pet-medication-schedule-planner .medication-safety strong,
|
|
344
|
+
.pet-medication-schedule-planner .medication-method strong {
|
|
345
|
+
color: var(--med-ink);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@media (max-width: 760px) {
|
|
349
|
+
.pet-medication-schedule-planner .medication-workbench {
|
|
350
|
+
grid-template-columns: 1fr;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@media (max-width: 480px) {
|
|
355
|
+
.pet-medication-schedule-planner .two-fields,
|
|
356
|
+
.pet-medication-schedule-planner .result-top {
|
|
357
|
+
grid-template-columns: 1fr;
|
|
358
|
+
}
|
|
359
|
+
.pet-medication-schedule-planner .medication-safety,
|
|
360
|
+
.pet-medication-schedule-planner .medication-method {
|
|
361
|
+
display: block;
|
|
362
|
+
}
|
|
363
|
+
.pet-medication-schedule-planner .medication-safety strong,
|
|
364
|
+
.pet-medication-schedule-planner .medication-method strong {
|
|
365
|
+
display: block;
|
|
366
|
+
margin-bottom: 0.25rem;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { petMedicationSchedulePlanner } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props { locale?: KnownLocale; }
|
|
7
|
+
const { locale = 'en' } = Astro.props as Props;
|
|
8
|
+
const content = await petMedicationSchedulePlanner.i18n[locale]?.();
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|