@estiva-app/ui 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/Button.d.ts +6 -1
- package/dist/Button.d.ts.map +1 -1
- package/dist/Card.d.ts +6 -1
- package/dist/Card.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +16 -1
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/Divider.d.ts +7 -0
- package/dist/Divider.d.ts.map +1 -1
- package/dist/IconButton.d.ts +12 -2
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Link.d.ts +6 -1
- package/dist/Link.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Popover.d.ts.map +1 -1
- package/dist/ReactionPicker.d.ts.map +1 -1
- package/dist/ScrollArea.d.ts +6 -0
- package/dist/ScrollArea.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts +6 -1
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/eslint/index.d.ts +4 -0
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +968 -3
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/no-rebuilt-behaviour.d.ts +88 -0
- package/dist/eslint/no-rebuilt-behaviour.d.ts.map +1 -0
- package/dist/eslint/no-restyled-part.d.ts +81 -0
- package/dist/eslint/no-restyled-part.d.ts.map +1 -0
- package/dist/index.js +300 -268
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/AttachmentCard.tsx +7 -2
- package/src/Banner.tsx +2 -2
- package/src/Breadcrumb.tsx +9 -4
- package/src/Button.mdx +7 -0
- package/src/Button.stories.tsx +4 -1
- package/src/Button.tsx +10 -2
- package/src/Card.mdx +7 -0
- package/src/Card.stories.tsx +11 -0
- package/src/Card.tsx +8 -0
- package/src/Checkbox.mdx +20 -1
- package/src/Checkbox.stories.tsx +44 -0
- package/src/Checkbox.test.tsx +63 -0
- package/src/Checkbox.tsx +30 -1
- package/src/CommandPalette.tsx +1 -1
- package/src/ConfirmDialog.tsx +2 -2
- package/src/DialogShell.tsx +6 -7
- package/src/Divider.mdx +3 -0
- package/src/Divider.tsx +12 -2
- package/src/EditableText.mdx +5 -0
- package/src/IconButton.mdx +9 -0
- package/src/IconButton.stories.tsx +10 -1
- package/src/IconButton.tsx +22 -3
- package/src/IdentityMenu.tsx +4 -1
- package/src/Link.mdx +6 -0
- package/src/Link.stories.tsx +12 -0
- package/src/Link.tsx +7 -2
- package/src/Menu.stories.tsx +1 -1
- package/src/Menu.test.tsx +2 -2
- package/src/Menu.tsx +6 -3
- package/src/Person.stories.tsx +5 -5
- package/src/Popover.test.tsx +2 -2
- package/src/Popover.tsx +3 -2
- package/src/PreviewCard.tsx +1 -1
- package/src/Property.stories.tsx +2 -2
- package/src/ReactionPicker.tsx +6 -3
- package/src/ScrollArea.stories.tsx +38 -6
- package/src/ScrollArea.tsx +7 -1
- package/src/SectionLabel.mdx +3 -2
- package/src/SectionLabel.stories.tsx +3 -0
- package/src/SectionLabel.tsx +7 -2
- package/src/Tooltip.tsx +12 -14
- package/src/eslint/index.test.ts +58 -9
- package/src/eslint/index.ts +19 -3
- package/src/eslint/no-rebuilt-behaviour.test.ts +276 -0
- package/src/eslint/no-rebuilt-behaviour.ts +593 -0
- package/src/eslint/no-restyled-part.test.ts +301 -0
- package/src/eslint/no-restyled-part.ts +647 -0
- package/stories/Choosing.mdx +1 -1
package/dist/eslint/index.js
CHANGED
|
@@ -262,6 +262,961 @@ var noRawElement = {
|
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
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", "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: "Closes on Escape, and takes its keys, by itself",
|
|
385
|
+
baseUi: ["Dialog", "AlertDialog", "Popover", "Menu", "Select", "Combobox", "Tooltip", "PreviewCard", "Toast"],
|
|
386
|
+
owners: ["DialogShell", "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"],
|
|
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"],
|
|
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"
|
|
437
|
+
}
|
|
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;
|
|
443
|
+
}
|
|
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;
|
|
449
|
+
}
|
|
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;
|
|
455
|
+
}
|
|
456
|
+
return void 0;
|
|
457
|
+
}
|
|
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;
|
|
465
|
+
}
|
|
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 [];
|
|
474
|
+
}
|
|
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;
|
|
482
|
+
}
|
|
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;
|
|
496
|
+
}
|
|
497
|
+
return token.slice(last + 1).replace(/^!/, "").replace(/!$/, "");
|
|
498
|
+
}
|
|
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}` : ""}`;
|
|
504
|
+
}
|
|
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");
|
|
511
|
+
}
|
|
512
|
+
function pageTarget(node, text) {
|
|
513
|
+
return (node.type === "Identifier" || node.type === "MemberExpression") && PAGE_TARGETS.has(text);
|
|
514
|
+
}
|
|
515
|
+
var noRebuiltBehaviour = {
|
|
516
|
+
meta: {
|
|
517
|
+
type: "problem",
|
|
518
|
+
docs: { description: "Behaviour @estiva-app/ui already owns, rebuilt by hand in an app" },
|
|
519
|
+
schema: [],
|
|
520
|
+
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.",
|
|
536
|
+
...ESCAPE_MESSAGES
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
create(context) {
|
|
540
|
+
const { sourceCode } = context;
|
|
541
|
+
const text = (node) => sourceCode.getText(node);
|
|
542
|
+
const escapes = /* @__PURE__ */ new Map();
|
|
543
|
+
const escaped = (anchor) => {
|
|
544
|
+
if (!escapes.has(anchor)) escapes.set(anchor, isEscaped(context, anchor));
|
|
545
|
+
return escapes.get(anchor) === true;
|
|
546
|
+
};
|
|
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}`);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const portalImports = /* @__PURE__ */ new Map();
|
|
578
|
+
let portalCalled = false;
|
|
579
|
+
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));
|
|
656
|
+
},
|
|
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;
|
|
671
|
+
}
|
|
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}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
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
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
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;
|
|
743
|
+
}
|
|
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));
|
|
750
|
+
}
|
|
751
|
+
var isPadding = (token) => {
|
|
752
|
+
const { variants, utility } = splitClass(token);
|
|
753
|
+
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^p[xytrblse]?-/.test(utility);
|
|
754
|
+
};
|
|
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
|
+
SectionLabel: ["tone"],
|
|
780
|
+
Select: ["size"],
|
|
781
|
+
Tabs: ["size"],
|
|
782
|
+
TextInput: ["size"],
|
|
783
|
+
Toast: ["type"],
|
|
784
|
+
Toolbar: ["surface"],
|
|
785
|
+
ToolbarButton: ["variant", "pressed", "glow"],
|
|
786
|
+
TopBar: ["variant"]
|
|
787
|
+
};
|
|
788
|
+
var CLASS_PROP = /^(?:className|[a-z][A-Za-z]*ClassName)$/;
|
|
789
|
+
var child = (node, key) => node[key];
|
|
790
|
+
var children = (node, key) => node[key] ?? [];
|
|
791
|
+
var nameOf = (node) => node?.type === "Identifier" || node?.type === "JSXIdentifier" ? node.name : node?.type === "Literal" && typeof node.value === "string" ? node.value : void 0;
|
|
792
|
+
function walk(root, visit) {
|
|
793
|
+
const stack = [root];
|
|
794
|
+
while (stack.length) {
|
|
795
|
+
const node = stack.pop();
|
|
796
|
+
visit(node);
|
|
797
|
+
for (const key of Object.keys(node)) {
|
|
798
|
+
if (key === "parent" || key === "loc" || key === "range" || key === "tokens" || key === "comments") continue;
|
|
799
|
+
const value = node[key];
|
|
800
|
+
if (Array.isArray(value)) for (const item of value) {
|
|
801
|
+
if (item && typeof item.type === "string") stack.push(item);
|
|
802
|
+
}
|
|
803
|
+
else if (value && typeof value.type === "string") stack.push(value);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
var PACKAGE = "@estiva-app/ui";
|
|
808
|
+
var moduleCache = /* @__PURE__ */ new Map();
|
|
809
|
+
var packageDirCache = /* @__PURE__ */ new Map();
|
|
810
|
+
var packagePartsCache = /* @__PURE__ */ new Map();
|
|
811
|
+
function parseFile(parser, filePath) {
|
|
812
|
+
const code2 = readFileSync(filePath, "utf8");
|
|
813
|
+
const options = { filePath, ecmaVersion: "latest", sourceType: "module", ecmaFeatures: { jsx: filePath.endsWith("x") }, range: true, loc: true };
|
|
814
|
+
try {
|
|
815
|
+
if (parser.parseForESLint) return parser.parseForESLint(code2, options).ast;
|
|
816
|
+
if (parser.parse) return parser.parse(code2, options);
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
function packageDirOf(file) {
|
|
822
|
+
const start = dirname(resolve(file));
|
|
823
|
+
if (packageDirCache.has(start)) return packageDirCache.get(start) ?? null;
|
|
824
|
+
let dir = start;
|
|
825
|
+
for (; ; ) {
|
|
826
|
+
if (existsSync2(join(dir, "package.json"))) break;
|
|
827
|
+
const up = dirname(dir);
|
|
828
|
+
if (up === dir) {
|
|
829
|
+
dir = "";
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
dir = up;
|
|
833
|
+
}
|
|
834
|
+
packageDirCache.set(start, dir || null);
|
|
835
|
+
return dir || null;
|
|
836
|
+
}
|
|
837
|
+
function resolveModule(fromFile, specifier) {
|
|
838
|
+
let base;
|
|
839
|
+
if (specifier.startsWith(".")) base = resolve(dirname(fromFile), specifier);
|
|
840
|
+
else if (specifier.startsWith("@/")) {
|
|
841
|
+
const dir = packageDirOf(fromFile);
|
|
842
|
+
if (!dir) return null;
|
|
843
|
+
base = join(dir, "src", specifier.slice(2));
|
|
844
|
+
} else return null;
|
|
845
|
+
for (const candidate of [base, `${base}.tsx`, `${base}.ts`, join(base, "index.tsx"), join(base, "index.ts")]) {
|
|
846
|
+
if (existsSync2(candidate) && statSync(candidate).isFile()) return candidate;
|
|
847
|
+
}
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
function packagePartsFor(file, parser) {
|
|
851
|
+
const dir = packageDirOf(file);
|
|
852
|
+
if (!dir) return null;
|
|
853
|
+
let name;
|
|
854
|
+
try {
|
|
855
|
+
name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name;
|
|
856
|
+
} catch {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
if (name !== PACKAGE) return null;
|
|
860
|
+
const index = join(dir, "src", "index.ts");
|
|
861
|
+
if (!existsSync2(index)) return null;
|
|
862
|
+
const mtime = statSync(index).mtimeMs;
|
|
863
|
+
const cached = packagePartsCache.get(index);
|
|
864
|
+
if (cached && cached.mtime === mtime) return cached.parts;
|
|
865
|
+
const parts = /* @__PURE__ */ new Set();
|
|
866
|
+
const ast = parseFile(parser, index);
|
|
867
|
+
for (const statement2 of ast ? children(ast, "body") : []) {
|
|
868
|
+
if (statement2?.type !== "ExportNamedDeclaration" || statement2.exportKind === "type") continue;
|
|
869
|
+
for (const specifier of children(statement2, "specifiers")) {
|
|
870
|
+
const exported = nameOf(child(specifier, "exported"));
|
|
871
|
+
if (specifier?.exportKind !== "type" && exported && /^[A-Z][a-z]/.test(exported)) parts.add(exported);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
packagePartsCache.set(index, { mtime, parts });
|
|
875
|
+
return parts;
|
|
876
|
+
}
|
|
877
|
+
function functionComponents(program) {
|
|
878
|
+
const found = /* @__PURE__ */ new Map();
|
|
879
|
+
const unwrap = (node) => {
|
|
880
|
+
let current = node;
|
|
881
|
+
while (current?.type === "CallExpression" && children(current, "arguments").length) {
|
|
882
|
+
const callee = child(current, "callee");
|
|
883
|
+
const text = callee?.type === "Identifier" ? nameOf(callee) : callee?.type === "MemberExpression" ? nameOf(child(callee, "property")) : void 0;
|
|
884
|
+
if (text !== "forwardRef" && text !== "memo") return null;
|
|
885
|
+
current = children(current, "arguments")[0];
|
|
886
|
+
}
|
|
887
|
+
return current?.type === "ArrowFunctionExpression" || current?.type === "FunctionExpression" ? current : null;
|
|
888
|
+
};
|
|
889
|
+
walk(program, (node) => {
|
|
890
|
+
if (node.type === "FunctionDeclaration") {
|
|
891
|
+
const name = nameOf(child(node, "id"));
|
|
892
|
+
if (name && /^[A-Z]/.test(name)) found.set(name, node);
|
|
893
|
+
} else if (node.type === "VariableDeclarator") {
|
|
894
|
+
const name = nameOf(child(node, "id"));
|
|
895
|
+
const fn = unwrap(child(node, "init"));
|
|
896
|
+
if (name && /^[A-Z]/.test(name) && fn) found.set(name, fn);
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
return found;
|
|
900
|
+
}
|
|
901
|
+
function moduleParts(file, program, parser, stack) {
|
|
902
|
+
const locals = /* @__PURE__ */ new Map();
|
|
903
|
+
const packageParts = packagePartsFor(file, parser);
|
|
904
|
+
const body = children(program, "body");
|
|
905
|
+
const exportsOf = (specifier) => {
|
|
906
|
+
if (specifier === PACKAGE) return "package";
|
|
907
|
+
const target = resolveModule(file, specifier);
|
|
908
|
+
if (!target) return null;
|
|
909
|
+
if (packageParts) return "package";
|
|
910
|
+
return moduleExports(target, parser, stack);
|
|
911
|
+
};
|
|
912
|
+
for (const statement2 of body) {
|
|
913
|
+
if (statement2?.type !== "ImportDeclaration" || statement2.importKind === "type") continue;
|
|
914
|
+
const source = exportsOf(String(child(statement2, "source").value));
|
|
915
|
+
if (!source) continue;
|
|
916
|
+
for (const specifier of children(statement2, "specifiers")) {
|
|
917
|
+
if (!specifier || specifier.importKind === "type") continue;
|
|
918
|
+
const local = nameOf(child(specifier, "local"));
|
|
919
|
+
if (!local) continue;
|
|
920
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
921
|
+
if (source === "package" && !packageParts) locals.set(local, "namespace");
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const imported = specifier.type === "ImportDefaultSpecifier" ? "default" : nameOf(child(specifier, "imported"));
|
|
925
|
+
if (!imported) continue;
|
|
926
|
+
if (source === "package") {
|
|
927
|
+
const isPart = packageParts ? packageParts.has(imported) : /^[A-Z]/.test(imported);
|
|
928
|
+
if (isPart) locals.set(local, { part: imported, props: "all" });
|
|
929
|
+
} else if (source.has(imported)) locals.set(local, source.get(imported));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const components = functionComponents(program);
|
|
933
|
+
if (packageParts) {
|
|
934
|
+
for (const name of components.keys()) if (packageParts.has(name) && !locals.has(name)) locals.set(name, { part: name, props: "all" });
|
|
935
|
+
}
|
|
936
|
+
if (!packageParts) {
|
|
937
|
+
let changed = true;
|
|
938
|
+
for (let round = 0; changed && round < 10; round += 1) {
|
|
939
|
+
changed = false;
|
|
940
|
+
for (const [name, fn] of components) {
|
|
941
|
+
const binding = handsOnTo(fn, (tag) => bindingOfTag(tag, locals));
|
|
942
|
+
if (!binding) continue;
|
|
943
|
+
const next = { ...binding, via: name };
|
|
944
|
+
const previous = locals.get(name);
|
|
945
|
+
if (!previous || JSON.stringify(previous) !== JSON.stringify(next)) {
|
|
946
|
+
if (previous && previous !== "namespace" && !previous.via) continue;
|
|
947
|
+
locals.set(name, next);
|
|
948
|
+
changed = true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
const exports = /* @__PURE__ */ new Map();
|
|
954
|
+
const localBinding = (name) => {
|
|
955
|
+
const binding = name ? locals.get(name) : void 0;
|
|
956
|
+
return binding && binding !== "namespace" ? binding : void 0;
|
|
957
|
+
};
|
|
958
|
+
for (const statement2 of body) {
|
|
959
|
+
if (!statement2) continue;
|
|
960
|
+
if (statement2.type === "ExportNamedDeclaration") {
|
|
961
|
+
const declaration = child(statement2, "declaration");
|
|
962
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
963
|
+
const name = nameOf(child(declaration, "id"));
|
|
964
|
+
const binding = localBinding(name);
|
|
965
|
+
if (name && binding) exports.set(name, binding);
|
|
966
|
+
} else if (declaration?.type === "VariableDeclaration") {
|
|
967
|
+
for (const declarator of children(declaration, "declarations")) {
|
|
968
|
+
const name = nameOf(child(declarator, "id"));
|
|
969
|
+
const init = child(declarator, "init");
|
|
970
|
+
const binding = localBinding(name) ?? (init?.type === "Identifier" ? localBinding(nameOf(init)) : void 0);
|
|
971
|
+
if (name && binding) exports.set(name, binding);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const source = child(statement2, "source");
|
|
975
|
+
const from = source ? exportsOf(String(source.value)) : null;
|
|
976
|
+
for (const specifier of children(statement2, "specifiers")) {
|
|
977
|
+
if (!specifier || specifier.exportKind === "type") continue;
|
|
978
|
+
const local = nameOf(child(specifier, "local"));
|
|
979
|
+
const exported = nameOf(child(specifier, "exported"));
|
|
980
|
+
if (!local || !exported) continue;
|
|
981
|
+
if (!source) {
|
|
982
|
+
const binding = localBinding(local);
|
|
983
|
+
if (binding) exports.set(exported, binding);
|
|
984
|
+
} else if (from === "package") {
|
|
985
|
+
if (/^[A-Z]/.test(local)) exports.set(exported, { part: local, props: "all" });
|
|
986
|
+
} else if (from?.has(local)) exports.set(exported, from.get(local));
|
|
987
|
+
}
|
|
988
|
+
} else if (statement2.type === "ExportAllDeclaration" && !child(statement2, "exported")) {
|
|
989
|
+
const from = exportsOf(String(child(statement2, "source").value));
|
|
990
|
+
if (from && from !== "package") for (const [name, binding] of from) exports.set(name, binding);
|
|
991
|
+
} else if (statement2.type === "ExportDefaultDeclaration") {
|
|
992
|
+
const declaration = child(statement2, "declaration");
|
|
993
|
+
const name = declaration?.type === "Identifier" ? nameOf(declaration) : nameOf(child(declaration, "id"));
|
|
994
|
+
const binding = localBinding(name);
|
|
995
|
+
if (binding) exports.set("default", binding);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return { locals, exports };
|
|
999
|
+
}
|
|
1000
|
+
function moduleExports(file, parser, stack) {
|
|
1001
|
+
const mtime = statSync(file).mtimeMs;
|
|
1002
|
+
const cached = moduleCache.get(file);
|
|
1003
|
+
if (cached && cached.mtime === mtime) return cached.exports;
|
|
1004
|
+
if (stack.has(file)) return /* @__PURE__ */ new Map();
|
|
1005
|
+
stack.add(file);
|
|
1006
|
+
const program = parseFile(parser, file);
|
|
1007
|
+
const exports = program ? moduleParts(file, program, parser, stack).exports : /* @__PURE__ */ new Map();
|
|
1008
|
+
stack.delete(file);
|
|
1009
|
+
moduleCache.set(file, { mtime, exports });
|
|
1010
|
+
return exports;
|
|
1011
|
+
}
|
|
1012
|
+
function bindingOfTag(tag, locals) {
|
|
1013
|
+
if (!tag) return void 0;
|
|
1014
|
+
if (tag.type === "JSXIdentifier") {
|
|
1015
|
+
const binding = locals.get(tag.name);
|
|
1016
|
+
return binding && binding !== "namespace" ? binding : void 0;
|
|
1017
|
+
}
|
|
1018
|
+
if (tag.type === "JSXMemberExpression") {
|
|
1019
|
+
const object = child(tag, "object");
|
|
1020
|
+
const property = nameOf(child(tag, "property"));
|
|
1021
|
+
if (object?.type === "JSXIdentifier" && locals.get(object.name) === "namespace" && property) return { part: property, props: "all" };
|
|
1022
|
+
}
|
|
1023
|
+
return void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function handsOnTo(fn, bindingOf) {
|
|
1026
|
+
const param = children(fn, "params")[0];
|
|
1027
|
+
if (!param) return void 0;
|
|
1028
|
+
let restName;
|
|
1029
|
+
let propsName;
|
|
1030
|
+
const classParams = /* @__PURE__ */ new Map();
|
|
1031
|
+
if (param.type === "ObjectPattern") {
|
|
1032
|
+
for (const property of children(param, "properties")) {
|
|
1033
|
+
if (!property) continue;
|
|
1034
|
+
if (property.type === "RestElement") restName = nameOf(child(property, "argument"));
|
|
1035
|
+
else if (property.type === "Property") {
|
|
1036
|
+
const key = nameOf(child(property, "key"));
|
|
1037
|
+
let value = child(property, "value");
|
|
1038
|
+
if (value?.type === "AssignmentPattern") value = child(value, "left");
|
|
1039
|
+
const local = nameOf(value);
|
|
1040
|
+
if (key && local && CLASS_PROP.test(key)) classParams.set(local, key);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
} else if (param.type === "Identifier") propsName = nameOf(param);
|
|
1044
|
+
let wrapper;
|
|
1045
|
+
const forwarded = { props: {} };
|
|
1046
|
+
const body = child(fn, "body");
|
|
1047
|
+
if (!body) return void 0;
|
|
1048
|
+
walk(body, (node) => {
|
|
1049
|
+
if (wrapper || node.type !== "JSXOpeningElement") return;
|
|
1050
|
+
const binding = bindingOf(child(node, "name"));
|
|
1051
|
+
if (!binding) return;
|
|
1052
|
+
for (const attribute2 of children(node, "attributes")) {
|
|
1053
|
+
if (!attribute2) continue;
|
|
1054
|
+
if (attribute2.type === "JSXSpreadAttribute") {
|
|
1055
|
+
const argument = nameOf(child(attribute2, "argument"));
|
|
1056
|
+
if (argument && (argument === restName || argument === propsName)) wrapper = { part: binding.part, props: binding.props };
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
const prop = nameOf(child(attribute2, "name"));
|
|
1060
|
+
if (!prop || !CLASS_PROP.test(prop)) continue;
|
|
1061
|
+
const partProp = binding.props === "all" ? prop : binding.props[prop];
|
|
1062
|
+
if (!partProp) continue;
|
|
1063
|
+
walk(attribute2, (inner) => {
|
|
1064
|
+
let callerProp;
|
|
1065
|
+
if (inner.type === "Identifier") callerProp = classParams.get(inner.name);
|
|
1066
|
+
if (inner.type === "MemberExpression" && nameOf(child(inner, "object")) === propsName) {
|
|
1067
|
+
const key = nameOf(child(inner, "property"));
|
|
1068
|
+
if (key && CLASS_PROP.test(key)) callerProp = key;
|
|
1069
|
+
}
|
|
1070
|
+
if (!callerProp) return;
|
|
1071
|
+
if (forwarded.part && forwarded.part !== binding.part) return;
|
|
1072
|
+
forwarded.part = binding.part;
|
|
1073
|
+
forwarded.props[callerProp] = partProp;
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
if (wrapper) return wrapper;
|
|
1078
|
+
return forwarded.part ? { part: forwarded.part, props: forwarded.props } : void 0;
|
|
1079
|
+
}
|
|
1080
|
+
var CLASS_FUNCTIONS = /* @__PURE__ */ new Set(["cn", "clsx", "cx", "twMerge", "classNames"]);
|
|
1081
|
+
function constantOf(identifier, scope) {
|
|
1082
|
+
let current = scope;
|
|
1083
|
+
while (current) {
|
|
1084
|
+
const variable = current.set.get(identifier.name);
|
|
1085
|
+
if (variable) {
|
|
1086
|
+
const definition = variable.defs[0];
|
|
1087
|
+
if (variable.defs.length !== 1 || definition?.type !== "Variable") return void 0;
|
|
1088
|
+
if (definition.parent?.kind !== "const") return void 0;
|
|
1089
|
+
return child(definition.node, "init") ?? void 0;
|
|
1090
|
+
}
|
|
1091
|
+
current = current.upper;
|
|
1092
|
+
}
|
|
1093
|
+
return void 0;
|
|
1094
|
+
}
|
|
1095
|
+
function classesOf(value, scopeOf, asMap = false, seen = /* @__PURE__ */ new Set()) {
|
|
1096
|
+
if (!value || seen.has(value) || seen.size > 200) return [];
|
|
1097
|
+
seen.add(value);
|
|
1098
|
+
const read = (node, map = false) => classesOf(node, scopeOf, map, seen);
|
|
1099
|
+
const words = (text) => typeof text === "string" ? text.split(/\s+/).filter(Boolean) : [];
|
|
1100
|
+
switch (value.type) {
|
|
1101
|
+
case "Literal":
|
|
1102
|
+
return words(value.value);
|
|
1103
|
+
case "TemplateLiteral":
|
|
1104
|
+
return [...children(value, "quasis").flatMap((quasi) => words((quasi?.value).cooked)), ...children(value, "expressions").flatMap((e) => read(e))];
|
|
1105
|
+
case "JSXExpressionContainer":
|
|
1106
|
+
case "TSAsExpression":
|
|
1107
|
+
case "TSSatisfiesExpression":
|
|
1108
|
+
case "TSNonNullExpression":
|
|
1109
|
+
case "ChainExpression":
|
|
1110
|
+
return read(child(value, "expression"), asMap);
|
|
1111
|
+
case "ConditionalExpression":
|
|
1112
|
+
return [...read(child(value, "consequent")), ...read(child(value, "alternate"))];
|
|
1113
|
+
case "LogicalExpression":
|
|
1114
|
+
return value.operator === "&&" ? read(child(value, "right")) : [...read(child(value, "left")), ...read(child(value, "right"))];
|
|
1115
|
+
case "BinaryExpression":
|
|
1116
|
+
return value.operator === "+" ? [...read(child(value, "left")), ...read(child(value, "right"))] : [];
|
|
1117
|
+
case "ArrayExpression":
|
|
1118
|
+
return children(value, "elements").flatMap((e) => read(e));
|
|
1119
|
+
case "ObjectExpression":
|
|
1120
|
+
return children(value, "properties").flatMap((property) => {
|
|
1121
|
+
if (property?.type !== "Property") return [];
|
|
1122
|
+
return asMap ? read(child(property, "value"), true) : property.computed ? [] : words(nameOf(child(property, "key")));
|
|
1123
|
+
});
|
|
1124
|
+
case "CallExpression": {
|
|
1125
|
+
const callee = child(value, "callee");
|
|
1126
|
+
return callee?.type === "Identifier" && CLASS_FUNCTIONS.has(callee.name) ? children(value, "arguments").flatMap((a) => read(a)) : [];
|
|
1127
|
+
}
|
|
1128
|
+
case "Identifier": {
|
|
1129
|
+
const constant = constantOf(value, scopeOf(value));
|
|
1130
|
+
return constant ? read(constant, asMap) : [];
|
|
1131
|
+
}
|
|
1132
|
+
case "MemberExpression": {
|
|
1133
|
+
const object = child(value, "object");
|
|
1134
|
+
const target = object?.type === "Identifier" ? constantOf(object, scopeOf(object)) : object;
|
|
1135
|
+
const map = target?.type === "TSAsExpression" || target?.type === "TSSatisfiesExpression" ? child(target, "expression") : target;
|
|
1136
|
+
if (map?.type !== "ObjectExpression") return [];
|
|
1137
|
+
const property = child(value, "property");
|
|
1138
|
+
const key = value.computed ? property?.type === "Literal" ? String(property.value) : void 0 : nameOf(property);
|
|
1139
|
+
return children(map, "properties").flatMap((entry) => {
|
|
1140
|
+
if (entry?.type !== "Property") return [];
|
|
1141
|
+
if (key !== void 0 && nameOf(child(entry, "key")) !== key) return [];
|
|
1142
|
+
return read(child(entry, "value"), true);
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
default:
|
|
1146
|
+
return [];
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
var code = (items) => items.map((item) => `\`${item}\``).join(" ");
|
|
1150
|
+
var or = (items) => items.length === 1 ? code(items) : `${items.slice(0, -1).map((i) => `\`${i}\``).join(", ")} or \`${items[items.length - 1]}\``;
|
|
1151
|
+
var noRestyledPart = {
|
|
1152
|
+
meta: {
|
|
1153
|
+
type: "problem",
|
|
1154
|
+
docs: { description: "Only placement passes into a part of @estiva-app/ui through a class prop" },
|
|
1155
|
+
schema: [],
|
|
1156
|
+
messages: {
|
|
1157
|
+
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.",
|
|
1158
|
+
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.",
|
|
1159
|
+
...ESCAPE_MESSAGES
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
create(context) {
|
|
1163
|
+
const parser = context.languageOptions?.parser ?? {};
|
|
1164
|
+
const file = context.filename;
|
|
1165
|
+
const sourceCode = context.sourceCode;
|
|
1166
|
+
const scopeOf = (node) => sourceCode.getScope(node);
|
|
1167
|
+
let locals;
|
|
1168
|
+
const escapes = /* @__PURE__ */ new Map();
|
|
1169
|
+
const escaped = (anchor) => {
|
|
1170
|
+
if (!escapes.has(anchor)) escapes.set(anchor, isEscaped(context, anchor));
|
|
1171
|
+
return escapes.get(anchor) === true;
|
|
1172
|
+
};
|
|
1173
|
+
const check = (element, binding, prop, value, at) => {
|
|
1174
|
+
const partProp = binding.props === "all" ? prop : binding.props[prop];
|
|
1175
|
+
if (!partProp || !value) return;
|
|
1176
|
+
const classes = classesOf(value, scopeOf);
|
|
1177
|
+
const handedOn = binding.props === "all" ? "its props" : `its \`${prop}\``;
|
|
1178
|
+
const via = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands ${handedOn} to \`${binding.part}\`)` : `\`${binding.part}\``;
|
|
1179
|
+
const where = `${via}${partProp === "className" ? "" : `'s \`${partProp}\``}`;
|
|
1180
|
+
const restyled = [...new Set(classes.filter((token) => !isPlacement(token)))];
|
|
1181
|
+
const padding = binding.part === "EmptyState" && binding.props === "all" ? [...new Set(classes.filter(isPadding))] : [];
|
|
1182
|
+
if (!restyled.length && !padding.length) return;
|
|
1183
|
+
if (escaped(element)) return;
|
|
1184
|
+
if (restyled.length) {
|
|
1185
|
+
const props = PART_LOOK_PROPS[binding.part];
|
|
1186
|
+
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.`;
|
|
1187
|
+
context.report({ loc: at.loc, messageId: "restyled", data: { classes: code(restyled), where, use } });
|
|
1188
|
+
}
|
|
1189
|
+
if (padding.length) context.report({ loc: at.loc, messageId: "emptyStatePadding", data: { classes: code(padding), where } });
|
|
1190
|
+
};
|
|
1191
|
+
return {
|
|
1192
|
+
Program(program) {
|
|
1193
|
+
locals = moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
|
|
1194
|
+
},
|
|
1195
|
+
JSXOpeningElement(ruleNode) {
|
|
1196
|
+
const element = ruleNode;
|
|
1197
|
+
const binding = locals && bindingOfTag(child(element, "name"), locals);
|
|
1198
|
+
if (!binding) return;
|
|
1199
|
+
for (const attribute2 of children(element, "attributes")) {
|
|
1200
|
+
if (!attribute2) continue;
|
|
1201
|
+
if (attribute2.type === "JSXAttribute") {
|
|
1202
|
+
const prop = nameOf(child(attribute2, "name"));
|
|
1203
|
+
if (prop && CLASS_PROP.test(prop)) check(element, binding, prop, child(attribute2, "value"), attribute2);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
let spread = child(attribute2, "argument");
|
|
1207
|
+
if (spread?.type === "Identifier") spread = constantOf(spread, scopeOf(spread));
|
|
1208
|
+
while (spread?.type === "TSAsExpression" || spread?.type === "TSSatisfiesExpression") spread = child(spread, "expression");
|
|
1209
|
+
if (spread?.type !== "ObjectExpression") continue;
|
|
1210
|
+
for (const property of children(spread, "properties")) {
|
|
1211
|
+
const key = property?.type === "Property" && !property.computed ? nameOf(child(property, "key")) : void 0;
|
|
1212
|
+
if (key && CLASS_PROP.test(key)) check(element, binding, key, child(property, "value"), property);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
265
1220
|
// src/eslint/raw-element-outside-a-wrapper.ts
|
|
266
1221
|
var RAW_ELEMENTS = /* @__PURE__ */ new Set(["a", "button", "input", "textarea", "select", "dialog", "form", "label"]);
|
|
267
1222
|
var rawElementOutsideAWrapper = {
|
|
@@ -302,13 +1257,16 @@ var rawElementOutsideAWrapper = {
|
|
|
302
1257
|
var { version } = createRequire(import.meta.url)("../../package.json");
|
|
303
1258
|
var PLUGIN_KEY = "estiva";
|
|
304
1259
|
var appRules = {
|
|
305
|
-
"no-raw-element": noRawElement
|
|
1260
|
+
"no-raw-element": noRawElement,
|
|
1261
|
+
"no-rebuilt-behaviour": noRebuiltBehaviour,
|
|
1262
|
+
"no-restyled-part": noRestyledPart
|
|
306
1263
|
};
|
|
307
1264
|
var packageRules = {
|
|
308
1265
|
"raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
|
|
309
1266
|
"no-hand-rolled-behaviour": noHandRolledBehaviour,
|
|
310
1267
|
"component-has-a-page": componentHasAPage,
|
|
311
|
-
"component-has-a-story": componentHasAStory
|
|
1268
|
+
"component-has-a-story": componentHasAStory,
|
|
1269
|
+
"no-restyled-part": noRestyledPart
|
|
312
1270
|
};
|
|
313
1271
|
var rules = { ...appRules, ...packageRules };
|
|
314
1272
|
var plugin = {
|
|
@@ -321,7 +1279,11 @@ var PACKAGE_RULE_IDS = Object.keys(packageRules).map((name) => `${PLUGIN_KEY}/${
|
|
|
321
1279
|
plugin.configs.recommended = {
|
|
322
1280
|
name: "@estiva-app/ui/recommended",
|
|
323
1281
|
plugins: { [PLUGIN_KEY]: plugin },
|
|
324
|
-
rules: {
|
|
1282
|
+
rules: {
|
|
1283
|
+
[`${PLUGIN_KEY}/no-raw-element`]: "error",
|
|
1284
|
+
[`${PLUGIN_KEY}/no-rebuilt-behaviour`]: "error",
|
|
1285
|
+
[`${PLUGIN_KEY}/no-restyled-part`]: "error"
|
|
1286
|
+
}
|
|
325
1287
|
};
|
|
326
1288
|
plugin.configs.strict = {
|
|
327
1289
|
name: "@estiva-app/ui/strict",
|
|
@@ -356,7 +1318,10 @@ export {
|
|
|
356
1318
|
APP_RULE_IDS,
|
|
357
1319
|
ESCAPE_MARKER,
|
|
358
1320
|
MIN_REASON,
|
|
1321
|
+
OWNED_BEHAVIOURS,
|
|
359
1322
|
PACKAGE_RULE_IDS,
|
|
1323
|
+
PART_LOOK_PROPS,
|
|
1324
|
+
PLACEMENT,
|
|
360
1325
|
PLUGIN_KEY,
|
|
361
1326
|
SETTINGS_KEY,
|
|
362
1327
|
countGates,
|