← back to CelebritySignatures
iOS app: white gallery theme + all images render + tap-signature works/books (Steve email 8/5)
91f1e57018792cb06960b17c24275a3e0785bc94 · 2026-08-05 17:33:50 -0700 · Steve Abrams
- Flip the charcoal liquid-glass palette to a white gallery wall (glass.tsx GLASS
palette, scene gradient, light BlurView tints, dark StatusBar, per-screen
surface fills) — 'make all white not black'
- sigImg(): route Commons SVG signatures (62 incl. Jefferson) through
Special:FilePath?width= for a rasterized PNG; solid white backings so dark-ink
transparent signatures are visible — 'make sure all images serve'
- SignatureDetail: new Works section — open-museum artworks for Artists
(Met→AIC→Cleveland→Wikidata) and Open Library books for everyone else, tiles
link out — 'on click of signature bring up artwork or book and details'
- Games inherit the fixes (stage images + light chrome); tsc + expo export clean
Files touched
M apps/mobile/App.tsxM apps/mobile/api.tsM apps/mobile/screens/AccountScreen.tsxM apps/mobile/screens/GalleryScreen.tsxM apps/mobile/screens/GameScreen.tsxM apps/mobile/screens/MuralsScreen.tsxM apps/mobile/screens/SignatureDetail.tsxM apps/mobile/ui/glass.tsx
Diff
commit 91f1e57018792cb06960b17c24275a3e0785bc94
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 5 17:33:50 2026 -0700
iOS app: white gallery theme + all images render + tap-signature works/books (Steve email 8/5)
- Flip the charcoal liquid-glass palette to a white gallery wall (glass.tsx GLASS
palette, scene gradient, light BlurView tints, dark StatusBar, per-screen
surface fills) — 'make all white not black'
- sigImg(): route Commons SVG signatures (62 incl. Jefferson) through
Special:FilePath?width= for a rasterized PNG; solid white backings so dark-ink
transparent signatures are visible — 'make sure all images serve'
- SignatureDetail: new Works section — open-museum artworks for Artists
(Met→AIC→Cleveland→Wikidata) and Open Library books for everyone else, tiles
link out — 'on click of signature bring up artwork or book and details'
- Games inherit the fixes (stage images + light chrome); tsc + expo export clean
---
apps/mobile/App.tsx | 2 +-
apps/mobile/api.ts | 17 +++
apps/mobile/screens/AccountScreen.tsx | 6 +-
apps/mobile/screens/GalleryScreen.tsx | 13 +-
apps/mobile/screens/GameScreen.tsx | 22 ++--
apps/mobile/screens/MuralsScreen.tsx | 4 +-
apps/mobile/screens/SignatureDetail.tsx | 204 +++++++++++++++++++++++++++++++-
apps/mobile/ui/glass.tsx | 41 ++++---
8 files changed, 262 insertions(+), 47 deletions(-)
diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 446d692..4597fbb 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -73,7 +73,7 @@ export default function App() {
return (
<GlassScene>
<SafeAreaView style={styles.safe}>
- <StatusBar style="light" />
+ <StatusBar style="dark" />
<View style={styles.body}>
{tab === 'gallery' && <GalleryScreen onOpen={openSignature} />}
diff --git a/apps/mobile/api.ts b/apps/mobile/api.ts
index f367e42..b38d85f 100644
--- a/apps/mobile/api.ts
+++ b/apps/mobile/api.ts
@@ -44,6 +44,23 @@ export function qidOf(sig: Signature): string | null {
return m ? m[0] : null;
}
+/**
+ * RN's <Image> cannot decode SVG (62 of the 1,554 signatures, incl. Jefferson),
+ * so route Commons SVG originals through Special:FilePath?width=N, which 302s
+ * to a rasterized PNG thumb (the same mechanism the evolution data already
+ * uses; hand-building …/thumb/… paths 400s on some shards). Raster originals
+ * pass through untouched — they decode natively and the thumb service errors
+ * when asked to upscale a bitmap past its native width.
+ */
+export function sigImg(url?: string, width = 640): string | undefined {
+ if (!url) return undefined;
+ const m = url.match(
+ /^https?:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/[0-9a-f]\/[0-9a-f]{2}\/([^/?#]+\.svg)$/i,
+ );
+ if (!m) return url;
+ return `https://commons.wikimedia.org/wiki/Special:FilePath/${m[1]}?width=${width}`;
+}
+
async function getJSON<T>(path: string): Promise<T> {
const r = await fetch(`${WEB_BASE}${path}`);
if (!r.ok) throw new Error(`${path} → ${r.status}`);
diff --git a/apps/mobile/screens/AccountScreen.tsx b/apps/mobile/screens/AccountScreen.tsx
index 927c124..ba89f62 100644
--- a/apps/mobile/screens/AccountScreen.tsx
+++ b/apps/mobile/screens/AccountScreen.tsx
@@ -455,7 +455,7 @@ const styles = StyleSheet.create({
overflow: 'hidden',
borderWidth: StyleSheet.hairlineWidth,
borderColor: GLASS.edge,
- backgroundColor: 'rgba(255,255,255,0.04)',
+ backgroundColor: '#ffffff',
marginBottom: 24,
},
toggleSegment: {
@@ -485,7 +485,7 @@ const styles = StyleSheet.create({
fieldWrap: {
borderRadius: 12,
overflow: 'hidden',
- backgroundColor: 'rgba(255,255,255,0.07)',
+ backgroundColor: '#ffffff',
borderWidth: StyleSheet.hairlineWidth,
borderColor: GLASS.edgeFaint,
},
@@ -544,7 +544,7 @@ const styles = StyleSheet.create({
deleteSection: {
paddingTop: 20,
borderTopWidth: StyleSheet.hairlineWidth,
- borderTopColor: 'rgba(255,255,255,0.08)',
+ borderTopColor: 'rgba(20,18,16,0.1)',
},
deleteCaption: {
fontSize: 12,
diff --git a/apps/mobile/screens/GalleryScreen.tsx b/apps/mobile/screens/GalleryScreen.tsx
index 87eda50..f46c0d0 100644
--- a/apps/mobile/screens/GalleryScreen.tsx
+++ b/apps/mobile/screens/GalleryScreen.tsx
@@ -16,7 +16,7 @@ import {
} from 'react-native';
import * as Haptics from 'expo-haptics';
-import { api, qidOf, Signature } from '../api';
+import { api, qidOf, sigImg, Signature } from '../api';
import { GLASS, GlassPanel, GlassPill, GlassButton } from '../ui/glass';
// ---------------------------------------------------------------------------
@@ -86,11 +86,12 @@ function SigCard({
style={styles.cardPressable}
>
<GlassPanel style={styles.card}>
- {/* parchment image backing so dark-ink signatures are legible */}
+ {/* solid white image backing — signatures are dark ink on transparent,
+ so anything but white renders them invisible */}
<View style={styles.imgBacking}>
{sig.signature_image_url ? (
<Image
- source={{ uri: sig.signature_image_url }}
+ source={{ uri: sigImg(sig.signature_image_url) }}
style={styles.img}
resizeMode="contain"
/>
@@ -293,7 +294,7 @@ const styles = StyleSheet.create({
marginBottom: 8,
borderRadius: 12,
overflow: 'hidden',
- backgroundColor: 'rgba(255,255,255,0.07)',
+ backgroundColor: '#ffffff',
borderWidth: StyleSheet.hairlineWidth,
borderColor: GLASS.edgeFaint,
},
@@ -338,7 +339,7 @@ const styles = StyleSheet.create({
// GlassPanel handles borderRadius + overflow
},
imgBacking: {
- backgroundColor: 'rgba(232,217,184,0.06)',
+ backgroundColor: '#ffffff',
borderRadius: 10,
height: 110,
alignItems: 'center',
@@ -353,7 +354,7 @@ const styles = StyleSheet.create({
imgPlaceholder: {
width: '100%',
height: '100%',
- backgroundColor: 'rgba(255,255,255,0.03)',
+ backgroundColor: 'rgba(0,0,0,0.03)',
},
cardName: {
fontSize: 14,
diff --git a/apps/mobile/screens/GameScreen.tsx b/apps/mobile/screens/GameScreen.tsx
index fe74c0b..579289b 100644
--- a/apps/mobile/screens/GameScreen.tsx
+++ b/apps/mobile/screens/GameScreen.tsx
@@ -36,7 +36,7 @@ import {
} from 'react-native';
import * as Haptics from 'expo-haptics';
-import { api, Signature, LeaderRow, Evolution, qidOf } from '../api';
+import { api, Signature, LeaderRow, Evolution, qidOf, sigImg } from '../api';
import { GLASS, GlassPanel, GlassButton, GlassPill } from '../ui/glass';
// ---------------------------------------------------------------------------
@@ -667,7 +667,7 @@ function Stage({ round }: { round: Round | undefined }) {
return (
<View style={styles.sigWrap}>
<GlassPanel style={styles.sigPanel} intensity={18} tint="light">
- <Image source={{ uri: round.answer.signature_image_url }} style={styles.sigImage} resizeMode="contain" accessibilityLabel="A handwritten signature" />
+ <Image source={{ uri: sigImg(round.answer.signature_image_url) }} style={styles.sigImage} resizeMode="contain" accessibilityLabel="A handwritten signature" />
</GlassPanel>
</View>
);
@@ -688,7 +688,7 @@ function Stage({ round }: { round: Round | undefined }) {
{(['leftUrl', 'rightUrl'] as const).map((k, idx) => (
<View key={k} style={styles.duoCell}>
<GlassPanel style={styles.sigPanel} intensity={18} tint="light">
- <Image source={{ uri: round[k] }} style={styles.duoImg} resizeMode="contain" accessibilityLabel={`Signature ${idx === 0 ? 'A' : 'B'}`} />
+ <Image source={{ uri: sigImg(round[k]) }} style={styles.duoImg} resizeMode="contain" accessibilityLabel={`Signature ${idx === 0 ? 'A' : 'B'}`} />
</GlassPanel>
<Text style={styles.duoLab}>{idx === 0 ? 'A' : 'B'}</Text>
</View>
@@ -952,7 +952,7 @@ const styles = StyleSheet.create({
gameGrid: { gap: 10, marginTop: 4 },
gcard: {
borderRadius: 16, borderWidth: StyleSheet.hairlineWidth, borderColor: GLASS.edgeFaint,
- backgroundColor: 'rgba(255,255,255,0.04)', padding: 14,
+ backgroundColor: '#ffffff', padding: 14,
},
gcardOn: { borderColor: GLASS.accent, backgroundColor: 'rgba(201,168,106,0.14)' },
gcardTitle: { color: GLASS.accentSoft, fontSize: 17, fontWeight: '800', letterSpacing: 0.2 },
@@ -964,7 +964,7 @@ const styles = StyleSheet.create({
chip: {
flexDirection: 'row', alignItems: 'center',
borderRadius: 999, borderWidth: StyleSheet.hairlineWidth, borderColor: GLASS.edgeFaint,
- paddingHorizontal: 14, paddingVertical: 9, backgroundColor: 'rgba(255,255,255,0.04)',
+ paddingHorizontal: 14, paddingVertical: 9, backgroundColor: '#ffffff',
},
chipActive: { borderColor: GLASS.accent, backgroundColor: 'rgba(201,168,106,0.2)' },
chipText: { color: GLASS.accentSoft, fontSize: 14, fontWeight: '600' },
@@ -978,7 +978,7 @@ const styles = StyleSheet.create({
prompt: { color: GLASS.accentSoft, fontSize: 18, fontWeight: '700', textAlign: 'center', letterSpacing: 0.2, lineHeight: 24 },
tag: { color: GLASS.accent, fontSize: 13, fontWeight: '800', letterSpacing: 0.8, textAlign: 'center', textTransform: 'uppercase' },
- timerTrack: { height: 6, borderRadius: 3, backgroundColor: 'rgba(255,255,255,0.08)', overflow: 'hidden' },
+ timerTrack: { height: 6, borderRadius: 3, backgroundColor: 'rgba(0,0,0,0.08)', overflow: 'hidden' },
timerFill: { height: 6, backgroundColor: GLASS.accent },
sigWrap: {
@@ -986,13 +986,13 @@ const styles = StyleSheet.create({
shadowOpacity: 0.22, shadowRadius: 24, elevation: 10,
},
sigPanel: {},
- sigImage: { width: '100%', height: 190 },
+ sigImage: { width: '100%', height: 190, backgroundColor: '#ffffff', borderRadius: 8 },
artImage: { width: '100%', height: 260, borderRadius: 8 },
// early duo
duo: { flexDirection: 'row', gap: 12 },
duoCell: { flex: 1, alignItems: 'center', gap: 6 },
- duoImg: { width: '100%', height: 150 },
+ duoImg: { width: '100%', height: 150, backgroundColor: '#ffffff', borderRadius: 8 },
duoLab: { color: GLASS.accent, fontSize: 16, fontWeight: '800' },
// choices
@@ -1006,10 +1006,10 @@ const styles = StyleSheet.create({
matchGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', gap: 10 },
matchCell: {
width: '48%', aspectRatio: 1, borderRadius: 14, overflow: 'hidden',
- borderWidth: 2, backgroundColor: 'rgba(255,255,255,0.04)',
+ borderWidth: 2, backgroundColor: '#ffffff',
},
matchImg: { width: '100%', height: '100%' },
- matchLabelWrap: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(13,11,10,0.82)', paddingVertical: 4, paddingHorizontal: 6 },
+ matchLabelWrap: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(255,255,254,0.92)', paddingVertical: 4, paddingHorizontal: 6 },
matchLabel: { color: GLASS.accentSoft, fontSize: 11, fontWeight: '600', textAlign: 'center' },
// reveal
@@ -1027,7 +1027,7 @@ const styles = StyleSheet.create({
rankText: { color: GLASS.accent, fontSize: 18, fontWeight: '700', textAlign: 'center' },
nameInput: {
borderRadius: 14, borderWidth: StyleSheet.hairlineWidth, borderColor: GLASS.edge,
- backgroundColor: 'rgba(255,255,255,0.06)', color: GLASS.accentSoft, fontSize: 16,
+ backgroundColor: '#ffffff', color: GLASS.accentSoft, fontSize: 16,
paddingHorizontal: 16, paddingVertical: 13,
},
submitBtn: { marginTop: 2 },
diff --git a/apps/mobile/screens/MuralsScreen.tsx b/apps/mobile/screens/MuralsScreen.tsx
index 1cd418d..2e657b4 100644
--- a/apps/mobile/screens/MuralsScreen.tsx
+++ b/apps/mobile/screens/MuralsScreen.tsx
@@ -475,10 +475,10 @@ const styles = StyleSheet.create({
marginBottom: 4,
},
input: {
- backgroundColor: 'rgba(255,255,255,0.06)',
+ backgroundColor: '#ffffff',
borderRadius: 10,
borderWidth: StyleSheet.hairlineWidth,
- borderColor: 'rgba(255,255,255,0.14)',
+ borderColor: 'rgba(20,18,16,0.14)',
paddingHorizontal: 13,
paddingVertical: Platform.OS === 'ios' ? 12 : 9,
color: GLASS.accentSoft,
diff --git a/apps/mobile/screens/SignatureDetail.tsx b/apps/mobile/screens/SignatureDetail.tsx
index 5ee9b7f..bf3fc4c 100644
--- a/apps/mobile/screens/SignatureDetail.tsx
+++ b/apps/mobile/screens/SignatureDetail.tsx
@@ -2,9 +2,11 @@
// The App wraps this in a Modal with its own GlassBar top chrome;
// this component is just the scrollable body content.
// Usage: <SignatureDetail sig={sig} evolution={ev} onOrderMural={(sig) => …} />
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
Image,
+ Linking,
+ Pressable,
ScrollView,
StyleSheet,
Text,
@@ -12,9 +14,125 @@ import {
} from 'react-native';
import * as Haptics from 'expo-haptics';
-import { qidOf, Signature, Evolution } from '../api';
+import { qidOf, sigImg, Signature, Evolution } from '../api';
import { GLASS, GlassPanel, GlassButton, GlassPill } from '../ui/glass';
+// ---------------------------------------------------------------------------
+// Works loader — tapping a signature opens this detail, which surfaces the
+// person's ARTWORK (Artists — free open-museum APIs, tried in order until we
+// have 6: The Met → Art Institute of Chicago → Cleveland → Wikidata/Commons)
+// or their BOOKS (everyone else — Open Library, keyless). Mirrors the web
+// popup's loader in public/index.html.
+// ---------------------------------------------------------------------------
+
+type Work = { img: string; title: string; date: string; url: string; src: string };
+const WORKS_CACHE: Record<string, Work[]> = {};
+const MAX_WORKS = 6;
+
+async function loadArtworks(qid: string | null, name: string): Promise<Work[]> {
+ const works: Work[] = [];
+ const last = name.toLowerCase().split(' ').pop() || '';
+ // 1) The Met
+ try {
+ const MET = 'https://collectionapi.metmuseum.org/public/collection/v1';
+ const s = await (await fetch(`${MET}/search?hasImages=true&artistOrCulture=true&q=${encodeURIComponent(name)}`)).json();
+ for (const id of (s.objectIDs || []).slice(0, 14)) {
+ if (works.length >= MAX_WORKS) break;
+ try {
+ const o = await (await fetch(`${MET}/objects/${id}`)).json();
+ if (!o.primaryImageSmall) continue;
+ if (!(o.artistDisplayName || '').toLowerCase().includes(last)) continue;
+ works.push({
+ img: o.primaryImageSmall, title: o.title || 'Untitled', date: o.objectDate || '',
+ url: o.objectURL || `https://www.metmuseum.org/art/collection/search/${id}`, src: 'The Met',
+ });
+ } catch { /* skip object */ }
+ }
+ } catch { /* Met unavailable */ }
+ // 2) Art Institute of Chicago
+ if (works.length < MAX_WORKS) try {
+ const j = await (await fetch(
+ `https://api.artic.edu/api/v1/artworks/search?q=${encodeURIComponent(name)}` +
+ '&query[term][is_public_domain]=true&fields=id,title,image_id,artist_title,date_display&limit=12',
+ )).json();
+ for (const a of (j.data || [])) {
+ if (works.length >= MAX_WORKS) break;
+ if (!a.image_id || !(a.artist_title || '').toLowerCase().includes(last)) continue;
+ works.push({
+ img: `https://www.artic.edu/iiif/2/${a.image_id}/full/400,/0/default.jpg`,
+ title: a.title || 'Untitled', date: a.date_display || '',
+ url: `https://www.artic.edu/artworks/${a.id}`, src: 'Art Institute of Chicago',
+ });
+ }
+ } catch { /* AIC unavailable */ }
+ // 3) Cleveland Museum of Art
+ if (works.length < MAX_WORKS) try {
+ const j = await (await fetch(
+ `https://openaccess-api.clevelandart.org/api/artworks/?artists=${encodeURIComponent(name)}&has_image=1&limit=10`,
+ )).json();
+ for (const a of (j.data || [])) {
+ if (works.length >= MAX_WORKS) break;
+ const img = a.images?.web?.url;
+ if (!img) continue;
+ if (!(a.creators || []).some((c: any) => (c.description || '').toLowerCase().includes(last))) continue;
+ works.push({
+ img, title: a.title || 'Untitled', date: a.creation_date || '',
+ url: a.url || 'https://www.clevelandart.org/art/collection/search', src: 'Cleveland Museum of Art',
+ });
+ }
+ } catch { /* Cleveland unavailable */ }
+ // 4) Wikidata works-by-QID (P170 creator + P18 image) — QID-exact fallback
+ if (qid && works.length < MAX_WORKS) try {
+ const q = `SELECT ?w ?wLabel ?img ?date WHERE { ?w wdt:P170 wd:${qid} ; wdt:P18 ?img . OPTIONAL { ?w wdt:P571 ?date } SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } } LIMIT ${MAX_WORKS * 2}`;
+ const j = await (await fetch(
+ `https://query.wikidata.org/sparql?format=json&query=${encodeURIComponent(q)}`,
+ { headers: { Accept: 'application/sparql-results+json' } },
+ )).json();
+ for (const b of (j.results?.bindings || [])) {
+ if (works.length >= MAX_WORKS) break;
+ const m = (b.img?.value || '').match(/Special:FilePath\/(.+)$/);
+ if (!m) continue;
+ works.push({
+ img: `https://commons.wikimedia.org/wiki/Special:FilePath/${m[1]}?width=400`,
+ title: b.wLabel?.value || 'Untitled', date: (b.date?.value || '').slice(0, 4),
+ url: b.w?.value || '', src: 'Wikimedia Commons',
+ });
+ }
+ } catch { /* WDQS unavailable */ }
+ return works;
+}
+
+async function loadBooks(name: string): Promise<Work[]> {
+ const works: Work[] = [];
+ try {
+ const j = await (await fetch(
+ `https://openlibrary.org/search.json?author=${encodeURIComponent(name)}&sort=editions&limit=10&fields=title,cover_i,first_publish_year,key`,
+ )).json();
+ for (const d of (j.docs || [])) {
+ if (works.length >= MAX_WORKS) break;
+ if (!d.cover_i) continue;
+ works.push({
+ img: `https://covers.openlibrary.org/b/id/${d.cover_i}-M.jpg`,
+ title: d.title || 'Untitled',
+ date: d.first_publish_year ? String(d.first_publish_year) : '',
+ url: d.key ? `https://openlibrary.org${d.key}` : 'https://openlibrary.org',
+ src: 'Open Library',
+ });
+ }
+ } catch { /* Open Library unavailable */ }
+ return works;
+}
+
+async function loadWorks(sig: Signature): Promise<Work[]> {
+ const key = qidOf(sig) || sig.full_name;
+ if (WORKS_CACHE[key]) return WORKS_CACHE[key];
+ const out = sig.category === 'Artists'
+ ? await loadArtworks(qidOf(sig), sig.full_name)
+ : await loadBooks(sig.full_name);
+ WORKS_CACHE[key] = out;
+ return out;
+}
+
// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------
@@ -82,6 +200,15 @@ export default function SignatureDetail({
const qid = qidOf(sig);
const evEntry = qid && evolution ? evolution[qid] ?? null : null;
+ // works (artwork or books) — loaded per signature; null = still loading
+ const [works, setWorks] = useState<Work[] | null>(null);
+ useEffect(() => {
+ let cancelled = false;
+ setWorks(null);
+ loadWorks(sig).then((w) => { if (!cancelled) setWorks(w); }).catch(() => { if (!cancelled) setWorks([]); });
+ return () => { cancelled = true; };
+ }, [sig]);
+
const handleOrder = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
onOrderMural(sig);
@@ -98,7 +225,7 @@ export default function SignatureDetail({
<View style={styles.heroBacking}>
{sig.signature_image_url ? (
<Image
- source={{ uri: sig.signature_image_url }}
+ source={{ uri: sigImg(sig.signature_image_url) }}
style={styles.heroImg}
resizeMode="contain"
/>
@@ -139,6 +266,38 @@ export default function SignatureDetail({
<EvolutionReel sigs={evEntry.sigs} />
)}
+ {/* Works — the person's artwork (Artists) or books (everyone else) */}
+ <View style={styles.section}>
+ <Text style={styles.sectionTitle}>
+ {sig.category === 'Artists' ? 'Works — open museum collections' : 'Books'}
+ </Text>
+ {works === null ? (
+ <Text style={styles.creditText}>loading…</Text>
+ ) : works.length === 0 ? (
+ <Text style={styles.creditText}>
+ {sig.category === 'Artists'
+ ? 'No openly-licensed works found in the open museum APIs.'
+ : 'No books found on Open Library.'}
+ </Text>
+ ) : (
+ <View style={styles.worksGrid}>
+ {works.map((w) => (
+ <Pressable
+ key={w.url + w.title}
+ style={styles.workTile}
+ onPress={() => { Haptics.selectionAsync(); Linking.openURL(w.url).catch(() => {}); }}
+ >
+ <Image source={{ uri: w.img }} style={styles.workImg} resizeMode="cover" />
+ <Text style={styles.workTitle} numberOfLines={2}>
+ {w.title}{w.date ? ` · ${w.date}` : ''}
+ </Text>
+ <Text style={styles.workSrc} numberOfLines={1}>{w.src}</Text>
+ </Pressable>
+ ))}
+ </View>
+ )}
+ </View>
+
{/* Image credit */}
{(sig.image_author || sig.image_license) && (
<View style={styles.section}>
@@ -180,7 +339,7 @@ const styles = StyleSheet.create({
// GlassPanel adds borderRadius + panelBody padding
},
heroBacking: {
- backgroundColor: 'rgba(232,217,184,0.07)',
+ backgroundColor: '#ffffff',
borderRadius: 12,
height: 200,
alignItems: 'center',
@@ -263,7 +422,7 @@ const styles = StyleSheet.create({
reelImgBacking: {
width: 120,
height: 80,
- backgroundColor: 'rgba(232,217,184,0.07)',
+ backgroundColor: '#ffffff',
borderRadius: 10,
overflow: 'hidden',
borderWidth: StyleSheet.hairlineWidth,
@@ -287,6 +446,41 @@ const styles = StyleSheet.create({
letterSpacing: 0.15,
},
+ // Works grid (artworks / book covers)
+ worksGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 10,
+ },
+ workTile: {
+ width: '48%',
+ borderRadius: 12,
+ overflow: 'hidden',
+ backgroundColor: '#ffffff',
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: GLASS.edgeFaint,
+ paddingBottom: 8,
+ },
+ workImg: {
+ width: '100%',
+ height: 150,
+ backgroundColor: 'rgba(0,0,0,0.04)',
+ },
+ workTitle: {
+ fontSize: 12,
+ fontWeight: '600',
+ color: GLASS.accentSoft,
+ lineHeight: 16,
+ paddingHorizontal: 8,
+ paddingTop: 6,
+ },
+ workSrc: {
+ fontSize: 10,
+ color: GLASS.textDim,
+ paddingHorizontal: 8,
+ paddingTop: 2,
+ },
+
// CTA
ctaWrap: {
marginTop: 4,
diff --git a/apps/mobile/ui/glass.tsx b/apps/mobile/ui/glass.tsx
index a40d992..a2f1cd0 100644
--- a/apps/mobile/ui/glass.tsx
+++ b/apps/mobile/ui/glass.tsx
@@ -6,21 +6,24 @@ import { BlurView } from 'expo-blur';
import { LinearGradient } from 'expo-linear-gradient';
// Liquid Glass primitives (reused from the Charge & Explore app, recolored to a
-// museum charcoal-and-gold palette for the signatures gallery). Every surface is
-// a real BlurView refracting the gradient scene behind it — not a faked rgba fill
-// — with a specular top edge so panels read as curved glass. Android's BlurView
-// needs the experimental method or it silently renders a gray slab.
+// white gallery-wall palette for the signatures gallery — Steve 2026-08-05:
+// "make all white not black"). Every surface is a real BlurView refracting the
+// paper-white scene behind it, with a specular top edge so panels read as glass.
+// Android's BlurView needs the experimental method or it silently renders a slab.
+// NOTE: `ink` stays the dark museum charcoal — it is the TEXT color and the
+// text-on-gold color; `accentSoft` (once parchment used as light-on-dark text)
+// is now the dark warm body-text color so every screen inherits readable text.
export const GLASS = {
ink: '#141210',
- inkDeep: '#0d0b0a',
- emerald: '#2a2118', // warm brown mid-tone for the scene gradient
- accent: '#c9a86a', // museum gold
- accentSoft: '#e8d9b8', // parchment
- textDim: '#a89f90',
- edge: 'rgba(255,255,255,0.26)',
- edgeFaint: 'rgba(255,255,255,0.11)',
- sheen: 'rgba(255,255,255,0.08)',
+ inkDeep: '#faf8f2', // scene mid-tone (was near-black)
+ emerald: '#f1ebdd', // warm paper tone closing the scene gradient
+ accent: '#9a7b3f', // museum gold, deepened to read on white
+ accentSoft: '#2b241a', // primary text — warm near-black on white
+ textDim: '#7a715f', // muted text on white
+ edge: 'rgba(20,18,16,0.16)',
+ edgeFaint: 'rgba(20,18,16,0.09)',
+ sheen: 'rgba(255,255,255,0.65)',
} as const;
const blurProps = Platform.select({
@@ -32,7 +35,7 @@ const blurProps = Platform.select({
export function GlassScene({ children, style }: { children: React.ReactNode; style?: ViewStyle }) {
return (
<LinearGradient
- colors={[GLASS.ink, GLASS.inkDeep, GLASS.emerald]}
+ colors={['#ffffff', GLASS.inkDeep, GLASS.emerald]}
locations={[0, 0.55, 1]}
start={{ x: 0.1, y: 0 }}
end={{ x: 0.9, y: 1 }}
@@ -40,7 +43,7 @@ export function GlassScene({ children, style }: { children: React.ReactNode; sty
>
{/* off-center warm-gold glow orb gives the blur a light source to bend */}
<LinearGradient
- colors={['rgba(201,168,106,0.26)', 'rgba(201,168,106,0)']}
+ colors={['rgba(201,168,106,0.18)', 'rgba(201,168,106,0)']}
style={styles.orb}
pointerEvents="none"
/>
@@ -51,7 +54,7 @@ export function GlassScene({ children, style }: { children: React.ReactNode; sty
/** Frosted panel — the base glass slab (cards, hero, sections). */
export function GlassPanel({
- children, style, intensity = 28, tint = 'dark',
+ children, style, intensity = 28, tint = 'light',
}: {
children: React.ReactNode; style?: ViewStyle; intensity?: number;
tint?: 'dark' | 'light';
@@ -74,7 +77,7 @@ export function GlassButton({
return (
<Pressable onPress={onPress} disabled={disabled || busy}
style={({ pressed }) => [styles.btnClip, pressed && styles.btnPressed]}>
- <BlurView intensity={solid ? 50 : 26} tint="dark" style={StyleSheet.absoluteFill} {...blurProps} />
+ <BlurView intensity={solid ? 50 : 26} tint="light" style={StyleSheet.absoluteFill} {...blurProps} />
{solid && (
<LinearGradient
colors={['rgba(201,168,106,0.95)', 'rgba(176,142,84,0.92)']}
@@ -95,7 +98,7 @@ export function GlassPill({
}: { children: React.ReactNode; tintColor?: string; style?: ViewStyle }) {
return (
<View style={[styles.pillClip, style]}>
- <BlurView intensity={30} tint="dark" style={StyleSheet.absoluteFill} {...blurProps} />
+ <BlurView intensity={30} tint="light" style={StyleSheet.absoluteFill} {...blurProps} />
{tintColor && <View style={[StyleSheet.absoluteFill, { backgroundColor: tintColor, opacity: 0.55 }]} />}
<View style={styles.sheenTop} pointerEvents="none" />
<View style={styles.pillBody}>{children}</View>
@@ -109,7 +112,7 @@ export function GlassBar({
}: { title: string; onDone: () => void; light?: boolean }) {
return (
<View style={styles.barClip}>
- <BlurView intensity={40} tint={light ? 'light' : 'dark'} style={StyleSheet.absoluteFill} {...blurProps} />
+ <BlurView intensity={40} tint="light" style={StyleSheet.absoluteFill} {...blurProps} />
<View style={styles.sheenTop} pointerEvents="none" />
<View style={styles.barRow}>
<Pressable onPress={onDone} hitSlop={12}>
@@ -161,6 +164,6 @@ const styles = StyleSheet.create({
},
barDone: { color: GLASS.accent, fontSize: 16, fontWeight: '600', width: 44 },
barDoneLight: { color: '#141210' },
- barTitle: { fontSize: 16, fontWeight: '700', color: '#fff' },
+ barTitle: { fontSize: 16, fontWeight: '700', color: GLASS.ink },
barTitleLight: { color: '#141210' },
});
← 701c009 mobile: harden games port per adversarial review
·
back to CelebritySignatures
·
web: clicking a signature opens its details popup; Books sec e2d7d1c →