Add calendar toolbox with weekly and monthly scheduling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
This commit is contained in:
parent
4f73e0068b
commit
992665f4ea
39 changed files with 4331 additions and 36 deletions
|
|
@ -182,6 +182,38 @@ Alertes timer :
|
|||
- le garde-fou bloque uniquement les comptes a rebours recurrents (`Pattern horaire`, `Intervalle`) si la repetition est inferieure a 5 minutes ;
|
||||
- un `Pattern horaire` comme `X:24:X` cible `hh:24:00` et se repete toutes les heures.
|
||||
|
||||
### Calendrier
|
||||
|
||||
Le calendrier est un outil de planification visuelle, distinct du timer.
|
||||
|
||||
Regles UI :
|
||||
|
||||
- l'outil propose une grille hebdomadaire jours x creneaux de 30 minutes ou une grille mensuelle datée avec navigation par mois ;
|
||||
- les 7 jours de la semaine doivent rester visibles sans scroll horizontal ; seul le scroll vertical sert a parcourir les heures ;
|
||||
- le formulaire compact est masque par defaut ; il s'ouvre via le bouton `+` du module ou par clic sur une entree ;
|
||||
- le bouton annuler ferme le formulaire, et desactiver le bouton `+` le ferme aussi quand il sert a creer une entree ;
|
||||
- l'ajout manuel et la selection de cellules alimentent le meme formulaire compact quand il est ouvert ;
|
||||
- les champs horaires hebdomadaires restent courts, acceptent un pas de 5 minutes, et les alertes d'echeance utilisent des checkboxes `10 min`, `5 min` et `Au debut et pendant` ;
|
||||
- les evenements sont affiches comme des blocs colores dans la grille, sans boutons internes et sans dominer la palette globale ;
|
||||
- quand deux evenements se chevauchent sur un meme jour, ils se partagent la largeur de la colonne ; la V1 gere visuellement jusqu'a deux evenements simultanes ;
|
||||
- la suppression d'une entree se fait dans le formulaire d'edition ;
|
||||
- la copie et le collage d'entree utilisent les raccourcis clavier `Ctrl+C` et `Ctrl+V` ;
|
||||
- le mini-switch repetable est actif par defaut ; une entree non repetable est supprimee quand son echeance de la semaine courante est terminee ;
|
||||
- les alertes reutilisent le switch a 3 etats du timer : aucune, toolbox visible, globale site ;
|
||||
- le choix de couleur reprend le pattern de palette compacte du bloc-notes ;
|
||||
- le choix initial s'affiche dans l'outil lui-meme ; la vue mensuelle affiche le nom du mois et des boutons mois precedent / courant / suivant ;
|
||||
- en vue mensuelle, la règle mensuelle peut cibler le numero du jour ou le rang du jour de semaine via un switch 2 états compact, indépendamment du choix de répétition.
|
||||
- en vue mensuelle, les dates de debut et de fin utilisent un champ compact qui ouvre un mini-calendrier avec navigation ;
|
||||
- en vue mensuelle, le formulaire distingue date de debut et date de fin ; une plage d'un seul jour utilise la meme valeur pour les deux ;
|
||||
- en vue mensuelle, la répétition propose `Aucune`, `Chaque semaine`, `Chaque mois`; `Chaque semaine` est indisponible pour la règle `Numéro du jour` ;
|
||||
- en vue mensuelle, la suppression d'une entree repetee distingue suppression de l'occurrence affichee et suppression de toute la serie ;
|
||||
- en vue mensuelle, un clic-glisse sur plusieurs cellules preconfigure la plage de jours du formulaire ;
|
||||
- en vue mensuelle, les champs horaires ne sont pas affiches ; les evenements couvrent des journees completes ;
|
||||
- en vue mensuelle, les evenements multi-jours s'affichent comme une barre horizontale et ne sont pas dupliques dans chaque cellule ;
|
||||
- les vues hebdomadaire et mensuelle affichent un indicateur discret du moment actuel.
|
||||
- en vue hebdomadaire, un switch standard peut masquer les longues plages horaires vides ; la tranche masquée reste indiquée dans la grille, pas dans un texte qui déborde de la colonne horaire.
|
||||
- le calendrier supporte l'import/export texte via le panneau d'ajout, pour les vues hebdomadaire et mensuelle.
|
||||
|
||||
### Combos
|
||||
|
||||
L'outil Combos affiche des inputs de manettes et clavier sous forme de tokens visuels compacts.
|
||||
|
|
|
|||
|
|
@ -402,6 +402,62 @@ Stockage compact :
|
|||
- `countdowns[].startMode` et `countdowns[].startTime` sont omis pour les intervalles qui démarrent à la création.
|
||||
- L'activation d'une alerte est bloquée dans l'interface si la fréquence configurée est inférieure à 5 minutes pour les comptes à rebours récurrents (`time_pattern` et `interval`). Le blocage ne dépend pas du temps restant avant la prochaine échéance.
|
||||
|
||||
## Outil Calendrier
|
||||
|
||||
Type : `calendar`
|
||||
|
||||
```json
|
||||
{
|
||||
"initialized": true,
|
||||
"viewMode": "month",
|
||||
"weekStartsOn": 1,
|
||||
"events": [
|
||||
{
|
||||
"id": "calendar1",
|
||||
"title": "Raid",
|
||||
"startDate": "2026-08-14",
|
||||
"endDate": "2026-08-16",
|
||||
"repeat": "monthly",
|
||||
"monthlyRepeatBy": "weekday",
|
||||
"excludedOccurrences": ["2026-09-11"],
|
||||
"alertMode": "site",
|
||||
"alertOffsetMinutes": [10, 5, 0],
|
||||
"color": "violet"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Stockage compact :
|
||||
|
||||
- `viewMode` vaut `week` ou `month` ; `week` est omis dans ce cas.
|
||||
- `initialized` indique que le choix du type de calendrier a été validé dans l'outil ; il est conservé pour garder un calendrier vide utilisable.
|
||||
- `weekStartsOn` vaut `1` par défaut et est omis dans ce cas.
|
||||
- `events` est omis tant qu'aucun événement valide n'est configuré.
|
||||
- les événements sans titre ou sans cible de jour/date valide sont supprimés ; `startMinutes` reste requis en hebdomadaire et vaut `0` par défaut en mensuel.
|
||||
- `dayIndex` vaut `0` pour lundi, `1` pour mardi, jusqu'à `6` pour dimanche.
|
||||
- en calendrier mensuel, `startDate` et `endDate` sont des dates locales `YYYY-MM-DD` ; `endDate` est omis si la plage dure un seul jour.
|
||||
- en calendrier mensuel, `monthlyRepeatBy` vaut `monthDay` ou `weekday` ; `monthDay` est la valeur par défaut et est omise dans ce cas.
|
||||
- en calendrier mensuel, `repeat` vaut `none`, `weekly` ou `monthly` ; `monthly` est la valeur par défaut et est omise dans ce cas.
|
||||
- `repeat=weekly` est réservé aux entrées mensuelles par `weekday` ; si une entrée par `monthDay` demande `weekly`, elle est normalisée en `none`.
|
||||
- une entrée mensuelle répétée par `monthDay` n'a pas d'occurrence dans un mois où le jour demandé n'existe pas.
|
||||
- une entrée mensuelle répétée par `weekday` et `monthly` se répète sur le même rang de jour de semaine que `startDate` ; par exemple le premier lundi du mois.
|
||||
- `excludedOccurrences` contient des dates locales `YYYY-MM-DD` correspondant aux occurrences supprimées individuellement.
|
||||
- `startMinutes` et `endMinutes` sont des minutes depuis `00:00` dans la journée ; en hebdomadaire, l'interface saisit les heures au pas de 5 minutes tout en gardant une grille visuelle de 30 minutes ; en mensuel, les champs horaires ne sont pas exposés et `startMinutes` est omis s'il vaut `0`.
|
||||
- `endMinutes` est conservé s'il produit une fin postérieure au début ; en mensuel, il peut être inférieur à `startMinutes` si la fin est sur un jour ultérieur.
|
||||
- en hebdomadaire, `repeat` n'est pas stocké dans le format cible : il est déduit de `repeatable` (`weekly` par défaut, `none` si `repeatable=false`).
|
||||
- en hebdomadaire, `repeatable` vaut `true` par défaut et est omis dans ce cas ; si `false`, l'entrée est supprimée quand son échéance de la semaine courante est terminée.
|
||||
- `alertMode` vaut `off`, `visible` ou `site`, et est omis si `off`.
|
||||
- `alertOffsetMinutes` est une liste composée de `10`, `5` ou `0` (`0` = au début et pendant l'événement pour les reprises de session) ; il est omis si l'alerte est désactivée ou si la liste vaut `[10]`.
|
||||
- `color` vaut `violet`, `gold`, `cyan`, `pink`, `green` ou `gray`, et est omis si `violet`.
|
||||
|
||||
Import/export texte :
|
||||
|
||||
- le format texte commence par `@viewMode: week` ou `@viewMode: month`, puis une ligne `event | ...` par entrée ;
|
||||
- en hebdomadaire, les champs `day`, `start` et `end` décrivent le jour de semaine et les heures locales (`HH:mm`) ;
|
||||
- en mensuel, `repeatBy=monthDay` ou `repeatBy=weekday` utilise `startDate`/`endDate` et `repeat=none|weekly|monthly`, par exemple `repeatBy=weekday | repeat=monthly | startDate=2026-08-03 | endDate=2026-08-05` ;
|
||||
- l'import texte remappe toujours les identifiants d'événements.
|
||||
|
||||
## Outil Task Planner
|
||||
|
||||
Type : `taskPlanner`
|
||||
|
|
|
|||
|
|
@ -329,6 +329,77 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.timer.emptyCountdowns",
|
||||
"toolboxes.modules.timer.renameTitle",
|
||||
"toolboxes.modules.timer.deleteTitle",
|
||||
"toolboxes.modules.calendar.init.title",
|
||||
"toolboxes.modules.calendar.init.weekly",
|
||||
"toolboxes.modules.calendar.init.weeklyDescription",
|
||||
"toolboxes.modules.calendar.init.monthly",
|
||||
"toolboxes.modules.calendar.init.monthlyDescription",
|
||||
"toolboxes.modules.calendar.init.soon",
|
||||
"toolboxes.modules.calendar.gridLabel",
|
||||
"toolboxes.modules.calendar.monthGridLabel",
|
||||
"toolboxes.modules.calendar.titleLabel",
|
||||
"toolboxes.modules.calendar.titlePlaceholder",
|
||||
"toolboxes.modules.calendar.dayLabel",
|
||||
"toolboxes.modules.calendar.startDayLabel",
|
||||
"toolboxes.modules.calendar.endDayLabel",
|
||||
"toolboxes.modules.calendar.startLabel",
|
||||
"toolboxes.modules.calendar.endLabel",
|
||||
"toolboxes.modules.calendar.alertModeTitle",
|
||||
"toolboxes.modules.calendar.alertTypeLabel",
|
||||
"toolboxes.modules.calendar.alertOffTitle",
|
||||
"toolboxes.modules.calendar.alertVisibleTitle",
|
||||
"toolboxes.modules.calendar.alertSiteTitle",
|
||||
"toolboxes.modules.calendar.alertOffsetLabel",
|
||||
"toolboxes.modules.calendar.alertOffset10",
|
||||
"toolboxes.modules.calendar.alertOffset5",
|
||||
"toolboxes.modules.calendar.alertOffset0",
|
||||
"toolboxes.modules.calendar.alertMessage",
|
||||
"toolboxes.modules.calendar.resumeAlertMessage",
|
||||
"toolboxes.modules.calendar.colorLabel",
|
||||
"toolboxes.modules.calendar.colorViolet",
|
||||
"toolboxes.modules.calendar.colorGold",
|
||||
"toolboxes.modules.calendar.colorCyan",
|
||||
"toolboxes.modules.calendar.colorPink",
|
||||
"toolboxes.modules.calendar.colorGreen",
|
||||
"toolboxes.modules.calendar.colorGray",
|
||||
"toolboxes.modules.calendar.repeatableTitle",
|
||||
"toolboxes.modules.calendar.repeatLabel",
|
||||
"toolboxes.modules.calendar.repeatNone",
|
||||
"toolboxes.modules.calendar.repeatWeekly",
|
||||
"toolboxes.modules.calendar.repeatMonthly",
|
||||
"toolboxes.modules.calendar.monthlyRepeatLabel",
|
||||
"toolboxes.modules.calendar.monthlyRepeatMonthDay",
|
||||
"toolboxes.modules.calendar.monthlyRepeatWeekday",
|
||||
"toolboxes.modules.calendar.monthDatePickerLabel",
|
||||
"toolboxes.modules.calendar.previousMonthTitle",
|
||||
"toolboxes.modules.calendar.nextMonthTitle",
|
||||
"toolboxes.modules.calendar.currentMonthTitle",
|
||||
"toolboxes.modules.calendar.previousShortLabel",
|
||||
"toolboxes.modules.calendar.nextShortLabel",
|
||||
"toolboxes.modules.calendar.hideEmptyHoursLabel",
|
||||
"toolboxes.modules.calendar.importPlaceholder",
|
||||
"toolboxes.modules.calendar.importOpenButton",
|
||||
"toolboxes.modules.calendar.exportOpenButton",
|
||||
"toolboxes.modules.calendar.importModalTitle",
|
||||
"toolboxes.modules.calendar.exportModalTitle",
|
||||
"toolboxes.modules.calendar.importButton",
|
||||
"toolboxes.modules.calendar.exportButton",
|
||||
"toolboxes.modules.calendar.exportInfo",
|
||||
"toolboxes.modules.calendar.importSuccess",
|
||||
"toolboxes.modules.calendar.exportSuccess",
|
||||
"toolboxes.modules.calendar.importError",
|
||||
"toolboxes.modules.calendar.addButton",
|
||||
"toolboxes.modules.calendar.saveButton",
|
||||
"toolboxes.modules.calendar.cancelButton",
|
||||
"toolboxes.modules.calendar.editTitle",
|
||||
"toolboxes.modules.calendar.copyTitle",
|
||||
"toolboxes.modules.calendar.copiedTitle",
|
||||
"toolboxes.modules.calendar.pastedTitle",
|
||||
"toolboxes.modules.calendar.deleteTitle",
|
||||
"toolboxes.modules.calendar.deleteOccurrenceTitle",
|
||||
"toolboxes.modules.calendar.deleteSeriesTitle",
|
||||
"toolboxes.modules.calendar.requiredError",
|
||||
"toolboxes.modules.calendar.invalidRangeError",
|
||||
"toolboxes.modules.taskPlanner.settingsTitle",
|
||||
"toolboxes.modules.taskPlanner.settingsButtonLabel",
|
||||
"toolboxes.modules.taskPlanner.hideCompletedTitle",
|
||||
|
|
@ -484,7 +555,7 @@ export function validateSiteContent(site) {
|
|||
});
|
||||
|
||||
const libraryToolDocs = valueAt(site, "library.toolDocs");
|
||||
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "timer", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
|
||||
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
|
||||
const docs = libraryToolDocs?.[toolType];
|
||||
assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`);
|
||||
assert.ok(Array.isArray(docs.controls), `library.toolDocs.${toolType}.controls must be an array`);
|
||||
|
|
@ -515,9 +586,11 @@ export function validateSiteContent(site) {
|
|||
});
|
||||
}
|
||||
});
|
||||
if (docs.importFormat !== undefined) {
|
||||
assert.equal(typeof docs.importFormat, "object", `library.toolDocs.${toolType}.importFormat must be an object`);
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.text");
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.example");
|
||||
}
|
||||
});
|
||||
|
||||
const aboutLimits = valueAt(site, "about.limits");
|
||||
|
|
@ -695,7 +768,7 @@ export function validateLibraryData(payload) {
|
|||
assert.deepEqual(new Set(twoColumnIds), moduleIds, "library.toolbox.moduleOrder.two must contain every module exactly once");
|
||||
assert.equal(twoColumnIds.length, moduleIds.size, "library.toolbox.moduleOrder.two must not contain duplicate modules");
|
||||
|
||||
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
|
||||
["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => {
|
||||
assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ test("vite entrypoint and app shell are wired", async () => {
|
|||
assert.match(modals, /export function CreateToolboxModal/);
|
||||
assert.match(modals, /export function LinkToolboxModal/);
|
||||
assert.match(modals, /export function NotificationToast/);
|
||||
assert.match(modals, /durationMs = 120000/);
|
||||
assert.match(modals, /setTimeout\(\(\) => onClose\(id\), durationMs\)/);
|
||||
assert.match(source, /SHORT_NOTIFICATION_DURATION_MS = 5000/);
|
||||
assert.match(source, /getNotificationDuration/);
|
||||
assert.match(modals, /useModalScrollLock/);
|
||||
assert.match(imageViewer, /export function ImageViewer/);
|
||||
assert.match(imageViewer, /function openImageInNewTab/);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import { applyDueResets, getTaskParentMap, getTaskTreeIds, moveTask } from "../w
|
|||
import { createInput, getInputLabel, normalizeCustomKey, parseHoldSeconds } from "../website/src/features/toolboxes/modules/combosUtils.js";
|
||||
import { getBoundsFromStartAndSize, getSelectionBounds, moveSelectionCells, parseTsvBlock, pasteCellBlock } from "../website/src/features/toolboxes/modules/tableUtils.js";
|
||||
import { canToggleAutoRefresh, getCountdownValidationError, isAlertGuarded } from "../website/src/features/toolboxes/modules/timerOptions.js";
|
||||
import { getCalendarOccurrences, getCurrentCalendarOccurrence, getMonthEnd, getMonthStart, getNextCalendarAlertTime, getWeekStart } from "../website/src/features/toolboxes/modules/calendarUtils.js";
|
||||
import { exportModuleText, importModuleText } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
import { compactCalendarDataForStorage, normalizeCalendarData } from "../website/src/features/toolboxes/storage/modules/calendar.js";
|
||||
|
||||
test("toolbox storage, cards and pages are wired", async () => {
|
||||
const source = await readFile("website/src/main.jsx", "utf8");
|
||||
|
|
@ -17,6 +20,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
|||
const storageCompactorRegistry = await readFile("website/src/features/toolboxes/storage/modules/index.js", "utf8");
|
||||
const checklistStorage = await readFile("website/src/features/toolboxes/storage/modules/checklist.js", "utf8");
|
||||
const timerStorage = await readFile("website/src/features/toolboxes/storage/modules/timer.js", "utf8");
|
||||
const calendarStorage = await readFile("website/src/features/toolboxes/storage/modules/calendar.js", "utf8");
|
||||
const equipmentStorage = await readFile("website/src/features/toolboxes/storage/modules/equipmentPlanner.js", "utf8");
|
||||
const indexedToolboxesHook = await readFile("website/src/features/toolboxes/storage/useIndexedToolboxes.js", "utf8");
|
||||
const storageQuota = await readFile("website/src/components/StorageQuota.jsx", "utf8");
|
||||
|
|
@ -56,6 +60,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
|||
assert.match(toolboxStorage, /normalizeCalculatorData/);
|
||||
assert.match(toolboxStorage, /normalizeTableData/);
|
||||
assert.match(toolboxStorage, /normalizeTimerData/);
|
||||
assert.match(toolboxStorage, /normalizeCalendarData/);
|
||||
assert.match(toolboxStorage, /normalizeTaskPlannerData/);
|
||||
assert.match(toolboxStorage, /normalizeEquipmentPlannerData/);
|
||||
assert.match(toolboxStorage, /summarizeEquipmentPlannerData/);
|
||||
|
|
@ -63,7 +68,10 @@ test("toolbox storage, cards and pages are wired", async () => {
|
|||
assert.match(toolboxStorage, /export function compactModuleDataForStorage\(type, value\)/);
|
||||
assert.match(storageCompactorRegistry, /export const MODULE_DATA_COMPACTORS/);
|
||||
assert.match(storageCompactorRegistry, /timer: compactTimerDataForStorage/);
|
||||
assert.match(storageCompactorRegistry, /calendar: compactCalendarDataForStorage/);
|
||||
assert.match(timerStorage, /scrollResults/);
|
||||
assert.match(calendarStorage, /normalizeCalendarData/);
|
||||
assert.match(toolboxStorage, /nextId\("calendar"\)/);
|
||||
assert.match(toolboxCard, /DEFAULT_TOOLBOX_ICON/);
|
||||
assert.match(toolboxCard, /export function ToolboxCard/);
|
||||
assert.match(toolboxCard, /export function ToolboxIconPicker/);
|
||||
|
|
@ -140,6 +148,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8");
|
||||
const timerParts = await readFile("website/src/features/toolboxes/modules/TimerParts.jsx", "utf8");
|
||||
const timerOptions = await readFile("website/src/features/toolboxes/modules/timerOptions.js", "utf8");
|
||||
const calendarModule = await readFile("website/src/features/toolboxes/modules/CalendarModule.jsx", "utf8");
|
||||
const calendarUtils = await readFile("website/src/features/toolboxes/modules/calendarUtils.js", "utf8");
|
||||
const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8");
|
||||
const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8");
|
||||
const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8");
|
||||
|
|
@ -177,6 +187,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(moduleRegistry, /table:/);
|
||||
assert.match(moduleRegistry, /table: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.table, Component: TableModule \}/);
|
||||
assert.match(moduleRegistry, /timer:/);
|
||||
assert.match(moduleRegistry, /calendar:/);
|
||||
assert.match(moduleRegistry, /taskPlanner:/);
|
||||
assert.match(moduleRegistry, /equipmentPlanner:/);
|
||||
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
|
||||
|
|
@ -287,6 +298,35 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(timerModule, /useDraftForm/);
|
||||
assert.match(timerModule, /normalizeTimerData/);
|
||||
assert.match(timerModule, /TextExchangeActions/);
|
||||
assert.match(calendarModule, /export function CalendarModule/);
|
||||
assert.match(calendarModule, /normalizeCalendarData/);
|
||||
assert.match(calendarModule, /calendar-mode-choice/);
|
||||
assert.match(calendarModule, /calendar-week-grid/);
|
||||
assert.match(calendarModule, /calendar-month-grid/);
|
||||
assert.match(calendarModule, /getMonthOccurrenceSegments/);
|
||||
assert.match(calendarModule, /calendar-month-bar/);
|
||||
assert.match(calendarModule, /MonthDateDropdown/);
|
||||
assert.match(calendarModule, /MonthEventRepeatSwitch/);
|
||||
assert.match(calendarModule, /monthlyRepeatBy/);
|
||||
assert.match(calendarModule, /displayedMonth/);
|
||||
assert.match(calendarModule, /deleteOccurrence/);
|
||||
assert.match(calendarModule, /onPointerDown/);
|
||||
assert.match(calendarModule, /copyEvent/);
|
||||
assert.match(calendarModule, /calendar-repeat-switch/);
|
||||
assert.match(calendarModule, /layout-switch calendar-month-repeat-switch/);
|
||||
assert.match(calendarModule, /WEEK_TIME_INPUT_STEP_MINUTES \* 60/);
|
||||
assert.match(calendarModule, /durationMinutes < 120/);
|
||||
assert.match(calendarModule, /is-compact/);
|
||||
assert.match(calendarModule, /handleKeyDown/);
|
||||
assert.match(calendarModule, /TextExchangeActions/);
|
||||
assert.match(calendarModule, /type="calendar"/);
|
||||
assert.match(calendarModule, /repeatLabel/);
|
||||
assert.match(calendarModule, /CalendarAlertModeSwitch/);
|
||||
assert.doesNotMatch(moduleRegistry, /calendarInitOpen/);
|
||||
assert.doesNotMatch(moduleRegistry, /calendar-init-modal/);
|
||||
assert.match(calendarUtils, /export function getCalendarOccurrences/);
|
||||
assert.match(calendarUtils, /CALENDAR_VIEW_MODES = \["week", "month"\]/);
|
||||
assert.match(calendarUtils, /export function getNextCalendarAlertTime/);
|
||||
assert.match(taskPlannerModule, /export function TaskPlannerModule/);
|
||||
assert.match(taskPlannerModule, /normalizeTaskPlannerData/);
|
||||
assert.match(taskPlannerModule, /applyDueResets/);
|
||||
|
|
@ -397,6 +437,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(textImport, /export function importModuleText/);
|
||||
assert.match(textImport, /export function hasTextExchangeContent/);
|
||||
assert.match(textImport, /line\.indexOf\(":"\)/);
|
||||
assert.match(textImport, /exportCalendarText/);
|
||||
assert.match(textImport, /importCalendarText/);
|
||||
assert.match(textImport, /CALENDAR_WEEKDAYS/);
|
||||
assert.match(imageViewer, /image-viewer-media/);
|
||||
assert.match(imageViewer, /image-viewer-image-frame/);
|
||||
assert.match(imageViewer, /image-viewer-marker/);
|
||||
|
|
@ -480,3 +523,111 @@ test("timer options keep alert guards and countdown validation stable", () => {
|
|||
assert.equal(getCountdownValidationError("duration", { duration: { hours: "", minutes: "", seconds: "" } }, {}), "Valeur requise.");
|
||||
assert.equal(getCountdownValidationError("daily_time", { time: { hours: "12", minutes: "30", seconds: "" } }, {}), "");
|
||||
});
|
||||
|
||||
test("calendar helpers keep normalization, occurrences and alerts stable", () => {
|
||||
const data = normalizeCalendarData({
|
||||
events: [
|
||||
{
|
||||
id: "h1",
|
||||
title: "Raid",
|
||||
dayIndex: 4,
|
||||
startMinutes: 1080,
|
||||
endMinutes: 1200,
|
||||
repeatable: false,
|
||||
alertMode: "site",
|
||||
alertOffsetMinutes: [10, 5, 0],
|
||||
color: "gold"
|
||||
},
|
||||
{ title: "", dayIndex: 9, startMinutes: -1 }
|
||||
]
|
||||
});
|
||||
assert.equal(data.events.length, 1);
|
||||
assert.equal(data.initialized, true);
|
||||
assert.equal(data.events[0].repeat, "none");
|
||||
assert.equal(data.events[0].repeatable, false);
|
||||
const compactCalendar = compactCalendarDataForStorage(data);
|
||||
assert.equal(compactCalendar.initialized, true);
|
||||
assert.deepEqual(compactCalendar.events[0], {
|
||||
id: "h1",
|
||||
title: "Raid",
|
||||
dayIndex: 4,
|
||||
startMinutes: 1080,
|
||||
endMinutes: 1200,
|
||||
repeatable: false,
|
||||
alertMode: "site",
|
||||
alertOffsetMinutes: [10, 5, 0],
|
||||
color: "gold"
|
||||
});
|
||||
|
||||
const weekStart = getWeekStart(new Date("2026-08-17T12:00:00.000Z"), 1);
|
||||
const occurrences = getCalendarOccurrences(data.events, weekStart, new Date(weekStart.getTime() + 7 * 24 * 60 * 60 * 1000));
|
||||
assert.equal(occurrences.length, 1);
|
||||
assert.equal(new Date(occurrences[0].startMs).getHours(), 18);
|
||||
assert.equal(getNextCalendarAlertTime(data.events[0], new Date(2026, 7, 14, 17, 49).getTime()), new Date(2026, 7, 14, 17, 50).getTime());
|
||||
assert.equal(getCurrentCalendarOccurrence(data.events[0], new Date(2026, 7, 14, 19, 0).getTime()).event.id, "h1");
|
||||
|
||||
const monthly = normalizeCalendarData({
|
||||
viewMode: "month",
|
||||
events: [
|
||||
{ id: "h2", title: "Prime", startDate: "2026-08-14", endDate: "2026-08-16", repeat: "monthly", alertMode: "visible", alertOffsetMinutes: [0] },
|
||||
{ id: "h3", title: "Premier lundi", startDate: "2026-08-03", endDate: "2026-08-05", repeat: "monthly", monthlyRepeatBy: "weekday" },
|
||||
{ id: "h6", title: "Hebdo", startDate: "2026-08-03", endDate: "2026-08-05", repeat: "weekly", monthlyRepeatBy: "weekday", excludedOccurrences: ["2026-08-10"] },
|
||||
{ id: "bad", title: "Sans cible", startMinutes: 720 }
|
||||
]
|
||||
});
|
||||
assert.equal(monthly.viewMode, "month");
|
||||
assert.equal(monthly.events.length, 3);
|
||||
assert.equal(monthly.events[0].repeat, "monthly");
|
||||
assert.equal(monthly.events[0].monthlyRepeatBy, "monthDay");
|
||||
assert.equal(monthly.events[1].monthlyRepeatBy, "weekday");
|
||||
assert.equal(monthly.events[2].repeat, "weekly");
|
||||
const compactMonthly = compactCalendarDataForStorage(monthly);
|
||||
assert.equal(compactMonthly.viewMode, "month");
|
||||
assert.equal(compactMonthly.events[0].startDate, "2026-08-14");
|
||||
assert.equal(compactMonthly.events[0].endDate, "2026-08-16");
|
||||
assert.equal("startMinutes" in compactMonthly.events[0], false);
|
||||
assert.equal(compactMonthly.events[1].monthlyRepeatBy, "weekday");
|
||||
assert.equal(compactMonthly.events[2].repeat, "weekly");
|
||||
assert.deepEqual(compactMonthly.events[2].excludedOccurrences, ["2026-08-10"]);
|
||||
|
||||
const augustStart = getMonthStart(new Date(2026, 7, 14));
|
||||
const augustOccurrences = getCalendarOccurrences(monthly.events, augustStart, getMonthEnd(augustStart));
|
||||
assert.equal(augustOccurrences.filter((occurrence) => occurrence.event.id === "h2").length, 1);
|
||||
assert.equal(augustOccurrences.filter((occurrence) => occurrence.event.id === "h3").length, 1);
|
||||
assert.equal(augustOccurrences.filter((occurrence) => occurrence.event.id === "h6").length, 4);
|
||||
assert.equal(new Date(augustOccurrences.find((occurrence) => occurrence.event.id === "h2").endMs).getDate(), 17);
|
||||
assert.equal(getNextCalendarAlertTime(monthly.events[0], new Date(2026, 7, 13, 23, 59).getTime()), new Date(2026, 7, 14, 0, 0).getTime());
|
||||
|
||||
const thirtyFirst = normalizeCalendarData({
|
||||
viewMode: "month",
|
||||
events: [{ id: "h4", title: "31", startDate: "2026-08-31", repeat: "monthly" }]
|
||||
});
|
||||
const septemberStart = getMonthStart(new Date(2026, 8, 1));
|
||||
assert.equal(getCalendarOccurrences(thirtyFirst.events, septemberStart, getMonthEnd(septemberStart)).length, 0);
|
||||
});
|
||||
|
||||
test("calendar text exchange imports and exports weekly and monthly calendars", () => {
|
||||
let uidIndex = 0;
|
||||
const context = {
|
||||
uid: (prefix) => `${prefix}${uidIndex += 1}`,
|
||||
normalizeCalendarData
|
||||
};
|
||||
const weekly = importModuleText("calendar", "@viewMode: week\nevent | title=Raid | day=vendredi | start=18:00 | end=20:00 | color=gold | alertMode=site | alerts=10,5", context);
|
||||
assert.equal(weekly.ok, true);
|
||||
assert.equal(weekly.data.viewMode, "week");
|
||||
assert.equal(weekly.data.events[0].dayIndex, 4);
|
||||
assert.equal(weekly.data.events[0].startMinutes, 1080);
|
||||
assert.deepEqual(weekly.data.events[0].alertOffsetMinutes, [10, 5]);
|
||||
assert.match(exportModuleText("calendar", weekly.data), /day=vendredi/);
|
||||
|
||||
const monthly = importModuleText("calendar", "@viewMode: month\nevent | title=Maintenance | repeatBy=weekday | repeat=monthly | startDate=2026-08-03 | endDate=2026-08-05 | color=pink\nevent | title=Event | repeatBy=monthDay | repeat=none | startDate=2026-08-20 | endDate=2026-08-27", context);
|
||||
assert.equal(monthly.ok, true);
|
||||
assert.equal(monthly.data.viewMode, "month");
|
||||
assert.equal(monthly.data.events[0].monthlyRepeatBy, "weekday");
|
||||
assert.equal(monthly.data.events[0].startDate, "2026-08-03");
|
||||
assert.equal(monthly.data.events[0].repeat, "monthly");
|
||||
assert.equal(monthly.data.events[1].startDate, "2026-08-20");
|
||||
assert.equal(monthly.data.events[1].repeat, "none");
|
||||
assert.match(exportModuleText("calendar", monthly.data), /startDate=2026-08-03/);
|
||||
assert.match(exportModuleText("calendar", monthly.data), /repeatBy=monthDay/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -786,6 +786,10 @@ test("notepad data migrates text, sanitizes html and compacts drawings", () => {
|
|||
assert.equal(normalized.drawings.strokes[0].width, 24);
|
||||
assert.deepEqual(normalized.drawings.strokes[0].points, [{ x: 4, y: 8 }, { x: 12.35, y: 16.79 }]);
|
||||
|
||||
const escaped = normalizeNotepadData({ html: "<p>"&amp;quot; & test</p>" });
|
||||
assert.equal(escaped.text, "\"\" & test");
|
||||
assert.equal(escaped.html, "<p>"" & test</p>");
|
||||
|
||||
const compactTemporary = compactModuleDataForStorage("notepad", {
|
||||
html: "<p>Note</p>",
|
||||
text: "Note",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,165 @@
|
|||
"id": "m9",
|
||||
"type": "timer"
|
||||
},
|
||||
{
|
||||
"id": "md",
|
||||
"type": "calendar",
|
||||
"title": "Calendrier hebdomadaire",
|
||||
"controls": [
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "calendar",
|
||||
"title": "Bouton changement de couleur",
|
||||
"preview": {
|
||||
"kind": "color"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "refresh",
|
||||
"title": "Répéter les événements",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "refresh",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "eye-closed",
|
||||
"title": "Masquer / Afficher les heures inutiles",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "eye-closed"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-mute",
|
||||
"title": "Alerte désactivée",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-mute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-min",
|
||||
"title": "Alerte sur la page",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-min"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-max",
|
||||
"title": "Alerte sur le site",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "trash",
|
||||
"title": "Bouton supprimer",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Exemple texte du calendrier hebdomadaire de démonstration.",
|
||||
"example": "@viewMode: week\nevent | title=Uturi | day=lundi | start=00:15 | end=00:20\nevent | title=Uturi | day=lundi | start=16:00 | end=16:05\nevent | title=Uturi | day=mardi | start=19:00 | end=19:05\nevent | title=Uturi | day=mercredi | start=22:15 | end=22:20\nevent | title=Uturi | day=jeudi | start=00:15 | end=00:20\nevent | title=Uturi | day=vendredi | start=12:00 | end=12:05\nevent | title=Uturi | day=samedi | start=02:00 | end=02:05\nevent | title=Uturi | day=dimanche | start=12:00 | end=12:05\nevent | title=Garmoth | color=pink | day=lundi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=mardi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=mercredi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=jeudi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=vendredi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=samedi | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=dimanche | start=14:00 | end=14:05\nevent | title=Garmoth | color=pink | day=lundi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=mardi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=mercredi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=jeudi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=vendredi | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=dimanche | start=23:15 | end=23:20\nevent | title=Garmoth | color=pink | day=dimanche | start=19:15 | end=19:20\nevent | title=Vell | color=cyan | day=mercredi | start=19:00 | end=19:05\nevent | title=Vell | color=cyan | day=dimanche | start=16:00 | end=16:05\nevent | title=Maintenance | color=gold | day=jeudi | start=10:00 | end=14:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "me",
|
||||
"type": "calendar",
|
||||
"title": "Calendrier mensuel",
|
||||
"controls": [
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "calendar",
|
||||
"title": "Bouton changement de couleur",
|
||||
"preview": {
|
||||
"kind": "color"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "day-number",
|
||||
"title": "Numéro de jour",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "day-number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "day-name",
|
||||
"title": "Nom de jour",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "day-name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "refresh",
|
||||
"title": "Répéter les événements",
|
||||
"preview": {
|
||||
"kind": "switch",
|
||||
"icon": "refresh",
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-mute",
|
||||
"title": "Alerte désactivée",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-mute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-min",
|
||||
"title": "Alerte sur la page",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-min"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "sound-max",
|
||||
"title": "Alerte sur le site",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Contrôles",
|
||||
"icon": "trash",
|
||||
"title": "Bouton supprimer",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Exemple texte du calendrier mensuel de démonstration.",
|
||||
"example": "@viewMode: month\nevent | title=Double EXP 1er Week end du mois | repeatBy=weekday | repeat=none | startDate=2026-08-01 | endDate=2026-08-02\nevent | title=Event de drop rare | color=gold | repeatBy=weekday | repeat=none | startDate=2026-08-18 | endDate=2026-08-25\nevent | title=Maintenance | color=pink | repeatBy=weekday | repeat=weekly | startDate=2026-08-04\nevent | title=Récompenses du 15 | color=green | repeatBy=monthDay | repeat=none | startDate=2026-08-15"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ma",
|
||||
"type": "taskPlanner"
|
||||
|
|
@ -54,8 +213,8 @@
|
|||
}
|
||||
],
|
||||
"moduleOrder": {
|
||||
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "ma", "mb", "mc"],
|
||||
"two": [["m1", "m2", "m3", "m4", "m5", "m6"], ["m7", "m8", "m9", "ma", "mb", "mc"]]
|
||||
"one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "m9", "md", "me", "ma", "mb", "mc"],
|
||||
"two": [["m1", "m2", "m3", "m4", "m5", "m6", "ma"], ["m7", "m8", "m9", "md", "me", "mb", "mc"]]
|
||||
},
|
||||
"updatedAt": "2026-08-09T07:34:31.425Z",
|
||||
"moduleColumns": 1
|
||||
|
|
@ -1325,6 +1484,238 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"md": {
|
||||
"initialized": true,
|
||||
"events": [
|
||||
{
|
||||
"id": "hdl7oex",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 15,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 20
|
||||
},
|
||||
{
|
||||
"id": "hgdodzy",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 960,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 965
|
||||
},
|
||||
{
|
||||
"id": "hy95mcm",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 1140,
|
||||
"dayIndex": 1,
|
||||
"endMinutes": 1145
|
||||
},
|
||||
{
|
||||
"id": "hs2tg2h",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 1335,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 1340
|
||||
},
|
||||
{
|
||||
"id": "hv903h8",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 15,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 20
|
||||
},
|
||||
{
|
||||
"id": "hanpqlt",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 720,
|
||||
"dayIndex": 4,
|
||||
"endMinutes": 725
|
||||
},
|
||||
{
|
||||
"id": "h0e6ma2",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 120,
|
||||
"dayIndex": 5,
|
||||
"endMinutes": 125
|
||||
},
|
||||
{
|
||||
"id": "hc3ocs0",
|
||||
"title": "Uturi",
|
||||
"startMinutes": 720,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 725
|
||||
},
|
||||
{
|
||||
"id": "hv1i4lf",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h1uz1u2",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 1,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hdci41f",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h08ha51",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hnz11qv",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 4,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h2dexvq",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 5,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hq9ctue",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 840,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 845,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hfq7scf",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 0,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hfbeg1y",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 1,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h2gfv4o",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hjjzqfz",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h1h2zsd",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 4,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "ht08lsw",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1395,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 1400,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "h8k8cbm",
|
||||
"title": "Garmoth",
|
||||
"startMinutes": 1155,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 1160,
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hzq4r4v",
|
||||
"title": "Vell",
|
||||
"startMinutes": 1140,
|
||||
"dayIndex": 2,
|
||||
"endMinutes": 1145,
|
||||
"color": "cyan"
|
||||
},
|
||||
{
|
||||
"id": "hxq7o9p",
|
||||
"title": "Vell",
|
||||
"startMinutes": 960,
|
||||
"dayIndex": 6,
|
||||
"endMinutes": 965,
|
||||
"color": "cyan"
|
||||
},
|
||||
{
|
||||
"id": "hfkycds",
|
||||
"title": "Maintenance",
|
||||
"startMinutes": 600,
|
||||
"dayIndex": 3,
|
||||
"endMinutes": 840,
|
||||
"color": "gold"
|
||||
}
|
||||
]
|
||||
},
|
||||
"me": {
|
||||
"initialized": true,
|
||||
"viewMode": "month",
|
||||
"events": [
|
||||
{
|
||||
"id": "hpnato7",
|
||||
"title": "Double EXP 1er Week end du mois",
|
||||
"startDate": "2026-08-01",
|
||||
"endDate": "2026-08-02",
|
||||
"monthlyRepeatBy": "weekday"
|
||||
},
|
||||
{
|
||||
"id": "hdygtpx",
|
||||
"title": "Event de drop rare",
|
||||
"startDate": "2026-08-18",
|
||||
"endDate": "2026-08-25",
|
||||
"monthlyRepeatBy": "weekday",
|
||||
"color": "gold"
|
||||
},
|
||||
{
|
||||
"id": "hy8bjtd",
|
||||
"title": "Maintenance",
|
||||
"startDate": "2026-08-04",
|
||||
"repeat": "weekly",
|
||||
"monthlyRepeatBy": "weekday",
|
||||
"color": "pink"
|
||||
},
|
||||
{
|
||||
"id": "hytouda",
|
||||
"title": "Récompenses du 15",
|
||||
"startDate": "2026-08-15",
|
||||
"color": "green"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ma": {
|
||||
"lastResetAt": 1786226400000,
|
||||
"hideCompleted": true,
|
||||
|
|
|
|||
|
|
@ -884,6 +884,90 @@
|
|||
"example": "@activeTab: countdown\nlap | label=Fosse 110 | elapsedMs=1046475\nlap | label=Fosse 55 | elapsedMs=50019\ncountdown | label=Fin de vague infernale | type=time_pattern | alertMode=off | pattern=X:55:X\ncountdown | label=Boss Mondial | type=daily_time | alertMode=off | time=14:00:00\ncountdown | label=Legion | type=interval | alertMode=off | autoRefresh=true | intervalMs=1800000 | startMode=now\ncountdown | label=Durée | type=duration | alertMode=off | durationMs=1800000"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"controls": [
|
||||
{
|
||||
"group": "Événement",
|
||||
"icon": "calendar",
|
||||
"title": "Sélection de créneaux",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "calendar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Événement",
|
||||
"icon": "copy",
|
||||
"title": "Copier un événement",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "copy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Alertes",
|
||||
"icon": "sound-max",
|
||||
"title": "Alerte site",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "sound-max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Événement",
|
||||
"icon": "trash",
|
||||
"title": "Supprimer",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "trash",
|
||||
"danger": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
{
|
||||
"title": "Vue hebdomadaire",
|
||||
"text": "Une grille jours par créneaux de 30 minutes pour planifier les événements de session.",
|
||||
"details": [
|
||||
"Les heures peuvent être saisies à 5 minutes près.",
|
||||
"La grille reste lisible dans la toolbox complète et dans le panneau latéral.",
|
||||
"Les plages vides peuvent être masquées sans retirer l'indicateur de tranche horaire."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Vue mensuelle",
|
||||
"text": "Une grille semaines par jours affiche les numéros du mois courant sans nommer le mois.",
|
||||
"details": [
|
||||
"Les événements mensuels peuvent cibler un numéro du jour ou un jour de semaine.",
|
||||
"Les événements multi-jours s'affichent en barre horizontale.",
|
||||
"Les jours hors mois courant ne sont pas sélectionnables."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Création rapide",
|
||||
"text": "Un événement peut être saisi au formulaire ou prérempli en sélectionnant des cellules.",
|
||||
"details": [
|
||||
"Un événement peut être ponctuel ou avoir une durée.",
|
||||
"Dans un calendrier hebdomadaire, les événements se répètent automatiquement chaque semaine.",
|
||||
"Un événement copié peut être collé sur un autre créneau en conservant durée, couleur et alerte.",
|
||||
"La couleur permet de distinguer rapidement les catégories d'événements."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Alertes",
|
||||
"text": "Les alertes utilisent les mêmes modes que le timer, avec un délai de 5 ou 10 minutes.",
|
||||
"details": [
|
||||
"Aucune alerte garde l'événement silencieux.",
|
||||
"Alerte visible prévient seulement quand la toolbox concernée est ouverte.",
|
||||
"Alerte site fonctionne sur toute l'application et signale aussi une session déjà en cours une seule fois."
|
||||
]
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Le calendrier s'échange en lignes texte : une meta @viewMode puis une ligne event par entrée.",
|
||||
"example": "@viewMode: week\nevent | title=Raid hebdo | day=vendredi | start=18:00 | end=20:00 | color=gold | alertMode=site | alerts=10,5\n\n@viewMode: month\nevent | title=Week-end Double EXP | repeatBy=monthDay | repeat=monthly | startDate=2026-08-07 | endDate=2026-08-09\nevent | title=Maintenance | repeatBy=weekday | repeat=weekly | startDate=2026-08-06 | color=pink"
|
||||
}
|
||||
},
|
||||
"taskPlanner": {
|
||||
"controls": [
|
||||
{
|
||||
|
|
@ -1792,6 +1876,81 @@
|
|||
"renameTitle": "Renommer",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"calendar": {
|
||||
"init": {
|
||||
"title": "Créer un calendrier",
|
||||
"weekly": "Hebdomadaire",
|
||||
"weeklyDescription": "Grille jours par créneaux de 30 minutes.",
|
||||
"monthly": "Mensuel",
|
||||
"monthlyDescription": "Grille mensuelle datée avec navigation.",
|
||||
"soon": "À venir"
|
||||
},
|
||||
"gridLabel": "Calendrier hebdomadaire",
|
||||
"monthGridLabel": "Calendrier mensuel",
|
||||
"titleLabel": "Titre",
|
||||
"titlePlaceholder": "Raid, reset, event...",
|
||||
"dayLabel": "Jour",
|
||||
"startDayLabel": "Date début",
|
||||
"endDayLabel": "Date fin",
|
||||
"startLabel": "Début",
|
||||
"endLabel": "Fin",
|
||||
"alertModeTitle": "Mode d'alerte",
|
||||
"alertTypeLabel": "Type d'alertes",
|
||||
"alertOffTitle": "Pas d'alerte",
|
||||
"alertVisibleTitle": "Alerte quand la toolbox est affichée",
|
||||
"alertSiteTitle": "Alerte sur tout le site",
|
||||
"alertOffsetLabel": "Alerte",
|
||||
"alertOffset10": "10 min avant",
|
||||
"alertOffset5": "5 min avant",
|
||||
"alertOffset0": "Au début et pendant",
|
||||
"alertMessage": "{label} commence bientôt dans {toolbox}.",
|
||||
"resumeAlertMessage": "{label} est en cours dans {toolbox}.",
|
||||
"hideEmptyHoursLabel": "Masquer les heures vides",
|
||||
"colorLabel": "Couleur",
|
||||
"colorViolet": "Violet",
|
||||
"colorGold": "Or",
|
||||
"colorCyan": "Cyan",
|
||||
"colorPink": "Rose",
|
||||
"colorGreen": "Vert",
|
||||
"colorGray": "Gris",
|
||||
"repeatableTitle": "Répétable",
|
||||
"repeatLabel": "Répétition",
|
||||
"repeatNone": "Aucune",
|
||||
"repeatWeekly": "Chaque semaine",
|
||||
"repeatMonthly": "Chaque mois",
|
||||
"monthlyRepeatLabel": "Règle mensuelle",
|
||||
"monthlyRepeatMonthDay": "Numéro du jour",
|
||||
"monthlyRepeatWeekday": "Jour de semaine",
|
||||
"monthDatePickerLabel": "Choisir une date",
|
||||
"previousMonthTitle": "Mois précédent",
|
||||
"nextMonthTitle": "Mois suivant",
|
||||
"currentMonthTitle": "Mois courant",
|
||||
"previousShortLabel": "Prec.",
|
||||
"nextShortLabel": "Suiv.",
|
||||
"importPlaceholder": "@viewMode: week\nevent | title=Raid hebdo | day=vendredi | start=18:00 | end=20:00 | color=gold | alertMode=site | alerts=10,5\n\n@viewMode: month\nevent | title=Maintenance | repeatBy=weekday | repeat=monthly | startDate=2026-08-03 | endDate=2026-08-05 | color=pink",
|
||||
"importOpenButton": "Importer du texte",
|
||||
"exportOpenButton": "Exporter en format texte",
|
||||
"importModalTitle": "Importer un calendrier",
|
||||
"exportModalTitle": "Exporter un calendrier",
|
||||
"importButton": "Importer le texte",
|
||||
"exportButton": "Copier le texte",
|
||||
"exportInfo": "Format calendrier texte compatible avec les vues hebdomadaire et mensuelle.",
|
||||
"importSuccess": "Import calendrier terminé.",
|
||||
"exportSuccess": "Export calendrier copié.",
|
||||
"importError": "Format de calendrier invalide.",
|
||||
"addButton": "Ajouter",
|
||||
"saveButton": "Enregistrer",
|
||||
"cancelButton": "Annuler",
|
||||
"editTitle": "Modifier",
|
||||
"copyTitle": "Copier",
|
||||
"copiedTitle": "Événement copié.",
|
||||
"pastedTitle": "Événement collé.",
|
||||
"deleteTitle": "Supprimer",
|
||||
"deleteOccurrenceTitle": "Supprimer cette occurrence",
|
||||
"deleteSeriesTitle": "Supprimer toutes les répétitions",
|
||||
"requiredError": "Titre, date et heure de début requis.",
|
||||
"invalidRangeError": "La fin doit être après le début."
|
||||
},
|
||||
"taskPlanner": {
|
||||
"importPlaceholder": "@resetTime: 06:00\n# Raid\n- daily Préparer la route\n @description: Notes rapides\n - unique Sous-tâche",
|
||||
"importOpenButton": "Importer du texte",
|
||||
|
|
|
|||
96
website/public/static/icons/calendar-month.svg
Normal file
96
website/public/static/icons/calendar-month.svg
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
inkscape:export-filename="test2.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.71329397"
|
||||
inkscape:cx="208.18906"
|
||||
inkscape:cy="324.55062"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g12"
|
||||
showgrid="false">
|
||||
<inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:1.38282;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="m 8.2553905,10.353164 c -0.4995098,0 -0.992121,0.05377 -1.4778512,0.161309 -0.4857302,0.104074 -0.8698233,0.21335 -1.1523047,0.327831 v 1.680821 h 0.1911912 c 0.3548241,-0.211618 0.7216867,-0.372913 1.1006252,-0.483926 0.3789384,-0.114481 0.7303398,-0.171738 1.0541599,-0.171738 0.130906,0 0.2876355,0.01042 0.4702149,0.03123 0.1825795,0.02081 0.3410204,0.06242 0.4753713,0.124863 0.1205713,0.05551 0.2204744,0.131848 0.2997069,0.228984 0.079233,0.09367 0.1188864,0.218537 0.1188864,0.374648 0,0.211618 -0.037942,0.372972 -0.11373,0.483985 -0.075788,0.107543 -0.1894406,0.194242 -0.341016,0.260156 -0.1446856,0.06245 -0.320357,0.102361 -0.5270507,0.119707 -0.2066937,0.01735 -0.4444427,0.02602 -0.7131446,0.02602 H 7.2012305 v 1.38416 h 0.4237502 c 0.2032488,0 0.4237083,0.0035 0.6614061,0.01043 0.2376978,0.0035 0.4409267,0.03297 0.6097266,0.08848 0.1687998,0.05204 0.3048759,0.140482 0.4082228,0.265371 0.1033468,0.12142 0.155039,0.294877 0.155039,0.520371 0,0.232433 -0.037885,0.411127 -0.1136722,0.536016 -0.075788,0.12142 -0.1774021,0.225459 -0.3048632,0.312188 -0.1619101,0.111013 -0.3600048,0.183863 -0.5942577,0.218554 -0.2342528,0.03122 -0.45129,0.04682 -0.6510939,0.04682 -0.3410446,0 -0.7182336,-0.06587 -1.131621,-0.197696 C 6.2539247,16.569911 5.9008119,16.413794 5.6045509,16.233398 H 5.4030466 v 1.701621 c 0.2859263,0.12489 0.6562117,0.237598 1.1109379,0.338203 0.458171,0.100606 1.0058972,0.150938 1.6432028,0.150938 0.5580729,0 1.0317427,-0.05726 1.4210158,-0.171738 0.3927181,-0.117951 0.7337929,-0.28617 1.0231639,-0.504727 0.296261,-0.225494 0.515009,-0.496124 0.65625,-0.811816 0.144686,-0.315693 0.217032,-0.655681 0.217032,-1.019942 0,-0.32263 -0.04998,-0.58104 -0.149884,-0.775312 -0.0999,-0.194272 -0.234207,-0.364296 -0.403007,-0.51 -0.16191,-0.138766 -0.341063,-0.24455 -0.537422,-0.317402 -0.196359,-0.07285 -0.382357,-0.121439 -0.558047,-0.145723 v -0.07283 c 0.434057,-0.09367 0.783688,-0.308786 1.048945,-0.645293 0.265257,-0.339977 0.39791,-0.72506 0.39791,-1.155234 0,-0.3261 -0.06892,-0.610519 -0.206718,-0.85336 -0.137796,-0.24284 -0.347929,-0.449289 -0.63041,-0.619277 C 10.198318,10.679269 9.8968973,10.56656 9.5317384,10.483301 9.1665795,10.396572 8.7411207,10.353164 8.2553905,10.353164 Z m 7.1558795,0.140508 c -0.01378,0.208149 -0.06547,0.381605 -0.155039,0.520371 -0.08957,0.138766 -0.223932,0.251533 -0.403067,0.338262 -0.151575,0.07285 -0.340996,0.124871 -0.56836,0.156094 -0.227363,0.02775 -0.465053,0.04166 -0.713085,0.04166 v 1.264454 h 1.62252 v 4.095351 h -1.62252 v 1.352989 h 5.136328 v -1.35299 H 17.12168 v -6.416191 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
96
website/public/static/icons/calendar-week.svg
Normal file
96
website/public/static/icons/calendar-week.svg
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
inkscape:export-filename="test2.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.71329397"
|
||||
inkscape:cx="208.18906"
|
||||
inkscape:cy="324.55062"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g12"
|
||||
showgrid="false">
|
||||
<inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:1.38282;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 9.2709372,10.640683 V 12.12375 H 13.223965 L 9.6068553,18.389004 H 11.818477 L 15.30123,12.180996 V 10.640683 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
4
website/public/static/icons/calendar.svg
Normal file
4
website/public/static/icons/calendar.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 9H21M7 3V5M17 3V5M6 12H8M11 12H13M16 12H18M6 15H8M11 15H13M16 15H18M6 18H8M11 18H13M16 18H18M6.2 21H17.8C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4802 21 18.9201 21 17.8V8.2C21 7.07989 21 6.51984 20.782 6.09202C20.5903 5.71569 20.2843 5.40973 19.908 5.21799C19.4802 5 18.9201 5 17.8 5H6.2C5.0799 5 4.51984 5 4.09202 5.21799C3.71569 5.40973 3.40973 5.71569 3.21799 6.09202C3 6.51984 3 7.07989 3 8.2V17.8C3 18.9201 3 19.4802 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21Z" stroke="#000000" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 864 B |
60
website/public/static/icons/day-name.svg
Normal file
60
website/public/static/icons/day-name.svg
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:0.750001;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 4.5591795,10.984394 V 17.79293 H 8.2763086 V 16.476035 H 5.888203 v -5.491641 z m 4.4398829,0 v 4.348536 c 0,0.838304 0.1926982,1.481497 0.5780276,1.929609 0.3876367,0.448111 0.958715,0.672129 1.713222,0.672129 0.754507,0 1.325586,-0.224018 1.713223,-0.672129 0.387637,-0.448112 0.581426,-1.092828 0.581426,-1.93418 v -4.343965 h -1.335937 v 4.24793 c 0,0.490789 -0.07732,0.847449 -0.231915,1.069981 -0.152285,0.222531 -0.394537,0.333808 -0.726797,0.333808 -0.336874,0 -0.580334,-0.115843 -0.730312,-0.347519 -0.149978,-0.231677 -0.224941,-0.583771 -0.224941,-1.05627 v -4.24793 z m 5.8243366,0 v 6.808536 h 1.218281 v -4.673145 l 2.187421,4.673145 h 1.280567 v -6.808536 h -1.218281 v 3.900411 l -1.879336,-3.900411 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
96
website/public/static/icons/day-number.svg
Normal file
96
website/public/static/icons/day-number.svg
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg4"
|
||||
sodipodi:docname="test.svg"
|
||||
inkscape:version="1.4.4 (dcaf3e7, 2026-05-05)"
|
||||
inkscape:export-filename="test2.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<rect
|
||||
x="137.39076"
|
||||
y="325.25159"
|
||||
width="493.48517"
|
||||
height="300.01657"
|
||||
id="rect5" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.71329397"
|
||||
inkscape:cx="208.18906"
|
||||
inkscape:cy="324.55062"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1009"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g12"
|
||||
showgrid="false">
|
||||
<inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
d="M 1.88,7.5956823 H 22.12 V 18.980683 c 0,1.397319 -1.132681,2.53 -2.53,2.53 H 4.41 c -1.397281,0 -2.53,-1.132681 -2.53,-2.53 z"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1" />
|
||||
<path
|
||||
d="M 4.1288917,4.6941768 H 1.88 v 2.919953 h 20.24 v -2.919953 h -2.248917 m -8.995541,0 h 2.248916"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path2"
|
||||
sodipodi:nodetypes="cccccccc" />
|
||||
<line
|
||||
x1="7.2060194"
|
||||
y1="2.4862077"
|
||||
x2="7.2060194"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line2" />
|
||||
<line
|
||||
x1="16.581165"
|
||||
y1="2.4862077"
|
||||
x2="16.581165"
|
||||
y2="5.0162082"
|
||||
stroke="#333333"
|
||||
stroke-width="2.53"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="line3" />
|
||||
<path
|
||||
id="text5"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:192px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.11px;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect5);fill:#333333;fill-opacity:1;stroke:#333333;stroke-width:1.38282;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 9.2709372,10.640683 V 12.12375 H 13.223965 L 9.6068553,18.389004 H 11.818477 L 15.30123,12.180996 V 10.640683 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3 KiB |
40
website/public/static/icons/trashcan-one.svg
Normal file
40
website/public/static/icons/trashcan-one.svg
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath5">
|
||||
<path
|
||||
id="path5"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1.95705;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="M 0.21029297,0.25236328 V 23.805059 H 23.636836 V 0.25236328 Z M 16.530645,9.2983593 a 7.0619658,7.1584014 0 0 1 7.061953,7.1583987 7.0619658,7.1584014 0 0 1 -7.061953,7.158398 7.0619658,7.1584014 0 0 1 -7.0619535,-7.158398 7.0619658,7.1584014 0 0 1 7.0619535,-7.1583987 z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
d="M 4,6 H 20 M 16,6 15.7294,5.18807 C 15.4671,4.40125 15.3359,4.00784 15.0927,3.71698 14.8779,3.46013 14.6021,3.26132 14.2905,3.13878 13.9376,3 13.523,3 12.6936,3 H 11.3064 C 10.477,3 10.0624,3 9.70951,3.13878 9.39792,3.26132 9.12208,3.46013 8.90729,3.71698 8.66405,4.00784 8.53292,4.40125 8.27064,5.18807 L 8,6 m 10,0 v 10.2 c 0,1.6802 0,2.5202 -0.327,3.162 -0.2876,0.5645 -0.7465,1.0234 -1.311,1.311 C 15.7202,21 14.8802,21 13.2,21 H 10.8 C 9.11984,21 8.27976,21 7.63803,20.673 7.07354,20.3854 6.6146,19.9265 6.32698,19.362 6,18.7202 6,17.8802 6,16.2 V 6 m 8,4 v 7 m -4,-7 v 7"
|
||||
stroke="#000000"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
id="path1"
|
||||
clip-path="url(#clipPath5)" />
|
||||
<path
|
||||
id="path3"
|
||||
style="fill:#ffffff;fill-opacity:0;stroke:#000000;stroke-width:2.05989;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
|
||||
d="m 16.299316,10.251504 a 6.08775,6.2262435 0 0 0 -6.087714,6.226289 6.08775,6.2262435 0 0 0 6.087714,6.22623 6.08775,6.2262435 0 0 0 6.087774,-6.22623 6.08775,6.2262435 0 0 0 -6.087774,-6.226289 z" />
|
||||
<path
|
||||
id="text6"
|
||||
style="font-weight:bold;font-size:9.34525px;line-height:0.2;font-family:Sans;-inkscape-font-specification:'Sans Bold';letter-spacing:-0.00535405px;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.92177;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:12.5;stroke-dasharray:none;paint-order:stroke fill markers"
|
||||
d="m 15.813443,12.788217 c -0.01236,0.188228 -0.05871,0.345068 -0.139028,0.470552 -0.08033,0.125487 -0.200793,0.227424 -0.361435,0.305853 -0.135927,0.06588 -0.305856,0.112981 -0.509746,0.141216 -0.203892,0.0251 -0.417006,0.03761 -0.639432,0.03761 v 1.143479 h 1.455026 v 3.703385 h -1.455026 v 1.223536 h 4.606055 v -1.223536 h -1.42259 v -5.8021 z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -78,6 +78,7 @@ export function AppOverlays({
|
|||
key={notification.id}
|
||||
id={notification.id}
|
||||
message={notification.message}
|
||||
durationMs={notification.durationMs}
|
||||
onClose={dismissNotification}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -24,11 +24,11 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance
|
|||
);
|
||||
}
|
||||
|
||||
export function NotificationToast({ id, message, onClose }) {
|
||||
export function NotificationToast({ id, message, durationMs = 120000, onClose }) {
|
||||
useEffect(() => {
|
||||
const timeoutId = window.setTimeout(() => onClose(id), 120000);
|
||||
const timeoutId = window.setTimeout(() => onClose(id), durationMs);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [id, onClose]);
|
||||
}, [durationMs, id, onClose]);
|
||||
|
||||
return (
|
||||
<div className="notification-toast" role="status">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
clampQty,
|
||||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
|
|
@ -191,6 +192,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
|
|
@ -220,7 +222,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
setShowTutorial(shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
||||
}, [pendingTutorialToolboxId, toolbox, toolboxId, toolboxTutorial]);
|
||||
|
||||
function addModule(type) {
|
||||
function addModule(type, initialData = null) {
|
||||
if (!type) return;
|
||||
const module = { id: uid("mod"), type };
|
||||
const modules = [...toolbox.modules, module];
|
||||
|
|
@ -229,6 +231,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
modules,
|
||||
moduleOrder: addModuleToOrder(toolbox.moduleOrder, modules, module.id)
|
||||
});
|
||||
if (initialData) updateModuleData(toolbox.id, module.id, initialData, type);
|
||||
}
|
||||
|
||||
function createImageAnnotationModule(dataUrl) {
|
||||
|
|
|
|||
1215
website/src/features/toolboxes/modules/CalendarModule.jsx
Normal file
1215
website/src/features/toolboxes/modules/CalendarModule.jsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -442,9 +442,6 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
<div className="combos-module">
|
||||
{(editing || editingComboId) && (
|
||||
<div className="module-add-panel combos-editor">
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
<div className="combos-device-row">
|
||||
<label>
|
||||
<span>{textContent.deviceLabel || "Périphérique"}</span>
|
||||
|
|
@ -592,6 +589,9 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
266
website/src/features/toolboxes/modules/calendarUtils.js
Normal file
266
website/src/features/toolboxes/modules/calendarUtils.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
// Rôle : regroupe les calculs hebdomadaires/mensuels, occurrences et alertes du calendrier.
|
||||
export const CALENDAR_VIEW_MODES = ["week", "month"];
|
||||
export const CALENDAR_MONTHLY_REPEAT_BY = ["monthDay", "weekday"];
|
||||
export const CALENDAR_REPEAT_MODES = ["none", "daily", "weekly", "monthly"];
|
||||
export const CALENDAR_MONTH_REPEAT_MODES = ["none", "weekly", "monthly"];
|
||||
export const CALENDAR_ALERT_MODES = ["off", "visible", "site"];
|
||||
export const CALENDAR_ALERT_OFFSETS = [10, 5, 0];
|
||||
export const CALENDAR_COLORS = ["violet", "gold", "cyan", "pink", "green", "gray"];
|
||||
export const WEEK_SLOT_MINUTES = 30;
|
||||
export const WEEK_TIME_INPUT_STEP_MINUTES = 5;
|
||||
export const WEEK_SLOT_COUNT = 48;
|
||||
export const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
export const MINUTE_MS = 60 * 1000;
|
||||
export const WEEK_DAY_COUNT = 7;
|
||||
|
||||
export function pad2(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
export function formatLocalDateKey(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
||||
}
|
||||
|
||||
export function parseLocalDateKey(value) {
|
||||
const match = String(value || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]) - 1;
|
||||
const day = Number(match[3]);
|
||||
const date = new Date(year, month, day);
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
export function minutesToTimeValue(minutes) {
|
||||
const normalized = Math.max(0, Math.min(23 * 60 + 59, Number(minutes) || 0));
|
||||
return `${pad2(Math.floor(normalized / 60))}:${pad2(normalized % 60)}`;
|
||||
}
|
||||
|
||||
export function timeValueToMinutes(value) {
|
||||
const match = String(value || "").match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (!match) return -1;
|
||||
const hours = Number(match[1]);
|
||||
const minutes = Number(match[2]);
|
||||
if (!Number.isInteger(hours) || !Number.isInteger(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return -1;
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
export function startOfLocalDay(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
export function addDays(dateLike, days) {
|
||||
const date = new Date(dateLike);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function addMonths(dateLike, months) {
|
||||
const date = new Date(dateLike);
|
||||
date.setMonth(date.getMonth() + months);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getLocalDayIndex(dateLike) {
|
||||
return (new Date(dateLike).getDay() + 6) % WEEK_DAY_COUNT;
|
||||
}
|
||||
|
||||
export function getWeekStart(dateLike, weekStartsOn = 1) {
|
||||
const day = startOfLocalDay(dateLike);
|
||||
const current = day.getDay();
|
||||
const offset = (current - weekStartsOn + 7) % 7;
|
||||
return addDays(day, -offset);
|
||||
}
|
||||
|
||||
export function getWeekDays(weekStart) {
|
||||
return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
|
||||
}
|
||||
|
||||
export function getMonthStart(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
}
|
||||
|
||||
export function getMonthEnd(dateLike) {
|
||||
const date = getMonthStart(dateLike);
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getDaysInMonth(dateLike) {
|
||||
return new Date(new Date(dateLike).getFullYear(), new Date(dateLike).getMonth() + 1, 0).getDate();
|
||||
}
|
||||
|
||||
export function getMonthGridDays(dateLike) {
|
||||
const monthStart = getMonthStart(dateLike);
|
||||
const monthEnd = getMonthEnd(dateLike);
|
||||
const gridStart = getWeekStart(monthStart, 1);
|
||||
const gridEnd = addDays(getWeekStart(addDays(monthEnd, -1), 1), 7);
|
||||
const days = [];
|
||||
for (let cursor = new Date(gridStart); cursor < gridEnd; cursor = addDays(cursor, 1)) {
|
||||
const date = new Date(cursor);
|
||||
days.push({
|
||||
date,
|
||||
dayNumber: date.getDate(),
|
||||
dayIndex: getLocalDayIndex(date),
|
||||
inMonth: date.getMonth() === monthStart.getMonth() && date.getFullYear() === monthStart.getFullYear()
|
||||
});
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
export function getDateForWeekDay(weekStart, dayIndex, minutes = 0) {
|
||||
const date = addDays(weekStart, Math.max(0, Math.min(WEEK_DAY_COUNT - 1, Number(dayIndex) || 0)));
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getDateForMonthDay(monthDate, monthDay, minutes = 0) {
|
||||
const day = Number(monthDay);
|
||||
const monthStart = getMonthStart(monthDate);
|
||||
if (!Number.isInteger(day) || day < 1 || day > getDaysInMonth(monthStart)) return null;
|
||||
const date = new Date(monthStart.getFullYear(), monthStart.getMonth(), day);
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function getWeekdayOrdinalInMonth(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return Math.floor((date.getDate() - 1) / 7) + 1;
|
||||
}
|
||||
|
||||
function getMonthlyWeekdayDate(monthDate, sourceDate, minutes = 0) {
|
||||
const source = new Date(sourceDate);
|
||||
const ordinal = getWeekdayOrdinalInMonth(source);
|
||||
const dayIndex = getLocalDayIndex(source);
|
||||
const monthStart = getMonthStart(monthDate);
|
||||
const firstDayOffset = (dayIndex - getLocalDayIndex(monthStart) + WEEK_DAY_COUNT) % WEEK_DAY_COUNT;
|
||||
const date = addDays(monthStart, firstDayOffset + (ordinal - 1) * WEEK_DAY_COUNT);
|
||||
if (date.getMonth() !== monthStart.getMonth()) return null;
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function getMonthlyDateRange(event, monthDate) {
|
||||
const sourceStart = parseLocalDateKey(event.startDate);
|
||||
const sourceEnd = parseLocalDateKey(event.endDate || event.startDate);
|
||||
if (!sourceStart || !sourceEnd || sourceEnd < sourceStart) return null;
|
||||
const spanDays = Math.max(0, Math.round((startOfLocalDay(sourceEnd).getTime() - startOfLocalDay(sourceStart).getTime()) / DAY_MS));
|
||||
const start = event.monthlyRepeatBy === "weekday"
|
||||
? getMonthlyWeekdayDate(monthDate, sourceStart, event.startMinutes)
|
||||
: getDateForMonthDay(monthDate, sourceStart.getDate(), event.startMinutes);
|
||||
if (!start) return null;
|
||||
const end = addDays(start, spanDays + 1);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function getDatedEventRange(event) {
|
||||
const start = parseLocalDateKey(event.startDate);
|
||||
const endSource = parseLocalDateKey(event.endDate || event.startDate);
|
||||
if (!start || !endSource || endSource < start) return null;
|
||||
start.setHours(0, Math.max(0, Number(event.startMinutes) || 0), 0, 0);
|
||||
const end = addDays(endSource, 1);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
export function getEventDurationMs(event) {
|
||||
const startMinutes = Number(event.startMinutes);
|
||||
const endMinutes = Number(event.endMinutes);
|
||||
return Number.isFinite(startMinutes) && Number.isFinite(endMinutes) && endMinutes > startMinutes ? (endMinutes - startMinutes) * MINUTE_MS : 0;
|
||||
}
|
||||
|
||||
export function getEventEndMs(event, startMs) {
|
||||
const durationMs = getEventDurationMs(event);
|
||||
return durationMs > 0 ? startMs + durationMs : startMs;
|
||||
}
|
||||
|
||||
export function getCalendarAlertOffsets(event) {
|
||||
const rawOffsets = Array.isArray(event?.alertOffsetMinutes) ? event.alertOffsetMinutes : [event?.alertOffsetMinutes];
|
||||
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => CALENDAR_ALERT_OFFSETS.includes(offset)))];
|
||||
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
|
||||
}
|
||||
|
||||
function getWeeklyOccurrenceStart(event, weekStart, offset) {
|
||||
if (event.repeat === "none") return getDateForWeekDay(weekStart, event.dayIndex, event.startMinutes);
|
||||
return getDateForWeekDay(addDays(weekStart, offset * 7), event.dayIndex, event.startMinutes);
|
||||
}
|
||||
|
||||
export function getCalendarOccurrences(events, rangeStart, rangeEnd) {
|
||||
const baseWeekStart = getWeekStart(rangeStart);
|
||||
const startMs = rangeStart.getTime();
|
||||
const endMs = rangeEnd.getTime();
|
||||
return events.flatMap((event) => {
|
||||
const durationMs = getEventDurationMs(event);
|
||||
const candidates = [];
|
||||
|
||||
if (event.startDate) {
|
||||
const baseRange = getDatedEventRange(event);
|
||||
if (baseRange) {
|
||||
const spanDays = Math.max(0, Math.round((baseRange.end.getTime() - baseRange.start.getTime()) / DAY_MS) - 1);
|
||||
if (event.repeat === "monthly") {
|
||||
for (let cursor = getMonthStart(rangeStart); cursor < rangeEnd; cursor = addMonths(cursor, 1)) {
|
||||
const range = getMonthlyDateRange(event, cursor);
|
||||
if (range) candidates.push(range);
|
||||
}
|
||||
} else if (event.repeat === "weekly") {
|
||||
const firstOffset = Math.floor((getWeekStart(rangeStart).getTime() - getWeekStart(baseRange.start).getTime()) / (7 * DAY_MS)) - 1;
|
||||
const weekCount = Math.ceil((rangeEnd.getTime() - rangeStart.getTime()) / (7 * DAY_MS)) + 3;
|
||||
for (let index = Math.max(-1, firstOffset); index < firstOffset + weekCount; index += 1) {
|
||||
const start = addDays(baseRange.start, index * 7);
|
||||
const end = addDays(start, spanDays + 1);
|
||||
candidates.push({ start, end });
|
||||
}
|
||||
} else {
|
||||
candidates.push(baseRange);
|
||||
}
|
||||
}
|
||||
} else if (event.repeat === "none") {
|
||||
candidates.push(getWeeklyOccurrenceStart(event, baseWeekStart, 0));
|
||||
} else {
|
||||
candidates.push(getWeeklyOccurrenceStart(event, baseWeekStart, -1), getWeeklyOccurrenceStart(event, baseWeekStart, 0), getWeeklyOccurrenceStart(event, baseWeekStart, 1));
|
||||
}
|
||||
|
||||
return candidates
|
||||
.map((candidate) => candidate instanceof Date ? { start: candidate, end: null } : candidate)
|
||||
.filter((candidate) => candidate?.start && !Number.isNaN(candidate.start.getTime()))
|
||||
.filter((candidate) => !event.excludedOccurrences?.includes?.(formatLocalDateKey(candidate.start)))
|
||||
.map((candidate) => {
|
||||
const occurrenceStartMs = candidate.start.getTime();
|
||||
const occurrenceEndMs = candidate.end && !Number.isNaN(candidate.end.getTime()) ? candidate.end.getTime() : 0;
|
||||
return {
|
||||
id: `${event.id}:${occurrenceStartMs}`,
|
||||
event,
|
||||
startMs: occurrenceStartMs,
|
||||
endMs: occurrenceEndMs > occurrenceStartMs ? occurrenceEndMs : durationMs > 0 ? occurrenceStartMs + durationMs : occurrenceStartMs
|
||||
};
|
||||
})
|
||||
.filter((occurrence) => {
|
||||
const visibleEndMs = occurrence.endMs || occurrence.startMs;
|
||||
return visibleEndMs >= startMs && occurrence.startMs < endMs;
|
||||
});
|
||||
}).sort((a, b) => a.startMs - b.startMs);
|
||||
}
|
||||
|
||||
export function getNextCalendarAlertTime(event, nowMs) {
|
||||
if (!event || event.alertMode === "off") return 0;
|
||||
const rangeStart = addDays(new Date(nowMs), -1);
|
||||
const rangeEnd = addDays(new Date(nowMs), 370);
|
||||
const alert = getCalendarOccurrences([event], rangeStart, rangeEnd)
|
||||
.flatMap((occurrence) => getCalendarAlertOffsets(event).map((offset) => occurrence.startMs - offset * MINUTE_MS))
|
||||
.filter((alertMs) => alertMs >= nowMs)
|
||||
.sort((a, b) => a - b)[0];
|
||||
return alert || 0;
|
||||
}
|
||||
|
||||
export function getCurrentCalendarOccurrence(event, nowMs) {
|
||||
if (!event?.endMinutes && !event?.startDate) return null;
|
||||
const rangeStart = addDays(new Date(nowMs), -1);
|
||||
const rangeEnd = addDays(new Date(nowMs), 1);
|
||||
return getCalendarOccurrences([event], rangeStart, rangeEnd)
|
||||
.find((occurrence) => occurrence.startMs <= nowMs && occurrence.endMs > nowMs) || null;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
|||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { CalendarModule } from "./CalendarModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CombosModule } from "./CombosModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
|
|
@ -28,6 +29,7 @@ const MODULE_COMPONENTS = {
|
|||
counters: { ...TOOLBOX_MODULE_DEFINITIONS.counters, Component: CountersModule },
|
||||
combos: { ...TOOLBOX_MODULE_DEFINITIONS.combos, Component: CombosModule },
|
||||
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
|
||||
calendar: { ...TOOLBOX_MODULE_DEFINITIONS.calendar, Component: CalendarModule },
|
||||
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
|
||||
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
|
||||
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
|
||||
|
|
@ -352,7 +354,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} />
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} setEditing={setEditing} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Rôle : parse et sérialise les formats texte/TSV d'échange des outils toolbox.
|
||||
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, minutesToTimeValue, timeValueToMinutes } from "./calendarUtils.js";
|
||||
import { cellAddress, columnIndexToName } from "./tableFormulaEngine.js";
|
||||
import { getCountdownTargetMs, getDailyTargetMs, getIntervalAnchorMs } from "./timerUtils.js";
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
|
|||
"calculator",
|
||||
"table",
|
||||
"timer",
|
||||
"calendar",
|
||||
"taskPlanner",
|
||||
"equipmentPlanner",
|
||||
"images",
|
||||
|
|
@ -36,6 +38,12 @@ const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboard
|
|||
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
|
||||
const TIMER_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||
const ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||
const CALENDAR_ALERT_MODE_SET = new Set(CALENDAR_ALERT_MODES);
|
||||
const CALENDAR_ALERT_OFFSET_SET = new Set(CALENDAR_ALERT_OFFSETS);
|
||||
const CALENDAR_COLOR_SET = new Set(CALENDAR_COLORS);
|
||||
const CALENDAR_MONTHLY_REPEAT_BY_SET = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||
const CALENDAR_MONTH_REPEAT_MODE_SET = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||
const CALENDAR_WEEKDAYS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
|
||||
const IMAGE_DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i;
|
||||
|
||||
export function supportsTextExchange(type) {
|
||||
|
|
@ -51,6 +59,7 @@ export function hasTextExchangeContent(type, data) {
|
|||
if (type === "calculator") return Boolean(data?.entries?.length || data?.scrollResults);
|
||||
if (type === "table") return Boolean(Object.keys(data?.cells || {}).length || Object.keys(data?.rowLabels || {}).length || Object.keys(data?.columnLabels || {}).length);
|
||||
if (type === "timer") return Boolean(data?.stopwatch?.laps?.length || data?.countdowns?.length || data?.stopwatch?.elapsedMs);
|
||||
if (type === "calendar") return Boolean(data?.events?.length);
|
||||
if (type === "taskPlanner") return Boolean(data?.tasks?.length);
|
||||
if (type === "equipmentPlanner") return Boolean(data?.equipments?.length);
|
||||
if (type === "images") return Boolean(data?.images?.length);
|
||||
|
|
@ -67,6 +76,7 @@ export function exportModuleText(type, data) {
|
|||
if (type === "calculator") return exportCalculatorText(data);
|
||||
if (type === "table") return exportTableTsv(data);
|
||||
if (type === "timer") return exportTimerText(data);
|
||||
if (type === "calendar") return exportCalendarText(data);
|
||||
if (type === "taskPlanner") return exportTaskPlannerText(data);
|
||||
if (type === "equipmentPlanner") return exportEquipmentPlannerText(data);
|
||||
if (type === "images") return exportImagesText(data);
|
||||
|
|
@ -87,6 +97,7 @@ export function importModuleText(type, text, context) {
|
|||
else if (type === "calculator") data = importCalculatorText(value, context);
|
||||
else if (type === "table") data = importTableTsv(value);
|
||||
else if (type === "timer") data = importTimerText(value, context);
|
||||
else if (type === "calendar") data = importCalendarText(value, context);
|
||||
else if (type === "taskPlanner") data = importTaskPlannerText(value, context);
|
||||
else if (type === "equipmentPlanner") data = importEquipmentPlannerText(value, context);
|
||||
else if (type === "images") data = importImagesText(value, context);
|
||||
|
|
@ -644,6 +655,112 @@ function importTimerText(text, context) {
|
|||
return context.normalizeTimerData(data);
|
||||
}
|
||||
|
||||
function parseCalendarDayIndex(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
const weekdayIndex = CALENDAR_WEEKDAYS.indexOf(normalized);
|
||||
if (weekdayIndex >= 0) return weekdayIndex;
|
||||
const parsed = parseInteger(normalized, -1);
|
||||
return parsed >= 0 && parsed <= 6 ? parsed : -1;
|
||||
}
|
||||
|
||||
function parseCalendarAlertOffsets(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map((item) => parseInteger(item.trim(), Number.NaN))
|
||||
.filter((offset, index, offsets) => CALENDAR_ALERT_OFFSET_SET.has(offset) && offsets.indexOf(offset) === index)
|
||||
.sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
function formatCalendarEventMeta(event, viewMode) {
|
||||
const alertMode = CALENDAR_ALERT_MODE_SET.has(event.alertMode) ? event.alertMode : "off";
|
||||
const meta = {
|
||||
title: event.title,
|
||||
color: event.color && event.color !== "violet" ? event.color : "",
|
||||
repeatable: event.repeatable === false ? "false" : "",
|
||||
alertMode: alertMode !== "off" ? alertMode : "",
|
||||
alerts: alertMode !== "off" ? (event.alertOffsetMinutes || [10]).join(",") : ""
|
||||
};
|
||||
if (viewMode === "month") {
|
||||
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(event.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||
if (monthlyRepeatBy === "weekday") {
|
||||
return {
|
||||
...meta,
|
||||
repeatBy: "weekday",
|
||||
repeat: event.repeat || "none",
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||
};
|
||||
}
|
||||
return {
|
||||
...meta,
|
||||
repeatBy: "monthDay",
|
||||
repeat: event.repeat || "none",
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||
};
|
||||
}
|
||||
return {
|
||||
...meta,
|
||||
day: CALENDAR_WEEKDAYS[event.dayIndex] || "",
|
||||
start: minutesToTimeValue(event.startMinutes),
|
||||
end: event.endMinutes ? minutesToTimeValue(event.endMinutes) : ""
|
||||
};
|
||||
}
|
||||
|
||||
function exportCalendarText(data) {
|
||||
const viewMode = data?.viewMode === "month" ? "month" : "week";
|
||||
const lines = [`@viewMode: ${viewMode}`];
|
||||
if (data?.weekStartsOn === 0) lines.push("@weekStartsOn: 0");
|
||||
(data?.events || []).forEach((event) => {
|
||||
lines.push(`event | ${formatMeta(formatCalendarEventMeta(event, viewMode))}`);
|
||||
});
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function importCalendarText(text, context) {
|
||||
const trimmed = String(text || "").trim();
|
||||
if (trimmed.startsWith("{")) {
|
||||
return context.normalizeCalendarData(JSON.parse(trimmed));
|
||||
}
|
||||
|
||||
const data = { initialized: true, viewMode: "week", weekStartsOn: 1, events: [] };
|
||||
splitLines(text).forEach((line, index) => {
|
||||
const cleanLine = line.trim();
|
||||
if (!cleanLine) return;
|
||||
const meta = parseMetaLine(cleanLine);
|
||||
if (meta) {
|
||||
if (meta.key === "viewMode") data.viewMode = meta.value === "month" ? "month" : "week";
|
||||
if (meta.key === "weekStartsOn") data.weekStartsOn = parseInteger(meta.value, 1) === 0 ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, meta: lineMeta } = parsePipeParts(cleanLine);
|
||||
if (title !== "event") throw lineError(index, `ligne calendrier invalide: ${cleanLine}`);
|
||||
const event = {
|
||||
id: context.uid("calendar"),
|
||||
title: lineMeta.title || "",
|
||||
repeatable: lineMeta.repeatable !== "false",
|
||||
alertMode: CALENDAR_ALERT_MODE_SET.has(lineMeta.alertMode) ? lineMeta.alertMode : "off",
|
||||
alertOffsetMinutes: parseCalendarAlertOffsets(lineMeta.alerts),
|
||||
color: CALENDAR_COLOR_SET.has(lineMeta.color) ? lineMeta.color : "violet"
|
||||
};
|
||||
if (data.viewMode === "month") {
|
||||
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(lineMeta.repeatBy) ? lineMeta.repeatBy : "monthDay";
|
||||
event.startMinutes = 0;
|
||||
event.monthlyRepeatBy = monthlyRepeatBy;
|
||||
event.repeat = CALENDAR_MONTH_REPEAT_MODE_SET.has(lineMeta.repeat) ? lineMeta.repeat : "none";
|
||||
event.startDate = lineMeta.startDate || "";
|
||||
event.endDate = lineMeta.endDate || event.startDate;
|
||||
} else {
|
||||
event.dayIndex = parseCalendarDayIndex(lineMeta.day);
|
||||
event.startMinutes = timeValueToMinutes(lineMeta.start);
|
||||
if (lineMeta.end) event.endMinutes = timeValueToMinutes(lineMeta.end);
|
||||
}
|
||||
data.events.push(event);
|
||||
});
|
||||
return context.normalizeCalendarData(data);
|
||||
}
|
||||
|
||||
function exportImagesText(data) {
|
||||
return (data.images || []).flatMap((image) => [
|
||||
image.label ? `# ${image.label}` : "#",
|
||||
|
|
|
|||
123
website/src/features/toolboxes/storage/modules/calendar.js
Normal file
123
website/src/features/toolboxes/storage/modules/calendar.js
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// Rôle : normalise et compacte les événements du calendrier hebdomadaire et mensuel.
|
||||
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, CALENDAR_VIEW_MODES, formatLocalDateKey, getLocalDayIndex, parseLocalDateKey, WEEK_DAY_COUNT } from "../../modules/calendarUtils.js";
|
||||
import { uid } from "./shared.js";
|
||||
|
||||
const VIEW_MODES = new Set(CALENDAR_VIEW_MODES);
|
||||
const ALERT_MODES = new Set(CALENDAR_ALERT_MODES);
|
||||
const ALERT_OFFSETS = new Set(CALENDAR_ALERT_OFFSETS);
|
||||
const COLORS = new Set(CALENDAR_COLORS);
|
||||
const MONTHLY_REPEAT_BY = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||
const MONTH_REPEAT_MODES = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||
|
||||
function normalizeDayIndex(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed < WEEK_DAY_COUNT ? parsed : -1;
|
||||
}
|
||||
|
||||
function normalizeMinuteOfDay(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed < 24 * 60 ? parsed : -1;
|
||||
}
|
||||
|
||||
function normalizeDateKey(value) {
|
||||
const date = parseLocalDateKey(value);
|
||||
return date ? formatLocalDateKey(date) : "";
|
||||
}
|
||||
|
||||
function normalizeExcludedOccurrences(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value.map(normalizeDateKey).filter(Boolean))].sort();
|
||||
}
|
||||
|
||||
function normalizeAlertOffsets(value) {
|
||||
const rawOffsets = Array.isArray(value) ? value : [value];
|
||||
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => ALERT_OFFSETS.has(offset)))];
|
||||
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
|
||||
}
|
||||
|
||||
export function normalizeCalendarData(data) {
|
||||
const viewMode = VIEW_MODES.has(data?.viewMode) ? data.viewMode : "week";
|
||||
const events = (Array.isArray(data?.events) ? data.events : [])
|
||||
.map((event) => {
|
||||
const title = String(event?.title || "").trim();
|
||||
const dayIndex = normalizeDayIndex(event?.dayIndex);
|
||||
const startDate = normalizeDateKey(event?.startDate);
|
||||
const endDate = normalizeDateKey(event?.endDate || event?.startDate);
|
||||
const parsedStartMinutes = normalizeMinuteOfDay(event?.startMinutes);
|
||||
const startMinutes = viewMode === "month" && parsedStartMinutes < 0 ? 0 : parsedStartMinutes;
|
||||
const monthlyRepeatBy = MONTHLY_REPEAT_BY.has(event?.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||
const monthRepeat = MONTH_REPEAT_MODES.has(event?.repeat) ? event.repeat : event?.repeatable === false ? "none" : "monthly";
|
||||
const hasValidDateTarget = viewMode === "month"
|
||||
? startDate && endDate && endDate >= startDate
|
||||
: dayIndex >= 0;
|
||||
if (!title || !hasValidDateTarget || startMinutes < 0) return null;
|
||||
|
||||
const endMinutes = normalizeMinuteOfDay(event?.endMinutes);
|
||||
const alertMode = ALERT_MODES.has(event?.alertMode) ? event.alertMode : "off";
|
||||
const alertOffsetMinutes = normalizeAlertOffsets(event?.alertOffsetMinutes);
|
||||
const color = COLORS.has(event?.color) ? event.color : "violet";
|
||||
const repeatable = viewMode === "month" ? monthRepeat !== "none" : event?.repeatable !== false;
|
||||
const normalized = {
|
||||
id: event?.id || uid("calendar"),
|
||||
title,
|
||||
startMinutes,
|
||||
repeat: viewMode === "month" ? monthlyRepeatBy === "monthDay" && monthRepeat === "weekly" ? "none" : monthRepeat : repeatable ? "weekly" : "none",
|
||||
repeatable,
|
||||
alertMode,
|
||||
alertOffsetMinutes,
|
||||
color
|
||||
};
|
||||
if (viewMode === "month") {
|
||||
normalized.monthlyRepeatBy = monthlyRepeatBy;
|
||||
normalized.startDate = startDate;
|
||||
normalized.endDate = endDate;
|
||||
normalized.dayIndex = dayIndex >= 0 ? dayIndex : getLocalDayIndex(parseLocalDateKey(startDate));
|
||||
normalized.excludedOccurrences = normalizeExcludedOccurrences(event?.excludedOccurrences);
|
||||
} else {
|
||||
normalized.dayIndex = dayIndex;
|
||||
}
|
||||
if (endMinutes > startMinutes) normalized.endMinutes = endMinutes;
|
||||
return normalized;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
initialized: data?.initialized === true || events.length > 0,
|
||||
viewMode,
|
||||
weekStartsOn: Number(data?.weekStartsOn) === 0 ? 0 : 1,
|
||||
events
|
||||
};
|
||||
}
|
||||
|
||||
export function compactCalendarDataForStorage(value) {
|
||||
const normalized = normalizeCalendarData(value);
|
||||
const compact = {};
|
||||
if (normalized.initialized) compact.initialized = true;
|
||||
if (normalized.viewMode !== "week") compact.viewMode = normalized.viewMode;
|
||||
if (normalized.weekStartsOn !== 1) compact.weekStartsOn = normalized.weekStartsOn;
|
||||
if (normalized.events.length) {
|
||||
compact.events = normalized.events.map((event) => {
|
||||
const compactEvent = {
|
||||
id: event.id,
|
||||
title: event.title
|
||||
};
|
||||
if (normalized.viewMode !== "month" || event.startMinutes !== 0) compactEvent.startMinutes = event.startMinutes;
|
||||
if (normalized.viewMode === "month") {
|
||||
compactEvent.startDate = event.startDate;
|
||||
if (event.endDate !== event.startDate) compactEvent.endDate = event.endDate;
|
||||
if (event.repeat !== "monthly") compactEvent.repeat = event.repeat;
|
||||
if (event.monthlyRepeatBy !== "monthDay") compactEvent.monthlyRepeatBy = event.monthlyRepeatBy;
|
||||
if (event.excludedOccurrences.length) compactEvent.excludedOccurrences = event.excludedOccurrences;
|
||||
} else {
|
||||
compactEvent.dayIndex = event.dayIndex;
|
||||
}
|
||||
if (normalized.viewMode !== "month" && event.endMinutes) compactEvent.endMinutes = event.endMinutes;
|
||||
if (normalized.viewMode !== "month" && event.repeatable === false) compactEvent.repeatable = false;
|
||||
if (event.alertMode !== "off") compactEvent.alertMode = event.alertMode;
|
||||
if (event.alertMode !== "off" && (event.alertOffsetMinutes.length !== 1 || event.alertOffsetMinutes[0] !== 10)) compactEvent.alertOffsetMinutes = event.alertOffsetMinutes;
|
||||
if (event.color !== "violet") compactEvent.color = event.color;
|
||||
return compactEvent;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// Rôle : expose le registre strict des compacteurs par type d'outil toolbox.
|
||||
import { compactCalculatorDataForStorage } from "./calculator.js";
|
||||
import { compactCalendarDataForStorage } from "./calendar.js";
|
||||
import { compactChecklistDataForStorage } from "./checklist.js";
|
||||
import { compactCombosDataForStorage } from "./combos.js";
|
||||
import { compactCountersDataForStorage } from "./counters.js";
|
||||
|
|
@ -20,6 +21,7 @@ export const MODULE_DATA_COMPACTORS = {
|
|||
counters: compactCountersDataForStorage,
|
||||
combos: compactCombosDataForStorage,
|
||||
calculator: compactCalculatorDataForStorage,
|
||||
calendar: compactCalendarDataForStorage,
|
||||
table: compactTableDataForStorage,
|
||||
timer: compactTimerDataForStorage,
|
||||
taskPlanner: compactTaskPlannerDataForStorage,
|
||||
|
|
|
|||
|
|
@ -15,13 +15,23 @@ const NOTEPAD_RGB_COLOR_MAP = {
|
|||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value || "")
|
||||
return decodeBasicEntitiesDeep(value)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function decodeBasicEntitiesDeep(value) {
|
||||
let current = String(value || "");
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
const next = decodeBasicEntities(current);
|
||||
if (next === current) return next;
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function decodeBasicEntities(value) {
|
||||
return String(value || "")
|
||||
.replace(/ /gi, " ")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export const ID_PREFIXES = {
|
|||
counter: "c",
|
||||
combo: "o",
|
||||
calc: "r",
|
||||
calendar: "h",
|
||||
marker: "k",
|
||||
timer: "z",
|
||||
task: "a",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MODULE_DATA_COMPACTORS } from "./modules/index.js";
|
|||
import { ID_PREFIXES, uid } from "./modules/shared.js";
|
||||
|
||||
export { normalizeCalculatorData } from "./modules/calculator.js";
|
||||
export { normalizeCalendarData } from "./modules/calendar.js";
|
||||
export { clampQty, normalizeChecklistData } from "./modules/checklist.js";
|
||||
export { normalizeCombosData } from "./modules/combos.js";
|
||||
export { normalizeCountersData } from "./modules/counters.js";
|
||||
|
|
@ -250,6 +251,12 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "calendar") {
|
||||
const remapped = { ...compact };
|
||||
if (compact.events) remapped.events = compact.events.map((event) => ({ ...event, id: nextId("calendar") }));
|
||||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "taskPlanner") {
|
||||
const taskIdMap = new Map();
|
||||
compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
editable: true,
|
||||
libraryCategory: "timeRoutines"
|
||||
},
|
||||
calendar: {
|
||||
label: "Calendrier",
|
||||
defaultTitle: "Calendrier",
|
||||
mode: "Agenda hebdomadaire",
|
||||
icon: "calendar",
|
||||
editable: true,
|
||||
scrollable: true,
|
||||
libraryCategory: "timeRoutines"
|
||||
},
|
||||
taskPlanner: {
|
||||
label: "Planificateur de tâches",
|
||||
defaultTitle: "Planificateur de tâches",
|
||||
|
|
@ -115,7 +124,7 @@ export const LIBRARY_CATEGORY_DEFINITIONS = [
|
|||
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] },
|
||||
{ key: "references", types: ["images", "imageAnnotation", "links"] },
|
||||
{ key: "calculationData", types: ["calculator", "table"] },
|
||||
{ key: "timeRoutines", types: ["timer", "taskPlanner"] },
|
||||
{ key: "timeRoutines", types: ["timer", "calendar", "taskPlanner"] },
|
||||
{ key: "buildsCommands", types: ["combos", "equipmentPlanner"] }
|
||||
];
|
||||
|
||||
|
|
|
|||
99
website/src/hooks/useToolboxAlerts.js
Normal file
99
website/src/hooks/useToolboxAlerts.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Rôle : surveille globalement les alertes internes des outils timer et calendrier.
|
||||
import { useEffect, useRef } from "react";
|
||||
import { getCalendarAlertOffsets, getCurrentCalendarOccurrence, getNextCalendarAlertTime } from "../features/toolboxes/modules/calendarUtils.js";
|
||||
import { getCountdownTargetMs } from "../features/toolboxes/modules/timerUtils.js";
|
||||
import { moduleStorageKey } from "../features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
function getVisibleToolboxIds(route, drawerGameId, links) {
|
||||
const visibleIds = new Set();
|
||||
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
|
||||
if (toolboxMatch?.[1]) visibleIds.add(toolboxMatch[1]);
|
||||
const drawerToolboxId = drawerGameId ? links[drawerGameId] : "";
|
||||
if (drawerToolboxId) visibleIds.add(drawerToolboxId);
|
||||
return visibleIds;
|
||||
}
|
||||
|
||||
function isAlertAllowed(alertMode, toolboxId, visibleToolboxIds) {
|
||||
if (alertMode === "off") return false;
|
||||
if (alertMode === "visible") return visibleToolboxIds.has(toolboxId);
|
||||
return alertMode === "site";
|
||||
}
|
||||
|
||||
function formatAlertMessage(template, toolbox, label) {
|
||||
return template
|
||||
.replaceAll("{label}", label)
|
||||
.replaceAll("{toolbox}", toolbox.name);
|
||||
}
|
||||
|
||||
export function useToolboxAlerts({ toolboxes, moduleData, links, route, drawerGameId, normalizeTimerData, normalizeCalendarData, textContent, notify }) {
|
||||
const previousTargetsRef = useRef(new Map());
|
||||
const resumedCalendarEventsRef = useRef(new Set());
|
||||
const lastTickRef = useRef(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
const visibleToolboxIds = getVisibleToolboxIds(route, drawerGameId, links);
|
||||
const timerAlertTemplate = textContent?.timer?.alertMessage || "Compte à rebours terminé : {label}";
|
||||
const calendarAlertTemplate = textContent?.calendar?.alertMessage || "Événement à venir : {label}";
|
||||
const calendarResumeTemplate = textContent?.calendar?.resumeAlertMessage || "Événement en cours : {label}";
|
||||
|
||||
function tick() {
|
||||
const nowMs = Date.now();
|
||||
const lastTickMs = lastTickRef.current;
|
||||
const nextTargets = new Map();
|
||||
|
||||
toolboxes.forEach((toolbox) => {
|
||||
toolbox.modules.forEach((module) => {
|
||||
if (module.type === "timer") {
|
||||
const data = normalizeTimerData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
|
||||
|
||||
data.countdowns.forEach((countdown) => {
|
||||
if (!isAlertAllowed(countdown.alertMode, toolbox.id, visibleToolboxIds)) return;
|
||||
|
||||
const key = `${toolbox.id}:${module.id}:${countdown.id}`;
|
||||
const targetMs = getCountdownTargetMs(countdown, nowMs);
|
||||
const previousTargetMs = previousTargetsRef.current.get(key) || targetMs;
|
||||
nextTargets.set(key, targetMs);
|
||||
|
||||
if (!targetMs || previousTargetMs <= lastTickMs || previousTargetMs > nowMs) return;
|
||||
notify(formatAlertMessage(timerAlertTemplate, toolbox, countdown.label));
|
||||
});
|
||||
}
|
||||
|
||||
if (module.type === "calendar") {
|
||||
const data = normalizeCalendarData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
|
||||
|
||||
data.events.forEach((event) => {
|
||||
if (!isAlertAllowed(event.alertMode, toolbox.id, visibleToolboxIds)) return;
|
||||
|
||||
const key = `${toolbox.id}:${module.id}:${event.id}`;
|
||||
getCalendarAlertOffsets(event).forEach((offset) => {
|
||||
const alertKey = `${key}:alert:${offset}`;
|
||||
const alertMs = getNextCalendarAlertTime({ ...event, alertOffsetMinutes: offset }, nowMs);
|
||||
const previousAlertMs = previousTargetsRef.current.get(alertKey) || alertMs;
|
||||
if (alertMs) nextTargets.set(alertKey, alertMs);
|
||||
|
||||
if (alertMs && previousAlertMs > lastTickMs && previousAlertMs <= nowMs) {
|
||||
notify(formatAlertMessage(calendarAlertTemplate, toolbox, event.title));
|
||||
}
|
||||
});
|
||||
|
||||
const currentOccurrence = getCurrentCalendarOccurrence(event, nowMs);
|
||||
if (!currentOccurrence) return;
|
||||
const resumeKey = `${key}:${currentOccurrence.startMs}`;
|
||||
if (resumedCalendarEventsRef.current.has(resumeKey)) return;
|
||||
resumedCalendarEventsRef.current.add(resumeKey);
|
||||
notify(formatAlertMessage(calendarResumeTemplate, toolbox, event.title));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
previousTargetsRef.current = nextTargets;
|
||||
lastTickRef.current = nowMs;
|
||||
}
|
||||
|
||||
tick();
|
||||
const intervalId = window.setInterval(tick, 500);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, [drawerGameId, links, moduleData, normalizeCalendarData, normalizeTimerData, notify, route, textContent, toolboxes]);
|
||||
}
|
||||
|
|
@ -9,12 +9,20 @@ import { useIndexedToolboxes } from "./features/toolboxes/storage/useIndexedTool
|
|||
import { useToolboxActions } from "./features/toolboxes/useToolboxActions.js";
|
||||
import { useAppData } from "./hooks/useAppData.js";
|
||||
import { useGameFilters } from "./hooks/useGameFilters.js";
|
||||
import { useTimerAlerts } from "./hooks/useTimerAlerts.js";
|
||||
import { useToolboxAlerts } from "./hooks/useToolboxAlerts.js";
|
||||
import { RouteContent } from "./router/RouteContent.jsx";
|
||||
import { useHashRoute } from "./router/hashRouter.js";
|
||||
import { normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
||||
import { normalizeCalendarData, normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
const ToolboxDrawer = lazy(() => import("./features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxDrawer })));
|
||||
const DEFAULT_NOTIFICATION_DURATION_MS = 120000;
|
||||
const SHORT_NOTIFICATION_DURATION_MS = 5000;
|
||||
|
||||
function getNotificationDuration(message, options = {}) {
|
||||
if (Number.isFinite(options.durationMs)) return Math.max(1000, Number(options.durationMs));
|
||||
const normalized = String(message || "").toLowerCase();
|
||||
return /\bcopi[ée]|\bcoll[ée]|presse-papiers/.test(normalized) ? SHORT_NOTIFICATION_DURATION_MS : DEFAULT_NOTIFICATION_DURATION_MS;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const route = useHashRoute();
|
||||
|
|
@ -31,9 +39,9 @@ function App() {
|
|||
const handleStorageError = useCallback((message) => setStorageError(message), []);
|
||||
const store = useIndexedToolboxes(handleStorageError);
|
||||
|
||||
const notify = useCallback((message) => {
|
||||
const notify = useCallback((message, options = {}) => {
|
||||
setNotifications((current) => [
|
||||
{ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, message },
|
||||
{ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, message, durationMs: getNotificationDuration(message, options) },
|
||||
...current
|
||||
].slice(0, 5));
|
||||
}, []);
|
||||
|
|
@ -44,7 +52,7 @@ function App() {
|
|||
|
||||
useEffect(() => {
|
||||
function handleNotification(event) {
|
||||
if (event.detail?.message) notify(event.detail.message);
|
||||
if (event.detail?.message) notify(event.detail.message, event.detail);
|
||||
}
|
||||
|
||||
window.addEventListener("sokkog:notify", handleNotification);
|
||||
|
|
@ -70,14 +78,15 @@ function App() {
|
|||
setImage
|
||||
});
|
||||
|
||||
useTimerAlerts({
|
||||
useToolboxAlerts({
|
||||
toolboxes: store.toolboxes,
|
||||
moduleData: store.moduleData,
|
||||
links: store.links,
|
||||
route,
|
||||
drawerGameId,
|
||||
normalizeTimerData,
|
||||
textContent: siteContent?.toolboxes?.modules?.timer,
|
||||
normalizeCalendarData,
|
||||
textContent: siteContent?.toolboxes?.modules,
|
||||
notify
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
clampQty,
|
||||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
|
|
@ -31,6 +32,7 @@ const MODULE_COMPONENTS = {
|
|||
counters: lazy(() => import("../features/toolboxes/modules/CountersModule.jsx").then((module) => ({ default: module.CountersModule }))),
|
||||
combos: lazy(() => import("../features/toolboxes/modules/CombosModule.jsx").then((module) => ({ default: module.CombosModule }))),
|
||||
calculator: lazy(() => import("../features/toolboxes/modules/CalculatorModule.jsx").then((module) => ({ default: module.CalculatorModule }))),
|
||||
calendar: lazy(() => import("../features/toolboxes/modules/CalendarModule.jsx").then((module) => ({ default: module.CalendarModule }))),
|
||||
table: lazy(() => import("../features/toolboxes/modules/TableModule.jsx").then((module) => ({ default: module.TableModule }))),
|
||||
timer: lazy(() => import("../features/toolboxes/modules/TimerModule.jsx").then((module) => ({ default: module.TimerModule }))),
|
||||
taskPlanner: lazy(() => import("../features/toolboxes/modules/TaskPlannerModule.jsx").then((module) => ({ default: module.TaskPlannerModule }))),
|
||||
|
|
@ -94,12 +96,15 @@ function getToolDescription(siteContent, definition) {
|
|||
}
|
||||
|
||||
function getLibraryCategories(modules, content) {
|
||||
const modulesByType = new Map((modules || []).map((module) => [module.type, module]));
|
||||
const modulesByType = new Map();
|
||||
(modules || []).forEach((module) => {
|
||||
modulesByType.set(module.type, [...(modulesByType.get(module.type) || []), module]);
|
||||
});
|
||||
return LIBRARY_CATEGORY_DEFINITIONS.map((category) => ({
|
||||
...category,
|
||||
title: content.categories?.[category.key] || category.key,
|
||||
description: content.categoryDescriptions?.[category.key] || "",
|
||||
modules: category.types.map((type) => modulesByType.get(type)).filter(Boolean)
|
||||
modules: category.types.flatMap((type) => modulesByType.get(type) || [])
|
||||
})).filter((category) => category.modules.length);
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +154,7 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
normalizeCountersData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeCalculatorData,
|
||||
normalizeCalendarData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
|
|
@ -245,7 +251,11 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||||
description={getToolDescription(siteContent, getToolboxModuleDefinition(module.type))}
|
||||
categoryTitle={activeCategory.title}
|
||||
docs={content.toolDocs?.[module.type]}
|
||||
docs={{
|
||||
...(content.toolDocs?.[module.type] || {}),
|
||||
...(module.controls ? { controls: module.controls } : {}),
|
||||
...(module.importFormat ? { importFormat: module.importFormat } : {})
|
||||
}}
|
||||
featureHeading={content.featureHeading}
|
||||
advancedHeading={content.advancedHeading}
|
||||
advancedCopyLabel={content.copyExampleLabel}
|
||||
|
|
|
|||
|
|
@ -613,12 +613,8 @@ span {
|
|||
}
|
||||
|
||||
.library-doc-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
display: grid;
|
||||
max-height: calc(100vh - var(--space-10));
|
||||
gap: var(--space-4);
|
||||
overflow: auto;
|
||||
padding: var(--space-4);
|
||||
scroll-margin-top: var(--space-5);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,31 @@
|
|||
-webkit-mask-image: url("/static/icons/clock.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calendar {
|
||||
mask-image: url("/static/icons/calendar.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calendar-month {
|
||||
mask-image: url("/static/icons/calendar-month.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar-month.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calendar-week {
|
||||
mask-image: url("/static/icons/calendar-week.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar-week.svg");
|
||||
}
|
||||
|
||||
.ui-icon-day-number {
|
||||
mask-image: url("/static/icons/day-number.svg");
|
||||
-webkit-mask-image: url("/static/icons/day-number.svg");
|
||||
}
|
||||
|
||||
.ui-icon-day-name {
|
||||
mask-image: url("/static/icons/day-name.svg");
|
||||
-webkit-mask-image: url("/static/icons/day-name.svg");
|
||||
}
|
||||
|
||||
.ui-icon-stopwatch {
|
||||
mask-image: url("/static/icons/stopwatch.svg");
|
||||
-webkit-mask-image: url("/static/icons/stopwatch.svg");
|
||||
|
|
@ -362,6 +387,11 @@
|
|||
-webkit-mask-image: url("/static/icons/trashcan.svg");
|
||||
}
|
||||
|
||||
.ui-icon-trashcan-one {
|
||||
mask-image: url("/static/icons/trashcan-one.svg");
|
||||
-webkit-mask-image: url("/static/icons/trashcan-one.svg");
|
||||
}
|
||||
|
||||
.game-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
|
|
|
|||
|
|
@ -243,7 +243,6 @@
|
|||
}
|
||||
|
||||
.library-doc-nav {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
|
|
|
|||
1113
website/src/styles/toolboxes/_calendar.scss
Normal file
1113
website/src/styles/toolboxes/_calendar.scss
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,7 @@
|
|||
@use "counters";
|
||||
@use "calculator";
|
||||
@use "timer";
|
||||
@use "calendar";
|
||||
@use "calculator-results";
|
||||
@use "table";
|
||||
@use "shared-controls";
|
||||
|
|
|
|||
|
|
@ -240,6 +240,11 @@
|
|||
-webkit-mask-image: url("/static/icons/clock.svg");
|
||||
}
|
||||
|
||||
.module-icon-calendar {
|
||||
mask-image: url("/static/icons/calendar.svg");
|
||||
-webkit-mask-image: url("/static/icons/calendar.svg");
|
||||
}
|
||||
|
||||
.module-icon-stopwatch {
|
||||
mask-image: url("/static/icons/stopwatch.svg");
|
||||
-webkit-mask-image: url("/static/icons/stopwatch.svg");
|
||||
|
|
|
|||
|
|
@ -127,7 +127,10 @@
|
|||
.task-planner-type-select,
|
||||
.task-planner-reset-override select,
|
||||
.task-planner-reset-override input,
|
||||
.task-planner-relations select {
|
||||
.task-planner-relations select,
|
||||
.combos-device-row select,
|
||||
.equipment-planner-socket-links select,
|
||||
.equipment-planner-socket-controls select {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
border: 1px solid var(--color-border);
|
||||
|
|
@ -140,7 +143,10 @@
|
|||
.task-planner-add-form select option,
|
||||
.task-planner-type-select option,
|
||||
.task-planner-reset-override select option,
|
||||
.task-planner-relations select option {
|
||||
.task-planner-relations select option,
|
||||
.combos-device-row select option,
|
||||
.equipment-planner-socket-links select option,
|
||||
.equipment-planner-socket-controls select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,10 @@
|
|||
.task-planner-add-form select,
|
||||
.task-planner-type-select,
|
||||
.task-planner-reset-override select,
|
||||
.task-planner-relations select {
|
||||
.task-planner-relations select,
|
||||
.combos-device-row select,
|
||||
.equipment-planner-socket-links select,
|
||||
.equipment-planner-socket-controls select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
overflow: hidden;
|
||||
|
|
@ -113,7 +116,13 @@
|
|||
.task-planner-reset-override select:hover,
|
||||
.task-planner-reset-override select:focus,
|
||||
.task-planner-relations select:hover,
|
||||
.task-planner-relations select:focus {
|
||||
.task-planner-relations select:focus,
|
||||
.combos-device-row select:hover,
|
||||
.combos-device-row select:focus,
|
||||
.equipment-planner-socket-links select:hover,
|
||||
.equipment-planner-socket-links select:focus,
|
||||
.equipment-planner-socket-controls select:hover,
|
||||
.equipment-planner-socket-controls select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue