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.
Files changed (127) hide show
  1. package/dist/cli/index.js +20 -17
  2. package/dist/cli-node/index.js +20 -17
  3. package/package.json +1 -1
  4. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  5. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  7. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  10. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  12. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  14. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  15. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  17. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  19. package/packages/omo-codex/plugin/components/teammode/package.json +2 -2
  20. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  27. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  28. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  29. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  30. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  31. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  32. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  33. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  34. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  35. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  36. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  37. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  38. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  39. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  40. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  41. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  42. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  43. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  44. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  45. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  49. package/packages/omo-codex/plugin/package-lock.json +18 -17
  50. package/packages/omo-codex/plugin/package.json +1 -1
  51. package/packages/omo-codex/plugin/shared/package.json +1 -1
  52. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +4 -4
  53. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +2 -2
  54. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +8 -1
  55. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +3 -2
  56. package/packages/omo-codex/plugin/skills/frontend/references/perfection/README.md +2 -1
  57. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/color.ts +132 -0
  58. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/css-parse.ts +103 -0
  59. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +156 -0
  60. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/findings.ts +34 -0
  61. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/gradient.ts +70 -0
  62. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/lint.ts +110 -0
  63. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/markup.ts +148 -0
  64. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/selector.ts +46 -0
  65. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +117 -0
  66. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/check-accent-stripes.ts +50 -0
  67. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/exempt-states.css +4 -0
  68. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/flat-stripe-side.tsx +11 -0
  69. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/full-surface-gradients.css +34 -0
  70. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +7 -0
  71. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +6 -0
  72. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/pseudo-overlays.css +3 -0
  73. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +11 -0
  74. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +3 -0
  75. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/wide-blocks-and-hairlines.css +3 -0
  76. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/apply.css +3 -0
  77. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/border-top-card.css +5 -0
  78. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/cva-variant.ts +1 -0
  79. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/gradient-edges.css +26 -0
  80. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inline-styles.tsx +6 -0
  81. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inset-shadow.css +4 -0
  82. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/non-visible-focus-and-nested.tsx +6 -0
  83. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/pseudo-bar.css +13 -0
  84. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/split-rule-and-token.scss +14 -0
  85. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/styled-components.tsx +14 -0
  86. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-cards.tsx +8 -0
  87. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-inset-and-style.vue +11 -0
  88. package/packages/omo-codex/plugin/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-pseudo.html +3 -0
  89. package/packages/omo-codex/scripts/install-dist/install-local.mjs +3 -3
  90. package/packages/shared-skills/skills/browser/runtime/omowright/index.js +4 -4
  91. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +2 -2
  92. package/packages/shared-skills/skills/frontend/SKILL.md +8 -1
  93. package/packages/shared-skills/skills/frontend/references/design/README.md +3 -2
  94. package/packages/shared-skills/skills/frontend/references/perfection/README.md +2 -1
  95. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/color.ts +132 -0
  96. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/css-parse.ts +103 -0
  97. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/declarations.ts +156 -0
  98. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/findings.ts +34 -0
  99. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/gradient.ts +70 -0
  100. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/lint.ts +110 -0
  101. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/markup.ts +148 -0
  102. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/selector.ts +46 -0
  103. package/packages/shared-skills/skills/frontend/scripts/perfection/accent-stripes/tailwind.ts +117 -0
  104. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.test.ts +86 -0
  105. package/packages/shared-skills/skills/frontend/scripts/perfection/check-accent-stripes.ts +50 -0
  106. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/exempt-states.css +4 -0
  107. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/flat-stripe-side.tsx +11 -0
  108. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/full-surface-gradients.css +34 -0
  109. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/inline-ok.tsx +7 -0
  110. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/neutral-and-flat.css +6 -0
  111. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/pseudo-overlays.css +3 -0
  112. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/tailwind-ok.tsx +11 -0
  113. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/uniform-and-rings.css +3 -0
  114. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/negative/wide-blocks-and-hairlines.css +3 -0
  115. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/apply.css +3 -0
  116. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/border-top-card.css +5 -0
  117. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/cva-variant.ts +1 -0
  118. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/gradient-edges.css +26 -0
  119. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inline-styles.tsx +6 -0
  120. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/inset-shadow.css +4 -0
  121. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/non-visible-focus-and-nested.tsx +6 -0
  122. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/pseudo-bar.css +13 -0
  123. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/split-rule-and-token.scss +14 -0
  124. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/styled-components.tsx +14 -0
  125. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-cards.tsx +8 -0
  126. package/packages/shared-skills/skills/frontend/scripts/perfection/fixtures/accent-stripes/positive/tailwind-inset-and-style.vue +11 -0
  127. 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
+ }
@@ -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
+ }
@@ -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,7 @@
1
+ export const Ok = () => (
2
+ <>
3
+ <div style={{ borderRadius: 8, border: "1px solid #e5e7eb" }}>Card</div>
4
+ <div style={{ borderRadius: 8, boxShadow: "inset 0 0 0 1px rgba(0,0,0,.1)" }}>Ring</div>
5
+ <p>Don't stripe {"rounded"} cards.</p>
6
+ </>
7
+ )
@@ -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,3 @@
1
+ .selected { border-radius: 8px; border: 2px solid #6366f1; }
2
+ .chip { border-radius: 999px; box-shadow: inset 0 0 0 2px #6366f1; }
3
+ .lift { border-radius: 12px; box-shadow: 0 4px 12px rgb(0 0 0 / 0.12); }
@@ -0,0 +1,3 @@
1
+ .media-card { position: relative; border-radius: 16px; overflow: hidden; }
2
+ .media-card::after { content: ""; position: absolute; top: 0; bottom: 0; right: 0; width: 120px; background: #6366f1; }
3
+ .subtle { border-radius: 8px; border-left: 1px solid #6366f1; }
@@ -0,0 +1,3 @@
1
+ .tile {
2
+ @apply rounded-xl border-l-4 border-sky-500 bg-white p-6;
3
+ }
@@ -0,0 +1,5 @@
1
+ .feature-card {
2
+ padding: 24px;
3
+ border-radius: 12px;
4
+ border-top: 4px solid #6366f1;
5
+ }
@@ -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
+ export const Tip = () => (
2
+ <>
3
+ <div style={{ borderRadius: 12, borderLeft: "4px solid #ef4444" }}>Careful</div>
4
+ <div className="rounded-lg" style={{ borderTop: '3px solid var(--brand)' }}>New</div>
5
+ </>
6
+ )
@@ -0,0 +1,4 @@
1
+ .note {
2
+ border-radius: 8px;
3
+ box-shadow: inset 4px 0 0 0 oklch(0.65 0.2 250), 0 1px 2px rgb(0 0 0 / 0.06);
4
+ }
@@ -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>
@@ -0,0 +1,13 @@
1
+ .stat {
2
+ position: relative;
3
+ border-radius: 12px;
4
+ overflow: hidden;
5
+ }
6
+
7
+ .stat::before {
8
+ content: "";
9
+ position: absolute;
10
+ inset: 0 auto 0 0;
11
+ width: 4px;
12
+ background: #10b981;
13
+ }