Skip to content

Commit 428bf41

Browse files
IM.codesclaude
andcommitted
feat: splash two-line layout, upload progress bar, iOS camera permission
- Splash screen: "IM." / "codes" two-line layout (web animation + iOS static image) - Fix "codes" typeOut animation width (2.8em → 3.4em) so "s" isn't clipped - Upload: XHR-based progress bar with percentage display - iOS: add NSCameraUsageDescription + NSPhotoLibraryUsageDescription - SubSessionBar: shorten "Sub-sessions" → "Subs" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0de3dbc commit 428bf41

8 files changed

Lines changed: 66 additions & 17 deletions

File tree

web/index.html

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,21 @@
4848
50% { opacity: 0.7; }
4949
}
5050

51-
/* Logo container */
51+
/* Logo container — two-line layout */
5252
.splash-logo {
5353
position: relative; z-index: 2;
54-
display: flex; align-items: baseline; gap: 0;
55-
font-size: clamp(42px, 10vw, 72px);
54+
display: flex; flex-direction: column; align-items: center; gap: 0;
5655
font-weight: 700;
5756
letter-spacing: -0.02em;
5857
line-height: 1;
5958
}
6059

60+
/* Top line: "IM." */
61+
.splash-top {
62+
display: flex; align-items: baseline;
63+
font-size: clamp(56px, 14vw, 96px);
64+
}
65+
6166
/* "IM" — glitch decode */
6267
.splash-im {
6368
color: #e0f0ff;
@@ -76,13 +81,16 @@
7681
animation: dotAppear 0.15s ease-out 0.7s both;
7782
}
7883

79-
/* "codes" — type out */
84+
/* Bottom line: "codes" — type out */
8085
.splash-codes {
86+
display: inline-block;
87+
font-size: clamp(40px, 10vw, 68px);
8188
color: #94a3b8;
8289
overflow: hidden;
8390
white-space: nowrap;
8491
width: 0;
8592
animation: typeOut 0.5s steps(5, end) 0.85s forwards;
93+
margin-top: 4px;
8694
}
8795

8896
/* Subtitle */
@@ -130,7 +138,7 @@
130138

131139
@keyframes typeOut {
132140
0% { width: 0; }
133-
100% { width: 2.8em; }
141+
100% { width: 3.4em; }
134142
}
135143

136144
@keyframes fadeUp {
@@ -153,7 +161,8 @@
153161
<div id="splash">
154162
<div class="splash-scan"></div>
155163
<div class="splash-logo">
156-
<span class="splash-im">IM</span><span class="splash-dot">.</span><span class="splash-codes">codes</span>
164+
<div class="splash-top"><span class="splash-im">IM</span><span class="splash-dot">.</span></div>
165+
<span class="splash-codes">codes</span>
157166
</div>
158167
<div class="splash-sub">initializing</div>
159168
<div class="splash-beam"></div>
-97.7 KB
Loading
-97.7 KB
Loading
-97.7 KB
Loading

web/ios/App/App/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
<string>IM.codes uses speech recognition to convert your voice to text commands.</string>
5252
<key>NSMicrophoneUsageDescription</key>
5353
<string>IM.codes needs microphone access for voice input.</string>
54+
<key>NSCameraUsageDescription</key>
55+
<string>IM.codes needs camera access to take photos for upload.</string>
56+
<key>NSPhotoLibraryUsageDescription</key>
57+
<string>IM.codes needs photo library access to select files for upload.</string>
5458
<key>CFBundleURLTypes</key>
5559
<array>
5660
<dict>

web/src/api.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -381,19 +381,43 @@ export interface AttachmentRefResponse {
381381
export async function uploadFile(
382382
serverId: string,
383383
file: File,
384+
onProgress?: (pct: number) => void,
384385
): Promise<{ ok: boolean; attachment: AttachmentRefResponse }> {
385386
const form = new FormData();
386387
form.append('file', file);
387-
// Use rawFetch to avoid Content-Type override (browser sets multipart boundary)
388-
const res = await rawFetch(`/api/server/${serverId}/upload`, {
389-
method: 'POST',
390-
body: form,
388+
389+
// Use XHR for upload progress reporting
390+
return new Promise((resolve, reject) => {
391+
const xhr = new XMLHttpRequest();
392+
xhr.open('POST', `${_baseUrl}/api/server/${serverId}/upload`);
393+
394+
// Auth headers (same as rawFetch)
395+
if (_apiKey) {
396+
xhr.setRequestHeader('Authorization', `Bearer ${_apiKey}`);
397+
} else {
398+
xhr.withCredentials = true;
399+
const csrf = document.cookie.match(/csrf_token=([^;]+)/)?.[1];
400+
if (csrf) xhr.setRequestHeader('X-CSRF-Token', csrf);
401+
}
402+
403+
xhr.upload.onprogress = (e) => {
404+
if (e.lengthComputable && onProgress) {
405+
onProgress(Math.round((e.loaded / e.total) * 100));
406+
}
407+
};
408+
409+
xhr.onload = () => {
410+
if (xhr.status >= 200 && xhr.status < 300) {
411+
try { resolve(JSON.parse(xhr.responseText)); }
412+
catch { reject(new ApiError(xhr.status, 'Invalid JSON response')); }
413+
} else {
414+
reject(new ApiError(xhr.status, xhr.responseText));
415+
}
416+
};
417+
418+
xhr.onerror = () => reject(new ApiError(0, 'Network error'));
419+
xhr.send(form);
391420
});
392-
if (!res.ok) {
393-
const body = await res.text().catch(() => '');
394-
throw new ApiError(res.status, body);
395-
}
396-
return res.json() as Promise<{ ok: boolean; attachment: AttachmentRefResponse }>;
397421
}
398422

399423
export async function downloadAttachment(serverId: string, attachmentId: string): Promise<void> {

web/src/components/SessionControls.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export function SessionControls({ ws, activeSession, inputRef, onAfterAction, on
115115
// File upload state
116116
const fileInputRef = useRef<HTMLInputElement>(null);
117117
const [uploading, setUploading] = useState(false);
118+
const [uploadProgress, setUploadProgress] = useState(0);
118119
const [uploadError, setUploadError] = useState<string | null>(null);
119120

120121
// Keep external inputRef in sync so parent can call .focus()
@@ -309,11 +310,12 @@ export function SessionControls({ ws, activeSession, inputRef, onAfterAction, on
309310
const handleFileUpload = useCallback(async (files: FileList | null) => {
310311
if (!files || files.length === 0 || !serverId) return;
311312
setUploading(true);
313+
setUploadProgress(0);
312314
setUploadError(null);
313315
const paths: string[] = [];
314316
for (const file of Array.from(files)) {
315317
try {
316-
const result = await uploadFile(serverId, file);
318+
const result = await uploadFile(serverId, file, (pct) => setUploadProgress(pct));
317319
if (result.attachment?.daemonPath) {
318320
paths.push('@' + result.attachment.daemonPath + ' ');
319321
}
@@ -504,6 +506,16 @@ export function SessionControls({ ws, activeSession, inputRef, onAfterAction, on
504506
)}
505507
</div>}
506508

509+
{/* Upload progress bar */}
510+
{uploading && (
511+
<div style={{ margin: '0 8px 4px', height: 18, display: 'flex', alignItems: 'center', gap: 8 }}>
512+
<div style={{ flex: 1, height: 4, background: 'rgba(255,255,255,0.1)', borderRadius: 2, overflow: 'hidden' }}>
513+
<div style={{ width: `${uploadProgress}%`, height: '100%', background: '#3b82f6', borderRadius: 2, transition: 'width 0.2s ease' }} />
514+
</div>
515+
<span style={{ fontSize: 11, color: '#94a3b8', minWidth: 32 }}>{uploadProgress}%</span>
516+
</div>
517+
)}
518+
507519
{/* Upload error banner */}
508520
{uploadError && (
509521
<div style={{ padding: '4px 12px', fontSize: 12, color: '#ef4444', background: 'rgba(239,68,68,0.1)', borderRadius: 4, margin: '0 8px 4px' }}>

web/src/components/SubSessionBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export function SubSessionBar({ subSessions, openIds, onOpen, onNew, onNewDiscus
200200
>
201201
202202
</button>
203-
<span class="subcard-toolbar-label">Sub-sessions ({subSessions.length})</span>
203+
<span class="subcard-toolbar-label">Subs ({subSessions.length})</span>
204204
{stats && (
205205
<span class="daemon-stats-inline" title={`${stats.daemonVersion ? `Daemon ${stats.daemonVersion} | ` : ''}Load: ${stats.load1} / ${stats.load5} / ${stats.load15} | Uptime: ${formatUptime(stats.uptime)}`}>
206206
{stats.daemonVersion && (

0 commit comments

Comments
 (0)