@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 +1 -1
- package/src/ActionList.astro +8 -0
- package/src/FilePicker.astro +9 -3
- package/src/Sheet.astro +56 -1
package/package.json
CHANGED
package/src/ActionList.astro
CHANGED
|
@@ -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 {
|
package/src/FilePicker.astro
CHANGED
|
@@ -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.
|
|
89
|
-
|
|
90
|
-
|
|
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;
|