From 8cd8fef3a36aac2196fd6e063f67baa06fd2cca2 Mon Sep 17 00:00:00 2001 From: Miro Date: Sat, 27 Jun 2026 22:47:04 +0200 Subject: [PATCH 01/18] feat(web): productivity tools - copy citation button and print styles --- .../web/app/components/CopyCitationButton.tsx | 56 +++++++++++++++ apps/web/app/routes/authority.tsx | 14 +++- apps/web/app/routes/company.tsx | 15 +++- apps/web/app/routes/contract.tsx | 70 +++++++++++-------- 4 files changed, 124 insertions(+), 31 deletions(-) create mode 100644 apps/web/app/components/CopyCitationButton.tsx diff --git a/apps/web/app/components/CopyCitationButton.tsx b/apps/web/app/components/CopyCitationButton.tsx new file mode 100644 index 000000000..bb7c6871d --- /dev/null +++ b/apps/web/app/components/CopyCitationButton.tsx @@ -0,0 +1,56 @@ +import { useState, useCallback } from 'react'; + +export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(() => { + if (typeof navigator !== 'undefined' && navigator.clipboard) { + navigator.clipboard.writeText(textToCopy).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + } + }, [textToCopy]); + + return ( + + ); +} diff --git a/apps/web/app/routes/authority.tsx b/apps/web/app/routes/authority.tsx index 20298c1bd..79ff497db 100644 --- a/apps/web/app/routes/authority.tsx +++ b/apps/web/app/routes/authority.tsx @@ -10,6 +10,7 @@ import { import type { Route } from './+types/authority'; import { Breadcrumbs } from '../components/Breadcrumbs'; import { PageHeader } from '../components/PageHeader'; +import { CopyCitationButton } from '../components/CopyCitationButton'; import { FactsList } from '../components/FactsList'; import { StackedBar } from '../components/StackedBar'; import { DataTable } from '../components/DataTable'; @@ -90,7 +91,18 @@ export default function Authority({ loaderData }: Route.ComponentProps) { } title={a.name} lede={`Колко публични средства е похарчила институцията за обществени поръчки през ${range} г. Зад всяко число по-долу стоят конкретните договори, които го формират.`} - /> + > +
+ +
+ + > +
+ +
+ } > - {c.eopTenderId && ( - // Deep-link to the procedure's page on the public ЦАИС ЕОП portal, where the official - // documents are published and downloadable. The portal keys this page on the numeric EOP - // tenderId (preserved on the parent tender as `eop_tender_id`), NOT the noticeId/document - // number. The portal is a client-rendered SPA, so this is a clickable deep link, not a - // scrapeable file list. - - - Виж документите в ЦАИС ЕОП - - - )} + + Виж документите в ЦАИС ЕОП + + + )} +
Date: Mon, 29 Jun 2026 11:06:29 +0200 Subject: [PATCH 02/18] fix(web): address PR #157 feedback for productivity tools --- .../web/app/components/CopyCitationButton.tsx | 20 +++- apps/web/app/lib/citation.test.ts | 97 +++++++++++++++++++ apps/web/app/lib/citation.ts | 47 +++++++++ apps/web/app/routes/authority.tsx | 10 +- apps/web/app/routes/company.tsx | 11 +-- apps/web/app/routes/contract.tsx | 11 +-- 6 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 apps/web/app/lib/citation.test.ts create mode 100644 apps/web/app/lib/citation.ts diff --git a/apps/web/app/components/CopyCitationButton.tsx b/apps/web/app/components/CopyCitationButton.tsx index bb7c6871d..92d130acb 100644 --- a/apps/web/app/components/CopyCitationButton.tsx +++ b/apps/web/app/components/CopyCitationButton.tsx @@ -1,13 +1,27 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { const [copied, setCopied] = useState(false); + const timeoutRef = useRef(null); + + useEffect(() => { + return () => { + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + }; + }, []); const handleCopy = useCallback(() => { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(textToCopy).then(() => { setCopied(true); - setTimeout(() => setCopied(false), 2000); + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + timeoutRef.current = window.setTimeout(() => setCopied(false), 2000); + }).catch((err) => { + console.error('Failed to copy text:', err); }); } }, [textToCopy]); @@ -50,7 +64,7 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { )} - {copied ? 'Копирано!' : 'Копирай'} + {copied ? 'Копирано!' : 'Копирай'} ); } diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts new file mode 100644 index 000000000..1626da963 --- /dev/null +++ b/apps/web/app/lib/citation.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from 'vitest'; +import { buildContractCitation, buildCompanyCitation, buildAuthorityCitation } from './citation'; + +describe('citation builders', () => { + it('builds a contract citation', () => { + const c = { + subject: 'Доставка на компютри', + authority: { name: 'Община Пловдив' }, + bidder: { displayName: 'Техно ООД' }, + value: { currentEur: 125000.5 }, + id: 'abc-123', + }; + + const citation = buildContractCitation(c); + expect(citation).toBe([ + 'Договор: Доставка на компютри', + 'Възложител: Община Пловдив', + 'Изпълнител: Техно ООД', + 'Стойност: 125 001 €', + 'Връзка: https://sigma.midt.bg/contracts/abc-123', + ].join('\n')); + }); + + it('handles contract with null value', () => { + const c = { + subject: 'Одит', + authority: { name: 'Община Пловдив' }, + bidder: { displayName: 'Техно ООД' }, + value: { currentEur: null }, + id: 'abc-123', + }; + + const citation = buildContractCitation(c); + expect(citation).toBe([ + 'Договор: Одит', + 'Възложител: Община Пловдив', + 'Изпълнител: Техно ООД', + 'Стойност: —', + 'Връзка: https://sigma.midt.bg/contracts/abc-123', + ].join('\n')); + }); + + it('builds a company citation with EIK', () => { + const c = { + displayName: 'Техно ООД', + eik: '123456789', + wonEur: 5000000, + contracts: 42, + slug: 'techno-ood', + }; + + const citation = buildCompanyCitation(c); + expect(citation).toBe([ + 'Компания: Техно ООД', + 'ЕИК: 123456789', + 'Общо спечелено: 5 000 000 €', + 'Брой договори: 42', + 'Връзка: https://sigma.midt.bg/companies/techno-ood', + ].join('\n')); + }); + + it('builds a company citation without EIK', () => { + const c = { + displayName: 'Чуждестранна фирма', + eik: null, + wonEur: 0, + contracts: 1, + slug: 'foreign-corp', + }; + + const citation = buildCompanyCitation(c); + expect(citation).toBe([ + 'Компания: Чуждестранна фирма', + 'ЕИК: Няма', + 'Общо спечелено: 0 €', + 'Брой договори: 1', + 'Връзка: https://sigma.midt.bg/companies/foreign-corp', + ].join('\n')); + }); + + it('builds an authority citation', () => { + const a = { + name: 'Община Варна', + spentEur: 1000000, + contracts: 5, + slug: 'obshtina-varna', + }; + + const citation = buildAuthorityCitation(a); + expect(citation).toBe([ + 'Институция: Община Варна', + 'Общо похарчено: 1 000 000 €', + 'Брой договори: 5', + 'Връзка: https://sigma.midt.bg/authorities/obshtina-varna', + ].join('\n')); + }); +}); diff --git a/apps/web/app/lib/citation.ts b/apps/web/app/lib/citation.ts new file mode 100644 index 000000000..766ecfc80 --- /dev/null +++ b/apps/web/app/lib/citation.ts @@ -0,0 +1,47 @@ +import { money, count } from '@sigma/shared'; + +export function buildContractCitation(c: { + subject: string; + authority: { name: string }; + bidder: { displayName: string }; + value: { currentEur: number | null }; + id: string; +}): string { + return [ + `Договор: ${c.subject}`, + `Възложител: ${c.authority.name}`, + `Изпълнител: ${c.bidder.displayName}`, + `Стойност: ${money(c.value.currentEur)}`, + `Връзка: https://sigma.midt.bg/contracts/${c.id}`, + ].join('\n'); +} + +export function buildCompanyCitation(c: { + displayName: string; + eik: string | null; + wonEur: number; + contracts: number; + slug: string; +}): string { + return [ + `Компания: ${c.displayName}`, + `ЕИК: ${c.eik || 'Няма'}`, + `Общо спечелено: ${money(c.wonEur)}`, + `Брой договори: ${count(c.contracts)}`, + `Връзка: https://sigma.midt.bg/companies/${c.slug}`, + ].join('\n'); +} + +export function buildAuthorityCitation(a: { + name: string; + spentEur: number; + contracts: number; + slug: string; +}): string { + return [ + `Институция: ${a.name}`, + `Общо похарчено: ${money(a.spentEur)}`, + `Брой договори: ${count(a.contracts)}`, + `Връзка: https://sigma.midt.bg/authorities/${a.slug}`, + ].join('\n'); +} diff --git a/apps/web/app/routes/authority.tsx b/apps/web/app/routes/authority.tsx index 79ff497db..16449694f 100644 --- a/apps/web/app/routes/authority.tsx +++ b/apps/web/app/routes/authority.tsx @@ -23,6 +23,7 @@ import { publicCache } from '../lib/cache'; import { coverageRange, getCoverageMeta } from '../lib/coverage'; import { networkColumns, networkRows, trendYearColumns } from '../lib/entity-tables'; import { withDbRetry } from '../lib/retry'; +import { buildAuthorityCitation } from '../lib/citation'; import { seoMeta } from '../lib/meta'; export function meta({ data, params, matches }: Route.MetaArgs) { @@ -93,14 +94,7 @@ export default function Authority({ loaderData }: Route.ComponentProps) { lede={`Колко публични средства е похарчила институцията за обществени поръчки през ${range} г. Зад всяко число по-долу стоят конкретните договори, които го формират.`} >
- +
diff --git a/apps/web/app/routes/company.tsx b/apps/web/app/routes/company.tsx index d8fe428c0..806822653 100644 --- a/apps/web/app/routes/company.tsx +++ b/apps/web/app/routes/company.tsx @@ -24,6 +24,7 @@ import { publicCache } from '../lib/cache'; import { coverageRange, getCoverageMeta } from '../lib/coverage'; import { networkColumns, networkRows, trendYearColumns } from '../lib/entity-tables'; import { withDbRetry } from '../lib/retry'; +import { buildCompanyCitation } from '../lib/citation'; import { seoMeta } from '../lib/meta'; function isSingleNaturalPersonProfile(kind: string, legalForm: string | null): boolean { @@ -130,15 +131,7 @@ export default function Company({ loaderData }: Route.ComponentProps) { lede={`Колко публични средства е ${wonVerb} ${subjectPhrase} по обществени поръчки за периода ${range} г.`} >
- +
diff --git a/apps/web/app/routes/contract.tsx b/apps/web/app/routes/contract.tsx index 11f1adb43..6b3ba17d9 100644 --- a/apps/web/app/routes/contract.tsx +++ b/apps/web/app/routes/contract.tsx @@ -11,6 +11,7 @@ import { Chip, Flag, Section, ExternalEikLink } from '../components/ui'; import { RiskIndicators } from '../components/RiskIndicators'; import { publicCache } from '../lib/cache'; import { eopSourceFiles } from '../lib/eopSource'; +import { buildContractCitation } from '../lib/citation'; import { seoMeta } from '../lib/meta'; /** @@ -111,15 +112,7 @@ export default function Contract({ loaderData }: Route.ComponentProps) { } >
- + {c.eopTenderId && ( // Deep-link to the procedure's page on the public ЦАИС ЕОП portal, where the official // documents are published and downloadable. The portal keys this page on the numeric EOP From e09d339e8011f9a65b1c424561d89971fcb018d2 Mon Sep 17 00:00:00 2001 From: Miro Date: Mon, 29 Jun 2026 13:13:53 +0200 Subject: [PATCH 03/18] test(web): fix citation test expectations for money formatting --- .../web/app/components/CopyCitationButton.tsx | 25 +++-- apps/web/app/lib/citation.test.ts | 91 +++++++++++-------- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/apps/web/app/components/CopyCitationButton.tsx b/apps/web/app/components/CopyCitationButton.tsx index 92d130acb..fbaf41240 100644 --- a/apps/web/app/components/CopyCitationButton.tsx +++ b/apps/web/app/components/CopyCitationButton.tsx @@ -14,15 +14,18 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { const handleCopy = useCallback(() => { if (typeof navigator !== 'undefined' && navigator.clipboard) { - navigator.clipboard.writeText(textToCopy).then(() => { - setCopied(true); - if (timeoutRef.current !== null) { - window.clearTimeout(timeoutRef.current); - } - timeoutRef.current = window.setTimeout(() => setCopied(false), 2000); - }).catch((err) => { - console.error('Failed to copy text:', err); - }); + navigator.clipboard + .writeText(textToCopy) + .then(() => { + setCopied(true); + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + timeoutRef.current = window.setTimeout(() => setCopied(false), 2000); + }) + .catch((err) => { + console.error('Failed to copy text:', err); + }); } }, [textToCopy]); @@ -64,7 +67,9 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { )} - {copied ? 'Копирано!' : 'Копирай'} + + {copied ? 'Копирано!' : 'Копирай'} + ); } diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index 1626da963..0586d7b30 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest'; import { buildContractCitation, buildCompanyCitation, buildAuthorityCitation } from './citation'; +import { money } from '@sigma/shared'; describe('citation builders', () => { it('builds a contract citation', () => { @@ -10,15 +11,17 @@ describe('citation builders', () => { value: { currentEur: 125000.5 }, id: 'abc-123', }; - + const citation = buildContractCitation(c); - expect(citation).toBe([ - 'Договор: Доставка на компютри', - 'Възложител: Община Пловдив', - 'Изпълнител: Техно ООД', - 'Стойност: 125 001 €', - 'Връзка: https://sigma.midt.bg/contracts/abc-123', - ].join('\n')); + expect(citation).toBe( + [ + 'Договор: Доставка на компютри', + 'Възложител: Община Пловдив', + 'Изпълнител: Техно ООД', + `Стойност: ${money(125000.5)}`, + 'Връзка: https://sigma.midt.bg/contracts/abc-123', + ].join('\n'), + ); }); it('handles contract with null value', () => { @@ -29,15 +32,17 @@ describe('citation builders', () => { value: { currentEur: null }, id: 'abc-123', }; - + const citation = buildContractCitation(c); - expect(citation).toBe([ - 'Договор: Одит', - 'Възложител: Община Пловдив', - 'Изпълнител: Техно ООД', - 'Стойност: —', - 'Връзка: https://sigma.midt.bg/contracts/abc-123', - ].join('\n')); + expect(citation).toBe( + [ + 'Договор: Одит', + 'Възложител: Община Пловдив', + 'Изпълнител: Техно ООД', + 'Стойност: —', + 'Връзка: https://sigma.midt.bg/contracts/abc-123', + ].join('\n'), + ); }); it('builds a company citation with EIK', () => { @@ -48,17 +53,19 @@ describe('citation builders', () => { contracts: 42, slug: 'techno-ood', }; - + const citation = buildCompanyCitation(c); - expect(citation).toBe([ - 'Компания: Техно ООД', - 'ЕИК: 123456789', - 'Общо спечелено: 5 000 000 €', - 'Брой договори: 42', - 'Връзка: https://sigma.midt.bg/companies/techno-ood', - ].join('\n')); + expect(citation).toBe( + [ + 'Компания: Техно ООД', + 'ЕИК: 123456789', + `Общо спечелено: ${money(5000000)}`, + 'Брой договори: 42', + 'Връзка: https://sigma.midt.bg/companies/techno-ood', + ].join('\n'), + ); }); - + it('builds a company citation without EIK', () => { const c = { displayName: 'Чуждестранна фирма', @@ -67,15 +74,17 @@ describe('citation builders', () => { contracts: 1, slug: 'foreign-corp', }; - + const citation = buildCompanyCitation(c); - expect(citation).toBe([ - 'Компания: Чуждестранна фирма', - 'ЕИК: Няма', - 'Общо спечелено: 0 €', - 'Брой договори: 1', - 'Връзка: https://sigma.midt.bg/companies/foreign-corp', - ].join('\n')); + expect(citation).toBe( + [ + 'Компания: Чуждестранна фирма', + 'ЕИК: Няма', + `Общо спечелено: ${money(0)}`, + 'Брой договори: 1', + 'Връзка: https://sigma.midt.bg/companies/foreign-corp', + ].join('\n'), + ); }); it('builds an authority citation', () => { @@ -85,13 +94,15 @@ describe('citation builders', () => { contracts: 5, slug: 'obshtina-varna', }; - + const citation = buildAuthorityCitation(a); - expect(citation).toBe([ - 'Институция: Община Варна', - 'Общо похарчено: 1 000 000 €', - 'Брой договори: 5', - 'Връзка: https://sigma.midt.bg/authorities/obshtina-varna', - ].join('\n')); + expect(citation).toBe( + [ + 'Институция: Община Варна', + `Общо похарчено: ${money(1000000)}`, + 'Брой договори: 5', + 'Връзка: https://sigma.midt.bg/authorities/obshtina-varna', + ].join('\n'), + ); }); }); From f14eac6841d6be301bfa0f7fa06d176b525b9ccd Mon Sep 17 00:00:00 2001 From: Miro Date: Mon, 29 Jun 2026 13:30:05 +0200 Subject: [PATCH 04/18] fix(web): address PR #157 review comments for citations --- apps/web/app/lib/citation.test.ts | 22 ++++++------ apps/web/app/lib/citation.ts | 58 ++++++++++++++++++------------- apps/web/app/routes/authority.tsx | 8 +++-- apps/web/app/routes/company.tsx | 8 +++-- apps/web/app/routes/contract.tsx | 8 +++-- 5 files changed, 61 insertions(+), 43 deletions(-) diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index 0586d7b30..e199a93e1 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -12,14 +12,14 @@ describe('citation builders', () => { id: 'abc-123', }; - const citation = buildContractCitation(c); + const citation = buildContractCitation(c, 'https://sigma.test'); expect(citation).toBe( [ 'Договор: Доставка на компютри', 'Възложител: Община Пловдив', 'Изпълнител: Техно ООД', `Стойност: ${money(125000.5)}`, - 'Връзка: https://sigma.midt.bg/contracts/abc-123', + 'Връзка: https://sigma.test/contracts/abc-123', ].join('\n'), ); }); @@ -33,14 +33,14 @@ describe('citation builders', () => { id: 'abc-123', }; - const citation = buildContractCitation(c); + const citation = buildContractCitation(c, 'https://sigma.test'); expect(citation).toBe( [ 'Договор: Одит', 'Възложител: Община Пловдив', 'Изпълнител: Техно ООД', 'Стойност: —', - 'Връзка: https://sigma.midt.bg/contracts/abc-123', + 'Връзка: https://sigma.test/contracts/abc-123', ].join('\n'), ); }); @@ -52,16 +52,17 @@ describe('citation builders', () => { wonEur: 5000000, contracts: 42, slug: 'techno-ood', + hasEik: true, }; - const citation = buildCompanyCitation(c); + const citation = buildCompanyCitation(c, 'https://sigma.test'); expect(citation).toBe( [ 'Компания: Техно ООД', 'ЕИК: 123456789', `Общо спечелено: ${money(5000000)}`, 'Брой договори: 42', - 'Връзка: https://sigma.midt.bg/companies/techno-ood', + 'Връзка: https://sigma.test/companies/techno-ood', ].join('\n'), ); }); @@ -73,16 +74,17 @@ describe('citation builders', () => { wonEur: 0, contracts: 1, slug: 'foreign-corp', + hasEik: false, }; - const citation = buildCompanyCitation(c); + const citation = buildCompanyCitation(c, 'https://sigma.test'); expect(citation).toBe( [ 'Компания: Чуждестранна фирма', 'ЕИК: Няма', `Общо спечелено: ${money(0)}`, 'Брой договори: 1', - 'Връзка: https://sigma.midt.bg/companies/foreign-corp', + 'Връзка: https://sigma.test/companies/foreign-corp', ].join('\n'), ); }); @@ -95,13 +97,13 @@ describe('citation builders', () => { slug: 'obshtina-varna', }; - const citation = buildAuthorityCitation(a); + const citation = buildAuthorityCitation(a, 'https://sigma.test'); expect(citation).toBe( [ 'Институция: Община Варна', `Общо похарчено: ${money(1000000)}`, 'Брой договори: 5', - 'Връзка: https://sigma.midt.bg/authorities/obshtina-varna', + 'Връзка: https://sigma.test/authorities/obshtina-varna', ].join('\n'), ); }); diff --git a/apps/web/app/lib/citation.ts b/apps/web/app/lib/citation.ts index 766ecfc80..e9870d7ef 100644 --- a/apps/web/app/lib/citation.ts +++ b/apps/web/app/lib/citation.ts @@ -1,47 +1,57 @@ import { money, count } from '@sigma/shared'; -export function buildContractCitation(c: { - subject: string; - authority: { name: string }; - bidder: { displayName: string }; - value: { currentEur: number | null }; - id: string; -}): string { +export function buildContractCitation( + c: { + subject: string; + authority: { name: string }; + bidder: { displayName: string }; + value: { currentEur: number | null }; + id: string; + }, + origin: string, +): string { return [ `Договор: ${c.subject}`, `Възложител: ${c.authority.name}`, `Изпълнител: ${c.bidder.displayName}`, `Стойност: ${money(c.value.currentEur)}`, - `Връзка: https://sigma.midt.bg/contracts/${c.id}`, + `Връзка: ${origin}/contracts/${c.id}`, ].join('\n'); } -export function buildCompanyCitation(c: { - displayName: string; - eik: string | null; - wonEur: number; - contracts: number; - slug: string; -}): string { +export function buildCompanyCitation( + c: { + displayName: string; + eik: string | null; + wonEur: number; + contracts: number; + slug: string; + hasEik?: boolean; + }, + origin: string, +): string { return [ `Компания: ${c.displayName}`, - `ЕИК: ${c.eik || 'Няма'}`, + `ЕИК: ${c.hasEik && c.eik ? c.eik : 'Няма'}`, `Общо спечелено: ${money(c.wonEur)}`, `Брой договори: ${count(c.contracts)}`, - `Връзка: https://sigma.midt.bg/companies/${c.slug}`, + `Връзка: ${origin}/companies/${c.slug}`, ].join('\n'); } -export function buildAuthorityCitation(a: { - name: string; - spentEur: number; - contracts: number; - slug: string; -}): string { +export function buildAuthorityCitation( + a: { + name: string; + spentEur: number; + contracts: number; + slug: string; + }, + origin: string, +): string { return [ `Институция: ${a.name}`, `Общо похарчено: ${money(a.spentEur)}`, `Брой договори: ${count(a.contracts)}`, - `Връзка: https://sigma.midt.bg/authorities/${a.slug}`, + `Връзка: ${origin}/authorities/${a.slug}`, ].join('\n'); } diff --git a/apps/web/app/routes/authority.tsx b/apps/web/app/routes/authority.tsx index 16449694f..8b2e86fea 100644 --- a/apps/web/app/routes/authority.tsx +++ b/apps/web/app/routes/authority.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router'; +import { Link, useMatches } from 'react-router'; import { count, money, moneyBare, pct, periodRange, plural } from '@sigma/shared'; import { authorityIdFromSlug, @@ -24,7 +24,7 @@ import { coverageRange, getCoverageMeta } from '../lib/coverage'; import { networkColumns, networkRows, trendYearColumns } from '../lib/entity-tables'; import { withDbRetry } from '../lib/retry'; import { buildAuthorityCitation } from '../lib/citation'; -import { seoMeta } from '../lib/meta'; +import { seoMeta, getRootOrigin } from '../lib/meta'; export function meta({ data, params, matches }: Route.MetaArgs) { const name = data?.authority.name ?? 'Институция'; @@ -60,6 +60,8 @@ export async function loader({ params, context }: Route.LoaderArgs) { } export default function Authority({ loaderData }: Route.ComponentProps) { + const matches = useMatches(); + const origin = getRootOrigin(matches); const a = loaderData.authority; const { trend, network, competition } = loaderData; const ct = competition; @@ -94,7 +96,7 @@ export default function Authority({ loaderData }: Route.ComponentProps) { lede={`Колко публични средства е похарчила институцията за обществени поръчки през ${range} г. Зад всяко число по-долу стоят конкретните договори, които го формират.`} >
- +
diff --git a/apps/web/app/routes/company.tsx b/apps/web/app/routes/company.tsx index 806822653..e55d097de 100644 --- a/apps/web/app/routes/company.tsx +++ b/apps/web/app/routes/company.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router'; +import { Link, useMatches } from 'react-router'; import { count, isNaturalPersonProfileName, @@ -25,7 +25,7 @@ import { coverageRange, getCoverageMeta } from '../lib/coverage'; import { networkColumns, networkRows, trendYearColumns } from '../lib/entity-tables'; import { withDbRetry } from '../lib/retry'; import { buildCompanyCitation } from '../lib/citation'; -import { seoMeta } from '../lib/meta'; +import { seoMeta, getRootOrigin } from '../lib/meta'; function isSingleNaturalPersonProfile(kind: string, legalForm: string | null): boolean { if (kind === 'consortium' || !legalForm) return false; @@ -81,6 +81,8 @@ export async function loader({ params, context }: Route.LoaderArgs) { } export default function Company({ loaderData }: Route.ComponentProps) { + const matches = useMatches(); + const origin = getRootOrigin(matches); const c = loaderData.company; const { trend, network } = loaderData; const range = coverageRange(loaderData.coverage.coverageEndYear); @@ -131,7 +133,7 @@ export default function Company({ loaderData }: Route.ComponentProps) { lede={`Колко публични средства е ${wonVerb} ${subjectPhrase} по обществени поръчки за периода ${range} г.`} >
- +
diff --git a/apps/web/app/routes/contract.tsx b/apps/web/app/routes/contract.tsx index 6b3ba17d9..ffe7e2c4a 100644 --- a/apps/web/app/routes/contract.tsx +++ b/apps/web/app/routes/contract.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router'; +import { Link, useMatches } from 'react-router'; import { count, longDate, money, moneyBare, plural, signedPct } from '@sigma/shared'; import { contractIdFromSlug, getContract } from '@sigma/db'; import type { ContractDetail } from '@sigma/api-contract'; @@ -12,7 +12,7 @@ import { RiskIndicators } from '../components/RiskIndicators'; import { publicCache } from '../lib/cache'; import { eopSourceFiles } from '../lib/eopSource'; import { buildContractCitation } from '../lib/citation'; -import { seoMeta } from '../lib/meta'; +import { seoMeta, getRootOrigin } from '../lib/meta'; /** * Compose the muted sub-line under „Брой оферти". The AOP feed gives us the gross submitted count @@ -71,6 +71,8 @@ export async function loader({ params, context }: Route.LoaderArgs) { const UNVERIFIED_VALUE_LABEL = 'стойност с непотвърдена достоверност'; export default function Contract({ loaderData }: Route.ComponentProps) { + const matches = useMatches(); + const origin = getRootOrigin(matches); const c = loaderData.contract; const v = c.value; const crumbId = c.unp || c.contractNumber || c.id; @@ -112,7 +114,7 @@ export default function Contract({ loaderData }: Route.ComponentProps) { } >
- + {c.eopTenderId && ( // Deep-link to the procedure's page on the public ЦАИС ЕОП portal, where the official // documents are published and downloadable. The portal keys this page on the numeric EOP From d92683563db8317674ba2fe6092dc15c2c7da1f1 Mon Sep 17 00:00:00 2001 From: Miro Date: Mon, 29 Jun 2026 13:39:12 +0200 Subject: [PATCH 05/18] fix(web): fallback origin for citation builders --- apps/web/app/routes/authority.tsx | 2 +- apps/web/app/routes/company.tsx | 2 +- apps/web/app/routes/contract.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/routes/authority.tsx b/apps/web/app/routes/authority.tsx index 8b2e86fea..a4e9f9915 100644 --- a/apps/web/app/routes/authority.tsx +++ b/apps/web/app/routes/authority.tsx @@ -61,7 +61,7 @@ export async function loader({ params, context }: Route.LoaderArgs) { export default function Authority({ loaderData }: Route.ComponentProps) { const matches = useMatches(); - const origin = getRootOrigin(matches); + const origin = getRootOrigin(matches) ?? 'https://sigma.midt.bg'; const a = loaderData.authority; const { trend, network, competition } = loaderData; const ct = competition; diff --git a/apps/web/app/routes/company.tsx b/apps/web/app/routes/company.tsx index e55d097de..a91c42e2b 100644 --- a/apps/web/app/routes/company.tsx +++ b/apps/web/app/routes/company.tsx @@ -82,7 +82,7 @@ export async function loader({ params, context }: Route.LoaderArgs) { export default function Company({ loaderData }: Route.ComponentProps) { const matches = useMatches(); - const origin = getRootOrigin(matches); + const origin = getRootOrigin(matches) ?? 'https://sigma.midt.bg'; const c = loaderData.company; const { trend, network } = loaderData; const range = coverageRange(loaderData.coverage.coverageEndYear); diff --git a/apps/web/app/routes/contract.tsx b/apps/web/app/routes/contract.tsx index ffe7e2c4a..ec68d4454 100644 --- a/apps/web/app/routes/contract.tsx +++ b/apps/web/app/routes/contract.tsx @@ -72,7 +72,7 @@ const UNVERIFIED_VALUE_LABEL = 'стойност с непотвърдена д export default function Contract({ loaderData }: Route.ComponentProps) { const matches = useMatches(); - const origin = getRootOrigin(matches); + const origin = getRootOrigin(matches) ?? 'https://sigma.midt.bg'; const c = loaderData.contract; const v = c.value; const crumbId = c.unp || c.contractNumber || c.id; From aa7585b1080c4c89eb5eea6244952351a8361c1d Mon Sep 17 00:00:00 2001 From: Bilko Date: Fri, 3 Jul 2026 15:31:32 -0700 Subject: [PATCH 06/18] style(web): move productivity-tools CSS into the styles/* split app.css is now @import-only (#84). Place the copy-citation button rules (.header-actions/.save-btn, reusing the .source-cta button language) next to .source-cta in chrome.css, and the print stylesheet in base.css alongside the other global @media blocks. --- apps/web/app/styles/base.css | 48 ++++++++++++++++++++++++++++++++++ apps/web/app/styles/chrome.css | 32 ++++++++++++++++++----- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/apps/web/app/styles/base.css b/apps/web/app/styles/base.css index c7752d4d4..38c9c5ad5 100644 --- a/apps/web/app/styles/base.css +++ b/apps/web/app/styles/base.css @@ -151,3 +151,51 @@ code, scroll-behavior: auto !important; } } + +/* =========== Print styles =========== */ +/* A print-friendly view: strip site chrome and interactive actions, force high-contrast + black-on-white, expose link targets inline, and avoid awkward mid-element page breaks. */ +@media print { + /* Hide navigation, footers, search drawer, and actions */ + .site-header, + .site-nav, + .nav-backdrop, + .search-drawer, + .site-footer, + .save-btn { + display: none !important; + } + + /* Expose URLs in print */ + a[href]:not([href^='#'])::after { + content: ' (' attr(href) ')'; + word-break: break-all; + font-size: 0.9em; + } + + /* Ensure background and text colors are optimized for printing */ + body { + background: #fff !important; + color: #000 !important; + } + + /* Prevent elements from breaking mid-page */ + .section, + .table-wrap, + .result, + .facts { + break-inside: avoid; + } + + /* Clean up page margins */ + @page { + margin: 1.5cm; + } + + /* Expand the content to fill the page width */ + #main, + .page-header { + max-width: none !important; + padding: 0 !important; + } +} diff --git a/apps/web/app/styles/chrome.css b/apps/web/app/styles/chrome.css index d966ab711..38576c8fe 100644 --- a/apps/web/app/styles/chrome.css +++ b/apps/web/app/styles/chrome.css @@ -645,14 +645,24 @@ main.narrow { font: 400 clamp(15px, 1.4vw, 17px)/1.55 var(--font-sans); color: var(--ink-mid); } -/* Source CTA — solid, icon-led deep link to the procedure's documents on the public ЦАИС ЕОП - portal. Rendered as a child of the page header so it sits under the contract lede and is the - first action a reader sees. Solid ink → accent on hover, matching the site's button language. */ -.source-cta { - display: inline-flex; +/* Header actions row — wraps the page header's inline actions (source CTA + copy-citation + button) so they sit on one line and wrap gracefully on narrow viewports. */ +.header-actions { + display: flex; align-items: center; gap: var(--s-3); margin-top: var(--s-6); + flex-wrap: wrap; +} + +/* Source CTA & copy-citation button — solid, icon-led actions. The source CTA is a deep link to + the procedure's documents on the public ЦАИС ЕОП portal; the copy button reuses the same button + language. Solid ink → accent on hover, matching the site's button language. */ +.source-cta, +.save-btn { + display: inline-flex; + align-items: center; + gap: var(--s-3); padding: 11px var(--s-5); border: 1px solid var(--ink); background: var(--ink); @@ -664,21 +674,29 @@ main.narrow { transition: background 0.15s ease, border-color 0.15s ease; + cursor: pointer; } /* Keep the label/icon legible once the link is visited — the global `a:visited { color: --ink }` (specificity 0,1,1) would otherwise repaint the text ink-on-ink into an invisible black box. */ .source-cta:visited { color: var(--paper); } -.source-cta:hover { +.source-cta:hover, +.save-btn:hover { background: var(--accent); border-color: var(--accent); color: var(--paper); text-decoration: none; } -.source-cta svg { +.source-cta svg, +.save-btn svg { flex: none; } +/* Confirmation state after a successful copy — flip to the positive (teal) accent. */ +.save-btn.is-saved { + background: var(--pos); + border-color: var(--pos); +} .source-cta .cta-ext { font-size: 13px; opacity: 0.75; From 458c4d03ab9e41b7a29bfe765068f682c1ab622d Mon Sep 17 00:00:00 2001 From: Bilko Date: Sun, 5 Jul 2026 14:49:26 -0700 Subject: [PATCH 07/18] fix(web): grow source-cta/save-btn touch targets to 44px on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both buttons were ~34px tall (11px padding + 12px mono label) — below the 44px floor already applied to .nav-search/.nav-toggle in the same media block. --- apps/web/app/styles/chrome.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/app/styles/chrome.css b/apps/web/app/styles/chrome.css index 38576c8fe..07e30a419 100644 --- a/apps/web/app/styles/chrome.css +++ b/apps/web/app/styles/chrome.css @@ -777,4 +777,11 @@ main.narrow { padding: 11px 0; margin: -11px 0; } + /* source-cta/save-btn were ~34px tall (11px padding + 12px mono label) — below the 44px floor. + Grow the hit area invisibly, same trick as .nav-search/.nav-toggle above. */ + .source-cta, + .save-btn { + padding-block: 16px; + margin-block: -5px; + } } From 17c8a25582ca61b481efc025f53a237f6bf97421 Mon Sep 17 00:00:00 2001 From: Bilko Date: Fri, 10 Jul 2026 08:57:09 -0700 Subject: [PATCH 08/18] test(web): assert concrete citation literals instead of re-deriving via money() The expected strings were built with the same money() the implementation uses, so a formatting regression in money() would pass silently. Assert the concrete localized output instead. --- apps/web/app/lib/citation.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index e199a93e1..d3bd81d73 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { buildContractCitation, buildCompanyCitation, buildAuthorityCitation } from './citation'; -import { money } from '@sigma/shared'; describe('citation builders', () => { it('builds a contract citation', () => { @@ -18,7 +17,7 @@ describe('citation builders', () => { 'Договор: Доставка на компютри', 'Възложител: Община Пловдив', 'Изпълнител: Техно ООД', - `Стойност: ${money(125000.5)}`, + 'Стойност: 125 хил. €', 'Връзка: https://sigma.test/contracts/abc-123', ].join('\n'), ); @@ -60,7 +59,7 @@ describe('citation builders', () => { [ 'Компания: Техно ООД', 'ЕИК: 123456789', - `Общо спечелено: ${money(5000000)}`, + 'Общо спечелено: 5 млн. €', 'Брой договори: 42', 'Връзка: https://sigma.test/companies/techno-ood', ].join('\n'), @@ -82,7 +81,7 @@ describe('citation builders', () => { [ 'Компания: Чуждестранна фирма', 'ЕИК: Няма', - `Общо спечелено: ${money(0)}`, + 'Общо спечелено: 0 €', 'Брой договори: 1', 'Връзка: https://sigma.test/companies/foreign-corp', ].join('\n'), @@ -101,7 +100,7 @@ describe('citation builders', () => { expect(citation).toBe( [ 'Институция: Община Варна', - `Общо похарчено: ${money(1000000)}`, + 'Общо похарчено: 1 млн. €', 'Брой договори: 5', 'Връзка: https://sigma.test/authorities/obshtina-varna', ].join('\n'), From b10aa22accfb1b8222d32ad559015e7b90219a20 Mon Sep 17 00:00:00 2001 From: Bilko Date: Fri, 10 Jul 2026 08:57:13 -0700 Subject: [PATCH 09/18] fix(web): add clipboard fallback and rename copy-button classes navigator.clipboard is unavailable in insecure contexts / older browsers, so the copy button silently did nothing there. Fall back to document.execCommand('copy') and show a visible failure state when both paths fail. Also rename the save-btn/is-saved classes to copy-btn/is-copied to match what the component actually does. --- .../web/app/components/CopyCitationButton.tsx | 56 +++++++++++++++---- apps/web/app/styles/base.css | 2 +- apps/web/app/styles/chrome.css | 12 ++-- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/apps/web/app/components/CopyCitationButton.tsx b/apps/web/app/components/CopyCitationButton.tsx index fbaf41240..b1452fe97 100644 --- a/apps/web/app/components/CopyCitationButton.tsx +++ b/apps/web/app/components/CopyCitationButton.tsx @@ -1,7 +1,27 @@ import { useState, useCallback, useEffect, useRef } from 'react'; +function copyWithExecCommand(text: string): boolean { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + textarea.setAttribute('aria-hidden', 'true'); + textarea.tabIndex = -1; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + let ok = false; + try { + ok = document.execCommand('copy'); + } catch { + ok = false; + } + document.body.removeChild(textarea); + return ok; +} + export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { - const [copied, setCopied] = useState(false); + const [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle'); const timeoutRef = useRef(null); useEffect(() => { @@ -12,30 +32,42 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { }; }, []); + const resetAfterDelay = useCallback(() => { + if (timeoutRef.current !== null) { + window.clearTimeout(timeoutRef.current); + } + timeoutRef.current = window.setTimeout(() => setStatus('idle'), 2000); + }, []); + const handleCopy = useCallback(() => { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard .writeText(textToCopy) .then(() => { - setCopied(true); - if (timeoutRef.current !== null) { - window.clearTimeout(timeoutRef.current); - } - timeoutRef.current = window.setTimeout(() => setCopied(false), 2000); + setStatus('copied'); + resetAfterDelay(); }) .catch((err) => { console.error('Failed to copy text:', err); + setStatus(copyWithExecCommand(textToCopy) ? 'copied' : 'failed'); + resetAfterDelay(); }); + return; } - }, [textToCopy]); + setStatus(copyWithExecCommand(textToCopy) ? 'copied' : 'failed'); + resetAfterDelay(); + }, [textToCopy, resetAfterDelay]); + + const copied = status === 'copied'; + const failed = status === 'failed'; return ( ); diff --git a/apps/web/app/styles/base.css b/apps/web/app/styles/base.css index 38c9c5ad5..892ca9c47 100644 --- a/apps/web/app/styles/base.css +++ b/apps/web/app/styles/base.css @@ -162,7 +162,7 @@ code, .nav-backdrop, .search-drawer, .site-footer, - .save-btn { + .copy-btn { display: none !important; } diff --git a/apps/web/app/styles/chrome.css b/apps/web/app/styles/chrome.css index 07e30a419..2d120fac3 100644 --- a/apps/web/app/styles/chrome.css +++ b/apps/web/app/styles/chrome.css @@ -659,7 +659,7 @@ main.narrow { the procedure's documents on the public ЦАИС ЕОП portal; the copy button reuses the same button language. Solid ink → accent on hover, matching the site's button language. */ .source-cta, -.save-btn { +.copy-btn { display: inline-flex; align-items: center; gap: var(--s-3); @@ -682,18 +682,18 @@ main.narrow { color: var(--paper); } .source-cta:hover, -.save-btn:hover { +.copy-btn:hover { background: var(--accent); border-color: var(--accent); color: var(--paper); text-decoration: none; } .source-cta svg, -.save-btn svg { +.copy-btn svg { flex: none; } /* Confirmation state after a successful copy — flip to the positive (teal) accent. */ -.save-btn.is-saved { +.copy-btn.is-copied { background: var(--pos); border-color: var(--pos); } @@ -777,10 +777,10 @@ main.narrow { padding: 11px 0; margin: -11px 0; } - /* source-cta/save-btn were ~34px tall (11px padding + 12px mono label) — below the 44px floor. + /* source-cta/copy-btn were ~34px tall (11px padding + 12px mono label) — below the 44px floor. Grow the hit area invisibly, same trick as .nav-search/.nav-toggle above. */ .source-cta, - .save-btn { + .copy-btn { padding-block: 16px; margin-block: -5px; } From 05ff318cbc1554e7cd2a2543fd0ebebc8b8aa7e2 Mon Sep 17 00:00:00 2001 From: Bilko Date: Fri, 10 Jul 2026 23:59:17 -0700 Subject: [PATCH 10/18] fix(web): address PR #206 review feedback - add CopyCitationButton test coverage (clipboard success, execCommand fallback, copied/failed states, unmount timeout cleanup); wires up jsdom + testing-library for the first React component tests in the repo - extract the duplicated fallback origin literal into FALLBACK_ORIGIN in lib/meta.ts and use it in authority/company/contract routes - restrict the print-mode href-reveal rule to absolute http(s) links so internal relative links no longer get their href appended --- .../components/CopyCitationButton.test.tsx | 83 ++++ apps/web/app/lib/meta.ts | 4 + apps/web/app/routes/authority.tsx | 4 +- apps/web/app/routes/company.tsx | 4 +- apps/web/app/routes/contract.tsx | 4 +- apps/web/app/styles/base.css | 4 +- apps/web/package.json | 2 + apps/web/vitest.config.ts | 3 +- pnpm-lock.yaml | 430 +++++++++++++++++- 9 files changed, 527 insertions(+), 11 deletions(-) create mode 100644 apps/web/app/components/CopyCitationButton.test.tsx diff --git a/apps/web/app/components/CopyCitationButton.test.tsx b/apps/web/app/components/CopyCitationButton.test.tsx new file mode 100644 index 000000000..69c93bd20 --- /dev/null +++ b/apps/web/app/components/CopyCitationButton.test.tsx @@ -0,0 +1,83 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; +import { CopyCitationButton } from './CopyCitationButton'; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + vi.useRealTimers(); +}); + +describe('CopyCitationButton', () => { + it('shows the copied state after a successful Clipboard API write', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.assign(navigator, { clipboard: { writeText } }); + + render(); + fireEvent.click(screen.getByRole('button')); + + await waitFor(() => expect(writeText).toHaveBeenCalledWith('hello')); + await screen.findByText('Копирано!'); + expect(screen.getByRole('button').className).toContain('is-copied'); + }); + + it('falls back to execCommand when navigator.clipboard is unavailable', async () => { + Object.assign(navigator, { clipboard: undefined }); + const execCommand = vi.fn().mockReturnValue(true); + Object.assign(document, { execCommand }); + + render(); + fireEvent.click(screen.getByRole('button')); + + await screen.findByText('Копирано!'); + expect(execCommand).toHaveBeenCalledWith('copy'); + expect(screen.getByRole('button').className).toContain('is-copied'); + }); + + it('shows the failed state when both the Clipboard API and execCommand fail', async () => { + const writeText = vi.fn().mockRejectedValue(new Error('denied')); + Object.assign(navigator, { clipboard: { writeText } }); + const execCommand = vi.fn().mockReturnValue(false); + Object.assign(document, { execCommand }); + vi.spyOn(console, 'error').mockImplementation(() => {}); + + render(); + fireEvent.click(screen.getByRole('button')); + + await screen.findByText('Неуспешно копиране'); + expect(screen.getByRole('button').className).not.toContain('is-copied'); + expect(screen.getByRole('button').getAttribute('aria-label')).toBe( + 'Копирането не бе успешно', + ); + }); + + it('reports failed when execCommand throws while navigator.clipboard is unavailable', async () => { + Object.assign(navigator, { clipboard: undefined }); + Object.assign(document, { + execCommand: vi.fn(() => { + throw new Error('unsupported'); + }), + }); + + render(); + fireEvent.click(screen.getByRole('button')); + + await screen.findByText('Неуспешно копиране'); + }); + + it('clears the pending reset timeout on unmount', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.assign(navigator, { clipboard: { writeText } }); + const clearTimeoutSpy = vi.spyOn(window, 'clearTimeout'); + + const { unmount } = render(); + fireEvent.click(screen.getByRole('button')); + await screen.findByText('Копирано!'); + + clearTimeoutSpy.mockClear(); + unmount(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/app/lib/meta.ts b/apps/web/app/lib/meta.ts index cd7cfd5ad..23a1fa814 100644 --- a/apps/web/app/lib/meta.ts +++ b/apps/web/app/lib/meta.ts @@ -7,6 +7,10 @@ type MetaMatches = ReadonlyArray<{ id?: string; data?: unknown } | undefined> | undefined; +// Used as the origin for absolute URLs (citations, canonical links) when the root loader's +// origin isn't available (e.g. during error boundaries or before it has run). +export const FALLBACK_ORIGIN = 'https://sigma.midt.bg'; + export function getRootOrigin(matches: MetaMatches): string | undefined { const data = matches?.find((m) => m?.id === 'root')?.data as { origin?: string } | undefined; return data?.origin || undefined; diff --git a/apps/web/app/routes/authority.tsx b/apps/web/app/routes/authority.tsx index a4e9f9915..5547e075f 100644 --- a/apps/web/app/routes/authority.tsx +++ b/apps/web/app/routes/authority.tsx @@ -24,7 +24,7 @@ import { coverageRange, getCoverageMeta } from '../lib/coverage'; import { networkColumns, networkRows, trendYearColumns } from '../lib/entity-tables'; import { withDbRetry } from '../lib/retry'; import { buildAuthorityCitation } from '../lib/citation'; -import { seoMeta, getRootOrigin } from '../lib/meta'; +import { seoMeta, getRootOrigin, FALLBACK_ORIGIN } from '../lib/meta'; export function meta({ data, params, matches }: Route.MetaArgs) { const name = data?.authority.name ?? 'Институция'; @@ -61,7 +61,7 @@ export async function loader({ params, context }: Route.LoaderArgs) { export default function Authority({ loaderData }: Route.ComponentProps) { const matches = useMatches(); - const origin = getRootOrigin(matches) ?? 'https://sigma.midt.bg'; + const origin = getRootOrigin(matches) ?? FALLBACK_ORIGIN; const a = loaderData.authority; const { trend, network, competition } = loaderData; const ct = competition; diff --git a/apps/web/app/routes/company.tsx b/apps/web/app/routes/company.tsx index a91c42e2b..89d643ce4 100644 --- a/apps/web/app/routes/company.tsx +++ b/apps/web/app/routes/company.tsx @@ -25,7 +25,7 @@ import { coverageRange, getCoverageMeta } from '../lib/coverage'; import { networkColumns, networkRows, trendYearColumns } from '../lib/entity-tables'; import { withDbRetry } from '../lib/retry'; import { buildCompanyCitation } from '../lib/citation'; -import { seoMeta, getRootOrigin } from '../lib/meta'; +import { seoMeta, getRootOrigin, FALLBACK_ORIGIN } from '../lib/meta'; function isSingleNaturalPersonProfile(kind: string, legalForm: string | null): boolean { if (kind === 'consortium' || !legalForm) return false; @@ -82,7 +82,7 @@ export async function loader({ params, context }: Route.LoaderArgs) { export default function Company({ loaderData }: Route.ComponentProps) { const matches = useMatches(); - const origin = getRootOrigin(matches) ?? 'https://sigma.midt.bg'; + const origin = getRootOrigin(matches) ?? FALLBACK_ORIGIN; const c = loaderData.company; const { trend, network } = loaderData; const range = coverageRange(loaderData.coverage.coverageEndYear); diff --git a/apps/web/app/routes/contract.tsx b/apps/web/app/routes/contract.tsx index ec68d4454..12ee21bfc 100644 --- a/apps/web/app/routes/contract.tsx +++ b/apps/web/app/routes/contract.tsx @@ -12,7 +12,7 @@ import { RiskIndicators } from '../components/RiskIndicators'; import { publicCache } from '../lib/cache'; import { eopSourceFiles } from '../lib/eopSource'; import { buildContractCitation } from '../lib/citation'; -import { seoMeta, getRootOrigin } from '../lib/meta'; +import { seoMeta, getRootOrigin, FALLBACK_ORIGIN } from '../lib/meta'; /** * Compose the muted sub-line under „Брой оферти". The AOP feed gives us the gross submitted count @@ -72,7 +72,7 @@ const UNVERIFIED_VALUE_LABEL = 'стойност с непотвърдена д export default function Contract({ loaderData }: Route.ComponentProps) { const matches = useMatches(); - const origin = getRootOrigin(matches) ?? 'https://sigma.midt.bg'; + const origin = getRootOrigin(matches) ?? FALLBACK_ORIGIN; const c = loaderData.contract; const v = c.value; const crumbId = c.unp || c.contractNumber || c.id; diff --git a/apps/web/app/styles/base.css b/apps/web/app/styles/base.css index 892ca9c47..b03061004 100644 --- a/apps/web/app/styles/base.css +++ b/apps/web/app/styles/base.css @@ -166,8 +166,8 @@ code, display: none !important; } - /* Expose URLs in print */ - a[href]:not([href^='#'])::after { + /* Expose URLs in print — absolute http(s) links only, not internal relative links */ + a[href^='http']::after { content: ' (' attr(href) ')'; word-break: break-all; font-size: 0.9em; diff --git a/apps/web/package.json b/apps/web/package.json index 8e7de3aab..201200400 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,9 +29,11 @@ "@cloudflare/vite-plugin": "^1.29.1", "@react-router/dev": "7.15.1", "@tailwindcss/vite": "^4.2.2", + "@testing-library/react": "^16.3.2", "@types/node": "^22", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "jsdom": "^29.1.1", "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "vite": "^8.0.3", diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts index 1ef58a5ee..9eae4e73d 100644 --- a/apps/web/vitest.config.ts +++ b/apps/web/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', - include: ['app/**/*.test.ts', 'workers/**/*.test.ts'], + include: ['app/**/*.test.ts', 'app/**/*.test.tsx', 'workers/**/*.test.ts'], + environmentMatchGlobs: [['app/**/*.test.tsx', 'jsdom']], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c55798672..555d5b482 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,7 +33,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.7 - version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) + version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) wrangler: specifier: ^4.93.1 version: 4.93.1(@cloudflare/workers-types@4.20260521.1) @@ -89,6 +89,9 @@ importers: '@tailwindcss/vite': specifier: ^4.2.2 version: 4.3.0(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0)) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@types/node': specifier: ^22 version: 22.19.19 @@ -98,6 +101,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.15) + jsdom: + specifier: ^29.1.1 + version: 29.1.1 tailwindcss: specifier: ^4.2.2 version: 4.3.0 @@ -163,6 +169,21 @@ packages: resolution: {integrity: sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==} engines: {node: '>=18'} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -319,6 +340,10 @@ packages: peerDependencies: '@babel/core': ^7.29.6 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -343,6 +368,10 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@cloudflare/kv-asset-handler@0.5.0': resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} engines: {node: '>=22.0.0'} @@ -399,6 +428,42 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -564,6 +629,15 @@ packages: cpu: [x64] os: [win32] + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + 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 + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -1138,6 +1212,25 @@ packages: peerDependencies: vite: ^7.3.5 + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@turbo/darwin-64@2.9.14': resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} cpu: [x64] @@ -1171,6 +1264,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1239,9 +1335,20 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1254,6 +1361,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -1291,9 +1401,17 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1303,6 +1421,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -1311,10 +1432,17 @@ packages: babel-plugin-macros: optional: true + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + electron-to-chromium@1.5.360: resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} @@ -1322,6 +1450,10 @@ packages: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -1379,6 +1511,13 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isbot@5.1.40: resolution: {integrity: sha512-yNeeynhhtIVRBk12tBV4eHNxwB42HzR4Q3Ea7vCOiJhImGaAIdIMrbJtacQlBizGLjUPw+akkFI5Dn9T70XoVQ==} engines: {node: '>=18'} @@ -1390,6 +1529,15 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.0.2: resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} engines: {node: '>=6'} @@ -1484,12 +1632,23 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + miniflare@4.20260520.0: resolution: {integrity: sha512-krgebvYME9k7CjxiveTzx89kAMeIstfK3KfTqtzLb/4mtLMD74KHtU009h/I0CTDSVIYtXm0JzJ40OtiVRGmOA==} engines: {node: '>=22.0.0'} @@ -1518,6 +1677,9 @@ packages: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -1546,11 +1708,22 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + react-dom@19.2.6: resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: react: ^19.2.6 + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -1573,6 +1746,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + rolldown@1.0.3: resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1583,6 +1760,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1619,6 +1800,9 @@ packages: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} @@ -1645,6 +1829,21 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.8: + resolution: {integrity: sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==} + + tldts@7.4.8: + resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -1818,6 +2017,22 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -1850,6 +2065,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1888,6 +2110,26 @@ snapshots: dependencies: json-schema: 0.4.0 + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2093,6 +2335,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -2139,6 +2383,10 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@cloudflare/kv-asset-handler@0.5.0': {} '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260520.1)': @@ -2181,6 +2429,30 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -2275,6 +2547,8 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@exodus/bytes@1.15.1': {} + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': @@ -2679,6 +2953,27 @@ snapshots: tailwindcss: 4.3.0 vite: 8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0) + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.15 + '@types/react-dom': 19.2.3(@types/react@19.2.15) + '@turbo/darwin-64@2.9.14': optional: true @@ -2702,6 +2997,8 @@ snapshots: tslib: 2.8.1 optional: true + '@types/aria-query@5.0.4': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2782,8 +3079,16 @@ snapshots: '@opentelemetry/api': 1.9.1 zod: 4.4.3 + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + arg@5.0.2: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + assertion-error@2.0.1: {} babel-dead-code-elimination@1.0.12: @@ -2797,6 +3102,10 @@ snapshots: baseline-browser-mapping@2.10.31: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big-integer@1.6.52: {} blake3-wasm@2.1.5: {} @@ -2825,16 +3134,34 @@ snapshots: cookie@1.1.1: {} + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + csstype@3.2.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + dedent@1.7.2: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} + dom-accessibility-api@0.5.16: {} + electron-to-chromium@1.5.360: {} enhanced-resolve@5.21.6: @@ -2842,6 +3169,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + entities@8.0.0: {} + error-stack-parser-es@1.0.5: {} es-module-lexer@1.7.0: {} @@ -2902,12 +3231,46 @@ snapshots: graceful-fs@4.2.11: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + is-potential-custom-element-name@1.0.1: {} + isbot@5.1.40: {} jiti@2.7.0: {} js-tokens@4.0.0: {} + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 7.28.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@3.0.2: {} json-schema@0.4.0: {} @@ -2967,14 +3330,20 @@ snapshots: lodash@4.18.1: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lz-string@1.5.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mdn-data@2.27.1: {} + miniflare@4.20260520.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -3002,6 +3371,10 @@ snapshots: p-map@7.0.4: {} + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-to-regexp@6.3.0: {} pathe@1.1.2: {} @@ -3026,11 +3399,21 @@ snapshots: prettier@3.8.3: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + react-dom@19.2.6(react@19.2.6): dependencies: react: 19.2.6 scheduler: 0.27.0 + react-is@17.0.2: {} + react-refresh@0.14.2: {} react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): @@ -3045,6 +3428,8 @@ snapshots: readdirp@4.1.2: {} + require-from-string@2.0.2: {} + rolldown@1.0.3: dependencies: '@oxc-project/types': 0.133.0 @@ -3097,6 +3482,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} semver@6.3.1: {} @@ -3146,6 +3535,8 @@ snapshots: supports-color@10.2.2: {} + symbol-tree@3.2.4: {} + tailwindcss@4.3.0: {} tapable@2.3.3: {} @@ -3166,6 +3557,20 @@ snapshots: tinyrainbow@3.1.0: {} + tldts-core@7.4.8: {} + + tldts@7.4.8: + dependencies: + tldts-core: 7.4.8 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.8 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tslib@2.8.1: optional: true @@ -3263,7 +3668,7 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 - vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)): + vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)): dependencies: '@vitest/expect': 4.1.7 '@vitest/mocker': 4.1.7(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) @@ -3288,9 +3693,26 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 25.9.1 + jsdom: 29.1.1 transitivePeerDependencies: - msw + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -3323,6 +3745,10 @@ snapshots: ws@8.21.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + yallist@3.1.1: {} youch-core@0.3.3: From bc61c8d993d90aa5e705a6437012e63aa5fccc5f Mon Sep 17 00:00:00 2001 From: Bilko Date: Sat, 11 Jul 2026 00:56:05 -0700 Subject: [PATCH 11/18] fix(web): drop invalid vitest environmentMatchGlobs option, fix prettier format environmentMatchGlobs is not a valid Vitest 4.1.7 InlineConfig property (TS2769); jsdom is already set per-file via the @vitest-environment docblock in CopyCitationButton.test.tsx. --- apps/web/app/components/CopyCitationButton.test.tsx | 4 +--- apps/web/vitest.config.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/web/app/components/CopyCitationButton.test.tsx b/apps/web/app/components/CopyCitationButton.test.tsx index 69c93bd20..9ab0f7dee 100644 --- a/apps/web/app/components/CopyCitationButton.test.tsx +++ b/apps/web/app/components/CopyCitationButton.test.tsx @@ -47,9 +47,7 @@ describe('CopyCitationButton', () => { await screen.findByText('Неуспешно копиране'); expect(screen.getByRole('button').className).not.toContain('is-copied'); - expect(screen.getByRole('button').getAttribute('aria-label')).toBe( - 'Копирането не бе успешно', - ); + expect(screen.getByRole('button').getAttribute('aria-label')).toBe('Копирането не бе успешно'); }); it('reports failed when execCommand throws while navigator.clipboard is unavailable', async () => { diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts index 9eae4e73d..b569e5888 100644 --- a/apps/web/vitest.config.ts +++ b/apps/web/vitest.config.ts @@ -4,6 +4,5 @@ export default defineConfig({ test: { environment: 'node', include: ['app/**/*.test.ts', 'app/**/*.test.tsx', 'workers/**/*.test.ts'], - environmentMatchGlobs: [['app/**/*.test.tsx', 'jsdom']], }, }); From b057aaf547a820841a5b8992791c217e1872f1d5 Mon Sep 17 00:00:00 2001 From: Bilko Date: Sat, 11 Jul 2026 15:02:55 -0700 Subject: [PATCH 12/18] fix(web): address ydimitrof review round on PR #206 (copy citation button) - guard async clipboard callbacks with a mounted ref to skip state updates after unmount - reflect the copied state in the button's aria-label so it matches the aria-live announcement - restore navigator.clipboard/document.execCommand in afterEach so test mutations don't leak between tests - add a regression test for the unmount-before-resolve guard --- .../components/CopyCitationButton.test.tsx | 27 +++++++++++++++++++ .../web/app/components/CopyCitationButton.tsx | 9 ++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/web/app/components/CopyCitationButton.test.tsx b/apps/web/app/components/CopyCitationButton.test.tsx index 9ab0f7dee..59d99076f 100644 --- a/apps/web/app/components/CopyCitationButton.test.tsx +++ b/apps/web/app/components/CopyCitationButton.test.tsx @@ -3,10 +3,15 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup, waitFor } from '@testing-library/react'; import { CopyCitationButton } from './CopyCitationButton'; +const originalClipboard = navigator.clipboard; +const originalExecCommand = document.execCommand; + afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.useRealTimers(); + Object.assign(navigator, { clipboard: originalClipboard }); + Object.assign(document, { execCommand: originalExecCommand }); }); describe('CopyCitationButton', () => { @@ -78,4 +83,26 @@ describe('CopyCitationButton', () => { expect(clearTimeoutSpy).toHaveBeenCalled(); }); + + it('does not update state after unmount when the clipboard write resolves late', async () => { + let resolveWrite: () => void = () => {}; + const writeText = vi.fn( + () => + new Promise((resolve) => { + resolveWrite = resolve; + }), + ); + Object.assign(navigator, { clipboard: { writeText } }); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + const { unmount } = render(); + fireEvent.click(screen.getByRole('button')); + await waitFor(() => expect(writeText).toHaveBeenCalled()); + + unmount(); + resolveWrite(); + await Promise.resolve(); + + expect(errorSpy).not.toHaveBeenCalled(); + }); }); diff --git a/apps/web/app/components/CopyCitationButton.tsx b/apps/web/app/components/CopyCitationButton.tsx index b1452fe97..20eafcda9 100644 --- a/apps/web/app/components/CopyCitationButton.tsx +++ b/apps/web/app/components/CopyCitationButton.tsx @@ -23,9 +23,12 @@ function copyWithExecCommand(text: string): boolean { export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { const [status, setStatus] = useState<'idle' | 'copied' | 'failed'>('idle'); const timeoutRef = useRef(null); + const mountedRef = useRef(true); useEffect(() => { + mountedRef.current = true; return () => { + mountedRef.current = false; if (timeoutRef.current !== null) { window.clearTimeout(timeoutRef.current); } @@ -44,11 +47,13 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { navigator.clipboard .writeText(textToCopy) .then(() => { + if (!mountedRef.current) return; setStatus('copied'); resetAfterDelay(); }) .catch((err) => { console.error('Failed to copy text:', err); + if (!mountedRef.current) return; setStatus(copyWithExecCommand(textToCopy) ? 'copied' : 'failed'); resetAfterDelay(); }); @@ -66,7 +71,9 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { type="button" onClick={handleCopy} className={`copy-btn ${copied ? 'is-copied' : ''}`} - aria-label={failed ? 'Копирането не бе успешно' : 'Копирай данните като цитат'} + aria-label={ + failed ? 'Копирането не бе успешно' : copied ? 'Копирано!' : 'Копирай данните като цитат' + } title={failed ? 'Копирането не бе успешно — опитайте отново' : 'Копирай основните факти'} > {copied ? ( From 9ba56361586eec08e2abd8adb36d33d544bc7428 Mon Sep 17 00:00:00 2001 From: Bilko Date: Sat, 11 Jul 2026 19:23:46 -0700 Subject: [PATCH 13/18] fix(web): address remaining PR #206 review threads on citation.ts hasEik is redundant with eik (ETL only ever populates eik when eik_valid=1), so drop the optional hasEik gate that silently hid a present EIK when a caller omitted the field. Guard bidder null in buildContractCitation to avoid throwing on an unawarded contract. --- apps/web/app/lib/citation.test.ts | 42 +++++++++++++++++++++++++++++++ apps/web/app/lib/citation.ts | 7 +++--- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index d3bd81d73..9405f738c 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -66,6 +66,27 @@ describe('citation builders', () => { ); }); + it('handles contract with no awarded bidder', () => { + const c = { + subject: 'Прекратена процедура', + authority: { name: 'Община Пловдив' }, + bidder: null, + value: { currentEur: null }, + id: 'abc-123', + }; + + const citation = buildContractCitation(c, 'https://sigma.test'); + expect(citation).toBe( + [ + 'Договор: Прекратена процедура', + 'Възложител: Община Пловдив', + 'Изпълнител: —', + 'Стойност: —', + 'Връзка: https://sigma.test/contracts/abc-123', + ].join('\n'), + ); + }); + it('builds a company citation without EIK', () => { const c = { displayName: 'Чуждестранна фирма', @@ -88,6 +109,27 @@ describe('citation builders', () => { ); }); + it('shows the EIK when hasEik is absent from loaderData but eik is present', () => { + const c = { + displayName: 'Техно ООД', + eik: '123456789', + wonEur: 5000000, + contracts: 42, + slug: 'techno-ood', + }; + + const citation = buildCompanyCitation(c, 'https://sigma.test'); + expect(citation).toBe( + [ + 'Компания: Техно ООД', + 'ЕИК: 123456789', + 'Общо спечелено: 5 млн. €', + 'Брой договори: 42', + 'Връзка: https://sigma.test/companies/techno-ood', + ].join('\n'), + ); + }); + it('builds an authority citation', () => { const a = { name: 'Община Варна', diff --git a/apps/web/app/lib/citation.ts b/apps/web/app/lib/citation.ts index e9870d7ef..68e800df4 100644 --- a/apps/web/app/lib/citation.ts +++ b/apps/web/app/lib/citation.ts @@ -4,7 +4,7 @@ export function buildContractCitation( c: { subject: string; authority: { name: string }; - bidder: { displayName: string }; + bidder: { displayName: string } | null; value: { currentEur: number | null }; id: string; }, @@ -13,7 +13,7 @@ export function buildContractCitation( return [ `Договор: ${c.subject}`, `Възложител: ${c.authority.name}`, - `Изпълнител: ${c.bidder.displayName}`, + `Изпълнител: ${c.bidder ? c.bidder.displayName : '—'}`, `Стойност: ${money(c.value.currentEur)}`, `Връзка: ${origin}/contracts/${c.id}`, ].join('\n'); @@ -26,13 +26,12 @@ export function buildCompanyCitation( wonEur: number; contracts: number; slug: string; - hasEik?: boolean; }, origin: string, ): string { return [ `Компания: ${c.displayName}`, - `ЕИК: ${c.hasEik && c.eik ? c.eik : 'Няма'}`, + `ЕИК: ${c.eik ?? 'Няма'}`, `Общо спечелено: ${money(c.wonEur)}`, `Брой договори: ${count(c.contracts)}`, `Връзка: ${origin}/companies/${c.slug}`, From b05305123b1de4050cd8475bba629e56ffb38fee Mon Sep 17 00:00:00 2001 From: Bilko Date: Fri, 17 Jul 2026 23:26:08 -0700 Subject: [PATCH 14/18] fix(web): address ydimitrof review round 4 on PR #206 - CopyCitationButton: document the execCommand-after-rejected-writeText gesture-context tradeoff instead of silently leaving it unexplained - citation.test.ts: drop stray hasEik fixture field unused by buildCompanyCitation (a sibling test already proves output only depends on eik) --- apps/web/app/components/CopyCitationButton.tsx | 7 +++++++ apps/web/app/lib/citation.test.ts | 2 -- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/app/components/CopyCitationButton.tsx b/apps/web/app/components/CopyCitationButton.tsx index 20eafcda9..7759f9d8e 100644 --- a/apps/web/app/components/CopyCitationButton.tsx +++ b/apps/web/app/components/CopyCitationButton.tsx @@ -54,6 +54,13 @@ export function CopyCitationButton({ textToCopy }: { textToCopy: string }) { .catch((err) => { console.error('Failed to copy text:', err); if (!mountedRef.current) return; + // execCommand('copy') here runs after an awaited promise rejection, so in some + // browsers the original click's user-gesture context may already be gone by this + // point, which can make execCommand return false even though a synchronous-first + // attempt would have succeeded. We still degrade correctly to the 'failed' UI state + // in that case, and writeText (tried first, above) is the modern/preferred path that + // succeeds in the overwhelming majority of real browsers — so this ordering is kept + // rather than reversing the priority to chase a rarer edge case. setStatus(copyWithExecCommand(textToCopy) ? 'copied' : 'failed'); resetAfterDelay(); }); diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index 9405f738c..0aa3007cb 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -51,7 +51,6 @@ describe('citation builders', () => { wonEur: 5000000, contracts: 42, slug: 'techno-ood', - hasEik: true, }; const citation = buildCompanyCitation(c, 'https://sigma.test'); @@ -94,7 +93,6 @@ describe('citation builders', () => { wonEur: 0, contracts: 1, slug: 'foreign-corp', - hasEik: false, }; const citation = buildCompanyCitation(c, 'https://sigma.test'); From dd5a559e1883cc3319cdc340bb8759c0fa15523e Mon Sep 17 00:00:00 2001 From: Bilko Date: Mon, 20 Jul 2026 15:13:50 -0700 Subject: [PATCH 15/18] fix(web): document canonical contract slug in citation + de-duplicate citation test --- apps/web/app/lib/citation.test.ts | 22 +++++++++++----------- apps/web/app/lib/citation.ts | 2 ++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index 0aa3007cb..cb334ab0d 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -107,23 +107,23 @@ describe('citation builders', () => { ); }); - it('shows the EIK when hasEik is absent from loaderData but eik is present', () => { + it('leaves the EIK line blank for an empty string, since ?? only catches null', () => { const c = { - displayName: 'Техно ООД', - eik: '123456789', - wonEur: 5000000, - contracts: 42, - slug: 'techno-ood', + displayName: 'Празен ЕИК ЕООД', + eik: '', + wonEur: 250000, + contracts: 3, + slug: 'prazen-eik-eood', }; const citation = buildCompanyCitation(c, 'https://sigma.test'); expect(citation).toBe( [ - 'Компания: Техно ООД', - 'ЕИК: 123456789', - 'Общо спечелено: 5 млн. €', - 'Брой договори: 42', - 'Връзка: https://sigma.test/companies/techno-ood', + 'Компания: Празен ЕИК ЕООД', + 'ЕИК: ', + 'Общо спечелено: 250 хил. €', + 'Брой договори: 3', + 'Връзка: https://sigma.test/companies/prazen-eik-eood', ].join('\n'), ); }); diff --git a/apps/web/app/lib/citation.ts b/apps/web/app/lib/citation.ts index 68e800df4..84268eaad 100644 --- a/apps/web/app/lib/citation.ts +++ b/apps/web/app/lib/citation.ts @@ -15,6 +15,8 @@ export function buildContractCitation( `Възложител: ${c.authority.name}`, `Изпълнител: ${c.bidder ? c.bidder.displayName : '—'}`, `Стойност: ${money(c.value.currentEur)}`, + // `c.id` is already the percent-encoded slug (contractSlug output, see + // routes/contract.tsx), so the path-safe URL below needs no `slug` field of its own. `Връзка: ${origin}/contracts/${c.id}`, ].join('\n'); } From 652476b98374a48c09e176561780aa3450093f40 Mon Sep 17 00:00:00 2001 From: Bilko Date: Tue, 21 Jul 2026 17:08:55 -0700 Subject: [PATCH 16/18] =?UTF-8?q?fix(web):=20show=20=D0=9D=D1=8F=D0=BC?= =?UTF-8?q?=D0=B0=20for=20empty-string=20EIK,=20cover=20protocol-relative?= =?UTF-8?q?=20print=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Empty-string eik previously rendered a blank ЕИК line via ?? (only catches null/undefined); switched to || so falsy empty string also falls back. Print URL suffix now also matches // protocol-relative links, not just http(s). --- apps/web/app/lib/citation.test.ts | 4 ++-- apps/web/app/lib/citation.ts | 2 +- apps/web/app/styles/base.css | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/web/app/lib/citation.test.ts b/apps/web/app/lib/citation.test.ts index cb334ab0d..6f97f04f8 100644 --- a/apps/web/app/lib/citation.test.ts +++ b/apps/web/app/lib/citation.test.ts @@ -107,7 +107,7 @@ describe('citation builders', () => { ); }); - it('leaves the EIK line blank for an empty string, since ?? only catches null', () => { + it('shows "Няма" for an empty-string EIK, not a blank line', () => { const c = { displayName: 'Празен ЕИК ЕООД', eik: '', @@ -120,7 +120,7 @@ describe('citation builders', () => { expect(citation).toBe( [ 'Компания: Празен ЕИК ЕООД', - 'ЕИК: ', + 'ЕИК: Няма', 'Общо спечелено: 250 хил. €', 'Брой договори: 3', 'Връзка: https://sigma.test/companies/prazen-eik-eood', diff --git a/apps/web/app/lib/citation.ts b/apps/web/app/lib/citation.ts index 84268eaad..e0138a5aa 100644 --- a/apps/web/app/lib/citation.ts +++ b/apps/web/app/lib/citation.ts @@ -33,7 +33,7 @@ export function buildCompanyCitation( ): string { return [ `Компания: ${c.displayName}`, - `ЕИК: ${c.eik ?? 'Няма'}`, + `ЕИК: ${c.eik || 'Няма'}`, `Общо спечелено: ${money(c.wonEur)}`, `Брой договори: ${count(c.contracts)}`, `Връзка: ${origin}/companies/${c.slug}`, diff --git a/apps/web/app/styles/base.css b/apps/web/app/styles/base.css index b03061004..e189d0365 100644 --- a/apps/web/app/styles/base.css +++ b/apps/web/app/styles/base.css @@ -166,8 +166,9 @@ code, display: none !important; } - /* Expose URLs in print — absolute http(s) links only, not internal relative links */ - a[href^='http']::after { + /* Expose URLs in print — absolute http(s)/protocol-relative links only, not internal relative links */ + a[href^='http']::after, + a[href^='//']::after { content: ' (' attr(href) ')'; word-break: break-all; font-size: 0.9em; From a560c8880716b88d3787d0e46d29fa1f16bce155 Mon Sep 17 00:00:00 2001 From: Bilko Date: Tue, 21 Jul 2026 22:43:53 -0700 Subject: [PATCH 17/18] build(deps): bump sharp to ^0.35.0 (GHSA-f88m-g3jw-g9cj) --- pnpm-lock.yaml | 387 ++++++++++++++++++++++++++++---------------- pnpm-workspace.yaml | 3 + 2 files changed, 251 insertions(+), 139 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 555d5b482..7fbeb0146 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,7 @@ overrides: vite@8: ^8.0.16 undici: ^7.28.0 '@babel/core': ^7.29.6 + sharp: ^0.35.0 importers: @@ -36,7 +37,7 @@ importers: version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.1)(jiti@2.7.0)) wrangler: specifier: ^4.93.1 - version: 4.93.1(@cloudflare/workers-types@4.20260521.1) + version: 4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@25.9.1) apps/etl: dependencies: @@ -82,10 +83,10 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.29.1 - version: 1.37.3(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(workerd@1.20260520.1)(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)) + version: 1.37.3(@types/node@22.19.19)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(workerd@1.20260520.1)(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19)) '@react-router/dev': specifier: 7.15.1 - version: 7.15.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)) + version: 7.15.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19)) '@tailwindcss/vite': specifier: ^4.2.2 version: 4.3.0(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0)) @@ -115,7 +116,7 @@ importers: version: 8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0) wrangler: specifier: ^4.75.0 - version: 4.93.1(@cloudflare/workers-types@4.20260521.1) + version: 4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19) packages/api-contract: dependencies: @@ -470,6 +471,9 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -642,152 +646,161 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -1776,12 +1789,22 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2395,15 +2418,16 @@ snapshots: optionalDependencies: workerd: 1.20260520.1 - '@cloudflare/vite-plugin@1.37.3(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(workerd@1.20260520.1)(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1))': + '@cloudflare/vite-plugin@1.37.3(@types/node@22.19.19)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(workerd@1.20260520.1)(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19))': dependencies: '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260520.1) - miniflare: 4.20260520.0 + miniflare: 4.20260520.0(@types/node@22.19.19) unenv: 2.0.0-rc.24 vite: 8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0) - wrangler: 4.93.1(@cloudflare/workers-types@4.20260521.1) + wrangler: 4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19) ws: 8.21.0 transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate - workerd @@ -2464,6 +2488,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -2551,98 +2580,108 @@ snapshots: '@img/colour@1.1.0': {} - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.2 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.2 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.2': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.2': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.2': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.2': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.2': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.2': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.2 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.2 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.2 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.2 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.2 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.2 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.3': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.3': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.11.2 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-ia32@0.35.3': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-x64@0.35.3': optional: true '@jridgewell/gen-mapping@0.3.13': @@ -2694,7 +2733,7 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@react-router/dev@7.15.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1))': + '@react-router/dev@7.15.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19))': dependencies: '@babel/core': 7.29.7 '@babel/generator': 7.29.1 @@ -2728,7 +2767,7 @@ snapshots: vite-node: 3.2.4(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0) optionalDependencies: typescript: 5.9.3 - wrangler: 4.93.1(@cloudflare/workers-types@4.20260521.1) + wrangler: 4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3344,15 +3383,29 @@ snapshots: mdn-data@2.27.1: {} - miniflare@4.20260520.0: + miniflare@4.20260520.0(@types/node@22.19.19): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.3(@types/node@22.19.19) + undici: 7.28.0 + workerd: 1.20260520.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + miniflare@4.20260520.0(@types/node@25.9.1): dependencies: '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 + sharp: 0.35.3(@types/node@25.9.1) undici: 7.28.0 workerd: 1.20260520.1 ws: 8.21.0 youch: 4.1.0-beta.10 transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate @@ -3492,38 +3545,75 @@ snapshots: semver@7.8.0: {} + semver@7.8.5: {} + set-cookie-parser@2.7.2: {} - sharp@0.34.5: + sharp@0.35.3(@types/node@22.19.19): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.0 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 22.19.19 + + sharp@0.35.3(@types/node@25.9.1): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 25.9.1 siginfo@2.0.0: {} @@ -3726,13 +3816,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260520.1 '@cloudflare/workerd-windows-64': 1.20260520.1 - wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1): + wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260520.1) blake3-wasm: 2.1.5 esbuild: 0.28.1 - miniflare: 4.20260520.0 + miniflare: 4.20260520.0(@types/node@22.19.19) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.24 workerd: 1.20260520.1 @@ -3740,6 +3830,25 @@ snapshots: '@cloudflare/workers-types': 4.20260521.1 fsevents: 2.3.3 transitivePeerDependencies: + - '@types/node' + - bufferutil + - utf-8-validate + + wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@25.9.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260520.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 4.20260520.0(@types/node@25.9.1) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260520.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260521.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - '@types/node' - bufferutil - utf-8-validate diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 96815ceab..e3872b5c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,9 @@ overrides: # @babel/core <7.29.6 — arbitrary file read via sourceMappingURL (GHSA-4x5r-pxfx-6jf8); # dev/build-time only (via @react-router/dev), never ships to the Worker. '@babel/core': '^7.29.6' + # sharp <0.35.0 — HIGH severity (CVSS 7.0) advisory GHSA-f88m-g3jw-g9cj, via + # wrangler→miniflare. Dev/build-time only; never ships to the Worker. + sharp: '^0.35.0' onlyBuiltDependencies: - esbuild From eaff39516f510ce0177968d3aea241ff0e9bccef Mon Sep 17 00:00:00 2001 From: Bilko Date: Mon, 27 Jul 2026 00:30:45 -0700 Subject: [PATCH 18/18] build(deps): patch postcss/valibot CVEs, bump react-router within 7.x, suppress RSC-only CSRF Bump postcss to ^8.5.18 (GHSA-r28c-9q8g-f849) and valibot to ^1.4.2 (GHSA-5qjj-4xww-7phc) via pnpm overrides - both patch-level, non-breaking fixes. Bump react-router/@react-router/dev to ^7.18.0 via override, fixing 4 real advisories (SSR hydration constructor injection, unauthenticated DoS, RSCErrorHandler XSS, open-redirect via backslash) - all fixed within the 7.x line, no major bump needed. Add a time-boxed osv-scanner.toml suppression for the one remaining advisory, GHSA-qwww-vcr4-c8h2, a CSRF flaw scoped to unstable RSC APIs this app does not use (verified via repo-wide grep) with no fix in the 7.x line; bumping to 8.x is out of scope for this patch. --- osv-scanner.toml | 26 +++++ pnpm-lock.yaml | 233 +++++++++++--------------------------------- pnpm-workspace.yaml | 15 +++ 3 files changed, 98 insertions(+), 176 deletions(-) create mode 100644 osv-scanner.toml diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 000000000..9959226bf --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,26 @@ +# OSV-Scanner suppression list, read automatically by the "Dependency audit" step in +# .github/workflows/ci.yml (`osv-scanner scan source -L pnpm-lock.yaml`). +# +# EVERY entry MUST document: which package + advisory, WHY it is safe to ignore (dev-only / +# not shipped), the version that FIXES it, and an `ignoreUntil` review date so the scanner +# re-flags it if we forget. +# +# AT EVERY MERGE / DEPENDENCY BUMP: re-check each entry. If the offending package has since +# been raised to (or past) its fixed version, DELETE the entry — do not let suppressions +# outlive the vulnerability they cover. `pnpm why ` shows the resolved version and +# what pulls it in. + +# ── react-router 7.18.0 — GHSA-qwww-vcr4-c8h2 (High, CVSS 7.1), fixed in 8.3.0 ────────── +# WHY IGNORED: this CVE is a CSRF flaw in react-router's UNSTABLE RSC (React Server +# Components) code paths only — "this only affects your application if you are using the +# unstable RSC APIs" per the advisory. Verified via `git grep` across this repo for RSC +# usage (unstable_.*RSC, react-server, unstable_RSCPayload, unstable_routeRSCServerRequest): +# zero hits. This app does not use RSC. No fix exists in the 7.x line (introduced in 7.12.0, +# only patched in 8.3.0) — upgrading to react-router 8.x is a major, breaking version bump +# out of scope for a security patch to a code path this app never exercises. +# REMOVE WHEN: this app adopts react-router's RSC APIs (re-evaluate applicability first), or +# a deliberate, separately-planned major-version upgrade to react-router 8.x lands. +[[IgnoredVulns]] +id = "GHSA-qwww-vcr4-c8h2" +ignoreUntil = 2026-10-01T00:00:00Z +reason = "CSRF in react-router's unstable RSC code paths only (GHSA-qwww-vcr4-c8h2) - this app does not use RSC (verified via repo-wide grep for RSC APIs). No fix in the 7.x line; upgrading to 8.x is a major breaking change out of scope for a security patch to an unused code path." diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fbeb0146..895f6b4cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,10 @@ overrides: undici: ^7.28.0 '@babel/core': ^7.29.6 sharp: ^0.35.0 + postcss: ^8.5.18 + valibot: ^1.4.2 + react-router: ^7.18.0 + '@react-router/dev': ^7.18.0 importers: @@ -78,15 +82,15 @@ importers: specifier: ^19.2.6 version: 19.2.6(react@19.2.6) react-router: - specifier: 7.15.1 - version: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: ^7.18.0 + version: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) devDependencies: '@cloudflare/vite-plugin': specifier: ^1.29.1 version: 1.37.3(@types/node@22.19.19)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(workerd@1.20260520.1)(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19)) '@react-router/dev': - specifier: 7.15.1 - version: 7.15.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19)) + specifier: ^7.18.0 + version: 7.18.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19)) '@tailwindcss/vite': specifier: ^4.2.2 version: 4.3.0(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0)) @@ -185,10 +189,6 @@ packages: '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -201,10 +201,6 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} @@ -223,10 +219,6 @@ packages: peerDependencies: '@babel/core': ^7.29.6 - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} @@ -235,20 +227,10 @@ packages: resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.29.7': resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.29.6 - '@babel/helper-module-transforms@7.29.7': resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} @@ -273,26 +255,14 @@ packages: resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.29.7': resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} @@ -301,11 +271,6 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} @@ -345,26 +310,14 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} @@ -848,14 +801,14 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@react-router/dev@7.15.1': - resolution: {integrity: sha512-BlFEU7SjPQHJDfYuw5qJU3+p4wMPEvKpf5Kj64/rRzQQjncXzhzkIJ0xreAQSYgGwJWjIXIK9swOaeE2czhulw==} + '@react-router/dev@7.18.1': + resolution: {integrity: sha512-1Yu5272BI8g5sl1jwmhM3aFeajXQwNbWP4qrdMYfC+ayT2khxV6KPCitLNJl+jT6DMGEKb/hmwcmKd0lFIBjxQ==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: - '@react-router/serve': ^7.15.1 + '@react-router/serve': ^7.18.1 '@vitejs/plugin-rsc': ~0.5.21 - react-router: ^7.15.1 + react-router: ^7.18.0 react-server-dom-webpack: ^19.2.3 typescript: ^5.1.0 || ^6.0.0 vite: ^7.3.5 @@ -872,11 +825,11 @@ packages: wrangler: optional: true - '@react-router/node@7.15.1': - resolution: {integrity: sha512-lv68RaqmIa/ZRlIrGcl79HimaqpU3yV1CFKnmItU+xqI+xn9g5fqsh2Vj2LdNjnlzJgVsRMEpnv00t/6RgDrgw==} + '@react-router/node@7.18.1': + resolution: {integrity: sha512-2GhAwa90z/+GMFM8Q5HsgwzakYogbQcZpqpNonhN4YWDRjWkSz+t5jgwNAFvG8ENR8fKGEVEsOHIaNVDfW9Ouw==} engines: {node: '>=20.0.0'} peerDependencies: - react-router: 7.15.1 + react-router: ^7.18.0 typescript: ^5.1.0 || ^6.0.0 peerDependenciesMeta: typescript: @@ -1670,8 +1623,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1712,8 +1665,8 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} prettier@3.8.3: @@ -1741,8 +1694,8 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - react-router@7.15.1: - resolution: {integrity: sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==} + react-router@7.18.1: + resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -1784,11 +1737,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} - engines: {node: '>=10'} - hasBin: true - semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -1903,8 +1851,8 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - valibot@1.4.0: - resolution: {integrity: sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -2153,12 +2101,6 @@ snapshots: '@asamuzakjp/nwsapi@2.3.9': {} - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2187,14 +2129,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.0.2 - '@babel/generator@7.29.7': dependencies: '@babel/parser': 7.29.7 @@ -2205,7 +2139,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/helper-compilation-targets@7.29.7': dependencies: @@ -2223,26 +2157,17 @@ snapshots: '@babel/helper-optimise-call-expression': 7.27.1 '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.7) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-globals@7.28.0': {} - '@babel/helper-globals@7.29.7': {} '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -2253,15 +2178,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -2273,7 +2189,7 @@ snapshots: '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/helper-plugin-utils@7.28.6': {} @@ -2282,27 +2198,21 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.27.1': {} - '@babel/helper-validator-option@7.29.7': {} '@babel/helpers@7.29.7': @@ -2310,10 +2220,6 @@ snapshots: '@babel/template': 7.29.7 '@babel/types': 7.29.7 - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 @@ -2331,7 +2237,7 @@ snapshots: '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color @@ -2351,7 +2257,7 @@ snapshots: dependencies: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 + '@babel/helper-validator-option': 7.29.7 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.7) '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.7) '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.7) @@ -2360,30 +2266,12 @@ snapshots: '@babel/runtime@7.29.7': {} - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -2396,11 +2284,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 @@ -2733,16 +2616,16 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@react-router/dev@7.15.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19))': + '@react-router/dev@7.18.1(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0)(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)(vite@8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0))(wrangler@4.93.1(@cloudflare/workers-types@4.20260521.1)(@types/node@22.19.19))': dependencies: '@babel/core': 7.29.7 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.7) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7) - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@react-router/node': 7.15.1(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@react-router/node': 7.18.1(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3) '@remix-run/node-fetch-server': 0.13.3 arg: 5.0.2 babel-dead-code-elimination: 1.0.12 @@ -2759,10 +2642,10 @@ snapshots: pkg-types: 2.3.1 prettier: 3.8.3 react-refresh: 0.14.2 - react-router: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - semver: 7.8.0 - tinyglobby: 0.2.16 - valibot: 1.4.0(typescript@5.9.3) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + semver: 7.8.5 + tinyglobby: 0.2.17 + valibot: 1.4.2(typescript@5.9.3) vite: 8.0.16(@types/node@22.19.19)(esbuild@0.28.1)(jiti@2.7.0) vite-node: 3.2.4(@types/node@22.19.19)(jiti@2.7.0)(lightningcss@1.32.0) optionalDependencies: @@ -2783,10 +2666,10 @@ snapshots: - tsx - yaml - '@react-router/node@7.15.1(react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)': + '@react-router/node@7.18.1(react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(typescript@5.9.3)': dependencies: '@mjackson/node-fetch-server': 0.2.0 - react-router: 7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router: 7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) optionalDependencies: typescript: 5.9.3 @@ -3133,9 +3016,9 @@ snapshots: babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.3 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -3411,7 +3294,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.12: {} + nanoid@3.3.16: {} node-releases@2.0.45: {} @@ -3444,9 +3327,9 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - postcss@8.5.15: + postcss@8.5.23: dependencies: - nanoid: 3.3.12 + nanoid: 3.3.16 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -3469,7 +3352,7 @@ snapshots: react-refresh@0.14.2: {} - react-router@7.15.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + react-router@7.18.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6): dependencies: cookie: 1.1.1 react: 19.2.6 @@ -3543,8 +3426,6 @@ snapshots: semver@6.3.1: {} - semver@7.8.0: {} - semver@7.8.5: {} set-cookie-parser@2.7.2: {} @@ -3693,7 +3574,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - valibot@1.4.0(typescript@5.9.3): + valibot@1.4.2(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -3723,9 +3604,9 @@ snapshots: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.23 rollup: 4.60.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 22.19.19 fsevents: 2.3.3 @@ -3736,7 +3617,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.23 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -3749,7 +3630,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.23 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e3872b5c4..08f1cfce9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -27,6 +27,21 @@ overrides: # sharp <0.35.0 — HIGH severity (CVSS 7.0) advisory GHSA-f88m-g3jw-g9cj, via # wrangler→miniflare. Dev/build-time only; never ships to the Worker. sharp: '^0.35.0' + # postcss <8.5.18 — path traversal via sourceMappingURL auto-load + # (GHSA-r28c-9q8g-f849); patch-level fix. + # valibot <1.4.2 — flatten() crashes on inherited-property keys + # (GHSA-5qjj-4xww-7phc); patch-level fix. + postcss: '^8.5.18' + valibot: '^1.4.2' + # react-router <7.18.0 — 4 advisories fixed within the 7.x line (no major bump needed): + # SSR hydration constructor injection (GHSA-337j-9hxr-rhxg), unauthenticated + # DoS via inefficient route matching (GHSA-chx6-hx7r-mcp5), RSCErrorHandler + # missing protocol validation / XSS (GHSA-h8fp-f39c-q6mh), open redirect via + # backslash in Link/useNavigate (GHSA-wrjc-x8rr-h8h6). The separate + # GHSA-qwww-vcr4-c8h2 (RSC-only CSRF, needs 8.x) is NOT fixed by this pin — + # see osv-scanner.toml for why that one is suppressed instead of bumped. + 'react-router': '^7.18.0' + '@react-router/dev': '^7.18.0' onlyBuiltDependencies: - esbuild