@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.
@@ -158,1155 +158,1422 @@ var noHandRolledBehaviour = {
158
158
  }
159
159
  };
160
160
 
161
- // src/eslint/no-handmade-header.ts
162
- var isElement = (n) => typeof n === "object" && n !== null && n.type === "JSXElement";
163
- function tagOf(el) {
164
- const n = el.openingElement.name;
165
- if (n.type === "JSXIdentifier") return n.name ?? "";
166
- if (n.type === "JSXMemberExpression") return `${n.object?.name ?? ""}.${n.property?.name ?? ""}`;
167
- return "";
168
- }
169
- function attribute(el, name) {
170
- return el.openingElement.attributes.find((a) => a.type === "JSXAttribute" && a.name?.name === name);
171
- }
172
- function strings(node, out = []) {
173
- if (!node || typeof node !== "object") return out;
174
- if (node.type === "Literal" && typeof node.value === "string") out.push(node.value);
175
- else if (node.type === "TemplateLiteral") for (const q of node.quasis) out.push(q.value.cooked ?? "");
176
- for (const [key, value] of Object.entries(node)) {
177
- if (key === "parent" || key === "loc" || key === "range") continue;
178
- if (Array.isArray(value)) for (const v of value) strings(v, out);
179
- else if (value && typeof value === "object" && value.type) strings(value, out);
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 out;
191
+ return { variants, utility: current.replace(/^!/, "").replace(/!$/, "").replace(/^-/, "") };
182
192
  }
183
- function classesOf(el) {
184
- const value = attribute(el, "className")?.value;
185
- if (!value) return [];
186
- const text = value.type === "Literal" ? String(value.value) : strings(value.expression).join(" ");
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
- function meaningful(el) {
190
- const out = [];
191
- for (const child2 of el.children) {
192
- if (child2.type === "JSXText") {
193
- if (String(child2.value).trim()) out.push(child2);
194
- } else if (child2.type === "JSXExpressionContainer") {
195
- const e = child2.expression;
196
- if (e.type === "JSXEmptyExpression") continue;
197
- if (e.type === "LogicalExpression" && isElement(e.right)) out.push(e.right);
198
- else if (isElement(e)) out.push(e);
199
- else out.push(child2);
200
- } else if (child2.type === "JSXElement" || child2.type === "JSXFragment") out.push(child2);
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 TEXT_PARTS = /* @__PURE__ */ new Set(["EditableText", "Person", "Breadcrumb", "Link", "SectionLabel", "InlineChip"]);
205
- var VALUE_EXPRESSIONS = /* @__PURE__ */ new Set(["Identifier", "MemberExpression", "TemplateLiteral", "Literal", "LogicalExpression", "ConditionalExpression", "ChainExpression", "CallExpression"]);
206
- function carriesTitle(el, depth = 0) {
207
- if (depth > 3) return false;
208
- for (const child2 of meaningful(el)) {
209
- if (child2.type === "JSXText") return true;
210
- if (child2.type === "JSXExpressionContainer") {
211
- if (VALUE_EXPRESSIONS.has(child2.expression.type)) return true;
212
- continue;
213
- }
214
- if (!isElement(child2)) continue;
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 false;
267
+ return null;
220
268
  }
221
- var ROW = (c) => c.includes("flex") && !c.includes("flex-col");
222
- var OWN_SIDE_PADDING = (c) => c.some((x) => /^-?(?:p|px|pl|pr|ps|pe)-/.test(x));
223
- var FILLS = (c) => c.includes("flex-col") && c.some((x) => /^(?:h-full|h-screen|flex-1|grow|min-h-0|h-dvh)$/.test(x));
224
- 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));
225
- var INTERACTIVE = ["onClick", "href", "role", "onKeyDown", "onPointerDown"];
226
- var noHandmadeHeader = {
227
- meta: {
228
- type: "problem",
229
- docs: { description: "A column's header bar drawn by hand where ContainerHeader is the part" },
230
- schema: [],
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 isOn(element, name) {
310
- const attr = attribute2(element, name);
311
- return attr !== void 0 && literal(attr) !== false;
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 classify(element) {
314
- if (element.name.type !== "JSXIdentifier" || !element.name.name) return null;
315
- const name = element.name.name;
316
- if (name === "input") {
317
- const attr = attribute2(element, "type");
318
- const type = attr ? literal(attr) : "text";
319
- if (typeof type !== "string") return { report: "raw", element: "<input>", part: ANY_INPUT };
320
- const key = type.toLowerCase();
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 (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) return null;
328
- if ((name === "audio" || name === "video") && !isOn(element, "controls")) return null;
329
- if (name === "img" && !attribute2(element, "usemap") && !attribute2(element, "useMap")) return null;
330
- const part = RAW_ELEMENT_PARTS[name];
331
- return part ? { report: "raw", element: `<${name}>`, part } : { report: "noPart", element: `<${name}>` };
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
- var noRawElement = {
334
- meta: {
335
- type: "problem",
336
- docs: { description: "A raw interactive element where @estiva-app/ui has a part, or where it needs one" },
337
- schema: [],
338
- messages: {
339
- raw: "Use `{{use}}` from @estiva-app/ui instead of a raw {{element}}.{{more}}",
340
- 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.",
341
- ...ESCAPE_MESSAGES
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
- create(context) {
345
- return {
346
- // ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.
347
- JSXOpeningElement(node) {
348
- const element = node;
349
- const found = classify(element);
350
- if (!found) return;
351
- if (isEscaped(context, element)) return;
352
- if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
353
- else context.report({ loc: element.loc, messageId: "raw", data: { element: found.element, use: found.part.use, more: found.part.more ?? "" } });
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
- // src/eslint/no-rebuilt-behaviour.ts
360
- var BASE_UI_PARTS = {
361
- "alert-dialog": { use: "DialogShell", more: " To ask yes or no, `ConfirmDialog`." },
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
- var PAGE_TARGETS = /* @__PURE__ */ new Set(["window", "document", "globalThis", "self", "document.body", "document.documentElement", "window.document"]);
534
- function anchorOf(node) {
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
- let child2 = node;
539
- for (let p = node.parent; p; child2 = p, p = p.parent) {
540
- if (p.type === "JSXOpeningElement" || p.type === "Property" || p.type === "MethodDefinition" || p.type === "PropertyDefinition") return p;
541
- if (p.type === "ArrayExpression") return child2;
542
- if (p.type !== "BlockStatement" && /(Statement|Declaration)$/.test(p.type)) return p;
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
- return node;
545
- }
546
- function enclosingFunction(node) {
547
- for (let p = node.parent; p; p = p.parent) {
548
- if (p.type === "FunctionDeclaration" || p.type === "FunctionExpression" || p.type === "ArrowFunctionExpression") return p;
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 void 0;
446
+ return { locals, exports };
551
447
  }
552
- function stringOf(node) {
553
- if (!node) return void 0;
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 stringsOf(node) {
561
- if (!node) return [];
562
- if (node.type === "JSXExpressionContainer") return stringsOf(node.expression);
563
- const s = stringOf(node);
564
- if (s !== void 0) return [s];
565
- if (node.type === "ConditionalExpression") return [...stringsOf(node.consequent), ...stringsOf(node.alternate)];
566
- if (node.type === "LogicalExpression") return [...stringsOf(node.left), ...stringsOf(node.right)];
567
- return [];
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 canBeTabStop(node) {
570
- if (!node) return false;
571
- if (node.type === "JSXExpressionContainer") return canBeTabStop(node.expression);
572
- if (node.type === "Literal") {
573
- if (typeof node.value === "number") return node.value >= 0;
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 (node.type === "ConditionalExpression") return canBeTabStop(node.consequent) || canBeTabStop(node.alternate);
578
- if (node.type === "LogicalExpression") return canBeTabStop(node.left) || canBeTabStop(node.right);
579
- if (node.type === "TSAsExpression" || node.type === "TSNonNullExpression") return canBeTabStop(node.expression);
580
- return false;
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 token.slice(last + 1).replace(/^!/, "").replace(/!$/, "");
474
+ return void 0;
592
475
  }
593
- function baseUiModule(source) {
594
- const m = /^@base-ui(?:-components)?\/([^/]+)(?:\/(.+))?$/.exec(source);
595
- if (!m) return void 0;
596
- const [, pkg, sub] = m;
597
- return pkg === "react" ? sub ?? "" : `${pkg}${sub ? `/${sub}` : ""}`;
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
- function isKeySubject(node) {
600
- if (!node) return false;
601
- if (node.type === "Identifier") return node.name === "key" || node.name === "code";
602
- if (node.type !== "MemberExpression" || node.computed) return false;
603
- const property = node.property;
604
- return property.type === "Identifier" && (property.name === "key" || property.name === "code");
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 pageTarget(node, text) {
607
- return (node.type === "Identifier" || node.type === "MemberExpression") && PAGE_TARGETS.has(text);
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 noRebuiltBehaviour = {
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: "Behaviour @estiva-app/ui already owns, rebuilt by hand in an app" },
605
+ docs: { description: "Only placement passes into a part of @estiva-app/ui through a class prop" },
613
606
  schema: [],
614
607
  messages: {
615
- baseUi: "Only @estiva-app/ui imports Base UI (`{{module}}`). Use `{{use}}` from @estiva-app/ui.{{more}}",
616
- 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.",
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 { sourceCode } = context;
635
- const text = (node) => sourceCode.getText(node);
636
- const escapes = /* @__PURE__ */ new Map();
637
- const escaped = (anchor) => {
638
- if (!escapes.has(anchor)) escapes.set(anchor, isEscaped(context, anchor));
639
- return escapes.get(anchor) === true;
640
- };
641
- const seen = /* @__PURE__ */ new Set();
642
- const report = (anchor, at, messageId, data = {}, once = `${at.range[0]}`) => {
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 classTokens = (node, value) => {
667
- for (const token of value.split(/\s+/)) {
668
- if (token && SCROLL_CLASS.test(utilityOf(token))) report(anchorOf(node), node, "scrollClass", { token }, `class@${token}`);
624
+ const check = (element, binding, prop, value, at) => {
625
+ const partProp = binding.props === "all" ? prop : binding.props[prop];
626
+ if (!partProp || !value) return;
627
+ const classes = classesOf(value, scopeOf);
628
+ const handedOn = binding.props === "all" ? "its props" : `its \`${prop}\``;
629
+ const via = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands ${handedOn} to \`${binding.part}\`)` : `\`${binding.part}\``;
630
+ const where = `${via}${partProp === "className" ? "" : `'s \`${partProp}\``}`;
631
+ const restyled = [...new Set(classes.filter((token) => !isPlacement(token)))];
632
+ const padding = binding.part === "EmptyState" && binding.props === "all" ? [...new Set(classes.filter(isPadding))] : [];
633
+ if (!restyled.length && !padding.length) return;
634
+ if (escaped(element)) return;
635
+ if (restyled.length) {
636
+ const props = PART_LOOK_PROPS[binding.part];
637
+ const use = props ? ` Use its ${or(props)}.` : ` \`${binding.part}\` has no prop for this yet: ask Katerina, and it gets made in @estiva-app/ui.`;
638
+ context.report({ loc: at.loc, messageId: "restyled", data: { classes: code(restyled), where, use } });
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
- ImportDeclaration(ruleNode) {
675
- const node = ruleNode;
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
- "Program:exit"() {
804
- if (portalCalled) return;
805
- for (const declaration of new Set(portalImports.values())) report(anchorOf(declaration), declaration, "portal");
806
- }
807
- };
808
- }
809
- };
810
-
811
- // src/eslint/no-restyled-part.ts
812
- import { existsSync as existsSync2, readFileSync, statSync } from "node:fs";
813
- import { dirname, join, resolve } from "node:path";
814
- var PLACEMENT = [
815
- { what: "space around", pattern: /^m[xytrblse]?-/ },
816
- { what: "space inside", pattern: /^p[xytrblse]?-/ },
817
- { what: "width and height", pattern: /^(?:w|h|size|min-w|min-h|max-w|max-h)-/ },
818
- { what: "shown, and how it lays out", pattern: /^(?:hidden|block|inline|inline-block|inline-flex|inline-grid|flex|grid|contents|flow-root)$/ },
819
- { 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)-/ },
820
- { what: "alignment", pattern: /^(?:self|justify|justify-items|justify-self|items|place-content|place-items|place-self)-|^content-(?!\[)/ },
821
- { what: "position", pattern: /^(?:static|relative|absolute|fixed|sticky)$|^(?:inset|inset-x|inset-y|top|right|bottom|left|start|end|z)-/ },
822
- // Draws nothing: it names the part, so what is inside can answer its hover (`group-hover:`).
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
- else if (value && typeof value.type === "string") stack.push(value);
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 PACKAGE = "@estiva-app/ui";
903
- var moduleCache = /* @__PURE__ */ new Map();
904
- var packageDirCache = /* @__PURE__ */ new Map();
905
- var packagePartsCache = /* @__PURE__ */ new Map();
906
- function parseFile(parser, filePath) {
907
- const code2 = readFileSync(filePath, "utf8");
908
- const options = { filePath, ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: filePath.endsWith("x") }, range: true, loc: true };
909
- try {
910
- if (parser.parseForESLint) return parser.parseForESLint(code2, options).ast;
911
- if (parser.parse) return parser.parse(code2, options);
912
- } catch {
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 null;
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
- function packageDirOf(file) {
917
- const start = dirname(resolve(file));
918
- if (packageDirCache.has(start)) return packageDirCache.get(start) ?? null;
919
- let dir = start;
920
- for (; ; ) {
921
- if (existsSync2(join(dir, "package.json"))) break;
922
- const up = dirname(dir);
923
- if (up === dir) {
924
- dir = "";
925
- break;
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
- packageDirCache.set(start, dir || null);
930
- return dir || null;
791
+ return { file, locals: partsOfFile(file, program, parser), components: functionComponents(program), imports };
931
792
  }
932
- function resolveModule(fromFile, specifier) {
933
- let base;
934
- if (specifier.startsWith(".")) base = resolve(dirname(fromFile), specifier);
935
- else if (specifier.startsWith("@/")) {
936
- const dir = packageDirOf(fromFile);
937
- if (!dir) return null;
938
- base = join(dir, "src", specifier.slice(2));
939
- } else return null;
940
- for (const candidate of [base, `${base}.tsx`, `${base}.ts`, join(base, "index.tsx"), join(base, "index.ts")]) {
941
- if (existsSync2(candidate) && statSync(candidate).isFile()) return candidate;
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
- return null;
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
- function packagePartsFor(file, parser) {
946
- const dir = packageDirOf(file);
947
- if (!dir) return null;
948
- let name;
949
- try {
950
- name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name;
951
- } catch {
952
- return null;
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
- if (name !== PACKAGE) return null;
955
- const index = join(dir, "src", "index.ts");
956
- if (!existsSync2(index)) return null;
957
- const mtime = statSync(index).mtimeMs;
958
- const cached = packagePartsCache.get(index);
959
- if (cached && cached.mtime === mtime) return cached.parts;
960
- const parts = /* @__PURE__ */ new Set();
961
- const ast = parseFile(parser, index);
962
- for (const statement2 of ast ? children(ast, "body") : []) {
963
- if (statement2?.type !== "ExportNamedDeclaration" || statement2.exportKind === "type") continue;
964
- for (const specifier of children(statement2, "specifiers")) {
965
- const exported = nameOf(child(specifier, "exported"));
966
- if (specifier?.exportKind !== "type" && exported && /^[A-Z][a-z]/.test(exported)) parts.add(exported);
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
- packagePartsCache.set(index, { mtime, parts });
970
- return parts;
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
- function functionComponents(program) {
973
- const found = /* @__PURE__ */ new Map();
974
- const unwrap = (node) => {
975
- let current = node;
976
- while (current?.type === "CallExpression" && children(current, "arguments").length) {
977
- const callee = child(current, "callee");
978
- const text = callee?.type === "Identifier" ? nameOf(callee) : callee?.type === "MemberExpression" ? nameOf(child(callee, "property")) : void 0;
979
- if (text !== "forwardRef" && text !== "memo") return null;
980
- current = children(current, "arguments")[0];
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
- return found;
995
- }
996
- function moduleParts(file, program, parser, stack) {
997
- const locals = /* @__PURE__ */ new Map();
998
- const packageParts = packagePartsFor(file, parser);
999
- const body = children(program, "body");
1000
- const exportsOf = (specifier) => {
1001
- if (specifier === PACKAGE) return "package";
1002
- const target = resolveModule(file, specifier);
1003
- if (!target) return null;
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
- const imported = specifier.type === "ImportDefaultSpecifier" ? "default" : nameOf(child(specifier, "imported"));
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
- const components = functionComponents(program);
1028
- if (packageParts) {
1029
- for (const name of components.keys()) if (packageParts.has(name) && !locals.has(name)) locals.set(name, { part: name, props: "all" });
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
- if (!packageParts) {
1032
- let changed = true;
1033
- for (let round = 0; changed && round < 10; round += 1) {
1034
- changed = false;
1035
- for (const [name, fn] of components) {
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
- const exports = /* @__PURE__ */ new Map();
1049
- const localBinding = (name) => {
1050
- const binding = name ? locals.get(name) : void 0;
1051
- return binding && binding !== "namespace" ? binding : void 0;
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 { locals, exports };
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 bindingOfTag(tag, locals) {
1108
- if (!tag) return void 0;
1109
- if (tag.type === "JSXIdentifier") {
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 handsOnTo(fn, bindingOf) {
1121
- const param = children(fn, "params")[0];
1122
- if (!param) return void 0;
1123
- let restName;
1124
- let propsName;
1125
- const classParams = /* @__PURE__ */ new Map();
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 classesOf2(value, scopeOf, asMap = false, seen = /* @__PURE__ */ new Set()) {
1191
- if (!value || seen.has(value) || seen.size > 200) return [];
1192
- seen.add(value);
1193
- const read = (node, map = false) => classesOf2(node, scopeOf, map, seen);
1194
- const words = (text) => typeof text === "string" ? text.split(/\s+/).filter(Boolean) : [];
1195
- switch (value.type) {
1196
- case "Literal":
1197
- return words(value.value);
1198
- case "TemplateLiteral":
1199
- return [...children(value, "quasis").flatMap((quasi) => words((quasi?.value).cooked)), ...children(value, "expressions").flatMap((e) => read(e))];
1200
- case "JSXExpressionContainer":
1201
- case "TSAsExpression":
1202
- case "TSSatisfiesExpression":
1203
- case "TSNonNullExpression":
1204
- case "ChainExpression":
1205
- return read(child(value, "expression"), asMap);
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
- var code = (items) => items.map((item) => `\`${item}\``).join(" ");
1245
- var or = (items) => items.length === 1 ? code(items) : `${items.slice(0, -1).map((i) => `\`${i}\``).join(", ")} or \`${items[items.length - 1]}\``;
1246
- var noRestyledPart = {
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: "Only placement passes into a part of @estiva-app/ui through a class prop" },
1383
+ docs: { description: "Behaviour @estiva-app/ui already owns, rebuilt by hand in an app" },
1250
1384
  schema: [],
1251
1385
  messages: {
1252
- 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.",
1253
- 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.",
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 parser = context.languageOptions?.parser ?? {};
1259
- const file = context.filename;
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 check = (element, binding, prop, value, at) => {
1269
- const partProp = binding.props === "all" ? prop : binding.props[prop];
1270
- if (!partProp || !value) return;
1271
- const classes = classesOf2(value, scopeOf);
1272
- const handedOn = binding.props === "all" ? "its props" : `its \`${prop}\``;
1273
- const via = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands ${handedOn} to \`${binding.part}\`)` : `\`${binding.part}\``;
1274
- const where = `${via}${partProp === "className" ? "" : `'s \`${partProp}\``}`;
1275
- const restyled = [...new Set(classes.filter((token) => !isPlacement(token)))];
1276
- const padding = binding.part === "EmptyState" && binding.props === "all" ? [...new Set(classes.filter(isPadding))] : [];
1277
- if (!restyled.length && !padding.length) return;
1278
- if (escaped(element)) return;
1279
- if (restyled.length) {
1280
- const props = PART_LOOK_PROPS[binding.part];
1281
- 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.`;
1282
- context.report({ loc: at.loc, messageId: "restyled", data: { classes: code(restyled), where, use } });
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
- Program(program) {
1288
- locals = moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
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
- JSXOpeningElement(ruleNode) {
1291
- const element = ruleNode;
1292
- const binding = locals && bindingOfTag(child(element, "name"), locals);
1293
- if (!binding) return;
1294
- for (const attribute3 of children(element, "attributes")) {
1295
- if (!attribute3) continue;
1296
- if (attribute3.type === "JSXAttribute") {
1297
- const prop = nameOf(child(attribute3, "name"));
1298
- if (prop && CLASS_PROP.test(prop)) check(element, binding, prop, child(attribute3, "value"), attribute3);
1299
- continue;
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
- let spread = child(attribute3, "argument");
1302
- if (spread?.type === "Identifier") spread = constantOf(spread, scopeOf(spread));
1303
- while (spread?.type === "TSAsExpression" || spread?.type === "TSSatisfiesExpression") spread = child(spread, "expression");
1304
- if (spread?.type !== "ObjectExpression") continue;
1305
- for (const property of children(spread, "properties")) {
1306
- const key = property?.type === "Property" && !property.computed ? nameOf(child(property, "key")) : void 0;
1307
- if (key && CLASS_PROP.test(key)) check(element, binding, key, child(property, "value"), property);
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,