Files
zeekayandHanzo Dev 51bcb189d8 the design-system layer, and the pipeline that gets the site onto it
src/styles/index.css now imports exactly three things: @hanzo/design's tokens,
system.css (this site's role vocabulary written entirely in those tokens), and
site.css (the few treatments this surface owns). No utility framework, no
per-page stylesheet, no fourth layer.

Deleted here because this layer is what replaces them: base.css, typography.css,
themes.css and utilities.css were a hand-rolled Tailwind substitute — a second
colour scale, a second type ramp, a second set of utilities — and App.css was
imported by nothing.

scripts/migrate.sh runs the translation end to end against the ORIGINAL sources
in git, so the codemod can be corrected and re-run without compounding earlier
passes, which it was, repeatedly. detailwind.mjs translates whole class SETS
rather than token by token, because the responsive behaviour belongs in the
class and not copy-pasted at 48,000 call sites. sweep-residual.mjs reuses its
translate() for the class names that live in data files the className walk
cannot see. css-check.mjs and shots.mjs are the checks: used-vs-defined class
coverage, and Playwright over all 93 routes at 390px and 1280px.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 14:22:30 -07:00

63 lines
3.3 KiB
JavaScript

/* eslint-disable */
/**
* The tail of the migration: class names that sit in a string the codemod's
* `className=` walk cannot see — a status→colour map in a data file, an object
* field named `iconTextClass`, a `badge:` property whose value is a class list.
*
* It reuses detailwind's `translate()` rather than carrying a lookup table of
* its own: there is one Tailwind→role translation in this repo, and a second
* copy of it would drift from the first the moment either is corrected.
*
* The whole risk here is false positives — a string that LOOKS like a class
* list but is a value being compared (`view === 'grid'`, `radius: 'rounded'`,
* `position: 'top-left'`). So a literal is only rewritten when it is
* unambiguous: either several tokens that are nearly all Tailwind, or a single
* token that could not be anything but a colour utility.
*/
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'
import { translate, IGNORE } from './detailwind.mjs'
const TW = /^-?(?:(?:[a-z][a-z0-9]*|\[[^\]]*\]):)*(?:flex|grid|hidden|inline-flex|container|truncate|uppercase|italic|underline|border|rounded|shadow|ring|(?:text|bg|from|via|to|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|w|h|min-w|max-w|min-h|max-h|gap|space-[xy]|items|justify|self|order|col|row|z|opacity|leading|tracking|font|whitespace|overflow|cursor|transition|duration|ease|delay|animate|backdrop|blur|scale|rotate|translate|inset|top|bottom|left|right|size|aspect|line-clamp|divide|outline|grow|shrink|basis|object|fill|stroke|snap|touch|select|resize|appearance|list|isolate|prose)-)/
// A lone token only counts when it is a colour utility: `bg-blue-900/30`,
// `text-[#fd4444]`, `border-white`. `rounded`, `grid` and `hidden` are words
// this codebase also uses as plain values, and translating those breaks logic.
const LONE = /^(?:hover:|focus:|group-hover:)?(?:(?:text|bg|border|from|via|to|ring|divide)-(?:[a-z]+-\d{2,3}(?:\/\d{1,3})?|black|white|transparent|\[[^\]]+\])|pointer-events-none|[mp][btlrxy]?-\d+(?:\.\d+)?)$/
const classy = (s) => {
// Parentheses are legal inside an arbitrary value (`bg-[var(--black)]`), so
// rejecting them skips every class list that reached for a CSS variable —
// which on this site is most of the colours. Only real code punctuation
// disqualifies a string.
if (!s || s.length > 300 || s.includes('${') || /[<>{};=]/.test(s)) return false
const parts = s.split(/\s+/).filter(Boolean)
if (!parts.length) return false
if (parts.length === 1) return LONE.test(parts[0])
return parts.filter((p) => TW.test(p)).length >= parts.length * 0.6
}
const walk = (dir, acc = []) => {
for (const f of readdirSync(dir)) {
const p = dir + '/' + f
statSync(p).isDirectory() ? walk(p, acc) : /\.(tsx|ts)$/.test(f) && !IGNORE.has(p) && acc.push(p)
}
return acc
}
let files = 0
let strings = 0
for (const p of walk('src')) {
const src = readFileSync(p, 'utf8')
const next = src.replace(/"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'/g, (all, d, s) => {
const q = all[0]
const body = d ?? s ?? ''
if (!classy(body)) return all
const t = translate(body)
if (t === body) return all
strings++
return q + t + q
})
if (next !== src) { writeFileSync(p, next); files++ }
}
console.log(`residual sweep: ${strings} strings in ${files} files`)