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":";
|
|
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.
|
|
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,
|