@estiva-app/ui 0.30.0 → 0.32.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/README.md +12 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/eslint/index.d.ts +2 -0
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +1289 -961
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/no-handmade-empty-state.d.ts +25 -0
- package/dist/eslint/no-handmade-empty-state.d.ts.map +1 -0
- package/dist/eslint/no-handmade-header.d.ts +17 -0
- package/dist/eslint/no-handmade-header.d.ts.map +1 -0
- package/dist/eslint/no-restyled-part.d.ts +20 -1
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/gates/app-checks.d.ts +1 -1
- package/dist/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/index.js +4 -2
- package/dist/gates/index.js.map +2 -2
- package/dist/index.js +142 -123
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/CommandPalette.tsx +1 -0
- package/src/ContainerHeader.mdx +3 -2
- package/src/DialogShell.tsx +22 -17
- package/src/EmptyState.mdx +5 -2
- package/src/FilePicker.stories.tsx +5 -1
- package/src/ReactionPicker.stories.tsx +2 -1
- package/src/eslint/index.test.ts +17 -9
- package/src/eslint/index.ts +12 -2
- package/src/eslint/no-handmade-empty-state.test.ts +151 -0
- package/src/eslint/no-handmade-empty-state.ts +344 -0
- package/src/eslint/no-handmade-header.test.ts +89 -0
- package/src/eslint/no-handmade-header.ts +153 -0
- package/src/eslint/no-restyled-part.ts +11 -0
- package/src/gates/app-checks.ts +5 -3
- package/src/gates/create-app.test.ts +1 -1
- package/src/gates/gates.test.ts +1 -1
package/dist/eslint/index.js
CHANGED
|
@@ -158,1061 +158,1383 @@ var noHandRolledBehaviour = {
|
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
// src/eslint/no-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
// src/eslint/no-handmade-empty-state.ts
|
|
162
|
+
import { statSync as statSync2 } from "node:fs";
|
|
163
|
+
|
|
164
|
+
// src/eslint/no-restyled-part.ts
|
|
165
|
+
import { existsSync as existsSync2, readFileSync, statSync } from "node:fs";
|
|
166
|
+
import { dirname, join, resolve } from "node:path";
|
|
167
|
+
var PLACEMENT = [
|
|
168
|
+
{ what: "space around", pattern: /^m[xytrblse]?-/ },
|
|
169
|
+
{ what: "space inside", pattern: /^p[xytrblse]?-/ },
|
|
170
|
+
{ what: "width and height", pattern: /^(?:w|h|size|min-w|min-h|max-w|max-h)-/ },
|
|
171
|
+
{ what: "shown, and how it lays out", pattern: /^(?:hidden|block|inline|inline-block|inline-flex|inline-grid|flex|grid|contents|flow-root)$/ },
|
|
172
|
+
{ what: "its place in a row or a grid", pattern: /^(?:grow|shrink)$|^(?:flex|basis|grow|shrink|order|gap|gap-x|gap-y|space-x|space-y|col|row|grid-cols|grid-rows|grid-flow|auto-cols|auto-rows)-/ },
|
|
173
|
+
{ what: "alignment", pattern: /^(?:self|justify|justify-items|justify-self|items|place-content|place-items|place-self)-|^content-(?!\[)/ },
|
|
174
|
+
{ what: "position", pattern: /^(?:static|relative|absolute|fixed|sticky)$|^(?:inset|inset-x|inset-y|top|right|bottom|left|start|end|z)-/ },
|
|
175
|
+
// Draws nothing: it names the part, so what is inside can answer its hover (`group-hover:`).
|
|
176
|
+
{ what: "a name for hover", pattern: /^(?:group|peer)(?:\/[\w-]+)?$/ }
|
|
177
|
+
];
|
|
178
|
+
var REACHING_VARIANT = /^\[|^(?:before|after|placeholder|file|marker|selection|first-letter|first-line|backdrop|\*|\*\*)$/;
|
|
179
|
+
function splitClass(token) {
|
|
180
|
+
const variants = [];
|
|
181
|
+
let current = "";
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (const character of token) {
|
|
184
|
+
if (character === "[") depth += 1;
|
|
185
|
+
if (character === "]") depth -= 1;
|
|
186
|
+
if (character === ":" && depth === 0) {
|
|
187
|
+
variants.push(current);
|
|
188
|
+
current = "";
|
|
189
|
+
} else current += character;
|
|
190
|
+
}
|
|
191
|
+
return { variants, utility: current.replace(/^!/, "").replace(/!$/, "").replace(/^-/, "") };
|
|
192
|
+
}
|
|
193
|
+
function isPlacement(token) {
|
|
194
|
+
const { variants, utility } = splitClass(token);
|
|
195
|
+
if (variants.some((variant) => REACHING_VARIANT.test(variant))) return false;
|
|
196
|
+
return PLACEMENT.some(({ pattern }) => pattern.test(utility));
|
|
197
|
+
}
|
|
198
|
+
var isPadding = (token) => {
|
|
199
|
+
const { variants, utility } = splitClass(token);
|
|
200
|
+
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^p[xytrblse]?-/.test(utility);
|
|
196
201
|
};
|
|
197
|
-
var
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
var PART_LOOK_PROPS = {
|
|
203
|
+
AppShell: ["variant"],
|
|
204
|
+
Avatar: ["size"],
|
|
205
|
+
AvatarGroup: ["size"],
|
|
206
|
+
Banner: ["tone"],
|
|
207
|
+
Button: ["variant", "size"],
|
|
208
|
+
Card: ["fill", "hover", "attention", "selected", "active", "hovered", "quietUntilHover", "unreadable", "clip"],
|
|
209
|
+
Checkbox: ["row"],
|
|
210
|
+
Chip: ["type"],
|
|
211
|
+
Divider: ["tone", "orientation"],
|
|
212
|
+
EmptyState: ["scope"],
|
|
213
|
+
FieldLine: ["tone"],
|
|
214
|
+
IconButton: ["variant", "pressed", "glow"],
|
|
215
|
+
InputChip: ["truncate"],
|
|
216
|
+
Link: ["variant", "truncate"],
|
|
217
|
+
MenuItem: ["size", "selected", "destructive"],
|
|
218
|
+
MenuSub: ["selected"],
|
|
219
|
+
NavItem: ["active"],
|
|
220
|
+
Person: ["size"],
|
|
221
|
+
ProgressBar: ["variant"],
|
|
222
|
+
Property: ["layout"],
|
|
223
|
+
RailItem: ["active"],
|
|
224
|
+
Reaction: ["pressed"],
|
|
225
|
+
ReactionPicker: ["surface"],
|
|
226
|
+
SectionHeader: ["hover"],
|
|
227
|
+
SectionLabel: ["tone"],
|
|
228
|
+
Select: ["size"],
|
|
229
|
+
Tabs: ["size"],
|
|
230
|
+
TextInput: ["size"],
|
|
231
|
+
Toast: ["type"],
|
|
232
|
+
Toolbar: ["surface"],
|
|
233
|
+
ToolbarButton: ["variant", "pressed", "glow"],
|
|
234
|
+
TopBar: ["variant"]
|
|
201
235
|
};
|
|
202
|
-
|
|
203
|
-
|
|
236
|
+
var CLASS_PROP = /^(?:className|[a-z][A-Za-z]*ClassName)$/;
|
|
237
|
+
var child = (node, key) => node[key];
|
|
238
|
+
var children = (node, key) => node[key] ?? [];
|
|
239
|
+
var nameOf = (node) => node?.type === "Identifier" || node?.type === "JSXIdentifier" ? node.name : node?.type === "Literal" && typeof node.value === "string" ? node.value : void 0;
|
|
240
|
+
function walk(root, visit) {
|
|
241
|
+
const stack = [root];
|
|
242
|
+
while (stack.length) {
|
|
243
|
+
const node = stack.pop();
|
|
244
|
+
visit(node);
|
|
245
|
+
for (const key of Object.keys(node)) {
|
|
246
|
+
if (key === "parent" || key === "loc" || key === "range" || key === "tokens" || key === "comments") continue;
|
|
247
|
+
const value = node[key];
|
|
248
|
+
if (Array.isArray(value)) for (const item of value) {
|
|
249
|
+
if (item && typeof item.type === "string") stack.push(item);
|
|
250
|
+
}
|
|
251
|
+
else if (value && typeof value.type === "string") stack.push(value);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
204
254
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
255
|
+
var PACKAGE = "@estiva-app/ui";
|
|
256
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
257
|
+
var packageDirCache = /* @__PURE__ */ new Map();
|
|
258
|
+
var packagePartsCache = /* @__PURE__ */ new Map();
|
|
259
|
+
function parseFile(parser, filePath) {
|
|
260
|
+
const code2 = readFileSync(filePath, "utf8");
|
|
261
|
+
const options = { filePath, ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: filePath.endsWith("x") }, range: true, loc: true };
|
|
262
|
+
try {
|
|
263
|
+
if (parser.parseForESLint) return parser.parseForESLint(code2, options).ast;
|
|
264
|
+
if (parser.parse) return parser.parse(code2, options);
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
214
268
|
}
|
|
215
|
-
function
|
|
216
|
-
const
|
|
217
|
-
|
|
269
|
+
function packageDirOf(file) {
|
|
270
|
+
const start = dirname(resolve(file));
|
|
271
|
+
if (packageDirCache.has(start)) return packageDirCache.get(start) ?? null;
|
|
272
|
+
let dir = start;
|
|
273
|
+
for (; ; ) {
|
|
274
|
+
if (existsSync2(join(dir, "package.json"))) break;
|
|
275
|
+
const up = dirname(dir);
|
|
276
|
+
if (up === dir) {
|
|
277
|
+
dir = "";
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
dir = up;
|
|
281
|
+
}
|
|
282
|
+
packageDirCache.set(start, dir || null);
|
|
283
|
+
return dir || null;
|
|
218
284
|
}
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
const part2 = RAW_INPUT_PARTS[key];
|
|
230
|
-
const shown = `<input type="${key}">`;
|
|
231
|
-
return part2 ? { report: "raw", element: shown, part: part2 } : { report: "noPart", element: shown };
|
|
285
|
+
function resolveModule(fromFile, specifier) {
|
|
286
|
+
let base;
|
|
287
|
+
if (specifier.startsWith(".")) base = resolve(dirname(fromFile), specifier);
|
|
288
|
+
else if (specifier.startsWith("@/")) {
|
|
289
|
+
const dir = packageDirOf(fromFile);
|
|
290
|
+
if (!dir) return null;
|
|
291
|
+
base = join(dir, "src", specifier.slice(2));
|
|
292
|
+
} else return null;
|
|
293
|
+
for (const candidate of [base, `${base}.tsx`, `${base}.ts`, join(base, "index.tsx"), join(base, "index.ts")]) {
|
|
294
|
+
if (existsSync2(candidate) && statSync(candidate).isFile()) return candidate;
|
|
232
295
|
}
|
|
233
|
-
|
|
234
|
-
if ((name === "audio" || name === "video") && !isOn(element, "controls")) return null;
|
|
235
|
-
if (name === "img" && !attribute(element, "usemap") && !attribute(element, "useMap")) return null;
|
|
236
|
-
const part = RAW_ELEMENT_PARTS[name];
|
|
237
|
-
return part ? { report: "raw", element: `<${name}>`, part } : { report: "noPart", element: `<${name}>` };
|
|
296
|
+
return null;
|
|
238
297
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
298
|
+
function packagePartsFor(file, parser) {
|
|
299
|
+
const dir = packageDirOf(file);
|
|
300
|
+
if (!dir) return null;
|
|
301
|
+
let name;
|
|
302
|
+
try {
|
|
303
|
+
name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
if (name !== PACKAGE) return null;
|
|
308
|
+
const index = join(dir, "src", "index.ts");
|
|
309
|
+
if (!existsSync2(index)) return null;
|
|
310
|
+
const mtime = statSync(index).mtimeMs;
|
|
311
|
+
const cached = packagePartsCache.get(index);
|
|
312
|
+
if (cached && cached.mtime === mtime) return cached.parts;
|
|
313
|
+
const parts = /* @__PURE__ */ new Set();
|
|
314
|
+
const ast = parseFile(parser, index);
|
|
315
|
+
for (const statement2 of ast ? children(ast, "body") : []) {
|
|
316
|
+
if (statement2?.type !== "ExportNamedDeclaration" || statement2.exportKind === "type") continue;
|
|
317
|
+
for (const specifier of children(statement2, "specifiers")) {
|
|
318
|
+
const exported = nameOf(child(specifier, "exported"));
|
|
319
|
+
if (specifier?.exportKind !== "type" && exported && /^[A-Z][a-z]/.test(exported)) parts.add(exported);
|
|
248
320
|
}
|
|
249
|
-
},
|
|
250
|
-
create(context) {
|
|
251
|
-
return {
|
|
252
|
-
// ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.
|
|
253
|
-
JSXOpeningElement(node) {
|
|
254
|
-
const element = node;
|
|
255
|
-
const found = classify(element);
|
|
256
|
-
if (!found) return;
|
|
257
|
-
if (isEscaped(context, element)) return;
|
|
258
|
-
if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
|
|
259
|
-
else context.report({ loc: element.loc, messageId: "raw", data: { element: found.element, use: found.part.use, more: found.part.more ?? "" } });
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
321
|
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
combobox: { thing: "list", part: LIST_PART },
|
|
321
|
-
tablist: { thing: "set of tabs", part: { use: "Tabs" } },
|
|
322
|
-
tab: { thing: "set of tabs", part: { use: "Tabs" } },
|
|
323
|
-
tabpanel: { thing: "set of tabs", part: { use: "Tabs" } },
|
|
324
|
-
toolbar: { thing: "toolbar", part: { use: "Toolbar" } },
|
|
325
|
-
progressbar: { thing: "progress bar", part: { use: "ProgressBar" } },
|
|
326
|
-
checkbox: { thing: "tick box", part: { use: "Checkbox" } },
|
|
327
|
-
separator: { thing: "divider", part: { use: "Divider" } },
|
|
328
|
-
button: { thing: "button", part: { use: "Button", more: " Icon only, `IconButton`." } },
|
|
329
|
-
link: { thing: "link", part: { use: "Link" } },
|
|
330
|
-
tooltip: { thing: "tooltip", part: { use: "Tooltip", more: " Around a control, `WithTooltip`." } },
|
|
331
|
-
alert: { thing: "message", part: MESSAGE_PART },
|
|
332
|
-
status: { thing: "message", part: MESSAGE_PART },
|
|
333
|
-
menubar: { thing: "menu bar", part: null },
|
|
334
|
-
switch: { thing: "switch", part: null },
|
|
335
|
-
radio: { thing: "radio button", part: null },
|
|
336
|
-
radiogroup: { thing: "radio group", part: null },
|
|
337
|
-
slider: { thing: "slider", part: null },
|
|
338
|
-
spinbutton: { thing: "number field", part: null },
|
|
339
|
-
meter: { thing: "meter", part: null }
|
|
340
|
-
};
|
|
341
|
-
var WALKING_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"];
|
|
342
|
-
var PAGE_EVENTS = {
|
|
343
|
-
mousedown: "press",
|
|
344
|
-
pointerdown: "press",
|
|
345
|
-
click: "press",
|
|
346
|
-
touchstart: "press",
|
|
347
|
-
keydown: "key",
|
|
348
|
-
keyup: "key",
|
|
349
|
-
keypress: "key",
|
|
350
|
-
focusin: "focus",
|
|
351
|
-
focusout: "focus",
|
|
352
|
-
scroll: "follow",
|
|
353
|
-
resize: "follow"
|
|
354
|
-
};
|
|
355
|
-
var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea", "summary", "iframe", "embed", "object", "audio", "video"]);
|
|
356
|
-
var SCROLL_CLASS = /^overflow(?:-[xy])?-(?:auto|scroll)$/;
|
|
357
|
-
var uses = (parts) => [
|
|
358
|
-
...new Set(parts.flatMap((p) => p ? [p.use, ...[...(p.more ?? "").matchAll(/`([A-Z]\w+)`/g)].map((m) => m[1])] : []))
|
|
359
|
-
];
|
|
360
|
-
var OWNED_BEHAVIOURS = [
|
|
361
|
-
{
|
|
362
|
-
id: "base-ui",
|
|
363
|
-
behaviour: "Is built on Base UI",
|
|
364
|
-
baseUi: Object.keys(BASE_UI_PARTS),
|
|
365
|
-
owners: uses(Object.values(BASE_UI_PARTS)),
|
|
366
|
-
reads: "an import from @base-ui/react (and the old @base-ui-components spelling)"
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
id: "portal",
|
|
370
|
-
behaviour: "Floats on top of the page",
|
|
371
|
-
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Autocomplete", "Tooltip", "PreviewCard", "Toast"],
|
|
372
|
-
owners: ["DialogShell", "ConfirmDialog", "CommandPalette", "Lightbox", "Popover", "Menu", "Select", "ChipInput", "Tooltip", "PreviewCard", "Toast"],
|
|
373
|
-
reads: "createPortal, called, or imported and never called"
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
id: "press-outside",
|
|
377
|
-
behaviour: "Closes on a press outside",
|
|
378
|
-
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard"],
|
|
379
|
-
owners: ["DialogShell", "Popover", "Menu", "Select", "PreviewCard"],
|
|
380
|
-
reads: "a mousedown, pointerdown, click or touchstart listener on window or document"
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
id: "page-keys",
|
|
384
|
-
behaviour: "Takes its keys by itself",
|
|
385
|
-
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard", "Toast"],
|
|
386
|
-
owners: ["DialogShell", "Lightbox", "Popover", "Menu", "Select", "Tabs", "Toolbar"],
|
|
387
|
-
reads: "a keydown, keyup or keypress listener on window or document"
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
id: "focus",
|
|
391
|
-
behaviour: "Holds focus inside while open, and gives it back",
|
|
392
|
-
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox"],
|
|
393
|
-
owners: ["DialogShell", "CommandPalette", "Lightbox"],
|
|
394
|
-
reads: "a focusin or focusout listener on window or document; the Tab key compared by hand"
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
id: "scroll-lock",
|
|
398
|
-
behaviour: "Stops the page behind it scrolling",
|
|
399
|
-
baseUi: ["Dialog", "AlertDialog"],
|
|
400
|
-
owners: ["DialogShell", "Lightbox"],
|
|
401
|
-
reads: "overflow written into the style of document.body or document.documentElement"
|
|
402
|
-
},
|
|
403
|
-
{
|
|
404
|
-
id: "follow",
|
|
405
|
-
behaviour: "Stays attached to its anchor on scroll and resize",
|
|
406
|
-
baseUi: ["Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard", "Toast"],
|
|
407
|
-
owners: ["Popover", "Menu", "Select", "Tooltip", "PreviewCard"],
|
|
408
|
-
reads: "a scroll or resize listener on window or document"
|
|
409
|
-
},
|
|
410
|
-
{
|
|
411
|
-
id: "walking",
|
|
412
|
-
behaviour: "Moves through its items with the arrow keys",
|
|
413
|
-
baseUi: ["Menu", "Select", "Combobox", "Autocomplete", "Tabs", "Toolbar"],
|
|
414
|
-
owners: ["Menu", "Select", "ChipInput", "CommandPalette", "Tabs", "Toolbar"],
|
|
415
|
-
reads: `a key compared by hand with ${WALKING_KEYS.join(", ")}`
|
|
416
|
-
},
|
|
417
|
-
{
|
|
418
|
-
id: "role",
|
|
419
|
-
behaviour: "Says what it is to assistive technology",
|
|
420
|
-
baseUi: ["Dialog", "AlertDialog", "Menu", "Select", "Combobox", "Tabs", "Toolbar", "Progress", "Checkbox", "Separator", "Button"],
|
|
421
|
-
owners: uses(Object.values(ROLE_PARTS).map((r) => r.part)),
|
|
422
|
-
reads: `role written by hand: ${Object.keys(ROLE_PARTS).join(", ")}`
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
id: "tab-stop",
|
|
426
|
-
behaviour: "Is reachable with Tab",
|
|
427
|
-
baseUi: ["Button", "Toggle", "Checkbox", "Tabs", "Toolbar", "ScrollArea"],
|
|
428
|
-
owners: ["Button", "IconButton", "Link"],
|
|
429
|
-
reads: "tabIndex that can be 0 or more, on an element that is not a control"
|
|
430
|
-
},
|
|
431
|
-
{
|
|
432
|
-
id: "scroll",
|
|
433
|
-
behaviour: "Scrolls with our scrollbar",
|
|
434
|
-
baseUi: ["ScrollArea"],
|
|
435
|
-
owners: ["ScrollArea"],
|
|
436
|
-
reads: "overflow-auto or overflow-scroll (x or y, behind any variant) in a string, or overflow auto or scroll in a style"
|
|
322
|
+
packagePartsCache.set(index, { mtime, parts });
|
|
323
|
+
return parts;
|
|
324
|
+
}
|
|
325
|
+
function functionComponents(program) {
|
|
326
|
+
const found = /* @__PURE__ */ new Map();
|
|
327
|
+
const unwrap = (node) => {
|
|
328
|
+
let current = node;
|
|
329
|
+
while (current?.type === "CallExpression" && children(current, "arguments").length) {
|
|
330
|
+
const callee = child(current, "callee");
|
|
331
|
+
const text = callee?.type === "Identifier" ? nameOf(callee) : callee?.type === "MemberExpression" ? nameOf(child(callee, "property")) : void 0;
|
|
332
|
+
if (text !== "forwardRef" && text !== "memo") return null;
|
|
333
|
+
current = children(current, "arguments")[0];
|
|
334
|
+
}
|
|
335
|
+
return current?.type === "ArrowFunctionExpression" || current?.type === "FunctionExpression" ? current : null;
|
|
336
|
+
};
|
|
337
|
+
walk(program, (node) => {
|
|
338
|
+
if (node.type === "FunctionDeclaration") {
|
|
339
|
+
const name = nameOf(child(node, "id"));
|
|
340
|
+
if (name && /^[A-Z]/.test(name)) found.set(name, node);
|
|
341
|
+
} else if (node.type === "VariableDeclarator") {
|
|
342
|
+
const name = nameOf(child(node, "id"));
|
|
343
|
+
const fn = unwrap(child(node, "init"));
|
|
344
|
+
if (name && /^[A-Z]/.test(name) && fn) found.set(name, fn);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return found;
|
|
348
|
+
}
|
|
349
|
+
function moduleParts(file, program, parser, stack) {
|
|
350
|
+
const locals = /* @__PURE__ */ new Map();
|
|
351
|
+
const packageParts = packagePartsFor(file, parser);
|
|
352
|
+
const body = children(program, "body");
|
|
353
|
+
const exportsOf = (specifier) => {
|
|
354
|
+
if (specifier === PACKAGE) return "package";
|
|
355
|
+
const target = resolveModule(file, specifier);
|
|
356
|
+
if (!target) return null;
|
|
357
|
+
if (packageParts) return "package";
|
|
358
|
+
return moduleExports(target, parser, stack);
|
|
359
|
+
};
|
|
360
|
+
for (const statement2 of body) {
|
|
361
|
+
if (statement2?.type !== "ImportDeclaration" || statement2.importKind === "type") continue;
|
|
362
|
+
const source = exportsOf(String(child(statement2, "source").value));
|
|
363
|
+
if (!source) continue;
|
|
364
|
+
for (const specifier of children(statement2, "specifiers")) {
|
|
365
|
+
if (!specifier || specifier.importKind === "type") continue;
|
|
366
|
+
const local = nameOf(child(specifier, "local"));
|
|
367
|
+
if (!local) continue;
|
|
368
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
369
|
+
if (source === "package" && !packageParts) locals.set(local, "namespace");
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const imported = specifier.type === "ImportDefaultSpecifier" ? "default" : nameOf(child(specifier, "imported"));
|
|
373
|
+
if (!imported) continue;
|
|
374
|
+
if (source === "package") {
|
|
375
|
+
const isPart2 = packageParts ? packageParts.has(imported) : /^[A-Z]/.test(imported);
|
|
376
|
+
if (isPart2) locals.set(local, { part: imported, props: "all" });
|
|
377
|
+
} else if (source.has(imported)) locals.set(local, source.get(imported));
|
|
378
|
+
}
|
|
437
379
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (node.type === "FunctionDeclaration") {
|
|
442
|
-
return node.parent && /^Export/.test(node.parent.type) ? node.parent : node;
|
|
380
|
+
const components = functionComponents(program);
|
|
381
|
+
if (packageParts) {
|
|
382
|
+
for (const name of components.keys()) if (packageParts.has(name) && !locals.has(name)) locals.set(name, { part: name, props: "all" });
|
|
443
383
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
384
|
+
if (!packageParts) {
|
|
385
|
+
let changed = true;
|
|
386
|
+
for (let round = 0; changed && round < 10; round += 1) {
|
|
387
|
+
changed = false;
|
|
388
|
+
for (const [name, fn] of components) {
|
|
389
|
+
const binding = handsOnTo(fn, (tag) => bindingOfTag(tag, locals));
|
|
390
|
+
if (!binding) continue;
|
|
391
|
+
const next = { ...binding, via: name };
|
|
392
|
+
const previous = locals.get(name);
|
|
393
|
+
if (!previous || JSON.stringify(previous) !== JSON.stringify(next)) {
|
|
394
|
+
if (previous && previous !== "namespace" && !previous.via) continue;
|
|
395
|
+
locals.set(name, next);
|
|
396
|
+
changed = true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
449
400
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
401
|
+
const exports = /* @__PURE__ */ new Map();
|
|
402
|
+
const localBinding = (name) => {
|
|
403
|
+
const binding = name ? locals.get(name) : void 0;
|
|
404
|
+
return binding && binding !== "namespace" ? binding : void 0;
|
|
405
|
+
};
|
|
406
|
+
for (const statement2 of body) {
|
|
407
|
+
if (!statement2) continue;
|
|
408
|
+
if (statement2.type === "ExportNamedDeclaration") {
|
|
409
|
+
const declaration = child(statement2, "declaration");
|
|
410
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
411
|
+
const name = nameOf(child(declaration, "id"));
|
|
412
|
+
const binding = localBinding(name);
|
|
413
|
+
if (name && binding) exports.set(name, binding);
|
|
414
|
+
} else if (declaration?.type === "VariableDeclaration") {
|
|
415
|
+
for (const declarator of children(declaration, "declarations")) {
|
|
416
|
+
const name = nameOf(child(declarator, "id"));
|
|
417
|
+
const init = child(declarator, "init");
|
|
418
|
+
const binding = localBinding(name) ?? (init?.type === "Identifier" ? localBinding(nameOf(init)) : void 0);
|
|
419
|
+
if (name && binding) exports.set(name, binding);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const source = child(statement2, "source");
|
|
423
|
+
const from = source ? exportsOf(String(source.value)) : null;
|
|
424
|
+
for (const specifier of children(statement2, "specifiers")) {
|
|
425
|
+
if (!specifier || specifier.exportKind === "type") continue;
|
|
426
|
+
const local = nameOf(child(specifier, "local"));
|
|
427
|
+
const exported = nameOf(child(specifier, "exported"));
|
|
428
|
+
if (!local || !exported) continue;
|
|
429
|
+
if (!source) {
|
|
430
|
+
const binding = localBinding(local);
|
|
431
|
+
if (binding) exports.set(exported, binding);
|
|
432
|
+
} else if (from === "package") {
|
|
433
|
+
if (/^[A-Z]/.test(local)) exports.set(exported, { part: local, props: "all" });
|
|
434
|
+
} else if (from?.has(local)) exports.set(exported, from.get(local));
|
|
435
|
+
}
|
|
436
|
+
} else if (statement2.type === "ExportAllDeclaration" && !child(statement2, "exported")) {
|
|
437
|
+
const from = exportsOf(String(child(statement2, "source").value));
|
|
438
|
+
if (from && from !== "package") for (const [name, binding] of from) exports.set(name, binding);
|
|
439
|
+
} else if (statement2.type === "ExportDefaultDeclaration") {
|
|
440
|
+
const declaration = child(statement2, "declaration");
|
|
441
|
+
const name = declaration?.type === "Identifier" ? nameOf(declaration) : nameOf(child(declaration, "id"));
|
|
442
|
+
const binding = localBinding(name);
|
|
443
|
+
if (binding) exports.set("default", binding);
|
|
444
|
+
}
|
|
455
445
|
}
|
|
456
|
-
return
|
|
446
|
+
return { locals, exports };
|
|
457
447
|
}
|
|
458
|
-
function
|
|
459
|
-
|
|
460
|
-
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
461
|
-
const quasis = node.quasis;
|
|
462
|
-
const expressions = node.expressions;
|
|
463
|
-
if (node.type === "TemplateLiteral" && expressions?.length === 0) return quasis?.[0]?.value.cooked ?? void 0;
|
|
464
|
-
return void 0;
|
|
448
|
+
function partsOfFile(file, program, parser) {
|
|
449
|
+
return moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
|
|
465
450
|
}
|
|
466
|
-
function
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
451
|
+
function moduleExports(file, parser, stack) {
|
|
452
|
+
const mtime = statSync(file).mtimeMs;
|
|
453
|
+
const cached = moduleCache.get(file);
|
|
454
|
+
if (cached && cached.mtime === mtime) return cached.exports;
|
|
455
|
+
if (stack.has(file)) return /* @__PURE__ */ new Map();
|
|
456
|
+
stack.add(file);
|
|
457
|
+
const program = parseFile(parser, file);
|
|
458
|
+
const exports = program ? moduleParts(file, program, parser, stack).exports : /* @__PURE__ */ new Map();
|
|
459
|
+
stack.delete(file);
|
|
460
|
+
moduleCache.set(file, { mtime, exports });
|
|
461
|
+
return exports;
|
|
474
462
|
}
|
|
475
|
-
function
|
|
476
|
-
if (!
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (typeof node.value === "string") return /^\s*\d+\s*$/.test(node.value);
|
|
481
|
-
return false;
|
|
463
|
+
function bindingOfTag(tag, locals) {
|
|
464
|
+
if (!tag) return void 0;
|
|
465
|
+
if (tag.type === "JSXIdentifier") {
|
|
466
|
+
const binding = locals.get(tag.name);
|
|
467
|
+
return binding && binding !== "namespace" ? binding : void 0;
|
|
482
468
|
}
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
function utilityOf(token) {
|
|
489
|
-
let depth = 0;
|
|
490
|
-
let last = -1;
|
|
491
|
-
for (let i = 0; i < token.length; i++) {
|
|
492
|
-
const c = token[i];
|
|
493
|
-
if (c === "[") depth++;
|
|
494
|
-
else if (c === "]") depth--;
|
|
495
|
-
else if (c === ":" && depth === 0) last = i;
|
|
469
|
+
if (tag.type === "JSXMemberExpression") {
|
|
470
|
+
const object = child(tag, "object");
|
|
471
|
+
const property = nameOf(child(tag, "property"));
|
|
472
|
+
if (object?.type === "JSXIdentifier" && locals.get(object.name) === "namespace" && property) return { part: property, props: "all" };
|
|
496
473
|
}
|
|
497
|
-
return
|
|
474
|
+
return void 0;
|
|
498
475
|
}
|
|
499
|
-
function
|
|
500
|
-
const
|
|
501
|
-
if (!
|
|
502
|
-
|
|
503
|
-
|
|
476
|
+
function handsOnTo(fn, bindingOf) {
|
|
477
|
+
const param = children(fn, "params")[0];
|
|
478
|
+
if (!param) return void 0;
|
|
479
|
+
let restName;
|
|
480
|
+
let propsName;
|
|
481
|
+
const classParams = /* @__PURE__ */ new Map();
|
|
482
|
+
if (param.type === "ObjectPattern") {
|
|
483
|
+
for (const property of children(param, "properties")) {
|
|
484
|
+
if (!property) continue;
|
|
485
|
+
if (property.type === "RestElement") restName = nameOf(child(property, "argument"));
|
|
486
|
+
else if (property.type === "Property") {
|
|
487
|
+
const key = nameOf(child(property, "key"));
|
|
488
|
+
let value = child(property, "value");
|
|
489
|
+
if (value?.type === "AssignmentPattern") value = child(value, "left");
|
|
490
|
+
const local = nameOf(value);
|
|
491
|
+
if (key && local && CLASS_PROP.test(key)) classParams.set(local, key);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} else if (param.type === "Identifier") propsName = nameOf(param);
|
|
495
|
+
let wrapper;
|
|
496
|
+
const forwarded = { props: {} };
|
|
497
|
+
const body = child(fn, "body");
|
|
498
|
+
if (!body) return void 0;
|
|
499
|
+
walk(body, (node) => {
|
|
500
|
+
if (wrapper || node.type !== "JSXOpeningElement") return;
|
|
501
|
+
const binding = bindingOf(child(node, "name"));
|
|
502
|
+
if (!binding) return;
|
|
503
|
+
for (const attribute3 of children(node, "attributes")) {
|
|
504
|
+
if (!attribute3) continue;
|
|
505
|
+
if (attribute3.type === "JSXSpreadAttribute") {
|
|
506
|
+
const argument = nameOf(child(attribute3, "argument"));
|
|
507
|
+
if (argument && (argument === restName || argument === propsName)) wrapper = { part: binding.part, props: binding.props };
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const prop = nameOf(child(attribute3, "name"));
|
|
511
|
+
if (!prop || !CLASS_PROP.test(prop)) continue;
|
|
512
|
+
const partProp = binding.props === "all" ? prop : binding.props[prop];
|
|
513
|
+
if (!partProp) continue;
|
|
514
|
+
walk(attribute3, (inner) => {
|
|
515
|
+
let callerProp;
|
|
516
|
+
if (inner.type === "Identifier") callerProp = classParams.get(inner.name);
|
|
517
|
+
if (inner.type === "MemberExpression" && nameOf(child(inner, "object")) === propsName) {
|
|
518
|
+
const key = nameOf(child(inner, "property"));
|
|
519
|
+
if (key && CLASS_PROP.test(key)) callerProp = key;
|
|
520
|
+
}
|
|
521
|
+
if (!callerProp) return;
|
|
522
|
+
if (forwarded.part && forwarded.part !== binding.part) return;
|
|
523
|
+
forwarded.part = binding.part;
|
|
524
|
+
forwarded.props[callerProp] = partProp;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
if (wrapper) return wrapper;
|
|
529
|
+
return forwarded.part ? { part: forwarded.part, props: forwarded.props } : void 0;
|
|
504
530
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
531
|
+
var CLASS_FUNCTIONS = /* @__PURE__ */ new Set(["cn", "clsx", "cx", "twMerge", "classNames"]);
|
|
532
|
+
function constantOf(identifier, scope) {
|
|
533
|
+
let current = scope;
|
|
534
|
+
while (current) {
|
|
535
|
+
const variable = current.set.get(identifier.name);
|
|
536
|
+
if (variable) {
|
|
537
|
+
const definition = variable.defs[0];
|
|
538
|
+
if (variable.defs.length !== 1 || definition?.type !== "Variable") return void 0;
|
|
539
|
+
if (definition.parent?.kind !== "const") return void 0;
|
|
540
|
+
return child(definition.node, "init") ?? void 0;
|
|
541
|
+
}
|
|
542
|
+
current = current.upper;
|
|
543
|
+
}
|
|
544
|
+
return void 0;
|
|
511
545
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
546
|
+
function classesOf(value, scopeOf, asMap = false, seen = /* @__PURE__ */ new Set()) {
|
|
547
|
+
if (!value || seen.has(value) || seen.size > 200) return [];
|
|
548
|
+
seen.add(value);
|
|
549
|
+
const read = (node, map = false) => classesOf(node, scopeOf, map, seen);
|
|
550
|
+
const words = (text) => typeof text === "string" ? text.split(/\s+/).filter(Boolean) : [];
|
|
551
|
+
switch (value.type) {
|
|
552
|
+
case "Literal":
|
|
553
|
+
return words(value.value);
|
|
554
|
+
case "TemplateLiteral":
|
|
555
|
+
return [...children(value, "quasis").flatMap((quasi) => words((quasi?.value).cooked)), ...children(value, "expressions").flatMap((e) => read(e))];
|
|
556
|
+
case "JSXExpressionContainer":
|
|
557
|
+
case "TSAsExpression":
|
|
558
|
+
case "TSSatisfiesExpression":
|
|
559
|
+
case "TSNonNullExpression":
|
|
560
|
+
case "ChainExpression":
|
|
561
|
+
return read(child(value, "expression"), asMap);
|
|
562
|
+
case "ConditionalExpression":
|
|
563
|
+
return [...read(child(value, "consequent")), ...read(child(value, "alternate"))];
|
|
564
|
+
case "LogicalExpression":
|
|
565
|
+
return value.operator === "&&" ? read(child(value, "right")) : [...read(child(value, "left")), ...read(child(value, "right"))];
|
|
566
|
+
case "BinaryExpression":
|
|
567
|
+
return value.operator === "+" ? [...read(child(value, "left")), ...read(child(value, "right"))] : [];
|
|
568
|
+
case "ArrayExpression":
|
|
569
|
+
return children(value, "elements").flatMap((e) => read(e));
|
|
570
|
+
case "ObjectExpression":
|
|
571
|
+
return children(value, "properties").flatMap((property) => {
|
|
572
|
+
if (property?.type !== "Property") return [];
|
|
573
|
+
return asMap ? read(child(property, "value"), true) : property.computed ? [] : words(nameOf(child(property, "key")));
|
|
574
|
+
});
|
|
575
|
+
case "CallExpression": {
|
|
576
|
+
const callee = child(value, "callee");
|
|
577
|
+
return callee?.type === "Identifier" && CLASS_FUNCTIONS.has(callee.name) ? children(value, "arguments").flatMap((a) => read(a)) : [];
|
|
578
|
+
}
|
|
579
|
+
case "Identifier": {
|
|
580
|
+
const constant = constantOf(value, scopeOf(value));
|
|
581
|
+
return constant ? read(constant, asMap) : [];
|
|
582
|
+
}
|
|
583
|
+
case "MemberExpression": {
|
|
584
|
+
const object = child(value, "object");
|
|
585
|
+
const target = object?.type === "Identifier" ? constantOf(object, scopeOf(object)) : object;
|
|
586
|
+
const map = target?.type === "TSAsExpression" || target?.type === "TSSatisfiesExpression" ? child(target, "expression") : target;
|
|
587
|
+
if (map?.type !== "ObjectExpression") return [];
|
|
588
|
+
const property = child(value, "property");
|
|
589
|
+
const key = value.computed ? property?.type === "Literal" ? String(property.value) : void 0 : nameOf(property);
|
|
590
|
+
return children(map, "properties").flatMap((entry) => {
|
|
591
|
+
if (entry?.type !== "Property") return [];
|
|
592
|
+
if (key !== void 0 && nameOf(child(entry, "key")) !== key) return [];
|
|
593
|
+
return read(child(entry, "value"), true);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
default:
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
514
599
|
}
|
|
515
|
-
var
|
|
600
|
+
var code = (items) => items.map((item) => `\`${item}\``).join(" ");
|
|
601
|
+
var or = (items) => items.length === 1 ? code(items) : `${items.slice(0, -1).map((i) => `\`${i}\``).join(", ")} or \`${items[items.length - 1]}\``;
|
|
602
|
+
var noRestyledPart = {
|
|
516
603
|
meta: {
|
|
517
604
|
type: "problem",
|
|
518
|
-
docs: { description: "
|
|
605
|
+
docs: { description: "Only placement passes into a part of @estiva-app/ui through a class prop" },
|
|
519
606
|
schema: [],
|
|
520
607
|
messages: {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
portal: "A layer put on top of the page by hand (`createPortal`). Every floating part of @estiva-app/ui carries its own: `DialogShell` for a dialog, `Popover` for a panel, `Menu`, `Select`, `Tooltip`, `PreviewCard`, `Toast`.",
|
|
524
|
-
press: "A `{{event}}` listener on `{{target}}`: closing on a press outside, by hand. `Popover`, `Menu`, `Select`, `DialogShell` and `PreviewCard` from @estiva-app/ui close themselves.",
|
|
525
|
-
key: "A `{{event}}` listener on `{{target}}`: keys taken for the whole page, by hand. `DialogShell`, `Popover`, `Menu` and `Select` from @estiva-app/ui close on Escape themselves; `Menu`, `Select`, `Tabs` and `Toolbar` move with the arrow keys.",
|
|
526
|
-
focus: "A `{{event}}` listener on `{{target}}`: focus held by hand. `DialogShell` and `CommandPalette` from @estiva-app/ui keep focus inside and give it back.",
|
|
527
|
-
follow: "A `{{event}}` listener on `{{target}}`: following an anchor by hand. `Popover`, `Menu`, `Select`, `Tooltip` and `PreviewCard` from @estiva-app/ui stay attached to theirs.",
|
|
528
|
-
scrollLock: "The page's scroll locked by hand. `DialogShell` from @estiva-app/ui stops the page scrolling while it is open, and gives it back.",
|
|
529
|
-
walking: "Arrow keys handled by hand (`{{key}}`). `Menu`, `Select`, `ChipInput`, `CommandPalette`, `Tabs` and `Toolbar` from @estiva-app/ui move through their items themselves.",
|
|
530
|
-
tabKey: "The Tab key handled by hand. `DialogShell` and `CommandPalette` from @estiva-app/ui keep focus inside themselves.",
|
|
531
|
-
role: 'A hand-written `role="{{role}}"` is a hand-made {{thing}}. Use `{{use}}` from @estiva-app/ui.{{more}}',
|
|
532
|
-
roleNoPart: 'A hand-written `role="{{role}}"` is a hand-made {{thing}}, and @estiva-app/ui has no part for one yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.',
|
|
533
|
-
tabStop: "`tabIndex={{value}}` makes a `<{{element}}>` a Tab stop by hand. Use `Button`, `IconButton` or `Link` from @estiva-app/ui, which are reachable already.",
|
|
534
|
-
scrollClass: "`{{token}}` scrolls with the browser's scrollbar. Use `ScrollArea` from @estiva-app/ui, which draws ours.",
|
|
535
|
-
scrollStyle: "`{{style}}` scrolls with the browser's scrollbar. Use `ScrollArea` from @estiva-app/ui, which draws ours.",
|
|
608
|
+
restyled: "{{classes}} on {{where}} changes how it looks. A part of @estiva-app/ui is placed from outside, never restyled: only space, size, flex and grid, and position pass in.{{use}} A look for what is around it goes on your own element around it.",
|
|
609
|
+
emptyStatePadding: "{{classes}} pads {{where}}. EmptyState takes no padding (Katerina, 14 September): its room comes from the box its rows live in, so put the padding on that box.",
|
|
536
610
|
...ESCAPE_MESSAGES
|
|
537
611
|
}
|
|
538
612
|
},
|
|
539
613
|
create(context) {
|
|
540
|
-
const
|
|
541
|
-
const
|
|
614
|
+
const parser = context.languageOptions?.parser ?? {};
|
|
615
|
+
const file = context.filename;
|
|
616
|
+
const sourceCode = context.sourceCode;
|
|
617
|
+
const scopeOf = (node) => sourceCode.getScope(node);
|
|
618
|
+
let locals;
|
|
542
619
|
const escapes = /* @__PURE__ */ new Map();
|
|
543
620
|
const escaped = (anchor) => {
|
|
544
621
|
if (!escapes.has(anchor)) escapes.set(anchor, isEscaped(context, anchor));
|
|
545
622
|
return escapes.get(anchor) === true;
|
|
546
623
|
};
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (part) report(anchor, node, "baseUi", { module: value, use: part.use, more: part.more ?? "" });
|
|
563
|
-
else report(anchor, node, "baseUiNoPart", { module: value });
|
|
564
|
-
};
|
|
565
|
-
const keyCompared = (node, key) => {
|
|
566
|
-
if (key === void 0) return;
|
|
567
|
-
const fn = enclosingFunction(node);
|
|
568
|
-
const anchor = anchorOf(fn ?? node);
|
|
569
|
-
if (WALKING_KEYS.includes(key)) report(anchor, node, "walking", { key }, `walking@${fn?.range[0] ?? node.range[0]}`);
|
|
570
|
-
else if (key === "Tab") report(anchor, node, "tabKey", {}, `tab@${fn?.range[0] ?? node.range[0]}`);
|
|
571
|
-
};
|
|
572
|
-
const classTokens = (node, value) => {
|
|
573
|
-
for (const token of value.split(/\s+/)) {
|
|
574
|
-
if (token && SCROLL_CLASS.test(utilityOf(token))) report(anchorOf(node), node, "scrollClass", { token }, `class@${token}`);
|
|
624
|
+
const check = (element, binding, prop, value, at) => {
|
|
625
|
+
const partProp = binding.props === "all" ? prop : binding.props[prop];
|
|
626
|
+
if (!partProp || !value) return;
|
|
627
|
+
const classes = classesOf(value, scopeOf);
|
|
628
|
+
const handedOn = binding.props === "all" ? "its props" : `its \`${prop}\``;
|
|
629
|
+
const via = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands ${handedOn} to \`${binding.part}\`)` : `\`${binding.part}\``;
|
|
630
|
+
const where = `${via}${partProp === "className" ? "" : `'s \`${partProp}\``}`;
|
|
631
|
+
const restyled = [...new Set(classes.filter((token) => !isPlacement(token)))];
|
|
632
|
+
const padding = binding.part === "EmptyState" && binding.props === "all" ? [...new Set(classes.filter(isPadding))] : [];
|
|
633
|
+
if (!restyled.length && !padding.length) return;
|
|
634
|
+
if (escaped(element)) return;
|
|
635
|
+
if (restyled.length) {
|
|
636
|
+
const props = PART_LOOK_PROPS[binding.part];
|
|
637
|
+
const use = props ? ` Use its ${or(props)}.` : ` \`${binding.part}\` has no prop for this yet: ask Katerina, and it gets made in @estiva-app/ui.`;
|
|
638
|
+
context.report({ loc: at.loc, messageId: "restyled", data: { classes: code(restyled), where, use } });
|
|
575
639
|
}
|
|
640
|
+
if (padding.length) context.report({ loc: at.loc, messageId: "emptyStatePadding", data: { classes: code(padding), where } });
|
|
576
641
|
};
|
|
577
|
-
const portalImports = /* @__PURE__ */ new Map();
|
|
578
|
-
let portalCalled = false;
|
|
579
642
|
return {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
importSource(node, node.source);
|
|
583
|
-
if (stringOf(node.source) !== "react-dom") return;
|
|
584
|
-
for (const specifier of node.specifiers) {
|
|
585
|
-
const imported = specifier.imported;
|
|
586
|
-
if (specifier.type === "ImportSpecifier" && imported?.name === "createPortal") portalImports.set(specifier.local.name, node);
|
|
587
|
-
}
|
|
588
|
-
},
|
|
589
|
-
ExportNamedDeclaration(ruleNode) {
|
|
590
|
-
const node = ruleNode;
|
|
591
|
-
if (node.source) importSource(node, node.source);
|
|
592
|
-
},
|
|
593
|
-
ExportAllDeclaration(ruleNode) {
|
|
594
|
-
const node = ruleNode;
|
|
595
|
-
importSource(node, node.source);
|
|
596
|
-
},
|
|
597
|
-
ImportExpression(ruleNode) {
|
|
598
|
-
const node = ruleNode;
|
|
599
|
-
importSource(node, node.source);
|
|
600
|
-
},
|
|
601
|
-
CallExpression(ruleNode) {
|
|
602
|
-
const node = ruleNode;
|
|
603
|
-
const callee = node.callee;
|
|
604
|
-
const args = node.arguments;
|
|
605
|
-
if (callee.type === "Identifier" && callee.name === "require") importSource(node, args[0]);
|
|
606
|
-
if (callee.type === "Identifier" && portalImports.has(callee.name)) {
|
|
607
|
-
portalCalled = true;
|
|
608
|
-
report(anchorOf(node), node, "portal");
|
|
609
|
-
}
|
|
610
|
-
if (callee.type !== "MemberExpression") return;
|
|
611
|
-
const object = callee.object;
|
|
612
|
-
const property = callee.property;
|
|
613
|
-
if (property.type !== "Identifier") return;
|
|
614
|
-
if (property.name === "createPortal") {
|
|
615
|
-
report(anchorOf(node), node, "portal");
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
if (property.name === "addEventListener") {
|
|
619
|
-
const target = text(object);
|
|
620
|
-
if (!pageTarget(object, target)) return;
|
|
621
|
-
const event = stringOf(args[0]);
|
|
622
|
-
const group = event === void 0 ? void 0 : PAGE_EVENTS[event];
|
|
623
|
-
if (!event || !group) return;
|
|
624
|
-
report(anchorOf(node), node, group, { event, target });
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if (property.name === "includes" && object.type === "ArrayExpression" && isKeySubject(args[0])) {
|
|
628
|
-
for (const element of object.elements) keyCompared(node, stringOf(element));
|
|
629
|
-
}
|
|
630
|
-
},
|
|
631
|
-
AssignmentExpression(ruleNode) {
|
|
632
|
-
const node = ruleNode;
|
|
633
|
-
const left = node.left;
|
|
634
|
-
if (left.type !== "MemberExpression") return;
|
|
635
|
-
const property = left.property;
|
|
636
|
-
const object = left.object;
|
|
637
|
-
const handler = property.type === "Identifier" ? /^on([a-z]+)$/.exec(property.name) : null;
|
|
638
|
-
if (handler && pageTarget(object, text(object)) && PAGE_EVENTS[handler[1]]) {
|
|
639
|
-
report(anchorOf(node), node, PAGE_EVENTS[handler[1]], { event: handler[1], target: text(object) });
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (/^document\.(?:body|documentElement)\.style\.overflow[XY]?$/.test(text(left))) report(anchorOf(node), node, "scrollLock");
|
|
643
|
-
},
|
|
644
|
-
BinaryExpression(ruleNode) {
|
|
645
|
-
const node = ruleNode;
|
|
646
|
-
if (!["===", "==", "!==", "!="].includes(node.operator)) return;
|
|
647
|
-
const left = node.left;
|
|
648
|
-
const right = node.right;
|
|
649
|
-
if (isKeySubject(left)) keyCompared(node, stringOf(right));
|
|
650
|
-
else if (isKeySubject(right)) keyCompared(node, stringOf(left));
|
|
651
|
-
},
|
|
652
|
-
SwitchCase(ruleNode) {
|
|
653
|
-
const node = ruleNode;
|
|
654
|
-
const statement2 = node.parent;
|
|
655
|
-
if (node.test && isKeySubject(statement2.discriminant)) keyCompared(node, stringOf(node.test));
|
|
643
|
+
Program(program) {
|
|
644
|
+
locals = moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
|
|
656
645
|
},
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
if (
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const { thing, part } = ROLE_PARTS[role];
|
|
668
|
-
if (part) report(element, node, "role", { role, thing, use: part.use, more: part.more ?? "" });
|
|
669
|
-
else report(element, node, "roleNoPart", { role, thing });
|
|
670
|
-
return;
|
|
646
|
+
JSXOpeningElement(ruleNode) {
|
|
647
|
+
const element = ruleNode;
|
|
648
|
+
const binding = locals && bindingOfTag(child(element, "name"), locals);
|
|
649
|
+
if (!binding) return;
|
|
650
|
+
for (const attribute3 of children(element, "attributes")) {
|
|
651
|
+
if (!attribute3) continue;
|
|
652
|
+
if (attribute3.type === "JSXAttribute") {
|
|
653
|
+
const prop = nameOf(child(attribute3, "name"));
|
|
654
|
+
if (prop && CLASS_PROP.test(prop)) check(element, binding, prop, child(attribute3, "value"), attribute3);
|
|
655
|
+
continue;
|
|
671
656
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (editable || !canBeTabStop(value)) return;
|
|
680
|
-
report(element, node, "tabStop", { value: value ? text(value).replace(/^\{|\}$/g, "") : "", element: tag.name });
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
if (name === "style" && value?.type === "JSXExpressionContainer") {
|
|
684
|
-
const object = value.expression;
|
|
685
|
-
if (object.type !== "ObjectExpression") return;
|
|
686
|
-
for (const property of object.properties) {
|
|
687
|
-
if (property.type !== "Property") continue;
|
|
688
|
-
const key = property.key;
|
|
689
|
-
const keyName = key.type === "Identifier" ? key.name : stringOf(key);
|
|
690
|
-
if (!keyName || !/^overflow[XY]?$/.test(keyName)) continue;
|
|
691
|
-
const setting = stringOf(property.value);
|
|
692
|
-
if (setting === "auto" || setting === "scroll") report(element, property, "scrollStyle", { style: `${keyName}: '${setting}'` }, `style@${keyName}`);
|
|
657
|
+
let spread = child(attribute3, "argument");
|
|
658
|
+
if (spread?.type === "Identifier") spread = constantOf(spread, scopeOf(spread));
|
|
659
|
+
while (spread?.type === "TSAsExpression" || spread?.type === "TSSatisfiesExpression") spread = child(spread, "expression");
|
|
660
|
+
if (spread?.type !== "ObjectExpression") continue;
|
|
661
|
+
for (const property of children(spread, "properties")) {
|
|
662
|
+
const key = property?.type === "Property" && !property.computed ? nameOf(child(property, "key")) : void 0;
|
|
663
|
+
if (key && CLASS_PROP.test(key)) check(element, binding, key, child(property, "value"), property);
|
|
693
664
|
}
|
|
694
665
|
}
|
|
695
|
-
},
|
|
696
|
-
Literal(ruleNode) {
|
|
697
|
-
const node = ruleNode;
|
|
698
|
-
if (typeof node.value !== "string") return;
|
|
699
|
-
const parent = node.parent;
|
|
700
|
-
if (parent && /^(Import|Export)/.test(parent.type)) return;
|
|
701
|
-
if (parent?.type === "TSLiteralType" || parent?.type === "TSExternalModuleReference") return;
|
|
702
|
-
classTokens(node, node.value);
|
|
703
|
-
},
|
|
704
|
-
TemplateElement(ruleNode) {
|
|
705
|
-
const node = ruleNode;
|
|
706
|
-
const cooked = node.value.cooked;
|
|
707
|
-
if (cooked && node.parent) classTokens(node.parent, cooked);
|
|
708
|
-
},
|
|
709
|
-
"Program:exit"() {
|
|
710
|
-
if (portalCalled) return;
|
|
711
|
-
for (const declaration of new Set(portalImports.values())) report(anchorOf(declaration), declaration, "portal");
|
|
712
666
|
}
|
|
713
667
|
};
|
|
714
668
|
}
|
|
715
669
|
};
|
|
716
670
|
|
|
717
|
-
// src/eslint/no-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
var
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
671
|
+
// src/eslint/no-handmade-empty-state.ts
|
|
672
|
+
var child2 = (node, key) => node[key];
|
|
673
|
+
var children2 = (node, key) => (node[key] ?? []).filter((n) => !!n);
|
|
674
|
+
var nameOf2 = (node) => node?.type === "Identifier" || node?.type === "JSXIdentifier" ? node.name : void 0;
|
|
675
|
+
var tagName = (element) => nameOf2(child2(child2(element, "openingElement"), "name"));
|
|
676
|
+
var isPart = (binding, part) => binding?.part === part && binding.props === "all";
|
|
677
|
+
var attributeOf = (opening, name) => children2(opening, "attributes").find((a) => a.type === "JSXAttribute" && nameOf2(child2(a, "name")) === name);
|
|
678
|
+
var SHORT = 120;
|
|
679
|
+
function onlyWords(element) {
|
|
680
|
+
let line = "";
|
|
681
|
+
let own = "";
|
|
682
|
+
for (const kid of children2(element, "children")) {
|
|
683
|
+
let piece;
|
|
684
|
+
if (kid.type === "JSXText") piece = String(kid.value);
|
|
685
|
+
else if (kid.type === "JSXExpressionContainer") {
|
|
686
|
+
const e = child2(kid, "expression");
|
|
687
|
+
if (e.type === "JSXEmptyExpression") continue;
|
|
688
|
+
if (e.type === "Literal" && typeof e.value === "string") piece = e.value;
|
|
689
|
+
else if (e.type === "TemplateLiteral" && children2(e, "expressions").length === 0) piece = children2(e, "quasis").map((q) => q.value.cooked).join("");
|
|
690
|
+
else if (e.type === "Identifier" || e.type === "MemberExpression") {
|
|
691
|
+
line += "x";
|
|
692
|
+
continue;
|
|
693
|
+
} else return void 0;
|
|
694
|
+
} else return void 0;
|
|
695
|
+
own += piece;
|
|
696
|
+
line += piece;
|
|
743
697
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
function isPlacement(token) {
|
|
747
|
-
const { variants, utility } = splitClass(token);
|
|
748
|
-
if (variants.some((variant) => REACHING_VARIANT.test(variant))) return false;
|
|
749
|
-
return PLACEMENT.some(({ pattern }) => pattern.test(utility));
|
|
698
|
+
if (!/[a-z]{2,}/i.test(own)) return void 0;
|
|
699
|
+
return line.replace(/\s+/g, " ").trim();
|
|
750
700
|
}
|
|
751
|
-
var
|
|
752
|
-
|
|
753
|
-
return !
|
|
701
|
+
var isCount = (node) => {
|
|
702
|
+
if (node?.type === "ChainExpression") return isCount(child2(node, "expression"));
|
|
703
|
+
return node?.type === "MemberExpression" && !node.computed && /^(?:length|size)$/.test(nameOf2(child2(node, "property")) ?? "");
|
|
754
704
|
};
|
|
755
|
-
var
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
705
|
+
var isNumber = (node, n) => node?.type === "Literal" && node.value === n;
|
|
706
|
+
function whenTrue(test) {
|
|
707
|
+
if (!test) return void 0;
|
|
708
|
+
if (isCount(test)) return "full";
|
|
709
|
+
switch (test.type) {
|
|
710
|
+
case "UnaryExpression": {
|
|
711
|
+
if (test.operator !== "!") return void 0;
|
|
712
|
+
const inner = whenTrue(child2(test, "argument"));
|
|
713
|
+
return inner === "empty" ? "full" : inner === "full" ? "empty" : void 0;
|
|
714
|
+
}
|
|
715
|
+
case "LogicalExpression":
|
|
716
|
+
if (test.operator !== "&&") return void 0;
|
|
717
|
+
return whenTrue(child2(test, "left")) ?? whenTrue(child2(test, "right"));
|
|
718
|
+
case "BinaryExpression": {
|
|
719
|
+
let [count, other, op] = [child2(test, "left"), child2(test, "right"), String(test.operator)];
|
|
720
|
+
if (!isCount(count)) [count, other, op] = [other, count, { "<": ">", ">": "<", "<=": ">=", ">=": "<=" }[op] ?? op];
|
|
721
|
+
if (!isCount(count)) return void 0;
|
|
722
|
+
if (isNumber(other, 0)) return op === "===" || op === "==" || op === "<=" ? "empty" : op === ">" || op === "!==" || op === "!=" ? "full" : void 0;
|
|
723
|
+
if (isNumber(other, 1)) return op === "<" ? "empty" : op === ">=" ? "full" : void 0;
|
|
724
|
+
return void 0;
|
|
725
|
+
}
|
|
726
|
+
case "CallExpression": {
|
|
727
|
+
const callee = child2(test, "callee");
|
|
728
|
+
const name = callee?.type === "MemberExpression" ? nameOf2(child2(callee, "property")) : nameOf2(callee);
|
|
729
|
+
return name && /^is(?:Empty|Blank)$/.test(name) ? "empty" : void 0;
|
|
730
|
+
}
|
|
731
|
+
default:
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
var isMapCall = (node) => {
|
|
736
|
+
const callee = node?.type === "CallExpression" ? child2(node, "callee") : void 0;
|
|
737
|
+
return callee?.type === "MemberExpression" && nameOf2(child2(callee, "property")) === "map";
|
|
788
738
|
};
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
739
|
+
function isAList(branch, depth = 0) {
|
|
740
|
+
if (!branch || depth > 2) return false;
|
|
741
|
+
if (isMapCall(branch)) return true;
|
|
742
|
+
if (branch.type !== "JSXElement" && branch.type !== "JSXFragment") return false;
|
|
743
|
+
if (branch.type === "JSXElement" && !/^[a-z]/.test(tagName(branch) ?? "")) return false;
|
|
744
|
+
const kids = children2(branch, "children").filter((k) => !(k.type === "JSXText" && !String(k.value).trim()) && !(k.type === "JSXExpressionContainer" && child2(k, "expression").type === "JSXEmptyExpression"));
|
|
745
|
+
return kids.length === 1 && (kids[0].type === "JSXExpressionContainer" ? isMapCall(child2(kids[0], "expression")) : isAList(kids[0], depth + 1));
|
|
746
|
+
}
|
|
747
|
+
function soleChildOf(parent, element) {
|
|
748
|
+
if (parent?.type !== "JSXElement" || !/^[a-z]/.test(tagName(parent) ?? "")) return false;
|
|
749
|
+
const kids = children2(parent, "children").filter((k) => !(k.type === "JSXText" && !String(k.value).trim()) && !(k.type === "JSXExpressionContainer" && child2(k, "expression").type === "JSXEmptyExpression"));
|
|
750
|
+
return kids.length === 1 && kids[0] === element;
|
|
751
|
+
}
|
|
752
|
+
function standsInForAList(element) {
|
|
753
|
+
let at = element;
|
|
754
|
+
for (let depth = 0; depth < 3 && soleChildOf(at.parent, at); depth += 1) at = at.parent;
|
|
755
|
+
const parent = at.parent;
|
|
756
|
+
if (!parent) return false;
|
|
757
|
+
if (parent.type === "ConditionalExpression" && parent.test !== at) {
|
|
758
|
+
const consequent = parent.consequent === at;
|
|
759
|
+
const other = child2(parent, consequent ? "alternate" : "consequent");
|
|
760
|
+
return whenTrue(child2(parent, "test")) === (consequent ? "empty" : "full") || isAList(other);
|
|
761
|
+
}
|
|
762
|
+
if (parent.type === "LogicalExpression" && parent.operator === "&&" && parent.right === at) return whenTrue(child2(parent, "left")) === "empty";
|
|
763
|
+
if (parent.type === "ReturnStatement") {
|
|
764
|
+
let holder = parent.parent;
|
|
765
|
+
if (holder?.type === "BlockStatement" && children2(holder, "body").length === 1) holder = holder.parent;
|
|
766
|
+
return holder?.type === "IfStatement" && child2(holder, "consequent") !== void 0 && whenTrue(child2(holder, "test")) === "empty";
|
|
767
|
+
}
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
var FILLS = /^(?:min-h-full|h-full)$/;
|
|
771
|
+
function isPageScope(opening) {
|
|
772
|
+
const scope = attributeOf(opening, "scope");
|
|
773
|
+
if (!scope) return true;
|
|
774
|
+
const value = child2(scope, "value");
|
|
775
|
+
return value?.type === "Literal" && value.value === "page";
|
|
776
|
+
}
|
|
777
|
+
var factsCache = /* @__PURE__ */ new Map();
|
|
778
|
+
function factsOf(file, program, parser) {
|
|
779
|
+
const imports = /* @__PURE__ */ new Map();
|
|
780
|
+
for (const statement2 of children2(program, "body")) {
|
|
781
|
+
if (statement2.type !== "ImportDeclaration" || statement2.importKind === "type") continue;
|
|
782
|
+
const target = resolveModule(file, String(child2(statement2, "source").value));
|
|
783
|
+
if (!target) continue;
|
|
784
|
+
for (const specifier of children2(statement2, "specifiers")) {
|
|
785
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") continue;
|
|
786
|
+
const local = nameOf2(child2(specifier, "local"));
|
|
787
|
+
const imported = nameOf2(child2(specifier, "imported"));
|
|
788
|
+
if (local && imported) imports.set(local, { file: target, name: imported });
|
|
805
789
|
}
|
|
806
790
|
}
|
|
791
|
+
return { file, locals: partsOfFile(file, program, parser), components: functionComponents(program), imports };
|
|
807
792
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
const
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
793
|
+
function factsOfFile(file, parser) {
|
|
794
|
+
const mtime = statSync2(file).mtimeMs;
|
|
795
|
+
const cached = factsCache.get(file);
|
|
796
|
+
if (cached && cached.mtime === mtime) return cached.facts;
|
|
797
|
+
factsCache.set(file, { mtime, facts: null });
|
|
798
|
+
const program = parseFile(parser, file);
|
|
799
|
+
const facts = program ? factsOf(file, program, parser) : null;
|
|
800
|
+
factsCache.set(file, { mtime, facts });
|
|
801
|
+
return facts;
|
|
802
|
+
}
|
|
803
|
+
function pageStatesOf(facts, name, parser, depth, seen) {
|
|
804
|
+
const key = `${facts.file}#${name}`;
|
|
805
|
+
if (depth > 4 || seen.has(key)) return [];
|
|
806
|
+
seen.add(key);
|
|
807
|
+
const local = facts.components.get(name);
|
|
808
|
+
if (!local) {
|
|
809
|
+
const from = facts.imports.get(name);
|
|
810
|
+
const there = from ? factsOfFile(from.file, parser) : null;
|
|
811
|
+
return there && from ? pageStatesOf(there, from.name, parser, depth + 1, seen) : [];
|
|
819
812
|
}
|
|
820
|
-
|
|
813
|
+
const found = [];
|
|
814
|
+
walk(local, (node) => {
|
|
815
|
+
if (node.type !== "JSXOpeningElement") return;
|
|
816
|
+
const tag = child2(node, "name");
|
|
817
|
+
const binding = bindingOfTag(tag, facts.locals);
|
|
818
|
+
if (isPart(binding, "EmptyState")) {
|
|
819
|
+
if (isPageScope(node)) found.push({ file: facts.file, line: node.loc?.start.line ?? 0 });
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const component = binding?.props === "all" ? void 0 : nameOf2(tag);
|
|
823
|
+
if (component && /^[A-Z]/.test(component)) found.push(...pageStatesOf(facts, component, parser, depth + 1, seen));
|
|
824
|
+
});
|
|
825
|
+
return found;
|
|
821
826
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
827
|
+
var fileName = (file) => file.replace(/\\/g, "/").split("/").pop();
|
|
828
|
+
var noHandmadeEmptyState = {
|
|
829
|
+
meta: {
|
|
830
|
+
type: "problem",
|
|
831
|
+
docs: { description: "An empty state drawn by hand, or a page-scope EmptyState that cannot centre" },
|
|
832
|
+
schema: [],
|
|
833
|
+
messages: {
|
|
834
|
+
handmade: 'A line saying there is nothing is an EmptyState. Use `EmptyState` from @estiva-app/ui: `scope="section"` inside a section, `scope="page"` for a whole page.',
|
|
835
|
+
wrongScope: 'A page-scope EmptyState inside a ScrollArea sits at the top of its pane, not the middle: the ScrollArea\'s content is only as tall as what is in it. Use `scope="section"` when the pane has anything else on it; when the whole pane is empty, draw the page EmptyState in place of the ScrollArea, not inside it.',
|
|
836
|
+
wrongScopeVia: '`{{component}}` draws a page-scope EmptyState ({{where}}) inside this ScrollArea, where it sits at the top of its pane, not the middle: the ScrollArea\'s content is only as tall as what is in it. Use `scope="section"` there when the pane has anything else on it; when the whole pane is empty, draw the page EmptyState in place of the ScrollArea, not inside it.',
|
|
837
|
+
...ESCAPE_MESSAGES
|
|
832
838
|
}
|
|
833
|
-
|
|
839
|
+
},
|
|
840
|
+
create(context) {
|
|
841
|
+
const parser = context.languageOptions?.parser ?? {};
|
|
842
|
+
const file = context.filename;
|
|
843
|
+
let facts;
|
|
844
|
+
const reported = /* @__PURE__ */ new Set();
|
|
845
|
+
const scopeOf = (node) => context.sourceCode.getScope(node);
|
|
846
|
+
return {
|
|
847
|
+
Program(program) {
|
|
848
|
+
facts = factsOf(file, program, parser);
|
|
849
|
+
},
|
|
850
|
+
JSXElement(ruleNode) {
|
|
851
|
+
const element = ruleNode;
|
|
852
|
+
const opening = child2(element, "openingElement");
|
|
853
|
+
const tag = tagName(element);
|
|
854
|
+
if (tag && /^[a-z]/.test(tag)) {
|
|
855
|
+
const words = onlyWords(element);
|
|
856
|
+
if (!words || words.length > SHORT) return;
|
|
857
|
+
if (!standsInForAList(element)) return;
|
|
858
|
+
if (isEscaped(context, opening)) return;
|
|
859
|
+
context.report({ loc: opening.loc, messageId: "handmade" });
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (!facts || !isPart(bindingOfTag(child2(opening, "name"), facts.locals), "ScrollArea")) return;
|
|
863
|
+
const content = attributeOf(opening, "contentClassName");
|
|
864
|
+
if (content && classesOf(child2(content, "value"), scopeOf).some((c) => FILLS.test(c.replace(/^(?:[^:\s[\]]+:)+/, "")))) return;
|
|
865
|
+
const here = facts;
|
|
866
|
+
for (const kid of children2(element, "children")) {
|
|
867
|
+
walk(kid, (node) => {
|
|
868
|
+
if (node.type !== "JSXOpeningElement") return;
|
|
869
|
+
const name = child2(node, "name");
|
|
870
|
+
const binding = bindingOfTag(name, here.locals);
|
|
871
|
+
if (isPart(binding, "EmptyState")) {
|
|
872
|
+
if (!isPageScope(node) || reported.has(node) || isEscaped(context, node)) return;
|
|
873
|
+
reported.add(node);
|
|
874
|
+
context.report({ loc: node.loc, messageId: "wrongScope" });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const component = binding?.props === "all" ? void 0 : nameOf2(name);
|
|
878
|
+
if (!component || !/^[A-Z]/.test(component)) return;
|
|
879
|
+
const states = pageStatesOf(here, component, parser, 0, /* @__PURE__ */ new Set());
|
|
880
|
+
if (!states.length || reported.has(node) || isEscaped(context, node)) return;
|
|
881
|
+
reported.add(node);
|
|
882
|
+
for (const state of [...states].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)) {
|
|
883
|
+
const where = state.file === file ? `line ${state.line}` : `${fileName(state.file)}:${state.line}`;
|
|
884
|
+
context.report({ loc: node.loc, messageId: "wrongScopeVia", data: { component, where } });
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
};
|
|
834
890
|
}
|
|
835
|
-
|
|
836
|
-
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// src/eslint/no-handmade-header.ts
|
|
894
|
+
var isElement = (n) => typeof n === "object" && n !== null && n.type === "JSXElement";
|
|
895
|
+
function tagOf(el) {
|
|
896
|
+
const n = el.openingElement.name;
|
|
897
|
+
if (n.type === "JSXIdentifier") return n.name ?? "";
|
|
898
|
+
if (n.type === "JSXMemberExpression") return `${n.object?.name ?? ""}.${n.property?.name ?? ""}`;
|
|
899
|
+
return "";
|
|
837
900
|
}
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (
|
|
901
|
+
function attribute(el, name) {
|
|
902
|
+
return el.openingElement.attributes.find((a) => a.type === "JSXAttribute" && a.name?.name === name);
|
|
903
|
+
}
|
|
904
|
+
function strings(node, out = []) {
|
|
905
|
+
if (!node || typeof node !== "object") return out;
|
|
906
|
+
if (node.type === "Literal" && typeof node.value === "string") out.push(node.value);
|
|
907
|
+
else if (node.type === "TemplateLiteral") for (const q of node.quasis) out.push(q.value.cooked ?? "");
|
|
908
|
+
for (const [key, value] of Object.entries(node)) {
|
|
909
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
910
|
+
if (Array.isArray(value)) for (const v of value) strings(v, out);
|
|
911
|
+
else if (value && typeof value === "object" && value.type) strings(value, out);
|
|
848
912
|
}
|
|
849
|
-
return
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
function classesOf2(el) {
|
|
916
|
+
const value = attribute(el, "className")?.value;
|
|
917
|
+
if (!value) return [];
|
|
918
|
+
const text = value.type === "Literal" ? String(value.value) : strings(value.expression).join(" ");
|
|
919
|
+
return text.split(/\s+/).filter(Boolean).map((c) => c.replace(/^(?:[^:\s[\]]+:)+/, ""));
|
|
920
|
+
}
|
|
921
|
+
function meaningful(el) {
|
|
922
|
+
const out = [];
|
|
923
|
+
for (const child3 of el.children) {
|
|
924
|
+
if (child3.type === "JSXText") {
|
|
925
|
+
if (String(child3.value).trim()) out.push(child3);
|
|
926
|
+
} else if (child3.type === "JSXExpressionContainer") {
|
|
927
|
+
const e = child3.expression;
|
|
928
|
+
if (e.type === "JSXEmptyExpression") continue;
|
|
929
|
+
if (e.type === "LogicalExpression" && isElement(e.right)) out.push(e.right);
|
|
930
|
+
else if (isElement(e)) out.push(e);
|
|
931
|
+
else out.push(child3);
|
|
932
|
+
} else if (child3.type === "JSXElement" || child3.type === "JSXFragment") out.push(child3);
|
|
933
|
+
}
|
|
934
|
+
return out;
|
|
935
|
+
}
|
|
936
|
+
var TEXT_PARTS = /* @__PURE__ */ new Set(["EditableText", "Person", "Breadcrumb", "Link", "SectionLabel", "InlineChip"]);
|
|
937
|
+
var VALUE_EXPRESSIONS = /* @__PURE__ */ new Set(["Identifier", "MemberExpression", "TemplateLiteral", "Literal", "LogicalExpression", "ConditionalExpression", "ChainExpression", "CallExpression"]);
|
|
938
|
+
function carriesTitle(el, depth = 0) {
|
|
939
|
+
if (depth > 3) return false;
|
|
940
|
+
for (const child3 of meaningful(el)) {
|
|
941
|
+
if (child3.type === "JSXText") return true;
|
|
942
|
+
if (child3.type === "JSXExpressionContainer") {
|
|
943
|
+
if (VALUE_EXPRESSIONS.has(child3.expression.type)) return true;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (!isElement(child3)) continue;
|
|
947
|
+
const tag = tagOf(child3);
|
|
948
|
+
if (/^h[1-6]$/.test(tag) || TEXT_PARTS.has(tag) || /\.(?:Title|Heading)$/.test(tag)) return true;
|
|
949
|
+
if (/^[a-z]/.test(tag) && carriesTitle(child3, depth + 1)) return true;
|
|
950
|
+
}
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
var ROW = (c) => c.includes("flex") && !c.includes("flex-col");
|
|
954
|
+
var OWN_SIDE_PADDING = (c) => c.some((x) => /^-?(?:p|px|pl|pr|ps|pe)-/.test(x));
|
|
955
|
+
var FILLS2 = (c) => c.includes("flex-col") && c.some((x) => /^(?:h-full|h-screen|flex-1|grow|min-h-0|h-dvh)$/.test(x));
|
|
956
|
+
var BAR = (c) => c.some((x) => /^(?:h|min-h)-(?:10|11|12|13|14|\[(?:4[0-9]|5[0-6])px\])$/.test(x)) || c.some((x) => /^border-b(?:-|$)/.test(x));
|
|
957
|
+
var INTERACTIVE = ["onClick", "href", "role", "onKeyDown", "onPointerDown"];
|
|
958
|
+
var noHandmadeHeader = {
|
|
959
|
+
meta: {
|
|
960
|
+
type: "problem",
|
|
961
|
+
docs: { description: "A column's header bar drawn by hand where ContainerHeader is the part" },
|
|
962
|
+
schema: [],
|
|
963
|
+
messages: {
|
|
964
|
+
handmade: "A column's header bar drawn by hand. Use `ContainerHeader` from @estiva-app/ui: 48px, the title, the column's buttons at the right edge (`actions`), a hairline under it.",
|
|
965
|
+
...ESCAPE_MESSAGES
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
create(context) {
|
|
969
|
+
return {
|
|
970
|
+
JSXElement(node) {
|
|
971
|
+
const column = node;
|
|
972
|
+
const kids = meaningful(column);
|
|
973
|
+
const first = kids[0];
|
|
974
|
+
if (kids.length < 2 || !isElement(first) || !/^[a-z]/.test(tagOf(first))) return;
|
|
975
|
+
const own = classesOf2(first);
|
|
976
|
+
if (!ROW(own) || !OWN_SIDE_PADDING(own)) return;
|
|
977
|
+
if (INTERACTIVE.some((name) => attribute(first, name))) return;
|
|
978
|
+
if (!FILLS2(classesOf2(column)) && !BAR(own)) return;
|
|
979
|
+
if (!carriesTitle(first)) return;
|
|
980
|
+
if (isEscaped(context, first.openingElement)) return;
|
|
981
|
+
context.report({ loc: first.openingElement.loc, messageId: "handmade" });
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// src/eslint/no-raw-element.ts
|
|
988
|
+
var RAW_ELEMENT_PARTS = {
|
|
989
|
+
a: { use: "Link", more: " For a chip, `InlineChip`; for a whole card, `Card` with `href`." },
|
|
990
|
+
button: { use: "Button" },
|
|
991
|
+
textarea: { use: "Textarea" },
|
|
992
|
+
select: { use: "Select" },
|
|
993
|
+
dialog: { use: "DialogShell", more: " To ask yes or no, `ConfirmDialog`." },
|
|
994
|
+
label: { use: "Field", more: " For words beside a tick box, `Checkbox` with `label`." },
|
|
995
|
+
details: { use: "CollapsibleSection" },
|
|
996
|
+
progress: { use: "ProgressBar" },
|
|
997
|
+
form: { use: "Form" },
|
|
998
|
+
meter: null,
|
|
999
|
+
iframe: null,
|
|
1000
|
+
embed: null,
|
|
1001
|
+
object: null,
|
|
1002
|
+
audio: null,
|
|
1003
|
+
video: null,
|
|
1004
|
+
img: null
|
|
1005
|
+
};
|
|
1006
|
+
var RAW_INPUT_PARTS = {
|
|
1007
|
+
search: { use: "SearchInput" },
|
|
1008
|
+
checkbox: { use: "Checkbox" },
|
|
1009
|
+
file: { use: "FilePicker" },
|
|
1010
|
+
submit: { use: "Button" },
|
|
1011
|
+
button: { use: "Button" },
|
|
1012
|
+
reset: { use: "Button" },
|
|
1013
|
+
image: { use: "Button" },
|
|
1014
|
+
radio: null,
|
|
1015
|
+
range: null,
|
|
1016
|
+
color: null,
|
|
1017
|
+
date: null,
|
|
1018
|
+
"datetime-local": null,
|
|
1019
|
+
month: null,
|
|
1020
|
+
week: null,
|
|
1021
|
+
time: null
|
|
1022
|
+
};
|
|
1023
|
+
var TEXT_INPUT = { use: "TextInput" };
|
|
1024
|
+
var ANY_INPUT = {
|
|
1025
|
+
use: "TextInput",
|
|
1026
|
+
more: " For a search, `SearchInput`; for a tick box, `Checkbox`; to pick files, `FilePicker`."
|
|
1027
|
+
};
|
|
1028
|
+
function attribute2(element, name) {
|
|
1029
|
+
return element.attributes.find((a) => a.type === "JSXAttribute" && a.name.name === name);
|
|
1030
|
+
}
|
|
1031
|
+
function literal(attr) {
|
|
1032
|
+
const value = attr.value;
|
|
1033
|
+
if (value === null) return true;
|
|
1034
|
+
if (value.type === "Literal") return typeof value.value === "string" ? value.value : void 0;
|
|
1035
|
+
if (value.type !== "JSXExpressionContainer" || !value.expression) return void 0;
|
|
1036
|
+
const expression = value.expression;
|
|
1037
|
+
if (expression.type === "Literal" && (typeof expression.value === "string" || typeof expression.value === "boolean")) return expression.value;
|
|
1038
|
+
if (expression.type === "TemplateLiteral" && expression.expressions?.length === 0) return expression.quasis?.[0]?.value.cooked ?? void 0;
|
|
1039
|
+
return void 0;
|
|
1040
|
+
}
|
|
1041
|
+
function isOn(element, name) {
|
|
1042
|
+
const attr = attribute2(element, name);
|
|
1043
|
+
return attr !== void 0 && literal(attr) !== false;
|
|
850
1044
|
}
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const cached = packagePartsCache.get(index);
|
|
865
|
-
if (cached && cached.mtime === mtime) return cached.parts;
|
|
866
|
-
const parts = /* @__PURE__ */ new Set();
|
|
867
|
-
const ast = parseFile(parser, index);
|
|
868
|
-
for (const statement2 of ast ? children(ast, "body") : []) {
|
|
869
|
-
if (statement2?.type !== "ExportNamedDeclaration" || statement2.exportKind === "type") continue;
|
|
870
|
-
for (const specifier of children(statement2, "specifiers")) {
|
|
871
|
-
const exported = nameOf(child(specifier, "exported"));
|
|
872
|
-
if (specifier?.exportKind !== "type" && exported && /^[A-Z][a-z]/.test(exported)) parts.add(exported);
|
|
873
|
-
}
|
|
1045
|
+
function classify(element) {
|
|
1046
|
+
if (element.name.type !== "JSXIdentifier" || !element.name.name) return null;
|
|
1047
|
+
const name = element.name.name;
|
|
1048
|
+
if (name === "input") {
|
|
1049
|
+
const attr = attribute2(element, "type");
|
|
1050
|
+
const type = attr ? literal(attr) : "text";
|
|
1051
|
+
if (typeof type !== "string") return { report: "raw", element: "<input>", part: ANY_INPUT };
|
|
1052
|
+
const key = type.toLowerCase();
|
|
1053
|
+
if (key === "hidden") return null;
|
|
1054
|
+
if (!(key in RAW_INPUT_PARTS)) return { report: "raw", element: "<input>", part: TEXT_INPUT };
|
|
1055
|
+
const part2 = RAW_INPUT_PARTS[key];
|
|
1056
|
+
const shown = `<input type="${key}">`;
|
|
1057
|
+
return part2 ? { report: "raw", element: shown, part: part2 } : { report: "noPart", element: shown };
|
|
874
1058
|
}
|
|
875
|
-
|
|
876
|
-
return
|
|
1059
|
+
if (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) return null;
|
|
1060
|
+
if ((name === "audio" || name === "video") && !isOn(element, "controls")) return null;
|
|
1061
|
+
if (name === "img" && !attribute2(element, "usemap") && !attribute2(element, "useMap")) return null;
|
|
1062
|
+
const part = RAW_ELEMENT_PARTS[name];
|
|
1063
|
+
return part ? { report: "raw", element: `<${name}>`, part } : { report: "noPart", element: `<${name}>` };
|
|
877
1064
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
return current?.type === "ArrowFunctionExpression" || current?.type === "FunctionExpression" ? current : null;
|
|
889
|
-
};
|
|
890
|
-
walk(program, (node) => {
|
|
891
|
-
if (node.type === "FunctionDeclaration") {
|
|
892
|
-
const name = nameOf(child(node, "id"));
|
|
893
|
-
if (name && /^[A-Z]/.test(name)) found.set(name, node);
|
|
894
|
-
} else if (node.type === "VariableDeclarator") {
|
|
895
|
-
const name = nameOf(child(node, "id"));
|
|
896
|
-
const fn = unwrap(child(node, "init"));
|
|
897
|
-
if (name && /^[A-Z]/.test(name) && fn) found.set(name, fn);
|
|
1065
|
+
var noRawElement = {
|
|
1066
|
+
meta: {
|
|
1067
|
+
type: "problem",
|
|
1068
|
+
docs: { description: "A raw interactive element where @estiva-app/ui has a part, or where it needs one" },
|
|
1069
|
+
schema: [],
|
|
1070
|
+
messages: {
|
|
1071
|
+
raw: "Use `{{use}}` from @estiva-app/ui instead of a raw {{element}}.{{more}}",
|
|
1072
|
+
noPart: "@estiva-app/ui has no part for a raw {{element}} yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.",
|
|
1073
|
+
...ESCAPE_MESSAGES
|
|
898
1074
|
}
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (packageParts) return "package";
|
|
911
|
-
return moduleExports(target, parser, stack);
|
|
912
|
-
};
|
|
913
|
-
for (const statement2 of body) {
|
|
914
|
-
if (statement2?.type !== "ImportDeclaration" || statement2.importKind === "type") continue;
|
|
915
|
-
const source = exportsOf(String(child(statement2, "source").value));
|
|
916
|
-
if (!source) continue;
|
|
917
|
-
for (const specifier of children(statement2, "specifiers")) {
|
|
918
|
-
if (!specifier || specifier.importKind === "type") continue;
|
|
919
|
-
const local = nameOf(child(specifier, "local"));
|
|
920
|
-
if (!local) continue;
|
|
921
|
-
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
922
|
-
if (source === "package" && !packageParts) locals.set(local, "namespace");
|
|
923
|
-
continue;
|
|
1075
|
+
},
|
|
1076
|
+
create(context) {
|
|
1077
|
+
return {
|
|
1078
|
+
// ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.
|
|
1079
|
+
JSXOpeningElement(node) {
|
|
1080
|
+
const element = node;
|
|
1081
|
+
const found = classify(element);
|
|
1082
|
+
if (!found) return;
|
|
1083
|
+
if (isEscaped(context, element)) return;
|
|
1084
|
+
if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
|
|
1085
|
+
else context.report({ loc: element.loc, messageId: "raw", data: { element: found.element, use: found.part.use, more: found.part.more ?? "" } });
|
|
924
1086
|
}
|
|
925
|
-
|
|
926
|
-
if (!imported) continue;
|
|
927
|
-
if (source === "package") {
|
|
928
|
-
const isPart = packageParts ? packageParts.has(imported) : /^[A-Z]/.test(imported);
|
|
929
|
-
if (isPart) locals.set(local, { part: imported, props: "all" });
|
|
930
|
-
} else if (source.has(imported)) locals.set(local, source.get(imported));
|
|
931
|
-
}
|
|
1087
|
+
};
|
|
932
1088
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
// src/eslint/no-rebuilt-behaviour.ts
|
|
1092
|
+
var BASE_UI_PARTS = {
|
|
1093
|
+
"alert-dialog": { use: "DialogShell", more: " To ask yes or no, `ConfirmDialog`." },
|
|
1094
|
+
autocomplete: { use: "CommandPalette" },
|
|
1095
|
+
avatar: { use: "Avatar" },
|
|
1096
|
+
button: { use: "Button", more: " Icon only, `IconButton`." },
|
|
1097
|
+
checkbox: { use: "Checkbox" },
|
|
1098
|
+
collapsible: { use: "CollapsibleSection" },
|
|
1099
|
+
combobox: { use: "ChipInput" },
|
|
1100
|
+
dialog: { use: "DialogShell", more: " To search and act, `CommandPalette`." },
|
|
1101
|
+
field: { use: "Field" },
|
|
1102
|
+
fieldset: { use: "Form" },
|
|
1103
|
+
form: { use: "Form" },
|
|
1104
|
+
input: { use: "TextInput", more: " For a search, `SearchInput`." },
|
|
1105
|
+
menu: { use: "Menu" },
|
|
1106
|
+
popover: { use: "Popover" },
|
|
1107
|
+
"preview-card": { use: "PreviewCard" },
|
|
1108
|
+
progress: { use: "ProgressBar" },
|
|
1109
|
+
"scroll-area": { use: "ScrollArea" },
|
|
1110
|
+
select: { use: "Select" },
|
|
1111
|
+
separator: { use: "Divider" },
|
|
1112
|
+
tabs: { use: "Tabs" },
|
|
1113
|
+
toast: { use: "Toast" },
|
|
1114
|
+
toggle: { use: "Reaction" },
|
|
1115
|
+
toolbar: { use: "Toolbar" },
|
|
1116
|
+
tooltip: { use: "Tooltip", more: " Around a control, `WithTooltip`." },
|
|
1117
|
+
accordion: null,
|
|
1118
|
+
"checkbox-group": null,
|
|
1119
|
+
"context-menu": null,
|
|
1120
|
+
drawer: null,
|
|
1121
|
+
menubar: null,
|
|
1122
|
+
meter: null,
|
|
1123
|
+
"navigation-menu": null,
|
|
1124
|
+
"number-field": null,
|
|
1125
|
+
"otp-field": null,
|
|
1126
|
+
radio: null,
|
|
1127
|
+
"radio-group": null,
|
|
1128
|
+
slider: null,
|
|
1129
|
+
switch: null,
|
|
1130
|
+
"toggle-group": null
|
|
1131
|
+
};
|
|
1132
|
+
var LIST_PART = {
|
|
1133
|
+
use: "Select",
|
|
1134
|
+
more: " To pick several, `ChipInput`; to search and act, `CommandPalette`; to tick several in a list, `Checkbox` with `row`."
|
|
1135
|
+
};
|
|
1136
|
+
var MESSAGE_PART = { use: "FieldLine", more: " For a notice, `Banner`; for a message that comes and goes, `Toast`." };
|
|
1137
|
+
var ROLE_PARTS = {
|
|
1138
|
+
dialog: { thing: "dialog", part: { use: "DialogShell", more: " To ask yes or no, `ConfirmDialog`." } },
|
|
1139
|
+
alertdialog: { thing: "dialog", part: { use: "ConfirmDialog" } },
|
|
1140
|
+
menu: { thing: "menu", part: { use: "Menu" } },
|
|
1141
|
+
menuitem: { thing: "menu", part: { use: "Menu" } },
|
|
1142
|
+
menuitemcheckbox: { thing: "menu", part: { use: "Menu" } },
|
|
1143
|
+
menuitemradio: { thing: "menu", part: { use: "Menu" } },
|
|
1144
|
+
listbox: { thing: "list", part: LIST_PART },
|
|
1145
|
+
option: { thing: "list", part: LIST_PART },
|
|
1146
|
+
combobox: { thing: "list", part: LIST_PART },
|
|
1147
|
+
tablist: { thing: "set of tabs", part: { use: "Tabs" } },
|
|
1148
|
+
tab: { thing: "set of tabs", part: { use: "Tabs" } },
|
|
1149
|
+
tabpanel: { thing: "set of tabs", part: { use: "Tabs" } },
|
|
1150
|
+
toolbar: { thing: "toolbar", part: { use: "Toolbar" } },
|
|
1151
|
+
progressbar: { thing: "progress bar", part: { use: "ProgressBar" } },
|
|
1152
|
+
checkbox: { thing: "tick box", part: { use: "Checkbox" } },
|
|
1153
|
+
separator: { thing: "divider", part: { use: "Divider" } },
|
|
1154
|
+
button: { thing: "button", part: { use: "Button", more: " Icon only, `IconButton`." } },
|
|
1155
|
+
link: { thing: "link", part: { use: "Link" } },
|
|
1156
|
+
tooltip: { thing: "tooltip", part: { use: "Tooltip", more: " Around a control, `WithTooltip`." } },
|
|
1157
|
+
alert: { thing: "message", part: MESSAGE_PART },
|
|
1158
|
+
status: { thing: "message", part: MESSAGE_PART },
|
|
1159
|
+
menubar: { thing: "menu bar", part: null },
|
|
1160
|
+
switch: { thing: "switch", part: null },
|
|
1161
|
+
radio: { thing: "radio button", part: null },
|
|
1162
|
+
radiogroup: { thing: "radio group", part: null },
|
|
1163
|
+
slider: { thing: "slider", part: null },
|
|
1164
|
+
spinbutton: { thing: "number field", part: null },
|
|
1165
|
+
meter: { thing: "meter", part: null }
|
|
1166
|
+
};
|
|
1167
|
+
var WALKING_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"];
|
|
1168
|
+
var PAGE_EVENTS = {
|
|
1169
|
+
mousedown: "press",
|
|
1170
|
+
pointerdown: "press",
|
|
1171
|
+
click: "press",
|
|
1172
|
+
touchstart: "press",
|
|
1173
|
+
keydown: "key",
|
|
1174
|
+
keyup: "key",
|
|
1175
|
+
keypress: "key",
|
|
1176
|
+
focusin: "focus",
|
|
1177
|
+
focusout: "focus",
|
|
1178
|
+
scroll: "follow",
|
|
1179
|
+
resize: "follow"
|
|
1180
|
+
};
|
|
1181
|
+
var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set(["a", "button", "input", "select", "textarea", "summary", "iframe", "embed", "object", "audio", "video"]);
|
|
1182
|
+
var SCROLL_CLASS = /^overflow(?:-[xy])?-(?:auto|scroll)$/;
|
|
1183
|
+
var uses = (parts) => [
|
|
1184
|
+
...new Set(parts.flatMap((p) => p ? [p.use, ...[...(p.more ?? "").matchAll(/`([A-Z]\w+)`/g)].map((m) => m[1])] : []))
|
|
1185
|
+
];
|
|
1186
|
+
var OWNED_BEHAVIOURS = [
|
|
1187
|
+
{
|
|
1188
|
+
id: "base-ui",
|
|
1189
|
+
behaviour: "Is built on Base UI",
|
|
1190
|
+
baseUi: Object.keys(BASE_UI_PARTS),
|
|
1191
|
+
owners: uses(Object.values(BASE_UI_PARTS)),
|
|
1192
|
+
reads: "an import from @base-ui/react (and the old @base-ui-components spelling)"
|
|
1193
|
+
},
|
|
1194
|
+
{
|
|
1195
|
+
id: "portal",
|
|
1196
|
+
behaviour: "Floats on top of the page",
|
|
1197
|
+
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Autocomplete", "Tooltip", "PreviewCard", "Toast"],
|
|
1198
|
+
owners: ["DialogShell", "ConfirmDialog", "CommandPalette", "Lightbox", "Popover", "Menu", "Select", "ChipInput", "Tooltip", "PreviewCard", "Toast"],
|
|
1199
|
+
reads: "createPortal, called, or imported and never called"
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
id: "press-outside",
|
|
1203
|
+
behaviour: "Closes on a press outside",
|
|
1204
|
+
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard"],
|
|
1205
|
+
owners: ["DialogShell", "Popover", "Menu", "Select", "PreviewCard"],
|
|
1206
|
+
reads: "a mousedown, pointerdown, click or touchstart listener on window or document"
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
id: "page-keys",
|
|
1210
|
+
behaviour: "Takes its keys by itself",
|
|
1211
|
+
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard", "Toast"],
|
|
1212
|
+
owners: ["DialogShell", "Lightbox", "Popover", "Menu", "Select", "Tabs", "Toolbar"],
|
|
1213
|
+
reads: "a keydown, keyup or keypress listener on window or document"
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
id: "focus",
|
|
1217
|
+
behaviour: "Holds focus inside while open, and gives it back",
|
|
1218
|
+
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox"],
|
|
1219
|
+
owners: ["DialogShell", "CommandPalette", "Lightbox"],
|
|
1220
|
+
reads: "a focusin or focusout listener on window or document; the Tab key compared by hand"
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
id: "scroll-lock",
|
|
1224
|
+
behaviour: "Stops the page behind it scrolling",
|
|
1225
|
+
baseUi: ["Dialog", "AlertDialog"],
|
|
1226
|
+
owners: ["DialogShell", "Lightbox"],
|
|
1227
|
+
reads: "overflow written into the style of document.body or document.documentElement"
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
id: "follow",
|
|
1231
|
+
behaviour: "Stays attached to its anchor on scroll and resize",
|
|
1232
|
+
baseUi: ["Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard", "Toast"],
|
|
1233
|
+
owners: ["Popover", "Menu", "Select", "Tooltip", "PreviewCard"],
|
|
1234
|
+
reads: "a scroll or resize listener on window or document"
|
|
1235
|
+
},
|
|
1236
|
+
{
|
|
1237
|
+
id: "walking",
|
|
1238
|
+
behaviour: "Moves through its items with the arrow keys",
|
|
1239
|
+
baseUi: ["Menu", "Select", "Combobox", "Autocomplete", "Tabs", "Toolbar"],
|
|
1240
|
+
owners: ["Menu", "Select", "ChipInput", "CommandPalette", "Tabs", "Toolbar"],
|
|
1241
|
+
reads: `a key compared by hand with ${WALKING_KEYS.join(", ")}`
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
id: "role",
|
|
1245
|
+
behaviour: "Says what it is to assistive technology",
|
|
1246
|
+
baseUi: ["Dialog", "AlertDialog", "Menu", "Select", "Combobox", "Tabs", "Toolbar", "Progress", "Checkbox", "Separator", "Button"],
|
|
1247
|
+
owners: uses(Object.values(ROLE_PARTS).map((r) => r.part)),
|
|
1248
|
+
reads: `role written by hand: ${Object.keys(ROLE_PARTS).join(", ")}`
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: "tab-stop",
|
|
1252
|
+
behaviour: "Is reachable with Tab",
|
|
1253
|
+
baseUi: ["Button", "Toggle", "Checkbox", "Tabs", "Toolbar", "ScrollArea"],
|
|
1254
|
+
owners: ["Button", "IconButton", "Link"],
|
|
1255
|
+
reads: "tabIndex that can be 0 or more, on an element that is not a control"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
id: "scroll",
|
|
1259
|
+
behaviour: "Scrolls with our scrollbar",
|
|
1260
|
+
baseUi: ["ScrollArea"],
|
|
1261
|
+
owners: ["ScrollArea"],
|
|
1262
|
+
reads: "overflow-auto or overflow-scroll (x or y, behind any variant) in a string, or overflow auto or scroll in a style"
|
|
936
1263
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const binding = handsOnTo(fn, (tag) => bindingOfTag(tag, locals));
|
|
943
|
-
if (!binding) continue;
|
|
944
|
-
const next = { ...binding, via: name };
|
|
945
|
-
const previous = locals.get(name);
|
|
946
|
-
if (!previous || JSON.stringify(previous) !== JSON.stringify(next)) {
|
|
947
|
-
if (previous && previous !== "namespace" && !previous.via) continue;
|
|
948
|
-
locals.set(name, next);
|
|
949
|
-
changed = true;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
1264
|
+
];
|
|
1265
|
+
var PAGE_TARGETS = /* @__PURE__ */ new Set(["window", "document", "globalThis", "self", "document.body", "document.documentElement", "window.document"]);
|
|
1266
|
+
function anchorOf(node) {
|
|
1267
|
+
if (node.type === "FunctionDeclaration") {
|
|
1268
|
+
return node.parent && /^Export/.test(node.parent.type) ? node.parent : node;
|
|
953
1269
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
for (const statement2 of body) {
|
|
960
|
-
if (!statement2) continue;
|
|
961
|
-
if (statement2.type === "ExportNamedDeclaration") {
|
|
962
|
-
const declaration = child(statement2, "declaration");
|
|
963
|
-
if (declaration?.type === "FunctionDeclaration") {
|
|
964
|
-
const name = nameOf(child(declaration, "id"));
|
|
965
|
-
const binding = localBinding(name);
|
|
966
|
-
if (name && binding) exports.set(name, binding);
|
|
967
|
-
} else if (declaration?.type === "VariableDeclaration") {
|
|
968
|
-
for (const declarator of children(declaration, "declarations")) {
|
|
969
|
-
const name = nameOf(child(declarator, "id"));
|
|
970
|
-
const init = child(declarator, "init");
|
|
971
|
-
const binding = localBinding(name) ?? (init?.type === "Identifier" ? localBinding(nameOf(init)) : void 0);
|
|
972
|
-
if (name && binding) exports.set(name, binding);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
const source = child(statement2, "source");
|
|
976
|
-
const from = source ? exportsOf(String(source.value)) : null;
|
|
977
|
-
for (const specifier of children(statement2, "specifiers")) {
|
|
978
|
-
if (!specifier || specifier.exportKind === "type") continue;
|
|
979
|
-
const local = nameOf(child(specifier, "local"));
|
|
980
|
-
const exported = nameOf(child(specifier, "exported"));
|
|
981
|
-
if (!local || !exported) continue;
|
|
982
|
-
if (!source) {
|
|
983
|
-
const binding = localBinding(local);
|
|
984
|
-
if (binding) exports.set(exported, binding);
|
|
985
|
-
} else if (from === "package") {
|
|
986
|
-
if (/^[A-Z]/.test(local)) exports.set(exported, { part: local, props: "all" });
|
|
987
|
-
} else if (from?.has(local)) exports.set(exported, from.get(local));
|
|
988
|
-
}
|
|
989
|
-
} else if (statement2.type === "ExportAllDeclaration" && !child(statement2, "exported")) {
|
|
990
|
-
const from = exportsOf(String(child(statement2, "source").value));
|
|
991
|
-
if (from && from !== "package") for (const [name, binding] of from) exports.set(name, binding);
|
|
992
|
-
} else if (statement2.type === "ExportDefaultDeclaration") {
|
|
993
|
-
const declaration = child(statement2, "declaration");
|
|
994
|
-
const name = declaration?.type === "Identifier" ? nameOf(declaration) : nameOf(child(declaration, "id"));
|
|
995
|
-
const binding = localBinding(name);
|
|
996
|
-
if (binding) exports.set("default", binding);
|
|
997
|
-
}
|
|
1270
|
+
let child3 = node;
|
|
1271
|
+
for (let p = node.parent; p; child3 = p, p = p.parent) {
|
|
1272
|
+
if (p.type === "JSXOpeningElement" || p.type === "Property" || p.type === "MethodDefinition" || p.type === "PropertyDefinition") return p;
|
|
1273
|
+
if (p.type === "ArrayExpression") return child3;
|
|
1274
|
+
if (p.type !== "BlockStatement" && /(Statement|Declaration)$/.test(p.type)) return p;
|
|
998
1275
|
}
|
|
999
|
-
return
|
|
1000
|
-
}
|
|
1001
|
-
function moduleExports(file, parser, stack) {
|
|
1002
|
-
const mtime = statSync(file).mtimeMs;
|
|
1003
|
-
const cached = moduleCache.get(file);
|
|
1004
|
-
if (cached && cached.mtime === mtime) return cached.exports;
|
|
1005
|
-
if (stack.has(file)) return /* @__PURE__ */ new Map();
|
|
1006
|
-
stack.add(file);
|
|
1007
|
-
const program = parseFile(parser, file);
|
|
1008
|
-
const exports = program ? moduleParts(file, program, parser, stack).exports : /* @__PURE__ */ new Map();
|
|
1009
|
-
stack.delete(file);
|
|
1010
|
-
moduleCache.set(file, { mtime, exports });
|
|
1011
|
-
return exports;
|
|
1276
|
+
return node;
|
|
1012
1277
|
}
|
|
1013
|
-
function
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const binding = locals.get(tag.name);
|
|
1017
|
-
return binding && binding !== "namespace" ? binding : void 0;
|
|
1018
|
-
}
|
|
1019
|
-
if (tag.type === "JSXMemberExpression") {
|
|
1020
|
-
const object = child(tag, "object");
|
|
1021
|
-
const property = nameOf(child(tag, "property"));
|
|
1022
|
-
if (object?.type === "JSXIdentifier" && locals.get(object.name) === "namespace" && property) return { part: property, props: "all" };
|
|
1278
|
+
function enclosingFunction(node) {
|
|
1279
|
+
for (let p = node.parent; p; p = p.parent) {
|
|
1280
|
+
if (p.type === "FunctionDeclaration" || p.type === "FunctionExpression" || p.type === "ArrowFunctionExpression") return p;
|
|
1023
1281
|
}
|
|
1024
1282
|
return void 0;
|
|
1025
1283
|
}
|
|
1026
|
-
function
|
|
1027
|
-
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
if (param.type === "ObjectPattern") {
|
|
1033
|
-
for (const property of children(param, "properties")) {
|
|
1034
|
-
if (!property) continue;
|
|
1035
|
-
if (property.type === "RestElement") restName = nameOf(child(property, "argument"));
|
|
1036
|
-
else if (property.type === "Property") {
|
|
1037
|
-
const key = nameOf(child(property, "key"));
|
|
1038
|
-
let value = child(property, "value");
|
|
1039
|
-
if (value?.type === "AssignmentPattern") value = child(value, "left");
|
|
1040
|
-
const local = nameOf(value);
|
|
1041
|
-
if (key && local && CLASS_PROP.test(key)) classParams.set(local, key);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
} else if (param.type === "Identifier") propsName = nameOf(param);
|
|
1045
|
-
let wrapper;
|
|
1046
|
-
const forwarded = { props: {} };
|
|
1047
|
-
const body = child(fn, "body");
|
|
1048
|
-
if (!body) return void 0;
|
|
1049
|
-
walk(body, (node) => {
|
|
1050
|
-
if (wrapper || node.type !== "JSXOpeningElement") return;
|
|
1051
|
-
const binding = bindingOf(child(node, "name"));
|
|
1052
|
-
if (!binding) return;
|
|
1053
|
-
for (const attribute2 of children(node, "attributes")) {
|
|
1054
|
-
if (!attribute2) continue;
|
|
1055
|
-
if (attribute2.type === "JSXSpreadAttribute") {
|
|
1056
|
-
const argument = nameOf(child(attribute2, "argument"));
|
|
1057
|
-
if (argument && (argument === restName || argument === propsName)) wrapper = { part: binding.part, props: binding.props };
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
const prop = nameOf(child(attribute2, "name"));
|
|
1061
|
-
if (!prop || !CLASS_PROP.test(prop)) continue;
|
|
1062
|
-
const partProp = binding.props === "all" ? prop : binding.props[prop];
|
|
1063
|
-
if (!partProp) continue;
|
|
1064
|
-
walk(attribute2, (inner) => {
|
|
1065
|
-
let callerProp;
|
|
1066
|
-
if (inner.type === "Identifier") callerProp = classParams.get(inner.name);
|
|
1067
|
-
if (inner.type === "MemberExpression" && nameOf(child(inner, "object")) === propsName) {
|
|
1068
|
-
const key = nameOf(child(inner, "property"));
|
|
1069
|
-
if (key && CLASS_PROP.test(key)) callerProp = key;
|
|
1070
|
-
}
|
|
1071
|
-
if (!callerProp) return;
|
|
1072
|
-
if (forwarded.part && forwarded.part !== binding.part) return;
|
|
1073
|
-
forwarded.part = binding.part;
|
|
1074
|
-
forwarded.props[callerProp] = partProp;
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
});
|
|
1078
|
-
if (wrapper) return wrapper;
|
|
1079
|
-
return forwarded.part ? { part: forwarded.part, props: forwarded.props } : void 0;
|
|
1080
|
-
}
|
|
1081
|
-
var CLASS_FUNCTIONS = /* @__PURE__ */ new Set(["cn", "clsx", "cx", "twMerge", "classNames"]);
|
|
1082
|
-
function constantOf(identifier, scope) {
|
|
1083
|
-
let current = scope;
|
|
1084
|
-
while (current) {
|
|
1085
|
-
const variable = current.set.get(identifier.name);
|
|
1086
|
-
if (variable) {
|
|
1087
|
-
const definition = variable.defs[0];
|
|
1088
|
-
if (variable.defs.length !== 1 || definition?.type !== "Variable") return void 0;
|
|
1089
|
-
if (definition.parent?.kind !== "const") return void 0;
|
|
1090
|
-
return child(definition.node, "init") ?? void 0;
|
|
1091
|
-
}
|
|
1092
|
-
current = current.upper;
|
|
1093
|
-
}
|
|
1284
|
+
function stringOf(node) {
|
|
1285
|
+
if (!node) return void 0;
|
|
1286
|
+
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
1287
|
+
const quasis = node.quasis;
|
|
1288
|
+
const expressions = node.expressions;
|
|
1289
|
+
if (node.type === "TemplateLiteral" && expressions?.length === 0) return quasis?.[0]?.value.cooked ?? void 0;
|
|
1094
1290
|
return void 0;
|
|
1095
1291
|
}
|
|
1096
|
-
function
|
|
1097
|
-
if (!
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
case "ConditionalExpression":
|
|
1113
|
-
return [...read(child(value, "consequent")), ...read(child(value, "alternate"))];
|
|
1114
|
-
case "LogicalExpression":
|
|
1115
|
-
return value.operator === "&&" ? read(child(value, "right")) : [...read(child(value, "left")), ...read(child(value, "right"))];
|
|
1116
|
-
case "BinaryExpression":
|
|
1117
|
-
return value.operator === "+" ? [...read(child(value, "left")), ...read(child(value, "right"))] : [];
|
|
1118
|
-
case "ArrayExpression":
|
|
1119
|
-
return children(value, "elements").flatMap((e) => read(e));
|
|
1120
|
-
case "ObjectExpression":
|
|
1121
|
-
return children(value, "properties").flatMap((property) => {
|
|
1122
|
-
if (property?.type !== "Property") return [];
|
|
1123
|
-
return asMap ? read(child(property, "value"), true) : property.computed ? [] : words(nameOf(child(property, "key")));
|
|
1124
|
-
});
|
|
1125
|
-
case "CallExpression": {
|
|
1126
|
-
const callee = child(value, "callee");
|
|
1127
|
-
return callee?.type === "Identifier" && CLASS_FUNCTIONS.has(callee.name) ? children(value, "arguments").flatMap((a) => read(a)) : [];
|
|
1128
|
-
}
|
|
1129
|
-
case "Identifier": {
|
|
1130
|
-
const constant = constantOf(value, scopeOf(value));
|
|
1131
|
-
return constant ? read(constant, asMap) : [];
|
|
1132
|
-
}
|
|
1133
|
-
case "MemberExpression": {
|
|
1134
|
-
const object = child(value, "object");
|
|
1135
|
-
const target = object?.type === "Identifier" ? constantOf(object, scopeOf(object)) : object;
|
|
1136
|
-
const map = target?.type === "TSAsExpression" || target?.type === "TSSatisfiesExpression" ? child(target, "expression") : target;
|
|
1137
|
-
if (map?.type !== "ObjectExpression") return [];
|
|
1138
|
-
const property = child(value, "property");
|
|
1139
|
-
const key = value.computed ? property?.type === "Literal" ? String(property.value) : void 0 : nameOf(property);
|
|
1140
|
-
return children(map, "properties").flatMap((entry) => {
|
|
1141
|
-
if (entry?.type !== "Property") return [];
|
|
1142
|
-
if (key !== void 0 && nameOf(child(entry, "key")) !== key) return [];
|
|
1143
|
-
return read(child(entry, "value"), true);
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
default:
|
|
1147
|
-
return [];
|
|
1292
|
+
function stringsOf(node) {
|
|
1293
|
+
if (!node) return [];
|
|
1294
|
+
if (node.type === "JSXExpressionContainer") return stringsOf(node.expression);
|
|
1295
|
+
const s = stringOf(node);
|
|
1296
|
+
if (s !== void 0) return [s];
|
|
1297
|
+
if (node.type === "ConditionalExpression") return [...stringsOf(node.consequent), ...stringsOf(node.alternate)];
|
|
1298
|
+
if (node.type === "LogicalExpression") return [...stringsOf(node.left), ...stringsOf(node.right)];
|
|
1299
|
+
return [];
|
|
1300
|
+
}
|
|
1301
|
+
function canBeTabStop(node) {
|
|
1302
|
+
if (!node) return false;
|
|
1303
|
+
if (node.type === "JSXExpressionContainer") return canBeTabStop(node.expression);
|
|
1304
|
+
if (node.type === "Literal") {
|
|
1305
|
+
if (typeof node.value === "number") return node.value >= 0;
|
|
1306
|
+
if (typeof node.value === "string") return /^\s*\d+\s*$/.test(node.value);
|
|
1307
|
+
return false;
|
|
1148
1308
|
}
|
|
1309
|
+
if (node.type === "ConditionalExpression") return canBeTabStop(node.consequent) || canBeTabStop(node.alternate);
|
|
1310
|
+
if (node.type === "LogicalExpression") return canBeTabStop(node.left) || canBeTabStop(node.right);
|
|
1311
|
+
if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression") return canBeTabStop(node.expression);
|
|
1312
|
+
return false;
|
|
1149
1313
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1314
|
+
function utilityOf(token) {
|
|
1315
|
+
let depth = 0;
|
|
1316
|
+
let last = -1;
|
|
1317
|
+
for (let i = 0; i < token.length; i++) {
|
|
1318
|
+
const c = token[i];
|
|
1319
|
+
if (c === "[") depth++;
|
|
1320
|
+
else if (c === "]") depth--;
|
|
1321
|
+
else if (c === ":" && depth === 0) last = i;
|
|
1322
|
+
}
|
|
1323
|
+
return token.slice(last + 1).replace(/^!/, "").replace(/!$/, "");
|
|
1324
|
+
}
|
|
1325
|
+
function baseUiModule(source) {
|
|
1326
|
+
const m = /^@base-ui(?:-components)?\/([^/]+)(?:\/(.+))?$/.exec(source);
|
|
1327
|
+
if (!m) return void 0;
|
|
1328
|
+
const [, pkg, sub] = m;
|
|
1329
|
+
return pkg === "react" ? sub ?? "" : `${pkg}${sub ? `/${sub}` : ""}`;
|
|
1330
|
+
}
|
|
1331
|
+
function isKeySubject(node) {
|
|
1332
|
+
if (!node) return false;
|
|
1333
|
+
if (node.type === "Identifier") return node.name === "key" || node.name === "code";
|
|
1334
|
+
if (node.type !== "MemberExpression" || node.computed) return false;
|
|
1335
|
+
const property = node.property;
|
|
1336
|
+
return property.type === "Identifier" && (property.name === "key" || property.name === "code");
|
|
1337
|
+
}
|
|
1338
|
+
function pageTarget(node, text) {
|
|
1339
|
+
return (node.type === "Identifier" || node.type === "MemberExpression") && PAGE_TARGETS.has(text);
|
|
1340
|
+
}
|
|
1341
|
+
var noRebuiltBehaviour = {
|
|
1153
1342
|
meta: {
|
|
1154
1343
|
type: "problem",
|
|
1155
|
-
docs: { description: "
|
|
1344
|
+
docs: { description: "Behaviour @estiva-app/ui already owns, rebuilt by hand in an app" },
|
|
1156
1345
|
schema: [],
|
|
1157
1346
|
messages: {
|
|
1158
|
-
|
|
1159
|
-
|
|
1347
|
+
baseUi: "Only @estiva-app/ui imports Base UI (`{{module}}`). Use `{{use}}` from @estiva-app/ui.{{more}}",
|
|
1348
|
+
baseUiNoPart: "Only @estiva-app/ui imports Base UI (`{{module}}`), and it has no part built on it yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.",
|
|
1349
|
+
portal: "A layer put on top of the page by hand (`createPortal`). Every floating part of @estiva-app/ui carries its own: `DialogShell` for a dialog, `Popover` for a panel, `Menu`, `Select`, `Tooltip`, `PreviewCard`, `Toast`.",
|
|
1350
|
+
press: "A `{{event}}` listener on `{{target}}`: closing on a press outside, by hand. `Popover`, `Menu`, `Select`, `DialogShell` and `PreviewCard` from @estiva-app/ui close themselves.",
|
|
1351
|
+
key: "A `{{event}}` listener on `{{target}}`: keys taken for the whole page, by hand. `DialogShell`, `Popover`, `Menu` and `Select` from @estiva-app/ui close on Escape themselves; `Menu`, `Select`, `Tabs` and `Toolbar` move with the arrow keys.",
|
|
1352
|
+
focus: "A `{{event}}` listener on `{{target}}`: focus held by hand. `DialogShell` and `CommandPalette` from @estiva-app/ui keep focus inside and give it back.",
|
|
1353
|
+
follow: "A `{{event}}` listener on `{{target}}`: following an anchor by hand. `Popover`, `Menu`, `Select`, `Tooltip` and `PreviewCard` from @estiva-app/ui stay attached to theirs.",
|
|
1354
|
+
scrollLock: "The page's scroll locked by hand. `DialogShell` from @estiva-app/ui stops the page scrolling while it is open, and gives it back.",
|
|
1355
|
+
walking: "Arrow keys handled by hand (`{{key}}`). `Menu`, `Select`, `ChipInput`, `CommandPalette`, `Tabs` and `Toolbar` from @estiva-app/ui move through their items themselves.",
|
|
1356
|
+
tabKey: "The Tab key handled by hand. `DialogShell` and `CommandPalette` from @estiva-app/ui keep focus inside themselves.",
|
|
1357
|
+
role: 'A hand-written `role="{{role}}"` is a hand-made {{thing}}. Use `{{use}}` from @estiva-app/ui.{{more}}',
|
|
1358
|
+
roleNoPart: 'A hand-written `role="{{role}}"` is a hand-made {{thing}}, and @estiva-app/ui has no part for one yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.',
|
|
1359
|
+
tabStop: "`tabIndex={{value}}` makes a `<{{element}}>` a Tab stop by hand. Use `Button`, `IconButton` or `Link` from @estiva-app/ui, which are reachable already.",
|
|
1360
|
+
scrollClass: "`{{token}}` scrolls with the browser's scrollbar. Use `ScrollArea` from @estiva-app/ui, which draws ours.",
|
|
1361
|
+
scrollStyle: "`{{style}}` scrolls with the browser's scrollbar. Use `ScrollArea` from @estiva-app/ui, which draws ours.",
|
|
1160
1362
|
...ESCAPE_MESSAGES
|
|
1161
1363
|
}
|
|
1162
1364
|
},
|
|
1163
1365
|
create(context) {
|
|
1164
|
-
const
|
|
1165
|
-
const
|
|
1166
|
-
const sourceCode = context.sourceCode;
|
|
1167
|
-
const scopeOf = (node) => sourceCode.getScope(node);
|
|
1168
|
-
let locals;
|
|
1366
|
+
const { sourceCode } = context;
|
|
1367
|
+
const text = (node) => sourceCode.getText(node);
|
|
1169
1368
|
const escapes = /* @__PURE__ */ new Map();
|
|
1170
1369
|
const escaped = (anchor) => {
|
|
1171
1370
|
if (!escapes.has(anchor)) escapes.set(anchor, isEscaped(context, anchor));
|
|
1172
1371
|
return escapes.get(anchor) === true;
|
|
1173
1372
|
};
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1373
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1374
|
+
const report = (anchor, at, messageId, data = {}, once = `${at.range[0]}`) => {
|
|
1375
|
+
const key = `${anchor.range[0]}|${messageId}|${once}`;
|
|
1376
|
+
if (seen.has(key)) return;
|
|
1377
|
+
seen.add(key);
|
|
1378
|
+
if (escaped(anchor)) return;
|
|
1379
|
+
context.report({ loc: at.loc, messageId, data });
|
|
1380
|
+
};
|
|
1381
|
+
const importSource = (node, source) => {
|
|
1382
|
+
const value = stringOf(source);
|
|
1383
|
+
if (value === void 0) return;
|
|
1384
|
+
const module = baseUiModule(value);
|
|
1385
|
+
if (module === void 0) return;
|
|
1386
|
+
const part = Object.hasOwn(BASE_UI_PARTS, module) ? BASE_UI_PARTS[module] : null;
|
|
1387
|
+
const anchor = anchorOf(node);
|
|
1388
|
+
if (part) report(anchor, node, "baseUi", { module: value, use: part.use, more: part.more ?? "" });
|
|
1389
|
+
else report(anchor, node, "baseUiNoPart", { module: value });
|
|
1390
|
+
};
|
|
1391
|
+
const keyCompared = (node, key) => {
|
|
1392
|
+
if (key === void 0) return;
|
|
1393
|
+
const fn = enclosingFunction(node);
|
|
1394
|
+
const anchor = anchorOf(fn ?? node);
|
|
1395
|
+
if (WALKING_KEYS.includes(key)) report(anchor, node, "walking", { key }, `walking@${fn?.range[0] ?? node.range[0]}`);
|
|
1396
|
+
else if (key === "Tab") report(anchor, node, "tabKey", {}, `tab@${fn?.range[0] ?? node.range[0]}`);
|
|
1397
|
+
};
|
|
1398
|
+
const classTokens = (node, value) => {
|
|
1399
|
+
for (const token of value.split(/\s+/)) {
|
|
1400
|
+
if (token && SCROLL_CLASS.test(utilityOf(token))) report(anchorOf(node), node, "scrollClass", { token }, `class@${token}`);
|
|
1189
1401
|
}
|
|
1190
|
-
if (padding.length) context.report({ loc: at.loc, messageId: "emptyStatePadding", data: { classes: code(padding), where } });
|
|
1191
1402
|
};
|
|
1403
|
+
const portalImports = /* @__PURE__ */ new Map();
|
|
1404
|
+
let portalCalled = false;
|
|
1192
1405
|
return {
|
|
1193
|
-
|
|
1194
|
-
|
|
1406
|
+
ImportDeclaration(ruleNode) {
|
|
1407
|
+
const node = ruleNode;
|
|
1408
|
+
importSource(node, node.source);
|
|
1409
|
+
if (stringOf(node.source) !== "react-dom") return;
|
|
1410
|
+
for (const specifier of node.specifiers) {
|
|
1411
|
+
const imported = specifier.imported;
|
|
1412
|
+
if (specifier.type === "ImportSpecifier" && imported?.name === "createPortal") portalImports.set(specifier.local.name, node);
|
|
1413
|
+
}
|
|
1195
1414
|
},
|
|
1196
|
-
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1415
|
+
ExportNamedDeclaration(ruleNode) {
|
|
1416
|
+
const node = ruleNode;
|
|
1417
|
+
if (node.source) importSource(node, node.source);
|
|
1418
|
+
},
|
|
1419
|
+
ExportAllDeclaration(ruleNode) {
|
|
1420
|
+
const node = ruleNode;
|
|
1421
|
+
importSource(node, node.source);
|
|
1422
|
+
},
|
|
1423
|
+
ImportExpression(ruleNode) {
|
|
1424
|
+
const node = ruleNode;
|
|
1425
|
+
importSource(node, node.source);
|
|
1426
|
+
},
|
|
1427
|
+
CallExpression(ruleNode) {
|
|
1428
|
+
const node = ruleNode;
|
|
1429
|
+
const callee = node.callee;
|
|
1430
|
+
const args = node.arguments;
|
|
1431
|
+
if (callee.type === "Identifier" && callee.name === "require") importSource(node, args[0]);
|
|
1432
|
+
if (callee.type === "Identifier" && portalImports.has(callee.name)) {
|
|
1433
|
+
portalCalled = true;
|
|
1434
|
+
report(anchorOf(node), node, "portal");
|
|
1435
|
+
}
|
|
1436
|
+
if (callee.type !== "MemberExpression") return;
|
|
1437
|
+
const object = callee.object;
|
|
1438
|
+
const property = callee.property;
|
|
1439
|
+
if (property.type !== "Identifier") return;
|
|
1440
|
+
if (property.name === "createPortal") {
|
|
1441
|
+
report(anchorOf(node), node, "portal");
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (property.name === "addEventListener") {
|
|
1445
|
+
const target = text(object);
|
|
1446
|
+
if (!pageTarget(object, target)) return;
|
|
1447
|
+
const event = stringOf(args[0]);
|
|
1448
|
+
const group = event === void 0 ? void 0 : PAGE_EVENTS[event];
|
|
1449
|
+
if (!event || !group) return;
|
|
1450
|
+
report(anchorOf(node), node, group, { event, target });
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (property.name === "includes" && object.type === "ArrayExpression" && isKeySubject(args[0])) {
|
|
1454
|
+
for (const element of object.elements) keyCompared(node, stringOf(element));
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
AssignmentExpression(ruleNode) {
|
|
1458
|
+
const node = ruleNode;
|
|
1459
|
+
const left = node.left;
|
|
1460
|
+
if (left.type !== "MemberExpression") return;
|
|
1461
|
+
const property = left.property;
|
|
1462
|
+
const object = left.object;
|
|
1463
|
+
const handler = property.type === "Identifier" ? /^on([a-z]+)$/.exec(property.name) : null;
|
|
1464
|
+
if (handler && pageTarget(object, text(object)) && PAGE_EVENTS[handler[1]]) {
|
|
1465
|
+
report(anchorOf(node), node, PAGE_EVENTS[handler[1]], { event: handler[1], target: text(object) });
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (/^document\.(?:body|documentElement)\.style\.overflow[XY]?$/.test(text(left))) report(anchorOf(node), node, "scrollLock");
|
|
1469
|
+
},
|
|
1470
|
+
BinaryExpression(ruleNode) {
|
|
1471
|
+
const node = ruleNode;
|
|
1472
|
+
if (!["===", "==", "!==", "!="].includes(node.operator)) return;
|
|
1473
|
+
const left = node.left;
|
|
1474
|
+
const right = node.right;
|
|
1475
|
+
if (isKeySubject(left)) keyCompared(node, stringOf(right));
|
|
1476
|
+
else if (isKeySubject(right)) keyCompared(node, stringOf(left));
|
|
1477
|
+
},
|
|
1478
|
+
SwitchCase(ruleNode) {
|
|
1479
|
+
const node = ruleNode;
|
|
1480
|
+
const statement2 = node.parent;
|
|
1481
|
+
if (node.test && isKeySubject(statement2.discriminant)) keyCompared(node, stringOf(node.test));
|
|
1482
|
+
},
|
|
1483
|
+
JSXAttribute(ruleNode) {
|
|
1484
|
+
const node = ruleNode;
|
|
1485
|
+
const nameNode = node.name;
|
|
1486
|
+
if (nameNode.type !== "JSXIdentifier") return;
|
|
1487
|
+
const name = nameNode.name;
|
|
1488
|
+
const element = node.parent;
|
|
1489
|
+
const value = node.value;
|
|
1490
|
+
if (name === "role") {
|
|
1491
|
+
for (const role of stringsOf(value).flatMap((v) => v.trim().split(/\s+/))) {
|
|
1492
|
+
if (!Object.hasOwn(ROLE_PARTS, role)) continue;
|
|
1493
|
+
const { thing, part } = ROLE_PARTS[role];
|
|
1494
|
+
if (part) report(element, node, "role", { role, thing, use: part.use, more: part.more ?? "" });
|
|
1495
|
+
else report(element, node, "roleNoPart", { role, thing });
|
|
1496
|
+
return;
|
|
1206
1497
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (name === "tabIndex" || name === "tabindex") {
|
|
1501
|
+
const tag = element.name;
|
|
1502
|
+
if (tag.type !== "JSXIdentifier" || !/^[a-z]/.test(tag.name) || INTERACTIVE_ELEMENTS.has(tag.name)) return;
|
|
1503
|
+
const attributes = element.attributes;
|
|
1504
|
+
const editable = attributes.some((a) => a.type === "JSXAttribute" && /^contenteditable$/i.test(a.name.name ?? ""));
|
|
1505
|
+
if (editable || !canBeTabStop(value)) return;
|
|
1506
|
+
report(element, node, "tabStop", { value: value ? text(value).replace(/^\{|\}$/g, "") : "", element: tag.name });
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
if (name === "style" && value?.type === "JSXExpressionContainer") {
|
|
1510
|
+
const object = value.expression;
|
|
1511
|
+
if (object.type !== "ObjectExpression") return;
|
|
1512
|
+
for (const property of object.properties) {
|
|
1513
|
+
if (property.type !== "Property") continue;
|
|
1514
|
+
const key = property.key;
|
|
1515
|
+
const keyName = key.type === "Identifier" ? key.name : stringOf(key);
|
|
1516
|
+
if (!keyName || !/^overflow[XY]?$/.test(keyName)) continue;
|
|
1517
|
+
const setting = stringOf(property.value);
|
|
1518
|
+
if (setting === "auto" || setting === "scroll") report(element, property, "scrollStyle", { style: `${keyName}: '${setting}'` }, `style@${keyName}`);
|
|
1214
1519
|
}
|
|
1215
1520
|
}
|
|
1521
|
+
},
|
|
1522
|
+
Literal(ruleNode) {
|
|
1523
|
+
const node = ruleNode;
|
|
1524
|
+
if (typeof node.value !== "string") return;
|
|
1525
|
+
const parent = node.parent;
|
|
1526
|
+
if (parent && /^(Import|Export)/.test(parent.type)) return;
|
|
1527
|
+
if (parent?.type === "TSLiteralType" || parent?.type === "TSExternalModuleReference") return;
|
|
1528
|
+
classTokens(node, node.value);
|
|
1529
|
+
},
|
|
1530
|
+
TemplateElement(ruleNode) {
|
|
1531
|
+
const node = ruleNode;
|
|
1532
|
+
const cooked = node.value.cooked;
|
|
1533
|
+
if (cooked && node.parent) classTokens(node.parent, cooked);
|
|
1534
|
+
},
|
|
1535
|
+
"Program:exit"() {
|
|
1536
|
+
if (portalCalled) return;
|
|
1537
|
+
for (const declaration of new Set(portalImports.values())) report(anchorOf(declaration), declaration, "portal");
|
|
1216
1538
|
}
|
|
1217
1539
|
};
|
|
1218
1540
|
}
|
|
@@ -1260,14 +1582,18 @@ var PLUGIN_KEY = "estiva";
|
|
|
1260
1582
|
var appRules = {
|
|
1261
1583
|
"no-raw-element": noRawElement,
|
|
1262
1584
|
"no-rebuilt-behaviour": noRebuiltBehaviour,
|
|
1263
|
-
"no-restyled-part": noRestyledPart
|
|
1585
|
+
"no-restyled-part": noRestyledPart,
|
|
1586
|
+
"no-handmade-header": noHandmadeHeader,
|
|
1587
|
+
"no-handmade-empty-state": noHandmadeEmptyState
|
|
1264
1588
|
};
|
|
1265
1589
|
var packageRules = {
|
|
1266
1590
|
"raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
|
|
1267
1591
|
"no-hand-rolled-behaviour": noHandRolledBehaviour,
|
|
1268
1592
|
"component-has-a-page": componentHasAPage,
|
|
1269
1593
|
"component-has-a-story": componentHasAStory,
|
|
1270
|
-
"no-restyled-part": noRestyledPart
|
|
1594
|
+
"no-restyled-part": noRestyledPart,
|
|
1595
|
+
"no-handmade-header": noHandmadeHeader,
|
|
1596
|
+
"no-handmade-empty-state": noHandmadeEmptyState
|
|
1271
1597
|
};
|
|
1272
1598
|
var rules = { ...appRules, ...packageRules };
|
|
1273
1599
|
var plugin = {
|
|
@@ -1283,7 +1609,9 @@ plugin.configs.recommended = {
|
|
|
1283
1609
|
rules: {
|
|
1284
1610
|
[`${PLUGIN_KEY}/no-raw-element`]: "error",
|
|
1285
1611
|
[`${PLUGIN_KEY}/no-rebuilt-behaviour`]: "error",
|
|
1286
|
-
[`${PLUGIN_KEY}/no-restyled-part`]: "error"
|
|
1612
|
+
[`${PLUGIN_KEY}/no-restyled-part`]: "error",
|
|
1613
|
+
[`${PLUGIN_KEY}/no-handmade-header`]: "error",
|
|
1614
|
+
[`${PLUGIN_KEY}/no-handmade-empty-state`]: "error"
|
|
1287
1615
|
}
|
|
1288
1616
|
};
|
|
1289
1617
|
plugin.configs.strict = {
|