@justai/cuts 0.72.0 → 0.74.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.72.0",
3
+ "version": "0.74.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": {
@@ -85,9 +85,15 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
85
85
 
86
86
  {/* NO `Done` here, deliberately. This sheet is a QUESTION, and its answer is a row being
87
87
  tapped inside the frame — so a Done that merely closed would be a Cancel wearing the word for
88
- finishing, on the one surface where the difference decides what the caller receives. The kernel
89
- inside shows its own Cancel, worded as the choice it actually is. */}
90
- <Sheet id={`${id}-sheet`} title={title} detent="large" done={false}>
88
+ finishing, on the one surface where the difference decides what the caller receives.
89
+
90
+ CANCEL IS IN THE BAR (2026-08-01). It used to be the kernel's own, in a footer beside Upload,
91
+ Camera and Record — which put it at the rank of the three SOURCES when it is not one of them:
92
+ those answer "where shall I get it", and this answers "nowhere". Apple's guidance is the same
93
+ shape — relying on Done alone "implies that completing the task is the only way to exit the
94
+ sheet" — and the leading edge of the bar is where iOS puts the way out. The kernel inside stops
95
+ drawing its own while it is somebody's guest; one exit, in the place a person already looks. */}
96
+ <Sheet id={`${id}-sheet`} title={title} detent="large" done={false} cancel="Cancel">
91
97
  <div class="jfp" id={id} data-picker={id} data-src={src}>
92
98
  {/* Empty until the first open. `title` is the accessible name of the document inside — a frame
93
99
  announced as "frame" is the one control on the page that says nothing about itself. */}
@@ -282,7 +288,11 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
282
288
  bus.proxy({
283
289
  service: "files",
284
290
  method: "pick",
285
- args: { accept: req.accept },
291
+ // `hostExit`: this cut draws the way out in its own sheet's bar, so the kernel must not
292
+ // draw a second one in its footer beside the three sources. TOLD, never inferred — the
293
+ // kernel is also embedded by hosts that frame it bare and give it no chrome at all, and
294
+ // one that hid its own exit on a guess would leave that person unable to answer "nowhere".
295
+ args: { accept: req.accept, hostExit: true },
286
296
  options: { timeout: req.timeout ?? 300000 },
287
297
  }));
288
298
  if (!result) return null;
package/src/Sheet.astro CHANGED
@@ -53,6 +53,23 @@ import "./ground.css";
53
53
  interface Props {
54
54
  /** DOM id — the handle `data-sheet-open` points at. */
55
55
  id: string;
56
+ // queries: cancel button on a sheet · how do I get out of a sheet without answering · sheet
57
+ // cancel top left · Cancel Done and Back together · dismiss a question
58
+ //
59
+ // ── THE WAY OUT, IN THE BAR ──────────────────────────────────────────────────────────────
60
+ //
61
+ // A sheet that ASKS something needs a visible way to answer "neither" — and Apple's own guidance
62
+ // is that relying on Done alone "implies that completing the task is the only way to exit the
63
+ // sheet, which can feel restrictive or misleading". So a question carries a leading dismiss.
64
+ //
65
+ // It is the LEADING edge because that is where iOS puts it and because Done owns the trailing
66
+ // one; a cancel on the right would be a control that looks like a commit. And it gives way to the
67
+ // BACK button when a page is pushed — the same guidance says not to show Cancel, Done and Back
68
+ // together, and going back is the way out of a page that has one.
69
+ //
70
+ // Default off: a sheet that only SHOWS something already has a grabber and a backdrop, and a
71
+ // labelled exit on a surface with nothing to abandon is a control asking a question nobody has.
72
+ cancel?: string | false;
56
73
  /** Shown at the top of the panel, beside the grabber. Omit for a bare sheet. */
57
74
  title?: string;
58
75
  /** How tall it opens. `medium` is iOS's half-height default; drag up promotes it to large. */
@@ -97,9 +114,10 @@ interface Props {
97
114
  // hardcoded the English would be a component making its consumer's copy decisions.
98
115
  done?: string | false;
99
116
  }
100
- const { id, title, detent = "medium", done, floorToParent = true } = Astro.props;
117
+ const { id, title, detent = "medium", done, cancel, floorToParent = true } = Astro.props;
101
118
  // Default: shown at large, absent at medium. `false` removes it, a string replaces the word.
102
119
  const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done" : null));
120
+ const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
103
121
  ---
104
122
 
105
123
  {/* aria-labelledby over aria-label when there IS a title, so the accessible name and the visible
@@ -136,6 +154,9 @@ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done"
136
154
 
137
155
  NO HOMEWORK: a consumer writes markup and two attributes. No script, and the sheet still
138
156
  works exactly as before for the consumers that never declare a page. */}
157
+ {cancelLabel && (
158
+ <button class="sheet-cancel" id={`${id}-cancel`} type="button" data-sheet-cancel>{cancelLabel}</button>
159
+ )}
139
160
  <button class="sheet-back" id={`${id}-back`} type="button" data-sheet-back hidden>
140
161
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>
141
162
  <span data-sheet-back-label></span>
@@ -206,6 +227,19 @@ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done"
206
227
  :global([data-sheet-page].is-current), :global([data-sheet-page].is-popping) { animation: none; }
207
228
  }
208
229
 
230
+ /* MIRRORS DONE ACROSS THE BAR — same band, same weight, opposite edge — and it is not tinted
231
+ like a commit: leaving without answering is an ordinary thing to do, not the reason you came. */
232
+ .sheet-cancel {
233
+ position: absolute; inset-block-start: 0; inset-inline-start: 0;
234
+ background: none; border: 0; cursor: pointer; padding: 12px 16px;
235
+ font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--text-dim);
236
+ }
237
+ .sheet-cancel:active { opacity: 0.6; }
238
+ .sheet-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
239
+ /* Cancel, Done and Back must never stand together — going back IS the way out of a pushed page,
240
+ so the leading slot belongs to whichever of the two is meaningful right now. */
241
+ .sheet:has(.sheet-back:not([hidden])) .sheet-cancel { display: none; }
242
+
209
243
  .sheet-done {
210
244
  position: absolute; inset-block-start: 0; inset-inline-end: 0;
211
245
  background: none; border: 0; cursor: pointer; padding: 12px 16px;
@@ -586,6 +620,16 @@ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done"
586
620
  (d.closest("dialog.sheet") as HTMLDialogElement | null)?.close();
587
621
  });
588
622
 
623
+ // Cancel closes too, and the difference is not in what it DOES — it is in what the caller may
624
+ // conclude. A sheet that resolves a promise reads its own `close` event and settles null there,
625
+ // so a cancel, a backdrop tap and a drag down are one path with one answer. Two words, one exit,
626
+ // and no second place for a caller to be told something different.
627
+ document.addEventListener("click", (e) => {
628
+ const c = (e.target as HTMLElement)?.closest?.("[data-sheet-cancel]");
629
+ if (!c) return;
630
+ (c.closest("dialog.sheet") as HTMLDialogElement | null)?.close();
631
+ });
632
+
589
633
  document.addEventListener("click", (e) => {
590
634
  const t = (e.target as HTMLElement)?.closest?.("[data-sheet-open]");
591
635
  if (!t) return;