@swiftbrowser/web 0.3.1 → 0.5.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.
@@ -0,0 +1,182 @@
1
+ /**
2
+ * What a page with a mouse adds to a phone's UI: `hover` events (`.onHover`),
3
+ * tooltips (`.help`), cursors (`.pointerStyle`), and SF Symbols' effects
4
+ * (`.symbolEffect`) drawn with CSS. The Swift shim draws an iPhone and sends
5
+ * none of these; producers in the page (@swiftbrowser/react) do.
6
+ */
7
+
8
+ import type { ElementId, Style, SymbolEffectStyle } from './protocol';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Hover
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const HOVER_ATTR = 'sbHover';
15
+
16
+ /** Sends `hover` as a mouse enters and leaves the boxes marked `data-sb-hover` under `root`. */
17
+ export class HoverTracker {
18
+ private readonly hovered = new Set<HTMLElement>();
19
+
20
+ constructor(
21
+ private readonly root: HTMLElement,
22
+ private readonly send: (id: ElementId, value: boolean) => void,
23
+ ) {
24
+ root.addEventListener('pointerover', this.onOver);
25
+ root.addEventListener('pointerout', this.onOut);
26
+ }
27
+
28
+ dispose(): void {
29
+ this.root.removeEventListener('pointerover', this.onOver);
30
+ this.root.removeEventListener('pointerout', this.onOut);
31
+ this.hovered.clear();
32
+ }
33
+
34
+ private readonly onOver = (event: PointerEvent): void => {
35
+ if (event.pointerType === 'touch') return;
36
+ for (let el = event.target as HTMLElement | null; el && el !== this.root; el = el.parentElement) {
37
+ if (el.dataset[HOVER_ATTR] === undefined || this.hovered.has(el)) continue;
38
+ this.hovered.add(el);
39
+ this.report(el, true);
40
+ }
41
+ };
42
+
43
+ private readonly onOut = (event: PointerEvent): void => {
44
+ if (event.pointerType === 'touch') return;
45
+ const next = event.relatedTarget as Node | null;
46
+ for (const el of [...this.hovered]) {
47
+ if (next && el.contains(next)) continue;
48
+ this.hovered.delete(el);
49
+ this.report(el, false);
50
+ }
51
+ };
52
+
53
+ private report(el: HTMLElement, value: boolean): void {
54
+ const id = Number(el.dataset['sbId']);
55
+ if (Number.isInteger(id) && el.isConnected) this.send(id, value);
56
+ }
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Style fields
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /** One run of each effect at speed 1 (seconds) and its keyframes on the CSS `scale`, `rotate`, `translate` and `opacity` properties. */
64
+ const EFFECTS: Record<string, { duration: number; frames: Keyframe[] }> = {
65
+ bounce: {
66
+ duration: 0.5,
67
+ frames: [
68
+ { scale: '1', translate: '0 0', offset: 0 },
69
+ { scale: '1.18', translate: '0 -12%', offset: 0.35 },
70
+ { scale: '0.94', translate: '0 0', offset: 0.7 },
71
+ { scale: '1', translate: '0 0', offset: 1 },
72
+ ],
73
+ },
74
+ pulse: { duration: 1.2, frames: [{ opacity: '1' }, { opacity: '0.35' }, { opacity: '1' }] },
75
+ variableColor: { duration: 1.2, frames: [{ opacity: '1' }, { opacity: '0.5', offset: 0.33 }, { opacity: '0.75', offset: 0.66 }, { opacity: '1' }] },
76
+ rotate: { duration: 1, frames: [{ rotate: '0deg' }, { rotate: '360deg' }] },
77
+ wiggle: {
78
+ duration: 0.6,
79
+ frames: [{ rotate: '0deg' }, { rotate: '-14deg', offset: 0.25 }, { rotate: '14deg', offset: 0.6 }, { rotate: '0deg' }],
80
+ },
81
+ breathe: { duration: 2, frames: [{ scale: '1', opacity: '1' }, { scale: '1.1', opacity: '0.7' }, { scale: '1', opacity: '1' }] },
82
+ };
83
+
84
+ const playing = new WeakMap<HTMLElement, { key: string; animation: globalThis.Animation }>();
85
+ const triggers = new WeakMap<HTMLElement, number>();
86
+
87
+ /** Apply the pointer and effect fields of a `styled` element's style (called on create and update). */
88
+ export function applyPointerAndEffects(el: HTMLElement, style: Style): void {
89
+ if (style.hover) el.dataset[HOVER_ATTR] = '';
90
+ if (style.help) {
91
+ el.title = style.help;
92
+ el.dataset['sbHelp'] = '';
93
+ } else if (el.hasAttribute('title') && el.classList.contains('sb-styled')) {
94
+ el.removeAttribute('title');
95
+ }
96
+ if (style.pointerStyle) el.style.cursor = style.pointerStyle;
97
+ applySymbolEffect(el, style.symbolEffect);
98
+ if (style.matchedGeometry) el.dataset['sbMatched'] = style.matchedGeometry;
99
+ if (style.contentTransition) {
100
+ el.dataset['sbContentTransition'] = style.contentTransition.kind;
101
+ const { countsDown } = style.contentTransition;
102
+ if (countsDown === undefined) delete el.dataset['sbCountsDown'];
103
+ else el.dataset['sbCountsDown'] = String(countsDown);
104
+ }
105
+ }
106
+
107
+ function applySymbolEffect(el: HTMLElement, effect: SymbolEffectStyle | undefined): void {
108
+ const s = el.style;
109
+ if (!effect) {
110
+ stopEffect(el);
111
+ return;
112
+ }
113
+ el.dataset['sbSymbolEffect'] = effect.effect;
114
+ // State effects: a look kept while the style carries them.
115
+ if (effect.effect === 'scale') {
116
+ s.transition = 'scale 0.25s';
117
+ s.scale = effect.direction === 'down' ? '0.85' : '1.2';
118
+ return;
119
+ }
120
+ if (effect.effect === 'appear' || effect.effect === 'disappear') {
121
+ s.transition = 'opacity 0.25s, scale 0.25s';
122
+ const shown = effect.effect === 'appear';
123
+ s.opacity = shown ? '1' : '0';
124
+ s.scale = shown ? '1' : '0.6';
125
+ return;
126
+ }
127
+ const spec = EFFECTS[effect.effect];
128
+ if (!spec || typeof el.animate !== 'function') return;
129
+ const duration = (spec.duration / Math.max(effect.speed ?? 1, 0.01)) * 1000;
130
+ const repeat = effect.repeat === 'forever' ? Infinity : Math.max(1, effect.repeat ?? 1);
131
+ if (effect.trigger !== undefined) {
132
+ // A discrete effect plays once per new trigger (never for the first value).
133
+ const last = triggers.get(el);
134
+ triggers.set(el, effect.trigger);
135
+ if (last === undefined || last === effect.trigger) return;
136
+ playing.get(el)?.animation.cancel();
137
+ playing.set(el, { key: `once:${effect.trigger}`, animation: el.animate(spec.frames, { duration, iterations: repeat, easing: 'ease-in-out' }) });
138
+ return;
139
+ }
140
+ // An indefinite effect keeps running across updates that leave it alone.
141
+ const key = `${effect.effect}:${duration}:${repeat === Infinity ? 'forever' : repeat}`;
142
+ const current = playing.get(el);
143
+ if (current && current.key === key && current.animation.playState !== 'finished') return;
144
+ current?.animation.cancel();
145
+ const iterations = effect.repeat === undefined ? Infinity : repeat;
146
+ playing.set(el, { key, animation: el.animate(spec.frames, { duration, iterations, easing: 'ease-in-out' }) });
147
+ }
148
+
149
+ function stopEffect(el: HTMLElement): void {
150
+ const current = playing.get(el);
151
+ if (current && !current.key.startsWith('once:')) {
152
+ current.animation.cancel();
153
+ playing.delete(el);
154
+ }
155
+ triggers.delete(el);
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Geometry readers that report their frame
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export interface SafeAreaInsets {
163
+ top: number;
164
+ leading: number;
165
+ bottom: number;
166
+ trailing: number;
167
+ }
168
+
169
+ /** How far the device's safe area reaches into a box at `rect` in a root of `size`. */
170
+ export function safeAreaOverlap(
171
+ rect: { x: number; y: number; width: number; height: number },
172
+ size: { width: number; height: number },
173
+ safeArea: SafeAreaInsets,
174
+ ): SafeAreaInsets {
175
+ const clamp = (value: number, max: number): number => Math.round(Math.min(Math.max(value, 0), max));
176
+ return {
177
+ top: clamp(safeArea.top - rect.y, rect.height),
178
+ bottom: clamp(rect.y + rect.height - (size.height - safeArea.bottom), rect.height),
179
+ leading: clamp(safeArea.leading - rect.x, rect.width),
180
+ trailing: clamp(rect.x + rect.width - (size.width - safeArea.trailing), rect.width),
181
+ };
182
+ }
package/src/protocol.ts CHANGED
@@ -719,7 +719,15 @@ export interface PickerProps {
719
719
  * `GeometryReader`: fills its proposal and reports its size through a
720
720
  * `geometry` event whenever it changes. Exactly one child.
721
721
  */
722
- export type GeometryProps = Record<string, never>;
722
+ export interface GeometryProps {
723
+ /**
724
+ * Report the reader's frame on screen too (`x`, `y` and `safeArea` in its
725
+ * `geometry` events, kept current as its ancestors scroll), for
726
+ * `GeometryProxy.frame(in: .global)` and `safeAreaInsets`. Absent: the size
727
+ * only, as before.
728
+ */
729
+ reportsFrame?: boolean;
730
+ }
723
731
 
724
732
  // Phase 6 kinds (docs/phase6/layout-containers.md).
725
733
 
@@ -795,6 +803,16 @@ export interface LazyVGridProps {
795
803
  spacing: number | null;
796
804
  alignment: FrameAlignment;
797
805
  }
806
+ /**
807
+ * `LazyHGrid`: `LazyVGrid` turned on its side. The `rows` share the height
808
+ * the grid is proposed; the cells fill them column by column, `spacing`
809
+ * (default 8) apart. Children are the cells (flattened). Nothing is lazy.
810
+ */
811
+ export interface LazyHGridProps {
812
+ rows: GridItem[];
813
+ spacing: number | null;
814
+ alignment: FrameAlignment;
815
+ }
798
816
  export type GaugeStyle =
799
817
  | 'automatic'
800
818
  | 'linearCapacity'
@@ -851,6 +869,12 @@ export type ToolbarPlacement = 'trailing' | 'leading' | 'principal' | 'bottom';
851
869
  */
852
870
  export interface ToolbarProps {
853
871
  placement: ToolbarPlacement;
872
+ /**
873
+ * A `ToolbarSpacer` (no children): it separates glass groups, and in a
874
+ * bottom bar flexible ones share the room left over and push the groups
875
+ * apart; fixed ones are 8 wide.
876
+ */
877
+ spacer?: 'flexible' | 'fixed';
854
878
  }
855
879
  /**
856
880
  * `.searchable(text:)`, an extra child of a `navscreen`: a 36pt rounded
@@ -870,6 +894,19 @@ export interface TapGestureProps {
870
894
  location?: boolean;
871
895
  }
872
896
 
897
+ /**
898
+ * A box the page fills with its own DOM (src/hostContent.ts): `content` is
899
+ * the key the page registered its element under. `sizing: "fill"` takes the
900
+ * proposal, the content's own size on an unconstrained axis; `"content"`
901
+ * takes the content's size, its width capped by the proposal and its height
902
+ * measured at that width, like text. Only page-side producers (such as
903
+ * @swiftbrowser/react) emit it; the Swift core never does.
904
+ */
905
+ export interface HostProps {
906
+ content: string;
907
+ sizing: 'fill' | 'content';
908
+ }
909
+
873
910
  export interface ElementPropsByKind {
874
911
  text: TextProps;
875
912
  vstack: VStackProps;
@@ -899,12 +936,14 @@ export interface ElementPropsByKind {
899
936
  grid: GridProps;
900
937
  gridrow: GridRowProps;
901
938
  lazyvgrid: LazyVGridProps;
939
+ lazyhgrid: LazyHGridProps;
902
940
  gauge: GaugeProps;
903
941
  decorated: DecoratedProps;
904
942
  menu: MenuProps;
905
943
  toolbar: ToolbarProps;
906
944
  searchfield: SearchFieldProps;
907
945
  tapgesture: TapGestureProps;
946
+ host: HostProps;
908
947
  }
909
948
 
910
949
  export type ElementKind = keyof ElementPropsByKind;
@@ -1289,6 +1328,16 @@ export interface GeometryEvent {
1289
1328
  id: ElementId;
1290
1329
  width: number;
1291
1330
  height: number;
1331
+ /** With `reportsFrame`: the box's origin in the root's coordinates, scrolling included (whole px). */
1332
+ x?: number;
1333
+ y?: number;
1334
+ /** With `reportsFrame`: how far the device's safe area reaches into the box on each edge. */
1335
+ safeArea?: { top: number; leading: number; bottom: number; trailing: number };
1336
+ /**
1337
+ * With `reportsFrame`, inside a scroll view or list: its visible rectangle
1338
+ * in the root's coordinates (whole px) and the axis it scrolls along.
1339
+ */
1340
+ scrollView?: { x: number; y: number; width: number; height: number; axis: 'horizontal' | 'vertical' };
1292
1341
  }
1293
1342
 
1294
1343
  /** Host measurements of one subview of a `layout` element (points, 2 decimals; `"infinity"` for an unbounded axis). */
@@ -1333,7 +1382,8 @@ export type RendererEvent =
1333
1382
  // A `video` element's playback (src/video.ts)
1334
1383
  | VideoEvent
1335
1384
  | LazyEvent
1336
- | MenuEvent;
1385
+ | MenuEvent
1386
+ | HoverEvent;
1337
1387
 
1338
1388
  /**
1339
1389
  * Phase 13: the page's answer to the `request` op with this `id`: `value`
@@ -1451,7 +1501,10 @@ export function isOp(value: unknown): value is Op {
1451
1501
  // ---------------------------------------------------------------------------
1452
1502
 
1453
1503
  /** `NavigationSplitView` in the regular size class: children are `[sidebar, detail]`, each a `navstack`. */
1454
- export type NavSplitProps = Record<string, never>;
1504
+ export interface NavSplitProps {
1505
+ /** iPadOS: false hides the sidebar (`NavigationSplitViewVisibility.detailOnly`); absent or true shows it. */
1506
+ sidebarVisible?: boolean;
1507
+ }
1455
1508
 
1456
1509
  export type ViewThatFitsAxes = 'horizontal' | 'vertical' | 'both';
1457
1510
  /**
@@ -1466,6 +1519,17 @@ export interface ViewThatFitsProps {
1466
1519
  export interface ElementPropsByKind {
1467
1520
  navsplit: NavSplitProps;
1468
1521
  viewthatfits: ViewThatFitsProps;
1522
+ inspector: InspectorProps;
1523
+ inspectorpanel: Record<string, never>;
1524
+ }
1525
+
1526
+ /**
1527
+ * `.inspector(isPresented:)` in the regular size class: the children are the
1528
+ * content, then an `inspectorpanel` (the trailing column's content).
1529
+ */
1530
+ export interface InspectorProps {
1531
+ /** Whether the panel is shown; false slides it off the trailing edge. */
1532
+ presented: boolean;
1469
1533
  }
1470
1534
 
1471
1535
  export interface Style {
@@ -1688,14 +1752,40 @@ export interface ContextMenuProps {
1688
1752
  }
1689
1753
 
1690
1754
  /**
1691
- * A `List` row that can be selected (`selectable`, `selected`) or deleted
1692
- * (`deletable`, from `ForEach.onDelete`). A tap on a selectable row sends
1693
- * `tap` with the row id; swipe-to-delete or the edit-mode minus sends `delete`.
1755
+ * A `List` row that can be selected (`selectable`, `selected`), deleted
1756
+ * (`deletable`, from `ForEach.onDelete`) or moved (`movable`, from
1757
+ * `ForEach.onMove`; absent means false). A tap on a selectable row sends
1758
+ * `tap` with the row id; swipe-to-delete or the edit-mode minus sends
1759
+ * `delete`. A movable row shows a grip at its trailing edge while the list
1760
+ * is editing, and a long press lifts it otherwise; dragging it among its
1761
+ * neighbours and letting go sends `move` (src/dragDrop.ts).
1694
1762
  */
1695
1763
  export interface ListRowProps {
1696
1764
  selectable?: boolean;
1697
1765
  selected?: boolean;
1698
1766
  deletable?: boolean;
1767
+ movable?: boolean;
1768
+ }
1769
+
1770
+ /**
1771
+ * `.draggable` / `.onDrag`: exactly one child, the child's size. A long
1772
+ * press (touch) or a 10 px drag (mouse) lifts a copy of the content, which
1773
+ * follows the pointer; `dragstart` goes out as it lifts and `dragend` once
1774
+ * it is dropped or let go (src/dragDrop.ts).
1775
+ */
1776
+ export type DraggableProps = Record<string, never>;
1777
+
1778
+ /**
1779
+ * `.dropDestination` / `.onDrop`: exactly one child, the child's size. A
1780
+ * lifted `draggable` over it sends `drop` with the phase `entered` /
1781
+ * `exited`, and `performed` with the source when released over it. Text,
1782
+ * links and files dragged in from outside the page (the browser's own drag
1783
+ * and drop) are offered when their MIME type matches one of `accepts`
1784
+ * (`text/plain`, `image/*`, or a star over a star for anything); they
1785
+ * arrive as `items`.
1786
+ */
1787
+ export interface DropDestinationProps {
1788
+ accepts?: string[];
1699
1789
  }
1700
1790
 
1701
1791
  export interface ElementPropsByKind {
@@ -1703,12 +1793,14 @@ export interface ElementPropsByKind {
1703
1793
  swipeactions: SwipeActionsProps;
1704
1794
  contextmenu: ContextMenuProps;
1705
1795
  listrow: ListRowProps;
1796
+ draggable: DraggableProps;
1797
+ dropdestination: DropDestinationProps;
1706
1798
  }
1707
1799
 
1708
- export const INTERACTION_KINDS: readonly ElementKind[] = ['gesture', 'swipeactions', 'contextmenu', 'listrow'];
1800
+ export const INTERACTION_KINDS: readonly ElementKind[] = ['gesture', 'swipeactions', 'contextmenu', 'listrow', 'draggable', 'dropdestination'];
1709
1801
 
1710
1802
  export interface ListProps {
1711
- /** `EditButton` / `editMode`: every `listrow` gains a 44 pt leading control and its content shifts right 44. */
1803
+ /** `EditButton` / `editMode`: a selectable or deletable `listrow` gains a 44 pt leading control and its content shifts right 40; a movable one a trailing grip. */
1712
1804
  editing?: boolean;
1713
1805
  /** `.refreshable`: a pull past 60 pt at the top sends `refresh`. */
1714
1806
  refreshable?: boolean;
@@ -1772,9 +1864,52 @@ export interface RefreshEvent {
1772
1864
  type: 'refresh';
1773
1865
  id: ElementId;
1774
1866
  }
1867
+ /**
1868
+ * A movable `listrow` was dragged to a new place: `to` is the index among
1869
+ * its container's children (the `list`'s, or its `section`'s) before which
1870
+ * it goes, counted before it leaves (0 ... the child count), as
1871
+ * `move(fromOffsets:toOffset:)` takes its destination.
1872
+ */
1873
+ export interface MoveEvent {
1874
+ type: 'move';
1875
+ id: ElementId;
1876
+ to: number;
1877
+ }
1878
+ /** A `draggable` was lifted: its payload is made now and kept until `dragend`. */
1879
+ export interface DragStartEvent {
1880
+ type: 'dragstart';
1881
+ id: ElementId;
1882
+ }
1883
+ /** The lifted `draggable` was dropped or let go. */
1884
+ export interface DragEndEvent {
1885
+ type: 'dragend';
1886
+ id: ElementId;
1887
+ }
1888
+ /** One thing dragged in from outside the page: text (or a link, `text/uri-list`), or a file's bytes in base64. */
1889
+ export interface DropItem {
1890
+ type: string;
1891
+ text?: string;
1892
+ name?: string;
1893
+ data?: string;
1894
+ }
1895
+ /**
1896
+ * A drag over a `dropdestination`: it `entered`, `exited`, or `performed`
1897
+ * (was released over it). `source` is the lifted `draggable`'s id for a drag
1898
+ * that started in the app; `items` what came from outside the page. The
1899
+ * point is in the destination's own coordinates (0, 0 while entering and leaving).
1900
+ */
1901
+ export interface DropEvent {
1902
+ type: 'drop';
1903
+ id: ElementId;
1904
+ phase: 'entered' | 'exited' | 'performed';
1905
+ source?: ElementId;
1906
+ items?: DropItem[];
1907
+ x: number;
1908
+ y: number;
1909
+ }
1775
1910
 
1776
1911
  /** The Phase 8 events (merged into `RendererEvent` above). */
1777
- export type InteractionEvent = DragEvent | LongPressEvent | DeleteEvent | RefreshEvent;
1912
+ export type InteractionEvent = DragEvent | LongPressEvent | DeleteEvent | RefreshEvent | MoveEvent | DragStartEvent | DragEndEvent | DropEvent;
1778
1913
  // Phase 8: alerts, controls, effects
1779
1914
  // (docs/ops-protocol.md, "Phase 8: interaction"; gestures, swipe actions,
1780
1915
  // context menus, list rows and refresh live in their own block)
@@ -1970,11 +2105,24 @@ export interface ListProps {
1970
2105
  background?: 'hidden';
1971
2106
  /** `.scrollDismissesKeyboard(.immediately | .interactively)`: scrolling blurs the text input being edited inside. */
1972
2107
  dismissesKeyboard?: boolean;
2108
+ /**
2109
+ * `.scrollEdgeEffectStyle(_:for:)` / `.scrollEdgeEffectHidden(_:for:)`: the
2110
+ * scroll edge effect where the list runs under its screen's bar (`top`) or
2111
+ * a bar covering the bottom (`bottom`); an absent edge is automatic (soft).
2112
+ */
2113
+ edgeEffect?: EdgeEffectProps;
2114
+ }
2115
+
2116
+ export interface EdgeEffectProps {
2117
+ top?: 'hard' | 'soft' | 'hidden';
2118
+ bottom?: 'hard' | 'soft' | 'hidden';
1973
2119
  }
1974
2120
 
1975
2121
  export interface ScrollViewProps {
1976
2122
  /** As for `list`. */
1977
2123
  dismissesKeyboard?: boolean;
2124
+ /** As for `list`. */
2125
+ edgeEffect?: EdgeEffectProps;
1978
2126
  }
1979
2127
 
1980
2128
  /**
@@ -1992,6 +2140,68 @@ export interface ElementPropsByKind {
1992
2140
  }
1993
2141
 
1994
2142
  /** `ScrollViewProxy.scrollTo(_:anchor:)`: where the target should end up (null: as little scrolling as shows it). */
2143
+ /**
2144
+ * SF Symbols' effects (`.symbolEffect`), drawn with CSS on the styled box:
2145
+ * `bounce`, `pulse`, `variableColor` (as a pulse), `scale` (up while
2146
+ * active; `down` shrinks), `rotate`, `wiggle`, `breathe`, `appear` /
2147
+ * `disappear` (shown / hidden while active). Without `trigger` the effect
2148
+ * runs while the style carries it (`repeat` times, or forever); with one, it
2149
+ * plays once (`repeat` times) whenever `trigger` changes, like
2150
+ * `.symbolEffect(_:value:)`.
2151
+ */
2152
+ export interface SymbolEffectStyle {
2153
+ effect: 'bounce' | 'pulse' | 'variableColor' | 'scale' | 'rotate' | 'wiggle' | 'breathe' | 'appear' | 'disappear';
2154
+ direction?: 'up' | 'down';
2155
+ /** Seconds per run at speed 1 is the effect's own; `speed` divides it. */
2156
+ speed?: number;
2157
+ repeat?: number | 'forever';
2158
+ trigger?: number;
2159
+ }
2160
+
2161
+ export interface Style {
2162
+ /** The page sends `hover` events as a mouse enters and leaves the box (`.onHover`). */
2163
+ hover?: boolean;
2164
+ /** `.help(_:)`: a tooltip on the box. */
2165
+ help?: string;
2166
+ /** `.pointerStyle(_:)`: the CSS cursor over the box (`pointer`, `text`, `grab`…). */
2167
+ pointerStyle?: string;
2168
+ symbolEffect?: SymbolEffectStyle;
2169
+ /**
2170
+ * `.matchedGeometryEffect(id:in:)`: when an animated pass removes a box
2171
+ * with this id and inserts another with it, the inserted one starts from
2172
+ * the removed one's frame and the removed one's exit travels to the new
2173
+ * frame. The namespace is part of the id.
2174
+ */
2175
+ matchedGeometry?: string;
2176
+ /**
2177
+ * `.contentTransition(_:)`: how a text under the box changes in a pass that
2178
+ * animates. `numericText` rolls the old text out upward and the new one in
2179
+ * from below, or the other way when the new text reads as a smaller number
2180
+ * (`countsDown` fixes the direction); `opacity` and `interpolate` crossfade
2181
+ * them.
2182
+ */
2183
+ contentTransition?: { kind: 'numericText' | 'opacity' | 'interpolate'; countsDown?: boolean };
2184
+ }
2185
+
2186
+ export interface Style {
2187
+ /**
2188
+ * `.containerRelativeFrame(_:…)`: on the marked axes the box is
2189
+ * `a × container + b` long, the container being the nearest scroll view's
2190
+ * visible area (or the screen), and its content is aligned in that frame.
2191
+ */
2192
+ containerRelativeFrame?: { horizontal?: boolean; vertical?: boolean; a: number; b: number; alignment: FrameAlignment };
2193
+ }
2194
+
2195
+ /**
2196
+ * A mouse (or pen) entered (`value: true`) or left a box whose style has
2197
+ * `hover`. Touch never hovers.
2198
+ */
2199
+ export interface HoverEvent {
2200
+ type: 'hover';
2201
+ id: ElementId;
2202
+ value: boolean;
2203
+ }
2204
+
1995
2205
  export interface ScrollToCommand {
1996
2206
  anchor: UnitPoint | null;
1997
2207
  /** Increases with every request (across readers); the page carries out each serial once. */