lazycodex-ai 5.1.35 → 5.1.38
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/dist/cli/index.js +20 -17
- package/dist/cli-node/index.js +20 -17
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +2 -2
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +18 -17
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/shared/package.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +4 -4
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +2 -2
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +8 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +3 -2
- package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/color.ts +132 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/css-parse.ts +103 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +156 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/findings.ts +34 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/gradient.ts +70 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/lint.ts +110 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/markup.ts +148 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/selector.ts +46 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +117 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +50 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/exempt-states.css +4 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/flat-stripe-side.tsx +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/full-surface-gradients.css +34 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +7 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/pseudo-overlays.css +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/wide-blocks-and-hairlines.css +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/apply.css +3 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/border-top-card.css +5 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/cva-variant.ts +1 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/gradient-edges.css +26 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inline-styles.tsx +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inset-shadow.css +4 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/non-visible-focus-and-nested.tsx +6 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/pseudo-bar.css +13 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/split-rule-and-token.scss +14 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/styled-components.tsx +14 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-cards.tsx +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-inset-and-style.vue +11 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-pseudo.html +3 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +3 -3
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +4 -4
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +2 -2
- package/packages/shared-skills/skills/frontend/SKILL.md +8 -1
- package/packages/shared-skills/skills/frontend/references/design/README.md +3 -2
- package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/color.ts +132 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/css-parse.ts +103 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +156 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/findings.ts +34 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/gradient.ts +70 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/lint.ts +110 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/markup.ts +148 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/selector.ts +46 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +117 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.test.ts +86 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +50 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/exempt-states.css +4 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/flat-stripe-side.tsx +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/full-surface-gradients.css +34 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +7 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/pseudo-overlays.css +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/wide-blocks-and-hairlines.css +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/apply.css +3 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/border-top-card.css +5 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/cva-variant.ts +1 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/gradient-edges.css +26 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inline-styles.tsx +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inset-shadow.css +4 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/non-visible-focus-and-nested.tsx +6 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/pseudo-bar.css +13 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/split-rule-and-token.scss +14 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/styled-components.tsx +14 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-cards.tsx +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-inset-and-style.vue +11 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-pseudo.html +3 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"version": "0.0.1",
|
|
3
3
|
"sourceDigest": "646863e33d2a1af1fdd3651ee157ada9ba24337e82430edef94342774c80f0f6",
|
|
4
4
|
"files": {
|
|
5
|
-
"index.js": "
|
|
5
|
+
"index.js": "d67d3a847fe0bbc43e824feded48b280890d31879538309e671cd5f671133995",
|
|
6
6
|
"page-bundle.js": "bfd51e8383cf47200d57387bbbed258bce5a3a3d00c38fc108034d2888e23f0c"
|
|
7
7
|
},
|
|
8
|
-
"stagedAtUtc": "2026-10-
|
|
8
|
+
"stagedAtUtc": "2026-10-19T10:08:12.859Z"
|
|
9
9
|
}
|
|
@@ -25,6 +25,13 @@ This file is a router, not a rulebook. The rules live in four rulesets under `re
|
|
|
25
25
|
|
|
26
26
|
**For implementation work, design + perfection load together.** Beauty with a 2 MB bundle fails; Lighthouse 100 that looks like AI slop fails. Both win or neither does.
|
|
27
27
|
|
|
28
|
+
## Draft guard — read before the first line of UI
|
|
29
|
+
|
|
30
|
+
Some tells regenerate by default, so they are kept out of the first draft rather than left for QA to find.
|
|
31
|
+
|
|
32
|
+
- **No coloured edge on a rounded surface, decorative or state.** A card, callout, stat tile, testimonial, alert, or list row never carries a coloured stripe on one side: `border-top: 4px solid <accent>` or `border-l-4 border-indigo-500` on a rounded box, a coloured `::before`/`::after` bar inside one, an inset `box-shadow` stripe, or a hard-stop gradient band or gradient `border-image` along one edge. The radius bends the stripe around the corner, and the result reads as generated whether the stripe decorates the card or marks it selected, focused, or active. Give the card its identity another way: a tonal surface (one ink at a low alpha, or the next step of the surface ramp), an eyebrow or heading set in the accent ink, a leading glyph, or more space between groups. Encode state with one ink at many alphas (hover/selected/active washes), a check glyph for selection, and tonal layering for focus. The coloured edges that stay allowed: keyboard `focus-visible` rings, validation states on form inputs, neutral 1px hairlines and dividers, and table rules. This covers pre-existing instances on any surface you touch.
|
|
33
|
+
- **Lint before visual QA.** Run `bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts <changed files or dirs>` on every stylesheet and component you wrote. It reads CSS/SCSS/Less, HTML, JSX/TSX, Vue, Svelte, Astro, Tailwind classes (including `@apply` and `cva`/`cn` strings), inline styles, and styled-components. Exit 1 means a stripe is in the draft: remove the stripe, do not argue with the finding.
|
|
34
|
+
|
|
28
35
|
## Design System and Component Workflow
|
|
29
36
|
|
|
30
37
|
Every implementation must choose one of these branches before UI code changes:
|
|
@@ -138,7 +145,7 @@ Domains: `product` `style` `typography` `color` `landing` `chart` `ux` `react` `
|
|
|
138
145
|
- **Concrete reference = contract.** When a screenshot, mockup, or annotated reference exists, match its pixels, copy, component structure, and responsive intent unless the user accepts a deviation.
|
|
139
146
|
- **Never weaken UX OR flatten the surface to buy points.** No dropping animations, hiding content, simplifying interactions, or replacing rendered/lit material with flat fills and flat geometric primitives for a Lighthouse score or a deadline. Hit 100 AND keep the surface dimensional — both, or neither.
|
|
140
147
|
- **No emojis as icons.** SVG icon sets only (Lucide, Heroicons, Radix, Phosphor).
|
|
141
|
-
- **No coloured
|
|
148
|
+
- **No coloured edges on rounded surfaces.** A decorative accent stripe on a card and a `border-l-2 border-primary` stripe on a selected row are the same tell, and so is a primary-tinted outline on a focused card. The Draft guard above carries the replacements and the allowed exceptions; `check-accent-stripes.ts` must exit 0 before `/visual-qa`. Applies to code you write AND to pre-existing instances on any surface you touch — sweep them out.
|
|
142
149
|
- **GPU-composited animation only** — `transform`, `opacity`, `filter`; never animate layout properties.
|
|
143
150
|
- **Slop animation is forbidden — motion serves meaning.** Every animation or hover must map to a real interaction, state change, or affordance. A hover that changes nothing, motion on a non-interactive element, or a decorative micro-animation with no informational purpose is slop — do not add it.
|
|
144
151
|
- **Done is the `/visual-qa` gate, not your own glance.** A frontend design task is verified through `/visual-qa` - its HIG-based checklist, its capture matrix (every page and every sibling surface, phone and desktop, light and dark, scrolled-end, motion driven and inspected) and its dual-oracle completion gate on fresh evidence. This skill does not keep its own list of visual checks; the verifier owns it.
|
|
@@ -202,7 +202,7 @@ Triggers: an animated, shader, particle, or grid background; a hero that must fe
|
|
|
202
202
|
## Anti-patterns — do not do these
|
|
203
203
|
|
|
204
204
|
- **Don't load nothing and just freestyle.** That produces the exact "generic AI SaaS slop" — purple-blue gradient backgrounds, rounded-2xl-on-everything, three feature cards in a grid, generic Inter font, lorem ipsum. The skills exist precisely to prevent this.
|
|
205
|
-
- **Don't
|
|
205
|
+
- **Don't put a coloured edge on a rounded surface, for decoration or for state.** A feature card with `border-top: 4px solid` accent, a callout with `border-l-4 border-indigo-500`, a stat tile with a coloured `::before` bar, `border-l-2 border-primary` on the selected row, a primary outline on the focused card: the radius bends the stripe and the result is the generic AI tell. Identity comes from a tonal surface, an eyebrow or heading in the accent ink, a glyph, or spacing; state is ink-alpha washes, a glyph for selection, tonal layering for focus. `focus-visible` rings, input validation states, neutral hairlines, and table rules are the only coloured or ruled edges allowed. Includes pre-existing instances on surfaces you touch.
|
|
206
206
|
- **Don't ignore the user's named brand.** If they say "Linear-style" and you build something that doesn't match Linear's actual aesthetic (purple, ultra-tight spacing, mono accents, etc.), you have failed the routing.
|
|
207
207
|
- **Don't apply a Layer B brand verbatim if the project is not that brand.** The DESIGN.md captures *inspiration* — extract the tokens (palette, type scale, component patterns) and apply them to the project's own content. Do not copy logos or trademarked imagery.
|
|
208
208
|
- **Don't use imagegen skills to write code.** They are explicitly image-only. The agent has been observed trying to "describe" the image as React code — that is the wrong skill, switch to `image-to-code-skill.md` instead.
|
|
@@ -221,7 +221,8 @@ Once references are loaded, before writing any UI code:
|
|
|
221
221
|
7. **Match the project's existing patterns FIRST.** If the codebase already uses CSS Modules, don't introduce Tailwind. If it uses styled-components, don't introduce CSS-in-JS variants. The references guide *style*, not *infrastructure*.
|
|
222
222
|
8. **All tokens trace back to `DESIGN.md`.** No orphan hex codes, no magic px values. If you need a new token, update `DESIGN.md` first.
|
|
223
223
|
9. **New reusable patterns (used 2+ times) get documented back into `DESIGN.md` Section 5.**
|
|
224
|
-
10. **No generic-default drift (expressive briefs).** The shipped CSS must use the `DESIGN.md` material, not the model's priors. Load the declared fonts (do not silently fall back to Inter or system fonts), build elevated surfaces from the declared multi-layer recipe (not a lone `backdrop-filter: blur`), and color from the ramp stops (not one tint at varied opacity). Grep your styles before QA: finding `Inter`, a single blur on "glass", one brand hex reused everywhere
|
|
224
|
+
10. **No generic-default drift (expressive briefs).** The shipped CSS must use the `DESIGN.md` material, not the model's priors. Load the declared fonts (do not silently fall back to Inter or system fonts), build elevated surfaces from the declared multi-layer recipe (not a lone `backdrop-filter: blur`), and color from the ramp stops (not one tint at varied opacity). Grep your styles before QA: finding `Inter`, a single blur on "glass", or one brand hex reused everywhere means you regressed to priors — fix it before declaring done.
|
|
225
|
+
11. **No coloured edge stripes.** Run `bun ../../scripts/perfection/check-accent-stripes.ts <changed files or dirs>` (from this directory; `$SKILL_DIR/scripts/perfection/check-accent-stripes.ts` from anywhere). It must exit 0 before Phase Final; every finding is a decorative or state stripe on a rounded surface to replace, per the anti-pattern above.
|
|
225
226
|
|
|
226
227
|
## Quick lookup table — most common requests
|
|
227
228
|
|
|
@@ -67,7 +67,7 @@ During the audit loop, after Lighthouse scores pass, run a Design System Complia
|
|
|
67
67
|
- **Spacing**: every margin/padding/gap value must be a multiple of the base unit (4px) and ideally use a declared token.
|
|
68
68
|
- **Components**: any component used 2+ times must be documented in `DESIGN.md` Section 5. If it isn't, add it.
|
|
69
69
|
- **Depth**: if `DESIGN.md` says "borders-only", there must be zero `box-shadow` declarations. If "tonal-shift", zero borders for surface separation.
|
|
70
|
-
- **
|
|
70
|
+
- **Accent stripes**: run `bun ../../scripts/perfection/check-accent-stripes.ts <src dirs>`. It flags a coloured one-side border (including a gradient `border-image`), a coloured `::before`/`::after` bar, an inset `box-shadow` stripe, or a hard-stop gradient band along one edge of a rounded surface, in CSS, Tailwind, inline styles, and styled-components, whether the stripe is decorative or marks selected/focused/active. Each finding is a violation — identity belongs to tonal surfaces, headings, glyphs, and spacing; state belongs to ink-alpha washes and glyphs. Keyboard `focus-visible` rings, input validation states, neutral hairlines, and table rules are exempt, and the script already skips them.
|
|
71
71
|
|
|
72
72
|
A page that scores Lighthouse 100 but uses 14 undeclared hex codes and 8 magic spacing values is **NOT DONE**. The design system is the architecture — Lighthouse measures the performance of that architecture.
|
|
73
73
|
|
|
@@ -92,6 +92,7 @@ Quick audit via the cross-platform Python CLI (macOS, Linux, Windows):
|
|
|
92
92
|
uv run $SKILL_DIR/scripts/perfection/lighthouse-audit.py https://localhost:3000
|
|
93
93
|
uv run $SKILL_DIR/scripts/perfection/lighthouse-audit.py https://localhost:3000 --threshold 95
|
|
94
94
|
uv run $SKILL_DIR/scripts/perfection/lighthouse-audit.py https://localhost:3000 --desktop-only
|
|
95
|
+
bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts src/ # exit 1 = coloured edge stripe on a rounded surface
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
Or use the TypeScript approach directly in your test suite:
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Colour classification for the accent-stripe linter.
|
|
2
|
+
// "chromatic" = an accent hue the stripe tell is made of; "neutral" = greys,
|
|
3
|
+
// inks, hairline tokens; "unknown" = cannot tell, so never flagged (low false positives).
|
|
4
|
+
|
|
5
|
+
export type ColorClass = "chromatic" | "neutral" | "unknown"
|
|
6
|
+
|
|
7
|
+
const HUES = [
|
|
8
|
+
"red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan",
|
|
9
|
+
"sky", "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose",
|
|
10
|
+
]
|
|
11
|
+
const STATUS_WORDS = [
|
|
12
|
+
"accent", "brand", "destructive", "danger", "error", "warning", "warn", "success",
|
|
13
|
+
"info", "alert", "highlight", "positive", "negative", "critical", "attention", "tint",
|
|
14
|
+
]
|
|
15
|
+
const NEUTRAL_WORDS = [
|
|
16
|
+
"slate", "gray", "grey", "zinc", "neutral", "stone", "black", "white", "transparent",
|
|
17
|
+
"current", "currentcolor", "inherit", "initial", "unset", "border", "divider",
|
|
18
|
+
"separator", "line", "hairline", "muted", "subtle", "input", "surface", "background",
|
|
19
|
+
"bg", "card", "base", "outline",
|
|
20
|
+
]
|
|
21
|
+
// "primary" names an accent only when it is not the primary ink (--text-primary, --fg-primary).
|
|
22
|
+
const INK_WORDS = ["text", "fg", "foreground", "ink", "content", "label", "body"]
|
|
23
|
+
|
|
24
|
+
const NAMED_NEUTRAL = new Set([
|
|
25
|
+
"black", "white", "gray", "grey", "silver", "gainsboro", "whitesmoke", "lightgray",
|
|
26
|
+
"lightgrey", "darkgray", "darkgrey", "dimgray", "dimgrey", "slategray", "slategrey",
|
|
27
|
+
"lightslategray", "darkslategray", "snow", "ivory", "linen", "transparent", "currentcolor",
|
|
28
|
+
"inherit", "initial", "unset", "none",
|
|
29
|
+
])
|
|
30
|
+
const NAMED_CHROMATIC = new Set([
|
|
31
|
+
"red", "orange", "yellow", "green", "blue", "purple", "violet", "indigo", "teal", "cyan",
|
|
32
|
+
"magenta", "fuchsia", "pink", "gold", "lime", "navy", "crimson", "tomato", "coral",
|
|
33
|
+
"royalblue", "dodgerblue", "deepskyblue", "steelblue", "seagreen", "limegreen", "orangered",
|
|
34
|
+
"hotpink", "deeppink", "darkorange", "goldenrod", "turquoise", "orchid", "salmon", "maroon",
|
|
35
|
+
"olive", "aqua", "rebeccapurple", "mediumpurple", "slateblue", "tan",
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const CHROMA_THRESHOLD = 0.2
|
|
39
|
+
|
|
40
|
+
export function classifyName(name: string): ColorClass {
|
|
41
|
+
const parts = name.toLowerCase().replace(/^-+/, "").split(/[-_/.\s]+/).filter(Boolean)
|
|
42
|
+
if (parts.some((p) => HUES.includes(p) || STATUS_WORDS.includes(p))) return "chromatic"
|
|
43
|
+
if (parts.includes("primary") || parts.includes("secondary")) {
|
|
44
|
+
if (parts.some((p) => INK_WORDS.includes(p) || NEUTRAL_WORDS.includes(p))) return "neutral"
|
|
45
|
+
return parts.includes("primary") ? "chromatic" : "unknown"
|
|
46
|
+
}
|
|
47
|
+
if (parts.some((p) => NEUTRAL_WORDS.includes(p) || INK_WORDS.includes(p))) return "neutral"
|
|
48
|
+
return "unknown"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function chromaFromRgb(r: number, g: number, b: number): ColorClass {
|
|
52
|
+
const max = Math.max(r, g, b)
|
|
53
|
+
const min = Math.min(r, g, b)
|
|
54
|
+
return (max - min) / 255 > CHROMA_THRESHOLD ? "chromatic" : "neutral"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseNumbers(args: string): number[] {
|
|
58
|
+
return args
|
|
59
|
+
.split(/[\s,/]+/)
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((n) => (n.endsWith("%") ? Number.parseFloat(n) / 100 : Number.parseFloat(n)))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function classifyFunction(fn: string, args: string): ColorClass {
|
|
65
|
+
const raw = args.split(/[\s,/]+/).filter(Boolean)
|
|
66
|
+
const n = parseNumbers(args)
|
|
67
|
+
if (n.some((x) => Number.isNaN(x))) return "unknown"
|
|
68
|
+
if (fn === "rgb" || fn === "rgba") {
|
|
69
|
+
const scale = (i: number) => (raw[i]?.endsWith("%") ? (n[i] ?? 0) * 255 : (n[i] ?? 0))
|
|
70
|
+
return chromaFromRgb(scale(0), scale(1), scale(2))
|
|
71
|
+
}
|
|
72
|
+
if (fn === "hsl" || fn === "hsla") {
|
|
73
|
+
const s = raw[1]?.endsWith("%") ? (n[1] ?? 0) : (n[1] ?? 0) / 100
|
|
74
|
+
const l = raw[2]?.endsWith("%") ? (n[2] ?? 0) : (n[2] ?? 0) / 100
|
|
75
|
+
return (1 - Math.abs(2 * l - 1)) * s > CHROMA_THRESHOLD ? "chromatic" : "neutral"
|
|
76
|
+
}
|
|
77
|
+
if (fn === "oklch") return (n[1] ?? 0) > 0.05 ? "chromatic" : "neutral"
|
|
78
|
+
if (fn === "lch") return (n[1] ?? 0) > 20 ? "chromatic" : "neutral"
|
|
79
|
+
return "unknown"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function classifyColor(input: string): ColorClass {
|
|
83
|
+
const value = input.trim().toLowerCase().replace(/\s*!important$/, "")
|
|
84
|
+
const hex = /^#([0-9a-f]{3,8})$/.exec(value)
|
|
85
|
+
if (hex?.[1]) {
|
|
86
|
+
let h = hex[1]
|
|
87
|
+
if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join("")
|
|
88
|
+
if (h.length !== 6 && h.length !== 8) return "unknown"
|
|
89
|
+
const [r, g, b] = [0, 2, 4].map((i) => Number.parseInt(h.slice(i, i + 2), 16))
|
|
90
|
+
return chromaFromRgb(r ?? 0, g ?? 0, b ?? 0)
|
|
91
|
+
}
|
|
92
|
+
const varMatch = /^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/.exec(value)
|
|
93
|
+
if (varMatch?.[1]) {
|
|
94
|
+
const byName = classifyName(varMatch[1])
|
|
95
|
+
if (byName !== "unknown" || !varMatch[2]) return byName
|
|
96
|
+
return classifyColor(varMatch[2])
|
|
97
|
+
}
|
|
98
|
+
const theme = /^theme\(\s*['"]?([\w.-]+)['"]?\s*\)$/.exec(value)
|
|
99
|
+
if (theme?.[1]) return classifyName(theme[1])
|
|
100
|
+
const fn = /^([a-z]+)\((.*)\)$/.exec(value)
|
|
101
|
+
if (fn?.[1] && fn[2] !== undefined) return classifyFunction(fn[1], fn[2])
|
|
102
|
+
if (NAMED_NEUTRAL.has(value)) return "neutral"
|
|
103
|
+
if (NAMED_CHROMATIC.has(value)) return "chromatic"
|
|
104
|
+
return "unknown"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function looksLikeColor(token: string): boolean {
|
|
108
|
+
const t = token.toLowerCase()
|
|
109
|
+
return (
|
|
110
|
+
t.startsWith("#") ||
|
|
111
|
+
/^(rgba?|hsla?|oklch|oklab|lch|lab|color-mix|var|theme)\(/.test(t) ||
|
|
112
|
+
NAMED_NEUTRAL.has(t) ||
|
|
113
|
+
NAMED_CHROMATIC.has(t)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const COLOR_IN_VALUE = /#[0-9a-f]{3,8}\b|(?:rgba?|hsla?|oklch|lch)\([^)]*\)|var\(--[\w-]+(?:,[^)]*)?\)|\b[a-z]+\b/gi
|
|
118
|
+
|
|
119
|
+
export function containsChromatic(value: string): boolean {
|
|
120
|
+
return (value.match(COLOR_IN_VALUE) ?? []).some((c) => looksLikeColor(c) && classifyColor(c) === "chromatic")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function classifyUtilityColor(fragment: string): ColorClass {
|
|
124
|
+
if (fragment.startsWith("[") && fragment.endsWith("]")) {
|
|
125
|
+
const inner = fragment.slice(1, -1).replace(/^color:/, "").replace(/_/g, " ")
|
|
126
|
+
return classifyColor(inner)
|
|
127
|
+
}
|
|
128
|
+
const bare = fragment.replace(/\/[\w.[\]]+$/, "")
|
|
129
|
+
const head = bare.split("-")[0] ?? ""
|
|
130
|
+
if (HUES.includes(head)) return "chromatic"
|
|
131
|
+
return classifyName(bare)
|
|
132
|
+
}
|
package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/css-parse.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type Declaration = { readonly prop: string; readonly value: string; readonly offset?: number }
|
|
2
|
+
|
|
3
|
+
export type CssRule = {
|
|
4
|
+
readonly selector: string
|
|
5
|
+
readonly declarations: readonly Declaration[]
|
|
6
|
+
readonly applyClasses: readonly string[]
|
|
7
|
+
readonly applyOffset?: number
|
|
8
|
+
readonly offset: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Frame = { selector: string; declarations: Declaration[]; applyClasses: string[]; applyOffset?: number; offset: number }
|
|
12
|
+
|
|
13
|
+
function blankComments(text: string, lineComments: boolean): string {
|
|
14
|
+
let out = text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
|
|
15
|
+
if (lineComments) out = out.replace(/(^|[^:"'])\/\/[^\n]*/g, (m, lead: string) => lead + " ".repeat(m.length - lead.length))
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function splitTopLevel(value: string, separator: "," | " "): string[] {
|
|
20
|
+
const parts: string[] = []
|
|
21
|
+
let depth = 0
|
|
22
|
+
let current = ""
|
|
23
|
+
for (const ch of value) {
|
|
24
|
+
if (ch === "(" || ch === "[") depth += 1
|
|
25
|
+
if (ch === ")" || ch === "]") depth -= 1
|
|
26
|
+
const splits = depth === 0 && (separator === "," ? ch === "," : /\s/.test(ch))
|
|
27
|
+
if (splits) {
|
|
28
|
+
if (current.trim()) parts.push(current.trim())
|
|
29
|
+
current = ""
|
|
30
|
+
} else current += ch
|
|
31
|
+
}
|
|
32
|
+
if (current.trim()) parts.push(current.trim())
|
|
33
|
+
return parts
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveSelector(parent: string | undefined, child: string): string {
|
|
37
|
+
if (!parent) return child
|
|
38
|
+
const parents = splitTopLevel(parent, ",")
|
|
39
|
+
return splitTopLevel(child, ",")
|
|
40
|
+
.flatMap((c) => parents.map((p) => (c.includes("&") ? c.replaceAll("&", p) : `${p} ${c}`)))
|
|
41
|
+
.join(", ")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nearestSelector(stack: readonly Frame[]): string | undefined {
|
|
45
|
+
for (let i = stack.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const frame = stack[i]
|
|
47
|
+
if (frame && !frame.selector.startsWith("@")) return frame.selector
|
|
48
|
+
}
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function addStatement(frame: Frame | undefined, statement: string, offset: number): void {
|
|
53
|
+
const text = statement.trim()
|
|
54
|
+
if (!frame || !text) return
|
|
55
|
+
if (text.startsWith("@apply")) {
|
|
56
|
+
frame.applyOffset ??= offset
|
|
57
|
+
frame.applyClasses.push(...text.slice("@apply".length).replace(/!important/g, "").split(/\s+/).filter(Boolean))
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
const colon = text.indexOf(":")
|
|
61
|
+
if (colon <= 0) return
|
|
62
|
+
frame.declarations.push({ prop: text.slice(0, colon).trim().toLowerCase(), value: text.slice(colon + 1).trim(), offset })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function parseCss(source: string, baseOffset = 0, lineComments = false): CssRule[] {
|
|
66
|
+
const text = blankComments(source, lineComments)
|
|
67
|
+
const rules: CssRule[] = []
|
|
68
|
+
const stack: Frame[] = []
|
|
69
|
+
let buffer = ""
|
|
70
|
+
let bufferStart = -1
|
|
71
|
+
let parens = 0
|
|
72
|
+
let quote = ""
|
|
73
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
74
|
+
const ch = text.charAt(i)
|
|
75
|
+
if (quote) {
|
|
76
|
+
buffer += ch
|
|
77
|
+
if (ch === quote && text.charAt(i - 1) !== "\\") quote = ""
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if (ch === '"' || ch === "'") quote = ch
|
|
81
|
+
if (ch === "(") parens += 1
|
|
82
|
+
if (ch === ")") parens -= 1
|
|
83
|
+
if (parens > 0 || quote || (ch !== "{" && ch !== "}" && ch !== ";")) {
|
|
84
|
+
if (bufferStart < 0 && /\S/.test(ch)) bufferStart = i
|
|
85
|
+
buffer += ch
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
const top = stack[stack.length - 1]
|
|
89
|
+
if (ch === "{") {
|
|
90
|
+
const raw = buffer.trim()
|
|
91
|
+
const selector = raw.startsWith("@") ? raw : resolveSelector(nearestSelector(stack), raw)
|
|
92
|
+
stack.push({ selector, declarations: [], applyClasses: [], offset: baseOffset + Math.max(bufferStart, 0) })
|
|
93
|
+
} else if (ch === ";") addStatement(top, buffer, baseOffset + Math.max(bufferStart, 0))
|
|
94
|
+
else {
|
|
95
|
+
addStatement(top, buffer, baseOffset + Math.max(bufferStart, 0))
|
|
96
|
+
const done = stack.pop()
|
|
97
|
+
if (done && !done.selector.startsWith("@")) rules.push(done)
|
|
98
|
+
}
|
|
99
|
+
buffer = ""
|
|
100
|
+
bufferStart = -1
|
|
101
|
+
}
|
|
102
|
+
return rules
|
|
103
|
+
}
|
package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/declarations.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { type ColorClass, classifyColor, containsChromatic, looksLikeColor } from "./color"
|
|
2
|
+
import { type Declaration, splitTopLevel } from "./css-parse"
|
|
3
|
+
|
|
4
|
+
export const MIN_STRIPE_PX = 2
|
|
5
|
+
export const MAX_BAR_PX = 8
|
|
6
|
+
|
|
7
|
+
const SIDES = ["top", "right", "bottom", "left"] as const
|
|
8
|
+
export type Side = (typeof SIDES)[number]
|
|
9
|
+
type Edge = { width?: number; color?: ColorClass; hidden?: boolean }
|
|
10
|
+
|
|
11
|
+
const LOGICAL_SIDE: Readonly<Record<string, Side>> = {
|
|
12
|
+
top: "top", right: "right", bottom: "bottom", left: "left",
|
|
13
|
+
"block-start": "top", "inline-end": "right", "block-end": "bottom", "inline-start": "left",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseLength(token: string): number | undefined {
|
|
17
|
+
const keyword: Readonly<Record<string, number>> = { thin: 1, medium: 3, thick: 5 }
|
|
18
|
+
const t = token.trim().toLowerCase()
|
|
19
|
+
if (t in keyword) return keyword[t]
|
|
20
|
+
const m = /^(-?\d*\.?\d+)(px|rem|em)?$/.exec(t)
|
|
21
|
+
if (!m?.[1]) return undefined
|
|
22
|
+
const n = Number.parseFloat(m[1])
|
|
23
|
+
return m[2] === "rem" || m[2] === "em" ? n * 16 : n
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function percent(token: string | undefined): number | undefined {
|
|
27
|
+
const m = token ? /^(\d*\.?\d+)%$/.exec(token.trim()) : null
|
|
28
|
+
return m?.[1] ? Number.parseFloat(m[1]) : undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function boxValues(value: string): [string, string, string, string] {
|
|
32
|
+
const v = splitTopLevel(value, " ")
|
|
33
|
+
const [a = "", b = a, c = a, d = b] = v
|
|
34
|
+
return [a, b, c, d]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseEdgeShorthand(value: string): Edge {
|
|
38
|
+
const edge: Edge = {}
|
|
39
|
+
for (const token of splitTopLevel(value, " ")) {
|
|
40
|
+
if (looksLikeColor(token)) edge.color = classifyColor(token)
|
|
41
|
+
else if (/^(none|hidden)$/i.test(token)) edge.hidden = true
|
|
42
|
+
else {
|
|
43
|
+
const length = parseLength(token)
|
|
44
|
+
if (length !== undefined) edge.width = length
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return edge
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectEdges(declarations: readonly Declaration[]): Map<Side, Edge> {
|
|
51
|
+
const edges = new Map<Side, Edge>(SIDES.map((s) => [s, {}]))
|
|
52
|
+
const merge = (side: Side, patch: Edge) => edges.set(side, { ...edges.get(side), ...patch })
|
|
53
|
+
let imageChromatic = false
|
|
54
|
+
for (const { prop, value } of declarations) {
|
|
55
|
+
if (prop === "border-image" || prop === "border-image-source") imageChromatic = containsChromatic(value)
|
|
56
|
+
else if (prop === "border") SIDES.forEach((s) => merge(s, parseEdgeShorthand(value)))
|
|
57
|
+
else if (prop === "border-width") boxValues(value).forEach((v, i) => merge(SIDES[i] ?? "top", { width: parseLength(v) ?? 0 }))
|
|
58
|
+
else if (prop === "border-color") boxValues(value).forEach((v, i) => merge(SIDES[i] ?? "top", { color: classifyColor(v) }))
|
|
59
|
+
else {
|
|
60
|
+
const m = /^border-(top|right|bottom|left|block-start|block-end|inline-start|inline-end)(?:-(width|color))?$/.exec(prop)
|
|
61
|
+
const side = m?.[1] ? LOGICAL_SIDE[m[1]] : undefined
|
|
62
|
+
if (!side) continue
|
|
63
|
+
if (m?.[2] === "width") merge(side, { width: parseLength(value) ?? 0 })
|
|
64
|
+
else if (m?.[2] === "color") merge(side, { color: classifyColor(value) })
|
|
65
|
+
else merge(side, parseEdgeShorthand(value))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (imageChromatic) {
|
|
69
|
+
for (const side of SIDES) if ((edges.get(side)?.width ?? 0) > 0) merge(side, { color: "chromatic" })
|
|
70
|
+
}
|
|
71
|
+
return edges
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type Corner = "tl" | "tr" | "br" | "bl"
|
|
75
|
+
export const ALL_CORNERS: ReadonlySet<Corner> = new Set(["tl", "tr", "br", "bl"])
|
|
76
|
+
export const SIDE_CORNERS: Readonly<Record<Side, readonly [Corner, Corner]>> = {
|
|
77
|
+
top: ["tl", "tr"], right: ["tr", "br"], bottom: ["br", "bl"], left: ["tl", "bl"],
|
|
78
|
+
}
|
|
79
|
+
const CORNER_LONGHAND: Readonly<Record<string, Corner>> = {
|
|
80
|
+
"top-left": "tl", "top-right": "tr", "bottom-right": "br", "bottom-left": "bl",
|
|
81
|
+
"start-start": "tl", "start-end": "tr", "end-end": "br", "end-start": "bl",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const nonZero = (token: string) => (parseLength(token) ?? percent(token) ?? 1) > 0
|
|
85
|
+
|
|
86
|
+
export function roundedCorners(declarations: readonly Declaration[]): Set<Corner> {
|
|
87
|
+
const corners = new Set<Corner>()
|
|
88
|
+
for (const { prop, value } of declarations) {
|
|
89
|
+
if (prop === "border-radius") {
|
|
90
|
+
const [tl, tr, br, bl] = boxValues(value.split("/")[0] ?? "")
|
|
91
|
+
const values: Array<[Corner, string]> = [["tl", tl], ["tr", tr], ["br", br], ["bl", bl]]
|
|
92
|
+
for (const [corner, v] of values) {
|
|
93
|
+
if (nonZero(v)) corners.add(corner)
|
|
94
|
+
else corners.delete(corner)
|
|
95
|
+
}
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const m = /^border-([a-z]+-[a-z]+)-radius$/.exec(prop)
|
|
99
|
+
const corner = m?.[1] ? CORNER_LONGHAND[m[1]] : undefined
|
|
100
|
+
if (!corner) continue
|
|
101
|
+
if (nonZero(splitTopLevel(value, " ")[0] ?? "0")) corners.add(corner)
|
|
102
|
+
else corners.delete(corner)
|
|
103
|
+
}
|
|
104
|
+
return corners
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function hasEdgeStripe(declarations: readonly Declaration[], corners: ReadonlySet<Corner> = ALL_CORNERS): boolean {
|
|
108
|
+
const bySide = collectEdges(declarations)
|
|
109
|
+
const edges = [...bySide.values()]
|
|
110
|
+
const isStripe = (e: Edge) => !e.hidden && (e.width ?? 0) >= MIN_STRIPE_PX && e.color === "chromatic"
|
|
111
|
+
const bent = SIDES.some((s) => {
|
|
112
|
+
const edge = bySide.get(s)
|
|
113
|
+
return edge !== undefined && isStripe(edge) && SIDE_CORNERS[s].some((c) => corners.has(c))
|
|
114
|
+
})
|
|
115
|
+
if (!bent) return false
|
|
116
|
+
const first = edges[0]
|
|
117
|
+
const uniform = edges.every((e) => e.width === first?.width && e.color === first?.color && e.hidden === first?.hidden)
|
|
118
|
+
return !uniform
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function hasRadius(declarations: readonly Declaration[]): boolean {
|
|
122
|
+
return roundedCorners(declarations).size > 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isInsetShadowStripe(value: string): boolean {
|
|
126
|
+
return splitTopLevel(value, ",").some((layer) => {
|
|
127
|
+
const tokens = splitTopLevel(layer, " ")
|
|
128
|
+
if (!tokens.some((t) => t.toLowerCase() === "inset")) return false
|
|
129
|
+
const lengths = tokens.map(parseLength).filter((n): n is number => n !== undefined)
|
|
130
|
+
const [x = 0, y = 0, blur = 0] = lengths
|
|
131
|
+
const oneSided = (Math.abs(x) >= MIN_STRIPE_PX) !== (Math.abs(y) >= MIN_STRIPE_PX) && (x === 0 || y === 0)
|
|
132
|
+
return oneSided && blur === 0 && tokens.some((t) => looksLikeColor(t) && classifyColor(t) === "chromatic")
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function hasInsetShadowStripe(declarations: readonly Declaration[]): boolean {
|
|
137
|
+
return declarations.some(({ prop, value }) => prop === "box-shadow" && isInsetShadowStripe(value))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const isBackground = (prop: string) => prop === "background" || prop === "background-color" || prop === "background-image"
|
|
141
|
+
|
|
142
|
+
function backgroundIsChromatic(declarations: readonly Declaration[]): boolean {
|
|
143
|
+
return declarations.some(({ prop, value }) => isBackground(prop) && containsChromatic(value))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isPseudoBar(declarations: readonly Declaration[]): boolean {
|
|
147
|
+
if (!backgroundIsChromatic(declarations)) return false
|
|
148
|
+
const get = (name: string) => declarations.findLast((d) => d.prop === name)?.value
|
|
149
|
+
const has = (name: string) => get(name) !== undefined
|
|
150
|
+
const thin = (name: string) => {
|
|
151
|
+
const n = parseLength(get(name) ?? "")
|
|
152
|
+
return n !== undefined && n > 0 && n <= MAX_BAR_PX
|
|
153
|
+
}
|
|
154
|
+
const long = (name: string, a: string, b: string) => (percent(get(name)) ?? 0) >= 50 || (has(a) && has(b)) || has("inset")
|
|
155
|
+
return (thin("width") && long("height", "top", "bottom")) || (thin("height") && long("width", "left", "right"))
|
|
156
|
+
}
|
package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/findings.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RuleId = "edge-stripe" | "pseudo-bar" | "inset-shadow-stripe" | "gradient-stripe"
|
|
2
|
+
|
|
3
|
+
export type Finding = {
|
|
4
|
+
readonly file: string
|
|
5
|
+
readonly line: number
|
|
6
|
+
readonly column: number
|
|
7
|
+
readonly rule: RuleId
|
|
8
|
+
readonly excerpt: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const RULE_MESSAGES: Readonly<Record<RuleId, string>> = {
|
|
12
|
+
"edge-stripe": "coloured one-side border on a rounded surface; the corners bend the stripe",
|
|
13
|
+
"pseudo-bar": "coloured ::before/::after bar on a rounded surface",
|
|
14
|
+
"inset-shadow-stripe": "inset box-shadow stripe on a rounded surface",
|
|
15
|
+
"gradient-stripe": "hard-stop gradient band along one edge of a rounded surface",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FIX_HINT =
|
|
19
|
+
"drop the stripe: separate with a tonal surface (one ink at a low alpha), an eyebrow or heading, a leading glyph, or spacing"
|
|
20
|
+
|
|
21
|
+
export function lineLocator(text: string): (offset: number) => { line: number; column: number } {
|
|
22
|
+
const starts = [0]
|
|
23
|
+
for (let i = 0; i < text.length; i += 1) if (text.charAt(i) === "\n") starts.push(i + 1)
|
|
24
|
+
return (offset) => {
|
|
25
|
+
let lo = 0
|
|
26
|
+
let hi = starts.length - 1
|
|
27
|
+
while (lo < hi) {
|
|
28
|
+
const mid = (lo + hi + 1) >> 1
|
|
29
|
+
if ((starts[mid] ?? 0) <= offset) lo = mid
|
|
30
|
+
else hi = mid - 1
|
|
31
|
+
}
|
|
32
|
+
return { line: lo + 1, column: offset - (starts[lo] ?? 0) + 1 }
|
|
33
|
+
}
|
|
34
|
+
}
|
package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/gradient.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { classifyColor, looksLikeColor } from "./color"
|
|
2
|
+
import { type Declaration, splitTopLevel } from "./css-parse"
|
|
3
|
+
import { isBackground, MAX_BAR_PX, parseLength } from "./declarations"
|
|
4
|
+
|
|
5
|
+
type Stop = { readonly chromatic: boolean; readonly positions: readonly string[] }
|
|
6
|
+
type Edge = { readonly near: number } | { readonly far: number }
|
|
7
|
+
|
|
8
|
+
function linearGradientBodies(value: string): string[] {
|
|
9
|
+
const bodies: string[] = []
|
|
10
|
+
for (const m of value.matchAll(/(?<![\w-])linear-gradient\(/gi)) {
|
|
11
|
+
const start = (m.index ?? 0) + m[0].length
|
|
12
|
+
let depth = 1
|
|
13
|
+
let i = start
|
|
14
|
+
for (; i < value.length && depth > 0; i += 1) {
|
|
15
|
+
if (value.charAt(i) === "(") depth += 1
|
|
16
|
+
if (value.charAt(i) === ")") depth -= 1
|
|
17
|
+
}
|
|
18
|
+
bodies.push(value.slice(start, i - 1))
|
|
19
|
+
}
|
|
20
|
+
return bodies
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stopsOf(body: string): Stop[] {
|
|
24
|
+
return splitTopLevel(body, ",").flatMap((arg) => {
|
|
25
|
+
const [color = "", ...positions] = splitTopLevel(arg, " ")
|
|
26
|
+
if (!looksLikeColor(color)) return []
|
|
27
|
+
return [{ chromatic: classifyColor(color) === "chromatic", positions }]
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function edgeOf(position: string | undefined): Edge | undefined {
|
|
32
|
+
if (position === undefined) return undefined
|
|
33
|
+
const far = /^calc\(\s*100%\s*-\s*([\d.]+(?:px|rem|em)?)\s*\)$/i.exec(position)
|
|
34
|
+
if (far?.[1]) {
|
|
35
|
+
const px = parseLength(far[1])
|
|
36
|
+
return px === undefined ? undefined : { far: px }
|
|
37
|
+
}
|
|
38
|
+
const px = parseLength(position)
|
|
39
|
+
return px === undefined ? undefined : { near: px }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const nearPx = (edge: Edge | undefined): number | undefined => (edge && "near" in edge ? edge.near : undefined)
|
|
43
|
+
const farPx = (edge: Edge | undefined): number | undefined => (edge && "far" in edge ? edge.far : undefined)
|
|
44
|
+
const isBand = (px: number | undefined): px is number => px !== undefined && px > 0 && px <= MAX_BAR_PX
|
|
45
|
+
|
|
46
|
+
function isEdgeBand(stops: readonly Stop[]): boolean {
|
|
47
|
+
return stops.some((stop, i) => {
|
|
48
|
+
if (!stop.chromatic) return false
|
|
49
|
+
const end = nearPx(edgeOf(stop.positions.at(-1)))
|
|
50
|
+
const nextStart = nearPx(edgeOf(stops[i + 1]?.positions[0]))
|
|
51
|
+
if (isBand(end) && nextStart !== undefined && nextStart <= end) return true
|
|
52
|
+
const start = farPx(edgeOf(stop.positions[0]))
|
|
53
|
+
const prevEnd = farPx(edgeOf(stops[i - 1]?.positions.at(-1)))
|
|
54
|
+
return isBand(start) && prevEnd !== undefined && prevEnd <= start
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTiled(declarations: readonly Declaration[]): boolean {
|
|
59
|
+
return declarations.some(
|
|
60
|
+
({ prop, value }) =>
|
|
61
|
+
prop === "background-size" && splitTopLevel(value, ",").some((layer) => splitTopLevel(layer, " ").some((t) => parseLength(t) !== undefined)),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function hasGradientEdgeBand(declarations: readonly Declaration[]): boolean {
|
|
66
|
+
if (isTiled(declarations)) return false
|
|
67
|
+
return declarations.some(
|
|
68
|
+
({ prop, value }) => isBackground(prop) && linearGradientBodies(value).some((body) => isEdgeBand(stopsOf(body))),
|
|
69
|
+
)
|
|
70
|
+
}
|