Move the console onto ReUI PRO

Install ReUI (MCP, skill, @reui registry, Tailwind v4, shadcn base-nova) and move
the shared UI onto it: Button, Tag, Check, Input/Textarea, Segmented, Alert, Panel,
Dialog, Tooltip, Menu and Select (Popover), Toast (sonner), the Table frame
(DataGrid), the Dashboard KPI strip, the shnt Ops palette, Geist and the type scale.

Design only: behaviour, texts and roles are unchanged. Tests that asserted the old
DOM were updated; the tests now format numbers in English wherever they run.
docs/reui-migration-plan.md records the plan and its decisions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-27 00:06:06 +07:00
commit f3531c64f9
680 files changed
+139836

No files matched your search

+123
View File
@@ -0,0 +1,123 @@
/**
* The guard against names cut letter by letter.
*
* Three declarations let the browser end a line inside a word wherever the edge
* falls: `word-break: break-all`, `word-break: break-word` and `overflow-wrap:
* anywhere`. The last two also tell a table, a flex row or a grid track sized by
* its content that the word could break after ANY letter, so the column shrinks to
* a few characters and every name in it comes out cut: `0.30.172.in-addr.arpa` in
* four lines in Zones, `raw.githubuser` / `content.com` in Blocking › Lists. On
* 2026-10-15 there were 23 of them in 17 stylesheets, found two or three at a time
* by looking at screenshots. This is what finds the rest.
*
* What to write instead:
* - a DNS name goes through `ui/DomainName`, which breaks only after its dots;
* - a URL through `ui/UrlText`, which breaks after its separators;
* - other text through `overflow-wrap: break-word`, which ends lines between words
* and breaks a word only when it alone does not fit, without shrinking the column;
* - or one line cut with an ellipsis and the whole value in the text and `title`
* (`Clip` in Query Logs).
*
* What genuinely has no words (a public key, a signature, a hex string, raw output)
* goes in `ALLOWED`, by file AND selector, with its reason. An entry that no longer
* matches anything is a finding too, so the list cannot only grow.
*
* It reads every `.css` file under `src/`, with comments stripped. Exit code is the
* number of findings, so it can sit in the gate.
*/
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
export const ALLOWED = [
{
file: 'src/ui/record.module.css',
selector: '.key',
reason: 'A public key or a hex string is one token with no words; it breaks by characters by design.',
},
{
file: 'src/screens/lists/Lists.module.css',
selector: '.key',
reason: 'The same key as ui/record, composed: a DNSKEY or an RRSIG would otherwise walk out of its cell.',
},
{
file: 'src/screens/lists/Lists.module.css',
selector: '.signatures',
reason: 'Preformatted DNSSEC and glue records: base64 signatures with no words, inside a box of fixed width.',
},
{
file: 'src/screens/zones/Zones.module.css',
selector: '.output',
reason: 'Preformatted text the server returns (a record comment), in a scrolling box of fixed width.',
},
{
file: 'src/screens/zones/Zones.module.css',
selector: '.val',
reason: 'A record value can be one token of hundreds of characters (a DKIM TXT, a hex SSHFP) '
+ 'and has to fit its table cell. Domain-valued records would read better through DomainName; not done yet.',
},
{
file: 'src/ui/kv.module.css',
selector: '.kv dd',
reason: 'Key/value lists hold DNSSEC keys and hex digests inside table cells; without it they push the table out.',
},
{
file: 'src/ui/SectionHeader.module.css',
selector: '.title',
reason: 'The h1 sits in a column of definite width (`min-width: 0`), so it only breaks a word that '
+ 'overflows it and never sizes anything; a zone name longer than the screen is the one case.',
},
]
const RULE = /(?:word-break\s*:\s*(break-all|break-word)|overflow-wrap\s*:\s*(anywhere))/g
export function stripComments(css) {
return css.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
}
/** Each breaking declaration with the selector of the rule that holds it. */
export function findBreaks(css) {
const text = stripComments(css)
const out = []
for (const m of text.matchAll(RULE)) {
const open = text.lastIndexOf('{', m.index)
const close = text.lastIndexOf('}', open)
const selector = text.slice(close + 1, open).trim().replace(/\s+/g, ' ')
out.push({ selector, value: m[1] ?? m[2], line: text.slice(0, m.index).split('\n').length })
}
return out
}
function cssFiles(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const p = path.join(dir, e.name)
if (e.isDirectory()) return cssFiles(p)
return e.name.endsWith('.css') ? [p] : []
})
}
export function check() {
const findings = []
const used = new Set()
for (const abs of cssFiles(path.join(ROOT, 'src'))) {
const file = path.relative(ROOT, abs).split(path.sep).join('/')
for (const b of findBreaks(fs.readFileSync(abs, 'utf8'))) {
const allowed = ALLOWED.findIndex((a) => a.file === file && a.selector === b.selector)
if (allowed >= 0) used.add(allowed)
else findings.push(`${file}:${b.line} ${b.selector} { ${b.value} }`)
}
}
ALLOWED.forEach((a, i) => {
if (!used.has(i)) findings.push(`ALLOWED entry matches nothing: ${a.file} ${a.selector}`)
})
return findings
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const findings = check()
for (const f of findings) console.log(f)
console.log(findings.length === 0 ? 'Word breaks: none outside the allowed list.' : `${findings.length} finding(s).`)
process.exit(findings.length)
}