@estiva-app/ui 0.33.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.
Files changed (98) hide show
  1. package/README.md +8 -1
  2. package/dist/AttachmentCard.d.ts.map +1 -1
  3. package/dist/Button.d.ts.map +1 -1
  4. package/dist/Card.d.ts.map +1 -1
  5. package/dist/Chip.d.ts.map +1 -1
  6. package/dist/ChipInput.d.ts.map +1 -1
  7. package/dist/CommandPalette.d.ts.map +1 -1
  8. package/dist/DialogShell.d.ts.map +1 -1
  9. package/dist/Lightbox.d.ts.map +1 -1
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/NavItem.d.ts.map +1 -1
  12. package/dist/PersonTrigger.d.ts.map +1 -1
  13. package/dist/Reaction.d.ts.map +1 -1
  14. package/dist/SearchInput.d.ts.map +1 -1
  15. package/dist/SectionLabel.d.ts +9 -2
  16. package/dist/SectionLabel.d.ts.map +1 -1
  17. package/dist/Select.d.ts.map +1 -1
  18. package/dist/TextInput.d.ts.map +1 -1
  19. package/dist/Textarea.d.ts.map +1 -1
  20. package/dist/Tooltip.d.ts.map +1 -1
  21. package/dist/eslint/index.d.ts +2 -0
  22. package/dist/eslint/index.d.ts.map +1 -1
  23. package/dist/eslint/index.js +392 -14
  24. package/dist/eslint/index.js.map +4 -4
  25. package/dist/eslint/looks-of.d.ts +54 -0
  26. package/dist/eslint/looks-of.d.ts.map +1 -0
  27. package/dist/eslint/no-copied-look.d.ts +3 -0
  28. package/dist/eslint/no-copied-look.d.ts.map +1 -0
  29. package/dist/gates/index.js +1 -1
  30. package/dist/gates/index.js.map +2 -2
  31. package/dist/index.js +171 -133
  32. package/dist/index.js.map +3 -3
  33. package/dist/looks.d.ts +34 -0
  34. package/dist/looks.d.ts.map +1 -0
  35. package/dist/registry/app-3ALZE6XO.js +10 -0
  36. package/dist/registry/app.d.ts.map +1 -1
  37. package/dist/registry/{build-LGFCCOLR.js → build-NDXMVFQ5.js} +3 -3
  38. package/dist/registry/build.d.ts.map +1 -1
  39. package/dist/registry/{chunk-XCBN2EKW.js → chunk-3WJ7ZZ7Z.js} +8 -4
  40. package/dist/registry/chunk-3WJ7ZZ7Z.js.map +7 -0
  41. package/dist/registry/{chunk-E4JNV7PC.js → chunk-PZO4BVV3.js} +2 -1
  42. package/dist/registry/chunk-PZO4BVV3.js.map +7 -0
  43. package/dist/registry/chunk-XVII7P6G.js +595 -0
  44. package/dist/registry/chunk-XVII7P6G.js.map +7 -0
  45. package/dist/registry/cli.js +3 -3
  46. package/dist/registry/index.js +3 -3
  47. package/dist/registry/schema.d.ts +8 -0
  48. package/dist/registry/schema.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/registry.json +347 -90
  51. package/src/AttachmentCard.tsx +2 -0
  52. package/src/Button.tsx +1 -0
  53. package/src/Card.stories.tsx +2 -2
  54. package/src/Card.tsx +1 -0
  55. package/src/Chip.tsx +2 -1
  56. package/src/ChipInput.tsx +4 -2
  57. package/src/CommandPalette.tsx +2 -0
  58. package/src/ContainerHeader.stories.tsx +2 -1
  59. package/src/DialogShell.tsx +1 -0
  60. package/src/Divider.stories.tsx +12 -10
  61. package/src/Lightbox.tsx +1 -0
  62. package/src/Link.stories.tsx +5 -4
  63. package/src/Menu.tsx +3 -1
  64. package/src/NavItem.tsx +1 -0
  65. package/src/PersonTrigger.tsx +1 -0
  66. package/src/Reaction.tsx +3 -1
  67. package/src/ReactionPicker.stories.tsx +3 -2
  68. package/src/ScrollArea.stories.tsx +13 -12
  69. package/src/SearchInput.tsx +5 -4
  70. package/src/SectionLabel.mdx +4 -2
  71. package/src/SectionLabel.stories.tsx +13 -0
  72. package/src/SectionLabel.tsx +19 -2
  73. package/src/Select.tsx +6 -4
  74. package/src/TextInput.tsx +6 -6
  75. package/src/Textarea.tsx +6 -4
  76. package/src/Tooltip.stories.tsx +3 -2
  77. package/src/Tooltip.tsx +2 -1
  78. package/src/eslint/index.test.ts +12 -8
  79. package/src/eslint/index.ts +13 -5
  80. package/src/eslint/looks-of.ts +302 -0
  81. package/src/eslint/no-copied-look.test.ts +155 -0
  82. package/src/eslint/no-copied-look.ts +203 -0
  83. package/src/eslint/no-restyled-part.ts +1 -1
  84. package/src/gates/app-checks.ts +1 -1
  85. package/src/gates/create-app.test.ts +1 -1
  86. package/src/gates/gates.test.ts +1 -1
  87. package/src/looks.ts +42 -0
  88. package/src/registry/app.ts +3 -0
  89. package/src/registry/build.ts +8 -0
  90. package/src/registry/registry.test.ts +1 -1
  91. package/src/registry/schema.ts +9 -0
  92. package/dist/registry/app-FXN4EI34.js +0 -10
  93. package/dist/registry/chunk-E4JNV7PC.js.map +0 -7
  94. package/dist/registry/chunk-NJN4MQAM.js +0 -397
  95. package/dist/registry/chunk-NJN4MQAM.js.map +0 -7
  96. package/dist/registry/chunk-XCBN2EKW.js.map +0 -7
  97. /package/dist/registry/{app-FXN4EI34.js.map → app-3ALZE6XO.js.map} +0 -0
  98. /package/dist/registry/{build-LGFCCOLR.js.map → build-NDXMVFQ5.js.map} +0 -0
@@ -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 words = (text) => typeof text === "string" ? text.split(/\s+/).filter(Boolean) : [];
550
+ const words2 = (text) => typeof text === "string" ? text.split(/\s+/).filter(Boolean) : [];
551
551
  switch (value.type) {
552
552
  case "Literal":
553
- return words(value.value);
553
+ return words2(value.value);
554
554
  case "TemplateLiteral":
555
- return [...children(value, "quasis").flatMap((quasi) => words((quasi?.value).cooked)), ...children(value, "expressions").flatMap((e) => read(e))];
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 ? [] : words(nameOf(child(property, "key")));
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 words = onlyWords(element);
856
- if (!words || words.length > SHORT) return;
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" });
@@ -984,10 +984,381 @@ 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
+
987
1358
  // src/eslint/no-native-title.ts
988
1359
  var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "SectionHeader"];
989
1360
  var child3 = (node, key) => node[key];
990
- var nameOf3 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
1361
+ var nameOf4 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
991
1362
  var noNativeTitle = {
992
1363
  meta: {
993
1364
  type: "problem",
@@ -1007,10 +1378,10 @@ var noNativeTitle = {
1007
1378
  },
1008
1379
  JSXAttribute(ruleNode) {
1009
1380
  const attribute3 = ruleNode;
1010
- if (nameOf3(child3(attribute3, "name")) !== "title") return;
1381
+ if (nameOf4(child3(attribute3, "name")) !== "title") return;
1011
1382
  const opening = attribute3.parent;
1012
1383
  const tag = child3(opening, "name");
1013
- const name = nameOf3(tag);
1384
+ const name = nameOf4(tag);
1014
1385
  if (!(name && /^[a-z]/.test(name))) {
1015
1386
  const binding = locals && bindingOfTag(tag, locals);
1016
1387
  if (!binding || binding.props !== "all") return;
@@ -1624,7 +1995,8 @@ var appRules = {
1624
1995
  "no-restyled-part": noRestyledPart,
1625
1996
  "no-handmade-header": noHandmadeHeader,
1626
1997
  "no-handmade-empty-state": noHandmadeEmptyState,
1627
- "no-native-title": noNativeTitle
1998
+ "no-native-title": noNativeTitle,
1999
+ "no-copied-look": noCopiedLook
1628
2000
  };
1629
2001
  var packageRules = {
1630
2002
  "raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
@@ -1634,7 +2006,8 @@ var packageRules = {
1634
2006
  "no-restyled-part": noRestyledPart,
1635
2007
  "no-handmade-header": noHandmadeHeader,
1636
2008
  "no-handmade-empty-state": noHandmadeEmptyState,
1637
- "no-native-title": noNativeTitle
2009
+ "no-native-title": noNativeTitle,
2010
+ "no-copied-look": noCopiedLook
1638
2011
  };
1639
2012
  var rules = { ...appRules, ...packageRules };
1640
2013
  var plugin = {
@@ -1653,7 +2026,9 @@ plugin.configs.recommended = {
1653
2026
  [`${PLUGIN_KEY}/no-restyled-part`]: "error",
1654
2027
  [`${PLUGIN_KEY}/no-handmade-header`]: "error",
1655
2028
  [`${PLUGIN_KEY}/no-handmade-empty-state`]: "error",
1656
- [`${PLUGIN_KEY}/no-native-title`]: "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"
1657
2032
  }
1658
2033
  };
1659
2034
  plugin.configs.strict = {
@@ -1664,7 +2039,8 @@ plugin.configs.strict = {
1664
2039
  plugin.configs.package = {
1665
2040
  name: "@estiva-app/ui/package",
1666
2041
  plugins: { [PLUGIN_KEY]: plugin },
1667
- rules: Object.fromEntries(PACKAGE_RULE_IDS.map((id) => [id, "error"]))
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"]))
1668
2044
  };
1669
2045
  var index_default = plugin;
1670
2046
  function countGates(results, seed = APP_RULE_IDS) {
@@ -1688,7 +2064,9 @@ function countGates(results, seed = APP_RULE_IDS) {
1688
2064
  export {
1689
2065
  APP_RULE_IDS,
1690
2066
  ESCAPE_MARKER,
2067
+ MIN_COVER,
1691
2068
  MIN_REASON,
2069
+ MIN_SHARED,
1692
2070
  OWNED_BEHAVIOURS,
1693
2071
  PACKAGE_RULE_IDS,
1694
2072
  PARTS_WITH_A_TITLE,