@justai/cuts 0.138.0 → 0.140.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 -3
- package/src/ActionList.astro +27 -31
- package/src/ActionRow.astro +64 -68
- package/src/BackToZone.astro +35 -39
- package/src/Catalogue.astro +88 -88
- package/src/CatalogueCard.astro +68 -72
- package/src/ChildPersona.astro +51 -55
- package/src/ConfirmSheet.astro +65 -69
- package/src/FilePicker.astro +13 -17
- package/src/LangPicker.astro +41 -45
- package/src/Posts.astro +169 -173
- package/src/Preroll.astro +193 -197
- package/src/Profile.astro +35 -39
- package/src/Segmented.astro +54 -58
- package/src/Sheet.astro +212 -237
- package/src/Toolbar.astro +280 -284
- package/src/behaviour/catalogue.ts +16 -8
- package/src/ground.css +120 -124
package/src/Segmented.astro
CHANGED
|
@@ -105,67 +105,63 @@ const on = value && options.some((o) => o.value === value) ? value : options[0]?
|
|
|
105
105
|
</div>
|
|
106
106
|
|
|
107
107
|
<style>
|
|
108
|
-
/*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
should have owned rather than each page discovering separately. */
|
|
140
|
-
.jseg[data-width="full"] { display: flex; width: 100%; }
|
|
141
|
-
.jseg[data-width="full"] .jseg-item { flex: 1; min-height: 44px; }
|
|
108
|
+
/* queries: segmented control styles · the chosen segment · ios segmented look
|
|
109
|
+
ONE TRACK, THE CHOSEN ONE RAISED. The track is recessed and the selection sits on top of it —
|
|
110
|
+
which is what makes a segmented control read as "these are the options" rather than as a row of
|
|
111
|
+
buttons where one happens to be coloured. */
|
|
112
|
+
.jseg {
|
|
113
|
+
/* ITS OWN BOX MODEL, not the consumer's. `width: 100%` plus a padding and a border comes out
|
|
114
|
+
SIX pixels wider than its parent unless something has already said `border-box` — and on a
|
|
115
|
+
bare page nothing has. Inheriting a reset is homework (law 1), and this is the shape that
|
|
116
|
+
homework takes when nobody notices: a control that fits everywhere it was tried. */
|
|
117
|
+
box-sizing: border-box;
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
align-items: stretch;
|
|
120
|
+
gap: 2px;
|
|
121
|
+
padding: 2px;
|
|
122
|
+
border-radius: var(--radius-md);
|
|
123
|
+
background: var(--surface-2);
|
|
124
|
+
border: 1px solid var(--border);
|
|
125
|
+
/* A control is one tab stop. The roving tabindex below is what makes that true for the keyboard;
|
|
126
|
+
this is what makes it true for the eye. */
|
|
127
|
+
isolation: isolate;
|
|
128
|
+
}
|
|
129
|
+
/* queries: segmented control height · 44px touch target · the segments are too small
|
|
130
|
+
TWO SIZES, AND THE DIFFERENCE IS WHAT THE CONTROL IS.
|
|
131
|
+
`full` is a VIEW'S OWN SWITCH — a thumb's first target on the screen, so it meets the fleet's
|
|
132
|
+
44px like every row and every action does. `inline` is a TOOLBAR CHIP, sitting among other
|
|
133
|
+
chrome at chrome's size; forcing 44 there would make a toolbar taller than its content, which
|
|
134
|
+
is why iOS ships its own segmented controls shorter in exactly that position.
|
|
135
|
+
Found by a consumer's spec measuring 33px where its page promised 44 — a number the component
|
|
136
|
+
should have owned rather than each page discovering separately. */
|
|
137
|
+
.jseg[data-width="full"] { display: flex; width: 100%; }
|
|
138
|
+
.jseg[data-width="full"] .jseg-item { flex: 1; min-height: 44px; }
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
.jseg-item {
|
|
141
|
+
box-sizing: border-box;
|
|
142
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
143
|
+
min-width: 0; padding: 7px 12px;
|
|
144
|
+
border: 0; border-radius: calc(var(--radius-md) - 3px);
|
|
145
|
+
background: transparent; color: var(--text-dim);
|
|
146
|
+
font: inherit; font-size: var(--fs-sm); font-weight: 500; line-height: 1.2;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
transition: background 0.18s var(--ease-out), color 0.18s var(--ease-out);
|
|
149
|
+
}
|
|
150
|
+
.jseg-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
151
|
+
.jseg-item :global(svg) { width: 16px; height: 16px; flex: none; }
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
/* THE CHOSEN ONE. `--surface` against a `--surface-2` track is the raise; the accent names it. */
|
|
154
|
+
.jseg-item[aria-checked="true"] {
|
|
155
|
+
background: var(--surface);
|
|
156
|
+
color: var(--accent);
|
|
157
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.06);
|
|
158
|
+
}
|
|
159
|
+
.jseg-item[aria-disabled="true"] { opacity: 0.45; cursor: default; }
|
|
160
|
+
.jseg-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
164
161
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
162
|
+
/* Motion is never forced on somebody who asked it off. */
|
|
163
|
+
@media (prefers-reduced-motion: reduce) {
|
|
164
|
+
.jseg-item { transition: none; }
|
|
169
165
|
}
|
|
170
166
|
</style>
|
|
171
167
|
|
package/src/Sheet.astro
CHANGED
|
@@ -187,260 +187,235 @@ const doneIsOurs = doneDefault || done === true;
|
|
|
187
187
|
</dialog>
|
|
188
188
|
|
|
189
189
|
<style>
|
|
190
|
-
/*
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
watches the page slide under a sheet they thought they were using. The founder named the
|
|
198
|
-
feeling exactly: "aktif sheet sanki aktif degilmis hissi veriyor". Measured before fixing:
|
|
199
|
-
scrollY went 2244 → 1644 with a sheet open.
|
|
190
|
+
/* queries: the page scrolls behind an open sheet · background scroll lock · the sheet feels
|
|
191
|
+
inactive · scroll behind the dialog
|
|
192
|
+
THE PAGE BEHIND MUST NOT MOVE. `showModal()` makes the rest of the document INERT — it cannot
|
|
193
|
+
be clicked or focused — but it does not stop it SCROLLING, and a person swiping over the dim
|
|
194
|
+
watches the page slide under a sheet they thought they were using. The founder named the
|
|
195
|
+
feeling exactly: "aktif sheet sanki aktif degilmis hissi veriyor". Measured before fixing:
|
|
196
|
+
scrollY went 2244 → 1644 with a sheet open.
|
|
200
197
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
Set by script on <html> and reference-counted, because sheets nest: unlocking on the first
|
|
199
|
+
close would free the page while a second sheet is still up. */
|
|
200
|
+
:global(html[data-sheet-locked]) { overflow: hidden; }
|
|
204
201
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
/* queries: sheet page transition · in-sheet push animation · the pages slide
|
|
224
|
-
|
|
225
|
-
Only one page is in the layout at a time, so the body's height is the CURRENT page's — which is
|
|
226
|
-
what makes the sheet grow and shrink around the content the way a navigation stack does. The
|
|
227
|
-
slide is 220ms and is skipped for reduced motion, where the pages simply swap. */
|
|
228
|
-
/* queries: the second sheet page appears BELOW the first · pushing shows both pages · the history
|
|
229
|
-
list is squeezed at the bottom · a page does not hide · consumer css beats the sheet
|
|
230
|
-
HIDING A PAGE IS A MECHANISM, NOT A SUGGESTION, so it is written where a class cannot outrank
|
|
231
|
-
it. `[data-sheet-page] { display: none }` is one attribute — specificity (0,1,0) — and any
|
|
232
|
-
consumer rule naming two classes beats it. files styles its detail page as `.pp .fd { display:
|
|
233
|
-
flex }`, which is ordinary and correct, and it meant the root page NEVER HID: pushing to the
|
|
234
|
-
versions drew them one under the other, so a person who tapped History got a cramped strip at
|
|
235
|
-
the bottom of a sheet still showing everything they had just left.
|
|
236
|
-
Qualified by the dialog it lives in, which raises this to (0,2,1) and keeps the rule honest —
|
|
237
|
-
a page outside a sheet is nobody's business here. */
|
|
238
|
-
@keyframes sheet-push { from { transform: translateX(18%); opacity: 0; } }
|
|
239
|
-
@keyframes sheet-pop { from { transform: translateX(-14%); opacity: 0; } }
|
|
240
|
-
@media (prefers-reduced-motion: reduce) {
|
|
241
|
-
:global([data-sheet-page].is-current), :global([data-sheet-page].is-popping) { animation: none; }
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* queries: cancel colour · should Cancel be tinted · cancel looks disabled · grey cancel ·
|
|
245
|
-
accent on the sheet bar · cancel vs done weight
|
|
246
|
-
MIRRORS DONE ACROSS THE BAR — same band, opposite edge, SAME TINT, LIGHTER WEIGHT.
|
|
247
|
-
|
|
248
|
-
This was grey (`--text-dim`), argued as "leaving without answering is an ordinary thing to do,
|
|
249
|
-
not the reason you came". True about the hierarchy, wrong about the means: on iOS a bar button
|
|
250
|
-
is TINTED, and grey text on a bar is what the platform uses to say UNAVAILABLE. Mail, Calendar
|
|
251
|
-
and Photos all put Cancel in the tint colour beside a bolder Done — the hierarchy is carried by
|
|
252
|
-
WEIGHT, 500 against 600, which is the one channel that does not collide with an existing
|
|
253
|
-
meaning. A dimmed way out reads as a door that will not open. */
|
|
254
|
-
.sheet-cancel {
|
|
255
|
-
position: absolute; inset-block-start: 0; inset-inline-start: 0;
|
|
256
|
-
background: none; border: 0; cursor: pointer; padding: 12px 16px;
|
|
257
|
-
font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--accent);
|
|
258
|
-
}
|
|
259
|
-
.sheet-cancel:active { opacity: 0.6; }
|
|
260
|
-
.sheet-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
|
|
261
|
-
/* Cancel, Done and Back must never stand together — going back IS the way out of a pushed page,
|
|
262
|
-
so the leading slot belongs to whichever of the two is meaningful right now. */
|
|
263
|
-
.sheet:has(.sheet-back:not([hidden])) .sheet-cancel { display: none; }
|
|
264
|
-
|
|
265
|
-
.sheet-done {
|
|
266
|
-
position: absolute; inset-block-start: 0; inset-inline-end: 0;
|
|
267
|
-
background: none; border: 0; cursor: pointer; padding: 12px 16px;
|
|
268
|
-
font: inherit; font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
|
|
269
|
-
}
|
|
270
|
-
.sheet-done:active { opacity: 0.6; }
|
|
271
|
-
.sheet-done:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
|
|
202
|
+
/* Trailing and level with the title, where a navigation bar's right-hand action lives — the one
|
|
203
|
+
place a thumb looks for "I am finished here". */
|
|
204
|
+
/* The back control mirrors Done across the header: same band, same weight, same restraint. It is
|
|
205
|
+
the accent colour because it is a NAVIGATION affordance rather than a destructive one, and the
|
|
206
|
+
chevron sits tight against the label the way iOS sets a back item. */
|
|
207
|
+
.sheet-back {
|
|
208
|
+
position: absolute; inset-inline-start: 12px; inset-block-start: 10px;
|
|
209
|
+
display: inline-flex; align-items: center; gap: 2px;
|
|
210
|
+
border: 0; background: none; padding: 6px 8px 6px 4px; cursor: pointer;
|
|
211
|
+
color: var(--accent); font: inherit; font-size: var(--fs-sm); font-weight: 500;
|
|
212
|
+
max-width: 42%;
|
|
213
|
+
}
|
|
214
|
+
.sheet-back[hidden] { display: none; }
|
|
215
|
+
.sheet-back svg { width: 20px; height: 20px; flex: none; }
|
|
216
|
+
.sheet-back span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
217
|
+
.sheet-back:active { opacity: 0.6; }
|
|
218
|
+
.sheet-back:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
|
|
272
219
|
|
|
273
|
-
|
|
274
|
-
/* The dialog is the full-screen STAGE; the PANEL is the sheet.
|
|
275
|
-
`position: fixed` is written out rather than inherited: the UA gives a dialog
|
|
276
|
-
`position: absolute`, and an absolutely-positioned top-layer element resolves against the
|
|
277
|
-
initial containing block — which is document-relative, so on a scrolled page the sheet would
|
|
278
|
-
be placed at coordinates the visitor is no longer looking at. */
|
|
279
|
-
position: fixed;
|
|
280
|
-
inset: 0;
|
|
281
|
-
width: 100%;
|
|
282
|
-
height: 100%;
|
|
283
|
-
max-width: none;
|
|
284
|
-
max-height: none;
|
|
285
|
-
margin: 0;
|
|
286
|
-
padding: 0;
|
|
287
|
-
border: 0;
|
|
288
|
-
background: none;
|
|
289
|
-
overflow: visible;
|
|
290
|
-
/* The stage seats the panel on the BOTTOM edge, centred. It has to be written as a
|
|
291
|
-
display:none/display:flex pair, because a `display` of our own on a <dialog> would defeat the
|
|
292
|
-
UA rule that hides it while closed — a sheet permanently visible on every page. */
|
|
293
|
-
display: none;
|
|
294
|
-
}
|
|
295
|
-
.sheet[open] {
|
|
296
|
-
display: flex;
|
|
297
|
-
align-items: flex-end;
|
|
298
|
-
justify-content: center;
|
|
299
|
-
}
|
|
220
|
+
/* queries: sheet page transition · in-sheet push animation · the pages slide
|
|
300
221
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
222
|
+
Only one page is in the layout at a time, so the body's height is the CURRENT page's — which is
|
|
223
|
+
what makes the sheet grow and shrink around the content the way a navigation stack does. The
|
|
224
|
+
slide is 220ms and is skipped for reduced motion, where the pages simply swap. */
|
|
225
|
+
/* queries: the second sheet page appears BELOW the first · pushing shows both pages · the history
|
|
226
|
+
list is squeezed at the bottom · a page does not hide · consumer css beats the sheet
|
|
227
|
+
HIDING A PAGE IS A MECHANISM, NOT A SUGGESTION, so it is written where a class cannot outrank
|
|
228
|
+
it. `[data-sheet-page] { display: none }` is one attribute — specificity (0,1,0) — and any
|
|
229
|
+
consumer rule naming two classes beats it. files styles its detail page as `.pp .fd { display:
|
|
230
|
+
flex }`, which is ordinary and correct, and it meant the root page NEVER HID: pushing to the
|
|
231
|
+
versions drew them one under the other, so a person who tapped History got a cramped strip at
|
|
232
|
+
the bottom of a sheet still showing everything they had just left.
|
|
233
|
+
Qualified by the dialog it lives in, which raises this to (0,2,1) and keeps the rule honest —
|
|
234
|
+
a page outside a sheet is nobody's business here. */
|
|
235
|
+
:global(dialog [data-sheet-page]:not(.is-current):not(.is-popping)) { display: none; }
|
|
236
|
+
:global([data-sheet-page]) { display: none; }
|
|
237
|
+
:global([data-sheet-page].is-current) { display: block; animation: sheet-push 0.22s var(--ease-out) both; }
|
|
238
|
+
:global([data-sheet-page].is-popping) { display: block; animation: sheet-pop 0.22s var(--ease-out) both; }
|
|
239
|
+
@keyframes sheet-push { from { transform: translateX(18%); opacity: 0; } }
|
|
240
|
+
@keyframes sheet-pop { from { transform: translateX(-14%); opacity: 0; } }
|
|
241
|
+
@media (prefers-reduced-motion: reduce) {
|
|
242
|
+
:global([data-sheet-page].is-current), :global([data-sheet-page].is-popping) { animation: none; }
|
|
243
|
+
}
|
|
315
244
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
flex-direction: column;
|
|
320
|
-
/* THE ONE SIZING RULE, and there is no media query anywhere in this file. The persona's column
|
|
321
|
-
is the active view, and its width is READ rather than repeated: `--column` is declared once in
|
|
322
|
-
@justai/ui and the page is sized from the same token. On a phone this is the whole screen; on a
|
|
323
|
-
desk it is the persona's own width, centred. It said 480 until 2026-08-04, in its own copy of
|
|
324
|
-
the number — so when the page became 768 the sheet stayed a phone's width on a tablet. */
|
|
325
|
-
/* THE FALLBACK IS NOT DECORATION. `--column` arrives with @justai/ui, and a page that uses this
|
|
326
|
-
cut without it — the harness's own sheet fixture is one — never sees the token at all. So the
|
|
327
|
-
literal here is the fleet's real width, not a historical one: it was 768 for an afternoon and
|
|
328
|
-
the sheet quietly rendered at 768 on a 640 fleet, which is how that was found. */
|
|
329
|
-
width: min(100%, var(--sheet-width, var(--column, 640px)));
|
|
330
|
-
/* dvh is Chromium 108+; the fallback above it survives because the build has targets. */
|
|
331
|
-
max-height: min(92vh, 100vh - 24px);
|
|
332
|
-
max-height: min(92dvh, 100dvh - 24px);
|
|
333
|
-
/* The material the fleet already agreed reads as legible glass (ConfirmSheet, 93%). */
|
|
334
|
-
background: color-mix(in srgb, var(--bg) 93%, transparent);
|
|
335
|
-
-webkit-backdrop-filter: blur(30px) saturate(1.5);
|
|
336
|
-
backdrop-filter: blur(30px) saturate(1.5);
|
|
337
|
-
border-top: 1px solid var(--border);
|
|
338
|
-
border-radius: 20px 20px 0 0;
|
|
339
|
-
/* Sliding UP with an overshoot spring would bounce the panel past the screen edge and back —
|
|
340
|
-
iOS decelerates into place instead — an overshoot spring belongs to something that scales,
|
|
341
|
-
not to something that travels to an edge. */
|
|
342
|
-
transform: translateY(0);
|
|
343
|
-
transition: transform 0.34s var(--ease-out);
|
|
344
|
-
will-change: transform;
|
|
345
|
-
}
|
|
346
|
-
@starting-style {
|
|
347
|
-
.sheet[open] .sheet-panel { transform: translateY(100%); }
|
|
348
|
-
}
|
|
349
|
-
/* The panel takes focus programmatically, so it must not wear a focus ring — it is a surface, not
|
|
350
|
-
a control. Focus a visitor MOVES lands on the rows, which do show one. */
|
|
351
|
-
.sheet-panel:focus { outline: none; }
|
|
352
|
-
/* While a finger is on it, the panel follows the finger with no transition — a 0.34s ease between
|
|
353
|
-
every pointermove is what makes a web sheet feel like a web page. */
|
|
354
|
-
.sheet[data-dragging] .sheet-panel { transition: none; }
|
|
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.
|
|
355
248
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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. */
|
|
255
|
+
.sheet-cancel {
|
|
256
|
+
position: absolute; inset-block-start: 0; inset-inline-start: 0;
|
|
257
|
+
background: none; border: 0; cursor: pointer; padding: 12px 16px;
|
|
258
|
+
font: inherit; font-size: var(--fs-sm); font-weight: 500; color: var(--accent);
|
|
259
|
+
}
|
|
260
|
+
.sheet-cancel:active { opacity: 0.6; }
|
|
261
|
+
.sheet-cancel:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
|
|
262
|
+
/* Cancel, Done and Back must never stand together — going back IS the way out of a pushed page,
|
|
263
|
+
so the leading slot belongs to whichever of the two is meaningful right now. */
|
|
264
|
+
.sheet:has(.sheet-back:not([hidden])) .sheet-cancel { display: none; }
|
|
371
265
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
color: var(--text);
|
|
380
|
-
text-align: center;
|
|
381
|
-
}
|
|
266
|
+
.sheet-done {
|
|
267
|
+
position: absolute; inset-block-start: 0; inset-inline-end: 0;
|
|
268
|
+
background: none; border: 0; cursor: pointer; padding: 12px 16px;
|
|
269
|
+
font: inherit; font-size: var(--fs-sm); font-weight: 600; color: var(--accent);
|
|
270
|
+
}
|
|
271
|
+
.sheet-done:active { opacity: 0.6; }
|
|
272
|
+
.sheet-done:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
|
|
382
273
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
274
|
+
.sheet {
|
|
275
|
+
/* The dialog is the full-screen STAGE; the PANEL is the sheet.
|
|
276
|
+
`position: fixed` is written out rather than inherited: the UA gives a dialog
|
|
277
|
+
`position: absolute`, and an absolutely-positioned top-layer element resolves against the
|
|
278
|
+
initial containing block — which is document-relative, so on a scrolled page the sheet would
|
|
279
|
+
be placed at coordinates the visitor is no longer looking at. */
|
|
280
|
+
position: fixed;
|
|
281
|
+
inset: 0;
|
|
282
|
+
width: 100%;
|
|
283
|
+
height: 100%;
|
|
284
|
+
max-width: none;
|
|
285
|
+
max-height: none;
|
|
286
|
+
margin: 0;
|
|
287
|
+
padding: 0;
|
|
288
|
+
border: 0;
|
|
289
|
+
background: none;
|
|
290
|
+
overflow: visible;
|
|
291
|
+
/* The stage seats the panel on the BOTTOM edge, centred. It has to be written as a
|
|
292
|
+
display:none/display:flex pair, because a `display` of our own on a <dialog> would defeat the
|
|
293
|
+
UA rule that hides it while closed — a sheet permanently visible on every page. */
|
|
294
|
+
display: none;
|
|
295
|
+
}
|
|
296
|
+
.sheet[open] {
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: flex-end;
|
|
299
|
+
justify-content: center;
|
|
300
|
+
}
|
|
392
301
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
padding: 6px 12px max(14px, env(safe-area-inset-bottom));
|
|
408
|
-
}
|
|
302
|
+
/* Entry motion is written with @starting-style, not with an [open] toggle. A dialog enters from
|
|
303
|
+
display:none, and a property that had no previous computed value does not transition — the
|
|
304
|
+
"from" state has to be declared, or the sheet simply appears where it belongs with no travel at
|
|
305
|
+
all. That silent no-op is the class of failure the harness exists to catch. */
|
|
306
|
+
.sheet::backdrop {
|
|
307
|
+
background: rgba(0, 0, 0, 0.4);
|
|
308
|
+
-webkit-backdrop-filter: blur(3px);
|
|
309
|
+
backdrop-filter: blur(3px);
|
|
310
|
+
opacity: 1;
|
|
311
|
+
transition: opacity 0.3s var(--ease-out);
|
|
312
|
+
}
|
|
313
|
+
@starting-style {
|
|
314
|
+
.sheet[open]::backdrop { opacity: 0; }
|
|
315
|
+
}
|
|
409
316
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
317
|
+
.sheet-panel {
|
|
318
|
+
position: relative; /* the Done action is positioned against this */
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
/* THE ONE SIZING RULE, and there is no media query anywhere in this file. The persona's column
|
|
322
|
+
is the active view, and its width is READ rather than repeated: `--column` is declared once in
|
|
323
|
+
@justai/ui and the page is sized from the same token. On a phone this is the whole screen; on a
|
|
324
|
+
desk it is the persona's own width, centred. It said 480 until 2026-08-04, in its own copy of
|
|
325
|
+
the number — so when the page became 768 the sheet stayed a phone's width on a tablet. */
|
|
326
|
+
/* THE FALLBACK IS NOT DECORATION. `--column` arrives with @justai/ui, and a page that uses this
|
|
327
|
+
cut without it — the harness's own sheet fixture is one — never sees the token at all. So the
|
|
328
|
+
literal here is the fleet's real width, not a historical one: it was 768 for an afternoon and
|
|
329
|
+
the sheet quietly rendered at 768 on a 640 fleet, which is how that was found. */
|
|
330
|
+
width: min(100%, var(--sheet-width, var(--column, 640px)));
|
|
331
|
+
/* dvh is Chromium 108+; the fallback above it survives because the build has targets. */
|
|
332
|
+
max-height: min(92vh, 100vh - 24px);
|
|
333
|
+
max-height: min(92dvh, 100dvh - 24px);
|
|
334
|
+
/* The material the fleet already agreed reads as legible glass (ConfirmSheet, 93%). */
|
|
335
|
+
background: color-mix(in srgb, var(--bg) 93%, transparent);
|
|
336
|
+
-webkit-backdrop-filter: blur(30px) saturate(1.5);
|
|
337
|
+
backdrop-filter: blur(30px) saturate(1.5);
|
|
338
|
+
border-top: 1px solid var(--border);
|
|
339
|
+
border-radius: 20px 20px 0 0;
|
|
340
|
+
/* Sliding UP with an overshoot spring would bounce the panel past the screen edge and back —
|
|
341
|
+
iOS decelerates into place instead — an overshoot spring belongs to something that scales,
|
|
342
|
+
not to something that travels to an edge. */
|
|
343
|
+
transform: translateY(0);
|
|
344
|
+
transition: transform 0.34s var(--ease-out);
|
|
345
|
+
will-change: transform;
|
|
346
|
+
}
|
|
347
|
+
@starting-style {
|
|
348
|
+
.sheet[open] .sheet-panel { transform: translateY(100%); }
|
|
349
|
+
}
|
|
350
|
+
/* The panel takes focus programmatically, so it must not wear a focus ring — it is a surface, not
|
|
351
|
+
a control. Focus a visitor MOVES lands on the rows, which do show one. */
|
|
352
|
+
.sheet-panel:focus { outline: none; }
|
|
353
|
+
/* While a finger is on it, the panel follows the finger with no transition — a 0.34s ease between
|
|
354
|
+
every pointermove is what makes a web sheet feel like a web page. */
|
|
355
|
+
.sheet[data-dragging] .sheet-panel { transition: none; }
|
|
413
356
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
357
|
+
.sheet-grip {
|
|
358
|
+
flex: none;
|
|
359
|
+
display: grid;
|
|
360
|
+
place-items: center;
|
|
361
|
+
height: 28px;
|
|
362
|
+
cursor: grab;
|
|
363
|
+
touch-action: none; /* the browser must not claim the vertical gesture we are reading */
|
|
418
364
|
}
|
|
365
|
+
.sheet-grip span {
|
|
366
|
+
width: 36px;
|
|
367
|
+
height: 5px;
|
|
368
|
+
border-radius: 2.5px;
|
|
369
|
+
background: color-mix(in srgb, var(--text) 22%, transparent);
|
|
370
|
+
}
|
|
371
|
+
.sheet:not([data-dragging]) .sheet-grip:active { cursor: grabbing; }
|
|
419
372
|
|
|
420
|
-
|
|
421
|
-
|
|
373
|
+
.sheet-title {
|
|
374
|
+
flex: none;
|
|
375
|
+
margin: 0;
|
|
376
|
+
padding: 2px 20px 10px;
|
|
377
|
+
font-size: var(--fs-md);
|
|
378
|
+
font-weight: 700;
|
|
379
|
+
letter-spacing: -0.01em;
|
|
380
|
+
color: var(--text);
|
|
381
|
+
text-align: center;
|
|
382
|
+
}
|
|
422
383
|
|
|
423
|
-
|
|
384
|
+
.sheet-body {
|
|
385
|
+
flex: 1 1 auto;
|
|
386
|
+
min-height: 0;
|
|
387
|
+
overflow-y: auto;
|
|
388
|
+
/* queries: the sheet scrolls sideways · horizontal scroll in a sheet · long text pushes the
|
|
389
|
+
panel wider · content wider than the sheet
|
|
390
|
+
NOTHING SCROLLS SIDEWAYS. A sheet is a column with a known width, and a person dragging it
|
|
391
|
+
left to read the end of a mime type has been handed a defect rather than a feature —
|
|
392
|
+
measured at 867px of content in a 390px panel, from one unbroken string.
|
|
424
393
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
394
|
+
Two halves, and both are needed: `overflow-x` stops the panel itself from becoming a
|
|
395
|
+
horizontal scroller, and `overflow-wrap` makes a long unbroken token WRAP instead of being
|
|
396
|
+
silently clipped by it. Content that is genuinely wide — a table, a code block — carries its
|
|
397
|
+
own scroller; the sheet never becomes one. */
|
|
398
|
+
overflow-x: hidden;
|
|
399
|
+
overflow-wrap: anywhere;
|
|
400
|
+
/* A scroll that reaches its end must not become the page's scroll behind the sheet. */
|
|
401
|
+
overscroll-behavior: contain;
|
|
402
|
+
-webkit-overflow-scrolling: touch;
|
|
403
|
+
/* The top padding is not spacing, it is CLEARANCE. This is a scroll container, so it clips at
|
|
404
|
+
its own edge — and a focused first child draws its ring OUTSIDE its border box, which lands
|
|
405
|
+
exactly on that edge and gets cut. The founder saw it on the picker's search field: the ring
|
|
406
|
+
reads as sliced along the top. 6px is a focus ring's width plus its offset, which is the only
|
|
407
|
+
number that makes the clipping impossible rather than unlikely. */
|
|
408
|
+
padding: 6px 12px max(14px, env(safe-area-inset-bottom));
|
|
409
|
+
}
|
|
429
410
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
these would hand that defect straight back, which is how it was found again: the spec named "a
|
|
434
|
-
consumer's own class cannot keep a page in the layout" failed the moment they went in.
|
|
411
|
+
/* The medium detent — half height, promoted to large by a pull upwards. It applies at EVERY width
|
|
412
|
+
now, because the gesture that changes it does too: the grabber is never hidden. */
|
|
413
|
+
.sheet[data-detent="medium"]:not([data-expanded]) .sheet-panel { max-height: min(56dvh, 100dvh - 24px); }
|
|
435
414
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
:global(dialog [data-sheet-page]:not(.is-current):not(.is-popping)) { display: none; }
|
|
441
|
-
:global([data-sheet-page]) { display: none; }
|
|
442
|
-
:global([data-sheet-page].is-current) { display: block; animation: sheet-push 0.22s var(--ease-out) both; }
|
|
443
|
-
:global([data-sheet-page].is-popping) { display: block; animation: sheet-pop 0.22s var(--ease-out) both; }
|
|
415
|
+
@media (prefers-reduced-motion: reduce) {
|
|
416
|
+
.sheet-panel, .sheet::backdrop { transition: none; }
|
|
417
|
+
.sheet-panel { transform: none; opacity: 1; }
|
|
418
|
+
}
|
|
444
419
|
</style>
|
|
445
420
|
|
|
446
421
|
<script>
|