synthesisui 0.16.501 → 0.16.502

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.
@@ -8,7 +8,7 @@ import { join, relative } from "node:path";
8
8
  import { classify, pixelRatio } from "../reading/classify.js";
9
9
  import { PAGE_SCRIPT } from "../reading/page-script.js";
10
10
  import { decodePng } from "../reading/png.js";
11
- import { moduleClassName, pxOf } from "../role-rules.js";
11
+ import { globalClassOf, globalStyles, moduleClassName, pxOf } from "../role-rules.js";
12
12
  import { agentsToMaintain } from "../agents-chosen.js";
13
13
  import { syncClaudeMd } from "../claude-md.js";
14
14
  import { projectRootFrom } from "./hook.js";
@@ -134,15 +134,25 @@ function moduleOf(cls, files) {
134
134
  }
135
135
  return null;
136
136
  }
137
+ let stylesOf = null;
137
138
  function homeOf(root, files, t) {
138
- const mod = sizeClassOf(t.cls) ? null : moduleOf(t.cls, files);
139
- const cls = sizeClassOf(t.cls) ?? mod?.name ?? null;
139
+ const sized = sizeClassOf(t.cls);
140
+ /* a classe global (o juízo dele, 19/10): `className="section-title"` com o tamanho no globals.css - a do elemento
141
+ primeiro, depois a do pai (o `<h3>` sem classe dentro de `.route`, medido em `.route h3`) */
142
+ if (!sized && stylesOf?.root !== root)
143
+ stylesOf = { root, styles: globalStyles(root) };
144
+ const glob = sized || moduleOf(t.cls, files) ? null
145
+ : globalClassOf(t.cls.split(" | ").flatMap((part) => part.split(/\s+/)), stylesOf?.styles ?? []);
146
+ const mod = sized ? null : (moduleOf(t.cls, files) ?? (glob ? { name: glob.name, local: glob.local } : null));
147
+ const cls = sized ?? mod?.name ?? null;
140
148
  const lineOf = (f) => {
141
149
  if (!cls)
142
150
  return null;
143
151
  const lines = readFileSync(f, "utf8").split("\n");
144
152
  const i = mod
145
- ? lines.findIndex((l) => new RegExp(`\\b\\w+(?:\\.${mod.local}\\b|\\[["']${mod.local}["']\\])`).test(l))
153
+ ? lines.findIndex((l) => glob
154
+ ? new RegExp(`className=[^>]*?(?<![\\w-])${mod.local.replace(/-/g, "\\-")}(?![\\w-])`).test(l)
155
+ : new RegExp(`\\b\\w+(?:\\.${mod.local}\\b|\\[["']${mod.local}["']\\])`).test(l))
146
156
  : lines.findIndex((l) => l.includes(cls));
147
157
  return i >= 0 ? i + 1 : null;
148
158
  };
@@ -260,6 +270,10 @@ export async function confirm(opts) {
260
270
  writeFileSync(join(dsDir, slug, "rules.json"), `${JSON.stringify(rules, null, 2)}\n`);
261
271
  const contract = await syncClaudeMd(root, await agentsToMaintain(root));
262
272
  say(`Your system: ${roles.length} roles and ${breaks.length} breaks, written to _synthesisui/ds/${slug}/rules.json.`);
273
+ /* NUNCA CALADO (o juízo dele, 19/10): um papel lido sem a classe que o identifica no código fica fora - e isso se diz */
274
+ const unplaced = R.roles.filter((r) => !r.home?.cls);
275
+ if (unplaced.length > 0)
276
+ say(`Left out: ${unplaced.map((r) => `${r.role.split(" ")[0]} ${r.px}px`).join(", ")} - the page shows ${unplaced.length === 1 ? "it" : "them"}, but no class in the code says where ${unplaced.length === 1 ? "it is" : "they are"}, so the check could not hold ${unplaced.length === 1 ? "it" : "them"}.`);
263
277
  say(contract ? "The contract your agent reads now lists them - every heading is checked against its role at the end of each turn." : "Run `synthesisui connect` so your agent reads and checks it.");
264
278
  }
265
279
  export async function read(opts) {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join } from "node:path";
4
4
  export async function loadRoleRules(root) {
@@ -76,6 +76,11 @@ function moduleClasses(attrs, imports) {
76
76
  }
77
77
  return out;
78
78
  }
79
+ /** As classes escritas como texto em `className="a b"` (ou num template sem expressão no meio do nome). */
80
+ function plainClasses(attrs) {
81
+ const lit = attrs.match(/className=\{?\s*["'`]([^"'`]*)["'`]/)?.[1] ?? "";
82
+ return lit.split(/\s+/).filter((t) => /^[A-Za-z_][\w-]*$/.test(t));
83
+ }
79
84
  /** O `font-size` de uma classe dentro do `.module.css` - o maior de um `clamp()` (o desktop), `rem` a 16px. */
80
85
  export function modulePx(css, local) {
81
86
  let best = null;
@@ -96,6 +101,60 @@ export function modulePx(css, local) {
96
101
  }
97
102
  return best;
98
103
  }
104
+ /**
105
+ * A CLASSE GLOBAL - achado no juízo dele (19/10, a landing da Naiker, `synthesisui-tests/juizo-porta`): o impeccable
106
+ * escreveu os títulos com classes comuns, `className="section-title"`, e o tamanho no `globals.css`. A leitura achou os
107
+ * papéis (h1 180, h2 70,4, h3 28,8) e o `--confirm` gravou ZERO, calado - o mesmo defeito do CSS Modules (porta-2) numa
108
+ * terceira forma de escrever estilo.
109
+ *
110
+ * A identidade é a mesma forma do módulo: o arquivo e a classe, `globals.css .section-title`. Vale a classe que tem um
111
+ * `font-size` numa folha de estilo comum do projeto (não `.module.css`), na ordem em que vem - a do próprio elemento
112
+ * primeiro.
113
+ */
114
+ export function globalStyles(root) {
115
+ const out = [];
116
+ const walk = (d) => {
117
+ let names = [];
118
+ try {
119
+ names = readdirSync(d);
120
+ }
121
+ catch {
122
+ return;
123
+ }
124
+ for (const n of names) {
125
+ if (n === "node_modules" || n.startsWith(".") || n === "_synthesisui" || n === "public")
126
+ continue;
127
+ const p = join(d, n);
128
+ let dir = false;
129
+ try {
130
+ dir = statSync(p).isDirectory();
131
+ }
132
+ catch {
133
+ continue;
134
+ }
135
+ if (dir)
136
+ walk(p);
137
+ else if (/\.s?css$/.test(n) && !/\.module\.s?css$/.test(n))
138
+ out.push({ file: n, css: readFileSync(p, "utf8") });
139
+ }
140
+ };
141
+ walk(existsSync(join(root, "src")) ? join(root, "src") : root);
142
+ if (existsSync(join(root, "app")) && existsSync(join(root, "src")))
143
+ walk(join(root, "app"));
144
+ return out;
145
+ }
146
+ export function globalClassOf(tokens, styles) {
147
+ for (const tok of tokens) {
148
+ if (!/^[A-Za-z_][\w-]*$/.test(tok))
149
+ continue;
150
+ for (const st of styles) {
151
+ const px = modulePx(st.css, tok);
152
+ if (px !== null)
153
+ return { name: moduleClassName(st.file, tok), local: tok, px };
154
+ }
155
+ }
156
+ return null;
157
+ }
99
158
  /** Dentro de um `.map(` ainda aberto = o título se repete = é de cartão. A mesma pergunta que a régua faz ao DOM. */
100
159
  function insideMap(src, at) {
101
160
  const before = src.slice(0, at);
@@ -121,6 +180,9 @@ export function checkRoles(rel, src, rules, root) {
121
180
  cssOf.set(file, existsSync(file) ? readFileSync(file, "utf8") : "");
122
181
  return modulePx(cssOf.get(file), local);
123
182
  };
183
+ /** As folhas de estilo comuns do projeto, lidas uma vez e só se algum título usar classe sem tamanho do Tailwind. */
184
+ let globals = null;
185
+ const styles = () => (globals ??= root ? globalStyles(root) : []);
124
186
  const component = basename(rel, extname(rel));
125
187
  const known = new Set((rules.knownDrift ?? []).filter((d) => d.at.startsWith(rel)).map((d) => d.class));
126
188
  const section = rules.roles.find((r) => r.element === "h2" && r.context === "section");
@@ -128,7 +190,8 @@ export function checkRoles(rel, src, rules, root) {
128
190
  for (const m of src.matchAll(/<(h1|h2|h3)\b([^>]*)>/g)) {
129
191
  const el = m[1];
130
192
  const mod = sizeClass(m[2]) ? null : (moduleClasses(m[2], imports)[0] ?? null);
131
- const cls = sizeClass(m[2]) ?? mod?.name ?? null;
193
+ const glob = sizeClass(m[2]) || mod ? null : globalClassOf(plainClasses(m[2]), styles());
194
+ const cls = sizeClass(m[2]) ?? mod?.name ?? glob?.name ?? null;
132
195
  if (!cls)
133
196
  continue;
134
197
  const line = src.slice(0, m.index).split("\n").length;
@@ -141,7 +204,7 @@ export function checkRoles(rel, src, rules, root) {
141
204
  continue;
142
205
  if (known.has(bare))
143
206
  continue;
144
- const px = mod ? pxOfModule(mod.path, mod.local) : pxOf(bare);
207
+ const px = mod ? pxOfModule(mod.path, mod.local) : glob ? glob.px : pxOf(bare);
145
208
  const role = mine.find((r) => r.context === context) ?? mine[0];
146
209
  const brk = rules.breaks.find((b) => b.class === bare);
147
210
  if (brk) {
@@ -184,7 +247,8 @@ export function checkRoles(rel, src, rules, root) {
184
247
  continue;
185
248
  const cls = m[2].match(/className=\{?\s*["'`]([^"'`]*)["'`]/)?.[1] ?? "";
186
249
  const mods = moduleClasses(m[2], imports).map((c) => c.name);
187
- if (!` ${cls} `.includes(` ${b.class} `) && !mods.includes(b.class))
250
+ const globs = b.class.includes(" .") && !b.class.includes(".module.") ? cls.split(/\s+/).filter(Boolean).map((t) => globalClassOf([t], styles())?.name) : [];
251
+ if (!` ${cls} `.includes(` ${b.class} `) && !mods.includes(b.class) && !globs.includes(b.class))
188
252
  continue;
189
253
  const line = src.slice(0, m.index).split("\n").length;
190
254
  if (said.has(`${line} ${b.class}`))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.501",
3
+ "version": "0.16.502",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {