@dsalazardev/ai-hub-cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +34 -0
- package/README.md +64 -2
- package/agents/cpp-build-resolver.md +25 -0
- package/agents/cpp-reviewer.md +28 -0
- package/agents/csharp-reviewer.md +28 -0
- package/agents/django-build-resolver.md +25 -0
- package/agents/django-reviewer.md +28 -0
- package/agents/explorer-domain.md +28 -0
- package/agents/fastapi-reviewer.md +28 -0
- package/agents/flutter-build-resolver.md +25 -0
- package/agents/flutter-reviewer.md +28 -0
- package/agents/go-build-resolver.md +25 -0
- package/agents/go-reviewer.md +28 -0
- package/agents/implementer-backend.md +30 -0
- package/agents/implementer-frontend.md +30 -0
- package/agents/java-build-resolver.md +26 -0
- package/agents/java-reviewer.md +29 -0
- package/agents/kotlin-build-resolver.md +25 -0
- package/agents/kotlin-reviewer.md +28 -0
- package/agents/mle-reviewer.md +28 -0
- package/agents/php-reviewer.md +29 -0
- package/agents/postgres-reviewer.md +28 -0
- package/agents/python-reviewer.md +28 -0
- package/agents/pytorch-build-resolver.md +25 -0
- package/agents/rag-reviewer.md +28 -0
- package/agents/react-build-resolver.md +26 -0
- package/agents/react-reviewer.md +29 -0
- package/agents/reviewer-architecture.md +32 -0
- package/agents/reviewer-tests.md +31 -0
- package/agents/rust-build-resolver.md +25 -0
- package/agents/rust-reviewer.md +28 -0
- package/agents/spec-author.md +36 -0
- package/agents/spec-miner.md +14 -0
- package/agents/swift-build-resolver.md +25 -0
- package/agents/swift-reviewer.md +28 -0
- package/agents/typescript-reviewer.md +28 -0
- package/agents/vue-reviewer.md +28 -0
- package/dist/backup/snapshots.d.ts +24 -0
- package/dist/backup/snapshots.js +171 -0
- package/dist/bin/ai-hub.d.ts +2 -0
- package/dist/bin/ai-hub.js +11 -0
- package/dist/cli/args.d.ts +4 -0
- package/dist/cli/args.js +63 -0
- package/dist/cli/context.d.ts +7 -0
- package/dist/cli/context.js +74 -0
- package/dist/cli/doctor.d.ts +14 -0
- package/dist/cli/doctor.js +200 -0
- package/dist/cli/drift.d.ts +2 -0
- package/dist/cli/drift.js +34 -0
- package/dist/cli/items.d.ts +5 -0
- package/dist/cli/items.js +10 -0
- package/dist/cli/legacy.d.ts +3 -0
- package/dist/cli/legacy.js +18 -0
- package/dist/cli/maintenance.d.ts +8 -0
- package/dist/cli/maintenance.js +262 -0
- package/dist/cli/memory.d.ts +2 -0
- package/dist/cli/memory.js +84 -0
- package/dist/cli/onboarding.d.ts +17 -0
- package/dist/cli/onboarding.js +234 -0
- package/dist/cli/run.d.ts +15 -0
- package/dist/cli/run.js +183 -0
- package/dist/cli/source.d.ts +5 -0
- package/dist/cli/source.js +22 -0
- package/dist/cli/trunk.d.ts +8 -0
- package/dist/cli/trunk.js +54 -0
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli/types.js +4 -0
- package/dist/cli/worktrees.d.ts +11 -0
- package/dist/cli/worktrees.js +141 -0
- package/dist/engine/engine.d.ts +48 -0
- package/dist/engine/engine.js +114 -0
- package/dist/engine/gates.d.ts +2 -0
- package/dist/engine/gates.js +93 -0
- package/dist/frontmatter.d.ts +11 -0
- package/dist/frontmatter.js +30 -0
- package/dist/harness/guards/adapters/context.d.ts +19 -0
- package/dist/harness/guards/adapters/context.js +153 -0
- package/dist/harness/guards/adapters/hermes.d.ts +13 -0
- package/dist/harness/guards/adapters/hermes.js +18 -0
- package/dist/harness/guards/adapters/hooks.d.ts +25 -0
- package/dist/harness/guards/adapters/hooks.js +52 -0
- package/dist/harness/guards/branch.d.ts +5 -0
- package/dist/harness/guards/branch.js +43 -0
- package/dist/harness/guards/danger.d.ts +13 -0
- package/dist/harness/guards/danger.js +157 -0
- package/dist/harness/guards/index.d.ts +6 -0
- package/dist/harness/guards/index.js +6 -0
- package/dist/harness/guards/preflight.d.ts +14 -0
- package/dist/harness/guards/preflight.js +37 -0
- package/dist/harness/guards/ship.d.ts +2 -0
- package/dist/harness/guards/ship.js +35 -0
- package/dist/harness/guards/trunk.d.ts +5 -0
- package/dist/harness/guards/trunk.js +41 -0
- package/dist/harness/guards/types.d.ts +50 -0
- package/dist/harness/guards/types.js +1 -0
- package/dist/harness/memory/ledger.d.ts +53 -0
- package/dist/harness/memory/ledger.js +136 -0
- package/dist/harness/memory/state.d.ts +35 -0
- package/dist/harness/memory/state.js +121 -0
- package/dist/harness/worktree/reaper.d.ts +32 -0
- package/dist/harness/worktree/reaper.js +54 -0
- package/dist/manifest/build.d.ts +1 -0
- package/dist/manifest/build.js +1 -0
- package/dist/manifest/cli.d.ts +2 -0
- package/dist/manifest/cli.js +44 -0
- package/dist/manifest/dispatch.d.ts +13 -0
- package/dist/manifest/dispatch.js +36 -0
- package/dist/manifest/labels.d.ts +15 -0
- package/dist/manifest/labels.js +70 -0
- package/dist/manifest/manifest.d.ts +113 -0
- package/dist/manifest/manifest.js +202 -0
- package/dist/package-info.d.ts +4 -0
- package/dist/package-info.js +7 -0
- package/dist/paths.d.ts +33 -0
- package/dist/paths.js +123 -0
- package/dist/presets/detect.d.ts +24 -0
- package/dist/presets/detect.js +116 -0
- package/dist/presets/materialize.d.ts +18 -0
- package/dist/presets/materialize.js +101 -0
- package/dist/presets/resolve.d.ts +37 -0
- package/dist/presets/resolve.js +83 -0
- package/dist/presets/schema.d.ts +57 -0
- package/dist/presets/schema.js +68 -0
- package/dist/source-checkout.d.ts +8 -0
- package/dist/source-checkout.js +19 -0
- package/dist/sync/config.d.ts +47 -0
- package/dist/sync/config.js +85 -0
- package/dist/sync/lock.d.ts +16 -0
- package/dist/sync/lock.js +43 -0
- package/dist/sync/render.d.ts +18 -0
- package/dist/sync/render.js +255 -0
- package/dist/sync/sync.d.ts +38 -0
- package/dist/sync/sync.js +288 -0
- package/dist/sync/targets.d.ts +24 -0
- package/dist/sync/targets.js +78 -0
- package/engine/coupling.json +4 -0
- package/engine/engine.yaml +250 -0
- package/engine/legacy-paths.json +4 -0
- package/engine/preflight.md +11 -0
- package/manifest.json +393 -0
- package/package.json +61 -4
- package/presets/angular-express/harness/rules/01-zero-any.sh +25 -0
- package/presets/angular-express/harness/rules/02-no-ng-module.sh +21 -0
- package/presets/angular-express/harness/rules/03-no-constructor-injection.sh +21 -0
- package/presets/angular-express/harness/rules/04-no-legacy-directives.sh +21 -0
- package/presets/angular-express/harness/rules/05-no-dolibarrdal-inheritance.sh +25 -0
- package/presets/angular-express/harness/rules/06-no-todos.sh +22 -0
- package/presets/angular-express/harness/run-all.sh +13 -0
- package/presets/angular-express/preset.yaml +55 -0
- package/presets/angular-express/references/common-imports-catalog.md +146 -0
- package/presets/angular-express/references/fichajes-migration-notes.md +57 -0
- package/presets/angular-express/references/module-implementation-flow.md +260 -0
- package/presets/angular-express/references/products-coding-standard.md +143 -0
- package/presets/angular-express/references/products-module-blueprint.md +860 -0
- package/presets/angular-express/references/replicate-module-checklist.md +72 -0
- package/presets/angular-express/references/users-migration-notes.md +172 -0
- package/presets/angular-express/rules/architecture.md +8 -0
- package/presets/angular-express/rules/backend.md +4 -0
- package/presets/angular-express/rules/frontend.md +53 -0
- package/presets/angular-express/skills/angular-developer/SKILL.md +130 -0
- package/presets/angular-express/skills/angular-developer/references/angular-animations.md +160 -0
- package/presets/angular-express/skills/angular-developer/references/angular-aria.md +410 -0
- package/presets/angular-express/skills/angular-developer/references/cli.md +86 -0
- package/presets/angular-express/skills/angular-developer/references/component-harnesses.md +59 -0
- package/presets/angular-express/skills/angular-developer/references/component-styling.md +91 -0
- package/presets/angular-express/skills/angular-developer/references/components.md +117 -0
- package/presets/angular-express/skills/angular-developer/references/creating-services.md +97 -0
- package/presets/angular-express/skills/angular-developer/references/data-resolvers.md +69 -0
- package/presets/angular-express/skills/angular-developer/references/define-routes.md +67 -0
- package/presets/angular-express/skills/angular-developer/references/defining-providers.md +72 -0
- package/presets/angular-express/skills/angular-developer/references/di-fundamentals.md +120 -0
- package/presets/angular-express/skills/angular-developer/references/e2e-testing.md +66 -0
- package/presets/angular-express/skills/angular-developer/references/effects.md +83 -0
- package/presets/angular-express/skills/angular-developer/references/hierarchical-injectors.md +43 -0
- package/presets/angular-express/skills/angular-developer/references/host-elements.md +80 -0
- package/presets/angular-express/skills/angular-developer/references/injection-context.md +63 -0
- package/presets/angular-express/skills/angular-developer/references/inputs.md +101 -0
- package/presets/angular-express/skills/angular-developer/references/linked-signal.md +59 -0
- package/presets/angular-express/skills/angular-developer/references/loading-strategies.md +61 -0
- package/presets/angular-express/skills/angular-developer/references/mcp.md +106 -0
- package/presets/angular-express/skills/angular-developer/references/migrations.md +30 -0
- package/presets/angular-express/skills/angular-developer/references/navigate-to-routes.md +69 -0
- package/presets/angular-express/skills/angular-developer/references/outputs.md +86 -0
- package/presets/angular-express/skills/angular-developer/references/reactive-forms.md +122 -0
- package/presets/angular-express/skills/angular-developer/references/rendering-strategies.md +44 -0
- package/presets/angular-express/skills/angular-developer/references/resource.md +74 -0
- package/presets/angular-express/skills/angular-developer/references/route-animations.md +56 -0
- package/presets/angular-express/skills/angular-developer/references/route-guards.md +52 -0
- package/presets/angular-express/skills/angular-developer/references/router-lifecycle.md +45 -0
- package/presets/angular-express/skills/angular-developer/references/router-testing.md +87 -0
- package/presets/angular-express/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
- package/presets/angular-express/skills/angular-developer/references/signal-forms.md +897 -0
- package/presets/angular-express/skills/angular-developer/references/signals-overview.md +94 -0
- package/presets/angular-express/skills/angular-developer/references/tailwind-css.md +69 -0
- package/presets/angular-express/skills/angular-developer/references/template-driven-forms.md +114 -0
- package/presets/angular-express/skills/angular-developer/references/testing-fundamentals.md +66 -0
- package/presets/angular-express/skills/angular-new-app/SKILL.md +62 -0
- package/presets/angular-express/skills/frontend-design/LICENSE.txt +177 -0
- package/presets/angular-express/skills/frontend-design/SKILL.md +55 -0
- package/presets/angular-express/skills/onna-architecture-angular/SKILL.md +229 -0
- package/presets/angular-express/skills/onna-architecture-bff/SKILL.md +446 -0
- package/presets/angular-express/skills/onna-architecture-common/SKILL.md +313 -0
- package/presets/angular-express/skills/onna-design-system/SKILL.md +615 -0
- package/presets/base/preset.yaml +27 -0
- package/presets/base/rules/core.md +35 -0
- package/presets/generic-ts/preset.yaml +46 -0
- package/presets/python/preset.yaml +47 -0
- package/presets/python/rules/python.md +10 -0
- package/schema/engine.schema.json +73 -0
- package/schema/manifest.schema.json +140 -0
- package/schema/preset.schema.json +98 -0
- package/schema/state.schema.json +27 -0
- package/skills/shared/ab-testing/SKILL.md +159 -0
- package/skills/shared/ab-testing/evals/README.md +8 -0
- package/skills/shared/ab-testing/evals/cases.yaml +49 -0
- package/skills/shared/ab-testing/references/pitfalls.md +74 -0
- package/skills/shared/ab-testing/references/sample-size-and-cuped.md +128 -0
- package/skills/shared/ab-testing/scripts/verify.sh +89 -0
- package/skills/shared/accessibility/SKILL.md +216 -0
- package/skills/shared/accessibility/evals/README.md +3 -0
- package/skills/shared/accessibility/evals/cases.yaml +47 -0
- package/skills/shared/accessibility/references/aria-patterns.md +113 -0
- package/skills/shared/accessibility/references/wcag22-checklist.md +83 -0
- package/skills/shared/accessibility/scripts/verify.sh +103 -0
- package/skills/shared/agent-eval/SKILL.md +187 -0
- package/skills/shared/agent-eval/evals/README.md +12 -0
- package/skills/shared/agent-eval/evals/cases.yaml +45 -0
- package/skills/shared/agent-eval/references/judge-design.md +118 -0
- package/skills/shared/agent-eval/references/runner-and-gate.md +183 -0
- package/skills/shared/agent-eval/scripts/verify.sh +161 -0
- package/skills/shared/agent-safety/SKILL.md +169 -0
- package/skills/shared/agent-safety/evals/README.md +12 -0
- package/skills/shared/agent-safety/evals/cases.yaml +46 -0
- package/skills/shared/agent-safety/references/threat-model.md +51 -0
- package/skills/shared/analyze/SKILL.md +123 -0
- package/skills/shared/analyze/evals/README.md +72 -0
- package/skills/shared/analyze/evals/cases.yaml +74 -0
- package/skills/shared/angular/SKILL.md +275 -0
- package/skills/shared/angular/evals/README.md +3 -0
- package/skills/shared/angular/evals/cases.yaml +38 -0
- package/skills/shared/angular/references/migration.md +81 -0
- package/skills/shared/angular/references/signals-rxjs.md +92 -0
- package/skills/shared/angular/scripts/verify.sh +122 -0
- package/skills/shared/api-connector-builder/SKILL.md +268 -0
- package/skills/shared/api-connector-builder/evals/README.md +11 -0
- package/skills/shared/api-connector-builder/evals/cases.yaml +47 -0
- package/skills/shared/api-connector-builder/references/auth-flows.md +132 -0
- package/skills/shared/api-connector-builder/references/pagination.md +144 -0
- package/skills/shared/api-connector-builder/scripts/verify.sh +172 -0
- package/skills/shared/api-design/SKILL.md +182 -0
- package/skills/shared/api-design/evals/README.md +3 -0
- package/skills/shared/api-design/evals/cases.yaml +45 -0
- package/skills/shared/api-design/references/graphql-design.md +70 -0
- package/skills/shared/api-design/references/openapi-contract.md +86 -0
- package/skills/shared/api-design/references/rest-conventions.md +63 -0
- package/skills/shared/api-design/references/versioning-and-evolution.md +49 -0
- package/skills/shared/api-design/scripts/verify.sh +138 -0
- package/skills/shared/astro/SKILL.md +257 -0
- package/skills/shared/astro/evals/README.md +3 -0
- package/skills/shared/astro/evals/cases.yaml +41 -0
- package/skills/shared/astro/references/content-layer.md +118 -0
- package/skills/shared/astro/references/deploy-and-integrations.md +163 -0
- package/skills/shared/astro/scripts/verify.sh +137 -0
- package/skills/shared/author-skill/SKILL.md +194 -0
- package/skills/shared/author-skill/evals/README.md +66 -0
- package/skills/shared/author-skill/evals/cases.yaml +75 -0
- package/skills/shared/author-skill/references/description-recipe.md +85 -0
- package/skills/shared/author-skill/references/eval-authoring.md +108 -0
- package/skills/shared/author-skill/references/skill-conventions.md +95 -0
- package/skills/shared/automation-flows/SKILL.md +122 -0
- package/skills/shared/automation-flows/evals/README.md +5 -0
- package/skills/shared/automation-flows/evals/cases.yaml +44 -0
- package/skills/shared/automation-flows/references/error-handling.md +58 -0
- package/skills/shared/automation-flows/references/n8n-workflow-json.md +63 -0
- package/skills/shared/automation-flows/scripts/verify.sh +78 -0
- package/skills/shared/aws-essentials/SKILL.md +206 -0
- package/skills/shared/aws-essentials/evals/README.md +10 -0
- package/skills/shared/aws-essentials/evals/cases.yaml +44 -0
- package/skills/shared/aws-essentials/references/iam-least-privilege.md +134 -0
- package/skills/shared/aws-essentials/references/rds-cloudfront-recipes.md +127 -0
- package/skills/shared/aws-essentials/scripts/verify.sh +99 -0
- package/skills/shared/backups/SKILL.md +131 -0
- package/skills/shared/backups/evals/README.md +3 -0
- package/skills/shared/backups/evals/cases.yaml +42 -0
- package/skills/shared/backups/references/engine-recipes.md +121 -0
- package/skills/shared/backups/references/restore-runbook.md +65 -0
- package/skills/shared/backups/scripts/verify.sh +80 -0
- package/skills/shared/bash-scripting/SKILL.md +227 -0
- package/skills/shared/bash-scripting/evals/README.md +3 -0
- package/skills/shared/bash-scripting/evals/cases.yaml +45 -0
- package/skills/shared/bash-scripting/references/portability.md +97 -0
- package/skills/shared/bash-scripting/scripts/verify.sh +140 -0
- package/skills/shared/building-agents/SKILL.md +386 -0
- package/skills/shared/building-agents/evals/README.md +68 -0
- package/skills/shared/building-agents/evals/cases.yaml +60 -0
- package/skills/shared/building-agents/references/agent-loops-and-harness.md +371 -0
- package/skills/shared/building-agents/references/evals-and-observability.md +420 -0
- package/skills/shared/building-agents/references/mcp-servers.md +294 -0
- package/skills/shared/building-agents/references/provider-abstraction.md +489 -0
- package/skills/shared/building-agents/references/tools-and-rag.md +417 -0
- package/skills/shared/building-agents/scripts/verify.sh +121 -0
- package/skills/shared/chatbot/SKILL.md +151 -0
- package/skills/shared/chatbot/evals/README.md +13 -0
- package/skills/shared/chatbot/evals/cases.yaml +43 -0
- package/skills/shared/chatbot/references/handoff-and-sales.md +71 -0
- package/skills/shared/chatbot/references/system-prompt-and-guardrails.md +78 -0
- package/skills/shared/chatbot/scripts/verify.sh +162 -0
- package/skills/shared/chrome-extension/SKILL.md +169 -0
- package/skills/shared/chrome-extension/evals/README.md +12 -0
- package/skills/shared/chrome-extension/evals/cases.yaml +40 -0
- package/skills/shared/chrome-extension/references/store-and-migration.md +84 -0
- package/skills/shared/chrome-extension/scripts/verify.sh +62 -0
- package/skills/shared/clarify/SKILL.md +161 -0
- package/skills/shared/clarify/evals/README.md +70 -0
- package/skills/shared/clarify/evals/cases.yaml +71 -0
- package/skills/shared/clickhouse-analytics/SKILL.md +159 -0
- package/skills/shared/clickhouse-analytics/evals/README.md +3 -0
- package/skills/shared/clickhouse-analytics/evals/cases.yaml +45 -0
- package/skills/shared/clickhouse-analytics/references/ingestion-and-mvs.md +109 -0
- package/skills/shared/clickhouse-analytics/references/query-optimization.md +76 -0
- package/skills/shared/clickhouse-analytics/references/schema-and-engines.md +63 -0
- package/skills/shared/clickhouse-analytics/scripts/verify.sh +109 -0
- package/skills/shared/cloudflare/SKILL.md +186 -0
- package/skills/shared/cloudflare/evals/README.md +15 -0
- package/skills/shared/cloudflare/evals/cases.yaml +46 -0
- package/skills/shared/cloudflare/references/storage-primitives.md +104 -0
- package/skills/shared/cloudflare/references/wrangler-config.md +91 -0
- package/skills/shared/cloudflare/scripts/verify.sh +133 -0
- package/skills/shared/code-review/SKILL.md +132 -0
- package/skills/shared/code-review/evals/README.md +3 -0
- package/skills/shared/code-review/evals/cases.yaml +55 -0
- package/skills/shared/code-review/references/pr-workflow.md +67 -0
- package/skills/shared/codebase-onboarding/SKILL.md +128 -0
- package/skills/shared/codebase-onboarding/evals/README.md +3 -0
- package/skills/shared/codebase-onboarding/evals/cases.yaml +69 -0
- package/skills/shared/codebase-onboarding/references/recon-playbook.md +57 -0
- package/skills/shared/codebase-onboarding/scripts/verify.sh +54 -0
- package/skills/shared/compose-multiplatform/SKILL.md +198 -0
- package/skills/shared/compose-multiplatform/evals/README.md +3 -0
- package/skills/shared/compose-multiplatform/evals/cases.yaml +40 -0
- package/skills/shared/compose-multiplatform/references/ios-interop.md +91 -0
- package/skills/shared/compose-multiplatform/references/project-setup.md +96 -0
- package/skills/shared/compose-multiplatform/scripts/verify.sh +123 -0
- package/skills/shared/connect-tool/SKILL.md +163 -0
- package/skills/shared/connect-tool/evals/cases.yaml +58 -0
- package/skills/shared/constitution/SKILL.md +158 -0
- package/skills/shared/constitution/evals/README.md +68 -0
- package/skills/shared/constitution/evals/cases.yaml +72 -0
- package/skills/shared/constitution/references/constitution-template.md +100 -0
- package/skills/shared/context-budget/SKILL.md +114 -0
- package/skills/shared/context-budget/evals/README.md +11 -0
- package/skills/shared/context-budget/evals/cases.yaml +40 -0
- package/skills/shared/context-budget/references/handoff-and-compaction.md +96 -0
- package/skills/shared/continuous-learning/SKILL.md +106 -0
- package/skills/shared/continuous-learning/evals/README.md +16 -0
- package/skills/shared/continuous-learning/evals/cases.yaml +39 -0
- package/skills/shared/continuous-learning/references/lesson-routing.md +106 -0
- package/skills/shared/coolify/SKILL.md +194 -0
- package/skills/shared/coolify/evals/README.md +21 -0
- package/skills/shared/coolify/evals/cases.yaml +46 -0
- package/skills/shared/coolify/references/databases-and-backups.md +99 -0
- package/skills/shared/coolify/references/deploy-recipes.md +105 -0
- package/skills/shared/coolify/references/install-and-proxy.md +80 -0
- package/skills/shared/coolify/scripts/verify.sh +123 -0
- package/skills/shared/cost-tracking/SKILL.md +176 -0
- package/skills/shared/cost-tracking/evals/README.md +3 -0
- package/skills/shared/cost-tracking/evals/cases.yaml +45 -0
- package/skills/shared/cost-tracking/references/cloud-caps.md +52 -0
- package/skills/shared/cost-tracking/references/pricing-tables.md +51 -0
- package/skills/shared/cost-tracking/scripts/verify.sh +135 -0
- package/skills/shared/cpp/SKILL.md +302 -0
- package/skills/shared/cpp/evals/README.md +14 -0
- package/skills/shared/cpp/evals/cases.yaml +44 -0
- package/skills/shared/cpp/references/cmake.md +167 -0
- package/skills/shared/cpp/references/move-and-templates.md +130 -0
- package/skills/shared/cpp/references/undefined-behavior.md +86 -0
- package/skills/shared/cpp/scripts/verify.sh +165 -0
- package/skills/shared/csharp-dotnet/SKILL.md +272 -0
- package/skills/shared/csharp-dotnet/evals/README.md +3 -0
- package/skills/shared/csharp-dotnet/evals/cases.yaml +48 -0
- package/skills/shared/csharp-dotnet/references/aspnetcore.md +99 -0
- package/skills/shared/csharp-dotnet/references/async.md +82 -0
- package/skills/shared/csharp-dotnet/references/efcore.md +96 -0
- package/skills/shared/csharp-dotnet/scripts/verify.sh +90 -0
- package/skills/shared/data-cleaning/SKILL.md +259 -0
- package/skills/shared/data-cleaning/evals/README.md +16 -0
- package/skills/shared/data-cleaning/evals/cases.yaml +57 -0
- package/skills/shared/data-cleaning/references/normalization-recipes.md +136 -0
- package/skills/shared/data-cleaning/references/validation-patterns.md +134 -0
- package/skills/shared/data-cleaning/scripts/verify.sh +115 -0
- package/skills/shared/data-scraper/SKILL.md +130 -0
- package/skills/shared/data-scraper/evals/README.md +3 -0
- package/skills/shared/data-scraper/evals/cases.yaml +46 -0
- package/skills/shared/data-scraper/references/anti-bot.md +85 -0
- package/skills/shared/data-scraper/references/frameworks.md +116 -0
- package/skills/shared/data-scraper/references/legal-compliance.md +64 -0
- package/skills/shared/data-scraper/scripts/verify.sh +166 -0
- package/skills/shared/db-migrations/SKILL.md +237 -0
- package/skills/shared/db-migrations/evals/README.md +10 -0
- package/skills/shared/db-migrations/evals/cases.yaml +46 -0
- package/skills/shared/db-migrations/references/backfill-and-batching.md +105 -0
- package/skills/shared/db-migrations/references/expand-contract-playbook.md +152 -0
- package/skills/shared/db-migrations/references/tools-and-runners.md +88 -0
- package/skills/shared/db-migrations/scripts/verify.sh +112 -0
- package/skills/shared/debug/SKILL.md +188 -0
- package/skills/shared/debug/evals/README.md +88 -0
- package/skills/shared/debug/evals/cases.yaml +74 -0
- package/skills/shared/decision-challenge/SKILL.md +113 -0
- package/skills/shared/decision-challenge/evals/README.md +3 -0
- package/skills/shared/decision-challenge/evals/cases.yaml +42 -0
- package/skills/shared/decision-records/SKILL.md +202 -0
- package/skills/shared/decision-records/evals/README.md +3 -0
- package/skills/shared/decision-records/evals/cases.yaml +43 -0
- package/skills/shared/decision-records/references/templates.md +252 -0
- package/skills/shared/decision-records/scripts/verify.sh +105 -0
- package/skills/shared/deep-learning/SKILL.md +226 -0
- package/skills/shared/deep-learning/evals/README.md +11 -0
- package/skills/shared/deep-learning/evals/cases.yaml +48 -0
- package/skills/shared/deep-learning/references/distributed.md +105 -0
- package/skills/shared/deep-learning/references/training-recipes.md +135 -0
- package/skills/shared/deployment/SKILL.md +374 -0
- package/skills/shared/deployment/evals/README.md +50 -0
- package/skills/shared/deployment/evals/cases.yaml +53 -0
- package/skills/shared/deployment/references/coolify.md +216 -0
- package/skills/shared/deployment/references/dockerfiles-by-stack.md +319 -0
- package/skills/shared/deployment/references/github-actions.md +295 -0
- package/skills/shared/deployment/references/hosting-targets.md +272 -0
- package/skills/shared/deployment/scripts/verify.sh +134 -0
- package/skills/shared/deprecation/SKILL.md +123 -0
- package/skills/shared/deprecation/evals/README.md +3 -0
- package/skills/shared/deprecation/evals/cases.yaml +42 -0
- package/skills/shared/design/IMPECCABLE-LICENSE.txt +191 -0
- package/skills/shared/design/SKILL.md +111 -0
- package/skills/shared/design/THIRD_PARTY.md +26 -0
- package/skills/shared/design/engine/LICENSE +21 -0
- package/skills/shared/design/engine/RUN.md +515 -0
- package/skills/shared/design/engine/UPSTREAM.json +4 -0
- package/skills/shared/design/engine/assets/app.css +216 -0
- package/skills/shared/design/engine/assets/base.css +105 -0
- package/skills/shared/design/engine/assets/base.js +37 -0
- package/skills/shared/design/engine/assets/charts.js +617 -0
- package/skills/shared/design/engine/assets/scenes/stage.css +28 -0
- package/skills/shared/design/engine/assets/scenes/stage.js +133 -0
- package/skills/shared/design/engine/library/app.json +1327 -0
- package/skills/shared/design/engine/library/directions.json +1078 -0
- package/skills/shared/design/engine/library/heroes.json +1086 -0
- package/skills/shared/design/engine/library/motions.json +92 -0
- package/skills/shared/design/engine/library/palettes.json +1485 -0
- package/skills/shared/design/engine/library/photos.json +7240 -0
- package/skills/shared/design/engine/library/sections.json +4429 -0
- package/skills/shared/design/engine/library/sites.json +8350 -0
- package/skills/shared/design/engine/library/type.json +2024 -0
- package/skills/shared/design/engine/references/app-screens.md +1199 -0
- package/skills/shared/design/engine/references/colour.md +141 -0
- package/skills/shared/design/engine/references/copy.md +177 -0
- package/skills/shared/design/engine/references/critique.md +182 -0
- package/skills/shared/design/engine/references/imagery.md +791 -0
- package/skills/shared/design/engine/references/layout.md +292 -0
- package/skills/shared/design/engine/references/motion.md +980 -0
- package/skills/shared/design/engine/references/research.md +356 -0
- package/skills/shared/design/engine/references/scenes.md +174 -0
- package/skills/shared/design/engine/references/tells.md +115 -0
- package/skills/shared/design/engine/references/type.md +170 -0
- package/skills/shared/design/engine/scripts/assets.mjs +865 -0
- package/skills/shared/design/engine/scripts/draw.mjs +584 -0
- package/skills/shared/design/engine/scripts/inspect.mjs +303 -0
- package/skills/shared/design/engine/scripts/lib/app-draw.mjs +203 -0
- package/skills/shared/design/engine/scripts/lib/cdp.mjs +248 -0
- package/skills/shared/design/engine/scripts/lib/probe.mjs +909 -0
- package/skills/shared/design/engine/scripts/lib/rules.mjs +243 -0
- package/skills/shared/design/engine/scripts/lint.mjs +119 -0
- package/skills/shared/design/engine/scripts/scenes.mjs +124 -0
- package/skills/shared/design/evals/cases.yaml +45 -0
- package/skills/shared/design/references/design-md.md +49 -0
- package/skills/shared/design/references/finish-review.md +46 -0
- package/skills/shared/design/references/modes.md +51 -0
- package/skills/shared/digitalocean/SKILL.md +227 -0
- package/skills/shared/digitalocean/evals/README.md +10 -0
- package/skills/shared/digitalocean/evals/cases.yaml +37 -0
- package/skills/shared/digitalocean/references/app-spec.md +126 -0
- package/skills/shared/digitalocean/references/droplet-ops.md +95 -0
- package/skills/shared/digitalocean/scripts/verify.sh +102 -0
- package/skills/shared/django/SKILL.md +263 -0
- package/skills/shared/django/evals/README.md +11 -0
- package/skills/shared/django/evals/cases.yaml +47 -0
- package/skills/shared/django/references/drf.md +109 -0
- package/skills/shared/django/references/orm-performance.md +91 -0
- package/skills/shared/django/references/security.md +81 -0
- package/skills/shared/django/references/testing.md +86 -0
- package/skills/shared/django/scripts/verify.sh +115 -0
- package/skills/shared/docker/SKILL.md +251 -0
- package/skills/shared/docker/evals/README.md +10 -0
- package/skills/shared/docker/evals/cases.yaml +44 -0
- package/skills/shared/docker/references/base-images-and-stages.md +104 -0
- package/skills/shared/docker/references/compose-recipes.md +109 -0
- package/skills/shared/docker/scripts/verify.sh +149 -0
- package/skills/shared/domains-dns/SKILL.md +131 -0
- package/skills/shared/domains-dns/evals/README.md +16 -0
- package/skills/shared/domains-dns/evals/cases.yaml +47 -0
- package/skills/shared/domains-dns/references/record-cookbook.md +94 -0
- package/skills/shared/domains-dns/references/tls-and-acme.md +90 -0
- package/skills/shared/domains-dns/references/verify-and-debug.md +64 -0
- package/skills/shared/domains-dns/scripts/verify.sh +163 -0
- package/skills/shared/drizzle-orm/SKILL.md +231 -0
- package/skills/shared/drizzle-orm/evals/README.md +12 -0
- package/skills/shared/drizzle-orm/evals/cases.yaml +47 -0
- package/skills/shared/drizzle-orm/references/relations-and-drivers.md +118 -0
- package/skills/shared/drizzle-orm/scripts/verify.sh +155 -0
- package/skills/shared/duckdb/SKILL.md +202 -0
- package/skills/shared/duckdb/evals/README.md +31 -0
- package/skills/shared/duckdb/evals/cases.yaml +41 -0
- package/skills/shared/duckdb/references/python-and-interop.md +105 -0
- package/skills/shared/duckdb/references/remote-and-lakehouse.md +101 -0
- package/skills/shared/duckdb/scripts/verify.sh +71 -0
- package/skills/shared/dynamodb/SKILL.md +204 -0
- package/skills/shared/dynamodb/evals/README.md +8 -0
- package/skills/shared/dynamodb/evals/cases.yaml +46 -0
- package/skills/shared/dynamodb/references/access-patterns.md +127 -0
- package/skills/shared/dynamodb/references/capacity-and-limits.md +78 -0
- package/skills/shared/dynamodb/scripts/verify.sh +108 -0
- package/skills/shared/e2e-testing/SKILL.md +233 -0
- package/skills/shared/e2e-testing/evals/README.md +10 -0
- package/skills/shared/e2e-testing/evals/cases.yaml +64 -0
- package/skills/shared/e2e-testing/references/config-and-ci.md +156 -0
- package/skills/shared/e2e-testing/references/flakiness-playbook.md +124 -0
- package/skills/shared/e2e-testing/scripts/verify.sh +117 -0
- package/skills/shared/electron/SKILL.md +209 -0
- package/skills/shared/electron/evals/README.md +13 -0
- package/skills/shared/electron/evals/cases.yaml +38 -0
- package/skills/shared/electron/references/packaging-and-updates.md +122 -0
- package/skills/shared/electron/references/security-and-ipc.md +158 -0
- package/skills/shared/electron/scripts/verify.sh +143 -0
- package/skills/shared/elixir/SKILL.md +209 -0
- package/skills/shared/elixir/evals/README.md +3 -0
- package/skills/shared/elixir/evals/cases.yaml +41 -0
- package/skills/shared/elixir/references/mix-and-releases.md +91 -0
- package/skills/shared/elixir/references/otp-patterns.md +96 -0
- package/skills/shared/elixir/scripts/verify.sh +76 -0
- package/skills/shared/email-deliverability/SKILL.md +148 -0
- package/skills/shared/email-deliverability/evals/README.md +21 -0
- package/skills/shared/email-deliverability/evals/cases.yaml +45 -0
- package/skills/shared/email-deliverability/scripts/verify.sh +98 -0
- package/skills/shared/embeddings-search/SKILL.md +184 -0
- package/skills/shared/embeddings-search/evals/README.md +10 -0
- package/skills/shared/embeddings-search/evals/cases.yaml +44 -0
- package/skills/shared/embeddings-search/references/evaluation.md +86 -0
- package/skills/shared/embeddings-search/references/models.md +73 -0
- package/skills/shared/embeddings-search/scripts/verify.sh +103 -0
- package/skills/shared/error-handling/SKILL.md +273 -0
- package/skills/shared/error-handling/evals/README.md +12 -0
- package/skills/shared/error-handling/evals/cases.yaml +46 -0
- package/skills/shared/error-handling/references/boundaries-and-messaging.md +120 -0
- package/skills/shared/error-handling/references/retry-and-resilience.md +154 -0
- package/skills/shared/error-handling/scripts/verify.sh +110 -0
- package/skills/shared/expo/SKILL.md +243 -0
- package/skills/shared/expo/evals/README.md +13 -0
- package/skills/shared/expo/evals/cases.yaml +44 -0
- package/skills/shared/expo/references/config-plugins.md +117 -0
- package/skills/shared/expo/references/eas-update.md +118 -0
- package/skills/shared/expo/scripts/verify.sh +132 -0
- package/skills/shared/fal/SKILL.md +196 -0
- package/skills/shared/fal/evals/README.md +3 -0
- package/skills/shared/fal/evals/cases.yaml +42 -0
- package/skills/shared/fal/references/models-and-cost.md +53 -0
- package/skills/shared/fal/references/queue-and-webhooks.md +153 -0
- package/skills/shared/fal/scripts/verify.sh +72 -0
- package/skills/shared/fastapi/SKILL.md +469 -0
- package/skills/shared/fastapi/evals/README.md +50 -0
- package/skills/shared/fastapi/evals/cases.yaml +55 -0
- package/skills/shared/fastapi/references/database.md +347 -0
- package/skills/shared/fastapi/references/production.md +338 -0
- package/skills/shared/fastapi/references/security.md +330 -0
- package/skills/shared/fastapi/references/testing.md +349 -0
- package/skills/shared/fastapi/scripts/verify.sh +157 -0
- package/skills/shared/finetuning/SKILL.md +232 -0
- package/skills/shared/finetuning/evals/README.md +13 -0
- package/skills/shared/finetuning/evals/cases.yaml +50 -0
- package/skills/shared/finetuning/references/hyperparameters-and-eval.md +74 -0
- package/skills/shared/finetuning/references/methods.md +189 -0
- package/skills/shared/firebase/SKILL.md +237 -0
- package/skills/shared/firebase/evals/README.md +12 -0
- package/skills/shared/firebase/evals/cases.yaml +45 -0
- package/skills/shared/firebase/references/cloud-functions.md +102 -0
- package/skills/shared/firebase/references/data-modeling.md +108 -0
- package/skills/shared/firebase/references/security-rules.md +137 -0
- package/skills/shared/firebase/scripts/verify.sh +98 -0
- package/skills/shared/flutter/SKILL.md +400 -0
- package/skills/shared/flutter/evals/README.md +54 -0
- package/skills/shared/flutter/evals/cases.yaml +69 -0
- package/skills/shared/flutter/references/architecture-and-state.md +499 -0
- package/skills/shared/flutter/references/i18n-and-dependencies.md +197 -0
- package/skills/shared/flutter/references/performance.md +299 -0
- package/skills/shared/flutter/references/testing.md +385 -0
- package/skills/shared/flutter/references/ui-and-navigation.md +378 -0
- package/skills/shared/flutter/scripts/verify.sh +104 -0
- package/skills/shared/fly-io/SKILL.md +197 -0
- package/skills/shared/fly-io/evals/README.md +3 -0
- package/skills/shared/fly-io/evals/cases.yaml +42 -0
- package/skills/shared/fly-io/references/fly-toml.md +155 -0
- package/skills/shared/fly-io/references/multi-region.md +66 -0
- package/skills/shared/fly-io/scripts/verify.sh +90 -0
- package/skills/shared/ftd/SKILL.md +111 -0
- package/skills/shared/ftd/evals/cases.yaml +66 -0
- package/skills/shared/ftd/evals/routing.json +24 -0
- package/skills/shared/game-design/SKILL.md +146 -0
- package/skills/shared/game-design/evals/README.md +13 -0
- package/skills/shared/game-design/evals/cases.yaml +47 -0
- package/skills/shared/game-design/references/mechanics-and-loops.md +78 -0
- package/skills/shared/game-design/references/playtesting-and-scope.md +58 -0
- package/skills/shared/game-design/references/progression-and-economy.md +70 -0
- package/skills/shared/game-storytelling/SKILL.md +134 -0
- package/skills/shared/game-storytelling/evals/README.md +28 -0
- package/skills/shared/game-storytelling/evals/cases.yaml +51 -0
- package/skills/shared/game-storytelling/references/branching-and-state.md +134 -0
- package/skills/shared/game-storytelling/references/dialogue-tools.md +117 -0
- package/skills/shared/game-storytelling/references/environmental-storytelling.md +71 -0
- package/skills/shared/gamedev-multiplayer/SKILL.md +237 -0
- package/skills/shared/gamedev-multiplayer/evals/README.md +12 -0
- package/skills/shared/gamedev-multiplayer/evals/cases.yaml +44 -0
- package/skills/shared/gamedev-multiplayer/references/godot-high-level-multiplayer.md +133 -0
- package/skills/shared/gamedev-multiplayer/references/prediction-and-latency.md +104 -0
- package/skills/shared/gamedev-multiplayer/references/unity-and-unreal-netcode.md +166 -0
- package/skills/shared/gamedev-pathing/SKILL.md +233 -0
- package/skills/shared/gamedev-pathing/evals/README.md +14 -0
- package/skills/shared/gamedev-pathing/evals/cases.yaml +49 -0
- package/skills/shared/gamedev-pathing/references/navmesh-workflow.md +110 -0
- package/skills/shared/gamedev-pathing/references/search-algorithms.md +135 -0
- package/skills/shared/gamedev-pathing/references/steering-and-avoidance.md +120 -0
- package/skills/shared/gamedev-physics/SKILL.md +225 -0
- package/skills/shared/gamedev-physics/evals/README.md +12 -0
- package/skills/shared/gamedev-physics/evals/cases.yaml +54 -0
- package/skills/shared/gamedev-physics/references/character-controllers.md +120 -0
- package/skills/shared/gamedev-physics/references/determinism-and-queries.md +124 -0
- package/skills/shared/gamedev-physics/references/layers-and-masks.md +115 -0
- package/skills/shared/gamedev-shaders/SKILL.md +232 -0
- package/skills/shared/gamedev-shaders/evals/README.md +14 -0
- package/skills/shared/gamedev-shaders/evals/cases.yaml +45 -0
- package/skills/shared/gamedev-shaders/references/effect-recipes.md +205 -0
- package/skills/shared/gamedev-shaders/references/godot-shading-language.md +133 -0
- package/skills/shared/gamedev-shaders/references/unity-and-unreal-shaders.md +169 -0
- package/skills/shared/gamedev-shipping/SKILL.md +220 -0
- package/skills/shared/gamedev-shipping/evals/README.md +9 -0
- package/skills/shared/gamedev-shipping/evals/cases.yaml +55 -0
- package/skills/shared/gamedev-shipping/references/mobile-and-desktop.md +127 -0
- package/skills/shared/gamedev-shipping/references/stores-and-ci.md +162 -0
- package/skills/shared/gamedev-shipping/references/web-export-headers.md +140 -0
- package/skills/shared/gcp-essentials/SKILL.md +324 -0
- package/skills/shared/gcp-essentials/evals/README.md +12 -0
- package/skills/shared/gcp-essentials/evals/cases.yaml +38 -0
- package/skills/shared/gcp-essentials/references/deploy-recipes.md +81 -0
- package/skills/shared/gcp-essentials/references/iam-and-auth.md +94 -0
- package/skills/shared/gcp-essentials/references/networking-and-sql.md +74 -0
- package/skills/shared/gcp-essentials/scripts/verify.sh +158 -0
- package/skills/shared/git-workflow/SKILL.md +214 -0
- package/skills/shared/git-workflow/evals/README.md +10 -0
- package/skills/shared/git-workflow/evals/cases.yaml +52 -0
- package/skills/shared/git-workflow/references/gitmoji.md +157 -0
- package/skills/shared/git-workflow/references/interactive-rebase.md +89 -0
- package/skills/shared/github-actions/SKILL.md +244 -0
- package/skills/shared/github-actions/evals/README.md +3 -0
- package/skills/shared/github-actions/evals/cases.yaml +45 -0
- package/skills/shared/github-actions/references/caching-and-matrix.md +92 -0
- package/skills/shared/github-actions/references/oidc-deploys.md +130 -0
- package/skills/shared/github-actions/scripts/verify.sh +105 -0
- package/skills/shared/gitlab-auth/SKILL.md +99 -0
- package/skills/shared/gitlab-workflow/SKILL.md +138 -0
- package/skills/shared/go/SKILL.md +390 -0
- package/skills/shared/go/evals/README.md +56 -0
- package/skills/shared/go/evals/cases.yaml +55 -0
- package/skills/shared/go/references/concurrency.md +557 -0
- package/skills/shared/go/references/http-services.md +529 -0
- package/skills/shared/go/references/testing.md +338 -0
- package/skills/shared/go/scripts/verify.sh +134 -0
- package/skills/shared/godot/SKILL.md +300 -0
- package/skills/shared/godot/evals/README.md +13 -0
- package/skills/shared/godot/evals/cases.yaml +50 -0
- package/skills/shared/godot/references/export-and-testing.md +102 -0
- package/skills/shared/godot/references/gdscript-style.md +84 -0
- package/skills/shared/godot/references/godot3-to-4-traps.md +89 -0
- package/skills/shared/godot/references/nodes-scenes-resources.md +109 -0
- package/skills/shared/harness/SKILL.md +316 -0
- package/skills/shared/harness/assets/_TEMPLATE/.env.example +8 -0
- package/skills/shared/harness/assets/_TEMPLATE/CREDENTIALS.md +25 -0
- package/skills/shared/harness/assets/_TEMPLATE/README.md +25 -0
- package/skills/shared/harness/assets/_TEMPLATE/gitignore +10 -0
- package/skills/shared/harness/assets/_TEMPLATE/test_connection.sh +30 -0
- package/skills/shared/harness/evals/README.md +54 -0
- package/skills/shared/harness/evals/cases.yaml +72 -0
- package/skills/shared/harness/examples/audit-example.md +120 -0
- package/skills/shared/harness/references/agents-md-template.md +41 -0
- package/skills/shared/harness/references/audit-report-template.html +140 -0
- package/skills/shared/harness/references/audit-report-template.md +116 -0
- package/skills/shared/harness/references/claude-md-template.md +85 -0
- package/skills/shared/harness/references/daily-curation-automation.md +68 -0
- package/skills/shared/harness/references/inbox-readme-template.md +51 -0
- package/skills/shared/harness/references/ingest-formats.md +185 -0
- package/skills/shared/harness/references/ingest-ignore-defaults.md +48 -0
- package/skills/shared/harness/references/obsidian-scaffolding.md +103 -0
- package/skills/shared/harness/references/providers.yaml +3546 -0
- package/skills/shared/harness/references/tools-readme-template.md +88 -0
- package/skills/shared/harness/references/wiki-archive-template.html +81 -0
- package/skills/shared/harness/references/wiki-article-template.md +60 -0
- package/skills/shared/harness/references/wiki-dashboard-template.html +136 -0
- package/skills/shared/harness/references/wiki-deep-improve-report-template.html +126 -0
- package/skills/shared/harness/references/wiki-gaps-template.md +22 -0
- package/skills/shared/harness/references/wiki-index-template.md +32 -0
- package/skills/shared/harness/references/wiki-protocol.md +926 -0
- package/skills/shared/harness/references/wiki-raw-template.md +7 -0
- package/skills/shared/harness/references/wiki-worklog-template.md +60 -0
- package/skills/shared/hetzner/SKILL.md +203 -0
- package/skills/shared/hetzner/evals/README.md +35 -0
- package/skills/shared/hetzner/evals/cases.yaml +46 -0
- package/skills/shared/hetzner/references/cloud-init.md +120 -0
- package/skills/shared/hetzner/references/plans-and-locations.md +56 -0
- package/skills/shared/hetzner/scripts/verify.sh +122 -0
- package/skills/shared/htmx/SKILL.md +261 -0
- package/skills/shared/htmx/evals/README.md +3 -0
- package/skills/shared/htmx/evals/cases.yaml +38 -0
- package/skills/shared/htmx/references/patterns.md +113 -0
- package/skills/shared/htmx/references/server-contract.md +91 -0
- package/skills/shared/htmx/scripts/verify.sh +93 -0
- package/skills/shared/huggingface/SKILL.md +191 -0
- package/skills/shared/huggingface/evals/README.md +11 -0
- package/skills/shared/huggingface/evals/cases.yaml +41 -0
- package/skills/shared/huggingface/references/endpoints-and-spaces.md +99 -0
- package/skills/shared/huggingface/references/hub-and-cli.md +85 -0
- package/skills/shared/huggingface/references/inference-providers.md +115 -0
- package/skills/shared/huggingface/scripts/verify.sh +123 -0
- package/skills/shared/idea-refinement/SKILL.md +121 -0
- package/skills/shared/idea-refinement/evals/README.md +3 -0
- package/skills/shared/idea-refinement/evals/cases.yaml +42 -0
- package/skills/shared/implement/SKILL.md +383 -0
- package/skills/shared/implement/evals/README.md +56 -0
- package/skills/shared/implement/evals/cases.yaml +43 -0
- package/skills/shared/implement/references/per-task-review.md +46 -0
- package/skills/shared/implement/scripts/review-package +59 -0
- package/skills/shared/implement/scripts/sdd-workspace +47 -0
- package/skills/shared/implement/scripts/task-brief +77 -0
- package/skills/shared/init/SKILL.md +209 -0
- package/skills/shared/init/evals/README.md +49 -0
- package/skills/shared/init/evals/cases.yaml +74 -0
- package/skills/shared/init/references/accompaniment-and-profile.md +122 -0
- package/skills/shared/init/references/discovery.md +90 -0
- package/skills/shared/init/references/recommend-skills.md +115 -0
- package/skills/shared/init/scripts/verify.sh +118 -0
- package/skills/shared/java/SKILL.md +341 -0
- package/skills/shared/java/evals/README.md +23 -0
- package/skills/shared/java/evals/cases.yaml +43 -0
- package/skills/shared/java/references/builds.md +133 -0
- package/skills/shared/java/references/concurrency.md +108 -0
- package/skills/shared/java/references/streams.md +102 -0
- package/skills/shared/java/scripts/verify.sh +107 -0
- package/skills/shared/knowledge-ops/SKILL.md +122 -0
- package/skills/shared/knowledge-ops/evals/README.md +16 -0
- package/skills/shared/knowledge-ops/evals/cases.yaml +50 -0
- package/skills/shared/knowledge-ops/references/gardening-playbook.md +126 -0
- package/skills/shared/kotlin-android/SKILL.md +245 -0
- package/skills/shared/kotlin-android/evals/README.md +13 -0
- package/skills/shared/kotlin-android/evals/cases.yaml +56 -0
- package/skills/shared/kotlin-android/references/architecture.md +200 -0
- package/skills/shared/kotlin-android/references/gradle-setup.md +125 -0
- package/skills/shared/kotlin-android/scripts/verify.sh +109 -0
- package/skills/shared/laravel/SKILL.md +301 -0
- package/skills/shared/laravel/evals/README.md +10 -0
- package/skills/shared/laravel/evals/cases.yaml +45 -0
- package/skills/shared/laravel/references/eloquent-patterns.md +126 -0
- package/skills/shared/laravel/references/queues-and-scheduling.md +153 -0
- package/skills/shared/laravel/scripts/verify.sh +128 -0
- package/skills/shared/level-design/SKILL.md +143 -0
- package/skills/shared/level-design/evals/README.md +35 -0
- package/skills/shared/level-design/evals/cases.yaml +58 -0
- package/skills/shared/level-design/references/blockout-to-art.md +118 -0
- package/skills/shared/level-design/references/guidance-and-readability.md +152 -0
- package/skills/shared/level-design/references/pacing-and-encounters.md +142 -0
- package/skills/shared/llm-pipeline/SKILL.md +150 -0
- package/skills/shared/llm-pipeline/evals/README.md +3 -0
- package/skills/shared/llm-pipeline/evals/cases.yaml +44 -0
- package/skills/shared/llm-pipeline/references/caching-layers.md +60 -0
- package/skills/shared/llm-pipeline/references/litellm-router.md +101 -0
- package/skills/shared/llm-pipeline/scripts/verify.sh +169 -0
- package/skills/shared/machine-learning/SKILL.md +228 -0
- package/skills/shared/machine-learning/evals/README.md +15 -0
- package/skills/shared/machine-learning/evals/cases.yaml +54 -0
- package/skills/shared/machine-learning/references/gbdt-and-tuning.md +83 -0
- package/skills/shared/machine-learning/references/metrics-and-imbalance.md +82 -0
- package/skills/shared/machine-learning/references/pipelines-and-cv.md +135 -0
- package/skills/shared/modal/SKILL.md +290 -0
- package/skills/shared/modal/evals/README.md +29 -0
- package/skills/shared/modal/evals/cases.yaml +50 -0
- package/skills/shared/modal/references/images-gpu-cookbook.md +160 -0
- package/skills/shared/modal/references/web-and-scaling.md +138 -0
- package/skills/shared/modal/scripts/verify.sh +127 -0
- package/skills/shared/mongodb/SKILL.md +342 -0
- package/skills/shared/mongodb/evals/README.md +29 -0
- package/skills/shared/mongodb/evals/cases.yaml +41 -0
- package/skills/shared/mongodb/references/aggregation.md +115 -0
- package/skills/shared/mongodb/references/data-modeling.md +135 -0
- package/skills/shared/mongodb/references/transactions-and-ops.md +128 -0
- package/skills/shared/mongodb/scripts/verify.sh +151 -0
- package/skills/shared/monitoring/SKILL.md +155 -0
- package/skills/shared/monitoring/evals/README.md +3 -0
- package/skills/shared/monitoring/evals/cases.yaml +47 -0
- package/skills/shared/monitoring/references/burn-rate-and-oncall.md +128 -0
- package/skills/shared/monitoring/references/tool-setup.md +154 -0
- package/skills/shared/monitoring/scripts/verify.sh +145 -0
- package/skills/shared/mysql/SKILL.md +249 -0
- package/skills/shared/mysql/evals/README.md +12 -0
- package/skills/shared/mysql/evals/cases.yaml +49 -0
- package/skills/shared/mysql/references/indexing-and-explain.md +161 -0
- package/skills/shared/mysql/references/mysql-vs-mariadb.md +78 -0
- package/skills/shared/mysql/references/online-ddl-and-migrations.md +120 -0
- package/skills/shared/mysql/references/replication-and-ha.md +115 -0
- package/skills/shared/mysql/scripts/verify.sh +141 -0
- package/skills/shared/neon/SKILL.md +218 -0
- package/skills/shared/neon/evals/README.md +11 -0
- package/skills/shared/neon/evals/cases.yaml +45 -0
- package/skills/shared/neon/references/branching-ci.md +86 -0
- package/skills/shared/neon/scripts/verify.sh +78 -0
- package/skills/shared/nestjs/SKILL.md +217 -0
- package/skills/shared/nestjs/evals/README.md +3 -0
- package/skills/shared/nestjs/evals/cases.yaml +38 -0
- package/skills/shared/nestjs/references/cross-cutting.md +135 -0
- package/skills/shared/nestjs/references/testing-recipes.md +105 -0
- package/skills/shared/nestjs/scripts/verify.sh +98 -0
- package/skills/shared/netlify/SKILL.md +208 -0
- package/skills/shared/netlify/evals/README.md +13 -0
- package/skills/shared/netlify/evals/cases.yaml +43 -0
- package/skills/shared/netlify/references/functions.md +97 -0
- package/skills/shared/netlify/references/netlify-toml.md +115 -0
- package/skills/shared/netlify/scripts/verify.sh +95 -0
- package/skills/shared/nextjs/SKILL.md +400 -0
- package/skills/shared/nextjs/evals/README.md +59 -0
- package/skills/shared/nextjs/evals/cases.yaml +56 -0
- package/skills/shared/nextjs/references/data-and-caching.md +309 -0
- package/skills/shared/nextjs/references/metadata.md +208 -0
- package/skills/shared/nextjs/references/performance.md +325 -0
- package/skills/shared/nextjs/references/react.md +383 -0
- package/skills/shared/nextjs/references/security.md +239 -0
- package/skills/shared/nextjs/references/testing.md +290 -0
- package/skills/shared/nextjs/scripts/verify.sh +141 -0
- package/skills/shared/nlp/SKILL.md +225 -0
- package/skills/shared/nlp/evals/README.md +10 -0
- package/skills/shared/nlp/evals/cases.yaml +45 -0
- package/skills/shared/nlp/references/evaluation.md +107 -0
- package/skills/shared/nlp/references/tokenization.md +95 -0
- package/skills/shared/no-code-app/SKILL.md +145 -0
- package/skills/shared/no-code-app/evals/README.md +3 -0
- package/skills/shared/no-code-app/evals/cases.yaml +43 -0
- package/skills/shared/no-code-app/references/platform-limits.md +100 -0
- package/skills/shared/nodejs/SKILL.md +229 -0
- package/skills/shared/nodejs/evals/README.md +3 -0
- package/skills/shared/nodejs/evals/cases.yaml +39 -0
- package/skills/shared/nodejs/references/express5-migration.md +53 -0
- package/skills/shared/nodejs/references/graceful-shutdown.md +73 -0
- package/skills/shared/nodejs/scripts/verify.sh +122 -0
- package/skills/shared/observability/SKILL.md +235 -0
- package/skills/shared/observability/evals/README.md +3 -0
- package/skills/shared/observability/evals/cases.yaml +51 -0
- package/skills/shared/observability/references/collector-config.md +98 -0
- package/skills/shared/observability/references/instrumentation-recipes.md +115 -0
- package/skills/shared/observability/scripts/verify.sh +156 -0
- package/skills/shared/ollama/SKILL.md +213 -0
- package/skills/shared/ollama/evals/README.md +9 -0
- package/skills/shared/ollama/evals/cases.yaml +43 -0
- package/skills/shared/ollama/references/api.md +148 -0
- package/skills/shared/ollama/references/hardware-sizing.md +87 -0
- package/skills/shared/ollama/scripts/verify.sh +116 -0
- package/skills/shared/open-weights/SKILL.md +239 -0
- package/skills/shared/open-weights/evals/README.md +14 -0
- package/skills/shared/open-weights/evals/cases.yaml +42 -0
- package/skills/shared/open-weights/references/licenses.md +123 -0
- package/skills/shared/open-weights/references/model-families.md +83 -0
- package/skills/shared/open-weights/references/sizing-and-quant.md +89 -0
- package/skills/shared/orient/SKILL.md +92 -0
- package/skills/shared/orient/evals/README.md +18 -0
- package/skills/shared/orient/evals/cases.yaml +67 -0
- package/skills/shared/orient/references/explain-ladder.md +93 -0
- package/skills/shared/orient/references/orientation-contract.md +86 -0
- package/skills/shared/parallel/SKILL.md +168 -0
- package/skills/shared/parallel/evals/README.md +62 -0
- package/skills/shared/parallel/evals/cases.yaml +44 -0
- package/skills/shared/performance/SKILL.md +229 -0
- package/skills/shared/performance/evals/README.md +3 -0
- package/skills/shared/performance/evals/cases.yaml +49 -0
- package/skills/shared/performance/references/profiling-playbook.md +54 -0
- package/skills/shared/performance/scripts/verify.sh +94 -0
- package/skills/shared/phoenix/SKILL.md +169 -0
- package/skills/shared/phoenix/evals/README.md +3 -0
- package/skills/shared/phoenix/evals/cases.yaml +40 -0
- package/skills/shared/phoenix/references/auth-and-scopes.md +82 -0
- package/skills/shared/phoenix/references/ecto-patterns.md +93 -0
- package/skills/shared/phoenix/references/liveview.md +134 -0
- package/skills/shared/phoenix/scripts/verify.sh +73 -0
- package/skills/shared/php/SKILL.md +397 -0
- package/skills/shared/php/evals/README.md +12 -0
- package/skills/shared/php/evals/cases.yaml +45 -0
- package/skills/shared/php/references/tooling.md +170 -0
- package/skills/shared/php/references/type-system.md +220 -0
- package/skills/shared/php/scripts/verify.sh +155 -0
- package/skills/shared/plan/SKILL.md +166 -0
- package/skills/shared/plan/evals/README.md +62 -0
- package/skills/shared/plan/evals/cases.yaml +49 -0
- package/skills/shared/plan/references/plan-template.md +153 -0
- package/skills/shared/planetscale/SKILL.md +198 -0
- package/skills/shared/planetscale/evals/README.md +11 -0
- package/skills/shared/planetscale/evals/cases.yaml +46 -0
- package/skills/shared/planetscale/references/deploy-requests.md +75 -0
- package/skills/shared/planetscale/references/no-foreign-keys.md +88 -0
- package/skills/shared/planetscale/scripts/verify.sh +115 -0
- package/skills/shared/planning-with-files/SKILL.md +278 -0
- package/skills/shared/planning-with-files/examples.md +202 -0
- package/skills/shared/planning-with-files/reference.md +218 -0
- package/skills/shared/planning-with-files/scripts/attest-plan.ps1 +118 -0
- package/skills/shared/planning-with-files/scripts/attest-plan.sh +110 -0
- package/skills/shared/planning-with-files/scripts/check-complete.ps1 +44 -0
- package/skills/shared/planning-with-files/scripts/check-complete.sh +46 -0
- package/skills/shared/planning-with-files/scripts/init-session.ps1 +166 -0
- package/skills/shared/planning-with-files/scripts/init-session.sh +284 -0
- package/skills/shared/planning-with-files/scripts/resolve-plan-dir.ps1 +44 -0
- package/skills/shared/planning-with-files/scripts/resolve-plan-dir.sh +76 -0
- package/skills/shared/planning-with-files/scripts/session-catchup.py +438 -0
- package/skills/shared/planning-with-files/scripts/set-active-plan.ps1 +50 -0
- package/skills/shared/planning-with-files/scripts/set-active-plan.sh +50 -0
- package/skills/shared/planning-with-files/templates/findings.md +95 -0
- package/skills/shared/planning-with-files/templates/progress.md +114 -0
- package/skills/shared/planning-with-files/templates/task_plan.md +132 -0
- package/skills/shared/postgresdb/SKILL.md +354 -0
- package/skills/shared/postgresdb/evals/README.md +55 -0
- package/skills/shared/postgresdb/evals/cases.yaml +57 -0
- package/skills/shared/postgresdb/references/migrations.md +279 -0
- package/skills/shared/postgresdb/references/operations-and-security.md +267 -0
- package/skills/shared/postgresdb/references/query-optimization.md +374 -0
- package/skills/shared/postgresdb/references/schema-and-indexing.md +379 -0
- package/skills/shared/postgresdb/scripts/verify.sh +191 -0
- package/skills/shared/prisma-orm/SKILL.md +313 -0
- package/skills/shared/prisma-orm/evals/README.md +12 -0
- package/skills/shared/prisma-orm/evals/cases.yaml +56 -0
- package/skills/shared/prisma-orm/references/migrations-and-v7-upgrade.md +197 -0
- package/skills/shared/prisma-orm/references/queries-and-performance.md +169 -0
- package/skills/shared/prisma-orm/scripts/verify.sh +137 -0
- package/skills/shared/prompt-engineering/SKILL.md +127 -0
- package/skills/shared/prompt-engineering/evals/README.md +11 -0
- package/skills/shared/prompt-engineering/evals/cases.yaml +46 -0
- package/skills/shared/prompt-engineering/references/eval-templates.md +94 -0
- package/skills/shared/prompt-engineering/references/output-contracts.md +120 -0
- package/skills/shared/prompt-engineering/scripts/verify.sh +84 -0
- package/skills/shared/python/SKILL.md +320 -0
- package/skills/shared/python/evals/README.md +19 -0
- package/skills/shared/python/evals/cases.yaml +46 -0
- package/skills/shared/python/references/async.md +136 -0
- package/skills/shared/python/references/stdlib.md +162 -0
- package/skills/shared/python/references/typing.md +160 -0
- package/skills/shared/python/scripts/verify.sh +125 -0
- package/skills/shared/rag/SKILL.md +209 -0
- package/skills/shared/rag/evals/README.md +13 -0
- package/skills/shared/rag/evals/cases.yaml +45 -0
- package/skills/shared/rag/references/evaluation.md +99 -0
- package/skills/shared/rag/references/pipeline.md +151 -0
- package/skills/shared/rag/scripts/verify.sh +99 -0
- package/skills/shared/rails/SKILL.md +264 -0
- package/skills/shared/rails/evals/README.md +12 -0
- package/skills/shared/rails/evals/cases.yaml +47 -0
- package/skills/shared/rails/references/activerecord.md +148 -0
- package/skills/shared/rails/references/hotwire.md +139 -0
- package/skills/shared/rails/references/testing.md +110 -0
- package/skills/shared/rails/scripts/verify.sh +128 -0
- package/skills/shared/railway/SKILL.md +245 -0
- package/skills/shared/railway/evals/README.md +14 -0
- package/skills/shared/railway/evals/cases.yaml +44 -0
- package/skills/shared/railway/references/cli-cookbook.md +137 -0
- package/skills/shared/railway/references/config-as-code.md +120 -0
- package/skills/shared/railway/scripts/verify.sh +162 -0
- package/skills/shared/react/SKILL.md +182 -0
- package/skills/shared/react/evals/README.md +3 -0
- package/skills/shared/react/evals/cases.yaml +43 -0
- package/skills/shared/react/references/data-and-state.md +152 -0
- package/skills/shared/react/references/performance.md +75 -0
- package/skills/shared/react/references/routing.md +99 -0
- package/skills/shared/react/scripts/verify.sh +123 -0
- package/skills/shared/react-native/SKILL.md +215 -0
- package/skills/shared/react-native/evals/README.md +3 -0
- package/skills/shared/react-native/evals/cases.yaml +42 -0
- package/skills/shared/react-native/references/native-modules.md +123 -0
- package/skills/shared/react-native/references/performance-debugging.md +46 -0
- package/skills/shared/react-native/scripts/verify.sh +120 -0
- package/skills/shared/redis/SKILL.md +298 -0
- package/skills/shared/redis/evals/README.md +10 -0
- package/skills/shared/redis/evals/cases.yaml +43 -0
- package/skills/shared/redis/references/caching.md +116 -0
- package/skills/shared/redis/references/locks-and-rate-limiting.md +140 -0
- package/skills/shared/redis/references/queues.md +102 -0
- package/skills/shared/redis/scripts/verify.sh +164 -0
- package/skills/shared/render/SKILL.md +256 -0
- package/skills/shared/render/evals/README.md +12 -0
- package/skills/shared/render/evals/cases.yaml +45 -0
- package/skills/shared/render/references/blueprint-reference.md +203 -0
- package/skills/shared/render/scripts/verify.sh +167 -0
- package/skills/shared/replicate/SKILL.md +210 -0
- package/skills/shared/replicate/evals/README.md +9 -0
- package/skills/shared/replicate/evals/cases.yaml +45 -0
- package/skills/shared/replicate/references/cog-packaging.md +89 -0
- package/skills/shared/replicate/references/deployments-api.md +87 -0
- package/skills/shared/replicate/references/webhooks-and-async.md +110 -0
- package/skills/shared/replicate/scripts/verify.sh +162 -0
- package/skills/shared/research-ops/SKILL.md +200 -0
- package/skills/shared/research-ops/evals/README.md +13 -0
- package/skills/shared/research-ops/evals/cases.yaml +38 -0
- package/skills/shared/research-ops/references/credibility-rubric.md +78 -0
- package/skills/shared/research-ops/references/memo-template.md +63 -0
- package/skills/shared/research-ops/scripts/verify.sh +181 -0
- package/skills/shared/review/SKILL.md +314 -0
- package/skills/shared/review/evals/README.md +84 -0
- package/skills/shared/review/evals/cases.yaml +55 -0
- package/skills/shared/ruby/SKILL.md +295 -0
- package/skills/shared/ruby/evals/README.md +12 -0
- package/skills/shared/ruby/evals/cases.yaml +41 -0
- package/skills/shared/ruby/references/gems-and-testing.md +208 -0
- package/skills/shared/ruby/references/metaprogramming.md +161 -0
- package/skills/shared/ruby/scripts/verify.sh +83 -0
- package/skills/shared/runpod/SKILL.md +238 -0
- package/skills/shared/runpod/evals/README.md +11 -0
- package/skills/shared/runpod/evals/cases.yaml +47 -0
- package/skills/shared/runpod/references/cost-and-scaling.md +85 -0
- package/skills/shared/runpod/references/serverless-workers.md +101 -0
- package/skills/shared/runpod/scripts/verify.sh +126 -0
- package/skills/shared/rust/SKILL.md +320 -0
- package/skills/shared/rust/evals/README.md +12 -0
- package/skills/shared/rust/evals/cases.yaml +42 -0
- package/skills/shared/rust/references/async-tokio.md +141 -0
- package/skills/shared/rust/references/axum-service.md +132 -0
- package/skills/shared/rust/references/ownership.md +86 -0
- package/skills/shared/rust/references/testing.md +108 -0
- package/skills/shared/rust/scripts/verify.sh +91 -0
- package/skills/shared/scaling/SKILL.md +137 -0
- package/skills/shared/scaling/evals/README.md +3 -0
- package/skills/shared/scaling/evals/cases.yaml +42 -0
- package/skills/shared/scaling/references/load-testing-k6.md +127 -0
- package/skills/shared/scaling/scripts/example.load.js +24 -0
- package/skills/shared/scaling/scripts/verify.sh +70 -0
- package/skills/shared/sdd/SKILL.md +250 -0
- package/skills/shared/sdd/evals/README.md +60 -0
- package/skills/shared/sdd/evals/cases.yaml +90 -0
- package/skills/shared/sdd/references/model-routing.md +194 -0
- package/skills/shared/sdd-init/SKILL.md +189 -0
- package/skills/shared/sdd-init/evals/README.md +3 -0
- package/skills/shared/sdd-init/evals/cases.yaml +43 -0
- package/skills/shared/secure-coding/SKILL.md +336 -0
- package/skills/shared/secure-coding/evals/README.md +68 -0
- package/skills/shared/secure-coding/evals/cases.yaml +55 -0
- package/skills/shared/secure-coding/references/authn-authz.md +249 -0
- package/skills/shared/secure-coding/references/owasp-by-stack.md +574 -0
- package/skills/shared/secure-coding/references/secrets-and-supply-chain.md +205 -0
- package/skills/shared/secure-coding/references/threat-modeling.md +213 -0
- package/skills/shared/secure-coding/scripts/verify.sh +208 -0
- package/skills/shared/security-scan/SKILL.md +219 -0
- package/skills/shared/security-scan/evals/README.md +14 -0
- package/skills/shared/security-scan/evals/cases.yaml +50 -0
- package/skills/shared/security-scan/references/tools.md +97 -0
- package/skills/shared/security-scan/references/triage.md +92 -0
- package/skills/shared/security-scan/scripts/verify.sh +108 -0
- package/skills/shared/ship/SKILL.md +272 -0
- package/skills/shared/ship/evals/README.md +89 -0
- package/skills/shared/ship/evals/cases.yaml +44 -0
- package/skills/shared/shopify/SKILL.md +229 -0
- package/skills/shared/shopify/evals/README.md +14 -0
- package/skills/shared/shopify/evals/cases.yaml +41 -0
- package/skills/shared/shopify/references/apps-graphql.md +103 -0
- package/skills/shared/shopify/references/checkout-extensibility.md +71 -0
- package/skills/shared/shopify/references/liquid-themes.md +89 -0
- package/skills/shared/shopify/scripts/verify.sh +120 -0
- package/skills/shared/simplify-code/SKILL.md +101 -0
- package/skills/shared/simplify-code/evals/README.md +3 -0
- package/skills/shared/simplify-code/evals/cases.yaml +42 -0
- package/skills/shared/skill-scout/SKILL.md +169 -0
- package/skills/shared/skill-scout/evals/README.md +12 -0
- package/skills/shared/skill-scout/evals/cases.yaml +56 -0
- package/skills/shared/skill-scout/references/install-commands.md +76 -0
- package/skills/shared/skill-scout/scripts/verify.sh +154 -0
- package/skills/shared/solid-js/SKILL.md +260 -0
- package/skills/shared/solid-js/evals/README.md +3 -0
- package/skills/shared/solid-js/evals/cases.yaml +38 -0
- package/skills/shared/solid-js/references/reactivity-deep-dive.md +89 -0
- package/skills/shared/solid-js/references/router-and-start.md +93 -0
- package/skills/shared/solid-js/scripts/verify.sh +130 -0
- package/skills/shared/source-grounded-development/SKILL.md +108 -0
- package/skills/shared/source-grounded-development/evals/README.md +3 -0
- package/skills/shared/source-grounded-development/evals/cases.yaml +42 -0
- package/skills/shared/specify/SKILL.md +391 -0
- package/skills/shared/specify/evals/README.md +73 -0
- package/skills/shared/specify/evals/cases.yaml +93 -0
- package/skills/shared/specify/references/eliciting-requirements.md +117 -0
- package/skills/shared/specify/references/spec-template.md +101 -0
- package/skills/shared/speckit-analyze/SKILL.md +35 -0
- package/skills/shared/speckit-assess-decide/SKILL.md +36 -0
- package/skills/shared/speckit-assess-define/SKILL.md +36 -0
- package/skills/shared/speckit-assess-intake/SKILL.md +38 -0
- package/skills/shared/speckit-assess-research/SKILL.md +36 -0
- package/skills/shared/speckit-assess-shape/SKILL.md +36 -0
- package/skills/shared/speckit-bug-assess/SKILL.md +38 -0
- package/skills/shared/speckit-bug-fix/SKILL.md +37 -0
- package/skills/shared/speckit-bug-test/SKILL.md +37 -0
- package/skills/shared/speckit-checklist/SKILL.md +35 -0
- package/skills/shared/speckit-clarify/SKILL.md +32 -0
- package/skills/shared/speckit-constitution/SKILL.md +35 -0
- package/skills/shared/speckit-converge/SKILL.md +36 -0
- package/skills/shared/speckit-implement/SKILL.md +32 -0
- package/skills/shared/speckit-plan/SKILL.md +37 -0
- package/skills/shared/speckit-specify/SKILL.md +36 -0
- package/skills/shared/speckit-tasks/SKILL.md +34 -0
- package/skills/shared/spreadsheet-ops/SKILL.md +180 -0
- package/skills/shared/spreadsheet-ops/evals/README.md +33 -0
- package/skills/shared/spreadsheet-ops/evals/cases.yaml +42 -0
- package/skills/shared/spreadsheet-ops/references/formula-cookbook.md +70 -0
- package/skills/shared/spreadsheet-ops/references/python-excel.md +87 -0
- package/skills/shared/spreadsheet-ops/references/sheets-api-appsscript.md +118 -0
- package/skills/shared/spreadsheet-ops/scripts/verify.sh +152 -0
- package/skills/shared/spring-boot/SKILL.md +350 -0
- package/skills/shared/spring-boot/evals/README.md +11 -0
- package/skills/shared/spring-boot/evals/cases.yaml +49 -0
- package/skills/shared/spring-boot/references/jpa.md +94 -0
- package/skills/shared/spring-boot/references/security.md +92 -0
- package/skills/shared/spring-boot/references/testing.md +95 -0
- package/skills/shared/spring-boot/scripts/verify.sh +115 -0
- package/skills/shared/sql/SKILL.md +286 -0
- package/skills/shared/sql/evals/README.md +9 -0
- package/skills/shared/sql/evals/cases.yaml +49 -0
- package/skills/shared/sql/references/ctes-and-recursion.md +63 -0
- package/skills/shared/sql/references/joins-and-sets.md +71 -0
- package/skills/shared/sql/references/portability.md +38 -0
- package/skills/shared/sql/references/window-functions.md +72 -0
- package/skills/shared/sql/scripts/verify.sh +139 -0
- package/skills/shared/sqlite-turso/SKILL.md +214 -0
- package/skills/shared/sqlite-turso/evals/README.md +24 -0
- package/skills/shared/sqlite-turso/evals/cases.yaml +45 -0
- package/skills/shared/sqlite-turso/references/embedded-replicas.md +96 -0
- package/skills/shared/sqlite-turso/scripts/verify.sh +95 -0
- package/skills/shared/structured-extraction/SKILL.md +230 -0
- package/skills/shared/structured-extraction/evals/README.md +13 -0
- package/skills/shared/structured-extraction/evals/cases.yaml +70 -0
- package/skills/shared/structured-extraction/references/providers.md +152 -0
- package/skills/shared/structured-extraction/scripts/verify.sh +160 -0
- package/skills/shared/suggest/SKILL.md +123 -0
- package/skills/shared/suggest/evals/README.md +17 -0
- package/skills/shared/suggest/evals/cases.yaml +78 -0
- package/skills/shared/supabase/SKILL.md +268 -0
- package/skills/shared/supabase/evals/README.md +12 -0
- package/skills/shared/supabase/evals/cases.yaml +42 -0
- package/skills/shared/supabase/references/auth-ssr.md +173 -0
- package/skills/shared/supabase/references/rls-cookbook.md +122 -0
- package/skills/shared/supabase/scripts/verify.sh +149 -0
- package/skills/shared/svelte/SKILL.md +238 -0
- package/skills/shared/svelte/evals/README.md +3 -0
- package/skills/shared/svelte/evals/cases.yaml +41 -0
- package/skills/shared/svelte/references/runes.md +97 -0
- package/skills/shared/svelte/references/sveltekit-data.md +156 -0
- package/skills/shared/svelte/scripts/verify.sh +128 -0
- package/skills/shared/swift-ios/SKILL.md +224 -0
- package/skills/shared/swift-ios/evals/README.md +3 -0
- package/skills/shared/swift-ios/evals/cases.yaml +46 -0
- package/skills/shared/swift-ios/references/concurrency.md +132 -0
- package/skills/shared/swift-ios/references/testing.md +112 -0
- package/skills/shared/swift-ios/scripts/verify.sh +98 -0
- package/skills/shared/tasks/SKILL.md +265 -0
- package/skills/shared/tasks/evals/README.md +70 -0
- package/skills/shared/tasks/evals/cases.yaml +75 -0
- package/skills/shared/tauri/SKILL.md +219 -0
- package/skills/shared/tauri/evals/README.md +12 -0
- package/skills/shared/tauri/evals/cases.yaml +46 -0
- package/skills/shared/tauri/references/bundling-distribution.md +129 -0
- package/skills/shared/tauri/references/security.md +143 -0
- package/skills/shared/tauri/scripts/verify.sh +178 -0
- package/skills/shared/technical-writing/SKILL.md +230 -0
- package/skills/shared/technical-writing/evals/README.md +12 -0
- package/skills/shared/technical-writing/evals/cases.yaml +53 -0
- package/skills/shared/technical-writing/references/diataxis-modes.md +131 -0
- package/skills/shared/technical-writing/references/vale-starter.md +90 -0
- package/skills/shared/technical-writing/scripts/verify.sh +83 -0
- package/skills/shared/testing-go/SKILL.md +263 -0
- package/skills/shared/testing-go/evals/README.md +3 -0
- package/skills/shared/testing-go/evals/cases.yaml +46 -0
- package/skills/shared/testing-go/references/coverage-and-benchmarks.md +85 -0
- package/skills/shared/testing-go/references/mocks-and-fakes.md +140 -0
- package/skills/shared/testing-go/references/synctest-and-concurrency.md +82 -0
- package/skills/shared/testing-go/scripts/verify.sh +72 -0
- package/skills/shared/testing-py/SKILL.md +197 -0
- package/skills/shared/testing-py/evals/README.md +5 -0
- package/skills/shared/testing-py/evals/cases.yaml +44 -0
- package/skills/shared/testing-py/references/mocking.md +141 -0
- package/skills/shared/testing-py/references/property-testing.md +99 -0
- package/skills/shared/testing-py/scripts/verify.sh +117 -0
- package/skills/shared/testing-web/SKILL.md +244 -0
- package/skills/shared/testing-web/evals/README.md +11 -0
- package/skills/shared/testing-web/evals/cases.yaml +52 -0
- package/skills/shared/testing-web/references/jest-setup.md +88 -0
- package/skills/shared/testing-web/references/recipes.md +116 -0
- package/skills/shared/testing-web/scripts/verify.sh +111 -0
- package/skills/shared/together-fireworks/SKILL.md +176 -0
- package/skills/shared/together-fireworks/evals/README.md +3 -0
- package/skills/shared/together-fireworks/evals/cases.yaml +50 -0
- package/skills/shared/together-fireworks/references/batch-and-tuning.md +59 -0
- package/skills/shared/together-fireworks/references/models-and-pricing.md +79 -0
- package/skills/shared/together-fireworks/scripts/verify.sh +165 -0
- package/skills/shared/training-data/SKILL.md +226 -0
- package/skills/shared/training-data/evals/README.md +13 -0
- package/skills/shared/training-data/evals/cases.yaml +41 -0
- package/skills/shared/training-data/references/formats.md +118 -0
- package/skills/shared/training-data/references/synthesis-dedup-quality.md +134 -0
- package/skills/shared/translation-l10n/SKILL.md +229 -0
- package/skills/shared/translation-l10n/evals/README.md +3 -0
- package/skills/shared/translation-l10n/evals/cases.yaml +39 -0
- package/skills/shared/translation-l10n/references/icu-cookbook.md +82 -0
- package/skills/shared/translation-l10n/references/rtl-and-bidi.md +60 -0
- package/skills/shared/typescript/SKILL.md +225 -0
- package/skills/shared/typescript/evals/README.md +15 -0
- package/skills/shared/typescript/evals/cases.yaml +46 -0
- package/skills/shared/typescript/references/build-and-monorepo.md +141 -0
- package/skills/shared/typescript/references/type-system.md +162 -0
- package/skills/shared/typescript/scripts/verify.sh +52 -0
- package/skills/shared/ui-engineering/SKILL.md +113 -0
- package/skills/shared/ui-engineering/evals/cases.yaml +38 -0
- package/skills/shared/unity/SKILL.md +282 -0
- package/skills/shared/unity/evals/README.md +8 -0
- package/skills/shared/unity/evals/cases.yaml +43 -0
- package/skills/shared/unity/references/build-and-platforms.md +65 -0
- package/skills/shared/unity/references/input-system.md +102 -0
- package/skills/shared/unity/references/lifecycle-deep-dive.md +75 -0
- package/skills/shared/unreal/SKILL.md +270 -0
- package/skills/shared/unreal/evals/README.md +12 -0
- package/skills/shared/unreal/evals/cases.yaml +45 -0
- package/skills/shared/unreal/references/blueprint-cpp-parity.md +109 -0
- package/skills/shared/unreal/references/gameplay-framework.md +95 -0
- package/skills/shared/unreal/references/packaging.md +83 -0
- package/skills/shared/unslop/SKILL.md +143 -0
- package/skills/shared/unslop/evals/README.md +25 -0
- package/skills/shared/unslop/evals/cases.yaml +61 -0
- package/skills/shared/unsloth/SKILL.md +254 -0
- package/skills/shared/unsloth/evals/README.md +10 -0
- package/skills/shared/unsloth/evals/cases.yaml +42 -0
- package/skills/shared/unsloth/references/export.md +75 -0
- package/skills/shared/unsloth/references/grpo.md +91 -0
- package/skills/shared/unsloth/references/masking-and-templates.md +91 -0
- package/skills/shared/variant-explorer/SKILL.md +93 -0
- package/skills/shared/variant-explorer/evals/cases.yaml +38 -0
- package/skills/shared/vector-db/SKILL.md +189 -0
- package/skills/shared/vector-db/evals/README.md +10 -0
- package/skills/shared/vector-db/evals/cases.yaml +45 -0
- package/skills/shared/vector-db/references/engines.md +175 -0
- package/skills/shared/vector-db/references/tuning.md +62 -0
- package/skills/shared/vector-db/scripts/verify.sh +110 -0
- package/skills/shared/vercel/SKILL.md +242 -0
- package/skills/shared/vercel/evals/README.md +23 -0
- package/skills/shared/vercel/evals/cases.yaml +45 -0
- package/skills/shared/vercel/references/cli-cookbook.md +98 -0
- package/skills/shared/vercel/references/vercel-json.md +120 -0
- package/skills/shared/vercel/scripts/verify.sh +168 -0
- package/skills/shared/verify/SKILL.md +230 -0
- package/skills/shared/verify/evals/README.md +78 -0
- package/skills/shared/verify/evals/cases.yaml +104 -0
- package/skills/shared/vllm/SKILL.md +235 -0
- package/skills/shared/vllm/evals/README.md +9 -0
- package/skills/shared/vllm/evals/cases.yaml +47 -0
- package/skills/shared/vllm/references/flags-and-endpoints.md +105 -0
- package/skills/shared/vllm/references/memory-and-throughput.md +78 -0
- package/skills/shared/vue-nuxt/SKILL.md +323 -0
- package/skills/shared/vue-nuxt/evals/README.md +11 -0
- package/skills/shared/vue-nuxt/evals/cases.yaml +49 -0
- package/skills/shared/vue-nuxt/references/data-and-state.md +127 -0
- package/skills/shared/vue-nuxt/references/migration-nuxt4.md +79 -0
- package/skills/shared/vue-nuxt/references/nitro-and-rendering.md +117 -0
- package/skills/shared/vue-nuxt/references/reactivity.md +135 -0
- package/skills/shared/vue-nuxt/scripts/verify.sh +148 -0
- package/skills/shared/webhooks/SKILL.md +229 -0
- package/skills/shared/webhooks/evals/README.md +15 -0
- package/skills/shared/webhooks/evals/cases.yaml +46 -0
- package/skills/shared/webhooks/references/framework-raw-body.md +97 -0
- package/skills/shared/webhooks/references/signature-schemes.md +66 -0
- package/skills/shared/webhooks/scripts/verify.sh +142 -0
- package/skills/shared/wordpress/SKILL.md +224 -0
- package/skills/shared/wordpress/evals/README.md +3 -0
- package/skills/shared/wordpress/evals/cases.yaml +50 -0
- package/skills/shared/wordpress/references/hardening.md +108 -0
- package/skills/shared/wordpress/references/performance.md +80 -0
- package/skills/shared/wordpress/references/woocommerce.md +65 -0
- package/skills/shared/wordpress/scripts/verify.sh +96 -0
- package/skills/shared/worktrees/SKILL.md +204 -0
- package/skills/shared/worktrees/evals/README.md +78 -0
- package/skills/shared/worktrees/evals/cases.yaml +47 -0
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
// probe() runs INSIDE the page (it is serialised and evaluated through cdp.mjs), so it must stay
|
|
2
|
+
// self-contained: no imports, no closures over module scope.
|
|
3
|
+
// It measures what a design reviewer looks at — headline line counts, the type system, colour,
|
|
4
|
+
// surfaces, decoration — and the specific tells this skill bans.
|
|
5
|
+
export function probe() {
|
|
6
|
+
const W = innerWidth, H = innerHeight, SY = scrollY;
|
|
7
|
+
const round = (n, d = 2) => Math.round(n * 10 ** d) / 10 ** d;
|
|
8
|
+
|
|
9
|
+
// --- colour normalisation (handles oklch(), color(), named colours, etc.)
|
|
10
|
+
const cvs = document.createElement('canvas'); cvs.width = cvs.height = 1;
|
|
11
|
+
const ctx = cvs.getContext('2d', { willReadFrequently: true });
|
|
12
|
+
const colorCache = new Map();
|
|
13
|
+
const rgba = (c) => {
|
|
14
|
+
if (colorCache.has(c)) return colorCache.get(c);
|
|
15
|
+
ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = 'rgba(0,0,0,0)'; ctx.fillStyle = c; ctx.fillRect(0, 0, 1, 1);
|
|
16
|
+
const d = ctx.getImageData(0, 0, 1, 1).data;
|
|
17
|
+
const v = { r: d[0], g: d[1], b: d[2], a: round(d[3] / 255) };
|
|
18
|
+
colorCache.set(c, v); return v;
|
|
19
|
+
};
|
|
20
|
+
const hex = (v) => '#' + [v.r, v.g, v.b].map((n) => n.toString(16).padStart(2, '0')).join('');
|
|
21
|
+
const lum = (v) => {
|
|
22
|
+
const f = (x) => { x /= 255; return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; };
|
|
23
|
+
return 0.2126 * f(v.r) + 0.7152 * f(v.g) + 0.0722 * f(v.b);
|
|
24
|
+
};
|
|
25
|
+
const contrast = (a, b) => { const l1 = lum(a), l2 = lum(b); return round((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05), 1); };
|
|
26
|
+
const hsl = (v) => {
|
|
27
|
+
const r = v.r / 255, g = v.g / 255, b = v.b / 255, mx = Math.max(r, g, b), mn = Math.min(r, g, b);
|
|
28
|
+
const l = (mx + mn) / 2; let h = 0, s = 0;
|
|
29
|
+
if (mx !== mn) {
|
|
30
|
+
const d = mx - mn; s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
|
|
31
|
+
h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4; h *= 60;
|
|
32
|
+
}
|
|
33
|
+
return { h: Math.round(h), s: round(s), l: round(l) };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// --- monospace detection by measurement, not by font name
|
|
37
|
+
const mctx = document.createElement('canvas').getContext('2d');
|
|
38
|
+
const monoCache = new Map();
|
|
39
|
+
const isMono = (family) => {
|
|
40
|
+
if (monoCache.has(family)) return monoCache.get(family);
|
|
41
|
+
mctx.font = `40px ${family}`;
|
|
42
|
+
const a = mctx.measureText('iiiiiiii').width, b = mctx.measureText('WWWWWWWW').width;
|
|
43
|
+
const v = Math.abs(a - b) < 1;
|
|
44
|
+
monoCache.set(family, v); return v;
|
|
45
|
+
};
|
|
46
|
+
const firstFam = (ff) => ff.split(',')[0].replace(/["']/g, '').trim();
|
|
47
|
+
// Text kept for screen readers only (the usual .sr-only / .visually-hidden patterns) is not on the page.
|
|
48
|
+
const srHidden = (el) => {
|
|
49
|
+
for (let p = el, i = 0; p && p !== document.body && i < 4; p = p.parentElement, i++) {
|
|
50
|
+
const cs = getComputedStyle(p); const r = p.getBoundingClientRect();
|
|
51
|
+
if (cs.clip === 'rect(0px, 0px, 0px, 0px)' || /inset\(50%\)/.test(cs.clipPath) || (r.width <= 2 && r.height <= 2 && cs.overflow !== 'visible')) return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
};
|
|
55
|
+
const GENERIC = /^(serif|sans-serif|monospace|system-ui|ui-sans-serif|ui-serif|ui-monospace|ui-rounded|cursive|fantasy|inherit|-apple-system|BlinkMacSystemFont|Segoe UI|Arial|Helvetica|Helvetica Neue|Times New Roman|Times|Georgia|Verdana|Tahoma|Courier New|Courier|Roboto|Apple Color Emoji)$/i;
|
|
56
|
+
|
|
57
|
+
// --- collect elements
|
|
58
|
+
const all = [...document.body.querySelectorAll('*')].slice(0, 9000);
|
|
59
|
+
const texts = [];
|
|
60
|
+
const big = [];
|
|
61
|
+
for (const el of all) {
|
|
62
|
+
const tag = el.tagName;
|
|
63
|
+
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' || tag === 'TEMPLATE') continue;
|
|
64
|
+
const r = el.getBoundingClientRect();
|
|
65
|
+
if (r.width < 1 || r.height < 1) continue;
|
|
66
|
+
const cs = getComputedStyle(el);
|
|
67
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') continue;
|
|
68
|
+
const area = r.width * r.height;
|
|
69
|
+
if (area >= 60 * 30) big.push({ el, r, cs, area, top: r.top + SY });
|
|
70
|
+
let direct = '';
|
|
71
|
+
for (const n of el.childNodes) if (n.nodeType === 3) direct += n.nodeValue;
|
|
72
|
+
direct = direct.replace(/\s+/g, ' ').trim();
|
|
73
|
+
if (!direct) continue;
|
|
74
|
+
if (tag === 'OPTION' || el.closest('svg') || srHidden(el)) continue;
|
|
75
|
+
const size = parseFloat(cs.fontSize);
|
|
76
|
+
if (!size || size < 6) continue;
|
|
77
|
+
const ls = cs.letterSpacing === 'normal' ? 0 : parseFloat(cs.letterSpacing) / size;
|
|
78
|
+
const lh = cs.lineHeight === 'normal' ? 1.2 : parseFloat(cs.lineHeight) / size;
|
|
79
|
+
texts.push({
|
|
80
|
+
el, tag, text: direct, chars: direct.length, fam: firstFam(cs.fontFamily), ff: cs.fontFamily,
|
|
81
|
+
size: round(size, 1), weight: +cs.fontWeight || 400, ls: round(ls, 3), lh: round(lh, 2),
|
|
82
|
+
upper: cs.textTransform === 'uppercase' || (direct.length > 2 && direct === direct.toUpperCase() && /[A-Z]{2}/.test(direct)),
|
|
83
|
+
italic: cs.fontStyle === 'italic', align: cs.textAlign, color: cs.color,
|
|
84
|
+
top: r.top + SY, bottom: r.bottom + SY, left: r.left, right: r.right, w: r.width, h: r.height,
|
|
85
|
+
clipText: (cs.webkitBackgroundClip || cs.backgroundClip || '').includes('text'),
|
|
86
|
+
inCode: !!el.closest('pre, code, kbd, samp, textarea'), inUI: !!el.closest('[data-ui]'), textShadow: cs.textShadow !== 'none',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- heading blocks: climb from a large text node to the block that holds the whole headline
|
|
91
|
+
// An element that is a complete text unit by itself: separate links, list items or paragraphs set large
|
|
92
|
+
// are separate lines of content, never the wrapped lines of one headline.
|
|
93
|
+
const UNIT = 'h1, h2, h3, h4, h5, h6, [role="heading"], p, li, a, button, blockquote, dt, dd, figcaption, td, th, label, summary';
|
|
94
|
+
const blockOf = (t) => {
|
|
95
|
+
let cur = t.el;
|
|
96
|
+
// usable as the block when it holds only this size of type (a title with its own small description is not one block)
|
|
97
|
+
const clean = (el) => (el.innerText || '').trim().length <= 320 && !texts.some((o) => o.el !== t.el && el.contains(o.el) && o.size < t.size * 0.72);
|
|
98
|
+
let unit = t.el.closest(UNIT);
|
|
99
|
+
if (unit && unit.matches('a, button')) { const outer = unit.parentElement?.closest('h1, h2, h3, h4, h5, h6, p, blockquote, li'); if (outer && clean(outer)) unit = outer; }
|
|
100
|
+
if (unit && clean(unit)) return unit;
|
|
101
|
+
while (cur.parentElement && cur.parentElement !== document.body) {
|
|
102
|
+
if (cur.matches(UNIT)) break;
|
|
103
|
+
const p = cur.parentElement;
|
|
104
|
+
if (p.matches('ul, ol, dl, nav, menu, table, tbody')) break;
|
|
105
|
+
const inner = (p.innerText || '').trim();
|
|
106
|
+
if (inner.length > 220) break;
|
|
107
|
+
let ok = true;
|
|
108
|
+
for (const o of texts) if (o.el !== t.el && p.contains(o.el) && o.size < t.size * 0.72) { ok = false; break; }
|
|
109
|
+
if (!ok) break;
|
|
110
|
+
cur = p;
|
|
111
|
+
}
|
|
112
|
+
return cur;
|
|
113
|
+
};
|
|
114
|
+
// Count lines from the text itself: inline images or media set inside a sentence are not lines.
|
|
115
|
+
const linesOf = (el, size) => {
|
|
116
|
+
const tops = [];
|
|
117
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
118
|
+
const range = document.createRange();
|
|
119
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
120
|
+
if (!node.nodeValue.trim()) continue;
|
|
121
|
+
range.selectNodeContents(node);
|
|
122
|
+
for (const rc of range.getClientRects()) {
|
|
123
|
+
if (rc.width < 1 || rc.height < size * 0.35) continue;
|
|
124
|
+
const mid = rc.top + rc.height / 2;
|
|
125
|
+
if (!tops.some((t) => Math.abs(t - mid) < size * 0.55)) tops.push(mid);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return Math.max(1, tops.length);
|
|
129
|
+
};
|
|
130
|
+
const describeBlock = (el, t) => {
|
|
131
|
+
const r = el.getBoundingClientRect();
|
|
132
|
+
const text = (el.innerText || t.text).replace(/\s+/g, ' ').trim();
|
|
133
|
+
const cx = (r.left + r.right) / 2 / W;
|
|
134
|
+
const fams = new Set(texts.filter((o) => el.contains(o.el)).map((o) => o.fam + (o.italic ? ' italic' : '')));
|
|
135
|
+
return {
|
|
136
|
+
text: text.slice(0, 200), words: text.split(' ').filter(Boolean).length, chars: text.length,
|
|
137
|
+
lines: linesOf(el, t.size), size: t.size, vw: round((t.size / W) * 100, 1), font: t.fam, weight: t.weight,
|
|
138
|
+
tracking: t.ls, leading: t.lh, upper: t.upper, italic: t.italic, mono: isMono(t.ff),
|
|
139
|
+
align: /center/.test(t.align) ? 'center' : /right|end/.test(t.align) ? 'right' : 'left',
|
|
140
|
+
position: cx < 0.42 ? 'left' : cx > 0.58 ? 'right' : 'center',
|
|
141
|
+
widthRatio: round(r.width / W), top: Math.round(r.top + SY), mixedFonts: fams.size > 1 ? [...fams] : undefined,
|
|
142
|
+
gradientText: texts.some((o) => el.contains(o.el) && o.clipText) || undefined, tag: el.tagName.toLowerCase(),
|
|
143
|
+
// a title (h1–h6, or unmarked large text) or a statement (a paragraph, quotation or list item set large)
|
|
144
|
+
kind: el.closest('h1,h2,h3,h4,h5,h6,[role="heading"]') || el.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]') ? 'heading'
|
|
145
|
+
: t.el.closest('p, blockquote, li, dd, q, figcaption') || text.length > 150 ? 'statement' : 'heading',
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
const headingMin = W < 600 ? 22 : Math.max(26, W * 0.019);
|
|
149
|
+
const seenBlocks = new Set();
|
|
150
|
+
const headings = [];
|
|
151
|
+
for (const t of texts.filter((x) => x.size >= headingMin && !x.inCode).sort((a, b) => b.size - a.size)) {
|
|
152
|
+
const blk = blockOf(t);
|
|
153
|
+
if (seenBlocks.has(blk) || [...seenBlocks].some((b) => b.contains(blk) || blk.contains(b))) continue;
|
|
154
|
+
seenBlocks.add(blk);
|
|
155
|
+
const d = describeBlock(blk, t);
|
|
156
|
+
if (d.kind === 'statement' && d.chars > 150 && d.size <= 30) continue; // a lead paragraph, not display type
|
|
157
|
+
headings.push({ ...d, _el: blk });
|
|
158
|
+
}
|
|
159
|
+
headings.sort((a, b) => a.top - b.top);
|
|
160
|
+
|
|
161
|
+
// hero headline = largest heading block that starts inside the first viewport
|
|
162
|
+
const inHero = headings.filter((h) => h.top < H * 0.95);
|
|
163
|
+
let hero = inHero.slice().sort((a, b) => b.size - a.size || b.chars - a.chars)[0] || null;
|
|
164
|
+
if (hero) hero.kind = 'heading'; // the main line of the first screen is a headline whatever its tag
|
|
165
|
+
const h1el = document.querySelector('h1');
|
|
166
|
+
if (!hero) {
|
|
167
|
+
// quiet pages: fall back to the h1, then to the largest text in the first viewport
|
|
168
|
+
const h1r = h1el && h1el.getBoundingClientRect();
|
|
169
|
+
const pick = (h1r && h1r.top < H * 0.95 && h1r.width > 0 && texts.find((t) => h1el.contains(t.el))) ||
|
|
170
|
+
texts.filter((t) => t.top - SY < H * 0.95 && t.size >= 18 && t.chars >= 8).sort((a, b) => b.size - a.size)[0];
|
|
171
|
+
if (pick) { const blk = h1el && h1el.contains(pick.el) ? h1el : blockOf(pick); hero = { ...describeBlock(blk, pick), _el: blk }; }
|
|
172
|
+
}
|
|
173
|
+
let h1 = null;
|
|
174
|
+
if (h1el && h1el.getBoundingClientRect().width > 0) {
|
|
175
|
+
const cs = getComputedStyle(h1el); const size = parseFloat(cs.fontSize);
|
|
176
|
+
const text = (h1el.innerText || '').replace(/\s+/g, ' ').trim();
|
|
177
|
+
h1 = { text: text.slice(0, 200), words: text.split(' ').filter(Boolean).length, lines: linesOf(h1el, size), size: round(size, 1), font: firstFam(cs.fontFamily) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- eyebrows: a small short label sitting directly above a heading block
|
|
181
|
+
// Sticky and fixed layers overlap other content by design; their geometry says nothing about what labels what.
|
|
182
|
+
const pinned = (el) => { for (let p = el; p && p !== document.body; p = p.parentElement) { const pos = getComputedStyle(p).position; if (pos === 'fixed' || pos === 'sticky') return true; } return false; };
|
|
183
|
+
const eyebrows = [];
|
|
184
|
+
for (const h of headings) {
|
|
185
|
+
const hr = h._el.getBoundingClientRect(); const htop = hr.top + SY;
|
|
186
|
+
if (h._el.closest('[data-ui]')) continue;
|
|
187
|
+
const cand = texts.filter((t) => !t.inUI && !h._el.contains(t.el) && t.size <= Math.min(18, h.size * 0.5) && t.chars <= 44 && t.chars >= 2 &&
|
|
188
|
+
t.bottom <= htop + 4 && htop - t.bottom <= Math.min(140, Math.max(72, h.size * 1.1)) && t.right > hr.left - 8 && t.left < hr.right + 8 &&
|
|
189
|
+
!t.el.closest('button') &&
|
|
190
|
+
!t.el.closest('nav, [role="navigation"], footer') && !(t.top < 120 && t.el.closest('header')) && !pinned(t.el) && pinned(h._el) === false)
|
|
191
|
+
.sort((a, b) => b.bottom - a.bottom)[0];
|
|
192
|
+
if (!cand) continue;
|
|
193
|
+
// must be alone on its row (a real paragraph above a heading is not an eyebrow)
|
|
194
|
+
if (texts.some((t) => t !== cand && Math.abs(t.top - cand.top) < 6 && t.chars > 44)) continue;
|
|
195
|
+
// a note that sits closer to the heading above it than to this one belongs to that heading
|
|
196
|
+
const gapDown = htop - cand.bottom;
|
|
197
|
+
if (headings.some((o) => o !== h && !o._el.contains(cand.el) && (() => { const or = o._el.getBoundingClientRect(); const gapUp = cand.top - (or.bottom + SY); return gapUp >= -2 && gapUp < gapDown && or.left < cand.right && or.right > cand.left; })())) continue;
|
|
198
|
+
let pill = false; let p = cand.el;
|
|
199
|
+
for (let i = 0; i < 3 && p; i++, p = p.parentElement) {
|
|
200
|
+
const cs = getComputedStyle(p); const pr = p.getBoundingClientRect();
|
|
201
|
+
if (pr.width < 420 && pr.height < 56 && parseFloat(cs.borderTopLeftRadius) >= 10 && (rgba(cs.backgroundColor).a > 0.04 || parseFloat(cs.borderTopWidth) > 0)) pill = true;
|
|
202
|
+
}
|
|
203
|
+
eyebrows.push({ text: cand.text.slice(0, 50), above: h.text.slice(0, 40), upper: cand.upper, tracked: cand.ls >= 0.04, mono: isMono(cand.ff), pill, size: cand.size, top: Math.round(cand.top) });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- decorative numbering: 01 / 02 / (03) / 04. / 001 / "Step 02"
|
|
207
|
+
const numbered = texts.filter((t) => !t.inCode && !t.inUI && (/^[(\[\/#]?\s?0\d{1,2}\s?[)\]\/.:—–-]?$/.test(t.text) || /^0\d\s?[\/.—–-]\s/.test(t.text) || /^(step|phase|no\.?|n°|nº|chapter|part|fig\.?)\s?0\d{1,2}\b/i.test(t.text)));
|
|
208
|
+
|
|
209
|
+
// --- type system
|
|
210
|
+
const famChars = new Map(); const sizeChars = new Map(); let totalChars = 0; let monoChars = 0; let monoProse = 0; const monoFams = new Set(); const monoSamples = [];
|
|
211
|
+
let microLabels = 0; let microLabelChars = 0; let upperChars = 0; let centerChars = 0; const microSamples = [];
|
|
212
|
+
for (const t of texts) {
|
|
213
|
+
totalChars += t.chars;
|
|
214
|
+
famChars.set(t.fam, (famChars.get(t.fam) || 0) + t.chars);
|
|
215
|
+
if (!t.inUI) { const s = Math.round(t.size); sizeChars.set(s, (sizeChars.get(s) || 0) + t.chars); } // interface fragments keep their own sizes
|
|
216
|
+
if (isMono(t.ff)) {
|
|
217
|
+
monoChars += t.chars; monoFams.add(t.fam);
|
|
218
|
+
if (!t.inCode) { monoProse += t.chars; if (monoSamples.length < 6) monoSamples.push(t.text.slice(0, 40)); }
|
|
219
|
+
}
|
|
220
|
+
if (t.size <= 14 && t.upper && t.ls >= 0.05 && !t.inUI && !t.el.closest('nav, button, a[href]')) { microLabels++; microLabelChars += t.chars; if (microSamples.length < 6) microSamples.push(t.text.slice(0, 40)); }
|
|
221
|
+
if (t.upper) upperChars += t.chars;
|
|
222
|
+
if (/center/.test(t.align)) centerChars += t.chars;
|
|
223
|
+
}
|
|
224
|
+
const families = [...famChars.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6).map(([family, chars]) => ({ family, share: round(chars / totalChars), mono: isMono(`"${family}", serif`) && isMono(`"${family}", sans-serif`) }));
|
|
225
|
+
const bodyTexts = texts.filter((t) => t.size >= 10 && t.size <= 24 && t.chars >= 60 && !t.inCode && !t.inUI);
|
|
226
|
+
const bodyBy = new Map();
|
|
227
|
+
for (const t of bodyTexts) { const k = t.fam + '|' + Math.round(t.size); bodyBy.set(k, (bodyBy.get(k) || 0) + t.chars); }
|
|
228
|
+
const bodyTop = [...bodyBy.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
229
|
+
const bodySample = bodyTop ? bodyTexts.find((t) => t.fam + '|' + Math.round(t.size) === bodyTop[0]) : null;
|
|
230
|
+
const longest = bodyTexts.slice().sort((a, b) => b.chars - a.chars)[0];
|
|
231
|
+
const sizes = [...sizeChars.entries()].sort((a, b) => a[0] - b[0]).map(([s]) => s);
|
|
232
|
+
const maxText = texts.slice().sort((a, b) => b.size - a.size)[0];
|
|
233
|
+
const smallText = texts.filter((t) => t.size < 13 && t.chars >= 12 && !t.inUI && !t.inCode).length; // interface fragments keep their true sizes
|
|
234
|
+
const loaded = new Set([...document.fonts].filter((f) => f.status === 'loaded').map((f) => f.family.replace(/["']/g, '').toLowerCase()));
|
|
235
|
+
const declared = new Set([...document.fonts].map((f) => f.family.replace(/["']/g, '').toLowerCase()));
|
|
236
|
+
const notLoaded = families.map((f) => f.family).filter((f) => !GENERIC.test(f) && !loaded.has(f.toLowerCase()) && (declared.has(f.toLowerCase()) || declared.size > 0));
|
|
237
|
+
|
|
238
|
+
// --- colours
|
|
239
|
+
const bodyBg = (() => {
|
|
240
|
+
for (const el of [document.body, document.documentElement]) { const v = rgba(getComputedStyle(el).backgroundColor); if (v.a > 0.5) return v; }
|
|
241
|
+
return { r: 255, g: 255, b: 255, a: 1 };
|
|
242
|
+
})();
|
|
243
|
+
const sectionBgs = [];
|
|
244
|
+
for (const b of big) {
|
|
245
|
+
if (b.area < W * H * 0.3) continue;
|
|
246
|
+
const v = rgba(b.cs.backgroundColor); if (v.a < 0.9) continue;
|
|
247
|
+
const hx = hex(v); if (!sectionBgs.some((s) => s.color === hx)) sectionBgs.push({ color: hx, top: Math.round(b.top), hsl: hsl(v) });
|
|
248
|
+
}
|
|
249
|
+
const textColorChars = new Map();
|
|
250
|
+
for (const t of texts) { const v = rgba(t.color); if (v.a < 0.3) continue; const hx = hex(v); textColorChars.set(hx, (textColorChars.get(hx) || 0) + t.chars); }
|
|
251
|
+
const textColors = [...textColorChars.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([color, c]) => ({ color, share: round(c / totalChars) }));
|
|
252
|
+
|
|
253
|
+
// effective background behind an element (null when an image or gradient sits behind it)
|
|
254
|
+
const bgBehind = (el) => {
|
|
255
|
+
for (let p = el; p && p !== document.documentElement; p = p.parentElement) {
|
|
256
|
+
const cs = getComputedStyle(p);
|
|
257
|
+
if (cs.backgroundImage !== 'none' || p.tagName === 'VIDEO' || p.tagName === 'CANVAS') return null;
|
|
258
|
+
const v = rgba(cs.backgroundColor); if (v.a >= 0.9) return v;
|
|
259
|
+
if (cs.position === 'absolute' || cs.position === 'fixed') return null; // may overlap media
|
|
260
|
+
}
|
|
261
|
+
return bodyBg;
|
|
262
|
+
};
|
|
263
|
+
// media that may sit behind text without being its ancestor (an absolutely placed hero image, a video layer)
|
|
264
|
+
const mediaRects = big.filter((b) => b.area >= 120 * 80 && (b.el.tagName === 'IMG' || b.el.tagName === 'VIDEO' || b.el.tagName === 'CANVAS' || b.el.tagName === 'PICTURE' || b.el.tagName === 'IFRAME' || b.el.tagName === 'svg' || b.cs.backgroundImage !== 'none')).map((b) => ({ el: b.el, l: b.r.left, r: b.r.right, t: b.r.top + SY, b: b.r.bottom + SY }));
|
|
265
|
+
const overMedia = (t) => {
|
|
266
|
+
const area = Math.max(1, t.w * t.h);
|
|
267
|
+
for (const m of mediaRects) {
|
|
268
|
+
const ox = Math.min(t.right, m.r) - Math.max(t.left, m.l), oy = Math.min(t.bottom, m.b) - Math.max(t.top, m.t);
|
|
269
|
+
if (ox > 0 && oy > 0 && (ox * oy) / area >= 0.25) return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
const lowContrast = [];
|
|
274
|
+
for (const t of texts) {
|
|
275
|
+
if (t.chars < 4 || t.top - SY > H * 6) continue;
|
|
276
|
+
const fg = rgba(t.color); if (fg.a < 0.99) continue;
|
|
277
|
+
if (overMedia(t)) continue; // cannot know the pixels behind it; judge that from the screenshot
|
|
278
|
+
// text blended with difference or exclusion inverts whatever lies under it; its colour alone says nothing
|
|
279
|
+
let blended = false;
|
|
280
|
+
for (let p = t.el, i = 0; p && p !== document.body && i < 6; p = p.parentElement, i++) if (/difference|exclusion/.test(getComputedStyle(p).mixBlendMode)) { blended = true; break; }
|
|
281
|
+
if (blended) continue;
|
|
282
|
+
const bg = bgBehind(t.el); if (!bg) continue;
|
|
283
|
+
const c = contrast(fg, bg); const need = t.size >= 24 || (t.size >= 18.5 && t.weight >= 700) ? 3 : 4.5;
|
|
284
|
+
if (c < need) lowContrast.push({ text: t.text.slice(0, 40), ratio: c, need, size: t.size, fg: hex(fg), bg: hex(bg) });
|
|
285
|
+
}
|
|
286
|
+
lowContrast.sort((a, b) => a.ratio - b.ratio);
|
|
287
|
+
|
|
288
|
+
// --- buttons
|
|
289
|
+
const buttons = [];
|
|
290
|
+
for (const el of document.querySelectorAll('a, button, [role="button"], input[type="submit"]')) {
|
|
291
|
+
const r = el.getBoundingClientRect(); if (r.height < 28 || r.height > 84 || r.width < 64 || r.width > Math.min(W * 0.96, 900)) continue;
|
|
292
|
+
const cs = getComputedStyle(el); let bg = rgba(cs.backgroundColor); const bw = parseFloat(cs.borderTopWidth) || 0;
|
|
293
|
+
const grad = cs.backgroundImage.includes('gradient(');
|
|
294
|
+
if (grad && bg.a < 0.5) { const first = cs.backgroundImage.match(/(rgba?\([^)]+\)|oklch\([^)]+\)|color\([^)]+\)|#[0-9a-f]{3,8})/i); if (first) bg = rgba(first[1]); }
|
|
295
|
+
if (bg.a < 0.5 && bw < 1) continue;
|
|
296
|
+
const rad = parseFloat(cs.borderTopLeftRadius) || 0;
|
|
297
|
+
buttons.push({
|
|
298
|
+
text: (el.innerText || el.value || '').replace(/\s+/g, ' ').trim().slice(0, 40), filled: bg.a >= 0.5, bg: bg.a >= 0.5 ? hex(bg) : null, bgHsl: bg.a >= 0.5 ? hsl(bg) : null, fg: hex(rgba(cs.color)),
|
|
299
|
+
radius: rad >= r.height / 2 - 1 ? 'pill' : Math.round(rad), h: Math.round(r.height), top: Math.round(r.top + SY),
|
|
300
|
+
shadow: cs.boxShadow !== 'none' && !/^rgba?\(0, 0, 0, 0\)/.test(cs.boxShadow), gradient: grad, upper: cs.textTransform === 'uppercase', mono: isMono(cs.fontFamily),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const mode = (arr) => { const m = new Map(); for (const a of arr) m.set(JSON.stringify(a), (m.get(JSON.stringify(a)) || 0) + 1); const top = [...m.entries()].sort((a, b) => b[1] - a[1])[0]; return top ? JSON.parse(top[0]) : undefined; };
|
|
304
|
+
const filledBtns = buttons.filter((b) => b.filled);
|
|
305
|
+
|
|
306
|
+
// --- surfaces and decoration
|
|
307
|
+
const isOverlay = (el) => { for (let p = el, i = 0; p && i < 4; p = p.parentElement, i++) { const pos = getComputedStyle(p).position; if (pos === 'fixed' || pos === 'sticky') return true; if (p.matches('dialog, [role="dialog"], [role="menu"], [role="tooltip"], [popover]')) return true; } return false; };
|
|
308
|
+
// real tables, form fields and interface fragments (marked data-ui) keep their own lines
|
|
309
|
+
const formish = (el) => el.matches('input, textarea, select, table, thead, tbody, tr, td, th, hr') || !!el.closest('table, [data-ui]');
|
|
310
|
+
let shadowStatic = 0, shadowOverlay = 0; const shadowSamples = []; let gradients = 0; let scrims = 0; const gradientSamples = []; let gradientText = 0;
|
|
311
|
+
let hairlines = 0; const hairlineSamples = []; let outlinedBoxes = 0; let blurred = 0; let roundedBoxes = 0; let squareMedia = 0; let roundedMedia = 0; const mediaRadii = []; const boxRadii = [];
|
|
312
|
+
let dashed = 0;
|
|
313
|
+
const label = (el) => (el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') + (typeof el.className === 'string' && el.className ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '')).slice(0, 60);
|
|
314
|
+
for (const b of big) {
|
|
315
|
+
const { cs, r, el } = b;
|
|
316
|
+
const sh = cs.boxShadow;
|
|
317
|
+
if (sh !== 'none' && !/^rgba?\(0, 0, 0, 0\) 0px 0px 0px 0px$/.test(sh)) {
|
|
318
|
+
const outer = sh.split(/,(?![^(]*\))/).some((part) => !/inset/.test(part) && !/ 0px 0px 0px 0px/.test(part) && !/rgba\(\d+, \d+, \d+, 0\)/.test(part));
|
|
319
|
+
if (outer) { if (isOverlay(el) || el.closest('[data-ui]')) shadowOverlay++; else { shadowStatic++; if (shadowSamples.length < 4) shadowSamples.push(label(el) + ' → ' + sh.slice(0, 80)); } }
|
|
320
|
+
}
|
|
321
|
+
if (cs.backgroundImage.includes('gradient(')) {
|
|
322
|
+
if ((cs.webkitBackgroundClip || cs.backgroundClip || '').includes('text')) gradientText++;
|
|
323
|
+
else if (b.area >= W * H * 0.02) {
|
|
324
|
+
// neutral stops with changing alpha = a legibility scrim over media, not decoration
|
|
325
|
+
const stops = [...cs.backgroundImage.matchAll(/rgba?\(([^)]+)\)/g)].map((mm) => mm[1].split(/[,\s/]+/).filter(Boolean).map(Number));
|
|
326
|
+
const exotic = /oklch|oklab|color\(|lab\(|lch\(|hsl/.test(cs.backgroundImage);
|
|
327
|
+
const scrim = !exotic && stops.length > 0 && stops.every((c) => Math.max(c[0], c[1], c[2]) - Math.min(c[0], c[1], c[2]) <= 14) && stops.some((c) => c.length > 3 && c[3] < 1);
|
|
328
|
+
if (scrim) scrims++; else { gradients++; if (gradientSamples.length < 4) gradientSamples.push(label(el) + ' → ' + cs.backgroundImage.slice(0, 110)); }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if ((cs.backdropFilter && cs.backdropFilter !== 'none') || (cs.webkitBackdropFilter && cs.webkitBackdropFilter !== 'none')) blurred++;
|
|
332
|
+
const bt = parseFloat(cs.borderTopWidth) || 0, bb = parseFloat(cs.borderBottomWidth) || 0, bl = parseFloat(cs.borderLeftWidth) || 0, br = parseFloat(cs.borderRightWidth) || 0;
|
|
333
|
+
const thin = (w, c, style) => w > 0 && w <= 1.5 && style !== 'none' && rgba(c).a > 0.02;
|
|
334
|
+
const topLine = thin(bt, cs.borderTopColor, cs.borderTopStyle), botLine = thin(bb, cs.borderBottomColor, cs.borderBottomStyle);
|
|
335
|
+
const leftLine = thin(bl, cs.borderLeftColor, cs.borderLeftStyle), rightLine = thin(br, cs.borderRightColor, cs.borderRightStyle);
|
|
336
|
+
if (!formish(el)) {
|
|
337
|
+
if (topLine && botLine && leftLine && rightLine) { if (r.width >= 150 && r.height >= 80) outlinedBoxes++; }
|
|
338
|
+
else if ((r.width >= 240 && (topLine || botLine)) || (r.height >= 240 && (leftLine || rightLine))) { hairlines++; if (hairlineSamples.length < 5) hairlineSamples.push(label(el) + ' ' + Math.round(r.width) + 'px'); }
|
|
339
|
+
// thin dashed or dotted lines; a heavy dotted edge (3px or more) is a drawn perforation, not a hairline
|
|
340
|
+
if ([[cs.borderTopStyle, bt], [cs.borderBottomStyle, bb], [cs.borderLeftStyle, bl], [cs.borderRightStyle, br]].some(([st, w]) => /dashed|dotted/.test(st) && w > 0 && w < 3)) dashed++;
|
|
341
|
+
}
|
|
342
|
+
const rad = parseFloat(cs.borderTopLeftRadius) || 0;
|
|
343
|
+
const isMedia = el.tagName === 'IMG' || el.tagName === 'VIDEO' || el.tagName === 'CANVAS' || /url\(/.test(cs.backgroundImage);
|
|
344
|
+
if (isMedia && r.width >= 200 && r.height >= 140) {
|
|
345
|
+
let effective = rad; let p = el.parentElement;
|
|
346
|
+
for (let i = 0; i < 3 && p && effective === 0; i++, p = p.parentElement) { const pcs = getComputedStyle(p); if (pcs.overflow !== 'visible') effective = parseFloat(pcs.borderTopLeftRadius) || 0; }
|
|
347
|
+
if (effective > 1) { roundedMedia++; mediaRadii.push(Math.round(effective)); } else squareMedia++;
|
|
348
|
+
} else if (rad >= 6 && r.width >= 150 && r.height >= 80 && (rgba(cs.backgroundColor).a > 0.04 || bt > 0)) { roundedBoxes++; boxRadii.push(Math.round(Math.min(rad, 99))); }
|
|
349
|
+
}
|
|
350
|
+
// elements that are themselves a thin rule (1px tall or 1px wide)
|
|
351
|
+
for (const el of all) {
|
|
352
|
+
const r = el.getBoundingClientRect();
|
|
353
|
+
const horiz = r.height > 0 && r.height <= 1.5 && r.width >= 160, vert = r.width > 0 && r.width <= 1.5 && r.height >= 160;
|
|
354
|
+
if (!horiz && !vert) continue; if (formish(el) && (el.tagName !== 'HR' || el.closest('[data-ui]'))) continue;
|
|
355
|
+
const cs = getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || +cs.opacity < 0.03) continue;
|
|
356
|
+
if (rgba(cs.backgroundColor).a > 0.02 || parseFloat(cs.borderTopWidth) > 0 || parseFloat(cs.borderLeftWidth) > 0 || cs.backgroundImage !== 'none') { hairlines++; if (hairlineSamples.length < 5) hairlineSamples.push(label(el) + ' ' + Math.round(Math.max(r.width, r.height)) + 'px rule'); }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- repeated card rows (three or more same-size boxed siblings)
|
|
360
|
+
const cardRows = [];
|
|
361
|
+
const seenParents = new Set();
|
|
362
|
+
for (const b of big) {
|
|
363
|
+
const p = b.el.parentElement; if (!p || seenParents.has(p)) continue; seenParents.add(p);
|
|
364
|
+
// cards carry text; a row of photographs or media tiles is not a card row
|
|
365
|
+
const kids = [...p.children].filter((k) => { const r = k.getBoundingClientRect(); return r.width >= 150 && r.width < W * 0.9 && r.height >= 90 && !k.matches('img, picture, video, canvas, svg, figure:not(:has(figcaption))') && (k.innerText || '').trim().length >= 12; }); // full-width bands are sections, not cards
|
|
366
|
+
if (kids.length < 3) continue;
|
|
367
|
+
const boxed = kids.filter((k) => { const cs = getComputedStyle(k); return (rgba(cs.backgroundColor).a > 0.04 || parseFloat(cs.borderTopWidth) > 0 || cs.boxShadow !== 'none') && true; });
|
|
368
|
+
if (boxed.length < 3) continue;
|
|
369
|
+
const w0 = boxed[0].getBoundingClientRect().width;
|
|
370
|
+
const same = boxed.filter((k) => Math.abs(k.getBoundingClientRect().width - w0) < 3);
|
|
371
|
+
if (same.length < 3) continue;
|
|
372
|
+
const iconTop = same.filter((k) => { const first = k.querySelector('svg, img, i, [class*="icon" i]'); if (!first) return false; const r = first.getBoundingClientRect(); return r.width > 0 && r.width <= 64 && r.height <= 64; }).length;
|
|
373
|
+
cardRows.push({ count: same.length, iconTopped: iconTop >= same.length - 1 && iconTop >= 3, rounded: (parseFloat(getComputedStyle(same[0]).borderTopLeftRadius) || 0) >= 6, top: Math.round(same[0].getBoundingClientRect().top + SY) });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- hero media
|
|
377
|
+
const heroMedia = [];
|
|
378
|
+
for (const b of big) {
|
|
379
|
+
const { el, r, cs } = b; if (r.top > H * 0.9 || r.bottom < 0) continue;
|
|
380
|
+
const type = el.tagName === 'IMG' ? 'img' : el.tagName === 'VIDEO' ? 'video' : el.tagName === 'CANVAS' ? 'canvas' : el.tagName === 'svg' && r.width > 240 ? 'svg' : /url\(/.test(cs.backgroundImage) ? 'bg-image' : el.tagName === 'IFRAME' ? 'iframe' : null;
|
|
381
|
+
if (!type) continue;
|
|
382
|
+
const vw = Math.max(0, Math.min(r.right, W) - Math.max(r.left, 0)), vh = Math.max(0, Math.min(r.bottom, H) - Math.max(r.top, 0));
|
|
383
|
+
const ratio = (vw * vh) / (W * H); if (ratio >= 0.08) heroMedia.push({ type, ratio: round(ratio) });
|
|
384
|
+
}
|
|
385
|
+
heroMedia.sort((a, b) => b.ratio - a.ratio);
|
|
386
|
+
const brokenImages = [...document.images].filter((i) => i.complete && i.naturalWidth === 0 && i.getBoundingClientRect().width > 0 && (i.currentSrc || i.src)).map((i) => (i.currentSrc || i.src).slice(0, 120)).slice(0, 8);
|
|
387
|
+
|
|
388
|
+
// --- navigation
|
|
389
|
+
const navEl = document.querySelector('header nav, nav, header, [role="navigation"]');
|
|
390
|
+
let nav = null;
|
|
391
|
+
if (navEl) {
|
|
392
|
+
let host = navEl; let fixed = false; let p = navEl;
|
|
393
|
+
for (let i = 0; i < 4 && p && p !== document.body; i++, p = p.parentElement) { const pos = getComputedStyle(p).position; if (pos === 'fixed' || pos === 'sticky') { fixed = true; host = p; break; } }
|
|
394
|
+
const hr = host.getBoundingClientRect(); const hcs = getComputedStyle(host);
|
|
395
|
+
const links = [...navEl.querySelectorAll('a')].filter((a) => { const r = a.getBoundingClientRect(); return r.width > 0 && r.top < 140; });
|
|
396
|
+
nav = { links: links.length, fixed, height: Math.round(hr.height), floatingPill: hr.width < W * 0.92 && parseFloat(hcs.borderTopLeftRadius) >= 16 && hr.top > 4, blur: (hcs.backdropFilter || 'none') !== 'none' };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// data-ui is for cropped interface fragments, not for wrapping the page
|
|
400
|
+
const uiAbuse = [...document.querySelectorAll('[data-ui]')].some((el) => el.querySelector('h1') || el.getBoundingClientRect().height > H * 1.6 || el === document.body);
|
|
401
|
+
const uiFragments = document.querySelectorAll('[data-ui]').length;
|
|
402
|
+
// pictures that were not drawn by the page's own code: photographs, footage, raster artwork.
|
|
403
|
+
// Inline SVG and .svg files do not count; cropped interface fragments (data-ui) are counted apart.
|
|
404
|
+
const svgSrc = (u) => /\.svg(\?|#|$)|^data:image\/svg/i.test(u || '');
|
|
405
|
+
let realPictures = 0, videoCount = 0; const pictureSamples = [];
|
|
406
|
+
for (const el of all) {
|
|
407
|
+
if (el.closest('[data-ui]') || el.closest('svg')) continue;
|
|
408
|
+
const r = el.getBoundingClientRect(); if (r.width < 120 || r.height < 90 || r.width * r.height < 160 * 120) continue;
|
|
409
|
+
if (el.tagName === 'IMG') {
|
|
410
|
+
const src = el.currentSrc || el.src;
|
|
411
|
+
if (el.naturalWidth > 0 && !svgSrc(src)) { realPictures++; if (pictureSamples.length < 8) pictureSamples.push(src.split('/').pop().slice(0, 48)); }
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (el.tagName === 'VIDEO') { realPictures++; videoCount++; continue; }
|
|
415
|
+
const bg = getComputedStyle(el).backgroundImage;
|
|
416
|
+
if (bg && bg.includes('url(')) { const u = (bg.match(/url\(["']?([^"')]+)/) || [])[1]; if (!svgSrc(u)) realPictures++; }
|
|
417
|
+
}
|
|
418
|
+
const uiPictures = [...document.querySelectorAll('[data-ui]')].filter((el) => { const r = el.getBoundingClientRect(); return r.width >= 300 && r.height >= 200; }).length;
|
|
419
|
+
// which elements push the page wider than the viewport (the deepest ones that are not clipped by an ancestor)
|
|
420
|
+
let overflowCulprits = [];
|
|
421
|
+
if (document.documentElement.scrollWidth > W + 2) {
|
|
422
|
+
const clippedByAncestor = (el) => { for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) { if (getComputedStyle(p).overflowX !== 'visible' && p.getBoundingClientRect().right <= W + 2) return true; } return false; };
|
|
423
|
+
const wide = all.filter((el) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.right > W + 2 && !clippedByAncestor(el); });
|
|
424
|
+
overflowCulprits = wide.filter((el) => !wide.some((o) => o !== el && el.contains(o))).slice(0, 3).map((el) => `${el.tagName.toLowerCase()}${el.id ? '#' + el.id : ''}${el.classList.length ? '.' + [...el.classList].slice(0, 2).join('.') : ''} (+${Math.round(el.getBoundingClientRect().right - W)}px)`);
|
|
425
|
+
}
|
|
426
|
+
// rows that scroll sideways with content past their edge (read as clipped in screenshots and by most visitors)
|
|
427
|
+
const sideScrollers = all.filter((el) => { if (el.closest('[data-ui], table, pre, code')) return false; const cs = getComputedStyle(el); return /auto|scroll/.test(cs.overflowX) && el.scrollWidth > el.clientWidth + 24 && el.clientHeight > 40; }).length;
|
|
428
|
+
const blendText = texts.filter((t) => { for (let p = t.el, i = 0; p && p !== document.body && i < 6; p = p.parentElement, i++) if (/difference|exclusion/.test(getComputedStyle(p).mixBlendMode)) return true; return false; }).length;
|
|
429
|
+
const bodyText = document.body.innerText || '';
|
|
430
|
+
const emoji = (bodyText.match(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/gu) || []);
|
|
431
|
+
const doc = document.documentElement; const pageH = Math.max(doc.scrollHeight, document.body.scrollHeight);
|
|
432
|
+
const headingStats = headings.map(({ _el, ...h }) => h);
|
|
433
|
+
const heroOut = hero ? (({ _el, ...h }) => h)(hero) : null;
|
|
434
|
+
const below = headingStats.filter((h) => !heroOut || h.top !== heroOut.top);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
viewport: { w: W, h: H },
|
|
438
|
+
page: { height: pageH, screens: round(pageH / H, 1), words: bodyText.split(/\s+/).filter(Boolean).length,
|
|
439
|
+
img: document.images.length, video: document.querySelectorAll('video').length, canvas: document.querySelectorAll('canvas').length, svg: document.querySelectorAll('svg').length, sections: document.querySelectorAll('section').length,
|
|
440
|
+
horizontalOverflow: doc.scrollWidth > W + 2 ? doc.scrollWidth - W : 0, overflowCulprits, uiFragments, uiAbuse, realPictures, videoCount, uiPictures, pictureSamples, blendText, sideScrollers, brokenImages, emoji: [...new Set(emoji)].slice(0, 10),
|
|
441
|
+
videosNoPoster: [...document.querySelectorAll('video')].filter((v) => !v.getAttribute('poster') && v.getBoundingClientRect().width >= 160).length },
|
|
442
|
+
hero: heroOut, h1,
|
|
443
|
+
headings: headingStats.slice(0, 40),
|
|
444
|
+
headingSummary: { count: headingStats.length, maxLines: Math.max(0, ...headingStats.map((h) => h.lines)), over2Lines: headingStats.filter((h) => h.lines > 2 && h.kind !== 'statement').length, statements: headingStats.filter((h) => h.kind === 'statement').length, centered: headingStats.filter((h) => h.align === 'center').length, medianWords: below.length ? below.map((h) => h.words).sort((a, b) => a - b)[Math.floor(below.length / 2)] : null, mixedFontHeadings: headingStats.filter((h) => h.mixedFonts).length },
|
|
445
|
+
type: {
|
|
446
|
+
families, familyCount: famChars.size, sizes, sizeCount: sizes.length, notLoaded,
|
|
447
|
+
body: bodySample ? { font: bodySample.fam, size: bodySample.size, leading: bodySample.lh, weight: bodySample.weight, tracking: bodySample.ls, contrast: (() => { const bg = bgBehind(bodySample.el); return bg ? contrast(rgba(bodySample.color), bg) : null; })() } : null,
|
|
448
|
+
measureChars: longest ? Math.round(longest.w / (longest.size * 0.5)) : null,
|
|
449
|
+
max: maxText ? { size: maxText.size, vw: round((maxText.size / W) * 100, 1), font: maxText.fam, text: maxText.text.slice(0, 40) } : null,
|
|
450
|
+
monoShare: round(monoChars / Math.max(1, totalChars)), monoProseShare: round(monoProse / Math.max(1, totalChars)), monoFamilies: [...monoFams], monoSamples,
|
|
451
|
+
microLabels, microLabelShare: round(microLabelChars / Math.max(1, totalChars), 3), microSamples,
|
|
452
|
+
upperShare: round(upperChars / Math.max(1, totalChars)), centerShare: round(centerChars / Math.max(1, totalChars)), smallText,
|
|
453
|
+
},
|
|
454
|
+
eyebrows, numbered: numbered.slice(0, 12).map((t) => ({ text: t.text.slice(0, 30), size: t.size, top: Math.round(t.top) })),
|
|
455
|
+
color: {
|
|
456
|
+
bodyBg: hex(bodyBg), bodyBgHsl: hsl(bodyBg), dark: lum(bodyBg) < 0.18, sectionBgs: sectionBgs.slice(0, 10), textColors,
|
|
457
|
+
button: filledBtns.length ? { bg: mode(filledBtns.map((b) => b.bg)), hsl: mode(filledBtns.map((b) => b.bgHsl)), fg: mode(filledBtns.map((b) => b.fg)) } : null,
|
|
458
|
+
lowContrast: lowContrast.slice(0, 6),
|
|
459
|
+
},
|
|
460
|
+
buttons: { count: buttons.length, inHero: buttons.filter((b) => b.top < H).length, radius: mode(buttons.map((b) => b.radius)), height: mode(buttons.map((b) => b.h)), withShadow: buttons.filter((b) => b.shadow).length, withGradient: buttons.filter((b) => b.gradient).length, mono: buttons.filter((b) => b.mono).length, upper: buttons.filter((b) => b.upper).length, heroLabels: buttons.filter((b) => b.top < H).map((b) => b.text).filter(Boolean).slice(0, 5) },
|
|
461
|
+
surfaces: { shadowStatic, shadowOverlay, shadowSamples, gradients, scrims, gradientSamples, gradientText, hairlines, hairlineSamples, outlinedBoxes, dashed, blurred, roundedBoxes, boxRadius: mode(boxRadii) ?? 0, squareMedia, roundedMedia, mediaRadius: mode(mediaRadii) ?? 0, textShadow: texts.filter((t) => t.textShadow).length, cardRows: cardRows.slice(0, 8) },
|
|
462
|
+
heroMedia: heroMedia.slice(0, 4), nav,
|
|
463
|
+
fontsLoaded: [...loaded].slice(0, 12),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// frame() runs at each scroll position the inspector photographs. It reports large text that something
|
|
468
|
+
// else is lying on top of (a picture sliding over a headline), and how much of the screen holds anything.
|
|
469
|
+
export function frame() {
|
|
470
|
+
const W = innerWidth, H = innerHeight;
|
|
471
|
+
const pinnedTop = (el) => { for (let p = el; p && p !== document.body; p = p.parentElement) { const cs = getComputedStyle(p); if ((cs.position === 'fixed' || cs.position === 'sticky') && p.getBoundingClientRect().top < 140 && p.getBoundingClientRect().height < 180) return true; } return false; };
|
|
472
|
+
// 1. covered text. Hit-testing skips elements with pointer-events: none (a pinned title usually has it),
|
|
473
|
+
// which would make whatever lies beneath look as if it were on top; switch it on while sampling.
|
|
474
|
+
const hitStyle = document.createElement('style');
|
|
475
|
+
hitStyle.textContent = '*, *::before, *::after { pointer-events: auto !important; }';
|
|
476
|
+
document.head.appendChild(hitStyle);
|
|
477
|
+
const covered = [], clipped = [], pinnedOver = [];
|
|
478
|
+
const label = (el, direct) => (el.innerText || direct).replace(/\s+/g, ' ').trim().slice(0, 48);
|
|
479
|
+
const pinnedLow = (el) => { for (let p = el; p && p !== document.body; p = p.parentElement) { const pcs = getComputedStyle(p); if (pcs.position === 'fixed' || pcs.position === 'sticky') return !pinnedTop(el); } return false; };
|
|
480
|
+
// Does the element found on top actually paint anything there? A transparent layout box does not hide text.
|
|
481
|
+
const paints = (hit, el) => {
|
|
482
|
+
for (let p = hit; p && p !== document.body && !p.contains(el); p = p.parentElement) {
|
|
483
|
+
if (/^(IMG|VIDEO|CANVAS|PICTURE|IFRAME|svg)$/.test(p.tagName) || p.closest('svg')) return true;
|
|
484
|
+
const pcs = getComputedStyle(p);
|
|
485
|
+
if (pcs.backgroundImage !== 'none') return true;
|
|
486
|
+
const m = pcs.backgroundColor.match(/rgba?\(([^)]+)\)/); const parts = m ? m[1].split(/[,\s/]+/).filter(Boolean) : [];
|
|
487
|
+
if (parts.length && (parts.length < 4 || parseFloat(parts[3]) > 0.05)) return true;
|
|
488
|
+
if (p === hit) for (const n of p.childNodes) if (n.nodeType === 3 && n.nodeValue.trim()) return true; // its own text lies on ours
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
};
|
|
492
|
+
const pictures = [...document.querySelectorAll('img, picture, svg, canvas, video')].filter((m) => !m.parentElement?.closest('svg')).map((m) => ({ m, r: m.getBoundingClientRect() })).filter(({ r }) => r.width >= 80 && r.height >= 80 && r.bottom > 0 && r.top < H);
|
|
493
|
+
for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, blockquote, li, a, button, span, div')) {
|
|
494
|
+
let direct = ''; for (const n of el.childNodes) if (n.nodeType === 3) direct += n.nodeValue;
|
|
495
|
+
if (direct.trim().length < 3) continue;
|
|
496
|
+
const cs = getComputedStyle(el); const size = parseFloat(cs.fontSize); if (size < 24 || cs.visibility === 'hidden' || +cs.opacity < 0.3) continue;
|
|
497
|
+
let srOnly = false;
|
|
498
|
+
for (let p = el, i = 0; p && p !== document.body && i < 4; p = p.parentElement, i++) { const pcs = getComputedStyle(p); const pr = p.getBoundingClientRect(); if (pcs.clip === 'rect(0px, 0px, 0px, 0px)' || /inset\(50%\)/.test(pcs.clipPath) || (pr.width <= 2 && pr.height <= 2 && pcs.overflow !== 'visible')) { srOnly = true; break; } }
|
|
499
|
+
if (srOnly) continue;
|
|
500
|
+
// the boxes that clip this text (overflow other than visible, or a clip-path)
|
|
501
|
+
const clips = []; for (let p = el.parentElement; p && p !== document.body; p = p.parentElement) { const pcs = getComputedStyle(p); if (pcs.overflowY !== 'visible' || pcs.overflowX !== 'visible' || pcs.clipPath !== 'none') clips.push(p.getBoundingClientRect()); }
|
|
502
|
+
// a scroll scene moves pictures across its own held stage by design; its text is judged by the scene rules
|
|
503
|
+
const pinned = size >= 32 && pinnedLow(el) && !el.closest('.scene.is-live');
|
|
504
|
+
// lines of one heading set as separate spans overlap each other's glyph boxes; that is not a collision
|
|
505
|
+
const unit = el.closest('h1, h2, h3, h4, h5, h6, p, blockquote');
|
|
506
|
+
const range = document.createRange(); let total = 0, hidden = 0, cutTop = false, area = 0, over = 0;
|
|
507
|
+
for (const node of el.childNodes) {
|
|
508
|
+
if (node.nodeType !== 3 || !node.nodeValue.trim()) continue;
|
|
509
|
+
range.selectNodeContents(node);
|
|
510
|
+
for (const r of range.getClientRects()) {
|
|
511
|
+
if (r.width < 6 || r.bottom < 0 || r.top > H) continue;
|
|
512
|
+
// sampled at two heights inside the line box (a display face's glyph box often spills past it and
|
|
513
|
+
// under the next line), so a band lying over the top or the bottom of the letters is found too
|
|
514
|
+
const lh = parseFloat(cs.lineHeight); const band = Math.min(r.height, Number.isFinite(lh) ? lh : r.height);
|
|
515
|
+
for (const dy of [-0.2, 0.2]) for (const fx of [0.12, 0.37, 0.63, 0.88]) {
|
|
516
|
+
const x = r.left + r.width * fx, y = r.top + r.height * 0.55 + band * dy; if (x < 0 || x >= W || y < 0 || y >= H) continue;
|
|
517
|
+
total++; const hit = document.elementFromPoint(x, y);
|
|
518
|
+
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el) && !pinnedTop(hit) && !(unit && unit.contains(hit) && !/^(IMG|VIDEO|CANVAS|PICTURE|svg)$/.test(hit.tagName) && !hit.closest('svg')) && paints(hit, el)) hidden++;
|
|
519
|
+
}
|
|
520
|
+
// the glyph box starts above the capitals (ascender room), so only a cut well into it is a real crop
|
|
521
|
+
if (size >= 40 && r.top >= -4 && clips.some((c) => c.top > r.top + r.height * 0.3 && c.top < r.bottom - r.height * 0.2)) cutTop = true;
|
|
522
|
+
if (pinned) { area += r.width * r.height; for (const { m, r: pr } of pictures) { if (m.contains(el) || el.contains(m)) continue; const ox = Math.min(r.right, pr.right) - Math.max(r.left, pr.left), oy = Math.min(r.bottom, pr.bottom) - Math.max(r.top, pr.top); if (ox > 0 && oy > 0) over += ox * oy; } }
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (total >= 4 && hidden / total >= 0.2) covered.push({ text: label(el, direct), share: Math.round((hidden / total) * 100) / 100 });
|
|
526
|
+
if (cutTop) clipped.push({ text: label(el, direct) });
|
|
527
|
+
if (pinned && area && over / area >= 0.15) pinnedOver.push({ text: label(el, direct), share: Math.min(1, Math.round((over / area) * 100) / 100) });
|
|
528
|
+
}
|
|
529
|
+
hitStyle.remove();
|
|
530
|
+
// 2. how much of the screen holds anything: text glyph boxes, media, or a ground that differs from the page's
|
|
531
|
+
const opaque = (c) => c && c !== 'transparent' && !/rgba\([^)]*,\s*0\)$/.test(c);
|
|
532
|
+
const pageBg = [document.body, document.documentElement].map((e) => getComputedStyle(e).backgroundColor).find(opaque) || 'rgb(255, 255, 255)';
|
|
533
|
+
const textRects = [];
|
|
534
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const rg = document.createRange();
|
|
535
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
536
|
+
if (!n.nodeValue.trim() || !n.parentElement) continue; const pcs = getComputedStyle(n.parentElement); if (pcs.visibility === 'hidden' || +pcs.opacity < 0.2) continue;
|
|
537
|
+
rg.selectNodeContents(n); for (const r of rg.getClientRects()) if (r.bottom > 0 && r.top < H && r.width > 1) textRects.push(r);
|
|
538
|
+
}
|
|
539
|
+
let filled = 0, samples = 0;
|
|
540
|
+
for (let gx = 0; gx < 24; gx++) for (let gy = 0; gy < 15; gy++) {
|
|
541
|
+
const x = (gx + 0.5) * (W / 24), y = (gy + 0.5) * (H / 15); samples++;
|
|
542
|
+
if (textRects.some((r) => x >= r.left - 6 && x <= r.right + 6 && y >= r.top - 4 && y <= r.bottom + 4)) { filled++; continue; }
|
|
543
|
+
let el = document.elementFromPoint(x, y);
|
|
544
|
+
for (let i = 0; el && i < 6 && el !== document.body && el !== document.documentElement; i++, el = el.parentElement) {
|
|
545
|
+
if (/^(IMG|VIDEO|CANVAS|PICTURE|svg|IFRAME)$/.test(el.tagName) || el.closest('svg')) { filled++; break; }
|
|
546
|
+
const cs = getComputedStyle(el);
|
|
547
|
+
if (cs.backgroundImage !== 'none') { filled++; break; }
|
|
548
|
+
const bg = cs.backgroundColor; if (bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') { if (bg !== pageBg) filled++; break; }
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// 3. the large text in view, to spot one statement held on screen for screen after screen
|
|
552
|
+
const big = [...new Set([...document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, blockquote')].filter((el) => { const r = el.getBoundingClientRect(); return !el.closest('.scene.is-live') && parseFloat(getComputedStyle(el).fontSize) >= 32 && r.top >= -4 && r.bottom <= H + 4 && r.height > 0; }).map((el) => (el.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 60)).filter((t) => t.length >= 4))];
|
|
553
|
+
return { covered, clipped, pinnedOver, filled: Math.round((filled / samples) * 100) / 100, big, y: Math.round(scrollY) };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// layout() runs once, back at the top after the page has been scrolled through (so reveals have fired).
|
|
557
|
+
// It measures the frame of the page rather than its details: the two outer margins, sections whose content
|
|
558
|
+
// huddles on one side, blank stretches with nothing in them, the title, untitled sections, text pressed
|
|
559
|
+
// against the edge of the screen and how many left edges the text uses.
|
|
560
|
+
export function layout() {
|
|
561
|
+
const W = innerWidth, H = innerHeight, SY = scrollY;
|
|
562
|
+
const D = document.documentElement, B = document.body;
|
|
563
|
+
const pageH = Math.max(D.scrollHeight, B.scrollHeight);
|
|
564
|
+
const pct = (arr, p) => { if (!arr.length) return null; const s = [...arr].sort((a, b) => a - b); return s[Math.min(s.length - 1, Math.max(0, Math.round((s.length - 1) * p)))]; };
|
|
565
|
+
const shown = (el) => { for (let p = el, i = 0; p && p !== B && i < 30; p = p.parentElement, i++) { const cs = getComputedStyle(p); if (cs.display === 'none' || cs.visibility === 'hidden' || +cs.opacity < 0.05) return false; if (i < 4 && (cs.clip === 'rect(0px, 0px, 0px, 0px)' || /inset\(50%\)/.test(cs.clipPath))) return false; } return true; };
|
|
566
|
+
const layerOf = (el) => { for (let p = el; p && p !== B; p = p.parentElement) { const pos = getComputedStyle(p).position; if (pos === 'fixed') return 'fixed'; if (pos === 'sticky') return 'sticky'; } return null; };
|
|
567
|
+
const sel = (el) => (el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') + (typeof el.className === 'string' && el.className.trim() ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '')).slice(0, 60);
|
|
568
|
+
const boxes = [];
|
|
569
|
+
// text, one box per text node (its line boxes merged)
|
|
570
|
+
const walker = document.createTreeWalker(B, NodeFilter.SHOW_TEXT); const rg = document.createRange();
|
|
571
|
+
for (let n = walker.nextNode(); n && boxes.length < 6000; n = walker.nextNode()) {
|
|
572
|
+
const t = n.nodeValue.replace(/\s+/g, ' ').trim(); if (t.length < 2) continue;
|
|
573
|
+
const el = n.parentElement; if (!el || /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE|OPTION)$/.test(el.tagName) || el.closest('svg')) continue;
|
|
574
|
+
rg.selectNodeContents(n); const r = rg.getBoundingClientRect(); if (r.width < 2 || r.height < 2) continue;
|
|
575
|
+
if (!shown(el)) continue;
|
|
576
|
+
const cs = getComputedStyle(el);
|
|
577
|
+
boxes.push({ kind: 'text', el, l: r.left, r: r.right, t: r.top + SY, b: r.bottom + SY, chars: t.length, text: t.slice(0, 50), size: parseFloat(cs.fontSize), align: cs.textAlign, layer: layerOf(el), tag: el.tagName });
|
|
578
|
+
}
|
|
579
|
+
// pictures, drawings and controls
|
|
580
|
+
for (const el of B.querySelectorAll('img, video, canvas, picture, iframe, svg, input, select, textarea, button, [role="button"], a, div, section, figure, span, li')) {
|
|
581
|
+
const tag = el.tagName;
|
|
582
|
+
let kind = null;
|
|
583
|
+
if (/^(IMG|VIDEO|CANVAS|PICTURE|IFRAME)$/.test(tag)) kind = 'media';
|
|
584
|
+
else if (tag === 'svg') { if (el.parentElement && el.parentElement.closest('svg')) continue; kind = 'media'; }
|
|
585
|
+
else if (/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(tag) || el.getAttribute('role') === 'button') kind = 'control';
|
|
586
|
+
else { const bi = getComputedStyle(el).backgroundImage; if (bi && bi.includes('url(')) kind = 'media'; else continue; }
|
|
587
|
+
const r = el.getBoundingClientRect(); if (r.width < 24 || r.height < 24) continue;
|
|
588
|
+
if (kind === 'media' && r.width * r.height < 48 * 48) continue;
|
|
589
|
+
if (!shown(el)) continue;
|
|
590
|
+
boxes.push({ kind, el, l: r.left, r: r.right, t: r.top + SY, b: r.bottom + SY, chars: 0, size: 0, layer: layerOf(el), tag });
|
|
591
|
+
}
|
|
592
|
+
const flow = boxes.filter((b) => b.layer !== 'fixed');
|
|
593
|
+
// 1. the frame: how far the page's content comes to each edge (full-bleed things say nothing about margins)
|
|
594
|
+
// a row that runs off one edge (a carousel, a bled display line) says nothing about that side's margin
|
|
595
|
+
const framed = flow.filter((b) => b.r - b.l < W * 0.97 && b.l >= -1 && b.r <= W + 1);
|
|
596
|
+
const L = framed.length ? Math.max(0, pct(framed.map((b) => b.l), 0.02)) : null;
|
|
597
|
+
const R = framed.length ? Math.max(0, W - pct(framed.map((b) => b.r), 0.98)) : null;
|
|
598
|
+
// 2. sections: descend through single wrappers, then take the blocks
|
|
599
|
+
const tall = (c) => { const r = c.getBoundingClientRect(); return r.height >= 120 && r.width >= W * 0.6 && getComputedStyle(c).position !== 'fixed' && shown(c); };
|
|
600
|
+
let root = B;
|
|
601
|
+
for (let i = 0; i < 12; i++) { const kids = [...root.children].filter(tall); if (kids.length === 1) root = kids[0]; else break; }
|
|
602
|
+
// a tall block is split into its parts; the parts of a block that has its own heading continue it
|
|
603
|
+
let blocks = [...root.children].filter(tall).map((el) => ({ el, cont: false }));
|
|
604
|
+
for (let pass = 0; pass < 2; pass++) blocks = blocks.flatMap((b) => { const k = [...b.el.children].filter(tall); if (b.el.getBoundingClientRect().height > H * 1.6 && k.length >= 2) { const titled = b.cont || !!b.el.querySelector('h1, h2, h3, h4, [role="heading"]'); return k.map((el) => ({ el, cont: titled })); } return [b]; });
|
|
605
|
+
const continues = new Set(blocks.filter((b) => b.cont).map((b) => b.el));
|
|
606
|
+
blocks = blocks.map((b) => b.el);
|
|
607
|
+
const bodySize = (() => { const m = new Map(); for (const b of flow) if (b.kind === 'text' && b.chars >= 40) m.set(Math.round(b.size), (m.get(Math.round(b.size)) || 0) + b.chars); return [...m.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || 16; })();
|
|
608
|
+
const sections = blocks.slice(0, 40).map((s, i) => {
|
|
609
|
+
const r = s.getBoundingClientRect(); const top = r.top + SY, bot = r.bottom + SY;
|
|
610
|
+
const inside = flow.filter((b) => b.t >= top - 2 && b.b <= bot + 2 && s.contains(b.el));
|
|
611
|
+
const own = inside.filter((b) => b.r - b.l < W * 0.97);
|
|
612
|
+
const text = inside.filter((b) => b.kind === 'text');
|
|
613
|
+
const media = inside.filter((b) => b.kind === 'media' && (b.r - b.l) * (b.b - b.t) >= 160 * 120);
|
|
614
|
+
const words = text.reduce((a, b) => a + b.text.split(' ').length + Math.max(0, Math.round((b.chars - 50) / 6)), 0);
|
|
615
|
+
const headRe = /^H[1-4]$/;
|
|
616
|
+
const heading = text.filter((b) => (b.el.closest('h1,h2,h3,h4,[role="heading"]') || b.size >= Math.max(22, bodySize * 1.35)) && b.t <= top + Math.max(520, (bot - top) * 0.45)).sort((a, b) => b.size - a.size)[0];
|
|
617
|
+
const cs = getComputedStyle(s);
|
|
618
|
+
// the widest stretch across the section's content band that nothing covers, edge to edge excluded
|
|
619
|
+
let middle = 0;
|
|
620
|
+
if (own.length >= 2) {
|
|
621
|
+
const iv = own.map((b) => [Math.max(0, b.l), Math.min(W, b.r)]).sort((a, b) => a[0] - b[0]);
|
|
622
|
+
let end = iv[0][1];
|
|
623
|
+
for (const [a, b2] of iv.slice(1)) { if (a > end) middle = Math.max(middle, a - end); end = Math.max(end, b2); }
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
sel: sel(s), tag: s.tagName.toLowerCase(), top: Math.round(top), height: Math.round(r.height), words, media: media.length, middle: Math.round(middle),
|
|
627
|
+
heading: heading ? { text: heading.text, size: Math.round(heading.size), tag: (heading.el.closest('h1,h2,h3,h4,h5,h6') || heading.el).tagName.toLowerCase() } : null,
|
|
628
|
+
left: own.length ? Math.round(Math.max(0, Math.min(...own.map((b) => b.l)))) : null, right: own.length ? Math.round(Math.max(0, W - Math.max(...own.map((b) => b.r)))) : null,
|
|
629
|
+
textLeft: text.length ? Math.round(Math.min(...text.map((b) => b.l))) : null, textRight: text.length ? Math.round(W - Math.max(...text.map((b) => b.r))) : null,
|
|
630
|
+
fullBleed: inside.some((b) => b.kind === 'media' && b.r - b.l >= W * 0.97), footer: s.tagName === 'FOOTER' || !!s.querySelector('footer') || i === blocks.length - 1,
|
|
631
|
+
hero: top < H * 0.5, continues: continues.has(s), pinned: cs.position === 'sticky' || !!s.querySelector('.pin-spacer') || [...s.querySelectorAll('*')].slice(0, 400).some((e) => getComputedStyle(e).position === 'sticky' && e.getBoundingClientRect().height > H * 0.5),
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
// 3. blank stretches: rows of the page with no text, picture or control, between the first and the last content.
|
|
635
|
+
// A sticky element's track (its parent) and a pinning spacer are scroll distance, not blank page.
|
|
636
|
+
const STEP = 4, rows = Math.ceil(pageH / STEP), occ = new Uint8Array(rows);
|
|
637
|
+
const mark = (t, b, v) => { for (let y = Math.max(0, Math.floor(t / STEP)); y < Math.min(rows, Math.ceil(b / STEP)); y++) occ[y] = v; };
|
|
638
|
+
for (const b of flow) if (b.layer !== 'sticky') mark(b.t, b.b, 1);
|
|
639
|
+
for (const el of B.querySelectorAll('.pin-spacer')) { const r = el.getBoundingClientRect(); mark(r.top + SY, r.bottom + SY, 2); }
|
|
640
|
+
let scanned = 0;
|
|
641
|
+
for (const el of B.querySelectorAll('*')) { if (++scanned > 9000) break; const cs = getComputedStyle(el); if (cs.position === 'sticky' && el.parentElement) { const r = el.parentElement.getBoundingClientRect(); mark(r.top + SY, r.bottom + SY, 2); } }
|
|
642
|
+
let first = 0; while (first < rows && occ[first] !== 1) first++;
|
|
643
|
+
let last = rows - 1; while (last > 0 && occ[last] !== 1) last--;
|
|
644
|
+
const gaps = []; let run = 0;
|
|
645
|
+
for (let y = first; y <= last + 1; y++) { if (y <= last && occ[y] === 0) run++; else { if (run * STEP >= H * 0.3) gaps.push({ top: (y - run) * STEP, height: run * STEP }); run = 0; } }
|
|
646
|
+
// 4. the title
|
|
647
|
+
const h1s = [...document.querySelectorAll('h1')].filter((h) => { const r = h.getBoundingClientRect(); return r.width > 0 && r.height > 0 && shown(h); }).map((h) => { const r = h.getBoundingClientRect(); return { text: (h.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 80), size: Math.round(parseFloat(getComputedStyle(h).fontSize)), top: Math.round(r.top + SY), firstScreen: r.top + SY < H * 0.95 }; });
|
|
648
|
+
// 5. text pressed against the edge of the screen (type deliberately bled past the edge is a different device)
|
|
649
|
+
// fixed headers and corner labels count here: that is where text most often creeps to the edge
|
|
650
|
+
const edge = 16;
|
|
651
|
+
const edgeText = boxes.filter((b) => b.kind === 'text' && b.chars >= 3 && b.size < 40 && b.l >= -1 && b.r <= W + 1 && (b.l < edge || b.r > W - edge) && !b.el.closest('[class*="marquee" i],[class*="ticker" i],marquee,[data-ui],table')).slice(0, 8).map((b) => ({ text: b.text, side: b.l < edge ? 'left' : 'right', px: Math.round(b.l < edge ? b.l : W - b.r) }));
|
|
652
|
+
// display type that runs off one edge by a sliver while it starts inside the margin: a fit error, not a bleed
|
|
653
|
+
const slivers = boxes.filter((b) => b.kind === 'text' && b.size >= 40 && b.chars >= 3 && ((b.l >= 8 && b.r > W + 1 && b.r - W < b.size * 0.25) || (b.r <= W - 8 && b.l < -1 && -b.l < b.size * 0.25))).slice(0, 4).map((b) => ({ text: b.text.slice(0, 30), px: Math.round(b.r > W ? b.r - W : -b.l), side: b.r > W ? 'right' : 'left' }));
|
|
654
|
+
// the largest empty rectangle on the first screen (below a 90px band for the navigation), on a 24 x 15 grid
|
|
655
|
+
let voidShare = 0, voidBox = null;
|
|
656
|
+
if (W >= 1000) {
|
|
657
|
+
const C = 24, Rn = 15, cw = W / C, ch = H / Rn, occ2 = Array.from({ length: Rn }, () => new Array(C).fill(0));
|
|
658
|
+
for (const b of boxes) { if (b.t - SY > H || b.b - SY < 0) continue; const c0 = Math.max(0, Math.floor(b.l / cw)), c1 = Math.min(C - 1, Math.floor((b.r - 1) / cw)), r0 = Math.max(0, Math.floor((b.t - SY) / ch)), r1 = Math.min(Rn - 1, Math.floor((b.b - SY - 1) / ch)); for (let r = r0; r <= r1; r++) for (let c = c0; c <= c1; c++) occ2[r][c] = 1; }
|
|
659
|
+
const top = Math.ceil(90 / ch), hgt = new Array(C).fill(0);
|
|
660
|
+
for (let r = top; r < Rn; r++) {
|
|
661
|
+
for (let c = 0; c < C; c++) hgt[c] = occ2[r][c] ? 0 : hgt[c] + 1;
|
|
662
|
+
for (let c = 0; c < C; c++) { let mn = Infinity; for (let k = c; k < C && hgt[k]; k++) { mn = Math.min(mn, hgt[k]); const a = mn * (k - c + 1); if (a > voidShare) { voidShare = a; voidBox = { x: Math.round(c * cw), y: Math.round((r - mn + 1) * ch), w: Math.round((k - c + 1) * cw), h: Math.round(mn * ch) }; } } }
|
|
663
|
+
}
|
|
664
|
+
voidShare = Math.round((voidShare / (C * Rn)) * 100) / 100;
|
|
665
|
+
}
|
|
666
|
+
// a fixed picture layer that covers the screen (a canvas scene, a film) hides what the DOM cannot see
|
|
667
|
+
const stage = boxes.some((b) => b.layer === 'fixed' && b.kind === 'media' && (b.r - b.l) * (b.b - b.t) >= W * H * 0.9);
|
|
668
|
+
// the first screen: is the title a bare name, with nothing at reading size saying what this is?
|
|
669
|
+
const firstText = flow.filter((b) => b.kind === 'text' && b.t < H * 0.95 && b.size >= 18 && !b.el.closest('h1, nav, header nav, button, a'));
|
|
670
|
+
const explains = firstText.some((b) => b.text.split(' ').length >= 6 || b.chars >= 36);
|
|
671
|
+
// 6. left edges used by running text and headings (left-aligned blocks only)
|
|
672
|
+
// headings and paragraphs only: the items of a grid or a list each start their own column on purpose
|
|
673
|
+
const lefts = flow.filter((b) => b.kind === 'text' && b.size >= 14 && !/center|right|end/.test(b.align) && b.l >= 0 && (b.el.closest('h1, h2, h3') || (b.chars >= 60 && b.el.closest('p, blockquote'))) && !b.el.closest('nav, footer, [data-ui], table, li, figcaption')).map((b) => b.l).sort((a, b) => a - b);
|
|
674
|
+
const edges = []; for (const x of lefts) { const e = edges.find((g) => Math.abs(g.x - x) <= 10); if (e) e.n++; else edges.push({ x: Math.round(x), n: 1 }); }
|
|
675
|
+
edges.sort((a, b) => b.n - a.n);
|
|
676
|
+
return { viewport: { w: W, h: H }, pageH, margins: { left: L == null ? null : Math.round(L), right: R == null ? null : Math.round(R) }, sections, gaps, h1s, edgeText, edges: edges.slice(0, 12), leftBlocks: lefts.length, bodySize, stage, explains, slivers, voidShare, voidBox };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// app() measures what an app screen (dashboard, admin, settings, inbox) gets right or wrong: the shell,
|
|
680
|
+
// how much of the canvas is used, panels and their edges, numbers in tables and KPIs, chart colours,
|
|
681
|
+
// the icon-in-a-tinted-square habit, placeholder data, and what happens to the sidebar on a phone.
|
|
682
|
+
export function app() {
|
|
683
|
+
const W = innerWidth, H = innerHeight, SY = scrollY, B = document.body;
|
|
684
|
+
const cvs = document.createElement('canvas'); cvs.width = cvs.height = 1;
|
|
685
|
+
const ctx = cvs.getContext('2d', { willReadFrequently: true });
|
|
686
|
+
const rgba = (c) => { ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = 'rgba(0,0,0,0)'; ctx.fillStyle = c; ctx.fillRect(0, 0, 1, 1); const d = ctx.getImageData(0, 0, 1, 1).data; return { r: d[0], g: d[1], b: d[2], a: d[3] / 255 }; };
|
|
687
|
+
const hex = (v) => '#' + [v.r, v.g, v.b].map((n) => n.toString(16).padStart(2, '0')).join('');
|
|
688
|
+
const lum = (v) => { const f = (x) => { x /= 255; return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; }; return 0.2126 * f(v.r) + 0.7152 * f(v.g) + 0.0722 * f(v.b); };
|
|
689
|
+
const contrast = (a, b) => { const l1 = lum(a), l2 = lum(b); return Math.round(((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)) * 100) / 100; };
|
|
690
|
+
const hsl = (v) => { const r = v.r / 255, g = v.g / 255, b = v.b / 255, mx = Math.max(r, g, b), mn = Math.min(r, g, b); const l = (mx + mn) / 2; let h = 0, s = 0; if (mx !== mn) { const d = mx - mn; s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn); h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4; h *= 60; } return { h: Math.round(h), s, l }; };
|
|
691
|
+
const shown = (el, cs) => { cs = cs || getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden' || +cs.opacity < 0.05) return false; const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0; };
|
|
692
|
+
const groundOf = (el) => { for (let p = el; p; p = p.parentElement) { const v = rgba(getComputedStyle(p).backgroundColor); if (v.a >= 0.9) return v; } return { r: 255, g: 255, b: 255, a: 1 }; };
|
|
693
|
+
const pageBg = groundOf(B);
|
|
694
|
+
const all = [...B.querySelectorAll('*')].slice(0, 12000);
|
|
695
|
+
// 1. the shell: a tall column at one side (sidebar) and a bar across the top
|
|
696
|
+
let sidebar = null, topbar = null;
|
|
697
|
+
for (const el of all) {
|
|
698
|
+
const r = el.getBoundingClientRect(); if (r.width < 40) continue;
|
|
699
|
+
if (!sidebar && r.left <= 1 && r.top <= 80 && r.height >= H * 0.7 && r.width >= 48 && r.width <= Math.min(360, W * 0.45) && (el.querySelectorAll('a, button').length >= 3) && shown(el)) sidebar = { w: Math.round(r.width), tag: el.tagName.toLowerCase(), links: el.querySelectorAll('a, button').length, share: Math.round((r.width / W) * 100) / 100 };
|
|
700
|
+
if (!topbar && r.top <= 1 && r.left <= (sidebar ? sidebar.w + 2 : 2) && r.width >= W * 0.6 && r.height >= 36 && r.height <= 96 && shown(el)) topbar = { h: Math.round(r.height) };
|
|
701
|
+
if (sidebar && topbar) break;
|
|
702
|
+
}
|
|
703
|
+
// 2. how much of the canvas the content uses (right of the sidebar)
|
|
704
|
+
const x0 = sidebar ? sidebar.w : 0;
|
|
705
|
+
// the text itself (a block element's box runs to the edge whatever its text does), pictures, charts and controls
|
|
706
|
+
const boxes = [];
|
|
707
|
+
const tw = document.createTreeWalker(B, NodeFilter.SHOW_TEXT); const trg = document.createRange();
|
|
708
|
+
for (let n = tw.nextNode(); n && boxes.length < 5000; n = tw.nextNode()) {
|
|
709
|
+
if (n.nodeValue.trim().length < 2 || !n.parentElement || n.parentElement.closest('svg')) continue;
|
|
710
|
+
const cs = getComputedStyle(n.parentElement); if (cs.visibility === 'hidden' || cs.display === 'none' || +cs.opacity < 0.05) continue;
|
|
711
|
+
trg.selectNodeContents(n); const r = trg.getBoundingClientRect(); if (r.width < 2 || r.left < x0 - 1 || (cs.position === 'fixed' && r.left > W * 0.5)) continue;
|
|
712
|
+
boxes.push(r);
|
|
713
|
+
}
|
|
714
|
+
for (const el of B.querySelectorAll('svg, canvas, img, video, input, select, textarea, button, table')) {
|
|
715
|
+
if (el.parentElement && el.parentElement.closest('svg')) continue;
|
|
716
|
+
const r = el.getBoundingClientRect(); if (r.width < 8 || r.height < 8 || r.left < x0 - 1 || !shown(el)) continue;
|
|
717
|
+
boxes.push(r);
|
|
718
|
+
}
|
|
719
|
+
const rights = boxes.map((r) => r.right).sort((a, b) => a - b), lefts = boxes.map((r) => r.left).sort((a, b) => a - b);
|
|
720
|
+
const canvas = boxes.length ? { left: Math.round(lefts[Math.floor(lefts.length * 0.02)] - x0), right: Math.round(W - rights[Math.floor(rights.length * 0.98)]) } : null;
|
|
721
|
+
// 3. panels: boxes that read as cards (a fill one step from the ground, a border, or a shadow)
|
|
722
|
+
const panels = []; const radii = new Map(); let heavyBorders = 0, heavyShadows = 0, softShadows = 0, glass = 0; const borderSamples = [], shadowSamples = [];
|
|
723
|
+
for (const el of all) {
|
|
724
|
+
const r = el.getBoundingClientRect(); if (r.width < 140 || r.height < 72 || r.width > W * 0.98) continue;
|
|
725
|
+
const cs = getComputedStyle(el); if (!shown(el, cs)) continue;
|
|
726
|
+
const bg = rgba(cs.backgroundColor), bw = parseFloat(cs.borderTopWidth) || 0, bc = rgba(cs.borderTopColor);
|
|
727
|
+
const ground = groundOf(el.parentElement || B);
|
|
728
|
+
const filled = bg.a >= 0.9 && contrast(bg, ground) >= 1.04, bordered = bw >= 0.5 && cs.borderTopStyle !== 'none' && bc.a > 0.05;
|
|
729
|
+
const sh = cs.boxShadow !== 'none' ? cs.boxShadow : '';
|
|
730
|
+
if (!filled && !bordered && !sh) continue;
|
|
731
|
+
if (!(el.innerText || '').trim()) continue;
|
|
732
|
+
panels.push({ el, w: Math.round(r.width), h: Math.round(r.height), top: Math.round(r.top + SY), left: Math.round(r.left), bottom: Math.round(r.bottom + SY), right: Math.round(r.right) });
|
|
733
|
+
const rad = Math.round(parseFloat(cs.borderTopLeftRadius) || 0); radii.set(rad, (radii.get(rad) || 0) + 1);
|
|
734
|
+
if (bordered) { const c = contrast({ ...bc, a: 1 }, bg.a >= 0.9 ? bg : ground); if (c >= (lum(ground) > 0.4 ? 1.45 : 1.9)) { heavyBorders++; if (borderSamples.length < 3) borderSamples.push(`${hex(bc)} on ${hex(bg.a >= 0.9 ? bg : ground)} (${c}:1)`); } }
|
|
735
|
+
if (sh) {
|
|
736
|
+
for (const part of sh.split(/,(?![^(]*\))/)) {
|
|
737
|
+
if (/inset/.test(part)) continue; const col = part.match(/rgba?\([^)]+\)/); const nums = part.replace(/rgba?\([^)]+\)/, '').trim().split(/\s+/).map(parseFloat);
|
|
738
|
+
const a = col ? rgba(col[0]).a : 1; const blur = nums[2] || 0, y = Math.abs(nums[1] || 0);
|
|
739
|
+
if (a <= 0.02 || (blur === 0 && y === 0 && !(nums[3] > 0))) continue;
|
|
740
|
+
// on a dark ground a black shadow barely shows; there it only counts when it is near-opaque
|
|
741
|
+
const darkGround = lum(ground) < 0.2;
|
|
742
|
+
if (darkGround ? a >= 0.45 : ((a >= 0.12 && blur >= 12) || y >= 8 || a >= 0.25)) { heavyShadows++; if (shadowSamples.length < 3) shadowSamples.push(part.trim().slice(0, 70)); } else softShadows++;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (cs.backdropFilter && cs.backdropFilter !== 'none') glass++;
|
|
746
|
+
}
|
|
747
|
+
// top-level panels only (a card inside a panel is part of it): rows that end ragged, and how many
|
|
748
|
+
// panels start on the first screen
|
|
749
|
+
const panelEls = new Set(panels.map((p) => p.el));
|
|
750
|
+
const tops = panels.filter((p) => { for (let a = p.el.parentElement; a && a !== B; a = a.parentElement) if (panelEls.has(a)) return false; return p.w < W * 0.97 || p.h < H * 2; });
|
|
751
|
+
// a column of cards stacked with a gutter of 24px or less is one block: two cards on the left that end level with
|
|
752
|
+
// one tall card on the right are not ragged. A pair is ragged only when the cards and their columns both end apart.
|
|
753
|
+
const below = (p) => tops.find((q) => q !== p && Math.abs(q.left - p.left) <= 4 && Math.abs(q.w - p.w) <= 4 && q.top >= p.bottom - 2 && q.top - p.bottom <= 24);
|
|
754
|
+
const columnBottom = (p) => { let b = p.bottom; for (let q = below(p), n = 0; q && n < 12; q = below(q), n++) b = q.bottom; return b; };
|
|
755
|
+
const ragged = [];
|
|
756
|
+
for (let i = 0; i < tops.length; i++) for (let k = i + 1; k < tops.length; k++) {
|
|
757
|
+
const a = tops[i], b = tops[k];
|
|
758
|
+
if (Math.abs(a.top - b.top) > 4 || a.w < 200 || b.w < 200 || (a.right > b.left - 4 && b.right > a.left - 4)) continue; // side by side, same row
|
|
759
|
+
const diff = Math.min(Math.abs(a.bottom - b.bottom), Math.abs(columnBottom(a) - columnBottom(b))), tall = Math.max(a.h, b.h);
|
|
760
|
+
if (diff > Math.max(24, tall * 0.12)) ragged.push({ diff, top: a.top });
|
|
761
|
+
}
|
|
762
|
+
// the largest blank band inside a card: the vertical gap between consecutive pieces of its content (text, charts,
|
|
763
|
+
// pictures, controls, rules, fills), or from the last piece to the card's padding edge. A legend pinned to the foot
|
|
764
|
+
// of a stretched card, or four rows in a card kept at its neighbour's height, leave one.
|
|
765
|
+
const voids = [];
|
|
766
|
+
const seen = new Map();
|
|
767
|
+
const visibleIn = (el, root) => { if (seen.has(el)) return seen.get(el); let ok = true; for (let p = el; p && p !== root.parentElement; p = p.parentElement) { const cs = getComputedStyle(p); if (cs.display === 'none' || cs.visibility === 'hidden' || +cs.opacity < 0.05 || cs.position === 'fixed') { ok = false; break; } } seen.set(el, ok); return ok; };
|
|
768
|
+
for (const p of tops) {
|
|
769
|
+
if (p.el.matches('nav, aside, header, footer, dialog, [role="navigation"], [role="dialog"]') || p.el.querySelector('nav') || (sidebar && p.left < sidebar.w - 4) || p.h < 120) continue;
|
|
770
|
+
if (panels.filter((q) => q.el !== p.el && p.el.contains(q.el)).length >= 2) continue; // a frame of cards, not a card
|
|
771
|
+
const cs = getComputedStyle(p.el), pr = p.el.getBoundingClientRect();
|
|
772
|
+
const inTop = pr.top + (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.paddingTop) || 0), inBottom = pr.bottom - (parseFloat(cs.borderBottomWidth) || 0) - (parseFloat(cs.paddingBottom) || 0);
|
|
773
|
+
const bg = rgba(cs.backgroundColor), spans = [];
|
|
774
|
+
const tw2 = document.createTreeWalker(p.el, NodeFilter.SHOW_TEXT), rg = document.createRange();
|
|
775
|
+
// text that opens a section (a heading, a summary, or text at 14px/600 or 15px/500 and up) may follow a wider, placed gap
|
|
776
|
+
for (let n = tw2.nextNode(); n; n = tw2.nextNode()) { if (!n.nodeValue.trim() || !n.parentElement || n.parentElement.closest('svg') || !visibleIn(n.parentElement, p.el)) continue; rg.selectNodeContents(n); const r = rg.getBoundingClientRect(); if (r.height > 0 && r.width > 0) { const pcs = getComputedStyle(n.parentElement); const fs = parseFloat(pcs.fontSize), fw = +pcs.fontWeight; spans.push([r.top, r.bottom, !!n.parentElement.closest('h1, h2, h3, h4, h5, h6, [role="heading"], summary, legend, caption') || (fw >= 600 && fs >= 14) || (fw >= 500 && fs >= 15)]); } }
|
|
777
|
+
for (const el of p.el.querySelectorAll('*')) {
|
|
778
|
+
if (el.parentElement && el.parentElement.closest('svg')) continue;
|
|
779
|
+
const r = el.getBoundingClientRect(); if (r.width < 2 || r.height < 1 || !visibleIn(el, p.el)) continue;
|
|
780
|
+
if (/^(svg|canvas|img|video|input|select|textarea|button|hr|progress|meter|iframe)$/i.test(el.tagName)) { spans.push([r.top, r.bottom]); continue; }
|
|
781
|
+
const ecs = getComputedStyle(el), fillC = rgba(ecs.backgroundColor);
|
|
782
|
+
if (fillC.a >= 0.1 && (bg.a < 0.9 || contrast({ ...fillC, a: 1 }, bg) >= 1.03) && r.height >= 2) spans.push([r.top, r.bottom]);
|
|
783
|
+
if ((parseFloat(ecs.borderTopWidth) || 0) >= 0.5 && ecs.borderTopStyle !== 'none' && rgba(ecs.borderTopColor).a > 0.05) spans.push([r.top, r.top + 1]);
|
|
784
|
+
if ((parseFloat(ecs.borderBottomWidth) || 0) >= 0.5 && ecs.borderBottomStyle !== 'none' && rgba(ecs.borderBottomColor).a > 0.05) spans.push([r.bottom - 1, r.bottom]);
|
|
785
|
+
}
|
|
786
|
+
const ys = spans.filter(([a, b]) => b > inTop - 2 && a < inBottom + 2).sort((a, b) => a[0] - b[0]);
|
|
787
|
+
if (!ys.length) continue;
|
|
788
|
+
// a band counts when it is over 40px, or over 64px when the content after it opens a new section
|
|
789
|
+
let end = ys[0][1], gap = 0, at = null;
|
|
790
|
+
for (const [a, b] of ys.slice(1)) { const g = a - end, section = ys.some(([t, , hd]) => hd && Math.abs(t - a) < 6); if (g > (section ? 64 : 40) && g > gap) { gap = g; at = end; } end = Math.max(end, b); }
|
|
791
|
+
if (inBottom - end > 40 && inBottom - end > gap) { gap = inBottom - end; at = end; }
|
|
792
|
+
const head = (p.el.querySelector('h1, h2, h3, h4, [role="heading"], header') || p.el).innerText || '';
|
|
793
|
+
if (gap > 40) voids.push({ gap: Math.round(gap), at: Math.round(at + SY), panel: head.trim().split('\n')[0].slice(0, 40) });
|
|
794
|
+
}
|
|
795
|
+
voids.sort((a, b) => b.gap - a.gap);
|
|
796
|
+
const firstScreen = tops.filter((p) => p.top < H - 48).length;
|
|
797
|
+
// the icon-in-a-tinted-square habit: small boxes with a coloured fill holding one icon, repeated
|
|
798
|
+
const chips = []; let tiles = 0;
|
|
799
|
+
for (const el of all) {
|
|
800
|
+
const r = el.getBoundingClientRect(); if (r.width < 24 || r.width > 64 || Math.abs(r.width - r.height) > 4) continue;
|
|
801
|
+
const cs = getComputedStyle(el); const bg = rgba(cs.backgroundColor); if (bg.a < 0.08) continue;
|
|
802
|
+
const icon = el.querySelector('svg, i, img, [class*="icon" i]'); if (!icon || (el.innerText || '').trim().length > 2) continue;
|
|
803
|
+
const c = hsl(bg); if (c.s < 0.18 && bg.a > 0.5) { if (r.width >= 28 && !el.closest('button, a, nav, aside, [role="navigation"]')) tiles++; continue; } // neutral: an avatar, a button, or a boxed icon
|
|
804
|
+
chips.push({ hue: c.h, size: Math.round(r.width), radius: cs.borderTopLeftRadius });
|
|
805
|
+
}
|
|
806
|
+
const chipHues = new Set(chips.map((c) => Math.round(c.hue / 30)));
|
|
807
|
+
// 5. numbers: tables and KPI figures
|
|
808
|
+
const numRe = /^[−\-+]?[$€£¥]?\s?\d[\d,.\s]*(%|[kKmMbB]|\s?(ms|s|h|min|GB|MB|kB|x))?$/;
|
|
809
|
+
const cells = [...B.querySelectorAll('td, [role="cell"], [role="gridcell"]')].filter((el) => numRe.test((el.innerText || '').trim()));
|
|
810
|
+
const leftNums = cells.filter((el) => { const a = getComputedStyle(el).textAlign; const inner = el.firstElementChild ? getComputedStyle(el.firstElementChild).textAlign : a; return /left|start/.test(a) && /left|start/.test(inner) && getComputedStyle(el).justifyContent !== 'flex-end'; }).length;
|
|
811
|
+
const figures = [];
|
|
812
|
+
for (const el of all) { if (el.children.length) continue; const t = (el.innerText || '').trim(); if (!numRe.test(t) || t.length < 2) continue; const cs = getComputedStyle(el); if (!shown(el, cs)) continue; figures.push({ size: parseFloat(cs.fontSize), tabular: /tabular-nums/.test(cs.fontVariantNumeric) || /"tnum"/.test(cs.fontFeatureSettings), weight: +cs.fontWeight }); }
|
|
813
|
+
const tabularShare = figures.length ? Math.round((figures.filter((f) => f.tabular).length / figures.length) * 100) / 100 : null;
|
|
814
|
+
const rows = [...B.querySelectorAll('tbody tr, [role="row"]')].map((tr) => Math.round(tr.getBoundingClientRect().height)).filter((h) => h > 0);
|
|
815
|
+
// 6. chart colours: saturated fills and strokes inside SVG charts and canvas-free charts
|
|
816
|
+
const hues = new Map();
|
|
817
|
+
for (const svg of B.querySelectorAll('svg')) {
|
|
818
|
+
const r = svg.getBoundingClientRect(); if (r.width < 120 || r.height < 60) continue;
|
|
819
|
+
for (const s of svg.querySelectorAll('path, rect, circle, line, polyline, polygon')) {
|
|
820
|
+
const cs = getComputedStyle(s);
|
|
821
|
+
for (const c of [cs.fill, cs.stroke]) { if (!c || c === 'none' || c.startsWith('url')) continue; const v = rgba(c); if (v.a < 0.3) continue; const h = hsl(v); if (h.s < 0.25 || h.l < 0.12 || h.l > 0.92) continue; const k = Math.round(h.h / 24); hues.set(k, (hues.get(k) || 0) + 1); }
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// bars and segments drawn with divs count too (a common way to fake a chart)
|
|
825
|
+
// 7. placeholder data
|
|
826
|
+
const text = B.innerText || '';
|
|
827
|
+
const placeholders = [...new Set((text.match(/\b(John|Jane) Doe\b|\bAcme( Inc| Corp)?\b|\bLorem ipsum\b|\bexample\.com\b|\bUser Name\b|\bFirstname Lastname\b|\b123 Main St\b|\bfoo(bar)?\b/gi) || []).map((s) => s.toLowerCase()))];
|
|
828
|
+
// 8. type at app sizes
|
|
829
|
+
const sizes = new Map(); let tiny = 0; const tinySamples = [];
|
|
830
|
+
const walker = document.createTreeWalker(B, NodeFilter.SHOW_TEXT);
|
|
831
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) { const t = n.nodeValue.trim(); if (t.length < 2 || !n.parentElement) continue; const cs = getComputedStyle(n.parentElement); if (cs.visibility === 'hidden' || cs.display === 'none') continue; const s = Math.round(parseFloat(cs.fontSize)); sizes.set(s, (sizes.get(s) || 0) + t.length); if (s < 11 && t.length >= 3) { tiny++; if (tinySamples.length < 4) tinySamples.push(t.slice(0, 24) + ' ' + s + 'px'); } }
|
|
832
|
+
const total = [...sizes.values()].reduce((a, b) => a + b, 0) || 1;
|
|
833
|
+
const under13 = Math.round(([...sizes.entries()].filter(([s]) => s < 13).reduce((a, [, c]) => a + c, 0) / total) * 100) / 100;
|
|
834
|
+
const h1 = document.querySelector('h1'); const h1r = h1 && h1.getBoundingClientRect();
|
|
835
|
+
// the screen's hero: its h1 when that is on the first screen, else the largest text there. Text is read with a
|
|
836
|
+
// space between element children, so a figure and its unit ("£55.94" and "at cost") do not run together.
|
|
837
|
+
const spaced = (el) => { const out = []; const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); for (let n = w.nextNode(); n; n = w.nextNode()) { const t = n.nodeValue.replace(/\s+/g, ' ').trim(); if (t && n.parentElement && !n.parentElement.closest('svg, script, style') && getComputedStyle(n.parentElement).display !== 'none') out.push(t); } return out.join(' ').replace(/\s+([,.;:!?%])/g, '$1').trim(); };
|
|
838
|
+
let heroEl = h1 && h1r && h1r.width > 0 && h1r.top + SY < H * 0.95 ? h1 : null;
|
|
839
|
+
if (!heroEl) {
|
|
840
|
+
let best = null; const hw = document.createTreeWalker(B, NodeFilter.SHOW_TEXT);
|
|
841
|
+
for (let n = hw.nextNode(); n; n = hw.nextNode()) {
|
|
842
|
+
const el = n.parentElement; if (!el || n.nodeValue.trim().length < 2 || el.closest('svg, nav, aside, [role="navigation"]')) continue;
|
|
843
|
+
const r = el.getBoundingClientRect(); if (r.top + SY > H * 0.95 || r.width < 1 || (sidebar && r.left < sidebar.w - 2)) continue;
|
|
844
|
+
const cs = getComputedStyle(el); if (cs.visibility === 'hidden' || +cs.opacity < 0.05) continue;
|
|
845
|
+
const size = parseFloat(cs.fontSize); if (!best || size > best.size) best = { el, size };
|
|
846
|
+
}
|
|
847
|
+
if (best) { heroEl = best.el; for (let p = heroEl; p && p !== B; p = p.parentElement) if (!/^inline/.test(getComputedStyle(p).display)) { heroEl = p; break; } }
|
|
848
|
+
}
|
|
849
|
+
let hero = null;
|
|
850
|
+
if (heroEl) {
|
|
851
|
+
const big = [...heroEl.querySelectorAll('*'), heroEl].map((e) => ({ e, s: parseFloat(getComputedStyle(e).fontSize) })).filter((o) => [...o.e.childNodes].some((c) => c.nodeType === 3 && c.nodeValue.trim())).sort((a, b) => b.s - a.s)[0]?.e || heroEl;
|
|
852
|
+
const cs = getComputedStyle(big), size = parseFloat(cs.fontSize), txt = spaced(heroEl), r = heroEl.getBoundingClientRect();
|
|
853
|
+
const tops2 = []; const lw = document.createTreeWalker(heroEl, NodeFilter.SHOW_TEXT); const lr = document.createRange();
|
|
854
|
+
for (let n = lw.nextNode(); n; n = lw.nextNode()) { if (!n.nodeValue.trim()) continue; lr.selectNodeContents(n); for (const rc of lr.getClientRects()) { const mid = rc.top + rc.height / 2; if (rc.width >= 1 && !tops2.some((t) => Math.abs(t - mid) < size * 0.55)) tops2.push(mid); } }
|
|
855
|
+
const lh = parseFloat(cs.lineHeight);
|
|
856
|
+
hero = { text: txt.slice(0, 200), words: txt.split(' ').filter(Boolean).length, chars: txt.length, lines: Math.max(1, tops2.length), size: Math.round(size), vw: Math.round((size / W) * 1000) / 10, font: cs.fontFamily.split(',')[0].replace(/["']/g, '').trim(), weight: +cs.fontWeight, tracking: Math.round(((parseFloat(cs.letterSpacing) || 0) / size) * 100) / 100, leading: Number.isFinite(lh) ? Math.round((lh / size) * 100) / 100 : 1.2, top: Math.round(r.top + SY), tag: heroEl.tagName.toLowerCase(), kind: 'heading', align: 'left', position: 'left' };
|
|
857
|
+
}
|
|
858
|
+
// charts and timelines on the screen (for the data-hue check): sizeable SVGs with marks, or blocks named as one
|
|
859
|
+
const chartRe = /(^|[\s_-])(chart|plot|graph|timeline|calendar|cal|gantt|heatmap|matrix|strip)([\s_-]|$)/i;
|
|
860
|
+
const chartCount = [...B.querySelectorAll('svg')].filter((s) => { if (s.parentElement && s.parentElement.closest('svg')) return false; const r = s.getBoundingClientRect(); return r.width >= 200 && r.height >= 80 && s.querySelectorAll('path, rect, line, polyline, circle').length >= 6 && shown(s); }).length
|
|
861
|
+
+ [...B.querySelectorAll('[class]')].filter((e) => typeof e.className === 'string' && chartRe.test(e.className) && !e.closest('svg') && (() => { const r = e.getBoundingClientRect(); return r.width >= 200 && r.height >= 80 && shown(e); })()).length;
|
|
862
|
+
return {
|
|
863
|
+
viewport: { w: W, h: H }, dark: lum(pageBg) < 0.2, ground: hex(pageBg),
|
|
864
|
+
sidebar, topbar, canvas, hero, charts: chartCount,
|
|
865
|
+
layout: { panels: tops.length, firstScreen, ragged: ragged.slice(0, 6), voids: voids.slice(0, 4) },
|
|
866
|
+
tiles,
|
|
867
|
+
panels: { count: panels.length, radii: [...radii.entries()].sort((a, b) => b[1] - a[1]).slice(0, 6), heavyBorders, borderSamples, heavyShadows, softShadows, shadowSamples, glass },
|
|
868
|
+
chips: { count: chips.length, hues: chipHues.size, samples: chips.slice(0, 4) },
|
|
869
|
+
numbers: { cells: cells.length, leftAligned: leftNums, figures: figures.length, tabularShare, kpiMax: figures.length ? Math.max(...figures.map((f) => f.size)) : null },
|
|
870
|
+
rows: rows.length ? { count: rows.length, median: rows.sort((a, b) => a - b)[Math.floor(rows.length / 2)] } : null,
|
|
871
|
+
chartHues: hues.size,
|
|
872
|
+
placeholders,
|
|
873
|
+
type: { under13, tiny, tinySamples, sizes: [...sizes.keys()].sort((a, b) => a - b) },
|
|
874
|
+
title: h1 && h1r.width > 0 ? { text: (h1.innerText || '').trim().slice(0, 60), size: Math.round(parseFloat(getComputedStyle(h1).fontSize)), top: Math.round(h1r.top + SY) } : null,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Share of a screenshot's pixels that are strongly coloured (saturated, neither near-black nor near-white).
|
|
879
|
+
// The studied app screens keep it near 3%; pale tints barely count. Runs inside any page; takes a data: URL.
|
|
880
|
+
export async function colourShare(dataUrl) {
|
|
881
|
+
const img = new Image(); img.src = dataUrl; await img.decode();
|
|
882
|
+
const w = 240, h = Math.max(1, Math.round((240 * img.height) / img.width));
|
|
883
|
+
const c = document.createElement('canvas'); c.width = w; c.height = h;
|
|
884
|
+
const x = c.getContext('2d', { willReadFrequently: true }); x.drawImage(img, 0, 0, w, h);
|
|
885
|
+
const d = x.getImageData(0, 0, w, h).data; let n = 0;
|
|
886
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
887
|
+
const r = d[i] / 255, g = d[i + 1] / 255, b = d[i + 2] / 255, mx = Math.max(r, g, b), mn = Math.min(r, g, b), l = (mx + mn) / 2;
|
|
888
|
+
const s = mx === mn ? 0 : l > 0.5 ? (mx - mn) / (2 - mx - mn) : (mx - mn) / (mx + mn);
|
|
889
|
+
if (s >= 0.4 && l >= 0.18 && l <= 0.82) n++;
|
|
890
|
+
}
|
|
891
|
+
return Math.round((n / (w * h)) * 1000) / 1000;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Dominant colours of a screenshot. Runs inside any page; takes a data: URL.
|
|
895
|
+
export async function palette(dataUrl) {
|
|
896
|
+
const img = new Image(); img.src = dataUrl; await img.decode();
|
|
897
|
+
const w = 96, h = Math.max(1, Math.round((96 * img.height) / img.width));
|
|
898
|
+
const c = document.createElement('canvas'); c.width = w; c.height = h;
|
|
899
|
+
const x = c.getContext('2d', { willReadFrequently: true }); x.drawImage(img, 0, 0, w, h);
|
|
900
|
+
const d = x.getImageData(0, 0, w, h).data; const bins = new Map();
|
|
901
|
+
for (let i = 0; i < d.length; i += 4) {
|
|
902
|
+
const k = (d[i] >> 4) << 8 | (d[i + 1] >> 4) << 4 | (d[i + 2] >> 4);
|
|
903
|
+
const b = bins.get(k) || [0, 0, 0, 0]; b[0] += d[i]; b[1] += d[i + 1]; b[2] += d[i + 2]; b[3]++; bins.set(k, b);
|
|
904
|
+
}
|
|
905
|
+
const total = w * h;
|
|
906
|
+
return [...bins.values()].sort((a, b) => b[3] - a[3]).slice(0, 6).map((b) => ({
|
|
907
|
+
color: '#' + [0, 1, 2].map((i) => Math.round(b[i] / b[3]).toString(16).padStart(2, '0')).join(''), share: Math.round((b[3] / total) * 100) / 100,
|
|
908
|
+
}));
|
|
909
|
+
}
|