@estiva-app/ui 0.30.0 → 0.32.0

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