From aa6f50e3c8f4b94c2a957686fe66122442a9534a Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Thu, 15 May 2025 18:38:36 +0200 Subject: [PATCH 01/13] set basis and model --- .gitignore | 3 + .mvn/maven.config | 3 + .mvn/settings-local.xml | 265 ++++++++++++++++++ agiledancers-be/pom.xml | 23 ++ .../java/nl/agilecc/agiledancers/Main.java | 17 ++ .../nl/agilecc/agiledancers/model/Coach.java | 6 + .../agiledancers/model/DanceGroup.java | 17 ++ .../nl/agilecc/agiledancers/model/Dancer.java | 7 + .../agilecc/agiledancers/model/DayPart.java | 12 + .../nl/agilecc/agiledancers/model/Show.java | 8 + .../model/specifications/DanceGroupStyle.java | 5 + .../model/specifications/DanceGroupType.java | 18 ++ .../model/specifications/DayPartType.java | 5 + pom.xml | 51 ++++ 14 files changed, 440 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/maven.config create mode 100644 .mvn/settings-local.xml create mode 100644 agiledancers-be/pom.xml create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupStyle.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupType.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DayPartType.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2e74c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.iml +.idea +target \ No newline at end of file diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 0000000..d267dac --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1,3 @@ +-s.mvn\settings-local.xml + +-Dmaven.repo.local=D:\workspace\private\.m2repo \ No newline at end of file diff --git a/.mvn/settings-local.xml b/.mvn/settings-local.xml new file mode 100644 index 0000000..24c1925 --- /dev/null +++ b/.mvn/settings-local.xml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + maven-default-http-blocker + external:http:* + Pseudo repository to mirror external repositories initially using HTTP. + http://0.0.0.0/ + true + + + + + + + + + + + + diff --git a/agiledancers-be/pom.xml b/agiledancers-be/pom.xml new file mode 100644 index 0000000..477cbba --- /dev/null +++ b/agiledancers-be/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + nl.agilecc.agiledancers + agiledancers + 1.0-SNAPSHOT + + + agiledancers-be + jar + + + + com.google.ortools + ortools-java + + + + \ No newline at end of file diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java new file mode 100644 index 0000000..00ddbb4 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java @@ -0,0 +1,17 @@ +package nl.agilecc.agiledancers; + +//TIP To Run code, press or +// click the icon in the gutter. +public class Main { + public static void main(String[] args) { + //TIP Press with your caret at the highlighted text + // to see how IntelliJ IDEA suggests fixing it. + System.out.printf("Hello and welcome!"); + + for (int i = 1; i <= 5; i++) { + //TIP Press to start debugging your code. We have set one breakpoint + // for you, but you can always add more by pressing . + System.out.println("i = " + i); + } + } +} \ No newline at end of file diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java new file mode 100644 index 0000000..f008c3c --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.model; + +public class Coach { + private Long id; + private String name; +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java new file mode 100644 index 0000000..5914190 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java @@ -0,0 +1,17 @@ +package nl.agilecc.agiledancers.model; + +import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; +import nl.agilecc.agiledancers.model.specifications.DanceGroupType; + +import java.util.Set; + +public class DanceGroup { + private Long id; + private String category; + private DanceGroupType type; + private int startAge; + private int endAge; + private Set dancers; + private Set styles; + private Coach coach; +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java new file mode 100644 index 0000000..5b3dfec --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java @@ -0,0 +1,7 @@ +package nl.agilecc.agiledancers.model; + +public class Dancer { + private Long id; + private String name; + private int age; +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java new file mode 100644 index 0000000..ba068f5 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java @@ -0,0 +1,12 @@ +package nl.agilecc.agiledancers.model; + +import nl.agilecc.agiledancers.model.specifications.DayPartType; + +import java.util.List; + +public class DayPart { + private Long id; + + private DayPartType type; + private List danceGroups; +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java new file mode 100644 index 0000000..b23a772 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java @@ -0,0 +1,8 @@ +package nl.agilecc.agiledancers.model; + +import java.util.List; + +public class Show { + private Long id; + private List dayParts; +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupStyle.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupStyle.java new file mode 100644 index 0000000..daa408c --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupStyle.java @@ -0,0 +1,5 @@ +package nl.agilecc.agiledancers.model.specifications; + +public enum DanceGroupStyle { + HIPHOP, JAZZ, MODERN, STREETDANCE, CLASSIC, BREAKDANCE +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupType.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupType.java new file mode 100644 index 0000000..0c494ae --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DanceGroupType.java @@ -0,0 +1,18 @@ +package nl.agilecc.agiledancers.model.specifications; + +import lombok.Getter; + +public enum DanceGroupType { + SHOW_CASE(3), + PRE_EDUCATION(1), // TODO check if this is one group mentioned with different docent each time, or three different groups - and thus if they are required 1 day or 3 + SELECTION(2), + REGULAR(2), + PRE_SCHOOLER(1); + + @Getter + private int dayPartsRequired; + + DanceGroupType(int dayPartsRequired) { + this.dayPartsRequired = dayPartsRequired; + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DayPartType.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DayPartType.java new file mode 100644 index 0000000..7bfdefa --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/specifications/DayPartType.java @@ -0,0 +1,5 @@ +package nl.agilecc.agiledancers.model.specifications; + +public enum DayPartType { + MORNING, AFTERNOON, EVENING; +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..eb10361 --- /dev/null +++ b/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + nl.agilecc.agiledancers + agiledancers + 1.0-SNAPSHOT + pom + + Agile Dancers + To map multiple dance groups to multiple dance performance dayparts with respect to certain constraints. + + + agiledancers-be + + + + + + 21 + 21 + UTF-8 + UTF-8 + + 9.12.4544 + 1.18.38 + + + + + + + com.google.ortools + ortools-java + ${ortools-java.version} + + + + + + + + org.projectlombok + lombok + ${lombok.version} + + + + \ No newline at end of file From e617bc09c1f71d2683a9d3a166a0fdd6856cb873 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Thu, 22 May 2025 18:42:39 +0200 Subject: [PATCH 02/13] WIP: verder uitbreiden model --- agiledancers-be/pom.xml | 13 +++++ .../java/nl/agilecc/agiledancers/Main.java | 17 ------- .../agiledancers/model/AbstractEntity.java | 30 +++++++++++ .../nl/agilecc/agiledancers/model/Coach.java | 12 +++-- .../agiledancers/model/DanceGroup.java | 41 ++++++++++++--- .../agiledancers/model/DanceSchool.java | 50 +++++++++++++++++++ .../nl/agilecc/agiledancers/model/Dancer.java | 22 ++++++-- .../agilecc/agiledancers/model/DayPart.java | 15 ++++-- .../nl/agilecc/agiledancers/model/Show.java | 12 +++-- .../repository/AbstractRepository.java | 27 ++++++++++ .../repository/CoachRepository.java | 7 +++ .../repository/DanceGroupRepository.java | 6 +++ .../repository/DanceSchoolRepository.java | 6 +++ .../repository/DancerRepository.java | 6 +++ .../repository/DayPartRepository.java | 6 +++ .../repository/ShowRepository.java | 6 +++ .../agiledancers/service/CoachService.java | 12 +++++ .../service/DanceGroupService.java | 27 ++++++++++ .../service/DanceSchoolService.java | 29 +++++++++++ .../agiledancers/service/DancerService.java | 15 ++++++ .../src/test/java/sit/DanceSchoolSit.java | 26 ++++++++++ pom.xml | 19 +++++++ 22 files changed, 366 insertions(+), 38 deletions(-) delete mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/AbstractEntity.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/CoachRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceGroupRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceSchoolRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DancerRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DayPartRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/ShowRepository.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/CoachService.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceGroupService.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceSchoolService.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DancerService.java create mode 100644 agiledancers-be/src/test/java/sit/DanceSchoolSit.java diff --git a/agiledancers-be/pom.xml b/agiledancers-be/pom.xml index 477cbba..26611a6 100644 --- a/agiledancers-be/pom.xml +++ b/agiledancers-be/pom.xml @@ -18,6 +18,19 @@ com.google.ortools ortools-java + + + + net.datafaker + datafaker + test + + + + org.junit.jupiter + junit-jupiter + test + \ No newline at end of file diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java deleted file mode 100644 index 00ddbb4..0000000 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/Main.java +++ /dev/null @@ -1,17 +0,0 @@ -package nl.agilecc.agiledancers; - -//TIP To Run code, press or -// click the icon in the gutter. -public class Main { - public static void main(String[] args) { - //TIP Press with your caret at the highlighted text - // to see how IntelliJ IDEA suggests fixing it. - System.out.printf("Hello and welcome!"); - - for (int i = 1; i <= 5; i++) { - //TIP Press to start debugging your code. We have set one breakpoint - // for you, but you can always add more by pressing . - System.out.println("i = " + i); - } - } -} \ No newline at end of file diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/AbstractEntity.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/AbstractEntity.java new file mode 100644 index 0000000..3a08a0e --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/AbstractEntity.java @@ -0,0 +1,30 @@ +package nl.agilecc.agiledancers.model; + +import lombok.*; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@Getter +@Setter +@EqualsAndHashCode +@ToString +@NoArgsConstructor +public abstract class AbstractEntity { + private Long id; + + protected void addNullSafe(Set set, T t) { + emptyIfNull(set).add(t); + } + + private Set emptyIfNull(Set set) { + return set == null ? + new HashSet<>() : + set; + } + + protected Set unmodifiableNullSafe(Set set) { + return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java index f008c3c..326b14c 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Coach.java @@ -1,6 +1,12 @@ package nl.agilecc.agiledancers.model; -public class Coach { - private Long id; - private String name; +import lombok.*; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@ToString(onlyExplicitlyIncluded = true) +public class Coach extends AbstractEntity { + @ToString.Include + private final String name; } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java index 5914190..c81a203 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java @@ -1,17 +1,46 @@ package nl.agilecc.agiledancers.model; +import lombok.*; import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; import nl.agilecc.agiledancers.model.specifications.DanceGroupType; +import java.util.Collections; +import java.util.HashSet; import java.util.Set; -public class DanceGroup { - private Long id; - private String category; - private DanceGroupType type; - private int startAge; - private int endAge; +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@ToString(onlyExplicitlyIncluded = true) +public class DanceGroup extends AbstractEntity { + @ToString.Include + private final String category; + @ToString.Include + private final DanceGroupType type; + @ToString.Include + private final int startAge; + @ToString.Include + private final int endAge; + @ToString.Include private Set dancers; + @ToString.Include private Set styles; + @ToString.Include private Coach coach; + + public Set getDancers() { + return super.unmodifiableNullSafe(dancers); + } + + public void addDancer(Dancer dancer) { + super.addNullSafe(dancers, dancer); + } + + public Set getStyles() { + return super.unmodifiableNullSafe(styles); + } + + public void addStyle(DanceGroupStyle style) { + super.addNullSafe(styles, style); + } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java new file mode 100644 index 0000000..63803d7 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java @@ -0,0 +1,50 @@ +package nl.agilecc.agiledancers.model; + +import lombok.*; + +import java.util.Collections; +import java.util.Set; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@ToString(onlyExplicitlyIncluded = true) +public class DanceSchool extends AbstractEntity { + private final String name; + private final Set dancers; + private final Set danceGroups; + private final Set shows; + private final Set coaches; + + public Set getDancers() { + return super.unmodifiableNullSafe(dancers); + } + + public Set getDanceGroups() { + return super.unmodifiableNullSafe(danceGroups); + } + + public Set getShows() { + return super.unmodifiableNullSafe(shows); + } + + public Set getCoaches() { + return super.unmodifiableNullSafe(coaches); + } + + public void addDancer(Dancer dancer) { + super.addNullSafe(dancers, dancer); + } + + public void addDanceGroup(DanceGroup danceGroup) { + super.addNullSafe(danceGroups, danceGroup); + } + + public void addShow(Show show) { + super.addNullSafe(shows, show); + } + + public void addCoach(Coach coach) { + super.addNullSafe(coaches, coach); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java index 5b3dfec..a628a44 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java @@ -1,7 +1,21 @@ package nl.agilecc.agiledancers.model; -public class Dancer { - private Long id; - private String name; - private int age; +import lombok.*; + +import java.time.LocalDate; +import java.time.Period; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@ToString(onlyExplicitlyIncluded = true) +public class Dancer extends AbstractEntity { + @ToString.Include + private final String name; + @ToString.Include + private LocalDate birthDate; + + public int age() { + return Period.between(birthDate, LocalDate.now()).getYears(); + } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java index ba068f5..6503d82 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java @@ -1,12 +1,17 @@ package nl.agilecc.agiledancers.model; +import lombok.*; import nl.agilecc.agiledancers.model.specifications.DayPartType; import java.util.List; -public class DayPart { - private Long id; - - private DayPartType type; - private List danceGroups; +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@ToString(onlyExplicitlyIncluded = true) +public class DayPart extends AbstractEntity { + @ToString.Include + private final DayPartType type; + @ToString.Include + private final List danceGroups; } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java index b23a772..de61f10 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java @@ -1,8 +1,14 @@ package nl.agilecc.agiledancers.model; +import lombok.*; + import java.util.List; -public class Show { - private Long id; - private List dayParts; +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@ToString(onlyExplicitlyIncluded = true) +public class Show extends AbstractEntity { + @ToString.Include + private final List dayParts; } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java new file mode 100644 index 0000000..0468b13 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java @@ -0,0 +1,27 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.AbstractEntity; + +import java.util.*; + +public abstract class AbstractRepository { + private long sequence = 0L; + + protected Map entities = new LinkedHashMap<>(); + + public T add(T entity) { + if (entity.getId() == null) { + entity.setId(sequence++); + } + entities.put(entity.getId(), entity); + return entity; + } + + public List findAll() { + return List.copyOf(entities.values()); + } + + public T findById(long id) { + return entities.get(id); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/CoachRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/CoachRepository.java new file mode 100644 index 0000000..2257e5c --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/CoachRepository.java @@ -0,0 +1,7 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.Coach; + +public class CoachRepository extends AbstractRepository { + +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceGroupRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceGroupRepository.java new file mode 100644 index 0000000..57f4ed2 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceGroupRepository.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.DanceGroup; + +public class DanceGroupRepository extends AbstractRepository { +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceSchoolRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceSchoolRepository.java new file mode 100644 index 0000000..de5ce27 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DanceSchoolRepository.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.DanceSchool; + +public class DanceSchoolRepository extends AbstractRepository { +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DancerRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DancerRepository.java new file mode 100644 index 0000000..98cf1f3 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DancerRepository.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.Dancer; + +public class DancerRepository extends AbstractRepository { +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DayPartRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DayPartRepository.java new file mode 100644 index 0000000..5d0a703 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/DayPartRepository.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.DayPart; + +public class DayPartRepository extends AbstractRepository { +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/ShowRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/ShowRepository.java new file mode 100644 index 0000000..4e96e32 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/ShowRepository.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.repository; + +import nl.agilecc.agiledancers.model.Show; + +public class ShowRepository extends AbstractRepository { +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/CoachService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/CoachService.java new file mode 100644 index 0000000..457ae29 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/CoachService.java @@ -0,0 +1,12 @@ +package nl.agilecc.agiledancers.service; + +import nl.agilecc.agiledancers.model.Coach; +import nl.agilecc.agiledancers.repository.CoachRepository; + +public class CoachService { + private CoachRepository coachRepository = new CoachRepository(); + + public Coach addCoach(String name, Coach coach) { + return coachRepository.add(Coach.builder().name(name).build()); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceGroupService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceGroupService.java new file mode 100644 index 0000000..a201877 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceGroupService.java @@ -0,0 +1,27 @@ +package nl.agilecc.agiledancers.service; + +import nl.agilecc.agiledancers.model.Coach; +import nl.agilecc.agiledancers.model.DanceGroup; +import nl.agilecc.agiledancers.model.Dancer; +import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; +import nl.agilecc.agiledancers.model.specifications.DanceGroupType; +import nl.agilecc.agiledancers.repository.DanceGroupRepository; + +import java.util.Set; + +public class DanceGroupService { + private final DanceGroupRepository danceGroupRepository = new DanceGroupRepository(); + + public DanceGroup add(String category, DanceGroupType type, int startAge, int endAge, Set dancers, Set styles, Coach coach) { + DanceGroup danceGroup = DanceGroup.builder() + .category(category) + .type(type) + .startAge(startAge) + .endAge(endAge) + .dancers(dancers) + .styles(styles) + .coach(coach).build(); + + return danceGroupRepository.add(danceGroup); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceSchoolService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceSchoolService.java new file mode 100644 index 0000000..a300d0d --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DanceSchoolService.java @@ -0,0 +1,29 @@ +package nl.agilecc.agiledancers.service; + +import nl.agilecc.agiledancers.model.*; +import nl.agilecc.agiledancers.repository.DanceSchoolRepository; + +public class DanceSchoolService { + private final DanceSchoolRepository danceSchoolRepository = new DanceSchoolRepository(); + + public DanceSchool add(String name) { + DanceSchool danceSchool = DanceSchool.builder().name(name).build(); + return danceSchoolRepository.add(danceSchool); + } + + public void addDancer(Long danceSchoolId, Dancer dancer) { + danceSchoolRepository.findById(danceSchoolId).addDancer(dancer); + } + + public void addDanceGroup(Long danceSchoolId, DanceGroup danceGroup) { + danceSchoolRepository.findById(danceSchoolId).addDanceGroup(danceGroup); + } + + public void addCoach(Long danceSchoolId, Coach coach) { + danceSchoolRepository.findById(danceSchoolId).addCoach(coach); + } + + public void addShow(Long danceSchoolId, Show show) { + danceSchoolRepository.findById(danceSchoolId).addShow(show); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DancerService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DancerService.java new file mode 100644 index 0000000..7898505 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/DancerService.java @@ -0,0 +1,15 @@ +package nl.agilecc.agiledancers.service; + +import nl.agilecc.agiledancers.model.Dancer; +import nl.agilecc.agiledancers.repository.DancerRepository; + +import java.time.LocalDate; + +public class DancerService { + private final DancerRepository dancerRepository = new DancerRepository(); + + public Dancer addDancer(String name, LocalDate birthDate) { + Dancer dancer = Dancer.builder().name(name).birthDate(birthDate).build(); + return dancerRepository.add(dancer); + } +} diff --git a/agiledancers-be/src/test/java/sit/DanceSchoolSit.java b/agiledancers-be/src/test/java/sit/DanceSchoolSit.java new file mode 100644 index 0000000..6d09bfa --- /dev/null +++ b/agiledancers-be/src/test/java/sit/DanceSchoolSit.java @@ -0,0 +1,26 @@ +package sit; + +import net.datafaker.Faker; +import nl.agilecc.agiledancers.model.DanceSchool; +import nl.agilecc.agiledancers.service.DanceSchoolService; +import org.junit.jupiter.api.Test; + +class DanceSchoolSit { + Faker faker = new Faker(); + int numberOfDanceGroups = 42; + + int aantalDansersTm5 = 10; + int aantalDansers6tm10 = 30; + int aantalDansers11tot14 = 70; + int aantalDansers14tm17 = 140; + int aantalDansers18Plus = 100; + + private DanceSchoolService danceSchoolService = new DanceSchoolService(); + + @Test + void testDanceSchoolModel() { + DanceSchool danceSchool = danceSchoolService.add("24 Dance Studio"); + + + } +} diff --git a/pom.xml b/pom.xml index eb10361..49e5777 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,10 @@ 9.12.4544 1.18.38 + + + 2.4.3 + 5.12.2 @@ -36,6 +40,21 @@ ortools-java ${ortools-java.version} + + + + net.datafaker + datafaker + ${datafaker.version} + test + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + From c037abbde9f8cc8129676c2f946341c9c10a72ab Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Mon, 26 Oct 2026 22:45:14 +0100 Subject: [PATCH 03/13] upgraded to Java 25 --- .mvn/maven.config | 3 +- README.md | 10 ++- .../agiledancers/model/DanceGroup.java | 2 - pom.xml | 72 +++++++++++++++++-- 4 files changed, 75 insertions(+), 12 deletions(-) diff --git a/.mvn/maven.config b/.mvn/maven.config index d267dac..f2181ac 100644 --- a/.mvn/maven.config +++ b/.mvn/maven.config @@ -1,3 +1,2 @@ --s.mvn\settings-local.xml +-s.mvn/settings-local.xml --Dmaven.repo.local=D:\workspace\private\.m2repo \ No newline at end of file diff --git a/README.md b/README.md index ade846d..f14e838 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ # agiledancers -To map dancegroups to performancedays \ No newline at end of file +To map dance groups to performance days. + +Requires JDK 25 and Maven 3.9.0 or newer. Set `JAVA_HOME` to your JDK 25 installation. + +Build and run all tests (including `*Sit` tests) from the repository root: + +```sh +mvn clean verify +``` diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java index c81a203..e47612a 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java @@ -4,8 +4,6 @@ import lombok.*; import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; import nl.agilecc.agiledancers.model.specifications.DanceGroupType; -import java.util.Collections; -import java.util.HashSet; import java.util.Set; @Getter diff --git a/pom.xml b/pom.xml index 49e5777..0060fc9 100644 --- a/pom.xml +++ b/pom.xml @@ -19,17 +19,16 @@ - 21 - 21 + 25 UTF-8 UTF-8 - 9.12.4544 - 1.18.38 + 9.15.6755 + 1.18.48 - 2.4.3 - 5.12.2 + 2.7.0 + 6.1.3 @@ -67,4 +66,63 @@ - \ No newline at end of file + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.16.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.1 + + + **/Test*.java + **/*Test.java + **/*Tests.java + **/*TestCase.java + **/*Sit.java + + + + + org.apache.maven.plugins + maven-clean-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-resources-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.1 + + + org.apache.maven.plugins + maven-install-plugin + 3.2.0 + + + org.apache.maven.plugins + maven-deploy-plugin + 3.2.0 + + + + + From 2bb3beef60ce2f0ffe6768a992d42e4f0937e4b9 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Mon, 26 Oct 2026 22:53:35 +0100 Subject: [PATCH 04/13] turned into a quarkus application --- .dockerignore | 5 +++ Dockerfile | 13 +++++++ README.md | 37 ++++++++++++++++++- agiledancers-be/pom.xml | 36 +++++++++++++++++- .../src/main/resources/application.properties | 2 + .../nl/agilecc/agiledancers/HealthTest.java | 18 +++++++++ pom.xml | 18 +++++---- 7 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 agiledancers-be/src/main/resources/application.properties create mode 100644 agiledancers-be/src/test/java/nl/agilecc/agiledancers/HealthTest.java diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..85df4cf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +**/target +.git +.idea +.mvn +*.iml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b348173 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM maven:3.9-eclipse-temurin-25 AS build +WORKDIR /workspace +COPY pom.xml ./ +COPY agiledancers-be/pom.xml agiledancers-be/pom.xml +COPY agiledancers-be/src agiledancers-be/src +RUN mvn -B -ntp clean verify + +FROM eclipse-temurin:25-jre +WORKDIR /app +COPY --from=build --chown=10001:10001 /workspace/agiledancers-be/target/quarkus-app/ ./ +USER 10001:10001 +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/quarkus-run.jar"] diff --git a/README.md b/README.md index f14e838..a48de3f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # agiledancers -To map dance groups to performance days. +To map dance groups to performance days. The backend runs on Quarkus with Java 25. Requires JDK 25 and Maven 3.9.0 or newer. Set `JAVA_HOME` to your JDK 25 installation. @@ -9,3 +9,38 @@ Build and run all tests (including `*Sit` tests) from the repository root: ```sh mvn clean verify ``` + +Start development mode with live reload from the repository root: + +```sh +mvn -pl agiledancers-be quarkus:dev +``` + +Run the packaged application after building: + +```sh +java -jar agiledancers-be/target/quarkus-app/quarkus-run.jar +``` + +Build and run a Docker container from the repository root (only Docker is required): + +```sh +docker build -t agiledancers . +docker run --rm --name agiledancers -p 8080:8080 agiledancers +``` + +The Docker build compiles the application and runs the tests in a Maven build stage, +then copies the Quarkus application into a Java 25 runtime image running as a non-root user. + +Check readiness at or overall health at +. For example: + +```sh +curl --fail http://localhost:8080/q/health/ready +``` + +Quarkus configuration can be overridden with environment variables, for example +`docker run --rm -e QUARKUS_HTTP_PORT=9090 -p 9090:9090 agiledancers`. + +The domain services and repositories retain their existing in-memory behavior; +data is lost when the process stops. Business REST endpoints have not yet been added. diff --git a/agiledancers-be/pom.xml b/agiledancers-be/pom.xml index 26611a6..3ef9b6f 100644 --- a/agiledancers-be/pom.xml +++ b/agiledancers-be/pom.xml @@ -14,12 +14,26 @@ jar + + io.quarkus + quarkus-smallrye-health + com.google.ortools ortools-java + + io.quarkus + quarkus-junit + test + + + io.rest-assured + rest-assured + test + net.datafaker datafaker @@ -33,4 +47,24 @@ - \ No newline at end of file + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + + + diff --git a/agiledancers-be/src/main/resources/application.properties b/agiledancers-be/src/main/resources/application.properties new file mode 100644 index 0000000..5265d44 --- /dev/null +++ b/agiledancers-be/src/main/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.application.name=agiledancers +quarkus.http.host=0.0.0.0 diff --git a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/HealthTest.java b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/HealthTest.java new file mode 100644 index 0000000..04130b4 --- /dev/null +++ b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/HealthTest.java @@ -0,0 +1,18 @@ +package nl.agilecc.agiledancers; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +@QuarkusTest +class HealthTest { + @Test + void applicationIsReady() { + given() + .when().get("/q/health/ready") + .then().statusCode(200) + .body("status", equalTo("UP")); + } +} diff --git a/pom.xml b/pom.xml index 0060fc9..348dee4 100644 --- a/pom.xml +++ b/pom.xml @@ -22,18 +22,25 @@ 25 UTF-8 UTF-8 + 3.40.1 9.15.6755 1.18.48 2.7.0 - 6.1.3 + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + com.google.ortools ortools-java @@ -48,12 +55,6 @@ test - - org.junit.jupiter - junit-jupiter - ${junit-jupiter.version} - test - @@ -88,6 +89,9 @@ maven-surefire-plugin 3.6.1 + + org.jboss.logmanager.LogManager + **/Test*.java **/*Test.java From 777d9f199d16c175f22ed384f29bdd9daf337454 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Tue, 27 Oct 2026 15:02:58 +0100 Subject: [PATCH 05/13] Add school management REST API and architecture checks --- CHANGELOG.md | 25 +++ README.md | 107 ++++++++++- agiledancers-be/pom.xml | 13 ++ .../agiledancers/model/DanceGroup.java | 28 ++- .../agiledancers/model/DanceSchool.java | 29 ++- .../nl/agilecc/agiledancers/model/Dancer.java | 4 +- .../nl/agilecc/agiledancers/model/Show.java | 5 + .../repository/AbstractRepository.java | 3 + .../agiledancers/rest/DanceGroupResource.java | 69 +++++++ .../rest/DanceSchoolResource.java | 52 ++++++ .../agiledancers/rest/DancerResource.java | 55 ++++++ .../rest/ManagementExceptionMapper.java | 21 +++ .../agiledancers/rest/ShowResource.java | 55 ++++++ .../rest/apimodel/DancerRequest.java | 8 + .../rest/apimodel/DancerView.java | 10 ++ .../rest/apimodel/DayPartView.java | 6 + .../agiledancers/rest/apimodel/ErrorView.java | 6 + .../rest/apimodel/GroupRequest.java | 12 ++ .../agiledancers/rest/apimodel/GroupView.java | 17 ++ .../rest/apimodel/SchoolRequest.java | 5 + .../rest/apimodel/SchoolView.java | 17 ++ .../rest/apimodel/ShowRequest.java | 7 + .../agiledancers/rest/apimodel/ShowView.java | 14 ++ .../service/ManagementException.java | 16 ++ .../service/SchoolManagementService.java | 170 ++++++++++++++++++ .../agiledancers/ArchitectureTest.java | 77 ++++++++ .../SchoolManagementResourceTest.java | 164 +++++++++++++++++ pom.xml | 7 + 28 files changed, 987 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceGroupResource.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceSchoolResource.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DancerResource.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ManagementExceptionMapper.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ShowResource.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerRequest.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerView.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DayPartView.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ErrorView.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupRequest.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupView.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolRequest.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolView.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowRequest.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowView.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ManagementException.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java create mode 100644 agiledancers-be/src/test/java/nl/agilecc/agiledancers/ArchitectureTest.java create mode 100644 agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolManagementResourceTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..605ac2a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## [1.0] - 2026-10-27 + +### Added + +- Quarkus backend running on Java 25, with health endpoints and Docker support. +- Domain model for dance schools, dancers, dance groups, coaches, shows and day parts. +- REST API to create, list, retrieve, update and delete dance schools, dancers, dance groups and shows. +- Membership management allowing a dancer to belong to multiple groups within a school. +- Shows with a name, date and three day parts: morning, afternoon and evening. +- Input validation, school-scoped resource access and cleanup of memberships when dancers are deleted. +- Shared in-memory storage for the REST API. +- Separate API model records in `rest.apimodel`. +- ArchUnit checks for package dependencies, domain independence, package cycles and API model conventions. +- HTTP integration tests for resource management, memberships, validation and school isolation. + +### Fixed + +- Adding entities to initially empty dance school and dance group collections now retains them. + +### Limitations + +- Data is lost when the application stops. +- Assigning dance groups to show day parts is not yet available. diff --git a/README.md b/README.md index a48de3f..ea024a4 100644 --- a/README.md +++ b/README.md @@ -42,5 +42,108 @@ curl --fail http://localhost:8080/q/health/ready Quarkus configuration can be overridden with environment variables, for example `docker run --rm -e QUARKUS_HTTP_PORT=9090 -p 9090:9090 agiledancers`. -The domain services and repositories retain their existing in-memory behavior; -data is lost when the process stops. Business REST endpoints have not yet been added. +The REST API uses shared in-memory storage; data is lost when the process stops. + +Architecture checks run with the normal test suite, or separately with: + +```sh +mvn -pl agiledancers-be -Dtest=ArchitectureTest test +``` + +`ArchitectureTest` guards the following packages below `nl.agilecc.agiledancers`: + +| Package | Responsibility | Allowed dependencies within the application | +| --- | --- | --- | +| `model` | Domain entities and specifications | Model | +| `service` | Application use cases and future ports | Model, repositories | +| `repository` | Current in-memory persistence | Model | +| `rest` | Future REST services | Services, model | +| `solver` | Future OR-Tools calculations | Services (ports), model | + +REST and solver packages are optional until implemented. OR-Tools dependencies +are allowed only in `solver`; JAX-RS and RESTEasy dependencies only in `rest`. +The model may depend only on itself, Java and Lombok. Package cycles and +production classes outside these packages fail the architecture tests. + +API models live in `rest.apimodel`, each in its own Java file as a public top-level +class or record. Their names end in `Request`, `Response`, `View`, `Dto` or `DTO`. +Architecture tests enforce this naming and location convention and require all +records in the REST layer to reside in `rest.apimodel`. + +The current services instantiate concrete repositories, so this is not yet strict +Onion architecture. That requires repository interfaces in the core and injected +implementations in an outer adapter. For future calculations, define a port in +`service.port` using domain types and implement it in `solver`; services must not +depend on solver implementations or expose OR-Tools types. + + +## School management REST API + +All endpoints consume and produce JSON. Resources are scoped to their dance school. +A dancer can belong to multiple dance groups within the same school. Memberships +are managed separately from group details and survive updates to dancers and groups. + +| Resource collection | Item URL | +| --- | --- | +| `/api/dance-schools` | `/api/dance-schools/{schoolId}` | +| `/api/dance-schools/{schoolId}/dancers` | `.../dancers/{dancerId}` | +| `/api/dance-schools/{schoolId}/dance-groups` | `.../dance-groups/{groupId}` | +| `/api/dance-schools/{schoolId}/shows` | `.../shows/{showId}` | + +Each collection supports `GET` (list) and `POST` (create). Each item supports `GET`, +`PUT` (replace editable details), and `DELETE`. Creation returns `201` with a +`Location` header; updates return `200`; deletion returns `204`. Invalid input +returns `400`, and missing resources or resources belonging to another school +return `404`. Client-supplied IDs are not used. + +Request bodies (dates use ISO `yyyy-MM-dd`): + +```json +{"name": "24 Dance Studio"} +``` + +```json +{"name": "Sam", "birthDate": "2010-05-12"} +``` + +```json +{ + "category": "Teens", + "type": "REGULAR", + "startAge": 12, + "endAge": 18, + "styles": ["HIPHOP", "JAZZ"] +} +``` + +```json +{"name": "Annual show", "date": "2027-06-12"} +``` + +Names and categories must be nonblank. Birth dates cannot be in the future. +Both ages must be nonnegative and `startAge <= endAge`; `styles` is required +and may be empty. Group types are `SHOW_CASE`, `PRE_EDUCATION`, `SELECTION`, +`REGULAR`, `PRE_SCHOOLER`. Styles are `HIPHOP`, `JAZZ`, `MODERN`, `STREETDANCE`, +`CLASSIC`, `BREAKDANCE`. + +School responses include `dancerIds`, `danceGroupIds`, and `showIds`; group +responses include `dancerIds`. To add or remove a dancer's group membership: + +```sh +curl -X PUT http://localhost:8080/api/dance-schools/0/dance-groups/0/dancers/0 +curl -X DELETE http://localhost:8080/api/dance-schools/0/dance-groups/0/dancers/0 +``` + +Both operations return `204` and are idempotent for existing groups and dancers. +Deleting a dancer removes all their memberships; deleting a group retains the +dancers; deleting a school also deletes its dancers, groups and shows. + +Shows have a name, a date and three automatically created day parts: `MORNING`, +`AFTERNOON`, `EVENING`. Each day part has an empty `danceGroupIds` list; assigning +groups is not exposed yet. Multiple shows per year are allowed. + +Run the HTTP integration tests with: + +```sh +mvn -pl agiledancers-be -Dtest=SchoolManagementResourceTest test +``` diff --git a/agiledancers-be/pom.xml b/agiledancers-be/pom.xml index 3ef9b6f..d6ae746 100644 --- a/agiledancers-be/pom.xml +++ b/agiledancers-be/pom.xml @@ -14,6 +14,14 @@ jar + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-hibernate-validator + io.quarkus quarkus-smallrye-health @@ -24,6 +32,11 @@ + + com.tngtech.archunit + archunit-junit5 + test + io.quarkus quarkus-junit diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java index e47612a..ad1f121 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceGroup.java @@ -5,6 +5,7 @@ import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; import nl.agilecc.agiledancers.model.specifications.DanceGroupType; import java.util.Set; +import java.util.LinkedHashSet; @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -12,17 +13,23 @@ import java.util.Set; @ToString(onlyExplicitlyIncluded = true) public class DanceGroup extends AbstractEntity { @ToString.Include - private final String category; + @Setter + private String category; @ToString.Include - private final DanceGroupType type; + @Setter + private DanceGroupType type; @ToString.Include - private final int startAge; + @Setter + private int startAge; @ToString.Include - private final int endAge; + @Setter + private int endAge; @ToString.Include - private Set dancers; + @Builder.Default + private final Set dancers = new LinkedHashSet<>(); @ToString.Include - private Set styles; + @Builder.Default + private final Set styles = new LinkedHashSet<>(); @ToString.Include private Coach coach; @@ -41,4 +48,13 @@ public class DanceGroup extends AbstractEntity { public void addStyle(DanceGroupStyle style) { super.addNullSafe(styles, style); } + + public void removeDancer(Dancer dancer) { + dancers.remove(dancer); + } + + public void replaceStyles(Set styles) { + this.styles.clear(); + this.styles.addAll(styles); + } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java index 63803d7..b5b5dae 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java @@ -2,19 +2,24 @@ package nl.agilecc.agiledancers.model; import lombok.*; -import java.util.Collections; import java.util.Set; +import java.util.LinkedHashSet; @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder @ToString(onlyExplicitlyIncluded = true) public class DanceSchool extends AbstractEntity { - private final String name; - private final Set dancers; - private final Set danceGroups; - private final Set shows; - private final Set coaches; + @Setter + private String name; + @Builder.Default + private final Set dancers = new LinkedHashSet<>(); + @Builder.Default + private final Set danceGroups = new LinkedHashSet<>(); + @Builder.Default + private final Set shows = new LinkedHashSet<>(); + @Builder.Default + private final Set coaches = new LinkedHashSet<>(); public Set getDancers() { return super.unmodifiableNullSafe(dancers); @@ -47,4 +52,16 @@ public class DanceSchool extends AbstractEntity { public void addCoach(Coach coach) { super.addNullSafe(coaches, coach); } + + public void removeDancer(Dancer entity) { + dancers.remove(entity); + } + + public void removeDanceGroup(DanceGroup entity) { + danceGroups.remove(entity); + } + + public void removeShow(Show entity) { + shows.remove(entity); + } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java index a628a44..b3c3ece 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Dancer.java @@ -11,8 +11,10 @@ import java.time.Period; @ToString(onlyExplicitlyIncluded = true) public class Dancer extends AbstractEntity { @ToString.Include - private final String name; + @Setter + private String name; @ToString.Include + @Setter private LocalDate birthDate; public int age() { diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java index de61f10..59cac67 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/Show.java @@ -3,12 +3,17 @@ package nl.agilecc.agiledancers.model; import lombok.*; import java.util.List; +import java.time.LocalDate; @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @Builder @ToString(onlyExplicitlyIncluded = true) public class Show extends AbstractEntity { + @Setter + private String name; + @Setter + private LocalDate date; @ToString.Include private final List dayParts; } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java index 0468b13..bd16561 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java @@ -24,4 +24,7 @@ public abstract class AbstractRepository { public T findById(long id) { return entities.get(id); } + public void delete(long id) { + entities.remove(id); + } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceGroupResource.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceGroupResource.java new file mode 100644 index 0000000..4b737b6 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceGroupResource.java @@ -0,0 +1,69 @@ +package nl.agilecc.agiledancers.rest; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.*; +import nl.agilecc.agiledancers.service.SchoolManagementService; + +import java.util.List; + +import nl.agilecc.agiledancers.rest.apimodel.GroupRequest; +import nl.agilecc.agiledancers.rest.apimodel.GroupView; + +@Path("/api/dance-schools/{schoolId}/dance-groups") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class DanceGroupResource { + @Inject + SchoolManagementService service; + + @PathParam("schoolId") + long schoolId; + + @GET + public List list() { + return service.groups(schoolId).stream().map(GroupView::of).toList(); + } + + @GET + @Path("/{id}") + public GroupView get(@PathParam("id") long id) { + return GroupView.of(service.group(schoolId, id)); + } + + @POST + public Response create(@NotNull @Valid GroupRequest request, @Context UriInfo uriInfo) { + var entity = service.createGroup(schoolId, request.category(), request.type(), request.startAge(), request.endAge(), request.styles()); + return Response.created(uriInfo.getAbsolutePathBuilder().path(entity.getId().toString()).build()) + .entity(GroupView.of(entity)).build(); + } + + @PUT + @Path("/{id}") + public GroupView update(@PathParam("id") long id, @NotNull @Valid GroupRequest request) { + return GroupView.of(service.updateGroup(schoolId, id, request.category(), request.type(), request.startAge(), request.endAge(), request.styles())); + } + + @DELETE + @Path("/{id}") + public Response delete(@PathParam("id") long id) { + service.deleteGroup(schoolId, id); + return Response.noContent().build(); + } + + @PUT + @Path("/{id}/dancers/{dancerId}") + public Response addDancer(@PathParam("id") long id, @PathParam("dancerId") long dancerId) { + service.addGroupDancer(schoolId, id, dancerId); + return Response.noContent().build(); + } + + @DELETE + @Path("/{id}/dancers/{dancerId}") + public Response removeDancer(@PathParam("id") long id, @PathParam("dancerId") long dancerId) { + service.removeGroupDancer(schoolId, id, dancerId); + return Response.noContent().build(); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceSchoolResource.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceSchoolResource.java new file mode 100644 index 0000000..ce9f9d0 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DanceSchoolResource.java @@ -0,0 +1,52 @@ +package nl.agilecc.agiledancers.rest; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.*; +import nl.agilecc.agiledancers.service.SchoolManagementService; + +import java.util.List; + +import nl.agilecc.agiledancers.rest.apimodel.SchoolRequest; +import nl.agilecc.agiledancers.rest.apimodel.SchoolView; + +@Path("/api/dance-schools") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class DanceSchoolResource { + @Inject + SchoolManagementService service; + + @GET + public List list() { + return service.schools().stream().map(SchoolView::of).toList(); + } + + @GET + @Path("/{id}") + public SchoolView get(@PathParam("id") long id) { + return SchoolView.of(service.school(id)); + } + + @POST + public Response create(@NotNull @Valid SchoolRequest request, @Context UriInfo uriInfo) { + var entity = service.createSchool(request.name()); + return Response.created(uriInfo.getAbsolutePathBuilder().path(entity.getId().toString()).build()) + .entity(SchoolView.of(entity)).build(); + } + + @PUT + @Path("/{id}") + public SchoolView update(@PathParam("id") long id, @NotNull @Valid SchoolRequest request) { + return SchoolView.of(service.updateSchool(id, request.name())); + } + + @DELETE + @Path("/{id}") + public Response delete(@PathParam("id") long id) { + service.deleteSchool(id); + return Response.noContent().build(); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DancerResource.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DancerResource.java new file mode 100644 index 0000000..4872d17 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/DancerResource.java @@ -0,0 +1,55 @@ +package nl.agilecc.agiledancers.rest; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.*; +import nl.agilecc.agiledancers.service.SchoolManagementService; + +import java.util.List; + +import nl.agilecc.agiledancers.rest.apimodel.DancerRequest; +import nl.agilecc.agiledancers.rest.apimodel.DancerView; + +@Path("/api/dance-schools/{schoolId}/dancers") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class DancerResource { + @Inject + SchoolManagementService service; + + @PathParam("schoolId") + long schoolId; + + @GET + public List list() { + return service.dancers(schoolId).stream().map(DancerView::of).toList(); + } + + @GET + @Path("/{id}") + public DancerView get(@PathParam("id") long id) { + return DancerView.of(service.dancer(schoolId, id)); + } + + @POST + public Response create(@NotNull @Valid DancerRequest request, @Context UriInfo uriInfo) { + var entity = service.createDancer(schoolId, request.name(), request.birthDate()); + return Response.created(uriInfo.getAbsolutePathBuilder().path(entity.getId().toString()).build()) + .entity(DancerView.of(entity)).build(); + } + + @PUT + @Path("/{id}") + public DancerView update(@PathParam("id") long id, @NotNull @Valid DancerRequest request) { + return DancerView.of(service.updateDancer(schoolId, id, request.name(), request.birthDate())); + } + + @DELETE + @Path("/{id}") + public Response delete(@PathParam("id") long id) { + service.deleteDancer(schoolId, id); + return Response.noContent().build(); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ManagementExceptionMapper.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ManagementExceptionMapper.java new file mode 100644 index 0000000..07d52e0 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ManagementExceptionMapper.java @@ -0,0 +1,21 @@ +package nl.agilecc.agiledancers.rest; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import nl.agilecc.agiledancers.service.ManagementException; +import nl.agilecc.agiledancers.rest.apimodel.ErrorView; + +@Provider +public class ManagementExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ManagementException exception) { + int status = switch (exception.getKind()) { + case NOT_FOUND -> 404; + case INVALID_INPUT -> 400; + }; + return Response.status(status).type(MediaType.APPLICATION_JSON_TYPE) + .entity(new ErrorView(exception.getMessage())).build(); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ShowResource.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ShowResource.java new file mode 100644 index 0000000..22208d6 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/ShowResource.java @@ -0,0 +1,55 @@ +package nl.agilecc.agiledancers.rest; + +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.*; +import nl.agilecc.agiledancers.service.SchoolManagementService; + +import java.util.List; + +import nl.agilecc.agiledancers.rest.apimodel.ShowRequest; +import nl.agilecc.agiledancers.rest.apimodel.ShowView; + +@Path("/api/dance-schools/{schoolId}/shows") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ShowResource { + @Inject + SchoolManagementService service; + + @PathParam("schoolId") + long schoolId; + + @GET + public List list() { + return service.shows(schoolId).stream().map(ShowView::of).toList(); + } + + @GET + @Path("/{id}") + public ShowView get(@PathParam("id") long id) { + return ShowView.of(service.show(schoolId, id)); + } + + @POST + public Response create(@NotNull @Valid ShowRequest request, @Context UriInfo uriInfo) { + var entity = service.createShow(schoolId, request.name(), request.date()); + return Response.created(uriInfo.getAbsolutePathBuilder().path(entity.getId().toString()).build()) + .entity(ShowView.of(entity)).build(); + } + + @PUT + @Path("/{id}") + public ShowView update(@PathParam("id") long id, @NotNull @Valid ShowRequest request) { + return ShowView.of(service.updateShow(schoolId, id, request.name(), request.date())); + } + + @DELETE + @Path("/{id}") + public Response delete(@PathParam("id") long id) { + service.deleteShow(schoolId, id); + return Response.noContent().build(); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerRequest.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerRequest.java new file mode 100644 index 0000000..501d2c1 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerRequest.java @@ -0,0 +1,8 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PastOrPresent; +import java.time.LocalDate; + +public record DancerRequest(@NotBlank String name, @NotNull @PastOrPresent LocalDate birthDate) {} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerView.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerView.java new file mode 100644 index 0000000..0939305 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DancerView.java @@ -0,0 +1,10 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import nl.agilecc.agiledancers.model.Dancer; +import java.time.LocalDate; + +public record DancerView(long id, String name, LocalDate birthDate) { + public static DancerView of(Dancer dancer) { + return new DancerView(dancer.getId(), dancer.getName(), dancer.getBirthDate()); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DayPartView.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DayPartView.java new file mode 100644 index 0000000..6c93251 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/DayPartView.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import nl.agilecc.agiledancers.model.specifications.DayPartType; +import java.util.List; + +public record DayPartView(DayPartType type, List danceGroupIds) {} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ErrorView.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ErrorView.java new file mode 100644 index 0000000..2a0b921 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ErrorView.java @@ -0,0 +1,6 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public record ErrorView(String message) {} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupRequest.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupRequest.java new file mode 100644 index 0000000..0a0abbb --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupRequest.java @@ -0,0 +1,12 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import nl.agilecc.agiledancers.model.specifications.DanceGroupType; +import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; +import java.util.Set; + +public record GroupRequest(@NotBlank String category, @NotNull DanceGroupType type, + @NotNull @Min(0) Integer startAge, @NotNull @Min(0) Integer endAge, + @NotNull Set<@NotNull DanceGroupStyle> styles) {} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupView.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupView.java new file mode 100644 index 0000000..abe79d0 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/GroupView.java @@ -0,0 +1,17 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import nl.agilecc.agiledancers.model.DanceGroup; +import nl.agilecc.agiledancers.model.Dancer; +import nl.agilecc.agiledancers.model.specifications.DanceGroupType; +import nl.agilecc.agiledancers.model.specifications.DanceGroupStyle; +import java.util.List; +import java.util.Set; + +public record GroupView(long id, String category, DanceGroupType type, int startAge, int endAge, + Set styles, List dancerIds) { + public static GroupView of(DanceGroup group) { + return new GroupView(group.getId(), group.getCategory(), group.getType(), group.getStartAge(), + group.getEndAge(), Set.copyOf(group.getStyles()), + group.getDancers().stream().map(Dancer::getId).toList()); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolRequest.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolRequest.java new file mode 100644 index 0000000..b93d02e --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolRequest.java @@ -0,0 +1,5 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import jakarta.validation.constraints.NotBlank; + +public record SchoolRequest(@NotBlank String name) {} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolView.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolView.java new file mode 100644 index 0000000..c677b24 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/SchoolView.java @@ -0,0 +1,17 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import nl.agilecc.agiledancers.model.DanceSchool; +import nl.agilecc.agiledancers.model.Dancer; +import nl.agilecc.agiledancers.model.DanceGroup; +import nl.agilecc.agiledancers.model.Show; +import java.util.List; + +public record SchoolView(long id, String name, List dancerIds, + List danceGroupIds, List showIds) { + public static SchoolView of(DanceSchool school) { + return new SchoolView(school.getId(), school.getName(), + school.getDancers().stream().map(Dancer::getId).toList(), + school.getDanceGroups().stream().map(DanceGroup::getId).toList(), + school.getShows().stream().map(Show::getId).toList()); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowRequest.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowRequest.java new file mode 100644 index 0000000..ef921ef --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowRequest.java @@ -0,0 +1,7 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +public record ShowRequest(@NotBlank String name, @NotNull LocalDate date) {} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowView.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowView.java new file mode 100644 index 0000000..fde7d1a --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/apimodel/ShowView.java @@ -0,0 +1,14 @@ +package nl.agilecc.agiledancers.rest.apimodel; + +import nl.agilecc.agiledancers.model.Show; +import nl.agilecc.agiledancers.model.DanceGroup; +import java.time.LocalDate; +import java.util.List; + +public record ShowView(long id, String name, LocalDate date, List dayParts) { + public static ShowView of(Show show) { + return new ShowView(show.getId(), show.getName(), show.getDate(), show.getDayParts().stream() + .map(dayPart -> new DayPartView(dayPart.getType(), dayPart.getDanceGroups().stream() + .map(DanceGroup::getId).toList())).toList()); + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ManagementException.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ManagementException.java new file mode 100644 index 0000000..cecedc7 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/ManagementException.java @@ -0,0 +1,16 @@ +package nl.agilecc.agiledancers.service; + +public class ManagementException extends RuntimeException { + public enum Kind { NOT_FOUND, INVALID_INPUT } + + private final Kind kind; + + public ManagementException(Kind kind, String message) { + super(message); + this.kind = kind; + } + + public Kind getKind() { + return kind; + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java new file mode 100644 index 0000000..1dd8a17 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java @@ -0,0 +1,170 @@ +package nl.agilecc.agiledancers.service; + +import jakarta.inject.Singleton; +import nl.agilecc.agiledancers.model.*; +import nl.agilecc.agiledancers.model.specifications.*; +import nl.agilecc.agiledancers.repository.*; + +import java.time.LocalDate; +import java.util.*; + +import static nl.agilecc.agiledancers.service.ManagementException.Kind.*; + +/** Owns the in-memory school aggregates; all writes go through this shared service. */ +@Singleton +public class SchoolManagementService { + private final DanceSchoolRepository schools = new DanceSchoolRepository(); + private final DancerRepository dancers = new DancerRepository(); + private final DanceGroupRepository groups = new DanceGroupRepository(); + private final ShowRepository shows = new ShowRepository(); + + public synchronized List schools() { + return schools.findAll(); + } + + public synchronized DanceSchool school(long schoolId) { + return required(schools.findById(schoolId), "Dance school", schoolId); + } + + public synchronized DanceSchool createSchool(String name) { + return schools.add(DanceSchool.builder().name(name).build()); + } + + public synchronized DanceSchool updateSchool(long schoolId, String name) { + DanceSchool school = school(schoolId); + school.setName(name); + return school; + } + + public synchronized void deleteSchool(long schoolId) { + DanceSchool school = school(schoolId); + school.getDanceGroups().forEach(group -> groups.delete(group.getId())); + school.getDancers().forEach(dancer -> dancers.delete(dancer.getId())); + school.getShows().forEach(show -> shows.delete(show.getId())); + schools.delete(schoolId); + } + + public synchronized List dancers(long schoolId) { + return List.copyOf(school(schoolId).getDancers()); + } + + public synchronized Dancer dancer(long schoolId, long dancerId) { + return member(school(schoolId).getDancers(), dancerId, "Dancer"); + } + + public synchronized Dancer createDancer(long schoolId, String name, LocalDate birthDate) { + DanceSchool school = school(schoolId); + Dancer dancer = dancers.add(Dancer.builder().name(name).birthDate(birthDate).build()); + school.addDancer(dancer); + return dancer; + } + + public synchronized Dancer updateDancer(long schoolId, long dancerId, String name, LocalDate birthDate) { + Dancer dancer = dancer(schoolId, dancerId); + dancer.setName(name); + dancer.setBirthDate(birthDate); + return dancer; + } + + public synchronized void deleteDancer(long schoolId, long dancerId) { + DanceSchool school = school(schoolId); + Dancer dancer = dancer(schoolId, dancerId); + school.getDanceGroups().forEach(group -> group.removeDancer(dancer)); + school.removeDancer(dancer); + dancers.delete(dancerId); + } + + public synchronized List groups(long schoolId) { + return List.copyOf(school(schoolId).getDanceGroups()); + } + + public synchronized DanceGroup group(long schoolId, long groupId) { + return member(school(schoolId).getDanceGroups(), groupId, "Dance group"); + } + + public synchronized DanceGroup createGroup(long schoolId, String category, DanceGroupType type, + int startAge, int endAge, Set styles) { + DanceSchool school = school(schoolId); + validateAgeRange(startAge, endAge); + DanceGroup group = groups.add(DanceGroup.builder().category(category).type(type) + .startAge(startAge).endAge(endAge).styles(new LinkedHashSet<>(styles)).build()); + school.addDanceGroup(group); + return group; + } + + public synchronized DanceGroup updateGroup(long schoolId, long groupId, String category, DanceGroupType type, + int startAge, int endAge, Set styles) { + DanceGroup group = group(schoolId, groupId); + validateAgeRange(startAge, endAge); + group.setCategory(category); + group.setType(type); + group.setStartAge(startAge); + group.setEndAge(endAge); + group.replaceStyles(styles); + return group; + } + + public synchronized void deleteGroup(long schoolId, long groupId) { + DanceGroup group = group(schoolId, groupId); + school(schoolId).removeDanceGroup(group); + groups.delete(groupId); + } + + public synchronized void addGroupDancer(long schoolId, long groupId, long dancerId) { + DanceGroup group = group(schoolId, groupId); + group.addDancer(dancer(schoolId, dancerId)); + } + + public synchronized void removeGroupDancer(long schoolId, long groupId, long dancerId) { + DanceGroup group = group(schoolId, groupId); + group.removeDancer(dancer(schoolId, dancerId)); + } + + public synchronized List shows(long schoolId) { + return List.copyOf(school(schoolId).getShows()); + } + + public synchronized Show show(long schoolId, long showId) { + return member(school(schoolId).getShows(), showId, "Show"); + } + + public synchronized Show createShow(long schoolId, String name, LocalDate date) { + DanceSchool school = school(schoolId); + List dayParts = Arrays.stream(DayPartType.values()) + .map(type -> DayPart.builder().type(type).danceGroups(List.of()).build()).toList(); + Show show = shows.add(Show.builder().name(name).date(date).dayParts(dayParts).build()); + school.addShow(show); + return show; + } + + public synchronized Show updateShow(long schoolId, long showId, String name, LocalDate date) { + Show show = show(schoolId, showId); + show.setName(name); + show.setDate(date); + return show; + } + + public synchronized void deleteShow(long schoolId, long showId) { + Show show = show(schoolId, showId); + school(schoolId).removeShow(show); + shows.delete(showId); + } + + private static void validateAgeRange(int startAge, int endAge) { + if (startAge > endAge) { + throw new ManagementException(INVALID_INPUT, "startAge must not exceed endAge"); + } + } + + private static T member(Collection entities, long id, String type) { + return entities.stream().filter(entity -> entity.getId() == id).findFirst() + .orElseThrow(() -> new ManagementException(NOT_FOUND, type + " " + id + " not found in this school")); + } + + private static T required(T value, String type, long id) { + if (value == null) { + throw new ManagementException(NOT_FOUND, type + " " + id + " not found"); + } + return value; + } +} diff --git a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/ArchitectureTest.java b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/ArchitectureTest.java new file mode 100644 index 0000000..e5afd7f --- /dev/null +++ b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/ArchitectureTest.java @@ -0,0 +1,77 @@ +package nl.agilecc.agiledancers; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; +import static com.tngtech.archunit.library.Architectures.layeredArchitecture; +import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices; + +@AnalyzeClasses(packages = "nl.agilecc.agiledancers", importOptions = ImportOption.DoNotIncludeTests.class) +class ArchitectureTest { + private static final String ROOT = "nl.agilecc.agiledancers"; + private static final String API_MODEL = ROOT + ".rest.apimodel"; + + // API model classes and records use the Request, Response, View, Dto or DTO suffix. + @ArchTest + static final ArchRule api_models_reside_in_the_api_model_package = classes() + .that().haveNameMatching(".*(Request|Response|View|Dto|DTO)$") + .should().resideInAPackage(API_MODEL); + + @ArchTest + static final ArchRule rest_records_reside_in_the_api_model_package = classes() + .that().resideInAPackage(ROOT + ".rest..") + .and().areRecords() + .should().resideInAPackage(API_MODEL); + + @ArchTest + static final ArchRule api_models_are_public_top_level_types = classes() + .that().resideInAPackage(API_MODEL + "..") + .should().bePublic() + .andShould().beTopLevelClasses() + .andShould().haveNameMatching(".*(Request|Response|View|Dto|DTO)$"); + + // The existing services depend on concrete repositories. This rule preserves that + // arrangement until repository ports and dependency injection are introduced. + @ArchTest + static final ArchRule layer_dependencies = layeredArchitecture() + .consideringOnlyDependenciesInLayers() + .layer("Model").definedBy(ROOT + ".model..") + .layer("Services").definedBy(ROOT + ".service..") + .layer("Repositories").definedBy(ROOT + ".repository..") + .optionalLayer("REST").definedBy(ROOT + ".rest..") + .optionalLayer("Solver").definedBy(ROOT + ".solver..") + .whereLayer("Model").mayNotAccessAnyLayer() + .whereLayer("Services").mayOnlyAccessLayers("Model", "Repositories") + .whereLayer("Repositories").mayOnlyAccessLayers("Model") + .whereLayer("REST").mayOnlyAccessLayers("Services", "Model") + .whereLayer("Solver").mayOnlyAccessLayers("Services", "Model"); + + @ArchTest + static final ArchRule production_classes_have_an_architectural_home = classes() + .should().resideInAnyPackage(ROOT + ".model..", ROOT + ".service..", + ROOT + ".repository..", ROOT + ".rest..", ROOT + ".solver.."); + + @ArchTest + static final ArchRule model_is_independent_of_frameworks = classes() + .that().resideInAPackage(ROOT + ".model..") + .should().onlyDependOnClassesThat() + .resideInAnyPackage(ROOT + ".model..", "java..", "lombok.."); + + @ArchTest + static final ArchRule ortools_is_encapsulated_in_solver = noClasses() + .that().resideOutsideOfPackage(ROOT + ".solver..") + .should().dependOnClassesThat().resideInAPackage("com.google.ortools.."); + + @ArchTest + static final ArchRule http_api_is_encapsulated_in_rest = noClasses() + .that().resideOutsideOfPackage(ROOT + ".rest..") + .should().dependOnClassesThat().resideInAnyPackage("jakarta.ws.rs..", "org.jboss.resteasy.."); + + @ArchTest + static final ArchRule packages_are_free_of_cycles = slices() + .matching(ROOT + ".(*)..").should().beFreeOfCycles(); +} diff --git a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolManagementResourceTest.java b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolManagementResourceTest.java new file mode 100644 index 0000000..eb815c8 --- /dev/null +++ b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolManagementResourceTest.java @@ -0,0 +1,164 @@ +package nl.agilecc.agiledancers; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.ValidatableResponse; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +class SchoolManagementResourceTest { + private static final String SCHOOLS = "/api/dance-schools"; + private static final Map DANCER = Map.of("name", "Sam", "birthDate", "2010-05-12"); + private static final Map GROUP = Map.of("category", "Teens", "type", "REGULAR", + "startAge", 12, "endAge", 18, "styles", new String[]{"HIPHOP"}); + private static final Map SHOW = Map.of("name", "Annual show", "date", "2027-06-12"); + + @Test + void managesSchoolAndItsResources() { + long schoolId = create(SCHOOLS, Map.of("name", "Dance Studio")); + String school = SCHOOLS + "/" + schoolId; + try { + given().get(school).then().statusCode(200).body("name", equalTo("Dance Studio")); + given().get(SCHOOLS).then().statusCode(200).body("id", hasItem((int) schoolId)); + put(school, Map.of("name", "Renamed studio")).statusCode(200).body("name", equalTo("Renamed studio")); + + long dancerId = create(school + "/dancers", DANCER); + String dancer = school + "/dancers/" + dancerId; + put(dancer, Map.of("name", "Sam updated", "birthDate", "2011-06-13")) + .statusCode(200).body("birthDate", equalTo("2011-06-13")); + given().get(dancer).then().statusCode(200).body("name", equalTo("Sam updated")); + given().get(school + "/dancers").then().statusCode(200).body("id", contains((int) dancerId)); + + long groupId = create(school + "/dance-groups", GROUP); + String group = school + "/dance-groups/" + groupId; + given().put(group + "/dancers/" + dancerId).then().statusCode(204); + put(group, Map.of("category", "Selection", "type", "SELECTION", "startAge", 10, + "endAge", 20, "styles", new String[]{"JAZZ", "MODERN"})) + .statusCode(200).body("category", equalTo("Selection")) + .body("dancerIds", contains((int) dancerId)).body("styles", containsInAnyOrder("JAZZ", "MODERN")); + given().get(group).then().statusCode(200).body("type", equalTo("SELECTION")); + given().get(school + "/dance-groups").then().statusCode(200).body("id", contains((int) groupId)); + + long showId = create(school + "/shows", SHOW); + String show = school + "/shows/" + showId; + given().get(show).then().statusCode(200) + .body("dayParts.type", contains("MORNING", "AFTERNOON", "EVENING")) + .body("dayParts.danceGroupIds", everyItem(empty())); + put(show, Map.of("name", "Summer show", "date", "2027-07-01")) + .statusCode(200).body("date", equalTo("2027-07-01")); + given().get(school + "/shows").then().statusCode(200).body("id", contains((int) showId)); + given().get(school).then().statusCode(200).body("dancerIds", contains((int) dancerId)) + .body("danceGroupIds", contains((int) groupId)).body("showIds", contains((int) showId)); + + for (String path : new String[]{dancer, group, show}) { + given().delete(path).then().statusCode(204); + given().get(path).then().statusCode(404); + given().delete(path).then().statusCode(404); + } + given().get(school).then().body("dancerIds", empty()).body("danceGroupIds", empty()).body("showIds", empty()); + } finally { + given().delete(school).then().statusCode(204); + } + given().get(school).then().statusCode(404); + } + + @Test + void dancerCanBelongToMultipleGroupsAndDeletingDancerRemovesMemberships() { + String school = SCHOOLS + "/" + create(SCHOOLS, Map.of("name", "Membership school")); + try { + long dancerId = create(school + "/dancers", DANCER); + String first = school + "/dance-groups/" + create(school + "/dance-groups", GROUP); + String second = school + "/dance-groups/" + create(school + "/dance-groups", GROUP); + for (String group : new String[]{first, second}) { + given().put(group + "/dancers/" + dancerId).then().statusCode(204); + given().put(group + "/dancers/" + dancerId).then().statusCode(204); + given().get(group).then().body("dancerIds", contains((int) dancerId)); + } + given().delete(first + "/dancers/" + dancerId).then().statusCode(204); + given().delete(first + "/dancers/" + dancerId).then().statusCode(204); + given().get(first).then().body("dancerIds", empty()); + given().get(second).then().body("dancerIds", contains((int) dancerId)); + given().put(first + "/dancers/" + dancerId).then().statusCode(204); + given().delete(school + "/dancers/" + dancerId).then().statusCode(204); + given().get(first).then().body("dancerIds", empty()); + given().get(second).then().body("dancerIds", empty()); + } finally { + given().delete(school).then().statusCode(204); + } + } + + @Test + void isolatesSchoolsAndCascadesSchoolDeletion() { + String first = SCHOOLS + "/" + create(SCHOOLS, Map.of("name", "First")); + String second = SCHOOLS + "/" + create(SCHOOLS, Map.of("name", "Second")); + try { + long dancer = create(first + "/dancers", DANCER); + long group = create(first + "/dance-groups", GROUP); + long show = create(first + "/shows", SHOW); + long otherGroup = create(second + "/dance-groups", GROUP); + for (String path : new String[]{"/dancers/" + dancer, "/dance-groups/" + group, "/shows/" + show}) { + given().get(second + path).then().statusCode(404); + given().delete(second + path).then().statusCode(404); + } + put(second + "/dancers/" + dancer, DANCER).statusCode(404); + put(second + "/dance-groups/" + group, GROUP).statusCode(404); + put(second + "/shows/" + show, SHOW).statusCode(404); + given().put(second + "/dance-groups/" + otherGroup + "/dancers/" + dancer).then().statusCode(404); + given().get(second + "/dance-groups/" + otherGroup).then().body("dancerIds", empty()); + given().delete(first).then().statusCode(204); + given().get(first + "/dancers").then().statusCode(404); + given().get(first + "/dance-groups").then().statusCode(404); + given().get(first + "/shows").then().statusCode(404); + given().get(second).then().statusCode(200); + } finally { + given().delete(first); + given().delete(second).then().statusCode(204); + } + } + + @Test + void rejectsInvalidInputWithoutChangingStoredData() { + given().contentType("application/json").body("{}").post(SCHOOLS).then().statusCode(400); + given().contentType("application/json").body("null").post(SCHOOLS).then().statusCode(400); + String school = SCHOOLS + "/" + create(SCHOOLS, Map.of("name", "Validation school")); + try { + put(school, Map.of("name", " ")).statusCode(400); + given().get(school).then().body("name", equalTo("Validation school")); + for (Object body : new Object[]{Map.of("name", "Sam"), Map.of("name", "Sam", "birthDate", "2999-01-01"), + Map.of("name", "Sam", "birthDate", "not-a-date")}) { + given().contentType("application/json").body(body).post(school + "/dancers").then().statusCode(400); + } + for (String body : new String[]{"{}", "{\"category\":\"Teens\",\"type\":\"INVALID\"}", + "{\"category\":\"Teens\",\"type\":\"REGULAR\",\"startAge\":-1,\"endAge\":18,\"styles\":[]}", + "{\"category\":\"Teens\",\"type\":\"REGULAR\",\"startAge\":18,\"endAge\":12,\"styles\":[]}"}) { + given().contentType("application/json").body(body).post(school + "/dance-groups").then().statusCode(400); + } + long group = create(school + "/dance-groups", GROUP); + put(school + "/dance-groups/" + group, Map.of("category", "Invalid", "type", "REGULAR", + "startAge", 20, "endAge", 10, "styles", new String[]{})).statusCode(400); + given().get(school + "/dance-groups/" + group).then().body("category", equalTo("Teens")); + given().contentType("application/json").body(Map.of("name", "Show")) + .post(school + "/shows").then().statusCode(400); + given().get(school + "/dancers").then().body("$", empty()); + given().get(school + "/shows").then().body("$", empty()); + given().contentType("application/json").body(DANCER) + .post(SCHOOLS + "/9223372036854775807/dancers").then().statusCode(404); + } finally { + given().delete(school).then().statusCode(204); + } + } + + private static long create(String path, Object body) { + return given().contentType("application/json").body(body).post(path).then() + .statusCode(201).header("Location", matchesPattern(".*/[0-9]+$")) + .extract().jsonPath().getLong("id"); + } + + private static ValidatableResponse put(String path, Object body) { + return given().contentType("application/json").body(body).put(path).then(); + } +} diff --git a/pom.xml b/pom.xml index 348dee4..bdd4768 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ 1.18.48 + 1.5.1 2.7.0 @@ -48,6 +49,12 @@ + + com.tngtech.archunit + archunit-junit5 + ${archunit.version} + test + net.datafaker datafaker From 02337fafaa2eedaa780389a61eb501ff709f22d5 Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Tue, 27 Oct 2026 15:34:56 +0100 Subject: [PATCH 06/13] frontend-toegevoegd --- CHANGELOG.md | 8 + README.md | 21 + agiledancers-fe/.editorconfig | 17 + agiledancers-fe/.gitignore | 48 + agiledancers-fe/.nvmrc | 1 + agiledancers-fe/.prettierrc | 12 + agiledancers-fe/.vscode/extensions.json | 4 + agiledancers-fe/.vscode/launch.json | 20 + agiledancers-fe/.vscode/tasks.json | 42 + agiledancers-fe/README.md | 86 + agiledancers-fe/angular.json | 72 + agiledancers-fe/e2e/school-management.spec.ts | 162 + agiledancers-fe/package-lock.json | 7015 +++++++++++++++++ agiledancers-fe/package.json | 36 + agiledancers-fe/playwright.config.ts | 26 + agiledancers-fe/proxy.conf.json | 7 + agiledancers-fe/public/favicon.svg | 1 + agiledancers-fe/src/app/app.config.ts | 17 + agiledancers-fe/src/app/app.css | 239 + agiledancers-fe/src/app/app.html | 46 + agiledancers-fe/src/app/app.routes.ts | 21 + agiledancers-fe/src/app/app.spec.ts | 19 + agiledancers-fe/src/app/app.ts | 10 + agiledancers-fe/src/app/core/api-error.ts | 24 + agiledancers-fe/src/app/core/api-models.ts | 74 + .../src/app/core/school-api.spec.ts | 69 + agiledancers-fe/src/app/core/school-api.ts | 63 + .../src/app/schools/school-page.html | 423 + .../src/app/schools/school-page.ts | 162 + .../src/app/schools/schools-page.html | 93 + .../src/app/schools/schools-page.ts | 55 + .../src/app/shared/confirm-dialog.ts | 40 + .../src/app/shared/dialog-frame.ts | 50 + .../src/app/shared/entity-editor.spec.ts | 69 + .../src/app/shared/entity-editor.ts | 222 + .../src/app/shared/group-members.spec.ts | 76 + .../src/app/shared/group-members.ts | 98 + agiledancers-fe/src/index.html | 13 + agiledancers-fe/src/main.ts | 5 + agiledancers-fe/src/styles.css | 1023 +++ agiledancers-fe/tsconfig.app.json | 14 + agiledancers-fe/tsconfig.json | 31 + agiledancers-fe/tsconfig.spec.json | 14 + 43 files changed, 10548 insertions(+) create mode 100644 agiledancers-fe/.editorconfig create mode 100644 agiledancers-fe/.gitignore create mode 100644 agiledancers-fe/.nvmrc create mode 100644 agiledancers-fe/.prettierrc create mode 100644 agiledancers-fe/.vscode/extensions.json create mode 100644 agiledancers-fe/.vscode/launch.json create mode 100644 agiledancers-fe/.vscode/tasks.json create mode 100644 agiledancers-fe/README.md create mode 100644 agiledancers-fe/angular.json create mode 100644 agiledancers-fe/e2e/school-management.spec.ts create mode 100644 agiledancers-fe/package-lock.json create mode 100644 agiledancers-fe/package.json create mode 100644 agiledancers-fe/playwright.config.ts create mode 100644 agiledancers-fe/proxy.conf.json create mode 100644 agiledancers-fe/public/favicon.svg create mode 100644 agiledancers-fe/src/app/app.config.ts create mode 100644 agiledancers-fe/src/app/app.css create mode 100644 agiledancers-fe/src/app/app.html create mode 100644 agiledancers-fe/src/app/app.routes.ts create mode 100644 agiledancers-fe/src/app/app.spec.ts create mode 100644 agiledancers-fe/src/app/app.ts create mode 100644 agiledancers-fe/src/app/core/api-error.ts create mode 100644 agiledancers-fe/src/app/core/api-models.ts create mode 100644 agiledancers-fe/src/app/core/school-api.spec.ts create mode 100644 agiledancers-fe/src/app/core/school-api.ts create mode 100644 agiledancers-fe/src/app/schools/school-page.html create mode 100644 agiledancers-fe/src/app/schools/school-page.ts create mode 100644 agiledancers-fe/src/app/schools/schools-page.html create mode 100644 agiledancers-fe/src/app/schools/schools-page.ts create mode 100644 agiledancers-fe/src/app/shared/confirm-dialog.ts create mode 100644 agiledancers-fe/src/app/shared/dialog-frame.ts create mode 100644 agiledancers-fe/src/app/shared/entity-editor.spec.ts create mode 100644 agiledancers-fe/src/app/shared/entity-editor.ts create mode 100644 agiledancers-fe/src/app/shared/group-members.spec.ts create mode 100644 agiledancers-fe/src/app/shared/group-members.ts create mode 100644 agiledancers-fe/src/index.html create mode 100644 agiledancers-fe/src/main.ts create mode 100644 agiledancers-fe/src/styles.css create mode 100644 agiledancers-fe/tsconfig.app.json create mode 100644 agiledancers-fe/tsconfig.json create mode 100644 agiledancers-fe/tsconfig.spec.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 605ac2a..42cbefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [Unreleased] + +### Added + +- Angular 22 frontend in `agiledancers-fe` with Dutch-language school management screens. +- Forms for dancers, groups and shows, including multiple group memberships and deletion confirmation. +- Development proxy to the Quarkus REST API, frontend unit tests and end-to-end browser tests. + ## [1.0] - 2026-10-27 ### Added diff --git a/README.md b/README.md index ea024a4..941b1f0 100644 --- a/README.md +++ b/README.md @@ -147,3 +147,24 @@ Run the HTTP integration tests with: ```sh mvn -pl agiledancers-be -Dtest=SchoolManagementResourceTest test ``` + + +## Angular frontend + +The Angular 22 frontend is in [`agiledancers-fe`](agiledancers-fe/README.md), +next to `agiledancers-be`. It provides Dutch-language screens to manage schools, +dancers, groups, memberships and shows through the REST API. + +Use Node.js 22.22.3+ (22.x), 24.15.0+ (24.x), or 26+. +Start the backend as described above, then run in a second terminal: + +```sh +cd agiledancers-fe +npm ci +npm start +``` + +Open . The development proxy forwards API requests to the +backend on port 8080. Frontend builds and tests run independently of Maven: +`npm run build` and `npm test`. See the frontend README for browser tests and +production hosting requirements. diff --git a/agiledancers-fe/.editorconfig b/agiledancers-fe/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/agiledancers-fe/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/agiledancers-fe/.gitignore b/agiledancers-fe/.gitignore new file mode 100644 index 0000000..9db805b --- /dev/null +++ b/agiledancers-fe/.gitignore @@ -0,0 +1,48 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db + +# Browser test artifacts +/test-results +/playwright-report diff --git a/agiledancers-fe/.nvmrc b/agiledancers-fe/.nvmrc new file mode 100644 index 0000000..941d7c0 --- /dev/null +++ b/agiledancers-fe/.nvmrc @@ -0,0 +1 @@ +22.22.3 diff --git a/agiledancers-fe/.prettierrc b/agiledancers-fe/.prettierrc new file mode 100644 index 0000000..d6c16d7 --- /dev/null +++ b/agiledancers-fe/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/agiledancers-fe/.vscode/extensions.json b/agiledancers-fe/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/agiledancers-fe/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/agiledancers-fe/.vscode/launch.json b/agiledancers-fe/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/agiledancers-fe/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/agiledancers-fe/.vscode/tasks.json b/agiledancers-fe/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/agiledancers-fe/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/agiledancers-fe/README.md b/agiledancers-fe/README.md new file mode 100644 index 0000000..7eaf866 --- /dev/null +++ b/agiledancers-fe/README.md @@ -0,0 +1,86 @@ +# Agile Dancers frontend + +Angular 22 application for managing dance schools through the existing Quarkus +REST API. + +## Requirements + +- Node.js 22.22.3 or newer in the 22.x line, Node.js 24.15.0 or newer in the 24.x + line, or Node.js 26+. See [Angular version compatibility](https://angular.dev/reference/versions). +- npm and the Java 25 / Maven backend setup described in the repository README. + +An `.nvmrc` is included. With nvm installed: + +```sh +nvm install +nvm use +npm ci +``` + +## Run locally + +Start the backend from the repository root: + +```sh +mvn -pl agiledancers-be quarkus:dev +``` + +In a second terminal, from `agiledancers-fe`: + +```sh +npm ci +npm start +``` + +Open . The Angular development proxy forwards `/api/**` +to `http://127.0.0.1:8080`. Update `proxy.conf.json` to use another backend address. + +## Screens + +- School overview: list schools and create a school. +- School dashboard: summary, quick actions, next show, rename and delete a school. +- Dancers: create, edit, search and delete dancers; see their group memberships. +- Groups: create, edit and delete groups; manage dancers in each group. +- Shows: create, edit and delete shows; see the three automatically created day parts. + +Deleting requires confirmation. Errors leave entered form values intact. +Membership changes are saved immediately, and a dancer can belong to multiple +groups. Assigning groups to show day parts is not available yet. + +The backend currently stores data in memory, so restarting it clears the data. + +## Checks + +```sh +npm test +npm run build +``` + +Unit tests cover API URLs and payloads, ID zero, form validation, and save failures. +The production build is written to `dist/agiledancers-fe/browser`. +When deploying it, configure the web server to forward `/api/**` to Quarkus and +serve `index.html` for application routes such as `/schools/0/overview`. +The development proxy is not included in the production bundle. + +## Browser tests + +Keep the backend running on port 8080, then run: + +```sh +npx playwright install chromium +npm run e2e +``` + +Playwright starts the frontend on port 4200. The tests create a uniquely named +school, exercise CRUD and multiple group memberships against the real backend, +check mobile layout, and clean up the school afterwards. A second test checks +connection failure and retry behavior. Tests use only their own school data. + +To use an existing Chrome installation instead of downloading Chromium: + +```sh +PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chrome npm run e2e +``` + +Browser test screenshots and failure traces are written to the ignored +`test-results` directory. diff --git a/agiledancers-fe/angular.json b/agiledancers-fe/angular.json new file mode 100644 index 0000000..05aeb9b --- /dev/null +++ b/agiledancers-fe/angular.json @@ -0,0 +1,72 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm", + "analytics": false + }, + "newProjectRoot": "projects", + "projects": { + "agiledancers-fe": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "agiledancers-fe:build:production" + }, + "development": { + "buildTarget": "agiledancers-fe:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/agiledancers-fe/e2e/school-management.spec.ts b/agiledancers-fe/e2e/school-management.spec.ts new file mode 100644 index 0000000..9b84e9d --- /dev/null +++ b/agiledancers-fe/e2e/school-management.spec.ts @@ -0,0 +1,162 @@ +import { test, expect, Page } from '@playwright/test'; + +const backend = 'http://127.0.0.1:8080'; +let schoolToDelete: number | undefined; + +test.afterEach(async ({ request }) => { + if (schoolToDelete !== undefined) { + await request.delete(`${backend}/api/dance-schools/${schoolToDelete}`); + schoolToDelete = undefined; + } +}); + +async function tab(page: Page, label: string) { + await page + .getByRole('navigation', { name: 'Dansschool onderdelen' }) + .getByRole('link', { name: new RegExp(`^${label}`) }) + .click(); +} +async function save(page: Page) { + await page.getByRole('dialog').getByRole('button', { name: 'Opslaan', exact: true }).click(); + await expect(page.getByRole('dialog')).toHaveCount(0); +} +async function confirmDelete(page: Page) { + await page + .getByRole('dialog') + .getByRole('button', { name: 'Definitief verwijderen', exact: true }) + .click(); + await expect(page.getByRole('dialog')).toHaveCount(0); +} + +test('manages a school, shared memberships and shows through the real REST API', async ({ + page, +}) => { + const schoolName = `E2E Studio ${Date.now()}`; + let schoolId: number | undefined; + const browserErrors: string[] = []; + page.on('pageerror', (error) => browserErrors.push(error.message)); + await page.goto('/'); + await page.getByRole('button', { name: 'Dansschool toevoegen' }).click(); + await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill(schoolName); + const created = page.waitForResponse( + (response) => + response.url().endsWith('/api/dance-schools') && response.request().method() === 'POST', + ); + await save(page); + schoolId = (await (await created).json()).id; + schoolToDelete = schoolId; + await page.getByRole('link', { name: new RegExp(schoolName) }).click(); + await expect(page.getByRole('heading', { name: schoolName, exact: true })).toBeVisible(); + + await page.getByRole('button', { name: 'School bewerken' }).click(); + await page + .getByRole('dialog') + .getByLabel('Naam', { exact: true }) + .fill(`${schoolName} vernieuwd`); + await save(page); + await expect( + page.getByRole('heading', { name: `${schoolName} vernieuwd`, exact: true }), + ).toBeVisible(); + + await page.getByRole('button', { name: /Voeg een danser toe/ }).click(); + await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('Sam Testdanser'); + await page.getByLabel('Geboortedatum').fill('2010-05-12'); + await save(page); + await tab(page, 'Dansers'); + await expect(page.getByRole('cell', { name: 'Sam Testdanser', exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Sam Testdanser bewerken' }).click(); + await page.getByLabel('Geboortedatum').fill('2011-06-13'); + await save(page); + await expect(page.getByRole('cell', { name: '13 jun 2011' })).toBeVisible(); + + await tab(page, 'Dansgroepen'); + for (const name of ['Hiphop Teens', 'Jazz Teens']) { + await page.getByRole('button', { name: 'Dansgroep toevoegen' }).click(); + const dialog = page.getByRole('dialog'); + await dialog.getByLabel('Naam / categorie').fill(name); + await dialog.getByLabel('Leeftijd vanaf').fill('12'); + await dialog.getByLabel('Leeftijd tot en met').fill('18'); + await dialog.getByLabel(name.startsWith('Hiphop') ? 'Hiphop' : 'Jazz', { exact: true }).check(); + await save(page); + await page.getByRole('button', { name: `Dansers beheren voor ${name}`, exact: true }).click(); + await page + .getByRole('dialog') + .locator('.member-row') + .filter({ hasText: 'Sam Testdanser' }) + .getByRole('checkbox') + .check(); + await expect(page.getByRole('dialog').getByText('1 van 1 dansers gekoppeld')).toBeVisible(); + await page.getByRole('button', { name: 'Gereed', exact: true }).click(); + await expect( + page + .getByRole('article') + .filter({ has: page.getByRole('heading', { name, exact: true }) }) + .getByText(/1 dansers/), + ).toBeVisible(); + } + await page.getByRole('button', { name: 'Hiphop Teens bewerken' }).click(); + await page.getByLabel('Naam / categorie').fill('Hiphop Selectie'); + await page.getByLabel('Type', { exact: true }).selectOption({ label: 'Selectie' }); + await save(page); + await expect( + page + .getByRole('article') + .filter({ hasText: 'Hiphop Selectie' }) + .getByText(/1 dansers/), + ).toBeVisible(); + await page.screenshot({ path: 'test-results/groups-desktop.png', fullPage: true }); + + await tab(page, 'Shows'); + await page.getByRole('button', { name: 'Show toevoegen' }).click(); + await page.getByRole('dialog').getByLabel('Naam', { exact: true }).fill('Zomershow'); + await page.getByLabel('Datum', { exact: true }).fill('2027-06-12'); + await save(page); + await expect(page.getByText('Ochtend', { exact: true })).toBeVisible(); + await expect(page.getByText('Middag', { exact: true })).toBeVisible(); + await expect(page.getByText('Avond', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Zomershow bewerken' }).click(); + await page.getByLabel('Datum', { exact: true }).fill('2027-07-01'); + await save(page); + await expect(page.locator('.date-tile')).toContainText('jul 2027'); + + await tab(page, 'Overzicht'); + await page.setViewportSize({ width: 390, height: 844 }); + await expect( + page.getByRole('heading', { name: `${schoolName} vernieuwd`, exact: true }), + ).toBeVisible(); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe( + true, + ); + await page.screenshot({ path: 'test-results/overview-mobile.png', fullPage: true }); + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.screenshot({ path: 'test-results/overview-desktop.png', fullPage: true }); + + await tab(page, 'Dansers'); + await page.getByRole('button', { name: 'Sam Testdanser verwijderen' }).click(); + await confirmDelete(page); + await expect(page.getByText('De dansvloer is nog leeg')).toBeVisible(); + await tab(page, 'Dansgroepen'); + await expect(page.getByText(/0 dansers/)).toHaveCount(2); + await page.getByRole('button', { name: 'Hiphop Selectie verwijderen' }).click(); + await confirmDelete(page); + await expect(page.getByRole('heading', { name: 'Hiphop Selectie', exact: true })).toHaveCount(0); + await tab(page, 'Shows'); + await page.getByRole('button', { name: 'Zomershow verwijderen' }).click(); + await confirmDelete(page); + await expect(page.getByText('Het podium wacht op je')).toBeVisible(); + await tab(page, 'Overzicht'); + await page.getByRole('button', { name: 'School verwijderen', exact: true }).click(); + await confirmDelete(page); + await expect(page).toHaveURL(/\/schools$/); + await expect(page.getByRole('link', { name: new RegExp(schoolName) })).toHaveCount(0); + expect(browserErrors).toEqual([]); +}); + +test('shows a useful connection error and recovers on retry', async ({ page }) => { + await page.route('**/api/dance-schools', (route) => route.abort('connectionfailed')); + await page.goto('/schools'); + await expect(page.getByRole('alert')).toContainText('niet bereikbaar'); + await page.unroute('**/api/dance-schools'); + await page.getByRole('button', { name: 'Opnieuw proberen' }).click(); + await expect(page.getByRole('heading', { name: /Mijn dansscholen/ })).toBeVisible(); +}); diff --git a/agiledancers-fe/package-lock.json b/agiledancers-fe/package-lock.json new file mode 100644 index 0000000..e2b4568 --- /dev/null +++ b/agiledancers-fe/package-lock.json @@ -0,0 +1,7015 @@ +{ + "name": "agiledancers-fe", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agiledancers-fe", + "version": "1.0.0", + "dependencies": { + "@angular/common": "22.2.3", + "@angular/compiler": "22.2.3", + "@angular/core": "22.2.3", + "@angular/forms": "22.2.3", + "@angular/platform-browser": "22.2.3", + "@angular/router": "22.2.3", + "rxjs": "7.8.2", + "tslib": "2.8.1" + }, + "devDependencies": { + "@angular/build": "22.2.4", + "@angular/cli": "22.2.4", + "@angular/compiler-cli": "22.2.3", + "@playwright/test": "1.64.1", + "jsdom": "30.1.3", + "prettier": "3.9.10", + "typescript": "6.0.3", + "vitest": "5.0.5" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2202.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2202.4.tgz", + "integrity": "sha512-hafew1tft3+bnv+DjAtLDEq/Yx5/UyAz/+M4VC5EWQQhcFCN81X47szTX3F9dNeiZIQb8d/WmCnmlbHz6CyupA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.4", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "22.2.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.2.4.tgz", + "integrity": "sha512-HDlsVK/qN7p5ttKgPvVLo87yrN0UcHR7OyQo1n/lvvMQcYrA0n01E+ix4t+q4C04ifpMbNQtrK0I1222OKi99w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.7", + "rxjs": "7.8.2", + "source-map": "0.8.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "22.2.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.2.4.tgz", + "integrity": "sha512-9pRNtcZN87yM7KQX+HTxGatrm86y30JxVwkF2WNG1pR0e9yeiYWc1KQ5hRS1Nq1LULoP95HApc+TBXqUXzF4Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.4", + "jsonc-parser": "3.3.1", + "magic-string": "1.4.1", + "ora": "9.4.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build": { + "version": "22.2.4", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.2.4.tgz", + "integrity": "sha512-Emb3pxRkvf749NQ5cz4ux2h9Cnbxx3FmcupdXIaoRxIyNXM8JxjFDCLZTe0yGGOWv5+hSaoR7XESOcN134vBPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2202.4", + "@babel/core": "8.0.5", + "@inquirer/confirm": "6.3.2", + "@parcel/watcher": "2.6.0", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.5.4", + "browserslist": "^4.26.0", + "chokidar": "5.0.0", + "esbuild": "0.28.2", + "https-proxy-agent": "9.1.0", + "jsonc-parser": "3.3.1", + "listr2": "11.1.0", + "magic-string": "1.4.1", + "mrmime": "2.0.1", + "oxc-parser": "0.150.0", + "parse5-html-rewriting-stream": "8.0.1", + "picomatch": "4.0.7", + "piscina": "5.3.2", + "rolldown": "1.2.8", + "sass": "1.104.1", + "sass-embedded": "1.104.1", + "semver": "7.8.5", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.17", + "vite": "8.3.0", + "xxhash-wasm": "1.1.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.6" + }, + "peerDependencies": { + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.2.4", + "istanbul-lib-instrument": "^6.0.0", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^22.0.0", + "postcss": "^8.4.0", + "rollup": "^4.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=6.0 <6.1", + "vitest": "^4.0.8 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "rollup": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "22.2.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.2.4.tgz", + "integrity": "sha512-luD9gNbMV7ME7mv9cUatVE+WF8Ecqa8v9Zo47YZK/6EPQUKdLxeosGWlHtrvBXElK1YehHItrUbawtZQmQwwlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2202.4", + "@angular-devkit/core": "22.2.4", + "@angular-devkit/schematics": "22.2.4", + "@inquirer/prompts": "8.7.2", + "@listr2/prompt-adapter-inquirer": "4.2.7", + "@modelcontextprotocol/server": "2.3.1", + "@schematics/angular": "22.2.4", + "jsonc-parser": "3.3.1", + "listr2": "11.1.0", + "npm-package-arg": "14.0.0", + "parse5-html-rewriting-stream": "8.0.1", + "semver": "7.8.5", + "yargs": "18.1.0", + "zod": "4.6.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.2.3.tgz", + "integrity": "sha512-sRzcvxzurbkCDVFJMguNVgsHds3NvDj5qbZ44NHjH7J9MBk5oZYXHppdHVm8AolBhCXAWvMk0vx+NO8Bt4rS7A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.2.3.tgz", + "integrity": "sha512-GjDkmOBqW+6RJTeW5mlLRivf6Zy7Ed0auVh+DYD67d4UF4Sk17F1vIt8puF4CQ2PIezlHh7oOnermCBNHAOGkQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.2.3.tgz", + "integrity": "sha512-em7Ht0Dv7cN7vZgvl2fl1yscAs+zUG2Vr2YKpVZ/rOWuTL2JZyT1qIfKhC4wQoo/3lN8fiUIwQMpflSGzIEPng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "8.0.1", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.2.3", + "typescript": ">=6.0 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz", + "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.0", + "@babel/helper-compilation-targets": "^8.0.0", + "@babel/helpers": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.0", + "@babel/types": "^8.0.0", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "semver": "^7.7.3" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/core": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.2.3.tgz", + "integrity": "sha512-ponarFrAZsPutT+htRt3PEmkt2/IB7DyrBy2f48sxsIx3ZA6xw09i2HOEBo+VBphlN6B/ynK9q+o04HPhdJ+dQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.2.3", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.2.3.tgz", + "integrity": "sha512-TKP8FpeRCY/++vKWsY8GC1J2sNtEkdNNBPdgnMAMLTwheW5JzjwVatuQlbuZEx3WaAjbll1Ux3d+eHeqAVrtQg==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0", + "zod": "^4.0.10" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.2.3", + "@angular/core": "22.2.3", + "@angular/platform-browser": "22.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.2.3.tgz", + "integrity": "sha512-eWZGsbG4bSrSg9UB/NE8wORGVrh1u+x6rirPGpMWqq/mTi6q+nS+pnPxAxmiEs8wmPH5nCz5b2pRKQeYUkhAGA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/animations": "22.2.3", + "@angular/common": "22.2.3", + "@angular/core": "22.2.3" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "22.2.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.2.3.tgz", + "integrity": "sha512-h4C3iJ3OXM9HItErmTo9fFrFRDj1DSqbvweHAWl8eYJvyuhDD80gBNgdpN8/5gpt9K1HV+PXk0YbyjyQ0zMfPA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.2.3", + "@angular/core": "22.2.3", + "@angular/platform-browser": "22.2.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.9.tgz", + "integrity": "sha512-vZ6JQVq8Ivgz6b/IBlAf90wZviXdluY13cQOPO+nJHbmXwGrPl2GcpqUY7a55/8ZVdfONXpCFMpRijOBCHNlcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.4.3", + "@csstools/css-color-parser": "^4.2.6", + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.5.tgz", + "integrity": "sha512-EgmJ7oEaGQECTn3H3wmnQpEy4hKY66VvM7VQndwPIQob3p877BgbyOUE3EX0LgXQCciF2H8KMdR+RznqjMNOLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", + "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^8.0.6", + "js-tokens": "^10.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/core": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.5.tgz", + "integrity": "sha512-2/oWkgTbBYoqioCWAE4XJobOrzwxTDa5/XjDP3tJ1BhDr/owcd9qnXBp4xc3/2G5X4bvXM04nrxbRJxFcXAxFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.5", + "@babel/helper-compilation-targets": "^8.0.5", + "@babel/helpers": "^8.0.5", + "@babel/parser": "^8.0.5", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.5", + "@babel/types": "^8.0.5", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.6.tgz", + "integrity": "sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.6", + "@babel/types": "^8.0.6", + "@jridgewell/gen-mapping": "0.4.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.31", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.7.tgz", + "integrity": "sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^8.0.5", + "@babel/helper-validator-option": "^8.0.0", + "browserslist": "^4.24.0", + "flru": "1.0.2", + "verkit": "^0.5.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/verkit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@babel/helper-globals": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz", + "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.8.tgz", + "integrity": "sha512-fVM+gSuKbdv9cpmyRNh5EvsDHUZCdxQH3FdnFKSkNYHpbsn95bbrxm+ES3MVeQjcqwi2O5ur/DjLfmTQaO60ng==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", + "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz", + "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helpers": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.8.tgz", + "integrity": "sha512-FBmhiDWs625wHg03OKiLQDeT6LOc2lWBbW97Yqs2iI1tZcucjpmYnO29NHsQ83XxKvredcvUymW/AAn2UtnakA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.8" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.8.tgz", + "integrity": "sha512-Gq65JogAr0S2p0BzvfSJboyuiRnxDQApyOqHkKmkrvm+VlLK4gC3pKsiOKEdnhFBemrFbSnOu90mZZWDpiJnmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/template": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.7.tgz", + "integrity": "sha512-bVV0brinsTWgc6ptSIoQo3GbITtl0VZAqEUJbHHcyfDPEu+q/fEvlYEa+6H85HoK8IdksKBFtKJbJFZ6Kff+zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.6", + "@babel/generator": "^8.0.6", + "@babel/helper-globals": "^8.0.6", + "@babel/parser": "^8.0.7", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.6", + "obug": "^3.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse/node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@babel/types": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.8.tgz", + "integrity": "sha512-iXSSZAKQWmaeAwGtFw10v9fg5hUiUs0L+w+Z2K11kym3pDNK+SF0VcAQPsEFvWcTSZ7LMgClMXDBvTbH0IlICg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.8", + "@babel/helper-validator-identifier": "^8.0.6" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.3.tgz", + "integrity": "sha512-0GEz8qJWokkUVYQYRukmkSHIFNoBH8QTreeHrAFbvz1XFYc3Y+puhMs8PQ5x8GNugiC2l5jvDBXKLbBIByeSBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.2.1" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.16.0.tgz", + "integrity": "sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.5.tgz", + "integrity": "sha512-4ScXlphaKEa49RpHmqwATq9QI9aUTdWVYDlJkzxhsD7ShJP+pHCYLznMO9n/wREY8jPjRZ59wLJY6HM5yB5Gkw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.10.tgz", + "integrity": "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.8.tgz", + "integrity": "sha512-NY/S1EalQhlonqhwwsarrcnkH7s9ByQuvjWYdk1jB2Gb9EPQJjKkhCTWjM3yQ0UpHaLm1XbIL89b+32G8rhO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/checkbox/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.6.tgz", + "integrity": "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.6.tgz", + "integrity": "sha512-Z5SOeRC5VNkINfKN4rY8BSaYKb3GYi8iSEqZZRqogsE2GhBBToQ0Xw25SbI+4bcatLzU009mZ5d9EuJNiJuUCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/external-editor": "^3.0.7", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.8.tgz", + "integrity": "sha512-dMEnTkpLqzM/sbILwL7QJgx9iX5hoEM8+bh6shgwd9mYJamdOnth7y2mMaFd08Glj9AXEFKllWcGYyBqnJsHrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.7.tgz", + "integrity": "sha512-enNqTJpojgISCG+a5acjnfJ02hcRTFUBVm8uXiCY40PRPZtSzFnNLskOTU+kDUUEODYma90syxuea1rRtUFh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.10.tgz", + "integrity": "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.9.tgz", + "integrity": "sha512-2Vd5T7lTxfgJMpAd3aOu6bQpbUnCU7tBgvTQIZkP/7juJzmRmTM2t5XfOCuirFduWLTSJI/WZp32Cq1E01TVeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/input/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.6.tgz", + "integrity": "sha512-22LnGGCDm1M/pKXxxF8n6ppr/1IztdsxqeqeBRUMTTNnJTkjIBiAkewiDM4u/gjtazj/FvZS+nk9AbO/vEcBFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.5.tgz", + "integrity": "sha512-0v0wLQA4uhWbybmdH6oPl7ooxCiymNylkvi2gCSc+mA5ZQB6QoJLVhoHq12T82lhozpp9fpMJnJkC1nJrU/A+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.2.tgz", + "integrity": "sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.5", + "@inquirer/confirm": "^6.3.2", + "@inquirer/editor": "^5.3.3", + "@inquirer/expand": "^5.1.5", + "@inquirer/input": "^5.1.6", + "@inquirer/number": "^4.2.3", + "@inquirer/password": "^5.2.2", + "@inquirer/rawlist": "^5.3.5", + "@inquirer/search": "^4.3.3", + "@inquirer/select": "^5.2.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.8.tgz", + "integrity": "sha512-h+6Y4pPvYvuRbksn1kIBdD9tCYD+E468W5sIcVfLmoLP5IasnY0r33N+r+46ej/P8b2PCREjyKR+MnnfPGdTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.6.tgz", + "integrity": "sha512-JZj9ttkIvuaFQIc2oxsyqC1ad05WL02ku6W/INVKTCzNaVq1KkhP3AfTRC0m7OWlKZyw+EWwPBFZfYiG9jF6Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.8.tgz", + "integrity": "sha512-BklkkAAOpjdCfuQw5fVGcDuhKxA7T1iBpn6b2JUvDDdkIsO3Ftlg+WnWUCVpkUBFagL2o036OFW+xL4lC9wIPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.10", + "@inquirer/core": "^12.0.6", + "@inquirer/figures": "^2.0.10", + "@inquirer/type": "4.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select/node_modules/@inquirer/type": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.1.tgz", + "integrity": "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.7.tgz", + "integrity": "sha512-k180vxSv2KqmnDxoato6pCqEJsov4U5C0EVmldZkAdNeVBxY48A/LM6TCNPGIzDK/iZSfsZP0Mw+gbWc5hXLyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 9", + "listr2": "11.1.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.6.tgz", + "integrity": "sha512-mY5FG4TjPAkY4P0w+OhHaUka5mDh2TX2WKYIwuKzJ1zeW3VvRgxdam/lGJTquI+bthTx5CSHDW+BAQCnNAzkEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.6.tgz", + "integrity": "sha512-foa+pwitysO8k+xhs7psBFfTKnVgR69NlZRRTHaFVDqphh7AdGpLeyRzKw/ofatr/sN6TiHRRW6mmop0ZrrppQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.6.tgz", + "integrity": "sha512-QR4YRyR5h5Z8eGXrNQjiyo2NNDfqi3tCc9dQG5Is1blCt+qWw1ZoBWhlWAr5d+jshkifMIJjVHzHGKbkKzF8Tw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.6.tgz", + "integrity": "sha512-HmiyFFdJa38s1heCMSooSPaBSFTHJ3C+ERPp28xAPlDX1YiALJVOgbry065nXd8Y7KISWjnw05zpG1RX8IfftA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.6.tgz", + "integrity": "sha512-ADzCuCF2cTNiX9kDScqcz1fjnAkxPpQNneV3KFTdV3wWtVlI2sTGzySoMTgDpinkMMFj1NTJlxA6XR8fwc4hlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.6.tgz", + "integrity": "sha512-J7A9aEQsQiv0TYtBGL7NDIPp2lOS8nnl+zm4sWZm1xlsTTaQ4PgD096Adzdrk27rw3UxCkDXdCUa4ax41oztBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.6.tgz", + "integrity": "sha512-1g7G0knRX2iV/voDu54yxrGqw5Dk0w2oIYb7dgJq8IkOi+m7wbD8Q3QpPFjh0C01G58S88dqGn03len6UPCXsg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz", + "integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz", + "integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.150.0.tgz", + "integrity": "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.150.0.tgz", + "integrity": "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.150.0.tgz", + "integrity": "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.150.0.tgz", + "integrity": "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.150.0.tgz", + "integrity": "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.150.0.tgz", + "integrity": "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.150.0.tgz", + "integrity": "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.150.0.tgz", + "integrity": "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.150.0.tgz", + "integrity": "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.150.0.tgz", + "integrity": "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.150.0.tgz", + "integrity": "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.150.0.tgz", + "integrity": "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.150.0.tgz", + "integrity": "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.150.0.tgz", + "integrity": "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.150.0.tgz", + "integrity": "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.150.0.tgz", + "integrity": "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.150.0.tgz", + "integrity": "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.150.0.tgz", + "integrity": "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.150.0.tgz", + "integrity": "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@playwright/test": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", + "integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@schematics/angular": { + "version": "22.2.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.2.4.tgz", + "integrity": "sha512-3yXpeDsn780rk/Pm+2QjoGpvS03doyuB2i8jUt/tNtDatIIVoXUJak3kjPOK3TVorOJX/fTNMjMxDwQzvec2+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.4", + "@angular-devkit/schematics": "22.2.4", + "jsonc-parser": "3.3.1", + "oxc-parser": "0.150.0", + "typescript": "6.0.3" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/gensync": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz", + "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^1.4.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9 || ^3.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.33.tgz", + "integrity": "sha512-yiVKDX+qCMRKBE084lXvq9EcwFUhKZ2nU8TK1EQQuOgPNR/lmbGxXOeDdlGUqC2Zz11Cs9rk1d/8NfgBmlkt0w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.5.4.tgz", + "integrity": "sha512-8/lOOjT+PElYCnaUbmhgd3Uad5R1VTQ4Wg0k4rK8bX04m5WEqHNmnlLy3Q/lJ7dk25XA+72gayzc/skTfKKpcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^7.0.0", + "css-what": "^8.0.0", + "dom-serializer": "^3.0.0", + "domhandler": "^6.0.0", + "htmlparser2": "^12.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/boolbase": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", + "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/browserslist": { + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.1.1.tgz", + "integrity": "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/colorjs.io": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.7.1.tgz", + "integrity": "sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", + "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0", + "css-what": "^8.0.0", + "domhandler": "^6.0.1", + "domutils": "^4.0.2", + "nth-check": "^3.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", + "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-urls": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz", + "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.2" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.465", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.465.tgz", + "integrity": "sha512-zg4iUpbeGw7YNZsswc2xKr2uiBex2d8dELAXo92Icw3but1ULbrvK8USe6wVLaWjgJnKBARv0pchx9Owktx6wQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", + "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz", + "integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.1.3", + "@asamuzakjp/dom-selector": "^10.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.15", + "@exodus/bytes": "^1.16.0", + "css-tree": "^3.2.1", + "data-urls": "^8.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^7.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.12.1", + "w3c-xmlserializer": "^6.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.2.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/listr2": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-11.1.0.tgz", + "integrity": "sha512-xsuczF6mWkEo/E80QHaEokQzKmTy7m80KcLRnkUt9wrcePres2w7dbQi5zOsWtDUg8Dac4gPFyu4EJ/IvKwTOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^6.1.1", + "log-update": "^8.0.0", + "wrap-ansi": "^10.0.1" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/lmdb": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.6.tgz", + "integrity": "sha512-j3uE8ReKNyUWDjhfEFSJqE/1DLtfTR5Z8yFzVHvBjAk37wNg7HdScjcv8ttPHRvrdgPQMPWxFFI0SsdBzI5lBw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.6", + "@lmdb/lmdb-darwin-x64": "3.5.6", + "@lmdb/lmdb-linux-arm": "3.5.6", + "@lmdb/lmdb-linux-arm64": "3.5.6", + "@lmdb/lmdb-linux-x64": "3.5.6", + "@lmdb/lmdb-win32-arm64": "3.5.6", + "@lmdb/lmdb-win32-x64": "3.5.6" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-8.0.0.tgz", + "integrity": "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.3.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0", + "strip-ansi": "^7.2.0", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-package-arg": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-14.0.0.tgz", + "integrity": "sha512-69XQh3k+dtGa1p+7RaR57IuG3rCko96xr/nUfN4yDYBXbTYICiWcOpsFKLN2GtGE9cyIljE+f1exnaYt9MvM+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^10.1.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^8.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/nth-check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", + "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.3.tgz", + "integrity": "sha512-Bc8LnA2x4ZHRQ9LjCm/NOdD3+CTQPhGNu8m7/6+ERtiB6Bb/fn7nCySwcmAC+5WCqwelpSAQgo1VX9BGpsC0tQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/oxc-parser": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.150.0.tgz", + "integrity": "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.150.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.150.0", + "@oxc-parser/binding-android-arm64": "0.150.0", + "@oxc-parser/binding-darwin-arm64": "0.150.0", + "@oxc-parser/binding-darwin-x64": "0.150.0", + "@oxc-parser/binding-freebsd-x64": "0.150.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", + "@oxc-parser/binding-linux-arm64-musl": "0.150.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-musl": "0.150.0", + "@oxc-parser/binding-openharmony-arm64": "0.150.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", + "@oxc-parser/binding-win32-x64-msvc": "0.150.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.3.2.tgz", + "integrity": "sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/playwright": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", + "integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.64.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.1.0.tgz", + "integrity": "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prettier": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz", + "integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.104.1.tgz", + "integrity": "sha512-yDA+1aIG3EHgN4V/BvuhCvu61FF6hEd4e+9DxikUm9U0CAGuvdIZ/UYy7qbOxjhbbWQraoyLlMuzdRGOSV5Bmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.104.1.tgz", + "integrity": "sha512-7LxbtPJ1MWKVbhzOnGx1sHTkHTcHVJ9hbgVsU29p/8gY1VeXt19fcJFr5xsx2f6zfrrR3FeOcYMt0uYDLAxpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.7.0", + "immutable": "^5.1.5", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.104.1", + "sass-embedded-android-arm": "1.104.1", + "sass-embedded-android-arm64": "1.104.1", + "sass-embedded-android-riscv64": "1.104.1", + "sass-embedded-android-x64": "1.104.1", + "sass-embedded-darwin-arm64": "1.104.1", + "sass-embedded-darwin-x64": "1.104.1", + "sass-embedded-linux-arm": "1.104.1", + "sass-embedded-linux-arm64": "1.104.1", + "sass-embedded-linux-musl-arm": "1.104.1", + "sass-embedded-linux-musl-arm64": "1.104.1", + "sass-embedded-linux-musl-riscv64": "1.104.1", + "sass-embedded-linux-musl-x64": "1.104.1", + "sass-embedded-linux-riscv64": "1.104.1", + "sass-embedded-linux-x64": "1.104.1", + "sass-embedded-unknown-all": "1.104.1", + "sass-embedded-win32-arm64": "1.104.1", + "sass-embedded-win32-x64": "1.104.1" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.104.1.tgz", + "integrity": "sha512-toyMCyUKkT6DsYlHuKZGjd8T6WltaW4TGX07F7HnF4tNJM5m/8VaVsd74zrq3QSPqGE9JjxIDK4NrNc/QY9pkA==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.104.1" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.104.1.tgz", + "integrity": "sha512-g/XPAJYAzSSQZFP4xCdGD0HLKB4mf24cJ/+5Nls8rMool3jNkBv9+AFO4EV0b+RxN8rLcEsMccH1CR64yIFYcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.104.1.tgz", + "integrity": "sha512-yW3BVW4YFpN2vu2cVQ+Pz5JrRTvoIz36aCdo+J/Mo1A9gEN8eVPV/AL7aeZ803YdKgIitd3qw3pY4wpmT6TXNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.104.1.tgz", + "integrity": "sha512-ZYY8oMKQS7Q/6FBoC8w3T16O3MucDe3xv8iKym41Nht5uvLCGGpm4fC4OF5nlv7wQkJv8oactYddGG3Wd0Rqkg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.104.1.tgz", + "integrity": "sha512-5WZKC4lSfjMIjzZdQUlYQUAV+UiatBhD8w28U7cYLLj4/dMgEPOBd4g3VMv0BwpgH2JOaycMS42dXq29W2bcDg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.104.1.tgz", + "integrity": "sha512-oatuHzn0NsEg0d9Dcze6CuWUPvuETtMxRK4cv0eRAg7p6R0vHnv5GZRyWG6gEy3nc/ha/L+GtTasWVU82f4jrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.104.1.tgz", + "integrity": "sha512-gql/5sHGzUTVwuHfM3QLCDeb6NYwen+LLj22gz81C2NKHF/4nUtDyh4N+fURWdT7J0/EhtMOd0+FLML3sGNP7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.104.1.tgz", + "integrity": "sha512-WfmshPJOp9vUKxgp8wZ2ncaKBbyrlW9J51M2aUQHWECjMNxTULnGOHWSmD7FViXv4zwn1Co5FIFQUipW7RCxtA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.104.1.tgz", + "integrity": "sha512-sVWUKXNTu8sP3X95Df0xo/t49Ce5IseTIMqXHxhFBKVmx7gf7kBWNLK0wH5ym+4khc+B5nNJPgq4IxpelG8tMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.104.1.tgz", + "integrity": "sha512-k0cQ/CxBzFVYiQEObg6CpF6CUglkdEhi/DZgZXQbwC27kbGGY+fatmIs/cFPJL2z1ulACwoAr86RQUFzYvw5eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.104.1.tgz", + "integrity": "sha512-H7b+LzZ/w8pwNExInM8HDHraqvxCMdUc5ZRGU/aSJJhZ9HjeRKBjF3GxaisVHnx7o18j1l4VQ5cLBL9tTmNzZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.104.1.tgz", + "integrity": "sha512-9DYdn4wfrkidjxJVtEd2iCx8HKdZe+uF82GDmrYEZhgu4NjVE6a4fQyW7OLwzn3erZMHKBaObBOH1d37DnXI1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.104.1.tgz", + "integrity": "sha512-5xz+nAr01b6yaRdrGVgE7lNvm1lGFjwNdUvnz+teidvHCbD5oTcbhrRVrnDoLHHvq5n3Jve9TzWaaHhezaEfHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.104.1.tgz", + "integrity": "sha512-oWryPJWTXK5adtlz/FD7wd2kS4I0CEgp6mWLUVb4oMFURMLz9bA9fO87D7lc4SV++lds7T51PU1kykCbOvNHMg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.104.1.tgz", + "integrity": "sha512-j/sX/yKTx/wJKepC5/ISlR6kPeSuHKThf76D2za5i2VnbHg0MN19jQq3WMorowR3kRghqt6azs3awogG+e7IRA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.104.1.tgz", + "integrity": "sha512-RaMsYZB5mRioL4qhoI7T5mmNb2OunakbPTJW2cuu3MeIULnGoxo7Wd5QKFxeQvF7sSQL57RuaQ+q9pH+/pQ9Fg==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.104.1" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.104.1.tgz", + "integrity": "sha512-fS6DL7lcnTPclqGLF4xRt8egZlFY9cU23d57ufbWqaUbCFu9JTu0+8qB4jMxfx1la6byi3GoSTfbnNOLfWv8sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.104.1.tgz", + "integrity": "sha512-/c4N2A93Ckms6DuRFVftKYISuI1tyC8gmlVJ8WaWFzgCBN/Swq2rARJIBUNeuxlmxjmYXfd3+2f/u887K63wbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.1.tgz", + "integrity": "sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/sync-child-process": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.1.1.tgz", + "integrity": "sha512-54zMOnH5qgjdKW8SGpCx3daSP+v+TtTfOxgPdhUE3P0hytMMpknZfP67S4JeRcx5IQRWJERz6890yO4dkTywww==", + "dev": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.19.tgz", + "integrity": "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.19.tgz", + "integrity": "sha512-goNXV6Ho9TIOfpJZanTd6CKG5DyYgYAL+k+22T0SLh8l6+KHIfBUPFuePuSZv05UttIaaLmI9tb7TmGIdK5AkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz", + "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "8.12.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz", + "integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-8.0.0.tgz", + "integrity": "sha512-SCv6OOV6Xj2/3cXy3dGmADluJTNcL3o7hZAglNPTe+WYuEuvxgJzxPrSDLZhF+CwyQOubqgecjMmTJGMVLWjYQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.155.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" + } + }, + "node_modules/vitest": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.4.3", + "obug": "^3.0.0", + "picomatch": "^4.0.7", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz", + "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/vitest/node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.3.0.tgz", + "integrity": "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.16.0", + "tr46": "^7.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.2.tgz", + "integrity": "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/agiledancers-fe/package.json b/agiledancers-fe/package.json new file mode 100644 index 0000000..d82989e --- /dev/null +++ b/agiledancers-fe/package.json @@ -0,0 +1,36 @@ +{ + "name": "agiledancers-fe", + "version": "1.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve --proxy-config proxy.conf.json", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test --watch=false", + "e2e": "playwright test" + }, + "private": true, + "dependencies": { + "@angular/common": "22.2.3", + "@angular/compiler": "22.2.3", + "@angular/core": "22.2.3", + "@angular/forms": "22.2.3", + "@angular/platform-browser": "22.2.3", + "@angular/router": "22.2.3", + "rxjs": "7.8.2", + "tslib": "2.8.1" + }, + "devDependencies": { + "@angular/build": "22.2.4", + "@angular/cli": "22.2.4", + "@angular/compiler-cli": "22.2.3", + "@playwright/test": "1.64.1", + "jsdom": "30.1.3", + "prettier": "3.9.10", + "typescript": "6.0.3", + "vitest": "5.0.5" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } +} diff --git a/agiledancers-fe/playwright.config.ts b/agiledancers-fe/playwright.config.ts new file mode 100644 index 0000000..282a1db --- /dev/null +++ b/agiledancers-fe/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, + workers: 1, + retries: 0, + timeout: 60_000, + reporter: 'list', + use: { + ...devices['Desktop Chrome'], + baseURL: 'http://127.0.0.1:4200', + trace: 'retain-on-failure', + actionTimeout: 15_000, + screenshot: 'only-on-failure', + launchOptions: process.env['PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH'] + ? { executablePath: process.env['PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH'] } + : {}, + }, + webServer: { + command: 'npm start -- --host 127.0.0.1 --port 4200', + url: 'http://127.0.0.1:4200', + reuseExistingServer: !process.env['CI'], + timeout: 60_000, + }, +}); diff --git a/agiledancers-fe/proxy.conf.json b/agiledancers-fe/proxy.conf.json new file mode 100644 index 0000000..a39fea6 --- /dev/null +++ b/agiledancers-fe/proxy.conf.json @@ -0,0 +1,7 @@ +{ + "/api/**": { + "target": "http://127.0.0.1:8080", + "secure": false, + "changeOrigin": true + } +} diff --git a/agiledancers-fe/public/favicon.svg b/agiledancers-fe/public/favicon.svg new file mode 100644 index 0000000..6a741b0 --- /dev/null +++ b/agiledancers-fe/public/favicon.svg @@ -0,0 +1 @@ + diff --git a/agiledancers-fe/src/app/app.config.ts b/agiledancers-fe/src/app/app.config.ts new file mode 100644 index 0000000..9417602 --- /dev/null +++ b/agiledancers-fe/src/app/app.config.ts @@ -0,0 +1,17 @@ +import { ApplicationConfig, LOCALE_ID, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { registerLocaleData } from '@angular/common'; +import localeNl from '@angular/common/locales/nl'; +import { provideRouter, withInMemoryScrolling } from '@angular/router'; +import { routes } from './app.routes'; + +registerLocaleData(localeNl); + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideHttpClient(), + provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'enabled' })), + { provide: LOCALE_ID, useValue: 'nl-NL' }, + ], +}; diff --git a/agiledancers-fe/src/app/app.css b/agiledancers-fe/src/app/app.css new file mode 100644 index 0000000..c3ffd89 --- /dev/null +++ b/agiledancers-fe/src/app/app.css @@ -0,0 +1,239 @@ +:host { + display: block; + min-height: 100vh; +} +.app-layout { + display: grid; + grid-template-columns: 250px minmax(0, 1fr); + min-height: 100vh; +} +.sidebar { + position: sticky; + top: 0; + height: 100vh; + display: flex; + flex-direction: column; + padding: 36px 22px 22px; + background: #fff; + border-right: 1px solid var(--line); +} +.brand { + display: flex; + align-items: center; + gap: 12px; + color: var(--ink); + font-size: 21px; + font-weight: 800; + letter-spacing: -0.8px; + padding: 0 8px; +} +.brand-mark { + background: var(--forest); + color: var(--lime); + width: 42px; + height: 44px; + border-radius: 13px 13px 13px 3px; + display: flex; + align-items: center; + justify-content: center; + font-size: 30px; + position: relative; +} +.brand-mark > span { + font-size: 23px; + position: absolute; + right: 3px; + top: -5px; +} +.brand-light { + font-weight: 450; +} +.brand small { + font-size: 8px; + display: block; + letter-spacing: 1.8px; + font-weight: 600; + margin-top: 5px; + color: var(--muted); +} +.nav-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 1.8px; + color: var(--muted); + margin: 60px 14px 14px; +} +.nav-item { + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; + font-weight: 650; + padding: 15px 12px; + border-radius: 9px; +} +.nav-item.selected { + background: #eaf1ec; + color: var(--forest); +} +.nav-item svg { + width: 20px; + height: 20px; +} +.nav-item > span { + margin-left: auto; +} +.sidebar-note { + margin-top: auto; + padding: 28px 16px; +} +.sidebar-note > span { + color: #569473; + font-size: 45px; +} +.sidebar-note h3 { + font-weight: 600; + font-size: 24px; + letter-spacing: -0.8px; + line-height: 1.25; + margin: 14px 0; +} +.sidebar-note p { + font-size: 12px; + line-height: 1.8; + color: var(--muted); +} +.sidebar-footer { + display: flex; + align-items: center; + gap: 8px; + border-top: 1px solid var(--line); + padding: 22px 10px 0; + font-size: 11px; + color: var(--muted); +} +.sidebar-footer > span:last-child { + margin-left: auto; +} +.status-dot, +.topbar-dot { + width: 6px; + height: 6px; + background: #6a9c7b; + border-radius: 50%; + display: inline-block; +} +.main-column { + min-width: 0; + display: flex; + flex-direction: column; +} +.topbar { + height: 78px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 48px; + border-bottom: 1px solid var(--line); + background: #fff; + font-size: 12px; + font-weight: 550; +} +.topbar-right { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted); + font-size: 11px; +} +main { + padding: 44px 48px 60px; + max-width: 1500px; + width: 100%; + margin: 0 auto; + flex: 1; +} +.page-footer { + margin: 0 48px; + border-top: 1px solid var(--line); + padding: 22px 0; + display: flex; + justify-content: space-between; + gap: 20px; + color: var(--muted); + font-size: 10px; +} +@media (min-width: 1600px) { + main { + padding: 58px 68px; + } +} +@media (max-width: 1100px) { + .app-layout { + grid-template-columns: 205px minmax(0, 1fr); + } + .sidebar { + padding-left: 14px; + padding-right: 14px; + } + .brand { + font-size: 18px; + gap: 8px; + } + main { + padding: 32px 28px 48px; + } + .topbar { + padding: 0 28px; + } + .page-footer { + margin: 0 28px; + } +} +@media (max-width: 760px) { + .app-layout { + display: block; + } + .sidebar { + position: static; + height: auto; + padding: 18px 20px; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--line); + border-right: 0; + gap: 18px; + } + .brand { + padding: 0; + } + .brand-mark { + width: 34px; + height: 36px; + } + .nav-label, + .sidebar-note, + .sidebar-footer, + .nav-item svg, + .nav-item > span { + display: none; + } + .nav-item { + font-size: 11px; + padding: 12px; + } + .topbar { + height: 48px; + padding: 0 22px; + } + .topbar-right { + font-size: 10px; + } + main { + padding: 28px 20px 42px; + } + .page-footer { + margin: 0 20px; + } +} diff --git a/agiledancers-fe/src/app/app.html b/agiledancers-fe/src/app/app.html new file mode 100644 index 0000000..5ff0ead --- /dev/null +++ b/agiledancers-fe/src/app/app.html @@ -0,0 +1,46 @@ + +
+ +
+
+ Dansschoolbeheer Studio workspace +
+
+
+ Gemaakt voor de mensen achter de dans.Agile Dancers +
+
+
diff --git a/agiledancers-fe/src/app/app.routes.ts b/agiledancers-fe/src/app/app.routes.ts new file mode 100644 index 0000000..d4d9aff --- /dev/null +++ b/agiledancers-fe/src/app/app.routes.ts @@ -0,0 +1,21 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { path: '', pathMatch: 'full', redirectTo: 'schools' }, + { + path: 'schools', + title: 'Mijn dansscholen · Agile Dancers', + loadComponent: () => import('./schools/schools-page').then((m) => m.SchoolsPage), + }, + { path: 'schools/:schoolId', pathMatch: 'full', redirectTo: 'schools/:schoolId/overview' }, + { + path: 'schools/:schoolId/:section', + title: 'Dansschool beheren · Agile Dancers', + loadComponent: () => import('./schools/school-page').then((m) => m.SchoolPage), + canMatch: [ + (_route, segments) => + ['overview', 'dancers', 'groups', 'shows'].includes(segments[2]?.path ?? ''), + ], + }, + { path: '**', redirectTo: 'schools' }, +]; diff --git a/agiledancers-fe/src/app/app.spec.ts b/agiledancers-fe/src/app/app.spec.ts new file mode 100644 index 0000000..745ec80 --- /dev/null +++ b/agiledancers-fe/src/app/app.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { App } from './app'; + +describe('Application shell', () => { + it('provides navigation and a keyboard skip link', async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [provideRouter([])], + }).compileComponents(); + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + const page = fixture.nativeElement as HTMLElement; + expect(page.querySelector('nav[aria-label="Hoofdnavigatie"] a')?.getAttribute('href')).toBe( + '/schools', + ); + expect(page.querySelector('.skip-link')?.getAttribute('href')).toBe('#main-content'); + }); +}); diff --git a/agiledancers-fe/src/app/app.ts b/agiledancers-fe/src/app/app.ts new file mode 100644 index 0000000..8e3eeeb --- /dev/null +++ b/agiledancers-fe/src/app/app.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { RouterLink, RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'app-root', + imports: [RouterOutlet, RouterLink], + templateUrl: './app.html', + styleUrl: './app.css', +}) +export class App {} diff --git a/agiledancers-fe/src/app/core/api-error.ts b/agiledancers-fe/src/app/core/api-error.ts new file mode 100644 index 0000000..bca7f1d --- /dev/null +++ b/agiledancers-fe/src/app/core/api-error.ts @@ -0,0 +1,24 @@ +import { HttpErrorResponse } from '@angular/common/http'; + +export function apiError(error: unknown): string { + if (!(error instanceof HttpErrorResponse)) return 'Er ging iets mis. Probeer het opnieuw.'; + if (error.status === 0 || error.status === 502 || error.status === 504) { + return 'De server is niet bereikbaar. Controleer of de backend draait en probeer het opnieuw.'; + } + if (error.status === 404) return 'Deze gegevens zijn niet meer beschikbaar. Vernieuw de pagina.'; + const body: unknown = error.error; + if (body && typeof body === 'object') { + if ('message' in body && typeof body.message === 'string') return body.message; + if ('violations' in body && Array.isArray(body.violations)) { + const messages = body.violations + .map((v: unknown) => + v && typeof v === 'object' && 'message' in v ? String(v.message) : '', + ) + .filter(Boolean); + if (messages.length) return messages.join(' · '); + } + } + return error.status === 400 + ? 'Controleer de ingevulde gegevens en probeer het opnieuw.' + : 'Opslaan is niet gelukt. Probeer het opnieuw.'; +} diff --git a/agiledancers-fe/src/app/core/api-models.ts b/agiledancers-fe/src/app/core/api-models.ts new file mode 100644 index 0000000..d918b77 --- /dev/null +++ b/agiledancers-fe/src/app/core/api-models.ts @@ -0,0 +1,74 @@ +export interface School { + id: number; + name: string; + dancerIds: number[]; + danceGroupIds: number[]; + showIds: number[]; +} +export interface Dancer { + id: number; + name: string; + birthDate: string; +} +export type GroupType = 'SHOW_CASE' | 'PRE_EDUCATION' | 'SELECTION' | 'REGULAR' | 'PRE_SCHOOLER'; +export type GroupStyle = 'HIPHOP' | 'JAZZ' | 'MODERN' | 'STREETDANCE' | 'CLASSIC' | 'BREAKDANCE'; +export interface DanceGroup { + id: number; + category: string; + type: GroupType; + startAge: number; + endAge: number; + styles: GroupStyle[]; + dancerIds: number[]; +} +export interface Show { + id: number; + name: string; + date: string; + dayParts: { type: 'MORNING' | 'AFTERNOON' | 'EVENING'; danceGroupIds: number[] }[]; +} +export interface SchoolRequest { + name: string; +} +export interface DancerRequest { + name: string; + birthDate: string; +} +export interface GroupRequest { + category: string; + type: GroupType; + startAge: number; + endAge: number; + styles: GroupStyle[]; +} +export interface ShowRequest { + name: string; + date: string; +} +export type ResourceKind = 'school' | 'dancer' | 'group' | 'show'; +export type EditorTarget = + | { kind: 'school'; value?: School } + | { kind: 'dancer'; value?: Dancer } + | { kind: 'group'; value?: DanceGroup } + | { kind: 'show'; value?: Show }; +export interface SchoolData { + school: School; + dancers: Dancer[]; + groups: DanceGroup[]; + shows: Show[]; +} +export const GROUP_TYPES: { value: GroupType; label: string }[] = [ + { value: 'REGULAR', label: 'Regulier' }, + { value: 'SHOW_CASE', label: 'Showcase' }, + { value: 'PRE_EDUCATION', label: 'Vooropleiding' }, + { value: 'SELECTION', label: 'Selectie' }, + { value: 'PRE_SCHOOLER', label: 'Kleuters' }, +]; +export const GROUP_STYLES: { value: GroupStyle; label: string }[] = [ + { value: 'HIPHOP', label: 'Hiphop' }, + { value: 'JAZZ', label: 'Jazz' }, + { value: 'MODERN', label: 'Modern' }, + { value: 'STREETDANCE', label: 'Streetdance' }, + { value: 'CLASSIC', label: 'Klassiek' }, + { value: 'BREAKDANCE', label: 'Breakdance' }, +]; diff --git a/agiledancers-fe/src/app/core/school-api.spec.ts b/agiledancers-fe/src/app/core/school-api.spec.ts new file mode 100644 index 0000000..34c9f88 --- /dev/null +++ b/agiledancers-fe/src/app/core/school-api.spec.ts @@ -0,0 +1,69 @@ +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { SchoolApi } from './school-api'; + +describe('SchoolApi contract', () => { + let api: SchoolApi; + let http: HttpTestingController; + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()], + }); + api = TestBed.inject(SchoolApi); + http = TestBed.inject(HttpTestingController); + }); + afterEach(() => http.verify()); + + it('loads all resources from the same school', () => { + const received = vi.fn(); + api.schoolData(7).subscribe(received); + const school = { id: 7, name: 'Studio', dancerIds: [], danceGroupIds: [], showIds: [] }; + http.expectOne('/api/dance-schools/7').flush(school); + http.expectOne('/api/dance-schools/7/dancers').flush([]); + http.expectOne('/api/dance-schools/7/dance-groups').flush([]); + expect(received).not.toHaveBeenCalled(); + http.expectOne('/api/dance-schools/7/shows').flush([]); + expect(received).toHaveBeenCalledWith({ school, dancers: [], groups: [], shows: [] }); + }); + + it('updates an existing entity even when its id is zero', () => { + api.saveSchool({ name: 'Renamed' }, 0).subscribe(); + const request = http.expectOne('/api/dance-schools/0'); + expect(request.request.method).toBe('PUT'); + expect(request.request.body).toEqual({ name: 'Renamed' }); + request.flush({ id: 0, name: 'Renamed', dancerIds: [], danceGroupIds: [], showIds: [] }); + }); + + it('keeps group details separate from memberships', () => { + api + .saveGroup( + 7, + { category: 'Teens', type: 'REGULAR', startAge: 12, endAge: 18, styles: ['HIPHOP'] }, + 0, + ) + .subscribe(); + const request = http.expectOne('/api/dance-schools/7/dance-groups/0'); + expect(request.request.method).toBe('PUT'); + expect(request.request.body).not.toHaveProperty('dancerIds'); + request.flush({}); + api.membership(7, 0, 3, true).subscribe(); + const add = http.expectOne('/api/dance-schools/7/dance-groups/0/dancers/3'); + expect(add.request.method).toBe('PUT'); + expect(add.request.body).toBeNull(); + add.flush(null); + api.membership(7, 0, 3, false).subscribe(); + const remove = http.expectOne('/api/dance-schools/7/dance-groups/0/dancers/3'); + expect(remove.request.method).toBe('DELETE'); + remove.flush(null); + }); + + it('propagates server errors instead of pretending a save succeeded', () => { + const failure = vi.fn(); + api.saveDancer(7, { name: 'Sam', birthDate: '2010-05-12' }).subscribe({ error: failure }); + http + .expectOne('/api/dance-schools/7/dancers') + .flush({ message: 'Invalid data' }, { status: 400, statusText: 'Bad Request' }); + expect(failure).toHaveBeenCalledOnce(); + }); +}); diff --git a/agiledancers-fe/src/app/core/school-api.ts b/agiledancers-fe/src/app/core/school-api.ts new file mode 100644 index 0000000..04b9b0c --- /dev/null +++ b/agiledancers-fe/src/app/core/school-api.ts @@ -0,0 +1,63 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { forkJoin, Observable } from 'rxjs'; +import { + DanceGroup, + Dancer, + DancerRequest, + GroupRequest, + ResourceKind, + School, + SchoolData, + SchoolRequest, + Show, + ShowRequest, +} from './api-models'; + +@Injectable({ providedIn: 'root' }) +export class SchoolApi { + private readonly http = inject(HttpClient); + private readonly base = '/api/dance-schools'; + + schools(): Observable { + return this.http.get(this.base); + } + schoolData(schoolId: number): Observable { + const base = `${this.base}/${schoolId}`; + return forkJoin({ + school: this.http.get(base), + dancers: this.http.get(`${base}/dancers`), + groups: this.http.get(`${base}/dance-groups`), + shows: this.http.get(`${base}/shows`), + }); + } + saveSchool(body: SchoolRequest, id?: number): Observable { + return id === undefined + ? this.http.post(this.base, body) + : this.http.put(`${this.base}/${id}`, body); + } + saveDancer(schoolId: number, body: DancerRequest, id?: number): Observable { + return this.save(`${this.base}/${schoolId}/dancers`, body, id); + } + saveGroup(schoolId: number, body: GroupRequest, id?: number): Observable { + return this.save(`${this.base}/${schoolId}/dance-groups`, body, id); + } + saveShow(schoolId: number, body: ShowRequest, id?: number): Observable { + return this.save(`${this.base}/${schoolId}/shows`, body, id); + } + delete(kind: ResourceKind, schoolId: number, id: number): Observable { + const collection = { school: '', dancer: '/dancers', group: '/dance-groups', show: '/shows' }[ + kind + ]; + const url = + kind === 'school' ? `${this.base}/${id}` : `${this.base}/${schoolId}${collection}/${id}`; + return this.http.delete(url); + } + membership(schoolId: number, groupId: number, dancerId: number, add: boolean): Observable { + const url = `${this.base}/${schoolId}/dance-groups/${groupId}/dancers/${dancerId}`; + return add ? this.http.put(url, null) : this.http.delete(url); + } + private save(url: string, body: unknown, id?: number): Observable { + return id === undefined ? this.http.post(url, body) : this.http.put(`${url}/${id}`, body); + } +} diff --git a/agiledancers-fe/src/app/schools/school-page.html b/agiledancers-fe/src/app/schools/school-page.html new file mode 100644 index 0000000..5af2e39 --- /dev/null +++ b/agiledancers-fe/src/app/schools/school-page.html @@ -0,0 +1,423 @@ +← Alle dansscholen +@if (loading()) { +
Dansschool laden…
+} @else if (!data()) { +
+

Dansschool niet beschikbaar

+

{{ error() }}

+ +
+} @else if (data(); as context) { +
+
+ +
+

MIJN DANSSCHOOL

+

{{ context.school.name }}

+

Jouw dansers. Jouw ritme. Jouw podium.

+
+
+ +
+ + @if (notice()) { +
+ {{ notice() + }} +
+ } + @if (error()) { + + } + @switch (section()) { + @case ('overview') { + +
+
+

DE VOLGENDE STAP

+

Breng je school in beweging

+

Een goede show begint in de studio. Zorg dat iedereen een plek heeft.

+
+ + + +
+
+
+ +

IN DE SPOTLIGHT

+ @if (upcoming(); as show) { +

{{ show.name }}

+

{{ show.date | date: 'd MMMM yyyy' }}

+

Drie dagdelen. Eén gedeelde passie.

+ Shows bekijken + } @else { +

Klaar voor
het grote podium?

+

Maak een show aan en geef je dansers iets om naartoe te werken.

+ + } +
+
+
+
+

Dansschool verwijderen

+

Verwijdert ook alle dansers, groepen en shows van deze school.

+
+ +
+ } + @case ('dancers') { +
+
+

Dansers

+

De mensen die jouw school in beweging brengen.

+
+ +
+ @if (context.dancers.length) { + + } +
+ + + + + + + + + + + + @for (dancer of dancers(); track dancer.id) { + + + + + + + } @empty { + + + + } + +
+ Dansers in + {{ + context.school.name + }} +
DanserGeboortedatumDansgroepenActies
+
+ {{ dancer.name }} +
+
{{ dancer.birthDate | date: 'd MMM yyyy' }}{{ groupNames(dancer.id) }} +
+ +
+
+
+

{{ search() ? 'Geen dansers gevonden' : 'De dansvloer is nog leeg' }}

+

+ {{ + search() + ? 'Probeer een andere naam.' + : 'Voeg je eerste danser toe. Daarna kun je die aan één of meerdere groepen koppelen.' + }} +

+ @if (!search()) { + + } +
+
+
+ } + @case ('groups') { +
+
+

Dansgroepen

+

Iedere groep een eigen stijl. Iedere danser een plek.

+
+ +
+
+ @for (group of context.groups; track group.id) { +
+
+ {{ groupType(group.type) }} +
+

{{ group.category }}

+

+ {{ group.startAge }}–{{ group.endAge }} jaar · {{ group.dancerIds.length }} dansers +

+
+ @for (style of group.styles; track style) { + {{ groupStyle(style) }} + } @empty { + Nog geen dansstijlen + } +
+ +
+ +
+
+ } @empty { +
+ +

Samen vind je het ritme

+

Maak een dansgroep aan en koppel er dansers aan.

+ +
+ } +
+ } + @case ('shows') { +
+
+

Shows

+

Van maanden oefenen naar een onvergetelijk optreden.

+
+ +
+
+ @for (show of shows(); track show.id) { +
+
+ {{ show.date | date: 'dd' }}{{ show.date | date: 'MMM yyyy' }} +
+
+

{{ show.name }}

+
+ OchtendMiddagAvond +
+

Groepen indelen over dagdelen is nog niet beschikbaar.

+
+
+ +
+
+ } @empty { +
+ +

Het podium wacht op je

+

Plan een show met een ochtend-, middag- en avonddeel.

+ +
+ } +
+ } + } + @if (editor(); as target) { + + } + @if (members(); as group) { + + } + @if (deleting(); as target) { + + } +} diff --git a/agiledancers-fe/src/app/schools/school-page.ts b/agiledancers-fe/src/app/schools/school-page.ts new file mode 100644 index 0000000..9bf9b88 --- /dev/null +++ b/agiledancers-fe/src/app/schools/school-page.ts @@ -0,0 +1,162 @@ +import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router, RouterLink, RouterLinkActive } from '@angular/router'; +import { catchError, EMPTY, firstValueFrom, switchMap } from 'rxjs'; +import { + DanceGroup, + EditorTarget, + GROUP_STYLES, + GROUP_TYPES, + ResourceKind, + SchoolData, +} from '../core/api-models'; +import { apiError } from '../core/api-error'; +import { SchoolApi } from '../core/school-api'; +import { EntityEditor } from '../shared/entity-editor'; +import { ConfirmDialog } from '../shared/confirm-dialog'; +import { GroupMembers } from '../shared/group-members'; + +interface DeleteTarget { + kind: ResourceKind; + id: number; + name: string; + explanation: string; +} + +@Component({ + selector: 'app-school-page', + imports: [DatePipe, RouterLink, RouterLinkActive, EntityEditor, ConfirmDialog, GroupMembers], + templateUrl: './school-page.html', +}) +export class SchoolPage implements OnInit { + private readonly api = inject(SchoolApi); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + readonly data = signal(null); + readonly section = signal('overview'); + readonly loading = signal(true); + readonly error = signal(''); + readonly notice = signal(''); + readonly editor = signal(null); + readonly members = signal(null); + readonly deleting = signal(null); + readonly deleteBusy = signal(false); + readonly deleteError = signal(''); + readonly search = signal(''); + readonly dancers = computed(() => + [...(this.data()?.dancers ?? [])] + .filter((d) => d.name.toLocaleLowerCase('nl').includes(this.search().toLocaleLowerCase('nl'))) + .sort((a, b) => a.name.localeCompare(b.name, 'nl')), + ); + readonly shows = computed(() => + [...(this.data()?.shows ?? [])].sort((a, b) => a.date.localeCompare(b.date)), + ); + readonly today = new Date().toLocaleDateString('en-CA'); + readonly upcoming = computed(() => this.shows().find((show) => show.date >= this.today)); + readonly tabs = [ + { value: 'overview', label: 'Overzicht' }, + { value: 'dancers', label: 'Dansers' }, + { value: 'groups', label: 'Dansgroepen' }, + { value: 'shows', label: 'Shows' }, + ]; + + ngOnInit(): void { + this.route.paramMap + .pipe( + switchMap((params) => { + this.loading.set(true); + this.error.set(''); + this.search.set(''); + this.editor.set(null); + this.members.set(null); + this.deleting.set(null); + this.section.set(params.get('section') ?? 'overview'); + return this.api.schoolData(Number(params.get('schoolId'))).pipe( + catchError((error) => { + this.data.set(null); + this.error.set(apiError(error)); + this.loading.set(false); + return EMPTY; + }), + ); + }), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((data) => { + this.data.set(data); + this.loading.set(false); + }); + } + async reload(): Promise { + const id = this.data()?.school.id; + if (id === undefined) return; + this.error.set(''); + try { + const data = await firstValueFrom(this.api.schoolData(id)); + if (this.data()?.school.id === id) this.data.set(data); + } catch (error: unknown) { + this.error.set(apiError(error)); + } + } + retry(): void { + window.location.reload(); + } + async saved(): Promise { + this.editor.set(null); + this.notice.set('Wijzigingen opgeslagen.'); + await this.reload(); + } + groupType(value: string): string { + return GROUP_TYPES.find((option) => option.value === value)?.label ?? value; + } + groupStyle(value: string): string { + return GROUP_STYLES.find((option) => option.value === value)?.label ?? value; + } + groupNames(dancerId: number): string { + return ( + this.data() + ?.groups.filter((group) => group.dancerIds.includes(dancerId)) + .map((group) => group.category) + .join(', ') || 'Nog geen groep' + ); + } + askDelete(target: DeleteTarget): void { + this.deleteError.set(''); + this.deleting.set(target); + } + membershipChanged(ids: number[]): void { + const groupId = this.members()?.id; + this.data.update((data) => + data + ? { + ...data, + groups: data.groups.map((group) => + group.id === groupId ? { ...group, dancerIds: [...ids] } : group, + ), + } + : data, + ); + } + async confirmDelete(): Promise { + const target = this.deleting(); + const schoolId = this.data()?.school.id; + if (!target || schoolId === undefined || this.deleteBusy()) return; + this.deleteBusy.set(true); + this.deleteError.set(''); + try { + await firstValueFrom(this.api.delete(target.kind, schoolId, target.id)); + this.deleting.set(null); + if (target.kind === 'school') await this.router.navigate(['/schools']); + else { + this.notice.set(`${target.name} is verwijderd.`); + await this.reload(); + } + } catch (error: unknown) { + this.deleteError.set(apiError(error)); + } finally { + this.deleteBusy.set(false); + } + } +} diff --git a/agiledancers-fe/src/app/schools/schools-page.html b/agiledancers-fe/src/app/schools/schools-page.html new file mode 100644 index 0000000..cf4fdd4 --- /dev/null +++ b/agiledancers-fe/src/app/schools/schools-page.html @@ -0,0 +1,93 @@ +
+
+

JOUW ORGANISATIE

+

Alle ruimte om te dansen.

+

Van de eerste les tot het grote podium. Beheer je dansschool op één plek.

+
+ +
+@if (notice()) { +
+ {{ notice() + }} +
+} +@if (loading()) { +
Dansscholen laden…
+} @else if (error()) { + +} @else { +
+
+ Dansscholen{{ schools().length }}Een plek voor iedere danser +
+
+ Dansers{{ totalDancers() }}Samen in beweging +
+
+ Dansgroepen{{ totalGroups() }}Ieder met een eigen ritme +
+
+
+

+ Mijn dansscholen {{ schools().length }} +

+ Selecteer een school om aan de slag te gaan +
+
+ @for (school of schools(); track school.id) { + +
+ +
+

{{ school.name }}

+

Dansschool

+
+ {{ school.dancerIds.length }} dansers{{ school.danceGroupIds.length }} groepen{{ school.showIds.length }} shows +
+ +
+ } @empty { +
+ +

Hier begint jouw dansschool

+

Maak een school aan en geef je dansers, groepen en shows een thuis.

+ +
+ } +
+ +} +@if (editor()) { + +} diff --git a/agiledancers-fe/src/app/schools/schools-page.ts b/agiledancers-fe/src/app/schools/schools-page.ts new file mode 100644 index 0000000..1163950 --- /dev/null +++ b/agiledancers-fe/src/app/schools/schools-page.ts @@ -0,0 +1,55 @@ +import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { RouterLink } from '@angular/router'; +import { School } from '../core/api-models'; +import { SchoolApi } from '../core/school-api'; +import { apiError } from '../core/api-error'; +import { EntityEditor } from '../shared/entity-editor'; + +@Component({ + selector: 'app-schools-page', + imports: [RouterLink, EntityEditor], + templateUrl: './schools-page.html', +}) +export class SchoolsPage implements OnInit { + private readonly api = inject(SchoolApi); + private readonly destroyRef = inject(DestroyRef); + readonly schools = signal([]); + readonly loading = signal(true); + readonly error = signal(''); + readonly editor = signal(false); + readonly notice = signal(''); + readonly totalDancers = computed(() => + this.schools().reduce((sum, school) => sum + school.dancerIds.length, 0), + ); + readonly totalGroups = computed(() => + this.schools().reduce((sum, school) => sum + school.danceGroupIds.length, 0), + ); + ngOnInit(): void { + this.load(); + } + load(): void { + this.loading.set(true); + this.error.set(''); + this.api + .schools() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (schools) => { + this.schools.set(schools); + this.loading.set(false); + }, + error: (error) => { + this.error.set(apiError(error)); + this.loading.set(false); + }, + }); + } + created(): void { + this.editor.set(false); + this.notice.set( + 'Dansschool aangemaakt. Open de school om dansers, groepen en shows toe te voegen.', + ); + this.load(); + } +} diff --git a/agiledancers-fe/src/app/shared/confirm-dialog.ts b/agiledancers-fe/src/app/shared/confirm-dialog.ts new file mode 100644 index 0000000..02b5874 --- /dev/null +++ b/agiledancers-fe/src/app/shared/confirm-dialog.ts @@ -0,0 +1,40 @@ +import { Component, input, output } from '@angular/core'; +import { DialogFrame } from './dialog-frame'; + +@Component({ + selector: 'app-confirm-dialog', + imports: [DialogFrame], + template: ` + +

+ Weet je zeker dat je {{ name() }} wilt verwijderen? +

+

{{ explanation() }}

+ @if (error()) { + + } +
+ + +
+
+ `, +}) +export class ConfirmDialog { + readonly name = input.required(); + readonly explanation = input('Dit kan niet ongedaan worden gemaakt.'); + readonly busy = input(false); + readonly error = input(''); + readonly confirmed = output(); + readonly closed = output(); +} diff --git a/agiledancers-fe/src/app/shared/dialog-frame.ts b/agiledancers-fe/src/app/shared/dialog-frame.ts new file mode 100644 index 0000000..fdf9c6e --- /dev/null +++ b/agiledancers-fe/src/app/shared/dialog-frame.ts @@ -0,0 +1,50 @@ +import { + AfterViewInit, + Component, + ElementRef, + input, + OnDestroy, + output, + viewChild, +} from '@angular/core'; + +@Component({ + selector: 'app-dialog-frame', + template: ` + +
+
+

AGILE DANCERS

+

{{ title() }}

+
+ +
+ +
+ `, +}) +export class DialogFrame implements AfterViewInit, OnDestroy { + readonly title = input.required(); + readonly busy = input(false); + readonly dismissed = output(); + private readonly dialog = viewChild.required>('dialog'); + ngAfterViewInit(): void { + this.dialog().nativeElement.showModal(); + } + ngOnDestroy(): void { + const dialog = this.dialog().nativeElement; + if (dialog.open) dialog.close(); + } + cancel(event: Event): void { + event.preventDefault(); + if (!this.busy()) this.dismissed.emit(); + } +} diff --git a/agiledancers-fe/src/app/shared/entity-editor.spec.ts b/agiledancers-fe/src/app/shared/entity-editor.spec.ts new file mode 100644 index 0000000..a279d9a --- /dev/null +++ b/agiledancers-fe/src/app/shared/entity-editor.spec.ts @@ -0,0 +1,69 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpErrorResponse } from '@angular/common/http'; +import { of, throwError } from 'rxjs'; +import { SchoolApi } from '../core/school-api'; +import { EntityEditor } from './entity-editor'; +import { EditorTarget } from '../core/api-models'; + +describe('EntityEditor', () => { + const api = { saveSchool: vi.fn(), saveGroup: vi.fn(), saveDancer: vi.fn(), saveShow: vi.fn() }; + beforeEach(() => { + vi.resetAllMocks(); + TestBed.configureTestingModule({ + imports: [EntityEditor], + providers: [{ provide: SchoolApi, useValue: api }], + }); + }); + function editor(target: EditorTarget) { + const fixture = TestBed.createComponent(EntityEditor); + fixture.componentRef.setInput('target', target); + fixture.componentRef.setInput('schoolId', 7); + fixture.componentInstance.ngOnInit(); + return fixture.componentInstance; + } + + it('rejects blank names and invalid age ranges before calling the API', async () => { + const component = editor({ kind: 'group' }); + component.name = ' '; + expect(component.validDetails()).toBe(false); + component.name = 'Teens'; + component.startAge = 18; + component.endAge = 12; + await component.save(); + expect(api.saveGroup).not.toHaveBeenCalled(); + component.endAge = 18.5; + expect(component.validDetails()).toBe(false); + }); + + it('does not accept a future birth date', () => { + const component = editor({ kind: 'dancer' }); + component.name = 'Sam'; + component.date = '2999-01-01'; + expect(component.validDetails()).toBe(false); + }); + + it('trims the school name and emits saved only after success', async () => { + api.saveSchool.mockReturnValue(of({ id: 0 })); + const component = editor({ kind: 'school' }); + component.name = ' Studio '; + const saved = vi.fn(); + component.saved.subscribe(saved); + await component.save(); + expect(api.saveSchool).toHaveBeenCalledWith({ name: 'Studio' }, undefined); + expect(saved).toHaveBeenCalledOnce(); + expect(component.busy()).toBe(false); + }); + + it('keeps the editor open and preserves input when saving fails', async () => { + api.saveSchool.mockReturnValue(throwError(() => new HttpErrorResponse({ status: 0 }))); + const component = editor({ kind: 'school' }); + component.name = 'Studio'; + const saved = vi.fn(); + component.saved.subscribe(saved); + await component.save(); + expect(saved).not.toHaveBeenCalled(); + expect(component.name).toBe('Studio'); + expect(component.error()).toContain('niet bereikbaar'); + expect(component.busy()).toBe(false); + }); +}); diff --git a/agiledancers-fe/src/app/shared/entity-editor.ts b/agiledancers-fe/src/app/shared/entity-editor.ts new file mode 100644 index 0000000..901b5a1 --- /dev/null +++ b/agiledancers-fe/src/app/shared/entity-editor.ts @@ -0,0 +1,222 @@ +import { Component, inject, input, OnInit, output, signal } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { firstValueFrom, Observable } from 'rxjs'; +import { EditorTarget, GROUP_STYLES, GROUP_TYPES, GroupStyle, GroupType } from '../core/api-models'; +import { SchoolApi } from '../core/school-api'; +import { apiError } from '../core/api-error'; +import { DialogFrame } from './dialog-frame'; + +@Component({ + selector: 'app-entity-editor', + imports: [FormsModule, DialogFrame], + template: ` + +
+ @if (error()) { + + } + @if (target().kind === 'group') { + + +
+ + +
+
+ Dansstijlen (optioneel) +
+ @for (style of styles; track style.value) { + + } +
+
+ } @else { + + @if (target().kind === 'dancer') { + + } + @if (target().kind === 'show') { + +

+ De show krijgt automatisch een ochtend-, middag- en avonddeel. De groepsindeling volgt + later. +

+ } + } +
+ + +
+
+
+ `, +}) +export class EntityEditor implements OnInit { + readonly target = input.required(); + readonly schoolId = input(); + readonly closed = output(); + readonly saved = output(); + readonly busy = signal(false); + readonly error = signal(''); + readonly types = GROUP_TYPES; + readonly styles = GROUP_STYLES; + readonly today = new Date().toLocaleDateString('en-CA'); + private readonly api = inject(SchoolApi); + name = ''; + date = ''; + type: GroupType = 'REGULAR'; + startAge: number | null = 0; + endAge: number | null = 18; + selectedStyles: GroupStyle[] = []; + + get title(): string { + const labels = { school: 'Dansschool', dancer: 'Danser', group: 'Dansgroep', show: 'Show' }; + return `${labels[this.target().kind]} ${this.target().value ? 'bewerken' : 'toevoegen'}`; + } + ngOnInit(): void { + const target = this.target(); + if (!target.value) return; + if (target.kind === 'group') { + this.name = target.value.category; + this.type = target.value.type; + this.startAge = target.value.startAge; + this.endAge = target.value.endAge; + this.selectedStyles = [...target.value.styles]; + } else { + this.name = target.value.name; + if (target.kind === 'dancer') this.date = target.value.birthDate; + if (target.kind === 'show') this.date = target.value.date; + } + } + toggleStyle(style: GroupStyle, checked: boolean): void { + this.selectedStyles = checked + ? [...this.selectedStyles, style] + : this.selectedStyles.filter((value) => value !== style); + } + validDetails(): boolean { + if (!this.name.trim()) return false; + if (this.target().kind === 'dancer') return !!this.date && this.date <= this.today; + if (this.target().kind === 'show') return !!this.date; + if (this.target().kind === 'group') + return ( + this.startAge !== null && + this.endAge !== null && + Number.isInteger(this.startAge) && + Number.isInteger(this.endAge) && + this.startAge >= 0 && + this.endAge >= this.startAge + ); + return true; + } + async save(): Promise { + if (this.busy() || !this.validDetails()) return; + this.busy.set(true); + this.error.set(''); + const target = this.target(); + const id = target.value?.id; + const schoolId = this.schoolId(); + let request: Observable; + switch (target.kind) { + case 'school': + request = this.api.saveSchool({ name: this.name.trim() }, id); + break; + case 'dancer': + request = this.api.saveDancer( + schoolId!, + { name: this.name.trim(), birthDate: this.date }, + id, + ); + break; + case 'group': + request = this.api.saveGroup( + schoolId!, + { + category: this.name.trim(), + type: this.type, + startAge: this.startAge!, + endAge: this.endAge!, + styles: this.selectedStyles, + }, + id, + ); + break; + case 'show': + request = this.api.saveShow(schoolId!, { name: this.name.trim(), date: this.date }, id); + break; + } + try { + await firstValueFrom(request); + this.saved.emit(); + } catch (error: unknown) { + this.error.set(apiError(error)); + } finally { + this.busy.set(false); + } + } +} diff --git a/agiledancers-fe/src/app/shared/group-members.spec.ts b/agiledancers-fe/src/app/shared/group-members.spec.ts new file mode 100644 index 0000000..f66941f --- /dev/null +++ b/agiledancers-fe/src/app/shared/group-members.spec.ts @@ -0,0 +1,76 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Subject } from 'rxjs'; +import { SchoolApi } from '../core/school-api'; +import { GroupMembers } from './group-members'; + +describe('Group membership persistence', () => { + const api = { membership: vi.fn() }; + beforeEach(() => { + vi.resetAllMocks(); + TestBed.configureTestingModule({ + imports: [GroupMembers], + providers: [{ provide: SchoolApi, useValue: api }], + }); + }); + function component() { + const fixture = TestBed.createComponent(GroupMembers); + fixture.componentRef.setInput('schoolId', 7); + fixture.componentRef.setInput('group', { + id: 0, + category: 'Teens', + type: 'REGULAR', + startAge: 12, + endAge: 18, + styles: [], + dancerIds: [], + }); + fixture.componentRef.setInput('dancers', [{ id: 3, name: 'Sam', birthDate: '2010-01-01' }]); + fixture.componentInstance.ngOnInit(); + return fixture.componentInstance; + } + function change(checked: boolean): { checkbox: HTMLInputElement; event: Event } { + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = checked; + const event = new Event('change'); + Object.defineProperty(event, 'target', { value: checkbox }); + return { checkbox, event }; + } + + it('shows the pending choice but emits new membership only after the server confirms', async () => { + const request = new Subject(); + api.membership.mockReturnValue(request); + const members = component(); + const changed = vi.fn(); + members.changed.subscribe(changed); + const { checkbox, event } = change(true); + const pending = members.toggle(3, event); + expect(checkbox.checked).toBe(true); + expect(members.busy()).toBe(true); + expect(changed).not.toHaveBeenCalled(); + request.next(); + request.complete(); + await pending; + expect(changed).toHaveBeenCalledWith([3]); + expect(members.memberIds()).toEqual([3]); + expect(members.busy()).toBe(false); + }); + + it('restores the previous checkbox state when saving fails', async () => { + const request = new Subject(); + api.membership.mockReturnValue(request); + const members = component(); + const changed = vi.fn(); + members.changed.subscribe(changed); + const { checkbox, event } = change(true); + const pending = members.toggle(3, event); + request.error(new HttpErrorResponse({ status: 0 })); + await pending; + expect(checkbox.checked).toBe(false); + expect(members.memberIds()).toEqual([]); + expect(changed).not.toHaveBeenCalled(); + expect(members.error()).toContain('niet bereikbaar'); + expect(members.busy()).toBe(false); + }); +}); diff --git a/agiledancers-fe/src/app/shared/group-members.ts b/agiledancers-fe/src/app/shared/group-members.ts new file mode 100644 index 0000000..3ec7f7a --- /dev/null +++ b/agiledancers-fe/src/app/shared/group-members.ts @@ -0,0 +1,98 @@ +import { Component, computed, inject, input, OnInit, output, signal } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { DanceGroup, Dancer } from '../core/api-models'; +import { SchoolApi } from '../core/school-api'; +import { apiError } from '../core/api-error'; +import { DialogFrame } from './dialog-frame'; + +@Component({ + selector: 'app-group-members', + imports: [DialogFrame], + template: ` + +

+ Koppel dansers aan deze groep. Een danser kan in meerdere groepen zitten. Wijzigingen worden + direct opgeslagen. +

+ @if (error()) { + + } +

+ {{ + busy() + ? 'Koppeling opslaan…' + : memberIds().length + ' van ' + dancers().length + ' dansers gekoppeld' + }} +

+
+ @for (dancer of sortedDancers(); track dancer.id) { + + } @empty { +

+ Er zijn nog geen dansers. Voeg eerst een danser toe via het tabblad Dansers. +

+ } +
+
+ +
+
+ `, +}) +export class GroupMembers implements OnInit { + readonly schoolId = input.required(); + readonly group = input.required(); + readonly dancers = input.required(); + readonly closed = output(); + readonly changed = output(); + readonly memberIds = signal([]); + readonly busy = signal(false); + readonly error = signal(''); + readonly sortedDancers = computed(() => + [...this.dancers()].sort((a, b) => a.name.localeCompare(b.name, 'nl')), + ); + private readonly api = inject(SchoolApi); + ngOnInit(): void { + this.memberIds.set([...this.group().dancerIds]); + } + async toggle(dancerId: number, event: Event): Promise { + const checkbox = event.target as HTMLInputElement; + const add = checkbox.checked; + if (this.busy()) { + checkbox.checked = this.memberIds().includes(dancerId); + return; + } + this.busy.set(true); + this.error.set(''); + try { + await firstValueFrom(this.api.membership(this.schoolId(), this.group().id, dancerId, add)); + this.memberIds.update((ids) => + add ? [...ids, dancerId] : ids.filter((id) => id !== dancerId), + ); + checkbox.checked = add; + this.changed.emit(this.memberIds()); + } catch (error: unknown) { + checkbox.checked = this.memberIds().includes(dancerId); + this.error.set(apiError(error)); + } finally { + this.busy.set(false); + } + } +} diff --git a/agiledancers-fe/src/index.html b/agiledancers-fe/src/index.html new file mode 100644 index 0000000..aa1802f --- /dev/null +++ b/agiledancers-fe/src/index.html @@ -0,0 +1,13 @@ + + + + + Agile Dancers · Dansschoolbeheer + + + + + + + + diff --git a/agiledancers-fe/src/main.ts b/agiledancers-fe/src/main.ts new file mode 100644 index 0000000..190f341 --- /dev/null +++ b/agiledancers-fe/src/main.ts @@ -0,0 +1,5 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/agiledancers-fe/src/styles.css b/agiledancers-fe/src/styles.css new file mode 100644 index 0000000..2f6be6f --- /dev/null +++ b/agiledancers-fe/src/styles.css @@ -0,0 +1,1023 @@ +:root { + --ink: #202f2b; + --muted: #68766e; + --forest: #245b46; + --lime: #e0edbe; + --line: #e2e7e0; + --canvas: #f7f8f4; + font-family: 'Avenir Next', 'Segoe UI', Arial, sans-serif; + color: var(--ink); + background: var(--canvas); + font-synthesis: none; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +button, +input, +select { + font: inherit; +} +a { + color: inherit; + text-decoration: none; +} +button, +a, +input, +select { + -webkit-tap-highlight-color: transparent; +} +button { + cursor: pointer; +} +button:disabled { + cursor: not-allowed; + opacity: 0.55; +} +button, +input, +select, +a { + transition: + background-color 0.15s, + border-color 0.15s, + box-shadow 0.15s; +} +:focus-visible { + outline: 3px solid #b18539; + outline-offset: 4px; +} +h1, +h2, +h3, +p { + margin-top: 0; +} +h1 { + font-size: clamp(27px, 2.5vw, 39px); + letter-spacing: -1.5px; + font-weight: 600; + line-height: 1.2; + margin-bottom: 14px; + overflow-wrap: anywhere; +} +h2 { + font-size: 21px; + font-weight: 600; + letter-spacing: -0.6px; + line-height: 1.35; + margin-bottom: 10px; +} +h3 { + font-size: 17px; + line-height: 1.35; + font-weight: 600; + letter-spacing: -0.3px; + margin-bottom: 8px; + overflow-wrap: anywhere; +} +p { + line-height: 1.65; +} +.eyebrow { + font-size: 9px; + font-weight: 700; + letter-spacing: 2px; + color: var(--forest); + margin-bottom: 15px; +} +.lede { + font-size: 13px; + color: var(--muted); + margin-bottom: 0; +} +.muted, +.form-hint { + color: var(--muted); + font-size: 12px; +} +.nowrap { + white-space: nowrap; +} +.page-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 26px; + margin-bottom: 36px; +} +.page-heading > .button { + flex-shrink: 0; +} +.button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + border: 1px solid transparent; + border-radius: 7px; + padding: 12px 18px; + font-size: 12px; + font-weight: 650; + line-height: 1.3; + min-height: 42px; +} +.primary { + background: var(--forest); + color: white; + border-color: var(--forest); +} +.primary:hover:not(:disabled) { + background: #194633; +} +.secondary { + background: white; + border-color: #d6ded6; + color: var(--ink); +} +.secondary:hover:not(:disabled) { + background: #f1f5ee; + border-color: #8fa693; +} +.light { + background: #e0edbe; + color: #234b38; +} +.light:hover { + background: #ebf4d6; +} +.danger { + background: #a53535; + color: white; +} +.danger:hover:not(:disabled) { + background: #872828; +} +.danger-outline { + color: #9b3e3e; + border-color: #e5c7c7; + background: transparent; +} +.danger-outline:hover { + background: #fff2f0; +} +.text-button { + border: 0; + background: none; + color: var(--forest); + font-size: 12px; + font-weight: 650; + padding: 10px 6px; +} +.text-button:hover { + text-decoration: underline; +} +.destructive { + color: #9b4444; +} +.icon-button { + background: #f1f4ee; + width: 34px; + height: 34px; + font-size: 24px; + border: none; + border-radius: 50%; + flex-shrink: 0; + color: var(--ink); +} +.full-width { + width: 100%; +} +.stats-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; + margin-bottom: 38px; +} +.stat { + background: #fff; + border: 1px solid var(--line); + border-radius: 10px; + padding: 22px 24px; + display: flex; + flex-direction: column; +} +a.stat:hover { + border-color: #8ea48f; +} +.stat-label { + font-size: 12px; + font-weight: 600; + display: flex; + justify-content: space-between; +} +.stat-label > span { + color: var(--muted); +} +.stat > strong { + font-size: 39px; + line-height: 1.3; + font-weight: 500; + letter-spacing: -1.5px; + margin: 8px 0; +} +.stat-caption { + font-size: 10px; + color: var(--muted); +} +.section-heading { + display: flex; + justify-content: space-between; + align-items: center; + gap: 18px; + margin: 8px 0 22px; +} +.section-heading h2 { + margin-bottom: 0; + display: flex; + align-items: center; + gap: 10px; +} +.section-heading p { + margin: 7px 0 0; +} +.section-heading > .muted { + font-size: 10px; +} +.count-badge { + background: #e6ece2; + color: var(--forest); + font-size: 10px; + letter-spacing: 0; + padding: 4px 8px; + border-radius: 5px; +} +.school-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 255px), 1fr)); + gap: 20px; +} +.school-card, +.group-card { + min-width: 0; + background: #fff; + padding: 24px; + border: 1px solid var(--line); + border-radius: 10px; +} +.school-card:hover { + border-color: #8ea48f; + box-shadow: 0 5px 18px #243e2910; +} +.card-top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + gap: 12px; +} +.avatar { + background: #e9eddc; + color: var(--forest); + border-radius: 12px; + width: 48px; + height: 48px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 24px; + font-weight: 600; + flex-shrink: 0; +} +.avatar.small { + width: 32px; + height: 32px; + font-size: 14px; + border-radius: 50%; +} +.avatar.large { + width: 68px; + height: 68px; + font-size: 32px; + border-radius: 18px; +} +.round-arrow { + background: #f6f8f3; + border: 1px solid var(--line); + border-radius: 50%; + width: 28px; + height: 28px; + display: grid; + place-items: center; + font-size: 15px; +} +.school-card > p { + color: var(--muted); + font-size: 11px; + margin-bottom: 26px; +} +.card-meta { + display: flex; + flex-wrap: wrap; + gap: 16px; + font-size: 10px; + color: var(--muted); + padding-bottom: 20px; +} +.card-meta strong { + color: var(--ink); + font-weight: 650; +} +.card-link { + padding-top: 16px; + border-top: 1px solid var(--line); + font-size: 11px; + font-weight: 650; + color: var(--forest); + display: flex; + justify-content: space-between; +} +.tip-panel { + display: flex; + align-items: center; + gap: 22px; + background: #edf0e4; + border-radius: 10px; + padding: 24px 28px; + margin-top: 32px; +} +.tip-panel h3 { + font-size: 14px; +} +.tip-panel p { + color: #66735e; + font-size: 11px; + margin: 0; + max-width: 650px; +} +.tip-icon { + font-size: 34px; + color: #647d39; +} +.empty-state { + text-align: center; + background: #fff; + border: 1px dashed #cdd8c9; + border-radius: 10px; + padding: 52px 24px; +} +.empty-state.full-width { + grid-column: 1 / -1; +} +.empty-state p { + max-width: 420px; + margin: 0 auto 22px; + font-size: 13px; + color: var(--muted); +} +.empty-symbol { + width: 64px; + height: 64px; + border-radius: 50%; + background: #edf1e3; + color: #597451; + display: grid; + place-items: center; + font-size: 42px; + margin: 0 auto 22px; +} +.loading-state { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + min-height: 280px; + color: var(--muted); + font-size: 14px; +} +.spinner { + width: 20px; + height: 20px; + border: 2px solid #d6e1d5; + border-top-color: var(--forest); + border-radius: 50%; + animation: spin 1s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.alert { + border-radius: 8px; + padding: 15px 18px; + font-size: 13px; + line-height: 1.6; + margin: 0 0 22px; +} +.alert p:last-child { + margin-bottom: 0; +} +.alert.error { + background: #fff0ed; + color: #88342b; + border: 1px solid #edc9c1; +} +.alert.success { + display: flex; + justify-content: space-between; + gap: 18px; + background: #e9f3e5; + color: #315d36; + border: 1px solid #cfdfc8; +} +.alert.success > button { + color: inherit; + background: transparent; + border: none; + font-size: 20px; +} +.back-link { + display: inline-block; + color: var(--muted); + font-size: 11px; + margin-bottom: 28px; +} +.back-link:hover { + color: var(--forest); + text-decoration: underline; +} +.school-title { + display: flex; + align-items: center; + gap: 22px; + min-width: 0; +} +.school-title .eyebrow { + margin-bottom: 8px; +} +.school-title h1 { + margin-bottom: 8px; +} +.tabs { + display: flex; + gap: 26px; + border-bottom: 1px solid #dce3d8; + margin-bottom: 30px; + overflow-x: auto; +} +.tabs > a { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 2px; + white-space: nowrap; + border-bottom: 2px solid transparent; + font-size: 12px; + font-weight: 550; + color: var(--muted); +} +.tabs > a.active { + color: var(--forest); + border-bottom-color: var(--forest); + font-weight: 700; +} +.tabs > a > span { + background: #e8ede4; + padding: 2px 6px; + border-radius: 5px; + font-size: 9px; +} +.overview-grid { + display: grid; + grid-template-columns: 1.3fr 1fr; + gap: 24px; +} +.panel { + background: white; + border: 1px solid var(--line); + border-radius: 10px; + padding: 28px; +} +.panel > .muted { + max-width: 350px; +} +.quick-actions { + margin-top: 22px; +} +.quick-actions button { + display: flex; + gap: 16px; + align-items: center; + text-align: left; + border: 0; + border-top: 1px solid var(--line); + background: white; + width: 100%; + padding: 18px 0; + color: var(--ink); +} +.quick-actions button:last-child { + padding-bottom: 0; +} +.quick-actions button:hover strong { + color: var(--forest); + text-decoration: underline; +} +.step-number { + font-size: 11px; + background: #f1f4ec; + color: #718663; + border-radius: 8px; + display: grid; + place-items: center; + width: 34px; + height: 34px; + flex-shrink: 0; +} +.quick-actions strong { + display: block; + font-size: 12px; + margin-bottom: 5px; + font-weight: 650; +} +.quick-actions small { + font-size: 10px; + color: var(--muted); +} +.quick-actions button > span:last-child { + margin-left: auto; + color: var(--forest); +} +.show-spotlight { + background: #254f40; + color: white; + border-radius: 10px; + padding: 30px; + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + align-items: flex-start; +} +.show-spotlight .eyebrow { + color: #cedcba; + position: relative; +} +.show-spotlight h2 { + font-size: 30px; + margin-top: 26px; + line-height: 1.2; + letter-spacing: -1px; + position: relative; +} +.show-spotlight p:not(.eyebrow) { + color: #d5e0d2; + font-size: 12px; + max-width: 260px; + position: relative; +} +.show-spotlight .button { + margin-top: auto; + position: relative; +} +.spotlight-decoration { + position: absolute; + color: #3d6650; + right: -20px; + top: 5px; + font-size: 200px; + line-height: 1; + transform: rotate(16deg); +} +.danger-zone { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + padding-top: 28px; + margin-top: 44px; + border-top: 1px solid var(--line); +} +.danger-zone h3 { + font-size: 12px; +} +.danger-zone p { + color: var(--muted); + font-size: 10px; + margin: 0; +} +.search-field { + display: flex; + align-items: center; + gap: 12px; + max-width: 320px; + margin-bottom: 20px; +} +.search-field > span { + font-size: 26px; + color: var(--muted); +} +.search-field input { + margin: 0; +} +.table-container { + overflow-x: auto; + border-radius: 10px; + border: 1px solid var(--line); + background: white; +} +table { + border-collapse: collapse; + width: 100%; + font-size: 12px; +} +th { + font-size: 10px; + letter-spacing: 0.6px; + text-transform: uppercase; + color: var(--muted); + background: #f1f4ed; + text-align: left; + padding: 16px 20px; + font-weight: 650; +} +td { + border-top: 1px solid var(--line); + padding: 18px 20px; +} +td .empty-state { + border: none; +} +.person { + display: flex; + gap: 10px; + align-items: center; + min-width: 145px; +} +.person > strong { + font-weight: 600; +} +.row-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; +} +.group-symbol { + background: #eef1df; + border-radius: 10px; + width: 36px; + height: 36px; + color: #637739; + display: grid; + place-items: center; + font-size: 23px; +} +.tag { + background: #eaf2e7; + color: #486e3d; + font-size: 9px; + font-weight: 650; + padding: 6px 9px; + border-radius: 5px; +} +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 14px 0 24px; +} +.style-tag { + background: #f0f2ec; + color: #56644e; + border-radius: 4px; + padding: 5px 8px; + font-size: 10px; +} +.card-bottom { + display: flex; + justify-content: space-between; + padding-top: 14px; + margin-top: 16px; + border-top: 1px solid var(--line); +} +.group-card > .muted { + margin-bottom: 8px; +} +.show-list { + display: grid; + gap: 16px; +} +.show-card { + display: flex; + gap: 24px; + align-items: center; + background: #fff; + border: 1px solid var(--line); + border-radius: 10px; + padding: 26px; +} +.date-tile { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 84px; + min-height: 90px; + background: #eef1e1; + border-radius: 9px; + flex-shrink: 0; + color: var(--forest); +} +.date-tile > strong { + font-size: 34px; + line-height: 1.2; + font-weight: 550; +} +.date-tile > span { + font-size: 10px; + margin-top: 5px; +} +.show-details { + flex: 1; + min-width: 0; +} +.show-details .tag-list { + margin: 12px 0; +} +.show-details p { + margin: 0; + font-size: 10px; +} +dialog { + border: 1px solid var(--line); + border-radius: 14px; + padding: 28px; + width: min(520px, calc(100% - 32px)); + max-height: calc(100dvh - 48px); + color: var(--ink); + box-shadow: 0 18px 80px #13291f30; +} +dialog::backdrop { + background: #14241e66; + backdrop-filter: blur(3px); +} +.dialog-heading { + display: flex; + justify-content: space-between; + gap: 20px; + align-items: flex-start; + margin-bottom: 24px; +} +.dialog-heading .eyebrow { + margin-bottom: 8px; + font-size: 8px; +} +.dialog-heading h2 { + margin: 0; +} +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 28px; + padding-top: 20px; + border-top: 1px solid var(--line); +} +form > label { + display: block; +} +label { + font-size: 12px; + font-weight: 600; +} +input:not([type='checkbox']), +select { + background: #fff; + color: var(--ink); + border: 1px solid #cfd8cc; + border-radius: 6px; + display: block; + width: 100%; + padding: 11px 12px; + min-height: 43px; + margin: 8px 0 20px; + font-size: 13px; + font-weight: 400; +} +input:not([type='checkbox']):focus, +select:focus { + border-color: var(--forest); +} +input.ng-invalid.ng-touched { + border-color: #b64f41; +} +input[type='checkbox'] { + accent-color: var(--forest); + width: 17px; + height: 17px; + flex-shrink: 0; +} +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} +fieldset { + margin: 0; + padding: 0; + border: 0; +} +legend { + font-size: 12px; + font-weight: 600; + margin-bottom: 12px; +} +.checkbox-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} +.checkbox { + display: flex; + gap: 8px; + align-items: center; + font-weight: 400; +} +.form-hint { + background: #f1f4ec; + padding: 12px; + border-radius: 6px; + font-size: 11px; +} +.member-list { + max-height: 340px; + overflow-y: auto; +} +.member-row { + display: flex; + gap: 12px; + align-items: center; + padding: 14px 2px; + border-top: 1px solid var(--line); +} +.member-row input { + margin-left: auto; +} +.member-count { + font-size: 11px; + color: var(--forest); + font-weight: 600; + margin-top: 20px; +} +.empty-inline { + color: var(--muted); + font-size: 13px; + padding: 20px 0; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} +.skip-link { + position: fixed; + left: 16px; + top: -100px; + z-index: 100; + background: white; + padding: 14px 20px; + border: 2px solid var(--forest); +} +.skip-link:focus { + top: 16px; +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation: none !important; + transition: none !important; + } +} +@media (max-width: 1100px) { + .page-heading { + align-items: flex-start; + } + .page-heading:not(.school-heading) { + flex-direction: column; + gap: 20px; + } + .stat { + padding: 18px; + } + .overview-grid { + grid-template-columns: 1fr; + } + .show-spotlight { + min-height: 300px; + } + .show-spotlight h2 { + margin-top: 12px; + } + .show-card { + flex-wrap: wrap; + } + .show-card .row-actions { + margin-left: auto; + } +} +@media (max-width: 600px) { + h1 { + font-size: 29px; + } + .stats-grid { + gap: 9px; + margin-bottom: 28px; + } + .stat { + padding: 14px 12px; + } + .stat-label { + font-size: 10px; + } + .stat > strong { + font-size: 29px; + } + .stat-caption { + font-size: 9px; + line-height: 1.5; + } + .section-heading { + align-items: flex-start; + flex-direction: column; + gap: 14px; + } + .section-heading > .muted { + display: none; + } + .school-heading { + flex-direction: column; + } + .school-title { + gap: 14px; + } + .avatar.large { + width: 50px; + height: 50px; + font-size: 25px; + } + .school-title h1 { + font-size: 26px; + } + .tabs { + gap: 22px; + } + .tabs > a { + font-size: 11px; + } + .tip-panel { + padding: 22px; + align-items: flex-start; + } + .danger-zone { + flex-direction: column; + align-items: flex-start; + } + .panel { + padding: 22px; + } + .show-card { + padding: 20px; + gap: 18px; + } + .date-tile { + width: 65px; + min-height: 80px; + } + .show-details { + flex-basis: calc(100% - 83px); + } + .show-card .row-actions { + width: 100%; + border-top: 1px solid var(--line); + padding-top: 8px; + } + dialog { + padding: 24px 20px; + } + .dialog-actions .button { + padding: 12px 14px; + } +} diff --git a/agiledancers-fe/tsconfig.app.json b/agiledancers-fe/tsconfig.app.json new file mode 100644 index 0000000..cb151e1 --- /dev/null +++ b/agiledancers-fe/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/agiledancers-fe/tsconfig.json b/agiledancers-fe/tsconfig.json new file mode 100644 index 0000000..96ffb9c --- /dev/null +++ b/agiledancers-fe/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve", + "strict": true + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/agiledancers-fe/tsconfig.spec.json b/agiledancers-fe/tsconfig.spec.json new file mode 100644 index 0000000..9c8efb9 --- /dev/null +++ b/agiledancers-fe/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +} From bd497aefd5e5f939c6f3d50e66aa2f150558b5bc Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Wed, 28 Oct 2026 07:26:47 +0100 Subject: [PATCH 07/13] Spec driven development setup --- AGENTS.md | 12 ++ skills/LICENSE.mattpocock | 21 ++++ skills/README.md | 22 ++++ skills/grill-me/SKILL.md | 7 ++ skills/grill-me/agents/openai.yaml | 5 + skills/grilling/SKILL.md | 30 +++++ skills/grilling/agents/openai.yaml | 3 + skills/upstream.json | 9 ++ specs/README.md | 4 + specs/S1_School-JSON-import-export.md | 160 ++++++++++++++++++++++++++ 10 files changed, 273 insertions(+) create mode 100644 AGENTS.md create mode 100644 skills/LICENSE.mattpocock create mode 100644 skills/README.md create mode 100644 skills/grill-me/SKILL.md create mode 100644 skills/grill-me/agents/openai.yaml create mode 100644 skills/grilling/SKILL.md create mode 100644 skills/grilling/agents/openai.yaml create mode 100644 skills/upstream.json create mode 100644 specs/README.md create mode 100644 specs/S1_School-JSON-import-export.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..546b2e4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,12 @@ +# Project skills and specifications + +- Project skills are stored in `skills/`; specifications belong in `specs/`. +- Use `grill-me` only when the user explicitly requests it, for example + `Use grill-me to review this plan` or `Gebruik grill-me voor dit ontwerp`. +- Before using it, read `skills/grill-me/SKILL.md` and its dependency + `skills/grilling/SKILL.md`. If no dedicated Skill tool is available, load these + files directly and follow their instructions within the available tools. +- Installing or mentioning a skill does not start an interview. Do not apply + `grilling` automatically to ordinary development requests. +- Keep the upstream skill files unchanged. Source revision and license are + recorded in `skills/upstream.json` and `skills/LICENSE.mattpocock`. diff --git a/skills/LICENSE.mattpocock b/skills/LICENSE.mattpocock new file mode 100644 index 0000000..f1dd2c0 --- /dev/null +++ b/skills/LICENSE.mattpocock @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Matt Pocock + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..d747095 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,22 @@ +# Project skills + +Reusable agent instructions for this repository. Invoke them explicitly in the +conversation, for example: + +> Gebruik grill-me om de JSON-import en -export met mij uit te werken. + +## Installed skills + +| Skill | Purpose | +| --- | --- | +| [grill-me](grill-me/SKILL.md) | Entry point for reviewing a plan or design through questions. | +| [grilling](grilling/SKILL.md) | Interview instructions required by `grill-me`. | + +The skills are copied from [Matt Pocock's skills.sh listing](https://skills.sh/mattpocock/skills/grill-me) +and its [upstream repository](https://github.com/mattpocock/skills). +The exact upstream commit and paths are recorded in [upstream.json](upstream.json). +The upstream [MIT license](LICENSE.mattpocock) is included. + +These project-local files are available to the agent from the next conversation +turn through the instructions in the root `AGENTS.md`. They are not installed +globally. Updating them requires explicitly downloading a new upstream revision. diff --git a/skills/grill-me/SKILL.md b/skills/grill-me/SKILL.md new file mode 100644 index 0000000..3947ff9 --- /dev/null +++ b/skills/grill-me/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-me +description: A relentless interview to sharpen a plan or design. +disable-model-invocation: true +--- + +Call the Skill tool with "grilling". diff --git a/skills/grill-me/agents/openai.yaml b/skills/grill-me/agents/openai.yaml new file mode 100644 index 0000000..4d6fb0c --- /dev/null +++ b/skills/grill-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill Me" + short_description: "Sharpen a plan through interview" +policy: + allow_implicit_invocation: false diff --git a/skills/grilling/SKILL.md b/skills/grilling/SKILL.md new file mode 100644 index 0000000..df69d99 --- /dev/null +++ b/skills/grilling/SKILL.md @@ -0,0 +1,30 @@ +--- +name: grilling +description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. +--- + +Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it. + +Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round. + +Format a round like so: + +``` +❓ **Q1** - ****: + +➡️ + +--- + +❓ **Q2** - ****: + +➡️ +``` + +Word each question so "yes" accepts your recommended answer. + +Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one. + +Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait. + +The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding. diff --git a/skills/grilling/agents/openai.yaml b/skills/grilling/agents/openai.yaml new file mode 100644 index 0000000..ddbdb96 --- /dev/null +++ b/skills/grilling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Grilling" + short_description: "Stress-test thinking a round of questions at a time" diff --git a/skills/upstream.json b/skills/upstream.json new file mode 100644 index 0000000..d3781de --- /dev/null +++ b/skills/upstream.json @@ -0,0 +1,9 @@ +{ + "repository": "https://github.com/mattpocock/skills", + "commit": "59db609eb1e18aaf59b63284d0e52255aff0c63c", + "source": "https://skills.sh/mattpocock/skills/grill-me", + "skills": { + "grill-me": "skills/productivity/grill-me", + "grilling": "skills/productivity/grilling" + } +} diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 0000000..2d96b97 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,4 @@ +# Specifications + +Store feature specifications, requirements and agreed design decisions here. +Use descriptive Markdown filenames, for example `school-json-import-export.md`. diff --git a/specs/S1_School-JSON-import-export.md b/specs/S1_School-JSON-import-export.md new file mode 100644 index 0000000..0b8fba6 --- /dev/null +++ b/specs/S1_School-JSON-import-export.md @@ -0,0 +1,160 @@ +# S1 — School JSON import and export + +Status: agreed after the grill-me review. + +## User story + +**As** a school maintainer, +**I want** to export and import my dance school as a JSON file, +**so that** I can store my school configuration locally while the application does +not offer persistent storage, +**and so that** I retain ownership of my data. + +## Intent + +Export downloads a complete snapshot of one dance school to a local JSON file. +Import restores that snapshot as a new school or replaces the corresponding +existing school after explicit confirmation. + +## Scope + +### In scope + +- Exporting an existing school to a readable JSON file. +- Importing a school that does not exist or no longer exists. +- Replacing an existing school with the complete imported snapshot. +- Preserving all configured school data and relationships, including: + - School details and its persistent unique key. + - Dancers and their details. + - Dance groups, types, age ranges and styles. + - Dancer memberships, including dancers belonging to multiple groups. + - Shows, dates, day parts and any existing group assignments to day parts. + - Coaches and any existing links between coaches and groups. +- Full validation, an import preview, confirmation and clear error messages. + +Preserving existing data does not add management screens for coaches or the +ability to assign groups to show day parts. + +### Out of scope + +- Merging an imported snapshot with an existing school. +- Persistent server-side storage. + +The existing-school prompt includes a merge option, but selecting it only displays +that merging is not yet supported. It must not change any data. Implementing merge +behavior requires a separate specification. + +## Identity and file format + +- Every school has a persistent unique key that is included in exports and + preserved through imports and subsequent exports. +- Import identifies an existing school by that key, not by its name or a + runtime-generated numeric ID. The key remains meaningful after a server restart. +- A school with the same name but a different unique key is a separate school. +- A file contains one school and all data needed to restore it without accessing + another school or the original server state. +- The readable, documented JSON format includes a `formatVersion` field. +- Users may edit the JSON manually. Edited files undergo the same validation as + files produced by the application. +- Unknown fields and unsupported format versions are rejected explicitly; data + must not be silently ignored. +- Relationships in the file must be unambiguous and restored correctly. Internal + numeric IDs need not remain identical, provided school identity and all + relationships are preserved without collisions with other schools. + +## Export flow + +1. The maintainer opens an existing school's management screen. +2. The maintainer selects the JSON export option. +3. The application downloads a JSON file containing a consistent, complete + snapshot of that school. +4. Export does not change the school or any other application data. + +## Import flow + +1. The maintainer selects the JSON import option and chooses a local file. +2. The application validates the entire file before changing any stored data. +3. For a valid file, the application shows a preview containing the school name + and the numbers of dancers, dance groups and shows. +4. If the unique school key is unknown, the maintainer confirms adding the school + or cancels the import. +5. If the key already exists, the application identifies the school that would be + replaced and offers replace, merge or cancel: + - **Replace:** explicitly warn that the existing school is replaced completely. + After confirmation, replace all its configured data with the snapshot. + - **Merge:** show a message that this functionality is not yet supported and + leave all data unchanged. + - **Cancel:** leave all data unchanged. +6. A confirmed import is committed as one operation. The application must not + expose a partially imported or partially replaced school. +7. If validation or importing fails, show **"Import niet gelukt"** together with + the concrete cause. The user can correct the file and try again. + +## Validation and failure behavior + +- Validate JSON syntax, the supported file structure and format version, required + data, field values and all relationships before applying the import. +- Reject unknown fields and references to missing objects, including a group's + reference to a dancer absent from the snapshot. +- A failure must not create a partial school, partially replace an existing + school, or modify another school. +- A replacement must not remove the original school until the complete + replacement can succeed. If it fails, the original school remains intact. +- Error messages explain the cause specifically enough to correct the file. + For example: **"Import niet gelukt: groep Hiphop verwijst naar een onbekende + danser."** + +## Acceptance criteria + +### Export + +1. Each existing school offers a JSON-file export that can be downloaded and + stored locally. +2. The export includes its persistent unique key, `formatVersion`, all configured + data listed in scope and all relationships between those objects. +3. Exporting leaves the application data unchanged. +4. A school with no dancers, groups or shows can be exported and imported. + +### Adding a school + +5. A valid file with an unknown school key shows the preview before any data is + added. Confirming creates the complete school; cancelling changes nothing. +6. An exported school can be restored after the original school is deleted or + the in-memory server state is lost. +7. An existing school with the same name but a different key is neither replaced + nor treated as the same school. + +### Replacing a school + +8. A valid file with an existing school key prompts the user to replace, merge or + cancel and clearly identifies the existing school. +9. Confirming replacement replaces the complete configuration. Objects and + relationships present only in the previous configuration are not retained as + part of the replaced school. +10. Other schools remain unchanged by replacement. +11. Selecting merge displays that merging is not yet supported and changes + nothing. Cancelling also changes nothing. + +### Validation and errors + +12. Malformed JSON, unsupported format versions, unknown fields, missing required + data, invalid field values and unresolved relationships cause the entire + import to fail. +13. Each failed import displays **"Import niet gelukt"** and a concrete cause. +14. A failed new-school import adds nothing. A failed replacement preserves the + entire existing school. Other schools are unaffected in either case. +15. After a failure, the user can select a corrected file and retry. + +### Round-trip integrity + +16. Exporting and importing a school preserves every configured field and + relationship, including a dancer's membership in multiple groups, shows and + their day parts, and any existing coaches or day-part group assignments. +17. Exporting the restored school again preserves its unique school key and + equivalent configuration, even if internal numeric IDs changed. + +## Implementation handoff + +This specification records agreed behavior. Backend endpoints, the detailed JSON +schema and frontend implementation remain implementation work; this review does +not implement the feature. From e3e362c2ac30d6b365f752f3da6c8b0250c019db Mon Sep 17 00:00:00 2001 From: Wanja Krah Date: Wed, 28 Oct 2026 08:07:42 +0100 Subject: [PATCH 08/13] import export functionality added --- .../agiledancers/model/DanceSchool.java | 2 + .../agilecc/agiledancers/model/DayPart.java | 6 +- .../repository/AbstractRepository.java | 6 +- .../rest/SchoolSnapshotResource.java | 43 +++++ .../service/SchoolManagementService.java | 55 +++++++ .../agiledancers/service/SchoolSnapshot.java | 96 +++++++++++ .../SchoolSnapshotResourceTest.java | 32 ++++ .../agiledancers/SchoolSnapshotTest.java | 70 ++++++++ .../src/app/schools/school-page.html | 1 + .../src/app/schools/school-page.ts | 11 +- .../src/app/schools/schools-page.html | 1 + .../src/app/schools/schools-page.ts | 3 +- .../src/app/shared/school-transfer.spec.ts | 86 ++++++++++ .../src/app/shared/school-transfer.ts | 152 ++++++++++++++++++ specs/School-JSON-format.md | 49 ++++++ .../S1_School-JSON-import-export.md | 0 16 files changed, 609 insertions(+), 4 deletions(-) create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/SchoolSnapshotResource.java create mode 100644 agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolSnapshot.java create mode 100644 agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotResourceTest.java create mode 100644 agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotTest.java create mode 100644 agiledancers-fe/src/app/shared/school-transfer.spec.ts create mode 100644 agiledancers-fe/src/app/shared/school-transfer.ts create mode 100644 specs/School-JSON-format.md rename specs/{ => review}/S1_School-JSON-import-export.md (100%) diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java index b5b5dae..2358db3 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DanceSchool.java @@ -10,6 +10,8 @@ import java.util.LinkedHashSet; @Builder @ToString(onlyExplicitlyIncluded = true) public class DanceSchool extends AbstractEntity { + @Builder.Default + private final java.util.UUID key = java.util.UUID.randomUUID(); @Setter private String name; @Builder.Default diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java index 6503d82..1cef8c1 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/model/DayPart.java @@ -13,5 +13,9 @@ public class DayPart extends AbstractEntity { @ToString.Include private final DayPartType type; @ToString.Include - private final List danceGroups; + private List danceGroups; + + public void removeDanceGroup(DanceGroup group) { + danceGroups = danceGroups.stream().filter(value -> value != group).toList(); + } } diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java index bd16561..5c26353 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/repository/AbstractRepository.java @@ -9,9 +9,13 @@ public abstract class AbstractRepository { protected Map entities = new LinkedHashMap<>(); + public long nextId() { + return sequence++; + } + public T add(T entity) { if (entity.getId() == null) { - entity.setId(sequence++); + entity.setId(nextId()); } entities.put(entity.getId(), entity); return entity; diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/SchoolSnapshotResource.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/SchoolSnapshotResource.java new file mode 100644 index 0000000..e797eb0 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/rest/SchoolSnapshotResource.java @@ -0,0 +1,43 @@ +package nl.agilecc.agiledancers.rest; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.core.JsonParser; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.*; +import nl.agilecc.agiledancers.service.*; +import nl.agilecc.agiledancers.rest.apimodel.SchoolView; + +@Path("/api/dance-schools") +@Produces(MediaType.APPLICATION_JSON) +public class SchoolSnapshotResource { + @Inject SchoolManagementService service; + @Inject ObjectMapper mapper; + + @GET @Path("/{id}/export") + public Response export(@PathParam("id") long id) throws Exception { + String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(service.exportSchool(id)); + return Response.ok(json).header("Content-Disposition", "attachment; filename=school-" + id + ".json").build(); + } + @POST @Path("/import/preview") @Consumes(MediaType.APPLICATION_JSON) + public SchoolManagementService.ImportPreview preview(String json) { return service.preview(parse(json)); } + + @POST @Path("/import") @Consumes(MediaType.APPLICATION_JSON) + public SchoolView apply(String json, @QueryParam("replaceId") Long replaceId) { + return SchoolView.of(service.importSchool(parse(json), replaceId)); + } + private SchoolSnapshot parse(String json) { + try { + SchoolSnapshot snapshot = mapper.copy().enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS) + .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS) + .disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT) + .enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION) + .readValue(json, SchoolSnapshot.class); + if (snapshot == null) throw new IllegalArgumentException("Het bestand bevat geen school"); + return snapshot; + } catch (Exception e) { + throw new ManagementException(ManagementException.Kind.INVALID_INPUT, "Ongeldig JSON-bestand: " + e.getMessage()); + } + } +} diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java index 1dd8a17..f6401f1 100644 --- a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolManagementService.java @@ -106,6 +106,7 @@ public class SchoolManagementService { public synchronized void deleteGroup(long schoolId, long groupId) { DanceGroup group = group(schoolId, groupId); + school(schoolId).getShows().forEach(show -> show.getDayParts().forEach(part -> part.removeDanceGroup(group))); school(schoolId).removeDanceGroup(group); groups.delete(groupId); } @@ -150,6 +151,60 @@ public class SchoolManagementService { shows.delete(showId); } + public record ImportPreview(String name, int dancers, int groups, int shows, Long existingId, String existingName) {} + + public synchronized SchoolSnapshot exportSchool(long id) { + return SchoolSnapshot.of(school(id)); + } + + public synchronized ImportPreview preview(SchoolSnapshot snapshot) { + snapshot.restore(); + DanceSchool existing = schools.findAll().stream().filter(s -> s.getKey().equals(snapshot.key())).findFirst().orElse(null); + return new ImportPreview(snapshot.name(), snapshot.dancers().size(), snapshot.groups().size(), snapshot.shows().size(), + existing == null ? null : existing.getId(), existing == null ? null : existing.getName()); + } + + public synchronized DanceSchool importSchool(SchoolSnapshot snapshot, Long replaceId) { + DanceSchool staged = snapshot.restore(); + ImportPreview preview = preview(snapshot); + if (!Objects.equals(preview.existingId(), replaceId)) { + throw new ManagementException(INVALID_INPUT, "De bestaande school is gewijzigd. Bekijk de import opnieuw."); + } + // All validation and construction finish before changing repository state. + // Rebuild sets after assigning runtime IDs: entity hash codes include the ID. + List importedDancers = new ArrayList<>(staged.getDancers()); + List importedGroups = new ArrayList<>(staged.getDanceGroups()); + List importedShows = new ArrayList<>(staged.getShows()); + Map> memberships = new IdentityHashMap<>(); + importedGroups.forEach(g -> memberships.put(g, new ArrayList<>(g.getDancers()))); + importedDancers.forEach(d -> d.setId(dancers.nextId())); + importedGroups.forEach(g -> { + g.setId(groups.nextId()); + }); + List rebuiltGroups = new ArrayList<>(); + Map replacements = new IdentityHashMap<>(); + for (DanceGroup g : importedGroups) { + DanceGroup rebuilt = DanceGroup.builder().category(g.getCategory()).type(g.getType()) + .startAge(g.getStartAge()).endAge(g.getEndAge()).styles(new LinkedHashSet<>(g.getStyles())) + .coach(g.getCoach()).dancers(new LinkedHashSet<>(memberships.get(g))).build(); + rebuilt.setId(g.getId()); rebuiltGroups.add(rebuilt); replacements.put(g, rebuilt); + } + List rebuiltShows = new ArrayList<>(); + for (Show show : importedShows) { + Show rebuilt = Show.builder().name(show.getName()).date(show.getDate()).dayParts(show.getDayParts().stream() + .map(p -> DayPart.builder().type(p.getType()).danceGroups(p.getDanceGroups().stream().map(replacements::get).toList()).build()).toList()).build(); + rebuilt.setId(shows.nextId()); rebuiltShows.add(rebuilt); + } + DanceSchool imported = DanceSchool.builder().key(staged.getKey()).name(staged.getName()) + .dancers(new LinkedHashSet<>(importedDancers)).danceGroups(new LinkedHashSet<>(rebuiltGroups)) + .shows(new LinkedHashSet<>(rebuiltShows)).coaches(new LinkedHashSet<>(staged.getCoaches())).build(); + if (replaceId != null) { deleteSchool(replaceId); imported.setId(replaceId); } + importedDancers.forEach(dancers::add); + rebuiltGroups.forEach(groups::add); + rebuiltShows.forEach(shows::add); + return schools.add(imported); + } + private static void validateAgeRange(int startAge, int endAge) { if (startAge > endAge) { throw new ManagementException(INVALID_INPUT, "startAge must not exceed endAge"); diff --git a/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolSnapshot.java b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolSnapshot.java new file mode 100644 index 0000000..37d1755 --- /dev/null +++ b/agiledancers-be/src/main/java/nl/agilecc/agiledancers/service/SchoolSnapshot.java @@ -0,0 +1,96 @@ +package nl.agilecc.agiledancers.service; + +import nl.agilecc.agiledancers.model.*; +import nl.agilecc.agiledancers.model.specifications.*; +import java.time.LocalDate; +import java.util.*; + +/** Version 1 file format. IDs are local to the file, never server identifiers. */ +public record SchoolSnapshot(Integer formatVersion, UUID key, String name, List dancers, + List coaches, List groups, List shows) { + public record Person(String id, String name, LocalDate birthDate) {} + public record Trainer(String id, String name) {} + public record Group(String id, String category, DanceGroupType type, Integer startAge, Integer endAge, + List styles, List dancerIds, String coachId) {} + public record Part(DayPartType type, List groupIds) {} + public record Performance(String id, String name, LocalDate date, List dayParts) {} + + public static SchoolSnapshot of(DanceSchool s) { + Map dancers = ids(s.getDancers()); + Map coaches = ids(s.getCoaches()); + Map groups = ids(s.getDanceGroups()); + Map shows = ids(s.getShows()); + return new SchoolSnapshot(1, s.getKey(), s.getName(), + s.getDancers().stream().map(d -> new Person(dancers.get(d), d.getName(), d.getBirthDate())).toList(), + s.getCoaches().stream().map(c -> new Trainer(coaches.get(c), c.getName())).toList(), + s.getDanceGroups().stream().map(g -> new Group(groups.get(g), g.getCategory(), g.getType(), + g.getStartAge(), g.getEndAge(), List.copyOf(g.getStyles()), + g.getDancers().stream().map(dancers::get).toList(), + g.getCoach() == null ? null : coaches.get(g.getCoach()))).toList(), + s.getShows().stream().map(show -> new Performance(shows.get(show), show.getName(), show.getDate(), + show.getDayParts().stream().map(p -> new Part(p.getType(), + p.getDanceGroups().stream().map(groups::get).toList())).toList())).toList()); + } + + private static Map ids(Collection values) { + Map result = new IdentityHashMap<>(); + for (T value : values) result.put(value, Integer.toString(result.size() + 1)); + return result; + } + + public DanceSchool restore() { + check(Objects.equals(formatVersion, 1), "Niet-ondersteunde formatVersion: " + formatVersion); + check(key != null, "key ontbreekt"); text(name, "schoolnaam"); + DanceSchool school = DanceSchool.builder().key(key).name(name).build(); + Map dancerMap = new LinkedHashMap<>(); + for (Person d : list(dancers, "dancers")) { + check(d != null, "Lege danser"); text(d.name, "dansernaam"); + check(d.birthDate != null && !d.birthDate.isAfter(LocalDate.now()), "Ongeldige geboortedatum voor " + d.name); + Dancer dancer = Dancer.builder().name(d.name).birthDate(d.birthDate).build(); + // Assign temporary distinct IDs before inserting entities into sets. + dancer.setId((long) dancerMap.size()); + put(dancerMap, d.id, dancer); school.addDancer(dancer); + } + Map coachMap = new LinkedHashMap<>(); + for (Trainer c : list(coaches, "coaches")) { + check(c != null, "Lege coach"); text(c.name, "coachnaam"); + Coach coach = Coach.builder().name(c.name).build(); coach.setId((long) coachMap.size()); + put(coachMap, c.id, coach); school.addCoach(coach); + } + Map groupMap = new LinkedHashMap<>(); + for (Group g : list(groups, "groups")) { + check(g != null, "Lege groep"); text(g.category, "groepsnaam"); + check(g.type != null && g.startAge != null && g.endAge != null && g.startAge >= 0 && g.endAge >= g.startAge, + "Ongeldig type of leeftijdsbereik voor groep " + g.category); + DanceGroup group = DanceGroup.builder().category(g.category).type(g.type).startAge(g.startAge).endAge(g.endAge) + .styles(new LinkedHashSet<>(list(g.styles, "styles"))).coach(g.coachId == null ? null : ref(coachMap, g.coachId, "groep " + g.category + " verwijst naar een onbekende coach" )).build(); + check(!group.getStyles().contains(null), "Ongeldige dansstijl"); + group.setId((long) groupMap.size()); + for (String id : unique(g.dancerIds, "dancerIds")) group.addDancer(ref(dancerMap, id, "groep " + g.category + " verwijst naar een onbekende danser")); + put(groupMap, g.id, group); school.addDanceGroup(group); + } + Set showIds = new HashSet<>(); + for (Performance show : list(shows, "shows")) { + check(show != null, "Lege show"); text(show.id, "show-id"); + check(showIds.add(show.id), "Dubbele show-id " + show.id); text(show.name, "shownaam"); + check(show.date != null, "Showdatum ontbreekt voor " + show.name); + List parts = new ArrayList<>(); Set types = new HashSet<>(); + for (Part part : list(show.dayParts, "dayParts")) { + check(part != null && part.type != null && types.add(part.type), "Ongeldig of dubbel dagdeel voor " + show.name); + parts.add(DayPart.builder().type(part.type).danceGroups(unique(part.groupIds, "groupIds").stream() + .map(id -> ref(groupMap, id, "Show " + show.name + " verwijst naar een onbekende groep")).toList()).build()); + } + Show entity = Show.builder().name(show.name).date(show.date).dayParts(parts).build(); + entity.setId((long) showIds.size()); school.addShow(entity); + } + return school; + } + private static void text(String value, String field) { check(value != null && !value.isBlank(), field + " ontbreekt"); } + private static List list(List value, String field) { check(value != null, field + " ontbreekt"); return value; } + private static List unique(List value, String field) { + list(value, field); check(value.stream().noneMatch(Objects::isNull) && new HashSet<>(value).size() == value.size(), "Ongeldige of dubbele verwijzing in " + field); return value; + } + private static void put(Map map, String id, T value) { text(id, "id"); check(map.putIfAbsent(id, value) == null, "Dubbele id " + id); } + private static T ref(Map map, String id, String error) { T value = map.get(id); check(value != null, error); return value; } + private static void check(boolean valid, String message) { if (!valid) throw new ManagementException(ManagementException.Kind.INVALID_INPUT, message); } +} diff --git a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotResourceTest.java b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotResourceTest.java new file mode 100644 index 0000000..0530fff --- /dev/null +++ b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotResourceTest.java @@ -0,0 +1,32 @@ +package nl.agilecc.agiledancers; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; + +@QuarkusTest +class SchoolSnapshotResourceTest { + private static final String BASE = "/api/dance-schools"; + @Test void downloadPreviewRestoreAndRejectInvalidFiles() { + long id = given().contentType("application/json").body("{\"name\":\"JSON school\"}").post(BASE) + .then().statusCode(201).extract().jsonPath().getLong("id"); + String json = given().get(BASE + "/" + id + "/export").then().statusCode(200) + .header("Content-Disposition", containsString(".json")).body("formatVersion", equalTo(1)).extract().asString(); + try { + given().contentType("application/json").body(json).post(BASE + "/import/preview").then().statusCode(200) + .body("existingId", equalTo((int) id)).body("dancers", equalTo(0)); + for (String bad : new String[]{"{", "null", json.replace("\"formatVersion\" : 1", "\"formatVersion\" : 2"), + json.replace("\"name\" :", "\"unknown\" :"), json + " {}", + json.replace("\"dancers\" : [ ]", "\"dancers\" : null")}) { + given().contentType("application/json").body(bad).post(BASE + "/import?replaceId=" + id) + .then().statusCode(400).body("message", not(emptyOrNullString())); + } + given().get(BASE + "/" + id + "/export").then().body(equalTo(json)); + given().delete(BASE + "/" + id).then().statusCode(204); + id = given().contentType("application/json").body(json).post(BASE + "/import") + .then().statusCode(200).extract().jsonPath().getLong("id"); + given().get(BASE + "/" + id + "/export").then().body(equalTo(json)); + } finally { given().delete(BASE + "/" + id); } + } +} diff --git a/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotTest.java b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotTest.java new file mode 100644 index 0000000..5da1f07 --- /dev/null +++ b/agiledancers-be/src/test/java/nl/agilecc/agiledancers/SchoolSnapshotTest.java @@ -0,0 +1,70 @@ +package nl.agilecc.agiledancers; + +import nl.agilecc.agiledancers.service.*; +import nl.agilecc.agiledancers.model.specifications.*; +import org.junit.jupiter.api.Test; +import java.time.LocalDate; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class SchoolSnapshotTest { + private SchoolSnapshot complete() { + return new SchoolSnapshot(1, UUID.randomUUID(), "Studio", List.of(new SchoolSnapshot.Person("a", "Sam", LocalDate.of(2010,1,1))), + List.of(new SchoolSnapshot.Trainer("c", "Alex")), + List.of(new SchoolSnapshot.Group("g", "Hiphop", DanceGroupType.REGULAR, 0, 99, List.of(DanceGroupStyle.HIPHOP), List.of("a"), "c"), + new SchoolSnapshot.Group("h", "Jazz", DanceGroupType.REGULAR, 0, 99, List.of(), List.of("a"), "c")), + List.of(new SchoolSnapshot.Performance("s", "Show", LocalDate.of(2027,1,1), + List.of(new SchoolSnapshot.Part(DayPartType.MORNING, List.of("g", "h")))))); + } + @Test void roundTripAfterRestartPreservesAllFieldsAndSharedRelationships() { + var snapshot = complete(); + var service = new SchoolManagementService(); + var existing = service.createSchool("Other"); + service.createDancer(existing.getId(), "Other dancer", LocalDate.of(2000,1,1)); + var imported = service.importSchool(snapshot, null); + assertEquals(2, imported.getDanceGroups().size()); + var dancer = imported.getDancers().iterator().next(); + imported.getDanceGroups().forEach(g -> assertSame(dancer, g.getDancers().iterator().next())); + assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(imported.getId())); + var restarted = new SchoolManagementService(); + var restored = restarted.importSchool(service.exportSchool(imported.getId()), null); + assertEquals(SchoolSnapshot.of(snapshot.restore()), restarted.exportSchool(restored.getId())); + // Membership removal must work after remapping hash-based sets. + service.deleteDancer(imported.getId(), dancer.getId()); + imported.getDanceGroups().forEach(g -> assertTrue(g.getDancers().isEmpty())); + assertEquals(1, existing.getDancers().size()); + var group = imported.getDanceGroups().iterator().next(); + service.deleteGroup(imported.getId(), group.getId()); + assertEquals(1, imported.getShows().iterator().next().getDayParts().getFirst().getDanceGroups().size()); + assertDoesNotThrow(() -> service.exportSchool(imported.getId()).restore()); + } + @Test void previewIsReadOnlyAndReplacementRemovesPreviousConfiguration() { + var service = new SchoolManagementService(); var snapshot = complete(); + assertNull(service.preview(snapshot).existingId()); assertTrue(service.schools().isEmpty()); + var original = service.importSchool(snapshot, null); + service.createDancer(original.getId(), "Extra", LocalDate.of(2000,1,1)); + var other = service.createSchool(snapshot.name()); + assertEquals(original.getId(), service.preview(snapshot).existingId()); + assertThrows(ManagementException.class, () -> service.importSchool(snapshot, null)); + assertEquals(2, original.getDancers().size()); + var replaced = service.importSchool(snapshot, original.getId()); + assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(replaced.getId())); + assertSame(other, service.school(other.getId())); + service.deleteSchool(replaced.getId()); + assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(service.importSchool(snapshot, null).getId())); + } + @Test void unresolvedReferenceLeavesExistingAndOtherSchoolsIntact() { + var service = new SchoolManagementService(); var valid = complete(); + var original = service.importSchool(valid, null); + var bad = new SchoolSnapshot(1, valid.key(), valid.name(), List.of(), valid.coaches(), valid.groups(), valid.shows()); + assertThrows(ManagementException.class, () -> service.preview(bad)); + assertThrows(ManagementException.class, () -> service.importSchool(bad, original.getId())); + assertEquals(SchoolSnapshot.of(valid.restore()), service.exportSchool(original.getId())); + } + @Test void emptySchoolRoundTrips() { + var service = new SchoolManagementService(); + var empty = service.createSchool("Empty"); var snapshot = service.exportSchool(empty.getId()); + service.deleteSchool(empty.getId()); + assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(service.importSchool(snapshot, null).getId())); + } +} diff --git a/agiledancers-fe/src/app/schools/school-page.html b/agiledancers-fe/src/app/schools/school-page.html index 5af2e39..2736626 100644 --- a/agiledancers-fe/src/app/schools/school-page.html +++ b/agiledancers-fe/src/app/schools/school-page.html @@ -27,6 +27,7 @@ School bewerken +