stylegallery 0.1.12 → 0.1.14

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.
Files changed (28) hide show
  1. package/DOMAINS.md +1 -1
  2. package/GOVERNANCE.md +1 -1
  3. package/README.md +64 -0
  4. package/consumer-reference/agent-native/v2/admission-policy.json +66 -1
  5. package/consumer-reference/agent-native/v2/material-registry.json +1 -1
  6. package/consumer-reference/agent-native/v2/schema/admission-policy.schema.json +2 -2
  7. package/design-engineering/evidence-patterns/autocomplete.md +83 -0
  8. package/design-engineering/evidence-patterns/bottom-navigation.md +78 -0
  9. package/design-engineering/evidence-patterns/breadcrumbs.md +78 -0
  10. package/design-engineering/evidence-patterns/dark-mode-contrast.md +83 -0
  11. package/design-engineering/evidence-patterns/data-tables.md +86 -0
  12. package/design-engineering/evidence-patterns/empty-states.md +80 -0
  13. package/design-engineering/evidence-patterns/error-recovery.md +84 -0
  14. package/design-engineering/evidence-patterns/faceted-filters.md +90 -0
  15. package/design-engineering/evidence-patterns/form-labels.md +82 -0
  16. package/design-engineering/evidence-patterns/index.md +69 -0
  17. package/design-engineering/evidence-patterns/inline-validation.md +94 -0
  18. package/design-engineering/evidence-patterns/input-masks.md +79 -0
  19. package/design-engineering/evidence-patterns/tabs-and-segmented-controls.md +81 -0
  20. package/design-engineering/index.md +17 -0
  21. package/design-engineering/loading-and-feedback.md +28 -1
  22. package/design-engineering/native-interaction-contracts.md +8 -0
  23. package/design-engineering/state-management/recipes/delete-undo.md +6 -0
  24. package/log.md +17 -0
  25. package/package.json +16 -1
  26. package/platform-guides/input-and-focus.md +10 -0
  27. package/scripts/stylegallery-cli.mjs +12 -0
  28. package/scripts/stylegallery-setup.mjs +216 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylegallery",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "A governed gallery of portable interface knowledge with knowledge CLIs, website compilation, and MCP servers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,6 +55,19 @@
55
55
  "design-engineering/component-contract.md",
56
56
  "design-engineering/consumer-migration-readiness.md",
57
57
  "design-engineering/decision-tree.md",
58
+ "design-engineering/evidence-patterns/autocomplete.md",
59
+ "design-engineering/evidence-patterns/bottom-navigation.md",
60
+ "design-engineering/evidence-patterns/breadcrumbs.md",
61
+ "design-engineering/evidence-patterns/dark-mode-contrast.md",
62
+ "design-engineering/evidence-patterns/data-tables.md",
63
+ "design-engineering/evidence-patterns/empty-states.md",
64
+ "design-engineering/evidence-patterns/error-recovery.md",
65
+ "design-engineering/evidence-patterns/faceted-filters.md",
66
+ "design-engineering/evidence-patterns/form-labels.md",
67
+ "design-engineering/evidence-patterns/index.md",
68
+ "design-engineering/evidence-patterns/inline-validation.md",
69
+ "design-engineering/evidence-patterns/input-masks.md",
70
+ "design-engineering/evidence-patterns/tabs-and-segmented-controls.md",
58
71
  "design-engineering/index.md",
59
72
  "design-engineering/interface-craft.md",
60
73
  "design-engineering/loading-and-feedback.md",
@@ -272,6 +285,7 @@
272
285
  "scripts/sg.mjs",
273
286
  "scripts/strict-json.mjs",
274
287
  "scripts/stylegallery-cli.mjs",
288
+ "scripts/stylegallery-setup.mjs",
275
289
  "showcase/README.md",
276
290
  "skills/stylegallery-search/SKILL.md"
277
291
  ],
@@ -324,6 +338,7 @@
324
338
  "sg:mcp:v1": "node scripts/sg-mcp.mjs",
325
339
  "stylegallery": "node scripts/sg-entry.mjs",
326
340
  "test:search": "node --test scripts/test-stylegallery-search.mjs",
341
+ "test:setup": "node --test scripts/test-stylegallery-setup.mjs",
327
342
  "eval:search": "node scripts/search-eval.mjs --gate"
328
343
  },
329
344
  "devDependencies": {
@@ -61,6 +61,16 @@ The product offers a drag handle and chooses a Move action with explicit destina
61
61
 
62
62
  This is a local proposal. It does not prescribe one keyboard shortcut or say that every list must be a composite ARIA widget.
63
63
 
64
+ ### Target And Gesture Numbers Without Conflation
65
+
66
+ Additional sources rechecked 2026-10-19: [Target Size Enhanced, SC 2.5.5](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html) and [Pointer Gestures, SC 2.5.1](https://www.w3.org/WAI/WCAG22/Understanding/pointer-gestures.html), alongside the Minimum target source above. These explain the 2024 WCAG 2.2 Recommendation; they are not target-size experiments.
67
+
68
+ - AA SC 2.5.8 uses 24 by 24 CSS pixels, with spacing, equivalent-control, inline, user-agent, and essential exceptions. Its spacing alternative uses a 24 CSS pixel diameter circle centered on each undersized target; it is not a blanket 24px gap rule.
69
+ - AAA SC 2.5.5 uses 44 by 44 CSS pixels, with its own exceptions. It is not the AA minimum and is not interchangeable with 44 native points.
70
+ - Level A SC 2.5.1 concerns multipoint/path-based gestures and requires a single-pointer alternative without a path-based gesture unless essential. AA SC 2.5.7 separately concerns dragging. Keyboard access answers neither pointer condition by itself.
71
+
72
+ Local application: measure actual hit regions of table sort controls, filter removals, and mobile navigation, including adjacent targets. Provide visible click/tap alternatives for custom gesture-only actions. Verify cancelled gestures and ensure fixed navigation never entirely hides focus. No physical-device or consumer execution is claimed.
73
+
64
74
  ## Opinionated Guidance
65
75
 
66
76
  Use shortcuts to accelerate visible actions. Scope listeners to the intended surface, and let text editing and composition retain their own keys. Give each cancellation action one clearly documented owner before allowing it to propagate outward.
@@ -37,6 +37,14 @@ Governed records:
37
37
  ops List operations.
38
38
  registrar | conservator Check claim records | measure Layout patterns in Chromium.
39
39
 
40
+ Connect your agents:
41
+ setup Install the search skill and register stylegallery-mcp for Claude Code and Codex.
42
+ --agent <claude|codex|all> Agents to configure (default: the ones detected on this machine).
43
+ --only <skill|mcp> Configure only the skill or only the MCP server.
44
+ --dry-run Print the planned changes without writing anything.
45
+ --uninstall Remove what setup installed.
46
+ --force Replace or remove a different existing entry with the same name.
47
+
40
48
  Site compilation: compile, timeline, transcribe, sale, gate, build, shots, probe, workflow (see stylegallery workflow).
41
49
 
42
50
  help, --help Show this help.
@@ -117,6 +125,10 @@ export async function runStylegalleryCli(argv, config = {}) {
117
125
  if (command === "version" || command === "--version" || command === "-v") {
118
126
  return { exitCode: 0, output: `${packageVersion()}\n`, report: { ok: true, operation: "version" } };
119
127
  }
128
+ if (command === "setup" && !rest.includes("--help")) {
129
+ const { runSetup } = await import("./stylegallery-setup.mjs");
130
+ return report(runSetup(rest));
131
+ }
120
132
  if (rest.includes("--help")) return { exitCode: 0, output: HELP_TEXT, report: { ok: true, operation: "help" } };
121
133
  if (command === "search") return runSearch(rest);
122
134
  if (command === "read") return runRead(rest);
@@ -0,0 +1,216 @@
1
+ // `stylegallery setup`: connect local coding agents to StyleGallery by copying
2
+ // the packaged search skill and registering the stylegallery-mcp server.
3
+ // Claude Code: ~/.claude/skills/<name>/SKILL.md and the user-scope "mcpServers"
4
+ // map in ~/.claude.json. Codex: ~/.agents/skills/<name>/SKILL.md and the
5
+ // [mcp_servers.<name>] table in $CODEX_HOME/config.toml (default ~/.codex).
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ const SKILL_NAME = "stylegallery-search";
13
+ const SERVER_NAME = "stylegallery";
14
+ export const MCP_COMMAND = "npx";
15
+ export const MCP_ARGS = ["-y", "--package", "stylegallery", "stylegallery-mcp"];
16
+ const AGENTS = ["claude", "codex"];
17
+ const PARTS = ["skill", "mcp"];
18
+
19
+ function onPath(binary, env) {
20
+ const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
21
+ return (env.PATH ?? "").split(path.delimiter).filter(Boolean)
22
+ .some((directory) => extensions.some((extension) => fs.existsSync(path.join(directory, binary + extension))));
23
+ }
24
+
25
+ function locations(home, env) {
26
+ const codexHome = env.CODEX_HOME || path.join(home, ".codex");
27
+ return {
28
+ claude: {
29
+ detect: [path.join(home, ".claude"), path.join(home, ".claude.json")],
30
+ skill: path.join(home, ".claude", "skills", SKILL_NAME, "SKILL.md"),
31
+ mcp: path.join(home, ".claude.json"),
32
+ },
33
+ codex: {
34
+ detect: [codexHome],
35
+ skill: path.join(home, ".agents", "skills", SKILL_NAME, "SKILL.md"),
36
+ mcp: path.join(codexHome, "config.toml"),
37
+ },
38
+ };
39
+ }
40
+
41
+ const readText = (file) => (fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null);
42
+
43
+ // --- Claude Code ~/.claude.json -------------------------------------------
44
+ const claudeEntry = () => ({ type: "stdio", command: MCP_COMMAND, args: [...MCP_ARGS], env: {} });
45
+ const sameClaudeEntry = (entry) => entry && entry.command === MCP_COMMAND
46
+ && JSON.stringify(entry.args) === JSON.stringify(MCP_ARGS)
47
+ && (entry.type === undefined || entry.type === "stdio")
48
+ && Object.keys(entry.env ?? {}).length === 0;
49
+
50
+ function planClaudeMcp(text, uninstall, force) {
51
+ let config = {};
52
+ if (text !== null && text.trim() !== "") {
53
+ try { config = JSON.parse(text); } catch { return { status: "error", message: "existing file is not valid JSON; left untouched" }; }
54
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return { status: "error", message: "existing file is not a JSON object; left untouched" };
55
+ }
56
+ const servers = config.mcpServers ?? {};
57
+ const current = servers[SERVER_NAME];
58
+ if (uninstall) {
59
+ if (current === undefined) return { status: "unchanged" };
60
+ if (!sameClaudeEntry(current) && !force) return { status: "conflict", message: `mcpServers.${SERVER_NAME} differs from the setup entry; rerun with --force to remove it` };
61
+ const { [SERVER_NAME]: _removed, ...others } = servers;
62
+ return { status: "removed", content: `${JSON.stringify({ ...config, mcpServers: others }, null, 2)}\n` };
63
+ }
64
+ if (sameClaudeEntry(current)) return { status: "unchanged" };
65
+ if (current !== undefined && !force) return { status: "conflict", message: `mcpServers.${SERVER_NAME} already exists with different settings; rerun with --force to replace it` };
66
+ return { status: current === undefined ? (text === null ? "created" : "updated") : "replaced", content: `${JSON.stringify({ ...config, mcpServers: { ...servers, [SERVER_NAME]: claudeEntry() } }, null, 2)}\n` };
67
+ }
68
+
69
+ // --- Codex config.toml ----------------------------------------------------
70
+ const tomlString = (value) => JSON.stringify(value);
71
+ const codexBlock = () => `[mcp_servers.${SERVER_NAME}]\ncommand = ${tomlString(MCP_COMMAND)}\nargs = [${MCP_ARGS.map(tomlString).join(", ")}]\n`;
72
+ const ownHeader = new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*(?:"${SERVER_NAME}"|${SERVER_NAME})\\s*(?:\\.[^\\]]*)?\\]\\s*(?:#.*)?$`);
73
+ const anyHeader = /^\s*\[/;
74
+
75
+ // Splits the file into the lines of our [mcp_servers.stylegallery] table (and
76
+ // its subtables) and every other line, so other content is preserved verbatim.
77
+ function splitCodex(text) {
78
+ const lines = text.split("\n");
79
+ const kept = [];
80
+ const own = [];
81
+ let inOwn = false;
82
+ let firstOwn = -1;
83
+ let inline = false;
84
+ let section = "";
85
+ for (const line of lines) {
86
+ if (anyHeader.test(line)) {
87
+ inOwn = ownHeader.test(line);
88
+ section = line.trim();
89
+ if (inOwn && firstOwn === -1) firstOwn = kept.length;
90
+ } else if (!inOwn && /^\[\s*mcp_servers\s*\]/.test(section) && new RegExp(`^\\s*"?${SERVER_NAME}"?\\s*=`).test(line)) inline = true;
91
+ else if (!inOwn && new RegExp(`^\\s*mcp_servers\\s*\\.\\s*"?${SERVER_NAME}"?\\s*[.=]`).test(line) && section === "") inline = true;
92
+ (inOwn ? own : kept).push(line);
93
+ }
94
+ return { kept, own, firstOwn, inline };
95
+ }
96
+
97
+ const normalizeToml = (lines) => lines.map((line) => line.replace(/\s+#.*$/, "").trim()).filter((line) => line && !line.startsWith("#")).join("\n");
98
+
99
+ function planCodexMcp(text, uninstall, force) {
100
+ const source = text ?? "";
101
+ const { kept, own, firstOwn, inline } = splitCodex(source);
102
+ if (inline) return { status: "conflict", message: `mcp_servers.${SERVER_NAME} is defined with dotted or inline keys; edit it by hand` };
103
+ const ours = normalizeToml(codexBlock().split("\n"));
104
+ const exists = own.length > 0;
105
+ const matches = exists && normalizeToml(own) === ours;
106
+ const join = (lines) => { const out = lines.join("\n").replace(/\n{3,}/g, "\n\n"); return out.trim() === "" ? "" : `${out.replace(/\n*$/, "")}\n`; };
107
+ if (uninstall) {
108
+ if (!exists) return { status: "unchanged" };
109
+ if (!matches && !force) return { status: "conflict", message: `[mcp_servers.${SERVER_NAME}] differs from the setup entry; rerun with --force to remove it` };
110
+ return { status: "removed", content: join(kept) };
111
+ }
112
+ if (matches) return { status: "unchanged" };
113
+ if (exists && !force) return { status: "conflict", message: `[mcp_servers.${SERVER_NAME}] already exists with different settings; rerun with --force to replace it` };
114
+ const block = codexBlock().replace(/\n$/, "").split("\n");
115
+ if (exists) {
116
+ const next = [...kept];
117
+ next.splice(firstOwn, 0, ...block, "");
118
+ return { status: "replaced", content: join(next) };
119
+ }
120
+ const prefix = join(kept);
121
+ return { status: text === null ? "created" : "updated", content: `${prefix}${prefix ? "\n" : ""}${codexBlock()}` };
122
+ }
123
+
124
+ // --- skills ---------------------------------------------------------------
125
+ function planSkill(target, skillText, uninstall, force) {
126
+ const current = readText(target);
127
+ if (uninstall) {
128
+ if (current === null) return { status: "unchanged" };
129
+ if (current !== skillText && !force) return { status: "conflict", message: "installed SKILL.md differs from the packaged skill; rerun with --force to remove it" };
130
+ return { status: "removed", remove: true };
131
+ }
132
+ if (current === skillText) return { status: "unchanged" };
133
+ if (current !== null && !force) return { status: "conflict", message: "a different SKILL.md is already installed; rerun with --force to replace it" };
134
+ return { status: current === null ? "created" : "replaced", content: skillText };
135
+ }
136
+
137
+ function backup(file, stamp) {
138
+ const target = `${file}.bak-stylegallery-${stamp}`;
139
+ fs.copyFileSync(file, target);
140
+ return target;
141
+ }
142
+
143
+ function apply(kind, file, plan, stamp) {
144
+ const result = {};
145
+ if (kind === "mcp" && fs.existsSync(file)) result.backup = backup(file, stamp);
146
+ if (plan.remove) {
147
+ fs.rmSync(file);
148
+ const directory = path.dirname(file);
149
+ if (fs.readdirSync(directory).length === 0) fs.rmdirSync(directory);
150
+ } else {
151
+ fs.mkdirSync(path.dirname(file), { recursive: true });
152
+ fs.writeFileSync(file, plan.content);
153
+ }
154
+ return result;
155
+ }
156
+
157
+ function parse(args) {
158
+ const options = { dryRun: false, force: false, uninstall: false, agent: null, only: null };
159
+ for (let index = 0; index < args.length; index += 1) {
160
+ const argument = args[index];
161
+ if (argument === "--dry-run") options.dryRun = true;
162
+ else if (argument === "--force") options.force = true;
163
+ else if (argument === "--uninstall") options.uninstall = true;
164
+ else if (argument === "--agent" || argument === "--only") {
165
+ const value = args[index + 1];
166
+ const allowed = argument === "--agent" ? [...AGENTS, "all"] : PARTS;
167
+ if (value === undefined || !allowed.includes(value)) return { error: ["argument_value_invalid", `${argument} must be one of ${allowed.join(", ")}`] };
168
+ options[argument.slice(2)] = value;
169
+ index += 1;
170
+ } else return { error: ["argument_unknown", `unknown option ${argument}; run stylegallery help`] };
171
+ }
172
+ return { options };
173
+ }
174
+
175
+ export function runSetup(args, { env = process.env, home = env.HOME || os.homedir(), now = new Date() } = {}) {
176
+ const parsed = parse(args);
177
+ if (parsed.error) return { ok: false, operation: "setup", failures: [{ code: parsed.error[0], message: parsed.error[1] }] };
178
+ const { options } = parsed;
179
+ const where = locations(home, env);
180
+ const detected = AGENTS.filter((agent) => where[agent].detect.some((file) => fs.existsSync(file)) || onPath(agent, env));
181
+ const agents = options.agent === null ? detected : options.agent === "all" ? AGENTS : [options.agent];
182
+ const parts = options.only === null ? PARTS : [options.only];
183
+ const skillText = fs.readFileSync(path.join(packageRoot, "skills", SKILL_NAME, "SKILL.md"), "utf8");
184
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
185
+ const changes = [];
186
+ const failures = [];
187
+ for (const agent of agents) {
188
+ for (const kind of parts) {
189
+ const file = where[agent][kind];
190
+ const plan = kind === "skill" ? planSkill(file, skillText, options.uninstall, options.force)
191
+ : agent === "claude" ? planClaudeMcp(readText(file), options.uninstall, options.force)
192
+ : planCodexMcp(readText(file), options.uninstall, options.force);
193
+ const change = { agent, kind, path: file, action: plan.status };
194
+ if (plan.message) change.message = plan.message;
195
+ if (plan.status === "conflict" || plan.status === "error") failures.push({ code: `setup_${plan.status}`, message: `${file}: ${plan.message}` });
196
+ else if (plan.status !== "unchanged" && !options.dryRun) Object.assign(change, apply(kind, file, plan, stamp));
197
+ changes.push(change);
198
+ }
199
+ }
200
+ const next = options.uninstall ? [] : [
201
+ ...(agents.includes("claude") && parts.includes("mcp") ? ["claude mcp list"] : []),
202
+ ...(agents.includes("codex") && parts.includes("mcp") ? ["codex mcp list"] : []),
203
+ ];
204
+ return {
205
+ ok: failures.length === 0,
206
+ operation: options.uninstall ? "setup.uninstall" : "setup",
207
+ dry_run: options.dryRun,
208
+ detected,
209
+ agents,
210
+ mcp: { name: SERVER_NAME, command: MCP_COMMAND, args: MCP_ARGS },
211
+ changes,
212
+ ...(agents.length === 0 ? { hint: "No Claude Code or Codex installation detected; pass --agent claude, codex, or all." } : {}),
213
+ ...(next.length && !options.dryRun && failures.length === 0 ? { next } : {}),
214
+ failures,
215
+ };
216
+ }