cursedbelt 4.6.0 → 5.0.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 +36 -0
- package/dist/react/chat/index.d.ts +1 -0
- package/dist/react/chat/index.d.ts.map +1 -1
- package/dist/react/chat/index.js +1 -0
- package/dist/react/chat/index.js.map +1 -1
- package/dist/react/chat/usePresence.d.ts +75 -0
- package/dist/react/chat/usePresence.d.ts.map +1 -0
- package/dist/react/chat/usePresence.js +109 -0
- package/dist/react/chat/usePresence.js.map +1 -0
- package/dist/react/components/SecretInput.d.ts +7 -2
- package/dist/react/components/SecretInput.d.ts.map +1 -1
- package/dist/react/components/SecretInput.js +5 -2
- package/dist/react/components/SecretInput.js.map +1 -1
- package/dist/react/components/TooltipImpl.d.ts.map +1 -1
- package/dist/react/components/TooltipImpl.js +52 -3
- package/dist/react/components/TooltipImpl.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +19 -1
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/master-lock/MasterLockDialog.d.ts.map +1 -1
- package/dist/react/master-lock/MasterLockDialog.js +2 -1
- package/dist/react/master-lock/MasterLockDialog.js.map +1 -1
- package/dist/react/master-lock/MasterLockProvider.d.ts +12 -18
- package/dist/react/master-lock/MasterLockProvider.d.ts.map +1 -1
- package/dist/react/master-lock/MasterLockProvider.js +14 -20
- package/dist/react/master-lock/MasterLockProvider.js.map +1 -1
- package/dist/react/media/MediaUploadControl.d.ts +3 -7
- package/dist/react/media/MediaUploadControl.d.ts.map +1 -1
- package/dist/react/media/MediaUploadControl.js +4 -13
- package/dist/react/media/MediaUploadControl.js.map +1 -1
- package/dist/react/media/fileUploadClient.d.ts +0 -8
- package/dist/react/media/fileUploadClient.d.ts.map +1 -1
- package/dist/react/media/fileUploadClient.js +0 -8
- package/dist/react/media/fileUploadClient.js.map +1 -1
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +4 -2
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/index.d.ts +0 -4
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +0 -4
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/sessionAuth.d.ts +1 -2
- package/dist/react/sessionAuth.d.ts.map +1 -1
- package/dist/react/sessionAuth.js +2 -3
- package/dist/react/sessionAuth.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/styles-areas/activity-bar.css +1 -1
- package/dist/styles-areas/auth.css +1 -1
- package/dist/styles-areas/charts.css +1 -1
- package/dist/styles-areas/chat.css +1 -1
- package/dist/styles-areas/core.css +1 -1
- package/dist/styles-areas/data-table.css +1 -1
- package/dist/styles-areas/disk-usage.css +1 -1
- package/dist/styles-areas/fields.css +1 -1
- package/dist/styles-areas/file-tree.css +1 -1
- package/dist/styles-areas/master-detail.css +1 -1
- package/dist/styles-areas/master-lock.css +108 -2
- package/dist/styles-areas/media-gallery.css +1 -1
- package/dist/styles-areas/media.css +1 -17
- package/dist/styles-areas/nav.css +1 -1
- package/dist/styles-areas/notifications.css +1 -1
- package/dist/styles-areas/spreadsheet.css +1 -1
- package/dist/styles-areas/stats.css +1 -1
- package/dist/styles-areas/wizard.css +1 -1
- package/dist/styles-areas/workbook-viewer.css +1 -1
- package/dist/styles-utilities.css +0 -16
- package/dist/testing/_noBun.d.ts +2 -0
- package/dist/testing/_noBun.d.ts.map +1 -0
- package/dist/testing/_noBun.js +10 -0
- package/dist/testing/_noBun.js.map +1 -0
- package/package.json +42 -19
- package/scripts/checkAreaStyles.spec.ts +71 -0
- package/scripts/checkAreaStyles.ts +150 -0
- package/scripts/guardrailsEnforce.spec.ts +43 -0
- package/scripts/guardrailsEnforce.ts +46 -1
- package/scripts/styleAreas.ts +1 -2
- package/src/barrelsReachNoOptionalPeer.spec.ts +6 -3
- package/src/bundlerOnlyEntries.spec.ts +101 -0
- package/src/declaredImports.spec.ts +4 -0
- package/src/optionalPeersTable.spec.ts +127 -0
- package/src/react/chat/index.ts +8 -0
- package/src/react/chat/usePresence.spec.tsx +138 -0
- package/src/react/chat/usePresence.ts +185 -0
- package/src/react/components/SecretInput.tsx +12 -1
- package/src/react/components/TooltipImpl.tsx +54 -5
- package/src/react/file-tree/FileTree.tsx +19 -1
- package/src/react/file-tree/fileTreeThumbs.spec.tsx +90 -0
- package/src/react/index.ts +8 -0
- package/src/react/master-lock/MasterLockDialog.tsx +13 -9
- package/src/react/master-lock/MasterLockProvider.spec.tsx +17 -0
- package/src/react/master-lock/MasterLockProvider.tsx +16 -19
- package/src/react/media/MediaUploadControl.tsx +8 -29
- package/src/react/media/fileUploadClient.ts +7 -17
- package/src/react/media/hlsSource.ts +4 -1
- package/src/react/media/index.ts +0 -16
- package/src/react/sessionAuth.ts +2 -3
- package/src/scriptsResolve.spec.ts +66 -0
- package/src/styles-areas/activity-bar.css +1 -1
- package/src/styles-areas/auth.css +1 -1
- package/src/styles-areas/charts.css +1 -1
- package/src/styles-areas/chat.css +1 -1
- package/src/styles-areas/core.css +1 -1
- package/src/styles-areas/data-table.css +1 -1
- package/src/styles-areas/disk-usage.css +1 -1
- package/src/styles-areas/fields.css +1 -1
- package/src/styles-areas/file-tree.css +1 -1
- package/src/styles-areas/master-detail.css +1 -1
- package/src/styles-areas/master-lock.css +108 -2
- package/src/styles-areas/media-gallery.css +1 -1
- package/src/styles-areas/media.css +1 -17
- package/src/styles-areas/nav.css +1 -1
- package/src/styles-areas/notifications.css +1 -1
- package/src/styles-areas/spreadsheet.css +1 -1
- package/src/styles-areas/stats.css +1 -1
- package/src/styles-areas/wizard.css +1 -1
- package/src/styles-areas/workbook-viewer.css +1 -1
- package/src/styles-utilities.css +0 -16
- package/src/testing/_noBun.ts +12 -0
- package/src/verifyGraph.spec.ts +11 -0
- package/dist/react/media/PrivateIndexView.d.ts +0 -6
- package/dist/react/media/PrivateIndexView.d.ts.map +0 -1
- package/dist/react/media/PrivateIndexView.js +0 -44
- package/dist/react/media/PrivateIndexView.js.map +0 -1
- package/dist/react/media/PrivateModeBadge.d.ts +0 -17
- package/dist/react/media/PrivateModeBadge.d.ts.map +0 -1
- package/dist/react/media/PrivateModeBadge.js +0 -24
- package/dist/react/media/PrivateModeBadge.js.map +0 -1
- package/dist/react/media/privateClient.d.ts +0 -34
- package/dist/react/media/privateClient.d.ts.map +0 -1
- package/dist/react/media/privateClient.js +0 -36
- package/dist/react/media/privateClient.js.map +0 -1
- package/dist/react/media/usePrivateUnlock.d.ts +0 -25
- package/dist/react/media/usePrivateUnlock.d.ts.map +0 -1
- package/dist/react/media/usePrivateUnlock.js +0 -68
- package/dist/react/media/usePrivateUnlock.js.map +0 -1
- package/src/react/media/PrivateIndexView.tsx +0 -113
- package/src/react/media/PrivateModeBadge.tsx +0 -50
- package/src/react/media/privateClient.spec.ts +0 -113
- package/src/react/media/privateClient.ts +0 -82
- package/src/react/media/usePrivateUnlock.ts +0 -132
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 2777 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -156,9 +156,6 @@
|
|
|
156
156
|
border-block-style: var(--tw-border-style);
|
|
157
157
|
border-block-width: 2px;
|
|
158
158
|
}
|
|
159
|
-
.border-\[color\:var\(--border-strong\)\] {
|
|
160
|
-
border-color: var(--border-strong);
|
|
161
|
-
}
|
|
162
159
|
.border-accent {
|
|
163
160
|
border-color: var(--accent);
|
|
164
161
|
}
|
|
@@ -171,12 +168,6 @@
|
|
|
171
168
|
border-color: color-mix(in oklab, var(--color-white) 90%, transparent);
|
|
172
169
|
}
|
|
173
170
|
}
|
|
174
|
-
.bg-\[color\:color-mix\(in_srgb\,var\(--accent-solid\)_8\%\,transparent\)\] {
|
|
175
|
-
background-color: var(--accent-solid);
|
|
176
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
177
|
-
background-color: color-mix(in srgb,var(--accent-solid) 8%,transparent);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
171
|
.bg-accent\/15 {
|
|
181
172
|
background-color: var(--accent);
|
|
182
173
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -283,9 +274,6 @@
|
|
|
283
274
|
.text-\[color\:var\(--accent\)\] {
|
|
284
275
|
color: var(--accent);
|
|
285
276
|
}
|
|
286
|
-
.text-\[color\:var\(--muted-foreground\)\] {
|
|
287
|
-
color: var(--muted-foreground);
|
|
288
|
-
}
|
|
289
277
|
.accent-white {
|
|
290
278
|
accent-color: var(--color-white);
|
|
291
279
|
}
|
|
@@ -322,10 +310,6 @@
|
|
|
322
310
|
.group-focus-within\:opacity-100:is(:where(.group):focus-within *) {
|
|
323
311
|
opacity: 100%;
|
|
324
312
|
}
|
|
325
|
-
.last\:border-b-0:last-child {
|
|
326
|
-
border-bottom-style: var(--tw-border-style);
|
|
327
|
-
border-bottom-width: 0px;
|
|
328
|
-
}
|
|
329
313
|
.focus-within\:opacity-100:focus-within {
|
|
330
314
|
opacity: 100%;
|
|
331
315
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 799 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 231 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 165 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 360 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 392 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
17
|
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
-
* when this file is stale.
|
|
18
|
+
* when this file is stale. 246 candidate(s) scanned.
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -1591,9 +1591,6 @@
|
|
|
1591
1591
|
--tw-border-style: solid;
|
|
1592
1592
|
border-style: solid;
|
|
1593
1593
|
}
|
|
1594
|
-
.border-\[color\:var\(--border-strong\)\] {
|
|
1595
|
-
border-color: var(--border-strong);
|
|
1596
|
-
}
|
|
1597
1594
|
.border-\[var\(--border\)\] {
|
|
1598
1595
|
border-color: var(--border);
|
|
1599
1596
|
}
|
|
@@ -1741,12 +1738,6 @@
|
|
|
1741
1738
|
.border-l-warning {
|
|
1742
1739
|
border-left-color: var(--warning);
|
|
1743
1740
|
}
|
|
1744
|
-
.bg-\[color\:color-mix\(in_srgb\,var\(--accent-solid\)_8\%\,transparent\)\] {
|
|
1745
|
-
background-color: var(--accent-solid);
|
|
1746
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
1747
|
-
background-color: color-mix(in srgb,var(--accent-solid) 8%,transparent);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
1741
|
.bg-\[var\(--accent\)\]\/12 {
|
|
1751
1742
|
background-color: var(--accent);
|
|
1752
1743
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -2626,9 +2617,6 @@
|
|
|
2626
2617
|
.text-\[color\:var\(--accent\)\] {
|
|
2627
2618
|
color: var(--accent);
|
|
2628
2619
|
}
|
|
2629
|
-
.text-\[color\:var\(--muted-foreground\)\] {
|
|
2630
|
-
color: var(--muted-foreground);
|
|
2631
|
-
}
|
|
2632
2620
|
.text-\[var\(--danger\)\] {
|
|
2633
2621
|
color: var(--danger);
|
|
2634
2622
|
}
|
|
@@ -3299,10 +3287,6 @@
|
|
|
3299
3287
|
border-right-style: var(--tw-border-style);
|
|
3300
3288
|
border-right-width: 0px;
|
|
3301
3289
|
}
|
|
3302
|
-
.last\:border-b-0:last-child {
|
|
3303
|
-
border-bottom-style: var(--tw-border-style);
|
|
3304
|
-
border-bottom-width: 0px;
|
|
3305
|
-
}
|
|
3306
3290
|
.last\:pr-1:last-child {
|
|
3307
3291
|
padding-right: var(--spacing);
|
|
3308
3292
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_noBun.d.ts","sourceRoot":"","sources":["../../src/testing/_noBun.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Loaded only on non-Bun runtimes (task 2087). The `cursedbelt/testing` export map routes the
|
|
2
|
+
// real module to Bun (the `bun` condition) and everything else here, so importing these helpers
|
|
3
|
+
// from Node fails with a sentence instead of `Named export 'waitFor' not found` — which is what
|
|
4
|
+
// Node said about `@testing-library/react`, a CommonJS module its ESM loader cannot name-import.
|
|
5
|
+
// Same shape as `cwip/testing`'s `_noBun`, and `tools/proveInstall.ts` excuses exactly this: a
|
|
6
|
+
// `bun` condition plus a plain `Error` of the package's own making.
|
|
7
|
+
throw new Error('cursedbelt/testing requires the Bun runtime — it drives @testing-library/react under bun:test + happy-dom. ' +
|
|
8
|
+
'Import it only from specs run with `bun test`.');
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=_noBun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_noBun.js","sourceRoot":"","sources":["../../src/testing/_noBun.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,gGAAgG;AAChG,gGAAgG;AAChG,iGAAiG;AACjG,+FAA+F;AAC/F,oEAAoE;AACpE,MAAM,IAAI,KAAK,CACb,6GAA6G;IAC3G,gDAAgD,CACnD,CAAC;AAEF,OAAO,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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.",
|
|
@@ -13,12 +13,41 @@
|
|
|
13
13
|
"typecheck": "typecheck",
|
|
14
14
|
"e2e": "e2e",
|
|
15
15
|
"build": "build"
|
|
16
|
-
},
|
|
17
|
-
"extraCommands": {
|
|
18
|
-
"format": "format"
|
|
19
16
|
}
|
|
20
17
|
}
|
|
21
18
|
},
|
|
19
|
+
"bundlerOnly": {
|
|
20
|
+
"why": "Entries that load only through a bundler (Vite, Bun), never under plain Node, and why. `css`: they import a stylesheet — their own or a dependency's — so Node refuses them with ERR_UNKNOWN_FILE_EXTENSION \".css\" by design. `directoryImport`: they reach @atlaskit/pragmatic-drag-and-drop, which ships nested package.json directories and no exports map, so Node ESM refuses its subpaths with ERR_UNSUPPORTED_DIR_IMPORT (importing its dist files directly would load a second copy of the drag engine under a bundler). This is the declaration tools/proveInstall.ts reads to excuse exactly that error for exactly these subpaths (task 2087); src/bundlerOnlyEntries.spec.ts fails when a list and the code disagree, in either direction.",
|
|
21
|
+
"css": [
|
|
22
|
+
"./react",
|
|
23
|
+
"./react/activity-bar",
|
|
24
|
+
"./react/dashboard-grid",
|
|
25
|
+
"./react/diff-viewer",
|
|
26
|
+
"./react/file-tree",
|
|
27
|
+
"./react/markdown-doc",
|
|
28
|
+
"./react/media",
|
|
29
|
+
"./react/media-gallery",
|
|
30
|
+
"./react/media/player-strip",
|
|
31
|
+
"./react/media/video-player",
|
|
32
|
+
"./react/pdf-viewer",
|
|
33
|
+
"./react/rich-text",
|
|
34
|
+
"./react/spreadsheet",
|
|
35
|
+
"./react/workbook-viewer"
|
|
36
|
+
],
|
|
37
|
+
"directoryImport": [
|
|
38
|
+
"./react",
|
|
39
|
+
"./react/disk-usage"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"unpeered": {
|
|
43
|
+
"why": "Packages some subpaths import that are deliberately NOT in peerDependencies, because npm cannot resolve them beside this package's own peers: npm checks an optional peer's OWN peer ranges even when nobody installs it, so `npm install cursedbelt` failed ERESOLVE (task 2087, measured on 4.7.0 and on packed probes). @glideapps/glide-data-grid@6 peers react <=18 and marked ^4 (this package: react ^19, marked >=14); @emoji-mart/react@1 peers react <=18; @hookform/resolvers@5 dragged zod 3 against zod ^4. lodash is glide's own peer, which glide does not declare as a dependency. An app that renders ./react/spreadsheet, ./react/workbook-viewer or ./react/media-gallery installs glide + lodash itself; EmojiPicker and makeFormConfig load theirs lazily and name the package when it is missing. Bun installs these without complaint; npm needs --legacy-peer-deps for them, which is the truth about those packages rather than about this one.",
|
|
44
|
+
"packages": {
|
|
45
|
+
"@emoji-mart/react": ">=1",
|
|
46
|
+
"@glideapps/glide-data-grid": ">=6",
|
|
47
|
+
"@hookform/resolvers": ">=5",
|
|
48
|
+
"lodash": ">=4.17.19"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
22
51
|
"scripts": {
|
|
23
52
|
"typecheck": "bun run typecheck:root && bun run typecheck:specs && bun run typecheck:scripts",
|
|
24
53
|
"typecheck:root": "tsc --noEmit",
|
|
@@ -26,8 +55,8 @@
|
|
|
26
55
|
"typecheck:scripts": "tsc --noEmit --project tsconfig.scripts.json",
|
|
27
56
|
"demo:check": "tsc --noEmit --project demo/tsconfig.json",
|
|
28
57
|
"demo:paths": "bun run demo/generatePaths.ts --write",
|
|
29
|
-
"guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip --scope cursedbelt src/react src/styles.css",
|
|
30
|
-
"
|
|
58
|
+
"guardrails:report": "bun run scripts/guardrailsEnforce.ts no-raw-action-button,no-raw-overflow,no-raw-inputs,no-hardcoded-colors,no-raw-modal-footer,no-unguarded-editing-modal,control-height-tokens,no-raw-zindex,no-raw-data-table,no-tailwind-palette,no-raw-title-attr,micro-typography-floor,choice-needs-empty-state,no-select-as-action-menu,no-native-dialog,no-fast-tooltip,no-raw-password --scope cursedbelt src/react src/styles.css",
|
|
59
|
+
"lint": "biome lint src e2e demo scripts",
|
|
31
60
|
"styles": "bun run styles:static && bun run styles:utilities && bun run styles:areas",
|
|
32
61
|
"styles:static": "bun run scripts/generateStaticStyles.ts",
|
|
33
62
|
"styles:utilities": "bun run scripts/generateUtilityStyles.ts",
|
|
@@ -404,7 +433,7 @@
|
|
|
404
433
|
"types": "./dist/testing/tooltip.d.ts",
|
|
405
434
|
"bun": "./src/testing/tooltip.ts",
|
|
406
435
|
"source": "./src/testing/tooltip.ts",
|
|
407
|
-
"
|
|
436
|
+
"default": "./dist/testing/_noBun.js"
|
|
408
437
|
},
|
|
409
438
|
"./theme.css": "./src/theme.css",
|
|
410
439
|
"./workspace.css": "./src/workspace.css"
|
|
@@ -432,12 +461,9 @@
|
|
|
432
461
|
"@codemirror/state": ">=6",
|
|
433
462
|
"@codemirror/view": ">=6",
|
|
434
463
|
"@emoji-mart/data": ">=1",
|
|
435
|
-
"@emoji-mart/react": ">=1",
|
|
436
464
|
"@ffmpeg/ffmpeg": ">=0.12",
|
|
437
465
|
"@ffmpeg/util": ">=0.12",
|
|
438
466
|
"@git-diff-view/react": ">=0.1.6",
|
|
439
|
-
"@glideapps/glide-data-grid": ">=6",
|
|
440
|
-
"@hookform/resolvers": ">=5",
|
|
441
467
|
"@lezer/highlight": ">=1",
|
|
442
468
|
"@tanstack/react-query": ">=5",
|
|
443
469
|
"@testing-library/react": ">=16",
|
|
@@ -500,9 +526,6 @@
|
|
|
500
526
|
"@emoji-mart/data": {
|
|
501
527
|
"optional": true
|
|
502
528
|
},
|
|
503
|
-
"@emoji-mart/react": {
|
|
504
|
-
"optional": true
|
|
505
|
-
},
|
|
506
529
|
"@ffmpeg/ffmpeg": {
|
|
507
530
|
"optional": true
|
|
508
531
|
},
|
|
@@ -512,12 +535,6 @@
|
|
|
512
535
|
"@git-diff-view/react": {
|
|
513
536
|
"optional": true
|
|
514
537
|
},
|
|
515
|
-
"@glideapps/glide-data-grid": {
|
|
516
|
-
"optional": true
|
|
517
|
-
},
|
|
518
|
-
"@hookform/resolvers": {
|
|
519
|
-
"optional": true
|
|
520
|
-
},
|
|
521
538
|
"@lezer/highlight": {
|
|
522
539
|
"optional": true
|
|
523
540
|
},
|
|
@@ -730,6 +747,12 @@
|
|
|
730
747
|
{
|
|
731
748
|
"script": "demo:check"
|
|
732
749
|
},
|
|
750
|
+
{
|
|
751
|
+
"script": "lint"
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
"script": "guardrails:report"
|
|
755
|
+
},
|
|
733
756
|
{
|
|
734
757
|
"script": "styles"
|
|
735
758
|
},
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { checkAreaStyles, classesInCss, readBeltSheets, tokensInJs } from './checkAreaStyles';
|
|
6
|
+
|
|
7
|
+
const BELT = {
|
|
8
|
+
union: `@layer utilities { .flex { display: flex } .rounded-full { border-radius: 9999px } .hover\\:bg-muted { &:hover { @media (hover: hover) { background: var(--muted) } } } .recharts-tip { opacity: .5; } }`,
|
|
9
|
+
areas: {
|
|
10
|
+
core: `.flex { display: flex } .hover\\:bg-muted:hover { background: x }`,
|
|
11
|
+
charts: `.recharts-tip { opacity: .5 }`,
|
|
12
|
+
chat: `.rounded-full { border-radius: 9999px }`,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('classesInCss', () => {
|
|
17
|
+
test('unescapes Tailwind selectors and ignores numbers inside declarations', () => {
|
|
18
|
+
const names = classesInCss(`.hover\\:bg-muted:hover{a:b} .\\32 xl\\:grid{x:y} .w-\\[1\\.5rem\\]{width:1.5rem} .a{opacity:.5}`);
|
|
19
|
+
expect([...names].sort()).toEqual(['2xl:grid', 'a', 'hover:bg-muted', 'w-[1.5rem]']);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("reads the real union — cursedbelt's own styles-utilities.css", () => {
|
|
23
|
+
const { union, areas } = readBeltSheets();
|
|
24
|
+
const names = classesInCss(union);
|
|
25
|
+
expect(names.size).toBeGreaterThan(500);
|
|
26
|
+
expect(names.has('truncate')).toBe(true);
|
|
27
|
+
expect(Object.keys(areas)).toContain('core');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('checkAreaStyles', () => {
|
|
32
|
+
test('🔴 a class the JS uses that the CSS does not style is reported WITH its area — the under-selected case', () => {
|
|
33
|
+
const js = [`const a="flex rounded-full";const b=\`recharts-tip \${x}\`;`];
|
|
34
|
+
const css = [`.flex{display:flex}.rounded-full{border-radius:9999px}`];
|
|
35
|
+
const result = checkAreaStyles({ js, css }, BELT);
|
|
36
|
+
expect(result.missing).toEqual([{ className: 'recharts-tip', area: 'charts' }]);
|
|
37
|
+
expect(result.referenced).toBe(3);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('complete when every referenced class has a rule; a word that is no belt class is ignored', () => {
|
|
41
|
+
const js = [`x("flex hover:bg-muted"), y("not-a-belt-class")`];
|
|
42
|
+
const css = [`.flex{display:flex}.hover\\:bg-muted:hover{background:red}`];
|
|
43
|
+
expect(checkAreaStyles({ js, css }, BELT).missing).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('tokensInJs splits on whitespace, quotes and backticks only', () => {
|
|
47
|
+
expect([...tokensInJs(`a="[&>*]:w-full p-2" b='x'`)]).toContain('[&>*]:w-full');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the CLI exits 2 on a directory with nothing to compare, 1 on a miss, 0 when complete', () => {
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), 'belt-area-check-'));
|
|
52
|
+
try {
|
|
53
|
+
const run = () =>
|
|
54
|
+
Bun.spawnSync(['bun', join(import.meta.dir, 'checkAreaStyles.ts'), dir], { stdout: 'pipe', stderr: 'pipe' });
|
|
55
|
+
expect(run().exitCode).toBe(2);
|
|
56
|
+
const { union } = readBeltSheets();
|
|
57
|
+
// `truncate` is a core utility in every build of the union.
|
|
58
|
+
expect(classesInCss(union).has('truncate')).toBe(true);
|
|
59
|
+
mkdirSync(join(dir, 'assets'));
|
|
60
|
+
writeFileSync(join(dir, 'assets', 'index.js'), 'const c="truncate";');
|
|
61
|
+
writeFileSync(join(dir, 'assets', 'index.css'), '.app{color:red}');
|
|
62
|
+
const miss = run();
|
|
63
|
+
expect(miss.exitCode).toBe(1);
|
|
64
|
+
expect(miss.stderr.toString()).toContain('truncate');
|
|
65
|
+
writeFileSync(join(dir, 'assets', 'index.css'), '.truncate{overflow:hidden}');
|
|
66
|
+
expect(run().exitCode).toBe(0);
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(dir, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Does an app's BUILT CSS style every cursedbelt class its BUILT JS uses? — the completeness
|
|
4
|
+
* check the per-area stylesheets need before any app may adopt them (task 079).
|
|
5
|
+
*
|
|
6
|
+
* ── Why adopting the areas was blocked on this ──────────────────────────────────────────
|
|
7
|
+
* `cursedbelt/styles-areas/*.css` lets an app import `core` plus the areas it renders instead
|
|
8
|
+
* of the 135 KB union (`scripts/generateAreaStyles.ts` has the numbers). The catch is the one
|
|
9
|
+
* `apps/flix/scripts/check-belt-styles.ts` was built for: under-select ONE area and the app
|
|
10
|
+
* typechecks, tests and builds green while a component renders unstyled. A byte count cannot
|
|
11
|
+
* tell "the right ten areas" from "nine of them" — a 4 KB miss is invisible in it.
|
|
12
|
+
*
|
|
13
|
+
* ── How it decides, with no new source of truth ─────────────────────────────────────────
|
|
14
|
+
* 1. The UNION — every utility cursedbelt's components use — is this package's own
|
|
15
|
+
* `src/styles-utilities.css`. Its class selectors are the universe.
|
|
16
|
+
* 2. What the app's JS references: every whitespace/quote-delimited token in the app's built
|
|
17
|
+
* `*.js` that is ALSO a union class. (Tailwind's extractor works the same way; a class
|
|
18
|
+
* name survives minification because it is a string.)
|
|
19
|
+
* 3. What the app's CSS styles: every class selector in the app's built `*.css`.
|
|
20
|
+
* 4. (2) minus (3) is a class cursedbelt draws with and this app ships no rule for. Each one is
|
|
21
|
+
* reported with the AREA that carries it (`src/styles-areas/*.css`), which is the fix.
|
|
22
|
+
*
|
|
23
|
+
* A false positive is possible — an app string that happens to equal a utility name and is not
|
|
24
|
+
* used as a class — and it is the cheap direction: adding the area costs bytes, missing one
|
|
25
|
+
* costs a broken screen on a live hostname. A class that appears in NO area is excluded (the
|
|
26
|
+
* area split already proves core-plus-every-area equals the union, `src/stylesAreas.spec.ts`).
|
|
27
|
+
*
|
|
28
|
+
* Usage, from an app, after its build:
|
|
29
|
+
* bun node_modules/cursedbelt/scripts/checkAreaStyles.ts dist
|
|
30
|
+
* Exit 0 = complete, 1 = a class is unstyled (listed with its area), 2 = could not measure
|
|
31
|
+
* (no JS or no CSS under the directory — a check that measured nothing has not passed).
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
34
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
|
|
37
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
/** This package's `src`, from `scripts/` (source) or `dist/scripts/` (compiled). */
|
|
39
|
+
const SRC = [resolve(HERE, '../src'), resolve(HERE, '../../src')].find((d) =>
|
|
40
|
+
existsSync(join(d, 'styles-utilities.css')),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** Undo CSS identifier escaping: `\:` → `:`, `\32 xl` → `2xl`. */
|
|
44
|
+
function unescapeIdent(raw: string): string {
|
|
45
|
+
return raw.replace(/\\([0-9a-fA-F]{1,6}) ?|\\(.)/g, (_m, hex: string | undefined, ch: string | undefined) =>
|
|
46
|
+
hex ? String.fromCodePoint(Number.parseInt(hex, 16)) : (ch ?? ''),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Every class selector in a stylesheet, unescaped. Comments are dropped first. */
|
|
51
|
+
export function classesInCss(css: string): Set<string> {
|
|
52
|
+
const out = new Set<string>();
|
|
53
|
+
const code = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
54
|
+
// Only selector text — the part before each `{` — so a `.5rem` in a declaration never counts.
|
|
55
|
+
for (const selector of code.split('{').map((chunk) => chunk.slice(chunk.lastIndexOf('}') + 1).split(';').pop() ?? '')) {
|
|
56
|
+
for (const m of selector.matchAll(/\.((?:\\[0-9a-fA-F]{1,6} ?|\\.|[A-Za-z0-9_-])+)/g)) {
|
|
57
|
+
const name = unescapeIdent(m[1] as string);
|
|
58
|
+
if (/^[0-9]/.test(name) && !/[a-z]/i.test(name)) continue; // `.5` in a stray number
|
|
59
|
+
out.add(name);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Every whitespace/quote/backtick-delimited token in a JS bundle. */
|
|
66
|
+
export function tokensInJs(js: string): Set<string> {
|
|
67
|
+
return new Set(js.split(/[\s"'`]+/).filter((t) => t.length > 0 && t.length < 200));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function filesUnder(dir: string, ext: string, out: string[] = []): string[] {
|
|
71
|
+
for (const entry of readdirSync(dir)) {
|
|
72
|
+
if (entry === 'node_modules') continue;
|
|
73
|
+
const full = join(dir, entry);
|
|
74
|
+
if (statSync(full).isDirectory()) filesUnder(full, ext, out);
|
|
75
|
+
else if (entry.endsWith(ext)) out.push(full);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AreaCheckResult {
|
|
81
|
+
/** Belt classes the JS uses and the CSS does not style → the area that carries each. */
|
|
82
|
+
missing: { className: string; area: string }[];
|
|
83
|
+
jsFiles: number;
|
|
84
|
+
cssFiles: number;
|
|
85
|
+
/** How many belt classes the JS references — a zero here means the check saw nothing. */
|
|
86
|
+
referenced: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The measurement, over explicit texts — what the spec drives. */
|
|
90
|
+
export function checkAreaStyles(
|
|
91
|
+
input: { js: string[]; css: string[] },
|
|
92
|
+
belt: { union: string; areas: Record<string, string> },
|
|
93
|
+
): Omit<AreaCheckResult, 'jsFiles' | 'cssFiles'> {
|
|
94
|
+
const union = classesInCss(belt.union);
|
|
95
|
+
const areaOf = new Map<string, string>();
|
|
96
|
+
for (const [area, css] of Object.entries(belt.areas)) {
|
|
97
|
+
for (const name of classesInCss(css)) if (!areaOf.has(name)) areaOf.set(name, area);
|
|
98
|
+
}
|
|
99
|
+
const emitted = new Set<string>();
|
|
100
|
+
for (const css of input.css) for (const name of classesInCss(css)) emitted.add(name);
|
|
101
|
+
const referenced = new Set<string>();
|
|
102
|
+
for (const js of input.js) for (const token of tokensInJs(js)) if (union.has(token) && areaOf.has(token)) referenced.add(token);
|
|
103
|
+
const missing = [...referenced]
|
|
104
|
+
.filter((name) => !emitted.has(name))
|
|
105
|
+
.sort()
|
|
106
|
+
.map((className) => ({ className, area: areaOf.get(className) as string }));
|
|
107
|
+
return { missing, referenced: referenced.size };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read this package's union + area sheets. */
|
|
111
|
+
export function readBeltSheets(src: string | undefined = SRC): { union: string; areas: Record<string, string> } {
|
|
112
|
+
if (!src) throw new Error("cursedbelt's src/styles-utilities.css is not beside this script — the package is incomplete");
|
|
113
|
+
const areasDir = join(src, 'styles-areas');
|
|
114
|
+
const areas: Record<string, string> = {};
|
|
115
|
+
for (const file of readdirSync(areasDir)) if (file.endsWith('.css')) areas[basename(file, '.css')] = readFileSync(join(areasDir, file), 'utf8');
|
|
116
|
+
return { union: readFileSync(join(src, 'styles-utilities.css'), 'utf8'), areas };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function checkDist(distDir: string): AreaCheckResult {
|
|
120
|
+
const jsFiles = filesUnder(distDir, '.js');
|
|
121
|
+
const cssFiles = filesUnder(distDir, '.css');
|
|
122
|
+
const result = checkAreaStyles(
|
|
123
|
+
{ js: jsFiles.map((f) => readFileSync(f, 'utf8')), css: cssFiles.map((f) => readFileSync(f, 'utf8')) },
|
|
124
|
+
readBeltSheets(),
|
|
125
|
+
);
|
|
126
|
+
return { ...result, jsFiles: jsFiles.length, cssFiles: cssFiles.length };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (import.meta.main) {
|
|
130
|
+
const dir = resolve(process.argv[2] ?? 'dist');
|
|
131
|
+
if (!existsSync(dir)) {
|
|
132
|
+
console.error(`✗ ${dir} does not exist — build the app first. A check that measured nothing has not passed.`);
|
|
133
|
+
process.exit(2);
|
|
134
|
+
}
|
|
135
|
+
const result = checkDist(dir);
|
|
136
|
+
if (result.jsFiles === 0 || result.cssFiles === 0) {
|
|
137
|
+
console.error(`✗ ${dir} holds ${result.jsFiles} .js and ${result.cssFiles} .css file(s) — nothing to compare.`);
|
|
138
|
+
process.exit(2);
|
|
139
|
+
}
|
|
140
|
+
if (result.missing.length === 0) {
|
|
141
|
+
console.log(`✓ belt area styles complete: all ${result.referenced} cursedbelt classes the build references are styled (${result.jsFiles} js, ${result.cssFiles} css).`);
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
const byArea = new Map<string, string[]>();
|
|
145
|
+
for (const { className, area } of result.missing) byArea.set(area, [...(byArea.get(area) ?? []), className]);
|
|
146
|
+
console.error(`✗ ${result.missing.length} cursedbelt class(es) the built JS uses have NO rule in the built CSS.`);
|
|
147
|
+
console.error(' Import the area that carries them (after core) — `@import "cursedbelt/styles-areas/<area>.css";`:');
|
|
148
|
+
for (const [area, names] of [...byArea].sort()) console.error(` ${area}.css ← ${names.slice(0, 8).join(' ')}${names.length > 8 ? ` … +${names.length - 8}` : ''}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
@@ -791,6 +791,14 @@ describe('no-native-dialog', () => {
|
|
|
791
791
|
expect(check('no-native-dialog', prompted)).toHaveLength(0);
|
|
792
792
|
});
|
|
793
793
|
|
|
794
|
+
test("🔴 a sibling import with the Node `.js` extension is the belt one too (task 269's spelling)", () => {
|
|
795
|
+
// DeleteButton's own `import { confirm } from '../confirm.js'` was reported as a native
|
|
796
|
+
// dialog from the day every relative specifier gained its `.js` until 2026-09-23.
|
|
797
|
+
const source = `import { confirm, type ConfirmOptions } from '../confirm.js';
|
|
798
|
+
const ok = await confirm({ title: 'Delete?' });`;
|
|
799
|
+
expect(check('no-native-dialog', source)).toHaveLength(0);
|
|
800
|
+
});
|
|
801
|
+
|
|
794
802
|
test('🔴 a belt import does NOT excuse an explicit window.confirm in the same file', () => {
|
|
795
803
|
// The migration's half-done state: one call site converted, one left behind.
|
|
796
804
|
// Exempting the whole FILE on the strength of its import list is how that
|
|
@@ -1176,3 +1184,38 @@ describe('--exclude', () => {
|
|
|
1176
1184
|
});
|
|
1177
1185
|
});
|
|
1178
1186
|
});
|
|
1187
|
+
|
|
1188
|
+
describe('no-raw-password', () => {
|
|
1189
|
+
test('🔴 flags the COMPONENT form no-raw-inputs is blind to', () => {
|
|
1190
|
+
expect(check('no-raw-password', `<Input id="ml-current" type="password" />`)).toHaveLength(1);
|
|
1191
|
+
expect(check('no-raw-inputs', `<Input id="ml-current" type="password" />`)).toHaveLength(0);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
test('flags a raw input, a JSX expression, a props object and template-literal HTML', () => {
|
|
1195
|
+
expect(check('no-raw-password', `<input type='password' />`)).toHaveLength(1);
|
|
1196
|
+
expect(check('no-raw-password', `<TextField type={"password"} />`)).toHaveLength(1);
|
|
1197
|
+
expect(check('no-raw-password', `const field = { name: 'pw', type: 'password' };`)).toHaveLength(1);
|
|
1198
|
+
expect(check('no-raw-password', 'const html = `<input name="pw" type="password">`;')).toHaveLength(1);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("passes SecretInput's own toggle — a ternary IS the eye", () => {
|
|
1202
|
+
expect(check('no-raw-password', ` type={shown ? 'text' : 'password'}`)).toHaveLength(0);
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
test('passes an ignore-commented line, same line or the block above', () => {
|
|
1206
|
+
expect(
|
|
1207
|
+
check('no-raw-password', `<input type="password" /> {/* guardrails-ignore no-raw-password: server page, own eye */}`),
|
|
1208
|
+
).toHaveLength(0);
|
|
1209
|
+
expect(
|
|
1210
|
+
check(
|
|
1211
|
+
'no-raw-password',
|
|
1212
|
+
`// guardrails-ignore no-raw-password: server-rendered, carries its own reveal button
|
|
1213
|
+
const html = \`<input type="password">\`;`,
|
|
1214
|
+
),
|
|
1215
|
+
).toHaveLength(0);
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test('prose in a comment is not a field', () => {
|
|
1219
|
+
expect(check('no-raw-password', `// never write type="password" by hand`)).toHaveLength(0);
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
@@ -122,6 +122,10 @@
|
|
|
122
122
|
// `--reimplements <Name,…>` from `fleet/standards/CURSEDBELT_INVENTORY.md`; with no
|
|
123
123
|
// list the rule is inert rather than guessing.
|
|
124
124
|
//
|
|
125
|
+
// no-raw-password — owner, 2026-09-18: every password field has an eye. Flags
|
|
126
|
+
// `type="password"` on ANY tag or props object (the `<Input type="password">` form that
|
|
127
|
+
// `no-raw-inputs` cannot see) and in template-literal HTML. Use `SecretInput`.
|
|
128
|
+
//
|
|
125
129
|
// Opt-out (mirrors this repo's existing `biome-ignore lint/<rule>: <reason>` convention —
|
|
126
130
|
// see e.g. packages/cursedbelt/src/react/components/ButtonGroup.tsx): put a same-shaped
|
|
127
131
|
// line comment as the FIRST thing inside the tag's attribute list (for `no-raw-action-button`
|
|
@@ -1487,7 +1491,9 @@ function importsBeltDialog(source: string, fn: string): boolean {
|
|
|
1487
1491
|
// static, from the package
|
|
1488
1492
|
new RegExp(`import\\s*${named}\\s*from\\s*${spec}`, "s").test(source) ||
|
|
1489
1493
|
// static, from a sibling inside cursedbelt itself
|
|
1490
|
-
|
|
1494
|
+
// (with or without the `.js` Node ESM needs — task 269 added it to every relative
|
|
1495
|
+
// specifier, and this pattern then reported `DeleteButton`'s own belt `confirm`)
|
|
1496
|
+
new RegExp(`import\\s*${named}\\s*from\\s*['"][./]+(?:react/)?${fn}(?:\\.[jt]sx?)?['"]`, "s").test(source) ||
|
|
1491
1497
|
// dynamic — `const { prompt } = await import('cursedbelt/react/prompt')`
|
|
1492
1498
|
new RegExp(`${named}\\s*=\\s*(?:await\\s+)?import\\(\\s*${spec}`, "s").test(source) ||
|
|
1493
1499
|
// and its type-only companion, `typeof import('…').prompt`
|
|
@@ -1626,6 +1632,44 @@ const noFastTooltip: Rule = {
|
|
|
1626
1632
|
},
|
|
1627
1633
|
};
|
|
1628
1634
|
|
|
1635
|
+
// ── no-raw-password (owner, 2026-09-18: "all passwords need eye icon") ─────────
|
|
1636
|
+
// A password field the person typing into cannot reveal. `SecretInput` has carried the eye
|
|
1637
|
+
// (and an opt-out copy button, `showCopy={false}` for a login form) the whole time, and on
|
|
1638
|
+
// 2026-09-19 twenty-eight fields across the fleet still typed into the dark — three of them in
|
|
1639
|
+
// cursedbelt's OWN master-lock dialog. They accumulated under an enforced guardrails suite
|
|
1640
|
+
// because there was no rule about masked fields at all: `no-raw-inputs` is about bare TAGS, and
|
|
1641
|
+
// every one of the three belt fields was `<Input type="password">`, a component it never reads.
|
|
1642
|
+
//
|
|
1643
|
+
// So this rule is on the ATTRIBUTE, on any tag: a raw `<input>`, a component (`<Input`,
|
|
1644
|
+
// `<TextField`), a props object (`{ type: 'password' }`), and server-rendered HTML in a
|
|
1645
|
+
// template literal (`<input type="password">`), which is the one place a React primitive
|
|
1646
|
+
// cannot reach — that markup takes an eye of its own. `SecretInput`'s own
|
|
1647
|
+
// `type={shown ? 'text' : 'password'}` does not match: a ternary is a toggle, which is the
|
|
1648
|
+
// point. Comments are blanked first; specs are fixtures and are skipped.
|
|
1649
|
+
const PASSWORD_TYPE_RE =
|
|
1650
|
+
/\btype\s*=\s*(?:\{\s*)?["'`]password["'`]|\btype\s*:\s*["'`]password["'`]/;
|
|
1651
|
+
const PASSWORD_IGNORE_RE = /guardrails-ignore\s+no-raw-password\s*:/;
|
|
1652
|
+
|
|
1653
|
+
const noRawPassword: Rule = {
|
|
1654
|
+
name: "no-raw-password",
|
|
1655
|
+
description:
|
|
1656
|
+
"Owner, 2026-09-18: every password field has an eye. A `type=\"password\"` field the person cannot reveal — on any tag, component or props object — must be cursedbelt's `SecretInput` (`showCopy={false}` for a login form, `autoComplete` passed through). Server-rendered HTML that cannot import it carries its own reveal toggle and takes the per-line opt-out naming it.",
|
|
1657
|
+
check(filePath, source) {
|
|
1658
|
+
if (isTestSource(filePath.replace(/\\/g, "/"))) return [];
|
|
1659
|
+
const raws = source.split("\n");
|
|
1660
|
+
const scan = blankComments(source).split("\n");
|
|
1661
|
+
const out: Violation[] = [];
|
|
1662
|
+
for (const [i, line] of scan.entries()) {
|
|
1663
|
+
if (!PASSWORD_TYPE_RE.test(line)) continue;
|
|
1664
|
+
const raw = raws[i] ?? "";
|
|
1665
|
+
if (PASSWORD_IGNORE_RE.test(raw)) continue;
|
|
1666
|
+
if (PASSWORD_IGNORE_RE.test(raws.slice(Math.max(0, i - 3), i).join("\n"))) continue;
|
|
1667
|
+
out.push({ file: filePath, line: i + 1, snippet: raw.trim() });
|
|
1668
|
+
}
|
|
1669
|
+
return out;
|
|
1670
|
+
},
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1629
1673
|
export const RULES: Record<string, Rule> = {
|
|
1630
1674
|
[noRawActionButton.name]: noRawActionButton,
|
|
1631
1675
|
[noRawOverflow.name]: noRawOverflow,
|
|
@@ -1644,6 +1688,7 @@ export const RULES: Record<string, Rule> = {
|
|
|
1644
1688
|
[noSelectAsActionMenu.name]: noSelectAsActionMenu,
|
|
1645
1689
|
[noNativeDialog.name]: noNativeDialog,
|
|
1646
1690
|
[noFastTooltip.name]: noFastTooltip,
|
|
1691
|
+
[noRawPassword.name]: noRawPassword,
|
|
1647
1692
|
};
|
|
1648
1693
|
|
|
1649
1694
|
// ── File-scoped allowances ──────────────────────────────────────────────────────
|