@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@justai/cuts",
3
- "version": "0.32.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.5.0",
34
+ "@justai/core": "^0.6.0",
35
35
  "@justai/ui": "^0.3.0",
36
36
  "@justai/atrium": "^0.1.9"
37
37
  }
@@ -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 { path: string; name: string; mime: string; bytes: ArrayBuffer }
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")) frame.setAttribute("src", root.dataset.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, title: req.title },
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
- return (result as Picked) ?? null;
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;