panelui-native 0.13.0 → 0.14.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 +39 -23
- package/lib/module/components/button/index.js +12 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/chip/index.js +313 -0
- package/lib/module/components/chip/index.js.map +1 -0
- package/lib/module/components/frame/index.js +41 -39
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +772 -0
- package/lib/module/components/heatmap-chart/index.js.map +1 -0
- package/lib/module/components/input/index.js +4 -0
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/scroll-canvas/index.js +175 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -0
- package/lib/module/components/scroll-text/index.js +213 -0
- package/lib/module/components/scroll-text/index.js.map +1 -0
- package/lib/module/components/select/index.js +20 -24
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +2 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/switch/index.js +15 -17
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/thinking-orb/index.js +741 -0
- package/lib/module/components/thinking-orb/index.js.map +1 -0
- package/lib/module/components/toggle-button/index.js +12 -3
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +99 -59
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +75 -0
- package/lib/module/hooks/use-reveal-progress.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +21 -2
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/scroll-progress.js +114 -0
- package/lib/module/primitives/scroll-progress.js.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts +3 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/chip/index.d.ts +304 -0
- package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +15 -13
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/input/index.d.ts +19 -4
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
- package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
- package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +2 -1
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +15 -7
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
- package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/button/index.tsx +13 -11
- package/src/components/frame/index.tsx +39 -50
- package/src/components/heatmap-chart/index.tsx +1024 -0
- package/src/components/input/index.tsx +23 -4
- package/src/components/scroll-canvas/index.tsx +220 -0
- package/src/components/scroll-text/index.tsx +277 -0
- package/src/components/select/index.tsx +4 -9
- package/src/components/slider/index.tsx +4 -7
- package/src/components/switch/index.tsx +5 -8
- package/src/components/thinking-orb/index.tsx +792 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-keyboard-avoidance.ts +111 -65
- package/src/hooks/use-reveal-progress.ts +106 -0
- package/src/index.ts +38 -0
- package/src/native/index.ts +15 -7
- package/src/primitives/keyboard-avoider.tsx +41 -4
- package/src/primitives/scroll-progress.tsx +167 -0
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HeatmapChart — a calendar of bins, shaded by how much happened in each.
|
|
5
|
+
*
|
|
6
|
+
* The contribution grid: one column per period (usually a week), one row per
|
|
7
|
+
* bin inside it (usually a weekday), and a colour ramp carrying the count. It
|
|
8
|
+
* answers "when was this busy" at a glance, which no line can — a year of daily
|
|
9
|
+
* numbers plotted as a series is a hairball, and as a grid it is a pattern.
|
|
10
|
+
*
|
|
11
|
+
* Composed rather than configured, so a chart that wants no axis simply does
|
|
12
|
+
* not have one:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <HeatmapChart data={weeks}>
|
|
16
|
+
* <HeatmapChart.YAxis />
|
|
17
|
+
* <HeatmapChart.XAxis />
|
|
18
|
+
* <HeatmapChart.Cells />
|
|
19
|
+
* <HeatmapChart.Tooltip />
|
|
20
|
+
* <HeatmapChart.Legend />
|
|
21
|
+
* </HeatmapChart>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The parts sort themselves into a real layout rather than stacking over the
|
|
25
|
+
* plot. That is the difference from a line chart, where the axis floats over
|
|
26
|
+
* the drawing: here the labels and the legend sit *beside* and *below* the
|
|
27
|
+
* grid, so they take up room, and the grid is sized with them accounted for.
|
|
28
|
+
* Only the cells and the rules between them are SVG; every label is a React
|
|
29
|
+
* Native view, because SVG text ignores the platform's text scaling and the
|
|
30
|
+
* theme's font.
|
|
31
|
+
*
|
|
32
|
+
* The ramp is one colour at five opacities rather than five colours. A heatmap
|
|
33
|
+
* reads as *more* and *less* of one thing, and five distinct hues read as five
|
|
34
|
+
* different things — which is what the `--color-chart-*` tokens are for, and
|
|
35
|
+
* why they are not used here. The base is `--color-chart-1`, so the ramp
|
|
36
|
+
* follows the theme, and `levelColors` replaces it outright when a brand needs
|
|
37
|
+
* its own.
|
|
38
|
+
*/
|
|
39
|
+
import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
40
|
+
import { StyleSheet, View } from 'react-native';
|
|
41
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
42
|
+
import Animated, { Easing, runOnJS, useAnimatedProps, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
43
|
+
import Svg, { ClipPath, Defs, G, Line as SvgLine, Rect } from 'react-native-svg';
|
|
44
|
+
import { useCSSVariable } from 'uniwind';
|
|
45
|
+
import { Text } from "../../primitives/text.js";
|
|
46
|
+
import { cn } from "../../utils/cn.js";
|
|
47
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
48
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rows in a week — the default, and what the calendar helper and the weekday
|
|
52
|
+
* labels assume. The grid itself takes a `rows` prop, so an hourly band or a
|
|
53
|
+
* single-row uptime strip is the same component with a different number.
|
|
54
|
+
*/
|
|
55
|
+
const DAYS_IN_WEEK = 7;
|
|
56
|
+
|
|
57
|
+
/** Opacity of the base colour at each activity level. Index 0 is "nothing". */
|
|
58
|
+
const LEVEL_OPACITY = [1, 0.28, 0.5, 0.74, 1];
|
|
59
|
+
|
|
60
|
+
/** Room left for the weekday labels when the y-axis does not ask for its own. */
|
|
61
|
+
const DEFAULT_AXIS_WIDTH = 26;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Height assumed for the readout when placing it above a cell. It is a single
|
|
65
|
+
* line of `xs` text in a padded box, so it does not vary — and measuring it
|
|
66
|
+
* would put the tooltip a frame behind the finger.
|
|
67
|
+
*/
|
|
68
|
+
const TOOLTIP_HEIGHT = 26;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How long the readout waits before claiming the touch, in milliseconds.
|
|
72
|
+
*
|
|
73
|
+
* Long enough that a swipe scrolls the chart instead of reading it, short
|
|
74
|
+
* enough that a deliberate press does not feel like it was ignored.
|
|
75
|
+
*/
|
|
76
|
+
const DEFAULT_HOLD = 180;
|
|
77
|
+
|
|
78
|
+
/** Weekday names, indexed from Sunday, as the `Date` API numbers them. */
|
|
79
|
+
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
80
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Where a part belongs in the layout. Read off the component itself, so
|
|
84
|
+
* composition stays a flat list of children instead of four nested slots the
|
|
85
|
+
* caller has to remember the order of.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/** One bin inside a column — usually a single day. */
|
|
89
|
+
|
|
90
|
+
/** One column — usually a week. Missing bins are drawn as empty cells. */
|
|
91
|
+
|
|
92
|
+
/** A cell resolved to its place in the grid, as the tooltip receives it. */
|
|
93
|
+
|
|
94
|
+
const HeatmapContext = /*#__PURE__*/createContext(null);
|
|
95
|
+
function useHeatmap(component) {
|
|
96
|
+
const context = useContext(HeatmapContext);
|
|
97
|
+
if (!context) {
|
|
98
|
+
throw new Error(`${component} must be used within a <HeatmapChart>`);
|
|
99
|
+
}
|
|
100
|
+
return context;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The cell under the finger, for something rendered *inside* the chart. A
|
|
105
|
+
* readout in the card's header is outside this provider — use
|
|
106
|
+
* `onActiveCellChange` for that.
|
|
107
|
+
*/
|
|
108
|
+
export function useHeatmapChart() {
|
|
109
|
+
const {
|
|
110
|
+
activeCell
|
|
111
|
+
} = useHeatmap('useHeatmapChart');
|
|
112
|
+
return {
|
|
113
|
+
activeCell
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* -------------------------------------------------------------------------- */
|
|
118
|
+
/* Data helpers */
|
|
119
|
+
/* -------------------------------------------------------------------------- */
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a year of columns from a flat list of dated counts.
|
|
123
|
+
*
|
|
124
|
+
* Every heatmap starts as "I have some dates and some numbers", and the
|
|
125
|
+
* bucketing into weeks is the same arithmetic every time — including the two
|
|
126
|
+
* parts that are easy to get wrong: the leading blanks before the first day of
|
|
127
|
+
* the first week, and days with no entry at all, which must still be drawn as
|
|
128
|
+
* empty cells or the calendar develops holes.
|
|
129
|
+
*/
|
|
130
|
+
export function buildHeatmapCalendar(entries, options = {}) {
|
|
131
|
+
const weekStartDay = options.weekStartDay ?? 0;
|
|
132
|
+
if (!entries.length && !options.start) return [];
|
|
133
|
+
const times = entries.map(entry => entry.date.getTime());
|
|
134
|
+
const start = options.start ?? new Date(Math.min(...times));
|
|
135
|
+
const end = options.end ?? new Date(Math.max(...times));
|
|
136
|
+
const byDay = new Map();
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const key = dayKey(entry.date);
|
|
139
|
+
byDay.set(key, (byDay.get(key) ?? 0) + entry.count);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Back up to the first day of the week the range starts in, so column 0 is a
|
|
143
|
+
// whole week and every row lines up with a weekday for the rest of the chart.
|
|
144
|
+
const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
|
|
145
|
+
cursor.setDate(cursor.getDate() - (cursor.getDay() - weekStartDay + 7) % 7);
|
|
146
|
+
const columns = [];
|
|
147
|
+
let column = [];
|
|
148
|
+
while (cursor <= end || column.length) {
|
|
149
|
+
const row = (cursor.getDay() - weekStartDay + 7) % 7;
|
|
150
|
+
const date = new Date(cursor);
|
|
151
|
+
// Before the range starts and after it ends the cell exists but has no
|
|
152
|
+
// reading — `count: 0` and no date, so the tooltip stays quiet on it.
|
|
153
|
+
const inRange = date >= startOfDay(start) && date <= end;
|
|
154
|
+
column.push({
|
|
155
|
+
bin: row,
|
|
156
|
+
count: inRange ? byDay.get(dayKey(date)) ?? 0 : 0,
|
|
157
|
+
date: inRange ? date : undefined
|
|
158
|
+
});
|
|
159
|
+
if (row === DAYS_IN_WEEK - 1) {
|
|
160
|
+
columns.push({
|
|
161
|
+
bin: columns.length,
|
|
162
|
+
bins: column
|
|
163
|
+
});
|
|
164
|
+
column = [];
|
|
165
|
+
if (cursor > end) break;
|
|
166
|
+
}
|
|
167
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
168
|
+
}
|
|
169
|
+
return columns;
|
|
170
|
+
}
|
|
171
|
+
function startOfDay(date) {
|
|
172
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
173
|
+
}
|
|
174
|
+
function dayKey(date) {
|
|
175
|
+
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Four thresholds from the data, so a chart of single-digit counts and a chart
|
|
180
|
+
* of thousands both use the whole ramp. Quartiles of the *non-zero* counts:
|
|
181
|
+
* zero is its own level, and counting it would drag every threshold down to
|
|
182
|
+
* nothing on a sparse chart.
|
|
183
|
+
*/
|
|
184
|
+
function deriveLevels(data) {
|
|
185
|
+
const counts = [];
|
|
186
|
+
for (const column of data) {
|
|
187
|
+
for (const bin of column.bins) {
|
|
188
|
+
if (bin.count > 0) counts.push(bin.count);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!counts.length) return [1, 2, 3, 4];
|
|
192
|
+
counts.sort((a, b) => a - b);
|
|
193
|
+
const at = fraction => counts[Math.min(counts.length - 1, Math.floor(counts.length * fraction))];
|
|
194
|
+
const thresholds = [1, at(0.25), at(0.5), at(0.75)];
|
|
195
|
+
// Ties collapse the ramp — nudge each threshold past the one below it so
|
|
196
|
+
// four distinct levels stay four distinct levels.
|
|
197
|
+
for (let i = 1; i < thresholds.length; i++) {
|
|
198
|
+
if (thresholds[i] <= thresholds[i - 1]) thresholds[i] = thresholds[i - 1] + 1;
|
|
199
|
+
}
|
|
200
|
+
return thresholds;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* -------------------------------------------------------------------------- */
|
|
204
|
+
/* Root */
|
|
205
|
+
/* -------------------------------------------------------------------------- */
|
|
206
|
+
|
|
207
|
+
const HeatmapChartRoot = /*#__PURE__*/forwardRef(function HeatmapChartRoot({
|
|
208
|
+
className,
|
|
209
|
+
data,
|
|
210
|
+
layout = 'fluid',
|
|
211
|
+
binSize = 12,
|
|
212
|
+
gap = 3,
|
|
213
|
+
cornerRadius = 2,
|
|
214
|
+
weekStartDay = 0,
|
|
215
|
+
rows = DAYS_IN_WEEK,
|
|
216
|
+
levels,
|
|
217
|
+
levelColors,
|
|
218
|
+
color,
|
|
219
|
+
animationDuration = 900,
|
|
220
|
+
inactiveOpacity = 1,
|
|
221
|
+
onActiveCellChange,
|
|
222
|
+
children,
|
|
223
|
+
...props
|
|
224
|
+
}, ref) {
|
|
225
|
+
const [width, setWidth] = useState(0);
|
|
226
|
+
const [activeCell, setActiveCellState] = useState(null);
|
|
227
|
+
const clipId = useRef(`panelui-heat-${Math.random().toString(36).slice(2, 9)}`).current;
|
|
228
|
+
const reveal = useSharedValue(0);
|
|
229
|
+
const reducedMotion = useReducedMotion();
|
|
230
|
+
const baseToken = useCSSVariable('--color-chart-1');
|
|
231
|
+
const emptyToken = useCSSVariable('--color-muted');
|
|
232
|
+
const base = color ?? (typeof baseToken === 'string' ? baseToken : '#262626');
|
|
233
|
+
const empty = typeof emptyToken === 'string' ? emptyToken : 'rgba(128,128,128,0.16)';
|
|
234
|
+
const parts = useMemo(() => splitParts(children), [children]);
|
|
235
|
+
// The y-axis is laid out beside the grid rather than over it, so its width
|
|
236
|
+
// has to come out of the grid's before the cells are sized.
|
|
237
|
+
const axisWidth = parts.y ? parts.y.props.width ?? DEFAULT_AXIS_WIDTH : 0;
|
|
238
|
+
const grid = useMemo(() => {
|
|
239
|
+
const count = data.length;
|
|
240
|
+
if (!count) return {
|
|
241
|
+
size: 0,
|
|
242
|
+
gap,
|
|
243
|
+
columns: 0,
|
|
244
|
+
rows,
|
|
245
|
+
width: 0,
|
|
246
|
+
height: 0
|
|
247
|
+
};
|
|
248
|
+
const available = Math.max(width - axisWidth, 0);
|
|
249
|
+
const size = layout === 'fill' && available > 0 ? Math.max((available - (count - 1) * gap) / count, 1) : binSize;
|
|
250
|
+
return {
|
|
251
|
+
size,
|
|
252
|
+
gap,
|
|
253
|
+
columns: count,
|
|
254
|
+
rows,
|
|
255
|
+
width: count * size + (count - 1) * gap,
|
|
256
|
+
height: rows * size + (rows - 1) * gap
|
|
257
|
+
};
|
|
258
|
+
}, [data.length, width, axisWidth, layout, binSize, gap, rows]);
|
|
259
|
+
const thresholds = useMemo(() => levels ?? deriveLevels(data), [levels, data]);
|
|
260
|
+
|
|
261
|
+
/*
|
|
262
|
+
* One colour at five opacities, unless the caller supplied five colours — in
|
|
263
|
+
* which case the opacities are dropped, since dimming a colour someone chose
|
|
264
|
+
* on purpose is not a ramp, it is a bug.
|
|
265
|
+
*/
|
|
266
|
+
const {
|
|
267
|
+
ramp,
|
|
268
|
+
opacities
|
|
269
|
+
} = useMemo(() => levelColors ? {
|
|
270
|
+
ramp: levelColors,
|
|
271
|
+
opacities: levelColors.map(() => 1)
|
|
272
|
+
} : {
|
|
273
|
+
ramp: [empty, base, base, base, base],
|
|
274
|
+
opacities: [...LEVEL_OPACITY]
|
|
275
|
+
}, [levelColors, empty, base]);
|
|
276
|
+
const levelOf = useMemo(() => count => {
|
|
277
|
+
if (count <= 0) return 0;
|
|
278
|
+
let level = 1;
|
|
279
|
+
for (let i = 1; i < thresholds.length; i++) {
|
|
280
|
+
if (count >= thresholds[i]) level = i + 1;
|
|
281
|
+
}
|
|
282
|
+
return Math.min(level, 4);
|
|
283
|
+
}, [thresholds]);
|
|
284
|
+
const cellAt = useMemo(() => (columnIndex, row) => {
|
|
285
|
+
const bin = data[columnIndex]?.bins.find(candidate => candidate.bin === row);
|
|
286
|
+
const count = bin?.count ?? 0;
|
|
287
|
+
return {
|
|
288
|
+
column: columnIndex,
|
|
289
|
+
row,
|
|
290
|
+
count,
|
|
291
|
+
level: levelOf(count),
|
|
292
|
+
date: bin?.date
|
|
293
|
+
};
|
|
294
|
+
}, [data, levelOf]);
|
|
295
|
+
const setActiveCell = useMemo(() => cell => {
|
|
296
|
+
setActiveCellState(cell);
|
|
297
|
+
onActiveCellChange?.(cell);
|
|
298
|
+
}, [onActiveCellChange]);
|
|
299
|
+
|
|
300
|
+
// Plays once, when there is both a grid to reveal and data to reveal in it.
|
|
301
|
+
const revealed = useRef(false);
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (revealed.current || grid.width <= 0) return;
|
|
304
|
+
revealed.current = true;
|
|
305
|
+
if (reducedMotion) {
|
|
306
|
+
reveal.value = 1;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
reveal.value = withTiming(1, {
|
|
310
|
+
duration: animationDuration,
|
|
311
|
+
easing: Easing.out(Easing.cubic)
|
|
312
|
+
});
|
|
313
|
+
}, [grid.width, reducedMotion, animationDuration, reveal]);
|
|
314
|
+
const onLayout = event => {
|
|
315
|
+
const next = event.nativeEvent.layout.width;
|
|
316
|
+
setWidth(current => Math.abs(current - next) < 1 ? current : next);
|
|
317
|
+
props.onLayout?.(event);
|
|
318
|
+
};
|
|
319
|
+
const context = useMemo(() => ({
|
|
320
|
+
data,
|
|
321
|
+
grid,
|
|
322
|
+
ramp,
|
|
323
|
+
opacities,
|
|
324
|
+
levelOf,
|
|
325
|
+
cellAt,
|
|
326
|
+
cornerRadius,
|
|
327
|
+
inactiveOpacity,
|
|
328
|
+
weekStartDay,
|
|
329
|
+
clipId,
|
|
330
|
+
activeCell,
|
|
331
|
+
setActiveCell
|
|
332
|
+
}), [data, grid, ramp, opacities, levelOf, cellAt, cornerRadius, inactiveOpacity, weekStartDay, clipId, activeCell, setActiveCell]);
|
|
333
|
+
|
|
334
|
+
/*
|
|
335
|
+
* One clip wiping left to right, rather than an animation per column. The
|
|
336
|
+
* effect is the same — columns arriving in order — and it costs one animated
|
|
337
|
+
* value instead of one per week, which for a year is fifty-two.
|
|
338
|
+
*/
|
|
339
|
+
const clipProps = useAnimatedProps(() => ({
|
|
340
|
+
width: grid.width * reveal.value
|
|
341
|
+
}));
|
|
342
|
+
return /*#__PURE__*/_jsx(HeatmapContext.Provider, {
|
|
343
|
+
value: context,
|
|
344
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
345
|
+
...props,
|
|
346
|
+
ref: ref,
|
|
347
|
+
onLayout: onLayout,
|
|
348
|
+
className: cn('w-full', className),
|
|
349
|
+
children: [parts.x ? /*#__PURE__*/_jsx(View, {
|
|
350
|
+
style: {
|
|
351
|
+
paddingLeft: axisWidth
|
|
352
|
+
},
|
|
353
|
+
children: parts.x
|
|
354
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
355
|
+
className: "flex-row",
|
|
356
|
+
children: [parts.y, /*#__PURE__*/_jsx(View, {
|
|
357
|
+
style: {
|
|
358
|
+
width: grid.width,
|
|
359
|
+
height: grid.height
|
|
360
|
+
},
|
|
361
|
+
children: grid.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
362
|
+
children: [/*#__PURE__*/_jsxs(Svg, {
|
|
363
|
+
width: grid.width,
|
|
364
|
+
height: grid.height,
|
|
365
|
+
style: StyleSheet.absoluteFill,
|
|
366
|
+
children: [/*#__PURE__*/_jsx(Defs, {
|
|
367
|
+
children: /*#__PURE__*/_jsx(ClipPath, {
|
|
368
|
+
id: clipId,
|
|
369
|
+
children: /*#__PURE__*/_jsx(AnimatedRect, {
|
|
370
|
+
x: 0,
|
|
371
|
+
y: 0,
|
|
372
|
+
height: grid.height,
|
|
373
|
+
animatedProps: clipProps
|
|
374
|
+
})
|
|
375
|
+
})
|
|
376
|
+
}), /*#__PURE__*/_jsxs(G, {
|
|
377
|
+
clipPath: `url(#${clipId})`,
|
|
378
|
+
children: [parts.rules, parts.cells]
|
|
379
|
+
})]
|
|
380
|
+
}), parts.tooltip]
|
|
381
|
+
}) : null
|
|
382
|
+
})]
|
|
383
|
+
}), parts.legend]
|
|
384
|
+
})
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
/** Sorts the children into the places the layout has for them. */
|
|
388
|
+
function splitParts(children) {
|
|
389
|
+
const parts = {
|
|
390
|
+
cells: [],
|
|
391
|
+
rules: [],
|
|
392
|
+
x: null,
|
|
393
|
+
y: null,
|
|
394
|
+
tooltip: [],
|
|
395
|
+
legend: [],
|
|
396
|
+
columns: 0
|
|
397
|
+
};
|
|
398
|
+
Children.forEach(children, (child, index) => {
|
|
399
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
400
|
+
const slot = child.type.slot ?? 'cells';
|
|
401
|
+
const keyed = /*#__PURE__*/_jsx(ChildSlot, {
|
|
402
|
+
children: child
|
|
403
|
+
}, index);
|
|
404
|
+
if (slot === 'x-axis') parts.x = keyed;else if (slot === 'y-axis') parts.y = child;else if (slot === 'rules') parts.rules.push(keyed);else if (slot === 'tooltip') parts.tooltip.push(keyed);else if (slot === 'legend') parts.legend.push(keyed);else parts.cells.push(keyed);
|
|
405
|
+
});
|
|
406
|
+
return parts;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Identity wrapper, purely so the partitioned arrays can carry keys. */
|
|
410
|
+
function ChildSlot({
|
|
411
|
+
children
|
|
412
|
+
}) {
|
|
413
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
414
|
+
children: children
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* -------------------------------------------------------------------------- */
|
|
419
|
+
/* Cells */
|
|
420
|
+
/* -------------------------------------------------------------------------- */
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The grid itself. Every row of every column is drawn, including the ones with
|
|
424
|
+
* nothing in them — a calendar with holes in it stops being a calendar.
|
|
425
|
+
*/
|
|
426
|
+
function HeatmapCells({
|
|
427
|
+
cornerRadius
|
|
428
|
+
}) {
|
|
429
|
+
const chart = useHeatmap('HeatmapChart.Cells');
|
|
430
|
+
const {
|
|
431
|
+
grid,
|
|
432
|
+
data,
|
|
433
|
+
ramp,
|
|
434
|
+
opacities,
|
|
435
|
+
levelOf,
|
|
436
|
+
activeCell,
|
|
437
|
+
inactiveOpacity
|
|
438
|
+
} = chart;
|
|
439
|
+
const radius = cornerRadius ?? chart.cornerRadius;
|
|
440
|
+
const step = grid.size + grid.gap;
|
|
441
|
+
return /*#__PURE__*/_jsx(G, {
|
|
442
|
+
children: data.map((column, columnIndex) => Array.from({
|
|
443
|
+
length: grid.rows
|
|
444
|
+
}, (_unused, row) => {
|
|
445
|
+
const bin = column.bins.find(candidate => candidate.bin === row);
|
|
446
|
+
const level = levelOf(bin?.count ?? 0);
|
|
447
|
+
const dimmed = activeCell !== null && !(activeCell.column === columnIndex && activeCell.row === row);
|
|
448
|
+
return /*#__PURE__*/_jsx(Rect, {
|
|
449
|
+
x: columnIndex * step,
|
|
450
|
+
y: row * step,
|
|
451
|
+
width: grid.size,
|
|
452
|
+
height: grid.size,
|
|
453
|
+
rx: radius,
|
|
454
|
+
ry: radius,
|
|
455
|
+
fill: ramp[level],
|
|
456
|
+
fillOpacity: opacities[level] * (dimmed ? inactiveOpacity : 1)
|
|
457
|
+
}, `${columnIndex}-${row}`);
|
|
458
|
+
}))
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
HeatmapCells.slot = 'cells';
|
|
462
|
+
|
|
463
|
+
/* -------------------------------------------------------------------------- */
|
|
464
|
+
/* Rules */
|
|
465
|
+
/* -------------------------------------------------------------------------- */
|
|
466
|
+
|
|
467
|
+
/** Vertical rules grouping the columns — quarters, months, sprints. */
|
|
468
|
+
function HeatmapSeparator({
|
|
469
|
+
every = 'quarter',
|
|
470
|
+
color,
|
|
471
|
+
dashArray
|
|
472
|
+
}) {
|
|
473
|
+
const {
|
|
474
|
+
grid
|
|
475
|
+
} = useHeatmap('HeatmapChart.Separator');
|
|
476
|
+
const token = useCSSVariable('--color-border');
|
|
477
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.3)');
|
|
478
|
+
const interval = every === 'quarter' ? 13 : every;
|
|
479
|
+
const step = grid.size + grid.gap;
|
|
480
|
+
if (interval <= 0) return null;
|
|
481
|
+
const lines = [];
|
|
482
|
+
for (let column = interval; column < grid.columns; column += interval) {
|
|
483
|
+
lines.push(column);
|
|
484
|
+
}
|
|
485
|
+
return /*#__PURE__*/_jsx(G, {
|
|
486
|
+
children: lines.map(column => {
|
|
487
|
+
const x = column * step - grid.gap / 2;
|
|
488
|
+
return /*#__PURE__*/_jsx(SvgLine, {
|
|
489
|
+
x1: x,
|
|
490
|
+
y1: 0,
|
|
491
|
+
x2: x,
|
|
492
|
+
y2: grid.height,
|
|
493
|
+
stroke: stroke,
|
|
494
|
+
strokeWidth: 1,
|
|
495
|
+
strokeDasharray: dashArray
|
|
496
|
+
}, column);
|
|
497
|
+
})
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
HeatmapSeparator.slot = 'rules';
|
|
501
|
+
|
|
502
|
+
/* -------------------------------------------------------------------------- */
|
|
503
|
+
/* Axes */
|
|
504
|
+
/* -------------------------------------------------------------------------- */
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Month labels above the grid.
|
|
508
|
+
*
|
|
509
|
+
* A label is emitted where the month changes rather than at a fixed interval,
|
|
510
|
+
* because months are not the same length — spacing them evenly puts "Mar" over
|
|
511
|
+
* a week in February.
|
|
512
|
+
*/
|
|
513
|
+
function HeatmapXAxis({
|
|
514
|
+
className,
|
|
515
|
+
formatLabel
|
|
516
|
+
}) {
|
|
517
|
+
const {
|
|
518
|
+
data,
|
|
519
|
+
grid
|
|
520
|
+
} = useHeatmap('HeatmapChart.XAxis');
|
|
521
|
+
const step = grid.size + grid.gap;
|
|
522
|
+
const labels = useMemo(() => {
|
|
523
|
+
const out = [];
|
|
524
|
+
let lastMonth = -1;
|
|
525
|
+
data.forEach((column, index) => {
|
|
526
|
+
const date = column.bins.find(bin => bin.date)?.date;
|
|
527
|
+
if (!date) return;
|
|
528
|
+
const month = date.getMonth();
|
|
529
|
+
if (month === lastMonth) return;
|
|
530
|
+
lastMonth = month;
|
|
531
|
+
const label = formatLabel ? formatLabel(date, index) : MONTHS[month];
|
|
532
|
+
if (label) out.push({
|
|
533
|
+
column: index,
|
|
534
|
+
label
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// The first month is usually a stub of a week or two, and a label over it
|
|
539
|
+
// collides with the next one. Drop it when it has no room.
|
|
540
|
+
if (out.length > 1 && out[1].column - out[0].column < 3) out.shift();
|
|
541
|
+
return out;
|
|
542
|
+
}, [data, formatLabel]);
|
|
543
|
+
return /*#__PURE__*/_jsx(View, {
|
|
544
|
+
className: cn('h-4', className),
|
|
545
|
+
style: {
|
|
546
|
+
width: grid.width
|
|
547
|
+
},
|
|
548
|
+
children: labels.map(({
|
|
549
|
+
column,
|
|
550
|
+
label
|
|
551
|
+
}) => /*#__PURE__*/_jsx(Text, {
|
|
552
|
+
size: "xs",
|
|
553
|
+
muted: true,
|
|
554
|
+
className: "absolute",
|
|
555
|
+
style: {
|
|
556
|
+
left: column * step
|
|
557
|
+
},
|
|
558
|
+
children: label
|
|
559
|
+
}, `${column}-${label}`))
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
HeatmapXAxis.slot = 'x-axis';
|
|
563
|
+
/** Weekday labels down the left of the grid. */
|
|
564
|
+
function HeatmapYAxis({
|
|
565
|
+
className,
|
|
566
|
+
width = DEFAULT_AXIS_WIDTH,
|
|
567
|
+
tickFilter = 'odd',
|
|
568
|
+
labelFormat = 'full',
|
|
569
|
+
labels
|
|
570
|
+
}) {
|
|
571
|
+
const {
|
|
572
|
+
grid,
|
|
573
|
+
weekStartDay
|
|
574
|
+
} = useHeatmap('HeatmapChart.YAxis');
|
|
575
|
+
const step = grid.size + grid.gap;
|
|
576
|
+
return /*#__PURE__*/_jsx(View, {
|
|
577
|
+
className: className,
|
|
578
|
+
style: {
|
|
579
|
+
width,
|
|
580
|
+
height: grid.height
|
|
581
|
+
},
|
|
582
|
+
children: Array.from({
|
|
583
|
+
length: grid.rows
|
|
584
|
+
}, (_unused, row) => {
|
|
585
|
+
if (tickFilter === 'odd' && row % 2 === 0) return null;
|
|
586
|
+
if (tickFilter === 'even' && row % 2 === 1) return null;
|
|
587
|
+
const name = labels ? labels[row] ?? '' : WEEKDAYS[(row + weekStartDay) % DAYS_IN_WEEK];
|
|
588
|
+
return /*#__PURE__*/_jsx(Text, {
|
|
589
|
+
size: "xs",
|
|
590
|
+
muted: true,
|
|
591
|
+
className: "absolute"
|
|
592
|
+
// Centred on the cell rather than aligned to its top edge, so a
|
|
593
|
+
// label reads as belonging to the row it sits beside.
|
|
594
|
+
,
|
|
595
|
+
style: {
|
|
596
|
+
top: row * step + grid.size / 2 - 7
|
|
597
|
+
},
|
|
598
|
+
children: labels ? name : labelFormat === 'initial' ? name.slice(0, 1) : name.slice(0, 3)
|
|
599
|
+
}, row);
|
|
600
|
+
})
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
HeatmapYAxis.slot = 'y-axis';
|
|
604
|
+
|
|
605
|
+
/* -------------------------------------------------------------------------- */
|
|
606
|
+
/* Tooltip */
|
|
607
|
+
/* -------------------------------------------------------------------------- */
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* A readout following the finger across the grid.
|
|
611
|
+
*
|
|
612
|
+
* It lives in the view layer, over the SVG: a gesture handler cannot be
|
|
613
|
+
* attached to an SVG node, and SVG text ignores the platform's text scaling.
|
|
614
|
+
* The cell under the finger is resolved on the UI thread and only crosses back
|
|
615
|
+
* into JS when it changes, so a drag across a year costs a handful of
|
|
616
|
+
* re-renders rather than one per frame.
|
|
617
|
+
*/
|
|
618
|
+
function HeatmapTooltip({
|
|
619
|
+
className,
|
|
620
|
+
formatLabel,
|
|
621
|
+
activateAfterLongPress = DEFAULT_HOLD
|
|
622
|
+
}) {
|
|
623
|
+
const {
|
|
624
|
+
grid,
|
|
625
|
+
activeCell,
|
|
626
|
+
setActiveCell,
|
|
627
|
+
cellAt
|
|
628
|
+
} = useHeatmap('HeatmapChart.Tooltip');
|
|
629
|
+
const step = grid.size + grid.gap;
|
|
630
|
+
|
|
631
|
+
// Mirrored for the worklet, which cannot read the JS closure's latest value.
|
|
632
|
+
const lastKey = useSharedValue(-1);
|
|
633
|
+
const resolve = useMemo(() => (column, row) => setActiveCell(cellAt(column, row)), [cellAt, setActiveCell]);
|
|
634
|
+
const clear = useMemo(() => () => setActiveCell(null), [setActiveCell]);
|
|
635
|
+
|
|
636
|
+
/*
|
|
637
|
+
* The cell is resolved here, on the UI thread, and only crosses into JS when
|
|
638
|
+
* it *changes*. Sending every frame's coordinates over and picking the cell
|
|
639
|
+
* in React would cost a re-render per frame for a value that changes maybe
|
|
640
|
+
* fifty times across a whole drag.
|
|
641
|
+
*/
|
|
642
|
+
const pick = (x, y) => {
|
|
643
|
+
'worklet';
|
|
644
|
+
|
|
645
|
+
const column = Math.floor(x / step);
|
|
646
|
+
const row = Math.floor(y / step);
|
|
647
|
+
if (column < 0 || column >= grid.columns || row < 0 || row >= grid.rows) return;
|
|
648
|
+
const key = column * grid.rows + row;
|
|
649
|
+
if (key === lastKey.value) return;
|
|
650
|
+
lastKey.value = key;
|
|
651
|
+
runOnJS(resolve)(column, row);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
/*
|
|
655
|
+
* The readout takes over only once the press has been held.
|
|
656
|
+
*
|
|
657
|
+
* `onStart` rather than `onBegin` is the other half of it: `onBegin` fires on
|
|
658
|
+
* touch-down whatever happens next, so picking there would light a cell and
|
|
659
|
+
* dim the rest of the grid for the first moment of every scroll swipe — the
|
|
660
|
+
* gesture would yield correctly and still leave a flicker behind it.
|
|
661
|
+
*/
|
|
662
|
+
const pan = Gesture.Pan().minDistance(0).activateAfterLongPress(activateAfterLongPress).onStart(event => {
|
|
663
|
+
'worklet';
|
|
664
|
+
|
|
665
|
+
pick(event.x, event.y);
|
|
666
|
+
}).onUpdate(event => {
|
|
667
|
+
'worklet';
|
|
668
|
+
|
|
669
|
+
pick(event.x, event.y);
|
|
670
|
+
}).onFinalize(() => {
|
|
671
|
+
'worklet';
|
|
672
|
+
|
|
673
|
+
// Fires whether or not the gesture ever activated, so a swipe that only
|
|
674
|
+
// scrolled must not report a cell change it never made.
|
|
675
|
+
if (lastKey.value === -1) return;
|
|
676
|
+
lastKey.value = -1;
|
|
677
|
+
runOnJS(clear)();
|
|
678
|
+
});
|
|
679
|
+
const label = activeCell ? formatLabel ? formatLabel(activeCell) : defaultTooltipLabel(activeCell) : null;
|
|
680
|
+
|
|
681
|
+
// Flipped to the left of the finger past the halfway mark, so the readout
|
|
682
|
+
// never runs off the edge it is closest to — and dropped below the cell in
|
|
683
|
+
// the top two rows, where there is no room above it inside the chart.
|
|
684
|
+
const anchorX = activeCell ? activeCell.column * step + grid.size / 2 : 0;
|
|
685
|
+
const flipped = anchorX > grid.width / 2;
|
|
686
|
+
const below = (activeCell?.row ?? 0) < 2;
|
|
687
|
+
const anchorY = activeCell ? below ? activeCell.row * step + grid.size + 6 : activeCell.row * step - TOOLTIP_HEIGHT - 6 : 0;
|
|
688
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
689
|
+
children: [/*#__PURE__*/_jsx(GestureDetector, {
|
|
690
|
+
gesture: pan,
|
|
691
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
692
|
+
style: StyleSheet.absoluteFill
|
|
693
|
+
})
|
|
694
|
+
}), activeCell && label ? /*#__PURE__*/_jsx(View, {
|
|
695
|
+
pointerEvents: "none",
|
|
696
|
+
className: cn('absolute rounded-lg border border-border bg-popover px-2 py-1 shadow-md', className),
|
|
697
|
+
style: {
|
|
698
|
+
top: anchorY,
|
|
699
|
+
[flipped ? 'right' : 'left']: flipped ? grid.width - anchorX : anchorX
|
|
700
|
+
},
|
|
701
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
702
|
+
size: "xs",
|
|
703
|
+
className: "text-popover-foreground",
|
|
704
|
+
children: label
|
|
705
|
+
})
|
|
706
|
+
}) : null]
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
HeatmapTooltip.slot = 'tooltip';
|
|
710
|
+
function defaultTooltipLabel(cell) {
|
|
711
|
+
const count = `${cell.count} ${cell.count === 1 ? 'contribution' : 'contributions'}`;
|
|
712
|
+
if (!cell.date) return 'No data';
|
|
713
|
+
const date = `${MONTHS[cell.date.getMonth()]} ${cell.date.getDate()}`;
|
|
714
|
+
return `${count} on ${date}`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* -------------------------------------------------------------------------- */
|
|
718
|
+
/* Legend */
|
|
719
|
+
/* -------------------------------------------------------------------------- */
|
|
720
|
+
|
|
721
|
+
/** The `Less ▢▢▢▢▢ More` key, under the grid. */
|
|
722
|
+
function HeatmapLegend({
|
|
723
|
+
className,
|
|
724
|
+
lessLabel = 'Less',
|
|
725
|
+
moreLabel = 'More',
|
|
726
|
+
swatchSize
|
|
727
|
+
}) {
|
|
728
|
+
const {
|
|
729
|
+
ramp,
|
|
730
|
+
opacities,
|
|
731
|
+
grid,
|
|
732
|
+
cornerRadius
|
|
733
|
+
} = useHeatmap('HeatmapChart.Legend');
|
|
734
|
+
const size = swatchSize ?? Math.max(grid.size, 10);
|
|
735
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
736
|
+
className: cn('flex-row items-center gap-1.5 pt-2', className),
|
|
737
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
738
|
+
className: "flex-1"
|
|
739
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
740
|
+
size: "xs",
|
|
741
|
+
muted: true,
|
|
742
|
+
children: lessLabel
|
|
743
|
+
}), ramp.map((fill, level) => /*#__PURE__*/_jsx(Svg, {
|
|
744
|
+
width: size,
|
|
745
|
+
height: size,
|
|
746
|
+
children: /*#__PURE__*/_jsx(Rect, {
|
|
747
|
+
x: 0,
|
|
748
|
+
y: 0,
|
|
749
|
+
width: size,
|
|
750
|
+
height: size,
|
|
751
|
+
rx: cornerRadius,
|
|
752
|
+
ry: cornerRadius,
|
|
753
|
+
fill: fill,
|
|
754
|
+
fillOpacity: opacities[level]
|
|
755
|
+
})
|
|
756
|
+
}, level)), /*#__PURE__*/_jsx(Text, {
|
|
757
|
+
size: "xs",
|
|
758
|
+
muted: true,
|
|
759
|
+
children: moreLabel
|
|
760
|
+
})]
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
HeatmapLegend.slot = 'legend';
|
|
764
|
+
export const HeatmapChart = Object.assign(HeatmapChartRoot, {
|
|
765
|
+
Cells: HeatmapCells,
|
|
766
|
+
Separator: HeatmapSeparator,
|
|
767
|
+
XAxis: HeatmapXAxis,
|
|
768
|
+
YAxis: HeatmapYAxis,
|
|
769
|
+
Tooltip: HeatmapTooltip,
|
|
770
|
+
Legend: HeatmapLegend
|
|
771
|
+
});
|
|
772
|
+
//# sourceMappingURL=index.js.map
|