@tendrilapp/cli 0.1.7 → 0.1.9
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/tendril.js +113 -8
- package/package.json +1 -1
package/dist/tendril.js
CHANGED
|
@@ -1839,6 +1839,7 @@ var init_browser = __esm({
|
|
|
1839
1839
|
|
|
1840
1840
|
// packages/verify/src/runtime.ts
|
|
1841
1841
|
import { existsSync as existsSync4, mkdtempSync, symlinkSync } from "node:fs";
|
|
1842
|
+
import { createRequire } from "node:module";
|
|
1842
1843
|
import os from "node:os";
|
|
1843
1844
|
import path4 from "node:path";
|
|
1844
1845
|
import { fileURLToPath } from "node:url";
|
|
@@ -1870,6 +1871,21 @@ function newScratchDir(prefix) {
|
|
|
1870
1871
|
}
|
|
1871
1872
|
return dir;
|
|
1872
1873
|
}
|
|
1874
|
+
function reactPinPlugin() {
|
|
1875
|
+
const req = createRequire(path4.join(runtimePackageRoot(), "package.json"));
|
|
1876
|
+
return {
|
|
1877
|
+
name: "tendril-react-pin",
|
|
1878
|
+
setup(b) {
|
|
1879
|
+
b.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => {
|
|
1880
|
+
try {
|
|
1881
|
+
return { path: req.resolve(args.path) };
|
|
1882
|
+
} catch {
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1873
1889
|
var init_runtime = __esm({
|
|
1874
1890
|
"packages/verify/src/runtime.ts"() {
|
|
1875
1891
|
"use strict";
|
|
@@ -3578,6 +3594,7 @@ if (root && C) createRoot(root).render(createElement(C, cfg.props));
|
|
|
3578
3594
|
const bundle = await build3({
|
|
3579
3595
|
stdin: { contents: mountSrc, resolveDir: runtimePackageRoot(), loader: "tsx" },
|
|
3580
3596
|
nodePaths: [runtimeNodeModules()],
|
|
3597
|
+
plugins: [reactPinPlugin()],
|
|
3581
3598
|
bundle: true,
|
|
3582
3599
|
write: false,
|
|
3583
3600
|
format: "iife",
|
|
@@ -4170,6 +4187,7 @@ if (root && C) createRoot(root).render(createElement(C, cfg.props));
|
|
|
4170
4187
|
const bundle = await build4({
|
|
4171
4188
|
stdin: { contents: mountSrc, resolveDir: runtimePackageRoot(), loader: "tsx" },
|
|
4172
4189
|
nodePaths: [runtimeNodeModules()],
|
|
4190
|
+
plugins: [reactPinPlugin()],
|
|
4173
4191
|
bundle: true,
|
|
4174
4192
|
write: false,
|
|
4175
4193
|
format: "iife",
|
|
@@ -4351,7 +4369,7 @@ var init_prelude = __esm({
|
|
|
4351
4369
|
- Component root: -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale (recorded rasterization); font-synthesis: none (a missing font weight must fall back visibly, never fake-bold); color-scheme MATCHING YOUR RECORDING (light for a light capture, dark for a dark one) and direction: ltr \u2014 pin them, so the render cannot follow the viewer OS preference and drift from the capture it is graded against; isolation: isolate (own stacking context; overlay z-indexes never fight the host).
|
|
4352
4370
|
- Interactive controls (buttons, options): touch-action: manipulation and user-select: none. Text inputs stay selectable (never user-select: none on them).
|
|
4353
4371
|
- Scrollable popovers/menus: overscroll-behavior: contain.
|
|
4354
|
-
- Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default
|
|
4372
|
+
- Focus indicators bind to :focus-visible, never bare :focus. If the recording contains a focus pose, style the indicator from that recorded truth. If NO focus pose is recorded, do NOT invent ring colors/widths/offsets \u2014 an invented ring is unrecorded pixels; keep the browser's default indicator for keyboard focus and state the gap in your report. Text inputs match :focus-visible even on mouse click BY SPEC; the ONE permitted refinement is suppressing the indicator on a POSITIVELY OBSERVED pointer press, fail-safe toward showing it (programmatic focus, restored focus, and assistive tech all count as keyboard and keep the ring \u2014 WCAG 2.4.7 binds keyboard operation). Never suppress more broadly than an observed press, and never restyle what you kept.
|
|
4355
4373
|
- Every animation wrapped in @media (prefers-reduced-motion: no-preference) or disabled under reduce.`;
|
|
4356
4374
|
}
|
|
4357
4375
|
});
|
|
@@ -4466,7 +4484,7 @@ var init_parity = __esm({
|
|
|
4466
4484
|
});
|
|
4467
4485
|
|
|
4468
4486
|
// packages/verify/src/composition.ts
|
|
4469
|
-
import { createRequire } from "node:module";
|
|
4487
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
4470
4488
|
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
|
|
4471
4489
|
import path17 from "node:path";
|
|
4472
4490
|
import { build as build6 } from "esbuild";
|
|
@@ -4478,7 +4496,7 @@ function getFontFaces4() {
|
|
|
4478
4496
|
async function compileInstrumentedMount(task, bundleDir) {
|
|
4479
4497
|
const entryTsx = path17.join(bundleDir, task.entry);
|
|
4480
4498
|
if (!existsSync12(entryTsx)) return { error: `${task.entry} missing` };
|
|
4481
|
-
const requireFromVerify =
|
|
4499
|
+
const requireFromVerify = createRequire2(path17.join(VERIFY_PKG_DIR, "package.json"));
|
|
4482
4500
|
let realJsxPath;
|
|
4483
4501
|
try {
|
|
4484
4502
|
realJsxPath = requireFromVerify.resolve("react/jsx-runtime");
|
|
@@ -4519,7 +4537,10 @@ if (root && Main) createRoot(root).render(createElement(Main, cfg.props));
|
|
|
4519
4537
|
b.onResolve({ filter: new RegExp(`^${REAL_JSX_SPEC}$`) }, () => ({ path: realJsxPath }));
|
|
4520
4538
|
b.onLoad({ filter: /.*/, namespace: "tendril-shim" }, () => ({ contents: JSX_SHIM, resolveDir: runtimePackageRoot(), loader: "js" }));
|
|
4521
4539
|
}
|
|
4522
|
-
}
|
|
4540
|
+
},
|
|
4541
|
+
// AFTER the stamp shim: the shim must own react/jsx-runtime
|
|
4542
|
+
// interception; the pin covers every remaining react import.
|
|
4543
|
+
reactPinPlugin()
|
|
4523
4544
|
]
|
|
4524
4545
|
});
|
|
4525
4546
|
return bundle.outputFiles[0]?.text ?? "";
|
|
@@ -4775,6 +4796,7 @@ for (const id of ["first", "second"]) {
|
|
|
4775
4796
|
const bundle = await build7({
|
|
4776
4797
|
stdin: { contents: src, resolveDir: runtimePackageRoot(), loader: "tsx" },
|
|
4777
4798
|
nodePaths: [runtimeNodeModules()],
|
|
4799
|
+
plugins: [reactPinPlugin()],
|
|
4778
4800
|
bundle: true,
|
|
4779
4801
|
write: false,
|
|
4780
4802
|
format: "iife",
|
|
@@ -7229,6 +7251,14 @@ function authorComponentApi(opts) {
|
|
|
7229
7251
|
props.push({ name: propNameFor(key), kind: "union", values: ordered.map(kebab3), default: kebab3(def) });
|
|
7230
7252
|
}
|
|
7231
7253
|
}
|
|
7254
|
+
const slots = (opts.textSlots ?? []).map((slot) => {
|
|
7255
|
+
let name = slot.prop;
|
|
7256
|
+
if (name !== "children" && (props.some((pr) => pr.name === name) || RESERVED_PROPS.has(name.toLowerCase()))) name = `${name}Text`;
|
|
7257
|
+
let n = 2;
|
|
7258
|
+
while (props.some((pr) => pr.name === name)) name = `${slot.prop}Text${n++}`;
|
|
7259
|
+
return { ...slot, prop: name };
|
|
7260
|
+
});
|
|
7261
|
+
for (const slot of slots) props.push({ name: slot.prop, kind: "string", default: slot.default });
|
|
7232
7262
|
const configs = [];
|
|
7233
7263
|
const seenAssignments = /* @__PURE__ */ new Map();
|
|
7234
7264
|
const componentIdent = pascal(opts.component);
|
|
@@ -7250,6 +7280,10 @@ function authorComponentApi(opts) {
|
|
|
7250
7280
|
}
|
|
7251
7281
|
}
|
|
7252
7282
|
if (tokens.length > 0) assignment["data-tendril-state"] = tokens.join(" ");
|
|
7283
|
+
for (const slot of slots) {
|
|
7284
|
+
const v = slot.overrides[p.slug];
|
|
7285
|
+
if (v !== void 0) assignment[slot.prop] = v;
|
|
7286
|
+
}
|
|
7253
7287
|
const canonical = JSON.stringify(Object.fromEntries(Object.entries(assignment).sort(([a], [b]) => a.localeCompare(b))));
|
|
7254
7288
|
const clash = seenAssignments.get(canonical);
|
|
7255
7289
|
if (clash !== void 0) {
|
|
@@ -7268,7 +7302,7 @@ function authorComponentApi(opts) {
|
|
|
7268
7302
|
name: componentIdent,
|
|
7269
7303
|
props: props.map((pr) => ({
|
|
7270
7304
|
name: pr.name,
|
|
7271
|
-
type: pr.kind === "boolean" ? "boolean" : pr.values.map((v) => `"${v}"`).join(" | "),
|
|
7305
|
+
type: pr.kind === "boolean" ? "boolean" : pr.kind === "string" ? "string" : pr.values.map((v) => `"${v}"`).join(" | "),
|
|
7272
7306
|
required: false,
|
|
7273
7307
|
...pr.default !== void 0 ? { default: pr.default } : {}
|
|
7274
7308
|
})),
|
|
@@ -7276,7 +7310,7 @@ function authorComponentApi(opts) {
|
|
|
7276
7310
|
poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length }
|
|
7277
7311
|
};
|
|
7278
7312
|
const propLines = props.map(
|
|
7279
|
-
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
7313
|
+
(pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
|
|
7280
7314
|
);
|
|
7281
7315
|
const provided = opts.fonts ?? [];
|
|
7282
7316
|
const recorded = opts.recordedFonts ?? [];
|
|
@@ -7289,7 +7323,8 @@ ${propLines.join("\n")}
|
|
|
7289
7323
|
[key: string]: unknown; // MUST spread unknown props (incl. data-*) onto the root element
|
|
7290
7324
|
})
|
|
7291
7325
|
|
|
7292
|
-
|
|
7326
|
+
${slots.length > 0 ? `CONTENT PROPS: every string prop defaults to its RECORDED text \u2014 render the PROP, never a hardcoded literal. Configs pass per-pose recorded strings wherever the recording varies, and pixels enforce them: a hardcoded string fails those configs. Content presence (a heading that only exists in some poses) follows the AXIS props; the string prop only supplies the text.
|
|
7327
|
+
` : ""}Rules: plain CSS in styles.css (tokens.css optional, loaded first) \u2014 no Tailwind, no imports beyond react/react-dom, and NEVER import the stylesheet from the entry module: the harness injects tokens.css and styles.css itself, and an entry that imports CSS does not compile here (measured cost: one full round, every config 0). The component renders the RECORDED content as its defaults \u2014 reproduce it from the emissions. ${forcingCanon}${fontsLine}The host sizes nothing: the component is its natural recorded size.`;
|
|
7293
7328
|
const mappedTokens = /* @__PURE__ */ new Set([...forcedStates, ...props.filter((p) => p.kind === "boolean").map((p) => p.name)]);
|
|
7294
7329
|
const unmappedInteractionEvidence = interactionEvidence.filter((e) => {
|
|
7295
7330
|
const sel = e.endsWith(" (selection axis)");
|
|
@@ -7399,6 +7434,67 @@ function recordedFontNeeds(setDir) {
|
|
|
7399
7434
|
function recordedFontFamilies(setDir) {
|
|
7400
7435
|
return recordedFontNeeds(setDir).map((n) => n.family);
|
|
7401
7436
|
}
|
|
7437
|
+
function recordedTextSlots(setDir, repSlugs) {
|
|
7438
|
+
const perRep = [];
|
|
7439
|
+
for (const slug of repSlugs) {
|
|
7440
|
+
const f = path26.join(setDir, slug, "get_design_context.json");
|
|
7441
|
+
if (!existsSync20(f)) continue;
|
|
7442
|
+
const code = envelopeText(f);
|
|
7443
|
+
const texts = [];
|
|
7444
|
+
for (const m of code.matchAll(/>([^<>{}]+)</g)) {
|
|
7445
|
+
const t = decodeXmlEntities(m[1]).trim();
|
|
7446
|
+
if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
|
|
7447
|
+
texts.push(t);
|
|
7448
|
+
}
|
|
7449
|
+
perRep.push({ slug, texts });
|
|
7450
|
+
}
|
|
7451
|
+
if (perRep.length === 0) return [];
|
|
7452
|
+
const slots = [];
|
|
7453
|
+
for (const rep of perRep) {
|
|
7454
|
+
const used = /* @__PURE__ */ new Set();
|
|
7455
|
+
const unmatched = [];
|
|
7456
|
+
for (const t of rep.texts) {
|
|
7457
|
+
const i = slots.findIndex((sl, idx) => !used.has(idx) && [...sl.values.values()].includes(t));
|
|
7458
|
+
if (i >= 0) {
|
|
7459
|
+
used.add(i);
|
|
7460
|
+
slots[i].values.set(rep.slug, t);
|
|
7461
|
+
} else {
|
|
7462
|
+
unmatched.push(t);
|
|
7463
|
+
}
|
|
7464
|
+
}
|
|
7465
|
+
for (const t of unmatched) {
|
|
7466
|
+
const i = slots.findIndex((_, idx) => !used.has(idx) && !slots[idx].values.has(rep.slug));
|
|
7467
|
+
if (i >= 0) {
|
|
7468
|
+
used.add(i);
|
|
7469
|
+
slots[i].values.set(rep.slug, t);
|
|
7470
|
+
} else {
|
|
7471
|
+
const sl = { values: /* @__PURE__ */ new Map() };
|
|
7472
|
+
sl.values.set(rep.slug, t);
|
|
7473
|
+
slots.push(sl);
|
|
7474
|
+
used.add(slots.length - 1);
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
}
|
|
7478
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
7479
|
+
return slots.map((sl, idx) => {
|
|
7480
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7481
|
+
for (const v of sl.values.values()) counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
7482
|
+
const def = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
7483
|
+
let prop;
|
|
7484
|
+
if (slots.length === 1) {
|
|
7485
|
+
prop = "children";
|
|
7486
|
+
} else {
|
|
7487
|
+
const word = /[A-Za-z][A-Za-z0-9]*/.exec(def)?.[0]?.toLowerCase();
|
|
7488
|
+
prop = word !== void 0 && word.length >= 2 ? word : `text${idx + 1}`;
|
|
7489
|
+
let n = 2;
|
|
7490
|
+
while (usedNames.has(prop)) prop = `${prop}${n++}`;
|
|
7491
|
+
}
|
|
7492
|
+
usedNames.add(prop);
|
|
7493
|
+
const overrides = {};
|
|
7494
|
+
for (const [slug, v] of sl.values) if (v !== def) overrides[slug] = v;
|
|
7495
|
+
return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0 };
|
|
7496
|
+
});
|
|
7497
|
+
}
|
|
7402
7498
|
function authorTaskFromSet(setDir, opts = {}) {
|
|
7403
7499
|
if (opts.fonts === void 0) {
|
|
7404
7500
|
const resolved = resolvedFontFamilies().map((f) => f.family);
|
|
@@ -7427,15 +7523,24 @@ function authorTaskFromSet(setDir, opts = {}) {
|
|
|
7427
7523
|
const latticeNames = manifest.latticeNames ?? (existsSync20(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
|
|
7428
7524
|
const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
|
|
7429
7525
|
const recordedFonts = recordedFontFamilies(setDir);
|
|
7526
|
+
const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
|
|
7430
7527
|
const api = authorComponentApi({
|
|
7431
7528
|
component: manifest.component,
|
|
7432
7529
|
poses,
|
|
7433
7530
|
...latticeNames !== void 0 ? { latticeNames } : {},
|
|
7434
7531
|
...opts.fonts !== void 0 ? { fonts: opts.fonts } : {},
|
|
7435
7532
|
...recordedFonts.length > 0 ? { recordedFonts } : {},
|
|
7436
|
-
...Object.keys(defaults).length > 0 ? { defaults } : {}
|
|
7533
|
+
...Object.keys(defaults).length > 0 ? { defaults } : {},
|
|
7534
|
+
...textSlots.length > 0 ? { textSlots } : {}
|
|
7437
7535
|
});
|
|
7438
7536
|
const { behaviors, prelude, disclosures } = authorBehaviors(api);
|
|
7537
|
+
for (const slot of textSlots) {
|
|
7538
|
+
if (!slot.varies) {
|
|
7539
|
+
disclosures.push(
|
|
7540
|
+
`content prop from recorded text is UNEXERCISED: the recording shows one constant string (${JSON.stringify(slot.default)}) for it, so no config pixel-verifies that the prop reaches the render \u2014 wire it anyway; a sentinel check is future work`
|
|
7541
|
+
);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7439
7544
|
if (manifest.notRecorded !== void 0) disclosures.push(`not recorded (from the completeness manifest): ${manifest.notRecorded}`);
|
|
7440
7545
|
const task = {
|
|
7441
7546
|
set: setDir,
|