From 137086109c5b1fcffeb8a359ecca0d5ddfc5e9c0 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Tue, 9 Jun 2026 09:42:38 -0400 Subject: [PATCH] =?UTF-8?q?SEV-329:=20@switchev/web-shared=20package=20?= =?UTF-8?q?=E2=80=94=20formatters,=20markdown,=20waitForImage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stands up the shared browser-helpers package (consumed by public/vendor/admin via the Gitea npm registry) to de-duplicate copy-pasted frontend code: - format.js: formatLabel/formatStatus/formatTimeline/formatBudget (single source of truth — these had drifted, see SEV-310). - markdown.js: renderMarkdown (marked + strict DOMPurify; both peer deps). - wait-image.js: waitForImage (was byte-identical in public + vendor). ESM source package (no build step — Vite consumers import directly). publishConfig points at the Gitea npm registry. node --test green (formatters). Next: publish v0.1.0, then migrate each site to consume it and delete the copies. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 32 ++++++++++++++++++++++++++++---- src/format.js | 39 +++++++++++++++++++++++++++++++++++++++ src/format.test.js | 26 ++++++++++++++++++++++++++ src/index.js | 5 +++++ src/markdown.js | 33 +++++++++++++++++++++++++++++++++ src/wait-image.js | 23 +++++++++++++++++++++++ 6 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/format.js create mode 100644 src/format.test.js create mode 100644 src/index.js create mode 100644 src/markdown.js create mode 100644 src/wait-image.js diff --git a/package.json b/package.json index f834ce9..f8e114b 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,32 @@ { - "name": "switchev-shared", + "name": "@switchev/web-shared", "version": "0.1.0", - "private": true, - "description": "Shared constants, validation schemas, and type definitions for SwitchEV", + "description": "Shared browser helpers (formatters, markdown, image polling) for the SwitchEV frontends", "type": "module", - "main": "index.js" + "exports": { + ".": "./src/index.js", + "./format": "./src/format.js", + "./markdown": "./src/markdown.js", + "./wait-image": "./src/wait-image.js" + }, + "files": [ + "src" + ], + "sideEffects": [ + "./src/markdown.js" + ], + "peerDependencies": { + "dompurify": ">=3", + "marked": ">=12" + }, + "scripts": { + "test": "node --test" + }, + "publishConfig": { + "registry": "https://git.debyl.io/api/packages/SwitchEV/npm/" + }, + "repository": { + "type": "git", + "url": "git+https://git.debyl.io/SwitchEV/shared.git" + } } diff --git a/src/format.js b/src/format.js new file mode 100644 index 0000000..002982c --- /dev/null +++ b/src/format.js @@ -0,0 +1,39 @@ +// SEV-329: shared display formatters. These were copy-pasted (and subtly +// divergent — SEV-310) across public/vendor/admin. Single source of truth here. + +/** + * Friendly label for an internal slug: replaces '-' and '_' with spaces and + * Title-Cases each word. e.g. "contract_pending" → "Contract Pending". + */ +export function formatLabel(val) { + if (!val) return ''; + return String(val).replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); +} + +/** Friendly status label (alias of formatLabel, named for intent at call sites). */ +export const formatStatus = formatLabel; + +const TIMELINE_LABELS = { + '4_to_6_months': '4–6 months', + '6_to_12_months': '6–12 months', + '12_to_24_months': '12–24 months', + flexible: 'Flexible', +}; + +/** Friendly conversion timeline. Falls back to formatLabel for unknown slugs. */ +export function formatTimeline(val) { + return TIMELINE_LABELS[val] || formatLabel(val); +} + +const BUDGET_LABELS = { + 'under-10k': 'Under $10,000', + '10k-25k': '$10,000 – $25,000', + '25k-50k': '$25,000 – $50,000', + '50k-100k': '$50,000 – $100,000', + '100k-plus': '$100,000+', +}; + +/** Friendly budget-range label for the wizard's slug values. */ +export function formatBudget(val) { + return BUDGET_LABELS[val] || val || ''; +} diff --git a/src/format.test.js b/src/format.test.js new file mode 100644 index 0000000..080b396 --- /dev/null +++ b/src/format.test.js @@ -0,0 +1,26 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { formatLabel, formatStatus, formatTimeline, formatBudget } from './format.js'; + +test('formatLabel replaces _ and - and title-cases', () => { + assert.equal(formatLabel('contract_pending'), 'Contract Pending'); + assert.equal(formatLabel('in-progress'), 'In Progress'); + assert.equal(formatLabel(''), ''); + assert.equal(formatLabel(null), ''); +}); + +test('formatStatus is formatLabel', () => { + assert.equal(formatStatus('matched'), 'Matched'); +}); + +test('formatTimeline maps known slugs, falls back otherwise', () => { + assert.equal(formatTimeline('6_to_12_months'), '6–12 months'); + assert.equal(formatTimeline('flexible'), 'Flexible'); + assert.equal(formatTimeline('something_else'), 'Something Else'); +}); + +test('formatBudget maps known ranges', () => { + assert.equal(formatBudget('25k-50k'), '$25,000 – $50,000'); + assert.equal(formatBudget('100k-plus'), '$100,000+'); + assert.equal(formatBudget(''), ''); +}); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a2067d9 --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +// @switchev/web-shared — shared browser helpers for the public/vendor/admin +// SwitchEV frontends. SEV-329. +export { formatLabel, formatStatus, formatTimeline, formatBudget } from './format.js'; +export { renderMarkdown } from './markdown.js'; +export { waitForImage } from './wait-image.js'; diff --git a/src/markdown.js b/src/markdown.js new file mode 100644 index 0000000..8ec4191 --- /dev/null +++ b/src/markdown.js @@ -0,0 +1,33 @@ +// SEV-329: shared safe-markdown renderer. marked parses the source; DOMPurify +// enforces a strict allowlist (no scripts, inline handlers,