quiver-cli 0.8.0 → 1.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/README.md +127 -45
- package/bin/quiver-cli.mjs +3 -1
- package/dist/cli.js +666 -436
- package/package.json +2 -2
- package/template/.agents/AGENTS.md +4 -3
- package/template/.agents/config.json +7 -0
- package/template/.agents/plugins/opencode/rtk.ts +34 -0
- package/template/.agents/skills/agent-browser/SKILL.md +1 -0
- package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
- package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
- package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
- package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
- package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
- package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
- package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
- package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
- package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
- package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
- package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
- package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
- package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
- package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
- package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
- package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
- package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
- package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
- package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
- package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
- package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
- package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
- package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
- package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
- package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
- package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
- package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
- package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
- package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
- package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
- package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
- package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
- package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
- package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
- package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
- package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
- package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
- package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
- package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
- package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
- package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
- package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
- package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
- package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
- package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
- package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
- package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
- package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
- package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
- package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
- package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
- package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
- package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
- package/template/.agents/skills/design/shadcn/cli.md +49 -16
- package/template/.agents/skills/design/shadcn/customization.md +14 -7
- package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
- package/template/.agents/skills/design/shadcn/mcp.md +27 -16
- package/template/.agents/skills/design/shadcn/registry.md +277 -0
- package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
- package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
- package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
- package/template/.agents/skills/find-skills/SKILL.md +2 -3
- package/template/.agents/skills/hono/SKILL.md +579 -0
- package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
- package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
- package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
- package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
- package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
- package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
- package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
- package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
- package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
- package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
- package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
- package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
- package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
- package/template/.agents/skills/supabase/SKILL.md +145 -0
- package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
- package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/template/.agents/upstreams.json +34 -12
- package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
- package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
- package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
- package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { profileStep, recordProfileEvent } from '../../profile/profiler.mjs';
|
|
5
|
-
import { parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs';
|
|
5
|
+
import { CSS_NAMED_COLORS, collectCssCustomProps, cssLengthToPx, parseAnyColor, resolveLengthPx, resolveVarRefs } from '../../rules/checks.mjs';
|
|
6
6
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// jsdom CSS-variable border override map
|
|
@@ -223,7 +223,7 @@ function unwrapCssAtLayer(source) {
|
|
|
223
223
|
// ---------------------------------------------------------------------------
|
|
224
224
|
|
|
225
225
|
const STATIC_INHERITED_PROPS = new Set([
|
|
226
|
-
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight',
|
|
226
|
+
'color', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'fontVariant',
|
|
227
227
|
'lineHeight', 'letterSpacing', 'textTransform', 'textAlign', 'hyphens',
|
|
228
228
|
'webkitHyphens',
|
|
229
229
|
]);
|
|
@@ -245,9 +245,14 @@ const STATIC_DEFAULT_STYLE = {
|
|
|
245
245
|
outlineColor: 'rgb(0, 0, 0)',
|
|
246
246
|
outlineStyle: 'none',
|
|
247
247
|
boxShadow: 'none',
|
|
248
|
+
// NOT in STATIC_INHERITED_PROPS even though text-shadow inherits in real
|
|
249
|
+
// CSS: the glow check only needs to fire once, on the element that
|
|
250
|
+
// declares the shadow, not on every descendant.
|
|
251
|
+
textShadow: 'none',
|
|
248
252
|
fontFamily: '',
|
|
249
253
|
fontSize: '16px',
|
|
250
254
|
fontStyle: 'normal',
|
|
255
|
+
fontVariant: 'normal',
|
|
251
256
|
fontWeight: '400',
|
|
252
257
|
lineHeight: 'normal',
|
|
253
258
|
letterSpacing: 'normal',
|
|
@@ -302,6 +307,7 @@ const STATIC_PROP_MAP = {
|
|
|
302
307
|
'outline-color': 'outlineColor',
|
|
303
308
|
'outline-style': 'outlineStyle',
|
|
304
309
|
'box-shadow': 'boxShadow',
|
|
310
|
+
'text-shadow': 'textShadow',
|
|
305
311
|
'font-family': 'fontFamily',
|
|
306
312
|
'font-size': 'fontSize',
|
|
307
313
|
'font-style': 'fontStyle',
|
|
@@ -339,18 +345,29 @@ const STATIC_PROP_MAP = {
|
|
|
339
345
|
'overflow-y': 'overflowY',
|
|
340
346
|
};
|
|
341
347
|
|
|
348
|
+
// parseStaticColor tries parseAnyColor first, which already resolves every
|
|
349
|
+
// name in the shared CSS_NAMED_COLORS table. This fallback only carries the
|
|
350
|
+
// keywords parseAnyColor deliberately returns null for: the cascade needs
|
|
351
|
+
// `transparent` to read as an actual zero-alpha color.
|
|
342
352
|
const STATIC_NAMED_COLORS = {
|
|
343
|
-
black: { r: 0, g: 0, b: 0, a: 1 },
|
|
344
|
-
white: { r: 255, g: 255, b: 255, a: 1 },
|
|
345
353
|
transparent: { r: 0, g: 0, b: 0, a: 0 },
|
|
346
|
-
gray: { r: 128, g: 128, b: 128, a: 1 },
|
|
347
|
-
grey: { r: 128, g: 128, b: 128, a: 1 },
|
|
348
|
-
silver: { r: 192, g: 192, b: 192, a: 1 },
|
|
349
|
-
red: { r: 255, g: 0, b: 0, a: 1 },
|
|
350
|
-
green: { r: 0, g: 128, b: 0, a: 1 },
|
|
351
|
-
blue: { r: 0, g: 0, b: 255, a: 1 },
|
|
352
354
|
};
|
|
353
355
|
|
|
356
|
+
// Named-color alternation for plucking a color token out of shorthand values
|
|
357
|
+
// (issue #359: a hardcoded 9-name list here silently dropped `purple`,
|
|
358
|
+
// `crimson`, `teal`, ... from border shorthands, so the side defaulted to
|
|
359
|
+
// neutral black and side-tab never fired on .html files). Derived from the
|
|
360
|
+
// same table parseAnyColor resolves against, so extraction and parsing can't
|
|
361
|
+
// drift apart. Longest-first so names containing other names as substrings
|
|
362
|
+
// (rebeccapurple) are matched whole.
|
|
363
|
+
const NAMED_COLOR_TOKENS = [...Object.keys(CSS_NAMED_COLORS), ...Object.keys(STATIC_NAMED_COLORS)]
|
|
364
|
+
.sort((a, b) => b.length - a.length)
|
|
365
|
+
.join('|');
|
|
366
|
+
const STATIC_COLOR_TOKEN_RE = new RegExp(
|
|
367
|
+
`(?:rgba?\\([^)]+\\)|oklch\\([^)]+\\)|oklab\\([^)]+\\)|lch\\([^)]+\\)|lab\\([^)]+\\)|hsla?\\([^)]+\\)|hwb\\([^)]+\\)|#[0-9a-f]{3,8}\\b|\\b(?:${NAMED_COLOR_TOKENS})\\b)`,
|
|
368
|
+
'i'
|
|
369
|
+
);
|
|
370
|
+
|
|
354
371
|
function splitCssList(value) {
|
|
355
372
|
const parts = [];
|
|
356
373
|
let depth = 0, quote = '', start = 0;
|
|
@@ -420,7 +437,23 @@ function extractStaticColor(value) {
|
|
|
420
437
|
if (!value) return '';
|
|
421
438
|
const raw = String(value).trim();
|
|
422
439
|
if (/^var\(/i.test(raw)) return raw;
|
|
423
|
-
|
|
440
|
+
// color-mix(...) needs balanced-paren capture (its arguments regularly
|
|
441
|
+
// contain nested var()/oklch() calls AND the keyword `transparent`, which
|
|
442
|
+
// the flat regex below would otherwise pluck out of the middle of the
|
|
443
|
+
// expression and report as the whole color).
|
|
444
|
+
const mixStart = raw.search(/color-mix\(/i);
|
|
445
|
+
if (mixStart !== -1) {
|
|
446
|
+
let depth = 0;
|
|
447
|
+
for (let i = raw.indexOf('(', mixStart); i < raw.length; i++) {
|
|
448
|
+
if (raw[i] === '(') depth++;
|
|
449
|
+
else if (raw[i] === ')') {
|
|
450
|
+
depth--;
|
|
451
|
+
if (depth === 0) return raw.slice(mixStart, i + 1);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return '';
|
|
455
|
+
}
|
|
456
|
+
const colorLike = raw.match(STATIC_COLOR_TOKEN_RE);
|
|
424
457
|
if (!colorLike) return '';
|
|
425
458
|
return colorLike[0];
|
|
426
459
|
}
|
|
@@ -532,6 +565,15 @@ function expandStaticDeclaration(prop, value) {
|
|
|
532
565
|
const beforeImage = hasImage ? v.split(/(?:repeating-)?(?:linear|radial|conic)-gradient\(|url\(/i)[0] : v;
|
|
533
566
|
const color = extractStaticColor(hasImage ? beforeImage : v);
|
|
534
567
|
if (color) out.push(['backgroundColor', color]);
|
|
568
|
+
// The `background` shorthand resets every longhand it does not set.
|
|
569
|
+
// Without this, `pre code { background: none }` leaves an earlier
|
|
570
|
+
// `background: var(--surface)` color standing and the contrast checks
|
|
571
|
+
// measure text against a surface the browser never paints. var() values
|
|
572
|
+
// stay untouched: they may resolve to a color later in the pipeline.
|
|
573
|
+
if (!color && !hasImage && !/var\(/i.test(v)) {
|
|
574
|
+
out.push(['backgroundColor', 'rgba(0, 0, 0, 0)']);
|
|
575
|
+
out.push(['backgroundImage', 'none']);
|
|
576
|
+
}
|
|
535
577
|
return out;
|
|
536
578
|
}
|
|
537
579
|
if (p === 'border') {
|
|
@@ -702,7 +744,20 @@ function collectStaticCssRules(cssText, csstree) {
|
|
|
702
744
|
});
|
|
703
745
|
});
|
|
704
746
|
for (const selector of splitCssList(selectorText)) {
|
|
705
|
-
if (selector)
|
|
747
|
+
if (!selector) continue;
|
|
748
|
+
// :hover rules can't be matched statically as-is (no interaction
|
|
749
|
+
// state), but they carry real cascade weight while hovered. Tag
|
|
750
|
+
// them and record a state-stripped selector so the hover pass can
|
|
751
|
+
// find their targets; specificity stays computed from the ORIGINAL
|
|
752
|
+
// selector (per CSS, :hover counts as a class).
|
|
753
|
+
const isHover = /:hover\b/i.test(selector);
|
|
754
|
+
let matchSelector = null;
|
|
755
|
+
if (isHover) {
|
|
756
|
+
matchSelector = selector.replace(/:hover\b/gi, '').trim();
|
|
757
|
+
if (!matchSelector || /[>+~]\s*$/.test(matchSelector)) matchSelector = null;
|
|
758
|
+
else matchSelector = matchSelector.replace(/(^|[\s>+~])(?=$|[\s>+~])/g, '$1*');
|
|
759
|
+
}
|
|
760
|
+
rules.push({ selector, declarations, specificity: staticSpecificity(selector), order: order++, isHover, matchSelector });
|
|
706
761
|
}
|
|
707
762
|
return;
|
|
708
763
|
}
|
|
@@ -805,6 +860,13 @@ class StaticDocument {
|
|
|
805
860
|
this.domutils = modules.domutils;
|
|
806
861
|
this._wrappers = new WeakMap();
|
|
807
862
|
this._styleMap = new WeakMap();
|
|
863
|
+
this._hoverStyleMap = new WeakMap();
|
|
864
|
+
this._accentDashPseudo = new WeakSet();
|
|
865
|
+
// Elements whose ::before/::after paints a full-cover opaque surface
|
|
866
|
+
// (position absolute/fixed + inset 0 + solid background). The pseudo is
|
|
867
|
+
// the element's visible background for contrast purposes even though it
|
|
868
|
+
// never joins the element cascade.
|
|
869
|
+
this._pseudoSurface = new WeakMap();
|
|
808
870
|
}
|
|
809
871
|
wrap(node) {
|
|
810
872
|
let wrapped = this._wrappers.get(node);
|
|
@@ -841,6 +903,24 @@ class StaticDocument {
|
|
|
841
903
|
getStyle(el) {
|
|
842
904
|
return this._styleMap.get(el.node) || makeStaticStyle();
|
|
843
905
|
}
|
|
906
|
+
setHoverStyle(node, style) {
|
|
907
|
+
this._hoverStyleMap.set(node, style);
|
|
908
|
+
}
|
|
909
|
+
getHoverStyle(el) {
|
|
910
|
+
return this._hoverStyleMap.get(el.node) || null;
|
|
911
|
+
}
|
|
912
|
+
setAccentDashPseudo(node) {
|
|
913
|
+
this._accentDashPseudo.add(node);
|
|
914
|
+
}
|
|
915
|
+
hasAccentDashPseudo(el) {
|
|
916
|
+
return this._accentDashPseudo.has(el.node);
|
|
917
|
+
}
|
|
918
|
+
setPseudoSurface(node, color) {
|
|
919
|
+
this._pseudoSurface.set(node, color);
|
|
920
|
+
}
|
|
921
|
+
getPseudoSurface(el) {
|
|
922
|
+
return this._pseudoSurface.get(el.node) || null;
|
|
923
|
+
}
|
|
844
924
|
}
|
|
845
925
|
|
|
846
926
|
function makeStaticStyle(values = {}) {
|
|
@@ -856,6 +936,9 @@ function buildStaticWindow(staticDoc) {
|
|
|
856
936
|
return {
|
|
857
937
|
document: staticDoc,
|
|
858
938
|
getComputedStyle: (el) => staticDoc.getStyle(el),
|
|
939
|
+
getHoverStyle: (el) => staticDoc.getHoverStyle(el),
|
|
940
|
+
hasAccentDashPseudo: (el) => staticDoc.hasAccentDashPseudo(el),
|
|
941
|
+
getPseudoSurface: (el) => staticDoc.getPseudoSurface(el),
|
|
859
942
|
};
|
|
860
943
|
}
|
|
861
944
|
|
|
@@ -869,7 +952,10 @@ function collectStaticCssText(root, fileDir, profile, filePath, modules) {
|
|
|
869
952
|
const rel = link.attribs?.rel || '';
|
|
870
953
|
const href = link.attribs?.href || '';
|
|
871
954
|
if (!/\bstylesheet\b/i.test(rel) || !href || /^(https?:)?\/\//i.test(href)) continue;
|
|
872
|
-
|
|
955
|
+
// Cache-busting hrefs (styles.css?v=3) resolve to the file, not to a
|
|
956
|
+
// literal path with the query in it; a versioned link otherwise made the
|
|
957
|
+
// whole stylesheet invisible to every element-level check.
|
|
958
|
+
const cssPath = path.resolve(fileDir, href.split(/[?#]/)[0]);
|
|
873
959
|
try {
|
|
874
960
|
const css = profileStep(profile, {
|
|
875
961
|
engine: 'static-html',
|
|
@@ -886,6 +972,13 @@ function collectStaticCssText(root, fileDir, profile, filePath, modules) {
|
|
|
886
972
|
|
|
887
973
|
function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePath) {
|
|
888
974
|
const specified = new Map();
|
|
975
|
+
// Declarations from :hover rules, matched via their state-stripped
|
|
976
|
+
// selectors. Merged per-property against the resting cascade in
|
|
977
|
+
// computeNode — a hover declaration only takes effect if it would win
|
|
978
|
+
// the cascade while the element is hovered (all resting rules still
|
|
979
|
+
// apply in that state).
|
|
980
|
+
const hoverSpecified = new Map();
|
|
981
|
+
const rootCustomProps = collectCssCustomProps(cssText);
|
|
889
982
|
const allNodes = modules.selectAll('*', root.children || []);
|
|
890
983
|
const rules = profileStep(profile, {
|
|
891
984
|
engine: 'static-html',
|
|
@@ -901,9 +994,65 @@ function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePat
|
|
|
901
994
|
target: filePath,
|
|
902
995
|
}, () => {
|
|
903
996
|
for (const rule of rules) {
|
|
997
|
+
// ::before/::after rules can't join the element cascade (pseudo
|
|
998
|
+
// elements aren't DOM nodes), but one shape matters to the eyebrow
|
|
999
|
+
// check: the short chromatic "kicker dash" (content box 8-80px wide,
|
|
1000
|
+
// 1-6px tall, accent-colored fill). Mark the base-selector matches
|
|
1001
|
+
// so checkElementHeroEyebrow can see the dash.
|
|
1002
|
+
if (!rule.isHover) {
|
|
1003
|
+
const pm = rule.selector.match(/^(.+?)\s*::?(?:before|after)$/i);
|
|
1004
|
+
if (pm) {
|
|
1005
|
+
const decls = new Map();
|
|
1006
|
+
for (const d of rule.declarations) decls.set(d.prop.toLowerCase(), d.value);
|
|
1007
|
+
const w = cssLengthToPx(resolveVarRefs(decls.get('width') || decls.get('inline-size') || '', rootCustomProps));
|
|
1008
|
+
const h = cssLengthToPx(resolveVarRefs(decls.get('height') || decls.get('block-size') || '', rootCustomProps));
|
|
1009
|
+
if (w != null && h != null && w >= 8 && w <= 80 && h >= 1 && h <= 6) {
|
|
1010
|
+
const bgRaw = String(resolveVarRefs(decls.get('background-color') || decls.get('background') || '', rootCustomProps));
|
|
1011
|
+
const token = bgRaw.match(/(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b/i);
|
|
1012
|
+
const c = parseAnyColor(token ? token[0] : bgRaw);
|
|
1013
|
+
if (c && (c.a ?? 1) >= 0.1 && Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) >= 30) {
|
|
1014
|
+
try {
|
|
1015
|
+
for (const node of modules.selectAll(pm[1], root.children || [])) {
|
|
1016
|
+
staticDoc.setAccentDashPseudo(node);
|
|
1017
|
+
}
|
|
1018
|
+
} catch { /* unsupported base selector */ }
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// Full-cover surface pseudo: the CTA construction where the
|
|
1022
|
+
// element itself stays transparent and a ::before/::after with
|
|
1023
|
+
// position absolute/fixed + inset 0 (or all four sides 0, or
|
|
1024
|
+
// 100% width and height) plus an opaque background paints the
|
|
1025
|
+
// visible surface. Mark base-selector matches so the contrast
|
|
1026
|
+
// checks measure text against the surface the browser renders.
|
|
1027
|
+
const pseudoPos = String(decls.get('position') || '').toLowerCase();
|
|
1028
|
+
if (pseudoPos === 'absolute' || pseudoPos === 'fixed') {
|
|
1029
|
+
const zeroLen = v => v != null && /^0(?:px)?$/.test(String(v).trim());
|
|
1030
|
+
const insetRaw = String(decls.get('inset') || '').trim();
|
|
1031
|
+
const coversBox = (insetRaw !== '' && insetRaw.split(/\s+/).every(t => /^0(?:px)?$/.test(t)))
|
|
1032
|
+
|| ['top', 'right', 'bottom', 'left'].every(side => zeroLen(decls.get(side)))
|
|
1033
|
+
|| (String(decls.get('width') || '').trim() === '100%'
|
|
1034
|
+
&& String(decls.get('height') || '').trim() === '100%');
|
|
1035
|
+
if (coversBox && decls.has('content')) {
|
|
1036
|
+
const surfRaw = String(resolveVarRefs(decls.get('background-color') || decls.get('background') || '', rootCustomProps));
|
|
1037
|
+
const surfToken = surfRaw.match(/(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color-mix)\([^)]*(?:\([^)]*\))?[^)]*\)|#[0-9a-f]{3,8}\b/i);
|
|
1038
|
+
const surf = parseAnyColor(surfToken ? surfToken[0] : surfRaw);
|
|
1039
|
+
if (surf && (surf.a ?? 1) >= 0.9 && !/gradient/i.test(surfRaw)) {
|
|
1040
|
+
try {
|
|
1041
|
+
for (const node of modules.selectAll(pm[1], root.children || [])) {
|
|
1042
|
+
staticDoc.setPseudoSurface(node, surf);
|
|
1043
|
+
}
|
|
1044
|
+
} catch { /* unsupported base selector */ }
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const matchSelector = rule.isHover ? rule.matchSelector : rule.selector;
|
|
1052
|
+
if (!matchSelector) continue;
|
|
904
1053
|
let matched;
|
|
905
1054
|
try {
|
|
906
|
-
matched = modules.selectAll(
|
|
1055
|
+
matched = modules.selectAll(matchSelector, root.children || []);
|
|
907
1056
|
} catch {
|
|
908
1057
|
recordProfileEvent(profile, {
|
|
909
1058
|
engine: 'static-html',
|
|
@@ -912,13 +1061,13 @@ function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePat
|
|
|
912
1061
|
target: filePath,
|
|
913
1062
|
ms: 0,
|
|
914
1063
|
findings: 0,
|
|
915
|
-
detail:
|
|
1064
|
+
detail: matchSelector,
|
|
916
1065
|
});
|
|
917
1066
|
continue;
|
|
918
1067
|
}
|
|
919
1068
|
for (const node of matched) {
|
|
920
1069
|
for (const decl of rule.declarations) {
|
|
921
|
-
applyStaticDeclaration(specified, node, decl.prop, decl.value, {
|
|
1070
|
+
applyStaticDeclaration(rule.isHover ? hoverSpecified : specified, node, decl.prop, decl.value, {
|
|
922
1071
|
important: decl.important,
|
|
923
1072
|
specificity: rule.specificity,
|
|
924
1073
|
order: rule.order,
|
|
@@ -961,6 +1110,28 @@ function buildStaticStyleMap(root, staticDoc, cssText, modules, profile, filePat
|
|
|
961
1110
|
}
|
|
962
1111
|
const style = makeStaticStyle(values);
|
|
963
1112
|
staticDoc.setStyle(node, style);
|
|
1113
|
+
|
|
1114
|
+
// Hover pass: limited to the two properties the hover-contrast check
|
|
1115
|
+
// consumes. A hover declaration wins only if it beats the resting
|
|
1116
|
+
// winner for that property under normal cascade rules (specificity /
|
|
1117
|
+
// order / importance) — exactly what a browser computes while the
|
|
1118
|
+
// element is hovered.
|
|
1119
|
+
const hoverMap = hoverSpecified.get(node);
|
|
1120
|
+
if (hoverMap) {
|
|
1121
|
+
let hoverValues = null;
|
|
1122
|
+
for (const prop of ['color', 'backgroundColor']) {
|
|
1123
|
+
const hoverDecl = hoverMap.get(prop);
|
|
1124
|
+
if (!hoverDecl) continue;
|
|
1125
|
+
const restingDecl = specifiedMap.get(prop);
|
|
1126
|
+
if (!compareStaticPriority(restingDecl, hoverDecl)) continue;
|
|
1127
|
+
const next = normalizeStaticCssValue(prop, hoverDecl.value, customProps, parentStyle, values);
|
|
1128
|
+
if (next === values[prop]) continue;
|
|
1129
|
+
if (!hoverValues) hoverValues = { ...values };
|
|
1130
|
+
hoverValues[prop] = next;
|
|
1131
|
+
}
|
|
1132
|
+
if (hoverValues) staticDoc.setHoverStyle(node, makeStaticStyle(hoverValues));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
964
1135
|
for (const child of node.children || []) {
|
|
965
1136
|
if (child.type === 'tag') computeNode(child, style, customProps);
|
|
966
1137
|
}
|
|
@@ -18,20 +18,23 @@ import {
|
|
|
18
18
|
checkElementGlow,
|
|
19
19
|
checkElementGptBorderShadow,
|
|
20
20
|
checkElementHeroEyebrow,
|
|
21
|
+
checkElementHoverContrast,
|
|
21
22
|
checkElementIconTile,
|
|
22
23
|
checkElementItalicSerif,
|
|
23
24
|
checkElementMotion,
|
|
24
25
|
checkElementOversizedH1,
|
|
25
26
|
checkElementQuality,
|
|
27
|
+
checkElementRadialSpotlight,
|
|
26
28
|
checkCreamPalette,
|
|
27
29
|
checkHtmlPatterns,
|
|
30
|
+
checkKickerAboveHeadingFromDoc,
|
|
31
|
+
checkNumberedSectionLabelsFromDoc,
|
|
28
32
|
checkPageLayout,
|
|
29
33
|
checkPageQualityFromDoc,
|
|
30
|
-
|
|
34
|
+
checkRepeatedContainerTextFromDoc,
|
|
31
35
|
resolveBackground,
|
|
32
36
|
resolveBorderRadiusPx,
|
|
33
37
|
} from '../../rules/checks.mjs';
|
|
34
|
-
import { filterByProviders } from '../../registry/antipatterns.mjs';
|
|
35
38
|
import { detectText, runTextContentAnalyzers } from '../regex/detect-text.mjs';
|
|
36
39
|
import {
|
|
37
40
|
StaticDocument,
|
|
@@ -57,9 +60,6 @@ function checkStaticPageTypography(document, window) {
|
|
|
57
60
|
for (const font of overusedFound) {
|
|
58
61
|
findings.push({ id: 'overused-font', snippet: `Primary font: ${font}` });
|
|
59
62
|
}
|
|
60
|
-
if (fonts.size === 1 && document.querySelectorAll('*').length >= 20) {
|
|
61
|
-
findings.push({ id: 'single-font', snippet: `only font used is ${[...fonts][0]}` });
|
|
62
|
-
}
|
|
63
63
|
const sizes = new Set();
|
|
64
64
|
for (const el of document.querySelectorAll('h1, h2, h3, h4, h5, h6, p, span, a, li, td, th, label, button, div')) {
|
|
65
65
|
const fontSize = parseFloat(window.getComputedStyle(el).fontSize);
|
|
@@ -90,8 +90,9 @@ function checkElementBrokenImage(el) {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const STATIC_ELEMENT_RULES = [
|
|
93
|
-
{ id: 'border-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window)) },
|
|
93
|
+
{ id: 'border-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementBorders(tag, style, null, resolveBorderRadiusPx(el, style, parseFloat(style.width) || 0, window), el) },
|
|
94
94
|
{ id: 'color-rules', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementColors(el, style, tag, window, customPropMap, false) },
|
|
95
|
+
{ id: 'hover-color-rules', selector: '*', run: (el, tag, style, window) => checkElementHoverContrast(el, style, tag, window) },
|
|
95
96
|
{ id: 'dark-glow', selector: '*', run: (el, tag, style, window, customPropMap) => checkElementGlow(tag, style, resolveBackground(el.parentElement || el, window, customPropMap)) },
|
|
96
97
|
{ id: 'motion-rules', selector: '*', run: (el, tag, style) => checkElementMotion(tag, style) },
|
|
97
98
|
{ id: 'icon-tile-stack', selector: 'h1,h2,h3,h4,h5,h6', run: (el, tag, _style, window) => checkElementIconTile(el, tag, window) },
|
|
@@ -102,6 +103,7 @@ const STATIC_ELEMENT_RULES = [
|
|
|
102
103
|
{ id: 'oversized-h1', selector: 'h1', run: (el, tag, style, window) => checkElementOversizedH1(el, style, tag, window) },
|
|
103
104
|
{ id: 'clipped-overflow-container', selector: '*', run: (el, tag, style, window) => checkElementClippedOverflow(el, style, tag, window) },
|
|
104
105
|
{ id: 'gpt-thin-border-wide-shadow', selector: '*', run: (el, tag, style) => checkElementGptBorderShadow(el, style) },
|
|
106
|
+
{ id: 'radial-spotlight-glow', selector: '*', run: (el, tag, style, window) => checkElementRadialSpotlight(el, style, tag, window) },
|
|
105
107
|
];
|
|
106
108
|
|
|
107
109
|
async function detectHtml(filePath, options = {}) {
|
|
@@ -197,7 +199,13 @@ async function detectHtml(filePath, options = {}) {
|
|
|
197
199
|
for (const f of runPageCheck('typography-rules', () => checkStaticPageTypography(document, window))) {
|
|
198
200
|
findings.push(finding(f.id, filePath, f.snippet));
|
|
199
201
|
}
|
|
200
|
-
for (const f of runPageCheck('
|
|
202
|
+
for (const f of runPageCheck('kicker-above-heading', () => checkKickerAboveHeadingFromDoc(document, window))) {
|
|
203
|
+
findings.push(finding(f.id, filePath, f.snippet));
|
|
204
|
+
}
|
|
205
|
+
for (const f of runPageCheck('numbered-section-labels', () => checkNumberedSectionLabelsFromDoc(document, window))) {
|
|
206
|
+
findings.push(finding(f.id, filePath, f.snippet));
|
|
207
|
+
}
|
|
208
|
+
for (const f of runPageCheck('repeated-container-text', () => checkRepeatedContainerTextFromDoc(document, window))) {
|
|
201
209
|
findings.push(finding(f.id, filePath, f.snippet));
|
|
202
210
|
}
|
|
203
211
|
for (const f of runPageCheck('layout-rules', () => checkPageLayout(document, window))) {
|
|
@@ -209,10 +217,33 @@ async function detectHtml(filePath, options = {}) {
|
|
|
209
217
|
for (const f of runPageCheck('skipped-heading', () => checkPageQualityFromDoc(document))) {
|
|
210
218
|
findings.push(finding(f.id, filePath, f.snippet));
|
|
211
219
|
}
|
|
212
|
-
|
|
220
|
+
// Scoped corpora for the pattern checks (see buildHtmlPatternCorpora in
|
|
221
|
+
// rules/checks.mjs): CSS-property regexes must not fire on prose ABOUT
|
|
222
|
+
// css — `<code>background-clip: text</code>` in a changelog is
|
|
223
|
+
// documentation, not styling. cssText already carries the <style>
|
|
224
|
+
// blocks and any linked local stylesheets; style/class attributes come
|
|
225
|
+
// from the parsed document, so escaped code samples never contribute.
|
|
226
|
+
const styleAttrParts = [];
|
|
227
|
+
const classAttrParts = [];
|
|
228
|
+
for (const el of document.querySelectorAll('*')) {
|
|
229
|
+
const styleAttr = el.getAttribute('style');
|
|
230
|
+
if (styleAttr) styleAttrParts.push(`style="${styleAttr}"`);
|
|
231
|
+
const classAttr = el.getAttribute('class');
|
|
232
|
+
if (classAttr) classAttrParts.push(classAttr);
|
|
233
|
+
}
|
|
234
|
+
const patternCorpora = {
|
|
235
|
+
styleText: [cssText, ...styleAttrParts].join('\n'),
|
|
236
|
+
classText: classAttrParts.join('\n'),
|
|
237
|
+
};
|
|
238
|
+
for (const f of runPageCheck('html-patterns', () => checkHtmlPatterns(html, patternCorpora).filter(item =>
|
|
213
239
|
item.id !== 'bounce-easing' && item.id !== 'layout-transition'
|
|
214
240
|
))) {
|
|
215
|
-
|
|
241
|
+
const item = finding(f.id, filePath, f.snippet);
|
|
242
|
+
// Position-aware severity promotion: checks may attach a per-finding
|
|
243
|
+
// severity (e.g. a pulsing dot inside a header/nav landmark) that
|
|
244
|
+
// overrides the registry default.
|
|
245
|
+
if (f.severity) item.severity = f.severity;
|
|
246
|
+
findings.push(item);
|
|
216
247
|
}
|
|
217
248
|
// Text-content analyzers (em-dash overuse, marketing buzzwords,
|
|
218
249
|
// numbered section markers, aphoristic cadence) live in the regex
|
|
@@ -224,11 +255,10 @@ async function detectHtml(filePath, options = {}) {
|
|
|
224
255
|
}
|
|
225
256
|
}
|
|
226
257
|
|
|
227
|
-
const byProvider = filterByProviders(findings, options.providers);
|
|
228
258
|
// Static-HTML findings carry no line number, so only whole-file
|
|
229
259
|
// `impeccable-disable` directives apply here — exactly the standalone-document
|
|
230
260
|
// waiver this primitive targets. Bypassed by `--no-config` / `--no-inline-ignores`.
|
|
231
|
-
return options?.inlineIgnores === false ?
|
|
261
|
+
return options?.inlineIgnores === false ? findings : applyInlineIgnores(findings, html);
|
|
232
262
|
}
|
|
233
263
|
|
|
234
264
|
export { checkStaticPageTypography, STATIC_ELEMENT_RULES, detectHtml };
|
|
@@ -6,7 +6,13 @@ function getAP(id) {
|
|
|
6
6
|
|
|
7
7
|
function finding(id, filePath, snippet, line = 0) {
|
|
8
8
|
const ap = getAP(id);
|
|
9
|
-
|
|
9
|
+
const base = { antipattern: id, name: ap.name, description: ap.description, severity: ap.severity || 'warning', category: ap.category || null, file: filePath, line, snippet };
|
|
10
|
+
// Advisory findings are detected but reported separately and never counted as
|
|
11
|
+
// failures. Carry the flag on the finding so every consumer (CLI, JSON, hook)
|
|
12
|
+
// can partition without a registry lookup. Only stamped when true to keep the
|
|
13
|
+
// finding shape stable for the vast majority of rules.
|
|
14
|
+
if (ap.advisory === true) base.advisory = true;
|
|
15
|
+
return base;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export { getAP, finding };
|
|
@@ -5,11 +5,24 @@ import path from 'node:path';
|
|
|
5
5
|
// File walker
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
+
// Hidden directories are skipped wholesale during recursion (below), which
|
|
9
|
+
// covers .git / .next / .nuxt / .svelte-kit / .turbo / .vercel and — the
|
|
10
|
+
// issue #303 class — every vendored AI-harness install (.claude, .cursor,
|
|
11
|
+
// .codex, .agents, .impeccable, ...) whose bundled detector source would
|
|
12
|
+
// otherwise be reported as findings on a root scan. Only the non-hidden
|
|
13
|
+
// build/dependency dirs need naming. An explicitly passed hidden target
|
|
14
|
+
// still scans: walkDir name-checks children, never the root it's given.
|
|
8
15
|
const SKIP_DIRS = new Set([
|
|
9
|
-
'node_modules', '
|
|
10
|
-
'.svelte-kit', '__pycache__', '.turbo', '.vercel',
|
|
16
|
+
'node_modules', 'dist', 'build', '__pycache__',
|
|
11
17
|
]);
|
|
12
18
|
|
|
19
|
+
// The exceptions to the hidden-dir rule: hidden directories that
|
|
20
|
+
// conventionally hold real UI source rather than tooling or vendored code.
|
|
21
|
+
// VitePress and VuePress keep custom theme components in
|
|
22
|
+
// .vitepress/theme/*.vue / .vuepress/theme/, and Storybook keeps preview
|
|
23
|
+
// decorators/styles in .storybook/.
|
|
24
|
+
const HIDDEN_SOURCE_DIRS = new Set(['.vitepress', '.vuepress', '.storybook']);
|
|
25
|
+
|
|
13
26
|
const SCANNABLE_EXTENSIONS = new Set([
|
|
14
27
|
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
|
15
28
|
'.jsx', '.tsx', '.js', '.ts',
|
|
@@ -24,6 +37,7 @@ function walkDir(dir) {
|
|
|
24
37
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }
|
|
25
38
|
for (const entry of entries) {
|
|
26
39
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
40
|
+
if (entry.isDirectory() && entry.name.startsWith('.') && !HIDDEN_SOURCE_DIRS.has(entry.name)) continue;
|
|
27
41
|
const full = path.join(dir, entry.name);
|
|
28
42
|
if (entry.isDirectory()) files.push(...walkDir(full));
|
|
29
43
|
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|