-
{transfer.fileName}
-
{formatSize(transfer.fileSize)}
+
{transfer.fileName}
+
{formatSize(transfer.fileSize)}
{transfer.status === "transferring" && (
-
-
+
{transfer.status === "pending" && (
- En attente
+ Waiting
)}
{transfer.status === "transferring" && (
-
+
{Math.round(transfer.progress * 100)}%
)}
{transfer.status === "done" && (
)}
{transfer.status === "error" && (
- Erreur
+ Failed
)}
diff --git a/web/src/design/tokens.ts b/web/src/design/tokens.ts
new file mode 100644
index 0000000..c78791c
--- /dev/null
+++ b/web/src/design/tokens.ts
@@ -0,0 +1,44 @@
+/**
+ * AnyDrop design tokens — Paper & Envelope direction.
+ *
+ * Principles:
+ * - Paper-warm neutrals + ink black, never slate/grey-blue.
+ * - ONE signature accent (oxblood). Use sparingly — accent earns its rarity.
+ * - Serif display + neutral sans body. Type does the heavy lifting, not color.
+ * - Shadows are textural, not dramatic. Radii stay sharp (2–6px).
+ * - Motion curves favor paper physics (ease-out-expo), never bounce.
+ */
+export const color = {
+ paper: "#F5F0E6",
+ paperDeep: "#EBE4D4",
+ paperEdge: "#DCD3BE",
+ ink: "#1A1714",
+ inkMuted: "#6B635A",
+ inkFaint: "#A89F93",
+
+ signal: "#7A2320",
+ signalQuiet: "#F3E2E0",
+
+ ok: "#3E6B4A",
+ warn: "#8B6914",
+ fail: "#8A3324",
+} as const;
+
+export const font = {
+ display: `"Fraunces", "GT Sectra", Georgia, serif`,
+ sans: `"Inter", "Söhne", system-ui, -apple-system, sans-serif`,
+ mono: `"JetBrains Mono", "Berkeley Mono", ui-monospace, monospace`,
+} as const;
+
+export const motion = {
+ fast: "160ms cubic-bezier(0.2, 0.0, 0.0, 1.0)",
+ base: "320ms cubic-bezier(0.2, 0.0, 0.0, 1.0)",
+ paper: "480ms cubic-bezier(0.16, 1, 0.3, 1)",
+} as const;
+
+export const shadow = {
+ paper:
+ "0 1px 0 rgba(26, 23, 20, 0.04), 0 1px 3px rgba(26, 23, 20, 0.06)",
+ lift:
+ "0 2px 6px rgba(26, 23, 20, 0.08), 0 8px 24px rgba(26, 23, 20, 0.08)",
+} as const;
diff --git a/web/src/index.css b/web/src/index.css
index ebf28ab..887b3ce 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -1,9 +1,77 @@
+@import url("https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap");
+
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
+ :root {
+ color-scheme: light;
+ }
+ html,
body {
- @apply min-h-screen;
+ background: #f5f0e6;
+ color: #1a1714;
+ font-family: "Inter", "Söhne", system-ui, -apple-system, sans-serif;
+ font-feature-settings: "ss01", "cv11";
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+ }
+ /* Paper texture — very subtle noise, 2% opacity */
+ body::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ background-image: url("data:image/svg+xml;utf8,
");
+ mix-blend-mode: multiply;
+ opacity: 0.35;
+ }
+ #root {
+ position: relative;
+ z-index: 1;
+ min-height: 100vh;
+ }
+ h1,
+ h2,
+ h3,
+ h4 {
+ font-family: "Fraunces", "GT Sectra", Georgia, serif;
+ font-weight: 500;
+ letter-spacing: -0.01em;
+ }
+ code,
+ pre,
+ .mono {
+ font-family: "JetBrains Mono", "Berkeley Mono", ui-monospace, monospace;
+ }
+ /* Selection: signal on quiet paper */
+ ::selection {
+ background: #7a2320;
+ color: #f5f0e6;
+ }
+}
+
+@layer utilities {
+ /* Thin ruled line — evokes paper stationery */
+ .rule {
+ border-bottom: 1px solid #dcd3be;
+ }
+ .rule-strong {
+ border-bottom: 1px solid #1a1714;
+ }
+ /* Envelope: subtle diagonal flap hint used on panels */
+ .paper-panel {
+ background: #f5f0e6;
+ border: 1px solid #dcd3be;
+ box-shadow:
+ 0 1px 0 rgba(26, 23, 20, 0.04),
+ 0 1px 3px rgba(26, 23, 20, 0.06);
+ }
+ .paper-panel-deep {
+ background: #ebe4d4;
+ border: 1px solid #dcd3be;
}
}
diff --git a/web/src/lib/fileTransfer.ts b/web/src/lib/fileTransfer.ts
index 12d90e2..211de0a 100644
--- a/web/src/lib/fileTransfer.ts
+++ b/web/src/lib/fileTransfer.ts
@@ -137,7 +137,7 @@ export function createFileReceiver(callbacks: FileReceiver) {
case "file-end": {
const file = receiving.get(msg.id);
if (file) {
- const blob = new Blob(file.chunks, { type: file.mime });
+ const blob = new Blob(file.chunks as BlobPart[], { type: file.mime });
callbacks.onComplete(msg.id, blob, file.name);
receiving.delete(msg.id);
}
diff --git a/web/src/lib/notifications.ts b/web/src/lib/notifications.ts
index 64ea120..0306c0e 100644
--- a/web/src/lib/notifications.ts
+++ b/web/src/lib/notifications.ts
@@ -33,7 +33,7 @@ export async function setupPushNotifications(
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
- applicationServerKey,
+ applicationServerKey: applicationServerKey as BufferSource,
});
}
diff --git a/web/src/lib/transferStore.ts b/web/src/lib/transferStore.ts
index d973c9a..ff4025b 100644
--- a/web/src/lib/transferStore.ts
+++ b/web/src/lib/transferStore.ts
@@ -128,7 +128,7 @@ export async function appendReceivedChunk(
const store = await tx(STORE_CHUNKS, "readwrite");
const existing = await reqToPromise(store.get(transferId));
const oldBlob: Blob = existing?.blob ?? new Blob();
- const newBlob = new Blob([oldBlob, chunk]);
+ const newBlob = new Blob([oldBlob, chunk as BlobPart]);
await reqToPromise(store.put({ transferId, blob: newBlob }));
return newBlob.size;
}
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx
index a4aad20..fffa00f 100644
--- a/web/src/pages/Home.tsx
+++ b/web/src/pages/Home.tsx
@@ -1,4 +1,5 @@
import { useCallback, useState } from "react";
+import { Link } from "react-router-dom";
import { useSignaling } from "../hooks/useSignaling";
import { useStore } from "../stores/useStore";
import { useProfileStore } from "../stores/useProfileStore";
@@ -22,8 +23,16 @@ export default function Home() {
}
function HomeConnected() {
- const { sendFiles, sendText, acceptTransfer, rejectTransfer, createPublicRoom, wakePeer, requestPairCode, resolvePairCode } =
- useSignaling();
+ const {
+ sendFiles,
+ sendText,
+ acceptTransfer,
+ rejectTransfer,
+ createPublicRoom,
+ wakePeer,
+ requestPairCode,
+ resolvePairCode,
+ } = useSignaling();
const peers = useStore((s) => s.peers);
const selectedPeerId = useStore((s) => s.selectedPeerId);
@@ -36,11 +45,10 @@ function HomeConnected() {
const { deviceName, avatar } = useProfileStore();
const [showProfileEdit, setShowProfileEdit] = useState(false);
- const [wakingDeviceId, setWakingDeviceId] = useState
(null);
+ const [, setWakingDeviceId] = useState(null);
const handlePeerSelect = useCallback(
(peerId: string) => {
- // Check if this is an offline peer
const peer = peers.find((p) => p.peerId === peerId);
if (peer && peer.online === false && peer.deviceId) {
setSelectedPeerId(peerId);
@@ -87,82 +95,113 @@ function HomeConnected() {
}, [incomingRequest, rejectTransfer]);
return (
-
-
- {/* Header */}
-
-
- AnyDrop
-
- Partage instantané, sans compte
-
- {/* Profile badge — tap to edit */}
-
+
+
+ {/* Masthead */}
+
- {/* Error banner */}
+ {/* Error */}
{error && (
-
-
{error}
-
@@ -189,3 +228,65 @@ function HomeConnected() {
);
}
+
+function DeviceChip({
+ name,
+ avatar,
+ onEdit,
+}: {
+ name: string;
+ avatar: string | null;
+ onEdit: () => void;
+}) {
+ return (
+
+ {avatar ? (
+
+ ) : (
+
+ ◦
+
+ )}
+ {name}
+
+ edit
+
+
+ );
+}
+
+function SectionLabel({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function SectionTitle({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
+function SectionLead({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/web/src/pages/JoinRoom.tsx b/web/src/pages/JoinRoom.tsx
index 4618b81..740abbb 100644
--- a/web/src/pages/JoinRoom.tsx
+++ b/web/src/pages/JoinRoom.tsx
@@ -75,93 +75,106 @@ function JoinRoomConnected({ code }: { code?: string }) {
}, [incomingRequest, rejectTransfer]);
return (
-