@krapwoo/ds-viewer 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/dev.js +49 -5
- package/dist/cli/workspace.js +37 -1
- package/dist/native/catalog/tokens.js +60 -12
- package/native/catalog/BoundedOverlayViewport.tsx +2 -2
- package/native/catalog/CatalogButton.tsx +2 -2
- package/native/catalog/CatalogSearchInput.tsx +3 -3
- package/native/catalog/CatalogShell.tsx +7 -7
- package/native/catalog/CatalogSidebar.tsx +12 -16
- package/native/catalog/ComparisonGrid.tsx +4 -4
- package/native/catalog/ComparisonGroups.tsx +3 -3
- package/native/catalog/ComparisonList.tsx +2 -2
- package/native/catalog/MotionSpecimen.tsx +3 -3
- package/native/catalog/PhoneFrame.tsx +2 -2
- package/native/catalog/PropsTable.tsx +7 -7
- package/native/catalog/ReferenceDetails.tsx +33 -22
- package/native/catalog/SectionBlock.tsx +10 -16
- package/native/catalog/SpacingScaleGallery.tsx +3 -3
- package/native/catalog/Swatch.tsx +3 -3
- package/native/catalog/TokenLayouts.tsx +6 -9
- package/native/catalog/TokenRow.tsx +2 -2
- package/native/catalog/TypeScaleGallery.tsx +2 -2
- package/native/catalog/UpdatePanel.tsx +535 -78
- package/native/catalog/VariantGroup.tsx +3 -6
- package/native/catalog/index.ts +3 -0
- package/native/catalog/tokens.ts +63 -12
- package/native/catalog/updatePanelState.ts +173 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
-
import {
|
|
3
|
+
import { CATALOG_TEXT, CATALOG_COLOR } from './tokens';
|
|
4
4
|
|
|
5
5
|
/** Labels one example (or small group of examples) inside a section's Examples card — a bold
|
|
6
6
|
* uppercase name plus a one-line description stacked below it, sitting above whatever demo content
|
|
@@ -39,10 +39,7 @@ const styles = StyleSheet.create({
|
|
|
39
39
|
group: { gap: 6 },
|
|
40
40
|
groupCenter: { alignItems: 'center' },
|
|
41
41
|
groupLeft: { alignItems: 'flex-start' },
|
|
42
|
-
name: {
|
|
43
|
-
|
|
44
|
-
textTransform: 'uppercase', letterSpacing: 0.4,
|
|
45
|
-
},
|
|
46
|
-
desc: { fontSize: CATALOG_TYPE.sm, color: CATALOG_COLOR.textMuted },
|
|
42
|
+
name: { ...CATALOG_TEXT.microLabel, color: CATALOG_COLOR.text },
|
|
43
|
+
desc: { ...CATALOG_TEXT.caption, color: CATALOG_COLOR.textMuted },
|
|
47
44
|
textCenter: { textAlign: 'center' },
|
|
48
45
|
});
|
package/native/catalog/index.ts
CHANGED
package/native/catalog/tokens.ts
CHANGED
|
@@ -28,6 +28,32 @@ export const CATALOG_TYPE = {
|
|
|
28
28
|
'4xl': 40,
|
|
29
29
|
} as const;
|
|
30
30
|
|
|
31
|
+
export const CATALOG_FONT = { code: 'Menlo, monospace' } as const;
|
|
32
|
+
|
|
33
|
+
/** The catalog's fixed set of typography roles, each a complete `fontSize`/`fontWeight`/
|
|
34
|
+
* `lineHeight` (plus `letterSpacing`/`textTransform`/`fontFamily` where the role needs them)
|
|
35
|
+
* built on `CATALOG_TYPE`. Every catalog-chrome text style spreads one of these rather than
|
|
36
|
+
* setting its own typography properties directly (enforced by
|
|
37
|
+
* `__tests__/textRoles.test.ts`). */
|
|
38
|
+
export const CATALOG_TEXT = {
|
|
39
|
+
pageTitle: { fontSize: CATALOG_TYPE.pageTitle, fontWeight: '700', lineHeight: 34 },
|
|
40
|
+
heading1: { fontSize: CATALOG_TYPE['2xl'], fontWeight: '700', lineHeight: 30 },
|
|
41
|
+
heading2: { fontSize: CATALOG_TYPE.xl, fontWeight: '700', lineHeight: 24 },
|
|
42
|
+
heading3: { fontSize: CATALOG_TYPE.lg, fontWeight: '700', lineHeight: 22 },
|
|
43
|
+
icon: { fontSize: CATALOG_TYPE.lg, fontWeight: '400', lineHeight: 22 },
|
|
44
|
+
body: { fontSize: CATALOG_TYPE.md, fontWeight: '400', lineHeight: 20 },
|
|
45
|
+
bodyStrong: { fontSize: CATALOG_TYPE.md, fontWeight: '700', lineHeight: 20 },
|
|
46
|
+
caption: { fontSize: CATALOG_TYPE.sm, fontWeight: '400', lineHeight: 18 },
|
|
47
|
+
captionStrong: { fontSize: CATALOG_TYPE.sm, fontWeight: '700', lineHeight: 18 },
|
|
48
|
+
label: { fontSize: CATALOG_TYPE.sm, fontWeight: '700', lineHeight: 18, letterSpacing: 0.72, textTransform: 'uppercase' },
|
|
49
|
+
micro: { fontSize: CATALOG_TYPE.xs, fontWeight: '400', lineHeight: 14 },
|
|
50
|
+
microLabel: { fontSize: CATALOG_TYPE.xs, fontWeight: '700', lineHeight: 14, letterSpacing: 0.6, textTransform: 'uppercase' },
|
|
51
|
+
code: { fontSize: CATALOG_TYPE.md, fontWeight: '400', lineHeight: 20, fontFamily: CATALOG_FONT.code },
|
|
52
|
+
codeSmall: { fontSize: CATALOG_TYPE.sm, fontWeight: '400', lineHeight: 18, fontFamily: CATALOG_FONT.code },
|
|
53
|
+
codeSmallStrong: { fontSize: CATALOG_TYPE.sm, fontWeight: '700', lineHeight: 18, fontFamily: CATALOG_FONT.code },
|
|
54
|
+
codeMicro: { fontSize: CATALOG_TYPE.xs, fontWeight: '400', lineHeight: 14, fontFamily: CATALOG_FONT.code },
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
31
57
|
export const CATALOG_SPACE = {
|
|
32
58
|
xs: 4,
|
|
33
59
|
sm: 8,
|
|
@@ -75,22 +101,44 @@ export const CATALOG_LAYOUT = {
|
|
|
75
101
|
export const CATALOG_MAX_CONTENT_WIDTH = 1200;
|
|
76
102
|
|
|
77
103
|
/** When to reach for each catalog-chrome type size. Rendered in the framework catalog's Type
|
|
78
|
-
* Scale page.
|
|
104
|
+
* Scale page. Every size is now consumed through a `CATALOG_TEXT` role (see `CATALOG_TEXT_USE`)
|
|
105
|
+
* rather than directly. */
|
|
79
106
|
export const CATALOG_TYPE_USE: Record<keyof typeof CATALOG_TYPE, string> = {
|
|
80
|
-
xs: '
|
|
81
|
-
tableHeader: '
|
|
82
|
-
sm: '
|
|
83
|
-
panelHeading: '
|
|
84
|
-
md: '
|
|
85
|
-
lg: '
|
|
86
|
-
brand: '
|
|
87
|
-
xl:
|
|
88
|
-
'2xl':
|
|
89
|
-
pageTitle: '
|
|
107
|
+
xs: 'Backs the `micro`, `microLabel` and `codeMicro` roles: token values and meta captions; sidebar group labels and OS badges (uppercase); a prop\'s type and default value (monospace).',
|
|
108
|
+
tableHeader: 'Reserved — no current consumer; grid column headers and list cell captions now use the `captionStrong` role (built on `sm`).',
|
|
109
|
+
sm: 'Backs the `caption`, `captionStrong`, `label`, `codeSmall` and `codeSmallStrong` roles: breadcrumbs, block/prop descriptions, swatch/token names, table captions, monospace prop names and file paths.',
|
|
110
|
+
panelHeading: 'Reserved — no current consumer; reference-panel and update-page headings now use the `label` role (built on `sm`).',
|
|
111
|
+
md: 'Backs the `body`, `bodyStrong` and `code` roles: descriptions, nav links, the filter field, switch labels, a long unbroken value that needs to wrap at its \'/\'s (monospace).',
|
|
112
|
+
lg: 'Backs the `heading3` and `icon` roles: a result note\'s title; pager arrows and the filter field\'s clear (×) glyph.',
|
|
113
|
+
brand: 'Reserved — no current consumer; the sidebar app name now uses the `heading2` role (built on `xl`).',
|
|
114
|
+
xl: 'Backs the `heading2` role: the app name at the top of the sidebar.',
|
|
115
|
+
'2xl': 'Backs the `heading1` role: the update page\'s version numbers and the arrow between them.',
|
|
116
|
+
pageTitle: 'Backs the `pageTitle` role: the selected page\'s title, including the update page\'s own title.',
|
|
90
117
|
'3xl': 'Reserved — no current consumer.',
|
|
91
118
|
'4xl': 'Reserved — no current consumer.',
|
|
92
119
|
};
|
|
93
120
|
|
|
121
|
+
/** When to reach for each catalog-chrome typography role. Rendered in the framework catalog's
|
|
122
|
+
* Type Scale page. */
|
|
123
|
+
export const CATALOG_TEXT_USE: Record<keyof typeof CATALOG_TEXT, string> = {
|
|
124
|
+
pageTitle: 'The selected page\'s title, including the update page\'s own title.',
|
|
125
|
+
heading1: 'The update page\'s version numbers and the arrow between them.',
|
|
126
|
+
heading2: 'The app name at the top of the sidebar.',
|
|
127
|
+
heading3: 'A result note\'s title (the update page\'s success/failure banner).',
|
|
128
|
+
icon: 'Glyph-only controls: pager arrows, the filter field\'s clear (×) glyph, the update banner\'s close (×) glyph.',
|
|
129
|
+
body: 'Default body text: descriptions, nav links, the filter field, update-page facts/notes/steps.',
|
|
130
|
+
bodyStrong: 'Emphasised body text: grid/list row headers, switch and step labels, inline-bold notice text.',
|
|
131
|
+
caption: 'Secondary text: breadcrumbs, status captions, block/prop descriptions, sidebar subtitle/footer, update-page captions.',
|
|
132
|
+
captionStrong: 'Emphasised secondary text: grid column headers, grid/list/table captions, reference-panel subheadings, swatch/token names, replay and result links, step-icon glyphs.',
|
|
133
|
+
label: 'Uppercase labels and headings: block and preview-frame labels, token-section labels, reference-panel and update-page headings.',
|
|
134
|
+
micro: 'The smallest text: token values and meta captions, a prop\'s default-value label.',
|
|
135
|
+
microLabel: 'Uppercase micro labels: variant-group names, sidebar group labels, OS badges.',
|
|
136
|
+
code: 'Monospace body text: a long unbroken value that needs to wrap at its \'/\'s.',
|
|
137
|
+
codeSmall: 'Monospace secondary text: a prop\'s optional marker, the update page\'s inline code, file paths and log lines.',
|
|
138
|
+
codeSmallStrong: 'Emphasised monospace secondary text: a prop\'s own name.',
|
|
139
|
+
codeMicro: 'Monospace micro text: a prop\'s type and default value.',
|
|
140
|
+
};
|
|
141
|
+
|
|
94
142
|
/** When to reach for each catalog-chrome spacing step. Rendered in the framework catalog's
|
|
95
143
|
* Spacing page. */
|
|
96
144
|
export const CATALOG_SPACE_USE: Record<keyof typeof CATALOG_SPACE, string> = {
|
|
@@ -135,7 +183,10 @@ export const CATALOG_COLOR = {
|
|
|
135
183
|
accentSubtle: '#e9efff',
|
|
136
184
|
/** 3px keyboard focus ring on every catalog control. */
|
|
137
185
|
focusRing: '#c9d7ff',
|
|
138
|
-
code
|
|
186
|
+
/** @deprecated kept for the public API; re-points at `CATALOG_FONT.code` so there is one source
|
|
187
|
+
* of truth. New code should reach for `CATALOG_FONT.code` (or a `CATALOG_TEXT` `code*` role)
|
|
188
|
+
* instead. */
|
|
189
|
+
code: CATALOG_FONT.code,
|
|
139
190
|
/** Major-update banner background/border/text (design §5 "Viewer notice"). */
|
|
140
191
|
warning: '#8a4b00',
|
|
141
192
|
warningSubtle: '#fff4e0',
|
|
@@ -219,3 +219,176 @@ export function resultNote(
|
|
|
219
219
|
export function isRemoteOrigin(protocol: string, hostname: string): boolean {
|
|
220
220
|
return !(protocol === 'http:' && hostname === 'localhost');
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
/** `VersionDetails`' own near-miss message, when the endpoint answered with a non-ok status (not
|
|
224
|
+
* a rejected fetch) and the page is open from a tunnelled/remote origin. Pulled out as its own
|
|
225
|
+
* function only so a test can pin it exactly — the approved design leaves this condition alone
|
|
226
|
+
* ("Keep the existing distinct remote-origin/tunnel message exactly as is"). */
|
|
227
|
+
export function remoteOriginLocalErrorMessage(): string {
|
|
228
|
+
return 'This page is open from another address, and the viewer only takes actions from the machine it runs on. Open the catalog on that machine, or run npx ds-viewer update there.';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The result of one endpoint call, classified the one way that matters for self-healing: did
|
|
232
|
+
* `fetch` itself reject (the socket is simply gone — *unreachable*, see the implementation
|
|
233
|
+
* brief), or did it resolve with a response at all, however unhappy (401/409/502 included — the
|
|
234
|
+
* endpoint answered, so the caller's existing status/body handling is unchanged and untouched by
|
|
235
|
+
* this module). Every endpoint call on the update page routes through this one function so "one
|
|
236
|
+
* condition has one treatment" instead of each call site re-deriving it. */
|
|
237
|
+
export type EndpointOutcome = { kind: 'ok'; response: Response } | { kind: 'unreachable' };
|
|
238
|
+
|
|
239
|
+
export async function requestOrUnreachable(doFetch: () => Promise<Response>): Promise<EndpointOutcome> {
|
|
240
|
+
try {
|
|
241
|
+
return { kind: 'ok', response: await doFetch() };
|
|
242
|
+
} catch {
|
|
243
|
+
return { kind: 'unreachable' };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Critical finding, Fable correction pass: `cli/endpoint.ts` answers a rejected `Origin` with
|
|
248
|
+
* 403 and *no* CORS headers, so the browser blocks the response and the page's own `fetch`
|
|
249
|
+
* rejects — exactly like a genuinely dead socket. Without this check, that rejection was routed
|
|
250
|
+
* straight into the page-wide reachability flow, which reloads and then polls forever for a
|
|
251
|
+
* server that was never down; self-healing is meaningless for a page that can never use the
|
|
252
|
+
* endpoint in the first place (a tunnel, or `isRemoteOrigin`'s own `127.0.0.1` case). Every call
|
|
253
|
+
* site with a rejected fetch must check this *before* calling into the reachability flow, and
|
|
254
|
+
* keep showing `remoteOriginLocalErrorMessage()` instead. */
|
|
255
|
+
export function classifyRejectedFetch(isRemoteOriginPage: boolean): 'remote-origin' | 'unreachable' {
|
|
256
|
+
return isRemoteOriginPage ? 'remote-origin' : 'unreachable';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The pre-existing offline plan-error outcome (`fetchPlan`'s own `catch`) — pulled out so the one
|
|
260
|
+
* other place that needs it (below) can't drift from this wording. */
|
|
261
|
+
export const PLAN_FETCH_ERROR: { error: string } = { error: "Couldn't prepare the update. You may be offline, or npm didn't respond. Nothing was changed." };
|
|
262
|
+
|
|
263
|
+
/** Correction pass 3: a background `fetchPlan` whose fetch was rejected routes through
|
|
264
|
+
* `routeUnreachableInBackground`, which refuses to act for a remote-origin page (it must never
|
|
265
|
+
* latch the page-wide `remoteOriginError` state for a call the user didn't make — correction pass
|
|
266
|
+
* #2). That refusal used to be the end of it: `planResult` stayed `undefined` forever, and
|
|
267
|
+
* `resolvePanelPhase` reads that as `'checking'` with no button and no way out. `handled` is
|
|
268
|
+
* whatever that route reports back — `true` once it has entered the reachability flow (which owns
|
|
269
|
+
* the page from there), `false` when it declined to act. Only the `false` case needs anything
|
|
270
|
+
* here: fall back to the exact same plan-error note (and working **Try again**) the pre-existing
|
|
271
|
+
* `catch` already shows for every other kind of plan failure. */
|
|
272
|
+
export function backgroundPlanFetchOutcome(handled: boolean): { error: string } | undefined {
|
|
273
|
+
return handled ? undefined : PLAN_FETCH_ERROR;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Critical finding, Fable correction pass: the restart-handoff rule ("a failed `/update/status`
|
|
277
|
+
* poll while an update was already in flight or restarting means 'still restarting,' not
|
|
278
|
+
* unreachable") used to be decided by reading a value assigned inside the very `setStatus`
|
|
279
|
+
* updater that decided it — updaters run during React's render phase, not at dispatch, so under
|
|
280
|
+
* any pending update the read could see stale or re-invoked state. Pulled out as a pure function
|
|
281
|
+
* of the *already-committed* phase (read from a ref, never from inside an updater) so the
|
|
282
|
+
* decision is both correct and independently testable. `undefined` means "not mid-update": the
|
|
283
|
+
* caller must still classify the failure itself (unreachable vs. remote-origin, vs. a delivered
|
|
284
|
+
* but unusable response). */
|
|
285
|
+
export function statusPollFailureOutcome(currentPhase: UpdateStatusView['phase']): UpdateStatusView | undefined {
|
|
286
|
+
return currentPhase === 'updating' || currentPhase === 'restarting' ? { phase: 'restarting' } : undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Correction pass 4 (Critical): a stable endpoint address means the restarted server answers the
|
|
290
|
+
* already-open page's poll *before* Metro has served the new bundle — same host, same pinned
|
|
291
|
+
* Metro origin, same reused secret, so the poll succeeds and would otherwise accept the delivered
|
|
292
|
+
* `{ phase: 'success' }` onto the still-old page. `restarting` must be terminal for the page: only
|
|
293
|
+
* the Metro-origin reload poller (which owns the actual reload) may leave it, by delivering
|
|
294
|
+
* `'restarting'` right back — every other delivered phase while `restarting` is stale and must be
|
|
295
|
+
* ignored. */
|
|
296
|
+
export function acceptsDeliveredStatus(current: UpdateStatusView['phase'], next: UpdateStatusView['phase']): boolean {
|
|
297
|
+
return current !== 'restarting' || next === 'restarting';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Correction pass 5 (Important): whether a just-fetched plan is the offline error note rather
|
|
301
|
+
* than a usable plan — `/plan` answers 200 with an `{ error }` body for that case, so a reached
|
|
302
|
+
* plan fetch alone doesn't mean "Everything below is current." Takes the plan straight from the
|
|
303
|
+
* fetch that produced it, never from a ref a passive effect fills in later, so this reads the
|
|
304
|
+
* value that was actually just stored. */
|
|
305
|
+
export function planShowsError(result?: UpdatePlanView | { error: string }): boolean {
|
|
306
|
+
return result !== undefined && 'error' in result && !isAlreadyUpToDateError(result);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The approved design's unreachable-recovery states (`waiting`/`stuck` replace this page's normal
|
|
310
|
+
* content; `recovered` is a transient banner over content that's already current). Never
|
|
311
|
+
* `'ok'`/absent here — the panel represents "nothing wrong" as no `ReachabilityState` at all, the
|
|
312
|
+
* same way `UpdateStatusView`'s `idle` phase represents nothing in flight.
|
|
313
|
+
*
|
|
314
|
+
* Final slice: a stable endpoint address (`cli/dev.ts` now reuses its port and secret across
|
|
315
|
+
* restarts) means an ordinary restart recovers through the `waiting` poller's own direct endpoint
|
|
316
|
+
* retry alone — no reload is needed, so there is no `reconnecting` phase anymore, and nothing in
|
|
317
|
+
* this module ever calls `window.location.reload()`. The only reload left is the user's own
|
|
318
|
+
* explicit press of **Reconnect** on `stuck`, handled entirely in `UpdatePanel.tsx`. */
|
|
319
|
+
export type ReachabilityPhase = 'waiting' | 'recovered' | 'stuck';
|
|
320
|
+
|
|
321
|
+
export interface ReachabilityState {
|
|
322
|
+
phase: ReachabilityPhase;
|
|
323
|
+
/** Consecutive failed endpoint retries while `waiting` — the bound `nextWaitingAttempt` gives up
|
|
324
|
+
* at. Meaningless (and left unchanged) for every other phase. */
|
|
325
|
+
attempts: number;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** ~20 seconds of 500ms retries (the same interval the existing restart flow already polls at) —
|
|
329
|
+
* long enough for someone to actually type `npx ds-viewer dev`, short enough that the page
|
|
330
|
+
* doesn't spin forever before offering the manual `Reconnect`. */
|
|
331
|
+
export const REACHABILITY_MAX_ATTEMPTS = 40;
|
|
332
|
+
|
|
333
|
+
/** One more failed attempt while `waiting`: advances the bounded counter and gives up once it
|
|
334
|
+
* reaches `REACHABILITY_MAX_ATTEMPTS`, exactly as the approved design's "Edge · Still not back"
|
|
335
|
+
* state requires. The caller (the `waiting` poller) only calls this when its own direct retry
|
|
336
|
+
* against the endpoint has already failed; a retry that succeeds recovers in place and never
|
|
337
|
+
* reaches this function at all. */
|
|
338
|
+
export function nextWaitingAttempt(current: ReachabilityState): ReachabilityState {
|
|
339
|
+
const attempts = current.attempts + 1;
|
|
340
|
+
return { phase: attempts >= REACHABILITY_MAX_ATTEMPTS ? 'stuck' : 'waiting', attempts };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The manual **Reconnect** button on the gave-up state: starts the same bounded cycle over from
|
|
344
|
+
* zero, exactly like the first time an endpoint call found the viewer unreachable. `UpdatePanel.tsx`
|
|
345
|
+
* pairs this with its own, separate `window.location.reload()` — one press, one reload, initiated
|
|
346
|
+
* by the user, so no marker or loop guard is needed here or anywhere else. */
|
|
347
|
+
export function restartReachability(): ReachabilityState {
|
|
348
|
+
return { phase: 'waiting', attempts: 0 };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Called on every endpoint call that actually succeeds. Most of the time there was nothing to
|
|
352
|
+
* recover from (`current` is `undefined`) and this is a no-op — the ordinary case, which must not
|
|
353
|
+
* show a "Reconnected" banner on every routine poll. Otherwise the panel shows the approved
|
|
354
|
+
* design's auto-dismissing `recovered` note while its normal content — already current, since the
|
|
355
|
+
* endpoint itself just answered — resumes underneath. */
|
|
356
|
+
export function recoveredReachabilityState(current: ReachabilityState | undefined): ReachabilityState | undefined {
|
|
357
|
+
if (current === undefined) return undefined;
|
|
358
|
+
return { phase: 'recovered', attempts: 0 };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface ReachabilityNote {
|
|
362
|
+
variant: 'muted' | 'ok';
|
|
363
|
+
spinner: boolean;
|
|
364
|
+
bold?: string;
|
|
365
|
+
text: string;
|
|
366
|
+
/** A recovery command to render in `Code`, split from its lead-in text so the component can
|
|
367
|
+
* place the two in separate `Text`/`Code` nodes. */
|
|
368
|
+
hint?: { prefix: string; command: string };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The exact approved copy (`.hermes-task/approved-unreachable-states.html`) for each reachability
|
|
372
|
+
* phase — never invent wording beyond this. */
|
|
373
|
+
export function reachabilityNote(phase: ReachabilityPhase): ReachabilityNote {
|
|
374
|
+
switch (phase) {
|
|
375
|
+
case 'waiting':
|
|
376
|
+
return {
|
|
377
|
+
variant: 'muted',
|
|
378
|
+
spinner: true,
|
|
379
|
+
bold: 'The viewer isn’t running.',
|
|
380
|
+
text: ' Nothing was changed. This page reconnects by itself as soon as it’s back.',
|
|
381
|
+
hint: { prefix: 'Start it with ', command: 'npx ds-viewer dev' },
|
|
382
|
+
};
|
|
383
|
+
case 'recovered':
|
|
384
|
+
return { variant: 'ok', spinner: false, text: 'Reconnected. Everything below is current.' };
|
|
385
|
+
case 'stuck':
|
|
386
|
+
return {
|
|
387
|
+
variant: 'muted',
|
|
388
|
+
spinner: false,
|
|
389
|
+
bold: 'The viewer still isn’t answering.',
|
|
390
|
+
text: ' Nothing was changed.',
|
|
391
|
+
hint: { prefix: 'or start it with ', command: 'npx ds-viewer dev' },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|