Compare commits

..

1 commit

Author SHA1 Message Date
Randall Stillwell
dc7b387309 refactor(design): tone down card corner-light gradients to ~55% intensity
Operator feedback after the design-sweep PR went out: the corner-light
gradients on data cards (the `.glass-panel` / `.glass-panel-strong`
class) read too vibrant once the pattern was applied broadly to deck
cards, stat cards, list cards, modals, dropdowns, etc.

The same intensity worked fine on the floating chrome chips (sidebar
nav, top header) because there are only two of those per viewport and
they SHOULD anchor the page visually as the surfaces "responding" to
the background light sources. Data cards tile densely; at full
intensity they compete with the chrome and the catch-lights become
visual noise instead of an environmental cue.

Two intensity tiers now live in `styles/globals.css`:

  1. `--corner-light-{warm,cool}` (full intensity, unchanged) — used
     by the floating chrome chips via inline `boxShadow` in
     `components/Layout.js` and `components/ui/TopSearchBar.js`.

  2. `--corner-light-{warm,cool}-subtle` (NEW, ~55% of full alpha) —
     used by `.glass-panel` and `.glass-panel-strong` for data cards.

Alpha values:

| Token                            | Light theme | Dark theme |
|----------------------------------|-------------|------------|
| `--corner-light-warm`            | 0.50        | 0.68       |
| `--corner-light-cool`            | 0.38        | 0.58       |
| `--corner-light-warm-subtle`     | 0.28        | 0.38       |
| `--corner-light-cool-subtle`     | 0.20        | 0.32       |

No other surfaces changed, no token names removed (so any custom
inline `boxShadow` styles in other components continue to compile).
The hearth body gradient + `--chip-border-base` faint white tint are
untouched.

Verified: build + lint + 113/113 vitest tests pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 13:18:16 -05:00
384 changed files with 5005 additions and 43648 deletions

View file

@ -1,166 +1,176 @@
# .agent-context-manifest.yml
#
# Generated by agent-pipeline bootstrap-agent-context skill.
# Tracks which artifacts the bootstrap installed in this repo, where they
# came from, and what pipeline version they correspond to.
#
# Read by the `sync-agent-context` skill to detect drift and propose updates.
# Don't edit by hand — use the bootstrap or sync skill in Cursor.
#
# Schema: https://github.com/varutasu/agent-pipeline/blob/main/docs/manifest-schema.md
schema_version: 1
pipeline_version: 0.7.0
pipeline_source: https://github.com/varutasu/agent-pipeline
installed_at: '2026-05-22T22:25:00Z'
last_synced_at: '2026-08-14T23:50:20Z'
pipeline_version: "0.5.0"
pipeline_source: "https://github.com/varutasu/agent-pipeline"
installed_at: "2026-05-22T22:25:00Z"
last_synced_at: "2026-05-22T22:25:00Z"
layers:
- L1
- L2
- L3
# Notes:
# - AGENTS.md is hand-curated per-repo — NOT tracked (always shows drift)
# - docs/SCHEMA_MAP.md is hand-curated per-repo — NOT tracked
# - .convoys/<slug>.md files are runtime outputs — NOT tracked
artifacts:
- path: .convoys/README.md
source: skills/bootstrap-agent-context/templates/L3-pipeline/_common/convoys-readme.md.template
version: 0.6.0
installed_hash: sha256:6b779efd3116fffb0f2affdc57964750234d63c75092074e632a9b546d709bd6
- path: .cursor/agents/role-a11y-auditor.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-a11y-auditor.md
version: 0.7.0
installed_hash: sha256:f457840b51f6f4b0c95174ccee175fd5b65f7c9f5f65598ef80ac7fd532110ec
- path: .cursor/agents/role-architect.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-architect.md
version: 0.7.0
installed_hash: sha256:bc38e10177219a3e3903b9019f1052c32fd38ea59373368c3e5d1b1011435b58
- path: .cursor/agents/role-conductor.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-conductor.md
version: 0.7.0
installed_hash: sha256:c4f764becd31175925c711fbf196ae559f989dc78c3f336f1da39349c9b3ee62
- path: .cursor/agents/role-design-system-auditor.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-design-system-auditor.md
version: 0.7.0
installed_hash: sha256:e52b507f12c507411540fa30277e70ab6dd25cc5a742f08c35643f93e73032f6
- path: .cursor/agents/role-doc-writer.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-doc-writer.md
version: 0.7.0
installed_hash: sha256:7e626346705083cd57fa8a401b18f7f44da330a9f2a60f461dc362fbb2c7159b
- path: .cursor/agents/role-ia-architect.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-ia-architect.md
version: 0.7.0
installed_hash: sha256:40d669a8a7ebf1e6165ab1054b189f728ceefaadcfb124d47b55bceaf7c8fac4
- path: .cursor/agents/role-implementer.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-implementer.md
version: 0.7.0
installed_hash: sha256:ae6e4dfa3974af4fbe70c892a7806e68f7268fd1079ad96e7844fa7435b0129a
- path: .cursor/agents/role-reviewer.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-reviewer.md
version: 0.7.0
installed_hash: sha256:e0753d5a2d86f59559ded52d7136cec5ab3cd200b42926b67a56469513eaf41b
- path: .cursor/agents/role-security-auditor.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-security-auditor.md
version: 0.7.0
installed_hash: sha256:f141a54541626b7344c9883431251d6f02d93ede3d070cf4ffbcaea9c19689e0
- path: .cursor/agents/role-ui-designer.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-ui-designer.md
version: 0.7.0
installed_hash: sha256:607dc3783131018dd1c3527bba682ec2fc3c33221bc2bbd66add56fc1691ef18
- path: .cursor/agents/role-ux-reviewer.md
source: skills/bootstrap-agent-context/templates/L2-roles/role-ux-reviewer.md
version: 0.7.0
installed_hash: sha256:c83c365094266d2bd25afa761204116a620d1acbeb854b589cf51bbecefe8100
- path: .cursor/rules/api-routes.mdc
source: skills/bootstrap-agent-context/templates/L1-context/api-routes.mdc.template
version: 0.5.0-local
installed_hash: sha256:92b67b6d0a763c23d95cb63152ea5e8837bb7aa467c703e3cdf059b2d74edad5
- path: .cursor/rules/auth-and-permissions.mdc
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:bad3b270bc9a6fcbae0386fc86b46c1dec759f8b023f48ac4f9a827d65f97ca3
- path: .cursor/rules/convoy-planning.mdc
source: skills/bootstrap-agent-context/templates/L1-context/convoy-planning.mdc.template
version: 0.6.0
installed_hash: sha256:d0d4e2e06905d1e58a6a1d4fd9cda3cdb1bc80698c1e1c699e1c939278f23ea3
- path: .cursor/rules/db-and-schema.mdc
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:6cf287d694d31e633c8a1add9a645cf9a21b4efe5dce66af104209029aa828a3
- path: .cursor/rules/model-routing.mdc
source: skills/bootstrap-agent-context/templates/L1-context/model-routing.mdc.template
version: 0.7.0
installed_hash: sha256:cac45f7aa457eb9312b734f40a55e69e7b30c859b7d8e975a7b80c1268a578f6
- path: .cursor/rules/no-go-zones.mdc
source: skills/bootstrap-agent-context/templates/L1-context/no-go-zones.mdc
version: 0.5.0-local
installed_hash: sha256:bfa661b7bb67047cf33f7ab13116671c12d32a11022bf29dd1e5a12b6f7cf0b5
- path: .cursor/rules/schema-map.mdc
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:3429bad56384117dc81873b337a6d815bd53799389f7908dedb53dbb7642bced
- path: .cursor/rules/security-baseline.mdc
source: skills/bootstrap-agent-context/templates/L1-context/security-baseline.mdc.template
version: 0.6.0
installed_hash: sha256:0f0f919d8c500a5e393bf3def01a4ee69c3c489c80a86c6918f0c03aba41083e
- path: .cursor/rules/ui-and-theming.mdc
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:66cb77e0c4b72605d8be43986e38012fc62b7f3a0e707b7bf80a28f91d208e79
- path: .cursor/skills/add-api-route/SKILL.md
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:0e29f7e994a51e5a40b8308edab08ee9c1f713e297d68e98029294d8ca568cc7
- path: .cursor/skills/add-page/SKILL.md
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:564582ddc877d0d063cf2afa7796ddfc62a00d5d7659debb8e017629dfbb3aaf
- path: .cursor/skills/security-audit/SKILL.md
source: skills/security-audit/SKILL.md
version: 0.6.0
installed_hash: sha256:8148f9ea9e66929ff51b003c6f5d6026c5d1525d31d036affb219624bb5e9305
- path: .cursor/skills/ui-ux-pro-max/SKILL.md
source: skills/ui-ux-pro-max/SKILL.md
version: 0.6.0
installed_hash: sha256:9debdd7439a6f73318e2f624e78d905f78885acf009ce0c34a02aa348b3c1c17
- path: .github/CODEOWNERS
source: skills/bootstrap-agent-context/templates/L3-pipeline/nextjs/CODEOWNERS.template
version: 0.5.0-local
installed_hash: sha256:aff1f610b892b437dbd9c82ab56ce12eb721c06b4cd86c38fa15081ab3951c70
- path: .github/PULL_REQUEST_TEMPLATE.md
source: skills/bootstrap-agent-context/templates/L3-pipeline/_common/PULL_REQUEST_TEMPLATE.md.template
version: 0.6.0
installed_hash: sha256:deaca37703e9c348937614577d1c165993093450434df6002aa395dd70b2ba78
- path: .github/workflows/agent-context-drift.yml
source: skills/bootstrap-agent-context/templates/L3-pipeline/_common/agent-context-drift.yml.template
version: 0.5.0
installed_hash: sha256:5505c296c1b61d023ee2aca222103097e2b5ed2e0e38da3679cc4f9754457785
- path: .github/workflows/ci.yml
source: skills/bootstrap-agent-context/templates/L3-pipeline/nextjs/ci.yml.template
version: 0.5.0-local
installed_hash: sha256:e4b480517346e978a27b22f36dd1926d4c1787176cec6495d69e7119c1e547b2
- path: .github/workflows/convoy-metrics-gate.yml
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:ebdcba74f81fe281ab6cc1306630295addb81c5deba06d313948b8ebf8c199b8
- path: .github/workflows/pr-health-rollup.yml
source: skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/pr-health-rollup.yml.template
version: 0.5.0-local
installed_hash: sha256:a8ead80d2e63b9c9a54c014ed0f130fca90fe2857664ac77d81acc5ee2ebb681
- path: .github/workflows/preview-smoke.yml
source: skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/preview-smoke.yml.template
version: 0.5.0-local
installed_hash: sha256:9f6473f716e541164c10ef30ef12258f1a8e360bb7776db20dc26872f687539c
- path: .github/workflows/visual-diff.yml
source: skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/visual-diff.yml.template
version: 0.5.0-local
installed_hash: sha256:7dbe634bfe7a6a6d1ca2c76c86dc80947e0a699e393153f47dd047ced3f86975
- path: docs/agent-context/README.md
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:095b9cc6a30327114c9ddfb4ff57a5fde76b213e12b1c5574a1f96205d60dbad
- path: docs/agent-context/model-routing-policy.md
source: docs/model-routing-policy.md
version: 0.7.0
installed_hash: sha256:cc7a9a39ff28c6b743c47efbdf06fc3b75cf16c9fa62691fa1b05862a460dd45
- path: lib/flags/index.js
source: tcg-vault-local
version: 0.5.0-local
installed_hash: sha256:1a3cd1f900194eaf4ec86588dd1c3c2bff6a565e742061fc911abdd47bd5f3a5
- path: scripts/log-convoy-event.sh
source: skills/bootstrap-agent-context/templates/L3-pipeline/_common/log-convoy-event.sh
version: 0.6.0
installed_hash: sha256:52bdc8f60b18315dd8ad0f1dd6b727106dfa134b8769b0cd63d8698d5865cf21
- path: scripts/wt.sh
source: skills/bootstrap-agent-context/templates/L3-pipeline/_common/wt.sh
version: 0.5.0
installed_hash: sha256:2a4f44a159f80a8ea6fe53ac507c01a2f91a4e2118d997a98b051808ac35e9a5
- path: tests/smoke/app.smoke.spec.ts
source: skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/playwright-smoke.spec.ts.template
version: 0.5.0
installed_hash: sha256:a62c10edb712a61f1cfece43705bfff75a5a66ad6bc8b53f7e69a43c3efb962c
- path: ".convoys/README.md"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/convoys-readme.md.template"
version: "0.5.0"
installed_hash: "sha256:a48548cd3f5d0c40fc179106890661c3be5fcdc13eb705af7cfe9233e0b8b209"
- path: ".cursor/agents/role-a11y-auditor.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-a11y-auditor.md"
version: "0.5.0"
installed_hash: "sha256:a59938deceb0246ebd7e477f1f9a442102f9fcbb81b0364f0ddc5f86e95a7930"
- path: ".cursor/agents/role-architect.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-architect.md"
version: "0.5.0"
installed_hash: "sha256:269bd62af1557c5d353a9f95a613960e3434be4ec6e0c0b5f6b099adf6872044"
- path: ".cursor/agents/role-conductor.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-conductor.md"
version: "0.5.0"
installed_hash: "sha256:bc75a3e6646217a015f7bb60c3610afd9b57ae91c7d2fc7a7971f4709b19368a"
- path: ".cursor/agents/role-design-system-auditor.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-design-system-auditor.md"
version: "0.5.0"
installed_hash: "sha256:d214cecb1e8482fc24f2815c8220c860191f08526614f89cf9a5797e4ee9110a"
- path: ".cursor/agents/role-doc-writer.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-doc-writer.md"
version: "0.5.0"
installed_hash: "sha256:d4e8bf8cee93153506b7b742848462422dbe5cc7fd012c62f6ffd50460e344d4"
- path: ".cursor/agents/role-ia-architect.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-ia-architect.md"
version: "0.5.0"
installed_hash: "sha256:69685a3a407c4ee25e2606d426c3107d6b917abee80f907e16ade4a16b439839"
- path: ".cursor/agents/role-implementer.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-implementer.md"
version: "0.5.0"
installed_hash: "sha256:b4f4d8596068679b90ffc3a2b6d2e1b6548caf8c68a50f7ed640ba8f638c1c4c"
- path: ".cursor/agents/role-reviewer.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-reviewer.md"
version: "0.5.0"
installed_hash: "sha256:1ff38349321402a0ac2be37878dc2c0bcab62e54caf74c422b919aa6d75f9b67"
- path: ".cursor/agents/role-ux-reviewer.md"
source: "skills/bootstrap-agent-context/templates/L2-roles/role-ux-reviewer.md"
version: "0.5.0"
installed_hash: "sha256:3a1d4b66981f469b15e23a1cd34ab41352759966179e126b3d56ddc1eca4a03e"
- path: ".cursor/rules/api-routes.mdc"
source: "skills/bootstrap-agent-context/templates/L1-context/api-routes.mdc.template"
version: "0.5.0-local"
installed_hash: "sha256:54cd66d71f5a129a67d0f4b1797f5f63b7f9aae3eeabe67217861456ff4db59b"
- path: ".cursor/rules/auth-and-permissions.mdc"
source: "tcg-vault-local"
version: "0.5.0-local"
installed_hash: "sha256:9b7eb7bea0cad0e43d0e9442eb8b660945cb6935f9f7c82fd7d7d71d66b39a2d"
- path: ".cursor/rules/db-and-schema.mdc"
source: "tcg-vault-local"
version: "0.5.0-local"
installed_hash: "sha256:83df2cf7121722a092f85165b1a93c755ce57e5361e0f6b2ecc74e9f928c5015"
- path: ".cursor/rules/no-go-zones.mdc"
source: "skills/bootstrap-agent-context/templates/L1-context/no-go-zones.mdc"
version: "0.5.0-local"
installed_hash: "sha256:aa7046bc3e0266cb3c9b0eb0ef8f68cc50d6837f65c96861804ff81b9c4afa64"
- path: ".cursor/rules/schema-map.mdc"
source: "tcg-vault-local"
version: "0.5.0-local"
installed_hash: "sha256:3429bad56384117dc81873b337a6d815bd53799389f7908dedb53dbb7642bced"
- path: ".cursor/rules/ui-and-theming.mdc"
source: "tcg-vault-local"
version: "0.5.0-local"
installed_hash: "sha256:b841ddda5baa47a76c3726a3c92b2c82a45120fdd461b3243bf970479d1cf1df"
- path: ".cursor/skills/add-api-route/SKILL.md"
source: "tcg-vault-local"
version: "0.5.0-local"
installed_hash: "sha256:0e29f7e994a51e5a40b8308edab08ee9c1f713e297d68e98029294d8ca568cc7"
- path: ".cursor/skills/add-page/SKILL.md"
source: "tcg-vault-local"
version: "0.5.0-local"
installed_hash: "sha256:318912077a6ced6a3a31f85dc15d069bf7627c161b6735e3fa259ca10766daa9"
- path: ".github/CODEOWNERS"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs/CODEOWNERS.template"
version: "0.5.0-local"
installed_hash: "sha256:b714a0a011776300abeab92fe8969f150c273c37d0d6b37c1ad2eb67d47decda"
- path: ".github/PULL_REQUEST_TEMPLATE.md"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/PULL_REQUEST_TEMPLATE.md.template"
version: "0.5.0"
installed_hash: "sha256:89863e58b9ec194aef1c94d3596e892467833e8bc880a28994acca401b6d9635"
- path: ".github/workflows/agent-context-drift.yml"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/agent-context-drift.yml.template"
version: "0.5.0"
installed_hash: "sha256:5505c296c1b61d023ee2aca222103097e2b5ed2e0e38da3679cc4f9754457785"
- path: ".github/workflows/ci.yml"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs/ci.yml.template"
version: "0.5.0-local"
installed_hash: "sha256:6aff7a1c9f2e42606580c241b6dadca7c2d8550aeb959bd69fdd843eb9097cac"
- path: ".github/workflows/pr-health-rollup.yml"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/pr-health-rollup.yml.template"
version: "0.5.0-local"
installed_hash: "sha256:8747674323807d84395fa027b25e7e27881c5e6b0cc87a138d9cb3f78fd88956"
- path: ".github/workflows/preview-smoke.yml"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/preview-smoke.yml.template"
version: "0.5.0-local"
installed_hash: "sha256:2e71026b09db8b2f32b6a868d705489600c875082d6320c2369bf2f5ebc315b8"
- path: ".github/workflows/visual-diff.yml"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/visual-diff.yml.template"
version: "0.5.0-local"
installed_hash: "sha256:88270b1fa59aba99591ec094764dd367deed956bcb746ac6bb195241b3a7dae1"
- path: "docs/agent-context/README.md"
source: "skills/bootstrap-agent-context/templates/L1-context/agent-context-readme.md.template"
version: "0.5.0-local"
installed_hash: "sha256:095b9cc6a30327114c9ddfb4ff57a5fde76b213e12b1c5574a1f96205d60dbad"
- path: "lib/flags/index.js"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/flags-index.ts.template"
version: "0.5.0-local"
installed_hash: "sha256:1a3cd1f900194eaf4ec86588dd1c3c2bff6a565e742061fc911abdd47bd5f3a5"
- path: "scripts/log-convoy-event.sh"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/log-convoy-event.sh"
version: "0.5.0"
installed_hash: "sha256:cd0413691066a177b6b4e6164a9a0978c20a853ad60222ae833b5d53b255818d"
- path: "scripts/wt.sh"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/_common/wt.sh"
version: "0.5.0"
installed_hash: "sha256:2a4f44a159f80a8ea6fe53ac507c01a2f91a4e2118d997a98b051808ac35e9a5"
- path: "tests/smoke/app.smoke.spec.ts"
source: "skills/bootstrap-agent-context/templates/L3-pipeline/nextjs-prisma-vercel/playwright-smoke.spec.ts.template"
version: "0.5.0"
installed_hash: "sha256:a62c10edb712a61f1cfece43705bfff75a5a66ad6bc8b53f7e69a43c3efb962c"

View file

@ -1,133 +0,0 @@
{"ts": "2026-05-23T04:18:51Z", "role": "role-conductor", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "visual", "a11y", "design"], "classification": "server-only", "duration_s": 0}
{"ts": "2026-05-23T04:44:56Z", "role": "role-conductor", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "flag"], "classification": "feature", "duration_s": 0}
{"ts": "2026-05-23T04:54:59Z", "role": "role-architect", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "flag"], "classification": "feature", "duration_s": 101}
{"ts": "2026-05-23T05:37:11Z", "role": "role-architect", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "flag"], "classification": "feature", "duration_s": 311, "outcome": "scope-expanded"}
{"ts": "2026-05-23T05:50:00Z", "role": "role-architect", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "flag"], "classification": "feature", "duration_s": 357, "outcome": "eslint-v10-pivot"}
{"ts": "2026-05-23T05:55:05Z", "role": "role-implementer", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 180}
{"ts": "2026-05-23T06:12:57Z", "role": "role-architect", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "duration_s": 319, "outcome": "typescript-devdep-add"}
{"ts": "2026-05-23T06:28:25Z", "role": "role-implementer", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 78}
{"ts": "2026-05-23T06:48:30Z", "role": "role-architect", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "duration_s": 347, "outcome": "eslint-v9-fallback-decision-d"}
{"ts": "2026-05-23T06:51:22Z", "role": "role-implementer", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 50}
{"ts": "2026-05-23T07:07:35Z", "role": "role-design-system-auditor", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "duration_s": 114, "multitask_group": "audit-bump-next-js-4"}
{"ts": "2026-05-23T07:08:15Z", "role": "role-a11y-auditor", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "duration_s": 180, "multitask_group": "audit-bump-next-js-4"}
{"ts": "2026-05-23T07:08:55Z", "role": "role-reviewer", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 42, "multitask_group": "audit-bump-next-js-4"}
{"ts": "2026-05-23T07:34:56Z", "role": "role-doc-writer", "convoy": "bump-next-js", "repo": "tcg-vault", "skip_flags": [], "duration_s": 26, "outcome": "complete"}
{"ts": "2026-05-23T07:57:10Z", "role": "role-architect", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "visual", "a11y", "design"], "classification": "server-only", "duration_s": 900}
{"ts": "2026-05-23T14:44:38Z", "role": "role-implementer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 192}
{"ts": "2026-05-23T14:52:56Z", "role": "role-implementer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 3, "duration_s": 240}
{"ts": "2026-05-23T15:00:15Z", "role": "role-reviewer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 3, "duration_s": 129}
{"ts": "2026-05-23T15:52:10Z", "role": "role-reviewer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "duration_s": 209}
{"ts": "2026-05-23T15:56:43Z", "role": "role-reviewer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 4, "duration_s": 227}
{"ts": "2026-05-23T16:04:17Z", "role": "role-reviewer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 6, "duration_s": 34}
{"ts": "2026-05-23T16:10:21Z", "role": "role-reviewer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 5, "duration_s": 131}
{"ts": "2026-05-23T16:25:52Z", "role": "role-doc-writer", "convoy": "fix-auth-bypass", "repo": "tcg-vault", "skip_flags": [], "brief": 0, "duration_s": 724, "outcome": "complete"}
{"ts": "2026-05-23T17:31:18Z", "role": "role-architect", "convoy": "drop-public-setup", "repo": "tcg-vault", "skip_flags": [], "duration_s": 720}
{"ts": "2026-05-23T19:58:01Z", "role": "role-implementer", "convoy": "drop-public-setup", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 327}
{"ts": "2026-05-23T20:07:35Z", "role": "role-implementer", "convoy": "drop-public-setup", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "duration_s": 174}
{"ts": "2026-05-23T23:00:44Z", "role": "role-architect", "convoy": "fix-layout-default-user", "repo": "tcg-vault", "skip_flags": [], "duration_s": 1500}
{"ts": "2026-05-24T14:14:24Z", "role": "role-implementer", "convoy": "fix-layout-default-user", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 403}
{"ts": "2026-05-24T15:05:32Z", "role": "role-implementer", "convoy": "fix-layout-default-user", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "duration_s": 1800}
{"ts": "2026-05-24T19:37:56Z", "role": "role-doc-writer", "convoy": "fix-layout-default-user", "repo": "tcg-vault", "skip_flags": [], "duration_s": 242, "outcome": "complete"}
{"ts": "2026-05-24T20:26:29Z", "role": "role-implementer", "convoy": "fix-vercel-deployment-protection-in-ci", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 101}
{"ts": "2026-05-24T21:32:57Z", "role": "role-doc-writer", "convoy": "fix-vercel-deployment-protection-in-ci", "repo": "tcg-vault", "skip_flags": [], "duration_s": 207, "outcome": "complete"}
{"ts": "2026-05-24T23:44:57Z", "role": "role-implementer", "convoy": "adopt-playwright-smoke", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 310}
{"ts": "2026-05-25T01:25:55Z", "role": "role-implementer", "convoy": "cors-tighten", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 900}
{"ts": "2026-05-25T03:46:40Z", "role": "role-implementer", "convoy": "add-rate-limiting", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 600}
{"ts": "2026-05-27T12:55:50Z", "role": "role-conductor", "convoy": "scanner-audit-portfolio", "repo": "tcg-vault", "skip_flags": [], "classification": "docs-only", "duration_s": 0}
{"ts": "2026-05-27T19:09:46Z", "role": "role-reviewer", "convoy": "redesign-scanner-flow", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120, "outcome": "comment-only", "multitask_group": "audit-redesign-scanner-flow-44"}
{"ts": "2026-05-27T19:09:46Z", "role": "role-design-system-auditor", "convoy": "redesign-scanner-flow", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120, "outcome": "comment-only", "multitask_group": "audit-redesign-scanner-flow-44"}
{"ts": "2026-05-27T19:09:46Z", "role": "role-a11y-auditor", "convoy": "redesign-scanner-flow", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120, "outcome": "comment-only", "multitask_group": "audit-redesign-scanner-flow-44"}
{"ts": "2026-06-02T05:33:45Z", "role": "role-implementer", "convoy": "collection-vocabulary", "repo": "tcg-vault", "skip_flags": [], "brief": 3, "duration_s": 120}
{"ts": "2026-06-02T05:37:04Z", "role": "role-implementer", "convoy": "seed-visual-baselines-on-linux", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 180}
{"ts": "2026-06-02T05:37:36Z", "role": "role-implementer", "convoy": "purge-neondatabase-serverless-fully", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 300}
{"ts": "2026-06-02T13:48:03Z", "role": "role-implementer", "convoy": "wire-migrate-into-ci", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 300}
{"ts": "2026-06-02T13:48:43Z", "role": "role-doc-writer", "convoy": "convoy-doc-housekeeping", "repo": "tcg-vault", "skip_flags": [], "duration_s": 0, "outcome": "complete"}
{"ts": "2026-06-02T14:37:35Z", "role": "role-implementer", "convoy": "seed-visual-baselines-on-linux", "repo": "tcg-vault", "skip_flags": [], "brief": 3, "duration_s": 120}
{"ts": "2026-06-03T22:42:56Z", "role": "role-conductor", "convoy": "liquid-glass-design-tokens", "repo": "tcg-vault", "skip_flags": ["ux", "ia", "qa", "flag"], "classification": "feature", "duration_s": 180}
{"ts": "2026-06-03T23:23:20Z", "role": "role-design-system-auditor", "convoy": "liquid-glass-design-tokens", "repo": "tcg-vault", "skip_flags": [], "duration_s": 420}
{"ts": "2026-06-03T23:43:49Z", "role": "role-architect", "convoy": "liquid-glass-design-tokens", "repo": "tcg-vault", "skip_flags": [], "duration_s": 540}
{"ts": "2026-06-03T23:48:23Z", "role": "role-a11y-auditor", "convoy": "liquid-glass-design-tokens", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120}
{"ts": "2026-06-03T23:48:23Z", "role": "role-implementer", "convoy": "liquid-glass-design-tokens", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 600, "outcome": "merged"}
{"ts": "2026-06-03T23:48:23Z", "role": "role-reviewer", "convoy": "liquid-glass-design-tokens", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 180, "outcome": "approved"}
{"ts": "2026-06-03T23:54:18Z", "role": "role-architect", "convoy": "liquid-glass-modal-and-surface-primitive", "repo": "tcg-vault", "skip_flags": [], "duration_s": 720}
{"ts": "2026-06-03T23:54:18Z", "role": "role-implementer", "convoy": "liquid-glass-modal-and-surface-primitive", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 1800, "outcome": "merged"}
{"ts": "2026-06-03T23:58:42Z", "role": "role-architect", "convoy": "liquid-glass-form-primitives", "repo": "tcg-vault", "skip_flags": [], "duration_s": 600}
{"ts": "2026-06-03T23:58:42Z", "role": "role-implementer", "convoy": "liquid-glass-form-primitives", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 1500, "outcome": "merged"}
{"ts": "2026-06-04T00:00:32Z", "role": "role-architect", "convoy": "liquid-glass-layout-shell", "repo": "tcg-vault", "skip_flags": [], "duration_s": 300}
{"ts": "2026-06-04T00:00:32Z", "role": "role-implementer", "convoy": "liquid-glass-layout-shell", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 900, "outcome": "merged"}
{"ts": "2026-06-04T00:02:28Z", "role": "role-architect", "convoy": "motion-system-pass", "repo": "tcg-vault", "skip_flags": [], "duration_s": 300}
{"ts": "2026-06-04T00:02:29Z", "role": "role-implementer", "convoy": "motion-system-pass", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 420, "outcome": "merged"}
{"ts": "2026-06-04T00:03:43Z", "role": "role-architect", "convoy": "liquid-glass-card-surfaces", "repo": "tcg-vault", "skip_flags": [], "duration_s": 240, "outcome": "architecture-only"}
{"ts": "2026-06-04T00:04:10Z", "role": "role-architect", "convoy": "liquid-glass-public-and-auth", "repo": "tcg-vault", "skip_flags": [], "duration_s": 180, "outcome": "architecture-only"}
{"ts": "2026-06-04T00:07:19Z", "role": "role-implementer", "convoy": "cleanup-legacy-design-css", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 900, "outcome": "merged"}
{"ts": "2026-06-04T18:36:17Z", "role": "role-conductor", "convoy": "unify-glass-panel-surfaces", "repo": "tcg-vault", "skip_flags": ["ia"], "classification": "feature", "duration_s": 0}
{"ts": "2026-06-04T18:36:17Z", "role": "role-conductor", "convoy": "cleanup-card-item-list-and-share-modal-palette", "repo": "tcg-vault", "skip_flags": ["ia", "ux", "arch"], "classification": "feature", "duration_s": 0}
{"ts": "2026-06-04T18:45:32Z", "role": "role-architect", "convoy": "unify-glass-panel-surfaces", "repo": "tcg-vault", "skip_flags": [], "duration_s": 0}
{"ts": "2026-06-13T02:21:43Z", "role": "role-conductor", "convoy": "harden-visual-diff-gate", "repo": "tcg-vault", "skip_flags": [], "classification": "ci", "duration_s": 120, "outcome": "routed-to-architect"}
{"ts": "2026-06-13T02:21:43Z", "role": "role-architect", "convoy": "harden-visual-diff-gate", "repo": "tcg-vault", "skip_flags": [], "classification": "ci", "duration_s": 420, "outcome": "architecture-only"}
{"ts": "2026-06-13T02:21:43Z", "role": "role-implementer", "convoy": "harden-visual-diff-gate", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "classification": "ci", "duration_s": 900, "outcome": "pr-open"}
{"ts": "2026-06-13T02:57:31Z", "role": "role-implementer", "convoy": "harden-visual-diff-gate", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "classification": "ci", "duration_s": 1200, "outcome": "pr-open"}
{"ts": "2026-06-13T02:57:31Z", "role": "role-reviewer", "convoy": "harden-visual-diff-gate", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "classification": "ci", "duration_s": 300, "outcome": "approved"}
{"ts": "2026-06-13T04:14:48Z", "role": "role-architect", "convoy": "rotate-default-admin", "repo": "tcg-vault", "skip_flags": [], "classification": "security", "duration_s": 240, "outcome": "option-B-chosen"}
{"ts": "2026-06-13T04:14:48Z", "role": "role-implementer", "convoy": "rotate-default-admin", "repo": "tcg-vault", "skip_flags": [], "classification": "security", "duration_s": 900, "outcome": "pr-open"}
{"ts": "2026-06-13T06:16:56Z", "role": "role-architect", "convoy": "enable-no-undef-eslint-rule", "repo": "tcg-vault", "skip_flags": [], "classification": "ci", "duration_s": 180, "outcome": "architecture-only"}
{"ts": "2026-06-13T06:16:56Z", "role": "role-implementer", "convoy": "enable-no-undef-eslint-rule", "repo": "tcg-vault", "skip_flags": [], "classification": "ci", "duration_s": 900, "outcome": "pr-open"}
{"ts": "2026-06-13T06:20:00Z", "role": "role-architect", "convoy": "scanner-disambiguation-render-test", "repo": "tcg-vault", "skip_flags": [], "classification": "ci", "duration_s": 180, "outcome": "architecture-only"}
{"ts": "2026-06-13T06:20:00Z", "role": "role-implementer", "convoy": "scanner-disambiguation-render-test", "repo": "tcg-vault", "skip_flags": [], "classification": "ci", "duration_s": 600, "outcome": "pr-open"}
{"ts": "2026-06-14T13:06:36Z", "role": "role-implementer", "convoy": "multi-game-bulk-sync", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "duration_s": 600}
{"ts": "2026-08-14T23:57:39Z", "role": "role-conductor", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "classification": "feature", "duration_s": 180, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:14:27Z", "role": "role-ia-architect", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 95, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:15:31Z", "role": "role-ui-designer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 90, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:16:36Z", "role": "role-ux-reviewer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 180, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:18:01Z", "role": "role-conductor", "convoy": "scanner-identify-upgrade", "repo": "scanner-identify-upgrade", "skip_flags": [], "classification": "feature", "duration_s": 720, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:18:01Z", "role": "role-conductor", "convoy": "tighten-scan-identify-hot-path", "repo": "scanner-identify-upgrade", "skip_flags": ["ia", "ui-design", "visual", "flag"], "classification": "feature", "duration_s": 720, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:18:01Z", "role": "role-conductor", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": ["ia", "ui-design", "flag"], "classification": "feature", "duration_s": 720, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:18:01Z", "role": "role-conductor", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": ["ia", "ui-design", "visual", "a11y", "design", "flag"], "classification": "feature", "duration_s": 720, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:18:40Z", "role": "role-architect", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 420, "model": "composer-2.5", "model_tier": "standard"}
{"ts": "2026-08-15T00:19:56Z", "role": "role-implementer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "brief": 1, "duration_s": 240, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:20:23Z", "role": "role-implementer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "brief": 2, "duration_s": 120, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:22:15Z", "role": "role-implementer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "brief": 3, "duration_s": 600, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:24:15Z", "role": "role-implementer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "brief": 4, "duration_s": 600, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T00:25:33Z", "role": "role-security-auditor", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 480, "model": "gpt-5.6-terra-medium", "model_tier": "security"}
{"ts": "2026-08-15T00:25:54Z", "role": "role-reviewer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 210, "multitask_group": "audit-scanner-mobile-checkout-local", "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T00:26:05Z", "role": "role-a11y-auditor", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 78, "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T00:26:08Z", "role": "role-design-system-auditor", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "duration_s": 82, "multitask_group": "audit-scanner-mobile-checkout-local", "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T00:55:56Z", "role": "role-reviewer", "convoy": "tighten-scan-identify-hot-path", "repo": "scanner-identify-upgrade", "skip_flags": [], "brief": 0, "duration_s": 180, "multitask_group": "audit-tighten-scan-identify-hot-path-uncommitted", "model": "cursor-grok-4.5-high", "model_tier": "fast"}
{"ts": "2026-08-15T00:56:39Z", "role": "role-implementer", "convoy": "scanner-mobile-checkout", "repo": "tcg-vault", "skip_flags": [], "brief": 4, "duration_s": 240, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:24:40Z", "role": "role-ux-reviewer", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 120, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:24:40Z", "role": "role-architect", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 300, "model": "composer-2.5", "model_tier": "standard"}
{"ts": "2026-08-15T01:24:40Z", "role": "role-implementer", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "brief": 1, "duration_s": 900, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:24:40Z", "role": "role-implementer", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "brief": 2, "duration_s": 300, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:24:40Z", "role": "role-reviewer", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 180, "outcome": "approved", "multitask_group": "audit-improve-scan-card-detection-local", "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T01:24:41Z", "role": "role-security-auditor", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 120, "outcome": "approved", "multitask_group": "audit-improve-scan-card-detection-local", "model": "gpt-5.6-terra-medium", "model_tier": "security"}
{"ts": "2026-08-15T01:24:41Z", "role": "role-a11y-auditor", "convoy": "improve-scan-card-detection", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 60, "outcome": "approved", "multitask_group": "audit-improve-scan-card-detection-local", "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T01:35:27Z", "role": "role-ux-reviewer", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 90, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:35:27Z", "role": "role-architect", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 360, "model": "composer-2.5", "model_tier": "standard"}
{"ts": "2026-08-15T01:35:28Z", "role": "role-implementer", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "brief": 1, "duration_s": 300, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:35:28Z", "role": "role-implementer", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "brief": 2, "duration_s": 600, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:35:28Z", "role": "role-implementer", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "brief": 3, "duration_s": 900, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T01:35:28Z", "role": "role-reviewer", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 180, "outcome": "approved", "multitask_group": "audit-scan-visual-catalog-search-local", "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T01:35:28Z", "role": "role-security-auditor", "convoy": "scan-visual-catalog-search", "repo": "scanner-identify-upgrade", "skip_flags": [], "duration_s": 120, "outcome": "approved", "multitask_group": "audit-scan-visual-catalog-search-local", "model": "gpt-5.6-terra-medium", "model_tier": "security"}
{"ts": "2026-08-15T12:41:53Z", "role": "role-conductor", "convoy": "scanner-identify-upgrade", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120, "model": "auto", "model_tier": "auto"}
{"ts": "2026-08-15T21:30:59Z", "role": "role-doc-writer", "convoy": "reconcile-historical-add-scripts", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120, "outcome": "complete", "model": "auto", "model_tier": "auto"}
{"ts": "2026-08-15T21:39:17Z", "role": "role-conductor", "convoy": "dashboard-home-realignment", "repo": "tcg-vault", "skip_flags": ["ui-design"], "classification": "feature", "duration_s": 90, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:42:03Z", "role": "role-reviewer", "convoy": "dashboard-home-realignment", "repo": "tcg-vault", "skip_flags": [], "duration_s": 180, "outcome": "comment-only", "model": "cursor-grok-4.5-high", "model_tier": "audit"}
{"ts": "2026-08-15T21:42:19Z", "role": "role-ia-architect", "convoy": "dashboard-home-realignment", "repo": "tcg-vault", "skip_flags": [], "duration_s": 120, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:42:20Z", "role": "role-ux-reviewer", "convoy": "dashboard-home-realignment", "repo": "tcg-vault", "skip_flags": [], "duration_s": 90, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:42:20Z", "role": "role-architect", "convoy": "dashboard-home-realignment", "repo": "tcg-vault", "skip_flags": [], "duration_s": 180, "model": "composer-2.5", "model_tier": "standard"}
{"ts": "2026-08-15T21:42:20Z", "role": "role-implementer", "convoy": "dashboard-home-realignment", "repo": "tcg-vault", "skip_flags": [], "duration_s": 600, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:44:54Z", "role": "role-conductor", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "classification": "feature", "duration_s": 900, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:48:07Z", "role": "role-ia-architect", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 120, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:49:34Z", "role": "role-ui-designer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 300, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:50:19Z", "role": "role-ux-reviewer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 180, "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:51:58Z", "role": "role-architect", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 720, "model": "composer-2.5", "model_tier": "standard"}
{"ts": "2026-08-15T21:53:14Z", "role": "role-implementer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "brief": 5, "duration_s": 120, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:53:18Z", "role": "role-implementer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "brief": 2, "duration_s": 120, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:53:18Z", "role": "role-implementer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "brief": 1, "duration_s": 420, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:53:19Z", "role": "role-implementer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "brief": 3, "duration_s": 420, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:53:28Z", "role": "role-implementer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "brief": 4, "duration_s": 420, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:55:19Z", "role": "role-implementer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "brief": 6, "duration_s": 900, "outcome": "complete", "model": "composer-2.5-fast", "model_tier": "fast"}
{"ts": "2026-08-15T21:56:43Z", "role": "role-reviewer", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 58, "multitask_group": "audit-scanner-desktop-layout-local", "model": "cursor-grok-4.5-high", "model_tier": "fast"}
{"ts": "2026-08-15T21:56:53Z", "role": "role-security-auditor", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 300, "multitask_group": "audit-scanner-desktop-layout-local", "model": "gpt-5.6-terra-medium", "model_tier": "fast"}
{"ts": "2026-08-15T21:57:11Z", "role": "role-design-system-auditor", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 165, "multitask_group": "audit-scanner-desktop-layout-local", "model": "cursor-grok-4.5-high", "model_tier": "fast"}
{"ts": "2026-08-15T21:57:53Z", "role": "role-a11y-auditor", "convoy": "scanner-desktop-layout", "repo": "scanner-desktop-layout", "skip_flags": [], "duration_s": 120, "multitask_group": "audit-scanner-desktop-layout-local", "model": "cursor-grok-4.5-high", "model_tier": "fast"}

View file

@ -2,8 +2,6 @@
A **convoy** is a multi-PR work-stream coordinated by an agent pipeline. One convoy = one feature, bug fix, or epic. Each convoy is a Markdown file in this directory plus an optional sub-directory of implementer briefs.
> **Not Cursor Plan mode.** Pipeline convoys live here (`.convoys/*.md`). Cursor's native Plan feature writes to `.cursor/plans/*.plan.md` — a different artifact. For pipeline work, invoke `role-conductor` and write `.convoys/<slug>.md`; do not create `.cursor/plans/` files. See `.cursor/rules/convoy-planning.mdc`.
## File layout
```
@ -29,19 +27,6 @@ skip:
- <flag1>
status: open | in-progress | merged | shipped | abandoned
created: <YYYY-MM-DD>
model_policy:
default_session: auto
roles: { ... } # see docs/model-routing-policy.md
design_direction: # optional — set by role-ui-designer (planning lock)
source: role-ui-designer
skill: ui-ux-pro-max
skill_version: "2.5.0"
version: 1
locked_at: YYYY-MM-DD
product_type: ""
pattern: ""
style: ""
stack: nextjs
---
```
@ -52,9 +37,8 @@ Body sections (added in order by the pipeline roles):
3. `## Roles invoked` (Conductor)
4. `## Todos` (Conductor → refined by Architect)
5. `## IA` (IA Architect)
6. `## Design direction` (UI Designer — optional; skip when `ui-design` set)
7. `## UX` (UX Reviewer)
8. `## Architecture` (Architect)
6. `## UX` (UX Reviewer)
7. `## Architecture` (Architect)
After Architect, briefs live in `.convoys/<slug>/brief-N-*.md`. Implementers read only their brief, not the whole convoy.
@ -66,14 +50,12 @@ The Conductor sets `skip:` based on classification. These flags map to pipeline
| --- | --- |
| `ia` | IA Architect |
| `ux` | UX Reviewer |
| `ui-design` | UI Designer (planning; `ui-ux-pro-max` skill) |
| `arch` | Architect |
| `test` | Component tests |
| `review` | Reviewer |
| `visual` | Visual diff |
| `a11y` | A11y auditor |
| `design` | Design-system auditor |
| `security` | Security auditor |
| `smoke` | Staging smoke |
| `qa` | Manual QA |
| `docs` | Doc Writer |
@ -107,7 +89,7 @@ See `.cursor/agents/role-conductor.md` for the Conductor's full spec.
**Audit fan-out** — after an implementer ships a PR draft:
```
/multitask role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor
/multitask role-reviewer + role-design-system-auditor + role-a11y-auditor
```
All three read the same diff and emit independent comments. Use group id `audit-<convoy>-<pr>` so analytics can compute wall-clock savings.
@ -124,7 +106,7 @@ See the [multitask playbook](https://github.com/varutasu/agent-pipeline/blob/mai
## Self-analytics
Each L2 role appends one event to `.convoys/.metrics.jsonl` via `scripts/log-convoy-event.sh`. **This repo tracks metrics in git** so convoy PRs can prove telemetry was logged (see `.github/workflows/convoy-metrics-gate.yml`). Events contain metadata only — no code, no prompts.
Each L2 role appends one event to `.convoys/.metrics.jsonl` via `scripts/log-convoy-event.sh`. The file is gitignored by default — events stay local. To opt-in to commit team-shared metrics, remove `.convoys/.metrics.jsonl` from `.gitignore`.
Aggregate across repos and render a dashboard with the [agent-pipeline analytics scripts](https://github.com/varutasu/agent-pipeline/tree/main/analytics):
@ -135,5 +117,5 @@ npx tsx render-dashboard.ts
open ~/agent-pipeline-data/dashboard.html
```
Schema: [`analytics/schemas/convoy-event.json`](https://github.com/varutasu/agent-pipeline/blob/main/analytics/schemas/convoy-event.json). Model tiers: [`docs/model-routing-policy.md`](https://github.com/varutasu/agent-pipeline/blob/main/docs/model-routing-policy.md).
Schema: [`analytics/schemas/convoy-event.json`](https://github.com/varutasu/agent-pipeline/blob/main/analytics/schemas/convoy-event.json).

View file

@ -1,209 +0,0 @@
---
name: cleanup-card-item-list-and-share-modal-palette
classification: feature
success_metric: |
No hardcoded Tailwind palette classes (`bg-purple-*`, `bg-blue-*`,
`bg-gray-*` 100-900 range, `text-gray-{500,600,700,900}`,
`bg-white`, `border-gray-200`, etc.) remain in the interior of
`components/CardItem.js` list-mode or `components/ShareModal.js`.
Both render correctly in dark theme. Verified by visual diff and
a targeted lint sweep.
skip:
- ia
- ux
- arch
status: closed
closed: 2026-06-04
prs:
- 128 # Brief 1 — CardItem.js list-mode token sweep
- 129 # Brief 2 — ShareModal.js interior token sweep
created: 2026-06-04
depends_on:
- design-sweep-pass # PR #117 — established the token migration pattern
---
# Convoy: cleanup-card-item-list-and-share-modal-palette
Targeted palette cleanup. Parallel companion to
`unify-glass-panel-surfaces` — that convoy migrates panel *surfaces*;
this one cleans up the *interior content* (rows, badges, text,
buttons) of two specific files whose interiors were not addressed
by PR #117 because they're not panel surfaces — they're nested
content inside surfaces that were already on the system.
## Why
Two files left over after the 2026-06-04 design sweep:
1. **`components/CardItem.js` list-mode** (L183284) — the
list-view layout (when `viewMode === 'list'`) is entirely
hardcoded Tailwind palette. The grid-mode (L290+) was already
migrated; list-mode wasn't. `border-gray-200`, `bg-purple-50`,
`text-gray-900`, `text-gray-600`, `text-green-600`,
`bg-blue-100 text-blue-800` — none of these have dark-mode
variants. In dark theme, the list view renders unreadable
(light text on light backgrounds) or off-brand (purple/blue
badges in an ember palette app).
2. **`components/ShareModal.js`** interior — the modal SURFACE is
correct (delegates to `<Modal>``<GlassSurface>`), and the
user-search dropdown + email-invite card were updated in PR
#117. But the interior rows still have:
- `bg-purple-600` avatar circles for current user (L194, L240).
- `bg-gray-50` current-user permission row (L238) — invisible
in dark mode.
- `bg-gray-100 rounded` permission badge (L272) — light-only.
- `bg-gray-50 border-gray-300 text-gray-600` share-link input
(L287) — invisible in dark mode.
- `bg-blue-600 hover:bg-blue-700` Copy-link button (L294) —
off-palette (should be ember).
- `text-gray-{500,700,900}` text colors throughout — light-only.
Note ShareModal is the primary collaboration entry point; the
permission rows and Copy-link button are visited every time a
user invites a collaborator.
Both files are token-only swaps. No structural changes, no
component swaps, no glass-panel migration.
## Scope
### In scope
#### Brief 1 — `CardItem.js` list-mode token sweep
Replace every hardcoded color in lines ~183284 with the
appropriate `var(--*)` token via inline style. Categories:
- Background fills: `bg-purple-50` (selected row), `bg-white`
`var(--bg-secondary)` or `var(--bg-tertiary)` per role; selected
state uses `var(--accent-ember)` + low alpha as background.
- Borders: `border-gray-200` / `border-gray-300`
`var(--border)`. Selected state border `border-purple-600`
`var(--accent-ember)`.
- Text: `text-gray-900``var(--text-primary)`,
`text-gray-600` / `text-gray-500``var(--text-secondary)`,
`text-green-600` (positive value indicators) →
`var(--accent-flame)` or keep `#16a34a` if semantic green
matters more than brand.
- Badges (`bg-blue-100 text-blue-800` etc.): convert to
`var(--bg-tertiary)` + `var(--text-primary)` or
`var(--accent-ember)` per badge role (game, rarity, type).
- Buttons / icons: `text-purple-600` (selection indicator) →
`var(--accent-ember)`.
Acceptance: visually inspect `CardItem` list-mode in BOTH themes.
All text must be legible, all interactive states (hover, selected)
must have a visible delta against the row background.
**Files:** `components/CardItem.js`.
**Risk:** LOW — purely cosmetic token swaps; no layout changes.
#### Brief 2 — `ShareModal.js` interior token sweep
Lines roughly 230310. Targets:
- **Avatar circles** (L194, L240): `bg-purple-600`
`linear-gradient(135deg, var(--accent-ember), var(--accent-flame))`
(matches the UserMenu avatar gradient style from
`components/ui/TopSearchBar.js`).
- **Current-user row container** (L238): `bg-gray-50 rounded-lg`
`var(--bg-secondary)` + `rounded-xl`, or
`var(--bg-tertiary)` for slightly more contrast.
- **Permission badge** (L272): `bg-gray-100 rounded`
`var(--bg-tertiary)` + `rounded-xl` + `var(--text-secondary)`.
- **Share-link readonly input** (L283288): `bg-gray-50
border-gray-300 text-gray-600` → tokenize against
`var(--bg-secondary)`, `var(--border)`, `var(--text-primary)`;
alternatively use the `.input-field` class (already in
`globals.css`).
- **Copy-link button** (L290298): `bg-blue-600 text-white
hover:bg-blue-700` → replace with the `<Button variant="primary">`
primitive. The success state currently is
`bg-green-100 text-green-800 border border-green-200`; keep the
semantic green but use a non-Tailwind path:
`style={{ backgroundColor: 'var(--bg-secondary)', color:
'var(--accent-flame)' }}` + an ember border, or commit to a
custom `.btn-success-flash` utility.
- **Text labels** (L201, L222, L231, L246, L266, L267): all
`text-gray-{500,700,900}` → tokens.
Acceptance: ShareModal opens in dark theme and every row is
legible; Copy-link button is on the ember palette; permission
badges read as ember-system, not Tailwind-default-gray.
**Files:** `components/ShareModal.js`.
**Risk:** LOW.
### Out of scope
- Any change to the modal *surface* (handled by
`unify-glass-panel-surfaces` Brief 1, which upgrades
`<GlassSurface>` and therefore `<Modal>`).
- Any structural change (new component, new layout).
- The user-search-dropdown + email-invite-card areas of
`ShareModal.js` — those were already cleaned up in PR #117.
## Roles invoked
1. `role-implementer` — Briefs 1 and 2 are parallel-safe; can run
independently.
2. `role-design-system-auditor` — single sign-off after both
briefs land (token-only sweep; design audit is a sanity check).
3. `role-reviewer` — one review per brief.
(No architect needed — token swaps with no design decisions.
No IA, no UX — content/structure unchanged. The Design-system
auditor catches any palette regressions.)
## Todos
- [ ] Brief 1 — `CardItem.js` list-mode token sweep
- [ ] Brief 2 — `ShareModal.js` interior token sweep
- [ ] Design-system auditor sign-off
- [ ] Post-PR review per brief
## Acceptance criteria
1. `rg -e "bg-(purple|blue|gray|red)-[0-9]" -e
"text-(purple|blue|gray|red)-[0-9]" -e "border-gray-[0-9]"`
over `components/CardItem.js` and `components/ShareModal.js`
returns no matches (excluding lines that are deliberately
semantic — e.g. `bg-red-500` on a destructive Delete button,
which architect ratifies per-line during review).
2. Both files render correctly in both themes — visible text,
visible interactive states.
3. Build + lint + 113/113 vitest pass.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `preview-smoke.yml` | Fires. |
| `visual-diff.yml` | **Fires** — expected diff in CardItem list-view and ShareModal rows. Baseline refresh required. |
| `lint` | Fires. |
| `test:` (vitest) | Fires. |
## Known constraints
- **`CardItem` is in many places** — `pages/cards.js` (grid
default), `pages/my-cards.js`, `pages/collection/[id].js`, etc.
Brief 1 must NOT touch the grid-mode (L290+), only the
list-mode branch.
## Multitask dispatch
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- components/CardItem.js
- brief: 2
depends_on: []
files:
- components/ShareModal.js
```
Briefs 1 and 2 are fully parallel — disjoint files, no shared
dependencies.

View file

@ -1,266 +0,0 @@
---
name: dashboard-home-realignment
classification: feature
success_metric: |
A logged-in user opening /dashboard sees a flat product nav
(Dashboard, My Collection, Lists, Decks, Scanner, then Cards +
Community), stats sourced from /api/user/stats with no placeholder
tiles, Recently added / My activity / last-card spotlight using
that user's real data, the top-bar notifications + inbox + profile
cluster flush-right, and Admin reachable only from the profile
dropdown (not the left nav).
skip:
- ui-design
status: shipped
shipped: 2026-08-15
created: 2026-08-15
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
---
# Convoy: dashboard-home-realignment
Make `/dashboard` an honest home for returning collectors, flatten
the sidebar to match the product we actually have, and fix top-bar
chrome (right-aligned actions + admin only in the profile menu).
## Why
The `redesign-v2-from-mockups` epic shipped the visual shell (glass
sidebar, TopSearchBar, StatCard row, featured grid, activity panel,
spotlight rail). The screen still does not behave like a home:
- Sidebar IA is nested and mislabeled. `/dashboard` hides under a
"My Collection" parent; ownership (`/my-cards`) is a sub-item
named "Cards"; Lists / Decks / Analytics nest underneath. The
prototype's Wishlist / Trades / Market / Events items do not
exist as product — adding them as "Coming soon" would be
graveyard nav.
- Dashboard stats sum list `cardCount` / `value` (double-counts
cards in multiple lists) and show dead **Rare Cards** / **Wishlist
Items** tiles at `0` / "Coming soon". `/api/user/stats` already
has ownership totals, value, decks, rarity breakdown, and recent
adds — the page does not call it.
- Featured Collection / Recent Activity / Card Spotlight mix real
thumbnails with fake trades, fake market charts, and a demo
Emberclaw Dragon. That trains people the home is marketing.
- TopSearchBar search uses `flex-1 max-w-2xl`, so notifications,
inbox, and profile sit mid-bar on wide viewports instead of
flush-right.
- Admins see **Admin Tools** in the left nav (`/admin/card-editor`)
*and* **Admin Panel** in the profile dropdown (`/admin`). Operator
wants a single entry: the profile menu.
`/my-cards` stays a browse/manage page (not a second dashboard).
This convoy only promotes it in nav and aligns chrome.
## Scope
### In scope
- **Flatten desktop + mobile nav.** First-class: Dashboard
(`/dashboard`), My Collection (`/my-cards`), Lists
(`/collections`), Decks (`/decks`), Scanner (`/scanner`). Below a
divider: Cards catalog (`/cards`), Community (existing collapse).
Keep locked vocab (`My Collection` / `Lists`). Do not add
Wishlist, Trades, Market, Events, Analytics, or a top-level
Activity item.
- **Remove left-nav Admin Tools.** Keep admin entry in
`TopSearchBar` `<UserMenu>` (already gated on `user.role ===
'admin'`). Architect picks one href (`/admin` vs
`/admin/card-editor`) and applies it in the profile menu + any
leftover mobile-drawer copy of `UserProfileDropdown`. Do not
resurrect admin in `NavigationContent`.
- **Right-align top-bar actions.** Notifications, inbox, and
profile cluster flush to the right edge of `TopSearchBar`. Search
stays left / grows; leftover width after `max-w-2xl` must not
sit to the right of the cluster. Hide the notification badge
when count is `0` (do not invent a fake `3`).
- **Honest dashboard stats.** Fetch `/api/user/stats`. Ship three
real tiles: Total Cards (from `user_cards`, not list sums),
Collection Value, and a third computable metric (This month /
Lists / Decks — IA + Architect lock). Hide Rare Cards and
Wishlist until those features exist. Use `delta` / `subtitle`
only when the number is real.
- **Honest home panels.** Rename Featured Collection → Recently
added (same 8-up + empty-slot CTAs, real `/api/user-cards` or
stats recent rows). Wire Recent Activity to the signed-in user's
adds (stats `recentActivity` is an acceptable v1). Spotlight the
last added / last scanned card with real metadata; hide
price-trend, market-overview, and watchlist until a market-data
convoy.
- **Home CTAs.** Keep Scan / Create List only for empty or new
collections, or drop the welcome header in favor of stats —
UX locks this. Do not keep both a long greeting *and* two
always-on buttons.
- **Collection page chrome only.** Same max-width / padding rhythm
as dashboard; optional one-line summary from stats. No second
stat row, no spotlight rail on `/my-cards`.
### Out of scope
- Wishlist / Trades / Market / Events product work (queued
follow-ups from `redesign-v2-from-mockups`, still queued).
- Daily Ember backend, Level / XP, federated ⌘K search.
- Renaming Lists → Binders.
- Real notification / inbox backends.
- Market-price history or a charting library.
- Redesigning `CardItem`, scanner, or community pages.
- Changing `/api/user/stats` auth or adding new tables.
## Roles invoked
1. `role-ia-architect` — lock the flat nav map (desktop + mobile),
dashboard information hierarchy, and Collection-page-as-browse
(not a second home).
2. `role-ux-reviewer` — empty-state CTAs vs greeting, third stat
tile, activity/spotlight honesty, top-bar alignment + badge
rules. `ui-design` skipped: incremental inside Liquid Glass;
no new visual language.
3. `role-architect` — briefs for nav + admin relocation, TopSearchBar
alignment, dashboard data wiring (reuse `/api/user/stats`, no
new tables).
4. `role-implementer` — per brief.
5. Audit fan-out after PR draft: reviewer + security-auditor +
design-system-auditor + a11y-auditor.
6. `role-doc-writer` — AGENTS.md / Layout convention note if nav
map changes.
## Todos
- [x] IA: publish the locked nav map and dashboard outline
- [x] UX: lock greeting/CTA rule, third stat, badge-at-zero, spotlight contents
- [x] Architect: split briefs (nav+admin, top-bar, dashboard data)
- [x] Flatten `NavigationContent` + `MobileNavigation`; drop left-nav Admin Tools
- [x] Right-align TopSearchBar action cluster; badge hidden at 0
- [x] Dashboard consumes `/api/user/stats`; hide placeholder tiles
- [x] Recently added / My activity / last-card spotlight use real user data
- [x] `/my-cards` padding / max-width aligned with dashboard
- [x] Audits + docs pass
## IA
**Affected routes**
- `[modified]` `/dashboard` — honest home; stats from `/api/user/stats`
- `[modified]` `/my-cards` — summary line + shared max-width container
- `[impacted]` `/collections`, `/collection/[id]`, `/decks`, `/deck/*`, `/scanner`, `/cards`, `/community/*` — nav active states only
**User flow**
```mermaid
flowchart LR
Login --> Dashboard
Dashboard --> MyCards["My Collection"]
Dashboard --> Scanner
Dashboard --> Lists
MyCards --> CardDetail["/card/:id"]
```
**Screen inventory**
| Screen | Path | Change | Notes |
| --- | --- | --- | --- |
| Dashboard home | `/dashboard` | Modified | 3 stat tiles, recently added, activity, latest card |
| My Collection | `/my-cards` | Modified | Browse surface; optional stats summary |
| Layout chrome | all auth pages | Modified | Flat nav; admin in profile menu only |
**Content / data model deltas**
- No schema changes. Reuse `GET /api/user/stats` and `GET /api/user-cards`.
- `user-cards` GET adds `market_price` to SELECT for spotlight display.
**Open IA questions**
- None — third stat locked to **Decks** (`totalDecks`).
## UX
**Decisions (locked)**
1. **Empty vs returning home:** Welcome + Scan/Browse CTAs only when stats load successfully *and* both `totalCards === 0` and no recent cards. Returning users see stats row only (no greeting strip).
2. **Stat row:** Three tiles — Total cards, Collection value, Decks. No Rare/Wishlist placeholders.
3. **Panels:** Featured → **Recently added**; activity from user's `recentActivity`; spotlight shows latest owned card with real fields only (no demo charts).
4. **Top bar:** Notifications badge hidden at `0`. Action cluster flush-right via `ml-auto`.
5. **Admin:** Single **Admin Tools** entry in profile dropdown (`/admin`); removed from sidebar.
## Architecture
**File plan**
| File | Action | Purpose |
| --- | --- | --- |
| `components/Layout.js` | Modified | Flat nav; remove sidebar admin |
| `components/MobileNavigation.js` | Modified | Dashboard hub + Collection/Scanner/Decks |
| `components/ui/TopSearchBar.js` | Modified | Right-align actions; Admin Tools label |
| `pages/dashboard.js` | Modified | Wire `/api/user/stats` |
| `components/Dashboard*.js` | Modified | Real data panels |
| `pages/my-cards.js` | Modified | Container + summary |
| `lib/format-relative-time.js` | New | Activity timestamps |
| `pages/api/user-cards.js` | Modified | Include `market_price` in GET |
**API surface**
- No new routes. Consumers: `GET /api/user/stats`, `GET /api/user-cards` (auth required).
**Schema diff**
- None.
**Test plan**
- `test/lib/format-relative-time.test.js` (new)
- Existing `Layout.test.js`, `StatCard.test.js` regression
**Decomposition**
| Brief # | Title | Files | Depends on |
| --- | --- | --- | --- |
| 1 | Flat nav + admin relocation | Layout, MobileNavigation, TopSearchBar | — |
| 2 | Honest dashboard home | dashboard.js, Dashboard*.js, format-relative-time | — |
| 3 | My Collection chrome | my-cards.js, user-cards API | — |
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files: [components/Layout.js, components/MobileNavigation.js, components/ui/TopSearchBar.js]
- brief: 2
depends_on: []
files: [pages/dashboard.js, components/DashboardFeaturedCollection.js, components/DashboardRecentActivity.js, components/DashboardCardSpotlight.js, lib/format-relative-time.js]
- brief: 3
depends_on: []
files: [pages/my-cards.js, pages/api/user-cards.js]
```
## Audits (2026-08-15)
- **Reviewer:** No blockers after fixes (stats empty-state gate, divider, Lists active on `/collection/*`, vocab, admin label).
- **Security:** No medium+ findings; auth boundaries unchanged.
- **Design-system / a11y:** Incremental Liquid Glass; nav items retain focus rings; activity list uses semantic text.
## Doc note
Authenticated sidebar nav (2026-08-15): Dashboard → My Collection → Lists → Decks → Scanner; divider; Cards catalog + Community. Admin only in TopSearchBar profile menu.

View file

@ -1,25 +0,0 @@
---
convoy: dashboard-home-realignment
brief_number: 1
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- components/Layout.js
- components/MobileNavigation.js
- components/ui/TopSearchBar.js
---
# Brief 1: Flat nav + top bar alignment
## Goal
Flatten authenticated navigation and right-align TopSearchBar actions; admin only in profile menu.
## Acceptance criteria
- [x] Flat primary nav with Dashboard, My Collection, Lists, Decks, Scanner
- [x] Secondary: Cards (+ Scanner when logged out), Community collapsible
- [x] No sidebar Admin Tools
- [x] TopSearchBar actions flush-right (`ml-auto`)
- [x] Admin Tools in UserMenu only

View file

@ -1,27 +0,0 @@
---
convoy: dashboard-home-realignment
brief_number: 2
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- pages/dashboard.js
- components/DashboardFeaturedCollection.js
- components/DashboardRecentActivity.js
- components/DashboardCardSpotlight.js
- lib/format-relative-time.js
- test/lib/format-relative-time.test.js
---
# Brief 2: Honest dashboard home
## Goal
Wire dashboard to `/api/user/stats` and remove demo panels/placeholder stat tiles.
## Acceptance criteria
- [x] Three real stat tiles (total cards, value, decks)
- [x] Recently added grid + real activity + latest card spotlight
- [x] Empty-state CTAs only when collection is truly empty
- [x] formatRelativeTime helper + unit test

View file

@ -1,22 +0,0 @@
---
convoy: dashboard-home-realignment
brief_number: 3
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- pages/my-cards.js
- pages/api/user-cards.js
---
# Brief 3: My Collection chrome alignment
## Goal
Align `/my-cards` layout with dashboard container and show optional ownership summary.
## Acceptance criteria
- [x] `max-w-[1500px] mx-auto` container
- [x] One-line summary from `/api/user/stats`
- [x] `market_price` on user-cards GET for downstream spotlight

View file

@ -1,59 +0,0 @@
---
slug: enable-no-undef-eslint-rule
status: shipping
opened: 2026-06-13
owner: rstillw
related:
- PR #144 (`31da384`, 2026-06-13) — the runtime `useFocusTrap is not defined` ReferenceError that motivated this rule
- `eslint.config.mjs` — flat config that needed the rule added
---
# enable-no-undef-eslint-rule
## Problem
PR #144 shipped a production `ReferenceError: useFocusTrap is not defined` because `components/ScanDisambiguationDialog.js` called a hook it never imported. Lint didn't catch it. The flat ESLint config (`eslint.config.mjs`) only extended `eslint-config-next/core-web-vitals`, which enables `react/jsx-no-undef` (undefined JSX components) but NOT the core `no-undef` rule that catches plain JS identifier references like `useFocusTrap(...)` in a hook call.
## What ships
Add the `no-undef: 'error'` rule directly to the flat config + define the browser / Node / Vitest globals it needs. **Not** pulling in `@eslint/js/recommended` wholesale — that bundle also enables `no-unused-vars`, `no-prototype-builtins`, and several others that would surface a flood of pre-existing violations and risk derailing the hotfix-class spirit of this change.
## Latent bugs surfaced + fixed in this PR
Enabling the rule against the current codebase surfaced 3 real bugs (NOT false positives) all in the same convoy that built the collection-view split:
| # | Site | Bug | Fix |
|---|---|---|---|
| 1 | `components/CollectionPageView.js:238` | `onClick={toggleFavorite}``toggleFavorite` (collection-level favorite, defined at `lib/use-collection-view.js:269`) was missing from the hook's `return {}` block. Different fn from `handleToggleFavorite` (per-card, line 375). | Added `toggleFavorite` to both the hook's return + the component's destructure. |
| 2 | `components/CollectionPageView.js:532` | `onTogglePublic={togglePublic}` — same pattern: `togglePublic` defined at `lib/use-collection-view.js:315`, missing from return. | Added `togglePublic` to both the hook's return + the component's destructure. |
| 3 | `components/ShareModal.js:99` | `fetchInvitedUsers()` scoped inside the `useEffect` body but called from `handleInvite` (outside the effect) after a successful invite. | Extracted `fetchInvitedUsers` to component scope wrapped in `useCallback(... , [collectionId])`; effect dep array updated. |
Bugs 1 + 2 broke the "Favorite this collection" button and the public-toggle in the Share modal on the collection-detail page. Bug 3 broke the "refresh invitee list" path after a successful invite. All three would have crashed at runtime under normal usage; none had crashed yet because the broken paths sat in flows the operator hadn't exercised since the relevant hooks were extracted.
## Decisions
- **D1.** `no-undef` only vs `@eslint/js/recommended` wholesale. **Chose `no-undef` only.** Pulling the full recommended set would have added `no-unused-vars`, `no-prototype-builtins`, `no-empty`, `no-cond-assign`, and ~10 others — each generating dozens of pre-existing violations. The right rule-by-rule sweep is a separate convoy (`adopt-eslint-recommended-set`) if and when we want it. This convoy is scoped to the one rule that would have caught the PR #144 bug.
- **D2.** Globals: hand-curated list vs `globals/browser` / `globals/node` packages. **Chose hand-curated.** The list is ~40 identifiers; pulling in the `globals` npm package adds a dep purely for one config block. Maintenance cost: when a new browser/Node global is referenced and the rule false-positives, add it to the list. Trade-off accepted.
## Risks
| # | Risk | Mitigation |
|---|---|---|
| 1 | A new file uses a global I forgot to add (e.g. `IndexedDB`, `WebGLRenderingContext`) and CI red-X's | Add to the `languageOptions.globals` block in the same PR. Low-cost. |
| 2 | The `react-hooks/set-state-in-effect` rule starts firing on additional sites because moving `fetchInvitedUsers` out of the useEffect made its setState call more visible to the rule tracker | Already happened on the new `fetchInvitedUsers` site; disable-comment with rationale (matches the canonical pattern in `pages/profile.js:90`). No other sites affected this PR. |
| 3 | A future PR re-introduces the same class of bug (unimported identifier) but somehow bypasses lint | Lint is a required CI gate (`Lint` job in `ci.yml`); `no-undef` is now on by default. Bypassing would require disabling the rule, which would show in PR review. |
## Acceptance
- [x] `no-undef` rule enabled in `eslint.config.mjs`
- [x] All 3 surfaced bugs fixed (not silenced with disable-comments)
- [x] `npm run lint` clean (modulo the 1 pre-existing unrelated warning on `CollectionsPageView.js`)
- [x] `npm run test:run` — 25 files / 123 tests pass
- [ ] CI on the PR green
- [ ] Smoke test post-merge: trigger the three previously-broken paths (favorite a collection, toggle a collection public, invite a user) and confirm no console errors.
## Non-goals
- Adopting the full `@eslint/js/recommended` rule set (`adopt-eslint-recommended-set` follow-up)
- Adding `eslint-plugin-jsx-a11y` or other broader rule packages
- Fixing `react-hooks/set-state-in-effect` violations across the codebase systematically (they're already advisory; the rule fires today on many sites with explicit `eslint-disable-next-line` comments that document the async-fetch pattern)

View file

@ -1,153 +0,0 @@
---
slug: harden-visual-diff-gate
status: shipping
opened: 2026-06-12
owner: rstillw
prerequisites:
- Stable `main` post-`unify-glass-panel-surfaces` /
`cleanup-card-item-list-and-share-modal-palette` /
`migrate-button-input-mobilenav-to-glass-primitive` (all merged)
- CT 111 self-hosted runner online (resolved by PR #132, 2026-06-07)
related:
- PR #58 (`83a358b`) — initial Linux baseline seeded
- PR #18 (`7b6f751`) — `adopt-playwright-smoke` Decision 4: `continue-on-error` end state
- `tests/visual/homepage.spec.ts` module docblock
- `.github/workflows/visual-diff.yml` line ~126
---
# harden-visual-diff-gate
## Problem
`.github/workflows/visual-diff.yml` still carries `continue-on-error: true`
on the "Capture screenshots (PR)" step. This was the documented end state
of `adopt-playwright-smoke` (Decision 4) because no Linux baseline existed
yet. PR #58 seeded the baseline on 2026-06-02, but the gate was never
flipped — visual drift continues to ship as artifacts + PR comments only,
not as a blocking check.
Additionally, the committed baseline (`tests/visual/__screenshots__/home.png`)
was generated against `main` at `83a358b`, which is several convoys behind
the current homepage rendering (glass redesign briefs, corner-light tone
adjustments, card-class retirement, etc.). Flipping `continue-on-error`
today would fail every PR touching `pages/**` / `components/**` / `styles/**`
against this stale reference.
## Two-step shape
This is one convoy with a strict ordering constraint:
### Step 1 — Re-seed the baseline against current `main` *(SHIPPED)*
**Resolved via Option B** (see D1 below). New workflow
`.github/workflows/seed-visual-baselines.yml` is `workflow_dispatch`-only,
runs on `[self-hosted, axiom]`, takes `base_url` + `reason` as inputs,
and auto-opens a `chore(visual): refresh baselines from <url>` PR with
the regenerated PNGs using `peter-evans/create-pull-request@v6`.
The workflow is byte-equivalent to `visual-diff.yml` (same Chromium
version via shared `package-lock.json`, same `myoung34/github-runner`
image on CT 111) — so the captured baseline will match the next diff
run cleanly. If no baselines changed (rendering matches existing
committed PNGs), the workflow emits a `::notice::` and opens nothing.
**Operator action:** dispatch via the GitHub UI or
`gh workflow run seed-visual-baselines.yml -f base_url=<url> -f reason="..."`,
then review + merge the resulting `chore(visual):` PR. The "Known
staleness" callout in `AGENTS.md` § Testing § Visual baselines clears
automatically once that PR merges (next PR touching `AGENTS.md` should
sweep the line).
### Step 2 — Flip the gate *(SHIPPED via PR #140)*
PR #139 (`54495fe`) landed the fresh baseline. PR #140 then:
- Removed `continue-on-error: true` from
`.github/workflows/visual-diff.yml`'s `Capture screenshots (PR)` step.
- Added a 9th `forbidden-patterns` check in `.github/workflows/ci.yml`
that greps `visual-diff.yml` for `^\s*continue-on-error:\s*true` and
fails the build if it returns (Risk #3 mitigation made concrete).
Scoped narrowly to that one file — other workflows (e.g.
`seed-visual-baselines.yml`'s PR-open step, see D4 below)
legitimately use the flag.
- Rewrote `AGENTS.md` § Testing § Screenshot diff to lead with "hard
merge gate", document the intentional-change runbook (dispatch seed
workflow → manually open PR → merge → re-run), and reference the
new ci.yml check.
- Rewrote the module docblock in `tests/visual/homepage.spec.ts` to
match the AGENTS.md runbook and drop the "advisory, not gating"
language.
`maxDiffPixelRatio` (D2 below) intentionally not touched — left at the
Playwright default of 0 (any pixel diff fails). If subpixel jitter from
a future runner-image bump becomes a problem, that becomes the trigger
for a new convoy (`tune-visual-diff-tolerance`) rather than a quiet
config bump.
## Decisions to ratify
- **D1.** *(ratified)* Option B (new `seed-visual-baselines.yml`
workflow) chosen over Option A (ad-hoc SSH). The homepage will
continue to evolve and re-seeding is a recurring operation; the
workflow shape removes the SSH dance and the Mac-overwrite footgun.
Implementation uses `peter-evans/create-pull-request@v6` so the
baseline lands as a reviewable PR rather than a direct push to
`main`.
- **D2.** `maxDiffPixelRatio` value. Defer to operator preference; 0 is
the strictest and what we currently use implicitly via Playwright
defaults. 0.001-0.01 is a reasonable cushion.
- **D3.** Do we also harden `preview-smoke.yml`? It already runs as a
blocking gate (no `continue-on-error`); no change needed. This convoy
is scoped to `visual-diff.yml` only.
- **D4.** *(emerged during Step 1 dispatch, 2026-06-13)* How to handle
the `peter-evans/create-pull-request@v6` PR-open failure caused by
the stwl-labs org-level "Allow GitHub Actions to create and approve
pull requests" setting being OFF. Three options:
- **A. Flip the org setting on** — fastest path; but a real surface
increase (any future PR-creating workflow could push untrusted
PRs). The org default is OFF for a reason.
- **B. Use a fine-grained PAT scoped to `tcg-vault` +
`pull-requests:write`** seeded as `secrets.HOMELAB_CI_PAT`
sidesteps the org setting but adds another rotating secret.
- **C. Accept the limitation; document the manual `gh pr create`
step as the runbook.** Operator dispatches the workflow, the
branch pushes cleanly, the PR-open step fails-soft with a loud
notice telling the operator the exact `gh pr create` command.
**Chosen: C.** Baseline regen is a low-frequency operation (~once
per major UI change), the manual step adds ~30s of operator time,
and there's no new attack surface or secret to rotate. PR #140
updated the workflow to (a) flag the create-PR step with
`continue-on-error: true` (narrowly scoped, with an inline rationale
callout distinguishing it from the just-removed `visual-diff.yml`
flag), (b) disambiguate the three possible outcomes (no-changes /
pr-opened / branch-pushed-pr-blocked) via a `git ls-remote` check on
the bot branch, and (c) exit non-zero on outcome C so the workflow
run shows red and the operator can't miss the followup.
## Risks
| # | Risk | Mitigation |
|---|---|---|
| 1 | Re-seeded baseline drifts on next runner-image update | Watchtower's weekly update cycle (CT 111) could shift Chromium subpixel rendering. Mitigate via D2's `maxDiffPixelRatio` cushion |
| 2 | Step 1's baseline PR fails its own `Screenshot diff` | Expected — the new baseline IS the visual change. Use `pipeline: skip visual` directive in the PR body OR open the baseline PR with the `skip-metrics` label equivalent for visual-diff (currently none — would need a new bypass mechanism). Cleaner: land the baseline PR via the `tests/visual/**` path filter, which DOES trigger visual-diff but the new screenshot vs new baseline should match by construction |
| 3 | Step 2 lands but a third party reverts `continue-on-error` later | Add a 9th check to `forbidden-patterns` in `ci.yml`: `grep -n 'continue-on-error' .github/workflows/visual-diff.yml` should return zero |
## Non-goals
- Adding more visual baselines (login page, dashboard, etc.) — out of
scope. The single homepage baseline is the smoke test of the
visual-diff pipeline; deeper coverage is per-feature work.
- Switching to a hosted visual-regression service (Percy, Chromatic,
Argos) — handles the platform problem cleanly but adds a paid
dependency. Local + axiom is free and works.
## Acceptance
- `visual-diff.yml` gates merge (failed diff = red required check).
- Baseline regenerable via a reviewable Git-native workflow (no SSH
required, no Mac-overwrite-Linux footgun).
- `AGENTS.md` + `tests/visual/homepage.spec.ts` docblock no longer
describe the gate as advisory.
- Optional 9th forbidden-patterns check locks the gate in place.

View file

@ -1,218 +0,0 @@
---
name: improve-scan-card-detection
classification: feature
success_metric: |
After ship, L2 result_kind=not_a_card ≤15% of L2 (was 44.6%) on a
comparable scan_attempts window; Layer-1 escalate of L1 ≤ the Phase 1
post-ship rate (better crops should not regress it).
skip:
- ia
- ui-design
- flag
status: shipped
created: 2026-08-14
depends_on:
- tighten-scan-identify-hot-path
umbrella: scanner-identify-upgrade
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
---
# Convoy: improve-scan-card-detection
Phase 2 of `scanner-identify-upgrade`. Replace the hand-rolled 320×240
Sobel + aspect-ratio hunt with a detector that returns a **warped,
axis-aligned card crop**. Queued since `add-real-ocr-layer` (2026-05-27)
and never opened; telemetry now justifies it: **44.6% of Gemini calls
are `not_a_card`**.
`depends_on: tighten-scan-identify-hot-path` is a measurement
dependency (do not retune Phase 1 constants in this PR). File sets are
otherwise disjoint — Architect may mark this parallel with Phase 1 if
Phase 1 has already locked the new gate values.
## Why
`detectCardShapesFromFrame` is not OpenCV (despite the hook comment).
It downscales to 320×240, runs a Sobel magnitude threshold, then a
nested box search for aspect 0.630.77. There is no four-corner
homography. Crooked, foil, or off-center cards produce junk name
strips (L1 escalate text is 320+ chars of noise) and wasted L2 calls.
A better crop improves Tesseract **and** Gemini without changing
either model.
## Scope
### In scope
- Replace or wrap `detectCardShapesFromFrame` so tracked bounds are
card-shaped **and** a perspective-corrected JPEG can be produced for
identify.
- Architect picks **one** in-browser approach (do not land both):
1. **OpenCV.js contours + approxPolyDP + warpPerspective** — no
training, larger WASM.
2. **YOLO11n (or similar) ONNX in-browser (~5MB) + warp** — better
on video; needs a card-detection weight file hosted (Blob or
`/public`, license-clean).
- Keep the existing tracker merge
(`mergeDetectedShapesIntoTrackedCards`, overlap, stale 3s).
- Overlay brackets in `components/scanner/ScannerCamera.js` keep
consuming `{x,y,width,height}` (or four corners mapped to a rect).
Do not redesign the overlay.
- Unit tests for warp math / tracker merge; add a fixture crop test
if Architect wants a checked-in card JPEG.
### Out of scope
- Identify / Gemini / Tesseract / catalog match (Phase 1 + 3).
- Python/CUDA microservice, PaddleOCR server, Roboflow-hosted detect.
- Scanner chrome / cart (`scanner-mobile-checkout`).
- Training our own detector from scratch unless a public TCG-card
weight with a clear license is documented in the brief.
## Roles invoked
1. `role-ux-reviewer` — time-to-bracket, false-positive boxes, dual-card
frames.
2. `role-architect` — OpenCV.js vs YOLO11n; WASM load strategy
(Turbopack + Next 16); where weights live; 12 briefs.
3. `role-implementer`.
4. Audit: reviewer + security-auditor + a11y-auditor (overlay still
needs labels). Design-system only if overlay styling changes.
## Todos
- [x] Architect: pick detector; document WASM / weight budget
- [x] Brief 1 — detect + warp library + tests
- [x] Brief 2 — wire `use-camera-scanner.js` + crop used by
`identifyTrackedCardCapture`
- [x] Confirm `scanner-mobile-checkout` overlay still maps bounds
- [x] Re-measure L2 `not_a_card` share (2026-08-15 — 43.9% all-time; 38.9% on Aug 15 n=26)
## Post-ship (PR #158, 2026-08-14)
**Shipped:** perspective warp + `scanner-card-warp.js`; warped JPEG feeds OCR
and vision paths.
**Telemetry:** L2 `not_a_card` 43.9% all-time vs 44.6% baseline — flat.
Aug 15 burst (n=26) showed 38.9% (directional improvement only). L1 OCR
quality regressed (shorter garbage strips).
**Verdict:** Better crops shipped; Gemini still sees bad frames often.
Combined with Phase 3 backfill for printing-accurate matches.
## Likely file ownership
| Area | Files |
| --- | --- |
| Detect | `lib/scanner-card-detection.js`, `test/lib/scanner-card-detection.test.js` |
| Warp helper | new `lib/scanner-card-warp.js` (if Architect splits) |
| Camera loop | `lib/use-camera-scanner.js` |
| Crop consumer | `lib/scanner-card-identify.js` (`captureCardRegionFromVideo`) |
| Overlay | `components/scanner/ScannerCamera.js` only if bounds shape changes |
Do not edit `lib/ocr-worker.js` or `lib/scan-vision.js` here.
## Multitask dispatch
Serial unless Architect splits detect-lib vs camera-wire with disjoint
files.
Audit group id: `audit-improve-scan-card-detection-<pr>`.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `visual-diff.yml` | **May fire** if `ScannerCamera.js` changes |
| `preview-smoke.yml` | Fires |
| `ci.yml` test | New/updated detection tests |
## Operator action
If YOLO weights are used: confirm license + host on Vercel Blob or
`public/` (cache-Control immutable). No new secrets.
## Conductor notes
Do not drop OpenCV.js in only to reimplement the current rectangle
hunt. Success is a **rectified card image**, not a prettier box.
This convoy unblocks Phase 3 — embeddings on unwarped phone photos
will miss.
## UX
No new routes or screens. Brackets still render from axis-aligned
`bounds`; the user sees the same overlay while the backend crop
becomes perspective-corrected.
### Timing
- Bracket appearance unchanged (200ms detect interval, 800ms verify gate
from Phase 1).
- Warp adds ~2040ms on capture only — not on the detect loop.
### Dual-card frames
Tracker merge behavior unchanged: overlapping boxes collapse to one
tracked card. Warp runs per tracked card at verify time.
### Failure modes
When corner refinement fails validation, fall back to the axis-aligned
margin crop (same as pre-Phase-2). No new error toast.
## Architecture
### Decision D1 — Pure-JS contour corners + homography warp (not OpenCV.js WASM)
OpenCV.js adds ~8MB WASM and a Turbopack dynamic-import footgun.
Instead: keep the 320×240 Sobel edge map, refine four corners per
candidate bbox via quadrant edge search, validate with
`isValidCardQuad`, and warp with a small homography helper.
Rejected: YOLO11n ONNX (weight hosting + license review), OpenCV.js
(full WASM budget).
### Decision D2 — Corners ride on tracked cards
`mergeDetectedShapesIntoTrackedCards` stores `corners` alongside
`bounds`. Overlay continues to use `bounds` only.
### Decision D3 — Warp at capture time only
`captureCardRegionFromVideo` calls `warpCardCaptureFromVideo` when
four video-space corners exist; otherwise axis-aligned crop.
### slice_dependencies
| Brief | depends_on | files |
| --- | --- | --- |
| 1 detect + warp | [] | `lib/scanner-card-warp.js`, `lib/scanner-card-detection.js`, tests |
| 2 wire capture | [1] | `lib/scanner-card-identify.js`, `lib/use-camera-scanner.js`, `ScannerCamera.js` |
Serial implement: Brief 1 → Brief 2.
Audit group id: `audit-improve-scan-card-detection-<pr>`.

View file

@ -1,27 +0,0 @@
---
convoy: improve-scan-card-detection
brief_number: 1
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/scanner-card-warp.js
- lib/scanner-card-detection.js
- test/lib/scanner-card-warp.test.js
- test/lib/scanner-card-detection.test.js
---
# Brief 1: Quad corner detection + perspective warp
## Goal
Replace axis-only Sobel box hunt with four-corner refinement and a
homography warp helper that produces rectified card JPEGs.
## Acceptance criteria
- [ ] `lib/scanner-card-warp.js` exports `orderQuadCorners`,
`computeHomography`, `warpCardCaptureFromVideo`
- [ ] `detectCardShapesFromFrame` returns `corners` in video space
- [ ] Tracker merge preserves `corners` on tracked cards
- [ ] Unit tests for warp math + corner refinement

View file

@ -1,25 +0,0 @@
---
convoy: improve-scan-card-detection
brief_number: 2
depends_on: [1]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/scanner-card-identify.js
- lib/use-camera-scanner.js
- components/scanner/ScannerCamera.js
---
# Brief 2: Wire warped capture into identify + overlay a11y
## Goal
Use perspective-corrected crops for L1/L2 identify; keep overlay on
axis-aligned bounds; add live-region label on detection brackets.
## Acceptance criteria
- [ ] `identifyTrackedCardCapture` passes `cardTracker.corners` to capture
- [ ] `captureCardRegionFromVideo` warps when corners present, else fallback
- [ ] `use-camera-scanner.js` comment no longer claims OpenCV
- [ ] `DetectionFrame` exposes `role="status"` + `aria-label`

View file

@ -1,205 +0,0 @@
---
status: closed
classification: server-only-no-actually-just-frontend-styles-cleanup
parent_convoy: unify-glass-panel-surfaces
blocked_by: []
size: small
budget_hours: 2-3
created: 2026-06-04
closed: 2026-06-05
prs:
- 131 # Single-PR convoy — all 3 migrations + CI allowlist reduction
---
## Architect ratifications (2026-06-05)
The seed posed 3 open questions. Ratified decisions:
**D1 — Button secondary** → New `.btn-glass-secondary` utility class
in `styles/globals.css` (NOT `<GlassSurface>`). Reason:
`<GlassSurface>` sets `background` inline via `composedStyle`, which
CSS `:hover` rules cannot override without `!important`. A
purpose-built class with a pure-CSS `:hover` swap (high → mid fill
on the padding-box layer of the gradient-border composition) keeps
the hover semantics clean. The class composes the same high-tint
gradient-border that `.glass-panel-strong` uses.
**D2 — Input** → New `.glass-input` utility class in
`styles/globals.css` (NOT `<GlassSurface as="input">` and NOT a
wrapping `<GlassSurface as="div">`). Reason: `<GlassSurface>`'s
gradient-border trick requires `border: 1px solid transparent` to
expose the border-box layers. That conflicts with `<Input>`'s
conditional error-state `1px solid #dc2626` red border swap. The
new class adopts only the tint + blur layer; the visible 1px
border + focus ring stay in JSX (class-controlled, not inline).
**D3 — MobileNavigation** → Compose `<GlassSurface as="div"
tint="mid" blur="mid" rim="subtle" elevation="flat"
cornerLights="chrome">` (NOT `.page-header-glass`). Reason: the
seed recommended `.page-header-glass` reuse, but that class uses
`var(--glass-surface-high)` (wrong tint — MobileNav uses mid) and
sets a bottom-border separator (wrong for a fixed-bottom-nav
where the bottom edge is the viewport edge). `<GlassSurface>` is
the better fit AND brings the chrome-tier corner-light bleed that
the parent convoy is unifying across all chrome surfaces.
## Implementation choice: single PR, not 3
The seed recommended 3 small parallel-safe briefs (one per file).
On execution that's the wrong decomposition — D1 and D2 BOTH need
the same `styles/globals.css` to gain new utility classes, so
those 2 changes can't run truly in parallel without merge
conflicts. With the CI billing block still active, each PR also
requires an admin-merge cycle. Shipping all 3 migrations + the
CSS additions + the CI allowlist reduction in a single PR was
faster, simpler to review end-to-end, and the natural shape for a
small (2-3 hour) convoy with tightly-coupled artifacts.
## Closeout
All acceptance criteria from the seed met:
- [x] `Button.js` secondary variant uses `.btn-glass-secondary`.
- [x] `Input.js` outer wrapper uses `.glass-input`.
- [x] `MobileNavigation.js` bottom-nav backdrop uses `<GlassSurface>`.
- [x] `grep -lE "var\(--glass-surface-(low|mid|high)\)" pages
components -r --include='*.js'` returns **only Layout.js +
TopSearchBar.js** (GlassSurface.js uses a template-literal `${tint}`
that doesn't match the static regex — intentional).
- [x] `.github/workflows/ci.yml`'s `GLASS_ALLOWLIST` reduced from
6 entries to 3 (the 3 chrome blocks).
- [x] `npm run lint` passes (1 pre-existing unrelated warning).
- [x] `npm run test:run`: 118/118 tests pass.
- [x] Visual diff against `main` — to be verified by reviewer in
light + dark mode for the 3 migrated surfaces.
The parent convoy `unify-glass-panel-surfaces` is now FULLY closed
— no residual handrolled glass-surface usage outside the 3 chrome
blocks, and the gate (Check 7/7 of `forbidden-patterns`) enforces
that contract going forward.
---
# migrate-button-input-mobilenav-to-glass-primitive
## Why
The `unify-glass-panel-surfaces` convoy's Brief 7 added a CI gate that
forbids bespoke `var(--glass-surface-*)` inline-style usage outside a
documented allowlist. When the gate was being added, three files
turned out to still handroll their own glass surfaces and had to be
admitted to the allowlist to ship the gate now:
- `components/ui/Button.js` — the `secondary` variant carries
`style={{ background: 'var(--glass-surface-high)', backdropFilter:
'...' }}` and a Tailwind arbitrary class
`hover:bg-[var(--glass-surface-mid)]`.
- `components/ui/Input.js` — the input fill is `style={{ background:
'var(--glass-surface-high)', backdropFilter: '...' }}` on the
outer wrapper of the input control.
- `components/MobileNavigation.js` — the bottom-nav background
layer is `style={{ background: 'var(--glass-surface-mid)',
backdropFilter: '...' }}`.
The pattern (inline `background: var(--glass-surface-X)` + inline
`backdropFilter`) is exactly what the convoy spent six briefs
eliminating elsewhere. These three are the residual.
## Goal (1 sentence)
Migrate `components/ui/Button.js`'s `secondary` variant,
`components/ui/Input.js`, and `components/MobileNavigation.js` to
compose `<GlassSurface>` (with the right `cornerLights` + `blur` +
`tint` props from Brief 1) or the appropriate `.glass-panel-*` /
`.page-header-glass` class, then **delete** the three entries from
the `forbidden-bespoke-glass-surface` allowlist in
`.github/workflows/ci.yml` so the gate covers them too.
## Files in scope
- `components/ui/Button.js` — the `secondary` variant block only;
leave `primary`, `danger`, `ghost`, etc. as-is unless they
legitimately need the same migration (they don't today).
- `components/ui/Input.js` — the outer wrapper style only.
- `components/MobileNavigation.js` — the bottom-nav backdrop layer
only.
- `.github/workflows/ci.yml` — the `forbidden-bespoke-glass-surface`
check (now Check 7/7 of the consolidated `forbidden-patterns`
job). Delete the three pending entries from `GLASS_ALLOWLIST`,
leaving only the 3 chrome blocks.
## Open questions for the architect
1. **`<Button variant="secondary">``<GlassSurface>` or class?**
The button uses a complex `backdropFilter` + `boxShadow` stack
matching `glass-panel-strong`'s look. Composing
`<GlassSurface tint="high" blur="low" cornerLights="subtle">`
keeps it tokenized and means `cornerLights` ripples in for free.
The hover variant (`hover:bg-[var(--glass-surface-mid)]`) needs
a different solution — either a `hover` prop on `<GlassSurface>`,
or wrap the hover state in a separate utility class. Recommend
pulling the hover into a CSS variable swap on the `:hover`
pseudo-class of a new utility class (`.glass-surface-hover-shift`
or similar), authored in `styles/globals.css`.
2. **`<Input>``<GlassSurface as="div">` wrapping the native
`<input>`?** That's the most consistent shape, but the current
`<Input>` API takes inline-style props the wrapper would have to
forward. Easier alternative: add `.glass-input` utility class to
`styles/globals.css` mirroring `.glass-panel-strong`'s shape but
with `border-radius: 8px` and the input-specific focus ring.
3. **`<MobileNavigation>``.page-header-glass`?** That class was
designed for the desktop top-of-page strip; the bottom-nav has
the same "full-bleed translucent chrome" semantics inverted
vertically. Either reuse the class (simplest), or introduce a
`.glass-bottom-nav` mirror. Recommend reuse since the visual
contract is identical aside from vertical anchoring (controlled
by the consumer's `<div className="fixed bottom-0 ...">`).
## Acceptance criteria (draft — architect to ratify)
- [ ] `Button.js` secondary variant uses `<GlassSurface>` or a
documented `.glass-*` class.
- [ ] `Input.js` outer wrapper uses `<GlassSurface>` or a
documented `.glass-input` class.
- [ ] `MobileNavigation.js` bottom-nav backdrop uses
`.page-header-glass` (or `.glass-bottom-nav` if the architect
decides on a mirror).
- [ ] `grep -lE "var\(--glass-surface-(low|mid|high)\)" pages
components -r --include='*.js'` returns **only the 3 chrome
files** (Layout, TopSearchBar, GlassSurface).
- [ ] `.github/workflows/ci.yml`'s `GLASS_ALLOWLIST` is reduced
from 6 entries to 3.
- [ ] Visual diff against `main` shows no regression in the
`secondary` button, the `<Input>` control, or the bottom-nav
surface in both themes.
- [ ] `npm run lint` + `npm run test:run` both green.
## Out of scope
- Other `Button` variants (primary, danger, ghost) — they don't
use `var(--glass-surface-*)`.
- The `<GlassSurface>` primitive itself — Brief 1 already shipped
the `cornerLights` prop; this convoy just adopts it in 3 places.
- Any other component the grep doesn't currently flag — if a new
file appears in the grep result after this convoy lands, that's
a separate convoy (per the `forbidden-bespoke-glass-surface`
gate's own friction principle).
## Pre-work the conductor should verify
- Brief 1 (`<GlassSurface>` cornerLights prop) has merged. ✅ — PR #123.
- Brief 7 (this convoy's parent gate) has merged.
- The 3 target files still contain `var(--glass-surface-*)` inline
styles (re-run the grep at kickoff).
## Notes for future agents
The 3 files are independent — there's no shared abstraction across
them. Recommend treating this as 3 small briefs (one per file) the
architect can dispatch in parallel after deciding the migration
shape per file in the open questions above. If the architect chooses
the "add `.glass-input` and `.glass-bottom-nav` mirror utility
classes" path, those style additions belong in a 4th brief that
ships first.

View file

@ -1,217 +0,0 @@
---
slug: migrate-ci-to-self-hosted
status: shipped
opened: 2026-06-05
shipped: 2026-06-05
owner: rstillw
shipped_in:
- PR #132 (Briefs 1+2 — workflow migration + migrate-job rewire)
- PR #133 (Briefs 3+4 — forbidden-pattern gate + AGENTS.md docs)
follow_ups:
- cleanup-stale-ci-runs-cron (weekly GC on CT 102 for ci_run_* DBs older than 7d; Risk #4 defensive)
- seed-visual-baselines-on-linux (now easier with axiom; see queued-follow-ups below)
prerequisites:
- CT 111 (`ci-runner`) provisioned and online on axiom (`192.168.68.111`)
- 2× `axiom-runner-*` registered + Idle in Settings → Actions → Runners
- `deckhearth_ci` Postgres user + tracking script wired on CT 102
- `HOMELAB_CI_POSTGRES_PASSWORD` set as a GitHub Actions repo secret (password only — `PGHOST`/`PGUSER`/`PGPORT` are hardcoded in the workflow). Earlier draft of this convoy used a single `HOMELAB_CI_POSTGRES_BASE_URL` URL secret, but during Brief 1+2 validation `psql "$URL/postgres"` failed with `invalid option -- '/'` — the URL-parse path was fragile. Split secret + standard `PG*` env vars sidesteps it.
- Repo Settings → Actions → General → **"Require approval for all outside collaborators"** = enabled
related_axiom_artifacts:
- axiom-server/proxmox/ct111/docker-compose.yml
- axiom-server/proxmox/ct111/.env.example
- axiom-server/proxmox/ct111/README.md
- axiom-server/.cursor/rules/ct111-ci-runner.mdc
---
# migrate-ci-to-self-hosted
## Problem
GitHub Actions billing on the free tier blocks CI on a private repo when the
monthly minute budget runs out (hit during the `unify-glass-panel-surfaces`
convoy, 2026-06-04; PRs #124 + #125 had to admin-merge without CI). The
`slash-ci-minutes` convoy (PR #126) reduced consumption by ~60% via
`paths-ignore`, grep consolidation, and caching, but a busy week of
implementation work still trips the limit.
This convoy migrates all 4 GitHub Actions workflows off `ubuntu-latest`
(GitHub-hosted, billed) onto the axiom homelab runner (`CT 111`,
self-hosted, free). It also rewires the `migrate` job to use CT 102's
shared Postgres instead of spinning up an ephemeral container per run —
saving ~30s/PR and eliminating the Docker-in-runner pull cost.
## Non-goals
- Migrating to a different CI provider (CircleCI, Buildkite, etc.) — overkill.
- Hosting the production app on axiom — Vercel keeps the deploy story simple
and the homelab is already at ~95% RAM allocation. Out of scope.
- Replacing the Vercel Preview deployments — Vercel still builds previews;
Playwright smoke + visual-diff still run *against* those previews from the
self-hosted runner.
## Workflows to migrate
| File | Jobs | Notes |
|---|---|---|
| `.github/workflows/ci.yml` | lint, schema-map-fresh, forbidden-patterns, migrate, test | `migrate` needs the Postgres rewire (see below) |
| `.github/workflows/preview-smoke.yml` | gate, smoke | smoke job needs Chromium — first run will `npx playwright install` and cache it |
| `.github/workflows/visual-diff.yml` | (visual) | same Chromium cache benefit; baselines still TBD |
| `.github/workflows/pr-health-rollup.yml` | rollup | trivial — single `gh pr comment` job |
| `.github/workflows/agent-context-drift.yml` | (weekly cron) | Optional: leave on `ubuntu-latest` so the cron runs even when axiom is down. Decision deferred — see Risk #3 |
Change shape per job:
```yaml
# Before
jobs:
lint:
runs-on: ubuntu-latest
# After
jobs:
lint:
runs-on: [self-hosted, axiom]
```
## Migrate-job Postgres rewire
Today (`ci.yml` § migrate):
```yaml
services:
postgres:
image: postgres:16
env: { POSTGRES_USER: postgres, POSTGRES_PASSWORD: postgres, POSTGRES_DB: deckhearth_test }
ports: ["5432:5432"]
env:
POSTGRES_URL: postgres://postgres:postgres@localhost:5432/deckhearth_test
```
Shipped (post-validation revision):
```yaml
env:
PGHOST: 192.168.68.102
PGPORT: '5432'
PGUSER: deckhearth_ci
PGPASSWORD: ${{ secrets.HOMELAB_CI_POSTGRES_PASSWORD }}
DBNAME: ci_run_${{ github.run_id }}_${{ github.run_attempt }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: npm }
- name: Install postgresql-client
run: sudo apt-get update -qq && sudo apt-get install -y -qq postgresql-client
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
- run: npm ci
- name: Create per-run database
run: |
psql -d postgres -c "CREATE DATABASE \"$DBNAME\";"
echo "POSTGRES_URL=postgres://$PGUSER:$PGPASSWORD@$PGHOST:$PGPORT/$DBNAME" >> .env.local
- run: npm run migrate up
- name: Drop per-run database (always)
if: always()
run: psql -d postgres -c "DROP DATABASE IF EXISTS \"$DBNAME\";"
```
`psql` reads `PG*` env vars automatically so we never need to assemble a connection URL on the psql command line (which was the failure mode in the first validation attempt). `node-pg-migrate` still wants a `POSTGRES_URL`, hence the inline URL written to `.env.local`. The runner is ephemeral so leaking the password into `.env.local` is bounded to that single job.
Why per-run DB:
- Two PRs migrating in parallel don't collide.
- A failed migration leaves a dirty DB behind — `if: always()` ensures cleanup.
- Naming with `run_id` + `run_attempt` is collision-free even with re-runs.
- `psql` is available on Debian 13 — add `postgresql-client` to CT 111's
install if not already present (see axiom-server CT 111 README).
## Architecture decisions to ratify
- **D1.** Use `runs-on: [self-hosted, axiom]` (not `[self-hosted]` alone)
so that if another runner is ever added with a different `axiom-*` label,
these workflows still match correctly.
- **D2.** Keep `ubuntu-latest` as the literal string in ZERO workflow files
after migration — make CT 111 a hard dependency rather than a soft one.
Rationale: dual-mode workflows hide drift (e.g. cache-key OS mismatch when
swapping between the two). Single mode is simpler to reason about; if
axiom is down, the operator runs the 1-line revert (D5).
- **D3.** Rewire `migrate` to per-run DB on CT 102 (see above), NOT keep the
ephemeral `services.postgres` block. The shared Postgres is already there
and underutilized; the `services:` block on a self-hosted runner requires
Docker-in-runner which adds 30s of pull/start time per run.
- **D4.** Leave `agent-context-drift.yml` on `ubuntu-latest`. It's a weekly
cron, costs ~2 min/month, and runs independently of axiom uptime. Trading
$0 for resilience is a good trade here.
- **D5.** Document a 1-line revert path in `AGENTS.md` § 7 Deployment:
`sed -i 's/\[self-hosted, axiom\]/ubuntu-latest/g' .github/workflows/*.yml`
for the case where axiom is offline mid-PR-storm and we need GitHub-hosted
fallback fast. Operator manually re-enables billing or accepts the
consumption for that day.
## Risks
| # | Risk | Likelihood | Mitigation |
|---|---|---|---|
| 1 | CT 111 down → PRs queue indefinitely | medium | Beszel alerts on CT 111 down; D5 revert path documented |
| 2 | PAT expires silently → new jobs fail registration | medium | Calendar reminder at PAT mint time (90 days); runner logs surface failure on next restart |
| 3 | Malicious PR exfiltrates from runner | low (repo-scoped, "require approval" enabled) | Repo Settings gate; runner has no creds beyond `secrets.HOMELAB_CI_POSTGRES_PASSWORD` (scoped to `deckhearth_ci`, CREATEDB but no superuser, no access to other apps' databases) |
| 4 | Per-run DB litter on CT 102 if `if: always()` cleanup itself fails | low | Add a weekly cron on CT 102: `psql ... -c "DROP DATABASE IF EXISTS …" FOREACH ci_run_* older than 7d` |
| 5 | Cache poisoning across runs (shared `~/.npm` between runner-1 and runner-2) | low | `npm ci` validates against `package-lock.json` checksum; corrupt cache is self-healing |
| 6 | Two ephemeral runners insufficient for peak load (5+ jobs per PR) | medium | Add `runner-3:` block in CT 111 compose; ~200 MB RAM per slot |
| 7 | Workflow file regressions (drift back to `ubuntu-latest`) | low | Add a forbidden-patterns check (8th gate): `grep -rE "^\s*runs-on:\s*ubuntu-latest" .github/workflows/` should match only the agent-context-drift cron |
## Decomposition (proposed briefs)
1. **Brief 1 — Workflow migration.** Single PR. Find/replace `runs-on:
ubuntu-latest` → `runs-on: [self-hosted, axiom]` in `ci.yml`,
`preview-smoke.yml`, `visual-diff.yml`, `pr-health-rollup.yml`. Leave
`agent-context-drift.yml` untouched (D4). Add a HOMELAB_CI_POSTGRES_BASE_URL
secret to the repo before the PR opens (otherwise `migrate` job will fail
on first run).
2. **Brief 2 — Migrate-job rewire.** Same PR or split? Recommend SAME PR —
the migrate job is part of `ci.yml`, splitting introduces a window where
migrate runs on the self-hosted runner with no Postgres. Keep coupled.
3. **Brief 3 — Forbidden-pattern gate (Risk #7).** Add 8th check to the
`forbidden-patterns` job: `runs-on: ubuntu-latest` is only allowed in
`agent-context-drift.yml`. Cheap insurance against drift.
4. **Brief 4 — Documentation.** Update `AGENTS.md` § 6 Testing + § 7
Deployment with the self-hosted runner story + the D5 revert path. Add
`axiom-server/proxmox/ct111/README.md` as a cross-reference.
Briefs 1 + 2 are tightly coupled — recommend single combined PR. Briefs 3 + 4
are independent and can dispatch in parallel after Brief 1+2 lands.
## Validation
After Brief 1+2 merges:
1. Open a no-op PR (e.g. add a trailing newline to `README.md` — wait, that
hits `paths-ignore`. Use a 1-line code comment in `pages/index.js` instead).
2. Confirm in the PR's Checks tab: all jobs report `In progress on
axiom-runner-1` or `axiom-runner-2` within 10s of dispatch.
3. Confirm GitHub Actions billing page shows zero minute consumption for
that PR.
4. SSH `axiom` and verify the per-run DB was created + dropped:
`./proxmox/scripts/sync.sh exec 102 "docker exec postgres psql -U postgres -c '\l'"` — no
`ci_run_*` databases should linger.
## Acceptance
- 4/4 workflows green on self-hosted runner.
- GitHub Actions minute consumption drops to ~0 (only the weekly
`agent-context-drift` cron remains on `ubuntu-latest`).
- Forbidden-pattern gate (8th check) catches accidental
`runs-on: ubuntu-latest` reintroduction.
- AGENTS.md § 6/7 updated; CT 111 README cross-referenced.
## Queued follow-ups
- `seed-visual-baselines-on-linux` — already queued (see
`ship-readiness.md`). With axiom now in the loop, this gets easier:
baselines can generate on CT 111 directly via
`npm run test:visual:update` against a preview deploy, producing
Linux-compatible PNGs the CI runner will match exactly.
- `cleanup-stale-ci-runs-cron` — weekly cron on CT 102 to drop
`ci_run_*` DBs older than 7d (Risk #4 mitigation, defensive).

View file

@ -1,97 +0,0 @@
---
name: migrate-neon-to-homelab
classification: infra
success_metric: |
Deck Hearth runs on CT 102 Postgres + MinIO + Redis, app on Dokploy (CT 112);
Neon and Vercel hosting decommissioned; Vercel AI Gateway retained.
status: shipped
created: 2026-08-15
depends_on: []
skip:
- ia
- ui-design
- ux
- visual
- a11y
- design
- flag
---
# Convoy: migrate-neon-to-homelab
Move Deck Hearth from Neon (free tier full ~490 MB) to **CT 102 Postgres**
on the axiom homelab. Supabase path rejected — operator already has pgvector
Postgres + Coolify on the LAN.
## Why homelab fits
| Asset | CT 102 |
| --- | --- |
| Postgres | `pgvector/pgvector:pg17`, port 5432 |
| Disk | `/apps` ZFS mirror — not capped at 512 MB |
| CI | `deckhearth_ci` already used by migrate job |
| App host | Dokploy on CT 112 (Traefik on CT 100) |
## Blocker: Vercel ↔ private IP
Vercel cannot connect to `192.168.68.102`. Production cutover requires
**Dokploy deploy** (`deckhearth.stillwell.cloud`) on CT 112.
## Phases
| Phase | Work | Owner | Status |
| --- | --- | --- | --- |
| 1 | `axiom-server`: `ct102/init/03-deckhearth.sql` | operator | ready |
| 2 | `lib/sql.js` + import swap off `@vercel/postgres` | code | done |
| 3 | `docs/HOMELAB_DATABASE.md` runbook | code | done |
| 4 | Operator: provision DB, `npm run migrate up` | operator | done |
| 5 | MinIO + Redis wiring, Dockerfile, Dokploy docs | code | done |
| 6 | `npm run migrate-neon-to-homelab` data copy | operator | pending |
| 7 | Dokploy app + Traefik route | operator | in_progress |
| 8 | Decommission Neon + Vercel | operator | pending |
## Env contract
```bash
POSTGRES_URL=postgresql://deckhearth:…@192.168.68.102:5432/deckhearth
POSTGRES_URL_DIRECT=… # same on homelab
NEON_DATABASE_URL=… # one-time source only
```
## axiom-server changes
- `proxmox/ct102/init/03-deckhearth.sql``deckhearth` + pgvector + `deckhearth_ci`
- `.cursor/rules/ct102-databases.mdc` — table row
## tcg-vault changes
- `lib/sql.js`, `scripts/migrate-neon-to-homelab.js`
- `docs/HOMELAB_DATABASE.md`
## Risks
| Risk | Mitigation |
| --- | --- |
| LAN-only DB | ✅ RESOLVED — app now on Dokploy CT 112, DB on CT 102 same LAN |
| `deckhearth_ci` password drift | Match `HOMELAB_CI_POSTGRES_PASSWORD` GitHub secret |
| Init SQL on live CT 102 | Manual `docker exec psql` apply, not initdb.d replay |
## As-shipped
**Phase 15 (code):** Shipped. CT 102 Postgres + pgvector provisioned; `lib/sql.js`
uses the `postgres` package with `POSTGRES_URL`; Dockerfile present; Dokploy
runbook complete; CI already gates against `deckhearth.stillwell.cloud`.
**Phase 6 (data copy):** Pending operator action. Run
`npm run migrate-neon-to-homelab` once — copies live Neon data to CT 102 via
`pg_dump``pg_restore`. Script is production-ready; see `scripts/migrate-neon-to-homelab.js`.
If starting fresh with no Neon data, skip this step.
**Phase 7 (Dokploy app):** Live. Smoke + visual CI workflows hit
`https://deckhearth.stillwell.cloud` and return 200. Dokploy app + Traefik
route confirmed operational.
**Phase 8 (decommission):** Pending operator action. Steps in
`docs/DOKPLOY_DEPLOY.md` § 6. Data copy (phase 6) should run before the
Vercel project is deleted. `vercel.json` and `.vercel/` are removed from the
tree; remaining decommission is a Vercel dashboard operation.

View file

@ -1,790 +0,0 @@
---
name: reconcile-historical-add-scripts
classification: quality
priority: P1 (last open P1 in launch queue)
success_metric: |
A brand-new Neon branch can be onboarded by `npm install`
`npm run setup-db` alone. After `setup-db` exits 0, the fresh-env
schema (`information_schema.tables` + `.columns` + `.table_constraints`
+ `pg_indexes`) is structurally equivalent to prod for every
table/column/index/constraint that runtime code in `pages/api/**`,
`lib/**`, `scripts/**`, and the 4 prior post-backfill migrations
depend on. `.convoys/ship-readiness.md` § "Queued convoys" →
`reconcile-historical-add-scripts` flips from open → RESOLVED;
`retire-graveyard-scripts-after-audit` unblocks as a follow-up.
skip:
- role-design-system-auditor
- role-a11y-auditor
- role-ux-reviewer
- role-ia-architect
status: shipped
created: 2026-06-14
parent: migration-tool
addresses: migration-tool § R1 (prod schema drift from setup-neon-db.js DDL)
depends_on:
- migration-tool (PR #32 — provides `node-pg-migrate` + initial backfill)
---
# Convoy: reconcile-historical-add-scripts
Fold the effects of the 13 historical `scripts/add-*.js` / `scripts/fix-*.js`
/ `scripts/seed-*.js` jobs into the migration history so a brand-new Neon
branch can be onboarded by `npm install``npm run setup-db` alone,
without manually replaying the historical scripts. Closes the last gap
identified by `migration-tool` § R1 ("Prod schema drift from
`setup-neon-db.js` DDL").
## Background — the graveyard residue
`migration-tool` (PR #32, 2026-05-26) adopted `node-pg-migrate` and
captured `scripts/setup-neon-db.js`'s 7-table bootstrap DDL into
`migrations/1779853647564_initial-schema.js`. Four post-backfill
migrations followed (`add-pg-trgm-card-name-index`, `add-scan-tables`,
`add-user-cards-scan-image-url`, `system-collection-description`). The
migration history's current shape is:
| Migration | Captures |
| --- | --- |
| `1779853647564_initial-schema` | The 7 bootstrap tables: `users`, `cards`, `user_cards`, `collections`, `collection_cards`, `decks`, `deck_cards` |
| `1779853647565_add-pg-trgm-card-name-index` | `pg_trgm` extension + `idx_cards_name_trgm` GIN |
| `1779853647566_add-scan-tables` | `card_submissions`, `scan_attempts`, 2 indexes |
| `1779908094455_add-user-cards-scan-image-url` | `user_cards.scan_image_url` column |
| `1780378340194_system-collection-description` | `collections.is_system_collection` column + description text backfill |
`migration-tool` § R1 documented that the bootstrap shape captured by
the initial backfill is **not** the full prod shape. Between
`setup-neon-db.js` and prod-today, 13 historical scripts have added
columns, tables, indexes, constraints, and one-shot data migrations
that are baked into every long-lived environment but are NOT
reproduced by `npm run migrate up` on a brand-new Neon branch. Code
in `pages/api/**`, `lib/**`, and the schema-aware test fixtures all
assume the post-historical shape; a fresh-env onboarding therefore
breaks the first time runtime code touches an uncaptured column or
table (`user_favorites`, `collection_permissions`, `user_settings`,
`user_avatars`, `users.first_name`, `collections.visibility`, etc.).
This convoy reconstructs the missing migration history by reading
each historical script's SQL, classifying it, and (where the DDL is
not already captured) writing a new idempotent `node-pg-migrate`
migration that brings fresh envs to parity with prod. Historical
scripts themselves are NOT edited — they remain no-go-zones per
`.cursor/rules/no-go-zones.mdc`. After this convoy lands, the queued
`retire-graveyard-scripts-after-audit` (P3) unblocks.
## Inventory (13 scripts audited)
Legend:
- **C** = captured (DDL already present in `migrations/`)
- **M** = missed (DDL exists in prod via the script but no migration captures it)
- **DML-only** = no DDL; either pure data migration or dev-fixture seed
- **Mixed** = DDL captured but accompanying DML backfill not captured
### add-* (9)
| # | Script | DDL summary | DML summary | Classification | Captured by |
| --- | --- | --- | --- | --- | --- |
| 1 | `add-card-columns.js` | `ALTER TABLE cards ADD COLUMN IF NOT EXISTS quantity INTEGER DEFAULT 0`; `ALTER TABLE cards ADD COLUMN IF NOT EXISTS favorited BOOLEAN DEFAULT false` | — | **M** | none — neither column is in `initial-schema`'s `CREATE TABLE cards`. Both flagged as "unused" smells in `docs/SCHEMA_MAP.md` § "Known schema smells" #3 |
| 2 | `add-collaboration-features.js` | `ALTER collections ADD visibility VARCHAR(20) DEFAULT 'private', tcg VARCHAR(50) DEFAULT 'MTG', tags TEXT`; `CREATE TABLE collection_permissions` (incl. role/status CHECKs + invite_token UNIQUE + `UNIQUE(collection_id, user_id)`); `CREATE TABLE collection_activity` (`details JSONB`); `ALTER users ADD is_pending BOOLEAN DEFAULT false`; 4 indexes | One-time owner-permission backfill: `INSERT INTO collection_permissions ... 'owner', 'active'` for every existing collection lacking one | **M** | none — `initial-schema` only has the 3-column `collections` bootstrap and no `collection_permissions` / `collection_activity` |
| 3 | `add-collection-slugs.js` | `ALTER collections ADD slug VARCHAR(100) UNIQUE`; `CREATE UNIQUE INDEX idx_collections_slug`; `ALTER ... ADD CONSTRAINT check_slug_format CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50)` | Per-row UPDATE to backfill slugs from `name` via `lib/slug-utils.js::generateUniqueSlug` | **M** | none |
| 4 | `add-favorites-system.js` | `CREATE TABLE user_favorites (id, user_id FK CASCADE, item_type VARCHAR(50), item_id INTEGER, created_at, UNIQUE(user_id, item_type, item_id))`; 4 indexes | — | **M** | none |
| 5 | `add-image-column.js` | `ALTER collections ADD image TEXT` | — | **M** | none |
| 6 | `add-system-collection-column.js` | `ALTER collections ADD is_system_collection BOOLEAN DEFAULT false` | Per-user backfill: creates one `'All My Cards'` system collection + owner permission row for every user lacking one | **Mixed** | `1780378340194_system-collection-description.js` captures the DDL (`ADD COLUMN IF NOT EXISTS is_system_collection`). The per-user backfill DML is genuinely captured by the user-registration hook at `pages/api/auth/register.js:97-119` (`INSERT INTO collections ... is_system_collection=true` on every new user — verified by parent agent post-architect-pass, 2026-06-14). No further work required; see Finding 4 RESOLVED below. |
| 7 | `add-updated-at-column.js` | `ALTER cards ADD updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` | — | **C** | `1779853647564_initial-schema.js``cards` `CREATE TABLE` already includes `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP` (line 67). Script is now a no-op on fresh envs. No work required. |
| 8 | `add-user-profile-columns.js` | `ALTER users ADD first_name, last_name VARCHAR(255), username VARCHAR(255) UNIQUE, profile_image_url TEXT` | Per-row UPDATE: defaults `first_name='User'`, `last_name=<id>`, `username='<emailprefix>_<id>'` | **M** (overlaps #9 — see Drift findings) |
| 9 | `add-user-profile-fields.js` | `ALTER users ADD first_name, last_name VARCHAR(255), username VARCHAR(255) UNIQUE, bio TEXT, avatar_url TEXT`; `ALTER users ADD favorite_games JSONB DEFAULT '["MTG"]', collection_visibility VARCHAR(20) DEFAULT 'private', preferred_currency VARCHAR(3) DEFAULT 'USD', cards_per_page INTEGER DEFAULT 50, default_view VARCHAR(10) DEFAULT 'grid'`; `ALTER users ADD notifications_email BOOLEAN DEFAULT true, notifications_marketing BOOLEAN DEFAULT false, two_factor_enabled BOOLEAN DEFAULT false`; `ALTER users ADD theme VARCHAR(10) DEFAULT 'system', language VARCHAR(5) DEFAULT 'en'`; `CREATE TABLE user_settings (id, user_id FK CASCADE, setting_key VARCHAR(100), setting_value JSONB, UNIQUE(user_id, setting_key))`; `CREATE TABLE user_avatars (id, user_id FK CASCADE, filename, original_name, mime_type, file_size, file_path, is_active BOOLEAN DEFAULT true)`; 6 CHECK constraints (`check_collection_visibility`, `check_preferred_currency`, `check_cards_per_page`, `check_default_view`, `check_theme`, `check_language`); 6 indexes | UPDATE on users to write defaults for any row with NULLs in the new columns | **M** |
### fix-* (2)
| # | Script | DDL summary | DML summary | Classification | Captured by |
| --- | --- | --- | --- | --- | --- |
| 10 | `fix-lorcana-images.js` | — | UPDATE on `cards WHERE game='Lorcana' AND image_url LIKE '%-716.webp' OR '%-512.webp'`: rewrite to `-1024.webp` for `image_url`, keep small as `stock_image_url` | **DML-only** | n/a — content-level data fix tied to a specific historical image-CDN payload shape. Re-running it on a fresh env that imports Lorcana via the canonical `import-lorcana` path would do nothing (new imports already use `-1024.webp`). |
| 11 | `fix-user-cards-constraints.js` | `ALTER user_cards ADD CONSTRAINT user_cards_user_card_unique UNIQUE (user_id, card_id)`; `ALTER collection_cards ADD CONSTRAINT collection_cards_collection_card_unique UNIQUE (collection_id, card_id)` | Dedup `user_cards` duplicates by `user_id, card_id`; sync owned cards to each user's `'All My Cards'` system collection | **M** (with conflict — see Drift findings) | DDL is **not** captured; the constraint exists in prod but not on fresh envs. Conflict: `initial-schema` already declares `UNIQUE(user_id, card_id, is_foil)` (3-col) on `user_cards`; this script adds a **stricter** 2-col `UNIQUE(user_id, card_id)` that contradicts the foil-distinguishing semantics encoded in the bootstrap. Halt-and-ask flagged below. |
### seed-* (2)
| # | Script | DDL summary | DML summary | Classification | Captured by |
| --- | --- | --- | --- | --- | --- |
| 12 | `seed-collections-alice-bob.js` | — | `DELETE FROM collection_cards / collection_permissions / collections` (destructive!); then INSERT 4 Alice + 5 Bob fixture collections | **DML-only** (dev fixture) | n/a — wires test users for local UI work; not migration material. |
| 13 | `seed-collections-with-cards.js` | — | Same destructive wipe; INSERT 13 sample cards (Black Lotus, Charizard, Elsa, …); INSERT 6 fixture collections with varied empty/thumbnail/card states | **DML-only** (dev fixture) | n/a — UI demo content; not migration material. |
### Inventory counts
- **Already captured (no work):** 2 — `add-updated-at-column.js` (#7) fully; `add-system-collection-column.js` (#6) DDL by `1780378340194` + DML backfill by `pages/api/auth/register.js:97-119` (Finding 4 RESOLVED)
- **Missed DDL (needs migration):** 7 — #1, #2, #3, #4, #5, #8#9 (deduplicate). #11's DDL is NOT folded into a migration (Finding 1 RESOLVED → Outcome A; see below)
- **DML-only (not migration material):** 3 — #10 fix-lorcana, #12 seed-alice-bob, #13 seed-with-cards
- **Halt-and-ask:** 0 (Finding 1 RESOLVED below)
## Drift findings
### Finding 1 — `user_cards` UNIQUE constraint conflict (RESOLVED — Outcome A, 2026-06-14)
**Resolution:** Operator ratified **Outcome A** post-architect-pass. The
3-col `UNIQUE(user_id, card_id, is_foil)` from
`migrations/1779853647564_initial-schema.js` is canonical and matches
the actual prod state. `scripts/fix-user-cards-constraints.js` was
either never applied to prod OR was applied + reverted at some point;
the canonical current state has the 3-col constraint, foil/non-foil
distinction is a real product invariant, and a separate fixed 2-col
`UNIQUE(user_id, card_id)` is **not present** on the prod `user_cards`
table.
**Consequence for this convoy:** B6 collapses entirely. No migration
is written for `fix-user-cards-constraints.js`. Per the recommendation
in the operator decision, a no-op documentation-only migration would
add clutter to `pgmigrations` for zero functional benefit; instead,
this Drift Finding entry + a sentence in B7's `docs/SCHEMA_MAP.md`
update + a sentence in the As-shipped section document the reasoning.
The historical script remains a no-go-zone per
`.cursor/rules/no-go-zones.mdc`; the queued
`retire-graveyard-scripts-after-audit` (P3) will delete or move it
along with the other 12 historical scripts.
The original audit narrative is preserved below for cross-reference.
---
**Original audit (pre-resolution):**
`migrations/1779853647564_initial-schema.js` line 82 declares:
```
UNIQUE(user_id, card_id, is_foil)
```
`scripts/fix-user-cards-constraints.js` lines 50-54 adds (separately, as
a named constraint):
```
ALTER TABLE user_cards
ADD CONSTRAINT user_cards_user_card_unique UNIQUE (user_id, card_id)
```
These are **semantically incompatible** when more than one foil/non-foil
copy of the same `(user_id, card_id)` exists:
- The bootstrap 3-column tuple allows `(1, 42, false)` AND `(1, 42, true)`
(user owns one regular + one foil copy of card 42).
- The fix-script's 2-column constraint **forbids** that pair.
In any prod environment where `fix-user-cards-constraints.js` was run
successfully (the script's dedup-first step would have removed
duplicates pre-constraint), the stricter constraint is in force AND
the looser tuple-implicit one also exists (autogen-named like
`user_cards_user_id_card_id_is_foil_key`). Two coexisting constraints
do not break Postgres semantics — the stricter wins for write
rejection.
Same situation on `collection_cards`: initial-schema's
`UNIQUE(collection_id, card_id)` (2-column) and the fix script's
`collection_cards_collection_card_unique` are **identical** in column
set, so this half is benign (Postgres rejects the duplicate at ADD
CONSTRAINT time; the script's caught `if (error.message.includes('already exists'))` swallows it).
**Why this is a halt-and-ask:** the runtime code path that touches
`is_foil` differentiation (see `pages/api/cards/[id]/own.js` and
`pages/api/user-cards.js`) needs to be audited to decide which
constraint matches actual product intent. Possible outcomes:
- **Outcome A:** Foil/non-foil distinction is a real product invariant
(a user should be able to track foil + non-foil copies of the same
card separately). The new migration should `DROP CONSTRAINT
user_cards_user_card_unique IF EXISTS` on prod, then ensure the
tuple constraint is the only one. Fresh envs already have the
correct tuple constraint from `initial-schema`; no add needed.
- **Outcome B:** Foil/non-foil should be unified at the user_cards row
level (use a separate `foil_quantity` column instead). This is a
product decision that belongs to a separate convoy
(`unify-user-cards-foil-tracking`).
- **Outcome C:** Both are tolerable (current prod state). The new
migration should add `ALTER user_cards ADD CONSTRAINT
user_cards_user_card_unique UNIQUE (user_id, card_id) IF NOT
EXISTS`-equivalent on fresh envs to match prod, AND surface the
smell in `docs/SCHEMA_MAP.md`.
**Halt point:** before B6 is written, operator picks A / B / C. **Recommended
default** if no decision arrives in 48h: **C** (replicate prod as-is;
surface as smell). Outcome A is the closest to "intent" but adding a
mid-convoy DROP CONSTRAINT on prod data deserves its own scoped
review.
_— Resolved 2026-06-14 as Outcome A (no DROP needed — the strict
constraint is not actually present on prod). See resolution block at
the top of this Finding._
### Finding 2 — `users.username` `profile_image_url` vs `avatar_url` redundancy
Scripts #8 (`add-user-profile-columns.js`) and #9 (`add-user-profile-fields.js`)
both add `first_name`, `last_name`, `username VARCHAR(255) UNIQUE` — the
overlap is idempotent (both use `ADD COLUMN IF NOT EXISTS`) so prod
ended up with the union. But:
- #8 adds `profile_image_url TEXT`
- #9 adds `avatar_url TEXT`
`docs/SCHEMA_MAP.md` § "Known schema smells" #1 already flags this
redundancy ("Pick one"). For this convoy: the new migration MUST add
**both** columns to match prod-as-is (since runtime code may read
either — to be verified). Surface as follow-up
`unify-user-avatar-column`.
### Finding 3 — `cards.quantity` and `cards.favorited` are dead columns
`add-card-columns.js` adds `quantity INTEGER DEFAULT 0` and `favorited
BOOLEAN DEFAULT false` to the `cards` table. `docs/SCHEMA_MAP.md`
flags both as **Unused** (smell #3). The actual `quantity` / `favorited`
semantics live on `user_cards` / `user_favorites`.
For this convoy: add both columns to fresh envs to match prod. Do NOT
drop them in prod (separate convoy). Surface as follow-up
`drop-dead-cards-columns` (deferred until a query-trace audit confirms
zero readers).
### Finding 4 — `add-system-collection-column.js` DML backfill (RESOLVED — verified, 2026-06-14)
**Resolution:** Verified by parent agent post-architect-pass. The
`pages/api/auth/register.js` handler at **lines 97-119** creates the
system collection on every new user signup:
```js
const collectionResult = await sql`
INSERT INTO collections (
name, description, tcg, is_public, user_id, slug,
is_system_collection, created_at, updated_at
)
VALUES (
${SYSTEM_COLLECTION_DB_NAME},
${VOCAB.SYSTEM_COLLECTION_SEED_DESCRIPTION},
'All', false, ${user.id}, ${uniqueSlug},
true, -- is_system_collection
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
RETURNING id
`;
```
The runtime invariant is intact. The historical script's per-user
backfill DML was the **one-time** reconciliation for legacy users
created before the register-hook existed; new envs never need it
because their users are all created post-hook.
For this convoy: **no migration, no follow-up convoy needed.** The
`add-system-collection-on-register` follow-up originally surfaced in
the prior architect pass is **withdrawn**. B7 will cite this
verification in `docs/SCHEMA_MAP.md` § `collections.is_system_collection`
notes.
### Finding 5 — Seed scripts wipe collection data
Both `seed-collections-alice-bob.js` and `seed-collections-with-cards.js`
begin with `DELETE FROM collection_cards / collection_permissions /
collections` — destructive against any environment where real user
data exists. They are clearly tagged as dev fixtures (the test users
`alice@tcgvault.com` / `bob@tcgvault.com` are local-only). Per D4
below, these stay out of migrations entirely.
## Decisions (post-IA round; this is a P1 quality convoy that skipped IA/UX/A11y/Design — see frontmatter `skip:`)
### D1 — Brief grouping: option (b), grouped by logical surface
**Ratified: 6 implementer briefs grouped by table/feature surface.**
(Originally 7; B6 collapsed post-Finding-1 resolution — see § Brief outline.)
Missed-DDL count is 8 (above the 1-3 threshold for option (d) and
above the 4-thing threshold that triggered option (b/c) in the
parent prompt). One-brief-per-script (option a, 13 briefs) is
excessive — many scripts touch the same DDL surface and ship one
migration each would create unnecessary `pgmigrations` rows and
make rollback narrative confusing.
The 6-brief plan groups every missed DDL by the table or feature it
touches:
- **B1** — Cards table reconciliation (#1, audit #7)
- **B2** — Collections table reconciliation (#2 partial, #3, #5)
- **B3** — Collaboration tables (#2 partial — `collection_permissions`,
`collection_activity`, `users.is_pending`)
- **B4** — Favorites system (#4)
- **B5** — User profile reconciliation (#8 #9, dedup'd; +
`user_settings`, `user_avatars`; + CHECK constraints; + indexes)
- **B7** — Documentation + verification runbook (`docs/SCHEMA_MAP.md`
refresh, AGENTS.md Gotcha #6 update, operator runbook)
**B6 was removed post-Finding-1 resolution** (Outcome A — the strict
2-col `UNIQUE(user_id, card_id)` is not present on prod; the canonical
3-col `UNIQUE(user_id, card_id, is_foil)` from `initial-schema` is
what fresh envs already get). No migration is needed. The brief
numbering preserves the original gap (B1-B5 + B7) so cross-references
to "B6" in prior drafts of this convoy file remain unambiguous (they
point to the removed brief).
Rationale:
1. **Independence.** Each of B1-B6 writes a new file under `migrations/`
with a fresh timestamp; the files don't overlap, so B1-B6 are
trivially parallel (subject to timestamp ordering — see slice
dependencies block below).
2. **Reviewability.** Each PR is one migration + one optional
`docs/SCHEMA_MAP.md` section update; reviewer can verify against
the historical script in a single sitting.
3. **Rollback granularity.** If B5's user-profile migration is found
buggy post-merge, the other 5 migrations are unaffected — they all
`pgmigrations`-row independently.
4. **Estimated LOC per brief stays under 400.** B5 is the largest
(~250 LOC of SQL across 13 ALTERs + 2 CREATE TABLE + 6 CHECKs + 6
indexes) — comfortably under budget.
Considered alternatives:
| Option | Why rejected |
| --- | --- |
| (a) one brief per script (13 briefs) | Splits #2 across `collections`-columns vs `collection_permissions` vs `users.is_pending` artificially. Pads review surface 2x. |
| (c) one DDL + one DML + one verification brief (3 briefs) | A single DDL brief would be a 500+ LOC mega-migration. Hard to review, hard to rollback. |
| (d) single PR | Missed-DDL count >> 1; rejected by spec. |
### D2 — Idempotency pattern: raw `pgm.sql(...)` with `IF [NOT] EXISTS` guards
**Ratified: raw `pgm.sql(...)`-style SQL matching `1779853647564_initial-schema.js`'s pattern, every statement guarded with `IF NOT EXISTS` (for CREATE) or `IF EXISTS` (for DROP).**
Rationale:
1. **Style consistency.** The five existing migrations all use
`pgm.sql()`. Mixing `pgm.createTable()` helpers would introduce a
second pattern for no operational benefit.
2. **Verbatim SQL transparency.** The reviewer can grep the new
migration's SQL string against the historical script's SQL string
and confirm byte-equivalent intent.
3. **Idempotency.** Every CREATE / ALTER ADD COLUMN / CREATE INDEX
statement uses the appropriate `IF NOT EXISTS` guard so re-running
against prod is a documented no-op. CHECK constraints don't accept
`IF NOT EXISTS` directly — wrap in a `DO $$ BEGIN ... EXCEPTION
WHEN duplicate_object THEN NULL; END $$;` block (matching the
historical script's try/catch pattern).
`node-pg-migrate` helpers (`pgm.createTable`, `pgm.addColumns`) would
work fine technically, but the existing migration corpus is 100% raw
SQL. Optimize for review uniformity.
### D3 — Drift detection: option (a) — assume change is in prod, add a migration to bring fresh envs to parity
**Ratified: (a) with a documented "evidence" trail per script in the Inventory section above.**
For every script in the **M** classification, the assumption is that
the script ran successfully against prod at some point and its DDL is
now baked into the prod schema. Evidence supporting this assumption:
- `docs/SCHEMA_MAP.md` (last reviewed 2026-05-22) documents all of these
columns/tables as live.
- Runtime code in `pages/api/**` reads/writes `user_favorites`,
`collection_permissions`, `collection_activity`, `user_settings`,
`user_avatars`, `users.first_name`, `users.username`, `users.bio`,
`users.avatar_url`, `users.theme`, `users.language`,
`collections.visibility`, `collections.tcg`, `collections.tags`,
`collections.slug`, `collections.image` — confirmed via grep
(`pages/api/user/settings.js`, `pages/api/user/profile.js`,
`pages/api/favorites.js`, `pages/api/invite/{accept,decline}.js`,
`pages/api/collections/[identifier]/permissions.js`, etc.).
- Each historical script is itself idempotent (`ADD COLUMN IF NOT
EXISTS`), so prod-vs-fresh divergence is the live state.
For Finding 1 (UNIQUE constraint conflict), Finding 4 (system-coll
backfill), and Finding 5 (seed wipes): per the recommendation in the
parent prompt, halt-and-ask is reserved for destructive changes. (1)
qualifies (DROP CONSTRAINT) → halt-and-ask. (4) is operationally
benign (no DROP) → defer to follow-up. (5) is destructive but the
recommended choice is "don't fold into migrations at all" — D4 below.
### D4 — Seed scripts: option (a) — leave alone
**Ratified: dev-fixture seed scripts stay out of the migration history.**
`seed-collections-alice-bob.js` and `seed-collections-with-cards.js`
are dev-fixture loaders tied to `alice@tcgvault.com` /
`bob@tcgvault.com` (which only exist in `scripts/create-test-users.js`,
gated post-`purge-weak-creds-from-helpers` behind `TEST_USERS_PASSWORD`).
They:
- Wipe destructive collection data (incompatible with any env with real users).
- Reference Disney/Lorcana sample art URLs (`example.com/elsa.jpg`).
- Are clearly UI-demo fodder.
They have no place in `setup-db`'s migrate pipeline. They will be
retired (or moved to `scripts/historical/`) by the queued
`retire-graveyard-scripts-after-audit` convoy.
If multiple developers feel friction managing dev-fixture state in
the future, surface a separate `consolidate-dev-seeds` follow-up
that designs a non-destructive `npm run seed:dev` flow keyed off a
fresh DB. Not surfaced from this convoy because no friction has been
reported yet.
### D5 — Verification: option (a) — manual operator runbook
**Ratified: post-merge manual verification by spinning up a fresh
Neon branch + running `npm run setup-db` + diffing schema against
prod.**
Automated CI verification belongs to the queued `wire-migrate-into-ci`
convoy and is out of scope here. The manual runbook will live in this
convoy file's "Verification plan" section (below) and be promoted to
`docs/operations/RECONCILE-VERIFICATION.md` by B7.
### D6 — `scripts/migrations/2026-05-24-rename-admin-email.js`: option (b) — leave where it is
**Ratified: do not move into `migrations/`.**
The lone pre-tool migration sits at `scripts/migrations/2026-05-24-rename-admin-email.js`
and was applied to every long-lived env at `pick-a-name` time
(2026-05-24). Moving it into `migrations/` would require backfilling a
`pgmigrations` row on every existing env, which is operationally
risky for zero functional benefit (the migration is already applied;
the runtime never re-checks it).
For fresh envs, the migration is a no-op (the seed admin row is
already created at `admin@deckhearth.com` by post-rename
`scripts/setup-neon-db.js`; there's no `@tcgvault.com` row to rename).
Document the rationale in B7's `docs/SCHEMA_MAP.md` update + a brief
note in AGENTS.md Gotcha #4's "Rotation script" subsection. If a
future fresh-env onboarding ever tries to roll back to pre-rename
state, surface `fold-rename-admin-email-into-migrations` as a
follow-up.
### D7 — `pgmigrations` table state on fresh envs: no special handling required
**Ratified: rely on `node-pg-migrate`'s standard sequential apply.**
On a fresh Neon branch, `npm run migrate up` will run all migrations
in timestamp order:
1. `1779853647564_initial-schema` (7 bootstrap tables)
2. `1779853647565_add-pg-trgm-card-name-index`
3. `1779853647566_add-scan-tables`
4. `1779908094455_add-user-cards-scan-image-url`
5. `1780378340194_system-collection-description`
6. **NEW: B1** — cards columns (quantity, favorited)
7. **NEW: B2** — collections columns (visibility, tcg, tags, slug, slug index/constraint, image)
8. **NEW: B3** — collaboration tables (collection_permissions, collection_activity, users.is_pending, 4 indexes)
9. **NEW: B4** — favorites system (user_favorites + 4 indexes)
10. **NEW: B5** — user profile reconciliation (15 ALTER-ADD-COLUMN + 2 CREATE TABLE + 6 CHECK + 6 indexes)
Each new migration is additive against the post-initial-schema state
that prior migrations leave behind; no inter-migration dependencies
crossed within this convoy.
Audited ordering risks for the new migrations:
- B1 depends on `cards` (initial-schema) ✅
- B2 depends on `collections` (initial-schema) ✅
- B3 depends on `collections` + `users` (initial-schema) ✅
- B4 depends on `users` (initial-schema) ✅
- B5 depends on `users` (initial-schema) ✅
For long-lived envs that already have all post-historical columns:
every new migration is a documented no-op due to `IF NOT EXISTS`
guards. The only state change is the `pgmigrations` row insertion.
## Brief outline
Six implementer briefs. Each B1-B5 ships exactly one new file under
`migrations/<timestamp>_<slug>.js`. B7 updates docs only. Implementer
briefs are drafted under `.convoys/reconcile-historical-add-scripts/brief-<N>-<slug>.md`.
| Brief | Title | Files (new) | Depends on | Est. LOC | Notes |
| --- | --- | --- | --- | --- | --- |
| B1 | Reconcile cards columns | `migrations/1781000000001_reconcile-cards-columns.js` | — | ~40 | Captures script #1; script #7 already captured by initial-schema (verify & document in PR body) |
| B2 | Reconcile collections columns | `migrations/1781000000002_reconcile-collections-columns.js` | — | ~120 | Captures #2 (collections half: visibility/tcg/tags), #3 (slug + index + CHECK), #5 (image). Defer slug backfill DML — generating slugs on a fresh env is moot. |
| B3 | Reconcile collaboration tables | `migrations/1781000000003_reconcile-collaboration-tables.js` | — | ~130 | Captures #2 (collaboration half: collection_permissions, collection_activity, users.is_pending, 4 indexes). Defer owner-permission backfill DML — fresh envs have no pre-existing collections needing backfill. |
| B4 | Reconcile favorites system | `migrations/1781000000004_reconcile-favorites-system.js` | — | ~50 | Captures #4 (user_favorites + 4 indexes). |
| B5 | Reconcile user profile | `migrations/1781000000005_reconcile-user-profile.js` | — | ~250 | Captures #8 #9 (deduplicated; both add columns coexist in prod). Includes user_settings, user_avatars, 6 CHECK constraints (wrapped in `DO $$ EXCEPTION` blocks for idempotency), 6 indexes. Defer defaults-backfill UPDATE — column DEFAULTs handle it. |
| ~~B6~~ | ~~Reconcile user_cards / collection_cards UNIQUE constraints~~ | ~~removed~~ | — | 0 | **REMOVED 2026-06-14 — Finding 1 RESOLVED as Outcome A.** The strict 2-col `UNIQUE(user_id, card_id)` is not present on prod; the canonical 3-col tuple is already on fresh envs via `initial-schema`. Documented in Drift Finding 1 + B7's SCHEMA_MAP update; no migration written. |
| B7 | Documentation + verification | `docs/SCHEMA_MAP.md` (update); `docs/MIGRATION_VERIFICATION_RUNBOOK.md` (new); `AGENTS.md` (Gotcha #6 audit); `.convoys/ship-readiness.md` (flip queued entry) | B1-B5 merged | ~150 | SCHEMA_MAP smell-list updates per Findings 1-3. Verification runbook (see § Verification plan below). AGENTS.md Gotcha #6 already RESOLVED post-migration-tool; verify and leave alone if accurate. ship-readiness.md "Queued convoys" → flip this convoy entry to RESOLVED + unblock `retire-graveyard-scripts-after-audit` + add `unify-user-avatar-column` + `drop-dead-cards-columns` follow-ups. |
Per-brief acceptance criteria (sketch — formalized in each brief file):
- Each migration file has a `down()` that **throws** with a clear message
(these are reconciliation migrations; the prod schema state is the
source of truth and rolling back would create inconsistency).
- Each migration's SQL is **byte-equivalent in intent** to the
historical script's SQL (per-statement comments cite the script
and line range).
- Re-running `npm run migrate up` against any current long-lived env
is a no-op (all guards trigger).
- `npm run migrate up` against a fresh Neon branch followed by the
verification runbook (D5) confirms structural parity with prod.
### Slice dependencies (multitask-ready)
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files: [migrations/1781000000001_reconcile-cards-columns.js]
- brief: 2
depends_on: []
files: [migrations/1781000000002_reconcile-collections-columns.js]
- brief: 3
depends_on: []
files: [migrations/1781000000003_reconcile-collaboration-tables.js]
- brief: 4
depends_on: []
files: [migrations/1781000000004_reconcile-favorites-system.js]
- brief: 5
depends_on: []
files: [migrations/1781000000005_reconcile-user-profile.js]
# brief 6 removed — Finding 1 RESOLVED as Outcome A; timestamp 1781000000006 is unused
- brief: 7
depends_on: [1, 2, 3, 4, 5]
files:
- docs/SCHEMA_MAP.md
- docs/MIGRATION_VERIFICATION_RUNBOOK.md
- AGENTS.md
- .convoys/ship-readiness.md
```
**Timestamp coordination.** Each B1-B5 writes a
`migrations/<timestamp>_*.js` file with a pre-assigned timestamp
(reservation token, not literal `Date.now()`). Pre-assigned
timestamps avoid the parallel-implementer collision risk documented
in `scaffold-nextjs-app` retro recommendation #4. The brief frontmatter
in each `.convoys/reconcile-historical-add-scripts/brief-<N>-*.md`
file declares the exact path; implementers MUST use that exact
filename (not `npm run migrate create`, which would `Date.now()`).
## Verification plan (D5 operator runbook)
To verify the reconstructed migration history produces parity with
prod, after B1-B6 merge:
```bash
# 1. Snapshot prod's structural shape (run against prod POSTGRES_URL)
# Schema-only dump, no data, no owner/ACL noise
POSTGRES_URL=<prod-url> pg_dump --schema-only --no-owner --no-acl \
--schema=public > /tmp/prod-schema.sql
# 2. Create a clean Neon branch from EMPTY (no parent branch) and onboard via setup-db
# (use the Neon dashboard or `neon branches create --empty`)
POSTGRES_URL=<fresh-branch-url> \
ADMIN_INITIAL_PASSWORD=$(openssl rand -base64 24) \
npm run setup-db
# 3. Snapshot the fresh branch's structural shape
POSTGRES_URL=<fresh-branch-url> pg_dump --schema-only --no-owner --no-acl \
--schema=public > /tmp/fresh-schema.sql
# 4. Diff. Expected differences are limited to:
# - constraint/index NAME differences (autogen tuple-UNIQUE names vs explicit names)
# - column-ORDER differences (prod has columns in historical-script-ALTER order;
# fresh envs have them in migration-order)
# Both are semantically irrelevant. Material differences = bug; flag and reopen.
diff <(sort /tmp/prod-schema.sql) <(sort /tmp/fresh-schema.sql)
```
Supplementary information-schema spot-checks for the highest-risk surfaces:
```sql
-- Every column on every table
SELECT table_name, column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
-- Every constraint
SELECT table_name, constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'public'
ORDER BY table_name, constraint_name;
-- Every index
SELECT tablename, indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;
```
Run both against prod and fresh-branch; the column-count + constraint-count
+ index-count totals should match exactly. Mismatch = bug.
B7 will move this runbook to `docs/operations/RECONCILE-VERIFICATION.md`
and link it from `AGENTS.md` Gotcha #6 + `migration-tool` § R1's
"resolved-by" note.
## Risks
### R1 — Missed DDL that is NOT actually in prod ("ghost migration")
The assumption (D3) is that every historical script ran successfully
against every long-lived env. If a script in fact failed silently
mid-execution on prod (e.g. `add-collaboration-features.js`'s
`CREATE INDEX idx_collections_visibility` errored partway through),
prod might not actually have that index even though SCHEMA_MAP says
it does.
**Mitigation:** the verification plan (D5) catches this. The fresh-env
`pg_dump` would contain the index; prod's `pg_dump` would not; the
diff would surface it. If found, operator decides: (a) the script's
intent was sound, apply the missed DDL to prod manually with
`POSTGRES_URL=<prod> psql -c "CREATE INDEX IF NOT EXISTS ..."`; or
(b) the index is unwanted, drop it from the new migration and document.
### R2 — `pgmigrations` row state on existing prod envs
After this convoy merges, an operator running `npm run migrate up`
against prod will see 6 new migrations apply (B1-B6) as no-ops (every
guarded statement triggers `IF [NOT] EXISTS`-skip). Six new
`pgmigrations` rows record successful application.
If for some reason a long-lived prod env genuinely lacks one of the
historical-script columns (Risk R1 above), the corresponding migration
will **add** that column on apply, no-op-ing the others. The
`pgmigrations` row records success; subsequent applies are no-ops.
This is the correct behavior, but the operator should run the
verification plan post-apply to confirm.
**Mitigation:** the verification plan covers prod-vs-fresh diff after
B1-B6 land. Run it once on each prod-shaped env immediately after
merge.
### R3 — Ordering: new migration depends on prior schema-state that doesn't exist at its execution point
Audited in D7. All B1-B6 dependencies on prior tables (`cards`,
`collections`, `users`, `user_cards`, `collection_cards`) are
satisfied by `initial-schema` (timestamp `1779853647564`, runs first
on fresh envs). No B-to-B inter-dependency required.
### R4 — Seed scripts depend on test users that don't exist on fresh envs
Out of scope (D4). The two seed scripts are dev fixtures and are not
folded into migrations. Test users (`alice@`, `bob@`) are managed by
`scripts/create-test-users.js` (post-`purge-weak-creds-from-helpers`,
gated behind `TEST_USERS_PASSWORD`). Surface as `consolidate-dev-seeds`
follow-up only if a developer reports friction.
### R5 — CHECK constraint reapplication on prod is loud
`add-user-profile-fields.js` wraps each CHECK constraint ADD in a
JS try/catch that swallows `already exists` errors. Postgres doesn't
accept `ADD CONSTRAINT ... IF NOT EXISTS` for CHECK; the SQL has to be
wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END
$$;`. B5's migration must use the exception-handling form so reapply
against prod is silent. Implementer brief will spell this out.
### R6 — Mid-convoy timestamp collision when implementers spawn in parallel
B1-B5 are mutually independent and can dispatch via `/multitask`.
Pre-assigned timestamps (D7 table) avoid the `Date.now()`-collision
risk that bit the `scaffold-nextjs-app` convoy. Each implementer
brief will name its file's exact timestamp; deviations require a
re-plan.
### R7 — Operator approval lag on Finding 1 (RESOLVED 2026-06-14)
Originally a risk because B6 was gated on operator decision. Finding 1
RESOLVED as Outcome A; B6 removed. No lag risk remains.
## Follow-ups
Surfaced by this convoy:
- **`retire-graveyard-scripts-after-audit`** (priority: **P3 polish — now
UNBLOCKED** once this convoy lands). Once B1-B6 capture every
missed historical DDL into the migration history, the 13 historical
scripts can be safely deleted or moved to `scripts/historical/` and
the corresponding no-go-zones rule line can be removed. Documented
in `.convoys/migration-tool.md` § Follow-ups; ship-readiness.md
"Queued convoys" entry; AGENTS.md Gotcha #6 cross-reference.
- **`wire-migrate-into-ci`** (priority: P2 CI infra — pre-existing).
Adds a CI job that runs `npm run migrate up` against a test DB on
every PR. This convoy's verification plan (D5) is the manual
precursor; the CI job is the automation upgrade. Already on the
follow-up list per `migration-tool` § Follow-ups + ship-readiness.md
"Queued convoys".
- **`unify-user-avatar-column`** (priority: P3 hygiene — NEW). Driven
by Finding 2. Two redundant TEXT columns
(`users.profile_image_url` and `users.avatar_url`) coexist; pick one
canonical column, migrate the other's data, drop the loser, update
runtime readers. Requires a query-trace audit first.
- **`drop-dead-cards-columns`** (priority: P3 hygiene — NEW). Driven
by Finding 3. `cards.quantity` and `cards.favorited` are documented
as unused. After a query-trace audit confirms zero readers, ship a
migration that DROPs them (with proper `down()` recreate).
- ~~**`add-system-collection-on-register`**~~**WITHDRAWN 2026-06-14**, Finding 4 RESOLVED. Verified the register hook exists at `pages/api/auth/register.js:97-119`.
- **`fold-rename-admin-email-into-migrations`** (priority: P3 polish,
conditional — NEW per D6). Only surface if a future fresh-env
onboarding needs the rename migration applied in order. Until then,
the script-shaped migration at
`scripts/migrations/2026-05-24-rename-admin-email.js` is left in
place per D6.
- ~~**`unify-user-cards-foil-tracking`**~~**WITHDRAWN 2026-06-14**, Finding 1 RESOLVED as Outcome A (not Outcome B). The 3-col tuple stays canonical; no foil-tracking redesign needed.
Not surfaced (no friction yet):
- `consolidate-dev-seeds` — D4 noted this can wait until multiple
developers report friction with the current per-script dev-fixture
loaders.
## As-shipped
Six implementer briefs (B1B6) landed between 2026-06-14 and 2026-07-06.
Brief 7 (documentation + verification runbook) closed the convoy on
2026-08-15 — a post-hoc docs-only pass delayed by an operator-initiated
pause on a self-hosted runner infra issue that did not affect migration
content correctness.
### Per-brief delivery
| Brief | PR | Squash | Merged | Migration file |
| --- | --- | --- | --- | --- |
| B1 cards columns | #148 | `a35ce01` | 2026-06-14 | `migrations/1781442330001_reconcile-cards-columns.js` |
| B2 collections columns | #150 | `e8619c5` | 2026-06-14 | `migrations/1781442330002_reconcile-collections-columns.js` |
| B3 collaboration tables | #151 | `15e02ee` | 2026-07-06 | `migrations/1781000000003_reconcile-collaboration-tables.js` |
| B4 favorites system | #152 | `ef2cfb8` | 2026-06-14 | `migrations/1781442330004_reconcile-favorites-system.js` |
| B5 user profile | #153 | `ee7da9a` | 2026-06-14 | `migrations/1781442330005_reconcile-user-profile.js` |
| B6 user_cards UNIQUE | #149 | `40402eb` | 2026-06-14 | `migrations/1781442330006_reconcile-user-cards-unique.js` |
| B7 docs + runbook | _(this PR)_ | — | 2026-08-15 | _(no migration — docs only)_ |
All six implementer PRs were green at merge on the (then-flaky) self-hosted
axiom runner pool.
### Reservation-timestamp rename (`ec9bb2b`)
The architect pre-assigned timestamps `1781000000001` through
`1781000000006` to avoid `Date.now()` collisions during parallel
implementer dispatch. Commit `ec9bb2b` (`feat(catalog): unified multi-game
bulk sync + schema map update`) renamed all except B3 to
`1781442330001``1781442330006` so they run **after** the catalog-sync
migrations that landed in the same period:
- `1781440700404_add-scryfall-bulk-columns`
- `1781440721350_add-tagger-tables`
- `1781442175729_normalize-lorcana-set-codes`
- `1781442329511_add-catalog-sync-log`
B3 kept its original `1781000000003` timestamp because it merged after the
rename commit. The rename is filename-only; migration `up()` content is
identical.
### B6 deviation from architect plan
The architect's gate-1 plan collapsed Finding 1 (Outcome A) into B7 as a
doc-only note. Implementers shipped B6 anyway as a tiny belt-and-suspenders
migration (`1781442330006_reconcile-user-cards-unique.js`) that:
- Confirms the canonical 3-column `UNIQUE(user_id, card_id, is_foil)` from
`initial-schema` is present.
- Drops the historical 2-column `user_cards_user_card_unique` constraint if
any env ran `fix-user-cards-constraints.js`.
The migration is a no-op on prod (Outcome A already holds) but guards
future envs that might have the stricter 2-col variant.
### Verification
Manual operator runbook for confirming fresh-env vs prod schema parity:
[`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](../docs/MIGRATION_VERIFICATION_RUNBOOK.md)
(per architect Decision D5). Automated CI verification remains queued as
`wire-migrate-into-ci`.
### B7 deviation from spec
B7 shipped ~2 months post-hoc (2026-08-15) rather than immediately after
B1B6. The delay was operator-initiated (runner-infra pause) and did not
block migration correctness — only documentation closure.

View file

@ -1,228 +0,0 @@
---
convoy: reconcile-historical-add-scripts
brief_number: 1
depends_on: []
files:
- migrations/1781000000001_reconcile-cards-columns.js
---
# Brief 1: Reconcile `cards` columns into migration history
## Goal (1 sentence)
Capture the DDL added by `scripts/add-card-columns.js` (the `cards.quantity` + `cards.favorited` columns) into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with both columns after `npm run setup-db`.
## Scope (files in scope — do not edit anything else)
- `migrations/1781000000001_reconcile-cards-columns.js` — **new**
## Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
`scripts/add-card-columns.js` lines 22-33 (verbatim):
```js
await sql`
ALTER TABLE cards
ADD COLUMN IF NOT EXISTS quantity INTEGER DEFAULT 0
`;
// ...
await sql`
ALTER TABLE cards
ADD COLUMN IF NOT EXISTS favorited BOOLEAN DEFAULT false
`;
```
**Sibling script `scripts/add-updated-at-column.js`** is already captured by `migrations/1779853647564_initial-schema.js` (the `cards` `CREATE TABLE` at lines 45-69 already declares `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`). Do NOT add an ALTER for `updated_at` — it would be redundant noise on `pgmigrations`. Mention this in the PR body so reviewers don't ask.
## Target migration file
Path: `migrations/1781000000001_reconcile-cards-columns.js`
Contents:
```js
/**
* Reconcile historical `scripts/add-card-columns.js` into the migration
* history. Adds `cards.quantity` and `cards.favorited` columns so a
* brand-new Neon branch ends up with the same shape that prod has had
* since the historical script's one-shot run.
*
* Both columns are flagged as **unused** in docs/SCHEMA_MAP.md
* § "Known schema smells" #3`quantity` lives on `user_cards`,
* `favorited` lives on `user_favorites`. They're added here for
* fresh-env parity with prod. A follow-up `drop-dead-cards-columns`
* convoy (queued, P3 hygiene) will drop both columns once a query-trace
* audit confirms zero runtime readers.
*
* Idempotent: re-running against any long-lived prod env is a no-op
* because both ALTERs use IF NOT EXISTS.
*
* Note: `scripts/add-updated-at-column.js` (the sibling historical
* script in the same convoy) is NOT reconciled here because
* `cards.updated_at` is already declared in
* `migrations/1779853647564_initial-schema.js`'s `CREATE TABLE cards`
* (line 67). No further work needed for that script.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
*/
export const up = (pgm) => {
pgm.sql(`
ALTER TABLE cards
ADD COLUMN IF NOT EXISTS quantity INTEGER DEFAULT 0;
ALTER TABLE cards
ADD COLUMN IF NOT EXISTS favorited BOOLEAN DEFAULT false;
`);
};
/**
* Down-migration intentionally throws. Dropping these columns on
* long-lived envs requires the `drop-dead-cards-columns` convoy's
* query-trace audit — bypassing it via a casual rollback risks
* dropping data on prod. Use `drop-dead-cards-columns` when ready.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000001_reconcile-cards-columns] Down not supported. ' +
'Dropping cards.quantity / cards.favorited belongs to the queued ' +
'drop-dead-cards-columns convoy, which performs a query-trace audit ' +
'before the DROP. Do not rollback this migration directly.'
);
};
```
## Conventions to follow
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is the canonical home for schema changes; one column-group per migration.
- `.cursor/rules/no-go-zones.mdc``scripts/add-card-columns.js` is append-only history. **Do not edit it.**
- Style: match the existing migrations under `migrations/`. Use raw `pgm.sql(...)` template literals (the `migration-tool` convoy ratified this in D2 of `.convoys/reconcile-historical-add-scripts.md`).
- ESM exports (`export const up = ...`, `export const down = ...`); no `module.exports`. The repo is `"type": "module"` per `package.json` line 5.
- Use `IF NOT EXISTS` on every ALTER — idempotent re-apply is a documented requirement of this convoy.
- Add a JSDoc docstring at the top of the file explaining what's being reconciled, citing the source script + the convoy file.
## Acceptance criteria
- [ ] `migrations/1781000000001_reconcile-cards-columns.js` exists with the exact filename above (the timestamp `1781000000001` is the reservation token assigned by the architect — do NOT use `npm run migrate create`, which would call `Date.now()` and assign a different timestamp).
- [ ] The file's `up()` adds both columns via `IF NOT EXISTS`.
- [ ] The file's `down()` throws with a clear message pointing at the `drop-dead-cards-columns` follow-up.
- [ ] The file's docstring cites `scripts/add-card-columns.js` and `.convoys/reconcile-historical-add-scripts.md`.
- [ ] `node --check migrations/1781000000001_reconcile-cards-columns.js` passes (syntactic validity).
- [ ] `node -e "import('./migrations/1781000000001_reconcile-cards-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined` (module loads cleanly).
- [ ] `npm run lint` exits clean against the baseline (no new lint errors introduced by this file).
- [ ] `npm run test:run` reports 21/21 passing (no test surface changes).
- [ ] The PR body documents that `add-updated-at-column.js` is already captured by `initial-schema` (per the source-script section above) and explains why no second migration is added.
## Verification
Run the following from the convoy worktree (`tcg-vault-worktrees/reconcile-historical-add-scripts/`) BEFORE opening the PR:
```bash
node --check migrations/1781000000001_reconcile-cards-columns.js
node -e "import('./migrations/1781000000001_reconcile-cards-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
npm run lint
npm run test:run
```
Expected:
- `node --check` exits 0 with no output.
- The `node -e` line prints exactly: `function function undefined`.
- `npm run lint` matches the existing baseline (no new errors).
- `npm run test:run` reports 21/21 tests passing.
**Do NOT** run `npm run migrate up` against any environment as part of this brief — that's a post-merge operator step covered by Brief 7's verification runbook.
## Commit message
```
feat(migrations): reconcile add-card-columns into migration history (brief 1/7)
Captures the DDL effect of scripts/add-card-columns.js (cards.quantity
+ cards.favorited columns) into a new node-pg-migrate migration. Both
columns are flagged as unused in docs/SCHEMA_MAP.md § "Known schema
smells" #3 — added here for fresh-env parity with prod; a follow-up
drop-dead-cards-columns convoy will drop them after a query-trace
audit.
Sibling script add-updated-at-column.js is already captured by
migrations/1779853647564_initial-schema.js (cards.updated_at is in the
CREATE TABLE); no second migration needed.
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B1.
Idempotent re-apply (IF NOT EXISTS guards).
```
## PR shape
**Title:** `feat(migrations): reconcile add-card-columns into migration history (brief 1/7)`
**Body template:**
```markdown
Brief 1 of the `reconcile-historical-add-scripts` convoy. See
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
## What this PR does
Adds `migrations/1781000000001_reconcile-cards-columns.js` — a new
`node-pg-migrate` migration that adds two columns to `cards` via
`ADD COLUMN IF NOT EXISTS`:
- `quantity INTEGER DEFAULT 0`
- `favorited BOOLEAN DEFAULT false`
Both columns already exist in long-lived prod environments (added by
the historical `scripts/add-card-columns.js`). This migration brings
fresh Neon branches to parity so `npm install``npm run setup-db`
alone produces the prod shape, without manually replaying historical
scripts.
## What this PR does NOT do
- Does **NOT** edit `scripts/add-card-columns.js` (no-go-zone).
- Does **NOT** add a migration for `scripts/add-updated-at-column.js`
`cards.updated_at` is already declared in
`migrations/1779853647564_initial-schema.js` line 67.
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
or AGENTS.md.
- Does **NOT** run `npm run migrate up` against any environment (that's
the post-merge operator step covered by Brief 7's runbook).
- Does **NOT** drop the columns (deferred to follow-up
`drop-dead-cards-columns`).
## Verification checklist
- [ ] `node --check migrations/1781000000001_reconcile-cards-columns.js` exits 0
- [ ] `node -e "import('./migrations/1781000000001_reconcile-cards-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
- [ ] `npm run lint` matches baseline (no new errors)
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Did not run `npm run migrate up` against any environment in this PR
- [ ] Operator post-merge: run the verification runbook from Brief 7 (`docs/MIGRATION_VERIFICATION_RUNBOOK.md` once it lands)
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Source script (no-go-zone, audit reference only): `scripts/add-card-columns.js`
- Follow-up after this convoy lands: `drop-dead-cards-columns` (P3 hygiene)
```
## DO NOT
- DO NOT edit `scripts/add-card-columns.js` or any other file under `scripts/` — append-only no-go-zone per `.cursor/rules/no-go-zones.mdc`.
- DO NOT edit any other file under `migrations/` — each migration is pinned by its `pgmigrations` row.
- DO NOT edit `scripts/setup-neon-db.js`.
- DO NOT edit `package.json` (no new deps).
- DO NOT edit `AGENTS.md` or `docs/SCHEMA_MAP.md` — that's Brief 7's job.
- DO NOT run `npm run migrate up` against any environment.
- DO NOT call `npm run migrate create` to scaffold the file — it uses `Date.now()` for the timestamp prefix, which would collide with the architect's pre-assigned reservation tokens for parallel briefs.
## Rationale (≤3 sentences)
Capturing `cards.quantity` + `cards.favorited` in a single small migration matches the per-table grouping of the convoy's D1 decision and keeps the diff easy to review. Sibling `add-updated-at-column.js` is already captured by `initial-schema`, so reconciling it would create a `pgmigrations` row for zero functional benefit. The columns themselves are dead per SCHEMA_MAP smell #3, but parity with prod is the convoy's success metric — the actual DROP is the `drop-dead-cards-columns` follow-up's responsibility.

View file

@ -1,307 +0,0 @@
---
convoy: reconcile-historical-add-scripts
brief_number: 2
depends_on: []
files:
- migrations/1781000000002_reconcile-collections-columns.js
---
# Brief 2: Reconcile `collections` columns into migration history
## Goal (1 sentence)
Capture the `collections`-table DDL added by three historical scripts (`add-collaboration-features.js` columns half, `add-collection-slugs.js`, `add-image-column.js`) into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with `visibility`, `tcg`, `tags`, `slug` (+ index + CHECK constraint), and `image` columns on `collections` after `npm run setup-db`.
## Scope (files in scope — do not edit anything else)
- `migrations/1781000000002_reconcile-collections-columns.js` — **new**
## Source scripts (read-only audit reference; DO NOT EDIT — all three are no-go-zones)
### `scripts/add-collaboration-features.js` lines 15-20 (collections half only — the `collection_permissions` / `collection_activity` / `users.is_pending` half is Brief 3's scope):
```js
await sql`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'private',
ADD COLUMN IF NOT EXISTS tcg VARCHAR(50) DEFAULT 'MTG',
ADD COLUMN IF NOT EXISTS tags TEXT
`;
```
Plus the visibility index at lines 77-80:
```js
await sql`
CREATE INDEX IF NOT EXISTS idx_collections_visibility
ON collections(visibility)
`;
```
### `scripts/add-collection-slugs.js` lines 17-99 (relevant DDL only):
```js
// Step 1: Add slug column
await sql`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE
`;
// Step 5: Add unique index
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug ON collections(slug)`;
// Step 6: Add format CHECK constraint
await sql`ALTER TABLE collections ADD CONSTRAINT check_slug_format CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50)`;
```
The script also backfills slugs via per-row UPDATE using `lib/slug-utils.js::generateUniqueSlug` (lines 41-73). **Do NOT fold the backfill DML into this migration.** Fresh envs have no pre-existing `collections` rows to backfill; on prod, the backfill ran once historically. Future migrations that need slug generation should run that logic in application code, not in a migration.
### `scripts/add-image-column.js` lines 14-17 (entire DDL):
```js
await sql`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS image TEXT
`;
```
## Target migration file
Path: `migrations/1781000000002_reconcile-collections-columns.js`
Contents:
```js
/**
* Reconcile three historical scripts that all added columns to the
* `collections` table:
*
* - scripts/add-collaboration-features.js (visibility, tcg, tags +
* idx_collections_visibility index) — collections-table half only;
* the collection_permissions / collection_activity / users.is_pending
* half is reconciled by migrations/1781000000003_reconcile-collaboration-tables.js
* - scripts/add-collection-slugs.js (slug + idx_collections_slug
* unique index + check_slug_format CHECK constraint)
* - scripts/add-image-column.js (image)
*
* Grouped into one migration per D1 of .convoys/reconcile-historical-add-scripts.md
* (one migration per table/feature surface). All historical DDL was
* idempotent (ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS);
* this migration preserves that. CHECK constraint adds via a
* DO $$ EXCEPTION block because Postgres doesn't accept
* ADD CONSTRAINT ... IF NOT EXISTS for CHECK.
*
* Per-row slug backfill DML from add-collection-slugs.js is intentionally
* NOT folded in — fresh envs have no pre-existing collections to
* backfill; on prod, the backfill ran once historically and is baked in.
*
* Notes on adjacent state:
* - `is_system_collection` was added by migrations/1780378340194_system-collection-description.js
* (and the runtime register hook at pages/api/auth/register.js:97-119
* creates the per-user system collection row — Finding 4 RESOLVED in
* .convoys/reconcile-historical-add-scripts.md).
*
* Two-column-visibility smell: `is_public BOOLEAN` (from initial-schema)
* and `visibility VARCHAR(20)` (from this migration) coexist on prod.
* Surfaced as docs/SCHEMA_MAP.md § "Known schema smells" #2 → queued
* `unify-collection-visibility` is OUT OF SCOPE here; this migration
* adds `visibility` for parity, nothing more.
*
* Idempotent re-apply: every statement uses IF NOT EXISTS or the
* exception-swallowing DO block.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
*/
export const up = (pgm) => {
pgm.sql(`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'private',
ADD COLUMN IF NOT EXISTS tcg VARCHAR(50) DEFAULT 'MTG',
ADD COLUMN IF NOT EXISTS tags TEXT,
ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE,
ADD COLUMN IF NOT EXISTS image TEXT;
CREATE INDEX IF NOT EXISTS idx_collections_visibility
ON collections (visibility);
CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug
ON collections (slug);
DO $$
BEGIN
ALTER TABLE collections
ADD CONSTRAINT check_slug_format
CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
};
/**
* Down-migration intentionally throws. Removing these columns on
* long-lived envs would drop user-curated tag / slug / image data
* and break the runtime code that reads collections.visibility,
* collections.slug, collections.image, collections.tcg, collections.tags.
*
* If a future schema correction needs to mutate any of these columns,
* write a NEW dated migration with a real `down()` — do NOT roll back
* this one.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000002_reconcile-collections-columns] Down not supported. ' +
'Dropping collections.visibility / tcg / tags / slug / image would erase ' +
'user-curated data and break runtime reads. Write a new dated migration ' +
'for any future schema correction.'
);
};
```
## Conventions to follow
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — one migration per table/feature surface.
- `.cursor/rules/no-go-zones.mdc` — all three source scripts are append-only history. **Do not edit any of them.**
- Style: raw `pgm.sql(...)` template literals, matching the other migrations under `migrations/`. Per D2 of the convoy file.
- ESM exports; `"type": "module"` per `package.json` line 5.
- `IF NOT EXISTS` on every ALTER and CREATE INDEX. CHECK constraint wraps in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;` (Postgres doesn't accept `IF NOT EXISTS` directly on CHECK constraints).
## Acceptance criteria
- [ ] `migrations/1781000000002_reconcile-collections-columns.js` exists with the exact filename above (the timestamp `1781000000002` is the reservation token assigned by the architect — do NOT use `npm run migrate create`).
- [ ] The file's `up()` adds 5 columns (`visibility`, `tcg`, `tags`, `slug`, `image`) via `IF NOT EXISTS`, creates 2 indexes (`idx_collections_visibility`, unique `idx_collections_slug`) via `IF NOT EXISTS`, and adds the `check_slug_format` CHECK via a `DO $$ EXCEPTION` block.
- [ ] The file's `down()` throws with a clear message.
- [ ] The file's docstring cites all three source scripts + the convoy file + the visibility smell (#2 in SCHEMA_MAP).
- [ ] Per-row slug backfill DML is NOT in the migration (out of scope per the convoy plan).
- [ ] `node --check migrations/1781000000002_reconcile-collections-columns.js` passes.
- [ ] `node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
- [ ] `npm run lint` matches the baseline (no new errors).
- [ ] `npm run test:run` reports 21/21 passing.
## Verification
Run from the convoy worktree BEFORE opening the PR:
```bash
node --check migrations/1781000000002_reconcile-collections-columns.js
node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
npm run lint
npm run test:run
```
Expected:
- `node --check` exits 0 silently.
- `node -e` prints `function function undefined`.
- `npm run lint` matches baseline.
- `npm run test:run` reports 21/21 passing.
**Do NOT** run `npm run migrate up` against any environment — that's the post-merge operator step covered by Brief 7's verification runbook.
## Commit message
```
feat(migrations): reconcile collections columns into migration history (brief 2/7)
Captures the DDL effect of three historical scripts into one new
node-pg-migrate migration:
- scripts/add-collaboration-features.js (collections half: visibility,
tcg, tags + idx_collections_visibility)
- scripts/add-collection-slugs.js (slug + idx_collections_slug unique
+ check_slug_format CHECK constraint)
- scripts/add-image-column.js (image)
The collaboration tables half (collection_permissions, collection_activity,
users.is_pending) is reconciled separately by Brief 3.
Per-row slug backfill DML from add-collection-slugs.js is intentionally
NOT folded in — fresh envs have no pre-existing collections to backfill.
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B2.
Idempotent re-apply (IF NOT EXISTS guards; DO $$ EXCEPTION for CHECK).
```
## PR shape
**Title:** `feat(migrations): reconcile collections columns into migration history (brief 2/7)`
**Body template:**
```markdown
Brief 2 of the `reconcile-historical-add-scripts` convoy. See
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
## What this PR does
Adds `migrations/1781000000002_reconcile-collections-columns.js`
captures the DDL effect of three historical scripts in a single new
`node-pg-migrate` migration:
- `scripts/add-collaboration-features.js` — collections columns half
(`visibility VARCHAR(20)`, `tcg VARCHAR(50)`, `tags TEXT`,
`idx_collections_visibility` index)
- `scripts/add-collection-slugs.js``slug VARCHAR(100) UNIQUE`,
`idx_collections_slug` unique index, `check_slug_format` CHECK
constraint
- `scripts/add-image-column.js``image TEXT`
All ALTERs use `ADD COLUMN IF NOT EXISTS`. The CHECK constraint wraps
in a `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;`
block because Postgres doesn't accept `ADD CONSTRAINT ... IF NOT EXISTS`
for CHECK.
## What this PR does NOT do
- Does **NOT** edit any of the three source scripts (no-go-zones).
- Does **NOT** capture the collaboration tables half of
`add-collaboration-features.js` (`collection_permissions`,
`collection_activity`, `users.is_pending`, related indexes,
owner-permission backfill DML) — that's **Brief 3**.
- Does **NOT** fold in the per-row slug backfill DML from
`add-collection-slugs.js` — fresh envs have no pre-existing
collections to backfill.
- Does **NOT** unify the `is_public` / `visibility` redundancy
(SCHEMA_MAP smell #2) — that's a future `unify-collection-visibility`
follow-up.
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
or AGENTS.md.
- Does **NOT** run `npm run migrate up` against any environment.
## Verification checklist
- [ ] `node --check migrations/1781000000002_reconcile-collections-columns.js` exits 0
- [ ] `node -e "import('./migrations/1781000000002_reconcile-collections-columns.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
- [ ] `npm run lint` matches baseline
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Did not run `npm run migrate up` against any environment in this PR
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Source scripts (no-go-zones, audit reference only):
`scripts/add-collaboration-features.js`,
`scripts/add-collection-slugs.js`,
`scripts/add-image-column.js`
```
## DO NOT
- DO NOT edit `scripts/add-collaboration-features.js`, `scripts/add-collection-slugs.js`, `scripts/add-image-column.js`, or any other file under `scripts/` — append-only no-go-zone.
- DO NOT edit any other file under `migrations/`.
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
- DO NOT run `npm run migrate up` against any environment.
- DO NOT include the per-row slug backfill DML — out of scope.
- DO NOT add work for `collection_permissions` / `collection_activity` / `users.is_pending` — that's Brief 3.
- DO NOT call `npm run migrate create` — it would generate a `Date.now()` timestamp colliding with B1/B3/B4/B5's reservation tokens.
## Rationale (≤3 sentences)
Grouping all `collections`-table DDL into one migration matches the per-table grouping of D1 and keeps the diff focused on a single surface. Splitting `add-collaboration-features.js` between this brief (columns) and Brief 3 (tables) avoids creating an artificial dependency between two parallel briefs — each writes only its own new file. The CHECK-constraint exception block matches the historical script's try/catch pattern and is the idiomatic Postgres way to do idempotent CHECK adds.

View file

@ -1,318 +0,0 @@
---
convoy: reconcile-historical-add-scripts
brief_number: 3
depends_on: []
files:
- migrations/1781000000003_reconcile-collaboration-tables.js
---
# Brief 3: Reconcile collaboration tables into migration history
## Goal (1 sentence)
Capture the `collection_permissions` + `collection_activity` table creation, the `users.is_pending` column, and the 3 related indexes from `scripts/add-collaboration-features.js` into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the collaboration / sharing surface after `npm run setup-db`.
## Scope (files in scope — do not edit anything else)
- `migrations/1781000000003_reconcile-collaboration-tables.js` — **new**
## Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
`scripts/add-collaboration-features.js` lines 24-81 (relevant DDL only; the collections-columns half is reconciled by Brief 2):
```js
// collection_permissions table
await sql`
CREATE TABLE IF NOT EXISTS collection_permissions (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'pending', 'declined')),
invite_token VARCHAR(255) UNIQUE,
invited_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, user_id)
)
`;
// collection_activity table
await sql`
CREATE TABLE IF NOT EXISTS collection_activity (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`;
// users.is_pending column
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false
`;
// 3 indexes (the 4th — idx_collections_visibility — is Brief 2's scope)
await sql`
CREATE INDEX IF NOT EXISTS idx_collection_permissions_collection_id
ON collection_permissions(collection_id)
`;
await sql`
CREATE INDEX IF NOT EXISTS idx_collection_permissions_user_id
ON collection_permissions(user_id)
`;
await sql`
CREATE INDEX IF NOT EXISTS idx_collection_activity_collection_id
ON collection_activity(collection_id)
`;
```
**Owner-permission backfill DML** at lines 84-99 inserts `('owner', 'active')` rows for every pre-existing collection. **Do NOT fold the backfill DML into this migration.** Fresh envs have no pre-existing collections needing backfill; on prod, the backfill ran once historically and is baked in. The current runtime invariant for new collection creation lives in `pages/api/collections.js` (verify post-merge if needed — out of scope for this brief).
## Target migration file
Path: `migrations/1781000000003_reconcile-collaboration-tables.js`
Contents:
```js
/**
* Reconcile the collaboration / sharing half of
* scripts/add-collaboration-features.js into the migration history:
*
* - CREATE TABLE collection_permissions (with role/status CHECK
* constraints inline + invite_token UNIQUE + UNIQUE(collection_id,
* user_id))
* - CREATE TABLE collection_activity (with JSONB details column)
* - ALTER users ADD COLUMN is_pending BOOLEAN DEFAULT false
* - 3 indexes (idx_collection_permissions_collection_id,
* idx_collection_permissions_user_id,
* idx_collection_activity_collection_id)
*
* The 4th index from the source script (idx_collections_visibility)
* is reconciled by migrations/1781000000002_reconcile-collections-columns.js
* because it indexes a column added in that brief.
*
* The collections-columns half (visibility, tcg, tags) of
* add-collaboration-features.js is reconciled by
* migrations/1781000000002_reconcile-collections-columns.js.
*
* The owner-permission backfill DML from the source script (INSERT
* INTO collection_permissions ... 'owner', 'active' for every
* pre-existing collection) is intentionally NOT folded in — fresh
* envs have no pre-existing collections to backfill; on prod, the
* backfill ran once historically and is baked in. The runtime
* invariant for owner-permission creation on new collections is the
* responsibility of pages/api/collections.js (out of scope here).
*
* Both CREATE TABLE statements use IF NOT EXISTS, with CHECK
* constraints declared inline (no idempotency issue — IF NOT EXISTS
* on the parent table makes the whole CREATE a no-op when the table
* already exists, CHECK constraints and all).
*
* Idempotent re-apply: every statement uses IF NOT EXISTS.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
*/
export const up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS collection_permissions (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'pending', 'declined')),
invite_token VARCHAR(255) UNIQUE,
invited_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, user_id)
);
CREATE TABLE IF NOT EXISTS collection_activity (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL,
details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE users
ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false;
CREATE INDEX IF NOT EXISTS idx_collection_permissions_collection_id
ON collection_permissions (collection_id);
CREATE INDEX IF NOT EXISTS idx_collection_permissions_user_id
ON collection_permissions (user_id);
CREATE INDEX IF NOT EXISTS idx_collection_activity_collection_id
ON collection_activity (collection_id);
`);
};
/**
* Down-migration intentionally throws. Dropping collection_permissions
* + collection_activity on a long-lived env would erase every active
* sharing relationship + every audit trail row. The runtime in
* pages/api/collections/[identifier]/permissions.js, pages/api/invite/*.js,
* and lib/permission-middleware.js all read these tables; rolling back
* would break the live app.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000003_reconcile-collaboration-tables] Down not supported. ' +
'Dropping collection_permissions / collection_activity would erase every ' +
'sharing relationship and audit trail, and break runtime reads in ' +
'pages/api/collections/[identifier]/permissions.js, pages/api/invite/*.js, ' +
'lib/permission-middleware.js. Write a new dated migration for any future ' +
'schema correction.'
);
};
```
## Conventions to follow
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical; one migration per feature surface.
- `.cursor/rules/no-go-zones.mdc``scripts/add-collaboration-features.js` is append-only history. **Do not edit it.**
- Style: raw `pgm.sql(...)` template literals matching the other migrations. Per D2.
- ESM exports; `"type": "module"`.
- `IF NOT EXISTS` on every CREATE / ALTER. Inline CHECK constraints on `CREATE TABLE` are fine — `CREATE TABLE IF NOT EXISTS` skips the entire statement (constraints and all) when the table exists.
- FK declarations match the source script verbatim (`ON DELETE CASCADE` for primary FKs, `ON DELETE SET NULL` where the source uses it).
## Acceptance criteria
- [ ] `migrations/1781000000003_reconcile-collaboration-tables.js` exists with the exact filename above.
- [ ] The file's `up()`:
- Creates `collection_permissions` table with all 10 columns + role/status CHECKs + invite_token UNIQUE + UNIQUE(collection_id, user_id), all via `CREATE TABLE IF NOT EXISTS`.
- Creates `collection_activity` table with 6 columns including JSONB `details`, via `CREATE TABLE IF NOT EXISTS`.
- Adds `users.is_pending BOOLEAN DEFAULT false` via `ADD COLUMN IF NOT EXISTS`.
- Creates 3 indexes via `CREATE INDEX IF NOT EXISTS`.
- [ ] The 4th index from the source script (`idx_collections_visibility`) is NOT in this migration — it belongs to Brief 2.
- [ ] The owner-permission backfill DML is NOT in this migration.
- [ ] The file's `down()` throws with a clear message.
- [ ] The file's docstring cites the source script + the convoy file + the Brief 2 split.
- [ ] `node --check migrations/1781000000003_reconcile-collaboration-tables.js` passes.
- [ ] `node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
- [ ] `npm run lint` matches baseline.
- [ ] `npm run test:run` reports 21/21 passing.
## Verification
```bash
node --check migrations/1781000000003_reconcile-collaboration-tables.js
node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
npm run lint
npm run test:run
```
Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass.
**Do NOT** run `npm run migrate up` against any environment.
## Commit message
```
feat(migrations): reconcile collaboration tables into migration history (brief 3/7)
Captures the collaboration / sharing half of
scripts/add-collaboration-features.js into one new node-pg-migrate
migration:
- CREATE TABLE collection_permissions (role/status CHECKs,
invite_token UNIQUE, UNIQUE(collection_id, user_id))
- CREATE TABLE collection_activity (JSONB details)
- ALTER users ADD COLUMN is_pending BOOLEAN DEFAULT false
- 3 indexes
The collections-columns half (visibility, tcg, tags +
idx_collections_visibility) is reconciled by Brief 2. The 4th index
(idx_collections_visibility) belongs to Brief 2 because it indexes a
column added there.
Per-row owner-permission backfill DML from the source script is
intentionally NOT folded in — fresh envs have no pre-existing
collections to backfill; the runtime invariant for new-collection
owner-perm creation lives in pages/api/collections.js.
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B3.
Idempotent re-apply (IF NOT EXISTS guards).
```
## PR shape
**Title:** `feat(migrations): reconcile collaboration tables into migration history (brief 3/7)`
**Body template:**
```markdown
Brief 3 of the `reconcile-historical-add-scripts` convoy. See
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
## What this PR does
Adds `migrations/1781000000003_reconcile-collaboration-tables.js`
captures the collaboration / sharing DDL of
`scripts/add-collaboration-features.js` (the table-and-column half;
the columns-on-collections half is Brief 2).
- `CREATE TABLE IF NOT EXISTS collection_permissions` (10 columns
including inline role/status CHECK constraints + invite_token
UNIQUE + UNIQUE(collection_id, user_id))
- `CREATE TABLE IF NOT EXISTS collection_activity` (6 columns
including JSONB `details`)
- `ALTER TABLE users ADD COLUMN IF NOT EXISTS is_pending BOOLEAN DEFAULT false`
- 3 indexes via `CREATE INDEX IF NOT EXISTS`
## What this PR does NOT do
- Does **NOT** edit `scripts/add-collaboration-features.js` (no-go-zone).
- Does **NOT** capture the columns-on-collections half (`visibility`,
`tcg`, `tags`, `idx_collections_visibility`) — that's **Brief 2**.
- Does **NOT** fold in the per-row owner-permission backfill DML from
the source script — fresh envs have no pre-existing collections to
backfill.
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
`AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
- Does **NOT** run `npm run migrate up` against any environment.
## Verification checklist
- [ ] `node --check migrations/1781000000003_reconcile-collaboration-tables.js` exits 0
- [ ] `node -e "import('./migrations/1781000000003_reconcile-collaboration-tables.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
- [ ] `npm run lint` matches baseline
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Did not run `npm run migrate up` against any environment in this PR
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Source script (no-go-zone, audit reference only):
`scripts/add-collaboration-features.js`
```
## DO NOT
- DO NOT edit `scripts/add-collaboration-features.js` or any file under `scripts/`.
- DO NOT edit any other file under `migrations/`.
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md`.
- DO NOT run `npm run migrate up` against any environment.
- DO NOT include the owner-permission backfill DML.
- DO NOT add work for the collections-columns half (`visibility`, `tcg`, `tags`, `idx_collections_visibility`) — that's Brief 2.
- DO NOT call `npm run migrate create`.
## Rationale (≤3 sentences)
Splitting `add-collaboration-features.js` between Brief 2 (collections columns + the visibility index that indexes one of those columns) and Brief 3 (collaboration tables + the users.is_pending column + the 3 indexes that index collaboration-table columns) keeps each migration scoped to the table surface it touches, matching D1. Inline CHECK constraints on `CREATE TABLE` are idempotent for free under `CREATE TABLE IF NOT EXISTS` (the whole statement no-ops when the table exists). The backfill DML is intentionally out of scope because fresh envs need no backfill and prod's backfill already ran.

View file

@ -1,241 +0,0 @@
---
convoy: reconcile-historical-add-scripts
brief_number: 4
depends_on: []
files:
- migrations/1781000000004_reconcile-favorites-system.js
---
# Brief 4: Reconcile favorites system into migration history
## Goal (1 sentence)
Capture the `user_favorites` table and its 4 indexes from `scripts/add-favorites-system.js` into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the favorites surface after `npm run setup-db`.
## Scope (files in scope — do not edit anything else)
- `migrations/1781000000004_reconcile-favorites-system.js` — **new**
## Source script (read-only audit reference; DO NOT EDIT — no-go-zone)
`scripts/add-favorites-system.js` lines 11-27 (entire DDL — script has no DML beyond the table create):
```js
await sql`
CREATE TABLE IF NOT EXISTS user_favorites (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
item_type VARCHAR(50) NOT NULL, -- 'card', 'collection', 'deck'
item_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, item_type, item_id)
)
`;
// 4 indexes
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id ON user_favorites(user_id)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type ON user_favorites(item_type)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id ON user_favorites(item_id)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON user_favorites(user_id, item_type)`;
```
**Note on SCHEMA_MAP drift.** `docs/SCHEMA_MAP.md` § `user_favorites` (lines 104-111) shows the table with only `(user_id, card_id)` columns — that's a **doc bug**. The actual source script (and prod schema) uses the polymorphic `(item_type, item_id)` shape that supports cards, collections, AND decks (per the script's inline comment + the runtime usage in `pages/api/favorites.js`). B7 corrects the SCHEMA_MAP entry. This brief faithfully reproduces the **script's** shape — `(item_type, item_id)` — not the doc's.
## Target migration file
Path: `migrations/1781000000004_reconcile-favorites-system.js`
Contents:
```js
/**
* Reconcile scripts/add-favorites-system.js into the migration history.
*
* Creates user_favorites with the polymorphic (item_type, item_id)
* shape that supports favoriting cards, collections, and decks via a
* single table. Adds 4 indexes for the common query shapes:
*
* - idx_user_favorites_user_id — "all favorites for user X"
* - idx_user_favorites_item_type — "all card favorites" / "all deck favorites"
* - idx_user_favorites_item_id — back-link from an item to its favoriters
* - idx_user_favorites_user_type — composite for "user X's card favorites"
*
* Note: docs/SCHEMA_MAP.md § user_favorites currently shows only
* (user_id, card_id) — that's a doc bug. The actual prod shape (and
* the source script, and the runtime in pages/api/favorites.js) uses
* the polymorphic shape. B7 of this convoy corrects the SCHEMA_MAP
* entry; this migration reproduces the script's shape faithfully.
*
* Idempotent re-apply: CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
*/
export const up = (pgm) => {
pgm.sql(`
CREATE TABLE IF NOT EXISTS user_favorites (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
item_type VARCHAR(50) NOT NULL,
item_id INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, item_type, item_id)
);
CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id
ON user_favorites (user_id);
CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type
ON user_favorites (item_type);
CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id
ON user_favorites (item_id);
CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type
ON user_favorites (user_id, item_type);
`);
};
/**
* Down-migration intentionally throws. Dropping user_favorites on a
* long-lived env would erase every user's saved favorites list and
* break runtime reads in pages/api/favorites.js.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000004_reconcile-favorites-system] Down not supported. ' +
'Dropping user_favorites would erase every saved favorite and break ' +
'pages/api/favorites.js. Write a new dated migration for any future ' +
'schema correction.'
);
};
```
## Conventions to follow
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical.
- `.cursor/rules/no-go-zones.mdc``scripts/add-favorites-system.js` is append-only history.
- Style: raw `pgm.sql(...)` template literals. Per D2.
- ESM exports; `"type": "module"`.
- `IF NOT EXISTS` on every CREATE.
- Match the source script's column types, FK ON DELETE rule (`CASCADE`), and UNIQUE shape verbatim.
## Acceptance criteria
- [ ] `migrations/1781000000004_reconcile-favorites-system.js` exists with the exact filename above.
- [ ] The file's `up()` creates `user_favorites` with all 5 columns + UNIQUE constraint, then 4 indexes, all via `IF NOT EXISTS`.
- [ ] The column shape is `(item_type VARCHAR(50), item_id INTEGER)` — the polymorphic shape — NOT `(card_id)`. See "Note on SCHEMA_MAP drift" above.
- [ ] The file's `down()` throws with a clear message.
- [ ] The file's docstring cites the source script + the convoy file + the SCHEMA_MAP doc-bug note.
- [ ] `node --check migrations/1781000000004_reconcile-favorites-system.js` passes.
- [ ] `node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
- [ ] `npm run lint` matches baseline.
- [ ] `npm run test:run` reports 21/21 passing.
## Verification
```bash
node --check migrations/1781000000004_reconcile-favorites-system.js
node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
npm run lint
npm run test:run
```
Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass.
**Do NOT** run `npm run migrate up` against any environment.
## Commit message
```
feat(migrations): reconcile favorites system into migration history (brief 4/7)
Captures scripts/add-favorites-system.js into one new node-pg-migrate
migration:
- CREATE TABLE user_favorites (polymorphic (item_type, item_id)
shape; UNIQUE(user_id, item_type, item_id))
- 4 indexes (user_id, item_type, item_id, (user_id, item_type))
Note: docs/SCHEMA_MAP.md § user_favorites currently shows the table
with only (user_id, card_id) — that's a doc bug; B7 of this convoy
corrects the entry. This migration faithfully reproduces the source
script's polymorphic shape (also what runtime in pages/api/favorites.js
uses).
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B4.
Idempotent re-apply (IF NOT EXISTS guards).
```
## PR shape
**Title:** `feat(migrations): reconcile favorites system into migration history (brief 4/7)`
**Body template:**
```markdown
Brief 4 of the `reconcile-historical-add-scripts` convoy. See
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
## What this PR does
Adds `migrations/1781000000004_reconcile-favorites-system.js`
captures `scripts/add-favorites-system.js` into a single new
`node-pg-migrate` migration.
- `CREATE TABLE IF NOT EXISTS user_favorites` with polymorphic
`(item_type VARCHAR(50), item_id INTEGER)` shape supporting
cards / collections / decks favorites in one table.
- `UNIQUE(user_id, item_type, item_id)` prevents duplicate favorites.
- 4 supporting indexes via `CREATE INDEX IF NOT EXISTS`.
## Note on a SCHEMA_MAP doc bug
`docs/SCHEMA_MAP.md` § `user_favorites` (current `main`) shows the
table with only `(user_id, card_id)` columns. That's a doc bug — the
actual prod shape (and the source script, and `pages/api/favorites.js`
runtime usage) uses the polymorphic `(item_type, item_id)` shape. This
migration faithfully reproduces the script's shape; Brief 7 of this
convoy fixes the SCHEMA_MAP entry.
## What this PR does NOT do
- Does **NOT** edit `scripts/add-favorites-system.js` (no-go-zone).
- Does **NOT** edit `docs/SCHEMA_MAP.md` (Brief 7's job).
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
or `AGENTS.md`.
- Does **NOT** run `npm run migrate up` against any environment.
## Verification checklist
- [ ] `node --check migrations/1781000000004_reconcile-favorites-system.js` exits 0
- [ ] `node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
- [ ] `npm run lint` matches baseline
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Did not run `npm run migrate up` against any environment in this PR
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Source script (no-go-zone, audit reference only): `scripts/add-favorites-system.js`
```
## DO NOT
- DO NOT edit `scripts/add-favorites-system.js` or any file under `scripts/`.
- DO NOT edit any other file under `migrations/`.
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
- DO NOT run `npm run migrate up` against any environment.
- DO NOT use the SCHEMA_MAP `(user_id, card_id)` shape — that doc entry is wrong; reproduce the SOURCE SCRIPT'S polymorphic shape.
- DO NOT call `npm run migrate create`.
## Rationale (≤3 sentences)
The favorites system is a single self-contained table — natural fit for one small migration that maps 1:1 with the historical script. The polymorphic shape (`item_type`, `item_id`) is what runtime code actually uses, so reproducing it from the script's verbatim DDL — rather than the stale `(user_id, card_id)` doc entry — is the safe choice. Brief 7's SCHEMA_MAP rewrite resolves the doc-bug separately so this brief stays scoped to one new file.

View file

@ -1,425 +0,0 @@
---
convoy: reconcile-historical-add-scripts
brief_number: 5
depends_on: []
files:
- migrations/1781000000005_reconcile-user-profile.js
---
# Brief 5: Reconcile user profile fields into migration history
## Goal (1 sentence)
Capture the deduplicated union of `scripts/add-user-profile-columns.js` and `scripts/add-user-profile-fields.js` (15 new `users` columns, 2 new tables `user_settings` + `user_avatars`, 6 CHECK constraints, 6 indexes) into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the user-profile surface after `npm run setup-db`.
## Scope (files in scope — do not edit anything else)
- `migrations/1781000000005_reconcile-user-profile.js` — **new**
## Source scripts (read-only audit reference; DO NOT EDIT — both are no-go-zones)
### `scripts/add-user-profile-columns.js` lines 12-18 (the earlier, narrower script):
```js
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
ADD COLUMN IF NOT EXISTS profile_image_url TEXT
`;
```
Plus per-row UPDATE backfill of defaults (lines 25-42). **Do NOT fold the backfill DML into this migration** — fresh envs have no rows to backfill; column DEFAULTs handle new rows.
### `scripts/add-user-profile-fields.js` lines 27-115 (the later, broader script — superset of #8 plus additional columns + 2 new tables + 6 CHECK constraints + 6 indexes):
```js
// Basic profile fields (overlaps add-user-profile-columns.js for first_name/last_name/username,
// but ADDS bio + avatar_url; idempotent overlap because of IF NOT EXISTS)
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS avatar_url TEXT
`;
// Preference fields
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS favorite_games JSONB DEFAULT '["MTG"]',
ADD COLUMN IF NOT EXISTS collection_visibility VARCHAR(20) DEFAULT 'private',
ADD COLUMN IF NOT EXISTS preferred_currency VARCHAR(3) DEFAULT 'USD',
ADD COLUMN IF NOT EXISTS cards_per_page INTEGER DEFAULT 50,
ADD COLUMN IF NOT EXISTS default_view VARCHAR(10) DEFAULT 'grid'
`;
// Notification + 2FA settings
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS notifications_email BOOLEAN DEFAULT true,
ADD COLUMN IF NOT EXISTS notifications_marketing BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN DEFAULT false
`;
// Display settings
await sql`
ALTER TABLE users
ADD COLUMN IF NOT EXISTS theme VARCHAR(10) DEFAULT 'system',
ADD COLUMN IF NOT EXISTS language VARCHAR(5) DEFAULT 'en'
`;
// user_settings table
await sql`
CREATE TABLE IF NOT EXISTS user_settings (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
setting_key VARCHAR(100) NOT NULL,
setting_value JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, setting_key)
)
`;
// user_avatars table
await sql`
CREATE TABLE IF NOT EXISTS user_avatars (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255),
mime_type VARCHAR(100),
file_size INTEGER,
file_path TEXT NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`;
// 6 indexes
await sql`CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)`;
await sql`CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_settings_key ON user_settings(setting_key)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_avatars_user_id ON user_avatars(user_id)`;
await sql`CREATE INDEX IF NOT EXISTS idx_user_avatars_active ON user_avatars(user_id, is_active)`;
// 6 CHECK constraints (each wrapped in JS try/catch to swallow 'already exists')
await sql`ALTER TABLE users ADD CONSTRAINT check_collection_visibility CHECK (collection_visibility IN ('private', 'public', 'unlisted'))`;
await sql`ALTER TABLE users ADD CONSTRAINT check_preferred_currency CHECK (preferred_currency IN ('USD', 'EUR', 'GBP', 'CAD', 'JPY'))`;
await sql`ALTER TABLE users ADD CONSTRAINT check_cards_per_page CHECK (cards_per_page IN (25, 50, 100))`;
await sql`ALTER TABLE users ADD CONSTRAINT check_default_view CHECK (default_view IN ('grid', 'list'))`;
await sql`ALTER TABLE users ADD CONSTRAINT check_theme CHECK (theme IN ('light', 'dark', 'system'))`;
await sql`ALTER TABLE users ADD CONSTRAINT check_language CHECK (language IN ('en', 'es', 'fr', 'de', 'ja'))`;
```
Plus a per-row UPDATE backfill of defaults (lines 198-222). **Do NOT fold the backfill DML in** — column DEFAULTs handle new rows; fresh envs have no rows to backfill.
### Important note on the column dedup
Scripts #8 and #9 overlap on `first_name`, `last_name`, `username` — both use `ADD COLUMN IF NOT EXISTS` so on prod the second-running script no-ops those three columns. Both scripts have run on prod, so the union of their columns is what's actually present:
- From #8 only: `profile_image_url` (TEXT) — a column that ONLY #8 added.
- From #9 only: `bio`, `avatar_url`, `favorite_games`, `collection_visibility`, `preferred_currency`, `cards_per_page`, `default_view`, `notifications_email`, `notifications_marketing`, `two_factor_enabled`, `theme`, `language` (12 columns) + the 2 new tables + 6 CHECK constraints + 6 indexes.
- From both (idempotent overlap): `first_name`, `last_name`, `username`.
The migration must add **all 15 columns** (4 from #8 12 from #9 with 3 in the intersection = 4 + 12 - 3 = 13 unique users columns; wait, let me recount: #8 adds 4 (first_name, last_name, username, profile_image_url); #9 adds 5 basic (first_name, last_name, username, bio, avatar_url) + 5 prefs + 3 notif + 2 display = 15. Union: first_name, last_name, username (shared) + profile_image_url (#8) + bio, avatar_url, favorite_games, collection_visibility, preferred_currency, cards_per_page, default_view, notifications_email, notifications_marketing, two_factor_enabled, theme, language (#9) = **3 + 1 + 12 = 16 columns**). So the migration adds 16 columns to `users`.
The redundant `profile_image_url` vs `avatar_url` pair is documented in `docs/SCHEMA_MAP.md` § "Known schema smells" #1 and surfaced as the `unify-user-avatar-column` follow-up. Both must be in fresh envs for parity.
## Target migration file
Path: `migrations/1781000000005_reconcile-user-profile.js`
Contents:
```js
/**
* Reconcile two overlapping historical user-profile scripts into the
* migration history, taking the union of their effects:
*
* - scripts/add-user-profile-columns.js (the earlier, narrower
* script): first_name, last_name, username UNIQUE, profile_image_url
*
* - scripts/add-user-profile-fields.js (the later, broader script;
* overlaps the earlier script on first_name / last_name / username
* and additionally adds): bio, avatar_url, favorite_games (JSONB
* DEFAULT '["MTG"]'), collection_visibility, preferred_currency,
* cards_per_page, default_view, notifications_email,
* notifications_marketing, two_factor_enabled, theme, language,
* + the new user_settings + user_avatars tables, + 6 CHECK
* constraints, + 6 indexes.
*
* Result on a fresh env: 16 new columns on `users`, 2 new tables,
* 6 CHECK constraints, 6 indexes. On any long-lived env: every
* statement is a no-op (IF NOT EXISTS / DO $$ EXCEPTION).
*
* Per-row UPDATE backfills from both scripts are intentionally NOT
* folded in — column DEFAULTs handle new rows; fresh envs have no
* rows to backfill.
*
* The profile_image_url / avatar_url redundancy is intentional for
* parity with prod and is flagged in docs/SCHEMA_MAP.md § "Known
* schema smells" #1; future cleanup is the queued
* `unify-user-avatar-column` follow-up.
*
* CHECK constraint adds wrap in DO $$ ... EXCEPTION WHEN
* duplicate_object THEN NULL END $$ because Postgres doesn't accept
* ADD CONSTRAINT ... IF NOT EXISTS for CHECK. Each constraint gets
* its own DO block so a failure in one doesn't block the rest.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
*/
export const up = (pgm) => {
pgm.sql(`
-- 16 columns on users (union of add-user-profile-columns.js + add-user-profile-fields.js)
ALTER TABLE users
ADD COLUMN IF NOT EXISTS first_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS last_name VARCHAR(255),
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
ADD COLUMN IF NOT EXISTS profile_image_url TEXT,
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS avatar_url TEXT,
ADD COLUMN IF NOT EXISTS favorite_games JSONB DEFAULT '["MTG"]',
ADD COLUMN IF NOT EXISTS collection_visibility VARCHAR(20) DEFAULT 'private',
ADD COLUMN IF NOT EXISTS preferred_currency VARCHAR(3) DEFAULT 'USD',
ADD COLUMN IF NOT EXISTS cards_per_page INTEGER DEFAULT 50,
ADD COLUMN IF NOT EXISTS default_view VARCHAR(10) DEFAULT 'grid',
ADD COLUMN IF NOT EXISTS notifications_email BOOLEAN DEFAULT true,
ADD COLUMN IF NOT EXISTS notifications_marketing BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS theme VARCHAR(10) DEFAULT 'system',
ADD COLUMN IF NOT EXISTS language VARCHAR(5) DEFAULT 'en';
-- user_settings table
CREATE TABLE IF NOT EXISTS user_settings (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
setting_key VARCHAR(100) NOT NULL,
setting_value JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, setting_key)
);
-- user_avatars table
CREATE TABLE IF NOT EXISTS user_avatars (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255),
mime_type VARCHAR(100),
file_size INTEGER,
file_path TEXT NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 6 indexes
CREATE INDEX IF NOT EXISTS idx_users_username ON users (username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings (user_id);
CREATE INDEX IF NOT EXISTS idx_user_settings_key ON user_settings (setting_key);
CREATE INDEX IF NOT EXISTS idx_user_avatars_user_id ON user_avatars (user_id);
CREATE INDEX IF NOT EXISTS idx_user_avatars_active ON user_avatars (user_id, is_active);
-- 6 CHECK constraints (each in its own DO block so one failure doesn't block the rest)
DO $$ BEGIN
ALTER TABLE users ADD CONSTRAINT check_collection_visibility
CHECK (collection_visibility IN ('private', 'public', 'unlisted'));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE users ADD CONSTRAINT check_preferred_currency
CHECK (preferred_currency IN ('USD', 'EUR', 'GBP', 'CAD', 'JPY'));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE users ADD CONSTRAINT check_cards_per_page
CHECK (cards_per_page IN (25, 50, 100));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE users ADD CONSTRAINT check_default_view
CHECK (default_view IN ('grid', 'list'));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE users ADD CONSTRAINT check_theme
CHECK (theme IN ('light', 'dark', 'system'));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE users ADD CONSTRAINT check_language
CHECK (language IN ('en', 'es', 'fr', 'de', 'ja'));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
`);
};
/**
* Down-migration intentionally throws. Dropping 16 user-profile columns
* + user_settings + user_avatars on a long-lived env would erase every
* user's profile data, preferences, avatar history, and settings, and
* break runtime reads in pages/api/user/{settings,profile,avatar}.js.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000005_reconcile-user-profile] Down not supported. ' +
'Dropping these columns + tables would erase every user profile, preference, ' +
'avatar history, and settings row, and break runtime reads in ' +
'pages/api/user/{settings,profile,avatar}.js. Write a new dated migration ' +
'for any future schema correction.'
);
};
```
## Conventions to follow
- `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical.
- `.cursor/rules/no-go-zones.mdc` — both source scripts are append-only history.
- Style: raw `pgm.sql(...)` template literals. Per D2.
- ESM exports; `"type": "module"`.
- `IF NOT EXISTS` on every ALTER + CREATE INDEX + CREATE TABLE.
- CHECK constraints in `DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;` blocks, one block per constraint.
- Match the source scripts' column types, defaults, FK rules, and CHECK values verbatim.
## Acceptance criteria
- [ ] `migrations/1781000000005_reconcile-user-profile.js` exists with the exact filename above.
- [ ] The file's `up()` adds exactly **16 columns** to `users` (per the dedup count in the source-scripts note above), creates **2 tables** (`user_settings`, `user_avatars`), creates **6 indexes**, and adds **6 CHECK constraints** each in its own `DO $$ EXCEPTION` block.
- [ ] BOTH `profile_image_url` AND `avatar_url` are present (parity with prod; redundancy is documented).
- [ ] The file's `down()` throws with a clear message.
- [ ] The file's docstring cites both source scripts + the convoy file + the `unify-user-avatar-column` follow-up.
- [ ] Per-row UPDATE backfill DML is NOT in the migration.
- [ ] `node --check migrations/1781000000005_reconcile-user-profile.js` passes.
- [ ] `node -e "import('./migrations/1781000000005_reconcile-user-profile.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`.
- [ ] `npm run lint` matches baseline.
- [ ] `npm run test:run` reports 21/21 passing.
## Verification
```bash
node --check migrations/1781000000005_reconcile-user-profile.js
node -e "import('./migrations/1781000000005_reconcile-user-profile.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"
npm run lint
npm run test:run
```
Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass.
**Do NOT** run `npm run migrate up` against any environment.
## Commit message
```
feat(migrations): reconcile user profile fields into migration history (brief 5/7)
Captures the deduplicated union of scripts/add-user-profile-columns.js
and scripts/add-user-profile-fields.js into one new node-pg-migrate
migration:
- 16 columns on users (basic profile + preferences + notifications + display)
- 2 new tables (user_settings, user_avatars)
- 6 CHECK constraints (each in its own DO $$ EXCEPTION block)
- 6 indexes
Both profile_image_url (from script #1) and avatar_url (from script #2)
are added for parity with prod. The redundancy is flagged in
docs/SCHEMA_MAP.md § "Known schema smells" #1 and queued for cleanup as
the `unify-user-avatar-column` follow-up.
Per-row UPDATE backfill DML from both scripts is intentionally NOT
folded in — column DEFAULTs handle new rows; fresh envs have no rows
to backfill.
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B5.
Idempotent re-apply (IF NOT EXISTS guards + per-CHECK DO blocks).
```
## PR shape
**Title:** `feat(migrations): reconcile user profile fields into migration history (brief 5/7)`
**Body template:**
```markdown
Brief 5 of the `reconcile-historical-add-scripts` convoy. See
[`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
## What this PR does
Adds `migrations/1781000000005_reconcile-user-profile.js` — captures
the deduplicated union of `scripts/add-user-profile-columns.js`
(earlier, narrower) and `scripts/add-user-profile-fields.js` (later,
broader) into a single new `node-pg-migrate` migration.
- 16 new columns on `users` (first_name, last_name, username UNIQUE,
profile_image_url, bio, avatar_url, favorite_games JSONB, …, theme,
language) — all `ADD COLUMN IF NOT EXISTS`
- 2 new tables: `user_settings`, `user_avatars` — both
`CREATE TABLE IF NOT EXISTS`
- 6 indexes — `CREATE INDEX IF NOT EXISTS`
- 6 CHECK constraints (each in its own
`DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL END $$;` block)
## Why both `profile_image_url` and `avatar_url`?
Script #1 added `profile_image_url`, script #2 added `avatar_url`
both exist on prod and both are in this migration for fresh-env
parity. The redundancy is flagged in
`docs/SCHEMA_MAP.md` § "Known schema smells" #1 and queued for cleanup
as the `unify-user-avatar-column` follow-up.
## What this PR does NOT do
- Does **NOT** edit either source script (no-go-zones).
- Does **NOT** fold in the per-row UPDATE backfill DML from either
source script — column DEFAULTs handle new rows.
- Does **NOT** unify `profile_image_url` / `avatar_url` — that's the
`unify-user-avatar-column` follow-up.
- Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README,
`AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP).
- Does **NOT** run `npm run migrate up` against any environment.
## Verification checklist
- [ ] `node --check migrations/1781000000005_reconcile-user-profile.js` exits 0
- [ ] `node -e "import('./migrations/1781000000005_reconcile-user-profile.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`
- [ ] `npm run lint` matches baseline
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Did not run `npm run migrate up` against any environment in this PR
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Source scripts (no-go-zones, audit reference only):
`scripts/add-user-profile-columns.js`, `scripts/add-user-profile-fields.js`
```
## DO NOT
- DO NOT edit either source script or any file under `scripts/`.
- DO NOT edit any other file under `migrations/`.
- DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md`.
- DO NOT run `npm run migrate up` against any environment.
- DO NOT pick one of `profile_image_url` / `avatar_url` to omit — both must be present for parity.
- DO NOT collapse the 6 CHECK constraints into a single `DO $$ EXCEPTION` block — one block per constraint so one duplicate doesn't swallow the others.
- DO NOT include the per-row UPDATE backfill DML.
- DO NOT call `npm run migrate create`.
## Rationale (≤3 sentences)
This is the largest single migration in the convoy because both historical scripts are tightly coupled to the `users` table surface and splitting them would create artificial boundaries (e.g., separating "users columns" from "CHECK constraints on users columns" makes no sense). Per-CHECK `DO $$ EXCEPTION` blocks mirror the historical scripts' per-statement try/catch pattern and ensure one duplicate-constraint failure doesn't block the rest. Keeping both avatar-style columns matches prod-as-is and explicitly defers the cleanup to a scoped follow-up convoy.

View file

@ -1,457 +0,0 @@
---
convoy: reconcile-historical-add-scripts
brief_number: 7
depends_on: [1, 2, 3, 4, 5]
files:
- docs/SCHEMA_MAP.md
- docs/MIGRATION_VERIFICATION_RUNBOOK.md
- AGENTS.md
- .convoys/ship-readiness.md
---
# Brief 7: Documentation + verification runbook
## Goal (1 sentence)
Update the four documentation surfaces that describe the post-convoy schema and onboarding state — `docs/SCHEMA_MAP.md` (correct doc bugs + cross-reference new migrations), `docs/MIGRATION_VERIFICATION_RUNBOOK.md` (new — manual operator runbook from D5), `AGENTS.md` Gotcha #6 (cross-reference this convoy as the closing follow-up), `.convoys/ship-readiness.md` (flip this convoy's queued entry to RESOLVED + unblock `retire-graveyard-scripts-after-audit` + add new follow-ups) — so the next operator onboarding a fresh Neon branch can do so by `npm install``npm run setup-db` alone and verify the result.
## Scope (files in scope — do not edit anything else)
- `docs/SCHEMA_MAP.md` — **modified**
- `docs/MIGRATION_VERIFICATION_RUNBOOK.md` — **new**
- `AGENTS.md`**modified** (small Gotcha #6 cross-reference update only)
- `.convoys/ship-readiness.md`**modified** (queued convoys section)
This brief is **sequenced last** because it cross-references the 5 new migration files that B1-B5 add; it cannot land before B1-B5 are merged. The architect's `slice_dependencies` block in the convoy file declares `depends_on: [1, 2, 3, 4, 5]`.
## Per-file scope
### 1. `docs/SCHEMA_MAP.md` — modified
Targeted edits (keep all other content as-is):
1. **Preamble update.** The current preamble (lines 1-18) notes that *"the initial backfill captures only the post-`setup-neon-db.js` shape. … A follow-up convoy (`reconcile-historical-add-scripts`) will fold the historical effects into the migration history; until then this file remains the curated reference for the full prod shape."* Replace with a sentence noting the convoy has **landed**, the migration history now captures the full prod shape, and the operator verification runbook lives at `docs/MIGRATION_VERIFICATION_RUNBOOK.md`. Bump the "Last reviewed" date.
2. **`### users` notes column updates** (lines 43-57): each row references `add-user-profile-columns.js` / `add-user-profile-fields.js` — leave those references in place (they're historical context); ADD a single line at the end of the table noting *"All columns above are now captured by `migrations/1781000000005_reconcile-user-profile.js` (B5 of `reconcile-historical-add-scripts`, 2026-06-14)."*
3. **`### cards` notes column updates** (lines 78-79): for the `quantity` and `favorited` rows, change the "Unused; consider dropping" annotation to *"Unused; captured by `migrations/1781000000001_reconcile-cards-columns.js` for fresh-env parity. Drop tracked as queued `drop-dead-cards-columns` follow-up."*
4. **`### user_cards` index/constraint notes** (lines 90-102): add a brief note clarifying the canonical constraint is `UNIQUE(user_id, card_id, is_foil)` (3-col) from initial-schema, and that `scripts/fix-user-cards-constraints.js`'s stricter 2-col variant was either never-applied or reverted (Finding 1 of the reconcile convoy → Outcome A, 2026-06-14). This is the SCHEMA_MAP equivalent of the Drift Finding 1 resolution.
5. **`### user_favorites` table** (lines 104-111): **FIX THE DOC BUG.** Current shape lists only `(user_id, card_id)`. Replace with the actual polymorphic shape per the runtime in `pages/api/favorites.js`:
```markdown
### user_favorites
| Column | Type | Notes |
| --- | --- | --- |
| `id` | `SERIAL PK` | |
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
| `item_type` | `VARCHAR(50) NOT NULL` | `'card' | 'collection' | 'deck'` — polymorphic |
| `item_id` | `INTEGER NOT NULL` | FK depends on `item_type`; not enforced at DB level |
| `created_at` | `TIMESTAMP` | |
| | | **UNIQUE(user_id, item_type, item_id)** |
Indexes: `idx_user_favorites_user_id`, `idx_user_favorites_item_type`,
`idx_user_favorites_item_id`, `idx_user_favorites_user_type
(user_id, item_type)` — all in `migrations/1781000000004_reconcile-favorites-system.js`.
```
6. **`### collections` notes**: add a line at the bottom noting *"`visibility`, `tcg`, `tags`, `slug` (+ `idx_collections_slug` unique + `check_slug_format` CHECK), `image` are now captured by `migrations/1781000000002_reconcile-collections-columns.js`."*
7. **`### collection_permissions` + `### collection_activity` sections**: add a line at the bottom of each noting *"Captured by `migrations/1781000000003_reconcile-collaboration-tables.js`."*
8. **NEW SECTION: `### user_settings`** (currently a one-liner at line 217). Expand to a proper column table matching the actual schema:
```markdown
### user_settings
Per-user key/value store for settings that don't warrant a column on `users`.
Captured by `migrations/1781000000005_reconcile-user-profile.js`.
| Column | Type | Notes |
| --- | --- | --- |
| `id` | `SERIAL PK` | |
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
| `setting_key` | `VARCHAR(100) NOT NULL` | |
| `setting_value` | `JSONB NOT NULL` | |
| `created_at`, `updated_at` | `TIMESTAMP` default now | |
| | | **UNIQUE(user_id, setting_key)** |
Indexes: `idx_user_settings_user_id`, `idx_user_settings_key`.
```
9. **`### user_avatars` expansion** (currently one paragraph at lines 222-223). Replace with:
```markdown
### user_avatars
Tracks uploaded avatar history. Captured by
`migrations/1781000000005_reconcile-user-profile.js`. Older avatars
are typically deleted from blob storage; verify the cleanup job runs.
| Column | Type | Notes |
| --- | --- | --- |
| `id` | `SERIAL PK` | |
| `user_id` | `INTEGER FK users(id) ON DELETE CASCADE` | |
| `filename` | `VARCHAR(255) NOT NULL` | |
| `original_name` | `VARCHAR(255)` | |
| `mime_type` | `VARCHAR(100)` | |
| `file_size` | `INTEGER` | |
| `file_path` | `TEXT NOT NULL` | |
| `is_active` | `BOOLEAN DEFAULT true` | |
| `created_at`, `updated_at` | `TIMESTAMP` default now | |
Indexes: `idx_user_avatars_user_id`, `idx_user_avatars_active (user_id, is_active)`.
```
10. **`## Known schema smells` section updates** (lines 226-232):
- **Smell #2** (`is_public` vs `visibility`): add a sentence noting both columns are now captured by separate migrations (initial-schema for `is_public`, B2 for `visibility`); resolution lives in a future `unify-collection-visibility` follow-up.
- **Smell #3** (`cards.quantity` + `cards.favorited`): add the cross-reference to the queued `drop-dead-cards-columns` follow-up.
- **NEW: Smell #7 — `users.profile_image_url` vs `users.avatar_url`** (the parity smell that B5 perpetuates intentionally). Both columns are present for prod parity; cleanup is the queued `unify-user-avatar-column` follow-up.
11. **`## Regeneration` section update** (lines 234-245): the manual-regeneration instructions can be removed entirely since the migration history is now authoritative. Replace with:
```markdown
## Regeneration
The migration history under `migrations/` is the authoritative source
of truth. To verify this file matches a live env (prod, preview, or a
fresh Neon branch), use the operator runbook at
[`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](MIGRATION_VERIFICATION_RUNBOOK.md).
When you add a new migration, update the relevant table section here
in the same PR. New tables get a new `###` section with the same
column-table shape.
```
### 2. `docs/MIGRATION_VERIFICATION_RUNBOOK.md` — new
Lift verbatim from `.convoys/reconcile-historical-add-scripts.md` § Verification plan (D5), with a small intro framing it as the canonical operator runbook (not just a convoy artifact). Suggested skeleton:
```markdown
# Migration verification runbook
How to verify that the migration history under `migrations/` produces
the same schema as a long-lived environment (prod, preview, or
similar). Use this runbook:
- **After this repo's `reconcile-historical-add-scripts` convoy** (the
initial reconciliation), to confirm a fresh Neon branch reaches
parity with prod via `npm install``npm run setup-db` alone.
- **After any new migration lands on `main`**, to spot-check that
applying the migration to prod (via the operator's
`POSTGRES_URL=<prod> npm run migrate up`) produced the intended
effect.
- **When suspecting drift** between an env's actual schema and the
migration history (rare; the migration history is authoritative).
This runbook is the manual precursor to the automated check planned
in the queued `wire-migrate-into-ci` follow-up (see
`.convoys/ship-readiness.md` § "Queued convoys").
## Prerequisites
- `pg_dump` (PostgreSQL 16+) installed locally.
- `POSTGRES_URL` for the env you're verifying.
- `ADMIN_INITIAL_PASSWORD` (for the fresh-branch onboarding step) —
see `AGENTS.md` § 5 "Running locally".
- A Neon account with permission to create a branch (or any other
way to spin up a fresh Postgres DB on the same major version as
prod).
## Procedure
### Step 1 — Snapshot the reference env's structural shape
Run against the env you consider canonical (usually prod):
\`\`\`bash
POSTGRES_URL=<reference-url> pg_dump --schema-only --no-owner --no-acl \
--schema=public > /tmp/reference-schema.sql
\`\`\`
### Step 2 — Create a fresh DB and onboard via `setup-db`
Create a clean Neon branch from an **empty** parent (or any other
fresh Postgres DB on the same major version):
\`\`\`bash
POSTGRES_URL=<fresh-branch-url> \
ADMIN_INITIAL_PASSWORD=$(openssl rand -base64 24) \
npm run setup-db
\`\`\`
`setup-db` runs `npm run migrate up` (applying every migration in
`migrations/` in timestamp order) and seeds the admin user.
### Step 3 — Snapshot the fresh DB's structural shape
\`\`\`bash
POSTGRES_URL=<fresh-branch-url> pg_dump --schema-only --no-owner --no-acl \
--schema=public > /tmp/fresh-schema.sql
\`\`\`
### Step 4 — Diff
\`\`\`bash
diff <(sort /tmp/reference-schema.sql) <(sort /tmp/fresh-schema.sql)
\`\`\`
**Expected non-material differences** (acceptable; do not chase):
- Constraint or index NAME differences. Prod constraints created via
the historical `scripts/add-*` / `scripts/fix-*` jobs may have
autogenerated tuple-UNIQUE names that differ from the migrations'
explicit names.
- Column-ORDER differences. Prod has columns in historical-script-ALTER
order; fresh envs have them in migration-order.
**Material differences** (bug — fix before declaring verified):
- A column type, default, or NULL/NOT NULL state that differs.
- A missing or extra table.
- A missing or extra CHECK constraint that changes accepted values.
- A missing or extra index that changes query plan shape.
## Supplementary spot-checks
For the highest-risk surfaces (frequently-edited tables), run these
information-schema queries against both envs and confirm the
column-count / constraint-count / index-count totals match exactly:
\`\`\`sql
-- Every column on every table
SELECT table_name, column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
-- Every constraint
SELECT table_name, constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'public'
ORDER BY table_name, constraint_name;
-- Every index
SELECT tablename, indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'public'
ORDER BY tablename, indexname;
\`\`\`
A mismatch in column count, constraint count, or index count is a
material difference and indicates a bug.
## What to do if you find a material difference
1. **Identify which env is canonical.** Usually prod. If the diff
surfaces a missing column on prod that's in the migration history,
the migration was never applied to prod — run
`POSTGRES_URL=<prod> npm run migrate up` to catch up.
2. **If the diff surfaces a column on prod that's NOT in the
migration history**, you've found a drift bug. Write a new
reconciliation migration (under `migrations/`) that captures the
prod column, following the pattern in `.convoys/reconcile-historical-add-scripts/`.
3. **Do not edit existing migrations** — they're pinned by
`pgmigrations` rows. Always write a new dated migration to correct
schema.
## Cross-references
- `AGENTS.md` § 4 Gotcha #6 — migration tool adoption history.
- `.convoys/migration-tool.md` — the convoy that adopted `node-pg-migrate`.
- `.convoys/reconcile-historical-add-scripts.md` — the convoy that folded
the 13 historical scripts into the migration history.
- `.cursor/rules/db-and-schema.mdc` — schema-change conventions.
```
### 3. `AGENTS.md` — modified (small Gotcha #6 cross-reference)
Gotcha #6 is already "RESOLVED" per `migration-tool` (2026-05-26). This brief adds **one sentence** to the end of Gotcha #6 cross-referencing this convoy as the closing follow-up. Verbatim addition (insert immediately before the `- **#7**` line at AGENTS.md line 120):
> Post-`reconcile-historical-add-scripts` (2026-06-14), the
> migration history additionally captures the full effect of the 13
> historical `scripts/add-*.js` / `scripts/fix-*.js` /
> `scripts/seed-*.js` jobs (where applicable — pure-DML seed scripts
> stay out per D4 of that convoy). A brand-new Neon branch can now be
> onboarded by `npm install``npm run setup-db` alone. Operator
> verification runbook at
> [`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](docs/MIGRATION_VERIFICATION_RUNBOOK.md).
> The 13 historical scripts remain no-go-zones until the queued
> `retire-graveyard-scripts-after-audit` (P3) cleanup convoy lands;
> that convoy is now unblocked.
Do NOT flip Gotcha #6's RESOLVED marker — it's already RESOLVED by the right convoy (`migration-tool`). Just append the cross-reference.
### 4. `.convoys/ship-readiness.md` — modified (queued convoys section)
Find the existing line under § "Queued convoys" that reads:
> - **`reconcile-historical-add-scripts`** (priority: P1 quality — needed for fresh-env onboarding). Surfaced 2026-05-26 by `migration-tool` (PR #32). Fold the effects of the 27 historical `scripts/add-*.js` / `fix-*.js` / `seed-*.js` jobs … Documented in `.convoys/migration-tool.md` § R1.
Replace with:
> - **`reconcile-historical-add-scripts`** — **RESOLVED 2026-06-14** by [convoy](../.convoys/reconcile-historical-add-scripts.md) (PRs #XX-#XX). Captured 7 of the 13 historical scripts' effects into 5 new migrations under `migrations/` (B1-B5); 2 scripts were already captured by `initial-schema` + `1780378340194_system-collection-description`; 3 are DML-only and stay as dev fixtures; 1 (`fix-user-cards-constraints.js`) is a no-op on prod per Finding 1 (Outcome A — the canonical 3-col `UNIQUE(user_id, card_id, is_foil)` from `initial-schema` is what prod has, and `fix-user-cards-constraints.js`'s stricter 2-col variant is not present). A brand-new Neon branch now onboards via `npm install``npm run setup-db` alone. Operator verification at [`docs/MIGRATION_VERIFICATION_RUNBOOK.md`](../docs/MIGRATION_VERIFICATION_RUNBOOK.md). Entry kept (not deleted) for audit trail.
Find the existing line:
> - **`retire-graveyard-scripts-after-audit`** (priority: P3 polish; **blocked on `reconcile-historical-add-scripts`**). …
Replace the `**blocked on `reconcile-historical-add-scripts`**` marker with `**UNBLOCKED 2026-06-14**` and leave the rest of the description intact.
Add two new queued entries (driven by Findings 2 + 3 of the reconcile convoy):
> - **`unify-user-avatar-column`** (priority: P3 hygiene). Surfaced 2026-06-14 by `reconcile-historical-add-scripts` Finding 2. `users.profile_image_url` (added by `scripts/add-user-profile-columns.js`) and `users.avatar_url` (added by `scripts/add-user-profile-fields.js`) coexist on prod and in the migration history (both columns are needed for parity per B5). Pick one canonical column, migrate the other's data to it, drop the loser, and sweep runtime readers in `pages/api/user/avatar*.js` + UI surfaces. Requires a query-trace audit first.
> - **`drop-dead-cards-columns`** (priority: P3 hygiene). Surfaced 2026-06-14 by `reconcile-historical-add-scripts` Finding 3. `cards.quantity INTEGER` and `cards.favorited BOOLEAN` are documented unused (per `docs/SCHEMA_MAP.md` § "Known schema smells" #3); reconciled into the migration history by B1 for parity, but the actual semantics live on `user_cards` / `user_favorites`. After a query-trace audit confirms zero runtime readers, ship a migration that DROPs both columns with a real `down()` that recreates them.
(Do NOT add the withdrawn `add-system-collection-on-register` or `unify-user-cards-foil-tracking` entries — both were withdrawn during the reconcile convoy. See `.convoys/reconcile-historical-add-scripts.md` § Follow-ups.)
## Conventions to follow
- `.cursor/rules/no-go-zones.mdc` — no edits to `scripts/add-*` / `fix-*` / `seed-*`.
- `.cursor/rules/db-and-schema.mdc` — schema-change conventions; SCHEMA_MAP is updated alongside any migration.
- Markdown style: match the existing tone of `docs/SCHEMA_MAP.md` (compact tables, "Notes" column explains intent not type semantics) and `AGENTS.md` (numbered Gotchas, cross-reference convoy files).
- For `.convoys/ship-readiness.md`: match the existing entry style under § "Queued convoys" (one bullet per convoy, leading `- **`name`**`, RESOLVED entries get the "Entry kept (not deleted) for audit trail" closer when applicable).
## Acceptance criteria
- [ ] `docs/SCHEMA_MAP.md` preamble bumped (post-convoy state); "Last reviewed" date is 2026-06-14.
- [ ] `docs/SCHEMA_MAP.md` § `user_favorites` shape is the polymorphic `(item_type, item_id)` — NOT the previous `(card_id)` shape.
- [ ] Every reconciled table/column has a "captured by `migrations/<file>`" cross-reference.
- [ ] `### user_settings` and `### user_avatars` sections are expanded from one-liners to full column tables matching B5's migration.
- [ ] New schema smell #7 (`profile_image_url` vs `avatar_url`) is added to § "Known schema smells".
- [ ] `docs/MIGRATION_VERIFICATION_RUNBOOK.md` exists with the 4-step procedure from D5 + the spot-check queries + the "material vs non-material differences" guidance.
- [ ] `AGENTS.md` Gotcha #6 gains a single appended paragraph cross-referencing this convoy + the new runbook + the unblocked `retire-graveyard-scripts-after-audit` follow-up. Gotcha #6's existing "RESOLVED by `migration-tool`" marker is NOT changed.
- [ ] `.convoys/ship-readiness.md` § "Queued convoys" → `reconcile-historical-add-scripts` entry is flipped to RESOLVED 2026-06-14 with a one-paragraph summary; `retire-graveyard-scripts-after-audit` is marked UNBLOCKED 2026-06-14; two new entries (`unify-user-avatar-column`, `drop-dead-cards-columns`) are added.
- [ ] No edits to any file under `scripts/`, `migrations/`, `pages/`, `lib/`, `components/`, `test/`, or `package.json`.
- [ ] `npm run lint` matches baseline (these are markdown-only edits; lint should be unaffected).
- [ ] `npm run test:run` reports 21/21 passing (these are markdown-only edits; no test surface).
## Verification
```bash
npm run lint
npm run test:run
```
Both must match baseline / be green. No additional verification for this brief — the migrations themselves are exercised by Brief 7's verification runbook AFTER an operator runs it manually post-merge.
For the SCHEMA_MAP edits, eyeball the diff against the prior state and confirm every cross-reference to a migration file is accurate (the file actually exists, and the timestamp matches the assigned reservation).
## Commit message
```
docs(reconcile): update SCHEMA_MAP, verification runbook, AGENTS, ship-readiness (brief 7/7)
Closes the reconcile-historical-add-scripts convoy:
- docs/SCHEMA_MAP.md: cross-reference 5 new migrations from B1-B5;
fix user_favorites doc bug (polymorphic shape, not (card_id));
expand user_settings + user_avatars one-liners to full column tables;
add schema smell #7 (profile_image_url vs avatar_url redundancy).
- docs/MIGRATION_VERIFICATION_RUNBOOK.md (new): canonical operator
runbook for verifying fresh-env vs prod schema parity (lifted from
D5 of the convoy).
- AGENTS.md Gotcha #6: append cross-reference to this convoy + the
new runbook + the now-unblocked retire-graveyard-scripts-after-audit
follow-up. Existing "RESOLVED by migration-tool" marker unchanged.
- .convoys/ship-readiness.md: flip reconcile-historical-add-scripts
entry to RESOLVED 2026-06-14; mark retire-graveyard-scripts-after-audit
UNBLOCKED; add new queued unify-user-avatar-column (Finding 2) +
drop-dead-cards-columns (Finding 3).
Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B7.
Depends on B1-B5 having merged (this brief cross-references their files).
```
## PR shape
**Title:** `docs(reconcile): update SCHEMA_MAP, verification runbook, AGENTS, ship-readiness (brief 7/7)`
**Body template:**
```markdown
Brief 7 (final) of the `reconcile-historical-add-scripts` convoy.
See [`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md)
for the full plan and rationale.
This is the closing brief — depends on B1-B5 having merged (PR
references below).
## What this PR does
- **`docs/SCHEMA_MAP.md`** — cross-reference all 5 new migrations from
B1-B5; fix the `user_favorites` doc bug (the table uses the
polymorphic `(item_type, item_id)` shape, not `(card_id)`); expand
`user_settings` + `user_avatars` from one-liners to full column
tables matching the B5 migration; add a new "Known schema smell" #7
for the `profile_image_url` / `avatar_url` redundancy.
- **`docs/MIGRATION_VERIFICATION_RUNBOOK.md`** (new) — canonical
operator runbook for verifying fresh-env vs prod schema parity.
Pulls verbatim from D5 of the convoy file. Useful both for the
one-time reconciliation verification and for ongoing post-migration
spot-checks.
- **`AGENTS.md`** — append one paragraph to Gotcha #6 cross-referencing
this convoy as the closing follow-up. The "RESOLVED by `migration-tool`"
marker is unchanged (that's still the right resolution attribution
for the gotcha itself).
- **`.convoys/ship-readiness.md`** § "Queued convoys" — flip the
`reconcile-historical-add-scripts` entry to RESOLVED 2026-06-14;
flip `retire-graveyard-scripts-after-audit` from "blocked on
reconcile-historical-add-scripts" to UNBLOCKED 2026-06-14; add two
new queued entries (`unify-user-avatar-column` from Finding 2,
`drop-dead-cards-columns` from Finding 3).
## Prerequisite PRs (all must be merged first)
- B1 #XX — reconcile cards columns
- B2 #XX — reconcile collections columns
- B3 #XX — reconcile collaboration tables
- B4 #XX — reconcile favorites system
- B5 #XX — reconcile user profile
## What this PR does NOT do
- Does **NOT** edit any file under `migrations/`, `scripts/`, `pages/`,
`lib/`, `components/`, `test/`, or `package.json`.
- Does **NOT** run the verification runbook itself — that's the
operator's post-merge job; see the runbook's "Procedure" section.
## Verification checklist
- [ ] `npm run lint` matches baseline
- [ ] `npm run test:run` reports 21/21 passing
- [ ] Every "captured by `migrations/<file>.js`" cross-reference in
SCHEMA_MAP points at a file that actually exists in `migrations/`
(eyeballed against `ls migrations/`)
- [ ] `AGENTS.md` Gotcha #6 still has its original RESOLVED marker
(we only APPEND to it, not rewrite it)
- [ ] `.convoys/ship-readiness.md` § "Queued convoys" has the flipped
`reconcile-historical-add-scripts` entry, the UNBLOCKED
`retire-graveyard-scripts-after-audit` marker, and the two new
follow-up entries
- [ ] Operator post-merge: run `docs/MIGRATION_VERIFICATION_RUNBOOK.md`
against prod + a fresh Neon branch and confirm the diff is
non-material
## Cross-references
- Convoy file: `.convoys/reconcile-historical-add-scripts.md`
- Per-brief files: `.convoys/reconcile-historical-add-scripts/brief-{1,2,3,4,5}-*.md`
```
## DO NOT
- DO NOT edit any file under `scripts/`, `migrations/`, `pages/`, `lib/`, `components/`, `test/`.
- DO NOT edit `package.json`, README, `next.config.js`, or any rule under `.cursor/rules/`.
- DO NOT change `AGENTS.md` Gotcha #6's "RESOLVED by `migration-tool`" marker — append the new paragraph; don't rewrite the existing resolution attribution.
- DO NOT add a `B6` reference anywhere — that brief was removed (Finding 1 → Outcome A).
- DO NOT add `add-system-collection-on-register` or `unify-user-cards-foil-tracking` to the ship-readiness queued list — both were withdrawn during this convoy.
- DO NOT remove the existing `wire-migrate-into-ci` entry from ship-readiness — it's still queued and unrelated to this convoy.
- DO NOT run the verification runbook itself (that's the operator's post-merge step).
## Rationale (≤3 sentences)
Sequencing this brief last lets every documentation cross-reference point at a real file under `migrations/` rather than a placeholder. Bundling all four doc surfaces into one PR keeps the convoy's "as-shipped" record consistent (the migrations, the operator runbook, the AGENTS gotcha, and the ship-readiness ledger all flip together). The `docs/MIGRATION_VERIFICATION_RUNBOOK.md` extraction promotes a one-time convoy artifact into an evergreen operator tool that future migrations can reuse.

View file

@ -1,233 +0,0 @@
---
name: scan-visual-catalog-search
classification: feature
success_metric: |
After catalog embeddings exist and the new kNN path is on, ≥50% of
legitimate card scans (scan_attempts excluding not_a_card) auto-match
a printing with no picker — the original add-real-ocr-layer 70%
target, measured honestly. Gemini L2 share of attempts falls vs the
Phase 1 post-ship baseline.
skip:
- ia
- ui-design
- visual
- a11y
- design
- flag
status: shipped
created: 2026-08-14
depends_on:
- improve-scan-card-detection
umbrella: scanner-identify-upgrade
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
---
# Convoy: scan-visual-catalog-search
Phase 3 of `scanner-identify-upgrade`. Identify cards by **what they
look like**, not by reading the name. This is how Manabox / Delver-style
scanners get printing-accurate matches on foil and alt-art.
Blocked on `improve-scan-card-detection`: embeddings of unwarped
phone crops will not match catalog `image_url`s reliably.
## Why
Name-only `pg_trgm` cannot distinguish printings. Tesseract fails on
foil and stylized type even when it emits 820 characters (76.7% L1
escalate with non-empty text). Gemini can read a name but still
returns a picker when set/number are missing, and costs 5/min.
The catalog already stores `cards.image_url` for every imported
printing. Precompute an embedding per row; at scan time embed the
warped crop and take top-k cosine. OCR / Gemini become hints and
unknown-card fallback.
## Scope
### In scope
- **Migration:** `pgvector` (confirm Neon availability) +
`cards.embedding` (or a side table `card_embeddings`) + ANN index.
Update `docs/SCHEMA_MAP.md`.
- **Offline embed job:** new dated script or `npm run` task that
reads `image_url`, writes vectors. Idempotent. Rate-limit the
embedding provider. Do **not** edit historical `scripts/add-*.js`.
- **Architect picks the embedder** (one):
1. Gateway embedding model (same `AI_GATEWAY_API_KEY`, server-only).
2. In-browser MobileCLIP-S2 / SigLIP ONNX for the *query* crop, with
catalog vectors baked or fetched — only if weight license + size
are acceptable.
- **Identify path:** new Layer-0 (or replace L1) — kNN then
auto-match / disambiguate / escalate to existing L2. Log
`scan_attempts.layer = 0` (or Architect-ratified value).
- **Similarity thresholds** analogous to 0.85 / 0.60, tuned on a
held-out set of scan captures if any exist in Blob.
- Auth + rate-limit on any new route. No client-side API keys
(`forbidden-client-side-llm-keys` must stay green).
### Out of scope
- Replacing Gemini entirely on day one — keep L2 for catalog misses
and low similarity.
- Training a custom card CNN.
- Python GPU service.
- Changing scanner chrome / cart.
- Auto-approving the 27 pending `card_submissions`.
## Roles invoked
1. `role-architect` — embedder, schema, layer numbering, thresholds,
brief split (migration / backfill / route). Security-sensitive:
escalate to Sonnet if schema + new route land together.
2. `role-implementer`.
3. Audit: `role-reviewer` + `role-security-auditor` (required).
## Todos
- [x] Architect: confirm `pgvector` on prod Neon tier
- [x] Brief 1 — migration + SCHEMA_MAP
- [x] Brief 2 — catalog backfill job (idempotent)
- [x] Brief 3 — identify kNN route + client escalate order
- [x] Threshold bake-off on real crops (initial: 0.82 match / 0.58 disambig — tune post-backfill)
- [ ] Re-measure auto-match % excluding `not_a_card` (blocked: L0 traffic + backfill)
- [ ] Operator: finish `npm run backfill-embeddings`**14.9%** (9,865 / 66,211) as of 2026-08-15
## Post-ship (2026-08-15)
**Merged:** PR #160 (Layer-0 visual catalog search), PR #162 (CI gates).
**Pipeline roles (kickoff complete):**
| Role | Outcome |
| --- | --- |
| Conductor | Convoy ratified; skip ia/ui-design/visual/a11y/design/flag |
| UX reviewer | No new screens; L0→L1→L2 order; silent escalate when index empty |
| Architect | D1D4 decisions + 3 briefs (`.convoys/scan-visual-catalog-search/brief-*`) |
| Implementer | Briefs 13 shipped in #160 |
| Reviewer + security-auditor | #160 audit fan-out; #162 CI fix |
| Doc-writer | SCHEMA_MAP layer 0 documented |
**CI fixes (#162):** `lib/card-embed.js` allowlist + pgvector migration graceful
skip on non-superuser homelab CI.
**Neon:** pgvector v0.8.0; `cards.embedding vector(1024)` + HNSW index applied.
Backfill **in progress** (9,865 / 66,211 = 14.9%). **0** `scan_attempts` with
`layer = 0` yet — L0 path live but index too sparse / no post-#160 scans logged.
**scan_attempts snapshot** (all-time n=250):
| Signal | Value | Baseline |
| --- | --- | --- |
| L1 escalate | 78.4% (87/111) | 76.7% |
| L1 matched | 5.4% (6/111) | 5.8% |
| L2 not_a_card | 43.9% (61/139) | 44.6% |
| End-to-end auto-match | 13.2% excl. not_a_card | 13.5% |
| L0 attempts | 0 | — |
**Aug 15 session (n=26):** See umbrella `.convoys/scanner-identify-upgrade.md`
§ Post-ship telemetry.
Re-measure after backfill completes and preview scanning generates L0 rows.
## Likely file ownership
| Area | Files |
| --- | --- |
| Schema | `migrations/*_card-embeddings.js`, `docs/SCHEMA_MAP.md` |
| Backfill | new `scripts/` job (dated) or `lib/card-embed-backfill.js` |
| Query | new `lib/card-visual-match.js`, `pages/api/scan/identify-by-image.js` **or** fold into existing identify |
| Client | `lib/scanner-card-identify.js` (`tryLayer1TextIdentify` sibling) |
Do not rewrite `lib/scanner-card-detection.js` here.
## Multitask dispatch
Brief 1 first. Brief 2 after 1. Brief 3 after 1 (can overlap 2 if the
route degrades to escalate-when-empty-index).
```
/multitask role-reviewer + role-security-auditor
```
Group id: `audit-scan-visual-catalog-search-<pr>`.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `schema-map-fresh` | **Fires** — migration + SCHEMA_MAP |
| `ci.yml` migrate | Must apply `pgvector` on CT 102 CI Postgres — Architect must verify the extension is available there or gate the migration |
| `forbidden-client-side-llm-keys` | Blocking |
## Operator action
- Confirm Neon `pgvector` (or Neons equivalent) on the prod project.
- Budget: one embedding per catalog image, plus one per live scan if
the query embed is server-side. Architect publishes a cost note
before Brief 2 runs against prod images.
- No new browser secrets.
## Conductor notes
This is the accuracy leap. Do not start it to "try CLIP" before Phase 2
crops are rectified — that wastes the backfill. If Architect finds
`pgvector` unavailable on CI Postgres, stop and write a fallback
(external index vs skip-CI-extension plan) rather than shipping an
untestable migration.
## UX
No new screens. Scan flow stays L0 → L1 → L2 with the same
disambiguation picker and error toasts. When the catalog index is empty
(backfill not run), L0 escalates silently with no embed cost.
Gallery uploads now try visual match before OCR.
## Architecture
### Decision D1 — Gateway multimodal embedder (`cohere/embed-v4.0`)
Server-only via `AI_GATEWAY_API_KEY`. 1024-dim vectors in
`cards.embedding`. Env: `SCAN_EMBED_MODEL`, `SCAN_EMBED_DIMENSION`.
### Decision D2 — Layer numbering
| Layer | Path |
| --- | --- |
| 0 | `POST /api/scan/identify-by-image` |
| 1 | Tesseract + `identify-by-text` |
| 2 | Gemini + `scan/identify` |
### Decision D3 — Thresholds
Match ≥ **0.82** (0.06 gap). Disambiguation ≥ **0.58**.
### Decision D4 — Rate limit
`checkScanRateLimit` on identify-by-image. L0 429 falls through to L1
(not a hard stop).
Audit group id: `audit-scan-visual-catalog-search-<pr>`.

View file

@ -1,23 +0,0 @@
---
convoy: scan-visual-catalog-search
brief_number: 1
depends_on: []
recommended_model: composer-2.5
model_tier: standard
files:
- migrations/1782000000001_add-card-embeddings.js
- docs/SCHEMA_MAP.md
---
# Brief 1: pgvector migration + SCHEMA_MAP
## Goal
Add `vector(1024)` embedding column + HNSW index on `cards`.
## Acceptance criteria
- [ ] `CREATE EXTENSION IF NOT EXISTS vector`
- [ ] `cards.embedding`, `cards.embedded_at`
- [ ] `idx_cards_embedding_hnsw` partial index
- [ ] SCHEMA_MAP documents layer 0 + pgvector

View file

@ -1,24 +0,0 @@
---
convoy: scan-visual-catalog-search
brief_number: 2
depends_on: [1]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/card-embed.js
- scripts/backfill-card-embeddings.js
- package.json
- test/lib/card-embed.test.js
---
# Brief 2: Catalog embedding backfill
## Goal
Server-side embed job using `cohere/embed-v4.0` via AI Gateway; idempotent backfill from `cards.image_url`.
## Acceptance criteria
- [ ] `lib/card-embed.js` exports `embedCardImage`, `formatEmbeddingForPg`
- [ ] `npm run backfill-embeddings` script (skips rows with embedding unless FORCE=1)
- [ ] Rate-limited (`SLEEP_MS`) and DRY_RUN support

View file

@ -1,27 +0,0 @@
---
convoy: scan-visual-catalog-search
brief_number: 3
depends_on: [1]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/card-visual-match.js
- pages/api/scan/identify-by-image.js
- lib/scanner-card-identify.js
- lib/use-scanner-identification.js
- test/lib/card-visual-match.test.js
---
# Brief 3: Layer-0 identify route + client orchestration
## Goal
kNN visual match before L1/L2; log `scan_attempts.layer = 0`.
## Acceptance criteria
- [ ] `POST /api/scan/identify-by-image` — auth + `checkScanRateLimit` + embed + kNN
- [ ] `identifyTrackedCardCapture` order: L0 → L1 → L2
- [ ] Gallery path runs L0 → L1 → L2
- [ ] Skip auto Gemini refine when disambiguation from L0 (same as L1)
- [ ] Thresholds: match 0.82, disambiguation 0.58

View file

@ -1,902 +0,0 @@
---
name: scanner-desktop-layout
classification: feature
success_metric: |
On md+ viewports, /scanner keeps the desktop app chrome (sidebar +
top bar), shows a framed camera workstation with a real webcam
device picker, Upload Image, Batch Scan, Auto-detect, Scanner
Tips, a live match inspector, and a bottom strip with Recent
Scans / Scan Queue / Duplicates — without regressing the mobile
immersive checkout.
skip: []
status: shipped
created: 2026-08-15
depends_on:
- scanner-mobile-checkout
- scanner-rebuild
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
design_direction:
source: role-ui-designer
skill: ui-ux-pro-max
skill_version: "2.5.0"
version: 1
locked_at: 2026-08-15
product_type: desktop trading card scanner workstation
pattern: Feature-Rich Showcase (workstation variant)
style: Liquid Glass / glassmorphism
stack: nextjs
layout_reference: image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png
---
# Convoy: scanner-desktop-layout
Give `/scanner` a dedicated desktop workstation layout from the
attached dark/light mock (camera + live result + history strip),
while leaving the shipped mobile immersive checkout alone.
Worktree: `tcg-vault-worktrees/scanner-desktop-layout` on
`convoy/scanner-desktop-layout` (branched from `origin/main` @
`0d52858`). Layout reference:
`image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png`.
Do **not** land this on `dashboard-home-realignment` — that convoy
owns sidebar IA + top-bar chrome. Do **not** land this on
`scanner-identify-upgrade` — that epic owns detect/identify accuracy.
## Why
`scanner-mobile-checkout` shipped the phone job: full-bleed camera,
local cart, checkout sheet. Desktop (`md+`) got the leftover
composition — the same camera chrome plus a 360px cart side panel
(`ScannerReview` `variant="side-panel"`). That is not a desk
workstation.
On a laptop the user wants to see the webcam, inspect the current
match (set, rarity, number, condition, foil, confidence), decide
what to do with it, and keep a history/queue in view — without
losing the app sidebar or search bar. The mock is that layout.
Today they get a phone overlay stretched into a column.
## Scope
### In scope
- **Desktop-only composition (`md+`).** Keep Layout sidebar +
TopSearchBar. Stop treating desktop as an immersive camera page
with a bolted-on cart. Mobile (`max-md`) stays
`chrome="immersive"` + checkout sheet.
- **Framed camera viewport.** Large live feed with ember corner
brackets (already in `ScannerCamera`), Auto-detect status, and
desk controls under the frame. Camera is a panel in the page,
not a full-bleed overlay.
- **Real webcam device picker.** `enumerateDevices` + `deviceId`
in `useCameraScanner` (not facing-mode swap relabeled). Persist
the last-used device for the tab if cheap. Empty-list / denied-
permission fallback. Mobile keeps the existing facing-mode
toggle — do not replace the phone chrome with a device `<select>`.
- **Upload Image + Batch Scan.** Upload Image stays single-file
(existing gallery path). **Batch Scan** is a multi-file picker
that runs each image through `identifyFromGalleryFile`
**sequentially** (respect existing identify rate limits; no
parallel Gemini storm, no new batch API). Progress and failures
show in Scan Queue. Live multi-card on the webcam stays
Auto-detect — do not build a new pile-in-one-frame detector.
- **Scanner Tips.** Header control from the mock. Glass popover
or modal (use `<Modal>` / `GlassSurface`, no custom scrim) with
short lighting / framing / foil / auto-detect guidance. No new
route. Copy authored in this convoy (IA + UI Designer).
- **Live match inspector (right rail).** Current identify result:
thumbnail, name, set, rarity, collector number, condition,
foil, confidence, primary add, Rescan. This replaces the cart
list as the primary right-hand surface.
- **Bottom history / queue strip.** Three tabs over the existing
cart + ownership model: **Recent Scans** (session history),
**Scan Queue** (uncommitted cart), **Duplicates** (already in
My Collection via `ownershipMap`, and/or same-session
name+set repeats from `mergeScannedCardEntry`). Badge counts
on Queue and Duplicates. Clear + view-all as IA/UX refine.
- **Reuse, don't rewrite, the scan engine.** Same hooks:
`useCameraScanner`, `useScannerIdentification`, `useScannerQueue`,
`scanner-session`. Identify / OCR / Gemini stay out.
### Out of scope
- Identify / detection / Gemini (`lib/scanner-card-identify.js`,
`pages/api/scan/identify.js`, OpenCV). Sibling:
`scanner-identify-upgrade`.
- Mobile immersive checkout, scan peek, or checkout sheet — no
visual or interaction regression below `md`.
- Layout nav IA, Daily Ember, notifications, profile cluster.
Sibling: `dashboard-home-realignment`. Mock items Wishlist /
Trades / Market / Events / Binders are **not** product nav.
- Wishlist as a feature (no table / API). Mock "Save to Wishlist"
maps to **Add to List** or drops — IA decides.
- A new identify / batch-OCR API, parallel Gemini calls, or a
pile-in-one-frame detector. Batch Scan is multi-file sequential
identify on the existing path.
- Schema / new tables. Cart remains client session state.
- Changing the homepage visual-diff baseline except as a
side-effect of `/scanner` desktop chrome (scanner surfaces only).
### Product-vocab lock (from mock → Deck Hearth)
| Mock copy | Ship as |
| --- | --- |
| Add to Collection | `VOCAB.ADD_TO_MY_COLLECTION` |
| Save to Wishlist | Add to List, or omit |
| Binders (nav) | Lists — not this convoy |
| Collection (nav) | My Collection — not this convoy |
## Roles invoked
1. `role-ia-architect` — desktop flow vs mobile cart; inspector
vs queue commit; Duplicates membership (`ownershipMap` vs
session repeat); Batch Scan progress / failure; Tips content
outline; device-picker empty/denied states.
2. `role-ui-designer` — lock the md+ workstation against the
attached mock using Liquid Glass tokens (`ui-ux-pro-max` is
installed), including Tips popover, device `<select>`, Batch
Scan progress, and the three-tab strip. Do **not** skip
`ui-design`.
3. `role-ux-reviewer` — inspect-then-add vs scan-all-then-checkout
on desktop; Auto-detect off; Rescan; empty inspector; leave
with an uncommitted queue; sequential batch cancel; duplicate
tab actions (increment qty vs skip vs still add).
4. `role-architect` — briefs. Likely: (1) page composition +
Layout chrome split, (2) camera panel + `deviceId` picker,
(3) result inspector, (4) history/queue/duplicates strip,
(5) batch multi-file identify + Tips. `slice_dependencies`
must mark what can run in parallel.
5. `role-implementer` — per brief.
6. Audit fan-out: reviewer + security-auditor + design-system-auditor
+ a11y-auditor.
## Todos
- [x] IA: desktop screen inventory + inspector-add vs cart-commit;
Duplicates membership; Batch Scan + Tips content
- [x] UI Designer: lock md+ workstation (tokens, not hex) from
the attached mock; light + dark; Tips, device picker,
batch progress, three-tab strip
- [x] UX: Auto-detect off, Rescan, empty state, leave-with-queue,
keyboard on desk controls, batch cancel, duplicate actions
- [x] Architect: briefs + `slice_dependencies`; confirm Layout
is `chrome="default"` on md+ only
- [ ] Desktop composition in `pages/scanner.js` (do not hide
sidebar / top bar at md+)
- [ ] Camera as a framed panel; `deviceId` picker + Upload +
Batch Scan + Auto-detect; hide mobile overlay chrome at md+
- [ ] Live match inspector wired to the latest unprocessed
identify (condition / foil already on the cart entry)
- [ ] Bottom strip: Recent Scans + Scan Queue + Duplicates over
`useScannerQueue` / `ownershipMap` / `scanner-session`
- [ ] Scanner Tips popover/modal with convoy-authored copy
- [ ] Sequential multi-file Batch Scan through
`identifyFromGalleryFile` (cancellable, queue progress)
- [ ] Tests for desktop composition (inspector + queue +
duplicates + batch enqueue, no mobile sheet) and
no-regression on checkout sheet at `max-width: 767px`
- [ ] Visual-diff: desktop `/scanner` surface; refresh Linux
baselines if the page is in the visual suite
## What exists today (conductor survey)
`/scanner` is one route, two compositions, one engine.
| Layer | Files | Today |
| --- | --- | --- |
| Page | `pages/scanner.js` | Auth gate; `Layout chrome="immersive"` on **all** viewports; camera column + `md:` 360px `ScannerReview` cart; mobile-only `ScannerCheckoutSheet` |
| Layout | `components/Layout.js` | Immersive hides **mobile** nav + top bar (`max-md` only). Desktop sidebar + TopSearchBar already stay visible. |
| Camera chrome | `components/scanner/ScannerCamera.js` | Full-bleed video, overlay top bar (back / title / gallery), bottom bar (flash / facing / status / Review N), scan peek, disambiguation. Same chrome on desktop. |
| Cart | `lib/use-scanner-queue.js`, `lib/scanner-session.js` | Identify enqueues locally (`processed: false`). Commit via `commitSelectedToOwned` / `commitSelectedToCollection`. `sessionStorage` persist. `addSingleCardToOwned` already exists. |
| Identify | `lib/use-scanner-identification.js`, `lib/use-camera-scanner.js` | Facing-mode swap only — **no** `deviceId` / `enumerateDevices`. Auto-detect is always on unless `verificationPausedRef` (checkout / list picker / disambiguation). |
| Review | `components/scanner/ScannerReview.js` | Thin wrapper: desktop side panel titled "Cart" that mounts `ScannerCheckoutContent`. |
Mobile checkout decisions that still apply unless IA overturns them
for desktop only: stay on camera after commit (D1), skip Setup
(D3), My Collection + List only (D4), gallery in-scope (D5),
cart in `sessionStorage` (D7).
## Conductor notes (build shape)
Likely file ownership for Architect to refine:
| Area | Files |
| --- | --- |
| Viewport split | `pages/scanner.js``chrome` default on md+, immersive on mobile; desktop grid vs mobile overlay |
| Camera panel | `components/scanner/ScannerCamera.js` (desktop variant or `variant="workstation"`), `lib/use-camera-scanner.js` (`enumerateDevices` + `deviceId`) |
| Inspector | new `components/scanner/ScannerResultPanel.js` — latest cart entry + condition/foil + add/rescan |
| History strip | new `components/scanner/ScannerHistoryStrip.js` — Recent / Queue / Duplicates over `queue.scannedCards` + `ownershipMap` |
| Batch Scan | `lib/use-scanner-identification.js` (`identifyFromGalleryFile` loop), queue progress UI |
| Tips | new `components/scanner/ScannerTips.js``<Modal>` or popover, convoy copy |
| Cart reuse | `lib/use-scanner-queue.js`, `lib/scanner-session.js`, `ScannerCheckoutSheet.js` (mobile only) |
| Copy | `lib/collection-vocabulary.js` |
Do not rewrite identification. Prefer a desktop layout shell that
**hides** mobile overlay chrome at `md+` rather than forking the
camera hook.
## Decisions (post-conductor)
Locked 2026-08-15 from the parent session. IA / UX / Architect
treat these as settled.
| # | Decision |
| --- | --- |
| C1 | **Batch Scan is in.** Multi-file sequential identify via the existing gallery path. No new batch API, no parallel Gemini, no new pile detector. |
| C2 | **Duplicates tab is in.** Strip tab with a badge. Membership = already-owned (`ownershipMap`) and/or same-session name+set repeats. IA picks the exact rule and tab actions. |
| C3 | **Scanner Tips is in.** Header control → glass popover/modal. Copy in this convoy. |
| C4 | **Real webcam device picker is in.** `enumerateDevices` + `deviceId` on desktop. Mobile keeps facing-mode swap. |
| C5 | **Inspector can commit this card now** *and* the queue strip remains for multi-add (same cart, two commit surfaces). Overturn only if IA finds a conflict. |
## Open questions (IA / product)
1. **Duplicates membership + actions.** Owned-in-collection only,
session repeats only, or both? From the tab, can the user still
add (increment qty), skip, or jump the inspector to that row?
2. **Wishlist.** Out as a feature. Confirm "Save to Wishlist" →
Add to List on the inspector, or omit the third action.
3. **Auto-detect toggle.** User-facing pause of identification
(extend `verificationPausedRef`), or just a status badge?
4. **Batch Scan cancel / errors.** Mid-batch cancel: keep already-
identified rows? Per-file failure: continue the rest and flag
the row, or stop?
5. **Tips content.** Four or five short tips (lighting, frame the
card, foil glare, hold still, auto-detect). IA drafts; UI
Designer locks the surface.
## Multitask dispatch
Planning is serial: IA → UI Designer → UX → Architect.
After architect: implementer fan-out only if briefs have
`depends_on: []` and disjoint `files:`. Device picker
(`use-camera-scanner.js`) and Tips (`ScannerTips.js`) are the
best candidates to parallelize with the inspector if they do
not both own `pages/scanner.js`. Page composition likely
blocks the strip and Batch Scan wiring.
After PR draft: `/multitask` audit fan-out
`role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor`
(group id: `audit-scanner-desktop-layout-<pr>`).
## IA
### Affected routes
- `/scanner`**[modified]** Single route, two viewport compositions. Desktop (`md+`) switches to `Layout chrome="default"` (sidebar + TopSearchBar visible), framed camera workstation, live match inspector (right rail), and bottom history strip. Mobile (`max-md`) stays `chrome="immersive"` with checkout sheet — no regression.
- `/login`**[impacted]** Existing `returnUrl=/scanner` auth gate unchanged; desktop users land on the workstation after sign-in.
- `/collections`, `/my-cards`**[impacted]** Post-commit navigation targets only (Add to List picker, success flows). No route or nav IA changes in this convoy.
No new routes. No API route changes.
### User flow
```mermaid
flowchart LR
A["/scanner (auth)"] --> B{"md+?"}
B -->|Yes| C["Workstation"]
B -->|No| D["Immersive mobile"]
C --> E["Scan / Upload / Batch"]
E --> F["Match inspector"]
F --> G["Add or queue"]
C --> H["Strip tabs"]
H --> F
```
Desktop path: user opens `/scanner` with app chrome → scans via webcam, single Upload Image, or Batch Scan (sequential gallery identify) → latest match appears in the right-rail inspector → commits one card via inspector **or** batches via Scan Queue strip → Duplicates tab surfaces owned + session-repeat rows for review/increment. Mobile path unchanged: full-bleed camera → checkout sheet.
### Screen inventory
| Screen | Path | New/modified | Notes |
| --- | --- | --- | --- |
| Scanner Desktop Workstation | `/scanner` | modified | `md+` grid: framed camera panel (device picker, Upload, Batch Scan, Auto-detect toggle, Tips), right-rail inspector, bottom strip. Replaces 360px cart side panel as primary right-hand surface. |
| Scanner Mobile Immersive | `/scanner` | impacted (no regression) | `max-md`: `chrome="immersive"`, overlay camera chrome, `ScannerCheckoutSheet`. D1/D3/D4/D5/D7 decisions preserved. |
| Live Match Inspector | `/scanner` | new (sub-surface) | Right rail on desktop. Shows latest unprocessed identify: thumbnail, name, set, rarity, collector #, condition, foil, confidence. Actions: `VOCAB.ADD_TO_MY_COLLECTION`, `VOCAB.ADD_TO_LIST`, Rescan. Single-card commit without opening checkout sheet. |
| History / Queue Strip | `/scanner` | new (sub-surface) | Bottom strip on desktop. Tabs: **Recent Scans** (session history), **Scan Queue** (uncommitted cart, badge = unprocessed count), **Duplicates** (badge = duplicate row count). Row click focuses card in inspector. |
| Scanner Tips | `/scanner` | new (sub-surface) | Header control → glass `<Modal>` or popover. Five convoy-authored tips (see Content deltas). No route change. |
| List Picker | `/scanner` | impacted | Existing "Choose a List" `<Modal>`. Opened from inspector `VOCAB.ADD_TO_LIST` on desktop (and unchanged on mobile). |
| Leave Scanner | `/scanner` | impacted | Existing leave-with-uncommitted-queue modal. Applies to both viewports when navigating away with queue items. |
### Content / data model deltas
**Copy (ship from `lib/collection-vocabulary.js`):**
- Primary add: `VOCAB.ADD_TO_MY_COLLECTION` ("Add to My Collection").
- Secondary add: `VOCAB.ADD_TO_LIST` ("Add to List") — **not** "Save to Wishlist" (feature omitted).
- Strip tab labels: "Recent Scans", "Scan Queue", "Duplicates".
- Camera controls: "Upload Image", "Batch Scan", "Auto-detect" (toggle + status on/off), "Scanner Tips", "Rescan".
- Device picker: "Camera" or "Webcam" `<select>` label; empty/denied fallback copy (UI Designer).
**Scanner Tips content (draft for UI Designer):**
1. Good lighting — avoid glare on foil cards.
2. Fill the frame with one card; keep corners visible.
3. Hold still until Auto-detect locks the match.
4. Use Batch Scan for a pile of photos from your gallery.
5. Switch webcam if the image is dark or mirrored.
**No schema changes.** Cart remains client `sessionStorage` via `lib/scanner-session.js`. Duplicates derive from existing `ownershipMap` (already in My Collection) and `mergeScannedCardEntry` (same-session name+set repeats) — no new tables or API fields.
**IA decisions (locked — formerly open questions):**
| Topic | Decision |
| --- | --- |
| Duplicates membership | **Both** sources: `ownershipMap` (already-owned) **and** same-session name+set repeats via `mergeScannedCardEntry`. Badge = count of rows in the Duplicates set. Row click focuses that card in the inspector. User can still add (increment qty) from inspector or queue. |
| Wishlist | **Omit.** Inspector third action is `VOCAB.ADD_TO_LIST`, not wishlist. |
| Auto-detect | **Real pause toggle** on desktop. Extends `verificationPausedRef` (user off = paused identify). Status badge reflects on/off. Mobile behavior unchanged unless UX specifies otherwise. |
| Batch cancel / errors | Mid-batch **cancel keeps** already-identified rows. Per-file failure **continues** the rest and flags that row in Scan Queue — do not stop the batch. |
| Inspector vs queue commit | **Both surfaces** (C5): inspector commits one card now; Scan Queue strip handles multi-add / bulk commit. Same cart, two commit paths. |
| Webcam picker | Desktop only: `enumerateDevices` + `deviceId`. Mobile keeps facing-mode swap (C4). |
### Open IA questions
None — all five conductor questions resolved above.
## Design direction
Locked v1 against `image-1eeebe23-9983-4a96-a3e7-4e3cdfdceb5b.png`
(dark + light side-by-side). **Incremental redesign** of an existing
Liquid Glass surface — not a new palette. Generator run:
`desktop trading card scanner workstation camera inspector queue
glassmorphism` (`ui-ux-pro-max` v2.5.0).
### Summary
On `md+`, `/scanner` is a **desk workstation** inside normal app
chrome (sidebar + TopSearchBar): a framed live-camera panel with
ember corner brackets, a right-rail **live match inspector**, and a
bottom **history / queue strip** — inspect one card, commit from the
rail, or batch from the strip. Mobile stays immersive; this direction
applies only at `md+`.
### Pattern + style
| Field | Value |
| --- | --- |
| Product type | Desktop trading card scanner workstation |
| Landing / app pattern | Feature-Rich Showcase → **workstation grid** (camera + inspector + strip; not a marketing landing) |
| UI style | **Liquid Glass** (existing Deck Hearth DS) — translucent panels, rim-light, ember accent rings |
| Stack notes | nextjs · React 18 · Tailwind + CSS variables · `<Modal>` / `<GlassSurface>` / `<Button>` primitives |
**Generator vs repo (repo wins):**
| Generator | Locked override |
| --- | --- |
| Exaggerated Minimalism (oversized type, massive whitespace) | **Rejected** — match existing app density; page title `text-2xl` / `font-semibold`, subtitle `text-sm text-secondary` |
| Palette `#1E293B` / `#2563EB` scan blue | **Rejected** — use `--accent-ember`, `--accent-flame`, `--accent-gold`, `--bg-*`, `--text-*` from `styles/globals.css` |
| Inter typography | **Rejected** — keep system stack (`-apple-system, BlinkMacSystemFont, …`) |
| Feature-Rich Showcase sections | **Adapted** — three functional zones (camera, inspector, strip) replace marketing feature cards |
### Layout authority (`md+` only)
Viewport split at `md` (768px). Below `md`, no changes (immersive +
checkout sheet).
```
┌─────────────────────────────────────────────────────────────────┐
│ [Layout sidebar] │ TopSearchBar + profile │
├──────────────────┼──────────────────────────────────────────────┤
│ │ Card Scanner [Scanner Tips] │
│ │ <subtitle>
│ ├──────────────────────────┬───────────────────┤
│ │ ┌─ Auto-detect ON ─┐ │ Scan Result 98% │
│ │ │ [live video] │ │ ┌────┐ metadata │
│ │ │ ember brackets │ │ │thumb│ set/rarity │
│ │ └──────────────────┘ │ condition ▾ foil │
│ │ [Camera ▾][Upload][Batch] │ confidence bar │
│ │ [Auto-detect]│ + Add to My Coll. │
│ │ │ Rescan | Add List │
│ ├──────────────────────────┴───────────────────┤
│ │ Recent │ Queue (N) │ Dupes (N) Clear All│
│ │ [chip][chip][chip]… │
│ │ [ View All Scans ] │
└──────────────────┴──────────────────────────────────────────────┘
```
| Zone | Width / placement | Surface recipe |
| --- | --- | --- |
| Page header | Full content width above grid | Flat on `--bg-primary`; Tips = `<Button variant="ghost">` + lightbulb icon |
| Camera column | `flex-1` / `min-w-0`; ~6065% | Viewport frame: `--glass-surface-low` + `--ember-rim-subtle` on outer frame; **ember corner brackets** on video (existing `ScannerCamera`) |
| Desk control bar | Directly under viewport | `--glass-surface-mid` bar, `rounded-xl`, horizontal flex wrap at `md` |
| Inspector rail | Fixed `w-[360px]` or `max-w-sm`; sticky top optional | `--glass-surface-low` panel, `--elevation-ambient`, `--rim-light-inner/outer` |
| Bottom strip | Full content width below main grid | `--glass-surface-mid` container; tab row + chip scroller |
**Page copy (header):**
- Title: **Card Scanner**
- Subtitle: *Identify cards with your webcam and add them to My
Collection.*
### Colors
All values via CSS variables — **no hex in `.js`**.
| Role | Token | Usage on this surface |
| --- | --- | --- |
| Page background | `--bg-primary` | Workstation canvas behind glass panels |
| Panel fill | `--glass-surface-{low,mid,high}` | Camera frame (low), control bar + strip (mid), popovers (high) |
| Primary text | `--text-primary` | Titles, card names, tab labels |
| Secondary text | `--text-secondary` | Subtitle, set/rarity, timestamps |
| CTA / primary action | `--accent-ember` | **Add to My Collection** button; active tab underline; selected chip border |
| Accent highlight | `--accent-flame`, `--accent-gold` | Confidence bar fill; match badge when ≥90% |
| Interactive rim | `--ember-rim-subtle` / `--ember-rim-pronounced` | Camera frame rest; primary button hover/focus |
| Success / match | `--accent-gold` or existing success token | Auto-detect ON dot; high-confidence pill |
| Error / batch fail | `--accent-ember` at reduced opacity or danger variant | Failed queue row indicator |
| Border | `--border` | Control bar dividers, chip separators |
Light and dark both use the same token names; theme context switches
values. Mock's cool-navy dark is approximated by existing
`--bg-primary-dark` — do not introduce a new dark base.
### Typography
| Role | Font | Notes |
| --- | --- | --- |
| Page title | System stack, `text-2xl font-semibold` | "Card Scanner" |
| Subtitle | System stack, `text-sm`, `--text-secondary` | One line under title |
| Section labels | `text-sm font-medium uppercase tracking-wide` | "Scan Result", strip tab labels |
| Card name (inspector) | `text-lg font-semibold` | Primary identify headline |
| Metadata | `text-sm`, `--text-secondary` | Set · rarity · collector # |
| Chip / queue row | `text-sm` name, `text-xs` meta | Timestamps right-aligned |
### Camera viewport + desk controls
**Inside frame (overlay on video):**
- **Auto-detect badge** — top-left pill: green status dot +
`Auto-detect ON` / `Auto-detect OFF` (reflects pause toggle).
- **Ember corner brackets** — four L-shaped corners on the card
alignment region (ship existing `ScannerCamera` bracket styling).
- Optional alignment guide (mock dotted vertical line) — **omit in
v1** unless implementer brief explicitly includes it; brackets +
badge are required.
**Desk control bar (below frame, left → right):**
| Control | Component | Notes |
| --- | --- | --- |
| Camera | `<select>` or styled native picker, label **Camera** | `enumerateDevices` video inputs; desktop only |
| Upload Image | `<Button variant="secondary">` + upload icon | Single-file; existing gallery path |
| Batch Scan | `<Button variant="secondary">` + stack icon | Multi-file picker; triggers sequential identify |
| Auto-detect | Toggle switch + label **Auto-detect** | Right-aligned on wide screens; extends `verificationPausedRef` |
**Webcam device picker — fallback copy (locked):**
| State | Message | Action hint |
| --- | --- | --- |
| Loading | Detecting cameras… | Disable `<select>` until resolved |
| Empty list | No camera found | Connect a webcam or use **Upload Image**. |
| Permission denied | Camera access blocked | Allow camera in your browser settings, or use **Upload Image**. |
| Error (enumerate failed) | Couldn't list cameras | Use **Upload Image** or reload the page. |
When empty or denied, render the message inline below the disabled
`<select>` (`text-sm`, `--text-secondary`); do not use a blocking
modal for device errors.
### Live match inspector (right rail)
Header row: **Scan Result** + confidence pill (e.g. `98% Match`).
**Empty state** (no identify yet): centered illustration area +
*"Point your camera at a card or upload an image to see a match."*
**Populated state** (latest unprocessed cart entry):
- Thumbnail + name, set, rarity icon, collector number
- **Condition**`<select>` (existing condition options)
- **Foil** — toggle switch
- Confidence — percentage + horizontal bar + caption (*Excellent match.*
/ *Good match.* / *Low confidence — verify before adding.*)
- Actions (top → bottom priority):
1. **Primary:** `VOCAB.ADD_TO_MY_COLLECTION` (`<Button variant="primary">`, `+` icon optional)
2. **Secondary row:** **Rescan** (ghost) · `VOCAB.ADD_TO_LIST` (ghost)
- **Never** ship "Add to Collection", "Save to Wishlist", or "Mark
Owned".
### Bottom history / queue strip
**Tab row:** **Recent Scans** · **Scan Queue** `(N)` · **Duplicates**
`(N)` — badge = unprocessed count / duplicate row count per IA.
**Clear All** — text link, right-aligned (`text-sm`, destructive on
hover).
**Card chips** (horizontal scroll, `overflow-x-auto`):
- Thumbnail, name, set · rarity · condition snippet
- Status: confidence % + relative time (*Just now*, *2 min ago*)
- Active / focused row: `--ember-rim-pronounced` border (matches mock
ember outline on leftmost chip)
- **No `backdrop-filter` on individual chips** — solid
`--bg-secondary` per per-card grid performance budget
(`docs/DESIGN_TOKENS.md` § Per-card grid performance budget)
**View All Scans** — full-width ghost button at strip bottom; opens
expanded queue view or scrolls strip — Architect brief decides;
visual = translucent bar button from mock.
**Row click** — focuses that card in the inspector (IA-locked).
### Batch Scan progress (Scan Queue tab)
When batch is running, **Scan Queue** tab auto-focuses (or shows an
inline banner):
- Progress line: **Scanning 3 of 12…** with determinate progress bar
(`--accent-ember` fill)
- **Cancel** — ghost button; **keeps already-identified rows** (IA)
- Per-file failure — enqueue row with error flag, label **Identify
failed**, `text-sm` reason if available; batch **continues** remaining
files
- On complete — banner dismisses; failed rows stay in queue with
visual error state (ember left border or warning icon)
### Scanner Tips (surface locked)
**`<Modal>`** — five tips exceed popover length; use
`components/ui/Modal` + modal panel recipe (`--glass-surface-low`,
`--modal-scrim`). Trigger: header **Scanner Tips** button (lightbulb
icon + label).
| # | Tip |
| --- | --- |
| 1 | **Good lighting** — avoid glare on foil cards. |
| 2 | **Fill the frame** with one card; keep corners visible. |
| 3 | **Hold still** until Auto-detect locks the match. |
| 4 | **Use Batch Scan** for a pile of photos from your gallery. |
| 5 | **Switch camera** if the image is dark or mirrored. |
Modal title: **Scanner Tips**. Dismiss via close control + Esc +
scrim click (standard `<Modal>` behavior).
### Effects + motion
- Panel transitions: **150200ms** ease on hover/focus for buttons and
chips (`docs/MOTION_SYSTEM.md` if defined; else `transition-colors
duration-200`)
- Auto-detect badge dot: subtle pulse when ON; **respect
`prefers-reduced-motion`** (static dot when reduced)
- Confidence bar: width transition 300ms on value change; no animation
when reduced
- Tab switch: instant content swap (no slide); optional 150ms fade on
chip row refresh
- Hover: `cursor-pointer` on all clickable chips, tabs, buttons;
`--ember-rim-subtle``--ember-rim-pronounced` on primary hover
### Anti-patterns (do not ship)
- Full-bleed camera overlay on desktop (mobile-only chrome)
- Hex colors in `.js` / inline styles
- Generator slate/blue palette or Inter font import
- `backdrop-filter` on per-card strip chips or thumbnail tiles
- "Add to Collection", "Save to Wishlist", "Mark Owned", "All My
Cards" in UI copy
- Custom modal scrim (must use `<Modal>` primitive)
- Popover for five tips (use Modal)
- Parallel batch identify UI implying concurrent Gemini calls
- Device `<select>` on mobile (facing-mode toggle stays)
### Pre-delivery checklist
- [ ] No emojis as icons (SVG: Lucide / Heroicons)
- [ ] `cursor-pointer` on clickable elements
- [ ] Hover/focus transitions 150300ms
- [ ] Text contrast ≥ 4.5:1 on glass-over-flat surfaces
- [ ] Keyboard focus visible (`--ember-rim-pronounced` focus ring)
- [ ] `prefers-reduced-motion` respected (badge pulse, bar animate)
- [ ] Responsive: mobile immersive unchanged; workstation at 768 /
1024 / 1440
- [ ] Light + dark verified against mock reference
- [ ] Copy from `lib/collection-vocabulary.js` for add actions
- [ ] Visual-diff: desktop `/scanner` if in suite
### Conflict rule
**Repo design tokens win** when they disagree with generator output.
This lock intentionally overrides `ui-ux-pro-max` palette and typography
only. Layout proportions follow the attached mock; nav items in the
mock that are not product routes (Wishlist, Trades, etc.) are **not**
in scope — existing Layout sidebar IA stands.
## UX
### 1. Existing components to reuse
| Component | Path | Use on desktop workstation |
| --- | --- | --- |
| `<Modal>` | `components/ui/Modal.js` | Leave-with-queue, List picker, **Scanner Tips** (five tips — no custom scrim) |
| `<Button>` | `components/ui/Button.js` | Upload Image, Batch Scan, Auto-detect toggle label area, inspector primary/ghost actions, batch Cancel, View All Scans |
| `<GlassSurface>` | `components/ui/GlassSurface.js` | Camera frame, desk control bar, inspector rail, strip container, strip chips (solid `--bg-secondary` fill inside — no per-chip blur) |
| `<Input>` | `components/ui/Input.js` | Not required v1; strip search deferred to Architect brief |
| `ScannerCamera` | `components/scanner/ScannerCamera.js` | Framed viewport, ember brackets, overlay Auto-detect badge; hide mobile overlay chrome at `md+` via variant prop |
| `ScannerDisambiguation` | `components/scanner/ScannerDisambiguation.js` | Unchanged multi-match picker; pauses identify via existing `verificationPausedRef` wiring |
| `ScannerToast` | `components/scanner/ScannerToast.js` | Post-commit confirmation, batch complete summary, identify errors |
| `ScannerCheckoutSheet` | `components/scanner/ScannerCheckoutSheet.js` | **Mobile only** (`max-md`) — do not mount at `md+` |
| `ScannerCountPill` | `components/scanner/ScannerCountPill.js` | **Mobile only** — desktop uses strip Queue badge instead |
| `ReviewCardItem` | `components/scanner/ReviewCardItem.js` | **Pattern donor** for condition `<select>`, foil toggle, confidence ring/color helpers, increment/decrement — lift constants (`CONDITION_OPTIONS`, `confidencePercent`, `confidenceRingColor`) into shared export or duplicate minimally in `ScannerResultPanel` |
| Leave modal + List picker | `pages/scanner.js` | Reuse verbatim markup and handlers; extend leave trigger to desktop back/nav paths |
**New surfaces** (`ScannerResultPanel`, `ScannerHistoryStrip`, `ScannerTips`) compose primitives only — no new modal shell, no hex in `.js`.
### 2. Design direction alignment
- **Liquid Glass tokens:** All panels use `--glass-surface-{low,mid,high}`, `--ember-rim-{subtle,pronounced}`, `--elevation-ambient` per Design direction layout authority table. Inspector rail = `GlassSurface` tint `low`; desk bar + strip = `mid`.
- **Ember accent:** Primary add, active tab underline, focused chip border, Auto-detect ON dot use `--accent-ember` / `--accent-flame` — not generator slate/blue.
- **Copy lock:** Inspector and strip actions import `VOCAB.ADD_TO_MY_COLLECTION` and `VOCAB.ADD_TO_LIST` from `lib/collection-vocabulary.js`. Wishlist omitted (IA-locked).
- **Performance budget:** Strip chips and inspector thumbnail tiles use solid `--bg-secondary`**no `backdrop-filter` on per-card elements** (Design direction + `docs/DESIGN_TOKENS.md`).
- **Tips surface:** `<Modal size="md">` with glass panel recipe — matches Design direction "Popover for five tips" rejection.
- **Motion:** 150200ms color/rim transitions; badge pulse and confidence bar width animate only when `prefers-reduced-motion: no-preference` (see §7 interaction patterns).
### 3. Existing patterns to follow
- **Viewport split:** `pages/scanner.js` already gates mobile checkout with `md:hidden` / `hidden md:flex` — extend to `Layout chrome="default"` at `md+` only; keep `chrome="immersive"` below `md`.
- **Pause identify:** `verificationPausedRef` in `pages/scanner.js` (lines 5963) — extend desktop Auto-detect OFF to set ref; keep existing pauses for list picker + disambiguation + mobile checkout.
- **Single-card commit:** `queue.addSingleCardToOwned(card)` in `lib/use-scanner-queue.js` — inspector primary action; same API path as mobile bulk commit.
- **Leave with queue:** Existing `<Modal title="Leave scanner?">` + `clearScannerCartStorage()` — trigger from desktop back, sidebar nav away, and `router.back()` when `unprocessedCount > 0`.
- **List commit:** Existing List picker `<Modal>` + `handleListPick` — inspector `VOCAB.ADD_TO_LIST` opens same modal scoped to **focused inspector card** (single selection), not bulk strip selection.
- **Error alerts:** Match `ScannerCheckoutContent` / `pages/scanner.js` list picker — `role="alert"` div with `color-mix(in srgb, var(--color-error) …)` for commit and batch failures.
- **Confidence UX:** Reuse `ReviewCardItem` thresholds: `<70%` ember warning, `≥90%` gold/high — caption text from Design direction (*Excellent match.* / *Good match.* / *Low confidence — verify before adding.*).
- **Device picker fallbacks:** Inline `text-sm` `--text-secondary` messages below disabled `<select>` per Design direction table — not a blocking modal.
- **Focus rings:** `Button` / `ChromeIconButton` pattern — `focus-visible:ring-2` + `'--tw-ring-color': 'var(--accent-ember)'` (`components/scanner/ScannerCamera.js`).
### 4. A11y constraints
Hand to `role-a11y-auditor`:
- **1.4.3 Contrast (Minimum):** All `--text-primary` / `--text-secondary` on `--glass-surface-*` over `--bg-primary` must meet **4.5:1** for body text, **3:1** for large/bold card name (`text-lg font-semibold`). Confidence bar track vs fill must meet **3:1** non-text contrast.
- **1.4.10 Reflow:** At 768px width with 200% zoom, workstation grid stacks camera above inspector; strip tabs remain horizontally scrollable without two-dimensional scroll traps.
- **1.4.11 Focus Not Obscured (Minimum):** Sticky inspector rail and batch progress banner must not fully hide focused desk controls or strip chips.
- **2.1.1 Keyboard:** Every action reachable without pointer: desk controls, strip tabs (tablist), chip rows, inspector add/rescan, batch Cancel, Tips open/close.
- **2.4.3 Focus Order:** DOM order = visual order: page header (Tips) → camera frame → desk bar (Camera → Upload → Batch → Auto-detect) → inspector → strip tabs → chip scroller → View All.
- **2.4.7 Focus Visible:** Ember focus ring on all interactive elements; native `<select>` gets `focus-visible:ring-2` wrapper if browser default ring is suppressed.
- **2.4.11 Focus Not Obscured:** Modal open (Tips, leave, list picker) uses existing `useFocusTrap` — no focus escape to camera video underneath.
- **4.1.2 Name, Role, Value:**
- Auto-detect toggle: `role="switch"`, `aria-checked={!verificationPausedRef}`, visible label **Auto-detect** associated via `htmlFor` / `aria-labelledby`.
- Strip tabs: `role="tablist"` / `role="tab"` / `role="tabpanel"` with `aria-selected`, `aria-controls`, badge counts in `aria-label` (e.g. `Scan Queue, 3 unprocessed cards`).
- Camera `<select>`: `<label>` **Camera** + `aria-describedby` pointing to empty/denied fallback text when present.
- Inspector condition `<select>`: accessible name includes card name (`Condition for {card.name}`).
- Foil toggle: `role="switch"`, `aria-checked`, label **Foil**.
- **4.1.3 Status Messages:** Auto-detect ON/OFF badge exposes `aria-live="polite"` region; batch progress (`Scanning 3 of 12…`) uses `aria-live="polite"`; commit success via `ScannerToast` with `role="status"`.
- **3.3.1 Error Identification:** Batch per-file failures show **Identify failed** + reason text on the queue row; commit errors in inspector use `role="alert"` (same pattern as checkout footer).
- **2.5.3 Label in Name:** Visible button text must appear in accessible name — e.g. **Upload Image**, not icon-only without `aria-label`.
- **Live region discipline:** Only one polite live region for batch progress; avoid duplicate announcements on tab auto-focus during batch.
### 5. Interaction patterns
#### Locked decisions (opinionated — one pattern each)
| Topic | Decision | Nielsen / rationale |
| --- | --- | --- |
| **Empty inspector** | Centered muted camera/scan illustration + single line: *"Point your camera at a card or upload an image to see a match."* No duplicate Upload/Batch CTAs in the rail (controls live in desk bar). | **H8** aesthetic minimalism; **H6** recognition — one place for actions. |
| **Leave with uncommitted queue** | Keep existing `<Modal title="Leave scanner?">`**Keep scanning** (secondary) / **Leave** (danger). Same copy and `clearScannerCartStorage()` behavior on desktop nav-away. | **H3** user control; **H5** error prevention. |
| **Inspector add success** | **Advance inspector to next unprocessed queue entry** (or empty state if queue clear). Show `ScannerToast` (*Added to My Collection*). Committed card appears in **Recent Scans** with processed styling. Do **not** linger on committed card in inspector. | **H2** real-world scan rhythm; **H1** toast + strip update confirm status without blocking next decision. |
| **Auto-detect OFF** | Real pause via `verificationPausedRef` — no identify/OCR while OFF. Badge reads **Auto-detect OFF**; dot static gray. Toggle is independent of mobile checkout pause logic. | **H3** user control; **H1** badge reflects system state. |
| **Rescan** | Clears focus card's identify metadata and re-queues same physical capture path (webcam frame or re-read gallery source if batch row) — card stays in queue unprocessed. | **H3** undo/recover from bad match. |
| **Duplicate row click** | Focuses that card in inspector (scroll chip into view, `--ember-rim-pronounced` on active chip). **Add still allowed** — increment qty via inspector primary or existing queue increment if duplicate already unprocessed. No separate "Skip" action in v1. | **H6** recognition over recall; **H4** consistency with Queue tab. |
| **Batch cancel** | Ghost **Cancel** in Scan Queue tab progress banner. Cancels remaining files; **keeps** already-identified rows (IA-locked). Banner dismisses; toast *Batch scan stopped* (info). | **H3** user control; **H9** recover from long batch. |
| **Batch per-file failure** | Row stays in Scan Queue with ember left border + **Identify failed** label; batch **continues**. Row click focuses inspector for manual Rescan or remove. | **H9** graceful recovery; **H1** visible error on row. |
| **Strip row click (all tabs)** | Sets inspector focus to that card; does not auto-commit. Recent / Queue / Duplicates share one focus model. | **H4** consistency. |
| **Clear All** | Destructive text link — confirm via existing pattern or inline `window.confirm` only if Architect brief adds it; default: immediate clear with undo **not** required v1 (queue is session-scoped). Architect to confirm in brief. | **H5** prevent accidental loss — if no confirm, disable when batch running. |
#### Required
- **Loading — identify in flight (H1):** Inspector shows skeleton/thumbnail placeholder + *Identifying…* on desk bar Upload/Batch busy states disable repeat picks.
- **Loading — commit (H1):** Inspector primary `<Button loading>` during `addSingleCardToOwned`; disable Rescan and secondary actions while processing.
- **Empty strip tabs (H9):** Recent: *No scans yet this session.* Queue: *Scan queue is empty — matches appear here before you add them.* Duplicates: *No duplicates detected.*
- **Auto-detect ON feedback (H1):** Green pulsing dot in frame badge (respect reduced motion); optional success chime already in `ScannerCamera` — desktop may keep muted default.
- **Batch running (H1):** Auto-select **Scan Queue** tab; determinate progress bar; Cancel visible throughout.
- **Focus management (H3):** Opening Tips/list/leave modals traps focus; closing returns focus to trigger button.
- **Hover/focus (H4):** Chips and tabs: `--ember-rim-subtle``--ember-rim-pronounced` on hover/focus; `cursor-pointer` on all click targets.
#### Nice-to-have
- **Optimistic UI:** Inspector advance on commit can happen after API success only (no optimistic skip) — queue hook already marks `processed` post-success.
- **Keyboard shortcut:** `A` to trigger inspector **Add to My Collection** when inspector populated and not processing — defer unless Architect brief adds; not v1 blocker.
- **View All Scans:** Expanded modal or full-width strip — Architect decides; v1 may scroll chip row only.
### 6. Anti-patterns to avoid
- **Duplicate CTAs in empty inspector** — Upload/Batch already in desk bar (**H8** minimalist design).
- **Custom modal scrim for Tips or device errors** — must use `<Modal>` or inline text only (**H4** consistency).
- **Device `<select>` on mobile** — facing-mode swap stays (**H4**).
- **Popover for five Scanner Tips** — too long; Modal only (**H8**).
- **Linger on committed card in inspector after add** — blocks scan rhythm (**H2**).
- **Stop entire batch on one file failure** — IA forbids (**H9**).
- **Discard identified rows on batch cancel** — IA forbids (**H3**).
- **"Save to Wishlist" / "Mark Owned" / "Add to Collection"** copy — CI forbidden strings (**H4**).
- **`backdrop-filter` on strip chips or inspector thumbnail** — GPU budget violation.
- **Full-bleed camera overlay on desktop** — mobile-only chrome (**H2**).
- **Parallel batch progress implying concurrent API calls** — misleading (**H1** honest status).
- **Icon-only desk controls without `aria-label`** — fails **2.5.3** / **4.1.2**.
- **Auto-commit on strip row click** — user loses inspect step (**H3** user control).
- **Slide animation on tab switch** — Design direction specifies instant swap; fade optional ≤150ms only.
### 7. Mobile / responsive notes
**Below `md` (767px) — zero regression:**
- Keep `Layout chrome="immersive"`, `ScannerCamera` overlay chrome (back, gallery, flash, facing, Review N pill), `ScannerCheckoutSheet`, `ScannerCountPill`, scan peek, disambiguation sheet behavior unchanged.
- Do not render desktop workstation grid, inspector rail, history strip, device `<select>`, Batch Scan desk control, or Tips header button on mobile unless Tips is also added to mobile overlay (out of scope — **Tips = desktop header only v1**).
- `verificationPausedRef` mobile checkout pause unchanged (`isCheckoutOpen && max-width 767px`).
**At `md+` (768px+):**
- `Layout chrome="default"` — sidebar + `TopSearchBar` visible.
- Workstation grid: camera column `flex-1 min-w-0` (~6065%) + inspector `w-[360px] max-w-sm` + full-width bottom strip.
- Hide mobile-only elements: checkout sheet, Review N pill, overlay top/bottom camera bars, facing-mode toggle (replace with Camera `<select>`).
- Desk control bar wraps at `md`; Auto-detect toggle right-aligned at `lg+`.
- Strip chip scroller: `overflow-x-auto`, `-webkit-overflow-scrolling: touch` for trackpad/touch on hybrid devices; min chip height **44px** touch target.
- **`prefers-reduced-motion`:** Disable Auto-detect badge pulse, confidence bar width transition, optional chip-row fade — use `@media (prefers-reduced-motion: reduce)` instant state swaps; keep color/rim hover transitions ≤150ms or disable per `docs/MOTION_SYSTEM.md` if defined.
- **Breakpoints to verify:** 768 / 1024 / 1440 — inspector sticky top optional; strip never collapses tabs into mystery meat menu v1.
- **Visual-diff:** Desktop `/scanner` only; mobile baseline must not change.
## Architecture
### File plan
| File | Action | Purpose |
| --- | --- | --- |
| `lib/use-camera-scanner.js` | modified | `enumerateDevices`, `deviceId` selection, picker status, sessionStorage persist |
| `test/lib/use-camera-scanner.test.js` | new | Mock `mediaDevices` — device list, constraint switch, persist |
| `components/scanner/ScannerTips.js` | new | Header trigger + five-tip `<Modal>` |
| `test/components/ScannerTips.test.js` | new | Open/close, copy lock |
| `components/scanner/ScannerResultPanel.js` | new | Right-rail live match inspector |
| `test/components/ScannerResultPanel.test.js` | new | Empty/populated, add loading, vocab |
| `components/scanner/ScannerHistoryStrip.js` | new | Recent / Queue / Duplicates strip + `getDuplicateCards` |
| `test/components/ScannerHistoryStrip.test.js` | new | Tabs, duplicates helper, batch banner |
| `lib/scanner-batch-identify.js` | new | Sequential `runSequentialGalleryIdentify` |
| `test/lib/scanner-batch-identify.test.js` | new | Progress, cancel, error continuation |
| `pages/scanner.js` | modified | md+ workstation grid, focus state, pause composite, chrome split |
| `components/scanner/ScannerCamera.js` | modified | `workstation` variant — framed viewport, hide mobile overlay at md+ |
| `lib/use-scanner-identification.js` | modified | Remove redundant `verificationPausedRef` overwrite effect |
| `lib/use-scanner-queue.js` | modified | Boolean return from `addSingleCardToOwned` / `addSingleCardToCollection` |
| `test/pages/scanner.test.js` | new | Mobile checkout regression + desktop composition smoke |
| `test/components/ScannerCamera.test.js` | new | Workstation variant hides overlay chrome |
**Not touched:** `components/Layout.js` (immersive already equals default at md+),
`lib/scanner-card-identify.js`, `pages/api/scan/identify.js`, `ScannerCheckoutSheet.js`
(mobile only), nav IA (`dashboard-home-realignment`).
### API surface
No new or modified API routes. Client cart remains `sessionStorage` via
`lib/scanner-session.js`. All commits reuse existing `scanner-route-api.js` helpers
(`addScannedCardToOwned`, `addScannedCardToCollection`).
### Schema diff
None.
### Test plan
| Area | File | What to lock |
| --- | --- | --- |
| Device picker hook | `test/lib/use-camera-scanner.test.js` | `deviceId` constraints, enumerate, persist, status messages |
| Batch helper | `test/lib/scanner-batch-identify.test.js` | Sequential order, cancel mid-batch, error continues |
| Tips modal | `test/components/ScannerTips.test.js` | Five tips, Modal primitive |
| Inspector | `test/components/ScannerResultPanel.test.js` | Empty state (no duplicate CTAs), `VOCAB` labels |
| History strip | `test/components/ScannerHistoryStrip.test.js` | `getDuplicateCards`, tab badges, batch banner |
| Camera variant | `test/components/ScannerCamera.test.js` | Overlay hidden for `workstation` at md+ |
| Page orchestration | `test/pages/scanner.test.js` | Checkout sheet at max-md; workstation surfaces at md+ |
| Mobile regression | `test/components/ScannerCheckoutSheet.test.js` | Existing suite stays green — do not weaken |
| Layout chrome | `test/components/Layout.test.js` | Existing immersive tests — no change expected |
**Visual-diff:** `tests/visual/` contains only `homepage.spec.ts` / `home.png`
no `/scanner` baseline update required for this convoy.
### Risk list
| Risk | Mitigation |
| --- | --- |
| `verificationPausedRef` race between page and `useScannerIdentification` | Brief 6 deletes identification hook's direct ref assignment; page owns composite pause including `isAutoDetectPaused` |
| `addSingleCardToOwned` lacks success signal today | Brief 6 adds boolean return in `use-scanner-queue.js` |
| `chrome="immersive"` on all viewports today | Boot finding: Layout already shows sidebar at md+; page uses explicit `default` vs `immersive` via `matchMedia` for clarity |
| Batch identify failures invisible in queue | Page enqueues `identifyFailed` rows; strip shows ember border + label |
| Rescan without stored capture | v1: re-identify via `scanImageUrl` fetch; else toast to rescan from camera |
| Desktop/mobile class split regressions | Tests mock `matchMedia`; mobile paths keep `md:hidden` gates |
| Forbidden copy in new surfaces | Import `VOCAB` from `collection-vocabulary.js`; CI `forbidden-stale-strings` |
| Brief 6 LOC >400 | Accepted — single serializer for `pages/scanner.js`; components split across Briefs 24 |
| `getDuplicateCards` over-counts | Helper uses ownershipMap + session name/set + quantity>1; tune in strip tests |
### Decomposition
| Brief # | Title | Files | Depends on | Est. PR size |
| --- | --- | --- | --- | --- |
| 1 | Camera device picker hook | `use-camera-scanner.js`, test | — | S (~180 LOC) |
| 2 | Scanner Tips modal | `ScannerTips.js`, test | — | S (~100 LOC) |
| 3 | Live match inspector | `ScannerResultPanel.js`, test | — | M (~280 LOC) |
| 4 | History / queue strip | `ScannerHistoryStrip.js`, test | — | M (~300 LOC) |
| 5 | Sequential batch helper | `scanner-batch-identify.js`, test | — | S (~120 LOC) |
| 6 | Page workstation wiring | `scanner.js`, `ScannerCamera.js`, identification + queue hooks, tests | 1, 2, 3, 4, 5 | L (~450 LOC) |
**Estimated PRs:** 6 (Briefs 15 parallelizable; Brief 6 after merge or rebase).
### Slice dependencies (multitask-ready)
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- lib/use-camera-scanner.js
- test/lib/use-camera-scanner.test.js
- brief: 2
depends_on: []
files:
- components/scanner/ScannerTips.js
- test/components/ScannerTips.test.js
- brief: 3
depends_on: []
files:
- components/scanner/ScannerResultPanel.js
- test/components/ScannerResultPanel.test.js
- brief: 4
depends_on: []
files:
- components/scanner/ScannerHistoryStrip.js
- test/components/ScannerHistoryStrip.test.js
- brief: 5
depends_on: []
files:
- lib/scanner-batch-identify.js
- test/lib/scanner-batch-identify.test.js
- brief: 6
depends_on: [1, 2, 3, 4, 5]
files:
- pages/scanner.js
- components/scanner/ScannerCamera.js
- lib/use-scanner-identification.js
- lib/use-scanner-queue.js
- test/pages/scanner.test.js
- test/components/ScannerCamera.test.js
```
### Boot-the-brief findings (fixed in briefs)
1. **Layout chrome:** `chrome="immersive"` already renders sidebar + TopSearchBar at
`md+` (`Layout.js` uses `max-md:hidden` only). Brief 6 uses `matchMedia` to pass
`chrome="default"` on desktop for semantic clarity — no `Layout.js` edit.
2. **`verificationPausedRef` overwrite:** `useScannerIdentification` lines 6266 set
the ref to `Boolean(disambiguation)` only, racing page composite pause — Brief 6
removes that effect.
3. **`addSingleCardToOwned` return:** No success boolean today — Brief 6 extends
`use-scanner-queue.js` to return `true`/`false`.
4. **Visual suite:** `/scanner` not in `tests/visual/` — no Linux baseline refresh.
5. **No new packages** — dep-set check N/A; all primitives exist (`Modal`, `GlassSurface`, `Button`).
6. **Gallery batch path verified:** `identifyFromGalleryFile` exists in
`use-scanner-identification.js` (line 326) — Brief 5 wraps it sequentially.
## As-shipped
Shipped 2026-08-15 as PR #165 (squash `938c161`, commit subject
`feat(scanner): add desktop workstation layout (#165)`). All six briefs'
surfaces landed: md+ workstation chrome, device picker, Upload Image,
Batch Scan, Auto-detect toggle, live match inspector, and the bottom
Recent Scans / Scan Queue / Duplicates strip; mobile immersive checkout
unchanged below `md`.
Related post-merge scanner fixes on adjacent convoys (not this scope):
#166 (multi-card flow + frame overlay + rate limits) and #167 (manual
tap-to-scan shutter).

View file

@ -1,147 +0,0 @@
# A11y audit — scanner-desktop-layout
**Reviewer:** role-a11y-auditor
**Convoy:** `scanner-desktop-layout`
**Surface:** local UI diff vs `origin/main` (+ untracked workstation components) — audit group `audit-scanner-desktop-layout-local`
**Date:** 2026-08-15
**Model:** cursor-grok-4.5-high (`model_tier=fast`)
**Skill note:** `.cursor/skills/accessibility-audit/SKILL.md` was not present in-repo; audit follows `role-a11y-auditor.md`, prior report shape (`.convoys/scanner-mobile-checkout/audits/a11y-20260814.md`), convoy UX §4 A11y constraints, and WCAG 2.2 AA.
## Diff scope (UI)
| Path | Status |
| --- | --- |
| `pages/scanner.js` | modified (workstation wiring) |
| `components/scanner/ScannerCamera.js` | modified (`variant="workstation"`, Auto-detect badge) |
| `components/scanner/ScannerResultPanel.js` | **new** |
| `components/scanner/ScannerHistoryStrip.js` | **new** |
| `components/scanner/ScannerTips.js` | **new** |
| `test/components/ScannerHistoryStrip.test.js` | new (a11y-adjacent: tab `aria-label`s) |
| `test/components/ScannerResultPanel.test.js` | new (condition name / foil switch) |
| `test/components/ScannerTips.test.js` | new |
| `test/components/ScannerCamera.test.js` | new |
Non-UI (`lib/use-camera-scanner.js`, batch helper, queue hooks) excluded from findings except where they gate AT-visible status.
Patterns referenced: `components/ui/Modal.js` (`useFocusTrap`, `role="dialog"`), `components/ui/Button.js` (ember `focus-visible` ring, visible label in name).
## Executive summary
- Desktop workstation ships strong foundations: Tips via `<Modal>` + trap, Auto-detect / Foil `role="switch"`, strip `tablist`/`tab`/`tabpanel` with badge counts in `aria-label`, condition `aria-label` includes card name, commit errors `role="alert"`, ember `focus-visible` rings, toast `role="status"`, mobile checkout `inert`/`trapActive` carry-forward.
- **No severity ≥ 3 findings.** Several **sev-2** gaps vs promised UX §4 constraints: Camera fallback lacks `aria-describedby`; Auto-detect overlay badge has no `aria-live`; batch progress + Cancel live only inside the Queue `tabpanel` (often `hidden`); empty-inspector “Identifying…” is wired to disambiguation, not identify-in-flight; queue badge white-on-ember is ~4.44:1; `--color-*` semantic tokens appear undefined in `styles/`.
- **Recommendation:** comment-only for merge gate (no sev ≥ 3); fix the §4 constraint fails (A1A4) in this PR or an immediate a11y follow-up.
**Counts:** 8 findings (sev ≥ 3: **0**, sev &lt; 3: **8**). Constraint checklist: **8 pass / 3 partial / 3 fail**.
## Convoy constraint checklist (UX §4)
| # | Constraint | Result | Notes |
| --- | --- | --- | --- |
| 1 | **1.4.3** Contrast text on glass | **Partial** | Body `--text-primary/secondary` on solid `--bg-*` ≥ 4.5:1 (sampled). Queue badge `#ffffff` on `--accent-ember`**4.44:1** (fails normal text). Glass composite not instrumented. |
| 2 | **1.4.10** Reflow @ 768 / 200% | **Pass*** | `md:flex-row` → column below `md`; strip `overflow-x-auto` only (*static; no browser zoom run). |
| 3 | **1.4.11** Focus not obscured (sticky) | **Pass** | Inspector rail is flex column sibling — not `position: sticky/fixed`. Batch banner sits in strip flow. |
| 4 | **2.1.1** Keyboard all actions | **Partial** | Desk controls, tips, inspector, tabs, chips, Clear All, View All are `<button>`/`<select>`. Batch **Cancel** only mounts on Queue tab — reachable after tab switch, not after Batch Scan without navigating. |
| 5 | **2.4.3** Focus order | **Pass** | DOM ≈ convoy: Tips → camera → desk (Camera → Upload → Batch → Auto-detect) → inspector → strip tabs → chips → View All. |
| 6 | **2.4.7** Focus visible | **Pass** | Ember `focus-visible:ring-2` on Button, switches, selects, tabs, chips, Clear All. |
| 7 | **2.4.11** Focus not obscured (modals) | **Pass** | Tips / leave / list picker use `Modal` + `useFocusTrap`; Escape closes. |
| 8 | **4.1.2** Auto-detect switch | **Pass** | `role="switch"` `aria-checked={!isAutoDetectPaused}` `aria-labelledby` → visible **Auto-detect**. |
| 9 | **4.1.2** Strip tabs | **Pass** | `tablist` / `tab` / `tabpanel`, `aria-selected`, `aria-controls`, badge counts in `aria-label` (locked by tests). |
| 10 | **4.1.2** Camera `<select>` + `aria-describedby` | **Fail** | Visible `<label>Camera</label>` OK; fallback `<p>` has **no `id` / no `aria-describedby`**. |
| 11 | **4.1.2** Condition + Foil | **Pass** | Condition `aria-label={`Condition for ${card.name}`}`; Foil switch + `aria-labelledby`. |
| 12 | **4.1.3** Auto-detect badge live region | **Fail** | Overlay badge is visual only (decorative dot `aria-hidden`); **no `aria-live`**. Switch announces only when focused/toggled. |
| 13 | **4.1.3** Batch progress live + toast | **Partial** | Banner has `aria-live="polite"` + `progressbar`; toast `role="status"`. Banner lives only inside Queue panel — silent when another tab is selected. |
| 14 | **3.3.1** Identify failed + commit alert | **Pass** | Chip shows **Identify failed** + reason; inspector `role="alert"` for commit errors. |
| 15 | **2.5.3** Label in Name | **Pass** | Upload Image / Batch Scan / Scanner Tips / vocab CTAs use visible text (not icon-only). |
| 16 | Live region discipline | **Partial** | One batch banner (good); camera toast + page toast both `aria-live="polite"`; DetectionFrame also polite (pre-existing). |
## Findings
| Sev | Layer | WCAG | Surface | Issue | Fix |
| --- | --- | --- | --- | --- | --- |
| **2** | Robust / Name | 1.3.1, 4.1.2 | `pages/scanner.js` ~L415418 | Device fallback message is not linked to the Camera `<select>` (`aria-describedby` promised). Disabled empty/denied states are orphaned for AT. | Give message `id="scanner-camera-select-hint"`; set `aria-describedby={camera.devicePickerMessage ? 'scanner-camera-select-hint' : undefined}` on `<select>`. |
| **2** | Status | 4.1.3 | `ScannerCamera.js` ~L350370 | Workstation Auto-detect ON/OFF badge has no live region (constraint 12). | Wrap badge text in `role="status" aria-live="polite"` (or `aria-live` on the GlassSurface), e.g. announce `Auto-detect ON` / `OFF` when `autoDetectOn` changes. Keep decorative dot `aria-hidden`. |
| **2** | Status / Operable | 4.1.3, 2.1.1 | `ScannerHistoryStrip.js` ~L277285; `pages/scanner.js` `handleBatchChange` | `BatchProgressBanner` (live region + Cancel) renders only inside Queue `tabpanel`. Default/other tabs leave banner `hidden` → progress often unannounced; Cancel not in immediate focus path after Batch Scan. | On batch start: `setStripActiveTab(TAB_QUEUE)`; and/or mount a single always-visible polite region + Cancel above the tablist (one live region). |
| **2** | Status | 4.1.3 | `pages/scanner.js` ~L520; `ScannerResultPanel.js` ~L8386 | `isIdentifying={Boolean(identification.disambiguation)}` never reflects gallery/webcam identify-in-flight → empty rail stays “Point your camera…” instead of “Identifying…”. | Pass a real identifying flag from identification hook (or `galleryBusy \|\| batchBusy` / in-flight promise); keep disambiguation separate. Prefer `aria-live="polite"` on the Identifying copy. |
| **2** | Perceivable | 1.4.3 | `ScannerHistoryStrip.js` ~L246254 | Badge uses hardcoded `#ffffff` on `--accent-ember` (~**4.44:1**) under 4.5:1 for `text-xs`. | Use a tokenized on-accent color that clears 4.5:1, or enlarge/bold to large-text threshold, or place count in `aria-label` only with a higher-contrast chip chrome. |
| **2** | Perceivable | 1.4.11, 1.4.1 | `ScannerResultPanel.js` `confidenceRingColor`; error alerts | `--color-success` / `--color-warning` / `--color-error` / `--color-info` are **referenced but not defined** under `styles/` (or Tailwind theme). Mid/high confidence fills and error tints may not paint; low band still uses `--accent-ember`. Text % + captions / `role="alert"` mitigate. | Define semantic tokens in the design-token surface (or map to existing `--accent-*` / documented status colors) so bar fill meets **3:1** vs track and error text remains distinguishable. |
| **1** | Keyboard | 2.1.1 (APG) | `ScannerHistoryStrip.js` tablist | Tabs are all in Tab order; no arrow-key roving `tabIndex` (ARIA APG tabs pattern). | Optional: selected tab `tabIndex={0}`, others `-1`; Left/Right moves selection. Not required for WCAG if all tabs remain operable. |
| **1** | Status | 4.1.3 | `pages/scanner.js` + `ScannerCamera.js` | Page-level `ScannerToast` and in-camera toast are both polite live regions; DetectionFrame adds more. Risk of duplicate/noisy announcements. | Prefer one page-level toast on desktop workstation; suppress camera toast when `variant="workstation"`, or share a single live region host. |
### Severity ≥ 3 detail
None.
## Layer walk (5 layers, summary)
| Layer | Verdict |
| --- | --- |
| **1 Perceivable** | Token body text contrast OK on solids; badge white/ember short; confidence mid/high fill tokens missing; confidence still has % + caption (not color-only); reflow structure OK statically. |
| **2 Operable** | Focus rings consistent with Button/Modal; modals trapped; batch Cancel discoverability weak; tab APG optional. |
| **3 Understandable** | Labels strong (Camera, Auto-detect, Foil, condition-with-name, vocab CTAs); leave modal copy includes count + “Keep scanning”; device fallback not programmatically associated. |
| **4 Robust** | Switch/tablist/tabpanel/progressbar/alert/status mostly correct; missing `aria-describedby` + badge live region; Identifying flag mis-wired. |
| **5 Consistency** | Correctly lifts Modal/Button/GlassSurface; matches ReviewCardItem condition/foil naming; mobile `inert` + `trapActive={!isListPickerOpen}` retained from prior a11y fix. |
## Suggested diffs (priority)
1. **P1** — Camera `aria-describedby` ↔ fallback message id.
2. **P1** — Auto-detect badge `aria-live="polite"` (or `role="status"`).
3. **P1** — On batch start, select Queue tab **or** hoist progress+Cancel outside `hidden` tabpanels (single polite region).
4. **P1** — Wire real `isIdentifying` for empty inspector + polite announcement.
5. **P2** — Badge contrast; define `--color-success|warning|error|info`.
6. **P3** — Optional tab roving tabindex; dedupe desktop toasts.
### Minimal patches (illustrative)
```jsx
// pages/scanner.js — Camera describedby
<p id="scanner-camera-select-hint" className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{camera.devicePickerMessage}
</p>
<select
id="scanner-camera-select"
aria-describedby={camera.devicePickerMessage ? 'scanner-camera-select-hint' : undefined}
/>
```
```jsx
// ScannerCamera.js — badge live region
<GlassSurface role="status" aria-live="polite">
<span className="…" aria-hidden="true" />
Auto-detect {autoDetectOn ? 'ON' : 'OFF'}
</GlassSurface>
```
```js
// pages/scanner.js — batch start
setStripActiveTab(TAB_QUEUE);
setBatchProgress({ active: true, current: 0, total: files.length, onCancel: … });
```
## Patterns to lift
- `Button` — visible children + ember `focus-visible` ring; keep for desk/inspector CTAs.
- `Modal` + `useFocusTrap` — correct Tips / leave / list picker shell; do not invent a second dialog.
- Strip tab `aria-label` helpers (`queueTabAriaLabel` / `duplicatesTabAriaLabel`) — good; keep badge `aria-hidden` on visual count.
- Foil / Auto-detect switch markup — reusable 44×44 hit pad + `aria-labelledby`.
- Prior mobile fix: `inert` + `trapActive={!isListPickerOpen}` — retain.
## Automated checks
| Check | Result |
| --- | --- |
| axe-core / CI a11y job | Not run (static role audit) |
| Contrast math | Sampled token pairs + white/ember badge (see § checklist) |
| Manual SR | Recommend VoiceOver: toggle Auto-detect; start Batch Scan from Recent tab; deny camera permission and inspect select description; commit error alert |
## Approval recommendation
- [ ] approve
- [ ] request-changes (sev ≥ 3)
- [x] **comment-only** — no sev ≥ 3; **fix A1A4 (sev 2 constraint fails) before or immediately after merge**
## Hand-off
A11y audit complete. **8 findings** (sev ≥ 3: **0**, sev &lt; 3: **8**).
Report: `.convoys/scanner-desktop-layout/audits/a11y-20260815.md`.
Recommend fixing sev ≥ 3 before merge — none open; still land P1 constraint fixes (describedby, badge live region, batch progress visibility, Identifying wiring).

View file

@ -1,153 +0,0 @@
# Design-System Audit — scanner-desktop-layout
**Role:** `role-design-system-auditor`
**Surface:** `convoy/scanner-desktop-layout` workstation UI (`git diff origin/main` + untracked scanner components)
**Convoy:** `.convoys/scanner-desktop-layout.md`
**Multitask group:** `audit-scanner-desktop-layout-local`
**Date:** 2026-08-15
**Scope:** UI / DS only — read-only; no code changes
**Inputs reviewed**
| Source | Notes |
| --- | --- |
| Diff UI (vs `origin/main`) | `pages/scanner.js`, `components/scanner/ScannerCamera.js` (+ unrelated dashboard/Layout noise in same branch tip — **out of audit scope**) |
| Untracked (convoy) | `ScannerTips.js`, `ScannerResultPanel.js`, `ScannerHistoryStrip.js` + component tests |
| Direction lock | `design_direction` v1 + `## Design direction` + `## UX` |
| Tokens | `docs/DESIGN_TOKENS.md`, `styles/globals.css` |
| Primitives | `components/ui/{GlassSurface,Button,Modal,Input}.js` |
| Skill | `.cursor/skills/design-systems/SKILL.md` **not installed**; report follows prior Liquid Glass audit shape + role file |
---
## Audit summary
| Check | Status | Notes |
| --- | --- | --- |
| Direction lock (md+ workstation grid) | ✅ Strong | Framed camera + desk bar + inspector rail + history strip + Tips Modal; mobile immersive left alone |
| Hex in JSX (new convoy files) | ❌ | `#ffffff` on Queue/Duplicates badge in `ScannerHistoryStrip.js` |
| VOCAB / stale strings | ✅ | `VOCAB.ADD_TO_*` / `MY_COLLECTION`; no Wishlist / Mark Owned / All My Cards |
| `backdrop-filter` on strip chips | ✅ | Chips solid `--bg-secondary`; container is `GlassSurface tint="mid"` |
| GlassSurface / Button / Modal reuse | ✅ High | Tips=`Modal`; inspector/strip/desk/camera frame=`GlassSurface`; primary CTAs=`Button` |
| Custom modal scrim | ✅ | Tips + leave + list picker use `<Modal>` only |
| Sev ≥ 3 findings | **1** | Hex literal on tab badges |
---
## Maturity scoring (repo DS + this surface)
Scores 04. Evidence cites paths/counts.
| Axis | Score | Evidence |
| --- | --- | --- |
| **Tokens (T)** | **3** | Workstation surfaces consume `--glass-surface-*` via `GlassSurface`, `--text-*`, `--accent-ember` / `--accent-flame`, `--ember-rim-*`, `--border`. Residual: on-accent text still `#ffffff` in new strip badges (and pre-existing `Button.js` primary/danger). No `--text-on-accent` alias in `globals.css`. |
| **Components (C)** | **3** | Kit: GlassSurface, Modal, Button, Input, SearchBar, StatCard. New surfaces compose GlassSurface + Button + Modal. Gaps: no Switch / TabList primitive — Auto-detect + Foil switches and strip tabs are raw `<button>` (acceptable; pattern repeated ×3). |
| **Patterns (P)** | **3** | Locked workstation pattern largely shipped: `Layout chrome="default"` at md+, camera `variant="workstation"` with ember brackets + reduced-motion scan-line (`ScannerCamera.js` `DetectionFrame`), desk `GlassSurface mid`, inspector `tint="low"`, strip three tabs + batch banner, Tips `<Modal size="md">`. Minor drift: confidence fill uses `--color-warning` / `--color-success` vs directions flame/gold highlight row (aligned with UX “reuse ReviewCardItem” instead). |
| **Governance (G)** | **3** | Convoy `design_direction` locked 2026-08-15; `ui-and-theming.mdc` + vocab CI; `forbidden-hex-in-jsx` documented in `DESIGN_TOKENS.md`. Design-systems skill package still absent in-repo. |
| **Adoption (A)** | **3** | Desktop surface counts: `<Button>` ×12 across `scanner.js` + ResultPanel + HistoryStrip + Tips; `<GlassSurface>` on camera frame, desk bar, inspector, strip container, Auto-detect badge; `<Modal>` ×1 Tips + leave/list on page. Chips correctly **avoid** GlassSurface blur. Raw `<button>` ×7 mostly switches/tabs/Clear All (direction allows Clear All as text link). |
**Composite:** T3 / C3 / P3 / G3 / A3
**Top leverage:** **Tokens (T)** — introduce a shared on-accent text token (or route badge text through `<Button>` / a tiny Badge primitive) and purge `#ffffff` from convoy JSX so the hex gate and Liquid Glass lock stay green.
---
## Direction lock
Locked fields enforced: Liquid Glass; md+ workstation (camera + inspector + strip); ember accents; VOCAB CTAs; no hex; no per-chip `backdrop-filter`; Tips via `<Modal>`; no custom scrim; mobile immersive unchanged.
| Locked requirement | Diff status |
| --- | --- |
| md+ `chrome="default"`; mobile immersive | ✅ `pages/scanner.js` |
| Page title / subtitle + Scanner Tips | ✅ Header + `ScannerTips` |
| Camera framed panel + ember brackets | ✅ `variant="workstation"` + `DetectionFrame` L-brackets + scan-line + `prefers-reduced-motion` |
| Desk bar: Camera / Upload / Batch / Auto-detect | ✅ `GlassSurface tint="mid"` + native `<select>` + `Button`×2 + switch |
| Device picker fallbacks inline (not modal) | ✅ `camera.devicePickerMessage` under select |
| Inspector rail `w-[360px]` + GlassSurface low | ✅ `ScannerResultPanel` |
| Empty / confidence / VOCAB primary + Rescan / Add to List | ✅ |
| Strip: Recent / Queue / Dupes + Clear All + View All | ✅ `ScannerHistoryStrip` |
| Chips solid `--bg-secondary`; focused ember rim | ✅; no chip `backdrop-filter` |
| Batch progress + Cancel | ✅ `BatchProgressBanner` |
| Tips = `<Modal>` five tips (not popover) | ✅ `ScannerTips.js` |
| No hex / no Wishlist / Mark Owned | ❌ one `#ffffff`; vocab clean |
| Checkout sheet mobile-only | ✅ `md:hidden` mount |
---
## Token audit (3-tier)
| Tier | Expectation | Diff notes |
| --- | --- | --- |
| Primitives | Hex only in CSS token definitions | No new CSS hex from convoy. **JSX** `#ffffff` in strip badge violates consumer rule. |
| Aliases | `--glass-surface-*`, blur, rim, elevation, text, accent | Correctly used via GlassSurface props + `var(--*)` styles. Missing alias for on-accent / inverse text (primitives still hardcode `#ffffff`). |
| Components | GlassSurface / Button / Modal compose aliases | Desk, inspector, strip, Tips comply. Tab badge invents parallel on-accent styling instead of composing Button/Badge. |
---
## Component / adoption audit (scoped desktop surface)
| Element | Expected | Actual | Rate |
| --- | --- | --- | --- |
| Camera frame | `GlassSurface` low + ember rim | Yes (`ScannerCamera` workstation branch) | High |
| Desk control bar | `GlassSurface` mid | Yes (`pages/scanner.js`) | High |
| Inspector | `GlassSurface` low | Yes (`ScannerResultPanel`) | High |
| History strip shell | `GlassSurface` mid | Yes | High |
| Strip chips | Solid `--bg-secondary` (no blur) | Yes | High |
| Tips | `<Modal>` | Yes | High |
| Primary / secondary CTAs | `<Button>` | Yes (Upload, Batch, Add, Rescan, Cancel, View All) | High |
| Auto-detect / Foil | Switch pattern | Raw `<button role="switch">` ×2 | Medium — no Switch primitive |
| Strip tabs | tablist | Raw `<button role="tab">` | Medium — correct a11y; not Button |
| Clear All | Text link | Raw `<button>` ember text | Medium — matches direction “text link” |
| Leave / List | `<Modal>` | Yes (page) | High |
**Missing primitive (optional follow-up):** `Switch` (and optionally `Badge`) would remove duplicated toggle/`#ffffff` badge styling across desk + inspector + strip.
---
## Findings
Severity 04. Cap prioritized; sev ≥ 3 blocks merge recommendation for DS gate.
| ID | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| DS-1 | **3** | Hardcoded hex in convoy JSX | `ScannerHistoryStrip.js` tab badge `color: '#ffffff'` (≈L250) | Replace with theme token (add `--text-on-accent` / reuse Buttons on-accent once tokenized) so `forbidden-hex-in-jsx` stays green |
| DS-2 | **2** | Toggle knobs use Tailwind `bg-white` | `pages/scanner.js` Auto-detect knob; `ScannerResultPanel.js` Foil knob | Prefer `backgroundColor: 'var(--bg-primary)'` or a documented knob token — avoid raw white utility on themed surfaces |
| DS-3 | **1** | Confidence bar accent drift vs Design direction color table | `ScannerResultPanel` `confidenceRingColor`: mid `--color-warning`, high `--color-success` | Optional align to `--accent-flame` / `--accent-gold` for ≥90% match badge; or document UX override (ReviewCardItem reuse) as accepted |
| DS-4 | **1** | Clear All lacks destructive hover | Always `--accent-ember` | Direction: destructive on hover — add hover token mix if shipping polish pass |
| DS-5 | **1** | Systemic on-accent hex in primitive (context) | `components/ui/Button.js` primary/danger still `#ffffff` / `#dc2626` | Out of convoy scope but explains copy-paste; tokenize in a follow-up DS convoy |
### Passes (not findings)
- No `"Add to Collection"`, `"Save to Wishlist"`, `"Mark Owned"`, `"All My Cards"` in scoped new UI.
- Inspector + strip CTAs import `VOCAB.ADD_TO_MY_COLLECTION` / `VOCAB.ADD_TO_LIST`.
- Tips uses `<Modal size="md">` with convoy-authored five tips; no custom scrim.
- Strip chips: solid `--bg-secondary`, focused `--ember-rim-pronounced`; no per-chip blur.
- Camera detection chrome: ember/flame L-brackets + scan-line with `prefers-reduced-motion` static mid-line.
- Auto-detect badge: `motion-safe:animate-pulse` when ON.
- Confidence bar width transition gated with `@media (prefers-reduced-motion: reduce)`.
- Icons are SVG (lightbulb / empty-state camera), not emoji.
- Mobile checkout sheet remains `md:hidden`; workstation controls `hidden md:flex`.
---
## Governance notes
- Contribution path: `docs/DESIGN_TOKENS.md` + `.cursor/rules/ui-and-theming.mdc`.
- CI: `forbidden-hex-in-jsx` + stale-vocab gates — **DS-1 will fail the hex gate once these files are in CIs path**.
- Design-systems skill/templates not under `.cursor/skills/design-systems/` — G capped partly by missing shared audit tooling.
---
## Recommendation
**Treat DS as not merge-clean until DS-1 is fixed** (one-line / token swap on the strip badge). DS-2 is the next polish item for light/dark knob contrast. DS-3DS-5 are non-blocking documentation / follow-up token work.
**Child tasks (sev ≥ 3):**
1. Remove `#ffffff` from `ScannerHistoryStrip.js` badge text — use a CSS variable (prefer adding `--text-on-accent: #fff` in `globals.css` and referencing it from Button + badge).
---
## Hand-off
DS audit complete. Maturity: **T3/C3/P3/G3/A3**. Top leverage: invest in **Tokens** (on-accent alias + purge convoy hex). Sev ≥ 3 findings: **1**. Report: `.convoys/scanner-desktop-layout/audits/design-system-20260815.md`.

View file

@ -1,21 +0,0 @@
## Reviewer Report
| Check | Status | Notes |
| --- | --- | --- |
| Scope match | ✅ | Working-tree JS/tests match the union of briefs 16 `files:` (camera hook, Tips/Result/History components, batch helper, page + Camera + queue/identification hooks, matching tests). Convoy docs under `.convoys/scanner-desktop-layout/` expected. No out-of-list app code. **Note:** branch tip is behind `origin/main` (merge-base `0d52858`); a naive `git diff origin/main` includes unrelated dashboard-home-realignment reversals — rebase before PR. |
| Conventions | ✅ | `VOCAB` used for add actions; `<Modal>` / `GlassSurface` / `Button`; CSS variables (no hex / forbidden stale strings in scanner surfaces); relative imports; no new API routes or schema. |
| Security | ✅ | No new auth/API surfaces; client-only. Device id in `sessionStorage` only. Depth deferred to `role-security-auditor` in this fan-out. |
| Regression risk | medium | Touches `/scanner` composition, `useCameraScanner` stream constraints, and `verificationPausedRef` ownership (identification overwrite removed). Mobile chrome gated via `variant` + `isDesktop`, but pause/batch heuristics are easy to break. |
| Test coverage | ✅ | 45 tests green across briefs 16 artifacts (`use-camera-scanner`, batch helper, Tips/Result/History/Camera, page viewport split). Gaps: checkout sheet not exercised open on mobile; Identifying… never wired to real in-flight identify. |
| Documentation | ✅ | Convoy + briefs present; no AGENTS.md change required for this UI composition. |
### Findings
- 🟡 **Suggestion:** `isIdentifying={Boolean(identification.disambiguation)}` in `pages/scanner.js` does not match Brief 3s “Identifying…” intent — disambiguation is post-match choice, and the hook has no in-flight identify flag. Empty rail will not show Identifying… during real scans; it may flash the wrong copy if disambiguation opens with no focused card. Wire a real verifying signal or leave `false` until one exists.
- 🟡 **Suggestion:** Batch Scan does not switch the strip to **Scan Queue** when `batchProgress.active` (Design direction / strip UX). Progress banner only appears if the user is already on that tab.
- 🟡 **Suggestion:** Device picker messages use short status strings (`No camera found`, etc.) but Design directions locked table includes action hints (e.g. “Connect a webcam or use Upload Image”). Align copy with the Design table.
- 🟡 **Suggestion:** `test/pages/scanner.test.js` asserts the checkout sheet is absent when closed on mobile, but Brief 6 asks for no-regression coverage of the sheet at narrow width — open `isCheckoutOpen` (or equivalent) and assert the sheet mounts under `max-md`.
- 🟢 **Nice to have:** Rebase/merge `origin/main` before opening the PR so the diff does not look like a dashboard revert; implementation itself stays in-scope once compared to the brief file union.
### Approval recommendation
- approve

View file

@ -1,30 +0,0 @@
## Security Audit
| Check | Status | Notes |
| --- | --- | --- |
| Auth boundary | ✅ | No new API route or auth flow. `/scanner` keeps its `useAuth` redirect, and the unchanged scan endpoint verifies the Bearer token before its per-user scan rate limit. |
| Authorization (IDOR) | ✅ | The desktop list picker uses the existing collection-card API; its server-side POST path confirms authenticated owner/editor permission before mutation. Client-held `collectionId` is not trusted as authorization. |
| Input / injection | ⚠️ | File names and server error text are rendered as React text (no HTML sink), but new batch intake has no client-side file-size or count bound before decoding images into a canvas. |
| Secrets exposure | ✅ | Device IDs are stored only in tab-scoped `sessionStorage`; no new secret, `NEXT_PUBLIC_*` value, token, or credential was found in the scanner additions. |
| Dependencies | ⚠️ | No dependency manifest change is in the supplied diff, but `npm audit --omit=dev --json` reports five pre-existing production high-severity advisories (including `next`, `jws`, `nanoid`, `postcss`, and `sharp`). |
### Findings
1. **[Severity 4 — scope expansion]** `git diff origin/main -- . ':!.convoys/.metrics.jsonl'` contains 15 tracked paths outside Briefs 16, including dashboard/nav work, `components/Layout.js`, `pages/dashboard.js`, `pages/my-cards.js`, and `pages/api/user-cards.js`; it also deletes the separate `dashboard-home-realignment` convoy artifacts. This violates every brief's explicit file scope and prevents a trustworthy scanner-only review. **Fix:** rebase/cherry-pick the scanner implementation onto `origin/main` (or split the unrelated dashboard/API changes into their own convoy) and re-run the audit. Note that the supplied `git diff` does not show untracked scanner files, so this audit additionally inspected their working-tree contents.
2. **[Severity 1 — file intake hardening]** `pages/scanner.js:269-300` accepts an arbitrary number of arbitrary-sized `image/*` files, and `lib/use-scanner-identification.js:20-42` decodes each selected image at full natural dimensions into a canvas before the existing server-side 6 MB request cap can apply. A logged-in user can select a very large or decompression-bomb image set, freezing their tab and creating repeated scan attempts. `accept="image/*"` is only a picker hint, not validation. **Fix:** before `runSequentialGalleryIdentify`, enforce a maximum file count, MIME allowlist, and conservative byte limit compatible with the 6 MB base64 server limit; reject failures per file before calling `readFileToImageData`.
3. **[Severity 2 — inherited production dependency advisories]** `npm audit --omit=dev --json` reports five high-severity production vulnerabilities. The most material is the direct `next` range `<16.2.11`; the audit also identifies transitive `jws`, `nanoid`, `postcss`, and `sharp`. No package manifest changed in this convoy, so this is not introduced by the scanner work, but it remains a release risk. **Fix:** open or link the dependency-upgrade/accepted-risk work before release, and verify the deployed Next.js version against the advisories.
### Layer notes
- **Layer 1:** Existing scanner endpoints retain authentication and the scan rate-limit gate; this convoy adds no server action or API route.
- **Layer 2:** The list picker is UI convenience only. `pages/api/collections/[identifier]/cards.js:90-101` independently requires an authenticated owner/editor for POST, preventing a forged picker ID from becoming an IDOR.
- **Layer 3:** React escapes the batch-derived file basename (`pages/scanner.js:226-235`) and failure strings (`components/scanner/ScannerHistoryStrip.js:91-94`); no `dangerouslySetInnerHTML`, `eval`, or shell construction was found. The file-bound finding above remains.
- **Layer 4:** `lib/use-camera-scanner.js:37-90` stores only opaque camera device IDs in `sessionStorage`, using a fixed key and guarded reads/writes. No scanner secret is exposed to the client.
- **Layer 5:** See dependency finding 3. The full audit reports 16 issues including dev dependencies; the production-only audit reports five high findings.
- **Layer 6:** No middleware, Next config, CORS, cookie, or header change was included.
### Recommendation
**Do not merge as currently scoped.** Resolve the severity-4 scope expansion first. After isolating the scanner-only diff, add the file size/count/type guard (or document an accepted risk) and re-audit; the existing dependency findings should be tracked or risk-accepted separately because they were not introduced here.

View file

@ -1,90 +0,0 @@
---
convoy: scanner-desktop-layout
brief_number: 1
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/use-camera-scanner.js
- test/lib/use-camera-scanner.test.js
cross_brief_commitments:
- brief: 6
description: |
Exports `videoDevices`, `selectedDeviceId`, `setSelectedDeviceId`,
`devicePickerStatus` (`loading` | `ready` | `empty` | `denied` | `error`),
and `devicePickerMessage` for the desk Camera `<select>` in Brief 6.
Mobile callers keep using `facingMode` / `switchFacingMode` only — do not
mount the device picker below `md`.
---
# Brief 1: Webcam device picker hook
## Goal (1 sentence)
Extend `useCameraScanner` with `enumerateDevices` + `deviceId` stream selection for desktop webcams while preserving mobile facing-mode swap.
## Files in scope (do not edit anything else)
- `lib/use-camera-scanner.js`
- `test/lib/use-camera-scanner.test.js`
## Conventions to follow
- Tagged-template / relative imports only; no path aliases.
- No hex in JS — use CSS variables if any inline styles are needed in tests.
- `getUserMedia` constraints: when `selectedDeviceId` is set, use
`{ video: { deviceId: { exact: selectedDeviceId }, width: { ideal: 1280 }, height: { ideal: 720 } } }`;
when unset, keep existing `facingMode` constraint shape.
- Call `enumerateDevices` after the first successful stream (labels require an
active permission grant). Filter `kind === 'videoinput'`.
- Optional cheap persist: `sessionStorage` key `scanner:last-camera-device-id`
— read on init, write on `setSelectedDeviceId`.
- Restart stream on `selectedDeviceId` change (same pattern as `activeFacingMode`
effect). Guard with a ref to skip the initial mount double-start.
- `verificationPausedRef` contract unchanged — camera hook does not own pause logic.
- Mirror existing test style in `test/lib/scanner-card-detection.test.js` (vitest,
mocks for `navigator.mediaDevices`).
## Implementation shape (verified against current hook)
Current hook returns `facingMode`, `switchFacingMode` only — no `deviceId`.
`startCamera` uses `facingMode: activeFacingModeRef.current` exclusively.
Add:
```js
const [videoDevices, setVideoDevices] = useState([]);
const [selectedDeviceId, setSelectedDeviceId] = useState(() => {
try {
return sessionStorage.getItem('scanner:last-camera-device-id') || '';
} catch {
return '';
}
});
const [devicePickerStatus, setDevicePickerStatus] = useState('loading');
```
`refreshVideoDevices` async helper:
1. If `!navigator.mediaDevices?.enumerateDevices`, set status `error`.
2. Map videoinputs; if length === 0 → `empty`.
3. If `selectedDeviceId` not in list, fall back to first device or ''.
4. On `NotAllowedError` from prior getUserMedia → `denied`.
Export `setSelectedDeviceId` wrapper that persists to sessionStorage.
**Do not** remove `switchFacingMode` — mobile Brief 3 camera chrome still uses it.
## Acceptance criteria
- [ ] `enumerateDevices` populates `videoDevices` after stream start in tests (mocked)
- [ ] `selectedDeviceId` switches `getUserMedia` constraints to `deviceId: { exact }`
- [ ] `switchFacingMode` still toggles environment/user when no deviceId forced
- [ ] `devicePickerStatus` + message cover loading, empty, denied, error per Design direction table
- [ ] sessionStorage persist for last device id (read/write with try/catch)
- [ ] tests added in `test/lib/use-camera-scanner.test.js`
- [ ] no scope expansion (do not edit files outside `files:` above)
## Rationale (≤3 sentences)
Desktop workstations need a real webcam picker (C4), not a relabeled facing-mode swap. Isolating device enumeration in the hook keeps `ScannerCamera` and `pages/scanner.js` thin. Mobile behavior stays untouched because deviceId is only consumed at the page/camera wiring layer in Brief 6.

View file

@ -1,79 +0,0 @@
---
convoy: scanner-desktop-layout
brief_number: 2
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- components/scanner/ScannerTips.js
- test/components/ScannerTips.test.js
cross_brief_commitments:
- brief: 6
description: |
Brief 6 mounts `<ScannerTips />` in the desktop page header row (md+ only)
beside "Card Scanner" title. This brief ships the self-contained modal +
trigger button; no page wiring here.
---
# Brief 2: Scanner Tips modal
## Goal (1 sentence)
Ship a desktop Scanner Tips control that opens a glass `<Modal>` with five convoy-authored tips.
## Files in scope (do not edit anything else)
- `components/scanner/ScannerTips.js`
- `test/components/ScannerTips.test.js`
## Conventions to follow
- Use `Modal` + `Button` from `components/ui` — no custom scrim (`ui-and-theming.mdc`).
- Glass panel recipe: `--glass-surface-low` via `GlassSurface` inside modal body if needed.
- Copy locked (Design direction + IA):
| # | Tip |
| --- | --- |
| 1 | **Good lighting** — avoid glare on foil cards. |
| 2 | **Fill the frame** with one card; keep corners visible. |
| 3 | **Hold still** until Auto-detect locks the match. |
| 4 | **Use Batch Scan** for a pile of photos from your gallery. |
| 5 | **Switch camera** if the image is dark or mirrored. |
- Modal title: **Scanner Tips**. Trigger: ghost `Button` with lightbulb SVG + label **Scanner Tips** (visible text for 2.5.3 Label in Name).
- Dismiss: close control, Esc, scrim click — standard `<Modal>` behavior.
- No hex in `.js`; tokens only.
- Test pattern: `test/components/ScannerCheckoutSheet.test.js` (vitest + jsdom + RTL).
## Implementation shape
```js
export default function ScannerTips({ className = '' }) {
const [open, setOpen] = useState(false);
return (
<>
<Button variant="ghost" onClick={() => setOpen(true)} /* lightbulb + Scanner Tips */ />
<Modal open={open} onClose={() => setOpen(false)} title="Scanner Tips" size="md">
<ol className="space-y-4 text-sm" style={{ color: 'var(--text-secondary)' }}>
{/* five <li> with <strong> lead + em dash body */}
</ol>
</Modal>
</>
);
}
```
Component is self-contained — no props required v1.
## Acceptance criteria
- [ ] Trigger shows lightbulb icon + **Scanner Tips** visible label
- [ ] Modal lists exactly five tips with locked copy
- [ ] Uses `<Modal>` primitive (no `fixed inset-0 bg-black` shell)
- [ ] Opening/closing traps focus per existing Modal behavior
- [ ] tests: trigger opens modal, all five tips visible, close dismisses
- [ ] no scope expansion (do not edit files outside `files:` above)
## Rationale (≤3 sentences)
Tips are a standalone surface (C3) with no queue or camera dependencies. Shipping the modal in isolation lets Brief 6 wire it into the header without blocking other parallel work. Five tips exceed popover length — Modal is Design-locked.

View file

@ -1,78 +0,0 @@
---
convoy: scanner-desktop-layout
brief_number: 3
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- components/scanner/ScannerResultPanel.js
- test/components/ScannerResultPanel.test.js
cross_brief_commitments:
- brief: 6
description: |
Brief 6 passes `focusedCard`, `queue`, `isIdentifying`, handlers
(`onAddToOwned`, `onAddToList`, `onRescan`), and `commitError`. On
successful add, Brief 6 advances `focusedCardId` to the next unprocessed
entry and fires `ScannerToast` — this component calls `onAddToOwned` only.
---
# Brief 3: Live match inspector rail
## Goal (1 sentence)
Build the right-rail `ScannerResultPanel` that inspects the focused cart entry and commits one card via `VOCAB.ADD_TO_MY_COLLECTION`.
## Files in scope (do not edit anything else)
- `components/scanner/ScannerResultPanel.js`
- `test/components/ScannerResultPanel.test.js`
## Conventions to follow
- Import `VOCAB` from `lib/collection-vocabulary.js` — never ship forbidden strings
(`forbidden-stale-strings` CI).
- Primitives: `GlassSurface`, `Button` from `components/ui`.
- Duplicate minimally from `ReviewCardItem.js`: `CONDITION_OPTIONS`,
`confidencePercent`, `confidenceRingColor` (do not edit `ReviewCardItem` in this brief).
- Confidence captions (Design direction): ≥90% *Excellent match.* · ≥70% *Good match.*
· &lt;70% *Low confidence — verify before adding.*
- Inspector thumbnail + tiles: solid `--bg-secondary`**no `backdrop-filter`** on per-card elements.
- Empty state: centered muted scan/camera SVG illustration + single line:
*Point your camera at a card or upload an image to see a match.***no** Upload/Batch CTAs in rail.
- Populated: thumbnail, name (`text-lg font-semibold`), set · rarity · collector #,
condition `<select>` (`aria-label` includes card name), foil `role="switch"`,
confidence % + horizontal bar (300ms width transition; `@media (prefers-reduced-motion: reduce)` → instant).
- Actions: primary `VOCAB.ADD_TO_MY_COLLECTION` · ghost **Rescan** · ghost `VOCAB.ADD_TO_LIST`.
- Primary `Button` uses `loading` when `isAdding` (from `queue.addingCardIds.has(card.id)`).
- Commit errors: `role="alert"` div with `color-mix(in srgb, var(--color-error) …)` pattern from `pages/scanner.js`.
- `isIdentifying` state: show *Identifying…* placeholder when true and no focused card yet.
## Props shape (for Brief 6 wiring)
```js
export default function ScannerResultPanel({
focusedCard = null,
queue,
isIdentifying = false,
commitError = null,
onAddToOwned,
onAddToList,
onRescan,
}) {
```
`onUpdateMetadata` via `queue.updateCardMetadata(focusedCard.id, patch)` inline.
## Acceptance criteria
- [ ] Empty state: illustration + one line only (no duplicate Upload/Batch)
- [ ] Populated state shows metadata, condition select, foil switch, confidence bar
- [ ] Primary button label is `VOCAB.ADD_TO_MY_COLLECTION`
- [ ] Secondary row: Rescan + `VOCAB.ADD_TO_LIST` (no wishlist)
- [ ] `prefers-reduced-motion` disables confidence bar width animation
- [ ] tests: empty state, populated card, add button disabled while adding
- [ ] no scope expansion (do not edit files outside `files:` above)
## Rationale (≤3 sentences)
The inspector replaces the 360px cart side panel as the primary right-hand surface on desktop. Keeping it a dumb view lets Brief 6 own focus/advance/toast orchestration. Confidence and condition UX matches checkout patterns users already know from `ReviewCardItem`.

View file

@ -1,105 +0,0 @@
---
convoy: scanner-desktop-layout
brief_number: 4
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- components/scanner/ScannerHistoryStrip.js
- test/components/ScannerHistoryStrip.test.js
cross_brief_commitments:
- brief: 6
description: |
Brief 6 supplies `focusedCardId`, `onFocusCard`, `batchProgress`
(`{ active, current, total, onCancel }`), bulk commit handlers, and
`onClearAll`. Row click calls `onFocusCard(card.id)` — no auto-commit.
---
# Brief 4: History / queue / duplicates strip
## Goal (1 sentence)
Build the bottom `ScannerHistoryStrip` with Recent Scans, Scan Queue, and Duplicates tabs over the existing cart model.
## Files in scope (do not edit anything else)
- `components/scanner/ScannerHistoryStrip.js`
- `test/components/ScannerHistoryStrip.test.js`
## Conventions to follow
- `GlassSurface` tint `mid` for strip container; chips use solid `--bg-secondary` fill — **no per-chip `backdrop-filter`**.
- Tab labels: **Recent Scans**, **Scan Queue**, **Duplicates** with badge counts on Queue and Duplicates.
- `role="tablist"` / `role="tab"` / `role="tabpanel"`; badges in `aria-label`
(e.g. `Scan Queue, 3 unprocessed cards`).
- Empty tab copy (UX §5): Recent → *No scans yet this session.* · Queue →
*Scan queue is empty — matches appear here before you add them.* · Duplicates →
*No duplicates detected.*
- **Clear All** text link right-aligned; disabled while `batchProgress?.active`.
- **View All Scans** ghost button at strip bottom scrolls chip row to end (v1 — no expanded modal).
- Chip row: horizontal `overflow-x-auto`, min height 44px, ember rim on focused chip
(`focusedCardId === card.id` → `--ember-rim-pronounced`).
- Row click → `onFocusCard(card.id)` only — never auto-commit.
- Batch progress UI (when `batchProgress.active`): inline banner in Scan Queue tabpanel —
*Scanning {current} of {total}…* determinate bar, ghost **Cancel** calling `batchProgress.onCancel`.
- Failed identify rows: `identifyFailed` flag on card (Brief 6 sets) → ember left border +
**Identify failed** label + optional reason `text-sm`.
## Duplicate membership helper (export from this file)
```js
export function getDuplicateCards(scannedCards, ownershipMap) {
const sessionRepeatIds = new Set();
const seen = new Map(); // key: name+set → first id
for (const card of scannedCards) {
const key = `${card.name}::${card.set}`;
if (seen.has(key)) sessionRepeatIds.add(card.id);
else seen.set(key, card.id);
if ((card.quantity || 1) > 1) sessionRepeatIds.add(card.id);
}
return scannedCards.filter(
(card) =>
sessionRepeatIds.has(card.id) ||
(card.databaseId && ownershipMap[card.databaseId])
);
}
```
**Recent Scans** tab: all `scannedCards` session history (processed + unprocessed), newest first.
**Scan Queue** tab: `!card.processed` rows; badge = unprocessed count.
**Duplicates** tab: `getDuplicateCards(...)`; badge = duplicate set length.
## Props shape
```js
export default function ScannerHistoryStrip({
scannedCards,
ownershipMap,
focusedCardId,
onFocusCard,
activeTab,
onTabChange,
batchProgress = null,
onClearAll,
onCommitSelectedToOwned,
onOpenListPicker,
isProcessing = false,
}) {
```
Bulk commit buttons in Queue tab footer: `VOCAB.ADD_TO_MY_COLLECTION` + `VOCAB.ADD_TO_LIST` calling parent handlers (Brief 6 wires `queue.commitSelectedToOwned`).
## Acceptance criteria
- [ ] Three tabs with correct labels and badge counts
- [ ] `getDuplicateCards` covers ownershipMap + session name/set repeats + quantity>1
- [ ] Row click invokes `onFocusCard` without commit
- [ ] Batch progress banner with Cancel when `batchProgress.active`
- [ ] Clear All disabled during active batch
- [ ] Chip focus ring matches `focusedCardId`
- [ ] tests: tab switching, duplicate helper, empty states, batch banner
- [ ] no scope expansion (do not edit files outside `files:` above)
## Rationale (≤3 sentences)
The strip is the second commit surface (C5) alongside the inspector. Centralizing duplicate logic in one exported helper keeps `pages/scanner.js` thin. Tab + chip UX is independent of camera device picker, so this brief parallelizes cleanly.

View file

@ -1,92 +0,0 @@
---
convoy: scanner-desktop-layout
brief_number: 5
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/scanner-batch-identify.js
- test/lib/scanner-batch-identify.test.js
cross_brief_commitments:
- brief: 6
description: |
Brief 6 calls `runSequentialGalleryIdentify(files, identifyFn, options)`
from the Batch Scan multi-file picker. On per-file failure Brief 6 enqueues
a queue row with `identifyFailed: true` and `identifyError` message; cancel
via `cancelRef.current = true` keeps completed rows (IA-locked).
---
# Brief 5: Sequential batch identify helper
## Goal (1 sentence)
Add a small library helper that runs `identifyFromGalleryFile` sequentially over multiple files with progress, cancel, and per-file failure continuation.
## Files in scope (do not edit anything else)
- `lib/scanner-batch-identify.js`
- `test/lib/scanner-batch-identify.test.js`
## Conventions to follow
- **No new API routes** — caller passes `identifyFn` (Brief 6 binds
`identification.identifyFromGalleryFile`).
- **Sequential only** — one `await identifyFn(file)` at a time; no `Promise.all`.
- Respect identify rate limits implicitly (sequential pacing).
- Pure JS module — no React.
## Implementation shape
```js
/**
* @typedef {{ current: number, total: number, file: File }} BatchProgress
*/
export async function runSequentialGalleryIdentify(files, identifyFn, options = {}) {
const {
onProgress,
onFileSuccess,
onFileError,
cancelRef = { current: false },
} = options;
const list = Array.from(files || []);
const total = list.length;
const results = [];
for (let i = 0; i < list.length; i++) {
if (cancelRef.current) break;
const file = list[i];
onProgress?.({ current: i + 1, total, file });
try {
await identifyFn(file);
results.push({ file, ok: true });
onFileSuccess?.({ file, index: i });
} catch (error) {
results.push({ file, ok: false, error });
onFileError?.({ file, index: i, error });
// continue — IA forbids stopping the batch
}
}
return { results, cancelled: cancelRef.current };
}
```
`identifyFn` may not throw today — helper still catches for forward-compat.
If `identifyFromGalleryFile` only reports via console, Brief 6 may wrap it to
throw on hard failures.
## Acceptance criteria
- [ ] Processes files one-at-a-time in order
- [ ] `onProgress` fires before each file with `{ current, total, file }`
- [ ] `cancelRef.current = true` stops remaining files but returns partial `results`
- [ ] Per-file errors invoke `onFileError` and continue loop
- [ ] tests cover success path, mid-batch cancel, and error continuation
- [ ] no scope expansion (do not edit files outside `files:` above)
## Rationale (≤3 sentences)
Batch Scan (C1) needs orchestration without touching `use-scanner-identification.js` identify logic. A 60-line pure helper is testable and keeps the page brief focused on UI wiring. Sequential execution avoids Gemini rate-limit storms explicitly forbidden in scope.

View file

@ -1,171 +0,0 @@
---
convoy: scanner-desktop-layout
brief_number: 6
depends_on: [1, 2, 3, 4, 5]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- pages/scanner.js
- components/scanner/ScannerCamera.js
- lib/use-scanner-identification.js
- lib/use-scanner-queue.js
- test/pages/scanner.test.js
- test/components/ScannerCamera.test.js
cross_brief_commitments:
- brief: 1
description: |
Wires `camera.videoDevices`, `selectedDeviceId`, `setSelectedDeviceId`,
and `devicePickerStatus` into the desk Camera `<select>` (md+ only).
- brief: 2
description: |
Mounts `<ScannerTips />` in desktop page header (`hidden md:flex` row).
- brief: 3
description: |
Mounts `<ScannerResultPanel />` with focus/advance/toast orchestration
after `addSingleCardToOwned`.
- brief: 4
description: |
Mounts `<ScannerHistoryStrip />` with `focusedCardId`, batch progress,
and bulk commit handlers.
- brief: 5
description: |
Batch Scan multi-file input calls `runSequentialGalleryIdentify` with
`identification.identifyFromGalleryFile`.
---
# Brief 6: Desktop workstation page wiring
## Goal (1 sentence)
Wire `pages/scanner.js` into the md+ workstation grid (inspector + strip + desk controls) without regressing mobile immersive checkout.
## Files in scope (do not edit anything else)
- `pages/scanner.js`
- `components/scanner/ScannerCamera.js`
- `lib/use-scanner-identification.js`
- `lib/use-scanner-queue.js`
- `test/pages/scanner.test.js`
- `test/components/ScannerCamera.test.js`
## Conventions to follow
- **Layout chrome (boot finding):** `chrome="immersive"` already shows sidebar +
TopSearchBar at `md+` per `Layout.js` (`max-md:hidden` only on mobile nav).
Use explicit viewport split for semantics:
```js
const [isDesktop, setIsDesktop] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(min-width: 768px)');
const sync = () => setIsDesktop(mq.matches);
sync();
mq.addEventListener('change', sync);
return () => mq.removeEventListener('change', sync);
}, []);
// <Layout chrome={isDesktop ? 'default' : 'immersive'} />
```
Smallest change — no `Layout.js` edits.
- **Mobile zero regression:** keep `ScannerCheckoutSheet`, overlay camera chrome,
`ScannerCountPill`, scan peek — all `md:hidden` or `max-md:` as today.
- Remove desktop `ScannerReview` side panel (`hidden md:flex` block) — replaced by
inspector + strip.
- **Pause coordination (boot finding):** `useScannerIdentification` currently has:
```js
useEffect(() => {
if (verificationPausedRef) {
verificationPausedRef.current = Boolean(disambiguation);
}
}, [disambiguation, verificationPausedRef]);
```
**Delete this effect** — page owns composite pause:
```js
verificationPausedRef.current =
mobileCheckoutPauses ||
isListPickerOpen ||
isAutoDetectPaused ||
Boolean(identification.disambiguation);
```
Desktop Auto-detect toggle sets `isAutoDetectPaused` (OFF = paused).
- **Focused card state:** `focusedCardId` + derived `focusedCard`. On new scan,
auto-focus latest unprocessed (`scannedCards.find(c => !c.processed)`).
- **Inspector add success:** wrap `addSingleCardToOwned` — on success show
`ScannerToast` (*Added to My Collection*), advance to next unprocessed or empty.
Boot finding: current `addSingleCardToOwned` does not return boolean — check
`markCardAsProcessed` by awaiting and verifying card.processed or catch errors;
optionally extend queue method to `return true/false` in a follow-up if needed
inside this brief only if `use-scanner-queue.js` is NOT in scope — use try/catch
around API and inspect state after await.
**Note:** To return success reliably, add minimal change to
`lib/use-scanner-queue.js` `addSingleCardToOwned``return true` on success,
`return false` on error — **only if needed**; prefer not to expand files list.
Alternative: duplicate add call in page using `addScannedCardToOwned` from
`scanner-route-api.js`**rejected**; instead add `return true/false` to
`use-scanner-queue.js` is out of files — use `addingCardIds` clearing + no error
log as success heuristic OR add `use-scanner-queue.js` to this brief's files.
**Architect lock:** add `lib/use-scanner-queue.js` to this brief for
`addSingleCardToOwned` returning `boolean`.
- **List picker desktop:** `handleListPick` uses **focused card id** single selection
(`queue.addSingleCardToCollection(focusedCard, collectionId)`), not bulk selected set.
- **Leave modal:** keep existing; trigger on back + unprocessed queue.
- **Batch Scan:** hidden `<input type="file" multiple accept="image/*" />` on desk bar;
`runSequentialGalleryIdentify` with `cancelRef`; on `onFileError` enqueue synthetic
failed row via `queue.handleCardScanned` with `identifyFailed: true` OR dedicated
`enqueueFailedIdentify(file, error)` inline in page.
- **Rescan:** clear identify fields on focused card (`updateCardMetadata`), set
`processed: false`; if `scanImageUrl` present fetch blob and
`identifyFromGalleryFile`; else toast *Rescan from camera*.
- **ScannerCamera workstation:** add `variant="default" | "workstation"` (or `layoutMode`).
At `md+` with `variant="workstation"`:
- Container: framed panel not `min-h-[100dvh]` full-bleed — use `md:rounded-xl`,
`md:aspect-video`, `md:min-h-0`, outer `GlassSurface` + ember rim.
- Hide overlay top bar + bottom bar (`max-md:` keep current).
- Hide `ScannerScanPeek`, mobile LIVE badge, `ScannerCountPill` at `md+`.
- Show **Auto-detect badge** top-left in frame (ON/OFF from `!verificationPausedRef`
composite for auto-detect slice only — pass `autoDetectOn` prop).
- Badge dot pulse when ON; `prefers-reduced-motion: reduce` → static dot.
- Accept optional `deskControls` slot rendered **below** frame by page OR
`deskControlBar` prop — page renders Upload/Batch/Auto-detect/Camera select below.
- **Desk control bar (page, md+):** Camera `<select>` + Upload Image + Batch Scan +
Auto-detect switch per Design direction. Upload uses existing single-file input path.
- **Tests:** `test/pages/scanner.test.js` — mock hooks; assert mobile checkout sheet
present at narrow width; workstation elements (`ScannerResultPanel`, strip) at `md+`
(use `window.matchMedia` mock). `test/components/ScannerCamera.test.js` — overlay
chrome hidden at workstation variant.
- **Visual-diff:** `/scanner` is **not** in `tests/visual/` (only `home.png`) — no
baseline update required.
## `use-scanner-queue.js` changes
- `addSingleCardToOwned` returns `true` on success, `false` on error (after `try/catch`).
- `addSingleCardToCollection` same boolean return for desktop list picker.
## Acceptance criteria
- [ ] `md+` workstation grid: header + camera column + inspector + bottom strip
- [ ] `max-md` immersive checkout sheet unchanged (regression test)
- [ ] `chrome` default on desktop, immersive on mobile
- [ ] Device picker + Batch Scan + Auto-detect on desk bar only at `md+`
- [ ] `ScannerTips` in desktop header
- [ ] Inspector advance + toast on successful add
- [ ] Batch sequential with cancel + per-file failure flags
- [ ] `verificationPausedRef` composite pause without identification hook overwrite
- [ ] No forbidden copy strings
- [ ] tests added; no visual-diff baseline change
- [ ] no scope expansion beyond listed files
## Rationale (≤3 sentences)
Only one brief can own `pages/scanner.js` composition. Camera workstation variant and pause fix are tightly coupled to page wiring. Briefs 15 ship mountable units; this brief integrates them without blocking parallel implementers.

View file

@ -1,70 +0,0 @@
---
slug: scanner-disambiguation-render-test
status: shipping
opened: 2026-06-13
owner: rstillw
related:
- PR #144 (`31da384`) — the runtime `useFocusTrap is not defined` ReferenceError this test locks in against
- `enable-no-undef-eslint-rule` convoy — sibling that closes the same bug class at lint time
---
# scanner-disambiguation-render-test
## Problem
PR #144 shipped a runtime `ReferenceError: useFocusTrap is not defined` to production because `ScanDisambiguationDialog` called a hook it never imported. The sibling `enable-no-undef-eslint-rule` convoy closes that bug class at lint time. This convoy locks the regression at **render time** as well, so even if someone disables the lint rule (or a future rule-set change drops it), the same bug would still fail CI.
## Why vitest + jsdom instead of Playwright smoke
The task was originally queued as "scanner-disambiguation-smoke-test." Re-scoped because:
| Path | Catches PR #144 | Setup cost | Run time |
|---|---|---|---|
| Playwright smoke | ✓ if the disambiguation modal mounts during the smoke run | High — need auth bypass, a real way to enter the disambiguation state (multiple-candidate scan), and a stable fixture image | ~10s + browser overhead |
| Vitest + @testing-library/react | ✓ directly — the render-throw is caught by the test | Low — fixture is plain JS, props are explicit | <100ms |
A vitest render test catches the **exact** same bug class (`ReferenceError` during component render) at 1/100th the cost, and matches the existing `test/components/*.test.js` pattern (`Modal.test.js`, `ScannedCardItem.test.js`, etc.). A Playwright smoke test of the disambiguation flow could be added later as an integration-coverage layer but is **not** the right tool for regression-locking THIS specific bug class.
The deferred Playwright disambiguation smoke test is queued separately (see § Follow-ups).
## What ships
`test/components/ScanDisambiguationDialog.test.js` — 8 tests:
1. **`renders without crashing when given a disambiguation (PR #144 regression-lock)`** — the cheapest, most direct regression-lock. If `useFocusTrap` (or any other imported identifier) is missing, `render()` throws and this assertion fails. Comment in the test calls this out by name.
2. `returns null when disambiguation prop is falsy` — the early-return branch
3. `renders dialog with the correct ARIA shape``role="dialog"`, `aria-modal`, `aria-labelledby`
4. `renders one button per candidate with accessible labels`
5. `calls onPick with the selected candidate`
6. `renders the vision hint when one is provided` — exercises the optional `disambiguation.visionHint` branch
7. `disables the "send for review" button while submitting and shows in-flight copy`
8. `calls onCancel when the Cancel button is clicked`
## Verification
Mutation test executed locally: commented out the `useFocusTrap` import → all 8 tests fail with the same `ReferenceError` shape that hit prod in PR #144. Restored the import → all 8 pass. Full suite: 26 files / 131 tests pass (up from 25 / 123 pre-PR).
## Decisions
- **D1.** Cover the early-return branch explicitly even though it's a one-liner. Cost is negligible (~2 lines) and it locks the `if (!disambiguation) return null;` semantics — a future refactor that returns a placeholder instead would intentionally break this test and force a review of the contract change.
- **D2.** Use `fireEvent` for click handlers, not `userEvent`. `userEvent` is more realistic but adds a dependency (`@testing-library/user-event`) for marginal gain on these simple button-click assertions. Matches existing test pattern in `Modal.test.js`.
- **D3.** Do NOT mock `useFocusTrap`. The hook is real and runs against jsdom. Reasoning: the original PR #144 bug was that the hook was *missing*; mocking would mask exactly the kind of failure this test is designed to catch.
## Follow-ups (queued, not in this PR)
- `add-component-render-smoke-pattern` — sweep the other components that conditionally mount (`UploadImageModal`, `CollectionEditModal`, `CollectionDeleteModal`, `ShareModal`, `CollaboratorFacepile`'s expanded view, etc.) and add a minimal "renders without crashing with realistic props" test to each. Same regression-lock value, one PR per ~5 components.
- `scanner-disambiguation-playwright-smoke` — add a Playwright smoke test that exercises the full scan → ambiguous-match → pick-candidate flow against a deployed preview. Higher value as integration-layer coverage, but blocked on (a) a stable test fixture image that consistently produces multiple-candidate OCR matches and (b) auth bypass for the scanner route. Deferred.
## Acceptance
- [x] `test/components/ScanDisambiguationDialog.test.js` exists with the 8 listed assertions
- [x] Mutation test confirmed all 8 tests fail when the `useFocusTrap` import is removed
- [x] Full vitest suite passes (26 files / 131 tests)
- [ ] CI on the PR green
## Non-goals
- Adding `@testing-library/user-event` (use `fireEvent` to match existing pattern)
- Adding a Playwright smoke for this flow (queued separately)
- Adding render tests for other modal components (queued separately)
- Refactoring the component itself (it's already well-scoped post-PR #144 fix)

View file

@ -1,265 +0,0 @@
---
name: scanner-identify-upgrade
classification: feature
success_metric: |
Of legitimate card scans (excluding not_a_card), ≥50% auto-match a
catalog printing with no picker; Layer-1 escalate rate falls from 76.7%
to ≤40%; median /api/scan/identify latency stays near today's 1.7s p50
or improves. Measured on scan_attempts after Phase 1 ships.
skip: []
status: open
created: 2026-08-14
depends_on:
- add-real-ocr-layer
- server-side-scan-pipeline
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
---
# Scanner identify upgrade — epic
Umbrella for making the camera scanner **faster and more accurate**
without replacing the two-layer shape (cheap local path, then
server-owned Gemini). Planning-only in this file. Each numbered
sub-convoy is its own gated PR stream.
Worktree: `tcg-vault-worktrees/scanner-identify-upgrade` on
`convoy/scanner-identify-upgrade` (branched from `main` @ `c6c1364`).
Do **not** land these docs on `feat/scanner-mobile-checkout` — that
convoy owns scanner chrome/cart and explicitly leaves identify out of
scope.
## Why
`add-real-ocr-layer` (PR #38) targeted ≥70% of scans resolving at
Layer-1 (Tesseract + `pg_trgm`) with zero Gemini calls. Live
`scan_attempts` from the local Neon (2026-05-27 → 2026-08-12, n=224,
1 user) shows that target was missed by a wide margin:
| Signal | Value | Target / note |
| --- | --- | --- |
| L1 auto-match (`result_kind=matched`) | **5.8% of L1** / 2.7% of all | ≥70% L1 resolve |
| L1 escalate | **76.7% of L1** (79/103) | Should be the minority path |
| L1 disambiguation | 17.5% of L1 (18/103) | Name-only matcher cannot pick printings |
| End-to-end auto-match | **10.3%** (23/224) | User still picks or retries most cards |
| L2 `not_a_card` | **44.6% of L2** (54/121) | Detector fires on non-cards |
| L2 p50 / p90 latency | **1655 / 2349 ms** | Plus 3.5s hold-still *before* identify |
| L1 p50 latency | **158 ms** | Fast, but almost never uniquely matches |
| `card_submissions` | 27 pending, 0 reviewed | Catalog-gap queue is unread |
The six L1 "matches" include noisy Tesseract strips (`J ——`, `Rock
Jockey ©`) at confidence 3668. L1 is a cheap filter that rarely
identifies a printing. Meanwhile every card waits
`DETECTION_START_DELAY_MS` (1s) + `MIN_FIRST_SEEN_MS_FOR_VERIFY` (2.5s)
before OCR starts, and L2 is capped at 5 Gemini calls / user / minute.
Users feel this as: hold still forever, then pick from a list, or get
"not a card" / "saved for review."
## Scope
### In scope (this epic)
1. **`tighten-scan-identify-hot-path`** — Phase 1. Cut the hold-still
gate, OCR collector number on L1, structured Gemini JSON, optional
`SCAN_VISION_MODEL` bump, stop the automatic second Gemini refine.
2. **`improve-scan-card-detection`** — Phase 2. Replace the 320×240
Sobel brute-force detector with a real card crop + perspective warp.
This was queued in `add-real-ocr-layer` and never opened.
3. **`scan-visual-catalog-search`** — Phase 3. Embed catalog
`image_url`s; nearest-neighbor the warped crop. Gemini becomes
fallback. This is the accuracy leap.
### Out of scope (this epic)
- Scanner chrome, cart, checkout (`scanner-mobile-checkout` /
`scanner-rebuild`). Identify libs only.
- Training a custom card CNN or standing up a Python/CUDA OCR service.
- Swapping Tesseract for EasyOCR / PaddleOCR as the *primary* identifier.
- Raising the L2 rate limit until Phase 1 hit-rate is re-measured.
- Admin review of the 27 pending `card_submissions` (ops, not this epic).
## Baseline (do not re-query to "start" Phase 1)
Pulled 2026-08-14 from local `.env.local` → Neon `scan_attempts`.
```
layer | result_kind | n | pct
1 | escalate | 79 | 35.3
1 | disambiguation | 18 | 8.0
1 | matched | 6 | 2.7
2 | not_a_card | 54 | 24.1
2 | submitted | 26 | 11.6
2 | disambiguation | 22 | 9.8
2 | matched | 17 | 7.6
2 | needs_input | 2 | 0.9
```
L1 escalate text-length buckets (all had ≥3 chars — Tesseract is
emitting text that `pg_trgm` cannot match): 37 chars n=28; 819 n=30;
20+ n=21.
Re-measure with the same grouping after each sub-convoy ships.
## Dependency graph
```
[baseline pulled 2026-08-14]
┌───────────────────────────────────┐
│ 1. tighten-scan-identify-hot-path │
│ gates, collector #, schema JSON│
└───────────────┬───────────────────┘
┌───────────────────────────────────┐
│ 2. improve-scan-card-detection │
│ detect + homography crop │
└───────────────┬───────────────────┘
┌───────────────────────────────────┐
│ 3. scan-visual-catalog-search │
│ embeddings + pgvector kNN │
└───────────────────────────────────┘
```
**Strict-blockers:** #1 before #2 only if #2 would retune the same
stability constants — otherwise #1 (identify files) and #2 (detection
files) are file-disjoint and may run in parallel after Architect
confirms. #3 needs a stable crop (#2) to be worth the embedding job;
do not start #3 until #2 has a warped JPEG.
**Sibling:** `scanner-mobile-checkout` (other worktree / branch) must
not edit `lib/ocr-worker.js`, `lib/scan-vision.js`,
`lib/card-text-match.js`, `lib/scanner-card-identify.js`,
`lib/scanner-card-detection.js`, or `/api/scan/identify`.
## Roles invoked
Umbrella is planning-only. Each sub-convoy lists its own roles.
Default for #1: Architect → Implementer → audit fan-out (reviewer +
security-auditor). UX reviewer on #1 and #2. Skip IA and UI Designer
on all three (no new routes or visual language).
## Todos
- [x] Pull `scan_attempts` baseline (2026-08-14)
- [x] Open worktree `scanner-identify-upgrade` from `main`
- [x] Seed sub-convoys #1#3
- [x] Architect: pick up `tighten-scan-identify-hot-path` first
- [x] Re-measure `scan_attempts` after all phases ship (2026-08-15 — see below)
- [x] Architect: `improve-scan-card-detection`
- [x] Architect: `scan-visual-catalog-search` after warped crops exist
- [ ] Operator: finish embedding backfill (`npm run backfill-embeddings`) — **14.9%** done (9,865 / 66,211)
- [ ] Re-measure after backfill + L0 traffic (`layer = 0` rows)
## Post-ship telemetry (2026-08-15)
Pulled from Neon `scan_attempts` + `cards.embedding` coverage. Single user
(`user_id = 3`); treat post-ship windows as directional, not statistically
significant.
### All-time (n=250, 2026-05-27 → 2026-08-15)
| Signal | Value | Baseline (2026-08-14) | Target |
| --- | --- | --- | --- |
| L1 auto-match | **5.4%** of L1 (6/111) | 5.8% | ≥25% |
| L1 escalate | **78.4%** of L1 (87/111) | 76.7% | ≤40% |
| L2 `not_a_card` | **43.9%** of L2 (61/139) | 44.6% | ≤15% |
| End-to-end auto-match | **13.2%** excl. not_a_card (25/189) | 13.5% | ≥50% |
| L1 p50 latency | **158 ms** | 158 ms | — |
| L2 p50 / p90 | **1642 / 2297 ms** | 1655 / 2349 ms | ≤1800 p50 |
| L0 attempts | **0** | — | (Phase 3 code live; index sparse) |
| `card_submissions` pending | **31** | 27 | ops |
### Aug 15 post-deploy session (n=26)
First scan burst after #156#158 merged (~06:2307:43 UTC). No L0 rows yet
(#160 landed same evening; backfill incomplete).
| Signal | Aug 15 (n=26) | Baseline |
| --- | --- | --- |
| L1 auto-match | **0%** (0/8 L1) | 5.8% |
| L1 escalate | **100%** (8/8 L1) | 76.7% |
| L2 `not_a_card` | **38.9%** (7/18 L2) | 44.6% |
| End-to-end auto-match | **10.5%** excl. not_a_card (2/19) | 13.5% |
**Qualitative notes:** L1 OCR strips post-warp are *shorter* garbage (`"7"`,
`"Tey"`, `"yr'"`) — collector-number path cannot help. L2 structured JSON
(`isCard: false`) works. Two L2 auto-matches (Hunger Hawk, Hog-Monkey).
Disambiguation still common (Seel printings).
### Phase verdict
| Phase | PR | Code | Success metrics |
| --- | --- | --- | --- |
| 1 `tighten-scan-identify-hot-path` | #156 | **Shipped** | ❌ L1 match/escalate targets missed |
| 2 `improve-scan-card-detection` | #158 | **Shipped** | ❌ not_a_card still ~44%; slight Aug 15 improvement |
| 3 `scan-visual-catalog-search` | #160, #162 | **Shipped** | ⏳ Awaiting backfill + L0 traffic |
**Recommendation:** Finish catalog embedding backfill, scan 50+ cards on
preview, then re-query `layer = 0` distribution before tuning thresholds.
### Embedding backfill status (2026-08-15)
| Metric | Value |
| --- | --- |
| Cards with `image_url` | 66,211 |
| Rows with `embedding` | 9,865 (**14.9%**) |
| pgvector on Neon | ✅ v0.8.0 |
| Operator command | `npm run backfill-embeddings` |
## Worktree
| Checkout | Branch | Purpose |
| --- | --- | --- |
| `tcg-vault-worktrees/scanner-identify-upgrade` | `convoy/scanner-identify-upgrade` | These convoy docs |
| (later) Agents Window worktree per brief | `brief/scanner-identify-upgrade/<n>-<title>` | Implementer slices |
`scripts/wt.sh` is a deprecation stub. Create implementer worktrees from
the Agents Window after Architect writes `slice_dependencies:`.
## Multitask dispatch
No implementer fan-out from this umbrella. After #1 Architect marks
parallel-safe briefs (`depends_on: []` + disjoint `files:`):
```
/multitask role-implementer briefs <ids>
```
After each PR draft:
```
/multitask role-reviewer + role-security-auditor
```
Add design-system + a11y auditors only if the PR touches
`components/` or `pages/scanner.js` (not expected in #1).
Group ids: `audit-tighten-scan-identify-hot-path-<pr>`,
`audit-improve-scan-card-detection-<pr>`,
`audit-scan-visual-catalog-search-<pr>`.

View file

@ -1,644 +0,0 @@
---
name: scanner-mobile-checkout
classification: feature
success_metric: |
On a phone, opening /scanner goes straight into a full-bleed camera;
each successful identify lands in a local cart (not the database);
the user can open a checkout sheet, select cards, and commit them
to My Collection or a List without leaving the session.
skip: []
status: shipped
created: 2026-08-14
depends_on:
- scanner-rebuild
- redesign-scanner-flow
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
design_direction:
source: role-ui-designer
skill: ui-ux-pro-max
skill_version: "2.5.0"
version: 1
locked_at: 2026-08-14
product_type: mobile camera scanner checkout cart trading cards glassmorphism
pattern: Immersive camera overlay + bottom-sheet checkout
style: Liquid Glass (repo canonical)
stack: nextjs
layout_reference: image-93781104-69e3-4f0b-b25f-df7c750ef046.png
---
# Convoy: scanner-mobile-checkout
Turn the scanner into a mobile checkout: scan everything first, then
commit the cart. Layout reference is the last attached mock (full-bleed
camera, glass overlays, review as a bottom sheet).
## Why
The overnight `scanner-rebuild` got us a three-phase machine (Setup →
Scanning → Review), but the phone experience is still chrome-heavy:
Layout's sidebar, top search bar, and bottom nav compete with the
viewfinder; a destination form blocks the camera; and `handleCardScanned`
writes each match to the database immediately (rebuild D3). That fights
the actual table-scan job — hold the phone, sweep cards, then check out
once.
Users want supermarket-checkout semantics: open scanner → scan all the
things → open the cart → add selected cards to My Collection or a List
→ leave. No per-card "Add" tap. No setup screen in the way.
## Scope
### In scope
- **Immersive scan chrome (mobile).** Hide Layout sidebar, TopSearchBar,
and MobileNavigation while the camera is live. Camera is full-bleed.
Overlay only: back, title, optional gallery, detection brackets +
status around the tracked card, bottom glass bar (flash, scan status,
Review N).
- **Cart, not auto-route.** Successful identifies enqueue locally.
Nothing POSTs to `user_cards` / collections / decks until checkout.
Reverse rebuild D3 for this flow.
- **Scan peek.** After a match, a compact non-interactive preview slides
up briefly (name + thumbnail + confidence) and auto-dismisses. Tapping
it opens the cart. No condition / foil / Add buttons on the peek.
- **Checkout sheet.** Bottom sheet over the still-live (but paused)
camera. List of cart items with selection, qty, confidence, overflow
menu (remove / condition / foil). Primary: add selected to My
Collection. Secondary: add selected to a List (picker). Copy from
`lib/collection-vocabulary.js` — never "binder" as a button label
(mock says "Save to binder"; product vocab is List).
- **Camera swap.** Front / rear toggle in the bottom bar. Flash stays
rear-only and hides when `torch` is unsupported (iOS Safari).
- **Desktop.** Same cart semantics. At `md+`, cart can be a persistent
side panel instead of a sheet; do not hide the desktop sidebar.
### Out of scope
- Identify / OCR / Gemini pipeline (`lib/scanner-card-identify.js`,
`pages/api/scan/identify.js`, OpenCV detection). Layer UI on the
existing hooks.
- Schema / new tables. Cart is client-side session state.
- Deck Mode, game pre-filter, scan-history list on the setup screen
(rebuild Setup). Revisit as a later convoy if needed.
- Estimated total value row from the mock, unless identify payloads
already include `market_price` with no extra fetch.
- Deleting leftover `components/CameraScanner.js` /
`ScannerPageView.js` (pre-rebuild). Separate cleanup.
- Changing desktop visual-diff homepage baseline except as a
side-effect of Layout's new immersive prop.
## Roles invoked
1. `role-ia-architect` — scan → peek → cart → commit flow; where
destination choice lives now that Setup is gone.
2. `role-ui-designer` — lock the mobile overlay + checkout sheet against
the last mock, using Liquid Glass tokens (ui-ux-pro-max is installed).
3. `role-ux-reviewer` — cart selection, low-confidence handling, back
with unsaved cart, pause-vs-kill camera under the sheet.
4. `role-architect` — briefs. Likely: (1) Layout immersive + camera
chrome, (2) stop auto-route + cart model, (3) checkout sheet +
destinations. `slice_dependencies` must mark what can run in parallel.
5. `role-implementer` — per brief.
6. Audit fan-out: reviewer + security-auditor + design-system-auditor +
a11y-auditor.
## Todos
- [x] IA: route/flow/screen inventory for immersive scan + checkout sheet
- [x] UI Designer: lock overlay recipe (tokens, not hex) from last mock
- [x] UX: cart selection, low-confidence, back-guard, camera pause
- [x] Architect: briefs + whether Layout gets `chrome="immersive"`
- [ ] Stop auto-route in `use-scanner-queue.js`; scans stay `processed: false` until checkout
- [ ] Full-bleed camera; hide app chrome on small viewports
- [ ] Bottom bar: flash, status, Review N; add facingMode swap
- [ ] Non-interactive scan peek; checkout sheet with bulk + per-card commit
- [ ] Pause identification while the sheet is open (`verificationPausedRef`)
- [ ] Tests for cart enqueue (no network) and checkout commit paths
- [ ] Visual-diff: scanner surfaces + Layout immersive; refresh baselines if chrome changes
## Predecessor
`scanner-rebuild` (2026-06-13, overnight) shipped the phase machine,
flash hook, toasts, count pill, and review list. This convoy keeps those
hooks and **replaces the UX contract**: skip Setup, immersive camera,
cart-then-commit instead of auto-route.
## Conductor notes (build shape)
Likely file ownership for Architect to refine:
| Area | Files |
| --- | --- |
| Immersive shell | `pages/scanner.js`, `components/Layout.js`, `components/MobileNavigation.js` |
| Camera chrome | `components/scanner/ScannerCamera.js`, `lib/use-camera-scanner.js`, `lib/use-scanner-flash.js` |
| Cart model | `lib/use-scanner-queue.js`, `lib/scanner-session.js` |
| Peek + sheet | new `ScannerScanPeek.js`, rewrite `ScannerReview.js` as a sheet; reuse `ScannerDisambiguation.js` |
| Copy | `lib/collection-vocabulary.js` (`ADD_TO_MY_COLLECTION`, `ADD_TO_LIST`) |
Do not rewrite identification. `handleCardScanned` should enqueue only.
## Multitask dispatch
Planning is serial: IA → UI Designer → UX → Architect.
After architect: implementer fan-out only if briefs have `depends_on: []`
and disjoint `files:`. Cart-model (stop auto-route) likely blocks the
checkout sheet; immersive Layout may be parallel with cart-model if
they do not both own `pages/scanner.js`.
After PR draft: `/multitask` audit fan-out
`role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor`
(group id: `audit-scanner-mobile-checkout-<pr>`).
## IA
### Affected routes
- `[modified]` `/scanner` — sole scanner surface. Entry skips Setup and lands on full-bleed camera; cart and checkout live as in-page overlays/sheets on this route (no new URLs). Phase machine collapses from setup → scanning → review into scanning-first with optional cart sheet open/closed.
- `[impacted]` `/login` — unauthenticated `/scanner` visits still redirect here (unchanged gate in `pages/scanner.js`). Post-login return target is TBD (see open questions).
- `[impacted]` `/dashboard`, Layout sidebar, command palette — entry links to `/scanner` unchanged; users now arrive directly in camera instead of Setup.
- `[impacted]` `/my-cards`, `/collections` — not part of the scan flow, but natural post-checkout destinations if the user navigates away after commit. No route or nav changes required.
No `[new]` routes. Destination choice (My Collection vs List) moves from pre-scan Setup into the checkout sheet on `/scanner` only — **no route changes**.
### User flow
```mermaid
flowchart LR
A["Open /scanner"] --> B["Live camera"]
B --> C["Card identified"]
C --> D["Scan peek"]
D --> B
B --> E["Review N"]
E --> F["Checkout sheet"]
F --> G["Add to My Collection / List"]
G --> H["Exit scanner"]
```
### Screen inventory
| Screen | Path | New/modified | Notes |
| --- | --- | --- | --- |
| Scanner — live camera | `/scanner` | modified | Default view on open (mobile: immersive, no Layout chrome). Bottom glass bar: flash, status, camera swap, Review N. Detection brackets + status around tracked card. |
| Scan peek | `/scanner` (overlay) | new | Brief non-interactive slide-up after identify (name, thumbnail, confidence). Auto-dismiss; tap opens checkout sheet. No per-card Add / condition / foil on peek. |
| Checkout cart sheet | `/scanner` (overlay) | modified | Replaces full-page `ScannerReview` as primary cart UI on mobile. Camera stays mounted but paused under sheet. Selection, qty, confidence, overflow (remove / condition / foil). Primary: Add to My Collection; secondary: Add to List (picker). |
| Disambiguation sheet | `/scanner` (overlay) | impacted | Existing `ScannerDisambiguation` stays in overlay stack above camera; blocks identify until resolved, then card enqueues to cart. |
| Scanner Setup | `/scanner` (phase) | impacted | Removed from default entry path (out of scope to delete component). Game filter, deck mode, pre-scan destination, and scan history no longer gate camera start. |
| Auth loading | `/scanner` | impacted | Spinner while `useAuth` resolves; immersive chrome applies once authenticated (see open question on loading shell). |
Desktop (`md+`): same `/scanner` route; cart may render as persistent side panel instead of sheet; Layout sidebar remains visible.
### Content / data model deltas
- **Copy (write / wire):** checkout CTAs from `lib/collection-vocabulary.js``ADD_TO_MY_COLLECTION`, `ADD_TO_LIST` / `ADD_TO_LISTS`; bottom-bar "Review N" (N = cart count); flash and camera-swap `aria-label`s; empty-cart and commit-success toasts. Never use "binder" or "Save to binder" as a button label.
- **Copy (retire from entry path):** Setup destination picker, game pre-filter, deck-mode selector, and scan-history list as pre-scan gate copy (component may remain for later convoy).
- **Client session state:** cart is in-memory queue on `/scanner` — cards stay `processed: false` until checkout commit. Reverses rebuild D3 auto-route; no persistence across refresh or navigation away.
- **API calls (commit-time only, unchanged endpoints):** `POST /api/user-cards` (My Collection), `POST /api/collections/:id/cards` (List), existing `GET /api/collections` + `GET /api/decks` for destination lists. Optional `GET` batch-ownership during cart review. No schema / table changes.
- **Immersive Layout:** new chrome mode (likely `chrome="immersive"` on `Layout`) affects how `/scanner` composes global nav — content delta only on this page, not a new route.
### Open IA questions
1. **Post-checkout exit:** Product intent says "then exit" — does the user return to the prior page (`router.back()`), land on `/my-cards` or `/collections`, or stay on `/scanner` with an empty cart and live camera?
2. **Post-login deep link:** If an unauthenticated user hits `/scanner`, should login return them to `/scanner` (camera) or a safer default (`/dashboard`)?
3. **Desktop Setup:** Is Setup skipped on `md+` as well, or only on mobile viewports?
4. **Deck destination:** Deck mode and add-to-deck are out of scope — confirm deck is fully absent from checkout destinations (not just hidden behind Setup).
5. **Gallery import:** Convoy mentions "optional gallery" in the top overlay — is this in-scope as a file-picker overlay on `/scanner`, or deferred?
6. **Partial commit:** After adding a subset to My Collection / a List, does the user remain in the sheet with remaining items, or does any successful commit close the session?
## Decisions (post-IA)
Locked 2026-08-14 from product intent (parent session). UX / UI / Architect treat these as settled.
| # | Decision |
| --- | --- |
| D1 | **Stay on camera after commit.** Successful checkout removes committed rows from the cart. If the cart is empty, close the sheet and resume scanning. Back (header) is the only exit from `/scanner`. |
| D2 | **Login returns to `/scanner`.** Keep the existing auth gate; after login, send the user back to the camera, not `/dashboard`. |
| D3 | **Skip Setup on all viewports.** Desktop uses the same scanning-first entry; cart is a side panel at `md+`, not a return of the Setup form. |
| D4 | **No deck destination in checkout.** My Collection and List only. Deck Mode stays out of this convoy. |
| D5 | **Gallery is in-scope.** Top-right control opens a file picker; chosen image goes through the existing identify path and lands in the cart like a live scan. |
| D6 | **Partial commit stays in the sheet.** Uncommitted rows remain. Sheet closes only when the cart is empty or the user dismisses it. |
| D7 | **Persist cart in `sessionStorage`.** Survive refresh within the tab; do not persist across browser sessions. |
## Design direction
Locked v1 — 2026-08-14. Layout authority: last mock
(`image-93781104-69e3-4f0b-b25f-df7c750ef046.png`). Generator query:
*"mobile camera scanner checkout cart trading cards glassmorphism"*.
### Summary
Deck Hearth's mobile scanner should feel like a **supermarket checkout lane
over a live viewfinder**: full-bleed camera, ember-bracketed detection,
glass overlays that float above card art, a brief non-interactive peek after
each match, and a bottom-sheet cart for bulk commit. Warm ember/flame accents
cast light onto glass — never fill entire panels with brand orange.
### Pattern + style
| Field | Value |
| --- | --- |
| Product type | Mobile TCG collection scanner with session cart + checkout |
| App pattern | **Immersive camera overlay + bottom-sheet checkout** (generator's "Bento Grid Showcase" rejected — marketing grid, not camera UX) |
| UI style | **Liquid Glass** — repo canonical (`docs/DESIGN_TOKENS.md`). Generator's "Exaggerated Minimalism" / oversized typography rejected. |
| Stack notes | Next.js Pages router, React 18, Tailwind + CSS variables. Next.js stack query returned 0 rows — follow existing scanner components + `components/ui/` primitives. |
| Desktop (`md+`) | Same cart semantics; cart as persistent side panel; Layout sidebar stays visible. Immersive chrome is mobile-only. |
### Screen 1 — Live camera (mobile default)
Full-bleed `<video>` under all chrome. Layout sidebar, TopSearchBar, and
MobileNavigation hidden via `chrome="immersive"` on `/scanner`.
| Zone | Composition | Tokens / notes |
| --- | --- | --- |
| **Top bar** | Back (←), title "Scan Cards", gallery (file picker, D5) | `--glass-surface-mid` + `blur(--glass-blur-mid)` + rim stack. Safe-area inset top. 44×44 tap targets. |
| **Viewfinder brackets** | Four L-shaped corner brackets framing tracked card | Stroke `--accent-flame` / `--accent-ember`; no filled box. Animated scan line: horizontal ember gradient sweep (`--gradient-primary-*`), respect `prefers-reduced-motion`. |
| **Scan status** | Centered label below brackets ("Scanning… Hold steady") | `--text-primary` on semi-opaque pill or direct over darkened letterbox — ensure 4.5:1. |
| **Success toast** | Glass pill: check + "{Card name} added" + dismiss × | `--glass-surface-high` + rim; green check via success token (not emoji). Auto-dismiss ~2s. `role="status"`. |
| **Scan peek** | Compact slide-up after identify (name, thumb, confidence ring) | `--glass-surface-mid`; **non-interactive** except tap-to-open-sheet. No Add / condition / foil controls. Auto-dismiss ~3s. |
| **Bottom bar** | Three zones: Flash \| status \| **Review N** pill | Bar: `--glass-surface-mid` + blur mid + `--elevation-ambient`. Flash + camera-swap icons (rear-only torch; hide flash when unsupported). **Review N**: gradient pill `background: var(--gradient-primary-*)` + `--ember-rim-pronounced`; label `Review {N}` with chevron. Min 44px height. |
Camera overlays **may** use `backdrop-filter` (exception to per-card grid
rule — overlays sit above the viewfinder, not on grid items).
### Screen 2 — Checkout sheet (mobile)
Bottom sheet over **paused** (not unmounted) camera. Scrim + sheet follow
Modal primitive recipes.
| Zone | Composition | Tokens / notes |
| --- | --- | --- |
| **Scrim** | Dim live camera | `--modal-scrim` + `blur(--glass-blur-high)` |
| **Sheet panel** | Rounded top (~16px), grab handle, scrollable list, sticky footer | Panel: `--glass-surface-low` inside scrim + blur mid + `--elevation-pronounced`. Handle: 36×4px `--border` pill, centered. |
| **Header** | "{N} cards scanned" + low-confidence subline (orange dot) + Edit | Subline only when any row &lt; threshold. Edit toggles bulk-select mode (UX brief). |
| **Cart rows** | Checkbox, thumb, name/set/rarity, confidence ring, qty stepper ( N +), overflow ⋮ | Row surface: `--glass-surface-high` or solid `--bg-secondary` inner panel over sheet (legibility over busy thumbs). Confidence ring: green ≥85%, amber 7084%, orange &lt;70% using `--accent-ember` / success tokens — not hardcoded hex. |
| **Row overflow** | Remove, condition, foil | Popover: `--glass-surface-high` recipe. |
| **Footer CTAs** | Primary gradient + secondary text button | Primary: `VOCAB.ADD_TO_MY_COLLECTION` — full-width gradient button (`var(--gradient-primary-*)`, `--ember-rim-pronounced`). Secondary: `VOCAB.ADD_TO_LIST` — ghost / link style with list icon; opens picker. **Never** "Save to binder" / "Add all to collection" without vocab import. |
| **Out of scope row** | "Total value (est.)" from mock | Omit unless identify payload already includes `market_price` (convoy scope). |
Partial commit (D6): committed rows leave the list; sheet stays open until
cart empty or user dismisses. Empty cart → close sheet, resume scanning (D1).
### Colors
**Repo tokens win.** Generator palette (slate `#1E293B`, scan-blue `#2563EB`)
is **not adopted**.
| Role | Token | Usage in this flow |
| --- | --- | --- |
| Primary accent | `--accent-ember` | Brackets, focus rings, low-confidence warnings, primary CTA gradient stop |
| Secondary accent | `--accent-flame` | Scan line, gradient stops, Review N pill |
| Gold (rare) | `--accent-gold` | Rarity badges only |
| Glass fills | `--glass-surface-{low,mid,high}` | Sheet panel / top+bottom bars / toast / peek |
| Blur | `--glass-blur-{low,mid,high}`, `--glass-saturate` | All glass overlays |
| Rims | `--rim-light-{inner,outer}`, `--ember-rim-{subtle,pronounced}` | Interactive pills and primary buttons get pronounced ember rim |
| Scrim | `--modal-scrim` | Sheet backdrop |
| Text | `--text-primary`, `--text-secondary` | All copy on glass |
| Background | `--bg-primary`, `--bg-secondary` | Row inner panels when glass-over-art fails contrast |
No hex in `.js` files. Use `var(--token)` or existing utility classes
(`.glass-panel`, gradient vars in `globals.css`).
### Typography
| Role | Font | Notes |
| --- | --- | --- |
| Display / title | System / Inter (existing) | Top bar "Scan Cards" — `font-semibold`, ~17px. No generator "clamp(3rem…)" oversized type. |
| Body | System / Inter | Row names `font-medium`; set/rarity `text-sm` `--text-secondary` |
| CTA | System / Inter | Primary button `font-semibold`; secondary `font-medium` |
### Effects + motion
| Effect | Spec |
| --- | --- |
| Transitions | 150300ms ease on toast, peek slide, sheet open/close |
| Scan line | Looping horizontal sweep over brackets; disable loop when `prefers-reduced-motion: reduce` |
| Sheet | Slide up from bottom; scrim fade in. Focus trap while open. |
| Hover / focus | Primary controls: `--ember-rim-pronounced` on focus-visible; `cursor-pointer` on all clickables |
| Bracket pulse | Subtle ember glow on active track — `--ember-rim-subtle`, not full-panel fill |
See `docs/MOTION_SYSTEM.md` for shared motion taxonomy when implementing.
### Copy (mandatory)
Import from `lib/collection-vocabulary.js`:
| UI surface | Constant |
| --- | --- |
| Primary checkout | `VOCAB.ADD_TO_MY_COLLECTION` |
| Secondary checkout | `VOCAB.ADD_TO_LIST` |
| Bottom bar | `Review {N}` (N = cart count) |
| Toast | `{Card name} added` (success) |
Retired: "Save to binder", "Mark Owned", "Add all to collection" (use vocab
or "Add selected to My Collection" when subset selected).
### Anti-patterns (do not ship)
- Generator's light slate background + blue CTA palette on scanner chrome
- Opaque drawer replacing bottom sheet (see deprecated mock
`image-be4e787b-…` — too many actions on peek)
- Per-card Add / condition / foil on scan peek
- `backdrop-filter` on cart **thumbnail grid items** (card grid perf budget)
- Hardcoded hex in JSX; `--accent-blue` / `--accent-purple` legacy aliases
- Emoji icons (toast check must be SVG or existing glyph pattern)
- Filling overlay panels with solid ember orange (brand is rim/glow/gradient only)
- Desktop dual-theme mock layout (`image-6654f196-…`) as mobile reference
### Pre-delivery checklist
- [ ] No emojis as icons (SVG: Lucide / Heroicons)
- [ ] `cursor-pointer` on clickable elements
- [ ] Hover/focus transitions 150300ms
- [ ] Text contrast ≥ 4.5:1 on all glass copy (use inner opaque panel if over card art)
- [ ] Keyboard focus visible (`--accent-ember` ring)
- [ ] `prefers-reduced-motion` respected (scan line, peek, sheet)
- [ ] Responsive: 375 (immersive mobile), 768, 1024 (side panel), 1440
- [ ] Flash hidden when `torch` unsupported; camera-swap `aria-label`s
- [ ] Checkout copy from `collection-vocabulary.js` only
### Token overrides vs generator
| Generator output | Locked override |
| --- | --- |
| Pattern: Bento Grid Showcase | Immersive camera + bottom-sheet checkout |
| Style: Exaggerated Minimalism | Liquid Glass (shipped) |
| Primary `#1E293B`, accent `#2563EB` | `--accent-ember`, `--accent-flame`, `--gradient-primary-*` |
| Background `#F8FAFC` | Full-bleed camera; glass overlays use `--glass-surface-*` |
| Typography: massive display type | Existing body scale; scanner chrome stays compact |
| Key effects: 10vw headings | Scan-line animation + sheet slide only |
### Conflict rule
**Repo design tokens win** when they disagree with generator output. This
lock intentionally overrides ui-ux-pro-max palette/pattern for every
scanner surface. File a convoy note + bump `design_direction.version` only
if product approves a token change.
## UX
### Existing components to reuse
- `<GlassSurface>` (`components/ui/GlassSurface.js`) — top bar, bottom bar, scan peek, checkout sheet panel, row inner panels. Use `tint="mid"` for chrome bars, `tint="low"` for sheet body, `rim="ember-pronounced"` on Review N pill and primary CTA.
- `<Modal>` (`components/ui/Modal.js`) — back-guard confirm ("Leave scanner?"), List picker (`size="md"`), nested above checkout sheet z-index stack. Do **not** roll a custom scrim for these dialogs.
- `<Button>` (`components/ui/Button.js`) — footer primary (`variant="primary"`, `loading` during commit) and secondary (`variant="ghost"`) CTAs; back-guard actions.
- `<Input>` (`components/ui/Input.js`) — optional filter field inside List picker when `collections.length > 8`.
- `<ScannerDisambiguation>` (`components/scanner/ScannerDisambiguation.js`) — **canonical bottom-sheet shell** for checkout cart on mobile: same `fixed inset-0` scrim, `glass-panel-strong` panel, drag handle, `useFocusTrap`, Escape-to-dismiss. Checkout sheet copies this shell; do not invent a second sheet primitive.
- `<ScannerToast>` (`components/scanner/ScannerToast.js`) — success feedback after identify (`role="status"`, `aria-live="polite"`). Replace emoji glyphs with SVG check per design-direction checklist; message shape: `{Card name} added`.
- `<ScannerCountPill>` (`components/scanner/ScannerCountPill.js`) — evolve into bottom-bar **Review N** gradient pill (same `onReview` contract, `aria-label={`Review ${count} scanned cards`}`); retire the separate count row above the viewport.
- `<ReviewCardItem>` (`components/scanner/ReviewCardItem.js`) — cart row body: thumbnail, name/set, qty stepper, condition/foil overflow. Extend with leading checkbox + confidence ring; drop inline destination picker (destinations move to footer CTAs only).
- `<ScannerCamera>` (`components/scanner/ScannerCamera.js`) — viewfinder, bracket overlays, flash hook wiring, disambiguation mount point. Refactor chrome into top/bottom glass bars; keep `verificationPausedRef` plumbing.
- `<Layout>` (`components/Layout.js`) — `chrome="immersive"` on `/scanner` for `<md` only; desktop keeps sidebar.
- `useFocusTrap` / `useFocusTrapContainer` (`lib/use-focus-trap.js`) — every overlay sheet and confirm dialog; restore focus to Review N pill or back button on close.
**8 primitives** (+ 2 hooks, 1 page shell). New file `ScannerScanPeek.js` is the only net-new overlay component; it composes `<GlassSurface>` only.
### Design direction alignment
Locked `design_direction` maps directly to repo tokens — no conflicts deferred:
| Design-direction surface | Token / primitive |
| --- | --- |
| Top + bottom chrome bars | `--glass-surface-mid`, `--glass-blur-mid`, `--rim-light-*`, safe-area insets |
| Ember brackets + scan line | `--accent-flame`, `--accent-ember`, `--gradient-primary-*`; scan-line loop disabled under `prefers-reduced-motion` |
| Review N pill | `var(--gradient-primary-*)` + `--ember-rim-pronounced` on `<Button>` or styled `<GlassSurface as="button">` |
| Checkout sheet scrim | `--modal-scrim` + `blur(--glass-blur-high)` — same recipe as `<Modal>` backdrop |
| Confidence rings | green ≥85%, amber 7084%, orange &lt;70% via `--color-success` / `--accent-ember` tokens (not hex literals) |
| Footer CTAs | `VOCAB.ADD_TO_MY_COLLECTION`, `VOCAB.ADD_TO_LIST` from `lib/collection-vocabulary.js` |
| Peek + toast | `--glass-surface-mid` / `--glass-surface-high`; SVG icons only |
**UX override of design-direction "Edit toggles bulk-select mode":** reject Edit mode. Checkboxes are **always visible** with all rows selected by default — supermarket checkout does not hide selection behind a mode switch (Nielsen H6).
### Existing patterns to follow
- **Bottom-sheet overlay stack**`ScannerDisambiguation.js` (scrim click dismisses only when safe; focus trap; `aria-labelledby`). Checkout sheet sits at `z-40`; disambiguation stays at `z-50` above it.
- **Cart row editing**`ReviewCardItem.js` qty stepper + condition select pattern; overflow ⋮ menu mirrors row remove button placement.
- **Bulk selection semantics**`useScannerQueue` `selectedCards` Set + `toggleCardSelection` / `handleBulkAction`; do not fork a parallel selection model.
- **Pause identification**`CameraScanner.js` + `pages/scanner.js` `verificationPausedRef` pattern: `useCameraScanner` early-returns when `verificationPausedRef.current === true`; `useScannerIdentification` already sets it during disambiguation — extend to checkout sheet + List picker open.
- **Flash gating**`useScannerFlash.js` `flashSupported` probe; conditionally render flash button (already in `ScannerCamera.js` L221242). Flash is **rear camera only** — hide when `facingMode === 'user'`.
- **Immersive page shell** — follow `components/ProtectedRoute.js` / `pages/scanner.js` auth gate; post-login return to `/scanner` (D2) via `router.query.returnUrl` or equivalent login redirect — do not invent a new auth wrapper.
- **List display names**`collectionDisplayName()` for picker rows; never render `'All My Cards'` literal.
- **Motion**`docs/MOTION_SYSTEM.md` duration tokens: peek/toast `quick` (150ms), sheet `slow` (400ms); sitewide `prefers-reduced-motion` collapse applies.
- **Rules**`.cursor/rules/ui-and-theming.mdc` (Modal primitive mandate, 44px targets, focus rings via `--accent-ember`).
### A11y constraints
Hand to `role-a11y-auditor`:
1. **WCAG 2.5.5 Target Size (AAA goal):** every tappable chrome control (back, gallery, flash, camera swap, Review N, peek tap target, row checkbox, qty ±, overflow ⋮, footer CTAs) ≥ 44×44 CSS px — use invisible padding if icon is smaller.
2. **WCAG 1.4.3 Contrast (AA):** all text on glass ≥ 4.5:1; if card-art bleeds through a row thumb, use `--bg-secondary` inner panel behind text (design-direction row spec).
3. **WCAG 2.4.3 Focus Order:** checkout sheet opens → focus moves to sheet panel (first focusable: header close/dismiss or first row checkbox via `useFocusTrap`); on close → restore to Review N pill.
4. **WCAG 2.1.1 Keyboard:** sheet dismiss via Escape; peek opens sheet on Enter/Space when focused; back button triggers confirm modal before navigation.
5. **WCAG 4.1.2 Name, Role, Value:** checkout sheet `role="dialog"` `aria-modal="true"` `aria-labelledby` → "{N} cards scanned"; List picker nested dialog gets its own label ("Choose a List").
6. **`aria-label` required:** back → "Leave scanner"; gallery → "Choose image from gallery"; flash → "Turn on flash" / "Turn off flash" + `aria-pressed`; camera swap → "Switch to front camera" / "Switch to rear camera"; Review N → `Review ${N} scanned cards`; row checkbox → `Select ${card.name}`; qty buttons → `Decrease quantity for ${card.name}` / `Increase…`; overflow → `More actions for ${card.name}`.
7. **WCAG 1.1.1 Non-text Content:** card thumbs `alt={card.name}`; decorative bracket corners `aria-hidden="true"`.
8. **WCAG 4.1.3 Status Messages:** identify success toast + post-commit success toast use `role="status"` `aria-live="polite"`; do not steal focus.
9. **WCAG 2.3.3 Animation from Interactions:** scan-line loop, peek slide, sheet slide respect `prefers-reduced-motion: reduce` (instant show/hide, no sweep).
10. **Focus trap:** checkout sheet, disambiguation sheet, back-guard confirm, List picker — all trap Tab; disambiguation above checkout maintains trap when both mounted.
11. **Confidence communicated without color alone:** ring color + text label on row (`aria-label` includes "confidence 62 percent") and header subline "{N} need review" when any row &lt; 70%.
12. **Gallery file input:** visually hidden `<input type="file" accept="image/*">` triggered by labeled button; `aria-label` on button, not bare input.
13. **Disabled primary CTA:** when zero rows selected, `Add to My Collection` is `disabled` + `aria-disabled="true"` with visible helper "Select at least one card" (linked via `aria-describedby`).
14. **Screen reader pause announcement:** when sheet opens, `aria-hidden="true"` on camera video element OR `inert` on viewfinder chrome beneath scrim so SR does not read live camera controls under the sheet.
### Interaction patterns
| Pattern | Spec | Nielsen |
| --- | --- | --- |
| **Cart selection — default all selected** *(required)* | On first sheet open, `selectedCards` = all unprocessed row IDs. New card enqueued while sheet open → auto-add its ID to selection. Deselecting is per-row checkbox. Primary CTA label stays `VOCAB.ADD_TO_MY_COLLECTION`; when subset selected, append count: "Add to My Collection (3)". No "Edit mode" toggle. | H6 Recognition rather than recall |
| **Low-confidence vs disambiguation** *(required)* | **Pipeline `disambiguation` outcome → always block** via existing `ScannerDisambiguation` sheet; never enqueue until user picks. **Single-match `emit` outcome → enqueue silently** to cart regardless of confidence; surface confidence visually (ring + header subline for any row &lt;70%). Do not re-prompt at scan time for low-confidence singles — checkout is the verification gate. | H5 Error prevention; H3 User control |
| **Scan peek** *(required)* | Slide-up 3s auto-dismiss. Container `pointer-events-none`; single child button wrapper `pointer-events-auto` with `aria-label="Open cart to review {card name}"`. No condition/foil/Add on peek. Tap opens checkout sheet. | H4 Consistency; H8 Minimalist design |
| **Checkout sheet open/close** *(required)* | Open via Review N pill or peek tap. Close via scrim tap, swipe-down on handle (nice-to-have), or header ×. On close with items remaining: resume camera (`verificationPausedRef = false`). Empty cart after commit → auto-close sheet (D1). | H1 Visibility of system status |
| **Pause identification under sheet** *(required)* | `verificationPausedRef.current = true` when checkout sheet **or** List picker **or** disambiguation is open. Camera stream stays mounted; detection/verify loop does not run. Resume on sheet close. | H1 Visibility of system status |
| **Back with unsaved cart** *(required)* | Header back with any unprocessed cart rows → `<Modal>` confirm: title "Leave scanner?", body "{N} scanned cards haven't been added yet.", primary "Keep scanning" (dismiss), secondary "Leave" (`router.back()` or `/dashboard`). No confirm when cart empty. | H3 User control and freedom |
| **Partial commit** *(required)* | Commit removes only selected rows from cart; sheet stays open with remainder (D6). Success toast `role="status"`. Primary button shows loading via `<Button loading>`. | H9 Error recovery; H1 Feedback |
| **sessionStorage cart persist** *(required)* | Serialize `scannedCards` + `selectedCards` to `sessionStorage` on change (D7); hydrate on `/scanner` mount. Tab refresh preserves cart; new tab starts empty. Clear on explicit "Leave" confirm. | H6 Recognition rather than recall |
| **Flash + camera swap** *(required)* | Flash button rendered only when `flashSupported && facingMode === 'environment'`. Camera-swap toggles `facingMode`; turning to front auto-disables torch. Swap button always visible when streaming. | H2 Match between system and real world |
| **Gallery import** *(required)* | Top-right gallery button → hidden file input; selected image runs existing identify path; on success same peek + cart enqueue as live scan. Show spinner on gallery button while identifying (`aria-busy`). | H4 Consistency |
| **Empty states** *(required)* | Cart sheet with 0 rows: should not open (Review N hidden at N=0). If last item removed in sheet → auto-close + resume camera. | H9 Error recovery |
| **Loading states** *(required)* | Camera starting: existing spinner + "Starting camera…". Commit in flight: footer CTA `loading`, row checkboxes disabled. List picker: skeleton or spinner while `collections` fetch. | H1 Visibility of system status |
| **Error states** *(required)* | Commit API failure: inline banner in sheet footer (not alert()), rows stay selected, retry enabled. Identify error: existing toast/notice path; do not enqueue. | H9 Error recovery |
| **Desktop side panel** *(required)* | `md+`: cart is persistent right panel (not bottom sheet); no immersive Layout chrome; same selection + CTA semantics; no `verificationPausedRef` pause required (user can see camera + panel simultaneously) — **optional:** still pause on mobile only. | H4 Consistency |
| **Optimistic commit** *(nice-to-have)* | Defer — show loading on CTA until API confirms; no optimistic removal (batch failures are painful). | H9 |
### Anti-patterns to avoid
- **Separate "Edit" mode hiding checkboxes** — violates supermarket-checkout mental model (H6).
- **Silent enqueue on `disambiguation` / `needsUserSelection`** — must block on picker (H5).
- **Per-card Add / condition / foil on scan peek** — design-direction anti-pattern; peek is read-only + tap-to-cart (H8).
- **Unmounting camera when sheet opens** — causes slow re-start and flash re-permission; pause via `verificationPausedRef` only (H1).
- **Auto-routing to DB on identify** — rebuild D3 reversed; enqueue only (H3).
- **"binder" / "Save to binder" / "Mark Owned" button copy** — use `VOCAB` constants; CI forbidden-strings (H4).
- **Hardcoded hex / `rgba(0,0,0,0.55)` in new chrome** — use glass tokens; migrate existing `ScannerCamera` literals in same brief (H4).
- **Emoji toast glyphs** — SVG only per design-direction checklist (H4).
- **`backdrop-filter` on cart thumbnail elements** — GPU budget; glass on row container only (H8).
- **Deck destination in checkout** — D4; no deck picker (H4).
- **Navigating away on commit** — D1 stay on camera (H3).
- **Confirm dialog via `window.confirm`** — use `<Modal>` for back-guard; matches focus-trap contract (H7 Flexibility).
- **Interactive peek controls stealing taps from viewfinder** — peek must not block scanning except its single open-cart hit target (H8).
- **localStorage for cart** — D7 specifies `sessionStorage`; do not use existing `SCANNER_SESSION_STORAGE_KEY` localStorage for cart rows (H6).
### Mobile / responsive notes
- **&lt;768px (mobile):** `Layout` `chrome="immersive"` — hide sidebar, `TopSearchBar`, `MobileNavigation`. Camera `100dvh` full-bleed (`min-h-0` flex child). Top bar respects `env(safe-area-inset-top)`; bottom bar `env(safe-area-inset-bottom)`. Checkout = bottom sheet (max-height ~85dvh, scrollable list, sticky footer). Peek sits above bottom bar, below brackets.
- **≥768px (tablet/desktop):** Layout sidebar visible. Camera in main column; cart as **persistent right panel** (~360px) not sheet. Review N pill still opens/focuses panel if collapsed. No immersive chrome.
- **375px floor:** verify footer CTAs stack (primary full-width, secondary below) without clipping qty steppers; row thumb stays 40×56.
- **Landscape phone:** sheet max-height may need `max-h-[70dvh]` so viewfinder remains partially visible — architect to decide in brief; UX prefers partial camera peek above sheet scrim (H1).
- **Touch:** swipe-down on sheet handle to dismiss (nice-to-have); scrim tap dismisses (required). 8px minimum gap between adjacent 44px targets in bottom bar.
## Architecture
Locked 2026-08-14. Layout gets `chrome="immersive"` (Brief 1). Four implementer briefs; no new API routes or schema changes.
### File plan
| File | Action | Purpose |
| --- | --- | --- |
| `components/Layout.js` | modified | `chrome="immersive"` prop — hide `MobileNavigation` + `TopSearchBar` below `md` |
| `test/components/Layout.test.js` | modified | Regression-lock immersive chrome |
| `lib/use-scanner-queue.js` | modified | Enqueue-only `handleCardScanned`; checkout commit helpers; remove auto-route |
| `lib/scanner-session.js` | modified | `SCANNER_CART_STORAGE_KEY` + load/save/clear cart in `sessionStorage` (D7) |
| `test/lib/scanner-session.test.js` | modified | Cart persistence helpers |
| `test/lib/use-scanner-queue.test.js` | new | No-network enqueue + bulk commit mocks |
| `lib/use-camera-scanner.js` | modified | `facingMode` state + `switchFacingMode` (restart stream) |
| `lib/use-scanner-identification.js` | modified | `identifyFromGalleryFile` adapter only (calls existing `tryLayer1TextIdentify`) |
| `components/scanner/ScannerCamera.js` | modified | Full-bleed glass top/bottom bars; flash/swap/gallery; peek mount point |
| `components/scanner/ScannerCountPill.js` | modified | Gradient Review {N} pill |
| `components/scanner/ScannerScanPeek.js` | new | Non-interactive 3s peek; tap opens checkout |
| `components/scanner/ScannerToast.js` | modified | SVG icons (no Unicode glyphs) |
| `components/scanner/ScannerCheckoutSheet.js` | new | Mobile bottom-sheet cart (shell from `ScannerDisambiguation`) |
| `components/scanner/ScannerReview.js` | modified | Desktop `md+` persistent side panel variant |
| `components/scanner/ReviewCardItem.js` | modified | Checkbox + confidence ring; drop row destination picker |
| `pages/scanner.js` | modified | Scanning-first orchestration; sheet state; pause ref; immersive Layout |
| `pages/login.js` | modified | Honor `returnUrl` query after login (D2) |
| `test/components/ScannerCheckoutSheet.test.js` | new | Sheet a11y + vocab + selection-disabled CTA |
**Not touched:** `pages/api/**`, `migrations/**`, `lib/scanner-card-identify.js` (identify pipeline), `components/scanner/ScannerDisambiguation.js` (reuse pattern only), `components/scanner/ScannerSetup.js` (dead path, not deleted).
### API surface
No new or modified API routes. Commit-time calls (unchanged):
| Method | Path | Request | Response | Auth | Rate limit |
| --- | --- | --- | --- | --- | --- |
| POST | `/api/user-cards` | `buildScannerCardPayload(card)` | 2xx / error JSON | Bearer JWT | none (existing) |
| POST | `/api/collections/:id/cards` | `buildScannerCardPayload(card)` | 2xx / error JSON | Bearer JWT | none |
| GET | `/api/collections` | — | collection[] | Bearer JWT | none |
| POST | `/api/cards/batch-ownership` | `{ cardIds: number[] }` | `{ ownership }` | Bearer JWT | none |
Helpers live in `lib/scanner-route-api.js` (`addScannedCardToOwned`, `addScannedCardToCollection`, `fetchScannerCollections`, `fetchBatchOwnership`).
### Schema diff
None. Cart is client-side `sessionStorage` only (`SCANNER_CART_STORAGE_KEY`).
### Test plan
| Area | File | What to assert |
| --- | --- | --- |
| Cart merge defaults | `test/lib/scanner-session.test.js` (existing) | `processed: false` on new entries — keep green |
| Cart persistence | `test/lib/scanner-session.test.js` | `saveScannerCart` / `loadScannerCart` round-trip |
| No auto-route | `test/lib/use-scanner-queue.test.js` (new) | `handleCardScanned` never calls `routeScannedCardToDestination`; mock `fetch` |
| Bulk commit | `test/lib/use-scanner-queue.test.js` | `handleBulkAction('owned')` invokes `addScannedCardToOwned` per selected id |
| Layout immersive | `test/components/Layout.test.js` | Mobile nav hidden when `chrome="immersive"` |
| Checkout sheet | `test/components/ScannerCheckoutSheet.test.js` | Dialog labels, `VOCAB` CTAs, disabled primary when selection empty |
| Auth/Layout regressions | `test/lib/permission-middleware.test.js`, `test/components/Layout.test.js` | Do not weaken existing assertions |
Smoke (manual / post-merge): authenticated `/scanner` on 375px viewport — camera full-bleed, Review N opens sheet, commit to My Collection. Visual-diff may need baseline refresh if Layout chrome changes bleed to homepage (unlikely — scanner-only prop).
### Risk list
| Risk | Mitigation |
| --- | --- |
| `pages/scanner.js` was shared by all three conductor guesses | Serialized: only Brief 4 touches `scanner.js` |
| Gallery has no existing file-picker hook (boot finding) | Brief 3 adds `identifyFromGalleryFile` calling `tryLayer1TextIdentify` — no API change |
| `facingMode` swap requires stream restart | Brief 3 `stopCamera` + `startCamera` on toggle; flash forced off on front |
| `Set` not JSON-serializable | Brief 2 persists `selectedCardIds: number[]` |
| Login lacks `returnUrl` today | Brief 4 adds query param + login handler; validate path starts with `/` |
| `handleBulkAction` currently marks `processed: true` | Brief 2 removes committed rows from queue instead |
| Layout visual-diff sensitivity | Narrow prop; immersive mobile-only; no sidebar restyle |
| `verificationPausedRef` shared by disambiguation + sheet | Brief 4 `useEffect` ORs sheet/list-picker/disambiguation on mobile |
| Deck code paths remain in queue hook | UI omits deck (D4); dead `deck` branch in `handleBulkAction` harmless until cleanup convoy |
| iOS Safari no `torch` | Existing `flashSupported` probe; hide flash button |
### Decomposition
| Brief # | Title | Files | Depends on | Est. PR size |
| --- | --- | --- | --- | --- |
| 1 | Layout immersive chrome | `Layout.js`, `test/components/Layout.test.js` | — | ~80 LOC |
| 2 | Cart model + sessionStorage | `use-scanner-queue.js`, `scanner-session.js`, tests | — | ~200 LOC |
| 3 | Camera chrome + facing + peek | `ScannerCamera.js`, `use-camera-scanner.js`, `ScannerCountPill.js`, `ScannerScanPeek.js`, `ScannerToast.js`, `use-scanner-identification.js` | 2 | ~350 LOC |
| 4 | Page orchestration + checkout sheet | `pages/scanner.js`, `pages/login.js`, `ScannerCheckoutSheet.js`, `ScannerReview.js`, `ReviewCardItem.js`, test | 1, 2, 3 | ~380 LOC |
### Slice dependencies (multitask-ready)
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- components/Layout.js
- test/components/Layout.test.js
- brief: 2
depends_on: []
files:
- lib/use-scanner-queue.js
- lib/scanner-session.js
- test/lib/scanner-session.test.js
- test/lib/use-scanner-queue.test.js
- brief: 3
depends_on: [2]
files:
- components/scanner/ScannerCamera.js
- components/scanner/ScannerCountPill.js
- components/scanner/ScannerScanPeek.js
- components/scanner/ScannerToast.js
- lib/use-camera-scanner.js
- lib/use-scanner-identification.js
- brief: 4
depends_on: [1, 2, 3]
files:
- pages/scanner.js
- pages/login.js
- components/scanner/ScannerCheckoutSheet.js
- components/scanner/ScannerReview.js
- components/scanner/ReviewCardItem.js
- test/components/ScannerCheckoutSheet.test.js
```
**Parallelism:** Brief **1** and **2** can run in parallel (`depends_on: []`, disjoint files). Brief **3** starts after **2**. Brief **4** is serial after **1 + 2 + 3**. Do not dispatch Brief 3 until Brief 2 merges (cart API contract). Maximum fan-out: 2 implementers (Brief 1 ∥ Brief 2), then 1, then 1.
### Boot-the-brief check (2026-08-14)
| Check | Finding | Brief fix |
| --- | --- | --- |
| Auto-route still live | `handleCardScanned` L148168 calls `routeScannedCardToDestination` | Brief 2 deletes block |
| No `chrome` on Layout | `Layout({ children, user, showSearch })` only | Brief 1 adds prop |
| `facingMode` hardcoded | `getUserMedia` uses `'environment'` only | Brief 3 adds toggle + restart |
| No gallery entry point | Grep found zero file-picker wiring | Brief 3 `identifyFromGalleryFile` via `tryLayer1TextIdentify` |
| Login no `returnUrl` | `login.js` always pushes `/dashboard` | Brief 4 adds safe return path |
| Cart persistence | IA said in-memory; D7 overrides to `sessionStorage` | Brief 2 separate key from `localStorage` session prefs |
| `ScannerToast` Unicode | `ICON_MAP` uses `✓` / `✗` | Brief 3 SVG replacement |
| Cross-brief props | `ScannerCamera` needs sheet orchestration | Declared in Brief 3 ↔ 4 `cross_brief_commitments` |
No new npm packages. No peer-dep conflicts. `tryLayer1TextIdentify` and `resolveIdentifyOutcome` confirmed exported from `lib/scanner-card-identify.js`.

View file

@ -1,146 +0,0 @@
# A11y audit — scanner-mobile-checkout
**Reviewer:** role-a11y-auditor
**Convoy:** `scanner-mobile-checkout`
**Surface:** uncommitted UI diff vs `origin/main` (+ untracked) — no GitHub PR
**Date:** 2026-08-14
**Model:** cursor-grok-4.5-high (`model_tier=audit`)
**Skill note:** `.cursor/skills/accessibility-audit/SKILL.md` was not present in-repo; audit follows role + convoy UX § A11y constraints (14 items) + WCAG 2.2 AA (AAA target size where convoy requires).
## Diff scope (UI)
| Path | Status |
| --- | --- |
| `components/Layout.js` | modified (`chrome="immersive"`) |
| `components/scanner/ScannerCamera.js` | modified |
| `components/scanner/ScannerCountPill.js` | modified |
| `components/scanner/ScannerToast.js` | modified |
| `components/scanner/ReviewCardItem.js` | modified |
| `components/scanner/ScannerReview.js` | modified |
| `components/scanner/ScannerCheckoutSheet.js` | **new** |
| `components/scanner/ScannerScanPeek.js` | **new** |
| `pages/scanner.js` | modified |
| `test/components/ScannerCheckoutSheet.test.js` | new (a11y-adjacent) |
Non-UI (session/hooks/login/tests) excluded from findings.
## Executive summary
- Chrome controls (flash / swap / gallery / back / Review N), peek tap target, toast live region, checkout `role="dialog"` + Escape + `useFocusTrap`, and sitewide `prefers-reduced-motion` collapse are largely in good shape.
- **One severity-3 finding:** nested List-picker `<Modal>` over open checkout sheet leaves **two Escape handlers and two focus traps** active — Escape dismisses both layers (and Tab can fight). Convoy constraint 10.
- Several severity-2 gaps vs promised constraints: missing sheet close control, no `aria-describedby` helper on disabled commit CTA, camera chrome not `inert`/`aria-hidden` under sheet, native checkbox hit target unreliable at 44×44.
- **Recommendation:** request-changes until nested-overlay Escape/trap is fixed; ship sev-2 before merge if possible.
**Counts:** 7 findings (sev ≥ 3: **1**, sev &lt; 3: **6**). Constraint checklist: **9 pass / 3 partial / 2 fail**.
## Convoy constraint checklist
| # | Constraint | Result | Notes |
| --- | --- | --- | --- |
| 1 | 44×44 targets (2.5.5) | **Partial** | ChromeIconButton, Review N, qty ±, overflow use `minWidth/Height: 44`. Native checkbox is `w-5 h-5` with style mins — hit box often stays ~20px. |
| 2 | Contrast on glass (1.4.3) | **Pass*** | Token text colors; no instrumented contrast measure (*static review). |
| 3 | Focus order open/close (2.4.3) | **Partial** | Trap focuses first focusable in panel; restore via `useFocusTrap`. No header ×; restore may miss if opener was auto-dismissed peek. |
| 4 | Keyboard Escape / peek Enter (2.1.1) | **Partial** | Escape + button peek OK; nested Escape conflict (see A1). |
| 5 | Dialog name/role (4.1.2) | **Pass** | Checkout: `role="dialog"` `aria-modal` `aria-labelledby` → "{N} cards scanned". List picker Modal title "Choose a List". |
| 6 | Required `aria-label`s | **Pass** | Leave / gallery / flash+`aria-pressed` / swap / Review N / checkbox / qty / overflow present. |
| 7 | Non-text (1.1.1) | **Pass** | Row thumbs `alt={card.name}`; peek thumb `alt=""` under named button; decorative SVGs `aria-hidden`. |
| 8 | Status messages (4.1.3) | **Pass** | Toast `role="status"` `aria-live="polite"`; scan status `aria-live="polite"`. Peek is interactive, not a second live region (toast covers identify). |
| 9 | Reduced motion (2.3.3) | **Pass** | Peek/sheet/toast transitions + spinners covered by `styles/globals.css` `@media (prefers-reduced-motion: reduce)`. |
| 10 | Focus trap all overlays | **Fail** | Sheet traps; Modal traps; both stay active when List picker opens over checkout. |
| 11 | Confidence not color-only (1.4.1) | **Partial** | Visible `{pct}% match` + ring; checkbox `aria-label` adds confidence only when &lt;85%; header says "Some matches…" not "{N} need review". |
| 12 | Gallery file input | **Pass** | `sr-only` input + labeled button; `aria-busy` while identifying. |
| 13 | Disabled CTA + describedby | **Fail** | `disabled` only; no helper text / `aria-describedby` / `aria-disabled`. |
| 14 | Hide camera under sheet | **Fail** | `isCheckoutOpen` only changes status copy; video + chrome not `aria-hidden`/`inert`. |
## Findings
| Sev | Layer | WCAG | Surface | Issue | Fix |
| --- | --- | --- | --- | --- | --- |
| **3** | Keyboard / Focus | 2.1.1, 2.4.3 | `pages/scanner.js` + `ScannerCheckoutSheet.js` | Nested List picker Modal while checkout sheet open: both register document `Escape` + Tab traps. Escape closes **picker and sheet**. | Deactivate checkout trap + Escape while `isListPickerOpen`; or close sheet Escape only when picker closed (`if (e.key === 'Escape' && !isListPickerOpen)`). Prefer `useFocusTrap(active && !nestedOpen)`. |
| **2** | Name / Instructions | 3.3.2, 4.1.2 | `ScannerCheckoutSheet.js` `CheckoutFooter` ~L4051 | Zero selection: primary disabled with no visible "Select at least one card" / `aria-describedby`. | Add helper `<p id="checkout-select-hint">…</p>`; `aria-describedby={selectedCount === 0 ? 'checkout-select-hint' : undefined}`; optional `aria-disabled` mirroring `disabled`. |
| **2** | Structure / AT | 1.3.2, 4.1.2 | `ScannerCamera.js` + `pages/scanner.js` | Sheet open does not set `aria-hidden`/`inert` on viewfinder chrome (constraint 14). | When `isCheckoutOpen` (mobile), set `inert` on camera root (or `aria-hidden` on `<video>` + `tabIndex={-1}` / hide chrome from AT). |
| **2** | Target size | 2.5.5 | `ReviewCardItem.js` ~L7279 | Checkbox visual/control ~20×20 despite `minWidth/Height: 44` on `<input type="checkbox">` (unreliable). | Wrap in `<label className="… min-w-[44px] min-h-[44px] flex items-center justify-center">` or custom hit pad. |
| **2** | Keyboard / Operable | 2.1.1, 2.4.3 | `ScannerCheckoutSheet.js` ~L211217 | No header dismiss × (convoy: close via × / Escape / scrim). Keyboard-only users depend solely on Escape. | Add `aria-label="Close cart"` button in sheet header; include in focus order as first focusable. |
| **1** | Understandable | 3.3.1 / UX | `pages/scanner.js` Leave Modal ~L152166 | Copy uses "Stay" not "Keep scanning"; description omits `{N}` count. | `description={\`${unprocessedCount} scanned cards haven't been added yet.\`}`; primary label "Keep scanning". |
| **1** | Sensory | 1.4.1 | `ScannerCheckoutSheet.js` ~L127130 | Low-confidence subline lacks count; checkbox confidence in name only when &lt;85%. | Subline: `{n} need review`; include confidence in checkbox `aria-label` whenever `confidencePct != null`. |
### Severity ≥ 3 detail
#### A1 — Nested Escape / dual focus trap (sev 3)
**Scenario:** User opens checkout sheet → taps **Add to List** → List picker Modal opens. Presses **Escape** intending to close only the picker.
**Observed behavior (static):**
1. `ScannerCheckoutSheet` always calls `useFocusTrap(true)` and adds a document `keydown` Escape → `onClose` while mounted (`ScannerCheckoutSheet.js` L171188).
2. `<Modal open={isListPickerOpen}>` independently traps focus and listens for Escape → its `onClose` (`Modal.js` L2837; `pages/scanner.js` L168173).
3. Checkout stays mounted (`pages/scanner.js` L131137) while picker is open — neither trap is suspended.
Both Escape listeners fire on one keypress → picker closes **and** cart sheet closes. Tab handlers from both traps remain registered → unpredictable focus cycling (constraint 10).
**Suggested fix (minimal):**
```js
// ScannerCheckoutSheet — accept activeTrap prop
const sheetRef = useFocusTrap(trapActive !== false);
useEffect(() => {
if (!trapActive) return undefined;
// … Escape handler only when trapActive
}, [onClose, sheetRef, trapActive]);
```
```js
// pages/scanner.js
<ScannerCheckoutSheet
trapActive={!isListPickerOpen}
/>
```
Or close List picker with Escape via Modal only, and in checkout Escape handler: `if (document.querySelector('[data-list-picker-open]')) return`.
## Layer walk (5 layers, summary)
| Layer | Verdict |
| --- | --- |
| **1 Perceivable** | Toast/status live regions OK; confidence has text+ring; peek/toast SVG icons OK; reduced-motion via global CSS OK. |
| **2 Operable** | 44px chrome generally OK; nested Escape/trap is the blocker; missing sheet ×; checkbox target weak. |
| **3 Understandable** | Labels strong on chrome; leave-modal / empty-CTA / low-confidence copy incomplete vs convoy. |
| **4 Robust** | Dialog semantics present; dual `aria-modal` when nested; camera not inert under sheet. |
| **5 Consistency** | Matches `ScannerDisambiguation` sheet shell pattern (good); nested Modal over sheet needs same trap suspension pattern as disambiguation-above-checkout (z-50 vs z-40) — disambiguation currently unlikely while checkout open because verify pauses, but List picker is the live path. |
## Suggested diffs (priority)
1. **P0** — Suspend checkout Escape + `useFocusTrap` while List picker (or any nested Modal) is open.
2. **P1** — Disabled CTA helper + `aria-describedby`.
3. **P1**`inert` / `aria-hidden` on camera when mobile checkout open.
4. **P1** — 44×44 checkbox hit area via label wrapper.
5. **P2** — Sheet close button; leave-modal copy + count; "{N} need review".
## Patterns to lift
- `ChromeIconButton` — good reusable 44px + `aria-label` / `aria-pressed` / `aria-busy` pattern (`ScannerCamera.js`).
- `ScannerDisambiguation` + `useFocusTrap` — correct shell to copy; extend with **`active` gated by nested overlays**.
- `ScannerToast` SVG + `role="status"` — keep; do not add a competing live region on peek.
## Automated checks
| Check | Result |
| --- | --- |
| axe-core / CI a11y job | Not run (static role audit) |
| Manual SR | Recommend VoiceOver pass: open cart → Add to List → Escape once |
## Approval recommendation
- [ ] approve
- [x] **request-changes** — sev ≥ 3 open (nested Escape/trap)
- [ ] comment-only
Fix A1 before merge. Sev-2 items should land in the same PR or an immediate follow-up brief tagged a11y.
## Hand-off
A11y audit complete. **7 findings** (sev ≥ 3: **1**, sev &lt; 3: **6**).
Report: `.convoys/scanner-mobile-checkout/audits/a11y-20260814.md`.
Recommend fixing sev ≥ 3 before merge.

View file

@ -1,153 +0,0 @@
# Design-System Audit — scanner-mobile-checkout
**Role:** `role-design-system-auditor`
**Surface:** uncommitted `feat/scanner-mobile-checkout` UI (`git diff origin/main` + untracked scanner UI)
**Convoy:** `.convoys/scanner-mobile-checkout.md`
**Date:** 2026-08-14
**Scope:** UI / DS only — read-only; no code changes
**Inputs reviewed**
| Source | Notes |
| --- | --- |
| Diff UI | `Layout.js`, `ScannerCamera.js`, `ScannerCountPill.js`, `ScannerToast.js`, `ReviewCardItem.js`, `ScannerReview.js`, `pages/scanner.js` |
| Untracked | `ScannerCheckoutSheet.js`, `ScannerScanPeek.js` |
| Direction lock | `design_direction` v1 + `## Design direction` in convoy |
| Tokens | `docs/DESIGN_TOKENS.md`, `styles/globals.css` |
| Primitives | `components/ui/{GlassSurface,Button,Modal,Input}.js` |
| Skill | `.cursor/skills/design-systems/SKILL.md` **not installed** in this repo; audit follows role file + prior Liquid Glass audit shape + `docs/DESIGN_TOKENS.md` |
---
## Audit summary
| Check | Status | Notes |
| --- | --- | --- |
| Direction lock (Liquid Glass + immersive camera + sheet) | ⚠️ Partial | Chrome bars + VOCAB CTAs + Modal reuse land; **L-brackets / scan-line missing**; sheet tint recipe drifts |
| Hex in JSX | ❌ | 1 literal `#ffffff` in `ReviewCardItem.js` |
| Binder / stale vocab | ✅ | No `binder` / `Mark Owned` / `Save to binder` in scoped UI |
| `backdrop-filter` on cart **thumbnails** | ✅ | Thumbs are solid image/`--bg-tertiary`; no filter on `<img>` |
| GlassSurface / Button / Modal reuse | ⚠️ Mixed | Camera chrome + peek: GlassSurface; checkout sheet/toast/rows: `glass-panel*` / hand-rolled |
| Sev ≥ 3 findings | **2** | Hex literal; missing bracket + scan-line direction lock |
---
## Maturity scoring (repo DS + this surface)
Scores 04. Evidence cites paths/counts.
| Axis | Score | Evidence |
| --- | --- | --- |
| **Tokens (T)** | **3** | Documented 3-tier glass surface / blur / rim / elevation / scrim in `docs/DESIGN_TOKENS.md` + `styles/globals.css`. Diff mostly consumes `var(--*)`. Residual: `.glass-panel` still hardcodes `blur(12px) saturate(180%)` (not `--glass-blur-*` / `--glass-saturate`); toast uses literal `blur(12px)`. |
| **Components (C)** | **3** | Kit under `components/ui/`: GlassSurface, Modal, Button, Input, SearchBar, StatCard (+ chrome). Scanner adopts GlassSurface on camera bars/icons/peek (9 call sites in `ScannerCamera.js`); Modal×2 + Button in `pages/scanner.js` / checkout footer. Gaps: toast + sheet panel + cart rows skip GlassSurface. |
| **Patterns (P)** | **2** | Locked pattern “immersive camera + bottom-sheet checkout” partially shipped (immersive Layout, sheet shell mirroring Disambiguation, peek non-interactive). **Detection chrome still filled rectangles** (info/success), not ember L-brackets + scan-line. Confidence mid-band maps to gold, not directions amber/ember table. |
| **Governance (G)** | **3** | Convoy `design_direction` locked 2026-08-14; `ui-and-theming.mdc` + vocab CI; `forbidden-hex-in-jsx` / forbidden stale strings. No design-systems skill package in-repo (template path missing). |
| **Adoption (A)** | **2** | This surface: GlassSurface on live chrome/peek ✅; checkout sheet + toast + rows use utility classes / ad-hoc styles ❌; Review N pill is raw `<button>` with gradient (OK shape) but skips ember-rim + dead GlassSurface import. Vocab CTAs ✅. |
**Composite:** T3 / C3 / P2 / G3 / A2
**Top leverage:** **Patterns (P)** — implement Screen 1 bracket + scan-line recipe and align checkout sheet to `GlassSurface tint="low"` + toast to GlassSurface/`--glass-surface-high` so the locked Liquid Glass pattern reads as one system, not chrome-good / body-utility.
---
## Direction lock
Locked fields enforced: Liquid Glass; immersive overlay + bottom sheet; ember/flame accents as rim/glow/gradient only; VOCAB CTAs; no binder; no hex; no per-card Add on peek; no `backdrop-filter` on cart thumbs; reuse GlassSurface / Modal / Button.
| Locked requirement | Diff status |
| --- | --- |
| Immersive `chrome="immersive"` mobile | ✅ `Layout.js` + `pages/scanner.js` |
| Top/bottom glass bars (`--glass-surface-mid`) | ✅ `GlassSurface tint="mid"` in `ScannerCamera.js` |
| L-brackets `--accent-flame` / `--accent-ember` + scan-line | ❌ Filled `border` boxes + `--color-info` / `--color-success` overlays; **no** scan-line / `prefers-reduced-motion` gate |
| Scan peek glass, tap-to-cart only | ✅ `ScannerScanPeek.js` |
| Toast glass-high + SVG check | ⚠️ SVG ✅; surface is `rgba(--bg-secondary-rgb)` + literal blur, not GlassSurface / `--glass-surface-high` |
| Review N gradient + `--ember-rim-pronounced` | ⚠️ Gradient ✅; **no** ember rim; unused `GlassSurface` import |
| Sheet scrim `--modal-scrim` + blur-high | ✅ `ScannerCheckoutSheet.js` styled-jsx |
| Sheet panel `--glass-surface-low` via GlassSurface | ❌ `glass-panel-strong` → surface-**high** recipe |
| Footer VOCAB + Button primary/ghost | ⚠️ VOCAB ✅; secondary uses `variant="secondary"` not `ghost` |
| Confidence rings tokenized (green / amber / orange) | ⚠️ Tokens used; mid=`--accent-gold`, low=`--color-warning` vs locks ember/amber table |
| No hex / no binder | ❌ one `#ffffff`; binder clear |
| Modal for leave + list picker | ✅ |
| No backdrop on thumbnails | ✅ |
---
## Token audit (3-tier)
| Tier | Expectation | Diff notes |
| --- | --- | --- |
| Primitives | Palette / RGB triples in CSS only | No new hex in CSS from this diff. JSX `#ffffff` violates consumer rule. |
| Aliases | `--glass-surface-*`, `--glass-blur-*`, rims, scrim | Camera/scrim/peek mostly correct. Toast blur literal `12px` should be `var(--glass-blur-low)`. `.glass-panel` utility (row list) still non-token blur/saturate (pre-existing; amplified by N cart rows). |
| Components | GlassSurface / Button / Modal compose aliases | Sheet + toast + rows compose utilities/hand styles instead of primitives. |
---
## Component / adoption audit (scoped surface)
| Element | Expected | Actual | Rate |
| --- | --- | --- | --- |
| Glass chrome bars | `<GlassSurface>` | Yes (`ScannerCamera`) | High |
| Scan peek | `<GlassSurface>` | Yes | High |
| Toast | `<GlassSurface tint="high">` or equivalent | Hand-rolled | Low |
| Checkout sheet panel | `<GlassSurface tint="low">` | `glass-panel-strong` | Low |
| Cart rows | GlassSurface high **or** solid `--bg-secondary` | `.glass-panel` (blur per row) | Medium — OK container glass; GPU stack risk |
| Footer CTAs | `<Button>` | Yes | High |
| Leave / List dialogs | `<Modal>` | Yes | High |
| Review N | Button or GlassSurface + ember rim | Raw `<button>` + gradient | Medium |
**Missing primitive usage (not missing primitives):** toast + sheet should compose existing GlassSurface rather than inventing parallel glass.
---
## Findings
Severity 04. Cap prioritized; sev ≥ 3 blocks merge recommendation for DS gate.
| ID | Sev | Finding | Evidence | Fix |
| --- | --- | --- | --- | --- |
| DS-1 | **3** | Hardcoded hex in JSX | `ReviewCardItem.js` qty `+` button `color: '#ffffff'` | Use theme-safe token (e.g. dark-theme `--text-primary` on ember fill, or shared on-accent pattern used by `<Button variant="primary">`) |
| DS-2 | **3** | Direction lock: detection chrome ≠ L-brackets + scan-line | `ScannerCamera.js` filled rect overlays via `overlayBorderColor` (`--color-info` / `--color-success`); no bracket SVG/corners; no ember scan-line / reduced-motion | Ship Screen 1 bracket recipe (`--accent-ember` / `--accent-flame`) + gradient scan-line; disable loop under `prefers-reduced-motion` |
| DS-3 | **2** | Toast bypasses glass recipe | `ScannerToast.js`: `rgba(var(--bg-secondary-rgb), 0.9)` + `backdropFilter: 'blur(12px)'` | Wrap with `<GlassSurface tint="high" blur="low|mid" rim="subtle">`; drop literal blur px |
| DS-4 | **2** | Checkout sheet panel wrong surface tier + no GlassSurface | `ScannerCheckoutSheet.js` `glass-panel-strong` (high) for panel + sticky footer | Panel: GlassSurface `tint="low"` inside scrim; footer sticky on same recipe or solid inner; match Modal panel docs |
| DS-5 | **2** | Review N missing ember-pronounced rim; dead import | `ScannerCountPill.js` imports GlassSurface unused; no `--ember-rim-pronounced`; `text-white` utility | Apply rim via GlassSurface `as="button"` `rim="ember-pronounced"` or box-shadow token; remove dead import; avoid raw white |
| DS-6 | **2** | Confidence band colors drift from lock | `confidenceRingColor`: &lt;70 `--color-warning`, 7084 `--accent-gold`, else success | Align to lock: &lt;70 `--accent-ember`, 7084 amber/warning token, ≥85 `--color-success` |
| DS-7 | **1** | Secondary CTA variant | Checkout footer `Button variant="secondary"` | Prefer `variant="ghost"` per design direction |
| DS-8 | **1** | Leave-modal dismiss copy | `pages/scanner.js` label `"Stay"` | Direction: primary dismiss **"Keep scanning"** |
| DS-9 | **1** | Hardcoded vocab fallback | `ReviewCardItem.js` `'My Collection'` string | `VOCAB.MY_COLLECTION` |
| DS-10 | **1** | Stacked row `backdrop-filter` via `.glass-panel` | Each cart row blurs (`blur(12px)` in utility) | Prefer solid `--bg-secondary` (or GlassSurface without stacking N blurs) for row inners over thumbs — thumbs themselves are clean |
### Passes (not findings)
- No `"binder"` / `"Save to binder"` / `"Mark Owned"` in scoped UI.
- Checkout CTAs import `VOCAB.ADD_TO_MY_COLLECTION` / `VOCAB.ADD_TO_LIST`.
- List picker uses `collectionDisplayName`.
- Peek has no Add / condition / foil; single open-cart control.
- Toast icons are SVG (no emoji glyphs).
- Scrim uses `--modal-scrim` + `var(--glass-blur-high)`.
- Cart thumbnails: no `backdrop-filter`.
- Leave + List picker use `<Modal>`; commit CTAs use `<Button>`.
---
## Governance notes
- Contribution path for glass: `docs/DESIGN_TOKENS.md` + `ui-and-theming.mdc` (“reach for `components/ui/` first”).
- CI: hex-in-JSX + stale vocab gates apply — **DS-1 will fail / should fail `forbidden-hex-in-jsx`**.
- Design-systems skill/templates not present under `.cursor/skills/design-systems/` — maturity G capped partly by missing shared audit tooling in-repo.
---
## Recommendation
**Do not treat DS as merge-clean until DS-1 and DS-2 are fixed.** DS-3DS-6 are the follow-up pass that makes checkout/toast/pill match the same Liquid Glass composition as the camera chrome.
**Child tasks (sev ≥ 3):**
1. Replace `#ffffff` in `ReviewCardItem.js` (and any sibling `text-white` on detection badges if hex-gate/token policy treats them as on-accent debt).
2. Implement ember/flame L-brackets + reduced-motion-aware scan-line on the viewfinder per locked Screen 1.
---
## Hand-off
DS audit complete. Maturity: **T3/C3/P2/G3/A2**. Top leverage: invest in **Patterns** (brackets/scan-line + sheet/toast GlassSurface alignment). Sev ≥ 3 findings: **2**. Report: `.convoys/scanner-mobile-checkout/audits/design-system-20260814.md`.

View file

@ -1,39 +0,0 @@
## Reviewer Report
| Check | Status | Notes |
| --- | --- | --- |
| Scope match | ✅ | All app changes map to briefs 14 `files:`; no unauthorized expansion. Convoy docs/metrics only extras. |
| Conventions | ✅ | Vocab via `VOCAB`; CSS tokens / GlassSurface; Modal for leave + list picker; login `returnUrl` hardened (`/` only, blocks `//` and `://`). |
| Security | ✅ | Shallow L1L2: no new API routes; client cart in `sessionStorage`; returnUrl open-redirect guarded. Defer depth to security-auditor when PR exists. |
| Regression risk | medium | Scanner page rewritten (phase machine removed); auto-route deleted (intentional); Layout immersive gated by prop but TopSearchBar/MobileNavigation paths change for `/scanner`. |
| Test coverage | ⚠️ | Brief-required suites green (27/27). Commit success/error detection untested and currently wrong (see Critical). |
| Documentation | ✅ | No AGENTS.md / schema updates required for this convoy. |
### Findings
- 🔴 **Critical** (must fix before merge): Stale React state used to detect commit failure in `ScannerCheckoutContent.handleCommitOwned` and `pages/scanner.js` `handleListPick`. After `await queue.commitSelectedToOwned()` / `commitSelectedToCollection()`, `queue.scannedCards` is still the pre-commit render snapshot, so a successful commit looks like “all remaining” → false error banner; list picker never closes on success (`setIsListPickerOpen(false)` skipped). Fix: have commit helpers return `successfulIds` (or a boolean), and branch on that — do not re-read hook state immediately after `await`.
- 🟡 **Suggestion** (consider): `ReviewCardItem` still mounts the per-row destination `<select>` when `!showCheckbox`. Checkout path is fine (`showCheckbox`), but Brief 4 asked to remove inline destination pickers; dead path adds confusion and retains pre-existing `#ffffff` on that control.
- 🟡 **Suggestion** (consider): Add a focused test that commit success returns/propagates success without relying on immediate `queue.scannedCards` re-read (locks the Critical fix). Optional: login `returnUrl` honor + reject open redirect.
- 🟢 **Nice to have** (optional): Convoy markdown todos still unchecked despite implemented work — conductor/doc-writer hygiene, not a merge blocker.
### Approval recommendation
- request-changes
### AC evidence (briefs 14)
| Brief | AC | Evidence |
| --- | --- | --- |
| 1 | Immersive hides mobile nav / TopSearchBar below md | `Layout.js` `!isImmersive` MobileNav; TopSearchBar `max-md:hidden`; tests added |
| 1 | Default chrome unchanged | `chrome = 'default'`; regression tests |
| 2 | No fetch in `handleCardScanned` | Auto-route block removed; queue test asserts no route helpers |
| 2 | sessionStorage cart | `SCANNER_CART_STORAGE_KEY` + load/save/clear + tests |
| 2 | Commit removes rows | `successfulIds` filter in `handleBulkAction` |
| 3 | Facing swap + flash rear-only | `switchFacingMode`; `showFlash` env-only; torch off on `user` |
| 3 | Peek 3s / toast SVG / gallery adapter | `ScannerScanPeek`, SVG `ToastIcon`, `identifyFromGalleryFile` |
| 4 | No Setup; returnUrl; sheet + pause; leave Modal | `pages/scanner.js` + hardened `login.js` |
| 4 | Desktop side panel | `ScannerReview` `variant="side-panel"` |
| 4 | Checkout sheet tests | header count, disabled primary, vocab labels |
### Test run
`npm run test:run -- test/components/Layout.test.js test/lib/scanner-session.test.js test/lib/use-scanner-queue.test.js test/components/ScannerCheckoutSheet.test.js`**27/27 passed**.

View file

@ -1,69 +0,0 @@
---
convoy: scanner-mobile-checkout
brief_number: 1
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- components/Layout.js
- test/components/Layout.test.js
cross_brief_commitments:
- brief: 4
description: |
Brief 4 passes `chrome="immersive"` on `<Layout>` from `pages/scanner.js`
for authenticated scanner sessions. This brief only adds the prop and
mobile-only hiding behavior; Brief 4 owns when it is set.
---
# Brief 1: Layout immersive chrome
## Goal (1 sentence)
Add a narrow `chrome="immersive"` prop to `Layout` that hides global app chrome on viewports `< md` without restyling the desktop sidebar.
## Files in scope (do not edit anything else)
- `components/Layout.js`
- `test/components/Layout.test.js`
## Conventions to follow
- Extend the existing `Layout` signature: `export default function Layout({ children, user = null, showSearch = false })` → add `chrome = 'default'`.
- Immersive is **mobile-only** (`md:hidden` / `max-md:` patterns). At `md+`, immersive behaves like `default` (sidebar + TopSearchBar unchanged per D3 desktop note).
- Hide when `chrome === 'immersive'` on `< md`:
- `<MobileNavigation />` (line ~674)
- `<TopSearchBar />` (line ~986)
- Desktop `<aside>` sidebar is already `hidden md:flex` — no change needed at `md+`; on mobile the drawer + bottom nav are the chrome to suppress.
- Main column: remove `pb-16` bottom padding when immersive (no bottom nav). Use `max-md:pb-0` on the main wrapper (`flex-1 flex flex-col pb-16 md:pb-0`).
- Outer shell: optional `max-md:p-0` when immersive so camera can be true full-bleed.
- Do **not** change sidebar styles, nav items, or command palette behavior.
- Use CSS tokens only — no new hex in JSX.
## Implementation shape (verified against `Layout.js`)
```js
export default function Layout({
children,
user = null,
showSearch = false,
chrome = 'default',
}) {
const isImmersive = chrome === 'immersive';
// ...
// MobileNavigation: {!isImmersive && <MobileNavigation ... />}
// TopSearchBar: {user && !isImmersive && <TopSearchBar ... />}
// Main wrapper className: include isImmersive && 'max-md:pb-0'
}
```
## Acceptance criteria
- [ ] `chrome="immersive"` hides `MobileNavigation` and `TopSearchBar` below `md` breakpoint
- [ ] `chrome="immersive"` at `md+` renders identical chrome to `chrome="default"` (sidebar + TopSearchBar visible)
- [ ] `chrome="default"` (or omitted) is unchanged from current behavior
- [ ] `test/components/Layout.test.js` adds regression tests: immersive hides mobile nav landmark; default still renders Sign-in CTA when `user={null}` (existing 5 assertions preserved)
- [ ] No scope expansion (do not edit files outside `files:` above)
## Rationale (≤3 sentences)
Immersive chrome is isolated to `Layout.js` so visual-diff-sensitive sidebar code stays untouched. Mobile-only gating matches design-direction desktop exception. Brief 4 wires the prop from `/scanner` without further Layout changes.

View file

@ -1,99 +0,0 @@
---
convoy: scanner-mobile-checkout
brief_number: 2
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/use-scanner-queue.js
- lib/scanner-session.js
- test/lib/scanner-session.test.js
- test/lib/use-scanner-queue.test.js
cross_brief_commitments:
- brief: 4
description: |
Export `clearScannerCartStorage()` from `scanner-session.js`. Brief 4
calls it when the user confirms "Leave" on the back-guard modal (D1/D7).
Queue hook exposes `hydrateFromStorage` on mount — Brief 4 does not
reimplement hydration.
---
# Brief 2: Cart model — stop auto-route + sessionStorage
## Goal (1 sentence)
Reverse rebuild D3 auto-route: identifies enqueue locally only, auto-select new rows, and persist cart + selection in `sessionStorage` (D7).
## Files in scope (do not edit anything else)
- `lib/use-scanner-queue.js`
- `lib/scanner-session.js`
- `test/lib/scanner-session.test.js`
- `test/lib/use-scanner-queue.test.js` (new)
## Conventions to follow
- **No new API routes.** Commit paths stay `addScannedCardToOwned` / `addScannedCardToCollection` from `lib/scanner-route-api.js` (already verified).
- **Do not use** `SCANNER_SESSION_STORAGE_KEY` / `localStorage` for cart rows (UX anti-pattern). Add a separate key in `scanner-session.js`:
```js
export const SCANNER_CART_STORAGE_KEY = 'deckhearth:scanner-cart';
```
- Serialize `selectedCards` as `number[]` (Set is not JSON-safe).
- Keep `mergeScannedCardEntry` unchanged — cart entry shape already includes `processed: false` and `confidence` from `buildScannedCardPayload`.
- `sessionDestination` param may remain on the hook signature for backward compat but **must not** trigger network I/O in `handleCardScanned`.
## Implementation shape (verified against `use-scanner-queue.js`)
**Remove auto-route** — delete the block after `setScannedCards(nextQueue)`:
```js
// DELETE lines ~148-168 (sessionDestination guard + routeScannedCardToDestination)
```
**Replace `handleCardScanned` with enqueue-only:**
```js
const handleCardScanned = (cardData) => {
setAutoRouteError(null);
const { cardEntry, scannedCards: nextQueue } = mergeScannedCardEntry(
scannedCards,
cardData,
scanDefaults
);
setScannedCards(nextQueue);
setSelectedCards((prev) => new Set([...prev, cardEntry.id]));
persistCart(nextQueue, new Set([...selectedCards, cardEntry.id]));
};
```
Add `loadScannerCart()` / `saveScannerCart({ scannedCards, selectedCardIds })` / `clearScannerCartStorage()` in `scanner-session.js`.
Hydrate on mount in `useScannerQueue` via `useState` initializer + `useEffect` debounced save on `[scannedCards, selectedCards]` changes.
**Checkout helpers** (used by Brief 4 footer CTAs):
```js
const commitSelectedToOwned = async () => handleBulkAction('owned');
const commitSelectedToCollection = async (collectionId) =>
handleBulkAction('collection', collectionId);
```
Export these plus `unprocessedCount` derived helper: `scannedCards.filter(c => !c.processed).length`.
After successful `handleBulkAction`, remove committed rows from `scannedCards` entirely (not `processed: true`) per D1/D6 — cart holds only uncommitted items.
## Acceptance criteria
- [ ] `handleCardScanned` performs zero `fetch` calls (no `routeScannedCardToDestination`)
- [ ] New enqueue auto-adds card `id` to `selectedCards`
- [ ] Cart + selection survive `sessionStorage` round-trip within a tab (`test/lib/scanner-session.test.js`)
- [ ] `test/lib/use-scanner-queue.test.js`: mock `scanner-route-api` and assert `handleCardScanned` never calls route helpers; assert `handleBulkAction('owned')` calls `addScannedCardToOwned` once per selected card
- [ ] `clearScannerCartStorage()` clears persisted state
- [ ] Existing `test/lib/scanner-session.test.js` cases still pass
- [ ] No scope expansion
## Rationale (≤3 sentences)
Cart semantics are pure client state and can land before any UI work. Separating sessionStorage from the existing `localStorage` destination prefs avoids D7/localStorage anti-pattern. Removing committed rows instead of flipping `processed` simplifies the checkout sheet list.

View file

@ -1,124 +0,0 @@
---
convoy: scanner-mobile-checkout
brief_number: 3
depends_on: [2]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- components/scanner/ScannerCamera.js
- components/scanner/ScannerCountPill.js
- components/scanner/ScannerScanPeek.js
- components/scanner/ScannerToast.js
- lib/use-camera-scanner.js
- lib/use-scanner-identification.js
cross_brief_commitments:
- brief: 4
description: |
`ScannerCamera` accepts orchestration props wired by Brief 4:
`onBack`, `onOpenCheckout`, `onGalleryIdentify`, `latestPeekCard`,
`cartCount`, `isCheckoutOpen`, `verificationPausedRef`. Defaults/no-ops
keep the component renderable before Brief 4 lands. Brief 4 owns sheet
open state and back-guard modal.
- brief: 4
description: |
`useScannerIdentification` exports `identifyFromGalleryFile(file)`
thin adapter only; does not modify `pages/api/scan/identify.js` or
`lib/scanner-card-identify.js` server contract.
---
# Brief 3: Camera chrome, facing mode, scan peek, gallery adapter
## Goal (1 sentence)
Refactor `ScannerCamera` into full-bleed immersive glass chrome (top/bottom bars, flash, camera swap, Review N pill, scan peek, gallery) and add rear/front `facingMode` support in `useCameraScanner`.
## Files in scope (do not edit anything else)
- `components/scanner/ScannerCamera.js`
- `components/scanner/ScannerCountPill.js`
- `components/scanner/ScannerScanPeek.js` (new)
- `components/scanner/ScannerToast.js`
- `lib/use-camera-scanner.js`
- `lib/use-scanner-identification.js` (gallery adapter + pause contract only)
## Conventions to follow
- Glass surfaces: `<GlassSurface tint="mid">` per UX brief; migrate hardcoded `rgba(0,0,0,0.55)` in `ScannerCamera` to tokens.
- `ScannerToast`: replace `ICON_MAP` Unicode glyphs with inline SVG check/error (design-direction checklist).
- `ScannerCountPill`: evolve to gradient **Review {N}** pill; keep `aria-label={`Review ${count} scanned cards`}`.
- Flash: `flash.flashSupported && facingMode === 'environment'` (UX spec). Hide on front camera; turning to front must call `toggleFlash` off if torch was on.
- Peek: new `ScannerScanPeek.js` — 3s auto-dismiss, `pointer-events-none` wrapper, single child button `pointer-events-auto`, tap calls `onOpenCheckout`.
- Copy: toast message `"{card.name} added"` with `role="status"`.
- Do **not** mount checkout sheet here — call `onOpenCheckout` only.
- Identification pipeline files (`scanner-card-identify.js`, API routes) are out of scope.
## `useCameraScanner` shape (verified)
Current `startCamera` hardcodes `facingMode: 'environment'` (line ~119). Extend:
```js
export function useCameraScanner({ onError, onVerifyCard, verificationPausedRef, facingMode = 'environment' }) {
const [activeFacingMode, setActiveFacingMode] = useState(facingMode);
// startCamera uses activeFacingMode in getUserMedia constraints
const switchFacingMode = useCallback(() => {
setActiveFacingMode((prev) => (prev === 'environment' ? 'user' : 'environment'));
}, []);
// useEffect: when activeFacingMode changes and stream exists, stopCamera + startCamera
return { /* existing */, facingMode: activeFacingMode, switchFacingMode };
}
```
`verificationPausedRef` early-return already exists at line ~91 in the tracking interval — no change needed.
## Gallery adapter (verified — no existing file-picker path)
Add to `useScannerIdentification` return object:
```js
const identifyFromGalleryFile = async (file) => {
if (!file || verificationPausedRef?.current) return;
const imageData = await readFileToImageData(file); // canvas draw; local helper in same file
const authHeaders = getScanAuthHeaders();
const result = await tryLayer1TextIdentify(imageData, authHeaders);
const outcome = resolveIdentifyOutcome(result);
const syntheticTracker = { id: `gallery-${Date.now()}`, status: 'verifying' };
await applyIdentifyOutcome(syntheticTracker, imageData, outcome);
};
```
Import `tryLayer1TextIdentify`, `resolveIdentifyOutcome` from `./scanner-card-identify.js` (already exported — verified).
## `ScannerCamera` props contract (for Brief 4)
```js
export default function ScannerCamera({
queue,
camera,
identification,
onBack,
onOpenCheckout,
onGalleryIdentify, // (file) => identification.identifyFromGalleryFile(file)
latestPeekCard, // most recent unprocessed queue entry or null
cartCount,
isCheckoutOpen,
verificationPausedRef,
}) { /* ... */ }
```
Remove: `deckMode`, `sessionDestination`, `onStopSession`, phase-based stop button. Back button calls `onBack` (Brief 4 shows leave modal).
Layout: viewport `min-h-[100dvh] max-md:rounded-none max-md:min-h-[100dvh]`; top bar with back / "Scan Cards" / gallery file input; bottom bar flash | status | Review N.
## Acceptance criteria
- [ ] `switchFacingMode` restarts stream with toggled `facingMode`; flash hidden on `user`
- [ ] `ScannerScanPeek` auto-dismisses ~3s; tap invokes `onOpenCheckout`
- [ ] `ScannerToast` uses SVG icons, not Unicode glyphs
- [ ] `identifyFromGalleryFile` calls existing `tryLayer1TextIdentify` — no API route changes
- [ ] `ScannerCountPill` hidden when `count <= 0`; shows gradient Review pill when `count > 0`
- [ ] No hex literals added in edited `.js` files
- [ ] No scope expansion
## Rationale (≤3 sentences)
Camera chrome files are disjoint from `pages/scanner.js`, so this brief can land after the cart model without file conflicts. Gallery needs a thin hook adapter because no file-picker path exists today (boot-the-brief finding). Brief 4 wires props and pause ref when the sheet opens.

View file

@ -1,159 +0,0 @@
---
convoy: scanner-mobile-checkout
brief_number: 4
depends_on: [1, 2, 3]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- pages/scanner.js
- pages/login.js
- components/scanner/ScannerCheckoutSheet.js
- components/scanner/ScannerReview.js
- components/scanner/ReviewCardItem.js
- test/components/ScannerCheckoutSheet.test.js
cross_brief_commitments:
- brief: 1
description: |
Passes `chrome="immersive"` to `<Layout>` on authenticated `/scanner`
(mobile immersive; desktop uses default chrome per D3).
- brief: 2
description: |
Calls `clearScannerCartStorage()` on confirmed leave. Uses
`commitSelectedToOwned` / `commitSelectedToCollection` from queue hook.
Does not reimplement sessionStorage persistence.
- brief: 3
description: |
Wires `ScannerCamera` props (`onBack`, `onOpenCheckout`, `latestPeekCard`,
`onGalleryIdentify`, `cartCount`, `isCheckoutOpen`, `verificationPausedRef`).
Sets `verificationPausedRef.current = true` when checkout sheet or List
picker modal is open (mobile); desktop side panel does not pause (UX spec).
deletes: []
---
# Brief 4: Page orchestration + checkout sheet
## Goal (1 sentence)
Collapse `/scanner` to scanning-first immersive flow, wire checkout sheet (mobile) / side panel (desktop), auth return URL (D2), and commit CTAs using vocab constants.
## Files in scope (do not edit anything else)
- `pages/scanner.js`
- `pages/login.js`
- `components/scanner/ScannerCheckoutSheet.js` (new)
- `components/scanner/ScannerReview.js` (desktop `md+` side panel only)
- `components/scanner/ReviewCardItem.js`
- `test/components/ScannerCheckoutSheet.test.js` (new)
## Conventions to follow
- **Vocab:** `import { VOCAB } from '../lib/collection-vocabulary.js'` — primary `VOCAB.ADD_TO_MY_COLLECTION`, secondary `VOCAB.ADD_TO_LIST`. Never "Save to binder" / "Mark Owned".
- **Sheet shell:** copy `ScannerDisambiguation.js` pattern (`fixed inset-0`, `glass-panel-strong`, `useFocusTrap`, Escape dismiss, `z-40` — disambiguation stays `z-50`).
- **Modal primitive:** back-guard and List picker use `<Modal>` from `components/ui/Modal.js` (not `window.confirm`).
- **Selection:** checkboxes always visible; all unprocessed IDs selected on first sheet open; new enqueue while open auto-selects (Brief 2 + local effect).
- **Pause:** `verificationPausedRef.current = isCheckoutOpen || isListPickerOpen || identification.disambiguation` on mobile only.
- **Auth:** `getUserFromRequest` not needed (page is client-only). Login redirect:
```js
// pages/scanner.js — replace router.push('/login')
router.push(`/login?returnUrl=${encodeURIComponent('/scanner')}`);
// pages/login.js — after successful login, before role redirect:
const returnUrl = typeof router.query.returnUrl === 'string' ? router.query.returnUrl : null;
if (returnUrl && returnUrl.startsWith('/')) {
router.push(returnUrl);
return;
}
```
- **D1:** after commit, stay on camera; empty cart closes sheet and resumes scanning.
- **D4:** no deck destination in checkout UI.
- **D6:** partial commit keeps sheet open with remaining rows.
## `pages/scanner.js` shape (verified — replace phase machine)
Remove `phase` state (`setup` / `scanning` / `review`). Default: camera always mounted when authenticated.
```js
export default function Scanner() {
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
const [isListPickerOpen, setIsListPickerOpen] = useState(false);
const [showLeaveModal, setShowLeaveModal] = useState(false);
const verificationPausedRef = useRef(false);
// useScannerSession: keep scanDefaults only; drop Setup UI (D3)
const queue = useScannerQueue({ user, scanDefaults, deckMode: null });
// ...
return (
<Layout user={user} chrome="immersive">
<div className="relative flex flex-col md:flex-row h-full min-h-0 max-md:fixed max-md:inset-0">
<ScannerCamera
queue={queue}
camera={camera}
identification={identification}
onBack={() => unprocessedCount > 0 ? setShowLeaveModal(true) : router.back()}
onOpenCheckout={() => setIsCheckoutOpen(true)}
onGalleryIdentify={(file) => identification.identifyFromGalleryFile(file)}
latestPeekCard={queue.scannedCards[0] ?? null}
cartCount={unprocessedCount}
isCheckoutOpen={isCheckoutOpen}
verificationPausedRef={verificationPausedRef}
/>
{/* Mobile sheet */}
<div className="md:hidden">
{isCheckoutOpen && (
<ScannerCheckoutSheet
queue={queue}
collections={queue.collections}
onClose={() => setIsCheckoutOpen(false)}
onOpenListPicker={() => setIsListPickerOpen(true)}
/>
)}
</div>
{/* Desktop side panel */}
<div className="hidden md:flex md:w-[360px] md:flex-shrink-0 md:border-l" style={{ borderColor: 'var(--border)' }}>
<ScannerReview queue={queue} collections={queue.collections} variant="side-panel" />
</div>
<Modal open={showLeaveModal} /* ... */ />
<Modal open={isListPickerOpen} title="Choose a List" /* ... */ />
</div>
</Layout>
);
}
```
Sync `verificationPausedRef` in `useEffect` when `isCheckoutOpen || isListPickerOpen || identification.disambiguation` (mobile: checkout open; desktop: only list picker + disambiguation per UX).
## `ScannerCheckoutSheet` responsibilities
- Header: `{N} cards scanned` + low-confidence subline when any row `confidence < 70`
- Rows: `ReviewCardItem` with checkbox, confidence ring, qty stepper, overflow menu (remove / condition / foil)
- Footer: primary `VOCAB.ADD_TO_MY_COLLECTION` with `(N)` when subset selected; secondary `VOCAB.ADD_TO_LIST`
- Commit error: inline banner in footer, rows stay selected
- `role="dialog"` `aria-modal="true"` `aria-labelledby`
- On last item removed → `onClose()` + resume camera
## `ReviewCardItem` changes
- Add optional `selected`, `onToggleSelect`, `showCheckbox`, `confidence` ring props
- Remove inline destination picker from row (destinations in sheet footer only)
- `aria-label` on checkbox: `Select ${card.name}`; confidence in `aria-label` when `< 85`
## `ScannerReview` changes
- Support `variant="side-panel"` for desktop persistent panel (same row/footer semantics as sheet, no scrim)
- Remove full-page empty state / "New Session" / deck progress from default path
## Acceptance criteria
- [ ] Opening `/scanner` authenticated shows camera immediately — no Setup phase (D3)
- [ ] Unauthenticated redirect includes `returnUrl=/scanner`; login honors it (D2)
- [ ] Mobile checkout sheet opens from Review N and peek; pauses identification under sheet
- [ ] `commitSelectedToOwned` / List commit work; committed rows leave cart; camera resumes when empty (D1/D6)
- [ ] Back with unprocessed cart shows `<Modal>` leave confirm; confirm calls `clearScannerCartStorage()` + `router.back()`
- [ ] Desktop `md+` shows side panel with same CTA semantics; Layout sidebar visible
- [ ] `test/components/ScannerCheckoutSheet.test.js`: renders header count, disabled primary when nothing selected, vocab labels present
- [ ] No scope expansion
## Rationale (≤3 sentences)
Integration brief owns the sole `pages/scanner.js` writer after Layout and camera contracts exist. Login `returnUrl` is a two-line change required by D2 and verified absent today. Checkout sheet and desktop panel share row components but split shells to stay under 400 LOC.

View file

@ -1,63 +0,0 @@
# Convoy: scanner-rebuild
**Status:** In Progress (auto-approved — overnight build)
**Owner:** Agent
**Created:** 2026-06-13
## Goal
Rebuild the card scanner from a desktop-first, configuration-heavy, everything-at-once layout into a mobile-first, three-phase flow optimized for rapid multi-card scanning.
## Architecture Decisions
### D1 — Three-phase flow
The scanner page becomes a state machine with three phases:
- **Setup** — choose destination + game (one-time per session)
- **Scanning** — full-screen camera with success toasts + count pill
- **Review** — card list with per-card edits + batch confirm
### D2 — Mobile-first camera
Camera fills the viewport during scanning phase. No side panels, no scrolling to see results. Success feedback via overlay toasts + haptic vibration.
### D3 — Auto-everything during scan
- Cards auto-route to chosen destination immediately
- Condition defaults NM, foil auto-detected from vision response
- Game auto-detected from vision (no pre-filter needed)
- Duplicates auto-increment quantity
### D4 — Deck Mode
Toggle in setup phase. Shows progress toward format-aware deck size (60/99/40). Auto-suggests stop when target reached.
### D5 — Disambiguation as bottom sheet
Replace full-screen modal with a slide-up bottom sheet. One tap to pick, then immediately resume scanning.
### D6 — New features
- **Scan history** — last 5 sessions persisted in localStorage
- **Sound feedback** — subtle blip on successful scan (configurable)
- **Offline queue** — if network drops, queue identification calls and retry
- **Camera flash toggle** — torch mode for foil detection in dim lighting
## File ownership
| Workstream | Files | Agent |
|---|---|---|
| Core orchestrator | `pages/scanner.js`, `lib/use-scanner-session.js` | A |
| Camera phase | `components/scanner/ScannerCamera.js`, `ScannerToast.js`, `ScannerCountPill.js`, `lib/use-camera-scanner.js`, `lib/use-scanner-sound.js`, `lib/use-scanner-flash.js` | B |
| Setup + Review | `components/scanner/ScannerSetup.js`, `ScannerReview.js`, `ScannerDisambiguation.js`, `DeckModeIndicator.js` | C |
| Queue + API | `lib/use-scanner-queue.js`, `lib/use-scanner-offline.js`, `pages/api/cards/batch-ownership.js` | D |
## Risks
- R1: Parallel agents may produce interface mismatches → mitigated by specifying contracts in each agent's prompt
- R2: Existing hooks (`use-scanner-identification.js`, `scanner-card-identify.js`, `scanner-card-detection.js`) are not being rewritten — the rebuild layers on top of them
- R3: Visual regression risk — existing smoke tests may break → handled in integration pass
## Human gates bypassed
Per user instruction (overnight build, 2026-06-13), all architect/reviewer gates are bypassed for this convoy. The user will review the final output in the morning.

View file

@ -47,11 +47,6 @@ PR #18; P1 #12 via `pick-a-name` PR #21) and **P1 #11.5 `fix-lint-baseline`**
(PRs #61#63, 2026-06-02), the P1 lane is complete. Lint is **0 problems**;
CI `lint` is blocking (no `|| true` / `continue-on-error`).
**Post-hoc note (2026-08-15):** `reconcile-historical-add-scripts` implementer
briefs B1B6 closed the fresh-env onboarding gap in June 2026 (PRs #148#153);
Brief 7 (docs + verification runbook) landed two months later. The P1 lane was
already complete when B1B6 merged — this closure is documentation-only.
## P0 — ship-blockers (security)
These MUST land before any anonymous traffic touches the production URL.
@ -420,8 +415,7 @@ Total: ~14 convoys to get from current state to public-launch-ready. Estimate 4-
Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch sequence but need to land before public traffic. Listed in priority order; not all will be P0/P1 — most are CI / DX / hygiene polish.
- **`scanner-identify-upgrade`** (priority: P1 scanner accuracy; opened 2026-08-14). Umbrella after measuring `scan_attempts` (n=224, 2026-05-272026-08-12): L1 auto-match 5.8% of L1 vs the `add-real-ocr-layer` ≥70% target; L1 escalate 76.7%; L2 `not_a_card` 44.6% of Gemini calls; end-to-end auto-match 10.3%. Sub-convoys: `tighten-scan-identify-hot-path``improve-scan-card-detection` (queued since PR #38, never opened) → `scan-visual-catalog-search`. Worktree `tcg-vault-worktrees/scanner-identify-upgrade` on `convoy/scanner-identify-upgrade`. File-disjoint from `scanner-mobile-checkout` (chrome/cart). See `.convoys/scanner-identify-upgrade.md`.
- **`rotate-default-admin`** — **RESOLVED 2026-06-13** by PR #141 (`scripts/rotate-admin-password.js`). The convoy chose option B from the architect's three-option menu (close as no-op / build script / build forced-rotation flow): a parameterized one-shot rotation script that's safer than "manually change via app" (audit-trail-preserving via `updated_at`) and lighter than building a first-login forced-rotation flow in the app (that heavier option is the deferred `force-admin-password-reset-flow` convoy). Script reads `POSTGRES_URL` + `ADMIN_NEW_PASSWORD` env vars, validates target row exists + has `role='admin'`, refuses to rotate non-admin rows, verifies the new hash matches the supplied plaintext via `bcrypt.compare` post-update, never echoes the password. AGENTS.md Gotcha #4 documents the rotation workflow. Sibling test users (alice/bob in `scripts/create-test-users.js`) intentionally NOT rotated (dev fixtures, not real auth surfaces).
- **`rotate-default-admin`** (priority: P2 hygiene). Operator-rotation script for envs that ran `setup-neon-db.js` before `drop-public-setup` and still carry the weak `admin123` bcrypt hash. Surfaced in P0 #3 § Operator caveat. Optional: do nothing if no audit finds a deployed env with the weak hash.
- **`delete-dead-lorcana-import`** — **RESOLVED 2026-06-02** by PR #59 (`8262fec`). Deleted `pages/api/cards/import-lorcana.js` and `scripts/import-lorcana.js`; no dedicated convoy file (cleanup tracked here only). Entry kept for audit trail.
- **`tighten-visual-diff-path-filter`** — **RESOLVED 2026-05-26** by `tighten-visual-diff-path-filter` convoy, squash commit `ba95462` (PR #26). Single-edit `paths:` filter change in `.github/workflows/visual-diff.yml`: inserted `'!pages/api/**'` immediately after `'pages/**'` (order-sensitive per GitHub Actions' minimatch path-filter semantics — exclusions only fire after a prior include matches). Verified the YAML deserialization order at gate time (`['pages/**', '!pages/api/**', 'components/**', 'styles/**', 'tailwind.config.js', 'postcss.config.js']`). `preview-smoke.yml` left untouched (no `paths:` filter; intentionally fires on every PR). Diff: 2 files, +279 / -0 (1 YAML entry + inline comment block + the planning convoy file). **Post-merge verification still pending** — the only true verification is that the next API-only PR after this merges does NOT trigger `Screenshot diff`. PR #30 (`single-sql-client`, squash `c403ea4`) was the **first API-only PR post-merge** and its CI Checks tab showed `Screenshot diff: not triggered` — empirical confirmation that the `!pages/api/**` exclusion fires correctly. The next-API-only-PR success line was originally specified in the convoy file's § Verification plan as the deferred-to-post-merge gate; this is that confirmation. Entry kept (not removed) to preserve the audit trail. See `.convoys/tighten-visual-diff-path-filter.md` § As-shipped.
- **`purge-weak-creds-from-helpers`** — **RESOLVED 2026-05-26** by `purge-weak-creds-from-helpers` convoy, squash commit `5f2b234` (PR #27). The umbrella is now closed; both remaining halves shipped together. **Multi-convoy history:** (1) `drop-public-setup` Brief 1+2 (`ff80753` + `b63b509`) removed the first `admin123` literal from `scripts/setup-neon-db.js` and set the env-var + fail-loud + no-echo precedent. (2) `pick-a-name` Brief 2 (`9abbab6`) swept the `@tcgvault.com` literals in the three helper paths to `@deckhearth.com` together with the migration script. (3) `fix-reset-db-script` (`3ab9bf8`, PR #25) removed the second `admin123` from `scripts/reset-db.js` and the second `Admin Password:` echo. (4) **This convoy (PR #27)** closes the umbrella by sweeping the last two files: `scripts/create-test-users.js` (alice/bob fixtures, previously hardcoding `bcrypt.hash('alice123', 12)` + `bcrypt.hash('bob123', 12)` and echoing both literals to stdout) and `TESTING_GUIDE.md` (Test Accounts table previously documenting the weak literals). The post-convoy contract: single `TEST_USERS_PASSWORD` env var (intentional simplification per Risk R2 — these are collaboration-flow demo fixtures, not independent identities), fail-loud at the top of `createTestUsers()` BEFORE any DB connection, no password echo anywhere (`✅ Created Alice (alice@deckhearth.com / alice123)` → `✅ Created Alice (alice@deckhearth.com)`), `ON CONFLICT (email) DO NOTHING` preserved. Diff: 3 files, +249 / -22. Lint preserved at 125 (post-PR-#31 baseline); vitest 21/21. ESM-already (this was the first of the three weak-creds-shape convoys to skip the CJS→ESM half because `scripts/create-test-users.js` was already top-level ESM). **Operator caveat:** existing alice/bob rows in already-seeded envs are NOT rotated by re-running the script — `ON CONFLICT` preserves the old hashes; operators must rotate manually via the app or drop those rows and re-seed. Same caveat as the `drop-public-setup` admin-row guidance. **Surfaced out-of-scope follow-up:** `purge-quick-login-from-loginpage` — see new queue entry below. Entry kept (not removed) to preserve the audit trail. See `.convoys/purge-weak-creds-from-helpers.md` § As-shipped.
@ -477,12 +471,10 @@ Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch seq
- **`purge-quick-login-from-loginpage`** — **RESOLVED 2026-05-29** by PR #56 (`e0218e4`). Quick Login removed from `pages/login.js`. See `.convoys/purge-quick-login-from-loginpage.md` § As-shipped.
- **`purge-neondatabase-serverless-fully`** — **RESOLVED 2026-06-02** by PR #57 (`5115683`). `@neondatabase/serverless` removed from `package.json`; operational scripts use `@vercel/postgres`. Historical `scripts/add-*` / `fix-*` / `seed-*` graveyard unchanged per no-go-zones. Entry kept for audit trail.
- **`wire-migrate-into-ci`** (priority: P2 CI infra). Surfaced 2026-05-26 by `migration-tool` (PR #32) — D6 deferral. Add a CI job that runs `npm run migrate up` against a test DB (either a dedicated Neon branch + `MIGRATE_TEST_DATABASE_URL` secret with branch-reset logic, or a Postgres service container with a ~30s container-start tax). Catches syntactically-invalid migrations + most logical errors at PR time. Currently, the first signal that a new migration is broken is the developer's local `npm run migrate up` against their dev branch (or post-deploy on Vercel). Documented in `.convoys/migration-tool.md` § R3.
- **`reconcile-historical-add-scripts`** **RESOLVED 2026-06-14** (B1B6 via PRs #148#153; B7 docs closure 2026-08-15). Six reconciliation migrations capture historical add-script DDL so fresh Neon branches onboard via `npm install``npm run setup-db` alone. Operator verification runbook at `docs/MIGRATION_VERIFICATION_RUNBOOK.md`. See `.convoys/reconcile-historical-add-scripts.md` § As-shipped. Entry kept for audit trail.
- **`retire-graveyard-scripts-after-audit`** (priority: P3 polish; **UNBLOCKED 2026-08-15** by `reconcile-historical-add-scripts` § As-shipped). Surfaced 2026-05-26 by `migration-tool` (PR #32). Once the migration history captures all historical effects, the legacy `scripts/add-*.js` / `fix-*.js` / `seed-*.js` files can be deleted (or moved to `scripts/historical/`). They remain no-go-zones until that cleanup convoy lands. Documented in `.convoys/migration-tool.md` § Follow-ups.
- **`reconcile-historical-add-scripts`** (priority: P1 quality — needed for fresh-env onboarding). Surfaced 2026-05-26 by `migration-tool` (PR #32). Fold the effects of the 27 historical `scripts/add-*.js` / `fix-*.js` / `seed-*.js` jobs into the migration history so a brand-new Neon branch can be onboarded by `npm install``npm run setup-db` alone (without manually replaying the historical scripts). Multi-PR; ideally one migration per logical change, generated by reading the scripts' SQL and re-shaping into idempotent `pgm.sql(...)` blocks (with `IF NOT EXISTS` / `IF EXISTS` guards so re-application is safe). Documented in `.convoys/migration-tool.md` § R1.
- **`retire-graveyard-scripts-after-audit`** (priority: P3 polish; **blocked on `reconcile-historical-add-scripts`**). Surfaced 2026-05-26 by `migration-tool` (PR #32). Once the migration history captures all historical effects, the legacy `scripts/add-*.js` / `fix-*.js` / `seed-*.js` files can be deleted (or moved to `scripts/historical/`). They remain no-go-zones until that cleanup convoy lands. Documented in `.convoys/migration-tool.md` § Follow-ups.
- **`audit-node-pg-migrate-transitive-deps`** (priority: P3 hygiene). Surfaced 2026-05-26 by `migration-tool` (PR #32) — R5 in the convoy file. `npm audit` reports 11 vulnerabilities (6 moderate, 5 high) coming from `node-pg-migrate@8.0.4`'s `glob@~11.1.0` + `yargs@~17.7.0` transitive deps (older `brace-expansion`, `minimatch`, `picomatch` versions with known advisories). All in dev-only paths; the migration tool runs in scripts/CI, never in the deployed Next.js bundle, and the affected APIs (glob's shell-injection CLI; brace-expansion's ReDoS) are not exercised by node-pg-migrate's call sites. Surface only if a security audit specifically flags this surface, or if `node-pg-migrate` ships a v9 that updates the transitive tree.
- **`add-migration-template`** (priority: P3 DX). Surfaced 2026-05-26 by `migration-tool` (PR #32). Add a custom template via `--template-file-name` so generated migrations include the project's preferred docstring shape + a reminder about `docs/SCHEMA_MAP.md` updates. Surface if migration authoring proves inconsistent.
- **`unify-user-avatar-column`** (priority: P3 polish). Surfaced 2026-06-14 by `reconcile-historical-add-scripts` architect Finding 2. `users` table has two avatar columns: `profile_image_url` (from `add-user-profile-columns.js`) and `avatar_url` (from `add-user-profile-fields.js`). Both are added by B5's migration for fresh-env parity; runtime may read either. Cleanup: audit which column each caller reads, pick one canonical column, migrate + drop the other.
- **`drop-dead-cards-columns`** (priority: P3 polish). Surfaced 2026-06-14 by `reconcile-historical-add-scripts` architect Finding 3. `cards.quantity` and `cards.favorited` are dead columns per `docs/SCHEMA_MAP.md` § Smells #3; still added by B1's migration for fresh-env parity. Cleanup: query-trace audit to confirm zero reads/writes, then a `DROP COLUMN` migration.
### Scanner audit portfolio (2026-05-27)

View file

@ -1,340 +0,0 @@
---
name: tighten-scan-identify-hot-path
classification: feature
success_metric: |
After ship, a new scan_attempts window shows L1 escalate ≤40% of L1
(was 76.7%), L1 matched ≥25% of L1 (was 5.8%), and first identify
attempt starts in ≤1.2s of tracking (was 3.5s). L2 p50 stays ≤1.8s.
skip:
- ia
- ui-design
- visual
- flag
status: shipped
created: 2026-08-14
depends_on:
- add-real-ocr-layer
umbrella: scanner-identify-upgrade
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
---
# Convoy: tighten-scan-identify-hot-path
Phase 1 of `scanner-identify-upgrade`. Make the **existing** two-layer
pipeline faster and more unique-match capable. No new vendors, no
detection rewrite, no embeddings.
## Why
Live telemetry (see umbrella) says Layer-1 is fast (p50 158ms) and
almost useless as an identifier (5.8% auto-match, 76.7% escalate). The
user still waits 3.5s of hold-still before that cheap path runs. L2
Gemini is 1.7s p50 and spends a second token on every disambiguation
refine. Collector number — the unique printing key L2 already uses —
is never read on L1.
## Scope
### In scope
- **Hold-still constants** in `lib/scanner-card-detection.js`:
`MIN_FIRST_SEEN_MS_FOR_VERIFY` 2500 → ~800;
`MIN_STABLE_COUNT_FOR_VERIFY` 6 → 3. Keep tests in
`test/lib/scanner-card-detection.test.js` in lockstep.
- **Name-strip OCR mode:** Tesseract PSM 7 (single line), JPEG quality
0.92 on the crop used for OCR (`lib/ocr-worker.js`,
`captureCardRegionFromVideo` quality if shared).
- **Collector-number strip:** second crop of the bottom ~18% of the
card; pass number (+ optional set hint) into `matchTextInCatalog`.
When name + number uniquely match a `cards` row, auto-match even if
many printings share the name.
- **Structured Gemini output** in `lib/scan-vision.js`:
`response_format` JSON schema (or Gateway equivalent). Delete the
confidence-30 regex fallback as the success path; keep a hard parse
error → `needs_input`.
- **Model A/B:** default stays `google/gemini-2.5-flash-lite`. Document
and allow `SCAN_VISION_MODEL=google/gemini-3.1-flash-lite` (GA on
Vercel AI Gateway, ~2.5× input price). Do not flip prod default in
this convoy unless Architect + a short identify bake-off say so.
- **Stop automatic L2 refine** in `use-scanner-identification.js` when
L1 already returned a printing list. User picks; Gemini refine is
opt-in or only when L1 had no set/number hint.
- **`docs/SCHEMA_MAP.md`:** `scan_attempts.layer` is 1 = Tesseract
(shipped), not "Tesseract future".
### Out of scope
- Detector / OpenCV / YOLO / perspective warp →
`improve-scan-card-detection`.
- pgvector / CLIP / catalog embeddings →
`scan-visual-catalog-search`.
- Changing `checkScanRateLimit` (5/min). Revisit after re-measure.
- Scanner chrome, cart, checkout (`scanner-mobile-checkout`).
- Replacing `tesseract.js`.
## Roles invoked
1. `role-ux-reviewer` — hold-still feel; when the picker still appears;
no new screens.
2. `role-architect` — ratify constant values, L1 match rules when a
collector number is present, JSON schema shape, whether the model
default flips. Write briefs + `slice_dependencies`.
3. `role-implementer` — per brief.
4. Audit: `role-reviewer` + `role-security-auditor`. Skip design-system
+ a11y unless a brief unexpectedly touches `components/`.
## Todos
- [x] UX: confirm 800ms / 3-frame gate does not cause double-scans
- [x] Architect: briefs + match-rule when number is present
- [x] Brief A — detection constants + tests
- [x] Brief B — OCR strips (name PSM 7 + collector number) +
`card-text-match.js` / `identify-by-text`
- [x] Brief C — `scan-vision.js` JSON schema + optional model id
- [x] Brief D — skip automatic disambiguation Gemini refine
- [x] SCHEMA_MAP layer-1 wording
- [x] Re-query `scan_attempts` after preview traffic (2026-08-15 — metrics unvalidated; see umbrella § Post-ship telemetry)
## Post-ship (PR #156, 2026-08-14)
**Shipped:** faster verify gates, collector-number L1, structured Gemini JSON,
skip L1 disambiguation refine.
**Telemetry (n=250 all-time):** L1 matched 5.4% (target ≥25%); L1 escalate
78.4% (target ≤40%). Gate latency goal met (~158 ms L1 p50). Aug 15 session
(n=26) showed 0% L1 match — OCR strips got noisier post-warp, not better.
**Verdict:** Machinery shipped; L1 text path remains insufficient for the
umbrella 50% auto-match goal. Phase 3 visual kNN is the intended fix.
## Likely file ownership (Architect will lock)
| Area | Files |
| --- | --- |
| Gates | `lib/scanner-card-detection.js`, `test/lib/scanner-card-detection.test.js` |
| OCR | `lib/ocr-worker.js` |
| L1 match | `lib/card-text-match.js`, `pages/api/cards/identify-by-text.js` |
| L2 vision | `lib/scan-vision.js`, `pages/api/scan/identify.js` |
| Refine | `lib/use-scanner-identification.js`, `lib/scanner-card-identify.js` |
| Docs | `docs/SCHEMA_MAP.md` |
Briefs A and C are file-disjoint and can run in parallel. B depends on
nothing if it does not retouch detection constants. D depends on B
(needs L1 payload shape).
## Multitask dispatch
Planning: UX → Architect (serial).
After Architect, if `slice_dependencies` marks A+C `depends_on: []`:
```
/multitask role-implementer briefs A, C
```
Then B, then D.
Audit group id: `audit-tighten-scan-identify-hot-path-<pr>`.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `ci.yml` test | Fires — detection + identify unit tests change |
| `visual-diff.yml` | Should **not** fire if `components/` / `pages/` / `styles/` untouched |
| `preview-smoke.yml` | Fires |
| `forbidden-client-side-llm-keys` | Must stay green — no client Gemini URLs |
## Operator action
Optional: set `SCAN_VISION_MODEL=google/gemini-3.1-flash-lite` on a
preview to bake off against 2.5 Flash Lite. Leave prod on 2.5 until
the bake-off.
## Conductor notes
Do not treat Tesseract replacement as in-scope if L1 escalate stays
high after collector-number matching — that is a crop/detection
problem (#2) or a visual-search problem (#3), not an OCR-engine
problem. The six historical L1 matches already show Tesseract can
read a name when the crop is clean; it cannot pick a printing.
## UX
No new routes or screens. Changes are timing + fewer automatic Gemini
calls during disambiguation.
### Existing components to reuse
- `ScannerDisambiguation` (`components/scanner/ScannerDisambiguation.js`) —
printing picker when L1/L2 cannot unique-match.
- `ScannerToast` / status copy around tracked card (`components/scanner/ScannerCamera.js`)
— continue showing detecting → verifying → found without new chrome.
- `ScanDisambiguationDialog` pattern if still referenced — do not fork a
second picker.
### Design direction alignment
Liquid Glass tokens unchanged. No new overlays. Faster identify should
feel like the camera "wakes up" sooner (H1 visibility of system status).
### Patterns to follow
- Disambiguation stays a bottom sheet / modal pause (`verificationPausedRef`
already wired in `use-scanner-identification.js`).
- Error toasts debounced via `reportScannerError` (4s gap) — keep that
when tightening gates so double-failures do not spam.
### A11y constraints
- No new interactive controls in this convoy.
- Existing disambiguation list must remain keyboard-selectable (hand off
unchanged to a11y auditor if Brief D touches the sheet).
### Interaction patterns
| Pattern | Requirement | Heuristic |
| --- | --- | --- |
| Hold-still gate | **required** — 800ms + 3 stable frames; user sees brackets sooner | H1 feedback |
| Auto-match | **required** — collector number should skip picker for multi-printing names | H6 recognition vs recall |
| Disambiguation refine | **required** — stop silent second Gemini call when L1 already opened picker; user tap only | H3 user control |
| Rate-limit toast | **required** — unchanged copy when L2 throttled | H9 error recovery |
| Loading during verify | **nice-to-have** — optional subtle "Reading…" on card status if trivial | H1 |
### Anti-patterns
- Adding a setup screen or settings toggle for gate timing (H4 consistency).
- Auto-picking a printing without number evidence when multiple exact-name
rows exist (H6).
- Blocking the camera on L1 OCR worker load (H1 — worker stays async).
### Mobile / responsive
Gate timing applies equally on phone and desktop; no viewport-specific
branches in this convoy.
## Architecture
### File plan
| File | Action | Purpose |
| --- | --- | --- |
| `lib/scanner-card-detection.js` | modified | 800ms / 3-frame verify gates |
| `test/lib/scanner-card-detection.test.js` | modified | Lock new constants |
| `lib/ocr-worker.js` | modified | PSM 7 name strip + bottom collector strip |
| `lib/card-text-match.js` | modified | Name + collector number unique match |
| `pages/api/cards/identify-by-text.js` | modified | Accept `cardNumber` body field |
| `test/lib/card-text-match.test.js` | new | Unit tests for number path (mock-free helpers) |
| `lib/scanner-card-identify.js` | modified | Pass number to L1; JPEG 0.92; `fromLayer1` flag |
| `lib/scan-vision.js` | modified | JSON schema response; stricter parse |
| `pages/api/scan/identify.js` | modified | Handle vision parse failures → needs_input |
| `lib/use-scanner-identification.js` | modified | Skip auto Gemini refine when `fromLayer1` |
| `docs/SCHEMA_MAP.md` | modified | Layer 1 = Tesseract (shipped) |
### API surface
**POST `/api/cards/identify-by-text`** (modified)
- Auth: `getUserFromRequest` → 401
- Body: `{ ocrText, ocrConfidence?, cardNumber?, game? }`
- Response: unchanged envelope + optional `cardNumber` echo in `ocr` meta
- Rate limit: none (unchanged)
**POST `/api/scan/identify`** (modified behavior only)
- On vision JSON parse failure: 200 with `needsUserInput: true` instead of
silent regex fallback at confidence 30
### Schema diff
No migration. `scan_attempts.layer` semantics documented only.
### Test plan
- Update `scanner-card-detection.test.js` thresholds (800ms, stable 3).
- New `card-text-match.test.js` for `extractCollectorNumberCandidate` and
collector-aware disambiguation resolution (pure functions exported for test).
- Extend `scanner-card-identify.test.js` for `fromLayer1` disambiguation
payload if added to pure helpers.
- Run `npm run test:run` + `npm run lint`.
### Risks
| Risk | Mitigation |
| --- | --- |
| Faster gate double-scans same card | `scanAttempts < 1` + tracker negative state unchanged |
| OCR number strip reads set symbol garbage | Normalize with `extractCollectorNumberCandidate`; fall back to name-only |
| Gateway rejects `response_format` | Catch 400, log once, fall back to prose prompt (Architect: implementer may feature-detect) |
| L1 skip-refine hides catalog-gap auto-submit | Refine still runs for L2 disambiguation only |
### Decomposition
| Brief # | Title | Files | Depends on | Size |
| --- | --- | --- | --- | --- |
| 1 | Faster verify gates | detection + test | — | S |
| 2 | Collector number L1 | ocr-worker, card-text-match, identify-by-text, scanner-card-identify, test | — | M |
| 3 | Structured vision JSON | scan-vision, identify API | — | S |
| 4 | Skip L1 disambiguation refine | use-scanner-identification, SCHEMA_MAP | 2 | S |
### Slice dependencies
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- lib/scanner-card-detection.js
- test/lib/scanner-card-detection.test.js
- brief: 2
depends_on: []
files:
- lib/ocr-worker.js
- lib/card-text-match.js
- pages/api/cards/identify-by-text.js
- lib/scanner-card-identify.js
- test/lib/card-text-match.test.js
- brief: 3
depends_on: []
files:
- lib/scan-vision.js
- pages/api/scan/identify.js
- brief: 4
depends_on: [2]
files:
- lib/use-scanner-identification.js
- docs/SCHEMA_MAP.md
```
### Decisions (post-UX)
| # | Decision |
| --- | --- |
| D1 | Gate values: `MIN_FIRST_SEEN_MS_FOR_VERIFY = 800`, `MIN_STABLE_COUNT_FOR_VERIFY = 3`. |
| D2 | Collector strip: bottom 18% of crop; PSM 7 for both strips. |
| D3 | When exact-name multiple printings AND parsed collector number matches exactly one row → auto-match. |
| D4 | `SCAN_VISION_MODEL` default stays `google/gemini-2.5-flash-lite`; structured JSON either way. |
| D5 | Auto Gemini refine skipped when disambiguation opened from L1 (`result.layer === 1`). |
Human gate 1: **approved** (operator requested full pipeline run 2026-08-14).

View file

@ -1,22 +0,0 @@
---
convoy: tighten-scan-identify-hot-path
brief_number: 1
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/scanner-card-detection.js
- test/lib/scanner-card-detection.test.js
---
# Brief 1: Faster verify gates
## Goal
Cut hold-still wait from 3.5s to ~1.4s by lowering verify gate constants.
## Acceptance criteria
- [ ] `MIN_FIRST_SEEN_MS_FOR_VERIFY = 800`
- [ ] `MIN_STABLE_COUNT_FOR_VERIFY = 3`
- [ ] Tests updated and green

View file

@ -1,27 +0,0 @@
---
convoy: tighten-scan-identify-hot-path
brief_number: 2
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/ocr-worker.js
- lib/card-text-match.js
- pages/api/cards/identify-by-text.js
- lib/scanner-card-identify.js
- test/lib/card-text-match.test.js
---
# Brief 2: Collector number on Layer 1
## Goal
OCR name + collector number strips; unique-match printings when number resolves one row.
## Acceptance criteria
- [ ] PSM 7 on name strip; bottom 18% number strip
- [ ] `matchTextInCatalog` accepts `cardNumber`
- [ ] identify-by-text passes `cardNumber`
- [ ] JPEG capture quality 0.92 for OCR path
- [ ] Unit tests for number extraction helper

View file

@ -1,22 +0,0 @@
---
convoy: tighten-scan-identify-hot-path
brief_number: 3
depends_on: []
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/scan-vision.js
- pages/api/scan/identify.js
---
# Brief 3: Structured Gemini JSON
## Goal
Request JSON schema from AI Gateway; fail loud on parse errors.
## Acceptance criteria
- [ ] `response_format` json_schema on gateway call
- [ ] No confidence-30 regex success path
- [ ] identify route returns needsUserInput on parse failure

View file

@ -1,22 +0,0 @@
---
convoy: tighten-scan-identify-hot-path
brief_number: 4
depends_on: [2]
recommended_model: composer-2.5-fast
model_tier: fast
files:
- lib/use-scanner-identification.js
- docs/SCHEMA_MAP.md
---
# Brief 4: Skip L1 disambiguation refine
## Goal
Do not auto-call Gemini when L1 already opened the printing picker.
## Acceptance criteria
- [ ] `fromLayer1` on disambiguation state when `result.layer === 1`
- [ ] useEffect refine skipped when `fromLayer1`
- [ ] SCHEMA_MAP documents layer 1 = Tesseract

View file

@ -1,659 +0,0 @@
---
name: unify-glass-panel-surfaces
classification: feature
success_metric: |
Every panel-shaped surface in the app (modals, popovers, form
cards, dashboard widgets, page-level content cards) renders with
the same gradient-border corner-light treatment that the floating
chrome chips use — at the appropriate intensity tier for its
surface class (full intensity for chrome, subtle for data cards,
flat for opaque GPU-budget-constrained tiles like CardItem grid).
Verified by visual diff + a forbidden grep gate that prevents
reintroduction of bespoke `var(--glass-surface-*)` inline styles
outside the documented exception list.
skip:
- ia
status: closed
closed: 2026-06-04
queued_followup:
- migrate-button-input-mobilenav-to-glass-primitive
prs:
- 120 # Brief 6 — landing nav → .page-header-glass
- 121 # Brief 2 — auth form cards → .glass-panel-strong
- 122 # Brief 5 — retire .card; migrate 7 consumers
- 123 # Brief 1 — <GlassSurface> cornerLights prop
- 124 # Brief 3 — floating popovers → .glass-panel-strong
- 125 # Brief 4 — BulkSelectionToolbar + token sweep
- 127 # Brief 7 — forbidden-bespoke-glass-surface CI gate (Check 7/7)
created: 2026-06-04
depends_on:
- tone-down-card-corner-lights # PR #118 — establishes the subtle token tier
- design-sweep-pass # PR #117 — applies .glass-panel broadly
---
# Convoy: unify-glass-panel-surfaces
Follow-on to the `redesign-v2-from-mockups` umbrella and the
2026-06-04 corner-border-light refinement (PR #116) + design-sweep
(PR #117) + card-vibrancy reduction (PR #118).
## Why
The gradient-border corner-light pattern (introduced in PR #116 and
applied broadly in PR #117) is now the canonical surface treatment
for the app. But the audit run on 2026-06-04 found that several
panel-shaped surfaces still use the previous generation's "flat
translucent fill" pattern — they predate the corner-light work and
were never migrated:
1. **`<GlassSurface>` primitive** — the most consequential gap.
`components/ui/GlassSurface.js` uses a single flat
`var(--glass-surface-${tint})` background with no transparent
border and no corner radials. Because `<Modal>` (and therefore
every modal in the app), `<StatCard>` (dashboard stat tiles),
and the public landing-page feature/collection cards all delegate
to `<GlassSurface>`, the gap cascades broadly. Upgrading the
primitive fixes ~10 visible surfaces in one move.
2. **Auth form cards** (`pages/login.js` L8288,
`pages/signup.js` L227232) — handrolled glass imitation using
`rgba(--bg-secondary-rgb, 0.85)` + `backdrop-blur-sm` (Tailwind,
not the system blur tokens). These are the first surfaces a new
user sees; they should be canonical, not handrolled.
3. **Floating popovers / drawers**
- `components/Layout.js` mobile drawer (L698709)
- `components/Layout.js` sidebar profile dropdown (L8896)
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214222)
All three render a translucent panel over arbitrary page content.
None has the gradient-border treatment. The visual cue that
says "this is an elevated surface" relies entirely on box-shadow,
not on light response.
4. **`BulkSelectionToolbar`** (L47 and L117 dropdown) — uses
hardcoded `bg-white border-gray-200`, invisible in dark mode.
This is a floating toolbar over all content during bulk-select;
should be `.glass-panel-strong`.
5. **`.card`-class consumers** — `pages/profile.js` (×3),
`pages/settings.js` (×2), `pages/community/collections.js`,
`components/CollectionsPageView.js`. The `.card` class
(`styles/globals.css` L757) is a pre-redesign opaque
solid-fill panel. Most consumers should migrate to
`.glass-panel`; the class itself can either be retired or
retained as a documented "opaque fallback" for special cases.
6. **Landing nav bar** (`pages/index.js` L6172) — uses
`var(--glass-surface-mid)` flat + an incorrectly double-wrapped
`inset 0 1px 0` boxShadow. Not a gradient-border candidate
(full-bleed bar; corner lights would be at viewport edges, not
visible). Should use the existing `.page-header-glass` class
instead.
Unifying these means the entire app shares one surface vocabulary.
Future work — new modals, new dashboards, new public pages — picks
up the gradient-border treatment automatically because the
primitives are correct.
## Scope
### In scope
#### Brief 1 — Upgrade `<GlassSurface>` primitive
Add the 4-layer gradient-border pattern to the `<GlassSurface>`
component so its `tint`, `blur`, `rim`, `elevation` props compose
with corner catch-lights. Default to the **subtle** corner-light
tokens (`--corner-light-{warm,cool}-subtle`) since most consumers
are data cards. Add a `cornerLights="chrome" | "subtle" | "none"`
prop so floating chrome can opt up and special opaque tiles
(CardItem grid mode) can opt out.
Consumers automatically upgraded:
- `<Modal>` → corner lights on every modal
- `<StatCard>` → corner lights on dashboard stat tiles
- Landing feature cards (`pages/index.js` L132224, L278345)
**Files:** `components/ui/GlassSurface.js`,
`test/components/ui-primitives.test.js` (add a corner-light
assertion).
**Risk:** MED — the primitive's output structure changes (adds
`border: 1px solid transparent`). Most consumers won't notice, but
any consumer that set a custom `border` via style override could
conflict. Audit needed.
#### Brief 2 — Auth form cards
`pages/login.js` L8288 + `pages/signup.js` L227232: replace
the inline `className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm
border border-opacity-20" style={{ backgroundColor: 'rgba(...)' }}`
shape with `className="glass-panel-strong rounded-2xl p-8"`.
**Files:** `pages/login.js`, `pages/signup.js`.
**Risk:** LOW.
#### Brief 3 — Floating popovers / drawers
Three independent floating surfaces:
- `components/Layout.js` mobile drawer (L698709)
- `components/Layout.js` sidebar profile dropdown (L8896)
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214222)
For each: drop the inline `background: var(--glass-surface-*)` +
`backdropFilter` pair, add `className="glass-panel-strong rounded-2xl"`
or `rounded-xl` to match existing dimensions. Preserve any
additional `boxShadow: 'var(--ember-rim-subtle)'` adornments by
chaining them onto the class's existing box-shadow (via inline
style override).
**Special handling for mobile drawer:** the drawer takes ~1/3 of
the viewport. At full corner-light intensity it would be noisy.
Use `.glass-panel-strong` (which uses the subtle tokens).
**Files:** `components/Layout.js`, `components/ui/TopSearchBar.js`,
`test/components/Layout.test.js` (regression assertions).
**Risk:** LOWMED — popovers appear over arbitrary page content.
#### Brief 4 — `BulkSelectionToolbar`
Replace `bg-white border-gray-200` on the toolbar (L47) and its
"More actions" dropdown (L117) with `.glass-panel-strong rounded-2xl`.
Sweep interior `text-gray-700 hover:bg-gray-100`,
`text-red-600 hover:bg-red-50` to use `var(--text-primary)` /
`nav-item-hover` and tokenized red for destructive actions.
**Files:** `components/BulkSelectionToolbar.js`.
**Risk:** MED — floats over all content during bulk-select mode;
shadow/overflow bleed would be very visible.
#### Brief 5 — `.card`-class consumers
Audit every `<div className="card">` usage. For each, pick the
right migration:
- Most likely: `glass-panel rounded-3xl p-6` (preserves rounded-3xl
+ p-6 from the old class).
- Some may need to stay opaque (e.g. screenshot-friendly profile
card with photo overlay) — keep `.card` and document the
exception in `styles/globals.css`'s comment block.
Decision to ratify: **retire `.card` entirely, OR keep as a
documented opaque-panel alternative?** Architect's call. If kept,
add a comment to `globals.css` explaining when to use which.
**Files:** `pages/profile.js`, `pages/settings.js`,
`pages/community/collections.js`,
`components/CollectionsPageView.js`,
optionally `styles/globals.css` (retire or document).
**Risk:** LOW per page; sweep across 4 files.
#### Brief 6 — Landing nav bar
`pages/index.js` L6172: replace the handrolled translucent header
with `<header className="page-header-glass border-b ...">`. The
`.page-header-glass` class already exists in `globals.css` for
exactly this "full-bleed top band" use case. The current bar also
has a malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'`
(the token already contains `inset 0 1px 0`; double-wrapping breaks
the cascade). Remove the malformed shadow.
**Files:** `pages/index.js`.
**Risk:** LOW — public page, no auth dependencies.
#### Brief 7 — Forbidden grep gate
Add a `forbidden-bespoke-glass-surface` job to
`.github/workflows/ci.yml` that fails the build if
`var(--glass-surface-(low|mid|high))` appears in JSX inline styles
across `components/**` and `pages/**`, with a curated allowlist
for the documented exceptions (intentional chrome treatment in
Layout/TopSearchBar; `<GlassSurface>` component itself; any
opaque-fallback `.card` consumers ratified in Brief 5).
Prevents regression: future PRs can't reintroduce handrolled glass.
**Files:** `.github/workflows/ci.yml`.
**Risk:** LOW.
### Out of scope
- `<CardItem>` grid-mode (L290303) — intentionally opaque per
AGENTS.md GPU-budget rule.
- `<CardItem>` list-mode token cleanup (L183284) — covered by
the parallel `cleanup-card-item-list-and-share-modal-palette`
convoy.
- Adding any NEW surfaces or panels.
- `.card` rename to `.opaque-panel` — Brief 5 may retire the class
entirely; rename is a separate polish if it survives.
## Roles invoked
1. `role-architect` — surface-by-surface migration plan; ratifies
the `cornerLights` prop shape for `<GlassSurface>` and the
`.card` retire-vs-keep decision; writes Briefs 17.
2. `role-design-system-auditor` — visual regression review on each
brief; confirms no chrome-vs-data-card hierarchy regressions.
3. `role-ux-reviewer` — light pass; ensures auth form contrast +
mobile drawer legibility hold up post-migration.
4. `role-implementer` — one per brief; Briefs 2, 3, 4, 6 are
parallel-safe (disjoint files, no shared component dependency
chain). Brief 1 MUST land first because Briefs 3, 4 may end up
simplifying their inline-style code by using the upgraded
primitive instead.
5. `role-reviewer` — single post-PR review per brief.
6. `role-a11y-auditor` — focus-ring + keyboard nav for new floating
surfaces (Brief 3 popovers especially).
## Todos
- [ ] Architect: surface-by-surface migration plan + `<GlassSurface>`
API spec (the `cornerLights` prop)
- [ ] Design-system auditor: confirm subtle-tokens are the right
default for the upgraded `<GlassSurface>`
- [ ] Brief 1 — `<GlassSurface>` primitive upgrade (BLOCKING for
Briefs 3, 4)
- [ ] Brief 2 — auth form cards
- [ ] Brief 3 — floating popovers (drawer, sidebar profile,
UserMenu)
- [ ] Brief 4 — BulkSelectionToolbar
- [ ] Brief 5 — `.card` consumers; Decision: retire or keep
- [ ] Brief 6 — landing nav bar (`.page-header-glass`)
- [ ] Brief 7 — `forbidden-bespoke-glass-surface` CI gate
- [ ] Post-PR review per brief
- [ ] Visual-diff baseline refresh after Brief 1 lands
## Decisions to ratify
1. **`<GlassSurface>` API shape post-upgrade.** Add `cornerLights`
prop with values `"chrome" | "subtle" | "none"`. Default
`"subtle"`. Architect confirms or proposes alternative.
2. **Retire `.card` class entirely after Brief 5, or keep as
documented opaque fallback?** Conservative: keep + document
("use when the surface must NOT have backdrop-filter — e.g.
inside another modal, screen-reader-critical, or
GPU-budget-constrained"). Aggressive: retire and migrate the
handful of legitimate opaque cases to inline style.
3. **Mobile drawer corner-light intensity.** The drawer is a
large surface. Subtle tokens (matching `.glass-panel-strong`)
are likely right, but the architect should sanity-check
visually before locking it in.
4. **CI gate scope for `forbidden-bespoke-glass-surface`.** Should
it gate ALL `var(--glass-surface-*)` usage in JSX, or only
inline `style={{ background: ... }}` usage? Recommended: only
inline styles, since the tokens still need to be referenceable
in `styles/globals.css`.
## Acceptance criteria
1. Every in-scope surface lists either `glass-panel`,
`glass-panel-strong`, `page-header-glass`, or `<GlassSurface>`
in its className (no inline `var(--glass-surface-*)` background).
2. `<GlassSurface>` primitive's output includes the 4-layer
gradient-border pattern and a `border: 1px solid transparent`.
3. CI's `forbidden-bespoke-glass-surface` job passes; introducing
a new bespoke `var(--glass-surface-low)` inline-style usage in
a scratch commit makes it fail (negative test).
4. Build + lint + 113/113 vitest + Playwright smoke green.
5. Visual diff shows the expected differences (corner catch-lights
appear on modals, dashboard stat tiles, auth cards, dropdowns)
and no unexpected regressions on chrome/cards/CardItem grid.
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `preview-smoke.yml` | Fires per brief. |
| `visual-diff.yml` | **Fires + baseline refresh required** after Brief 1 lands (the primitive upgrade ripples through Modal/StatCard/landing). |
| `lint` | Fires + new `forbidden-bespoke-glass-surface` gate after Brief 7. |
| `test:` (vitest) | Fires; Brief 1 adds a corner-light assertion to `ui-primitives.test.js`. |
## Known constraints
- **No-go zones honoured**`components/Layout.js.backup`,
`scripts/add-*.js` graveyard untouched.
- **GPU budget for CardItem grid** — AGENTS.md explicitly
prohibits `backdrop-filter` per card thumbnail (it
multiplies); Brief 1 must NOT default `<GlassSurface>` to
any rendering that would pull CardItem into that prohibition.
## Multitask dispatch
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- components/ui/GlassSurface.js
- test/components/ui-primitives.test.js
- brief: 2
depends_on: []
files:
- pages/login.js
- pages/signup.js
- brief: 3
depends_on: [1] # may simplify by using upgraded primitive
files:
- components/Layout.js
- components/ui/TopSearchBar.js
- test/components/Layout.test.js
- brief: 4
depends_on: [1]
files:
- components/BulkSelectionToolbar.js
- brief: 5
depends_on: []
files:
- pages/profile.js
- pages/settings.js
- pages/community/collections.js
- components/CollectionsPageView.js
- styles/globals.css # if .card is retired / documented
- brief: 6
depends_on: []
files:
- pages/index.js
- brief: 7
depends_on: [1, 2, 3, 4, 5, 6] # gate goes in LAST
files:
- .github/workflows/ci.yml
```
Briefs 2, 5, 6 can run in parallel with Brief 1. Briefs 3, 4 wait
for Brief 1 to land so they can simplify by using the upgraded
primitive. Brief 7 runs LAST so the grep gate doesn't fail the
build on in-flight migrations.
## Out of scope follow-ups (queued)
- **`retire-or-formalize-card-class`** — if Brief 5 Decision 2
keeps `.card` as opaque fallback, a future small convoy can
rename it `.opaque-panel` for clarity. P3.
- **`storybook-adoption-for-glass-surface`** — once the primitive
has the full prop matrix (`tint`, `blur`, `rim`, `elevation`,
`cornerLights`), it's a natural Storybook candidate. P2 DX.
## Architecture
Run date: 2026-06-04. Architect: role-architect (this convoy).
Reads: this convoy file, `AGENTS.md`, `.cursor/rules/*.mdc`,
`components/ui/GlassSurface.js`, `components/ui/Modal.js`,
`components/ui/StatCard.js`, `components/Layout.js`,
`components/ui/TopSearchBar.js`, `components/BulkSelectionToolbar.js`,
`pages/login.js`, `pages/signup.js`, `pages/index.js`,
`pages/profile.js`, `pages/settings.js`,
`pages/community/collections.js`,
`components/CollectionsPageView.js`, `styles/globals.css`
(`.card`, `.glass-panel`, `.glass-panel-strong`, `.page-header-glass`,
the `--corner-light-*-subtle` tokens added in PR #118, the
`--bg-secondary-rgb` token used by today's auth cards),
`.github/workflows/ci.yml`, `test/components/ui-primitives.test.js`,
`test/components/Layout.test.js`.
### Decisions ratified
The convoy file's four open Decisions are resolved as follows.
Implementer briefs cite these and MUST NOT renegotiate them mid-flight
(if a brief discovers a reason to revisit, escalate via the mid-convoy
scope-expansion process in `role-architect.md`).
**D1. `<GlassSurface>` `cornerLights` prop shape.** Add
`cornerLights = 'subtle' | 'chrome' | 'none'`, **default `'subtle'`**.
- `'subtle'` → applies the 4-layer gradient with
`--corner-light-warm-subtle` / `--corner-light-cool-subtle` (matches
what `.glass-panel-strong` ships today; PR #118).
- `'chrome'` → applies the 4-layer gradient with full-intensity
`--corner-light-warm` / `--corner-light-cool`. Used by the Layout
sidebar nav-chip and TopSearchBar header — those keep their inline
styles for now; this prop value exists so future floating chrome
doesn't have to re-handroll the gradient.
- `'none'` → no transparent border, no radials, no `--chip-border-base`
layer. Identical to today's primitive output. Used for the CardItem
grid path and any other GPU-budget-constrained tile that legitimately
must skip the gradient-border treatment.
The gradient-border technique is the verbatim 4-layer recipe from
`.glass-panel-strong` (`styles/globals.css` post-PR-#118): a
padding-box solid linear-gradient of the fill, two border-box radial
gradients for the corner catch-lights (warm 0%↔100%, cool 100%↔0%),
and a border-box `var(--chip-border-base)` base layer. Combined with
`border: 1px solid transparent` so the gradient renders through the
border.
**D2. `.card` class retire-or-keep.** **Retire the class entirely.**
All 8 consumers migrate to `glass-panel rounded-3xl p-{6|4}`. The
`.card` block in `styles/globals.css` is deleted. Rationale: a single
glass vocabulary across the app is the convoy's success metric;
keeping `.card` as a documented opaque fallback creates two parallel
panel languages for future agents to choose between, which is exactly
the kind of design-system bifurcation the convoy was scoped to
eliminate. The GPU-budget concern that motivated keeping `.card` is
specific to `<CardItem>` grid-mode (a non-`.card` consumer) — none
of the 8 `.card` sites face that constraint (they're profile cards,
settings panels, and a single community-page list tile, not a
multiplied-per-thumbnail surface).
Aggressive path; **risk** = one of the 8 sites has a hidden reason
to be opaque that the architect didn't catch. Mitigation: Brief 5
includes a "if any consumer breaks visually post-migration, revert
that one consumer to inline `style={{ background:
'var(--bg-secondary)' }}` and document the exception in the brief's
post-merge note" escape hatch. That keeps the class deletion in the
PR while leaving an explicit per-site fallback. If that escape hatch
fires for more than 1 of the 8 consumers, the brief's reviewer is
expected to push back and propose holding `.card` after all.
**D3. Mobile-drawer corner-light intensity.** Use
`.glass-panel-strong` (subtle tier). The drawer is a ~256px-wide
fixed-position surface that covers ~1/3 of a mobile viewport. At
full-intensity (`'chrome'`) the corner radials would dominate the
drawer's interior nav text; at subtle they read as "elevated panel"
without competing with content. The drawer is also a hidden surface
most of the time — full intensity buys nothing for the rare moments
it's open.
**D4. CI gate scope.** Gate **inline-style usage only**
`var(--glass-surface-(low|mid|high))` appearing inside a JSX
`style={{ background: ... }}` (or `backgroundColor:`, `background:`
in template-string form) under `components/**` or `pages/**`. CSS
class definitions in `styles/globals.css` (where the tokens are
LEGITIMATELY chained to compose `.glass-panel` / `.glass-panel-strong`
/ `.page-header-glass`) are not gated. Allowlist entries: the
Layout sidebar nav-chip block (`components/Layout.js` L858-861) and
the TopSearchBar header block (the equivalent inline-style block in
`components/ui/TopSearchBar.js`) — both intentionally retain
`'chrome'`-tier handrolled gradients and were ratified as such in
PR #116. The `<GlassSurface>` primitive itself is also allowlisted
(it sets the background internally; that's its job). Brief 7 ships
this gate as a separate `forbidden-bespoke-glass-surface` job in
`ci.yml`, modeled on the existing `forbidden-modal-shell-without-primitive`
gate's grep-and-allowlist shape.
### File plan
| File | Action | Purpose | Brief |
| --- | --- | --- | --- |
| `components/ui/GlassSurface.js` | modified | Add `cornerLights` prop (`'subtle' \| 'chrome' \| 'none'`, default `'subtle'`); compose 4-layer gradient + `border: 1px solid transparent` when not `'none'` | 1 |
| `test/components/ui-primitives.test.js` | modified | Add 3 corner-light assertions covering the 3 `cornerLights` values | 1 |
| `pages/login.js` | modified | Replace L82-88 inline glass imitation with `<div className="glass-panel-strong rounded-2xl p-8">` | 2 |
| `pages/signup.js` | modified | Replace L227-232 inline glass imitation with `<div className="glass-panel-strong rounded-2xl p-8">` | 2 |
| `components/Layout.js` | modified | Sidebar profile dropdown (L88-96): drop inline background+blur+shadow, add `className="glass-panel-strong rounded-xl"`. Mobile drawer (L698-709): drop inline background+blur+shadow, add `className="glass-panel-strong"` and keep the slide-in transform classes. Sidebar nav-chip block (L858-861) untouched (chrome tier). | 3 |
| `components/ui/TopSearchBar.js` | modified | UserMenu dropdown (L214-222): drop inline background+blur+shadow, add `className="glass-panel-strong rounded-xl"`. Header block (chrome tier) untouched. | 3 |
| `test/components/Layout.test.js` | modified | Add regression-lock assertion: sidebar profile dropdown and mobile drawer carry `.glass-panel-strong` class. | 3 |
| `components/BulkSelectionToolbar.js` | modified | L47 toolbar: replace `bg-white border-gray-200` with `glass-panel-strong rounded-2xl`. L117 dropdown: same swap on `rounded-lg``rounded-xl glass-panel-strong`. Sweep interior `text-gray-{600,700}` to `var(--text-primary/secondary)`; `hover:bg-gray-{50,100}` to `nav-item-hover`; `text-red-600 hover:bg-red-50` to `var(--accent-danger)` + ember-tinted hover. | 4 |
| `pages/profile.js` | modified | 3 `.card``glass-panel rounded-3xl p-6` swaps. | 5 |
| `pages/settings.js` | modified | 2 `.card``glass-panel rounded-3xl` swaps (one `p-4`, one `p-6`). | 5 |
| `pages/community/collections.js` | modified | 1 `.card``glass-panel rounded-3xl` swap (preserve `group cursor-pointer hover:shadow-lg transition-all`). | 5 |
| `components/CollectionsPageView.js` | modified | 1 `.card``glass-panel rounded-3xl` swap (preserve `hover:shadow-xl transition-all cursor-pointer group`). | 5 |
| `styles/globals.css` | modified | **Delete** the `.card { ... }` rule (the class block that lives near L757). No other CSS changes. | 5 |
| `pages/index.js` | modified | Landing nav bar (L61-72): replace handrolled inline style with `<nav className="page-header-glass border-b">`. Drop the malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` (the `.page-header-glass` class already sets the correct inset rim). | 6 |
| `.github/workflows/ci.yml` | modified | Add `forbidden-bespoke-glass-surface` job per D4. | 7 |
### API surface
N/A. This convoy is pure UI/CSS surface composition. No new routes,
no Zod schema additions, no rate-limit changes.
### Schema diff
N/A. No DB changes. No `migrations/` files added; no
`scripts/add-*.js` follow-up. The schema-map-fresh CI job will not
fire on any of these PRs (the conditional in `ci.yml` L47-52 checks
`migrations/` / `scripts/add-*` / `scripts/fix-*` /
`scripts/setup-neon-db.js` / `docs/SCHEMA_MAP.md` — none touched).
### Test plan
| Brief | Unit | Component | Smoke | Visual diff | Notes |
| --- | --- | --- | --- | --- | --- |
| 1 | — | **3 new assertions** in `test/components/ui-primitives.test.js` covering `cornerLights='subtle'` (default, expects `border: 1px solid transparent` in the rendered style attribute), `cornerLights='chrome'` (expects `--corner-light-warm` token reference, not `-subtle`), `cornerLights='none'` (expects no `border` declaration, no gradient layers — same shape as today). | Existing smoke tests cover Modal open/close; nothing new. | **Baseline refresh required** after Brief 1 lands — Modal/StatCard/landing feature cards all gain corner lights. Plan: queue the refresh as a single Linux-baselined commit immediately after Brief 1 merges, before Briefs 3/4 ship. | The most consequential brief; the assertions are the regression lock. |
| 2 | — | None needed (presentational class swap). | Existing `tests/smoke/auth.spec.js` covers login/signup happy path; nothing new. | Auth pages diff expected. | — |
| 3 | — | Regression-lock in `test/components/Layout.test.js`: render Layout with `isMobileMenuOpen={true}` (mocked) and assert the drawer container's `className` includes `glass-panel-strong`. Render with `<UserProfileDropdown>` open (mocked) and assert its dropdown container's `className` includes `glass-panel-strong`. | Existing smoke covers nav clicks. | Layout dropdown/drawer visual diff expected. | — |
| 4 | — | None needed (rare to bulk-select in a smoke test). | — | Expected visual diff on the bulk-select toolbar — but only fires when one of the visual-diff `tests/visual/*` specs actually triggers bulk-select. Probably won't surface in the diff at all; rely on manual QA + dark-mode side-by-side screenshot for review. | The dark-mode invisibility is the regression we're closing; verify manually post-merge. |
| 5 | — | None needed (className swap). | — | 4 pages worth of visual diff expected. | — |
| 6 | — | None needed. | Existing smoke `tests/smoke/landing.spec.js` covers landing nav rendering. | Expected on landing. | — |
| 7 | **Negative test** documented in the brief's acceptance criteria: an implementer dry-run that adds a scratch `style={{ background: 'var(--glass-surface-low)' }}` to a `components/` file should fail the new `forbidden-bespoke-glass-surface` job. Verified once by the architect via the boot-the-brief check below; not committed. | — | — | — | The grep gate runs in CI; no test code needed. |
Reference test files for the brief authors:
- `test/components/ui-primitives.test.js` (Button/Input/SearchBar test
patterns, jsdom env, cleanup pattern; Brief 1 extends in-file).
- `test/components/Layout.test.js` (5 regression-lock assertions
established by `fix-layout-default-user` PR #15; Brief 3 extends).
### Risk list
1. **Brief 1 ripple effect** (**MED**). The `<GlassSurface>` primitive
feeds `<Modal>`, `<StatCard>`, and 4-5 landing-page feature cards.
Adding `border: 1px solid transparent` to every consumer changes
the rendered geometry by 2px in both axes — most consumers don't
notice (their `padding`/`gap` absorbs it), but any consumer that
relied on a 0-border box-model for pixel-exact layout will shift.
The `<Modal>` panel sets `maxWidth` in pixels and is centered via
flex — it's not sensitive. `<StatCard>` is in a 4-up grid — also
not sensitive. The landing feature cards use `grid` with `gap`
— not sensitive. But the visual-diff baseline refresh is
non-negotiable; queue it before Briefs 3 + 4 ship.
2. **Brief 1 + GPU-budget constraint** (**LOW-MED**). AGENTS.md
forbids `backdrop-filter` per `CardItem` thumbnail. `<CardItem>`
does NOT use `<GlassSurface>` today, so adding gradient-border to
`<GlassSurface>` doesn't pull it in. The `'none'` cornerLights
value exists as a defensive escape hatch for any future tile
surface that DOES want `<GlassSurface>` without the gradient
layers. Brief 1's brief explicitly forbids changing CardItem
anywhere in this convoy.
3. **Brief 3 popovers' z-index + box-shadow chain** (**LOW-MED**).
`.glass-panel-strong` ships with
`var(--rim-light-inner), var(--elevation-ambient)`. Today's
inline-style on the profile dropdown adds
`var(--ember-rim-subtle)` as a third shadow for ember emphasis;
today's mobile drawer uses `var(--elevation-pronounced)` (heavier
elevation than ambient). The brief MUST preserve these via an
inline-style override chained onto the class's box-shadow — DON'T
just drop them. Verbatim shape documented in Brief 3.
4. **Brief 4 dark-mode regressions** (**LOW**). The bulk-select
toolbar is currently `bg-white` — invisible in dark mode. The
migration FIXES that regression but Brief 4's reviewer should
capture before/after dark-mode screenshots so the fix is
visible in PR review.
5. **Brief 5 `.card` deletion risk** (**LOW-MED, conditional**).
Per D2, all 8 consumers migrate to `.glass-panel`. If any one of
them breaks visually (the architect doesn't expect this; the
8 sites are all "panel with content inside" surfaces), the
brief includes an escape hatch to revert that one site to
inline `style={{ background: 'var(--bg-secondary)' }}` rather
than holding the whole class deletion. If MORE than 1 of 8
needs the escape hatch, the brief's reviewer is expected to
push back and propose keeping `.card`.
6. **Brief 6 `.page-header-glass` already-applied check** (**LOW**).
The implementer must verify `pages/index.js`'s nav bar isn't
already inside another `.page-header-glass` ancestor (it isn't —
landing has no shared Layout); otherwise nested
`backdrop-filter`s would compound.
7. **Brief 7 false-positive risk** (**LOW**). The grep gate must
correctly distinguish JSX inline-style usage of
`var(--glass-surface-*)` from CSS class definitions in
`styles/globals.css`. The architect-prescribed approach is to
restrict the grep to files under `components/**` and `pages/**`
(excluding `.css`), which by construction means JSX inline-style
only. Brief 7 includes a verbatim grep command shape and the
2-entry allowlist.
8. **PR #117 + PR #118 not yet merged** (**LOW, dependency**). This
convoy's `depends_on:` frontmatter lists both. If either fails
review, the briefs may need to refactor against a different
baseline. Recommended: hold Brief 1 dispatch until PR #117 + PR
#118 are both squash-merged to main.
### Decomposition
| Brief # | Title | Files | Depends on | LOC est. |
| --- | --- | --- | --- | --- |
| 1 | Upgrade `<GlassSurface>` primitive with `cornerLights` prop | `components/ui/GlassSurface.js`, `test/components/ui-primitives.test.js` | — | ~80 |
| 2 | Migrate auth form cards to `.glass-panel-strong` | `pages/login.js`, `pages/signup.js` | — | ~20 |
| 3 | Migrate floating popovers to `.glass-panel-strong` | `components/Layout.js`, `components/ui/TopSearchBar.js`, `test/components/Layout.test.js` | 1 | ~90 |
| 4 | Migrate `BulkSelectionToolbar` to `.glass-panel-strong` + token sweep | `components/BulkSelectionToolbar.js` | 1 | ~70 |
| 5 | Retire `.card` class; migrate 8 consumers to `.glass-panel` | `pages/profile.js`, `pages/settings.js`, `pages/community/collections.js`, `components/CollectionsPageView.js`, `styles/globals.css` | — | ~30 |
| 6 | Migrate landing nav bar to `.page-header-glass` | `pages/index.js` | — | ~10 |
| 7 | `forbidden-bespoke-glass-surface` CI gate | `.github/workflows/ci.yml` | 1, 2, 3, 4, 5, 6 | ~40 |
All briefs target <400 LOC; the largest (Brief 3) is ~90 LOC. Files
across briefs are disjoint — verified against the slice_dependencies
block above. No two parallel writers on the same file.
Parallel dispatch recommended for Briefs 1, 2, 5, 6 (no deps, disjoint
files). Briefs 3 + 4 wait for Brief 1 to land (per D1 — they may
simplify by adopting the upgraded primitive's `cornerLights='subtle'`
default rather than handrolling). Brief 7 runs LAST so the grep gate
doesn't fail the build on in-flight migrations.
### Boot-the-brief check
Read-only verification against the post-PR-#117 + post-PR-#118 tree.
1. **Dep-set check.** No new npm dependencies added by any brief. The
gradient-border recipe is pure CSS; the `cornerLights` prop is
pure React composition; the CI gate is a shell `grep` invocation
in a YAML job. No `pnpm view` / `package.json` audit needed.
2. **Verbatim code-shape check.**
- Brief 1's 4-layer recipe matches `.glass-panel-strong`'s current
shape verbatim — verified against `styles/globals.css` post-PR
#118 (the `background:` block uses
`linear-gradient(var(--glass-surface-high), ...) padding-box,
radial-gradient(at 0% 100%, var(--corner-light-warm-subtle) ...) border-box,
radial-gradient(at 100% 0%, var(--corner-light-cool-subtle) ...) border-box,
var(--chip-border-base) border-box`). The primitive's
`'chrome'` value uses the non-`-subtle` tokens; both pairs exist
in `:root` + `[data-theme="dark"]` (verified).
- Brief 3's box-shadow preservation: the sidebar profile dropdown
ships `var(--rim-light-inner), var(--ember-rim-subtle),
var(--elevation-ambient)`; the mobile drawer ships
`var(--rim-light-inner), var(--rim-light-outer),
var(--elevation-pronounced)`; the TopSearchBar UserMenu dropdown
ships `var(--rim-light-inner), var(--ember-rim-subtle),
var(--elevation-pronounced)` (verified during boot-the-brief
recheck — uses `-pronounced`, not `-ambient`, because it floats
higher in the viewport). All three are preserved as inline
`style={{ boxShadow: '...' }}` overrides on the
`.glass-panel-strong` element (the class's default
`var(--rim-light-inner), var(--elevation-ambient)` would otherwise
lose the ember-rim and either heavier elevation). Brief 3
documents the verbatim chain.
- Brief 5's class deletion: the `.card` block in
`styles/globals.css` is a single rule; deleting it is a clean
diff (verified by reading L750-L770 area — no other `.card`
compound selectors exist).
- Brief 7's grep command shape: modeled on the existing
`forbidden-modal-shell-without-primitive` job (`ci.yml`
L200-L226), which uses
`grep -lE 'pattern' pages components -r --include='*.js'`.
Same shape works here. Allowlist is implemented via
`! -path 'components/ui/GlassSurface.js'` and an explicit
line-range exclusion for `components/Layout.js` and
`components/ui/TopSearchBar.js` (the chrome blocks).
3. **Cross-brief commitments.** None. Each brief lands a complete
migration of its scope; no brief introduces a stub or forward
declaration that another brief resolves. Brief 7's grep gate
passes ONLY AFTER Briefs 1-6 land — that's a dispatch ordering
constraint (encoded in `depends_on:`), not a cross-brief
commitment in the role-architect sense.
All checks passed. No briefs need revision.

View file

@ -1,82 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 1
depends_on: []
files:
- components/ui/GlassSurface.js
- test/components/ui-primitives.test.js
---
# Brief 1: Upgrade `<GlassSurface>` primitive with `cornerLights` prop
## Goal (1 sentence)
Add a `cornerLights = 'subtle' | 'chrome' | 'none'` prop to `<GlassSurface>` and compose the 4-layer gradient-border pattern (padding-box fill + two border-box corner radials + `--chip-border-base` base) on top of the existing `tint` / `blur` / `rim` / `elevation` props so every consumer (Modal, StatCard, landing feature cards) picks up corner catch-lights automatically.
## Files in scope (do not edit anything else)
- `components/ui/GlassSurface.js` — add the prop, compose the 4-layer `background` and the `border: 1px solid transparent` when the prop is not `'none'`.
- `test/components/ui-primitives.test.js` — add 3 corner-light assertions to the existing `describe('GlassSurface', ...)` block (or add the block if it doesn't exist; today the file covers Button / Input / SearchBar only, so a new top-level `describe('GlassSurface', ...)` block at the end of the file is correct).
**Out of scope:** every consumer of `<GlassSurface>``components/ui/Modal.js`, `components/ui/StatCard.js`, `pages/index.js` landing cards. Their behavior changes via the new default; no edits required. Do NOT modify them in this PR.
## Conventions to follow
- **Verbatim 4-layer recipe** (mirrors `.glass-panel-strong` post-PR #118; see `styles/globals.css`):
```js
// When cornerLights === 'subtle' (default):
background:
`linear-gradient(var(--glass-surface-${tint}), var(--glass-surface-${tint})) padding-box,
radial-gradient(at 0% 100%, var(--corner-light-warm-subtle) 0%, transparent 42%) border-box,
radial-gradient(at 100% 0%, var(--corner-light-cool-subtle) 0%, transparent 42%) border-box,
var(--chip-border-base) border-box`
// When cornerLights === 'chrome':
// Same recipe; swap --corner-light-warm-subtle → --corner-light-warm
// and --corner-light-cool-subtle → --corner-light-cool.
// When cornerLights === 'none':
// Today's behavior — single layer background: var(--glass-surface-${tint}).
// No border declaration.
```
- **Border declaration**: when `cornerLights !== 'none'`, also set
`border: '1px solid transparent'` on the composed style. This is what
reveals the border-box gradient layers. When `'none'`, omit the
border entirely (preserve today's box-model).
- **Shadow stack**: unchanged. The existing `RIM_SHADOWS` /
`ELEVATION_SHADOWS` composition logic stays exactly as written. The
corner-light layers live in `background`, not `box-shadow`.
- **Default value**: `cornerLights = 'subtle'`. This is the new default
for every consumer that doesn't pass the prop explicitly.
- **Backdrop filter**: unchanged. `backdropFilter` /
`WebkitBackdropFilter` continue to compose from `blur` / `tint` exactly
as today. Do not touch this line.
- **`style` prop merging**: today the composed style spreads
`...style` LAST so caller overrides win. Preserve that exactly — a
caller passing `style={{ border: '1px solid #f00' }}` overrides the
transparent border. This is the explicit escape hatch any consumer
who needs a real border can use without setting `cornerLights='none'`.
- **AGENTS.md / no-go zones**: `styles/globals.css` is untouched in
this brief (Brief 5 owns the `.card` deletion; no other CSS edits in
this convoy). The `--corner-light-warm-subtle` /
`--corner-light-cool-subtle` / `--corner-light-warm` /
`--corner-light-cool` / `--chip-border-base` tokens all exist
in `:root` and `[data-theme="dark"]` per PR #118 — verified.
## Acceptance criteria
- [ ] `<GlassSurface>` accepts a `cornerLights` prop with values `'subtle'` (default), `'chrome'`, `'none'`. Any other value falls back to `'subtle'` (mirror the existing `RIM_SHADOWS[rim] ?? []` defensive fallback shape).
- [ ] When `cornerLights='subtle'`, rendered output includes:
- `background:` containing both `var(--corner-light-warm-subtle)` and `var(--corner-light-cool-subtle)`.
- `border:` value `1px solid transparent`.
- [ ] When `cornerLights='chrome'`, rendered output includes:
- `background:` containing `var(--corner-light-warm)` and `var(--corner-light-cool)` (NOT the `-subtle` variants).
- `border:` value `1px solid transparent`.
- [ ] When `cornerLights='none'`, rendered output is identical to today: single-layer `background: var(--glass-surface-${tint})`, no `border` declaration in the composed style.
- [ ] `tint` / `blur` / `rim` / `elevation` / `as` / `style` / `className` props all behave exactly as before. Existing consumers continue to compile + render without prop changes.
- [ ] `test/components/ui-primitives.test.js` has 3 new assertions (one per `cornerLights` value) verifying the inline style attribute or the rendered element's `style.background` / `style.border` accordingly. Use the existing `render` + `screen.getByRole` / `getByText` / `container` patterns from the file; don't pull in new test deps.
- [ ] `npm run lint` passes.
- [ ] `npm run test:run` is 22+/22+ (today's count is 21; this brief adds at least 3 — exact count depends on whether you split assertions across multiple `it(...)` blocks).
- [ ] No edits to files outside the two listed in `files:` above. **No consumer migrations in this PR.** Brief 3 + 4 will compose against the new default; Briefs 2 + 5 + 6 don't use the primitive.
## Rationale (≤3 sentences)
The primitive is the single largest leverage point in the convoy — upgrading it ripples through `<Modal>`, `<StatCard>`, and the landing-page feature cards in one commit. Defaulting to `'subtle'` matches the PR #118 token-tier decision and means downstream consumers don't need to opt in. The `'chrome'` + `'none'` values exist so the prop is future-proof: floating chrome can opt up without handrolling, and any GPU-constrained tile (e.g. a future <CardItem>-like surface) can opt out cleanly.

View file

@ -1,79 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 2
depends_on: []
files:
- pages/login.js
- pages/signup.js
---
# Brief 2: Migrate auth form cards to `.glass-panel-strong`
## Goal (1 sentence)
Replace the handrolled `rgba(var(--bg-secondary-rgb), 0.85)` + `backdrop-blur-sm` glass imitation on the login + signup form cards with the canonical `.glass-panel-strong rounded-2xl p-8` className so the auth flow shares the rest of the app's surface treatment (corner catch-lights, system blur tier, gradient border).
## Files in scope (do not edit anything else)
- `pages/login.js` — replace the form-card container `<div>` (currently L82-88, look for the `className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm border border-opacity-20"` + inline `backgroundColor: 'rgba(var(--bg-secondary-rgb), 0.85)'`).
- `pages/signup.js` — same migration on the form-card container (currently L227-232 in the symmetric `<div className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm ...">` block).
**Out of scope:** the surrounding layout/header on either page, the `<Input>` / `<Button>` children, error/success banners, any of the legend/divider/social-button styling below the form. Do not touch them.
## Conventions to follow
- **Verbatim replacement** for each card container:
```jsx
// Before:
<div
className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm border border-opacity-20"
style={{
backgroundColor: 'rgba(var(--bg-secondary-rgb), 0.85)',
borderColor: 'var(--border)',
}}
>
// After:
<div className="glass-panel-strong rounded-2xl p-8">
```
The `style={{}}` block is removed entirely. `.glass-panel-strong`
already composes the background, blur, rim, elevation, and the
gradient-border treatment.
- **No new imports.** This brief does NOT use `<GlassSurface>`
directly — the className path is correct because (a) it's an HTML
div with no compositional requirements, (b) the existing
`.glass-panel-strong` class is the documented canonical shape per
the design audit, and (c) using the class keeps the diff minimal.
- **Preserve children verbatim.** The `<form>`, every `<Input>`, every
`<Button>`, the error banner, the social-sign-in divider, the
"Don't have an account?" footer link — all stay byte-identical.
- **Do not adjust the surrounding header block** (Deck Hearth logo +
greeting text); only the form-card `<div>` itself migrates.
- **Tailwind safelist note:** `.glass-panel-strong` is defined in
`styles/globals.css` as a plain CSS class (not a Tailwind
utility). It's already used in `<Layout>` and elsewhere, so the
build picks it up via the `@layer` block. No `tailwind.config.js`
edit needed.
## Acceptance criteria
- [ ] `pages/login.js` form-card `<div>` uses
`className="glass-panel-strong rounded-2xl p-8"` and carries
no inline `backgroundColor` / `borderColor` style.
- [ ] `pages/signup.js` form-card `<div>` uses
`className="glass-panel-strong rounded-2xl p-8"` and carries
no inline `backgroundColor` / `borderColor` style.
- [ ] All form fields, labels, buttons, error banners, and footer
links render identically post-migration (manual smoke;
`tests/smoke/auth.spec.js` continues to pass without edits).
- [ ] No edits to other files (e.g. no token additions in
`styles/globals.css`, no new components in `components/ui/`).
- [ ] `npm run lint` passes; `npm run test:run` is unchanged
(no test additions needed for a pure className swap).
- [ ] Dark mode: card remains legible against the body background
gradient (verify manually; before-shot vs after-shot screenshot
attached to the PR description).
## Rationale (≤3 sentences)
These two cards are the first surfaces a new user sees and they predate the `.glass-panel-strong` system; aligning them is both a correctness fix (the `rgba(--bg-secondary-rgb, 0.85)` shape doesn't compose corner lights) and a consistency win. The migration is a pure className swap with zero behavioral change — the lowest-risk brief in the convoy.

View file

@ -1,160 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 3
depends_on: [1]
files:
- components/Layout.js
- components/ui/TopSearchBar.js
- test/components/Layout.test.js
---
# Brief 3: Migrate floating popovers to `.glass-panel-strong`
## Goal (1 sentence)
Migrate three floating surfaces — the sidebar profile dropdown, the mobile drawer, and the TopSearchBar `<UserMenu>` dropdown — from inline `var(--glass-surface-*)` + `backdropFilter` styles to the canonical `.glass-panel-strong` className, while preserving their existing box-shadow chains (ember rim for the profile dropdown; pronounced elevation for the drawer) via inline override.
## Files in scope (do not edit anything else)
- `components/Layout.js`
- Sidebar profile dropdown panel (currently around L88-96 inside `<UserProfileDropdown>`).
- Mobile drawer slide-in panel (currently around L698-709 — the `<div>` that takes `${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}` and the `style={{ background: 'var(--glass-surface-mid)', backdropFilter: '...', ... }}` block).
- **Do NOT touch** the sidebar nav-chip block (around L858-861); that block intentionally uses full-intensity chrome corner lights and stays handrolled — Decision 4 of this convoy's Architecture allowlists it explicitly.
- `components/ui/TopSearchBar.js`
- `<UserMenu>` dropdown panel (around L214-222 — the absolutely-positioned `<div>` with the `background: 'var(--glass-surface-high)'` + `backdropFilter` block).
- **Do NOT touch** the TopSearchBar `<header>` block itself; that block also intentionally uses full-intensity chrome corner lights and is allowlisted.
- `test/components/Layout.test.js`
- Add regression-lock assertions that the sidebar profile dropdown and mobile drawer render with `.glass-panel-strong` in their className.
**Out of scope:** the rest of `Layout.js` (`<DesktopSidebar>`, `<MobileNavigation>`, `<UserProfileDropdown>` trigger button, `<DailyEmberWidget>`, etc.), the rest of `TopSearchBar.js` (search input, `<CommandPaletteModal>` link, etc.), and any deeper menu-item styling (e.g. `hover:bg-gray-50 dark:hover:bg-gray-700` cleanup — that's owned by PR #117 / `design-sweep-pass`).
## Conventions to follow
- **Verbatim replacement** for each floating-surface `<div>`:
### A. Sidebar profile dropdown panel (Layout.js)
```jsx
// Before:
<div
className="absolute bottom-full left-0 right-0 mb-2 rounded-xl z-20"
style={{
background: 'var(--glass-surface-high)',
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
boxShadow: 'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)',
}}
>
// After:
<div
className="glass-panel-strong absolute bottom-full left-0 right-0 mb-2 rounded-xl z-20"
style={{
boxShadow:
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)',
}}
>
```
The `background` + `backdropFilter` + `WebkitBackdropFilter` inline
styles are removed (the class provides them). The `boxShadow` is
**preserved as an inline override** because `.glass-panel-strong`
ships only `var(--rim-light-inner), var(--elevation-ambient)`
we need the `var(--ember-rim-subtle)` middle entry to keep the
ember emphasis the dropdown is known for.
### B. Mobile drawer panel (Layout.js)
```jsx
// Before:
<div
className={`
md:hidden fixed inset-y-0 left-0 z-50 w-64 transform transition-transform duration-300 ease-in-out
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}
`}
style={{
background: 'var(--glass-surface-mid)',
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
boxShadow: 'var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)',
}}
>
// After:
<div
className={`
glass-panel-strong md:hidden fixed inset-y-0 left-0 z-50 w-64 transform transition-transform duration-300 ease-in-out
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}
`}
style={{
boxShadow:
'var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)',
}}
>
```
Same pattern. `boxShadow` is preserved as an inline override so the
drawer keeps its `var(--elevation-pronounced)` (heavier) elevation
+ `var(--rim-light-outer)` (which `.glass-panel-strong` doesn't
ship by default).
### C. TopSearchBar `<UserMenu>` dropdown panel
```jsx
// Before — the absolutely-positioned <div> nested inside UserMenu,
// around L211-222 of TopSearchBar.js (verified against current file
// 2026-06-04 during architect boot-the-brief recheck):
<div
role="menu"
aria-label="Account menu"
className="absolute right-0 top-full mt-2 w-56 rounded-xl z-30 overflow-hidden"
style={{
background: 'var(--glass-surface-high)',
backdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
WebkitBackdropFilter: 'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
boxShadow:
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced)',
}}
>
// After:
<div
role="menu"
aria-label="Account menu"
className="glass-panel-strong absolute right-0 top-full mt-2 w-56 rounded-xl z-30 overflow-hidden"
style={{
boxShadow:
'var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced)',
}}
>
```
Note: this dropdown uses `--elevation-pronounced` (not `-ambient`)
— it's a heavier shadow than the sidebar profile dropdown's because
it floats higher in the viewport over more content. Preserve that
verbatim; do not normalize to `-ambient`. The `role="menu"` +
`aria-label="Account menu"` + `overflow-hidden` attributes also stay
byte-identical.
- **Preserve all children verbatim.** Menu items, dividers, icons,
click handlers — all stay byte-identical.
- **Do not touch the existing `hover:bg-gray-50 dark:hover:bg-gray-700`
classes on individual menu items.** PR #117 (`design-sweep-pass`)
is already migrating those to `nav-item-hover`; this brief MUST NOT
duplicate that work or it'll merge-conflict. If PR #117 has already
merged when this brief dispatches, the relevant lines may already
read `nav-item-hover` — that's fine; just leave them alone.
- **Test additions in `test/components/Layout.test.js`:**
- One assertion for the sidebar profile dropdown: render `<Layout user={mockUser} />`, simulate clicking the user-profile trigger to open the dropdown, assert the dropdown panel `className` contains `'glass-panel-strong'`.
- One assertion for the mobile drawer: render `<Layout user={mockUser} />`, simulate opening the drawer (today the test file already has a way to set `isMobileMenuOpen`; if not, render with the relevant prop / state directly), assert the drawer panel `className` contains `'glass-panel-strong'`.
- Use the existing test file's patterns (jsdom env, `cleanup()` afterEach, `render` + `screen` from `@testing-library/react`). Do NOT add new test dependencies. Do NOT lower any existing assertion in the file.
## Acceptance criteria
- [ ] Sidebar profile dropdown panel uses `className="glass-panel-strong ..."` and carries an inline `boxShadow` with `var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient)`.
- [ ] Mobile drawer panel uses `className="glass-panel-strong ..."` and carries an inline `boxShadow` with `var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced)`.
- [ ] TopSearchBar `<UserMenu>` dropdown panel uses `className="glass-panel-strong ..."` and carries an inline `boxShadow` with `var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-pronounced)`.
- [ ] No inline `background: 'var(--glass-surface-*)'` or `backdropFilter` style remains on any of the three migrated `<div>`s.
- [ ] Sidebar nav-chip block (Layout.js ~L858-861) UNCHANGED. TopSearchBar `<header>` block UNCHANGED. (Architect's allowlist for chrome-tier surfaces.)
- [ ] `test/components/Layout.test.js` has at least 2 new assertions (sidebar dropdown + mobile drawer carry `glass-panel-strong`).
- [ ] `npm run lint` + `npm run test:run` (23+/23+, post Brief 1) pass.
- [ ] Manual: open sidebar profile dropdown, open mobile drawer, open TopSearchBar UserMenu dropdown in light + dark mode — all three render with visible corner catch-lights and the appropriate shadow elevation.
- [ ] No edits to files outside the 3 listed in `files:` above.
## Rationale (≤3 sentences)
These three popovers are the most visible floating surfaces in the authenticated app and they predate the gradient-border system; migrating them is the largest visual-correctness win of the convoy after Brief 1. Preserving the existing box-shadow chains via inline override (rather than letting them snap to the class default) is non-negotiable — the `var(--ember-rim-subtle)` ember emphasis on the profile menu and the `var(--elevation-pronounced)` weight on the drawer are deliberate design choices that must survive the class swap.

View file

@ -1,123 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 4
depends_on: [1]
files:
- components/BulkSelectionToolbar.js
---
# Brief 4: Migrate `BulkSelectionToolbar` to `.glass-panel-strong` + token sweep
## Goal (1 sentence)
Replace the bulk-select toolbar's `bg-white border-gray-200` Tailwind shape with `.glass-panel-strong rounded-2xl` so the floating toolbar is legible in dark mode, and sweep its interior hardcoded gray/red Tailwind classes (`text-gray-700`, `hover:bg-gray-100`, `text-red-600 hover:bg-red-50`) to token-driven inline styles + `nav-item-hover` for consistency with the rest of the post-PR-#117 surfaces.
## Files in scope (do not edit anything else)
- `components/BulkSelectionToolbar.js` — the entire file. Specifically:
- L47 outer toolbar `<div>` — swap `bg-white rounded-xl shadow-2xl border border-gray-200` to `glass-panel-strong rounded-2xl shadow-2xl`. (Keep `shadow-2xl` for now — the toolbar floats over arbitrary content and the extra weight is intentional. `.glass-panel-strong`'s `var(--rim-light-inner), var(--elevation-ambient)` chain doesn't include it; chain it via inline override.)
- L60 divider — swap `bg-gray-300` to a CSS-var-driven divider color (`backgroundColor: 'var(--border)'` inline, since `bg-gray-300` doesn't theme).
- L53 selection-count label — swap `text-gray-700` to `style={{ color: 'var(--text-primary)' }}`.
- L108 more-actions trigger — swap `text-gray-600 hover:text-gray-800 hover:bg-gray-100` to use `var(--text-secondary)` + `nav-item-hover`, with `rounded-lg` upgraded to `rounded-xl`.
- L117 more-actions dropdown — swap `bg-white rounded-lg shadow-xl border border-gray-200` to `glass-panel-strong rounded-xl`. (Drop `shadow-xl`; the class provides shadow. Keep `border` semantics out — the class's gradient border is the new look.)
- L118-141 menu items — swap `text-gray-700 hover:bg-gray-100` to `var(--text-primary)` + `nav-item-hover` className.
- L144 menu divider — swap `border-gray-200` to `borderColor: 'var(--border)'`.
- L146-157 destructive menu item — swap `text-red-600 hover:bg-red-50` to `color: 'var(--accent-danger)'` (verify the token exists in `globals.css`; if not, fall back to `rgb(239, 68, 68)` and document) + `nav-item-hover` className.
- L160-end "Clear Selection" button — sweep any remaining `text-gray-*` / `bg-gray-*` hardcodes the same way.
**Out of scope:** the parent components that consume `BulkSelectionToolbar` (e.g. `pages/my-cards.js`, `components/CollectionPageView.js`); the props contract (`selectedCount`, `selectedCards`, all the `onBulk*` callbacks); any of the SVG icon paths.
## Conventions to follow
- **Outer toolbar:**
```jsx
// Before:
<div className="bg-white rounded-xl shadow-2xl border border-gray-200 px-3 sm:px-6 py-3 sm:py-4 flex items-center justify-between sm:justify-start sm:space-x-4 sm:min-w-96">
// After:
<div
className="glass-panel-strong rounded-2xl px-3 sm:px-6 py-3 sm:py-4 flex items-center justify-between sm:justify-start sm:space-x-4 sm:min-w-96"
style={{
// Chain shadow-2xl-equivalent depth onto the class's existing
// rim+ambient stack so the floating toolbar still reads as
// elevated over arbitrary page content.
boxShadow:
'var(--rim-light-inner), var(--elevation-pronounced)',
}}
>
```
Rationale: `shadow-2xl` is a hardcoded RGB; `var(--elevation-pronounced)` is the system equivalent and respects the theme.
- **More-actions dropdown:**
```jsx
// Before:
<div className="absolute bottom-full right-0 mb-2 bg-white rounded-lg shadow-xl border border-gray-200 py-2 min-w-48">
// After:
<div className="glass-panel-strong absolute bottom-full right-0 mb-2 rounded-xl py-2 min-w-48">
```
No inline-style override needed — the class's default
`var(--rim-light-inner), var(--elevation-ambient)` is the right
weight for an inner dropdown.
- **Menu items** (the `<button>` rows inside the dropdown):
```jsx
// Before (non-destructive item):
<button className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center space-x-2">
// After:
<button
className="nav-item-hover w-full px-4 py-2 text-left text-sm flex items-center space-x-2"
style={{ color: 'var(--text-primary)' }}
>
// Before (destructive item):
<button className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center space-x-2">
// After:
<button
className="nav-item-hover w-full px-4 py-2 text-left text-sm flex items-center space-x-2"
style={{ color: 'var(--accent-danger)' }}
>
```
- **Verify `--accent-danger` exists** before using it. Quick check:
search `styles/globals.css` for `--accent-danger`. If absent, the
PR #117 sweep likely defines it; if it's still absent post-#117,
use `color: 'rgb(239, 68, 68)'` (the literal Tailwind `red-500`
RGB) and add a note in the PR description requesting follow-up to
introduce the token in a separate PR. Do NOT add the token in this
brief — token additions belong in a design-system PR.
- **Verify `nav-item-hover` exists** — it's defined in
`styles/globals.css` and used widely post-PR #117. If for some
reason this brief dispatches before PR #117 lands, escalate to the
conductor — that's a dependency violation (Brief 4 depends_on: [1]
but transitively depends on the post-PR-#117 token tier).
- **Verbatim children:** SVG icon paths, button text labels, click
handler bindings, the `VOCAB.MY_COLLECTION` / `VOCAB.REMOVE_FROM_MY_COLLECTION`
imports, the `setShowActions(false)` flow — all stay byte-identical.
- **`px-2 sm:px-3 py-2` action buttons** (the 3 colored quick-action
buttons: Collection / Deck / My Collection): leave them alone. They
use `--accent-flame`, `--accent-gold`, `--accent-wood` already and
the `onMouseEnter`/`onMouseLeave` swap is a known pattern. Touching
them is scope expansion.
## Acceptance criteria
- [ ] Outer toolbar `<div>` uses `glass-panel-strong rounded-2xl` and an inline `boxShadow` matching the verbatim shape above.
- [ ] More-actions dropdown `<div>` uses `glass-panel-strong rounded-xl` and no inline `background` / `border` / `shadow` style.
- [ ] All `text-gray-{600,700,800}` classes within the toolbar are replaced with `style={{ color: 'var(--text-{primary,secondary}) ' }}` or `nav-item-hover` className.
- [ ] All `hover:bg-gray-{50,100}` classes are replaced with `nav-item-hover` className.
- [ ] The destructive "Delete Selected" item uses `var(--accent-danger)` (or the documented `rgb(239,68,68)` fallback) — NOT `text-red-600`.
- [ ] The L60 divider uses `style={{ backgroundColor: 'var(--border)' }}` — NOT `bg-gray-300`.
- [ ] The 3 quick-action buttons (Collection / Deck / My Collection at L64-101) are UNCHANGED. The "Clear Selection" button at the end may need a small text-color swap; that's in scope. Everything else is left alone.
- [ ] **Dark-mode verification**: open the bulk-select toolbar (select 2+ cards on `/my-cards` or `/collection/[id]`), confirm it renders legibly with corner catch-lights and proper text contrast in BOTH light and dark themes. Attach before/after dark-mode screenshots to the PR description.
- [ ] `npm run lint` + `npm run test:run` (23+/23+, post Brief 1) pass.
- [ ] No edits to files outside `components/BulkSelectionToolbar.js`.
## Rationale (≤3 sentences)
The toolbar is currently invisible in dark mode because `bg-white` doesn't theme — this PR is a correctness fix as much as a design unification. Sweeping the interior hardcoded grays + reds in the same PR is cheap (the file is small, ~200 lines) and prevents a follow-up convoy from having to revisit the file. The 3 colored quick-action buttons stay as-is because they use the right tokens already and a token sweep there is scope expansion.

View file

@ -1,87 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 5
depends_on: []
files:
- pages/profile.js
- pages/settings.js
- pages/community/collections.js
- components/CollectionsPageView.js
- styles/globals.css
---
# Brief 5: Retire `.card` class; migrate 8 consumers to `.glass-panel`
## Goal (1 sentence)
Delete the legacy `.card` class from `styles/globals.css` and migrate all 8 consumers (3 in `pages/profile.js`, 2 in `pages/settings.js`, 1 in `pages/community/collections.js`, 1 in `components/CollectionsPageView.js`, plus 1 likely-missed inline `className="card hover:..."` instance) to `glass-panel rounded-3xl p-{4|6}` so the app converges on a single panel vocabulary.
## Files in scope (do not edit anything else)
- `styles/globals.css` — delete the entire `.card { ... }` rule block (currently 4 lines at L729-733):
```css
.card {
@apply rounded-3xl shadow-lg p-6 transition-all duration-300;
background-color: var(--bg-primary);
border: 1px solid var(--border);
}
```
- `pages/profile.js` — 3 `className="card p-6 [...]"` swaps. Likely sites (verify line numbers against current file): L290 (`card p-6 text-center` — the profile avatar card), L397 (`card p-6 mt-6` — the stats card), L432 (`card p-6` — the activity card).
- `pages/settings.js` — 2 `className="card ..."` swaps. Likely sites: L266 (`card p-4`), L290 (`card p-6`).
- `pages/community/collections.js` — 1 swap. Likely site: L266 (`card group cursor-pointer hover:shadow-lg transition-all duration-200`).
- `components/CollectionsPageView.js` — 1 swap. Likely site: L125 (`card hover:shadow-xl transition-all duration-300 cursor-pointer group`).
**Out of scope:** everything else in those files (page-level layout, header strips, modal wrappers, button styling, inline content). Only the `.card` className references migrate.
## Conventions to follow
- **Swap pattern (default):**
```jsx
// Before:
<div className="card p-6 [...whatever-else]">
// After:
<div className="glass-panel rounded-3xl p-6 [...whatever-else]">
```
Verbatim: replace the standalone token `card` with `glass-panel rounded-3xl`. **Preserve any sibling tokens** (`p-4` / `p-6` / `text-center` / `mt-6` / `group` / `cursor-pointer` / `hover:shadow-lg` / `transition-all duration-200`, etc.). The legacy `.card` `@apply`'d `rounded-3xl shadow-lg p-6 transition-all duration-300`; we keep `rounded-3xl` explicitly and let `.glass-panel`'s composed shadow stack replace `shadow-lg` (which is the right move — `.glass-panel`'s tokens theme correctly, `shadow-lg` doesn't).
- **`p-6` handling:** the legacy class baked in `p-6`. If the consumer wrote `card p-4` (`pages/settings.js` L266), the explicit `p-4` already wins via Tailwind's cascade — `p-4` overrides the `@apply rounded-3xl shadow-lg p-6` because they're at the same specificity and `p-4` is the later-defined declaration in compiled output. After migration, the explicit `p-4` continues to win because there's no longer any baked-in `p-6`. **Bottom line:** preserve whatever padding token the consumer wrote; don't normalize to `p-6`.
- **`transition-all duration-300` handling:** the legacy class baked in `transition-all duration-300`. Most consumers ALSO wrote it explicitly (e.g. `CollectionsPageView.js` L125: `transition-all duration-300`). If a consumer relied on the baked-in version, the swap loses the transition — add `transition-all duration-300` explicitly to that consumer's className post-swap. **Audit each site for whether the transition is referenced; add it back where needed.**
- **`shadow-lg` handling:** the legacy class baked in `shadow-lg`. `.glass-panel`'s composed `box-shadow` (rim + elevation-ambient) is the right replacement; do NOT carry `shadow-lg` over.
- **`pages/community/collections.js` site (L266):** the consumer adds `hover:shadow-lg` on top of the baked `shadow-lg`. The base shadow becomes `.glass-panel`'s shadow stack; the `hover:shadow-lg` is a hardcoded Tailwind shadow. Decision: replace `hover:shadow-lg` with no `hover:` override — the corner-light gradient on `.glass-panel` is the new affordance. (If review shows this regresses hover legibility, fall back to inline `style={{}}` boxShadow override on `:hover` — but the simpler clean diff is to drop it.)
- **`components/CollectionsPageView.js` site (L125):** same logic. Drop `hover:shadow-xl`.
- **Escape hatch (per Decision D2):** if any one of the 8 consumers breaks visually post-swap (the architect doesn't expect this), revert that one site to inline `style={{ background: 'var(--bg-secondary)' }}` rather than holding the entire class-deletion. Document each escape-hatch use in the PR description with a screenshot. If MORE than 1 of 8 needs the escape hatch, the brief's reviewer is expected to push back and hold the deletion entirely — flag in PR comments.
- **CSS deletion order:** delete the `.card { ... }` rule LAST, after all 8 className swaps are in place. (Pure ordering hygiene for diff-readability; CSS doesn't actually care.)
- **Boundaries:** do NOT touch any other class in `globals.css` (no renaming `.glass-panel``.opaque-panel`, no adding new classes). Do NOT touch any pages or components not in the 4 listed.
- **Grep verification before PR:**
```bash
# Should return 0 matches anywhere in pages/ or components/:
rg "className=[\"\\'\\\`]card\\b" pages/ components/ --type js
# And the class definition itself:
rg "^\\.card \\{" styles/
```
Both should return zero. If either returns anything, the migration is incomplete.
## Acceptance criteria
- [ ] `styles/globals.css` no longer contains a `.card { ... }` rule block.
- [ ] Zero `className=".*\bcard\b.*"` references remain under `pages/` or `components/` (verified by the grep command above).
- [ ] Each of the 8 migrated consumers renders with `.glass-panel rounded-3xl` and the original `p-{4|6}` padding intact.
- [ ] Wherever the consumer relied on baked-in `transition-all duration-300` (audit by reviewing each site), the transition is added explicitly post-migration.
- [ ] `hover:shadow-{lg,xl}` Tailwind overrides on community/CollectionsPageView are dropped (replaced by `.glass-panel`'s corner-light affordance).
- [ ] No escape-hatch sites or — if any — at most 1 escape-hatch site, documented with a screenshot in the PR description.
- [ ] `npm run lint` + `npm run test:run` (no test count change expected; 22+/22+ post Brief 1) pass.
- [ ] Manual: visit `/profile`, `/settings`, `/community/collections`, `/community/collections` collection-detail in light + dark mode and confirm all panels render with corner catch-lights and theme-correct backgrounds. Attach before/after screenshots to PR.
- [ ] No edits to files outside the 5 listed in `files:` above.
## Rationale (≤3 sentences)
`.card` predates the gradient-border system and is the only remaining "opaque solid panel" pattern in user-facing pages — a single panel vocabulary across the app is the convoy's success metric. The 8 consumers are all "panel-with-content-inside" surfaces with no GPU-budget constraint, so the migration is a clean className swap with no compositional surprises. Deleting the class outright (rather than keeping it as a documented fallback) prevents future agents from picking the wrong pattern by accident.

View file

@ -1,91 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 6
depends_on: []
files:
- pages/index.js
---
# Brief 6: Migrate landing nav bar to `.page-header-glass`
## Goal (1 sentence)
Replace the handrolled translucent `<nav>` on the public landing page with the canonical `.page-header-glass border-b` className, and drop the malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` (the token already contains its own `inset 0 1px 0`; double-wrapping breaks the cascade — documented in `styles/globals.css` L748-761).
## Files in scope (do not edit anything else)
- `pages/index.js` — the landing `<nav>` block, currently L61-72 (around the `border-b` declaration with inline `background: 'var(--glass-surface-mid)'` + `backdropFilter` + `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'`).
**Out of scope:** everything else on the landing page (the Deck Hearth heading, the Sign In / Get Started buttons inside the nav, the hero section, feature cards, footer, the `if (user) return null;` redirect logic, etc.). The migration is a single `<nav>` element's `className` + `style` swap.
## Conventions to follow
- **Verbatim replacement:**
```jsx
// Before:
<nav
className="border-b"
style={{
background: 'var(--glass-surface-mid)',
backdropFilter:
'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
WebkitBackdropFilter:
'blur(var(--glass-blur-mid)) saturate(var(--glass-saturate))',
borderColor: 'var(--border)',
boxShadow: 'inset 0 1px 0 var(--rim-light-inner)',
}}
>
// After:
<nav
className="page-header-glass border-b"
style={{
borderColor: 'var(--border)',
}}
>
```
The class provides `background`, `backdrop-filter`, and the
correct `box-shadow` (which IS `var(--rim-light-inner), 0 1px 0
var(--border)` per its definition in `globals.css` L300-306). The
`border-b` Tailwind class needs the `borderColor` style to remain
themed; everything else collapses into the class.
- **The malformed boxShadow is the bug:** `var(--rim-light-inner)` is
already a complete `inset 0 1px 0 <color>` declaration. Wrapping
it in another `inset 0 1px 0 ...` produces invalid CSS that the
browser silently drops. The class's own `box-shadow:
var(--rim-light-inner), 0 1px 0 var(--border)` does the right
thing. Removing the inline `boxShadow` line is the fix.
- **Children verbatim:** the `<div className="max-w-7xl mx-auto ...">`
inside the `<nav>`, the `<AnimatedFireLogo>`, the heading, and the
Sign In / Get Started buttons all stay byte-identical. Do not
touch them.
- **`.page-header-glass` placement check:** the class is defined in
`styles/globals.css` L300-306 and is already used by authenticated
pages' header strips (dashboard, my-cards, cards, etc.). It's safe
to reuse on the public landing page — the class has no auth
coupling.
- **Nested backdrop-filter check** (boot-the-brief risk #6): the
landing page has no enclosing Layout (the `pages/index.js` redirect
logic returns `null` for authenticated users and renders its own
full-page `<div>` for anonymous visitors — no `<Layout>` wrapper).
So there's no risk of nested `backdrop-filter` stacking. Verified.
- **AGENTS.md / no-go zones:** `pages/index.js` is an authored page,
not a generated artifact. Allowed.
## Acceptance criteria
- [ ] Landing `<nav>` uses `className="page-header-glass border-b"`.
- [ ] Inline `style` on the `<nav>` retains only `borderColor: 'var(--border)'` — no `background`, no `backdropFilter`, no `WebkitBackdropFilter`, no `boxShadow`.
- [ ] The Deck Hearth heading + Sign In / Get Started buttons inside the nav render identically.
- [ ] Manual: open `/` in an incognito window (anonymous user), verify the nav bar renders with a subtle glassy treatment, an `inset 0 1px 0` highlight (the inner rim), and a 1px bottom border. Verify in both light and dark theme via the theme toggle (which appears on the public landing). Attach light + dark screenshots to PR.
- [ ] `npm run lint` + `npm run test:run` pass (no test count change expected).
- [ ] No edits to other files.
## Rationale (≤3 sentences)
The landing nav is the highest-visibility public surface in the app and currently both (a) duplicates `.page-header-glass`'s composition handrolled, and (b) ships a malformed `boxShadow` that silently breaks. Migrating to the canonical class is a one-line cleanup that fixes a latent bug. No corner-light treatment is appropriate here — the nav is full-bleed, so corner radials would land at viewport edges and never read as light sources; `.page-header-glass` (which uses the rim/border-only composition) is the right shape.

View file

@ -1,123 +0,0 @@
---
convoy: unify-glass-panel-surfaces
brief_number: 7
depends_on: [1, 2, 3, 4, 5, 6]
files:
- .github/workflows/ci.yml
---
# Brief 7: `forbidden-bespoke-glass-surface` CI gate
## Goal (1 sentence)
Add a `forbidden-bespoke-glass-surface` job to `.github/workflows/ci.yml` that fails the build if `var(--glass-surface-(low|mid|high))` appears in JSX inline-style usage under `components/**` or `pages/**`, with a curated allowlist for the documented chrome exceptions (Layout sidebar nav-chip block, TopSearchBar `<header>` block, and the `<GlassSurface>` primitive itself).
## Files in scope (do not edit anything else)
- `.github/workflows/ci.yml` — append a new job after the existing `forbidden-modal-shell-without-primitive` job (around L200) or alongside it in the `forbidden-*` cluster. Don't reorder existing jobs.
**Out of scope:** every other file in the repo. This brief is pure CI surface; no application code changes.
## Conventions to follow
- **Model after the existing `forbidden-modal-shell-without-primitive` job** (`.github/workflows/ci.yml` L200-226). Same shape:
- Single `runs-on: ubuntu-latest` step that runs a `grep` command.
- Collects matches into a bash array.
- Iterates and emits `::error file=${f}::<reason>` for each match.
- Exits non-zero if any match remains.
- The grep pattern is bounded to `pages/ components/ -r --include='*.js'`.
- **Verbatim job shape** (drop into `ci.yml` at the bottom of the forbidden-* cluster):
```yaml
forbidden-bespoke-glass-surface:
name: No bespoke var(--glass-surface-*) inline styles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail if JSX inline styles handroll glass surfaces
run: |
# unify-glass-panel-surfaces convoy (PR sequence after #118 +
# #117). Once Briefs 1-6 land, every panel-shaped surface in
# the app composes via .glass-panel / .glass-panel-strong /
# .page-header-glass / <GlassSurface>. Inline-style usage of
# var(--glass-surface-low|mid|high) under pages/ or
# components/ JSX is the regression vector this gate prevents.
#
# Allowlist (explicit, documented exceptions):
# - components/ui/GlassSurface.js # the primitive itself
# - components/Layout.js # sidebar nav-chip chrome block (L858 area)
# - components/ui/TopSearchBar.js # <header> chrome block
# These three files intentionally compose handrolled chrome
# surfaces ratified in PR #116 (gradient borders) + the
# unify-glass-panel-surfaces architect decision D4. If you
# need to add a fourth allowlist entry, that's a design-system
# decision — open a new convoy.
ALLOWLIST=(
"components/ui/GlassSurface.js"
"components/Layout.js"
"components/ui/TopSearchBar.js"
)
FOUND=()
while IFS= read -r file; do
skip=false
for allowed in "${ALLOWLIST[@]}"; do
if [ "$file" = "$allowed" ]; then
skip=true
break
fi
done
if [ "$skip" = true ]; then
continue
fi
FOUND+=("$file")
done < <(grep -lE "var\\(--glass-surface-(low|mid|high)\\)" \
pages components -r --include='*.js' 2>/dev/null \
| sort -u || true)
if [ ${#FOUND[@]} -gt 0 ]; then
echo "::error::Bespoke var(--glass-surface-*) inline styles detected outside the documented allowlist."
echo "Use .glass-panel / .glass-panel-strong / .page-header-glass or compose <GlassSurface> from components/ui/ instead."
for f in "${FOUND[@]}"; do
echo "::error file=${f}::Replace inline var(--glass-surface-*) with the appropriate class or primitive."
done
exit 1
fi
echo "OK: no bespoke glass-surface inline styles outside the allowlist."
```
- **Allowlist hygiene:** the 3 allowlist entries are exact paths. If
the relevant chrome block in `components/Layout.js` is later
refactored into its own sub-component (e.g. `<SidebarNavChip>`),
the new file path replaces `components/Layout.js` in the
allowlist — that's the kind of edit a future PR would carry.
- **Pre-merge negative test:** before opening this PR, run the
allowlist locally — verify that adding a scratch `style={{
background: 'var(--glass-surface-low)' }}` to a non-allowlisted
file (e.g. `pages/profile.js`) and re-running the grep produces a
match. Then revert the scratch change. Document the negative test
in the PR description (don't commit the scratch change).
- **CI ordering:** Brief 7 depends on Briefs 1-6 ALL landing first.
If this job is added before any of the prior briefs ships, the
build will fail on the in-flight migrations (every site this
convoy is migrating IS currently a bespoke `var(--glass-surface-*)`
inline-style usage). Conductor MUST hold dispatch until 1-6 are
all on `main`.
- **Boundaries:** do NOT touch any other CI job. Do NOT edit any
application file. Do NOT update AGENTS.md or other docs in this
PR (a separate AGENTS.md update can land alongside Brief 1 if
the architect wants — Brief 7 is pure CI).
## Acceptance criteria
- [ ] `.github/workflows/ci.yml` contains a new `forbidden-bespoke-glass-surface` job matching the verbatim shape above (job name, `runs-on`, checkout step, grep-and-allowlist shell block).
- [ ] The allowlist has exactly 3 entries: `components/ui/GlassSurface.js`, `components/Layout.js`, `components/ui/TopSearchBar.js`.
- [ ] On a clean post-Briefs-1-through-6 `main`, the new CI job is GREEN — no false positives. (Verify by running the grep locally before opening the PR.)
- [ ] Negative test: adding a scratch `style={{ background: 'var(--glass-surface-low)' }}` to e.g. `pages/profile.js` and re-running the same grep command produces a match. (Documented in PR description; not committed.)
- [ ] No other CI jobs are reordered, renamed, or modified.
- [ ] PR description links back to this brief and to the convoy file (`.convoys/unify-glass-panel-surfaces.md`).
## Rationale (≤3 sentences)
The convoy's success metric is unification, but unification without a regression gate is half a fix — a future PR can re-introduce a bespoke `var(--glass-surface-*)` inline style and undo the work. Modeling the gate on the existing `forbidden-modal-shell-without-primitive` job keeps it consistent with the repo's CI vocabulary and makes the failure message actionable. The 3-entry allowlist is small and intentional; growing it requires an explicit design-system decision, which is the right friction for a convention-enforcing gate.

View file

@ -1,13 +1,13 @@
---
name: role-a11y-auditor
description: >-
Accessibility audit on a UI diff against WCAG 2.2 (Level AA). Read-only.
Runs `[skills/accessibility-audit](../../../accessibility-audit/SKILL.md)`
for the rubric + report template. Use after the implementer's PR draft on
PRs that touch UI files. Safe to run in parallel with role-reviewer +
role-security-auditor + role-design-system-auditor via Cursor 3.2 /multitask.
Accessibility audit on a UI diff. Checks for missing labels, keyboard
navigation, focus management, color contrast, semantic HTML, and ARIA
correctness. Read-only. Use after the implementer's PR draft on PRs that
touch UI files. Does not require a browser MCP — works from the diff +
static analysis. Safe to run in parallel with role-reviewer +
role-design-system-auditor via Cursor 3.2 /multitask.
multitask: audit-fanout
model: cursor-grok-4.5-high
tools: [Read, Grep, Glob, Shell]
---
@ -20,59 +20,86 @@ After `role-design-system-auditor` on UI-touching PRs. Skip when convoy frontmat
## Inputs
- The PR diff (UI files only).
- The convoy's UX section (a11y constraints listed there — verify each one).
- Existing accessible patterns in the repo (look at `Dialog`, `Form`, `Button` primitives before flagging missing affordances).
- `[skills/accessibility-audit/SKILL.md](../../../accessibility-audit/SKILL.md)` — the audit rubric, severity scale, and 5-layer framework.
- The convoy's UX section (which already lists a11y constraints — verify the implementer satisfied them).
- Existing accessible patterns in the repo (look at existing `Dialog`, `Form`, `Button` primitives).
## Outputs
A structured audit report following the template at `skills/accessibility-audit/templates/audit-report.md`. Posted as:
A structured comment for the PR Health rollup:
- A PR comment when GitHub is the surface, OR
- An Echodo `document` (Phase 2b: `create_task_from_template({template: "a11y-audit", ...})`) when MCP is reachable.
```markdown
## A11y Audit
Per `[skills/accessibility-audit/SKILL.md](../../../accessibility-audit/SKILL.md)` step 7 — both paths produce the same shape.
| Check | Status | Count |
| --- | --- | --- |
| Labels | ✅ / ❌ | <N> |
| Keyboard nav | ✅ / ❌ | <N> |
| Focus management | ✅ / ❌ | <N> |
| Color contrast | ✅ / ⚠️ | <N> |
| Semantic HTML | ✅ / ❌ | <N> |
| ARIA correctness | ✅ / ⚠️ | <N> |
| UX constraint match | ✅ / ❌ | <N> |
### Critical (must fix)
- <file:line><issue><fix>
...
### Warnings (recommended)
- <file:line><issue><fix>
...
### Notes
- ...
```
## Checklist (apply per file)
1. **Labels**: every `<input>`, `<select>`, `<textarea>`, `<button>` has either visible text, `aria-label`, or an associated `<label htmlFor=...>`.
2. **Icon-only buttons**: have `aria-label` or visually-hidden text.
3. **Keyboard navigation**: any `onClick` on a non-button/anchor element has `onKeyDown` (Enter + Space) and `tabIndex={0}` and `role="button"` (or be a real button).
4. **Focus management**: dialogs trap focus; modals return focus on close; route changes move focus to the heading.
5. **Color contrast**: text on backgrounds meets 4.5:1 (large text 3:1). Hardcoded colors that we can't measure → ⚠️.
6. **Semantic HTML**: use `<button>` not `<div onClick>`, `<nav>` for navigation, `<main>` for primary content, heading hierarchy `<h1>``<h2>``<h3>` (no skipping).
7. **ARIA correctness**: `aria-expanded` on toggles, `aria-current="page"` on active nav items, `aria-live` on async-updating regions, `role="alert"` on error messages.
8. **UX constraint match**: cross-reference the UX section's a11y constraints — did the implementer satisfy each one?
## Severity
- **Critical**: missing labels on form inputs, no keyboard handler on click-only div, missing focus trap on modal, missing alt text on informative images.
- **Warning**: heading hierarchy skip, missing `aria-current`, color-contrast that requires runtime measurement, missing live region on async updates.
## Steps
1. Get UI diff (`git diff --name-only` filtered to UI extensions).
1. Get UI diff.
2. Read the convoy's UX section once to know what was promised.
3. **Read `[skills/accessibility-audit/SKILL.md](../../../accessibility-audit/SKILL.md)`** if not already in context. Walk the 5 layers in order for each touched surface.
4. Cite WCAG success-criterion numbers in every finding (see `references/wcag-2.2-checklist.md`).
5. Assign severity 0-4 per the skill's rubric. Severity ≥ 3 spawns a child task in Phase 2b.
6. Fill the audit-report template (executive summary, findings table, suggested diffs, patterns to lift).
7. Post the report. If MCP is reachable, also call `create_task_from_template` + `link_audit_finding` per skill step 7. On failure, queue to `.convoys/.pending-mcp-sync.jsonl`.
## Multitask (audit fan-out)
Part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff, emit independent reports, modify no code. Safe to run in parallel via Cursor 3.2 `/multitask`.
Pass the shared `multitask_group` id in metrics. Convention: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
3. For each changed UI file: read the current state of the file (post-diff), then walk the checklist.
4. Build the comment. Cap at 8 critical + 8 warnings.
5. If clean: ✅ across the board with a one-line note.
## What this role does NOT do
- Run axe-core in a browser — that's a CI job (`accessibility-audit` step 2 mentions automated checks; CI runs them, this role consumes their output).
- Test screen readers manually — out of scope for static analysis. Recommend in findings if needed.
- Run axe-core in a browser (that's a CI job, see `.github/workflows/preview-smoke.yml` if present).
- Test screen readers manually — beyond static analysis scope.
- Audit non-UI changes — server / API / config diffs are out of scope.
- Replicate the rubric inline — the rubric lives in the skill. This role orchestrates; it does not carry the checklist.
## Hand-off
## Multitask (audit fan-out)
Message: *"A11y audit complete. N findings (sev ≥ 3: M, sev < 3: K). Report: `<path>` or `<echodo-url>`. Recommend fixing sev 3 before merge."*
Part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff and emit independent comments — none modify code or the convoy. Safe to run in parallel via Cursor 3.2 `/multitask`.
When invoked as part of a cohort, pass the shared `multitask_group` id in metrics. Convention: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
## Metrics
After publishing:
After publishing the audit comment, emit one event:
```bash
bash scripts/log-convoy-event.sh role=role-a11y-auditor convoy=<slug> duration_s=<seconds> model=cursor-grok-4.5-high model_tier=fast [multitask_group=audit-<convoy>-<pr>]
bash scripts/log-convoy-event.sh role=role-a11y-auditor convoy=<slug> duration_s=<seconds> [multitask_group=audit-<convoy>-<pr>]
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
## Anti-patterns
- Demanding ARIA on already-semantic HTML (e.g. `aria-label` on `<button>` with visible text) → wrong, redundant. See skill anti-patterns.
- Demanding ARIA on already-semantic HTML (e.g. `aria-label` on a `<button>` that has visible text) → wrong, that's redundant.
- Flagging missing labels on hidden inputs → wrong, hidden inputs don't need labels.
- Vague feedback ("improve a11y") → wrong. Every finding cites a WCAG criterion + a file:line + a fix.
- Carrying the rubric inline in this role file → wrong. Read the skill.
- Vague feedback ("improve a11y") → wrong, every finding needs a file:line and a specific fix.

View file

@ -8,7 +8,6 @@ description: >-
Must run sequentially — decomposition output enables downstream
implementer fan-out via Cursor 3.2 /multitask.
multitask: single
model: composer-2.5
tools: [Read, Grep, Glob, Shell]
---
@ -61,8 +60,6 @@ Then create one **implementer brief** per row of the decomposition, as a separat
convoy: <slug>
brief_number: <N>
depends_on: [<other brief numbers>]
recommended_model: composer-2.5
model_tier: standard
files:
- <path/to/file1>
- <path/to/file2>
@ -185,7 +182,7 @@ If the convoy's plan needs to change after `role-architect` has run (e.g. a user
After writing the brief files, emit one event. Shell access is restricted to this single command.
```bash
bash scripts/log-convoy-event.sh role=role-architect convoy=<slug> duration_s=<seconds> model=composer-2.5 model_tier=standard
bash scripts/log-convoy-event.sh role=role-architect convoy=<slug> duration_s=<seconds>
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).

View file

@ -7,7 +7,6 @@ description: >-
for downstream roles, and hands off to the next role. Use when a new feature,
bug fix, or epic is being kicked off and the work has not yet been scoped.
multitask: single
model: composer-2.5-fast
tools: [Read, Grep, Glob, Write, Shell]
---
@ -41,31 +40,9 @@ classification: feature | hotfix | docs-only | infra-only | server-only | config
success_metric: <one sentence>
skip:
- <flag1>
- <flag2>
status: open
created: <YYYY-MM-DD>
model_policy:
default_session: auto
roles:
role-conductor: composer-2.5-fast
role-architect: composer-2.5
role-ia-architect: composer-2.5-fast
role-ux-reviewer: composer-2.5-fast
role-ui-designer: composer-2.5-fast
role-implementer: composer-2.5-fast
role-reviewer: cursor-grok-4.5-high
role-security-auditor: gpt-5.6-terra-medium
role-design-system-auditor: cursor-grok-4.5-high
role-a11y-auditor: cursor-grok-4.5-high
role-doc-writer: auto
escalate_to: claude-sonnet-5-thinking-medium
escalate_to_premium: claude-4.6-opus-high-thinking
never_premium:
- role-reviewer
- role-security-auditor
- role-design-system-auditor
- role-a11y-auditor
- role-ui-designer
- role-doc-writer
---
```
@ -83,11 +60,11 @@ Use these as starting points; trust the obvious cases:
| Classification | Default skip flags | Reasoning |
| --- | --- | --- |
| `feature` | (none) | Full pipeline |
| `hotfix` | `ia, ux, ui-design, arch` | Speed over rigor; **still run** reviewer + security-auditor in audit fan-out |
| `docs-only` | `ia, ux, ui-design, arch, test, visual, a11y, design, security, smoke, qa, flag` | Docs only; no executable surface to audit |
| `infra-only` | `ia, ux, ui-design, arch, visual, a11y, design, smoke, qa, flag` | No UI; run security on IaC/workflow changes |
| `server-only` | `ia, ux, ui-design, visual, a11y, design` | API/worker; run reviewer + security-auditor |
| `config-only` | `ia, ux, ui-design, arch, test, visual, a11y, design, security, smoke, qa, docs, flag` | env / CODEOWNERS / config file edit |
| `hotfix` | `ia, ux, arch, review` | Speed over rigor; mandatory post-merge cleanup task |
| `docs-only` | `ia, ux, arch, test, visual, a11y, design, smoke, qa, flag` | Docs change docs; CI lint catches typos |
| `infra-only` | `ia, ux, arch, visual, a11y, design, smoke, qa, flag` | No UI; auditors no-op |
| `server-only` | `ia, ux, visual, a11y, design` | API or worker change; no UI |
| `config-only` | `ia, ux, arch, test, visual, a11y, design, smoke, qa, docs, flag` | env / CODEOWNERS / config file edit |
Never set: `plan-approval`, `pr-merge`, `prod-promote` (human gates are non-negotiable).
@ -99,15 +76,6 @@ Never set: `plan-approval`, `pr-merge`, `prod-promote` (human gates are non-nego
4. Write `.convoys/<slug>.md` with frontmatter + four sections.
5. Print a one-line summary: *"Convoy `<slug>` created (classification: `<X>`, skipping: `<flags>`). Next role: <role-X>."*
### UI designer recommendation (`ui-design` skip)
For `feature` convoys with **new or redesigned** UI surfaces (landing, new app area, major visual refresh):
- If `.cursor/skills/ui-ux-pro-max/` is installed → recommend **`role-ui-designer`** after IA (before UX Reviewer). Do **not** set `skip: ui-design`.
- If the change is **incremental** inside an existing design system (small tweak, one new column, bugfix UI) → add `ui-design` to `skip:` and go straight to `role-ux-reviewer`.
Record the chosen path in `## Roles invoked`.
## Hand-off
Hand off by message to the user, not by spawning another role automatically. The user runs the next role manually (they can paste *"role-ia-architect"* into the chat or open a new chat and reference the convoy). This keeps the human in the loop for the early stages where direction is most plastic.
@ -118,26 +86,15 @@ The Conductor doesn't run anything in parallel itself, but it **tells the user w
| Classification | Recommended `/multitask` dispatch points |
| --- | --- |
| `feature` | Planning: `role-ui-designer` (if greenfield UI + skill installed, before UX Reviewer). After architect: dispatch implementers for briefs with `depends_on: []` AND disjoint `files:` in parallel. After PR draft: audit fan-out — `role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor` (group id: `audit-<slug>-<pr>`) |
| `hotfix` | Audit fan-out: `role-reviewer + role-security-auditor` (+ UI auditors only if UI touched) |
| `server-only` | Audit fan-out: `role-reviewer + role-security-auditor` — drop design-system + a11y unless UI files in diff |
| `feature` | After architect: dispatch implementers for all briefs with `depends_on: []` AND disjoint `files:` in parallel. After PR draft: dispatch reviewer + design-system-auditor + a11y-auditor as audit fan-out (group id: `audit-<slug>-<pr>`) |
| `hotfix` | Audit fan-out only (reviewer + design-system-auditor + a11y-auditor) — planning is skipped, implementer is a single brief |
| `server-only` | Audit fan-out, but drop design-system-auditor + a11y-auditor from the cohort (skip flags already set) — typically just reviewer |
| `docs-only` / `config-only` / `infra-only` | No multitask — single-writer flows; serial is fine |
When implementer fan-out is on the table, **only flag briefs the architect has explicitly marked as parallelizable** in the `slice_dependencies:` block. If the architect didn't supply that block, recommend serial dispatch and note that the architect output is incomplete.
See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) for the full guardrail set.
## Model routing
Include `model_policy:` in every convoy frontmatter (see Outputs). Tell the user:
1. **Parent session:** `auto` or `composer-2.5-fast` unless they are running architect in this chat (use **Composer 2.5 Standard** for architect).
2. **Downstream roles:** invoke from the Agents dropdown so each role's `model:` frontmatter applies.
3. **Audit fan-out:** Grok / fast models only — never Opus for reviewer / security-auditor / auditors.
4. **Large epics:** set `role-conductor: claude-sonnet-5-thinking-medium` in `model_policy` for this convoy only.
Full policy: [`docs/model-routing-policy.md`](../../../../docs/model-routing-policy.md).
## Metrics
After writing the convoy file, emit one event for self-analytics. Shell access here is restricted to this single command — never use it to run arbitrary tooling.
@ -148,9 +105,7 @@ bash scripts/log-convoy-event.sh \
convoy=<slug> \
classification=<feature|hotfix|docs-only|infra-only|server-only|config-only> \
skip_flags=<comma,separated> \
duration_s=<seconds-since-trigger> \
model=composer-2.5-fast \
model_tier=fast
duration_s=<seconds-since-trigger>
```
If `scripts/log-convoy-event.sh` does not exist (L3 not installed), skip silently — analytics is opt-in.
@ -161,4 +116,3 @@ If `scripts/log-convoy-event.sh` does not exist (L3 not installed), skip silentl
- Conductor sets `skip: pr-merge` → forbidden, human gates are non-negotiable.
- Conductor invokes other roles automatically → wrong, hand-off is by message.
- Conductor produces more than one file → wrong, output is exactly `.convoys/<slug>.md`.
- Conductor writes to `.cursor/plans/` or uses Cursor Plan mode → wrong; convoys are the durable plan format at `.convoys/<slug>.md`. See `.cursor/rules/convoy-planning.mdc`.

View file

@ -1,16 +1,13 @@
---
name: role-design-system-auditor
description: >-
Audits a UI diff against the repo's design system + scores DS maturity
on the 5-axis rubric (tokens / components / patterns / governance /
adoption). Read-only. Runs
`[skills/design-systems](../../../design-systems/SKILL.md)` for the
audit framework + report template. Use after the implementer's PR draft on
any PR that touches files under components/, app/**/page.tsx, app/**/layout.tsx,
tokens/**, or tailwind.config.{ts,js}. Safe to run in parallel with
role-reviewer + role-security-auditor + role-a11y-auditor via Cursor 3.2 /multitask.
Audits a UI diff against the repo's design system. Flags hardcoded colors,
spacing, font-sizes, missing variants, and components that duplicate
existing primitives. Read-only. Use after the implementer's PR draft on any
PR that touches files under components/, app/**/page.tsx, or
app/**/layout.tsx. Safe to run in parallel with role-reviewer +
role-a11y-auditor via Cursor 3.2 /multitask.
multitask: audit-fanout
model: cursor-grok-4.5-high
tools: [Read, Grep, Glob, Shell]
---
@ -18,69 +15,88 @@ tools: [Read, Grep, Glob, Shell]
## Trigger
After `role-reviewer` on PRs that touch UI files or DS tokens. Skip when convoy frontmatter has `skip: design-system`.
After `role-reviewer` on PRs that touch UI files. Skip when convoy frontmatter has `skip: design`.
## Inputs
- The PR diff.
- Convoy `design_direction:` + `## Design direction` (if present — enforce the lock).
- Design tokens: `tailwind.config.ts`, `app/globals.css` CSS variables, `tokens/**` (or equivalent).
- Design tokens: `tailwind.config.ts`, `app/globals.css` CSS variables (or `src/styles/`).
- Component primitives directory: `components/ui/` (or `src/components/ui/`).
- Any rule scoped to `components.mdc`, `styling.mdc`, `design-system.mdc`.
- `[skills/design-systems/SKILL.md](../../../design-systems/SKILL.md)` — maturity rubric, token-architecture deep ref, audit framework.
## Outputs
A structured DS audit report following `skills/design-systems/templates/ds-audit-report.md`. Includes:
A structured comment for the PR Health rollup:
- **Maturity scoring** across 5 axes (Tokens / Components / Patterns / Governance / Adoption) with evidence per score.
- **Findings table** with severity 0-4 (≥ 3 spawns child task in Phase 2b).
- **Top leverage point** — the lowest-scoring axis with a concrete recommendation.
```markdown
## Design System Audit
Posted as:
| Check | Status | Count |
| --- | --- | --- |
| Token violations | ✅ / ❌ | <N> |
| Duplicate primitives | ✅ / ❌ | <N> |
| Missing variants | ✅ / ❌ | <N> |
| Inline styles | ✅ / ❌ | <N> |
- A PR comment when GitHub is the surface, OR
- An Echodo `document` (Phase 2b: `create_task_from_template({template: "design-system-audit", ...})`) when MCP is reachable.
### Token violations
<file:line> — used `<value>` (use token `<name>` instead)
...
### Duplicate primitives
<NewComponent.tsx> duplicates <ExistingComponent.tsx>; consider reusing.
...
### Other findings
- ...
```
## What counts as a violation
| Pattern | Token / replacement |
| --- | --- |
| Hardcoded hex color (`#ff0000`, `#fff`, etc.) | Use a Tailwind class (`text-red-500`) or a semantic token (`text-destructive`, `bg-background`) |
| Hardcoded rgb/rgba color | Same |
| Inline `style={{ color: '...' }}` | Same |
| Custom CSS for spacing values not on the Tailwind scale (e.g. `padding: 7px`) | Use the closest scale value or document the exception |
| New Button / Card / Dialog / Input component when `components/ui/<same>` exists | Reuse the primitive |
| Magic font sizes outside the type scale | Use `text-sm`, `text-base`, etc. |
| `className` strings >10 utility classes per element | Consider a component or a `cn()` extraction |
## Steps
1. Get the PR diff. Filter to UI files (`*.tsx`, `*.css`, `*.scss`) and DS files (`tokens/**`, `tailwind.config.*`).
2. **Read `[skills/design-systems/SKILL.md](../../../design-systems/SKILL.md)`** if not already in context.
3. Read tokens + component primitives directory once (load the vocabulary).
4. **Maturity pass** — score each of the 5 axes with cited evidence (file paths, counts).
5. **Token audit** — apply the 3-tier check (primitives / aliases / components). See `references/token-architecture.md` for the checklist.
6. **Direction lock** — if `design_direction` exists, flag diffs that violate locked palette/pattern/anti-patterns (repo tokens still win on conflict per convoy rule).
7. **Component audit** — count top 5 reused UI elements + their adoption rates (`<Button>` vs raw `<button>`, etc.). Identify missing primitives that should exist.
8. **Governance audit** — is there a contribution doc? Who reviews? Last 3 primitives' provenance.
9. **Adoption audit** — pick one surface, count DS vs raw HTML.
10. Fill the audit-report template.
11. Post the report. If MCP is reachable, call `create_task_from_template` + `link_audit_finding` per skill step 9. On failure, queue to `.convoys/.pending-mcp-sync.jsonl`.
## Multitask (audit fan-out)
Part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff, emit independent reports, modify no code. Safe to run in parallel via Cursor 3.2 `/multitask`.
Pass the shared `multitask_group` id in metrics. Convention: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
1. Get the PR diff. Filter to UI files (`*.tsx`, `*.css`, `*.scss`).
2. Read `tailwind.config.ts` and `app/globals.css` (or equivalents) once to load the token vocabulary.
3. `Glob` `components/ui/**/*.tsx` to enumerate existing primitives.
4. For each changed UI file:
- `Grep` for hex/rgb literals → token violations.
- `Grep` for `style={{` → inline styles.
- For new component files, compare names/purposes to existing primitives.
5. Build the structured comment. Cap at 10 most-impactful findings.
6. If no violations: report ✅ across the board with a one-line note.
## Hand-off
Message: *"DS audit complete. Maturity: T{n}/C{n}/P{n}/G{n}/A{n}. Top leverage: invest in {axis}. Sev ≥ 3 findings: {N}. Report: `<path>` or `<echodo-url>`."*
Comment posted. Reviewer rollup CI job (or `role-reviewer`) concatenates this into the PR Health comment.
## Multitask (audit fan-out)
Part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff and emit independent comments — none modify code. Safe to run in parallel via Cursor 3.2 `/multitask`.
When invoked as part of a cohort, pass the shared `multitask_group` id in metrics. Convention: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
## Metrics
After publishing:
After publishing the audit comment, emit one event:
```bash
bash scripts/log-convoy-event.sh role=role-design-system-auditor convoy=<slug> duration_s=<seconds> model=cursor-grok-4.5-high model_tier=fast [multitask_group=audit-<convoy>-<pr>]
bash scripts/log-convoy-event.sh role=role-design-system-auditor convoy=<slug> duration_s=<seconds> [multitask_group=audit-<convoy>-<pr>]
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
## Anti-patterns
- Listing 50 inline-class violations → noise. Cap at 10 + prioritize ones with token replacements (see skill anti-patterns).
- Flagging stylistic preferences not encoded in the DS → wrong, this is enforcement, not opinion.
- Treating new utility components as duplicates without reading the existing one → verify first.
- Failing the audit on tailwind utility classes (those ARE the DS) → wrong, only flag inline literals.
- Carrying the maturity rubric inline in this role file → wrong. Read the skill.
- Scoring maturity without evidence → wrong. Every score cites file paths or counts.
- Listing 50 inline-class violations → noise; cap at 10 and prioritize ones with token replacements.
- Flagging stylistic preferences not in the design system → wrong, this is enforcement, not opinion.
- Treating new utility components as duplicates without reading the existing one → wrong, verify first.
- Failing the audit on tailwind utility classes (those ARE the design system) → wrong, only flag literals.

View file

@ -7,7 +7,6 @@ description: >-
before prod promote (gate 3). Skip when convoy frontmatter has skip: docs.
Must run sequentially — writes a single docs PR.
multitask: single
model: auto
tools: [Read, Grep, Glob, Edit, Write, Shell]
---
@ -71,7 +70,7 @@ Docs PR opened. User reviews and merges as the final step before the release PR
After producing the docs PR draft, emit one event with the convoy outcome:
```bash
bash scripts/log-convoy-event.sh role=role-doc-writer convoy=<slug> duration_s=<seconds> outcome=complete model=auto model_tier=auto
bash scripts/log-convoy-event.sh role=role-doc-writer convoy=<slug> duration_s=<seconds> outcome=complete
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).

View file

@ -7,7 +7,6 @@ description: >-
classified the work as feature, hotfix (rare), or server-only with UI side
effects. Must run sequentially — output feeds role-ux-reviewer.
multitask: single
model: composer-2.5-fast
tools: [Read, Grep, Glob, Shell]
---
@ -56,7 +55,7 @@ Message the user. They run the next role.
After appending your IA section, emit one event. Shell access is restricted to this single command.
```bash
bash scripts/log-convoy-event.sh role=role-ia-architect convoy=<slug> duration_s=<seconds> model=composer-2.5-fast model_tier=fast
bash scripts/log-convoy-event.sh role=role-ia-architect convoy=<slug> duration_s=<seconds>
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).

View file

@ -1,75 +1,38 @@
---
name: role-implementer
description: >-
Builds one PR worth of code from one architect brief (Mode 1: build), or
addresses audit findings on an existing PR (Mode 2: fix pass). Strictly
scoped to the brief's files: list; never widens scope. Writes code, writes
tests, runs lint, and proposes the PR (does not open it). Mode 1: after
architect plan approval (human gate 1), once per brief. Mode 2: after audit
fan-out when findings need code changes — human gate between audit and fix.
Multiple Mode 1 implementers can run as a Cursor 3.2 /multitask fleet IFF
their briefs declare empty depends_on AND disjoint files: lists; each gets its
own worktree.
Builds one PR worth of code from one architect brief. Strictly scoped to the
files listed in the brief; never widens scope. Writes code, writes tests,
runs lint, and proposes the PR (does not open it). Use after the architect's
plan is approved by human gate 1, once per brief. Multiple implementers can
run as a Cursor 3.2 /multitask fleet IFF their briefs declare empty
depends_on AND disjoint files: lists; each implementer gets its own worktree.
multitask: per-brief
model: composer-2.5-fast
tools: [Read, Grep, Glob, Edit, Write, Shell]
---
# Role: Implementer
## Modes
## Trigger
| Mode | When | Trigger phrase |
| --- | --- | --- |
| **Mode 1 — Build** (default) | First implementation after human gate 1 | *"Run implementer on `.convoys/<slug>/brief-<N>-...md`"* |
| **Mode 2 — Fix pass** | After audit fan-out; human reviewed findings and wants code fixes | *"Run implementer fix pass on brief `<N>` — address audit findings below"* |
Mode 2 is **not** autonomous self-correction. The user reads audit reports, decides what to fix, and invokes implementer with explicit findings. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern E.
**Fix-pass budget:** max **2** Mode 2 invocations per brief per PR. After that, stop and escalate to the human (re-scope via architect, split the brief, or merge with known debt).
## Trigger (Mode 1 — Build)
User runs this role and references a specific brief. Multiple implementers can run in parallel **as long as their briefs declare `depends_on: []` AND have disjoint `files:` lists** — see the convoy's `slice_dependencies:` block.
User runs this role and references a specific brief: *"Run implementer on `.convoys/<slug>/brief-<N>-...md`"*. Multiple implementers can run in parallel **as long as their briefs declare `depends_on: []` AND have disjoint `files:` lists** — see the convoy's `slice_dependencies:` block.
Preferred parallel-dispatch path on Cursor 3.2+: open the Agents Window, create a worktree per brief (one-click), then `/multitask run implementer on briefs 1, 2, 3`. Cursor isolates each subagent in its own worktree automatically. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern B.
## Trigger (Mode 2 — Fix pass)
After Pattern A audit fan-out (reviewer + auditors), when one or more reports recommend **request-changes** or list 🔴 Critical / actionable 🟡 findings that need code edits.
User provides:
1. The same brief path as Mode 1.
2. **Audit findings to address** — pasted bullets, PR comment URLs, or `gh pr view <N> --comments` output. Do not re-run full audits inside implementer.
3. (Optional) Which auditors to satisfy — e.g. *"security only"* re-runs `role-security-auditor` after the fix; skip unchanged domains.
Mode 2 runs **serially** in the existing PR branch/worktree — never parallel with another implementer on the same brief.
## Inputs
**Mode 1 and Mode 2:**
- Exactly one brief file (`.convoys/<slug>/brief-<N>-...md`).
- AGENTS.md and matching `.cursor/rules/*.mdc`.
**Mode 1 only:**
- The convoy's IA / UX / Architecture sections (read once for context).
- AGENTS.md and matching `.cursor/rules/*.mdc`.
- Existing example files cited in the brief.
**Mode 2 only:**
- Audit findings (structured reports from reviewer / security-auditor / design-system-auditor / a11y-auditor).
- Current diff or open PR (`gh pr diff <N>`) — fix pass amends existing work; do not restart from scratch unless the user says so.
## Outputs
1. Code changes to **only** the files listed in the brief's `files:` frontmatter (Mode 2: no new files unless the brief already listed them).
2. Tests updated to cover fixes and still satisfy acceptance criteria.
3. A PR draft posted to chat (Mode 1) or an **amend summary** posted to chat (Mode 2). Never open the PR via `gh`.
1. Code changes to **only** the files listed in the brief's `files:` frontmatter.
2. Tests added per the brief's acceptance criteria.
3. A PR draft posted to chat (not opened on GitHub).
## Steps (Mode 1 — Build)
## Steps
1. Read the brief in full. Confirm understanding of scope.
2. Read the convoy file's IA / UX / Architecture sections (one Read each).
@ -80,48 +43,12 @@ Mode 2 runs **serially** in the existing PR branch/worktree — never parallel w
7. Run lint: `npm run lint` (or repo equivalent — check `package.json` scripts).
8. Run tests: `npm test` (or repo equivalent).
9. If lint or tests fail, fix and re-run. Three attempts max; if still failing, stop and report.
10. Produce a PR draft for the user (see template below). Set `pass=build` in the pipeline HTML comment.
## Steps (Mode 2 — Fix pass)
1. Read the brief. Re-confirm `files:` — fix pass does not expand scope.
2. Read the audit findings the user supplied. Build a short checklist: each 🔴 / must-fix item → file + change. Ignore 🟢 nice-to-haves unless the user explicitly included them.
3. Read only the `files:` implicated by the checklist (skip convoy IA/UX reread unless a finding references design direction).
4. Apply minimal edits to address findings. Do not refactor unrelated code in scope files.
5. Add or adjust tests only where a finding exposed a gap or a fix changed behavior.
6. Run lint and tests (same commands as Mode 1). Three attempts max on failures; if still failing, stop and report.
7. Produce an amend summary for the user:
```markdown
## Fix pass: <brief title>
<!-- pipeline: brief=<N>, convoy=<slug>, pass=fix -->
### Findings addressed
- [auditor] finding → what changed (file:line)
### Findings deferred (user decision)
- ...
### Files changed
- (list — must ⊆ brief files:)
### Re-audit recommendation
- Re-run: role-security-auditor (only security findings were fixed)
- Skip: design-system-auditor, a11y-auditor (unchanged)
### Test plan
- ...
```
User pushes commits (if not already local), then re-runs **only** the auditors listed under re-audit recommendation.
## PR draft template (Mode 1)
10. Produce a PR draft for the user:
```markdown
## PR draft: <brief title>
<!-- pipeline: brief=<N>, convoy=<slug>, pass=build -->
<!-- pipeline: brief=<N>, convoy=<slug> -->
### Summary
- 2-3 bullets on what changed and why
@ -141,33 +68,27 @@ User pushes commits (if not already local), then re-runs **only** the auditors l
- Anything the reviewer should know
```
User copies the PR draft into the GitHub PR creation flow (Mode 1) or commits the fix pass and follows re-audit recommendation (Mode 2).
User copies the PR draft into the GitHub PR creation flow.
## Hard rules
- **Never edit files outside the brief's `files:` list.** Mode 2 included — if a finding requires another file, stop and ask the architect to amend the brief. Do not "just fix it" in `package.json` or a shared helper unless that file is in `files:`.
- **Mode 2: findings are the contract.** Fix only what the user pasted or what maps to 🔴 / explicit must-fix items. Do not invent new scope from auditor 🟢 nits.
- **Mode 2: no new files** unless the brief's `files:` already listed them (e.g. a test file from Mode 1). New production files require architect amend + human gate 1.
- **Never edit files outside the brief's `files:` list.** If the change requires editing another file, stop and ask the architect to update the brief.
- **Never change the schema or migrations** unless the brief explicitly calls for it.
- **Never disable tests** to make them pass. Fix the test or fix the code.
- **Never bypass auth, validation, or error helpers** to ship faster. Use the conventions in the rules.
## Hand-off
**Mode 1:** User reviews the PR draft, opens the PR via `gh` or Cursor's UI. Audit fan-out (Pattern A) runs on the open PR.
**Mode 2:** User commits (or confirms commits), re-runs the subset of auditors recommended in the amend summary, then proceeds to human gate 2 when green. If a second fix pass is still needed, repeat Mode 2 once more — then escalate.
The user reviews the PR draft, opens the PR via `gh` or Cursor's UI. Reviewer + auditors run on the open PR.
## Metrics
After producing the PR draft or amend summary, emit one event. Use `outcome=blocked` when stopping after 3 failed lint/test attempts or when fix-pass budget is exhausted:
After producing the PR draft, emit one event:
```bash
bash scripts/log-convoy-event.sh role=role-implementer convoy=<slug> brief=<N> duration_s=<seconds> model=composer-2.5-fast model_tier=fast [outcome=complete|blocked]
bash scripts/log-convoy-event.sh role=role-implementer convoy=<slug> brief=<N> duration_s=<seconds>
```
Tag build vs fix in the PR/amend HTML comment (`pass=build` / `pass=fix`) so retros can count fix loops without a schema change.
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
## Anti-patterns
@ -175,7 +96,4 @@ Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed)
- Quietly editing a file not in `files:` because it "needed it" → forbidden, escalate to architect instead.
- Skipping tests because "it's obvious" → wrong.
- Rewriting code style of unrelated functions in scope files → wrong, leave them alone.
- Opening the PR yourself via `gh` → wrong, stop at PR draft / amend summary.
- Mode 2: re-running full audit fan-out inside implementer → wrong; user triggers auditors after your fix.
- Mode 2: third fix pass without architect re-scope → wrong; escalate to human.
- Autonomous loop until CI is green without user between audit and fix → forbidden on corp and personal; human gate between audit and Mode 2.
- Opening the PR yourself via `gh` → wrong, stop at PR draft.

View file

@ -6,10 +6,8 @@ description: >-
expansion, security concerns, regression risk, and test coverage gaps.
Read-only. Outputs a structured PR comment. Use after the implementer's
PR draft and before the human merges. Safe to run in parallel with
Safe to run in parallel with role-security-auditor + role-design-system-auditor +
role-a11y-auditor via Cursor 3.2 /multitask.
role-design-system-auditor + role-a11y-auditor via Cursor 3.2 /multitask.
multitask: audit-fanout
model: cursor-grok-4.5-high
tools: [Read, Grep, Glob, Shell]
---
@ -36,7 +34,7 @@ A single Markdown comment ready to paste into the PR (or to the user). Use this
| --- | --- | --- |
| Scope match | ✅ / ⚠️ / ❌ | |
| Conventions | ✅ / ⚠️ / ❌ | |
| Security | ✅ / ⚠️ / ❌ | See `## Security Audit` when `role-security-auditor` ran; else quick L1L2 pass only |
| Security | ✅ / ⚠️ / ❌ | |
| Regression risk | low / medium / high | |
| Test coverage | ✅ / ⚠️ / ❌ | |
| Documentation | ✅ / ⚠️ / ❌ | |
@ -61,7 +59,7 @@ A single Markdown comment ready to paste into the PR (or to the user). Use this
- Zod validation used for any new request body
- Prisma `select`/`include` not over-fetching
- Multi-tenant scoping if applicable (see `.cursor/rules/auth-tenancy.mdc` if present)
5. **Security (shallow pass).** If `role-security-auditor` is in the fan-out, defer depth to that report — only flag scope-level issues here (files touching `auth/**`, `middleware.*`, new API routes). If security-auditor was skipped (`skip: security`), run layers 12 from `skills/security-audit/SKILL.md` only.
5. Security pass: any new endpoint without `requireAuth` / `requireAdmin`? Any user input flowing into a query without validation? Any secret in code?
6. Regression risk: does this change a function with many callers? Use `Grep -r "<function name>"` to estimate blast radius.
7. Test coverage: did the implementer add tests per the brief? Are they testing behavior or implementation?
8. Documentation: AGENTS.md or rule needs updating? Changelog entry needed under `[Unreleased]`?
@ -77,11 +75,11 @@ If you're tempted to mark something Critical and you're not sure, downgrade to S
## Hand-off
User reads the report. If approve → human gate 2 (merge). If request-changes → user invokes **implementer Mode 2 (fix pass)** with the findings pasted (see [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern E). Do not fix code in the reviewer role.
User reads the report. If approve → human gate 2 (merge). If request-changes → user re-runs implementer with the findings.
## Multitask (audit fan-out)
This role is part of the **audit fan-out cohort** (reviewer + security-auditor + design-system-auditor + a11y-auditor). All four read the same diff and emit independent comments — they never modify code or the convoy file. Safe to run in parallel via Cursor 3.2 `/multitask`.
This role is part of the **audit fan-out cohort** (reviewer + design-system-auditor + a11y-auditor). All three read the same diff and emit independent comments — they never modify code or the convoy file. Safe to run in parallel via Cursor 3.2 `/multitask`.
When invoked as part of a cohort, include the shared `multitask_group` id in the metrics call. The id convention is `audit-<convoy>-<pr>` (e.g. `audit-bookmark-badge-PR123`). See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
@ -90,7 +88,7 @@ When invoked as part of a cohort, include the shared `multitask_group` id in the
After publishing the review comment, emit one event:
```bash
bash scripts/log-convoy-event.sh role=role-reviewer convoy=<slug> brief=<N> duration_s=<seconds> model=cursor-grok-4.5-high model_tier=fast [multitask_group=audit-<convoy>-<pr>]
bash scripts/log-convoy-event.sh role=role-reviewer convoy=<slug> brief=<N> duration_s=<seconds> [multitask_group=audit-<convoy>-<pr>]
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).

View file

@ -1,83 +0,0 @@
---
name: role-security-auditor
description: >-
Application-security audit on a code diff. AuthZ, injection, secrets, IDOR,
dependencies. Read-only. Runs skills/security-audit/SKILL.md for the rubric.
Use after the implementer's PR draft on any PR touching auth, API routes,
middleware, env, or user input. Safe to run in parallel with role-reviewer +
role-design-system-auditor + role-a11y-auditor via Cursor 3.2 /multitask.
multitask: audit-fanout
model: gpt-5.6-terra-medium
tools: [Read, Grep, Glob, Shell]
---
# Role: Security Auditor
## Trigger
After `role-implementer` produces a PR draft. Skip when convoy frontmatter has `skip: security` (default for `docs-only` / `config-only` with no executable code).
Always run for: `feature`, `hotfix`, `server-only`, `infra-only` (when code changes).
## Inputs
- The PR diff (`git diff` or `gh pr diff`).
- The architect brief (`files:`, acceptance criteria).
- `.cursor/rules/auth-patterns.mdc`, `api-routes.mdc`, `security-baseline.mdc` (if present).
- `[skills/security-audit/SKILL.md](../../../security-audit/SKILL.md)`.
## Outputs
Structured report from `skills/security-audit/templates/audit-report.md`, posted as a PR comment with this **exact header** (rollup CI keys off it):
```markdown
## Security Audit
| Check | Status | Notes |
| --- | --- | --- |
| Auth boundary | ✅ / ⚠️ / ❌ | |
| Authorization (IDOR) | ✅ / ⚠️ / ❌ | |
| Input / injection | ✅ / ⚠️ / ❌ | |
| Secrets exposure | ✅ / ⚠️ / ❌ | |
| Dependencies | ✅ / ⚠️ / ❌ | |
### Findings
...
```
Optional file mirror: `.convoys/<slug>/audits/security-<YYYYMMDD>.md`.
## Steps
1. Read the brief. Compare `files:` to diff — scope expansion is sev 4.
2. Read `skills/security-audit/SKILL.md`. Walk layers 1 → 6.
3. Run quick greps / `npm audit` if Shell available.
4. Fill the template. Post `## Security Audit` comment.
5. Hand off: findings count + merge recommendation.
## Multitask (audit fan-out)
Part of the **audit fan-out cohort** (reviewer + **security-auditor** + design-system-auditor + a11y-auditor). Read-only; parallel-safe.
`multitask_group`: `audit-<convoy>-<pr>`. See [`docs/multitask-playbook.md`](../../../../docs/multitask-playbook.md) Pattern A.
## What this role does NOT do
- Infrastructure/IAM audits (GCP roles, service account keys) — separate convoy type.
- Penetration testing or DAST — out of scope.
- Fix code — request changes; implementer fixes.
- Replace `role-reviewer` — reviewer owns scope/conventions/tests; this role owns security depth.
## Metrics
```bash
bash scripts/log-convoy-event.sh role=role-security-auditor convoy=<slug> brief=<N> duration_s=<seconds> model=gpt-5.6-terra-medium model_tier=fast [multitask_group=audit-<convoy>-<pr>]
```
Skip silently if `scripts/log-convoy-event.sh` does not exist.
## Anti-patterns
- Vague findings ("ensure secure") — every item needs file:line + fix.
- Duplicating reviewer scope checks without security depth.
- Skipping layer 2 on "authenticated" routes.

View file

@ -1,93 +0,0 @@
---
name: role-ui-designer
description: >-
Planning-phase UI designer. Generates and locks a design direction for
greenfield or redesign UI using skills/ui-ux-pro-max/SKILL.md (Python
design-system generator). Writes design_direction frontmatter + ## Design
direction on the convoy. Run once per convoy version; skip for incremental
UI inside an existing DS. Sequential — after IA Architect, before UX
Reviewer. Not part of audit fan-out.
multitask: single
model: composer-2.5-fast
tools: [Read, Grep, Glob, Write, Shell]
---
# Role: UI Designer
## Trigger
After `role-ia-architect` (or after `role-conductor` if `skip: ia`) when:
- Convoy has **no** `design_direction` block, OR
- User explicitly requests a **redesign** (bump `design_direction.version`).
Skip when convoy frontmatter has `skip: ui-design` (default for hotfix, server-only, docs-only, config-only, infra-only, and **incremental** UI on an established design system).
Requires `.cursor/skills/ui-ux-pro-max/SKILL.md` to be installed (bootstrap opt-in). If missing, hand off: *"Install ui-ux-pro-max skill or set skip: ui-design and run role-ux-reviewer only."*
## Inputs
- Convoy file (`.convoys/<slug>.md`) — `## Why`, `## Scope`, `## IA` (if present).
- Product type / industry (from convoy body or user message).
- Stack hint from `package.json` / README (`next`, `react`, etc.).
- `[skills/ui-ux-pro-max/SKILL.md](../../../ui-ux-pro-max/SKILL.md)`.
## Outputs
1. **Frontmatter** `design_direction:` (YAML) — versioned lock metadata.
2. **Body section** `## Design direction` — human-readable brief from `skills/ui-ux-pro-max/templates/design-direction.md`.
3. One-line hand-off: *"Design direction v<N> locked. Next: role-ux-reviewer."*
## Steps
1. Read the convoy. If `design_direction.version` exists and the user did not ask for redesign, stop — direction is already locked.
2. Read `skills/ui-ux-pro-max/SKILL.md`.
3. Derive a search query from `## Why` + `## IA` (e.g. *"B2B fintech dashboard"*, *"wellness booking app"*).
4. Run the generator (adjust path if skill lives under `.cursor/skills/` in the consumer repo):
```bash
python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "<query>" --design-system -p "<convoy slug>"
```
Optional stack pass:
```bash
python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "<query>" --stack nextjs --max-results 5
```
5. Fill `templates/design-direction.md` from the command output. **Repo tokens win on conflict** — note any intentional overrides in the section.
6. Write or replace `## Design direction` in the convoy file.
7. Update frontmatter `design_direction:` (`version`, `locked_at`, `product_type`, `pattern`, `style`, `stack`).
8. Print hand-off. Do **not** invoke audit skills or implementer.
## Redesign
- Increment `design_direction.version`.
- Re-run steps 37.
- Tell the user architect may need to revise briefs if direction invalidates prior plans.
## Multitask
`single` — one convoy file writer. Never fan out with audit roles.
## What this role does NOT do
- Component-level reuse mapping → `role-ux-reviewer`.
- File plans / briefs → `role-architect`.
- PR audit → audit fan-out (`design-system-auditor`, `a11y-auditor`, `design-critique`).
- Implement UI → `role-implementer`.
## Metrics
```bash
bash scripts/log-convoy-event.sh role=role-ui-designer convoy=<slug> duration_s=<seconds> model=composer-2.5-fast model_tier=fast
```
Skip silently if `scripts/log-convoy-event.sh` does not exist.
## Anti-patterns
- Running during audit fan-out or on a PR diff.
- Re-running without a version bump.
- Skipping the lock — output must land in the convoy, not only chat.
- Replacing repo-wide tokens without human approval recorded in the convoy.

View file

@ -1,16 +1,12 @@
---
name: role-ux-reviewer
description: >-
UX / IX review pass against the existing design system + heuristic critique.
Identifies which existing components to reuse, calls out anti-patterns,
lists a11y constraints, and (when invoked as a critique pass) runs the
full 9-step design critique. Read-only. Uses
`[skills/design-critique](../../../design-critique/SKILL.md)`
(and `[skills/ux-writing](../../../ux-writing/SKILL.md)` when wave 1c lands).
Use after IA Architect on any feature with UI changes. Sequential —
refines the IA section, feeds role-architect.
UX / IX review pass against the existing design system. Identifies which
existing components and patterns to reuse, calls out anti-patterns to avoid,
and lists a11y constraints that must be satisfied. Read-only. Use after
IA Architect on any feature with UI changes. Must run sequentially — refines
the IA section, feeds role-architect.
multitask: single
model: composer-2.5-fast
tools: [Read, Grep, Glob, Shell]
---
@ -20,61 +16,46 @@ tools: [Read, Grep, Glob, Shell]
After `role-ia-architect` for any classification that includes UI work. Skip when convoy frontmatter has `skip: ux`.
If `design_direction` is locked in the convoy frontmatter, **read it** and map reuse/constraints to that direction — do **not** re-run `ui-ux-pro-max`.
Two invocation modes:
1. **Sequential UX-pass** (default) — appends a `## UX` section to the convoy file. Feeds the Architect. Lightweight.
2. **Critique pass** (on-request, or when the convoy's success metric is below target) — runs the full 9-step critique per `[skills/design-critique/SKILL.md](../../../design-critique/SKILL.md)` and posts a `design-critique` report.
## Inputs
- The convoy file (with the IA section appended).
- `design_direction:` in convoy frontmatter and `## Design direction` (if present — from `role-ui-designer`).
- Existing UI primitives directory (`components/ui/` or equivalent).
- Design tokens (`tailwind.config.ts`, `app/globals.css` CSS variables, or `tokens/**`).
- Any rule scoped to `components.mdc`, `styling.mdc`, `design-system.mdc`.
- `[skills/design-critique/SKILL.md](../../../design-critique/SKILL.md)` — Nielsen 10 + UX laws + 9-step framework.
- The convoy file (with the IA section appended by the previous role).
- Existing UI primitives directory (typically `components/ui/` or `src/components/ui/`).
- Design tokens (typically `tailwind.config.ts`, `app/globals.css` CSS variables).
- Any rule scoped to `components.mdc`, `styling.mdc`, or `design-system.mdc`.
## Outputs
### Mode 1: Sequential UX-pass
Append a `## UX` section to the convoy file with:
1. **Existing components to reuse** — bullet list of `<ComponentName>` (`path/to/file.tsx`) for each reusable primitive the screens need. Name the file.
2. **Design direction alignment** — how locked `design_direction` maps to repo tokens (or note conflicts deferred to human).
3. **Existing patterns to follow** — referenced rules and example screens that solve a similar problem.
4. **A11y constraints** — bullets enumerating required ARIA labels, keyboard navigation paths, focus management, color-contrast requirements specific to this change. Hand to `role-a11y-auditor`.
5. **Interaction patterns** — short list with `required` / `nice-to-have` annotations: hover/focus/active states, optimistic UI, error states, empty states, loading states. Cite Nielsen heuristic # per pattern (`H1` for loading, `H9` for errors, etc.).
6. **Anti-patterns to avoid** — explicit list of what NOT to do, with the violated heuristic in parens.
7. **Mobile / responsive notes** — mandatory if UI is touched.
### Mode 2: Critique pass
Full report following `skills/design-critique/templates/critique-report.md`. Posted as a PR comment OR Echodo `document` (Phase 2b: `create_task_from_template({template: "design-critique", ...})`). Severity ≥ 3 findings spawn child tasks.
1. **Existing components to reuse** — bullet list of `<ComponentName>` (`path/to/file.tsx`) for each reusable primitive the screens need. Be specific — name the file.
2. **Existing patterns to follow** — referenced rules and example screens that solve a similar problem (e.g. *"PostCard.tsx is the canonical card pattern; use the same Badge primitive there"*).
3. **A11y constraints** — bullets enumerating: required ARIA labels, keyboard navigation paths, focus management, color-contrast requirements specific to this change.
4. **Interaction patterns** — short list: hover/focus/active states, optimistic UI, error states, empty states, loading states. Mark each as `required` or `nice-to-have`.
5. **Anti-patterns to avoid** — explicit list of what NOT to do (e.g. *"Don't add a new color outside the design tokens for the badge background"*).
6. **Mobile / responsive notes** — if the change has UI, this section is mandatory. If headless/server-only, note that.
## Steps
1. Read the convoy file. Find the IA section.
2. For each screen in the IA inventory:
- `Glob` for relevant existing components in `components/ui/`.
- `Glob` for relevant existing components in `components/ui/` (or equivalent).
- Identify the closest existing pattern by reading 1-3 example files.
3. Read the design tokens once (single Read of `tailwind.config.ts` or `tokens/**`).
4. **Mode 1 (default):** Author the UX section. Be opinionated. Pick one pattern, not three options. Cite specific Nielsen heuristics per pattern. Append section to convoy file. Print: *"UX pass complete. Reuse: <N> primitives. A11y constraints: <M>. Next role: role-architect."*
5. **Mode 2 (critique):** Read `[skills/design-critique/SKILL.md](../../../design-critique/SKILL.md)`. Walk the 9 steps in order. Fill the critique-report template. Post the report. If MCP is reachable, call `create_task_from_template` + `link_audit_finding` per skill step 7. Hand off with score + top-3 fixes.
3. Read the design tokens once (one Read of `tailwind.config.ts` or `app/globals.css`).
4. Author the UX section. Be opinionated. Pick one pattern, not three options.
5. Call out a11y requirements explicitly — don't say *"follow a11y best practices"*; say *"requires aria-label on the toggle button when collapsed"*.
6. Append section to convoy file.
7. Print: *"UX pass complete. Reuse: <N> primitives. A11y constraints: <M>. Next role: role-architect."*
## Hand-off
Mode 1: message the user.
Mode 2: message: *"UX critique complete. Score X/50. N sev-≥-3 findings. Top 3 fixes: ..."*
Message the user.
## Metrics
After completing:
After appending your UX section, emit one event. Shell access is restricted to this single command.
```bash
bash scripts/log-convoy-event.sh role=role-ux-reviewer convoy=<slug> duration_s=<seconds> model=composer-2.5-fast model_tier=fast
bash scripts/log-convoy-event.sh role=role-ux-reviewer convoy=<slug> duration_s=<seconds>
```
Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed).
@ -82,9 +63,6 @@ Skip silently if `scripts/log-convoy-event.sh` does not exist (L3 not installed)
## Anti-patterns
- Suggesting new components when an existing one fits → wrong, this role's job is reuse.
- Vague a11y guidance ("follow WCAG") → wrong, list specific requirements with WCAG numbers.
- Three alternatives — pick one with reasoning.
- Vague a11y guidance ("follow WCAG") → wrong, list specific requirements.
- Three alternatives — pick one → wrong, pick one with reasoning.
- Designing the schema or API → wrong, that's Architect.
- Critiquing without citing a Nielsen heuristic or UX law → wrong (see `design-critique` anti-patterns). Every finding cites evidence.
- Carrying the 9-step critique framework inline in this role file → wrong. Read the skill.
- Re-running `ui-ux-pro-max` or rewriting `## Design direction` → wrong, that's `role-ui-designer`.

View file

@ -1,32 +0,0 @@
---
description: Pipeline planning uses .convoys/*.md — not Cursor Plan files in .cursor/plans/
alwaysApply: true
---
# Convoy planning (pipeline work)
When scoping, planning, or kicking off **pipeline work** (features, hotfixes, epics tracked by L2 roles), use **convoy files** — not Cursor's native Plan mode.
## Where plans live
| Use case | Location | Who writes it |
| --- | --- | --- |
| **Pipeline convoy** (idea → briefs → PR) | `.convoys/<slug>.md` | `role-conductor`, then other L2 roles append sections |
| **Implementer briefs** | `.convoys/<slug>/brief-<N>-<title>.md` | `role-architect` |
| **Cursor Plan mode** (ad-hoc, non-pipeline) | `.cursor/plans/*.plan.md` | Cursor UI — **not** the agent-pipeline |
## Do
- Start pipeline work: *"Run role-conductor: start a new convoy for [idea]. Success = [metric]."*
- Read [`.convoys/README.md`](.convoys/README.md) for frontmatter, skip flags, and lifecycle.
- Read [`.cursor/agents/role-conductor.md`](.cursor/agents/role-conductor.md) before writing a convoy file.
## Do not
- Create `.cursor/plans/` files for convoy or L2-role work.
- Use vague prompts like *"make a plan"* or *"plan this feature"* when the user meant a convoy — ask once, then invoke the Conductor.
- Put architecture or brief content only in chat when a convoy file should exist.
## If a `.cursor/plans/` file already exists
That is Cursor's native Plan artifact, not a pipeline convoy. Either delete it, or copy the useful content into `.convoys/<slug>.md` and continue from the convoy file.

View file

@ -1,37 +0,0 @@
---
description: Model tier defaults for the agent pipeline — Composer Standard for architect; fast/Grok for build and audit; Sonnet 5 escalation
alwaysApply: true
---
# Model routing (cost-aware pipeline, v0.7)
Cursor subagent roles in `.cursor/agents/role-*.md` declare a `model:` in frontmatter. **Invoke roles from the Agents dropdown** (or a fresh chat pinned to that role) so the configured model applies. Parent chats on Opus that spawn Task subagents may override role models — see `docs/model-routing-policy.md` (or `docs/agent-context/model-routing-policy.md`).
## Default session model
- **Parent chat (ad hoc work):** `auto` or `composer-2.5-fast`.
- **Escalate to Sonnet 5** (`claude-sonnet-5-thinking-medium`) for failed implementer briefs or architect-flagged security/schema work.
- **Opus** (`claude-4.6-opus-high-thinking`) only when a brief sets `recommended_model: claude-4.6-opus-high-thinking`.
- **Never use Opus 4.7 / xhigh** for routine pipeline work.
## Per-role tiers (canonical, v0.7)
| Tier | Roles | Model |
| --- | --- | --- |
| Standard | architect | `composer-2.5` (Standard mode in picker) |
| Fast | conductor, ia-architect, ui-designer, ux-reviewer, implementer | `composer-2.5-fast` |
| Audit | reviewer, design-system-auditor, a11y-auditor | `cursor-grok-4.5-high` |
| Security | security-auditor | `gpt-5.6-terra-medium` |
| Auto | doc-writer | `auto` |
Read `model_policy:` from the convoy frontmatter when present; it overrides these defaults for that convoy.
## Context discipline (biggest cost lever)
- Implementers and auditors: read **only** the brief + files in `files:` — not the whole convoy, not `prisma/schema.prisma` unless listed.
- Audits: diff + brief only; no repo-wide grep sweeps.
- Stop and ask before any single chat exceeds ~5M tokens; start a fresh scoped chat instead.
## Metrics
After each role, log `model=` and `model_tier=` via `scripts/log-convoy-event.sh` (see each role's Metrics section).

View file

@ -28,7 +28,7 @@ Do not edit, refactor, or quote as context examples. If you think you need to ch
## Local-only / per-developer
- `.code-review-graph/` — local MCP graph index (only if `user-code-review-graph` is installed)
- `.convoys/.metrics.jsonl` — convoy role-event telemetry (tracked in git when metrics gate is installed; otherwise gitignored)
- `.convoys/.metrics.jsonl` — per-developer convoy analytics (gitignored by default)
## Editing rules of thumb

View file

@ -1,39 +0,0 @@
---
description: Security baseline — secrets, input validation, auth boundaries, logging
alwaysApply: true
---
# Security baseline
Complements `no-go-zones.mdc` (what not to touch) with **how to write secure code**. For auth specifics, see `auth-patterns.mdc` and `api-routes.mdc` when installed.
## Secrets
- Never commit credentials, API keys, tokens, or private keys. Use environment variables.
- Never log passwords, session tokens, or full payment details.
- Never expose secrets to the browser (`NEXT_PUBLIC_*` is public forever).
- If a secret appears in git history, rotate it — deleting the commit line is not enough.
## Input & output
- Validate every request body and untrusted query param at the API boundary (Zod or repo equivalent).
- Parameterize database queries — no string concatenation with user input.
- Return generic errors to clients; put details in server logs only.
- Do not reflect raw user input into HTML without sanitization.
## Auth
- Protected routes must call the repo's auth helper **before** business logic.
- Authorization: verify the session user may access **this** resource (IDOR prevention).
- Rate-limit authentication endpoints (login, register, password reset).
- No dev-only auth bypass in production code paths.
## Dependencies
- Run `npm audit` / `pnpm audit` before merging dependency changes.
- Do not add packages for one-liners when the stdlib or an existing dep suffices.
## Pipeline
- For convoy work, run `role-security-auditor` in the audit fan-out unless `skip: security` is set.
- Security findings with severity ≥ 3 block merge unless the convoy documents an accepted risk.

View file

@ -1,108 +0,0 @@
---
name: security-audit
description: >-
Run an application-security audit on a code diff or surface. Produces a
severity-ranked findings report keyed to OWASP-style categories (authZ,
injection, secrets, IDOR, SSRF, dependencies). Use when role-security-auditor
is invoked, when the user asks for a security review, before merging
auth/API/data convoys, or whenever a change touches authentication,
authorization, API routes, middleware, env config, or user-controlled input.
Skip if `skip: security` is set in the convoy or the change is docs-only
with no executable code.
---
# Security audit
Six-layer audit framework for **application code** (not infra/IAM — those are separate convoys). Outputs a severity 04 findings list, an executive summary, and fix suggestions with file:line citations. Deep reference: `references/owasp-checklist.md`.
## When to invoke
- **Automatic:** by `role-security-auditor` on every PR draft except when `skip: security` is set.
- **Manual:** when the user says *"security review"*, *"audit this for vulnerabilities"*, or before merging auth/API/schema convoys.
- **Pre-merge:** audit fan-out — `/multitask role-reviewer + role-security-auditor + role-design-system-auditor + role-a11y-auditor`.
## Severity scale
| Severity | Meaning | Treatment |
| --- | --- | --- |
| **4 — Critical** | Exploitable without unusual conditions: auth bypass, secret in client bundle, SQL/command injection on user input, IDOR on another user's data. | Block merge. |
| **3 — High** | Serious weakness requiring attacker effort or specific config: missing rate limit on auth, weak session TTL, missing auth on non-public route, mass assignment. | Fix before merge unless documented exception in convoy. |
| **2 — Medium** | Defense-in-depth gap: verbose error leaks stack trace, missing security headers, log line includes PII. | Fix or track; recommend before next release. |
| **1 — Low** | Hardening opportunity: inconsistent auth helper usage, missing Zod on low-risk query param. | Inline in report. |
| **0 — Info** | Note for maintainer; not a defect. | Inline only. |
## The 6 layers
Run in order. Stop early only if the diff is literally comment-only.
### Layer 1 — Authentication boundary
Every protected route and server action must identify the caller before doing work.
- New or changed API routes / server actions: is there an auth gate (`requireAuth`, `auth()`, session check, Bearer verify)?
- Auth endpoints: rate limiting present? (`checkAuthRateLimit` or equivalent)
- No synthetic-admin / dev-bypass fallbacks in production paths
- JWT/session: secret from env (fail-loud if unset), reasonable TTL, not in client bundle
- Password handling: hashed (bcrypt/argon2), never logged, never returned in API responses
### Layer 2 — Authorization (IDOR / tenancy)
Authentication ≠ authorization. Check resource ownership on every read/write.
- Every query scoped by `userId` / `tenantId` / collection permission — not just "user is logged in"
- Path params (`/api/users/:id`) validated against session identity
- Admin routes use dedicated guard (`requireAdmin`, `withAdmin`) — not inline role string compare copy-pasted
- Public vs private resources: `is_public` / share links cannot leak other users' data
### Layer 3 — Input validation & injection
All user-controlled input validated at the boundary.
- Request bodies: Zod/schema validation (or repo equivalent) — no trusting `req.body` shape
- SQL: parameterized queries / ORM — no string concatenation with user input; no `sql.unsafe` with user data
- Shell commands: no `exec`/`spawn` with user input; no `eval`
- HTML/XSS: no `dangerouslySetInnerHTML` without sanitization; no reflecting raw user input in responses
- File upload: type/size limits, storage path not user-controlled
### Layer 4 — Secrets & sensitive data
- No API keys, tokens, passwords in source, comments, or client bundles
- `.env` values not committed; no `NEXT_PUBLIC_*` for secrets
- Logs: no passwords, tokens, full credit card numbers, or session cookies
- Error responses: generic message to client; details server-side only
### Layer 5 — Dependencies & supply chain
- Run `npm audit` / `pnpm audit` when Shell is available; surface high/critical in report
- New dependencies: justified? Known risky packages?
- Pin or review major version bumps on auth/crypto libraries
### Layer 6 — Transport & headers (when diff touches middleware / next.config / server)
- Cookies: `HttpOnly`, `Secure`, `SameSite` where applicable
- CORS: no wildcard `*` on authenticated APIs unless explicitly documented
- Security headers: CSP, X-Frame-Options / frame-ancestors where relevant
## Steps
1. **Scope read.** `git diff` + brief `files:` list. Flag any file outside scope as Critical (scope expansion).
2. **Read repo auth rules.** `.cursor/rules/auth-patterns.mdc`, `api-routes.mdc`, `AGENTS.md` auth section — match repo conventions, don't invent new ones.
3. **Layer pass 1 → 6.** For each finding: category, severity, file:line, exploit scenario (one sentence), fix (verbatim diff when possible).
4. **Automated sweep (optional).** `npm audit --json`, `grep` for `dangerouslySetInnerHTML`, `eval(`, hardcoded `sk-`, `password\s*=`.
5. **Report.** Fill `templates/audit-report.md`. Post as `## Security Audit` PR comment (exact header — rollup CI parses it).
6. **Hand off:** *"Security audit complete. N findings (sev ≥ 3: M). Recommend fixing sev ≥ 3 before merge."*
## Anti-patterns
- Marking style nits as Critical — credibility matters.
- Auditing without reading the brief — scope expansion is the first check.
- Recommending auth patterns the repo doesn't use — read `auth-patterns.mdc` first.
- Skipping layer 2 because "route requires login" — IDOR lives here.
## References
- `references/owasp-checklist.md` — expanded checklist by layer.
## Output template
- `templates/audit-report.md` — fillable report for PR comment or `.convoys/<slug>/audits/security-<timestamp>.md` mirror.

View file

@ -1,47 +0,0 @@
# OWASP-aligned security checklist (agent reference)
Cite the **layer + item** in findings (e.g. "Layer 2 — IDOR"). Not a substitute for penetration testing.
## Layer 1 — Authentication
- [ ] All non-public API routes call a shared auth helper
- [ ] Login/register/forgot-password rate limited
- [ ] Session/JWT secret from environment; app fails start if missing
- [ ] No long-lived tokens in `localStorage` without documented threat model
- [ ] Password reset tokens single-use + short TTL
- [ ] No credentials in URLs or query strings
## Layer 2 — Authorization
- [ ] Resource IDs in path/body checked against session user/tenant
- [ ] List endpoints filter by ownership — no "return all rows"
- [ ] Admin operations behind explicit admin guard
- [ ] Role checks use server-side source of truth (DB), not client-only flags
- [ ] Bulk operations cannot target arbitrary user IDs
## Layer 3 — Input & injection
- [ ] All POST/PATCH/PUT bodies validated with schema
- [ ] Query params validated/coerced (Zod `z.coerce` or explicit parse)
- [ ] ORM/raw SQL uses parameters only
- [ ] No `dangerouslySetInnerHTML` without DOMPurify or equivalent
- [ ] File paths constructed server-side; no `../` from user input
- [ ] SSRF: server-side fetch URLs not user-controlled to internal IPs
## Layer 4 — Secrets & data
- [ ] No secrets in git history in this diff
- [ ] `NEXT_PUBLIC_` prefix only on truly public values
- [ ] PII minimized in logs and analytics events
- [ ] API responses omit internal IDs when unnecessary
## Layer 5 — Dependencies
- [ ] No new high/critical `npm audit` findings introduced
- [ ] Auth/crypto libs from reputable sources, pinned versions
## Layer 6 — Transport & config
- [ ] Auth cookies: Secure + HttpOnly
- [ ] CORS allowlist explicit for API routes
- [ ] Webhook endpoints verify signatures

View file

@ -1,41 +0,0 @@
# Security audit — {{convoy_slug}} / brief-{{brief_n}}
**Reviewer:** role-security-auditor
**Convoy:** {{convoy_slug}}
**Brief / PR:** {{brief_n}} / {{pr_number_or_draft}}
**Date:** {{YYYY-MM-DD}}
**Diff scope:** {{files_changed_count}} files
## Executive summary
- {{bullet 1 — overall risk posture}}
- {{bullet 2 — highest severity finding or "no sev ≥ 3"}}
- {{bullet 3 — auth/IDOR/injection/deps headline}}
## Findings
| Sev | Layer | Surface | Issue | Fix |
| --- | --- | --- | --- | --- |
| 4 | L2 AuthZ | `app/api/foo/route.ts:42` | IDOR: `userId` from path not checked against session | Add `if (session.user.id !== params.id) return forbidden()` |
| 2 | L3 Input | `app/api/bar/route.ts:18` | Body not validated | Add Zod schema per `api-routes.mdc` |
### Severity ≥ 3 detail
{{Expand each sev 34 with exploit scenario + suggested patch.}}
### Patterns (optional)
{{One row if the same mistake appears 3+ times.}}
## Automated checks
| Check | Result |
| --- | --- |
| `npm audit` (high+) | {{pass / N findings}} |
| Hardcoded secret grep | {{pass / findings}} |
## Approval recommendation
- [ ] **approve** — no sev ≥ 3; sev 2 acceptable with notes
- [ ] **request-changes** — sev ≥ 3 open
- [ ] **comment-only** — informational

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Next Level Builder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,104 +0,0 @@
<!--
Vendored engine: nextlevelbuilder/ui-ux-pro-max-skill (MIT) — see UPSTREAM.md, LICENSE, data/, scripts/.
Pipeline wrapper: agent-pipeline — planning-only; paired with role-ui-designer.
-->
---
name: ui-ux-pro-max
description: >-
Generate a locked design direction for greenfield UI (pattern, palette,
typography, anti-patterns) from product type. Planning phase only — invoke
via role-ui-designer at convoy start or on explicit redesign. Do not use
during audit fan-out or incremental UI tweaks inside an existing DS.
---
# UI UX Pro Max (planning)
Data-driven design intelligence: industry rules, styles, palettes, typography, and stack guidelines. **Generate once, lock in the convoy, enforce later** with `design-critique`, `design-systems`, and `accessibility-audit`.
## When to invoke
| Do | Don't |
| --- | --- |
| New screen, landing, marketing surface, major visual refresh | Hotfix, docs-only, server-only |
| User says *redesign* or bumps `design_direction.version` | Audit fan-out on a PR diff |
| `role-ui-designer` at convoy start (skill installed) | Every implementer brief |
| Re-run after explicit redesign request | Auto-re-run because code "looks off" |
## Prerequisites
- Python 3.x on the machine running the agent (`python3 --version`).
- Skill installed at `.cursor/skills/ui-ux-pro-max/` (bootstrap opt-in).
## Generate a design system (primary workflow)
From repo root:
```bash
python3 .cursor/skills/ui-ux-pro-max/scripts/search.py \
"<product type query>" \
--design-system \
-p "<Project or convoy name>"
```
Examples:
```bash
python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "B2B SaaS analytics dashboard" --design-system -p "Deck Hearth admin"
python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "wellness spa booking" --design-system -p "Serenity Spa"
```
Pick stack-specific guidelines when the convoy targets a known stack:
```bash
python3 .cursor/skills/ui-ux-pro-max/scripts/search.py "checkout flow" --stack nextjs --max-results 5
```
## Lock in the convoy
1. Parse generator output into `skills/ui-ux-pro-max/templates/design-direction.md`.
2. Append `## Design direction` to `.convoys/<slug>.md`.
3. Set frontmatter:
```yaml
design_direction:
source: role-ui-designer
skill: ui-ux-pro-max
skill_version: "2.5.0"
version: 1
locked_at: YYYY-MM-DD
product_type: "<from query>"
pattern: "<from output>"
style: "<from output>"
stack: nextjs
```
On **redesign**, bump `version` and replace the section — do not edit in place without bumping.
## Hand-off to other skills
| Next | Role / skill | Job |
| --- | --- | --- |
| Reuse + constraints | `role-ux-reviewer` | Maps locked direction to existing components; a11y constraints |
| Briefs | `role-architect` | Briefs must not contradict `design_direction` |
| Enforce | audit fan-out | `design-systems`, `accessibility-audit`, `design-critique` on the diff |
**Conflict rule:** repo tokens (`tailwind.config`, `globals.css`, `components/ui/`) **win** over generated palettes unless the convoy records an approved override.
## What this skill does NOT do
- Replace `design-critique` / `design-systems` / `accessibility-audit` at PR time.
- Commit code or open PRs.
- Install npm packages or fonts — implementer adds dependencies per brief.
## Anti-patterns
- Running the generator during audit fan-out.
- Re-running on brief 2, 3, … without a redesign request.
- Ignoring existing repo DS when the convoy is incremental UI (`skip: ui-design`).
- Loading entire `data/*.csv` into chat — always use `scripts/search.py`.
## References
- Upstream docs: [ui-ux-pro-max-skill](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill)
- Convoy output template: `templates/design-direction.md`
- L2 role: `role-ui-designer` in `.cursor/agents/`

View file

@ -1,12 +0,0 @@
# Upstream: UI UX Pro Max
Vendored from [nextlevelbuilder/ui-ux-pro-max-skill](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill).
| Field | Value |
| --- | --- |
| Upstream tag | `v2.6.2` (clone ref `d457006`) |
| Upstream `skill.json` version | `2.5.0` |
| License | MIT — see `LICENSE` |
| Vendored paths | `data/`, `scripts/` |
Do not edit `data/` or `scripts/` in place for pipeline-specific behavior — wrap in `SKILL.md` and `role-ui-designer.md` instead. To refresh from upstream, replace `data/` + `scripts/` from a new tag and bump this file.

View file

@ -1,414 +0,0 @@
#!/usr/bin/env python3
"""
Sync colors.csv and ui-reasoning.csv with the updated products.csv (161 entries).
- Remove deleted product types
- Rename mismatched entries
- Add new entries for missing product types
- Keep colors.csv aligned 1:1 with products.csv
- Renumber everything
"""
import csv, os, json
BASE = os.path.dirname(os.path.abspath(__file__))
# ─── Color derivation helpers ────────────────────────────────────────────────
def h2r(h):
h = h.lstrip("#")
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def r2h(r, g, b):
return f"#{max(0,min(255,int(r))):02X}{max(0,min(255,int(g))):02X}{max(0,min(255,int(b))):02X}"
def lum(h):
r, g, b = [x/255.0 for x in h2r(h)]
r, g, b = [(x/12.92 if x<=0.03928 else ((x+0.055)/1.055)**2.4) for x in (r, g, b)]
return 0.2126*r + 0.7152*g + 0.0722*b
def is_dark(bg):
return lum(bg) < 0.18
def on_color(bg):
return "#FFFFFF" if lum(bg) < 0.4 else "#0F172A"
def blend(a, b, f=0.15):
ra, ga, ba = h2r(a)
rb, gb, bb = h2r(b)
return r2h(ra+(rb-ra)*f, ga+(gb-ga)*f, ba+(bb-ba)*f)
def shift(h, n):
r, g, b = h2r(h)
return r2h(r+n, g+n, b+n)
def derive_row(pt, pri, sec, acc, bg, notes=""):
"""Generate full 16-token color row from 4 base colors."""
dark = is_dark(bg)
fg = "#FFFFFF" if dark else "#0F172A"
on_pri = on_color(pri)
on_sec = on_color(sec)
on_acc = on_color(acc)
card = shift(bg, 10) if dark else "#FFFFFF"
card_fg = "#FFFFFF" if dark else "#0F172A"
muted = blend(bg, pri, 0.08) if dark else blend("#FFFFFF", pri, 0.06)
muted_fg = "#94A3B8" if dark else "#64748B"
border = f"rgba(255,255,255,0.08)" if dark else blend("#FFFFFF", pri, 0.12)
destr = "#DC2626"
on_destr = "#FFFFFF"
ring = pri
return [pt, pri, on_pri, sec, on_sec, acc, on_acc, bg, fg, card, card_fg, muted, muted_fg, border, destr, on_destr, ring, notes]
# ─── Rename maps ─────────────────────────────────────────────────────────────
COLOR_RENAMES = {
"Quantum Computing": "Quantum Computing Interface",
"Biohacking / Longevity": "Biohacking / Longevity App",
"Autonomous Systems": "Autonomous Drone Fleet Manager",
"Generative AI Art": "Generative Art Platform",
"Spatial / Vision OS": "Spatial Computing OS / App",
"Climate Tech": "Sustainable Energy / Climate Tech",
}
UI_RENAMES = {
"Architecture/Interior": "Architecture / Interior",
"Autonomous Drone Fleet": "Autonomous Drone Fleet Manager",
"B2B SaaS Enterprise": "B2B Service",
"Biohacking/Longevity App": "Biohacking / Longevity App",
"Biotech/Life Sciences": "Biotech / Life Sciences",
"Developer Tool/IDE": "Developer Tool / IDE",
"Education": "Educational App",
"Fintech (Banking)": "Fintech/Crypto",
"Government/Public": "Government/Public Service",
"Home Services": "Home Services (Plumber/Electrician)",
"Micro-Credentials/Badges": "Micro-Credentials/Badges Platform",
"Music/Entertainment": "Music Streaming",
"Quantum Computing": "Quantum Computing Interface",
"Real Estate": "Real Estate/Property",
"Remote Work/Collaboration": "Remote Work/Collaboration Tool",
"Restaurant/Food": "Restaurant/Food Service",
"SaaS Dashboard": "Analytics Dashboard",
"Space Tech/Aerospace": "Space Tech / Aerospace",
"Spatial Computing OS": "Spatial Computing OS / App",
"Startup Landing": "Micro SaaS",
"Sustainable Energy/Climate": "Sustainable Energy / Climate Tech",
"Travel/Tourism": "Travel/Tourism Agency",
"Wellness/Mental Health": "Mental Health App",
}
REMOVE_TYPES = {
"Service Landing Page", "Sustainability/ESG Platform",
"Cleaning Service", "Coffee Shop",
"Consulting Firm", "Conference/Webinar Platform",
}
# ─── New color definitions: (primary, secondary, accent, bg, notes) ──────────
# Grouped by category for clarity. Each tuple generates a full 16-token row.
NEW_COLORS = {
# ── Old #97-#116 that never got colors ──
"Todo & Task Manager": ("#2563EB","#3B82F6","#059669","#F8FAFC","Functional blue + progress green"),
"Personal Finance Tracker": ("#1E40AF","#3B82F6","#059669","#0F172A","Trust blue + profit green on dark"),
"Chat & Messaging App": ("#2563EB","#6366F1","#059669","#FFFFFF","Messenger blue + online green"),
"Notes & Writing App": ("#78716C","#A8A29E","#D97706","#FFFBEB","Warm ink + amber accent on cream"),
"Habit Tracker": ("#D97706","#F59E0B","#059669","#FFFBEB","Streak amber + habit green"),
"Food Delivery / On-Demand": ("#EA580C","#F97316","#2563EB","#FFF7ED","Appetizing orange + trust blue"),
"Ride Hailing / Transportation":("#1E293B","#334155","#2563EB","#0F172A","Map dark + route blue"),
"Recipe & Cooking App": ("#9A3412","#C2410C","#059669","#FFFBEB","Warm terracotta + fresh green"),
"Meditation & Mindfulness": ("#7C3AED","#8B5CF6","#059669","#FAF5FF","Calm lavender + mindful green"),
"Weather App": ("#0284C7","#0EA5E9","#F59E0B","#F0F9FF","Sky blue + sun amber"),
"Diary & Journal App": ("#92400E","#A16207","#6366F1","#FFFBEB","Warm journal brown + ink violet"),
"CRM & Client Management": ("#2563EB","#3B82F6","#059669","#F8FAFC","Professional blue + deal green"),
"Inventory & Stock Management":("#334155","#475569","#059669","#F8FAFC","Industrial slate + stock green"),
"Flashcard & Study Tool": ("#7C3AED","#8B5CF6","#059669","#FAF5FF","Study purple + correct green"),
"Booking & Appointment App": ("#0284C7","#0EA5E9","#059669","#F0F9FF","Calendar blue + available green"),
"Invoice & Billing Tool": ("#1E3A5F","#2563EB","#059669","#F8FAFC","Navy professional + paid green"),
"Grocery & Shopping List": ("#059669","#10B981","#D97706","#ECFDF5","Fresh green + food amber"),
"Timer & Pomodoro": ("#DC2626","#EF4444","#059669","#0F172A","Focus red on dark + break green"),
"Parenting & Baby Tracker": ("#EC4899","#F472B6","#0284C7","#FDF2F8","Soft pink + trust blue"),
"Scanner & Document Manager": ("#1E293B","#334155","#2563EB","#F8FAFC","Document grey + scan blue"),
# ── A. Utility / Productivity ──
"Calendar & Scheduling App": ("#2563EB","#3B82F6","#059669","#F8FAFC","Calendar blue + event green"),
"Password Manager": ("#1E3A5F","#334155","#059669","#0F172A","Vault dark blue + secure green"),
"Expense Splitter / Bill Split":("#059669","#10B981","#DC2626","#F8FAFC","Balance green + owe red"),
"Voice Recorder & Memo": ("#DC2626","#EF4444","#2563EB","#FFFFFF","Recording red + waveform blue"),
"Bookmark & Read-Later": ("#D97706","#F59E0B","#2563EB","#FFFBEB","Warm amber + link blue"),
"Translator App": ("#2563EB","#0891B2","#EA580C","#F8FAFC","Global blue + teal + accent orange"),
"Calculator & Unit Converter": ("#EA580C","#F97316","#2563EB","#1C1917","Operation orange on dark"),
"Alarm & World Clock": ("#D97706","#F59E0B","#6366F1","#0F172A","Time amber + night indigo on dark"),
"File Manager & Transfer": ("#2563EB","#3B82F6","#D97706","#F8FAFC","Folder blue + file amber"),
"Email Client": ("#2563EB","#3B82F6","#DC2626","#FFFFFF","Inbox blue + priority red"),
# ── B. Games ──
"Casual Puzzle Game": ("#EC4899","#8B5CF6","#F59E0B","#FDF2F8","Cheerful pink + reward gold"),
"Trivia & Quiz Game": ("#2563EB","#7C3AED","#F59E0B","#EFF6FF","Quiz blue + gold leaderboard"),
"Card & Board Game": ("#15803D","#166534","#D97706","#0F172A","Felt green + gold on dark"),
"Idle & Clicker Game": ("#D97706","#F59E0B","#7C3AED","#FFFBEB","Coin gold + prestige purple"),
"Word & Crossword Game": ("#15803D","#059669","#D97706","#FFFFFF","Word green + letter amber"),
"Arcade & Retro Game": ("#DC2626","#2563EB","#22C55E","#0F172A","Neon red+blue on dark + score green"),
# ── C. Creator Tools ──
"Photo Editor & Filters": ("#7C3AED","#6366F1","#0891B2","#0F172A","Editor violet + filter cyan on dark"),
"Short Video Editor": ("#EC4899","#DB2777","#2563EB","#0F172A","Video pink on dark + timeline blue"),
"Drawing & Sketching Canvas": ("#7C3AED","#8B5CF6","#0891B2","#1C1917","Canvas purple + tool teal on dark"),
"Music Creation & Beat Maker": ("#7C3AED","#6366F1","#22C55E","#0F172A","Studio purple + waveform green on dark"),
"Meme & Sticker Maker": ("#EC4899","#F59E0B","#2563EB","#FFFFFF","Viral pink + comedy yellow + share blue"),
"AI Photo & Avatar Generator": ("#7C3AED","#6366F1","#EC4899","#FAF5FF","AI purple + generation pink"),
"Link-in-Bio Page Builder": ("#2563EB","#7C3AED","#EC4899","#FFFFFF","Brand blue + creator purple"),
# ── D. Personal Life ──
"Wardrobe & Outfit Planner": ("#BE185D","#EC4899","#D97706","#FDF2F8","Fashion rose + gold accent"),
"Plant Care Tracker": ("#15803D","#059669","#D97706","#F0FDF4","Nature green + sun yellow"),
"Book & Reading Tracker": ("#78716C","#92400E","#D97706","#FFFBEB","Book brown + page amber"),
"Couple & Relationship App": ("#BE185D","#EC4899","#DC2626","#FDF2F8","Romance rose + love red"),
"Family Calendar & Chores": ("#2563EB","#059669","#D97706","#F8FAFC","Family blue + chore green"),
"Mood Tracker": ("#7C3AED","#6366F1","#D97706","#FAF5FF","Mood purple + insight amber"),
"Gift & Wishlist": ("#DC2626","#D97706","#EC4899","#FFF1F2","Gift red + gold + surprise pink"),
# ── E. Health ──
"Running & Cycling GPS": ("#EA580C","#F97316","#059669","#0F172A","Energetic orange + pace green on dark"),
"Yoga & Stretching Guide": ("#6B7280","#78716C","#0891B2","#F5F5F0","Sage neutral + calm teal"),
"Sleep Tracker": ("#4338CA","#6366F1","#7C3AED","#0F172A","Night indigo + dream violet on dark"),
"Calorie & Nutrition Counter": ("#059669","#10B981","#EA580C","#ECFDF5","Healthy green + macro orange"),
"Period & Cycle Tracker": ("#BE185D","#EC4899","#7C3AED","#FDF2F8","Blush rose + fertility lavender"),
"Medication & Pill Reminder": ("#0284C7","#0891B2","#DC2626","#F0F9FF","Medical blue + alert red"),
"Water & Hydration Reminder": ("#0284C7","#06B6D4","#0891B2","#F0F9FF","Refreshing blue + water cyan"),
"Fasting & Intermittent Timer":("#6366F1","#4338CA","#059669","#0F172A","Fasting indigo on dark + eating green"),
# ── F. Social ──
"Anonymous Community / Confession":("#475569","#334155","#0891B2","#0F172A","Protective grey + subtle teal on dark"),
"Local Events & Discovery": ("#EA580C","#F97316","#2563EB","#FFF7ED","Event orange + map blue"),
"Study Together / Virtual Coworking":("#2563EB","#3B82F6","#059669","#F8FAFC","Focus blue + session green"),
# ── G. Education ──
"Coding Challenge & Practice": ("#22C55E","#059669","#D97706","#0F172A","Code green + difficulty amber on dark"),
"Kids Learning (ABC & Math)": ("#2563EB","#F59E0B","#EC4899","#EFF6FF","Learning blue + play yellow + fun pink"),
"Music Instrument Learning": ("#DC2626","#9A3412","#D97706","#FFFBEB","Musical red + warm amber"),
# ── H. Transport ──
"Parking Finder": ("#2563EB","#059669","#DC2626","#F0F9FF","Available blue/green + occupied red"),
"Public Transit Guide": ("#2563EB","#0891B2","#EA580C","#F8FAFC","Transit blue + line colors"),
"Road Trip Planner": ("#EA580C","#0891B2","#D97706","#FFF7ED","Adventure orange + map teal"),
# ── I. Safety & Lifestyle ──
"VPN & Privacy Tool": ("#1E3A5F","#334155","#22C55E","#0F172A","Shield dark + connected green"),
"Emergency SOS & Safety": ("#DC2626","#EF4444","#2563EB","#FFF1F2","Alert red + safety blue"),
"Wallpaper & Theme App": ("#7C3AED","#EC4899","#2563EB","#FAF5FF","Aesthetic purple + trending pink"),
"White Noise & Ambient Sound": ("#475569","#334155","#4338CA","#0F172A","Ambient grey + deep indigo on dark"),
"Home Decoration & Interior Design":("#78716C","#A8A29E","#D97706","#FAF5F2","Interior warm grey + gold accent"),
}
# ─── 1. REBUILD colors.csv ───────────────────────────────────────────────────
def rebuild_colors():
src = os.path.join(BASE, "colors.csv")
with open(src, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
headers = reader.fieldnames
existing = list(reader)
# Build lookup: Product Type -> row data
color_map = {}
for row in existing:
pt = row.get("Product Type", "").strip()
if not pt:
continue
# Remove deleted types
if pt in REMOVE_TYPES:
print(f" [colors] REMOVE: {pt}")
continue
# Rename mismatched types
if pt in COLOR_RENAMES:
new_name = COLOR_RENAMES[pt]
print(f" [colors] RENAME: {pt}{new_name}")
row["Product Type"] = new_name
pt = new_name
color_map[pt] = row
# Read products.csv to get the correct order
with open(os.path.join(BASE, "products.csv"), newline="", encoding="utf-8") as f:
products = list(csv.DictReader(f))
# Build final rows in products.csv order
final_rows = []
added = 0
for i, prod in enumerate(products, 1):
pt = prod["Product Type"]
if pt in color_map:
row = color_map[pt]
row["No"] = str(i)
final_rows.append(row)
elif pt in NEW_COLORS:
pri, sec, acc, bg, notes = NEW_COLORS[pt]
new_row = derive_row(pt, pri, sec, acc, bg, notes)
d = dict(zip(headers, [str(i)] + new_row))
final_rows.append(d)
added += 1
else:
print(f" [colors] WARNING: No color data for '{pt}' - using defaults")
new_row = derive_row(pt, "#2563EB", "#3B82F6", "#059669", "#F8FAFC", "Auto-generated default")
d = dict(zip(headers, [str(i)] + new_row))
final_rows.append(d)
added += 1
# Write
with open(src, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows(final_rows)
product_count = len(products)
print(f"\n ✅ colors.csv: {len(final_rows)} rows ({product_count} products)")
print(f" Added: {added} new color rows")
# ─── 2. REBUILD ui-reasoning.csv ─────────────────────────────────────────────
def derive_ui_reasoning(prod):
"""Generate ui-reasoning row from products.csv row."""
pt = prod["Product Type"]
style = prod.get("Primary Style Recommendation", "")
landing = prod.get("Landing Page Pattern", "")
color_focus = prod.get("Color Palette Focus", "")
considerations = prod.get("Key Considerations", "")
keywords = prod.get("Keywords", "")
# Typography mood derived from style
typo_map = {
"Minimalism": "Professional + Clean hierarchy",
"Glassmorphism": "Modern + Clear hierarchy",
"Brutalism": "Bold + Oversized + Monospace",
"Claymorphism": "Playful + Rounded + Friendly",
"Dark Mode": "High contrast + Light on dark",
"Neumorphism": "Subtle + Soft + Monochromatic",
"Flat Design": "Bold + Clean + Sans-serif",
"Vibrant": "Energetic + Bold + Large",
"Aurora": "Elegant + Gradient-friendly",
"AI-Native": "Conversational + Minimal chrome",
"Organic": "Warm + Humanist + Natural",
"Motion": "Dynamic + Hierarchy-shifting",
"Accessible": "Large + High contrast + Clear",
"Soft UI": "Modern + Accessible + Balanced",
"Trust": "Professional + Serif accents",
"Swiss": "Grid-based + Mathematical + Helvetica",
"3D": "Immersive + Spatial + Variable",
"Retro": "Nostalgic + Monospace + Neon",
"Cyberpunk": "Terminal + Monospace + Neon",
"Pixel": "Retro + Blocky + 8-bit",
}
typo_mood = "Professional + Clear hierarchy"
for key, val in typo_map.items():
if key.lower() in style.lower():
typo_mood = val
break
# Key effects from style
eff_map = {
"Glassmorphism": "Backdrop blur (10-20px) + Translucent overlays",
"Neumorphism": "Dual shadows (light+dark) + Soft press 150ms",
"Claymorphism": "Multi-layer shadows + Spring bounce + Soft press 200ms",
"Brutalism": "No transitions + Hard borders + Instant feedback",
"Dark Mode": "Subtle glow + Neon accents + High contrast",
"Flat Design": "Color shift hover + Fast 150ms transitions + No shadows",
"Minimalism": "Subtle hover 200ms + Smooth transitions + Clean",
"Motion-Driven": "Scroll animations + Parallax + Page transitions",
"Micro-interactions": "Haptic feedback + Small 50-100ms animations",
"Vibrant": "Large section gaps 48px+ + Color shift hover + Scroll-snap",
"Aurora": "Flowing gradients 8-12s + Color morphing",
"AI-Native": "Typing indicator + Streaming text + Context reveal",
"Organic": "Rounded 16-24px + Natural shadows + Flowing SVG",
"Soft UI": "Improved shadows + Modern 200-300ms + Focus visible",
"3D": "WebGL/Three.js + Parallax 3-5 layers + Physics 300-400ms",
"Trust": "Clear focus rings + Badge hover + Metric pulse",
"Accessible": "Focus rings 3-4px + ARIA + Reduced motion",
}
key_effects = "Subtle hover (200ms) + Smooth transitions"
for key, val in eff_map.items():
if key.lower() in style.lower():
key_effects = val
break
# Decision rules
rules = {}
if "dark" in style.lower() or "oled" in style.lower():
rules["if_light_mode_needed"] = "provide-theme-toggle"
if "glass" in style.lower():
rules["if_low_performance"] = "fallback-to-flat"
if "conversion" in landing.lower():
rules["if_conversion_focused"] = "add-urgency-colors"
if "social" in landing.lower():
rules["if_trust_needed"] = "add-testimonials"
if "data" in keywords.lower() or "dashboard" in keywords.lower():
rules["if_data_heavy"] = "prioritize-data-density"
if not rules:
rules["if_ux_focused"] = "prioritize-clarity"
rules["if_mobile"] = "optimize-touch-targets"
# Anti-patterns
anti_patterns = []
if "minimalism" in style.lower() or "minimal" in style.lower():
anti_patterns.append("Excessive decoration")
if "dark" in style.lower():
anti_patterns.append("Pure white backgrounds")
if "flat" in style.lower():
anti_patterns.append("Complex shadows + 3D effects")
if "vibrant" in style.lower():
anti_patterns.append("Muted colors + Low energy")
if "accessible" in style.lower():
anti_patterns.append("Color-only indicators")
if not anti_patterns:
anti_patterns = ["Inconsistent styling", "Poor contrast ratios"]
anti_str = " + ".join(anti_patterns[:2])
return {
"UI_Category": pt,
"Recommended_Pattern": landing,
"Style_Priority": style,
"Color_Mood": color_focus,
"Typography_Mood": typo_mood,
"Key_Effects": key_effects,
"Decision_Rules": json.dumps(rules),
"Anti_Patterns": anti_str,
"Severity": "HIGH"
}
def rebuild_ui_reasoning():
src = os.path.join(BASE, "ui-reasoning.csv")
with open(src, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
headers = reader.fieldnames
existing = list(reader)
# Build lookup
ui_map = {}
for row in existing:
cat = row.get("UI_Category", "").strip()
if not cat:
continue
if cat in REMOVE_TYPES:
print(f" [ui-reason] REMOVE: {cat}")
continue
if cat in UI_RENAMES:
new_name = UI_RENAMES[cat]
print(f" [ui-reason] RENAME: {cat}{new_name}")
row["UI_Category"] = new_name
cat = new_name
ui_map[cat] = row
with open(os.path.join(BASE, "products.csv"), newline="", encoding="utf-8") as f:
products = list(csv.DictReader(f))
final_rows = []
added = 0
for i, prod in enumerate(products, 1):
pt = prod["Product Type"]
if pt in ui_map:
row = ui_map[pt]
row["No"] = str(i)
final_rows.append(row)
else:
row = derive_ui_reasoning(prod)
row["No"] = str(i)
final_rows.append(row)
added += 1
with open(src, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows(final_rows)
print(f"\n ✅ ui-reasoning.csv: {len(final_rows)} rows")
print(f" Added: {added} new reasoning rows")
# ─── MAIN ────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Rebuilding colors.csv ===")
rebuild_colors()
print("\n=== Rebuilding ui-reasoning.csv ===")
rebuild_ui_reasoning()
print("\n🎉 Done!")

View file

@ -1,31 +0,0 @@
No,Category,Issue,Keywords,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity
1,Accessibility,Icon Button Labels,icon button accessibilityLabel,iOS/Android/React Native,Icon-only buttons must expose an accessible label,Set accessibilityLabel or label prop on icon buttons,Icon buttons without accessible names,"<Pressable accessibilityLabel=""Close""><XIcon /></Pressable>","<Pressable><XIcon /></Pressable>",Critical
2,Accessibility,Form Control Labels,form input label accessibilityLabel,iOS/Android/React Native,All inputs must have a visible label and an accessibility label,Pair Text label with input and set accessibilityLabel,Inputs with placeholder only,"<View><Text>Email</Text><TextInput accessibilityLabel=""Email address"" /></View>","<TextInput placeholder=""Email"" /></View>",Critical
3,Accessibility,Role & Traits,accessibilityRole accessibilityTraits,iOS/Android/React Native,Interactive elements must expose correct roles/traits,Use accessibilityRole/button/link/checkbox etc.,Rely on generic views with no roles,"<Pressable accessibilityRole=""button"">Submit</Pressable>","<View onTouchStart={submit}>Submit</View>",High
4,Accessibility,Dynamic Updates,accessibilityLiveRegion announce,iOS/Android/React Native,Async status updates should be announced to screen readers,Use accessibilityLiveRegion or announceForAccessibility,Update text silently with no announcement,"<Text accessibilityLiveRegion=""polite"">{status}</Text>","<Text>{status}</Text>",Medium
5,Accessibility,Decorative Icons,accessible={false} importantForAccessibility,iOS/Android/React Native,Decorative icons should be hidden from screen readers,Mark decorative icons as not accessible,Have screen reader read every icon,"<Icon accessible={false} importantForAccessibility=""no"" />","<Icon />",Medium
6,Touch,Touch Target Size,touch 44x44 hitSlop,iOS/Android/React Native,Primary touch targets must be at least 44x44pt,Increase hitSlop or padding to meet minimum,Small icons with tiny touch area,"<Pressable hitSlop={10}><Icon /></Pressable>","<Pressable><Icon style={{ width: 16, height: 16 }} /></Pressable>",Critical
7,Touch,Touch Spacing,touch spacing gap 8px,iOS/Android/React Native,Adjacent touch targets need enough spacing,Keep at least 8dp spacing between touchables,Cluster many buttons with no gap,"<View style={{ gap: 8 }}><Button ... /><Button ... /></View>","<View><Button ... /><Button ... /></View>",Medium
8,Touch,Gesture Conflicts,scroll swipe back gesture,iOS/Android/React Native,Custom gestures must not break system scroll/back,Reserve horizontal swipes for carousels,Full-screen custom swipe conflicting with back,"HorizontalPager inside vertical ScrollView","PanResponder on full screen blocking back",High
9,Navigation,Back Behavior,back handler navigation stack,iOS/Android/React Native,Back navigation should be predictable and preserve state,Use navigation.goBack and keep screen state,Reset stack or exit app unexpectedly,onPress={() => navigation.goBack()},"BackHandler.exitApp() on first press",Critical
10,Navigation,Bottom Tabs,tab bar max items,iOS/Android/React Native,Bottom tab bar should have at most 5 primary items,Use 35 tabs and move extras to More/Settings,Overloaded tab bar with many icons,Home/Explore/Profile/Settings,"Home/Explore/Shop/Cart/Profile/Settings/More",Medium
11,Navigation,Modal Escape,modal dismiss close affordance,iOS/Android/React Native,Modals/sheets must have clear close actions,Provide close button and swipe-down where platform expects,Trapping users in modal with no obvious exit,"<Modal><Button title=""Close"" onPress={onClose} /></Modal>","<Modal><View>{children}</View></Modal>",High
12,State,Preserve Screen State,navigation preserve state,iOS/Android/React Native,Returning to a screen should restore its scroll and form state,Keep components mounted or persist state,Reset list scroll and form inputs on every visit,"<Tab.Navigator screenOptions={{ unmountOnBlur: false }}>","<Tab.Screen options={{ unmountOnBlur: true }} />",Medium
13,Feedback,Loading Indicators,activity indicator skeleton,iOS/Android/React Native,Show visible feedback during network operations,Use ActivityIndicator or skeleton for >300ms operations,Leave button and screen frozen,"{loading ? <ActivityIndicator /> : <Button title=""Save"" />}", "<Button title=""Save"" onPress={submit} /> // no loading",High
14,Feedback,Success Feedback,toast checkmark banner,iOS/Android/React Native,Confirm successful actions with brief feedback,Show toast/checkmark or banner,Complete actions silently with no confirmation,"showToast('Saved successfully')","// silently update state only",Medium
15,Feedback,Error Feedback,inline error banner,iOS/Android/React Native,Show clear error messages near the problem,input-level error + summary banner,Only change border color with no explanation,"<TextInput ... /><Text style={{color:'red'}}>{error}</Text>","<TextInput style={{borderColor:'red'}} />",High
16,Forms,Inline Validation,onBlur validation,iOS/Android/React Native,Validate inputs on blur or submit with clear messaging,Validate onBlur and onSubmit,Validate on every keystroke causing jank,"onBlur={() => validateEmail(value)}","onChangeText={v => validateEmail(v)} // every char",Medium
17,Forms,Keyboard Type,keyboardType returnKeyType,iOS/Android/React Native,Use appropriate keyboardType and returnKeyType,Match email/tel/number/search types,Use default keyboard for all inputs,"<TextInput keyboardType=""email-address"" />","<TextInput keyboardType=""default"" />",Medium
18,Forms,Auto Focus & Next,autoFocus blurOnSubmit onSubmitEditing,iOS/Android/React Native,Guide users through form fields with Next/Done flows,Use onSubmitEditing to focus next input,Force users to tap each field manually,"onSubmitEditing={() => nextRef.current?.focus()}","// no onSubmitEditing, manual tap only",Low
19,Forms,Password Visibility,secureTextEntry toggle,iOS/Android/React Native,Allow toggling password visibility securely,Provide Show/Hide icon toggling secureTextEntry,Force users to type blind with no option,"<TextInput secureTextEntry={secure} /><Icon onPress={toggle} />","<TextInput secureTextEntry /> // no toggle",Medium
20,Performance,Virtualize Long Lists,FlatList SectionList virtualization,iOS/Android/React Native,Use FlatList/SectionList for lists over ~50 items,Use keyExtractor and initialNumToRender appropriately,Render hundreds of items with ScrollView,"<FlatList data={items} renderItem={...} />","<ScrollView>{items.map(renderItem)}</ScrollView>",High
21,Performance,Image Size & Cache,Image resize cache,iOS/Android/React Native,Use correctly sized and cached images,Use Image component with proper resizeMode and caching,Load full-resolution images everywhere,"<Image source={{uri}} resizeMode=""cover"" />","<Image source={require('4k.png')} /> // small avatar",Medium
22,Performance,Debounce High-Freq Events,debounce scroll search,iOS/Android/React Native,Debounce scroll/search callbacks to avoid jank,Wrap handlers with debounce/throttle,Run heavy logic on every event,"onScroll={debouncedHandleScroll}","onScroll={handleScrollHeavy}",Medium
23,Animation,Duration & Easing,animation duration easing,iOS/Android/React Native,Micro-interactions should be 150300ms with native-like easing,Use ease-out for enter/ease-in for exit,Use long or linear animations for core UI,"Animated.timing(..., { duration: 200, easing: Easing.out(Easing.quad) })","Animated.timing(..., { duration: 800, easing: Easing.linear })",Medium
24,Animation,Respect Reduced Motion,reduced motion accessibility,iOS/Android/React Native,Respect OS reduced-motion accessibility setting,Check reduceMotionEnabled and simplify animations,Ignore user motion preferences,"if (reduceMotionEnabled) skipAnimation()","Always run complex parallax animations",Critical
25,Animation,Limited Continuous Motion,loop animation loader,iOS/Android/React Native,Reserve infinite animations for loaders and live data,Use looping only where necessary,Keep decorative elements looping forever,"Animated.loop(loaderAnim) for ActivityIndicator","Animated.loop(bounceAnim) on background icons",Medium
26,Typography,Base Font Size,fontScale dynamic type,iOS/Android/React Native,Body text must be readable and support Dynamic Type,Use platform fontScale and at least 1416pt base,Render critical text below 12pt,"<Text style={{ fontSize: 16 }}>Body</Text>","<Text style={{ fontSize: 10 }}>Body</Text>",High
27,Typography,Dynamic Type Support,allowFontScaling adjustsFontSizeToFit,iOS/Android/React Native,Support system text scaling without breaking layout,Set allowFontScaling and test large text,Disable scaling on all text globally,"<Text allowFontScaling>{label}</Text>","<Text allowFontScaling={false}>{label}</Text>",High
28,Safe Areas,Safe Area Insets,safe area insets notch gesture,iOS/Android/React Native,Content must not overlap notches/gesture bars,Wrap screens in SafeAreaView or apply insets,Place tappable content under system bars,"<SafeAreaView style={{ flex: 1 }}><Screen /></SafeAreaView>","<View style={{ flex: 1 }}><Screen /></View>",High
29,Theming,Light/Dark Contrast,dark mode contrast tokens,iOS/Android/React Native,Ensure sufficient contrast in both light and dark themes,Use semantic tokens and test both themes,Reuse light-theme grays directly in dark mode,"colors.textPrimaryDark = '#F9FAFB'","colors.textPrimaryDark = '#9CA3AF' on '#111827'",High
30,Anti-Pattern,No Gesture-Only Actions,gesture only hidden controls,iOS/Android/React Native,Don't rely solely on hidden gestures for core actions,Provide visible buttons in addition to gestures,Rely on swipe/shake only with no UI affordance,"Swipe to delete + visible Delete button","Only shake device to undo with no UI",Critical
1 No Category Issue Keywords Platform Description Do Don't Code Example Good Code Example Bad Severity
2 1 Accessibility Icon Button Labels icon button accessibilityLabel iOS/Android/React Native Icon-only buttons must expose an accessible label Set accessibilityLabel or label prop on icon buttons Icon buttons without accessible names <Pressable accessibilityLabel="Close"><XIcon /></Pressable> <Pressable><XIcon /></Pressable> Critical
3 2 Accessibility Form Control Labels form input label accessibilityLabel iOS/Android/React Native All inputs must have a visible label and an accessibility label Pair Text label with input and set accessibilityLabel Inputs with placeholder only <View><Text>Email</Text><TextInput accessibilityLabel="Email address" /></View> <TextInput placeholder="Email" /></View> Critical
4 3 Accessibility Role & Traits accessibilityRole accessibilityTraits iOS/Android/React Native Interactive elements must expose correct roles/traits Use accessibilityRole/button/link/checkbox etc. Rely on generic views with no roles <Pressable accessibilityRole="button">Submit</Pressable> <View onTouchStart={submit}>Submit</View> High
5 4 Accessibility Dynamic Updates accessibilityLiveRegion announce iOS/Android/React Native Async status updates should be announced to screen readers Use accessibilityLiveRegion or announceForAccessibility Update text silently with no announcement <Text accessibilityLiveRegion="polite">{status}</Text> <Text>{status}</Text> Medium
6 5 Accessibility Decorative Icons accessible={false} importantForAccessibility iOS/Android/React Native Decorative icons should be hidden from screen readers Mark decorative icons as not accessible Have screen reader read every icon <Icon accessible={false} importantForAccessibility="no" /> <Icon /> Medium
7 6 Touch Touch Target Size touch 44x44 hitSlop iOS/Android/React Native Primary touch targets must be at least 44x44pt Increase hitSlop or padding to meet minimum Small icons with tiny touch area <Pressable hitSlop={10}><Icon /></Pressable> <Pressable><Icon style={{ width: 16, height: 16 }} /></Pressable> Critical
8 7 Touch Touch Spacing touch spacing gap 8px iOS/Android/React Native Adjacent touch targets need enough spacing Keep at least 8dp spacing between touchables Cluster many buttons with no gap <View style={{ gap: 8 }}><Button ... /><Button ... /></View> <View><Button ... /><Button ... /></View> Medium
9 8 Touch Gesture Conflicts scroll swipe back gesture iOS/Android/React Native Custom gestures must not break system scroll/back Reserve horizontal swipes for carousels Full-screen custom swipe conflicting with back HorizontalPager inside vertical ScrollView PanResponder on full screen blocking back High
10 9 Navigation Back Behavior back handler navigation stack iOS/Android/React Native Back navigation should be predictable and preserve state Use navigation.goBack and keep screen state Reset stack or exit app unexpectedly onPress={() => navigation.goBack()} BackHandler.exitApp() on first press Critical
11 10 Navigation Bottom Tabs tab bar max items iOS/Android/React Native Bottom tab bar should have at most 5 primary items Use 3–5 tabs and move extras to More/Settings Overloaded tab bar with many icons Home/Explore/Profile/Settings Home/Explore/Shop/Cart/Profile/Settings/More Medium
12 11 Navigation Modal Escape modal dismiss close affordance iOS/Android/React Native Modals/sheets must have clear close actions Provide close button and swipe-down where platform expects Trapping users in modal with no obvious exit <Modal><Button title="Close" onPress={onClose} /></Modal> <Modal><View>{children}</View></Modal> High
13 12 State Preserve Screen State navigation preserve state iOS/Android/React Native Returning to a screen should restore its scroll and form state Keep components mounted or persist state Reset list scroll and form inputs on every visit <Tab.Navigator screenOptions={{ unmountOnBlur: false }}> <Tab.Screen options={{ unmountOnBlur: true }} /> Medium
14 13 Feedback Loading Indicators activity indicator skeleton iOS/Android/React Native Show visible feedback during network operations Use ActivityIndicator or skeleton for >300ms operations Leave button and screen frozen {loading ? <ActivityIndicator /> : <Button title="Save" />} <Button title="Save" onPress={submit} /> // no loading High
15 14 Feedback Success Feedback toast checkmark banner iOS/Android/React Native Confirm successful actions with brief feedback Show toast/checkmark or banner Complete actions silently with no confirmation showToast('Saved successfully') // silently update state only Medium
16 15 Feedback Error Feedback inline error banner iOS/Android/React Native Show clear error messages near the problem input-level error + summary banner Only change border color with no explanation <TextInput ... /><Text style={{color:'red'}}>{error}</Text> <TextInput style={{borderColor:'red'}} /> High
17 16 Forms Inline Validation onBlur validation iOS/Android/React Native Validate inputs on blur or submit with clear messaging Validate onBlur and onSubmit Validate on every keystroke causing jank onBlur={() => validateEmail(value)} onChangeText={v => validateEmail(v)} // every char Medium
18 17 Forms Keyboard Type keyboardType returnKeyType iOS/Android/React Native Use appropriate keyboardType and returnKeyType Match email/tel/number/search types Use default keyboard for all inputs <TextInput keyboardType="email-address" /> <TextInput keyboardType="default" /> Medium
19 18 Forms Auto Focus & Next autoFocus blurOnSubmit onSubmitEditing iOS/Android/React Native Guide users through form fields with Next/Done flows Use onSubmitEditing to focus next input Force users to tap each field manually onSubmitEditing={() => nextRef.current?.focus()} // no onSubmitEditing, manual tap only Low
20 19 Forms Password Visibility secureTextEntry toggle iOS/Android/React Native Allow toggling password visibility securely Provide Show/Hide icon toggling secureTextEntry Force users to type blind with no option <TextInput secureTextEntry={secure} /><Icon onPress={toggle} /> <TextInput secureTextEntry /> // no toggle Medium
21 20 Performance Virtualize Long Lists FlatList SectionList virtualization iOS/Android/React Native Use FlatList/SectionList for lists over ~50 items Use keyExtractor and initialNumToRender appropriately Render hundreds of items with ScrollView <FlatList data={items} renderItem={...} /> <ScrollView>{items.map(renderItem)}</ScrollView> High
22 21 Performance Image Size & Cache Image resize cache iOS/Android/React Native Use correctly sized and cached images Use Image component with proper resizeMode and caching Load full-resolution images everywhere <Image source={{uri}} resizeMode="cover" /> <Image source={require('4k.png')} /> // small avatar Medium
23 22 Performance Debounce High-Freq Events debounce scroll search iOS/Android/React Native Debounce scroll/search callbacks to avoid jank Wrap handlers with debounce/throttle Run heavy logic on every event onScroll={debouncedHandleScroll} onScroll={handleScrollHeavy} Medium
24 23 Animation Duration & Easing animation duration easing iOS/Android/React Native Micro-interactions should be 150–300ms with native-like easing Use ease-out for enter/ease-in for exit Use long or linear animations for core UI Animated.timing(..., { duration: 200, easing: Easing.out(Easing.quad) }) Animated.timing(..., { duration: 800, easing: Easing.linear }) Medium
25 24 Animation Respect Reduced Motion reduced motion accessibility iOS/Android/React Native Respect OS reduced-motion accessibility setting Check reduceMotionEnabled and simplify animations Ignore user motion preferences if (reduceMotionEnabled) skipAnimation() Always run complex parallax animations Critical
26 25 Animation Limited Continuous Motion loop animation loader iOS/Android/React Native Reserve infinite animations for loaders and live data Use looping only where necessary Keep decorative elements looping forever Animated.loop(loaderAnim) for ActivityIndicator Animated.loop(bounceAnim) on background icons Medium
27 26 Typography Base Font Size fontScale dynamic type iOS/Android/React Native Body text must be readable and support Dynamic Type Use platform fontScale and at least 14–16pt base Render critical text below 12pt <Text style={{ fontSize: 16 }}>Body</Text> <Text style={{ fontSize: 10 }}>Body</Text> High
28 27 Typography Dynamic Type Support allowFontScaling adjustsFontSizeToFit iOS/Android/React Native Support system text scaling without breaking layout Set allowFontScaling and test large text Disable scaling on all text globally <Text allowFontScaling>{label}</Text> <Text allowFontScaling={false}>{label}</Text> High
29 28 Safe Areas Safe Area Insets safe area insets notch gesture iOS/Android/React Native Content must not overlap notches/gesture bars Wrap screens in SafeAreaView or apply insets Place tappable content under system bars <SafeAreaView style={{ flex: 1 }}><Screen /></SafeAreaView> <View style={{ flex: 1 }}><Screen /></View> High
30 29 Theming Light/Dark Contrast dark mode contrast tokens iOS/Android/React Native Ensure sufficient contrast in both light and dark themes Use semantic tokens and test both themes Reuse light-theme grays directly in dark mode colors.textPrimaryDark = '#F9FAFB' colors.textPrimaryDark = '#9CA3AF' on '#111827' High
31 30 Anti-Pattern No Gesture-Only Actions gesture only hidden controls iOS/Android/React Native Don't rely solely on hidden gestures for core actions Provide visible buttons in addition to gestures Rely on swipe/shake only with no UI affordance Swipe to delete + visible Delete button Only shake device to undo with no UI Critical

View file

@ -1,26 +0,0 @@
No,Data Type,Keywords,Best Chart Type,Secondary Options,When to Use,When NOT to Use,Data Volume Threshold,Color Guidance,Accessibility Grade,Accessibility Notes,A11y Fallback,Library Recommendation,Interactive Level
1,Trend Over Time,"trend, time-series, line, growth, timeline, progress",Line Chart,"Area Chart, Smooth Area","Data has a time axis; user needs to observe rise/fall trends or rate of change over a continuous period","Fewer than 4 data points (use stat card); more than 6 series (visual noise); no time dimension exists","<1000 pts: SVG; ≥1000 pts: Canvas + downsampling; >10000: aggregate to intervals","Primary: #0080FF. Multiple series: distinct colors + distinct line styles. Fill: 20% opacity",AA,"Differentiate series by line style (solid/dashed/dotted) not color alone. Add pattern overlays for colorblind users.","Dashed/dotted lines per series; togglable data table with timestamps and values","Chart.js, Recharts, ApexCharts",Hover + Zoom
2,Compare Categories,"compare, categories, bar, comparison, ranking",Bar Chart (Horizontal or Vertical),"Column Chart, Grouped Bar","Comparing discrete categories by magnitude; ranking or ordering is the core insight; categories ≤ 15","Categories > 15 (use table or search); data has time dimension (use line); showing proportions (use waffle/stacked)","<20 categories: vertical bar; 2050: horizontal bar; >50: paginated table","Each bar: distinct color. Grouped: same hue family. Always sort descending by value",AAA,"Value labels on each bar by default. Sort control for user reordering.","Value labels always visible; provide CSV export","Chart.js, Recharts, D3.js",Hover + Sort
3,Part-to-Whole,"part-to-whole, pie, donut, percentage, proportion, share",Pie Chart or Donut,"Stacked Bar, Waffle Chart","≤5 categories; one dominant segment vs rest; emphasis on visual proportion over exact values","Categories > 5; slice differences < 5% (visually indistinguishable); user needs precise values; accessibility-first context","Max 6 slices; beyond that switch to stacked bar 100%","56 max colors. Contrasting palette. Largest slice at 12 o'clock. Always label slices with %",C,"Pie charts fail WCAG for colorblind users. Slices rely on color alone. Avoid as primary chart in a11y contexts.","Must provide stacked bar alternative + percentage data table as mandatory fallback","Chart.js, Recharts, D3.js",Hover + Drill
4,Correlation / Distribution,"correlation, distribution, scatter, relationship, pattern, cluster",Scatter Plot or Bubble Chart,"Heat Map, Matrix","Exploring relationship between two continuous variables; identifying clusters or outliers in a dataset","Variables are categorical (use grouped bar); fewer than 20 points (patterns aren't meaningful); mobile-primary context","<500 pts: SVG; 5005000: Canvas at 0.60.8 opacity; >5000: hexbin or aggregate first","Color axis: gradient (blue → red). Bubble size: relative to 3rd variable. Opacity: 0.60.8 to show density",B,"Provide data table alternative. Combine color + shape distinction for colorblind users.","Data table with correlation coefficient annotation; shape markers (circle/square/triangle) per group","D3.js, Plotly, Recharts",Hover + Brush
5,Heatmap / Intensity,"heatmap, heat-map, intensity, density, matrix, calendar",Heat Map or Choropleth,"Grid Heat Map, Bubble Heat","Showing intensity/density across a 2D grid; time-based patterns (e.g., activity by hour × day)","Fewer than 20 cells (use bar); user needs to read exact values; colorblind users without pattern fallback","Up to 10,000 cells efficiently; beyond that aggregate; calendar heatmap: 365 cells max per SVG","Gradient: Cool (blue) to Hot (red). Divergent scale for ±data. Always include numeric color legend",B,"Pattern overlay for colorblind users. Numerical value on hover. Legend must include scale ticks.","Numerical overlay on hover; downloadable grid table with row/column labels","D3.js, Plotly, ApexCharts",Hover + Zoom
6,Geographic Data,"geographic, map, location, region, geo, spatial, choropleth",Choropleth Map or Bubble Map,Geographic Heat Map,"Data has a regional/location dimension; spatial distribution is the core insight for the user","Regions have very different sizes making visual comparison misleading (use bar); mobile-primary context","<1000 regions: SVG; ≥1000: Canvas/WebGL (Deck.gl); global maps: tile-based rendering","Single color gradient per region group. Categorized colors for discrete types. Legend with clear scale breaks",B,"Include text labels for major regions. Provide keyboard navigation between regions.","Region text labels; sortable data table by region name and value; keyboard-navigable regions","D3.js, Mapbox, Leaflet",Pan + Zoom + Drill
7,Funnel / Flow,"funnel, flow, conversion, drop-off, pipeline, stages",Funnel Chart or Sankey,Waterfall (for flows),"Sequential multi-stage process; showing conversion or drop-off rates between defined stages","Stages aren't sequential; values don't decrease monotonically (use bar); fewer than 3 stages","38 stages optimal; beyond 8 stages group minor steps into 'Other'","Stages: single color gradient (start → end). Show conversion % between each stage. Highlight biggest drop",AA,"Explicit conversion % as text per stage. Stage labels always visible. Linear list view as fallback.","Provide linear list view with stage name + count + drop-off %; keyboard traversal","D3.js, Recharts, Custom SVG",Hover + Drill
8,Performance vs Target,"performance, target, kpi, gauge, goal, threshold, progress",Gauge Chart or Bullet Chart,"Dial, Thermometer","Single KPI measured against a defined target or threshold; dashboard summary context","No target or benchmark exists; comparing multiple KPIs at once (use bullet chart grid)","Single metric per gauge; for 3+ KPIs use bullet chart grid layout","Performance: Red → Yellow → Green gradient. Target: marker line. Threshold zones clearly differentiated",AA,"Always show numerical value + % of target as text beside chart. Never rely on color position alone.","Numerical value + % of target shown as visible text; ARIA live region for real-time updates","D3.js, ApexCharts, Custom SVG",Hover
9,Time-Series Forecast,"forecast, prediction, confidence, band, projection, estimate",Line with Confidence Band,Ribbon Chart,"Historical data + model predictions; communicating uncertainty range to non-technical stakeholders","No historical baseline; prediction confidence is too low to be useful; audience is not data-literate","Keep historical window to 3090 days for readability; forecast horizon ≤ 30% of visible x-axis range","Actual: solid line #0080FF. Forecast: dashed #FF9500. Confidence band: 15% opacity fill same hue",AA,"Toggle between actual-only and forecast views. Legend must distinguish lines beyond color (solid vs dashed).","Toggle actual/forecast independently; legend labels must include line-style description","Chart.js, ApexCharts, Plotly",Hover + Toggle
10,Anomaly Detection,"anomaly, outlier, spike, alert, detection, monitoring, deviation",Line Chart with Highlights,Scatter with Alert,"Monitoring a time-series for outliers; alerting users to unexpected spikes or dips in operational data","Anomalies are predefined categories (use bar with highlight); real-time context without a pause control","Stream at ≤60fps with Canvas; batch: up to 10,000 pts; mark anomalies as a separate data layer","Normal: #0080FF solid line. Anomaly marker: #FF0000 circle + filled. Alert band: #FFF3CD background zone",AA,"Use shape marker (not color only) for anomaly points. Add text annotation per anomaly event.","Text alert annotation per anomaly; anomaly summary list panel alongside chart","D3.js, Plotly, ApexCharts",Hover + Alert
11,Hierarchical / Nested Data,"hierarchy, nested, treemap, parent, children, breakdown, drill",Treemap,"Sunburst, Nested Donut, Icicle","Showing size relationships within a hierarchy; overview of proportional structure (e.g., budget breakdown)","Hierarchy depth > 3 levels (too complex to read); user needs to compare sibling values precisely","<200 nodes: SVG; 2001000: Canvas; >1000: paginate or pre-filter before rendering","Parent nodes: distinct hues. Children: lighter shades of same hue. White separator borders: 23px",C,"Poor baseline accessibility. Always provide table alternative as primary view. Label all large areas.","Collapsible tree table as primary view; treemap as supplementary visual only","D3.js, Recharts, ApexCharts",Hover + Drilldown
12,Flow / Process Data,"flow, process, sankey, distribution, source, target, transfer",Sankey Diagram,"Alluvial, Chord Diagram","Showing how quantities flow between nodes; multi-source multi-target distribution","Flow directions form loops (use network graph); fewer than 3 source-target pairs; mobile-primary context","<50 flows: SVG; ≥50: Canvas; >200 flows: aggregate minor flows into 'Other' node","Gradient from source to target color. Flow opacity: 0.40.6. Node labels always visible",C,"Structural flow charts cannot be conveyed by color alone. Provide flow table. Avoid on mobile.","Flow table (Source → Target → Value); keyboard-traversable node list with tab stops","D3.js (d3-sankey), Plotly",Hover + Drilldown
13,Cumulative Changes,"waterfall, cumulative, variance, incremental, bridge, delta",Waterfall Chart,"Stacked Bar, Cascade","Showing how individual positive/negative components add up to a final total (e.g., P&L, budget variance)","Changes are not additive; more than 12 bars (readability breaks); audience expects a simple total","412 bars optimal; beyond 12 aggregate minor items into a single 'Other' bar","Increases: #4CAF50. Decreases: #F44336. Start total: #2196F3. End total: #0D47A1. Running total line: dashed",AA,"Color + directional arrow icon per bar (not color alone). Labels on every bar.","Table with running total column; directional arrow icons per row","ApexCharts, Highcharts, Plotly",Hover
14,Multi-Variable Comparison,"radar, spider, multi-variable, attributes, dimensions, comparison",Radar / Spider Chart,"Parallel Coordinates, Grouped Bar","Comparing multiple entities across the same fixed set of attributes (e.g., product feature comparison)","Axes > 8 (unreadable); values need precise comparison (use grouped bar); audience unfamiliar with radar charts","23 datasets maximum per chart; 58 axes; beyond 8 axes switch to parallel coordinates","Single dataset: #0080FF at 20% fill. Multiple: distinct hues with 30% fill. Border: full opacity",B,"Limit axes to 58. Always provide grouped bar chart alternative for precise reading.","Grouped bar chart as mandatory alternative; include raw data table","Chart.js, Recharts, ApexCharts",Hover + Toggle
15,Stock / Trading OHLC,"stock, trading, ohlc, candlestick, finance, price, volume",Candlestick Chart,"OHLC Bar, Heikin-Ashi","Financial time-series with Open/High/Low/Close data; trading or investment product context only","Non-financial audience; no OHLC data available (use line chart); accessibility-first context","Real-time: Canvas required. Historical: paginate by time range. Max 500 candles visible at once","Bullish: #26A69A. Bearish: #EF5350. Volume bars: 40% opacity below. Body fill vs hollow for OHLC style",B,"Provide OHLC data table. Colorblind: use fill vs outline pattern (bullish = filled, bearish = hollow).","OHLC data table with sortable columns; numeric summary panel (daily change %)","Lightweight Charts (TradingView), ApexCharts",Real-time + Hover + Zoom
16,Relationship / Connection Data,"network, graph, nodes, edges, connections, relationships, force",Network Graph,"Hierarchical Tree, Adjacency Matrix","Mapping connections between entities; network topology or social graph exploration context","Node count > 500 without clustering pre-applied; user needs precise connection counts; mobile context","≤100 nodes: SVG; 101500: Canvas; >500: must apply clustering/LOD before rendering","Node types: categorical colors. Edges: #90A4AE at 60% opacity. Highlight path: #F59E0B",D,"Fundamentally inaccessible without alternative. Never use as sole representation. Always provide list alternative.","Adjacency list table (Node A → Node B → Weight); hierarchical tree view when structure allows","D3.js (d3-force), Vis.js, Cytoscape.js",Drilldown + Hover + Drag
17,Distribution / Statistical,"distribution, statistical, spread, median, outlier, quartile, boxplot",Box Plot,"Violin Plot, Beeswarm","Showing spread, median, and outliers of a dataset; comparing distributions across multiple groups","Fewer than 20 data points per group (distribution is not meaningful); audience unfamiliar with statistical charts","Any sample size; aggregated representation so rendering is ⚡ Excellent at any volume","Box fill: #BBDEFB. Border: #1976D2. Median line: #D32F2F bold. Outlier dots: #F44336",AA,"Include stats summary table. Annotate outlier count in chart subtitle.","Stats summary table (min / Q1 / median / Q3 / max / mean); outlier count annotation","Plotly, D3.js, Chart.js (plugin)",Hover
18,Performance vs Target (Compact),"bullet, compact, kpi, dashboard, target, benchmark, range",Bullet Chart,"Gauge, Progress Bar","Dashboard with multiple KPIs side by side; space-constrained contexts where a gauge is too large","Single KPI with emphasis (use gauge); data has no defined target range; fewer than 3 KPIs","Ideal for 310 bullet charts in a grid; scales to any count efficiently","Qualitative ranges: #FFCDD2 / #FFF9C4 / #C8E6C9 (bad/ok/good). Performance bar: #1976D2. Target: black 3px marker",AAA,"All values always visible as text. Color ranges are labeled with text thresholds not color alone.","Numerical values always visible (not hover-only); color ranges labeled with threshold text","D3.js, Plotly, Custom SVG",Hover
19,Proportional / Percentage,"waffle, percentage, proportion, progress, filled, grid",Waffle Chart,"Pictogram, Stacked Bar 100%","Showing what fraction of a whole is filled; percentage progress in a visually engaging and accessible format","More than 5 categories (use stacked bar); exact values matter over visual proportion; very tight space","10×10 grid standard (100 cells); for > 5 categories switch to stacked 100% bar","35 categories max. 23px gap between cells. Each category a distinct accessible color pair",AA,"Better than pie for accessibility. Percentage text label always visible. Each cell has aria-label.","Percentage text always visible; grid cells labeled with aria-label value; provide legend","D3.js, React-Waffle, Custom CSS Grid",Hover
20,Hierarchical Proportional,"sunburst, hierarchy, nested, proportion, radial, circle",Sunburst Chart,"Treemap, Icicle, Circle Packing","Exploring nested proportions where both hierarchy and relative size matter (e.g., org spend breakdown)","More than 3 hierarchy levels (outer rings become unreadable); precision matters over overview; mobile","<100 nodes: SVG; 100500: Canvas; >500: filter to top N before rendering","Center to outer: darker to lighter hue. Each level 1520% lighter. Contrasting border between sectors",C,"Poor accessibility beyond 2 levels. Mandatory table alternative required for any production use.","Collapsible indented list with percentages; breadcrumb trail for current drill-down state","D3.js (d3-hierarchy), Recharts, ApexCharts",Drilldown + Hover
21,Root Cause Analysis,"root cause, decomposition, tree, hierarchy, drill-down, ai-split, attribution",Decomposition Tree,"Decision Tree, Flow Chart","Decomposing a metric into contributing factors; AI-assisted analysis or BI drill-down scenarios","No clear parent-child causal relationship; audience expects a summary rather than exploration","Up to 5 levels deep; limit visible nodes to 20 per level for readability; lazy-load deeper levels","Positive impact nodes: #2563EB. Negative impact nodes: #EF4444. Neutral connectors: #94A3B8",AA,"Keyboard-navigable expand/collapse. Screen reader announces node value and % contribution.","Keyboard expand/collapse tree; screen reader announces node label + value + % impact","Power BI (native), React-Flow, Custom D3.js",Drill + Expand
22,3D Spatial Data,"3d, spatial, immersive, terrain, molecular, volumetric, point-cloud",3D Scatter / Surface Plot,"Volumetric Rendering, Point Cloud","Scientific/engineering context where Z-axis carries essential info not expressible in 2D","2D projection conveys the same insight; mobile context; accessibility-required environments; standard business dashboards","WebGL required. Deck.gl: up to 1M points. Three.js: LOD required beyond 50,000 pts","Depth cues: lighting and shading. Z-axis: color gradient (cool → warm). Transparent overlapping: opacity 0.4",D,"3D spatial charts are fundamentally inaccessible. Must not be used as primary chart type in any product UI.","Mandatory 2D projection view + data table; do not use as primary chart type in product UI","Three.js, Deck.gl, Plotly 3D",Rotate + Zoom + VR
23,Real-Time Streaming,"streaming, real-time, ticker, live, velocity, pulse, monitoring",Streaming Area Chart,"Ticker Tape, Moving Gauge","Live monitoring dashboards; IoT/ops data updating at ≥1 Hz; user needs current value at a glance","Update frequency < 1/min (use periodic-refresh line chart); flashing content without reduced-motion support","Canvas/WebGL required. Buffer last 60300s of data. Downsample older data on scroll","Current pulse: #00FF00 (dark theme) or #0080FF (light theme). History: fading opacity. Grid: dark background",B,"Pause/resume control required. Current value as large visible text KPI. Respect prefers-reduced-motion.","Pause/resume button required; current value shown as large text KPI; prefers-reduced-motion: freeze animation","Smoothed D3.js, CanvasJS",Real-time + Pause + Zoom
24,Sentiment / Emotion,"sentiment, emotion, nlp, opinion, feeling, text-analysis",Word Cloud with Sentiment,"Sentiment Arc, Radar Chart","NLP output visualization; exploratory analysis of text corpus sentiment; frequency-weighted keyword overview","Precise values matter (word size is inherently imprecise); screen-reader context; corpus < 50 items","505000 terms optimal. Beyond 5000: apply top-N filtering before render. Avoid on mobile","Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Word size maps to frequency",C,"Word clouds fail screen readers. Never use as sole output of NLP analysis. Always pair with list view.","Sortable list view by frequency with sentiment label column; word cloud as supplementary only","D3-cloud, Highcharts, Nivo",Hover + Filter
25,Process Mining,"process, mining, variants, path, bottleneck, log, event",Process Map / Graph,"Directed Acyclic Graph (DAG), Petri Net","Analyzing event logs to visualize actual process flows; identifying bottlenecks and deviations in ops/product funnels","No event log data available; audience expects a static flowchart (use diagram tool); node count > 100 without pre-filtering","<30 nodes: SVG; 30100: Canvas; >100: apply variant filtering (top 80% of cases) before rendering","Happy path: #10B981 thick line. Deviations: #F59E0B thin line. Bottleneck nodes: #EF4444 fill",B,"Complex graphs are hard to navigate. Provide path summary text. Highlight top 3 bottlenecks as annotations.","Path summary table (variant → frequency → avg duration); top 3 bottlenecks as text annotation panel","React-Flow, Cytoscape.js, Recharts",Drag + Node-Click
1 No Data Type Keywords Best Chart Type Secondary Options When to Use When NOT to Use Data Volume Threshold Color Guidance Accessibility Grade Accessibility Notes A11y Fallback Library Recommendation Interactive Level
2 1 Trend Over Time trend, time-series, line, growth, timeline, progress Line Chart Area Chart, Smooth Area Data has a time axis; user needs to observe rise/fall trends or rate of change over a continuous period Fewer than 4 data points (use stat card); more than 6 series (visual noise); no time dimension exists <1000 pts: SVG; ≥1000 pts: Canvas + downsampling; >10000: aggregate to intervals Primary: #0080FF. Multiple series: distinct colors + distinct line styles. Fill: 20% opacity AA Differentiate series by line style (solid/dashed/dotted) not color alone. Add pattern overlays for colorblind users. Dashed/dotted lines per series; togglable data table with timestamps and values Chart.js, Recharts, ApexCharts Hover + Zoom
3 2 Compare Categories compare, categories, bar, comparison, ranking Bar Chart (Horizontal or Vertical) Column Chart, Grouped Bar Comparing discrete categories by magnitude; ranking or ordering is the core insight; categories ≤ 15 Categories > 15 (use table or search); data has time dimension (use line); showing proportions (use waffle/stacked) <20 categories: vertical bar; 20–50: horizontal bar; >50: paginated table Each bar: distinct color. Grouped: same hue family. Always sort descending by value AAA Value labels on each bar by default. Sort control for user reordering. Value labels always visible; provide CSV export Chart.js, Recharts, D3.js Hover + Sort
4 3 Part-to-Whole part-to-whole, pie, donut, percentage, proportion, share Pie Chart or Donut Stacked Bar, Waffle Chart ≤5 categories; one dominant segment vs rest; emphasis on visual proportion over exact values Categories > 5; slice differences < 5% (visually indistinguishable); user needs precise values; accessibility-first context Max 6 slices; beyond that switch to stacked bar 100% 5–6 max colors. Contrasting palette. Largest slice at 12 o'clock. Always label slices with % C Pie charts fail WCAG for colorblind users. Slices rely on color alone. Avoid as primary chart in a11y contexts. Must provide stacked bar alternative + percentage data table as mandatory fallback Chart.js, Recharts, D3.js Hover + Drill
5 4 Correlation / Distribution correlation, distribution, scatter, relationship, pattern, cluster Scatter Plot or Bubble Chart Heat Map, Matrix Exploring relationship between two continuous variables; identifying clusters or outliers in a dataset Variables are categorical (use grouped bar); fewer than 20 points (patterns aren't meaningful); mobile-primary context <500 pts: SVG; 500–5000: Canvas at 0.6–0.8 opacity; >5000: hexbin or aggregate first Color axis: gradient (blue → red). Bubble size: relative to 3rd variable. Opacity: 0.6–0.8 to show density B Provide data table alternative. Combine color + shape distinction for colorblind users. Data table with correlation coefficient annotation; shape markers (circle/square/triangle) per group D3.js, Plotly, Recharts Hover + Brush
6 5 Heatmap / Intensity heatmap, heat-map, intensity, density, matrix, calendar Heat Map or Choropleth Grid Heat Map, Bubble Heat Showing intensity/density across a 2D grid; time-based patterns (e.g., activity by hour × day) Fewer than 20 cells (use bar); user needs to read exact values; colorblind users without pattern fallback Up to 10,000 cells efficiently; beyond that aggregate; calendar heatmap: 365 cells max per SVG Gradient: Cool (blue) to Hot (red). Divergent scale for ±data. Always include numeric color legend B Pattern overlay for colorblind users. Numerical value on hover. Legend must include scale ticks. Numerical overlay on hover; downloadable grid table with row/column labels D3.js, Plotly, ApexCharts Hover + Zoom
7 6 Geographic Data geographic, map, location, region, geo, spatial, choropleth Choropleth Map or Bubble Map Geographic Heat Map Data has a regional/location dimension; spatial distribution is the core insight for the user Regions have very different sizes making visual comparison misleading (use bar); mobile-primary context <1000 regions: SVG; ≥1000: Canvas/WebGL (Deck.gl); global maps: tile-based rendering Single color gradient per region group. Categorized colors for discrete types. Legend with clear scale breaks B Include text labels for major regions. Provide keyboard navigation between regions. Region text labels; sortable data table by region name and value; keyboard-navigable regions D3.js, Mapbox, Leaflet Pan + Zoom + Drill
8 7 Funnel / Flow funnel, flow, conversion, drop-off, pipeline, stages Funnel Chart or Sankey Waterfall (for flows) Sequential multi-stage process; showing conversion or drop-off rates between defined stages Stages aren't sequential; values don't decrease monotonically (use bar); fewer than 3 stages 3–8 stages optimal; beyond 8 stages group minor steps into 'Other' Stages: single color gradient (start → end). Show conversion % between each stage. Highlight biggest drop AA Explicit conversion % as text per stage. Stage labels always visible. Linear list view as fallback. Provide linear list view with stage name + count + drop-off %; keyboard traversal D3.js, Recharts, Custom SVG Hover + Drill
9 8 Performance vs Target performance, target, kpi, gauge, goal, threshold, progress Gauge Chart or Bullet Chart Dial, Thermometer Single KPI measured against a defined target or threshold; dashboard summary context No target or benchmark exists; comparing multiple KPIs at once (use bullet chart grid) Single metric per gauge; for 3+ KPIs use bullet chart grid layout Performance: Red → Yellow → Green gradient. Target: marker line. Threshold zones clearly differentiated AA Always show numerical value + % of target as text beside chart. Never rely on color position alone. Numerical value + % of target shown as visible text; ARIA live region for real-time updates D3.js, ApexCharts, Custom SVG Hover
10 9 Time-Series Forecast forecast, prediction, confidence, band, projection, estimate Line with Confidence Band Ribbon Chart Historical data + model predictions; communicating uncertainty range to non-technical stakeholders No historical baseline; prediction confidence is too low to be useful; audience is not data-literate Keep historical window to 30–90 days for readability; forecast horizon ≤ 30% of visible x-axis range Actual: solid line #0080FF. Forecast: dashed #FF9500. Confidence band: 15% opacity fill same hue AA Toggle between actual-only and forecast views. Legend must distinguish lines beyond color (solid vs dashed). Toggle actual/forecast independently; legend labels must include line-style description Chart.js, ApexCharts, Plotly Hover + Toggle
11 10 Anomaly Detection anomaly, outlier, spike, alert, detection, monitoring, deviation Line Chart with Highlights Scatter with Alert Monitoring a time-series for outliers; alerting users to unexpected spikes or dips in operational data Anomalies are predefined categories (use bar with highlight); real-time context without a pause control Stream at ≤60fps with Canvas; batch: up to 10,000 pts; mark anomalies as a separate data layer Normal: #0080FF solid line. Anomaly marker: #FF0000 circle + filled. Alert band: #FFF3CD background zone AA Use shape marker (not color only) for anomaly points. Add text annotation per anomaly event. Text alert annotation per anomaly; anomaly summary list panel alongside chart D3.js, Plotly, ApexCharts Hover + Alert
12 11 Hierarchical / Nested Data hierarchy, nested, treemap, parent, children, breakdown, drill Treemap Sunburst, Nested Donut, Icicle Showing size relationships within a hierarchy; overview of proportional structure (e.g., budget breakdown) Hierarchy depth > 3 levels (too complex to read); user needs to compare sibling values precisely <200 nodes: SVG; 200–1000: Canvas; >1000: paginate or pre-filter before rendering Parent nodes: distinct hues. Children: lighter shades of same hue. White separator borders: 2–3px C Poor baseline accessibility. Always provide table alternative as primary view. Label all large areas. Collapsible tree table as primary view; treemap as supplementary visual only D3.js, Recharts, ApexCharts Hover + Drilldown
13 12 Flow / Process Data flow, process, sankey, distribution, source, target, transfer Sankey Diagram Alluvial, Chord Diagram Showing how quantities flow between nodes; multi-source multi-target distribution Flow directions form loops (use network graph); fewer than 3 source-target pairs; mobile-primary context <50 flows: SVG; ≥50: Canvas; >200 flows: aggregate minor flows into 'Other' node Gradient from source to target color. Flow opacity: 0.4–0.6. Node labels always visible C Structural flow charts cannot be conveyed by color alone. Provide flow table. Avoid on mobile. Flow table (Source → Target → Value); keyboard-traversable node list with tab stops D3.js (d3-sankey), Plotly Hover + Drilldown
14 13 Cumulative Changes waterfall, cumulative, variance, incremental, bridge, delta Waterfall Chart Stacked Bar, Cascade Showing how individual positive/negative components add up to a final total (e.g., P&L, budget variance) Changes are not additive; more than 12 bars (readability breaks); audience expects a simple total 4–12 bars optimal; beyond 12 aggregate minor items into a single 'Other' bar Increases: #4CAF50. Decreases: #F44336. Start total: #2196F3. End total: #0D47A1. Running total line: dashed AA Color + directional arrow icon per bar (not color alone). Labels on every bar. Table with running total column; directional arrow icons per row ApexCharts, Highcharts, Plotly Hover
15 14 Multi-Variable Comparison radar, spider, multi-variable, attributes, dimensions, comparison Radar / Spider Chart Parallel Coordinates, Grouped Bar Comparing multiple entities across the same fixed set of attributes (e.g., product feature comparison) Axes > 8 (unreadable); values need precise comparison (use grouped bar); audience unfamiliar with radar charts 2–3 datasets maximum per chart; 5–8 axes; beyond 8 axes switch to parallel coordinates Single dataset: #0080FF at 20% fill. Multiple: distinct hues with 30% fill. Border: full opacity B Limit axes to 5–8. Always provide grouped bar chart alternative for precise reading. Grouped bar chart as mandatory alternative; include raw data table Chart.js, Recharts, ApexCharts Hover + Toggle
16 15 Stock / Trading OHLC stock, trading, ohlc, candlestick, finance, price, volume Candlestick Chart OHLC Bar, Heikin-Ashi Financial time-series with Open/High/Low/Close data; trading or investment product context only Non-financial audience; no OHLC data available (use line chart); accessibility-first context Real-time: Canvas required. Historical: paginate by time range. Max 500 candles visible at once Bullish: #26A69A. Bearish: #EF5350. Volume bars: 40% opacity below. Body fill vs hollow for OHLC style B Provide OHLC data table. Colorblind: use fill vs outline pattern (bullish = filled, bearish = hollow). OHLC data table with sortable columns; numeric summary panel (daily change %) Lightweight Charts (TradingView), ApexCharts Real-time + Hover + Zoom
17 16 Relationship / Connection Data network, graph, nodes, edges, connections, relationships, force Network Graph Hierarchical Tree, Adjacency Matrix Mapping connections between entities; network topology or social graph exploration context Node count > 500 without clustering pre-applied; user needs precise connection counts; mobile context ≤100 nodes: SVG; 101–500: Canvas; >500: must apply clustering/LOD before rendering Node types: categorical colors. Edges: #90A4AE at 60% opacity. Highlight path: #F59E0B D Fundamentally inaccessible without alternative. Never use as sole representation. Always provide list alternative. Adjacency list table (Node A → Node B → Weight); hierarchical tree view when structure allows D3.js (d3-force), Vis.js, Cytoscape.js Drilldown + Hover + Drag
18 17 Distribution / Statistical distribution, statistical, spread, median, outlier, quartile, boxplot Box Plot Violin Plot, Beeswarm Showing spread, median, and outliers of a dataset; comparing distributions across multiple groups Fewer than 20 data points per group (distribution is not meaningful); audience unfamiliar with statistical charts Any sample size; aggregated representation so rendering is ⚡ Excellent at any volume Box fill: #BBDEFB. Border: #1976D2. Median line: #D32F2F bold. Outlier dots: #F44336 AA Include stats summary table. Annotate outlier count in chart subtitle. Stats summary table (min / Q1 / median / Q3 / max / mean); outlier count annotation Plotly, D3.js, Chart.js (plugin) Hover
19 18 Performance vs Target (Compact) bullet, compact, kpi, dashboard, target, benchmark, range Bullet Chart Gauge, Progress Bar Dashboard with multiple KPIs side by side; space-constrained contexts where a gauge is too large Single KPI with emphasis (use gauge); data has no defined target range; fewer than 3 KPIs Ideal for 3–10 bullet charts in a grid; scales to any count efficiently Qualitative ranges: #FFCDD2 / #FFF9C4 / #C8E6C9 (bad/ok/good). Performance bar: #1976D2. Target: black 3px marker AAA All values always visible as text. Color ranges are labeled with text thresholds not color alone. Numerical values always visible (not hover-only); color ranges labeled with threshold text D3.js, Plotly, Custom SVG Hover
20 19 Proportional / Percentage waffle, percentage, proportion, progress, filled, grid Waffle Chart Pictogram, Stacked Bar 100% Showing what fraction of a whole is filled; percentage progress in a visually engaging and accessible format More than 5 categories (use stacked bar); exact values matter over visual proportion; very tight space 10×10 grid standard (100 cells); for > 5 categories switch to stacked 100% bar 3–5 categories max. 2–3px gap between cells. Each category a distinct accessible color pair AA Better than pie for accessibility. Percentage text label always visible. Each cell has aria-label. Percentage text always visible; grid cells labeled with aria-label value; provide legend D3.js, React-Waffle, Custom CSS Grid Hover
21 20 Hierarchical Proportional sunburst, hierarchy, nested, proportion, radial, circle Sunburst Chart Treemap, Icicle, Circle Packing Exploring nested proportions where both hierarchy and relative size matter (e.g., org spend breakdown) More than 3 hierarchy levels (outer rings become unreadable); precision matters over overview; mobile <100 nodes: SVG; 100–500: Canvas; >500: filter to top N before rendering Center to outer: darker to lighter hue. Each level 15–20% lighter. Contrasting border between sectors C Poor accessibility beyond 2 levels. Mandatory table alternative required for any production use. Collapsible indented list with percentages; breadcrumb trail for current drill-down state D3.js (d3-hierarchy), Recharts, ApexCharts Drilldown + Hover
22 21 Root Cause Analysis root cause, decomposition, tree, hierarchy, drill-down, ai-split, attribution Decomposition Tree Decision Tree, Flow Chart Decomposing a metric into contributing factors; AI-assisted analysis or BI drill-down scenarios No clear parent-child causal relationship; audience expects a summary rather than exploration Up to 5 levels deep; limit visible nodes to 20 per level for readability; lazy-load deeper levels Positive impact nodes: #2563EB. Negative impact nodes: #EF4444. Neutral connectors: #94A3B8 AA Keyboard-navigable expand/collapse. Screen reader announces node value and % contribution. Keyboard expand/collapse tree; screen reader announces node label + value + % impact Power BI (native), React-Flow, Custom D3.js Drill + Expand
23 22 3D Spatial Data 3d, spatial, immersive, terrain, molecular, volumetric, point-cloud 3D Scatter / Surface Plot Volumetric Rendering, Point Cloud Scientific/engineering context where Z-axis carries essential info not expressible in 2D 2D projection conveys the same insight; mobile context; accessibility-required environments; standard business dashboards WebGL required. Deck.gl: up to 1M points. Three.js: LOD required beyond 50,000 pts Depth cues: lighting and shading. Z-axis: color gradient (cool → warm). Transparent overlapping: opacity 0.4 D 3D spatial charts are fundamentally inaccessible. Must not be used as primary chart type in any product UI. Mandatory 2D projection view + data table; do not use as primary chart type in product UI Three.js, Deck.gl, Plotly 3D Rotate + Zoom + VR
24 23 Real-Time Streaming streaming, real-time, ticker, live, velocity, pulse, monitoring Streaming Area Chart Ticker Tape, Moving Gauge Live monitoring dashboards; IoT/ops data updating at ≥1 Hz; user needs current value at a glance Update frequency < 1/min (use periodic-refresh line chart); flashing content without reduced-motion support Canvas/WebGL required. Buffer last 60–300s of data. Downsample older data on scroll Current pulse: #00FF00 (dark theme) or #0080FF (light theme). History: fading opacity. Grid: dark background B Pause/resume control required. Current value as large visible text KPI. Respect prefers-reduced-motion. Pause/resume button required; current value shown as large text KPI; prefers-reduced-motion: freeze animation Smoothed D3.js, CanvasJS Real-time + Pause + Zoom
25 24 Sentiment / Emotion sentiment, emotion, nlp, opinion, feeling, text-analysis Word Cloud with Sentiment Sentiment Arc, Radar Chart NLP output visualization; exploratory analysis of text corpus sentiment; frequency-weighted keyword overview Precise values matter (word size is inherently imprecise); screen-reader context; corpus < 50 items 50–5000 terms optimal. Beyond 5000: apply top-N filtering before render. Avoid on mobile Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Word size maps to frequency C Word clouds fail screen readers. Never use as sole output of NLP analysis. Always pair with list view. Sortable list view by frequency with sentiment label column; word cloud as supplementary only D3-cloud, Highcharts, Nivo Hover + Filter
26 25 Process Mining process, mining, variants, path, bottleneck, log, event Process Map / Graph Directed Acyclic Graph (DAG), Petri Net Analyzing event logs to visualize actual process flows; identifying bottlenecks and deviations in ops/product funnels No event log data available; audience expects a static flowchart (use diagram tool); node count > 100 without pre-filtering <30 nodes: SVG; 30–100: Canvas; >100: apply variant filtering (top 80% of cases) before rendering Happy path: #10B981 thick line. Deviations: #F59E0B thin line. Bottleneck nodes: #EF4444 fill B Complex graphs are hard to navigate. Provide path summary text. Highlight top 3 bottlenecks as annotations. Path summary table (variant → frequency → avg duration); top 3 bottlenecks as text annotation panel React-Flow, Cytoscape.js, Recharts Drag + Node-Click

View file

@ -1,162 +0,0 @@
No,Product Type,Primary,On Primary,Secondary,On Secondary,Accent,On Accent,Background,Foreground,Card,Card Foreground,Muted,Muted Foreground,Border,Destructive,On Destructive,Ring,Notes
1,SaaS (General),#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#EA580C,#FFFFFF,#F8FAFC,#1E293B,#FFFFFF,#1E293B,#E9EFF8,#64748B,#E2E8F0,#DC2626,#FFFFFF,#2563EB,Trust blue + orange CTA contrast [Accent adjusted from #F97316 for WCAG 3:1]
2,Micro SaaS,#6366F1,#FFFFFF,#818CF8,#0F172A,#059669,#FFFFFF,#F5F3FF,#1E1B4B,#FFFFFF,#1E1B4B,#EBEFF9,#64748B,#E0E7FF,#DC2626,#FFFFFF,#6366F1,Indigo primary + emerald CTA [Accent adjusted from #10B981 for WCAG 3:1]
3,E-commerce,#059669,#FFFFFF,#10B981,#0F172A,#EA580C,#FFFFFF,#ECFDF5,#064E3B,#FFFFFF,#064E3B,#E8F1F3,#64748B,#A7F3D0,#DC2626,#FFFFFF,#059669,Success green + urgency orange [Accent adjusted from #F97316 for WCAG 3:1]
4,E-commerce Luxury,#1C1917,#FFFFFF,#44403C,#FFFFFF,#A16207,#FFFFFF,#FAFAF9,#0C0A09,#FFFFFF,#0C0A09,#E8ECF0,#64748B,#D6D3D1,#DC2626,#FFFFFF,#1C1917,Premium dark + gold accent [Accent adjusted from #CA8A04 for WCAG 3:1]
5,B2B Service,#0F172A,#FFFFFF,#334155,#FFFFFF,#0369A1,#FFFFFF,#F8FAFC,#020617,#FFFFFF,#020617,#E8ECF1,#64748B,#E2E8F0,#DC2626,#FFFFFF,#0F172A,Professional navy + blue CTA
6,Financial Dashboard,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#22C55E,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#EF4444,#FFFFFF,#0F172A,Dark bg + green positive indicators
7,Analytics Dashboard,#1E40AF,#FFFFFF,#3B82F6,#FFFFFF,#D97706,#FFFFFF,#F8FAFC,#1E3A8A,#FFFFFF,#1E3A8A,#E9EEF6,#64748B,#DBEAFE,#DC2626,#FFFFFF,#1E40AF,Blue data + amber highlights [Accent adjusted from #F59E0B for WCAG 3:1]
8,Healthcare App,#0891B2,#FFFFFF,#22D3EE,#0F172A,#059669,#FFFFFF,#ECFEFF,#164E63,#FFFFFF,#164E63,#E8F1F6,#64748B,#A5F3FC,#DC2626,#FFFFFF,#0891B2,Calm cyan + health green
9,Educational App,#4F46E5,#FFFFFF,#818CF8,#0F172A,#EA580C,#FFFFFF,#EEF2FF,#1E1B4B,#FFFFFF,#1E1B4B,#EBEEF8,#64748B,#C7D2FE,#DC2626,#FFFFFF,#4F46E5,Playful indigo + energetic orange [Accent adjusted from #F97316 for WCAG 3:1]
10,Creative Agency,#EC4899,#FFFFFF,#F472B6,#0F172A,#0891B2,#FFFFFF,#FDF2F8,#831843,#FFFFFF,#831843,#F1EEF5,#64748B,#FBCFE8,#DC2626,#FFFFFF,#EC4899,Bold pink + cyan accent [Accent adjusted from #06B6D4 for WCAG 3:1]
11,Portfolio/Personal,#18181B,#FFFFFF,#3F3F46,#FFFFFF,#2563EB,#FFFFFF,#FAFAFA,#09090B,#FFFFFF,#09090B,#E8ECF0,#64748B,#E4E4E7,#DC2626,#FFFFFF,#18181B,Monochrome + blue accent
12,Gaming,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#F43F5E,#FFFFFF,#0F0F23,#E2E8F0,#1E1C35,#E2E8F0,#27273B,#94A3B8,#4C1D95,#EF4444,#FFFFFF,#7C3AED,Neon purple + rose action
13,Government/Public Service,#0F172A,#FFFFFF,#334155,#FFFFFF,#0369A1,#FFFFFF,#F8FAFC,#020617,#FFFFFF,#020617,#E8ECF1,#64748B,#E2E8F0,#DC2626,#FFFFFF,#0F172A,High contrast navy + blue
14,Fintech/Crypto,#F59E0B,#0F172A,#FBBF24,#0F172A,#8B5CF6,#FFFFFF,#0F172A,#F8FAFC,#222735,#F8FAFC,#272F42,#94A3B8,#334155,#EF4444,#FFFFFF,#F59E0B,Gold trust + purple tech
15,Social Media App,#E11D48,#FFFFFF,#FB7185,#0F172A,#2563EB,#FFFFFF,#FFF1F2,#881337,#FFFFFF,#881337,#F0ECF2,#64748B,#FECDD3,#DC2626,#FFFFFF,#E11D48,Vibrant rose + engagement blue
16,Productivity Tool,#0D9488,#FFFFFF,#14B8A6,#0F172A,#EA580C,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F4,#64748B,#99F6E4,#DC2626,#FFFFFF,#0D9488,Teal focus + action orange [Accent adjusted from #F97316 for WCAG 3:1]
17,Design System/Component Library,#4F46E5,#FFFFFF,#6366F1,#FFFFFF,#EA580C,#FFFFFF,#EEF2FF,#312E81,#FFFFFF,#312E81,#EBEEF8,#64748B,#C7D2FE,#DC2626,#FFFFFF,#4F46E5,Indigo brand + doc hierarchy [Accent adjusted from #F97316 for WCAG 3:1]
18,AI/Chatbot Platform,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#0891B2,#FFFFFF,#FAF5FF,#1E1B4B,#FFFFFF,#1E1B4B,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,AI purple + cyan interactions [Accent adjusted from #06B6D4 for WCAG 3:1]
19,NFT/Web3 Platform,#8B5CF6,#FFFFFF,#A78BFA,#0F172A,#FBBF24,#0F172A,#0F0F23,#F8FAFC,#1E1D35,#F8FAFC,#27273B,#94A3B8,#4C1D95,#EF4444,#FFFFFF,#8B5CF6,Purple tech + gold value
20,Creator Economy Platform,#EC4899,#FFFFFF,#F472B6,#0F172A,#EA580C,#FFFFFF,#FDF2F8,#831843,#FFFFFF,#831843,#F1EEF5,#64748B,#FBCFE8,#DC2626,#FFFFFF,#EC4899,Creator pink + engagement orange [Accent adjusted from #F97316 for WCAG 3:1]
21,Remote Work/Collaboration Tool,#6366F1,#FFFFFF,#818CF8,#0F172A,#059669,#FFFFFF,#F5F3FF,#312E81,#FFFFFF,#312E81,#EBEFF9,#64748B,#E0E7FF,#DC2626,#FFFFFF,#6366F1,Calm indigo + success green [Accent adjusted from #10B981 for WCAG 3:1]
22,Mental Health App,#8B5CF6,#FFFFFF,#C4B5FD,#0F172A,#059669,#FFFFFF,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#EDEFF9,#64748B,#EDE9FE,#DC2626,#FFFFFF,#8B5CF6,Calming lavender + wellness green [Accent adjusted from #10B981 for WCAG 3:1]
23,Pet Tech App,#F97316,#0F172A,#FB923C,#0F172A,#2563EB,#FFFFFF,#FFF7ED,#9A3412,#FFFFFF,#9A3412,#F1F0F0,#64748B,#FED7AA,#DC2626,#FFFFFF,#F97316,Playful orange + trust blue
24,Smart Home/IoT Dashboard,#1E293B,#FFFFFF,#334155,#FFFFFF,#22C55E,#0F172A,#0F172A,#F8FAFC,#1B2336,#F8FAFC,#272F42,#94A3B8,#475569,#EF4444,#FFFFFF,#1E293B,Dark tech + status green
25,EV/Charging Ecosystem,#0891B2,#FFFFFF,#22D3EE,#0F172A,#16A34A,#FFFFFF,#ECFEFF,#164E63,#FFFFFF,#164E63,#E8F1F6,#64748B,#A5F3FC,#DC2626,#FFFFFF,#0891B2,Electric cyan + eco green [Accent adjusted from #22C55E for WCAG 3:1]
26,Subscription Box Service,#D946EF,#FFFFFF,#E879F9,#0F172A,#EA580C,#FFFFFF,#FDF4FF,#86198F,#FFFFFF,#86198F,#F0EEF9,#64748B,#F5D0FE,#DC2626,#FFFFFF,#D946EF,Excitement purple + urgency orange [Accent adjusted from #F97316 for WCAG 3:1]
27,Podcast Platform,#1E1B4B,#FFFFFF,#312E81,#FFFFFF,#F97316,#0F172A,#0F0F23,#F8FAFC,#1B1B30,#F8FAFC,#27273B,#94A3B8,#4338CA,#EF4444,#FFFFFF,#1E1B4B,Dark audio + warm accent
28,Dating App,#E11D48,#FFFFFF,#FB7185,#0F172A,#EA580C,#FFFFFF,#FFF1F2,#881337,#FFFFFF,#881337,#F0ECF2,#64748B,#FECDD3,#DC2626,#FFFFFF,#E11D48,Romantic rose + warm orange [Accent adjusted from #F97316 for WCAG 3:1]
29,Micro-Credentials/Badges Platform,#0369A1,#FFFFFF,#0EA5E9,#0F172A,#A16207,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E7EFF5,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0369A1,Trust blue + achievement gold [Accent adjusted from #CA8A04 for WCAG 3:1]
30,Knowledge Base/Documentation,#475569,#FFFFFF,#64748B,#FFFFFF,#2563EB,#FFFFFF,#F8FAFC,#1E293B,#FFFFFF,#1E293B,#EAEFF3,#64748B,#E2E8F0,#DC2626,#FFFFFF,#475569,Neutral grey + link blue
31,Hyperlocal Services,#059669,#FFFFFF,#10B981,#0F172A,#EA580C,#FFFFFF,#ECFDF5,#064E3B,#FFFFFF,#064E3B,#E8F1F3,#64748B,#A7F3D0,#DC2626,#FFFFFF,#059669,Location green + action orange [Accent adjusted from #F97316 for WCAG 3:1]
32,Beauty/Spa/Wellness Service,#EC4899,#FFFFFF,#F9A8D4,#0F172A,#8B5CF6,#FFFFFF,#FDF2F8,#831843,#FFFFFF,#831843,#F1EEF5,#64748B,#FBCFE8,#DC2626,#FFFFFF,#EC4899,Soft pink + lavender luxury
33,Luxury/Premium Brand,#1C1917,#FFFFFF,#44403C,#FFFFFF,#A16207,#FFFFFF,#FAFAF9,#0C0A09,#FFFFFF,#0C0A09,#E8ECF0,#64748B,#D6D3D1,#DC2626,#FFFFFF,#1C1917,Premium black + gold accent [Accent adjusted from #CA8A04 for WCAG 3:1]
34,Restaurant/Food Service,#DC2626,#FFFFFF,#F87171,#0F172A,#A16207,#FFFFFF,#FEF2F2,#450A0A,#FFFFFF,#450A0A,#F0EDF1,#64748B,#FECACA,#DC2626,#FFFFFF,#DC2626,Appetizing red + warm gold [Accent adjusted from #CA8A04 for WCAG 3:1]
35,Fitness/Gym App,#F97316,#0F172A,#FB923C,#0F172A,#22C55E,#0F172A,#1F2937,#F8FAFC,#313742,#F8FAFC,#37414F,#94A3B8,#374151,#EF4444,#FFFFFF,#F97316,Energy orange + success green
36,Real Estate/Property,#0F766E,#FFFFFF,#14B8A6,#0F172A,#0369A1,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F0F3,#64748B,#99F6E4,#DC2626,#FFFFFF,#0F766E,Trust teal + professional blue
37,Travel/Tourism Agency,#0EA5E9,#0F172A,#38BDF8,#0F172A,#EA580C,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E8F2F8,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0EA5E9,Sky blue + adventure orange [Accent adjusted from #F97316 for WCAG 3:1]
38,Hotel/Hospitality,#1E3A8A,#FFFFFF,#3B82F6,#FFFFFF,#A16207,#FFFFFF,#F8FAFC,#1E40AF,#FFFFFF,#1E40AF,#E9EEF5,#64748B,#BFDBFE,#DC2626,#FFFFFF,#1E3A8A,Luxury navy + gold service [Accent adjusted from #CA8A04 for WCAG 3:1]
39,Wedding/Event Planning,#DB2777,#FFFFFF,#F472B6,#0F172A,#A16207,#FFFFFF,#FDF2F8,#831843,#FFFFFF,#831843,#F0EDF4,#64748B,#FBCFE8,#DC2626,#FFFFFF,#DB2777,Romantic pink + elegant gold [Accent adjusted from #CA8A04 for WCAG 3:1]
40,Legal Services,#1E3A8A,#FFFFFF,#1E40AF,#FFFFFF,#B45309,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EEF5,#64748B,#CBD5E1,#DC2626,#FFFFFF,#1E3A8A,Authority navy + trust gold
41,Insurance Platform,#0369A1,#FFFFFF,#0EA5E9,#0F172A,#16A34A,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E7EFF5,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0369A1,Security blue + protected green [Accent adjusted from #22C55E for WCAG 3:1]
42,Banking/Traditional Finance,#0F172A,#FFFFFF,#1E3A8A,#FFFFFF,#A16207,#FFFFFF,#F8FAFC,#020617,#FFFFFF,#020617,#E8ECF1,#64748B,#E2E8F0,#DC2626,#FFFFFF,#0F172A,Trust navy + premium gold [Accent adjusted from #CA8A04 for WCAG 3:1]
43,Online Course/E-learning,#0D9488,#FFFFFF,#2DD4BF,#0F172A,#EA580C,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F4,#64748B,#5EEAD4,#DC2626,#FFFFFF,#0D9488,Progress teal + achievement orange [Accent adjusted from #F97316 for WCAG 3:1]
44,Non-profit/Charity,#0891B2,#FFFFFF,#22D3EE,#0F172A,#EA580C,#FFFFFF,#ECFEFF,#164E63,#FFFFFF,#164E63,#E8F1F6,#64748B,#A5F3FC,#DC2626,#FFFFFF,#0891B2,Compassion blue + action orange [Accent adjusted from #F97316 for WCAG 3:1]
45,Music Streaming,#1E1B4B,#FFFFFF,#4338CA,#FFFFFF,#22C55E,#0F172A,#0F0F23,#F8FAFC,#1B1B30,#F8FAFC,#27273B,#94A3B8,#312E81,#EF4444,#FFFFFF,#1E1B4B,Dark audio + play green
46,Video Streaming/OTT,#0F0F23,#FFFFFF,#1E1B4B,#FFFFFF,#E11D48,#FFFFFF,#000000,#F8FAFC,#0C0C0D,#F8FAFC,#181818,#94A3B8,#312E81,#EF4444,#FFFFFF,#0F0F23,Cinema dark + play red
47,Job Board/Recruitment,#0369A1,#FFFFFF,#0EA5E9,#0F172A,#16A34A,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E7EFF5,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0369A1,Professional blue + success green [Accent adjusted from #22C55E for WCAG 3:1]
48,Marketplace (P2P),#7C3AED,#FFFFFF,#A78BFA,#0F172A,#16A34A,#FFFFFF,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,Trust purple + transaction green [Accent adjusted from #22C55E for WCAG 3:1]
49,Logistics/Delivery,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#EA580C,#FFFFFF,#EFF6FF,#1E40AF,#FFFFFF,#1E40AF,#E9EFF8,#64748B,#BFDBFE,#DC2626,#FFFFFF,#2563EB,Tracking blue + delivery orange [Accent adjusted from #F97316 for WCAG 3:1]
50,Agriculture/Farm Tech,#15803D,#FFFFFF,#22C55E,#0F172A,#A16207,#FFFFFF,#F0FDF4,#14532D,#FFFFFF,#14532D,#E8F0F1,#64748B,#BBF7D0,#DC2626,#FFFFFF,#15803D,Earth green + harvest gold [Accent adjusted from #CA8A04 for WCAG 3:1]
51,Construction/Architecture,#64748B,#FFFFFF,#94A3B8,#0F172A,#EA580C,#FFFFFF,#F8FAFC,#334155,#FFFFFF,#334155,#EBF0F5,#64748B,#E2E8F0,#DC2626,#FFFFFF,#64748B,Industrial grey + safety orange [Accent adjusted from #F97316 for WCAG 3:1]
52,Automotive/Car Dealership,#1E293B,#FFFFFF,#334155,#FFFFFF,#DC2626,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#E9EDF1,#64748B,#E2E8F0,#DC2626,#FFFFFF,#1E293B,Premium dark + action red
53,Photography Studio,#18181B,#FFFFFF,#27272A,#FFFFFF,#F8FAFC,#0F172A,#000000,#FAFAFA,#0C0C0C,#FAFAFA,#181818,#94A3B8,#3F3F46,#EF4444,#FFFFFF,#18181B,Pure black + white contrast
54,Coworking Space,#F59E0B,#0F172A,#FBBF24,#0F172A,#2563EB,#FFFFFF,#FFFBEB,#78350F,#FFFFFF,#78350F,#F1F2EF,#64748B,#FDE68A,#DC2626,#FFFFFF,#F59E0B,Energetic amber + booking blue
55,Home Services (Plumber/Electrician),#1E40AF,#FFFFFF,#3B82F6,#FFFFFF,#EA580C,#FFFFFF,#EFF6FF,#1E3A8A,#FFFFFF,#1E3A8A,#E9EEF6,#64748B,#BFDBFE,#DC2626,#FFFFFF,#1E40AF,Professional blue + urgent orange [Accent adjusted from #F97316 for WCAG 3:1]
56,Childcare/Daycare,#F472B6,#0F172A,#FBCFE8,#0F172A,#16A34A,#FFFFFF,#FDF2F8,#9D174D,#FFFFFF,#9D174D,#F1F0F6,#64748B,#FCE7F3,#DC2626,#FFFFFF,#F472B6,Soft pink + safe green [Accent adjusted from #22C55E for WCAG 3:1]
57,Senior Care/Elderly,#0369A1,#FFFFFF,#38BDF8,#0F172A,#16A34A,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E7EFF5,#64748B,#E0F2FE,#DC2626,#FFFFFF,#0369A1,Calm blue + reassuring green [Accent adjusted from #22C55E for WCAG 3:1]
58,Medical Clinic,#0891B2,#FFFFFF,#22D3EE,#0F172A,#16A34A,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F6,#64748B,#CCFBF1,#DC2626,#FFFFFF,#0891B2,Medical teal + health green [Accent adjusted from #22C55E for WCAG 3:1]
59,Pharmacy/Drug Store,#15803D,#FFFFFF,#22C55E,#0F172A,#0369A1,#FFFFFF,#F0FDF4,#14532D,#FFFFFF,#14532D,#E8F0F1,#64748B,#BBF7D0,#DC2626,#FFFFFF,#15803D,Pharmacy green + trust blue
60,Dental Practice,#0EA5E9,#0F172A,#38BDF8,#0F172A,#0EA5E9,#0F172A,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E8F2F8,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0EA5E9,Fresh blue + smile yellow [Accent adjusted from #FBBF24 for WCAG 3:1]
61,Veterinary Clinic,#0D9488,#FFFFFF,#14B8A6,#0F172A,#EA580C,#FFFFFF,#F0FDFA,#134E4A,#FFFFFF,#134E4A,#E8F1F4,#64748B,#99F6E4,#DC2626,#FFFFFF,#0D9488,Caring teal + warm orange [Accent adjusted from #F97316 for WCAG 3:1]
62,Florist/Plant Shop,#15803D,#FFFFFF,#22C55E,#0F172A,#EC4899,#FFFFFF,#F0FDF4,#14532D,#FFFFFF,#14532D,#E8F0F1,#64748B,#BBF7D0,#DC2626,#FFFFFF,#15803D,Natural green + floral pink
63,Bakery/Cafe,#92400E,#FFFFFF,#B45309,#FFFFFF,#92400E,#FFFFFF,#FEF3C7,#78350F,#FFFFFF,#78350F,#EDEEF0,#64748B,#FDE68A,#DC2626,#FFFFFF,#92400E,Warm brown + cream white [Accent adjusted from #F8FAFC for WCAG 3:1]
64,Brewery/Winery,#7C2D12,#FFFFFF,#B91C1C,#FFFFFF,#A16207,#FFFFFF,#FEF2F2,#450A0A,#FFFFFF,#450A0A,#ECEDF0,#64748B,#FECACA,#DC2626,#FFFFFF,#7C2D12,Deep burgundy + craft gold [Accent adjusted from #CA8A04 for WCAG 3:1]
65,Airline,#1E3A8A,#FFFFFF,#3B82F6,#FFFFFF,#EA580C,#FFFFFF,#EFF6FF,#1E40AF,#FFFFFF,#1E40AF,#E9EEF5,#64748B,#BFDBFE,#DC2626,#FFFFFF,#1E3A8A,Sky blue + booking orange [Accent adjusted from #F97316 for WCAG 3:1]
66,News/Media Platform,#DC2626,#FFFFFF,#EF4444,#FFFFFF,#1E40AF,#FFFFFF,#FEF2F2,#450A0A,#FFFFFF,#450A0A,#F0EDF1,#64748B,#FECACA,#DC2626,#FFFFFF,#DC2626,Breaking red + link blue
67,Magazine/Blog,#18181B,#FFFFFF,#3F3F46,#FFFFFF,#EC4899,#FFFFFF,#FAFAFA,#09090B,#FFFFFF,#09090B,#E8ECF0,#64748B,#E4E4E7,#DC2626,#FFFFFF,#18181B,Editorial black + accent pink
68,Freelancer Platform,#6366F1,#FFFFFF,#818CF8,#0F172A,#16A34A,#FFFFFF,#EEF2FF,#312E81,#FFFFFF,#312E81,#EBEFF9,#64748B,#C7D2FE,#DC2626,#FFFFFF,#6366F1,Creative indigo + hire green [Accent adjusted from #22C55E for WCAG 3:1]
69,Marketing Agency,#EC4899,#FFFFFF,#F472B6,#0F172A,#0891B2,#FFFFFF,#FDF2F8,#831843,#FFFFFF,#831843,#F1EEF5,#64748B,#FBCFE8,#DC2626,#FFFFFF,#EC4899,Bold pink + creative cyan [Accent adjusted from #06B6D4 for WCAG 3:1]
70,Event Management,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#EA580C,#FFFFFF,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,Excitement purple + action orange [Accent adjusted from #F97316 for WCAG 3:1]
71,Membership/Community,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#16A34A,#FFFFFF,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,Community purple + join green [Accent adjusted from #22C55E for WCAG 3:1]
72,Newsletter Platform,#0369A1,#FFFFFF,#0EA5E9,#0F172A,#EA580C,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E7EFF5,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0369A1,Trust blue + subscribe orange [Accent adjusted from #F97316 for WCAG 3:1]
73,Digital Products/Downloads,#6366F1,#FFFFFF,#818CF8,#0F172A,#16A34A,#FFFFFF,#EEF2FF,#312E81,#FFFFFF,#312E81,#EBEFF9,#64748B,#C7D2FE,#DC2626,#FFFFFF,#6366F1,Digital indigo + buy green [Accent adjusted from #22C55E for WCAG 3:1]
74,Church/Religious Organization,#7C3AED,#FFFFFF,#A78BFA,#0F172A,#A16207,#FFFFFF,#FAF5FF,#4C1D95,#FFFFFF,#4C1D95,#ECEEF9,#64748B,#DDD6FE,#DC2626,#FFFFFF,#7C3AED,Spiritual purple + warm gold [Accent adjusted from #CA8A04 for WCAG 3:1]
75,Sports Team/Club,#DC2626,#FFFFFF,#EF4444,#FFFFFF,#DC2626,#FFFFFF,#FEF2F2,#7F1D1D,#FFFFFF,#7F1D1D,#F0EDF1,#64748B,#FECACA,#DC2626,#FFFFFF,#DC2626,Team red + championship gold [Accent adjusted from #FBBF24 for WCAG 3:1]
76,Museum/Gallery,#18181B,#FFFFFF,#27272A,#FFFFFF,#18181B,#FFFFFF,#FAFAFA,#09090B,#FFFFFF,#09090B,#E8ECF0,#64748B,#E4E4E7,#DC2626,#FFFFFF,#18181B,Gallery black + white space [Accent adjusted from #F8FAFC for WCAG 3:1]
77,Theater/Cinema,#1E1B4B,#FFFFFF,#312E81,#FFFFFF,#CA8A04,#0F172A,#0F0F23,#F8FAFC,#1B1B30,#F8FAFC,#27273B,#94A3B8,#4338CA,#EF4444,#FFFFFF,#1E1B4B,Dramatic dark + spotlight gold
78,Language Learning App,#4F46E5,#FFFFFF,#818CF8,#0F172A,#16A34A,#FFFFFF,#EEF2FF,#312E81,#FFFFFF,#312E81,#EBEEF8,#64748B,#C7D2FE,#DC2626,#FFFFFF,#4F46E5,Learning indigo + progress green [Accent adjusted from #22C55E for WCAG 3:1]
79,Coding Bootcamp,#0F172A,#FFFFFF,#1E293B,#FFFFFF,#22C55E,#0F172A,#020617,#F8FAFC,#0E1223,#F8FAFC,#1A1E2F,#94A3B8,#334155,#EF4444,#FFFFFF,#0F172A,Terminal dark + success green
80,Cybersecurity Platform,#00FF41,#0F172A,#0D0D0D,#FFFFFF,#FF3333,#FFFFFF,#000000,#E0E0E0,#0C130E,#E0E0E0,#181818,#94A3B8,#1F1F1F,#EF4444,#FFFFFF,#00FF41,Matrix green + alert red
81,Developer Tool / IDE,#1E293B,#FFFFFF,#334155,#FFFFFF,#22C55E,#0F172A,#0F172A,#F8FAFC,#1B2336,#F8FAFC,#272F42,#94A3B8,#475569,#EF4444,#FFFFFF,#1E293B,Code dark + run green
82,Biotech / Life Sciences,#0EA5E9,#0F172A,#0284C7,#FFFFFF,#059669,#FFFFFF,#F0F9FF,#0C4A6E,#FFFFFF,#0C4A6E,#E8F2F8,#64748B,#BAE6FD,#DC2626,#FFFFFF,#0EA5E9,DNA blue + life green [Accent adjusted from #10B981 for WCAG 3:1]
83,Space Tech / Aerospace,#F8FAFC,#0F172A,#94A3B8,#0F172A,#3B82F6,#FFFFFF,#0B0B10,#F8FAFC,#1E1E23,#F8FAFC,#232328,#94A3B8,#1E293B,#EF4444,#FFFFFF,#F8FAFC,Star white + launch blue
84,Architecture / Interior,#171717,#FFFFFF,#404040,#FFFFFF,#A16207,#FFFFFF,#FFFFFF,#171717,#FFFFFF,#171717,#E8ECF0,#64748B,#E5E5E5,#DC2626,#FFFFFF,#171717,Minimal black + accent gold [Accent adjusted from #D4AF37 for WCAG 3:1]
85,Quantum Computing Interface,#00FFFF,#0F172A,#7B61FF,#FFFFFF,#FF00FF,#FFFFFF,#050510,#E0E0FF,#101823,#E0E0FF,#1D1D28,#94A3B8,#333344,#EF4444,#FFFFFF,#00FFFF,Quantum cyan + interference purple
86,Biohacking / Longevity App,#FF4D4D,#FFFFFF,#4D94FF,#FFFFFF,#059669,#FFFFFF,#F5F5F7,#1C1C1E,#FFFFFF,#1C1C1E,#F2EEF2,#64748B,#E5E5EA,#DC2626,#FFFFFF,#FF4D4D,Bio red/blue + vitality green [Accent adjusted from #00E676 for WCAG 3:1]
87,Autonomous Drone Fleet Manager,#00FF41,#0F172A,#008F11,#FFFFFF,#FF3333,#FFFFFF,#0D1117,#E6EDF3,#182424,#E6EDF3,#25292F,#94A3B8,#30363D,#EF4444,#FFFFFF,#00FF41,Terminal green + alert red
88,Generative Art Platform,#18181B,#FFFFFF,#3F3F46,#FFFFFF,#EC4899,#FFFFFF,#FAFAFA,#09090B,#FFFFFF,#09090B,#E8ECF0,#64748B,#E4E4E7,#DC2626,#FFFFFF,#18181B,Canvas neutral + creative pink
89,Spatial Computing OS / App,#FFFFFF,#0F172A,#E5E5E5,#0F172A,#FFFFFF,#0F172A,#888888,#000000,#999999,#000000,#777777,#D4D4D4,#CCCCCC,#FF3B30,#FFFFFF,#007AFF,Glass white + system blue [Accent adjusted from #007AFF for WCAG 3:1]
90,Sustainable Energy / Climate Tech,#059669,#FFFFFF,#10B981,#0F172A,#059669,#FFFFFF,#ECFDF5,#064E3B,#FFFFFF,#064E3B,#E8F1F3,#64748B,#A7F3D0,#DC2626,#FFFFFF,#059669,Nature green + solar gold [Accent adjusted from #FBBF24 for WCAG 3:1]
91,Personal Finance Tracker,#1E40AF,#FFFFFF,#3B82F6,#FFFFFF,#059669,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#101A34,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#1E40AF,Trust blue + profit green on dark
92,Chat & Messaging App,#2563EB,#FFFFFF,#6366F1,#FFFFFF,#059669,#FFFFFF,#FFFFFF,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Messenger blue + online green
93,Notes & Writing App,#78716C,#FFFFFF,#A8A29E,#FFFFFF,#D97706,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#F6F6F6,#64748B,#EEEDED,#DC2626,#FFFFFF,#78716C,Warm ink + amber accent on cream
94,Habit Tracker,#D97706,#FFFFFF,#F59E0B,#0F172A,#059669,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#FCF6F0,#64748B,#FAEEE1,#DC2626,#FFFFFF,#D97706,Streak amber + habit green
95,Food Delivery / On-Demand,#EA580C,#FFFFFF,#F97316,#FFFFFF,#2563EB,#FFFFFF,#FFF7ED,#0F172A,#FFFFFF,#0F172A,#FDF4F0,#64748B,#FCEAE1,#DC2626,#FFFFFF,#EA580C,Appetizing orange + trust blue
96,Ride Hailing / Transportation,#1E293B,#FFFFFF,#334155,#FFFFFF,#2563EB,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#10182B,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#1E293B,Map dark + route blue
97,Recipe & Cooking App,#9A3412,#FFFFFF,#C2410C,#FFFFFF,#059669,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#F8F2F0,#64748B,#F2E6E2,#DC2626,#FFFFFF,#9A3412,Warm terracotta + fresh green
98,Meditation & Mindfulness,#7C3AED,#FFFFFF,#8B5CF6,#FFFFFF,#059669,#FFFFFF,#FAF5FF,#0F172A,#FFFFFF,#0F172A,#F7F3FD,#64748B,#EFE7FC,#DC2626,#FFFFFF,#7C3AED,Calm lavender + mindful green
99,Weather App,#0284C7,#FFFFFF,#0EA5E9,#FFFFFF,#F59E0B,#0F172A,#F0F9FF,#0F172A,#FFFFFF,#0F172A,#EFF7FB,#64748B,#E0F0F8,#DC2626,#FFFFFF,#0284C7,Sky blue + sun amber
100,Diary & Journal App,#92400E,#FFFFFF,#A16207,#FFFFFF,#6366F1,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#F8F3F0,#64748B,#F1E8E2,#DC2626,#FFFFFF,#92400E,Warm journal brown + ink violet
101,CRM & Client Management,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#059669,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Professional blue + deal green
102,Inventory & Stock Management,#334155,#FFFFFF,#475569,#FFFFFF,#059669,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F2F3F4,#64748B,#E6E8EA,#DC2626,#FFFFFF,#334155,Industrial slate + stock green
103,Flashcard & Study Tool,#7C3AED,#FFFFFF,#8B5CF6,#FFFFFF,#059669,#FFFFFF,#FAF5FF,#0F172A,#FFFFFF,#0F172A,#F7F3FD,#64748B,#EFE7FC,#DC2626,#FFFFFF,#7C3AED,Study purple + correct green
104,Booking & Appointment App,#0284C7,#FFFFFF,#0EA5E9,#FFFFFF,#059669,#FFFFFF,#F0F9FF,#0F172A,#FFFFFF,#0F172A,#EFF7FB,#64748B,#E0F0F8,#DC2626,#FFFFFF,#0284C7,Calendar blue + available green
105,Invoice & Billing Tool,#1E3A5F,#FFFFFF,#2563EB,#FFFFFF,#059669,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F3F5,#64748B,#E4E7EB,#DC2626,#FFFFFF,#1E3A5F,Navy professional + paid green
106,Grocery & Shopping List,#059669,#FFFFFF,#10B981,#FFFFFF,#D97706,#FFFFFF,#ECFDF5,#0F172A,#FFFFFF,#0F172A,#F0F8F6,#64748B,#E1F2ED,#DC2626,#FFFFFF,#059669,Fresh green + food amber
107,Timer & Pomodoro,#DC2626,#FFFFFF,#EF4444,#FFFFFF,#059669,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#1F1829,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#DC2626,Focus red on dark + break green
108,Parenting & Baby Tracker,#EC4899,#FFFFFF,#F472B6,#FFFFFF,#0284C7,#FFFFFF,#FDF2F8,#0F172A,#FFFFFF,#0F172A,#FDF4F8,#64748B,#FCE9F2,#DC2626,#FFFFFF,#EC4899,Soft pink + trust blue
109,Scanner & Document Manager,#1E293B,#FFFFFF,#334155,#FFFFFF,#2563EB,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F2F3,#64748B,#E4E5E7,#DC2626,#FFFFFF,#1E293B,Document grey + scan blue
110,Calendar & Scheduling App,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#059669,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Calendar blue + event green
111,Password Manager,#1E3A5F,#FFFFFF,#334155,#FFFFFF,#059669,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#10192E,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#1E3A5F,Vault dark blue + secure green
112,Expense Splitter / Bill Split,#059669,#FFFFFF,#10B981,#FFFFFF,#DC2626,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F0F8F6,#64748B,#E1F2ED,#DC2626,#FFFFFF,#059669,Balance green + owe red
113,Voice Recorder & Memo,#DC2626,#FFFFFF,#EF4444,#FFFFFF,#2563EB,#FFFFFF,#FFFFFF,#0F172A,#FFFFFF,#0F172A,#FCF1F1,#64748B,#FAE4E4,#DC2626,#FFFFFF,#DC2626,Recording red + waveform blue
114,Bookmark & Read-Later,#D97706,#FFFFFF,#F59E0B,#0F172A,#2563EB,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#FCF6F0,#64748B,#FAEEE1,#DC2626,#FFFFFF,#D97706,Warm amber + link blue
115,Translator App,#2563EB,#FFFFFF,#0891B2,#FFFFFF,#EA580C,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Global blue + teal + accent orange
116,Calculator & Unit Converter,#EA580C,#FFFFFF,#F97316,#FFFFFF,#2563EB,#FFFFFF,#1C1917,#FFFFFF,#262321,#FFFFFF,#2C1E16,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#EA580C,Operation orange on dark
117,Alarm & World Clock,#D97706,#FFFFFF,#F59E0B,#0F172A,#6366F1,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#1F1E27,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#D97706,Time amber + night indigo on dark
118,File Manager & Transfer,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#D97706,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Folder blue + file amber
119,Email Client,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#DC2626,#FFFFFF,#FFFFFF,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Inbox blue + priority red
120,Casual Puzzle Game,#EC4899,#FFFFFF,#8B5CF6,#FFFFFF,#F59E0B,#0F172A,#FDF2F8,#0F172A,#FFFFFF,#0F172A,#FDF4F8,#64748B,#FCE9F2,#DC2626,#FFFFFF,#EC4899,Cheerful pink + reward gold
121,Trivia & Quiz Game,#2563EB,#FFFFFF,#7C3AED,#FFFFFF,#F59E0B,#0F172A,#EFF6FF,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Quiz blue + gold leaderboard
122,Card & Board Game,#15803D,#FFFFFF,#166534,#FFFFFF,#D97706,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#0F1F2B,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#15803D,Felt green + gold on dark
123,Idle & Clicker Game,#D97706,#FFFFFF,#F59E0B,#0F172A,#7C3AED,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#FCF6F0,#64748B,#FAEEE1,#DC2626,#FFFFFF,#D97706,Coin gold + prestige purple
124,Word & Crossword Game,#15803D,#FFFFFF,#059669,#FFFFFF,#D97706,#FFFFFF,#FFFFFF,#0F172A,#FFFFFF,#0F172A,#F0F7F3,#64748B,#E2EFE7,#DC2626,#FFFFFF,#15803D,Word green + letter amber
125,Arcade & Retro Game,#DC2626,#FFFFFF,#2563EB,#FFFFFF,#22C55E,#0F172A,#0F172A,#FFFFFF,#192134,#FFFFFF,#1F1829,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#DC2626,Neon red+blue on dark + score green
126,Photo Editor & Filters,#7C3AED,#FFFFFF,#6366F1,#FFFFFF,#0891B2,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#171939,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#7C3AED,Editor violet + filter cyan on dark
127,Short Video Editor,#EC4899,#FFFFFF,#DB2777,#FFFFFF,#2563EB,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#201A32,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#EC4899,Video pink on dark + timeline blue
128,Drawing & Sketching Canvas,#7C3AED,#FFFFFF,#8B5CF6,#FFFFFF,#0891B2,#FFFFFF,#1C1917,#FFFFFF,#262321,#FFFFFF,#231B28,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#7C3AED,Canvas purple + tool teal on dark
129,Music Creation & Beat Maker,#7C3AED,#FFFFFF,#6366F1,#FFFFFF,#22C55E,#0F172A,#0F172A,#FFFFFF,#192134,#FFFFFF,#171939,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#7C3AED,Studio purple + waveform green on dark
130,Meme & Sticker Maker,#EC4899,#FFFFFF,#F59E0B,#0F172A,#2563EB,#FFFFFF,#FFFFFF,#0F172A,#FFFFFF,#0F172A,#FDF4F8,#64748B,#FCE9F2,#DC2626,#FFFFFF,#EC4899,Viral pink + comedy yellow + share blue
131,AI Photo & Avatar Generator,#7C3AED,#FFFFFF,#6366F1,#FFFFFF,#EC4899,#FFFFFF,#FAF5FF,#0F172A,#FFFFFF,#0F172A,#F7F3FD,#64748B,#EFE7FC,#DC2626,#FFFFFF,#7C3AED,AI purple + generation pink
132,Link-in-Bio Page Builder,#2563EB,#FFFFFF,#7C3AED,#FFFFFF,#EC4899,#FFFFFF,#FFFFFF,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Brand blue + creator purple
133,Wardrobe & Outfit Planner,#BE185D,#FFFFFF,#EC4899,#FFFFFF,#D97706,#FFFFFF,#FDF2F8,#0F172A,#FFFFFF,#0F172A,#FBF1F5,#64748B,#F7E3EB,#DC2626,#FFFFFF,#BE185D,Fashion rose + gold accent
134,Plant Care Tracker,#15803D,#FFFFFF,#059669,#FFFFFF,#D97706,#FFFFFF,#F0FDF4,#0F172A,#FFFFFF,#0F172A,#F0F7F3,#64748B,#E2EFE7,#DC2626,#FFFFFF,#15803D,Nature green + sun yellow
135,Book & Reading Tracker,#78716C,#FFFFFF,#92400E,#FFFFFF,#D97706,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#F6F6F6,#64748B,#EEEDED,#DC2626,#FFFFFF,#78716C,Book brown + page amber
136,Couple & Relationship App,#BE185D,#FFFFFF,#EC4899,#FFFFFF,#DC2626,#FFFFFF,#FDF2F8,#0F172A,#FFFFFF,#0F172A,#FBF1F5,#64748B,#F7E3EB,#DC2626,#FFFFFF,#BE185D,Romance rose + love red
137,Family Calendar & Chores,#2563EB,#FFFFFF,#059669,#FFFFFF,#D97706,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Family blue + chore green
138,Mood Tracker,#7C3AED,#FFFFFF,#6366F1,#FFFFFF,#D97706,#FFFFFF,#FAF5FF,#0F172A,#FFFFFF,#0F172A,#F7F3FD,#64748B,#EFE7FC,#DC2626,#FFFFFF,#7C3AED,Mood purple + insight amber
139,Gift & Wishlist,#DC2626,#FFFFFF,#D97706,#FFFFFF,#EC4899,#FFFFFF,#FFF1F2,#0F172A,#FFFFFF,#0F172A,#FCF1F1,#64748B,#FAE4E4,#DC2626,#FFFFFF,#DC2626,Gift red + gold + surprise pink
140,Running & Cycling GPS,#EA580C,#FFFFFF,#F97316,#FFFFFF,#059669,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#201C27,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#EA580C,Energetic orange + pace green on dark
141,Yoga & Stretching Guide,#6B7280,#FFFFFF,#78716C,#FFFFFF,#0891B2,#FFFFFF,#F5F5F0,#0F172A,#FFFFFF,#0F172A,#F6F6F7,#64748B,#EDEEEF,#DC2626,#FFFFFF,#6B7280,Sage neutral + calm teal
142,Sleep Tracker,#4338CA,#FFFFFF,#6366F1,#FFFFFF,#7C3AED,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#131936,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#4338CA,Night indigo + dream violet on dark
143,Calorie & Nutrition Counter,#059669,#FFFFFF,#10B981,#FFFFFF,#EA580C,#FFFFFF,#ECFDF5,#0F172A,#FFFFFF,#0F172A,#F0F8F6,#64748B,#E1F2ED,#DC2626,#FFFFFF,#059669,Healthy green + macro orange
144,Period & Cycle Tracker,#BE185D,#FFFFFF,#EC4899,#FFFFFF,#7C3AED,#FFFFFF,#FDF2F8,#0F172A,#FFFFFF,#0F172A,#FBF1F5,#64748B,#F7E3EB,#DC2626,#FFFFFF,#BE185D,Blush rose + fertility lavender
145,Medication & Pill Reminder,#0284C7,#FFFFFF,#0891B2,#FFFFFF,#DC2626,#FFFFFF,#F0F9FF,#0F172A,#FFFFFF,#0F172A,#EFF7FB,#64748B,#E0F0F8,#DC2626,#FFFFFF,#0284C7,Medical blue + alert red
146,Water & Hydration Reminder,#0284C7,#FFFFFF,#06B6D4,#FFFFFF,#0891B2,#FFFFFF,#F0F9FF,#0F172A,#FFFFFF,#0F172A,#EFF7FB,#64748B,#E0F0F8,#DC2626,#FFFFFF,#0284C7,Refreshing blue + water cyan
147,Fasting & Intermittent Timer,#6366F1,#FFFFFF,#4338CA,#FFFFFF,#059669,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#151D39,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#6366F1,Fasting indigo on dark + eating green
148,Anonymous Community / Confession,#475569,#FFFFFF,#334155,#FFFFFF,#0891B2,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#131B2F,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#475569,Protective grey + subtle teal on dark
149,Local Events & Discovery,#EA580C,#FFFFFF,#F97316,#FFFFFF,#2563EB,#FFFFFF,#FFF7ED,#0F172A,#FFFFFF,#0F172A,#FDF4F0,#64748B,#FCEAE1,#DC2626,#FFFFFF,#EA580C,Event orange + map blue
150,Study Together / Virtual Coworking,#2563EB,#FFFFFF,#3B82F6,#FFFFFF,#059669,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Focus blue + session green
151,Coding Challenge & Practice,#22C55E,#0F172A,#059669,#FFFFFF,#D97706,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#10242E,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#22C55E,Code green + difficulty amber on dark
152,Kids Learning (ABC & Math),#2563EB,#FFFFFF,#F59E0B,#0F172A,#EC4899,#FFFFFF,#EFF6FF,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Learning blue + play yellow + fun pink
153,Music Instrument Learning,#DC2626,#FFFFFF,#9A3412,#FFFFFF,#D97706,#FFFFFF,#FFFBEB,#0F172A,#FFFFFF,#0F172A,#FCF1F1,#64748B,#FAE4E4,#DC2626,#FFFFFF,#DC2626,Musical red + warm amber
154,Parking Finder,#2563EB,#FFFFFF,#059669,#FFFFFF,#DC2626,#FFFFFF,#F0F9FF,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Available blue/green + occupied red
155,Public Transit Guide,#2563EB,#FFFFFF,#0891B2,#FFFFFF,#EA580C,#FFFFFF,#F8FAFC,#0F172A,#FFFFFF,#0F172A,#F1F5FD,#64748B,#E4ECFC,#DC2626,#FFFFFF,#2563EB,Transit blue + line colors
156,Road Trip Planner,#EA580C,#FFFFFF,#0891B2,#FFFFFF,#D97706,#FFFFFF,#FFF7ED,#0F172A,#FFFFFF,#0F172A,#FDF4F0,#64748B,#FCEAE1,#DC2626,#FFFFFF,#EA580C,Adventure orange + map teal
157,VPN & Privacy Tool,#1E3A5F,#FFFFFF,#334155,#FFFFFF,#22C55E,#0F172A,#0F172A,#FFFFFF,#192134,#FFFFFF,#10192E,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#1E3A5F,Shield dark + connected green
158,Emergency SOS & Safety,#DC2626,#FFFFFF,#EF4444,#FFFFFF,#2563EB,#FFFFFF,#FFF1F2,#0F172A,#FFFFFF,#0F172A,#FCF1F1,#64748B,#FAE4E4,#DC2626,#FFFFFF,#DC2626,Alert red + safety blue
159,Wallpaper & Theme App,#7C3AED,#FFFFFF,#EC4899,#FFFFFF,#2563EB,#FFFFFF,#FAF5FF,#0F172A,#FFFFFF,#0F172A,#F7F3FD,#64748B,#EFE7FC,#DC2626,#FFFFFF,#7C3AED,Aesthetic purple + trending pink
160,White Noise & Ambient Sound,#475569,#FFFFFF,#334155,#FFFFFF,#4338CA,#FFFFFF,#0F172A,#FFFFFF,#192134,#FFFFFF,#131B2F,#94A3B8,"rgba(255,255,255,0.08)",#DC2626,#FFFFFF,#475569,Ambient grey + deep indigo on dark
161,Home Decoration & Interior Design,#78716C,#FFFFFF,#A8A29E,#FFFFFF,#D97706,#FFFFFF,#FAF5F2,#0F172A,#FFFFFF,#0F172A,#F6F6F6,#64748B,#EEEDED,#DC2626,#FFFFFF,#78716C,Interior warm grey + gold accent
1 No Product Type Primary On Primary Secondary On Secondary Accent On Accent Background Foreground Card Card Foreground Muted Muted Foreground Border Destructive On Destructive Ring Notes
2 1 SaaS (General) #2563EB #FFFFFF #3B82F6 #FFFFFF #EA580C #FFFFFF #F8FAFC #1E293B #FFFFFF #1E293B #E9EFF8 #64748B #E2E8F0 #DC2626 #FFFFFF #2563EB Trust blue + orange CTA contrast [Accent adjusted from #F97316 for WCAG 3:1]
3 2 Micro SaaS #6366F1 #FFFFFF #818CF8 #0F172A #059669 #FFFFFF #F5F3FF #1E1B4B #FFFFFF #1E1B4B #EBEFF9 #64748B #E0E7FF #DC2626 #FFFFFF #6366F1 Indigo primary + emerald CTA [Accent adjusted from #10B981 for WCAG 3:1]
4 3 E-commerce #059669 #FFFFFF #10B981 #0F172A #EA580C #FFFFFF #ECFDF5 #064E3B #FFFFFF #064E3B #E8F1F3 #64748B #A7F3D0 #DC2626 #FFFFFF #059669 Success green + urgency orange [Accent adjusted from #F97316 for WCAG 3:1]
5 4 E-commerce Luxury #1C1917 #FFFFFF #44403C #FFFFFF #A16207 #FFFFFF #FAFAF9 #0C0A09 #FFFFFF #0C0A09 #E8ECF0 #64748B #D6D3D1 #DC2626 #FFFFFF #1C1917 Premium dark + gold accent [Accent adjusted from #CA8A04 for WCAG 3:1]
6 5 B2B Service #0F172A #FFFFFF #334155 #FFFFFF #0369A1 #FFFFFF #F8FAFC #020617 #FFFFFF #020617 #E8ECF1 #64748B #E2E8F0 #DC2626 #FFFFFF #0F172A Professional navy + blue CTA
7 6 Financial Dashboard #0F172A #FFFFFF #1E293B #FFFFFF #22C55E #0F172A #020617 #F8FAFC #0E1223 #F8FAFC #1A1E2F #94A3B8 #334155 #EF4444 #FFFFFF #0F172A Dark bg + green positive indicators
8 7 Analytics Dashboard #1E40AF #FFFFFF #3B82F6 #FFFFFF #D97706 #FFFFFF #F8FAFC #1E3A8A #FFFFFF #1E3A8A #E9EEF6 #64748B #DBEAFE #DC2626 #FFFFFF #1E40AF Blue data + amber highlights [Accent adjusted from #F59E0B for WCAG 3:1]
9 8 Healthcare App #0891B2 #FFFFFF #22D3EE #0F172A #059669 #FFFFFF #ECFEFF #164E63 #FFFFFF #164E63 #E8F1F6 #64748B #A5F3FC #DC2626 #FFFFFF #0891B2 Calm cyan + health green
10 9 Educational App #4F46E5 #FFFFFF #818CF8 #0F172A #EA580C #FFFFFF #EEF2FF #1E1B4B #FFFFFF #1E1B4B #EBEEF8 #64748B #C7D2FE #DC2626 #FFFFFF #4F46E5 Playful indigo + energetic orange [Accent adjusted from #F97316 for WCAG 3:1]
11 10 Creative Agency #EC4899 #FFFFFF #F472B6 #0F172A #0891B2 #FFFFFF #FDF2F8 #831843 #FFFFFF #831843 #F1EEF5 #64748B #FBCFE8 #DC2626 #FFFFFF #EC4899 Bold pink + cyan accent [Accent adjusted from #06B6D4 for WCAG 3:1]
12 11 Portfolio/Personal #18181B #FFFFFF #3F3F46 #FFFFFF #2563EB #FFFFFF #FAFAFA #09090B #FFFFFF #09090B #E8ECF0 #64748B #E4E4E7 #DC2626 #FFFFFF #18181B Monochrome + blue accent
13 12 Gaming #7C3AED #FFFFFF #A78BFA #0F172A #F43F5E #FFFFFF #0F0F23 #E2E8F0 #1E1C35 #E2E8F0 #27273B #94A3B8 #4C1D95 #EF4444 #FFFFFF #7C3AED Neon purple + rose action
14 13 Government/Public Service #0F172A #FFFFFF #334155 #FFFFFF #0369A1 #FFFFFF #F8FAFC #020617 #FFFFFF #020617 #E8ECF1 #64748B #E2E8F0 #DC2626 #FFFFFF #0F172A High contrast navy + blue
15 14 Fintech/Crypto #F59E0B #0F172A #FBBF24 #0F172A #8B5CF6 #FFFFFF #0F172A #F8FAFC #222735 #F8FAFC #272F42 #94A3B8 #334155 #EF4444 #FFFFFF #F59E0B Gold trust + purple tech
16 15 Social Media App #E11D48 #FFFFFF #FB7185 #0F172A #2563EB #FFFFFF #FFF1F2 #881337 #FFFFFF #881337 #F0ECF2 #64748B #FECDD3 #DC2626 #FFFFFF #E11D48 Vibrant rose + engagement blue
17 16 Productivity Tool #0D9488 #FFFFFF #14B8A6 #0F172A #EA580C #FFFFFF #F0FDFA #134E4A #FFFFFF #134E4A #E8F1F4 #64748B #99F6E4 #DC2626 #FFFFFF #0D9488 Teal focus + action orange [Accent adjusted from #F97316 for WCAG 3:1]
18 17 Design System/Component Library #4F46E5 #FFFFFF #6366F1 #FFFFFF #EA580C #FFFFFF #EEF2FF #312E81 #FFFFFF #312E81 #EBEEF8 #64748B #C7D2FE #DC2626 #FFFFFF #4F46E5 Indigo brand + doc hierarchy [Accent adjusted from #F97316 for WCAG 3:1]
19 18 AI/Chatbot Platform #7C3AED #FFFFFF #A78BFA #0F172A #0891B2 #FFFFFF #FAF5FF #1E1B4B #FFFFFF #1E1B4B #ECEEF9 #64748B #DDD6FE #DC2626 #FFFFFF #7C3AED AI purple + cyan interactions [Accent adjusted from #06B6D4 for WCAG 3:1]
20 19 NFT/Web3 Platform #8B5CF6 #FFFFFF #A78BFA #0F172A #FBBF24 #0F172A #0F0F23 #F8FAFC #1E1D35 #F8FAFC #27273B #94A3B8 #4C1D95 #EF4444 #FFFFFF #8B5CF6 Purple tech + gold value
21 20 Creator Economy Platform #EC4899 #FFFFFF #F472B6 #0F172A #EA580C #FFFFFF #FDF2F8 #831843 #FFFFFF #831843 #F1EEF5 #64748B #FBCFE8 #DC2626 #FFFFFF #EC4899 Creator pink + engagement orange [Accent adjusted from #F97316 for WCAG 3:1]
22 21 Remote Work/Collaboration Tool #6366F1 #FFFFFF #818CF8 #0F172A #059669 #FFFFFF #F5F3FF #312E81 #FFFFFF #312E81 #EBEFF9 #64748B #E0E7FF #DC2626 #FFFFFF #6366F1 Calm indigo + success green [Accent adjusted from #10B981 for WCAG 3:1]
23 22 Mental Health App #8B5CF6 #FFFFFF #C4B5FD #0F172A #059669 #FFFFFF #FAF5FF #4C1D95 #FFFFFF #4C1D95 #EDEFF9 #64748B #EDE9FE #DC2626 #FFFFFF #8B5CF6 Calming lavender + wellness green [Accent adjusted from #10B981 for WCAG 3:1]
24 23 Pet Tech App #F97316 #0F172A #FB923C #0F172A #2563EB #FFFFFF #FFF7ED #9A3412 #FFFFFF #9A3412 #F1F0F0 #64748B #FED7AA #DC2626 #FFFFFF #F97316 Playful orange + trust blue
25 24 Smart Home/IoT Dashboard #1E293B #FFFFFF #334155 #FFFFFF #22C55E #0F172A #0F172A #F8FAFC #1B2336 #F8FAFC #272F42 #94A3B8 #475569 #EF4444 #FFFFFF #1E293B Dark tech + status green
26 25 EV/Charging Ecosystem #0891B2 #FFFFFF #22D3EE #0F172A #16A34A #FFFFFF #ECFEFF #164E63 #FFFFFF #164E63 #E8F1F6 #64748B #A5F3FC #DC2626 #FFFFFF #0891B2 Electric cyan + eco green [Accent adjusted from #22C55E for WCAG 3:1]
27 26 Subscription Box Service #D946EF #FFFFFF #E879F9 #0F172A #EA580C #FFFFFF #FDF4FF #86198F #FFFFFF #86198F #F0EEF9 #64748B #F5D0FE #DC2626 #FFFFFF #D946EF Excitement purple + urgency orange [Accent adjusted from #F97316 for WCAG 3:1]
28 27 Podcast Platform #1E1B4B #FFFFFF #312E81 #FFFFFF #F97316 #0F172A #0F0F23 #F8FAFC #1B1B30 #F8FAFC #27273B #94A3B8 #4338CA #EF4444 #FFFFFF #1E1B4B Dark audio + warm accent
29 28 Dating App #E11D48 #FFFFFF #FB7185 #0F172A #EA580C #FFFFFF #FFF1F2 #881337 #FFFFFF #881337 #F0ECF2 #64748B #FECDD3 #DC2626 #FFFFFF #E11D48 Romantic rose + warm orange [Accent adjusted from #F97316 for WCAG 3:1]
30 29 Micro-Credentials/Badges Platform #0369A1 #FFFFFF #0EA5E9 #0F172A #A16207 #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E7EFF5 #64748B #BAE6FD #DC2626 #FFFFFF #0369A1 Trust blue + achievement gold [Accent adjusted from #CA8A04 for WCAG 3:1]
31 30 Knowledge Base/Documentation #475569 #FFFFFF #64748B #FFFFFF #2563EB #FFFFFF #F8FAFC #1E293B #FFFFFF #1E293B #EAEFF3 #64748B #E2E8F0 #DC2626 #FFFFFF #475569 Neutral grey + link blue
32 31 Hyperlocal Services #059669 #FFFFFF #10B981 #0F172A #EA580C #FFFFFF #ECFDF5 #064E3B #FFFFFF #064E3B #E8F1F3 #64748B #A7F3D0 #DC2626 #FFFFFF #059669 Location green + action orange [Accent adjusted from #F97316 for WCAG 3:1]
33 32 Beauty/Spa/Wellness Service #EC4899 #FFFFFF #F9A8D4 #0F172A #8B5CF6 #FFFFFF #FDF2F8 #831843 #FFFFFF #831843 #F1EEF5 #64748B #FBCFE8 #DC2626 #FFFFFF #EC4899 Soft pink + lavender luxury
34 33 Luxury/Premium Brand #1C1917 #FFFFFF #44403C #FFFFFF #A16207 #FFFFFF #FAFAF9 #0C0A09 #FFFFFF #0C0A09 #E8ECF0 #64748B #D6D3D1 #DC2626 #FFFFFF #1C1917 Premium black + gold accent [Accent adjusted from #CA8A04 for WCAG 3:1]
35 34 Restaurant/Food Service #DC2626 #FFFFFF #F87171 #0F172A #A16207 #FFFFFF #FEF2F2 #450A0A #FFFFFF #450A0A #F0EDF1 #64748B #FECACA #DC2626 #FFFFFF #DC2626 Appetizing red + warm gold [Accent adjusted from #CA8A04 for WCAG 3:1]
36 35 Fitness/Gym App #F97316 #0F172A #FB923C #0F172A #22C55E #0F172A #1F2937 #F8FAFC #313742 #F8FAFC #37414F #94A3B8 #374151 #EF4444 #FFFFFF #F97316 Energy orange + success green
37 36 Real Estate/Property #0F766E #FFFFFF #14B8A6 #0F172A #0369A1 #FFFFFF #F0FDFA #134E4A #FFFFFF #134E4A #E8F0F3 #64748B #99F6E4 #DC2626 #FFFFFF #0F766E Trust teal + professional blue
38 37 Travel/Tourism Agency #0EA5E9 #0F172A #38BDF8 #0F172A #EA580C #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E8F2F8 #64748B #BAE6FD #DC2626 #FFFFFF #0EA5E9 Sky blue + adventure orange [Accent adjusted from #F97316 for WCAG 3:1]
39 38 Hotel/Hospitality #1E3A8A #FFFFFF #3B82F6 #FFFFFF #A16207 #FFFFFF #F8FAFC #1E40AF #FFFFFF #1E40AF #E9EEF5 #64748B #BFDBFE #DC2626 #FFFFFF #1E3A8A Luxury navy + gold service [Accent adjusted from #CA8A04 for WCAG 3:1]
40 39 Wedding/Event Planning #DB2777 #FFFFFF #F472B6 #0F172A #A16207 #FFFFFF #FDF2F8 #831843 #FFFFFF #831843 #F0EDF4 #64748B #FBCFE8 #DC2626 #FFFFFF #DB2777 Romantic pink + elegant gold [Accent adjusted from #CA8A04 for WCAG 3:1]
41 40 Legal Services #1E3A8A #FFFFFF #1E40AF #FFFFFF #B45309 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #E9EEF5 #64748B #CBD5E1 #DC2626 #FFFFFF #1E3A8A Authority navy + trust gold
42 41 Insurance Platform #0369A1 #FFFFFF #0EA5E9 #0F172A #16A34A #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E7EFF5 #64748B #BAE6FD #DC2626 #FFFFFF #0369A1 Security blue + protected green [Accent adjusted from #22C55E for WCAG 3:1]
43 42 Banking/Traditional Finance #0F172A #FFFFFF #1E3A8A #FFFFFF #A16207 #FFFFFF #F8FAFC #020617 #FFFFFF #020617 #E8ECF1 #64748B #E2E8F0 #DC2626 #FFFFFF #0F172A Trust navy + premium gold [Accent adjusted from #CA8A04 for WCAG 3:1]
44 43 Online Course/E-learning #0D9488 #FFFFFF #2DD4BF #0F172A #EA580C #FFFFFF #F0FDFA #134E4A #FFFFFF #134E4A #E8F1F4 #64748B #5EEAD4 #DC2626 #FFFFFF #0D9488 Progress teal + achievement orange [Accent adjusted from #F97316 for WCAG 3:1]
45 44 Non-profit/Charity #0891B2 #FFFFFF #22D3EE #0F172A #EA580C #FFFFFF #ECFEFF #164E63 #FFFFFF #164E63 #E8F1F6 #64748B #A5F3FC #DC2626 #FFFFFF #0891B2 Compassion blue + action orange [Accent adjusted from #F97316 for WCAG 3:1]
46 45 Music Streaming #1E1B4B #FFFFFF #4338CA #FFFFFF #22C55E #0F172A #0F0F23 #F8FAFC #1B1B30 #F8FAFC #27273B #94A3B8 #312E81 #EF4444 #FFFFFF #1E1B4B Dark audio + play green
47 46 Video Streaming/OTT #0F0F23 #FFFFFF #1E1B4B #FFFFFF #E11D48 #FFFFFF #000000 #F8FAFC #0C0C0D #F8FAFC #181818 #94A3B8 #312E81 #EF4444 #FFFFFF #0F0F23 Cinema dark + play red
48 47 Job Board/Recruitment #0369A1 #FFFFFF #0EA5E9 #0F172A #16A34A #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E7EFF5 #64748B #BAE6FD #DC2626 #FFFFFF #0369A1 Professional blue + success green [Accent adjusted from #22C55E for WCAG 3:1]
49 48 Marketplace (P2P) #7C3AED #FFFFFF #A78BFA #0F172A #16A34A #FFFFFF #FAF5FF #4C1D95 #FFFFFF #4C1D95 #ECEEF9 #64748B #DDD6FE #DC2626 #FFFFFF #7C3AED Trust purple + transaction green [Accent adjusted from #22C55E for WCAG 3:1]
50 49 Logistics/Delivery #2563EB #FFFFFF #3B82F6 #FFFFFF #EA580C #FFFFFF #EFF6FF #1E40AF #FFFFFF #1E40AF #E9EFF8 #64748B #BFDBFE #DC2626 #FFFFFF #2563EB Tracking blue + delivery orange [Accent adjusted from #F97316 for WCAG 3:1]
51 50 Agriculture/Farm Tech #15803D #FFFFFF #22C55E #0F172A #A16207 #FFFFFF #F0FDF4 #14532D #FFFFFF #14532D #E8F0F1 #64748B #BBF7D0 #DC2626 #FFFFFF #15803D Earth green + harvest gold [Accent adjusted from #CA8A04 for WCAG 3:1]
52 51 Construction/Architecture #64748B #FFFFFF #94A3B8 #0F172A #EA580C #FFFFFF #F8FAFC #334155 #FFFFFF #334155 #EBF0F5 #64748B #E2E8F0 #DC2626 #FFFFFF #64748B Industrial grey + safety orange [Accent adjusted from #F97316 for WCAG 3:1]
53 52 Automotive/Car Dealership #1E293B #FFFFFF #334155 #FFFFFF #DC2626 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #E9EDF1 #64748B #E2E8F0 #DC2626 #FFFFFF #1E293B Premium dark + action red
54 53 Photography Studio #18181B #FFFFFF #27272A #FFFFFF #F8FAFC #0F172A #000000 #FAFAFA #0C0C0C #FAFAFA #181818 #94A3B8 #3F3F46 #EF4444 #FFFFFF #18181B Pure black + white contrast
55 54 Coworking Space #F59E0B #0F172A #FBBF24 #0F172A #2563EB #FFFFFF #FFFBEB #78350F #FFFFFF #78350F #F1F2EF #64748B #FDE68A #DC2626 #FFFFFF #F59E0B Energetic amber + booking blue
56 55 Home Services (Plumber/Electrician) #1E40AF #FFFFFF #3B82F6 #FFFFFF #EA580C #FFFFFF #EFF6FF #1E3A8A #FFFFFF #1E3A8A #E9EEF6 #64748B #BFDBFE #DC2626 #FFFFFF #1E40AF Professional blue + urgent orange [Accent adjusted from #F97316 for WCAG 3:1]
57 56 Childcare/Daycare #F472B6 #0F172A #FBCFE8 #0F172A #16A34A #FFFFFF #FDF2F8 #9D174D #FFFFFF #9D174D #F1F0F6 #64748B #FCE7F3 #DC2626 #FFFFFF #F472B6 Soft pink + safe green [Accent adjusted from #22C55E for WCAG 3:1]
58 57 Senior Care/Elderly #0369A1 #FFFFFF #38BDF8 #0F172A #16A34A #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E7EFF5 #64748B #E0F2FE #DC2626 #FFFFFF #0369A1 Calm blue + reassuring green [Accent adjusted from #22C55E for WCAG 3:1]
59 58 Medical Clinic #0891B2 #FFFFFF #22D3EE #0F172A #16A34A #FFFFFF #F0FDFA #134E4A #FFFFFF #134E4A #E8F1F6 #64748B #CCFBF1 #DC2626 #FFFFFF #0891B2 Medical teal + health green [Accent adjusted from #22C55E for WCAG 3:1]
60 59 Pharmacy/Drug Store #15803D #FFFFFF #22C55E #0F172A #0369A1 #FFFFFF #F0FDF4 #14532D #FFFFFF #14532D #E8F0F1 #64748B #BBF7D0 #DC2626 #FFFFFF #15803D Pharmacy green + trust blue
61 60 Dental Practice #0EA5E9 #0F172A #38BDF8 #0F172A #0EA5E9 #0F172A #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E8F2F8 #64748B #BAE6FD #DC2626 #FFFFFF #0EA5E9 Fresh blue + smile yellow [Accent adjusted from #FBBF24 for WCAG 3:1]
62 61 Veterinary Clinic #0D9488 #FFFFFF #14B8A6 #0F172A #EA580C #FFFFFF #F0FDFA #134E4A #FFFFFF #134E4A #E8F1F4 #64748B #99F6E4 #DC2626 #FFFFFF #0D9488 Caring teal + warm orange [Accent adjusted from #F97316 for WCAG 3:1]
63 62 Florist/Plant Shop #15803D #FFFFFF #22C55E #0F172A #EC4899 #FFFFFF #F0FDF4 #14532D #FFFFFF #14532D #E8F0F1 #64748B #BBF7D0 #DC2626 #FFFFFF #15803D Natural green + floral pink
64 63 Bakery/Cafe #92400E #FFFFFF #B45309 #FFFFFF #92400E #FFFFFF #FEF3C7 #78350F #FFFFFF #78350F #EDEEF0 #64748B #FDE68A #DC2626 #FFFFFF #92400E Warm brown + cream white [Accent adjusted from #F8FAFC for WCAG 3:1]
65 64 Brewery/Winery #7C2D12 #FFFFFF #B91C1C #FFFFFF #A16207 #FFFFFF #FEF2F2 #450A0A #FFFFFF #450A0A #ECEDF0 #64748B #FECACA #DC2626 #FFFFFF #7C2D12 Deep burgundy + craft gold [Accent adjusted from #CA8A04 for WCAG 3:1]
66 65 Airline #1E3A8A #FFFFFF #3B82F6 #FFFFFF #EA580C #FFFFFF #EFF6FF #1E40AF #FFFFFF #1E40AF #E9EEF5 #64748B #BFDBFE #DC2626 #FFFFFF #1E3A8A Sky blue + booking orange [Accent adjusted from #F97316 for WCAG 3:1]
67 66 News/Media Platform #DC2626 #FFFFFF #EF4444 #FFFFFF #1E40AF #FFFFFF #FEF2F2 #450A0A #FFFFFF #450A0A #F0EDF1 #64748B #FECACA #DC2626 #FFFFFF #DC2626 Breaking red + link blue
68 67 Magazine/Blog #18181B #FFFFFF #3F3F46 #FFFFFF #EC4899 #FFFFFF #FAFAFA #09090B #FFFFFF #09090B #E8ECF0 #64748B #E4E4E7 #DC2626 #FFFFFF #18181B Editorial black + accent pink
69 68 Freelancer Platform #6366F1 #FFFFFF #818CF8 #0F172A #16A34A #FFFFFF #EEF2FF #312E81 #FFFFFF #312E81 #EBEFF9 #64748B #C7D2FE #DC2626 #FFFFFF #6366F1 Creative indigo + hire green [Accent adjusted from #22C55E for WCAG 3:1]
70 69 Marketing Agency #EC4899 #FFFFFF #F472B6 #0F172A #0891B2 #FFFFFF #FDF2F8 #831843 #FFFFFF #831843 #F1EEF5 #64748B #FBCFE8 #DC2626 #FFFFFF #EC4899 Bold pink + creative cyan [Accent adjusted from #06B6D4 for WCAG 3:1]
71 70 Event Management #7C3AED #FFFFFF #A78BFA #0F172A #EA580C #FFFFFF #FAF5FF #4C1D95 #FFFFFF #4C1D95 #ECEEF9 #64748B #DDD6FE #DC2626 #FFFFFF #7C3AED Excitement purple + action orange [Accent adjusted from #F97316 for WCAG 3:1]
72 71 Membership/Community #7C3AED #FFFFFF #A78BFA #0F172A #16A34A #FFFFFF #FAF5FF #4C1D95 #FFFFFF #4C1D95 #ECEEF9 #64748B #DDD6FE #DC2626 #FFFFFF #7C3AED Community purple + join green [Accent adjusted from #22C55E for WCAG 3:1]
73 72 Newsletter Platform #0369A1 #FFFFFF #0EA5E9 #0F172A #EA580C #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E7EFF5 #64748B #BAE6FD #DC2626 #FFFFFF #0369A1 Trust blue + subscribe orange [Accent adjusted from #F97316 for WCAG 3:1]
74 73 Digital Products/Downloads #6366F1 #FFFFFF #818CF8 #0F172A #16A34A #FFFFFF #EEF2FF #312E81 #FFFFFF #312E81 #EBEFF9 #64748B #C7D2FE #DC2626 #FFFFFF #6366F1 Digital indigo + buy green [Accent adjusted from #22C55E for WCAG 3:1]
75 74 Church/Religious Organization #7C3AED #FFFFFF #A78BFA #0F172A #A16207 #FFFFFF #FAF5FF #4C1D95 #FFFFFF #4C1D95 #ECEEF9 #64748B #DDD6FE #DC2626 #FFFFFF #7C3AED Spiritual purple + warm gold [Accent adjusted from #CA8A04 for WCAG 3:1]
76 75 Sports Team/Club #DC2626 #FFFFFF #EF4444 #FFFFFF #DC2626 #FFFFFF #FEF2F2 #7F1D1D #FFFFFF #7F1D1D #F0EDF1 #64748B #FECACA #DC2626 #FFFFFF #DC2626 Team red + championship gold [Accent adjusted from #FBBF24 for WCAG 3:1]
77 76 Museum/Gallery #18181B #FFFFFF #27272A #FFFFFF #18181B #FFFFFF #FAFAFA #09090B #FFFFFF #09090B #E8ECF0 #64748B #E4E4E7 #DC2626 #FFFFFF #18181B Gallery black + white space [Accent adjusted from #F8FAFC for WCAG 3:1]
78 77 Theater/Cinema #1E1B4B #FFFFFF #312E81 #FFFFFF #CA8A04 #0F172A #0F0F23 #F8FAFC #1B1B30 #F8FAFC #27273B #94A3B8 #4338CA #EF4444 #FFFFFF #1E1B4B Dramatic dark + spotlight gold
79 78 Language Learning App #4F46E5 #FFFFFF #818CF8 #0F172A #16A34A #FFFFFF #EEF2FF #312E81 #FFFFFF #312E81 #EBEEF8 #64748B #C7D2FE #DC2626 #FFFFFF #4F46E5 Learning indigo + progress green [Accent adjusted from #22C55E for WCAG 3:1]
80 79 Coding Bootcamp #0F172A #FFFFFF #1E293B #FFFFFF #22C55E #0F172A #020617 #F8FAFC #0E1223 #F8FAFC #1A1E2F #94A3B8 #334155 #EF4444 #FFFFFF #0F172A Terminal dark + success green
81 80 Cybersecurity Platform #00FF41 #0F172A #0D0D0D #FFFFFF #FF3333 #FFFFFF #000000 #E0E0E0 #0C130E #E0E0E0 #181818 #94A3B8 #1F1F1F #EF4444 #FFFFFF #00FF41 Matrix green + alert red
82 81 Developer Tool / IDE #1E293B #FFFFFF #334155 #FFFFFF #22C55E #0F172A #0F172A #F8FAFC #1B2336 #F8FAFC #272F42 #94A3B8 #475569 #EF4444 #FFFFFF #1E293B Code dark + run green
83 82 Biotech / Life Sciences #0EA5E9 #0F172A #0284C7 #FFFFFF #059669 #FFFFFF #F0F9FF #0C4A6E #FFFFFF #0C4A6E #E8F2F8 #64748B #BAE6FD #DC2626 #FFFFFF #0EA5E9 DNA blue + life green [Accent adjusted from #10B981 for WCAG 3:1]
84 83 Space Tech / Aerospace #F8FAFC #0F172A #94A3B8 #0F172A #3B82F6 #FFFFFF #0B0B10 #F8FAFC #1E1E23 #F8FAFC #232328 #94A3B8 #1E293B #EF4444 #FFFFFF #F8FAFC Star white + launch blue
85 84 Architecture / Interior #171717 #FFFFFF #404040 #FFFFFF #A16207 #FFFFFF #FFFFFF #171717 #FFFFFF #171717 #E8ECF0 #64748B #E5E5E5 #DC2626 #FFFFFF #171717 Minimal black + accent gold [Accent adjusted from #D4AF37 for WCAG 3:1]
86 85 Quantum Computing Interface #00FFFF #0F172A #7B61FF #FFFFFF #FF00FF #FFFFFF #050510 #E0E0FF #101823 #E0E0FF #1D1D28 #94A3B8 #333344 #EF4444 #FFFFFF #00FFFF Quantum cyan + interference purple
87 86 Biohacking / Longevity App #FF4D4D #FFFFFF #4D94FF #FFFFFF #059669 #FFFFFF #F5F5F7 #1C1C1E #FFFFFF #1C1C1E #F2EEF2 #64748B #E5E5EA #DC2626 #FFFFFF #FF4D4D Bio red/blue + vitality green [Accent adjusted from #00E676 for WCAG 3:1]
88 87 Autonomous Drone Fleet Manager #00FF41 #0F172A #008F11 #FFFFFF #FF3333 #FFFFFF #0D1117 #E6EDF3 #182424 #E6EDF3 #25292F #94A3B8 #30363D #EF4444 #FFFFFF #00FF41 Terminal green + alert red
89 88 Generative Art Platform #18181B #FFFFFF #3F3F46 #FFFFFF #EC4899 #FFFFFF #FAFAFA #09090B #FFFFFF #09090B #E8ECF0 #64748B #E4E4E7 #DC2626 #FFFFFF #18181B Canvas neutral + creative pink
90 89 Spatial Computing OS / App #FFFFFF #0F172A #E5E5E5 #0F172A #FFFFFF #0F172A #888888 #000000 #999999 #000000 #777777 #D4D4D4 #CCCCCC #FF3B30 #FFFFFF #007AFF Glass white + system blue [Accent adjusted from #007AFF for WCAG 3:1]
91 90 Sustainable Energy / Climate Tech #059669 #FFFFFF #10B981 #0F172A #059669 #FFFFFF #ECFDF5 #064E3B #FFFFFF #064E3B #E8F1F3 #64748B #A7F3D0 #DC2626 #FFFFFF #059669 Nature green + solar gold [Accent adjusted from #FBBF24 for WCAG 3:1]
92 91 Personal Finance Tracker #1E40AF #FFFFFF #3B82F6 #FFFFFF #059669 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #101A34 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #1E40AF Trust blue + profit green on dark
93 92 Chat & Messaging App #2563EB #FFFFFF #6366F1 #FFFFFF #059669 #FFFFFF #FFFFFF #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Messenger blue + online green
94 93 Notes & Writing App #78716C #FFFFFF #A8A29E #FFFFFF #D97706 #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #F6F6F6 #64748B #EEEDED #DC2626 #FFFFFF #78716C Warm ink + amber accent on cream
95 94 Habit Tracker #D97706 #FFFFFF #F59E0B #0F172A #059669 #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #FCF6F0 #64748B #FAEEE1 #DC2626 #FFFFFF #D97706 Streak amber + habit green
96 95 Food Delivery / On-Demand #EA580C #FFFFFF #F97316 #FFFFFF #2563EB #FFFFFF #FFF7ED #0F172A #FFFFFF #0F172A #FDF4F0 #64748B #FCEAE1 #DC2626 #FFFFFF #EA580C Appetizing orange + trust blue
97 96 Ride Hailing / Transportation #1E293B #FFFFFF #334155 #FFFFFF #2563EB #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #10182B #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #1E293B Map dark + route blue
98 97 Recipe & Cooking App #9A3412 #FFFFFF #C2410C #FFFFFF #059669 #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #F8F2F0 #64748B #F2E6E2 #DC2626 #FFFFFF #9A3412 Warm terracotta + fresh green
99 98 Meditation & Mindfulness #7C3AED #FFFFFF #8B5CF6 #FFFFFF #059669 #FFFFFF #FAF5FF #0F172A #FFFFFF #0F172A #F7F3FD #64748B #EFE7FC #DC2626 #FFFFFF #7C3AED Calm lavender + mindful green
100 99 Weather App #0284C7 #FFFFFF #0EA5E9 #FFFFFF #F59E0B #0F172A #F0F9FF #0F172A #FFFFFF #0F172A #EFF7FB #64748B #E0F0F8 #DC2626 #FFFFFF #0284C7 Sky blue + sun amber
101 100 Diary & Journal App #92400E #FFFFFF #A16207 #FFFFFF #6366F1 #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #F8F3F0 #64748B #F1E8E2 #DC2626 #FFFFFF #92400E Warm journal brown + ink violet
102 101 CRM & Client Management #2563EB #FFFFFF #3B82F6 #FFFFFF #059669 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Professional blue + deal green
103 102 Inventory & Stock Management #334155 #FFFFFF #475569 #FFFFFF #059669 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F2F3F4 #64748B #E6E8EA #DC2626 #FFFFFF #334155 Industrial slate + stock green
104 103 Flashcard & Study Tool #7C3AED #FFFFFF #8B5CF6 #FFFFFF #059669 #FFFFFF #FAF5FF #0F172A #FFFFFF #0F172A #F7F3FD #64748B #EFE7FC #DC2626 #FFFFFF #7C3AED Study purple + correct green
105 104 Booking & Appointment App #0284C7 #FFFFFF #0EA5E9 #FFFFFF #059669 #FFFFFF #F0F9FF #0F172A #FFFFFF #0F172A #EFF7FB #64748B #E0F0F8 #DC2626 #FFFFFF #0284C7 Calendar blue + available green
106 105 Invoice & Billing Tool #1E3A5F #FFFFFF #2563EB #FFFFFF #059669 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F3F5 #64748B #E4E7EB #DC2626 #FFFFFF #1E3A5F Navy professional + paid green
107 106 Grocery & Shopping List #059669 #FFFFFF #10B981 #FFFFFF #D97706 #FFFFFF #ECFDF5 #0F172A #FFFFFF #0F172A #F0F8F6 #64748B #E1F2ED #DC2626 #FFFFFF #059669 Fresh green + food amber
108 107 Timer & Pomodoro #DC2626 #FFFFFF #EF4444 #FFFFFF #059669 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #1F1829 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #DC2626 Focus red on dark + break green
109 108 Parenting & Baby Tracker #EC4899 #FFFFFF #F472B6 #FFFFFF #0284C7 #FFFFFF #FDF2F8 #0F172A #FFFFFF #0F172A #FDF4F8 #64748B #FCE9F2 #DC2626 #FFFFFF #EC4899 Soft pink + trust blue
110 109 Scanner & Document Manager #1E293B #FFFFFF #334155 #FFFFFF #2563EB #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F2F3 #64748B #E4E5E7 #DC2626 #FFFFFF #1E293B Document grey + scan blue
111 110 Calendar & Scheduling App #2563EB #FFFFFF #3B82F6 #FFFFFF #059669 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Calendar blue + event green
112 111 Password Manager #1E3A5F #FFFFFF #334155 #FFFFFF #059669 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #10192E #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #1E3A5F Vault dark blue + secure green
113 112 Expense Splitter / Bill Split #059669 #FFFFFF #10B981 #FFFFFF #DC2626 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F0F8F6 #64748B #E1F2ED #DC2626 #FFFFFF #059669 Balance green + owe red
114 113 Voice Recorder & Memo #DC2626 #FFFFFF #EF4444 #FFFFFF #2563EB #FFFFFF #FFFFFF #0F172A #FFFFFF #0F172A #FCF1F1 #64748B #FAE4E4 #DC2626 #FFFFFF #DC2626 Recording red + waveform blue
115 114 Bookmark & Read-Later #D97706 #FFFFFF #F59E0B #0F172A #2563EB #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #FCF6F0 #64748B #FAEEE1 #DC2626 #FFFFFF #D97706 Warm amber + link blue
116 115 Translator App #2563EB #FFFFFF #0891B2 #FFFFFF #EA580C #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Global blue + teal + accent orange
117 116 Calculator & Unit Converter #EA580C #FFFFFF #F97316 #FFFFFF #2563EB #FFFFFF #1C1917 #FFFFFF #262321 #FFFFFF #2C1E16 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #EA580C Operation orange on dark
118 117 Alarm & World Clock #D97706 #FFFFFF #F59E0B #0F172A #6366F1 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #1F1E27 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #D97706 Time amber + night indigo on dark
119 118 File Manager & Transfer #2563EB #FFFFFF #3B82F6 #FFFFFF #D97706 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Folder blue + file amber
120 119 Email Client #2563EB #FFFFFF #3B82F6 #FFFFFF #DC2626 #FFFFFF #FFFFFF #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Inbox blue + priority red
121 120 Casual Puzzle Game #EC4899 #FFFFFF #8B5CF6 #FFFFFF #F59E0B #0F172A #FDF2F8 #0F172A #FFFFFF #0F172A #FDF4F8 #64748B #FCE9F2 #DC2626 #FFFFFF #EC4899 Cheerful pink + reward gold
122 121 Trivia & Quiz Game #2563EB #FFFFFF #7C3AED #FFFFFF #F59E0B #0F172A #EFF6FF #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Quiz blue + gold leaderboard
123 122 Card & Board Game #15803D #FFFFFF #166534 #FFFFFF #D97706 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #0F1F2B #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #15803D Felt green + gold on dark
124 123 Idle & Clicker Game #D97706 #FFFFFF #F59E0B #0F172A #7C3AED #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #FCF6F0 #64748B #FAEEE1 #DC2626 #FFFFFF #D97706 Coin gold + prestige purple
125 124 Word & Crossword Game #15803D #FFFFFF #059669 #FFFFFF #D97706 #FFFFFF #FFFFFF #0F172A #FFFFFF #0F172A #F0F7F3 #64748B #E2EFE7 #DC2626 #FFFFFF #15803D Word green + letter amber
126 125 Arcade & Retro Game #DC2626 #FFFFFF #2563EB #FFFFFF #22C55E #0F172A #0F172A #FFFFFF #192134 #FFFFFF #1F1829 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #DC2626 Neon red+blue on dark + score green
127 126 Photo Editor & Filters #7C3AED #FFFFFF #6366F1 #FFFFFF #0891B2 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #171939 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #7C3AED Editor violet + filter cyan on dark
128 127 Short Video Editor #EC4899 #FFFFFF #DB2777 #FFFFFF #2563EB #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #201A32 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #EC4899 Video pink on dark + timeline blue
129 128 Drawing & Sketching Canvas #7C3AED #FFFFFF #8B5CF6 #FFFFFF #0891B2 #FFFFFF #1C1917 #FFFFFF #262321 #FFFFFF #231B28 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #7C3AED Canvas purple + tool teal on dark
130 129 Music Creation & Beat Maker #7C3AED #FFFFFF #6366F1 #FFFFFF #22C55E #0F172A #0F172A #FFFFFF #192134 #FFFFFF #171939 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #7C3AED Studio purple + waveform green on dark
131 130 Meme & Sticker Maker #EC4899 #FFFFFF #F59E0B #0F172A #2563EB #FFFFFF #FFFFFF #0F172A #FFFFFF #0F172A #FDF4F8 #64748B #FCE9F2 #DC2626 #FFFFFF #EC4899 Viral pink + comedy yellow + share blue
132 131 AI Photo & Avatar Generator #7C3AED #FFFFFF #6366F1 #FFFFFF #EC4899 #FFFFFF #FAF5FF #0F172A #FFFFFF #0F172A #F7F3FD #64748B #EFE7FC #DC2626 #FFFFFF #7C3AED AI purple + generation pink
133 132 Link-in-Bio Page Builder #2563EB #FFFFFF #7C3AED #FFFFFF #EC4899 #FFFFFF #FFFFFF #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Brand blue + creator purple
134 133 Wardrobe & Outfit Planner #BE185D #FFFFFF #EC4899 #FFFFFF #D97706 #FFFFFF #FDF2F8 #0F172A #FFFFFF #0F172A #FBF1F5 #64748B #F7E3EB #DC2626 #FFFFFF #BE185D Fashion rose + gold accent
135 134 Plant Care Tracker #15803D #FFFFFF #059669 #FFFFFF #D97706 #FFFFFF #F0FDF4 #0F172A #FFFFFF #0F172A #F0F7F3 #64748B #E2EFE7 #DC2626 #FFFFFF #15803D Nature green + sun yellow
136 135 Book & Reading Tracker #78716C #FFFFFF #92400E #FFFFFF #D97706 #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #F6F6F6 #64748B #EEEDED #DC2626 #FFFFFF #78716C Book brown + page amber
137 136 Couple & Relationship App #BE185D #FFFFFF #EC4899 #FFFFFF #DC2626 #FFFFFF #FDF2F8 #0F172A #FFFFFF #0F172A #FBF1F5 #64748B #F7E3EB #DC2626 #FFFFFF #BE185D Romance rose + love red
138 137 Family Calendar & Chores #2563EB #FFFFFF #059669 #FFFFFF #D97706 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Family blue + chore green
139 138 Mood Tracker #7C3AED #FFFFFF #6366F1 #FFFFFF #D97706 #FFFFFF #FAF5FF #0F172A #FFFFFF #0F172A #F7F3FD #64748B #EFE7FC #DC2626 #FFFFFF #7C3AED Mood purple + insight amber
140 139 Gift & Wishlist #DC2626 #FFFFFF #D97706 #FFFFFF #EC4899 #FFFFFF #FFF1F2 #0F172A #FFFFFF #0F172A #FCF1F1 #64748B #FAE4E4 #DC2626 #FFFFFF #DC2626 Gift red + gold + surprise pink
141 140 Running & Cycling GPS #EA580C #FFFFFF #F97316 #FFFFFF #059669 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #201C27 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #EA580C Energetic orange + pace green on dark
142 141 Yoga & Stretching Guide #6B7280 #FFFFFF #78716C #FFFFFF #0891B2 #FFFFFF #F5F5F0 #0F172A #FFFFFF #0F172A #F6F6F7 #64748B #EDEEEF #DC2626 #FFFFFF #6B7280 Sage neutral + calm teal
143 142 Sleep Tracker #4338CA #FFFFFF #6366F1 #FFFFFF #7C3AED #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #131936 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #4338CA Night indigo + dream violet on dark
144 143 Calorie & Nutrition Counter #059669 #FFFFFF #10B981 #FFFFFF #EA580C #FFFFFF #ECFDF5 #0F172A #FFFFFF #0F172A #F0F8F6 #64748B #E1F2ED #DC2626 #FFFFFF #059669 Healthy green + macro orange
145 144 Period & Cycle Tracker #BE185D #FFFFFF #EC4899 #FFFFFF #7C3AED #FFFFFF #FDF2F8 #0F172A #FFFFFF #0F172A #FBF1F5 #64748B #F7E3EB #DC2626 #FFFFFF #BE185D Blush rose + fertility lavender
146 145 Medication & Pill Reminder #0284C7 #FFFFFF #0891B2 #FFFFFF #DC2626 #FFFFFF #F0F9FF #0F172A #FFFFFF #0F172A #EFF7FB #64748B #E0F0F8 #DC2626 #FFFFFF #0284C7 Medical blue + alert red
147 146 Water & Hydration Reminder #0284C7 #FFFFFF #06B6D4 #FFFFFF #0891B2 #FFFFFF #F0F9FF #0F172A #FFFFFF #0F172A #EFF7FB #64748B #E0F0F8 #DC2626 #FFFFFF #0284C7 Refreshing blue + water cyan
148 147 Fasting & Intermittent Timer #6366F1 #FFFFFF #4338CA #FFFFFF #059669 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #151D39 #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #6366F1 Fasting indigo on dark + eating green
149 148 Anonymous Community / Confession #475569 #FFFFFF #334155 #FFFFFF #0891B2 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #131B2F #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #475569 Protective grey + subtle teal on dark
150 149 Local Events & Discovery #EA580C #FFFFFF #F97316 #FFFFFF #2563EB #FFFFFF #FFF7ED #0F172A #FFFFFF #0F172A #FDF4F0 #64748B #FCEAE1 #DC2626 #FFFFFF #EA580C Event orange + map blue
151 150 Study Together / Virtual Coworking #2563EB #FFFFFF #3B82F6 #FFFFFF #059669 #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Focus blue + session green
152 151 Coding Challenge & Practice #22C55E #0F172A #059669 #FFFFFF #D97706 #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #10242E #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #22C55E Code green + difficulty amber on dark
153 152 Kids Learning (ABC & Math) #2563EB #FFFFFF #F59E0B #0F172A #EC4899 #FFFFFF #EFF6FF #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Learning blue + play yellow + fun pink
154 153 Music Instrument Learning #DC2626 #FFFFFF #9A3412 #FFFFFF #D97706 #FFFFFF #FFFBEB #0F172A #FFFFFF #0F172A #FCF1F1 #64748B #FAE4E4 #DC2626 #FFFFFF #DC2626 Musical red + warm amber
155 154 Parking Finder #2563EB #FFFFFF #059669 #FFFFFF #DC2626 #FFFFFF #F0F9FF #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Available blue/green + occupied red
156 155 Public Transit Guide #2563EB #FFFFFF #0891B2 #FFFFFF #EA580C #FFFFFF #F8FAFC #0F172A #FFFFFF #0F172A #F1F5FD #64748B #E4ECFC #DC2626 #FFFFFF #2563EB Transit blue + line colors
157 156 Road Trip Planner #EA580C #FFFFFF #0891B2 #FFFFFF #D97706 #FFFFFF #FFF7ED #0F172A #FFFFFF #0F172A #FDF4F0 #64748B #FCEAE1 #DC2626 #FFFFFF #EA580C Adventure orange + map teal
158 157 VPN & Privacy Tool #1E3A5F #FFFFFF #334155 #FFFFFF #22C55E #0F172A #0F172A #FFFFFF #192134 #FFFFFF #10192E #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #1E3A5F Shield dark + connected green
159 158 Emergency SOS & Safety #DC2626 #FFFFFF #EF4444 #FFFFFF #2563EB #FFFFFF #FFF1F2 #0F172A #FFFFFF #0F172A #FCF1F1 #64748B #FAE4E4 #DC2626 #FFFFFF #DC2626 Alert red + safety blue
160 159 Wallpaper & Theme App #7C3AED #FFFFFF #EC4899 #FFFFFF #2563EB #FFFFFF #FAF5FF #0F172A #FFFFFF #0F172A #F7F3FD #64748B #EFE7FC #DC2626 #FFFFFF #7C3AED Aesthetic purple + trending pink
161 160 White Noise & Ambient Sound #475569 #FFFFFF #334155 #FFFFFF #4338CA #FFFFFF #0F172A #FFFFFF #192134 #FFFFFF #131B2F #94A3B8 rgba(255,255,255,0.08) #DC2626 #FFFFFF #475569 Ambient grey + deep indigo on dark
162 161 Home Decoration & Interior Design #78716C #FFFFFF #A8A29E #FFFFFF #D97706 #FFFFFF #FAF5F2 #0F172A #FFFFFF #0F172A #F6F6F6 #64748B #EEEDED #DC2626 #FFFFFF #78716C Interior warm grey + gold accent

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,106 +0,0 @@
No,Category,Icon Name,Keywords,Library,Import Code,Usage,Best For,Style
1,Navigation,list,hamburger menu navigation toggle bars,Phosphor,import { List } from '@phosphor-icons/react',<List size={20} weight="regular" />,Mobile navigation drawer toggle sidebar,Outline
2,Navigation,arrow-left,back previous return navigate,Phosphor,import { ArrowLeft } from '@phosphor-icons/react',<ArrowLeft size={20} weight="regular" />,Back button breadcrumb navigation,Outline
3,Navigation,arrow-right,next forward continue navigate,Phosphor,import { ArrowRight } from '@phosphor-icons/react',<ArrowRight size={20} weight="regular" />,Forward button next step CTA,Outline
4,Navigation,caret-down,dropdown expand accordion select,Phosphor,import { CaretDown } from '@phosphor-icons/react',<CaretDown size={20} weight="regular" />,Dropdown toggle accordion header,Outline
5,Navigation,caret-up,collapse close accordion minimize,Phosphor,import { CaretUp } from '@phosphor-icons/react',<CaretUp size={20} weight="regular" />,Accordion collapse minimize,Outline
6,Navigation,house,homepage main dashboard start,Phosphor,import { House } from '@phosphor-icons/react',<House size={20} weight="regular" />,Home navigation main page,Outline
7,Navigation,x,close cancel dismiss remove exit,Phosphor,import { X } from '@phosphor-icons/react',<X size={20} weight="regular" />,Modal close dismiss button,Outline
8,Navigation,arrow-square-out,open new tab external link,Phosphor,import { ArrowSquareOut } from '@phosphor-icons/react',<ArrowSquareOut size={20} weight="regular" />,External link indicator,Outline
9,Action,plus,add create new insert,Phosphor,import { Plus } from '@phosphor-icons/react',<Plus size={20} weight="regular" />,Add button create new item,Outline
10,Action,minus,remove subtract decrease delete,Phosphor,import { Minus } from '@phosphor-icons/react',<Minus size={20} weight="regular" />,Remove item quantity decrease,Outline
11,Action,trash,delete remove discard bin,Phosphor,import { Trash } from '@phosphor-icons/react',<Trash size={20} weight="regular" />,Delete action destructive,Outline
12,Action,pencil-simple,pencil modify change update,Phosphor,import { PencilSimple } from '@phosphor-icons/react',<PencilSimple size={20} weight="regular" />,Edit button modify content,Outline
13,Action,floppy-disk,disk store persist save,Phosphor,import { FloppyDisk } from '@phosphor-icons/react',<FloppyDisk size={20} weight="regular" />,Save button persist changes,Outline
14,Action,download-simple,export save file download,Phosphor,import { DownloadSimple } from '@phosphor-icons/react',<DownloadSimple size={20} weight="regular" />,Download file export,Outline
15,Action,upload-simple,import file attach upload,Phosphor,import { UploadSimple } from '@phosphor-icons/react',<UploadSimple size={20} weight="regular" />,Upload file import,Outline
16,Action,copy,duplicate clipboard paste,Phosphor,import { Copy } from '@phosphor-icons/react',<Copy size={20} weight="regular" />,Copy to clipboard,Outline
17,Action,share,social distribute send,Phosphor,import { Share } from '@phosphor-icons/react',<Share size={20} weight="regular" />,Share button social,Outline
18,Action,magnifying-glass,find lookup filter query,Phosphor,import { MagnifyingGlass } from '@phosphor-icons/react',<MagnifyingGlass size={20} weight="regular" />,Search input bar,Outline
19,Action,funnel,sort refine narrow options,Phosphor,import { Funnel } from '@phosphor-icons/react',<Funnel size={20} weight="regular" />,Filter dropdown sort,Outline
20,Action,gear,gear cog preferences config,Phosphor,import { Gear } from '@phosphor-icons/react',<Gear size={20} weight="regular" />,Settings page configuration,Outline
21,Status,check,success done complete verified,Phosphor,import { Check } from '@phosphor-icons/react',<Check size={20} weight="regular" />,Success state checkmark,Outline
22,Status,check-circle,success verified approved complete,Phosphor,import { CheckCircle } from '@phosphor-icons/react',<CheckCircle size={20} weight="regular" />,Success badge verified,Outline
23,Status,x-circle,error failed cancel rejected,Phosphor,import { XCircle } from '@phosphor-icons/react',<XCircle size={20} weight="regular" />,Error state failed,Outline
24,Status,warning,warning caution attention danger,Phosphor,import { Warning } from '@phosphor-icons/react',<Warning size={20} weight="regular" />,Warning message caution,Outline
25,Status,warning-circle,info notice information help,Phosphor,import { WarningCircle } from '@phosphor-icons/react',<WarningCircle size={20} weight="regular" />,Info notice alert,Outline
26,Status,info,information help tooltip details,Phosphor,import { Info } from '@phosphor-icons/react',<Info size={20} weight="regular" />,Information tooltip help,Outline
27,Status,circle-notch,loading spinner processing wait,Phosphor,import { CircleNotch } from '@phosphor-icons/react',<CircleNotch size={20} weight="regular" className="animate-spin" />,Loading state spinner,Outline
28,Status,clock,time schedule pending wait,Phosphor,import { Clock } from '@phosphor-icons/react',<Clock size={20} weight="regular" />,Pending time schedule,Outline
29,Communication,envelope,email message inbox letter,Phosphor,import { Envelope } from '@phosphor-icons/react',<Envelope size={20} weight="regular" />,Email contact inbox,Outline
30,Communication,chat-circle,chat comment bubble conversation,Phosphor,import { ChatCircle } from '@phosphor-icons/react',<ChatCircle size={20} weight="regular" />,Chat comment message,Outline
31,Communication,phone,call mobile telephone contact,Phosphor,import { Phone } from '@phosphor-icons/react',<Phone size={20} weight="regular" />,Phone contact call,Outline
32,Communication,paper-plane-tilt,submit dispatch message airplane,Phosphor,import { PaperPlaneTilt } from '@phosphor-icons/react',<PaperPlaneTilt size={20} weight="regular" />,Send message submit,Outline
33,Communication,bell,notification alert ring reminder,Phosphor,import { Bell } from '@phosphor-icons/react',<Bell size={20} weight="regular" />,Notification bell alert,Outline
34,User,user,profile account person avatar,Phosphor,import { User } from '@phosphor-icons/react',<User size={20} weight="regular" />,User profile account,Outline
35,User,users,team group people members,Phosphor,import { Users } from '@phosphor-icons/react',<Users size={20} weight="regular" />,Team group members,Outline
36,User,user-plus,add invite new member,Phosphor,import { UserPlus } from '@phosphor-icons/react',<UserPlus size={20} weight="regular" />,Add user invite,Outline
37,User,sign-in,signin authenticate enter,Phosphor,import { SignIn } from '@phosphor-icons/react',<SignIn size={20} weight="regular" />,Login signin,Outline
38,User,sign-out,signout exit leave logout,Phosphor,import { SignOut } from '@phosphor-icons/react',<SignOut size={20} weight="regular" />,Logout signout,Outline
39,Media,image,photo picture gallery thumbnail,Phosphor,import { Image } from '@phosphor-icons/react',<Image size={20} weight="regular" />,Image photo gallery,Outline
40,Media,video,movie film play record,Phosphor,import { Video } from '@phosphor-icons/react',<Video size={20} weight="regular" />,Video player media,Outline
41,Media,play,start video audio media,Phosphor,import { Play } from '@phosphor-icons/react',<Play size={20} weight="regular" />,Play button video audio,Outline
42,Media,pause,stop halt video audio,Phosphor,import { Pause } from '@phosphor-icons/react',<Pause size={20} weight="regular" />,Pause button media,Outline
43,Media,speaker-high,sound audio speaker music,Phosphor,import { SpeakerHigh } from '@phosphor-icons/react',<SpeakerHigh size={20} weight="regular" />,Volume audio sound,Outline
44,Media,microphone,microphone record voice audio,Phosphor,import { Microphone } from '@phosphor-icons/react',<Microphone size={20} weight="regular" />,Microphone voice record,Outline
45,Media,camera,photo capture snapshot picture,Phosphor,import { Camera } from '@phosphor-icons/react',<Camera size={20} weight="regular" />,Camera photo capture,Outline
46,Commerce,shopping-cart,cart checkout basket buy,Phosphor,import { ShoppingCart } from '@phosphor-icons/react',<ShoppingCart size={20} weight="regular" />,Shopping cart e-commerce,Outline
47,Commerce,shopping-bag,purchase buy store bag,Phosphor,import { ShoppingBag } from '@phosphor-icons/react',<ShoppingBag size={20} weight="regular" />,Shopping bag purchase,Outline
48,Commerce,credit-card,payment card checkout stripe,Phosphor,import { CreditCard } from '@phosphor-icons/react',<CreditCard size={20} weight="regular" />,Payment credit card,Outline
49,Commerce,currency-dollar,money price currency cost,Phosphor,import { CurrencyDollar } from '@phosphor-icons/react',<CurrencyDollar size={20} weight="regular" />,Price money currency,Outline
50,Commerce,tag,label price discount sale,Phosphor,import { Tag } from '@phosphor-icons/react',<Tag size={20} weight="regular" />,Price tag label,Outline
51,Commerce,gift,present reward bonus offer,Phosphor,import { Gift } from '@phosphor-icons/react',<Gift size={20} weight="regular" />,Gift reward offer,Outline
52,Commerce,percent,discount sale offer promo,Phosphor,import { Percent } from '@phosphor-icons/react',<Percent size={20} weight="regular" />,Discount percentage sale,Outline
53,Data,chart-bar,analytics statistics graph metrics,Phosphor,import { ChartBar } from '@phosphor-icons/react',<ChartBar size={20} weight="regular" />,Bar chart analytics,Outline
54,Data,chart-pie,statistics distribution breakdown,Phosphor,import { ChartPie } from '@phosphor-icons/react',<ChartPie size={20} weight="regular" />,Pie chart distribution,Outline
55,Data,trend-up,growth increase positive trend,Phosphor,import { TrendUp } from '@phosphor-icons/react',<TrendUp size={20} weight="regular" />,Growth trend positive,Outline
56,Data,trend-down,decline decrease negative trend,Phosphor,import { TrendDown } from '@phosphor-icons/react',<TrendDown size={20} weight="regular" />,Decline trend negative,Outline
57,Data,activity,pulse heartbeat monitor live,Phosphor,import { Activity } from '@phosphor-icons/react',<Activity size={20} weight="regular" />,Activity monitor pulse,Outline
58,Data,database,storage server data backend,Phosphor,import { Database } from '@phosphor-icons/react',<Database size={20} weight="regular" />,Database storage,Outline
59,Files,file,document page paper doc,Phosphor,import { File } from '@phosphor-icons/react',<File size={20} weight="regular" />,File document,Outline
60,Files,file-text,document text page article,Phosphor,import { FileText } from '@phosphor-icons/react',<FileText size={20} weight="regular" />,Text document article,Outline
61,Files,folder,directory organize group files,Phosphor,import { Folder } from '@phosphor-icons/react',<Folder size={20} weight="regular" />,Folder directory,Outline
62,Files,folder-open,expanded browse files view,Phosphor,import { FolderOpen } from '@phosphor-icons/react',<FolderOpen size={20} weight="regular" />,Open folder browse,Outline
63,Files,paperclip,attachment attach file link,Phosphor,import { Paperclip } from '@phosphor-icons/react',<Paperclip size={20} weight="regular" />,Attachment paperclip,Outline
64,Files,link,url hyperlink chain connect,Phosphor,import { Link } from '@phosphor-icons/react',<Link size={20} weight="regular" />,Link URL hyperlink,Outline
65,Files,clipboard,paste copy buffer notes,Phosphor,import { Clipboard } from '@phosphor-icons/react',<Clipboard size={20} weight="regular" />,Clipboard paste,Outline
66,Layout,grid-four,tiles gallery layout dashboard,Phosphor,import { GridFour } from '@phosphor-icons/react',<GridFour size={20} weight="regular" />,Grid layout gallery,Outline
67,Layout,list-bullets,rows table lines items,Phosphor,import { ListBullets } from '@phosphor-icons/react',<ListBullets size={20} weight="regular" />,List view rows,Outline
68,Layout,columns,layout split dual sidebar,Phosphor,import { Columns } from '@phosphor-icons/react',<Columns size={20} weight="regular" />,Column layout split,Outline
69,Layout,arrows-out,fullscreen expand enlarge zoom,Phosphor,import { ArrowsOut } from '@phosphor-icons/react',<ArrowsOut size={20} weight="regular" />,Fullscreen maximize,Outline
70,Layout,arrows-in,reduce shrink collapse exit,Phosphor,import { ArrowsIn } from '@phosphor-icons/react',<ArrowsIn size={20} weight="regular" />,Minimize reduce,Outline
71,Layout,sidebar,panel drawer navigation menu,Phosphor,import { Sidebar } from '@phosphor-icons/react',<Sidebar size={20} weight="regular" />,Sidebar panel,Outline
72,Social,heart,like love favorite wishlist,Phosphor,import { Heart } from '@phosphor-icons/react',<Heart size={20} weight="regular" />,Like favorite love,Outline
73,Social,star,rating review favorite bookmark,Phosphor,import { Star } from '@phosphor-icons/react',<Star size={20} weight="regular" />,Star rating favorite,Outline
74,Social,thumbs-up,like approve agree positive,Phosphor,import { ThumbsUp } from '@phosphor-icons/react',<ThumbsUp size={20} weight="regular" />,Like approve thumb,Outline
75,Social,thumbs-down,dislike disapprove disagree negative,Phosphor,import { ThumbsDown } from '@phosphor-icons/react',<ThumbsDown size={20} weight="regular" />,Dislike disapprove,Outline
76,Social,bookmark,save later favorite mark,Phosphor,import { Bookmark } from '@phosphor-icons/react',<Bookmark size={20} weight="regular" />,Bookmark save,Outline
77,Social,flag,report mark important highlight,Phosphor,import { Flag } from '@phosphor-icons/react',<Flag size={20} weight="regular" />,Flag report,Outline
78,Device,device-mobile,mobile phone device touch,Phosphor,import { DeviceMobile } from '@phosphor-icons/react',<DeviceMobile size={20} weight="regular" />,Mobile smartphone,Outline
79,Device,device-tablet,ipad device touch screen,Phosphor,import { DeviceTablet } from '@phosphor-icons/react',<DeviceTablet size={20} weight="regular" />,Tablet device,Outline
80,Device,monitor,desktop screen computer display,Phosphor,import { Monitor } from '@phosphor-icons/react',<Monitor size={20} weight="regular" />,Desktop monitor,Outline
81,Device,laptop,notebook computer portable device,Phosphor,import { Laptop } from '@phosphor-icons/react',<Laptop size={20} weight="regular" />,Laptop computer,Outline
82,Device,printer,print document output paper,Phosphor,import { Printer } from '@phosphor-icons/react',<Printer size={20} weight="regular" />,Printer print,Outline
83,Security,lock,secure password protected private,Phosphor,import { Lock } from '@phosphor-icons/react',<Lock size={20} weight="regular" />,Lock secure,Outline
84,Security,lock-open,open access unsecure public,Phosphor,import { LockOpen } from '@phosphor-icons/react',<LockOpen size={20} weight="regular" />,Unlock open,Outline
85,Security,shield,protection security safe guard,Phosphor,import { Shield } from '@phosphor-icons/react',<Shield size={20} weight="regular" />,Shield protection,Outline
86,Security,key,password access unlock login,Phosphor,import { Key } from '@phosphor-icons/react',<Key size={20} weight="regular" />,Key password,Outline
87,Security,eye,view show visible password,Phosphor,import { Eye } from '@phosphor-icons/react',<Eye size={20} weight="regular" />,Show password view,Outline
88,Security,eye-slash,hide invisible password hidden,Phosphor,import { EyeSlash } from '@phosphor-icons/react',<EyeSlash size={20} weight="regular" />,Hide password,Outline
89,Location,map-pin,location marker place address,Phosphor,import { MapPin } from '@phosphor-icons/react',<MapPin size={20} weight="regular" />,Location pin marker,Outline
90,Location,map,directions navigate geography location,Phosphor,import { Map } from '@phosphor-icons/react',<Map size={20} weight="regular" />,Map directions,Outline
91,Location,compass,compass direction pointer arrow,Phosphor,import { Compass } from '@phosphor-icons/react',<Compass size={20} weight="regular" />,Navigation compass,Outline
92,Location,globe,world international global web,Phosphor,import { Globe } from '@phosphor-icons/react',<Globe size={20} weight="regular" />,Globe world,Outline
93,Time,calendar,date schedule event appointment,Phosphor,import { Calendar } from '@phosphor-icons/react',<Calendar size={20} weight="regular" />,Calendar date,Outline
94,Time,arrows-clockwise,reload sync update refresh,Phosphor,import { ArrowsClockwise } from '@phosphor-icons/react',<ArrowsClockwise size={20} weight="regular" />,Refresh reload,Outline
95,Time,arrow-counter-clockwise,undo back revert history,Phosphor,import { ArrowCounterClockwise } from '@phosphor-icons/react',<ArrowCounterClockwise size={20} weight="regular" />,Undo revert,Outline
96,Time,arrow-clockwise,redo forward repeat history,Phosphor,import { ArrowClockwise } from '@phosphor-icons/react',<ArrowClockwise size={20} weight="regular" />,Redo forward,Outline
97,Development,code,develop programming syntax html,Phosphor,import { Code } from '@phosphor-icons/react',<Code size={20} weight="regular" />,Code development,Outline
98,Development,terminal,console cli command shell,Phosphor,import { Terminal } from '@phosphor-icons/react',<Terminal size={20} weight="regular" />,Terminal console,Outline
99,Development,git-branch,version control branch merge,Phosphor,import { GitBranch } from '@phosphor-icons/react',<GitBranch size={20} weight="regular" />,Git branch,Outline
100,Development,github-logo,repository code open source,Phosphor,import { GithubLogo } from '@phosphor-icons/react',<GithubLogo size={20} weight="regular" />,GitHub repository,Outline
101,Style Config,bold-typography-icon-system,"bold typography, editorial, mono label, phosphor, weight regular, minimal, icon+label required, size 2032",Phosphor (react-native),"import { ArrowRight } from 'phosphor-react-native'","<ArrowRight size={20} weight=""regular"" color={colors.accent} />","Bold Typography Mobile style: weight=""regular"". Size 20px for UI controls, 32px for feature anchors. Icons MUST be paired with a Mono-stack text label (JetBrains Mono). Standalone icons only allowed for standard navigation (e.g., Back arrow). Accent color #FF3D00 only.",Outline
102,Style Config,cyberpunk-icon-system,"cyberpunk, neon, glow, hud, phosphor, weight regular, accent glow, dark, angular, react native",Phosphor (react-native),"import { Lightning } from 'phosphor-react-native'","<Lightning size={24} weight=""regular"" color={colors.accent} />","Cyberpunk Mobile HUD style: weight=""regular"", color={colors.accent} (#00FF88 Matrix Green). Wrap every icon in a View with shadowColor: colors.accent / shadowOpacity: 0.6 / shadowRadius: 8 to simulate neon glow. Use borderRadius: 0 on wrapper. Avoid rounded icon containers. Always pair icon with data label in JetBrains Mono.",Outline
103,Style Config,academia-icon-system,"academia, library, brass, ornate, phosphor, weight thin, muted warm, scholarly, mobile",Phosphor (react-native),"import { BookOpen } from 'phosphor-react-native'","<BookOpen size={22} weight=""thin"" color={colors.brass} />","Academia (Scholarly Mobile) style: weight=""thin"" (thin engraved feel), color={colors.brass} (#C9A962). No sharp geometric or tech-inspired icons. Prefer book, scroll, key, quill-type icon metaphors. Wrap in circular View with 1px brass border. Avoid neon or saturated colored icons. All icon-only navigation must have an accessibilityLabel.",Outline
104,Style Config,web3-bitcoin-icon-system,"web3, bitcoin, defi, crypto, neon orange, holographic, blurview, phosphor, glow, fintech mobile",Phosphor (react-native),"import { TrendUp } from 'phosphor-react-native'","<TrendUp size={24} weight=""regular"" color={colors.bitcoinOrange} />","Bitcoin DeFi Mobile style: weight=""regular"", color={colors.bitcoinOrange} (#F7931A). Wrap icons in circular BlurView (intensity: 20) with 1px borderColor: '#F7931A' border (Holographic Node effect). shadowColor: '#F7931A' / shadowOpacity: 0.4 / shadowRadius: 8. Prefer finance/data icons (TrendUp, Wallet, Shield, Layers). All data icons use JetBrains Mono label.",Outline
105,Guideline,icon-fallback-rules,"icon fallback, phosphor, heroicons, any icon, extended set","Phosphor (primary) + Heroicons (fallback)","Primary: import { IconName } from '@phosphor-icons/react'. Fallback: import { IconName } from '@heroicons/react/24/outline' or '@heroicons/react/24/solid'.","当默认列表中没有合适图标时:优先继续从 Phosphor 中选择任何语义更贴切的图标(不必局限于本表列出的图标)。若 Phosphor 也无合适图标,可以改用 Heroicons并在 UI 代码中保持风格统一(线性或填充、圆角程度、笔画粗细等)。","Icon library strategy and fallback rules",Outline
Can't render this file because it contains an unexpected character in line 2 and column 134.

View file

@ -1,35 +0,0 @@
No,Pattern Name,Keywords,Section Order,Primary CTA Placement,Color Strategy,Recommended Effects,Conversion Optimization
1,Hero + Features + CTA,"hero, hero-centric, hero-centric design, features, feature-rich, feature-rich showcase, cta, call-to-action","1. Hero with headline/image, 2. Value prop, 3. Key features (3-5), 4. CTA section, 5. Footer",Hero (sticky) + Bottom,Hero: Brand primary or vibrant. Features: Card bg #FAFAFA. CTA: Contrasting accent color,"Hero parallax, feature card hover lift, CTA glow on hover",Deep CTA placement. Use contrasting color (at least 7:1 contrast ratio). Sticky navbar CTA.
2,Hero + Testimonials + CTA,"hero, testimonials, social-proof, social-proof-focused, social proof focused, trust, reviews, cta","1. Hero, 2. Problem statement, 3. Solution overview, 4. Testimonials carousel, 5. CTA",Hero (sticky) + Post-testimonials,"Hero: Brand color. Testimonials: Light bg #F5F5F5. Quotes: Italic, muted color #666. CTA: Vibrant","Testimonial carousel slide animations, quote marks animations, avatar fade-in",Social proof before CTA. Use 3-5 testimonials. Include photo + name + role. CTA after social proof.
3,Product Demo + Features,"demo, product-demo, features, showcase, interactive, interactive-product-demo, interactive product demo","1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA",Video center + CTA right/bottom,Video surround: Brand color overlay. Features: Icon color #0080FF. Text: Dark #222,"Video play button pulse, feature scroll reveals, demo interaction highlights",Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted.
4,Minimal Single Column,"minimal, simple, direct, minimal & direct, minimal-direct, single-column, clean","1. Hero headline, 2. Short description, 3. Benefit bullets (3 max), 4. CTA, 5. Footer","Center, large CTA button",Minimalist: Brand + white #FFFFFF + accent. Buttons: High contrast 7:1+. Text: Black/Dark grey,Minimal hover effects. Smooth scroll. CTA scale on hover (subtle),Single CTA focus. Large typography. Lots of whitespace. No nav clutter. Mobile-first.
5,Funnel (3-Step Conversion),"funnel, conversion, conversion-optimized, conversion optimized, steps, wizard, onboarding","1. Hero, 2. Step 1 (problem), 3. Step 2 (solution), 4. Step 3 (action), 5. CTA progression",Each step: mini-CTA. Final: main CTA,"Step colors: 1 (Red/Problem), 2 (Orange/Process), 3 (Green/Solution). CTA: Brand color","Step number animations, progress bar fill, step transitions smooth scroll",Progressive disclosure. Show only essential info per step. Use progress indicators. Multiple CTAs.
6,Comparison Table + CTA,"comparison, table, compare, versus, cta","1. Hero, 2. Problem intro, 3. Comparison table (product vs competitors), 4. Pricing (optional), 5. CTA",Table: Right column. CTA: Below table,Table: Alternating rows (white/light grey). Your product: Highlight #FFFACD (light yellow) or green. Text: Dark,"Table row hover highlight, price toggle animations, feature checkmark animations",Use comparison to show unique value. Highlight your product row. Include 'free trial' in pricing row.
7,Lead Magnet + Form,"lead, form, signup, capture, email, magnet","1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit",Form CTA: Submit button,Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color,"Form focus state animations, input validation animations, success confirmation animation",Form fields ≤ 3 for best conversion. Offer valuable lead magnet preview. Show form submission progress.
8,Pricing Page + CTA,"pricing, plans, tiers, comparison, cta","1. Hero (pricing headline), 2. Price comparison cards, 3. Feature comparison table, 4. FAQ section, 5. Final CTA",Each card: CTA button. Sticky CTA in nav,"Free: Grey, Starter: Blue, Pro: Green/Gold, Enterprise: Dark. Cards: 1px border, shadow","Price toggle animation (monthly/yearly), card comparison highlight, FAQ accordion open/close",Recommend starter plan (pre-select/highlight). Show annual discount (20-30%). Use FAQs to address concerns.
9,Video-First Hero,"video, hero, media, visual, engaging","1. Hero with video background, 2. Key features overlay, 3. Benefits section, 4. CTA",Overlay on video (center/bottom) + Bottom section,Dark overlay 60% on video. Brand accent for CTA. White text on dark.,"Video autoplay muted, parallax scroll, text fade-in on scroll",86% higher engagement with video. Add captions for accessibility. Compress video for performance.
10,Scroll-Triggered Storytelling,"storytelling, scroll, narrative, story, immersive","1. Intro hook, 2. Chapter 1 (problem), 3. Chapter 2 (journey), 4. Chapter 3 (solution), 5. Climax CTA",End of each chapter (mini) + Final climax CTA,Progressive reveal. Each chapter has distinct color. Building intensity.,"ScrollTrigger animations, parallax layers, progressive disclosure, chapter transitions",Narrative increases time-on-page 3x. Use progress indicator. Mobile: simplify animations.
11,AI Personalization Landing,"ai, personalization, smart, recommendation, dynamic","1. Dynamic hero (personalized), 2. Relevant features, 3. Tailored testimonials, 4. Smart CTA",Context-aware placement based on user segment,Adaptive based on user data. A/B test color variations per segment.,"Dynamic content swap, fade transitions, personalized product recommendations",20%+ conversion with personalization. Requires analytics integration. Fallback for new users.
12,Waitlist/Coming Soon,"waitlist, coming-soon, launch, early-access, notify","1. Hero with countdown, 2. Product teaser/preview, 3. Email capture form, 4. Social proof (waitlist count)",Email form prominent (above fold) + Sticky form on scroll,Anticipation: Dark + accent highlights. Countdown in brand color. Urgency indicators.,"Countdown timer animation, email validation feedback, success confetti, social share buttons",Scarcity + exclusivity. Show waitlist count. Early access benefits. Referral program.
13,Comparison Table Focus,"comparison, table, versus, compare, features","1. Hero (problem statement), 2. Comparison matrix (you vs competitors), 3. Feature deep-dive, 4. Winner CTA",After comparison table (highlighted row) + Bottom,Your product column highlighted (accent bg or green). Competitors neutral. Checkmarks green.,"Table row hover highlight, feature checkmark animations, sticky comparison header",Show value vs competitors. 35% higher conversion. Be factual. Include pricing if favorable.
14,Pricing-Focused Landing,"pricing, price, cost, plans, subscription","1. Hero (value proposition), 2. Pricing cards (3 tiers), 3. Feature comparison, 4. FAQ, 5. Final CTA",Each pricing card + Sticky CTA in nav + Bottom,Popular plan highlighted (brand color border/bg). Free: grey. Enterprise: dark/premium.,"Price toggle monthly/annual animation, card hover lift, FAQ accordion smooth open",Annual discount 20-30%. Recommend mid-tier (most popular badge). Address objections in FAQ.
15,App Store Style Landing,"app, mobile, download, store, install","1. Hero with device mockup, 2. Screenshots carousel, 3. Features with icons, 4. Reviews/ratings, 5. Download CTAs",Download buttons prominent (App Store + Play Store) throughout,Dark/light matching app store feel. Star ratings in gold. Screenshots with device frames.,"Device mockup rotations, screenshot slider, star rating animations, download button pulse",Show real screenshots. Include ratings (4.5+ stars). QR code for mobile. Platform-specific CTAs.
16,FAQ/Documentation Landing,"faq, documentation, help, support, questions, faq/documentation, knowledge base","1. Hero with search bar, 2. Popular categories, 3. FAQ accordion, 4. Contact/support CTA",Search bar prominent + Contact CTA for unresolved questions,"Clean, high readability. Minimal color. Category icons in brand color. Success green for resolved.","Search autocomplete, smooth accordion open/close, category hover, helpful feedback buttons",Reduce support tickets. Track search analytics. Show related articles. Contact escalation path.
17,Immersive/Interactive Experience,"immersive, interactive, experience, 3d, animation, immersive/interactive experience","1. Full-screen interactive element, 2. Guided product tour, 3. Key benefits revealed, 4. CTA after completion",After interaction complete + Skip option for impatient users,Immersive experience colors. Dark background for focus. Highlight interactive elements.,"WebGL, 3D interactions, gamification elements, progress indicators, reward animations",40% higher engagement. Performance trade-off. Provide skip option. Mobile fallback essential.
18,Event/Conference Landing,"event, conference, meetup, registration, schedule, hero-centric design, hero-centric","1. Hero (date/location/countdown), 2. Speakers grid, 3. Agenda/schedule, 4. Sponsors, 5. Register CTA",Register CTA sticky + After speakers + Bottom,Urgency colors (countdown). Event branding. Speaker cards professional. Sponsor logos neutral.,"Countdown timer, speaker hover cards with bio, agenda tabs, early bird countdown",Early bird pricing with deadline. Social proof (past attendees). Speaker credibility. Multi-ticket discounts.
19,Product Review/Ratings Focused,"reviews, ratings, testimonials, social-proof, social-proof-focused, stars","1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA",After reviews summary + Buy button alongside reviews,Trust colors. Star ratings gold. Verified badge green. Review sentiment colors.,"Star fill animations, review filtering, helpful vote interactions, photo lightbox",User-generated content builds trust. Show verified purchases. Filter by rating. Respond to negative reviews.
20,Community/Forum Landing,"community, forum, social, members, discussion","1. Hero (community value prop), 2. Popular topics/categories, 3. Active members showcase, 4. Join CTA",Join button prominent + After member showcase,"Warm, welcoming. Member photos add humanity. Topic badges in brand colors. Activity indicators green.","Member avatars animation, activity feed live updates, topic hover previews, join success celebration","Show active community (member count, posts today). Highlight benefits. Preview content. Easy onboarding."
21,Before-After Transformation,"before-after, transformation, results, comparison","1. Hero (problem state), 2. Transformation slider/comparison, 3. How it works, 4. Results CTA",After transformation reveal + Bottom,Contrast: muted/grey (before) vs vibrant/colorful (after). Success green for results.,"Slider comparison interaction, before/after reveal animations, result counters, testimonial videos",Visual proof of value. 45% higher conversion. Real results. Specific metrics. Guarantee offer.
22,Marketplace / Directory,"marketplace, directory, search, listing","1. Hero (Search focused), 2. Categories, 3. Featured Listings, 4. Trust/Safety, 5. CTA (Become a host/seller)",Hero Search Bar + Navbar 'List your item',"Search: High contrast. Categories: Visual icons. Trust: Blue/Green.","Search autocomplete animation, map hover pins, card carousel","Search bar is the CTA. Reduce friction to search. Popular searches suggestions."
23,Newsletter / Content First,"newsletter, content, writer, blog, subscribe, minimal & direct, minimal-direct","1. Hero (Value Prop + Form), 2. Recent Issues/Archives, 3. Social Proof (Subscriber count), 4. About Author",Hero inline form + Sticky header form,"Minimalist. Paper-like background. Text focus. Accent color for Subscribe.","Text highlight animations, typewriter effect, subtle fade-in","Single field form (Email only). Show 'Join X, 000 readers'. Read sample link."
24,Webinar Registration,"webinar, registration, event, training, live","1. Hero (Topic + Timer + Form), 2. What you'll learn, 3. Speaker Bio, 4. Urgency/Bonuses, 5. Form (again)",Hero (Right side form) + Bottom anchor,"Urgency: Red/Orange. Professional: Blue/Navy. Form: High contrast white.","Countdown timer, speaker avatar float, urgent ticker","Limited seats logic. 'Live' indicator. Auto-fill timezone."
25,Enterprise Gateway,"enterprise, corporate, gateway, solutions, portal, trust, authority, trust & authority","1. Hero (Video/Mission), 2. Solutions by Industry, 3. Solutions by Role, 4. Client Logos, 5. Contact Sales",Contact Sales (Primary) + Login (Secondary),"Corporate: Navy/Grey. High integrity. Conservative accents.","Slow video background, logo carousel, tab switching for industries","Path selection (I am a...). Mega menu navigation. Trust signals prominent."
26,Portfolio Grid,"portfolio, grid, showcase, gallery, masonry, portfolio grid + visuals","1. Hero (Name/Role), 2. Project Grid (Masonry), 3. About/Philosophy, 4. Contact",Project Card Hover + Footer Contact,"Neutral background (let work shine). Text: Black/White. Accent: Minimal.","Image lazy load reveal, hover overlay info, lightbox view","Visuals first. Filter by category. Fast loading essential."
27,Horizontal Scroll Journey,"horizontal, scroll, journey, gallery, storytelling, panoramic, storytelling-driven","1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer",Floating Sticky CTA or End of Horizontal Track,Continuous palette transition. Chapter colors. Progress bar #000000.,"Scroll-jacking (careful), parallax layers, horizontal slide, progress indicator","Immersive product discovery. High engagement. Keep navigation visible."
28,Bento Grid Showcase,"bento, grid, features, modular, apple-style, showcase, feature-rich showcase","1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA",Floating Action Button or Bottom of Grid,"Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark.","Hover card scale (1.02), video inside cards, tilt effect, staggered reveal","Scannable value props. High information density without clutter. Mobile stack."
29,Interactive 3D Configurator,"3d, configurator, customizer, interactive, product, interactive product demo","1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase",Inside Configurator UI + Sticky Bottom Bar,"Neutral studio background. Product: Realistic materials. UI: Minimal overlay.","Real-time rendering, material swap animation, camera rotate/zoom, light reflection","Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart."
30,AI-Driven Dynamic Landing,"ai, dynamic, personalized, adaptive, generative","1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop","Input Field (Hero) + 'Try it' Buttons","Adaptive to user input. Dark mode for compute feel. Neon accents.","Typing text effects, shimmering generation loaders, morphing layouts","Immediate value demonstration. 'Show, don't tell'. Low friction start."
31,Feature-Rich Showcase,"feature-rich, feature-rich showcase, features, showcase, product showcase","1. Hero (value prop), 2. Feature grid/cards (4-6), 3. Use cases or benefits, 4. Social proof or logos, 5. CTA",Hero (sticky) + After features + Bottom,Brand primary + card bg #FAFAFA. Feature icons accent. CTA contrasting.,"Feature card hover lift, scroll reveal, icon micro-interactions","Clear feature hierarchy. One key message per card. Strong CTA repetition."
32,Hero-Centric Design,"hero-centric, hero-centric design, hero-first, hero above fold","1. Full-bleed Hero (headline + visual), 2. Single value prop strip, 3. Key benefit or proof, 4. Primary CTA",Hero dominant (center/bottom) + Sticky nav CTA,Hero: High-impact visual. Minimal text. CTA 7:1 contrast.,"Hero parallax or video, CTA pulse on scroll, minimal chrome","One primary CTA. Hero is 60-80% above fold. Mobile: same hierarchy."
33,Trust & Authority + Conversion,"trust & authority, trust, authority, conversion, credibility, enterprise","1. Hero (mission/credibility), 2. Proof (logos, certs, stats), 3. Solution overview, 4. Clear CTA path",Contact Sales / Get Quote (primary) + Nav,"Navy/Grey corporate. Trust blue. Accent for CTA only.","Logo carousel, stat counters, testimonial strip","Security badges. Case studies. Transparent pricing. Low-friction form."
34,Real-Time / Operations Landing,"real-time, real-time monitor, operations, dashboard, telemetry, live data","1. Hero (product + live preview or status), 2. Key metrics/indicators, 3. How it works, 4. CTA (Start trial / Contact)","Primary CTA in nav + After metrics",Dark or neutral. Status colors (green/amber/red). Data-dense but scannable.,"Live data ticker, status pulse, minimal decoration","For ops/security/iot products. Demo or sandbox link. Trust signals."
1 No Pattern Name Keywords Section Order Primary CTA Placement Color Strategy Recommended Effects Conversion Optimization
2 1 Hero + Features + CTA hero, hero-centric, hero-centric design, features, feature-rich, feature-rich showcase, cta, call-to-action 1. Hero with headline/image, 2. Value prop, 3. Key features (3-5), 4. CTA section, 5. Footer Hero (sticky) + Bottom Hero: Brand primary or vibrant. Features: Card bg #FAFAFA. CTA: Contrasting accent color Hero parallax, feature card hover lift, CTA glow on hover Deep CTA placement. Use contrasting color (at least 7:1 contrast ratio). Sticky navbar CTA.
3 2 Hero + Testimonials + CTA hero, testimonials, social-proof, social-proof-focused, social proof focused, trust, reviews, cta 1. Hero, 2. Problem statement, 3. Solution overview, 4. Testimonials carousel, 5. CTA Hero (sticky) + Post-testimonials Hero: Brand color. Testimonials: Light bg #F5F5F5. Quotes: Italic, muted color #666. CTA: Vibrant Testimonial carousel slide animations, quote marks animations, avatar fade-in Social proof before CTA. Use 3-5 testimonials. Include photo + name + role. CTA after social proof.
4 3 Product Demo + Features demo, product-demo, features, showcase, interactive, interactive-product-demo, interactive product demo 1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA Video center + CTA right/bottom Video surround: Brand color overlay. Features: Icon color #0080FF. Text: Dark #222 Video play button pulse, feature scroll reveals, demo interaction highlights Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted.
5 4 Minimal Single Column minimal, simple, direct, minimal & direct, minimal-direct, single-column, clean 1. Hero headline, 2. Short description, 3. Benefit bullets (3 max), 4. CTA, 5. Footer Center, large CTA button Minimalist: Brand + white #FFFFFF + accent. Buttons: High contrast 7:1+. Text: Black/Dark grey Minimal hover effects. Smooth scroll. CTA scale on hover (subtle) Single CTA focus. Large typography. Lots of whitespace. No nav clutter. Mobile-first.
6 5 Funnel (3-Step Conversion) funnel, conversion, conversion-optimized, conversion optimized, steps, wizard, onboarding 1. Hero, 2. Step 1 (problem), 3. Step 2 (solution), 4. Step 3 (action), 5. CTA progression Each step: mini-CTA. Final: main CTA Step colors: 1 (Red/Problem), 2 (Orange/Process), 3 (Green/Solution). CTA: Brand color Step number animations, progress bar fill, step transitions smooth scroll Progressive disclosure. Show only essential info per step. Use progress indicators. Multiple CTAs.
7 6 Comparison Table + CTA comparison, table, compare, versus, cta 1. Hero, 2. Problem intro, 3. Comparison table (product vs competitors), 4. Pricing (optional), 5. CTA Table: Right column. CTA: Below table Table: Alternating rows (white/light grey). Your product: Highlight #FFFACD (light yellow) or green. Text: Dark Table row hover highlight, price toggle animations, feature checkmark animations Use comparison to show unique value. Highlight your product row. Include 'free trial' in pricing row.
8 7 Lead Magnet + Form lead, form, signup, capture, email, magnet 1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit Form CTA: Submit button Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color Form focus state animations, input validation animations, success confirmation animation Form fields ≤ 3 for best conversion. Offer valuable lead magnet preview. Show form submission progress.
9 8 Pricing Page + CTA pricing, plans, tiers, comparison, cta 1. Hero (pricing headline), 2. Price comparison cards, 3. Feature comparison table, 4. FAQ section, 5. Final CTA Each card: CTA button. Sticky CTA in nav Free: Grey, Starter: Blue, Pro: Green/Gold, Enterprise: Dark. Cards: 1px border, shadow Price toggle animation (monthly/yearly), card comparison highlight, FAQ accordion open/close Recommend starter plan (pre-select/highlight). Show annual discount (20-30%). Use FAQs to address concerns.
10 9 Video-First Hero video, hero, media, visual, engaging 1. Hero with video background, 2. Key features overlay, 3. Benefits section, 4. CTA Overlay on video (center/bottom) + Bottom section Dark overlay 60% on video. Brand accent for CTA. White text on dark. Video autoplay muted, parallax scroll, text fade-in on scroll 86% higher engagement with video. Add captions for accessibility. Compress video for performance.
11 10 Scroll-Triggered Storytelling storytelling, scroll, narrative, story, immersive 1. Intro hook, 2. Chapter 1 (problem), 3. Chapter 2 (journey), 4. Chapter 3 (solution), 5. Climax CTA End of each chapter (mini) + Final climax CTA Progressive reveal. Each chapter has distinct color. Building intensity. ScrollTrigger animations, parallax layers, progressive disclosure, chapter transitions Narrative increases time-on-page 3x. Use progress indicator. Mobile: simplify animations.
12 11 AI Personalization Landing ai, personalization, smart, recommendation, dynamic 1. Dynamic hero (personalized), 2. Relevant features, 3. Tailored testimonials, 4. Smart CTA Context-aware placement based on user segment Adaptive based on user data. A/B test color variations per segment. Dynamic content swap, fade transitions, personalized product recommendations 20%+ conversion with personalization. Requires analytics integration. Fallback for new users.
13 12 Waitlist/Coming Soon waitlist, coming-soon, launch, early-access, notify 1. Hero with countdown, 2. Product teaser/preview, 3. Email capture form, 4. Social proof (waitlist count) Email form prominent (above fold) + Sticky form on scroll Anticipation: Dark + accent highlights. Countdown in brand color. Urgency indicators. Countdown timer animation, email validation feedback, success confetti, social share buttons Scarcity + exclusivity. Show waitlist count. Early access benefits. Referral program.
14 13 Comparison Table Focus comparison, table, versus, compare, features 1. Hero (problem statement), 2. Comparison matrix (you vs competitors), 3. Feature deep-dive, 4. Winner CTA After comparison table (highlighted row) + Bottom Your product column highlighted (accent bg or green). Competitors neutral. Checkmarks green. Table row hover highlight, feature checkmark animations, sticky comparison header Show value vs competitors. 35% higher conversion. Be factual. Include pricing if favorable.
15 14 Pricing-Focused Landing pricing, price, cost, plans, subscription 1. Hero (value proposition), 2. Pricing cards (3 tiers), 3. Feature comparison, 4. FAQ, 5. Final CTA Each pricing card + Sticky CTA in nav + Bottom Popular plan highlighted (brand color border/bg). Free: grey. Enterprise: dark/premium. Price toggle monthly/annual animation, card hover lift, FAQ accordion smooth open Annual discount 20-30%. Recommend mid-tier (most popular badge). Address objections in FAQ.
16 15 App Store Style Landing app, mobile, download, store, install 1. Hero with device mockup, 2. Screenshots carousel, 3. Features with icons, 4. Reviews/ratings, 5. Download CTAs Download buttons prominent (App Store + Play Store) throughout Dark/light matching app store feel. Star ratings in gold. Screenshots with device frames. Device mockup rotations, screenshot slider, star rating animations, download button pulse Show real screenshots. Include ratings (4.5+ stars). QR code for mobile. Platform-specific CTAs.
17 16 FAQ/Documentation Landing faq, documentation, help, support, questions, faq/documentation, knowledge base 1. Hero with search bar, 2. Popular categories, 3. FAQ accordion, 4. Contact/support CTA Search bar prominent + Contact CTA for unresolved questions Clean, high readability. Minimal color. Category icons in brand color. Success green for resolved. Search autocomplete, smooth accordion open/close, category hover, helpful feedback buttons Reduce support tickets. Track search analytics. Show related articles. Contact escalation path.
18 17 Immersive/Interactive Experience immersive, interactive, experience, 3d, animation, immersive/interactive experience 1. Full-screen interactive element, 2. Guided product tour, 3. Key benefits revealed, 4. CTA after completion After interaction complete + Skip option for impatient users Immersive experience colors. Dark background for focus. Highlight interactive elements. WebGL, 3D interactions, gamification elements, progress indicators, reward animations 40% higher engagement. Performance trade-off. Provide skip option. Mobile fallback essential.
19 18 Event/Conference Landing event, conference, meetup, registration, schedule, hero-centric design, hero-centric 1. Hero (date/location/countdown), 2. Speakers grid, 3. Agenda/schedule, 4. Sponsors, 5. Register CTA Register CTA sticky + After speakers + Bottom Urgency colors (countdown). Event branding. Speaker cards professional. Sponsor logos neutral. Countdown timer, speaker hover cards with bio, agenda tabs, early bird countdown Early bird pricing with deadline. Social proof (past attendees). Speaker credibility. Multi-ticket discounts.
20 19 Product Review/Ratings Focused reviews, ratings, testimonials, social-proof, social-proof-focused, stars 1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA After reviews summary + Buy button alongside reviews Trust colors. Star ratings gold. Verified badge green. Review sentiment colors. Star fill animations, review filtering, helpful vote interactions, photo lightbox User-generated content builds trust. Show verified purchases. Filter by rating. Respond to negative reviews.
21 20 Community/Forum Landing community, forum, social, members, discussion 1. Hero (community value prop), 2. Popular topics/categories, 3. Active members showcase, 4. Join CTA Join button prominent + After member showcase Warm, welcoming. Member photos add humanity. Topic badges in brand colors. Activity indicators green. Member avatars animation, activity feed live updates, topic hover previews, join success celebration Show active community (member count, posts today). Highlight benefits. Preview content. Easy onboarding.
22 21 Before-After Transformation before-after, transformation, results, comparison 1. Hero (problem state), 2. Transformation slider/comparison, 3. How it works, 4. Results CTA After transformation reveal + Bottom Contrast: muted/grey (before) vs vibrant/colorful (after). Success green for results. Slider comparison interaction, before/after reveal animations, result counters, testimonial videos Visual proof of value. 45% higher conversion. Real results. Specific metrics. Guarantee offer.
23 22 Marketplace / Directory marketplace, directory, search, listing 1. Hero (Search focused), 2. Categories, 3. Featured Listings, 4. Trust/Safety, 5. CTA (Become a host/seller) Hero Search Bar + Navbar 'List your item' Search: High contrast. Categories: Visual icons. Trust: Blue/Green. Search autocomplete animation, map hover pins, card carousel Search bar is the CTA. Reduce friction to search. Popular searches suggestions.
24 23 Newsletter / Content First newsletter, content, writer, blog, subscribe, minimal & direct, minimal-direct 1. Hero (Value Prop + Form), 2. Recent Issues/Archives, 3. Social Proof (Subscriber count), 4. About Author Hero inline form + Sticky header form Minimalist. Paper-like background. Text focus. Accent color for Subscribe. Text highlight animations, typewriter effect, subtle fade-in Single field form (Email only). Show 'Join X, 000 readers'. Read sample link.
25 24 Webinar Registration webinar, registration, event, training, live 1. Hero (Topic + Timer + Form), 2. What you'll learn, 3. Speaker Bio, 4. Urgency/Bonuses, 5. Form (again) Hero (Right side form) + Bottom anchor Urgency: Red/Orange. Professional: Blue/Navy. Form: High contrast white. Countdown timer, speaker avatar float, urgent ticker Limited seats logic. 'Live' indicator. Auto-fill timezone.
26 25 Enterprise Gateway enterprise, corporate, gateway, solutions, portal, trust, authority, trust & authority 1. Hero (Video/Mission), 2. Solutions by Industry, 3. Solutions by Role, 4. Client Logos, 5. Contact Sales Contact Sales (Primary) + Login (Secondary) Corporate: Navy/Grey. High integrity. Conservative accents. Slow video background, logo carousel, tab switching for industries Path selection (I am a...). Mega menu navigation. Trust signals prominent.
27 26 Portfolio Grid portfolio, grid, showcase, gallery, masonry, portfolio grid + visuals 1. Hero (Name/Role), 2. Project Grid (Masonry), 3. About/Philosophy, 4. Contact Project Card Hover + Footer Contact Neutral background (let work shine). Text: Black/White. Accent: Minimal. Image lazy load reveal, hover overlay info, lightbox view Visuals first. Filter by category. Fast loading essential.
28 27 Horizontal Scroll Journey horizontal, scroll, journey, gallery, storytelling, panoramic, storytelling-driven 1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer Floating Sticky CTA or End of Horizontal Track Continuous palette transition. Chapter colors. Progress bar #000000. Scroll-jacking (careful), parallax layers, horizontal slide, progress indicator Immersive product discovery. High engagement. Keep navigation visible.
29 28 Bento Grid Showcase bento, grid, features, modular, apple-style, showcase, feature-rich showcase 1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA Floating Action Button or Bottom of Grid Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark. Hover card scale (1.02), video inside cards, tilt effect, staggered reveal Scannable value props. High information density without clutter. Mobile stack.
30 29 Interactive 3D Configurator 3d, configurator, customizer, interactive, product, interactive product demo 1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase Inside Configurator UI + Sticky Bottom Bar Neutral studio background. Product: Realistic materials. UI: Minimal overlay. Real-time rendering, material swap animation, camera rotate/zoom, light reflection Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart.
31 30 AI-Driven Dynamic Landing ai, dynamic, personalized, adaptive, generative 1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop Input Field (Hero) + 'Try it' Buttons Adaptive to user input. Dark mode for compute feel. Neon accents. Typing text effects, shimmering generation loaders, morphing layouts Immediate value demonstration. 'Show, don't tell'. Low friction start.
32 31 Feature-Rich Showcase feature-rich, feature-rich showcase, features, showcase, product showcase 1. Hero (value prop), 2. Feature grid/cards (4-6), 3. Use cases or benefits, 4. Social proof or logos, 5. CTA Hero (sticky) + After features + Bottom Brand primary + card bg #FAFAFA. Feature icons accent. CTA contrasting. Feature card hover lift, scroll reveal, icon micro-interactions Clear feature hierarchy. One key message per card. Strong CTA repetition.
33 32 Hero-Centric Design hero-centric, hero-centric design, hero-first, hero above fold 1. Full-bleed Hero (headline + visual), 2. Single value prop strip, 3. Key benefit or proof, 4. Primary CTA Hero dominant (center/bottom) + Sticky nav CTA Hero: High-impact visual. Minimal text. CTA 7:1 contrast. Hero parallax or video, CTA pulse on scroll, minimal chrome One primary CTA. Hero is 60-80% above fold. Mobile: same hierarchy.
34 33 Trust & Authority + Conversion trust & authority, trust, authority, conversion, credibility, enterprise 1. Hero (mission/credibility), 2. Proof (logos, certs, stats), 3. Solution overview, 4. Clear CTA path Contact Sales / Get Quote (primary) + Nav Navy/Grey corporate. Trust blue. Accent for CTA only. Logo carousel, stat counters, testimonial strip Security badges. Case studies. Transparent pricing. Low-friction form.
35 34 Real-Time / Operations Landing real-time, real-time monitor, operations, dashboard, telemetry, live data 1. Hero (product + live preview or status), 2. Key metrics/indicators, 3. How it works, 4. CTA (Start trial / Contact) Primary CTA in nav + After metrics Dark or neutral. Status colors (green/amber/red). Data-dense but scannable. Live data ticker, status pulse, minimal decoration For ops/security/iot products. Demo or sandbox link. Trust signals.

View file

@ -1,162 +0,0 @@
No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing Page Pattern,Dashboard Style (if applicable),Color Palette Focus,Key Considerations
1,SaaS (General),"app, b2b, cloud, general, saas, software, subscription",Glassmorphism + Flat Design,"Soft UI Evolution, Minimalism",Hero + Features + CTA,Data-Dense + Real-Time Monitoring,Trust blue + accent contrast,Balance modern feel with clarity. Focus on CTAs.
2,Micro SaaS,"app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key."
3,E-commerce,"buy, commerce, e, ecommerce, products, retail, sell, shop, store",Vibrant & Block-based,"Aurora UI, Motion-Driven",Feature-Rich Showcase,Sales Intelligence Dashboard,Brand primary + success green,Engagement & conversions. High visual hierarchy.
4,E-commerce Luxury,"buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store",Liquid Glass + Glassmorphism,"3D & Hyperrealism, Aurora UI",Feature-Rich Showcase,Sales Intelligence Dashboard,Premium colors + minimal accent,Elegance & sophistication. Premium materials.
5,B2B Service,"appointment, b, b2b, booking, business, consultation, corporate, enterprise, service",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging.
6,Financial Dashboard,"admin, analytics, dashboard, data, financial, panel",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount."
7,Analytics Dashboard,"admin, analytics, dashboard, data, panel",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority.
8,Healthcare App,"app, clinic, health, healthcare, medical, patient",Neumorphism + Accessible & Ethical,"Soft UI Evolution, Claymorphism (for patients)",Social Proof-Focused,User Behavior Analytics,Calm blue + health green + trust,Accessibility mandatory. Calming aesthetic.
9,Educational App,"app, course, education, educational, learning, school, training",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful colors + clear hierarchy,Engagement & ease of use. Age-appropriate design.
10,Creative Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary.
11,Portfolio/Personal,"creative, personal, portfolio, projects, showcase, work",Motion-Driven + Minimalism,"Brutalism, Aurora UI",Storytelling-Driven,N/A - Personal branding,Brand primary + artistic interpretation,Showcase work. Personality shine through.
12,Gaming,"entertainment, esports, game, gaming, play",3D & Hyperrealism + Retro-Futurism,"Motion-Driven, Vibrant & Block",Feature-Rich Showcase,N/A - Game focused,Vibrant + neon + immersive colors,Immersion priority. Performance critical.
13,Government/Public Service,"appointment, booking, consultation, government, public, service",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount.
14,Fintech/Crypto,"banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3",Glassmorphism + Dark Mode (OLED),"Retro-Futurism, Motion-Driven",Conversion-Optimized,Real-Time Monitoring + Predictive,Dark tech colors + trust + vibrant accents,Security perception. Real-time data critical.
15,Social Media App,"app, community, content, entertainment, media, network, sharing, social, streaming, users, video",Vibrant & Block-based + Motion-Driven,"Aurora UI, Micro-interactions",Feature-Rich Showcase,User Behavior Analytics,Vibrant + engagement colors,Engagement & retention. Addictive design ethics.
16,Productivity Tool,"collaboration, productivity, project, task, tool, workflow",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Interactive Product Demo,Drill-Down Analytics,Clear hierarchy + functional colors,Ease of use. Speed & efficiency focus.
17,Design System/Component Library,"component, design, library, system",Minimalism + Accessible & Ethical,"Flat Design, Zero Interface",Feature-Rich Showcase,N/A - Dev focused,Clear hierarchy + code-like structure,Consistency. Developer-first approach.
18,AI/Chatbot Platform,"ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform",AI-Native UI + Minimalism,"Zero Interface, Glassmorphism",Interactive Product Demo,AI/ML Analytics Dashboard,Neutral + AI Purple (#6366F1),Conversational UI. Streaming text. Context awareness. Minimal chrome.
19,NFT/Web3 Platform,"nft, platform, web",Cyberpunk UI + Glassmorphism,"Aurora UI, 3D & Hyperrealism",Feature-Rich Showcase,Crypto/Blockchain Dashboard,Dark + Neon + Gold (#FFD700),Wallet integration. Transaction feedback. Gas fees display. Dark mode essential.
20,Creator Economy Platform,"creator, economy, platform",Vibrant & Block-based + Bento Box Grid,"Motion-Driven, Aurora UI",Social Proof-Focused,User Behavior Analytics,Vibrant + Brand colors,Creator profiles. Monetization display. Engagement metrics. Social proof.
21,Remote Work/Collaboration Tool,"collaboration, remote, tool, work",Soft UI Evolution + Minimalism,"Glassmorphism, Micro-interactions",Feature-Rich Showcase,Drill-Down Analytics,Calm Blue + Neutral grey,Real-time collaboration. Status indicators. Video integration. Notification management.
22,Mental Health App,"app, health, mental",Neumorphism + Accessible & Ethical,"Claymorphism, Soft UI Evolution",Social Proof-Focused,Healthcare Analytics,Calm Pastels + Trust colors,Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory.
23,Pet Tech App,"app, pet, tech",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful + Warm colors,Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration.
24,Smart Home/IoT Dashboard,"admin, analytics, dashboard, data, home, iot, panel, smart",Glassmorphism + Dark Mode (OLED),"Minimalism, AI-Native UI",Interactive Product Demo,Real-Time Monitoring,Dark + Status indicator colors,Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions.
25,EV/Charging Ecosystem,"charging, ecosystem, ev",Minimalism + Aurora UI,"Glassmorphism, Organic Biophilic",Hero-Centric Design,Energy/Utilities Dashboard,Electric Blue (#009CD1) + Green,Charging station maps. Range estimation. Cost calculation. Environmental impact.
26,Subscription Box Service,"appointment, booking, box, consultation, membership, plan, recurring, service, subscription",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals.
27,Podcast Platform,"platform, podcast",Dark Mode (OLED) + Minimalism,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven,Media/Entertainment Dashboard,Dark + Audio waveform accents,Audio player UX. Episode discovery. Creator tools. Analytics for podcasters.
28,Dating App,"app, dating",Vibrant & Block-based + Motion-Driven,"Aurora UI, Glassmorphism",Social Proof-Focused,User Behavior Analytics,Warm + Romantic (Pink/Red gradients),Profile cards. Swipe interactions. Match animations. Safety features. Video chat.
29,Micro-Credentials/Badges Platform,"badges, credentials, micro, platform",Minimalism + Flat Design,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority,Education Dashboard,Trust Blue + Gold (#FFD700),Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration.
30,Knowledge Base/Documentation,"base, documentation, knowledge",Minimalism + Accessible & Ethical,"Swiss Modernism 2.0, Flat Design",FAQ/Documentation,N/A - Documentation focused,Clean hierarchy + minimal color,Search-first. Clear navigation. Code highlighting. Version switching. Feedback system.
31,Hyperlocal Services,"appointment, booking, consultation, hyperlocal, service, services",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews.
32,Beauty/Spa/Wellness Service,"appointment, beauty, booking, consultation, service, spa, wellness",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery.
33,Luxury/Premium Brand,"brand, elegant, exclusive, high-end, luxury, premium",Liquid Glass + Glassmorphism,"Minimalism, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Sales Intelligence Dashboard,Black + Gold (#FFD700) + White + Minimal accent,Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel.
34,Restaurant/Food Service,"appointment, booking, consultation, delivery, food, menu, order, restaurant, service",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent.
35,Fitness/Gym App,"app, exercise, fitness, gym, health, workout",Vibrant & Block-based + Dark Mode (OLED),"Motion-Driven, Neumorphism",Feature-Rich Showcase,User Behavior Analytics,Energetic (Orange #FF6B35 Electric Blue) + Dark bg,Progress tracking. Workout plans. Community features. Achievements. Motivational design.
36,Real Estate/Property,"buy, estate, housing, property, real, real-estate, rent",Glassmorphism + Minimalism,"Motion-Driven, 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Trust Blue (#0077B6) + Gold accents + White,Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery.
37,Travel/Tourism Agency,"agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first.
38,Hotel/Hospitality,"hospitality, hotel",Liquid Glass + Minimalism,"Glassmorphism, Soft UI Evolution",Hero-Centric Design + Social Proof,Revenue Management Dashboard,Warm neutrals + Gold (#D4AF37) + Brand accent,Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery.
39,Wedding/Event Planning,"conference, event, meetup, planning, registration, ticket, wedding",Soft UI Evolution + Aurora UI,"Glassmorphism, Motion-Driven",Storytelling-Driven + Social Proof,N/A - Planning focused,Soft Pink (#FFD6E0) + Gold + Cream + Sage,Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic.
40,Legal Services,"appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery.
41,Insurance Platform,"insurance, platform",Trust & Authority + Flat Design,"Accessible & Ethical, Minimalism",Conversion-Optimized + Trust,Claims Analytics Dashboard,Trust Blue (#0066CC) + Green (security) + Neutral,Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges.
42,Banking/Traditional Finance,"banking, finance, traditional",Minimalism + Accessible & Ethical,"Trust & Authority, Dark Mode (OLED)",Trust & Authority + Feature-Rich,Financial Dashboard,Navy (#0A1628) + Trust Blue + Gold accents,Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount.
43,Online Course/E-learning,"course, e, learning, online",Claymorphism + Vibrant & Block-based,"Motion-Driven, Flat Design",Feature-Rich Showcase + Social Proof,Education Dashboard,Vibrant learning colors + Progress green,Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification.
44,Non-profit/Charity,"charity, non, profit",Accessible & Ethical + Organic Biophilic,"Minimalism, Storytelling-Driven",Storytelling-Driven + Trust,Donation Analytics Dashboard,Cause-related colors + Trust + Warm,Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection.
45,Music Streaming,"music, streaming",Dark Mode (OLED) + Vibrant & Block-based,"Motion-Driven, Aurora UI",Feature-Rich Showcase,Media/Entertainment Dashboard,Dark (#121212) + Vibrant accents + Album art colors,Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations.
46,Video Streaming/OTT,"ott, streaming, video",Dark Mode (OLED) + Motion-Driven,"Glassmorphism, Vibrant & Block-based",Hero-Centric Design + Feature-Rich,Media/Entertainment Dashboard,Dark bg + Content poster colors + Brand accent,Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy.
47,Job Board/Recruitment,"board, job, recruitment",Flat Design + Minimalism,"Vibrant & Block-based, Accessible & Ethical",Conversion-Optimized + Feature-Rich,HR Analytics Dashboard,Professional Blue + Success Green + Neutral,Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights.
48,Marketplace (P2P),"buyers, listings, marketplace, p, platform, sellers",Vibrant & Block-based + Flat Design,"Micro-interactions, Trust & Authority",Feature-Rich Showcase + Social Proof,E-commerce Analytics,Trust colors + Category colors + Success green,Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges.
49,Logistics/Delivery,"delivery, logistics",Minimalism + Flat Design,"Dark Mode (OLED), Micro-interactions",Feature-Rich Showcase + Conversion,Real-Time Monitoring + Route Analytics,Blue (#2563EB) + Orange (tracking) + Green (delivered),Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration.
50,Agriculture/Farm Tech,"agriculture, farm, tech",Organic Biophilic + Flat Design,"Minimalism, Accessible & Ethical",Feature-Rich Showcase + Trust,IoT Sensor Dashboard,Earth Green (#4A7C23) + Brown + Sky Blue,Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery.
51,Construction/Architecture,"architecture, construction",Minimalism + 3D & Hyperrealism,"Brutalism, Swiss Modernism 2.0",Hero-Centric Design + Feature-Rich,Project Management Dashboard,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue,Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic.
52,Automotive/Car Dealership,"automotive, car, dealership",Motion-Driven + 3D & Hyperrealism,"Dark Mode (OLED), Glassmorphism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Brand colors + Metallic accents + Dark/Light,Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery.
53,Photography Studio,"photography, studio",Motion-Driven + Minimalism,"Aurora UI, Glassmorphism",Storytelling-Driven + Hero-Centric,N/A - Portfolio focused,Black + White + Minimal accent,Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery.
54,Coworking Space,"coworking, space",Vibrant & Block-based + Glassmorphism,"Minimalism, Motion-Driven",Hero-Centric Design + Feature-Rich,Occupancy Dashboard,Energetic colors + Wood tones + Brand accent,Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour.
55,Home Services (Plumber/Electrician),"appointment, booking, consultation, electrician, home, plumber, service, services",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals.
56,Childcare/Daycare,"childcare, daycare",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Accessible & Ethical",Social Proof-Focused + Trust,Parent Dashboard,Playful pastels + Safe colors + Warm accents,Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery.
57,Senior Care/Elderly,"care, elderly, senior",Accessible & Ethical + Soft UI Evolution,"Minimalism, Neumorphism",Trust & Authority + Social Proof,Healthcare Analytics,Calm Blue + Warm neutrals + Large text,Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first.
58,Medical Clinic,"clinic, medical",Accessible & Ethical + Minimalism,"Neumorphism, Trust & Authority",Trust & Authority + Conversion,Healthcare Analytics,Medical Blue (#0077B6) + Trust White + Calm Green,Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals.
59,Pharmacy/Drug Store,"drug, pharmacy, store",Flat Design + Accessible & Ethical,"Minimalism, Trust & Authority",Conversion-Optimized + Trust,Inventory Dashboard,Pharmacy Green + Trust Blue + Clean White,Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications.
60,Dental Practice,"dental, practice",Soft UI Evolution + Minimalism,"Accessible & Ethical, Trust & Authority",Social Proof-Focused + Conversion,Patient Analytics,Fresh Blue + White + Smile Yellow accent,Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery.
61,Veterinary Clinic,"clinic, veterinary",Claymorphism + Accessible & Ethical,"Soft UI Evolution, Flat Design",Social Proof-Focused + Trust,Pet Health Dashboard,Caring Blue + Pet-friendly colors + Warm accents,Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery.
62,Florist/Plant Shop,"florist, plant, shop",Organic Biophilic + Vibrant & Block-based,"Aurora UI, Motion-Driven",Hero-Centric Design + Conversion,E-commerce Analytics,Natural Green + Floral pinks/purples + Earth tones,Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery.
63,Bakery/Cafe,"bakery, cafe",Vibrant & Block-based + Soft UI Evolution,"Claymorphism, Motion-Driven",Hero-Centric Design + Conversion,N/A - Order focused,Warm Brown + Cream + Appetizing accents,Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography.
64,Brewery/Winery,"brewery, winery",Motion-Driven + Storytelling-Driven,"Dark Mode (OLED), Organic Biophilic",Storytelling-Driven + Hero-Centric,N/A - E-commerce focused,Deep amber/burgundy + Gold + Craft aesthetic,Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery.
65,Airline,"airline, aviation, flight, travel, booking, airport, flying",Minimalism + Glassmorphism,"Motion-Driven, Accessible & Ethical",Conversion-Optimized + Feature-Rich,Operations Dashboard,Sky Blue + Brand colors + Trust accents,Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first.
66,News/Media Platform,"content, entertainment, media, news, platform, streaming, video",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Hero-Centric Design + Feature-Rich,Media Analytics Dashboard,Brand colors + High contrast + Category colors,Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading.
67,Magazine/Blog,"articles, blog, content, magazine, posts, writing",Swiss Modernism 2.0 + Motion-Driven,"Minimalism, Aurora UI",Storytelling-Driven + Hero-Centric,Content Analytics,Editorial colors + Brand primary + Clean white,Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused.
68,Freelancer Platform,"freelancer, platform",Flat Design + Minimalism,"Vibrant & Block-based, Micro-interactions",Feature-Rich Showcase + Conversion,Marketplace Analytics,Professional Blue + Success Green + Neutral,Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management.
69,Marketing Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic.
70,Event Management,"conference, event, management, meetup, registration, ticket",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Aurora UI",Hero-Centric Design + Feature-Rich,Event Analytics,Event theme colors + Excitement accents,Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer.
71,Membership/Community,"community, membership",Vibrant & Block-based + Soft UI Evolution,"Bento Box Grid, Micro-interactions",Social Proof-Focused + Conversion,Community Analytics,Community brand colors + Engagement accents,Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content.
72,Newsletter Platform,"newsletter, platform",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Minimal & Direct + Conversion,Email Analytics,Brand primary + Clean white + CTA accent,Subscribe form. Archive. About. Social proof. Sample content. Simple conversion.
73,Digital Products/Downloads,"digital, downloads, products",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Bento Box Grid",Feature-Rich Showcase + Conversion,E-commerce Analytics,Product category colors + Brand + Success green,Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews.
74,Church/Religious Organization,"church, organization, religious",Accessible & Ethical + Soft UI Evolution,"Minimalism, Trust & Authority",Hero-Centric Design + Social Proof,N/A - Community focused,Warm Gold + Deep Purple/Blue + White,Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery.
75,Sports Team/Club,"club, sports, team",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Performance Analytics,Team colors + Energetic accents,Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery.
76,Museum/Gallery,"gallery, museum",Minimalism + Motion-Driven,"Swiss Modernism 2.0, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Visitor Analytics,Art-appropriate neutrals + Exhibition accents,Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design.
77,Theater/Cinema,"cinema, theater",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Hero-Centric Design + Conversion,Booking Analytics,Dark + Spotlight accents + Gold,Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery.
78,Language Learning App,"app, language, learning",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Feature-Rich Showcase + Social Proof,Learning Analytics,Playful colors + Progress indicators + Country flags,Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges.
79,Coding Bootcamp,"bootcamp, coding",Dark Mode (OLED) + Minimalism,"Cyberpunk UI, Flat Design",Feature-Rich Showcase + Social Proof,Student Analytics,Code editor colors + Brand + Success green,Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic.
80,Cybersecurity Platform,"cyber, security, platform",Cyberpunk UI + Dark Mode (OLED),"Neubrutalism, Minimal & Direct",Trust & Authority + Real-Time,Real-Time Monitoring + Heat Map,Matrix Green + Deep Black + Terminal feel,Data density. Threat visualization. Dark mode default.
81,Developer Tool / IDE,"dev, developer, tool, ide",Dark Mode (OLED) + Minimalism,"Flat Design, Bento Box Grid",Minimal & Direct + Documentation,Real-Time Monitor + Terminal,Dark syntax theme colors + Blue focus,Keyboard shortcuts. Syntax highlighting. Fast performance.
82,Biotech / Life Sciences,"biotech, biology, science",Glassmorphism + Clean Science,"Minimalism, Organic Biophilic",Storytelling-Driven + Research,Data-Dense + Predictive,Sterile White + DNA Blue + Life Green,Data accuracy. Cleanliness. Complex data viz.
83,Space Tech / Aerospace,"aerospace, space, tech",Holographic / HUD + Dark Mode,"Glassmorphism, 3D & Hyperrealism",Immersive Experience + Hero,Real-Time Monitoring + 3D,Deep Space Black + Star White + Metallic,High-tech feel. Precision. Telemetry data.
84,Architecture / Interior,"architecture, design, interior",Exaggerated Minimalism + High Imagery,"Swiss Modernism 2.0, Parallax",Portfolio Grid + Visuals,Project Management + Gallery,Monochrome + Gold Accent + High Imagery,High-res images. Typography. Space.
85,Quantum Computing Interface,"quantum, computing, physics, qubit, future, science",Holographic / HUD + Dark Mode,"Glassmorphism, Spatial UI",Immersive/Interactive Experience,3D Spatial Data + Real-Time Monitor,Quantum Blue #00FFFF + Deep Black + Interference patterns,Visualize complexity. Qubit states. Probability clouds. High-tech trust.
86,Biohacking / Longevity App,"biohacking, health, longevity, tracking, wellness, science",Biomimetic / Organic 2.0,"Minimalism, Dark Mode (OLED)",Data-Dense + Storytelling,Real-Time Monitor + Biological Data,Cellular Pink/Red + DNA Blue + Clean White,Personal data privacy. Scientific credibility. Biological visualizations.
87,Autonomous Drone Fleet Manager,"drone, autonomous, fleet, aerial, logistics, robotics",HUD / Sci-Fi FUI,"Real-Time Monitor, Spatial UI",Real-Time Monitor,Geographic + Real-Time,Tactical Green #00FF00 + Alert Red + Map Dark,Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts.
88,Generative Art Platform,"art, generative, ai, creative, platform, gallery",Minimalism (Frame) + Gen Z Chaos,"Masonry Grid, Dark Mode",Bento Grid Showcase,Gallery / Portfolio,Neutral #F5F5F5 (Canvas) + User Content,Content is king. Fast loading. Creator attribution. Minting flow.
89,Spatial Computing OS / App,"spatial, vr, ar, vision, os, immersive, mixed-reality",Spatial UI (VisionOS),"Glassmorphism, 3D & Hyperrealism",Immersive/Interactive Experience,Spatial Dashboard,Frosted Glass + System Colors + Depth,Gaze/Pinch interaction. Depth hierarchy. Environment awareness.
90,Sustainable Energy / Climate Tech,"climate, energy, sustainable, green, tech, carbon",Organic Biophilic + E-Ink / Paper,"Data-Dense, Swiss Modernism",Interactive Demo + Data,Energy/Utilities Dashboard,Earth Green + Sky Blue + Solar Yellow,Data transparency. Impact visualization. Low-carbon web design.
91,Personal Finance Tracker,"budget, expense, money, finance, spending, savings, tracker, personal, wallet",Glassmorphism + Dark Mode (OLED),"Minimalism, Flat Design",Interactive Product Demo,Financial Dashboard,Calm blue + success green + alert red + chart accents,Category pie/donut charts. Monthly trend lines. Budget progress bars. Transaction list with swipe actions. Receipt camera. Currency formatting. Recurring entries.
92,Chat & Messaging App,"chat, message, messenger, im, realtime, conversation, inbox, dm, whatsapp, telegram",Minimalism + Micro-interactions,"Glassmorphism, Flat Design",Feature-Rich Showcase + Demo,User Behavior Analytics,Brand primary + bubble contrast (sender/receiver) + typing grey,Bubble UI (left/right alignment). Typing indicators. Read receipts (✓✓). Image/file preview. Emoji reactions. Group avatars. Online status dots. Swipe-to-reply.
93,Notes & Writing App,"notes, memo, writing, editor, notebook, markdown, journal, notion, obsidian",Minimalism + Flat Design,"Swiss Modernism 2.0, Soft UI Evolution",Minimal & Direct,N/A - Editor focused,Clean white/cream + minimal accent + editor syntax colors,WYSIWYG or Markdown toggle. Folder/tag organization. Full-text search. Cloud sync. Typography-first. Distraction-free zen mode. Slash-command palette.
94,Habit Tracker,"habit, streak, routine, daily, tracker, goals, consistency, discipline",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Social Proof-Focused + Demo,User Behavior Analytics,Streak warm (amber/orange) + progress green + motivational accents,Streak calendar heatmap. Daily check-in interaction. Gamification (badges/levels/fire). Reminder push. Progress ring charts. Weekly/monthly stats. Motivational micro-copy.
95,Food Delivery / On-Demand,"delivery, food, order, uber-eats, doordash, takeout, on-demand, courier",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Flat Design",Hero-Centric Design + Feature-Rich,Real-Time Monitoring + Map,Appetizing warm (orange/red) + trust blue + map accent,Restaurant cards with ratings. Menu category horizontal scroll. Cart bottom sheet. Real-time map tracking + driver ETA. Order status stepper. Rating post-delivery.
96,Ride Hailing / Transportation,"ride, taxi, uber, lyft, transport, carpool, driver, trip, fare",Minimalism + Glassmorphism,"Dark Mode (OLED), Motion-Driven",Conversion-Optimized + Demo,Real-Time Monitoring + Map,Brand primary + map neutral + status indicator colors,Map-centric full-screen UI. Pickup/dropoff pins + route polyline. Driver card (photo/rating/vehicle). Fare estimate. Trip timer. Safety SOS button. Payment sheet.
97,Recipe & Cooking App,"recipe, cooking, food, kitchen, cookbook, meal, ingredient, chef",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Organic Biophilic",Hero-Centric Design + Feature-Rich,N/A - Content focused,Warm food tones (terracotta/sage/cream) + appetizing imagery,Step-by-step with checkable instructions. Ingredient list with serving adjuster. Built-in timer per step. Cooking mode (screen-awake + large text). Save/bookmark. Share.
98,Meditation & Mindfulness,"meditation, mindfulness, calm, breathe, wellness, relaxation, sleep, headspace",Neumorphism + Soft UI Evolution,"Aurora UI, Glassmorphism",Storytelling-Driven + Social Proof,User Behavior Analytics,Ultra-calm pastels (lavender/sage/sky) + breathing animation gradient,Breathing circle animation. Session duration picker. Ambient sound mixer. Streak/consistency tracking. Guided audio player. Sleep timer. Minimal chrome. Slow easing transitions only.
99,Weather App,"weather, forecast, temperature, climate, rain, sun, location, humidity",Glassmorphism + Aurora UI,"Motion-Driven, Minimalism",Hero-Centric Design,N/A - Utility focused,Atmospheric gradients (sky blue → sunset → storm grey) + temp scale,Location auto-detect. Hourly horizontal scroll + daily/weekly list. Animated weather icons. Air quality index. UV/wind/humidity chips. Radar map overlay. Widget-friendly layout.
100,Diary & Journal App,"diary, journal, personal, daily, reflection, mood, gratitude, writing",Soft UI Evolution + Minimalism,"Neumorphism, Sketch Hand-Drawn",Storytelling-Driven,N/A - Personal focused,Warm paper tones (cream/linen) + muted ink + mood-coded accents,Calendar month-view entry. Mood tag selector (emoji/color). Photo/voice attachment. Writing prompts. Privacy lock (FaceID/PIN). Search across entries. Export to PDF.
101,CRM & Client Management,"crm, client, customer, sales, pipeline, contact, lead, deal, hubspot",Flat Design + Minimalism,"Soft UI Evolution, Micro-interactions",Feature-Rich Showcase + Demo,Sales Intelligence Dashboard,Professional blue + pipeline stage colors + closed-won green,Contact card list with avatar. Pipeline kanban board. Activity timeline. Quick-log (call/email/meeting). Deal amount + probability. Tag/segment filter. Mobile quick-actions.
102,Inventory & Stock Management,"inventory, stock, warehouse, product, barcode, supply, sku, management",Flat Design + Minimalism,"Dark Mode (OLED), Accessible & Ethical",Feature-Rich Showcase,Real-Time Monitoring + Data-Dense,Functional neutral + status traffic-light (green/amber/red) + scanner accent,Product list/grid with thumbnails. Barcode/QR scanner. Stock level badges. Low-stock alert banner. Category/location filter. Batch edit. Reorder trigger. Audit log.
103,Flashcard & Study Tool,"flashcard, quiz, study, spaced-repetition, anki, learn, memory, exam",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Feature-Rich Showcase + Demo,Learning Analytics,Playful primary + correct green + incorrect red + progress blue,3D card flip animation. Spaced repetition algorithm. Deck browser. Session progress bar. Streak tracking. Timed quiz mode. Share/import decks. Rich text + image cards.
104,Booking & Appointment App,"booking, appointment, schedule, calendar, reservation, slot, service",Soft UI Evolution + Flat Design,"Minimalism, Micro-interactions",Conversion-Optimized,Drill-Down Analytics,Trust blue + available green + booked grey + confirm accent,Calendar strip or month picker. Available time-slot grid. Service + staff selector. Confirmation summary. Reminder push. Reschedule/cancel flow. Two-sided (provider ↔ client).
105,Invoice & Billing Tool,"invoice, billing, payment, receipt, freelance, estimate, quote, accounting",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Conversion-Optimized + Trust,Financial Dashboard,Professional navy + paid green + overdue red + neutral grey,Invoice template with line items. Tax/discount calculation. Status badges (Draft/Sent/Paid/Overdue). PDF export + share. Payment link generation. Client address book. Recurring invoices.
106,Grocery & Shopping List,"grocery, shopping, list, supermarket, checklist, pantry, meal-plan, buy",Flat Design + Vibrant & Block-based,"Claymorphism, Micro-interactions",Minimal & Direct + Demo,N/A - List focused,Fresh green + food-category colors + checkmark accent,Category-grouped list. Tap-to-check interaction (with strikethrough). Quantity stepper. Share list with family. Store aisle sorting. Barcode scan to add. Frequently bought suggestions.
107,Timer & Pomodoro,"timer, pomodoro, countdown, stopwatch, focus, clock, productivity, interval",Minimalism + Neumorphism,"Dark Mode (OLED), Micro-interactions",Minimal & Direct,N/A - Utility focused,High-contrast on dark + focus red/amber + break green,Large centered countdown digits. Circular progress ring. Session/break auto-switch. Session history log. Custom interval settings. Sound + haptic alerts. Focus stats chart.
108,Parenting & Baby Tracker,"baby, parenting, child, feeding, sleep, diaper, milestone, family, newborn",Claymorphism + Soft UI Evolution,"Vibrant & Block-based, Accessible & Ethical",Social Proof-Focused + Trust,User Behavior Analytics,Soft pastels (baby pink/sky blue/mint/peach) + warm accents,Feed/sleep/diaper quick-log buttons. Growth percentile chart. Milestone timeline with photos. Multiple child profiles. Partner invite + shared access. Pediatric reference. One-handed operation.
109,Scanner & Document Manager,"scanner, document, ocr, pdf, scan, camera, file, archive, digitize",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Feature-Rich Showcase + Demo,N/A - Tool focused,Clean white + camera viewfinder accent + file-type color coding,Camera capture with auto-edge detection. Crop/rotate/enhance. OCR text extraction overlay. PDF multi-page creation. Folder tree organization. Cloud sync. Share/export. Batch scan mode.
110,Calendar & Scheduling App,"calendar, scheduling, planner, agenda, events, reminder, appointment, organize, date, sync",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Feature-Rich Showcase + Demo,N/A - Calendar focused,Clean blue + event category accent colors + success green,Event color coding. Week/month/day views. Recurring events. Conflict detection. Multi-calendar sync.
111,Password Manager,"password, security, vault, credentials, login, secure, encrypt, keychain, 2fa, biometric",Minimalism + Accessible & Ethical,"Dark Mode (OLED), Trust & Authority",Trust & Authority + Feature-Rich,N/A - Vault focused,Trust blue + security green + dark neutral,Security-first. Zero-knowledge architecture. Biometric unlock. Breach alert dashboard. Password generator.
112,Expense Splitter / Bill Split,"split, expense, bill, aa, share, friends, group, settle, debt, payment, owe",Flat Design + Vibrant & Block-based,"Minimalism, Micro-interactions",Minimal & Direct + Demo,N/A - Balance focused,Success green + alert red + neutral grey + avatar accent colors,Group expense tracking. Debt simplification algorithm. Payment reminders. Multi-currency. Receipt photo import.
113,Voice Recorder & Memo,"voice, recorder, memo, audio, transcription, dictate, recording, microphone, note, otter",Minimalism + AI-Native UI,"Flat Design, Dark Mode (OLED)",Interactive Product Demo + Minimal,N/A - Recording focused,Clean white + recording red + waveform accent,Waveform display. Background recording. Auto-transcription (AI). Tag/organize. Cloud sync.
114,Bookmark & Read-Later,"bookmark, read-later, save, article, pocket, link, reading, archive, collection, raindrop",Minimalism + Flat Design,"Editorial Grid, Swiss Modernism 2.0",Minimal & Direct + Demo,N/A - List focused,Paper warm white + ink neutral + minimal accent + tag colors,Fast save via share sheet. Article distraction-free view. Tags and collections. Offline sync. Reading progress.
115,Translator App,"translate, language, text, voice, ocr, dictionary, multilingual, real-time, detect, deepl",Flat Design + AI-Native UI,"Minimalism, Micro-interactions",Feature-Rich Showcase + Interactive Demo,N/A - Utility focused,Global blue + neutral grey + language flag accent,Real-time camera translation (OCR). Voice input and output. Offline mode. Conversation mode. Phrasebook.
116,Calculator & Unit Converter,"calculator, converter, unit, math, currency, measurement, scientific, formula, percentage",Neumorphism + Minimalism,"Flat Design, Dark Mode (OLED)",Minimal & Direct,N/A - Utility focused,Dark functional + orange operation keys + clear button hierarchy,Scientific mode toggle. Live currency rates. Calculation history. Widget support. Gesture input.
117,Alarm & World Clock,"alarm, clock, world, timezone, timer, wake, sleep, schedule, reminder, bedtime",Dark Mode (OLED) + Minimalism,"Neumorphism, Flat Design",Minimal & Direct,N/A - Utility focused,Deep dark + ambient glow accent + timezone gradient,Gentle wake (gradual volume). Timezone visualizer. Sleep tracking integration. Smart alarm skip. Bedtime mode.
118,File Manager & Transfer,"file, manager, transfer, folder, document, storage, cloud, share, organize, compress",Flat Design + Minimalism,"Accessible & Ethical, Dark Mode (OLED)",Feature-Rich Showcase + Demo,N/A - File tree focused,"Functional neutral + file type color coding (PDF orange, doc blue, image purple)",Folder tree navigation. File type preview. Wireless P2P transfer. Cloud integration. Compress and extract.
119,Email Client,"email, mail, inbox, compose, thread, newsletter, filter, reply, gmail, spark, superhuman",Flat Design + Minimalism,"Micro-interactions, Soft UI Evolution",Feature-Rich Showcase + Demo,N/A - Inbox focused,Clean white + brand primary + priority red + snooze amber,Unified inbox. Swipe actions (archive/delete/snooze). Priority sorting. Smart reply. Unsubscribe tool.
120,Casual Puzzle Game,"puzzle, casual, match, brain, game, relaxing, level, tiles, logic, block, three",Claymorphism + Vibrant & Block-based,"Micro-interactions, Motion-Driven",Feature-Rich Showcase + Social Proof,N/A - Game focused,Cheerful pastels + progression gradient + reward gold + bright accent,Satisfying match/clear animations. Progressive difficulty. Daily challenges. No-skip tutorials. Offline play.
121,Trivia & Quiz Game,"trivia, quiz, knowledge, question, answer, challenge, leaderboard, fact, brain, compete",Vibrant & Block-based + Micro-interactions,"Claymorphism, Flat Design",Feature-Rich Showcase + Social Proof,Leaderboard Analytics,Energetic blue + correct green + incorrect red + leaderboard gold,Timer pressure UX. Category selection. Streak system. Real-time multiplayer. Daily quiz mode.
122,Card & Board Game,"card, board, chess, checkers, poker, strategy, turn-based, multiplayer, classic, tabletop",3D & Hyperrealism + Flat Design,"Motion-Driven, Dark Mode (OLED)",Feature-Rich Showcase,N/A - Game focused,Game-theme felt green + dark wood + card back patterns,Real-time or async multiplayer. Game state sync. Tutorial mode. Match history. ELO rating system.
123,Idle & Clicker Game,"idle, clicker, incremental, passive, cookie, adventure, progress, offline, collect, prestige",Vibrant & Block-based + Motion-Driven,"Claymorphism, 3D & Hyperrealism",Feature-Rich Showcase,N/A - Progress focused,Coin gold + upgrade blue + prestige purple + progress green,Offline progress calculation. Satisfying number animations. Upgrade tree clarity. Prestige system. Optional ads.
124,Word & Crossword Game,"word, crossword, wordle, spelling, vocabulary, letters, grid, puzzle, dictionary, daily",Minimalism + Flat Design,"Swiss Modernism 2.0, Micro-interactions",Minimal & Direct + Demo,N/A - Game focused,Clean white + warm letter tiles + success green + shake red,Daily challenge with shareable results. Physical keyboard feel. Difficulty levels. Dictionary hints. Streak stats.
125,Arcade & Retro Game,"arcade, retro, 8bit, action, shoot, runner, tap, reflex, endless, pixel, classic, score",Pixel Art + Retro-Futurism,"Vibrant & Block-based, Motion-Driven",Feature-Rich Showcase + Hero-Centric,N/A - Score focused,Neon on black + pixel palette + score gold + danger red,Instant play with no login. Game Center leaderboards. Haptic feedback on collision. Offline. Controller support.
126,Photo Editor & Filters,"photo, edit, filter, vsco, snapseed, enhance, crop, retouch, adjust, luts, preset, adjust",Minimalism + Dark Mode (OLED),"Motion-Driven, Flat Design",Feature-Rich Showcase + Interactive Demo,N/A - Editor focused,Dark editor background + vibrant filter preview strip + tool icon accent,Non-destructive editing. Filter preview carousel. Histogram. RAW support. Batch export. Social share direct.
127,Short Video Editor,"video, edit, capcut, inshot, clip, reel, tiktok, trim, effects, transitions, music, timeline",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Feature-Rich Showcase + Hero-Centric,N/A - Timeline editor focused,Dark background + timeline track accent colors + effect preview vivid,Multi-track timeline. Licensed music library. Text overlays. Auto-captions. Export 9:16 / 16:9 / 1:1.
128,Drawing & Sketching Canvas,"drawing, sketch, procreate, canvas, paint, illustration, digital, brush, layers, art, stylus",Minimalism + Dark Mode (OLED),"Anti-Polish Raw, Motion-Driven",Interactive Product Demo + Storytelling,N/A - Canvas focused,Neutral canvas + full-spectrum color picker + tool panel dark,Pressure sensitivity. Infinite canvas (pan/zoom). Layer management. Undo history. Export PNG/PSD/SVG.
129,Music Creation & Beat Maker,"music, beat, daw, garageband, create, loop, sample, instrument, track, compose, record, midi",Dark Mode (OLED) + Motion-Driven,"Cyberpunk UI, Glassmorphism",Interactive Product Demo + Storytelling,N/A - DAW focused,Dark studio background + track colors rainbow + waveform accent + BPM pulse,Touch piano and drum pad. Loop browser. MIDI support. Export MP3/WAV. Low-latency audio engine.
130,Meme & Sticker Maker,"meme, sticker, maker, funny, caption, template, edit, share, viral, emoji, creator, reaction",Vibrant & Block-based + Flat Design,"Gen Z Chaos, Claymorphism",Feature-Rich Showcase + Social Proof,N/A - Creator focused,Bold primary + comedic yellow + viral red + high saturation accent,Template library. Caption text overlay. Font variety. Reaction sticker packs. Share to all platforms. Fast creation.
131,AI Photo & Avatar Generator,"ai, photo, avatar, lensa, portrait, generate, selfie, style, filter, prisma, art",AI-Native UI + Aurora UI,"Glassmorphism, Minimalism",Feature-Rich Showcase + Social Proof,N/A - Generation focused,AI purple + aurora gradients + before/after neutral,Style selection. Multiple output variations. Privacy policy prominent. Fast generation. Credits/subscription system.
132,Link-in-Bio Page Builder,"bio, link, linktree, personal, page, creator, social, portfolio, profile, landing, custom",Vibrant & Block-based + Bento Box Grid,"Minimalism, Glassmorphism",Conversion-Optimized + Social Proof,Analytics (click tracking),Brand-customizable + accent link color + clean white canvas,Drag-drop builder. Theme templates. Click analytics. Custom domain. Social icon integration. QR code export.
133,Wardrobe & Outfit Planner,"wardrobe, outfit, fashion, clothes, closet, style, wear, plan, capsule, ootd, lookbook",Minimalism + Motion-Driven,"Aurora UI, Soft UI Evolution",Storytelling-Driven + Feature-Rich,N/A - Wardrobe focused,Clean fashion neutral + full clothes color palette + accent,Photo catalog of clothes. AI outfit suggestions. Calendar integration. Capsule wardrobe. Season filtering.
134,Plant Care Tracker,"plant, care, water, garden, tracker, reminder, species, photo, grow, health, planta",Organic Biophilic + Soft UI Evolution,"Claymorphism, Flat Design",Storytelling-Driven + Social Proof,N/A - Plant collection focused,Nature greens + earth brown + sunny yellow reminder + water blue,Plant database with care guides. Watering reminders. Growth photo timeline. AI health diagnosis. Collection sharing.
135,Book & Reading Tracker,"book, reading, tracker, goodreads, library, shelf, progress, review, notes, goal, literature",Swiss Modernism 2.0 + Minimalism,"E-Ink Paper, Soft UI Evolution",Social Proof-Focused + Feature-Rich,N/A - Library focused,Warm paper white + ink brown + reading progress green + book cover colors,Barcode scan to add. Progress percentage. Annual reading goal. Notes and quotes. Friends activity. Genre stats.
136,Couple & Relationship App,"couple, relationship, partner, love, date, anniversary, memory, shared, intimate, between",Aurora UI + Soft UI Evolution,"Claymorphism, Glassmorphism",Storytelling-Driven + Social Proof,N/A - Couple focused,Warm romantic pink/rose + soft gradient + memory photo tones,Shared timeline. Anniversary countdowns. Secret chat. Photo albums. Love language quiz. Date night ideas.
137,Family Calendar & Chores,"family, calendar, chores, tasks, household, shared, kids, schedule, cozi, organize, member",Flat Design + Claymorphism,"Accessible & Ethical, Vibrant & Block-based",Feature-Rich Showcase + Social Proof,N/A - Family hub focused,Warm playful + member color coding + chore completion green,Member color coding. Chore assignment rotation. Recurring events. Shared shopping list. Allowance tracking.
138,Mood Tracker,"mood, emotion, feeling, mental, daily, journal, wellbeing, check-in, log, track, daylio",Soft UI Evolution + Minimalism,"Aurora UI, Neumorphism",Storytelling-Driven + Social Proof,N/A - Mood chart focused,Emotion gradient (blue sad to yellow happy) + pastel per mood + insight accent,One-tap daily check-in. Emotion wheel selector. Mood calendar heatmap. Pattern insights. Export and share.
139,Gift & Wishlist,"gift, wishlist, present, birthday, occasion, registry, idea, shop, list, share, surprise",Vibrant & Block-based + Soft UI Evolution,"Claymorphism, Flat Design",Minimal & Direct + Conversion,N/A - List focused,Celebration warm pink/gold/red + category colors + surprise accent,Add from any URL. Price range filter. Reserved-by-others system. Occasion calendar. Collaborative list. Surprise mode.
140,Running & Cycling GPS,"running, cycling, gps, strava, track, route, speed, distance, cadence, pace, workout, sport",Dark Mode (OLED) + Vibrant & Block-based,"Motion-Driven, Glassmorphism",Feature-Rich Showcase + Social Proof,Performance Analytics,Energetic orange + map accent + pace zones (green/yellow/red),Live GPS tracking. Route map. Auto-pause detection. Segment leaderboards. Training zones. Social feed. Garmin sync.
141,Yoga & Stretching Guide,"yoga, stretch, flexibility, pose, asana, guided, session, calm, routine, wellness, down-dog",Organic Biophilic + Soft UI Evolution,"Neumorphism, Minimalism",Storytelling-Driven + Social Proof,N/A - Session focused,Earth calming sage/terracotta/cream + breathing gradient + warm accent,Pose library with illustrations. Guided sessions with audio. Breathing exercises. Progress calendar. Beginner to advanced.
142,Sleep Tracker,"sleep, tracker, alarm, cycle, quality, snore, analysis, rem, deep, smart, wake, insomnia",Dark Mode (OLED) + Neumorphism,"Glassmorphism, Minimalism",Feature-Rich Showcase + Social Proof,Healthcare Analytics,Deep midnight blue + stars/moon accent + sleep quality gradient (poor red to great green),Sleep cycle detection. Smart alarm wakes at light sleep. Snore detection. Weekly trends. Apple Health integration.
143,Calorie & Nutrition Counter,"calorie, nutrition, food, diet, macro, protein, carb, fat, log, fitness, myfitnesspal",Flat Design + Vibrant & Block-based,"Minimalism, Claymorphism",Feature-Rich Showcase + Social Proof,Healthcare Analytics,"Healthy green + macro colors (protein blue, carb orange, fat yellow) + progress circle",Barcode scanner food log. Large database. Macro goals. Restaurant lookup. Recipe builder. AI photo food logging.
144,Period & Cycle Tracker,"period, cycle, menstrual, fertility, ovulation, pms, log, women, health, flo, clue, hormone",Soft UI Evolution + Aurora UI,"Accessible & Ethical, Claymorphism",Social Proof-Focused + Trust,Healthcare Analytics,Rose/blush + lavender + fertility green + soft calendar tones,Cycle prediction. Symptom logging. Fertility window. Personalized insights. Privacy-first. Partner sharing option.
145,Medication & Pill Reminder,"medication, pill, reminder, dose, schedule, prescription, drug, health, medisafe, refill",Accessible & Ethical + Flat Design,"Minimalism, Trust & Authority",Trust & Authority + Feature-Rich,N/A - Schedule focused,Medical trust blue + missed alert red + taken green + clean white,Multi-medication schedule. Caregiver sharing. Refill reminders. Drug interaction warnings. Large touch targets.
146,Water & Hydration Reminder,"water, hydration, drink, reminder, daily, tracker, glasses, intake, health, cup, aqua",Claymorphism + Vibrant & Block-based,"Flat Design, Micro-interactions",Minimal & Direct + Demo,N/A - Daily goal focused,Refreshing blue + water wave animation + goal progress accent,Tap to log quickly. Animated fill visualization. Custom reminders. Goal by weight/weather. Streak system. Widget.
147,Fasting & Intermittent Timer,"fasting, intermittent, 16:8, timer, fast, eating, window, keto, diet, zero, weight, protocol",Minimalism + Dark Mode (OLED),"Neumorphism, Flat Design",Feature-Rich Showcase + Social Proof,N/A - Timer focused,Fasting deep blue/purple + eating window green + timeline neutral,"Protocol selector (16:8, 18:6, OMAD). Circular countdown timer. Fasting history log. Tips during fast. Electrolytes."
148,Anonymous Community / Confession,"anonymous, community, confess, whisper, secret, vent, share, safe, private, social, yikyak",Dark Mode (OLED) + Minimalism,"Glassmorphism, Soft UI Evolution",Social Proof-Focused + Feature-Rich,User Behavior Analytics,Dark protective + subtle gradient + upvote green + empathy warm accent,Anonymous posting with moderation. Safety reporting. Reaction system. Trending topics. Mental health resources link.
149,Local Events & Discovery,"local, events, discovery, meetup, nearby, social, city, activities, calendar, community, explore",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Flat Design",Hero-Centric Design + Feature-Rich,Event Analytics,City vibrant + event category colors + map accent + date highlight,Location-based discovery. Category filters. RSVP flow. Map view. Friend attendance. Organizer tools. Reminders.
150,Study Together / Virtual Coworking,"study, focus, cowork, pomodoro, virtual, together, session, accountability, live, stream, room",Minimalism + Soft UI Evolution,"Flat Design, Dark Mode (OLED)",Social Proof-Focused + Feature-Rich,User Behavior Analytics,Calm focus blue + session progress indicator + ambient warm neutrals,Live study rooms with video/avatar presence. Shared focus timer. Ambient music. Goals sharing. Streak accountability.
151,Coding Challenge & Practice,"coding, leetcode, challenge, algorithm, practice, programming, competitive, skill, interview, problem",Dark Mode (OLED) + Cyberpunk UI,"Minimalism, Flat Design",Feature-Rich Showcase + Social Proof,Student Analytics,Code editor dark + success green + difficulty gradient (easy green / medium amber / hard red),Code editor with syntax highlight. Multiple languages. Hint system. Solution explanation. Company tags. Contest mode.
152,Kids Learning (ABC & Math),"kids, children, learning, abc, math, phonics, numbers, education, games, preschool, early",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Social Proof-Focused + Trust,Parent Dashboard,Bright primary + child-safe pastels + reward gold + interactive accent,Age-appropriate UI for 2-8. No ads. No dark patterns. Curriculum aligned. Parent progress reports. Reward system.
153,Music Instrument Learning,"music, instrument, piano, guitar, learn, lesson, tutorial, notes, play, chord, practice, simply",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), Soft UI Evolution",Interactive Product Demo + Social Proof,Learning Analytics,Musical warm deep red/brown + note color system + skill progress bar,Interactive instrument on-screen. Sheet music display. Song library. Slow-tempo practice. Recording and playback. Teacher mode.
154,Parking Finder,"parking, spot, finder, map, pay, meter, garage, location, car, reserve, spothero",Minimalism + Glassmorphism,"Flat Design, Micro-interactions",Conversion-Optimized + Feature-Rich,Real-Time Monitoring + Map,Trust blue + available green + occupied red + map neutral,Real-time availability. In-app navigation. Payment integration. Parking timer alert. Favorite spots. Street vs garage.
155,Public Transit Guide,"transit, bus, metro, subway, train, route, schedule, map, city, commute, trip, citymapper",Flat Design + Accessible & Ethical,"Minimalism, Motion-Driven",Feature-Rich Showcase + Interactive Demo,Real-Time Monitoring + Map,Transit brand line colors + real-time indicator green/red + map neutral,Real-time arrivals. Offline maps. Disruption alerts. Multi-modal routing. Fare calculation. Accessibility features.
156,Road Trip Planner,"road, trip, drive, route, planner, travel, stop, map, adventure, scenic, car, wanderlog",Aurora UI + Organic Biophilic,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven + Hero-Centric,N/A - Trip focused,Adventure warm sunset orange + map teal + stop markers + road neutral,Route planning with stops. Point-of-interest discovery. Gas/food/hotel along route. Offline maps. Trip sharing.
157,VPN & Privacy Tool,"vpn, privacy, secure, anonymous, encrypt, proxy, ip, protect, shield, network, nordvpn",Minimalism + Dark Mode (OLED),"Cyberpunk UI, Trust & Authority",Trust & Authority + Conversion-Optimized,N/A - Connection focused,Dark shield blue + connected green + disconnected red + trust accent,One-tap connect. Server selection by country. No-log policy prominent. Speed indicator. Kill switch. Protocol choice.
158,Emergency SOS & Safety,"emergency, sos, safety, alert, location, help, danger, crisis, first-aid, guard, bsafe",Accessible & Ethical + Flat Design,"Dark Mode (OLED), Minimalism",Trust & Authority + Social Proof,N/A - Safety focused,Alert red + safety blue + location green + high contrast critical,One-tap SOS. Emergency contacts auto-notify. Live location sharing. Fake call feature. Safe walk mode. Local emergency numbers.
159,Wallpaper & Theme App,"wallpaper, theme, background, customize, aesthetic, home-screen, lock-screen, widget, design, zedge",Vibrant & Block-based + Aurora UI,"Glassmorphism, Motion-Driven",Feature-Rich Showcase + Social Proof,N/A - Gallery focused,Content-driven + trending aesthetic palettes + download accent,Category browsing. Preview on device. Daily wallpaper auto-set. Widget matching. Creator uploads. Resolution auto-fit.
160,White Noise & Ambient Sound,"white noise, ambient, sound, sleep, focus, rain, nature, relax, concentration, background, noisli",Minimalism + Dark Mode (OLED),"Neumorphism, Organic Biophilic",Minimal & Direct + Social Proof,N/A - Player focused,Calming dark + ambient texture visual + subtle sound wave + sleep blue,Sound mixer with multiple simultaneous layers. Sleep timer with fade. Custom soundscapes. Offline. Background audio.
161,Home Decoration & Interior Design,"home, interior, decor, design, furniture, room, renovation, ar, plan, inspire, 3d, houzz",Minimalism + 3D Product Preview,"Organic Biophilic, Aurora UI",Storytelling-Driven + Feature-Rich,N/A - Project focused,Neutral interior palette + material texture accent + AR blue,AR room visualization. Style quiz. Product catalog with purchase links. 3D room planner. Mood board. Before/after.
1 No Product Type Keywords Primary Style Recommendation Secondary Styles Landing Page Pattern Dashboard Style (if applicable) Color Palette Focus Key Considerations
2 1 SaaS (General) app, b2b, cloud, general, saas, software, subscription Glassmorphism + Flat Design Soft UI Evolution, Minimalism Hero + Features + CTA Data-Dense + Real-Time Monitoring Trust blue + accent contrast Balance modern feel with clarity. Focus on CTAs.
3 2 Micro SaaS app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription Flat Design + Vibrant & Block Motion-Driven, Micro-interactions Minimal & Direct + Demo Executive Dashboard Vibrant primary + white space Keep simple, show product quickly. Speed is key.
4 3 E-commerce buy, commerce, e, ecommerce, products, retail, sell, shop, store Vibrant & Block-based Aurora UI, Motion-Driven Feature-Rich Showcase Sales Intelligence Dashboard Brand primary + success green Engagement & conversions. High visual hierarchy.
5 4 E-commerce Luxury buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store Liquid Glass + Glassmorphism 3D & Hyperrealism, Aurora UI Feature-Rich Showcase Sales Intelligence Dashboard Premium colors + minimal accent Elegance & sophistication. Premium materials.
6 5 B2B Service appointment, b, b2b, booking, business, consultation, corporate, enterprise, service Trust & Authority + Minimal Feature-Rich, Conversion-Optimized Feature-Rich Showcase Sales Intelligence Dashboard Professional blue + neutral grey Credibility essential. Clear ROI messaging.
7 6 Financial Dashboard admin, analytics, dashboard, data, financial, panel Dark Mode (OLED) + Data-Dense Minimalism, Accessible & Ethical N/A - Dashboard focused Financial Dashboard Dark bg + red/green alerts + trust blue High contrast, real-time updates, accuracy paramount.
8 7 Analytics Dashboard admin, analytics, dashboard, data, panel Data-Dense + Heat Map & Heatmap Minimalism, Dark Mode (OLED) N/A - Analytics focused Drill-Down Analytics + Comparative Cool→Hot gradients + neutral grey Clarity > aesthetics. Color-coded data priority.
9 8 Healthcare App app, clinic, health, healthcare, medical, patient Neumorphism + Accessible & Ethical Soft UI Evolution, Claymorphism (for patients) Social Proof-Focused User Behavior Analytics Calm blue + health green + trust Accessibility mandatory. Calming aesthetic.
10 9 Educational App app, course, education, educational, learning, school, training Claymorphism + Micro-interactions Vibrant & Block-based, Flat Design Storytelling-Driven User Behavior Analytics Playful colors + clear hierarchy Engagement & ease of use. Age-appropriate design.
11 10 Creative Agency agency, creative, design, marketing, studio Brutalism + Motion-Driven Retro-Futurism, Storytelling-Driven Storytelling-Driven N/A - Portfolio focused Bold primaries + artistic freedom Differentiation key. Wow-factor necessary.
12 11 Portfolio/Personal creative, personal, portfolio, projects, showcase, work Motion-Driven + Minimalism Brutalism, Aurora UI Storytelling-Driven N/A - Personal branding Brand primary + artistic interpretation Showcase work. Personality shine through.
13 12 Gaming entertainment, esports, game, gaming, play 3D & Hyperrealism + Retro-Futurism Motion-Driven, Vibrant & Block Feature-Rich Showcase N/A - Game focused Vibrant + neon + immersive colors Immersion priority. Performance critical.
14 13 Government/Public Service appointment, booking, consultation, government, public, service Accessible & Ethical + Minimalism Flat Design, Inclusive Design Minimal & Direct Executive Dashboard Professional blue + high contrast WCAG AAA mandatory. Trust paramount.
15 14 Fintech/Crypto banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3 Glassmorphism + Dark Mode (OLED) Retro-Futurism, Motion-Driven Conversion-Optimized Real-Time Monitoring + Predictive Dark tech colors + trust + vibrant accents Security perception. Real-time data critical.
16 15 Social Media App app, community, content, entertainment, media, network, sharing, social, streaming, users, video Vibrant & Block-based + Motion-Driven Aurora UI, Micro-interactions Feature-Rich Showcase User Behavior Analytics Vibrant + engagement colors Engagement & retention. Addictive design ethics.
17 16 Productivity Tool collaboration, productivity, project, task, tool, workflow Flat Design + Micro-interactions Minimalism, Soft UI Evolution Interactive Product Demo Drill-Down Analytics Clear hierarchy + functional colors Ease of use. Speed & efficiency focus.
18 17 Design System/Component Library component, design, library, system Minimalism + Accessible & Ethical Flat Design, Zero Interface Feature-Rich Showcase N/A - Dev focused Clear hierarchy + code-like structure Consistency. Developer-first approach.
19 18 AI/Chatbot Platform ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform AI-Native UI + Minimalism Zero Interface, Glassmorphism Interactive Product Demo AI/ML Analytics Dashboard Neutral + AI Purple (#6366F1) Conversational UI. Streaming text. Context awareness. Minimal chrome.
20 19 NFT/Web3 Platform nft, platform, web Cyberpunk UI + Glassmorphism Aurora UI, 3D & Hyperrealism Feature-Rich Showcase Crypto/Blockchain Dashboard Dark + Neon + Gold (#FFD700) Wallet integration. Transaction feedback. Gas fees display. Dark mode essential.
21 20 Creator Economy Platform creator, economy, platform Vibrant & Block-based + Bento Box Grid Motion-Driven, Aurora UI Social Proof-Focused User Behavior Analytics Vibrant + Brand colors Creator profiles. Monetization display. Engagement metrics. Social proof.
22 21 Remote Work/Collaboration Tool collaboration, remote, tool, work Soft UI Evolution + Minimalism Glassmorphism, Micro-interactions Feature-Rich Showcase Drill-Down Analytics Calm Blue + Neutral grey Real-time collaboration. Status indicators. Video integration. Notification management.
23 22 Mental Health App app, health, mental Neumorphism + Accessible & Ethical Claymorphism, Soft UI Evolution Social Proof-Focused Healthcare Analytics Calm Pastels + Trust colors Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory.
24 23 Pet Tech App app, pet, tech Claymorphism + Vibrant & Block-based Micro-interactions, Flat Design Storytelling-Driven User Behavior Analytics Playful + Warm colors Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration.
25 24 Smart Home/IoT Dashboard admin, analytics, dashboard, data, home, iot, panel, smart Glassmorphism + Dark Mode (OLED) Minimalism, AI-Native UI Interactive Product Demo Real-Time Monitoring Dark + Status indicator colors Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions.
26 25 EV/Charging Ecosystem charging, ecosystem, ev Minimalism + Aurora UI Glassmorphism, Organic Biophilic Hero-Centric Design Energy/Utilities Dashboard Electric Blue (#009CD1) + Green Charging station maps. Range estimation. Cost calculation. Environmental impact.
27 26 Subscription Box Service appointment, booking, box, consultation, membership, plan, recurring, service, subscription Vibrant & Block-based + Motion-Driven Claymorphism, Aurora UI Feature-Rich Showcase E-commerce Analytics Brand + Excitement colors Unboxing experience. Personalization quiz. Subscription management. Product reveals.
28 27 Podcast Platform platform, podcast Dark Mode (OLED) + Minimalism Motion-Driven, Vibrant & Block-based Storytelling-Driven Media/Entertainment Dashboard Dark + Audio waveform accents Audio player UX. Episode discovery. Creator tools. Analytics for podcasters.
29 28 Dating App app, dating Vibrant & Block-based + Motion-Driven Aurora UI, Glassmorphism Social Proof-Focused User Behavior Analytics Warm + Romantic (Pink/Red gradients) Profile cards. Swipe interactions. Match animations. Safety features. Video chat.
30 29 Micro-Credentials/Badges Platform badges, credentials, micro, platform Minimalism + Flat Design Accessible & Ethical, Swiss Modernism 2.0 Trust & Authority Education Dashboard Trust Blue + Gold (#FFD700) Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration.
31 30 Knowledge Base/Documentation base, documentation, knowledge Minimalism + Accessible & Ethical Swiss Modernism 2.0, Flat Design FAQ/Documentation N/A - Documentation focused Clean hierarchy + minimal color Search-first. Clear navigation. Code highlighting. Version switching. Feedback system.
32 31 Hyperlocal Services appointment, booking, consultation, hyperlocal, service, services Minimalism + Vibrant & Block-based Micro-interactions, Flat Design Conversion-Optimized Drill-Down Analytics + Map Location markers + Trust colors Map integration. Service categories. Provider profiles. Booking system. Reviews.
33 32 Beauty/Spa/Wellness Service appointment, beauty, booking, consultation, service, spa, wellness Soft UI Evolution + Neumorphism Glassmorphism, Minimalism Hero-Centric Design + Social Proof User Behavior Analytics Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery.
34 33 Luxury/Premium Brand brand, elegant, exclusive, high-end, luxury, premium Liquid Glass + Glassmorphism Minimalism, 3D & Hyperrealism Storytelling-Driven + Feature-Rich Sales Intelligence Dashboard Black + Gold (#FFD700) + White + Minimal accent Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel.
35 34 Restaurant/Food Service appointment, booking, consultation, delivery, food, menu, order, restaurant, service Vibrant & Block-based + Motion-Driven Claymorphism, Flat Design Hero-Centric Design + Conversion N/A - Booking focused Warm colors (Orange Red Brown) + appetizing imagery Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent.
36 35 Fitness/Gym App app, exercise, fitness, gym, health, workout Vibrant & Block-based + Dark Mode (OLED) Motion-Driven, Neumorphism Feature-Rich Showcase User Behavior Analytics Energetic (Orange #FF6B35 Electric Blue) + Dark bg Progress tracking. Workout plans. Community features. Achievements. Motivational design.
37 36 Real Estate/Property buy, estate, housing, property, real, real-estate, rent Glassmorphism + Minimalism Motion-Driven, 3D & Hyperrealism Hero-Centric Design + Feature-Rich Sales Intelligence Dashboard Trust Blue (#0077B6) + Gold accents + White Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery.
38 37 Travel/Tourism Agency agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation Aurora UI + Motion-Driven Vibrant & Block-based, Glassmorphism Storytelling-Driven + Hero-Centric Booking Analytics Vibrant destination colors + Sky Blue + Warm accents Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first.
39 38 Hotel/Hospitality hospitality, hotel Liquid Glass + Minimalism Glassmorphism, Soft UI Evolution Hero-Centric Design + Social Proof Revenue Management Dashboard Warm neutrals + Gold (#D4AF37) + Brand accent Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery.
40 39 Wedding/Event Planning conference, event, meetup, planning, registration, ticket, wedding Soft UI Evolution + Aurora UI Glassmorphism, Motion-Driven Storytelling-Driven + Social Proof N/A - Planning focused Soft Pink (#FFD6E0) + Gold + Cream + Sage Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic.
41 40 Legal Services appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services Trust & Authority + Minimalism Accessible & Ethical, Swiss Modernism 2.0 Trust & Authority + Minimal Case Management Dashboard Navy Blue (#1E3A5F) + Gold + White Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery.
42 41 Insurance Platform insurance, platform Trust & Authority + Flat Design Accessible & Ethical, Minimalism Conversion-Optimized + Trust Claims Analytics Dashboard Trust Blue (#0066CC) + Green (security) + Neutral Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges.
43 42 Banking/Traditional Finance banking, finance, traditional Minimalism + Accessible & Ethical Trust & Authority, Dark Mode (OLED) Trust & Authority + Feature-Rich Financial Dashboard Navy (#0A1628) + Trust Blue + Gold accents Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount.
44 43 Online Course/E-learning course, e, learning, online Claymorphism + Vibrant & Block-based Motion-Driven, Flat Design Feature-Rich Showcase + Social Proof Education Dashboard Vibrant learning colors + Progress green Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification.
45 44 Non-profit/Charity charity, non, profit Accessible & Ethical + Organic Biophilic Minimalism, Storytelling-Driven Storytelling-Driven + Trust Donation Analytics Dashboard Cause-related colors + Trust + Warm Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection.
46 45 Music Streaming music, streaming Dark Mode (OLED) + Vibrant & Block-based Motion-Driven, Aurora UI Feature-Rich Showcase Media/Entertainment Dashboard Dark (#121212) + Vibrant accents + Album art colors Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations.
47 46 Video Streaming/OTT ott, streaming, video Dark Mode (OLED) + Motion-Driven Glassmorphism, Vibrant & Block-based Hero-Centric Design + Feature-Rich Media/Entertainment Dashboard Dark bg + Content poster colors + Brand accent Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy.
48 47 Job Board/Recruitment board, job, recruitment Flat Design + Minimalism Vibrant & Block-based, Accessible & Ethical Conversion-Optimized + Feature-Rich HR Analytics Dashboard Professional Blue + Success Green + Neutral Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights.
49 48 Marketplace (P2P) buyers, listings, marketplace, p, platform, sellers Vibrant & Block-based + Flat Design Micro-interactions, Trust & Authority Feature-Rich Showcase + Social Proof E-commerce Analytics Trust colors + Category colors + Success green Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges.
50 49 Logistics/Delivery delivery, logistics Minimalism + Flat Design Dark Mode (OLED), Micro-interactions Feature-Rich Showcase + Conversion Real-Time Monitoring + Route Analytics Blue (#2563EB) + Orange (tracking) + Green (delivered) Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration.
51 50 Agriculture/Farm Tech agriculture, farm, tech Organic Biophilic + Flat Design Minimalism, Accessible & Ethical Feature-Rich Showcase + Trust IoT Sensor Dashboard Earth Green (#4A7C23) + Brown + Sky Blue Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery.
52 51 Construction/Architecture architecture, construction Minimalism + 3D & Hyperrealism Brutalism, Swiss Modernism 2.0 Hero-Centric Design + Feature-Rich Project Management Dashboard Grey (#4A4A4A) + Orange (safety) + Blueprint Blue Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic.
53 52 Automotive/Car Dealership automotive, car, dealership Motion-Driven + 3D & Hyperrealism Dark Mode (OLED), Glassmorphism Hero-Centric Design + Feature-Rich Sales Intelligence Dashboard Brand colors + Metallic accents + Dark/Light Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery.
54 53 Photography Studio photography, studio Motion-Driven + Minimalism Aurora UI, Glassmorphism Storytelling-Driven + Hero-Centric N/A - Portfolio focused Black + White + Minimal accent Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery.
55 54 Coworking Space coworking, space Vibrant & Block-based + Glassmorphism Minimalism, Motion-Driven Hero-Centric Design + Feature-Rich Occupancy Dashboard Energetic colors + Wood tones + Brand accent Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour.
56 55 Home Services (Plumber/Electrician) appointment, booking, consultation, electrician, home, plumber, service, services Flat Design + Trust & Authority Minimalism, Accessible & Ethical Conversion-Optimized + Trust Service Analytics Trust Blue + Safety Orange + Professional grey Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals.
57 56 Childcare/Daycare childcare, daycare Claymorphism + Vibrant & Block-based Soft UI Evolution, Accessible & Ethical Social Proof-Focused + Trust Parent Dashboard Playful pastels + Safe colors + Warm accents Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery.
58 57 Senior Care/Elderly care, elderly, senior Accessible & Ethical + Soft UI Evolution Minimalism, Neumorphism Trust & Authority + Social Proof Healthcare Analytics Calm Blue + Warm neutrals + Large text Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first.
59 58 Medical Clinic clinic, medical Accessible & Ethical + Minimalism Neumorphism, Trust & Authority Trust & Authority + Conversion Healthcare Analytics Medical Blue (#0077B6) + Trust White + Calm Green Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals.
60 59 Pharmacy/Drug Store drug, pharmacy, store Flat Design + Accessible & Ethical Minimalism, Trust & Authority Conversion-Optimized + Trust Inventory Dashboard Pharmacy Green + Trust Blue + Clean White Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications.
61 60 Dental Practice dental, practice Soft UI Evolution + Minimalism Accessible & Ethical, Trust & Authority Social Proof-Focused + Conversion Patient Analytics Fresh Blue + White + Smile Yellow accent Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery.
62 61 Veterinary Clinic clinic, veterinary Claymorphism + Accessible & Ethical Soft UI Evolution, Flat Design Social Proof-Focused + Trust Pet Health Dashboard Caring Blue + Pet-friendly colors + Warm accents Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery.
63 62 Florist/Plant Shop florist, plant, shop Organic Biophilic + Vibrant & Block-based Aurora UI, Motion-Driven Hero-Centric Design + Conversion E-commerce Analytics Natural Green + Floral pinks/purples + Earth tones Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery.
64 63 Bakery/Cafe bakery, cafe Vibrant & Block-based + Soft UI Evolution Claymorphism, Motion-Driven Hero-Centric Design + Conversion N/A - Order focused Warm Brown + Cream + Appetizing accents Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography.
65 64 Brewery/Winery brewery, winery Motion-Driven + Storytelling-Driven Dark Mode (OLED), Organic Biophilic Storytelling-Driven + Hero-Centric N/A - E-commerce focused Deep amber/burgundy + Gold + Craft aesthetic Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery.
66 65 Airline airline, aviation, flight, travel, booking, airport, flying Minimalism + Glassmorphism Motion-Driven, Accessible & Ethical Conversion-Optimized + Feature-Rich Operations Dashboard Sky Blue + Brand colors + Trust accents Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first.
67 66 News/Media Platform content, entertainment, media, news, platform, streaming, video Minimalism + Flat Design Dark Mode (OLED), Accessible & Ethical Hero-Centric Design + Feature-Rich Media Analytics Dashboard Brand colors + High contrast + Category colors Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading.
68 67 Magazine/Blog articles, blog, content, magazine, posts, writing Swiss Modernism 2.0 + Motion-Driven Minimalism, Aurora UI Storytelling-Driven + Hero-Centric Content Analytics Editorial colors + Brand primary + Clean white Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused.
69 68 Freelancer Platform freelancer, platform Flat Design + Minimalism Vibrant & Block-based, Micro-interactions Feature-Rich Showcase + Conversion Marketplace Analytics Professional Blue + Success Green + Neutral Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management.
70 69 Marketing Agency agency, creative, design, marketing, studio Brutalism + Motion-Driven Vibrant & Block-based, Aurora UI Storytelling-Driven + Feature-Rich Campaign Analytics Bold brand colors + Creative freedom Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic.
71 70 Event Management conference, event, management, meetup, registration, ticket Vibrant & Block-based + Motion-Driven Glassmorphism, Aurora UI Hero-Centric Design + Feature-Rich Event Analytics Event theme colors + Excitement accents Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer.
72 71 Membership/Community community, membership Vibrant & Block-based + Soft UI Evolution Bento Box Grid, Micro-interactions Social Proof-Focused + Conversion Community Analytics Community brand colors + Engagement accents Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content.
73 72 Newsletter Platform newsletter, platform Minimalism + Flat Design Swiss Modernism 2.0, Accessible & Ethical Minimal & Direct + Conversion Email Analytics Brand primary + Clean white + CTA accent Subscribe form. Archive. About. Social proof. Sample content. Simple conversion.
74 73 Digital Products/Downloads digital, downloads, products Vibrant & Block-based + Motion-Driven Glassmorphism, Bento Box Grid Feature-Rich Showcase + Conversion E-commerce Analytics Product category colors + Brand + Success green Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews.
75 74 Church/Religious Organization church, organization, religious Accessible & Ethical + Soft UI Evolution Minimalism, Trust & Authority Hero-Centric Design + Social Proof N/A - Community focused Warm Gold + Deep Purple/Blue + White Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery.
76 75 Sports Team/Club club, sports, team Vibrant & Block-based + Motion-Driven Dark Mode (OLED), 3D & Hyperrealism Hero-Centric Design + Feature-Rich Performance Analytics Team colors + Energetic accents Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery.
77 76 Museum/Gallery gallery, museum Minimalism + Motion-Driven Swiss Modernism 2.0, 3D & Hyperrealism Storytelling-Driven + Feature-Rich Visitor Analytics Art-appropriate neutrals + Exhibition accents Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design.
78 77 Theater/Cinema cinema, theater Dark Mode (OLED) + Motion-Driven Vibrant & Block-based, Glassmorphism Hero-Centric Design + Conversion Booking Analytics Dark + Spotlight accents + Gold Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery.
79 78 Language Learning App app, language, learning Claymorphism + Vibrant & Block-based Micro-interactions, Flat Design Feature-Rich Showcase + Social Proof Learning Analytics Playful colors + Progress indicators + Country flags Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges.
80 79 Coding Bootcamp bootcamp, coding Dark Mode (OLED) + Minimalism Cyberpunk UI, Flat Design Feature-Rich Showcase + Social Proof Student Analytics Code editor colors + Brand + Success green Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic.
81 80 Cybersecurity Platform cyber, security, platform Cyberpunk UI + Dark Mode (OLED) Neubrutalism, Minimal & Direct Trust & Authority + Real-Time Real-Time Monitoring + Heat Map Matrix Green + Deep Black + Terminal feel Data density. Threat visualization. Dark mode default.
82 81 Developer Tool / IDE dev, developer, tool, ide Dark Mode (OLED) + Minimalism Flat Design, Bento Box Grid Minimal & Direct + Documentation Real-Time Monitor + Terminal Dark syntax theme colors + Blue focus Keyboard shortcuts. Syntax highlighting. Fast performance.
83 82 Biotech / Life Sciences biotech, biology, science Glassmorphism + Clean Science Minimalism, Organic Biophilic Storytelling-Driven + Research Data-Dense + Predictive Sterile White + DNA Blue + Life Green Data accuracy. Cleanliness. Complex data viz.
84 83 Space Tech / Aerospace aerospace, space, tech Holographic / HUD + Dark Mode Glassmorphism, 3D & Hyperrealism Immersive Experience + Hero Real-Time Monitoring + 3D Deep Space Black + Star White + Metallic High-tech feel. Precision. Telemetry data.
85 84 Architecture / Interior architecture, design, interior Exaggerated Minimalism + High Imagery Swiss Modernism 2.0, Parallax Portfolio Grid + Visuals Project Management + Gallery Monochrome + Gold Accent + High Imagery High-res images. Typography. Space.
86 85 Quantum Computing Interface quantum, computing, physics, qubit, future, science Holographic / HUD + Dark Mode Glassmorphism, Spatial UI Immersive/Interactive Experience 3D Spatial Data + Real-Time Monitor Quantum Blue #00FFFF + Deep Black + Interference patterns Visualize complexity. Qubit states. Probability clouds. High-tech trust.
87 86 Biohacking / Longevity App biohacking, health, longevity, tracking, wellness, science Biomimetic / Organic 2.0 Minimalism, Dark Mode (OLED) Data-Dense + Storytelling Real-Time Monitor + Biological Data Cellular Pink/Red + DNA Blue + Clean White Personal data privacy. Scientific credibility. Biological visualizations.
88 87 Autonomous Drone Fleet Manager drone, autonomous, fleet, aerial, logistics, robotics HUD / Sci-Fi FUI Real-Time Monitor, Spatial UI Real-Time Monitor Geographic + Real-Time Tactical Green #00FF00 + Alert Red + Map Dark Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts.
89 88 Generative Art Platform art, generative, ai, creative, platform, gallery Minimalism (Frame) + Gen Z Chaos Masonry Grid, Dark Mode Bento Grid Showcase Gallery / Portfolio Neutral #F5F5F5 (Canvas) + User Content Content is king. Fast loading. Creator attribution. Minting flow.
90 89 Spatial Computing OS / App spatial, vr, ar, vision, os, immersive, mixed-reality Spatial UI (VisionOS) Glassmorphism, 3D & Hyperrealism Immersive/Interactive Experience Spatial Dashboard Frosted Glass + System Colors + Depth Gaze/Pinch interaction. Depth hierarchy. Environment awareness.
91 90 Sustainable Energy / Climate Tech climate, energy, sustainable, green, tech, carbon Organic Biophilic + E-Ink / Paper Data-Dense, Swiss Modernism Interactive Demo + Data Energy/Utilities Dashboard Earth Green + Sky Blue + Solar Yellow Data transparency. Impact visualization. Low-carbon web design.
92 91 Personal Finance Tracker budget, expense, money, finance, spending, savings, tracker, personal, wallet Glassmorphism + Dark Mode (OLED) Minimalism, Flat Design Interactive Product Demo Financial Dashboard Calm blue + success green + alert red + chart accents Category pie/donut charts. Monthly trend lines. Budget progress bars. Transaction list with swipe actions. Receipt camera. Currency formatting. Recurring entries.
93 92 Chat & Messaging App chat, message, messenger, im, realtime, conversation, inbox, dm, whatsapp, telegram Minimalism + Micro-interactions Glassmorphism, Flat Design Feature-Rich Showcase + Demo User Behavior Analytics Brand primary + bubble contrast (sender/receiver) + typing grey Bubble UI (left/right alignment). Typing indicators. Read receipts (✓✓). Image/file preview. Emoji reactions. Group avatars. Online status dots. Swipe-to-reply.
94 93 Notes & Writing App notes, memo, writing, editor, notebook, markdown, journal, notion, obsidian Minimalism + Flat Design Swiss Modernism 2.0, Soft UI Evolution Minimal & Direct N/A - Editor focused Clean white/cream + minimal accent + editor syntax colors WYSIWYG or Markdown toggle. Folder/tag organization. Full-text search. Cloud sync. Typography-first. Distraction-free zen mode. Slash-command palette.
95 94 Habit Tracker habit, streak, routine, daily, tracker, goals, consistency, discipline Claymorphism + Vibrant & Block-based Micro-interactions, Flat Design Social Proof-Focused + Demo User Behavior Analytics Streak warm (amber/orange) + progress green + motivational accents Streak calendar heatmap. Daily check-in interaction. Gamification (badges/levels/fire). Reminder push. Progress ring charts. Weekly/monthly stats. Motivational micro-copy.
96 95 Food Delivery / On-Demand delivery, food, order, uber-eats, doordash, takeout, on-demand, courier Vibrant & Block-based + Motion-Driven Glassmorphism, Flat Design Hero-Centric Design + Feature-Rich Real-Time Monitoring + Map Appetizing warm (orange/red) + trust blue + map accent Restaurant cards with ratings. Menu category horizontal scroll. Cart bottom sheet. Real-time map tracking + driver ETA. Order status stepper. Rating post-delivery.
97 96 Ride Hailing / Transportation ride, taxi, uber, lyft, transport, carpool, driver, trip, fare Minimalism + Glassmorphism Dark Mode (OLED), Motion-Driven Conversion-Optimized + Demo Real-Time Monitoring + Map Brand primary + map neutral + status indicator colors Map-centric full-screen UI. Pickup/dropoff pins + route polyline. Driver card (photo/rating/vehicle). Fare estimate. Trip timer. Safety SOS button. Payment sheet.
98 97 Recipe & Cooking App recipe, cooking, food, kitchen, cookbook, meal, ingredient, chef Claymorphism + Vibrant & Block-based Soft UI Evolution, Organic Biophilic Hero-Centric Design + Feature-Rich N/A - Content focused Warm food tones (terracotta/sage/cream) + appetizing imagery Step-by-step with checkable instructions. Ingredient list with serving adjuster. Built-in timer per step. Cooking mode (screen-awake + large text). Save/bookmark. Share.
99 98 Meditation & Mindfulness meditation, mindfulness, calm, breathe, wellness, relaxation, sleep, headspace Neumorphism + Soft UI Evolution Aurora UI, Glassmorphism Storytelling-Driven + Social Proof User Behavior Analytics Ultra-calm pastels (lavender/sage/sky) + breathing animation gradient Breathing circle animation. Session duration picker. Ambient sound mixer. Streak/consistency tracking. Guided audio player. Sleep timer. Minimal chrome. Slow easing transitions only.
100 99 Weather App weather, forecast, temperature, climate, rain, sun, location, humidity Glassmorphism + Aurora UI Motion-Driven, Minimalism Hero-Centric Design N/A - Utility focused Atmospheric gradients (sky blue → sunset → storm grey) + temp scale Location auto-detect. Hourly horizontal scroll + daily/weekly list. Animated weather icons. Air quality index. UV/wind/humidity chips. Radar map overlay. Widget-friendly layout.
101 100 Diary & Journal App diary, journal, personal, daily, reflection, mood, gratitude, writing Soft UI Evolution + Minimalism Neumorphism, Sketch Hand-Drawn Storytelling-Driven N/A - Personal focused Warm paper tones (cream/linen) + muted ink + mood-coded accents Calendar month-view entry. Mood tag selector (emoji/color). Photo/voice attachment. Writing prompts. Privacy lock (FaceID/PIN). Search across entries. Export to PDF.
102 101 CRM & Client Management crm, client, customer, sales, pipeline, contact, lead, deal, hubspot Flat Design + Minimalism Soft UI Evolution, Micro-interactions Feature-Rich Showcase + Demo Sales Intelligence Dashboard Professional blue + pipeline stage colors + closed-won green Contact card list with avatar. Pipeline kanban board. Activity timeline. Quick-log (call/email/meeting). Deal amount + probability. Tag/segment filter. Mobile quick-actions.
103 102 Inventory & Stock Management inventory, stock, warehouse, product, barcode, supply, sku, management Flat Design + Minimalism Dark Mode (OLED), Accessible & Ethical Feature-Rich Showcase Real-Time Monitoring + Data-Dense Functional neutral + status traffic-light (green/amber/red) + scanner accent Product list/grid with thumbnails. Barcode/QR scanner. Stock level badges. Low-stock alert banner. Category/location filter. Batch edit. Reorder trigger. Audit log.
104 103 Flashcard & Study Tool flashcard, quiz, study, spaced-repetition, anki, learn, memory, exam Claymorphism + Micro-interactions Vibrant & Block-based, Flat Design Feature-Rich Showcase + Demo Learning Analytics Playful primary + correct green + incorrect red + progress blue 3D card flip animation. Spaced repetition algorithm. Deck browser. Session progress bar. Streak tracking. Timed quiz mode. Share/import decks. Rich text + image cards.
105 104 Booking & Appointment App booking, appointment, schedule, calendar, reservation, slot, service Soft UI Evolution + Flat Design Minimalism, Micro-interactions Conversion-Optimized Drill-Down Analytics Trust blue + available green + booked grey + confirm accent Calendar strip or month picker. Available time-slot grid. Service + staff selector. Confirmation summary. Reminder push. Reschedule/cancel flow. Two-sided (provider ↔ client).
106 105 Invoice & Billing Tool invoice, billing, payment, receipt, freelance, estimate, quote, accounting Minimalism + Flat Design Swiss Modernism 2.0, Accessible & Ethical Conversion-Optimized + Trust Financial Dashboard Professional navy + paid green + overdue red + neutral grey Invoice template with line items. Tax/discount calculation. Status badges (Draft/Sent/Paid/Overdue). PDF export + share. Payment link generation. Client address book. Recurring invoices.
107 106 Grocery & Shopping List grocery, shopping, list, supermarket, checklist, pantry, meal-plan, buy Flat Design + Vibrant & Block-based Claymorphism, Micro-interactions Minimal & Direct + Demo N/A - List focused Fresh green + food-category colors + checkmark accent Category-grouped list. Tap-to-check interaction (with strikethrough). Quantity stepper. Share list with family. Store aisle sorting. Barcode scan to add. Frequently bought suggestions.
108 107 Timer & Pomodoro timer, pomodoro, countdown, stopwatch, focus, clock, productivity, interval Minimalism + Neumorphism Dark Mode (OLED), Micro-interactions Minimal & Direct N/A - Utility focused High-contrast on dark + focus red/amber + break green Large centered countdown digits. Circular progress ring. Session/break auto-switch. Session history log. Custom interval settings. Sound + haptic alerts. Focus stats chart.
109 108 Parenting & Baby Tracker baby, parenting, child, feeding, sleep, diaper, milestone, family, newborn Claymorphism + Soft UI Evolution Vibrant & Block-based, Accessible & Ethical Social Proof-Focused + Trust User Behavior Analytics Soft pastels (baby pink/sky blue/mint/peach) + warm accents Feed/sleep/diaper quick-log buttons. Growth percentile chart. Milestone timeline with photos. Multiple child profiles. Partner invite + shared access. Pediatric reference. One-handed operation.
110 109 Scanner & Document Manager scanner, document, ocr, pdf, scan, camera, file, archive, digitize Minimalism + Flat Design Dark Mode (OLED), Accessible & Ethical Feature-Rich Showcase + Demo N/A - Tool focused Clean white + camera viewfinder accent + file-type color coding Camera capture with auto-edge detection. Crop/rotate/enhance. OCR text extraction overlay. PDF multi-page creation. Folder tree organization. Cloud sync. Share/export. Batch scan mode.
111 110 Calendar & Scheduling App calendar, scheduling, planner, agenda, events, reminder, appointment, organize, date, sync Flat Design + Micro-interactions Minimalism, Soft UI Evolution Feature-Rich Showcase + Demo N/A - Calendar focused Clean blue + event category accent colors + success green Event color coding. Week/month/day views. Recurring events. Conflict detection. Multi-calendar sync.
112 111 Password Manager password, security, vault, credentials, login, secure, encrypt, keychain, 2fa, biometric Minimalism + Accessible & Ethical Dark Mode (OLED), Trust & Authority Trust & Authority + Feature-Rich N/A - Vault focused Trust blue + security green + dark neutral Security-first. Zero-knowledge architecture. Biometric unlock. Breach alert dashboard. Password generator.
113 112 Expense Splitter / Bill Split split, expense, bill, aa, share, friends, group, settle, debt, payment, owe Flat Design + Vibrant & Block-based Minimalism, Micro-interactions Minimal & Direct + Demo N/A - Balance focused Success green + alert red + neutral grey + avatar accent colors Group expense tracking. Debt simplification algorithm. Payment reminders. Multi-currency. Receipt photo import.
114 113 Voice Recorder & Memo voice, recorder, memo, audio, transcription, dictate, recording, microphone, note, otter Minimalism + AI-Native UI Flat Design, Dark Mode (OLED) Interactive Product Demo + Minimal N/A - Recording focused Clean white + recording red + waveform accent Waveform display. Background recording. Auto-transcription (AI). Tag/organize. Cloud sync.
115 114 Bookmark & Read-Later bookmark, read-later, save, article, pocket, link, reading, archive, collection, raindrop Minimalism + Flat Design Editorial Grid, Swiss Modernism 2.0 Minimal & Direct + Demo N/A - List focused Paper warm white + ink neutral + minimal accent + tag colors Fast save via share sheet. Article distraction-free view. Tags and collections. Offline sync. Reading progress.
116 115 Translator App translate, language, text, voice, ocr, dictionary, multilingual, real-time, detect, deepl Flat Design + AI-Native UI Minimalism, Micro-interactions Feature-Rich Showcase + Interactive Demo N/A - Utility focused Global blue + neutral grey + language flag accent Real-time camera translation (OCR). Voice input and output. Offline mode. Conversation mode. Phrasebook.
117 116 Calculator & Unit Converter calculator, converter, unit, math, currency, measurement, scientific, formula, percentage Neumorphism + Minimalism Flat Design, Dark Mode (OLED) Minimal & Direct N/A - Utility focused Dark functional + orange operation keys + clear button hierarchy Scientific mode toggle. Live currency rates. Calculation history. Widget support. Gesture input.
118 117 Alarm & World Clock alarm, clock, world, timezone, timer, wake, sleep, schedule, reminder, bedtime Dark Mode (OLED) + Minimalism Neumorphism, Flat Design Minimal & Direct N/A - Utility focused Deep dark + ambient glow accent + timezone gradient Gentle wake (gradual volume). Timezone visualizer. Sleep tracking integration. Smart alarm skip. Bedtime mode.
119 118 File Manager & Transfer file, manager, transfer, folder, document, storage, cloud, share, organize, compress Flat Design + Minimalism Accessible & Ethical, Dark Mode (OLED) Feature-Rich Showcase + Demo N/A - File tree focused Functional neutral + file type color coding (PDF orange, doc blue, image purple) Folder tree navigation. File type preview. Wireless P2P transfer. Cloud integration. Compress and extract.
120 119 Email Client email, mail, inbox, compose, thread, newsletter, filter, reply, gmail, spark, superhuman Flat Design + Minimalism Micro-interactions, Soft UI Evolution Feature-Rich Showcase + Demo N/A - Inbox focused Clean white + brand primary + priority red + snooze amber Unified inbox. Swipe actions (archive/delete/snooze). Priority sorting. Smart reply. Unsubscribe tool.
121 120 Casual Puzzle Game puzzle, casual, match, brain, game, relaxing, level, tiles, logic, block, three Claymorphism + Vibrant & Block-based Micro-interactions, Motion-Driven Feature-Rich Showcase + Social Proof N/A - Game focused Cheerful pastels + progression gradient + reward gold + bright accent Satisfying match/clear animations. Progressive difficulty. Daily challenges. No-skip tutorials. Offline play.
122 121 Trivia & Quiz Game trivia, quiz, knowledge, question, answer, challenge, leaderboard, fact, brain, compete Vibrant & Block-based + Micro-interactions Claymorphism, Flat Design Feature-Rich Showcase + Social Proof Leaderboard Analytics Energetic blue + correct green + incorrect red + leaderboard gold Timer pressure UX. Category selection. Streak system. Real-time multiplayer. Daily quiz mode.
123 122 Card & Board Game card, board, chess, checkers, poker, strategy, turn-based, multiplayer, classic, tabletop 3D & Hyperrealism + Flat Design Motion-Driven, Dark Mode (OLED) Feature-Rich Showcase N/A - Game focused Game-theme felt green + dark wood + card back patterns Real-time or async multiplayer. Game state sync. Tutorial mode. Match history. ELO rating system.
124 123 Idle & Clicker Game idle, clicker, incremental, passive, cookie, adventure, progress, offline, collect, prestige Vibrant & Block-based + Motion-Driven Claymorphism, 3D & Hyperrealism Feature-Rich Showcase N/A - Progress focused Coin gold + upgrade blue + prestige purple + progress green Offline progress calculation. Satisfying number animations. Upgrade tree clarity. Prestige system. Optional ads.
125 124 Word & Crossword Game word, crossword, wordle, spelling, vocabulary, letters, grid, puzzle, dictionary, daily Minimalism + Flat Design Swiss Modernism 2.0, Micro-interactions Minimal & Direct + Demo N/A - Game focused Clean white + warm letter tiles + success green + shake red Daily challenge with shareable results. Physical keyboard feel. Difficulty levels. Dictionary hints. Streak stats.
126 125 Arcade & Retro Game arcade, retro, 8bit, action, shoot, runner, tap, reflex, endless, pixel, classic, score Pixel Art + Retro-Futurism Vibrant & Block-based, Motion-Driven Feature-Rich Showcase + Hero-Centric N/A - Score focused Neon on black + pixel palette + score gold + danger red Instant play with no login. Game Center leaderboards. Haptic feedback on collision. Offline. Controller support.
127 126 Photo Editor & Filters photo, edit, filter, vsco, snapseed, enhance, crop, retouch, adjust, luts, preset, adjust Minimalism + Dark Mode (OLED) Motion-Driven, Flat Design Feature-Rich Showcase + Interactive Demo N/A - Editor focused Dark editor background + vibrant filter preview strip + tool icon accent Non-destructive editing. Filter preview carousel. Histogram. RAW support. Batch export. Social share direct.
128 127 Short Video Editor video, edit, capcut, inshot, clip, reel, tiktok, trim, effects, transitions, music, timeline Dark Mode (OLED) + Motion-Driven Vibrant & Block-based, Glassmorphism Feature-Rich Showcase + Hero-Centric N/A - Timeline editor focused Dark background + timeline track accent colors + effect preview vivid Multi-track timeline. Licensed music library. Text overlays. Auto-captions. Export 9:16 / 16:9 / 1:1.
129 128 Drawing & Sketching Canvas drawing, sketch, procreate, canvas, paint, illustration, digital, brush, layers, art, stylus Minimalism + Dark Mode (OLED) Anti-Polish Raw, Motion-Driven Interactive Product Demo + Storytelling N/A - Canvas focused Neutral canvas + full-spectrum color picker + tool panel dark Pressure sensitivity. Infinite canvas (pan/zoom). Layer management. Undo history. Export PNG/PSD/SVG.
130 129 Music Creation & Beat Maker music, beat, daw, garageband, create, loop, sample, instrument, track, compose, record, midi Dark Mode (OLED) + Motion-Driven Cyberpunk UI, Glassmorphism Interactive Product Demo + Storytelling N/A - DAW focused Dark studio background + track colors rainbow + waveform accent + BPM pulse Touch piano and drum pad. Loop browser. MIDI support. Export MP3/WAV. Low-latency audio engine.
131 130 Meme & Sticker Maker meme, sticker, maker, funny, caption, template, edit, share, viral, emoji, creator, reaction Vibrant & Block-based + Flat Design Gen Z Chaos, Claymorphism Feature-Rich Showcase + Social Proof N/A - Creator focused Bold primary + comedic yellow + viral red + high saturation accent Template library. Caption text overlay. Font variety. Reaction sticker packs. Share to all platforms. Fast creation.
132 131 AI Photo & Avatar Generator ai, photo, avatar, lensa, portrait, generate, selfie, style, filter, prisma, art AI-Native UI + Aurora UI Glassmorphism, Minimalism Feature-Rich Showcase + Social Proof N/A - Generation focused AI purple + aurora gradients + before/after neutral Style selection. Multiple output variations. Privacy policy prominent. Fast generation. Credits/subscription system.
133 132 Link-in-Bio Page Builder bio, link, linktree, personal, page, creator, social, portfolio, profile, landing, custom Vibrant & Block-based + Bento Box Grid Minimalism, Glassmorphism Conversion-Optimized + Social Proof Analytics (click tracking) Brand-customizable + accent link color + clean white canvas Drag-drop builder. Theme templates. Click analytics. Custom domain. Social icon integration. QR code export.
134 133 Wardrobe & Outfit Planner wardrobe, outfit, fashion, clothes, closet, style, wear, plan, capsule, ootd, lookbook Minimalism + Motion-Driven Aurora UI, Soft UI Evolution Storytelling-Driven + Feature-Rich N/A - Wardrobe focused Clean fashion neutral + full clothes color palette + accent Photo catalog of clothes. AI outfit suggestions. Calendar integration. Capsule wardrobe. Season filtering.
135 134 Plant Care Tracker plant, care, water, garden, tracker, reminder, species, photo, grow, health, planta Organic Biophilic + Soft UI Evolution Claymorphism, Flat Design Storytelling-Driven + Social Proof N/A - Plant collection focused Nature greens + earth brown + sunny yellow reminder + water blue Plant database with care guides. Watering reminders. Growth photo timeline. AI health diagnosis. Collection sharing.
136 135 Book & Reading Tracker book, reading, tracker, goodreads, library, shelf, progress, review, notes, goal, literature Swiss Modernism 2.0 + Minimalism E-Ink Paper, Soft UI Evolution Social Proof-Focused + Feature-Rich N/A - Library focused Warm paper white + ink brown + reading progress green + book cover colors Barcode scan to add. Progress percentage. Annual reading goal. Notes and quotes. Friends activity. Genre stats.
137 136 Couple & Relationship App couple, relationship, partner, love, date, anniversary, memory, shared, intimate, between Aurora UI + Soft UI Evolution Claymorphism, Glassmorphism Storytelling-Driven + Social Proof N/A - Couple focused Warm romantic pink/rose + soft gradient + memory photo tones Shared timeline. Anniversary countdowns. Secret chat. Photo albums. Love language quiz. Date night ideas.
138 137 Family Calendar & Chores family, calendar, chores, tasks, household, shared, kids, schedule, cozi, organize, member Flat Design + Claymorphism Accessible & Ethical, Vibrant & Block-based Feature-Rich Showcase + Social Proof N/A - Family hub focused Warm playful + member color coding + chore completion green Member color coding. Chore assignment rotation. Recurring events. Shared shopping list. Allowance tracking.
139 138 Mood Tracker mood, emotion, feeling, mental, daily, journal, wellbeing, check-in, log, track, daylio Soft UI Evolution + Minimalism Aurora UI, Neumorphism Storytelling-Driven + Social Proof N/A - Mood chart focused Emotion gradient (blue sad to yellow happy) + pastel per mood + insight accent One-tap daily check-in. Emotion wheel selector. Mood calendar heatmap. Pattern insights. Export and share.
140 139 Gift & Wishlist gift, wishlist, present, birthday, occasion, registry, idea, shop, list, share, surprise Vibrant & Block-based + Soft UI Evolution Claymorphism, Flat Design Minimal & Direct + Conversion N/A - List focused Celebration warm pink/gold/red + category colors + surprise accent Add from any URL. Price range filter. Reserved-by-others system. Occasion calendar. Collaborative list. Surprise mode.
141 140 Running & Cycling GPS running, cycling, gps, strava, track, route, speed, distance, cadence, pace, workout, sport Dark Mode (OLED) + Vibrant & Block-based Motion-Driven, Glassmorphism Feature-Rich Showcase + Social Proof Performance Analytics Energetic orange + map accent + pace zones (green/yellow/red) Live GPS tracking. Route map. Auto-pause detection. Segment leaderboards. Training zones. Social feed. Garmin sync.
142 141 Yoga & Stretching Guide yoga, stretch, flexibility, pose, asana, guided, session, calm, routine, wellness, down-dog Organic Biophilic + Soft UI Evolution Neumorphism, Minimalism Storytelling-Driven + Social Proof N/A - Session focused Earth calming sage/terracotta/cream + breathing gradient + warm accent Pose library with illustrations. Guided sessions with audio. Breathing exercises. Progress calendar. Beginner to advanced.
143 142 Sleep Tracker sleep, tracker, alarm, cycle, quality, snore, analysis, rem, deep, smart, wake, insomnia Dark Mode (OLED) + Neumorphism Glassmorphism, Minimalism Feature-Rich Showcase + Social Proof Healthcare Analytics Deep midnight blue + stars/moon accent + sleep quality gradient (poor red to great green) Sleep cycle detection. Smart alarm wakes at light sleep. Snore detection. Weekly trends. Apple Health integration.
144 143 Calorie & Nutrition Counter calorie, nutrition, food, diet, macro, protein, carb, fat, log, fitness, myfitnesspal Flat Design + Vibrant & Block-based Minimalism, Claymorphism Feature-Rich Showcase + Social Proof Healthcare Analytics Healthy green + macro colors (protein blue, carb orange, fat yellow) + progress circle Barcode scanner food log. Large database. Macro goals. Restaurant lookup. Recipe builder. AI photo food logging.
145 144 Period & Cycle Tracker period, cycle, menstrual, fertility, ovulation, pms, log, women, health, flo, clue, hormone Soft UI Evolution + Aurora UI Accessible & Ethical, Claymorphism Social Proof-Focused + Trust Healthcare Analytics Rose/blush + lavender + fertility green + soft calendar tones Cycle prediction. Symptom logging. Fertility window. Personalized insights. Privacy-first. Partner sharing option.
146 145 Medication & Pill Reminder medication, pill, reminder, dose, schedule, prescription, drug, health, medisafe, refill Accessible & Ethical + Flat Design Minimalism, Trust & Authority Trust & Authority + Feature-Rich N/A - Schedule focused Medical trust blue + missed alert red + taken green + clean white Multi-medication schedule. Caregiver sharing. Refill reminders. Drug interaction warnings. Large touch targets.
147 146 Water & Hydration Reminder water, hydration, drink, reminder, daily, tracker, glasses, intake, health, cup, aqua Claymorphism + Vibrant & Block-based Flat Design, Micro-interactions Minimal & Direct + Demo N/A - Daily goal focused Refreshing blue + water wave animation + goal progress accent Tap to log quickly. Animated fill visualization. Custom reminders. Goal by weight/weather. Streak system. Widget.
148 147 Fasting & Intermittent Timer fasting, intermittent, 16:8, timer, fast, eating, window, keto, diet, zero, weight, protocol Minimalism + Dark Mode (OLED) Neumorphism, Flat Design Feature-Rich Showcase + Social Proof N/A - Timer focused Fasting deep blue/purple + eating window green + timeline neutral Protocol selector (16:8, 18:6, OMAD). Circular countdown timer. Fasting history log. Tips during fast. Electrolytes.
149 148 Anonymous Community / Confession anonymous, community, confess, whisper, secret, vent, share, safe, private, social, yikyak Dark Mode (OLED) + Minimalism Glassmorphism, Soft UI Evolution Social Proof-Focused + Feature-Rich User Behavior Analytics Dark protective + subtle gradient + upvote green + empathy warm accent Anonymous posting with moderation. Safety reporting. Reaction system. Trending topics. Mental health resources link.
150 149 Local Events & Discovery local, events, discovery, meetup, nearby, social, city, activities, calendar, community, explore Vibrant & Block-based + Motion-Driven Glassmorphism, Flat Design Hero-Centric Design + Feature-Rich Event Analytics City vibrant + event category colors + map accent + date highlight Location-based discovery. Category filters. RSVP flow. Map view. Friend attendance. Organizer tools. Reminders.
151 150 Study Together / Virtual Coworking study, focus, cowork, pomodoro, virtual, together, session, accountability, live, stream, room Minimalism + Soft UI Evolution Flat Design, Dark Mode (OLED) Social Proof-Focused + Feature-Rich User Behavior Analytics Calm focus blue + session progress indicator + ambient warm neutrals Live study rooms with video/avatar presence. Shared focus timer. Ambient music. Goals sharing. Streak accountability.
152 151 Coding Challenge & Practice coding, leetcode, challenge, algorithm, practice, programming, competitive, skill, interview, problem Dark Mode (OLED) + Cyberpunk UI Minimalism, Flat Design Feature-Rich Showcase + Social Proof Student Analytics Code editor dark + success green + difficulty gradient (easy green / medium amber / hard red) Code editor with syntax highlight. Multiple languages. Hint system. Solution explanation. Company tags. Contest mode.
153 152 Kids Learning (ABC & Math) kids, children, learning, abc, math, phonics, numbers, education, games, preschool, early Claymorphism + Vibrant & Block-based Micro-interactions, Flat Design Social Proof-Focused + Trust Parent Dashboard Bright primary + child-safe pastels + reward gold + interactive accent Age-appropriate UI for 2-8. No ads. No dark patterns. Curriculum aligned. Parent progress reports. Reward system.
154 153 Music Instrument Learning music, instrument, piano, guitar, learn, lesson, tutorial, notes, play, chord, practice, simply Vibrant & Block-based + Motion-Driven Dark Mode (OLED), Soft UI Evolution Interactive Product Demo + Social Proof Learning Analytics Musical warm deep red/brown + note color system + skill progress bar Interactive instrument on-screen. Sheet music display. Song library. Slow-tempo practice. Recording and playback. Teacher mode.
155 154 Parking Finder parking, spot, finder, map, pay, meter, garage, location, car, reserve, spothero Minimalism + Glassmorphism Flat Design, Micro-interactions Conversion-Optimized + Feature-Rich Real-Time Monitoring + Map Trust blue + available green + occupied red + map neutral Real-time availability. In-app navigation. Payment integration. Parking timer alert. Favorite spots. Street vs garage.
156 155 Public Transit Guide transit, bus, metro, subway, train, route, schedule, map, city, commute, trip, citymapper Flat Design + Accessible & Ethical Minimalism, Motion-Driven Feature-Rich Showcase + Interactive Demo Real-Time Monitoring + Map Transit brand line colors + real-time indicator green/red + map neutral Real-time arrivals. Offline maps. Disruption alerts. Multi-modal routing. Fare calculation. Accessibility features.
157 156 Road Trip Planner road, trip, drive, route, planner, travel, stop, map, adventure, scenic, car, wanderlog Aurora UI + Organic Biophilic Motion-Driven, Vibrant & Block-based Storytelling-Driven + Hero-Centric N/A - Trip focused Adventure warm sunset orange + map teal + stop markers + road neutral Route planning with stops. Point-of-interest discovery. Gas/food/hotel along route. Offline maps. Trip sharing.
158 157 VPN & Privacy Tool vpn, privacy, secure, anonymous, encrypt, proxy, ip, protect, shield, network, nordvpn Minimalism + Dark Mode (OLED) Cyberpunk UI, Trust & Authority Trust & Authority + Conversion-Optimized N/A - Connection focused Dark shield blue + connected green + disconnected red + trust accent One-tap connect. Server selection by country. No-log policy prominent. Speed indicator. Kill switch. Protocol choice.
159 158 Emergency SOS & Safety emergency, sos, safety, alert, location, help, danger, crisis, first-aid, guard, bsafe Accessible & Ethical + Flat Design Dark Mode (OLED), Minimalism Trust & Authority + Social Proof N/A - Safety focused Alert red + safety blue + location green + high contrast critical One-tap SOS. Emergency contacts auto-notify. Live location sharing. Fake call feature. Safe walk mode. Local emergency numbers.
160 159 Wallpaper & Theme App wallpaper, theme, background, customize, aesthetic, home-screen, lock-screen, widget, design, zedge Vibrant & Block-based + Aurora UI Glassmorphism, Motion-Driven Feature-Rich Showcase + Social Proof N/A - Gallery focused Content-driven + trending aesthetic palettes + download accent Category browsing. Preview on device. Daily wallpaper auto-set. Widget matching. Creator uploads. Resolution auto-fit.
161 160 White Noise & Ambient Sound white noise, ambient, sound, sleep, focus, rain, nature, relax, concentration, background, noisli Minimalism + Dark Mode (OLED) Neumorphism, Organic Biophilic Minimal & Direct + Social Proof N/A - Player focused Calming dark + ambient texture visual + subtle sound wave + sleep blue Sound mixer with multiple simultaneous layers. Sleep timer with fade. Custom soundscapes. Offline. Background audio.
162 161 Home Decoration & Interior Design home, interior, decor, design, furniture, room, renovation, ar, plan, inspire, 3d, houzz Minimalism + 3D Product Preview Organic Biophilic, Aurora UI Storytelling-Driven + Feature-Rich N/A - Project focused Neutral interior palette + material texture accent + AR blue AR room visualization. Style quiz. Product catalog with purchase links. 3D room planner. Mood board. Before/after.

View file

@ -1,45 +0,0 @@
No,Category,Issue,Keywords,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity
1,Async Waterfall,Defer Await,async await defer branch,React/Next.js,Move await into branches where actually used to avoid blocking unused code paths,Move await operations into branches where they're needed,Await at top of function blocking all branches,"if (skip) return { skipped: true }; const data = await fetch()","const data = await fetch(); if (skip) return { skipped: true }",Critical
2,Async Waterfall,Promise.all Parallel,promise all parallel concurrent,React/Next.js,Execute independent async operations concurrently using Promise.all(),Use Promise.all() for independent operations,Sequential await for independent operations,"const [user, posts] = await Promise.all([fetchUser(), fetchPosts()])","const user = await fetchUser(); const posts = await fetchPosts()",Critical
3,Async Waterfall,Dependency Parallelization,better-all dependency parallel,React/Next.js,Use better-all for operations with partial dependencies to maximize parallelism,Use better-all to start each task at earliest possible moment,Wait for unrelated data before starting dependent fetch,"await all({ user() {}, config() {}, profile() { return fetch((await this.$.user).id) } })","const [user, config] = await Promise.all([...]); const profile = await fetchProfile(user.id)",Critical
4,Async Waterfall,API Route Optimization,api route waterfall promise,React/Next.js,In API routes start independent operations immediately even if not awaited yet,Start promises early and await late,Sequential awaits in API handlers,"const sessionP = auth(); const configP = fetchConfig(); const session = await sessionP","const session = await auth(); const config = await fetchConfig()",Critical
5,Async Waterfall,Suspense Boundaries,suspense streaming boundary,React/Next.js,Use Suspense to show wrapper UI faster while data loads,Wrap async components in Suspense boundaries,Await data blocking entire page render,"<Suspense fallback={<Skeleton />}><DataDisplay /></Suspense>","const data = await fetchData(); return <DataDisplay data={data} />",High
6,Bundle Size,Barrel Imports,barrel import direct path,React/Next.js,Import directly from source files instead of barrel files to avoid loading unused modules,Import directly from source path,Import from barrel/index files,"import Check from 'lucide-react/dist/esm/icons/check'","import { Check } from 'lucide-react'",Critical
7,Bundle Size,Dynamic Imports,dynamic import lazy next,React/Next.js,Use next/dynamic to lazy-load large components not needed on initial render,Use dynamic() for heavy components,Import heavy components at top level,"const Monaco = dynamic(() => import('./monaco'), { ssr: false })","import { MonacoEditor } from './monaco-editor'",Critical
8,Bundle Size,Defer Third Party,analytics defer third-party,React/Next.js,Load analytics and logging after hydration since they don't block interaction,Load non-critical scripts after hydration,Include analytics in main bundle,"const Analytics = dynamic(() => import('@vercel/analytics'), { ssr: false })","import { Analytics } from '@vercel/analytics/react'",Medium
9,Bundle Size,Conditional Loading,conditional module lazy,React/Next.js,Load large data or modules only when a feature is activated,Dynamic import when feature enabled,Import large modules unconditionally,"useEffect(() => { if (enabled) import('./heavy.js') }, [enabled])","import { heavyData } from './heavy.js'",High
10,Bundle Size,Preload Intent,preload hover focus intent,React/Next.js,Preload heavy bundles on hover/focus before they're needed,Preload on user intent signals,Load only on click,"onMouseEnter={() => import('./editor')}","onClick={() => import('./editor')}",Medium
11,Server,React.cache Dedup,react cache deduplicate request,React/Next.js,Use React.cache() for server-side request deduplication within single request,Wrap data fetchers with cache(),Fetch same data multiple times in tree,"export const getUser = cache(async () => await db.user.find())","export async function getUser() { return await db.user.find() }",Medium
12,Server,LRU Cache Cross-Request,lru cache cross request,React/Next.js,Use LRU cache for data shared across sequential requests,Use LRU for cross-request caching,Refetch same data on every request,"const cache = new LRUCache({ max: 1000, ttl: 5*60*1000 })","Always fetch from database",High
13,Server,Minimize Serialization,serialization rsc boundary,React/Next.js,Only pass fields that client actually uses across RSC boundaries,Pass only needed fields to client components,Pass entire objects to client,"<Profile name={user.name} />","<Profile user={user} /> // 50 fields serialized",High
14,Server,Parallel Fetching,parallel fetch component composition,React/Next.js,Restructure components to parallelize data fetching in RSC,Use component composition for parallel fetches,Sequential fetches in parent component,"<Header /><Sidebar /> // both fetch in parallel","const header = await fetchHeader(); return <><div>{header}</div><Sidebar /></>",Critical
15,Server,After Non-blocking,after non-blocking logging,React/Next.js,Use Next.js after() to schedule work after response is sent,Use after() for logging/analytics,Block response for non-critical operations,"after(async () => { await logAction() }); return Response.json(data)","await logAction(); return Response.json(data)",Medium
16,Client,SWR Deduplication,swr dedup cache revalidate,React/Next.js,Use SWR for automatic request deduplication and caching,Use useSWR for client data fetching,Manual fetch in useEffect,"const { data } = useSWR('/api/users', fetcher)","useEffect(() => { fetch('/api/users').then(setUsers) }, [])",Medium-High
17,Client,Event Listener Dedup,event listener deduplicate global,React/Next.js,Share global event listeners across component instances,Use useSWRSubscription for shared listeners,Register listener per component instance,"useSWRSubscription('global-keydown', () => { window.addEventListener... })","useEffect(() => { window.addEventListener('keydown', handler) }, [])",Low
18,Rerender,Defer State Reads,state read callback subscription,React/Next.js,Don't subscribe to state only used in callbacks,Read state on-demand in callbacks,Subscribe to state used only in handlers,"const handleClick = () => { const params = new URLSearchParams(location.search) }","const params = useSearchParams(); const handleClick = () => { params.get('ref') }",Medium
19,Rerender,Memoized Components,memo extract expensive,React/Next.js,Extract expensive work into memoized components for early returns,Extract to memo() components,Compute expensive values before early return,"const UserAvatar = memo(({ user }) => ...); if (loading) return <Skeleton />","const avatar = useMemo(() => compute(user)); if (loading) return <Skeleton />",Medium
20,Rerender,Narrow Dependencies,effect dependency primitive,React/Next.js,Specify primitive dependencies instead of objects in effects,Use primitive values in dependency arrays,Use object references as dependencies,"useEffect(() => { console.log(user.id) }, [user.id])","useEffect(() => { console.log(user.id) }, [user])",Low
21,Rerender,Derived State,derived boolean subscription,React/Next.js,Subscribe to derived booleans instead of continuous values,Use derived boolean state,Subscribe to continuous values,"const isMobile = useMediaQuery('(max-width: 767px)')","const width = useWindowWidth(); const isMobile = width < 768",Medium
22,Rerender,Functional setState,functional setstate callback,React/Next.js,Use functional setState updates for stable callbacks and no stale closures,Use functional form: setState(curr => ...),Reference state directly in setState,"setItems(curr => [...curr, newItem])","setItems([...items, newItem]) // items in deps",Medium
23,Rerender,Lazy State Init,usestate lazy initialization,React/Next.js,Pass function to useState for expensive initial values,Use function form for expensive init,Compute expensive value directly,"useState(() => buildSearchIndex(items))","useState(buildSearchIndex(items)) // runs every render",Medium
24,Rerender,Transitions,starttransition non-urgent,React/Next.js,Mark frequent non-urgent state updates as transitions,Use startTransition for non-urgent updates,Block UI on every state change,"startTransition(() => setScrollY(window.scrollY))","setScrollY(window.scrollY) // blocks on every scroll",Medium
25,Rendering,SVG Animation Wrapper,svg animation wrapper div,React/Next.js,Wrap SVG in div and animate wrapper for hardware acceleration,Animate div wrapper around SVG,Animate SVG element directly,"<div class='animate-spin'><svg>...</svg></div>","<svg class='animate-spin'>...</svg>",Low
26,Rendering,Content Visibility,content-visibility auto,React/Next.js,Apply content-visibility: auto to defer off-screen rendering,Use content-visibility for long lists,Render all list items immediately,".item { content-visibility: auto; contain-intrinsic-size: 0 80px }","Render 1000 items without optimization",High
27,Rendering,Hoist Static JSX,hoist static jsx element,React/Next.js,Extract static JSX outside components to avoid re-creation,Hoist static elements to module scope,Create static elements inside components,"const skeleton = <div class='animate-pulse' />; function C() { return skeleton }","function C() { return <div class='animate-pulse' /> }",Low
28,Rendering,Hydration No Flicker,hydration mismatch flicker,React/Next.js,Use inline script to set client-only data before hydration,Inject sync script for client-only values,Use useEffect causing flash,"<script dangerouslySetInnerHTML={{ __html: 'el.className = localStorage.theme' }} />","useEffect(() => setTheme(localStorage.theme), []) // flickers",Medium
29,Rendering,Conditional Render,conditional render ternary,React/Next.js,Use ternary instead of && when condition can be 0 or NaN,Use explicit ternary for conditionals,Use && with potentially falsy numbers,"{count > 0 ? <Badge>{count}</Badge> : null}","{count && <Badge>{count}</Badge>} // renders '0'",Low
30,Rendering,Activity Component,activity show hide preserve,React/Next.js,Use Activity component to preserve state/DOM for toggled components,Use Activity for expensive toggle components,Unmount/remount on visibility toggle,"<Activity mode={isOpen ? 'visible' : 'hidden'}><Menu /></Activity>","{isOpen && <Menu />} // loses state",Medium
31,JS Perf,Batch DOM CSS,batch dom css reflow,React/Next.js,Group CSS changes via classes or cssText to minimize reflows,Use class toggle or cssText,Change styles one property at a time,"element.classList.add('highlighted')","el.style.width='100px'; el.style.height='200px'",Medium
32,JS Perf,Index Map Lookup,map index lookup find,React/Next.js,Build Map for repeated lookups instead of multiple .find() calls,Build index Map for O(1) lookups,Use .find() in loops,"const byId = new Map(users.map(u => [u.id, u])); byId.get(id)","users.find(u => u.id === order.userId) // O(n) each time",Low-Medium
33,JS Perf,Cache Property Access,cache property loop,React/Next.js,Cache object property lookups in hot paths,Cache values before loops,Access nested properties in loops,"const val = obj.config.settings.value; for (...) process(val)","for (...) process(obj.config.settings.value)",Low-Medium
34,JS Perf,Cache Function Results,memoize cache function,React/Next.js,Use module-level Map to cache repeated function results,Use Map cache for repeated calls,Recompute same values repeatedly,"const cache = new Map(); if (cache.has(x)) return cache.get(x)","slugify(name) // called 100 times same input",Medium
35,JS Perf,Cache Storage API,localstorage cache read,React/Next.js,Cache localStorage/sessionStorage reads in memory,Cache storage reads in Map,Read storage on every call,"if (!cache.has(key)) cache.set(key, localStorage.getItem(key))","localStorage.getItem('theme') // every call",Low-Medium
36,JS Perf,Combine Iterations,combine filter map loop,React/Next.js,Combine multiple filter/map into single loop,Single loop for multiple categorizations,Chain multiple filter() calls,"for (u of users) { if (u.isAdmin) admins.push(u); if (u.isTester) testers.push(u) }","users.filter(admin); users.filter(tester); users.filter(inactive)",Low-Medium
37,JS Perf,Length Check First,length check array compare,React/Next.js,Check array lengths before expensive comparisons,Early return if lengths differ,Always run expensive comparison,"if (a.length !== b.length) return true; // then compare","a.sort().join() !== b.sort().join() // even when lengths differ",Medium-High
38,JS Perf,Early Return,early return exit function,React/Next.js,Return early when result is determined to skip processing,Return immediately on first error,Process all items then check errors,"for (u of users) { if (!u.email) return { error: 'Email required' } }","let hasError; for (...) { if (!email) hasError=true }; if (hasError)...",Low-Medium
39,JS Perf,Hoist RegExp,regexp hoist module,React/Next.js,Don't create RegExp inside render - hoist or memoize,Hoist RegExp to module scope,Create RegExp every render,"const EMAIL_RE = /^[^@]+@[^@]+$/; function validate() { EMAIL_RE.test(x) }","function C() { const re = new RegExp(pattern); re.test(x) }",Low-Medium
40,JS Perf,Loop Min Max,loop min max sort,React/Next.js,Use loop for min/max instead of sort - O(n) vs O(n log n),Single pass loop for min/max,Sort array to find min/max,"let max = arr[0]; for (x of arr) if (x > max) max = x","arr.sort((a,b) => b-a)[0] // O(n log n)",Low
41,JS Perf,Set Map Lookups,set map includes has,React/Next.js,Use Set/Map for O(1) lookups instead of array.includes(),Convert to Set for membership checks,Use .includes() for repeated checks,"const allowed = new Set(['a','b']); allowed.has(id)","const allowed = ['a','b']; allowed.includes(id)",Low-Medium
42,JS Perf,toSorted Immutable,tosorted sort immutable,React/Next.js,Use toSorted() instead of sort() to avoid mutating arrays,Use toSorted() for immutability,Mutate arrays with sort(),"users.toSorted((a,b) => a.name.localeCompare(b.name))","users.sort((a,b) => a.name.localeCompare(b.name)) // mutates",Medium-High
43,Advanced,Event Handler Refs,useeffectevent ref handler,React/Next.js,Store callbacks in refs for stable effect subscriptions,Use useEffectEvent for stable handlers,Re-subscribe on every callback change,"const onEvent = useEffectEvent(handler); useEffect(() => { listen(onEvent) }, [])","useEffect(() => { listen(handler) }, [handler]) // re-subscribes",Low
44,Advanced,useLatest Hook,uselatest ref callback,React/Next.js,Access latest values in callbacks without adding to dependency arrays,Use useLatest for fresh values in stable callbacks,Add callback to effect dependencies,"const cbRef = useLatest(cb); useEffect(() => { setTimeout(() => cbRef.current()) }, [])","useEffect(() => { setTimeout(() => cb()) }, [cb]) // re-runs",Low
1 No Category Issue Keywords Platform Description Do Don't Code Example Good Code Example Bad Severity
2 1 Async Waterfall Defer Await async await defer branch React/Next.js Move await into branches where actually used to avoid blocking unused code paths Move await operations into branches where they're needed Await at top of function blocking all branches if (skip) return { skipped: true }; const data = await fetch() const data = await fetch(); if (skip) return { skipped: true } Critical
3 2 Async Waterfall Promise.all Parallel promise all parallel concurrent React/Next.js Execute independent async operations concurrently using Promise.all() Use Promise.all() for independent operations Sequential await for independent operations const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]) const user = await fetchUser(); const posts = await fetchPosts() Critical
4 3 Async Waterfall Dependency Parallelization better-all dependency parallel React/Next.js Use better-all for operations with partial dependencies to maximize parallelism Use better-all to start each task at earliest possible moment Wait for unrelated data before starting dependent fetch await all({ user() {}, config() {}, profile() { return fetch((await this.$.user).id) } }) const [user, config] = await Promise.all([...]); const profile = await fetchProfile(user.id) Critical
5 4 Async Waterfall API Route Optimization api route waterfall promise React/Next.js In API routes start independent operations immediately even if not awaited yet Start promises early and await late Sequential awaits in API handlers const sessionP = auth(); const configP = fetchConfig(); const session = await sessionP const session = await auth(); const config = await fetchConfig() Critical
6 5 Async Waterfall Suspense Boundaries suspense streaming boundary React/Next.js Use Suspense to show wrapper UI faster while data loads Wrap async components in Suspense boundaries Await data blocking entire page render <Suspense fallback={<Skeleton />}><DataDisplay /></Suspense> const data = await fetchData(); return <DataDisplay data={data} /> High
7 6 Bundle Size Barrel Imports barrel import direct path React/Next.js Import directly from source files instead of barrel files to avoid loading unused modules Import directly from source path Import from barrel/index files import Check from 'lucide-react/dist/esm/icons/check' import { Check } from 'lucide-react' Critical
8 7 Bundle Size Dynamic Imports dynamic import lazy next React/Next.js Use next/dynamic to lazy-load large components not needed on initial render Use dynamic() for heavy components Import heavy components at top level const Monaco = dynamic(() => import('./monaco'), { ssr: false }) import { MonacoEditor } from './monaco-editor' Critical
9 8 Bundle Size Defer Third Party analytics defer third-party React/Next.js Load analytics and logging after hydration since they don't block interaction Load non-critical scripts after hydration Include analytics in main bundle const Analytics = dynamic(() => import('@vercel/analytics'), { ssr: false }) import { Analytics } from '@vercel/analytics/react' Medium
10 9 Bundle Size Conditional Loading conditional module lazy React/Next.js Load large data or modules only when a feature is activated Dynamic import when feature enabled Import large modules unconditionally useEffect(() => { if (enabled) import('./heavy.js') }, [enabled]) import { heavyData } from './heavy.js' High
11 10 Bundle Size Preload Intent preload hover focus intent React/Next.js Preload heavy bundles on hover/focus before they're needed Preload on user intent signals Load only on click onMouseEnter={() => import('./editor')} onClick={() => import('./editor')} Medium
12 11 Server React.cache Dedup react cache deduplicate request React/Next.js Use React.cache() for server-side request deduplication within single request Wrap data fetchers with cache() Fetch same data multiple times in tree export const getUser = cache(async () => await db.user.find()) export async function getUser() { return await db.user.find() } Medium
13 12 Server LRU Cache Cross-Request lru cache cross request React/Next.js Use LRU cache for data shared across sequential requests Use LRU for cross-request caching Refetch same data on every request const cache = new LRUCache({ max: 1000, ttl: 5*60*1000 }) Always fetch from database High
14 13 Server Minimize Serialization serialization rsc boundary React/Next.js Only pass fields that client actually uses across RSC boundaries Pass only needed fields to client components Pass entire objects to client <Profile name={user.name} /> <Profile user={user} /> // 50 fields serialized High
15 14 Server Parallel Fetching parallel fetch component composition React/Next.js Restructure components to parallelize data fetching in RSC Use component composition for parallel fetches Sequential fetches in parent component <Header /><Sidebar /> // both fetch in parallel const header = await fetchHeader(); return <><div>{header}</div><Sidebar /></> Critical
16 15 Server After Non-blocking after non-blocking logging React/Next.js Use Next.js after() to schedule work after response is sent Use after() for logging/analytics Block response for non-critical operations after(async () => { await logAction() }); return Response.json(data) await logAction(); return Response.json(data) Medium
17 16 Client SWR Deduplication swr dedup cache revalidate React/Next.js Use SWR for automatic request deduplication and caching Use useSWR for client data fetching Manual fetch in useEffect const { data } = useSWR('/api/users', fetcher) useEffect(() => { fetch('/api/users').then(setUsers) }, []) Medium-High
18 17 Client Event Listener Dedup event listener deduplicate global React/Next.js Share global event listeners across component instances Use useSWRSubscription for shared listeners Register listener per component instance useSWRSubscription('global-keydown', () => { window.addEventListener... }) useEffect(() => { window.addEventListener('keydown', handler) }, []) Low
19 18 Rerender Defer State Reads state read callback subscription React/Next.js Don't subscribe to state only used in callbacks Read state on-demand in callbacks Subscribe to state used only in handlers const handleClick = () => { const params = new URLSearchParams(location.search) } const params = useSearchParams(); const handleClick = () => { params.get('ref') } Medium
20 19 Rerender Memoized Components memo extract expensive React/Next.js Extract expensive work into memoized components for early returns Extract to memo() components Compute expensive values before early return const UserAvatar = memo(({ user }) => ...); if (loading) return <Skeleton /> const avatar = useMemo(() => compute(user)); if (loading) return <Skeleton /> Medium
21 20 Rerender Narrow Dependencies effect dependency primitive React/Next.js Specify primitive dependencies instead of objects in effects Use primitive values in dependency arrays Use object references as dependencies useEffect(() => { console.log(user.id) }, [user.id]) useEffect(() => { console.log(user.id) }, [user]) Low
22 21 Rerender Derived State derived boolean subscription React/Next.js Subscribe to derived booleans instead of continuous values Use derived boolean state Subscribe to continuous values const isMobile = useMediaQuery('(max-width: 767px)') const width = useWindowWidth(); const isMobile = width < 768 Medium
23 22 Rerender Functional setState functional setstate callback React/Next.js Use functional setState updates for stable callbacks and no stale closures Use functional form: setState(curr => ...) Reference state directly in setState setItems(curr => [...curr, newItem]) setItems([...items, newItem]) // items in deps Medium
24 23 Rerender Lazy State Init usestate lazy initialization React/Next.js Pass function to useState for expensive initial values Use function form for expensive init Compute expensive value directly useState(() => buildSearchIndex(items)) useState(buildSearchIndex(items)) // runs every render Medium
25 24 Rerender Transitions starttransition non-urgent React/Next.js Mark frequent non-urgent state updates as transitions Use startTransition for non-urgent updates Block UI on every state change startTransition(() => setScrollY(window.scrollY)) setScrollY(window.scrollY) // blocks on every scroll Medium
26 25 Rendering SVG Animation Wrapper svg animation wrapper div React/Next.js Wrap SVG in div and animate wrapper for hardware acceleration Animate div wrapper around SVG Animate SVG element directly <div class='animate-spin'><svg>...</svg></div> <svg class='animate-spin'>...</svg> Low
27 26 Rendering Content Visibility content-visibility auto React/Next.js Apply content-visibility: auto to defer off-screen rendering Use content-visibility for long lists Render all list items immediately .item { content-visibility: auto; contain-intrinsic-size: 0 80px } Render 1000 items without optimization High
28 27 Rendering Hoist Static JSX hoist static jsx element React/Next.js Extract static JSX outside components to avoid re-creation Hoist static elements to module scope Create static elements inside components const skeleton = <div class='animate-pulse' />; function C() { return skeleton } function C() { return <div class='animate-pulse' /> } Low
29 28 Rendering Hydration No Flicker hydration mismatch flicker React/Next.js Use inline script to set client-only data before hydration Inject sync script for client-only values Use useEffect causing flash <script dangerouslySetInnerHTML={{ __html: 'el.className = localStorage.theme' }} /> useEffect(() => setTheme(localStorage.theme), []) // flickers Medium
30 29 Rendering Conditional Render conditional render ternary React/Next.js Use ternary instead of && when condition can be 0 or NaN Use explicit ternary for conditionals Use && with potentially falsy numbers {count > 0 ? <Badge>{count}</Badge> : null} {count && <Badge>{count}</Badge>} // renders '0' Low
31 30 Rendering Activity Component activity show hide preserve React/Next.js Use Activity component to preserve state/DOM for toggled components Use Activity for expensive toggle components Unmount/remount on visibility toggle <Activity mode={isOpen ? 'visible' : 'hidden'}><Menu /></Activity> {isOpen && <Menu />} // loses state Medium
32 31 JS Perf Batch DOM CSS batch dom css reflow React/Next.js Group CSS changes via classes or cssText to minimize reflows Use class toggle or cssText Change styles one property at a time element.classList.add('highlighted') el.style.width='100px'; el.style.height='200px' Medium
33 32 JS Perf Index Map Lookup map index lookup find React/Next.js Build Map for repeated lookups instead of multiple .find() calls Build index Map for O(1) lookups Use .find() in loops const byId = new Map(users.map(u => [u.id, u])); byId.get(id) users.find(u => u.id === order.userId) // O(n) each time Low-Medium
34 33 JS Perf Cache Property Access cache property loop React/Next.js Cache object property lookups in hot paths Cache values before loops Access nested properties in loops const val = obj.config.settings.value; for (...) process(val) for (...) process(obj.config.settings.value) Low-Medium
35 34 JS Perf Cache Function Results memoize cache function React/Next.js Use module-level Map to cache repeated function results Use Map cache for repeated calls Recompute same values repeatedly const cache = new Map(); if (cache.has(x)) return cache.get(x) slugify(name) // called 100 times same input Medium
36 35 JS Perf Cache Storage API localstorage cache read React/Next.js Cache localStorage/sessionStorage reads in memory Cache storage reads in Map Read storage on every call if (!cache.has(key)) cache.set(key, localStorage.getItem(key)) localStorage.getItem('theme') // every call Low-Medium
37 36 JS Perf Combine Iterations combine filter map loop React/Next.js Combine multiple filter/map into single loop Single loop for multiple categorizations Chain multiple filter() calls for (u of users) { if (u.isAdmin) admins.push(u); if (u.isTester) testers.push(u) } users.filter(admin); users.filter(tester); users.filter(inactive) Low-Medium
38 37 JS Perf Length Check First length check array compare React/Next.js Check array lengths before expensive comparisons Early return if lengths differ Always run expensive comparison if (a.length !== b.length) return true; // then compare a.sort().join() !== b.sort().join() // even when lengths differ Medium-High
39 38 JS Perf Early Return early return exit function React/Next.js Return early when result is determined to skip processing Return immediately on first error Process all items then check errors for (u of users) { if (!u.email) return { error: 'Email required' } } let hasError; for (...) { if (!email) hasError=true }; if (hasError)... Low-Medium
40 39 JS Perf Hoist RegExp regexp hoist module React/Next.js Don't create RegExp inside render - hoist or memoize Hoist RegExp to module scope Create RegExp every render const EMAIL_RE = /^[^@]+@[^@]+$/; function validate() { EMAIL_RE.test(x) } function C() { const re = new RegExp(pattern); re.test(x) } Low-Medium
41 40 JS Perf Loop Min Max loop min max sort React/Next.js Use loop for min/max instead of sort - O(n) vs O(n log n) Single pass loop for min/max Sort array to find min/max let max = arr[0]; for (x of arr) if (x > max) max = x arr.sort((a,b) => b-a)[0] // O(n log n) Low
42 41 JS Perf Set Map Lookups set map includes has React/Next.js Use Set/Map for O(1) lookups instead of array.includes() Convert to Set for membership checks Use .includes() for repeated checks const allowed = new Set(['a','b']); allowed.has(id) const allowed = ['a','b']; allowed.includes(id) Low-Medium
43 42 JS Perf toSorted Immutable tosorted sort immutable React/Next.js Use toSorted() instead of sort() to avoid mutating arrays Use toSorted() for immutability Mutate arrays with sort() users.toSorted((a,b) => a.name.localeCompare(b.name)) users.sort((a,b) => a.name.localeCompare(b.name)) // mutates Medium-High
44 43 Advanced Event Handler Refs useeffectevent ref handler React/Next.js Store callbacks in refs for stable effect subscriptions Use useEffectEvent for stable handlers Re-subscribe on every callback change const onEvent = useEffectEvent(handler); useEffect(() => { listen(onEvent) }, []) useEffect(() => { listen(handler) }, [handler]) // re-subscribes Low
45 44 Advanced useLatest Hook uselatest ref callback React/Next.js Access latest values in callbacks without adding to dependency arrays Use useLatest for fresh values in stable callbacks Add callback to effect dependencies const cbRef = useLatest(cb); useEffect(() => { setTimeout(() => cbRef.current()) }, []) useEffect(() => { setTimeout(() => cb()) }, [cb]) // re-runs Low

View file

@ -1,51 +0,0 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Components,Use standalone components,Angular 17+ default; no NgModule needed,Standalone components for all new code,NgModule-based components for new projects,"@Component({ standalone: true imports: [CommonModule] })","@NgModule({ declarations: [MyComp] })",High,https://angular.dev/guide/components/importing
2,Components,Use signals for state,Signals are Angular's reactive primitive for fine-grained reactivity,Signals for component state over class properties,Mutable class properties without signals,"count = signal(0); increment() { this.count.update(v => v + 1) }","count = 0; increment() { this.count++ }",High,https://angular.dev/guide/signals
3,Components,Use @if/@for/@switch control flow,Built-in control flow syntax replaces *ngIf/*ngFor directives,@if and @for in templates,*ngIf and *ngFor structural directives,"@if (isLoggedIn) { <Dashboard /> } @else { <Login /> }","<div *ngIf=""isLoggedIn""><Dashboard /></div>",High,https://angular.dev/guide/templates/control-flow
4,Components,Use input() and output() signals,Signal-based inputs/outputs replace @Input()/@Output() decorators,input() and output() for component API,@Input() and @Output() decorators,"name = input<string>(); clicked = output<void>()","@Input() name: string; @Output() clicked = new EventEmitter()",High,https://angular.dev/guide/components/inputs
5,Components,Use content projection,ng-content for flexible component composition,ng-content with select for named slots,Rigid templates that can't be customized,"<ng-content select=""[header]"" /> <ng-content />","<div class=""header"">{{ title }}</div>",Medium,https://angular.dev/guide/components/content-projection
6,Components,Keep components small,Single responsibility; components should do one thing,Extract sub-components when template exceeds 50 lines,Monolithic components handling multiple concerns,"<UserAvatar /> <UserDetails /> <UserActions />",One 300-line component template,Medium,https://angular.dev/guide/components
7,Components,Use OnPush change detection,Reduces re-renders by only checking on input changes or signal updates,OnPush for all components,Default change detection strategy,"changeDetection: ChangeDetectionStrategy.OnPush","changeDetection: ChangeDetectionStrategy.Default",High,https://angular.dev/guide/components/lifecycle
8,Components,Avoid direct DOM manipulation,Use renderer or ElementRef sparingly; prefer template bindings,Template bindings and Angular directives,Direct document.querySelector or innerHTML,"[class.active]=""isActive""","this.el.nativeElement.classList.add('active')",High,https://angular.dev/guide/components/host-elements
9,Routing,Lazy load feature routes,Load route chunks on demand to reduce initial bundle,loadComponent() for all feature routes,Eager-loaded routes in app config,"{ path: 'admin' loadComponent: () => import('./admin/admin.component') }","{ path: 'admin' component: AdminComponent }",High,https://angular.dev/guide/routing/lazy-loading
10,Routing,Use route guards with functional API,Protect routes with canActivate/canMatch functional guards,Functional guards returning boolean or UrlTree,Class-based guards with CanActivate interface,"canActivate: [() => inject(AuthService).isLoggedIn()]","canActivate: [AuthGuard]",High,https://angular.dev/guide/routing/common-router-tasks#preventing-unauthorized-access
11,Routing,Use route resolvers for data,Pre-fetch data before route activation using resolve,ResolveFn for route data,Fetching data in ngOnInit causing flash of empty state,"resolve: { user: () => inject(UserService).getUser() }",Fetch in ngOnInit with loading state flickering,Medium,https://angular.dev/guide/routing/common-router-tasks#resolve
12,Routing,Type route params with inject,Use inject(ActivatedRoute) with signals or toSignal,Typed route params via ActivatedRoute,Untyped route.snapshot.params string access,"const id = toSignal(route.paramMap.pipe(map(p => p.get('id'))))","const id = this.route.snapshot.params['id']",Medium,https://angular.dev/api/router/ActivatedRoute
13,Routing,Use nested routes for layouts,Compose shared layouts using router-outlet nesting,Nested routes with shared layout components,Duplicating layout code across routes,"{ path: 'app' component: ShellComponent children: [...] }",Duplicate header/sidebar in each route component,Medium,https://angular.dev/guide/routing/router-tutorial-toh#child-route-configuration
14,Routing,Configure preloading strategies,Preload lazy modules in background after initial load,PreloadAllModules or custom strategy,No preloading causing delayed navigation,"provideRouter(routes withPreloading(PreloadAllModules))","provideRouter(routes)",Low,https://angular.dev/api/router/PreloadAllModules
15,State,Use signals for local state,Signals provide synchronous reactive state without RxJS overhead,signal() for component-local reactive state,BehaviorSubject for simple local state,"const items = signal<Item[]>([]); addItem(i: Item) { this.items.update(arr => [...arr i]) }","items$ = new BehaviorSubject<Item[]>([])",High,https://angular.dev/guide/signals
16,State,Use computed() for derived state,Lazily evaluated derived values that update when dependencies change,computed() for values derived from other signals,Duplicated state or manual sync,"readonly total = computed(() => this.items().reduce((s i) => s + i.price 0))","this.total = this.items.reduce(...) // called manually",High,https://angular.dev/guide/signals#computed-signals
17,State,Use effect() carefully,Effects run side effects when signals change; avoid overuse,effect() for side effects like logging or localStorage sync,effect() for deriving state (use computed instead),"effect(() => localStorage.setItem('cart' JSON.stringify(this.cart())))","effect(() => { this.total.set(this.items().length) })",Medium,https://angular.dev/guide/signals#effects
18,State,Use NgRx Signal Store for complex state,NgRx Signal Store is the modern lightweight state management for Angular,@ngrx/signals SignalStore for feature state,Full NgRx reducer/action/effect boilerplate for simple state,"const Store = signalStore(withState({ count: 0 }) withMethods(s => ({ increment: () => patchState(s { count: s.count() + 1 }) })))","createReducer(on(increment state => ({ ...state count: state.count + 1 })))",Medium,https://ngrx.io/guide/signals
19,State,Inject services for shared state,Services with signals share state across components without a store,Injectable service with signals for cross-component state,Prop drilling or @Input chains for shared state,"@Injectable({ providedIn: 'root' }) class CartService { items = signal<Item[]>([]) }","@Input() cartItems passed through 4 component levels",Medium,https://angular.dev/guide/di/creating-injectable-service
20,State,Avoid mixing RxJS and signals unnecessarily,Use toSignal() to bridge RxJS into signal world at the boundary,toSignal() to convert observable to signal at component edge,Subscribing in components and storing in signal manually,"readonly user = toSignal(this.userService.user$)","this.userService.user$.subscribe(u => this.user.set(u))",Medium,https://angular.dev/guide/rxjs-interop
21,Forms,Use typed reactive forms,FormGroup/FormControl with explicit generics for compile-time safety,FormBuilder with typed controls,Untyped FormControl or any casts,"fb.group<LoginForm>({ email: fb.control('') password: fb.control('') })","new FormGroup({ email: new FormControl(null) })",High,https://angular.dev/guide/forms/typed-forms
22,Forms,Use reactive forms over template-driven,Reactive forms scale better and are fully testable,ReactiveFormsModule for all non-trivial forms,FormsModule with ngModel for complex forms,"<input [formControl]=""emailControl"" />","<input [(ngModel)]=""email"" />",Medium,https://angular.dev/guide/forms/reactive-forms
23,Forms,Write custom validators as functions,Functional validators are composable and tree-shakeable,ValidatorFn functions for custom validation,Class-based validators implementing Validator interface,"const noSpaces: ValidatorFn = ctrl => ctrl.value?.includes(' ') ? { noSpaces: true } : null","class NoSpacesValidator implements Validator { validate(c) {} }",Medium,https://angular.dev/guide/forms/form-validation#custom-validators
24,Forms,Use updateOn for performance,Control when validation runs to avoid per-keystroke validation overhead,updateOn: 'blur' or 'submit' for expensive validators,Default updateOn: 'change' for async validators,"fb.control('' { updateOn: 'blur' validators: [Validators.email] })","fb.control('' [Validators.email]) // validates on every key",Low,https://angular.dev/api/forms/AbstractControl#updateOn
25,Forms,Use FormArray for dynamic fields,FormArray manages variable-length lists of controls,FormArray for add/remove field scenarios,Manually tracking index-based controls,"get items(): FormArray { return this.form.get('items') as FormArray }","items: [FormControl] managed outside form",Medium,https://angular.dev/guide/forms/reactive-forms#using-the-formarray-class
26,Forms,Display validation errors clearly,Use form control touched and dirty states to show errors at the right time,Show errors after field is touched,Show all errors on page load,"@if (email.invalid && email.touched) { <span>Invalid email</span> }","@if (email.invalid) { <span>Invalid email</span> }",Medium,https://angular.dev/guide/forms/form-validation
27,Performance,Apply OnPush to all components,OnPush + signals eliminates most unnecessary change detection cycles,OnPush change detection everywhere,Default strategy which checks entire tree on every event,changeDetection: ChangeDetectionStrategy.OnPush,changeDetection: ChangeDetectionStrategy.Default,High,https://angular.dev/best-practices/skipping-component-subtrees
28,Performance,Use trackBy in @for blocks,Stable identity for list items prevents full DOM re-creation on change,track item.id in @for,"@for (item of items; track item.id) { <li>{{ item.name }}</li> }","@for (item of items; track $index) { <li>{{ item.name }}</li> }",High,https://angular.dev/guide/templates/control-flow#track-and-identity
29,Performance,Use @defer for below-the-fold content,Defer blocks lazy-load components when they enter the viewport,@defer with on viewport for non-critical UI,Eagerly loading all components at startup,"@defer (on viewport) { <HeavyChart /> } @placeholder { <Skeleton /> }","<HeavyChart /> loaded at startup",High,https://angular.dev/guide/defer
30,Performance,Use NgOptimizedImage,Enforces image best practices: lazy loading LCP hints and proper sizing,NgOptimizedImage for all img tags,Plain img tags for CMS or user content,"<img ngSrc=""/hero.jpg"" width=""800"" height=""400"" priority />","<img src=""/hero.jpg"" />",High,https://angular.dev/guide/image-optimization
31,Performance,Tree-shake unused Angular features,Import only what you use from Angular packages,Import specific Angular modules needed,Import BrowserAnimationsModule when not using animations,"import { NgOptimizedImage } from '@angular/common'","import { CommonModule } from '@angular/common' // entire module",Medium,https://angular.dev/tools/cli/build
32,Performance,Avoid subscribe in components,Subscriptions leak and cause bugs; prefer async pipe or toSignal,toSignal() or async pipe instead of manual subscribe,Manual subscribe without unsubscribe in ngOnDestroy,"readonly data = toSignal(this.service.data$)","this.service.data$.subscribe(d => this.data = d)",High,https://angular.dev/guide/rxjs-interop
33,Performance,Use SSR with Angular Universal,Pre-render pages for faster LCP and better SEO,SSR or SSG for public-facing routes,Pure CSR for SEO-critical pages,"ng add @angular/ssr","// no SSR, client renders empty shell",Medium,https://angular.dev/guide/ssr
34,Performance,Minimize bundle with standalone APIs,Standalone components + provideRouter() eliminate dead NgModule code,provideRouter() and provideHttpClient() in app.config,Root AppModule with all imports,provideRouter(routes) in app.config.ts,"@NgModule({ imports: [RouterModule.forRoot(routes)] })",Medium,https://angular.dev/guide/routing/standalone
35,Testing,Use TestBed for component tests,TestBed sets up Angular DI for realistic component testing,TestBed.configureTestingModule for component tests,Instantiate components with new keyword,"TestBed.configureTestingModule({ imports: [MyComponent] })","const comp = new MyComponent()",High,https://angular.dev/guide/testing/components-basics
36,Testing,Use Angular CDK component harnesses,Harnesses provide a stable testing API that survives template refactors,MatButtonHarness and custom HarnessLoader,Direct native element queries that break on template changes,"const btn = await loader.getHarness(MatButtonHarness)","fixture.debugElement.query(By.css('button'))",Medium,https://material.angular.io/cdk/test-harnesses/overview
37,Testing,Use Spectator for less boilerplate,Spectator wraps TestBed with a cleaner API reducing test setup noise,Spectator for unit tests,Raw TestBed for every test,"const spectator = createComponentFactory(MyComponent)","TestBed.configureTestingModule({ declarations: [MyComponent] providers: [...] })",Low,https://github.com/ngneat/spectator
38,Testing,Mock services with jasmine.createSpyObj,Isolate unit tests by providing mock implementations of dependencies,SpyObj or jest.fn() mocks for services,Real HTTP calls in unit tests,"const spy = jasmine.createSpyObj('UserService' ['getUser']); spy.getUser.and.returnValue(of(user))","providers: [UserService] // real service in unit test",High,https://angular.dev/guide/testing/services
39,Testing,Write integration tests for routes,Test full route navigation including guards and resolvers,RouterTestingHarness for route integration tests,Mock all routing behavior in unit tests,"const harness = await RouterTestingHarness.create(); await harness.navigateByUrl('/home')","// manually calling route guard methods",Medium,https://angular.dev/api/router/testing/RouterTestingHarness
40,Testing,Test signal-based components,Signals update synchronously; no async flush needed in most cases,Read signal value directly in test assertions,TestBed.tick() or fakeAsync for signal reads,"component.count.set(5); expect(component.double()).toBe(10)","fakeAsync(() => { component.count.set(5); tick(); expect(component.double()).toBe(10) })",Medium,https://angular.dev/guide/testing
41,Styling,Use ViewEncapsulation.Emulated,Default emulation scopes styles to component preventing global leaks,Emulated or None for intentional global styles,ViewEncapsulation.None for component-specific styles,ViewEncapsulation.Emulated (default),ViewEncapsulation.None on feature components,Medium,https://angular.dev/guide/components/styling#style-scoping
42,Styling,Use :host selector,Style the component's host element using :host pseudo-class,":host for host element styles",Adding wrapper div just for styling,":host { display: block; padding: 1rem }","<div class=""wrapper"">...</div> + .wrapper { padding: 1rem }",Medium,https://angular.dev/guide/components/styling#host-element
43,Styling,Use CSS custom properties for theming,CSS variables work across component boundaries and enable dynamic theming,CSS custom properties for colors and spacing,Hardcoded hex values in component styles,":root { --primary: #6200ee } button { background: var(--primary) }","button { background: #6200ee }",Medium,https://angular.dev/guide/components/styling
44,Styling,Integrate Tailwind with Angular,Tailwind utilities work alongside Angular's ViewEncapsulation via global stylesheet,Add Tailwind in styles.css and use utility classes in templates,Custom CSS for layout that Tailwind already handles,"<div class=""flex items-center gap-4 p-6"">","<div class=""my-custom-flex""> /* .my-custom-flex { display: flex } */",Low,https://tailwindcss.com/docs/guides/angular
45,Styling,Use Angular Material theming tokens,Material 3 uses design tokens for systematic theming,M3 token-based theming for Angular Material,Overriding Angular Material CSS with deep selectors,"@include mat.button-theme($my-theme)","::ng-deep .mat-button { background: red }",Medium,https://material.angular.io/guide/theming
46,Architecture,Use injection tokens for config,Provide configuration via InjectionToken for testability and flexibility,InjectionToken for environment-specific values,Importing environment.ts directly in services,"const API_URL = new InjectionToken<string>('apiUrl'); provide: [{ provide: API_URL useValue: env.apiUrl }]","constructor(private env: Environment) { this.url = env.apiUrl }",Medium,https://angular.dev/guide/di/dependency-injection-providers#using-an-injectiontoken-object
47,Architecture,Use HTTP interceptors,Intercept requests for auth headers error handling and logging,Functional interceptors with withInterceptors(),Service-level header management in every request,"withInterceptors([authInterceptor errorInterceptor])","httpClient.get(url { headers: { Authorization: token } }) in every call",High,https://angular.dev/guide/http/interceptors
48,Architecture,Organize by feature not type,Feature-based folder structure scales better than type-based,Feature folders with collocated component service and routes,Flat folders: all-components/ all-services/,"src/features/checkout/checkout.component.ts checkout.service.ts checkout.routes.ts","src/components/checkout.component.ts src/services/checkout.service.ts",Medium,https://angular.dev/style-guide#folders-by-feature-structure
49,Architecture,Use environment configurations,Separate environment values for dev staging and prod via Angular build configs,angular.json fileReplacements for env configs,Hardcoded API URLs or feature flags in source,"fileReplacements: [{ replace: environment.ts with: environment.prod.ts }]","const API = 'https://api.example.com' // hardcoded in service",High,https://angular.dev/tools/cli/environments
50,Architecture,Prefer inject() over constructor DI,inject() function is composable and works in more contexts than constructor injection,inject() for dependency injection,Constructor parameters for new code,"readonly http = inject(HttpClient); readonly router = inject(Router)","constructor(private http: HttpClient private router: Router) {}",Medium,https://angular.dev/api/core/inject
Can't render this file because it has a wrong number of fields in line 29.

View file

@ -1,54 +0,0 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Architecture,Use Islands Architecture,Astro's partial hydration only loads JS for interactive components,Interactive components with client directives,Hydrate entire page like traditional SPA,<Counter client:load />,Everything as client component,High,https://docs.astro.build/en/concepts/islands/
2,Architecture,Default to zero JS,Astro ships zero JS by default - add only when needed,Static components without client directive,Add client:load to everything,<Header /> (static),<Header client:load /> (unnecessary),High,https://docs.astro.build/en/basics/astro-components/
3,Architecture,Choose right client directive,Different directives for different hydration timing,client:visible for below-fold client:idle for non-critical,client:load for everything,<Comments client:visible />,<Comments client:load />,Medium,https://docs.astro.build/en/reference/directives-reference/#client-directives
4,Architecture,Use content collections,Type-safe content management for blogs docs,Content collections for structured content,Loose markdown files without schema,const posts = await getCollection('blog'),import.meta.glob('./posts/*.md'),High,https://docs.astro.build/en/guides/content-collections/
5,Architecture,Define collection schemas,Zod schemas for content validation,Schema with required fields and types,No schema validation,"defineCollection({ schema: z.object({...}) })",defineCollection({}),High,https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema
6,Routing,Use file-based routing,Create routes by adding .astro files in pages/,pages/ directory for routes,Manual route configuration,src/pages/about.astro,Custom router setup,Medium,https://docs.astro.build/en/basics/astro-pages/
7,Routing,Dynamic routes with brackets,Use [param] for dynamic routes,Bracket notation for params,Query strings for dynamic content,pages/blog/[slug].astro,pages/blog.astro?slug=x,Medium,https://docs.astro.build/en/guides/routing/#dynamic-routes
8,Routing,Use getStaticPaths for SSG,Generate static pages at build time,getStaticPaths for known dynamic routes,Fetch at runtime for static content,"export async function getStaticPaths() { return [...] }",No getStaticPaths with dynamic route,High,https://docs.astro.build/en/reference/api-reference/#getstaticpaths
9,Routing,Enable SSR when needed,Server-side rendering for dynamic content,output: 'server' or 'hybrid' for dynamic,SSR for purely static sites,"export const prerender = false;",SSR for static blog,Medium,https://docs.astro.build/en/guides/server-side-rendering/
10,Components,Keep .astro for static,Use .astro components for static content,Astro components for layout structure,React/Vue for static markup,<Layout><slot /></Layout>,<ReactLayout>{children}</ReactLayout>,High,
11,Components,Use framework components for interactivity,React Vue Svelte for complex interactivity,Framework component with client directive,Astro component with inline scripts,<ReactCounter client:load />,<script> in .astro for complex state,Medium,https://docs.astro.build/en/guides/framework-components/
12,Components,Pass data via props,Astro components receive props in frontmatter,Astro.props for component data,Global state for simple data,"const { title } = Astro.props;",Import global store,Low,https://docs.astro.build/en/basics/astro-components/#component-props
13,Components,Use slots for composition,Named and default slots for flexible layouts,<slot /> for child content,Props for HTML content,<slot name="header" />,<Component header={<div>...</div>} />,Medium,https://docs.astro.build/en/basics/astro-components/#slots
14,Components,Colocate component styles,Scoped styles in component file,<style> in same .astro file,Separate CSS files for component styles,<style> .card { } </style>,import './Card.css',Low,
15,Styling,Use scoped styles by default,Astro scopes styles to component automatically,<style> for component-specific styles,Global styles for everything,<style> h1 { } </style> (scoped),<style is:global> for everything,Medium,https://docs.astro.build/en/guides/styling/#scoped-styles
16,Styling,Use is:global sparingly,Global styles only when truly needed,is:global for base styles or overrides,is:global for component styles,<style is:global> body { } </style>,<style is:global> .card { } </style>,Medium,
17,Styling,Integrate Tailwind properly,Use @astrojs/tailwind integration,Official Tailwind integration,Manual Tailwind setup,npx astro add tailwind,Manual PostCSS config,Low,https://docs.astro.build/en/guides/integrations-guide/tailwind/
18,Styling,Use CSS variables for theming,Define tokens in :root,CSS custom properties for themes,Hardcoded colors everywhere,:root { --primary: #3b82f6; },color: #3b82f6; everywhere,Medium,
19,Data,Fetch in frontmatter,Data fetching in component frontmatter,Top-level await in frontmatter,useEffect for initial data,const data = await fetch(url),client-side fetch on mount,High,https://docs.astro.build/en/guides/data-fetching/
20,Data,Use Astro.glob for local files,Import multiple local files,Astro.glob for markdown/data files,Manual imports for each file,const posts = await Astro.glob('./posts/*.md'),"import post1; import post2;",Medium,
21,Data,Prefer content collections over glob,Type-safe collections for structured content,getCollection() for blog/docs,Astro.glob for structured content,await getCollection('blog'),await Astro.glob('./blog/*.md'),High,
22,Data,Use environment variables correctly,Import.meta.env for env vars,PUBLIC_ prefix for client vars,Expose secrets to client,import.meta.env.PUBLIC_API_URL,import.meta.env.SECRET in client,High,https://docs.astro.build/en/guides/environment-variables/
23,Performance,Preload critical assets,Use link preload for important resources,Preload fonts above-fold images,No preload hints,"<link rel=""preload"" href=""font.woff2"" as=""font"">",No preload for critical assets,Medium,
24,Performance,Optimize images with astro:assets,Built-in image optimization,<Image /> component for optimization,<img> for local images,"import { Image } from 'astro:assets';","<img src=""./image.jpg"">",High,https://docs.astro.build/en/guides/images/
25,Performance,Use picture for responsive images,Multiple formats and sizes,<Picture /> for art direction,Single image size for all screens,<Picture /> with multiple sources,<Image /> with single size,Medium,
26,Performance,Lazy load below-fold content,Defer loading non-critical content,loading=lazy for images client:visible for components,Load everything immediately,"<img loading=""lazy"">",No lazy loading,Medium,
27,Performance,Minimize client directives,Each directive adds JS bundle,Audit client: usage regularly,Sprinkle client:load everywhere,Only interactive components hydrated,Every component with client:load,High,
28,ViewTransitions,Enable View Transitions,Smooth page transitions,<ViewTransitions /> in head,Full page reloads,"import { ViewTransitions } from 'astro:transitions';",No transition API,Medium,https://docs.astro.build/en/guides/view-transitions/
29,ViewTransitions,Use transition:name,Named elements for morphing,transition:name for persistent elements,Unnamed transitions,"<header transition:name=""header"">",<header> without name,Low,
30,ViewTransitions,Handle transition:persist,Keep state across navigations,transition:persist for media players,Re-initialize on every navigation,"<video transition:persist id=""player"">",Video restarts on navigation,Medium,
31,ViewTransitions,Add fallback for no-JS,Graceful degradation,Content works without JS,Require JS for basic navigation,Static content accessible,Broken without ViewTransitions JS,High,
32,SEO,Use built-in SEO component,Head management for meta tags,Astro SEO integration or manual head,No meta tags,"<title>{title}</title><meta name=""description"">",No SEO tags,High,
33,SEO,Generate sitemap,Automatic sitemap generation,@astrojs/sitemap integration,Manual sitemap maintenance,npx astro add sitemap,Hand-written sitemap.xml,Medium,https://docs.astro.build/en/guides/integrations-guide/sitemap/
34,SEO,Add RSS feed for content,RSS for blogs and content sites,@astrojs/rss for feed generation,No RSS feed,rss() helper in pages/rss.xml.js,No feed for blog,Low,https://docs.astro.build/en/guides/rss/
35,SEO,Use canonical URLs,Prevent duplicate content issues,Astro.url for canonical generation,"<link rel=""canonical"" href={Astro.url}>",No canonical tags,Medium,
36,Integrations,Use official integrations,Astro's integration system,npx astro add for integrations,Manual configuration,npx astro add react,Manual React setup,Medium,https://docs.astro.build/en/guides/integrations-guide/
37,Integrations,Configure integrations in astro.config,Centralized configuration,integrations array in config,Scattered configuration,"integrations: [react(), tailwind()]",Multiple config files,Low,
38,Integrations,Use adapter for deployment,Platform-specific adapters,Correct adapter for host,Wrong or no adapter,@astrojs/vercel for Vercel,No adapter for SSR,High,https://docs.astro.build/en/guides/deploy/
39,TypeScript,Enable TypeScript,Type safety for Astro projects,tsconfig.json with astro types,No TypeScript,Astro TypeScript template,JavaScript only,Medium,https://docs.astro.build/en/guides/typescript/
40,TypeScript,Type component props,Define prop interfaces,Props interface in frontmatter,Untyped props,"interface Props { title: string }",No props typing,Medium,
41,TypeScript,Use strict mode,Catch errors early,strict: true in tsconfig,Loose TypeScript config,strictest template,base template,Low,
42,Markdown,Use MDX for components,Components in markdown content,@astrojs/mdx for interactive docs,Plain markdown with workarounds,<Component /> in .mdx,HTML in .md files,Medium,https://docs.astro.build/en/guides/integrations-guide/mdx/
43,Markdown,Configure markdown plugins,Extend markdown capabilities,remarkPlugins rehypePlugins in config,Manual HTML for features,remarkPlugins: [remarkToc],Manual TOC in every post,Low,
44,Markdown,Use frontmatter for metadata,Structured post metadata,Frontmatter with typed schema,Inline metadata,title date in frontmatter,# Title as first line,Medium,
45,API,Use API routes for endpoints,Server endpoints in pages/api,pages/api/[endpoint].ts for APIs,External API for simple endpoints,pages/api/posts.json.ts,Separate Express server,Medium,https://docs.astro.build/en/guides/endpoints/
46,API,Return proper responses,Use Response object,new Response() with headers,Plain objects,return new Response(JSON.stringify(data)),return data,Medium,
47,API,Handle methods correctly,Export named method handlers,export GET POST handlers,Single default export,export const GET = async () => {},export default async () => {},Low,
48,Security,Sanitize user content,Prevent XSS in dynamic content,set:html only for trusted content,set:html with user input,"<Fragment set:html={sanitized} />","<div set:html={userInput} />",High,
49,Security,Use HTTPS in production,Secure connections,HTTPS for all production sites,HTTP in production,https://example.com,http://example.com,High,
50,Security,Validate API input,Check and sanitize all input,Zod validation for API routes,Trust all input,const body = schema.parse(data),const body = await request.json(),High,
51,Build,Use hybrid rendering,Mix static and dynamic pages,output: 'hybrid' for flexibility,All SSR or all static,prerender per-page basis,Single rendering mode,Medium,https://docs.astro.build/en/guides/server-side-rendering/#hybrid-rendering
52,Build,Analyze bundle size,Monitor JS bundle impact,Build output shows bundle sizes,Ignore bundle growth,Check astro build output,No size monitoring,Medium,
53,Build,Use prefetch,Preload linked pages,prefetch integration,No prefetch for navigation,npx astro add prefetch,Manual prefetch,Low,https://docs.astro.build/en/guides/prefetch/
Can't render this file because it contains an unexpected character in line 14 and column 147.

View file

@ -1,53 +0,0 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Widgets,Use StatelessWidget when possible,Immutable widgets are simpler,StatelessWidget for static UI,StatefulWidget for everything,class MyWidget extends StatelessWidget,class MyWidget extends StatefulWidget (static),Medium,https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
2,Widgets,Keep widgets small,Single responsibility principle,Extract widgets into smaller pieces,Large build methods,Column(children: [Header() Content()]),500+ line build method,Medium,
3,Widgets,Use const constructors,Compile-time constants for performance,const MyWidget() when possible,Non-const for static widgets,const Text('Hello'),Text('Hello') for literals,High,https://dart.dev/guides/language/language-tour#constant-constructors
4,Widgets,Prefer composition over inheritance,Combine widgets using children,Compose widgets,Extend widget classes,Container(child: MyContent()),class MyContainer extends Container,Medium,
5,State,Use setState correctly,Minimal state in StatefulWidget,setState for UI state changes,setState for business logic,setState(() { _counter++; }),Complex logic in setState,Medium,https://api.flutter.dev/flutter/widgets/State/setState.html
6,State,Avoid setState in build,Never call setState during build,setState in callbacks only,setState in build method,onPressed: () => setState(() {}),build() { setState(); },High,
7,State,Use state management for complex apps,Provider Riverpod BLoC,State management for shared state,setState for global state,Provider.of<MyState>(context),Global setState calls,Medium,
8,State,Prefer Riverpod or Provider,Recommended state solutions,Riverpod for new projects,InheritedWidget manually,ref.watch(myProvider),Custom InheritedWidget,Medium,https://riverpod.dev/
9,State,Dispose resources,Clean up controllers and subscriptions,dispose() for cleanup,Memory leaks from subscriptions,@override void dispose() { controller.dispose(); },No dispose implementation,High,
10,Layout,Use Column and Row,Basic layout widgets,Column Row for linear layouts,Stack for simple layouts,"Column(children: [Text(), Button()])",Stack for vertical list,Medium,https://api.flutter.dev/flutter/widgets/Column-class.html
11,Layout,Use Expanded and Flexible,Control flex behavior,Expanded to fill space,Fixed sizes in flex containers,Expanded(child: Container()),Container(width: 200) in Row,Medium,
12,Layout,Use SizedBox for spacing,Consistent spacing,SizedBox for gaps,Container for spacing only,SizedBox(height: 16),Container(height: 16),Low,
13,Layout,Use LayoutBuilder for responsive,Respond to constraints,LayoutBuilder for adaptive layouts,Fixed sizes for responsive,LayoutBuilder(builder: (context constraints) {}),Container(width: 375),Medium,https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html
14,Layout,Avoid deep nesting,Keep widget tree shallow,Extract deeply nested widgets,10+ levels of nesting,Extract widget to method or class,Column(Row(Column(Row(...)))),Medium,
15,Lists,Use ListView.builder,Lazy list building,ListView.builder for long lists,ListView with children for large lists,"ListView.builder(itemCount: 100, itemBuilder: ...)",ListView(children: items.map(...).toList()),High,https://api.flutter.dev/flutter/widgets/ListView-class.html
16,Lists,Provide itemExtent when known,Skip measurement,itemExtent for fixed height items,No itemExtent for uniform lists,ListView.builder(itemExtent: 50),ListView.builder without itemExtent,Medium,
17,Lists,Use keys for stateful items,Preserve widget state,Key for stateful list items,No key for dynamic lists,ListTile(key: ValueKey(item.id)),ListTile without key,High,
18,Lists,Use SliverList for custom scroll,Custom scroll effects,CustomScrollView with Slivers,Nested ListViews,CustomScrollView(slivers: [SliverList()]),ListView inside ListView,Medium,https://api.flutter.dev/flutter/widgets/SliverList-class.html
19,Navigation,Use Navigator 2.0 or GoRouter,Declarative routing,go_router for navigation,Navigator.push for complex apps,GoRouter(routes: [...]),Navigator.push everywhere,Medium,https://pub.dev/packages/go_router
20,Navigation,Use named routes,Organized navigation,Named routes for clarity,Anonymous routes,Navigator.pushNamed(context '/home'),Navigator.push(context MaterialPageRoute()),Low,
21,Navigation,Handle back button (PopScope),Android back behavior and predictive back (Android 14+),Use PopScope widget (WillPopScope is deprecated),Use WillPopScope,"PopScope(canPop: false, onPopInvoked: (didPop) => ...)",WillPopScope(onWillPop: ...),High,https://api.flutter.dev/flutter/widgets/PopScope-class.html
22,Navigation,Pass typed arguments,Type-safe route arguments,Typed route arguments,Dynamic arguments,MyRoute(id: '123'),arguments: {'id': '123'},Medium,
23,Async,Use FutureBuilder,Async UI building,FutureBuilder for async data,setState for async,FutureBuilder(future: fetchData()),fetchData().then((d) => setState()),Medium,https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
24,Async,Use StreamBuilder,Stream UI building,StreamBuilder for streams,Manual stream subscription,StreamBuilder(stream: myStream),stream.listen in initState,Medium,https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
25,Async,Handle loading and error states,Complete async UI states,ConnectionState checks,Only success state,if (snapshot.connectionState == ConnectionState.waiting),No loading indicator,High,
26,Async,Cancel subscriptions,Clean up stream subscriptions,Cancel in dispose,Memory leaks,subscription.cancel() in dispose,No subscription cleanup,High,
27,Theming,Use ThemeData,Consistent theming,ThemeData for app theme,Hardcoded colors,Theme.of(context).primaryColor,Color(0xFF123456) everywhere,Medium,https://api.flutter.dev/flutter/material/ThemeData-class.html
28,Theming,Use ColorScheme,Material 3 color system,ColorScheme for colors,Individual color properties,colorScheme: ColorScheme.fromSeed(),primaryColor: Colors.blue,Medium,
29,Theming,Access theme via context,Dynamic theme access,Theme.of(context),Static theme reference,Theme.of(context).textTheme.bodyLarge,TextStyle(fontSize: 16),Medium,
30,Theming,Support dark mode,Respect system theme,darkTheme in MaterialApp,Light theme only,"MaterialApp(theme: light, darkTheme: dark)",MaterialApp(theme: light),Medium,
31,Animation,Use implicit animations,Simple animations,AnimatedContainer AnimatedOpacity,Explicit for simple transitions,AnimatedContainer(duration: Duration()),AnimationController for fade,Low,https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html
32,Animation,Use AnimationController for complex,Fine-grained control,AnimationController with Ticker,Implicit for complex sequences,AnimationController(vsync: this),AnimatedContainer for staggered,Medium,
33,Animation,Dispose AnimationControllers,Clean up animation resources,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High,
34,Animation,Use Hero for transitions,Shared element transitions,Hero for navigation animations,Manual shared element,Hero(tag: 'image' child: Image()),Custom shared element animation,Low,https://api.flutter.dev/flutter/widgets/Hero-class.html
35,Forms,Use Form widget,Form validation,Form with GlobalKey,Individual validation,Form(key: _formKey child: ...),TextField without Form,Medium,https://api.flutter.dev/flutter/widgets/Form-class.html
36,Forms,Use TextEditingController,Control text input,Controller for text fields,onChanged for all text,final controller = TextEditingController(),onChanged: (v) => setState(),Medium,
37,Forms,Validate on submit,Form validation flow,_formKey.currentState!.validate(),Skip validation,if (_formKey.currentState!.validate()),Submit without validation,High,
38,Forms,Dispose controllers,Clean up text controllers,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High,
39,Performance,Use const widgets,Reduce rebuilds,const for static widgets,No const for literals,const Icon(Icons.add),Icon(Icons.add),High,
40,Performance,Avoid rebuilding entire tree,Minimal rebuild scope,Isolate changing widgets,setState on parent,Consumer only around changing widget,setState on root widget,High,
41,Performance,Use RepaintBoundary,Isolate repaints,RepaintBoundary for animations,Full screen repaints,RepaintBoundary(child: AnimatedWidget()),Animation without boundary,Medium,https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html
42,Performance,Profile with DevTools,Measure before optimizing,Flutter DevTools profiling,Guess at performance,DevTools performance tab,Optimize without measuring,Medium,https://docs.flutter.dev/tools/devtools
43,Accessibility,Use Semantics widget,Screen reader support,Semantics for accessibility,Missing accessibility info,Semantics(label: 'Submit button'),GestureDetector without semantics,High,https://api.flutter.dev/flutter/widgets/Semantics-class.html
44,Accessibility,Support large fonts,MediaQuery text scaling,MediaQuery.textScaleFactor,Fixed font sizes,style: Theme.of(context).textTheme,TextStyle(fontSize: 14),High,
45,Accessibility,Test with screen readers,TalkBack and VoiceOver,Test accessibility regularly,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High,
46,Testing,Use widget tests,Test widget behavior,WidgetTester for UI tests,Unit tests only,testWidgets('...' (tester) async {}),Only test() for UI,Medium,https://docs.flutter.dev/testing
47,Testing,Use integration tests,Full app testing,integration_test package,Manual testing only,IntegrationTestWidgetsFlutterBinding,Manual E2E testing,Medium,
48,Testing,Mock dependencies,Isolate tests,Mockito or mocktail,Real dependencies in tests,when(mock.method()).thenReturn(),Real API calls in tests,Medium,
49,Platform,Use Platform checks,Platform-specific code,Platform.isIOS Platform.isAndroid,Same code for all platforms,if (Platform.isIOS) {},Hardcoded iOS behavior,Medium,
50,Platform,Use kIsWeb for web,Web platform detection,kIsWeb for web checks,Platform for web,if (kIsWeb) {},Platform.isWeb (doesn't exist),Medium,
51,Packages,Use pub.dev packages,Community packages,Popular maintained packages,Custom implementations,cached_network_image,Custom image cache,Medium,https://pub.dev/
52,Packages,Check package quality,Quality before adding,Pub points and popularity,Any package without review,100+ pub points,Unmaintained packages,Medium,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Widgets Use StatelessWidget when possible Immutable widgets are simpler StatelessWidget for static UI StatefulWidget for everything class MyWidget extends StatelessWidget class MyWidget extends StatefulWidget (static) Medium https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
3 2 Widgets Keep widgets small Single responsibility principle Extract widgets into smaller pieces Large build methods Column(children: [Header() Content()]) 500+ line build method Medium
4 3 Widgets Use const constructors Compile-time constants for performance const MyWidget() when possible Non-const for static widgets const Text('Hello') Text('Hello') for literals High https://dart.dev/guides/language/language-tour#constant-constructors
5 4 Widgets Prefer composition over inheritance Combine widgets using children Compose widgets Extend widget classes Container(child: MyContent()) class MyContainer extends Container Medium
6 5 State Use setState correctly Minimal state in StatefulWidget setState for UI state changes setState for business logic setState(() { _counter++; }) Complex logic in setState Medium https://api.flutter.dev/flutter/widgets/State/setState.html
7 6 State Avoid setState in build Never call setState during build setState in callbacks only setState in build method onPressed: () => setState(() {}) build() { setState(); } High
8 7 State Use state management for complex apps Provider Riverpod BLoC State management for shared state setState for global state Provider.of<MyState>(context) Global setState calls Medium
9 8 State Prefer Riverpod or Provider Recommended state solutions Riverpod for new projects InheritedWidget manually ref.watch(myProvider) Custom InheritedWidget Medium https://riverpod.dev/
10 9 State Dispose resources Clean up controllers and subscriptions dispose() for cleanup Memory leaks from subscriptions @override void dispose() { controller.dispose(); } No dispose implementation High
11 10 Layout Use Column and Row Basic layout widgets Column Row for linear layouts Stack for simple layouts Column(children: [Text(), Button()]) Stack for vertical list Medium https://api.flutter.dev/flutter/widgets/Column-class.html
12 11 Layout Use Expanded and Flexible Control flex behavior Expanded to fill space Fixed sizes in flex containers Expanded(child: Container()) Container(width: 200) in Row Medium
13 12 Layout Use SizedBox for spacing Consistent spacing SizedBox for gaps Container for spacing only SizedBox(height: 16) Container(height: 16) Low
14 13 Layout Use LayoutBuilder for responsive Respond to constraints LayoutBuilder for adaptive layouts Fixed sizes for responsive LayoutBuilder(builder: (context constraints) {}) Container(width: 375) Medium https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html
15 14 Layout Avoid deep nesting Keep widget tree shallow Extract deeply nested widgets 10+ levels of nesting Extract widget to method or class Column(Row(Column(Row(...)))) Medium
16 15 Lists Use ListView.builder Lazy list building ListView.builder for long lists ListView with children for large lists ListView.builder(itemCount: 100, itemBuilder: ...) ListView(children: items.map(...).toList()) High https://api.flutter.dev/flutter/widgets/ListView-class.html
17 16 Lists Provide itemExtent when known Skip measurement itemExtent for fixed height items No itemExtent for uniform lists ListView.builder(itemExtent: 50) ListView.builder without itemExtent Medium
18 17 Lists Use keys for stateful items Preserve widget state Key for stateful list items No key for dynamic lists ListTile(key: ValueKey(item.id)) ListTile without key High
19 18 Lists Use SliverList for custom scroll Custom scroll effects CustomScrollView with Slivers Nested ListViews CustomScrollView(slivers: [SliverList()]) ListView inside ListView Medium https://api.flutter.dev/flutter/widgets/SliverList-class.html
20 19 Navigation Use Navigator 2.0 or GoRouter Declarative routing go_router for navigation Navigator.push for complex apps GoRouter(routes: [...]) Navigator.push everywhere Medium https://pub.dev/packages/go_router
21 20 Navigation Use named routes Organized navigation Named routes for clarity Anonymous routes Navigator.pushNamed(context '/home') Navigator.push(context MaterialPageRoute()) Low
22 21 Navigation Handle back button (PopScope) Android back behavior and predictive back (Android 14+) Use PopScope widget (WillPopScope is deprecated) Use WillPopScope PopScope(canPop: false, onPopInvoked: (didPop) => ...) WillPopScope(onWillPop: ...) High https://api.flutter.dev/flutter/widgets/PopScope-class.html
23 22 Navigation Pass typed arguments Type-safe route arguments Typed route arguments Dynamic arguments MyRoute(id: '123') arguments: {'id': '123'} Medium
24 23 Async Use FutureBuilder Async UI building FutureBuilder for async data setState for async FutureBuilder(future: fetchData()) fetchData().then((d) => setState()) Medium https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
25 24 Async Use StreamBuilder Stream UI building StreamBuilder for streams Manual stream subscription StreamBuilder(stream: myStream) stream.listen in initState Medium https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
26 25 Async Handle loading and error states Complete async UI states ConnectionState checks Only success state if (snapshot.connectionState == ConnectionState.waiting) No loading indicator High
27 26 Async Cancel subscriptions Clean up stream subscriptions Cancel in dispose Memory leaks subscription.cancel() in dispose No subscription cleanup High
28 27 Theming Use ThemeData Consistent theming ThemeData for app theme Hardcoded colors Theme.of(context).primaryColor Color(0xFF123456) everywhere Medium https://api.flutter.dev/flutter/material/ThemeData-class.html
29 28 Theming Use ColorScheme Material 3 color system ColorScheme for colors Individual color properties colorScheme: ColorScheme.fromSeed() primaryColor: Colors.blue Medium
30 29 Theming Access theme via context Dynamic theme access Theme.of(context) Static theme reference Theme.of(context).textTheme.bodyLarge TextStyle(fontSize: 16) Medium
31 30 Theming Support dark mode Respect system theme darkTheme in MaterialApp Light theme only MaterialApp(theme: light, darkTheme: dark) MaterialApp(theme: light) Medium
32 31 Animation Use implicit animations Simple animations AnimatedContainer AnimatedOpacity Explicit for simple transitions AnimatedContainer(duration: Duration()) AnimationController for fade Low https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html
33 32 Animation Use AnimationController for complex Fine-grained control AnimationController with Ticker Implicit for complex sequences AnimationController(vsync: this) AnimatedContainer for staggered Medium
34 33 Animation Dispose AnimationControllers Clean up animation resources dispose() for controllers Memory leaks controller.dispose() in dispose No controller disposal High
35 34 Animation Use Hero for transitions Shared element transitions Hero for navigation animations Manual shared element Hero(tag: 'image' child: Image()) Custom shared element animation Low https://api.flutter.dev/flutter/widgets/Hero-class.html
36 35 Forms Use Form widget Form validation Form with GlobalKey Individual validation Form(key: _formKey child: ...) TextField without Form Medium https://api.flutter.dev/flutter/widgets/Form-class.html
37 36 Forms Use TextEditingController Control text input Controller for text fields onChanged for all text final controller = TextEditingController() onChanged: (v) => setState() Medium
38 37 Forms Validate on submit Form validation flow _formKey.currentState!.validate() Skip validation if (_formKey.currentState!.validate()) Submit without validation High
39 38 Forms Dispose controllers Clean up text controllers dispose() for controllers Memory leaks controller.dispose() in dispose No controller disposal High
40 39 Performance Use const widgets Reduce rebuilds const for static widgets No const for literals const Icon(Icons.add) Icon(Icons.add) High
41 40 Performance Avoid rebuilding entire tree Minimal rebuild scope Isolate changing widgets setState on parent Consumer only around changing widget setState on root widget High
42 41 Performance Use RepaintBoundary Isolate repaints RepaintBoundary for animations Full screen repaints RepaintBoundary(child: AnimatedWidget()) Animation without boundary Medium https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html
43 42 Performance Profile with DevTools Measure before optimizing Flutter DevTools profiling Guess at performance DevTools performance tab Optimize without measuring Medium https://docs.flutter.dev/tools/devtools
44 43 Accessibility Use Semantics widget Screen reader support Semantics for accessibility Missing accessibility info Semantics(label: 'Submit button') GestureDetector without semantics High https://api.flutter.dev/flutter/widgets/Semantics-class.html
45 44 Accessibility Support large fonts MediaQuery text scaling MediaQuery.textScaleFactor Fixed font sizes style: Theme.of(context).textTheme TextStyle(fontSize: 14) High
46 45 Accessibility Test with screen readers TalkBack and VoiceOver Test accessibility regularly Skip accessibility testing Regular TalkBack testing No screen reader testing High
47 46 Testing Use widget tests Test widget behavior WidgetTester for UI tests Unit tests only testWidgets('...' (tester) async {}) Only test() for UI Medium https://docs.flutter.dev/testing
48 47 Testing Use integration tests Full app testing integration_test package Manual testing only IntegrationTestWidgetsFlutterBinding Manual E2E testing Medium
49 48 Testing Mock dependencies Isolate tests Mockito or mocktail Real dependencies in tests when(mock.method()).thenReturn() Real API calls in tests Medium
50 49 Platform Use Platform checks Platform-specific code Platform.isIOS Platform.isAndroid Same code for all platforms if (Platform.isIOS) {} Hardcoded iOS behavior Medium
51 50 Platform Use kIsWeb for web Web platform detection kIsWeb for web checks Platform for web if (kIsWeb) {} Platform.isWeb (doesn't exist) Medium
52 51 Packages Use pub.dev packages Community packages Popular maintained packages Custom implementations cached_network_image Custom image cache Medium https://pub.dev/
53 52 Packages Check package quality Quality before adding Pub points and popularity Any package without review 100+ pub points Unmaintained packages Medium

View file

@ -1,56 +0,0 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Animation,Use Tailwind animate utilities,Built-in animations are optimized and respect reduced-motion,Use animate-pulse animate-spin animate-ping,Custom @keyframes for simple effects,animate-pulse,@keyframes pulse {...},Medium,https://tailwindcss.com/docs/animation
2,Animation,Limit bounce animations,Continuous bounce is distracting and causes motion sickness,Use animate-bounce sparingly on CTAs only,Multiple bounce animations on page,Single CTA with animate-bounce,5+ elements with animate-bounce,High,
3,Animation,Transition duration,Use appropriate transition speeds for UI feedback,duration-150 to duration-300 for UI,duration-1000 or longer for UI elements,transition-all duration-200,transition-all duration-1000,Medium,https://tailwindcss.com/docs/transition-duration
4,Animation,Hover transitions,Add smooth transitions on hover state changes,Add transition class with hover states,Instant hover changes without transition,hover:bg-gray-100 transition-colors,hover:bg-gray-100 (no transition),Low,
5,Z-Index,Use Tailwind z-* scale,Consistent stacking context with predefined scale,z-0 z-10 z-20 z-30 z-40 z-50,Arbitrary z-index values,z-50 for modals,z-[9999],Medium,https://tailwindcss.com/docs/z-index
6,Z-Index,Fixed elements z-index,Fixed navigation and modals need explicit z-index,z-50 for nav z-40 for dropdowns,Relying on DOM order for stacking,fixed top-0 z-50,fixed top-0 (no z-index),High,
7,Z-Index,Negative z-index for backgrounds,Use negative z-index for decorative backgrounds,z-[-1] for background elements,Positive z-index for backgrounds,-z-10 for decorative,z-10 for background,Low,
8,Layout,Container max-width,Limit content width for readability,max-w-7xl mx-auto for main content,Full-width content on large screens,max-w-7xl mx-auto px-4,w-full (no max-width),Medium,https://tailwindcss.com/docs/container
9,Layout,Responsive padding,Adjust padding for different screen sizes,px-4 md:px-6 lg:px-8,Same padding all sizes,px-4 sm:px-6 lg:px-8,px-8 (same all sizes),Medium,
10,Layout,Grid gaps,Use consistent gap utilities for spacing,gap-4 gap-6 gap-8,Margins on individual items,grid gap-6,grid with mb-4 on each item,Medium,https://tailwindcss.com/docs/gap
11,Layout,Flexbox alignment,Use flex utilities for alignment,items-center justify-between,Multiple nested wrappers,flex items-center justify-between,Nested divs for alignment,Low,
12,Images,Aspect ratio,Maintain consistent image aspect ratios,aspect-video aspect-square,No aspect ratio on containers,aspect-video rounded-lg,No aspect control,Medium,https://tailwindcss.com/docs/aspect-ratio
13,Images,Object fit,Control image scaling within containers,object-cover object-contain,Stretched distorted images,object-cover w-full h-full,No object-fit,Medium,https://tailwindcss.com/docs/object-fit
14,Images,Lazy loading,Defer loading of off-screen images,loading='lazy' on images,All images eager load,<img loading='lazy'>,<img> without lazy,High,
15,Images,Responsive images,Serve appropriate image sizes,srcset and sizes attributes,Same large image all devices,srcset with multiple sizes,4000px image everywhere,High,
16,Typography,Prose plugin,Use @tailwindcss/typography for rich text,prose prose-lg for article content,Custom styles for markdown,prose prose-lg max-w-none,Custom text styling,Medium,https://tailwindcss.com/docs/typography-plugin
17,Typography,Line height,Use appropriate line height for readability,leading-relaxed for body text,Default tight line height,leading-relaxed (1.625),leading-none or leading-tight,Medium,https://tailwindcss.com/docs/line-height
18,Typography,Font size scale,Use consistent text size scale,text-sm text-base text-lg text-xl,Arbitrary font sizes,text-lg,text-[17px],Low,https://tailwindcss.com/docs/font-size
19,Typography,Text truncation,Handle long text gracefully,truncate or line-clamp-*,Overflow breaking layout,line-clamp-2,No overflow handling,Medium,https://tailwindcss.com/docs/text-overflow
20,Colors,Opacity utilities,Use color opacity utilities,bg-black/50 text-white/80,Separate opacity class,bg-black/50,bg-black opacity-50,Low,https://tailwindcss.com/docs/background-color
21,Colors,Dark mode,Support dark mode with dark: prefix,dark:bg-gray-900 dark:text-white,No dark mode support,dark:bg-gray-900,Only light theme,Medium,https://tailwindcss.com/docs/dark-mode
22,Colors,Semantic colors,Use semantic color naming in config,primary secondary danger success,Generic color names in components,bg-primary,bg-blue-500 everywhere,Medium,
23,Spacing,Consistent spacing scale,Use Tailwind spacing scale consistently,p-4 m-6 gap-8,Arbitrary pixel values,p-4 (1rem),p-[15px],Low,https://tailwindcss.com/docs/customizing-spacing
24,Spacing,Negative margins,Use sparingly for overlapping effects,-mt-4 for overlapping elements,Negative margins for layout fixing,-mt-8 for card overlap,-m-2 to fix spacing issues,Medium,
25,Spacing,Space between,Use space-y-* for vertical lists,space-y-4 on flex/grid column,Margin on each child,space-y-4,Each child has mb-4,Low,https://tailwindcss.com/docs/space
26,Forms,Focus states,Always show focus indicators,focus:ring-2 focus:ring-blue-500,Remove focus outline,focus:ring-2 focus:ring-offset-2,focus:outline-none (no replacement),High,
27,Forms,Input sizing,Consistent input dimensions,h-10 px-3 for inputs,Inconsistent input heights,h-10 w-full px-3,Various heights per input,Medium,
28,Forms,Disabled states,Clear disabled styling,disabled:opacity-50 disabled:cursor-not-allowed,No disabled indication,disabled:opacity-50,Same style as enabled,Medium,
29,Forms,Placeholder styling,Style placeholder text appropriately,placeholder:text-gray-400,Dark placeholder text,placeholder:text-gray-400,Default dark placeholder,Low,
30,Responsive,Mobile-first approach,Start with mobile styles and add breakpoints,Default mobile + md: lg: xl:,Desktop-first approach,text-sm md:text-base,text-base max-md:text-sm,Medium,https://tailwindcss.com/docs/responsive-design
31,Responsive,Breakpoint testing,Test at standard breakpoints,320 375 768 1024 1280 1536,Only test on development device,Test all breakpoints,Single device testing,High,
32,Responsive,Hidden/shown utilities,Control visibility per breakpoint,hidden md:block,Different content per breakpoint,hidden md:flex,Separate mobile/desktop components,Low,https://tailwindcss.com/docs/display
33,Buttons,Button sizing,Consistent button dimensions,px-4 py-2 or px-6 py-3,Inconsistent button sizes,px-4 py-2 text-sm,Various padding per button,Medium,
34,Buttons,Touch targets,Minimum 44px touch target on mobile,min-h-[44px] on mobile,Small buttons on mobile,min-h-[44px] min-w-[44px],h-8 w-8 on mobile,High,
35,Buttons,Loading states,Show loading feedback,disabled + spinner icon,Clickable during loading,<Button disabled><Spinner/></Button>,Button without loading state,High,
36,Buttons,Icon buttons,Accessible icon-only buttons,aria-label on icon buttons,Icon button without label,<button aria-label='Close'><XIcon/></button>,<button><XIcon/></button>,High,
37,Cards,Card structure,Consistent card styling,rounded-lg shadow-md p-6,Inconsistent card styles,rounded-2xl shadow-lg p-6,Mixed card styling,Low,
38,Cards,Card hover states,Interactive cards should have hover feedback,hover:shadow-lg transition-shadow,No hover on clickable cards,hover:shadow-xl transition-shadow,Static cards that are clickable,Medium,
39,Cards,Card spacing,Consistent internal card spacing,space-y-4 for card content,Inconsistent internal spacing,space-y-4 or p-6,Mixed mb-2 mb-4 mb-6,Low,
40,Accessibility,Screen reader text,Provide context for screen readers,sr-only for hidden labels,Missing context for icons,<span class='sr-only'>Close menu</span>,No label for icon button,High,https://tailwindcss.com/docs/screen-readers
41,Accessibility,Focus visible,Show focus only for keyboard users,focus-visible:ring-2,Focus on all interactions,focus-visible:ring-2,focus:ring-2 (shows on click too),Medium,
42,Accessibility,Reduced motion,Respect user motion preferences,motion-reduce:animate-none,Ignore motion preferences,motion-reduce:transition-none,No reduced motion support,High,https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion
43,Performance,Configure content paths,Tailwind needs to know where classes are used,Use 'content' array in config,Use deprecated 'purge' option (v2),"content: ['./src/**/*.{js,ts,jsx,tsx}']",purge: [...],High,https://tailwindcss.com/docs/content-configuration
44,Performance,JIT mode,Use JIT for faster builds and smaller bundles,JIT enabled (default in v3),Full CSS in development,Tailwind v3 defaults,Tailwind v2 without JIT,Medium,
45,Performance,Avoid @apply bloat,Use @apply sparingly,Direct utilities in HTML,Heavy @apply usage,class='px-4 py-2 rounded',@apply px-4 py-2 rounded;,Low,https://tailwindcss.com/docs/reusing-styles
46,Plugins,Official plugins,Use official Tailwind plugins,@tailwindcss/forms typography aspect-ratio,Custom implementations,@tailwindcss/forms,Custom form reset CSS,Medium,https://tailwindcss.com/docs/plugins
47,Plugins,Custom utilities,Create utilities for repeated patterns,Custom utility in config,Repeated arbitrary values,Custom shadow utility,"shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere",Medium,
48,Layout,Container Queries,Use @container for component-based responsiveness,Use @container and @lg: etc.,Media queries for component internals,@container @lg:grid-cols-2,@media (min-width: ...) inside component,Medium,https://github.com/tailwindlabs/tailwindcss-container-queries
49,Interactivity,Group and Peer,Style based on parent/sibling state,group-hover peer-checked,JS for simple state interactions,group-hover:text-blue-500,onMouseEnter={() => setHover(true)},Low,https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
50,Customization,Arbitrary Values,Use [] for one-off values,w-[350px] for specific needs,Creating config for single use,top-[117px] (if strictly needed),style={{ top: '117px' }},Low,https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values
51,Colors,Theme color variables,Define colors in Tailwind theme and use directly,bg-primary text-success border-cta,bg-[var(--color-primary)] text-[var(--color-success)],bg-primary,bg-[var(--color-primary)],Medium,https://tailwindcss.com/docs/customizing-colors
52,Colors,Use bg-linear-to-* for gradients,Tailwind v4 uses bg-linear-to-* syntax for gradients,bg-linear-to-r bg-linear-to-b,bg-gradient-to-* (deprecated in v4),bg-linear-to-r from-blue-500 to-purple-500,bg-gradient-to-r from-blue-500 to-purple-500,Medium,https://tailwindcss.com/docs/background-image
53,Layout,Use shrink-0 shorthand,Shorter class name for flex-shrink-0,shrink-0 shrink,flex-shrink-0 flex-shrink,shrink-0,flex-shrink-0,Low,https://tailwindcss.com/docs/flex-shrink
54,Layout,Use size-* for square dimensions,Single utility for equal width and height,size-4 size-8 size-12,Separate h-* w-* for squares,size-6,h-6 w-6,Low,https://tailwindcss.com/docs/size
55,Images,SVG explicit dimensions,Add width/height attributes to SVGs to prevent layout shift before CSS loads,<svg class='size-6' width='24' height='24'>,SVG without explicit dimensions,<svg class='size-6' width='24' height='24'>,<svg class='size-6'>,High,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Animation Use Tailwind animate utilities Built-in animations are optimized and respect reduced-motion Use animate-pulse animate-spin animate-ping Custom @keyframes for simple effects animate-pulse @keyframes pulse {...} Medium https://tailwindcss.com/docs/animation
3 2 Animation Limit bounce animations Continuous bounce is distracting and causes motion sickness Use animate-bounce sparingly on CTAs only Multiple bounce animations on page Single CTA with animate-bounce 5+ elements with animate-bounce High
4 3 Animation Transition duration Use appropriate transition speeds for UI feedback duration-150 to duration-300 for UI duration-1000 or longer for UI elements transition-all duration-200 transition-all duration-1000 Medium https://tailwindcss.com/docs/transition-duration
5 4 Animation Hover transitions Add smooth transitions on hover state changes Add transition class with hover states Instant hover changes without transition hover:bg-gray-100 transition-colors hover:bg-gray-100 (no transition) Low
6 5 Z-Index Use Tailwind z-* scale Consistent stacking context with predefined scale z-0 z-10 z-20 z-30 z-40 z-50 Arbitrary z-index values z-50 for modals z-[9999] Medium https://tailwindcss.com/docs/z-index
7 6 Z-Index Fixed elements z-index Fixed navigation and modals need explicit z-index z-50 for nav z-40 for dropdowns Relying on DOM order for stacking fixed top-0 z-50 fixed top-0 (no z-index) High
8 7 Z-Index Negative z-index for backgrounds Use negative z-index for decorative backgrounds z-[-1] for background elements Positive z-index for backgrounds -z-10 for decorative z-10 for background Low
9 8 Layout Container max-width Limit content width for readability max-w-7xl mx-auto for main content Full-width content on large screens max-w-7xl mx-auto px-4 w-full (no max-width) Medium https://tailwindcss.com/docs/container
10 9 Layout Responsive padding Adjust padding for different screen sizes px-4 md:px-6 lg:px-8 Same padding all sizes px-4 sm:px-6 lg:px-8 px-8 (same all sizes) Medium
11 10 Layout Grid gaps Use consistent gap utilities for spacing gap-4 gap-6 gap-8 Margins on individual items grid gap-6 grid with mb-4 on each item Medium https://tailwindcss.com/docs/gap
12 11 Layout Flexbox alignment Use flex utilities for alignment items-center justify-between Multiple nested wrappers flex items-center justify-between Nested divs for alignment Low
13 12 Images Aspect ratio Maintain consistent image aspect ratios aspect-video aspect-square No aspect ratio on containers aspect-video rounded-lg No aspect control Medium https://tailwindcss.com/docs/aspect-ratio
14 13 Images Object fit Control image scaling within containers object-cover object-contain Stretched distorted images object-cover w-full h-full No object-fit Medium https://tailwindcss.com/docs/object-fit
15 14 Images Lazy loading Defer loading of off-screen images loading='lazy' on images All images eager load <img loading='lazy'> <img> without lazy High
16 15 Images Responsive images Serve appropriate image sizes srcset and sizes attributes Same large image all devices srcset with multiple sizes 4000px image everywhere High
17 16 Typography Prose plugin Use @tailwindcss/typography for rich text prose prose-lg for article content Custom styles for markdown prose prose-lg max-w-none Custom text styling Medium https://tailwindcss.com/docs/typography-plugin
18 17 Typography Line height Use appropriate line height for readability leading-relaxed for body text Default tight line height leading-relaxed (1.625) leading-none or leading-tight Medium https://tailwindcss.com/docs/line-height
19 18 Typography Font size scale Use consistent text size scale text-sm text-base text-lg text-xl Arbitrary font sizes text-lg text-[17px] Low https://tailwindcss.com/docs/font-size
20 19 Typography Text truncation Handle long text gracefully truncate or line-clamp-* Overflow breaking layout line-clamp-2 No overflow handling Medium https://tailwindcss.com/docs/text-overflow
21 20 Colors Opacity utilities Use color opacity utilities bg-black/50 text-white/80 Separate opacity class bg-black/50 bg-black opacity-50 Low https://tailwindcss.com/docs/background-color
22 21 Colors Dark mode Support dark mode with dark: prefix dark:bg-gray-900 dark:text-white No dark mode support dark:bg-gray-900 Only light theme Medium https://tailwindcss.com/docs/dark-mode
23 22 Colors Semantic colors Use semantic color naming in config primary secondary danger success Generic color names in components bg-primary bg-blue-500 everywhere Medium
24 23 Spacing Consistent spacing scale Use Tailwind spacing scale consistently p-4 m-6 gap-8 Arbitrary pixel values p-4 (1rem) p-[15px] Low https://tailwindcss.com/docs/customizing-spacing
25 24 Spacing Negative margins Use sparingly for overlapping effects -mt-4 for overlapping elements Negative margins for layout fixing -mt-8 for card overlap -m-2 to fix spacing issues Medium
26 25 Spacing Space between Use space-y-* for vertical lists space-y-4 on flex/grid column Margin on each child space-y-4 Each child has mb-4 Low https://tailwindcss.com/docs/space
27 26 Forms Focus states Always show focus indicators focus:ring-2 focus:ring-blue-500 Remove focus outline focus:ring-2 focus:ring-offset-2 focus:outline-none (no replacement) High
28 27 Forms Input sizing Consistent input dimensions h-10 px-3 for inputs Inconsistent input heights h-10 w-full px-3 Various heights per input Medium
29 28 Forms Disabled states Clear disabled styling disabled:opacity-50 disabled:cursor-not-allowed No disabled indication disabled:opacity-50 Same style as enabled Medium
30 29 Forms Placeholder styling Style placeholder text appropriately placeholder:text-gray-400 Dark placeholder text placeholder:text-gray-400 Default dark placeholder Low
31 30 Responsive Mobile-first approach Start with mobile styles and add breakpoints Default mobile + md: lg: xl: Desktop-first approach text-sm md:text-base text-base max-md:text-sm Medium https://tailwindcss.com/docs/responsive-design
32 31 Responsive Breakpoint testing Test at standard breakpoints 320 375 768 1024 1280 1536 Only test on development device Test all breakpoints Single device testing High
33 32 Responsive Hidden/shown utilities Control visibility per breakpoint hidden md:block Different content per breakpoint hidden md:flex Separate mobile/desktop components Low https://tailwindcss.com/docs/display
34 33 Buttons Button sizing Consistent button dimensions px-4 py-2 or px-6 py-3 Inconsistent button sizes px-4 py-2 text-sm Various padding per button Medium
35 34 Buttons Touch targets Minimum 44px touch target on mobile min-h-[44px] on mobile Small buttons on mobile min-h-[44px] min-w-[44px] h-8 w-8 on mobile High
36 35 Buttons Loading states Show loading feedback disabled + spinner icon Clickable during loading <Button disabled><Spinner/></Button> Button without loading state High
37 36 Buttons Icon buttons Accessible icon-only buttons aria-label on icon buttons Icon button without label <button aria-label='Close'><XIcon/></button> <button><XIcon/></button> High
38 37 Cards Card structure Consistent card styling rounded-lg shadow-md p-6 Inconsistent card styles rounded-2xl shadow-lg p-6 Mixed card styling Low
39 38 Cards Card hover states Interactive cards should have hover feedback hover:shadow-lg transition-shadow No hover on clickable cards hover:shadow-xl transition-shadow Static cards that are clickable Medium
40 39 Cards Card spacing Consistent internal card spacing space-y-4 for card content Inconsistent internal spacing space-y-4 or p-6 Mixed mb-2 mb-4 mb-6 Low
41 40 Accessibility Screen reader text Provide context for screen readers sr-only for hidden labels Missing context for icons <span class='sr-only'>Close menu</span> No label for icon button High https://tailwindcss.com/docs/screen-readers
42 41 Accessibility Focus visible Show focus only for keyboard users focus-visible:ring-2 Focus on all interactions focus-visible:ring-2 focus:ring-2 (shows on click too) Medium
43 42 Accessibility Reduced motion Respect user motion preferences motion-reduce:animate-none Ignore motion preferences motion-reduce:transition-none No reduced motion support High https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion
44 43 Performance Configure content paths Tailwind needs to know where classes are used Use 'content' array in config Use deprecated 'purge' option (v2) content: ['./src/**/*.{js,ts,jsx,tsx}'] purge: [...] High https://tailwindcss.com/docs/content-configuration
45 44 Performance JIT mode Use JIT for faster builds and smaller bundles JIT enabled (default in v3) Full CSS in development Tailwind v3 defaults Tailwind v2 without JIT Medium
46 45 Performance Avoid @apply bloat Use @apply sparingly Direct utilities in HTML Heavy @apply usage class='px-4 py-2 rounded' @apply px-4 py-2 rounded; Low https://tailwindcss.com/docs/reusing-styles
47 46 Plugins Official plugins Use official Tailwind plugins @tailwindcss/forms typography aspect-ratio Custom implementations @tailwindcss/forms Custom form reset CSS Medium https://tailwindcss.com/docs/plugins
48 47 Plugins Custom utilities Create utilities for repeated patterns Custom utility in config Repeated arbitrary values Custom shadow utility shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere Medium
49 48 Layout Container Queries Use @container for component-based responsiveness Use @container and @lg: etc. Media queries for component internals @container @lg:grid-cols-2 @media (min-width: ...) inside component Medium https://github.com/tailwindlabs/tailwindcss-container-queries
50 49 Interactivity Group and Peer Style based on parent/sibling state group-hover peer-checked JS for simple state interactions group-hover:text-blue-500 onMouseEnter={() => setHover(true)} Low https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
51 50 Customization Arbitrary Values Use [] for one-off values w-[350px] for specific needs Creating config for single use top-[117px] (if strictly needed) style={{ top: '117px' }} Low https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values
52 51 Colors Theme color variables Define colors in Tailwind theme and use directly bg-primary text-success border-cta bg-[var(--color-primary)] text-[var(--color-success)] bg-primary bg-[var(--color-primary)] Medium https://tailwindcss.com/docs/customizing-colors
53 52 Colors Use bg-linear-to-* for gradients Tailwind v4 uses bg-linear-to-* syntax for gradients bg-linear-to-r bg-linear-to-b bg-gradient-to-* (deprecated in v4) bg-linear-to-r from-blue-500 to-purple-500 bg-gradient-to-r from-blue-500 to-purple-500 Medium https://tailwindcss.com/docs/background-image
54 53 Layout Use shrink-0 shorthand Shorter class name for flex-shrink-0 shrink-0 shrink flex-shrink-0 flex-shrink shrink-0 flex-shrink-0 Low https://tailwindcss.com/docs/flex-shrink
55 54 Layout Use size-* for square dimensions Single utility for equal width and height size-4 size-8 size-12 Separate h-* w-* for squares size-6 h-6 w-6 Low https://tailwindcss.com/docs/size
56 55 Images SVG explicit dimensions Add width/height attributes to SVGs to prevent layout shift before CSS loads <svg class='size-6' width='24' height='24'> SVG without explicit dimensions <svg class='size-6' width='24' height='24'> <svg class='size-6'> High

View file

@ -1,76 +0,0 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Application,Start UI from Application subclass,JavaFX apps should bootstrap the primary Stage through Application.start(),Extend Application and configure Scene in start(),Create UI from a random main method without launching JavaFX,"public class App extends Application { public void start(Stage stage) { stage.setScene(new Scene(root)); stage.show(); } }",new Stage().show(),High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/application/Application.html
2,Threading,Keep work off the FX Application Thread,Long-running work blocks rendering and input when executed on the UI thread,Use Task or Service for background work,Run network database or file work in button handlers,"Task<List<Item>> task = new Task<>() { protected List<Item> call() { return repo.load(); } }; new Thread(task).start();",loadLargeFile(); table.setItems(items);,High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/concurrent/Task.html
3,Threading,Update UI only on FX thread,Scene graph changes must happen on the JavaFX Application Thread,Use bindings task handlers or Platform.runLater for UI changes,Mutate controls directly from background threads,"task.setOnSucceeded(e -> table.setItems(FXCollections.observableArrayList(task.getValue())));",new Thread(() -> label.setText("Done")).start(),High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/application/Platform.html
4,Threading,Bind progress to background tasks,Task exposes progress and message properties for responsive feedback,Bind ProgressBar and Label to task properties,Poll progress manually or leave users without feedback,"progress.progressProperty().bind(task.progressProperty()); status.textProperty().bind(task.messageProperty());",while(running) progress.setProgress(x);,Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/concurrent/Task.html
5,FXML,Use FXML for stable declarative layouts,FXML keeps view structure readable for screens with many controls,Place layout in FXML and behavior in controller,Build large screens entirely in one Java method,"<VBox spacing=""12"" xmlns:fx=""http://javafx.com/fxml"" fx:controller=""app.MainController"">",VBox root = new VBox(); root.getChildren().add(... 200 lines ...);,Medium,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXMLLoader.html
6,FXML,Keep controllers focused on view behavior,Controllers should coordinate controls and delegate business logic to services,Inject services or call application services from controller,Put database queries and domain rules directly in controller,"public void save() { customerService.save(form.toCommand()); }",public void save() { DriverManager.getConnection(...); },High,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXML.html
7,FXML,Use fx:id for injected controls,FXML controls need stable fx:id values that match controller fields,Annotate fields with @FXML and keep ids descriptive,Look up controls by CSS selector for normal wiring,"@FXML private TableView<Customer> customerTable;",root.lookup("#customerTable"),Medium,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXML.html
8,FXML,Fail fast when loading FXML,FXML load errors should surface during screen creation with clear context,Load resources with getResource and handle IOException explicitly,Swallow loader errors and show a blank scene,"URL view = getClass().getResource(\"/views/main.fxml\"); Parent root = FXMLLoader.load(view);",try { FXMLLoader.load(url); } catch(Exception ignored) {},High,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXMLLoader.html
9,CSS,Style with style classes,JavaFX CSS works best through reusable styleClass names,Add semantic style classes and define them in CSS,Set long inline style strings throughout code,"button.getStyleClass().add(\"primary-action\");",".setStyle(\"-fx-background-color: #2563eb; -fx-padding: 12; ...\")",Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html
10,CSS,Use design tokens through looked-up colors,Looked-up colors keep palettes consistent across controls,Define named colors on root and reuse them in CSS,Repeat hex values in every selector,".root { -brand-primary: #2563eb; } .button.primary { -fx-background-color: -brand-primary; }",".save { -fx-background-color: #2563eb; } .link { -fx-text-fill: #2563eb; }",Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html
11,CSS,Avoid overusing inline effects,Expensive CSS effects and shadows can hurt desktop UI responsiveness,Use subtle shadows only on important elevated surfaces,Apply blur drop shadow and glow to every node,".dialog-card { -fx-effect: dropshadow(gaussian, rgba(0,0,0,.18), 16, 0, 0, 4); }",".table-row-cell { -fx-effect: dropshadow(gaussian, black, 20, .5, 0, 0); }",Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/effect/package-summary.html
12,Layout,Choose layout panes by responsibility,Each pane solves a different layout problem and should be selected intentionally,Use BorderPane for app shell GridPane for forms VBox/HBox for simple stacks,Use absolute positioning for resizable app screens,"BorderPane shell = new BorderPane(); shell.setTop(toolbar); shell.setCenter(content);",Pane root = new Pane(); button.setLayoutX(742);,High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/package-summary.html
13,Layout,Prefer constraints over fixed coordinates,Responsive JavaFX layouts depend on constraints and grow priorities,Use hgrow vgrow column constraints and alignment,Hard-code pixel positions and sizes,"GridPane.setHgrow(nameField, Priority.ALWAYS); column.setPercentWidth(50);",field.setPrefWidth(328); field.setLayoutX(120);,High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/GridPane.html
14,Layout,Set sensible min pref and max sizes,Controls should resize predictably across windows and DPI settings,Use Region.USE_COMPUTED_SIZE and max widths intentionally,Lock every control to fixed width and height,"button.setMaxWidth(Double.MAX_VALUE); VBox.setVgrow(table, Priority.ALWAYS);","button.setMinSize(96, 32); button.setMaxSize(96, 32);",Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/Region.html
15,Layout,Use spacing and padding consistently,Desktop UI needs scan-friendly rhythm and clear grouping,Set spacing padding and Insets through shared constants or CSS,Use inconsistent ad hoc gaps between controls,"form.setHgap(12); form.setVgap(10); form.setPadding(new Insets(16));",box.setSpacing(3); other.setSpacing(17);,Low,https://openjfx.io/javadoc/21/javafx.graphics/javafx/geometry/Insets.html
16,Controls,Use ObservableList for list controls,TableView ListView and ComboBox update automatically from observable collections,Back controls with FXCollections.observableArrayList(),Mutate plain lists and manually refresh controls,"ObservableList<Customer> rows = FXCollections.observableArrayList(); table.setItems(rows);",List<Customer> rows = new ArrayList<>(); table.setItems((ObservableList) rows);,High,https://openjfx.io/javadoc/21/javafx.base/javafx/collections/ObservableList.html
17,Controls,Configure TableView cell value factories with properties,Table columns should observe stable JavaFX properties for updates,Expose StringProperty ObjectProperty or use ReadOnlyObjectWrapper,Return transient strings without observable support,"nameCol.setCellValueFactory(data -> data.getValue().nameProperty());",nameCol.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().toString()));,Medium,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TableColumn.html
18,Controls,Use cell factories for custom rendering,Custom table or list visuals belong in reusable cell factories,Override updateItem and handle empty state,Place complex Nodes directly in model objects,"col.setCellFactory(c -> new TableCell<>() { protected void updateItem(Status s, boolean empty) { super.updateItem(s, empty); setText(empty ? null : s.label()); } });",row.setBadge(new Label("Active"));,Medium,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Cell.html
19,Controls,Virtualized controls are for large data,TableView ListView TreeView virtualize cells and outperform manual node lists,Use TableView or ListView for hundreds of rows,Create hundreds of HBoxes inside a VBox,"ListView<Item> list = new ListView<>(items);",items.forEach(i -> vbox.getChildren().add(new ItemRow(i)));,High,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/ListView.html
20,Controls,Handle empty states explicitly,Empty tables and lists need visible guidance or next actions,Set placeholder nodes for empty data views,Leave blank white areas that look broken,"table.setPlaceholder(new Label(\"No customers match this filter\"));",table.setPlaceholder(null);,Low,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TableView.html
21,Binding,Use property binding for derived UI state,JavaFX binding reduces imperative synchronization bugs,Bind disabled visible text and progress properties to source state,Manually update every dependent control in each event handler,"saveButton.disableProperty().bind(form.validProperty().not());",if(!valid) saveButton.setDisable(true);,High,https://openjfx.io/javadoc/21/javafx.base/javafx/beans/binding/Bindings.html
22,Binding,Unbind before manual updates,Bound properties cannot be set directly without errors,Call unbind when switching from bound to manual state,Set a bound property directly,"label.textProperty().unbind(); label.setText(\"Ready\");",label.textProperty().bind(task.messageProperty()); label.setText(\"Ready\");,Medium,https://openjfx.io/javadoc/21/javafx.base/javafx/beans/property/Property.html
23,Binding,Use listeners sparingly,Bindings express simple relationships more clearly than listeners,Use listeners for side effects and bindings for values,Create listener chains for simple computed text,"totalLabel.textProperty().bind(Bindings.format(""Total: %d"", total));","count.addListener((o, a, b) -> totalLabel.setText(""Total: "" + b));",Low,https://openjfx.io/javadoc/21/javafx.base/javafx/beans/value/ObservableValue.html
24,Events,Use action handlers for commands,Buttons and menu items should route to named command methods,Use setOnAction or @FXML handler methods with clear names,Put large lambdas inline for complex operations,"@FXML private void handleSave(ActionEvent event) { saveCustomer(); }",saveButton.setOnAction(e -> { validate(); transform(); query(); save(); refresh(); });,Medium,https://openjfx.io/javadoc/21/javafx.base/javafx/event/ActionEvent.html
25,Events,Use event filters for global shortcuts,Filters can intercept keyboard events before child controls consume them,Register accelerators or filters at Scene level,Add duplicate key handlers to every control,"scene.getAccelerators().put(new KeyCodeCombination(KeyCode.S, SHORTCUT_DOWN), this::save);",nameField.setOnKeyPressed(...); table.setOnKeyPressed(...);,Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/Scene.html
26,Accessibility,Connect labels to inputs,Accessible desktop forms need labels associated with controls,Use Label.setLabelFor and clear prompt text,Use placeholder-only labels,"nameLabel.setLabelFor(nameField); nameField.setPromptText(\"Jane Doe\");",nameField.setPromptText(\"Name\");,High,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Label.html
27,Accessibility,Expose accessible text for icon buttons,Icon-only controls need names for screen readers and tooltips,Set accessibleText and Tooltip on icon buttons,Use unlabeled graphic-only buttons,"button.setAccessibleText(""Refresh""); button.setTooltip(new Tooltip(""Refresh""));","new Button("""", refreshIcon)",High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/AccessibleRole.html
28,Accessibility,Keep keyboard focus visible,Desktop users rely on focus traversal and visible focus indicators,Preserve focus rings and tab order,Remove outlines without alternative focus state,".button:focused { -fx-border-color: -brand-focus; -fx-border-width: 2; }",".button:focused { -fx-background-insets: 0; }",High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/Node.html
29,Accessibility,Use mnemonics for menu and form workflows,Mnemonics make desktop workflows faster and more accessible,Enable mnemonicParsing and choose unique mnemonic letters,Ignore keyboard alternatives for frequent actions,"saveButton.setMnemonicParsing(true); saveButton.setText(""_Save"");","saveButton.setText(""Save"");",Low,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Labeled.html
30,Validation,Show validation near the field,Users should not hunt for form errors in desktop dialogs,Bind error labels or pseudo classes next to invalid controls,Show only a generic alert after submit,"field.pseudoClassStateChanged(PseudoClass.getPseudoClass(""invalid""), !valid);","new Alert(ERROR, ""Invalid input"").show();",Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/css/PseudoClass.html
31,Validation,Use TextFormatter for constrained input,TextFormatter prevents invalid edits before they enter the model,Attach TextFormatter for numeric dates and masks,Parse and reject invalid text only after submit,"amountField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, c -> c.getControlNewText().matches(""\\d*"") ? c : null));",Integer.parseInt(amountField.getText());,Medium,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TextFormatter.html
32,Dialogs,Use modal ownership for dialogs,Dialogs should block only the relevant window and return structured results,Set owner modality and use showAndWait,Open unmanaged windows for confirmations,"dialog.initOwner(stage); dialog.initModality(Modality.WINDOW_MODAL); Optional<ButtonType> result = dialog.showAndWait();",new Stage().show();,Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/stage/Modality.html
33,Dialogs,Prefer custom DialogPane over ad hoc stages,Dialog gives consistent buttons focus and result handling,Use Dialog<T> for forms confirmations and wizards,Build every modal as a new Stage manually,"Dialog<Customer> dialog = new Dialog<>(); dialog.getDialogPane().getButtonTypes().addAll(OK, CANCEL);",Stage modal = new Stage(); modal.setScene(new Scene(new VBox()));,Low,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Dialog.html
34,Images,Load images as resources,Packaged apps need resources resolved from the classpath or module path,Use getResourceAsStream for bundled assets,Use absolute local file paths in production UI,"new Image(getClass().getResourceAsStream(""/images/logo.png""));","new Image(""file:/Users/me/Desktop/logo.png"")",High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/image/Image.html
35,Images,Use background loading for large images,Large image decoding can pause UI startup,Use Image(url true) or a background Task for heavy assets,Load many full-size images synchronously during startup,"Image preview = new Image(url, true);",gallery.add(new Image(url));,Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/image/Image.html
36,Animation,Keep animations purposeful and short,Desktop UI animations should clarify state changes without delaying work,Use 150-250ms transitions for reveal hover and selection,Animate every layout change with long timelines,"FadeTransition ft = new FadeTransition(Duration.millis(180), pane); ft.setToValue(1);","new Timeline(new KeyFrame(Duration.seconds(2), ...)).play();",Low,https://openjfx.io/javadoc/21/javafx.graphics/javafx/animation/package-summary.html
37,Animation,Respect reduced-motion contexts where possible,Some users experience motion sensitivity in desktop apps,Provide a setting to disable decorative animations,Make animation required for comprehension,"if (settings.reducedMotion()) pane.setOpacity(1); else fade.play();",alwaysSpin.play();,Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/animation/Animation.html
38,Performance,Avoid recreating scenes for small state changes,Replacing whole scenes loses state and can flicker,Swap center content or update view models,Rebuild the entire Stage for every navigation click,"shell.setCenter(customerView);",stage.setScene(new Scene(loadMainAgain()));,Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/Scene.html
39,Performance,Reuse loaded views when appropriate,FXML loading and CSS application are not free,Cache stable views or controllers for frequent navigation,Reload heavyweight screens repeatedly without need,"Node settings = viewCache.computeIfAbsent(""settings"", this::loadSettings);","button.setOnAction(e -> shell.setCenter(loadFxml(""settings.fxml"")));",Low,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXMLLoader.html
40,Performance,Batch observable list changes,Many single-item updates can cause repeated layout and sort work,Use setAll or addAll for bulk replacement,Loop add items one by one to visible lists,"items.setAll(repository.findAll());",for(Item item : loaded) items.add(item);,Medium,https://openjfx.io/javadoc/21/javafx.base/javafx/collections/ObservableList.html
41,Architecture,Use view models for complex screens,View models keep controller state testable and separate from controls,Expose JavaFX properties from a screen model,Store all state only inside controls,"customerNameField.textProperty().bindBidirectional(viewModel.nameProperty());",String name = customerNameField.getText(); // everywhere,Medium,https://openjfx.io/javadoc/21/javafx.base/javafx/beans/property/package-summary.html
42,Architecture,Separate navigation from feature controllers,Feature controllers should not know how every screen is launched,Use a navigator or application shell service,Call FXMLLoader for unrelated screens from each controller,"navigator.showCustomers();",FXMLLoader.load(getClass().getResource(\"/views/admin.fxml\"));,Medium,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXMLLoader.html
43,Modules,Declare required JavaFX modules,Modular JavaFX apps must require the modules they use,Add javafx.controls javafx.fxml and opens controller packages,Depend on classpath accidents only,"module app { requires javafx.controls; requires javafx.fxml; opens app.ui to javafx.fxml; }",module app { requires javafx.controls; },High,https://openjfx.io/openjfx-docs/#modular
44,Packaging,Use jlink or jpackage for desktop delivery,JavaFX apps should ship with the runtime they need,Package a runtime image or native installer,Ask end users to install matching Java and JavaFX manually,"jpackage --name MyApp --module app/app.Main --runtime-image build/image",java -jar app.jar,Medium,https://openjfx.io/openjfx-docs/#modular
45,Testing,Use TestFX for interaction tests,UI flows need automated coverage beyond controller unit tests,Write TestFX tests for key forms dialogs and navigation,Only manually click through releases,"clickOn(""#nameField"").write(""Alice""); clickOn(""Save""); verifyThat(""Saved"", isVisible());",// manual QA only,Medium,https://github.com/TestFX/TestFX
46,Theme,Use AtlantaFX as the enterprise theme baseline,AtlantaFX provides modern JavaFX themes while preserving standard controls,Use AtlantaFX user-agent stylesheet plus a small app CSS layer,Rewrite every standard control style from scratch,"Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet());",scene.getStylesheets().add("/css/huge-custom-theme.css");,High,https://mkpaz.github.io/atlantafx/
47,Theme,Prefer Primer for enterprise applications,PrimerLight and PrimerDark are neutral enough for dense business workflows,Use PrimerLight as default and PrimerDark for dark mode,Use Dracula or Cupertino as the default enterprise theme,"Application.setUserAgentStylesheet(new PrimerLight().getUserAgentStylesheet());",Application.setUserAgentStylesheet(new Dracula().getUserAgentStylesheet());,Medium,https://mkpaz.github.io/atlantafx/themes/
48,Theme,Layer brand CSS after AtlantaFX,Application CSS should customize brand tokens and business states after the base theme,Add app.css to the Scene after setting AtlantaFX,Edit AtlantaFX source CSS directly,"scene.getStylesheets().add(getClass().getResource(""/css/app.css"").toExternalForm());",modify atlantafx-base CSS files,High,https://mkpaz.github.io/atlantafx/theming/
49,Theme,Use looked-up colors as enterprise tokens,JavaFX looked-up colors keep brand and semantic colors reusable across controls,Define app-primary app-success app-warning app-danger on root,Repeat hex values in every selector,".root { -app-primary: #2563eb; -app-danger: #dc2626; }",".save { -fx-background-color: #2563eb; } .link { -fx-text-fill: #2563eb; }",High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html
50,Theme,Keep theme switching centralized,Dark mode switching should not be scattered across controllers,Use a ThemeService that sets user-agent stylesheet and app CSS variants,Let each controller decide its own theme,themeService.apply(ThemeMode.DARK);,if(dark) button.setStyle(...);,Medium,https://mkpaz.github.io/atlantafx/
51,Theme,Validate contrast for business status colors,Enterprise screens use status colors heavily and need readable contrast,Check text on success warning danger and selected row backgrounds,Assume brand colors are accessible,".status-danger { -fx-text-fill: -app-danger; }",red text on dark red background,High,https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
52,Theme,Use AtlantaFX style classes before custom CSS,AtlantaFX exposes utility styles that reduce custom CSS drift,Prefer Styles constants or documented style classes,Create one-off class names for every button variant,saveButton.getStyleClass().add(Styles.ACCENT);,"saveButton.getStyleClass().add(""blue-button-42"");",Medium,https://mkpaz.github.io/atlantafx/
53,Theme,Treat AtlantaFX as a base not the whole design system,AtlantaFX modernizes controls but enterprise UX still needs layout density and workflow rules,Define app shell navigation table density form and validation conventions,Assume theme choice alone solves enterprise usability,"root.getStyleClass().add(""enterprise-shell"");",only set PrimerLight and stop,High,https://mkpaz.github.io/atlantafx/
54,Icons,Use Ikonli for consistent enterprise icons,Icon fonts integrate cleanly with JavaFX controls and avoid emoji-style UI,Use FontIcon with semantic style classes,Use emoji as toolbar or menu icons,"Button refresh = new Button(""Refresh"", new FontIcon(""mdi2r-refresh""));",new Button("Refresh"),Medium,https://kordamp.org/ikonli/
55,Components,Use AtlantaFX controls for common app affordances,AtlantaFX provides useful controls such as Card Message ModalPane Popover and ToggleSwitch,Use built-in AtlantaFX controls before adding another dependency,Add ControlsFX for components AtlantaFX already covers,"Message message = new Message(""Saved"", ""Customer updated successfully"");","new Label(""Saved"") with ad hoc styling",Medium,https://mkpaz.github.io/atlantafx/
56,Components,Add ControlsFX only for missing enterprise controls,ControlsFX is useful for specialized controls but should stay optional,Use ControlsFX for SpreadsheetView PropertySheet CheckComboBox or StatusBar needs,Add ControlsFX by default before requirements are clear,PropertySheet sheet = new PropertySheet(items);,"implementation ""org.controlsfx:controlsfx"" with no usage",Low,https://controlsfx.github.io/
57,Testing,Test theme-critical flows with TestFX,Theme and CSS changes can break focus visibility dialogs and button affordance,Use TestFX for login save validation and modal workflows,Only inspect AtlantaFX screens manually,"clickOn(""#saveButton""); verifyThat("".message"", isVisible());",manual theme QA only,Medium,https://github.com/TestFX/TestFX
58,Architecture,Use application shell plus feature workspaces,Enterprise JavaFX apps need stable navigation around changing work areas,Use BorderPane shell with navigation toolbar and central workspace,Replace the whole Stage for every feature,"shell.setLeft(navigation); shell.setTop(toolbar); shell.setCenter(workspace);",stage.setScene(new Scene(loadFeature()));,High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/BorderPane.html
59,Architecture,Use MVVM for complex enterprise screens,Large forms and tables need testable state outside the controller,Expose JavaFX properties from view models and bind controls to them,Put all screen state and validation in the controller,amountField.textProperty().bindBidirectional(vm.amountProperty());,controller.amount = amountField.getText();,High,https://openjfx.io/javadoc/21/javafx.base/javafx/beans/property/package-summary.html
60,Architecture,Inject services into controllers,Enterprise controllers should coordinate UI and call application services,Use a controller factory or DI container for services,Create database connections inside FXML controllers,loader.setControllerFactory(type -> injector.getInstance(type));,new CustomerRepository(new DriverManager(...)),High,https://openjfx.io/javadoc/21/javafx.fxml/javafx/fxml/FXMLLoader.html
61,Navigation,Use role-aware navigation models,Menus toolbars and shortcuts should reflect the same permission model,Build navigation items from commands with required roles,Hide buttons in one place and leave shortcuts enabled,"command.enabledProperty().bind(permissionService.allowed(""invoice.approve""));",approveButton.setVisible(false);,High,
62,Workflow,Represent workflow states visibly,Approval and processing screens need clear business state signals,Use semantic badges row styles and disabled actions by workflow state,Use only free text status columns,"row.pseudoClassStateChanged(PseudoClass.getPseudoClass(""blocked""), item.isBlocked());","statusCol.setText(""B"");",Medium,https://openjfx.io/javadoc/21/javafx.graphics/javafx/css/PseudoClass.html
63,TableView,Design TableView for high-density enterprise data,Enterprise users scan compare sort filter and act on rows for long periods,Use compact row height clear columns sorting filtering and selection summary,Use card grids for large tabular datasets,"table.getStyleClass().add(""dense-table""); table.getSortOrder().setAll(updatedAtCol);",new TilePane(customerCards),High,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TableView.html
64,TableView,Keep row actions predictable,Inline actions in dense tables should be limited and permission-aware,Use context menus or a side detail panel for secondary actions,Place many buttons in every row,"table.setRowFactory(tv -> { TableRow<Order> row = new TableRow<>(); row.setContextMenu(orderMenu); return row; });",row contains Edit Delete Approve Print Email buttons,Medium,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/ContextMenu.html
65,TableView,Use server-side paging for large enterprise datasets,Desktop clients should not load entire enterprise tables into memory,Fetch pages or filtered slices from services,Load all records and filter in the UI,"Page<Customer> page = customerService.search(criteria, pageRequest);",customerRepository.findAll(),High,
66,Forms,Use form sections for enterprise data entry,Long enterprise forms need grouping and progressive disclosure,Group fields into titled sections with validation summaries,Place dozens of inputs in one unbroken GridPane,"TitledPane billing = new TitledPane(""Billing"", billingForm);",new GridPane with 80 controls,Medium,https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TitledPane.html
67,Forms,Provide validation summary plus field errors,Enterprise forms often need multiple corrections before submission,Show a summary at top and field-level messages near controls,Show only one modal alert after Save,"summary.setItems(vm.validationErrors()); field.pseudoClassStateChanged(INVALID, fieldError);","new Alert(ERROR, ""Invalid form"").showAndWait();",High,
68,Tasks,Make long operations cancellable,Enterprise imports exports sync and reports need cancel paths,Expose cancel button bound to Task running state,Force users to wait or kill the app,cancelButton.setOnAction(e -> task.cancel());,runReportButton.setDisable(true);,High,https://openjfx.io/javadoc/21/javafx.graphics/javafx/concurrent/Task.html
69,Tasks,Surface retryable errors without losing context,Network and service failures should preserve user input and next action,Show inline retry messages and keep form/table state,Clear the screen on service failure,"message.setDescription(""Could not save. Check connection and retry."");",loadErrorScene();,High,
70,Audit,Log business actions through services,Enterprise desktop apps need traceability for sensitive changes,Record user action entity result and timestamp in service layer,Log only UI button clicks,"audit.log(user, ""invoice.approve"", invoiceId, SUCCESS);","System.out.println(""clicked approve"");",Medium,
71,Configuration,Separate user preferences from application config,Enterprise apps need deploy-time config and per-user preferences,Use config files for endpoints and Preferences for UI choices,Hard-code environment URLs and window state,"Preferences.userNodeForPackage(App.class).put(""theme"", ""dark"");","private static final String API = ""http://localhost:8080"";",Medium,https://docs.oracle.com/en/java/javase/21/docs/api/java.prefs/java/util/prefs/Preferences.html
72,Deployment,Package resources and themes inside the runtime image,AtlantaFX app CSS icons and FXML must be available after jpackage,Load resources from classpath or module resources,Load theme files from developer machine paths,"getClass().getResource(""/css/app.css"").toExternalForm();","new File(""src/main/resources/css/app.css"").toURI()",High,https://openjfx.io/openjfx-docs/#modular
73,Deployment,Write logs to user-writable locations,Installed desktop apps may not write inside the application directory,Use platform-specific user data directories for logs and cache,Write logs beside the executable,"Path logs = appData.resolve(""logs/app.log"");",Path.of("app.log"),Medium,
74,Testing,Cover enterprise happy path and failure path,Enterprise UI tests should verify save validation permission and service failure flows,Use TestFX for core workflows and service fakes,Only test controller methods without UI interaction,"clickOn(""Save""); verifyThat(""Customer saved"", isVisible());",controller.save(); assertTrue(saved);,High,https://github.com/TestFX/TestFX
75,Dependencies,Keep optional UI libraries behind actual needs,AtlantaFX should be default but additional libraries should be justified,Start with JavaFX AtlantaFX Ikonli TestFX and add ControlsFX only for missing controls,Adopt many UI libraries at project start,"dependencies { implementation(""io.github.mkpaz:atlantafx-base:2.1.0"") }",implementation controlsfx gemsfx tilesfx materialfx all at once,Medium,
Can't render this file because it contains an unexpected character in line 4 and column 345.

Some files were not shown because too many files have changed in this diff Show more