@justai/cuts 0.73.0 → 0.75.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.73.0",
3
+ "version": "0.75.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": {
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@justai/core": "^0.9.0",
39
+ "@justai/core": "^0.10.0",
40
40
  "@justai/ui": "^0.4.0",
41
41
  "@justai/atrium": "^0.1.9"
42
42
  }
@@ -93,7 +93,7 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
93
93
  shape — relying on Done alone "implies that completing the task is the only way to exit the
94
94
  sheet" — and the leading edge of the bar is where iOS puts the way out. The kernel inside stops
95
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">
96
+ <Sheet id={`${id}-sheet`} title={title} detent="large" done={false} cancel>
97
97
  <div class="jfp" id={id} data-picker={id} data-src={src}>
98
98
  {/* Empty until the first open. `title` is the accessible name of the document inside — a frame
99
99
  announced as "frame" is the one control on the page that says nothing about itself. */}
@@ -288,7 +288,11 @@ const { id, title = "Choose a file", src = "/files" } = Astro.props;
288
288
  bus.proxy({
289
289
  service: "files",
290
290
  method: "pick",
291
- 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 },
292
296
  options: { timeout: req.timeout ?? 300000 },
293
297
  }));
294
298
  if (!result) return null;
package/src/Sheet.astro CHANGED
@@ -69,7 +69,10 @@ interface Props {
69
69
  //
70
70
  // Default off: a sheet that only SHOWS something already has a grabber and a backdrop, and a
71
71
  // labelled exit on a surface with nothing to abandon is a control asking a question nobody has.
72
- cancel?: string | false;
72
+ //
73
+ // `true` asks for THE ECOSYSTEM'S WORD, translated into the page's language by this cut — which
74
+ // is what a consumer wants almost every time. Pass a string only to say something else on purpose.
75
+ cancel?: string | boolean;
73
76
  /** Shown at the top of the panel, beside the grabber. Omit for a bare sheet. */
74
77
  title?: string;
75
78
  /** How tall it opens. `medium` is iOS's half-height default; drag up promotes it to large. */
@@ -110,14 +113,26 @@ interface Props {
110
113
  // Hence the rule this cut applies: **offered at `large`, absent at `medium`.** A consumer that
111
114
  // wants it anyway passes a label; one that wants it gone passes `done={false}`.
112
115
  //
113
- // It is a LABEL rather than a boolean because the fleet speaks thirteen languages, and a cut that
114
- // hardcoded the English would be a component making its consumer's copy decisions.
115
- done?: string | false;
116
+ // It used to be a LABEL rather than a boolean, reasoned as "the fleet speaks thirteen languages,
117
+ // and a cut that hardcoded the English would be a component making its consumer's copy decisions".
118
+ // That was backwards, and its own default proved it: `"Done"`, hardcoded, in English. Handing the
119
+ // word out does not avoid the decision — it makes it thirteen times per repo and leaves the wrong
120
+ // answer as the default. `Done` is the ECOSYSTEM'S word, and the cut now carries it (law 1).
121
+ done?: string | boolean;
116
122
  }
117
123
  const { id, title, detent = "medium", done, cancel, floorToParent = true } = Astro.props;
118
124
  // Default: shown at large, absent at medium. `false` removes it, a string replaces the word.
119
- const doneLabel = done === false ? null : (done ?? (detent === "large" ? "Done" : null));
120
- const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
125
+ //
126
+ // THREE ANSWERS, AND THE MIDDLE ONE IS THE POINT. `true` asks for the ecosystem's word — rendered
127
+ // English here and corrected to the page's language by the script below. A STRING is a consumer
128
+ // making a deliberate copy decision ("Not now", "Discard") and is left exactly as written. `false`
129
+ // removes the control. Only the ecosystem's own word carries `data-chrome`, so a consumer's never
130
+ // gets overwritten.
131
+ const doneDefault = done === undefined && detent === "large";
132
+ const doneLabel = done === false ? null : (doneDefault || done === true ? "Done" : (done as string | undefined) ?? null);
133
+ const cancelLabel = cancel === false || cancel === undefined ? null : (cancel === true ? "Cancel" : cancel);
134
+ const cancelIsOurs = cancel === true;
135
+ const doneIsOurs = doneDefault || done === true;
121
136
  ---
122
137
 
123
138
  {/* aria-labelledby over aria-label when there IS a title, so the accessible name and the visible
@@ -155,7 +170,7 @@ const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
155
170
  NO HOMEWORK: a consumer writes markup and two attributes. No script, and the sheet still
156
171
  works exactly as before for the consumers that never declare a page. */}
157
172
  {cancelLabel && (
158
- <button class="sheet-cancel" id={`${id}-cancel`} type="button" data-sheet-cancel>{cancelLabel}</button>
173
+ <button class="sheet-cancel" id={`${id}-cancel`} type="button" data-sheet-cancel data-chrome={cancelIsOurs ? "cancel" : undefined}>{cancelLabel}</button>
159
174
  )}
160
175
  <button class="sheet-back" id={`${id}-back`} type="button" data-sheet-back hidden>
161
176
  <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>
@@ -165,7 +180,7 @@ const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
165
180
  {/* `id`-suffixed for the same reason the title is: a surface that translates at RUNTIME needs a
166
181
  stable hook to re-label it from its own dictionary. */}
167
182
  {doneLabel && (
168
- <button class="sheet-done" id={`${id}-done`} type="button" data-sheet-done>{doneLabel}</button>
183
+ <button class="sheet-done" id={`${id}-done`} type="button" data-sheet-done data-chrome={doneIsOurs ? "done" : undefined}>{doneLabel}</button>
169
184
  )}
170
185
  <div class="sheet-body"><slot /></div>
171
186
  </div>
@@ -227,12 +242,20 @@ const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
227
242
  :global([data-sheet-page].is-current), :global([data-sheet-page].is-popping) { animation: none; }
228
243
  }
229
244
 
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. */
245
+ /* queries: cancel colour · should Cancel be tinted · cancel looks disabled · grey cancel ·
246
+ accent on the sheet bar · cancel vs done weight
247
+ MIRRORS DONE ACROSS THE BAR — same band, opposite edge, SAME TINT, LIGHTER WEIGHT.
248
+
249
+ This was grey (`--text-dim`), argued as "leaving without answering is an ordinary thing to do,
250
+ not the reason you came". True about the hierarchy, wrong about the means: on iOS a bar button
251
+ is TINTED, and grey text on a bar is what the platform uses to say UNAVAILABLE. Mail, Calendar
252
+ and Photos all put Cancel in the tint colour beside a bolder Done — the hierarchy is carried by
253
+ WEIGHT, 500 against 600, which is the one channel that does not collide with an existing
254
+ meaning. A dimmed way out reads as a door that will not open. */
232
255
  .sheet-cancel {
233
256
  position: absolute; inset-block-start: 0; inset-inline-start: 0;
234
257
  background: none; border: 0; cursor: pointer; padding: 12px 16px;
235
- font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--text-dim);
258
+ font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--accent);
236
259
  }
237
260
  .sheet-cancel:active { opacity: 0.6; }
238
261
  .sheet-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
@@ -388,6 +411,8 @@ const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
388
411
  </style>
389
412
 
390
413
  <script>
414
+ import { chromeCopy } from "@justai/core";
415
+
391
416
  // The sheet's behaviour, once, for every persona that mounts one. Consumers write no JavaScript:
392
417
  // an element with data-sheet-open="<id>" is a trigger, and that is the whole contract.
393
418
  //
@@ -395,6 +420,37 @@ const cancelLabel = cancel === false || cancel === undefined ? null : cancel;
395
420
  // component still had a desktop branch; now that there is one behaviour, there is one code path,
396
421
  // and a mouse can pull the grabber exactly as a finger does.
397
422
  (function () {
423
+ // queries: sheet says Cancel in English · the picker's cancel is not translated · Done stays
424
+ // English · translate the sheet bar · why does the cut translate itself
425
+ //
426
+ // THE BAR SAYS ITS OWN WORDS, IN THE PAGE'S LANGUAGE.
427
+ //
428
+ // Found on an Arabic page: every word in the picker in Arabic, and `Cancel` in English in the
429
+ // sheet's own bar — because the cut rendered the literal and left translating it to whoever
430
+ // mounted it.
431
+ //
432
+ // WHY AT RUNTIME rather than at build. There is no locale to read at build. The fleet routes
433
+ // languages through a `[lang]` PARAMETER, not Astro's i18n config, so `Astro.currentLocale` is
434
+ // undefined in every persona that has one — and the two surfaces that need this most (`files`
435
+ // and `/me`) are ONE bundle serving thirteen languages, so no build-time answer could be right
436
+ // for them anyway. `<html lang>` is the one place every one of them agrees.
437
+ //
438
+ // A CONSUMER'S WORD ALWAYS WINS. `<Sheet cancel="Not now">` is a deliberate choice and this must
439
+ // not overwrite it — so only the DEFAULTS carry `data-chrome`, and only those are rewritten.
440
+ // That is also what keeps the runtime-translating surfaces working: they re-label by id from
441
+ // their own dictionary, which is the older contract and still the more specific one.
442
+ const say = () => {
443
+ const w = chromeCopy(document.documentElement.lang);
444
+ for (const el of document.querySelectorAll<HTMLElement>("[data-chrome]")) {
445
+ const word = w[el.dataset.chrome as "cancel" | "done"];
446
+ if (word) el.textContent = word;
447
+ }
448
+ };
449
+ say();
450
+ // The child personas set `<html lang>` from their own storage AFTER this module runs — a sheet
451
+ // translated only at load would be correct on a host and English inside a guest. Cheaper than a
452
+ // MutationObserver and exact: the attribute cannot change without the element being replaced.
453
+ new MutationObserver(say).observe(document.documentElement, { attributeFilter: ["lang"] });
398
454
  const DRAG_CLOSE = 0.28; // fraction of the panel's height that dismisses on release
399
455
  const FLICK = 0.55; // px/ms — a fast flick dismisses regardless of distance
400
456
  const wired = new WeakSet<HTMLDialogElement>();