@swiftbrowser/web 0.1.0 → 0.3.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 +141 -63
- package/app/index.html +21 -10
- package/node/dist/node/apps.d.ts +16 -0
- package/node/dist/node/apps.d.ts.map +1 -1
- package/node/dist/node/apps.js +9 -0
- package/node/dist/node/apps.js.map +1 -1
- package/node/dist/node/config.d.ts +14 -1
- package/node/dist/node/config.d.ts.map +1 -1
- package/node/dist/node/config.js +20 -3
- package/node/dist/node/config.js.map +1 -1
- package/node/dist/node/examples.d.ts +2 -1
- package/node/dist/node/examples.d.ts.map +1 -1
- package/node/dist/node/examples.js +12 -2
- package/node/dist/node/examples.js.map +1 -1
- package/node/dist/plugins/app-manifests.d.ts +4 -1
- package/node/dist/plugins/app-manifests.d.ts.map +1 -1
- package/node/dist/plugins/app-manifests.js +3 -2
- package/node/dist/plugins/app-manifests.js.map +1 -1
- package/node/dist/plugins/asset-catalogs.d.ts +15 -3
- package/node/dist/plugins/asset-catalogs.d.ts.map +1 -1
- package/node/dist/plugins/asset-catalogs.js +111 -13
- package/node/dist/plugins/asset-catalogs.js.map +1 -1
- package/node/dist/plugins/included-sites.d.ts +26 -0
- package/node/dist/plugins/included-sites.d.ts.map +1 -0
- package/node/dist/plugins/included-sites.js +70 -0
- package/node/dist/plugins/included-sites.js.map +1 -0
- package/node/dist/plugins/pdfImages.d.ts +27 -0
- package/node/dist/plugins/pdfImages.d.ts.map +1 -0
- package/node/dist/plugins/pdfImages.js +145 -0
- package/node/dist/plugins/pdfImages.js.map +1 -0
- package/node/dist/plugins/swift-wasm.d.ts +19 -1
- package/node/dist/plugins/swift-wasm.d.ts.map +1 -1
- package/node/dist/plugins/swift-wasm.js +45 -7
- package/node/dist/plugins/swift-wasm.js.map +1 -1
- package/node/dist/plugins/symbolTemplates.d.ts +22 -0
- package/node/dist/plugins/symbolTemplates.d.ts.map +1 -0
- package/node/dist/plugins/symbolTemplates.js +83 -0
- package/node/dist/plugins/symbolTemplates.js.map +1 -0
- package/package.json +4 -2
- package/plugins/app-manifests.ts +3 -2
- package/plugins/asset-catalogs.ts +119 -14
- package/plugins/included-sites.ts +74 -0
- package/plugins/pdfImages.ts +163 -0
- package/plugins/swift-wasm.ts +52 -6
- package/plugins/symbolTemplates.ts +88 -0
- package/public/_headers +7 -0
- package/public/app/oauth-callback.html +62 -0
- package/src/animation.ts +235 -8
- package/src/appFiles.ts +201 -0
- package/src/base64.ts +43 -0
- package/src/bitmapPlan.ts +143 -0
- package/src/bitmapStore.ts +206 -0
- package/src/bitmapWorker.ts +41 -0
- package/src/bitmapWorkerProtocol.ts +15 -0
- package/src/bitmaps.ts +283 -0
- package/src/blockedHosts.ts +29 -0
- package/src/canvasDrawing.ts +361 -0
- package/src/chartDom.ts +2 -2
- package/src/chromeColor.ts +179 -0
- package/src/clipboard.ts +78 -0
- package/src/colorMultiply.ts +93 -0
- package/src/colorPicker.ts +44 -0
- package/src/corsProxy.ts +45 -0
- package/src/gaugeRing.ts +74 -0
- package/src/gif.ts +297 -0
- package/src/hostRequests.ts +426 -0
- package/src/imageOps.ts +592 -0
- package/src/interaction.ts +68 -3
- package/src/landing.ts +2 -2
- package/src/largeTitle.ts +79 -0
- package/src/layout/chart.ts +9 -3
- package/src/layout/engine.ts +681 -174
- package/src/layout/index.ts +3 -1
- package/src/layout/text.ts +217 -1
- package/src/layout/types.ts +31 -0
- package/src/layout/typography.ts +24 -7
- package/src/layout/video.ts +24 -0
- package/src/layoutDom.ts +66 -6
- package/src/main.ts +127 -4
- package/src/media.ts +132 -0
- package/src/mock.ts +4 -3
- package/src/notifications.ts +109 -0
- package/src/pathShape.ts +156 -0
- package/src/protocol.ts +782 -13
- package/src/renderer.ts +603 -44
- package/src/richText.ts +65 -0
- package/src/scanner.ts +234 -0
- package/src/scrollTo.ts +123 -0
- package/src/styles.css +521 -100
- package/src/styles.ts +61 -0
- package/src/swiftData.ts +128 -0
- package/src/symbols.ts +658 -28
- package/src/translation.ts +126 -0
- package/src/video.ts +299 -0
- package/src/vite-env.d.ts +6 -0
- package/src/wasm.ts +227 -32
- package/src/webAuth.ts +267 -0
- package/src/webSocket.ts +148 -0
- package/src/zoneinfo.ts +269 -0
package/src/interaction.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* 2. the wrappers between the target and the root, innermost first: a
|
|
10
10
|
* `gesture` (drag, long press), a `contextmenu` (long press) and a swipe
|
|
11
11
|
* row (`swipeactions`, or a deletable `listrow` without one);
|
|
12
|
-
* 3. the sheet card (drag down to dismiss)
|
|
12
|
+
* 3. the sheet card (drag down to dismiss): inside a vertical scroll view
|
|
13
|
+
* only while that view is at its top, as UIKit hands a sheet's scroll
|
|
14
|
+
* view's downward drag to the sheet there (a touch's first downward move
|
|
15
|
+
* is kept from scrolling natively so the pointer reaches the sheet);
|
|
13
16
|
* 4. pull to refresh on a `refreshable` scroller at its top.
|
|
14
17
|
*
|
|
15
18
|
* Nothing owns the pointer at first. On each move the candidates are asked in
|
|
@@ -24,6 +27,9 @@
|
|
|
24
27
|
* reach the renderer's click handler untouched, so controls inside gestures
|
|
25
28
|
* and rows keep working.
|
|
26
29
|
*
|
|
30
|
+
* A horizontal scroll view keeps horizontal motion (touch or wheel) from the
|
|
31
|
+
* swipe rows around it, as UIKit's inner scroll view wins over a row's swipe.
|
|
32
|
+
*
|
|
27
33
|
* Horizontal wheel scrolling slides swipe rows and vertical wheel at the
|
|
28
34
|
* top of a `refreshable` scroller pulls to refresh; touch moves are read
|
|
29
35
|
* directly for pull to refresh (the browser cancels the pointer once it
|
|
@@ -185,6 +191,8 @@ export class InteractionController {
|
|
|
185
191
|
private wheelPull: { scroller: HTMLElement; pulled: number; sent: boolean; timer: number } | null = null;
|
|
186
192
|
/** Touch-driven pull (the browser cancels the pointer once the list scrolls). */
|
|
187
193
|
private touchPull: { scroller: HTMLElement; id: ElementId; startY: number; sent: boolean } | null = null;
|
|
194
|
+
/** A touch in a sheet's scroll view at its top: its first move decides who scrolls. */
|
|
195
|
+
private touchSheet: { scroller: HTMLElement; startY: number } | null = null;
|
|
188
196
|
|
|
189
197
|
constructor(private readonly host: InteractionHost) {
|
|
190
198
|
const root = host.root;
|
|
@@ -195,6 +203,7 @@ export class InteractionController {
|
|
|
195
203
|
root.addEventListener('scroll', this.onScroll, true);
|
|
196
204
|
root.addEventListener('touchstart', this.onTouchStart, { passive: true });
|
|
197
205
|
root.addEventListener('touchmove', this.onTouchMove, { passive: true });
|
|
206
|
+
root.addEventListener('touchmove', this.onSheetTouchMove, { passive: false });
|
|
198
207
|
root.addEventListener('touchend', this.onTouchEnd, { passive: true });
|
|
199
208
|
root.addEventListener('touchcancel', this.onTouchEnd, { passive: true });
|
|
200
209
|
document.addEventListener('pointerdown', this.onDocumentPointerDown, true);
|
|
@@ -209,6 +218,7 @@ export class InteractionController {
|
|
|
209
218
|
root.removeEventListener('scroll', this.onScroll, true);
|
|
210
219
|
root.removeEventListener('touchstart', this.onTouchStart);
|
|
211
220
|
root.removeEventListener('touchmove', this.onTouchMove);
|
|
221
|
+
root.removeEventListener('touchmove', this.onSheetTouchMove);
|
|
212
222
|
root.removeEventListener('touchend', this.onTouchEnd);
|
|
213
223
|
root.removeEventListener('touchcancel', this.onTouchEnd);
|
|
214
224
|
document.removeEventListener('pointerdown', this.onDocumentPointerDown, true);
|
|
@@ -379,8 +389,11 @@ export class InteractionController {
|
|
|
379
389
|
out.push({ kind: 'swipeback', screen, id, width: rect.width || screen.offsetWidth, below: below?.classList.contains('sb-navscreen') ? below : null });
|
|
380
390
|
}
|
|
381
391
|
}
|
|
392
|
+
// A horizontal scroll view takes horizontal motion from the rows around it.
|
|
393
|
+
let inHorizontalScroller = false;
|
|
382
394
|
for (let el: HTMLElement | null = target; el && el !== this.host.root; el = el.parentElement) {
|
|
383
395
|
const id = Number(el.dataset[ID_ATTR]);
|
|
396
|
+
if (isHorizontalScroller(el)) inHorizontalScroller = true;
|
|
384
397
|
if (el.classList.contains('sb-gesture') && Number.isInteger(id)) {
|
|
385
398
|
const node = this.host.nodeOf(id);
|
|
386
399
|
const props = node?.props ?? {};
|
|
@@ -392,14 +405,16 @@ export class InteractionController {
|
|
|
392
405
|
out.push({ kind: 'gesture', el, id, drag, longPress, minDistance, minDuration });
|
|
393
406
|
} else if (el.classList.contains('sb-contextmenu') && Number.isInteger(id)) {
|
|
394
407
|
out.push({ kind: 'contextmenu', el, id });
|
|
395
|
-
} else if (el.classList.contains('sb-swipeactions') || el.classList.contains('sb-listrow')) {
|
|
408
|
+
} else if ((el.classList.contains('sb-swipeactions') || el.classList.contains('sb-listrow')) && !inHorizontalScroller) {
|
|
396
409
|
const host = this.swipeHostFor(el);
|
|
397
410
|
if (host) out.push({ kind: 'swipe', host });
|
|
398
411
|
}
|
|
399
412
|
}
|
|
400
413
|
const card = target.closest<HTMLElement>('.sb-sheet-card');
|
|
401
414
|
const sheet = card?.parentElement;
|
|
402
|
-
|
|
415
|
+
// A scroll view in the sheet that is scrolled down keeps the drag.
|
|
416
|
+
const sheetScroller = card ? verticalScrollerIn(target, card) : null;
|
|
417
|
+
if (card && sheet?.classList.contains('sb-sheet') && sheet.dataset['sbSheetPosition'] !== 'below' && !(sheetScroller && sheetScroller.scrollTop > 0)) {
|
|
403
418
|
const id = Number(sheet.dataset[ID_ATTR]);
|
|
404
419
|
const detents = (sheet.dataset['sbDetents'] ?? '').split(' ');
|
|
405
420
|
// A full-screen cover (`detents: ["full"]`) has no drag to dismiss.
|
|
@@ -863,6 +878,10 @@ export class InteractionController {
|
|
|
863
878
|
const rowEl = target.closest<HTMLElement>('.sb-swipeactions, .sb-listrow');
|
|
864
879
|
const host = rowEl ? this.swipeHostFor(rowEl) : null;
|
|
865
880
|
if (!host) return;
|
|
881
|
+
// A horizontal scroll view in the row scrolls instead.
|
|
882
|
+
for (let el: HTMLElement | null = target; el && el !== rowEl; el = el.parentElement) {
|
|
883
|
+
if (isHorizontalScroller(el)) return;
|
|
884
|
+
}
|
|
866
885
|
let state = this.wheelSwipe;
|
|
867
886
|
if (!state || state.host.el !== host.el) {
|
|
868
887
|
if (state) this.finishWheelSwipe();
|
|
@@ -936,7 +955,15 @@ export class InteractionController {
|
|
|
936
955
|
|
|
937
956
|
private readonly onTouchStart = (event: TouchEvent): void => {
|
|
938
957
|
const touch = event.touches[0];
|
|
958
|
+
this.touchSheet = null;
|
|
939
959
|
if (!touch || event.touches.length !== 1) return;
|
|
960
|
+
const target = event.target as HTMLElement;
|
|
961
|
+
const card = target.closest<HTMLElement>('.sb-sheet-card');
|
|
962
|
+
const sheetScroller = card ? verticalScrollerIn(target, card) : null;
|
|
963
|
+
// A refreshable scroller keeps its pull to refresh.
|
|
964
|
+
if (sheetScroller && sheetScroller.scrollTop <= 0 && sheetScroller.dataset['sbRefreshable'] === undefined) {
|
|
965
|
+
this.touchSheet = { scroller: sheetScroller, startY: touch.clientY };
|
|
966
|
+
}
|
|
940
967
|
const scroller = this.refreshableScrollerOf(event.target as Element);
|
|
941
968
|
if (!scroller || scroller.el.scrollTop > 0) return;
|
|
942
969
|
this.touchPull = { scroller: scroller.el, id: scroller.id, startY: touch.clientY, sent: false };
|
|
@@ -959,6 +986,25 @@ export class InteractionController {
|
|
|
959
986
|
|
|
960
987
|
private readonly onTouchEnd = (): void => {
|
|
961
988
|
this.touchPull = null;
|
|
989
|
+
this.touchSheet = null;
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* The first move of a touch in a sheet's scroll view at its top: downwards,
|
|
994
|
+
* it is kept from scrolling (or overscrolling) the view, so the pointer
|
|
995
|
+
* stays with the sheet's drag; upwards, the view scrolls as usual.
|
|
996
|
+
*/
|
|
997
|
+
private readonly onSheetTouchMove = (event: TouchEvent): void => {
|
|
998
|
+
const pending = this.touchSheet;
|
|
999
|
+
const touch = event.touches[0];
|
|
1000
|
+
if (!pending || !touch) return;
|
|
1001
|
+
const dy = touch.clientY - pending.startY;
|
|
1002
|
+
if (dy === 0) return;
|
|
1003
|
+
if (dy > 0 && pending.scroller.scrollTop <= 0 && event.cancelable) {
|
|
1004
|
+
event.preventDefault();
|
|
1005
|
+
return; // keep deciding: every move of this drag stays with the sheet
|
|
1006
|
+
}
|
|
1007
|
+
this.touchSheet = null;
|
|
962
1008
|
};
|
|
963
1009
|
|
|
964
1010
|
private readonly onContextMenu = (event: MouseEvent): void => {
|
|
@@ -1077,6 +1123,25 @@ function clearSelectionIn(root: HTMLElement): void {
|
|
|
1077
1123
|
if (anchor && root.contains(anchor)) selection.removeAllRanges();
|
|
1078
1124
|
}
|
|
1079
1125
|
|
|
1126
|
+
/** A scroll view that scrolls horizontally (`ScrollView(.horizontal)` or both axes). */
|
|
1127
|
+
/** The innermost vertically scrolling `list` / `scrollview` from `target` up to `limit` (excluded). */
|
|
1128
|
+
function verticalScrollerIn(target: HTMLElement, limit: HTMLElement): HTMLElement | null {
|
|
1129
|
+
for (let el: HTMLElement | null = target; el && el !== limit; el = el.parentElement) {
|
|
1130
|
+
if (el.classList.contains('sb-list')) return el;
|
|
1131
|
+
if (el.classList.contains('sb-scrollview')) {
|
|
1132
|
+
const axes = el.dataset['sbAxes'];
|
|
1133
|
+
if (axes === 'vertical' || axes === 'both' || axes === undefined) return el;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function isHorizontalScroller(el: HTMLElement): boolean {
|
|
1140
|
+
if (!el.classList.contains('sb-scrollview')) return false;
|
|
1141
|
+
const axes = el.dataset['sbAxes'];
|
|
1142
|
+
return axes === 'horizontal' || axes === 'both';
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1080
1145
|
/** The content slot of a swipe row (`swipeactions` / `listrow`). */
|
|
1081
1146
|
function contentOf(rowEl: HTMLElement): HTMLElement | null {
|
|
1082
1147
|
return rowEl.querySelector<HTMLElement>(':scope > .sb-swipe-content');
|
package/src/landing.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { toString as qrToString } from 'qrcode';
|
|
12
12
|
|
|
13
|
-
const REPO_URL = 'https://github.com/
|
|
13
|
+
const REPO_URL = 'https://github.com/noya-app/SwiftBrowser';
|
|
14
14
|
|
|
15
15
|
/** The card's subtitle: the spec's description, else a generic line. */
|
|
16
16
|
export function describeApp(app: Pick<SBLandingApp, 'name' | 'description'>): string {
|
|
@@ -132,7 +132,7 @@ function renderBuildInfo(): void {
|
|
|
132
132
|
footer.append(time);
|
|
133
133
|
}
|
|
134
134
|
footer.append(' · ');
|
|
135
|
-
const repo = el('a', 'build-repo', '
|
|
135
|
+
const repo = el('a', 'build-repo', 'noya-app/SwiftBrowser');
|
|
136
136
|
repo.href = REPO_URL;
|
|
137
137
|
footer.append(repo);
|
|
138
138
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A navigation screen's large title scrolls with the screen's root list or
|
|
3
|
+
* scroll view (iOS): the engine starts that scroll view under the bar, insets
|
|
4
|
+
* its content by the title row and marks it with a `largeTitle` chrome rect
|
|
5
|
+
* (layout/engine.ts, `placeNavScreen`). Here the title follows the scroll
|
|
6
|
+
* offset and fades out at the bar's edge, as the scrolled content does
|
|
7
|
+
* (`data-sb-scrolled`, iOS 26's scroll edge effect), and once it has gone
|
|
8
|
+
* under the bar the screen gets `data-sb-collapsed`, which shows the inline
|
|
9
|
+
* title (styles.css). Pulled past the top (a touch overscroll), the title
|
|
10
|
+
* moves down with the content (iOS 26 no longer grows it).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The scroll offset past which the title has gone under the bar. */
|
|
14
|
+
const COLLAPSE_OFFSET = 44;
|
|
15
|
+
/** How far below the bar's edge content fades in (styles.css matches). */
|
|
16
|
+
const EDGE_FADE = 16;
|
|
17
|
+
|
|
18
|
+
const bound = new WeakSet<HTMLElement>();
|
|
19
|
+
|
|
20
|
+
function largeTitleOf(scroller: HTMLElement): { screen: HTMLElement; title: HTMLElement } | null {
|
|
21
|
+
const screen = scroller.closest<HTMLElement>('.sb-navscreen');
|
|
22
|
+
const title = screen?.querySelector<HTMLElement>(':scope > .sb-navscreen-body > .sb-navbar-large-title');
|
|
23
|
+
return screen && title ? { screen, title } : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Called for every list and scroll view the layout reaches: `scrolls` is
|
|
28
|
+
* whether the engine marked it as carrying its screen's large title.
|
|
29
|
+
*/
|
|
30
|
+
export function bindLargeTitle(scroller: HTMLElement, scrolls: boolean): void {
|
|
31
|
+
if (!scrolls) {
|
|
32
|
+
if (scroller.dataset['sbScrollsTitle'] !== undefined) {
|
|
33
|
+
delete scroller.dataset['sbScrollsTitle'];
|
|
34
|
+
delete scroller.dataset['sbScrolled'];
|
|
35
|
+
const parts = largeTitleOf(scroller);
|
|
36
|
+
if (parts) reset(parts.screen, parts.title);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
scroller.dataset['sbScrollsTitle'] = '';
|
|
41
|
+
if (!bound.has(scroller)) {
|
|
42
|
+
bound.add(scroller);
|
|
43
|
+
scroller.addEventListener('scroll', () => syncLargeTitle(scroller), { passive: true });
|
|
44
|
+
}
|
|
45
|
+
syncLargeTitle(scroller);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function syncLargeTitle(scroller: HTMLElement): void {
|
|
49
|
+
if (scroller.dataset['sbScrollsTitle'] === undefined) return;
|
|
50
|
+
const parts = largeTitleOf(scroller);
|
|
51
|
+
if (!parts) return;
|
|
52
|
+
const { screen, title } = parts;
|
|
53
|
+
const offset = scroller.scrollTop;
|
|
54
|
+
const s = title.style;
|
|
55
|
+
if (offset > 0) {
|
|
56
|
+
s.transform = `translateY(${-offset}px)`;
|
|
57
|
+
// Hide what has gone above the title's row (under the bar), fading in
|
|
58
|
+
// below the edge.
|
|
59
|
+
setMask(title, `linear-gradient(to bottom, transparent ${offset}px, #000 ${offset + EDGE_FADE}px)`);
|
|
60
|
+
scroller.dataset['sbScrolled'] = '';
|
|
61
|
+
} else {
|
|
62
|
+
s.transform = offset < 0 ? `translateY(${-offset}px)` : '';
|
|
63
|
+
setMask(title, '');
|
|
64
|
+
delete scroller.dataset['sbScrolled'];
|
|
65
|
+
}
|
|
66
|
+
if (offset > COLLAPSE_OFFSET) screen.dataset['sbCollapsed'] = '';
|
|
67
|
+
else delete screen.dataset['sbCollapsed'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function setMask(el: HTMLElement, mask: string): void {
|
|
71
|
+
el.style.setProperty('mask-image', mask);
|
|
72
|
+
el.style.setProperty('-webkit-mask-image', mask);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function reset(screen: HTMLElement, title: HTMLElement): void {
|
|
76
|
+
title.style.transform = '';
|
|
77
|
+
setMask(title, '');
|
|
78
|
+
delete screen.dataset['sbCollapsed'];
|
|
79
|
+
}
|
package/src/layout/chart.ts
CHANGED
|
@@ -14,6 +14,8 @@ export const CHART_DEFAULT_WIDTH = 300;
|
|
|
14
14
|
export const CHART_DEFAULT_HEIGHT = 200;
|
|
15
15
|
/** Gap between an axis label and the plot edge (the ticks sit inside it). */
|
|
16
16
|
export const CHART_AXIS_PAD = 6;
|
|
17
|
+
/** How far a leading-anchored x label (a date's) starts after its tick. */
|
|
18
|
+
export const CHART_LEADING_LABEL_OFFSET = 2;
|
|
17
19
|
export const CHART_TICK_LENGTH = 4;
|
|
18
20
|
/** Legend glyph: a `symbolSize`-px symbol, or an 18 × 3 line when the item has no symbol. */
|
|
19
21
|
export const CHART_LEGEND_GLYPH = 10;
|
|
@@ -32,8 +34,11 @@ export type ChartItemPosition = 'top' | 'bottom' | 'leading' | 'trailing' | 'ove
|
|
|
32
34
|
|
|
33
35
|
export interface ChartAxisInput {
|
|
34
36
|
position: ChartAxisPosition;
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Text labels: unit position and measured width (height is `labelHeight`);
|
|
39
|
+
* `leading`: an x label starts at its tick (a date's) instead of centering on it.
|
|
40
|
+
*/
|
|
41
|
+
labels: { at: number; width: number; leading?: boolean }[];
|
|
37
42
|
/** Line height of the axis font. */
|
|
38
43
|
labelHeight: number;
|
|
39
44
|
/** View labels (`xLabel` / `yLabel` children) at their ideal size. */
|
|
@@ -186,7 +191,8 @@ export function layoutChart(input: ChartLayoutInput): ChartGeometry {
|
|
|
186
191
|
const top = (h: number) => (xTop ? plot.y - CHART_AXIS_PAD - h : plot.y + plot.height + CHART_AXIS_PAD);
|
|
187
192
|
for (const label of xAxis.labels) {
|
|
188
193
|
const h = xAxis.labelHeight;
|
|
189
|
-
const
|
|
194
|
+
const start = label.leading ? mapX(plot, label.at) + CHART_LEADING_LABEL_OFFSET : mapX(plot, label.at) - label.width / 2;
|
|
195
|
+
const x = clampTo(start, label.width, 0, width);
|
|
190
196
|
xLabels.push({ x, y: top(h), width: label.width, height: h });
|
|
191
197
|
}
|
|
192
198
|
for (const view of xAxis.views) {
|