Skip to content

Commit 7c18235

Browse files
jamesmblairJames Blairadbergen
authored
DC-172/DC-225: Extract shared design system into @sebt/design-system (2/4) (#98)
* DC-172: Add documentation, ADRs, skills, and repo cleanup - Add ADRs for locale section filtering and rich-text rendering - Add TDD design docs, implementation plans, and Figma comparison docs - Add Claude Code skills (figma-verify, multi-repo-git, review-prep, test) - Remove tracked .idea/ files and update .gitignore with blanket .idea/ rule - Update CLAUDE.md with locale generation and design system guidance * DC-172: Extract shared design system into @sebt/design-system workspace package - Create packages/design-system with shared UI components, layout, providers, i18n, design tokens, SASS theme, and content generation scripts - Move components (Alert, Button, InputField, TextLink, Header, Footer, HelpSection, SkipNav, LanguageSelector) from Portal Web to design-system - Move content CSVs, locale generation script, design token scripts, SASS, and state JSON configs to design-system - Add RichText component wrapping markdown-to-jsx - Refactor Portal Web imports from @/components to @sebt/design-system - Configure pnpm workspace, transpilePackages, and webpack alias for shared React - Add workspace package.json, tsconfig, vitest config, and test setup * fix: broken import, next version mismatch, RichText safety comment * fix: generate fonts.ts to caller's cwd so Next.js @/ alias resolves in CI * fix: remove duplicate react/next from design-system devDeps to prevent dual-instance hook errors * fix: restore design-system devDeps and add Turbopack resolveAlias for React deduplication * fix: use production build for E2E tests to avoid Turbopack dev React duplication * fix: split design-system barrel to resolve dual React instance in E2E builds * fix: initialize i18next before I18nProvider renders to prevent SSR hang * fix: run next start in subshell so pnpm commands stay in repo root --------- Co-authored-by: James Blair <jblair@codeforamerica.org> Co-authored-by: Anthony Bergen <anthonydbergen@gmail.com>
1 parent 67b7174 commit 7c18235

137 files changed

Lines changed: 801 additions & 2679 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/playwright-e2e.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ jobs:
8181
- name: Install Chrome for Pa11y
8282
run: cd src/SEBT.Portal.Web && pnpm exec puppeteer browsers install chrome
8383

84+
- name: Build frontend for E2E
85+
env:
86+
STATE: dc
87+
NEXT_PUBLIC_STATE: dc
88+
run: ./.github/workflows/scripts/build-frontend.sh --production
89+
8490
- name: Run Pa11y and Playwright E2E tests
8591
env:
8692
CI: true
@@ -92,9 +98,12 @@ jobs:
9298
JwtSettings__SecretKey: "ci-e2e-jwt-secret-at-least-32-characters-long"
9399
IdentifierHasher__SecretKey: "ci-e2e-identifier-hasher-key-32chars"
94100
Oidc__CompleteLoginSigningKey: "ci-e2e-oidc-signing-key-at-least-32-chars"
101+
PluginAssemblyPaths__0: "plugins-dc"
95102
UseMockHouseholdData: "true"
96103
run: |
97-
pnpm dev &
104+
# Start API in dev mode + frontend from production build
105+
pnpm api:dev &
106+
(cd src/SEBT.Portal.Web && pnpm start) &
98107
echo "Waiting for server at http://localhost:3000..."
99108
for i in $(seq 1 90); do
100109
curl -sf --max-time 15 http://localhost:3000 > /dev/null && echo "Server ready" && break

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ packages/design-system/content/.copy-hash
482482
packages/design-system/design/tokens.css
483483
packages/design-system/design/fonts.ts
484484
packages/design-system/design/sass/_uswds-theme-*.scss
485+
!packages/design-system/design/sass/_uswds-theme-custom-styles.scss
485486
packages/design-system/design/sass/_uswds-settings.scss
486487

487488
# JetBrains IDE configuration

packages/design-system/content/scripts/.gitkeep

Whitespace-only changes.

src/SEBT.Portal.Web/content/scripts/generate-locales.js renamed to packages/design-system/content/scripts/generate-locales.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
* - Variable interpolation: Preserves {state}, {year} placeholders for runtime
4242
*/
4343

44-
import '../../design/scripts/load-env.js';
44+
// load-env.js is portal-specific and not needed in the shared package.
45+
// The script does not use any process.env variables.
4546
import { createHash } from 'crypto';
4647
import {
4748
existsSync,
@@ -51,9 +52,23 @@ import {
5152
rmSync,
5253
writeFileSync,
5354
} from 'fs';
55+
import * as path from 'path';
5456
import { dirname, join, relative } from 'path';
5557
import { fileURLToPath } from 'url';
5658

59+
// Parse CLI arguments
60+
const cliArgs = process.argv.slice(2)
61+
function getCliArg(name) {
62+
const idx = cliArgs.indexOf(name)
63+
return idx !== -1 ? cliArgs[idx + 1] : null
64+
}
65+
const csvDirOverride = getCliArg('--csv-dir')
66+
const outDirOverride = getCliArg('--out-dir')
67+
const tsOutOverride = getCliArg('--ts-out')
68+
const appFilter = getCliArg('--app') // 'portal' | 'enrollment' | null (all)
69+
const sectionsFilter = getCliArg('--sections') // comma-separated, e.g., 'S1,GLOBAL'
70+
const allowedSections = sectionsFilter ? sectionsFilter.split(',').map(s => s.trim()) : null
71+
5772
const __filename = fileURLToPath(import.meta.url);
5873
const __dirname = dirname(__filename);
5974
const contentDir = join(__dirname, '..');
@@ -63,8 +78,12 @@ const rel = p => relative(rootDir, p);
6378
// Configuration
6479
const CONFIG = {
6580
// State CSV files directory (content/states/{state}.csv)
66-
statesDir: join(contentDir, 'states'),
67-
outputDir: join(contentDir, 'locales'),
81+
statesDir: csvDirOverride
82+
? path.resolve(process.cwd(), csvDirOverride)
83+
: join(contentDir, 'states'),
84+
outputDir: outDirOverride
85+
? path.resolve(process.cwd(), outDirOverride)
86+
: join(contentDir, 'locales'),
6887
hashFile: join(contentDir, '.copy-hash'),
6988
locales: {
7089
en: 'English',
@@ -312,6 +331,9 @@ function buildStateLocaleData(rows, state) {
312331
const parsed = parseContentKey(contentKey);
313332
if (!parsed) continue;
314333

334+
// Section-level filter: skip rows from sections not in the allowed list
335+
if (allowedSections && !allowedSections.includes(parsed.section)) continue;
336+
315337
const { namespace, key } = parsed;
316338

317339
// English — don't overwrite a non-empty value with an empty one
@@ -474,6 +496,36 @@ function validateStateCompleteness(stateData, state) {
474496
return warnings;
475497
}
476498

499+
// Which namespaces belong to which app.
500+
// 'all' = shared, included in every app's barrel file.
501+
const NAMESPACE_APP = {
502+
common: 'all',
503+
landing: 'all',
504+
disclaimer: 'all',
505+
personalInfo: 'all',
506+
confirmInfo: 'all',
507+
result: 'all',
508+
// Portal-specific
509+
login: 'portal',
510+
idProofing: 'portal',
511+
optIn: 'portal',
512+
offBoarding: 'portal',
513+
dashboard: 'portal',
514+
edit: 'portal',
515+
editContactPreferences: 'portal',
516+
editMailingAddress: 'portal',
517+
stepUpDisclaimer: 'portal',
518+
stepUpFailure: 'portal',
519+
proto: 'portal',
520+
}
521+
522+
function isNamespaceForApp(namespace, app) {
523+
if (!app) return true
524+
// eslint-disable-next-line security/detect-object-injection -- namespace comes from JSON filenames, not user input
525+
const mapped = NAMESPACE_APP[namespace] ?? 'all'
526+
return mapped === 'all' || mapped === app
527+
}
528+
477529
/**
478530
* Generate TypeScript resource file from locale directory
479531
*
@@ -484,7 +536,9 @@ function validateStateCompleteness(stateData, state) {
484536
* Output: src/lib/generated-locale-resources.ts
485537
*/
486538
function generateResourceFile() {
487-
const outputPath = join(rootDir, 'src', 'lib', 'generated-locale-resources.ts');
539+
const outputPath = tsOutOverride
540+
? path.resolve(process.cwd(), tsOutOverride)
541+
: join(rootDir, 'src', 'lib', 'generated-locale-resources.ts');
488542

489543
if (!existsSync(CONFIG.outputDir)) {
490544
console.log(
@@ -525,7 +579,8 @@ function generateResourceFile() {
525579
.sort();
526580

527581
for (const file of files) {
528-
const namespace = file.replace('.json', '');
582+
const namespace = file.replace('.json', '')
583+
if (!isNamespaceForApp(namespace, appFilter)) continue
529584
// Sanitize namespace for use as a JS identifier (handle hyphens → camelCase)
530585
const safeNs = namespace.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
531586
const varName = `${lang}${state.toUpperCase()}${safeNs.charAt(0).toUpperCase()}${safeNs.slice(1)}`;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Tests for generate-locales.js --app, --out-dir, --ts-out CLI args.
4+
* Run: node packages/design-system/content/scripts/generate-locales.test.js
5+
*/
6+
import { strict as assert } from 'assert'
7+
import { execFileSync } from 'child_process'
8+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
9+
import { join } from 'path'
10+
import { fileURLToPath } from 'url'
11+
12+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
13+
const script = join(__dirname, 'generate-locales.js')
14+
const tmpDir = join(__dirname, '__test_tmp__')
15+
16+
function setup() {
17+
rmSync(tmpDir, { recursive: true, force: true })
18+
mkdirSync(join(tmpDir, 'states'), { recursive: true })
19+
mkdirSync(join(tmpDir, 'locales'), { recursive: true })
20+
mkdirSync(join(tmpDir, 'ts-out'), { recursive: true })
21+
22+
// Minimal fixture CSV with portal-only and shared content
23+
const csv = [
24+
'Content,English,Español',
25+
'GLOBAL - Button Continue,Continue,Continuar',
26+
'S1 - Landing Page - Title,Portal Landing,Portal Landing ES',
27+
'S7 - Portal Dashboard - Heading,Dashboard,Panel',
28+
].join('\n')
29+
writeFileSync(join(tmpDir, 'states', 'co.csv'), csv)
30+
}
31+
32+
function teardown() {
33+
rmSync(tmpDir, { recursive: true, force: true })
34+
}
35+
36+
setup()
37+
38+
// Test: --app portal includes landing and dashboard, generates to --out-dir and --ts-out
39+
execFileSync('node', [
40+
script,
41+
'--csv-dir', join(tmpDir, 'states'),
42+
'--out-dir', join(tmpDir, 'locales'),
43+
'--ts-out', join(tmpDir, 'ts-out', 'portal-resources.ts'),
44+
'--app', 'portal',
45+
], { stdio: 'inherit' })
46+
47+
const portalContent = readFileSync(join(tmpDir, 'ts-out', 'portal-resources.ts'), 'utf8')
48+
assert.ok(portalContent.includes('landing'), 'portal barrel must include landing namespace')
49+
assert.ok(portalContent.includes('dashboard'), 'portal barrel must include dashboard namespace')
50+
assert.ok(portalContent.includes('common'), 'portal barrel must include common namespace')
51+
52+
// Test: --app enrollment includes common but excludes dashboard
53+
execFileSync('node', [
54+
script,
55+
'--csv-dir', join(tmpDir, 'states'),
56+
'--out-dir', join(tmpDir, 'locales'),
57+
'--ts-out', join(tmpDir, 'ts-out', 'enrollment-resources.ts'),
58+
'--app', 'enrollment',
59+
], { stdio: 'inherit' })
60+
61+
const enrollmentContent = readFileSync(join(tmpDir, 'ts-out', 'enrollment-resources.ts'), 'utf8')
62+
assert.ok(enrollmentContent.includes('common'), 'enrollment barrel must include common namespace')
63+
assert.ok(!enrollmentContent.includes('dashboard'), 'enrollment barrel must NOT include dashboard namespace')
64+
65+
// Test: locale JSON was written to --out-dir (not to script's own directory)
66+
assert.ok(existsSync(join(tmpDir, 'locales', 'en', 'co', 'landing.json')), 'locale JSON must be written to --out-dir')
67+
68+
console.log('✅ All generate-locales CLI arg tests passed')
69+
teardown()

packages/design-system/content/states/.gitkeep

Whitespace-only changes.
File renamed without changes.
File renamed without changes.

packages/design-system/design/sass/.gitkeep

Whitespace-only changes.

src/SEBT.Portal.Web/design/sass/_uswds-theme-custom-styles.scss renamed to packages/design-system/design/sass/_uswds-theme-custom-styles.scss

File renamed without changes.

0 commit comments

Comments
 (0)