@justai/cuts 0.71.0 → 0.73.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.71.0",
3
+ "version": "0.73.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": {
@@ -45,7 +45,15 @@ const { title, note } = Astro.props;
45
45
  </div>
46
46
 
47
47
  <style>
48
+ /* queries: two action lists touching · no gap between groups · the second group sits on the first
49
+ THE BREAK IS THE GRAMMAR, so the component owns it.
50
+ A group carried its INTERNAL 7px and nothing between itself and the next one, which left the
51
+ break — the thing that separates "things you might do" from "the door with no way back" — to
52
+ whichever consumer remembered a wrapper. files did in one sheet and not in another, and the
53
+ second one read as one long list with a red row at the end.
54
+ `:last-child` so a lone group adds no trailing space it did not ask for. */
48
55
  .jal-group { display: flex; flex-direction: column; gap: 7px; }
56
+ .jal-group + .jal-group { margin-block-start: 22px; }
49
57
  /* Inset and grouped: the card is the group, and `overflow: hidden` is what lets a row's own
50
58
  background reach the rounded corner instead of squaring it off on press. */
51
59
  .jal {
@@ -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. */}
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>
@@ -186,6 +207,17 @@ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done"
186
207
  Only one page is in the layout at a time, so the body's height is the CURRENT page's — which is
187
208
  what makes the sheet grow and shrink around the content the way a navigation stack does. The
188
209
  slide is 220ms and is skipped for reduced motion, where the pages simply swap. */
210
+ /* queries: the second sheet page appears BELOW the first · pushing shows both pages · the history
211
+ list is squeezed at the bottom · a page does not hide · consumer css beats the sheet
212
+ HIDING A PAGE IS A MECHANISM, NOT A SUGGESTION, so it is written where a class cannot outrank
213
+ it. `[data-sheet-page] { display: none }` is one attribute — specificity (0,1,0) — and any
214
+ consumer rule naming two classes beats it. files styles its detail page as `.pp .fd { display:
215
+ flex }`, which is ordinary and correct, and it meant the root page NEVER HID: pushing to the
216
+ versions drew them one under the other, so a person who tapped History got a cramped strip at
217
+ the bottom of a sheet still showing everything they had just left.
218
+ Qualified by the dialog it lives in, which raises this to (0,2,1) and keeps the rule honest —
219
+ a page outside a sheet is nobody's business here. */
220
+ :global(dialog [data-sheet-page]:not(.is-current):not(.is-popping)) { display: none; }
189
221
  :global([data-sheet-page]) { display: none; }
190
222
  :global([data-sheet-page].is-current) { display: block; animation: sheet-push 0.22s var(--ease-out) both; }
191
223
  :global([data-sheet-page].is-popping) { display: block; animation: sheet-pop 0.22s var(--ease-out) both; }
@@ -195,6 +227,19 @@ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done"
195
227
  :global([data-sheet-page].is-current), :global([data-sheet-page].is-popping) { animation: none; }
196
228
  }
197
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
+
198
243
  .sheet-done {
199
244
  position: absolute; inset-block-start: 0; inset-inline-end: 0;
200
245
  background: none; border: 0; cursor: pointer; padding: 12px 16px;
@@ -575,6 +620,16 @@ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done"
575
620
  (d.closest("dialog.sheet") as HTMLDialogElement | null)?.close();
576
621
  });
577
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
+
578
633
  document.addEventListener("click", (e) => {
579
634
  const t = (e.target as HTMLElement)?.closest?.("[data-sheet-open]");
580
635
  if (!t) return;