soureeui 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.
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ const fs = require('fs')
5
+ const os = require('os')
6
+ const path = require('path')
7
+ const readline = require('readline')
8
+
9
+ const PKG = require('../package.json')
10
+ const ROOT = path.resolve(__dirname, '..')
11
+ const PAYLOAD = ['AGENTS.md', 'SKILL.md', 'references', 'templates']
12
+ const BODY_DIR = '.soureeui'
13
+ const MARK_BEGIN = '<!-- soureeui:begin -->'
14
+ const MARK_END = '<!-- soureeui:end -->'
15
+
16
+ const POINTER = (bodyPath) => `${MARK_BEGIN}
17
+ ## UI / UX work
18
+
19
+ Before any task that touches UI, UX, layout, visual design, styling, components,
20
+ responsive behavior, or a redesign, read \`${bodyPath}\` and follow it. It defines
21
+ the design process, the anti-generic-UI rules, and the reference files to load on demand.
22
+ ${MARK_END}`
23
+
24
+ const CURSOR_RULE = `---
25
+ description: UI/UX design process for any interface, layout, styling, component, or redesign work
26
+ globs:
27
+ alwaysApply: false
28
+ ---
29
+
30
+ Read \`${BODY_DIR}/AGENTS.md\` and follow it for any task touching UI, UX, layout,
31
+ visual design, styling, components, responsive behavior, or a redesign. It defines
32
+ the design process, the anti-generic-UI rules, and reference files to load on demand.
33
+ `
34
+
35
+ // agent registry -------------------------------------------------------------
36
+
37
+ const claudeSkillDir = (ctx) => ctx.global
38
+ ? path.join(os.homedir(), '.claude', 'skills', 'soureeui')
39
+ : path.join(ctx.dir, '.claude', 'skills', 'soureeui')
40
+
41
+ const AGENTS = {
42
+ claude: {
43
+ label: 'Claude Code',
44
+ detect: ['.claude', 'CLAUDE.md'],
45
+ install: (ctx) => {
46
+ ctx.copyBody(claudeSkillDir(ctx), { skill: true })
47
+ ctx.note('loads automatically on UI work, or run /soureeui')
48
+ },
49
+ paths: (ctx) => [claudeSkillDir(ctx)],
50
+ },
51
+ codex: {
52
+ label: 'Codex',
53
+ detect: ['AGENTS.md', '.codex'],
54
+ install: (ctx) => {
55
+ ctx.body()
56
+ ctx.pointer(path.join(ctx.dir, 'AGENTS.md'))
57
+ },
58
+ paths: (ctx) => [path.join(ctx.dir, BODY_DIR), path.join(ctx.dir, 'AGENTS.md')],
59
+ },
60
+ antigravity: {
61
+ label: 'Antigravity',
62
+ detect: ['.agents', '.agent'],
63
+ install: (ctx) => {
64
+ ctx.body()
65
+ ctx.pointer(ctx.global
66
+ ? path.join(os.homedir(), '.gemini', 'GEMINI.md')
67
+ : path.join(ctx.dir, 'AGENTS.md'))
68
+ ctx.write(path.join(ctx.dir, '.agents', 'rules', 'soureeui.md'),
69
+ POINTER(`${BODY_DIR}/AGENTS.md`) + '\n')
70
+ },
71
+ paths: (ctx) => [
72
+ path.join(ctx.dir, BODY_DIR),
73
+ path.join(ctx.dir, '.agents', 'rules', 'soureeui.md'),
74
+ path.join(ctx.dir, 'AGENTS.md'),
75
+ ],
76
+ },
77
+ cursor: {
78
+ label: 'Cursor',
79
+ detect: ['.cursor'],
80
+ install: (ctx) => {
81
+ ctx.body()
82
+ ctx.write(path.join(ctx.dir, '.cursor', 'rules', 'soureeui.mdc'), CURSOR_RULE)
83
+ },
84
+ paths: (ctx) => [path.join(ctx.dir, BODY_DIR), path.join(ctx.dir, '.cursor', 'rules', 'soureeui.mdc')],
85
+ },
86
+ windsurf: {
87
+ label: 'Windsurf',
88
+ detect: ['.windsurf'],
89
+ install: (ctx) => {
90
+ ctx.body()
91
+ ctx.write(path.join(ctx.dir, '.windsurf', 'rules', 'soureeui.md'),
92
+ POINTER(`${BODY_DIR}/AGENTS.md`) + '\n')
93
+ },
94
+ paths: (ctx) => [path.join(ctx.dir, BODY_DIR), path.join(ctx.dir, '.windsurf', 'rules', 'soureeui.md')],
95
+ },
96
+ gemini: {
97
+ label: 'Gemini CLI',
98
+ detect: ['GEMINI.md', '.gemini'],
99
+ install: (ctx) => {
100
+ ctx.body()
101
+ ctx.pointer(ctx.global
102
+ ? path.join(os.homedir(), '.gemini', 'GEMINI.md')
103
+ : path.join(ctx.dir, 'GEMINI.md'))
104
+ },
105
+ paths: (ctx) => [path.join(ctx.dir, BODY_DIR), path.join(ctx.dir, 'GEMINI.md')],
106
+ },
107
+ copilot: {
108
+ label: 'GitHub Copilot',
109
+ detect: [path.join('.github', 'copilot-instructions.md')],
110
+ install: (ctx) => {
111
+ ctx.body()
112
+ ctx.pointer(path.join(ctx.dir, '.github', 'copilot-instructions.md'))
113
+ },
114
+ paths: (ctx) => [path.join(ctx.dir, BODY_DIR), path.join(ctx.dir, '.github', 'copilot-instructions.md')],
115
+ },
116
+ generic: {
117
+ label: 'Any other agent',
118
+ detect: [],
119
+ install: (ctx) => {
120
+ ctx.body()
121
+ ctx.note(`point your agent at ${BODY_DIR}/AGENTS.md`)
122
+ },
123
+ paths: (ctx) => [path.join(ctx.dir, BODY_DIR)],
124
+ },
125
+ }
126
+
127
+ const NAMES = Object.keys(AGENTS)
128
+
129
+ // output ---------------------------------------------------------------------
130
+
131
+ const ESC = String.fromCharCode(27)
132
+ const tty = process.stdout.isTTY && !process.env.NO_COLOR
133
+ const paint = (code, s) => (tty ? `${ESC}[${code}m${s}${ESC}[0m` : s)
134
+ const bold = (s) => paint(1, s)
135
+ const dim = (s) => paint(2, s)
136
+ const green = (s) => paint(32, s)
137
+ const yellow = (s) => paint(33, s)
138
+ const red = (s) => paint(31, s)
139
+
140
+ // piping into head/less closes stdout early; that is not an error
141
+ process.stdout.on('error', (err) => { if (err.code === 'EPIPE') process.exit(0) })
142
+
143
+ const log = (s = '') => process.stdout.write(s + '\n')
144
+ const fail = (msg) => { process.stderr.write(red('error: ') + msg + '\n'); process.exit(1) }
145
+
146
+ // args -----------------------------------------------------------------------
147
+
148
+ function parseArgs (argv) {
149
+ const out = { _: [], ai: [], dir: process.cwd(), global: false, force: false, dryRun: false, yes: false }
150
+ for (let i = 0; i < argv.length; i++) {
151
+ const a = argv[i]
152
+ const next = () => {
153
+ const v = argv[++i]
154
+ if (v === undefined || v.startsWith('-')) fail(`${a} needs a value`)
155
+ return v
156
+ }
157
+ const addAi = (v) => out.ai.push(...v.split(',').map((s) => s.trim()).filter(Boolean))
158
+ if (a === '--ai' || a === '-a') addAi(next())
159
+ else if (a.startsWith('--ai=')) addAi(a.slice(5))
160
+ else if (a === '--dir' || a === '-d') out.dir = path.resolve(next())
161
+ else if (a.startsWith('--dir=')) out.dir = path.resolve(a.slice(6))
162
+ else if (a === '--global' || a === '-g') out.global = true
163
+ else if (a === '--force' || a === '-f') out.force = true
164
+ else if (a === '--dry-run' || a === '-n') out.dryRun = true
165
+ else if (a === '--yes' || a === '-y') out.yes = true
166
+ else if (a === '--help' || a === '-h') out.help = true
167
+ else if (a === '--version' || a === '-v') out.version = true
168
+ else if (a.startsWith('-')) fail(`unknown flag: ${a}`)
169
+ else out._.push(a)
170
+ }
171
+ return out
172
+ }
173
+
174
+ // fs helpers -----------------------------------------------------------------
175
+
176
+ function copyDir (src, dest) {
177
+ fs.mkdirSync(dest, { recursive: true })
178
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
179
+ const from = path.join(src, entry.name)
180
+ const to = path.join(dest, entry.name)
181
+ if (entry.isDirectory()) copyDir(from, to)
182
+ else fs.copyFileSync(from, to)
183
+ }
184
+ }
185
+
186
+ function makeCtx (opts, actions) {
187
+ const ctx = {
188
+ dir: opts.dir,
189
+ global: opts.global,
190
+ rel: (p) => (p === opts.dir || p.startsWith(opts.dir + path.sep)
191
+ ? path.relative(opts.dir, p) || '.'
192
+ : p.replace(os.homedir(), '~')),
193
+ note: (msg) => actions.push({ kind: 'note', msg }),
194
+ write: (file, content) => actions.push({ kind: 'write', file, content }),
195
+ pointer: (file) => actions.push({ kind: 'pointer', file }),
196
+ copyBody: (dest, o = {}) => actions.push({ kind: 'body', dest, skill: !!o.skill }),
197
+ body: () => ctx.copyBody(path.join(opts.dir, BODY_DIR)),
198
+ }
199
+ return ctx
200
+ }
201
+
202
+ function applyActions (actions, opts, ctx) {
203
+ for (const a of actions) {
204
+ if (a.kind === 'note') {
205
+ log(' ' + dim(a.msg))
206
+ continue
207
+ }
208
+
209
+ if (a.kind === 'body') {
210
+ if (!opts.dryRun) {
211
+ fs.mkdirSync(a.dest, { recursive: true })
212
+ for (const item of PAYLOAD) {
213
+ const src = path.join(ROOT, item)
214
+ if (!fs.existsSync(src)) continue
215
+ const dest = path.join(a.dest, item)
216
+ if (fs.statSync(src).isDirectory()) {
217
+ fs.rmSync(dest, { recursive: true, force: true })
218
+ copyDir(src, dest)
219
+ } else {
220
+ fs.copyFileSync(src, dest)
221
+ }
222
+ }
223
+ fs.rmSync(path.join(a.dest, a.skill ? 'AGENTS.md' : 'SKILL.md'), { force: true })
224
+ }
225
+ log(' ' + green('+') + ' skill body ' + dim('->') + ' ' + ctx.rel(a.dest) + '/')
226
+ continue
227
+ }
228
+
229
+ if (a.kind === 'write') {
230
+ const exists = fs.existsSync(a.file)
231
+ if (exists && !opts.force && fs.readFileSync(a.file, 'utf8') === a.content) {
232
+ log(' ' + dim('=') + ' ' + ctx.rel(a.file) + ' ' + dim('already current'))
233
+ continue
234
+ }
235
+ if (!opts.dryRun) {
236
+ fs.mkdirSync(path.dirname(a.file), { recursive: true })
237
+ fs.writeFileSync(a.file, a.content)
238
+ }
239
+ log(' ' + green(exists ? '~' : '+') + ' ' + ctx.rel(a.file))
240
+ continue
241
+ }
242
+
243
+ if (a.kind === 'pointer') {
244
+ const bodyPath = opts.global
245
+ ? path.join(os.homedir(), BODY_DIR, 'AGENTS.md').replace(os.homedir(), '~')
246
+ : `${BODY_DIR}/AGENTS.md`
247
+ const block = POINTER(bodyPath)
248
+ const exists = fs.existsSync(a.file)
249
+ const current = exists ? fs.readFileSync(a.file, 'utf8') : ''
250
+
251
+ if (current.includes(MARK_BEGIN)) {
252
+ const start = current.indexOf(MARK_BEGIN)
253
+ const end = current.indexOf(MARK_END, start)
254
+ const next = current.slice(0, start) + block + current.slice(end + MARK_END.length)
255
+ if (next !== current && !opts.dryRun) fs.writeFileSync(a.file, next)
256
+ log(' ' + dim('=') + ' ' + ctx.rel(a.file) + ' ' +
257
+ dim(next === current ? 'already current' : 'pointer refreshed'))
258
+ continue
259
+ }
260
+
261
+ if (!opts.dryRun) {
262
+ fs.mkdirSync(path.dirname(a.file), { recursive: true })
263
+ const head = current ? current.replace(/\n*$/, '\n\n') : ''
264
+ fs.writeFileSync(a.file, head + block + '\n')
265
+ }
266
+ log(' ' + green(exists ? '~' : '+') + ' ' + ctx.rel(a.file) + ' ' +
267
+ dim(exists ? 'pointer appended' : 'created'))
268
+ }
269
+ }
270
+ }
271
+
272
+ // agent selection ------------------------------------------------------------
273
+
274
+ function detect (dir) {
275
+ const found = []
276
+ for (const name of NAMES) {
277
+ for (const marker of AGENTS[name].detect) {
278
+ if (fs.existsSync(path.join(dir, marker))) {
279
+ found.push(name)
280
+ break
281
+ }
282
+ }
283
+ }
284
+ return found
285
+ }
286
+
287
+ function ask (question) {
288
+ return new Promise((resolve) => {
289
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
290
+ rl.question(question, (answer) => {
291
+ rl.close()
292
+ resolve(answer.trim())
293
+ })
294
+ })
295
+ }
296
+
297
+ async function pickAgents (opts) {
298
+ if (opts.ai.length) {
299
+ if (opts.ai.includes('all')) return NAMES.filter((n) => n !== 'generic')
300
+ const unknown = opts.ai.filter((n) => !NAMES.includes(n))
301
+ if (unknown.length) fail(`unknown agent: ${unknown.join(', ')}\n known: ${NAMES.join(', ')}, all`)
302
+ return [...new Set(opts.ai)]
303
+ }
304
+
305
+ const found = detect(opts.dir)
306
+ const fallback = found.length ? found : ['claude', 'codex']
307
+
308
+ if (opts.yes || !process.stdin.isTTY) {
309
+ if (found.length) log(dim(`detected: ${found.join(', ')}`))
310
+ return fallback
311
+ }
312
+
313
+ log('')
314
+ log(bold('Which agents?') + dim(' numbers, names, or a for all'))
315
+ NAMES.forEach((n, i) => {
316
+ const hit = found.includes(n) ? green(' detected') : ''
317
+ log(` ${dim(String(i + 1) + ')')} ${n.padEnd(12)} ${dim(AGENTS[n].label)}${hit}`)
318
+ })
319
+ const answer = await ask(`\n${dim('default: ' + fallback.join(', '))}\n> `)
320
+ if (!answer) return fallback
321
+ if (/^(a|all)$/i.test(answer)) return NAMES.filter((n) => n !== 'generic')
322
+
323
+ const picked = answer.split(/[\s,]+/).filter(Boolean).map((token) => {
324
+ const i = Number(token)
325
+ if (Number.isInteger(i) && i >= 1 && i <= NAMES.length) return NAMES[i - 1]
326
+ if (NAMES.includes(token)) return token
327
+ return fail(`not an option: ${token}`)
328
+ })
329
+ return [...new Set(picked)]
330
+ }
331
+
332
+ // commands -------------------------------------------------------------------
333
+
334
+ async function cmdInit (opts) {
335
+ if (!fs.existsSync(opts.dir)) fail(`no such directory: ${opts.dir}`)
336
+ const agents = await pickAgents(opts)
337
+
338
+ log('')
339
+ log(`${bold('soureeui')} ${dim('v' + PKG.version)} ${opts.dir}${opts.dryRun ? yellow(' dry run') : ''}`)
340
+ log('')
341
+
342
+ for (const name of agents) {
343
+ log(bold(AGENTS[name].label))
344
+ const actions = []
345
+ const ctx = makeCtx(opts, actions)
346
+ AGENTS[name].install(ctx)
347
+ applyActions(actions, opts, ctx)
348
+ log('')
349
+ }
350
+
351
+ log(green('Done.') + dim(' Ask your agent to build or redesign a screen and it will run the design process.'))
352
+ log('')
353
+ }
354
+
355
+ function cmdList () {
356
+ log('')
357
+ log(bold('Supported agents'))
358
+ NAMES.forEach((n) => log(` ${n.padEnd(12)} ${dim(AGENTS[n].label)}`))
359
+ log('')
360
+ log(dim(' soureeui init --ai cursor'))
361
+ log(dim(' soureeui init --ai claude,codex'))
362
+ log(dim(' soureeui init --ai all'))
363
+ log('')
364
+ }
365
+
366
+ function installedPaths (name, ctx) {
367
+ return AGENTS[name].paths(ctx).filter((p) => {
368
+ if (!fs.existsSync(p)) return false
369
+ if (/\.(md|mdc)$/.test(p) && !p.includes('soureeui')) {
370
+ return fs.readFileSync(p, 'utf8').includes(MARK_BEGIN)
371
+ }
372
+ return true
373
+ })
374
+ }
375
+
376
+ function cmdDoctor (opts) {
377
+ log('')
378
+ log(`${bold('soureeui doctor')} ${dim(opts.dir)}`)
379
+ log('')
380
+ const ctx = makeCtx(opts, [])
381
+ let any = false
382
+ for (const name of NAMES) {
383
+ const present = installedPaths(name, ctx)
384
+ if (!present.length) continue
385
+ any = true
386
+ log(`${green('installed')} ${bold(AGENTS[name].label)}`)
387
+ present.forEach((p) => log(` ${dim(ctx.rel(p))}`))
388
+ }
389
+ if (!any) log(dim('nothing installed here. run: soureeui init'))
390
+ log('')
391
+ }
392
+
393
+ async function cmdRemove (opts) {
394
+ const agents = opts.ai.length ? (opts.ai.includes('all') ? NAMES : opts.ai) : NAMES
395
+ const ctx = makeCtx(opts, [])
396
+ const targets = [...new Set(agents.flatMap((n) => installedPaths(n, ctx)))]
397
+
398
+ if (!targets.length) {
399
+ log(dim('nothing to remove.'))
400
+ return
401
+ }
402
+
403
+ log('')
404
+ log(bold('Will remove or clean:'))
405
+ targets.forEach((p) => log(' ' + ctx.rel(p)))
406
+ log('')
407
+
408
+ if (!opts.yes && !opts.dryRun && process.stdin.isTTY) {
409
+ const answer = await ask('Proceed? (y/N) ')
410
+ if (!/^y(es)?$/i.test(answer)) {
411
+ log(dim('cancelled.'))
412
+ return
413
+ }
414
+ }
415
+
416
+ for (const p of targets) {
417
+ const isInstructionFile = /\.(md|mdc)$/.test(p) && !p.includes('soureeui')
418
+ if (isInstructionFile) {
419
+ const content = fs.readFileSync(p, 'utf8')
420
+ const start = content.indexOf(MARK_BEGIN)
421
+ const end = content.indexOf(MARK_END, start)
422
+ const cleaned = (content.slice(0, start) + content.slice(end + MARK_END.length)).trim()
423
+ if (!opts.dryRun) {
424
+ if (cleaned) fs.writeFileSync(p, cleaned + '\n')
425
+ else fs.rmSync(p)
426
+ }
427
+ log(' ' + green('-') + ' pointer removed from ' + ctx.rel(p))
428
+ } else {
429
+ if (!opts.dryRun) fs.rmSync(p, { recursive: true, force: true })
430
+ log(' ' + green('-') + ' ' + ctx.rel(p))
431
+ }
432
+ }
433
+ log('')
434
+ }
435
+
436
+ function help () {
437
+ log(`
438
+ ${bold('soureeui')} ${dim('- install the UI Architect design skill into your AI coding agent')}
439
+
440
+ ${bold('Usage')}
441
+ soureeui init [--ai <agents>] [options]
442
+ soureeui list
443
+ soureeui doctor
444
+ soureeui remove [--ai <agents>]
445
+
446
+ ${bold('Examples')}
447
+ ${dim('$')} soureeui init --ai cursor
448
+ ${dim('$')} soureeui init --ai claude,codex
449
+ ${dim('$')} soureeui init --ai all
450
+ ${dim('$')} soureeui init --ai claude --global
451
+ ${dim('$')} soureeui init ${dim('detects what the project uses, or asks')}
452
+
453
+ ${bold('Agents')}
454
+ ${NAMES.join(', ')}, all
455
+
456
+ ${bold('Options')}
457
+ -a, --ai <list> agents to install for, comma separated
458
+ -d, --dir <path> target project (default: current directory)
459
+ -g, --global install machine-wide, where the agent supports it
460
+ -f, --force overwrite existing rule files
461
+ -n, --dry-run show what would change, write nothing
462
+ -y, --yes no prompts
463
+ -h, --help this text
464
+ -v, --version version
465
+ `)
466
+ }
467
+
468
+ // main -----------------------------------------------------------------------
469
+
470
+ async function main () {
471
+ const opts = parseArgs(process.argv.slice(2))
472
+ if (opts.version) return log(PKG.version)
473
+ const cmd = opts._[0] || (opts.ai.length ? 'init' : null)
474
+ if (opts.help || !cmd) return help()
475
+
476
+ switch (cmd) {
477
+ case 'init':
478
+ case 'install':
479
+ case 'add': return cmdInit(opts)
480
+ case 'update': return cmdInit({ ...opts, force: true })
481
+ case 'list':
482
+ case 'agents': return cmdList()
483
+ case 'doctor':
484
+ case 'status': return cmdDoctor(opts)
485
+ case 'remove':
486
+ case 'uninstall': return cmdRemove(opts)
487
+ default: return fail(`unknown command: ${cmd}\n try: soureeui --help`)
488
+ }
489
+ }
490
+
491
+ main().catch((err) => fail(err && err.stack ? err.stack : String(err)))
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "soureeui",
3
+ "version": "1.0.0",
4
+ "description": "Install the UI Architect design skill into Claude Code, Codex, Antigravity, Cursor, Windsurf, Gemini CLI, or Copilot, so the agent designs before it codes instead of producing generic AI-looking UI.",
5
+ "bin": {
6
+ "soureeui": "bin/soureeui.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "references",
11
+ "templates",
12
+ "AGENTS.md",
13
+ "SKILL.md",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "keywords": [
20
+ "ui",
21
+ "ux",
22
+ "design",
23
+ "design-system",
24
+ "ai",
25
+ "agent",
26
+ "skill",
27
+ "claude-code",
28
+ "codex",
29
+ "antigravity",
30
+ "cursor",
31
+ "windsurf",
32
+ "copilot",
33
+ "agents-md"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/shree2698/pro-ui.git"
38
+ },
39
+ "homepage": "https://github.com/shree2698/pro-ui#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/shree2698/pro-ui/issues"
42
+ },
43
+ "license": "MIT",
44
+ "scripts": {
45
+ "test": "node bin/soureeui.js --help > /dev/null && node bin/soureeui.js list > /dev/null && echo ok"
46
+ }
47
+ }
@@ -0,0 +1,77 @@
1
+ # Accessibility
2
+
3
+ Part of the design, not a pass afterwards. Retrofitting is more expensive than building it correctly.
4
+
5
+ Target: WCAG 2.2 AA unless the project states otherwise.
6
+
7
+ ---
8
+
9
+ ## Structure
10
+
11
+ - semantic elements over styled generic containers: `button`, `a`, `nav`, `main`, `header`, `footer`, `section`, `ul`, `table`, `dialog`
12
+ - one `h1` per page; heading levels descend without gaps. Rank is semantic — size is styling
13
+ - landmarks so a screen reader can jump: main content, navigation, search, footer
14
+ - a skip link to main content when navigation precedes it
15
+ - lists are lists, tables are tables with real headers and scopes
16
+
17
+ ## Controls
18
+
19
+ - **a button does something, a link goes somewhere.** Never a clickable `div`
20
+ - icon-only controls carry an accessible name
21
+ - toggles, tabs, menus, comboboxes, and dialogs expose their state (pressed, selected, expanded, current)
22
+ - custom controls follow the established keyboard pattern for that role — if that is expensive, use a primitives library (`packages.md`)
23
+
24
+ ## Keyboard
25
+
26
+ - every interactive element is reachable and operable by keyboard
27
+ - tab order follows visual order
28
+ - **visible focus on every focusable element.** Never remove the focus ring without replacing it with something at least as visible
29
+ - focus-visible for keyboard users; do not show rings on mouse click if the project prefers that, but never suppress both
30
+ - modals trap focus, close on Escape, and return focus to the trigger
31
+ - no keyboard traps anywhere else
32
+
33
+ ## Contrast
34
+
35
+ | Content | Minimum |
36
+ |---|---|
37
+ | Body text | 4.5:1 |
38
+ | Large text (≥24px, or ≥19px bold) | 3:1 |
39
+ | Icons and meaningful graphics | 3:1 |
40
+ | Control boundaries, focus indicators | 3:1 against adjacent colors |
41
+
42
+ Check at token-definition time. Placeholder text, muted text on tinted surfaces, and text over images are the usual failures — text over an image needs a scrim or a guaranteed-safe zone.
43
+
44
+ **Color is never the only signal.** Pair it with text, icon, shape, or position — for status, validation, chart series, and required fields.
45
+
46
+ ## Forms
47
+
48
+ - every field has a persistent visible label. Placeholder is not a label
49
+ - errors are associated with the field, announced, and stated in text next to it
50
+ - required fields marked in text, not by color or an unexplained asterisk alone
51
+ - related controls grouped with a group label
52
+ - appropriate input types and autocomplete hints
53
+ - do not disable submit silently; explain what is missing
54
+
55
+ ## Content
56
+
57
+ - alt text describes purpose, not appearance. Decorative images get empty alt
58
+ - link text makes sense alone — "read the routing guide", not "click here"
59
+ - captions and transcripts for media
60
+ - dynamic updates announced through a polite live region; never hijack focus for a background update
61
+
62
+ ## Interaction
63
+
64
+ - touch targets ~44px minimum with spacing between adjacent targets
65
+ - do not rely on hover to reveal essential content or actions
66
+ - respect reduced motion, and provide a reduced alternative rather than removing meaning (`motion.md`)
67
+ - page stays usable at 200% zoom and at 320px effective width with no loss of content or function
68
+ - do not suppress zoom
69
+
70
+ ## Quick verification
71
+
72
+ 1. Unplug the mouse. Complete the primary task.
73
+ 2. Tab through. Is focus always visible and in a sensible order?
74
+ 3. Zoom to 200%. Does anything break or get clipped?
75
+ 4. Check contrast on text, icons, and focus rings.
76
+ 5. Read only the headings. Does the page make sense?
77
+ 6. Turn on reduced motion. Is everything still understandable?
@@ -0,0 +1,74 @@
1
+ # Anti-AI Patterns
2
+
3
+ The failure mode this skill exists to prevent: an interface that is competent, symmetrical, glossy, and indistinguishable from ten thousand others.
4
+
5
+ Read this before finalizing any visual design.
6
+
7
+ ---
8
+
9
+ ## The pattern list
10
+
11
+ Each is banned as a *default*. Each is allowed when there is a stated product reason.
12
+
13
+ | Pattern | Why it fails | Instead |
14
+ |---|---|---|
15
+ | Purple-to-blue gradient as brand | Belongs to no product; signals "generated" instantly | Derive color from the actual brand, product, or domain |
16
+ | Gradient hero with giant centered heading | Zero information, maximum height, no hierarchy | Lead with the actual value proposition and a real interface or artifact |
17
+ | Floating blobs / abstract shapes | Decoration standing in for content | Real imagery, real data, real product UI, or nothing |
18
+ | Glow and neon halos | Attention without meaning | Contrast and position carry emphasis |
19
+ | Glass everywhere | Layering with nothing to layer over; kills contrast | Glass only over actual imagery or depth |
20
+ | Everything rounded to the same large radius | Erases hierarchy; every element reads the same weight | A radius system with intent (`design-system.md`) |
21
+ | Every section is a card | Cards become the page instead of grouping content | Cards for genuinely discrete, repeated objects only |
22
+ | Repetitive 3-column feature grid | Three features, equal weight, no priority — a lie about the product | Rank features; give the important one more space |
23
+ | Stacked drop shadows | Fake depth with no light source | One elevation scale, used sparingly |
24
+ | "AI Powered" / "Next-Gen" badges | Says nothing; dates instantly | Say what it does |
25
+ | Fake "Trusted by" logo rows | Fabricated social proof | Real customers, or cut the section |
26
+ | Invented statistics ("10x faster", "99.9%") | Fabricated claims | Real numbers, or qualitative specifics |
27
+ | Sparkle icons as decoration | Meaningless glyph noise | Icons that label real actions |
28
+ | Entrance animation on every element | Delays content; motion sickness; nothing is emphasized | Motion where it explains a change |
29
+ | Constant floating / pulsing loops | Permanent distraction | Static, unless movement carries meaning |
30
+ | Generic SaaS section order transplanted onto an unrelated product | Structure unrelated to this product's story | Section order from the actual user journey |
31
+ | Whitespace without hierarchy | Airy but unreadable; nothing dominates | Space that groups and ranks |
32
+ | Identical icon-title-text triplets down the page | Templated rhythm | Vary composition where content differs |
33
+ | Emoji as UI icons | Inconsistent rendering, unprofessional, not accessible | Icon library (`imagery-and-icons.md`) |
34
+ | Lorem ipsum or "Transform your workflow" | Design built on nothing | Real product copy (`content-and-copy.md`) |
35
+ | Centered everything | No reading axis, no tension | Deliberate alignment; asymmetry where it helps |
36
+ | Dark hero, light body, dark footer for no reason | Banded structure copied from templates | Structure from content |
37
+
38
+ ---
39
+
40
+ ## The human-design test
41
+
42
+ Run every question. Any "yes" in the wrong direction is a fix, not a note.
43
+
44
+ 1. **Does it look like a template?** → Change composition, hierarchy, or type — not just color.
45
+ 2. **Could this belong to hundreds of unrelated products?** → Add product-specific decisions.
46
+ 3. **Are decorative effects louder than the content?** → Remove effects until content wins.
47
+ 4. **Are all the cards identical?** → Rank them, or stop using cards.
48
+ 5. **Is everything rounded?** → Rebuild the radius system.
49
+ 6. **Is it purple/blue with a gradient?** → Rebuild the palette from the product.
50
+ 7. **Does every element animate?** → Cut motion to what explains change.
51
+ 8. **Does every section have the same structure?** → Vary deliberately.
52
+ 9. **Would removing this element hurt?** → If no, remove it.
53
+ 10. **Can a stranger tell what the product does in five seconds?** → If no, the design is failing its first job.
54
+
55
+ ---
56
+
57
+ ## The originality floor
58
+
59
+ Every interface needs at least **two decisions no competitor's site would share**. Usually one structural and one expressive:
60
+
61
+ - a layout shaped by the actual workflow or data, not by a grid preset
62
+ - a color taken from the physical product, material, domain, or existing brand
63
+ - a type pairing with a reason from the sector's own visual history
64
+ - a signature component that only this product needs
65
+ - a density chosen for how these users actually work
66
+ - a real artifact — screenshot, chart, map, document, photograph — instead of decoration
67
+
68
+ Without those, the direction is a costume.
69
+
70
+ ---
71
+
72
+ ## Self-check before shipping
73
+
74
+ Ask: *if this screenshot appeared in a feed with no caption, would anyone guess which product it belongs to?* If not, go back to the originality floor.