@estiva-app/ui 0.20.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -2
- package/dist/gates/app-checks.d.ts +40 -0
- package/dist/gates/app-checks.d.ts.map +1 -0
- package/dist/gates/chunk-AUXD4GCY.js +478 -0
- package/dist/gates/chunk-AUXD4GCY.js.map +7 -0
- package/dist/gates/chunk-ZGJ2J5NU.js +754 -0
- package/dist/gates/chunk-ZGJ2J5NU.js.map +7 -0
- package/dist/gates/cli.d.ts +2 -0
- package/dist/gates/cli.d.ts.map +1 -0
- package/dist/gates/cli.js +46 -0
- package/dist/gates/cli.js.map +7 -0
- package/dist/gates/count.d.ts +20 -0
- package/dist/gates/count.d.ts.map +1 -0
- package/dist/gates/create-app-cli.d.ts +2 -0
- package/dist/gates/create-app-cli.d.ts.map +1 -0
- package/dist/gates/create-app.d.ts +23 -0
- package/dist/gates/create-app.d.ts.map +1 -0
- package/dist/gates/create-app.js +30 -0
- package/dist/gates/create-app.js.map +7 -0
- package/dist/gates/gate-config.d.ts +44 -0
- package/dist/gates/gate-config.d.ts.map +1 -0
- package/dist/gates/hook.d.ts +18 -0
- package/dist/gates/hook.d.ts.map +1 -0
- package/dist/gates/index.d.ts +30 -0
- package/dist/gates/index.d.ts.map +1 -0
- package/dist/gates/index.js +417 -0
- package/dist/gates/index.js.map +7 -0
- package/dist/gates/status.d.ts +88 -0
- package/dist/gates/status.d.ts.map +1 -0
- package/dist/gates/token-lint.d.ts +26 -0
- package/dist/gates/token-lint.d.ts.map +1 -0
- package/package.json +30 -4
- package/src/gates/app-checks.ts +226 -0
- package/src/gates/cli.ts +61 -0
- package/src/gates/count.ts +76 -0
- package/src/gates/create-app-cli.ts +25 -0
- package/src/gates/create-app.test.ts +72 -0
- package/src/gates/create-app.ts +797 -0
- package/src/gates/gate-config.ts +78 -0
- package/src/gates/gates.test.ts +194 -0
- package/src/gates/hook.ts +111 -0
- package/src/gates/index.ts +30 -0
- package/src/gates/status.ts +545 -0
- package/src/gates/token-lint.ts +231 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/**
|
|
3
|
+
* The token contract as lint settings, written once (docs/GATES.md §23).
|
|
4
|
+
*
|
|
5
|
+
* Until UIG-10 this block was pasted into every repo: `eslint.config.js` here,
|
|
6
|
+
* `eslint.tokens.js` in Peek, `eslint.config.js` in Ship. Katerina ruled on 17 September that a
|
|
7
|
+
* gate piece ships in the package and an app imports it, so this is that one
|
|
8
|
+
* copy. Peek and Ship move onto it in UIG-32.
|
|
9
|
+
*
|
|
10
|
+
* Two configs, as the copies had:
|
|
11
|
+
*
|
|
12
|
+
* - `tokenLint` — a class Tailwind does not generate (the doubled token name,
|
|
13
|
+
* `bg-bg-surface`), Tailwind's own type ramp or palette, a raw colour, an
|
|
14
|
+
* opacity modifier on a token colour.
|
|
15
|
+
* - `tokenValues` — UIG-28: type, corners and shadows written by hand, and an
|
|
16
|
+
* inline style that sets a colour (errors); heights, spacing and border
|
|
17
|
+
* widths written by hand (warnings, never blocking).
|
|
18
|
+
*
|
|
19
|
+
* The patterns are the same for everyone. Only the words differ: an app is told
|
|
20
|
+
* to use "the package", the package is told to add the token to `tokens.css`.
|
|
21
|
+
* Measured on 17 September: the UIG-28 block was byte for byte the same in Peek
|
|
22
|
+
* and Ship, and this repository's had the same patterns with seven messages
|
|
23
|
+
* worded for the package.
|
|
24
|
+
*
|
|
25
|
+
* Where the plugin looks: every `className`, every `cn()` / `clsx()` argument,
|
|
26
|
+
* and the values of a class map whose name ends in `Styles` / `_STYLES` or
|
|
27
|
+
* `Classes` / `_CLASSES`. A class map named any other way is invisible to it.
|
|
28
|
+
*
|
|
29
|
+
* Tests are out of `tokenValues` (ruling of 13 September): a test mounts markup
|
|
30
|
+
* to test it. A kept hand-written value says why:
|
|
31
|
+
* `// eslint-disable-next-line <rule> -- @estiva-escape: <reason>` (A2).
|
|
32
|
+
*/
|
|
33
|
+
import { createRequire } from 'node:module'
|
|
34
|
+
import { fileURLToPath } from 'node:url'
|
|
35
|
+
import type { Linter } from 'eslint'
|
|
36
|
+
import betterTailwindcss from 'eslint-plugin-better-tailwindcss'
|
|
37
|
+
import { getDefaultSelectors } from 'eslint-plugin-better-tailwindcss/defaults'
|
|
38
|
+
import { parser as typescriptParser } from 'typescript-eslint'
|
|
39
|
+
|
|
40
|
+
/** Who reads the messages. */
|
|
41
|
+
export type TokenAudience = 'app' | 'package'
|
|
42
|
+
|
|
43
|
+
export interface TokenLintOptions {
|
|
44
|
+
/** The Tailwind config the unknown-class rule reads, relative to where ESLint runs. */
|
|
45
|
+
tailwindConfig?: string
|
|
46
|
+
/** `app` (the default) names the package; `package` names `tokens.css` and the preset. */
|
|
47
|
+
audience?: TokenAudience
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What the token lint never reads: build output. */
|
|
51
|
+
export const TOKEN_LINT_IGNORES = ['dist/', 'storybook-static/', 'node_modules/']
|
|
52
|
+
|
|
53
|
+
const CLASS_MAP_SELECTOR = {
|
|
54
|
+
kind: 'variable',
|
|
55
|
+
name: '^(?:[A-Z][A-Z0-9_]*_(?:STYLES|CLASSES)|[a-z][a-zA-Z0-9]*(?:Styles|Classes))$',
|
|
56
|
+
match: [{ type: 'strings' }, { type: 'objectValues' }],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const WORDS = {
|
|
60
|
+
app: {
|
|
61
|
+
ramp: "Tailwind's type ramp is not a token. Use the ramp from the package: text-body-2, text-caption, text-h3, text-btn-default...",
|
|
62
|
+
palette: "Tailwind's palette is not a token. Use a colour from the package (bg-bg-surface, text-text-primary, border-border-subtle...); a colour with no token is a missing token.",
|
|
63
|
+
raw: 'A raw colour is a missing token: add it to the package (tokens.css in every theme block, and the preset), or say why not in an eslint-disable comment.',
|
|
64
|
+
opacity: 'An opacity modifier on a token colour compiles to nothing. A transparent colour is a token of its own (D16): add it to the package.',
|
|
65
|
+
type: '$0 is type written by hand. A token from the package sets the size, line height, letter spacing and weight together (text-body-2, text-caption, text-h5...); if none fits, add one to the package, or say why not in an eslint-disable comment.',
|
|
66
|
+
corner: '$0 is a corner written by hand, and no token has it. Use a corner from the package (rounded-sm 4px, rounded-md 6px, rounded-lg 8px...), or say why not in an eslint-disable comment.',
|
|
67
|
+
shadow: '$0 is a shadow written by hand. Use a shadow from the package (shadow-sm, shadow-md, shadow-focus-ring, drop-shadow-glow-success...); a shadow with no token is a missing token.',
|
|
68
|
+
},
|
|
69
|
+
package: {
|
|
70
|
+
ramp: "Tailwind's type ramp is not a token. Use the ramp in tailwind-preset.js: text-body-2, text-caption, text-h3, text-btn-default...",
|
|
71
|
+
palette: "Tailwind's palette is not a token. Use a colour from tailwind-preset.js (bg-bg-surface, text-text-primary, border-border-subtle...); a colour with no token is a missing token.",
|
|
72
|
+
raw: 'A raw colour is a missing token: add it to tokens.css in every theme block and to the preset, or say why not in an eslint-disable comment.',
|
|
73
|
+
opacity: 'An opacity modifier on a token colour compiles to nothing. A transparent colour is a token of its own (D16): add it to tokens.css in every theme block and to the preset.',
|
|
74
|
+
type: '$0 is type written by hand. A token from tailwind-preset.js sets the size, line height, letter spacing and weight together (text-body-2, text-caption, text-h5...); if none fits, add one to the preset and to cn.ts, or say why not in an eslint-disable comment.',
|
|
75
|
+
corner: '$0 is a corner written by hand, and no token has it. Use a corner from tailwind-preset.js (rounded-sm 4px, rounded-md 6px, rounded-lg 8px...), or say why not in an eslint-disable comment.',
|
|
76
|
+
shadow: '$0 is a shadow written by hand. Use a shadow from tailwind-preset.js (shadow-sm, shadow-md, shadow-focus-ring, drop-shadow-glow-success...); a shadow with no token is a missing token.',
|
|
77
|
+
},
|
|
78
|
+
} satisfies Record<TokenAudience, Record<string, string>>
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The class rules. `no-unknown-classes` reads the app's own Tailwind config, so
|
|
82
|
+
* it knows exactly what the preset and the app generate.
|
|
83
|
+
*/
|
|
84
|
+
export function tokenLint({ tailwindConfig = 'tailwind.config.js', audience = 'app' }: TokenLintOptions = {}): Linter.Config {
|
|
85
|
+
const words = WORDS[audience]
|
|
86
|
+
return {
|
|
87
|
+
files: ['**/*.{ts,tsx}'],
|
|
88
|
+
languageOptions: {
|
|
89
|
+
parser: typescriptParser,
|
|
90
|
+
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
91
|
+
},
|
|
92
|
+
plugins: { 'better-tailwindcss': betterTailwindcss },
|
|
93
|
+
settings: {
|
|
94
|
+
'better-tailwindcss': {
|
|
95
|
+
tailwindConfig,
|
|
96
|
+
selectors: [...getDefaultSelectors(), CLASS_MAP_SELECTOR],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
rules: {
|
|
100
|
+
'better-tailwindcss/no-unknown-classes': 'error',
|
|
101
|
+
'better-tailwindcss/no-restricted-classes': [
|
|
102
|
+
'error',
|
|
103
|
+
{
|
|
104
|
+
restrict: [
|
|
105
|
+
{ pattern: '^(?:[a-z0-9-]+:)*text-(?:xs|sm|base|lg|xl|[2-9]xl)$', message: words.ramp },
|
|
106
|
+
{
|
|
107
|
+
pattern:
|
|
108
|
+
'^(?:[a-z0-9-]+:)*(?:text|bg|border|ring|outline|fill|stroke|decoration|divide|placeholder|from|via|to|accent|caret|shadow)-(?:black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)(?:-[0-9]{2,3})?(?:/[0-9]{1,3})?$',
|
|
109
|
+
message: words.palette,
|
|
110
|
+
},
|
|
111
|
+
{ pattern: '^(?:[a-z0-9-]+:)*[a-z-]+-\\[[^\\]]*(?:#[0-9a-fA-F]{3}|rgba?\\(|hsla?\\(|oklch\\()', message: words.raw },
|
|
112
|
+
{
|
|
113
|
+
pattern: '^(?:[a-z0-9-]+:)*(?:text|bg|border|ring|outline|fill|stroke|decoration|divide|placeholder|from|via|to|accent|caret|shadow)-(?:bg|text|border|accent|info|warning|success|error)-[a-z-]+/[0-9]{1,3}$',
|
|
114
|
+
message: words.opacity,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// UIG-28's patterns. The variant part reads arbitrary variants too
|
|
124
|
+
// (`[&_pre]:`, `data-[state]:`, `group-hover/row:`). A colour inside `text-[…]`
|
|
125
|
+
// is left to the raw-colour rule of `tokenLint`.
|
|
126
|
+
const V = '^(?:(?:[^:\\[\\]\\s]|\\[[^\\]]*\\])+:)*!?'
|
|
127
|
+
const NOT_COLOUR = '(?!color:|#|rgba?\\(|hsla?\\(|oklch\\(|var\\()'
|
|
128
|
+
const HAND_WRITTEN = {
|
|
129
|
+
type: `${V}(?:text-\\[${NOT_COLOUR}|text-\\[length:|leading-\\[|tracking-\\[)[^\\]]+\\](?:/\\S*)?$`,
|
|
130
|
+
corner: `${V}rounded(?:-(?:t|r|b|l|s|e|tl|tr|br|bl|ss|se|es|ee))?-\\[[^\\]]+\\]$`,
|
|
131
|
+
shadow: `${V}(?:shadow|drop-shadow)-\\[(?!color:)[^\\]]+\\]$`,
|
|
132
|
+
spacing: `${V}-?(?:w|h|size|min-w|min-h|max-w|max-h|p[xytrblse]?|m[xytrblse]?|gap(?:-[xy])?|space-[xy]|inset(?:-[xy])?|top|right|bottom|left|translate-[xy]|basis|indent|scroll-[mp][xytrblse]?|border-spacing(?:-[xy])?)-\\[[^\\]]+\\]$`,
|
|
133
|
+
width: `${V}(?:border(?:-[xytrblse])?|divide-[xy]|outline|ring|ring-offset)-\\[(?:length:)?-?[0-9.]+(?:px|rem|em)?\\]$`,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const INLINE_STYLE_TOKEN_PROPERTIES =
|
|
137
|
+
'/^(color|background|backgroundColor|backgroundImage|fill|stroke|fontSize|border|borderTop|borderRight|borderBottom|borderLeft|borderBlock|borderInline|borderStyle|boxShadow|textShadow)$|Color$/'
|
|
138
|
+
|
|
139
|
+
interface Preset {
|
|
140
|
+
theme: { extend: { fontSize: Record<string, string | [string, unknown]>; borderRadius: Record<string, string> } }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The preset, loaded the way Tailwind loads it: it imports `tailwindcss/plugin`,
|
|
145
|
+
* which a plain `import` cannot resolve. Read from beside this module, so an app
|
|
146
|
+
* and this repository read the one preset the package ships.
|
|
147
|
+
*/
|
|
148
|
+
function loadPreset(): Preset {
|
|
149
|
+
const require = createRequire(import.meta.url)
|
|
150
|
+
const loadConfig = require('tailwindcss/loadConfig') as (path: string) => Preset
|
|
151
|
+
return loadConfig(fileURLToPath(new URL('../../tailwind-preset.js', import.meta.url)))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* UIG-28: the values the tokens already name, written by hand (Katerina's
|
|
156
|
+
* rulings B11, B12, C3 of 13 September and R2 of 15 September). Register it
|
|
157
|
+
* after `tokenLint`, whose parser and plugin settings it borrows. Under its own
|
|
158
|
+
* rule names, `token-values` and `token-spacing` (the same plugin twice, so one
|
|
159
|
+
* runs as errors and one as warnings): an escape for a hand-written size then
|
|
160
|
+
* cannot also silence a raw colour on the same line.
|
|
161
|
+
*/
|
|
162
|
+
export function tokenValues({ audience = 'app' }: Pick<TokenLintOptions, 'audience'> = {}): Linter.Config {
|
|
163
|
+
const words = WORDS[audience]
|
|
164
|
+
const preset = loadPreset()
|
|
165
|
+
// `text-[14px]` names the token of that size, read from the preset, so the
|
|
166
|
+
// message stays true when the ramp changes.
|
|
167
|
+
const tokensBySize: Record<string, string[]> = {}
|
|
168
|
+
for (const [name, value] of Object.entries(preset.theme.extend.fontSize)) {
|
|
169
|
+
const size = Array.isArray(value) ? value[0] : value
|
|
170
|
+
;(tokensBySize[size] ??= []).push(`text-${name}`)
|
|
171
|
+
}
|
|
172
|
+
const cornersBySize: Record<string, string[]> = {}
|
|
173
|
+
for (const [name, size] of Object.entries(preset.theme.extend.borderRadius)) (cornersBySize[size] ??= []).push(name === 'DEFAULT' ? 'rounded' : `rounded-${name}`)
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
files: ['**/*.{ts,tsx}'],
|
|
177
|
+
ignores: ['**/*.test.ts', '**/*.test.tsx'],
|
|
178
|
+
plugins: { 'token-values': betterTailwindcss, 'token-spacing': betterTailwindcss },
|
|
179
|
+
rules: {
|
|
180
|
+
'token-values/no-restricted-classes': [
|
|
181
|
+
'error',
|
|
182
|
+
{
|
|
183
|
+
restrict: [
|
|
184
|
+
...Object.entries(tokensBySize).map(([size, names]) => ({
|
|
185
|
+
pattern: `${V}text-\\[${size.replace('.', '\\.')}\\](?:/\\S*)?$`,
|
|
186
|
+
message: `$0 is the type ramp written by hand. Use ${names.join(' or ')}.`,
|
|
187
|
+
})),
|
|
188
|
+
{
|
|
189
|
+
pattern: HAND_WRITTEN.type,
|
|
190
|
+
message: words.type,
|
|
191
|
+
},
|
|
192
|
+
...Object.entries(cornersBySize).map(([size, names]) => ({
|
|
193
|
+
pattern: `${V}rounded(?:-(?:t|r|b|l|s|e|tl|tr|br|bl|ss|se|es|ee))?-\\[${size.replace('.', '\\.')}\\]$`,
|
|
194
|
+
message: `$0 is a corner written by hand. Use ${names.join(' or ')} (with the same side, if it has one).`,
|
|
195
|
+
})),
|
|
196
|
+
{
|
|
197
|
+
pattern: HAND_WRITTEN.corner,
|
|
198
|
+
message: words.corner,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pattern: HAND_WRITTEN.shadow,
|
|
202
|
+
message: words.shadow,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
'token-spacing/no-restricted-classes': [
|
|
208
|
+
'warn',
|
|
209
|
+
{
|
|
210
|
+
restrict: [
|
|
211
|
+
{
|
|
212
|
+
pattern: HAND_WRITTEN.spacing,
|
|
213
|
+
message: '$0 is a size or a space written by hand. Use a step of the spacing scale (p-3, h-9, gap-2...) if one fits. Reported, not blocked.',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
pattern: HAND_WRITTEN.width,
|
|
217
|
+
message: '$0 is a border or ring width written by hand. Use border, border-2, ring-1... if one fits. Reported, not blocked.',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
'no-restricted-syntax': [
|
|
223
|
+
'error',
|
|
224
|
+
{
|
|
225
|
+
selector: `JSXAttribute[name.name="style"] > JSXExpressionContainer > ObjectExpression > Property[key.name=${INLINE_STYLE_TOKEN_PROPERTIES}]`,
|
|
226
|
+
message: 'An inline style that sets a colour, a font size, a border or a shadow is outside the token contract: use a token class. If the value is computed (a palette, a size from a prop), say why in an eslint-disable comment. Width, height and transforms are fine.',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
}
|