@justai/cuts 0.32.0 → 0.33.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.33.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": {
@@ -94,7 +94,16 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
94
94
  // would silently take the first one's place.
95
95
  import atrium from "@justai/atrium";
96
96
 
97
- interface Picked { path: string; name: string; mime: string; bytes: ArrayBuffer }
97
+ interface Picked {
98
+ path: string;
99
+ name: string;
100
+ mime: string;
101
+ bytes: ArrayBuffer;
102
+ /** The same file as a Blob — for URL.createObjectURL, an upload body, an <img> src. */
103
+ blob(): Blob;
104
+ /** The same file as a data: URL — for anything that must EMBED it (an SVG, a saved document). */
105
+ dataUrl(): Promise<string>;
106
+ }
98
107
  interface PickRequest { accept?: string; title?: string; timeout?: number }
99
108
 
100
109
  for (const root of document.querySelectorAll<HTMLElement>("[data-picker]")) {
@@ -139,6 +148,11 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
139
148
  async function pick(req: PickRequest = {}): Promise<Picked | null> {
140
149
  const bus = await router();
141
150
  if (!frame.getAttribute("src")) frame.setAttribute("src", root.dataset.src!);
151
+ // ONE HEADING, and it is the sheet's. A per-call title retitles the sheet rather than being
152
+ // forwarded into the frame: the kernel renders a heading of its own for callers that have no
153
+ // sheet around them, and sending it one here printed "Upload logo" twice, one under the other.
154
+ const heading = sheet.querySelector<HTMLElement>(".sheet-title");
155
+ if (heading && req.title) heading.textContent = req.title;
142
156
  sheet.showModal();
143
157
 
144
158
  // TWO WAYS THIS ENDS, and only one of them is the kernel's.
@@ -155,14 +169,36 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
155
169
  .proxy({
156
170
  service: "files",
157
171
  method: "pick",
158
- args: { accept: req.accept, title: req.title },
172
+ args: { accept: req.accept },
159
173
  options: { timeout: req.timeout ?? 300000 },
160
174
  }))
161
175
  .catch(() => null);
162
176
 
163
177
  const result = await Promise.race([answered, dismissed]);
164
178
  if (sheet.open) sheet.close();
165
- return (result as Picked) ?? null;
179
+ if (!result) return null;
180
+
181
+ // THE CUT BRINGS ITS OWN GROUND, and bytes are not ground. Every consumer of this needs the
182
+ // file in some other shape, and the obvious hand-written conversion is a trap:
183
+ //
184
+ // btoa(String.fromCharCode(...new Uint8Array(bytes)))
185
+ //
186
+ // makes one argument per BYTE, so a 7.5 MB photo is a 7.5-million-argument call and the stack
187
+ // gives out — "Maximum call stack size exceeded", thrown far from anything that mentions size,
188
+ // and invisible to any test whose fixture image is small. qr wrote exactly that line and a
189
+ // person found it with a real photograph. It belongs here once, not in every persona.
190
+ const r = result as any;
191
+ const blob = () => new Blob([r.bytes], { type: r.mime });
192
+ return {
193
+ ...r,
194
+ blob,
195
+ dataUrl: () => new Promise<string>((resolve, reject) => {
196
+ const rd = new FileReader();
197
+ rd.onload = () => resolve(rd.result as string);
198
+ rd.onerror = () => reject(rd.error);
199
+ rd.readAsDataURL(blob()); // no ceiling, unlike the spread above
200
+ }),
201
+ } as Picked;
166
202
  }
167
203
 
168
204
  (host as any).pick = pick;