@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 +6 -1
- package/dist/eslint/index.d.ts +2 -0
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +63 -20
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/no-native-title.d.ts +31 -0
- package/dist/eslint/no-native-title.d.ts.map +1 -0
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/Tooltip.mdx +1 -0
- package/src/eslint/index.test.ts +9 -6
- package/src/eslint/index.ts +8 -3
- package/src/eslint/no-native-title.test.ts +67 -0
- package/src/eslint/no-native-title.ts +72 -0
- package/src/gates/create-app.test.ts +1 -1
- package/src/gates/gates.test.ts +1 -1
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
|
|
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
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/eslint/index.js
CHANGED
|
@@ -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,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
|
|
1271
|
-
for (let p = node.parent; p;
|
|
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
|
|
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,
|