@monoes/monodesign 1.0.0

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 (159) hide show
  1. package/LICENSE +191 -0
  2. package/cli/bin/cli.js +83 -0
  3. package/cli/bin/commands/ignores.mjs +331 -0
  4. package/cli/engine/browser/injected/index.mjs +1938 -0
  5. package/cli/engine/cli/main.mjs +322 -0
  6. package/cli/engine/design-system.mjs +815 -0
  7. package/cli/engine/detect-antipatterns-browser.js +5246 -0
  8. package/cli/engine/detect-antipatterns.mjs +51 -0
  9. package/cli/engine/engines/browser/detect-url.mjs +278 -0
  10. package/cli/engine/engines/regex/detect-text.mjs +574 -0
  11. package/cli/engine/engines/static-html/css-cascade.mjs +1016 -0
  12. package/cli/engine/engines/static-html/detect-html.mjs +235 -0
  13. package/cli/engine/engines/visual/screenshot-contrast.mjs +190 -0
  14. package/cli/engine/findings.mjs +13 -0
  15. package/cli/engine/node/file-system.mjs +199 -0
  16. package/cli/engine/profile/profiler.mjs +167 -0
  17. package/cli/engine/registry/antipatterns.mjs +515 -0
  18. package/cli/engine/rules/checks.mjs +2704 -0
  19. package/cli/engine/shared/color.mjs +125 -0
  20. package/cli/engine/shared/constants.mjs +102 -0
  21. package/cli/engine/shared/fonts.mjs +31 -0
  22. package/cli/engine/shared/inline-ignores.mjs +149 -0
  23. package/cli/engine/shared/page.mjs +8 -0
  24. package/cli/lib/monodesign-config.mjs +641 -0
  25. package/package.json +49 -0
  26. package/skill/SKILL.src.md +277 -0
  27. package/skill/agents/monodesign-asset-producer.md +100 -0
  28. package/skill/agents/monodesign-manual-edit-applier.md +103 -0
  29. package/skill/commands/adapt.md +53 -0
  30. package/skill/commands/animate.md +65 -0
  31. package/skill/commands/audit.md +89 -0
  32. package/skill/commands/bolder.md +50 -0
  33. package/skill/commands/clarify.md +64 -0
  34. package/skill/commands/colorize.md +68 -0
  35. package/skill/commands/craft.md +51 -0
  36. package/skill/commands/critique.md +66 -0
  37. package/skill/commands/delight.md +47 -0
  38. package/skill/commands/distill.md +56 -0
  39. package/skill/commands/document.md +83 -0
  40. package/skill/commands/extract.md +74 -0
  41. package/skill/commands/harden.md +65 -0
  42. package/skill/commands/init.md +30 -0
  43. package/skill/commands/layout.md +36 -0
  44. package/skill/commands/live.md +34 -0
  45. package/skill/commands/onboard.md +50 -0
  46. package/skill/commands/optimize.md +64 -0
  47. package/skill/commands/overdrive.md +56 -0
  48. package/skill/commands/polish.md +68 -0
  49. package/skill/commands/quieter.md +57 -0
  50. package/skill/commands/shape.md +71 -0
  51. package/skill/commands/teach.md +18 -0
  52. package/skill/commands/typeset.md +59 -0
  53. package/skill/reference/adapt.md +312 -0
  54. package/skill/reference/adapt.native.md +58 -0
  55. package/skill/reference/android.md +40 -0
  56. package/skill/reference/animate.md +203 -0
  57. package/skill/reference/antipatterns-catalog.md +365 -0
  58. package/skill/reference/audit.md +135 -0
  59. package/skill/reference/audit.native.md +139 -0
  60. package/skill/reference/bolder.md +120 -0
  61. package/skill/reference/brand-workflow.md +180 -0
  62. package/skill/reference/brand.md +108 -0
  63. package/skill/reference/clarify.md +288 -0
  64. package/skill/reference/codex.md +105 -0
  65. package/skill/reference/cognitive-load.md +106 -0
  66. package/skill/reference/color-and-contrast.md +105 -0
  67. package/skill/reference/colorize.md +257 -0
  68. package/skill/reference/component-specs.md +260 -0
  69. package/skill/reference/component-states.md +274 -0
  70. package/skill/reference/component-system.md +358 -0
  71. package/skill/reference/copy-formulas.md +160 -0
  72. package/skill/reference/craft.md +123 -0
  73. package/skill/reference/critique.md +819 -0
  74. package/skill/reference/delight.md +302 -0
  75. package/skill/reference/design-principles.md +246 -0
  76. package/skill/reference/distill.md +111 -0
  77. package/skill/reference/document.md +429 -0
  78. package/skill/reference/extract.md +69 -0
  79. package/skill/reference/harden.md +347 -0
  80. package/skill/reference/heuristics-scoring.md +234 -0
  81. package/skill/reference/hooks.md +92 -0
  82. package/skill/reference/image-prompts.md +130 -0
  83. package/skill/reference/init.md +221 -0
  84. package/skill/reference/interaction-design.md +189 -0
  85. package/skill/reference/ios.md +45 -0
  86. package/skill/reference/layout.md +185 -0
  87. package/skill/reference/live.md +722 -0
  88. package/skill/reference/motion-design.md +109 -0
  89. package/skill/reference/onboard.md +234 -0
  90. package/skill/reference/optimize.md +258 -0
  91. package/skill/reference/overdrive.md +130 -0
  92. package/skill/reference/personas.md +179 -0
  93. package/skill/reference/polish.md +241 -0
  94. package/skill/reference/pre-delivery-checklist.md +108 -0
  95. package/skill/reference/product.md +60 -0
  96. package/skill/reference/quieter.md +99 -0
  97. package/skill/reference/responsive-design.md +114 -0
  98. package/skill/reference/shape.md +165 -0
  99. package/skill/reference/spatial-design.md +100 -0
  100. package/skill/reference/teach.md +10 -0
  101. package/skill/reference/token-architecture.md +222 -0
  102. package/skill/reference/typeset.md +301 -0
  103. package/skill/reference/typography.md +159 -0
  104. package/skill/reference/ux-research.md +143 -0
  105. package/skill/reference/ux-rules.md +211 -0
  106. package/skill/reference/ux-writing.md +107 -0
  107. package/skill/scripts/command-metadata.json +94 -0
  108. package/skill/scripts/context-signals.mjs +227 -0
  109. package/skill/scripts/context.mjs +1062 -0
  110. package/skill/scripts/critique-storage.mjs +243 -0
  111. package/skill/scripts/detect-csp.mjs +199 -0
  112. package/skill/scripts/detect.mjs +34 -0
  113. package/skill/scripts/hook-admin.mjs +662 -0
  114. package/skill/scripts/hook-before-edit.mjs +517 -0
  115. package/skill/scripts/hook-lib.mjs +1777 -0
  116. package/skill/scripts/hook.mjs +62 -0
  117. package/skill/scripts/lib/design-parser.mjs +843 -0
  118. package/skill/scripts/lib/is-generated.mjs +70 -0
  119. package/skill/scripts/lib/monodesign-paths.mjs +130 -0
  120. package/skill/scripts/lib/provider.mjs +5 -0
  121. package/skill/scripts/lib/target-args.mjs +43 -0
  122. package/skill/scripts/live/browser-script-parts.mjs +51 -0
  123. package/skill/scripts/live/completion.mjs +20 -0
  124. package/skill/scripts/live/event-validation.mjs +138 -0
  125. package/skill/scripts/live/insert-ui.mjs +459 -0
  126. package/skill/scripts/live/manual-apply.mjs +940 -0
  127. package/skill/scripts/live/manual-edit-routes.mjs +358 -0
  128. package/skill/scripts/live/manual-edits-buffer.mjs +153 -0
  129. package/skill/scripts/live/session-store.mjs +290 -0
  130. package/skill/scripts/live/svelte-component.mjs +827 -0
  131. package/skill/scripts/live/sveltekit-adapter.mjs +275 -0
  132. package/skill/scripts/live/ui-core.mjs +181 -0
  133. package/skill/scripts/live/vocabulary.mjs +37 -0
  134. package/skill/scripts/live-accept.mjs +813 -0
  135. package/skill/scripts/live-browser-dom.js +147 -0
  136. package/skill/scripts/live-browser-session.js +124 -0
  137. package/skill/scripts/live-browser.js +11241 -0
  138. package/skill/scripts/live-commit-manual-edits.mjs +1242 -0
  139. package/skill/scripts/live-complete.mjs +76 -0
  140. package/skill/scripts/live-copy-edit-agent.mjs +684 -0
  141. package/skill/scripts/live-discard-manual-edits.mjs +52 -0
  142. package/skill/scripts/live-inject.mjs +584 -0
  143. package/skill/scripts/live-insert.mjs +273 -0
  144. package/skill/scripts/live-manual-edit-evidence.mjs +364 -0
  145. package/skill/scripts/live-poll.mjs +385 -0
  146. package/skill/scripts/live-resume.mjs +95 -0
  147. package/skill/scripts/live-server.mjs +1138 -0
  148. package/skill/scripts/live-status.mjs +62 -0
  149. package/skill/scripts/live-target.mjs +31 -0
  150. package/skill/scripts/live-wrap.mjs +895 -0
  151. package/skill/scripts/live.mjs +298 -0
  152. package/skill/scripts/modern-screenshot.umd.js +15 -0
  153. package/skill/scripts/palette.mjs +634 -0
  154. package/skill/scripts/pin.mjs +222 -0
  155. package/src/antipatterns.ts +164 -0
  156. package/src/index.ts +13 -0
  157. package/src/rules.ts +72 -0
  158. package/src/tokens.css +116 -0
  159. package/src/tokens.ts +99 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
3
+ /**
4
+ * Pin/unpin sub-commands as standalone skill shortcuts.
5
+ *
6
+ * Usage:
7
+ * node <scripts_path>/pin.mjs pin <command>
8
+ * node <scripts_path>/pin.mjs unpin <command>
9
+ *
10
+ * `pin audit` creates a lightweight audit skill that redirects to Monodesign's audit workflow.
11
+ * `unpin audit` removes that shortcut.
12
+ *
13
+ * The script discovers harness directories (.claude/skills, .cursor/skills, etc.)
14
+ * in the project root and creates/removes the pin in all of them.
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
18
+ import { basename, join, resolve, dirname } from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+
23
+ // All known harness directories
24
+ const HARNESS_DIRS = [
25
+ '.claude', '.cursor', '.gemini', '.codex', '.agents',
26
+ '.trae', '.trae-cn', '.pi', '.opencode', '.kiro', '.rovodev',
27
+ ];
28
+
29
+ const CODEX_HARNESSES = new Set(['.codex', '.agents']);
30
+
31
+ // Valid sub-command names
32
+ const VALID_COMMANDS = [
33
+ 'craft', 'init', 'extract', 'document', 'shape',
34
+ 'critique', 'audit',
35
+ 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live',
36
+ 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive',
37
+ 'clarify', 'adapt', 'optimize',
38
+ ];
39
+
40
+ // Marker to identify pinned skills (so unpin doesn't delete user skills)
41
+ const PIN_MARKER = '<!-- monodesign-pinned-skill -->';
42
+
43
+ /**
44
+ * Walk up from startDir to find a project root.
45
+ */
46
+ function findProjectRoot(startDir = process.cwd()) {
47
+ let dir = resolve(startDir);
48
+ while (dir !== '/') {
49
+ if (
50
+ existsSync(join(dir, 'package.json')) ||
51
+ existsSync(join(dir, '.git')) ||
52
+ existsSync(join(dir, 'skills-lock.json'))
53
+ ) {
54
+ return dir;
55
+ }
56
+ const parent = resolve(dir, '..');
57
+ if (parent === dir) break;
58
+ dir = parent;
59
+ }
60
+ return resolve(startDir);
61
+ }
62
+
63
+ /**
64
+ * Find harness skill directories that have an monodesign skill installed.
65
+ */
66
+ function findHarnessDirs(projectRoot) {
67
+ const dirs = [];
68
+ for (const harness of HARNESS_DIRS) {
69
+ const skillsDir = join(projectRoot, harness, 'skills');
70
+ // Only pin in harness dirs that already have monodesign installed
71
+ const monodesignDir = join(skillsDir, 'monodesign');
72
+ if (existsSync(monodesignDir) || existsSync(join(skillsDir, 'i-monodesign'))) {
73
+ dirs.push(skillsDir);
74
+ }
75
+ }
76
+ return dirs;
77
+ }
78
+
79
+ /**
80
+ * Load command metadata (descriptions for pinned skills).
81
+ */
82
+ function loadCommandMetadata() {
83
+ const metadataPath = join(__dirname, 'command-metadata.json');
84
+ if (existsSync(metadataPath)) {
85
+ return JSON.parse(readFileSync(metadataPath, 'utf-8'));
86
+ }
87
+ return {};
88
+ }
89
+
90
+ /**
91
+ * Generate a pinned skill's SKILL.md content.
92
+ */
93
+ function commandPrefixForSkillsDir(skillsDir) {
94
+ return CODEX_HARNESSES.has(basename(dirname(skillsDir))) ? '$' : '/';
95
+ }
96
+
97
+ function generatePinnedSkill(command, metadata, commandPrefix) {
98
+ const desc = metadata[command]?.description || `Shortcut for ${commandPrefix}monodesign ${command}.`;
99
+ const hint = metadata[command]?.argumentHint || '[target]';
100
+
101
+ return `---
102
+ name: ${command}
103
+ description: "${desc}"
104
+ argument-hint: "${hint}"
105
+ user-invocable: true
106
+ ---
107
+
108
+ ${PIN_MARKER}
109
+
110
+ This is a pinned shortcut for \`${commandPrefix}monodesign ${command}\`.
111
+
112
+ Invoke ${commandPrefix}monodesign ${command}, passing along any arguments provided here, and follow its instructions.
113
+ `;
114
+ }
115
+
116
+ /**
117
+ * Pin a command: create shortcut skill in all harness dirs.
118
+ */
119
+ function pin(command, projectRoot) {
120
+ const metadata = loadCommandMetadata();
121
+ const harnessDirs = findHarnessDirs(projectRoot);
122
+
123
+ if (harnessDirs.length === 0) {
124
+ console.log('No harness directories with monodesign installed found.');
125
+ return false;
126
+ }
127
+
128
+ let created = 0;
129
+
130
+ for (const skillsDir of harnessDirs) {
131
+ const commandPrefix = commandPrefixForSkillsDir(skillsDir);
132
+ const content = generatePinnedSkill(command, metadata, commandPrefix);
133
+ // Check if skill already exists (and isn't a pin)
134
+ const skillDir = join(skillsDir, command);
135
+ if (existsSync(skillDir)) {
136
+ const existingMd = join(skillDir, 'SKILL.md');
137
+ if (existsSync(existingMd)) {
138
+ const existing = readFileSync(existingMd, 'utf-8');
139
+ if (!existing.includes(PIN_MARKER)) {
140
+ console.log(` SKIP: ${skillDir} (non-pinned skill already exists)`);
141
+ continue;
142
+ }
143
+ }
144
+ }
145
+
146
+ mkdirSync(skillDir, { recursive: true });
147
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
148
+ console.log(` + ${skillDir}`);
149
+ created++;
150
+ }
151
+
152
+ if (created > 0) {
153
+ console.log(`\nPinned '${command}' as a standalone shortcut in ${created} location(s).`);
154
+ console.log('Use the pinned command directly in each harness.');
155
+ }
156
+
157
+ return created > 0;
158
+ }
159
+
160
+ /**
161
+ * Unpin a command: remove shortcut skill from all harness dirs.
162
+ */
163
+ function unpin(command, projectRoot) {
164
+ const harnessDirs = findHarnessDirs(projectRoot);
165
+ let removed = 0;
166
+
167
+ for (const skillsDir of harnessDirs) {
168
+ const skillDir = join(skillsDir, command);
169
+ if (!existsSync(skillDir)) continue;
170
+
171
+ const skillMd = join(skillDir, 'SKILL.md');
172
+ if (!existsSync(skillMd)) continue;
173
+
174
+ // Safety: only remove if it's a pinned skill
175
+ const content = readFileSync(skillMd, 'utf-8');
176
+ if (!content.includes(PIN_MARKER)) {
177
+ console.log(` SKIP: ${skillDir} (not a pinned skill)`);
178
+ continue;
179
+ }
180
+
181
+ rmSync(skillDir, { recursive: true, force: true });
182
+ console.log(` - ${skillDir}`);
183
+ removed++;
184
+ }
185
+
186
+ if (removed > 0) {
187
+ console.log(`\nUnpinned '${command}' from ${removed} location(s).`);
188
+ console.log(`Use Monodesign's '${command}' workflow directly to access it.`);
189
+ } else {
190
+ console.log(`No pinned '${command}' shortcut found.`);
191
+ }
192
+
193
+ return removed > 0;
194
+ }
195
+
196
+ // --- CLI ---
197
+ const [,, action, command] = process.argv;
198
+
199
+ if (!action || !command) {
200
+ console.log('Usage: node pin.mjs <pin|unpin> <command>');
201
+ console.log(`\nAvailable commands: ${VALID_COMMANDS.join(', ')}`);
202
+ process.exit(1);
203
+ }
204
+
205
+ if (action !== 'pin' && action !== 'unpin') {
206
+ console.error(`Unknown action: ${action}. Use 'pin' or 'unpin'.`);
207
+ process.exit(1);
208
+ }
209
+
210
+ if (!VALID_COMMANDS.includes(command)) {
211
+ console.error(`Unknown command: ${command}`);
212
+ console.error(`Available commands: ${VALID_COMMANDS.join(', ')}`);
213
+ process.exit(1);
214
+ }
215
+
216
+ const root = findProjectRoot();
217
+
218
+ if (action === 'pin') {
219
+ pin(command, root);
220
+ } else {
221
+ unpin(command, root);
222
+ }
@@ -0,0 +1,164 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { extname } from 'node:path';
3
+
4
+ export type AntipatternCategory = 'slop' | 'quality' | 'performance' | 'accessibility';
5
+
6
+ export interface AntipatternFinding {
7
+ id: string;
8
+ category: AntipatternCategory;
9
+ name: string;
10
+ severity: 'error' | 'warning' | 'info';
11
+ message: string;
12
+ file?: string;
13
+ line?: number;
14
+ }
15
+
16
+ export interface DetectionResult {
17
+ findings: AntipatternFinding[];
18
+ fileCount: number;
19
+ durationMs: number;
20
+ }
21
+
22
+ // Shape of the bundled engine (cli/engine/detect-antipatterns.mjs).
23
+ interface DetectorEngine {
24
+ detectText: (content: string, filePath: string, options?: Record<string, unknown>) => RawFinding[];
25
+ detectHtml: (filePath: string, options?: Record<string, unknown>) => Promise<RawFinding[]>;
26
+ detectUrl: (url: string, options?: Record<string, unknown>) => Promise<RawFinding[]>;
27
+ walkDir: (dir: string) => string[];
28
+ ANTIPATTERNS: Array<{ id: string; category?: string; severity?: string }>;
29
+ }
30
+
31
+ interface RawFinding {
32
+ antipattern?: string;
33
+ id?: string;
34
+ name?: string;
35
+ description?: string;
36
+ severity?: string;
37
+ file?: string;
38
+ line?: number;
39
+ snippet?: string;
40
+ }
41
+
42
+ // Typed as plain string so tsc emits a true dynamic import instead of trying
43
+ // to type-resolve the untyped .mjs engine module.
44
+ const ENGINE_SPECIFIER: string = '../cli/engine/detect-antipatterns.mjs';
45
+
46
+ let enginePromise: Promise<DetectorEngine | null> | undefined;
47
+
48
+ async function loadEngine(): Promise<DetectorEngine | null> {
49
+ enginePromise ??= import(ENGINE_SPECIFIER).then(
50
+ (mod) => mod as unknown as DetectorEngine,
51
+ () => null,
52
+ );
53
+ return enginePromise;
54
+ }
55
+
56
+ /** True when the bundled detection engine can be loaded. */
57
+ export async function detectorAvailable(): Promise<boolean> {
58
+ return (await loadEngine()) !== null;
59
+ }
60
+
61
+ const HTML_EXTENSIONS = new Set(['.html', '.htm']);
62
+
63
+ function mapSeverity(severity: string | undefined): AntipatternFinding['severity'] {
64
+ if (severity === 'error') return 'error';
65
+ if (severity === 'advisory' || severity === 'info') return 'info';
66
+ return 'warning';
67
+ }
68
+
69
+ function toFinding(raw: RawFinding, categoryById: Map<string, string>): AntipatternFinding {
70
+ const id = String(raw.antipattern ?? raw.id ?? '');
71
+ return {
72
+ id,
73
+ category: (categoryById.get(id) ?? 'quality') as AntipatternCategory,
74
+ name: String(raw.name ?? id),
75
+ severity: mapSeverity(raw.severity),
76
+ message: String(raw.description ?? raw.snippet ?? ''),
77
+ file: raw.file ? String(raw.file) : undefined,
78
+ line: raw.line ? Number(raw.line) : undefined,
79
+ };
80
+ }
81
+
82
+ async function detectFile(engine: DetectorEngine, filePath: string): Promise<RawFinding[]> {
83
+ if (HTML_EXTENSIONS.has(extname(filePath).toLowerCase())) {
84
+ return engine.detectHtml(filePath);
85
+ }
86
+ return engine.detectText(readFileSync(filePath, 'utf-8'), filePath);
87
+ }
88
+
89
+ /**
90
+ * Run antipattern detection on a path (file, directory, or URL) using the
91
+ * bundled detection engine (cli/engine/detect-antipatterns.mjs). URL targets
92
+ * require the optional puppeteer dependency.
93
+ */
94
+ export async function detectAntipatterns(target: string): Promise<DetectionResult> {
95
+ const engine = await loadEngine();
96
+ if (!engine) {
97
+ throw new Error(
98
+ 'The monodesign detection engine could not be loaded. ' +
99
+ 'Reinstall @monoes/monodesign (its cli/engine/ directory is missing or its dependencies are not installed).',
100
+ );
101
+ }
102
+
103
+ const start = Date.now();
104
+ const categoryById = new Map(
105
+ engine.ANTIPATTERNS.map((ap) => [ap.id, ap.category ?? 'quality']),
106
+ );
107
+
108
+ let raw: RawFinding[] = [];
109
+ let fileCount = 0;
110
+
111
+ if (/^https?:\/\//i.test(target)) {
112
+ raw = await engine.detectUrl(target);
113
+ fileCount = 1;
114
+ } else if (existsSync(target) && statSync(target).isDirectory()) {
115
+ const files = engine.walkDir(target);
116
+ fileCount = files.length;
117
+ for (const file of files) {
118
+ raw.push(...(await detectFile(engine, file)));
119
+ }
120
+ } else if (existsSync(target)) {
121
+ raw = await detectFile(engine, target);
122
+ fileCount = 1;
123
+ } else {
124
+ throw new Error(`Target not found: ${target}`);
125
+ }
126
+
127
+ return {
128
+ findings: raw.map((f) => toFinding(f, categoryById)),
129
+ fileCount,
130
+ durationMs: Date.now() - start,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Format findings as a human-readable string for display in agent output.
136
+ */
137
+ export function formatFindings(result: DetectionResult): string {
138
+ if (result.findings.length === 0) {
139
+ return `No antipatterns detected in ${result.fileCount} file(s). (${result.durationMs}ms)`;
140
+ }
141
+
142
+ const byCategory = result.findings.reduce<Record<string, AntipatternFinding[]>>((acc, f) => {
143
+ (acc[f.category] ??= []).push(f);
144
+ return acc;
145
+ }, {});
146
+
147
+ const lines: string[] = [
148
+ `${result.findings.length} finding(s) in ${result.fileCount} file(s) — ${result.durationMs}ms`,
149
+ '',
150
+ ];
151
+
152
+ for (const [cat, items] of Object.entries(byCategory)) {
153
+ lines.push(`[${cat.toUpperCase()}]`);
154
+ for (const f of items) {
155
+ const loc = f.file ? ` ${f.file}${f.line ? `:${f.line}` : ''}` : '';
156
+ lines.push(` ${f.severity === 'error' ? '✗' : '⚠'} ${f.name}`);
157
+ lines.push(` ${f.message}`);
158
+ if (loc) lines.push(loc);
159
+ }
160
+ lines.push('');
161
+ }
162
+
163
+ return lines.join('\n');
164
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * monodesign — frontend design intelligence for AI agents
3
+ *
4
+ * Provides:
5
+ * - Design tokens (OKLCH color system, spacing, motion, shadows)
6
+ * - Antipattern detection (bundled engine at cli/engine/)
7
+ * - Design rule constants
8
+ * - Skill invocation helpers
9
+ */
10
+
11
+ export * from './tokens.js';
12
+ export * from './antipatterns.js';
13
+ export * from './rules.js';
package/src/rules.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared design rules — the invariants every monodesign generation must satisfy.
3
+ * These mirror the "Absolute bans" and "Shared design laws" in the skill.
4
+ */
5
+
6
+ export const ABSOLUTE_BANS = [
7
+ {
8
+ id: 'side-stripe',
9
+ rule: 'No side-stripe borders',
10
+ description:
11
+ 'border-left or border-right > 1px as a colored accent on cards/list items/callouts is never intentional. Rewrite with full borders, background tints, leading numbers/icons, or nothing.',
12
+ },
13
+ {
14
+ id: 'gradient-text',
15
+ rule: 'No gradient text',
16
+ description:
17
+ 'background-clip:text with a gradient is decorative and never meaningful. Use a single solid color; emphasis via weight or size.',
18
+ },
19
+ {
20
+ id: 'glassmorphism-default',
21
+ rule: 'No glassmorphism as default',
22
+ description:
23
+ 'Blurs and glass cards used decoratively. Rare and purposeful, or nothing.',
24
+ },
25
+ {
26
+ id: 'hero-metric-template',
27
+ rule: 'No hero-metric template',
28
+ description:
29
+ 'Big number + small label + supporting stats + gradient accent is a SaaS cliché. Find a distinctive composition instead.',
30
+ },
31
+ {
32
+ id: 'identical-card-grid',
33
+ rule: 'No identical card grids',
34
+ description:
35
+ 'Same-sized cards with icon + heading + text, repeated endlessly, signals a lack of hierarchy. Vary size, weight, or content structure.',
36
+ },
37
+ {
38
+ id: 'modal-first',
39
+ rule: 'Modal is not the first thought',
40
+ description:
41
+ 'Modals are usually laziness. Exhaust inline / progressive disclosure alternatives first.',
42
+ },
43
+ ] as const;
44
+
45
+ export const COLOR_STRATEGIES = ['restrained', 'committed', 'full-palette', 'drenched'] as const;
46
+ export type ColorStrategy = (typeof COLOR_STRATEGIES)[number];
47
+
48
+ export const SHADOW_MAX_ALPHA = 0.15;
49
+
50
+ /** Validates a shadow string does not exceed the max alpha. */
51
+ export function shadowAlphaComplies(shadow: string): boolean {
52
+ const matches = shadow.match(/rgba?\([^)]+\)/g) ?? [];
53
+ for (const m of matches) {
54
+ const parts = m.replace(/rgba?\(/, '').replace(')', '').split(',').map(Number);
55
+ const alpha = parts.length === 4 ? parts[3] : 1;
56
+ if (alpha > SHADOW_MAX_ALPHA) return false;
57
+ }
58
+ return true;
59
+ }
60
+
61
+ export type Register = 'brand' | 'product';
62
+
63
+ /** Infer register from surface description heuristic. */
64
+ export function inferRegister(surface: string): Register {
65
+ const lower = surface.toLowerCase();
66
+ const brandKeywords = ['landing', 'marketing', 'campaign', 'homepage', 'portfolio', 'blog'];
67
+ const productKeywords = ['dashboard', 'admin', 'app', 'settings', 'onboard', 'console'];
68
+
69
+ if (brandKeywords.some((k) => lower.includes(k))) return 'brand';
70
+ if (productKeywords.some((k) => lower.includes(k))) return 'product';
71
+ return 'product'; // safe default
72
+ }
package/src/tokens.css ADDED
@@ -0,0 +1,116 @@
1
+ /* monodesign — design token system
2
+ * OKLCH color space, 8px grid, expo easing
3
+ * Apache 2.0 — pattern adapted from impeccable (Apache-2.0), rebranded for monomind
4
+ */
5
+
6
+ :root {
7
+ /* ─── Typography ─────────────────────────────── */
8
+ --font-display: 'Cormorant Garamond', Georgia, serif;
9
+ --font-body: 'Instrument Sans', system-ui, sans-serif;
10
+ --font-mono: 'Space Grotesk', monospace;
11
+ --font-size-xs: 0.75rem; /* 12px */
12
+ --font-size-sm: 0.875rem; /* 14px */
13
+ --font-size-base: 1rem; /* 16px */
14
+ --font-size-lg: 1.125rem; /* 18px */
15
+ --font-size-xl: 1.25rem; /* 20px */
16
+ --font-size-2xl: 1.5rem; /* 24px */
17
+ --font-size-3xl: 1.875rem; /* 30px */
18
+ --font-size-4xl: 2.25rem; /* 36px */
19
+ --font-size-5xl: 3rem; /* 48px */
20
+ --font-size-6xl: 3.75rem; /* 60px */
21
+ --font-size-7xl: 4.5rem; /* 72px */
22
+ --line-height-tight: 1.2;
23
+ --line-height-snug: 1.375;
24
+ --line-height-base: 1.5;
25
+ --line-height-relaxed: 1.625;
26
+ --line-height-loose: 2;
27
+
28
+ /* ─── Spacing (8px base grid) ────────────────── */
29
+ --space-1: 4px;
30
+ --space-2: 8px; /* xs */
31
+ --space-3: 12px;
32
+ --space-4: 16px; /* sm */
33
+ --space-5: 20px;
34
+ --space-6: 24px; /* md */
35
+ --space-7: 28px;
36
+ --space-8: 32px; /* lg */
37
+ --space-10: 40px;
38
+ --space-12: 48px; /* xl */
39
+ --space-16: 64px;
40
+ --space-20: 80px; /* 2xl */
41
+ --space-24: 96px;
42
+ --space-30: 120px; /* 3xl */
43
+
44
+ /* ─── Motion ─────────────────────────────────── */
45
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* expo-out — primary */
46
+ --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* sharper variant */
47
+ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
48
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
49
+ --duration-instant: 0s;
50
+ --duration-fast: 0.15s; /* color / opacity */
51
+ --duration-base: 0.3s; /* default transforms */
52
+ --duration-slow: 0.6s; /* orchestrated entrances */
53
+ --duration-slower: 0.8s;
54
+ --duration-slowest: 1.2s;
55
+
56
+ /* ─── Colors (OKLCH) ─────────────────────────── */
57
+ /* Neutrals */
58
+ --color-ink: oklch(10% 0 0); /* primary text */
59
+ --color-charcoal: oklch(25% 0 0); /* secondary text */
60
+ --color-ash: oklch(55% 0 0); /* tertiary / meta */
61
+ --color-mist: oklch(92% 0 0); /* hairline borders */
62
+ --color-cream: oklch(96% 0.005 350); /* warm page background */
63
+ --color-paper: oklch(98% 0 0); /* card / elevated surfaces */
64
+ --color-white: oklch(99.5% 0 0); /* near-white (never pure) */
65
+
66
+ /* Accent — Editorial Magenta (use ≤10% of screen in restrained palette) */
67
+ --color-accent: oklch(60% 0.25 350);
68
+ --color-accent-hover: oklch(52% 0.25 350);
69
+ --color-accent-dim: oklch(60% 0.25 350 / 0.15);
70
+ --color-accent-soft: oklch(60% 0.25 350 / 0.25);
71
+ --color-accent-text: oklch(45% 0.22 350); /* darker for body-on-white */
72
+
73
+ /* Tonal ramp — Editorial Magenta */
74
+ --color-accent-1: oklch(22% 0.15 350);
75
+ --color-accent-2: oklch(33% 0.20 350);
76
+ --color-accent-3: oklch(42% 0.23 350);
77
+ --color-accent-4: oklch(52% 0.25 350);
78
+ --color-accent-5: oklch(60% 0.25 350); /* base */
79
+ --color-accent-6: oklch(68% 0.22 350);
80
+ --color-accent-7: oklch(76% 0.18 350);
81
+ --color-accent-8: oklch(84% 0.12 350);
82
+ --color-accent-9: oklch(94% 0.05 350);
83
+
84
+ /* Command category tints (product register) */
85
+ --cat-build-bg: #fdf2f8; --cat-build-border: #ec4899;
86
+ --cat-evaluate-bg: #fdf4ff; --cat-evaluate-border: #d946ef;
87
+ --cat-refine-bg: #eff6ff; --cat-refine-border: #3b82f6;
88
+ --cat-enhance-bg: #fffbeb; --cat-enhance-border: #f59e0b;
89
+ --cat-fix-bg: #f0fdf4; --cat-fix-border: #22c55e;
90
+ --cat-iterate-bg: #f5f5f4; --cat-iterate-border: #78716c;
91
+
92
+ /* ─── Shadows ─────────────────────────────────── */
93
+ /* Max 0.15 alpha. No heavy drop shadows. */
94
+ --shadow-none: none;
95
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
96
+ --shadow-md: 0 4px 24px -4px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.06);
97
+ --shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.08);
98
+ --shadow-accent: 0 20px 60px oklch(60% 0.25 350 / 0.15);
99
+
100
+ /* ─── Border radius ──────────────────────────── */
101
+ --radius-none: 0;
102
+ --radius-sm: 4px;
103
+ --radius-md: 8px;
104
+ --radius-lg: 12px;
105
+ --radius-xl: 16px;
106
+ --radius-full: 9999px;
107
+
108
+ /* ─── Z-index ─────────────────────────────────── */
109
+ --z-below: -1;
110
+ --z-base: 0;
111
+ --z-raised: 1;
112
+ --z-overlay: 10;
113
+ --z-modal: 20;
114
+ --z-toast: 30;
115
+ --z-top: 40;
116
+ }
package/src/tokens.ts ADDED
@@ -0,0 +1,99 @@
1
+ /** Design tokens as typed JS constants — mirrors tokens.css */
2
+
3
+ export const fontFamily = {
4
+ display: "'Cormorant Garamond', Georgia, serif",
5
+ body: "'Instrument Sans', system-ui, sans-serif",
6
+ mono: "'Space Grotesk', monospace",
7
+ } as const;
8
+
9
+ export const fontSize = {
10
+ xs: '0.75rem',
11
+ sm: '0.875rem',
12
+ base: '1rem',
13
+ lg: '1.125rem',
14
+ xl: '1.25rem',
15
+ '2xl': '1.5rem',
16
+ '3xl': '1.875rem',
17
+ '4xl': '2.25rem',
18
+ '5xl': '3rem',
19
+ '6xl': '3.75rem',
20
+ '7xl': '4.5rem',
21
+ } as const;
22
+
23
+ export const spacing = {
24
+ 1: '4px',
25
+ 2: '8px',
26
+ 3: '12px',
27
+ 4: '16px',
28
+ 5: '20px',
29
+ 6: '24px',
30
+ 7: '28px',
31
+ 8: '32px',
32
+ 10: '40px',
33
+ 12: '48px',
34
+ 16: '64px',
35
+ 20: '80px',
36
+ 24: '96px',
37
+ 30: '120px',
38
+ } as const;
39
+
40
+ export const easing = {
41
+ out: 'cubic-bezier(0.16, 1, 0.3, 1)',
42
+ outQuint: 'cubic-bezier(0.22, 1, 0.36, 1)',
43
+ inOut: 'cubic-bezier(0.65, 0, 0.35, 1)',
44
+ in: 'cubic-bezier(0.4, 0, 1, 1)',
45
+ } as const;
46
+
47
+ export const duration = {
48
+ instant: '0s',
49
+ fast: '0.15s',
50
+ base: '0.3s',
51
+ slow: '0.6s',
52
+ slower: '0.8s',
53
+ slowest: '1.2s',
54
+ } as const;
55
+
56
+ /** OKLCH color palette */
57
+ export const color = {
58
+ ink: 'oklch(10% 0 0)',
59
+ charcoal: 'oklch(25% 0 0)',
60
+ ash: 'oklch(55% 0 0)',
61
+ mist: 'oklch(92% 0 0)',
62
+ cream: 'oklch(96% 0.005 350)',
63
+ paper: 'oklch(98% 0 0)',
64
+ white: 'oklch(99.5% 0 0)',
65
+ accent: 'oklch(60% 0.25 350)',
66
+ accentHover: 'oklch(52% 0.25 350)',
67
+ accentDim: 'oklch(60% 0.25 350 / 0.15)',
68
+ accentSoft: 'oklch(60% 0.25 350 / 0.25)',
69
+ accentText: 'oklch(45% 0.22 350)',
70
+ } as const;
71
+
72
+ export const accentScale = {
73
+ 1: 'oklch(22% 0.15 350)',
74
+ 2: 'oklch(33% 0.20 350)',
75
+ 3: 'oklch(42% 0.23 350)',
76
+ 4: 'oklch(52% 0.25 350)',
77
+ 5: 'oklch(60% 0.25 350)',
78
+ 6: 'oklch(68% 0.22 350)',
79
+ 7: 'oklch(76% 0.18 350)',
80
+ 8: 'oklch(84% 0.12 350)',
81
+ 9: 'oklch(94% 0.05 350)',
82
+ } as const;
83
+
84
+ export const shadow = {
85
+ none: 'none',
86
+ sm: '0 1px 3px rgba(0, 0, 0, 0.06)',
87
+ md: '0 4px 24px -4px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.06)',
88
+ lg: '0 20px 40px rgba(0, 0, 0, 0.08)',
89
+ accent: '0 20px 60px oklch(60% 0.25 350 / 0.15)',
90
+ } as const;
91
+
92
+ export const radius = {
93
+ none: '0',
94
+ sm: '4px',
95
+ md: '8px',
96
+ lg: '12px',
97
+ xl: '16px',
98
+ full: '9999px',
99
+ } as const;