@sigmela/router 0.1.3 → 0.2.1

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 (52) hide show
  1. package/README.md +177 -833
  2. package/lib/module/Navigation.js +1 -10
  3. package/lib/module/NavigationStack.js +168 -19
  4. package/lib/module/Router.js +1523 -501
  5. package/lib/module/RouterContext.js +1 -1
  6. package/lib/module/ScreenStack/ScreenStack.web.js +388 -117
  7. package/lib/module/ScreenStack/ScreenStackContext.js +21 -0
  8. package/lib/module/ScreenStack/animationHelpers.js +72 -0
  9. package/lib/module/ScreenStackItem/ScreenStackItem.js +2 -1
  10. package/lib/module/ScreenStackItem/ScreenStackItem.web.js +76 -16
  11. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +2 -1
  12. package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.web.js +1 -1
  13. package/lib/module/SplitView/RenderSplitView.native.js +85 -0
  14. package/lib/module/SplitView/RenderSplitView.web.js +109 -0
  15. package/lib/module/SplitView/SplitView.js +89 -0
  16. package/lib/module/SplitView/SplitViewContext.js +4 -0
  17. package/lib/module/SplitView/index.js +5 -0
  18. package/lib/module/SplitView/useSplitView.js +11 -0
  19. package/lib/module/StackRenderer.js +4 -2
  20. package/lib/module/TabBar/RenderTabBar.native.js +118 -33
  21. package/lib/module/TabBar/RenderTabBar.web.js +52 -47
  22. package/lib/module/TabBar/TabBar.js +116 -3
  23. package/lib/module/TabBar/index.js +4 -1
  24. package/lib/module/TabBar/useTabBarHeight.js +22 -0
  25. package/lib/module/index.js +3 -4
  26. package/lib/module/navigationNode.js +3 -0
  27. package/lib/module/styles.css +693 -28
  28. package/lib/typescript/src/NavigationStack.d.ts +25 -13
  29. package/lib/typescript/src/Router.d.ts +147 -34
  30. package/lib/typescript/src/RouterContext.d.ts +1 -1
  31. package/lib/typescript/src/ScreenStack/ScreenStack.web.d.ts +0 -2
  32. package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +31 -0
  33. package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +6 -0
  34. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.types.d.ts +5 -1
  35. package/lib/typescript/src/ScreenStackItem/ScreenStackItem.web.d.ts +1 -1
  36. package/lib/typescript/src/SplitView/RenderSplitView.native.d.ts +8 -0
  37. package/lib/typescript/src/SplitView/RenderSplitView.web.d.ts +8 -0
  38. package/lib/typescript/src/SplitView/SplitView.d.ts +31 -0
  39. package/lib/typescript/src/SplitView/SplitViewContext.d.ts +3 -0
  40. package/lib/typescript/src/SplitView/index.d.ts +5 -0
  41. package/lib/typescript/src/SplitView/useSplitView.d.ts +2 -0
  42. package/lib/typescript/src/StackRenderer.d.ts +2 -1
  43. package/lib/typescript/src/TabBar/TabBar.d.ts +27 -3
  44. package/lib/typescript/src/TabBar/index.d.ts +3 -0
  45. package/lib/typescript/src/TabBar/useTabBarHeight.d.ts +18 -0
  46. package/lib/typescript/src/createController.d.ts +1 -0
  47. package/lib/typescript/src/index.d.ts +4 -3
  48. package/lib/typescript/src/navigationNode.d.ts +41 -0
  49. package/lib/typescript/src/types.d.ts +29 -32
  50. package/package.json +6 -5
  51. package/lib/module/web/TransitionStack.js +0 -227
  52. package/lib/typescript/src/web/TransitionStack.d.ts +0 -21
@@ -10,37 +10,500 @@
10
10
  .screen-stack > .screen-stack-item {
11
11
  position: absolute;
12
12
  inset: 0;
13
- display: flex;
14
13
  font-size: 24px;
15
- display: none;
16
14
  overflow: hidden;
15
+ display: none;
16
+ /* z-index is set inline in ScreenStack.web.tsx */
17
+
18
+ /* Base transition for opening screen */
19
+ transition:
20
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
21
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
17
22
  }
18
23
 
19
- .screen-stack > .screen-stack-item.active {
24
+ /* Inner container for regular screen */
25
+ .stack-screen-container {
20
26
  display: flex;
27
+ flex: 1;
28
+ height: 100%;
29
+ }
30
+
31
+ /* Inner container for modal (NOT flex: 1, to avoid stretching to full width) */
32
+ .stack-modal-container {
33
+ display: flex;
34
+ height: 100%;
35
+ }
36
+
37
+ /* Overlay inside ScreenStackItem — always full-screen, only opacity animation */
38
+ .stack-modal-overlay {
39
+ position: absolute;
40
+ inset: 0;
41
+ background: rgba(0, 0, 0, 0);
42
+ opacity: 0;
43
+ pointer-events: none;
44
+ z-index: 1;
45
+ will-change: opacity;
46
+ }
47
+
48
+ /* Show overlay only for modal-like presentations.
49
+ IMPORTANT: use direct child (>) to avoid collapsing overlay from nested stacks,
50
+ when modal ScreenStackItem is inside push ScreenStackItem container. */
51
+ .screen-stack-item:not(.modal):not(.contained-modal):not(.fullscreen-modal):not(.formsheet):not(.pagesheet):not(.sheet)
52
+ > .stack-modal-overlay {
53
+ display: none;
54
+ }
55
+
56
+ /* Keyframes for overlay appearance */
57
+ @keyframes modal-overlay-enter {
58
+ from {
59
+ opacity: 0;
60
+ }
61
+ to {
62
+ opacity: 0.5;
63
+ }
64
+ }
65
+
66
+ /* Keyframes for overlay disappearance */
67
+ @keyframes modal-overlay-exit {
68
+ from {
69
+ opacity: 0.5;
70
+ }
71
+ to {
72
+ opacity: 0;
73
+ }
74
+ }
75
+
76
+ /* Overlay in initial state — transparent */
77
+ .screen-stack-item.modal.transition-preEnter > .stack-modal-overlay,
78
+ .screen-stack-item.contained-modal.transition-preEnter > .stack-modal-overlay,
79
+ .screen-stack-item.fullscreen-modal.transition-preEnter > .stack-modal-overlay,
80
+ .screen-stack-item.formsheet.transition-preEnter > .stack-modal-overlay,
81
+ .screen-stack-item.pagesheet.transition-preEnter > .stack-modal-overlay,
82
+ .screen-stack-item.sheet.transition-preEnter > .stack-modal-overlay {
83
+ opacity: 0;
84
+ background: rgba(0, 0, 0, 0.5);
85
+ pointer-events: none;
86
+ }
87
+
88
+ /* Overlay on enter — start animation */
89
+ .screen-stack-item.modal.transition-entering > .stack-modal-overlay,
90
+ .screen-stack-item.modal.phase-active.transition-preEnter > .stack-modal-overlay,
91
+ .screen-stack-item.contained-modal.transition-entering > .stack-modal-overlay,
92
+ .screen-stack-item.contained-modal.phase-active.transition-preEnter > .stack-modal-overlay,
93
+ .screen-stack-item.fullscreen-modal.transition-entering > .stack-modal-overlay,
94
+ .screen-stack-item.fullscreen-modal.phase-active.transition-preEnter > .stack-modal-overlay,
95
+ .screen-stack-item.formsheet.transition-entering > .stack-modal-overlay,
96
+ .screen-stack-item.formsheet.phase-active.transition-preEnter > .stack-modal-overlay,
97
+ .screen-stack-item.pagesheet.transition-entering > .stack-modal-overlay,
98
+ .screen-stack-item.pagesheet.phase-active.transition-preEnter > .stack-modal-overlay,
99
+ .screen-stack-item.sheet.transition-entering > .stack-modal-overlay,
100
+ .screen-stack-item.sheet.phase-active.transition-preEnter > .stack-modal-overlay {
101
+ background: rgba(0, 0, 0, 0.5);
102
+ pointer-events: none;
103
+ animation: modal-overlay-enter 300ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
104
+ }
105
+
106
+ /* For modal-like in active / entered states — fully visible */
107
+ .screen-stack-item.modal.phase-active > .stack-modal-overlay,
108
+ .screen-stack-item.modal.transition-entered > .stack-modal-overlay,
109
+ .screen-stack-item.contained-modal.phase-active > .stack-modal-overlay,
110
+ .screen-stack-item.contained-modal.transition-entered > .stack-modal-overlay,
111
+ .screen-stack-item.fullscreen-modal.phase-active > .stack-modal-overlay,
112
+ .screen-stack-item.fullscreen-modal.transition-entered > .stack-modal-overlay,
113
+ .screen-stack-item.formsheet.phase-active > .stack-modal-overlay,
114
+ .screen-stack-item.formsheet.transition-entered > .stack-modal-overlay,
115
+ .screen-stack-item.pagesheet.phase-active > .stack-modal-overlay,
116
+ .screen-stack-item.pagesheet.transition-entered > .stack-modal-overlay,
117
+ .screen-stack-item.sheet.phase-active > .stack-modal-overlay,
118
+ .screen-stack-item.sheet.transition-entered > .stack-modal-overlay {
119
+ opacity: 0.5;
120
+ background: rgba(0, 0, 0, 0.5);
121
+ pointer-events: auto;
122
+ }
123
+
124
+ /* Overlay on modal-like close — disappearance animation */
125
+ .screen-stack-item.modal.phase-exiting > .stack-modal-overlay,
126
+ .screen-stack-item.modal.transition-exiting > .stack-modal-overlay,
127
+ .screen-stack-item.contained-modal.phase-exiting > .stack-modal-overlay,
128
+ .screen-stack-item.contained-modal.transition-exiting > .stack-modal-overlay,
129
+ .screen-stack-item.fullscreen-modal.phase-exiting > .stack-modal-overlay,
130
+ .screen-stack-item.fullscreen-modal.transition-exiting > .stack-modal-overlay,
131
+ .screen-stack-item.formsheet.phase-exiting > .stack-modal-overlay,
132
+ .screen-stack-item.formsheet.transition-exiting > .stack-modal-overlay,
133
+ .screen-stack-item.pagesheet.phase-exiting > .stack-modal-overlay,
134
+ .screen-stack-item.pagesheet.transition-exiting > .stack-modal-overlay,
135
+ .screen-stack-item.sheet.phase-exiting > .stack-modal-overlay,
136
+ .screen-stack-item.sheet.transition-exiting > .stack-modal-overlay {
137
+ background: rgba(0, 0, 0, 0.5);
138
+ pointer-events: none;
139
+ animation: modal-overlay-exit 300ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
140
+ }
141
+
142
+ /* Overlay for transparent-modal — transparent */
143
+ .screen-stack-item.transparent-modal > .stack-modal-overlay {
144
+ opacity: 0 !important;
145
+ pointer-events: none !important;
146
+ }
147
+
148
+ /* Overlay for contained-transparent-modal — transparent */
149
+ .screen-stack-item.contained-transparent-modal > .stack-modal-overlay {
150
+ opacity: 0 !important;
151
+ pointer-events: none !important;
21
152
  }
22
153
 
23
- .screen-stack[data-animation="navigation"].animating > .screen-stack-item {
24
- transition: transform .3s cubic-bezier(.4, .0, .2, 1), filter .3s cubic-bezier(.4, .0, .2, 1)
154
+ /* Modal-like content container always above overlay */
155
+ .screen-stack-item.modal .stack-modal-container,
156
+ .screen-stack-item.contained-modal .stack-modal-container,
157
+ .screen-stack-item.fullscreen-modal .stack-modal-container,
158
+ .screen-stack-item.formsheet .stack-modal-container,
159
+ .screen-stack-item.pagesheet .stack-modal-container,
160
+ .screen-stack-item.sheet .stack-modal-container {
161
+ position: relative;
162
+ z-index: 2;
25
163
  }
26
164
 
27
- .screen-stack[data-animation="navigation"].animating.backwards > .screen-stack-item {
28
- transition: transform .25s cubic-bezier(.4, .0, .2, 1), filter .25s cubic-bezier(.4, .0, .2, 1);
165
+ /* Show active / entering elements */
166
+ .screen-stack > .screen-stack-item.phase-active,
167
+ .screen-stack > .screen-stack-item.transition-entered,
168
+ .screen-stack > .screen-stack-item.transition-entering,
169
+ .screen-stack > .screen-stack-item.transition-preEnter {
170
+ display: flex;
29
171
  }
30
172
 
31
- .screen-stack[data-animation="modal"].animating > .screen-stack-item {
32
- transition: transform .3s cubic-bezier(.4, .0, .2, 1), filter .3s cubic-bezier(.4, .0, .2, 1)
173
+ /* Base state for active elements after entry */
174
+ /* Overridden via animation classes (push-enter, pop-enter, etc.) */
175
+ .screen-stack > .screen-stack-item.phase-active.transition-entered:not(.push-enter):not(.pop-enter):not(.modal):not(.sheet) {
176
+ transform: translateX(0);
177
+ filter: none;
33
178
  }
34
179
 
35
- .screen-stack[data-animation="modal"].animating.backwards > .screen-stack-item {
36
- transition: transform .25s cubic-bezier(.4, .0, .2, 1), filter .25s cubic-bezier(.4, .0, .2, 1);
180
+ /* Background (inactive) — visible, but without interactions */
181
+ .screen-stack > .screen-stack-item.phase-inactive {
182
+ display: flex;
183
+ pointer-events: none; /* Don't intercept clicks */
37
184
  }
38
185
 
186
+ /* Exiting elements are also shown during animation */
187
+ /* Important: also show elements with pop-exit, even if they are still in transition-entered */
188
+ .screen-stack > .screen-stack-item.phase-exiting,
189
+ .screen-stack > .screen-stack-item.transition-exiting,
190
+ .screen-stack > .screen-stack-item.transition-preExit,
191
+ .screen-stack > .screen-stack-item.pop-exit {
192
+ display: flex;
193
+ }
194
+
195
+ /* Fully exited — hide */
196
+ .screen-stack > .screen-stack-item.transition-exited,
197
+ .screen-stack > .screen-stack-item.transition-unmounted {
198
+ display: none;
199
+ }
200
+
201
+ /* ==================== MODALS: base transform neutralization ==================== */
202
+
203
+ /* IMPORTANT: modal elements should not move like regular screens */
204
+ .screen-stack > .screen-stack-item.modal,
205
+ .screen-stack > .screen-stack-item.sheet {
206
+ transform: translate3d(0, 0, 0) !important;
207
+ }
208
+
209
+ /* ==================== COMMON STYLES BY SCREEN TYPE ==================== */
210
+
211
+ /* ==================== PUSH ANIMATIONS ==================== */
212
+
213
+ /* Forward navigation (push) — opening screen 300ms */
214
+ .screen-stack-item.push.push-enter {
215
+ transition:
216
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
217
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
218
+ }
219
+
220
+ /* PUSH ENTER - regular screen enters from right */
221
+ .screen-stack-item.push.push-enter.transition-preEnter {
222
+ transform: translateX(100%);
223
+ }
224
+
225
+ .screen-stack-item.push.push-enter.transition-entering,
226
+ .screen-stack-item.push.push-enter.transition-entered {
227
+ transform: translateX(0);
228
+ }
229
+
230
+ /* PUSH BACKGROUND - background screen shifts left */
231
+ /* Important: use !important to override general rules for inactive */
232
+ .screen-stack-item.push.push-background {
233
+ transform: translateX(-25%) !important;
234
+ transition:
235
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
236
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
237
+ }
238
+
239
+ /* POP BACKGROUND - background screen on pop stays at -25% */
240
+ /* Background elements on pop get pop-background instead of none, to preserve -25% position */
241
+ .screen-stack-item.push.pop-background {
242
+ transform: translateX(-25%) !important;
243
+ /* Don't set transition, so element stays in place without animation */
244
+ filter: none;
245
+ }
246
+
247
+ /* Backward navigation (pop) — closing screen 300ms */
248
+ /* IMPORTANT: transition is set on element with pop-exit */
249
+ .screen-stack-item.push.pop-exit {
250
+ transition:
251
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
252
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
253
+ }
254
+
255
+ /* POP EXIT - top screen exits to right */
256
+ /* Key difference from old version: in old version selector .phase-exiting was applied WITHOUT dependency on transition-exiting */
257
+ /* Here we do the same: if element has pop-exit and phase-exiting, it should exit to right */
258
+ .screen-stack-item.push.pop-exit.phase-exiting,
259
+ .screen-stack-item.push.pop-exit.transition-exiting {
260
+ transform: translateX(100%);
261
+ filter: none;
262
+ }
263
+
264
+ /* POP ENTER - screen returns to center from -25% */
265
+ /* Applied only to active (top) element on pop */
266
+ /* In old version: element was at -25% (as background), then on pop becomes active and goes to 0 */
267
+ /* Simplified approach as in old version: phase-active simply becomes 0 */
268
+ .screen-stack-item.push.pop-enter {
269
+ transition:
270
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
271
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
272
+ }
273
+
274
+ /* Active element on pop simply in center (0) - final state */
275
+ /* As in old version: .phase-active on pop → transform: translateX(0) */
276
+ .screen-stack-item.push.pop-enter.phase-active,
277
+ .screen-stack-item.push.pop-enter.phase-active.transition-entered {
278
+ transform: translateX(0);
279
+ filter: none;
280
+ }
281
+
282
+ /* ==================== MODAL STACK: don't move background ==================== */
283
+
284
+ /* If there's a modal on top — don't move previous screen and don't dim it,
285
+ overlay is responsible for dimming. */
286
+ .screen-stack:has(> .screen-stack-item.modal.phase-active)
287
+ > .screen-stack-item.push.phase-inactive,
288
+ .screen-stack:has(> .screen-stack-item.modal.phase-active)
289
+ > .screen-stack-item.push.phase-inactive.transition-entered {
290
+ transform: translate3d(0, 0, 0) !important;
291
+ filter: none;
292
+ }
293
+
294
+ /* ==================== MOBILE MODAL (<= 639px) — bottom sheet ==================== */
295
+ @media (max-width: 639px) {
296
+ /* Inner container for modal — bottom sheet full width with top border radius */
297
+ .screen-stack-item.modal .stack-modal-container {
298
+ width: 100%;
299
+ margin: 0;
300
+ border-radius: 0;
301
+ border-top-left-radius: 38px;
302
+ border-top-right-radius: 38px;
303
+ align-self: flex-end; /* align to bottom edge of parent */
304
+ overflow: hidden;
305
+ background: #fff; /* or var(--modal-bg) */
306
+
307
+ transform: translateY(0);
308
+ transition:
309
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
310
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
311
+ }
312
+
313
+ /* MODAL ENTER - modal enters (bottom to top) */
314
+ .screen-stack-item.modal.modal-enter.transition-preEnter .stack-modal-container {
315
+ transform: translateY(100%);
316
+ filter: none;
317
+ }
318
+
319
+ .screen-stack-item.modal.modal-enter.transition-entering .stack-modal-container,
320
+ .screen-stack-item.modal.modal-enter.transition-entered .stack-modal-container {
321
+ transform: translateY(0);
322
+ filter: none;
323
+ }
324
+
325
+ /* MODAL EXIT - modal closes (exits downward) */
326
+ .screen-stack-item.modal.modal-exit.transition-exiting .stack-modal-container {
327
+ transform: translateY(100%);
328
+ filter: none;
329
+ transition:
330
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
331
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
332
+ }
333
+
334
+ /* SHEET - sheet styles for mobile version */
335
+ .screen-stack-item.sheet .stack-modal-container {
336
+ width: 100%;
337
+ margin: 0;
338
+ border-radius: 0;
339
+ border-top-left-radius: 38px;
340
+ border-top-right-radius: 38px;
341
+ align-self: flex-end;
342
+ overflow: hidden;
343
+ background: #fff;
344
+
345
+ transform: translateY(0);
346
+ transition:
347
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
348
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
349
+ }
350
+
351
+ /* SHEET ENTER - sheet enters (bottom to top) */
352
+ .screen-stack-item.sheet.sheet-enter.transition-preEnter .stack-modal-container {
353
+ transform: translateY(100%);
354
+ filter: none;
355
+ }
356
+
357
+ .screen-stack-item.sheet.sheet-enter.transition-entering .stack-modal-container,
358
+ .screen-stack-item.sheet.sheet-enter.transition-entered .stack-modal-container {
359
+ transform: translateY(0);
360
+ filter: none;
361
+ }
362
+
363
+ /* SHEET EXIT - sheet closes */
364
+ .screen-stack-item.sheet.sheet-exit.transition-exiting .stack-modal-container {
365
+ transform: translateY(100%);
366
+ filter: none;
367
+ transition:
368
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
369
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
370
+ }
371
+ }
372
+
373
+ /* ==================== DESKTOP MODAL (>= 640px) — sheet on right ==================== */
374
+ @media (min-width: 640px) {
375
+ /* Inner container for modal — right sheet 393px, with margin and border radius */
376
+ .screen-stack-item.modal .stack-modal-container {
377
+ width: 393px;
378
+ max-width: calc(100% - 36px);
379
+ height: calc(100% - 36px);
380
+ margin: 18px;
381
+ margin-left: auto; /* align to right edge */
382
+ border-radius: 16px;
383
+ overflow: hidden;
384
+ background: #fff; /* or var(--modal-bg) */
385
+
386
+ transform: translateX(0);
387
+ transition:
388
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
389
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
390
+ }
391
+
392
+ /* MODAL ENTER - modal enters (right to left) */
393
+ .screen-stack-item.modal.modal-enter.transition-preEnter .stack-modal-container {
394
+ transform: translateX(100%);
395
+ filter: none;
396
+ }
397
+
398
+ .screen-stack-item.modal.modal-enter.transition-entering .stack-modal-container,
399
+ .screen-stack-item.modal.modal-enter.transition-entered .stack-modal-container {
400
+ transform: translateX(0);
401
+ filter: none;
402
+ }
403
+
404
+ /* MODAL EXIT - modal closes (exits to right) */
405
+ .screen-stack-item.modal.modal-exit.transition-exiting .stack-modal-container {
406
+ transform: translateX(100%);
407
+ filter: none;
408
+ transition:
409
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
410
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
411
+ }
412
+
413
+ /* SHEET ENTER - sheet enters (right to left) */
414
+ .screen-stack-item.sheet.sheet-enter.transition-preEnter
415
+ .stack-modal-container {
416
+ transform: translateX(100%);
417
+ filter: none;
418
+ }
419
+
420
+ .screen-stack-item.sheet.sheet-enter.transition-entering
421
+ .stack-modal-container,
422
+ .screen-stack-item.sheet.sheet-enter.transition-entered
423
+ .stack-modal-container {
424
+ transform: translateX(0);
425
+ filter: none;
426
+ }
427
+
428
+ /* SHEET EXIT - sheet closes */
429
+ .screen-stack-item.sheet.sheet-exit.transition-exiting
430
+ .stack-modal-container {
431
+ transform: translateX(100%);
432
+ filter: none;
433
+ transition:
434
+ transform 300ms cubic-bezier(0.22, 0.61, 0.36, 1),
435
+ filter 300ms cubic-bezier(0.22, 0.61, 0.36, 1);
436
+ }
437
+
438
+ /* Background screens under modal — don't move, don't dim (overlay does this) */
439
+ .screen-stack:has(> .screen-stack-item.modal.phase-active)
440
+ > .screen-stack-item.push.phase-inactive,
441
+ .screen-stack:has(> .screen-stack-item.modal.phase-active)
442
+ > .screen-stack-item.push.phase-inactive.transition-entered {
443
+ transform: translate3d(0, 0, 0) !important;
444
+ filter: none;
445
+ }
446
+ }
447
+
448
+ /* ==================== INACTIVE SCREENS (common settings) ==================== */
449
+
450
+ /* Inactive screens (background stack elements)
451
+ Background elements on pop get animationType: pop-background and stay at -25%
452
+ (except modal case, there we override via :has) */
453
+ .screen-stack > .screen-stack-item.phase-inactive:not(.push-background):not(.pop-background):not(.pop-enter):not(.pop-exit):not(.modal):not(.sheet) {
454
+ /* Don't set transform explicitly - element will preserve previous position */
455
+ filter: none;
456
+ }
457
+
458
+ /* ==================== SPECIAL CASES ==================== */
459
+
460
+ /* NO-ANIMATE - no animation (screenOptions.animated === false) */
461
+ /* IMPORTANT: this rule should override all other animation rules */
462
+ .screen-stack-item.no-animate,
463
+ .screen-stack-item.no-animate.push-enter,
464
+ .screen-stack-item.no-animate.push-exit,
465
+ .screen-stack-item.no-animate.push-background,
466
+ .screen-stack-item.no-animate.pop-enter,
467
+ .screen-stack-item.no-animate.pop-exit,
468
+ .screen-stack-item.no-animate.pop-background {
469
+ transform: translate3d(0, 0, 0) !important;
470
+ transition: none !important;
471
+ filter: none !important;
472
+ }
473
+
474
+ /* NONE - no animation (initial mount) */
475
+ /* IMPORTANT: this rule should NOT be applied to pop-background elements */
476
+ .screen-stack-item.none:not(.pop-background) {
477
+ transform: translate3d(0, 0, 0) !important;
478
+ transition: none !important;
479
+ filter: none !important;
480
+ }
481
+
482
+
483
+
39
484
  .tab-stacks-container {
40
485
  display: flex;
41
486
  flex-direction: column;
42
487
  flex: 1 1 auto;
43
- min-height: 100vhd;
488
+ min-height: 100dvh;
489
+ position: relative;
490
+ }
491
+
492
+ .tab-stacks-container > .screen-stack {
493
+ padding-bottom: calc(73px + env(safe-area-inset-bottom));
494
+ }
495
+
496
+ .tab-bar-blur-overlay {
497
+ position: fixed;
498
+ bottom: 0;
499
+ left: 0;
500
+ right: 0;
501
+ height: calc(73px + 16px + env(safe-area-inset-bottom));
502
+ backdrop-filter: blur(20px) saturate(180%);
503
+ background-color: rgba(255, 255, 255, 0.05);
504
+ /* -webkit-backdrop-filter: blur(20px) saturate(180%); */
505
+ pointer-events: none;
506
+ z-index: 99;
44
507
  }
45
508
 
46
509
  .tab-bar {
@@ -49,8 +512,27 @@
49
512
  align-items: center;
50
513
  justify-content: center;
51
514
  flex-shrink: 0;
52
- position: relative;
53
- height: 49px;
515
+ position: fixed;
516
+ bottom: 0;
517
+ left: 0;
518
+ right: 0;
519
+ padding: 8px 16px;
520
+ padding-bottom: max(16px, env(safe-area-inset-bottom));
521
+ background: transparent;
522
+ z-index: 100;
523
+ }
524
+
525
+ .tab-bar-inner {
526
+ display: flex;
527
+ flex-direction: row;
528
+ align-items: center;
529
+ background: #FFFFFF;
530
+ box-shadow: 0 2px 8px #00000014;
531
+ border-radius: 100px;
532
+ padding: 4px;
533
+ gap: 4px;
534
+ width: 100%;
535
+ max-width: 100%;
54
536
  }
55
537
 
56
538
  .tab-item {
@@ -58,28 +540,46 @@
58
540
  background: transparent;
59
541
  border: 0;
60
542
  margin: 0;
61
- padding: 4px 0 2px;
62
- height: 100%;
543
+ padding: 8px 16px;
544
+ height: 49px;
63
545
  color: inherit;
64
546
  display: flex;
65
- flex: 1 1 0%;
547
+ flex: 1;
66
548
  flex-direction: column;
67
549
  align-items: center;
68
550
  justify-content: center;
551
+ gap: 4px;
69
552
  position: relative;
70
553
  cursor: pointer;
554
+ border-radius: 100px;
555
+ transition: all 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
71
556
  }
72
557
 
558
+ .tab-item.active {
559
+ background: #EDEDED;
560
+ }
561
+
562
+ .tab-item:hover:not(.active) {
563
+ background: color-mix(in srgb, currentColor 8%, transparent);
564
+ }
565
+
566
+
73
567
  .tab-item:focus {
74
568
  outline: none;
75
569
  }
76
570
 
571
+ .tab-item:focus-visible {
572
+ outline: 2px solid currentColor;
573
+ outline-offset: 2px;
574
+ }
575
+
77
576
  .tab-item-icon {
78
577
  width: 24px;
79
578
  height: 24px;
80
579
  display: flex;
81
580
  align-items: center;
82
581
  justify-content: center;
582
+ flex-shrink: 0;
83
583
  }
84
584
 
85
585
  .tab-item-icon > * {
@@ -90,28 +590,193 @@
90
590
  }
91
591
 
92
592
  .tab-item-label {
93
- font-size: 11px;
94
- line-height: 12px;
593
+ font-size: 12px;
594
+ line-height: 16px;
595
+ font-weight: 500;
95
596
  text-align: center;
96
597
  white-space: nowrap;
97
598
  pointer-events: none;
98
- margin-top: 2px;
99
599
  }
100
600
 
101
601
  .tab-item-label-badge {
102
602
  position: absolute;
103
- top: 4px;
104
- left: 50%;
105
- transform: translateX(10px);
106
- min-width: 16px;
107
- height: 16px;
108
- padding: 0 5px;
603
+ top: 2px;
604
+ right: 2px;
605
+ min-width: 18px;
606
+ height: 18px;
607
+ padding: 0 6px;
109
608
  border-radius: 9999px;
110
609
  background-color: #dc3545;
111
610
  color: #fff;
112
- font-size: 10px;
113
- line-height: 16px;
611
+ font-size: 11px;
612
+ font-weight: 600;
613
+ line-height: 18px;
114
614
  display: inline-flex;
115
615
  align-items: center;
116
616
  justify-content: center;
617
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
618
+ }
619
+
620
+ /* ==================== DESKTOP TAB BAR (>= 641px) ==================== */
621
+ @media (min-width: 641px) {
622
+ .tab-stacks-container {
623
+ flex-direction: row;
624
+ }
625
+
626
+ .tab-stacks-container > .screen-stack {
627
+ padding-bottom: 0;
628
+ margin-left: 260px;
629
+ min-width: 0;
630
+ width: calc(100% - 260px);
631
+ }
632
+
633
+ .tab-bar-blur-overlay {
634
+ display: none;
635
+ }
636
+
637
+ .tab-bar {
638
+ position: absolute;
639
+ top: 0;
640
+ left: 0;
641
+ bottom: 0;
642
+ width: 260px;
643
+ height: 100%;
644
+ flex-direction: column;
645
+ align-items: stretch;
646
+ justify-content: flex-start;
647
+ padding: 0;
648
+ flex-shrink: 0;
649
+ order: 1;
650
+ background: transparent;
651
+ z-index: auto;
652
+ }
653
+
654
+ .tab-bar-inner {
655
+ height: 100%;
656
+ width: 100%;
657
+ flex-direction: column;
658
+ align-items: stretch;
659
+ justify-content: flex-start;
660
+ border-radius: 0;
661
+ padding: 12px 0 0 0;
662
+ gap: 0;
663
+ background: #FFFFFF;
664
+ box-shadow: none;
665
+ }
666
+
667
+ .tab-item {
668
+ margin: 0 12px;
669
+ margin-bottom: 4px;
670
+ height: 44px;
671
+ flex: 0 0 auto;
672
+ flex-direction: row;
673
+ align-items: center;
674
+ justify-content: flex-start;
675
+ padding: 0 12px;
676
+ width: auto;
677
+ border-radius: 10px;
678
+ transition: background-color 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
679
+ }
680
+
681
+ .tab-item.active {
682
+ background: var(--backgroundTertiaryTrans, rgba(201, 204, 209, 0.32));
683
+ }
684
+
685
+ .tab-item:active {
686
+ transform: none;
687
+ }
688
+
689
+ .tab-item-icon {
690
+ width: 24px;
691
+ height: 24px;
692
+ padding-right: 12px;
693
+ flex-shrink: 0;
694
+ }
695
+
696
+ .tab-item-label {
697
+ flex: 1;
698
+ font-size: 14px;
699
+ line-height: 20px;
700
+ font-weight: 600;
701
+ text-align: left;
702
+ padding-right: 3px;
703
+ white-space: nowrap;
704
+ }
705
+
706
+ .tab-item-label-badge {
707
+ position: static;
708
+ transform: none;
709
+ margin-left: auto;
710
+ }
711
+ }
712
+
713
+ /* ==================== SPLIT VIEW (web) ==================== */
714
+
715
+ .split-view-container {
716
+ position: relative;
717
+ display: block; /* narrow by default; wide enabled via injected @media rule */
718
+ height: 100dvh;
719
+ min-height: 100dvh;
720
+ width: 100%;
721
+ overflow: hidden;
722
+ }
723
+
724
+ .split-view-container .screen-stack {
725
+ /* ScreenStack has min-width: 100%, which breaks side-by-side layouts */
726
+ min-width: 0;
727
+ }
728
+
729
+ .split-view-primary {
730
+ position: relative;
731
+ width: 100%;
732
+ height: 100%;
733
+ min-width: 0;
734
+ }
735
+
736
+ .split-view-secondary {
737
+ position: absolute;
738
+ inset: 0;
739
+ z-index: 2;
740
+ width: 100%;
741
+ height: 100%;
742
+ min-width: 0;
743
+ }
744
+
745
+ /* In narrow mode, hide secondary completely if it has no active screen */
746
+ .split-view-secondary:not(:has(.screen-stack-item.phase-active)):not(:has(.screen-stack-item.phase-exiting)):not(:has(.screen-stack-item.transition-entering)):not(:has(.screen-stack-item.transition-entered)):not(:has(.screen-stack-item.transition-preEnter)):not(:has(.screen-stack-item.transition-exiting)):not(:has(.screen-stack-item.transition-preExit)):not(:has(.screen-stack-item.pop-exit)) {
747
+ display: none;
748
+ }
749
+
750
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.phase-active)) .split-view-primary,
751
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.phase-exiting)) .split-view-primary,
752
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.transition-entering)) .split-view-primary,
753
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.transition-entered)) .split-view-primary,
754
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.transition-preEnter)) .split-view-primary,
755
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.transition-exiting)) .split-view-primary,
756
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.transition-preExit)) .split-view-primary,
757
+ .split-view-container:has(.split-view-secondary:has(.screen-stack-item.pop-exit)) .split-view-primary {
758
+ pointer-events: none;
759
+ }
760
+
761
+ /* Split view wide layout - applied when viewport width >= 640px */
762
+ @media (min-width: 640px) {
763
+ .split-view-container {
764
+ display: flex;
765
+ flex-direction: row;
766
+ }
767
+
768
+ .split-view-primary {
769
+ position: relative;
770
+ flex: 0 0 auto;
771
+ max-width: var(--split-view-primary-max-width, 390px);
772
+ border-right: 1px solid rgba(0, 0, 0, 0.08);
773
+ }
774
+
775
+ .split-view-secondary {
776
+ display: flex;
777
+ position: relative;
778
+ inset: auto;
779
+ z-index: auto;
780
+ flex: 1 1 auto;
781
+ }
117
782
  }