@justai/cuts 0.32.0 → 0.34.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/package.json +2 -2
- package/src/FilePicker.astro +49 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "A persona's named parts \u2014 the page shell that holds them, and the cuts themselves.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@justai/core": "^0.
|
|
34
|
+
"@justai/core": "^0.6.0",
|
|
35
35
|
"@justai/ui": "^0.3.0",
|
|
36
36
|
"@justai/atrium": "^0.1.9"
|
|
37
37
|
}
|
package/src/FilePicker.astro
CHANGED
|
@@ -93,8 +93,18 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
93
93
|
// two kernels at once would otherwise have both arrive calling themselves "files", and the second
|
|
94
94
|
// would silently take the first one's place.
|
|
95
95
|
import atrium from "@justai/atrium";
|
|
96
|
+
import { PRESENTATION_PARAM, PRESENTATION_EMBEDDED } from "@justai/core";
|
|
96
97
|
|
|
97
|
-
interface Picked {
|
|
98
|
+
interface Picked {
|
|
99
|
+
path: string;
|
|
100
|
+
name: string;
|
|
101
|
+
mime: string;
|
|
102
|
+
bytes: ArrayBuffer;
|
|
103
|
+
/** The same file as a Blob — for URL.createObjectURL, an upload body, an <img> src. */
|
|
104
|
+
blob(): Blob;
|
|
105
|
+
/** The same file as a data: URL — for anything that must EMBED it (an SVG, a saved document). */
|
|
106
|
+
dataUrl(): Promise<string>;
|
|
107
|
+
}
|
|
98
108
|
interface PickRequest { accept?: string; title?: string; timeout?: number }
|
|
99
109
|
|
|
100
110
|
for (const root of document.querySelectorAll<HTMLElement>("[data-picker]")) {
|
|
@@ -138,7 +148,20 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
138
148
|
|
|
139
149
|
async function pick(req: PickRequest = {}): Promise<Picked | null> {
|
|
140
150
|
const bus = await router();
|
|
141
|
-
if (!frame.getAttribute("src"))
|
|
151
|
+
if (!frame.getAttribute("src")) {
|
|
152
|
+
// DECLARE THE PRESENTATION. A page opened by a person and a page opened by another persona
|
|
153
|
+
// are two different situations, and the same controls do not belong in both — standalone
|
|
154
|
+
// there is nothing to cancel. The frame is the slower truth (a handshake takes a beat), so
|
|
155
|
+
// the declaration rides in the URL and is available in the first paint. It is presentation
|
|
156
|
+
// only: nothing that must not be forged is decided by it.
|
|
157
|
+
const src = root.dataset.src!;
|
|
158
|
+
frame.setAttribute("src", src + (src.includes("?") ? "&" : "?") + PRESENTATION_PARAM + "=" + PRESENTATION_EMBEDDED);
|
|
159
|
+
}
|
|
160
|
+
// ONE HEADING, and it is the sheet's. A per-call title retitles the sheet rather than being
|
|
161
|
+
// forwarded into the frame: the kernel renders a heading of its own for callers that have no
|
|
162
|
+
// sheet around them, and sending it one here printed "Upload logo" twice, one under the other.
|
|
163
|
+
const heading = sheet.querySelector<HTMLElement>(".sheet-title");
|
|
164
|
+
if (heading && req.title) heading.textContent = req.title;
|
|
142
165
|
sheet.showModal();
|
|
143
166
|
|
|
144
167
|
// TWO WAYS THIS ENDS, and only one of them is the kernel's.
|
|
@@ -155,14 +178,36 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
|
155
178
|
.proxy({
|
|
156
179
|
service: "files",
|
|
157
180
|
method: "pick",
|
|
158
|
-
args: { accept: req.accept
|
|
181
|
+
args: { accept: req.accept },
|
|
159
182
|
options: { timeout: req.timeout ?? 300000 },
|
|
160
183
|
}))
|
|
161
184
|
.catch(() => null);
|
|
162
185
|
|
|
163
186
|
const result = await Promise.race([answered, dismissed]);
|
|
164
187
|
if (sheet.open) sheet.close();
|
|
165
|
-
|
|
188
|
+
if (!result) return null;
|
|
189
|
+
|
|
190
|
+
// THE CUT BRINGS ITS OWN GROUND, and bytes are not ground. Every consumer of this needs the
|
|
191
|
+
// file in some other shape, and the obvious hand-written conversion is a trap:
|
|
192
|
+
//
|
|
193
|
+
// btoa(String.fromCharCode(...new Uint8Array(bytes)))
|
|
194
|
+
//
|
|
195
|
+
// makes one argument per BYTE, so a 7.5 MB photo is a 7.5-million-argument call and the stack
|
|
196
|
+
// gives out — "Maximum call stack size exceeded", thrown far from anything that mentions size,
|
|
197
|
+
// and invisible to any test whose fixture image is small. qr wrote exactly that line and a
|
|
198
|
+
// person found it with a real photograph. It belongs here once, not in every persona.
|
|
199
|
+
const r = result as any;
|
|
200
|
+
const blob = () => new Blob([r.bytes], { type: r.mime });
|
|
201
|
+
return {
|
|
202
|
+
...r,
|
|
203
|
+
blob,
|
|
204
|
+
dataUrl: () => new Promise<string>((resolve, reject) => {
|
|
205
|
+
const rd = new FileReader();
|
|
206
|
+
rd.onload = () => resolve(rd.result as string);
|
|
207
|
+
rd.onerror = () => reject(rd.error);
|
|
208
|
+
rd.readAsDataURL(blob()); // no ceiling, unlike the spread above
|
|
209
|
+
}),
|
|
210
|
+
} as Picked;
|
|
166
211
|
}
|
|
167
212
|
|
|
168
213
|
(host as any).pick = pick;
|