hamzus-ui 0.0.242 → 0.0.244

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -18,6 +18,8 @@ export { default as Form } from "./src/components/hamzus-ui/Form/Form.svelte"
18
18
  export { default as Input } from "./src/components/hamzus-ui/Input/Input.svelte"
19
19
  export { default as InputNumber } from "./src/components/hamzus-ui/InputNumber/InputNumber.svelte"
20
20
  export { default as InputSelector } from "./src/components/hamzus-ui/InputSelector/InputSelector.svelte"
21
+ export { default as Readme } from "./src/components/hamzus-ui/Readme/Readme.svelte"
22
+ export { default as ReadmePreview } from "./src/components/hamzus-ui/ReadmePreview/ReadmePreview.svelte"
21
23
  export { default as TextArea } from "./src/components/hamzus-ui/TextArea/TextArea.svelte"
22
24
  export { default as Checkbox } from "./src/components/hamzus-ui/Checkboxes/Checkbox/Checkbox.svelte"
23
25
  export { default as Switch } from "./src/components/hamzus-ui/Switch/Switch.svelte"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hamzus-ui",
3
- "version": "0.0.242",
3
+ "version": "0.0.244",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "svelte": "index.js",
@@ -281,7 +281,7 @@
281
281
  <svelte:component this={component} selected={isSelected()} {...result} onClick={handleSelect}
282
282
  ></svelte:component>
283
283
  {/each}
284
- {#if results.length === 0 && offset === 0}
284
+ {#if results.length === 0}
285
285
  <slot name="nothing"/>
286
286
  {/if}
287
287
  {#if results.length < totalOfRows}
@@ -0,0 +1,623 @@
1
+ <script>
2
+ import Button from '..//Button/Button.svelte';
3
+ import IconButton from '..//IconButton/IconButton.svelte';
4
+
5
+ // ---------- État ----------
6
+ export let initialValue = `# Mon Projet
7
+
8
+ > Une courte description qui donne envie de scroller.
9
+
10
+ ## Installation
11
+
12
+ \`\`\`bash
13
+ npm install mon-projet
14
+ \`\`\`
15
+
16
+ ## Utilisation
17
+
18
+ \`\`\`js
19
+ import { run } from 'mon-projet'
20
+ run()
21
+ \`\`\`
22
+
23
+ - [x] Setup du repo
24
+ - [ ] Écrire les tests
25
+ - [ ] Publier sur npm
26
+
27
+ Voir la [documentation](https://example.com) pour plus de détails.
28
+ `;
29
+
30
+ let source = initialValue;
31
+ let mode = 'split'; // 'edit' | 'split' | 'preview'
32
+ let filename = 'README.md';
33
+ let dirty = false;
34
+ let textareaEl;
35
+ let copied = false;
36
+
37
+ $: wordCount = (source.match(/\S+/g) || []).length;
38
+ $: charCount = source.length;
39
+ $: readMinutes = Math.max(1, Math.round(wordCount / 200));
40
+ $: html = renderMarkdown(source);
41
+
42
+ function onInput(e) {
43
+ source = e.target.value;
44
+ dirty = true;
45
+ }
46
+
47
+ // ---------- Petit moteur markdown (sans dépendance) ----------
48
+ function escapeHtml(str) {
49
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
50
+ }
51
+
52
+ function renderInline(text) {
53
+ let t = escapeHtml(text);
54
+ t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
55
+ t = t.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" />');
56
+ t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
57
+ t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
58
+ t = t.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1<em>$2</em>');
59
+ t = t.replace(/~~([^~]+)~~/g, '<del>$1</del>');
60
+ return t;
61
+ }
62
+
63
+ function renderMarkdown(md) {
64
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
65
+ let out = [];
66
+ let i = 0;
67
+ let inCodeBlock = false;
68
+ let codeLang = '';
69
+ let codeBuf = [];
70
+ let listBuf = [];
71
+ let listType = null;
72
+
73
+ function flushList() {
74
+ if (listBuf.length) {
75
+ const tag = listType === 'ol' ? 'ol' : 'ul';
76
+ out.push(`<${tag}>${listBuf.join('')}</${tag}>`);
77
+ listBuf = [];
78
+ listType = null;
79
+ }
80
+ }
81
+
82
+ while (i < lines.length) {
83
+ const line = lines[i];
84
+ const fence = line.match(/^```(.*)$/);
85
+
86
+ if (fence) {
87
+ if (!inCodeBlock) {
88
+ inCodeBlock = true;
89
+ codeLang = fence[1].trim();
90
+ codeBuf = [];
91
+ } else {
92
+ inCodeBlock = false;
93
+ out.push(
94
+ `<pre class="code-block" data-lang="${escapeHtml(codeLang || 'text')}"><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`
95
+ );
96
+ }
97
+ i++;
98
+ continue;
99
+ }
100
+ if (inCodeBlock) {
101
+ codeBuf.push(line);
102
+ i++;
103
+ continue;
104
+ }
105
+
106
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
107
+ if (heading) {
108
+ flushList();
109
+ const level = heading[1].length;
110
+ out.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
111
+ i++;
112
+ continue;
113
+ }
114
+
115
+ const quote = line.match(/^>\s?(.*)$/);
116
+ if (quote) {
117
+ flushList();
118
+ out.push(`<blockquote class="p">${renderInline(quote[1])}</blockquote>`);
119
+ i++;
120
+ continue;
121
+ }
122
+
123
+ const hr = line.match(/^(-{3,}|\*{3,})\s*$/);
124
+ if (hr) {
125
+ flushList();
126
+ out.push('<hr />');
127
+ i++;
128
+ continue;
129
+ }
130
+
131
+ const taskItem = line.match(/^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/);
132
+ if (taskItem) {
133
+ if (listType !== 'ul') {
134
+ flushList();
135
+ listType = 'ul';
136
+ }
137
+ const checked = taskItem[1].toLowerCase() === 'x';
138
+ listBuf.push(
139
+ `<li class="task"><input type="checkbox" disabled ${checked ? 'checked' : ''} />${renderInline(taskItem[2])}</li>`
140
+ );
141
+ i++;
142
+ continue;
143
+ }
144
+
145
+ const ulItem = line.match(/^\s*[-*]\s+(.*)$/);
146
+ if (ulItem) {
147
+ if (listType !== 'ul') {
148
+ flushList();
149
+ listType = 'ul';
150
+ }
151
+ listBuf.push(`<li>${renderInline(ulItem[1])}</li>`);
152
+ i++;
153
+ continue;
154
+ }
155
+
156
+ const olItem = line.match(/^\s*\d+\.\s+(.*)$/);
157
+ if (olItem) {
158
+ if (listType !== 'ol') {
159
+ flushList();
160
+ listType = 'ol';
161
+ }
162
+ listBuf.push(`<li>${renderInline(olItem[1])}</li>`);
163
+ i++;
164
+ continue;
165
+ }
166
+
167
+ if (line.trim() === '') {
168
+ flushList();
169
+ i++;
170
+ continue;
171
+ }
172
+
173
+ flushList();
174
+ out.push(`<p>${renderInline(line)}</p>`);
175
+ i++;
176
+ }
177
+ flushList();
178
+ if (inCodeBlock && codeBuf.length) {
179
+ out.push(`<pre class="code-block"><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
180
+ }
181
+ return out.join('\n');
182
+ }
183
+
184
+ // ---------- Actions de la barre d'outils ----------
185
+ function wrapSelection(before, after = before, placeholder = '') {
186
+ if (!textareaEl) return;
187
+ const start = textareaEl.selectionStart;
188
+ const end = textareaEl.selectionEnd;
189
+ const selected = source.slice(start, end) || placeholder;
190
+ const next = source.slice(0, start) + before + selected + after + source.slice(end);
191
+ source = next;
192
+ dirty = true;
193
+ requestAnimationFrame(() => {
194
+ textareaEl.focus();
195
+ const cursor = start + before.length + selected.length + after.length;
196
+ textareaEl.setSelectionRange(cursor, cursor);
197
+ });
198
+ }
199
+
200
+ function insertLinePrefix(prefix) {
201
+ if (!textareaEl) return;
202
+ const start = textareaEl.selectionStart;
203
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
204
+ source = source.slice(0, lineStart) + prefix + source.slice(lineStart);
205
+ dirty = true;
206
+ requestAnimationFrame(() => textareaEl.focus());
207
+ }
208
+
209
+ const toolbarActions = [
210
+ { label: 'H2', title: 'Titre', run: () => insertLinePrefix('## ') },
211
+ { label: 'B', title: 'Gras', run: () => wrapSelection('**', '**', 'texte en gras') },
212
+ { label: 'I', title: 'Italique', run: () => wrapSelection('*', '*', 'texte en italique') },
213
+ { label: '</>', title: 'Code inline', run: () => wrapSelection('`', '`', 'code') },
214
+ { label: '{ }', title: 'Bloc de code', run: () => wrapSelection('```\n', '\n```', 'code') },
215
+ { label: '🔗', title: 'Lien', run: () => wrapSelection('[', '](https://)', 'texte du lien') },
216
+ { label: '•', title: 'Liste', run: () => insertLinePrefix('- ') },
217
+ { label: '☑', title: 'Tâche', run: () => insertLinePrefix('- [ ] ') },
218
+ { label: '❝', title: 'Citation', run: () => insertLinePrefix('> ') }
219
+ ];
220
+
221
+ async function copyMarkdown() {
222
+ try {
223
+ await navigator.clipboard.writeText(source);
224
+ copied = true;
225
+ setTimeout(() => (copied = false), 1600);
226
+ } catch (e) {
227
+ // clipboard indisponible, on ignore silencieusement
228
+ }
229
+ }
230
+
231
+ function downloadFile() {
232
+ const blob = new Blob([source], { type: 'text/markdown;charset=utf-8' });
233
+ const url = URL.createObjectURL(blob);
234
+ const a = document.createElement('a');
235
+ a.href = url;
236
+ a.download = filename || 'README.md';
237
+ a.click();
238
+ URL.revokeObjectURL(url);
239
+ dirty = false;
240
+ }
241
+ </script>
242
+
243
+ <div class="readme-editor">
244
+ <div class="toolbar">
245
+ {#each toolbarActions as action}
246
+ <IconButton variant="ghost" onClick={action.run}>
247
+ <h5>{action.label}</h5>
248
+ </IconButton>
249
+ {/each}
250
+ <span class="spacer"></span>
251
+
252
+ <div class="view-toggle" role="tablist" aria-label="Mode d'affichage">
253
+ <Button variant={mode === 'edit' ? 'secondary' : 'ghost'} onClick={() => (mode = 'edit')}><h5>Édition</h5></Button>
254
+ <Button variant={mode === 'split' ? 'secondary' : 'ghost'} onClick={() => (mode = 'split')}><h5>Scindé</h5></Button>
255
+ <Button variant={mode === 'preview' ? 'secondary' : 'ghost'} onClick={() => (mode = 'preview')}><h5>Aperçu</h5></Button>
256
+ </div>
257
+ </div>
258
+
259
+ <div class="panes" class:single={mode !== 'split'}>
260
+ {#if mode !== 'preview'}
261
+ <textarea
262
+ bind:this={textareaEl}
263
+ class="editor-pane"
264
+ spellcheck="false"
265
+ value={source}
266
+ on:input={onInput}
267
+ placeholder="Écris ton README en Markdown..."
268
+ ></textarea>
269
+ {/if}
270
+
271
+ {#if mode !== 'edit'}
272
+ <div class="preview-pane">
273
+ <div class="markdown-body">
274
+ {@html html}
275
+ </div>
276
+ </div>
277
+ {/if}
278
+ </div>
279
+
280
+ <div class="status-bar">
281
+ <span>{wordCount} mots</span>
282
+ <span class="sep">·</span>
283
+ <span>{charCount} caractères</span>
284
+ <span class="sep">·</span>
285
+ <span>~{readMinutes} min de lecture</span>
286
+ <span class="sep">·</span>
287
+ <span class:dirty-text={dirty}>{dirty ? 'non enregistré' : 'à jour'}</span>
288
+ </div>
289
+ </div>
290
+
291
+ <style>
292
+ :root {
293
+ --bg: var(--bg-1);
294
+ --panel: var(--bg-blur);
295
+ --panel-alt: var(--bg-blur);
296
+ --border: var(--stroke);
297
+ --text: var(--font-1);
298
+ --muted: var(--font-2);
299
+ --amber: var(--orange);
300
+ --teal: var(--blur);
301
+ --code-bg: var(--bg-2);
302
+ --danger: var(--orange);
303
+ }
304
+
305
+ .readme-editor {
306
+ display: flex;
307
+ flex-direction: column;
308
+ height: 640px;
309
+ max-height: 90vh;
310
+ background: var(--bg);
311
+ color: var(--text);
312
+ border: 1px solid var(--border);
313
+ border-radius: 10px;
314
+ overflow: hidden;
315
+ }
316
+
317
+
318
+
319
+ .view-toggle {
320
+ display: flex;
321
+ gap: 2px;
322
+ background: var(--bg-1);
323
+ border: 1px solid var(--border);
324
+ border-radius: var(--radius-xl);
325
+ padding: 2px;
326
+ }
327
+
328
+
329
+
330
+ .toolbar {
331
+ display: flex;
332
+ align-items: center;
333
+ gap: 4px;
334
+ padding: 6px 10px;
335
+ background: var(--panel-alt);
336
+ border-bottom: 1px solid var(--border);
337
+ flex-shrink: 0;
338
+ overflow-x: auto;
339
+ }
340
+
341
+ .tool-btn {
342
+ border: 1px solid transparent;
343
+ background: transparent;
344
+ color: var(--muted);
345
+ font-size: 12px;
346
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
347
+ padding: 5px 9px;
348
+ border-radius: 6px;
349
+ cursor: pointer;
350
+ white-space: nowrap;
351
+ transition:
352
+ background 0.15s,
353
+ color 0.15s,
354
+ border-color 0.15s;
355
+ }
356
+
357
+ .tool-btn:hover {
358
+ background: var(--panel);
359
+ color: var(--text);
360
+ border-color: var(--border);
361
+ }
362
+
363
+ .tool-btn:focus-visible {
364
+ outline: 2px solid var(--teal);
365
+ outline-offset: 1px;
366
+ }
367
+
368
+
369
+
370
+ .spacer {
371
+ flex: 1;
372
+ }
373
+
374
+ .panes {
375
+ display: grid;
376
+ grid-template-columns: 1fr 1fr;
377
+ flex: 1;
378
+ min-height: 0;
379
+ }
380
+
381
+ .panes.single {
382
+ grid-template-columns: 1fr;
383
+ }
384
+
385
+ .editor-pane {
386
+ resize: none;
387
+ border: none;
388
+ outline: none;
389
+ background: var(--bg);
390
+ color: var(--text);
391
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
392
+ font-size: 13.5px;
393
+ line-height: 1.65;
394
+ padding: 18px 20px;
395
+ border-right: 1px solid var(--border);
396
+ }
397
+
398
+ .panes.single .editor-pane {
399
+ border-right: none;
400
+ }
401
+
402
+ .editor-pane::placeholder {
403
+ color: var(--font-2);
404
+ }
405
+
406
+ .preview-pane {
407
+ overflow-y: auto;
408
+ padding: 18px 26px;
409
+ background: var(--panel);
410
+ }
411
+
412
+ .markdown-body {
413
+ max-width: 640px;
414
+ font-size: 14.5px;
415
+ line-height: 1.7;
416
+ color: var(--text);
417
+ }
418
+
419
+ .markdown-body :global(h1),
420
+ .markdown-body :global(h2),
421
+ .markdown-body :global(h3) {
422
+ font-family: 'Inter', sans-serif;
423
+ font-weight: 700;
424
+ color: var(--text);
425
+ margin: 1.1em 0 0.5em;
426
+ line-height: 1.3;
427
+ }
428
+
429
+ .markdown-body :global(h1) {
430
+ font-size: 1.7em;
431
+ border-bottom: 1px solid var(--border);
432
+ padding-bottom: 0.3em;
433
+ }
434
+
435
+ .markdown-body :global(h2) {
436
+ font-size: 1.32em;
437
+ border-bottom: 1px solid var(--border);
438
+ padding-bottom: 0.25em;
439
+ }
440
+
441
+ .markdown-body :global(h3) {
442
+ font-size: 1.1em;
443
+ color: var(--teal);
444
+ }
445
+
446
+ .markdown-body :global(p) {
447
+ margin: 0.6em 0;
448
+ }
449
+
450
+ .markdown-body :global(a) {
451
+ color: var(--teal);
452
+ text-decoration: none;
453
+ border-bottom: 1px solid rgba(79, 209, 197, 0.35);
454
+ }
455
+
456
+ .markdown-body :global(a:hover) {
457
+ border-bottom-color: var(--teal);
458
+ }
459
+
460
+ .markdown-body :global(code) {
461
+ background: var(--code-bg);
462
+ color: var(--amber);
463
+ padding: 0.15em 0.4em;
464
+ border-radius: 4px;
465
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
466
+ font-size: 0.88em;
467
+ }
468
+
469
+ .markdown-body :global(.code-block) {
470
+ background: var(--code-bg);
471
+ border: 1px solid var(--border);
472
+ border-radius: 8px;
473
+ padding: 14px 16px;
474
+ overflow-x: auto;
475
+ margin: 0.8em 0;
476
+ position: relative;
477
+ }
478
+
479
+ .markdown-body :global(.code-block::before) {
480
+ content: attr(data-lang);
481
+ position: absolute;
482
+ top: 6px;
483
+ right: 10px;
484
+ font-size: 10px;
485
+ letter-spacing: 0.05em;
486
+ text-transform: uppercase;
487
+ color: var(--muted);
488
+ }
489
+
490
+ .markdown-body :global(.code-block code) {
491
+ background: none;
492
+ color: var(--text);
493
+ padding: 0;
494
+ font-size: 0.85em;
495
+ }
496
+
497
+ .markdown-body :global(blockquote) {
498
+ margin: 0.8em 0;
499
+ padding: 0.4em 1em;
500
+ border-left: 3px solid var(--amber);
501
+ color: var(--muted);
502
+ background: rgba(232, 179, 57, 0.05);
503
+ }
504
+
505
+ .markdown-body :global(ul),
506
+ .markdown-body :global(ol) {
507
+ margin: 0.6em 0;
508
+ padding-left: 1.4em;
509
+ }
510
+
511
+ .markdown-body :global(li) {
512
+ margin: 0.25em 0;
513
+ }
514
+
515
+ .markdown-body :global(li.task) {
516
+ list-style: none;
517
+ margin-left: -1.4em;
518
+ display: flex;
519
+ align-items: baseline;
520
+ gap: 6px;
521
+ }
522
+
523
+ .markdown-body :global(hr) {
524
+ border: none;
525
+ border-top: 1px solid var(--border);
526
+ margin: 1.4em 0;
527
+ }
528
+
529
+ .markdown-body :global(img) {
530
+ max-width: 100%;
531
+ border-radius: 6px;
532
+ }
533
+
534
+ .markdown-body :global(strong) {
535
+ color: var(--text);
536
+ }
537
+
538
+ .status-bar {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 6px;
542
+ padding: 6px 14px;
543
+ background: var(--panel);
544
+ border-top: 1px solid var(--border);
545
+ font-size: 11.5px;
546
+ color: var(--muted);
547
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
548
+ flex-shrink: 0;
549
+ }
550
+
551
+ .sep {
552
+ opacity: 0.4;
553
+ }
554
+
555
+ .dirty-text {
556
+ color: var(--amber);
557
+ }
558
+
559
+ @media (max-width: 640px) {
560
+ .panes {
561
+ grid-template-columns: 1fr !important;
562
+ grid-auto-rows: 1fr;
563
+ }
564
+ .editor-pane {
565
+ border-right: none;
566
+ border-bottom: 1px solid var(--border);
567
+ }
568
+ }
569
+
570
+ .readme-editor :global(input[type='checkbox']) {
571
+ appearance: none;
572
+ -webkit-appearance: none;
573
+ width: 16px;
574
+ height: 16px;
575
+ flex-shrink: 0;
576
+ margin: 0;
577
+ border: 1.5px solid var(--border, #262c3a);
578
+ border-radius: 4px;
579
+ background: var(--code-bg, #0d0f16);
580
+ cursor: pointer;
581
+ position: relative;
582
+ transition:
583
+ background 0.15s,
584
+ border-color 0.15s;
585
+ }
586
+
587
+ .readme-editor :global(input[type='checkbox']:hover) {
588
+ border-color: var(--amber, #e8b339);
589
+ }
590
+
591
+ .readme-editor :global(input[type='checkbox']:checked) {
592
+ background: var(--amber, #e8b339);
593
+ border-color: var(--amber, #e8b339);
594
+ }
595
+
596
+ .readme-editor :global(input[type='checkbox']:checked::after) {
597
+ content: '';
598
+ position: absolute;
599
+ left: 4px;
600
+ top: 1px;
601
+ width: 4px;
602
+ height: 8px;
603
+ border: solid var(--font-1);
604
+ border-width: 0 2px 2px 0;
605
+ transform: rotate(45deg);
606
+ }
607
+
608
+ .readme-editor :global(input[type='checkbox']:focus-visible) {
609
+ outline: 2px solid var(--teal, #4fd1c5);
610
+ outline-offset: 2px;
611
+ }
612
+
613
+ /* état désactivé : même style, juste un peu estompé, pas grisé façon navigateur */
614
+ .readme-editor :global(input[type='checkbox']:disabled) {
615
+ cursor: default;
616
+ opacity: 0.85;
617
+ }
618
+
619
+ .readme-editor :global(input[type='checkbox']:disabled:checked) {
620
+ background: var(--amber, #e8b339);
621
+ border-color: var(--amber, #e8b339);
622
+ }
623
+ </style>
@@ -0,0 +1,383 @@
1
+ <script>
2
+ // ---------- État ----------
3
+ export let initialValue = `# Mon Projet
4
+
5
+ > Une courte description qui donne envie de scroller.
6
+
7
+ ## Installation
8
+
9
+ \`\`\`bash
10
+ npm install mon-projet
11
+ \`\`\`
12
+
13
+ ## Utilisation
14
+
15
+ \`\`\`js
16
+ import { run } from 'mon-projet'
17
+ run()
18
+ \`\`\`
19
+
20
+ - [x] Setup du repo
21
+ - [ ] Écrire les tests
22
+ - [ ] Publier sur npm
23
+
24
+ Voir la [documentation](https://example.com) pour plus de détails.
25
+ `;
26
+ export let filename = 'README.md';
27
+
28
+ let source = initialValue;
29
+
30
+ $: html = renderMarkdown(source);
31
+
32
+ // ---------- Petit moteur markdown (sans dépendance) ----------
33
+ function escapeHtml(str) {
34
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
35
+ }
36
+
37
+ function renderInline(text) {
38
+ let t = escapeHtml(text);
39
+ t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
40
+ t = t.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" />');
41
+ t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
42
+ t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
43
+ t = t.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1<em>$2</em>');
44
+ t = t.replace(/~~([^~]+)~~/g, '<del>$1</del>');
45
+ return t;
46
+ }
47
+
48
+ function renderMarkdown(md) {
49
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
50
+ let out = [];
51
+ let i = 0;
52
+ let inCodeBlock = false;
53
+ let codeLang = '';
54
+ let codeBuf = [];
55
+ let listBuf = [];
56
+ let listType = null;
57
+
58
+ function flushList() {
59
+ if (listBuf.length) {
60
+ const tag = listType === 'ol' ? 'ol' : 'ul';
61
+ out.push(`<${tag}>${listBuf.join('')}</${tag}>`);
62
+ listBuf = [];
63
+ listType = null;
64
+ }
65
+ }
66
+
67
+ while (i < lines.length) {
68
+ const line = lines[i];
69
+ const fence = line.match(/^```(.*)$/);
70
+
71
+ if (fence) {
72
+ if (!inCodeBlock) {
73
+ inCodeBlock = true;
74
+ codeLang = fence[1].trim();
75
+ codeBuf = [];
76
+ } else {
77
+ inCodeBlock = false;
78
+ out.push(
79
+ `<pre class="code-block" data-lang="${escapeHtml(codeLang || 'text')}"><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`
80
+ );
81
+ }
82
+ i++;
83
+ continue;
84
+ }
85
+ if (inCodeBlock) {
86
+ codeBuf.push(line);
87
+ i++;
88
+ continue;
89
+ }
90
+
91
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
92
+ if (heading) {
93
+ flushList();
94
+ const level = heading[1].length;
95
+ out.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
96
+ i++;
97
+ continue;
98
+ }
99
+
100
+ const quote = line.match(/^>\s?(.*)$/);
101
+ if (quote) {
102
+ flushList();
103
+ out.push(`<blockquote>${renderInline(quote[1])}</blockquote>`);
104
+ i++;
105
+ continue;
106
+ }
107
+
108
+ const hr = line.match(/^(-{3,}|\*{3,})\s*$/);
109
+ if (hr) {
110
+ flushList();
111
+ out.push('<hr />');
112
+ i++;
113
+ continue;
114
+ }
115
+
116
+ const taskItem = line.match(/^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/);
117
+ if (taskItem) {
118
+ if (listType !== 'ul') {
119
+ flushList();
120
+ listType = 'ul';
121
+ }
122
+ const checked = taskItem[1].toLowerCase() === 'x';
123
+ listBuf.push(
124
+ `<li class="task"><input type="checkbox" disabled ${checked ? 'checked' : ''} />${renderInline(taskItem[2])}</li>`
125
+ );
126
+ i++;
127
+ continue;
128
+ }
129
+
130
+ const ulItem = line.match(/^\s*[-*]\s+(.*)$/);
131
+ if (ulItem) {
132
+ if (listType !== 'ul') {
133
+ flushList();
134
+ listType = 'ul';
135
+ }
136
+ listBuf.push(`<li>${renderInline(ulItem[1])}</li>`);
137
+ i++;
138
+ continue;
139
+ }
140
+
141
+ const olItem = line.match(/^\s*\d+\.\s+(.*)$/);
142
+ if (olItem) {
143
+ if (listType !== 'ol') {
144
+ flushList();
145
+ listType = 'ol';
146
+ }
147
+ listBuf.push(`<li>${renderInline(olItem[1])}</li>`);
148
+ i++;
149
+ continue;
150
+ }
151
+
152
+ if (line.trim() === '') {
153
+ flushList();
154
+ i++;
155
+ continue;
156
+ }
157
+
158
+ flushList();
159
+ out.push(`<p>${renderInline(line)}</p>`);
160
+ i++;
161
+ }
162
+ flushList();
163
+ if (inCodeBlock && codeBuf.length) {
164
+ out.push(`<pre class="code-block"><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
165
+ }
166
+ return out.join('\n');
167
+ }
168
+ </script>
169
+
170
+ <div class="readme-preview">
171
+
172
+ <div class="preview-pane">
173
+ <div class="markdown-body">
174
+ {@html html}
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <style>
180
+ :root {
181
+ --bg: var(--bg-1);
182
+ --panel: var(--bg-blur);
183
+ --panel-alt: var(--bg-blur);
184
+ --border: var(--stroke);
185
+ --text: var(--font-1);
186
+ --muted: var(--font-3);
187
+ --amber: var(--orange);
188
+ --teal: var(--blur);
189
+ --code-bg: var(--bg-2);
190
+ }
191
+
192
+ .readme-preview {
193
+ display: flex;
194
+ flex-direction: column;
195
+ height: 640px;
196
+ max-height: 90vh;
197
+ background: var(--bg);
198
+ color: var(--text);
199
+ border: 1px solid var(--border);
200
+ border-radius: 10px;
201
+ overflow: hidden;
202
+ font-family:
203
+ 'Inter',
204
+ -apple-system,
205
+ BlinkMacSystemFont,
206
+ 'Segoe UI',
207
+ sans-serif;
208
+ }
209
+
210
+ .chrome {
211
+ display: flex;
212
+ align-items: center;
213
+ background: var(--panel);
214
+ border-bottom: 1px solid var(--border);
215
+ padding: 0 10px;
216
+ flex-shrink: 0;
217
+ }
218
+
219
+ .preview-pane {
220
+ flex: 1;
221
+ min-height: 0;
222
+ overflow-y: auto;
223
+ padding: 20px 28px;
224
+ background: var(--panel);
225
+ }
226
+
227
+ .markdown-body {
228
+ max-width: 640px;
229
+ font-size: 14.5px;
230
+ line-height: 1.7;
231
+ color: var(--text);
232
+ }
233
+
234
+ .markdown-body :global(h1),
235
+ .markdown-body :global(h2),
236
+ .markdown-body :global(h3) {
237
+ font-family: 'Inter', sans-serif;
238
+ font-weight: 700;
239
+ color: var(--text);
240
+ margin: 1.1em 0 0.5em;
241
+ line-height: 1.3;
242
+ }
243
+
244
+ .markdown-body :global(h1) {
245
+ font-size: 1.7em;
246
+ border-bottom: 1px solid var(--border);
247
+ padding-bottom: 0.3em;
248
+ }
249
+
250
+ .markdown-body :global(h2) {
251
+ font-size: 1.32em;
252
+ border-bottom: 1px solid var(--border);
253
+ padding-bottom: 0.25em;
254
+ }
255
+
256
+ .markdown-body :global(h3) {
257
+ font-size: 1.1em;
258
+ color: var(--teal);
259
+ }
260
+
261
+ .markdown-body :global(p) {
262
+ margin: 0.6em 0;
263
+ }
264
+
265
+ .markdown-body :global(a) {
266
+ color: var(--teal);
267
+ text-decoration: none;
268
+ border-bottom: 1px solid rgba(79, 209, 197, 0.35);
269
+ }
270
+
271
+ .markdown-body :global(a:hover) {
272
+ border-bottom-color: var(--teal);
273
+ }
274
+
275
+ .markdown-body :global(code) {
276
+ background: var(--code-bg);
277
+ color: var(--amber);
278
+ padding: 0.15em 0.4em;
279
+ border-radius: 4px;
280
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
281
+ font-size: 0.88em;
282
+ }
283
+
284
+ .markdown-body :global(.code-block) {
285
+ background: var(--code-bg);
286
+ border: 1px solid var(--border);
287
+ border-radius: 8px;
288
+ padding: 14px 16px;
289
+ overflow-x: auto;
290
+ margin: 0.8em 0;
291
+ position: relative;
292
+ }
293
+
294
+ .markdown-body :global(.code-block::before) {
295
+ content: attr(data-lang);
296
+ position: absolute;
297
+ top: 6px;
298
+ right: 10px;
299
+ font-size: 10px;
300
+ letter-spacing: 0.05em;
301
+ text-transform: uppercase;
302
+ color: var(--muted);
303
+ }
304
+
305
+ .markdown-body :global(.code-block code) {
306
+ background: none;
307
+ color: var(--text);
308
+ padding: 0;
309
+ font-size: 0.85em;
310
+ }
311
+
312
+ .markdown-body :global(blockquote) {
313
+ margin: 0.8em 0;
314
+ padding: 0.4em 1em;
315
+ border-left: 3px solid var(--amber);
316
+ color: var(--muted);
317
+ background: rgba(232, 179, 57, 0.05);
318
+ }
319
+
320
+ .markdown-body :global(ul),
321
+ .markdown-body :global(ol) {
322
+ margin: 0.6em 0;
323
+ padding-left: 1.4em;
324
+ }
325
+
326
+ .markdown-body :global(li) {
327
+ margin: 0.25em 0;
328
+ }
329
+
330
+ .markdown-body :global(li.task) {
331
+ list-style: none;
332
+ margin-left: -1.4em;
333
+ display: flex;
334
+ align-items: baseline;
335
+ gap: 6px;
336
+ }
337
+
338
+ .markdown-body :global(hr) {
339
+ border: none;
340
+ border-top: 1px solid var(--border);
341
+ margin: 1.4em 0;
342
+ }
343
+
344
+ .markdown-body :global(img) {
345
+ max-width: 100%;
346
+ border-radius: 6px;
347
+ }
348
+
349
+ .markdown-body :global(strong) {
350
+ color: var(--text);
351
+ }
352
+
353
+ .readme-preview :global(input[type='checkbox']) {
354
+ appearance: none;
355
+ -webkit-appearance: none;
356
+ width: 16px;
357
+ height: 16px;
358
+ flex-shrink: 0;
359
+ margin: 0;
360
+ border: 1.5px solid var(--border, #262c3a);
361
+ border-radius: 4px;
362
+ background: var(--code-bg, #0d0f16);
363
+ cursor: default;
364
+ position: relative;
365
+ }
366
+
367
+ .readme-preview :global(input[type='checkbox']:checked) {
368
+ background: var(--amber, #e8b339);
369
+ border-color: var(--amber, #e8b339);
370
+ }
371
+
372
+ .readme-preview :global(input[type='checkbox']:checked::after) {
373
+ content: '';
374
+ position: absolute;
375
+ left: 4px;
376
+ top: 1px;
377
+ width: 4px;
378
+ height: 8px;
379
+ border: solid var(--font-1);
380
+ border-width: 0 2px 2px 0;
381
+ transform: rotate(45deg);
382
+ }
383
+ </style>
@@ -0,0 +1,382 @@
1
+ <script>
2
+ // ---------- État ----------
3
+ export let initialValue = `# Mon Projet
4
+
5
+ > Une courte description qui donne envie de scroller.
6
+
7
+ ## Installation
8
+
9
+ \`\`\`bash
10
+ npm install mon-projet
11
+ \`\`\`
12
+
13
+ ## Utilisation
14
+
15
+ \`\`\`js
16
+ import { run } from 'mon-projet'
17
+ run()
18
+ \`\`\`
19
+
20
+ - [x] Setup du repo
21
+ - [ ] Écrire les tests
22
+ - [ ] Publier sur npm
23
+
24
+ Voir la [documentation](https://example.com) pour plus de détails.
25
+ `;
26
+
27
+ let source = initialValue;
28
+
29
+ $: html = renderMarkdown(source);
30
+
31
+ // ---------- Petit moteur markdown (sans dépendance) ----------
32
+ function escapeHtml(str) {
33
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
34
+ }
35
+
36
+ function renderInline(text) {
37
+ let t = escapeHtml(text);
38
+ t = t.replace(/`([^`]+)`/g, '<code>$1</code>');
39
+ t = t.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img alt="$1" src="$2" />');
40
+ t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
41
+ t = t.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
42
+ t = t.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1<em>$2</em>');
43
+ t = t.replace(/~~([^~]+)~~/g, '<del>$1</del>');
44
+ return t;
45
+ }
46
+
47
+ function renderMarkdown(md) {
48
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
49
+ let out = [];
50
+ let i = 0;
51
+ let inCodeBlock = false;
52
+ let codeLang = '';
53
+ let codeBuf = [];
54
+ let listBuf = [];
55
+ let listType = null;
56
+
57
+ function flushList() {
58
+ if (listBuf.length) {
59
+ const tag = listType === 'ol' ? 'ol' : 'ul';
60
+ out.push(`<${tag}>${listBuf.join('')}</${tag}>`);
61
+ listBuf = [];
62
+ listType = null;
63
+ }
64
+ }
65
+
66
+ while (i < lines.length) {
67
+ const line = lines[i];
68
+ const fence = line.match(/^```(.*)$/);
69
+
70
+ if (fence) {
71
+ if (!inCodeBlock) {
72
+ inCodeBlock = true;
73
+ codeLang = fence[1].trim();
74
+ codeBuf = [];
75
+ } else {
76
+ inCodeBlock = false;
77
+ out.push(
78
+ `<pre class="code-block" data-lang="${escapeHtml(codeLang || 'text')}"><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`
79
+ );
80
+ }
81
+ i++;
82
+ continue;
83
+ }
84
+ if (inCodeBlock) {
85
+ codeBuf.push(line);
86
+ i++;
87
+ continue;
88
+ }
89
+
90
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
91
+ if (heading) {
92
+ flushList();
93
+ const level = heading[1].length;
94
+ out.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
95
+ i++;
96
+ continue;
97
+ }
98
+
99
+ const quote = line.match(/^>\s?(.*)$/);
100
+ if (quote) {
101
+ flushList();
102
+ out.push(`<blockquote>${renderInline(quote[1])}</blockquote>`);
103
+ i++;
104
+ continue;
105
+ }
106
+
107
+ const hr = line.match(/^(-{3,}|\*{3,})\s*$/);
108
+ if (hr) {
109
+ flushList();
110
+ out.push('<hr />');
111
+ i++;
112
+ continue;
113
+ }
114
+
115
+ const taskItem = line.match(/^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/);
116
+ if (taskItem) {
117
+ if (listType !== 'ul') {
118
+ flushList();
119
+ listType = 'ul';
120
+ }
121
+ const checked = taskItem[1].toLowerCase() === 'x';
122
+ listBuf.push(
123
+ `<li class="task"><input type="checkbox" disabled ${checked ? 'checked' : ''} />${renderInline(taskItem[2])}</li>`
124
+ );
125
+ i++;
126
+ continue;
127
+ }
128
+
129
+ const ulItem = line.match(/^\s*[-*]\s+(.*)$/);
130
+ if (ulItem) {
131
+ if (listType !== 'ul') {
132
+ flushList();
133
+ listType = 'ul';
134
+ }
135
+ listBuf.push(`<li>${renderInline(ulItem[1])}</li>`);
136
+ i++;
137
+ continue;
138
+ }
139
+
140
+ const olItem = line.match(/^\s*\d+\.\s+(.*)$/);
141
+ if (olItem) {
142
+ if (listType !== 'ol') {
143
+ flushList();
144
+ listType = 'ol';
145
+ }
146
+ listBuf.push(`<li>${renderInline(olItem[1])}</li>`);
147
+ i++;
148
+ continue;
149
+ }
150
+
151
+ if (line.trim() === '') {
152
+ flushList();
153
+ i++;
154
+ continue;
155
+ }
156
+
157
+ flushList();
158
+ out.push(`<p>${renderInline(line)}</p>`);
159
+ i++;
160
+ }
161
+ flushList();
162
+ if (inCodeBlock && codeBuf.length) {
163
+ out.push(`<pre class="code-block"><code>${escapeHtml(codeBuf.join('\n'))}</code></pre>`);
164
+ }
165
+ return out.join('\n');
166
+ }
167
+ </script>
168
+
169
+ <div class="readme-preview">
170
+
171
+ <div class="preview-pane">
172
+ <div class="markdown-body">
173
+ {@html html}
174
+ </div>
175
+ </div>
176
+ </div>
177
+
178
+ <style>
179
+ :root {
180
+ --bg: var(--bg-1);
181
+ --panel: var(--bg-blur);
182
+ --panel-alt: var(--bg-blur);
183
+ --border: var(--stroke);
184
+ --text: var(--font-1);
185
+ --muted: var(--font-3);
186
+ --amber: var(--orange);
187
+ --teal: var(--blur);
188
+ --code-bg: var(--bg-2);
189
+ }
190
+
191
+ .readme-preview {
192
+ display: flex;
193
+ flex-direction: column;
194
+ height: 640px;
195
+ max-height: 90vh;
196
+ background: var(--bg);
197
+ color: var(--text);
198
+ border: 1px solid var(--border);
199
+ border-radius: 10px;
200
+ overflow: hidden;
201
+ font-family:
202
+ 'Inter',
203
+ -apple-system,
204
+ BlinkMacSystemFont,
205
+ 'Segoe UI',
206
+ sans-serif;
207
+ }
208
+
209
+ .chrome {
210
+ display: flex;
211
+ align-items: center;
212
+ background: var(--panel);
213
+ border-bottom: 1px solid var(--border);
214
+ padding: 0 10px;
215
+ flex-shrink: 0;
216
+ }
217
+
218
+ .preview-pane {
219
+ flex: 1;
220
+ min-height: 0;
221
+ overflow-y: auto;
222
+ padding: 20px 28px;
223
+ background: var(--panel);
224
+ }
225
+
226
+ .markdown-body {
227
+ max-width: 640px;
228
+ font-size: 14.5px;
229
+ line-height: 1.7;
230
+ color: var(--text);
231
+ }
232
+
233
+ .markdown-body :global(h1),
234
+ .markdown-body :global(h2),
235
+ .markdown-body :global(h3) {
236
+ font-family: 'Inter', sans-serif;
237
+ font-weight: 700;
238
+ color: var(--text);
239
+ margin: 1.1em 0 0.5em;
240
+ line-height: 1.3;
241
+ }
242
+
243
+ .markdown-body :global(h1) {
244
+ font-size: 1.7em;
245
+ border-bottom: 1px solid var(--border);
246
+ padding-bottom: 0.3em;
247
+ }
248
+
249
+ .markdown-body :global(h2) {
250
+ font-size: 1.32em;
251
+ border-bottom: 1px solid var(--border);
252
+ padding-bottom: 0.25em;
253
+ }
254
+
255
+ .markdown-body :global(h3) {
256
+ font-size: 1.1em;
257
+ color: var(--teal);
258
+ }
259
+
260
+ .markdown-body :global(p) {
261
+ margin: 0.6em 0;
262
+ }
263
+
264
+ .markdown-body :global(a) {
265
+ color: var(--teal);
266
+ text-decoration: none;
267
+ border-bottom: 1px solid rgba(79, 209, 197, 0.35);
268
+ }
269
+
270
+ .markdown-body :global(a:hover) {
271
+ border-bottom-color: var(--teal);
272
+ }
273
+
274
+ .markdown-body :global(code) {
275
+ background: var(--code-bg);
276
+ color: var(--amber);
277
+ padding: 0.15em 0.4em;
278
+ border-radius: 4px;
279
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
280
+ font-size: 0.88em;
281
+ }
282
+
283
+ .markdown-body :global(.code-block) {
284
+ background: var(--code-bg);
285
+ border: 1px solid var(--border);
286
+ border-radius: 8px;
287
+ padding: 14px 16px;
288
+ overflow-x: auto;
289
+ margin: 0.8em 0;
290
+ position: relative;
291
+ }
292
+
293
+ .markdown-body :global(.code-block::before) {
294
+ content: attr(data-lang);
295
+ position: absolute;
296
+ top: 6px;
297
+ right: 10px;
298
+ font-size: 10px;
299
+ letter-spacing: 0.05em;
300
+ text-transform: uppercase;
301
+ color: var(--muted);
302
+ }
303
+
304
+ .markdown-body :global(.code-block code) {
305
+ background: none;
306
+ color: var(--text);
307
+ padding: 0;
308
+ font-size: 0.85em;
309
+ }
310
+
311
+ .markdown-body :global(blockquote) {
312
+ margin: 0.8em 0;
313
+ padding: 0.4em 1em;
314
+ border-left: 3px solid var(--amber);
315
+ color: var(--muted);
316
+ background: rgba(232, 179, 57, 0.05);
317
+ }
318
+
319
+ .markdown-body :global(ul),
320
+ .markdown-body :global(ol) {
321
+ margin: 0.6em 0;
322
+ padding-left: 1.4em;
323
+ }
324
+
325
+ .markdown-body :global(li) {
326
+ margin: 0.25em 0;
327
+ }
328
+
329
+ .markdown-body :global(li.task) {
330
+ list-style: none;
331
+ margin-left: -1.4em;
332
+ display: flex;
333
+ align-items: baseline;
334
+ gap: 6px;
335
+ }
336
+
337
+ .markdown-body :global(hr) {
338
+ border: none;
339
+ border-top: 1px solid var(--border);
340
+ margin: 1.4em 0;
341
+ }
342
+
343
+ .markdown-body :global(img) {
344
+ max-width: 100%;
345
+ border-radius: 6px;
346
+ }
347
+
348
+ .markdown-body :global(strong) {
349
+ color: var(--text);
350
+ }
351
+
352
+ .readme-preview :global(input[type='checkbox']) {
353
+ appearance: none;
354
+ -webkit-appearance: none;
355
+ width: 16px;
356
+ height: 16px;
357
+ flex-shrink: 0;
358
+ margin: 0;
359
+ border: 1.5px solid var(--border, #262c3a);
360
+ border-radius: 4px;
361
+ background: var(--code-bg, #0d0f16);
362
+ cursor: default;
363
+ position: relative;
364
+ }
365
+
366
+ .readme-preview :global(input[type='checkbox']:checked) {
367
+ background: var(--amber, #e8b339);
368
+ border-color: var(--amber, #e8b339);
369
+ }
370
+
371
+ .readme-preview :global(input[type='checkbox']:checked::after) {
372
+ content: '';
373
+ position: absolute;
374
+ left: 4px;
375
+ top: 1px;
376
+ width: 4px;
377
+ height: 8px;
378
+ border: solid var(--font-1);
379
+ border-width: 0 2px 2px 0;
380
+ transform: rotate(45deg);
381
+ }
382
+ </style>