@amityco/social-plus-vise 0.12.2 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/server.js +34 -19
- package/dist/tools/design.js +586 -5
- package/dist/tools/project.js +10 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -250,6 +250,7 @@ The flow above is what the skill teaches your AI agent. You — the human — dr
|
|
|
250
250
|
| `vise design extract --from-project [path] [--no-write]` | No external prototype? Derive the contract from the host project's **own** design system — CSS custom properties (incl. shadcn `:root` and Tailwind v4 `@theme`), TS/JS token modules, inline tailwind configs, **Android** `colors.xml`/`dimens.xml`, **Flutter** `Color(0x…)`, and **iOS** `.xcassets/*.colorset` + Swift `Color(hex:)`/`Color(red:g:b:)`. Reference values (`var()`/`theme()`/`calc()`) are skipped, so a var-mapped config contributes nothing rather than wrong tokens |
|
|
251
251
|
| `vise design check [path]` | Advisory, **non-blocking** report on how closely the UI code matches the contract (token coverage + on/off-contract color literals). Never fails a build and is **not** a `vise check` gate |
|
|
252
252
|
| `vise design preview [path] [--reference <prototype>]` | Write a self-contained `sp-vise/design-preview.html`: the contract's tokens as visual swatches + the conformance report + the HTML reference embedded for side-by-side review. Vise renders the artifact; a human/VLM judges the visual match. Dependency-free — **not** an automated pixel diff |
|
|
253
|
+
| `vise design reference [path] [--title <name>]` | Write a self-contained `sp-vise/design-reference.html`: human/VLM-readable design-system spec — token swatches, type samples, component demos, and a growth-layer summary. Pairs with `design-contract.json` (machine-readable). Use `--title` to name the design system (e.g. `--title Streamly`). Advisory — **not** an enforcement gate |
|
|
253
254
|
|
|
254
255
|
The extracted contract is **advisory input for generation**, not an enforcement gate: a token-poor prototype yields a weaker — never wrong — contract, and absence of a prototype simply means no contract (the existing `*.design.reuse-detected-tokens` rules still cover reuse of a host project's own design system).
|
|
255
256
|
|
package/dist/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { attestRule, attestRuleTool, checkCompliance, checkComplianceTool, explainRule, explainRuleTool, initCompliance, initComplianceTool, initEngagement, initEngagementTool, showEngagement, showEngagementTool, statusCompliance, syncCompliance, syncComplianceTool, } from "./tools/compliance.js";
|
|
10
|
-
import { designCheckTool, designExtractTool, designPreviewTool } from "./tools/design.js";
|
|
10
|
+
import { designCheckTool, designExtractTool, designPreviewTool, designReferenceTool } from "./tools/design.js";
|
|
11
11
|
import { getDocPageTool, searchDocsTool } from "./tools/docs.js";
|
|
12
12
|
import { planHarnessTool } from "./tools/harness.js";
|
|
13
13
|
import { planIntegrationTool } from "./tools/integration.js";
|
|
@@ -37,6 +37,7 @@ const tools = new Map([
|
|
|
37
37
|
designExtractTool,
|
|
38
38
|
designCheckTool,
|
|
39
39
|
designPreviewTool,
|
|
40
|
+
designReferenceTool,
|
|
40
41
|
].map((tool) => [tool.name, tool]));
|
|
41
42
|
const bundledSkillName = "social-plus-vise";
|
|
42
43
|
const cliResult = await handleCli(process.argv.slice(2));
|
|
@@ -295,7 +296,16 @@ async function handleCli(args) {
|
|
|
295
296
|
});
|
|
296
297
|
return "exit";
|
|
297
298
|
}
|
|
298
|
-
|
|
299
|
+
if (sub === "reference") {
|
|
300
|
+
assertOnlyKnownFlags(subArgs, ["title", "no-write"], "design reference");
|
|
301
|
+
await printToolResult(designReferenceTool, {
|
|
302
|
+
repoPath: positionalRepoPath(subArgs),
|
|
303
|
+
title: flagValue(subArgs, "title"),
|
|
304
|
+
write: !hasFlag(subArgs, "no-write"),
|
|
305
|
+
});
|
|
306
|
+
return "exit";
|
|
307
|
+
}
|
|
308
|
+
console.error(`Unknown design subcommand: ${sub ?? "(none)"}. Expected "extract", "check", "preview", or "reference".`);
|
|
299
309
|
process.exitCode = 1;
|
|
300
310
|
return "exit";
|
|
301
311
|
}
|
|
@@ -504,23 +514,28 @@ Usage:
|
|
|
504
514
|
vise design extract --from-project [repoPath] [--no-write]
|
|
505
515
|
vise design check [repoPath]
|
|
506
516
|
vise design preview [repoPath] [--reference <prototypePath>]
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
517
|
+
vise design reference [repoPath] [--title "Streamly"] [--no-write]
|
|
518
|
+
|
|
519
|
+
extract Build a graded design contract and write it to sp-vise/design-contract.json.
|
|
520
|
+
Declared CSS custom properties become exact tokens; repeated literal values
|
|
521
|
+
become inferred (advisory) tokens; single-use literals are treated as one-offs.
|
|
522
|
+
With --from-project (no external prototype), derive the contract from the host
|
|
523
|
+
project's OWN design system: CSS custom properties (incl. shadcn :root and
|
|
524
|
+
Tailwind v4 @theme), TS/JS token modules, inline tailwind configs, Android
|
|
525
|
+
colors.xml/dimens.xml, Flutter Color(0x..), and iOS .xcassets/.colorset +
|
|
526
|
+
Swift colors. Reference values (var()/theme()/calc()) are skipped, so a
|
|
527
|
+
var-mapped config contributes nothing rather than wrong tokens.
|
|
528
|
+
check Advisory, non-blocking report on how closely the project's UI code
|
|
529
|
+
matches the contract (token coverage + on/off-contract color literals).
|
|
530
|
+
Never fails a build; it is NOT a \`vise check\` gate.
|
|
531
|
+
preview Write a self-contained sp-vise/design-preview.html: the contract's tokens
|
|
532
|
+
as visual swatches + the conformance report + the HTML reference embedded
|
|
533
|
+
(with --reference) for side-by-side review. Vise renders; a human/VLM
|
|
534
|
+
judges the visual match. Dependency-free — NOT an automated pixel diff.
|
|
535
|
+
reference Write a self-contained sp-vise/design-reference.html: human/VLM-readable
|
|
536
|
+
design-system spec (token swatches, type samples, component demos, growth-layer
|
|
537
|
+
summary). Pairs with the machine-readable design-contract.json. Use --title to
|
|
538
|
+
set the design system name. Advisory — not an enforcement gate.`;
|
|
524
539
|
}
|
|
525
540
|
return `${packageName}
|
|
526
541
|
|
package/dist/tools/design.js
CHANGED
|
@@ -297,7 +297,14 @@ export function renderDesignPreview(contract, check, referenceHtml) {
|
|
|
297
297
|
const shadows = byCat("shadow").map((t) => tokenRow(t, `<span class="box" style="box-shadow:${safeCss(t.value)}"></span>`)).join("");
|
|
298
298
|
const fonts = byCat("fontFamily").map((t) => tokenRow(t, `<span class="type" style="font-family:${safeCss(t.value)}">Ag</span>`)).join("");
|
|
299
299
|
const sizes = byCat("fontSize").map((t) => tokenRow(t, `<span class="type" style="font-size:${safeCss(t.value)}">Ag</span>`)).join("");
|
|
300
|
+
const weights = byCat("fontWeight").map((t) => tokenRow(t, `<span class="type" style="font-weight:${safeCss(t.value)};font-size:20px">Ag</span>`)).join("");
|
|
301
|
+
const lineHeights = byCat("lineHeight").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
|
|
302
|
+
const letterSpacings = byCat("letterSpacing").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
|
|
303
|
+
const borderWidths = byCat("borderWidth").map((t) => tokenRow(t, `<span class="box" style="border:${safeCss(t.value)} solid #888"></span>`)).join("");
|
|
304
|
+
const breakpoints = byCat("breakpoint").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
|
|
300
305
|
const motions = byCat("motion").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
|
|
306
|
+
const opacities = byCat("opacity").map((t) => tokenRow(t, `<span class="sw" style="background:#888;opacity:${safeCss(t.value)}"></span>`)).join("");
|
|
307
|
+
const zIndices = byCat("zIndex").map((t) => tokenRow(t, `<span class="mo">${esc(t.value)}</span>`)).join("");
|
|
301
308
|
const group = (title, body) => (body ? `<h2>${esc(title)}</h2><div class="grid">${body}</div>` : "");
|
|
302
309
|
const reference = referenceHtml
|
|
303
310
|
? `<iframe class="ref" sandbox="allow-same-origin" srcdoc="${escAttr(referenceHtml)}" title="reference prototype"></iframe>`
|
|
@@ -351,9 +358,16 @@ ${group("Colors", colors)}
|
|
|
351
358
|
${group("Spacing", spaces)}
|
|
352
359
|
${group("Radius", radii)}
|
|
353
360
|
${group("Shadow", shadows)}
|
|
361
|
+
${group("Border width", borderWidths)}
|
|
354
362
|
${group("Type — families", fonts)}
|
|
355
363
|
${group("Type — sizes", sizes)}
|
|
364
|
+
${group("Font weight", weights)}
|
|
365
|
+
${group("Line height", lineHeights)}
|
|
366
|
+
${group("Letter spacing", letterSpacings)}
|
|
367
|
+
${group("Breakpoints", breakpoints)}
|
|
356
368
|
${group("Motion", motions)}
|
|
369
|
+
${group("Opacity", opacities)}
|
|
370
|
+
${group("Z-index", zIndices)}
|
|
357
371
|
</body></html>`;
|
|
358
372
|
}
|
|
359
373
|
function esc(s) {
|
|
@@ -366,6 +380,529 @@ function escAttr(s) {
|
|
|
366
380
|
function safeCss(value) {
|
|
367
381
|
return value.replace(/[<>"]/g, "").slice(0, 200);
|
|
368
382
|
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Design-system reference document (human/VLM-readable, advisory)
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
//
|
|
387
|
+
// Pairs with design-contract.json (machine-readable): this is the "v1.0" visual
|
|
388
|
+
// spec a human or VLM can read, share, or diff. It reads the source CSS so that
|
|
389
|
+
// var(--x) resolves live in the browser; for non-CSS projects (Android, Flutter,
|
|
390
|
+
// iOS, TS module) it falls back to rendering directly from the contract tokens.
|
|
391
|
+
//
|
|
392
|
+
// NOT a gate — advisory documentation only.
|
|
393
|
+
export const designReferenceTool = {
|
|
394
|
+
name: "design_reference",
|
|
395
|
+
description: "Generate a self-contained HTML design-system reference (token swatches, type samples, component demos, growth-layer summary) from the Vise design contract. Human/VLM-readable; advisory, not an enforcement gate.",
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: "object",
|
|
398
|
+
properties: {
|
|
399
|
+
repoPath: { type: "string", description: "Project root containing sp-vise/design-contract.json. Defaults to current directory." },
|
|
400
|
+
title: { type: "string", description: "Design system name shown in the document heading (e.g. 'Streamly'). Defaults to 'Design System'." },
|
|
401
|
+
write: { type: "boolean", description: "Write sp-vise/design-reference.html (default true)." },
|
|
402
|
+
},
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
},
|
|
405
|
+
async call(input) {
|
|
406
|
+
const args = objectInput(input);
|
|
407
|
+
const repoPath = optionalStringField(args, "repoPath") ?? ".";
|
|
408
|
+
const title = optionalStringField(args, "title") ?? "Design System";
|
|
409
|
+
const write = args.write !== false;
|
|
410
|
+
const contract = await readDesignContract(repoPath);
|
|
411
|
+
if (!contract) {
|
|
412
|
+
return textResult({
|
|
413
|
+
status: "no-contract",
|
|
414
|
+
message: "No sp-vise/design-contract.json found. Run `vise design extract` first.",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const html = await generateDesignReference(repoPath, contract, title);
|
|
418
|
+
let written;
|
|
419
|
+
if (write) {
|
|
420
|
+
const target = path.join(path.resolve(repoPath), "sp-vise", "design-reference.html");
|
|
421
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
422
|
+
await writeFile(target, html, "utf8");
|
|
423
|
+
written = target;
|
|
424
|
+
}
|
|
425
|
+
return textResult({
|
|
426
|
+
status: "rendered",
|
|
427
|
+
written,
|
|
428
|
+
digest: contract.digest,
|
|
429
|
+
title,
|
|
430
|
+
note: "Advisory reference document — not an enforcement gate. Open the HTML in a browser to see the full design system.",
|
|
431
|
+
where: written ? `Open ${written} in a browser.` : "Not written (write=false).",
|
|
432
|
+
});
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
/**
|
|
436
|
+
* Generate a self-contained HTML design-system reference from the contract.
|
|
437
|
+
* Reads source CSS files for full var() resolution; falls back to contract tokens
|
|
438
|
+
* for non-CSS projects (Android XML, Flutter Dart, iOS, TS module sources).
|
|
439
|
+
*/
|
|
440
|
+
export async function generateDesignReference(repoPath, contract, title) {
|
|
441
|
+
const root = path.resolve(repoPath);
|
|
442
|
+
const cssTexts = await Promise.all((contract.source?.inputs ?? []).map(async (rel) => {
|
|
443
|
+
try {
|
|
444
|
+
const info = await stat(path.join(root, rel));
|
|
445
|
+
if (info.size > MAX_FILE_BYTES)
|
|
446
|
+
return "";
|
|
447
|
+
return readFile(path.join(root, rel), "utf8");
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return "";
|
|
451
|
+
}
|
|
452
|
+
}));
|
|
453
|
+
const tokenCss = cssTexts.join("\n");
|
|
454
|
+
// `ref` = how this token is referenced in a CSS style attribute.
|
|
455
|
+
// CSS projects: `var(--name)` so the token resolves live via the inlined :root.
|
|
456
|
+
// Non-CSS projects (Android/Flutter/iOS/TS module): use the concrete value directly
|
|
457
|
+
// since there is no :root to resolve from.
|
|
458
|
+
const hasCssInputs = (contract.source?.inputs ?? []).some((rel) => /\.(css|scss)$/i.test(rel));
|
|
459
|
+
let allTokens;
|
|
460
|
+
if (hasCssInputs && tokenCss.trim()) {
|
|
461
|
+
const contractNames = new Set(contract.tokens.map((t) => t.name).filter((n) => n !== null));
|
|
462
|
+
allTokens = [...tokenCss.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)].map((m) => ({
|
|
463
|
+
name: m[1],
|
|
464
|
+
value: m[2].trim(),
|
|
465
|
+
ref: `var(${m[1]})`,
|
|
466
|
+
inContract: contractNames.has(m[1]),
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
// Non-CSS project: render from contract tokens directly, using concrete values.
|
|
471
|
+
allTokens = contract.tokens
|
|
472
|
+
.filter((t) => t.name !== null)
|
|
473
|
+
.map((t) => ({ name: t.name, value: t.value, ref: safeCss(t.value), inContract: true, category: t.category }));
|
|
474
|
+
}
|
|
475
|
+
const GROUP_DEFS = [
|
|
476
|
+
{ id: "brand", label: "Brand", kind: "color", match: (n) => n.startsWith("--color-brand") },
|
|
477
|
+
{ id: "bg", label: "Background", kind: "color", match: (n) => /^--color-(bg|surface|overlay)/.test(n) },
|
|
478
|
+
{ id: "text", label: "Text", kind: "color", match: (n) => n.startsWith("--color-text") },
|
|
479
|
+
{ id: "line", label: "Lines & semantic", kind: "color", match: (n) => /^--color-(border|success|warning|danger|info)/.test(n) },
|
|
480
|
+
{ id: "font", label: "Font families", kind: "family", match: (n) => n.startsWith("--font-") },
|
|
481
|
+
{ id: "fs", label: "Type scale", kind: "fontsize", match: (n) => n.startsWith("--fs-") },
|
|
482
|
+
{ id: "fw", label: "Font weight", kind: "weight", match: (n) => n.startsWith("--fw-") },
|
|
483
|
+
{ id: "lh", label: "Line height / tracking", kind: "chip", match: (n) => /^--(lh|ls)-/.test(n) },
|
|
484
|
+
{ id: "space", label: "Spacing", kind: "space", match: (n) => n.startsWith("--space-") },
|
|
485
|
+
{ id: "size", label: "Sizing (controls & icons)", kind: "size", match: (n) => /^--(size|control)-/.test(n) },
|
|
486
|
+
{ id: "radius", label: "Radius", kind: "radius", match: (n) => n.startsWith("--radius-") },
|
|
487
|
+
{ id: "border", label: "Border width", kind: "border", match: (n) => n.startsWith("--border-width-") },
|
|
488
|
+
{ id: "shadow", label: "Elevation", kind: "shadow", match: (n) => n.startsWith("--shadow-") },
|
|
489
|
+
{ id: "opacity", label: "Opacity", kind: "opacity", match: (n) => n.startsWith("--opacity-") },
|
|
490
|
+
{ id: "motion", label: "Motion", kind: "chip", match: (n) => /^--(duration|ease)-/.test(n) },
|
|
491
|
+
{ id: "bp", label: "Breakpoints", kind: "chip", match: (n) => n.startsWith("--bp-") },
|
|
492
|
+
{ id: "z", label: "Z-index", kind: "chip", match: (n) => n.startsWith("--z-") },
|
|
493
|
+
];
|
|
494
|
+
// Maps contract category → rendering kind; used as fallback for non-CSS tokens
|
|
495
|
+
// whose names don't carry the --prefix conventions above.
|
|
496
|
+
const CATEGORY_TO_KIND = {
|
|
497
|
+
color: "color", space: "space", radius: "radius", shadow: "shadow",
|
|
498
|
+
fontFamily: "family", fontSize: "fontsize", motion: "chip", opacity: "opacity",
|
|
499
|
+
};
|
|
500
|
+
const CATEGORY_LABEL = {
|
|
501
|
+
color: "Colors", space: "Spacing", radius: "Radius", shadow: "Elevation",
|
|
502
|
+
fontFamily: "Font families", fontSize: "Type scale", motion: "Motion", opacity: "Opacity",
|
|
503
|
+
};
|
|
504
|
+
const used = new Set();
|
|
505
|
+
const grouped = GROUP_DEFS.map((g) => {
|
|
506
|
+
const items = allTokens.filter((t) => g.match(t.name));
|
|
507
|
+
items.forEach((t) => used.add(t.name));
|
|
508
|
+
return { id: g.id, label: g.label, kind: g.kind, items };
|
|
509
|
+
}).filter((g) => g.items.length > 0);
|
|
510
|
+
// For non-CSS tokens not matched by name-prefix above, fall back to grouping by
|
|
511
|
+
// the contract category field so native projects produce a properly-sectioned doc.
|
|
512
|
+
const unmatchedByName = allTokens.filter((t) => !used.has(t.name));
|
|
513
|
+
const catBuckets = new Map();
|
|
514
|
+
const trueUngrouped = [];
|
|
515
|
+
for (const t of unmatchedByName) {
|
|
516
|
+
const cat = t.category;
|
|
517
|
+
if (cat && CATEGORY_TO_KIND[cat]) {
|
|
518
|
+
const bucket = catBuckets.get(cat) ?? [];
|
|
519
|
+
bucket.push(t);
|
|
520
|
+
catBuckets.set(cat, bucket);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
trueUngrouped.push(t);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const [cat, items] of catBuckets) {
|
|
527
|
+
grouped.push({ id: cat, label: CATEGORY_LABEL[cat] ?? cat, kind: CATEGORY_TO_KIND[cat] ?? "chip", items });
|
|
528
|
+
}
|
|
529
|
+
if (trueUngrouped.length > 0)
|
|
530
|
+
grouped.push({ id: "other", label: "Other", kind: "chip", items: trueUngrouped });
|
|
531
|
+
const provTag = (t) => (t.inContract === false ? ` <span class="prov">not extracted</span>` : "");
|
|
532
|
+
function renderRefItem(t, kind) {
|
|
533
|
+
const n = esc(t.name);
|
|
534
|
+
const v = esc(t.value);
|
|
535
|
+
const r = t.ref;
|
|
536
|
+
const meta = `<div class="tk-name">${n}${provTag(t)}</div><div class="tk-val">${v}</div>`;
|
|
537
|
+
switch (kind) {
|
|
538
|
+
case "color": return `<div class="tk"><div class="sw" style="background:${r}"></div>${meta}</div>`;
|
|
539
|
+
case "family": return `<div class="tk wide"><div class="fam" style="font-family:${r}">${esc(title)} — The quick brown fox</div>${meta}</div>`;
|
|
540
|
+
case "fontsize": return `<div class="tk wide"><div class="samp" style="font-size:${r}">${esc(title)} ${v}</div>${meta}</div>`;
|
|
541
|
+
case "weight": return `<div class="tk wide"><div class="samp" style="font-weight:${r};font-size:20px">${esc(title)} ${v}</div>${meta}</div>`;
|
|
542
|
+
case "space": return `<div class="tk"><div class="bar" style="width:${r}"></div>${meta}</div>`;
|
|
543
|
+
case "size": return `<div class="tk"><div class="bar" style="width:${r};height:${r};max-height:48px"></div>${meta}</div>`;
|
|
544
|
+
case "radius": return `<div class="tk"><div class="rad" style="border-radius:${r}"></div>${meta}</div>`;
|
|
545
|
+
case "border": return `<div class="tk"><div class="rad" style="border:${r} solid var(--color-border-strong,#404040)"></div>${meta}</div>`;
|
|
546
|
+
case "opacity": return `<div class="tk"><div class="sw" style="background:var(--color-brand,#e50914);opacity:${r}"></div>${meta}</div>`;
|
|
547
|
+
case "shadow": return `<div class="tk"><div class="elev" style="box-shadow:${r}"></div>${meta}</div>`;
|
|
548
|
+
default: return `<div class="tk chip">${meta}</div>`;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function renderTokenRow(t) {
|
|
552
|
+
return `<div class="ds-token-row"><code class="ds-token-name">${esc(t.name)}${provTag(t)}</code><code class="ds-token-pill">${esc(t.value)}</code></div>`;
|
|
553
|
+
}
|
|
554
|
+
// Super-group assignment for nav + section tags.
|
|
555
|
+
const GROUP_SUPER = {
|
|
556
|
+
brand: "COLOR", bg: "COLOR", text: "COLOR", line: "COLOR",
|
|
557
|
+
font: "TYPOGRAPHY", fs: "TYPOGRAPHY", fw: "TYPOGRAPHY", lh: "TYPOGRAPHY",
|
|
558
|
+
space: "LAYOUT", size: "LAYOUT", bp: "LAYOUT",
|
|
559
|
+
radius: "SURFACE", border: "SURFACE", shadow: "SURFACE",
|
|
560
|
+
opacity: "EFFECTS", motion: "EFFECTS", z: "EFFECTS",
|
|
561
|
+
// category-based (native projects)
|
|
562
|
+
color: "COLOR", fontFamily: "TYPOGRAPHY", fontSize: "TYPOGRAPHY",
|
|
563
|
+
other: "OTHER",
|
|
564
|
+
};
|
|
565
|
+
const SUPER_ORDER = ["COLOR", "TYPOGRAPHY", "LAYOUT", "SURFACE", "EFFECTS", "OTHER"];
|
|
566
|
+
const superNav = new Map();
|
|
567
|
+
for (const g of grouped) {
|
|
568
|
+
const sg = GROUP_SUPER[g.id] ?? "OTHER";
|
|
569
|
+
const list = superNav.get(sg) ?? [];
|
|
570
|
+
list.push(g);
|
|
571
|
+
superNav.set(sg, list);
|
|
572
|
+
}
|
|
573
|
+
const navHtml = SUPER_ORDER.filter((sg) => superNav.has(sg))
|
|
574
|
+
.map((sg) => {
|
|
575
|
+
const items = (superNav.get(sg) ?? [])
|
|
576
|
+
.map((g) => {
|
|
577
|
+
const dot = g.kind === "color" && g.items.length > 0
|
|
578
|
+
? `style="background:${g.items[0].ref}"`
|
|
579
|
+
: `style="background:var(--color-text-faint,var(--text-subdued,#888))"`;
|
|
580
|
+
return `<a class="ds-nav-item" href="#${esc(g.id)}"><span class="ds-nav-dot" ${dot}></span>${esc(g.label)}</a>`;
|
|
581
|
+
})
|
|
582
|
+
.join("\n ");
|
|
583
|
+
return `<div class="ds-nav-section">
|
|
584
|
+
<div class="ds-nav-group-label">${esc(sg)}</div>
|
|
585
|
+
${items}
|
|
586
|
+
</div>`;
|
|
587
|
+
})
|
|
588
|
+
.join("\n ");
|
|
589
|
+
const sectionsHtml = grouped
|
|
590
|
+
.map((g) => {
|
|
591
|
+
const sg = GROUP_SUPER[g.id] ?? "OTHER";
|
|
592
|
+
const useList = g.kind === "chip";
|
|
593
|
+
const body = useList
|
|
594
|
+
? `<div class="ds-token-list">${g.items.map((t) => renderTokenRow(t)).join("")}</div>`
|
|
595
|
+
: `<div class="grid">${g.items.map((t) => renderRefItem(t, g.kind)).join("")}</div>`;
|
|
596
|
+
return `<section id="${esc(g.id)}" class="ds-section">
|
|
597
|
+
<div class="ds-section-header">
|
|
598
|
+
<div class="ds-section-tag">${esc(sg)}</div>
|
|
599
|
+
<h2 class="ds-section-title">${esc(g.label)}</h2>
|
|
600
|
+
<div class="ds-section-count">${g.items.length} token${g.items.length === 1 ? "" : "s"}</div>
|
|
601
|
+
</div>
|
|
602
|
+
${body}
|
|
603
|
+
</section>`;
|
|
604
|
+
})
|
|
605
|
+
.join("\n ");
|
|
606
|
+
const digestShort = esc(contract.digest.slice(0, 23));
|
|
607
|
+
const logoLetter = esc(title.slice(0, 1).toUpperCase());
|
|
608
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
|
|
609
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
610
|
+
<title>${esc(title)} — Design System</title>
|
|
611
|
+
<style>
|
|
612
|
+
${tokenCss}
|
|
613
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
614
|
+
html{scroll-behavior:smooth}
|
|
615
|
+
body{
|
|
616
|
+
font-family:var(--font-body,var(--encore-body-font-stack,system-ui,sans-serif));
|
|
617
|
+
background:var(--color-bg,var(--background-base,var(--bg-base,#fafafa)));
|
|
618
|
+
color:var(--color-text,var(--text-base,var(--text-primary,#1a1a1a)));
|
|
619
|
+
min-height:100vh;font-size:13px;line-height:1.5;
|
|
620
|
+
}
|
|
621
|
+
.ds-layout{display:flex;min-height:100vh}
|
|
622
|
+
|
|
623
|
+
/* ── Sidebar ── */
|
|
624
|
+
.ds-sidebar{
|
|
625
|
+
position:fixed;top:0;left:0;width:240px;height:100vh;
|
|
626
|
+
background:var(--color-surface,var(--background-elevated-base,var(--bg-elevated,#f0f0f0)));
|
|
627
|
+
border-right:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
|
|
628
|
+
display:flex;flex-direction:column;overflow:hidden;z-index:200;
|
|
629
|
+
}
|
|
630
|
+
.ds-sidebar-header{
|
|
631
|
+
padding:20px 16px 16px;flex-shrink:0;
|
|
632
|
+
border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
|
|
633
|
+
}
|
|
634
|
+
.ds-logo{display:flex;align-items:center;gap:10px}
|
|
635
|
+
.ds-logo-mark{
|
|
636
|
+
width:32px;height:32px;border-radius:6px;flex-shrink:0;
|
|
637
|
+
background:var(--color-brand,var(--essential-bright-accent,var(--action-primary,#888)));
|
|
638
|
+
display:flex;align-items:center;justify-content:center;
|
|
639
|
+
font-size:15px;font-weight:700;color:#fff;
|
|
640
|
+
}
|
|
641
|
+
.ds-logo-name{
|
|
642
|
+
font-size:13px;font-weight:700;line-height:1.2;
|
|
643
|
+
color:var(--color-text,var(--text-base,var(--text-primary,inherit)));
|
|
644
|
+
}
|
|
645
|
+
.ds-logo-sub{
|
|
646
|
+
font-size:10px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;margin-top:1px;
|
|
647
|
+
color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
|
|
648
|
+
}
|
|
649
|
+
.ds-nav{flex:1;overflow-y:auto;padding:12px 8px;scrollbar-width:thin}
|
|
650
|
+
.ds-nav-section{margin-bottom:20px}
|
|
651
|
+
.ds-nav-group-label{
|
|
652
|
+
font-size:10px;font-weight:600;letter-spacing:.10em;text-transform:uppercase;
|
|
653
|
+
padding:0 8px;margin-bottom:4px;
|
|
654
|
+
color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
|
|
655
|
+
}
|
|
656
|
+
.ds-nav-item{
|
|
657
|
+
display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:6px;
|
|
658
|
+
font-size:13px;font-weight:500;
|
|
659
|
+
color:var(--color-text-muted,var(--text-subdued,var(--text-secondary,#666)));
|
|
660
|
+
text-decoration:none;cursor:pointer;transition:background .1s,color .1s;
|
|
661
|
+
}
|
|
662
|
+
.ds-nav-item:hover{
|
|
663
|
+
background:var(--color-surface-raised,var(--background-elevated-highlight,rgba(0,0,0,.06)));
|
|
664
|
+
color:var(--color-text,var(--text-base,var(--text-primary,inherit)));
|
|
665
|
+
}
|
|
666
|
+
.ds-nav-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
|
667
|
+
.ds-sidebar-footer{
|
|
668
|
+
padding:12px 16px;flex-shrink:0;
|
|
669
|
+
border-top:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
|
|
670
|
+
font-size:11px;
|
|
671
|
+
color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/* ── Main ── */
|
|
675
|
+
.ds-main{flex:1;margin-left:240px;min-height:100vh}
|
|
676
|
+
.ds-topbar{
|
|
677
|
+
position:sticky;top:0;z-index:150;
|
|
678
|
+
background:var(--color-overlay,rgba(0,0,0,.55));
|
|
679
|
+
backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
|
|
680
|
+
border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.1)));
|
|
681
|
+
padding:10px 48px;display:flex;align-items:center;justify-content:space-between;
|
|
682
|
+
}
|
|
683
|
+
.ds-topbar-title{
|
|
684
|
+
font-size:13px;font-weight:600;letter-spacing:.02em;
|
|
685
|
+
color:var(--color-text-muted,var(--text-subdued,#888));
|
|
686
|
+
}
|
|
687
|
+
.ds-topbar-meta{
|
|
688
|
+
font-size:11px;font-family:ui-monospace,'SF Mono',monospace;
|
|
689
|
+
color:var(--color-text-faint,var(--text-subdued,#aaa));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* ── Section scaffold ── */
|
|
693
|
+
.ds-section{
|
|
694
|
+
padding:48px;
|
|
695
|
+
border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.06)));
|
|
696
|
+
}
|
|
697
|
+
.ds-section-header{margin-bottom:32px}
|
|
698
|
+
.ds-section-tag{
|
|
699
|
+
font-size:10px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;margin-bottom:6px;
|
|
700
|
+
color:var(--color-brand,var(--essential-bright-accent,var(--action-primary,#888)));
|
|
701
|
+
}
|
|
702
|
+
.ds-section-title{
|
|
703
|
+
font-family:var(--font-display,var(--encore-title-font-stack,inherit));
|
|
704
|
+
font-size:22px;font-weight:700;line-height:1.1;margin-bottom:4px;
|
|
705
|
+
color:var(--color-text,var(--text-base,var(--text-primary,inherit)));
|
|
706
|
+
transition:color .15s;
|
|
707
|
+
}
|
|
708
|
+
.ds-section-count{
|
|
709
|
+
font-size:12px;
|
|
710
|
+
color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
|
|
711
|
+
}
|
|
712
|
+
section:target .ds-section-title{
|
|
713
|
+
color:var(--color-brand,var(--essential-bright-accent,var(--action-primary,#888)));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* ── Swatch grid ── */
|
|
717
|
+
.grid{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end}
|
|
718
|
+
.tk{width:150px}
|
|
719
|
+
.tk.wide{width:100%}
|
|
720
|
+
.tk.chip{width:auto;
|
|
721
|
+
background:var(--color-surface,var(--background-elevated-base,rgba(0,0,0,.04)));
|
|
722
|
+
border:1px solid var(--color-border,rgba(0,0,0,.08));
|
|
723
|
+
border-radius:var(--radius-md,8px);padding:8px 12px;
|
|
724
|
+
}
|
|
725
|
+
.tk-name{
|
|
726
|
+
font-size:11px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;margin-top:6px;
|
|
727
|
+
color:var(--color-text-muted,var(--text-subdued,var(--text-secondary,#666)));
|
|
728
|
+
}
|
|
729
|
+
.tk-val{
|
|
730
|
+
font-size:11px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;
|
|
731
|
+
color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
|
|
732
|
+
}
|
|
733
|
+
.prov{
|
|
734
|
+
color:var(--color-warning,var(--essential-warning,var(--status-warning,#e8a020)));
|
|
735
|
+
font-size:10px;
|
|
736
|
+
}
|
|
737
|
+
.sw{height:64px;border-radius:var(--radius-md,8px);border:1px solid var(--color-border,rgba(0,0,0,.1))}
|
|
738
|
+
.bar{height:12px;background:var(--color-brand,var(--essential-bright-accent,#888));border-radius:3px;min-width:2px}
|
|
739
|
+
.rad{
|
|
740
|
+
width:80px;height:64px;
|
|
741
|
+
background:var(--color-surface-raised,var(--background-elevated-highlight,rgba(0,0,0,.06)));
|
|
742
|
+
border:1px solid var(--color-border-strong,var(--border-default,rgba(0,0,0,.15)));
|
|
743
|
+
}
|
|
744
|
+
.elev{
|
|
745
|
+
width:120px;height:64px;border-radius:var(--radius-md,8px);
|
|
746
|
+
background:var(--color-surface-raised,var(--background-elevated-base,#fff));
|
|
747
|
+
}
|
|
748
|
+
.samp,.fam{color:var(--color-text,var(--text-base,var(--text-primary,inherit)))}
|
|
749
|
+
|
|
750
|
+
/* ── Token list (motion / bp / z-index / lh) ── */
|
|
751
|
+
.ds-token-list{display:flex;flex-direction:column;max-width:680px}
|
|
752
|
+
.ds-token-row{
|
|
753
|
+
display:flex;align-items:baseline;gap:16px;padding:8px 0;
|
|
754
|
+
border-bottom:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.06)));
|
|
755
|
+
}
|
|
756
|
+
.ds-token-row:last-child{border-bottom:none}
|
|
757
|
+
.ds-token-name{
|
|
758
|
+
font-size:12px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;
|
|
759
|
+
color:var(--color-text-muted,var(--text-subdued,var(--text-secondary,#666)));
|
|
760
|
+
min-width:220px;flex-shrink:0;
|
|
761
|
+
}
|
|
762
|
+
.ds-token-pill{
|
|
763
|
+
font-size:11px;font-family:ui-monospace,'SF Mono','Fira Code',monospace;
|
|
764
|
+
background:var(--color-surface-raised,var(--background-elevated-base,rgba(0,0,0,.05)));
|
|
765
|
+
color:var(--color-text,var(--text-base,inherit));
|
|
766
|
+
border-radius:4px;padding:2px 8px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* ── Components ── */
|
|
770
|
+
.comps{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
|
|
771
|
+
.btn{
|
|
772
|
+
border:none;cursor:pointer;font-weight:var(--fw-medium,500);
|
|
773
|
+
font-size:var(--fs-sm,var(--encore-text-size-smaller,13px));
|
|
774
|
+
padding:8px 20px;
|
|
775
|
+
border-radius:var(--radius-sm,var(--encore-corner-radius-base,4px));
|
|
776
|
+
font-family:inherit;transition:opacity .12s;
|
|
777
|
+
}
|
|
778
|
+
.btn-primary{
|
|
779
|
+
background:var(--color-brand,var(--essential-bright-accent,#888));
|
|
780
|
+
color:var(--color-text-on-brand,var(--text-bright-accent,#fff));
|
|
781
|
+
}
|
|
782
|
+
.btn-secondary{
|
|
783
|
+
background:var(--color-surface-raised,var(--background-elevated-base,rgba(0,0,0,.06)));
|
|
784
|
+
color:var(--color-text,var(--text-base,inherit));
|
|
785
|
+
border:1px solid var(--color-border,rgba(0,0,0,.1));
|
|
786
|
+
}
|
|
787
|
+
.card{
|
|
788
|
+
width:200px;height:112px;
|
|
789
|
+
background:var(--color-surface,var(--background-elevated-base,#f4f4f4));
|
|
790
|
+
border-radius:var(--radius-md,var(--encore-corner-radius-larger-2,8px));
|
|
791
|
+
box-shadow:var(--shadow-2,var(--encore-overlay-box-shadow,0 4px 12px rgba(0,0,0,.2)));
|
|
792
|
+
display:flex;align-items:flex-end;padding:12px;
|
|
793
|
+
border:1px solid var(--color-border,rgba(0,0,0,.08));
|
|
794
|
+
}
|
|
795
|
+
.badge{
|
|
796
|
+
background:var(--color-brand,var(--essential-bright-accent,#888));
|
|
797
|
+
color:var(--color-text-on-brand,#fff);font-size:11px;padding:2px 8px;
|
|
798
|
+
border-radius:var(--radius-pill,var(--encore-border-radius-rounded,999px));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* ── Growth layer ── */
|
|
802
|
+
.growth-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px}
|
|
803
|
+
.growth-card{
|
|
804
|
+
padding:16px;border-radius:var(--radius-md,8px);
|
|
805
|
+
background:var(--color-surface-raised,var(--background-elevated-base,rgba(0,0,0,.03)));
|
|
806
|
+
border:1px solid var(--color-border,var(--border-subtle,rgba(0,0,0,.08)));
|
|
807
|
+
}
|
|
808
|
+
.growth-card-title{
|
|
809
|
+
font-size:13px;font-weight:600;margin-bottom:4px;
|
|
810
|
+
color:var(--color-text,var(--text-base,inherit));
|
|
811
|
+
}
|
|
812
|
+
.growth-card-desc{
|
|
813
|
+
font-size:12px;line-height:1.5;
|
|
814
|
+
color:var(--color-text-faint,var(--text-subdued,#888));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* ── Footer ── */
|
|
818
|
+
.ds-footer{
|
|
819
|
+
padding:24px 48px;font-size:11px;
|
|
820
|
+
color:var(--color-text-faint,var(--text-subdued,var(--text-tertiary,#888)));
|
|
821
|
+
border-top:1px solid var(--color-border,rgba(0,0,0,.06));
|
|
822
|
+
}
|
|
823
|
+
</style></head>
|
|
824
|
+
<body>
|
|
825
|
+
<div class="ds-layout">
|
|
826
|
+
|
|
827
|
+
<aside class="ds-sidebar">
|
|
828
|
+
<div class="ds-sidebar-header">
|
|
829
|
+
<div class="ds-logo">
|
|
830
|
+
<div class="ds-logo-mark">${logoLetter}</div>
|
|
831
|
+
<div>
|
|
832
|
+
<div class="ds-logo-name">${esc(title.toUpperCase())}</div>
|
|
833
|
+
<div class="ds-logo-sub">Design System</div>
|
|
834
|
+
</div>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
<nav class="ds-nav">
|
|
838
|
+
${navHtml}
|
|
839
|
+
<div class="ds-nav-section">
|
|
840
|
+
<div class="ds-nav-group-label">REFERENCE</div>
|
|
841
|
+
<a class="ds-nav-item" href="#components"><span class="ds-nav-dot" style="background:var(--color-brand,var(--essential-bright-accent,#888))"></span>Components</a>
|
|
842
|
+
<a class="ds-nav-item" href="#growth"><span class="ds-nav-dot" style="background:var(--color-text-faint,#888)"></span>Growth layer</a>
|
|
843
|
+
</div>
|
|
844
|
+
</nav>
|
|
845
|
+
<div class="ds-sidebar-footer">
|
|
846
|
+
<div>v1.0 · ${digestShort}</div>
|
|
847
|
+
<div style="margin-top:2px">${contract.tokens.length} tokens extracted</div>
|
|
848
|
+
</div>
|
|
849
|
+
</aside>
|
|
850
|
+
|
|
851
|
+
<div class="ds-main">
|
|
852
|
+
<div class="ds-topbar">
|
|
853
|
+
<span class="ds-topbar-title">${esc(title)} — Design Reference</span>
|
|
854
|
+
<span class="ds-topbar-meta">${digestShort}</span>
|
|
855
|
+
</div>
|
|
856
|
+
<main>
|
|
857
|
+
${sectionsHtml}
|
|
858
|
+
<section id="components" class="ds-section">
|
|
859
|
+
<div class="ds-section-header">
|
|
860
|
+
<div class="ds-section-tag">REFERENCE</div>
|
|
861
|
+
<h2 class="ds-section-title">Component Samples</h2>
|
|
862
|
+
<div class="ds-section-count">built from the tokens above</div>
|
|
863
|
+
</div>
|
|
864
|
+
<div class="comps">
|
|
865
|
+
<button class="btn btn-primary">▶ Play</button>
|
|
866
|
+
<button class="btn btn-secondary">+ My List</button>
|
|
867
|
+
<span class="badge">NEW</span>
|
|
868
|
+
<div class="card"><span style="font-size:12px;color:var(--color-text-muted,var(--text-subdued,#888))">${esc(title)}</span></div>
|
|
869
|
+
</div>
|
|
870
|
+
</section>
|
|
871
|
+
<section id="growth" class="ds-section">
|
|
872
|
+
<div class="ds-section-header">
|
|
873
|
+
<div class="ds-section-tag">GROWTH LAYER</div>
|
|
874
|
+
<h2 class="ds-section-title">Extends This Template</h2>
|
|
875
|
+
<div class="ds-section-count">advisory — not an enforcement gate</div>
|
|
876
|
+
</div>
|
|
877
|
+
<div class="growth-grid">
|
|
878
|
+
<div class="growth-card">
|
|
879
|
+
<div class="growth-card-title">Component specs ✓</div>
|
|
880
|
+
<div class="growth-card-desc">Buttons, card, badge shown above. Every component is a composition of the tokens in this document.</div>
|
|
881
|
+
</div>
|
|
882
|
+
<div class="growth-card">
|
|
883
|
+
<div class="growth-card-title">Usage guidelines</div>
|
|
884
|
+
<div class="growth-card-desc">Do / don't, when-to-use, pairing rules per token group. Defined by the team, not by extraction.</div>
|
|
885
|
+
</div>
|
|
886
|
+
<div class="growth-card">
|
|
887
|
+
<div class="growth-card-title">Accessibility</div>
|
|
888
|
+
<div class="growth-card-desc">Contrast ratios, focus states, motion-reduce overrides. Requires computation against real backgrounds.</div>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
<p style="margin-top:20px;font-size:12px;color:var(--color-text-faint,var(--text-subdued,#888))">
|
|
892
|
+
The token taxonomy above is the <strong>minimum template</strong> — deterministically extracted, never fabricated.
|
|
893
|
+
Components, usage guidelines, and accessibility are the advisory growth layer: they extend the source-of-truth tokens but are never gates.
|
|
894
|
+
</p>
|
|
895
|
+
</section>
|
|
896
|
+
</main>
|
|
897
|
+
<footer class="ds-footer">
|
|
898
|
+
Generated by Vise design-reference — advisory documentation, not an enforcement gate.
|
|
899
|
+
Contract digest: ${esc(contract.digest)}. Source: ${esc((contract.source?.inputs ?? []).join(", "))}.
|
|
900
|
+
</footer>
|
|
901
|
+
</div>
|
|
902
|
+
|
|
903
|
+
</div>
|
|
904
|
+
</body></html>`;
|
|
905
|
+
}
|
|
369
906
|
const ADVISORY_NOTE = "Advisory only — non-blocking and NOT part of `vise check`. One-off literals (overlays, scrims, pure #fff/#000, gradients) are expected and fine; off-contract literals are review hints, not violations.";
|
|
370
907
|
export const designCheckTool = {
|
|
371
908
|
name: "design_check",
|
|
@@ -912,14 +1449,21 @@ function categorizeDeclaredVar(name, value) {
|
|
|
912
1449
|
if (/radius|radii|corner|\bround/.test(n)) {
|
|
913
1450
|
return "radius";
|
|
914
1451
|
}
|
|
1452
|
+
// borderWidth: name-and-value gated, MUST come before the color branch because
|
|
1453
|
+
// the color regex matches /border/ — `--border-width-thin: 1px` is a length, not a color.
|
|
1454
|
+
if (/border-?width|stroke-?width|outline-?width/.test(n) && LENGTH.test(v)) {
|
|
1455
|
+
return "borderWidth";
|
|
1456
|
+
}
|
|
915
1457
|
if (/(color|colour|bg|background|fg|foreground|surface|border|brand|primary|secondary|accent|fill|stroke|ink|text-color)/.test(n) || isColor(v)) {
|
|
916
1458
|
return "color";
|
|
917
1459
|
}
|
|
918
1460
|
if (/(font-family|fontfamily|typeface|family)/.test(n) && /[a-z]/i.test(v) && !LENGTH.test(v) && !isColor(v)) {
|
|
919
1461
|
return "fontFamily";
|
|
920
1462
|
}
|
|
921
|
-
|
|
922
|
-
|
|
1463
|
+
// fontSize: broaden to include the common --fs-* naming convention (e.g. --fs-sm: 14px).
|
|
1464
|
+
// 'leading' and 'line-height' are moved to lineHeight below.
|
|
1465
|
+
if (/(font-size|fontsize|text-size)/.test(n) || /\btext-(xs|sm|base|md|lg|xl|\dxl|\d)\b/.test(n) || /^--fs-\w/.test(n)) {
|
|
1466
|
+
return "fontSize"; // incl. the Tailwind text-scale convention (--text-sm/base/lg) and --fs-* shorthand
|
|
923
1467
|
}
|
|
924
1468
|
// Motion is value-gated: only time/easing values become motion tokens. This
|
|
925
1469
|
// stops substring matches like "increase"/"decrease" (which contain "ease")
|
|
@@ -935,6 +1479,30 @@ function categorizeDeclaredVar(name, value) {
|
|
|
935
1479
|
if (/(font|type)/.test(n) && /[a-z]/i.test(v) && !LENGTH.test(v) && !isColor(v) && !TIME.test(v)) {
|
|
936
1480
|
return "fontFamily";
|
|
937
1481
|
}
|
|
1482
|
+
// fontWeight: name-gated (bare integers collide with z-index). Value must be a
|
|
1483
|
+
// 3-digit integer in the valid CSS font-weight range 100–950.
|
|
1484
|
+
if (/\bfw\b|font-?weight|\bweight\b/.test(n)) {
|
|
1485
|
+
const num = Number(v);
|
|
1486
|
+
if (/^\d{3}$/.test(v) && num >= 100 && num <= 950) {
|
|
1487
|
+
return "fontWeight";
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
// lineHeight: name-gated + unitless value (1.1, 1.4, 1.6 etc.). Captures both
|
|
1491
|
+
// 'lh-*' shorthand and 'leading-*'/'line-height-*' naming conventions.
|
|
1492
|
+
if (/\blh\b|leading|line-?height|lineheight/.test(n) && /^-?[0-9]*\.?[0-9]+$/.test(v) && !LENGTH.test(v)) {
|
|
1493
|
+
return "lineHeight";
|
|
1494
|
+
}
|
|
1495
|
+
// letterSpacing: name-gated + any CSS length. Must come before the generic
|
|
1496
|
+
// LENGTH→space fallback so --ls-* tokens aren't misfiled as spacing.
|
|
1497
|
+
if (/\bls\b|letter-?spacing|letterspacing|\btracking\b/.test(n) && LENGTH.test(v)) {
|
|
1498
|
+
return "letterSpacing";
|
|
1499
|
+
}
|
|
1500
|
+
// breakpoint: name-gated + length. Must precede the LENGTH fallback. The
|
|
1501
|
+
// contract already captures @media breakpoints in contract.breakpoints[]; this
|
|
1502
|
+
// catches explicit --bp-* / --breakpoint-* custom-property tokens.
|
|
1503
|
+
if (/\bbp\b|break-?point|viewport-?width|screen-?size/.test(n) && LENGTH.test(v)) {
|
|
1504
|
+
return "breakpoint";
|
|
1505
|
+
}
|
|
938
1506
|
if (/(space|spacing|gap|size|gutter|inset|pad|margin)/.test(n) && LENGTH.test(v)) {
|
|
939
1507
|
return "space";
|
|
940
1508
|
}
|
|
@@ -944,6 +1512,16 @@ function categorizeDeclaredVar(name, value) {
|
|
|
944
1512
|
if (LENGTH.test(v)) {
|
|
945
1513
|
return "space";
|
|
946
1514
|
}
|
|
1515
|
+
// Opacity: declared-only (a bare 0.4 in code could be line-height/scale/anything — never infer).
|
|
1516
|
+
if (/opacity/.test(n) && /^(0(\.\d+)?|\.\d+|1(\.0*)?)$/.test(v)) {
|
|
1517
|
+
return "opacity";
|
|
1518
|
+
}
|
|
1519
|
+
// zIndex: name-gated strictly; integers collide with fontWeight so the name is
|
|
1520
|
+
// the only reliable signal. Use z-index / z-idx or the common --z-* prefix.
|
|
1521
|
+
// Note: bare `/z/` would catch --zoom, --size, etc. — do NOT relax this guard.
|
|
1522
|
+
if ((/z-?index|z-?idx/.test(n) || /^--z-\w/.test(n)) && /^-?\d+$/.test(v)) {
|
|
1523
|
+
return "zIndex";
|
|
1524
|
+
}
|
|
947
1525
|
return null;
|
|
948
1526
|
}
|
|
949
1527
|
/** Emit zero or more (category, normalizedValue) observations from one literal declaration. */
|
|
@@ -1118,7 +1696,7 @@ function friendlyComponentName(cls) {
|
|
|
1118
1696
|
// ---------------------------------------------------------------------------
|
|
1119
1697
|
// Sorting, summarizing, digest
|
|
1120
1698
|
// ---------------------------------------------------------------------------
|
|
1121
|
-
const CATEGORY_ORDER = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "motion"];
|
|
1699
|
+
const CATEGORY_ORDER = ["color", "space", "radius", "shadow", "fontFamily", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "borderWidth", "breakpoint", "motion", "opacity", "zIndex"];
|
|
1122
1700
|
function sortTokens(tokens) {
|
|
1123
1701
|
return [...tokens].sort((a, b) => {
|
|
1124
1702
|
const cat = CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category);
|
|
@@ -1435,8 +2013,11 @@ function categorizeTokenModuleValue(keyPath, value) {
|
|
|
1435
2013
|
return null; // reference / computed — not a concrete token
|
|
1436
2014
|
}
|
|
1437
2015
|
const key = keyPath.toLowerCase();
|
|
1438
|
-
|
|
1439
|
-
|
|
2016
|
+
// These categories are CSS-only (declared custom properties): the name-gating
|
|
2017
|
+
// that makes them safe against false positives only works in CSS. Module/native
|
|
2018
|
+
// extraction skips them rather than risk inventing wrong tokens.
|
|
2019
|
+
if (/screen|breakpoint|media|z-?index|opacity|weight|\blh\b|leading|line-?height|letter-?spacing/.test(key)) {
|
|
2020
|
+
return null;
|
|
1440
2021
|
}
|
|
1441
2022
|
// Colors: unambiguous from the value alone.
|
|
1442
2023
|
if (isColor(v)) {
|
package/dist/tools/project.js
CHANGED
|
@@ -499,8 +499,16 @@ async function validateEnvSecretHygiene(root, platform, sourceContent) {
|
|
|
499
499
|
const findings = [];
|
|
500
500
|
if (platform === "typescript") {
|
|
501
501
|
const envSecretFiles = await committedEnvSecretFiles(root);
|
|
502
|
-
|
|
503
|
-
|
|
502
|
+
if (envSecretFiles.length > 0) {
|
|
503
|
+
// Only fire when the secret-bearing env file is NOT gitignored. A gitignored local `.env` (the
|
|
504
|
+
// exact practice this rule's own recommendation endorses) won't be committed, so flagging it is
|
|
505
|
+
// a false positive — committed-env is about a `.env` that WILL be committed (no .gitignore cover).
|
|
506
|
+
const gitignore = await readIfExists(path.join(root, ".gitignore"));
|
|
507
|
+
if (!gitignore || !gitignoreIgnoresEnvFiles(gitignore)) {
|
|
508
|
+
for (const file of envSecretFiles) {
|
|
509
|
+
findings.push(finding("typescript.secret.committed-env", "warning", "A local env file appears to contain a social.plus secret.", file, "Do not commit .env files with real API keys. Commit a placeholder .env.example and keep local env files ignored."));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
504
512
|
}
|
|
505
513
|
}
|
|
506
514
|
if (platform === "typescript" || platform === "react-native") {
|
package/package.json
CHANGED