@satinhq/kit 0.1.0-beta.2 → 0.1.0-beta.3
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/NOTICE +2 -2
- package/README.md +1 -1
- package/dist/acknowledgements/inventory.json +5 -5
- package/dist/actions/RowActions.d.ts +2 -2
- package/dist/actions/RowActions.js +2 -2
- package/dist/controls/civilDate.d.ts +1 -1
- package/dist/controls/civilDate.js +1 -1
- package/dist/docs/components/file-importer.md +1 -1
- package/dist/docs/components/paste-button.md +1 -1
- package/dist/docs/components/photos-picker.md +1 -1
- package/dist/docs/components/share-link.md +1 -1
- package/dist/docs/limitations.md +1 -1
- package/dist/docs/push.md +3 -3
- package/dist/docs/reference.md +1870 -0
- package/dist/glass/lens/controlLift.d.ts +1 -1
- package/dist/glass/lens/controlLift.js +1 -1
- package/dist/glass/lens/interactivePress.d.ts +2 -2
- package/dist/glass/lens/interactivePress.js +2 -2
- package/dist/glass/lens/menu-opening-native-fits.json +1 -1
- package/dist/glass/lens/menuContextFit.json +1 -1
- package/dist/glass/lens/menuDropFit.json +1 -1
- package/dist/glass/lens/menuField.d.ts +2 -2
- package/dist/glass/lens/menuField.js +3 -3
- package/dist/glass/lens/menuGlyphDropFit.json +1 -1
- package/dist/glass/lens/menuMorphFit.json +1 -1
- package/dist/glass/lens/menuMorphPhoneFit.json +1 -1
- package/dist/glass/lens/tabAccessoryMotion.d.ts +1 -1
- package/dist/glass/lens/tabSearchMember.d.ts +1 -1
- package/dist/glass/lens/tabSearchMember.js +1 -1
- package/dist/glass/menuPresenter.js +1 -1
- package/dist/glass/regions.d.ts +1 -1
- package/dist/glass/regions.js +1 -1
- package/dist/glass/shapeTrack.d.ts +1 -1
- package/dist/glass/submenuMotion.d.ts +1 -1
- package/dist/glass/submenuMotion.js +1 -1
- package/dist/glass/webgpu/entry.worker.js +3 -6
- package/dist/glass/webgpu/frameClock.d.ts +6 -2
- package/dist/glass/webgpu/frameClock.js +9 -2
- package/dist/glass/webgpu/frameProbe.d.ts +1 -1
- package/dist/glass/webgpu/frameProbe.js +3 -3
- package/dist/glass/webgpu/production.js +4 -0
- package/dist/glass/webgpu/raster.d.ts +18 -0
- package/dist/glass/webgpu/raster.js +105 -15
- package/dist/glass/webgpu/rasterMaterial.d.ts +4 -0
- package/dist/glass/webgpu/rasterMaterial.js +17 -1
- package/dist/glass/webgpu/regionScene.d.ts +4 -1
- package/dist/glass/webgpu/regionScene.js +30 -1
- package/dist/glass/webgpu/renderer.js +23 -7
- package/dist/keyboard/adapter.js +2 -2
- package/dist/keyboard/avoidance.d.ts +1 -1
- package/dist/keyboard/avoidance.js +1 -1
- package/dist/keyboard/motion.d.ts +1 -1
- package/dist/keyboard/motion.js +1 -1
- package/dist/list-form/ListForm.d.ts +2 -2
- package/dist/list-form/ListForm.js +1 -1
- package/dist/list-form/ListItem.js +1 -1
- package/dist/list-form/RowLabel.d.ts +1 -1
- package/dist/list-form/RowLabel.js +1 -1
- package/dist/list-form/Slider.js +1 -1
- package/dist/list-form/editMotion.js +1 -1
- package/dist/list-form/editing.d.ts +5 -5
- package/dist/list-form/editing.js +1 -1
- package/dist/motion/animationValue.d.ts +2 -2
- package/dist/motion/animationValue.js +9 -10
- package/dist/motion/matchedGeometry.d.ts +4 -4
- package/dist/motion/matchedGeometry.js +7 -8
- package/dist/motion/springTrack.d.ts +1 -2
- package/dist/motion/springTrack.js +10 -5
- package/dist/navigation/ScreenContainer.js +1 -1
- package/dist/navigation/chromeCurves.js +1 -1
- package/dist/navigation/types.d.ts +1 -1
- package/dist/navigation/zoomGeometry.js +1 -1
- package/dist/navigation/zoomMotion.d.ts +1 -1
- package/dist/navigation/zoomMotion.js +2 -2
- package/dist/navigation/zoomTransition.js +3 -4
- package/dist/presentation/Presentation.d.ts +2 -2
- package/dist/presentation/Presentation.js +1 -1
- package/dist/presentation/presentationSource.js +1 -1
- package/dist/public/controls.d.ts +4 -4
- package/dist/public/controls.js +7 -7
- package/dist/public/glass.d.ts +3 -4
- package/dist/public/glass.js +1 -2
- package/dist/public/navigationTransition.d.ts +1 -1
- package/dist/public/navigationTransition.js +1 -1
- package/dist/public/scroll.d.ts +1 -1
- package/dist/public/scroll.js +1 -1
- package/dist/public/toolbar.js +1 -1
- package/dist/safe-area/SafeAreaInset.js +3 -3
- package/dist/safe-area/modifiers.js +2 -2
- package/dist/safe-area/useSafeAreaMetrics.js +2 -2
- package/dist/scroll/ScrollSurface.d.ts +5 -5
- package/dist/scroll/ScrollSurface.js +1 -1
- package/dist/scroll/position.js +1 -1
- package/dist/scroll/screenGeometry.d.ts +1 -1
- package/dist/scroll/screenGeometry.js +1 -1
- package/dist/search/SearchDrawer.js +1 -1
- package/dist/search/searchMotion.d.ts +1 -1
- package/dist/search/searchMotion.js +1 -1
- package/dist/search/useSearchPresentation.js +1 -1
- package/dist/shapes/corners.js +1 -1
- package/dist/styles.css +55 -34
- package/dist/text-input/TextEditor.js +7 -3
- package/package.json +4 -4
- package/dist/docs/swiftui-reference.md +0 -1895
|
@@ -186,7 +186,8 @@ export declare class RegionScene {
|
|
|
186
186
|
private owner;
|
|
187
187
|
/** Worker-to-page events (a close morph that ended, so its panel can be torn down). */
|
|
188
188
|
private notify;
|
|
189
|
-
|
|
189
|
+
private readonly chrome?;
|
|
190
|
+
constructor(owner: LayerDevice, notify?: (event: GlassRegionEvent) => void, submissionAllowed?: () => boolean, chrome?: WebGPURaster);
|
|
190
191
|
request(request: GlassRendererRequest): Promise<unknown>;
|
|
191
192
|
/** New source images (uploads) since the last install: only those need a completion fence. */
|
|
192
193
|
private presentationReceipt;
|
|
@@ -200,6 +201,8 @@ export declare class RegionScene {
|
|
|
200
201
|
height: number;
|
|
201
202
|
pixels: Uint8Array<ArrayBuffer>;
|
|
202
203
|
}>;
|
|
204
|
+
/** Geometry sampled for the frame trace, in viewport points. No DOM measurements. */
|
|
205
|
+
accessoryGeometry(): string;
|
|
203
206
|
diagnostics(): {
|
|
204
207
|
navigation: {
|
|
205
208
|
key: string;
|
|
@@ -1132,7 +1132,9 @@ export class RegionScene {
|
|
|
1132
1132
|
owner;
|
|
1133
1133
|
/** Worker-to-page events (a close morph that ended, so its panel can be torn down). */
|
|
1134
1134
|
notify;
|
|
1135
|
-
|
|
1135
|
+
chrome;
|
|
1136
|
+
constructor(owner, notify = () => { }, submissionAllowed = () => true, chrome) {
|
|
1137
|
+
this.chrome = chrome;
|
|
1136
1138
|
this.submissionAllowed = () => !this.sourcePending && submissionAllowed();
|
|
1137
1139
|
this.owner = owner;
|
|
1138
1140
|
this.notify = notify;
|
|
@@ -1683,6 +1685,12 @@ export class RegionScene {
|
|
|
1683
1685
|
width: bounds.width,
|
|
1684
1686
|
height: bounds.height,
|
|
1685
1687
|
};
|
|
1688
|
+
if (this.chrome) {
|
|
1689
|
+
if (region.accessoryTrack?.held && p.descriptor.role === 'material')
|
|
1690
|
+
p.raster?.joinPresentation(this.chrome, p.descriptor.origin);
|
|
1691
|
+
else
|
|
1692
|
+
p.raster?.leavePresentation();
|
|
1693
|
+
}
|
|
1686
1694
|
const viewportTrack = region.presentationViewportTrack ?? region.accessoryViewportTrack ?? region.accessoryTrack;
|
|
1687
1695
|
const accessoryTranslation = viewportTrack && !held ? accessoryOffset(viewportTrack, this.trackClock()) : undefined;
|
|
1688
1696
|
const viewport = held
|
|
@@ -1902,6 +1910,8 @@ export class RegionScene {
|
|
|
1902
1910
|
p.raster.setViewport(viewport);
|
|
1903
1911
|
p.raster.setSubmissionGate(this.submissionAllowed);
|
|
1904
1912
|
await p.raster.init(p.canvas, this.owner, region.layer, p.descriptor);
|
|
1913
|
+
if (this.chrome && region.accessoryTrack?.held && p.descriptor.role === 'material')
|
|
1914
|
+
p.raster.joinPresentation(this.chrome, p.descriptor.origin);
|
|
1905
1915
|
p.raster.setVisible(this.visible);
|
|
1906
1916
|
fence = true;
|
|
1907
1917
|
}
|
|
@@ -2070,6 +2080,25 @@ export class RegionScene {
|
|
|
2070
2080
|
throw Error('Unknown glass presenter snapshot ' + id);
|
|
2071
2081
|
return raster.snapshot();
|
|
2072
2082
|
}
|
|
2083
|
+
/** Geometry sampled for the frame trace, in viewport points. No DOM measurements. */
|
|
2084
|
+
accessoryGeometry() {
|
|
2085
|
+
return JSON.stringify([...this.presenters.values()].flatMap(p => {
|
|
2086
|
+
const region = this.scene.regions.find(r => r.id === p.descriptor.regionIds[0]);
|
|
2087
|
+
const spec = p.raster?.spec;
|
|
2088
|
+
if (!region?.accessoryTrack || !spec || p.descriptor.role !== 'material')
|
|
2089
|
+
return [];
|
|
2090
|
+
const shape = spec.shape.first;
|
|
2091
|
+
return [
|
|
2092
|
+
{
|
|
2093
|
+
region: region.id,
|
|
2094
|
+
left: p.descriptor.origin.x + spec.frameX + shape.centerX - shape.halfWidth,
|
|
2095
|
+
right: p.descriptor.origin.x + spec.frameX + shape.centerX + shape.halfWidth,
|
|
2096
|
+
top: p.descriptor.origin.y + spec.frameY + shape.centerY - shape.halfHeight,
|
|
2097
|
+
inkLayers: spec.ink?.layers.length ?? 0,
|
|
2098
|
+
},
|
|
2099
|
+
];
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2073
2102
|
diagnostics() {
|
|
2074
2103
|
return {
|
|
2075
2104
|
navigation: this.navigation
|
|
@@ -65,7 +65,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
|
|
|
65
65
|
const raster = new WebGPURaster();
|
|
66
66
|
raster.setSubmissionGate(submissionAllowed);
|
|
67
67
|
const layers = new Map([['chrome', raster]]);
|
|
68
|
-
const appRegions = new RegionScene(owner, event => emit({ glassEvent: event }), submissionAllowed);
|
|
68
|
+
const appRegions = new RegionScene(owner, event => emit({ glassEvent: event }), submissionAllowed, clock.kind === 'worker-raf' ? raster : undefined);
|
|
69
69
|
let tab = new TabMotion(), sharedTabLift = false, reducedMotion = false;
|
|
70
70
|
const menuLift = new TabMotion(TAB_INTERACTIVE_MOTION);
|
|
71
71
|
let tabTiming;
|
|
@@ -435,8 +435,15 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
|
|
|
435
435
|
restart: restarted,
|
|
436
436
|
};
|
|
437
437
|
loopSample = undefined;
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
if (clock.kind === 'worker-raf')
|
|
439
|
+
raster.withSharedPresentation(() => {
|
|
440
|
+
appRegions.sample(at, sampleAt);
|
|
441
|
+
render(x, [], sampleAt);
|
|
442
|
+
});
|
|
443
|
+
else {
|
|
444
|
+
appRegions.sample(at, sampleAt);
|
|
445
|
+
render(x, [], sampleAt);
|
|
446
|
+
}
|
|
440
447
|
}
|
|
441
448
|
catch (error) {
|
|
442
449
|
frameContext = undefined;
|
|
@@ -481,7 +488,16 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
|
|
|
481
488
|
late: 0,
|
|
482
489
|
interval: previous === undefined ? null : at - previous,
|
|
483
490
|
restart: restarted,
|
|
484
|
-
clock: 'page-raf',
|
|
491
|
+
clock: clock.kind ?? 'page-raf',
|
|
492
|
+
homeLeft: raster.spec &&
|
|
493
|
+
raster.spec.frameX +
|
|
494
|
+
raster.spec.shape.first.centerX -
|
|
495
|
+
raster.spec.shape.first.halfWidth,
|
|
496
|
+
homeRight: raster.spec &&
|
|
497
|
+
raster.spec.frameX +
|
|
498
|
+
raster.spec.shape.first.centerX +
|
|
499
|
+
raster.spec.shape.first.halfWidth,
|
|
500
|
+
accessoryGeometry: clock.kind === 'worker-raf' ? appRegions.accessoryGeometry() : undefined,
|
|
485
501
|
callbackDelayMs,
|
|
486
502
|
presented: takePresentedCanvases(),
|
|
487
503
|
tabCentre: tab.frame.centre,
|
|
@@ -509,7 +525,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
|
|
|
509
525
|
}
|
|
510
526
|
};
|
|
511
527
|
owner.subscribe(device => {
|
|
512
|
-
frameProbe.useDevice(device);
|
|
528
|
+
frameProbe.useDevice(device, clock.kind !== 'worker-raf');
|
|
513
529
|
lost = !device;
|
|
514
530
|
if (!device) {
|
|
515
531
|
cancel();
|
|
@@ -934,7 +950,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
|
|
|
934
950
|
}
|
|
935
951
|
break;
|
|
936
952
|
case 'capability':
|
|
937
|
-
frameProbe.useDevice(await owner.ready());
|
|
953
|
+
frameProbe.useDevice(await owner.ready(), clock.kind !== 'worker-raf');
|
|
938
954
|
value = { backend: 'webgpu', timestampQuery: frameProbe.timestampQuery };
|
|
939
955
|
break;
|
|
940
956
|
case 'init':
|
|
@@ -1291,7 +1307,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
|
|
|
1291
1307
|
callbacks,
|
|
1292
1308
|
framePending: !!handle,
|
|
1293
1309
|
rate: 60,
|
|
1294
|
-
clock: 'page-raf',
|
|
1310
|
+
clock: clock.kind ?? 'page-raf',
|
|
1295
1311
|
visible,
|
|
1296
1312
|
presentationGate: {
|
|
1297
1313
|
allowed: submissionAllowed(),
|
package/dist/keyboard/adapter.js
CHANGED
|
@@ -150,7 +150,7 @@ export function dismissKeyboard() {
|
|
|
150
150
|
}
|
|
151
151
|
/** One adapter per SatinRoot. Owns viewport listeners, never cancels editing gestures. */
|
|
152
152
|
/**
|
|
153
|
-
* Tapping a control keeps the editor's keyboard,
|
|
153
|
+
* Tapping a control keeps the editor's keyboard, so button presses preserve the first
|
|
154
154
|
* responder: a hand-rolled composer's Send must not close the keyboard (iPad, October 12, 0/5 kept).
|
|
155
155
|
* Controls that leave the editor end editing themselves: navigation and tab switches dismiss
|
|
156
156
|
* explicitly, and a presented modal takes focus through showModal().
|
|
@@ -517,7 +517,7 @@ export function installKeyboardAdapter(root) {
|
|
|
517
517
|
releaseSurface();
|
|
518
518
|
revealedFor = '';
|
|
519
519
|
revealedEditor = null;
|
|
520
|
-
// Navigation ends editing,
|
|
520
|
+
// Navigation ends editing, when a screen is pushed or popped.
|
|
521
521
|
if (editable(document.activeElement) && root.contains(document.activeElement))
|
|
522
522
|
document.activeElement.blur();
|
|
523
523
|
};
|
|
@@ -7,7 +7,7 @@ export type KeyboardClearance = Readonly<{
|
|
|
7
7
|
bottom: number;
|
|
8
8
|
}>;
|
|
9
9
|
/**
|
|
10
|
-
* Keyboard avoidance for the focused editor,
|
|
10
|
+
* Keyboard avoidance for the focused editor, through the keyboard safe area: every
|
|
11
11
|
* scroll surface around the editor gets room below its content for the keyboard and scrolls the
|
|
12
12
|
* editor (a tall editor: its caret line) above the keyboard, innermost first. An editor on a page
|
|
13
13
|
* whose surfaces cannot bring it up (a screen without a scroll surface, or a nested scroller pinned
|
|
@@ -29,7 +29,7 @@ function keepEditorInBox(delta, editor, box, offsetTop, room) {
|
|
|
29
29
|
return Math.min(high, Math.max(low, delta));
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* Keyboard avoidance for the focused editor,
|
|
32
|
+
* Keyboard avoidance for the focused editor, through the keyboard safe area: every
|
|
33
33
|
* scroll surface around the editor gets room below its content for the keyboard and scrolls the
|
|
34
34
|
* editor (a tall editor: its caret line) above the keyboard, innermost first. An editor on a page
|
|
35
35
|
* whose surfaces cannot bring it up (a screen without a scroll surface, or a nested scroller pinned
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The software keyboard's show and hide motion. Measured per frame from the native composer's
|
|
3
|
-
* presentation layer (
|
|
3
|
+
* presentation layer (`ScrollView` + `.safeAreaInset(edge: .bottom)` chat, iOS 27.0
|
|
4
4
|
* Simulator, 4 shows and 4 hides; UIKit reports duration 0.3833 s, curve 7): a spring with mass 1,
|
|
5
5
|
* stiffness 535 and damping 45.5, released at rest, fits every frame of both directions within
|
|
6
6
|
* 0.84 pt of a 313 pt travel. UIKit ends the animation at 383 ms (0.4 pt short of rest).
|
package/dist/keyboard/motion.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { menuSpring } from '../glass/lens/menuSpring.js';
|
|
2
2
|
/**
|
|
3
3
|
* The software keyboard's show and hide motion. Measured per frame from the native composer's
|
|
4
|
-
* presentation layer (
|
|
4
|
+
* presentation layer (`ScrollView` + `.safeAreaInset(edge: .bottom)` chat, iOS 27.0
|
|
5
5
|
* Simulator, 4 shows and 4 hides; UIKit reports duration 0.3833 s, curve 7): a spring with mass 1,
|
|
6
6
|
* stiffness 535 and damping 45.5, released at rest, fits every frame of both directions within
|
|
7
7
|
* 0.84 pt of a 313 pt travel. UIKit ends the animation at 383 ms (0.4 pt short of rest).
|
|
@@ -11,13 +11,13 @@ export type ListEngineProps = Omit<ScrollSurfaceProps, 'axis'> & {
|
|
|
11
11
|
variant?: 'insetGrouped' | 'plain' | 'grouped' | 'sidebar';
|
|
12
12
|
navigationTitle?: string;
|
|
13
13
|
navigationTitleDisplay?: 'large' | 'inline';
|
|
14
|
-
/**
|
|
14
|
+
/** A search field in the navigation bar drawer above this List. */
|
|
15
15
|
searchable?: Searchable;
|
|
16
16
|
/** Default keeps measured header/footer gaps. Numbers are section gaps in points. */
|
|
17
17
|
listSectionSpacing?: 'default' | 'compact' | number;
|
|
18
18
|
/** Controls the scroll surface background, leaving row backgrounds intact. */
|
|
19
19
|
scrollContentBackground?: 'hidden' | 'visible';
|
|
20
|
-
/**
|
|
20
|
+
/** Selection state resolved by the public List. */
|
|
21
21
|
selectionBinding?: ListSelectionBinding;
|
|
22
22
|
};
|
|
23
23
|
/** A controlled List selection: one optional row, or a set of rows. */
|
|
@@ -410,7 +410,7 @@ export function StepperControl({ value, onChange, min = -Infinity, max = Infinit
|
|
|
410
410
|
return (_jsxs("span", { "data-satin-row-intrinsic": "true", "data-satin-stepper": "true", "data-satin-stepper-disabled": disabled ? 'true' : undefined, role: "group", "aria-label": `${label}, ${value}`, children: [_jsx(StepperButton, { onEditingChange: onEditingChange, label: `Decrease ${label}, ${value}`, disabled: disabled || value <= min, onStep: () => onChange(clampStepper(value, min, max, -step)), children: _jsx(SymbolImage, { name: "minus", weight: "medium" }) }), _jsx(StepperButton, { onEditingChange: onEditingChange, label: `Increase ${label}, ${value}`, disabled: disabled || value >= max, onStep: () => onChange(clampStepper(value, min, max, step)), children: _jsx(SymbolImage, { name: "plus", weight: "semibold" }) })] }));
|
|
411
411
|
}
|
|
412
412
|
export function StepperRow(props) {
|
|
413
|
-
return _jsx(Row, { label: props.label, trailing: _jsx(StepperControl, { ...props }) });
|
|
413
|
+
return (_jsx(Row, { className: "satin-stepper-row", label: props.label, trailing: _jsx(StepperControl, { ...props }) }));
|
|
414
414
|
}
|
|
415
415
|
export function SegmentedControl({ value, onChange, options, disabled = false, label = 'Options', style: cssStyle, }) {
|
|
416
416
|
const choose = useChooseOption({ options, disabled, onChange });
|
|
@@ -208,7 +208,7 @@ function playEditShift(item, before, after) {
|
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
210
|
* The reorder handle's pointer-down: lifts this row and hands it to the drag. The move is
|
|
211
|
-
* reported through `onMove` with
|
|
211
|
+
* reported through `onMove` with source-sequence offsets once the row settles; the new order must then
|
|
212
212
|
* come back through `data`, and until it does the rows stay where they were dropped.
|
|
213
213
|
*/
|
|
214
214
|
function useReorder(element, item, editing) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* A row title with a secondary line under it:
|
|
3
|
+
* A row title with a secondary line under it: a two-line label (a title, then a
|
|
4
4
|
* `.subheadline` `.secondary` line). `RowLabel`, a Toggle subtitle and any other two-line row
|
|
5
5
|
* label share this one body, so their type and spacing cannot drift apart.
|
|
6
6
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* A row title with a secondary line under it:
|
|
3
|
+
* A row title with a secondary line under it: a two-line label (a title, then a
|
|
4
4
|
* `.subheadline` `.secondary` line). `RowLabel`, a Toggle subtitle and any other two-line row
|
|
5
5
|
* label share this one body, so their type and spacing cannot drift apart.
|
|
6
6
|
*
|
package/dist/list-form/Slider.js
CHANGED
|
@@ -5,7 +5,7 @@ import { requestGlassSourceDamage } from '../glass/sourceDamage.js';
|
|
|
5
5
|
import { CONTROL_LIFT, controlLensSize } from '../glass/lens/controlLift.js';
|
|
6
6
|
import { SliderContact } from './sliderContact.js';
|
|
7
7
|
/**
|
|
8
|
-
* Native geometry (
|
|
8
|
+
* Native geometry (a slider in a form, iPhone 17 Pro Max Simulator, iOS 27.0, 3×
|
|
9
9
|
* stills, native control measurements `continuous.mov`): a 6 pt capsule track spans the row's content
|
|
10
10
|
* width, the 37 × 24 pt white thumb stays inside it (its centre travels from 18.5 pt to width −
|
|
11
11
|
* 18.5 pt), and the accent fill runs from the track start to the thumb centre with a round end.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Edit mode's row motion, measured from a
|
|
2
|
+
* Edit mode's row motion, measured from a native list with `onDelete` and `onMove`
|
|
3
3
|
* (iOS 27.0 Simulator, native reference `native list editing fixture native editing fixture`, five Edit/Done toggles, frame counter
|
|
4
4
|
* retimed: 0 skipped refreshes of 922; fits in native measurements).
|
|
5
5
|
*
|
|
@@ -5,7 +5,7 @@ export type EditMode = 'inactive' | 'active';
|
|
|
5
5
|
/** A row's identity in `List` selection: the `ForEach` id. */
|
|
6
6
|
export type ListItemID = string | number;
|
|
7
7
|
/**
|
|
8
|
-
* Every navigation destination owns one edit mode,
|
|
8
|
+
* Every navigation destination owns one edit mode, for its retained entry:
|
|
9
9
|
* its `EditButton` and its lists read and write the same value, and another screen keeps its own.
|
|
10
10
|
*/
|
|
11
11
|
export declare function EditModeScope({ children }: {
|
|
@@ -25,14 +25,14 @@ export type ListSelectionModel = Readonly<{
|
|
|
25
25
|
/** `single` selects one row (an optional binding); `multiple` collects a set. */
|
|
26
26
|
mode: 'single' | 'multiple';
|
|
27
27
|
isSelected(id: ListItemID): boolean;
|
|
28
|
-
/** A row tap: replaces a single selection; toggles a set only while editing
|
|
28
|
+
/** A row tap: replaces a single selection; toggles a set only while editing. */
|
|
29
29
|
activate(id: ListItemID, editing: boolean): void;
|
|
30
30
|
}>;
|
|
31
31
|
export declare const ListSelectionContext: import("react").Context<Readonly<{
|
|
32
32
|
/** `single` selects one row (an optional binding); `multiple` collects a set. */
|
|
33
33
|
mode: 'single' | 'multiple';
|
|
34
34
|
isSelected(id: ListItemID): boolean;
|
|
35
|
-
/** A row tap: replaces a single selection; toggles a set only while editing
|
|
35
|
+
/** A row tap: replaces a single selection; toggles a set only while editing. */
|
|
36
36
|
activate(id: ListItemID, editing: boolean): void;
|
|
37
37
|
}> | null>;
|
|
38
38
|
/**
|
|
@@ -82,7 +82,7 @@ export declare function forEachRows<T>(props: ForEachProps<T>): ForEachRow[];
|
|
|
82
82
|
/** What an editable row needs from its list item: edit state, selection and the item's actions. */
|
|
83
83
|
export type ListItemState = Readonly<{
|
|
84
84
|
editing: boolean;
|
|
85
|
-
/** While editing with a selection, a row's own press selects it, as a
|
|
85
|
+
/** While editing with a selection, a row's own press selects it, as a list row does. */
|
|
86
86
|
pressSelects: boolean;
|
|
87
87
|
select(): void;
|
|
88
88
|
/** Rows that also declare actions hand them to the item, which owns the row's one gesture host. */
|
|
@@ -94,7 +94,7 @@ export type RowActionsDelegation = RowActionsProps & Readonly<{
|
|
|
94
94
|
}>;
|
|
95
95
|
export declare const ListItemContext: import("react").Context<Readonly<{
|
|
96
96
|
editing: boolean;
|
|
97
|
-
/** While editing with a selection, a row's own press selects it, as a
|
|
97
|
+
/** While editing with a selection, a row's own press selects it, as a list row does. */
|
|
98
98
|
pressSelects: boolean;
|
|
99
99
|
select(): void;
|
|
100
100
|
/** Rows that also declare actions hand them to the item, which owns the row's one gesture host. */
|
|
@@ -3,7 +3,7 @@ import { createContext, Fragment, useContext, useMemo, useState } from 'react';
|
|
|
3
3
|
const inactiveEverywhere = ['inactive', () => { }];
|
|
4
4
|
const EditModeContext = createContext(inactiveEverywhere);
|
|
5
5
|
/**
|
|
6
|
-
* Every navigation destination owns one edit mode,
|
|
6
|
+
* Every navigation destination owns one edit mode, for its retained entry:
|
|
7
7
|
* its `EditButton` and its lists read and write the same value, and another screen keeps its own.
|
|
8
8
|
*/
|
|
9
9
|
export function EditModeScope({ children }) {
|
|
@@ -4,14 +4,14 @@ declare const brand: unique symbol;
|
|
|
4
4
|
export type Animation = Readonly<{
|
|
5
5
|
[brand]: true;
|
|
6
6
|
}>;
|
|
7
|
-
/**
|
|
7
|
+
/** Options for a spring defined by duration and bounce. */
|
|
8
8
|
export type SpringOptions = Readonly<{
|
|
9
9
|
/** The perceptual duration in milliseconds. Defaults to 500. */
|
|
10
10
|
durationMs?: number;
|
|
11
11
|
/** 0 settles without overshoot. Values are clamped to 0 through 0.99. Defaults to 0. */
|
|
12
12
|
bounce?: number;
|
|
13
13
|
}>;
|
|
14
|
-
/**
|
|
14
|
+
/** Named spring timing values. `default` is the curve `withAnimation` uses with no argument. */
|
|
15
15
|
export declare const Animation: Readonly<{
|
|
16
16
|
spring: (options?: SpringOptions) => Animation;
|
|
17
17
|
smooth: Animation;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const brand = Symbol('satin.animation');
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Duration is the
|
|
4
4
|
* period of the undamped spring (omega = 2π / duration), and bounce is 1 − damping ratio for
|
|
5
|
-
* bounce ≥ 0.
|
|
5
|
+
* bounce ≥ 0. Negative, over-damped bounces are not offered: the shared spring
|
|
6
6
|
* solver (glass/lens/menuSpring.ts) models damping ratios up to 1.
|
|
7
7
|
*/
|
|
8
8
|
function spring({ durationMs = 500, bounce = 0 } = {}) {
|
|
@@ -15,24 +15,23 @@ function spring({ durationMs = 500, bounce = 0 } = {}) {
|
|
|
15
15
|
const animation = Object.freeze({ [brand]: true, omega, zeta });
|
|
16
16
|
return animation;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
18
|
+
/** Named spring timing values. `default` is the curve `withAnimation` uses with no argument. */
|
|
19
19
|
export const Animation = Object.freeze({
|
|
20
20
|
spring,
|
|
21
|
-
/**
|
|
21
|
+
/** Smooth spring: duration 0.5 s, bounce 0. */
|
|
22
22
|
smooth: spring({ durationMs: 500, bounce: 0 }),
|
|
23
|
-
/**
|
|
23
|
+
/** Snappy spring: duration 0.5 s, bounce 0.15. */
|
|
24
24
|
snappy: spring({ durationMs: 500, bounce: 0.15 }),
|
|
25
|
-
/**
|
|
25
|
+
/** Bouncy spring: duration 0.5 s, bounce 0.3. */
|
|
26
26
|
bouncy: spring({ durationMs: 500, bounce: 0.3 }),
|
|
27
|
-
/**
|
|
27
|
+
/** Default spring. The value is the measured fit recorded in the measured reference
|
|
28
28
|
* (matchedGeometryEffect, October 11); see DEFAULT_ANIMATION_DURATION_MS. */
|
|
29
29
|
get default() {
|
|
30
30
|
return defaultAnimation;
|
|
31
31
|
},
|
|
32
32
|
});
|
|
33
|
-
/*
|
|
34
|
-
*
|
|
35
|
-
* matched-geometry implementation (the measured reference, October 11). */
|
|
33
|
+
/* Default perceptual duration for the critically damped animation spring. The measured
|
|
34
|
+
* fit and recording provenance live in the measured reference. */
|
|
36
35
|
export const DEFAULT_ANIMATION_DURATION_MS = 500;
|
|
37
36
|
const defaultAnimation = spring({ durationMs: DEFAULT_ANIMATION_DURATION_MS, bounce: 0 });
|
|
38
37
|
/** The spring with a unit displacement and the given initial velocity (units per second). */
|
|
@@ -27,22 +27,22 @@ export type MatchedGeometryEffectProps = Readonly<{
|
|
|
27
27
|
children: ReactNode;
|
|
28
28
|
}>;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* When a `withAnimation` change removes this
|
|
31
31
|
* view and inserts another with the same id, the new one moves from the old one's frame to its
|
|
32
32
|
* own while the two cross-fade. A view that only moves or resizes
|
|
33
33
|
* animates from where it was. A matched view with no partner fades in or out.
|
|
34
34
|
*
|
|
35
35
|
* Wrap one element that has no transform of its own. Changes made outside `withAnimation`
|
|
36
|
-
* apply at once
|
|
36
|
+
* apply at once.
|
|
37
37
|
* @example
|
|
38
38
|
* const art = useNamespace()
|
|
39
39
|
* <MatchedGeometryEffect id="artwork" in={art}><Image src={cover} alt="" /></MatchedGeometryEffect>
|
|
40
|
-
* @experimental
|
|
40
|
+
* @experimental Matches the frame around its center only.
|
|
41
41
|
* A non-source view does not follow its source outside an animation.
|
|
42
42
|
*/
|
|
43
43
|
export declare function MatchedGeometryEffect({ id, in: namespace, isSource, children, }: MatchedGeometryEffectProps): import("react").JSX.Element;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Applies the state changes in `update`, then animates the
|
|
46
46
|
* `MatchedGeometryEffect` views they move, insert or remove. Call it from an event handler.
|
|
47
47
|
* Keyed List rows also move to their new positions. Other views take their new layout at once.
|
|
48
48
|
* Returns the callback result and propagates errors. Only synchronous updates participate.
|
|
@@ -17,9 +17,8 @@ import { fadeSettleMs, runFrameTimes, sampleChannel, sampleProgress, } from './s
|
|
|
17
17
|
*
|
|
18
18
|
* A view whose id leaves and enters in the same change keeps its identity: the new view starts
|
|
19
19
|
* from the old one's frame and fades in, and the old view stays on screen as a "ghost" (its own
|
|
20
|
-
* detached DOM node, already decoded) that travels with it and fades out.
|
|
21
|
-
*
|
|
22
|
-
* on the animation's own spring (the measured reference, October 11).
|
|
20
|
+
* detached DOM node, already decoded) that travels with it and fades out. Their opacities sum to 1
|
|
21
|
+
* throughout the spring-driven cross-fade. Measurement provenance lives in the measured reference.
|
|
23
22
|
*/
|
|
24
23
|
const namespaceBrand = Symbol('satin.namespace');
|
|
25
24
|
const entries = new Set();
|
|
@@ -40,17 +39,17 @@ export function defineNamespace() {
|
|
|
40
39
|
return Object.freeze({ [namespaceBrand]: true });
|
|
41
40
|
}
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
42
|
+
* When a `withAnimation` change removes this
|
|
44
43
|
* view and inserts another with the same id, the new one moves from the old one's frame to its
|
|
45
44
|
* own while the two cross-fade. A view that only moves or resizes
|
|
46
45
|
* animates from where it was. A matched view with no partner fades in or out.
|
|
47
46
|
*
|
|
48
47
|
* Wrap one element that has no transform of its own. Changes made outside `withAnimation`
|
|
49
|
-
* apply at once
|
|
48
|
+
* apply at once.
|
|
50
49
|
* @example
|
|
51
50
|
* const art = useNamespace()
|
|
52
51
|
* <MatchedGeometryEffect id="artwork" in={art}><Image src={cover} alt="" /></MatchedGeometryEffect>
|
|
53
|
-
* @experimental
|
|
52
|
+
* @experimental Matches the frame around its center only.
|
|
54
53
|
* A non-source view does not follow its source outside an animation.
|
|
55
54
|
*/
|
|
56
55
|
export function MatchedGeometryEffect({ id, in: namespace, isSource = true, children, }) {
|
|
@@ -226,13 +225,13 @@ function moveView(element, key, from, box, animation) {
|
|
|
226
225
|
target: box,
|
|
227
226
|
velocity: from.velocity,
|
|
228
227
|
animation,
|
|
229
|
-
// A new partner fades in
|
|
228
|
+
// A new partner fades in through the default opacity transition; a view that stays carries
|
|
230
229
|
// on from the opacity it is showing.
|
|
231
230
|
opacity: { from: stayed ? from.opacity : 0, to: 1 },
|
|
232
231
|
ghost: false,
|
|
233
232
|
});
|
|
234
233
|
}
|
|
235
|
-
/**
|
|
234
|
+
/** An opacity transition for a matched view with no partner. */
|
|
236
235
|
function fadeView(element, key, box, animation, opacity) {
|
|
237
236
|
startRun({
|
|
238
237
|
element,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type MenuSpring } from '../glass/lens/menuSpring.js';
|
|
2
|
-
/** A
|
|
3
|
-
* `delay` ms after the animation starts. */
|
|
2
|
+
/** A unit-mass spring defined by stiffness and damping, released after an optional delay in ms. */
|
|
4
3
|
export type SpringParameters = Readonly<{
|
|
5
4
|
stiffness: number;
|
|
6
5
|
damping: number;
|
|
@@ -28,19 +28,22 @@ export function sampledKeyframes(track, frame) {
|
|
|
28
28
|
return track.progress.map((progress, index) => ({ ...frame(progress), offset: index / last }));
|
|
29
29
|
}
|
|
30
30
|
export function sampledMotionTrack(duration, sample, tolerance = 0.2) {
|
|
31
|
-
if (!(duration > 0) ||
|
|
31
|
+
if (!(duration > 0) ||
|
|
32
|
+
!Number.isFinite(duration) ||
|
|
33
|
+
!(tolerance > 0) ||
|
|
34
|
+
!Number.isFinite(tolerance))
|
|
32
35
|
throw Error('Motion tracks require a finite positive duration and tolerance');
|
|
33
36
|
let count = Math.max(1, Math.ceil(duration / (1000 / 120)));
|
|
34
37
|
// GPU and compositor consumers require even offsets. Refine the whole grid,
|
|
35
38
|
// rather than giving them independent adaptive sample positions.
|
|
36
39
|
while (count <= 4096) {
|
|
37
|
-
const values = Array.from({ length: count + 1 }, (_, index) => sample(duration * index / count));
|
|
40
|
+
const values = Array.from({ length: count + 1 }, (_, index) => sample((duration * index) / count));
|
|
38
41
|
if (!values.every(Number.isFinite))
|
|
39
42
|
throw Error('Motion track contains a non-finite value');
|
|
40
43
|
let accurate = true;
|
|
41
44
|
for (let index = 0; accurate && index < count; index++) {
|
|
42
45
|
for (const fraction of [0.25, 0.5, 0.75]) {
|
|
43
|
-
const exact = sample(duration * (index + fraction) / count);
|
|
46
|
+
const exact = sample((duration * (index + fraction)) / count);
|
|
44
47
|
const linear = values[index] + (values[index + 1] - values[index]) * fraction;
|
|
45
48
|
if (!Number.isFinite(exact))
|
|
46
49
|
throw Error('Motion track contains a non-finite value');
|
|
@@ -59,11 +62,13 @@ export function sampledMotionTrack(duration, sample, tolerance = 0.2) {
|
|
|
59
62
|
/** Read the submitted segment, including its velocity, when a run is interrupted. */
|
|
60
63
|
export function sampledMotionState(track, milliseconds) {
|
|
61
64
|
const last = track.values.length - 1;
|
|
62
|
-
const position = Math.max(0, Math.min(last, milliseconds / track.duration * last));
|
|
65
|
+
const position = Math.max(0, Math.min(last, (milliseconds / track.duration) * last));
|
|
63
66
|
const index = Math.min(last - 1, Math.floor(position));
|
|
64
67
|
const from = track.values[index], to = track.values[index + 1];
|
|
65
68
|
return {
|
|
66
69
|
value: from + (to - from) * (position - index),
|
|
67
|
-
velocity: milliseconds < 0 || milliseconds >= track.duration
|
|
70
|
+
velocity: milliseconds < 0 || milliseconds >= track.duration
|
|
71
|
+
? 0
|
|
72
|
+
: (((to - from) * last) / track.duration) * 1000,
|
|
68
73
|
};
|
|
69
74
|
}
|
|
@@ -130,7 +130,7 @@ export function ScreenContainer({ children, entry, interactionBlocked = false, c
|
|
|
130
130
|
}), [entry.key, entry.params, entry.transition.phase, isCovered, scrollRegistry, tabActive]);
|
|
131
131
|
useLayoutEffect(() => navigator[navigatorScrollRuntime].bind(entry.key, scrollRegistry), [entry.key, navigator, scrollRegistry]);
|
|
132
132
|
// Without a primary scroll view, content is laid out in the safe area below the bar, as a
|
|
133
|
-
//
|
|
133
|
+
// VStack is (styles.css). The attribute follows the registry in the same frame.
|
|
134
134
|
// The primary's vertical scroll indicator is drawn in the screen frame (scroll/scrollIndicator.ts).
|
|
135
135
|
const motionLayer = useRef(null);
|
|
136
136
|
const indicatorTrack = useRef(null);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Measured iOS 27 navigation bar chrome curves (what bar items do while a page moves) and the
|
|
2
2
|
// keyframe builders that turn them into Web Animations keyframes. Pure data and math: no DOM.
|
|
3
3
|
/** Incoming bar chrome, measured on the iOS 27.0 Simulator from the presentation layers of stock
|
|
4
|
-
*
|
|
4
|
+
* Measured at every display frame,
|
|
5
5
|
* light and dark agree within 0.04): milliseconds since the page starts moving. A push brings the
|
|
6
6
|
* destination's glass items in faster than a pop brings the revealed screen's back: a push shows
|
|
7
7
|
* 0.56 at 100 ms (Reminders-style list → list, back circle and trailing "…" together), a pop 0.30
|
|
@@ -16,7 +16,7 @@ export type NavigationBarDefinition<Params = unknown> = Readonly<{
|
|
|
16
16
|
display?: TitleDisplayMode;
|
|
17
17
|
visibility?: Visibility;
|
|
18
18
|
title: string | ((params: Params) => string);
|
|
19
|
-
/**
|
|
19
|
+
/** A line under the title, inline or large. */
|
|
20
20
|
subtitle?: string;
|
|
21
21
|
}>;
|
|
22
22
|
export type TransitionControllerContext = Readonly<{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* Zoom navigation geometry, as pure functions of a rectangle.
|
|
3
3
|
*
|
|
4
4
|
* At every moment the pushed screen is shown inside a rounded "window" rectangle that moves between
|
|
5
5
|
* the source view's frame and the full screen. As natively, the screen's content is scaled so its
|
|
@@ -9,7 +9,7 @@ export type ZoomSprings = Readonly<{
|
|
|
9
9
|
size: ZoomSpring;
|
|
10
10
|
}>;
|
|
11
11
|
/**
|
|
12
|
-
* Push: the centre and the width move together, critically damped (half way at 88 ms;
|
|
12
|
+
* Push: the centre and the width move together, critically damped (half way at 88 ms; the measured
|
|
13
13
|
* duration 0.33 s); the height lags slightly. Native moves the width with the centre, so the
|
|
14
14
|
* width shares the centre's spring here and the height takes the size spring (rms 0.002, 51 ticks).
|
|
15
15
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Measured values
|
|
2
|
+
* Measured spring values for the zoom navigation transition on iOS 27.
|
|
3
3
|
*
|
|
4
4
|
* native reference: a stock native fixture (a 180 pt tile with a 10 pt corner in a
|
|
5
5
|
* NavigationStack grid, pushed to a full-screen page), iPhone 17 Pro Max Simulator, iOS 27.0.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (the measured reference, October 11).
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Push: the centre and the width move together, critically damped (half way at 88 ms;
|
|
13
|
+
* Push: the centre and the width move together, critically damped (half way at 88 ms; the measured
|
|
14
14
|
* duration 0.33 s); the height lags slightly. Native moves the width with the centre, so the
|
|
15
15
|
* width shares the centre's spring here and the height takes the size spring (rms 0.002, 51 ticks).
|
|
16
16
|
*/
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
3
|
-
* from, and `.navigationTransition(.zoom(sourceID:in:))` on the pushed screen.
|
|
2
|
+
* Zoom navigation connects a MatchedTransitionSource to a screen configured with a zoom transition.
|
|
4
3
|
*
|
|
5
4
|
* A push whose screen asks for a zoom, and whose source is on screen in the screen it covers, grows
|
|
6
5
|
* out of the source instead of sliding in. Its pop (the back button) and a downward drag on the
|
|
@@ -116,7 +115,7 @@ export function visibleZoomSource(entryKey, rear, excluded) {
|
|
|
116
115
|
}
|
|
117
116
|
return chosen?.view;
|
|
118
117
|
}
|
|
119
|
-
/**
|
|
118
|
+
/** Zoom transitions require a visible source: at least part of it in the viewport. */
|
|
120
119
|
function isOnScreen(view) {
|
|
121
120
|
const bounds = view.getBoundingClientRect();
|
|
122
121
|
if (!(bounds.width > 0 && bounds.height > 0))
|
|
@@ -950,7 +949,7 @@ export function windowKeyframes(from, to, springs, velocity) {
|
|
|
950
949
|
frames.push({ ms: frames.at(-1).ms + FRAME_MS, rect: to });
|
|
951
950
|
return frames;
|
|
952
951
|
}
|
|
953
|
-
/** A measured Core Animation spring as Satin's `Animation` (
|
|
952
|
+
/** A measured Core Animation spring as Satin's `Animation` (the duration/bounce representation is
|
|
954
953
|
* exact: omega = 2π / duration, damping ratio = 1 − bounce). */
|
|
955
954
|
function animationOf(spring) {
|
|
956
955
|
return SpringAnimation.spring({
|