@makershq/react 0.0.0-stage → 0.3.1
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/LICENSE +21 -0
- package/README.md +56 -2
- package/THIRD_PARTY_NOTICES.md +77 -0
- package/dist/api.d.ts +82 -0
- package/dist/api.js +183 -0
- package/dist/cache.d.ts +22 -0
- package/dist/cache.js +86 -0
- package/dist/components/icons/external-link/index.d.ts +52 -0
- package/dist/components/icons/external-link/index.js +60 -0
- package/dist/components/icons/index.d.ts +31 -0
- package/dist/components/icons/index.js +56 -0
- package/dist/components/icons/reduced-motion.d.ts +4 -0
- package/dist/components/icons/reduced-motion.js +24 -0
- package/dist/components/icons/x/index.d.ts +98 -0
- package/dist/components/icons/x/index.js +87 -0
- package/dist/components/release-markdown/index.d.ts +22 -0
- package/dist/components/release-markdown/index.js +101 -0
- package/dist/components/release-notes/index.d.ts +51 -0
- package/dist/components/release-notes/index.js +89 -0
- package/dist/connectors/use-release-note-list-connector.d.ts +24 -0
- package/dist/connectors/use-release-note-list-connector.js +219 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -0
- package/dist/styles.css +2 -0
- package/dist/styles.d.ts +8 -0
- package/dist/tauri/components/native-child/index.d.ts +19 -0
- package/dist/tauri/components/native-child/index.js +146 -0
- package/dist/tauri/components/native-child/theme.d.ts +12 -0
- package/dist/tauri/components/native-child/theme.js +45 -0
- package/dist/tauri/components/release-notes-host/index.d.ts +36 -0
- package/dist/tauri/components/release-notes-host/index.js +81 -0
- package/dist/tauri/components/release-notes-settings/index.d.ts +10 -0
- package/dist/tauri/components/release-notes-settings/index.js +23 -0
- package/dist/tauri/components/update-window/index.d.ts +20 -0
- package/dist/tauri/components/update-window/index.js +68 -0
- package/dist/tauri/context.d.ts +20 -0
- package/dist/tauri/context.js +22 -0
- package/dist/tauri/contracts.d.ts +84 -0
- package/dist/tauri/contracts.js +7 -0
- package/dist/tauri/hooks/use-release-notes.d.ts +92 -0
- package/dist/tauri/hooks/use-release-notes.js +101 -0
- package/dist/tauri/host-controller.d.ts +134 -0
- package/dist/tauri/host-controller.js +951 -0
- package/dist/tauri/index.d.ts +10 -0
- package/dist/tauri/index.js +9 -0
- package/dist/tauri/peer-protocol.d.ts +36 -0
- package/dist/tauri/peer-protocol.js +43 -0
- package/dist/tauri/preferences.d.ts +18 -0
- package/dist/tauri/preferences.js +39 -0
- package/dist/tauri/protocol.d.ts +71 -0
- package/dist/tauri/protocol.js +106 -0
- package/dist/tauri/remote-settings-client.d.ts +72 -0
- package/dist/tauri/remote-settings-client.js +265 -0
- package/dist/website/components/update-toast/index.d.ts +14 -0
- package/dist/website/components/update-toast/index.js +22 -0
- package/dist/website/components/website-updates/index.d.ts +24 -0
- package/dist/website/components/website-updates/index.js +35 -0
- package/dist/website/components/whats-new-panel/index.d.ts +23 -0
- package/dist/website/components/whats-new-panel/index.js +38 -0
- package/dist/website/preferences.d.ts +32 -0
- package/dist/website/preferences.js +85 -0
- package/dist/website/use-website-updates-connector.d.ts +34 -0
- package/dist/website/use-website-updates-connector.js +318 -0
- package/dist/website/version.d.ts +27 -0
- package/dist/website/version.js +73 -0
- package/package.json +71 -3
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/** Connects an existing local settings window to the one native main host.
|
|
2
|
+
*
|
|
3
|
+
* Receives presentation snapshots and forwards only bounded settings actions.
|
|
4
|
+
*
|
|
5
|
+
* Keep updater resources, installation and background notifications in the main window.
|
|
6
|
+
*/
|
|
7
|
+
import { emitTo } from '@tauri-apps/api/event';
|
|
8
|
+
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
|
9
|
+
import { readPreferences } from './preferences.js';
|
|
10
|
+
import { STATE_EVENT, hasPresentationState, } from './protocol.js';
|
|
11
|
+
import { PEER_ACTION_EVENT, PEER_CONNECT_EVENT, PEER_RESULT_EVENT, } from './peer-protocol.js';
|
|
12
|
+
/** Internal read/presentation client for one existing settings window. */
|
|
13
|
+
export class RemoteSettingsClient {
|
|
14
|
+
projectId;
|
|
15
|
+
hostLabel;
|
|
16
|
+
state;
|
|
17
|
+
listeners = new Set();
|
|
18
|
+
token = null;
|
|
19
|
+
snapshotRevision = -1;
|
|
20
|
+
nonce = crypto.randomUUID();
|
|
21
|
+
disposers = [];
|
|
22
|
+
pending = new Map();
|
|
23
|
+
connectionWaiters = new Set();
|
|
24
|
+
connectionFailure = null;
|
|
25
|
+
disposed = false;
|
|
26
|
+
label = getCurrentWebviewWindow().label;
|
|
27
|
+
/** Connects to the main host without invoking consumer updater callbacks.
|
|
28
|
+
* @param projectId - Expected release-note project.
|
|
29
|
+
* @param hostLabel - Existing callback-owning main window.
|
|
30
|
+
*/
|
|
31
|
+
constructor(projectId, hostLabel) {
|
|
32
|
+
this.projectId = projectId;
|
|
33
|
+
this.hostLabel = hostLabel;
|
|
34
|
+
this.state = {
|
|
35
|
+
ready: false,
|
|
36
|
+
presentation: 'native',
|
|
37
|
+
readerRequest: null,
|
|
38
|
+
updateRequest: null,
|
|
39
|
+
projectId,
|
|
40
|
+
name: '',
|
|
41
|
+
currentVersion: '',
|
|
42
|
+
icon: '',
|
|
43
|
+
theme: 'system',
|
|
44
|
+
preferences: readPreferences(null),
|
|
45
|
+
status: 'idle',
|
|
46
|
+
candidate: null,
|
|
47
|
+
progress: null,
|
|
48
|
+
canCancel: false,
|
|
49
|
+
error: null,
|
|
50
|
+
readerVersion: null,
|
|
51
|
+
};
|
|
52
|
+
void this.initialize();
|
|
53
|
+
}
|
|
54
|
+
/** Subscribes to main-host state.
|
|
55
|
+
* @param listener - React external-store listener.
|
|
56
|
+
* @returns Unsubscribe callback.
|
|
57
|
+
*/
|
|
58
|
+
subscribe = (listener) => {
|
|
59
|
+
this.listeners.add(listener);
|
|
60
|
+
return () => this.listeners.delete(listener);
|
|
61
|
+
};
|
|
62
|
+
/** Reads the stable presentation snapshot.
|
|
63
|
+
* @returns Latest authenticated main-host state.
|
|
64
|
+
*/
|
|
65
|
+
getSnapshot = () => this.state;
|
|
66
|
+
patch(patch) {
|
|
67
|
+
if (this.disposed)
|
|
68
|
+
return;
|
|
69
|
+
this.state = { ...this.state, ...patch };
|
|
70
|
+
for (const listener of this.listeners)
|
|
71
|
+
listener();
|
|
72
|
+
}
|
|
73
|
+
async initialize() {
|
|
74
|
+
try {
|
|
75
|
+
const current = getCurrentWebviewWindow();
|
|
76
|
+
const stateCleanup = await current.listen(STATE_EVENT, ({ payload }) => {
|
|
77
|
+
if (!payload || typeof payload !== 'object')
|
|
78
|
+
return;
|
|
79
|
+
const message = payload;
|
|
80
|
+
if (message.sender !== this.hostLabel ||
|
|
81
|
+
message.connectionNonce !== this.nonce ||
|
|
82
|
+
typeof message.token !== 'string' ||
|
|
83
|
+
!/^[a-f0-9-]{36}$/.test(message.token) ||
|
|
84
|
+
(this.token !== null && this.token !== message.token) ||
|
|
85
|
+
typeof message.revision !== 'number' ||
|
|
86
|
+
!Number.isSafeInteger(message.revision) ||
|
|
87
|
+
message.revision < 0 ||
|
|
88
|
+
!message.state ||
|
|
89
|
+
typeof message.state !== 'object')
|
|
90
|
+
return;
|
|
91
|
+
const state = message.state;
|
|
92
|
+
if (!hasPresentationState(message.state) ||
|
|
93
|
+
state.projectId !== this.projectId ||
|
|
94
|
+
typeof state.currentVersion !== 'string' ||
|
|
95
|
+
!state.preferences ||
|
|
96
|
+
typeof state.preferences.showAfterUpdate !== 'boolean')
|
|
97
|
+
return;
|
|
98
|
+
this.token = message.token;
|
|
99
|
+
if (message.revision <= this.snapshotRevision)
|
|
100
|
+
return;
|
|
101
|
+
this.snapshotRevision = message.revision;
|
|
102
|
+
this.patch(state);
|
|
103
|
+
this.settleConnection();
|
|
104
|
+
});
|
|
105
|
+
if (this.disposed) {
|
|
106
|
+
stateCleanup();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.disposers.push(stateCleanup);
|
|
110
|
+
const resultCleanup = await current.listen(PEER_RESULT_EVENT, ({ payload }) => {
|
|
111
|
+
if (!payload || typeof payload !== 'object')
|
|
112
|
+
return;
|
|
113
|
+
const message = payload;
|
|
114
|
+
if (message.sender !== this.hostLabel ||
|
|
115
|
+
message.token !== this.token ||
|
|
116
|
+
typeof message.request !== 'string' ||
|
|
117
|
+
typeof message.ok !== 'boolean')
|
|
118
|
+
return;
|
|
119
|
+
const pending = this.pending.get(message.request);
|
|
120
|
+
if (!pending)
|
|
121
|
+
return;
|
|
122
|
+
clearTimeout(pending.timer);
|
|
123
|
+
this.pending.delete(message.request);
|
|
124
|
+
if (message.ok)
|
|
125
|
+
pending.resolve();
|
|
126
|
+
else
|
|
127
|
+
pending.reject(new Error('The app could not complete this settings action. Please try again.'));
|
|
128
|
+
});
|
|
129
|
+
if (this.disposed) {
|
|
130
|
+
resultCleanup();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.disposers.push(resultCleanup);
|
|
134
|
+
const connect = () => {
|
|
135
|
+
if (!this.token && !this.disposed)
|
|
136
|
+
void emitTo({ kind: 'WebviewWindow', label: this.hostLabel }, PEER_CONNECT_EVENT, { sender: this.label, nonce: this.nonce }).catch(() => this.patch({
|
|
137
|
+
error: 'Could not connect to the app. Check local native event capabilities.',
|
|
138
|
+
}));
|
|
139
|
+
};
|
|
140
|
+
connect();
|
|
141
|
+
const interval = setInterval(connect, 500);
|
|
142
|
+
const timeout = setTimeout(() => {
|
|
143
|
+
clearInterval(interval);
|
|
144
|
+
if (!this.token) {
|
|
145
|
+
const error = new Error('Could not connect to the app. Keep ReleaseNotesHost mounted in its main window and check local native event capabilities.');
|
|
146
|
+
this.patch({ error: error.message });
|
|
147
|
+
this.settleConnection(error);
|
|
148
|
+
}
|
|
149
|
+
}, 15000);
|
|
150
|
+
this.disposers.push(() => {
|
|
151
|
+
clearInterval(interval);
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
const error = new Error('Could not connect to the app. Check local native event capabilities.');
|
|
157
|
+
this.patch({ error: error.message });
|
|
158
|
+
this.settleConnection(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
settleConnection(error) {
|
|
162
|
+
this.connectionFailure = error ?? null;
|
|
163
|
+
for (const waiter of this.connectionWaiters) {
|
|
164
|
+
clearTimeout(waiter.timer);
|
|
165
|
+
if (error)
|
|
166
|
+
waiter.reject(error);
|
|
167
|
+
else
|
|
168
|
+
waiter.resolve();
|
|
169
|
+
}
|
|
170
|
+
this.connectionWaiters.clear();
|
|
171
|
+
}
|
|
172
|
+
waitForConnection() {
|
|
173
|
+
if (this.disposed)
|
|
174
|
+
return Promise.reject(new Error('The settings window closed.'));
|
|
175
|
+
if (this.token)
|
|
176
|
+
return Promise.resolve();
|
|
177
|
+
if (this.connectionFailure)
|
|
178
|
+
return Promise.reject(this.connectionFailure);
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const waiter = {
|
|
181
|
+
resolve,
|
|
182
|
+
reject,
|
|
183
|
+
timer: setTimeout(() => {
|
|
184
|
+
this.connectionWaiters.delete(waiter);
|
|
185
|
+
reject(new Error('The settings window could not connect to the main app.'));
|
|
186
|
+
}, 15000),
|
|
187
|
+
};
|
|
188
|
+
this.connectionWaiters.add(waiter);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async send(action) {
|
|
192
|
+
try {
|
|
193
|
+
await this.waitForConnection();
|
|
194
|
+
if (this.disposed)
|
|
195
|
+
throw new Error('The settings window closed.');
|
|
196
|
+
const request = crypto.randomUUID();
|
|
197
|
+
await new Promise((resolve, reject) => {
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
this.pending.delete(request);
|
|
200
|
+
reject(new Error('The main app did not complete this settings action.'));
|
|
201
|
+
}, 30000);
|
|
202
|
+
this.pending.set(request, { resolve, reject, timer });
|
|
203
|
+
void emitTo({ kind: 'WebviewWindow', label: this.hostLabel }, PEER_ACTION_EVENT, { sender: this.label, token: this.token, request, action }).catch(() => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
this.pending.delete(request);
|
|
206
|
+
reject(new Error('The settings action could not reach the main app.'));
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const failure = error instanceof Error
|
|
212
|
+
? error
|
|
213
|
+
: new Error('The settings action failed.');
|
|
214
|
+
this.patch({ error: failure.message });
|
|
215
|
+
throw failure;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Opens a reader owned by main and returning focus to this settings window.
|
|
219
|
+
* @returns Completion after the native reader is ready.
|
|
220
|
+
*/
|
|
221
|
+
open = () => this.send({ type: 'open' });
|
|
222
|
+
/** Runs the main host's manual check, bypassing its skipped candidate.
|
|
223
|
+
* @returns Completion after the host finishes presenting its result.
|
|
224
|
+
*/
|
|
225
|
+
check = () => this.send({ type: 'check' });
|
|
226
|
+
/** Saves the main host's shared preference.
|
|
227
|
+
* @param value - New after-update preference.
|
|
228
|
+
* @returns Completion after host persistence and synchronization.
|
|
229
|
+
*/
|
|
230
|
+
setShowAfterUpdate = (value) => this.send({ type: 'preference', value });
|
|
231
|
+
/** Sends one bounded command for a captured headless presentation.
|
|
232
|
+
* @param kind - Reader or update role.
|
|
233
|
+
* @param generation - Private identity from the authenticated snapshot.
|
|
234
|
+
* @param action - Bounded presentation command.
|
|
235
|
+
* @param action.type - Fixed command discriminator.
|
|
236
|
+
* @param action.version - Exact displayed candidate or rendered release version.
|
|
237
|
+
* @param action.cached - Whether rendered content came from offline storage.
|
|
238
|
+
* @returns Command acceptance; long operations report through synchronized state.
|
|
239
|
+
*/
|
|
240
|
+
presentationAction = (kind, generation, action) => this.send({ type: 'presentation', kind, generation, action });
|
|
241
|
+
/** Rejects secondary-window background notifications.
|
|
242
|
+
* @returns A rejected promise; consumer background checking belongs in the main host.
|
|
243
|
+
*/
|
|
244
|
+
notifyUpdateAvailable = () => Promise.reject(new Error('Send background update notifications only from the configured main host window.'));
|
|
245
|
+
/** Releases secondary-window listeners without touching main's updater resources. */
|
|
246
|
+
dispose() {
|
|
247
|
+
if (this.token)
|
|
248
|
+
void emitTo({ kind: 'WebviewWindow', label: this.hostLabel }, PEER_ACTION_EVENT, {
|
|
249
|
+
sender: this.label,
|
|
250
|
+
token: this.token,
|
|
251
|
+
request: crypto.randomUUID(),
|
|
252
|
+
action: { type: 'disconnect' },
|
|
253
|
+
}).catch(() => undefined);
|
|
254
|
+
this.disposed = true;
|
|
255
|
+
this.settleConnection(new Error('The settings window closed.'));
|
|
256
|
+
for (const dispose of this.disposers)
|
|
257
|
+
dispose();
|
|
258
|
+
for (const pending of this.pending.values()) {
|
|
259
|
+
clearTimeout(pending.timer);
|
|
260
|
+
pending.reject(new Error('The settings window closed.'));
|
|
261
|
+
}
|
|
262
|
+
this.pending.clear();
|
|
263
|
+
this.listeners.clear();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Presents one pending website deployment.
|
|
2
|
+
* @param props - Refresh state and current-candidate callbacks.
|
|
3
|
+
* @param props.refreshing - Whether the consumer save barrier is pending.
|
|
4
|
+
* @param props.error - Safe retryable refresh failure.
|
|
5
|
+
* @param props.onRefresh - Requests a single consumer save and document reload.
|
|
6
|
+
* @param props.onDismiss - Defers only the displayed build in this tab.
|
|
7
|
+
* @returns The compact deployment notification.
|
|
8
|
+
*/
|
|
9
|
+
export declare function UpdateToast({ refreshing, error, onRefresh, onDismiss, }: {
|
|
10
|
+
refreshing: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
onRefresh: () => void;
|
|
13
|
+
onDismiss: () => void;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Presents the latest deployment candidate without interrupting the running website.
|
|
4
|
+
*
|
|
5
|
+
* Owns native Refresh and Not now controls with safe save-barrier feedback.
|
|
6
|
+
*
|
|
7
|
+
* Keep detection and reload ownership in the private website connector.
|
|
8
|
+
*/
|
|
9
|
+
import { useId } from 'react';
|
|
10
|
+
import { IconAction, RefreshIcon } from '../../../components/icons/index.js';
|
|
11
|
+
/** Presents one pending website deployment.
|
|
12
|
+
* @param props - Refresh state and current-candidate callbacks.
|
|
13
|
+
* @param props.refreshing - Whether the consumer save barrier is pending.
|
|
14
|
+
* @param props.error - Safe retryable refresh failure.
|
|
15
|
+
* @param props.onRefresh - Requests a single consumer save and document reload.
|
|
16
|
+
* @param props.onDismiss - Defers only the displayed build in this tab.
|
|
17
|
+
* @returns The compact deployment notification.
|
|
18
|
+
*/
|
|
19
|
+
export function UpdateToast({ refreshing, error, onRefresh, onDismiss, }) {
|
|
20
|
+
const title = useId();
|
|
21
|
+
return (_jsxs("section", { className: "flwu-toast", role: "status", "aria-labelledby": title, children: [_jsxs("div", { className: "flwu-toast-topline", children: [_jsx(RefreshIcon, { size: 16, "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("h2", { id: title, children: "Update available" }), _jsx("p", { children: "Refresh to get the latest version." })] })] }), error && (_jsx("p", { className: "flwu-error", role: "alert", children: error })), _jsxs("div", { className: "flwu-actions", children: [_jsx("button", { type: "button", disabled: refreshing, onClick: onDismiss, children: "Not now" }), _jsx(IconAction, { disabled: refreshing, children: _jsx("button", { type: "button", className: "flwu-primary", disabled: refreshing, onClick: onRefresh, children: refreshing ? 'Refreshing…' : 'Refresh' }) })] })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds deployment refresh prompts and exact after-refresh release previews to a website.
|
|
3
|
+
*
|
|
4
|
+
* Owns the default nonmodal presentation over the shared browser updates connector.
|
|
5
|
+
*
|
|
6
|
+
* Compile currentBuild into the JavaScript bundle and publish its manifest after deploying assets.
|
|
7
|
+
*/
|
|
8
|
+
import { type WebsiteUpdatesOptions } from '../../use-website-updates-connector.js';
|
|
9
|
+
/** Configuration for one website updates mount. */
|
|
10
|
+
export interface WebsiteUpdatesProps extends WebsiteUpdatesOptions {
|
|
11
|
+
theme?: 'system' | 'light' | 'dark';
|
|
12
|
+
}
|
|
13
|
+
/** Displays the latest deployment prompt or the loaded version's fresh release preview.
|
|
14
|
+
* @param props - Public project, compiled build, appearance and optional save barrier.
|
|
15
|
+
* @param props.projectId - Public project which owns release notes.
|
|
16
|
+
* @param props.currentBuild - Build compiled into the loaded JavaScript.
|
|
17
|
+
* @param props.currentBuild.buildId - Opaque loaded deployment identity.
|
|
18
|
+
* @param props.currentBuild.releaseVersion - Optional loaded semantic version.
|
|
19
|
+
* @param props.manifestUrl - Deployment manifest location, default /version.json.
|
|
20
|
+
* @param props.theme - Notification color scheme, default system.
|
|
21
|
+
* @param props.beforeRefresh - Consumer synchronous or asynchronous save barrier.
|
|
22
|
+
* @returns One nonmodal notification, or null when there is nothing to show.
|
|
23
|
+
*/
|
|
24
|
+
export declare function WebsiteUpdates({ projectId, currentBuild, manifestUrl, theme, beforeRefresh, }: WebsiteUpdatesProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Adds deployment refresh prompts and exact after-refresh release previews to a website.
|
|
4
|
+
*
|
|
5
|
+
* Owns the default nonmodal presentation over the shared browser updates connector.
|
|
6
|
+
*
|
|
7
|
+
* Compile currentBuild into the JavaScript bundle and publish its manifest after deploying assets.
|
|
8
|
+
*/
|
|
9
|
+
import { useWebsiteUpdatesConnector, } from '../../use-website-updates-connector.js';
|
|
10
|
+
import { UpdateToast } from '../update-toast/index.js';
|
|
11
|
+
import { WhatsNewPanel } from '../whats-new-panel/index.js';
|
|
12
|
+
/** Displays the latest deployment prompt or the loaded version's fresh release preview.
|
|
13
|
+
* @param props - Public project, compiled build, appearance and optional save barrier.
|
|
14
|
+
* @param props.projectId - Public project which owns release notes.
|
|
15
|
+
* @param props.currentBuild - Build compiled into the loaded JavaScript.
|
|
16
|
+
* @param props.currentBuild.buildId - Opaque loaded deployment identity.
|
|
17
|
+
* @param props.currentBuild.releaseVersion - Optional loaded semantic version.
|
|
18
|
+
* @param props.manifestUrl - Deployment manifest location, default /version.json.
|
|
19
|
+
* @param props.theme - Notification color scheme, default system.
|
|
20
|
+
* @param props.beforeRefresh - Consumer synchronous or asynchronous save barrier.
|
|
21
|
+
* @returns One nonmodal notification, or null when there is nothing to show.
|
|
22
|
+
*/
|
|
23
|
+
export function WebsiteUpdates({ projectId, currentBuild, manifestUrl = '/version.json', theme = 'system', beforeRefresh, }) {
|
|
24
|
+
const updates = useWebsiteUpdatesConnector({
|
|
25
|
+
projectId,
|
|
26
|
+
currentBuild,
|
|
27
|
+
manifestUrl,
|
|
28
|
+
beforeRefresh,
|
|
29
|
+
});
|
|
30
|
+
if (!updates.displayedRelease && !updates.candidate)
|
|
31
|
+
return null;
|
|
32
|
+
return (_jsx("div", { className: "fl-website-updates", "data-theme": theme, children: updates.displayedRelease ? (_jsx(WhatsNewPanel, { projectId: projectId, release: updates.displayedRelease, onRendered: updates.onReleaseRendered, onDismiss: updates.dismissRelease }, updates.displayedRelease.id)) : updates.candidate ? (_jsx(UpdateToast, { refreshing: updates.refreshing, error: updates.error, onRefresh: () => {
|
|
33
|
+
void updates.refresh();
|
|
34
|
+
}, onDismiss: updates.dismissCandidate })) : null }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Release } from '../../../api.js';
|
|
2
|
+
/** Presents a single latched release preview.
|
|
3
|
+
* @param props - Public release data and private render/dismiss callbacks.
|
|
4
|
+
* @param props.projectId - Project which owns the preview image.
|
|
5
|
+
* @param props.release - Exact installed release to preview.
|
|
6
|
+
* @param props.release.id - Stable public release identifier.
|
|
7
|
+
* @param props.release.version - Optional exact application version.
|
|
8
|
+
* @param props.release.title - Human-readable title.
|
|
9
|
+
* @param props.release.excerpt - Optional plain-text preview.
|
|
10
|
+
* @param props.release.previewImageSource - Optional preview image source.
|
|
11
|
+
* @param props.release.markdown - Complete release document.
|
|
12
|
+
* @param props.release.date - Publication calendar date.
|
|
13
|
+
* @param props.release.url - Public article URL.
|
|
14
|
+
* @param props.onRendered - Acknowledges this matching fresh release after commit.
|
|
15
|
+
* @param props.onDismiss - Removes the panel without changing the loaded website.
|
|
16
|
+
* @returns A nonmodal release region with a real external article anchor.
|
|
17
|
+
*/
|
|
18
|
+
export declare function WhatsNewPanel({ projectId, release, onRendered, onDismiss, }: {
|
|
19
|
+
projectId: string;
|
|
20
|
+
release: Release;
|
|
21
|
+
onRendered: () => void;
|
|
22
|
+
onDismiss: () => void;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Shows a compact published preview for the release currently loaded in the website.
|
|
4
|
+
*
|
|
5
|
+
* Owns original image proportions, text-only fallback and explicit article dismissal.
|
|
6
|
+
*
|
|
7
|
+
* Acknowledge only after this release renders; keep its lifetime independent of storage updates.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useId, useState } from 'react';
|
|
10
|
+
import { releaseImageUrl } from '../../../api.js';
|
|
11
|
+
import { ExternalLink, IconAction, NewspaperIcon, X, } from '../../../components/icons/index.js';
|
|
12
|
+
/** Presents a single latched release preview.
|
|
13
|
+
* @param props - Public release data and private render/dismiss callbacks.
|
|
14
|
+
* @param props.projectId - Project which owns the preview image.
|
|
15
|
+
* @param props.release - Exact installed release to preview.
|
|
16
|
+
* @param props.release.id - Stable public release identifier.
|
|
17
|
+
* @param props.release.version - Optional exact application version.
|
|
18
|
+
* @param props.release.title - Human-readable title.
|
|
19
|
+
* @param props.release.excerpt - Optional plain-text preview.
|
|
20
|
+
* @param props.release.previewImageSource - Optional preview image source.
|
|
21
|
+
* @param props.release.markdown - Complete release document.
|
|
22
|
+
* @param props.release.date - Publication calendar date.
|
|
23
|
+
* @param props.release.url - Public article URL.
|
|
24
|
+
* @param props.onRendered - Acknowledges this matching fresh release after commit.
|
|
25
|
+
* @param props.onDismiss - Removes the panel without changing the loaded website.
|
|
26
|
+
* @returns A nonmodal release region with a real external article anchor.
|
|
27
|
+
*/
|
|
28
|
+
export function WhatsNewPanel({ projectId, release, onRendered, onDismiss, }) {
|
|
29
|
+
const title = useId();
|
|
30
|
+
const [failedImage, setFailedImage] = useState(null);
|
|
31
|
+
const source = release.previewImageSource
|
|
32
|
+
? releaseImageUrl(projectId, release.id, release.previewImageSource)
|
|
33
|
+
: undefined;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
onRendered();
|
|
36
|
+
}, [release, onRendered]);
|
|
37
|
+
return (_jsxs("section", { className: "flwu-panel", role: "region", "aria-labelledby": title, children: [_jsxs("header", { children: [_jsxs("h2", { id: title, children: [_jsx(NewspaperIcon, { size: 16, animateOnView: true, animateOnViewOnce: true, "aria-hidden": "true" }), "What\u2019s new"] }), _jsx(IconAction, { children: _jsx("button", { type: "button", className: "flwu-close", "aria-label": "Dismiss what's new", onClick: onDismiss, children: _jsx(X, { size: 16, "aria-hidden": "true" }) }) })] }), _jsxs("div", { className: "flwu-content", children: [source && failedImage !== source && (_jsx("img", { src: source, alt: "", onError: () => setFailedImage(source) })), _jsx("h3", { children: release.title }), release.excerpt && _jsx("p", { children: release.excerpt })] }), _jsx("footer", { children: _jsx(IconAction, { children: _jsxs("a", { href: release.url, target: "_blank", rel: "noopener noreferrer", onClick: onDismiss, children: ["Read release notes", _jsx(ExternalLink, { size: 16, "aria-hidden": "true" })] }) }) })] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One durable release acknowledgement. */
|
|
2
|
+
export interface WebsitePreferences {
|
|
3
|
+
acknowledgedVersion: string | null;
|
|
4
|
+
}
|
|
5
|
+
/** Creates one storage scope for a public project and resolved deployment manifest.
|
|
6
|
+
* @param projectId - Public project identifier.
|
|
7
|
+
* @param manifestUrl - Fully resolved manifest URL.
|
|
8
|
+
* @returns The isolated preferences key.
|
|
9
|
+
*/
|
|
10
|
+
export declare function websitePreferenceKey(projectId: string, manifestUrl: string): string;
|
|
11
|
+
/** Loads or establishes a silent first-visit baseline.
|
|
12
|
+
* @param key - Isolated project/manifest key.
|
|
13
|
+
* @param baseline - Currently loaded semantic version, or null.
|
|
14
|
+
* @returns Durable preferences, or null when storage cannot be safely used.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readWebsitePreferences(key: string, baseline: string | null): WebsitePreferences | null;
|
|
17
|
+
/** Acknowledges rendered notes without overwriting a newer tab's acknowledgement.
|
|
18
|
+
* @param key - Isolated project/manifest key.
|
|
19
|
+
* @param version - Exact release which rendered successfully.
|
|
20
|
+
* @returns Whether durable storage is available.
|
|
21
|
+
*/
|
|
22
|
+
export declare function acknowledgeWebsiteVersion(key: string, version: string): boolean;
|
|
23
|
+
/** Reads this tab's most recently dismissed deployment.
|
|
24
|
+
* @param key - Isolated project/manifest key.
|
|
25
|
+
* @returns An opaque build ID, or null when unavailable.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readDismissedWebsiteBuild(key: string): string | null;
|
|
28
|
+
/** Remembers a deployment dismissal only in this tab.
|
|
29
|
+
* @param key - Isolated project/manifest key.
|
|
30
|
+
* @param buildId - Candidate dismissed by the user.
|
|
31
|
+
*/
|
|
32
|
+
export declare function dismissWebsiteBuild(key: string, buildId: string): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persists website release acknowledgements and per-tab deployment dismissals.
|
|
3
|
+
*
|
|
4
|
+
* Owns project/manifest isolation, first-visit baselines and monotonic version writes.
|
|
5
|
+
*
|
|
6
|
+
* Treat denied or corrupt storage conservatively and never show an automatic panel from memory alone.
|
|
7
|
+
*/
|
|
8
|
+
import { gt } from 'semver';
|
|
9
|
+
import { websiteReleaseVersion } from './version.js';
|
|
10
|
+
/** Creates one storage scope for a public project and resolved deployment manifest.
|
|
11
|
+
* @param projectId - Public project identifier.
|
|
12
|
+
* @param manifestUrl - Fully resolved manifest URL.
|
|
13
|
+
* @returns The isolated preferences key.
|
|
14
|
+
*/
|
|
15
|
+
export function websitePreferenceKey(projectId, manifestUrl) {
|
|
16
|
+
return `makershq.website-updates.v1:${encodeURIComponent(projectId)}:${encodeURIComponent(manifestUrl)}`;
|
|
17
|
+
}
|
|
18
|
+
/** Loads or establishes a silent first-visit baseline.
|
|
19
|
+
* @param key - Isolated project/manifest key.
|
|
20
|
+
* @param baseline - Currently loaded semantic version, or null.
|
|
21
|
+
* @returns Durable preferences, or null when storage cannot be safely used.
|
|
22
|
+
*/
|
|
23
|
+
export function readWebsitePreferences(key, baseline) {
|
|
24
|
+
try {
|
|
25
|
+
const saved = window.localStorage.getItem(key);
|
|
26
|
+
if (saved !== null) {
|
|
27
|
+
const value = JSON.parse(saved);
|
|
28
|
+
if (value &&
|
|
29
|
+
(value.acknowledgedVersion === null ||
|
|
30
|
+
(typeof value.acknowledgedVersion === 'string' &&
|
|
31
|
+
websiteReleaseVersion(value.acknowledgedVersion) ===
|
|
32
|
+
value.acknowledgedVersion)))
|
|
33
|
+
return { acknowledgedVersion: value.acknowledgedVersion };
|
|
34
|
+
}
|
|
35
|
+
const preferences = { acknowledgedVersion: baseline };
|
|
36
|
+
window.localStorage.setItem(key, JSON.stringify(preferences));
|
|
37
|
+
return preferences;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Acknowledges rendered notes without overwriting a newer tab's acknowledgement.
|
|
44
|
+
* @param key - Isolated project/manifest key.
|
|
45
|
+
* @param version - Exact release which rendered successfully.
|
|
46
|
+
* @returns Whether durable storage is available.
|
|
47
|
+
*/
|
|
48
|
+
export function acknowledgeWebsiteVersion(key, version) {
|
|
49
|
+
try {
|
|
50
|
+
const preferences = readWebsitePreferences(key, version);
|
|
51
|
+
if (!preferences)
|
|
52
|
+
return false;
|
|
53
|
+
if (!preferences.acknowledgedVersion ||
|
|
54
|
+
gt(version, preferences.acknowledgedVersion))
|
|
55
|
+
window.localStorage.setItem(key, JSON.stringify({ acknowledgedVersion: version }));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Reads this tab's most recently dismissed deployment.
|
|
63
|
+
* @param key - Isolated project/manifest key.
|
|
64
|
+
* @returns An opaque build ID, or null when unavailable.
|
|
65
|
+
*/
|
|
66
|
+
export function readDismissedWebsiteBuild(key) {
|
|
67
|
+
try {
|
|
68
|
+
return window.sessionStorage.getItem(key + ':dismissed');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Remembers a deployment dismissal only in this tab.
|
|
75
|
+
* @param key - Isolated project/manifest key.
|
|
76
|
+
* @param buildId - Candidate dismissed by the user.
|
|
77
|
+
*/
|
|
78
|
+
export function dismissWebsiteBuild(key, buildId) {
|
|
79
|
+
try {
|
|
80
|
+
window.sessionStorage.setItem(key + ':dismissed', buildId);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
/* The mounted component still remembers the dismissal. */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Release } from '../api.js';
|
|
2
|
+
import { type WebsiteBuild } from './version.js';
|
|
3
|
+
/** Configuration for browser deployment and after-refresh notifications. */
|
|
4
|
+
export interface WebsiteUpdatesOptions {
|
|
5
|
+
projectId: string;
|
|
6
|
+
/** Public project identifier. */
|
|
7
|
+
currentBuild: WebsiteBuild;
|
|
8
|
+
/** Build identity compiled into the running JavaScript. */
|
|
9
|
+
manifestUrl?: string;
|
|
10
|
+
/** Deployment manifest location; defaults to /version.json. */
|
|
11
|
+
beforeRefresh?: () => void | Promise<void>; /** App-owned save barrier; rejection keeps refresh retryable.
|
|
12
|
+
* @returns Completion after pending app state is saved.
|
|
13
|
+
*/
|
|
14
|
+
}
|
|
15
|
+
/** Coordinates deployment detection, fresh exact notes and safe refresh.
|
|
16
|
+
* @param options - Loaded build, release project and refresh configuration.
|
|
17
|
+
* @param options.projectId - Public project identifier.
|
|
18
|
+
* @param options.currentBuild - Build compiled into the running JavaScript.
|
|
19
|
+
* @param options.currentBuild.buildId - Opaque loaded deployment identity.
|
|
20
|
+
* @param options.currentBuild.releaseVersion - Optional loaded semantic release version.
|
|
21
|
+
* @param options.manifestUrl - Deployment manifest location.
|
|
22
|
+
* @param options.beforeRefresh - Consumer save barrier before reloading.
|
|
23
|
+
* @returns Visible release/candidate and actions bound to that presentation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useWebsiteUpdatesConnector({ projectId, currentBuild, manifestUrl, beforeRefresh, }: WebsiteUpdatesOptions): {
|
|
26
|
+
candidate: WebsiteBuild | null;
|
|
27
|
+
displayedRelease: Release | null;
|
|
28
|
+
refreshing: boolean;
|
|
29
|
+
error: string | null;
|
|
30
|
+
refresh: () => Promise<void>;
|
|
31
|
+
dismissCandidate: () => void;
|
|
32
|
+
dismissRelease: () => void;
|
|
33
|
+
onReleaseRendered: () => void;
|
|
34
|
+
};
|