@estiva-app/ui 0.48.0 → 0.50.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
@@ -79,6 +79,8 @@ instead. There are seven — six block, one warns:
79
79
  `className` or an inner box's class prop — a colour, a text size, a border, a
80
80
  corner, a shadow — and names the part's look props (`PART_LOOK_PROPS`). Only
81
81
  placement goes through (`PLACEMENT`), and `EmptyState` takes no padding.
82
+ A `Form` takes no layout of its own, and sits in no spaced box beside other
83
+ things: what comes before its fields goes inside it.
82
84
  - `estiva/no-handmade-header` refuses a column's header bar drawn by hand: a
83
85
  row first in a column, with its own side padding and a title, where the column
84
86
  fills its height or the row has a bar's height or hairline. It reads the
@@ -102,6 +104,12 @@ instead. There are seven — six block, one warns:
102
104
  `registry.json`, and the app's own exported components; the fix is to use
103
105
  the part. In the package the fix is to write the look once in `src/looks.ts`.
104
106
  A look written once as an exported `…_CLASSES` constant counts as a look too.
107
+ - `estiva/no-field-outside-a-form` refuses a `Field`, `TextInput` or
108
+ `Textarea` with no `Form` around it in its file (the palette's
109
+ `CommandPaletteForm` is one), and names `Form`: without it nothing owns
110
+ sending the field. In a dialog that is `DialogShell` with a `Form`, never a
111
+ field inside `ConfirmDialog`. `EditableText` and `SearchInput` pass; a
112
+ component that draws fields for its caller's form says so with an escape.
105
113
 
106
114
  ```js
107
115
  // 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"}
@@ -218,6 +218,10 @@ var isFieldsLayout = (token) => {
218
218
  const { variants, utility } = splitClass(token);
219
219
  return !variants.some((variant) => REACHING_VARIANT.test(variant)) && FIELDS_LAYOUT.test(utility);
220
220
  };
221
+ var isSpacing = (token) => {
222
+ const { variants, utility } = splitClass(token);
223
+ return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^(?:gap|gap-x|gap-y|space-x|space-y)-/.test(utility);
224
+ };
221
225
  var isPadding = (token) => {
222
226
  const { variants, utility } = splitClass(token);
223
227
  return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^p[xytrblse]?-/.test(utility);
@@ -635,6 +639,7 @@ var noRestyledPart = {
635
639
  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.",
636
640
  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.",
637
641
  formLayout: '{{classes}} lays out the fields of {{where}}. A Form places its own fields, 24px apart (Katerina, 28 September), so every form reads the same: remove it. A form that is not a list of fields (a field and its button, a composer) says `layout="free"` and lays itself out.',
642
+ formSpacedFromOutside: "{{classes}} on the box around `Form` spaces the form from what is beside it, so the gap above its first field is not the form\u2019s 24px. Put what is beside it inside the `Form`, as one block before the fields, and drop the box: the Form puts 24px between them (Katerina, 1 October; MoveDialog in Peek does this).",
638
643
  ...ESCAPE_MESSAGES
639
644
  }
640
645
  },
@@ -688,6 +693,26 @@ var noRestyledPart = {
688
693
  const where = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands its props to \`${binding.part}\`)` : `\`${binding.part}\``;
689
694
  context.report({ loc: at.loc, messageId: "restyledStyle", data: { keys: code([...new Set(keys)]), where, use } });
690
695
  };
696
+ const checkSpacedFromOutside = (opening) => {
697
+ const form = opening.parent;
698
+ const box = form?.parent;
699
+ if (form?.type !== "JSXElement" || box?.type !== "JSXElement") return;
700
+ const beside = children(box, "children").filter((node) => {
701
+ if (!node || node === form) return false;
702
+ if (node.type === "JSXText") return String(node.value).trim() !== "";
703
+ if (node.type === "JSXExpressionContainer") return child(node, "expression")?.type !== "JSXEmptyExpression";
704
+ return true;
705
+ });
706
+ if (!beside.length) return;
707
+ const boxOpening = child(box, "openingElement");
708
+ const spacing = children(boxOpening, "attributes").flatMap((attribute3) => {
709
+ if (attribute3?.type !== "JSXAttribute") return [];
710
+ const prop = nameOf(child(attribute3, "name"));
711
+ return prop === "className" || prop === "bodyClassName" ? classesOf(child(attribute3, "value"), scopeOf).filter(isSpacing) : [];
712
+ });
713
+ if (!spacing.length || escaped(opening)) return;
714
+ context.report({ loc: opening.loc, messageId: "formSpacedFromOutside", data: { classes: code([...new Set(spacing)]) } });
715
+ };
691
716
  return {
692
717
  Program(program) {
693
718
  locals = moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
@@ -696,6 +721,7 @@ var noRestyledPart = {
696
721
  const element = ruleNode;
697
722
  const binding = locals && bindingOfTag(child(element, "name"), locals);
698
723
  if (!binding) return;
724
+ if (binding.part === "Form" && binding.props === "all" && placesItsFields(element)) checkSpacedFromOutside(element);
699
725
  for (const attribute3 of children(element, "attributes")) {
700
726
  if (!attribute3) continue;
701
727
  if (attribute3.type === "JSXAttribute") {
@@ -991,16 +1017,16 @@ function classesOf2(el) {
991
1017
  }
992
1018
  function meaningful(el) {
993
1019
  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;
1020
+ for (const child5 of el.children) {
1021
+ if (child5.type === "JSXText") {
1022
+ if (String(child5.value).trim()) out.push(child5);
1023
+ } else if (child5.type === "JSXExpressionContainer") {
1024
+ const e = child5.expression;
999
1025
  if (e.type === "JSXEmptyExpression") continue;
1000
1026
  if (e.type === "LogicalExpression" && isElement(e.right)) out.push(e.right);
1001
1027
  else if (isElement(e)) out.push(e);
1002
- else out.push(child4);
1003
- } else if (child4.type === "JSXElement" || child4.type === "JSXFragment") out.push(child4);
1028
+ else out.push(child5);
1029
+ } else if (child5.type === "JSXElement" || child5.type === "JSXFragment") out.push(child5);
1004
1030
  }
1005
1031
  return out;
1006
1032
  }
@@ -1008,16 +1034,16 @@ var TEXT_PARTS = /* @__PURE__ */ new Set(["EditableText", "Person", "Breadcrumb"
1008
1034
  var VALUE_EXPRESSIONS = /* @__PURE__ */ new Set(["Identifier", "MemberExpression", "TemplateLiteral", "Literal", "LogicalExpression", "ConditionalExpression", "ChainExpression", "CallExpression"]);
1009
1035
  function carriesTitle(el, depth = 0) {
1010
1036
  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;
1037
+ for (const child5 of meaningful(el)) {
1038
+ if (child5.type === "JSXText") return true;
1039
+ if (child5.type === "JSXExpressionContainer") {
1040
+ if (VALUE_EXPRESSIONS.has(child5.expression.type)) return true;
1015
1041
  continue;
1016
1042
  }
1017
- if (!isElement(child4)) continue;
1018
- const tag = tagOf(child4);
1043
+ if (!isElement(child5)) continue;
1044
+ const tag = tagOf(child5);
1019
1045
  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;
1046
+ if (/^[a-z]/.test(tag) && carriesTitle(child5, depth + 1)) return true;
1021
1047
  }
1022
1048
  return false;
1023
1049
  }
@@ -1439,10 +1465,65 @@ var noCopiedLook = {
1439
1465
  }
1440
1466
  };
1441
1467
 
1442
- // src/eslint/no-native-title.ts
1443
- var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "Panel", "SectionHeader"];
1468
+ // src/eslint/no-field-outside-a-form.ts
1469
+ var FIELD_PARTS = ["Field", "TextInput", "Textarea"];
1470
+ var FORM_PARTS = ["Form", "CommandPaletteForm"];
1444
1471
  var child3 = (node, key) => node[key];
1445
1472
  var nameOf4 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
1473
+ var noFieldOutsideAForm = {
1474
+ meta: {
1475
+ type: "problem",
1476
+ docs: { description: "A field with no Form around it" },
1477
+ schema: [],
1478
+ messages: {
1479
+ 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).",
1480
+ ...ESCAPE_MESSAGES
1481
+ }
1482
+ },
1483
+ create(context) {
1484
+ const parser = context.languageOptions?.parser ?? {};
1485
+ let locals;
1486
+ const loose = [];
1487
+ let drawsAField = false;
1488
+ const partOf = (element) => {
1489
+ const tag = child3(child3(element, "openingElement"), "name");
1490
+ const name = nameOf4(tag);
1491
+ if (!name || /^[a-z]/.test(name)) return void 0;
1492
+ const binding = locals && bindingOfTag(tag, locals);
1493
+ return binding && binding.props === "all" ? binding.part : void 0;
1494
+ };
1495
+ return {
1496
+ Program(program) {
1497
+ locals = partsOfFile(context.filename, program, parser);
1498
+ },
1499
+ JSXElement(ruleNode) {
1500
+ const element = ruleNode;
1501
+ const part = partOf(element);
1502
+ if (!part || !FIELD_PARTS.includes(part)) return;
1503
+ if (part === "Field") drawsAField = true;
1504
+ for (let up = element.parent; up; up = up.parent) {
1505
+ if (up.type !== "JSXElement") continue;
1506
+ const around = partOf(up);
1507
+ if (around && FORM_PARTS.includes(around)) return;
1508
+ if (around === "Field") return;
1509
+ }
1510
+ loose.push({ opening: child3(element, "openingElement"), part });
1511
+ },
1512
+ "Program:exit"() {
1513
+ for (const { opening, part } of loose) {
1514
+ if (part !== "Field" && drawsAField) continue;
1515
+ if (isEscaped(context, opening)) continue;
1516
+ context.report({ loc: opening.loc, messageId: "loose", data: { part } });
1517
+ }
1518
+ }
1519
+ };
1520
+ }
1521
+ };
1522
+
1523
+ // src/eslint/no-native-title.ts
1524
+ var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "Panel", "SectionHeader"];
1525
+ var child4 = (node, key) => node[key];
1526
+ var nameOf5 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
1446
1527
  var noNativeTitle = {
1447
1528
  meta: {
1448
1529
  type: "problem",
@@ -1462,10 +1543,10 @@ var noNativeTitle = {
1462
1543
  },
1463
1544
  JSXAttribute(ruleNode) {
1464
1545
  const attribute3 = ruleNode;
1465
- if (nameOf4(child3(attribute3, "name")) !== "title") return;
1546
+ if (nameOf5(child4(attribute3, "name")) !== "title") return;
1466
1547
  const opening = attribute3.parent;
1467
- const tag = child3(opening, "name");
1468
- const name = nameOf4(tag);
1548
+ const tag = child4(opening, "name");
1549
+ const name = nameOf5(tag);
1469
1550
  if (!(name && /^[a-z]/.test(name))) {
1470
1551
  const binding = locals && bindingOfTag(tag, locals);
1471
1552
  if (!binding || binding.props !== "all") return;
@@ -1789,10 +1870,10 @@ function anchorOf(node) {
1789
1870
  if (node.type === "FunctionDeclaration") {
1790
1871
  return node.parent && /^Export/.test(node.parent.type) ? node.parent : node;
1791
1872
  }
1792
- let child4 = node;
1793
- for (let p = node.parent; p; child4 = p, p = p.parent) {
1873
+ let child5 = node;
1874
+ for (let p = node.parent; p; child5 = p, p = p.parent) {
1794
1875
  if (p.type === "JSXOpeningElement" || p.type === "Property" || p.type === "MethodDefinition" || p.type === "PropertyDefinition") return p;
1795
- if (p.type === "ArrayExpression") return child4;
1876
+ if (p.type === "ArrayExpression") return child5;
1796
1877
  if (p.type !== "BlockStatement" && /(Statement|Declaration)$/.test(p.type)) return p;
1797
1878
  }
1798
1879
  return node;
@@ -2107,7 +2188,8 @@ var APP_RULE_NAMES = [
2107
2188
  "no-handmade-header",
2108
2189
  "no-handmade-empty-state",
2109
2190
  "no-native-title",
2110
- "no-copied-look"
2191
+ "no-copied-look",
2192
+ "no-field-outside-a-form"
2111
2193
  ];
2112
2194
  var APP_RULE_IDS = APP_RULE_NAMES.map((name) => `${PLUGIN_KEY}/${name}`);
2113
2195
 
@@ -2120,7 +2202,8 @@ var appRules = {
2120
2202
  "no-handmade-header": noHandmadeHeader,
2121
2203
  "no-handmade-empty-state": noHandmadeEmptyState,
2122
2204
  "no-native-title": noNativeTitle,
2123
- "no-copied-look": noCopiedLook
2205
+ "no-copied-look": noCopiedLook,
2206
+ "no-field-outside-a-form": noFieldOutsideAForm
2124
2207
  };
2125
2208
  var packageRules = {
2126
2209
  "raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
@@ -2151,7 +2234,8 @@ plugin.configs.recommended = {
2151
2234
  [`${PLUGIN_KEY}/no-handmade-empty-state`]: "error",
2152
2235
  [`${PLUGIN_KEY}/no-native-title`]: "error",
2153
2236
  // A warning, never an error (Katerina, 13 September): likeness is a judgement.
2154
- [`${PLUGIN_KEY}/no-copied-look`]: "warn"
2237
+ [`${PLUGIN_KEY}/no-copied-look`]: "warn",
2238
+ [`${PLUGIN_KEY}/no-field-outside-a-form`]: "error"
2155
2239
  }
2156
2240
  };
2157
2241
  plugin.configs.strict = {
@@ -2187,6 +2271,7 @@ function countGates(results, seed = APP_RULE_IDS) {
2187
2271
  export {
2188
2272
  APP_RULE_IDS,
2189
2273
  ESCAPE_MARKER,
2274
+ FIELD_PARTS,
2190
2275
  MIN_COVER,
2191
2276
  MIN_REASON,
2192
2277
  MIN_SHARED,