@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.
Files changed (103) hide show
  1. package/README.md +13 -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 +4 -0
  22. package/dist/eslint/index.d.ts.map +1 -1
  23. package/dist/eslint/index.js +449 -28
  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/eslint/no-native-title.d.ts +31 -0
  30. package/dist/eslint/no-native-title.d.ts.map +1 -0
  31. package/dist/gates/index.js +1 -1
  32. package/dist/gates/index.js.map +2 -2
  33. package/dist/index.js +171 -133
  34. package/dist/index.js.map +3 -3
  35. package/dist/looks.d.ts +34 -0
  36. package/dist/looks.d.ts.map +1 -0
  37. package/dist/registry/app-3ALZE6XO.js +10 -0
  38. package/dist/registry/app.d.ts.map +1 -1
  39. package/dist/registry/{build-LGFCCOLR.js → build-NDXMVFQ5.js} +3 -3
  40. package/dist/registry/build.d.ts.map +1 -1
  41. package/dist/registry/{chunk-XCBN2EKW.js → chunk-3WJ7ZZ7Z.js} +8 -4
  42. package/dist/registry/chunk-3WJ7ZZ7Z.js.map +7 -0
  43. package/dist/registry/{chunk-E4JNV7PC.js → chunk-PZO4BVV3.js} +2 -1
  44. package/dist/registry/chunk-PZO4BVV3.js.map +7 -0
  45. package/dist/registry/chunk-XVII7P6G.js +595 -0
  46. package/dist/registry/chunk-XVII7P6G.js.map +7 -0
  47. package/dist/registry/cli.js +3 -3
  48. package/dist/registry/index.js +3 -3
  49. package/dist/registry/schema.d.ts +8 -0
  50. package/dist/registry/schema.d.ts.map +1 -1
  51. package/package.json +1 -1
  52. package/registry.json +347 -90
  53. package/src/AttachmentCard.tsx +2 -0
  54. package/src/Button.tsx +1 -0
  55. package/src/Card.stories.tsx +2 -2
  56. package/src/Card.tsx +1 -0
  57. package/src/Chip.tsx +2 -1
  58. package/src/ChipInput.tsx +4 -2
  59. package/src/CommandPalette.tsx +2 -0
  60. package/src/ContainerHeader.stories.tsx +2 -1
  61. package/src/DialogShell.tsx +1 -0
  62. package/src/Divider.stories.tsx +12 -10
  63. package/src/Lightbox.tsx +1 -0
  64. package/src/Link.stories.tsx +5 -4
  65. package/src/Menu.tsx +3 -1
  66. package/src/NavItem.tsx +1 -0
  67. package/src/PersonTrigger.tsx +1 -0
  68. package/src/Reaction.tsx +3 -1
  69. package/src/ReactionPicker.stories.tsx +3 -2
  70. package/src/ScrollArea.stories.tsx +13 -12
  71. package/src/SearchInput.tsx +5 -4
  72. package/src/SectionLabel.mdx +4 -2
  73. package/src/SectionLabel.stories.tsx +13 -0
  74. package/src/SectionLabel.tsx +19 -2
  75. package/src/Select.tsx +6 -4
  76. package/src/TextInput.tsx +6 -6
  77. package/src/Textarea.tsx +6 -4
  78. package/src/Tooltip.mdx +1 -0
  79. package/src/Tooltip.stories.tsx +3 -2
  80. package/src/Tooltip.tsx +2 -1
  81. package/src/eslint/index.test.ts +16 -9
  82. package/src/eslint/index.ts +19 -6
  83. package/src/eslint/looks-of.ts +302 -0
  84. package/src/eslint/no-copied-look.test.ts +155 -0
  85. package/src/eslint/no-copied-look.ts +203 -0
  86. package/src/eslint/no-native-title.test.ts +67 -0
  87. package/src/eslint/no-native-title.ts +72 -0
  88. package/src/eslint/no-restyled-part.ts +1 -1
  89. package/src/gates/app-checks.ts +1 -1
  90. package/src/gates/create-app.test.ts +1 -1
  91. package/src/gates/gates.test.ts +1 -1
  92. package/src/looks.ts +42 -0
  93. package/src/registry/app.ts +3 -0
  94. package/src/registry/build.ts +8 -0
  95. package/src/registry/registry.test.ts +1 -1
  96. package/src/registry/schema.ts +9 -0
  97. package/dist/registry/app-FXN4EI34.js +0 -10
  98. package/dist/registry/chunk-E4JNV7PC.js.map +0 -7
  99. package/dist/registry/chunk-NJN4MQAM.js +0 -397
  100. package/dist/registry/chunk-NJN4MQAM.js.map +0 -7
  101. package/dist/registry/chunk-XCBN2EKW.js.map +0 -7
  102. /package/dist/registry/{app-FXN4EI34.js.map → app-3ALZE6XO.js.map} +0 -0
  103. /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" });
@@ -920,16 +920,16 @@ function classesOf2(el) {
920
920
  }
921
921
  function meaningful(el) {
922
922
  const out = [];
923
- for (const child3 of el.children) {
924
- if (child3.type === "JSXText") {
925
- if (String(child3.value).trim()) out.push(child3);
926
- } else if (child3.type === "JSXExpressionContainer") {
927
- const e = child3.expression;
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(child3);
932
- } else if (child3.type === "JSXElement" || child3.type === "JSXFragment") out.push(child3);
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 child3 of meaningful(el)) {
941
- if (child3.type === "JSXText") return true;
942
- if (child3.type === "JSXExpressionContainer") {
943
- if (VALUE_EXPRESSIONS.has(child3.expression.type)) return true;
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(child3)) continue;
947
- const tag = tagOf(child3);
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(child3, depth + 1)) return true;
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 child3 = node;
1271
- for (let p = node.parent; p; child3 = p, p = p.parent) {
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 child3;
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
- 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"]))
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,