@estiva-app/ui 0.48.0 → 0.49.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
@@ -102,6 +102,12 @@ instead. There are seven — six block, one warns:
102
102
  `registry.json`, and the app's own exported components; the fix is to use
103
103
  the part. In the package the fix is to write the look once in `src/looks.ts`.
104
104
  A look written once as an exported `…_CLASSES` constant counts as a look too.
105
+ - `estiva/no-field-outside-a-form` refuses a `Field`, `TextInput` or
106
+ `Textarea` with no `Form` around it in its file (the palette's
107
+ `CommandPaletteForm` is one), and names `Form`: without it nothing owns
108
+ sending the field. In a dialog that is `DialogShell` with a `Form`, never a
109
+ field inside `ConfirmDialog`. `EditableText` and `SearchInput` pass; a
110
+ component that draws fields for its caller's form says so with an escape.
105
111
 
106
112
  ```js
107
113
  // eslint.config.js — alongside your own rules
@@ -1 +1 @@
1
- {"version":3,"file":"DialogShell.d.ts","sourceRoot":"","sources":["../src/DialogShell.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAS9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,gBAAgB;IAC/B;8CAC0C;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;yDACqD;IACrD,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB;uCACmC;IACnC,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;IACnB,0GAA0G;IAC1G,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,KAAW,EAAE,KAAa,EAAE,EAAE,gBAAgB,+BAuG1J"}
1
+ {"version":3,"file":"DialogShell.d.ts","sourceRoot":"","sources":["../src/DialogShell.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAS9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,gBAAgB;IAC/B;8CAC0C;IAC1C,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB;yDACqD;IACrD,aAAa,CAAC,EAAE,SAAS,CAAA;IACzB;uCACmC;IACnC,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;IACnB,0GAA0G;IAC1G,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,KAAW,EAAE,KAAa,EAAE,EAAE,gBAAgB,+BAyG1J"}
@@ -5,6 +5,7 @@ export { OWNED_BEHAVIOURS, type OwnedBehaviour } from './no-rebuilt-behaviour';
5
5
  export { PART_LOOK_PROPS, PLACEMENT } from './no-restyled-part';
6
6
  export { PARTS_WITH_A_TITLE } from './no-native-title';
7
7
  export { MIN_COVER, MIN_SHARED } from './looks-of';
8
+ export { FIELD_PARTS } from './no-field-outside-a-form';
8
9
  export { APP_RULE_IDS, PLUGIN_KEY };
9
10
  declare const plugin: {
10
11
  meta: {
@@ -23,6 +24,7 @@ declare const plugin: {
23
24
  'no-copied-look': import("eslint").Rule.RuleModule;
24
25
  'no-raw-element': import("eslint").Rule.RuleModule;
25
26
  'no-rebuilt-behaviour': import("eslint").Rule.RuleModule;
27
+ 'no-field-outside-a-form': import("eslint").Rule.RuleModule;
26
28
  };
27
29
  configs: {
28
30
  recommended: Linter.Config;
@@ -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;AAW5C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAoB,MAAM,YAAY,CAAA;AAEvE,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;AACtD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAIlD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,CAAA;AAoDnC,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,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,UAAmE,CAAA;AAoChG,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;AAY5C,OAAO,EAAE,YAAY,EAAE,UAAU,EAAoB,MAAM,YAAY,CAAA;AAEvE,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;AACtD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAIvD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,CAAA;AAqDnC,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,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,UAAmE,CAAA;AAqChG,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"}
@@ -991,16 +991,16 @@ function classesOf2(el) {
991
991
  }
992
992
  function meaningful(el) {
993
993
  const out = [];
994
- for (const child4 of el.children) {
995
- if (child4.type === "JSXText") {
996
- if (String(child4.value).trim()) out.push(child4);
997
- } else if (child4.type === "JSXExpressionContainer") {
998
- const e = child4.expression;
994
+ for (const child5 of el.children) {
995
+ if (child5.type === "JSXText") {
996
+ if (String(child5.value).trim()) out.push(child5);
997
+ } else if (child5.type === "JSXExpressionContainer") {
998
+ const e = child5.expression;
999
999
  if (e.type === "JSXEmptyExpression") continue;
1000
1000
  if (e.type === "LogicalExpression" && isElement(e.right)) out.push(e.right);
1001
1001
  else if (isElement(e)) out.push(e);
1002
- else out.push(child4);
1003
- } else if (child4.type === "JSXElement" || child4.type === "JSXFragment") out.push(child4);
1002
+ else out.push(child5);
1003
+ } else if (child5.type === "JSXElement" || child5.type === "JSXFragment") out.push(child5);
1004
1004
  }
1005
1005
  return out;
1006
1006
  }
@@ -1008,16 +1008,16 @@ var TEXT_PARTS = /* @__PURE__ */ new Set(["EditableText", "Person", "Breadcrumb"
1008
1008
  var VALUE_EXPRESSIONS = /* @__PURE__ */ new Set(["Identifier", "MemberExpression", "TemplateLiteral", "Literal", "LogicalExpression", "ConditionalExpression", "ChainExpression", "CallExpression"]);
1009
1009
  function carriesTitle(el, depth = 0) {
1010
1010
  if (depth > 3) return false;
1011
- for (const child4 of meaningful(el)) {
1012
- if (child4.type === "JSXText") return true;
1013
- if (child4.type === "JSXExpressionContainer") {
1014
- if (VALUE_EXPRESSIONS.has(child4.expression.type)) return true;
1011
+ for (const child5 of meaningful(el)) {
1012
+ if (child5.type === "JSXText") return true;
1013
+ if (child5.type === "JSXExpressionContainer") {
1014
+ if (VALUE_EXPRESSIONS.has(child5.expression.type)) return true;
1015
1015
  continue;
1016
1016
  }
1017
- if (!isElement(child4)) continue;
1018
- const tag = tagOf(child4);
1017
+ if (!isElement(child5)) continue;
1018
+ const tag = tagOf(child5);
1019
1019
  if (/^h[1-6]$/.test(tag) || TEXT_PARTS.has(tag) || /\.(?:Title|Heading)$/.test(tag)) return true;
1020
- if (/^[a-z]/.test(tag) && carriesTitle(child4, depth + 1)) return true;
1020
+ if (/^[a-z]/.test(tag) && carriesTitle(child5, depth + 1)) return true;
1021
1021
  }
1022
1022
  return false;
1023
1023
  }
@@ -1439,10 +1439,65 @@ var noCopiedLook = {
1439
1439
  }
1440
1440
  };
1441
1441
 
1442
- // src/eslint/no-native-title.ts
1443
- var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "Panel", "SectionHeader"];
1442
+ // src/eslint/no-field-outside-a-form.ts
1443
+ var FIELD_PARTS = ["Field", "TextInput", "Textarea"];
1444
+ var FORM_PARTS = ["Form", "CommandPaletteForm"];
1444
1445
  var child3 = (node, key) => node[key];
1445
1446
  var nameOf4 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
1447
+ var noFieldOutsideAForm = {
1448
+ meta: {
1449
+ type: "problem",
1450
+ docs: { description: "A field with no Form around it" },
1451
+ schema: [],
1452
+ messages: {
1453
+ loose: "`{{part}}` has no `Form` around it, so nothing owns sending it: Enter and Ctrl+Enter, `busy`, and focus after. Put it in `Form` from @estiva-app/ui; in a dialog, that is `DialogShell` with a `Form`, never `ConfirmDialog` (Primitives/Form says how).",
1454
+ ...ESCAPE_MESSAGES
1455
+ }
1456
+ },
1457
+ create(context) {
1458
+ const parser = context.languageOptions?.parser ?? {};
1459
+ let locals;
1460
+ const loose = [];
1461
+ let drawsAField = false;
1462
+ const partOf = (element) => {
1463
+ const tag = child3(child3(element, "openingElement"), "name");
1464
+ const name = nameOf4(tag);
1465
+ if (!name || /^[a-z]/.test(name)) return void 0;
1466
+ const binding = locals && bindingOfTag(tag, locals);
1467
+ return binding && binding.props === "all" ? binding.part : void 0;
1468
+ };
1469
+ return {
1470
+ Program(program) {
1471
+ locals = partsOfFile(context.filename, program, parser);
1472
+ },
1473
+ JSXElement(ruleNode) {
1474
+ const element = ruleNode;
1475
+ const part = partOf(element);
1476
+ if (!part || !FIELD_PARTS.includes(part)) return;
1477
+ if (part === "Field") drawsAField = true;
1478
+ for (let up = element.parent; up; up = up.parent) {
1479
+ if (up.type !== "JSXElement") continue;
1480
+ const around = partOf(up);
1481
+ if (around && FORM_PARTS.includes(around)) return;
1482
+ if (around === "Field") return;
1483
+ }
1484
+ loose.push({ opening: child3(element, "openingElement"), part });
1485
+ },
1486
+ "Program:exit"() {
1487
+ for (const { opening, part } of loose) {
1488
+ if (part !== "Field" && drawsAField) continue;
1489
+ if (isEscaped(context, opening)) continue;
1490
+ context.report({ loc: opening.loc, messageId: "loose", data: { part } });
1491
+ }
1492
+ }
1493
+ };
1494
+ }
1495
+ };
1496
+
1497
+ // src/eslint/no-native-title.ts
1498
+ var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "Panel", "SectionHeader"];
1499
+ var child4 = (node, key) => node[key];
1500
+ var nameOf5 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
1446
1501
  var noNativeTitle = {
1447
1502
  meta: {
1448
1503
  type: "problem",
@@ -1462,10 +1517,10 @@ var noNativeTitle = {
1462
1517
  },
1463
1518
  JSXAttribute(ruleNode) {
1464
1519
  const attribute3 = ruleNode;
1465
- if (nameOf4(child3(attribute3, "name")) !== "title") return;
1520
+ if (nameOf5(child4(attribute3, "name")) !== "title") return;
1466
1521
  const opening = attribute3.parent;
1467
- const tag = child3(opening, "name");
1468
- const name = nameOf4(tag);
1522
+ const tag = child4(opening, "name");
1523
+ const name = nameOf5(tag);
1469
1524
  if (!(name && /^[a-z]/.test(name))) {
1470
1525
  const binding = locals && bindingOfTag(tag, locals);
1471
1526
  if (!binding || binding.props !== "all") return;
@@ -1789,10 +1844,10 @@ function anchorOf(node) {
1789
1844
  if (node.type === "FunctionDeclaration") {
1790
1845
  return node.parent && /^Export/.test(node.parent.type) ? node.parent : node;
1791
1846
  }
1792
- let child4 = node;
1793
- for (let p = node.parent; p; child4 = p, p = p.parent) {
1847
+ let child5 = node;
1848
+ for (let p = node.parent; p; child5 = p, p = p.parent) {
1794
1849
  if (p.type === "JSXOpeningElement" || p.type === "Property" || p.type === "MethodDefinition" || p.type === "PropertyDefinition") return p;
1795
- if (p.type === "ArrayExpression") return child4;
1850
+ if (p.type === "ArrayExpression") return child5;
1796
1851
  if (p.type !== "BlockStatement" && /(Statement|Declaration)$/.test(p.type)) return p;
1797
1852
  }
1798
1853
  return node;
@@ -2107,7 +2162,8 @@ var APP_RULE_NAMES = [
2107
2162
  "no-handmade-header",
2108
2163
  "no-handmade-empty-state",
2109
2164
  "no-native-title",
2110
- "no-copied-look"
2165
+ "no-copied-look",
2166
+ "no-field-outside-a-form"
2111
2167
  ];
2112
2168
  var APP_RULE_IDS = APP_RULE_NAMES.map((name) => `${PLUGIN_KEY}/${name}`);
2113
2169
 
@@ -2120,7 +2176,8 @@ var appRules = {
2120
2176
  "no-handmade-header": noHandmadeHeader,
2121
2177
  "no-handmade-empty-state": noHandmadeEmptyState,
2122
2178
  "no-native-title": noNativeTitle,
2123
- "no-copied-look": noCopiedLook
2179
+ "no-copied-look": noCopiedLook,
2180
+ "no-field-outside-a-form": noFieldOutsideAForm
2124
2181
  };
2125
2182
  var packageRules = {
2126
2183
  "raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
@@ -2151,7 +2208,8 @@ plugin.configs.recommended = {
2151
2208
  [`${PLUGIN_KEY}/no-handmade-empty-state`]: "error",
2152
2209
  [`${PLUGIN_KEY}/no-native-title`]: "error",
2153
2210
  // A warning, never an error (Katerina, 13 September): likeness is a judgement.
2154
- [`${PLUGIN_KEY}/no-copied-look`]: "warn"
2211
+ [`${PLUGIN_KEY}/no-copied-look`]: "warn",
2212
+ [`${PLUGIN_KEY}/no-field-outside-a-form`]: "error"
2155
2213
  }
2156
2214
  };
2157
2215
  plugin.configs.strict = {
@@ -2187,6 +2245,7 @@ function countGates(results, seed = APP_RULE_IDS) {
2187
2245
  export {
2188
2246
  APP_RULE_IDS,
2189
2247
  ESCAPE_MARKER,
2248
+ FIELD_PARTS,
2190
2249
  MIN_COVER,
2191
2250
  MIN_REASON,
2192
2251
  MIN_SHARED,