Citrine: add safe dev-preview flag to review the UI in a browser (1.3.2)
The CitrineCard is Android-only (renders null off-native), so its four visual
states couldn't be reviewed without an APK build. Adds a strictly cosmetic
preview switch:
localStorage.setItem('citrine:preview', '1') // show card, real behaviour
localStorage.setItem('citrine:preview', 'live') // force connected state
localStorage.setItem('citrine:preview', 'waiting') // force not-detected
localStorage.setItem('citrine:preview', 'searching') // force searching
localStorage.removeItem('citrine:preview') // back to normal
Safety: the flag unlocks RENDERING ONLY via isCitrinePossible(). It never sets
citrine.active and never yields a relay url, so no query or publish is ever
routed anywhere different — production relay behaviour is identical with or
without the flag. A forced state is purely visual (enabled/probing are derived
locally in the card for preview; the real probe still governs active/url).
lib/citrine.ts: citrinePreview()/isCitrinePreview() + CitrinePreviewState.
useCitrine: exposes `preview`. CitrineCard: forced state overrides the computed
state for review only.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
2e6a20dc2b
commit
335fb40f72
4 changed files with 74 additions and 15 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "skate-spots",
|
||||
"private": true,
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"type": "module",
|
||||
"homepage": "https://skate-spots.com",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import {
|
||||
CITRINE_RELAY_URL,
|
||||
citrinePreview,
|
||||
isCitrinePossible,
|
||||
probeCitrine,
|
||||
} from '@/lib/citrine';
|
||||
|
|
@ -46,6 +47,10 @@ export function useCitrine() {
|
|||
|
||||
const active = possible && enabled && reachable;
|
||||
|
||||
// DEV PREVIEW ONLY: a forced visual state for browser design review. Purely
|
||||
// cosmetic — it never affects `active`/`url`, so no traffic is ever rerouted.
|
||||
const preview = citrinePreview();
|
||||
|
||||
const setEnabled = useCallback(
|
||||
(next: boolean) => {
|
||||
updateConfig((current) => ({ ...current, useCitrineRelay: next }));
|
||||
|
|
@ -67,6 +72,12 @@ export function useCitrine() {
|
|||
*/
|
||||
settled: isFetched,
|
||||
url: active ? CITRINE_RELAY_URL : null,
|
||||
/**
|
||||
* DEV PREVIEW ONLY — a forced UI state ('off'|'searching'|'live'|'waiting')
|
||||
* when the citrine:preview flag is set, else null. Cosmetic; does not affect
|
||||
* `active`/`url`. The Settings card uses it to override its computed state.
|
||||
*/
|
||||
preview,
|
||||
setEnabled,
|
||||
/** Re-run the reachability probe (e.g. after the user starts Citrine). */
|
||||
recheck: refetch,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,45 @@
|
|||
/** Citrine's default localhost relay endpoint. */
|
||||
export const CITRINE_RELAY_URL = 'ws://127.0.0.1:4869';
|
||||
|
||||
/** Forcible UI states for the dev preview flag. */
|
||||
export type CitrinePreviewState = 'off' | 'searching' | 'live' | 'waiting';
|
||||
const PREVIEW_KEY = 'citrine:preview';
|
||||
|
||||
/**
|
||||
* DEV PREVIEW ONLY — lets the (normally Android-only) Citrine UI render in a
|
||||
* browser so the design can be reviewed without an APK build.
|
||||
*
|
||||
* Set in the browser console:
|
||||
* localStorage.setItem('citrine:preview', '1') // show the card, real behaviour
|
||||
* localStorage.setItem('citrine:preview', 'live') // force the "connected" state
|
||||
* localStorage.setItem('citrine:preview', 'waiting') // force "not detected"
|
||||
* localStorage.setItem('citrine:preview', 'searching')// force "searching…"
|
||||
* localStorage.removeItem('citrine:preview') // back to normal
|
||||
*
|
||||
* CRITICAL: this unlocks RENDERING ONLY. It never makes the local relay
|
||||
* `active` and never yields a relay `url`, so no query or publish is ever routed
|
||||
* anywhere different — production relay behaviour is completely unchanged. A
|
||||
* forced state is purely cosmetic.
|
||||
*/
|
||||
export function citrinePreview(): CitrinePreviewState | null {
|
||||
try {
|
||||
const raw = (globalThis as { localStorage?: Storage }).localStorage?.getItem(PREVIEW_KEY);
|
||||
if (!raw) return null;
|
||||
if (raw === 'off' || raw === 'searching' || raw === 'live' || raw === 'waiting') {
|
||||
return raw;
|
||||
}
|
||||
// Any other truthy value ("1"/"true") = show the card with real behaviour.
|
||||
return 'off';
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** True if the dev preview flag is set at all (any value). */
|
||||
export function isCitrinePreview(): boolean {
|
||||
return citrinePreview() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when running inside a Capacitor native Android shell. Citrine is an
|
||||
* Android app, so the local relay can only exist there. This never throws in a
|
||||
|
|
@ -39,13 +78,13 @@ export function isAndroidNative(): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* True if the Citrine relay URL *could* be reachable on this device — i.e. we're
|
||||
* on native Android. This is a cheap synchronous gate used to decide whether to
|
||||
* even offer the feature / attempt a probe. Actual reachability is confirmed by
|
||||
* probeCitrine().
|
||||
* True if the Citrine UI/feature should be offered on this device — native
|
||||
* Android, OR the dev preview flag is set (browser design review). Actual
|
||||
* reachability is still confirmed separately by probeCitrine(), which fails
|
||||
* cleanly in a browser, so preview never routes real traffic.
|
||||
*/
|
||||
export function isCitrinePossible(): boolean {
|
||||
return isAndroidNative();
|
||||
return isAndroidNative() || isCitrinePreview();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -283,7 +283,9 @@ function CitrineCard() {
|
|||
// Not on native Android → the local relay can't exist. Hide entirely.
|
||||
if (!citrine.possible) return null;
|
||||
|
||||
const state: 'off' | 'searching' | 'live' | 'waiting' = !citrine.enabled
|
||||
// Normal computed state. In dev preview (citrine.preview set), a forced state
|
||||
// overrides it purely for visual review — real relay behaviour is unaffected.
|
||||
const computedState: 'off' | 'searching' | 'live' | 'waiting' = !citrine.enabled
|
||||
? 'off'
|
||||
: citrine.reachable
|
||||
? 'live'
|
||||
|
|
@ -291,6 +293,13 @@ function CitrineCard() {
|
|||
? 'searching'
|
||||
: 'waiting';
|
||||
|
||||
const state = citrine.preview ?? computedState;
|
||||
// Treat any non-off forced preview state as "enabled" so the expanded detail
|
||||
// (chips / CTA / status) renders for review.
|
||||
const enabled = citrine.preview ? citrine.preview !== 'off' : citrine.enabled;
|
||||
// Spinner reflects a real probe, or a forced "searching" preview state.
|
||||
const probing = citrine.probing || state === 'searching';
|
||||
|
||||
const handleToggle = () => {
|
||||
const next = !citrine.enabled;
|
||||
citrine.setEnabled(next);
|
||||
|
|
@ -339,7 +348,7 @@ function CitrineCard() {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
{citrine.enabled && (
|
||||
{enabled && (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-black',
|
||||
|
|
@ -386,23 +395,23 @@ function CitrineCard() {
|
|||
onClick={handleToggle}
|
||||
className={cn(
|
||||
'relative w-11 h-6 rounded-full transition-all duration-300 flex-shrink-0 active:scale-95',
|
||||
citrine.enabled ? 'bg-[#ccff00]' : 'bg-white/10',
|
||||
enabled ? 'bg-[#ccff00]' : 'bg-white/10',
|
||||
)}
|
||||
role="switch"
|
||||
aria-checked={citrine.enabled}
|
||||
aria-checked={enabled}
|
||||
aria-label="Toggle local Citrine relay"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-0.5 w-5 h-5 rounded-full bg-black transition-all duration-300 ease-out',
|
||||
citrine.enabled ? 'left-[calc(100%-22px)]' : 'left-0.5',
|
||||
enabled ? 'left-[calc(100%-22px)]' : 'left-0.5',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Expanding detail — only when enabled */}
|
||||
{citrine.enabled && (
|
||||
{enabled && (
|
||||
<div className="px-4 pb-4 space-y-3 animate-in fade-in slide-in-from-top-1 duration-300">
|
||||
{/* Status row + recheck */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
|
|
@ -430,14 +439,14 @@ function CitrineCard() {
|
|||
</span>
|
||||
<button
|
||||
onClick={() => citrine.recheck()}
|
||||
disabled={citrine.probing}
|
||||
disabled={probing}
|
||||
className={cn(
|
||||
'ml-auto inline-flex items-center gap-1 rounded-full px-2 py-1 -mr-1 text-muted-foreground',
|
||||
'hover:text-white hover:bg-white/5 transition-colors disabled:opacity-50',
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={cn('w-3 h-3', citrine.probing && 'animate-spin')} />
|
||||
{citrine.probing ? 'Checking' : 'Recheck'}
|
||||
<RefreshCw className={cn('w-3 h-3', probing && 'animate-spin')} />
|
||||
{probing ? 'Checking' : 'Recheck'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue