synthesisui 0.1.8 → 0.1.10
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/dist/commands/advise.js +31 -0
- package/dist/guide.js +61 -2
- package/dist/index.js +13 -0
- package/dist/registry.js +26 -0
- package/dist/repo-context.js +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolveRegistry } from "../config.js";
|
|
2
|
+
import { postAdvisor } from "../registry.js";
|
|
3
|
+
import { buildRepoContext } from "../repo-context.js";
|
|
4
|
+
/**
|
|
5
|
+
* Asks the hosted advisor for engagement-pattern proposals, grounded in THIS
|
|
6
|
+
* project (the CLI gathers a compact repo summary) + the value proposition you
|
|
7
|
+
* pass. The advisor proposes only — it changes nothing in your project.
|
|
8
|
+
*/
|
|
9
|
+
export async function advise(valueProp, opts) {
|
|
10
|
+
const base = resolveRegistry(opts.registry);
|
|
11
|
+
const root = opts.dir ?? process.cwd();
|
|
12
|
+
const repo = await buildRepoContext(root);
|
|
13
|
+
const context = `Proposta de valor: ${valueProp}\n\n${repo}`;
|
|
14
|
+
console.log(`→ asking the advisor at ${base} …`);
|
|
15
|
+
const res = await postAdvisor(base, context);
|
|
16
|
+
if (res.proposals.length === 0) {
|
|
17
|
+
console.log("No proposals returned.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`\nEngagement proposals (${res.model}):\n`);
|
|
21
|
+
res.proposals.forEach((p, i) => {
|
|
22
|
+
console.log(`${i + 1}. ${p.pattern}`);
|
|
23
|
+
console.log(` ${p.rationale}`);
|
|
24
|
+
if (p.suggestedBlocks.length) {
|
|
25
|
+
console.log(` blocks: ${p.suggestedBlocks.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
console.log("");
|
|
28
|
+
});
|
|
29
|
+
console.log(`(${res.usage.inputTokens} in / ${res.usage.outputTokens} out tokens — ` +
|
|
30
|
+
`proposals only; nothing in your project was changed)`);
|
|
31
|
+
}
|
package/dist/guide.js
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
const kebab = (v) => v.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2
2
|
const list = (items) => items.length ? items.map((i) => `\`${i}\``).join(", ") : "_(none)_";
|
|
3
3
|
const dataAttrs = (variants) => Object.entries(variants).map(([axis, opts]) => `data-${kebab(axis)}="${Object.keys(opts).join("|")}"`);
|
|
4
|
+
// Famílias genéricas do CSS — fallbacks, não webfonts a carregar.
|
|
5
|
+
const GENERIC_FAMILIES = new Set([
|
|
6
|
+
"sans-serif",
|
|
7
|
+
"serif",
|
|
8
|
+
"monospace",
|
|
9
|
+
"system-ui",
|
|
10
|
+
"ui-sans-serif",
|
|
11
|
+
"ui-serif",
|
|
12
|
+
"ui-monospace",
|
|
13
|
+
"cursive",
|
|
14
|
+
"fantasy",
|
|
15
|
+
"inherit",
|
|
16
|
+
"initial",
|
|
17
|
+
]);
|
|
18
|
+
/** Famílias custom do documento (display/body/mono), deduplicadas, sem genéricos. */
|
|
19
|
+
function customFontFamilies(families) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const family of [families.display, families.body, families.mono]) {
|
|
23
|
+
const name = family?.trim();
|
|
24
|
+
if (!name)
|
|
25
|
+
continue;
|
|
26
|
+
const key = name.toLowerCase();
|
|
27
|
+
if (GENERIC_FAMILIES.has(key) || seen.has(key))
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(key);
|
|
30
|
+
out.push(name);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
4
34
|
/** One entry per component: class, variant data-*, states, and multi-part anatomy. */
|
|
5
35
|
function componentEntry(cname, recipe) {
|
|
6
36
|
const cls = `.ds-${kebab(cname)}`;
|
|
@@ -31,6 +61,32 @@ export function buildGuide(payload) {
|
|
|
31
61
|
const { meta, foundations, motion, components } = doc;
|
|
32
62
|
const semanticRoles = Object.keys(foundations.color.semantic);
|
|
33
63
|
const seriesKeys = Object.keys(foundations.color.series ?? {});
|
|
64
|
+
const fontFamilies = customFontFamilies(foundations.typography.families);
|
|
65
|
+
const fontsHref = fontFamilies.length > 0
|
|
66
|
+
? `https://fonts.googleapis.com/css2?${fontFamilies
|
|
67
|
+
.map((n) => `family=${n.replace(/ /g, "+")}:wght@400;500;600;700`)
|
|
68
|
+
.join("&")}&display=swap`
|
|
69
|
+
: null;
|
|
70
|
+
const fontsSection = fontsHref
|
|
71
|
+
? `
|
|
72
|
+
## Fonts
|
|
73
|
+
|
|
74
|
+
This system's type relies on ${list(fontFamilies)} — **the DS ships token names, not the
|
|
75
|
+
fonts themselves.** If you don't load them they fall back to a generic family and the system loses
|
|
76
|
+
its typographic identity. Load them once (any one approach):
|
|
77
|
+
|
|
78
|
+
- **Google Fonts** — drop in your \`<head>\` (or root layout):
|
|
79
|
+
\`\`\`html
|
|
80
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
81
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
82
|
+
<link rel="stylesheet" href="${fontsHref}" />
|
|
83
|
+
\`\`\`
|
|
84
|
+
- **Next.js** (\`next/font/google\`), **Fontsource**, or self-hosted \`@font-face\` work too — just
|
|
85
|
+
register the families above. If a family isn't on Google Fonts, self-host it.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
`
|
|
89
|
+
: "";
|
|
34
90
|
const weights = Object.keys(foundations.typography.weights);
|
|
35
91
|
const hasAlt = foundations.color.semanticAlt &&
|
|
36
92
|
Object.keys(foundations.color.semanticAlt).length > 0;
|
|
@@ -82,7 +138,7 @@ ${hasAlt
|
|
|
82
138
|
root.toggleAttribute("data-scheme"); // present = ${altScheme}, absent = ${meta.scheme}
|
|
83
139
|
\`\`\`
|
|
84
140
|
`
|
|
85
|
-
: ""}${hasTailwind
|
|
141
|
+
: ""}${fontsSection}${hasTailwind
|
|
86
142
|
? `
|
|
87
143
|
## Styling with Tailwind v4 (preferred in this project)
|
|
88
144
|
|
|
@@ -206,7 +262,10 @@ ${hasTailwind
|
|
|
206
262
|
weights${hasTailwind ? " (utility: `font-<key>`)" : ""}: ${list(weights)};
|
|
207
263
|
scale \`--ds-typography-scale-<key>-font-size\`${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${list(Object.keys(foundations.typography.scale))}.
|
|
208
264
|
- Motion: durations \`--ds-motion-durations-<key>\` (${list(Object.keys(motion.durations))}) and
|
|
209
|
-
easings \`--ds-motion-easings-<key>\` (${list(Object.keys(motion.easings))}).
|
|
265
|
+
easings \`--ds-motion-easings-<key>\` (${list(Object.keys(motion.easings))}). Use them on
|
|
266
|
+
\`transition\`/\`animation\` (e.g. \`transition: color var(--ds-motion-durations-fast) var(--ds-motion-easings-standard)\`)
|
|
267
|
+
so timing stays on-brand. The DS ships timing tokens, **not** a runtime — for entrance/reveal/stagger
|
|
268
|
+
pair them with a motion lib (e.g. \`motion\`/Framer) or CSS \`@keyframes\`.
|
|
210
269
|
- When **creating a new component** the DS does not cover yet: compose it from these semantic
|
|
211
270
|
tokens to inherit the system's identity; do not invent colors/measures outside the scale.
|
|
212
271
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { add } from "./commands/add.js";
|
|
3
|
+
import { advise } from "./commands/advise.js";
|
|
3
4
|
import { list } from "./commands/list.js";
|
|
4
5
|
import { login } from "./commands/login.js";
|
|
5
6
|
import { RegistryError } from "./registry.js";
|
|
@@ -9,6 +10,7 @@ Usage:
|
|
|
9
10
|
synthesisui login [options] connect the CLI to your account (device-flow)
|
|
10
11
|
synthesisui list [options] list the published design systems
|
|
11
12
|
synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
|
|
13
|
+
synthesisui advise "<value prop>" engagement-pattern proposals for this project (login required)
|
|
12
14
|
|
|
13
15
|
Options:
|
|
14
16
|
--registry <url> registry URL (or env SYNTHESISUI_REGISTRY_URL)
|
|
@@ -22,6 +24,7 @@ Examples:
|
|
|
22
24
|
synthesisui add halogen
|
|
23
25
|
synthesisui add halogen --version 3
|
|
24
26
|
synthesisui add halogen --registry http://localhost:3737
|
|
27
|
+
synthesisui advise "habit-building app for tracking personal finances"
|
|
25
28
|
`;
|
|
26
29
|
/** Extracts simple `--flag value` pairs and the remaining positionals. */
|
|
27
30
|
function parseFlags(argv) {
|
|
@@ -84,6 +87,16 @@ async function main() {
|
|
|
84
87
|
case "login":
|
|
85
88
|
await login({ registry });
|
|
86
89
|
break;
|
|
90
|
+
case "advise": {
|
|
91
|
+
const valueProp = args.join(" ").trim();
|
|
92
|
+
if (!valueProp) {
|
|
93
|
+
console.error('error: describe your product — `synthesisui advise "<value proposition>"`');
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await advise(valueProp, { registry, dir });
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
87
100
|
default:
|
|
88
101
|
console.error(`unknown command: "${command}"\n`);
|
|
89
102
|
console.log(HELP);
|
package/dist/registry.js
CHANGED
|
@@ -42,3 +42,29 @@ export async function fetchDesignSystem(base, slug, version) {
|
|
|
42
42
|
}
|
|
43
43
|
return (await res.json());
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Calls the hosted advisor (`POST /api/ai/advisor`). Gated + metered server-side:
|
|
47
|
+
* 401 = not logged in, 429 = daily quota reached. Sends the Bearer token if present.
|
|
48
|
+
*/
|
|
49
|
+
export async function postAdvisor(base, context) {
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(`${base}/api/ai/advisor`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json", ...(await authHeaders()) },
|
|
55
|
+
body: JSON.stringify({ context }),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new RegistryError(`Could not reach the registry at ${base}. ` +
|
|
60
|
+
`Check the URL (--registry / SYNTHESISUI_REGISTRY_URL) and your connection.`);
|
|
61
|
+
}
|
|
62
|
+
if (res.status === 401) {
|
|
63
|
+
throw new RegistryError("Not authenticated. Run `synthesisui login` first.");
|
|
64
|
+
}
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const body = (await res.json().catch(() => ({})));
|
|
67
|
+
throw new RegistryError(body.message ?? `Advisor responded ${res.status}.`);
|
|
68
|
+
}
|
|
69
|
+
return (await res.json());
|
|
70
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Monta um resumo COMPACTO e aterrado do projeto pro advisor — barato em tokens
|
|
5
|
+
* e suficiente pra propostas específicas: stack (package.json), forma do repo
|
|
6
|
+
* (árvore nível 1), DS SynthesisUI instalado(s) e o começo do README. Sem dump
|
|
7
|
+
* de código (custo/ruído): o advisor propõe padrões, não lê implementação.
|
|
8
|
+
*/
|
|
9
|
+
const SKIP_DIRS = new Set([
|
|
10
|
+
"node_modules",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
"out",
|
|
14
|
+
"coverage",
|
|
15
|
+
".next",
|
|
16
|
+
".nx",
|
|
17
|
+
".turbo",
|
|
18
|
+
".cache",
|
|
19
|
+
".vercel",
|
|
20
|
+
]);
|
|
21
|
+
const MAX_README_LINES = 40;
|
|
22
|
+
const MAX_DEPS = 40;
|
|
23
|
+
async function readJson(path) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function topLevelTree(root) {
|
|
32
|
+
try {
|
|
33
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
34
|
+
return entries
|
|
35
|
+
.filter((e) => !e.name.startsWith(".") && !SKIP_DIRS.has(e.name))
|
|
36
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
37
|
+
.sort();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function installedDesignSystems(root) {
|
|
44
|
+
const dsRoot = join(root, "_synthesisui", "ds");
|
|
45
|
+
const out = [];
|
|
46
|
+
try {
|
|
47
|
+
for (const e of await readdir(dsRoot, { withFileTypes: true })) {
|
|
48
|
+
if (!e.isDirectory())
|
|
49
|
+
continue;
|
|
50
|
+
const lock = await readJson(join(dsRoot, e.name, ".lock"));
|
|
51
|
+
out.push(`${lock?.name ?? e.name} (v${lock?.version ?? "?"})`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// nenhum DS instalado — tudo bem
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
async function readmeHead(root) {
|
|
60
|
+
for (const name of ["README.md", "readme.md", "Readme.md"]) {
|
|
61
|
+
try {
|
|
62
|
+
const txt = await readFile(join(root, name), "utf8");
|
|
63
|
+
return txt.split("\n").slice(0, MAX_README_LINES).join("\n").trim();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// tenta o próximo
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
export async function buildRepoContext(root) {
|
|
72
|
+
const pkg = await readJson(join(root, "package.json"));
|
|
73
|
+
const parts = [];
|
|
74
|
+
if (pkg) {
|
|
75
|
+
parts.push(`Projeto: ${pkg.name ?? "(sem nome)"}${pkg.description ? ` — ${pkg.description}` : ""}`);
|
|
76
|
+
const deps = [
|
|
77
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
78
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
79
|
+
].slice(0, MAX_DEPS);
|
|
80
|
+
if (deps.length)
|
|
81
|
+
parts.push(`Dependências: ${deps.join(", ")}`);
|
|
82
|
+
const scripts = Object.keys(pkg.scripts ?? {});
|
|
83
|
+
if (scripts.length)
|
|
84
|
+
parts.push(`Scripts: ${scripts.join(", ")}`);
|
|
85
|
+
}
|
|
86
|
+
const tree = await topLevelTree(root);
|
|
87
|
+
if (tree.length)
|
|
88
|
+
parts.push(`Estrutura (nível 1): ${tree.join(", ")}`);
|
|
89
|
+
const ds = await installedDesignSystems(root);
|
|
90
|
+
if (ds.length)
|
|
91
|
+
parts.push(`Design systems SynthesisUI instalados: ${ds.join(", ")}`);
|
|
92
|
+
const rd = await readmeHead(root);
|
|
93
|
+
if (rd)
|
|
94
|
+
parts.push(`README (início):\n${rd}`);
|
|
95
|
+
return parts.join("\n\n");
|
|
96
|
+
}
|