@thomas.f/prnext 0.1.16-alpha → 0.1.17-alpha
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/AI.md +63 -0
- package/README.md +384 -9
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +5 -0
- package/build/analyze.mjs +184 -0
- package/build/app-config.mjs +7 -8
- package/build/app-static.mjs +12 -7
- package/build/app.mjs +23 -10
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +20 -5
- package/build/check.mjs +56 -11
- package/build/compiler.mjs +25 -1
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +21 -13
- package/build/css-modules.mjs +15 -4
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +83 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +55 -4
- package/build/edge.mjs +3 -2
- package/build/framework-imports.mjs +40 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +96 -101
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +2 -3
- package/build/migrate.mjs +60 -57
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +97 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-pages.mjs +90 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +80 -0
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +15 -3
- package/build/transform-cache.mjs +37 -4
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +91 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +22 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +5 -4
- package/build/webpack-graph.mjs +164 -27
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +105 -121
- package/compat/index.d.ts +21 -7
- package/compat/link.d.cts +4 -3
- package/compat/navigation.d.cts +4 -3
- package/compat/router.d.cts +4 -3
- package/compat/typed-route.d.cts +17 -0
- package/docs/ai/compatibility.md +106 -0
- package/docs/ai/data.md +128 -0
- package/docs/ai/deployment.md +111 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +146 -0
- package/docs/ai/routing.md +132 -0
- package/docs/ai/troubleshooting.md +120 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +25 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +17 -6
- package/runtime/app-render.mjs +12 -0
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +239 -45
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +186 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +18 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +307 -0
- package/runtime/env.mjs +2 -1
- package/runtime/pages-client.mjs +10 -1
- package/runtime/pages-navigation.mjs +6 -1
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/rsc-worker.mjs +7 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +12 -5
- package/runtime/worker.mjs +36 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// A bounded, reversible transfer between the existing issue ring and menu border.
|
|
2
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
3
|
+
const circleStart = -113.4 * Math.PI / 180, arcControl = 4 / 3 * Math.tan(Math.PI / 16), cornerControl = 4 / 3 * Math.tan(Math.PI / 8);
|
|
4
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
5
|
+
const number = value => Number(value.toFixed(4));
|
|
6
|
+
let revealSequence = 0;
|
|
7
|
+
|
|
8
|
+
function circle(cx, cy, radius) {
|
|
9
|
+
const result = [cx + radius * Math.cos(circleStart), cy + radius * Math.sin(circleStart)];
|
|
10
|
+
for (let index = 0; index < 8; index++) {
|
|
11
|
+
const a = circleStart + index * Math.PI / 4, b = a + Math.PI / 4;
|
|
12
|
+
result.push(cx + radius * (Math.cos(a) - arcControl * Math.sin(a)), cy + radius * (Math.sin(a) + arcControl * Math.cos(a)),
|
|
13
|
+
cx + radius * (Math.cos(b) + arcControl * Math.sin(b)), cy + radius * (Math.sin(b) - arcControl * Math.cos(b)), cx + radius * Math.cos(b), cy + radius * Math.sin(b));
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function rectangle(x, y, width, height, radius) {
|
|
18
|
+
const r = Math.max(0, Math.min(radius, width / 2, height / 2)), k = r * cornerControl, right = x + width, bottom = y + height;
|
|
19
|
+
const points = [x + r, y];
|
|
20
|
+
const line = (ax, ay, bx, by) => points.push(lerp(ax, bx, 1 / 3), lerp(ay, by, 1 / 3), lerp(ax, bx, 2 / 3), lerp(ay, by, 2 / 3), bx, by);
|
|
21
|
+
line(x + r, y, right - r, y); points.push(right - r + k, y, right, y + r - k, right, y + r);
|
|
22
|
+
line(right, y + r, right, bottom - r); points.push(right, bottom - r + k, right - r + k, bottom, right - r, bottom);
|
|
23
|
+
line(right - r, bottom, x + r, bottom); points.push(x + r - k, bottom, x, bottom - r + k, x, bottom - r);
|
|
24
|
+
line(x, bottom - r, x, y + r); points.push(x, y + r - k, x + r - k, y, x + r, y);
|
|
25
|
+
return points;
|
|
26
|
+
}
|
|
27
|
+
function path(points) {
|
|
28
|
+
let result = `M${number(points[0])} ${number(points[1])}`;
|
|
29
|
+
for (let index = 2; index < points.length; index += 6) result += 'C' + points.slice(index, index + 6).map(number).join(' ');
|
|
30
|
+
return result + 'Z';
|
|
31
|
+
}
|
|
32
|
+
const clamp = value => Math.max(0, Math.min(1, value));
|
|
33
|
+
const easing = t => t * t * (3 - 2 * t);
|
|
34
|
+
// One continuous ease-in-out: gentle endpoints, a quicker middle, no overshoot.
|
|
35
|
+
const transferEasing = t => t * t * t * (t * (6 * t - 15) + 10);
|
|
36
|
+
const gatherEnd = .12, travelStart = .08, arrival = .82;
|
|
37
|
+
const leftDepartureOffset = 100 - (Math.PI - circleStart) / (2 * Math.PI) * 100;
|
|
38
|
+
const rightDepartureOffset = circleStart / (2 * Math.PI) * 100;
|
|
39
|
+
|
|
40
|
+
function entryOnSide(points, right) {
|
|
41
|
+
const segments = Array.from({ length: 8 }, (_, index) => points.slice(2 + index * 6, 8 + index * 6));
|
|
42
|
+
const next = right ? 3 : 6;
|
|
43
|
+
return [...segments[next - 1].slice(4), ...segments.slice(next).flat(), ...segments.slice(0, next).flat()];
|
|
44
|
+
}
|
|
45
|
+
function roundedRoute(points, maximumRadius = 4) {
|
|
46
|
+
let result = `M${points[0].map(number).join(' ')}`;
|
|
47
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
48
|
+
const [x, y] = points[index], previous = points[index - 1], next = points[index + 1];
|
|
49
|
+
const before = Math.hypot(x - previous[0], y - previous[1]), after = Math.hypot(next[0] - x, next[1] - y);
|
|
50
|
+
const radius = Math.min(maximumRadius, before / 2, after / 2);
|
|
51
|
+
if (!radius) { result += `L${number(x)} ${number(y)}`; continue; }
|
|
52
|
+
const a = [x - (x - previous[0]) / before * radius, y - (y - previous[1]) / before * radius];
|
|
53
|
+
const b = [x + (next[0] - x) / after * radius, y + (next[1] - y) / after * radius];
|
|
54
|
+
result += `L${a.map(number).join(' ')}Q${number(x)} ${number(y)} ${b.map(number).join(' ')}`;
|
|
55
|
+
}
|
|
56
|
+
return result + `L${points.at(-1).map(number).join(' ')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createDevIssueMotion({ host, shadow, ringSvg, issues, panel }) {
|
|
60
|
+
const view = host.ownerDocument.defaultView, noop = { update() {}, refresh() {}, closeImmediately() {}, dispose() {} };
|
|
61
|
+
if (!view.requestAnimationFrame || !view.matchMedia || !host.ownerDocument.createElementNS) return noop;
|
|
62
|
+
const svg = host.ownerDocument.createElementNS(NS, 'svg'), outline = host.ownerDocument.createElementNS(NS, 'path');
|
|
63
|
+
const ring = host.ownerDocument.createElementNS(NS, 'path'), streak = host.ownerDocument.createElementNS(NS, 'path');
|
|
64
|
+
const defs = host.ownerDocument.createElementNS(NS, 'defs'), clip = host.ownerDocument.createElementNS(NS, 'clipPath'), reveal = host.ownerDocument.createElementNS(NS, 'rect');
|
|
65
|
+
clip.id = `prnext-issue-border-reveal-${++revealSequence}`; clip.setAttribute('clipPathUnits', 'userSpaceOnUse');
|
|
66
|
+
reveal.classList.add('issue-motion-reveal'); clip.append(reveal); defs.append(clip);
|
|
67
|
+
svg.classList.add('issue-motion'); svg.setAttribute('viewBox', '0 0 1 1'); svg.setAttribute('aria-hidden', 'true');
|
|
68
|
+
outline.classList.add('issue-motion-path'); ring.classList.add('issue-motion-ring'); streak.classList.add('issue-motion-streak');
|
|
69
|
+
for (const stroke of [ring, outline]) { stroke.setAttribute('pathLength', '100'); stroke.setAttribute('fill', 'none'); stroke.setAttribute('stroke-linecap', 'round'); }
|
|
70
|
+
outline.setAttribute('stroke-width', '1'); streak.setAttribute('fill', 'none'); streak.setAttribute('stroke-width', '1.5'); streak.setAttribute('stroke-linecap', 'round');
|
|
71
|
+
outline.setAttribute('clip-path', `url(#${clip.id})`);
|
|
72
|
+
svg.append(defs, ring, outline, streak); shadow.append(svg);
|
|
73
|
+
const controller = new view.AbortController(), events = { signal: controller.signal }, reduced = view.matchMedia('(prefers-reduced-motion: reduce)');
|
|
74
|
+
let severity = 'none', opened = false, disposed = false, frame = 0, animation, current, source, target;
|
|
75
|
+
function measureSource(rest = false) {
|
|
76
|
+
const origin = host.getBoundingClientRect(), rect = ringSvg.getBoundingClientRect(), width = rest ? ringSvg.clientWidth : rect.width;
|
|
77
|
+
if (!width) return;
|
|
78
|
+
return { x: rect.left - origin.left + rect.width / 2, y: rect.top - origin.top + rect.height / 2,
|
|
79
|
+
radius: 17.25 * width / 36, width: 1.5 * width / 36 };
|
|
80
|
+
}
|
|
81
|
+
function measureTarget() {
|
|
82
|
+
if (issues.hidden || panel.hidden) return;
|
|
83
|
+
const origin = host.getBoundingClientRect(), rect = issues.getBoundingClientRect(), parent = panel.getBoundingClientRect(), style = view.getComputedStyle(panel);
|
|
84
|
+
const values = /^matrix\(([^)]+)\)/.exec(style.transform)?.[1].split(',').map(Number), sx = values?.[0] || 1, sy = values?.[3] || 1;
|
|
85
|
+
const [ox, oy] = style.transformOrigin.split(' ').map(parseFloat);
|
|
86
|
+
// Undo the bubble content's temporary scale/translation, keeping its final border as target.
|
|
87
|
+
const panelX = parent.left - origin.left - (values?.[4] || 0) - (ox || 0) * (1 - sx);
|
|
88
|
+
const panelY = parent.top - origin.top - (values?.[5] || 0) - (oy || 0) * (1 - sy);
|
|
89
|
+
const x = panelX + (rect.left - parent.left) / sx + .5, y = panelY + (rect.top - parent.top) / sy + .5;
|
|
90
|
+
const width = rect.width / sx - 1, height = rect.height / sy - 1;
|
|
91
|
+
const right = host.dataset.position?.endsWith('right') || false;
|
|
92
|
+
const points = entryOnSide(rectangle(x, y, width, height, Math.max(0, parseFloat(view.getComputedStyle(issues).borderTopLeftRadius) - .5)), right);
|
|
93
|
+
const below = source.y > panelY + panel.offsetHeight / 2;
|
|
94
|
+
const railX = right ? panelX + parent.width / sx - parseFloat(style.paddingRight) / 2 : panelX + parseFloat(style.paddingLeft) / 2;
|
|
95
|
+
const marginY = below ? panelY + panel.offsetHeight - parseFloat(style.paddingBottom) / 2 : panelY + parseFloat(style.paddingTop) / 2;
|
|
96
|
+
// Leave the ring on the menu's outside edge, tangent to its circumference. The
|
|
97
|
+
// segment must never start at the center or cut across the logo artwork.
|
|
98
|
+
const departureX = source.x + (right ? source.radius : -source.radius);
|
|
99
|
+
const route = [[departureX, source.y], [departureX, marginY], [railX, marginY], [railX, points[1]], [points[0], points[1]]];
|
|
100
|
+
return { points, route, right, routeRadius: below ? 6 : 4, reveal: { x: x - 1, y: y - 1, width: width + 2, height: height + 2 } };
|
|
101
|
+
}
|
|
102
|
+
function measure(destination = opened) {
|
|
103
|
+
source = measureSource(true) || source;
|
|
104
|
+
if (destination && source) {
|
|
105
|
+
target = measureTarget() || target;
|
|
106
|
+
if (target) {
|
|
107
|
+
outline.setAttribute('d', path(target.points)); streak.setAttribute('d', roundedRoute(target.route, target.routeRadius));
|
|
108
|
+
for (const key of ['x', 'y', 'height']) reveal.setAttribute(key, String(number(target.reveal[key])));
|
|
109
|
+
// Geometry is measured once per placement, never in the animation loop.
|
|
110
|
+
target.routeLength = Math.max(14, streak.getTotalLength());
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function sample(progress) {
|
|
115
|
+
// Flight and border tracing share one eased clock. Re-easing each of these
|
|
116
|
+
// phases made the short flight stop and accelerate at every handoff.
|
|
117
|
+
const gather = easing(clamp(progress / gatherEnd)), travel = clamp((progress - travelStart) / (arrival - travelStart));
|
|
118
|
+
const fill = clamp((progress - arrival) / (1 - arrival)), formation = easing(clamp((gather - .45) / .55));
|
|
119
|
+
const length = (14 + 24 * travel * (1 - travel)) * formation, arcLength = severity === 'critical' ? 50 : 13;
|
|
120
|
+
return { progress, ringX: source.x, ringY: source.y, ringRadius: source.radius, ringWidth: source.width,
|
|
121
|
+
ringOpacity: 1 - easing(clamp((gather - .55) / .45)), ringLength: arcLength * (1 - gather),
|
|
122
|
+
ringGap: lerp(50 - arcLength, 100, gather), ringOffset: (target?.right ? rightDepartureOffset : leftDepartureOffset) * gather,
|
|
123
|
+
streakOffset: Math.max(0, ((target?.routeLength || 14) - 14) * travel + (14 * formation - length)),
|
|
124
|
+
streakLength: length, streakWidth: lerp(source.width, 1, travel),
|
|
125
|
+
streakOpacity: formation * (1 - easing(clamp(fill / .18))), fill };
|
|
126
|
+
}
|
|
127
|
+
function paint(pose) {
|
|
128
|
+
current = pose;
|
|
129
|
+
const length = Math.max(0, Math.min(50, pose.ringLength)), gap = Math.max(0, Math.min(100 - 2 * length, pose.ringGap)), fill = clamp(pose.fill);
|
|
130
|
+
ring.setAttribute('d', path(circle(pose.ringX, pose.ringY, Math.max(0, pose.ringRadius))));
|
|
131
|
+
ring.setAttribute('stroke-dasharray', [length, gap, length, Math.max(0, 100 - 2 * length - gap)].map(number).join(' '));
|
|
132
|
+
ring.setAttribute('stroke-dashoffset', String(number(pose.ringOffset)));
|
|
133
|
+
ring.setAttribute('stroke-width', String(number(pose.ringWidth))); ring.setAttribute('opacity', String(number(clamp(pose.ringOpacity))));
|
|
134
|
+
// Reveal from the arriving thread's side, keeping the contour fixed.
|
|
135
|
+
if (target) reveal.setAttribute('x', String(number(target.reveal.x + (target.right ? target.reveal.width * (1 - fill) : 0))));
|
|
136
|
+
reveal.setAttribute('width', String(number((target?.reveal.width || 0) * fill)));
|
|
137
|
+
outline.setAttribute('opacity', fill > 0 ? '1' : '0');
|
|
138
|
+
streak.setAttribute('stroke-dasharray', `${number(Math.max(0, pose.streakLength))} ${number((target?.routeLength || 14) + 14)}`);
|
|
139
|
+
streak.setAttribute('stroke-width', String(number(pose.streakWidth)));
|
|
140
|
+
streak.setAttribute('stroke-dashoffset', String(number(-pose.streakOffset))); streak.setAttribute('opacity', String(number(clamp(pose.streakOpacity))));
|
|
141
|
+
svg.dataset.progress = String(number(pose.progress)); svg.dataset.fill = String(number(fill));
|
|
142
|
+
svg.dataset.stage = pose.progress <= 0 ? 'ring' : pose.progress >= 1 ? 'border' : pose.progress < gatherEnd ? 'gather' : pose.progress < arrival ? 'travel' : 'fill';
|
|
143
|
+
}
|
|
144
|
+
function cancel() { if (frame) view.cancelAnimationFrame(frame); frame = 0; animation = undefined; }
|
|
145
|
+
function phase(name, running = false) { svg.dataset.phase = name; svg.dataset.running = String(running); host.dataset.issueTransfer = running ? 'moving' : name; }
|
|
146
|
+
function finish() {
|
|
147
|
+
cancel(); if (source) paint(sample(opened ? 1 : 0)); phase(opened ? 'open' : 'closed');
|
|
148
|
+
}
|
|
149
|
+
function disable() {
|
|
150
|
+
cancel(); severity = 'none'; opened = false; current = source = target = undefined;
|
|
151
|
+
svg.dataset.phase = 'disabled'; svg.dataset.running = 'false'; svg.dataset.progress = '0'; svg.dataset.fill = '0'; svg.dataset.stage = 'ring';
|
|
152
|
+
ring.setAttribute('opacity', '0'); ring.removeAttribute('d'); outline.setAttribute('opacity', '0'); outline.removeAttribute('d'); streak.setAttribute('opacity', '0'); streak.removeAttribute('d'); delete host.dataset.issueTransfer;
|
|
153
|
+
}
|
|
154
|
+
function tick(now) {
|
|
155
|
+
frame = 0; if (!animation || disposed) return;
|
|
156
|
+
const amount = clamp((now - animation.start) / animation.duration), t = transferEasing(amount), from = animation.from;
|
|
157
|
+
const progress = lerp(animation.origin, animation.to, t), pose = sample(progress);
|
|
158
|
+
const correction = 1 - clamp((t - animation.correctionAt) / Math.max(.000001, 1 - animation.correctionAt));
|
|
159
|
+
// Correct from the last rendered pose, not from a restarted phase. This also
|
|
160
|
+
// preserves the short traveling stroke when the severity changes mid-transfer.
|
|
161
|
+
for (const key of Object.keys(pose)) if (key !== 'progress') pose[key] += (from[key] - animation.baseline[key]) * correction;
|
|
162
|
+
paint(pose);
|
|
163
|
+
if (amount >= 1) finish(); else frame = view.requestAnimationFrame(tick);
|
|
164
|
+
}
|
|
165
|
+
function start() {
|
|
166
|
+
const interrupted = Boolean(animation); cancel();
|
|
167
|
+
source = measureSource(true) || source;
|
|
168
|
+
// Keep both endpoints fixed during reversals. Only the short stroke travels;
|
|
169
|
+
// the destination contour never morphs or follows it through the menu.
|
|
170
|
+
if (opened && (!interrupted || !target)) measure();
|
|
171
|
+
if (!source) return;
|
|
172
|
+
if (!current || reduced.matches) { finish(); return; }
|
|
173
|
+
// The nearby top-corner target uses a quicker clock, synchronized with its
|
|
174
|
+
// 140ms panel entrance. Keep the longer bottom-corner transfer unchanged.
|
|
175
|
+
const duration = host.dataset.position?.startsWith('top') ? (opened ? 240 : 200)
|
|
176
|
+
: opened ? Math.min(480, Math.max(360, (target?.routeLength || 300) * 1.6)) : 360;
|
|
177
|
+
animation = { from: current, baseline: sample(current.progress), origin: current.progress, correctionAt: 0,
|
|
178
|
+
to: opened ? 1 : 0, duration, start: view.performance.now() };
|
|
179
|
+
phase(opened ? 'opening' : 'closing', true); frame = view.requestAnimationFrame(tick);
|
|
180
|
+
}
|
|
181
|
+
function refresh({ immediate = false } = {}) {
|
|
182
|
+
if (disposed || severity === 'none') return;
|
|
183
|
+
if (immediate || reduced.matches || !animation) { measure(); finish(); }
|
|
184
|
+
}
|
|
185
|
+
reduced.addEventListener('change', () => refresh({ immediate: true }), events);
|
|
186
|
+
view.addEventListener('resize', () => refresh({ immediate: true }), events);
|
|
187
|
+
view.addEventListener('blur', () => refresh({ immediate: true }), events);
|
|
188
|
+
view.visualViewport?.addEventListener('resize', () => refresh({ immediate: true }), events);
|
|
189
|
+
panel.addEventListener('scroll', () => refresh({ immediate: true }), events);
|
|
190
|
+
disable();
|
|
191
|
+
return {
|
|
192
|
+
update({ severity: next = 'none', open = false } = {}) {
|
|
193
|
+
if (disposed) return;
|
|
194
|
+
if (next === 'none') { disable(); return; }
|
|
195
|
+
const changed = opened !== open, first = severity === 'none', changedSeverity = severity !== next;
|
|
196
|
+
severity = next; opened = open;
|
|
197
|
+
if (first) { source = measureSource(); if (source) paint(sample(0)); }
|
|
198
|
+
if (!current) return;
|
|
199
|
+
if (!changed && !first && !changedSeverity) return;
|
|
200
|
+
if (!changed && !animation) { finish(); return; }
|
|
201
|
+
if (!changed && animation) {
|
|
202
|
+
// A severity-only update must not restart the travel/fill clock: the
|
|
203
|
+
// closing contour still has to disappear before the menu is hidden.
|
|
204
|
+
animation.from = current; animation.baseline = sample(current.progress);
|
|
205
|
+
animation.correctionAt = transferEasing(clamp((view.performance.now() - animation.start) / animation.duration));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
start();
|
|
209
|
+
},
|
|
210
|
+
refresh,
|
|
211
|
+
closeImmediately() { if (disposed) return; opened = false; if (severity !== 'none') { measure(false); finish(); } else cancel(); },
|
|
212
|
+
dispose() { if (disposed) return; disposed = true; cancel(); controller.abort(); svg.remove(); delete host.dataset.issueTransfer; },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Capture user activation synchronously for detached windows, even on a slow
|
|
2
|
+
// connection. Import failures/disposal close reservations instead of leaking tabs.
|
|
3
|
+
export function createLazyDevTool(load,{onError=console.error,reserve}={}) {
|
|
4
|
+
let instance,pending,disposed=false,epoch=0;
|
|
5
|
+
const get=()=>pending||=(async()=>{const value=await load();if(disposed){value.dispose();throw Error('Development tool was closed');}return instance=value;})().catch(error=>{pending=undefined;throw error;});
|
|
6
|
+
return new Proxy({}, {get(_target,key){
|
|
7
|
+
if(key==='dispose')return()=>{disposed=true;instance?.dispose();};
|
|
8
|
+
if(key==='hide')return()=>{epoch++;instance?.hide();};
|
|
9
|
+
if(key==='previewState')return()=>instance?.previewState()||{};
|
|
10
|
+
return(...args)=>{
|
|
11
|
+
if(disposed)return;
|
|
12
|
+
if(instance)return instance[key](...args);
|
|
13
|
+
const popup=reserve?.(key,args);
|
|
14
|
+
const expected=epoch;
|
|
15
|
+
void get().then(tool=>{if(disposed||expected!==epoch){popup?.close();return;}return tool[key](...args,...(popup!==undefined?[popup]:[]));}).catch(error=>{popup?.close();if(!disposed)onError(error);});
|
|
16
|
+
};
|
|
17
|
+
}});
|
|
18
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// PRNext brand asset from .github/assets/prnext-logo.svg; embedded for offline development.
|
|
2
|
+
export default "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%22250%20250%20756%20756%22%20fill%3D%22none%22%3E%3Ctitle%3EPRNext%3C%2Ftitle%3E%3Crect%20x%3D%22250%22%20y%3D%22250%22%20width%3D%22756%22%20height%3D%22756%22%20rx%3D%22156%22%20fill%3D%22%2309070d%22%2F%3E%3Cimage%20href%3D%22data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAABOYAAATmCAMAAACyIl4YAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAwBQTFRFAAAA%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5%2F%2Fn5mcfEEAAAAQB0Uk5TAB8sGSAiHhoYISUnKS0vMC4qKCQmIysxHRsWCwQFE2ack5aXlZKZmp2em5iUkYyGgoGAf357dm1jVkc3Dgim%2F%2Fv39vX08%2FLx7uXZybahiGhKMhAGBxyj%2Fv38%2Bvjw7N%2FQz8i8rI5rQhei5NfHtIR4aVQ06ebarYdgQD44DODYw%2B3Mr3VQCvnnzad0WEY6D9KxbAIB6NO6rl5LoMSPAxXin9WDYg3ecRLKqm5wUuPcPah6XeHRPBFXu0RO1sJIpQk%2Fzlo7asXbZ6RP1HKNW7Jlwanvy3wzfXmFb3O5ZLXrTOqLObd33UMUq7BZvjU2ir9fwElTs5BFuEFVvYlRXGHGTaE6%2BPQAAFpWSURBVHic7N0FWJXXHwfw995L2wp6g4sdf2ubORUUC3sooM7uWUOdWLMDW1SurdgzZqDO7sKcTmfr7BYQG1EE%2FpfQOYM4573ved%2FD9%2FM8KgLvOb97Hd%2B9cUIlAABwTcW6AAAAy0LMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMQcAnEPMAQDnEHMAwDnEHABwDjEHAJxDzAEA5xBzAMA5xBwAcA4xBwCcQ8wBAOcQcwDAOcQcAHAOMUdBlST%2B47g4jfmjyAyqj97ROPXzLNE2b%2BLszR8%2FtxIcXjsIr23%2F%2FbI6%2FrdI%2B48%2B8fK%2F7b%2FLLJgP%2BXDM2w9fsBGEN%2FaJh5qPMX9bgijb999i%2Fnr8NyR6nfRpm38bfvNRpwlFZPi354yxCS%2Ftte1bwfpZ%2FIcZNELMq3%2B%2FN0t0fEvqxKLMDTu81sTFf2iXVIT5VcfYR8bFZXgZlSv%2BvYiLEKI11iqVbXwRDuGC4BiZWIzaXL%2F5XUl8oa8Tv57YZWIX71%2F0%2B08lHWVu0fymxL%2F2xLfz7fujPpT373vw75sWmVmwibTRaNQq1V0B0hvEHDFjUsjFJPyovtPEaQTVG8Hu45iLEhxihDjBWoj%2F8TcHQfR%2F3vCEdEj4YpKoT7swR0f8T%2B2X%2FpHi3h8Z9SFholWffkeizw%2BP%2B%2BTv1h91ndhYdPxRmsikz3xcmPkVJbTwvlVNpN1%2Fe4gTohxi4qyF14LaLrG%2BWEFll1hcfIuJDZi%2Fz%2FrfV2wXndRGQpfvv0P4wqfiX3d00mv%2F8HYmFRr%2FfcJ%2Fjv3gjZ2gjhHeqezf%2F4%2Fp%2Ff%2Bi%2Fiod8sUDgCeIOWJuRyvg3VMe1WcOR8XGvYn1VK1lXRpYCn5QifmoVI9Y1wBppxUS%2F7M3B9wDTa6P4m4O68rAUhBzxLwPe15kXQNQU2lUmpwZzSm3pV6IW%2FztVaQdfxBzxBBzfLEtrlK9dkg4r5v5bOBo1uWAiBBzxBBzfNHGX8c%2BMDwuo1KFxsyPZV0OiAgxRwz35rikf6ipFGOlWlLVRTVq6GDW1YAoEHPEfEIqI%2BZ45XGgSuJzifED%2FFjXAtQQc8QQc9xz%2BU6lGmst9FX1YF0JUEHMEcNFK%2Fc89jdQqabbOJyuizF1ioaYI%2Ba9vu5z1jWAxXlcylR8fkfVZecpmCWmWIg5YsaYYctZ1wCSMDxppFp0PX9tbUfWlQARxBwx4%2FB3iLn0QWulqnb71eU6vu9YVwJEEHPEjMPVi1jXANJxz7umseqvoFmtWBcCaYaYI4aYS3dsW0dmeHCs0ayDrAuBtEHMEUPMpTt6dWgze5WqB56wKwxijhhiLl1S14z6fZSqTWTK3wmygZgjhphLrzTd9mmWRGE9TuVAzBFDzKVX2kdWXTcfH%2BnFug5ILcQcMQwoScc83v7ZSaXyiU75O0EGEHPEMDw4fdP0Wv%2F46PPs91jXASlDzBFDzKVvWpU63%2BlHF16l%2FJ3AGmKOGGIu3VN75QmfPURVj3UdkALEHDHEHAiCe%2FiB8U1Xdsa0fllDzBHz3lwTK5SAoDdsb%2FA%2FzACTNcQcMcQcxNM%2B7urQb2Jt1mVAMhBzxLzXd8KWNxBP5bpiZrRnDOsy4GsQc8Swsxd8YOfnP6j995V7sq4DvggxRwwxBx%2BorKqV%2Ff3HWjihkyfEHDHsBQEfs%2FquzqFIHc7n5AgxRww7e8EnyhTvtERbjXUV8BnEHDHEHHzCI0T396SqrKuAzyDmiOGiFT6lDxvctun11azLgE8g5ojhEQR8gdGqXc2%2BebEFmKwg5ogh5uALdKGlts0ciCU3ZQUxRwwxB19k0NexafCUdRXwEcQcMcQcfIVz195Xi2H7L%2FlAzBFDzMFXaEu6d7z2lnUV8AFijhietMJXWY1sU3%2B6FsszyQRijhjGzcHXqUo96%2BA60Y91GZAAMUcMMQfJ0Kqts6zCfyDygJgjhpiDZGmK7PNYe4d1FSAg5ij4%2FLF%2FAOsaQNbcMrpXecO6CEDMUUDMQUps%2FHecrtKDdRWAmCPmduQgYg6SpbI2dqlcfw3rMtI9xBwxtxObRrKuAeTObsgvAS1vs64ivUPMEXM7WgHvHqRI53fHdwSm8jOFH1RiexoXw7sHKdJmr1LANYp1FekbflCJ7WnSZyvrGkAJinZUVX7Juoh0DTFHzH%2Fy%2FKmsawBF8Ki7f4ATRtCxg5gj5j8uBPubQGpoIwodfviAdRXpGGKOmH%2FAXsQcpI5JpVIVZ11E%2BoWYI%2BZvMs1iXQMohUfUj%2B52uG5lBDFHDDEHqafVZN365j7rKtIrxBwxxBykhXXdpXnWsS4inULMEUPMQZqo7SdVHodxwiwg5ogh5iBtdOXq1LjHuoh0CTFHDE9aIY3srPKvDGVdRHqEmCOGcXOQVrZVG5V%2BxbqIdAgxR2yPD2ZBQFpVjQjuhwXopIaYI4aYg7QztnGp%2BJh1EekOYo4Ypu4DAfv8m14i5ySGmCOGhZiAyDzVnesYVyIp%2FKASc7v0P7x7kHburc43wUY4ksIPKjG3I9uxSDoQKNP8ekvknJQQc8R8%2FmiIfVqBhPPJIo99MBpJOog5YtiOGghpxzQofBzzIaSDmCPmfdjzIusaQKHynHnwkHUN6Qhijpj35prPWdcACmUzyMUd2xpKBjFHzBgzbDnrGkChdBG%2FnO3Huoj0AzFHzDj8HWIOCOnH9B93qBXrKtILxBwxo2rEItY1gHJ1erXEy491EekEYo6Y8y%2FzcrGuAZRL712mxSHWRaQTiDliQYvu5mZdAyiY%2BtfBZ3PcZV1FuoCYIxZkOwQxBzTmHb2cC6sySQAxRyzoxjon1jWAso2uPgGTISSAmCOGmANaZTbuxBWBBBBzxBBzQMs585EbI3E%2BZ3GIOWKIOaDXbkr9nMg5S0PMEUPMAT2bCm8C3rEugnuIOWJBN5bhvgrQWqTx34JRJRaGmCOGcXMgip%2FaY41NC0PMEUPMgSjyjjKyLoF3iDlimOwForBulLNVFOsi%2BIaYI%2BY8p09O1jUAD3w8Tyxfw7oIriHmiDmrMyDmQAQquydnXrMugmuIOWJYiAlEondVd2VdA9cQc8QQcyAWK4dDvQezLoJjiDlixuHqRaxrAD5oA2refsW6CI4h5ohhkXQQjXXDMk2wBY7FIOaIYcsbEI%2FdHBVGYVoMYo4YYg7Eo%2Bp9sT%2FrGviFmCPm7Xgf%2B7SCSLRhpWucww44FoKYI%2Ba9vtNF1jUAPwat2vyAdQ28QswR8z7siZgD0dj3bD6xI%2BsiOIWYI%2BajUj1iXQPww%2FDk5i4D6yI4hZgj5vPH%2FgGsawCOGH7MO%2FIIlp6zBMQcMbcj20eyrgG40rSU9z3WNXAJMUcMZ3Mgsk7DDupZ18AlxBwx7%2FV1MaAExGTVMUv7h6yL4BFijpj3pnZ40gqiWuDwd23WNfAIMUfMdZltU9Y1AF80dguxIrUFIOaI%2BfzRdWus7TuN%2BcOYOGshKk5tl7DIRIaoOI1K0ETaC%2F2SvvOtsKm%2BYDPB%2FFFcwvsdFyXYvW8lyiHxz0hBcHj%2FuUg7IX7RbAdzu%2Ba2hbikfyZzT9Yfeo82%2F1KZPxOjiRTeHxojaGLjj7RP7OP952MSDk3qxSHhV0zCZ8y%2Fx%2F8lvr%2F4LvoJ8SW%2Bdnhfz0ccEpsREsuIef%2Bn%2BeXEmQ8095p4fEIDcfEvyVy1KqHT%2BPrMzav6vU18A9TxR8clvJD4Cs2vNO59Jyo782fsopNeV1z8i1ebvx6rilNZVS3ps%2FvgjdgcnN%2Bh1z%2B%2B0j0QD1tFh5ijYGMfY2W72%2FxB9dfmH91XDjYJOzTZxj3LaF9xVK6GVToLczqvjv9UOUE43niO%2BYP9QhXzbxviPrzvcWM6mH8fIoyaLwgdkj43XxgkjBbi%2F149SoiLeyU4hid8PmN8qLwX34Cdue8h8UcmHjpEEPIMVI0WBk4x%2F%2BWXMULS54fEfzDqfdMd4n8NSfhE%2FO%2FxnxDmDzS35hlfrvlbXndI%2Bs6PfWgnoa0hSX8cjorPYzvza%2F3oeGF%2FOfN3x39td0JRo%2BJfjvmLt3LPiX%2FpD80HH45KeCHmV5bB%2FJJeOSY2G55BE%2FMqgyZWFd%2BiMGRPrDmto6MFa%2Btwx9c1ijhUzzr5eU5P693hXSbEcDxboOyOqdVY18AfxBwF5%2FLHyyW8g4npc%2Fz9582p9uGN%2FTCCmORiJC6Fr3%2Fyrxf35X%2FNuM%2F%2FmeOSyk7LP%2F%2F7YlSJH6r%2B85mvdvqhpke5%2Fv1KSj68p%2FFvafy7KYTWyr3O5%2FXxhXb%2FWK3MNTMmuYOVbUGv8lhfU3SIOVCQwBfTm91o12PLlfV59rGuxTLUuS9cjWBdBHcQc6A8nb1%2BnTHPzukyjxevNnVuO%2BJ8TmSIOVAe08Q8tb5zKuDL4006rXpgtTDWRfAGMQfKFDjqb4c1h2%2Fm5G847YazbqxL4A1iDhTL50G3agNi3%2FIWdO4XDyzH01ZRIeZAyeaXOL0vhrec66L%2FkcOrcZYQc6BkPvvXrm28cx%2FrMsRVpq%2BVI%2Bsa%2BIKYA4XT7Lt662DO%2B6zLEJHabnkxnl4Pe4g5UDz10Ti7sTxd5unOTavKugauIOZA8QxhZVuvj0z5%2BxRD67bkeMrfBamGmAMePKvk8jaadRHisbn7xHUN6yI4gpgDLuRymjw2lnUR4llnVQR358SDmAMu%2BI8o%2Bu1N1kWIp5avAUtTiwcxB3wIfFT42OV3rKsQi3Odl3dGsy6CH4g54EW43fAH3CxJaTu%2F5BPWNfADMQe8cLuUofIt1kWIxVDoSSDrGviBmANu%2BHRvXyQbJ8uo60Y3Ocm6Bn4g5oAjr45O4iTntCXnvSh6kHUVvEDMAUe8t1%2BYv491ESLxfLiEtzUJmEHMAU8G9Vv2O%2BsaROIyx%2BkV6xp4gZgDnpj2O%2Bc%2BycfjVkP%2BLk1DWBfBCcQc8MVxRrm2rGsQR%2BmQExzN62AKMQd8GXThqtt51kWIwtpv5BHWNXACMQecCej2Xd4sPCzLZKgx5xjrGjiBmAPuVGiy0YmDh5T6J5OHVOnBugouIOaAO3ZP1xXlIB60mjmz%2FsBDCDEg5oA%2F2gbTf4hiXYQIyq71mMO6Bi4g5oBHl4PPxCr%2FutXWeYM9H6NjGEPMAY9sjyz7S%2FmjMfTtKufl4WEKc4g54FJWp70tWNdAzydwQWXWNfAAMQdc8srdv88d1kVQszIW7ce6Bh4g5oBLQfnurnrBughqzhrdeNY18AAxB58wjum01v6UIBQRnrkLQpkGi6O9Dj6ye5c19rUg2KsF4alVFpUQIQiJC5LnFFwPxv8tWhP%2FtxhrQcie1I75k0KsdcKHCd%2BaNeHDWHML5iaEaLWQ3S0k%2FnuyC64b4o9XCe9s573pKNrrcFi45cEb0VpjQ%2F94k7MNHkJQQ8zBJ3w2vlw%2FpY4g9BSGHhOGlM6x%2BX6GCjUaRp4RFp8R4iY3O996zc%2F5BgoDhJXx3%2FzjOKFYBeFFI2FFwlnHaUFYlympHfMnhW%2FzJDa5xPxbsRUfuijpINwclqlVt%2FPmZsYJwv6Z8cf3ETTlbp8u5PZiydq1oryQzG0Kr1X8Y4ilM2pVY12D8iHm4BOm3luXDB%2Be9Jd88b%2FFTzn66Nqpv%2FlX%2Bfd%2FSWE60lcvufon%2FF7%2BowbGC%2F3Lt96%2FdnSHPdlj0ljylxnv5x45T5SWGFpWdWgr1jUoH2IOPrXUc9SJnIwGnekjOtkWbrgmXwYxks4YsqPGIIUvJrzg5mk%2F1jUoH2IOPrehQQ1xTqjIWNsU%2Bymb%2FgcRtp1%2FtXHzbfpWWLLa8mIxco4WYg4%2B17ja1RMs%2B9eqnwTfKyJC0vo33LGBvhWm%2FAbVmMK6BsVDzMHn9tRo77qQaQUq70tTDlWlbiZIVemOvwjlMGS0Lt%2BFdQ2Kh5iDL3B13dDtENtpRqqib1tvp9543ntbYW2lHWLUw4qztnpd1jUoHmIOvuhE5hGsx2s56NdNoB9GN%2BnSoAFKnsSvCrwZJt5gwnQKMQdfZLvq3QyWjyHMnKuHHl1P3crmNyvtbolQDStazdkGWg5Wz2MKMQdfZn980k3GJahKbZiYlXpwrNd31k1%2FVfI6H%2BtG5EfM0UHMwdcU10azLsEtf7%2FK9FMiNtSu56jgnHO406kn6xoUDjEHX3N9gIr1TS21ffFo%2BuEUnWts6aXg8yG7vP9DzNFBzMHXNK72tC7rdLA21GszjvqH3Pbcjg3KncVvyD53G%2Ba1UkHMwVdl%2BN%2BkeawftwqT4y5G0J%2FMXB4TxfrMlFzGIZUYPw1SOsQcfJXPJg8vtqOEzYyzDCXpt7dSl8oYJ0IxbLgFF17HugZlQ8xBMm7pJp1nfT7nO2y%2FI3UjSw%2BOGHlBhGKYMHZcPpd1DcqGmIPkHDhcYO19tiXowxfkfUfdSmevadFK3dPQ49KIfKxrUDbEHCTH7cjELW8Z16DzHf8HdSNBDwud2y9CMSwY%2BpfEzykVvH2QLKMxx4tcjO%2Fea3L3L0E%2FhC%2FbL9FHmA8EJKLTDHR9wroIRUPMQQqif7rHejCGUePVhL6GLLl772R9o5FQ06VHWJegaIg5SEHn2TupFwqhpFXnmGpF3UrQlku5I0WohoEJDxqyLkHREHOQAp%2F1Z8ffYF1EGddz%2Bg60jQRdXZ9LjGKkV8Y3D%2BsSFA0xBylx6z%2Bg%2BQ7Wo87c6j14Rbv3i%2Bl5iTvrFXl7zvp2ZD%2FWE1KUDDEHKTtazIf5YAxrk3sodSPZCgb7KXI2RB3Drwq9rSgLiDlImd2GW%2BVZn0zo252a3pe2iKXds4e0EKUcia1bZU19zZ6OIeYgFea%2FaNsthvVpUJMOhai36fKvsahvB9YX4AQaz7%2F9mHUNCoaYg1S5sP4s49kQgoch36CD1K30GtosK%2BvATruiGec%2BY12DgiHmIFWyFZzH%2BrJV92Te5KnUrehDfN%2BynteRdvZb3A6xrkHBEHOQKm6zpjvSn0rR0f3615HN92hbuT9Mp7xZXzb%2BbyqzrkHBEHOQOgGlHs3PzvpqL3dVv2K0YWuauM1ukNLWTFcvPdfpDusilAsxB6kTZFvf6ifWt%2Becn0ysSH2PytRs5BOlRYbOqnNlBT45kQvEHKSWw3abIaxnt1p3eDTOn3bb0s3Vfq4zTZRypKNuXhhXrcQQc5BqGfcee8Z8B%2Ftq%2FUflpF00fX79UVeysb4ATxtD2VWYvU8MMQep5n0kuxvzJXhtbI8Upb09F7RobMYJrC%2FA00bf%2BXQv1jUoF2IO0mDT%2BjtvWN8iKrO5Yx%2FqRjLOD%2FuD9QV4mujqlyrKugblQsxBWmzSHV7DuARtydV3h1KP4cvQve5IJW2XpSu55i%2FWNSgXYg7Swn%2FKP41Y1yB02vc97VMIIejGsFXzxChGIog5Gog5SIvAElmPrGJdhMY%2BoBL9DM9GN9esZP5AJdW0Yfdu%2FMD6RFqxEHOQJia%2Fc%2BFDWF%2FtmaY2FGHR9Fsjs54QoRipbDo0N5h1DUqFmIM0Olfot8Wsa9BmnGFH3chy50vLRKhFKhmbt1fUQxM5QcxBGnnN%2FfsW85xzuPGkYAhlG97TGh3wEKUaSYxp%2FTyMdQ1KhZiDtPIv9XxhdtajzgbtmuWxlrINk%2F%2B%2BBafod7qWiE9b3UvWNSgVYg7S7l3HO46sJxFodhrpozasYtOrSpnFb7c1%2B1PWNSgVYg7STrNy0%2B1YxjXoC2elnxYQEJKrboAIxUhBY1yPlTUJIeaAwHyPLswvoDxaZX2pp23EzePs8yjW8zpSR1UmJi%2FrlU2VCjEHBIK2eO58x%2Fpqz66ttiX1IpvC82OLmb%2BS1Clacxnt3cj0CjEHBEwTlvb4hvWabfpCjR7Vom7FZa6HhzL2bi095KfVrGtQKMQcENGXWzqc9eBaVeuxR2YPpm3FZU2ZqmJUY3Hum%2B4%2BYl2DQiHmgIz%2Bzuy1Tqwft%2Bat2qcy7YWc8VGvzoNZj49JDdtm4X1Z16BQiDkg4zxnRNE3rNNB3cDel3bgm2nipVJlWV%2BAp4Zm5cUqrGtQKMQckMpc2DH7XcY12P7y%2FTd%2BtIsJa4Znv3ZSlHIsyiPLzg2sa1AoxBwQy1Sn0D%2BsL1uFOn%2F4V6Ntw25xoUnvmL%2BSlKh%2BdinHugaFQswBMeP426eZj8UwrZ9DfekcdGPr8OXMX0lKdE8nYwFhMog5oDChdVPWJeib%2BTUeQ91KlsPz%2F5L7KGFdREAJ1jUoFGIOKGhtD7VhPffdoKnRdxDt7Tkh62wrue9pqH0cWIx1DQqFmAMKpqGn7w1kXYS2wfistHt9CUt9d%2FcWoxgL0oY%2FYz1SUakQc0AjYET2ltQRQ8uqhmc52im2znkmPBsnSjWWoz%2B%2FjXoSb%2FqEmAMqpq4nhjPf09DuV4Mb9YPSnH6uw2Q%2B6yvsHtYoIYKYA0rblz9gv3h305e2KtrlOzLlmW2S96iSAlMysy5BmRBzQKlz47hxrB9DCLWOHCxOu2j60h45y98SpRpLaVK9COsSlAkxB7T2WBdjv3drhpnfPaFu5HrdPNnpl3aynBZu%2F2NdgjIh5oCaUZ%2BV%2Bd4QgqF3L9rTOUGoWEN1ScbXrVV%2Fu8q6BGVCzAE10409%2BkjWRWjtu9FPhVp%2B%2F8JtGT%2BGaHyA%2BVbgyoSYA3rL7ab2Yb6lgqFVXEfqc0q3MTK4AP%2BqMSumsy5BmRBzIIKlM1utdWI9KdRU4nVwK8o2AnvffNuB9fiYr2pzcyTrEpQJMQciCNJlW3YxhnER%2BifDcuWjbcQ4y32YbOca1KrozroEZULMgSgyzn%2B9lPWwEn298Xu%2FoX1Sql9wfSPzG41fseCaB%2BsSlAkxB%2BJQFRi0g%2FlgjLyn%2FylFO%2FcsoOrQKEfWF%2BBfZt%2FJm3UJyoSYA3E4H3e89yvrdHB%2BsjHja8o2Alfn%2BGUE6822vwwxRwgxB2Kp%2BETH%2BvacYL0m01Qr2lWZMmz7c4ssh5UY1UtYl6BMiDkQS2f7pw%2BiWBchdDXVpd7TUNfq0S05Pm616%2BTDugRlQsyBWAIHXLk2jHURglXurdSXzoEldG%2FHsr4A%2FwJbzVbWJSgTYg5EY3hWbdBk1nOldM9uj69P3cq9qi4yvGy19vRlXYIyIeZARD%2FtbP0P68etBm3072M6Ujaiv3Vxgvz2brUKysu6BGVCzIGYgufH0D7ppNe4eg3qrLX6pvhNEUoRl9Xc%2FKxLUCbEHIjJpUWHLsyv9my%2FH0Y9rMR%2F7FT7jawvwD%2BlMSxlXYIyIeZAXJ4L2c99XxB3ZcMc2kYqhpe7K7PHrWrbbaxLUCbEHIjr%2FrDrOZkvPieEts8WSbto%2BqYXi9lfgP%2BHeuo3rEtQJsQciMvo0vYW872%2BBIeb4Ta09%2Be85rwewPp5yn8h5ggh5kBk9y8%2F%2BpP9Gh%2BVN16l3dNQHhfgH3NW%2Fca6BGVCzIHYvAcV6RjD%2Bu69of9hl3q0jdiO3yarvSGcw7ezLkGZEHMgNtM4o%2B981nPfternAdTbYBldnn8np72%2BPPbtZF2CMiHmQHzqn7NcY34WZFtoSm7aZyH3L8feWixKNaIwhO5iXYIyIebAAuZP7Mc%2BHcoM97tAu9mXcfrpEEf2D46TqNR7WJegTIg5sACf4fVzs65BUFVZVm0uZRum6Coz7r0RpRwR6NXLWJegTIg5sIDA1QMvb2O%2BKJM%2BW8Z5a6tRNmLf49wb5vM6kujUy1mXoEyIObAIq7WRC9mfBeWr0dGRdga%2B140m23OxfnCcSPdwH%2BsSlAkxBxYRNHjn2r2sixAM3V92uk3ZhulRl4hx8rg9h5gjhJgDy9jjeTYkiPmcUOsiVRpTNzKwkWNb%2BlJEoFfhopUIYg4sZeKMve1Y55w2%2FJ5XDdrbc16F22nai1IOJTyCIISYA0sx7c16n%2FljCME4NwN11rrszkJ%2FTigClYr9fQBFQsyBxXgt%2B%2Fkm6y2qBUOWuCEGyja8N5y5dmaHKOVQwSwIQog5sKD%2B7kuY37xXGXuMOUH7uHXgu4d3WM9fw9R9Yog5sKDNGcdkY79DVmWXeqtp927N2PVlBPNRJViIiRBiDizI%2BKxJ1j9ZFyFYd3CuFUnZhuHlhOCsrHMOMUcIMQeWtDzzJtUF1kUI9vo2V1%2FRLiZcYV1v1ssRYJF0Qog5sKx3f287y%2F66NexkwX6UObfZv2J2xo8hNG4j2BagVIg5sKz5b%2FNPfMu6CMFnxtD%2BlI8hnJ8WbrdanGpIYZ9WQog5sLAcBcpcZj%2F3PcPDG08om3B1Ot%2BE7S4XNiUDmPavWIg5sDSjYwfGZ0Fm%2BtCMP3SgbaRCcJ8YlhfgxiY%2FMOxdwRBzYGmmg8O7sa5B0KqWr25K20jA%2F6a%2FZXliaj%2BkEsPeFQwxBxYX1OLw5pOsixDsV8flpJ17ZlVmbg%2BG03QXXPNg17mSIebA8jqf3fsD%2B9mtDt%2BXP%2B9H2UbQ3w3msxs9V7pqA2Z9KxpiDiwvaGTHwVVZFyHoIx5Mr0zbSEC9EbHMcq5Mm5KsulY2xBxIwPjGpU4I8zmhutpTz1WgfVZ6LuMZZo873XpnZ9W1siHmQBIHSm2fzjznhCoFDs2hbGLpz4tCTohSTNpV1rF%2FlqNIiDmQRJBv2WLsZ31ZGZ%2Bto2zCVLzq77NEKSbtRvx0hVHPCoeYA2n4R65pdoX1qkz6J5GL8lG2Edi7T4eRtAs7kZnaidV5pMIh5kAqzwdeZL7Ips76mI%2FDYMpGMmyL8WcyfG7TutYsulU%2BxBxIRXN%2Fwl3WSxkJgm%2B22pNoF5%2Bzc148UJRi0iiuQV8W3SofYg6k4jN87VHaZd%2FoqUvv3O1I2YZPcFF9dgazvu49C5O%2BUx4g5kAy87%2FJvJr9jgoalwvnctDeW1u9%2F5Lkl63akqv6tJK6Uz4g5kBC31a7z37tOWFHk6aRHemasL29eYfUr0T3dHJRibvkBWIOJLQl1zb2p3OCKbD5asrhc94j9h%2B9LU41qaYf0%2FS4xF3yAjEHEsqw99gR1iuNmy26XqZof7rFhDu32bZfpGpSS9U7qonEXfICMQdSqpR1DO2eDKJ4cm4K3eNW5zn9m0l8YuphOBUobY%2FcQMyBlBxPTDjPuoZ4ZX9uRTe51eTgGBwt7Whn9e5n2STtkB%2BIOZCSV3vd5LusizDT2J%2B7T%2FmotKH2srSTdK3HX28saYf8QMyBtOYdkPrW%2FRcZ6%2BkL6qlaaPzr5GhJRzvbXZ5dW8r%2BOIKYA2mdX3mAdQnxdBo777pULTjX1TyT9HGK%2FRRHJyn74whiDqQVNUQmoyL85vVuQ3Vi%2BUsm1T6RakmVRVNnst8hTZkQcyCtnC4Zc7Gf2RqvR599FWlOx7JnXSrlzFZ9swZb6knYH08QcyCtoAeXY2UwdM5MF24q95LieM2bBctEKyZlVsatg2QxGEeBEHMgsV23f4thXUMird3FcWfJh8%2Ft8WhzVcRqUmI92fW5hN1xBTEHEtu75tpr1jW8l79pCZrHrXkqSDhyzljZ74V0vfEFMQcSa3RpPLM9Yz5lNaZ3U4oLwXx5JVwndEGMa6h0vfEFMQcSy7H1%2BGrWNbynH2CqTTHk9ubGNeLVkpLcT3azX61PoRBzILHOjwznWNfwgc76xOhXxIu4lXH%2FU8xikueZo9Nb6XrjC2IOvsaU9OenV3Umuud9hicTD8hjREmCNqHjM5Iusnnp1GxRa0nW1JJG1jsGKRZiDr7E5DBcVU71yPxRrrjjccLwyB6CKU6Y2FeYqConHI%2FrR5F0QcN8d8lomKt66Qsn0kXTN8yT8KFAgcwOo6XrjS%2BIOTkJ0vlkXnl58ZY6D1aMF%2FL4LDF%2FqtjHX3%2B%2FuEfSJ1ck%2FtHso6989NXErzcTzrcWhCXvP5W0M9SST5pIaCO%2BkdaJX1zhlyfLeuHNJvPf6tsKDYUTYeeL6aMLZnkmmD8tCA1PlG7TTLjRVFjoX3CYX1pfpSnTbjb7%2F32Z2iN6lJasoNmnn0h2Xup89GB5Ob1tioKYk4XAgw9XGb02ZL6zenJUgTFnTnXrPn5iWFxsrEqIE%2BIShjzcT%2FiXSvznikv4TS8UTQi3%2BF2e76s%2FaixO%2BPfr5i8%2BVAtxce%2B%2FpkpoIS7uv02Yvy2hfVX8181fjKtx5r8%2FvrpQVWzOjz6lVatUD6xVglPQghFp%2FkE3zVopr2Gu83bvdCeqaFKWXVLFnLbIj%2F%2BTqCsOIeZkwfXK6p6F%2FH%2F735sao%2Fs998sVd9%2BcLO%2Fz6uuSvv7Vf8O4xC%2BqPm5I9dGBH3%2Bk%2BvBn%2FKfiPv3pNVfzn60PtPGR%2BEBYtc%2FVd20KVX5GdjFnnOtSJc2vIp7624JSxZzOsO%2BkRF1xCDHHWJBt976Zli%2F23DXU%2FvrbOJnM9kwlTfvASwQ3p0wz88hmfHACH6eWs4m2wMmRP69U%2F2D6WhUKS9QVhxBzDDm3mf9O81fE23urZDLLM21ULgcMhwiOM01us0%2FsWihorRb8%2FDiY6FCr80OlijnVqoDxEnXFIcQcQ8srzWg6zbnDT06xn10kKoHLBJft1QiOk1nMGfLZlH5Atp9hhmUrpPqHcz71ZwaJuuIQYo6VHEtLZHux9vqpnEpMuARNPH4lmgQgr5jTPQuo%2BJTw2PCX%2FaX619PUmhguUVccQsyx4HY40z%2FL6i86IuGMSNF5ROy%2FGEhyEhQ0bM3wKNHLIebQxjeM9NiJ87%2BRKOb0VjPqhkjTFY8QcwyYes4MbTTreWysYk%2FkzEqX%2BLEg0eY1g%2BaMkNMsiKGN%2Bg0mPbZC7T1ilpIM65aZ78rr8bSiIOYkl3FoiF2NdbGVZbD9PIXS3VqXINu5XrNl5XWRiyGnnvTNijdkN%2BbMPH%2F2F7OYZLgbvEinagBiTmrzO73qP3DBeTns4UdD93hDs8eEF1GOp3YvFLcacna6HrWIL1kF4cJxqV6Jb4HKWFOTHGJOWtvuWO%2BSdts7i3DOt6Q36bK75w7pZLPeXJ8R58dTXAv%2BEC3V0kgTutx%2BLFFXPELMSSjboiFHT414m9LkBgVwcF5G%2FAO%2Bt8qIfSKWQkFbJ3jPAOIbc2YF80n1LGW17Su6XWXTN8ScVLz2RpSu6BGo5IerHzgsKOZAfN1dsdtCebwJhohrZ%2BwpjjeOX5FJomHdhn61Cx6UpisuIeakEbhm3YNZl2Sy1QstVUD0BLJpA2bO868clcetSXtN1%2Fo0x7uuG3FWrFpSYHdNys11%2BIOYk4TP%2BrM3d5924mNZRI%2F7vtXUxEm1yT3w7IOUv00Cq48%2Fb05zvOrY7m1i1ZKCMgNzSNQTnxBzEggatGpN6SWcnMoJ1hVKd35EfnjP%2BlINwkjBtI4%2FRVINRcs622qaWMWkoLYX5u3TQMxZnPPY1j97D2NdhWj0P7cIe0V8tGli8M7tIlZDTGXTrSPd7KlA4%2BG%2FpPpfV%2BA%2FFBvzAGLO8hrPPbH%2FqIxWBaejdd7%2BYDXJhP1EhteN8sliXHSbceOykr%2BMeJurd4qW6up7Vhls6kUDMWdh9%2BMaLlstk5tRYlA127qe4nD7Ts9uilUKjQVP90yhvFO6t31eiU7mtJqmx7ENBA3EnGXdX%2BZ4jaeRAOpf3gRSTCH3%2FjnHBBlsaKBVFyz%2BO9F6wf8yfb%2FoiUSPlAwRW%2FFzSgVvn2X9Nd1oI4uLNJG0M10ZTTr9wUxr2%2FaKDB436yNMmdbQzYQ37c%2F2%2Bp1E01nsfi5lkKYnXiHmLEm7%2F%2Fryt8qf2vUvvx7fuaZ5J6%2BPRD2dL4fQL3potxNlE%2Ffvl%2FAQpZZUGGPIFytVX3xCzFlM4ITQ0J4x8hgJKw59%2BJKl%2FSiO9wmxL5xFBqnv3kefmXL6gveuu4uI1hQloCv06%2FIOEvXFKcScxeypEbZvbYwMfqpFY6ebN55mCqjueMgGOTyMmZ%2FHvQnl4m2d7TNek%2Brq29muxVmKGwWAmLMcbffl%2FXdKdfdGEnZ%2FuDuvpjg%2B4PK4nrdFq4aY2nvJX1rak%2BznoV0kGyNUelN3pBwdxJxlOIfe8bWRwTNFMXVxrUj1iirleCuDjQs9ck%2FYmJuyDTfHC3rp9vAY8fQ2Fg6mg5izjIAKQdeVtelqyl6cm0T141Yms5MM3pHS%2FfKZiJcLTtLYtEaqG3Nm4%2F5%2Bi5ijg5iziFzGeUs52yS9lk5nQzNtQL9z9UX2Kad9bN%2B5M%2B3qSWETPWdK9mjJULBYM16mQzODmLOAoL5nBt%2BVx5pqolnkaypI89Omiep4WwajIjQbhkylbCKo6xAXCddhsLJth%2FmstBBz4gtcHTWJn6n6iXQz%2B%2BekmW8U2GDDSRncqtT5nLhKeblpelQv6Jo41aSuv7WVa0jYHZ8Qc%2BLLdmhitAx%2BpMWk9n41wEBxnWZ8WrXmEYlW2k1Ovgu3fqV8ajm%2F9thf2otTTaos87zyQsLu%2BISYE5366IhsnKWcVf%2F%2BS4vTNDCo1fL9YhVDTtfYvRDF6iqJbnnkk3K1EE1AkSfYBYIWYk5kzm26DJXPNqQi8Xuwi2bAnOCz5ceb7G%2Bj60IfHM1C24ir73pJdxEvsyUHxVIJkAgxJzKng8uPSbXdk2R%2BX5%2BhFc3x25yW3Wc%2F%2FcFON2811aroZkEVJkRJ%2BrzYM8cVrMFEDTEnLs39GTdkMNJfVB419Hn0FJfhgd%2BVutaT%2Fa6NuhcFZr2lbMM27NgYaZ8XN23ZXsIherxCzInJ5JDn51ysixCbyuN6Q5odsLwdY%2B6Tr6ounjI7z9A2sWdwkUFSPn4QtCW9SsymHcwMiDlRGVU2FXg7lxPKOJfeTnPd5LDown4ZjJgTZpd%2FTttEePsoaTcTd9b0Gl0FUyCoIebEZF2yxQbWNYhMa63zvk5zR2u515JgGUxlXbSlZVHKES1u2us%2BUu1YmMTu92lDpO2RT4g5EWmmX30gg8FholL5t7hF1cCBDnIYD6HZ9Vt72nkptodKVRWlmNSr1WPMGIm75BJiTkS%2FO6zgan25eFY7us2hauDmCak2M01OhrtjO9NOQw0%2BdVjqGXztMt%2BgWawZkiDmRLP5f7NOZ%2Bct5Xr0Dc9MEw9Ofz3ZyH5ZdJXd%2Ffq5aNdsuz4gXOoVVmw26cfjAYQIEHNicX7qvKsF6yLEtqBdP1U9iuO1E%2FIPY79FrdbVJ3AcZRv%2Bpuy%2Fj5H6f2LLarnSnUpDIsScWAI6%2Br5lPwRWZJmWluhH86Cv55SWMriMN0x1zkU7%2Fc7BZZef5K9k3u5IXLOKATEnlu2rbtMOPpUbY8c2oTQPSTdfKHD6EvuUE37Peph2Jqv6YuAVqU9LdWHjKjtyNjuaEcScOHy2Tbkrhx1IxaSp%2FG0LmnG9Pruye%2F0pWjXENK3zP79Kd2MuaMv4LE0lH%2FtnsCrVS%2Bo%2BOYWYE4c%2B5zc3WdcgNt9ff6ZKB%2F2pl5JOGfiKot0q0Y4L9n%2FcZIAotaSJ1ZbddaXvlUuIOVEEjhx5hLvLi7CT86gG4P%2FTqyL7h6xC6RZrD9Cu8aEt2ylAlGLSpJZbw8fS98olxJwoHLZvP8i6BpHZVi1qRfOQ1XX4oYvsH8loNUG2jYMpG8lQvOZV6V%2BKhyF8A2%2F%2FUbGCmBOBaeLooxflMG1TRPp2Rye8pDjee%2BfaiW9Eq4aYyr4l7SgfU%2FEwTTCDyS0uc5zksOIBFxBzIvApsLKAI%2FszF1GVXf8gK83P9rZbb9eyX3xJKH1tC%2B0D0qUHYt6w2Fa8Sm0fzp5psYOYE8HSUqv3sq5BXIYK0xx1a8mPN0bmHr5SBtN7O0X7TaCcRtD5bCkbBntRaq3%2BeriWdhgMJEHM0fPZcLGbDK7PRKQbHafOR9PA0uazV4lVDDltmG%2FWIZSPH7wL%2FtFyuzjlpInuWbgMBuPwAjFHL%2BfcuVn4urwwFdn8G9V9e7tDVjJYJc3QbtPJtnQnc4HD87o%2FZHFaql4ehCWYRIOYo1dxSKAMFlQT0%2BDzBWkesgqZC9b5h%2F3sB3Ulm%2BG0D4a8y90%2BJ0oxaWTQNfQKY9ExnxBztEyTamXKIYPxYeLR%2B8%2BZRjPJKLD3sFaD2N%2BY09k4zKVNuYCQbvOZPFuyH5yVasdI%2BA%2FEHK3A7n%2F75WJ%2F6iIi48lsd%2FpSXHO6zembWdJN%2Fr5M7bShAe1uMROdN7MZ9e259Bz7lV34gZij5Rxy0sS6BlEVHXazCs0TFdvBTXvI4SK%2BrbEGbRMTg5xzMrnrap%2FZ8Ce2ZxUPYo7SnuqTN7LfaVk82qczcrymWdbcuVWNLSdEK4eYbZ%2BZGooRMfH2VM8zap441aSR1q5ofyYd8woxR8lpUOwx9hdo4jE0eFTyLM2M%2Fc1zX%2BSUwVDp2v2sAikf9jr9NeQNmyfouqzfdmHSMa8Qc5Q2zS7EYPCoxdi0OLScborR4QKNRaqFRmWXHoMG0zVh84t9IzajYjS9xtySwf8pOIKYoxNQ4PZR2q1U5KTsthtUKefS4uxrqfeF%2BZz%2B8Z3C6%2BmaME2YUnA8o9GQpbsswn5eokLM0cnV5hjrEkS1aSjdre%2B9R%2BvKYFywdcs7mSl3uHHO02E7qzOqys0rRTDqmlOIOTqeiz1ZlyAi7bRBDan2%2BbtXa9l49iPm1M1KUj0rjueYZclAUYpJuwU3mwW0YtQ3pxBzdB4PvsC6BPFYdy1X4inF8W5Hzh7bKoPnMRln%2F0M76T3DwmPMdhbPOLkwo565hZijYZrokJufSfu6xv0e06ScoOnTYJBYxZDTllzf5SXdJatp4tpFFxgtI6VVnaoyl03X%2FELM0ehco2ZDGSyqJhLfwRF0F0tZ%2FrRuK1ItFEpv3dWDcsTcckOpRjkYnZZqKhYafJtN1%2FxCzNHIZpjD6gaO6AwRty7Q%2FdcQtqVyO%2Fah72Kd15%2FyDNuldre%2FFotTTdo5ZJqYm1Xf3ELM0bg1vJcMniuKwzT31GGa453Vp0yX2Y%2F20r2YeLo5XROBq3JMYbYlmb7w09%2FZv4u8QczRCN4l%2BR7FFtNu3WGqlX9sZpTsz%2F5cTvfkYaEjdAMZA9%2B9Kspi74dEpe8erEh5yQ2fQcxRsNmnk8GCQ%2BKo%2FNtTE82NOaf8gwLZR77KfvW%2B%2BpRtuP7kNE6UYogMyDUaKSc6xByFXb7fcZJy%2BojL3U00S8y9sPHq%2Bla8ekiVbuFBu7VpgM%2F2lczWYtBpBrsNoRzYDJ9DzFE4OC4zJ4uj21st3%2FCEZom54O%2FzZ5fBLaWmp91oB9Ze3xrK7hajppJ%2BN%2B0aefA5xByF8ysY7FJsCfbLz7pT3Veb3%2FBqP7GKIedxOMAjlu7GnPHSd%2BXvsLvFmLvAlCfMOucYYo5Y0LDh%2B6NlMOZfBBknDqba4abz7J0L2Ae%2Bx90jO53omthTqzDLIUJWW7Lm4GkhCNlAzBEzvNo3jv3PtijKrxxKtQHWrJzTaXddEEEZ51adKW%2FfO4zJH8DwcXGxtyNpVjSFr0HMEbM7fGIZ6xpEYfReXJXmtrfb3mmRZ9if1lq79dlAOWIuYE2pZ2z2foine3xdq8OdOUtAzBHLka0hFxsGG5f0phqQ6jVnZktmo2n%2FpZt9vRRtG3s768QohZCVS%2FcyDLvnGWKOmPrlo7asaxBDl2%2B%2Bm0gzlyP8%2B4Hb2Z%2FLCYZGK6kHnC3cze5cznzR%2FdP3eP5gGYg5Ylm87nGwOokh%2F9OF3x2kaEAdHDNDBnv%2BFDt0h3Ypypx3941heGPO5fsSA7Gbl2Ug5ki5VTqTTflPxTwidpiq0jRgCqh%2BTaxiKCy42agkZUbY7%2FjnN3Z5rQ3PXTJTB2bdcw4xR0rz5pYM7kjRcu%2BZ5znN8YEH896TwUwQ7ZxhgXQtmOJWDGM4x0tQdSxUjmH3nEPMkQoLH83yRo5Ith4OpXo6GebdVgY35jya%2F9mU8nJz85upC1n%2Bb8t29P4%2BDLvnHGKO1OyCY9lPVaekc6128g3NiLlGDZe9Zb8sidBpwq1ndC0YXzjXZvnc3FafX49LVotBzJF699xbBkNiqegjbB8fojg%2BaJjj5haiVUPOqlQd2r0f5jvOYPk8SVdw3QPaNQfg6xBzpDw7ntnBugZKbUJ71qS5b994jJOXDB6yCkOnnaB9GPTXty0Z3mLUjvF4sIk2qeHrEHOkLp2fJoPLNRqlNRO6zKE43nXw9FoyGLRva6g6YADdIs6bra6ty87wFqOqbPRUdr2nA4g5UuPzr1X0Kky6EqW63KJqIWuBzDI4lzO0HXwzlK4J%2F8gzhU6KUw0Rj8zdbGTwTnIMMUeqh9UDRcecVbdqwXQ3vcvWksM6VLmPh6%2BivN47sOTea5Zn5suqFTvBwVN7GUPMEXJ2zOHIfiQFhbJ7r1NNLercx6kr%2B5zXhRbKX5ou5by3nNx2kuVAb2PBfoWxZaFFIeYIedX5jXUJVNy7lLCluem%2BfNW3h2Uw1816xyTK9TxNjs9iV4lTDBGtxj1LU4b9pwuIOUK5sudkXQKVhm6%2F%2BNCsvjQq9p937C9ZtS%2B%2BKUq5Krrxpe%2Bhd%2BJUQ8SjedYOVGuaQsoQc4S2r7rKugQKNoaLZ8bT7P1wqHilizJ40NzlyBraC%2BfgAdvbilEKqU4NDZRDmyFFiDlCnivqsC6BnEfU0yU0P1umcVP1g0Srhpi%2B4vd13OmGtDhHnPwlG8tbjMZZp%2B5SLgYKKULMETp%2F3591CeR8Vv%2Bjobnpbny6cbgMzuWsvxuzOJJuxJxr9kGTmT5JKu6yqTHdS4CUIeYI3RxxnXUJ5DL3nkF1EnSvS6l9IpVCwXq0Zvxquibse%2BRfw%2FJBirrS9oujsS%2BrxSHmCOWfP4x1CaQ0LuVHU53LPcvTaL8MTuY8b0ynq8Lkkz26G8uhHB7uLfU0k4ohlRBzhJr4yWBjUjJ%2B80Oopolv7jl8nli1UNCdO0fZQuDDYK0opZBacOY17stJATFHqK%2FdAdYlEKq8IjwDzYi5jHNu72I5AiOJ8%2FkHj%2BhaMKoyl6Kb7kZHtXB0ptvU%2B1dAKiDmCL04w3DbYgoq%2B4v6qqPJjzf55DwbIIOZST512rSj2ltWEOwP3ZskTjGE%2Fbc1HWbZfzqCmCN09W8T6xJI6J7Udm5M00CgcQDLTf7eM1RatoZy52afBWsY7v1g%2FtEbP7YBVtKUBmKOUP3ZclhQMs1cpmUxUA0TMx68J4fT2NGHZsRRnlNOyLg6F8t5HCN%2BOeuJS1ZpIOYIRQbL4TZ8GmmtnzY5RLPEnGCtH7eJ%2FX5mmpYzFhena8LrUcfjF8SphojBe2tOh8EMC0hXEHOErhxXYMzpS9r3omrAq%2BbVh%2BxTTuexKNiRrgnjC5%2F7r8Sphoje6kT7vgz7T2cQc4S2rmD5kI6QpvhSun3dPef1imU%2FYd%2B2UmBXiqco8VQurQ6x3MrD%2FZdXK%2F0Y9p%2FOIOYIKTHmimasu4smHlyaf7tZDgujFciUl3bmwITqfgwHOGvDFoX%2BjccP0kHMEVJgzNnMP7aL5sacsczfB2Tw3MU5YmMeygtntxN5DQz3ftCWVFPePIC0QcwRejPqIOsS0qp4j%2BO%2F0uSDXU%2Fn5zLYzKxTwDV%2FysnujhWabmJ47a0qVeMpZj9ICTFHKNSksJjTtPy%2B4lOqFk4Mz8R%2BVXTBz6nPwHp0TWRZ7DSE4Yg5Qzf1FMolByBtEHOEIkICWJeQJirfIYXX0TQw6EL5Hey3n9KFLnc8QLf3g3O96jFMB8V43lTkyHIlQ8wROnVqMesS0sTYLqoL1XjaLGsCsrDf48fZSxd1ju6S1WvWi6ExLF9J1lmU8zcgrRBzhHbeUVbMbbKfSZUOwY9rtGO%2F%2BJL%2B51XzIumaWK67uJGyCSrt%2Flqagf3Qw3QGMUdoaMlprEtIA7VxkGsYxfHeG0pqI9mnnNCj303aC%2BewGtVZbj1decOV%2BnTrukPaIeYIKSvmfPvpmtKczN0f33Yc%2BytWQbj7oA7tNNCpnkzPSqeXDMSa6JJDzBGam11BMVer3TI9zQiG%2B8uKF2ovWjXErLycu%2FWjGxjs2rXoljPsArvTsb9G0T1AARKIOUIhy1hO%2FE4Tfbjrt5Oohr%2Fs%2Fdlnn0jFULAefbML3Vw1obEh81GGez9kuBxeIoRd9%2BkXYo6QgmJOMyB7GZrjTasjM4pVCwVtk4HXo%2BmacL7Q8x67lNNazTFkwtMHFhBzhBQUc8t0GV%2FTHO9yauNCsWohp424c2Uk3cpFzmUur%2BzJ7sZcmZ11iz3BjTkWEHOElHNvrtPgV3Y0I%2BaswpYdptk7QiTOd%2FZT%2Fse6tE3ulgw38MhwbWwvOSx8kB4h5ggp5UmrNmzQkFsUKWUqfuLWPyy3Mk3irGlSn66FwBHbFp0XpxgC%2BgGNjnkpbH4gPxBzhJQyC8L6j8UuDyj2hjFeKuucQwZTWbs8oh3Rsrni5oXsdiRb5BN8IAIbTzOCmCP09IAi5rQa%2B738IZzieJ%2BNl%2FfJ4MackPvUg16US4pXcD0uTi0EDNm61OiHZTSZQcwRUsTUfW3Jf3IuoToJsh3sNkysasjpH3nkp9qPTBD2VF8RRPmclpgudEDugVXw8IEdxBwhRaw3p29WsLieasL%2B9sC3rMLhI0ZNkInyXKhR%2BaLLWS0xZ93Sa%2FUrpBxDiDlC7d8pYPVgq9ARXlQNHO2nzsV%2Bkpchm0dnyipy7bk4g9XeD%2FbT501iv4RVuoaYI6SERdLt3FsvpLmhZXi7ciLViDuRtAs8RdeAUbWmTHNG53Iel4aoCrLpGt5DzBFSQsyNvt7%2FV5prpUxaz7vs9%2FEShG4ZKZcLvj%2B%2FDdXy8DRq%2BR46SLkNGdBCzBGSf8xpe2cuuZ1mnrhT88xH34pWDjHf%2FseW093Y8rdr2Y7VEnOeC9pkxx5erCHmCMn%2B3pyh4NrtBpoGAgv%2BeYH9fTnBpdeD55T7w2Qqko3RAGcb%2F1zWVP8IIArEHCHZP2l1yVv1GsWwYCHwwMk8MlhIU1h9eX9myqeUI69GMxrg3O7UsnE0%2FwggDsQcIbnHnPWCC%2FeoLpZyXDkpg1tKmkrbr42nCwp9vel1nFiclurCfm6Qq4sM3kRAzBGSe8zVfkC3fVSGBZvvsBqB8S%2BVy5RYJ7om7DZ858dkxrxz3gM184djuJwcIOYIyTvmtKoZRR9RHG9ymNt%2FjQxmstrNK0h5V820p83sKHGKSRudoW4rGbyDEA8xR0jeMeejUb%2BimTZwf8Pm144yeP4wdKcH3ZriPrtOd33N4hajdRPXqq60%2B1aASBBzhNrrZRxz9j1LfUMTUl6FQ%2BvIYJ0pjf0f7o3prvo2P809SKRq0kBb0nA0ZuUL6TuGL0PMEZLxuDmt6sXEzlQhnPlQxrYiFUNB36xQxQi6Jnx23%2FCWfvElXWjFoXp3bFMoH4g5QjK%2BaDWMfl1sKtXSZodf%2FrlDrGrI%2BRawWk%2B5%2BNI27cHD0t8gW6QPnNgzExaXkw%2FEHCEZx5yDVyNHmuOXek65HMP8xpz2sVtRB7pJXsaWa9qdk3p9d32Y7aXJW%2BZI3CskCzFHSL4xZ9OwL9WezUvrWDWWwRwvg02WQLoW3I7k3t9CnGJSz1Chu%2F%2BpdVL3CslDzBGS77KaSzdHWtPct5997pfBMhgJ4d5tE%2BVc0CydayyWfMK%2B78DwyMGUl9ogNsQcIbnGnLrNX7O0FCtpGkNyhLWTwSQvP6eaL%2BlayPS%2FISapz0oXOH9%2F2F7iPiFliDlCMo05g9eIsTQ7YPmENH5xQ7RqyGmKTnugp2nAe9rznI3EqiZ1dCVe56hSSQ4L9MEnEHOEZHpvblFMX1%2BaAbVW0yqNl8HG8NoOutojqKayNi5b00%2FiJXudM%2BvGjZpKtSY9WAZijlCoSYYxpw3%2FbcNvIRQNuA3%2BrrUMTkd0oREPaOaqCYLd025D2opTTCppO9wY89oN8x5kCTFHSJYXrc5Zy7emOd7UvGp2sWqhUcs3imrCfuDBGraLRKoldaxa3Dzt9ocM%2F88H8RBzhGQZcy6LnKkWNQ%2FM%2FLsMzuUEoenym1RXzj6HgqdLe%2Bmd4fb1bOq%2BWI5EphBzhOS4HfWgPje%2BpTmhcGye508ZDCURVmbITNdAhqnrXolTSqrYNm9wruhZujUGwJIQc4R23lnMuoRPaJ03b29Bc2NOE75wO5M1i%2F7LdlShYnRhm2HB2UOSrZWnsvG9M%2B%2BaL1bPlDPEHKGhJWWwhMd%2FqDwL16VqYJT1VpFKoVJMO57yVKxs1x3SbUhm0%2FdJB8oRfmBpiDlCsos59%2FAmP9L8cPuP0Y2Xw0sa0yq0J9WpkW53xFCpFkVX2S7%2BYV2eBa2k6Q1IIeYIyS3m1LtXt6Bacsi%2B07ObItVCQafJ9K0%2F1dMDq7WRm6UZu6Yr0eZst64TwiXpDGgg5gjJLebaeOagOt5n%2FpK1Mpjjpe5QMxfN8ab9v868JckKc1rN47LBC76XoiughZgjNDe7vGJu8%2BPcVMf3KrQ6J%2FPFlwS97UJ7qqcggwb%2Bdv%2BKFE%2BL9RG%2FD3mzehJ2mlYExByh2O4XWJfwEZfAGQ40ez%2B4DvjZ%2B550t%2B2%2Fql3znVRLzBme5658Xqxivk4Xavf7Y2Of1TKYFgepgZgj1O5hJOsS%2FuVsk3MCzRWn98FMjf4UrRpiutC7Pldo1hY3jXAtYfmZCCqrapcP9B8l9XqdQA4xR0hWMTei16X6NPGQobjbSfabsgo2%2Fa5mjKBYW9w0cUPO%2FhY%2Fw9KVrHX1zoANV7EIunIg5gjJKOY8IgYG5KL4oQt88cLvR4lX8%2FgSzdgjg6hGzO15HPa7WMV8hc7jdbnuh7KWwuxVRUHMEZJPzOkqndbMpWlgT87pF8UqhoK2R%2BtIqtuDS3%2Fu3bK9WNV8gVYTkVP9eEnwcKy1pDSIOULyiTnNyvyZqe4T2Sy5vl2sYshpHz2%2BoKcKkAHfLs5qyYeszhFtVmxxrFA3EjP0lQYxR0g%2BMbehb06qWQPnTu5%2BJ4Pb6dY1snahOT6wYcbBzy12Y05lNyBgTP2fxz22VAdgSYg5QnKJOcOT0Nu2FD%2FdQd26VAmWwcAIG339njSnYm7aq022iVbNf%2BjDPDO9bVC%2FZslwnMYpFGKOkFxibsEPDX6jSaml9ysNzsV%2BXLDgeWP2JJpnl1YXX4%2BzzDmpLuKHA%2F2qrozY4IZnq0qFmCMkk5irdb9ha5qTIM0%2F2s63RauGmNZ12qOMNCmlORblb4EVQdU1629ZFVRi449OMrisB2KIOUKyiDlt%2BKyaY5tTNOBdcH1rGTx%2BUDlPHD%2BVqoVnLZ%2BLVEsS7SOrefeXFwiqXq9hGYweUTrEHCFZxJzKpf5zmv2vTJM6XoyVwRWrjec9Ld0VYYECIp%2FLqWx%2FKhHkNNhp%2BCsV7sgpHmKOkCzmtC446htBc7xR62Wh2%2FZp4rclfwuqTVmz%2F7XpsHhXldqIZvY5tfNG7PbRVqSZWwKygZgjFLKMfcw5FF88huZkzq52n5nM7znp%2FY%2BPDqJbz8ihROG7osziUGnmHnGucLFJkaaOzRxk8PgZRIKYIySDmNOX%2B6lVMMXxzovW%2FcT6gkz7KDB32P%2BoFogzTeizMzPNYxit%2BcdA9UBfbJd67qJfXIc3mEmz1gvIEWKOEPuYM8zsO6IczSnHq5LB4xmfzHncDdhouracalMsr5%2B0k6IpbjDqHmtzVjzrVGRF1Qq5%2FA8cUetjcR7HG8QcIeYxZzXSpcRTiuO9igw830%2B0atLOfB539ohX3%2BP20XTtaCIqzBtI0HvNvVb1cv9Z%2Fdtr42v%2Bs3NZu7ZdDnrbYaNVTiHmCLGOOd2kzV2p1k7K7tp7bNz7RepUSb9%2FvmhdGv8D%2BbeBLx4Y9%2F531cTFpa6sctsQmrbmv2BTrgUvhLD%2FdP2ZpFqcBKFv%2F%2FLXheHqTRcqXa4evSF0wS7V0YkN8ZyBd4g5Qqxjzt76iC3VFWfGc%2F7HYmN%2B2WS45zVeHWP12kF4JQgZomLjz65UGiEmTrCOilPbfVgZycruSIWkjfoyHllxTCj%2FbdIX5pRvFv9HhaiEO2wZzL%2Fih9o4vDIf8fJsr86CkLSQeLnyzb6Li9OYG4%2Ft%2F3bC%2Bq1Fm9u8GDKY5iUkuBUyYaVgHzbn2Etz11Hxya8WYlV2Ca%2FinfklxNfypt%2Bxzh2EP%2FQ%2BuY%2BsO3Lo2K%2Fjtu12G7P8jc2rtznUfut9MLuBd4g5Qqxjrte5IXQN6MsVKVcntuZIQTjV7WCJ7CNqCTsEwUPYHj9CZZj51whBCEz8ZJKyNm%2F%2FfP9R5uPlhONJny8nJIzMTfqih%2FlX%2FIDjWjuEssKflTKbv6txwhdWm7%2FvkMf3Pc2NZ525bneuZlvEWc%2FIpqywXohcXc7ceGLPQq3twrCeCa9ihPklxH%2FiiblgcxVzeo9eYPvqrUo18%2FoSLKaUniDmCLGNObXd7yb6EyGA9AExR4htzE3IWmkILrUAUgcxR4hpzGW4%2BNSEnd4BUgkxR4hhzOnDm795i6EPAKmFmCPEMObsJi74gWpALUD6gpgjxC7mrIfH1HzLqG8AJULMEWIXc7V%2F8GA%2B3x5ASRBzhNjF3IuH%2FfGQFSANEHOEWMXcoqoej9cy6RlAqRBzhOZmn8aiW5ch%2FbZFsegYQLkQc4SGlmQRc9rCxTLWY9AvgJIh5ggxiTkP96xlHDEbEyBtEHOEWFy0quxH7u8jea8ASoeYI8TiEYT9Bh3VDjcA6RNijhCLmJv85G9sUwCQZog5QtLHnKGES9bO2KYAIM0Qc4QkjzlVqb3uU6TtEoAPiDlCkj9pLVqieDNM8gIggJgjJHXMmdZ%2FX4JqZ3qAdAsxR0jamNOVKDd9g4T9AfAEMUdI2phzLmalwUNWADKIOULSxlzjK8seS9gdAFcQc4QkjbkyW4s9PChddwB8QcwRkjTmxn1vUmHzBwBCiDlCEsacqcQz7RupOgPgD2KOkHQxpx52bOQrifoC4BFijpB0MedwvdVgiboC4BJijpBUMafT2G8c3VGSrgA4hZgjJFXM2U%2B%2FWUOSjgC4hZgjJFHMqWr1eIY5XgBUEHOEnh4IkKKbaZrhsdjIC4AKYo5QRIgEMWf4UZe3yxrL9wPANcQcISliTjf6Rd4sFu8FgHeIOUKnTi22eB8m44QrwRbvBYB3iDlCO%2B9YOua0ofPybMemrADUEHOELP%2Bk1ZD15DELdwGQLiDmCFl%2BLwhTp3%2BeWLgLgHQBMUeoe6yFY87h%2BxKvWlm2C4D0ATFHqN3DSIu2b%2BXVfMAci%2FYAkF4g5gitOHHCks3rsv%2FpmamnJXsASDcQc4TObNpuwdZVdhPdnlqwfYD0BDFHaGycJWOuzaK%2Fn1mweYB0BTFHaN9ZS07CKuDV5Y4FmwdIVxBzhCwac%2FNa3%2B2HvR8ARIKYI9S2tMVizqrdvVEvLdU4QPqDmCPUJsdJC7WsLRi8qriF2gZIjxBzhCwXc2V8ag%2F1s1DbAOkRYo6QpWJOH1aj2yrs%2FQAgIsQcIUvFnE2P2tYWaRgg3ULMEbLUk9Y%2BY2%2Fet0jDAOkWYo6QhWKuWMiAcAwlARAVYo6QRWJOF%2FZgxgzs%2FQAgLsQcoeE2FpjsZW2cPCkXTuYAxIWYI2SJmLPqlbX2K9FbBUjvEHOELBBzul%2FUblFiNwoAiDlC4secNrz0t5EYMQcgOsQcoTOLxV5W0%2B6G%2B1yRmwQAATFHTPTVg43WLUKbi9skAMRDzBESPeamZevthoesABaAmCMk8iLpOhvfqn9UE7NFAEiCmCMk7iMIw%2Bh6phoitgcA%2F0LMERI35tzjfJ1EbA4APoKYIyRqzGmWX5kaLF5zAPAxxBwhUWNur6mXeI0BwH8h5giJGHP6Rz3aR4jVGAB8CjFHSMSYK%2BpRO2CwWI0BwKcQc4TEiznj3IN1Y0RqCwA%2Bh5gjJFbMaR%2BZrHZhWDCABSHmCIkVc85aox%2FO5QAsCTFHaGycODE3JlulJ6I0BABfgZgjJNIi6S7FXMJ6itEQAHwNYo6QSDG38tYk7P0AYFmIOUKixJxm7M01rjiZA7AsxBwhMWLOYHU60yERagGA5CDmCIkRc%2B6PVzjcEaEWAEgOYo4Q%2FZNWXbi9zgWzHwAsDjFHiH7cnPXyE3VFKQUAkoWYI0Qfc74Bp7BdIYAEEHOEqGOu2MG7xQ%2BKUwsAJAcxR4hyLwidvnD9piFiFQMAyUDMEaLb2UsbmneulWi1AEByEHOE6sVGUhyt9rctjwn7ANJAzBFq95Ai5vS%2FBk2LE68WAEgOYo4QTcxpxxlsRCwFAJKFmCNEE3P6oz%2BOFbEUAEgWYo4QRcy5h1uX6CBmLQCQHMQcIfKY81C3%2FPaxqLUAQHIQc4TIY853%2BA7ftaLWAgDJQcwRiu1%2Bgeg4lc0NH9yYA5ASYo5QyDKimNPru96pKnYtAJAcxBwhwpizWeobh1XRASSFmCNEGHPVynbGQpoA0kLMESKKOU2pZ%2Bpi2HsaQFqIOUJEMddG028QUg5AYog5Qt2vp31AiZ9TwZ%2BCLVALACQHMUco7ePmdGGlQm7fs0gxAJAMxByhtMecR5vQUhYpBQCShZgjlPaYa9PG9p1FSgGAZCHmCKV5Wc1Ogx%2BN62mZWgAgOYg5QmlcJF0bOniXerSligGAZCDmCKVxZy%2F1tiqNs3S0VDEAkAzEHKG0xZyVbdvOTyxWCwAkBzFHKE0xp2vs9zLccrUAQHIQc4TSEnPax%2Fev%2FoAJ%2BwCMIOYIpSXmDJm%2F7WLBUgAgWYg5QmmIOesKI39rZclaACA5iDlCaYi5PEUnDcaIOQBmEHOEUh9zuUOedMOIOQB2EHOEzmxKXczpHtXK39jCtQBAchBzhFI7C0JdxJQP6wUDsISYI5TKmFPlnZX3gaVrAYDkIOYIpTLmGgeF37V0KQCQLMQcodTFnG5YmbFYFR2ALcQcoVTF3CK7DXfwkBWAMcQcodTEnMkYbXwrQS0AkBzEHKGUY06rLrmmF2Y%2FADCHmCOU8vBgD13ZctOwxBwAc4g5QinGnEeberucpKkFAJKDmCOUYsw5VDm5WppSACBZiDlCKd6by3VvVDVpSgGAZCHmCNUrkmzMadZkGZoTy5IAyAFijlD368ltYOg8%2FlDXCMlqAYDkIOYIhSy7kMxX3a7sqL5WsloAIDmIOUJzs0%2F76te0Nm%2BmrPSTsBgASAZijtDQkl%2BNOV347HtVpKwFAJKDmCN06tTir31J3eHBHwelrAUAkoOYIxQREvC1L%2BXJMu%2B1lKUAQLIQc4Ta6792wlZr8b0s9yStBQCSg5gjtHXFra985fcl%2FSStBACSh5gjNCPLvC9%2B3qpd%2BYGY5AUgJ4g5QleOfzHmVOMvdX8pdS0AkBzEHKGvxJz74psSFwIAKUDMEfpKzJWfFCt1JQCQPMQcoS%2Fem7Mf2%2F1H7HADIDOIOUJfetLqoTZ0xog5ALlBzBF6M%2BrzcXONm%2BnKY%2FoDgNwg5gh9HnMG3YgdPe8wKQYAkoGYI%2FRZzOkran47zKYWAEgOYo7QZ3Na7YOvr8DW0wAyhJgj9PTAf2JOFz5pYxRSDkCOEHOEPlmIST1s63hWpQBAshBzhD5ZVtNn6c1QVqUAQLIQc4T%2Bu0h60YwN%2FsSq6ADyhJgjFNv93y1vtOELZ%2FTUMywGAJKBmCPU7uG%2FGxiqfM%2FP7YdJXgAyhZgj9FHMeRQuXvoVy1oAIDmIOUIfxdyY1g%2BRcgDyhZgj9G%2FM6Tu2KIiZrADyhZgj9OERhG5mu55ne7ItBgCSgZgjFLIsMeas2h1f9JxxLQCQHMQcoaSY09k8OfCMdS0AkBzEHKGkWRALft3VFVNZAWQNMUcoYeq%2BPtzuD7yDADKHH1JC7fUHBcHu13c1WBcCAClAzBFK2PLGs6ftO9aFAEAKEHOE4jcwdAu%2Bme0e60IAIAWIOUIzjkWH5S48DXs%2FAMgeYo5Q6FGTwet4LkzYB5A9xByhdtV3VesQzboKAEgZYo7QsbyPr9djXQQApAJijlDXdjsy44oVQAkQc4QWX9Ih5QAUATFHaPaQMNYlAECqIOYAgHOIOQDgHGIOADiHmAMAziHmAIBziDkA4BxiDgA4h5gDAM79v506kAEAAAAY5G99j68g0hwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAOc0Bc5oD5jQHzGkOmNMcMKc5YE5zwJzmgDnNAXOaA%2BY0B8xpDpjTHDCnOWBOc8Cc5oA5zQFzmgPmNAfMaQ6Y0xwwpzlgTnPAnOaAuQBnvTcx2S%2F5QAAAAABJRU5ErkJggg%3D%3D%22%20width%3D%221254%22%20height%3D%221254%22%2F%3E%3C%2Fsvg%3E%0A";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Leave space for the inherited control envelope below its 64 KiB limit.
|
|
2
|
+
export const MAX_DEV_NOTIFICATION_BYTES=60*1024;
|
|
3
|
+
const size=value=>Buffer.byteLength(JSON.stringify(value));
|
|
4
|
+
|
|
5
|
+
// Measure the JSON representation too: control characters and lone surrogates
|
|
6
|
+
// expand when escaped, independently of their UTF-8 source length.
|
|
7
|
+
function boundedString(value,limit){
|
|
8
|
+
if(size(value)<=limit)return value;
|
|
9
|
+
const suffix='…';
|
|
10
|
+
const prefix=end=>{
|
|
11
|
+
if(end>0&&end<value.length&&/[\uD800-\uDBFF]/.test(value[end-1])&&/[\uDC00-\uDFFF]/.test(value[end]))end--;
|
|
12
|
+
return value.slice(0,end);
|
|
13
|
+
};
|
|
14
|
+
let low=0,high=value.length;
|
|
15
|
+
while(low<high){const middle=Math.ceil((low+high)/2);if(size(prefix(middle)+suffix)<=limit)low=middle;else high=middle-1;}
|
|
16
|
+
return prefix(low)+suffix;
|
|
17
|
+
}
|
|
18
|
+
function boundedList(value,{count,itemBytes,totalBytes}){
|
|
19
|
+
const result=[];let bytes=2;
|
|
20
|
+
for(const entry of value.slice(0,count)){
|
|
21
|
+
if(typeof entry!=='string')continue;
|
|
22
|
+
const comma=result.length?1:0,remaining=totalBytes-bytes-comma;
|
|
23
|
+
if(remaining<5)break;
|
|
24
|
+
const text=boundedString(entry,Math.min(itemBytes,remaining));
|
|
25
|
+
result.push(text);bytes+=size(text)+comma;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
const serialized=value=>{try{return JSON.stringify(value);}catch{return undefined;}};
|
|
30
|
+
|
|
31
|
+
/** Keep the wire event and state file identical, bounded, and conservative. */
|
|
32
|
+
export function prepareDevNotification(value){
|
|
33
|
+
let notification={...value};const truncated=new Set();
|
|
34
|
+
for(const [key,limits]of Object.entries({changed:{count:32,itemBytes:512,totalBytes:16*1024},typeErrors:{count:32,itemBytes:4096,totalBytes:8*1024}})){
|
|
35
|
+
if(notification[key]===undefined)continue;
|
|
36
|
+
const original=notification[key],next=Array.isArray(original)?boundedList(original,limits):[];
|
|
37
|
+
if(serialized(original)!==JSON.stringify(next))truncated.add(key);
|
|
38
|
+
notification[key]=next;
|
|
39
|
+
}
|
|
40
|
+
if(notification.error!==undefined){
|
|
41
|
+
const next=typeof notification.error==='string'?boundedString(notification.error,8*1024):'Development compilation failed.';
|
|
42
|
+
if(next!==notification.error)truncated.add('error');notification.error=next;
|
|
43
|
+
}
|
|
44
|
+
if(truncated.has('changed')&&Object.hasOwn(notification,'affectedRoutes')){delete notification.affectedRoutes;truncated.add('affectedRoutes');}
|
|
45
|
+
const stamp=()=>{
|
|
46
|
+
if(truncated.size){notification.notificationTruncated=true;notification.truncatedFields=[...truncated];}
|
|
47
|
+
};
|
|
48
|
+
const trimSelection=()=>{
|
|
49
|
+
for(const key of ['routes','compiler','transforms','affectedRoutes'])if(Object.hasOwn(notification,key)){delete notification[key];truncated.add(key);}
|
|
50
|
+
};
|
|
51
|
+
const fits=text=>typeof text==='string'&&Buffer.byteLength(text)<=MAX_DEV_NOTIFICATION_BYTES;
|
|
52
|
+
stamp();let text=serialized(notification);
|
|
53
|
+
if(fits(text))return {notification,text};
|
|
54
|
+
|
|
55
|
+
// Removing affectedRoutes asks every active page to refresh. Never send a
|
|
56
|
+
// shortened selection, which could silently leave an affected page stale.
|
|
57
|
+
trimSelection();stamp();text=serialized(notification);
|
|
58
|
+
if(fits(text))return {notification,text};
|
|
59
|
+
|
|
60
|
+
// Unexpected metadata or large identities must not defeat the final bound.
|
|
61
|
+
// Preserve complete usable IDs/URLs; an omitted client manifest causes the
|
|
62
|
+
// browser's existing conservative reload path instead of fetching a bad URL.
|
|
63
|
+
const previous=notification;notification={};truncated.add('metadata');
|
|
64
|
+
for(const [key,limit]of Object.entries({state:128,buildId:512,session:512,clientManifest:8192,pagesManifest:8192})){
|
|
65
|
+
if(typeof previous[key]==='string'&&size(previous[key])<=limit)notification[key]=previous[key];
|
|
66
|
+
else if(previous[key]!==undefined)truncated.add(key);
|
|
67
|
+
}
|
|
68
|
+
for(const key of ['sequence','listenerPid'])if(Number.isSafeInteger(previous[key])&&previous[key]>=0)notification[key]=previous[key];
|
|
69
|
+
for(const key of ['safeMode','cssOnly'])if(typeof previous[key]==='boolean')notification[key]=previous[key];
|
|
70
|
+
for(const key of ['changed','typeErrors','error'])if(previous[key]!==undefined)notification[key]=previous[key];
|
|
71
|
+
if(previous.reason&&typeof previous.reason==='object'){
|
|
72
|
+
notification.reason={};
|
|
73
|
+
for(const [key,limit]of Object.entries({code:256,summary:2048}))if(typeof previous.reason[key]==='string')notification.reason[key]=boundedString(previous.reason[key],limit);
|
|
74
|
+
if(serialized(previous.reason)!==JSON.stringify(notification.reason))truncated.add('reason');
|
|
75
|
+
}
|
|
76
|
+
stamp();text=JSON.stringify(notification);
|
|
77
|
+
// The allowlisted fields have a combined byte budget below 60 KiB. Keep this
|
|
78
|
+
// assertion next to serialization so future additions cannot break the RPC.
|
|
79
|
+
if(!fits(text))throw Error('Development notification exceeds its byte budget');
|
|
80
|
+
return {notification,text};
|
|
81
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Created synchronously from a user click; never opens a background popup.
|
|
2
|
+
export function createToolWindow({ owner, title, width, height, onClose, reservedWindow }) {
|
|
3
|
+
let popup=reservedWindow;
|
|
4
|
+
try { if(popup===undefined)popup = owner.open('about:blank', '_blank', `popup=yes,width=${width},height=${height},resizable=yes,scrollbars=yes`); }
|
|
5
|
+
catch { return null; }
|
|
6
|
+
if (!popup) return null;
|
|
7
|
+
let finished = false, timer;
|
|
8
|
+
function cleanup() {
|
|
9
|
+
if (finished) return;
|
|
10
|
+
finished = true; owner.clearInterval(timer);
|
|
11
|
+
owner.removeEventListener('pagehide', close);
|
|
12
|
+
onClose?.();
|
|
13
|
+
}
|
|
14
|
+
function close() { try { if (!popup.closed) popup.close(); } finally { cleanup(); } }
|
|
15
|
+
try {
|
|
16
|
+
// The UI uses the retained window reference; it needs no reverse opener.
|
|
17
|
+
popup.opener = null;
|
|
18
|
+
const document = popup.document;
|
|
19
|
+
document.title = title; document.documentElement.lang = 'en';
|
|
20
|
+
const viewport=document.createElement('meta');viewport.name='viewport';viewport.content='width=device-width, initial-scale=1';document.head.append(viewport);
|
|
21
|
+
document.body.style.cssText = 'margin:0;background:#080808;color-scheme:dark';
|
|
22
|
+
const host = document.createElement('prnext-dev-window'), shadow = host.attachShadow({mode:'open'});
|
|
23
|
+
const style = document.createElement('style');
|
|
24
|
+
style.textContent = `:host{display:block;color-scheme:dark}*,*::before,*::after{box-sizing:border-box}[hidden]{display:none!important}svg{width:16px;height:16px;flex:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}`;
|
|
25
|
+
shadow.append(style); document.body.replaceChildren(host);
|
|
26
|
+
popup.addEventListener('pagehide', cleanup, {once:true});
|
|
27
|
+
owner.addEventListener('pagehide', close, {once:true});
|
|
28
|
+
timer = owner.setInterval(() => { if (popup.closed) cleanup(); }, 500);
|
|
29
|
+
return { window:popup, shadow, close, focus() { try { popup.focus(); } catch {} } };
|
|
30
|
+
} catch { close(); return null; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const popoutIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3h7v7m0-7L11 13"/><path d="M10 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5"/></svg>';
|
|
34
|
+
export const dockIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14H3V7m0 7L13 4"/><path d="M3 18v1a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-1"/></svg>';
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Pointer capture stays on the logo: dragging never installs page-wide styles.
|
|
2
|
+
// A tab-local preference cannot override a newly configured default corner.
|
|
3
|
+
const corners = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
4
|
+
const preference = 'prnext:dev-corner:v1';
|
|
5
|
+
|
|
6
|
+
export function createDevPosition({ host, trigger, position, onDrag, announce }) {
|
|
7
|
+
const view = host.ownerDocument.defaultView;
|
|
8
|
+
const controller = new AbortController(), events = { signal: controller.signal };
|
|
9
|
+
let current = corners.includes(position) ? position : 'bottom-left';
|
|
10
|
+
const configured = current;
|
|
11
|
+
try {
|
|
12
|
+
const saved = JSON.parse(view.sessionStorage.getItem(preference));
|
|
13
|
+
if (saved?.configured === configured && corners.includes(saved.corner)) current = saved.corner;
|
|
14
|
+
} catch { /* Storage can be disabled by the browser. */ }
|
|
15
|
+
host.dataset.position = current;
|
|
16
|
+
trigger.style.touchAction = 'none';
|
|
17
|
+
trigger.setAttribute('aria-description', 'Drag to a corner. Use Alt + arrow keys to move the logo.');
|
|
18
|
+
trigger.setAttribute('aria-keyshortcuts', 'Alt+ArrowUp Alt+ArrowDown Alt+ArrowLeft Alt+ArrowRight');
|
|
19
|
+
const hints = view.document.createElement('div'); hints.className = 'corner-hints'; hints.hidden = true; hints.setAttribute('aria-hidden', 'true');
|
|
20
|
+
for (const corner of corners) {
|
|
21
|
+
const node = view.document.createElement('span'); node.className = 'corner-hint'; node.dataset.corner = corner; hints.append(node);
|
|
22
|
+
}
|
|
23
|
+
host.shadowRoot.append(hints);
|
|
24
|
+
let drag, motion, suppressClick = false;
|
|
25
|
+
function viewport() {
|
|
26
|
+
const port = view.visualViewport;
|
|
27
|
+
return { x: port?.offsetLeft || 0, y: port?.offsetTop || 0, width: port?.width || view.innerWidth, height: port?.height || view.innerHeight };
|
|
28
|
+
}
|
|
29
|
+
function nearest(x, y) {
|
|
30
|
+
const port = viewport();
|
|
31
|
+
return `${y < port.y + port.height / 2 ? 'top' : 'bottom'}-${x < port.x + port.width / 2 ? 'left' : 'right'}`;
|
|
32
|
+
}
|
|
33
|
+
function highlight(corner) {
|
|
34
|
+
for (const node of hints.children) node.dataset.active = String(node.dataset.corner === corner);
|
|
35
|
+
}
|
|
36
|
+
function clearCoordinates() {
|
|
37
|
+
for (const name of ['top', 'right', 'bottom', 'left']) host.style.removeProperty(name);
|
|
38
|
+
}
|
|
39
|
+
function settle(corner, from, save = true) {
|
|
40
|
+
motion?.cancel(); motion = null;
|
|
41
|
+
current = corner; host.dataset.position = corner; clearCoordinates();
|
|
42
|
+
const target = host.getBoundingClientRect();
|
|
43
|
+
if (from && !view.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
44
|
+
motion = host.animate([
|
|
45
|
+
{ transform: `translate(${from.x - target.x}px,${from.y - target.y}px)` },
|
|
46
|
+
{ transform: 'translate(0,0)' },
|
|
47
|
+
], { duration: 420, easing: 'cubic-bezier(.16,1,.3,1)' });
|
|
48
|
+
}
|
|
49
|
+
if (save) {
|
|
50
|
+
try { view.sessionStorage.setItem(preference, JSON.stringify({ configured, corner })); } catch {}
|
|
51
|
+
announce(`Logo moved to ${corner.replace('-', ' ')}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function finish(cancel = false) {
|
|
55
|
+
if (!drag) return;
|
|
56
|
+
const state = drag; drag = null;
|
|
57
|
+
if (state.moved) {
|
|
58
|
+
suppressClick = true;
|
|
59
|
+
const from = host.getBoundingClientRect();
|
|
60
|
+
hints.hidden = true; delete host.dataset.dragging;
|
|
61
|
+
settle(cancel ? state.corner : nearest(from.x + 22, from.y + 22), from, !cancel);
|
|
62
|
+
}
|
|
63
|
+
if (trigger.hasPointerCapture(state.pointer)) trigger.releasePointerCapture(state.pointer);
|
|
64
|
+
}
|
|
65
|
+
trigger.addEventListener('pointerdown', event => {
|
|
66
|
+
if (drag || event.button !== 0 || !event.isPrimary) return;
|
|
67
|
+
suppressClick = false;
|
|
68
|
+
const rect = host.getBoundingClientRect();
|
|
69
|
+
drag = { pointer: event.pointerId, x: event.clientX, y: event.clientY, rect, corner: current, moved: false };
|
|
70
|
+
trigger.setPointerCapture(event.pointerId);
|
|
71
|
+
}, events);
|
|
72
|
+
trigger.addEventListener('pointermove', event => {
|
|
73
|
+
if (!drag || event.pointerId !== drag.pointer) return;
|
|
74
|
+
const dx = event.clientX - drag.x, dy = event.clientY - drag.y;
|
|
75
|
+
if (!drag.moved && Math.hypot(dx, dy) < 6) return;
|
|
76
|
+
if (!drag.moved) {
|
|
77
|
+
drag.moved = true; motion?.cancel(); motion = null; onDrag();
|
|
78
|
+
host.dataset.dragging = 'true'; hints.hidden = false;
|
|
79
|
+
}
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
const port = viewport();
|
|
82
|
+
const x = Math.max(port.x + 4, Math.min(port.x + port.width - 48, drag.rect.x + dx));
|
|
83
|
+
const y = Math.max(port.y + 4, Math.min(port.y + port.height - 48, drag.rect.y + dy));
|
|
84
|
+
host.style.left = x + 'px'; host.style.top = y + 'px'; host.style.right = 'auto'; host.style.bottom = 'auto';
|
|
85
|
+
highlight(nearest(x + 22, y + 22));
|
|
86
|
+
}, events);
|
|
87
|
+
trigger.addEventListener('pointerup', event => { if(event.pointerId === drag?.pointer) finish(); }, events);
|
|
88
|
+
trigger.addEventListener('pointercancel', event => { if(event.pointerId === drag?.pointer) finish(true); }, events);
|
|
89
|
+
trigger.addEventListener('lostpointercapture', event => { if(event.pointerId === drag?.pointer) finish(true); }, events);
|
|
90
|
+
trigger.addEventListener('dragstart', event => event.preventDefault(), events);
|
|
91
|
+
trigger.addEventListener('click', event => {
|
|
92
|
+
if (suppressClick && event.detail !== 0) { suppressClick = false; event.preventDefault(); event.stopImmediatePropagation(); }
|
|
93
|
+
}, { ...events, capture: true });
|
|
94
|
+
trigger.addEventListener('keydown', event => {
|
|
95
|
+
if (event.key === 'Escape' && drag?.moved) { event.preventDefault(); event.stopPropagation(); finish(true); }
|
|
96
|
+
if (!event.altKey || !['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
|
97
|
+
event.preventDefault(); event.stopPropagation();
|
|
98
|
+
const rect = host.getBoundingClientRect(); onDrag();
|
|
99
|
+
const [vertical, horizontal] = current.split('-');
|
|
100
|
+
settle(`${event.key === 'ArrowUp' ? 'top' : event.key === 'ArrowDown' ? 'bottom' : vertical}-${event.key === 'ArrowLeft' ? 'left' : event.key === 'ArrowRight' ? 'right' : horizontal}`, rect);
|
|
101
|
+
}, events);
|
|
102
|
+
function resize() { if (drag) finish(true); motion?.cancel(); motion = null; }
|
|
103
|
+
view.addEventListener('resize', resize, events);
|
|
104
|
+
view.addEventListener('blur', () => finish(true), events);
|
|
105
|
+
view.visualViewport?.addEventListener('resize', resize, events);
|
|
106
|
+
return { dispose() { controller.abort(); motion?.cancel(); hints.remove(); } };
|
|
107
|
+
}
|