Redesign the dashboard and sign-in page with the Lightwell design language
CI / test (push) Successful in 2m27s
CI / test (push) Successful in 2m27s
Warm neutrals, teal accent, 15px type, and Lightwell's component shapes. Status is a badge with text instead of a colored dot. Every destructive action asks first, naming the object and what is lost. Dialogs trap focus, close on Escape and return focus. The sidebar is a drawer on phones, there is a theme switch, loading shows skeleton rows, and empty states explain what belongs there. Emoji and dashes are gone, and server errors are capitalized and tell visitors what to do next. Adds scripts/lint-design.mjs and docs/design.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b7cb5dd35
commit
d74e6c1e72
7 files changed
+536
-253
No files matched your search
@@ -0,0 +1,57 @@
|
||||
// Style gate for the dashboard, following the Lightwell design language (see docs/design.md).
|
||||
// Fails on em or en dashes, emoji, exclamation marks in interface copy, and raw colors outside the
|
||||
// token block of web/index.html. Run with: node scripts/lint-design.mjs
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { extname, join, relative, sep } from "node:path";
|
||||
|
||||
const root = new URL("..", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
|
||||
const targets = ["web", "docs", "README.md", "deploy", "scripts", "cmd", "."];
|
||||
const skipDirs = new Set(["node_modules", ".git", "e2e", "internal"]);
|
||||
const exts = new Set([".html", ".md", ".go", ".sh", ".mjs", ".example"]);
|
||||
|
||||
const dash = /[–—−]/;
|
||||
const emoji = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{1F900}-\u{1F9FF}\u{FE0F}\u{2B50}\u{2705}\u{274C}]/u;
|
||||
const rawColor = /#[0-9a-fA-F]{3,8}\b|\brgba?\(|\bhsla?\(/;
|
||||
|
||||
const seen = new Set();
|
||||
function* walk(p, top) {
|
||||
const st = statSync(p, { throwIfNoEntry: false });
|
||||
if (!st || seen.has(p)) return;
|
||||
if (st.isFile()) { seen.add(p); return yield p; }
|
||||
for (const name of readdirSync(p)) {
|
||||
if (skipDirs.has(name) || (top && name.startsWith("."))) continue;
|
||||
const full = join(p, name);
|
||||
// the "." target only contributes the files at the top level
|
||||
if (p === root && statSync(full).isDirectory()) continue;
|
||||
yield* walk(full, false);
|
||||
}
|
||||
}
|
||||
|
||||
const problems = [];
|
||||
for (const t of targets) {
|
||||
for (const file of walk(join(root, t), true)) {
|
||||
if (!exts.has(extname(file)) && !file.endsWith(".env.example")) continue;
|
||||
const rel = relative(root, file).split(sep).join("/");
|
||||
if (rel.endsWith("go.sum") || rel === "scripts/lint-design.mjs") continue;
|
||||
const isPage = rel.startsWith("web/") && rel.endsWith(".html");
|
||||
let inTokens = false;
|
||||
readFileSync(file, "utf8").split("\n").forEach((line, i) => {
|
||||
const at = (msg) => problems.push(`${rel}:${i + 1}: ${msg}`);
|
||||
if (line.includes("tokens:begin")) inTokens = true;
|
||||
if (dash.test(line)) at("em dash, en dash or minus sign. Use a comma, colon, period or the word to.");
|
||||
if (emoji.test(line)) at("emoji or pictographic symbol. Use an approved outline icon.");
|
||||
if (isPage && !inTokens && rawColor.test(line.replace(/\/\*.*?\*\//g, "")) && !/placeholder|href="#|&#/.test(line)) {
|
||||
at("raw color outside the token block. Use a token.");
|
||||
}
|
||||
if (isPage && /(?:toast|textContent\s*=|class:'(?:empty|notice)')[^;\n]*'[^']*[A-Za-z]![^']*'/.test(line)) at("exclamation mark in interface copy.");
|
||||
if (line.includes("tokens:end")) inTokens = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length) {
|
||||
console.error(problems.join("\n"));
|
||||
console.error(`\n${problems.length} style problem(s).`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Style checks passed.");
|
||||
Reference in new issue
Block a user