@satinhq/kit 0.1.0-beta.3 → 0.1.0-beta.4

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.
Files changed (240) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +1 -2
  3. package/dist/acknowledgements/inventory.json +1 -1
  4. package/dist/actions/RowActions.d.ts +2 -2
  5. package/dist/actions/RowActions.js +91 -25
  6. package/dist/actions/fullSwipeContact.d.ts +73 -0
  7. package/dist/actions/fullSwipeContact.js +186 -0
  8. package/dist/actions/rowScrub.d.ts +7 -4
  9. package/dist/actions/rowScrub.js +39 -37
  10. package/dist/actions/types.d.ts +13 -7
  11. package/dist/app/InstallComponents.d.ts +4 -7
  12. package/dist/app/InstallComponents.js +7 -7
  13. package/dist/app/InstallationDiagnostics.d.ts +2 -1
  14. package/dist/app/InstallationDiagnostics.js +2 -1
  15. package/dist/app/appearance.d.ts +5 -3
  16. package/dist/app/appearance.js +5 -3
  17. package/dist/app/appearanceStore.d.ts +3 -2
  18. package/dist/app/buildInfo.d.ts +4 -4
  19. package/dist/app/buildInfo.js +2 -2
  20. package/dist/app/definition.d.ts +11 -3
  21. package/dist/app/definition.js +2 -0
  22. package/dist/app/head.d.ts +2 -1
  23. package/dist/app/head.js +13 -16
  24. package/dist/app/inlineCSP.d.ts +6 -0
  25. package/dist/app/inlineCSP.js +25 -0
  26. package/dist/app/install.d.ts +1 -0
  27. package/dist/app/install.js +5 -2
  28. package/dist/app/installCheck.js +32 -86
  29. package/dist/app/installContext.d.ts +10 -5
  30. package/dist/app/installContext.js +2 -0
  31. package/dist/app/installCustom.js +1 -0
  32. package/dist/app/installExperience.js +0 -60
  33. package/dist/app/installationFailure.d.ts +3 -2
  34. package/dist/app/installationFailure.js +1 -1
  35. package/dist/app/launchScreen.d.ts +6 -2
  36. package/dist/app/launchScreen.js +104 -1
  37. package/dist/app/launchScreenRuntime.d.ts +31 -4
  38. package/dist/app/launchScreenRuntime.js +150 -22
  39. package/dist/app/launchWatchdog.js +25 -65
  40. package/dist/app/platformScripts.js +11 -0
  41. package/dist/app/styles.d.ts +3 -0
  42. package/dist/app/styles.js +42 -0
  43. package/dist/app/viewportValue.d.ts +2 -1
  44. package/dist/app/vite.d.ts +4 -0
  45. package/dist/app/vite.js +22 -24
  46. package/dist/content/content.d.ts +35 -30
  47. package/dist/content/content.js +19 -16
  48. package/dist/content/layout.d.ts +10 -2
  49. package/dist/content/layout.js +4 -2
  50. package/dist/content/scaledMetric.d.ts +2 -1
  51. package/dist/content/scaledMetric.js +2 -1
  52. package/dist/controls/ColorPicker.d.ts +2 -4
  53. package/dist/controls/ColorPicker.js +2 -4
  54. package/dist/controls/DatePicker.d.ts +4 -4
  55. package/dist/controls/DatePicker.js +2 -2
  56. package/dist/controls/ProgressView.d.ts +4 -4
  57. package/dist/controls/ProgressView.js +1 -2
  58. package/dist/data/database.d.ts +9 -8
  59. package/dist/data/database.js +4 -4
  60. package/dist/docs/reference.md +982 -964
  61. package/dist/docs/tailwind.md +76 -0
  62. package/dist/docs/tokens.md +1 -1
  63. package/dist/gestures/GestureView.d.ts +3 -1
  64. package/dist/gestures/GestureView.js +2 -1
  65. package/dist/gestures/gesture.d.ts +22 -2
  66. package/dist/gestures/gesture.js +7 -2
  67. package/dist/glass/GlassEffect.d.ts +2 -1
  68. package/dist/glass/GlassEffect.js +2 -1
  69. package/dist/glass/GlassPresentation.d.ts +1 -0
  70. package/dist/glass/GlassPresentation.js +21 -4
  71. package/dist/glass/TabBar.js +23 -1
  72. package/dist/glass/errorAlert.js +2 -1
  73. package/dist/glass/errors.d.ts +3 -1
  74. package/dist/glass/fallback/runtime.d.ts +1 -0
  75. package/dist/glass/fallback/runtime.js +8 -1
  76. package/dist/glass/liquidContext.d.ts +2 -0
  77. package/dist/glass/menuPresenter.d.ts +2 -0
  78. package/dist/glass/regions.d.ts +2 -1
  79. package/dist/glass/renderSpecs.d.ts +1 -0
  80. package/dist/glass/renderSpecs.js +1 -0
  81. package/dist/glass/webgpu/controlRim.d.ts +29 -0
  82. package/dist/glass/webgpu/controlRim.js +68 -0
  83. package/dist/glass/webgpu/passes.d.ts +2 -2
  84. package/dist/glass/webgpu/production.d.ts +1 -0
  85. package/dist/glass/webgpu/production.js +27 -1
  86. package/dist/glass/webgpu/rasterUniforms.js +9 -0
  87. package/dist/glass/webgpu/shaders.d.ts +2 -2
  88. package/dist/glass/webgpu/shaders.js +5 -1
  89. package/dist/haptics/SensoryButton.d.ts +4 -4
  90. package/dist/haptics/SensoryButton.js +3 -3
  91. package/dist/image/Image.d.ts +6 -3
  92. package/dist/image/Image.js +4 -3
  93. package/dist/image/asset.d.ts +2 -1
  94. package/dist/keyboard/adapter.d.ts +3 -4
  95. package/dist/keyboard/geometry.d.ts +1 -1
  96. package/dist/list-form/ListForm.d.ts +8 -7
  97. package/dist/list-form/ListForm.js +2 -2
  98. package/dist/list-form/editing.d.ts +5 -6
  99. package/dist/list-form/editing.js +4 -5
  100. package/dist/list-form/offsets.d.ts +2 -2
  101. package/dist/list-form/offsets.js +2 -2
  102. package/dist/media/react.d.ts +2 -1
  103. package/dist/media/react.js +2 -1
  104. package/dist/media/types.d.ts +6 -3
  105. package/dist/motion/TransitionView.d.ts +4 -2
  106. package/dist/motion/TransitionView.js +3 -2
  107. package/dist/motion/animationValue.d.ts +1 -1
  108. package/dist/motion/matchedGeometry.d.ts +6 -5
  109. package/dist/motion/matchedGeometry.js +2 -1
  110. package/dist/motion/transition.d.ts +4 -1
  111. package/dist/motion/transition.js +2 -0
  112. package/dist/navigation/DetailList.d.ts +1 -1
  113. package/dist/navigation/DetailList.js +1 -1
  114. package/dist/navigation/Sidebar.d.ts +3 -1
  115. package/dist/navigation/Sidebar.js +1 -1
  116. package/dist/navigation/SidebarSymbol.d.ts +2 -1
  117. package/dist/navigation/SidebarSymbol.js +2 -1
  118. package/dist/navigation/navigationContext.d.ts +2 -1
  119. package/dist/navigation/navigationContext.js +2 -1
  120. package/dist/navigation/splitContext.d.ts +2 -1
  121. package/dist/navigation/splitContext.js +2 -1
  122. package/dist/navigation/toolbarMinimization.d.ts +4 -3
  123. package/dist/navigation/types.d.ts +7 -5
  124. package/dist/presentation/Presentation.d.ts +8 -7
  125. package/dist/presentation/Presentation.js +3 -2
  126. package/dist/presentation/model.d.ts +2 -1
  127. package/dist/press/Pressable.d.ts +5 -1
  128. package/dist/press/Pressable.js +2 -0
  129. package/dist/press/stateMachine.d.ts +1 -1
  130. package/dist/public/Label.d.ts +2 -1
  131. package/dist/public/Label.js +2 -1
  132. package/dist/public/Screen.d.ts +4 -6
  133. package/dist/public/actions.d.ts +2 -1
  134. package/dist/public/actions.js +2 -1
  135. package/dist/public/buttonBorderShape.d.ts +2 -1
  136. package/dist/public/colors.d.ts +7 -4
  137. package/dist/public/colors.js +3 -2
  138. package/dist/public/controlStyle.d.ts +17 -14
  139. package/dist/public/controls.d.ts +27 -26
  140. package/dist/public/controls.js +20 -19
  141. package/dist/public/deviceActions.d.ts +6 -4
  142. package/dist/public/deviceActions.js +4 -2
  143. package/dist/public/environment.d.ts +16 -13
  144. package/dist/public/environment.js +11 -9
  145. package/dist/public/filePickers.d.ts +6 -4
  146. package/dist/public/filePickers.js +3 -2
  147. package/dist/public/glass.d.ts +6 -3
  148. package/dist/public/glass.js +2 -1
  149. package/dist/public/glassValue.d.ts +3 -4
  150. package/dist/public/glassValue.js +3 -4
  151. package/dist/public/keyboard.d.ts +4 -1
  152. package/dist/public/keyboard.js +2 -1
  153. package/dist/public/navigationTransition.d.ts +5 -5
  154. package/dist/public/navigationTransition.js +2 -2
  155. package/dist/public/presentation.d.ts +8 -7
  156. package/dist/public/presentation.js +5 -5
  157. package/dist/public/runtime.d.ts +9 -8
  158. package/dist/public/runtime.js +7 -7
  159. package/dist/public/scroll.d.ts +6 -3
  160. package/dist/public/scroll.js +3 -1
  161. package/dist/public/structure.d.ts +2 -2
  162. package/dist/public/toolbar.d.ts +15 -11
  163. package/dist/public/toolbar.js +12 -7
  164. package/dist/reachability/model.d.ts +4 -3
  165. package/dist/reachability/react.d.ts +4 -1
  166. package/dist/reachability/react.js +2 -1
  167. package/dist/root/SatinRoot.d.ts +3 -3
  168. package/dist/root/SatinRoot.js +12 -36
  169. package/dist/root/mountSatin.d.ts +1 -1
  170. package/dist/root/mountSatin.js +26 -35
  171. package/dist/root/runtimeContext.d.ts +2 -1
  172. package/dist/root/runtimeContext.js +2 -1
  173. package/dist/safe-area/Overlay.d.ts +3 -2
  174. package/dist/safe-area/Overlay.js +2 -1
  175. package/dist/safe-area/SafeAreaInset.d.ts +11 -7
  176. package/dist/safe-area/SafeAreaInset.js +7 -6
  177. package/dist/safe-area/context.d.ts +3 -3
  178. package/dist/safe-area/context.js +1 -1
  179. package/dist/safe-area/displayCorners.d.ts +4 -2
  180. package/dist/safe-area/displayCorners.js +4 -2
  181. package/dist/safe-area/modifiers.d.ts +14 -10
  182. package/dist/safe-area/modifiers.js +8 -4
  183. package/dist/scroll/ScrollSurface.js +5 -1
  184. package/dist/scroll/ScrollTarget.d.ts +1 -1
  185. package/dist/scroll/ScrollTarget.js +1 -1
  186. package/dist/scroll/edgePolicy.d.ts +2 -2
  187. package/dist/scroll/position.d.ts +5 -3
  188. package/dist/scroll/types.d.ts +1 -1
  189. package/dist/search/PadSearchDrawer.d.ts +7 -0
  190. package/dist/search/PadSearchDrawer.js +8 -0
  191. package/dist/search/PadSearchPresentationContext.d.ts +6 -0
  192. package/dist/search/PadSearchPresentationContext.js +4 -0
  193. package/dist/search/SearchDrawer.d.ts +1 -1
  194. package/dist/search/SearchDrawer.js +6 -1
  195. package/dist/search/SearchSuggestionsPopover.d.ts +6 -0
  196. package/dist/search/SearchSuggestionsPopover.js +42 -0
  197. package/dist/search/SearchToolbarField.d.ts +2 -1
  198. package/dist/search/SearchToolbarField.js +28 -7
  199. package/dist/search/padSearchBarMotion.d.ts +69 -0
  200. package/dist/search/padSearchBarMotion.js +73 -0
  201. package/dist/search/padSearchFieldSamples.json +491 -0
  202. package/dist/search/searchPlacement.d.ts +3 -0
  203. package/dist/search/searchPlacement.js +7 -0
  204. package/dist/search/searchable.d.ts +14 -12
  205. package/dist/search/usePadSearchFieldMotion.d.ts +4 -0
  206. package/dist/search/usePadSearchFieldMotion.js +100 -0
  207. package/dist/search/useSearchPresentation.d.ts +2 -1
  208. package/dist/search/useSearchPresentation.js +23 -5
  209. package/dist/server.d.ts +4 -4
  210. package/dist/server.js +4 -4
  211. package/dist/shapes/corners.d.ts +3 -3
  212. package/dist/shapes/corners.js +1 -2
  213. package/dist/styles.css +8289 -8244
  214. package/dist/sw/vite.d.ts +4 -0
  215. package/dist/sw/vite.js +2 -0
  216. package/dist/symbols/Symbol.d.ts +7 -5
  217. package/dist/symbols/Symbol.js +5 -3
  218. package/dist/symbols/catalogue.d.ts +1 -1
  219. package/dist/symbols/types.d.ts +6 -4
  220. package/dist/tabs/TabSection.d.ts +3 -2
  221. package/dist/tabs/TabSection.js +2 -1
  222. package/dist/tabs/TabView.js +54 -51
  223. package/dist/tasks/useTask.d.ts +5 -4
  224. package/dist/tasks/useTask.js +4 -3
  225. package/dist/testing.d.ts +3 -2
  226. package/dist/testing.js +3 -2
  227. package/dist/text-input/focusChain.d.ts +4 -3
  228. package/dist/text-input/focusChain.js +3 -2
  229. package/dist/text-input/kinds.d.ts +1 -1
  230. package/dist/web/WebView.d.ts +10 -3
  231. package/dist/web/WebView.js +3 -2
  232. package/dist/web/authentication.d.ts +12 -1
  233. package/dist/web/authentication.js +2 -1
  234. package/dist/web/openURL.d.ts +10 -4
  235. package/dist/web/openURL.js +4 -2
  236. package/dist/web/useWebAuthentication.d.ts +2 -1
  237. package/dist/web/useWebAuthentication.js +2 -1
  238. package/dist/zoom/Zoomable.d.ts +3 -3
  239. package/dist/zoom/Zoomable.js +2 -2
  240. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ # Using Tailwind with Satin
2
+
3
+ Add Tailwind v4 to a Satin starter:
4
+
5
+ ```sh
6
+ npm install -D tailwindcss@4 @tailwindcss/vite@4
7
+ ```
8
+
9
+ Keep `satinApp()` in `vite.config.ts` and add the Tailwind plugin:
10
+
11
+ ```ts
12
+ import react from '@vitejs/plugin-react'
13
+ import tailwindcss from '@tailwindcss/vite'
14
+ import { defineApp, satinApp } from '@satinhq/kit/vite'
15
+ import { defineConfig } from 'vite'
16
+ import { app } from './src/app'
17
+
18
+ export default defineConfig({
19
+ plugins: [react(), tailwindcss(), satinApp(defineApp(app))],
20
+ })
21
+ ```
22
+
23
+ Put this in your app's `src/styles.css`, which the starter already imports:
24
+
25
+ ```css
26
+ @layer theme, base, satin, components, utilities;
27
+ @import 'tailwindcss/theme.css' layer(theme);
28
+ @import 'tailwindcss/utilities.css' layer(utilities);
29
+
30
+ @custom-variant dark (&:where([data-satin-appearance='dark'], [data-satin-appearance='dark'] *));
31
+ ```
32
+
33
+ This follows Tailwind's [Preflight opt-out](https://tailwindcss.com/docs/preflight#disabling-preflight).
34
+ Importing all of `tailwindcss` also installs a global reset. Omit that reset:
35
+ Satin supplies control defaults, and Preflight changes plain headings, images
36
+ and other app content. Keep app resets out of the framework's document and controls.
37
+
38
+ `satinApp()` includes the framework stylesheet automatically, before app CSS.
39
+ Satin reserves the layer order shown above and puts its rules in `satin`.
40
+ Utilities therefore win over framework stylesheet declarations; ordinary unlayered
41
+ app CSS wins over both. Inline component styles and explicit component props
42
+ still take precedence over ordinary utility classes. Prefer the component's
43
+ props for control appearance, sizing and row geometry.
44
+
45
+ Use utilities on app content or components that accept `className`:
46
+
47
+ ```tsx
48
+ import { Button, ListRow, Text } from '@satinhq/kit'
49
+
50
+ export function Example() {
51
+ return (
52
+ <ListRow>
53
+ <Text className="text-fuchsia-600">A colored label</Text>
54
+ <Button className="text-fuchsia-600" onPress={() => {}}>Save</Button>
55
+ </ListRow>
56
+ )
57
+ }
58
+ ```
59
+
60
+ `ListRow` does not accept `className`. Use `listRowBackground`, `listRowInsets`
61
+ and `listRowSeparator`, or apply utilities to content inside the row.
62
+
63
+ The custom `dark` variant follows Satin's effective appearance, including an
64
+ in-app override. Tailwind's default media-query variant follows only the system.
65
+ For safe areas, continue using Satin containers and geometry tokens. Do not add
66
+ viewport-height, raw scrolling or `env(safe-area-inset-*)` utilities to the shell.
67
+
68
+ Run your app's typecheck, fidelity lint and build after adding classes.
69
+ Fidelity lint does not expand dependency imports or utility names, so a passing
70
+ lint cannot approve a reset or a geometry utility.
71
+ `satin check <url>` checks a deployment's files and headers; it does not inspect
72
+ the CSS cascade or certify the appearance of utility classes.
73
+
74
+ For a custom build integration that does not use `satinApp()`, the stylesheet
75
+ fallback is `import '@satinhq/kit/styles.css'` once, before app styles. Apps using
76
+ `satinApp()` need no framework CSS import; existing imports remain compatible.
@@ -1,6 +1,6 @@
1
1
  # Geometry and appearance tokens
2
2
 
3
- Load `@satinhq/kit/styles.css` once. Satin supplies these properties inside its mounted root and may override geometry in adaptive pane descendants. Consume the nearest value; do not copy root measurements into a sidebar or detail pane. Unknown custom-property names do not produce a TypeScript error and can leave a CSS declaration invalid.
3
+ `satinApp()` includes framework styles automatically. Satin supplies these properties inside its mounted root and may override geometry in adaptive pane descendants. Consume the nearest value; do not copy root measurements into a sidebar or detail pane. Unknown custom-property names do not produce a TypeScript error and can leave a CSS declaration invalid.
4
4
 
5
5
  | Property | Value | Use |
6
6
  | --- | --- | --- |
@@ -1,12 +1,14 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { Gesture } from './gesture.js';
3
+ /** The gestures a `GestureView` recognizes: a normal, a simultaneous and a high-priority gesture. */
3
4
  export type GestureViewProps = Readonly<{
4
5
  gesture?: Gesture;
5
6
  simultaneousGesture?: Gesture;
6
7
  highPriorityGesture?: Gesture;
7
8
  children: ReactNode;
8
9
  }>;
9
- /** A scoped gesture region. Scroll and navigation retain their own ownership.
10
+ /** A region that recognizes custom gestures on its children. Scrolling and the back swipe still
11
+ * win the touches they own.
10
12
  * @experimental Real-touch arbitration and native thresholds remain unqualified.
11
13
  */
12
14
  export declare function GestureView({ gesture, simultaneousGesture, highPriorityGesture, children, }: GestureViewProps): import("react").JSX.Element;
@@ -6,7 +6,8 @@ import { cancelPointerPressForOwnedGesture, observePointerPressCancellation, } f
6
6
  import { createGestureArena, failGesture, reduceGestureArena, } from './arena.js';
7
7
  import { interactivePopRecognizer, nativeScrollRecognizer } from './existingCompetitors.js';
8
8
  const regions = new WeakMap();
9
- /** A scoped gesture region. Scroll and navigation retain their own ownership.
9
+ /** A region that recognizes custom gestures on its children. Scrolling and the back swipe still
10
+ * win the touches they own.
10
11
  * @experimental Real-touch arbitration and native thresholds remain unqualified.
11
12
  */
12
13
  export function GestureView({ gesture, simultaneousGesture, highPriorityGesture, children, }) {
@@ -1,18 +1,23 @@
1
1
  import { type GestureDecision, type GestureRecognizerContext } from './arena.js';
2
+ /** A point in CSS pixels. */
2
3
  export type GesturePoint = Readonly<{
3
4
  x: number;
4
5
  y: number;
5
6
  }>;
7
+ /** A drag's current and start locations, its translation and its velocity. */
6
8
  export type DragGestureValue = Readonly<{
7
9
  location: GesturePoint;
8
10
  startLocation: GesturePoint;
9
11
  translation: GesturePoint;
10
12
  velocity: GesturePoint;
11
13
  }>;
14
+ /** A pinch's scale relative to its start, and the point it started around. */
12
15
  export type MagnifyGestureValue = Readonly<{
13
16
  magnification: number;
14
17
  startAnchor: GesturePoint;
15
18
  }>;
19
+ /** The binding `useGestureState` returns. A gesture updates it while active and resets it when it
20
+ * ends. */
16
21
  export type GestureState<Value> = Readonly<{
17
22
  update(value: Value): void;
18
23
  reset(): void;
@@ -27,6 +32,8 @@ export type GestureSample = Readonly<{
27
32
  drag: DragGestureValue;
28
33
  magnify: MagnifyGestureValue | undefined;
29
34
  }>;
35
+ /** A gesture made by `DragGesture`, `LongPressGesture`, `TapGesture` or `MagnifyGesture`, for a
36
+ * `GestureView`. */
30
37
  export type Gesture = Readonly<{
31
38
  kind: 'drag' | 'longPress' | 'tap' | 'magnify';
32
39
  axis: 'horizontal' | 'vertical' | 'any';
@@ -37,26 +44,39 @@ export type Gesture = Readonly<{
37
44
  ended(sample: GestureSample): void;
38
45
  cancelled(): void;
39
46
  }>;
40
- /** A React value plus a gesture-owned update/reset binding. Cancellation and release
41
- * restore the initial value; no mutable proxy is exposed to application code. */
47
+ /** Returns a value that a gesture drives, and the binding to pass as the gesture's `state`. The
48
+ * value goes back to `initialValue` when the gesture ends or is cancelled. */
42
49
  export declare function useGestureState<Value>(initialValue: Value): readonly [Value, GestureState<Value>];
50
+ /** A drag's callbacks and state, the distance it must travel before it starts (10 by default) and
51
+ * the axis it accepts. */
43
52
  export type DragGestureOptions = Callbacks<DragGestureValue> & Readonly<{
44
53
  minimumDistance?: number;
45
54
  axis?: Gesture['axis'];
46
55
  }>;
56
+ /** A drag that starts after the finger moves `minimumDistance` along the accepted axis. */
47
57
  export declare function DragGesture({ minimumDistance, axis, ...options }?: DragGestureOptions): Gesture;
58
+ /** A long press's callbacks and state, how long it must be held (500 ms by default) and how far
59
+ * the finger may move (10 by default). */
48
60
  export type LongPressGestureOptions = Callbacks<boolean> & Readonly<{
49
61
  minimumDurationMs?: number;
50
62
  maximumDistance?: number;
51
63
  }>;
64
+ /** A press held for `minimumDurationMs` without moving more than `maximumDistance`. */
52
65
  export declare function LongPressGesture({ minimumDurationMs, maximumDistance, ...options }?: LongPressGestureOptions): Gesture;
66
+ /** A tap's callbacks and state, the number of taps (1 by default) and how far the finger may
67
+ * move (10 by default). */
53
68
  export type TapGestureOptions = Callbacks<GesturePoint> & Readonly<{
54
69
  count?: number;
55
70
  maximumDistance?: number;
56
71
  }>;
72
+ /** A tap, or `count` taps, that ends without moving more than `maximumDistance`. It reports where
73
+ * the finger lifted. */
57
74
  export declare function TapGesture({ count, maximumDistance, ...options }?: TapGestureOptions): Gesture;
75
+ /** A pinch's callbacks and state, and how far the scale must change before it starts (0.01 by
76
+ * default). */
58
77
  export type MagnifyGestureOptions = Callbacks<MagnifyGestureValue> & Readonly<{
59
78
  minimumScaleDelta?: number;
60
79
  }>;
80
+ /** A two-finger pinch that starts once the scale changes by `minimumScaleDelta`. */
61
81
  export declare function MagnifyGesture({ minimumScaleDelta, ...options }?: MagnifyGestureOptions): Gesture;
62
82
  export {};
@@ -1,7 +1,7 @@
1
1
  import { useMemo, useRef, useState } from 'react';
2
2
  import { deferGesture, failGesture, recognizeGesture, } from './arena.js';
3
- /** A React value plus a gesture-owned update/reset binding. Cancellation and release
4
- * restore the initial value; no mutable proxy is exposed to application code. */
3
+ /** Returns a value that a gesture drives, and the binding to pass as the gesture's `state`. The
4
+ * value goes back to `initialValue` when the gesture ends or is cancelled. */
5
5
  export function useGestureState(initialValue) {
6
6
  const [value, setValue] = useState(initialValue);
7
7
  const initial = useRef(initialValue);
@@ -38,6 +38,7 @@ function distance(value, name) {
38
38
  throw new Error(`${name} must be finite and non-negative.`);
39
39
  return value;
40
40
  }
41
+ /** A drag that starts after the finger moves `minimumDistance` along the accepted axis. */
41
42
  export function DragGesture({ minimumDistance = 10, axis = 'any', ...options } = {}) {
42
43
  distance(minimumDistance, 'Drag minimumDistance');
43
44
  return Object.freeze({
@@ -56,6 +57,7 @@ export function DragGesture({ minimumDistance = 10, axis = 'any', ...options } =
56
57
  },
57
58
  });
58
59
  }
60
+ /** A press held for `minimumDurationMs` without moving more than `maximumDistance`. */
59
61
  export function LongPressGesture({ minimumDurationMs = 500, maximumDistance = 10, ...options } = {}) {
60
62
  distance(minimumDurationMs, 'Long press minimumDurationMs');
61
63
  distance(maximumDistance, 'Long press maximumDistance');
@@ -74,6 +76,8 @@ export function LongPressGesture({ minimumDurationMs = 500, maximumDistance = 10
74
76
  },
75
77
  });
76
78
  }
79
+ /** A tap, or `count` taps, that ends without moving more than `maximumDistance`. It reports where
80
+ * the finger lifted. */
77
81
  export function TapGesture({ count = 1, maximumDistance = 10, ...options } = {}) {
78
82
  if (!Number.isInteger(count) || count < 1)
79
83
  throw new Error('Tap count must be a positive integer.');
@@ -90,6 +94,7 @@ export function TapGesture({ count = 1, maximumDistance = 10, ...options } = {})
90
94
  },
91
95
  });
92
96
  }
97
+ /** A two-finger pinch that starts once the scale changes by `minimumScaleDelta`. */
93
98
  export function MagnifyGesture({ minimumScaleDelta = 0.01, ...options } = {}) {
94
99
  distance(minimumScaleDelta, 'Magnify minimumScaleDelta');
95
100
  return Object.freeze({
@@ -28,7 +28,8 @@ export type GlassEffectProps = HTMLAttributes<HTMLDivElement> & Readonly<{
28
28
  }>;
29
29
  /** Local content remains DOM-owned; only the material canvas is painted by reference. */
30
30
  export declare function GlassEffect({ glass, shape, touchLight, glassEffectID, namespace, union, transition, press, emerge, onEmergeSettled, onEmergeInk, children, style, onPointerDown, onPointerUp, onClick, ...props }: GlassEffectProps): import("react").JSX.Element;
31
- /** `GlassEffectContainer`: groups related glass outlines into one field. */
31
+ /** Groups nearby glass effects so they render together and blend or morph into one another.
32
+ * `spacing`, 20 pt by default, sets how close effects must be to merge. */
32
33
  export declare function GlassEffectContainer({ spacing, children, ...props }: HTMLAttributes<HTMLDivElement> & {
33
34
  spacing?: number;
34
35
  }): import("react").JSX.Element;
@@ -245,7 +245,8 @@ export function GlassEffect({ glass = Glass.regular, shape = 'capsule', touchLig
245
245
  justifyContent: style?.justifyContent,
246
246
  }, children: children })) : (children)] }));
247
247
  }
248
- /** `GlassEffectContainer`: groups related glass outlines into one field. */
248
+ /** Groups nearby glass effects so they render together and blend or morph into one another.
249
+ * `spacing`, 20 pt by default, sets how close effects must be to merge. */
249
250
  export function GlassEffectContainer({ spacing = 20, children, ...props }) {
250
251
  const id = useId(), element = useRef(null), canvas = useRef(null);
251
252
  const liquid = useContext(MaterialEffectContext);
@@ -14,6 +14,7 @@ export type GlassPresentationConsumer = Readonly<{
14
14
  intrinsicWidth?: boolean;
15
15
  placement?: 'bottom' | 'top';
16
16
  itemFrames?: readonly TabItemFrame[];
17
+ searchProgress?: number;
17
18
  items: readonly TabBarItem[];
18
19
  selected: string;
19
20
  onSelect: (id: string) => void;
@@ -182,6 +182,8 @@ export function GlassPresentation({ consumer }) {
182
182
  ? consumer.tabs.itemFrames
183
183
  : undefined;
184
184
  const tabBarPlacement = consumer?.tabs?.placement;
185
+ const searchProgress = tabBarPlacement === 'top' ? (consumer?.tabs?.searchProgress ?? 0) : 0;
186
+ const tabBarOpacity = 1 - searchProgress;
185
187
  const compactTabs = readDeviceIdiom() === 'phone' && width > height;
186
188
  const intrinsicTabs = Boolean(consumer?.tabs?.intrinsicWidth) && !compactTabs;
187
189
  const intrinsicGeometry = useMemo(() => (intrinsicTabs ? intrinsicTabGeometry(dockWidth, tabCount) : undefined), [intrinsicTabs, dockWidth, tabCount]);
@@ -232,8 +234,11 @@ export function GlassPresentation({ consumer }) {
232
234
  : 7;
233
235
  // The measured TabView material occupies the bottom safe-area band; its
234
236
  // glyphs remain above the Home indicator inside that material.
235
- const dockY = insetFrame?.y ??
236
- (isDragPage ? dragFrames.capsule.y : height - GLASS_DOCK_BOTTOM_GAP_PX - fallbackDockHeight);
237
+ const dockY = (insetFrame?.y ??
238
+ (isDragPage
239
+ ? dragFrames.capsule.y
240
+ : height - GLASS_DOCK_BOTTOM_GAP_PX - fallbackDockHeight)) -
241
+ 64 * searchProgress;
237
242
  const dockRadius = isDragPage
238
243
  ? dragFrames.capsule.height / 2
239
244
  : topFrames
@@ -276,6 +281,7 @@ export function GlassPresentation({ consumer }) {
276
281
  tabBarPillWidth,
277
282
  tabBarPillWidths,
278
283
  tabBarPlacement,
284
+ tabBarOpacity,
279
285
  tabBarPillHeight,
280
286
  tabBarPillCenterY,
281
287
  tabBarSelectedIndex: tabBarVisible ? selectedTab : undefined,
@@ -654,12 +660,13 @@ export function GlassPresentation({ consumer }) {
654
660
  // DOM, selected state and renderer-neutral region registrations mounted.
655
661
  // eslint-disable-next-line react-hooks/exhaustive-deps
656
662
  }, [consumer?.backend]);
657
- useEffect(() => {
663
+ const synchronizeGeometry = useCallback(() => {
658
664
  runtimeRef.current?.updateGeometry({
659
665
  tabBarCentres,
660
666
  tabBarPillWidth,
661
667
  tabBarPillWidths,
662
668
  tabBarPlacement,
669
+ tabBarOpacity,
663
670
  tabBarMinimizedOffsetX,
664
671
  tabBarPillHeight,
665
672
  tabBarPillCenterY,
@@ -683,6 +690,7 @@ export function GlassPresentation({ consumer }) {
683
690
  tabBarCentres,
684
691
  tabBarPillWidths,
685
692
  tabBarPlacement,
693
+ tabBarOpacity,
686
694
  tabBarMinimizedOffsetX,
687
695
  tabBarPillWidth,
688
696
  tabBarPillHeight,
@@ -703,6 +711,14 @@ export function GlassPresentation({ consumer }) {
703
711
  rightX,
704
712
  width,
705
713
  ]);
714
+ useLayoutEffect(() => {
715
+ if (readDeviceIdiom() === 'pad')
716
+ synchronizeGeometry();
717
+ }, [synchronizeGeometry]);
718
+ useEffect(() => {
719
+ if (readDeviceIdiom() !== 'pad')
720
+ synchronizeGeometry();
721
+ }, [synchronizeGeometry]);
706
722
  useEffect(() => runtimeRef.current?.setTabSelection(tabBarVisible ? selectedTab : undefined), [selectedTab, tabBarVisible]);
707
723
  useLayoutEffect(() => runtimeRef.current?.commitAppearance(appearance), [appearance]);
708
724
  // Settings and reference backgrounds are different DOM roots. Hand the newly
@@ -864,12 +880,13 @@ export function GlassPresentation({ consumer }) {
864
880
  height: liquidEnvelope.frameHeight,
865
881
  }
866
882
  : {
883
+ opacity: tabBarOpacity,
867
884
  borderRadius: dockRadius,
868
885
  left: dockX,
869
886
  top: dockY,
870
887
  width: dockWidth,
871
888
  height: dockHeight,
872
- }, "data-satin-tab-layout": topFrames ? 'top' : compactTabs ? 'compact' : undefined, role: liquidMotion !== 'none' ? 'presentation' : tabBarVisible ? 'tablist' : undefined, "aria-label": liquidMotion !== 'none'
889
+ }, inert: searchProgress > 0 || undefined, "aria-hidden": searchProgress > 0 || undefined, "data-satin-tab-layout": topFrames ? 'top' : compactTabs ? 'compact' : undefined, role: liquidMotion !== 'none' ? 'presentation' : tabBarVisible ? 'tablist' : undefined, "aria-label": liquidMotion !== 'none'
873
890
  ? `Liquid ${liquidMotion} surface`
874
891
  : tabBarVisible
875
892
  ? probe
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PadSearchBarMotion } from '../search/padSearchBarMotion.js';
3
+ import { PadSearchPresentationContext } from '../search/PadSearchPresentationContext.js';
2
4
  import { useControlStyle } from '../public/controlStyle.js';
3
5
  import { orderedTabItems, TopTabLabel } from '../tabs/presentation.js';
4
6
  import { handleTabKeyDown } from '../tabs/keyboard.js';
@@ -20,6 +22,8 @@ import { useGlassMaterialContext } from './context.js';
20
22
  import { resolveTabBarModel, TabBarAccessoryContext } from './tabBarModel.js';
21
23
  import {} from './tokens.js';
22
24
  export function TabBar({ className, adaptive = true, onShowSidebar, items: declaredItems, onSelect, selected, style, variant = 'regular', ...props }) {
25
+ const searchPresentation = useContext(PadSearchPresentationContext);
26
+ const searchProgress = searchPresentation?.owner === selected ? 1 : 0;
23
27
  const { tint } = useControlStyle();
24
28
  const liquid = useContext(MaterialEffectContext);
25
29
  const layoutClass = useLayoutClass();
@@ -37,6 +41,19 @@ export function TabBar({ className, adaptive = true, onShowSidebar, items: decla
37
41
  const { chromeLayer, registerRegion } = useGlassMaterialContext();
38
42
  const regionId = useId();
39
43
  const elementRef = useRef(null);
44
+ const padBarMotion = useRef(null);
45
+ const previousSearchProgress = useRef(searchProgress);
46
+ useLayoutEffect(() => {
47
+ if (!sidebar || liquidTop || previousSearchProgress.current === searchProgress)
48
+ return;
49
+ previousSearchProgress.current = searchProgress;
50
+ const element = elementRef.current;
51
+ if (!element)
52
+ return;
53
+ padBarMotion.current ??= new PadSearchBarMotion();
54
+ padBarMotion.current.start(element, searchProgress, undefined);
55
+ }, [sidebar, liquidTop, searchProgress]);
56
+ useLayoutEffect(() => () => padBarMotion.current?.dispose(), []);
40
57
  const hasAccessory = useContext(TabBarAccessoryContext);
41
58
  const compact = readDeviceIdiom() === 'phone' && metrics.layoutViewport.width > metrics.layoutViewport.height;
42
59
  const intrinsicWidth = readDeviceIdiom() === 'phone' && !compact && items.length < 4 && !hasAccessory;
@@ -88,6 +105,7 @@ export function TabBar({ className, adaptive = true, onShowSidebar, items: decla
88
105
  tint,
89
106
  insetHost: elementRef.current ?? undefined,
90
107
  placement: 'top',
108
+ searchProgress,
91
109
  itemFrames,
92
110
  });
93
111
  return () => liquid?.registerTabs(regionId, undefined);
@@ -102,6 +120,7 @@ export function TabBar({ className, adaptive = true, onShowSidebar, items: decla
102
120
  tint,
103
121
  liquidTop,
104
122
  itemFrames,
123
+ searchProgress,
105
124
  ]);
106
125
  useLayoutEffect(() => {
107
126
  if (sidebar)
@@ -152,12 +171,15 @@ export function TabBar({ className, adaptive = true, onShowSidebar, items: decla
152
171
  const tabBarStyle = {
153
172
  ...style,
154
173
  ...(sidebar ? sidebarCssTokens : {}),
174
+ ...(sidebar && searchProgress
175
+ ? { translate: `0 ${-64 * searchProgress}px`, opacity: 1 - searchProgress }
176
+ : {}),
155
177
  '--satin-tab-tint': tint,
156
178
  '--satin-tab-count': items.length,
157
179
  '--satin-tab-selection-offset': `${selectedIndex * 100}%`,
158
180
  '--satin-tab-selected-index': selectedIndex,
159
181
  };
160
- return createPortal(_jsxs("nav", { ...props, ref: elementRef, role: "tablist", onKeyDown: handleTabKeyDown, "aria-label": props['aria-label'] ?? 'App tabs', className: className, "data-satin-glass-region": sidebar ? undefined : regionId, "data-satin-tab-placement": sidebar ? 'top' : 'bottom', "data-satin-glass-variant": variant, "data-satin-role": "chrome", "data-satin-tab-bar": "true", style: tabBarStyle, children: [_jsx(GlassBackground, { glass: variant === 'clear' ? Glass.clear : Glass.regular }), _jsxs("div", { "data-satin-tab-items": "true", children: [onShowSidebar && (_jsx(Pressable, { "data-satin-tab-sidebar-show": "true", "aria-label": "Show Tab Sidebar", onPress: onShowSidebar, children: _jsx(SymbolImage, { name: "sidebar.left" }) })), items.map((item, index) => {
182
+ return createPortal(_jsxs("nav", { ...props, ref: elementRef, role: "tablist", inert: props.inert || (sidebar && searchProgress > 0) || undefined, "aria-hidden": props['aria-hidden'] || (sidebar && searchProgress > 0) || undefined, onKeyDown: handleTabKeyDown, "aria-label": props['aria-label'] ?? 'App tabs', className: className, "data-satin-glass-region": sidebar ? undefined : regionId, "data-satin-tab-placement": sidebar ? 'top' : 'bottom', "data-satin-glass-variant": variant, "data-satin-role": "chrome", "data-satin-tab-bar": "true", style: tabBarStyle, children: [_jsx(GlassBackground, { glass: variant === 'clear' ? Glass.clear : Glass.regular }), _jsxs("div", { "data-satin-tab-items": "true", children: [onShowSidebar && (_jsx(Pressable, { "data-satin-tab-sidebar-show": "true", "aria-label": "Show Tab Sidebar", onPress: onShowSidebar, children: _jsx(SymbolImage, { name: "sidebar.left" }) })), items.map((item, index) => {
161
183
  const isSelected = item.id === selected;
162
184
  return (_jsxs(Pressable, { role: "tab", id: item.tabId, "aria-controls": item.panelId, "aria-selected": isSelected, tabIndex: isSelected || (selectedIndex < 0 && index === 0) ? 0 : -1, "aria-label": item.accessibilityLabel ??
163
185
  (sidebar && item.role === 'search'
@@ -33,7 +33,8 @@ export function installGlassErrorAlert() {
33
33
  const errors = getGlassErrors().filter(error => !error.transient);
34
34
  if (!errors.length)
35
35
  return;
36
- if (document.getElementById('satin-launch-error')) {
36
+ const prelaunch = window.__SATIN_PRELAUNCH__;
37
+ if (prelaunch && ['preparing', 'failed'].includes(prelaunch.snapshot().state)) {
37
38
  document.querySelector(':root > pre[data-satin-glass-error]')?.remove();
38
39
  return;
39
40
  }
@@ -1,5 +1,7 @@
1
- /** Serializable across the renderer worker boundary. Release failures may select the fallback material. */
1
+ /** Why the glass renderer failed, such as `gpu-unavailable` or `device-lost`. In a release build a
2
+ * failure can switch glass to the fallback material. */
2
3
  export type GlassErrorCode = 'gpu-unavailable' | 'adapter-unavailable' | 'device-request' | 'context-configure' | 'device-lost' | 'validation' | 'out-of-memory' | 'shader-compilation' | 'worker-startup' | 'readiness-timeout' | 'presentation' | 'unsupported-configuration';
4
+ /** One glass renderer failure: its code, message, the stage it happened in and when. */
3
5
  export type GlassError = Readonly<{
4
6
  code: GlassErrorCode;
5
7
  message: string;
@@ -7,6 +7,7 @@ export declare class FallbackGlassRuntime {
7
7
  private regions;
8
8
  private dock;
9
9
  private thumb;
10
+ private padSearch?;
10
11
  private minimize;
11
12
  private minimizeDom;
12
13
  private tab;
@@ -1,3 +1,4 @@
1
+ import { PadSearchBarMotion } from '../../search/padSearchBarMotion.js';
1
2
  import { getGlassErrors } from '../errors.js';
2
3
  import { createRenderSpecs, } from '../renderSpecs.js';
3
4
  import { TabMotion, TAB_INTERACTIVE_MOTION } from '../lens/tabMotion.js';
@@ -13,6 +14,7 @@ export class FallbackGlassRuntime {
13
14
  regions;
14
15
  dock;
15
16
  thumb;
17
+ padSearch;
16
18
  minimize = new TabMinimizeIntent();
17
19
  minimizeDom;
18
20
  tab = new TabMotion(TAB_INTERACTIVE_MOTION);
@@ -93,7 +95,7 @@ export class FallbackGlassRuntime {
93
95
  dockSpec() {
94
96
  const spec = createRenderSpecs(this.options.geometry, this.options.appearance).find(spec => spec.id === 'dock');
95
97
  // Chrome's DOM host already owns the viewport placement.
96
- return { ...spec, frameX: 0, frameY: 0 };
98
+ return { ...spec, opacity: 1, frameX: 0, frameY: 0 };
97
99
  }
98
100
  minimizeTrack(direction, path = 'tap') {
99
101
  const track = this.minimizeDom.start(direction, this.dockSpec(), path);
@@ -131,9 +133,13 @@ export class FallbackGlassRuntime {
131
133
  this.thumb.paint(this.thumbSpec(selection), this.options.appearance);
132
134
  }
133
135
  updateGeometry(next) {
136
+ const previousOpacity = this.options.geometry.tabBarOpacity;
134
137
  const previousPlacement = this.options.geometry.tabBarPlacement;
135
138
  this.options = { ...this.options, geometry: { ...this.options.geometry, ...next } };
136
139
  const geometry = this.options.geometry;
140
+ if (previousPlacement === 'top' && geometry.tabBarOpacity !== undefined && previousOpacity !== geometry.tabBarOpacity) {
141
+ (this.padSearch ??= new PadSearchBarMotion()).start(this.options.elements.chrome.dock, 1 - geometry.tabBarOpacity, this.dockSpec());
142
+ }
137
143
  this.tab.configure(geometry.tabBarCentres, geometry.tabBarPillWidth, geometry.tabBarPillHeight, geometry.dockWidth, {
138
144
  selected: geometry.tabBarSelectedIndex ?? 0,
139
145
  centerY: geometry.tabBarPillCenterY,
@@ -274,6 +280,7 @@ export class FallbackGlassRuntime {
274
280
  this.dispose(owner);
275
281
  }
276
282
  dispose(_owner) {
283
+ this.padSearch?.dispose();
277
284
  if (this.disposed)
278
285
  return;
279
286
  this.disposed = true;
@@ -7,6 +7,8 @@ export type RegisteredTabBar = TabBarProps & Readonly<{
7
7
  /** 'top': the regular-width iPad bar, laid out by `itemFrames` inside `insetHost`. */
8
8
  placement?: 'bottom' | 'top';
9
9
  itemFrames?: readonly TabItemFrame[];
10
+ /** iPad search handoff; applied to material, foreground and controls together. */
11
+ searchProgress?: number;
10
12
  }>;
11
13
  export type MaterialEffectRegistration = Readonly<{
12
14
  /** The one glass renderer. */
@@ -2,6 +2,8 @@ import type { MenuEntry } from './menuTypes.js';
2
2
  import { type MenuContactSample } from '../menu/menuContact.js';
3
3
  import { type ReactNode } from 'react';
4
4
  import { type PressableProps } from '../press/Pressable.js';
5
+ /** The props a custom `Menu` trigger receives. Forward all of them to the element that opens the
6
+ * menu. */
5
7
  export type MenuTriggerProps = Pick<PressableProps, 'onPress' | 'disabled' | 'aria-expanded' | 'aria-haspopup' | 'aria-controls'>;
6
8
  export type MenuProps = Readonly<{
7
9
  label: ReactNode;
@@ -45,7 +45,8 @@ type GlassRect = Readonly<{
45
45
  width: number;
46
46
  height: number;
47
47
  }>;
48
- /** Normalized RGBA channels in 0...1. Glass.tintRGBA rejects nonfinite or out-of-range channels. */
48
+ /** A glass tint as red, green, blue and alpha channels from 0 to 1. `Glass.tintRGBA` rejects
49
+ * values outside that range. */
49
50
  export type GlassTint = readonly [red: number, green: number, blue: number, alpha: number];
50
51
  export type GlassMaterial = Readonly<{
51
52
  variant: 'regular' | 'clear';
@@ -202,6 +202,7 @@ export type RuntimeGeometry = Readonly<{
202
202
  tabBarPillWidths?: readonly number[];
203
203
  /** 'top' is the regular-width iPad tab bar; the phone dock is 'bottom' (the default). */
204
204
  tabBarPlacement?: 'bottom' | 'top';
205
+ tabBarOpacity?: number;
205
206
  tabBarPillHeight?: number;
206
207
  tabBarPillCenterY?: number;
207
208
  tabBarCentres?: readonly number[];
@@ -151,6 +151,7 @@ export function createRenderSpecs(geometry, appearance = 'light', liquidMotion =
151
151
  frameX: geometry.dockX,
152
152
  frameY: geometry.dockY,
153
153
  id: 'dock',
154
+ opacity: geometry.tabBarOpacity,
154
155
  radius: geometry.dockRadius,
155
156
  },
156
157
  {
@@ -0,0 +1,29 @@
1
+ /** Exterior rim measured from the iOS 27 stock 44 pt circle, at 3 device pixels/pt.
2
+ * See scripts/research/glass-blackroom/fit-rim.py and the round-2 evidence packet.
3
+ *
4
+ * The dark line is outside the material aperture. Its colour comes from the local
5
+ * backdrop, while the interior specular is a separate normal-dependent lobe.
6
+ * Both lateral normals have the same response; a signed horizontal term would
7
+ * introduce lighting that the native flat-background captures do not contain.
8
+ */
9
+ export declare function controlRimUniform(appearance: 'light' | 'dark'): [number, number, number, number];
10
+ /** Native dark regular glass changes with the short side, independently of corner radius.
11
+ * The 44/50/62/64/70/80/100 pt sweep measured white interior 185/185/185/185/175/159/128.
12
+ * Grey and 75% grey deltas follow backdrop squared. Limit extrapolation to the measured range.
13
+ */
14
+ export declare function controlSizeResponse(appearance: 'light' | 'dark', width: number, height: number): number;
15
+ /** White backdrop, bottom exterior at 2 pt: native 44/62 pt controls are 252,
16
+ * 80 pt controls are 248 light / 246 dark; the unscaled kernel is 245.
17
+ * Keep the broad shadow separate from the sub-point rim coverage.
18
+ */
19
+ export declare function controlShadowScale(appearance: 'light' | 'dark', width: number, height: number): number;
20
+ /** Native 44 pt sweep at background code levels 32/64/96/128: first exterior pixel 6/17/32/52. */
21
+ export declare const CONTROL_RIM_LOW_LUMA_GAIN = 0.6891894;
22
+ /** Joint least-squares fit of the unclipped black/grey/white cardinal profiles.
23
+ * Final amplitudes include the 1.02493656 contrast fit over 32 unclipped flat-background
24
+ * vertical rays after the measured size response. Photo cases were held out.
25
+ * Shared across appearances: the native colour matrix supplies their different contrast.
26
+ * Narrow support/amount, diffuse support/amount (all support distances in points).
27
+ */
28
+ export declare const CONTROL_SPECULAR: readonly [1.25, 1.5385214, 6.4, 0.38803352];
29
+ export declare const controlRimWGSL = "\nfn materialBoundary(distance: f32, normal: vec2<f32>, uv: vec2<f32>) -> vec4<f32> {\n let rim = global.uControlRim_slot;\n let angular = normal.x * normal.x + rim.w * normal.y * normal.y;\n let backdrop = materialMip(uv, 0.0);\n let luminance = dot(backdrop, vec3f(0.2126, 0.7152, 0.0722));\n let opacity = rim.y + 0.6891894 * max(0.5 - luminance, 0.0);\n let radial = opacity * clamp(1.0 - max(distance, 0.0) / rim.x, 0.0, 1.0);\n // Size sweep: exterior contrast changes linearly above mid-grey, while the face\n // changes quadratically. Normalizing by luminance expresses that contrast as coverage.\n let sizeContrast = global.uControlSize_slot.x * max(2.0 * luminance - 1.0, 0.0);\n let sizeCoverage = sizeContrast / max(0.6 * luminance, 0.001);\n let coverage = clamp(radial * angular * (1.0 + sizeCoverage), 0.0, 1.0);\n // Nine-level native sweep: light reflects a quadratic colour transform;\n // dark retains a small highlight in the upper half of the backdrop range.\n let lightColour = clamp(backdrop * (backdrop - vec3f(0.5)), vec3f(0.0), vec3f(1.0));\n let darkColour = 0.6 * max(backdrop - vec3f(0.5), vec3f(0.0)) * (vec3f(1.0) - backdrop);\n let colour = mix(lightColour, darkColour, rim.z);\n return vec4f(colour * coverage, coverage);\n}\nfn controlSpecular(distance: f32, normal: vec2<f32>) -> f32 {\n let profile = global.uControlSpecular_slot;\n let depth = max(-distance, 0.0);\n let narrow = profile.y * max(1.0 - depth / profile.x, 0.0);\n let diffuse = profile.w * max(1.0 - depth / profile.z, 0.0);\n let facing = clamp((abs(normal.y) - 0.16) / 0.84, 0.0, 1.0);\n return (narrow + diffuse) * facing * facing * facing;\n}\n";