fix(mobile): render notes with marked (proper Markdown→HTML) + design CSS soigné

- Install marked package (UMD, hors-ligne)
- buildHtml: parse Markdown server-side avec marked, inject HTML statique
- CSS: typographie soignée, blockquotes brandés, code dark, tables propres
- Plus de CDN, fonctionne hors-ligne

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Antigravity
2026-05-29 16:43:24 +00:00
parent 7c8695cacf
commit 0ef12f7399
9 changed files with 131 additions and 29 deletions

View File

@@ -0,0 +1,13 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

View File

@@ -0,0 +1,3 @@
{
"devices": []
}

14
memento-mobile/.expo/types/router.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
/* eslint-disable */
import * as Router from 'expo-router';
export * from 'expo-router';
declare module 'expo-router' {
export namespace ExpoRouter {
export interface __routes<T extends string | object = string> {
hrefInputParams: { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(auth)'}/login` | `/login`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/home` | `/home`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/notebooks` | `/notebooks`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/profile` | `/profile`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/search` | `/search`; params?: Router.UnknownInputParams; } | { pathname: `/note/[id]`, params: Router.UnknownInputParams & { id: string | number; } } | { pathname: `/notebook/[id]`, params: Router.UnknownInputParams & { id: string | number; } };
hrefOutputParams: { pathname: Router.RelativePathString, params?: Router.UnknownOutputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownOutputParams } | { pathname: `/_sitemap`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(auth)'}/login` | `/login`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(tabs)'}/home` | `/home`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(tabs)'}/notebooks` | `/notebooks`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(tabs)'}/profile` | `/profile`; params?: Router.UnknownOutputParams; } | { pathname: `${'/(tabs)'}/search` | `/search`; params?: Router.UnknownOutputParams; } | { pathname: `/note/[id]`, params: Router.UnknownOutputParams & { id: string; } } | { pathname: `/notebook/[id]`, params: Router.UnknownOutputParams & { id: string; } };
href: Router.RelativePathString | Router.ExternalPathString | `/_sitemap${`?${string}` | `#${string}` | ''}` | `${'/(auth)'}/login${`?${string}` | `#${string}` | ''}` | `/login${`?${string}` | `#${string}` | ''}` | `${'/(tabs)'}/home${`?${string}` | `#${string}` | ''}` | `/home${`?${string}` | `#${string}` | ''}` | `${'/(tabs)'}/notebooks${`?${string}` | `#${string}` | ''}` | `/notebooks${`?${string}` | `#${string}` | ''}` | `${'/(tabs)'}/profile${`?${string}` | `#${string}` | ''}` | `/profile${`?${string}` | `#${string}` | ''}` | `${'/(tabs)'}/search${`?${string}` | `#${string}` | ''}` | `/search${`?${string}` | `#${string}` | ''}` | { pathname: Router.RelativePathString, params?: Router.UnknownInputParams } | { pathname: Router.ExternalPathString, params?: Router.UnknownInputParams } | { pathname: `/_sitemap`; params?: Router.UnknownInputParams; } | { pathname: `${'/(auth)'}/login` | `/login`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/home` | `/home`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/notebooks` | `/notebooks`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/profile` | `/profile`; params?: Router.UnknownInputParams; } | { pathname: `${'/(tabs)'}/search` | `/search`; params?: Router.UnknownInputParams; } | `/note/${Router.SingleRoutePart<T>}${`?${string}` | `#${string}` | ''}` | `/notebook/${Router.SingleRoutePart<T>}${`?${string}` | `#${string}` | ''}` | { pathname: `/note/[id]`, params: Router.UnknownInputParams & { id: string | number; } } | { pathname: `/notebook/[id]`, params: Router.UnknownInputParams & { id: string | number; } };
}
}
}

6
memento-mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli

View File

@@ -7,6 +7,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { useLocalSearchParams, useRouter } from 'expo-router' import { useLocalSearchParams, useRouter } from 'expo-router'
import { ArrowLeft, Share2 } from 'lucide-react-native' import { ArrowLeft, Share2 } from 'lucide-react-native'
import { WebView } from 'react-native-webview' import { WebView } from 'react-native-webview'
import { marked } from 'marked'
import { apiFetch } from '@/lib/api' import { apiFetch } from '@/lib/api'
import { ENDPOINTS } from '@/lib/config' import { ENDPOINTS } from '@/lib/config'
import { C } from '../_layout' import { C } from '../_layout'
@@ -20,30 +21,74 @@ interface Note {
} }
function buildHtml(content: string, title: string) { function buildHtml(content: string, title: string) {
// marked runs in Node/JS context — parse server-side, inject final HTML
marked.setOptions({ breaks: true, gfm: true })
const bodyHtml = marked.parse(content) as string
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<style> <style>
:root {
--brand: #A47148;
--ink: #1A1A18;
--paper: #FAFAF8;
--concrete: #8A8A82;
--border: #E8E6E0;
--code-bg: #f0ede8;
--dark-bg: #1e1e1c;
}
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, sans-serif; font-size: 16px; line-height: 1.7; body {
color: #1A1A18; background: #FAFAF8; padding: 0 16px 32px; } font-family: -apple-system, 'Helvetica Neue', sans-serif;
h1 { font-size: 22px; font-weight: 700; margin: 20px 0 12px; } font-size: 16px; line-height: 1.75; color: var(--ink);
h2 { font-size: 18px; font-weight: 700; margin: 16px 0 10px; } background: var(--paper); padding: 8px 20px 64px;
h3 { font-size: 16px; font-weight: 600; margin: 14px 0 8px; } word-break: break-word;
p { margin: 0 0 12px; } }
ul, ol { padding-left: 20px; margin: 0 0 12px; } .note-title {
li { margin-bottom: 4px; } font-size: 26px; font-weight: 800; letter-spacing: -0.5px;
blockquote { border-left: 3px solid #A47148; padding-left: 12px; color: #666; margin: 12px 0; } color: var(--ink); margin: 20px 0 4px; line-height: 1.25;
code { background: #f0ede8; padding: 2px 6px; border-radius: 4px; font-size: 13px; } }
pre { background: #f0ede8; padding: 12px; border-radius: 8px; overflow: auto; margin: 12px 0; } .note-meta { font-size: 12px; color: var(--concrete); margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
a { color: #A47148; } h1 { font-size: 22px; font-weight: 700; margin: 28px 0 10px; line-height: 1.3; }
img { max-width: 100%; border-radius: 8px; margin: 8px 0; } h2 { font-size: 19px; font-weight: 700; margin: 24px 0 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
h3 { font-size: 16px; font-weight: 600; margin: 20px 0 6px; }
h4, h5, h6 { font-size: 15px; font-weight: 600; margin: 14px 0 4px; color: var(--concrete); }
p { margin: 0 0 14px; }
ul, ol { padding-left: 24px; margin: 0 0 14px; }
li { margin-bottom: 6px; }
li::marker { color: var(--brand); }
li > ul, li > ol { margin-top: 4px; margin-bottom: 2px; }
input[type=checkbox] { accent-color: var(--brand); margin-right: 6px; }
blockquote {
border-left: 3px solid var(--brand); margin: 16px 0; padding: 10px 14px;
background: #f7f2ec; border-radius: 0 10px 10px 0; color: #5a5a52; font-style: italic;
}
blockquote p { margin: 0; }
code {
background: var(--code-bg); padding: 2px 7px; border-radius: 6px;
font-size: 13px; font-family: 'Menlo', 'Courier New', monospace; color: var(--brand);
}
pre { background: var(--dark-bg); padding: 16px; border-radius: 12px; overflow-x: auto; margin: 16px 0; }
pre code { background: none; padding: 0; color: #e8e6e0; font-size: 13px; line-height: 1.6; }
a { color: var(--brand); text-decoration: none; border-bottom: 1px solid #d4b896; }
img { max-width: 100%; border-radius: 12px; margin: 12px 0; display: block; }
hr { border: none; border-top: 1px solid var(--border); margin: 24px 0; }
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; }
th { background: var(--code-bg); font-weight: 700; padding: 10px 12px; text-align: left; border-bottom: 2px solid var(--border); }
td { padding: 9px 12px; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
strong { font-weight: 700; } strong { font-weight: 700; }
em { font-style: italic; } em { font-style: italic; }
del { text-decoration: line-through; color: var(--concrete); }
</style> </style>
</head> </head>
<body>${content}</body> <body>
<div class="note-title">${title || 'Sans titre'}</div>
<div class="note-meta">Note Momento</div>
<div id="content">${bodyHtml}</div>
</body>
</html>` </html>`
} }

View File

@@ -1,6 +1,6 @@
// API base URL — change for dev/prod // API base URL — change for dev/prod
export const API_URL = __DEV__ export const API_URL = __DEV__
? 'http://192.168.1.190:3000' // local network dev server ? 'http://192.168.1.83:3000' // serveur de dev local
: 'https://memento-note.com' : 'https://memento-note.com'
export const ENDPOINTS = { export const ENDPOINTS = {

View File

@@ -18,6 +18,7 @@
"expo-splash-screen": "~31.0.13", "expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9", "expo-status-bar": "~3.0.9",
"lucide-react-native": "^0.477.0", "lucide-react-native": "^0.477.0",
"marked": "^18.0.4",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
@@ -6225,6 +6226,18 @@
"tmpl": "1.0.5" "tmpl": "1.0.5"
} }
}, },
"node_modules/marked": {
"version": "18.0.4",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.4.tgz",
"integrity": "sha512-c/BTaKzg0G6ezQx97DAkYU7k0HM6ys0FqYeKBL6hlBByZwy+ycA1+f0vDdjMHKKeEjdgkx0GOv9Il6D+85cOqA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/marky": { "node_modules/marky": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",

View File

@@ -10,23 +10,24 @@
"build:ios": "eas build --platform ios" "build:ios": "eas build --platform ios"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "2.2.0",
"expo": "~54.0.35", "expo": "~54.0.35",
"expo-router": "~6.0.24",
"expo-status-bar": "~3.0.9",
"expo-secure-store": "~15.0.8",
"expo-font": "~14.0.12",
"expo-splash-screen": "~31.0.13",
"expo-linking": "~8.0.12",
"expo-constants": "~18.0.13", "expo-constants": "~18.0.13",
"expo-font": "~14.0.12",
"expo-linking": "~8.0.12",
"expo-router": "~6.0.24",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"lucide-react-native": "^0.477.0",
"marked": "^18.0.4",
"react": "19.1.0", "react": "19.1.0",
"react-native": "0.81.5", "react-native": "0.81.5",
"react-native-safe-area-context": "~5.6.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.12.0",
"react-native-webview": "13.15.0", "react-native-webview": "13.15.0",
"@react-native-async-storage/async-storage": "2.2.0", "zustand": "^5.0.2"
"zustand": "^5.0.2",
"lucide-react-native": "^0.477.0",
"react-native-svg": "15.12.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
@@ -34,4 +35,3 @@
"typescript": "~5.9.2" "typescript": "~5.9.2"
} }
} }

View File

@@ -3,8 +3,16 @@
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"paths": { "paths": {
"@/*": ["./*"] "@/*": [
"./*"
]
} }
}, },
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.d.ts", "expo-env.d.ts"] "include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.d.ts",
"expo-env.d.ts",
".expo/types/**/*.ts"
]
} }