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