@estiva-app/ui 0.35.1 → 0.36.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/dist/eslint/escape.d.ts +23 -6
- package/dist/eslint/escape.d.ts.map +1 -1
- package/dist/eslint/index.js +70 -9
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-raw-element.d.ts +7 -0
- package/dist/eslint/no-raw-element.d.ts.map +1 -1
- package/dist/eslint/no-restyled-part.d.ts +8 -0
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-DRIHHXQK.js → chunk-CC6L4B5U.js} +23 -17
- package/dist/gates/chunk-CC6L4B5U.js.map +7 -0
- package/dist/gates/chunk-SYVKGOWM.js +1131 -0
- package/dist/gates/chunk-SYVKGOWM.js.map +7 -0
- package/dist/gates/ci.d.ts +18 -0
- package/dist/gates/ci.d.ts.map +1 -0
- package/dist/gates/cli.js +20 -3
- package/dist/gates/cli.js.map +2 -2
- package/dist/gates/create-app.d.ts.map +1 -1
- package/dist/gates/create-app.js +1 -1
- package/dist/gates/escapes.d.ts +23 -0
- package/dist/gates/escapes.d.ts.map +1 -0
- package/dist/gates/gate-config.d.ts.map +1 -1
- package/dist/gates/hook.d.ts +2 -0
- package/dist/gates/hook.d.ts.map +1 -1
- package/dist/gates/index.d.ts +5 -1
- package/dist/gates/index.d.ts.map +1 -1
- package/dist/gates/index.js +41 -185
- package/dist/gates/index.js.map +4 -4
- package/dist/gates/name-escapes.d.ts +14 -0
- package/dist/gates/name-escapes.d.ts.map +1 -0
- package/dist/gates/status.d.ts +3 -1
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/gates/token-lint.d.ts.map +1 -1
- package/dist/gates/token-style.d.ts +21 -0
- package/dist/gates/token-style.d.ts.map +1 -0
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/registry/{app-3ALZE6XO.js → app-YMABQII6.js} +3 -3
- package/dist/registry/app.d.ts.map +1 -1
- package/dist/registry/behaviours.d.ts +71 -0
- package/dist/registry/behaviours.d.ts.map +1 -0
- package/dist/registry/{build-NDXMVFQ5.js → build-3EANARLQ.js} +2 -2
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-3WJ7ZZ7Z.js → chunk-EVJW26UR.js} +51 -4
- package/dist/registry/chunk-EVJW26UR.js.map +7 -0
- package/dist/registry/{chunk-XVII7P6G.js → chunk-SIB44CES.js} +276 -90
- package/dist/registry/chunk-SIB44CES.js.map +7 -0
- package/dist/registry/{chunk-KMZIMB3G.js → chunk-XK747STJ.js} +46 -3
- package/dist/registry/chunk-XK747STJ.js.map +7 -0
- package/dist/registry/cli.js +93 -34
- package/dist/registry/cli.js.map +3 -3
- package/dist/registry/contract.d.ts +27 -1
- package/dist/registry/contract.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +11 -5
- package/dist/registry/siblings.d.ts +9 -0
- package/dist/registry/siblings.d.ts.map +1 -1
- package/dist/registry/storymap.d.ts +19 -5
- package/dist/registry/storymap.d.ts.map +1 -1
- package/package.json +4 -1
- package/registry.json +293 -17
- package/skill/estiva-ui.md +2 -1
- package/src/AppShell.mdx +2 -1
- package/src/AttachmentCard.tsx +7 -7
- package/src/Button.tsx +1 -1
- package/src/Card.tsx +2 -2
- package/src/ChipInput.mdx +2 -0
- package/src/ChipInput.tsx +1 -1
- package/src/CommandPalette.tsx +3 -3
- package/src/DialogShell.tsx +1 -1
- package/src/EmptyState.mdx +4 -3
- package/src/Lightbox.tsx +1 -1
- package/src/NavItem.tsx +1 -1
- package/src/PersonTrigger.tsx +1 -1
- package/src/Reaction.tsx +1 -1
- package/src/ScrollArea.mdx +4 -0
- package/src/Select.tsx +1 -1
- package/src/TopBar.mdx +1 -1
- package/src/eslint/escape.ts +43 -14
- package/src/eslint/has-a-page-and-a-story.test.ts +1 -1
- package/src/eslint/index.test.ts +2 -2
- package/src/eslint/no-copied-look.test.ts +1 -1
- package/src/eslint/no-hand-rolled-behaviour.test.ts +3 -3
- package/src/eslint/no-handmade-empty-state.test.ts +2 -2
- package/src/eslint/no-handmade-header.test.ts +2 -2
- package/src/eslint/no-native-title.test.ts +2 -2
- package/src/eslint/no-raw-element.test.ts +47 -15
- package/src/eslint/no-raw-element.ts +53 -3
- package/src/eslint/no-rebuilt-behaviour.test.ts +9 -9
- package/src/eslint/no-restyled-part.test.ts +21 -2
- package/src/eslint/no-restyled-part.ts +30 -0
- package/src/eslint/raw-element-outside-a-wrapper.test.ts +3 -3
- package/src/gates/app-checks.ts +4 -3
- package/src/gates/ci.ts +152 -0
- package/src/gates/cli.ts +19 -3
- package/src/gates/count.ts +1 -1
- package/src/gates/create-app.test.ts +2 -2
- package/src/gates/create-app.ts +22 -16
- package/src/gates/escapes.ts +111 -0
- package/src/gates/gate-config.ts +6 -2
- package/src/gates/gates.test.ts +93 -1
- package/src/gates/hook.ts +44 -6
- package/src/gates/index.ts +5 -1
- package/src/gates/name-escapes.ts +77 -0
- package/src/gates/status.ts +45 -2
- package/src/gates/token-lint.ts +11 -4
- package/src/gates/token-style.ts +79 -0
- package/src/how.test.ts +282 -0
- package/src/pages.test.ts +17 -0
- package/src/registry/app.test.ts +34 -0
- package/src/registry/app.ts +52 -2
- package/src/registry/behaviours.ts +223 -0
- package/src/registry/build.ts +36 -0
- package/src/registry/cli.ts +37 -18
- package/src/registry/contract.test.ts +63 -3
- package/src/registry/contract.ts +75 -2
- package/src/registry/index.ts +1 -1
- package/src/registry/registry.test.ts +14 -0
- package/src/registry/siblings.test.ts +24 -1
- package/src/registry/siblings.ts +23 -0
- package/src/registry/storymap.test.ts +52 -31
- package/src/registry/storymap.ts +76 -37
- package/stories/Choosing.mdx +8 -2
- package/dist/gates/chunk-DRIHHXQK.js.map +0 -7
- package/dist/gates/chunk-GI6M4P7H.js +0 -606
- package/dist/gates/chunk-GI6M4P7H.js.map +0 -7
- package/dist/registry/chunk-3WJ7ZZ7Z.js.map +0 -7
- package/dist/registry/chunk-KMZIMB3G.js.map +0 -7
- package/dist/registry/chunk-XVII7P6G.js.map +0 -7
- /package/dist/registry/{app-3ALZE6XO.js.map → app-YMABQII6.js.map} +0 -0
- /package/dist/registry/{build-NDXMVFQ5.js.map → build-3EANARLQ.js.map} +0 -0
package/dist/eslint/escape.d.ts
CHANGED
|
@@ -3,15 +3,21 @@ import type { AST, Rule } from 'eslint';
|
|
|
3
3
|
* The escape marker (UIG-3, seam S4 in docs/GATES.md §16): one written reason
|
|
4
4
|
* why one element stays as it is.
|
|
5
5
|
*
|
|
6
|
-
* A line comment directly above the element,
|
|
6
|
+
* A line comment directly above the element, naming the rule it keeps off,
|
|
7
7
|
*
|
|
8
|
-
* // @estiva-escape: <reason>
|
|
8
|
+
* // @estiva-escape(no-raw-element): <reason>
|
|
9
9
|
*
|
|
10
10
|
* or, where the element is a JSX child, the JSX comment on the line above,
|
|
11
11
|
*
|
|
12
|
-
* {/* @estiva-escape: <reason> *\/}
|
|
12
|
+
* {/* @estiva-escape(no-raw-element): <reason> *\/}
|
|
13
13
|
*
|
|
14
|
-
* with at least ten characters of reason, spaces not counted.
|
|
14
|
+
* with at least ten characters of reason, spaces not counted. It escapes only
|
|
15
|
+
* the rules it names (Katerina's ruling B3, 25 September: one reason used to
|
|
16
|
+
* switch off every rule on the element, so a raw button that also wrote a
|
|
17
|
+
* dialog's role by hand passed under one reason). Two rules on one element are
|
|
18
|
+
* named together, `@estiva-escape(no-raw-element, no-rebuilt-behaviour)`, each
|
|
19
|
+
* on purpose. A marker that names no rule escapes nothing, and says which name
|
|
20
|
+
* to write.
|
|
15
21
|
*
|
|
16
22
|
* Not `eslint-disable`. A directive switches a rule off without saying it was
|
|
17
23
|
* meant, and the count of escapes (`.gates-count.json`) could not see it. A
|
|
@@ -33,10 +39,21 @@ export declare const SETTINGS_KEY = "estiva";
|
|
|
33
39
|
* reports through the rule that called it.
|
|
34
40
|
*/
|
|
35
41
|
export declare const ESCAPE_MESSAGES: {
|
|
36
|
-
readonly escapeWithoutReason: "An escape needs its reason, at least 10 characters: `// @estiva-escape: <why this stays>`. An escape with no reason hides nothing.";
|
|
37
|
-
readonly escapeInDirective: "Write the escape as its own comment on the line above: `// @estiva-escape: <reason>`. Inside an eslint-disable comment it switches the rule off instead of recording why.";
|
|
42
|
+
readonly escapeWithoutReason: "An escape needs its reason, at least 10 characters: `// @estiva-escape({{rule}}): <why this stays>`. An escape with no reason hides nothing.";
|
|
43
|
+
readonly escapeInDirective: "Write the escape as its own comment on the line above: `// @estiva-escape({{rule}}): <reason>`. Inside an eslint-disable comment it switches the rule off instead of recording why.";
|
|
44
|
+
readonly escapeWithoutRule: "Name the rule this escape keeps off: `@estiva-escape({{rule}}): <reason>`. An escape now keeps off only the rules it names.";
|
|
38
45
|
readonly escaped: "Escaped: {{reason}}";
|
|
39
46
|
};
|
|
47
|
+
/** A rule's name as a marker writes it: `no-raw-element` for `estiva/no-raw-element`. */
|
|
48
|
+
export declare const shortRuleName: (id: string) => string;
|
|
49
|
+
/**
|
|
50
|
+
* What a marker says, read from a comment's text: the rules it names (none when
|
|
51
|
+
* it names none) and its reason. Null when the comment carries no marker.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readMarker(text: string): {
|
|
54
|
+
rules: string[];
|
|
55
|
+
reason: string;
|
|
56
|
+
} | null;
|
|
40
57
|
export interface EstivaSettings {
|
|
41
58
|
/**
|
|
42
59
|
* Report every sanctioned escape as a message with the id `escaped`, so a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/eslint/escape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEvC
|
|
1
|
+
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/eslint/escape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,aAAa,mBAAmB,CAAA;AAE7C,gDAAgD;AAChD,eAAO,MAAM,UAAU,KAAK,CAAA;AAE5B,2DAA2D;AAC3D,eAAO,MAAM,YAAY,WAAW,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;CAKlB,CAAA;AAEV,yFAAyF;AACzF,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,WAAsC,CAAA;AAE9E;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQnF;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,UAAU,OAAO;IACf,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,GAAG,IAAI,CAAA;IAC/B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzB;AAaD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAwC3E"}
|
package/dist/eslint/index.js
CHANGED
|
@@ -9,10 +9,21 @@ var ESCAPE_MARKER = "@estiva-escape";
|
|
|
9
9
|
var MIN_REASON = 10;
|
|
10
10
|
var SETTINGS_KEY = "estiva";
|
|
11
11
|
var ESCAPE_MESSAGES = {
|
|
12
|
-
escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \`// ${ESCAPE_MARKER}: <why this stays>\`. An escape with no reason hides nothing.`,
|
|
13
|
-
escapeInDirective: `Write the escape as its own comment on the line above: \`// ${ESCAPE_MARKER}: <reason>\`. Inside an eslint-disable comment it switches the rule off instead of recording why.`,
|
|
12
|
+
escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \`// ${ESCAPE_MARKER}({{rule}}): <why this stays>\`. An escape with no reason hides nothing.`,
|
|
13
|
+
escapeInDirective: `Write the escape as its own comment on the line above: \`// ${ESCAPE_MARKER}({{rule}}): <reason>\`. Inside an eslint-disable comment it switches the rule off instead of recording why.`,
|
|
14
|
+
escapeWithoutRule: `Name the rule this escape keeps off: \`${ESCAPE_MARKER}({{rule}}): <reason>\`. An escape now keeps off only the rules it names.`,
|
|
14
15
|
escaped: "Escaped: {{reason}}"
|
|
15
16
|
};
|
|
17
|
+
var shortRuleName = (id) => id.slice(id.lastIndexOf("/") + 1);
|
|
18
|
+
function readMarker(text) {
|
|
19
|
+
const at = text.indexOf(ESCAPE_MARKER);
|
|
20
|
+
if (at === -1) return null;
|
|
21
|
+
const rest = text.slice(at + ESCAPE_MARKER.length);
|
|
22
|
+
const named = /^\s*\(([^)]*)\)/.exec(rest);
|
|
23
|
+
const rules2 = named ? named[1].split(/[\s,]+/).filter(Boolean).map(shortRuleName) : [];
|
|
24
|
+
const reason = (named ? rest.slice(named[0].length) : rest).replace(/^\s*:/, "").trim();
|
|
25
|
+
return { rules: rules2, reason };
|
|
26
|
+
}
|
|
16
27
|
var DIRECTIVE = /^\s*eslint-disable(?:-next-line|-line)?(?=\s|$)([^]*)$/;
|
|
17
28
|
function directiveRules(rest) {
|
|
18
29
|
const dashes = rest.search(/(?:^|\s)--(?:\s|$)/);
|
|
@@ -26,18 +37,24 @@ function isEscaped(context, node) {
|
|
|
26
37
|
const nodeStart = node.range[0];
|
|
27
38
|
const comment = sourceCode.getAllComments().find((c) => c.loc && c.range && c.loc.end.line === line - 1 && BETWEEN.test(sourceCode.text.slice(c.range[1], nodeStart)));
|
|
28
39
|
if (!comment?.loc) return false;
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
40
|
+
const marker = readMarker(comment.value);
|
|
41
|
+
if (!marker) return false;
|
|
42
|
+
const rule = shortRuleName(context.id);
|
|
31
43
|
const directive = DIRECTIVE.exec(comment.value);
|
|
32
44
|
if (directive) {
|
|
33
45
|
const rules2 = directiveRules(directive[1]);
|
|
34
46
|
if (rules2.length > 0 && !rules2.includes(context.id)) return false;
|
|
35
|
-
context.report({ loc: comment.loc, messageId: "escapeInDirective" });
|
|
47
|
+
context.report({ loc: comment.loc, messageId: "escapeInDirective", data: { rule } });
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (marker.rules.length > 0 && !marker.rules.includes(rule)) return false;
|
|
51
|
+
if (marker.rules.length === 0) {
|
|
52
|
+
context.report({ loc: comment.loc, messageId: "escapeWithoutRule", data: { rule } });
|
|
36
53
|
return false;
|
|
37
54
|
}
|
|
38
|
-
const reason =
|
|
55
|
+
const { reason } = marker;
|
|
39
56
|
if (reason.replace(/\s/g, "").length < MIN_REASON) {
|
|
40
|
-
context.report({ loc: comment.loc, messageId: "escapeWithoutReason" });
|
|
57
|
+
context.report({ loc: comment.loc, messageId: "escapeWithoutReason", data: { rule } });
|
|
41
58
|
return false;
|
|
42
59
|
}
|
|
43
60
|
const settings = context.settings[SETTINGS_KEY];
|
|
@@ -175,6 +192,7 @@ var PLACEMENT = [
|
|
|
175
192
|
// Draws nothing: it names the part, so what is inside can answer its hover (`group-hover:`).
|
|
176
193
|
{ what: "a name for hover", pattern: /^(?:group|peer)(?:\/[\w-]+)?$/ }
|
|
177
194
|
];
|
|
195
|
+
var PLACEMENT_STYLE = /^(?:margin|padding|inset|scrollMargin)(?:Top|Right|Bottom|Left|Block|Inline|BlockStart|BlockEnd|InlineStart|InlineEnd)?$|^(?:width|height|minWidth|minHeight|maxWidth|maxHeight|inlineSize|blockSize|minInlineSize|minBlockSize|maxInlineSize|maxBlockSize)$|^(?:display|flex|flexGrow|flexShrink|flexBasis|order|gap|rowGap|columnGap|alignSelf|justifySelf|placeSelf|gridArea|gridColumn|gridColumnStart|gridColumnEnd|gridRow|gridRowStart|gridRowEnd)$|^(?:position|top|right|bottom|left|zIndex|transform|translate)$/;
|
|
178
196
|
var REACHING_VARIANT = /^\[|^(?:before|after|placeholder|file|marker|selection|first-letter|first-line|backdrop|\*|\*\*)$/;
|
|
179
197
|
function splitClass(token) {
|
|
180
198
|
const variants = [];
|
|
@@ -606,6 +624,7 @@ var noRestyledPart = {
|
|
|
606
624
|
schema: [],
|
|
607
625
|
messages: {
|
|
608
626
|
restyled: "{{classes}} on {{where}} changes how it looks. A part of @estiva-app/ui is placed from outside, never restyled: only space, size, flex and grid, and position pass in.{{use}} A look for what is around it goes on your own element around it.",
|
|
627
|
+
restyledStyle: "{{keys}} in the `style` of {{where}} changes how it looks. A part of @estiva-app/ui is placed from outside, never restyled, by class or by style: only space, size, flex and grid, and position pass in.{{use}} A look for what is around it goes on your own element around it.",
|
|
609
628
|
emptyStatePadding: "{{classes}} pads {{where}}. EmptyState takes no padding (Katerina, 14 September): its room comes from the box its rows live in, so put the padding on that box.",
|
|
610
629
|
...ESCAPE_MESSAGES
|
|
611
630
|
}
|
|
@@ -639,6 +658,19 @@ var noRestyledPart = {
|
|
|
639
658
|
}
|
|
640
659
|
if (padding.length) context.report({ loc: at.loc, messageId: "emptyStatePadding", data: { classes: code(padding), where } });
|
|
641
660
|
};
|
|
661
|
+
const checkStyle = (element, binding, value, at) => {
|
|
662
|
+
if (binding.props !== "all" || value?.type !== "JSXExpressionContainer") return;
|
|
663
|
+
let object = child(value, "expression");
|
|
664
|
+
if (object?.type === "Identifier") object = constantOf(object, scopeOf(object));
|
|
665
|
+
while (object?.type === "TSAsExpression" || object?.type === "TSSatisfiesExpression") object = child(object, "expression");
|
|
666
|
+
if (object?.type !== "ObjectExpression") return;
|
|
667
|
+
const keys = children(object, "properties").map((property) => property?.type === "Property" && !property.computed ? nameOf(child(property, "key")) : void 0).filter((key) => !!key && !key.startsWith("--") && !PLACEMENT_STYLE.test(key));
|
|
668
|
+
if (!keys.length || escaped(element)) return;
|
|
669
|
+
const props = PART_LOOK_PROPS[binding.part];
|
|
670
|
+
const use = props ? ` Use its ${or(props)}.` : ` \`${binding.part}\` has no prop for this yet: ask Katerina, and it gets made in @estiva-app/ui.`;
|
|
671
|
+
const where = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands its props to \`${binding.part}\`)` : `\`${binding.part}\``;
|
|
672
|
+
context.report({ loc: at.loc, messageId: "restyledStyle", data: { keys: code([...new Set(keys)]), where, use } });
|
|
673
|
+
};
|
|
642
674
|
return {
|
|
643
675
|
Program(program) {
|
|
644
676
|
locals = moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
|
|
@@ -652,6 +684,7 @@ var noRestyledPart = {
|
|
|
652
684
|
if (attribute3.type === "JSXAttribute") {
|
|
653
685
|
const prop = nameOf(child(attribute3, "name"));
|
|
654
686
|
if (prop && CLASS_PROP.test(prop)) check(element, binding, prop, child(attribute3, "value"), attribute3);
|
|
687
|
+
else if (prop === "style") checkStyle(element, binding, child(attribute3, "value"), attribute3);
|
|
655
688
|
continue;
|
|
656
689
|
}
|
|
657
690
|
let spread = child(attribute3, "argument");
|
|
@@ -1452,6 +1485,28 @@ function isOn(element, name) {
|
|
|
1452
1485
|
const attr = attribute2(element, name);
|
|
1453
1486
|
return attr !== void 0 && literal(attr) !== false;
|
|
1454
1487
|
}
|
|
1488
|
+
var CLICK_HANDLERS = ["onClick", "onMouseDown", "onMouseUp", "onPointerDown", "onPointerUp", "onDoubleClick"];
|
|
1489
|
+
function onlyStops(handler) {
|
|
1490
|
+
if (!handler || handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression") return false;
|
|
1491
|
+
const body = handler.body;
|
|
1492
|
+
const calls = body.type === "BlockStatement" ? body.body.map((s) => s.type === "ExpressionStatement" ? s.expression : s) : [body];
|
|
1493
|
+
return calls.length > 0 && calls.every((call) => {
|
|
1494
|
+
if (call.type !== "CallExpression") return false;
|
|
1495
|
+
const callee = call.callee;
|
|
1496
|
+
const property = callee.type === "MemberExpression" ? callee.property : void 0;
|
|
1497
|
+
return property?.type === "Identifier" && (property.name === "stopPropagation" || property.name === "preventDefault");
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
function clickHandler(element) {
|
|
1501
|
+
for (const name of CLICK_HANDLERS) {
|
|
1502
|
+
const attr = attribute2(element, name);
|
|
1503
|
+
if (!attr) continue;
|
|
1504
|
+
const value = attr.value;
|
|
1505
|
+
const handler = value?.type === "JSXExpressionContainer" ? value.expression : void 0;
|
|
1506
|
+
if (!onlyStops(handler)) return name;
|
|
1507
|
+
}
|
|
1508
|
+
return void 0;
|
|
1509
|
+
}
|
|
1455
1510
|
function classify(element) {
|
|
1456
1511
|
if (element.name.type !== "JSXIdentifier" || !element.name.name) return null;
|
|
1457
1512
|
const name = element.name.name;
|
|
@@ -1466,7 +1521,11 @@ function classify(element) {
|
|
|
1466
1521
|
const shown = `<input type="${key}">`;
|
|
1467
1522
|
return part2 ? { report: "raw", element: shown, part: part2 } : { report: "noPart", element: shown };
|
|
1468
1523
|
}
|
|
1469
|
-
if (!Object.hasOwn(RAW_ELEMENT_PARTS, name))
|
|
1524
|
+
if (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) {
|
|
1525
|
+
if (!/^[a-z][a-z0-9]*$/.test(name) || name === "option" || name === "summary") return null;
|
|
1526
|
+
const handler = clickHandler(element);
|
|
1527
|
+
return handler ? { report: "clickable", element: `<${name} ${handler}>` } : null;
|
|
1528
|
+
}
|
|
1470
1529
|
if ((name === "audio" || name === "video") && !isOn(element, "controls")) return null;
|
|
1471
1530
|
if (name === "img" && !attribute2(element, "usemap") && !attribute2(element, "useMap")) return null;
|
|
1472
1531
|
const part = RAW_ELEMENT_PARTS[name];
|
|
@@ -1479,6 +1538,7 @@ var noRawElement = {
|
|
|
1479
1538
|
schema: [],
|
|
1480
1539
|
messages: {
|
|
1481
1540
|
raw: "Use `{{use}}` from @estiva-app/ui instead of a raw {{element}}.{{more}}",
|
|
1541
|
+
clickable: "A clickable {{element}} is a button made by hand: it takes no focus and answers no key. Use `Button` from @estiva-app/ui; for a whole card that opens something, `Card` with `href`. A handler that only stops the click (`e.stopPropagation()`) is fine.",
|
|
1482
1542
|
noPart: "@estiva-app/ui has no part for a raw {{element}} yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.",
|
|
1483
1543
|
...ESCAPE_MESSAGES
|
|
1484
1544
|
}
|
|
@@ -1491,7 +1551,8 @@ var noRawElement = {
|
|
|
1491
1551
|
const found = classify(element);
|
|
1492
1552
|
if (!found) return;
|
|
1493
1553
|
if (isEscaped(context, element)) return;
|
|
1494
|
-
if (found.report === "
|
|
1554
|
+
if (found.report === "clickable") context.report({ loc: element.loc, messageId: "clickable", data: { element: found.element } });
|
|
1555
|
+
else if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
|
|
1495
1556
|
else context.report({ loc: element.loc, messageId: "raw", data: { element: found.element, use: found.part.use, more: found.part.more ?? "" } });
|
|
1496
1557
|
}
|
|
1497
1558
|
};
|