chore(init): initial commit

This commit is contained in:
devcodetools committed 2026-09-07 15:16:13 +02:00
commit aade4fdc1d
23 files changed
+2078

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 devcodetools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+100
View File
@@ -0,0 +1,100 @@
# MD Editor
Éditeur markdown en temps réel avec prévisualisation instantanée, coloration syntaxique, persistance locale et thème clair/sombre.
## Fonctionnalités
- **Split view** : zone de texte à gauche, prévisualisation à droite
- **Rendu markdown temps réel** à chaque frappe
- **Coloration syntaxique** des blocs de code (highlight.js), fond sombre type VSCode
- **HTML sanitisé** avant injection (DOMPurify)
- **Sauvegarde automatique** dans le localStorage (restaurée au rechargement)
- **Bouton copier** le markdown ou le HTML (feedback visuel vert)
- **Bouton télécharger** le contenu en `download.md`
- **Bouton effacer** tout le contenu
- **Thème clair/sombre** persistable, transition douce 350ms
- **Responsive** jusqu'au mobile (panneaux empilés sous 768px)
## Stack technique
| Couche | Technologie |
|--------|-------------|
| Framework | React 19 + Vite 8 |
| Langage | TypeScript ~6.0 (ESM) |
| Styling | Tailwind CSS 4 (plugin `@tailwindcss/vite`) |
| Markdown | marked 18 + marked-highlight 2 |
| Coloration | highlight.js 11 |
| Sanitisation | DOMPurify 3 |
| Linter | oxlint |
| Package manager | pnpm |
## Prérequis
- Node.js ≥ 20
- pnpm ≥ 9 (ou utiliser npx si absent)
## Installation
```bash
# 1. Cloner et entrer dans le projet
git clone <url-du-repo> md-poc
cd md-poc
# 2. Installer les dépendances
pnpm install
# 3. Lancer le serveur de développement
pnpm dev
```
Ouvrir http://localhost:5173.
## Scripts
| Commande | Description |
|----------|-------------|
| `pnpm dev` | Serveur de développement (HMR) |
| `pnpm build` | Typecheck + build de production dans `dist/` |
| `pnpm lint` | Lint oxlint |
| `pnpm preview` | Prévisualise le build de production |
## Structure du projet
```
src/
App.tsx # Orchestrateur : état markdown, parsing, handlers
main.tsx # Entry point React
index.css # Tailwind + variables de thème + styles markdown/hljs
types/types.ts # Types et interfaces partagés
hooks/
useTheme.ts # Thème + persistance + data-theme
components/
Header.tsx # Barre supérieure (titre, actions, toggle thème)
CopyButton.tsx # Bouton copie avec feedback "Copié !"
EditorInput.tsx # Textarea markdown
Preview.tsx # Prévisualisation HTML sanitisé
ThemeToggle.tsx # Bouton lune/soleil
utils/
download.ts # Téléchargement download.md
```
## Détails techniques
### Rendu markdown
- `marked.use(markedHighlight(...))` configuré une fois en haut de `App.tsx`
- Bloc de code avec langage inconnu → fallback `plaintext`
- Le HTML généré passe **toujours** par `DOMPurify.sanitize` avant injection
- Mémorisation du parse avec `useMemo` (pas de recalcul au changement de thème)
### Thème
- Variables CSS (+ surcharge `[data-theme="dark"]`), pas de classes `dark:`
- `useTheme` persiste le choix sous la clé `md-theme` et pose `data-theme` sur `<html>`
- Les blocs de code restent sombres quel que soit le thème
- Transition 350ms avec support `prefers-reduced-motion`
### Persistance
- Contenu markdown : clé `md-content` (écrite à chaque frappe)
## Licence
MIT — voir le fichier [LICENSE](./LICENSE).
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Éditeur markdown en temps réel avec prévisualisation instantanée" />
<title>MD Editor</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
{
"name": "md-poc",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "oxlint",
"preview": "vite preview"
},
"dependencies": {
"dompurify": "^3.4.15",
"highlight.js": "^11.12.0",
"marked": "^18.0.11",
"marked-highlight": "^2.2.4",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.3",
"@types/dompurify": "^3.0.5",
"@types/highlight.js": "^9.12.4",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.1.0",
"oxlint": "^1.79.0",
"tailwindcss": "^4.3.3",
"typescript": "~6.0.2",
"vite": "^8.2.2"
}
}
+1193
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="12" fill="#0d1117"/>
<text x="32" y="42" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="26" font-weight="700" fill="#ff7b72" text-anchor="middle">MD</text>
</svg>

After

Width:  |  Height:  |  Size: 295 B

+70
View File
@@ -0,0 +1,70 @@
import { useMemo, useState } from 'react'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
import { marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
import Header from './components/Header'
import EditorInput from './components/EditorInput'
import Preview from './components/Preview'
import { useTheme } from './hooks/useTheme'
import { downloadMarkdown } from './utils/download'
import './index.css'
const STORAGE_KEY = 'md-content'
marked.use(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
return hljs.highlight(code, { language }).value
},
}),
)
function App() {
const { theme, toggleTheme } = useTheme()
const [markdown, setMarkdown] = useState<string>(
() => localStorage.getItem(STORAGE_KEY) ?? '',
)
const html = useMemo(() => {
const raw = marked.parse(markdown) as string
return DOMPurify.sanitize(raw)
}, [markdown])
function handleChange(value: string) {
setMarkdown(value)
localStorage.setItem(STORAGE_KEY, value)
}
function copy(text: string) {
navigator.clipboard.writeText(text)
}
function handleClear() {
setMarkdown('')
localStorage.removeItem(STORAGE_KEY)
}
return (
<div className="app-root h-screen flex flex-col" style={{ backgroundColor: 'var(--bg)', color: 'var(--fg)' }}>
<Header
theme={theme}
onToggleTheme={toggleTheme}
onCopyMarkdown={() => copy(markdown)}
onCopyHtml={() => copy(html)}
onClear={handleClear}
onDownload={() => downloadMarkdown(markdown)}
downloadDisabled={markdown.length === 0}
/>
<main className="flex-1 flex min-h-0 flex-col md:flex-row">
<EditorInput value={markdown} onChange={handleChange} />
<Preview html={html} />
</main>
</div>
)
}
export default App
+27
View File
@@ -0,0 +1,27 @@
import type { CopyButtonProps } from '../types/types';
function CopyButton({
label,
copiedLabel,
isCopied,
onClick,
}: CopyButtonProps) {
const btn =
"toolbar-btn relative px-3 py-1.5 text-sm rounded border transition cursor-pointer";
return (
<button type="button" className={btn} onClick={onClick}>
<span className="invisible">{label}</span>
<span
className={`absolute inset-0 flex items-center justify-center transition-colors ${
isCopied ? "toolbar-btn-green" : ""
}`}
style={isCopied ? undefined : { color: "var(--fg)" }}
>
{isCopied ? copiedLabel : label}
</span>
</button>
);
}
export default CopyButton;
+21
View File
@@ -0,0 +1,21 @@
import type { EditorInputProps } from '../types/types'
function EditorInput({ value, onChange }: EditorInputProps) {
return (
<textarea
className="flex-1 md:flex-1 md:w-1/2 w-full h-[45vh] md:h-auto p-3 md:p-4 font-mono text-sm outline-none border-b md:border-b-0 md:border-r"
style={{
borderColor: 'var(--border)',
backgroundColor: 'var(--bg)',
color: 'var(--fg)',
caretColor: 'var(--fg)',
}}
placeholder="# Titre&#10;Saisissez votre markdown ici..."
value={value}
onChange={(e) => onChange(e.target.value)}
spellCheck={false}
/>
)
}
export default EditorInput
+100
View File
@@ -0,0 +1,100 @@
import { useEffect, useRef, useState } from "react";
import type { HeaderProps } from '../types/types';
import ThemeToggle from "./ThemeToggle";
import CopyButton from "./CopyButton";
function Header({
theme,
onToggleTheme,
onCopyMarkdown,
onCopyHtml,
onClear,
onDownload,
downloadDisabled,
}: HeaderProps) {
const [copied, setCopied] = useState<"md" | "html" | null>(null);
const timeoutRef = useRef<number | null>(null);
useEffect(
() => () => {
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
},
[],
);
function handleCopy(target: "md" | "html", action: () => void) {
action();
setCopied(target);
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => setCopied(null), 1500);
}
return (
<header
className="flex items-center justify-between gap-2 flex-wrap px-3 md:px-4 py-2 border-b"
style={{
borderColor: "var(--border)",
backgroundColor: "var(--surface)",
}}
>
<h1
className="text-base md:text-lg font-semibold"
style={{ color: "var(--fg-strong)" }}
>
MD Editor
</h1>
<div className="flex gap-2 items-center flex-wrap">
<CopyButton
label="Copier MD"
copiedLabel="Copié !"
isCopied={copied === "md"}
onClick={() => handleCopy("md", onCopyMarkdown)}
/>
<CopyButton
label="Copier HTML"
copiedLabel="Copié !"
isCopied={copied === "html"}
onClick={() => handleCopy("html", onCopyHtml)}
/>
<button
type="button"
className="toolbar-btn toolbar-btn-danger px-3 py-1.5 text-sm rounded border transition cursor-pointer"
onClick={onClear}
>
Effacer tout
</button>
<button
type="button"
title="Télécharger download.md"
aria-label="Télécharger download.md"
disabled={downloadDisabled}
onClick={onDownload}
className="toolbar-btn flex items-center justify-center w-8 h-8 rounded border transition cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
style={{ color: 'var(--fg)' }}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</button>
<div
className="hidden md:block w-px h-6"
style={{ backgroundColor: "var(--border)" }}
/>
<ThemeToggle theme={theme} onToggle={onToggleTheme} />
</div>
</header>
);
}
export default Header;
+18
View File
@@ -0,0 +1,18 @@
import type { PreviewProps } from '../types/types'
function Preview({ html }: PreviewProps) {
return (
<div
className="flex-1 min-h-0 overflow-auto p-3 md:p-4"
style={{ backgroundColor: 'var(--bg)', color: 'var(--fg)' }}
>
{html ? (
<div className="markdown-body" dangerouslySetInnerHTML={{ __html: html }} />
) : (
<p style={{ color: 'var(--fg-muted)' }}>Aperçu</p>
)}
</div>
)
}
export default Preview
+47
View File
@@ -0,0 +1,47 @@
import type { ThemeToggleProps } from '../types/types'
function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
const isDark = theme === 'dark'
return (
<button
type="button"
onClick={onToggle}
title={isDark ? 'Passer en thème clair' : 'Passer en thème sombre'}
aria-label={isDark ? 'Passer en thème clair' : 'Passer en thème sombre'}
className="toolbar-btn flex items-center justify-center w-8 h-8 rounded border transition cursor-pointer"
style={{ color: 'var(--fg)' }}
>
{isDark ? (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
</svg>
) : (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
)}
</button>
)
}
export default ThemeToggle
+21
View File
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react'
import type { Theme } from '../types/types'
const THEME_KEY = 'md-theme'
export function useTheme() {
const [theme, setTheme] = useState<Theme>(() => {
return localStorage.getItem(THEME_KEY) === 'dark' ? 'dark' : 'light'
})
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem(THEME_KEY, theme)
}, [theme])
function toggleTheme() {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}
return { theme, toggleTheme }
}
+261
View File
@@ -0,0 +1,261 @@
@import "tailwindcss";
:root {
color-scheme: light;
--bg: #ffffff;
--fg: #1f2937;
--fg-strong: #111827;
--fg-muted: #4b5563;
--border: #e5e7eb;
--border-soft: #e5e7eb;
--surface: #ffffff;
--surface-muted: #f9fafb;
--surface-hover: #f3f4f6;
--code-inline-bg: #f3f4f6;
--syn-bg: #0d1117;
--syn-fg: #c9d1d9;
--syn-comment: #8b949e;
--syn-keyword: #ff7b72;
--syn-string: #a5d6ff;
--syn-number: #79c0ff;
--syn-title: #d2a8ff;
--syn-built: #ffa657;
--syn-attribute: #79c0ff;
--syn-punctuation: #c9d1d9;
}
[data-theme="dark"] {
color-scheme: dark;
--bg: #16171d;
--fg: #d1d5db;
--fg-strong: #f3f4f6;
--fg-muted: #9ca3af;
--border: #2e303a;
--border-soft: #2e303a;
--surface: #1f2028;
--surface-muted: #212329;
--surface-hover: #2a2c35;
--code-inline-bg: #2a2c35;
}
.app-root,
.app-root * {
transition:
background-color 350ms ease,
color 350ms ease,
border-color 350ms ease,
caret-color 350ms ease;
}
@media (prefers-reduced-motion: reduce) {
.app-root,
.app-root * {
transition: none;
}
}
.toolbar-btn {
border-color: var(--border);
color: var(--fg);
}
.toolbar-btn:hover {
background-color: var(--surface-hover);
}
.toolbar-btn-danger {
border-color: #fca5a5;
color: #f87171;
}
.toolbar-btn-danger:hover {
background-color: rgba(248, 113, 113, 0.1);
}
.toolbar-btn-green {
color: #22c55e;
}
.markdown-body pre code.hljs {
background: var(--syn-bg);
color: var(--syn-fg);
}
.hljs-comment,
.hljs-quote {
color: var(--syn-comment);
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-doctag {
color: var(--syn-keyword);
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-meta .hljs-string {
color: var(--syn-string);
}
.hljs-number,
.hljs-symbol {
color: var(--syn-number);
}
.hljs-title,
.hljs-section,
.hljs-selector-id,
.hljs-title.class_,
.hljs-title.function_ {
color: var(--syn-title);
}
.hljs-built_in,
.hljs-type,
.hljs-attr,
.hljs-selector-attr,
.hljs-selector-class {
color: var(--syn-built);
}
.hljs-attribute,
.hljs-variable,
.hljs-template-variable {
color: var(--syn-attribute);
}
.hljs-punctuation {
color: var(--syn-punctuation);
}
.markdown-body {
font-size: 0.95rem;
line-height: 1.7;
word-wrap: break-word;
color: var(--fg);
}
.markdown-body > :first-child {
margin-top: 0;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
font-weight: 700;
margin: 1.4em 0 0.6em;
line-height: 1.3;
color: var(--fg-strong);
}
.markdown-body h1 {
font-size: 1.75em;
padding-bottom: 0.3em;
border-bottom: 1px solid var(--border);
}
.markdown-body h2 {
font-size: 1.45em;
padding-bottom: 0.3em;
border-bottom: 1px solid var(--border);
}
.markdown-body h3 {
font-size: 1.2em;
}
.markdown-body h4 {
font-size: 1.05em;
}
.markdown-body h5 {
font-size: 0.95em;
}
.markdown-body h6 {
font-size: 0.9em;
color: var(--fg-muted);
}
.markdown-body p,
.markdown-body ul,
.markdown-body ol,
.markdown-body blockquote,
.markdown-body pre {
margin: 0.9em 0;
}
.markdown-body ul {
list-style: disc;
padding-left: 1.6em;
}
.markdown-body ol {
list-style: decimal;
padding-left: 1.6em;
}
.markdown-body li {
margin: 0.25em 0;
}
.markdown-body li > ul,
.markdown-body li > ol {
margin: 0.25em 0;
}
.markdown-body a {
color: #2563eb;
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body code {
background: var(--code-inline-bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.15em 0.35em;
font-size: 0.88em;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.markdown-body pre {
background: var(--syn-bg);
border-radius: 8px;
padding: 1em 1.2em;
overflow: auto;
border: 1px solid #21262d;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.markdown-body pre code {
background: transparent;
border: none;
padding: 0;
font-size: 0.85em;
line-height: 1.6;
}
.markdown-body blockquote {
border-left: 4px solid var(--border);
padding: 0.25em 1.2em;
color: var(--fg-muted);
background: var(--surface-muted);
border-radius: 0 6px 6px 0;
}
.markdown-body table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 0.9em;
display: block;
overflow-x: auto;
}
.markdown-body th,
.markdown-body td {
border: 1px solid var(--border);
padding: 0.5em 0.8em;
text-align: left;
}
.markdown-body th {
background: var(--surface-muted);
font-weight: 600;
}
.markdown-body tr:nth-child(even) {
background: var(--surface-muted);
}
.markdown-body hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.5em 0;
}
.markdown-body img {
max-width: 100%;
border-radius: 6px;
}
.markdown-body strong {
color: var(--fg-strong);
font-weight: 700;
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+32
View File
@@ -0,0 +1,32 @@
export type Theme = 'light' | 'dark'
export interface CopyButtonProps {
label: string
copiedLabel: string
isCopied: boolean
onClick: () => void
}
export interface HeaderProps {
theme: Theme
onToggleTheme: () => void
onCopyMarkdown: () => void
onCopyHtml: () => void
onClear: () => void
onDownload: () => void
downloadDisabled: boolean
}
export interface EditorInputProps {
value: string
onChange: (value: string) => void
}
export interface PreviewProps {
html: string
}
export interface ThemeToggleProps {
theme: Theme
onToggle: () => void
}
+11
View File
@@ -0,0 +1,11 @@
const FILENAME = 'download.md'
export function downloadMarkdown(content: string) {
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = FILENAME
a.click()
URL.revokeObjectURL(url)
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"allowArbitraryExtensions": true,
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"module": "nodenext",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+7
View File
@@ -0,0 +1,7 @@
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})