@hegemonart/get-design-done 1.40.5 → 1.41.5
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +4 -0
- package/agents/design-auditor.md +5 -6
- package/bin/gdd-detect +20 -0
- package/package.json +5 -1
- package/reference/anti-patterns.md +26 -0
- package/scripts/lib/detect/cli.cjs +111 -0
- package/scripts/lib/detect/engine.cjs +83 -0
- package/scripts/lib/detect/rule-schema.json +31 -0
- package/scripts/lib/detect/rules/ban-01.cjs +33 -0
- package/scripts/lib/detect/rules/ban-02.cjs +33 -0
- package/scripts/lib/detect/rules/ban-03.cjs +33 -0
- package/scripts/lib/detect/rules/ban-05.cjs +33 -0
- package/scripts/lib/detect/rules/ban-06.cjs +33 -0
- package/scripts/lib/detect/rules/ban-07.cjs +33 -0
- package/scripts/lib/detect/rules/ban-08.cjs +33 -0
- package/scripts/lib/detect/rules/ban-09.cjs +33 -0
- package/scripts/lib/detect/rules/ban-11.cjs +33 -0
- package/scripts/lib/detect/rules/ban-12.cjs +33 -0
- package/scripts/lib/detect/rules/ban-13.cjs +33 -0
- package/scripts/lib/detect/rules/index.cjs +21 -0
- package/scripts/lib/manifest/README.md +46 -0
- package/scripts/lib/manifest/harnesses.cjs +3 -0
- package/scripts/lib/manifest/harnesses.json +91 -0
- package/scripts/lib/manifest/index.cjs +26 -0
- package/scripts/lib/manifest/loader.cjs +51 -0
- package/scripts/lib/manifest/prose-denylist.json +126 -0
- package/scripts/lib/manifest/schemas/harnesses.schema.json +38 -0
- package/scripts/lib/manifest/schemas/prose-denylist.schema.json +41 -0
- package/scripts/lib/manifest/schemas/skills.schema.json +33 -0
- package/scripts/lib/manifest/skills.json +255 -0
- package/skills/quality-gate/SKILL.md +1 -1
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.41.5"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "get-design-done",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
|
|
15
|
-
"version": "1.
|
|
15
|
+
"version": "1.41.5",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "hegemonart"
|
|
18
18
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get-design-done",
|
|
3
3
|
"short_name": "gdd",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.41.5",
|
|
5
5
|
"description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain. v1.27.7 ships gdd-mcp (Phase 27.7): 12 read-only MCP tools for sub-3s priming. v1.28.0 (Phase 28): Foundational References Tier 2 — 5 new reference files (color-theory, composition, proportion-systems, i18n, contrast-advanced), 2 verifier i18n probes + 1 explore i18n-readiness probe, 12 additive cross-link insertions across 10 existing references, 2 orthogonal audit-scoring lens-tags (composition_alignment + i18n_readiness).",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "hegemonart",
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,86 @@ All notable changes to get-design-done are documented here. Versions follow [sem
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.41.5] - 2026-06-02
|
|
8
|
+
|
|
9
|
+
### Phase 41.5 — SoT Manifest Consolidation
|
|
10
|
+
|
|
11
|
+
A 2026-05-16 audit found Phase 42/44/45/47 each scoping its own "single source of truth" in a different
|
|
12
|
+
corner — four formats, four schemas, four CI drift gates. 41.5 lands **one** root, **one** schema
|
|
13
|
+
directory, **one** validator, **before** those phases plan their work. **No new runtime dependency, no
|
|
14
|
+
new egress.**
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`scripts/lib/manifest/`** — the cross-phase SoT root:
|
|
19
|
+
- `loader.cjs` — the one shared reader; a missing/unparseable file returns an empty fallback + a
|
|
20
|
+
one-line warning (never throws), with a file-mtime cache. Dep-free.
|
|
21
|
+
- `index.cjs` — typed readers `readHarnesses()` / `readSkills()` / `readProseDenylist()`.
|
|
22
|
+
- `harnesses.json` (+ `.cjs` view) — the 14 canonical runtimes (Phase 42 adds build config, Phase 45
|
|
23
|
+
the capability matrix, as views of this one record). `skills.json` — the 83 live skill names (Phase
|
|
24
|
+
47 enriches). `prose-denylist.json` — the AI-tell denylist + em-dash/`--` markers (Phase 43/44 consume).
|
|
25
|
+
- `schemas/*.schema.json` — one JSON Schema per manifest. `README.md` — the contract + migration note.
|
|
26
|
+
- **`scripts/validate-manifest.cjs`** (`npm run validate:manifest`) — the single ajv CI gate that
|
|
27
|
+
replaces the four per-file drift gates 43/44/45/47 would each have shipped.
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
- **No new runtime dependency** — the shipped `loader.cjs`/`index.cjs` are dep-free; `validate-manifest.cjs`
|
|
32
|
+
uses the already-present `ajv` and is maintainer-only (not shipped, like `lint-changelog.cjs`).
|
|
33
|
+
- 6-manifest lockstep at **v1.41.5** + `OFF_CADENCE_VERSIONS.add('1.41.5')` + the 36 live-pinned
|
|
34
|
+
`manifests-version.txt` baselines forward-propagated 1.41.0 → 1.41.5.
|
|
35
|
+
- Inventory: tarball golden 747 → 757 (+10: `scripts/lib/manifest/**`). No skill/agent/registry deltas.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## [1.41.0] - 2026-06-02
|
|
40
|
+
|
|
41
|
+
### Phase 41 — Deterministic Anti-Pattern CLI (`gdd-detect`)
|
|
42
|
+
|
|
43
|
+
GDD's BAN-NN anti-pattern catalogue was consumable only by spawning a Claude session through
|
|
44
|
+
`design-auditor` — no fast CI gate, no pre-commit hook, no zero-LLM regression check. 41 ships
|
|
45
|
+
**`gdd-detect`**: a **dep-free, offline** Node CLI that scans HTML/CSS/JSX for the 11 statically-
|
|
46
|
+
detectable BAN rules and emits JSON or human findings, each linked to its catalogue paragraph.
|
|
47
|
+
(Inspired by `pbakaus/impeccable`.) **No new runtime dependency, no new egress.**
|
|
48
|
+
|
|
49
|
+
### Breaking changes
|
|
50
|
+
|
|
51
|
+
**None.** `gdd-detect` is purely additive — a new `bin` entry + a new `lint:design` script + new
|
|
52
|
+
`scripts/lib/detect/` modules. Nothing existing changes behavior; `design-auditor` swaps its inline
|
|
53
|
+
BAN greps for a `gdd-detect --json` call (same findings, deterministic). Upgrading from v1.40.5 is a no-op for existing workflows.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`bin/gdd-detect`** (`gdd-detect <path> [--json] [--fast] [--rule BAN-NN] [--puppeteer]`) — exit
|
|
58
|
+
`0` clean / `2` findings / `1` invocation error. Recursive scan of HTML/CSS/JSX/TSX.
|
|
59
|
+
- **`scripts/lib/detect/`** — a pure, dep-free engine + `rule-schema.json` + 11 per-rule matchers
|
|
60
|
+
(`rules/ban-NN.cjs`) ported verbatim from the catalogue's own `**Grep**` patterns
|
|
61
|
+
(BAN-01/02/03/05/06/07/08/09/11/12/13). BAN-04 + BAN-10 are subjective (matcher-exempt).
|
|
62
|
+
- **`scripts/sync-rule-catalogue.cjs`** — bidirectional parity gate: every rule ↔ a `### BAN-NN:`
|
|
63
|
+
heading + a `bdId: BAN-NN` marker; no orphans; no un-ported detectable rule.
|
|
64
|
+
- **`scripts/hooks/pre-commit-detect.sh`** — opt-in pre-commit scaffold.
|
|
65
|
+
- **`lint:design`** npm script + `sync:rule-catalogue` npm script.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- **`reference/anti-patterns.md`** — `bdId: BAN-NN` markers under every BAN heading.
|
|
70
|
+
- **`agents/design-auditor.md`** — Pillar-7 inline BAN greps replaced by one `gdd-detect --json` call.
|
|
71
|
+
- **`skills/quality-gate/SKILL.md`** — `lint:design` added to the Phase-25 auto-detect linter allowlist.
|
|
72
|
+
|
|
73
|
+
### Notes
|
|
74
|
+
|
|
75
|
+
- **Two execution paths**: regex-fast (the dep-free default) and DOM-aware (`jsdom`, a soft
|
|
76
|
+
`try-require` optional — **not** a `package.json` dependency); `--fast` forces regex; absent jsdom
|
|
77
|
+
falls back with a one-line warning. A `http(s)://` path needs `--puppeteer` (also soft-optional) and
|
|
78
|
+
prints a clear install message otherwise — never a stack trace. The CLI is **offline by default** (a
|
|
79
|
+
static network-isolation test fails the build on any network primitive in the detect tree).
|
|
80
|
+
- 6-manifest lockstep at **v1.41.0** + `OFF_CADENCE_VERSIONS.add('1.41.0')` (minor) + the 35 live-pinned
|
|
81
|
+
`manifests-version.txt` baselines forward-propagated 1.40.5 → 1.41.0.
|
|
82
|
+
- Inventory: tarball golden 731 → 747 (+16: `bin/gdd-detect` + `scripts/lib/detect/**`). No
|
|
83
|
+
skill/agent/registry deltas (`sync-rule-catalogue.cjs` + the pre-commit scaffold are maintainer-only).
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
7
87
|
## [1.40.5] - 2026-06-01
|
|
8
88
|
|
|
9
89
|
### Phase 40.5 — GDD CLI Localization
|
package/README.md
CHANGED
|
@@ -198,6 +198,10 @@ GDD's single-operator baseline now extends to **teams** — git-native + advisor
|
|
|
198
198
|
|
|
199
199
|
GDD's README is multilingual — now its **CLI** is too. [`/gdd:locale <code>`](skills/locale/SKILL.md) sets the language of `--help`, common error messages, and skill prompt headers, resolved by [`scripts/lib/i18n`](scripts/lib/i18n/index.cjs) (precedence: `config.locale` > env `LANG` > `en`; fallback chain `locale → base → en`). Flat-JSON message tables ship for **en** (complete source), **ru** (full), and **uk/de/fr/zh/ja** placeholders — a missing key always falls back to English, so a partial locale never breaks the CLI. Skills + agents can carry an opt-in `description_i18n` map. Adding a locale is a PR: translate the table, add a `NOTICE` credit (the contribution path is in [`reference/cli-localization.md`](reference/cli-localization.md)). Completeness is **warn-only**. **No new runtime dependency.**
|
|
200
200
|
|
|
201
|
+
### Deterministic anti-pattern CLI — `gdd-detect` (v1.41.0)
|
|
202
|
+
|
|
203
|
+
GDD's BAN-NN anti-pattern catalogue is now executable. [`gdd-detect`](bin/gdd-detect) is a **dep-free, offline** Node CLI that scans HTML/CSS/JSX for the 11 statically-detectable BAN rules ([`scripts/lib/detect/rules/`](scripts/lib/detect/)) — `transition: all`, `will-change: all`, gradient text, bounce easing, `scale(0)` entry, naked `outline: none`, pure-black dark mode, disabled zoom, tinted image outline — each finding linked to its [`reference/anti-patterns.md`](reference/anti-patterns.md) paragraph. `gdd-detect <path> [--json] [--fast] [--rule BAN-NN]` exits `0` clean / `2` findings / `1` error. The rule files are the canonical executable; the markdown stays canonical prose; [`sync-rule-catalogue`](scripts/sync-rule-catalogue.cjs) keeps them in parity. `npm run lint:design` is the CI gate; an opt-in pre-commit scaffold ships too. `design-auditor` and the Phase-25 quality-gate consume it; `jsdom`/`puppeteer` are soft-optional (regex-fast is the default). **No new runtime dependency.**
|
|
204
|
+
|
|
201
205
|
### Previous releases
|
|
202
206
|
|
|
203
207
|
- **v1.26.0** — Headless Model Resolver (per-runtime tier→model map, `resolved_models` router field, per-runtime price tables, `reasoning-class` runtime-neutral alias).
|
package/agents/design-auditor.md
CHANGED
|
@@ -277,12 +277,11 @@ grep -rEn "confirm\b|Confirm\b|areYouSure|destructive|danger" src/ --include="*.
|
|
|
277
277
|
Collect findings from the micro-polish sections of the mapper outputs (`.design/map/motion.md`, `.design/map/tokens.md`, `.design/map/visual-hierarchy.md`, `.design/map/a11y.md`). If those files are not yet available, run targeted grep passes:
|
|
278
278
|
|
|
279
279
|
```bash
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
grep -rEn "will-change:\s*all" src/ --include="*.css" --include="*.scss" 2>/dev/null | head -5
|
|
280
|
+
# BAN-NN anti-patterns — Phase 41: run the deterministic detector instead of hand-grepping each
|
|
281
|
+
# rule. One pass, --json, every BAN rule (transition:all, will-change:all, gradient text, bounce
|
|
282
|
+
# easing, scale(0), naked outline:none, pure-black dark mode, disabled zoom, tinted image outline),
|
|
283
|
+
# each finding linked to its reference/anti-patterns.md paragraph. Offline + zero-LLM.
|
|
284
|
+
node "${CLAUDE_PLUGIN_ROOT:-.}/bin/gdd-detect" src/ --json 2>/dev/null || true
|
|
286
285
|
|
|
287
286
|
# Missing AnimatePresence initial={false}
|
|
288
287
|
grep -rEn "<AnimatePresence" src/ --include="*.tsx" --include="*.jsx" 2>/dev/null | grep -v "initial={false}" | head -10
|
package/bin/gdd-detect
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/gdd-detect — Phase 41 deterministic anti-pattern CLI.
|
|
3
|
+
//
|
|
4
|
+
// Thin CJS entry: the engine + CLI are plain dep-free CommonJS under scripts/lib/detect/, so unlike
|
|
5
|
+
// bin/gdd-sdk this needs no compiled/strip-types dual mode — it just requires the CLI and forwards
|
|
6
|
+
// argv + the exit code (0 clean · 2 findings · 1 invocation error).
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
|
|
12
|
+
let cli;
|
|
13
|
+
try {
|
|
14
|
+
cli = require(path.resolve(__dirname, '..', 'scripts', 'lib', 'detect', 'cli.cjs'));
|
|
15
|
+
} catch (err) {
|
|
16
|
+
process.stderr.write('gdd-detect: failed to load the detector: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
process.exit(cli.main(process.argv.slice(2)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hegemonart/get-design-done",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.5",
|
|
4
4
|
"description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
|
|
5
5
|
"author": "Hegemon",
|
|
6
6
|
"homepage": "https://github.com/hegemonart/get-design-done",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"NOTICE"
|
|
34
34
|
],
|
|
35
35
|
"bin": {
|
|
36
|
+
"gdd-detect": "./bin/gdd-detect",
|
|
36
37
|
"gdd-events": "./scripts/cli/gdd-events.mjs",
|
|
37
38
|
"gdd-graph": "./bin/gdd-graph",
|
|
38
39
|
"gdd-mcp": "./bin/gdd-mcp",
|
|
@@ -57,6 +58,9 @@
|
|
|
57
58
|
"lint:links": "npx --yes lychee --no-progress --accept 200,206,403,429 \"**/*.md\" || true",
|
|
58
59
|
"lint:agentskills": "node scripts/lint-agentskills-spec.cjs",
|
|
59
60
|
"lint:changelog": "node scripts/lint-changelog.cjs",
|
|
61
|
+
"lint:design": "node bin/gdd-detect test/fixtures/detect/negative --json",
|
|
62
|
+
"sync:rule-catalogue": "node scripts/sync-rule-catalogue.cjs --check",
|
|
63
|
+
"validate:manifest": "node scripts/validate-manifest.cjs --check",
|
|
60
64
|
"validate:schemas": "node --experimental-strip-types scripts/validate-schemas.ts",
|
|
61
65
|
"validate:frontmatter": "node --experimental-strip-types scripts/validate-frontmatter.ts agents/",
|
|
62
66
|
"detect:stale-refs": "node scripts/detect-stale-refs.cjs",
|
|
@@ -18,6 +18,8 @@ Zero tolerance. Each violation = −3 points from Anti-Pattern score. Rewrite ra
|
|
|
18
18
|
|
|
19
19
|
### BAN-01: Side-Stripe Borders
|
|
20
20
|
|
|
21
|
+
bdId: BAN-01
|
|
22
|
+
|
|
21
23
|
```css
|
|
22
24
|
/* BANNED — AI-generated card tell, regardless of color */
|
|
23
25
|
border-left: 4px solid var(--color-primary);
|
|
@@ -32,6 +34,8 @@ border-right: 3px solid #6366f1;
|
|
|
32
34
|
|
|
33
35
|
### BAN-02: Gradient Text
|
|
34
36
|
|
|
37
|
+
bdId: BAN-02
|
|
38
|
+
|
|
35
39
|
```css
|
|
36
40
|
/* BANNED */
|
|
37
41
|
background: linear-gradient(to right, #6366f1, #8b5cf6);
|
|
@@ -48,6 +52,8 @@ background-clip: text;
|
|
|
48
52
|
|
|
49
53
|
### BAN-03: Bounce/Elastic Easing
|
|
50
54
|
|
|
55
|
+
bdId: BAN-03
|
|
56
|
+
|
|
51
57
|
```css
|
|
52
58
|
/* BANNED */
|
|
53
59
|
transition: all 300ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
@@ -63,6 +69,8 @@ animation-timing-function: spring(1, 80, 10, 0);
|
|
|
63
69
|
|
|
64
70
|
### BAN-04: Animating Keyboard Actions
|
|
65
71
|
|
|
72
|
+
bdId: BAN-04
|
|
73
|
+
|
|
66
74
|
No animation on: command palette open/close, keyboard shortcuts, tab switching, filter/sort toggles, navigation item expand/collapse.
|
|
67
75
|
|
|
68
76
|
**Why**: These repeat 100+ times per day. Every millisecond of animation accumulates into felt sluggishness.
|
|
@@ -71,6 +79,8 @@ No animation on: command palette open/close, keyboard shortcuts, tab switching,
|
|
|
71
79
|
|
|
72
80
|
### BAN-05: Pure Black Dark Mode
|
|
73
81
|
|
|
82
|
+
bdId: BAN-05
|
|
83
|
+
|
|
74
84
|
```css
|
|
75
85
|
/* BANNED */
|
|
76
86
|
background: #000000;
|
|
@@ -85,6 +95,8 @@ background: rgb(0, 0, 0);
|
|
|
85
95
|
|
|
86
96
|
### BAN-06: Disabling Zoom
|
|
87
97
|
|
|
98
|
+
bdId: BAN-06
|
|
99
|
+
|
|
88
100
|
```html
|
|
89
101
|
<!-- BANNED -->
|
|
90
102
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
@@ -97,6 +109,8 @@ background: rgb(0, 0, 0);
|
|
|
97
109
|
|
|
98
110
|
### BAN-07: Naked `outline: none`
|
|
99
111
|
|
|
112
|
+
bdId: BAN-07
|
|
113
|
+
|
|
100
114
|
```css
|
|
101
115
|
/* BANNED — unless a custom focus indicator replaces it */
|
|
102
116
|
:focus { outline: none; }
|
|
@@ -111,6 +125,8 @@ button:focus { outline: 0; }
|
|
|
111
125
|
|
|
112
126
|
### BAN-08: `transition: all`
|
|
113
127
|
|
|
128
|
+
bdId: BAN-08
|
|
129
|
+
|
|
114
130
|
```css
|
|
115
131
|
/* BANNED */
|
|
116
132
|
transition: all 300ms ease;
|
|
@@ -124,6 +140,8 @@ transition: all 300ms ease;
|
|
|
124
140
|
|
|
125
141
|
### BAN-09: `scale(0)` Animation Entry
|
|
126
142
|
|
|
143
|
+
bdId: BAN-09
|
|
144
|
+
|
|
127
145
|
```css
|
|
128
146
|
/* BANNED — nothing in the real world materializes from nothing */
|
|
129
147
|
@keyframes enter { from { transform: scale(0); } }
|
|
@@ -339,6 +357,8 @@ If YES to any → rewrite that element before proceeding.
|
|
|
339
357
|
|
|
340
358
|
### BAN-10: Same Border-Radius on Nested Surfaces
|
|
341
359
|
|
|
360
|
+
bdId: BAN-10
|
|
361
|
+
|
|
342
362
|
Applying the same `border-radius` to a container and an element inside it (when the element is separated by padding) makes the inner element appear to "float" — the radii should be concentric, not equal.
|
|
343
363
|
|
|
344
364
|
**Grep (Tailwind):**
|
|
@@ -355,6 +375,8 @@ Source: jakubkrehel/make-interfaces-feel-better (MIT)
|
|
|
355
375
|
|
|
356
376
|
### BAN-11: Tinted Image Outline
|
|
357
377
|
|
|
378
|
+
bdId: BAN-11
|
|
379
|
+
|
|
358
380
|
Using a colored outline on images (e.g., `outline-slate-200`, `outline-gray-300`, or a hex-value outline color) competes visually with the image content and creates color contamination.
|
|
359
381
|
|
|
360
382
|
**Grep (Tailwind):**
|
|
@@ -376,6 +398,8 @@ Source: jakubkrehel/make-interfaces-feel-better (MIT)
|
|
|
376
398
|
|
|
377
399
|
### BAN-12: `transition: all`
|
|
378
400
|
|
|
401
|
+
bdId: BAN-12
|
|
402
|
+
|
|
379
403
|
`transition: all` animates every animatable CSS property on the element, including layout-triggering properties (width, height, padding, margin). This causes layout recalculation on EVERY transition, creating jank and unexpected visual effects (e.g., a hover transition that also animates the element's size if any dimensions change).
|
|
380
404
|
|
|
381
405
|
**Grep (CSS):**
|
|
@@ -393,6 +417,8 @@ Source: jakubkrehel/make-interfaces-feel-better (MIT)
|
|
|
393
417
|
|
|
394
418
|
### BAN-13: `will-change: all`
|
|
395
419
|
|
|
420
|
+
bdId: BAN-13
|
|
421
|
+
|
|
396
422
|
`will-change: all` promotes every animatable property to its own GPU compositor layer, consuming GPU memory for each property. On complex components this can allocate hundreds of MB of texture memory per instance, causing performance degradation and potential crashes on mobile.
|
|
397
423
|
|
|
398
424
|
**Grep:**
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — gdd-detect CLI. Dep-free by default (regex-fast). The DOM-aware (jsdom) and URL
|
|
3
|
+
// (puppeteer) paths are SOFT optionals loaded via try-require — never a package.json dependency, so
|
|
4
|
+
// the SC#10 network-isolation scan stays clean and the plugin keeps its zero-runtime-dep guarantee.
|
|
5
|
+
//
|
|
6
|
+
// gdd-detect <path> [--json] [--fast] [--rule BAN-NN] [--puppeteer]
|
|
7
|
+
//
|
|
8
|
+
// Exit codes: 0 = clean · 2 = findings · 1 = invocation error.
|
|
9
|
+
|
|
10
|
+
const engine = require('./engine.cjs');
|
|
11
|
+
|
|
12
|
+
const HELP = `gdd-detect — scan HTML/CSS/JSX for GDD anti-patterns (BAN-NN).
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
gdd-detect <path> [options]
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
<path> A file or directory (scanned recursively), or a http(s):// URL (needs --puppeteer).
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--json Machine-readable JSON output.
|
|
22
|
+
--fast Regex-only; do not load jsdom even if present.
|
|
23
|
+
--rule <BAN-NN> Run a single rule (e.g. --rule BAN-08).
|
|
24
|
+
--puppeteer Allow scanning a URL via Puppeteer (an optional, separately-installed dependency).
|
|
25
|
+
-h, --help This help.
|
|
26
|
+
|
|
27
|
+
Exit codes: 0 clean · 2 findings · 1 invocation error.`;
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const opts = { path: null, json: false, fast: false, rule: null, puppeteer: false, help: false };
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a === '--json') opts.json = true;
|
|
34
|
+
else if (a === '--fast') opts.fast = true;
|
|
35
|
+
else if (a === '--puppeteer') opts.puppeteer = true;
|
|
36
|
+
else if (a === '-h' || a === '--help') opts.help = true;
|
|
37
|
+
else if (a === '--rule') opts.rule = argv[++i] || null;
|
|
38
|
+
else if (a.startsWith('--rule=')) opts.rule = a.slice('--rule='.length);
|
|
39
|
+
else if (!a.startsWith('-') && opts.path === null) opts.path = a;
|
|
40
|
+
}
|
|
41
|
+
return opts;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isUrl(p) {
|
|
45
|
+
return /^https?:\/\//i.test(String(p || ''));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Select the detection engine. Returns { mode, warning }. Regex-fast is the dep-free default. */
|
|
49
|
+
function selectEngine(opts, requireFn) {
|
|
50
|
+
if (opts.fast) return { mode: 'regex-fast', warning: null };
|
|
51
|
+
let hasJsdom = false;
|
|
52
|
+
try { requireFn('jsdom'); hasJsdom = true; } catch { hasJsdom = false; }
|
|
53
|
+
if (hasJsdom) return { mode: 'dom-aware', warning: null };
|
|
54
|
+
return { mode: 'regex-fast', warning: 'jsdom not installed — using regex-fast (install jsdom for DOM-aware mode, or pass --fast to silence this).' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderHuman(result, mode) {
|
|
58
|
+
const lines = [];
|
|
59
|
+
for (const f of result.findings) {
|
|
60
|
+
lines.push(`${f.file}:${f.line}:${f.column} ${f.severity.toUpperCase()} ${f.ruleId} ${f.name} — ${f.references[0]}`);
|
|
61
|
+
}
|
|
62
|
+
const errs = result.findings.filter((f) => f.severity === 'error').length;
|
|
63
|
+
const warns = result.findings.length - errs;
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(`gdd-detect (${mode}): ${result.filesScanned} file(s), ${result.findings.length} finding(s) — ${errs} error, ${warns} warn.`);
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string[]} argv process.argv.slice(2)
|
|
71
|
+
* @param {{ cwd?: string, log?: fn, err?: fn, requireFn?: fn }} [io] injectable for tests
|
|
72
|
+
* @returns {number} exit code
|
|
73
|
+
*/
|
|
74
|
+
function main(argv, io) {
|
|
75
|
+
const o = io || {};
|
|
76
|
+
const log = o.log || ((s) => process.stdout.write(s + '\n'));
|
|
77
|
+
const err = o.err || ((s) => process.stderr.write(s + '\n'));
|
|
78
|
+
const requireFn = o.requireFn || require;
|
|
79
|
+
const cwd = o.cwd || process.cwd();
|
|
80
|
+
const opts = parseArgs(argv);
|
|
81
|
+
|
|
82
|
+
if (opts.help || (!opts.path && argv.length === 0)) { log(HELP); return opts.help ? 0 : 1; }
|
|
83
|
+
if (!opts.path) { err('gdd-detect: missing <path>. See --help.'); return 1; }
|
|
84
|
+
if (opts.rule && !/^BAN-\d{2}$/i.test(opts.rule)) { err(`gdd-detect: --rule expects a BAN-NN id (got "${opts.rule}").`); return 1; }
|
|
85
|
+
|
|
86
|
+
// URL path → Puppeteer (optional, separately installed). Never a stack trace.
|
|
87
|
+
if (isUrl(opts.path)) {
|
|
88
|
+
if (!opts.puppeteer) { err('gdd-detect: scanning a URL requires --puppeteer. Pass --puppeteer (and `npm i -D puppeteer`) to enable URL scans.'); return 1; }
|
|
89
|
+
let hasPuppeteer = false;
|
|
90
|
+
try { requireFn('puppeteer'); hasPuppeteer = true; } catch { hasPuppeteer = false; }
|
|
91
|
+
if (!hasPuppeteer) { err('gdd-detect: --puppeteer given but puppeteer is not installed. Install it with `npm i -D puppeteer` (it stays an optional dependency).'); return 1; }
|
|
92
|
+
err('gdd-detect: URL scanning is not wired in this build; clone the page locally and scan the files instead.');
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { mode, warning } = selectEngine(opts, requireFn);
|
|
97
|
+
if (warning && !opts.json) err('gdd-detect: ' + warning);
|
|
98
|
+
|
|
99
|
+
let result;
|
|
100
|
+
try { result = engine.run(opts.path, { ruleId: opts.rule, cwd }); }
|
|
101
|
+
catch (e) { err('gdd-detect: ' + (e && e.message ? e.message : String(e))); return 1; }
|
|
102
|
+
|
|
103
|
+
if (opts.json) log(JSON.stringify({ mode, ...result }, null, 2));
|
|
104
|
+
else log(renderHuman(result, mode));
|
|
105
|
+
|
|
106
|
+
return result.findings.length > 0 ? 2 : 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { main, parseArgs, isUrl, selectEngine, HELP };
|
|
110
|
+
|
|
111
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — gdd-detect engine. Pure, dep-free (regex-fast path). Walks a path, runs each rule's
|
|
3
|
+
// matcher against file content, returns structured findings. The DOM-aware (jsdom) + URL (puppeteer)
|
|
4
|
+
// paths are layered on in cli.cjs via soft try-require; the engine itself never touches the network
|
|
5
|
+
// or any optional dependency — so the SC#10 network-isolation scan stays clean.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { RULES, EXEMPT } = require('./rules/index.cjs');
|
|
10
|
+
|
|
11
|
+
const SCANNABLE_EXT = new Set(['.html', '.htm', '.css', '.scss', '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte']);
|
|
12
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.design', '.planning']);
|
|
13
|
+
|
|
14
|
+
/** Recursively collect scannable file paths under `root` (a file or dir). */
|
|
15
|
+
function walk(root) {
|
|
16
|
+
const out = [];
|
|
17
|
+
if (!fs.existsSync(root)) return out;
|
|
18
|
+
const st = fs.statSync(root);
|
|
19
|
+
if (st.isFile()) {
|
|
20
|
+
if (SCANNABLE_EXT.has(path.extname(root).toLowerCase())) out.push(root);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const stack = [root];
|
|
24
|
+
while (stack.length) {
|
|
25
|
+
const dir = stack.pop();
|
|
26
|
+
let entries;
|
|
27
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
const full = path.join(dir, e.name);
|
|
30
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(full); }
|
|
31
|
+
else if (e.isFile() && SCANNABLE_EXT.has(path.extname(e.name).toLowerCase())) out.push(full);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Select the active rule set. `ruleId` (e.g. 'BAN-08') narrows to one rule. */
|
|
38
|
+
function selectRules(ruleId) {
|
|
39
|
+
if (!ruleId) return RULES;
|
|
40
|
+
const id = String(ruleId).toUpperCase();
|
|
41
|
+
return RULES.filter((r) => r.id === id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Run `rules` over one file's content. Returns findings with file-relative metadata merged in. */
|
|
45
|
+
function scanContent(content, ctx, rules) {
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const rule of rules) {
|
|
48
|
+
let hits = [];
|
|
49
|
+
try { hits = rule.matcher({ content, ext: ctx.ext, path: ctx.path }) || []; } catch { hits = []; }
|
|
50
|
+
for (const h of hits) {
|
|
51
|
+
findings.push({
|
|
52
|
+
ruleId: rule.id, category: rule.category, name: rule.name, severity: rule.severity,
|
|
53
|
+
file: ctx.path, line: h.line, column: h.column, match: h.match, references: rule.references,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run the detector over a path.
|
|
62
|
+
* @param {string} root file or directory
|
|
63
|
+
* @param {{ruleId?: string, cwd?: string}} [opts]
|
|
64
|
+
* @returns {{findings: object[], filesScanned: number, errors: number, rules: number}}
|
|
65
|
+
*/
|
|
66
|
+
function run(root, opts) {
|
|
67
|
+
const o = opts || {};
|
|
68
|
+
const rules = selectRules(o.ruleId);
|
|
69
|
+
const cwd = o.cwd || process.cwd();
|
|
70
|
+
const files = walk(root);
|
|
71
|
+
const findings = [];
|
|
72
|
+
let errors = 0;
|
|
73
|
+
for (const abs of files) {
|
|
74
|
+
let content;
|
|
75
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch { errors++; continue; }
|
|
76
|
+
const rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
77
|
+
findings.push(...scanContent(content, { path: rel || abs, ext: path.extname(abs).toLowerCase() }, rules));
|
|
78
|
+
}
|
|
79
|
+
findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column || a.ruleId.localeCompare(b.ruleId));
|
|
80
|
+
return { findings, filesScanned: files.length, errors, rules: rules.length };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { run, walk, scanContent, selectRules, RULES, EXEMPT, SCANNABLE_EXT, SKIP_DIRS };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://get-design-done.example/schemas/gdd-detect-rule.schema.json",
|
|
4
|
+
"title": "gdd-detect rule",
|
|
5
|
+
"description": "Phase 41 — the shape of a scripts/lib/detect/rules/ban-NN.cjs module export. Each rule ports a statically-detectable BAN-NN anti-pattern from reference/anti-patterns.md into an executable matcher. The markdown catalogue stays the canonical prose; the rule files are the canonical executable; scripts/sync-rule-catalogue.cjs keeps them in parity.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "category", "name", "description", "references", "severity"],
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^BAN-[0-9]{2}$",
|
|
13
|
+
"description": "The BAN-NN id — matches a `### BAN-NN:` heading + a `bdId: BAN-NN` marker in reference/anti-patterns.md."
|
|
14
|
+
},
|
|
15
|
+
"category": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["decoration", "motion", "color", "accessibility", "performance", "layout", "typography"],
|
|
18
|
+
"description": "Coarse grouping for report sectioning."
|
|
19
|
+
},
|
|
20
|
+
"name": { "type": "string", "minLength": 1, "description": "Human title (the BAN heading text after the id)." },
|
|
21
|
+
"description": { "type": "string", "minLength": 1, "description": "One-line explanation of what the rule flags + why." },
|
|
22
|
+
"references": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"items": { "type": "string", "pattern": "^reference/.+#" },
|
|
26
|
+
"description": "Bidirectional links to the catalogue paragraph(s) that explain the alternative (e.g. reference/anti-patterns.md#BAN-04)."
|
|
27
|
+
},
|
|
28
|
+
"severity": { "type": "string", "enum": ["error", "warn"], "description": "error fails a strict gate; warn is advisory (open-Q default: error|warn)." },
|
|
29
|
+
"pattern": { "type": "string", "description": "The regex source ported from the catalogue's **Grep** (regex-fast path)." }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-01: Side-Stripe Borders. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "border-left:\\s*[2-9][0-9]*px|border-right:\\s*[2-9][0-9]*px";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-01",
|
|
26
|
+
category: "decoration",
|
|
27
|
+
name: "Side-Stripe Borders",
|
|
28
|
+
description: "A thick (>=2px) left/right accent border — a dated, decorative side-stripe.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-01"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-02: Gradient Text. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "background-clip:\\s*text|text-fill-color:\\s*transparent";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-02",
|
|
26
|
+
category: "decoration",
|
|
27
|
+
name: "Gradient Text",
|
|
28
|
+
description: "Gradient-filled text via background-clip:text — low legibility, an AI-era cliche.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-02"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-03: Bounce/Elastic Easing. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "cubic-bezier\\(.*-[0-9]|bounce|elastic|spring\\(";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-03",
|
|
26
|
+
category: "motion",
|
|
27
|
+
name: "Bounce/Elastic Easing",
|
|
28
|
+
description: "Bounce/elastic/spring easing — playful overshoot that reads as unserious for product UI.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-03"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|