@synopackageland/ui 0.23.4 → 0.23.5

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.
@@ -2,6 +2,25 @@
2
2
 
3
3
  How the UI SDK behaves on a phone (390×844) and a touch tablet (768×1024).
4
4
 
5
+ ## Phone behaviour contract
6
+
7
+ What every App gets on a touch phone, from the UI SDK, with no code of its
8
+ own. An App does not override these; a fix to one is made here, for every
9
+ App. Each line links to its details below.
10
+
11
+ | Behaviour | The rule |
12
+ | --- | --- |
13
+ | FAB and speed dial | The page's one action is a 56px circle (`border-radius: 50%`) with its icon and elevation; two or more open from a 56px circle with `+` as labelled pills. 56px is Material 3's FAB size (56dp; M3's default FAB shape is a rounded square, the UI draws a circle) and clears iOS's 44pt minimum tap target. See [Page actions on a FAB](#native-phone-shell). |
14
+ | One compact top bar | One 44px row: Back, the menu button or the App icon, the title, at most two icons and one "…". No brand row, no second header row. |
15
+ | No tab heading on tab pages | A page of the tab bar draws no title, icon or brand in the bar (`phone-bar-title="none"`) and no heading of its own: the tab bar says where the user is. A sub-page keeps Back and its title. |
16
+ | Title on a tab page is opt-in | Showing a title on a tab page is the App's explicit choice: `phone-bar-title="auto"` on `AppLayout`, and the SDK AppShell's own opt-in (`phoneTitle`). |
17
+ | Hide on scroll | By `transform` only; the scroller keeps its size, position and padding and nothing writes `scrollTop`, so a momentum fling keeps going. Slow scrolls use distance hysteresis, flicks decide at once. See [Hide on scroll](#native-phone-shell) and [Gesture velocity](#gesture-velocity). |
18
+ | No horizontal pan | The page never pans sideways: the phone layout's content area and page scroller clip `overflow-x`, and `check:phone-layout` asserts `scrollWidth == clientWidth` for the document and the page's scrollers at 320, 360, 390 and 430px. An inner horizontal scroller (a table, a tab strip) still scrolls. |
19
+ | Bottom accessory | `#accessory` docks above the tab bar, lifts the FAB and toasts, and slides into the tab bar's place with the bars. |
20
+ | Sticky offsets | `--ui-sticky-top-offset` counts the covered padding, a sticky `PageHeader`, the tabs strip and every `data-ui-sticky` row, so scrolling to an element lands below them. A row in an App's own nested scroller (a side pane) sticks at that scroller's top and counts only there. |
21
+ | No end space under docked bars | The room under the last row is constant (`--ui-page-end-space`, plus `--ui-bars-room` under docked bars); a fill page's list runs under the FAB and bars to the screen's bottom with no blank band. |
22
+ | Gesture velocity | One estimator and one set of thresholds for every gesture decision the UI makes. See [Gesture velocity](#gesture-velocity). |
23
+
5
24
  ## Width containment and the release gate
6
25
 
7
26
  Typography tables scroll horizontally within their parent, including a flex
@@ -78,7 +97,7 @@ control and grid sizes; it does not change the 16px field rule.
78
97
  | `--ui-tab-bar-height` | – | – | **48** | the tab bar's row, above `--ui-safe-bottom` |
79
98
  | `--ui-section-tab-height` | 44 | 44 | **40** | the drawn height of a phone's tabs strip (its tap box stays 44) |
80
99
  | `--ui-sticky-top-offset` | 0 | 0 | measured | what sticks at the top of a PageContent (see below) |
81
- | `--ui-page-end-space` | 24 | 24 | 24, the safe area while the bars are hidden on a fill page | room after a page's last row |
100
+ | `--ui-page-end-space` | 24 | 24 | 24 (constant: hiding the bars never changes it) | room after a page's last row |
82
101
  | `--ui-radius-sheet` (new) | 12 | 12 | 12 | top corners of a bottom sheet |
83
102
  | `--ui-safe-top/right/bottom/left` (new) | `env(safe-area-inset-*, 0px)` | same | same | notch and home indicator; non-zero only once the viewport meta has `viewport-fit=cover` |
84
103
  | `--ui-pressed` (new) | 8% of the text colour | same | same | the pressed tint |
@@ -188,6 +207,18 @@ own) keeps scrolling. An App that puts small controls flush with the edge of
188
207
  its own scroller, outside a `PageContent`, clips that content sideways the
189
208
  same way.
190
209
 
210
+ When an App shell makes the `PageContent` itself its page scroller
211
+ (`overflow-y: auto` on it, like the SDK's `.app-page`), the browser turns its
212
+ sideways clip into `hidden`, which is a scroller again: a page-header ⋯ on its
213
+ right edge made it 13px wider than its view in a landscape phone window. So
214
+ the `PageContent` body clips too. The body reaches over the page's side
215
+ padding (`--ui-page-bleed`, the gutter of a page with a surface, 0 for
216
+ `surface="none"`) with a negative margin and the same padding inside, so its
217
+ content stays where it was, and it clips at the page's own edge: a box keeps
218
+ its 44px over the gutter and loses only what would lie outside the page. The
219
+ body's width and `max-width` allow for the bleed; an App that sizes the body
220
+ itself (`width: 100%`) is overridden by the body's inline width.
221
+
191
222
  A control at the end edge of its own component is the component's job, not
192
223
  the page's: a dismissible Callout is often full width in an App's own
193
224
  scroller with no `PageContent` around it. Such a control sets
@@ -438,6 +469,46 @@ is the App's presentation (`mobile`), set by AppRoot or AppLayout. The
438
469
  `#actions` slot sits above the section list. A `SettingsPage` inside a pushed
439
470
  section keeps its sticky footer.
440
471
 
472
+ ## Gesture velocity
473
+
474
+ Scrolling and dragging stay the browser's own; the UI never animates a
475
+ scroll or replaces momentum. Where the UI itself decides what a gesture
476
+ means, it reads one estimator with one set of thresholds
477
+ (`src/interaction/velocity.ts`, exported from the package):
478
+
479
+ - `VelocityEstimator`: velocity along one axis (px/s) from a least-squares
480
+ line through the samples of the last 100ms (`VELOCITY_WINDOW_MS`), at most
481
+ 20 (`VELOCITY_MAX_SAMPLES`). A sample with the same timestamp replaces the
482
+ last, an out-of-order one is ignored, a reversal of more than 0.5px
483
+ (`VELOCITY_REVERSAL_PX`) starts a new fit from the turning point, a gap
484
+ longer than the window starts over, and a fit older than the window reads 0
485
+ (the finger stopped before lifting).
486
+ - `GESTURE_THRESHOLDS`:
487
+
488
+ | Name | Value | Used for |
489
+ | --- | --- | --- |
490
+ | `flick` | 1000 px/s | hide or show the phone bars at once; faster bar motion |
491
+ | `swipe` | 500 px/s | a short swipe commits (pop a pushed page, next image) |
492
+ | `swipeDistance` | 48px | a slow swipe commits (next image) |
493
+ | `hideDistance` | 48px | a slow scroll down hides the bars |
494
+ | `showDistance` | 24px | a slow scroll up shows the bars |
495
+ | `slop` | 6px | travel before a touch has a direction |
496
+
497
+ - `velocityDuration(velocity, baseMs)`: a motion set off by a flick takes 60%
498
+ of its base duration (never under 120ms); anything slower takes the base.
499
+ - `swipeCommits(travel, velocity, distance)`: a released swipe commits past
500
+ `distance`, or past `slop` at `swipe` speed or faster in its direction.
501
+
502
+ Reduced motion is the stylesheet's: every transition finishes at once, so a
503
+ decision still happens and nothing animates.
504
+
505
+ Where it is used: hide on scroll and the FAB (the page's `scrollTop`), the
506
+ pushed page's edge swipe back (`PushTransition`: past 35% of the width, or a
507
+ `swipe`), and the image lightbox's swipe to the next file. The BottomSheet's
508
+ drag and snap, the navigation drawer's swipe and a toast's swipe to dismiss
509
+ are decided by Reka UI, which the UI does not fork; they keep Reka's own
510
+ thresholds. SegmentControl and lists have no swipe gestures.
511
+
441
512
  ## RecordPanel and AsidePanel on the BottomSheet
442
513
 
443
514
  Both panels use `BottomSheet` on a phone, so they share its grabber, drag,
@@ -586,10 +657,10 @@ the page's actions (PageHeader `#actions`, a pushed page's
586
657
  at the bottom right, 16px from the edges and above the tab bar. No App
587
658
  change: the same slot moves.
588
659
 
589
- - One action: the FAB is that action (56px, its icon only, its label as the
590
- accessible name). A tap runs it. It is the area's one primary: a danger
660
+ - One action: the FAB is that action (a 56px circle, its icon only, its
661
+ label as the accessible name). A tap runs it. It is the area's one primary: a danger
591
662
  action stays red.
592
- - Two or more: the FAB (`+`, "Page actions") opens a speed dial: the actions
663
+ - Two or more: the FAB (a 56px circle with `+`, "Page actions") opens a speed dial: the actions
593
664
  as labelled 48px pills over a scrim, staggered in. Focus moves to the first
594
665
  and stays in the dial; Escape, Back, a press on the scrim or the FAB close
595
666
  it and focus returns to the FAB. A pick runs the action and closes the dial;
@@ -627,15 +698,38 @@ change: the same slot moves.
627
698
  (fades out) while the speed dial is open; it moves with the bars (`--ui-motion-bars`, at once with reduced motion). A page without a FAB,
628
699
  a DSM window and a desktop place toasts as before.
629
700
 
630
- **Hide on scroll.** On by default (`AppLayout` `hide-on-scroll`). Scrolling
631
- the page down 16px or more slides the bar, the tab bar, the FAB and a clean
632
- settings footer out (`data-ui-bars-hidden`, `all` when the page scrolls under
633
- the bar, `bottom` when the document scrolls); any scroll up of 4px, the top of
634
- the page and a scroll that stops at its end bring them back. Safe areas are
635
- kept, and they move in `--ui-motion-bars` (200ms; 0 with reduced motion). They
636
- never hide while the speed dial, the drawer, a sheet or a dialog is open or a
637
- text field has focus, and focus moving into a bar shows them. A page shorter
638
- than the bars' room never hides them.
701
+ **Hide on scroll.** On by default (`AppLayout` `hide-on-scroll`). The page
702
+ scrolls natively; the decision reads the shared velocity estimator (see
703
+ [Gesture velocity](#gesture-velocity)) and the distance travelled one way:
704
+
705
+ - a slow scroll down of `hideDistance` (48px) slides the bar, the tab bar,
706
+ the FAB and a clean settings footer out (`data-ui-bars-hidden`); a slow
707
+ scroll up of `showDistance` (24px) brings them back, so a slow or wobbly
708
+ drag never flickers them;
709
+ - a flick at `flick` (1000 px/s) or faster hides them, or shows them, at
710
+ once, and they move in 60% of `--ui-motion-bars` (never under 120ms;
711
+ `--ui-bars-motion` on the layout), else in `--ui-motion-bars` (200ms; at
712
+ once with reduced motion);
713
+ - the top of the page and a scroll that stops at its end bring them back.
714
+
715
+ The bars move by `transform` only. When the page scrolls in an area under
716
+ the bar (a fill layout, or an App shell whose `main` does not let the
717
+ document scroll) the layout marks itself `data-ui-bars-overlay`: the content
718
+ runs under the bar, the tab bar and the accessory from the start, and the
719
+ end of what scrolls has a constant spacer (`--ui-bars-room`: the bar, the
720
+ tab bar or the safe area, and the accessory) so the last row clears them.
721
+ Hiding then translates the bar, the content area and the tab bar
722
+ (`data-ui-bars-hidden="all"`). When the document scrolls, the bar scrolls
723
+ away with it, and only the tab bar, the accessory and the FAB slide
724
+ (`bottom`). Either way the scroller keeps its size, position and padding, no
725
+ `scrollTop` is written, and a momentum fling keeps going on iOS and Android.
726
+ While the content area is translated it is the containing block of a
727
+ `position: fixed` element inside it; the UI's own overlays (sheets, menus,
728
+ toasts) render in the floating layer outside it.
729
+
730
+ They never hide while the speed dial, the drawer, a sheet or a dialog is
731
+ open or a text field has focus, and focus moving into a bar shows them. A
732
+ page that scrolls less than the bars' room never hides them.
639
733
 
640
734
  - Opt out for the App: `<AppLayout :hide-on-scroll="false">`; for one page:
641
735
  `<PageContent :hide-on-scroll="false">`. Both keep the settings footer too.
@@ -719,15 +813,32 @@ An App sets no `top` of its own and measures nothing.
719
813
  page only.
720
814
  - The offset is the page's, not per element: content above a sticky row in
721
815
  the middle of a page also stops that much lower (never under it).
816
+ - A row inside a scroll container of the App's own in the body (a wide
817
+ layout's side pane with `overflow-y: auto`, or any ancestor whose overflow
818
+ is not `visible` or `clip`, which is where the browser sticks it) sticks in
819
+ that container, not in the page. It stacks only on that container's earlier
820
+ rows (`top` 0 for the first), never counts in the page's offset, and the
821
+ container gets its own `--ui-sticky-top-offset` (its top padding plus its
822
+ rows), so what scrolls in the pane stops below the pane's rows and not
823
+ below the page's header. Before 0.23.5 such a row was stuck the page's
824
+ header height down inside its pane.
825
+ - A nested scroller without rows still inherits the page's offset as its
826
+ content's scroll margin. Mark it `data-ui-sticky-scope` and what scrolls in
827
+ it takes no page offset. A fill page's own list scroller
828
+ (`data-ui-page-scroller`) never takes it: the header is above the list, not
829
+ over it.
722
830
  - The rule has one attribute's specificity, so an App's own class wins for
723
831
  `position`, `background` or `z-index`.
724
832
  - Before 0.23.4 an App measured its row and added it to the token by hand;
725
833
  remove that code when it marks the row instead.
726
834
 
727
835
  **The end of a page: `--ui-page-end-space`.** The room after a page's last
728
- row, 24px. While the bars are hidden on a fill page (`data-ui-page-fills`)
729
- it is only `--ui-safe-bottom`, so the content's scroller runs to the bottom
730
- of the screen. An App shell that pads its page end reads it:
836
+ row, 24px, constant: hiding the bars never changes it, so the content's
837
+ length never changes mid-scroll. Under docked bars (`data-ui-bars-overlay`)
838
+ the room for the bars is the separate `--ui-bars-room` spacer, and a fill
839
+ page whose own scroller has the FAB's room gets no end space below that
840
+ scroller, so no blank band sits under a list. An App shell that pads its
841
+ page end reads it:
731
842
  `padding-bottom: var(--ui-page-end-space, var(--ui-space-6, 24px))`.
732
843
 
733
844
  **Scroll helpers.** A phone page scrolls in a different element than a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synopackageland/ui",
3
- "version": "0.23.4",
3
+ "version": "0.23.5",
4
4
  "description": "Shared Vue 3 UI component library with a DSM 7 inspired application theme",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,63 +60,6 @@
60
60
  "./themes/dsm7-embedded-reset.css": "./dist/themes/dsm7-embedded-reset.css",
61
61
  "./themes/dsm7-embedded-theme.css": "./dist/themes/dsm7-embedded-theme.css"
62
62
  },
63
- "scripts": {
64
- "check:docs": "node --test scripts/check-doc-versions.test.mjs && node scripts/check-doc-versions.mjs",
65
- "dev": "vite build --watch",
66
- "build": "vite build && vite build --mode json && vite build --mode rich-text && vite build --mode grid && vite build --mode cli && vite build --mode preview && vite build --mode embedded && node scripts/postbuild.mjs",
67
- "lint": "eslint .",
68
- "typecheck": "vue-tsc --noEmit -p tsconfig.json",
69
- "test": "vitest run",
70
- "test:watch": "vitest",
71
- "test:changed": "echo \"test:changed skips the tests the changes since ${SYNOPKGLAND_GATE_BASE:-origin/master} do not reach (vitest --changed); pnpm check runs them all\" && vitest run --changed ${SYNOPKGLAND_GATE_BASE:-origin/master}",
72
- "story": "histoire dev",
73
- "story:build": "histoire build",
74
- "check:host": "node scripts/dsm-host-shots.mjs",
75
- "check:switch": "node scripts/check-switch.mjs",
76
- "check:tabs": "node scripts/check-tabs.mjs",
77
- "check:mobile": "node scripts/check-mobile-overflow.mjs",
78
- "check:ui-patch": "node scripts/check-ui-patch.mjs",
79
- "check:narrow": "node scripts/check-narrow-controls.mjs",
80
- "check:drawer-form": "node scripts/check-drawer-form.mjs",
81
- "check:responsive": "node scripts/check-responsive.mjs",
82
- "check:adaptive": "node scripts/check-adaptive.mjs",
83
- "check:sidebar-focus": "node scripts/check-sidebar-focus.mjs",
84
- "check:json-render": "node scripts/check-json-render.mjs",
85
- "check:motion": "node scripts/check-motion.mjs",
86
- "check:topology": "node scripts/check-topology.mjs",
87
- "check:wizard": "node scripts/check-wizard.mjs",
88
- "check:edges": "node scripts/check-edges.mjs",
89
- "check:data-grid": "node scripts/check-data-grid.mjs",
90
- "check:truncation": "node scripts/check-truncation.mjs",
91
- "check:link": "node scripts/check-link.mjs",
92
- "check:text-select": "node scripts/check-text-select.mjs",
93
- "check:date-picker": "node scripts/check-date-picker.mjs",
94
- "check:full-page": "node scripts/check-full-page.mjs",
95
- "check:short-viewport": "node scripts/check-short-viewport.mjs",
96
- "check:phone-layout": "node scripts/check-phone-layout.mjs",
97
- "check:text-zoom": "node scripts/check-text-zoom.mjs",
98
- "check:focus-restore": "node scripts/check-focus-restore.mjs",
99
- "check:detail-surface": "node scripts/check-detail-surface.mjs",
100
- "check:internal": "node --test scripts/check-internal-content.test.mjs scripts/check-npm-metadata.test.mjs && node scripts/check-internal-content.mjs --repo .",
101
- "check:npm-metadata": "node scripts/check-npm-metadata.mjs",
102
- "check:package": "node scripts/check-package-content.mjs",
103
- "check:public-api-consumer": "node scripts/check-public-api-consumer.mjs",
104
- "notices:update": "node scripts/third-party-notices.mjs --write",
105
- "notices:check": "node scripts/third-party-notices.mjs --check",
106
- "check": "bash scripts/docker-check.sh \"npm run gates\"",
107
- "check:fast": "bash scripts/docker-check.sh \"npm run gates\"",
108
- "check:local-browser": "node scripts/app-local-browser.mjs",
109
- "check:infotip": "node scripts/check-infotip.mjs",
110
- "release:stage": "node scripts/stage-release.mjs",
111
- "check:polish": "bash scripts/docker-check.sh --build --out .polish-shots 'node scripts/check-polish.mjs'",
112
- "check:acceptance": "node scripts/check-acceptance.mjs",
113
- "check:palette": "node scripts/check-palette.mjs",
114
- "gates": "node scripts/gates.mjs",
115
- "gates:full": "node scripts/gates.mjs --full",
116
- "check:layout": "node scripts/check-layout.mjs",
117
- "release:smoke": "node scripts/release-smoke.mjs",
118
- "check:browser-install": "playwright install chromium"
119
- },
120
63
  "peerDependencies": {
121
64
  "vue": "^3.5.0"
122
65
  },
@@ -167,13 +110,6 @@
167
110
  "engines": {
168
111
  "node": ">=20"
169
112
  },
170
- "packageManager": "pnpm@10.33.2",
171
- "pnpm": {
172
- "onlyBuiltDependencies": [
173
- "esbuild",
174
- "vue-demi"
175
- ]
176
- },
177
113
  "config": {
178
114
  "bundledRuntime": {
179
115
  "@tanstack/vue-virtual": "3.13.39",
@@ -181,5 +117,62 @@
181
117
  "reka-ui": "2.11.0"
182
118
  }
183
119
  },
184
- "homepage": "https://www.varvis.me/docs/"
185
- }
120
+ "homepage": "https://www.varvis.me/docs/",
121
+ "scripts": {
122
+ "check:docs": "node --test scripts/check-doc-versions.test.mjs && node scripts/check-doc-versions.mjs",
123
+ "dev": "vite build --watch",
124
+ "build": "vite build && vite build --mode json && vite build --mode rich-text && vite build --mode grid && vite build --mode cli && vite build --mode preview && vite build --mode embedded && node scripts/postbuild.mjs",
125
+ "lint": "eslint .",
126
+ "typecheck": "vue-tsc --noEmit -p tsconfig.json",
127
+ "test": "vitest run",
128
+ "test:watch": "vitest",
129
+ "test:changed": "echo \"test:changed skips the tests the changes since ${SYNOPKGLAND_GATE_BASE:-origin/master} do not reach (vitest --changed); pnpm check runs them all\" && vitest run --changed ${SYNOPKGLAND_GATE_BASE:-origin/master}",
130
+ "story": "histoire dev",
131
+ "story:build": "histoire build",
132
+ "check:host": "node scripts/dsm-host-shots.mjs",
133
+ "check:switch": "node scripts/check-switch.mjs",
134
+ "check:tabs": "node scripts/check-tabs.mjs",
135
+ "check:mobile": "node scripts/check-mobile-overflow.mjs",
136
+ "check:ui-patch": "node scripts/check-ui-patch.mjs",
137
+ "check:narrow": "node scripts/check-narrow-controls.mjs",
138
+ "check:drawer-form": "node scripts/check-drawer-form.mjs",
139
+ "check:responsive": "node scripts/check-responsive.mjs",
140
+ "check:adaptive": "node scripts/check-adaptive.mjs",
141
+ "check:sidebar-focus": "node scripts/check-sidebar-focus.mjs",
142
+ "check:json-render": "node scripts/check-json-render.mjs",
143
+ "check:motion": "node scripts/check-motion.mjs",
144
+ "check:topology": "node scripts/check-topology.mjs",
145
+ "check:wizard": "node scripts/check-wizard.mjs",
146
+ "check:edges": "node scripts/check-edges.mjs",
147
+ "check:data-grid": "node scripts/check-data-grid.mjs",
148
+ "check:truncation": "node scripts/check-truncation.mjs",
149
+ "check:link": "node scripts/check-link.mjs",
150
+ "check:text-select": "node scripts/check-text-select.mjs",
151
+ "check:date-picker": "node scripts/check-date-picker.mjs",
152
+ "check:full-page": "node scripts/check-full-page.mjs",
153
+ "check:short-viewport": "node scripts/check-short-viewport.mjs",
154
+ "check:phone-layout": "node scripts/check-phone-layout.mjs",
155
+ "check:text-zoom": "node scripts/check-text-zoom.mjs",
156
+ "check:focus-restore": "node scripts/check-focus-restore.mjs",
157
+ "check:detail-surface": "node scripts/check-detail-surface.mjs",
158
+ "check:internal": "node --test scripts/check-internal-content.test.mjs scripts/check-npm-metadata.test.mjs && node scripts/check-internal-content.mjs --repo .",
159
+ "check:npm-metadata": "node scripts/check-npm-metadata.mjs",
160
+ "check:package": "node scripts/check-package-content.mjs",
161
+ "check:public-api-consumer": "node scripts/check-public-api-consumer.mjs",
162
+ "notices:update": "node scripts/third-party-notices.mjs --write",
163
+ "notices:check": "node scripts/third-party-notices.mjs --check",
164
+ "check": "bash scripts/docker-check.sh \"npm run gates\"",
165
+ "check:fast": "bash scripts/docker-check.sh \"npm run gates\"",
166
+ "check:local-browser": "node scripts/app-local-browser.mjs",
167
+ "check:infotip": "node scripts/check-infotip.mjs",
168
+ "release:stage": "node scripts/stage-release.mjs",
169
+ "check:polish": "bash scripts/docker-check.sh --build --out .polish-shots 'node scripts/check-polish.mjs'",
170
+ "check:acceptance": "node scripts/check-acceptance.mjs",
171
+ "check:palette": "node scripts/check-palette.mjs",
172
+ "gates": "node scripts/gates.mjs",
173
+ "gates:full": "node scripts/gates.mjs --full",
174
+ "check:layout": "node scripts/check-layout.mjs",
175
+ "release:smoke": "node scripts/release-smoke.mjs",
176
+ "check:browser-install": "playwright install chromium"
177
+ }
178
+ }