cursedbelt 5.75.0 → 5.76.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.
@@ -1 +1 @@
1
- {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AAuPA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AAgwCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AAw0BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AA0FD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAgCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAmGhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
1
+ {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AA6PA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AAgwCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AAg4BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AA0FD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAiCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAmGhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.75.0",
3
+ "version": "5.76.0",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -1066,6 +1066,53 @@ describe('no-native-select — the OS draws its list (owner, 2026-10-10)', () =>
1066
1066
  });
1067
1067
  });
1068
1068
 
1069
+ describe('typed-field-outside-a-form — Enter submits (owner, 2026-10-11; forge task 3043)', () => {
1070
+ // vault's Settings before 3043: a password beside a click-only button, no <form> anywhere.
1071
+ const clickOnly = `<SecretInput value={pw} onChange={setPw} label="Master password" />
1072
+ <Input value={hint} onChange={(e) => setHint(e.target.value)} />
1073
+ <Button disabled={!pw} onClick={onSave}>Save</Button>`;
1074
+
1075
+ test('flags each typed field in a file where nothing handles Enter', () => {
1076
+ expect(check('typed-field-outside-a-form', clickOnly)).toHaveLength(2);
1077
+ expect(check('typed-field-outside-a-form', `<input value={v} onChange={set} />\n<OtpInput length={6} />`)).toHaveLength(2);
1078
+ });
1079
+
1080
+ test('a form, a submit or Enter handler, a self-submitting code or an autosave answers it', () => {
1081
+ for (const handled of [
1082
+ `<form className="contents" onSubmit={submit}>${clickOnly}</form>`,
1083
+ `${clickOnly}\n<Wizard onSubmit={finish} />`,
1084
+ `<SecretInput value={pw} onEnter={unlock} />`,
1085
+ `<OtpInput length={6} onComplete={verify} />`,
1086
+ `<Input onKeyDown={(e) => { if (e.key === "Enter") save(); }} />`,
1087
+ `const autosave = useAutosave<Draft>({ save });\n<Input value={title} />`,
1088
+ ]) {
1089
+ expect(check('typed-field-outside-a-form', handled)).toHaveLength(0);
1090
+ }
1091
+ });
1092
+
1093
+ test('a field with nothing to submit is not one, and a comment naming one is not one', () => {
1094
+ expect(check('typed-field-outside-a-form', `<Input type="search" value={q} onChange={setQ} />`)).toHaveLength(0);
1095
+ expect(check('typed-field-outside-a-form', `<input type="checkbox" checked={on} />\n<input type={"range"} />`)).toHaveLength(0);
1096
+ expect(check('typed-field-outside-a-form', `<Input value={url} readOnly />`)).toHaveLength(0);
1097
+ expect(check('typed-field-outside-a-form', `// a <Input> used to sit here\n<Button onClick={go}>Go</Button>`)).toHaveLength(0);
1098
+ // `readOnly={false}` is an editable field.
1099
+ expect(check('typed-field-outside-a-form', `<Input value={v} readOnly={false} />`)).toHaveLength(1);
1100
+ });
1101
+
1102
+ test('a field whose form is in its parent says so', () => {
1103
+ const source = `{/* guardrails-ignore typed-field-outside-a-form: rendered inside AccountForm's <form> */}
1104
+ <SecretInput value={pw} onChange={setPw} />`;
1105
+ expect(check('typed-field-outside-a-form', source)).toHaveLength(0);
1106
+ });
1107
+
1108
+ test('specs and the kit\'s own components are fixtures, not screens', () => {
1109
+ expect(RULES['typed-field-outside-a-form'].check('src/App.spec.tsx', clickOnly)).toHaveLength(0);
1110
+ expect(RULES['typed-field-outside-a-form'].check('libs/cursedbelt/src/react/components/SecretInput.tsx', clickOnly)).toHaveLength(0);
1111
+ expect(RULES['typed-field-outside-a-form'].check('src/server/page.ts', clickOnly)).toHaveLength(0);
1112
+ expect(APP_RULES).toContain('typed-field-outside-a-form');
1113
+ });
1114
+ });
1115
+
1069
1116
  describe('no-native-dialog', () => {
1070
1117
  test('flags each of the three native dialogs', () => {
1071
1118
  expect(check('no-native-dialog', `if (!window.confirm("sure?")) return;`)).toHaveLength(1);
@@ -1292,6 +1339,8 @@ describe('--all-app-rules (task 2219: a new belt rule reaches every app by itsel
1292
1339
  const rows: Record<string, number> = Object.fromEntries(APP_RULES.map((name) => [name, 0]));
1293
1340
  rows['no-hardcoded-colors'] = 2;
1294
1341
  rows['no-raw-inputs'] = 1;
1342
+ // The same <input>, with no form in its file: Enter has nothing to submit (forge task 3043).
1343
+ rows['typed-field-outside-a-form'] = 1;
1295
1344
  for (const [name, count] of Object.entries(overrides)) {
1296
1345
  if (count === undefined) delete rows[name];
1297
1346
  else rows[name] = count;
@@ -118,6 +118,12 @@
118
118
  // `PropertyPicker` / `ChoiceMenu` instead; a use where the OS picker is genuinely better says
119
119
  // why with `// guardrails-ignore no-native-select: <why>` on the line above. Ratcheted per app.
120
120
  //
121
+ // typed-field-outside-a-form — owner, 2026-10-11: Enter submits. A typed field in a file with no
122
+ // `<form` / `onSubmit` / `onEnter` / `onComplete` / `"Enter"` handler is a field beside a
123
+ // click-only button. Search, checkbox-like and read-only inputs are not matched; a field whose
124
+ // form is in its parent says so with `// guardrails-ignore typed-field-outside-a-form: <why>`.
125
+ // Ratcheted per app.
126
+ //
121
127
  // no-native-dialog — DESIGN.md core mandate: no `window.confirm` / `window.prompt` /
122
128
  // `window.alert`. The browser draws them outside the page, so no stylesheet reaches
123
129
  // them; they block the main thread; a browser can SUPPRESS a repeat dialog, after
@@ -2062,6 +2068,62 @@ const noNativeSelect: Rule = {
2062
2068
  },
2063
2069
  };
2064
2070
 
2071
+ // ── typed-field-outside-a-form ──────────────────────────────────────────────
2072
+ // The owner, 2026-10-11, on auth's emailed code: *"I entered an email code and pressed enter key
2073
+ // and nothing happened … Make sure other pages like the email password form let's hitting enter
2074
+ // submit it too as applicable."* Auth was fixed in auth e958057 and vault's Settings in forge task
2075
+ // 3043 — both the same shape: a typed field beside a click-only button, with no `<form>`, so Enter
2076
+ // (the browser's implicit submission) had nothing to submit.
2077
+ //
2078
+ // A file with a typed field — `<input>`, `<Input`, `<SecretInput`, `<OtpInput` — must handle Enter
2079
+ // SOMEWHERE: a `<form`, an `onSubmit=`, an `onEnter=`, an `onComplete=` (OtpInput submits itself
2080
+ // at its last digit), a key handler naming `"Enter"`, or a `useAutosave(` — an editor that saves
2081
+ // as you type has nothing to submit (vault's EntryEditor, forge task 3016.5). It is FILE-scoped on purpose: a field
2082
+ // whose form is in its parent file cannot be seen from here, so it would take an opt-out, and the
2083
+ // rule has to be quiet enough that an opt-out means something. Fields with nothing to submit are
2084
+ // not matched — `type="search"` (a live filter), a checkbox/radio/range/color/file/hidden input,
2085
+ // and a `readOnly` one. A field genuinely without an action says why on the line above:
2086
+ // `// guardrails-ignore typed-field-outside-a-form: <why>`. Ratcheted per app.
2087
+ const TYPED_FIELD_RE = /<(?:input|Input|SecretInput|OtpInput)\b/g;
2088
+ const ENTER_HANDLED_RE = /<form\b|\bonSubmit\s*=|\bonEnter\s*=|\bonComplete\s*=|["'`]Enter["'`]|\buseAutosave\s*(?:<[^>]*>\s*)?\(/;
2089
+ const NOTHING_TO_SUBMIT_RE =
2090
+ /\btype\s*=\s*(?:\{\s*)?["'`](?:search|checkbox|radio|range|color|file|hidden|button|submit|reset|image)["'`]|\breadOnly\b(?!\s*=\s*\{\s*false\s*\})/;
2091
+ const TYPED_FIELD_IGNORE_RE = /guardrails-ignore\s+typed-field-outside-a-form\s*:/;
2092
+
2093
+ /** The attribute text of the JSX tag opening at `start` — up to its `>`, skipping `{…}` expressions. */
2094
+ function tagText(source: string, start: number): string {
2095
+ let depth = 0;
2096
+ for (let i = start + 1; i < source.length && i < start + 2000; i++) {
2097
+ const c = source[i];
2098
+ if (c === "{") depth++;
2099
+ else if (c === "}") depth--;
2100
+ else if (c === ">" && depth === 0) return source.slice(start, i + 1);
2101
+ }
2102
+ return source.slice(start, start + 2000);
2103
+ }
2104
+
2105
+ const typedFieldOutsideAForm: Rule = {
2106
+ name: "typed-field-outside-a-form",
2107
+ description:
2108
+ "Owner, 2026-10-11: Enter must submit. A typed field (`<input>`, `Input`, `SecretInput`, `OtpInput`) in a file with no `<form`, `onSubmit`, `onEnter`, `onComplete` or `\"Enter\"` key handler is a field beside a click-only button, where Enter does nothing. Wrap the field and its button in `<form onSubmit>` (`className=\"contents\"` keeps a flex/grid parent's layout) and make the button `type=\"submit\"`. A field whose form is in its parent, or that truly has no action, says why on the line above: `// guardrails-ignore typed-field-outside-a-form: <why>`.",
2109
+ check(filePath, source) {
2110
+ const unix = filePath.replace(/\\/g, "/");
2111
+ if (/(^|\/)(?:packages|libs)\/cursedbelt\/src\/react\/components\//.test(unix)) return [];
2112
+ if (isTestSource(unix) || !/\.[jt]sx$/.test(unix)) return [];
2113
+ const scannable = blankComments(source);
2114
+ if (ENTER_HANDLED_RE.test(scannable)) return [];
2115
+ const lines = source.split("\n");
2116
+ const out: Violation[] = [];
2117
+ for (const match of scannable.matchAll(TYPED_FIELD_RE)) {
2118
+ if (NOTHING_TO_SUBMIT_RE.test(tagText(scannable, match.index ?? 0))) continue;
2119
+ const line = lineOf(source, match.index ?? 0);
2120
+ if (TYPED_FIELD_IGNORE_RE.test(lines[line - 1] ?? "") || TYPED_FIELD_IGNORE_RE.test(blockAbove(lines, line))) continue;
2121
+ out.push({ file: filePath, line, snippet: lines[line - 1]?.trim() ?? "" });
2122
+ }
2123
+ return out;
2124
+ },
2125
+ };
2126
+
2065
2127
  // ── no-native-dialog ────────────────────────────────────────────────────────
2066
2128
  // `window.confirm` / `window.prompt` / `window.alert` — and their bare global
2067
2129
  // forms — are the browser's own chrome, and cursedbelt has shipped styled,
@@ -2966,6 +3028,7 @@ export const RULES: Record<string, Rule> = {
2966
3028
  [noSelectAsActionMenu.name]: noSelectAsActionMenu,
2967
3029
  [noNativeDialog.name]: noNativeDialog,
2968
3030
  [noNativeSelect.name]: noNativeSelect,
3031
+ [typedFieldOutsideAForm.name]: typedFieldOutsideAForm,
2969
3032
  [noFastTooltip.name]: noFastTooltip,
2970
3033
  [noRawPassword.name]: noRawPassword,
2971
3034
  [noRawNav.name]: noRawNav,