@inglorious/ui 7.4.0 → 7.6.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.
package/README.md CHANGED
@@ -46,17 +46,22 @@ The public CSS exports are:
46
46
 
47
47
  ## Safe-area utilities
48
48
 
49
- The base styles also expose CSS variables and utility classes for iPhone notch and home-indicator safe areas.
49
+ The base styles also expose CSS variables and utility classes for the system bars' insets: the notch, rounded corners, the home indicator and Android's status and navigation bars. They are in `base.css`, or on their own in `@inglorious/ui/safe-area.css`.
50
50
 
51
51
  ```css
52
52
  :root {
53
- --safe-area-top: env(safe-area-inset-top, 0px);
54
- --safe-area-bottom: env(safe-area-inset-bottom, 0px);
55
- --safe-area-left: env(safe-area-inset-left, 0px);
56
- --safe-area-right: env(safe-area-inset-right, 0px);
53
+ --safe-area-top: …;
54
+ --safe-area-bottom: …;
55
+ --safe-area-left: …;
56
+ --safe-area-right: …;
57
57
  }
58
58
  ```
59
59
 
60
+ They are `env(safe-area-inset-*)`, with two corrections:
61
+
62
+ - **In a Capacitor app on Android**, they use the `--safe-area-inset-*` variables Capacitor's SystemBars plugin injects. `env()` alone doubles the status bar on WebViews that Capacitor already pads natively.
63
+ - **In a browser tab, the top inset is 0.** The browser's chrome always sits between the page and the status bar, but browsers that embed a WebView can still report one (DuckDuckGo on Android reports 37px under its toolbar). The top inset applies in the installed PWA and in the native app, which `mount` from `@inglorious/web` marks with the `iw-native` class on `<html>`.
64
+
60
65
  The following utility classes are available:
61
66
 
62
67
  ```css
@@ -73,5 +78,3 @@ The following utility classes are available:
73
78
  padding-right: var(--safe-area-right);
74
79
  }
75
80
  ```
76
-
77
- These are useful for mobile layouts that need to respect notches, rounded corners, and the home indicator. They are intentionally generic utilities so apps can opt into them without having to define custom safe-area values repeatedly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "A design system for Inglorious Web with render-function components and customizable themes",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  "./navigation.css": "./src/navigation/navigation.css",
36
36
  "./surfaces.css": "./src/surfaces/surfaces.css",
37
37
  "./tokens.css": "./src/styles/tokens.css",
38
+ "./safe-area.css": "./src/styles/safe-area.css",
38
39
  "./themes/inglorious.css": "./src/styles/themes/inglorious.css",
39
40
  "./themes/material.css": "./src/styles/themes/material.css",
40
41
  "./themes/bootstrap.css": "./src/styles/themes/bootstrap.css",
@@ -305,10 +306,10 @@
305
306
  "**/*.css"
306
307
  ],
307
308
  "peerDependencies": {
308
- "@inglorious/charts": "10.0.5",
309
+ "@inglorious/charts": "10.0.6",
309
310
  "@inglorious/store": "11.0.3",
310
311
  "@inglorious/utils": "5.0.0",
311
- "@inglorious/web": "6.5.2"
312
+ "@inglorious/web": "6.6.0"
312
313
  },
313
314
  "devDependencies": {
314
315
  "@fontsource/material-symbols-outlined": "^5.3.5",
@@ -317,7 +318,7 @@
317
318
  "@inglorious/store": "11.0.3",
318
319
  "@inglorious/vite-plugin-jsx": "2.0.3",
319
320
  "@inglorious/vite-plugin-vue": "1.0.4",
320
- "@inglorious/web": "6.5.2",
321
+ "@inglorious/web": "6.6.0",
321
322
  "@storybook/addon-docs": "^10.6.0",
322
323
  "@storybook/web-components-vite": "^10.6.0",
323
324
  "happy-dom": "^20.14.0",
@@ -1,44 +1,9 @@
1
1
  /**
2
2
  * @typedef {import("../../../types/navigation/view-transitions").TransitionsStore} Store
3
3
  * @typedef {import("../../../types/navigation/view-transitions").ViewTransitionType} ViewTransitionType
4
+ * @typedef {import("../../../types/navigation/view-transitions").TransitionsOptions} TransitionsOptions
4
5
  */
5
6
 
6
- /** The path the router matches against. Hashes are deliberately excluded: they
7
- * change the fragment, not the page, so they must not animate one. */
8
- function getPath() {
9
- return window.location.pathname + window.location.search
10
- }
11
-
12
- /** The position of the current entry in the session history, stamped by the
13
- * Router as it navigates. Comparing it to the previous one is what tells a push
14
- * from a traversal, and in which direction. */
15
- function getIndex() {
16
- return window.history.state?.idx ?? 0
17
- }
18
-
19
- function supportsViewTransitions() {
20
- return typeof document.startViewTransition === "function"
21
- }
22
-
23
- /** `types` landed after `startViewTransition` itself, so an engine can have the
24
- * method and still throw when handed the object form of the argument. */
25
- function supportsTransitionTypes() {
26
- return (
27
- typeof ViewTransition !== "undefined" && "types" in ViewTransition.prototype
28
- )
29
- }
30
-
31
- /**
32
- * @param {number} index - The index of the entry now current.
33
- * @param {number} previousIndex - The index of the entry before this render.
34
- * @returns {ViewTransitionType}
35
- */
36
- function getTransitionType(index, previousIndex) {
37
- if (index < previousIndex) return "back"
38
- if (index > previousIndex) return "forward"
39
- return "replace"
40
- }
41
-
42
7
  /**
43
8
  * Wraps a store so that the renders which follow a navigation are run inside a
44
9
  * view transition, tagged with the direction they navigated in. Pair it with
@@ -57,11 +22,28 @@ function getTransitionType(index, previousIndex) {
57
22
  *
58
23
  * mount(withTransitions(store), renderApp, document.getElementById("root"))
59
24
  *
25
+ * Pass `routes` to choose which navigations animate; the rest change page
26
+ * without a transition. A list of route names animates every navigation into
27
+ * or out of one of them. A function receives `{ type, from, to }`, where `type`
28
+ * is the direction read from the history and `from` / `to` carry the `path`,
29
+ * `index` and `route` of each side, and returns whether to animate.
30
+ *
31
+ * @example
32
+ * // Slide into and out of detail pages; switch tabs instantly.
33
+ * withTransitions(store, { routes: ["eventDetail", "userDetail"] })
34
+ *
35
+ * @example
36
+ * // Slide only when going deeper or coming back up, never sideways.
37
+ * withTransitions(store, {
38
+ * routes: ({ from, to }) => from.path.startsWith(to.path) || to.path.startsWith(from.path),
39
+ * })
40
+ *
60
41
  * @param {Store} store - The store to wrap.
42
+ * @param {TransitionsOptions} [options]
61
43
  * @returns {Store} The same store, with a wrapped `subscribe`.
62
44
  * @throws {Error} If the store has no `Router` type.
63
45
  */
64
- export function withTransitions(store) {
46
+ export function withTransitions(store, { routes } = {}) {
65
47
  if (!store.getType("Router")) {
66
48
  throw new Error(
67
49
  '[withTransitions] Requires the "Router" type from @inglorious/web. The ' +
@@ -71,38 +53,60 @@ export function withTransitions(store) {
71
53
  }
72
54
 
73
55
  const subscribe = store.subscribe
56
+ const shouldTransition = toPredicate(routes)
74
57
  const listeners = new Set()
75
58
 
76
59
  let currentPath = ""
77
60
  let currentIndex = 0
61
+ let currentRoute
78
62
  let unsubscribeStore = null
79
63
 
80
- /* The update callback of a view transition does not run synchronously: the
81
- * browser captures the old state first and calls it on a later task. Reading
82
- * the state here rather than in the subscription keeps a listener from
83
- * replaying whatever the store held when the transition started. */
84
- function notify() {
85
- const state = store.getState()
86
- // Copied, so that a listener subscribing during this pass waits for the
87
- // next one instead of being notified by the pass that added it.
88
- ;[...listeners].forEach((listener) => listener(state))
64
+ store.subscribe = (listener) => {
65
+ /* Taken here rather than when the store is wrapped, so that anything that
66
+ navigated in between — an initial redirect, a renderer mounting late — is
67
+ part of the baseline instead of a navigation in its own right. It also
68
+ keeps `window` untouched until something subscribes, so wrapping a store
69
+ during a server render is harmless. */
70
+ if (!unsubscribeStore) {
71
+ currentPath = getPath()
72
+ currentIndex = getIndex()
73
+ currentRoute = getRoute(store.getState())
74
+ unsubscribeStore = subscribe.call(store, onStateChange)
75
+ }
76
+
77
+ listeners.add(listener)
78
+
79
+ return () => {
80
+ listeners.delete(listener)
81
+
82
+ if (!listeners.size) {
83
+ unsubscribeStore?.()
84
+ unsubscribeStore = null
85
+ }
86
+ }
89
87
  }
90
88
 
91
89
  function onStateChange() {
92
90
  const path = getPath()
93
91
  const index = getIndex()
92
+ const route = getRoute(store.getState())
94
93
  /* Both are compared against the baseline before it moves forward,
95
94
  * otherwise the comparison would always be against itself and every
96
95
  * navigation would read as a replacement. */
97
96
  const hasNavigated = path !== currentPath
98
- const previousIndex = currentIndex
97
+ const from = { path: currentPath, index: currentIndex, route: currentRoute }
98
+ const to = { path, index, route }
99
99
 
100
100
  currentPath = path
101
101
  currentIndex = index
102
+ currentRoute = route
102
103
 
103
104
  if (!hasNavigated || !supportsViewTransitions()) return notify()
104
105
 
105
- const types = [getTransitionType(index, previousIndex)]
106
+ const type = getTransitionType(to.index, from.index)
107
+ if (!shouldTransition({ type, from, to })) return notify()
108
+
109
+ const types = [type]
106
110
 
107
111
  if (supportsTransitionTypes()) {
108
112
  document.startViewTransition({ update: notify, types })
@@ -113,29 +117,67 @@ export function withTransitions(store) {
113
117
  }
114
118
  }
115
119
 
116
- store.subscribe = (listener) => {
117
- /* Taken here rather than when the store is wrapped, so that anything that
118
- navigated in between — an initial redirect, a renderer mounting late — is
119
- part of the baseline instead of a navigation in its own right. It also
120
- keeps `window` untouched until something subscribes, so wrapping a store
121
- during a server render is harmless. */
122
- if (!unsubscribeStore) {
123
- currentPath = getPath()
124
- currentIndex = getIndex()
125
- unsubscribeStore = subscribe.call(store, onStateChange)
126
- }
120
+ /* The update callback of a view transition does not run synchronously: the
121
+ * browser captures the old state first and calls it on a later task. Reading
122
+ * the state here rather than in the subscription keeps a listener from
123
+ * replaying whatever the store held when the transition started. */
124
+ function notify() {
125
+ const state = store.getState()
126
+ // Copied, so that a listener subscribing during this pass waits for the
127
+ // next one instead of being notified by the pass that added it.
128
+ ;[...listeners].forEach((listener) => listener(state))
129
+ }
127
130
 
128
- listeners.add(listener)
131
+ return store
132
+ }
129
133
 
130
- return () => {
131
- listeners.delete(listener)
134
+ /** `routes` as a predicate over a navigation. Without it every navigation
135
+ * animates. */
136
+ function toPredicate(routes) {
137
+ if (!routes) return () => true
138
+ if (typeof routes === "function") return routes
132
139
 
133
- if (!listeners.size) {
134
- unsubscribeStore?.()
135
- unsubscribeStore = null
136
- }
137
- }
138
- }
140
+ const names = new Set(routes)
141
+ return ({ from, to }) => names.has(from.route) || names.has(to.route)
142
+ }
139
143
 
140
- return store
144
+ /** The path the router matches against. Hashes are deliberately excluded: they
145
+ * change the fragment, not the page, so they must not animate one. */
146
+ function getPath() {
147
+ return window.location.pathname + window.location.search
148
+ }
149
+
150
+ /** The position of the current entry in the session history, stamped by the
151
+ * Router as it navigates. Comparing it to the previous one is what tells a push
152
+ * from a traversal, and in which direction. */
153
+ function getIndex() {
154
+ return window.history.state?.idx ?? 0
155
+ }
156
+
157
+ /** The route the Router entity resolved, whatever the entity is called. */
158
+ function getRoute(state) {
159
+ return Object.values(state).find((entity) => entity?.type === "Router")?.route
160
+ }
161
+
162
+ function supportsViewTransitions() {
163
+ return typeof document.startViewTransition === "function"
164
+ }
165
+
166
+ /**
167
+ * @param {number} index - The index of the entry now current.
168
+ * @param {number} previousIndex - The index of the entry before this render.
169
+ * @returns {ViewTransitionType}
170
+ */
171
+ function getTransitionType(index, previousIndex) {
172
+ if (index < previousIndex) return "back"
173
+ if (index > previousIndex) return "forward"
174
+ return "replace"
175
+ }
176
+
177
+ /** `types` landed after `startViewTransition` itself, so an engine can have the
178
+ * method and still throw when handed the object form of the argument. */
179
+ function supportsTransitionTypes() {
180
+ return (
181
+ typeof ViewTransition !== "undefined" && "types" in ViewTransition.prototype
182
+ )
141
183
  }
@@ -1,8 +1,39 @@
1
+ /* The system bars' insets.
2
+
3
+ In a native app on Android, Capacitor's SystemBars plugin injects
4
+ --safe-area-inset-* with the right value in every case: 0 where it pads the
5
+ WebView natively (WebView < 140), the real inset where the app is
6
+ edge-to-edge. env() is only right on the newer WebViews, and adding it on top
7
+ of that native padding doubles the status bar. Everywhere else there is no
8
+ injected variable, and env() is the fallback.
9
+
10
+ The top inset is the exception: in a browser tab the browser's own chrome
11
+ always sits between the page and the status bar, so it is 0 there even when
12
+ the browser reports otherwise. Browsers that embed a WebView can (DuckDuckGo
13
+ on Android says 37px with its toolbar in the way). It only applies where the
14
+ page can reach the status bar: the installed PWA, and the native app, which
15
+ `mount` from @inglorious/web marks with `iw-native`. */
1
16
  :root {
2
- --safe-area-top: env(safe-area-inset-top, 0px);
3
- --safe-area-bottom: env(safe-area-inset-bottom, 0px);
4
- --safe-area-left: env(safe-area-inset-left, 0px);
5
- --safe-area-right: env(safe-area-inset-right, 0px);
17
+ --safe-area-top: var(--safe-area-inset-top, 0px);
18
+ --safe-area-bottom: var(
19
+ --safe-area-inset-bottom,
20
+ env(safe-area-inset-bottom, 0px)
21
+ );
22
+ --safe-area-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
23
+ --safe-area-right: var(
24
+ --safe-area-inset-right,
25
+ env(safe-area-inset-right, 0px)
26
+ );
27
+ }
28
+
29
+ @media (display-mode: standalone), (display-mode: fullscreen) {
30
+ :root {
31
+ --safe-area-top: env(safe-area-inset-top, 0px);
32
+ }
33
+ }
34
+
35
+ :root.iw-native {
36
+ --safe-area-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
6
37
  }
7
38
 
8
39
  .pt-safe {
@@ -10,8 +10,33 @@ export interface TransitionsStore<S = unknown> {
10
10
  getType(name: string): unknown
11
11
  }
12
12
 
13
+ /** One side of a navigation. */
14
+ export interface NavigationSide {
15
+ path: string
16
+ /** The history index the Router stamped on the entry. */
17
+ index: number
18
+ /** The route the Router resolved, if any. */
19
+ route?: string
20
+ }
21
+
22
+ export interface Navigation {
23
+ /** The direction read from the history. */
24
+ type: ViewTransitionType
25
+ from: NavigationSide
26
+ to: NavigationSide
27
+ }
28
+
29
+ export interface TransitionsOptions {
30
+ /** Which navigations animate: those into or out of the named routes, or
31
+ * those the function accepts. Omit it to animate every navigation. */
32
+ routes?: string[] | ((navigation: Navigation) => boolean)
33
+ }
34
+
13
35
  export interface WithTransitions {
14
- <S>(store: TransitionsStore<S>): TransitionsStore<S>
36
+ <S>(
37
+ store: TransitionsStore<S>,
38
+ options?: TransitionsOptions,
39
+ ): TransitionsStore<S>
15
40
  }
16
41
 
17
42
  export declare const withTransitions: WithTransitions