@synopackageland/ui 0.23.4 → 0.23.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.23.6 (2026-11-07)
6
+
7
+ Phone fixes for a fill page whose scroller is an App's own element: it is no longer cut off above the tab bar in portrait (a 0.23.5 regression), its header's "…" no longer pans it sideways in landscape, and a sticky row in it sticks below its PageHeader. No App change is required; a DSM window and desktop are unchanged.
8
+
9
+ - **Fixed (regression in 0.23.5): a fill page's nested scroller stopped about 110px above the tab bar in portrait.** PageContent read `--ui-page-end-space` with `parseFloat` when it looked for a fill page's scroller. Under docked bars (0.23.5's `data-ui-bars-overlay`) the property's text is a `calc()` expression (`calc(24px + calc(45px + max(49px, 0px) + 0px))`), which parsed as 0, so an App scroller nested in the page with the end space on a wrapper around it was not found as the page's scroller: the end space (118px at 390x844) stayed under it and the FAB's room (88px) and the bars' room went under the page instead of at the end of its content. PageContent now reads the resolved length from a hidden probe (`display: none`, `margin-top: var(--ui-page-end-space)`). Landscape was not affected (a plain `24px`). The other `parseFloat`/`parseInt` calls in the package read computed lengths or the UI's own pixel tokens; none parses a `calc()` custom property
10
+ - **A PageHeader's last action no longer pans an App's nested scroller sideways.** In a fill page whose scroller is the App's own element holding the PageHeader, the 44px tap box of a trailing "…" IconButton reached 13px past the scroller's edge (590/578 at 825x360, 518/506 at 753x279); 0.23.5's clip covered only a PageContent that is itself the scroller. The last action in a PageHeader's actions now sets `--ui-hit-end-room` to the header's end padding: its box stops at the header's edge and grows towards its start side, still 44px. The scroller is not clipped, so an App scroller that also scrolls a table sideways is unchanged
11
+ - **An Accordion no longer pans an App's own scroller sideways.** Its trigger was 8px wider than the accordion (`-mx-1`, `width: calc(100% + 0.5rem)`) so its pressed wash reached past the text; flush with the edge of a scroller without a PageContent clip (an App's record pane) that scroller scrolled 4px sideways at every width. The trigger is now exactly as wide as the accordion, its text where it was, and the pressed wash reaches 4px past each side by `box-shadow`, which no scroller counts
12
+ - **A sticky row in a nested scroller sticks below a sticky PageHeader in that scroller.** A `data-ui-sticky` row (an audio player) in an App's own scroller stacked from that scroller's top and covered a sticky PageHeader in the same scroller (title, Share, "…") once the page scrolled. A PageHeader now reports its element with its height, and PageContent places each sticky header and tabs strip in the scroller it sticks in: in a nested scroller its height is where the scroller's rows start, it counts in that scroller's `--ui-sticky-top-offset` and sets that scroller's `--ui-page-sticky-top`, and it is left out of the page's offset
13
+ - docs/mobile-native.md: the phone behaviour contract, the hit-area, sticky-row and end-space sections describe the nested scroller cases
14
+ - `check:phone-layout`: `search-nested` and `search-nested-end` (a fill page whose scroller is the App's own pane holding the PageHeader with a trailing "…", the App's end space on a wrapper around it) check that the pane is the page's scroller, reaches the screen's bottom with no end space under it, holds the FAB's and the bars' room, and with the bars kept ends its last row above the FAB (a speed dial's + when the page has two actions); `record-player-nested` checks the player sticks right below the PageHeader in the App's pane, counts in its offset and `scrollToStart` lands below it. The side-pan check runs both at portrait and the two landscape sizes
15
+
16
+ ## 0.23.5 (2026-11-07)
17
+
18
+ Phone fixes: a fling keeps its momentum while the bars hide, the page never pans sideways (also from a tap box at its edge), and the FAB is a circle. Gesture decisions share one velocity estimator, and a sticky row in an App's own side pane sticks in that pane. No App change is required; a DSM window and desktop are unchanged.
19
+
20
+ - **Hide on scroll no longer stops a fling on iOS.** Hiding or showing the bars changed the page's layout mid-scroll: the phone bar collapsed by a negative margin, the tab bar's room and the page's end space changed, and in an App shell whose content area scrolls itself the column's bottom padding animated. Each change resized or moved the scroller, and iOS Safari ends a momentum scroll when that happens. The bars now move by `transform` only, and the scroller is sized once. When the page scrolls in an area under the bar (a fill layout, or an App shell like the SDK's AppShell), the layout is marked `data-ui-bars-overlay`: the content runs under the bar, the tab bar and the accessory from the start, and a constant spacer at the end of what scrolls (`--ui-bars-room`) keeps the last row clear of them. Hiding translates the bar, the content area and the tab bar; when the document scrolls, only the tab bar, the accessory and the FAB slide. `--ui-page-end-space` no longer changes while the bars are hidden. While the content area is translated it is the containing block of a `position: fixed` element inside it (the UI's overlays render outside it)
21
+ - **Hide on scroll reads the scroll's velocity.** A slow scroll hides the bars after 48px down and shows them after 24px up (was 16px and 4px), so a slow or wobbly drag no longer flickers them. A flick at 1000 px/s or faster hides or shows them at once and moves them in 60% of `--ui-motion-bars` (at least 120ms). Reduced motion still moves them at once
22
+ - **New shared gesture velocity estimator** (`VelocityEstimator`, `GESTURE_THRESHOLDS`, `velocityDuration`, `swipeCommits`, exported): a least-squares fit over the last 100ms (at most 20 samples) that drops stale, duplicate and out-of-order samples and restarts on a reversal. Native scrolling is unchanged; the estimator only decides. Hide on scroll, the pushed page's edge swipe back (a swipe at 500 px/s or faster, or past 35% of the width; it used the speed of the last touch move) and the image lightbox's swipe (now also a short fast swipe; 48px still moves) use it. The BottomSheet, the navigation drawer and toast swipes are decided by Reka UI and keep its thresholds
23
+ - **No sideways pan on a phone.** The phone layout's content area and page scroller clip `overflow-x` (`clip`, so neither becomes a scroll container), so content wider than the screen can no longer pan the page; inner horizontal scrollers (tables, tab strips) still scroll. Measurement found no overflowing scene at 320 to 430px; the clip is a guard
24
+ - **A tap box at a page's edge no longer pans the page.** An IconButton narrower than 44px on the right edge of a page that is its App's scroller (a page-header "…" in an App shell that makes the PageContent scroll, like the SDK's `.app-page`) grew its tap box past the page, and the page scrolled sideways by about 13px (590/578 at 825x360, 518/506 at 753x279). The page's own sideways clip turns into `hidden` when it scrolls vertically, so it was a scroller again. Now the PageContent body clips too: it reaches over the page's side padding (`--ui-page-bleed`: a negative margin and the same padding inside, so nothing moves), so a box keeps its whole 44px over the gutter and loses only what would lie past the page's edge. The body's width and `max-width` (`readable`, a custom width, the phone's 100%) allow for the bleed. In a DSM window the clip is repeated above the host reset
25
+ - **A sticky row in an App's own nested scroller sticks in it.** A `data-ui-sticky` row inside a scroll container of the App's own in the page's body (a wide layout's side pane) was stuck the page's sticky height down (`--ui-sticky-row-top`) inside its pane, counted in the page's offset, and what scrolled in the pane stopped below the page's header. PageContent now finds the scroll container each row sticks in: rows in the page's scroller work as before; rows in a nested one stack from that scroller's top, are left out of the page's offset, and the scroller gets its own `--ui-sticky-top-offset` (its top padding plus its rows). A nested scroller without rows can be marked `data-ui-sticky-scope` to take no page offset; a fill page's list scroller (`data-ui-page-scroller`) never takes it
26
+ - **The FAB is a circle.** The single-action FAB and the speed dial's `+` are 56px circles (`border-radius: 50%`), keeping their elevation and icon; the speed dial's items stay labelled pills
27
+ - docs/mobile-native.md: a "Phone behaviour contract" section lists what every App gets on a phone, and a "Gesture velocity" section documents the estimator and its thresholds
28
+ - `check:phone-layout`: a `search` scene (a tab page with a search field, a four-way SegmentControl, a grouped list with badges and "…", a FAB, `?fill=1` too); every hide-on-scroll scene checks that the scroller's size is the same with the bars shown and hidden; the FAB is checked to be a 56px circle; a side-pan check measures the document and the page's scrollers at 320, 360, 390 and 430px for every built-in scene, shown and after scrolling. The side-pan check also runs at the short landscape sizes 825x360 and 753x279 and on a `search-edge` scene (a "…" flush with the right edge of a page that is the App's scroller). A swipe that Chrome's synthetic touch gesture does not scroll (a headless Linux gate) falls back to scrolling the page's scroller natively in frame-sized steps, and the run prints how many swipes each way took. `--scene` can repeat. `phone-layout.html?probe=1` adds a momentum probe for a real phone or a simulator (momentum after the lift, size changes of the scroller, the longest frame). The run removes its Chrome profile when it ends
29
+
5
30
  ## 0.23.4 (2026-11-07)
6
31
 
7
32
  An App's own sticky rows count in the sticky offset. No App change is required; an App that measured its row by hand can mark it instead.
package/dist/cli.js CHANGED
File without changes
@@ -1,8 +1,8 @@
1
1
  import { InjectionKey, Ref, WatchSource } from 'vue';
2
- /** Scrolling down this far in one go hides the bars (a small threshold against jitter). */
3
- export declare const HIDE_AFTER_PX = 16;
4
- /** Scrolling up this far brings them back: any real scroll up, not a sub-pixel wobble. */
5
- export declare const SHOW_AFTER_PX = 4;
2
+ /** A slow scroll down this far hides the bars (GESTURE_THRESHOLDS.hideDistance). */
3
+ export declare const HIDE_AFTER_PX: 48;
4
+ /** A slow scroll up this far brings them back (GESTURE_THRESHOLDS.showDistance). */
5
+ export declare const SHOW_AFTER_PX: 24;
6
6
  /**
7
7
  * Which bars a scroll hides: `all` (the phone bar, the tab bar and the FAB)
8
8
  * when the page scrolls in an area under the bar, `bottom` (the tab bar and
@@ -12,26 +12,39 @@ export type BarsHidden = false | 'all' | 'bottom';
12
12
  export interface HideOnScrollOptions {
13
13
  /** The layout's content column: scrolls in it (or of the document) count. */
14
14
  column: Readonly<Ref<HTMLElement | null>>;
15
- /** The phone bar, to tell whether the scroller carries it. */
16
- bar: () => HTMLElement | null;
15
+ /** Whether the phone bar lies over the page's scroller (AppLayout `data-ui-bars-overlay`), so it can slide away. */
16
+ overlay: () => boolean;
17
17
  /** On only for a phone with the feature on (no page opted out). */
18
18
  enabled: () => boolean;
19
19
  /** Never hide while this holds (the speed dial or the navigation drawer is open). */
20
20
  blocked: () => boolean;
21
21
  /** The room the bars take: a page that scrolls less than this keeps them, or hiding them would end its scroll. */
22
22
  room: () => number;
23
- /** How long hiding or showing moves the layout; scrolls in that time are its own, not the user's. */
24
- settleMs: () => number;
25
23
  /** Each change shows the bars again (a new page). */
26
24
  reset: WatchSource[];
27
25
  }
26
+ /** What a decision reports besides the state: the scroll's speed (px/s) when it was made, for the motion's length. */
27
+ export interface HideOnScrollState {
28
+ hidden: Ref<BarsHidden>;
29
+ /** The scroll velocity (px/s) of the last change; 0 for a change not made by a scroll. */
30
+ velocity: Ref<number>;
31
+ }
28
32
  /**
29
- * Hide-on-scroll for the phone shell: scrolling the page down slides the bars
30
- * out, any scroll up brings them back at once, and so do the top of the page
31
- * and a scroll that ends at its bottom. It never hides while a sheet, dialog,
32
- * the speed dial or the drawer is open, or while a text field has focus, and
33
- * focus moving into the bars shows them.
33
+ * Hide-on-scroll for the phone shell. The page scrolls natively; each scroll
34
+ * event feeds the shared velocity estimator (interaction/velocity.ts) with
35
+ * the scroller's position, and the decision reads the velocity and the
36
+ * distance travelled in one direction, never one event's delta:
37
+ * - a flick down at GESTURE_THRESHOLDS.flick or faster hides the bars at
38
+ * once, a flick up that fast shows them at once;
39
+ * - a slower scroll hides them after `hideDistance` down and shows them after
40
+ * `showDistance` up, so a slow or wobbly drag does not flicker them;
41
+ * - the top of the page and a scroll that ends at its bottom show them.
42
+ * The bars move by transform only (adaptive.css), so a decision never
43
+ * changes the scroller's size or position and the scroll keeps its momentum.
44
+ * It never hides while a sheet, dialog, the speed dial or the drawer is
45
+ * open, or while a text field has focus, and focus moving into the bars
46
+ * shows them.
34
47
  */
35
- export declare function useHideOnScroll(options: HideOnScrollOptions): Ref<BarsHidden>;
48
+ export declare function useHideOnScroll(options: HideOnScrollOptions): HideOnScrollState;
36
49
  /** A page's opt-out from hide-on-scroll (PageContent `hide-on-scroll="false"`): counts while it is mounted. */
37
50
  export declare const APP_KEEP_BARS_KEY: InjectionKey<() => () => void>;
@@ -1,5 +1,17 @@
1
1
  import { Ref } from 'vue';
2
- /** For PageContent: the tallest height its sticky headers report, in px. */
3
- export declare function providePageSticky(): Readonly<Ref<number>>;
2
+ /**
3
+ * The sticky PageHeaders of a PageContent and the height each takes while it
4
+ * sticks, so what else sticks in the page (a SectionTabs strip, an App's
5
+ * `data-ui-sticky` row) sticks below it instead of under it. PageContent
6
+ * places each header in the scroller it sticks in: one in the page's own
7
+ * scroller sets `--ui-page-sticky-top`, one inside a scroll container of the
8
+ * App's own (a fill page's record pane) counts only in that container.
9
+ */
10
+ export type PageStickyHeader = {
11
+ element: HTMLElement;
12
+ height: number;
13
+ };
14
+ /** For PageContent: its sticky headers, in the order they reported. */
15
+ export declare function providePageSticky(): Readonly<Ref<readonly PageStickyHeader[]>>;
4
16
  /** For PageHeader: reports its height while it sticks, and nothing once it scrolls with the page. */
5
17
  export declare function useStickyHeight(element: Readonly<Ref<HTMLElement | null>>, sticks: () => boolean): void;
package/dist/index.d.ts CHANGED
@@ -307,8 +307,8 @@ export { computeOverflowFit, ITEM_GAP } from './components/composite/overflow-to
307
307
  export type { OverflowToolbarItem, OverflowToolbarMenuEntry, OverflowToolbarMoreMenu, OverflowToolbarMoreState, OverflowToolbarMoreText, } from './components/composite/overflow-toolbar/types';
308
308
  export { MOTION_OFF_SELECTOR, motionMs, motionOff, transitionMs, useInsertedKeys, useSwapMotion } from './lib/motion';
309
309
  export type { InsertedKeysOptions, MotionToken } from './lib/motion';
310
- export { COMPOSITION_END_GRACE_MS, createCompositionGuard, createEditSession, createFocusOwner, DIALOG_SELECTOR, DRAG_THRESHOLD_PX, dropIndexAt, editSessionOutside, focusSync, IGNORE_OUTSIDE_ATTR, IGNORE_OUTSIDE_SELECTOR, isComposingKey, isInForeignDialog, isPrintableKey, outsideLayerCount, pathInForeignDialog, registerOutsideLayer, swallowNextClick, trackPointerGesture, useEditSession, useOutsideLayer, usePointerGesture, } from './interaction';
311
- export type { CompositionGuard, EditCommitResult, EditSession, EditSessionOptions, EditSessionState, FocusOwner, GestureAxis, GestureCancelReason, OutsideLayer, OutsideLayerOptions, OutsideResult, PointerGesture, PointerGestureOptions, PointerGestureState, } from './interaction';
310
+ export { COMPOSITION_END_GRACE_MS, createCompositionGuard, createEditSession, createFocusOwner, DIALOG_SELECTOR, DRAG_THRESHOLD_PX, dropIndexAt, editSessionOutside, focusSync, GESTURE_THRESHOLDS, IGNORE_OUTSIDE_ATTR, IGNORE_OUTSIDE_SELECTOR, isComposingKey, isInForeignDialog, isPrintableKey, outsideLayerCount, pathInForeignDialog, registerOutsideLayer, swallowNextClick, swipeCommits, trackPointerGesture, useEditSession, useOutsideLayer, usePointerGesture, VELOCITY_MAX_SAMPLES, VELOCITY_REVERSAL_PX, VELOCITY_WINDOW_MS, VelocityEstimator, velocityDuration, } from './interaction';
311
+ export type { CompositionGuard, EditCommitResult, EditSession, EditSessionOptions, EditSessionState, FocusOwner, GestureAxis, GestureCancelReason, OutsideLayer, OutsideLayerOptions, OutsideResult, PointerGesture, PointerGestureOptions, PointerGestureState, VelocitySample, } from './interaction';
312
312
  export { AUTO_SCROLL_MAX_SPEED, AUTO_SCROLL_MIN_ZONE_PX, AUTO_SCROLL_ZONE_PERCENT, createAutoScroll, createDragKit, DND_GHOST_ATTR, DND_INDEX_ATTR, DND_KEY_ATTR, DND_LANE_ATTR, DND_STATE_ATTR, edgeSpeed, gridGeometry as dragGridGeometry, listGeometry as dragListGeometry, measuredGeometry as dragMeasuredGeometry, SWAP_LOCK_MS, TOUCH_HOLD_MS, TOUCH_HOLD_TOLERANCE_PX, useDragKit, } from './dnd';
313
313
  export type { Arrangement as DragArrangement, AutoScroll, AutoScrollOptions, Axis as DragAxis, DragDrop, DragItemBinding, DragKey, DragKit, DragKitOptions, DragLaneOptions, DragMode, DragPhase, DragPlaceholder, DragPosition, DragState, LaneGeometry as DragLaneGeometry, LaneLayout as DragLaneLayout, Rect as DragRect, UseDragKit, } from './dnd';
314
314
  export type { UiLocale } from './i18n/locale';