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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type CssRule, type Declaration, parseCss } from "./css-parse"
|
|
2
|
+
import { ALL_CORNERS, type Corner, hasEdgeStripe, hasInsetShadowStripe, hasRadius, isPseudoBar, roundedCorners } from "./declarations"
|
|
3
|
+
import { type Finding, lineLocator, type RuleId } from "./findings"
|
|
4
|
+
import { hasGradientEdgeBand } from "./gradient"
|
|
5
|
+
import { classGroups, cssTemplates, inlineStyles, styleBlocks } from "./markup"
|
|
6
|
+
import { type Compound, compoundCovers, isExemptSelector, parseCompound, selectorParts } from "./selector"
|
|
7
|
+
import { checkClassSet } from "./tailwind"
|
|
8
|
+
|
|
9
|
+
export type SourceFile = { readonly path: string; readonly text: string }
|
|
10
|
+
|
|
11
|
+
const STYLESHEET = /\.(?:css|scss|less|pcss|postcss)$/i
|
|
12
|
+
const SCRIPT = /\.(?:[cm]?[jt]sx?)$/i
|
|
13
|
+
|
|
14
|
+
function cssRulesOf(file: SourceFile): CssRule[] {
|
|
15
|
+
if (STYLESHEET.test(file.path)) return parseCss(file.text, 0, !/\.css$/i.test(file.path))
|
|
16
|
+
const blocks = SCRIPT.test(file.path) ? cssTemplates(file.text) : styleBlocks(file.text)
|
|
17
|
+
return blocks.flatMap((b) => parseCss(b.css, b.offset, true))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ruleHasRadius(rule: CssRule): boolean {
|
|
21
|
+
return hasRadius(rule.declarations) || rule.applyClasses.some((c) => /^rounded(?:-|$)/.test(c) && !/none$/.test(c))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function radiusIndex(rules: readonly CssRule[]): Compound[] {
|
|
25
|
+
return rules.filter(ruleHasRadius).flatMap((r) => selectorParts(r.selector).map(parseCompound))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const NO_CORNERS: ReadonlySet<Corner> = new Set()
|
|
29
|
+
|
|
30
|
+
function hostCorners(part: string, rule: CssRule, index: readonly Compound[]): ReadonlySet<Corner> {
|
|
31
|
+
if (!parseCompound(part).pseudoElement) {
|
|
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
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const EDGE_PROP = /^border(?:-(?:top|right|bottom|left|block|inline|width|color|image)(?:-[\w-]+)?)?$/
|
|
40
|
+
const BACKGROUND_PROP = /^background(?:-color|-image)?$/
|
|
41
|
+
|
|
42
|
+
function declarationOffset(declarations: readonly Declaration[], prop: RegExp, fallback: number): number {
|
|
43
|
+
return declarations.find((d) => prop.test(d.prop) && !d.prop.endsWith("radius"))?.offset ?? fallback
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Hit = { readonly id: RuleId; readonly offset: number }
|
|
47
|
+
|
|
48
|
+
function declarationHits(declarations: readonly Declaration[], corners: ReadonlySet<Corner>, fallback: number): Hit[] {
|
|
49
|
+
const hits: Hit[] = []
|
|
50
|
+
if (hasEdgeStripe(declarations, corners)) hits.push({ id: "edge-stripe", offset: declarationOffset(declarations, EDGE_PROP, fallback) })
|
|
51
|
+
if (hasInsetShadowStripe(declarations)) hits.push({ id: "inset-shadow-stripe", offset: declarationOffset(declarations, /^box-shadow$/, fallback) })
|
|
52
|
+
if (hasGradientEdgeBand(declarations)) hits.push({ id: "gradient-stripe", offset: declarationOffset(declarations, BACKGROUND_PROP, fallback) })
|
|
53
|
+
return hits
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkRule(rule: CssRule, index: readonly Compound[]): Hit[] {
|
|
57
|
+
const found = new Map<RuleId, number>()
|
|
58
|
+
const add = (hit: Hit) => found.set(hit.id, found.get(hit.id) ?? hit.offset)
|
|
59
|
+
for (const part of selectorParts(rule.selector)) {
|
|
60
|
+
const corners = isExemptSelector(part) ? NO_CORNERS : hostCorners(part, rule, index)
|
|
61
|
+
if (corners.size === 0) continue
|
|
62
|
+
if (parseCompound(part).pseudoElement) {
|
|
63
|
+
if (isPseudoBar(rule.declarations)) add({ id: "pseudo-bar", offset: declarationOffset(rule.declarations, BACKGROUND_PROP, rule.offset) })
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
declarationHits(rule.declarations, corners, rule.offset).forEach(add)
|
|
67
|
+
const ownRounding = rule.applyClasses.some((c) => c.startsWith("rounded"))
|
|
68
|
+
for (const id of checkClassSet(ownRounding ? rule.applyClasses : ["rounded", ...rule.applyClasses])) {
|
|
69
|
+
add({ id, offset: rule.applyOffset ?? rule.offset })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...found].map(([id, offset]) => ({ id, offset }))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function excerptAt(text: string, offset: number): string {
|
|
76
|
+
const lineStart = text.lastIndexOf("\n", offset - 1) + 1
|
|
77
|
+
const lineEnd = text.indexOf("\n", offset)
|
|
78
|
+
return text.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim().slice(0, 140)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function lintFiles(files: readonly SourceFile[]): Finding[] {
|
|
82
|
+
const rulesByFile = new Map(files.map((f) => [f.path, cssRulesOf(f)]))
|
|
83
|
+
const index = radiusIndex([...rulesByFile.values()].flat())
|
|
84
|
+
const findings: Finding[] = []
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
const locate = lineLocator(file.text)
|
|
87
|
+
const seen = new Set<string>()
|
|
88
|
+
const report = (offset: number, rule: RuleId) => {
|
|
89
|
+
const at = locate(offset)
|
|
90
|
+
const key = `${at.line}:${rule}`
|
|
91
|
+
if (seen.has(key)) return
|
|
92
|
+
seen.add(key)
|
|
93
|
+
findings.push({ file: file.path, ...at, rule, excerpt: excerptAt(file.text, offset) })
|
|
94
|
+
}
|
|
95
|
+
for (const rule of rulesByFile.get(file.path) ?? []) {
|
|
96
|
+
for (const hit of checkRule(rule, index)) report(hit.offset, hit.id)
|
|
97
|
+
}
|
|
98
|
+
if (STYLESHEET.test(file.path)) continue
|
|
99
|
+
for (const group of classGroups(file.text, SCRIPT.test(file.path))) {
|
|
100
|
+
for (const id of checkClassSet(group.tokens)) report(group.offset, id)
|
|
101
|
+
}
|
|
102
|
+
for (const style of inlineStyles(file.text)) {
|
|
103
|
+
const own = roundedCorners(style.declarations)
|
|
104
|
+
const corners = own.size > 0 ? own : style.tagRounded ? ALL_CORNERS : NO_CORNERS
|
|
105
|
+
if (corners.size === 0) continue
|
|
106
|
+
for (const hit of declarationHits(style.declarations, corners, style.offset)) report(style.offset, hit.id)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column)
|
|
110
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Declaration } from "./css-parse"
|
|
2
|
+
import { looksLikeClassList } from "./tailwind"
|
|
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 }
|
|
6
|
+
export type StyleBlock = { readonly css: string; readonly offset: number }
|
|
7
|
+
|
|
8
|
+
const UNITLESS = new Set(["opacity", "z-index", "flex", "flex-grow", "flex-shrink", "font-weight", "line-height", "order"])
|
|
9
|
+
const ROUNDED_CLASS = /(?:^|[\s"'`:])rounded(?:-(?!none\b)[\w[\]./-]+)?(?=[\s"'`]|$)/
|
|
10
|
+
|
|
11
|
+
export function styleBlocks(text: string): StyleBlock[] {
|
|
12
|
+
return [...text.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => ({
|
|
13
|
+
css: m[1] ?? "",
|
|
14
|
+
offset: (m.index ?? 0) + m[0].indexOf(">") + 1,
|
|
15
|
+
}))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CSS_TEMPLATE_TAG = /(?:\bstyled(?:\.\w+|\([^()]*\))(?:\.attrs\([^)]*\))?|\bcss|\bcreateGlobalStyle|\binjectGlobal|<style[^>]*>\s*\{)\s*$/
|
|
19
|
+
const CSS_TEMPLATE_HOST = ".css-in-js-host"
|
|
20
|
+
|
|
21
|
+
function blankInterpolations(body: string): string {
|
|
22
|
+
let out = ""
|
|
23
|
+
let depth = 0
|
|
24
|
+
for (let i = 0; i < body.length; i += 1) {
|
|
25
|
+
const ch = body.charAt(i)
|
|
26
|
+
if (depth === 0 && ch === "$" && body.charAt(i + 1) === "{") {
|
|
27
|
+
depth = 1
|
|
28
|
+
out += " "
|
|
29
|
+
i += 1
|
|
30
|
+
} else if (depth > 0) {
|
|
31
|
+
if (ch === "{") depth += 1
|
|
32
|
+
else if (ch === "}") depth -= 1
|
|
33
|
+
out += ch === "\n" ? "\n" : " "
|
|
34
|
+
} else out += ch
|
|
35
|
+
}
|
|
36
|
+
return out
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function cssTemplates(text: string): StyleBlock[] {
|
|
40
|
+
const blocks: StyleBlock[] = []
|
|
41
|
+
for (const m of text.matchAll(/`((?:\\.|[^`\\])*)`/g)) {
|
|
42
|
+
const start = m.index ?? 0
|
|
43
|
+
if (!CSS_TEMPLATE_TAG.test(text.slice(Math.max(0, start - 120), start))) continue
|
|
44
|
+
const prefix = `${CSS_TEMPLATE_HOST}{`
|
|
45
|
+
blocks.push({ css: `${prefix}${blankInterpolations(m[1] ?? "")}}`, offset: start + 1 - prefix.length })
|
|
46
|
+
}
|
|
47
|
+
return blocks
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stripInterpolation(literal: string): string {
|
|
51
|
+
return literal.replace(/\$\{[^}]*\}/g, " ")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function tokensOf(text: string): string[] {
|
|
55
|
+
return text.split(/\s+/).filter(Boolean)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readBalanced(text: string, start: number): { end: number; literals: string[] } {
|
|
59
|
+
const literals: string[] = []
|
|
60
|
+
let depth = 0
|
|
61
|
+
for (let i = start; i < text.length; i += 1) {
|
|
62
|
+
const ch = text.charAt(i)
|
|
63
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
64
|
+
const close = text.indexOf(ch, i + 1)
|
|
65
|
+
if (close < 0) return { end: text.length, literals }
|
|
66
|
+
literals.push(stripInterpolation(text.slice(i + 1, close)))
|
|
67
|
+
i = close
|
|
68
|
+
} else if (ch === "{") depth += 1
|
|
69
|
+
else if (ch === "}") {
|
|
70
|
+
depth -= 1
|
|
71
|
+
if (depth === 0) return { end: i + 1, literals }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { end: text.length, literals }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function classGroups(text: string, scanLiterals: boolean): ClassGroup[] {
|
|
78
|
+
const groups: ClassGroup[] = []
|
|
79
|
+
const covered: Array<[number, number]> = []
|
|
80
|
+
for (const m of text.matchAll(/(?:^|[\s{(,])(?::class|class:list|className|class|tw)\s*=\s*/g)) {
|
|
81
|
+
const start = (m.index ?? 0) + m[0].length
|
|
82
|
+
const ch = text.charAt(start)
|
|
83
|
+
if (ch === '"' || ch === "'") {
|
|
84
|
+
const close = text.indexOf(ch, start + 1)
|
|
85
|
+
if (close < 0) continue
|
|
86
|
+
groups.push({ tokens: tokensOf(text.slice(start + 1, close)), offset: start })
|
|
87
|
+
covered.push([start, close])
|
|
88
|
+
} else if (ch === "{") {
|
|
89
|
+
const { end, literals } = readBalanced(text, start)
|
|
90
|
+
groups.push({ tokens: literals.flatMap(tokensOf), offset: start })
|
|
91
|
+
covered.push([start, end])
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!scanLiterals) return groups
|
|
95
|
+
for (const m of text.matchAll(/(["'`])((?:\\.|(?!\1)[^\\\n])*)\1/g)) {
|
|
96
|
+
const offset = m.index ?? 0
|
|
97
|
+
if (covered.some(([a, b]) => offset >= a && offset <= b)) continue
|
|
98
|
+
const body = stripInterpolation(m[2] ?? "")
|
|
99
|
+
if (looksLikeClassList(body)) groups.push({ tokens: tokensOf(body), offset })
|
|
100
|
+
}
|
|
101
|
+
return groups
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function kebab(key: string): string {
|
|
105
|
+
return key.replace(/^["']|["']$/g, "").replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function objectDeclarations(source: string): Declaration[] {
|
|
109
|
+
const pattern = /([A-Za-z]+|"[\w-]+"|'[\w-]+')\s*:\s*("[^"]*"|'[^']*'|`[^`$]*`|-?\d+(?:\.\d+)?)/g
|
|
110
|
+
return [...source.matchAll(pattern)].map((m) => {
|
|
111
|
+
const prop = kebab(m[1] ?? "")
|
|
112
|
+
const raw = m[2] ?? ""
|
|
113
|
+
const numeric = /^-?\d/.test(raw)
|
|
114
|
+
const value = numeric ? (UNITLESS.has(prop) ? raw : `${raw}px`) : raw.slice(1, -1)
|
|
115
|
+
return { prop, value }
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cssDeclarations(source: string): Declaration[] {
|
|
120
|
+
return source
|
|
121
|
+
.split(";")
|
|
122
|
+
.map((d) => d.split(/:(.*)/s))
|
|
123
|
+
.filter((p): p is [string, string, ...string[]] => p.length >= 2 && Boolean(p[0]?.trim()))
|
|
124
|
+
.map(([prop, value]) => ({ prop: prop.trim().toLowerCase(), value: value.trim() }))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function tagIsRounded(text: string, offset: number, end: number): boolean {
|
|
128
|
+
const open = text.lastIndexOf("<", offset)
|
|
129
|
+
const close = text.indexOf(">", end)
|
|
130
|
+
if (open < 0) return false
|
|
131
|
+
return ROUNDED_CLASS.test(text.slice(open, close < 0 ? end : close))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function inlineStyles(text: string): InlineStyle[] {
|
|
135
|
+
const styles: InlineStyle[] = []
|
|
136
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*\{\{([\s\S]*?)\}\}/g)) {
|
|
137
|
+
const offset = m.index ?? 0
|
|
138
|
+
const end = offset + m[0].length
|
|
139
|
+
styles.push({ declarations: objectDeclarations(m[1] ?? ""), offset, tagRounded: tagIsRounded(text, offset, end) })
|
|
140
|
+
}
|
|
141
|
+
for (const m of text.matchAll(/\bstyle\s*=\s*(["'])([\s\S]*?)\1/g)) {
|
|
142
|
+
const offset = m.index ?? 0
|
|
143
|
+
const body = (m[2] ?? "").trim()
|
|
144
|
+
const declarations = body.startsWith("{") ? objectDeclarations(body) : cssDeclarations(body)
|
|
145
|
+
styles.push({ declarations, offset, tagRounded: tagIsRounded(text, offset, offset + m[0].length) })
|
|
146
|
+
}
|
|
147
|
+
return styles
|
|
148
|
+
}
|
package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/selector.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { splitTopLevel } from "./css-parse"
|
|
2
|
+
|
|
3
|
+
export type Compound = {
|
|
4
|
+
readonly tag: string
|
|
5
|
+
readonly classes: readonly string[]
|
|
6
|
+
readonly ids: readonly string[]
|
|
7
|
+
readonly pseudoElement: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const EXEMPT_STATE = /:(focus-visible|invalid|user-invalid)\b|\[aria-invalid|\.(is-invalid|has-error|was-validated)\b/i
|
|
11
|
+
const EXEMPT_TAGS = new Set(["table", "thead", "tbody", "tfoot", "tr", "th", "td", "hr", "input", "select", "textarea"])
|
|
12
|
+
|
|
13
|
+
function lastCompound(part: string): string {
|
|
14
|
+
const pieces = part.replace(/[>+~]/g, " ").split(/\s+(?![^[(]*[\])])/).filter(Boolean)
|
|
15
|
+
return pieces[pieces.length - 1] ?? ""
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseCompound(part: string): Compound {
|
|
19
|
+
const raw = lastCompound(part)
|
|
20
|
+
const pseudoElement = /::?(before|after)\b/i.test(raw)
|
|
21
|
+
const bare = raw.replace(/::?[\w-]+(\([^)]*\))?/g, "").replace(/\[[^\]]*\]/g, "")
|
|
22
|
+
return {
|
|
23
|
+
tag: (/^[a-z][\w-]*/i.exec(bare)?.[0] ?? "").toLowerCase(),
|
|
24
|
+
classes: [...bare.matchAll(/\.([\w-]+)/g)].map((m) => m[1] ?? ""),
|
|
25
|
+
ids: [...bare.matchAll(/#([\w-]+)/g)].map((m) => m[1] ?? ""),
|
|
26
|
+
pseudoElement,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isExemptSelector(part: string): boolean {
|
|
31
|
+
if (EXEMPT_STATE.test(part)) return true
|
|
32
|
+
return EXEMPT_TAGS.has(parseCompound(part).tag)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function selectorParts(selector: string): string[] {
|
|
36
|
+
return splitTopLevel(selector, ",")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function compoundCovers(general: Compound, specific: Compound): boolean {
|
|
40
|
+
if (general.pseudoElement) return false
|
|
41
|
+
if (general.classes.length + general.ids.length === 0 && !general.tag) return false
|
|
42
|
+
if (general.tag && general.tag !== specific.tag) return false
|
|
43
|
+
return (
|
|
44
|
+
general.classes.every((c) => specific.classes.includes(c)) && general.ids.every((id) => specific.ids.includes(id))
|
|
45
|
+
)
|
|
46
|
+
}
|
package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { classifyUtilityColor } from "./color"
|
|
2
|
+
import { type Corner, isInsetShadowStripe, MAX_BAR_PX, MIN_STRIPE_PX, parseLength } from "./declarations"
|
|
3
|
+
import type { RuleId } from "./findings"
|
|
4
|
+
|
|
5
|
+
const EXEMPT_VARIANT = /^(?:[\w-]+-)?(focus-visible|invalid|user-invalid|aria-invalid|aria-\[invalid[^\]]*\])$/
|
|
6
|
+
const BORDER_STYLE = new Set(["solid", "dashed", "dotted", "double", "hidden", "none"])
|
|
7
|
+
const SPACING_PX: Readonly<Record<string, number>> = { px: 1, "0.5": 2, "1": 4, "1.5": 6, "2": 8 }
|
|
8
|
+
|
|
9
|
+
type Utility = { readonly util: string; readonly layer: "base" | "before" | "after" }
|
|
10
|
+
|
|
11
|
+
function splitVariants(token: string): string[] {
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
let depth = 0
|
|
14
|
+
let current = ""
|
|
15
|
+
for (const ch of token) {
|
|
16
|
+
if (ch === "[") depth += 1
|
|
17
|
+
if (ch === "]") depth -= 1
|
|
18
|
+
if (ch === ":" && depth === 0) {
|
|
19
|
+
parts.push(current)
|
|
20
|
+
current = ""
|
|
21
|
+
} else current += ch
|
|
22
|
+
}
|
|
23
|
+
parts.push(current)
|
|
24
|
+
return parts
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toUtility(token: string): Utility | undefined {
|
|
28
|
+
const parts = splitVariants(token.trim())
|
|
29
|
+
const util = (parts.pop() ?? "").replace(/^!|!$/g, "").replace(/^-/, "")
|
|
30
|
+
if (!util || parts.some((v) => EXEMPT_VARIANT.test(v))) return undefined
|
|
31
|
+
const layer = parts.includes("before") ? "before" : parts.includes("after") ? "after" : "base"
|
|
32
|
+
return { util, layer }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function arbitraryPx(value: string): number | undefined {
|
|
36
|
+
return value.startsWith("[") && value.endsWith("]") ? parseLength(value.slice(1, -1)) : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SIDE_TOUCHES: Readonly<Record<string, readonly Corner[]>> = {
|
|
40
|
+
t: ["tl", "tr"], r: ["tr", "br"], b: ["br", "bl"], l: ["tl", "bl"], s: ["tl", "bl"], e: ["tr", "br"],
|
|
41
|
+
x: ["tl", "tr", "br", "bl"], y: ["tl", "tr", "br", "bl"], bs: ["tl", "tr"], be: ["br", "bl"],
|
|
42
|
+
tl: ["tl"], tr: ["tr"], br: ["br"], bl: ["bl"], ss: ["tl"], se: ["tr"], ee: ["br"], es: ["bl"],
|
|
43
|
+
}
|
|
44
|
+
const ALL: readonly Corner[] = ["tl", "tr", "br", "bl"]
|
|
45
|
+
|
|
46
|
+
function wideSideCorners(util: string): readonly Corner[] {
|
|
47
|
+
const m = /^border-([tlrbsexy]|bs|be)-(\d+|\[[^\]]+\])$/.exec(util)
|
|
48
|
+
if (!m?.[1] || !m[2]) return []
|
|
49
|
+
const width = /^\d+$/.test(m[2]) ? Number.parseInt(m[2], 10) : arbitraryPx(m[2])
|
|
50
|
+
return (width ?? 0) >= MIN_STRIPE_PX ? (SIDE_TOUCHES[m[1]] ?? []) : []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function roundedCorners(utils: readonly string[]): Set<Corner> {
|
|
54
|
+
const corners = new Set<Corner>()
|
|
55
|
+
for (const util of utils) {
|
|
56
|
+
const m = /^rounded(?:-(tl|tr|br|bl|ss|se|ee|es|[trblse]))?(?:-(.+))?$/.exec(util)
|
|
57
|
+
if (!m) continue
|
|
58
|
+
const touched = m[1] ? (SIDE_TOUCHES[m[1]] ?? []) : ALL
|
|
59
|
+
const off = m[2] === "none" || m[2] === "0" || m[2] === "[0]" || m[2] === "[0px]"
|
|
60
|
+
for (const c of touched) {
|
|
61
|
+
if (off) corners.delete(c)
|
|
62
|
+
else corners.add(c)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return corners
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function borderColorIsChromatic(util: string): boolean {
|
|
69
|
+
const m = /^border(?:-(?:[tlrbsexy]|bs|be))?-(.+)$/.exec(util)
|
|
70
|
+
const fragment = m?.[1]
|
|
71
|
+
if (!fragment || BORDER_STYLE.has(fragment) || /^\d+$/.test(fragment)) return false
|
|
72
|
+
if (fragment.startsWith("[") && arbitraryPx(fragment) !== undefined) return false
|
|
73
|
+
return classifyUtilityColor(fragment) === "chromatic"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function thinBar(utils: readonly string[], axis: "w" | "h"): boolean {
|
|
77
|
+
return utils.some((u) => {
|
|
78
|
+
const m = new RegExp(`^${axis}-(px|0\\.5|1|1\\.5|2|\\[[^\\]]+\\])$`).exec(u)
|
|
79
|
+
if (!m?.[1]) return false
|
|
80
|
+
const px = SPACING_PX[m[1]] ?? arbitraryPx(m[1])
|
|
81
|
+
return px !== undefined && px > 0 && px <= MAX_BAR_PX
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function longBar(utils: readonly string[], axis: "w" | "h"): boolean {
|
|
86
|
+
const full = axis === "h" ? ["h-full", "inset-y-0", "inset-0"] : ["w-full", "inset-x-0", "inset-0"]
|
|
87
|
+
const pair = axis === "h" ? ["top-0", "bottom-0"] : ["left-0", "right-0"]
|
|
88
|
+
return utils.some((u) => full.includes(u)) || pair.every((p) => utils.includes(p))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isPseudoBar(utils: readonly string[]): boolean {
|
|
92
|
+
const chromaticBg = utils.some((u) => u.startsWith("bg-") && classifyUtilityColor(u.slice(3)) === "chromatic")
|
|
93
|
+
return chromaticBg && ((thinBar(utils, "w") && longBar(utils, "h")) || (thinBar(utils, "h") && longBar(utils, "w")))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function checkClassSet(tokens: readonly string[]): RuleId[] {
|
|
97
|
+
const utilities = tokens.map(toUtility).filter((u): u is Utility => u !== undefined)
|
|
98
|
+
const base = utilities.filter((u) => u.layer === "base").map((u) => u.util)
|
|
99
|
+
const corners = roundedCorners(base)
|
|
100
|
+
if (corners.size === 0) return []
|
|
101
|
+
const found: RuleId[] = []
|
|
102
|
+
const bent = base.some((u) => wideSideCorners(u).some((c) => corners.has(c)))
|
|
103
|
+
if (bent && base.some(borderColorIsChromatic)) found.push("edge-stripe")
|
|
104
|
+
const shadows = base.filter((u) => u.startsWith("shadow-[") && u.endsWith("]"))
|
|
105
|
+
if (shadows.some((u) => isInsetShadowStripe(u.slice("shadow-[".length, -1).replace(/_/g, " ")))) {
|
|
106
|
+
found.push("inset-shadow-stripe")
|
|
107
|
+
}
|
|
108
|
+
for (const layer of ["before", "after"] as const) {
|
|
109
|
+
const utils = utilities.filter((u) => u.layer === layer).map((u) => u.util)
|
|
110
|
+
if (isPseudoBar(utils) && !found.includes("pseudo-bar")) found.push("pseudo-bar")
|
|
111
|
+
}
|
|
112
|
+
return found
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function looksLikeClassList(text: string): boolean {
|
|
116
|
+
return /(?:^|\s)(?:[\w-]+:)*rounded/.test(text)
|
|
117
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Flags the "coloured accent edge on a rounded card" slop tell in CSS, Tailwind and JSX.
|
|
3
|
+
// Usage: bun check-accent-stripes.ts [--json] <file-or-dir>... Exit 0 clean, 1 findings, 2 usage.
|
|
4
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
import { FIX_HINT, RULE_MESSAGES } from "./accent-stripes/findings"
|
|
7
|
+
import { lintFiles, type SourceFile } from "./accent-stripes/lint"
|
|
8
|
+
|
|
9
|
+
const SCANNED = /\.(?:css|scss|less|pcss|postcss|html?|[cm]?[jt]sx?|vue|svelte|astro)$/i
|
|
10
|
+
const SKIPPED_DIRS = new Set([
|
|
11
|
+
"node_modules", ".git", "dist", "build", "out", ".next", ".nuxt", ".svelte-kit", ".astro",
|
|
12
|
+
".turbo", ".cache", "coverage", "vendor", ".vercel", ".output",
|
|
13
|
+
])
|
|
14
|
+
const MAX_BYTES = 1_000_000
|
|
15
|
+
|
|
16
|
+
function collect(path: string, into: string[]): void {
|
|
17
|
+
const stat = statSync(path)
|
|
18
|
+
if (stat.isDirectory()) {
|
|
19
|
+
for (const entry of readdirSync(path)) {
|
|
20
|
+
if (!SKIPPED_DIRS.has(entry)) collect(join(path, entry), into)
|
|
21
|
+
}
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
const minified = /\.min\.[a-z]+$/i.test(path) || path.endsWith(".d.ts")
|
|
25
|
+
if (SCANNED.test(path) && !minified && stat.size <= MAX_BYTES) into.push(path)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function main(argv: readonly string[]): number {
|
|
29
|
+
const json = argv.includes("--json")
|
|
30
|
+
const targets = argv.filter((a) => a !== "--json")
|
|
31
|
+
if (targets.length === 0) {
|
|
32
|
+
process.stderr.write("usage: check-accent-stripes.ts [--json] <file-or-dir>...\n")
|
|
33
|
+
return 2
|
|
34
|
+
}
|
|
35
|
+
const paths: string[] = []
|
|
36
|
+
for (const target of targets) collect(target, paths)
|
|
37
|
+
const files: SourceFile[] = paths.map((path) => ({ path, text: readFileSync(path, "utf8") }))
|
|
38
|
+
const findings = lintFiles(files)
|
|
39
|
+
if (json) process.stdout.write(`${JSON.stringify(findings, null, 2)}\n`)
|
|
40
|
+
else {
|
|
41
|
+
for (const f of findings) {
|
|
42
|
+
process.stdout.write(`${f.file}:${f.line}:${f.column} ${f.rule} ${RULE_MESSAGES[f.rule]}\n > ${f.excerpt}\n`)
|
|
43
|
+
}
|
|
44
|
+
const summary = findings.length === 0 ? "no accent stripes" : `${findings.length} accent stripe(s); ${FIX_HINT}`
|
|
45
|
+
process.stdout.write(`check-accent-stripes: ${paths.length} file(s), ${summary}\n`)
|
|
46
|
+
}
|
|
47
|
+
return findings.length === 0 ? 0 : 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.exitCode = main(process.argv.slice(2))
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
.card:focus-visible { border-radius: 12px; border-left: 3px solid var(--accent); }
|
|
2
|
+
input:invalid { border-radius: 6px; border-bottom: 2px solid #dc2626; }
|
|
3
|
+
.field[aria-invalid="true"] { border-radius: 6px; border-bottom: 2px solid #dc2626; }
|
|
4
|
+
table td.sorted { border-radius: 4px; border-left: 3px solid #3b82f6; }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const css = `
|
|
2
|
+
.alert { border-left: 4px solid #dc2626; border-radius: 0 6px 6px 0; }
|
|
3
|
+
`
|
|
4
|
+
|
|
5
|
+
export const Quotes = () => (
|
|
6
|
+
<>
|
|
7
|
+
<blockquote className="rounded-r-lg border-l-4 border-blue-500 pl-4">Square on the stripe side</blockquote>
|
|
8
|
+
<aside className="rounded-xl rounded-l-none border-l-4 border-sky-500">Left corners squared</aside>
|
|
9
|
+
<style>{`.note { border-left: 4px solid #16a34a; border-top-right-radius: 8px; border-bottom-right-radius: 8px; }`}</style>
|
|
10
|
+
</>
|
|
11
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.hero {
|
|
2
|
+
border-radius: 16px;
|
|
3
|
+
background: linear-gradient(135deg, #6366f1, #a855f7);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.fade {
|
|
7
|
+
border-radius: 12px;
|
|
8
|
+
background-image: linear-gradient(to bottom, rgba(99, 102, 241, 0.12), transparent 60%);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.uniform-image {
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
border: 2px solid;
|
|
14
|
+
border-image: linear-gradient(#6366f1, #a855f7) 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.soft-edge {
|
|
18
|
+
border-radius: 12px;
|
|
19
|
+
background: linear-gradient(to right, #6366f1 4px, #fff);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.two-tone {
|
|
23
|
+
border-radius: 12px;
|
|
24
|
+
background: linear-gradient(to right, #6366f1 40px, #fff 40px);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.grid-texture {
|
|
28
|
+
border-radius: 12px;
|
|
29
|
+
background:
|
|
30
|
+
linear-gradient(rgba(3, 158, 179, 0.4) 1px, transparent 1px),
|
|
31
|
+
linear-gradient(90deg, rgba(3, 158, 179, 0.4) 1px, transparent 1px),
|
|
32
|
+
#0b1220;
|
|
33
|
+
background-size: 28px 28px, 28px 28px, auto;
|
|
34
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
.list-item { border-bottom: 1px solid #e5e7eb; }
|
|
2
|
+
.panel { border-radius: 12px; border-top: 1px solid rgba(0, 0, 0, 0.08); }
|
|
3
|
+
.quiet { border-radius: 12px; border-left: 4px solid #e5e7eb; }
|
|
4
|
+
.section { border-top: 4px solid #6366f1; }
|
|
5
|
+
.token { border-radius: 8px; border-left: 2px solid var(--color-border); }
|
|
6
|
+
.ink { border-radius: 8px; border-left: 2px solid var(--text-primary); }
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
.card { position: relative; border-radius: 16px; }
|
|
2
|
+
.card::before { content: ""; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(#6366f1, #a855f7); opacity: 0.1; }
|
|
3
|
+
.rule::after { content: ""; display: block; height: 1px; width: 100%; background: #e5e7eb; }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const Ok = () => (
|
|
2
|
+
<div>
|
|
3
|
+
<div className="rounded-lg border border-zinc-200 p-4">Plain</div>
|
|
4
|
+
<div className="rounded-xl border-l-4 border-l-zinc-300 p-4">Neutral edge</div>
|
|
5
|
+
<button className="rounded-md focus-visible:ring-2 focus-visible:ring-blue-500">Go</button>
|
|
6
|
+
<input className="rounded-md border-b-2 aria-invalid:border-b-red-500 invalid:border-red-500" />
|
|
7
|
+
<div className="border-l-4 border-blue-500 pl-4">Flat quote, no corners</div>
|
|
8
|
+
<div className="rounded-none border-l-4 border-blue-500">Square</div>
|
|
9
|
+
<div className="rounded-lg bg-blue-50 p-4 text-blue-900">Tonal surface</div>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const card = cva("rounded-xl bg-white p-6 border-l-4 border-rose-500")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.hard-stop {
|
|
2
|
+
border-radius: 12px;
|
|
3
|
+
background: linear-gradient(to right, #6366f1 4px, #fff 4px);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.image-border {
|
|
7
|
+
border-radius: 12px;
|
|
8
|
+
border-left: 4px solid;
|
|
9
|
+
border-image: linear-gradient(#6366f1, #a855f7) 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.glow { position: relative; border-radius: 12px; }
|
|
13
|
+
.glow::before {
|
|
14
|
+
content: "";
|
|
15
|
+
position: absolute;
|
|
16
|
+
top: 0;
|
|
17
|
+
bottom: 0;
|
|
18
|
+
left: 0;
|
|
19
|
+
width: 3px;
|
|
20
|
+
background: linear-gradient(to bottom, #6366f1, #a855f7);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.end-band {
|
|
24
|
+
border-radius: 12px;
|
|
25
|
+
background-image: linear-gradient(to right, #fff calc(100% - 4px), #f43f5e calc(100% - 4px));
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
const css = css`
|
|
2
|
+
.card:focus-within { border-radius: 12px; border-left: 3px solid #6366f1; }
|
|
3
|
+
td .promo-card { border-radius: 8px; border-left: 4px solid #f43f5e; }
|
|
4
|
+
`
|
|
5
|
+
|
|
6
|
+
export const Focused = () => <div className="rounded-xl focus:border-l-4 focus:border-indigo-500">Card</div>
|