@justai/cuts 0.143.0 → 0.145.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.143.0",
3
+ "version": "0.145.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": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@justai/core": "^0.37.0",
44
+ "@justai/core": "^0.38.0",
45
45
  "@justai/ui": "^0.26.0",
46
46
  "@justai/atrium": "^0.1.9"
47
47
  },
@@ -16,6 +16,13 @@ import "./ground.css";
16
16
  // something can end, the safe choice wears the most confident face. iOS system red for
17
17
  // destruction. This is /me's "Erase everything?".
18
18
  //
19
+ // A THIRD CHOICE, when the safe answer is not "do nothing" (`safeActionLabel`) — Apple's own
20
+ // multi-action sheet. Some destructive questions have a way OUT of the loss rather than only a
21
+ // way around it: "your work isn't saved yet" is answered best by saving it, not by cancelling and
22
+ // then hunting for the Save button. That action joins the grouped card ABOVE the destructive one,
23
+ // because a sheet reads top to bottom and the preserving path is the one to meet first. Cancel
24
+ // stays detached and boldest — the anatomy does not change, it gains one row.
25
+ //
19
26
  // ASK (`ask` prop) — the alert/ATT genre, for INVITATIONS: both choices live INSIDE the grouped
20
27
  // card, stacked; the action wears the preferred bold face (Apple's preferredAction) and takes
21
28
  // the focus; the safe choice sits beneath it in calm regular weight — equal size, equal tap,
@@ -61,10 +68,19 @@ interface Props {
61
68
  * remembered as a choice, so the chosen no is stored while a slip-away stores nothing and the
62
69
  * ask returns next visit. */
63
70
  cancelValue?: string;
71
+ /** Optional THIRD choice: an action that PRESERVES rather than ends, placed above the main action
72
+ * inside the grouped card and painted as the preferred path. Omit it and the sheet is exactly
73
+ * what it always was — same DOM, same ids, same returnValues. */
74
+ safeActionLabel?: string;
75
+ /** What returnValue that third choice reports. Default "safe": three answers need three words. */
76
+ safeActionValue?: string;
64
77
  /** Anything else (data-* attributes, aria overrides) lands on the <dialog> itself. */
65
78
  [key: string]: unknown;
66
79
  }
67
- const { id, title, body, actionLabel, cancelLabel, destructive = false, ask = false, cancelValue = "", ...rest } = Astro.props;
80
+ const {
81
+ id, title, body, actionLabel, cancelLabel, destructive = false, ask = false, cancelValue = "",
82
+ safeActionLabel, safeActionValue = "safe", ...rest
83
+ } = Astro.props;
68
84
  ---
69
85
 
70
86
  <dialog class="confirm" id={id} aria-labelledby={title ? `${id}-t` : `${id}-b`} aria-describedby={`${id}-b`} {...rest}>
@@ -79,6 +95,9 @@ const { id, title, body, actionLabel, cancelLabel, destructive = false, ask = fa
79
95
  on a closed dialog, noise about a focus that was never going to happen. Focus is the
80
96
  OPENER's job now, at showModal time: ask → the invitation (CONSENT_JS does it),
81
97
  destructive → the cancel (the consumer does it). The genre decides, as before. */}
98
+ {safeActionLabel && (
99
+ <button class="confirm-act preferred" id={`${id}-safe`} value={safeActionValue}>{safeActionLabel}</button>
100
+ )}
82
101
  <button class:list={["confirm-act", { destructive, preferred: ask }]} id={`${id}-act`} value="confirm">{actionLabel}</button>
83
102
  {ask && <button class="confirm-act quiet" id={`${id}-cancel`} value={cancelValue} formnovalidate>{cancelLabel}</button>}
84
103
  </div>
package/src/Toolbar.astro CHANGED
@@ -88,6 +88,17 @@ interface Props {
88
88
  // that nothing keeps in step. The strip is one line and its vocabulary should be one kind of
89
89
  // thing — names or sections, not one of each — so the API says so out loud.
90
90
  kernelTitle?: string;
91
+ /**
92
+ * Which cut a visit with no hash opens on. Defaults to the kernel, which is right for every
93
+ * persona whose kernel is the thing it does.
94
+ *
95
+ * app.zone is the exception this exists for: its kernel is a catalogue of the OTHER personas, so
96
+ * landing there opens the house on a list of its rooms and scrolls the mark, the wordmark and the
97
+ * sentence that say what app.zone IS past before anyone reads them. Passed to `personaScroll`
98
+ * through a data attribute, the same way the two title strings already travel — this cut renders
99
+ * markup, and the behaviour reads the markup rather than being handed a closure.
100
+ */
101
+ landOn?: string;
91
102
  postsTitle?: string;
92
103
 
93
104
  // queries: toolbar shows a generic person icon · the profile icon is not my persona ·
@@ -160,7 +171,7 @@ interface Props {
160
171
  import BackToZone from "./BackToZone.astro";
161
172
  const {
162
173
  sectionsLabel, profileLabel, kernelLabel, postsLabel,
163
- files = true, filesLabel = "Files", kernelTitle, postsTitle, avatar = "/icon.svg", parent,
174
+ files = true, filesLabel = "Files", kernelTitle, postsTitle, landOn, avatar = "/icon.svg", parent,
164
175
  } = Astro.props;
165
176
  // A child's row answers UP to its parent, not out to the zone, and has no stream of its own — so
166
177
  // both default off the moment `parent` is given, rather than each consumer remembering to say so.
@@ -172,7 +183,7 @@ import AuthMount from "./AuthMount.astro";
172
183
  make this script inline and it has an import. Astro drops an attribute whose value is undefined,
173
184
  so a persona that passes neither leaves no trace in the markup at all. -->
174
185
  <div class="tb-slot">
175
- <header class="toolbar" data-kernel-title={kernelTitle} data-posts-title={postsTitle}>
186
+ <header class="toolbar" data-kernel-title={kernelTitle} data-posts-title={postsTitle} data-land-on={landOn}>
176
187
  <div class="tb-row">
177
188
  <div class="tb-left">
178
189
  {home && <BackToZone apex={apex} />}
@@ -82,8 +82,13 @@ document.querySelector(".toolbar")?.addEventListener(
82
82
  const bar = document.querySelector(".toolbar") as HTMLElement | null;
83
83
  const kt = bar?.dataset.kernelTitle;
84
84
  const pt = bar?.dataset.postsTitle;
85
- personaScroll(
86
- kt && pt
85
+ // `landOn` travels as a data attribute for the same reason the two titles do: this file runs in a
86
+ // page it did not render, so the markup is the interface. Undefined when the attribute is absent,
87
+ // which is the option's default — the kernel.
88
+ const landOn = bar?.dataset.landOn;
89
+ personaScroll({
90
+ ...(kt && pt
87
91
  ? { titleFor: (id: string) => (id === "kernel" ? kt : id === "posts" ? pt : "") }
88
- : undefined,
89
- );
92
+ : {}),
93
+ ...(landOn ? { landOn } : {}),
94
+ });