@blotch/dolan 22.2.4 → 22.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/commands/add.test.js +2 -0
  2. package/dist/commands/add.test.js.map +1 -1
  3. package/dist/commands/doctor.js +78 -21
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/doctor.test.js +167 -26
  6. package/dist/commands/doctor.test.js.map +1 -1
  7. package/dist/commands/init.test.js +4 -7
  8. package/dist/commands/init.test.js.map +1 -1
  9. package/dist/commands/update.d.ts +3 -1
  10. package/dist/commands/update.js +78 -15
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/commands/update.test.js +257 -2
  13. package/dist/commands/update.test.js.map +1 -1
  14. package/dist/lockfile.d.ts +10 -0
  15. package/dist/lockfile.js +20 -1
  16. package/dist/lockfile.js.map +1 -1
  17. package/dist/lockfile.test.js +43 -3
  18. package/dist/lockfile.test.js.map +1 -1
  19. package/dist/registry.js +73 -11
  20. package/dist/registry.js.map +1 -1
  21. package/dist/registry.test.js +16 -3
  22. package/dist/registry.test.js.map +1 -1
  23. package/dist/test-utils.d.ts +24 -2
  24. package/dist/test-utils.js +41 -2
  25. package/dist/test-utils.js.map +1 -1
  26. package/dist/ui/boolean-inputs.test.ts +76 -0
  27. package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
  28. package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
  29. package/dist/ui/components/action-sheet/index.ts +1 -0
  30. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
  31. package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
  32. package/dist/ui/components/alert-dialog/index.ts +5 -0
  33. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  34. package/dist/ui/components/badge/badge.ts +2 -1
  35. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
  36. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
  37. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  38. package/dist/ui/components/button/button.test.ts +36 -1
  39. package/dist/ui/components/button/button.ts +17 -7
  40. package/dist/ui/components/button-group/button-group.test.ts +53 -9
  41. package/dist/ui/components/button-group/button-group.ts +67 -21
  42. package/dist/ui/components/card/card.test.ts +16 -0
  43. package/dist/ui/components/card/card.ts +2 -1
  44. package/dist/ui/components/chart/chart-presets.ts +6 -6
  45. package/dist/ui/components/chart/chart.test.ts +14 -0
  46. package/dist/ui/components/chart/chart.ts +10 -3
  47. package/dist/ui/components/checkbox/checkbox.ts +2 -1
  48. package/dist/ui/components/collapsible/collapsible.ts +2 -1
  49. package/dist/ui/components/dialog/dialog.ts +6 -6
  50. package/dist/ui/components/dock/dock-scroll.ts +9 -0
  51. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  52. package/dist/ui/components/dock/dock.test.ts +4 -1
  53. package/dist/ui/components/dock/dock.ts +529 -51
  54. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  55. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  56. package/dist/ui/components/dynamic-color/index.ts +5 -0
  57. package/dist/ui/components/glass/glass.test.ts +39 -1
  58. package/dist/ui/components/glass/glass.ts +87 -17
  59. package/dist/ui/components/glass/index.ts +1 -0
  60. package/dist/ui/components/icon/icon-theme.test.ts +28 -0
  61. package/dist/ui/components/icon/icon-theme.ts +38 -7
  62. package/dist/ui/components/input/input.ts +2 -1
  63. package/dist/ui/components/label/label.ts +8 -2
  64. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  65. package/dist/ui/components/list/list.ts +5 -4
  66. package/dist/ui/components/menu/menu-popup.test.ts +18 -14
  67. package/dist/ui/components/menu/menu-popup.ts +24 -3
  68. package/dist/ui/components/menu/menu.test.ts +99 -5
  69. package/dist/ui/components/menu/menu.ts +74 -34
  70. package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
  71. package/dist/ui/components/navigation-bar/index.ts +1 -0
  72. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  73. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  74. package/dist/ui/components/popover/index.ts +9 -0
  75. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  76. package/dist/ui/components/popover/popover-panel.ts +603 -0
  77. package/dist/ui/components/popover/popover.test.ts +286 -0
  78. package/dist/ui/components/popover/popover.ts +207 -0
  79. package/dist/ui/components/progress/progress.ts +2 -1
  80. package/dist/ui/components/radio-group/radio-group.ts +2 -1
  81. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  82. package/dist/ui/components/search-field/index.ts +15 -0
  83. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  84. package/dist/ui/components/search-field/keyboard.ts +123 -0
  85. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  86. package/dist/ui/components/search-field/search-field.ts +628 -0
  87. package/dist/ui/components/segmented-control/index.ts +8 -0
  88. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  89. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  90. package/dist/ui/components/select/select.ts +2 -1
  91. package/dist/ui/components/sheet/sheet.test.ts +90 -3
  92. package/dist/ui/components/sheet/sheet.ts +59 -4
  93. package/dist/ui/components/slider/index.ts +1 -0
  94. package/dist/ui/components/slider/slider.test.ts +695 -0
  95. package/dist/ui/components/slider/slider.ts +901 -0
  96. package/dist/ui/components/switch/index.ts +1 -1
  97. package/dist/ui/components/switch/switch.test.ts +464 -48
  98. package/dist/ui/components/switch/switch.ts +613 -43
  99. package/dist/ui/components/tabs/index.ts +7 -1
  100. package/dist/ui/components/tabs/tabs.test.ts +278 -2
  101. package/dist/ui/components/tabs/tabs.ts +233 -30
  102. package/dist/ui/components/textarea/textarea.ts +2 -1
  103. package/dist/ui/components/toggle/index.ts +1 -0
  104. package/dist/ui/components/toggle/toggle.test.ts +310 -4
  105. package/dist/ui/components/toggle/toggle.ts +241 -26
  106. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  107. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  108. package/dist/ui/components/toolbar/index.ts +12 -0
  109. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  110. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  111. package/dist/utils/analyze.d.ts +10 -5
  112. package/dist/utils/analyze.js +3 -2
  113. package/dist/utils/analyze.js.map +1 -1
  114. package/dist/utils/installed.d.ts +13 -1
  115. package/dist/utils/installed.js +17 -0
  116. package/dist/utils/installed.js.map +1 -1
  117. package/dist/utils/installed.test.js +39 -1
  118. package/dist/utils/installed.test.js.map +1 -1
  119. package/dist/utils/tailwind.d.ts +25 -5
  120. package/dist/utils/tailwind.js +53 -14
  121. package/dist/utils/tailwind.js.map +1 -1
  122. package/dist/utils/tailwind.test.js +85 -22
  123. package/dist/utils/tailwind.test.js.map +1 -1
  124. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ UiToolbar,
3
+ UiToolbarGroup,
4
+ UiToolbarItem,
5
+ UiToolbarSpacer,
6
+ } from './toolbar';
7
+ export {
8
+ UiBarScroll,
9
+ UiBarState,
10
+ type UiBottomBar,
11
+ type UiTopBar,
12
+ } from './bar-scroll';