@estiva-app/ui 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Button.d.ts.map +1 -1
- package/dist/Card.d.ts.map +1 -1
- package/dist/Chip.d.ts.map +1 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/CommandPalette.d.ts.map +1 -1
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/Lightbox.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/NavItem.d.ts.map +1 -1
- package/dist/PersonTrigger.d.ts.map +1 -1
- package/dist/Reaction.d.ts.map +1 -1
- package/dist/SearchInput.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts +9 -2
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/TextInput.d.ts.map +1 -1
- package/dist/Textarea.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 +449 -28
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/looks-of.d.ts +54 -0
- package/dist/eslint/looks-of.d.ts.map +1 -0
- package/dist/eslint/no-copied-look.d.ts +3 -0
- package/dist/eslint/no-copied-look.d.ts.map +1 -0
- package/dist/eslint/no-native-title.d.ts +31 -0
- package/dist/eslint/no-native-title.d.ts.map +1 -0
- package/dist/gates/index.js +1 -1
- package/dist/gates/index.js.map +2 -2
- package/dist/index.js +171 -133
- package/dist/index.js.map +3 -3
- package/dist/looks.d.ts +34 -0
- package/dist/looks.d.ts.map +1 -0
- package/dist/registry/app-3ALZE6XO.js +10 -0
- package/dist/registry/app.d.ts.map +1 -1
- package/dist/registry/{build-LGFCCOLR.js → build-NDXMVFQ5.js} +3 -3
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-XCBN2EKW.js → chunk-3WJ7ZZ7Z.js} +8 -4
- package/dist/registry/chunk-3WJ7ZZ7Z.js.map +7 -0
- package/dist/registry/{chunk-E4JNV7PC.js → chunk-PZO4BVV3.js} +2 -1
- package/dist/registry/chunk-PZO4BVV3.js.map +7 -0
- package/dist/registry/chunk-XVII7P6G.js +595 -0
- package/dist/registry/chunk-XVII7P6G.js.map +7 -0
- package/dist/registry/cli.js +3 -3
- package/dist/registry/index.js +3 -3
- package/dist/registry/schema.d.ts +8 -0
- package/dist/registry/schema.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +347 -90
- package/src/AttachmentCard.tsx +2 -0
- package/src/Button.tsx +1 -0
- package/src/Card.stories.tsx +2 -2
- package/src/Card.tsx +1 -0
- package/src/Chip.tsx +2 -1
- package/src/ChipInput.tsx +4 -2
- package/src/CommandPalette.tsx +2 -0
- package/src/ContainerHeader.stories.tsx +2 -1
- package/src/DialogShell.tsx +1 -0
- package/src/Divider.stories.tsx +12 -10
- package/src/Lightbox.tsx +1 -0
- package/src/Link.stories.tsx +5 -4
- package/src/Menu.tsx +3 -1
- package/src/NavItem.tsx +1 -0
- package/src/PersonTrigger.tsx +1 -0
- package/src/Reaction.tsx +3 -1
- package/src/ReactionPicker.stories.tsx +3 -2
- package/src/ScrollArea.stories.tsx +13 -12
- package/src/SearchInput.tsx +5 -4
- package/src/SectionLabel.mdx +4 -2
- package/src/SectionLabel.stories.tsx +13 -0
- package/src/SectionLabel.tsx +19 -2
- package/src/Select.tsx +6 -4
- package/src/TextInput.tsx +6 -6
- package/src/Textarea.tsx +6 -4
- package/src/Tooltip.mdx +1 -0
- package/src/Tooltip.stories.tsx +3 -2
- package/src/Tooltip.tsx +2 -1
- package/src/eslint/index.test.ts +16 -9
- package/src/eslint/index.ts +19 -6
- package/src/eslint/looks-of.ts +302 -0
- package/src/eslint/no-copied-look.test.ts +155 -0
- package/src/eslint/no-copied-look.ts +203 -0
- package/src/eslint/no-native-title.test.ts +67 -0
- package/src/eslint/no-native-title.ts +72 -0
- package/src/eslint/no-restyled-part.ts +1 -1
- package/src/gates/app-checks.ts +1 -1
- package/src/gates/create-app.test.ts +1 -1
- package/src/gates/gates.test.ts +1 -1
- package/src/looks.ts +42 -0
- package/src/registry/app.ts +3 -0
- package/src/registry/build.ts +8 -0
- package/src/registry/registry.test.ts +1 -1
- package/src/registry/schema.ts +9 -0
- package/dist/registry/app-FXN4EI34.js +0 -10
- package/dist/registry/chunk-E4JNV7PC.js.map +0 -7
- package/dist/registry/chunk-NJN4MQAM.js +0 -397
- package/dist/registry/chunk-NJN4MQAM.js.map +0 -7
- package/dist/registry/chunk-XCBN2EKW.js.map +0 -7
- /package/dist/registry/{app-FXN4EI34.js.map → app-3ALZE6XO.js.map} +0 -0
- /package/dist/registry/{build-LGFCCOLR.js.map → build-NDXMVFQ5.js.map} +0 -0
package/dist/eslint/index.js
CHANGED
|
@@ -224,7 +224,7 @@ var PART_LOOK_PROPS = {
|
|
|
224
224
|
Reaction: ["pressed"],
|
|
225
225
|
ReactionPicker: ["surface"],
|
|
226
226
|
SectionHeader: ["hover"],
|
|
227
|
-
SectionLabel: ["tone"],
|
|
227
|
+
SectionLabel: ["tone", "truncate"],
|
|
228
228
|
Select: ["size"],
|
|
229
229
|
Tabs: ["size"],
|
|
230
230
|
TextInput: ["size"],
|
|
@@ -547,12 +547,12 @@ function classesOf(value, scopeOf, asMap = false, seen = /* @__PURE__ */ new Set
|
|
|
547
547
|
if (!value || seen.has(value) || seen.size > 200) return [];
|
|
548
548
|
seen.add(value);
|
|
549
549
|
const read = (node, map = false) => classesOf(node, scopeOf, map, seen);
|
|
550
|
-
const
|
|
550
|
+
const words2 = (text) => typeof text === "string" ? text.split(/\s+/).filter(Boolean) : [];
|
|
551
551
|
switch (value.type) {
|
|
552
552
|
case "Literal":
|
|
553
|
-
return
|
|
553
|
+
return words2(value.value);
|
|
554
554
|
case "TemplateLiteral":
|
|
555
|
-
return [...children(value, "quasis").flatMap((quasi) =>
|
|
555
|
+
return [...children(value, "quasis").flatMap((quasi) => words2((quasi?.value).cooked)), ...children(value, "expressions").flatMap((e) => read(e))];
|
|
556
556
|
case "JSXExpressionContainer":
|
|
557
557
|
case "TSAsExpression":
|
|
558
558
|
case "TSSatisfiesExpression":
|
|
@@ -570,7 +570,7 @@ function classesOf(value, scopeOf, asMap = false, seen = /* @__PURE__ */ new Set
|
|
|
570
570
|
case "ObjectExpression":
|
|
571
571
|
return children(value, "properties").flatMap((property) => {
|
|
572
572
|
if (property?.type !== "Property") return [];
|
|
573
|
-
return asMap ? read(child(property, "value"), true) : property.computed ? [] :
|
|
573
|
+
return asMap ? read(child(property, "value"), true) : property.computed ? [] : words2(nameOf(child(property, "key")));
|
|
574
574
|
});
|
|
575
575
|
case "CallExpression": {
|
|
576
576
|
const callee = child(value, "callee");
|
|
@@ -852,8 +852,8 @@ var noHandmadeEmptyState = {
|
|
|
852
852
|
const opening = child2(element, "openingElement");
|
|
853
853
|
const tag = tagName(element);
|
|
854
854
|
if (tag && /^[a-z]/.test(tag)) {
|
|
855
|
-
const
|
|
856
|
-
if (!
|
|
855
|
+
const words2 = onlyWords(element);
|
|
856
|
+
if (!words2 || words2.length > SHORT) return;
|
|
857
857
|
if (!standsInForAList(element)) return;
|
|
858
858
|
if (isEscaped(context, opening)) return;
|
|
859
859
|
context.report({ loc: opening.loc, messageId: "handmade" });
|
|
@@ -920,16 +920,16 @@ function classesOf2(el) {
|
|
|
920
920
|
}
|
|
921
921
|
function meaningful(el) {
|
|
922
922
|
const out = [];
|
|
923
|
-
for (const
|
|
924
|
-
if (
|
|
925
|
-
if (String(
|
|
926
|
-
} else if (
|
|
927
|
-
const e =
|
|
923
|
+
for (const child4 of el.children) {
|
|
924
|
+
if (child4.type === "JSXText") {
|
|
925
|
+
if (String(child4.value).trim()) out.push(child4);
|
|
926
|
+
} else if (child4.type === "JSXExpressionContainer") {
|
|
927
|
+
const e = child4.expression;
|
|
928
928
|
if (e.type === "JSXEmptyExpression") continue;
|
|
929
929
|
if (e.type === "LogicalExpression" && isElement(e.right)) out.push(e.right);
|
|
930
930
|
else if (isElement(e)) out.push(e);
|
|
931
|
-
else out.push(
|
|
932
|
-
} else if (
|
|
931
|
+
else out.push(child4);
|
|
932
|
+
} else if (child4.type === "JSXElement" || child4.type === "JSXFragment") out.push(child4);
|
|
933
933
|
}
|
|
934
934
|
return out;
|
|
935
935
|
}
|
|
@@ -937,16 +937,16 @@ var TEXT_PARTS = /* @__PURE__ */ new Set(["EditableText", "Person", "Breadcrumb"
|
|
|
937
937
|
var VALUE_EXPRESSIONS = /* @__PURE__ */ new Set(["Identifier", "MemberExpression", "TemplateLiteral", "Literal", "LogicalExpression", "ConditionalExpression", "ChainExpression", "CallExpression"]);
|
|
938
938
|
function carriesTitle(el, depth = 0) {
|
|
939
939
|
if (depth > 3) return false;
|
|
940
|
-
for (const
|
|
941
|
-
if (
|
|
942
|
-
if (
|
|
943
|
-
if (VALUE_EXPRESSIONS.has(
|
|
940
|
+
for (const child4 of meaningful(el)) {
|
|
941
|
+
if (child4.type === "JSXText") return true;
|
|
942
|
+
if (child4.type === "JSXExpressionContainer") {
|
|
943
|
+
if (VALUE_EXPRESSIONS.has(child4.expression.type)) return true;
|
|
944
944
|
continue;
|
|
945
945
|
}
|
|
946
|
-
if (!isElement(
|
|
947
|
-
const tag = tagOf(
|
|
946
|
+
if (!isElement(child4)) continue;
|
|
947
|
+
const tag = tagOf(child4);
|
|
948
948
|
if (/^h[1-6]$/.test(tag) || TEXT_PARTS.has(tag) || /\.(?:Title|Heading)$/.test(tag)) return true;
|
|
949
|
-
if (/^[a-z]/.test(tag) && carriesTitle(
|
|
949
|
+
if (/^[a-z]/.test(tag) && carriesTitle(child4, depth + 1)) return true;
|
|
950
950
|
}
|
|
951
951
|
return false;
|
|
952
952
|
}
|
|
@@ -984,6 +984,416 @@ var noHandmadeHeader = {
|
|
|
984
984
|
}
|
|
985
985
|
};
|
|
986
986
|
|
|
987
|
+
// src/eslint/no-copied-look.ts
|
|
988
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync, statSync as statSync3 } from "node:fs";
|
|
989
|
+
import { dirname as dirname3, join as join3, relative, resolve as resolve2 } from "node:path";
|
|
990
|
+
|
|
991
|
+
// src/eslint/looks-of.ts
|
|
992
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
993
|
+
import { dirname as dirname2, join as join2, resolve as resolvePath } from "node:path";
|
|
994
|
+
import ts from "typescript";
|
|
995
|
+
var PLACEMENT2 = [
|
|
996
|
+
/^m[xytrblse]?-/,
|
|
997
|
+
/^p[xytrblse]?-/,
|
|
998
|
+
/^(?:w|h|size|min-w|min-h|max-w|max-h)-/,
|
|
999
|
+
/^(?:hidden|block|inline|inline-block|inline-flex|inline-grid|flex|grid|contents|flow-root)$/,
|
|
1000
|
+
/^(?: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)-/,
|
|
1001
|
+
/^(?:self|justify|justify-items|justify-self|items|place-content|place-items|place-self)-|^content-(?!\[)/,
|
|
1002
|
+
/^(?:static|relative|absolute|fixed|sticky)$|^(?:inset|inset-x|inset-y|top|right|bottom|left|start|end|z)-/,
|
|
1003
|
+
/^(?:group|peer)(?:\/[\w-]+)?$/
|
|
1004
|
+
];
|
|
1005
|
+
var bare = (token) => token.replace(/^(?:[^:\s[\]]+:)+/, "");
|
|
1006
|
+
var isPlacementWord = (token) => PLACEMENT2.some((pattern) => pattern.test(bare(token)));
|
|
1007
|
+
var CLASSY = /^!?(?:[a-z0-9-]+:|\[[^\]\s]+\]:)*-?[a-z@][a-z0-9-]*(?:-\[[^\]\s]+\]|\/[\w.[\]-]+|-[\w.]+)*$|^\[[^\]\s]+\]$/;
|
|
1008
|
+
var words = (text) => text.split(/\s+/).filter(Boolean);
|
|
1009
|
+
var CLASS_FUNCTIONS2 = /* @__PURE__ */ new Set(["cn", "clsx", "cx", "twMerge", "classNames"]);
|
|
1010
|
+
var CLASS_PROP2 = /^(?:className|[a-z][A-Za-z]*ClassName)$/;
|
|
1011
|
+
function isClassList(text) {
|
|
1012
|
+
const list = words(text);
|
|
1013
|
+
return list.length >= 3 && list.every((w) => CLASSY.test(w)) && list.filter((w) => /[-:[]/.test(w)).length >= 2;
|
|
1014
|
+
}
|
|
1015
|
+
var CAP = 24;
|
|
1016
|
+
function classUnits(source, follow) {
|
|
1017
|
+
const consts = /* @__PURE__ */ new Map();
|
|
1018
|
+
const collect = (node) => {
|
|
1019
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isVariableDeclarationList(node.parent) && node.parent.flags & ts.NodeFlags.Const) {
|
|
1020
|
+
consts.set(node.name.text, node.initializer);
|
|
1021
|
+
}
|
|
1022
|
+
ts.forEachChild(node, collect);
|
|
1023
|
+
};
|
|
1024
|
+
collect(source);
|
|
1025
|
+
const product = (lists) => {
|
|
1026
|
+
let out = [[]];
|
|
1027
|
+
for (const alternatives of lists) {
|
|
1028
|
+
const next = [];
|
|
1029
|
+
for (const a of out) for (const b of alternatives) if (next.length < CAP) next.push([...a, ...b]);
|
|
1030
|
+
out = next;
|
|
1031
|
+
}
|
|
1032
|
+
return out;
|
|
1033
|
+
};
|
|
1034
|
+
const resolve3 = (node, depth = 0) => {
|
|
1035
|
+
if (!node || depth > 8) return [[]];
|
|
1036
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return [words(node.text)];
|
|
1037
|
+
if (ts.isTemplateExpression(node)) return product([[words(node.head.text)], ...node.templateSpans.flatMap((span) => [resolve3(span.expression, depth + 1), [words(span.literal.text)]])]);
|
|
1038
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return resolve3(node.expression, depth + 1);
|
|
1039
|
+
if (ts.isJsxExpression(node)) return resolve3(node.expression, depth + 1);
|
|
1040
|
+
if (ts.isCallExpression(node)) return ts.isIdentifier(node.expression) && CLASS_FUNCTIONS2.has(node.expression.text) ? product(node.arguments.map((a) => resolve3(a, depth + 1))) : [[]];
|
|
1041
|
+
if (ts.isConditionalExpression(node)) return [...resolve3(node.whenTrue, depth + 1), ...resolve3(node.whenFalse, depth + 1)].slice(0, CAP);
|
|
1042
|
+
if (ts.isBinaryExpression(node)) {
|
|
1043
|
+
if (node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) return [[], ...resolve3(node.right, depth + 1)];
|
|
1044
|
+
if (node.operatorToken.kind === ts.SyntaxKind.BarBarToken || node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) return [...resolve3(node.left, depth + 1), ...resolve3(node.right, depth + 1)].slice(0, CAP);
|
|
1045
|
+
return [[]];
|
|
1046
|
+
}
|
|
1047
|
+
if (ts.isArrayLiteralExpression(node)) return product(node.elements.map((e) => resolve3(e, depth + 1)));
|
|
1048
|
+
if (ts.isObjectLiteralExpression(node)) return [[], node.properties.flatMap((p) => p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? words(p.name.text) : [])];
|
|
1049
|
+
if (ts.isIdentifier(node)) {
|
|
1050
|
+
const init = consts.get(node.text) ?? follow?.(node.text);
|
|
1051
|
+
return init ? resolve3(init, depth + 1) : [[]];
|
|
1052
|
+
}
|
|
1053
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
1054
|
+
let target = node.expression;
|
|
1055
|
+
if (ts.isIdentifier(target)) target = consts.get(target.text) ?? follow?.(target.text);
|
|
1056
|
+
while (target && (ts.isAsExpression(target) || ts.isSatisfiesExpression(target) || ts.isParenthesizedExpression(target))) target = target.expression;
|
|
1057
|
+
if (!target || !ts.isObjectLiteralExpression(target)) return [[]];
|
|
1058
|
+
const key = ts.isPropertyAccessExpression(node) ? node.name.text : ts.isStringLiteral(node.argumentExpression) ? node.argumentExpression.text : null;
|
|
1059
|
+
return target.properties.filter((p) => ts.isPropertyAssignment(p) && (key === null || !!p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === key)).flatMap((p) => resolve3(p.initializer, depth + 1)).slice(0, CAP);
|
|
1060
|
+
}
|
|
1061
|
+
return [[]];
|
|
1062
|
+
};
|
|
1063
|
+
const units = [];
|
|
1064
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1065
|
+
const sharedAt = (node) => {
|
|
1066
|
+
for (let at = node; at; at = at.parent) {
|
|
1067
|
+
if (ts.isVariableDeclaration(at)) return ts.isIdentifier(at.name) && /_CLASSES$/.test(at.name.text) && !!at.parent?.parent && ts.isVariableStatement(at.parent.parent) && !!at.parent.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
1068
|
+
}
|
|
1069
|
+
return false;
|
|
1070
|
+
};
|
|
1071
|
+
const lineStart = (node) => {
|
|
1072
|
+
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line;
|
|
1073
|
+
let at = node;
|
|
1074
|
+
while (at.parent && !ts.isSourceFile(at.parent) && source.getLineAndCharacterOfPosition(at.parent.getStart(source)).line === line) at = at.parent;
|
|
1075
|
+
return at.getStart(source);
|
|
1076
|
+
};
|
|
1077
|
+
const add = (tokens, node, via, anchor = via === "literal" ? lineStart(node) : node.getStart(source)) => {
|
|
1078
|
+
const list = [...new Set(tokens.filter((t) => CLASSY.test(t)))];
|
|
1079
|
+
const look = list.filter((t) => !isPlacementWord(t));
|
|
1080
|
+
if (look.length === 0) return;
|
|
1081
|
+
const key = `${via}:${node.getStart(source)}:${[...list].sort().join(" ")}`;
|
|
1082
|
+
if (seen.has(key)) return;
|
|
1083
|
+
seen.add(key);
|
|
1084
|
+
units.push({ tokens: list, look, start: node.getStart(source), end: node.getEnd(), via, anchor, shared: via === "literal" && sharedAt(node) });
|
|
1085
|
+
};
|
|
1086
|
+
const visit = (node) => {
|
|
1087
|
+
if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && CLASS_PROP2.test(node.name.text) && node.initializer) {
|
|
1088
|
+
const element = node.parent.parent;
|
|
1089
|
+
for (const alternative of resolve3(node.initializer)) add(alternative, node, "attribute", element.getStart(source));
|
|
1090
|
+
}
|
|
1091
|
+
if ((ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) && isClassList(node.text)) add(words(node.text), node, "literal");
|
|
1092
|
+
ts.forEachChild(node, visit);
|
|
1093
|
+
};
|
|
1094
|
+
visit(source);
|
|
1095
|
+
return units;
|
|
1096
|
+
}
|
|
1097
|
+
function componentsOf(source) {
|
|
1098
|
+
const found = /* @__PURE__ */ new Map();
|
|
1099
|
+
const fn = (node) => {
|
|
1100
|
+
let current = node;
|
|
1101
|
+
while (current && ts.isCallExpression(current) && current.arguments.length) current = current.arguments[0];
|
|
1102
|
+
return current && (ts.isArrowFunction(current) || ts.isFunctionExpression(current)) ? current : void 0;
|
|
1103
|
+
};
|
|
1104
|
+
for (const statement2 of source.statements) {
|
|
1105
|
+
if (ts.isFunctionDeclaration(statement2) && statement2.name && /^[A-Z]/.test(statement2.name.text)) found.set(statement2.name.text, { start: statement2.getStart(source), end: statement2.getEnd() });
|
|
1106
|
+
if (ts.isVariableStatement(statement2)) {
|
|
1107
|
+
for (const declaration of statement2.declarationList.declarations) {
|
|
1108
|
+
const body = fn(declaration.initializer);
|
|
1109
|
+
if (ts.isIdentifier(declaration.name) && /^[A-Z]/.test(declaration.name.text) && body) found.set(declaration.name.text, { start: body.getStart(source), end: body.getEnd() });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return found;
|
|
1114
|
+
}
|
|
1115
|
+
var MIN_LOOK_WORDS = 4;
|
|
1116
|
+
function looksOf(source, names) {
|
|
1117
|
+
const components = componentsOf(source);
|
|
1118
|
+
const units = classUnits(source, importedConstants(source)).filter((u) => u.look.length >= MIN_LOOK_WORDS);
|
|
1119
|
+
const inside = (u, r) => u.start >= r.start && u.start < r.end;
|
|
1120
|
+
const out = /* @__PURE__ */ new Map();
|
|
1121
|
+
for (const name of names ?? components.keys()) {
|
|
1122
|
+
const range = components.get(name);
|
|
1123
|
+
const mine = units.filter((u) => (range ? inside(u, range) : false) || ![...components.values()].some((r) => inside(u, r)));
|
|
1124
|
+
out.set(name, [...new Set(mine.map((u) => u.look.join(" ")))]);
|
|
1125
|
+
}
|
|
1126
|
+
return out;
|
|
1127
|
+
}
|
|
1128
|
+
function parseForLooks(fileName2, text) {
|
|
1129
|
+
return ts.createSourceFile(fileName2, text, ts.ScriptTarget.Latest, true, fileName2.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
1130
|
+
}
|
|
1131
|
+
var MIN_SHARED = 4;
|
|
1132
|
+
var MIN_COVER = 0.8;
|
|
1133
|
+
function copies(list, look) {
|
|
1134
|
+
const mine = new Set(list);
|
|
1135
|
+
const shared = look.filter((word) => mine.has(word));
|
|
1136
|
+
if (shared.length < MIN_SHARED) return null;
|
|
1137
|
+
return shared.length / Math.min(list.length, look.length) >= MIN_COVER ? shared : null;
|
|
1138
|
+
}
|
|
1139
|
+
function importTarget(fromFile, specifier) {
|
|
1140
|
+
let base;
|
|
1141
|
+
if (specifier.startsWith(".")) base = resolvePath(dirname2(fromFile), specifier);
|
|
1142
|
+
else if (specifier.startsWith("@/")) {
|
|
1143
|
+
let dir = dirname2(resolvePath(fromFile));
|
|
1144
|
+
while (!existsSync3(join2(dir, "package.json")) && dirname2(dir) !== dir) dir = dirname2(dir);
|
|
1145
|
+
base = join2(dir, "src", specifier.slice(2));
|
|
1146
|
+
} else return null;
|
|
1147
|
+
for (const candidate of [`${base}.ts`, `${base}.tsx`, join2(base, "index.ts")]) if (existsSync3(candidate)) return candidate;
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
var constantsCache = /* @__PURE__ */ new Map();
|
|
1151
|
+
function constantsOf(file) {
|
|
1152
|
+
const cached = constantsCache.get(file);
|
|
1153
|
+
if (cached) return cached;
|
|
1154
|
+
const found = /* @__PURE__ */ new Map();
|
|
1155
|
+
const source = parseForLooks(file, readFileSync2(file, "utf8"));
|
|
1156
|
+
for (const statement2 of source.statements) {
|
|
1157
|
+
if (!ts.isVariableStatement(statement2)) continue;
|
|
1158
|
+
for (const d of statement2.declarationList.declarations) if (ts.isIdentifier(d.name) && d.initializer) found.set(d.name.text, d.initializer);
|
|
1159
|
+
}
|
|
1160
|
+
constantsCache.set(file, found);
|
|
1161
|
+
return found;
|
|
1162
|
+
}
|
|
1163
|
+
function importedConstants(source) {
|
|
1164
|
+
const imported = /* @__PURE__ */ new Map();
|
|
1165
|
+
for (const statement2 of source.statements) {
|
|
1166
|
+
if (!ts.isImportDeclaration(statement2) || !ts.isStringLiteral(statement2.moduleSpecifier)) continue;
|
|
1167
|
+
const bindings = statement2.importClause?.namedBindings;
|
|
1168
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
1169
|
+
const target = importTarget(source.fileName, statement2.moduleSpecifier.text);
|
|
1170
|
+
if (!target) continue;
|
|
1171
|
+
for (const element of bindings.elements) {
|
|
1172
|
+
const local = element.name.text;
|
|
1173
|
+
if (/_CLASSES$/.test(element.propertyName?.text ?? local)) imported.set(local, { file: target, name: element.propertyName?.text ?? local });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return (name) => {
|
|
1177
|
+
const from = imported.get(name);
|
|
1178
|
+
if (!from) return void 0;
|
|
1179
|
+
try {
|
|
1180
|
+
return constantsOf(from.file).get(from.name);
|
|
1181
|
+
} catch {
|
|
1182
|
+
return void 0;
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function sharedLooksOf(source) {
|
|
1187
|
+
const out = /* @__PURE__ */ new Map();
|
|
1188
|
+
const lookWords = (text) => words(text).filter((w) => CLASSY.test(w) && !isPlacementWord(w));
|
|
1189
|
+
for (const statement2 of source.statements) {
|
|
1190
|
+
if (!ts.isVariableStatement(statement2) || !statement2.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) continue;
|
|
1191
|
+
for (const d of statement2.declarationList.declarations) {
|
|
1192
|
+
if (!ts.isIdentifier(d.name) || !/_CLASSES$/.test(d.name.text) || !d.initializer) continue;
|
|
1193
|
+
let init = d.initializer;
|
|
1194
|
+
while (ts.isAsExpression(init) || ts.isSatisfiesExpression(init) || ts.isParenthesizedExpression(init)) init = init.expression;
|
|
1195
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
1196
|
+
const look = lookWords(init.text);
|
|
1197
|
+
if (look.length >= MIN_LOOK_WORDS) out.set(d.name.text, [look.join(" ")]);
|
|
1198
|
+
} else if (ts.isObjectLiteralExpression(init)) {
|
|
1199
|
+
for (const p of init.properties) {
|
|
1200
|
+
if (!ts.isPropertyAssignment(p) || !p.name || !(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))) continue;
|
|
1201
|
+
if (!(ts.isStringLiteral(p.initializer) || ts.isNoSubstitutionTemplateLiteral(p.initializer))) continue;
|
|
1202
|
+
const look = lookWords(p.initializer.text);
|
|
1203
|
+
if (look.length >= MIN_LOOK_WORDS) out.set(`${d.name.text}.${p.name.text}`, [look.join(" ")]);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return out;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/eslint/no-copied-look.ts
|
|
1212
|
+
var packageDirCache2 = /* @__PURE__ */ new Map();
|
|
1213
|
+
function packageDirOf2(file) {
|
|
1214
|
+
const start = dirname3(resolve2(file));
|
|
1215
|
+
if (packageDirCache2.has(start)) return packageDirCache2.get(start) ?? null;
|
|
1216
|
+
let dir = start;
|
|
1217
|
+
for (; ; ) {
|
|
1218
|
+
if (existsSync4(join3(dir, "package.json"))) break;
|
|
1219
|
+
const up = dirname3(dir);
|
|
1220
|
+
if (up === dir) {
|
|
1221
|
+
dir = "";
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
dir = up;
|
|
1225
|
+
}
|
|
1226
|
+
packageDirCache2.set(start, dir || null);
|
|
1227
|
+
return dir || null;
|
|
1228
|
+
}
|
|
1229
|
+
var PACKAGE2 = "@estiva-app/ui";
|
|
1230
|
+
var nameOf3 = (dir) => {
|
|
1231
|
+
try {
|
|
1232
|
+
return JSON.parse(readFileSync3(join3(dir, "package.json"), "utf8")).name;
|
|
1233
|
+
} catch {
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
var shippedCache = /* @__PURE__ */ new Map();
|
|
1238
|
+
function shippedLooks(appDir) {
|
|
1239
|
+
for (let dir = appDir; ; dir = dirname3(dir)) {
|
|
1240
|
+
const file = join3(dir, "node_modules", PACKAGE2, "registry.json");
|
|
1241
|
+
if (existsSync4(file)) {
|
|
1242
|
+
const mtime = statSync3(file).mtimeMs;
|
|
1243
|
+
const cached = shippedCache.get(file);
|
|
1244
|
+
if (cached && cached.mtime === mtime) return cached.looks;
|
|
1245
|
+
let looks = [];
|
|
1246
|
+
try {
|
|
1247
|
+
const registry = JSON.parse(readFileSync3(file, "utf8"));
|
|
1248
|
+
looks = registry.entries.flatMap((e) => (e.looks ?? []).map((l) => ({ part: e.name, file: e.sourceFile, look: l.split(" "), from: "package" })));
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
shippedCache.set(file, { mtime, looks });
|
|
1252
|
+
return looks;
|
|
1253
|
+
}
|
|
1254
|
+
if (dirname3(dir) === dir) return [];
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function exportedNames(source) {
|
|
1258
|
+
const names = /* @__PURE__ */ new Set();
|
|
1259
|
+
const text = source.text;
|
|
1260
|
+
for (const m of text.matchAll(/export\s+(?:default\s+)?(?:function|const)\s+([A-Z]\w*)/g)) names.add(m[1]);
|
|
1261
|
+
for (const m of text.matchAll(/export\s*\{([^}]*)\}/g)) for (const part of m[1].split(",")) {
|
|
1262
|
+
const name = part.trim().split(/\s+as\s+/).pop();
|
|
1263
|
+
if (name && /^[A-Z]/.test(name)) names.add(name);
|
|
1264
|
+
}
|
|
1265
|
+
return names;
|
|
1266
|
+
}
|
|
1267
|
+
var ownCache = /* @__PURE__ */ new Map();
|
|
1268
|
+
function ownLooks(repoDir) {
|
|
1269
|
+
const out = [];
|
|
1270
|
+
const walk2 = (dir) => {
|
|
1271
|
+
let names = [];
|
|
1272
|
+
try {
|
|
1273
|
+
names = readdirSync(dir);
|
|
1274
|
+
} catch {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
for (const name of names) {
|
|
1278
|
+
if (name === "node_modules" || name === "dist") continue;
|
|
1279
|
+
const path = join3(dir, name);
|
|
1280
|
+
if (statSync3(path).isDirectory()) walk2(path);
|
|
1281
|
+
else if (/\.tsx?$/.test(name) && !/\.(?:stories|test)\.tsx?$/.test(name) && !name.endsWith(".d.ts")) {
|
|
1282
|
+
const mtime = statSync3(path).mtimeMs;
|
|
1283
|
+
let cached = ownCache.get(path);
|
|
1284
|
+
if (!cached || cached.mtime !== mtime) {
|
|
1285
|
+
const text = readFileSync3(path, "utf8");
|
|
1286
|
+
const source = parseForLooks(path, text);
|
|
1287
|
+
const exported = name.endsWith(".tsx") ? [...componentsOf(source).keys()].filter((n) => exportedNames(source).has(n)) : [];
|
|
1288
|
+
const file = relative(repoDir, path).replace(/\\/g, "/");
|
|
1289
|
+
const looks = [...looksOf(source, exported)].flatMap(([part, lists]) => lists.map((l) => ({ part, file, look: l.split(" "), from: "own" })));
|
|
1290
|
+
if (text.includes("_CLASSES")) for (const [part, lists] of sharedLooksOf(source)) for (const l of lists) looks.push({ part, file, look: l.split(" "), from: "shared" });
|
|
1291
|
+
cached = { mtime, looks };
|
|
1292
|
+
ownCache.set(path, cached);
|
|
1293
|
+
}
|
|
1294
|
+
out.push(...cached.looks);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
walk2(join3(repoDir, "src"));
|
|
1299
|
+
return out;
|
|
1300
|
+
}
|
|
1301
|
+
var noCopiedLook = {
|
|
1302
|
+
meta: {
|
|
1303
|
+
type: "suggestion",
|
|
1304
|
+
docs: { description: "A part's look typed again by hand, where the part (or a shared look) should be used" },
|
|
1305
|
+
schema: [],
|
|
1306
|
+
messages: {
|
|
1307
|
+
usePart: "This class list is `{{part}}`'s look, typed again ({{count}} words: {{words}}). Use `{{part}}` from {{from}}: a copy looks right today and stays behind the day `{{part}}` changes.",
|
|
1308
|
+
shareIt: "This look is also written in `{{part}}` ({{file}}): {{words}}. Write it once in src/looks.ts and use it in both, so the two cannot drift apart.",
|
|
1309
|
+
useShared: "This class list is `{{part}}` ({{file}}), typed again: {{words}}. Use `{{part}}`: a look written once cannot drift apart.",
|
|
1310
|
+
...ESCAPE_MESSAGES
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
create(context) {
|
|
1314
|
+
const file = context.filename;
|
|
1315
|
+
const repoDir = packageDirOf2(file);
|
|
1316
|
+
if (!repoDir) return {};
|
|
1317
|
+
const inPackage = nameOf3(repoDir) === PACKAGE2;
|
|
1318
|
+
return {
|
|
1319
|
+
Program() {
|
|
1320
|
+
const here = relative(repoDir, file).replace(/\\/g, "/");
|
|
1321
|
+
const source = parseForLooks(file, context.sourceCode.text);
|
|
1322
|
+
const declared = new Set(componentsOf(source).keys());
|
|
1323
|
+
const looks = [...inPackage ? [] : shippedLooks(repoDir), ...ownLooks(repoDir)].filter((l) => l.file !== here && !declared.has(l.part));
|
|
1324
|
+
if (!looks.length) return;
|
|
1325
|
+
const units = classUnits(source);
|
|
1326
|
+
const within = (u) => units.some((o) => o !== u && o.via === "attribute" && u.via === "literal" && u.start >= o.start && u.end <= o.end);
|
|
1327
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1328
|
+
for (const unit of units) {
|
|
1329
|
+
if (within(unit) || reported.has(unit.start)) continue;
|
|
1330
|
+
let best = null;
|
|
1331
|
+
for (const look of looks) {
|
|
1332
|
+
if (unit.shared && look.from !== "package") continue;
|
|
1333
|
+
const shared = copies(unit.look, look.look);
|
|
1334
|
+
if (shared && (!best || shared.length > best.shared.length || shared.length === best.shared.length && look.from === "shared")) best = { look, shared };
|
|
1335
|
+
}
|
|
1336
|
+
if (!best) continue;
|
|
1337
|
+
const found = best;
|
|
1338
|
+
if (unit.via === "attribute" && units.some((o) => o.via === "literal" && !within(o) && found.shared.every((w) => o.look.includes(w)) && looks.some((l) => copies(o.look, l.look)))) continue;
|
|
1339
|
+
const loc = { start: context.sourceCode.getLocFromIndex(unit.start), end: context.sourceCode.getLocFromIndex(unit.end) };
|
|
1340
|
+
const at = { start: context.sourceCode.getLocFromIndex(unit.anchor), end: loc.end };
|
|
1341
|
+
if (isEscaped(context, { loc: at, range: [unit.anchor, unit.end] })) continue;
|
|
1342
|
+
reported.add(unit.start);
|
|
1343
|
+
const words2 = best.shared.map((w) => `\`${w}\``).join(" ");
|
|
1344
|
+
if (best.look.from === "shared") context.report({ loc, messageId: "useShared", data: { part: best.look.part, file: best.look.file, words: words2 } });
|
|
1345
|
+
else if (inPackage) context.report({ loc, messageId: "shareIt", data: { part: best.look.part, file: best.look.file, words: words2 } });
|
|
1346
|
+
else
|
|
1347
|
+
context.report({
|
|
1348
|
+
loc,
|
|
1349
|
+
messageId: "usePart",
|
|
1350
|
+
data: { part: best.look.part, count: String(best.shared.length), words: words2, from: best.look.from === "package" ? PACKAGE2 : `\`${best.look.file}\`` }
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
// src/eslint/no-native-title.ts
|
|
1359
|
+
var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "SectionHeader"];
|
|
1360
|
+
var child3 = (node, key) => node[key];
|
|
1361
|
+
var nameOf4 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
|
|
1362
|
+
var noNativeTitle = {
|
|
1363
|
+
meta: {
|
|
1364
|
+
type: "problem",
|
|
1365
|
+
docs: { description: "A title attribute: the browser's tooltip where WithTooltip is the part" },
|
|
1366
|
+
schema: [],
|
|
1367
|
+
messages: {
|
|
1368
|
+
native: "`title` is the browser's tooltip, not ours. Wrap it in `WithTooltip` from @estiva-app/ui, or use `IconButton`'s `tooltip`: its page (Primitives/Tooltip) says when it shows and how.",
|
|
1369
|
+
...ESCAPE_MESSAGES
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
create(context) {
|
|
1373
|
+
const parser = context.languageOptions?.parser ?? {};
|
|
1374
|
+
let locals;
|
|
1375
|
+
return {
|
|
1376
|
+
Program(program) {
|
|
1377
|
+
locals = partsOfFile(context.filename, program, parser);
|
|
1378
|
+
},
|
|
1379
|
+
JSXAttribute(ruleNode) {
|
|
1380
|
+
const attribute3 = ruleNode;
|
|
1381
|
+
if (nameOf4(child3(attribute3, "name")) !== "title") return;
|
|
1382
|
+
const opening = attribute3.parent;
|
|
1383
|
+
const tag = child3(opening, "name");
|
|
1384
|
+
const name = nameOf4(tag);
|
|
1385
|
+
if (!(name && /^[a-z]/.test(name))) {
|
|
1386
|
+
const binding = locals && bindingOfTag(tag, locals);
|
|
1387
|
+
if (!binding || binding.props !== "all") return;
|
|
1388
|
+
if (PARTS_WITH_A_TITLE.includes(binding.part)) return;
|
|
1389
|
+
}
|
|
1390
|
+
if (isEscaped(context, opening)) return;
|
|
1391
|
+
context.report({ loc: attribute3.loc, messageId: "native" });
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
|
|
987
1397
|
// src/eslint/no-raw-element.ts
|
|
988
1398
|
var RAW_ELEMENT_PARTS = {
|
|
989
1399
|
a: { use: "Link", more: " For a chip, `InlineChip`; for a whole card, `Card` with `href`." },
|
|
@@ -1267,10 +1677,10 @@ function anchorOf(node) {
|
|
|
1267
1677
|
if (node.type === "FunctionDeclaration") {
|
|
1268
1678
|
return node.parent && /^Export/.test(node.parent.type) ? node.parent : node;
|
|
1269
1679
|
}
|
|
1270
|
-
let
|
|
1271
|
-
for (let p = node.parent; p;
|
|
1680
|
+
let child4 = node;
|
|
1681
|
+
for (let p = node.parent; p; child4 = p, p = p.parent) {
|
|
1272
1682
|
if (p.type === "JSXOpeningElement" || p.type === "Property" || p.type === "MethodDefinition" || p.type === "PropertyDefinition") return p;
|
|
1273
|
-
if (p.type === "ArrayExpression") return
|
|
1683
|
+
if (p.type === "ArrayExpression") return child4;
|
|
1274
1684
|
if (p.type !== "BlockStatement" && /(Statement|Declaration)$/.test(p.type)) return p;
|
|
1275
1685
|
}
|
|
1276
1686
|
return node;
|
|
@@ -1584,7 +1994,9 @@ var appRules = {
|
|
|
1584
1994
|
"no-rebuilt-behaviour": noRebuiltBehaviour,
|
|
1585
1995
|
"no-restyled-part": noRestyledPart,
|
|
1586
1996
|
"no-handmade-header": noHandmadeHeader,
|
|
1587
|
-
"no-handmade-empty-state": noHandmadeEmptyState
|
|
1997
|
+
"no-handmade-empty-state": noHandmadeEmptyState,
|
|
1998
|
+
"no-native-title": noNativeTitle,
|
|
1999
|
+
"no-copied-look": noCopiedLook
|
|
1588
2000
|
};
|
|
1589
2001
|
var packageRules = {
|
|
1590
2002
|
"raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
|
|
@@ -1593,7 +2005,9 @@ var packageRules = {
|
|
|
1593
2005
|
"component-has-a-story": componentHasAStory,
|
|
1594
2006
|
"no-restyled-part": noRestyledPart,
|
|
1595
2007
|
"no-handmade-header": noHandmadeHeader,
|
|
1596
|
-
"no-handmade-empty-state": noHandmadeEmptyState
|
|
2008
|
+
"no-handmade-empty-state": noHandmadeEmptyState,
|
|
2009
|
+
"no-native-title": noNativeTitle,
|
|
2010
|
+
"no-copied-look": noCopiedLook
|
|
1597
2011
|
};
|
|
1598
2012
|
var rules = { ...appRules, ...packageRules };
|
|
1599
2013
|
var plugin = {
|
|
@@ -1611,7 +2025,10 @@ plugin.configs.recommended = {
|
|
|
1611
2025
|
[`${PLUGIN_KEY}/no-rebuilt-behaviour`]: "error",
|
|
1612
2026
|
[`${PLUGIN_KEY}/no-restyled-part`]: "error",
|
|
1613
2027
|
[`${PLUGIN_KEY}/no-handmade-header`]: "error",
|
|
1614
|
-
[`${PLUGIN_KEY}/no-handmade-empty-state`]: "error"
|
|
2028
|
+
[`${PLUGIN_KEY}/no-handmade-empty-state`]: "error",
|
|
2029
|
+
[`${PLUGIN_KEY}/no-native-title`]: "error",
|
|
2030
|
+
// A warning, never an error (Katerina, 13 September): likeness is a judgement.
|
|
2031
|
+
[`${PLUGIN_KEY}/no-copied-look`]: "warn"
|
|
1615
2032
|
}
|
|
1616
2033
|
};
|
|
1617
2034
|
plugin.configs.strict = {
|
|
@@ -1622,7 +2039,8 @@ plugin.configs.strict = {
|
|
|
1622
2039
|
plugin.configs.package = {
|
|
1623
2040
|
name: "@estiva-app/ui/package",
|
|
1624
2041
|
plugins: { [PLUGIN_KEY]: plugin },
|
|
1625
|
-
|
|
2042
|
+
// Every inward rule blocks, but the copied look, which warns here too (UIG-25).
|
|
2043
|
+
rules: Object.fromEntries(PACKAGE_RULE_IDS.map((id) => [id, id === `${PLUGIN_KEY}/no-copied-look` ? "warn" : "error"]))
|
|
1626
2044
|
};
|
|
1627
2045
|
var index_default = plugin;
|
|
1628
2046
|
function countGates(results, seed = APP_RULE_IDS) {
|
|
@@ -1646,9 +2064,12 @@ function countGates(results, seed = APP_RULE_IDS) {
|
|
|
1646
2064
|
export {
|
|
1647
2065
|
APP_RULE_IDS,
|
|
1648
2066
|
ESCAPE_MARKER,
|
|
2067
|
+
MIN_COVER,
|
|
1649
2068
|
MIN_REASON,
|
|
2069
|
+
MIN_SHARED,
|
|
1650
2070
|
OWNED_BEHAVIOURS,
|
|
1651
2071
|
PACKAGE_RULE_IDS,
|
|
2072
|
+
PARTS_WITH_A_TITLE,
|
|
1652
2073
|
PART_LOOK_PROPS,
|
|
1653
2074
|
PLACEMENT,
|
|
1654
2075
|
PLUGIN_KEY,
|