@estiva-app/ui 0.32.0 → 0.33.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 CHANGED
@@ -64,7 +64,7 @@ export default {
64
64
 
65
65
  The UI Guardrails' rules ship with the package, as an ESLint plugin, so an app
66
66
  gets a new rule with a version bump. Each rule names the component to use
67
- instead. There are five:
67
+ instead. There are six:
68
68
 
69
69
  - `estiva/no-raw-element` refuses a raw interactive element (`<a>`, `<input>`,
70
70
  `<form>`, `<dialog>`…) and names the part to use, or says the package has none yet.
@@ -90,6 +90,11 @@ instead. There are five:
90
90
  `ScrollArea` whose content is only as tall as what is in it, where it sits at
91
91
  the top instead of the middle, following the app's own components into their
92
92
  files to find one.
93
+ - `estiva/no-native-title` refuses a `title` attribute, the browser's own
94
+ tooltip, and names `WithTooltip`. It fires on a plain element and on a part
95
+ that hands `title` on to its element (`<Button title>`); a part whose
96
+ `title` is its own heading (`PARTS_WITH_A_TITLE`) and an `<svg>`'s `<title>`
97
+ element pass.
93
98
 
94
99
  ```js
95
100
  // eslint.config.js — alongside your own rules
@@ -2,6 +2,7 @@ import type { ESLint, Linter } from 'eslint';
2
2
  export { ESCAPE_MARKER, MIN_REASON, SETTINGS_KEY, isEscaped, type EstivaSettings } from './escape';
3
3
  export { OWNED_BEHAVIOURS, type OwnedBehaviour } from './no-rebuilt-behaviour';
4
4
  export { PART_LOOK_PROPS, PLACEMENT } from './no-restyled-part';
5
+ export { PARTS_WITH_A_TITLE } from './no-native-title';
5
6
  /** The name the configs register the plugin under, so every rule id is `estiva/<rule>`. */
6
7
  export declare const PLUGIN_KEY = "estiva";
7
8
  declare const plugin: {
@@ -17,6 +18,7 @@ declare const plugin: {
17
18
  'no-restyled-part': import("eslint").Rule.RuleModule;
18
19
  'no-handmade-header': import("eslint").Rule.RuleModule;
19
20
  'no-handmade-empty-state': import("eslint").Rule.RuleModule;
21
+ 'no-native-title': import("eslint").Rule.RuleModule;
20
22
  'no-raw-element': import("eslint").Rule.RuleModule;
21
23
  'no-rebuilt-behaviour': import("eslint").Rule.RuleModule;
22
24
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAU5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAClG,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAI/D,2FAA2F;AAC3F,eAAO,MAAM,UAAU,WAAW,CAAA;AA6ClC,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;aAGK;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAA;KAAE;CACrE,CAAA;AAEzB,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAA+D,CAAA;AACxF,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,UAAmE,CAAA;AAgChG,eAAe,MAAM,CAAA;AAErB,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACpC;;;OAGG;IACH,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,SAAS,CAmB1G"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAW5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAClG,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAItD,2FAA2F;AAC3F,eAAO,MAAM,UAAU,WAAW,CAAA;AA+ClC,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;aAGK;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAA;KAAE;CACrE,CAAA;AAEzB,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAA+D,CAAA;AACxF,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,UAAmE,CAAA;AAiChG,eAAe,MAAM,CAAA;AAErB,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACpC;;;OAGG;IACH,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,SAAS,CAmB1G"}
@@ -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,45 @@ var noHandmadeHeader = {
984
984
  }
985
985
  };
986
986
 
987
+ // src/eslint/no-native-title.ts
988
+ var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "SectionHeader"];
989
+ var child3 = (node, key) => node[key];
990
+ var nameOf3 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
991
+ var noNativeTitle = {
992
+ meta: {
993
+ type: "problem",
994
+ docs: { description: "A title attribute: the browser's tooltip where WithTooltip is the part" },
995
+ schema: [],
996
+ messages: {
997
+ 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.",
998
+ ...ESCAPE_MESSAGES
999
+ }
1000
+ },
1001
+ create(context) {
1002
+ const parser = context.languageOptions?.parser ?? {};
1003
+ let locals;
1004
+ return {
1005
+ Program(program) {
1006
+ locals = partsOfFile(context.filename, program, parser);
1007
+ },
1008
+ JSXAttribute(ruleNode) {
1009
+ const attribute3 = ruleNode;
1010
+ if (nameOf3(child3(attribute3, "name")) !== "title") return;
1011
+ const opening = attribute3.parent;
1012
+ const tag = child3(opening, "name");
1013
+ const name = nameOf3(tag);
1014
+ if (!(name && /^[a-z]/.test(name))) {
1015
+ const binding = locals && bindingOfTag(tag, locals);
1016
+ if (!binding || binding.props !== "all") return;
1017
+ if (PARTS_WITH_A_TITLE.includes(binding.part)) return;
1018
+ }
1019
+ if (isEscaped(context, opening)) return;
1020
+ context.report({ loc: attribute3.loc, messageId: "native" });
1021
+ }
1022
+ };
1023
+ }
1024
+ };
1025
+
987
1026
  // src/eslint/no-raw-element.ts
988
1027
  var RAW_ELEMENT_PARTS = {
989
1028
  a: { use: "Link", more: " For a chip, `InlineChip`; for a whole card, `Card` with `href`." },
@@ -1267,10 +1306,10 @@ function anchorOf(node) {
1267
1306
  if (node.type === "FunctionDeclaration") {
1268
1307
  return node.parent && /^Export/.test(node.parent.type) ? node.parent : node;
1269
1308
  }
1270
- let child3 = node;
1271
- for (let p = node.parent; p; child3 = p, p = p.parent) {
1309
+ let child4 = node;
1310
+ for (let p = node.parent; p; child4 = p, p = p.parent) {
1272
1311
  if (p.type === "JSXOpeningElement" || p.type === "Property" || p.type === "MethodDefinition" || p.type === "PropertyDefinition") return p;
1273
- if (p.type === "ArrayExpression") return child3;
1312
+ if (p.type === "ArrayExpression") return child4;
1274
1313
  if (p.type !== "BlockStatement" && /(Statement|Declaration)$/.test(p.type)) return p;
1275
1314
  }
1276
1315
  return node;
@@ -1584,7 +1623,8 @@ var appRules = {
1584
1623
  "no-rebuilt-behaviour": noRebuiltBehaviour,
1585
1624
  "no-restyled-part": noRestyledPart,
1586
1625
  "no-handmade-header": noHandmadeHeader,
1587
- "no-handmade-empty-state": noHandmadeEmptyState
1626
+ "no-handmade-empty-state": noHandmadeEmptyState,
1627
+ "no-native-title": noNativeTitle
1588
1628
  };
1589
1629
  var packageRules = {
1590
1630
  "raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
@@ -1593,7 +1633,8 @@ var packageRules = {
1593
1633
  "component-has-a-story": componentHasAStory,
1594
1634
  "no-restyled-part": noRestyledPart,
1595
1635
  "no-handmade-header": noHandmadeHeader,
1596
- "no-handmade-empty-state": noHandmadeEmptyState
1636
+ "no-handmade-empty-state": noHandmadeEmptyState,
1637
+ "no-native-title": noNativeTitle
1597
1638
  };
1598
1639
  var rules = { ...appRules, ...packageRules };
1599
1640
  var plugin = {
@@ -1611,7 +1652,8 @@ plugin.configs.recommended = {
1611
1652
  [`${PLUGIN_KEY}/no-rebuilt-behaviour`]: "error",
1612
1653
  [`${PLUGIN_KEY}/no-restyled-part`]: "error",
1613
1654
  [`${PLUGIN_KEY}/no-handmade-header`]: "error",
1614
- [`${PLUGIN_KEY}/no-handmade-empty-state`]: "error"
1655
+ [`${PLUGIN_KEY}/no-handmade-empty-state`]: "error",
1656
+ [`${PLUGIN_KEY}/no-native-title`]: "error"
1615
1657
  }
1616
1658
  };
1617
1659
  plugin.configs.strict = {
@@ -1649,6 +1691,7 @@ export {
1649
1691
  MIN_REASON,
1650
1692
  OWNED_BEHAVIOURS,
1651
1693
  PACKAGE_RULE_IDS,
1694
+ PARTS_WITH_A_TITLE,
1652
1695
  PART_LOOK_PROPS,
1653
1696
  PLACEMENT,
1654
1697
  PLUGIN_KEY,