@justai/cuts 0.19.0 → 0.20.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 +1 -1
- package/src/Base.astro +19 -14
- package/src/ConfirmSheet.astro +36 -9
package/package.json
CHANGED
package/src/Base.astro
CHANGED
|
@@ -248,7 +248,7 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
|
|
|
248
248
|
before the work), and it never asks twice (a slip-away counts as the quiet no; the door in
|
|
249
249
|
the footer is the change-of-mind path). */}
|
|
250
250
|
{gaId && (
|
|
251
|
-
<ConfirmSheet id="consent" body={t.consentText}
|
|
251
|
+
<ConfirmSheet id="consent" ask body={t.consentText}
|
|
252
252
|
actionLabel={t.consentAllow} cancelLabel={t.consentDecline} cancelValue="denied"
|
|
253
253
|
data-choice-label={t.consentChoice} />
|
|
254
254
|
<script is:inline>
|
|
@@ -258,21 +258,24 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
|
|
|
258
258
|
var h = location.hostname;
|
|
259
259
|
if (h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]" || h.indexOf("dev-") === 0) return;
|
|
260
260
|
var el = document.getElementById("consent"); if (!el || !el.showModal) return;
|
|
261
|
-
// Whether the CURRENT open is the first-visit auto-ask (vs a change-your-mind reopen) —
|
|
262
|
-
// it decides what a slip-away means in the close handler below.
|
|
263
|
-
var autoAsk = false;
|
|
264
261
|
// returnValue survives a close, so reset before every open or a stale answer replays.
|
|
265
262
|
var open = function () { if (!el.open) { el.returnValue = ""; el.showModal(); } };
|
|
266
263
|
el.addEventListener("close", function () {
|
|
267
|
-
var rv = el.returnValue
|
|
264
|
+
var rv = el.returnValue;
|
|
268
265
|
// "confirm" = the ❤️. "denied" = a CHOSEN "Stay invisible" (the sheet's cancelValue).
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
// Anything else (Escape, a backdrop tap) is NOT an answer, and ONLY A CHOICE MAY BE
|
|
267
|
+
// REMEMBERED AS A CHOICE (founder's law, 2026-07-24 — an earlier draft recorded the
|
|
268
|
+
// slip-away as a "quiet no", i.e. an answer the visitor never gave). A slip-away
|
|
269
|
+
// stores nothing: analytics stays at the regional default and the ask RETURNS on the
|
|
270
|
+
// next visit; the session flag below just keeps THIS visit quiet — one dodge buys one
|
|
271
|
+
// visit of peace, not an answer.
|
|
272
|
+
var v = rv === "confirm" ? "granted" : rv === "denied" ? "denied" : null;
|
|
273
|
+
if (v) {
|
|
274
|
+
gtag("consent", "update", { analytics_storage: v });
|
|
275
|
+
try { localStorage.setItem("consent", v); } catch (_) {}
|
|
276
|
+
} else {
|
|
277
|
+
try { sessionStorage.setItem("consent-later", "1"); } catch (_) {}
|
|
278
|
+
}
|
|
276
279
|
});
|
|
277
280
|
// THE CHANGE-YOUR-MIND DOOR — and it is universal, not a consent-region courtesy. Trust is
|
|
278
281
|
// the choice staying reachable: the Posts cut's footer carries a hidden anchor
|
|
@@ -288,12 +291,14 @@ const localePath = (l: string) => (l === defaultLocale ? "/" : `/${l}/`);
|
|
|
288
291
|
var w = a.closest("[data-consent-door]"); if (w) w.hidden = false;
|
|
289
292
|
a.addEventListener("click", function (ev) { ev.preventDefault(); open(); });
|
|
290
293
|
}
|
|
291
|
-
// auto-ask:
|
|
294
|
+
// auto-ask: no stored CHOICE, not already dodged THIS visit, only where consent is
|
|
295
|
+
// legally asked, only on a content page
|
|
292
296
|
if (localStorage.getItem("consent")) return;
|
|
297
|
+
try { if (sessionStorage.getItem("consent-later")) return; } catch (_) {}
|
|
293
298
|
if (!document.getElementById("kernel")) return; // no kernel = not a content page (e.g. 404) — don't ask for consent on a dead end
|
|
294
299
|
if ((Intl.DateTimeFormat().resolvedOptions().timeZone || "").indexOf("Europe/") !== 0) return;
|
|
295
300
|
// the first second belongs to the kernel; the question arrives on the second beat
|
|
296
|
-
setTimeout(
|
|
301
|
+
setTimeout(open, 700);
|
|
297
302
|
} catch (_) {}
|
|
298
303
|
})();
|
|
299
304
|
</script>
|
package/src/ConfirmSheet.astro
CHANGED
|
@@ -6,10 +6,22 @@
|
|
|
6
6
|
// actions with one dialect while another improvises a different one is how a fleet stops feeling
|
|
7
7
|
// like one hand made it. Anything that needs "are you sure?" uses THIS.
|
|
8
8
|
//
|
|
9
|
-
// The anatomy is Apple's, faithfully
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// The anatomy is Apple's, faithfully — and Apple has TWO genres, so this sheet has two:
|
|
10
|
+
//
|
|
11
|
+
// CONFIRM (default) — the destructive action sheet: message + the action in the grouped card,
|
|
12
|
+
// and a PHYSICALLY DETACHED Cancel in its own card, bolder than the action, because when
|
|
13
|
+
// something can end, the safe choice wears the most confident face. iOS system red for
|
|
14
|
+
// destruction. This is /me's "Erase everything?".
|
|
15
|
+
//
|
|
16
|
+
// ASK (`ask` prop) — the alert/ATT genre, for INVITATIONS: both choices live INSIDE the grouped
|
|
17
|
+
// card, stacked; the action wears the preferred bold face (Apple's preferredAction) and takes
|
|
18
|
+
// the focus; the safe choice sits beneath it in calm regular weight — equal size, equal tap,
|
|
19
|
+
// one hairline apart, but the invitation is the emphasized path. Porting the confirm genre to a
|
|
20
|
+
// friendly ask was tried and rejected by the founder on sight (2026-07-24): a detached, boldest,
|
|
21
|
+
// focus-ringed "Stay invisible" ENCOURAGED declining — the destructive anatomy exists to protect,
|
|
22
|
+
// and there is nothing to protect from in a warm question.
|
|
23
|
+
//
|
|
24
|
+
// The sheet springs up from the bottom (reduced-motion honored).
|
|
13
25
|
//
|
|
14
26
|
// WHY native <dialog> + showModal(), and not a hand-rolled scrim: the /me original had the visible
|
|
15
27
|
// half of Apple right and the INVISIBLE half missing — no focus trap, no Escape, no focus restore.
|
|
@@ -36,15 +48,20 @@ interface Props {
|
|
|
36
48
|
cancelLabel: string;
|
|
37
49
|
/** Paint the action iOS system red — for actions that delete or end something. */
|
|
38
50
|
destructive?: boolean;
|
|
51
|
+
/** The ASK genre: an invitation, not a guard. Both choices join the grouped card, the action is
|
|
52
|
+
* the preferred (bold, focused) path, the safe choice sits quietly beneath. Use for questions
|
|
53
|
+
* you'd be glad to hear "yes" to (consent); never for actions that end something. */
|
|
54
|
+
ask?: boolean;
|
|
39
55
|
/** What returnValue reports when the safe button is chosen. Default "" — indistinguishable from
|
|
40
56
|
* Escape/backdrop, which is fine when both mean "do nothing". Pass a distinct value when a
|
|
41
|
-
* CHOSEN no must be tellable apart from a slip-away
|
|
42
|
-
*
|
|
57
|
+
* CHOSEN no must be tellable apart from a slip-away — consent does: only a choice may be
|
|
58
|
+
* remembered as a choice, so the chosen no is stored while a slip-away stores nothing and the
|
|
59
|
+
* ask returns next visit. */
|
|
43
60
|
cancelValue?: string;
|
|
44
61
|
/** Anything else (data-* attributes, aria overrides) lands on the <dialog> itself. */
|
|
45
62
|
[key: string]: unknown;
|
|
46
63
|
}
|
|
47
|
-
const { id, title, body, actionLabel, cancelLabel, destructive = false, cancelValue = "", ...rest } = Astro.props;
|
|
64
|
+
const { id, title, body, actionLabel, cancelLabel, destructive = false, ask = false, cancelValue = "", ...rest } = Astro.props;
|
|
48
65
|
---
|
|
49
66
|
|
|
50
67
|
<dialog class="confirm" id={id} aria-labelledby={title ? `${id}-t` : `${id}-b`} aria-describedby={`${id}-b`} {...rest}>
|
|
@@ -54,9 +71,10 @@ const { id, title, body, actionLabel, cancelLabel, destructive = false, cancelVa
|
|
|
54
71
|
{title && <strong id={`${id}-t`}>{title}</strong>}
|
|
55
72
|
<span id={`${id}-b`}>{body}</span>
|
|
56
73
|
</div>
|
|
57
|
-
<button class:list={["confirm-act", { destructive }]} id={`${id}-act`} value="confirm">{actionLabel}</button>
|
|
74
|
+
<button class:list={["confirm-act", { destructive, preferred: ask }]} id={`${id}-act`} value="confirm" autofocus={ask || undefined}>{actionLabel}</button>
|
|
75
|
+
{ask && <button class="confirm-act quiet" id={`${id}-cancel`} value={cancelValue} formnovalidate>{cancelLabel}</button>}
|
|
58
76
|
</div>
|
|
59
|
-
<button class="confirm-act confirm-cancel" id={`${id}-cancel`} value={cancelValue} formnovalidate autofocus>{cancelLabel}</button>
|
|
77
|
+
{!ask && <button class="confirm-act confirm-cancel" id={`${id}-cancel`} value={cancelValue} formnovalidate autofocus>{cancelLabel}</button>}
|
|
60
78
|
</form>
|
|
61
79
|
</dialog>
|
|
62
80
|
|
|
@@ -101,8 +119,17 @@ const { id, title, body, actionLabel, cancelLabel, destructive = false, cancelVa
|
|
|
101
119
|
transition: background 0.15s;
|
|
102
120
|
}
|
|
103
121
|
.confirm-act:active { background: color-mix(in srgb, var(--text) 9%, transparent); }
|
|
122
|
+
/* focus is a wash, not a ring: the fleet's 4px focus ring gets CLIPPED by the card's
|
|
123
|
+
overflow:hidden into two stray bars — inside a sheet, the focused button fills with a
|
|
124
|
+
whisper of its own colour instead (currentColor: accent, red, or text alike) */
|
|
125
|
+
.confirm-act:focus-visible { box-shadow: none; outline: none; background: color-mix(in srgb, currentColor 12%, transparent); }
|
|
104
126
|
/* iOS system red — the one colour that means "this ends something" on every persona alike */
|
|
105
127
|
.confirm-act.destructive { color: #ff453a; }
|
|
128
|
+
/* the ASK genre: the invitation is the preferred path (Apple's bold preferredAction)… */
|
|
129
|
+
.confirm-act.preferred { font-weight: 700; }
|
|
130
|
+
/* …and the safe choice answers in a calm, full-contrast regular voice — equal size, equal tap,
|
|
131
|
+
never dimmed (a decline you have to hunt for is not a free choice), just not the emphasis */
|
|
132
|
+
.confirm-act.quiet { color: var(--text); font-weight: 400; }
|
|
106
133
|
/* the safe choice wears the boldest face, in its own detached card */
|
|
107
134
|
.confirm-cancel {
|
|
108
135
|
border: 1px solid var(--border); border-radius: 18px;
|