@stapel/eslint-plugin 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/index.js +194 -0
- package/lib/colors.js +73 -0
- package/lib/data.js +349 -0
- package/package.json +54 -0
- package/rules/clickable-needs-event.js +194 -0
- package/rules/demo-literal-meta.js +124 -0
- package/rules/event-literal-meta.js +154 -0
- package/rules/i18n-key-exists.js +77 -0
- package/rules/known-event.js +133 -0
- package/rules/no-direct-analytics-provider.js +102 -0
- package/rules/no-double-count.js +180 -0
- package/rules/no-hardcoded-text.js +99 -0
- package/rules/no-raw-colors.js +197 -0
- package/rules/no-raw-fetch.js +100 -0
- package/rules/no-raw-token-import.js +65 -0
- package/rules/no-string-paths.js +168 -0
- package/rules/query-keys-from-factory.js +143 -0
- package/rules/require-disable-description.js +60 -0
- package/stylelint/index.js +93 -0
- package/stylelint/preset.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stapel contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @stapel/eslint-plugin
|
|
2
|
+
|
|
3
|
+
The enforcement tier of the Stapel frontend guardrails (frontend-guardrails §2):
|
|
4
|
+
static lint rules that close the bypass paths a design system leaves open. Every
|
|
5
|
+
rule is **data-driven** — it reads the same generated manifests the codegen
|
|
6
|
+
writes (`@stapel/tokens/manifest.json`, pair i18n key registries), so lint and
|
|
7
|
+
code never drift. Error messages **teach**: what's wrong, what to do instead, and
|
|
8
|
+
where the catalog lives.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add -D @stapel/eslint-plugin
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Flat config
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// eslint.config.mjs
|
|
20
|
+
import tseslint from "typescript-eslint";
|
|
21
|
+
import stapel from "@stapel/eslint-plugin";
|
|
22
|
+
|
|
23
|
+
export default [
|
|
24
|
+
...tseslint.configs.strict,
|
|
25
|
+
...stapel.configs.recommended, // spread LAST — its file overrides must win
|
|
26
|
+
];
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `recommended` preset:
|
|
30
|
+
|
|
31
|
+
- turns the rules on for `**/*.{ts,tsx,js,jsx,mjs,…}` (JSX-only rules on `*.{tsx,jsx}`);
|
|
32
|
+
- **overrides** `no-raw-token-import` **off** in theme-config / showcase / demo / scripts,
|
|
33
|
+
`no-raw-fetch` and `no-string-paths` **off** in the codegen api layer
|
|
34
|
+
(`**/api/**`, `*client.ts`, `generated/`), and `query-keys-from-factory`
|
|
35
|
+
**off** in the key-factory file (`**/queryKeys.*`);
|
|
36
|
+
- **overrides** the content rules off in tests and fixtures (they exercise the
|
|
37
|
+
anti-patterns on purpose).
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
| Rule | Catches |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `stapel/no-raw-colors` | hex/rgb/hsl/named colours in style objects & CSS templates; Tailwind arbitrary colour values `bg-[#…]`; arbitrary values built by interpolation `bg-[${x}]` (JIT-invisible); bare raw-ramp refs `gray.500` |
|
|
44
|
+
| `stapel/no-raw-token-import` | importing `@stapel/tokens/raw` outside theme-config / showcase |
|
|
45
|
+
| `stapel/no-raw-fetch` | `fetch` / `globalThis.fetch` / `new XMLHttpRequest()` / `axios` / `ky` outside the codegen client |
|
|
46
|
+
| `stapel/no-string-paths` | a hand-written API path — `client.get("/…")` on an http verb, or a bare literal/template that IS a catalogued operation path (`manifest.json §operations`) — outside the codegen api layer. Call the named operation instead. Off in `api/`, `*client.ts`, `generated/` |
|
|
47
|
+
| `stapel/query-keys-from-factory` | an inline `queryKey`/`mutationKey` array literal in `useQuery`/`useMutation`/`queryClient.*` — keys come only from the module's `<module>QueryKeys` factory (drift from invalidations otherwise). Off in the factory file (`**/queryKeys.*`) |
|
|
48
|
+
| `stapel/i18n-key-exists` | `t("…")` keys absent from the generated registry (only within a managed namespace — app-local keys are left alone) |
|
|
49
|
+
| `stapel/no-hardcoded-text` | user-facing JSX text and `alt`/`title`/`placeholder`/`aria-*` string literals |
|
|
50
|
+
| `stapel/require-disable-description` | an `eslint-disable` without a `-- reason` (§2.4 escape-hatch policy) |
|
|
51
|
+
| `stapel/clickable-needs-event` | an interactive JSX element (`onClick`/`onSubmit`/…) with no analytics outcome — needs `tracked()`/`trackedSubmit()`, `data-analytics="flow"`, or `data-analytics="none"` + a reason (§3.2). Decorative `stopPropagation`/`preventDefault`-only handlers are exempt |
|
|
52
|
+
| `stapel/no-double-count` | `tracked()`/`trackedSubmit()` over a handler that also steps a flow machine (`run`/`step`/`submit*`, or `data-analytics="flow"` on the same element) — the funnel already auto-emits (hard ban, Q12а / §3.2) |
|
|
53
|
+
| `stapel/event-literal-meta` | `defineEvent()` with a non-literal argument (dynamic `name`/`description`, non-`prop.*` props) — breaks static extraction into `events.json` (§3.1) |
|
|
54
|
+
| `stapel/known-event` (warn) | `track()`/`tracked()` with an event name absent from the generated `events.json` — registry drift; run `pnpm gen:events` (§3) |
|
|
55
|
+
| `stapel/no-direct-analytics-provider` | importing an analytics vendor SDK (posthog-js, mixpanel, `@amplitude/*`, `@segment/*`, …) outside the core facade's provider adapters (`analytics/providers.*`) — bypasses consent/PII/queue (§3). Extend the vendor list via `options.providers` or `settings.stapel.providerModules` |
|
|
56
|
+
| `stapel/demo-literal-meta` | `defineDemo()` with non-literal meta (dynamic `id`/`title`/`description`/`covers`) — breaks static extraction into `demos.json`/`manifest.demos` (§4.2) |
|
|
57
|
+
|
|
58
|
+
### Settings
|
|
59
|
+
|
|
60
|
+
Rules resolve their catalogs automatically; override for non-standard layouts:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
settings: {
|
|
64
|
+
stapel: {
|
|
65
|
+
tokensManifest: {...}, // or tokensManifestPath
|
|
66
|
+
i18nKeys: ["auth.otp.…"], // or i18nManifests: [manifest, …]
|
|
67
|
+
httpModules: ["my-http"], // extra banned HTTP clients
|
|
68
|
+
rawModules: ["@x/raw"], // extra raw-token entry points
|
|
69
|
+
eventsManifests: [manifest],// or eventNames: ["pricing.plan.selected", …]
|
|
70
|
+
operationsManifests: [manifest], // or operationPaths: ["/auth/api/me/", …]
|
|
71
|
+
httpVerbs: ["get","post"], // client methods no-string-paths inspects
|
|
72
|
+
queryHooks: ["useQuery"], // extra react-query hooks to inspect for keys
|
|
73
|
+
trackedWrappers: ["tracked"],
|
|
74
|
+
clickHandlers: ["onClick"], // extra interactive handler props
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Stylelint preset
|
|
80
|
+
|
|
81
|
+
A self-contained stylelint plugin (no third-party strict-value dependency): colour
|
|
82
|
+
properties may only be `var(--stapel-*)`, and no hex/rgb/hsl literal is allowed in
|
|
83
|
+
any declaration.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
// stylelint.config.js
|
|
87
|
+
import stapelPreset from "@stapel/eslint-plugin/stylelint/preset";
|
|
88
|
+
export default { ...stapelPreset };
|
|
89
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @stapel/eslint-plugin — the enforcement tier of the frontend guardrails
|
|
2
|
+
// (frontend-guardrails §2). Rules are data-driven: they read the same generated
|
|
3
|
+
// manifests (@stapel/tokens/manifest.json, pair i18n key registries) the
|
|
4
|
+
// codegen writes, so lint and code never drift. Ships a flat-config
|
|
5
|
+
// `recommended` preset; the stylelint preset lives at @stapel/eslint-plugin/
|
|
6
|
+
// stylelint.
|
|
7
|
+
import noRawColors from "./rules/no-raw-colors.js";
|
|
8
|
+
import noRawTokenImport from "./rules/no-raw-token-import.js";
|
|
9
|
+
import noRawFetch from "./rules/no-raw-fetch.js";
|
|
10
|
+
import noStringPaths from "./rules/no-string-paths.js";
|
|
11
|
+
import queryKeysFromFactory from "./rules/query-keys-from-factory.js";
|
|
12
|
+
import i18nKeyExists from "./rules/i18n-key-exists.js";
|
|
13
|
+
import noHardcodedText from "./rules/no-hardcoded-text.js";
|
|
14
|
+
import requireDisableDescription from "./rules/require-disable-description.js";
|
|
15
|
+
import clickableNeedsEvent from "./rules/clickable-needs-event.js";
|
|
16
|
+
import noDoubleCount from "./rules/no-double-count.js";
|
|
17
|
+
import eventLiteralMeta from "./rules/event-literal-meta.js";
|
|
18
|
+
import knownEvent from "./rules/known-event.js";
|
|
19
|
+
import noDirectAnalyticsProvider from "./rules/no-direct-analytics-provider.js";
|
|
20
|
+
import demoLiteralMeta from "./rules/demo-literal-meta.js";
|
|
21
|
+
|
|
22
|
+
const rules = {
|
|
23
|
+
"no-raw-colors": noRawColors,
|
|
24
|
+
"no-raw-token-import": noRawTokenImport,
|
|
25
|
+
"no-raw-fetch": noRawFetch,
|
|
26
|
+
// Server-state guardrails (frontend-guardrails §2.2 / §2.6).
|
|
27
|
+
"no-string-paths": noStringPaths,
|
|
28
|
+
"query-keys-from-factory": queryKeysFromFactory,
|
|
29
|
+
"i18n-key-exists": i18nKeyExists,
|
|
30
|
+
"no-hardcoded-text": noHardcodedText,
|
|
31
|
+
"require-disable-description": requireDisableDescription,
|
|
32
|
+
// Typed-analytics guardrails (frontend-guardrails §3, task G4).
|
|
33
|
+
"clickable-needs-event": clickableNeedsEvent,
|
|
34
|
+
"no-double-count": noDoubleCount,
|
|
35
|
+
"event-literal-meta": eventLiteralMeta,
|
|
36
|
+
"known-event": knownEvent,
|
|
37
|
+
"no-direct-analytics-provider": noDirectAnalyticsProvider,
|
|
38
|
+
// Showcase guardrail (frontend-guardrails §4, task G7).
|
|
39
|
+
"demo-literal-meta": demoLiteralMeta,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const plugin = {
|
|
43
|
+
meta: { name: "@stapel/eslint-plugin", version: "0.1.0" },
|
|
44
|
+
rules,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const TS_JS = ["**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"];
|
|
48
|
+
const JSX = ["**/*.{tsx,jsx}"];
|
|
49
|
+
|
|
50
|
+
// Tests and fixtures legitimately exercise the anti-patterns these rules
|
|
51
|
+
// forbid — raw ramps as validator input, deliberately-unknown i18n keys,
|
|
52
|
+
// throwaway JSX copy — so the guardrails are scoped off there. Product source,
|
|
53
|
+
// demos (`demo/`), and the showcase stay covered.
|
|
54
|
+
const TEST_FILES = [
|
|
55
|
+
"**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}",
|
|
56
|
+
"**/test/**",
|
|
57
|
+
"**/tests/**",
|
|
58
|
+
"**/__tests__/**",
|
|
59
|
+
"**/__mocks__/**",
|
|
60
|
+
"**/fixtures/**",
|
|
61
|
+
"**/*.fixture.{ts,tsx,js,jsx}",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Files that are ALLOWED to reach the raw ramps (theme config + showcase +
|
|
65
|
+
// build scripts) — no-raw-token-import is off here (§2.2 overrides).
|
|
66
|
+
const RAW_ALLOWED = [
|
|
67
|
+
"**/theme/**",
|
|
68
|
+
"**/*.theme.{ts,tsx,js,mjs,json}",
|
|
69
|
+
"**/stapel.theme.*",
|
|
70
|
+
"**/tokens/**",
|
|
71
|
+
"**/demo/**",
|
|
72
|
+
"**/demos/**",
|
|
73
|
+
"**/*.demo.{ts,tsx,jsx}",
|
|
74
|
+
"**/showcase/**",
|
|
75
|
+
"**/scripts/**",
|
|
76
|
+
"**/*.stories.{ts,tsx,jsx}",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// The codegen API layer — the one legal home of fetch (§2.2 override).
|
|
80
|
+
const FETCH_ALLOWED = [
|
|
81
|
+
"**/api/**",
|
|
82
|
+
"**/*client.{ts,js}",
|
|
83
|
+
"**/analytics/providers.{ts,js}",
|
|
84
|
+
"**/scripts/**",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
// The codegen API layer — the one legal home of path STRINGS (the operation
|
|
88
|
+
// definitions themselves), mirroring the fetch carve-out (§2.2 override).
|
|
89
|
+
const PATHS_ALLOWED = [
|
|
90
|
+
"**/api/**",
|
|
91
|
+
"**/*client.{ts,js}",
|
|
92
|
+
"**/generated/**",
|
|
93
|
+
"**/scripts/**",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// The query-key factory file — where the literal key arrays legitimately live
|
|
97
|
+
// (§2.2 override; the rest of the pair/app must reach them through the factory).
|
|
98
|
+
const KEY_FACTORY = [
|
|
99
|
+
"**/queryKeys.{ts,tsx,js,mjs}",
|
|
100
|
+
"**/*QueryKeys.{ts,tsx,js,mjs}",
|
|
101
|
+
"**/query-keys.{ts,tsx,js,mjs}",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Flat-config `recommended` preset. Consumers spread it AFTER their parser
|
|
106
|
+
* config:
|
|
107
|
+
*
|
|
108
|
+
* import stapel from "@stapel/eslint-plugin";
|
|
109
|
+
* export default [ ...tseslint.configs.strict, ...stapel.configs.recommended ];
|
|
110
|
+
*/
|
|
111
|
+
const recommended = [
|
|
112
|
+
{ plugins: { stapel: plugin } },
|
|
113
|
+
{
|
|
114
|
+
files: TS_JS,
|
|
115
|
+
rules: {
|
|
116
|
+
"stapel/no-raw-colors": "error",
|
|
117
|
+
"stapel/no-raw-token-import": "error",
|
|
118
|
+
"stapel/no-raw-fetch": "error",
|
|
119
|
+
// Server state: reach endpoints through named operations, keys through the
|
|
120
|
+
// factory (§2.2 / §2.6). Both carved out in their one legal home below.
|
|
121
|
+
"stapel/no-string-paths": "error",
|
|
122
|
+
"stapel/query-keys-from-factory": "error",
|
|
123
|
+
"stapel/i18n-key-exists": "error",
|
|
124
|
+
"stapel/require-disable-description": "error",
|
|
125
|
+
// Typed analytics (§3). Literal-meta keeps events statically extractable;
|
|
126
|
+
// double-count is a hard ban (Q12а); known-event is drift → warn (goes
|
|
127
|
+
// green after `pnpm gen:events`). These fire on .ts (defineEvent, track).
|
|
128
|
+
"stapel/event-literal-meta": "error",
|
|
129
|
+
"stapel/no-double-count": "error",
|
|
130
|
+
"stapel/known-event": "warn",
|
|
131
|
+
// Vendor SDKs only behind the core facade (§2.2 / F9).
|
|
132
|
+
"stapel/no-direct-analytics-provider": "error",
|
|
133
|
+
// Showcase (§4): defineDemo meta must stay literal (extractable).
|
|
134
|
+
"stapel/demo-literal-meta": "error",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
files: JSX,
|
|
139
|
+
rules: {
|
|
140
|
+
"stapel/no-hardcoded-text": "error",
|
|
141
|
+
// Clickable-without-an-outcome is a JSX concern (§3.2).
|
|
142
|
+
"stapel/clickable-needs-event": "error",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
files: RAW_ALLOWED,
|
|
147
|
+
rules: { "stapel/no-raw-token-import": "off" },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
files: FETCH_ALLOWED,
|
|
151
|
+
rules: { "stapel/no-raw-fetch": "off" },
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
files: PATHS_ALLOWED,
|
|
155
|
+
rules: { "stapel/no-string-paths": "off" },
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
files: KEY_FACTORY,
|
|
159
|
+
rules: { "stapel/query-keys-from-factory": "off" },
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
// The facade's provider adapters — the ONE legal home of vendor SDK
|
|
163
|
+
// imports (§2.2 override; mirrors the FETCH_ALLOWED api-layer carve-out).
|
|
164
|
+
files: ["**/analytics/providers.{ts,js}", "**/analytics/providers/**"],
|
|
165
|
+
rules: { "stapel/no-direct-analytics-provider": "off" },
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
files: TEST_FILES,
|
|
169
|
+
rules: {
|
|
170
|
+
"stapel/no-raw-colors": "off",
|
|
171
|
+
"stapel/no-hardcoded-text": "off",
|
|
172
|
+
"stapel/i18n-key-exists": "off",
|
|
173
|
+
"stapel/no-raw-fetch": "off",
|
|
174
|
+
"stapel/no-string-paths": "off",
|
|
175
|
+
"stapel/query-keys-from-factory": "off",
|
|
176
|
+
"stapel/no-raw-token-import": "off",
|
|
177
|
+
// Fixtures / headless test factories legitimately train the analytics
|
|
178
|
+
// anti-patterns (dynamic defineEvent, deliberate double-count, unknown
|
|
179
|
+
// events, un-tracked clickables) — off there, on in product/demos.
|
|
180
|
+
"stapel/clickable-needs-event": "off",
|
|
181
|
+
"stapel/event-literal-meta": "off",
|
|
182
|
+
"stapel/no-double-count": "off",
|
|
183
|
+
"stapel/known-event": "off",
|
|
184
|
+
"stapel/no-direct-analytics-provider": "off",
|
|
185
|
+
"stapel/demo-literal-meta": "off",
|
|
186
|
+
// require-disable-description stays ON — disable hygiene applies everywhere.
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
plugin.configs = { recommended };
|
|
192
|
+
|
|
193
|
+
export default plugin;
|
|
194
|
+
export { rules, recommended };
|
package/lib/colors.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Colour-syntax detection shared by no-raw-colors (ESLint) and the stylelint
|
|
2
|
+
// preset. Kept deliberately narrow so it only fires in colour CONTEXTS
|
|
3
|
+
// (style-object values, className arbitrary values, CSS templates) — never on
|
|
4
|
+
// arbitrary strings — to keep false positives near zero (frontend-guardrails
|
|
5
|
+
// §1.5: the anti-pattern is a hardcoded hex in a styled surface, not the
|
|
6
|
+
// substring "#abc" in an anchor href).
|
|
7
|
+
|
|
8
|
+
// #rgb, #rgba, #rrggbb, #rrggbbaa
|
|
9
|
+
export const HEX_RE = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})(?![0-9a-fA-F])/;
|
|
10
|
+
// rgb()/rgba()/hsl()/hsla() function syntax
|
|
11
|
+
export const COLOR_FUNC_RE = /\b(?:rgba?|hsla?)\s*\(/i;
|
|
12
|
+
|
|
13
|
+
// A curated set of CSS named colours. Only checked when the value is the WHOLE
|
|
14
|
+
// value of a colour property (style objects / CSS decls), so "red" as an
|
|
15
|
+
// identifier or a word inside prose is never touched.
|
|
16
|
+
export const NAMED_COLORS = new Set([
|
|
17
|
+
"black", "white", "red", "green", "blue", "yellow", "orange", "purple",
|
|
18
|
+
"pink", "gray", "grey", "cyan", "magenta", "lime", "teal", "navy", "maroon",
|
|
19
|
+
"olive", "silver", "gold", "indigo", "violet", "coral", "salmon", "crimson",
|
|
20
|
+
"turquoise", "beige", "tan", "brown", "aqua", "fuchsia",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// CSS/React style property keys that carry a colour. Used to gate NAMED colours
|
|
24
|
+
// and to know a style value is colour-ish. Anything ending in "color" counts,
|
|
25
|
+
// plus these composite properties.
|
|
26
|
+
const COLOR_PROP_EXTRA = new Set([
|
|
27
|
+
"background", "border", "outline", "fill", "stroke", "boxShadow",
|
|
28
|
+
"textShadow", "borderTop", "borderRight", "borderBottom", "borderLeft",
|
|
29
|
+
"box-shadow", "text-shadow", "border-top", "border-right", "border-bottom",
|
|
30
|
+
"border-left",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function isColorProperty(key) {
|
|
34
|
+
if (typeof key !== "string") return false;
|
|
35
|
+
const k = key.trim();
|
|
36
|
+
return /color$/i.test(k) || COLOR_PROP_EXTRA.has(k);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Any hex or colour-function token in a string (context already colour-ish). */
|
|
40
|
+
export function hasColorSyntax(value) {
|
|
41
|
+
return HEX_RE.test(value) || COLOR_FUNC_RE.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whole-value named colour (e.g. a style value of exactly "red"). */
|
|
45
|
+
export function isNamedColorValue(value) {
|
|
46
|
+
return NAMED_COLORS.has(String(value).trim().toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Scan a className string for Tailwind arbitrary values that embed a raw
|
|
51
|
+
* colour: `bg-[#4657d9]`, `text-[rgb(...)]`, `border-[gray.500]`. Returns the
|
|
52
|
+
* first offending fragment or null. `rampNames` lets us flag raw-ramp refs
|
|
53
|
+
* (`gray.500`) that slipped into an arbitrary value.
|
|
54
|
+
*/
|
|
55
|
+
export function findArbitraryColor(className, rampNames) {
|
|
56
|
+
const re = /(?:^|[\s])[\w-]*\[([^\]]*)\]/g;
|
|
57
|
+
let m;
|
|
58
|
+
while ((m = re.exec(className)) !== null) {
|
|
59
|
+
const inner = m[1];
|
|
60
|
+
if (hasColorSyntax(inner)) return inner;
|
|
61
|
+
// raw ramp reference inside the arbitrary value, e.g. bg-[gray.500]
|
|
62
|
+
const ramp = /^([a-z]+)\.[\w-]+$/i.exec(inner.trim());
|
|
63
|
+
if (ramp && rampNames && rampNames.has(ramp[1])) return inner;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** True when a string looks like a bare raw-ramp reference: `<ramp>.<step>`. */
|
|
69
|
+
export function matchRampRef(value, rampNames) {
|
|
70
|
+
const m = /^([a-z]+)\.[\w-]+$/i.exec(String(value).trim());
|
|
71
|
+
if (m && rampNames && rampNames.has(m[1])) return m[1];
|
|
72
|
+
return null;
|
|
73
|
+
}
|