lazycodex-ai 5.1.40 → 5.1.42
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.de.md +118 -0
- package/README.es.md +118 -0
- package/README.fr.md +118 -0
- package/README.it.md +118 -0
- package/README.ja.md +2 -1
- package/README.ko.md +2 -1
- package/README.md +3 -2
- package/README.pt-br.md +118 -0
- package/README.ru.md +2 -1
- package/README.zh-cn.md +2 -1
- package/dist/cli/index.js +115 -27
- package/dist/cli/service-auth/index.d.ts +3 -0
- package/dist/cli-node/index.js +115 -27
- package/package.json +1 -1
- package/packages/lsp-daemon/dist/cli.js +10 -0
- package/packages/lsp-daemon/dist/client.js +10 -0
- package/packages/lsp-daemon/dist/index.js +10 -0
- package/packages/lsp-tools-mcp/dist/cli.js +10 -0
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +10 -0
- package/packages/lsp-tools-mcp/dist/mcp.js +10 -0
- package/packages/lsp-tools-mcp/dist/tools.js +10 -0
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +84 -9
- 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 +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +10 -0
- 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 +1 -1
- 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 +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +22 -13
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/findings.ts +7 -6
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/lint.ts +18 -24
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/markup.ts +14 -7
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/selector.ts +5 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +32 -14
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +1 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +1 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +1 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +5 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +2 -2
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.css +4 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.tsx +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/{negative → positive}/flat-stripe-side.tsx +3 -1
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/report-callouts.html +8 -0
- package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/square-surfaces.css +8 -0
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +10 -2
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +10 -0
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-visualization.md +23 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +86 -11
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/frontend/SKILL.md +6 -6
- package/packages/shared-skills/skills/frontend/references/design/README.md +2 -2
- package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +22 -13
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/findings.ts +7 -6
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/lint.ts +18 -24
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/markup.ts +14 -7
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/selector.ts +5 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +32 -14
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.test.ts +15 -6
- package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +1 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +1 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +1 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +5 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +2 -2
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.css +4 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/accent-outlines.tsx +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/{negative → positive}/flat-stripe-side.tsx +3 -1
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/report-callouts.html +8 -0
- package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/square-surfaces.css +8 -0
- package/packages/shared-skills/skills/ulw-research/SKILL.md +10 -2
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +10 -0
- package/packages/shared-skills/skills/ulw-research/references/report-visualization.md +23 -0
|
@@ -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 put a coloured
|
|
205
|
+
- **Don't put a coloured bar on one side of a box, rounded or square, or an accent outline on a rounded card, for decoration or for state.** A feature card with `border-top: 4px solid` accent, a callout or cover block 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 2px primary outline on the focused card: each 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, table rules, and a blockquote's neutral rule 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.
|
|
@@ -222,7 +222,7 @@ Once references are loaded, before writing any UI code:
|
|
|
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
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
|
|
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, or an accent outline, to replace, per the anti-pattern above.
|
|
226
226
|
|
|
227
227
|
## Quick lookup table — most common requests
|
|
228
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
|
-
- **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
|
|
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 any box, rounded or square, and a 2px-or-wider accent outline (border or inset ring) on a rounded card, 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, table rules, and a blockquote's neutral rule 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,7 +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
|
|
95
|
+
bun $SKILL_DIR/scripts/perfection/check-accent-stripes.ts src/ # exit 1 = coloured one-side bar, or accent outline on a rounded card
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
Or use the TypeScript approach directly in your test suite:
|
package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/declarations.ts
CHANGED
|
@@ -72,10 +72,6 @@ function collectEdges(declarations: readonly Declaration[]): Map<Side, Edge> {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
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
75
|
const CORNER_LONGHAND: Readonly<Record<string, Corner>> = {
|
|
80
76
|
"top-left": "tl", "top-right": "tr", "bottom-right": "br", "bottom-left": "bl",
|
|
81
77
|
"start-start": "tl", "start-end": "tr", "end-end": "br", "end-start": "bl",
|
|
@@ -104,20 +100,33 @@ export function roundedCorners(declarations: readonly Declaration[]): Set<Corner
|
|
|
104
100
|
return corners
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
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
|
|
103
|
+
const isStripe = (e: Edge) => !e.hidden && (e.width ?? 0) >= MIN_STRIPE_PX && e.color === "chromatic"
|
|
104
|
+
|
|
105
|
+
export function hasEdgeStripe(declarations: readonly Declaration[]): boolean {
|
|
106
|
+
const edges = [...collectEdges(declarations).values()]
|
|
107
|
+
if (!edges.some(isStripe)) return false
|
|
116
108
|
const first = edges[0]
|
|
117
109
|
const uniform = edges.every((e) => e.width === first?.width && e.color === first?.color && e.hidden === first?.hidden)
|
|
118
110
|
return !uniform
|
|
119
111
|
}
|
|
120
112
|
|
|
113
|
+
function isInsetRing(value: string): boolean {
|
|
114
|
+
return splitTopLevel(value, ",").some((layer) => {
|
|
115
|
+
const tokens = splitTopLevel(layer, " ")
|
|
116
|
+
if (!tokens.some((t) => t.toLowerCase() === "inset")) return false
|
|
117
|
+
const [x = 0, y = 0, blur = 0, spread = 0] = tokens.map(parseLength).filter((n): n is number => n !== undefined)
|
|
118
|
+
return x === 0 && y === 0 && blur === 0 && spread >= MIN_STRIPE_PX && tokens.some((t) => looksLikeColor(t) && classifyColor(t) === "chromatic")
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A uniform accent border (not a gradient border-image) or inset ring of MIN_STRIPE_PX or more. */
|
|
123
|
+
export function hasAccentOutline(declarations: readonly Declaration[]): boolean {
|
|
124
|
+
const image = declarations.some(({ prop }) => prop === "border-image" || prop === "border-image-source")
|
|
125
|
+
const edges = [...collectEdges(declarations).values()]
|
|
126
|
+
const frame = !image && edges.every((e) => isStripe(e) && e.width === edges[0]?.width)
|
|
127
|
+
return frame || declarations.some(({ prop, value }) => prop === "box-shadow" && isInsetRing(value))
|
|
128
|
+
}
|
|
129
|
+
|
|
121
130
|
export function hasRadius(declarations: readonly Declaration[]): boolean {
|
|
122
131
|
return roundedCorners(declarations).size > 0
|
|
123
132
|
}
|
package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/findings.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type RuleId = "edge-stripe" | "pseudo-bar" | "inset-shadow-stripe" | "gradient-stripe"
|
|
1
|
+
export type RuleId = "edge-stripe" | "pseudo-bar" | "inset-shadow-stripe" | "gradient-stripe" | "accent-outline"
|
|
2
2
|
|
|
3
3
|
export type Finding = {
|
|
4
4
|
readonly file: string
|
|
@@ -9,14 +9,15 @@ export type Finding = {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const RULE_MESSAGES: Readonly<Record<RuleId, string>> = {
|
|
12
|
-
"edge-stripe": "coloured one-side border on a rounded
|
|
13
|
-
"pseudo-bar": "coloured ::before/::after bar
|
|
14
|
-
"inset-shadow-stripe": "inset box-shadow stripe
|
|
15
|
-
"gradient-stripe": "hard-stop gradient band along one edge of a
|
|
12
|
+
"edge-stripe": "coloured one-side border on a box, rounded or square",
|
|
13
|
+
"pseudo-bar": "coloured ::before/::after bar along one side of a box",
|
|
14
|
+
"inset-shadow-stripe": "inset box-shadow stripe along one side of a box",
|
|
15
|
+
"gradient-stripe": "hard-stop gradient band along one edge of a box",
|
|
16
|
+
"accent-outline": "accent-tinted full outline on a rounded card",
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
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
|
+
"drop the stripe or outline: separate with a tonal surface (one ink at a low alpha), an eyebrow or heading, a leading glyph, or spacing"
|
|
20
21
|
|
|
21
22
|
export function lineLocator(text: string): (offset: number) => { line: number; column: number } {
|
|
22
23
|
const starts = [0]
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type CssRule, type Declaration, parseCss } from "./css-parse"
|
|
2
|
-
import {
|
|
2
|
+
import { hasAccentOutline, hasEdgeStripe, hasInsetShadowStripe, hasRadius, isPseudoBar, roundedCorners } from "./declarations"
|
|
3
3
|
import { type Finding, lineLocator, type RuleId } from "./findings"
|
|
4
4
|
import { hasGradientEdgeBand } from "./gradient"
|
|
5
5
|
import { classGroups, cssTemplates, inlineStyles, styleBlocks } from "./markup"
|
|
6
|
-
import { type Compound, compoundCovers, isExemptSelector, parseCompound, selectorParts } from "./selector"
|
|
6
|
+
import { type Compound, compoundCovers, isExemptSelector, isExemptTag, parseCompound, selectorParts } from "./selector"
|
|
7
7
|
import { checkClassSet } from "./tailwind"
|
|
8
8
|
|
|
9
9
|
export type SourceFile = { readonly path: string; readonly text: string }
|
|
@@ -25,18 +25,14 @@ function radiusIndex(rules: readonly CssRule[]): Compound[] {
|
|
|
25
25
|
return rules.filter(ruleHasRadius).flatMap((r) => selectorParts(r.selector).map(parseCompound))
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (hasRadius(rule.declarations)) return roundedCorners(rule.declarations)
|
|
33
|
-
if (ruleHasRadius(rule)) return ALL_CORNERS
|
|
34
|
-
}
|
|
35
|
-
const host = parseCompound(part.replace(/::?(before|after)\b/gi, ""))
|
|
36
|
-
return index.some((general) => compoundCovers(general, host)) ? ALL_CORNERS : NO_CORNERS
|
|
28
|
+
function isRoundedHost(part: string, rule: CssRule, index: readonly Compound[]): boolean {
|
|
29
|
+
if (ruleHasRadius(rule)) return true
|
|
30
|
+
const host = parseCompound(part)
|
|
31
|
+
return index.some((general) => compoundCovers(general, host))
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
const EDGE_PROP = /^border(?:-(?:top|right|bottom|left|block|inline|width|color|image)(?:-[\w-]+)?)?$/
|
|
35
|
+
const OUTLINE_PROP = /^(?:border(?:-width|-color)?|box-shadow)$/
|
|
40
36
|
const BACKGROUND_PROP = /^background(?:-color|-image)?$/
|
|
41
37
|
|
|
42
38
|
function declarationOffset(declarations: readonly Declaration[], prop: RegExp, fallback: number): number {
|
|
@@ -45,9 +41,10 @@ function declarationOffset(declarations: readonly Declaration[], prop: RegExp, f
|
|
|
45
41
|
|
|
46
42
|
type Hit = { readonly id: RuleId; readonly offset: number }
|
|
47
43
|
|
|
48
|
-
function declarationHits(declarations: readonly Declaration[],
|
|
44
|
+
function declarationHits(declarations: readonly Declaration[], rounded: boolean, fallback: number): Hit[] {
|
|
49
45
|
const hits: Hit[] = []
|
|
50
|
-
if (hasEdgeStripe(declarations
|
|
46
|
+
if (hasEdgeStripe(declarations)) hits.push({ id: "edge-stripe", offset: declarationOffset(declarations, EDGE_PROP, fallback) })
|
|
47
|
+
else if (rounded && hasAccentOutline(declarations)) hits.push({ id: "accent-outline", offset: declarationOffset(declarations, OUTLINE_PROP, fallback) })
|
|
51
48
|
if (hasInsetShadowStripe(declarations)) hits.push({ id: "inset-shadow-stripe", offset: declarationOffset(declarations, /^box-shadow$/, fallback) })
|
|
52
49
|
if (hasGradientEdgeBand(declarations)) hits.push({ id: "gradient-stripe", offset: declarationOffset(declarations, BACKGROUND_PROP, fallback) })
|
|
53
50
|
return hits
|
|
@@ -57,17 +54,14 @@ function checkRule(rule: CssRule, index: readonly Compound[]): Hit[] {
|
|
|
57
54
|
const found = new Map<RuleId, number>()
|
|
58
55
|
const add = (hit: Hit) => found.set(hit.id, found.get(hit.id) ?? hit.offset)
|
|
59
56
|
for (const part of selectorParts(rule.selector)) {
|
|
60
|
-
|
|
61
|
-
if (corners.size === 0) continue
|
|
57
|
+
if (isExemptSelector(part)) continue
|
|
62
58
|
if (parseCompound(part).pseudoElement) {
|
|
63
59
|
if (isPseudoBar(rule.declarations)) add({ id: "pseudo-bar", offset: declarationOffset(rule.declarations, BACKGROUND_PROP, rule.offset) })
|
|
64
60
|
continue
|
|
65
61
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const id of checkClassSet(
|
|
69
|
-
add({ id, offset: rule.applyOffset ?? rule.offset })
|
|
70
|
-
}
|
|
62
|
+
const rounded = isRoundedHost(part, rule, index)
|
|
63
|
+
declarationHits(rule.declarations, rounded, rule.offset).forEach(add)
|
|
64
|
+
for (const id of checkClassSet(rule.applyClasses, rounded)) add({ id, offset: rule.applyOffset ?? rule.offset })
|
|
71
65
|
}
|
|
72
66
|
return [...found].map(([id, offset]) => ({ id, offset }))
|
|
73
67
|
}
|
|
@@ -97,13 +91,13 @@ export function lintFiles(files: readonly SourceFile[]): Finding[] {
|
|
|
97
91
|
}
|
|
98
92
|
if (STYLESHEET.test(file.path)) continue
|
|
99
93
|
for (const group of classGroups(file.text, SCRIPT.test(file.path))) {
|
|
94
|
+
if (isExemptTag(group.tag)) continue
|
|
100
95
|
for (const id of checkClassSet(group.tokens)) report(group.offset, id)
|
|
101
96
|
}
|
|
102
97
|
for (const style of inlineStyles(file.text)) {
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
for (const hit of declarationHits(style.declarations, corners, style.offset)) report(style.offset, hit.id)
|
|
98
|
+
if (isExemptTag(style.tag)) continue
|
|
99
|
+
const rounded = style.tagRounded || roundedCorners(style.declarations).size > 0
|
|
100
|
+
for (const hit of declarationHits(style.declarations, rounded, style.offset)) report(style.offset, hit.id)
|
|
107
101
|
}
|
|
108
102
|
}
|
|
109
103
|
return findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Declaration } from "./css-parse"
|
|
2
2
|
import { looksLikeClassList } from "./tailwind"
|
|
3
3
|
|
|
4
|
-
export type ClassGroup = { readonly tokens: readonly string[]; readonly offset: number }
|
|
5
|
-
export type InlineStyle = { readonly declarations: readonly Declaration[]; readonly offset: number; readonly tagRounded: boolean }
|
|
4
|
+
export type ClassGroup = { readonly tokens: readonly string[]; readonly offset: number; readonly tag: string }
|
|
5
|
+
export type InlineStyle = { readonly declarations: readonly Declaration[]; readonly offset: number; readonly tagRounded: boolean; readonly tag: string }
|
|
6
6
|
export type StyleBlock = { readonly css: string; readonly offset: number }
|
|
7
7
|
|
|
8
8
|
const UNITLESS = new Set(["opacity", "z-index", "flex", "flex-grow", "flex-shrink", "font-weight", "line-height", "order"])
|
|
@@ -51,6 +51,13 @@ function stripInterpolation(literal: string): string {
|
|
|
51
51
|
return literal.replace(/\$\{[^}]*\}/g, " ")
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Name of the element whose open tag contains `offset` ("" outside a tag, e.g. a cva string). */
|
|
55
|
+
function tagNameAt(text: string, offset: number): string {
|
|
56
|
+
const open = text.lastIndexOf("<", offset)
|
|
57
|
+
if (open < 0 || text.slice(open, offset).includes(">")) return ""
|
|
58
|
+
return /^<([A-Za-z][\w.-]*)/.exec(text.slice(open))?.[1] ?? ""
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
function tokensOf(text: string): string[] {
|
|
55
62
|
return text.split(/\s+/).filter(Boolean)
|
|
56
63
|
}
|
|
@@ -83,11 +90,11 @@ export function classGroups(text: string, scanLiterals: boolean): ClassGroup[] {
|
|
|
83
90
|
if (ch === '"' || ch === "'") {
|
|
84
91
|
const close = text.indexOf(ch, start + 1)
|
|
85
92
|
if (close < 0) continue
|
|
86
|
-
groups.push({ tokens: tokensOf(text.slice(start + 1, close)), offset: start })
|
|
93
|
+
groups.push({ tokens: tokensOf(text.slice(start + 1, close)), offset: start, tag: tagNameAt(text, start) })
|
|
87
94
|
covered.push([start, close])
|
|
88
95
|
} else if (ch === "{") {
|
|
89
96
|
const { end, literals } = readBalanced(text, start)
|
|
90
|
-
groups.push({ tokens: literals.flatMap(tokensOf), offset: start })
|
|
97
|
+
groups.push({ tokens: literals.flatMap(tokensOf), offset: start, tag: tagNameAt(text, start) })
|
|
91
98
|
covered.push([start, end])
|
|
92
99
|
}
|
|
93
100
|
}
|
|
@@ -96,7 +103,7 @@ export function classGroups(text: string, scanLiterals: boolean): ClassGroup[] {
|
|
|
96
103
|
const offset = m.index ?? 0
|
|
97
104
|
if (covered.some(([a, b]) => offset >= a && offset <= b)) continue
|
|
98
105
|
const body = stripInterpolation(m[2] ?? "")
|
|
99
|
-
if (looksLikeClassList(body)) groups.push({ tokens: tokensOf(body), offset })
|
|
106
|
+
if (looksLikeClassList(body)) groups.push({ tokens: tokensOf(body), offset, tag: "" })
|
|
100
107
|
}
|
|
101
108
|
return groups
|
|
102
109
|
}
|
|
@@ -136,13 +143,13 @@ export function inlineStyles(text: string): InlineStyle[] {
|
|
|
136
143
|
for (const m of text.matchAll(/\bstyle\s*=\s*\{\{([\s\S]*?)\}\}/g)) {
|
|
137
144
|
const offset = m.index ?? 0
|
|
138
145
|
const end = offset + m[0].length
|
|
139
|
-
styles.push({ declarations: objectDeclarations(m[1] ?? ""), offset, tagRounded: tagIsRounded(text, offset, end) })
|
|
146
|
+
styles.push({ declarations: objectDeclarations(m[1] ?? ""), offset, tagRounded: tagIsRounded(text, offset, end), tag: tagNameAt(text, offset) })
|
|
140
147
|
}
|
|
141
148
|
for (const m of text.matchAll(/\bstyle\s*=\s*(["'])([\s\S]*?)\1/g)) {
|
|
142
149
|
const offset = m.index ?? 0
|
|
143
150
|
const body = (m[2] ?? "").trim()
|
|
144
151
|
const declarations = body.startsWith("{") ? objectDeclarations(body) : cssDeclarations(body)
|
|
145
|
-
styles.push({ declarations, offset, tagRounded: tagIsRounded(text, offset, offset + m[0].length) })
|
|
152
|
+
styles.push({ declarations, offset, tagRounded: tagIsRounded(text, offset, offset + m[0].length), tag: tagNameAt(text, offset) })
|
|
146
153
|
}
|
|
147
154
|
return styles
|
|
148
155
|
}
|
package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/selector.ts
CHANGED
|
@@ -27,9 +27,13 @@ export function parseCompound(part: string): Compound {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function isExemptTag(tag: string): boolean {
|
|
31
|
+
return EXEMPT_TAGS.has(tag.toLowerCase())
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
export function isExemptSelector(part: string): boolean {
|
|
31
35
|
if (EXEMPT_STATE.test(part)) return true
|
|
32
|
-
return
|
|
36
|
+
return isExemptTag(parseCompound(part).tag)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
export function selectorParts(selector: string): string[] {
|
package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts
CHANGED
|
@@ -43,11 +43,15 @@ const SIDE_TOUCHES: Readonly<Record<string, readonly Corner[]>> = {
|
|
|
43
43
|
}
|
|
44
44
|
const ALL: readonly Corner[] = ["tl", "tr", "br", "bl"]
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return (width
|
|
46
|
+
const SIDE = /^(?:[tlrbsexy]|bs|be)-/
|
|
47
|
+
|
|
48
|
+
function isWide(width: string | undefined): boolean {
|
|
49
|
+
if (!width) return false
|
|
50
|
+
return ((/^\d+$/.test(width) ? Number.parseInt(width, 10) : arbitraryPx(width)) ?? 0) >= MIN_STRIPE_PX
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isWideSide(util: string): boolean {
|
|
54
|
+
return isWide(/^border-(?:[tlrbsexy]|bs|be)-(\d+|\[[^\]]+\])$/.exec(util)?.[1])
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
function roundedCorners(utils: readonly string[]): Set<Corner> {
|
|
@@ -65,14 +69,30 @@ function roundedCorners(utils: readonly string[]): Set<Corner> {
|
|
|
65
69
|
return corners
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
function
|
|
69
|
-
const m = /^border(?:-(?:[tlrbsexy]|bs|be))?-(.+)$/.exec(util)
|
|
70
|
-
const fragment = m?.[1]
|
|
72
|
+
function colorIsChromatic(fragment: string | undefined): boolean {
|
|
71
73
|
if (!fragment || BORDER_STYLE.has(fragment) || /^\d+$/.test(fragment)) return false
|
|
72
74
|
if (fragment.startsWith("[") && arbitraryPx(fragment) !== undefined) return false
|
|
73
75
|
return classifyUtilityColor(fragment) === "chromatic"
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
function borderColorIsChromatic(util: string): boolean {
|
|
79
|
+
return colorIsChromatic(/^border(?:-(?:[tlrbsexy]|bs|be))?-(.+)$/.exec(util)?.[1])
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hasOutline(utils: readonly string[]): boolean {
|
|
83
|
+
const border = utils.some((u) => isWide(/^border-(\d+|\[[^\]]+\])$/.exec(u)?.[1]))
|
|
84
|
+
const borderColor = utils.some((u) => {
|
|
85
|
+
const fragment = /^border-(.+)$/.exec(u)?.[1]
|
|
86
|
+
return fragment !== undefined && !SIDE.test(fragment) && colorIsChromatic(fragment)
|
|
87
|
+
})
|
|
88
|
+
const ring = utils.some((u) => isWide(/^ring-(\d+|\[[^\]]+\])$/.exec(u)?.[1]))
|
|
89
|
+
const ringColor = utils.some((u) => {
|
|
90
|
+
const fragment = /^ring-(.+)$/.exec(u)?.[1]
|
|
91
|
+
return fragment !== undefined && !/^(?:inset|offset-)/.test(fragment) && colorIsChromatic(fragment)
|
|
92
|
+
})
|
|
93
|
+
return (border && borderColor) || (ring && ringColor)
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
function thinBar(utils: readonly string[], axis: "w" | "h"): boolean {
|
|
77
97
|
return utils.some((u) => {
|
|
78
98
|
const m = new RegExp(`^${axis}-(px|0\\.5|1|1\\.5|2|\\[[^\\]]+\\])$`).exec(u)
|
|
@@ -93,14 +113,12 @@ function isPseudoBar(utils: readonly string[]): boolean {
|
|
|
93
113
|
return chromaticBg && ((thinBar(utils, "w") && longBar(utils, "h")) || (thinBar(utils, "h") && longBar(utils, "w")))
|
|
94
114
|
}
|
|
95
115
|
|
|
96
|
-
export function checkClassSet(tokens: readonly string[]): RuleId[] {
|
|
116
|
+
export function checkClassSet(tokens: readonly string[], hostRounded = false): RuleId[] {
|
|
97
117
|
const utilities = tokens.map(toUtility).filter((u): u is Utility => u !== undefined)
|
|
98
118
|
const base = utilities.filter((u) => u.layer === "base").map((u) => u.util)
|
|
99
|
-
const corners = roundedCorners(base)
|
|
100
|
-
if (corners.size === 0) return []
|
|
101
119
|
const found: RuleId[] = []
|
|
102
|
-
|
|
103
|
-
if (
|
|
120
|
+
if (base.some(isWideSide) && base.some(borderColorIsChromatic)) found.push("edge-stripe")
|
|
121
|
+
else if ((hostRounded || roundedCorners(base).size > 0) && hasOutline(base)) found.push("accent-outline")
|
|
104
122
|
const shadows = base.filter((u) => u.startsWith("shadow-[") && u.endsWith("]"))
|
|
105
123
|
if (shadows.some((u) => isInsetShadowStripe(u.slice("shadow-[".length, -1).replace(/_/g, " ")))) {
|
|
106
124
|
found.push("inset-shadow-stripe")
|
|
@@ -113,5 +131,5 @@ export function checkClassSet(tokens: readonly string[]): RuleId[] {
|
|
|
113
131
|
}
|
|
114
132
|
|
|
115
133
|
export function looksLikeClassList(text: string): boolean {
|
|
116
|
-
return /(?:^|\s)(?:[\w-]+:)*rounded/.test(text)
|
|
134
|
+
return /(?:^|\s)(?:[\w-]+:)*(?:rounded|border-|ring-|shadow-\[|(?:before|after):)/.test(text)
|
|
117
135
|
}
|
package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.test.ts
CHANGED
|
@@ -22,6 +22,11 @@ const EXPECTED: Readonly<Record<string, ReadonlyArray<[number, RuleId]>>> = {
|
|
|
22
22
|
"gradient-edges.css": [[3, "gradient-stripe"], [8, "edge-stripe"], [20, "pseudo-bar"], [25, "gradient-stripe"]],
|
|
23
23
|
"non-visible-focus-and-nested.tsx": [[2, "edge-stripe"], [3, "edge-stripe"], [6, "edge-stripe"]],
|
|
24
24
|
"styled-components.tsx": [[6, "edge-stripe"], [12, "edge-stripe"]],
|
|
25
|
+
"flat-stripe-side.tsx": [[2, "edge-stripe"], [7, "edge-stripe"], [8, "edge-stripe"], [9, "edge-stripe"], [10, "edge-stripe"], [11, "edge-stripe"]],
|
|
26
|
+
"report-callouts.html": [[3, "edge-stripe"], [5, "edge-stripe"]],
|
|
27
|
+
"square-surfaces.css": [[1, "edge-stripe"], [4, "pseudo-bar"], [6, "inset-shadow-stripe"], [8, "gradient-stripe"]],
|
|
28
|
+
"accent-outlines.css": [[1, "accent-outline"], [2, "accent-outline"], [4, "accent-outline"]],
|
|
29
|
+
"accent-outlines.tsx": [[3, "accent-outline"], [4, "accent-outline"], [5, "accent-outline"], [6, "accent-outline"]],
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
function lintFixture(dir: string, name: string) {
|
|
@@ -29,7 +34,7 @@ function lintFixture(dir: string, name: string) {
|
|
|
29
34
|
return lintFiles([{ path, text: readFileSync(path, "utf8") }])
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
describe("#given a positive fixture with a coloured
|
|
37
|
+
describe("#given a positive fixture with a coloured one-side bar or an accent outline on a rounded card", () => {
|
|
33
38
|
test("#then every positive fixture has an expectation", () => {
|
|
34
39
|
expect(readdirSync(join(FIXTURES, "positive")).sort()).toEqual(Object.keys(EXPECTED).sort())
|
|
35
40
|
})
|
|
@@ -42,7 +47,7 @@ describe("#given a positive fixture with a coloured edge on a rounded surface",
|
|
|
42
47
|
}
|
|
43
48
|
})
|
|
44
49
|
|
|
45
|
-
describe("#given a negative fixture (neutral hairlines,
|
|
50
|
+
describe("#given a negative fixture (neutral hairlines and rules, thin outlines, focus rings, validation, tables, overlays)", () => {
|
|
46
51
|
for (const name of readdirSync(join(FIXTURES, "negative"))) {
|
|
47
52
|
test(`#when ${name} is linted #then it reports nothing`, () => {
|
|
48
53
|
expect(lintFixture("negative", name)).toEqual([])
|
|
@@ -50,13 +55,17 @@ describe("#given a negative fixture (neutral hairlines, flat edges, focus rings,
|
|
|
50
55
|
}
|
|
51
56
|
})
|
|
52
57
|
|
|
53
|
-
describe("#given the radius and the
|
|
54
|
-
test("#when both files are linted together #then the
|
|
58
|
+
describe("#given the radius and the accent outline live in different stylesheets", () => {
|
|
59
|
+
test("#when both files are linted together #then the outline rule is flagged in its own file", () => {
|
|
55
60
|
const findings = lintFiles([
|
|
56
61
|
{ path: "base.css", text: ".card { border-radius: 12px; }\n" },
|
|
57
|
-
{ path: "feature.css", text: "\n.card.card--promo { border
|
|
62
|
+
{ path: "feature.css", text: "\n.card.card--promo { border: 2px solid #f97316; }\n" },
|
|
58
63
|
])
|
|
59
|
-
expect(findings.map((f) => [f.file, f.line, f.rule])).toEqual([["feature.css", 2, "
|
|
64
|
+
expect(findings.map((f) => [f.file, f.line, f.rule])).toEqual([["feature.css", 2, "accent-outline"]])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("#when the outline rule is linted without the radius stylesheet #then a square box is not flagged", () => {
|
|
68
|
+
expect(lintFiles([{ path: "feature.css", text: ".card.card--promo { border: 2px solid #f97316; }\n" }])).toEqual([])
|
|
60
69
|
})
|
|
61
70
|
})
|
|
62
71
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// Flags
|
|
2
|
+
// Flags coloured one-side bars on any box and accent outlines on rounded cards in CSS, Tailwind and JSX.
|
|
3
3
|
// Usage: bun check-accent-stripes.ts [--json] <file-or-dir>... Exit 0 clean, 1 findings, 2 usage.
|
|
4
4
|
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
5
5
|
import { join } from "node:path"
|
|
@@ -3,5 +3,6 @@ export const Ok = () => (
|
|
|
3
3
|
<div style={{ borderRadius: 8, border: "1px solid #e5e7eb" }}>Card</div>
|
|
4
4
|
<div style={{ borderRadius: 8, boxShadow: "inset 0 0 0 1px rgba(0,0,0,.1)" }}>Ring</div>
|
|
5
5
|
<p>Don't stripe {"rounded"} cards.</p>
|
|
6
|
+
<table><tbody><tr><td style={{ borderLeft: "4px solid #6366f1" }}>Sorted</td></tr></tbody></table>
|
|
6
7
|
</>
|
|
7
8
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.list-item { border-bottom: 1px solid #e5e7eb; }
|
|
2
2
|
.panel { border-radius: 12px; border-top: 1px solid rgba(0, 0, 0, 0.08); }
|
|
3
3
|
.quiet { border-radius: 12px; border-left: 4px solid #e5e7eb; }
|
|
4
|
-
|
|
4
|
+
blockquote { border-left: 3px solid #d4d4d8; padding-left: 16px; }
|
|
5
5
|
.token { border-radius: 8px; border-left: 2px solid var(--color-border); }
|
|
6
6
|
.ink { border-radius: 8px; border-left: 2px solid var(--text-primary); }
|
|
@@ -4,8 +4,11 @@ export const Ok = () => (
|
|
|
4
4
|
<div className="rounded-xl border-l-4 border-l-zinc-300 p-4">Neutral edge</div>
|
|
5
5
|
<button className="rounded-md focus-visible:ring-2 focus-visible:ring-blue-500">Go</button>
|
|
6
6
|
<input className="rounded-md border-b-2 aria-invalid:border-b-red-500 invalid:border-red-500" />
|
|
7
|
-
<
|
|
8
|
-
<
|
|
7
|
+
<blockquote className="border-l-2 border-zinc-300 pl-4">Neutral quote rule</blockquote>
|
|
8
|
+
<button className="rounded-md border border-indigo-500 px-3">Outline button</button>
|
|
9
9
|
<div className="rounded-lg bg-blue-50 p-4 text-blue-900">Tonal surface</div>
|
|
10
|
+
<input className="border-b-2 border-indigo-500" />
|
|
11
|
+
<select className="rounded-md border-2 border-indigo-500" />
|
|
12
|
+
<table><tbody><tr><td className="border-l-4 border-indigo-500">Sorted</td></tr></tbody></table>
|
|
10
13
|
</div>
|
|
11
14
|
)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
.
|
|
2
|
-
.
|
|
1
|
+
.outline-button { border-radius: 6px; border: 1px solid var(--primary); }
|
|
2
|
+
.neutral-frame { border-radius: 8px; border: 2px solid #e5e7eb; }
|
|
3
3
|
.lift { border-radius: 12px; box-shadow: 0 4px 12px rgb(0 0 0 / 0.12); }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const Plans = () => (
|
|
2
|
+
<>
|
|
3
|
+
<div className="rounded-xl border-2 border-indigo-500 p-6">Pro</div>
|
|
4
|
+
<div className="rounded-2xl p-6 ring-2 ring-primary">Team</div>
|
|
5
|
+
<div className="rounded-lg focus:ring-2 focus:ring-blue-500">Focused</div>
|
|
6
|
+
<div style={{ borderRadius: 12, border: "2px solid #6366f1" }}>Inline</div>
|
|
7
|
+
</>
|
|
8
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const styles = css`
|
|
2
2
|
.alert { border-left: 4px solid #dc2626; border-radius: 0 6px 6px 0; }
|
|
3
3
|
`
|
|
4
4
|
|
|
@@ -7,5 +7,7 @@ export const Quotes = () => (
|
|
|
7
7
|
<blockquote className="rounded-r-lg border-l-4 border-blue-500 pl-4">Square on the stripe side</blockquote>
|
|
8
8
|
<aside className="rounded-xl rounded-l-none border-l-4 border-sky-500">Left corners squared</aside>
|
|
9
9
|
<style>{`.note { border-left: 4px solid #16a34a; border-top-right-radius: 8px; border-bottom-right-radius: 8px; }`}</style>
|
|
10
|
+
<div className="border-l-4 border-blue-500 pl-4">Flat quote, no corners</div>
|
|
11
|
+
<div className="rounded-none border-l-4 border-blue-500">Square</div>
|
|
10
12
|
</>
|
|
11
13
|
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
h2 { padding-bottom: 4px; border-bottom: 2px solid #1d1d1f; }
|
|
3
|
+
.cover { border-left: 6px solid #2f7d4f; padding: 6px 0 6px 14px; margin-bottom: 14px; }
|
|
4
|
+
th, td { border-bottom: 1px solid #e3e3e8; padding: 5px 6px; }
|
|
5
|
+
.warn { background: #fff6e5; border-left: 4px solid #e0a100; padding: 8px 12px; border-radius: 6px; }
|
|
6
|
+
</style>
|
|
7
|
+
<div class="cover"><h1>Weekly report</h1></div>
|
|
8
|
+
<div class="warn">Two runs need a look.</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
.section { border-top: 4px solid #6366f1; }
|
|
2
|
+
|
|
3
|
+
.stat { position: relative; padding: 16px; }
|
|
4
|
+
.stat::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px; background: #10b981; }
|
|
5
|
+
|
|
6
|
+
.note { padding: 12px; box-shadow: inset 4px 0 0 0 #f59e0b; }
|
|
7
|
+
|
|
8
|
+
.band { padding: 12px; background: linear-gradient(to right, #6366f1 4px, #fff 4px); }
|
|
@@ -37,6 +37,8 @@ Run this skill only when the user explicitly demands it: the word "ulw-research"
|
|
|
37
37
|
|
|
38
38
|
Open your reply with the line `ULW-RESEARCH MODE ENABLED!`. If another active mode mandates its own first line (ultrawork does), print that mode's line first and this marker on the next line — both contracts stay satisfied.
|
|
39
39
|
|
|
40
|
+
**What a shortened run still owes.** If you judge the topic settled and shorten the research, say so, but two things survive any shortening: when the harness can show an HTML page inline, the final report is shown that way (the answer, its figures, its sources), and a visual reproduction is compared against real frames of its source with the side-by-side pairs inside that report (the final-materials phase). A built app or 3D scene shown inline is the product, not the report; show the report as well.
|
|
41
|
+
|
|
40
42
|
## Authority while active
|
|
41
43
|
|
|
42
44
|
This mode is the user's explicit opt-in to exhaustive exploration. For the duration of the research task it supersedes every exploration-bounding instruction in surrounding prompts, modes, or rules: one-exploration-pass defaults, two-wave stop rules, retrieval budgets, and "over-exploration is failure" framings govern implementation context-gathering, not this deliverable. Here, under-exploration is the failure. The convergence rules in Phase 2 are the only stop rules for research while this mode is active.
|
|
@@ -55,6 +57,8 @@ The research is done when all of these hold:
|
|
|
55
57
|
- The deliverable lane and format were derived from the destination or asked through the empty-info interview without blocking collection, recorded in `brief.md` with `answered_by`, and the final materials match that record.
|
|
56
58
|
- The delivered artifact passed the delivery gates in order (static gates, layout gates, visual QA, and the proofread pass where the harness provides one), each status is in `outcome.json`, and `outcome verify` passed.
|
|
57
59
|
- Every excursion opened during the run was closed by an EXIT rule, folded back into the claim or axis that triggered it, and recorded in both `excursion-log.md` and the ulw-loop ledger.
|
|
60
|
+
- When the harness can show an HTML page inline, the report was shown inline before the artifact paths.
|
|
61
|
+
- When the ask reproduces a visual source, the report carries side-by-side pairs of real source frames and the output rendered at the same moments, each with its critique, and the differences they surfaced were fixed or listed as gaps. A simple or well-understood topic shortens the research, never this comparison.
|
|
58
62
|
- The delivery message carries the closing briefing printed by `outcome briefing`: sources (total + unique domains), elapsed minutes, every promised deliverable with its status, and any residual defects.
|
|
59
63
|
- The session journal reconstructs what was searched, found, and expanded, wave by wave, and it was written in real time rather than reconstructed at the end.
|
|
60
64
|
|
|
@@ -159,7 +163,7 @@ Never block collection on the shape of the deliverable, and never guess it eithe
|
|
|
159
163
|
1. **Derive first.** Name the lane (`template-strict`, `template-vibe`, `no-format`, `edit-existing`) and the promised formats from the request and its destination (the reference's section 3). When the request refers to an existing deliverable, decide its state with `node "$SKILL_DIR/scripts/report-tools.mjs" outcome state --deliverable <path> --session-dir "$SESSION_DIR"`: `partial` resumes the skeleton on disk, `complete` means edit-existing and never a regenerated report. `$SKILL_DIR` is this skill's own directory, the folder containing this SKILL.md.
|
|
160
164
|
2. **Read the requester's format memory** with the memory tool when the harness exposes one: the pointer `system/human/report-style.md`, then `reference/human-report-style.md`; take the choice recorded for the most similar context (same destination kind and audience) as the first option.
|
|
161
165
|
3. **Ask only what is still missing**: at most the reference's three questions (destination and format, audience and length, template lineage) in one call to the harness question tool (`ask_user_question` or `request_user_input`), non-blocking where the harness supports it, each with its default first, a free-text "describe the format" path, and "don't care, you decide". Collection starts without waiting; a late answer is folded in until the assembly worker starts, and after that it becomes a re-render request.
|
|
162
|
-
4. **Record it.** Write `## Deliverable` into `brief.md` (lane, state, formats, destination, audience, template, format description, each with `answered_by: user|default|request`) and open the manifest in the same step: `node "$SKILL_DIR/scripts/report-tools.mjs" outcome init --promised <formats> --lane <lane> --session-dir "$SESSION_DIR"`.
|
|
166
|
+
4. **Record it.** Write `## Deliverable` into `brief.md` (lane, state, formats, destination, audience, template, format description, each with `answered_by: user|default|request`) and open the manifest in the same step: `node "$SKILL_DIR/scripts/report-tools.mjs" outcome init --promised <formats> --lane <lane> --session-dir "$SESSION_DIR"`. When the ask is to recreate, reproduce, or rebuild something visual, add `Reference compare: required` with the source (URL or file) and the moments to compare; the comparison runs on every round of the build, not only at the end.
|
|
163
167
|
|
|
164
168
|
Phase 5 opens by turning the recorded fields into `design-spec.md`.
|
|
165
169
|
|
|
@@ -334,6 +338,8 @@ Asset workers (background, parallel, each fed `design-spec.md`) — visuals are
|
|
|
334
338
|
|
|
335
339
|
**Every final report is visual — binding.** It carries at least one data visualization of the run's own findings (a chart drawn from the data table, next to the claim it supports), a diagram wherever structure matters, and the topic's own screenshots and video, embedded or linked with attribution; every figure is captioned, sourced, and given alt text. Mermaid for flows, timelines, and relations; generated charts for numbers; image generation only where an illustration genuinely helps; PlotNeuralNet for paper-format reports that describe model architectures. Every asset lane reads [references/report-visualization.md](references/report-visualization.md) first: which tool fits which finding, the render commands, the quality bar, and the checklist the gates apply. A light-tier post still carries one visual when it reports numbers or structure.
|
|
336
340
|
|
|
341
|
+
**Visual reproductions are compared against the source — binding.** When the ask is to recreate, reproduce, or rebuild something visual (a clip in 3D, an interface from a screenshot, a figure), real frames or captures from the source are the reference: extract them at the moments that define the source, render the output at the same moments and viewpoints, compose side-by-side pairs, critique every pair by looking at it, fix the largest differences, and compare again within the repair bound. The final pairs, and the first-round pairs in the correction log, go into the report. Procedure: [references/report-visualization.md](references/report-visualization.md), section 3, Reference compare. A reproduction checked only against itself is unverified.
|
|
342
|
+
|
|
337
343
|
**Verify the asset manifest before rendering.** List every asset the document references, assert each file exists and is non-empty on disk, and re-render whatever is missing. A document that renders with three broken diagrams is a document you will publish twice.
|
|
338
344
|
|
|
339
345
|
Assembly worker — `task(category="deep-low", load_skills=["frontend", "visual-qa", "open-design", "data-scientist", "imagegen", "ulw-loop"], run_in_background=true, ...)`: before writing, read every available design and visualization skill and apply it — the report is a designed artifact, not a text dump; the worker's prompt carries `design-spec.md`. Use the template the user approved; absent a stronger house style the default skeleton is executive summary → key findings by theme → detailed analysis (quotes under 20 words with attribution, charts, Mermaid graphs, generated visuals, SHA-pinned permalinks, verification results) → comparative analysis when options compete → numbered sources with access dates → methodology appendix (workers, waves, searches, verifications, debate rounds) → correction log naming what verification overturned. Write it long and specific: every claim cites `[Source N]`, and the sources section lists every source the run actually used rather than a curated few.
|
|
@@ -349,7 +355,7 @@ Nothing reaches the user until the gates pass. Record each result with `node "$S
|
|
|
349
355
|
|
|
350
356
|
**Repair is bounded.** After every gate run that found defects, ask `node "$SKILL_DIR/scripts/report-tools.mjs" repair decide --state "$SESSION_DIR/repair-state.json" --defects "$SESSION_DIR/defects.json" --artifact-bytes <bytes> --renders <pages> --session-dir "$SESSION_DIR"` and obey it: `repair` means fix and re-run; `deliver` means stop and ship with the residual defects the manifest now lists; `block` means a content-integrity defect remains (or there is no usable artifact), so do not deliver: tell the user what blocks it and record the row as `failed` with the reason. Never repair past a stop decision (3 attempts, a 2-attempt plateau, an oscillation, or 15 minutes; the reference's section 8).
|
|
351
357
|
|
|
352
|
-
Then resolve every manifest row (`outcome set <format> delivered --path <file>` or `blocked_capability` / `skipped` / `failed` with `--reason`) and deliver: the artifact
|
|
358
|
+
Then resolve every manifest row (`outcome set <format> delivered --path <file>` or `blocked_capability` / `skipped` / `failed` with `--reason`) and deliver. **Show the report inline first when the harness can.** When this session has a tool that shows an HTML page inline in the conversation (`show_html_page`, `html_render`), the requester's first view of the result is the report itself: the gated `report.html` when it fits the tool, otherwise an inline edition built from the same synthesis and assets, with the reference-compare pairs when the run has them ([references/deliverable-phase.md](references/deliverable-phase.md), section 9, Showing the report inline). Then give the artifact paths and a compact chat-readable summary of what the report says — the answer in a few sentences, the numbers that matter, and what to look at first. This holds on every tier, a chat answer included, and when the thing built is itself a page (an app, a 3D scene), showing it does not replace the report page. Without an inline page tool, deliver the artifact paths plus that summary. The document is the deliverable; the summary is what gets it read.
|
|
353
359
|
|
|
354
360
|
### The closing briefing — every run ends with it
|
|
355
361
|
|
|
@@ -414,4 +420,6 @@ High-yield combinations: official docs (`site:<docs domain>`), GitHub implementa
|
|
|
414
420
|
| Chasing an interesting find with no ENTER trigger | Excursions need a named trigger; everything else stays a queued lead |
|
|
415
421
|
| An excursion that never came back, or drifted into a new mission | EXIT rules are unconditional; depth 3 means promote it to an axis or record it as a gap |
|
|
416
422
|
| An excursion whose result was never folded back | Every EXIT writes what it changed in the top-level answer, `none` included, and mirrors into the loop ledger |
|
|
423
|
+
| Ending with file paths while an inline HTML page tool is available | Show the report inline first (the gated `report.html` or its inline edition), then the paths and the summary |
|
|
424
|
+
| A visual reproduction checked only against itself | Reference compare: real source frames next to renders at the same moments, a critique per pair, fixes, and the pairs in the report |
|
|
417
425
|
| Delivering without the closing briefing | Paste the block `outcome briefing` prints; never compute or recall its numbers by hand |
|