@justai/cuts 0.42.0 → 0.43.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.42.0",
3
+ "version": "0.43.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": {
@@ -72,7 +72,11 @@ interface Props {
72
72
  const { id, title = "Choose a file", src = "/files" } = Astro.props;
73
73
  ---
74
74
 
75
- <Sheet id={`${id}-sheet`} title={title} detent="large">
75
+ {/* NO `Done` here, deliberately. This sheet is a QUESTION, and its answer is a row being
76
+ tapped inside the frame — so a Done that merely closed would be a Cancel wearing the word for
77
+ finishing, on the one surface where the difference decides what the caller receives. The kernel
78
+ inside shows its own Cancel, worded as the choice it actually is. */}
79
+ <Sheet id={`${id}-sheet`} title={title} detent="large" done={false}>
76
80
  <div class="jfp" id={id} data-picker={id} data-src={src}>
77
81
  {/* Empty until the first open. `title` is the accessible name of the document inside — a frame
78
82
  announced as "frame" is the one control on the page that says nothing about itself. */}
package/src/Sheet.astro CHANGED
@@ -57,8 +57,31 @@ interface Props {
57
57
  title?: string;
58
58
  /** How tall it opens. `medium` is iOS's half-height default; drag up promotes it to large. */
59
59
  detent?: "medium" | "large";
60
+ // queries: should a sheet have a Done button · how do I close a full height sheet · sheet done ·
61
+ // the exit is not obvious · dismiss a large sheet
62
+ //
63
+ // DONE MEANS FINISHED, NOT APPLIED — and that distinction decides whether it belongs.
64
+ //
65
+ // A sheet whose changes are STAGED needs a commit: Done applies, Cancel discards. A sheet whose
66
+ // changes are LIVE — every tap already redrawn behind you — has nothing to commit, and a button
67
+ // labelled Save or Apply on one of those is a lie about what pressing it does.
68
+ //
69
+ // So why offer it at all? THE EXIT. At the medium detent the page is visible behind the sheet and
70
+ // the grabber sits in plain sight, so leaving is obvious. At `large` the sheet is the screen: a
71
+ // person has to already know that a swipe down or an Escape will work, and "already knowing" is
72
+ // not an affordance. iOS answers the same way — its half-height sheets carry no Done and its
73
+ // full-height ones do.
74
+ //
75
+ // Hence the rule this cut applies: **offered at `large`, absent at `medium`.** A consumer that
76
+ // wants it anyway passes a label; one that wants it gone passes `done={false}`.
77
+ //
78
+ // It is a LABEL rather than a boolean because the fleet speaks thirteen languages, and a cut that
79
+ // hardcoded the English would be a component making its consumer's copy decisions.
80
+ done?: string | false;
60
81
  }
61
- const { id, title, detent = "medium" } = Astro.props;
82
+ const { id, title, detent = "medium", done } = Astro.props;
83
+ // Default: shown at large, absent at medium. `false` removes it, a string replaces the word.
84
+ const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done" : null));
62
85
  ---
63
86
 
64
87
  {/* aria-labelledby over aria-label when there IS a title, so the accessible name and the visible
@@ -78,11 +101,26 @@ const { id, title, detent = "medium" } = Astro.props;
78
101
  keyboard/assistive paths, and a decorative pill announced as a control is noise. */}
79
102
  <div class="sheet-grip" data-grip aria-hidden="true"><span></span></div>
80
103
  {title && <h2 class="sheet-title" id={`${id}-title`}>{title}</h2>}
104
+ {/* `id`-suffixed for the same reason the title is: a surface that translates at RUNTIME needs a
105
+ stable hook to re-label it from its own dictionary. */}
106
+ {doneLabel && (
107
+ <button class="sheet-done" id={`${id}-done`} type="button" data-sheet-done>{doneLabel}</button>
108
+ )}
81
109
  <div class="sheet-body"><slot /></div>
82
110
  </div>
83
111
  </dialog>
84
112
 
85
113
  <style>
114
+ /* Trailing and level with the title, where a navigation bar's right-hand action lives — the one
115
+ place a thumb looks for "I am finished here". */
116
+ .sheet-done {
117
+ position: absolute; inset-block-start: 0; inset-inline-end: 0;
118
+ background: none; border: 0; cursor: pointer; padding: 12px 16px;
119
+ font: inherit; font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
120
+ }
121
+ .sheet-done:active { opacity: 0.6; }
122
+ .sheet-done:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
123
+
86
124
  .sheet {
87
125
  /* The dialog is the full-screen STAGE; the PANEL is the sheet.
88
126
  `position: fixed` is written out rather than inherited: the UA gives a dialog
@@ -127,6 +165,7 @@ const { id, title, detent = "medium" } = Astro.props;
127
165
  }
128
166
 
129
167
  .sheet-panel {
168
+ position: relative; /* the Done action is positioned against this */
130
169
  display: flex;
131
170
  flex-direction: column;
132
171
  /* THE ONE SIZING RULE, and there is no media query anywhere in this file. The persona's column
@@ -275,6 +314,15 @@ const { id, title, detent = "medium" } = Astro.props;
275
314
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", wireAll);
276
315
  else wireAll();
277
316
 
317
+ // Done closes, and NOTHING ELSE. It is not a commit — a sheet whose changes are live has
318
+ // nothing to commit — so it must not become a hook consumers hang saving off, or the word
319
+ // starts meaning two things in two personas.
320
+ document.addEventListener("click", (e) => {
321
+ const d = (e.target as HTMLElement)?.closest?.("[data-sheet-done]");
322
+ if (!d) return;
323
+ (d.closest("dialog.sheet") as HTMLDialogElement | null)?.close();
324
+ });
325
+
278
326
  document.addEventListener("click", (e) => {
279
327
  const t = (e.target as HTMLElement)?.closest?.("[data-sheet-open]");
280
328
  if (!t) return;