@blotch/dolan 22.2.3 → 22.2.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.
- package/dist/commands/add.js +9 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/add.test.js +38 -3
- package/dist/commands/add.test.js.map +1 -1
- package/dist/commands/doctor.js +258 -21
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +425 -11
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +38 -18
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +105 -2
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/theme.js +1 -1
- package/dist/commands/theme.js.map +1 -1
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +194 -50
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/update.test.js +570 -3
- package/dist/commands/update.test.js.map +1 -1
- package/dist/lockfile.d.ts +17 -0
- package/dist/lockfile.js +56 -1
- package/dist/lockfile.js.map +1 -1
- package/dist/lockfile.test.d.ts +1 -0
- package/dist/lockfile.test.js +65 -0
- package/dist/lockfile.test.js.map +1 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +119 -11
- package/dist/registry.js.map +1 -1
- package/dist/registry.test.js +61 -6
- package/dist/registry.test.js.map +1 -1
- package/dist/test-utils.d.ts +27 -0
- package/dist/test-utils.js +45 -1
- package/dist/test-utils.js.map +1 -1
- package/dist/testing/fire-lynx-event.d.ts +14 -0
- package/dist/testing/fire-lynx-event.js +19 -0
- package/dist/testing/fire-lynx-event.js.map +1 -0
- package/dist/testing/fire-lynx-event.test.d.ts +1 -0
- package/dist/testing/fire-lynx-event.test.js +44 -0
- package/dist/testing/fire-lynx-event.test.js.map +1 -0
- package/dist/testing/lynx-main-thread.d.ts +22 -0
- package/dist/testing/lynx-main-thread.js +11 -0
- package/dist/testing/lynx-main-thread.js.map +1 -0
- package/dist/testing/lynx-main-thread.test.d.ts +1 -0
- package/dist/testing/lynx-main-thread.test.js +24 -0
- package/dist/testing/lynx-main-thread.test.js.map +1 -0
- package/dist/testing/set-input-signal.d.ts +5 -4
- package/dist/testing/set-input-signal.js +5 -4
- package/dist/testing/set-input-signal.js.map +1 -1
- package/dist/ui/boolean-inputs.test.ts +76 -0
- package/dist/ui/components/accordion/accordion.test.ts +158 -144
- package/dist/ui/components/accordion/accordion.ts +19 -14
- package/dist/ui/components/action-sheet/action-sheet.test.ts +637 -183
- package/dist/ui/components/action-sheet/action-sheet.ts +339 -55
- package/dist/ui/components/action-sheet/index.ts +1 -0
- package/dist/ui/components/alert/alert.test.ts +67 -19
- package/dist/ui/components/alert/alert.ts +14 -16
- package/dist/ui/components/alert-dialog/alert-dialog.test.ts +740 -129
- package/dist/ui/components/alert-dialog/alert-dialog.ts +672 -58
- package/dist/ui/components/alert-dialog/index.ts +5 -0
- package/dist/ui/components/area-chart/area-chart.test.ts +81 -1
- package/dist/ui/components/area-chart/area-chart.ts +3 -2
- package/dist/ui/components/avatar/avatar.test.ts +89 -45
- package/dist/ui/components/badge/badge.test.ts +57 -23
- package/dist/ui/components/badge/badge.ts +14 -15
- package/dist/ui/components/bar-chart/bar-chart.test.ts +82 -1
- package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +1314 -273
- package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1046 -119
- package/dist/ui/components/bottom-sheet/index.ts +7 -1
- package/dist/ui/components/button/button.test.ts +646 -2
- package/dist/ui/components/button/button.ts +443 -64
- package/dist/ui/components/button/index.ts +2 -0
- package/dist/ui/components/button-group/button-group.test.ts +528 -2
- package/dist/ui/components/button-group/button-group.ts +428 -70
- package/dist/ui/components/button-group/index.ts +3 -0
- package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +82 -1
- package/dist/ui/components/card/card.test.ts +97 -61
- package/dist/ui/components/card/card.ts +2 -1
- package/dist/ui/components/chart/chart-presets.test.ts +197 -1
- package/dist/ui/components/chart/chart-presets.ts +14 -7
- package/dist/ui/components/chart/chart-theme.ts +12 -0
- package/dist/ui/components/chart/chart.test.ts +1031 -0
- package/dist/ui/components/chart/chart.ts +66 -46
- package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
- package/dist/ui/components/checkbox/checkbox.ts +25 -24
- package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
- package/dist/ui/components/collapsible/collapsible.ts +21 -15
- package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
- package/dist/ui/components/context-menu/context-menu.ts +110 -0
- package/dist/ui/components/context-menu/index.ts +1 -0
- package/dist/ui/components/dialog/dialog.test.ts +210 -152
- package/dist/ui/components/dialog/dialog.ts +35 -29
- package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
- package/dist/ui/components/dock/dock-scroll.ts +142 -0
- package/dist/ui/components/dock/dock-search.test.ts +594 -0
- package/dist/ui/components/dock/dock.test.ts +752 -0
- package/dist/ui/components/dock/dock.ts +1438 -0
- package/dist/ui/components/dock/index.ts +2 -0
- package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
- package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
- package/dist/ui/components/dynamic-color/index.ts +5 -0
- package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
- package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
- package/dist/ui/components/glass/concentric-radius.ts +17 -0
- package/dist/ui/components/glass/glass-design.test.ts +78 -0
- package/dist/ui/components/glass/glass-design.ts +119 -0
- package/dist/ui/components/glass/glass.test.ts +376 -0
- package/dist/ui/components/glass/glass.ts +430 -0
- package/dist/ui/components/glass/index.ts +19 -0
- package/dist/ui/components/icon/icon-theme.test.ts +64 -0
- package/dist/ui/components/icon/icon-theme.ts +70 -16
- package/dist/ui/components/icon/icon.test.ts +52 -41
- package/dist/ui/components/input/input.test.ts +178 -101
- package/dist/ui/components/input/input.ts +17 -13
- package/dist/ui/components/label/label.test.ts +11 -24
- package/dist/ui/components/label/label.ts +8 -2
- package/dist/ui/components/line-chart/line-chart.test.ts +76 -1
- package/dist/ui/components/line-chart/line-chart.ts +3 -2
- package/dist/ui/components/list/list.test.ts +80 -49
- package/dist/ui/components/list/list.ts +5 -4
- package/dist/ui/components/menu/index.ts +25 -0
- package/dist/ui/components/menu/menu-placement.test.ts +153 -0
- package/dist/ui/components/menu/menu-placement.ts +159 -0
- package/dist/ui/components/menu/menu-popup.test.ts +636 -0
- package/dist/ui/components/menu/menu-popup.ts +565 -0
- package/dist/ui/components/menu/menu.test.ts +523 -0
- package/dist/ui/components/menu/menu.ts +455 -0
- package/dist/ui/components/nav-drawer/nav-drawer.test.ts +425 -260
- package/dist/ui/components/nav-drawer/nav-drawer.ts +75 -58
- package/dist/ui/components/navigation-bar/index.ts +1 -0
- package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
- package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
- package/dist/ui/components/pagination/pagination.test.ts +136 -86
- package/dist/ui/components/popover/index.ts +9 -0
- package/dist/ui/components/popover/popover-panel.test.ts +735 -0
- package/dist/ui/components/popover/popover-panel.ts +603 -0
- package/dist/ui/components/popover/popover.test.ts +286 -0
- package/dist/ui/components/popover/popover.ts +207 -0
- package/dist/ui/components/progress/progress.test.ts +102 -46
- package/dist/ui/components/progress/progress.ts +54 -55
- package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
- package/dist/ui/components/radio-group/radio-group.ts +26 -25
- package/dist/ui/components/scatter-chart/scatter-chart.test.ts +74 -1
- package/dist/ui/components/scroll-area/scroll-area.test.ts +72 -36
- package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
- package/dist/ui/components/search-field/index.ts +15 -0
- package/dist/ui/components/search-field/keyboard.test.ts +147 -0
- package/dist/ui/components/search-field/keyboard.ts +123 -0
- package/dist/ui/components/search-field/search-field.test.ts +549 -0
- package/dist/ui/components/search-field/search-field.ts +628 -0
- package/dist/ui/components/segmented-control/index.ts +8 -0
- package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
- package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
- package/dist/ui/components/select/select.test.ts +138 -78
- package/dist/ui/components/select/select.ts +19 -12
- package/dist/ui/components/separator/separator.test.ts +18 -17
- package/dist/ui/components/sheet/sheet.test.ts +151 -11
- package/dist/ui/components/sheet/sheet.ts +68 -5
- package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
- package/dist/ui/components/skeleton/skeleton.ts +10 -16
- package/dist/ui/components/slider/index.ts +1 -0
- package/dist/ui/components/slider/slider.test.ts +695 -0
- package/dist/ui/components/slider/slider.ts +901 -0
- package/dist/ui/components/spinner/spinner.test.ts +64 -46
- package/dist/ui/components/spinner/spinner.ts +16 -19
- package/dist/ui/components/switch/index.ts +1 -1
- package/dist/ui/components/switch/switch.test.ts +534 -48
- package/dist/ui/components/switch/switch.ts +613 -48
- package/dist/ui/components/tab-bar/index.ts +1 -0
- package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
- package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
- package/dist/ui/components/tabs/index.ts +7 -1
- package/dist/ui/components/tabs/tabs.test.ts +484 -150
- package/dist/ui/components/tabs/tabs.ts +344 -149
- package/dist/ui/components/textarea/textarea.test.ts +206 -100
- package/dist/ui/components/textarea/textarea.ts +16 -13
- package/dist/ui/components/toast/toast.test.ts +434 -350
- package/dist/ui/components/toast/toast.ts +42 -51
- package/dist/ui/components/toggle/index.ts +1 -0
- package/dist/ui/components/toggle/toggle.test.ts +559 -146
- package/dist/ui/components/toggle/toggle.ts +241 -26
- package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
- package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
- package/dist/ui/components/toolbar/index.ts +12 -0
- package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
- package/dist/ui/components/toolbar/toolbar.ts +535 -0
- package/dist/ui/theme/chart-colors.test.ts +8 -3
- package/dist/ui/theme/dark.css +38 -2
- package/dist/ui/theme/default.css +55 -7
- package/dist/ui/theme/icon-colors.test.ts +8 -3
- package/dist/ui/theme/rgb-channels.test.ts +84 -0
- package/dist/ui/theme/tailwind-build.test.ts +114 -0
- package/dist/ui/theme/tailwind-css.test.ts +83 -0
- package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
- package/dist/ui/theme/tailwind-plugin.ts +93 -29
- package/dist/ui/theme/tailwind.css +113 -0
- package/dist/ui/utils/animate.test.ts +13 -1
- package/dist/utils/analyze.d.ts +19 -0
- package/dist/utils/analyze.js +16 -0
- package/dist/utils/analyze.js.map +1 -1
- package/dist/utils/analyze.test.js +14 -1
- package/dist/utils/analyze.test.js.map +1 -1
- package/dist/utils/installed.d.ts +25 -1
- package/dist/utils/installed.js +33 -0
- package/dist/utils/installed.js.map +1 -1
- package/dist/utils/installed.test.js +55 -1
- package/dist/utils/installed.test.js.map +1 -1
- package/dist/utils/resolve-paths.d.ts +4 -4
- package/dist/utils/resolve-paths.js +5 -7
- package/dist/utils/resolve-paths.js.map +1 -1
- package/dist/utils/resolve-paths.test.js +9 -5
- package/dist/utils/resolve-paths.test.js.map +1 -1
- package/dist/utils/tailwind.d.ts +45 -0
- package/dist/utils/tailwind.js +90 -0
- package/dist/utils/tailwind.js.map +1 -0
- package/dist/utils/tailwind.test.d.ts +1 -0
- package/dist/utils/tailwind.test.js +134 -0
- package/dist/utils/tailwind.test.js.map +1 -0
- package/package.json +9 -1
- package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -63
- package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -58
- package/dist/ui/components/button/button-render.test.ts +0 -132
- package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
- package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -59
- package/dist/ui/components/chart/chart-render.test.ts +0 -730
- package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -83
- package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -64
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
DestroyRef,
|
|
4
|
+
ElementRef,
|
|
5
|
+
Injectable,
|
|
6
|
+
Injector,
|
|
7
|
+
Renderer2,
|
|
8
|
+
ViewEncapsulation,
|
|
9
|
+
computed,
|
|
10
|
+
inject,
|
|
11
|
+
signal,
|
|
12
|
+
type Signal,
|
|
13
|
+
type WritableSignal,
|
|
14
|
+
} from '@angular/core';
|
|
15
|
+
import { LYNX_ELEMENTS, LynxSystemInfo } from '@blotch/angular-lynx';
|
|
16
|
+
|
|
17
|
+
import { cn } from '../../utils/cn';
|
|
18
|
+
import { type GlassDesign, injectGlassDesign } from '../glass/glass-design';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A bar over the top of the screen, as the scroll view under it sees it: a
|
|
22
|
+
* navigation bar, or a bar of your own.
|
|
23
|
+
*/
|
|
24
|
+
export type UiTopBar = {
|
|
25
|
+
/** The bar's height below the status bar, in px. Content starts below it. */
|
|
26
|
+
readonly height: Signal<number>;
|
|
27
|
+
/**
|
|
28
|
+
* A large title for the top of the content, which scrolls away with it
|
|
29
|
+
* while the bar shows the title inline, or `null` for none.
|
|
30
|
+
*/
|
|
31
|
+
readonly largeTitle: Signal<string | null>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** A bar over the bottom of the screen, such as a toolbar. */
|
|
35
|
+
export type UiBottomBar = {
|
|
36
|
+
/** The bar's height above the home indicator, in px. */
|
|
37
|
+
readonly height: Signal<number>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What the bars of a screen and the scroll view under them tell each other.
|
|
42
|
+
* The bars say how much room they take, and the scroll view makes that room
|
|
43
|
+
* at its ends and says where its content is: the frosted bar turns opaque
|
|
44
|
+
* once content scrolls under it, and Material's changes color.
|
|
45
|
+
*
|
|
46
|
+
* A service, because neither can inject the other: the bars and the scroll
|
|
47
|
+
* view are siblings in a screen's template. It's provided in the root, which
|
|
48
|
+
* fits one screen at a time. When two screens with bars show at once, such
|
|
49
|
+
* as a sheet with its own navigation bar over a screen with one, give each
|
|
50
|
+
* its own by providing `UiBarState` on the component that holds it:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* @Component({ providers: [UiBarState], … })
|
|
54
|
+
* export class EditNoteSheet {}
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
@Injectable({ providedIn: 'root' })
|
|
58
|
+
export class UiBarState {
|
|
59
|
+
readonly topBar = signal<UiTopBar | null>(null);
|
|
60
|
+
readonly bottomBar = signal<UiBottomBar | null>(null);
|
|
61
|
+
/** The scroll view under the bars (a `ui-bar-scroll`), while there's one. */
|
|
62
|
+
readonly content = signal<object | null>(null);
|
|
63
|
+
|
|
64
|
+
/** Whether content has scrolled under the top bar. */
|
|
65
|
+
readonly scrolledUnderTop = signal(false);
|
|
66
|
+
/**
|
|
67
|
+
* Whether the large title has scrolled under the top bar, so the bar shows
|
|
68
|
+
* the title instead.
|
|
69
|
+
*/
|
|
70
|
+
readonly largeTitleUnder = signal(false);
|
|
71
|
+
/** Whether content continues under the bottom bar. */
|
|
72
|
+
readonly scrolledUnderBottom = signal(false);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Puts `value` in `slot` until `destroyRef`'s component goes, and returns
|
|
76
|
+
* the callback's removal. It clears the slot only if it still holds
|
|
77
|
+
* `value`: when the router swaps screens, the next screen's bar may
|
|
78
|
+
* already be in it.
|
|
79
|
+
*/
|
|
80
|
+
attach<T extends object>(
|
|
81
|
+
slot: WritableSignal<T | null>,
|
|
82
|
+
value: T,
|
|
83
|
+
destroyRef: DestroyRef,
|
|
84
|
+
): () => void {
|
|
85
|
+
slot.set(value);
|
|
86
|
+
return destroyRef.onDestroy(() => {
|
|
87
|
+
if (slot() === value) slot.set(null);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* How far content scrolls before the large title has passed under the bar,
|
|
94
|
+
* which then shows the title instead, in px: past iOS's 34pt bold title in
|
|
95
|
+
* its 52pt band, and Material 3 Expressive's 28sp title in the 48dp its
|
|
96
|
+
* medium flexible app bar adds.
|
|
97
|
+
*/
|
|
98
|
+
const LARGE_TITLE_UNDER: Readonly<Record<GlassDesign, number>> = {
|
|
99
|
+
glass: 44,
|
|
100
|
+
frosted: 44,
|
|
101
|
+
material: 40,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* How the scroll edge effect fades content under a glass bar: how much of it
|
|
106
|
+
* shows (0 to 1) at fractions of the bar's space from the screen's edge.
|
|
107
|
+
* None of it shows under the status bar, little under the bar's controls,
|
|
108
|
+
* so they stay legible over whatever passes beneath them, and all of it from
|
|
109
|
+
* the bar's inner edge, where content rests before it scrolls.
|
|
110
|
+
*/
|
|
111
|
+
const EDGE_FADE: readonly (readonly [at: number, alpha: number])[] = [
|
|
112
|
+
[0, 0],
|
|
113
|
+
[0.7, 0],
|
|
114
|
+
[0.9, 0.12],
|
|
115
|
+
[1, 1],
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The scroll edge effect for a glass bar to draw itself, where the scroll
|
|
120
|
+
* view can't: Android, whose `<scroll-view>` takes no `mask-image` (a plain
|
|
121
|
+
* view does). It's the mask's fade the other way round, in the theme's
|
|
122
|
+
* background color, from the screen's edge `toward` the content, over the
|
|
123
|
+
* bar's own box, which takes the safe area as the scroll view's spacer does.
|
|
124
|
+
* So content fades to the background color rather than to what's behind.
|
|
125
|
+
*/
|
|
126
|
+
export const edgeFadeOverlay = (toward: 'bottom' | 'top'): string => {
|
|
127
|
+
const stops = EDGE_FADE.map(
|
|
128
|
+
([at, alpha]) =>
|
|
129
|
+
`rgba(var(--background-rgb), ${Number((1 - alpha).toFixed(2))}) ${Number((at * 100).toFixed(2))}%`,
|
|
130
|
+
);
|
|
131
|
+
return `linear-gradient(to ${toward}, ${stops.join(', ')})`;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** What Lynx sends with `scroll` and `layoutchange`, as far as this reads. */
|
|
135
|
+
type ScrollDetail = { scrollTop: number; scrollHeight: number };
|
|
136
|
+
type LayoutDetail = { height: number };
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The platform, as SystemInfo names it, or undefined where there's no
|
|
140
|
+
* SystemInfo to read, such as in unit tests, where LynxSystemInfo throws.
|
|
141
|
+
*/
|
|
142
|
+
export const platformOf = (injector: Injector): string | undefined => {
|
|
143
|
+
try {
|
|
144
|
+
return injector.get(LynxSystemInfo).platform;
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A mask stop: content fully hidden (`0`) to fully shown (`1`) at `at`.
|
|
152
|
+
*/
|
|
153
|
+
const stop = (alpha: number, at: string): string =>
|
|
154
|
+
`rgba(0, 0, 0, ${alpha}) ${at}`;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Makes a `<scroll-view>` the content under a screen's bars
|
|
158
|
+
* (`ui-navigation-bar` and `ui-toolbar`), as UIKit adjusts a scroll view for
|
|
159
|
+
* the bars over it:
|
|
160
|
+
*
|
|
161
|
+
* ```html
|
|
162
|
+
* <ui-navigation-bar title="Notes" largeTitle />
|
|
163
|
+
* <scroll-view ui-bar-scroll scroll-orientation="vertical" class="h-full">
|
|
164
|
+
* …
|
|
165
|
+
* </scroll-view>
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* - **Room for the bars:** the content starts below the top bar and the
|
|
169
|
+
* status bar, and ends above the bottom bar and the home indicator, so it
|
|
170
|
+
* scrolls beneath them but rests clear of them.
|
|
171
|
+
* - **The large title** sits at the top of the content and scrolls away with
|
|
172
|
+
* it, and the bar shows the title once it has gone under.
|
|
173
|
+
* - **The scroll edge effect:** under glass bars, content fades out as it
|
|
174
|
+
* passes under them, so their controls stay legible. It's a mask on the
|
|
175
|
+
* scroll view, so it fades to whatever is behind, the screen's background
|
|
176
|
+
* or an image.
|
|
177
|
+
* - **Where the content is,** for the bars: the frosted bars before iOS 26
|
|
178
|
+
* turn opaque, and Material's top app bar changes color, once content
|
|
179
|
+
* scrolls under them.
|
|
180
|
+
*
|
|
181
|
+
* It listens to the scroll view's events itself, as `ui-dock-scroll` does,
|
|
182
|
+
* so it updates the bars only when they should change, rather than running
|
|
183
|
+
* change detection on every event.
|
|
184
|
+
*/
|
|
185
|
+
@Component({
|
|
186
|
+
selector: 'scroll-view[ui-bar-scroll]',
|
|
187
|
+
imports: [LYNX_ELEMENTS],
|
|
188
|
+
encapsulation: ViewEncapsulation.None,
|
|
189
|
+
host: { '[style.mask-image]': 'edgeMask()' },
|
|
190
|
+
// The spacers measure themselves, for the edge effect: their height counts
|
|
191
|
+
// the safe area, which only CSS knows.
|
|
192
|
+
template: `
|
|
193
|
+
<view
|
|
194
|
+
[style.height]="topInset()"
|
|
195
|
+
(bindlayoutchange)="measureTop($event)"
|
|
196
|
+
></view>
|
|
197
|
+
@if (largeTitle(); as title) {
|
|
198
|
+
<view [class]="largeTitleClass()">
|
|
199
|
+
<text accessibility-traits="header" [class]="largeTitleTextClass()">{{
|
|
200
|
+
title
|
|
201
|
+
}}</text>
|
|
202
|
+
</view>
|
|
203
|
+
}
|
|
204
|
+
<ng-content />
|
|
205
|
+
<view
|
|
206
|
+
[style.height]="bottomInset()"
|
|
207
|
+
(bindlayoutchange)="measureBottom($event)"
|
|
208
|
+
></view>
|
|
209
|
+
`,
|
|
210
|
+
})
|
|
211
|
+
export class UiBarScroll {
|
|
212
|
+
readonly #state = inject(UiBarState);
|
|
213
|
+
readonly #renderer = inject(Renderer2);
|
|
214
|
+
readonly #element = inject(ElementRef).nativeElement;
|
|
215
|
+
readonly #design = injectGlassDesign();
|
|
216
|
+
readonly #platform = platformOf(inject(Injector));
|
|
217
|
+
|
|
218
|
+
/** The scroll view's own height and its spacers', from Lynx's layout. */
|
|
219
|
+
protected readonly viewport = signal(0);
|
|
220
|
+
protected readonly topSpace = signal(0);
|
|
221
|
+
protected readonly bottomSpace = signal(0);
|
|
222
|
+
|
|
223
|
+
/** The last scroll position and content height Lynx reported. */
|
|
224
|
+
#scrollTop = 0;
|
|
225
|
+
#scrollHeight = 0;
|
|
226
|
+
|
|
227
|
+
protected readonly attachment = this.#state.attach(
|
|
228
|
+
this.#state.content,
|
|
229
|
+
this,
|
|
230
|
+
inject(DestroyRef),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
protected readonly listeners = [
|
|
234
|
+
this.#renderer.listen(
|
|
235
|
+
this.#element,
|
|
236
|
+
'bindscroll',
|
|
237
|
+
(event: { detail: ScrollDetail }) => this.#scrolled(event.detail),
|
|
238
|
+
),
|
|
239
|
+
// iOS and Android report the content's height when it changes, which a
|
|
240
|
+
// frosted toolbar needs before anything scrolls.
|
|
241
|
+
this.#renderer.listen(
|
|
242
|
+
this.#element,
|
|
243
|
+
'bindcontentsizechanged',
|
|
244
|
+
(event: { detail: ScrollDetail }) => this.#scrolled(event.detail),
|
|
245
|
+
),
|
|
246
|
+
this.#renderer.listen(
|
|
247
|
+
this.#element,
|
|
248
|
+
'bindlayoutchange',
|
|
249
|
+
(event: { detail: LayoutDetail }) => {
|
|
250
|
+
this.viewport.set(event.detail.height);
|
|
251
|
+
this.#update();
|
|
252
|
+
},
|
|
253
|
+
),
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
protected readonly cleanup = inject(DestroyRef).onDestroy(() => {
|
|
257
|
+
for (const remove of this.listeners) remove();
|
|
258
|
+
if (this.#state.content() === null) {
|
|
259
|
+
this.#state.scrolledUnderTop.set(false);
|
|
260
|
+
this.#state.largeTitleUnder.set(false);
|
|
261
|
+
this.#state.scrolledUnderBottom.set(false);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
protected readonly largeTitle = computed(
|
|
266
|
+
() => this.#state.topBar()?.largeTitle() ?? null,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
protected readonly topInset = computed(() => {
|
|
270
|
+
const bar = this.#state.topBar();
|
|
271
|
+
return bar ? `calc(env(safe-area-inset-top) + ${bar.height()}px)` : null;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
protected readonly bottomInset = computed(() => {
|
|
275
|
+
const bar = this.#state.bottomBar();
|
|
276
|
+
return bar ? `calc(env(safe-area-inset-bottom) + ${bar.height()}px)` : null;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
protected readonly largeTitleClass = computed(() =>
|
|
280
|
+
cn(
|
|
281
|
+
'flex-col px-[16px] flex justify-end',
|
|
282
|
+
this.#design() === 'material' ? 'h-[48px] pb-[4px]' : 'h-[52px] pb-[8px]',
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
protected readonly largeTitleTextClass = computed(() =>
|
|
287
|
+
this.#design() === 'material'
|
|
288
|
+
? 'text-[28px] leading-[36px] text-foreground'
|
|
289
|
+
: 'text-[34px] font-bold leading-[41px] text-foreground',
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The scroll edge effect: a mask that fades content out under glass bars,
|
|
294
|
+
* from the spacers' measured heights. The other designs mark the edge with
|
|
295
|
+
* the bar's own background instead, as their platforms do.
|
|
296
|
+
*
|
|
297
|
+
* Native Lynx reads only percentages as gradient stops, so there the stops
|
|
298
|
+
* come from the scroll view's height too. The web preview's scroll view
|
|
299
|
+
* reports no layout (web-elements' `<x-scroll-view>` sends no
|
|
300
|
+
* `layoutchange`), but the browser reads lengths, so there the stops are
|
|
301
|
+
* px from each end. Android's scroll view takes no mask, so there the
|
|
302
|
+
* bars draw the edge themselves (see edgeFadeOverlay).
|
|
303
|
+
*/
|
|
304
|
+
protected readonly edgeMask = computed(() => {
|
|
305
|
+
const top = this.topSpace();
|
|
306
|
+
const bottom = this.bottomSpace();
|
|
307
|
+
const height = this.viewport();
|
|
308
|
+
const web = this.#platform === 'web';
|
|
309
|
+
if (
|
|
310
|
+
this.#design() !== 'glass' ||
|
|
311
|
+
this.#platform === 'Android' ||
|
|
312
|
+
top + bottom <= 0 ||
|
|
313
|
+
(!web && height <= 0)
|
|
314
|
+
) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const percent = (px: number) =>
|
|
318
|
+
`${Math.min(100, Math.max(0, (px / height) * 100)).toFixed(2)}%`;
|
|
319
|
+
const length = (px: number) => `${Number(px.toFixed(2))}px`;
|
|
320
|
+
const fromTop = (px: number) => (web ? length(px) : percent(px));
|
|
321
|
+
const fromBottom = (px: number) =>
|
|
322
|
+
web ? `calc(100% - ${length(px)})` : percent(height - px);
|
|
323
|
+
// Top down to the content, then the bottom bar's fade the other way up.
|
|
324
|
+
const stops = [
|
|
325
|
+
...(top > 0
|
|
326
|
+
? EDGE_FADE.map(([edge, alpha]) => stop(alpha, fromTop(top * edge)))
|
|
327
|
+
: [stop(1, fromTop(0))]),
|
|
328
|
+
...(bottom > 0
|
|
329
|
+
? EDGE_FADE.map(([edge, alpha]) =>
|
|
330
|
+
stop(alpha, fromBottom(bottom * edge)),
|
|
331
|
+
).reverse()
|
|
332
|
+
: [stop(1, fromBottom(0))]),
|
|
333
|
+
];
|
|
334
|
+
return `linear-gradient(to bottom, ${stops.join(', ')})`;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
protected measureTop(event: Event): void {
|
|
338
|
+
this.topSpace.set(
|
|
339
|
+
(event as Event & { detail: LayoutDetail }).detail.height,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
protected measureBottom(event: Event): void {
|
|
344
|
+
this.bottomSpace.set(
|
|
345
|
+
(event as Event & { detail: LayoutDetail }).detail.height,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#scrolled({ scrollTop, scrollHeight }: ScrollDetail): void {
|
|
350
|
+
this.#scrollTop = scrollTop;
|
|
351
|
+
this.#scrollHeight = scrollHeight;
|
|
352
|
+
this.#update();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Tells the bars where the content is. Signals ignore a value they already
|
|
357
|
+
* hold, so the bars change only when content crosses an edge.
|
|
358
|
+
*/
|
|
359
|
+
#update(): void {
|
|
360
|
+
const state = this.#state;
|
|
361
|
+
if (state.content() !== this) return;
|
|
362
|
+
const top = this.#scrollTop;
|
|
363
|
+
state.scrolledUnderTop.set(top > 0);
|
|
364
|
+
state.largeTitleUnder.set(
|
|
365
|
+
!!this.largeTitle() && top >= LARGE_TITLE_UNDER[this.#design()],
|
|
366
|
+
);
|
|
367
|
+
const viewport = this.viewport();
|
|
368
|
+
state.scrolledUnderBottom.set(
|
|
369
|
+
viewport > 0 && top + viewport < this.#scrollHeight - 1,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|