Compare commits
25 Commits
ed028e793e
...
feat/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02c59dd98 | ||
|
|
a5d20f264e | ||
|
|
39e7ee2c71 | ||
|
|
f14c8b9eea | ||
|
|
2632434263 | ||
|
|
649c3f8f8a | ||
|
|
5518122b69 | ||
|
|
64110033bd | ||
|
|
29bf45d15a | ||
|
|
3f25f1fd73 | ||
|
|
fcd91c2e81 | ||
|
|
c7bf35f011 | ||
|
|
20cceefbe1 | ||
|
|
2394b020ef | ||
|
|
d9a4faf4da | ||
|
|
6817f42c13 | ||
|
|
9cb44fc70c | ||
|
|
4966855c24 | ||
|
|
832a8dfe2f | ||
|
|
0f613e49ce | ||
|
|
507fa088fd | ||
|
|
f26a0f4336 | ||
|
|
0981355247 | ||
|
|
0dd58556a7 | ||
|
|
22ec808b2d |
@@ -1,5 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
> For a human-readable project overview, see [README.md](./README.md).
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -185,3 +185,40 @@ Quick reminders:
|
||||
- No premature abstractions — KISS beats DRY
|
||||
- No backwards-compatibility shims for code that has no callers
|
||||
- Validate at system boundaries only (user input, external APIs)
|
||||
|
||||
## Frontend Domain Boundaries
|
||||
|
||||
The frontend mirrors the backend's package-by-domain structure. Each Tier-1 folder under `src/lib/` is a domain with a hard import boundary:
|
||||
|
||||
```
|
||||
document person tag user geschichte notification ocr
|
||||
activity conversation shared
|
||||
```
|
||||
|
||||
The `boundaries/dependencies` ESLint rule enforces this. The full allow-list lives in `frontend/eslint.config.js`. The rule fires at error severity and blocks `npm run lint`.
|
||||
|
||||
### Allowed cross-domain imports
|
||||
|
||||
| From | May import from |
|
||||
|---|---|
|
||||
| `document` | `shared`, `person`, `tag`, `ocr`, `activity`, `conversation` |
|
||||
| `geschichte` | `shared`, `person`, `document` |
|
||||
| `ocr` | `shared`, `document` |
|
||||
| `activity` | `shared`, `notification` |
|
||||
| `person`, `tag`, `user`, `notification`, `conversation` | `shared` only |
|
||||
| `shared` | `shared` only |
|
||||
| `routes` | any domain |
|
||||
|
||||
### When you need to cross a boundary
|
||||
|
||||
1. **Move the code to `$lib/shared/`** — the correct fix when the code is truly generic (a UI primitive, a pure utility, a formatting helper).
|
||||
2. **Add an explicit rule** — if a cross-domain dependency is architecturally justified (e.g., `document` importing `PersonTypeahead`), add the allow entry to `eslint.config.js` with a comment explaining the reason.
|
||||
3. **Use `// eslint-disable-next-line boundaries/dependencies`** — last resort, only for cases where neither option is practical. Leave a comment explaining why.
|
||||
|
||||
### Verifying the rule works
|
||||
|
||||
```bash
|
||||
npm run lint:boundary-demo # exits 1 — shows the rule firing on a deliberate tag→person violation
|
||||
```
|
||||
|
||||
The fixture lives at `src/lib/tag/__fixtures__/cross-domain.fixture.ts` and is excluded from `npm run lint` via `--ignore-pattern`.
|
||||
|
||||
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Familienarchiv
|
||||
|
||||
Familienarchiv is a private web application for digitising, organising, and searching a family document collection — letters, postcards, and photographs from 1899 to 1950. Family members upload scans, transcribe handwritten text (Kurrent/Sütterlin), and read the archive from any device.
|
||||
|
||||
---
|
||||
|
||||
## Subsystems
|
||||
|
||||
- `frontend/` — SvelteKit 2 / Svelte 5 / TypeScript / Tailwind 4 web app (server-side rendered)
|
||||
- `backend/` — Spring Boot 4 (Java 21) REST API; handles documents, persons, search, and user management
|
||||
- `ocr-service/` — Python FastAPI microservice for OCR and handwritten text recognition (HTR); single-node by design — see [ADR-001](docs/adr/001-ocr-python-microservice.md). Not part of the default dev stack (see Quick start below)
|
||||
- `infra/` — Gitea Actions CI/CD config; future home for infrastructure-as-code
|
||||
- `scripts/` — operational and data-pipeline helpers (`reset-db.sh`, `clean-e2e-data.sh`, import scripts)
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
**Prerequisites:** Java 21, Node 24, Docker with the `docker compose` plugin (V2).
|
||||
|
||||
### 1. Configure environment
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# The defaults in .env.example work for local development without changes.
|
||||
```
|
||||
|
||||
### 2. Start infrastructure
|
||||
|
||||
```bash
|
||||
# Starts PostgreSQL, MinIO (object storage), and Mailpit (dev mail catcher)
|
||||
docker compose up -d db minio mailpit
|
||||
```
|
||||
|
||||
### 3. Start the backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./mvnw spring-boot:run
|
||||
# Starts on http://localhost:8080
|
||||
# API docs (dev profile, auto-enabled): http://localhost:8080/v3/api-docs
|
||||
```
|
||||
|
||||
### 4. Start the frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
# Starts on http://localhost:5173
|
||||
```
|
||||
|
||||
Open **http://localhost:5173** — you should see the Familienarchiv login screen.
|
||||
|
||||
Default development credentials:
|
||||
|
||||
```
|
||||
# local dev only — change before any network-exposed deployment
|
||||
Email: admin@familyarchive.local
|
||||
Password: admin123
|
||||
```
|
||||
|
||||
> **Development setup only.** The default `docker compose` config exposes the database port and uses root MinIO credentials. Do not connect this to a network without first reading `docs/DEPLOYMENT.md` _(coming: [DOC-5, #399](http://heim-nas:3005/marcel/familienarchiv/issues/399))_.
|
||||
|
||||
### Running the full stack via Docker (optional)
|
||||
|
||||
To run everything including the backend and frontend in containers:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Note: the OCR service (`ocr-service/`) builds its Docker image locally and downloads ~6 GB of ML models on first start. Expect 30–60 minutes on a first run. The rest of the stack starts independently; OCR can be excluded with `--scale ocr-service=0` on memory-constrained machines (requires ≥ 12 GB RAM).
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
| Resource | Purpose |
|
||||
|---|---|
|
||||
| [docs/architecture/c4-diagrams.md](docs/architecture/c4-diagrams.md) | C4 container and component diagrams (current system view) |
|
||||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) _(coming: [DOC-2, #396](http://heim-nas:3005/marcel/familienarchiv/issues/396))_ | Full architecture guide with domain list |
|
||||
| [docs/GLOSSARY.md](docs/GLOSSARY.md) | Overloaded terms: Person vs AppUser, Chronik vs Aktivität, etc. |
|
||||
| [CONTRIBUTING.md](CONTRIBUTING.md) _(coming: [DOC-4, #398](http://heim-nas:3005/marcel/familienarchiv/issues/398))_ | How to add a domain, endpoint, or SvelteKit route |
|
||||
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) _(coming: [DOC-5, #399](http://heim-nas:3005/marcel/familienarchiv/issues/399))_ | Production deployment checklist and secrets guide |
|
||||
| [docs/adr/](docs/adr/) | Architecture Decision Records — the "why" behind key choices |
|
||||
| [Gitea issue tracker](http://heim-nas:3005/marcel/familienarchiv/issues) _(internal — home network only)_ | Bug reports, feature requests, and project planning |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
Private project — all rights reserved. Not licensed for redistribution.
|
||||
@@ -108,6 +108,12 @@
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit-junit5</artifactId>
|
||||
<version>1.3.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Excel Bearbeitung (Apache POI) -->
|
||||
<dependency>
|
||||
|
||||
@@ -102,6 +102,21 @@ public class UserDataInitializer {
|
||||
log.info("E2E seed: 'reader'-Testbenutzer erstellt.");
|
||||
}
|
||||
|
||||
if (userRepository.findByEmail("reset@familyarchive.local").isEmpty()) {
|
||||
log.info("E2E seed: Erstelle 'reset'-Testbenutzer...");
|
||||
UserGroup leserGroup = groupRepository.findByName("Leser").orElseGet(() ->
|
||||
groupRepository.save(UserGroup.builder()
|
||||
.name("Leser")
|
||||
.permissions(Set.of("READ_ALL"))
|
||||
.build()));
|
||||
userRepository.save(AppUser.builder()
|
||||
.email("reset@familyarchive.local")
|
||||
.password(passwordEncoder.encode("reset123"))
|
||||
.groups(Set.of(leserGroup))
|
||||
.build());
|
||||
log.info("E2E seed: 'reset'-Testbenutzer erstellt.");
|
||||
}
|
||||
|
||||
if (personRepo.count() > 0) {
|
||||
log.info("E2E seed: Personendaten bereits vorhanden, überspringe Dokument-Seed.");
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.raddatz.familienarchiv.shared;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import jakarta.persistence.Entity;
|
||||
import org.raddatz.familienarchiv.FamilienarchivApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
@AnalyzeClasses(packagesOf = FamilienarchivApplication.class)
|
||||
class ArchitectureTest {
|
||||
|
||||
// Rule 1: Controllers must never inject repositories directly.
|
||||
// Security rationale: bypassing the service layer skips @RequirePermission
|
||||
// AOP checks that are enforced on service methods.
|
||||
@ArchTest
|
||||
static final ArchRule no_controller_injects_repository_directly =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(RestController.class)
|
||||
.should().dependOnClassesThat().areAssignableTo(JpaRepository.class);
|
||||
|
||||
// Rule 2: Services access only their own domain's repositories, never a foreign domain's.
|
||||
// Prevents hidden coupling between domains that should communicate via service APIs.
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_document =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..document..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("document"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_person =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..person..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("person"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_tag =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..tag..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("tag"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_user =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..user..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("user"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_dashboard =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..dashboard..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("dashboard"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_geschichte =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..geschichte..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("geschichte"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_notification =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..notification..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("notification"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_ocr =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..ocr..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("ocr"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_importing =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..importing..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("importing"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule services_only_access_own_domain_repositories_audit =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Service.class)
|
||||
.and().resideInAPackage("..audit..")
|
||||
.should().dependOnClassesThat(foreignJpaRepositoryFor("audit"));
|
||||
|
||||
// Rule 3: Infrastructure @Configuration classes must not end up scattered in domain packages.
|
||||
// Keeps cross-cutting setup (security, async, DB, storage) in dedicated packages
|
||||
// where it can be audited and reasoned about independently.
|
||||
@ArchTest
|
||||
static final ArchRule no_configuration_class_in_domain_packages =
|
||||
noClasses()
|
||||
.that().areAnnotatedWith(Configuration.class)
|
||||
.and().areNotAnnotatedWith(SpringBootApplication.class)
|
||||
.should().resideInAnyPackage(
|
||||
"..document..", "..person..", "..tag..",
|
||||
"..geschichte..", "..notification..", "..ocr..",
|
||||
"..filestorage..", "..importing..", "..dashboard..", "..audit.."
|
||||
);
|
||||
|
||||
// Rule 4: Entities belong to their domain packages, not to a shared model layer.
|
||||
// Prevents regression to a flat, layer-based package layout.
|
||||
@ArchTest
|
||||
static final ArchRule entities_reside_in_domain_packages =
|
||||
classes()
|
||||
.that().areAnnotatedWith(Entity.class)
|
||||
.should().resideInAnyPackage(
|
||||
"..document..", "..person..", "..tag..", "..user..",
|
||||
"..geschichte..", "..notification..", "..ocr..", "..audit.."
|
||||
);
|
||||
|
||||
// TODO Rule 5: Controllers expose endpoints under their domain prefix
|
||||
// (e.g., classes in ..document.. are annotated with @RequestMapping("/api/documents")).
|
||||
// Implementing this requires a custom ArchUnit DescribedPredicate inspecting the
|
||||
// @RequestMapping annotation value — deferred due to brittleness concerns.
|
||||
// Tracked in: http://heim-nas:3005/marcel/familienarchiv/issues/427
|
||||
|
||||
private static DescribedPredicate<JavaClass> foreignJpaRepositoryFor(String ownDomain) {
|
||||
// Exact-segment match: prevents a domain name that is a substring of another
|
||||
// (e.g. "tag" inside "tagging") from silently escaping the predicate.
|
||||
// The pattern matches the domain as a complete path segment, with an optional sub-package.
|
||||
String ownPackagePattern = ".*\\.familienarchiv\\." + ownDomain + "(\\..+)?$";
|
||||
return new DescribedPredicate<JavaClass>("be a JPA repository from a domain other than " + ownDomain) {
|
||||
@Override
|
||||
public boolean test(JavaClass clazz) {
|
||||
return clazz.isAssignableTo(JpaRepository.class)
|
||||
&& !clazz.getPackageName().matches(ownPackagePattern);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
| J3 — Edit document sender + tags | ✅ COVERED | `documents.spec.ts` |
|
||||
| J4 — Tag create via TagInput | ✅ COVERED | `documents.spec.ts` |
|
||||
| J5 — Create person + add relationship | ✅ COVERED | `persons.spec.ts` |
|
||||
| J6 — Search with text + sender filter | ✅ COVERED | `documents.spec.ts` |
|
||||
| J6 — Search with text + tag filter | ✅ COVERED | `documents.spec.ts` |
|
||||
| J7 — Full transcription journey | ✅ COVERED | `transcription.spec.ts` |
|
||||
| J8 — Geschichte create, publish + link person | ✅ COVERED | `geschichten.spec.ts` |
|
||||
| J9 — Bilateral conversation timeline | ✅ COVERED | `korrespondenz.spec.ts` |
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
**Pre-existing coverage:** Date range filter; text search (separate tests).
|
||||
|
||||
**Gap filled:** A test in `documents.spec.ts` creates two documents — one seeded with a known sender — then applies both a text query and a sender filter simultaneously and asserts only matching results appear.
|
||||
**Gap filled:** A test in `documents.spec.ts` navigates with both a text query (`?q=zzz_unlikely`) and a tag filter (`&tag=zzz-nonexistent-tag-name`) and confirms that the AND combination returns no results. A second test verifies that a `?q=E2E&from=2000-01-01` URL preserves both parameters. Note: a dedicated sender filter test remains a gap — see follow-up issue.
|
||||
|
||||
---
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
**Pre-existing coverage:** Deep-link scroll in `notification-deep-link.spec.ts`.
|
||||
|
||||
**Gap filled:** A test seeds a comment, then via the bell button opens the notification dropdown, verifies the unread count badge, clicks a notification to mark it as read, and confirms the badge disappears.
|
||||
**Gap filled:** A test in `notification-deep-link.spec.ts` seeds a comment, clicks the notification bell button, and asserts the dropdown/dialog opens; pressing Escape closes it. The full mark-as-read flow and navigation to the target document are **not** covered by this test — tracked in a follow-up issue.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -233,10 +233,9 @@ test.describe('Admin system tab — mass import trigger', () => {
|
||||
await expect(importBtn.first()).toBeVisible({ timeout: 10_000 });
|
||||
await importBtn.first().click();
|
||||
|
||||
// After triggering, either a RUNNING status text appears (job started)
|
||||
// or a DONE/FAILED result text appears (job finished quickly or was already done).
|
||||
// After triggering, a status message specific to the import operation appears.
|
||||
await expect(
|
||||
page.locator('text=/Importiert|Dokument|Import|Läuft|DONE|laufend/i').first()
|
||||
page.locator('text=/Import läuft|Import abgeschlossen|Fehler:/').first()
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await page.screenshot({ path: 'test-results/e2e/admin-mass-import-triggered.png' });
|
||||
|
||||
@@ -209,8 +209,6 @@ test.describe('PDF viewer', () => {
|
||||
let noFileDocHref: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
|
||||
// Create a document with a PDF file.
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E PDF Viewer Test' }
|
||||
@@ -229,7 +227,7 @@ test.describe('PDF viewer', () => {
|
||||
}
|
||||
});
|
||||
if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`);
|
||||
pdfDocHref = `${baseURL}/documents/${doc.id}`;
|
||||
pdfDocHref = `/documents/${doc.id}`;
|
||||
|
||||
// Create a document WITHOUT a file — used to verify no canvas is rendered.
|
||||
const noFileRes = await request.post('/api/documents', {
|
||||
@@ -237,7 +235,7 @@ test.describe('PDF viewer', () => {
|
||||
});
|
||||
if (!noFileRes.ok()) throw new Error(`Create no-file document failed: ${noFileRes.status()}`);
|
||||
const noFileDoc = await noFileRes.json();
|
||||
noFileDocHref = `${baseURL}/documents/${noFileDoc.id}`;
|
||||
noFileDocHref = `/documents/${noFileDoc.id}`;
|
||||
});
|
||||
|
||||
test('PDF renders in the custom viewer — canvas is present instead of iframe', async ({
|
||||
@@ -306,8 +304,7 @@ test.describe('PDF annotations — admin', () => {
|
||||
});
|
||||
if (!uploadRes.ok()) throw new Error(`Upload PDF failed: ${uploadRes.status()}`);
|
||||
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
annotationDocHref = `${baseURL}/documents/${doc.id}`;
|
||||
annotationDocHref = `/documents/${doc.id}`;
|
||||
sharedAnnotationDocId = doc.id;
|
||||
});
|
||||
|
||||
@@ -404,7 +401,6 @@ test.describe('PDF annotations — admin', () => {
|
||||
// ─── PDF Annotations — file hash (version awareness) ─────────────────────────
|
||||
|
||||
test.describe('PDF annotations — file hash versioning', () => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
const PDF_FIXTURE2 = path.resolve(__dirname, 'fixtures/minimal2.pdf');
|
||||
|
||||
test('annotations are hidden after a different file is uploaded', async ({ page, request }) => {
|
||||
@@ -436,7 +432,7 @@ test.describe('PDF annotations — file hash versioning', () => {
|
||||
if (!annotRes.ok()) throw new Error(`Create annotation failed: ${annotRes.status()}`);
|
||||
|
||||
// 3. Verify annotation appears before re-upload
|
||||
await page.goto(`${baseURL}/documents/${doc.id}`);
|
||||
await page.goto(`/documents/${doc.id}`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
|
||||
await expect(page.locator('[data-testid^="annotation-"]').first()).toBeVisible({
|
||||
@@ -520,7 +516,7 @@ test.describe('PDF annotations — file hash versioning', () => {
|
||||
if (!restoreRes.ok()) throw new Error(`Restore failed: ${restoreRes.status()}`);
|
||||
|
||||
// 5. Verify annotation reappears and notice is gone
|
||||
await page.goto(`${baseURL}/documents/${doc.id}`);
|
||||
await page.goto(`/documents/${doc.id}`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.locator('canvas').first().waitFor({ state: 'visible', timeout: 20000 });
|
||||
|
||||
@@ -548,8 +544,7 @@ test.describe('PDF annotations — read-only user', () => {
|
||||
await page.waitForURL('/');
|
||||
|
||||
// Navigate directly to the PDF document created by the admin beforeAll.
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
await page.goto(`${baseURL}/documents/${sharedAnnotationDocId}`);
|
||||
await page.goto(`/documents/${sharedAnnotationDocId}`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// Reader users do not have ANNOTATE_ALL permission — the button must not be shown at all.
|
||||
@@ -563,41 +558,69 @@ test.describe('PDF annotations — read-only user', () => {
|
||||
// ── J3: Edit document — add an existing tag ────────────────────────────────
|
||||
//
|
||||
// Verifies that a user can open a document's edit page and assign a tag using
|
||||
// the TagInput component, then save and see the tag chip on the detail page.
|
||||
// the TagInput component, then save and see the tag link on the detail page.
|
||||
// Seeds a unique tag via a throwaway document so the test never depends on the
|
||||
// seeded "Familie" tag (which admin tests rename during their lifecycle).
|
||||
|
||||
test.describe('Document editing — tags (J3)', () => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
let tagDocHref: string;
|
||||
let tagDocId: string;
|
||||
let seedDocId: string;
|
||||
let seededTagName: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const stamp = Date.now().toString(36);
|
||||
seededTagName = `E2E-J3-Tag-${stamp}`;
|
||||
|
||||
// Create a throwaway document and associate the unique tag with it so it
|
||||
// exists in the system for the TagInput suggestion list.
|
||||
const seederRes = await request.post('/api/documents', {
|
||||
multipart: { title: `E2E J3 Tag Seeder ${stamp}` }
|
||||
});
|
||||
if (!seederRes.ok()) throw new Error(`Create seeder failed: ${seederRes.status()}`);
|
||||
const seeder = await seederRes.json();
|
||||
seedDocId = seeder.id;
|
||||
|
||||
const seedTagRes = await request.put(`/api/documents/${seedDocId}`, {
|
||||
multipart: { title: seeder.title, tags: seededTagName }
|
||||
});
|
||||
if (!seedTagRes.ok()) throw new Error(`Seed tag failed: ${seedTagRes.status()}`);
|
||||
|
||||
// Create the test document without the tag — the test will add it.
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Tag Edit Test' }
|
||||
multipart: { title: `E2E Tag Edit Test ${stamp}` }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`);
|
||||
const doc = await createRes.json();
|
||||
tagDocHref = `${baseURL}/documents/${doc.id}`;
|
||||
tagDocId = doc.id;
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (tagDocId) await request.delete(`/api/documents/${tagDocId}`);
|
||||
if (seedDocId) await request.delete(`/api/documents/${seedDocId}`);
|
||||
});
|
||||
|
||||
test('user adds an existing tag and sees it on the detail page', async ({ page }) => {
|
||||
await page.goto(`${tagDocHref}/edit`);
|
||||
await page.goto(`/documents/${tagDocId}/edit`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// TagInput has placeholder "Schlagworte hinzufügen..." when empty.
|
||||
const tagInput = page.getByPlaceholder('Schlagworte hinzufügen...');
|
||||
await expect(tagInput).toBeVisible();
|
||||
|
||||
// Type the beginning of the seeded "Familie" tag and wait for the suggestion.
|
||||
await tagInput.fill('Fami');
|
||||
const suggestion = page.getByRole('option', { name: /Familie/i }).first();
|
||||
// Type the seeded tag name and wait for the suggestion.
|
||||
await tagInput.fill(seededTagName);
|
||||
const suggestion = page.getByRole('option', { name: seededTagName }).first();
|
||||
await expect(suggestion).toBeVisible({ timeout: 5_000 });
|
||||
await suggestion.click();
|
||||
|
||||
// Save the document.
|
||||
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||
|
||||
// Redirected to detail page — the tag chip must be visible.
|
||||
// Redirected to detail page — the tag link must be visible in the metadata section.
|
||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||
await expect(page.getByText(/Familie/)).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.locator('a[href*="?tag="]', { hasText: seededTagName })).toBeVisible({
|
||||
timeout: 5_000
|
||||
});
|
||||
await page.screenshot({ path: 'test-results/e2e/document-edit-tag.png' });
|
||||
});
|
||||
});
|
||||
@@ -605,24 +628,30 @@ test.describe('Document editing — tags (J3)', () => {
|
||||
// ── J4: Create a brand-new tag via TagInput ────────────────────────────────
|
||||
//
|
||||
// Types a tag name that does not exist yet, confirms creation with Enter, and
|
||||
// verifies the tag chip persists after save.
|
||||
// verifies the tag chip persists after save AND after a full page reload.
|
||||
|
||||
test.describe('Document editing — new tag creation (J4)', () => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
let newTagDocHref: string;
|
||||
const newTagName = `E2E-Tag-${Date.now().toString(36)}`;
|
||||
let newTagDocId: string;
|
||||
const stamp = Date.now().toString(36);
|
||||
const newTagName = `E2E-Tag-${stamp}`;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E New Tag Test' }
|
||||
multipart: { title: `E2E New Tag Test ${stamp}` }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`);
|
||||
const doc = await createRes.json();
|
||||
newTagDocHref = `${baseURL}/documents/${doc.id}`;
|
||||
newTagDocId = doc.id;
|
||||
});
|
||||
|
||||
test('user types a new tag name, presses Enter, saves, and sees the chip', async ({ page }) => {
|
||||
await page.goto(`${newTagDocHref}/edit`);
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (newTagDocId) await request.delete(`/api/documents/${newTagDocId}`);
|
||||
});
|
||||
|
||||
test('user types a new tag name, presses Enter, saves, and tag persists after reload', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(`/documents/${newTagDocId}/edit`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
const tagInput = page.getByPlaceholder('Schlagworte hinzufügen...');
|
||||
@@ -637,8 +666,19 @@ test.describe('Document editing — new tag creation (J4)', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Speichern', exact: true }).click();
|
||||
|
||||
// Detail page after redirect — tag link must be visible.
|
||||
await expect(page).toHaveURL(/\/documents\/[^/]+$/);
|
||||
await expect(page.getByText(newTagName)).toBeVisible({ timeout: 5_000 });
|
||||
await expect(page.locator('a[href*="?tag="]', { hasText: newTagName })).toBeVisible({
|
||||
timeout: 5_000
|
||||
});
|
||||
|
||||
// Reload to verify the tag survived the round-trip (not just client-side state).
|
||||
await page.reload();
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await expect(page.locator('a[href*="?tag="]', { hasText: newTagName })).toBeVisible({
|
||||
timeout: 5_000
|
||||
});
|
||||
|
||||
await page.screenshot({ path: 'test-results/e2e/document-new-tag-created.png' });
|
||||
});
|
||||
});
|
||||
@@ -650,10 +690,11 @@ test.describe('Document editing — new tag creation (J4)', () => {
|
||||
|
||||
test.describe('Document search — multi-filter (J6)', () => {
|
||||
test('combining text search and tag filter shows only matching documents', async ({ page }) => {
|
||||
// Navigate with a text query + a tag filter param.
|
||||
// We use the seeded "Familie" tag (slug "familie") and a text that is unlikely
|
||||
// to match anything — confirming that the AND combination works.
|
||||
await page.goto('/?q=zzz_unlikely&tagId=nonexistent-tag-id');
|
||||
// Navigate with a text query + a tag filter param. Using an unlikely text string and
|
||||
// a nonexistent tag name confirms that the AND combination of both filters returns no
|
||||
// results without relying on seeded data. Note: the correct URL param is "tag" (tag name),
|
||||
// not "tagId".
|
||||
await page.goto('/?q=zzz_unlikely&tag=zzz-nonexistent-tag-name');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
await expect(page.getByText('Keine Dokumente gefunden')).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
@@ -25,15 +25,13 @@ let commentId: string;
|
||||
|
||||
test.describe('Notification deep-link scroll', () => {
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Deep-Link Test', documentDate: '1945-05-08' }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`);
|
||||
const doc = await createRes.json();
|
||||
docId = doc.id;
|
||||
docHref = `${baseURL}/documents/${docId}`;
|
||||
docHref = `/documents/${docId}`;
|
||||
|
||||
const uploadRes = await request.put(`/api/documents/${docId}`, {
|
||||
multipart: {
|
||||
@@ -74,6 +72,10 @@ test.describe('Notification deep-link scroll', () => {
|
||||
commentId = comment.id;
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (docId) await request.delete(`/api/documents/${docId}`);
|
||||
});
|
||||
|
||||
async function openDeepLink(page: Page) {
|
||||
const url = `${docHref}?commentId=${commentId}&annotationId=${annotationId}`;
|
||||
await page.goto(url);
|
||||
@@ -90,7 +92,9 @@ test.describe('Notification deep-link scroll', () => {
|
||||
await openDeepLink(page);
|
||||
|
||||
// Transcribe mode was auto-entered — Fertig button is visible
|
||||
await expect(page.getByRole('button', { name: 'Fertig' })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('button', { name: 'Fertig', exact: true })).toBeVisible({
|
||||
timeout: 15_000
|
||||
});
|
||||
|
||||
// The target comment article is in the DOM and visible
|
||||
const article = page.locator(`#comment-${commentId}`);
|
||||
@@ -119,16 +123,17 @@ test.describe('Notification deep-link scroll', () => {
|
||||
// ── Notification bell — J10 ────────────────────────────────────────────────
|
||||
//
|
||||
// Verifies the notification bell in the global header: clicking it opens the
|
||||
// dropdown, an unread notification is visible, clicking it marks it as read
|
||||
// and navigates to the target document.
|
||||
// dropdown and it closes on Escape. Full mark-as-read and navigation flows are
|
||||
// tracked in a follow-up issue.
|
||||
|
||||
test.describe('Notification bell', () => {
|
||||
let bellDocId: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const stamp = Date.now().toString(36);
|
||||
// Seed a document + comment to ensure the notification list has content to render.
|
||||
const createRes = await request.post('/api/documents', {
|
||||
multipart: { title: 'E2E Bell Test Doc', documentDate: '1930-01-01' }
|
||||
multipart: { title: `E2E Bell Test Doc ${stamp}`, documentDate: '1930-01-01' }
|
||||
});
|
||||
if (!createRes.ok()) throw new Error(`Create document failed: ${createRes.status()}`);
|
||||
const doc = await createRes.json();
|
||||
@@ -140,6 +145,10 @@ test.describe('Notification bell', () => {
|
||||
if (!commentRes.ok()) throw new Error(`Create comment failed: ${commentRes.status()}`);
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
if (bellDocId) await request.delete(`/api/documents/${bellDocId}`);
|
||||
});
|
||||
|
||||
test('bell opens dropdown, shows notifications list', async ({ page }) => {
|
||||
test.setTimeout(30_000);
|
||||
|
||||
|
||||
@@ -42,8 +42,9 @@ test.describe('Password reset', () => {
|
||||
});
|
||||
|
||||
test('full password reset flow', async ({ page }) => {
|
||||
const testEmail = process.env.E2E_EMAIL ?? 'admin@familyarchive.local';
|
||||
const originalPassword = process.env.E2E_PASSWORD ?? 'admin123';
|
||||
// Uses a dedicated low-privilege test account so the admin account is never touched.
|
||||
const testEmail = 'reset@familyarchive.local';
|
||||
const originalPassword = 'reset123';
|
||||
const newPassword = 'NewP@ssw0rd_E2E!';
|
||||
|
||||
// 1. Request reset
|
||||
@@ -70,7 +71,7 @@ test.describe('Password reset', () => {
|
||||
|
||||
// 5. Log in with new password
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
await page.getByLabel('Benutzername').fill(process.env.E2E_USERNAME ?? 'admin');
|
||||
await page.getByLabel('Benutzername').fill(testEmail);
|
||||
await page.getByLabel('Passwort').fill(newPassword);
|
||||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||||
await expect(page).toHaveURL('/');
|
||||
@@ -85,7 +86,7 @@ test.describe('Password reset', () => {
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
|
||||
// 7. Log back in with original password to confirm restore worked
|
||||
await page.getByLabel('Benutzername').fill(process.env.E2E_USERNAME ?? 'admin');
|
||||
await page.getByLabel('Benutzername').fill(testEmail);
|
||||
await page.getByLabel('Passwort').fill(originalPassword);
|
||||
await page.getByRole('button', { name: 'Anmelden' }).click();
|
||||
await expect(page).toHaveURL('/');
|
||||
|
||||
@@ -188,8 +188,7 @@ test.describe('Person detail — sent and received documents', () => {
|
||||
// uses the AddRelationshipForm to link them. Asserts the chip appears.
|
||||
|
||||
test.describe('Person relationship — add via edit page (J5)', () => {
|
||||
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:3000';
|
||||
let personAHref: string;
|
||||
let personAId: string;
|
||||
let personBName: string;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
@@ -200,7 +199,7 @@ test.describe('Person relationship — add via edit page (J5)', () => {
|
||||
});
|
||||
if (!aRes.ok()) throw new Error(`Create person A failed: ${aRes.status()}`);
|
||||
const a = await aRes.json();
|
||||
personAHref = `${baseURL}/persons/${a.id}`;
|
||||
personAId = a.id;
|
||||
|
||||
const bRes = await request.post('/api/persons', {
|
||||
data: { firstName: 'E2E-Rel-B', lastName: stamp }
|
||||
@@ -213,7 +212,7 @@ test.describe('Person relationship — add via edit page (J5)', () => {
|
||||
test('user adds a SPOUSE_OF relationship and sees the chip on the edit page', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(`${personAHref}/edit`);
|
||||
await page.goto(`/persons/${personAId}/edit`);
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// Open the AddRelationshipForm by clicking the "+ Beziehung hinzufügen" button.
|
||||
@@ -234,8 +233,14 @@ test.describe('Person relationship — add via edit page (J5)', () => {
|
||||
// Submit the relationship form.
|
||||
await page.getByRole('button', { name: 'Hinzufügen' }).click();
|
||||
|
||||
// The relationship chip should appear in the Stammbaum section.
|
||||
await expect(page.getByText(personBName)).toBeVisible({ timeout: 8_000 });
|
||||
// The relationship chip should appear inside the Beziehungen section.
|
||||
const relCard = page
|
||||
.locator('div')
|
||||
.filter({ has: page.locator('h2', { hasText: 'Beziehungen' }) })
|
||||
.first();
|
||||
await expect(relCard.locator('a[href^="/persons/"]', { hasText: personBName })).toBeVisible({
|
||||
timeout: 8_000
|
||||
});
|
||||
await page.screenshot({ path: 'test-results/e2e/person-relationship-added.png' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import boundaries from 'eslint-plugin-boundaries';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import globals from 'globals';
|
||||
import ts from 'typescript-eslint';
|
||||
@@ -12,7 +13,16 @@ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default defineConfig(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
{ ignores: ['src/paraglide/**', '.svelte-kit.old/**'] },
|
||||
{
|
||||
ignores: [
|
||||
'src/paraglide/**',
|
||||
'.svelte-kit.old/**',
|
||||
// Fixture files are intentionally invalid imports used to demonstrate
|
||||
// that the boundaries/dependencies rule fires. Exclude them from the
|
||||
// regular lint pass so `npm run lint` stays green.
|
||||
'src/lib/**/__fixtures__/**'
|
||||
]
|
||||
},
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
@@ -61,5 +71,87 @@ export default defineConfig(
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
plugins: { boundaries },
|
||||
settings: {
|
||||
'import/resolver': { typescript: { project: './tsconfig.json' } },
|
||||
// $lib/paraglide and $lib/generated are intentionally omitted from the elements list —
|
||||
// they are treated as external (third-party-like) by the rule and may be imported
|
||||
// from any domain without restriction.
|
||||
'boundaries/elements': [
|
||||
{ type: 'document', pattern: 'src/lib/document/**' },
|
||||
{ type: 'person', pattern: 'src/lib/person/**' },
|
||||
{ type: 'tag', pattern: 'src/lib/tag/**' },
|
||||
{ type: 'user', pattern: 'src/lib/user/**' },
|
||||
{ type: 'geschichte', pattern: 'src/lib/geschichte/**' },
|
||||
{ type: 'notification', pattern: 'src/lib/notification/**' },
|
||||
{ type: 'ocr', pattern: 'src/lib/ocr/**' },
|
||||
{ type: 'activity', pattern: 'src/lib/activity/**' },
|
||||
{ type: 'conversation', pattern: 'src/lib/conversation/**' },
|
||||
{ type: 'shared', pattern: 'src/lib/shared/**' },
|
||||
{ type: 'routes', pattern: 'src/routes/**' }
|
||||
]
|
||||
},
|
||||
rules: {
|
||||
'boundaries/dependencies': [
|
||||
'error',
|
||||
{
|
||||
default: 'disallow',
|
||||
message:
|
||||
"Cross-domain import blocked. Move shared code to $lib/shared/, or expose it via the domain's index.ts.",
|
||||
rules: [
|
||||
// Document composes person components (D-FE-1) and tag components (D-FE-2),
|
||||
// and hosts the OCR trigger in the transcription editor.
|
||||
{
|
||||
from: { type: 'document' },
|
||||
allow: {
|
||||
to: { type: ['shared', 'conversation', 'activity', 'person', 'tag', 'ocr'] }
|
||||
}
|
||||
},
|
||||
// Geschichte editor selects persons and documents by design.
|
||||
{
|
||||
from: { type: 'geschichte' },
|
||||
allow: { to: { type: ['shared', 'person', 'document'] } }
|
||||
},
|
||||
// OCR trigger embeds the document script-type selector.
|
||||
{
|
||||
from: { type: 'ocr' },
|
||||
allow: { to: { type: ['shared', 'document'] } }
|
||||
},
|
||||
// Activity feed (Chronik) reads notification items for its inbox panel.
|
||||
{
|
||||
from: { type: 'activity' },
|
||||
allow: { to: { type: ['shared', 'notification'] } }
|
||||
},
|
||||
{ from: { type: 'person' }, allow: { to: { type: ['shared'] } } },
|
||||
{ from: { type: 'tag' }, allow: { to: { type: ['shared'] } } },
|
||||
{ from: { type: 'user' }, allow: { to: { type: ['shared'] } } },
|
||||
{ from: { type: 'notification' }, allow: { to: { type: ['shared'] } } },
|
||||
{ from: { type: 'conversation' }, allow: { to: { type: ['shared'] } } },
|
||||
{ from: { type: 'shared' }, allow: { to: { type: ['shared'] } } },
|
||||
{
|
||||
from: { type: 'routes' },
|
||||
allow: {
|
||||
to: {
|
||||
type: [
|
||||
'document',
|
||||
'person',
|
||||
'tag',
|
||||
'user',
|
||||
'geschichte',
|
||||
'notification',
|
||||
'ocr',
|
||||
'activity',
|
||||
'conversation',
|
||||
'shared'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
725
frontend/package-lock.json
generated
725
frontend/package-lock.json
generated
@@ -34,6 +34,8 @@
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-boundaries": "6.0.2",
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"globals": "^16.5.0",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
@@ -194,6 +196,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@boundaries/elements": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-2.0.1.tgz",
|
||||
"integrity": "sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-import-resolver-node": "0.3.9",
|
||||
"eslint-module-utils": "2.12.1",
|
||||
"handlebars": "4.7.9",
|
||||
"is-core-module": "2.16.1",
|
||||
"micromatch": "4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
@@ -340,6 +359,40 @@
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.2.1",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
@@ -1415,6 +1468,19 @@
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
@@ -2799,6 +2865,17 @@
|
||||
"@tiptap/pm": "3.22.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
@@ -3151,6 +3228,275 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
|
||||
"integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-android-arm64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz",
|
||||
"integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-darwin-arm64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz",
|
||||
"integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-darwin-x64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz",
|
||||
"integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-freebsd-x64": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz",
|
||||
"integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz",
|
||||
"integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz",
|
||||
"integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-arm64-musl": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz",
|
||||
"integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz",
|
||||
"integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-x64-gnu": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz",
|
||||
"integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-linux-x64-musl": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
|
||||
"integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-wasm32-wasi": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz",
|
||||
"integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "^0.2.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz",
|
||||
"integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz",
|
||||
"integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@unrs/resolver-binding-win32-x64-msvc": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz",
|
||||
"integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.0.tgz",
|
||||
@@ -3538,6 +3884,19 @@
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -4009,6 +4368,137 @@
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-context": {
|
||||
"version": "0.1.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz",
|
||||
"integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-tsconfig": "^4.10.1",
|
||||
"stable-hash-x": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-import-context"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"unrs-resolver": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"unrs-resolver": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
"integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7",
|
||||
"is-core-module": "^2.13.0",
|
||||
"resolve": "^1.22.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-typescript": {
|
||||
"version": "4.4.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz",
|
||||
"integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.1",
|
||||
"eslint-import-context": "^0.1.8",
|
||||
"get-tsconfig": "^4.10.1",
|
||||
"is-bun-module": "^2.0.0",
|
||||
"stable-hash-x": "^0.2.0",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"unrs-resolver": "^1.7.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.17.0 || >=18.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-import-resolver-typescript"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "*",
|
||||
"eslint-plugin-import": "*",
|
||||
"eslint-plugin-import-x": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint-plugin-import": {
|
||||
"optional": true
|
||||
},
|
||||
"eslint-plugin-import-x": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz",
|
||||
"integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-boundaries": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-6.0.2.tgz",
|
||||
"integrity": "sha512-wSHgiYeMEbziP91lH0UQ9oslgF2djG1x+LV9z/qO19ggMKZaCB8pKIGePHAY91eLF4EAgpsxQk8MRSFGRPfPzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@boundaries/elements": "2.0.1",
|
||||
"chalk": "4.1.2",
|
||||
"eslint-import-resolver-node": "0.3.9",
|
||||
"eslint-module-utils": "2.12.1",
|
||||
"handlebars": "4.7.9",
|
||||
"micromatch": "4.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-svelte": {
|
||||
"version": "3.15.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.15.2.tgz",
|
||||
@@ -4238,6 +4728,19 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@@ -4301,6 +4804,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -4334,6 +4850,28 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.9",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
|
||||
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.2",
|
||||
"source-map": "^0.6.1",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "^3.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -4450,6 +4988,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-bun-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
|
||||
"integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
@@ -4496,6 +5044,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -5103,6 +5661,33 @@
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/mini-svg-data-uri": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
|
||||
@@ -5126,6 +5711,16 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -5172,6 +5767,22 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
|
||||
"integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"napi-postinstall": "lib/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/napi-postinstall"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -5179,6 +5790,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-readable-to-web-readable-stream": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
|
||||
@@ -5909,6 +6527,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
@@ -6050,6 +6678,16 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -6071,6 +6709,16 @@
|
||||
"kysely": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash-x": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz",
|
||||
"integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
@@ -6320,6 +6968,19 @@
|
||||
"integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
@@ -6367,6 +7028,14 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -6431,6 +7100,20 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "3.19.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
||||
@@ -6463,6 +7146,41 @@
|
||||
"node": ">=18.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz",
|
||||
"integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"napi-postinstall": "^0.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/unrs-resolver"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@unrs/resolver-binding-android-arm-eabi": "1.11.1",
|
||||
"@unrs/resolver-binding-android-arm64": "1.11.1",
|
||||
"@unrs/resolver-binding-darwin-arm64": "1.11.1",
|
||||
"@unrs/resolver-binding-darwin-x64": "1.11.1",
|
||||
"@unrs/resolver-binding-freebsd-x64": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
|
||||
"@unrs/resolver-binding-linux-x64-musl": "1.11.1",
|
||||
"@unrs/resolver-binding-wasm32-wasi": "1.11.1",
|
||||
"@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
|
||||
"@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
|
||||
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -6845,6 +7563,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"lint:boundary-demo": "eslint src/lib/tag/__fixtures__/",
|
||||
"test:unit": "vitest",
|
||||
"test": "npm run test:unit -- --run",
|
||||
"test:coverage": "vitest run --coverage --project=server",
|
||||
@@ -47,6 +48,8 @@
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-boundaries": "6.0.2",
|
||||
"eslint-plugin-svelte": "^3.13.0",
|
||||
"globals": "^16.5.0",
|
||||
"openapi-typescript": "^7.8.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import TagInput, { type Tag } from '$lib/tag/TagInput.svelte';
|
||||
import FieldLabelBadge from './FieldLabelBadge.svelte';
|
||||
import FieldLabelBadge from '$lib/shared/primitives/FieldLabelBadge.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { statusDotClass, statusLabel } from '$lib/person/personFormat';
|
||||
|
||||
type DocumentStatus = 'PLACEHOLDER' | 'UPLOADED' | 'TRANSCRIBED' | 'REVIEWED' | 'ARCHIVED';
|
||||
import {
|
||||
formatDocumentStatus,
|
||||
statusDotClass,
|
||||
type DocumentStatus
|
||||
} from '$lib/document/documentStatusLabel';
|
||||
|
||||
type Props = {
|
||||
status: DocumentStatus;
|
||||
@@ -10,7 +12,7 @@ type Props = {
|
||||
let { status }: Props = $props();
|
||||
|
||||
const dotClass = $derived(statusDotClass(status));
|
||||
const label = $derived(statusLabel(status));
|
||||
const label = $derived(formatDocumentStatus(status));
|
||||
</script>
|
||||
|
||||
<span
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import PersonTypeahead from '$lib/person/PersonTypeahead.svelte';
|
||||
import PersonMultiSelect from '$lib/person/PersonMultiSelect.svelte';
|
||||
import FieldLabelBadge from './FieldLabelBadge.svelte';
|
||||
import FieldLabelBadge from '$lib/shared/primitives/FieldLabelBadge.svelte';
|
||||
import { isoToGerman, handleGermanDateInput } from '$lib/shared/utils/date';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatDocumentStatus } from './documentStatusLabel';
|
||||
import { formatDocumentStatus, statusDotClass } from './documentStatusLabel';
|
||||
|
||||
describe('formatDocumentStatus', () => {
|
||||
it('maps PLACEHOLDER to correct label', () => {
|
||||
@@ -26,3 +26,25 @@ describe('formatDocumentStatus', () => {
|
||||
expect(formatDocumentStatus('SOMETHING_NEW')).toBe('Unbekannt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('statusDotClass', () => {
|
||||
it('PLACEHOLDER → bg-gray-400', () => {
|
||||
expect(statusDotClass('PLACEHOLDER')).toBe('bg-gray-400');
|
||||
});
|
||||
|
||||
it('UPLOADED → bg-emerald-500', () => {
|
||||
expect(statusDotClass('UPLOADED')).toBe('bg-emerald-500');
|
||||
});
|
||||
|
||||
it('TRANSCRIBED → bg-blue-400', () => {
|
||||
expect(statusDotClass('TRANSCRIBED')).toBe('bg-blue-400');
|
||||
});
|
||||
|
||||
it('REVIEWED → bg-amber-400', () => {
|
||||
expect(statusDotClass('REVIEWED')).toBe('bg-amber-400');
|
||||
});
|
||||
|
||||
it('ARCHIVED → bg-emerald-600', () => {
|
||||
expect(statusDotClass('ARCHIVED')).toBe('bg-emerald-600');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
/**
|
||||
* Maps a document status string to a localised human-readable label.
|
||||
* Falls back to "Unknown" for unrecognised values.
|
||||
*/
|
||||
export type DocumentStatus = 'PLACEHOLDER' | 'UPLOADED' | 'TRANSCRIBED' | 'REVIEWED' | 'ARCHIVED';
|
||||
|
||||
export function formatDocumentStatus(status: string): string {
|
||||
switch (status) {
|
||||
case 'PLACEHOLDER':
|
||||
@@ -20,3 +18,18 @@ export function formatDocumentStatus(status: string): string {
|
||||
return m.doc_status_unknown();
|
||||
}
|
||||
}
|
||||
|
||||
export function statusDotClass(status: DocumentStatus): string {
|
||||
switch (status) {
|
||||
case 'PLACEHOLDER':
|
||||
return 'bg-gray-400';
|
||||
case 'UPLOADED':
|
||||
return 'bg-emerald-500';
|
||||
case 'TRANSCRIBED':
|
||||
return 'bg-blue-400';
|
||||
case 'REVIEWED':
|
||||
return 'bg-amber-400';
|
||||
case 'ARCHIVED':
|
||||
return 'bg-emerald-600';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { components } from '$lib/generated/api';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { clickOutside } from '$lib/shared/actions/clickOutside';
|
||||
import { createTypeahead } from '$lib/shared/hooks/useTypeahead.svelte';
|
||||
import FieldLabelBadge from '$lib/document/FieldLabelBadge.svelte';
|
||||
import FieldLabelBadge from '$lib/shared/primitives/FieldLabelBadge.svelte';
|
||||
type Person = components['schemas']['Person'];
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getInitials,
|
||||
abbreviateName,
|
||||
formatXsMeta,
|
||||
personAvatarColor,
|
||||
statusDotClass,
|
||||
statusLabel
|
||||
} from './personFormat';
|
||||
import { getInitials, abbreviateName, formatXsMeta, personAvatarColor } from './personFormat';
|
||||
import { formatDate } from '$lib/shared/utils/date';
|
||||
|
||||
// ─── getInitials ─────────────────────────────────────────────────────────────
|
||||
@@ -146,51 +139,3 @@ describe('formatDate', () => {
|
||||
expect(formatDate('1944-01-01', 'short')).toBe('01.01.1944');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── statusDotClass ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('statusDotClass', () => {
|
||||
it('PLACEHOLDER → bg-gray-400', () => {
|
||||
expect(statusDotClass('PLACEHOLDER')).toBe('bg-gray-400');
|
||||
});
|
||||
|
||||
it('UPLOADED → bg-emerald-500', () => {
|
||||
expect(statusDotClass('UPLOADED')).toBe('bg-emerald-500');
|
||||
});
|
||||
|
||||
it('TRANSCRIBED → bg-blue-400', () => {
|
||||
expect(statusDotClass('TRANSCRIBED')).toBe('bg-blue-400');
|
||||
});
|
||||
|
||||
it('REVIEWED → bg-amber-400', () => {
|
||||
expect(statusDotClass('REVIEWED')).toBe('bg-amber-400');
|
||||
});
|
||||
|
||||
it('ARCHIVED → bg-emerald-600', () => {
|
||||
expect(statusDotClass('ARCHIVED')).toBe('bg-emerald-600');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── statusLabel ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('statusLabel', () => {
|
||||
it('PLACEHOLDER → "Platzhalter"', () => {
|
||||
expect(statusLabel('PLACEHOLDER')).toBe('Platzhalter');
|
||||
});
|
||||
|
||||
it('UPLOADED → "Hochgeladen"', () => {
|
||||
expect(statusLabel('UPLOADED')).toBe('Hochgeladen');
|
||||
});
|
||||
|
||||
it('TRANSCRIBED → "Transkribiert"', () => {
|
||||
expect(statusLabel('TRANSCRIBED')).toBe('Transkribiert');
|
||||
});
|
||||
|
||||
it('REVIEWED → "Geprüft"', () => {
|
||||
expect(statusLabel('REVIEWED')).toBe('Geprüft');
|
||||
});
|
||||
|
||||
it('ARCHIVED → "Archiviert"', () => {
|
||||
expect(statusLabel('ARCHIVED')).toBe('Archiviert');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { formatDocumentStatus } from '$lib/document/documentStatusLabel';
|
||||
import { formatDate } from '$lib/shared/utils/date';
|
||||
|
||||
type Person = { firstName?: string | null; lastName: string; displayName: string };
|
||||
type DocumentStatus = 'PLACEHOLDER' | 'UPLOADED' | 'TRANSCRIBED' | 'REVIEWED' | 'ARCHIVED';
|
||||
type DocForMeta = {
|
||||
sender?: Person | null;
|
||||
receivers?: Person[];
|
||||
@@ -75,22 +73,3 @@ export function formatXsMeta(doc: DocForMeta): string {
|
||||
export function personAvatarColor(personId: string): string {
|
||||
return AVATAR_PALETTE[djb2(personId) % AVATAR_PALETTE.length];
|
||||
}
|
||||
|
||||
export function statusDotClass(status: DocumentStatus): string {
|
||||
switch (status) {
|
||||
case 'PLACEHOLDER':
|
||||
return 'bg-gray-400';
|
||||
case 'UPLOADED':
|
||||
return 'bg-emerald-500';
|
||||
case 'TRANSCRIBED':
|
||||
return 'bg-blue-400';
|
||||
case 'REVIEWED':
|
||||
return 'bg-amber-400';
|
||||
case 'ARCHIVED':
|
||||
return 'bg-emerald-600';
|
||||
}
|
||||
}
|
||||
|
||||
export function statusLabel(status: string): string {
|
||||
return formatDocumentStatus(status);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import type { FlatMessage } from '$lib/shared/types';
|
||||
import { extractQuote } from '$lib/shared/discussion/comment';
|
||||
// eslint-disable-next-line boundaries/dependencies -- discussion UI needs person initials for avatars; move to shared if getInitials becomes generic
|
||||
import { getInitials } from '$lib/person/personFormat';
|
||||
import { relativeTime } from '$lib/shared/utils/time';
|
||||
import { renderBody } from '$lib/shared/discussion/mention';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { components } from '$lib/generated/api';
|
||||
// eslint-disable-next-line boundaries/dependencies -- mention dropdown needs person date formatting; extract to shared if it becomes reusable
|
||||
import { formatLifeDateRange } from '$lib/person/personLifeDates';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Deliberate boundary violation: tag domain importing from person domain.
|
||||
// This file exists to prove the boundaries/dependencies rule fires.
|
||||
// It is excluded from `npm run lint` via --ignore-pattern and must NEVER be imported by production code.
|
||||
// Run `npm run lint:boundary-demo` to see ESLint report the violation (exit 1).
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { getInitials } from '$lib/person/personFormat';
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import DropZone from './DropZone.svelte';
|
||||
import DashboardResumeStrip from '$lib/shared/dashboard/DashboardResumeStrip.svelte';
|
||||
import MissionControlStrip from '$lib/shared/dashboard/MissionControlStrip.svelte';
|
||||
import MissionControlStrip from '$lib/document/MissionControlStrip.svelte';
|
||||
import DashboardFamilyPulse from '$lib/shared/dashboard/DashboardFamilyPulse.svelte';
|
||||
import DashboardActivityFeed from '$lib/activity/DashboardActivityFeed.svelte';
|
||||
import EnrichmentBlock from '$lib/document/EnrichmentBlock.svelte';
|
||||
|
||||
Reference in New Issue
Block a user