@justai/cuts 0.42.0 → 0.44.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 +3 -2
- package/src/FilePicker.astro +18 -3
- package/src/Sheet.astro +49 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justai/cuts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./ground.css": "./src/ground.css",
|
|
25
25
|
"./FilePicker.astro": "./src/FilePicker.astro",
|
|
26
26
|
"./ActionList.astro": "./src/ActionList.astro",
|
|
27
|
-
"./ActionRow.astro": "./src/ActionRow.astro"
|
|
27
|
+
"./ActionRow.astro": "./src/ActionRow.astro",
|
|
28
|
+
"./Files.astro": "./src/FilePicker.astro"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
31
|
"astro": ">=5"
|
package/src/FilePicker.astro
CHANGED
|
@@ -49,8 +49,19 @@ import Sheet from "./Sheet.astro";
|
|
|
49
49
|
*
|
|
50
50
|
* One element rather than two cuts, because it is one room: two frames would be two documents,
|
|
51
51
|
* two atrium routers and two copies of the same kernel open at once, to ask the same file system
|
|
52
|
-
* a question in each direction.
|
|
53
|
-
*
|
|
52
|
+
* a question in each direction.
|
|
53
|
+
*
|
|
54
|
+
* ── THE NAME (settled 2026-07-27) ────────────────────────────────────────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* It was called `FilePicker` when picking was all it did, and by the time it could pick, save and
|
|
57
|
+
* read, the name described a third of it. A name that has to be explained is a name that is wrong:
|
|
58
|
+
* a builder reading "picker" in the catalogue has no reason to look here for saving.
|
|
59
|
+
*
|
|
60
|
+
* So it is exported as **`Files.astro`** — the persona's door to its files, which is what it is.
|
|
61
|
+
* `FilePicker.astro` still resolves to the same component and always will: eleven personas import
|
|
62
|
+
* it under that name, and breaking them to correct a word would be charging the fleet for the
|
|
63
|
+
* ecosystem's own late naming. An alias costs one line in `exports`; a rename costs eleven repos,
|
|
64
|
+
* eleven deploys, and one of them forgotten. New consumers import `Files`.
|
|
54
65
|
*
|
|
55
66
|
* A method on the element rather than a global, so two pickers on one page are two pickers and
|
|
56
67
|
* nothing has to be coordinated through a name only one of them can own.
|
|
@@ -72,7 +83,11 @@ interface Props {
|
|
|
72
83
|
const { id, title = "Choose a file", src = "/files" } = Astro.props;
|
|
73
84
|
---
|
|
74
85
|
|
|
75
|
-
|
|
86
|
+
{/* NO `Done` here, deliberately. This sheet is a QUESTION, and its answer is a row being
|
|
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. The kernel
|
|
89
|
+
inside shows its own Cancel, worded as the choice it actually is. */}
|
|
90
|
+
<Sheet id={`${id}-sheet`} title={title} detent="large" done={false}>
|
|
76
91
|
<div class="jfp" id={id} data-picker={id} data-src={src}>
|
|
77
92
|
{/* Empty until the first open. `title` is the accessible name of the document inside — a frame
|
|
78
93
|
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;
|