diff --git a/custom-elements.json b/custom-elements.json new file mode 100644 index 0000000..f3d21ad --- /dev/null +++ b/custom-elements.json @@ -0,0 +1,75 @@ +{ + "schemaVersion": "2.1.0", + "readme": "README.md", + "modules": [ + { + "kind": "javascript-module", + "path": "src/human-machine-swapper.js", + "declarations": [ + { + "kind": "class", + "customElement": true, + "tagName": "human-machine-swapper", + "name": "HumanMachineSwapper", + "description": "A Human / Machine pill. Machine shows the site's llms.txt, the plain-text reading written for language models, rendered in place over the page. Renders nothing at all when the site has no llms.txt to offer.", + "attributes": [ + { + "name": "data-position", + "description": "Which edge the pill is fixed to.", + "type": { "text": "'bottom-center' | 'top-center'" }, + "default": "'bottom-center'" + }, + { + "name": "data-llms", + "description": "The machine reading to link to. 'auto' reads , then probes /llms.txt and /llms-full.txt. When nothing answers, the component renders nothing and explains why in the console.", + "type": { "text": "'auto' | string" }, + "default": "'auto'" + }, + { + "name": "data-accent", + "description": "Accent colour for the current dot and hover state.", + "type": { "text": "string" }, + "default": "'#a8f326'" + }, + { + "name": "data-labels-human", + "description": "Overrides the Human label.", + "type": { "text": "string" }, + "default": "'Human'" + }, + { + "name": "data-labels-machine", + "description": "Overrides the Machine label.", + "type": { "text": "string" }, + "default": "'Machine'" + } + ], + "cssProperties": [ + { + "name": "--hms-top", + "description": "Distance from the top edge. Set this AND --hms-bottom together when overriding: a fixed box with both set and height auto stretches. Use when the site's own chrome occupies the default edge.", + "default": "calc(14px + env(safe-area-inset-top)) when data-position is top-center, otherwise auto" + }, + { + "name": "--hms-bottom", + "description": "Distance from the bottom edge. See --hms-top.", + "default": "calc(20px + env(safe-area-inset-bottom)) when data-position is bottom-center, otherwise auto" + } + ], + "members": [], + "events": [] + } + ], + "exports": [ + { + "kind": "custom-element-definition", + "name": "human-machine-swapper", + "declaration": { + "name": "HumanMachineSwapper", + "module": "src/human-machine-swapper.js" + } + } + ] + } + ] +} diff --git a/package.json b/package.json index 3934390..332c6f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "human-machine-swapper", - "version": "1.3.0", + "version": "1.4.0", "description": "A web component that adds a Human / Machine pill to any website. Machine shows the site's llms.txt, the plain-text reading written for language models, rendered in place. One file, about 6 KB gzipped, no dependencies and no build step.", "keywords": [ "llms.txt", @@ -41,6 +41,7 @@ "bin", "src", "agent", + "custom-elements.json", "llms.txt", "AGENTS.md", "README.md", @@ -59,5 +60,6 @@ }, "bin": { "human-machine-swapper": "bin/cli.mjs" - } + }, + "customElements": "custom-elements.json" } diff --git a/test/component.test.mjs b/test/component.test.mjs index df1195c..3191e18 100644 --- a/test/component.test.mjs +++ b/test/component.test.mjs @@ -82,3 +82,33 @@ test("the published tarball carries the files an agent reads", () => { assert.ok(PKG.files.includes(f), `${f} is missing from package.json files`); } }); + +/** + * The Custom Elements Manifest is what webcomponents.org indexes and what an IDE reads for + * autocomplete. A manifest that drifts from the component is worse than none: it documents + * attributes that do nothing, in the one place a tool trusts without checking. + */ +test("the custom elements manifest matches the component", () => { + const manifest = JSON.parse(readFileSync(join(ROOT, "custom-elements.json"), "utf8")); + const decl = manifest.modules[0].declarations[0]; + + assert.equal(decl.tagName, "human-machine-swapper"); + assert.equal(manifest.modules[0].path, PKG.main, "manifest path must match the entry point"); + + // Every attribute the manifest advertises must actually be read by the source. + for (const attr of decl.attributes) { + assert.ok( + SOURCE.includes(`"${attr.name}"`), + `manifest advertises ${attr.name}, which the component never reads` + ); + } + // And every custom property it documents must be used. + for (const prop of decl.cssProperties) { + assert.ok(SOURCE.includes(prop.name), `manifest documents ${prop.name}, unused in source`); + } +}); + +test("package.json points at the manifest and ships it", () => { + assert.equal(PKG.customElements, "custom-elements.json"); + assert.ok(PKG.files.includes("custom-elements.json")); +});