cursedbelt 5.1.0 → 5.3.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/dist/react/engagement/beacon.d.ts +38 -0
- package/dist/react/engagement/beacon.d.ts.map +1 -0
- package/dist/react/engagement/beacon.js +62 -0
- package/dist/react/engagement/beacon.js.map +1 -0
- package/dist/react/engagement/beaconClient.d.ts +119 -0
- package/dist/react/engagement/beaconClient.d.ts.map +1 -0
- package/dist/react/engagement/beaconClient.js +203 -0
- package/dist/react/engagement/beaconClient.js.map +1 -0
- package/dist/react/engagement/index.d.ts +3 -0
- package/dist/react/engagement/index.d.ts.map +1 -0
- package/dist/react/engagement/index.js +5 -0
- package/dist/react/engagement/index.js.map +1 -0
- package/dist/react/lib/directUpload.d.ts +130 -0
- package/dist/react/lib/directUpload.d.ts.map +1 -0
- package/dist/react/lib/directUpload.js +228 -0
- package/dist/react/lib/directUpload.js.map +1 -0
- package/dist/styles-areas/auth.css +2 -2
- package/dist/styles-areas/core.css +14 -2
- package/dist/styles-areas/spreadsheet.css +2 -2
- package/dist/styles-areas/workbook-viewer.css +2 -2
- package/dist/styles-utilities.css +12 -0
- package/package.json +13 -1
- package/scripts/checkAreaStyles.spec.ts +90 -0
- package/scripts/checkAreaStyles.ts +127 -6
- package/scripts/generateAreaStyles.ts +12 -4
- package/scripts/generateUtilityStyles.ts +76 -1
- package/scripts/styleAreas.ts +2 -0
- package/src/react/engagement/beacon.tsx +63 -0
- package/src/react/engagement/beaconClient.spec.ts +138 -0
- package/src/react/engagement/beaconClient.ts +228 -0
- package/src/react/engagement/index.ts +13 -0
- package/src/react/lib/directUpload.spec.ts +128 -0
- package/src/react/lib/directUpload.ts +334 -0
- package/src/styles-areas/auth.css +2 -2
- package/src/styles-areas/core.css +14 -2
- package/src/styles-areas/spreadsheet.css +2 -2
- package/src/styles-areas/workbook-viewer.css +2 -2
- package/src/styles-utilities.css +12 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt/react/upload-direct` — browser-side resumable upload, straight from the file input
|
|
3
|
+
* to binary-server.
|
|
4
|
+
*
|
|
5
|
+
* Lifted 2026-09-23 (cursedbelt 5.2.0, task 056-343's browser half) from `src/kit/directUpload.ts`,
|
|
6
|
+
* identical in collections and family. The server half — minting the session — is
|
|
7
|
+
* `cursedbelt-server/binary-store/upload-session`'s `createUploadSession`.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 THIS FILE RUNS IN A BROWSER. It must never import `node:*`, `binaryStore.ts`, or
|
|
10
|
+
* anything that reaches a private key — see {@link UploadSession}. Keep it dependency-free.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the bytes skip the app server
|
|
13
|
+
*
|
|
14
|
+
* Every satellite's upload route reads the whole body (`formData()` → `arrayBuffer()`) before
|
|
15
|
+
* handing it to binary-server, inside a unit capped at `MemoryMax=300M` on a shared t3.micro
|
|
16
|
+
* that also runs nginx and every other app. That is why the fleet's upload ceiling is 32 MB
|
|
17
|
+
* and why raising the number was never the fix: a 10 GB video does not fit in the process, in
|
|
18
|
+
* the nginx body limit, or in the Cloudflare Tunnel's ~100 MB request wall.
|
|
19
|
+
*
|
|
20
|
+
* So a large file does not go through the app server at all. The app server mints a session
|
|
21
|
+
* token naming exactly one destination key; the browser PUTs bounded parts directly to
|
|
22
|
+
* binary-server, which assembles them; the app server is told afterwards and records the
|
|
23
|
+
* metadata. No byte of video touches the EC2 box, which is the owner's standing rule for
|
|
24
|
+
* video (2026-08-12) and also the only shape that survives the memory limit.
|
|
25
|
+
*
|
|
26
|
+
* ## What makes it survive 10 GB
|
|
27
|
+
*
|
|
28
|
+
* - **Resume.** `GET /upload-chunk` reports which parts already landed, so an upload that
|
|
29
|
+
* dies at 90% resumes instead of restarting. That answer comes from the parts on disk, so
|
|
30
|
+
* it survives a binary-server restart too.
|
|
31
|
+
* - **Bounded concurrency.** A few parts in flight keeps a home upstream saturated without
|
|
32
|
+
* putting the whole file in memory. Only one part's bytes are ever read at a time per slot.
|
|
33
|
+
* - **Per-part retry.** A dropped part costs one part, not the upload.
|
|
34
|
+
* - **Slicing, not reading.** `Blob.slice` is lazy — the 10 GB file is never materialized.
|
|
35
|
+
*/
|
|
36
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
37
|
+
/** Split `[0, total)` into `chunkBytes`-sized part indices. */
|
|
38
|
+
export const chunkCount = (totalBytes, chunkBytes) => Math.max(1, Math.ceil(totalBytes / chunkBytes));
|
|
39
|
+
const chunkUrl = (s, token, ci) => `${s.baseUrl}/upload-chunk/${s.path
|
|
40
|
+
.split("/")
|
|
41
|
+
.map(encodeURIComponent)
|
|
42
|
+
.join("/")}?token=${encodeURIComponent(token)}&ci=${ci}`;
|
|
43
|
+
const statusUrl = (s, token) => `${s.baseUrl}/upload-chunk/${s.path
|
|
44
|
+
.split("/")
|
|
45
|
+
.map(encodeURIComponent)
|
|
46
|
+
.join("/")}?token=${encodeURIComponent(token)}`;
|
|
47
|
+
/** Which parts binary-server already holds, plus whether the object is already complete.
|
|
48
|
+
* A fresh session answers `{ received: [], complete: false }`, so callers need no special
|
|
49
|
+
* case for "first attempt". */
|
|
50
|
+
export async function fetchSessionState(session, token = session.token, signal) {
|
|
51
|
+
const res = await fetch(statusUrl(session, token), { signal });
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
return {
|
|
54
|
+
received: new Set(),
|
|
55
|
+
complete: false,
|
|
56
|
+
size: null,
|
|
57
|
+
checksum: null,
|
|
58
|
+
freeBytes: null,
|
|
59
|
+
};
|
|
60
|
+
const body = (await res.json());
|
|
61
|
+
return {
|
|
62
|
+
received: new Set(body.received ?? []),
|
|
63
|
+
complete: Boolean(body.complete),
|
|
64
|
+
size: body.size ?? null,
|
|
65
|
+
checksum: body.checksum ?? null,
|
|
66
|
+
freeBytes: body.freeBytes ?? null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Thrown when binary-server has no room for the file. Distinct from a transport failure so a
|
|
70
|
+
* UI can say "the server is full" — which is actionable — rather than "upload failed", which
|
|
71
|
+
* invites the person to retry forever against a disk that will never have space. */
|
|
72
|
+
export class InsufficientStorageError extends Error {
|
|
73
|
+
needBytes;
|
|
74
|
+
freeBytes;
|
|
75
|
+
overridable;
|
|
76
|
+
constructor(needBytes, freeBytes,
|
|
77
|
+
/**
|
|
78
|
+
* 🔴 Vestigial since 2026-08-13 and kept only so an older deployed binary-server can
|
|
79
|
+
* still be understood: THIS service no longer refuses an upload for disk space at all,
|
|
80
|
+
* so nothing here should be producing an overridable refusal any more. If you see one,
|
|
81
|
+
* the origin is running a build from before that change.
|
|
82
|
+
*/
|
|
83
|
+
overridable = false) {
|
|
84
|
+
super(freeBytes === null
|
|
85
|
+
? "binary-server is out of space for this upload"
|
|
86
|
+
: `binary-server has ${(freeBytes / 1024 ** 3).toFixed(1)} GB free, which is not enough for a ${(needBytes / 1024 ** 3).toFixed(1)} GB upload`);
|
|
87
|
+
this.needBytes = needBytes;
|
|
88
|
+
this.freeBytes = freeBytes;
|
|
89
|
+
this.overridable = overridable;
|
|
90
|
+
this.name = "InsufficientStorageError";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Upload `file` to binary-server in parts, resuming anything already delivered.
|
|
95
|
+
*
|
|
96
|
+
* Resolves once every part has landed and bs has assembled the object. The caller then tells
|
|
97
|
+
* its own app server to record the metadata — see each app's `/complete` route.
|
|
98
|
+
*/
|
|
99
|
+
export async function uploadDirect(file, session, opts = {}) {
|
|
100
|
+
const { onProgress, signal, refreshToken } = opts;
|
|
101
|
+
const concurrency = Math.max(1, opts.concurrency ?? 3);
|
|
102
|
+
const retries = Math.max(1, opts.retries ?? 5);
|
|
103
|
+
const { onLowDisk } = opts;
|
|
104
|
+
const total = session.totalChunks;
|
|
105
|
+
// A token can expire mid-upload on a slow line; `refreshToken` swaps in a new one and
|
|
106
|
+
// every subsequent request picks it up.
|
|
107
|
+
let token = session.token;
|
|
108
|
+
const prior = await fetchSessionState(session, token, signal);
|
|
109
|
+
if (prior.complete) {
|
|
110
|
+
return {
|
|
111
|
+
key: session.path,
|
|
112
|
+
size: prior.size ?? file.size,
|
|
113
|
+
checksum: prior.checksum,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Bail before sending anything if the file plainly cannot land. bs keeps a reserve so a
|
|
117
|
+
// full disk never takes the Mac down, and it reports free space on the status call above —
|
|
118
|
+
// so an upload with nowhere to go costs one request instead of gigabytes of transfer.
|
|
119
|
+
const remainingBytes = file.size - prior.received.size * session.chunkBytes;
|
|
120
|
+
/*
|
|
121
|
+
* 🔴 NO PREFLIGHT REFUSAL. Owner instruction, 2026-08-13: *"Remove all blockers for space
|
|
122
|
+
* and only allow warning me. I'll manage space myself beyond that."*
|
|
123
|
+
*
|
|
124
|
+
* This used to throw when the remaining bytes exceeded the reported free space. It is gone
|
|
125
|
+
* rather than softened because a client-side estimate is the WORST place to make this
|
|
126
|
+
* call: `remainingBytes` counts every part not yet delivered, `freeBytes` was read before
|
|
127
|
+
* the transfer began, and neither accounts for what the run itself frees or the owner
|
|
128
|
+
* clears while it goes. binary-server no longer refuses either; the real limit is ENOSPC
|
|
129
|
+
* from the filesystem, which is about the actual write rather than a projection.
|
|
130
|
+
*
|
|
131
|
+
* `onLowDisk` is the replacement, and it only WARNS.
|
|
132
|
+
*/
|
|
133
|
+
if (prior.freeBytes !== null && remainingBytes > prior.freeBytes) {
|
|
134
|
+
onLowDisk?.({ freeBytes: prior.freeBytes, needBytes: remainingBytes });
|
|
135
|
+
}
|
|
136
|
+
const pending = Array.from({ length: total }, (_, i) => i).filter((i) => !prior.received.has(i));
|
|
137
|
+
let done = total - pending.length;
|
|
138
|
+
let assembled = null;
|
|
139
|
+
const report = () => onProgress?.(total === 0 ? 1 : done / total, done * session.chunkBytes, file.size);
|
|
140
|
+
report();
|
|
141
|
+
const sendOne = async (ci) => {
|
|
142
|
+
const start = ci * session.chunkBytes;
|
|
143
|
+
// Lazy view onto the file — nothing is read until fetch streams it.
|
|
144
|
+
const part = file.slice(start, Math.min(start + session.chunkBytes, file.size));
|
|
145
|
+
let lastErr;
|
|
146
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
147
|
+
signal?.throwIfAborted();
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetch(chunkUrl(session, token, ci), {
|
|
150
|
+
method: "PUT",
|
|
151
|
+
body: part,
|
|
152
|
+
// bs stores the mime from the assembling request's content-type.
|
|
153
|
+
// 🔴 `x-allow-low-disk` is what turns binary-server's disk RESERVE from a
|
|
154
|
+
// wall into a warning. Owner instruction 2026-08-13, after a large upload
|
|
155
|
+
// stopped dead: *"Do not let the server block me. It can warn me and I can
|
|
156
|
+
// say do it anyway or cancel if I want."* The reserve is sized for the worst
|
|
157
|
+
// case a chunked upload can transiently need, so it refused uploads that
|
|
158
|
+
// would have fit. bs still refuses absolutely below its hard floor, where
|
|
159
|
+
// the machine itself is what is at risk.
|
|
160
|
+
headers: file.type ? { "content-type": file.type } : undefined,
|
|
161
|
+
signal,
|
|
162
|
+
});
|
|
163
|
+
if (res.status === 403 && refreshToken) {
|
|
164
|
+
// Almost always an expired session token on a long upload. Re-mint once per
|
|
165
|
+
// attempt and let the loop retry with it.
|
|
166
|
+
token = await refreshToken();
|
|
167
|
+
throw new Error("token refreshed, retrying");
|
|
168
|
+
}
|
|
169
|
+
// Out of space is terminal — retrying cannot make room, and a 10 GB upload
|
|
170
|
+
// hammering a full disk five times over is the opposite of helpful.
|
|
171
|
+
if (res.status === 507) {
|
|
172
|
+
const body = (await res.json().catch(() => ({})));
|
|
173
|
+
// Terminal — retrying cannot make room, and a 10 GB upload hammering a full
|
|
174
|
+
// disk five times over is the opposite of helpful. 🔴 A CURRENT
|
|
175
|
+
// binary-server never sends this for its own reserve any more (see
|
|
176
|
+
// `overridable`); reaching here means the volume is genuinely out of room
|
|
177
|
+
// and the filesystem said so.
|
|
178
|
+
throw new InsufficientStorageError(file.size, body.freeBytes ?? null, body.overridable === true);
|
|
179
|
+
}
|
|
180
|
+
if (!res.ok)
|
|
181
|
+
throw new Error(`chunk ${ci}: HTTP ${res.status}`);
|
|
182
|
+
const body = (await res.json());
|
|
183
|
+
if (body.assembled) {
|
|
184
|
+
assembled = {
|
|
185
|
+
key: body.key ?? session.path,
|
|
186
|
+
size: body.size ?? file.size,
|
|
187
|
+
checksum: body.checksum ?? null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
done++;
|
|
191
|
+
report();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
if (signal?.aborted)
|
|
196
|
+
throw err;
|
|
197
|
+
// Terminal: no amount of backoff creates disk space.
|
|
198
|
+
if (err instanceof InsufficientStorageError)
|
|
199
|
+
throw err;
|
|
200
|
+
lastErr = err;
|
|
201
|
+
// Backoff, capped — a home connection that dropped needs seconds, not minutes.
|
|
202
|
+
await sleep(Math.min(1000 * 2 ** attempt, 15_000));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`chunk ${ci} failed after ${retries} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
206
|
+
};
|
|
207
|
+
// Bounded worker pool over the pending indices.
|
|
208
|
+
const queue = [...pending];
|
|
209
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
210
|
+
for (;;) {
|
|
211
|
+
const next = queue.shift();
|
|
212
|
+
if (next === undefined)
|
|
213
|
+
return;
|
|
214
|
+
await sendOne(next);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
await Promise.all(workers);
|
|
218
|
+
if (assembled)
|
|
219
|
+
return assembled;
|
|
220
|
+
// Every part reported OK but no response carried the assemble result — possible if the
|
|
221
|
+
// final 201 was the one that got retried after its response was lost. Ask.
|
|
222
|
+
const after = await fetchSessionState(session, token, signal);
|
|
223
|
+
if (after.complete) {
|
|
224
|
+
return { key: session.path, size: after.size ?? file.size, checksum: after.checksum };
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`upload finished but binary-server did not assemble ${session.path} (${after.received.size}/${total} parts)`);
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=directUpload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directUpload.js","sourceRoot":"","sources":["../../../src/react/lib/directUpload.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 10 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("caret-transparent
|
|
27
|
+
@source inline("caret-transparent focus-within:border-field-ring focus-within:ring-2");
|
|
28
28
|
@source inline("focus-within:ring-field-ring/30 h-12 has-[[aria-invalid=true]]:border-field-error min-h-dvh");
|
|
29
29
|
@source inline("self-start self-stretch sm:w-11");
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
16
16
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
17
17
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
18
|
-
*
|
|
18
|
+
* 706 candidate(s).
|
|
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
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
@source inline("border-collapse border-current border-destructive/30 border-destructive/40 border-destructive/50");
|
|
53
53
|
@source inline("border-field-border border-success/30 border-success/40 border-success/50 border-t");
|
|
54
54
|
@source inline("border-transparent border-warning/30 border-warning/40 border-warning/50 border-x-4");
|
|
55
|
-
@source inline("border-x-transparent bot bottom bottom-0 break break-all c ca cap capitalize co contain");
|
|
55
|
+
@source inline("border-x-transparent bot bottom bottom-0 break break-all c ca cap capitalize co com contain");
|
|
56
56
|
@source inline("container content contents cursor cursor-col-resize cursor-default cursor-ew-resize");
|
|
57
57
|
@source inline("cursor-pointer cursor-row-resize d dark dark:bg-card-overlay dark:bg-card-raised");
|
|
58
58
|
@source inline("dark:bg-gray-800 data data-[disabled]:hover:bg-border data-[disabled]:opacity-50");
|
|
@@ -144,3 +144,15 @@
|
|
|
144
144
|
@source inline("w-[var(--control-h-lg)] w-[var(--control-h-md)] w-[var(--control-h-sm)] w-auto w-full w-px white");
|
|
145
145
|
@source inline("whitespace-nowrap whitespace-pre-wrap wrap wrap-break-word z z-10 z-20 z-30 z-40 z-50");
|
|
146
146
|
@source inline("z-[var(--z-popover)]");
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* Theme variables cursedbelt's components read BY NAME (`var(--color-card)` in a style prop).
|
|
150
|
+
* Tailwind emits a theme variable only when the app's own pass sees it used, and a `var()` here is
|
|
151
|
+
* that use — `@source inline` cannot mark one. 9 variable(s), derived by
|
|
152
|
+
* scripts/generateUtilityStyles.ts → themeVariablesReadByName(). Nothing reads this property.
|
|
153
|
+
*/
|
|
154
|
+
@layer theme {
|
|
155
|
+
:root, :host {
|
|
156
|
+
--cb-theme-read-by-name: var(--color-accent) var(--color-border) var(--color-card) var(--color-muted) var(--color-popover) var(--duration-base) var(--ease-out) var(--text-caption) var(--text-sm);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 2 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("
|
|
27
|
+
@source inline("col collapse");
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 5 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
|
-
@source inline("
|
|
27
|
+
@source inline("col collapse min-w-16 w w-48");
|
|
@@ -4969,3 +4969,15 @@
|
|
|
4969
4969
|
}
|
|
4970
4970
|
}
|
|
4971
4971
|
}
|
|
4972
|
+
|
|
4973
|
+
/*
|
|
4974
|
+
* Theme variables cursedbelt's components read BY NAME (`var(--color-card)` in a style prop).
|
|
4975
|
+
* Tailwind emits a theme variable only when the app's own pass sees it used, and a `var()` here is
|
|
4976
|
+
* that use — `@source inline` cannot mark one. 9 variable(s), derived by
|
|
4977
|
+
* scripts/generateUtilityStyles.ts → themeVariablesReadByName(). Nothing reads this property.
|
|
4978
|
+
*/
|
|
4979
|
+
@layer theme {
|
|
4980
|
+
:root, :host {
|
|
4981
|
+
--cb-theme-read-by-name: var(--color-accent) var(--color-border) var(--color-card) var(--color-muted) var(--color-popover) var(--duration-base) var(--ease-out) var(--text-caption) var(--text-sm);
|
|
4982
|
+
}
|
|
4983
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.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.",
|
|
@@ -203,6 +203,12 @@
|
|
|
203
203
|
"source": "./src/react/components/DropdownMenu.tsx",
|
|
204
204
|
"import": "./dist/react/components/DropdownMenu.js"
|
|
205
205
|
},
|
|
206
|
+
"./react/engagement-beacon": {
|
|
207
|
+
"types": "./dist/react/engagement/index.d.ts",
|
|
208
|
+
"bun": "./src/react/engagement/index.ts",
|
|
209
|
+
"source": "./src/react/engagement/index.ts",
|
|
210
|
+
"import": "./dist/react/engagement/index.js"
|
|
211
|
+
},
|
|
206
212
|
"./react/file-tree": {
|
|
207
213
|
"types": "./dist/react/file-tree/index.d.ts",
|
|
208
214
|
"bun": "./src/react/file-tree/index.ts",
|
|
@@ -395,6 +401,12 @@
|
|
|
395
401
|
"source": "./src/react/components/Switch.tsx",
|
|
396
402
|
"import": "./dist/react/components/Switch.js"
|
|
397
403
|
},
|
|
404
|
+
"./react/upload-direct": {
|
|
405
|
+
"types": "./dist/react/lib/directUpload.d.ts",
|
|
406
|
+
"bun": "./src/react/lib/directUpload.ts",
|
|
407
|
+
"source": "./src/react/lib/directUpload.ts",
|
|
408
|
+
"import": "./dist/react/lib/directUpload.js"
|
|
409
|
+
},
|
|
398
410
|
"./react/workbook-viewer": {
|
|
399
411
|
"types": "./dist/react/workbook-viewer/index.d.ts",
|
|
400
412
|
"bun": "./src/react/workbook-viewer/index.ts",
|
|
@@ -8,9 +8,14 @@ import {
|
|
|
8
8
|
classesInCss,
|
|
9
9
|
hasVariantPrefix,
|
|
10
10
|
inlineCandidatesIn,
|
|
11
|
+
onlyAsPropertyName,
|
|
11
12
|
onlyInMergeTables,
|
|
12
13
|
readBeltSheets,
|
|
14
|
+
readThemeVariables,
|
|
15
|
+
themeVariablesIn,
|
|
13
16
|
tokensInJs,
|
|
17
|
+
undefinedThemeVariables,
|
|
18
|
+
variablesDeclared,
|
|
14
19
|
} from './checkAreaStyles';
|
|
15
20
|
import { compileStylesheet } from './generateUtilityStyles';
|
|
16
21
|
|
|
@@ -133,6 +138,28 @@ describe('🔴 tailwind-merge tables are not usage (task 2118)', () => {
|
|
|
133
138
|
});
|
|
134
139
|
});
|
|
135
140
|
|
|
141
|
+
describe('🔴 a property NAME is not class usage (task 2131, station)', () => {
|
|
142
|
+
// Verbatim from station's build: d3's scale check, inside recharts.
|
|
143
|
+
const STATION = 'function RE(e){if(e!=null)return"invert"in e&&typeof e.invert=="function"?e.invert:null}';
|
|
144
|
+
|
|
145
|
+
test('an `in` test and an object key are excused', () => {
|
|
146
|
+
expect(onlyAsPropertyName('invert', STATION)).toBe(true);
|
|
147
|
+
expect(onlyAsPropertyName('invert', 'const o={"invert":1}')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('refused when the token is ALSO a class, or a lone string value (`cn("italic")` IS usage)', () => {
|
|
151
|
+
expect(onlyAsPropertyName('invert', `${STATION};x({className:"invert grayscale"})`)).toBe(false);
|
|
152
|
+
expect(onlyAsPropertyName('italic', 'x=cn("italic",y)')).toBe(false);
|
|
153
|
+
expect(onlyAsPropertyName('h-8', 'const v={size:{sm:"h-8"}}')).toBe(false);
|
|
154
|
+
expect(onlyAsPropertyName('invert', '')).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the station shape reports no `invert` miss', () => {
|
|
158
|
+
const { missing } = checkAreaStyles({ js: [STATION], css: [''] }, readBeltSheets());
|
|
159
|
+
expect(missing.map((m) => m.className)).not.toContain('invert');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
136
163
|
describe('an area is `@source inline` candidates since 5.1.0 (task 2124)', () => {
|
|
137
164
|
test('inlineCandidatesIn reads both quote kinds and skips markers', () => {
|
|
138
165
|
const css = `/* banner */\n@source inline("flex sm:hidden peer");\n@source inline('before:content-[""]');\n`;
|
|
@@ -203,3 +230,66 @@ describe('🔴 the ORDER check — a base utility after a variant that sets the
|
|
|
203
230
|
}
|
|
204
231
|
});
|
|
205
232
|
});
|
|
233
|
+
|
|
234
|
+
describe('🔴 the THEME VARIABLE check — a `var()` of a theme variable no built stylesheet declares (5.3.0)', () => {
|
|
235
|
+
// station's DataTable frozen column and DropIndicator, verbatim in shape from its minified bundle.
|
|
236
|
+
const STATION_JS =
|
|
237
|
+
'style:Je?{"--cb-frozen-bg":ke%2===1?"color-mix(in srgb, var(--color-muted) 50%, var(--color-card))":"var(--color-card)"}:void 0,' +
|
|
238
|
+
'function ni({orientation:e,side:t,color:r}){const n=r??"var(--color-accent)"}';
|
|
239
|
+
const APP = '@import "tailwindcss" source(none);\n@import "./styles-static.css";\n';
|
|
240
|
+
const THEME_READ_BLOCK = /\n\/\*\n \* Theme variables cursedbelt's components read BY NAME[\s\S]*$/;
|
|
241
|
+
|
|
242
|
+
test("station's pre-fix shape — core WITHOUT the theme-read block — is RED on exactly the three it lost", async () => {
|
|
243
|
+
const core = readBeltSheets().areas.core ?? '';
|
|
244
|
+
expect(core).toMatch(THEME_READ_BLOCK); // the block exists, so stripping it below is the 5.2.0 core
|
|
245
|
+
const preFix = (await compileStylesheet(`${APP}${core.replace(THEME_READ_BLOCK, '\n')}\n@import "./theme.css";\n`)).build([]);
|
|
246
|
+
expect(undefinedThemeVariables({ js: [STATION_JS], css: [preFix] }, readThemeVariables())).toEqual([
|
|
247
|
+
'--color-accent',
|
|
248
|
+
'--color-card',
|
|
249
|
+
'--color-muted',
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('the fix: the shipped core makes the app\'s ONE pass declare every theme variable a component reads by name', async () => {
|
|
254
|
+
const fixed = (await compileStylesheet(`${APP}@import "./styles-areas/core.css";\n@import "./theme.css";\n`)).build([]);
|
|
255
|
+
expect(undefinedThemeVariables({ js: [STATION_JS], css: [fixed] }, readThemeVariables())).toEqual([]);
|
|
256
|
+
// …and what core names is what the check scopes to — every one a theme variable.
|
|
257
|
+
const block = THEME_READ_BLOCK.exec(readBeltSheets().areas.core ?? '')?.[0] ?? '';
|
|
258
|
+
const named = [...block.matchAll(/var\((--[\w-]+)\)/g)].map((m) => m[1]);
|
|
259
|
+
const theme = readThemeVariables();
|
|
260
|
+
expect(named.filter((n) => !theme.has(n as string))).toEqual([]);
|
|
261
|
+
for (const v of ['--color-card', '--color-muted', '--color-accent', '--text-caption']) expect(named).toContain(v);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('scope: a runtime-set or plain custom property is not a theme variable, and a fallback does not excuse one', () => {
|
|
265
|
+
const theme = new Set(['--color-card']);
|
|
266
|
+
const js = ['"var(--offset-right)" "var(--radix-popper-available-height)" "var(--color-card, white)"'];
|
|
267
|
+
expect(undefinedThemeVariables({ js, css: [''] }, theme)).toEqual(['--color-card']);
|
|
268
|
+
expect(undefinedThemeVariables({ js, css: [':root{--color-card:var(--card)}'] }, theme)).toEqual([]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('themeVariablesIn reads every @theme form and skips Tailwind\'s `initial` (unset) keys', () => {
|
|
272
|
+
const css =
|
|
273
|
+
'@theme inline { --color-card: var(--card); }\n@theme default { --text-sm: .875rem; ' +
|
|
274
|
+
'--default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial); --x: initial; }\n:root { --card: #fff; }';
|
|
275
|
+
expect([...themeVariablesIn(css)].sort()).toEqual(['--color-card', '--text-sm']);
|
|
276
|
+
expect(readThemeVariables().has('--color-card')).toBe(true); // theme.css
|
|
277
|
+
expect(readThemeVariables().has('--ease-out')).toBe(true); // tailwindcss/theme.css, resolved
|
|
278
|
+
expect([...variablesDeclared('@property --tw-x{syntax:"*"}.a{--b:1;color:var(--c)}')].sort()).toEqual(['--b', '--tw-x']);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('the CLI exits 1 on a build that reads an undeclared theme variable, naming it', () => {
|
|
282
|
+
const dir = mkdtempSync(join(tmpdir(), 'belt-theme-var-check-'));
|
|
283
|
+
try {
|
|
284
|
+
mkdirSync(join(dir, 'assets'));
|
|
285
|
+
writeFileSync(join(dir, 'assets', 'index.js'), `const c="truncate";${STATION_JS}`);
|
|
286
|
+
writeFileSync(join(dir, 'assets', 'index.css'), '@layer theme{:root{--color-muted:var(--muted);--color-accent:var(--accent)}}@layer utilities{.truncate{overflow:hidden}}');
|
|
287
|
+
const r = Bun.spawnSync(['bun', join(import.meta.dir, 'checkAreaStyles.ts'), dir], { stdout: 'pipe', stderr: 'pipe' });
|
|
288
|
+
expect(r.exitCode).toBe(1);
|
|
289
|
+
expect(r.stderr.toString()).toContain('--color-card');
|
|
290
|
+
expect(r.stderr.toString()).not.toContain('--color-muted');
|
|
291
|
+
} finally {
|
|
292
|
+
rmSync(dir, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|