diff --git a/.gitignore b/.gitignore index d743649c..0a09f9a2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ # next.js /.next/ /out/ +public/robots.txt +public/sitemap*.xml # production /build diff --git a/app/[locale]/[state]/analytics/components/default-output-window.tsx b/app/[locale]/[state]/analytics/components/default-output-window.tsx index 87a87604..987a1c70 100644 --- a/app/[locale]/[state]/analytics/components/default-output-window.tsx +++ b/app/[locale]/[state]/analytics/components/default-output-window.tsx @@ -1,4 +1,9 @@ import React from 'react'; +import { useTranslations } from 'next-intl'; +import { Button, Icon, Text } from 'opub-ui'; + +import { docsLink, userGuideLink } from '@/config/site'; +import { cn } from '@/lib/utils'; import { Ellipse, Exposure, @@ -7,19 +12,11 @@ import { RiskScore, Vulnerability, } from '@/components/FactorIcons'; -import { useTranslations } from 'next-intl'; -import { Button, Icon, Text } from 'opub-ui'; - -import { docsLink, userGuideLink } from '@/config/site'; -import { cn } from '@/lib/utils'; import Icons from '@/components/icons'; import { MediaRendering } from '@/components/media-rendering'; import styles from './styles.module.scss'; -export function DefaultWindow({ - indicatorDescriptions, - onClose, -}: any) { +export function DefaultWindow({ indicatorDescriptions, onClose }: any) { const t = useTranslations('analytics.detail'); const tCommon = useTranslations('common'); const list: { title: string; slug: string; description: string }[] = []; @@ -57,11 +54,7 @@ export function DefaultWindow({ > {/* State-level header with only close button (no icon/title) */}
-
@@ -79,11 +72,7 @@ export function DefaultWindow({ ); } -export const AboutIndicator = ({ - IndicatorData, -}: { - IndicatorData: any; -}) => { +export const AboutIndicator = ({ IndicatorData }: { IndicatorData: any }) => { const t = useTranslations('analytics.about'); const IconMap: { [key: string]: React.ReactNode } = { 'risk-score': , @@ -114,10 +103,11 @@ export const AboutIndicator = ({ /> - - {t('calculation')} - - + {IndicatorData.length > 1 && ( + + {t('calculation')} + + )}
{IndicatorData.slice(1)?.map((indicator: any, index: number) => (
); }; - - diff --git a/app/[locale]/glossary/page.tsx b/app/[locale]/glossary/page.tsx new file mode 100644 index 00000000..227a02be --- /dev/null +++ b/app/[locale]/glossary/page.tsx @@ -0,0 +1,48 @@ +import { notFound } from 'next/navigation'; +import { getTranslations, setRequestLocale } from 'next-intl/server'; +import { Text } from 'opub-ui'; + +import { features, glossaryCsv } from '@/config/site'; +import { parseGlossary } from '@/lib/glossary'; +import GlossaryClient from '@/components/glossary/glossary-client'; +import GlossaryHeaderNav from '@/components/glossary/glossary-header-nav'; + +export const dynamic = 'force-static'; + +export default async function GlossaryPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setRequestLocale(locale); + + if (!features.glossary) notFound(); + + const t = await getTranslations({ locale, namespace: 'glossary' }); + const terms = parseGlossary(glossaryCsv); + + return ( +
+
+
+ + {t('heading')} + + + {t('description')} + + +
+
+ +
+ +
+
+ ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 7bde969a..603c97c8 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -126,20 +126,22 @@ export default async function LocaleLayout({ - - - - - - - - {children} - - {Footer && ( +
+ + + -
+ - )} + +
{children}
+ + {Footer && ( + +
+ + )} +
diff --git a/components/glossary/glossary-client.tsx b/components/glossary/glossary-client.tsx new file mode 100644 index 00000000..521ab167 --- /dev/null +++ b/components/glossary/glossary-client.tsx @@ -0,0 +1,345 @@ +'use client'; + +import * as React from 'react'; +import { useTranslations } from 'next-intl'; +import type { GlossaryTerm } from 'ids-drr-branding-types'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Divider, + SearchInput, + Tab, + TabList, + TabPanel, + Tabs, + Tag, + Text, +} from 'opub-ui'; + +import { slugifyGlossaryTerm } from '@/lib/glossary'; + +type Props = { + terms: GlossaryTerm[]; +}; + +type Indexed = GlossaryTerm & { slug: string; letter: string }; + +function indexTerms(terms: GlossaryTerm[]): Indexed[] { + return terms.map((t) => ({ + ...t, + slug: slugifyGlossaryTerm(t.term), + letter: (t.term[0] ?? '#').toUpperCase(), + })); +} + +function groupByLetter(items: Indexed[]) { + const map = new Map(); + for (const item of items) { + const existing = map.get(item.letter); + if (existing) existing.push(item); + else map.set(item.letter, [item]); + } + return [...map.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([letter, group]) => ({ + letter, + items: group.sort((x, y) => x.term.localeCompare(y.term)), + })); +} + +function uniqueTags(terms: GlossaryTerm[]): string[] { + const labelsByKey = new Map(); + for (const t of terms) { + const raw = t.tag?.trim(); + if (!raw) continue; + const key = raw.toLowerCase(); + if (!labelsByKey.has(key)) labelsByKey.set(key, raw); + } + return [...labelsByKey.values()].sort((a, b) => a.localeCompare(b)); +} + +export default function GlossaryClient({ terms }: Props) { + const t = useTranslations('glossary'); + const tFilters = useTranslations('common.filters'); + + const indexed = React.useMemo(() => indexTerms(terms), [terms]); + const tags = React.useMemo(() => uniqueTags(terms), [terms]); + + const [selectedTag, setSelectedTag] = React.useState(null); + const [query, setQuery] = React.useState(''); + const [openSlug, setOpenSlug] = React.useState(null); + const deferredQuery = React.useDeferredValue(query); + const normalizedQuery = deferredQuery.trim().toLowerCase(); + + const filtered = React.useMemo(() => { + const tagKey = selectedTag?.toLowerCase(); + return indexed.filter((item) => { + if (tagKey && item.tag?.toLowerCase() !== tagKey) return false; + if (normalizedQuery && !item.term.toLowerCase().includes(normalizedQuery)) + return false; + return true; + }); + }, [indexed, selectedTag, normalizedQuery]); + + const grouped = React.useMemo(() => groupByLetter(filtered), [filtered]); + + return ( +
+ setQuery(value)} + onClear={() => setQuery('')} + /> + +
+
{ + setSelectedTag(null); + }} + className="cursor-pointer" + > + + {tFilters('all')} + +
+ {tags.map((filter) => { + const isActive = selectedTag?.toLowerCase() === filter.toLowerCase(); + return ( +
{ + setSelectedTag(filter); + setOpenSlug(null); + }} + className="cursor-pointer" + > + + {filter} + +
+ ); + })} +
+ + {grouped.length > 0 ? ( + grouped.map(({ letter, items }) => ( +
+
+ + {letter} + +
+ + + { + setOpenSlug(val || null); + }} + className="divide-y " + > + {items.map((item) => { + const isOpen = openSlug === item.slug; + return ( + + +
+
+ {item.term} +
+ {item.summary} +
+
+ + +
+ {isOpen && ( +
+
+ + {t('detail.headings.definition')} + + + {item.definition} + +
+ +
+ + + +
+ + + + {item.disasterMethodology && + item.disasterMethodology.length > 0 && ( +
+ + {t('detail.headings.disasterMethodology')} + +
+ {item.disasterMethodology.map((d) => ( +
+ + {d.disasterType} + + + {d.methodology} + +
+ ))} +
+
+ )} + + {item.related && item.related.length > 0 && ( + + )} + + {item.misinterpretation && ( +
+ + {t('detail.headings.misinterpretation')} + + + {item.misinterpretation} + +
+ )} +
+ )} +
+
+
+ ); + })} +
+
+ )) + ) : ( +
+ + {t('empty')} + +
+ )} +
+ ); +} + +function InfoCard({ title, items }: { title: string; items: string[] }) { + return ( +
+ + {title} + +
+ {items.map((item) => ( + + {item} + + ))} +
+
+ ); +} + +function DetailsCard({ title, body }: { title: string; body: string }) { + return ( +
+ + {title} + + + {body} + +
+ ); +} + +function ContextTabs({ policy, model }: { policy?: string; model?: string }) { + const t = useTranslations('glossary.detail'); + if (!policy && !model) return null; + return ( +
+ + {t('headings.interpretation')} + + + + + {policy && {t('tabs.policy.title')}} + {model && {t('tabs.model.title')}} + + {policy && ( + +
+ + {t('tabs.policy.heading')} + + + {policy} + +
+
+ )} + {model && ( + +
+ + {t('tabs.model.heading')} + + + {model} + +
+
+ )} +
+
+ ); +} diff --git a/components/glossary/glossary-header-nav.tsx b/components/glossary/glossary-header-nav.tsx new file mode 100644 index 00000000..676d8d72 --- /dev/null +++ b/components/glossary/glossary-header-nav.tsx @@ -0,0 +1,53 @@ +'use client'; + +import React from 'react'; +import Link from 'next/link'; +import { useTranslations } from 'next-intl'; +import { Icon, Text } from 'opub-ui'; + +import { docsLink, userManualLink } from '@/config/site'; +import Icons from '../icons'; + +export default function GlossaryHeaderNav() { + const t = useTranslations('glossary'); + return ( +
+ {userManualLink && ( + +
+ + {t('userManualLink')} + + +
+ + )} + {docsLink && ( + +
+ + {t('docsLink')} + + +
+ + )} +
+ ); +} diff --git a/components/main-nav.tsx b/components/main-nav.tsx index 3a6d2c71..dff931ff 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -3,10 +3,10 @@ import React from 'react'; import Image from 'next/image'; import { useKeyDetect } from '@/hooks/use-key-detect'; -import { languages, logo, mainNav } from '@/config/site'; import { useTranslations } from 'next-intl'; import { Text } from 'opub-ui'; +import { languages, logo, mainNav } from '@/config/site'; import { routes } from '@/lib/routes'; import { TranslateDropdown } from './langSelect/lang-select'; import NavLink from './nav-link'; diff --git a/config/site.ts b/config/site.ts index 6acfe44e..69b0dc29 100644 --- a/config/site.ts +++ b/config/site.ts @@ -2,8 +2,8 @@ import { config } from 'ids-drr-branding'; import type { Language, Resource, - StaticImageAsset, State, + StaticImageAsset, Story, } from 'ids-drr-branding-types'; @@ -17,7 +17,8 @@ export const languages: Language[] = config.languages ?? []; // Images export const logo: StaticImageAsset | undefined = config.logo; -export const heroForeground: StaticImageAsset | undefined = config.heroForeground; +export const heroForeground: StaticImageAsset | undefined = + config.heroForeground; export const heroBackground: string = config.heroBackground ?? ''; export const favicon: string = config.favicon ?? ''; export const appleIcon: string = config.appleIcon ?? ''; @@ -25,15 +26,21 @@ export const openGraphImage: string = config.openGraphImage ?? ''; // Links export const userGuideLink: string = config.userGuideLink ?? ''; +export const userManualLink: string = config.userManualLink ?? ''; export const docsLink: string = config.docsLink ?? ''; +// Content +export const glossaryCsv: string = config.glossaryCsv ?? ''; + // English is always loaded internally as the missing-key fallback, // but a deployment can omit it from `locales` to disable /en/ URLs. export const FALLBACK_LOCALE = 'en'; export const locales: string[] = config.locales ?? [FALLBACK_LOCALE]; export const defaultLocale: string = config.defaultLocale ?? locales[0]; -export const messages: Record> = - config.messages ?? {}; +export const messages: Record< + string, + Record +> = config.messages ?? {}; // Feature flags const dataSpaceEnabled = Boolean(process.env.NEXT_PUBLIC_BACKEND_URL); @@ -42,6 +49,7 @@ export const features = { datasets: dataSpaceEnabled, aboutUs: config.features?.aboutUs ?? false, reports: config.features?.reports ?? false, + glossary: Boolean(config.glossaryCsv), }; // Navigation @@ -63,4 +71,7 @@ export const mainNav: { key: NavLinkKey; href: string }[] = [ ...(features.aboutUs ? [{ key: 'aboutUs' as const, href: routes.aboutUs }] : []), + ...(features.glossary + ? [{ key: 'glossary' as const, href: routes.glossary }] + : []), ]; diff --git a/jest.config.ts b/jest.config.ts index 1e783b6c..001cf774 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -23,6 +23,7 @@ const config: Config = { // Mock d3 modules '^d3-scale$': '/tests/__mocks__/d3-scale.ts', '^d3-scale-chromatic$': '/tests/__mocks__/d3-scale-chromatic.ts', + '\\.csv$': '/tests/__mocks__/csv.ts', }, collectCoverageFrom: [ '{app,components,config,hooks,i18n,lib}/**/*.{ts,tsx}', diff --git a/knip.jsonc b/knip.jsonc index b4bd694b..9b3fbc81 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -9,7 +9,7 @@ // graphql-codegen output, over which we don't have full control. "gql/generated/**", // @import'd by styles/globals.css (knip doesn't follow CSS imports). - "styles/tokens/_variables.css" + "styles/tokens/_variables.css", ], "ignoreDependencies": [ // `graphql-codegen --require dotenv/config` in npm scripts. @@ -30,6 +30,5 @@ // direct source imports knip can trace. "@jest/globals", "ts-jest", - "ts-node" - ] + ], } diff --git a/lib/glossary.ts b/lib/glossary.ts new file mode 100644 index 00000000..c51f62ef --- /dev/null +++ b/lib/glossary.ts @@ -0,0 +1,126 @@ +import type { GlossaryTerm } from 'ids-drr-branding-types'; + +type CsvRow = Record; + +function parseCsv(text: string): CsvRow[] { + const rows: string[][] = []; + let currentField = ''; + let currentRow: string[] = []; + let inQuotes = false; + + const pushField = () => { + currentRow.push(currentField); + currentField = ''; + }; + + const pushRow = () => { + // skip empty trailing lines + if (currentRow.length === 1 && currentRow[0].trim() === '') { + currentRow = []; + return; + } + rows.push(currentRow); + currentRow = []; + }; + + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + + if (ch === '"') { + if (inQuotes && text[i + 1] === '"') { + currentField += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + continue; + } + + if (!inQuotes && ch === ',') { + pushField(); + continue; + } + + if (!inQuotes && (ch === '\n' || ch === '\r')) { + // handle CRLF + if (ch === '\r' && text[i + 1] === '\n') i++; + pushField(); + pushRow(); + continue; + } + + currentField += ch; + } + + // final field/row + pushField(); + if (currentRow.length > 0) pushRow(); + + if (rows.length === 0) return []; + const headers = rows[0].map((h) => h.trim()); + + return rows.slice(1).map((r) => { + const obj: CsvRow = {}; + for (let i = 0; i < headers.length; i++) { + obj[headers[i]] = (r[i] ?? '').trim(); + } + return obj; + }); +} + +function buildDisasterMethodology(row: CsvRow) { + const items: { disasterType: string; methodology: string }[] = []; + for (let i = 0; i < 3; i++) { + const t = row[`disaster_type_${i}`]; + const d = row[`difference_${i}`]; + if (t && d) items.push({ disasterType: t, methodology: d }); + } + return items; +} + +export function slugifyGlossaryTerm(term: string) { + return term + .toLowerCase() + .trim() + .replace(/['"]/g, '') + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)/g, ''); +} + +export function parseGlossary(csv: string): GlossaryTerm[] { + const rows = parseCsv(csv).filter((r) => r.term); + + return rows + .map((r) => { + const tag = r.tag?.trim(); + const policy = r.policy?.trim() ?? ''; + const model = r.model?.trim() ?? ''; + const interpretation = policy || model ? { policy, model } : undefined; + const disasterMethodology = buildDisasterMethodology(r); + const related = (r.related_terms ?? '') + .split(',') + .map((x) => x.trim()) + .filter(Boolean); + const misinterpretation = r.common_misinterpretation?.trim() ?? ''; + + return { + term: r.term, + ...(tag ? { tag } : {}), + summary: r.short ?? '', + definition: r.long ?? '', + methodology: r.ids_drr ?? '', + usage: r.where_seen ?? '', + significance: r.why_it_matters ?? '', + ...(disasterMethodology.length > 0 ? { disasterMethodology } : {}), + ...(interpretation ? { interpretation } : {}), + ...(misinterpretation ? { misinterpretation } : {}), + ...(related.length > 0 ? { related } : {}), + }; + }) + .sort((a, b) => + a.term.localeCompare(b.term, undefined, { + sensitivity: 'base', + numeric: true, + }) + ); +} diff --git a/locales/en.json b/locales/en.json index 1411aca1..2d1946c0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -10,6 +10,7 @@ "apply": "Apply", "reset": "Reset", "clearAll": "Clear All", + "all": "All", "close": "Close" }, "copy": { @@ -193,6 +194,39 @@ } } }, + "glossary": { + "heading": "Glossary", + "description": "Understand key terms used across the IDS-DRR platform", + "userManualLink": "User Manual", + "docsLink": "Full Documentation", + "search": { + "label": "Search", + "placeholder": "Search" + }, + "detail": { + "headings": { + "definition": "Definition", + "methodology": "IDS-DRR Context", + "disasterMethodology": "How this differs across disaster contexts", + "usage": "Where You See It", + "significance": "Why It Matters", + "interpretation": "Contextual interpretation", + "misinterpretation": "Common misinterpretation", + "related": "Related terms" + }, + "tabs": { + "policy": { + "title": "In Policy", + "heading": "Policy" + }, + "model": { + "title": "In Model", + "heading": "Model" + } + } + }, + "empty": "No results found" + }, "datasets": { "search": { "label": "Search", diff --git a/next.config.ts b/next.config.ts index 8620e528..ab9dc648 100644 --- a/next.config.ts +++ b/next.config.ts @@ -11,6 +11,11 @@ const nextConfig: NextConfig = { serverExternalPackages: ['@prisma/instrumentation', '@fastify/otel'], // https://github.com/CivicDataLab/opub-mono/pull/403 webpack: (config) => { + config.module.rules.push({ + test: /\.csv$/, + type: 'asset/source', + include: /node_modules\/ids-drr-branding/, + }); config.module.rules.push({ test: /\.(js|mjs)$/, enforce: 'pre', diff --git a/package-lock.json b/package-lock.json index 96009290..786f584f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,6 @@ "tailwind-merge": "^1.12.0", "tailwindcss": "^3.3.2", "ts-jest": "^29.4.1", - "ts-node": "^10.9.2", "typescript": "^5.0.4" } }, @@ -1309,6 +1308,8 @@ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -1322,6 +1323,8 @@ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -10181,28 +10184,36 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", @@ -11340,6 +11351,8 @@ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "acorn": "^8.11.0" }, @@ -12990,7 +13003,9 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/cross-fetch": { "version": "3.2.0", @@ -13464,6 +13479,8 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=0.3.1" } @@ -15632,7 +15649,7 @@ }, "node_modules/ids-drr-branding-types": { "version": "0.1.0", - "resolved": "git+ssh://git@github.com/open-contracting/ids-drr-branding-types.git#8c9942e543f76fccd1bb095b197b2d73eb628bc5" + "resolved": "git+ssh://git@github.com/open-contracting/ids-drr-branding-types.git#4b7ca97cb7fdd6f1ff31936e952c3774edc1607f" }, "node_modules/ieee754": { "version": "1.2.1", @@ -22803,6 +22820,8 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -22846,7 +22865,9 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -23338,7 +23359,9 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -23915,6 +23938,8 @@ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=6" } diff --git a/package.json b/package.json index 25b4d501..36264d7a 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,6 @@ "tailwind-merge": "^1.12.0", "tailwindcss": "^3.3.2", "ts-jest": "^29.4.1", - "ts-node": "^10.9.2", "typescript": "^5.0.4" }, "overrides": { diff --git a/styles/tokens/tailwind/color.js b/styles/tokens/tailwind/color.js index ccb3f29d..1a63b21e 100644 --- a/styles/tokens/tailwind/color.js +++ b/styles/tokens/tailwind/color.js @@ -83,6 +83,10 @@ module.exports = { baseAmberSolid10: '#FFA01C', baseAmberSolid11: '#AD5700', baseAmberSolid12: '#4E2009', + baseSurfaceSubdued: '#FAFBFB', + baseWhite: '#FFFFFF', + baseAlertSubued: '#FFEBD3', + baseSurfacePressed: '#F1f2f3', basePureWhite: '#FFFFFF', basePureBlack: '#000000', textDefault: 'var(--base-gray-slate-solid-12)', diff --git a/tailwind.config.js b/tailwind.config.js index 9d236092..eb7652b7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -23,7 +23,10 @@ module.exports = { // ids-drr-branding is an npm `file:` dep symlinked to ./branding-stub/, // which in deployments is bind-mounted to the real branding package. // Scan it so branding-only Tailwind classes make it into the compiled CSS. + // Real branding is copied into branding-stub for Docker builds; local + // installs may also place the package under node_modules/ids-drr-branding. './branding-stub/src/**/*.{js,ts,jsx,tsx}', + './node_modules/ids-drr-branding/src/**/*.{js,ts,jsx,tsx}', ], theme: { colors, diff --git a/tests/__mocks__/csv.ts b/tests/__mocks__/csv.ts new file mode 100644 index 00000000..28d6a6e6 --- /dev/null +++ b/tests/__mocks__/csv.ts @@ -0,0 +1,3 @@ +const csvMock = ''; + +export default csvMock; diff --git a/types/global.d.ts b/types/global.d.ts index a14fdefb..ebba9f6a 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -3,3 +3,8 @@ type IntlMessages = typeof import('../locales/en.json'); // Types derived from Messages are in global.d.ts for simplicity. type RiskLevel = keyof IntlMessages['analytics']['risk']; type NavLinkKey = keyof IntlMessages['nav']['links']; + +declare module '*.csv' { + const content: string; + export default content; +}