@justai/cuts 0.40.0 → 0.42.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.40.0",
3
+ "version": "0.42.0",
4
4
  "description": "A persona's named parts — the page shell that holds them, and the cuts themselves.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -51,11 +51,18 @@ interface Props {
51
51
  // without knowing this component's internals.
52
52
  /** Key for a runtime translator — lands on the LABEL, never on the row. */
53
53
  i18n?: string;
54
+ /**
55
+ * ANYTHING ELSE LANDS ON THE BUTTON. A consumer's `data-*` is how strings reach a bundled script
56
+ * in this ecosystem — a module script cannot read the frontmatter, so the DOM carries them — and
57
+ * a component that silently swallowed them would send the consumer looking for a bug in its own
58
+ * code. Astro drops unknown props unless they are spread, so they are spread.
59
+ */
60
+ [key: string]: unknown;
54
61
  }
55
- const { id, label, tone = "plain", detail, hidden, i18n } = Astro.props;
62
+ const { id, label, tone = "plain", detail, hidden, i18n, ...rest } = Astro.props;
56
63
  ---
57
64
 
58
- <button class:list={["jal-row", tone !== "plain" && `is-${tone}`]} type="button" id={id} hidden={hidden}>
65
+ <button class:list={["jal-row", tone !== "plain" && `is-${tone}`]} type="button" id={id} hidden={hidden} {...rest}>
59
66
  <span class="jal-ico" aria-hidden="true"><slot name="icon" /></span>
60
67
  <span class="jal-label" id={id ? `${id}-label` : undefined} data-i={i18n}>{label}</span>
61
68
  {detail && <span class="jal-detail">{detail}</span>}
@@ -43,8 +43,9 @@ import Sheet from "./Sheet.astro";
43
43
  *
44
44
  * const saved = await document.getElementById("logoPicker").save({
45
45
  * name: "café wifi.qr", bytes, mime: "application/vnd.justai.qr+json",
46
+ * thumb: await canvas.convertToBlob(), // optional: give the file a face
46
47
  * });
47
- * // → { path, name } | null
48
+ * // → { path, id, name } | null
48
49
  *
49
50
  * One element rather than two cuts, because it is one room: two frames would be two documents,
50
51
  * two atrium routers and two copies of the same kernel open at once, to ask the same file system
@@ -152,6 +153,19 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
152
153
  /** Save BACK to a file already saved once. NO QUESTION IS ASKED: it was answered the first time. */
153
154
  id?: string;
154
155
  bytes: ArrayBuffer | Uint8Array;
156
+ /**
157
+ * THE FILE'S FACE — the ecosystem's optional standard, and worth taking.
158
+ *
159
+ * Most files can show themselves; a persona's OWN format cannot, and the persona is the only
160
+ * thing in the system that knows what it looks like. Hand over a small render (a 256px PNG is
161
+ * plenty) and the file has a face in the list and in its detail sheet instead of a large empty
162
+ * box saying there is no preview for this kind. Say nothing and nothing is lost.
163
+ *
164
+ * Kept small on purpose: the kernel drops a face over 64 KB rather than let a thumbnail become
165
+ * a second copy of the work in an evictable quota. Over the ceiling the FILE is still written —
166
+ * a cosmetic loss must never become a refused save.
167
+ */
168
+ thumb?: ArrayBuffer | Uint8Array | Blob;
155
169
  mime?: string;
156
170
  title?: string;
157
171
  timeout?: number;
@@ -302,10 +316,19 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
302
316
  // text does) would otherwise have to know that the kernel wants the buffer, and that a typed
303
317
  // array's `.buffer` may be a larger pool it is only a window onto — a subtle way to save a
304
318
  // file with somebody else's bytes stuck to the end of it.
305
- const b: any = req.bytes;
306
- const bytes: ArrayBuffer =
307
- b instanceof ArrayBuffer ? b : b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
308
- const args = { name: req.name, ext: req.ext, id: req.id, bytes, mime: req.mime };
319
+ // THE CUT BRINGS ITS OWN GROUND for both payloads. A caller holds whatever its work produced
320
+ // a typed array from an encoder, a Blob from a canvas — and must not have to know that a
321
+ // typed array's `.buffer` may be a larger pool it is only a window onto, which is a subtle
322
+ // way to save a file with somebody else's bytes stuck to the end of it.
323
+ const buffer = async (v: any): Promise<ArrayBuffer | undefined> => {
324
+ if (!v) return undefined;
325
+ if (v instanceof ArrayBuffer) return v;
326
+ if (typeof Blob !== "undefined" && v instanceof Blob) return v.arrayBuffer();
327
+ return v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength);
328
+ };
329
+ const bytes = (await buffer(req.bytes))!;
330
+ const thumb = await buffer(req.thumb);
331
+ const args = { name: req.name, ext: req.ext, id: req.id, bytes, thumb, mime: req.mime };
309
332
  // SAVING BACK OPENS NO SHEET, and that is the point rather than an optimisation. The person
310
333
  // named this file the first time; asking again on every save is not care, it is a persona
311
334
  // that cannot remember what it is working on. So this path never touches `ask`.