Repository navigation
Expand file tree
/
Copy pathtool-pattern.html
More file actions
1615 lines (1459 loc) · 78.9 KB
/
Copy pathtool-pattern.html
File metadata and controls
1615 lines (1459 loc) · 78.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kleine Werkzeuge aus einer Datei — zehn Bausteine</title>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #222535;
--border: #2e3248;
--accent: #8fa8ff;
--accent2: #b9a3ff;
--success: #4ade80;
--warning: #fbbf24;
--danger: #f87171;
--text: #e8edf7;
--text-muted: #98a1b8;
--code-bg: #141722;
--radius: 8px;
--font-mono: ui-monospace, 'Cascadia Code', Menlo, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 56px 1fr;
-webkit-text-size-adjust: 100%;
}
/* ── KOPFZEILE ── */
header {
grid-column: 1 / -1;
background: var(--surface);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
}
header h1 { font-size: 1rem; font-weight: 600; }
header .sub { color: var(--text-muted); font-size: .85rem; }
/* ── SEITENLEISTE ── */
nav {
background: var(--surface);
border-right: 1px solid var(--border);
padding: 16px 0;
overflow-y: auto;
}
nav .section-label {
font-size: .68rem;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--text-muted);
padding: 12px 16px 4px;
}
nav button {
width: 100%;
text-align: left;
background: none;
border: none;
color: var(--text-muted);
font-family: inherit;
font-size: .88rem;
padding: 9px 16px;
cursor: pointer;
display: flex;
align-items: center;
gap: 9px;
border-left: 2px solid transparent;
}
nav button:hover { color: var(--text); background: var(--surface2); }
nav button.active {
color: var(--accent);
background: rgba(143,168,255,.12);
border-left-color: var(--accent);
}
nav button .icon { font-size: 1rem; flex-shrink: 0; }
.pattern-num {
margin-left: auto;
font-size: .7rem;
background: var(--surface2);
padding: 1px 6px;
border-radius: 4px;
color: var(--text-muted);
}
/* ── HAUPTBEREICH ── */
main {
overflow-y: auto;
padding: 26px 30px 60px;
max-width: 820px;
}
.panel { display: none; }
.panel.active { display: block; }
.pattern-header { margin-bottom: 20px; }
.pattern-header h2 { font-size: 1.35rem; font-weight: 700; line-height: 1.3; }
.pattern-header .lead {
color: var(--text);
margin-top: 10px;
line-height: 1.65;
font-size: 1rem;
}
/* ── KARTEN ── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
margin-bottom: 16px;
}
.card h3 {
font-size: .95rem;
font-weight: 700;
color: var(--text);
margin-bottom: 10px;
}
.card p { font-size: .95rem; line-height: 1.65; color: var(--text); }
.card p + p { margin-top: 0.7rem; }
.card .muted { color: var(--text-muted); }
.card ul, .card ol { margin: 0.6rem 0 0 1.15rem; font-size: .95rem; line-height: 1.6; }
.card li { margin-bottom: 0.4rem; }
.card li strong { color: var(--text); }
/* Erklärkasten für ein einzelnes Fachwort */
.term {
background: var(--surface2);
border-left: 3px solid var(--accent2);
border-radius: 0 6px 6px 0;
padding: 10px 13px;
margin-top: 12px;
font-size: .9rem;
line-height: 1.6;
color: var(--text-muted);
}
.term b { color: var(--accent2); }
.try {
background: rgba(74,222,128,.09);
border: 1px solid rgba(74,222,128,.28);
border-radius: 6px;
padding: 9px 12px;
font-size: .9rem;
color: var(--success);
margin-bottom: 12px;
}
/* ── BEDIENELEMENTE ── */
textarea, input[type=text], input[type=url], input[type=password], select {
width: 100%;
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-family: var(--font-mono);
font-size: 16px; /* unter 16px zoomt iOS beim Antippen hinein */
padding: 10px 12px;
resize: vertical;
outline: none;
}
textarea:focus, input:focus { border-color: var(--accent); }
textarea { min-height: 100px; }
button.btn {
background: var(--accent);
color: #10131c;
border: none;
border-radius: var(--radius);
padding: 9px 15px;
font-family: inherit;
font-size: .88rem;
font-weight: 700;
cursor: pointer;
}
button.btn:hover { filter: brightness(1.08); }
button.btn.secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
button.btn.success { background: var(--success); color: #06210f; }
button.btn.danger { background: var(--danger); color: #2a0b0b; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; align-items: center; }
/* ── CODE ── */
details.code { margin-top: 14px; }
details.code > summary {
cursor: pointer;
font-size: .88rem;
color: var(--text-muted);
padding: 8px 0;
list-style: none;
}
details.code > summary::-webkit-details-marker { display: none; }
details.code > summary::before { content: '▸ '; color: var(--accent); }
details.code[open] > summary::before { content: '▾ '; }
details.code > summary:hover { color: var(--text); }
pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 13px;
overflow-x: auto;
font-family: var(--font-mono);
font-size: .78rem;
line-height: 1.6;
color: #b6c2e2;
margin-top: 8px;
}
pre .kw { color: var(--accent2); }
pre .fn { color: var(--accent); }
pre .str { color: var(--success); }
pre .cmt { color: var(--text-muted); font-style: italic; }
pre .num { color: var(--warning); }
code {
font-family: var(--font-mono);
font-size: .88em;
background: var(--code-bg);
padding: 1px 5px;
border-radius: 4px;
color: var(--accent2);
word-break: break-word;
}
/* ── AUSGABEN ── */
.output {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
font-family: var(--font-mono);
font-size: .82rem;
min-height: 60px;
margin-top: 10px;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--success);
}
.output.empty { color: var(--text-muted); font-style: italic; }
.status { font-size: .88rem; padding: 8px 11px; border-radius: 5px; margin-top: 8px; line-height: 1.5; }
.status.ok { background: rgba(74,222,128,.13); color: var(--success); border: 1px solid rgba(74,222,128,.3); }
.status.err { background: rgba(248,113,113,.13); color: var(--danger); border: 1px solid rgba(248,113,113,.3); }
.status.info{ background: rgba(143,168,255,.13); color: var(--accent); border: 1px solid rgba(143,168,255,.3); }
/* ── EINZELNE DEMOS ── */
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
.field-row label, .lbl { font-size: .85rem; color: var(--text-muted); display: block; margin-bottom: 4px; }
.ls-indicator { display: flex; align-items: center; gap: 7px; font-size: .85rem; color: var(--text-muted); margin-top: 8px; flex-wrap: wrap; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
.dot.saved { background: var(--success); }
.dot.saving { background: var(--warning); }
.drop-zone {
border: 2px dashed var(--border);
border-radius: var(--radius);
padding: 28px 16px;
text-align: center;
cursor: pointer;
color: var(--text-muted);
}
.drop-zone:hover, .drop-zone.dragover { border-color: var(--accent); background: rgba(143,168,255,.06); color: var(--text); }
.drop-zone .big-icon { font-size: 2rem; margin-bottom: 8px; }
.drop-zone p { font-size: .9rem; color: inherit; }
.result-item { padding: 8px 0; border-bottom: 1px solid var(--border); font-size: .9rem; line-height: 1.5; }
.result-item:last-child { border-bottom: none; }
.result-item strong { color: var(--accent); }
.result-item span { color: var(--text-muted); font-size: .82rem; }
.format-tag {
display: inline-block; background: var(--surface2); border: 1px solid var(--border);
border-radius: 4px; padding: 2px 8px; font-size: .75rem;
font-family: var(--font-mono); margin: 3px; color: var(--accent2);
}
.chat-bubble {
background: var(--surface2); border-radius: var(--radius); padding: 11px 14px;
font-size: .95rem; line-height: 1.6; margin-top: 10px; border-left: 3px solid var(--accent);
}
/* ── ÜBERSICHT ── */
.pattern-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; margin-top: 16px; }
.pattern-card {
background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
padding: 14px; cursor: pointer; text-align: left; font-family: inherit; color: inherit;
}
.pattern-card:hover { border-color: var(--accent); background: rgba(143,168,255,.07); }
.pattern-card .icon { font-size: 1.5rem; margin-bottom: 8px; }
.pattern-card h4 { font-size: .92rem; font-weight: 700; margin-bottom: 4px; }
.pattern-card p { font-size: .85rem; color: var(--text-muted); line-height: 1.5; }
/* ── WÖRTERBUCH ── */
dl.glossar { margin-top: 4px; }
dl.glossar dt { font-weight: 700; color: var(--accent); margin-top: 14px; font-size: .95rem; }
dl.glossar dt:first-child { margin-top: 0; }
dl.glossar dd { margin: 4px 0 0; font-size: .95rem; line-height: 1.65; color: var(--text-muted); }
dl.glossar dd .say { color: var(--text); font-style: italic; }
/* ── KLEINKRAM ── */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
width: 15px; height: 15px; border: 2px solid var(--border); border-top-color: var(--accent);
border-radius: 50%; animation: spin .7s linear infinite; display: inline-block;
vertical-align: middle; margin-right: 6px;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
.pulsing { animation: pulse .8s ease infinite; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; font-size: .92rem; }
.two-col .h { font-weight: 700; margin-bottom: 6px; }
.two-col .body { color: var(--text-muted); line-height: 1.7; }
/* ── HANDY ── */
@media (max-width: 820px) {
body { display: block; }
header { position: sticky; top: 0; z-index: 5; height: 52px; }
header .sub { display: none; }
nav {
position: sticky; top: 52px; z-index: 4;
display: flex; gap: 6px; overflow-x: auto; padding: 8px 10px;
border-right: none; border-bottom: 1px solid var(--border);
-webkit-overflow-scrolling: touch;
}
nav .section-label { display: none; }
nav button {
width: auto; white-space: nowrap; border-left: none;
border: 1px solid var(--border); border-radius: 99px; padding: 7px 13px;
}
nav button.active { border-color: var(--accent); background: rgba(143,168,255,.14); }
.pattern-num { display: none; }
main { overflow: visible; max-width: none; padding: 18px 14px 60px; }
.field-row, .two-col { grid-template-columns: 1fr; }
.pattern-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<header>
<div>🛠️</div>
<h1>Kleine Werkzeuge aus einer Datei</h1>
<span class="sub">— zehn Bausteine, jeder zum Ausprobieren</span>
</header>
<nav id="sidebar">
<div class="section-label">Anfang</div>
<button class="active" data-panel="home" onclick="showPanel('home')">
<span class="icon">🏠</span> Worum geht es hier?
</button>
<button data-panel="glossar" onclick="showPanel('glossar')">
<span class="icon">📖</span> Wörterbuch
</button>
<div class="section-label">Grundlagen</div>
<button data-panel="anatomy" onclick="showPanel('anatomy')">
<span class="icon">🧬</span> Aufbau einer Datei <span class="pattern-num">1</span>
</button>
<button data-panel="clipboard" onclick="showPanel('clipboard')">
<span class="icon">📋</span> Kopieren und Einfügen <span class="pattern-num">2</span>
</button>
<button data-panel="url-state" onclick="showPanel('url-state')">
<span class="icon">🔗</span> Merken in der Adresse <span class="pattern-num">3</span>
</button>
<button data-panel="localstorage" onclick="showPanel('localstorage')">
<span class="icon">💾</span> Merken im Browser <span class="pattern-num">4</span>
</button>
<div class="section-label">Rein und raus</div>
<button data-panel="file-input" onclick="showPanel('file-input')">
<span class="icon">📂</span> Dateien öffnen <span class="pattern-num">5</span>
</button>
<button data-panel="file-download" onclick="showPanel('file-download')">
<span class="icon">⬇️</span> Dateien erzeugen <span class="pattern-num">6</span>
</button>
<button data-panel="cors" onclick="showPanel('cors')">
<span class="icon">🌐</span> Daten aus dem Netz <span class="pattern-num">7</span>
</button>
<button data-panel="llm" onclick="showPanel('llm')">
<span class="icon">🤖</span> Eine KI mitbenutzen <span class="pattern-num">8</span>
</button>
<div class="section-label">Für später</div>
<button data-panel="cdn" onclick="showPanel('cdn')">
<span class="icon">📦</span> Fremden Code einbinden <span class="pattern-num">9</span>
</button>
<button data-panel="debug" onclick="showPanel('debug')">
<span class="icon">🔍</span> Werkzeuge zum Nachsehen <span class="pattern-num">10</span>
</button>
</nav>
<main>
<!-- ══════════════════ START ══════════════════ -->
<div id="panel-home" class="panel active">
<div class="pattern-header">
<h2>🛠️ Werkzeuge, die aus einer einzigen Datei bestehen</h2>
<p class="lead">
Diese Seite zeigt zehn wiederkehrende Bausteine für kleine Web-Werkzeuge — genau die Art, aus der auch alle anderen Tools in dieser Sammlung gebaut sind. Jeder Baustein hat eine Demo zum Anfassen; der Code steht darunter und ist zugeklappt, bis du ihn sehen willst.
</p>
</div>
<div class="card">
<h3>Was ist überhaupt gemeint?</h3>
<p>
Eine Internetseite besteht normalerweise aus vielen Teilen, die ein Server zusammensetzt und ausliefert. Ein <strong>Werkzeug aus einer Datei</strong> bricht damit: Alles steckt in einer einzigen Datei, die auf <code>.html</code> endet. Text, Aussehen und Verhalten liegen darin nebeneinander.
</p>
<p>
Diese Datei kann man per E-Mail verschicken, auf den Schreibtisch legen und doppelklicken. Sie öffnet sich im Browser und funktioniert — ohne Installation, ohne Konto, ohne dass irgendwo im Hintergrund ein Rechner mitlaufen muss.
</p>
<div class="term">
<b>Browser</b> — das Programm, mit dem du gerade liest: Safari, Chrome, Firefox. Es kann viel mehr, als Seiten anzeigen: rechnen, zeichnen, Dateien lesen, Daten abholen. Genau das nutzen diese Werkzeuge aus.
</div>
</div>
<div class="card">
<h3>Vier Regeln, die alles zusammenhalten</h3>
<ul>
<li><strong>Eine Datei.</strong> Aussehen und Verhalten stecken mit im selben Dokument, nicht in Nachbardateien. Was du hast, ist das ganze Werkzeug.</li>
<li><strong>Kein Zusammenbau nötig.</strong> Die Datei wird nicht vorher durch ein Programm geschickt, das aus ihr etwas Lauffähiges macht. Sie <em>ist</em> schon lauffähig.</li>
<li><strong>Fremder Code nur, wenn er wirklich nötig ist</strong> — und dann direkt aus dem Netz nachgeladen, nicht mitinstalliert.</li>
<li><strong>Klein bleiben.</strong> Ein paar hundert Zeilen. Kurz genug, dass man sie ganz liest — und dass eine KI sie am Stück umschreiben kann.</li>
</ul>
<div class="term">
<b>Zusammenbau</b> (auf Englisch <i>build step</i>) — bei größeren Web-Projekten schreibt man Code in einer bequemeren Form, den der Browser gar nicht versteht. Ein Hilfsprogramm übersetzt ihn vorher. Das ist mächtig, kostet aber Werkzeugkette, Wartezeit und Fehlerquellen. Für kleine Werkzeuge lohnt es sich nicht.
</div>
</div>
<div class="card">
<h3>Warum das gerade jetzt interessant ist</h3>
<p>
Eine KI kann ein solches Werkzeug in einem Rutsch schreiben, weil das Ergebnis ein einziger Textblock ist: kopieren, als <code>.html</code> speichern, öffnen, fertig. Passt etwas nicht, beschreibt man die Änderung und bekommt die ganze Datei neu. Bei einem Projekt aus dreißig Dateien mit Zusammenbau funktioniert das nicht annähernd so gut.
</p>
<p class="muted">
Die Sammlung der Bausteine geht auf einen Artikel von Simon Willison zurück:
<a href="https://simonwillison.net/2025/Dec/10/html-tools/" target="_blank" rel="noopener" style="color:var(--accent)">Useful patterns for building HTML tools</a> (englisch).
Diese Seite ist selbst eine einzige <code>.html</code>-Datei und damit ihr eigenes Beispiel.
</p>
</div>
<h3 style="margin:22px 0 0;font-size:1rem">Die Bausteine der Reihe nach</h3>
<div class="pattern-grid" id="pattern-grid">
<button class="pattern-card" onclick="showPanel('anatomy')">
<div class="icon">🧬</div><h4>1 · Aufbau einer Datei</h4>
<p>Das Grundgerüst, in das alles andere hineinwächst</p>
</button>
<button class="pattern-card" onclick="showPanel('clipboard')">
<div class="icon">📋</div><h4>2 · Kopieren und Einfügen</h4>
<p>Die Zwischenablage als schnellster Weg rein und raus</p>
</button>
<button class="pattern-card" onclick="showPanel('url-state')">
<div class="icon">🔗</div><h4>3 · Merken in der Adresse</h4>
<p>Eingaben stehen in der Webadresse — teilbar per Link</p>
</button>
<button class="pattern-card" onclick="showPanel('localstorage')">
<div class="icon">💾</div><h4>4 · Merken im Browser</h4>
<p>Entwürfe und Zugangsschlüssel bleiben auf dem Gerät</p>
</button>
<button class="pattern-card" onclick="showPanel('file-input')">
<div class="icon">📂</div><h4>5 · Dateien öffnen</h4>
<p>Eine Datei hineinziehen, ohne sie irgendwohin zu schicken</p>
</button>
<button class="pattern-card" onclick="showPanel('file-download')">
<div class="icon">⬇️</div><h4>6 · Dateien erzeugen</h4>
<p>Tabellen, Bilder, Termine im Browser bauen und speichern</p>
</button>
<button class="pattern-card" onclick="showPanel('cors')">
<div class="icon">🌐</div><h4>7 · Daten aus dem Netz</h4>
<p>Fremde Datenquellen direkt abfragen</p>
</button>
<button class="pattern-card" onclick="showPanel('llm')">
<div class="icon">🤖</div><h4>8 · Eine KI mitbenutzen</h4>
<p>Claude & Co. direkt aus der Seite heraus fragen</p>
</button>
<button class="pattern-card" onclick="showPanel('cdn')">
<div class="icon">📦</div><h4>9 · Fremden Code einbinden</h4>
<p>Fertige Bausteine anderer Leute nachladen</p>
</button>
<button class="pattern-card" onclick="showPanel('debug')">
<div class="icon">🔍</div><h4>10 · Werkzeuge zum Nachsehen</h4>
<p>Erst hinschauen, was möglich ist — dann bauen</p>
</button>
</div>
<div class="card" style="margin-top:16px">
<h3>📖 Ein Fachwort verstanden nicht?</h3>
<p>Im <strong>Wörterbuch</strong> steht jeder Begriff dieser Seite in einem Satz erklärt, ohne weitere Fachwörter.</p>
<div class="btn-row"><button class="btn" onclick="showPanel('glossar')">Zum Wörterbuch</button></div>
</div>
</div>
<!-- ══════════════════ WÖRTERBUCH ══════════════════ -->
<div id="panel-glossar" class="panel">
<div class="pattern-header">
<h2>📖 Wörterbuch</h2>
<p class="lead">Jeder Begriff, der auf dieser Seite vorkommt — in Alltagssprache, ohne dass man dafür etwas anderes wissen muss.</p>
</div>
<div class="card">
<h3>Das Grundvokabular</h3>
<dl class="glossar">
<dt>Browser</dt>
<dd>Das Programm, in dem du Internetseiten ansiehst — Safari, Chrome, Firefox, Edge.</dd>
<dt>HTML</dt>
<dd>Die Sprache, in der der <em>Inhalt</em> einer Seite beschrieben wird: Überschrift, Absatz, Knopf, Eingabefeld. Reiner Text mit spitzen Klammern drumherum.</dd>
<dt>CSS</dt>
<dd>Die Sprache für das <em>Aussehen</em>: Farben, Abstände, Schriftgrößen. Legt fest, wie das aussieht, was HTML benennt.</dd>
<dt>JavaScript</dt>
<dd>Die Sprache für das <em>Verhalten</em>: Was passiert, wenn jemand tippt oder klickt. Jeder Browser kann sie ausführen, ohne dass etwas installiert werden muss.</dd>
<dt>Server</dt>
<dd>Ein Rechner, der irgendwo dauerhaft läuft und auf Anfragen antwortet. Die Werkzeuge hier brauchen keinen — sie rechnen auf deinem Gerät.</dd>
<dt>Auf dem Gerät / lokal</dt>
<dd><span class="say">„Passiert bei dir, nicht bei jemand anderem."</span> Nichts wird verschickt, nichts landet auf einem fremden Rechner.</dd>
</dl>
</div>
<div class="card">
<h3>Wörter, die in den Bausteinen vorkommen</h3>
<dl class="glossar">
<dt>Zwischenablage</dt>
<dd>Der unsichtbare Zwischenspeicher beim Kopieren und Einfügen. Enthält oft mehrere Fassungen desselben Inhalts gleichzeitig — etwa Text mit und ohne Formatierung.</dd>
<dt>Webadresse (URL)</dt>
<dd>Das, was oben in der Adresszeile steht. Alles ab dem Rautezeichen <code>#</code> nennt man den <strong>Anker</strong>. Der Teil wird nie an einen Server geschickt — deshalb eignet er sich, um Einstellungen in einem teilbaren Link unterzubringen.</dd>
<dt>Browser-Speicher (<code>localStorage</code>)</dt>
<dd>Ein kleines Notizfach, das jede Internetseite in deinem Browser bekommt. Was dort liegt, bleibt nach dem Schließen erhalten, gilt aber nur für diesen einen Browser auf diesem einen Gerät — und geht niemanden sonst etwas an.</dd>
<dt>Datenquelle / Schnittstelle (API)</dt>
<dd>Eine Internetadresse, die keine Seite zum Ansehen zurückgibt, sondern rohe Daten zum Weiterverarbeiten. <span class="say">„Wie ein Automat: Frage rein, Datenpaket raus."</span></dd>
<dt>JSON</dt>
<dd>Das übliche Format solcher Datenpakete. Sieht aus wie eine verschachtelte Liste mit Doppelpunkten und geschweiften Klammern, ist aber schlicht Text und für Menschen lesbar.</dd>
<dt>CORS</dt>
<dd>Die Erlaubnis einer fremden Datenquelle, direkt aus einer Seite heraus abgefragt zu werden. Ohne sie blockiert der Browser die Anfrage — als Schutz, damit nicht jede Seite in deinem Namen überall nachfragen kann. <span class="say">„Steht die Erlaubnis nicht dran, geht es nicht — und das ist kein Fehler, sondern Absicht."</span></dd>
<dt>Bibliothek</dt>
<dd>Fertiger Code von anderen Leuten für eine wiederkehrende Aufgabe, etwa Diagramme zeichnen oder Tabellen einlesen. Man bindet ihn ein, statt es selbst zu schreiben.</dd>
<dt>CDN</dt>
<dd>Ein öffentliches Lager für solche Bibliotheken, aus dem der Browser sie beim Öffnen der Seite direkt nachlädt. Eine Zeile in der Datei genügt, es wird nichts installiert.</dd>
<dt>Zusammenbau (<i>build step</i>)</dt>
<dd>Ein Zwischenschritt, bei dem ein Hilfsprogramm den geschriebenen Code erst in eine Form übersetzt, die der Browser versteht. Für große Projekte üblich, für kleine Werkzeuge überflüssiger Ballast.</dd>
<dt>React</dt>
<dd>Ein sehr verbreitetes Baukastensystem für große Web-Oberflächen. Es setzt einen Zusammenbau voraus — und fällt für Werkzeuge aus einer Datei deshalb weg.</dd>
<dt>Zugangsschlüssel (API-Schlüssel)</dt>
<dd>Eine lange Zeichenkette, mit der sich ein Programm bei einem Dienst ausweist — wie ein Passwort, nur für Maschinen. Gehört nie in den Code der Datei, sondern in den Browser-Speicher.</dd>
<dt>KI-Sprachmodell (LLM)</dt>
<dd>Ein Dienst wie Claude, ChatGPT oder Gemini. Man schickt Text hin und bekommt Text zurück — technisch gesehen eine ganz normale Datenquelle mit Zugangsschlüssel.</dd>
<dt>GitHub Pages</dt>
<dd>Ein kostenloser Ort, an dem eine <code>.html</code>-Datei unter einer echten Internetadresse liegen kann. Datei hochladen, Link teilen. So ist auch diese Seite erreichbar.</dd>
</dl>
</div>
</div>
<!-- ══════════════════ 1 · AUFBAU ══════════════════ -->
<div id="panel-anatomy" class="panel">
<div class="pattern-header">
<h2>🧬 Baustein 1 — Der Aufbau einer Datei</h2>
<p class="lead">Jedes dieser Werkzeuge hat dasselbe Grundgerüst. Wer es einmal gesehen hat, erkennt es in jeder Datei wieder — und weiß, wo man etwas ändert.</p>
</div>
<div class="card">
<h3>Drei Bereiche, immer in derselben Reihenfolge</h3>
<ol>
<li><strong>Der Kopf.</strong> Titel der Seite und das Aussehen (alle Farben, Abstände, Schriftgrößen an einer Stelle).</li>
<li><strong>Der sichtbare Teil.</strong> Was man auf dem Bildschirm sieht: Überschriften, Eingabefelder, Knöpfe, Ausgabebereiche.</li>
<li><strong>Das Verhalten.</strong> Ganz unten der Teil, der auf Klicks und Eingaben reagiert und die Ausgaben füllt.</li>
</ol>
<p style="margin-top:0.7rem">Mehr ist es nicht. Alles Weitere auf dieser Seite sind Ergänzungen an genau diesen drei Stellen.</p>
</div>
<div class="card">
<h3>Warum kein Zusammenbau, warum kein React?</h3>
<p>
Sobald ein Projekt einen Zusammenbau braucht, ist die geschriebene Datei nicht mehr das, was am Ende läuft. Man kann sie nicht mehr einfach doppelklicken, nicht mehr per Mail schicken, und eine KI kann ihr Ergebnis nicht mehr direkt einsetzbar ausgeben.
</p>
<p>
Für kleine Werkzeuge ist der Preis zu hoch. Deshalb die Faustregel: Wenn du eine KI um so ein Werkzeug bittest, schreib <strong>„ohne React, alles in einer HTML-Datei"</strong> dazu. Dieser Halbsatz erspart die gesamte Werkzeugkette.
</p>
<div class="term">
<b>React</b> — ein Baukasten für große Web-Oberflächen, in der Branche sehr verbreitet. Für zehn Eingabefelder und einen Knopf ist er das falsche Werkzeug, und er zieht den Zusammenbau zwingend nach sich.
</div>
</div>
<div class="card">
<h3>Warum eine einzige Datei so viel wert ist</h3>
<ul>
<li>Aus der Antwort einer KI direkt speicherbar — kein Verteilen auf Ordner.</li>
<li>In Sekunden veröffentlicht, etwa über GitHub Pages.</li>
<li>Eine KI kann das ganze Werkzeug am Stück neu schreiben, weil sie es am Stück lesen kann.</li>
<li>Nichts kann fehlen. Die Datei ist vollständig, für immer.</li>
</ul>
</div>
<details class="code">
<summary>Das Grundgerüst als Code ansehen</summary>
<pre><span class="cmt"><!-- Eine einzige .html-Datei — mehr braucht es nicht --></span>
<span class="kw"><!DOCTYPE html></span>
<span class="kw"><html</span> lang=<span class="str">"de"</span><span class="kw">></span>
<span class="kw"><head></span>
<span class="kw"><meta</span> charset=<span class="str">"UTF-8"</span><span class="kw">></span>
<span class="cmt"><!-- diese Zeile sorgt dafür, dass es auf dem Handy passt --></span>
<span class="kw"><meta</span> name=<span class="str">"viewport"</span> content=<span class="str">"width=device-width, initial-scale=1.0"</span><span class="kw">></span>
<span class="kw"><title></span>Mein Werkzeug<span class="kw"></title></span>
<span class="kw"><style></span>
<span class="cmt">/* 1. Das Aussehen — alles an einer Stelle */</span>
<span class="fn">:root</span> { <span class="kw">--akzent:</span> <span class="str">#6c8eff</span>; }
<span class="kw"></style></span>
<span class="kw"></head></span>
<span class="kw"><body></span>
<span class="cmt"><!-- 2. Der sichtbare Teil: Eingabe → Knopf → Ausgabe --></span>
<span class="kw"><script></span>
<span class="cmt">// 3. Das Verhalten. Reines JavaScript, kein React, kein Zusammenbau.</span>
<span class="cmt">// Fehler werden in der Seite angezeigt, nie als Warnfenster.</span>
<span class="kw"></script></span>
<span class="kw"></body></span>
<span class="kw"></html></span></pre>
</details>
</div>
<!-- ══════════════════ 2 · ZWISCHENABLAGE ══════════════════ -->
<div id="panel-clipboard" class="panel">
<div class="pattern-header">
<h2>📋 Baustein 2 — Kopieren und Einfügen</h2>
<p class="lead">
Der schnellste Weg, etwas in ein Werkzeug hinein- und wieder herauszubekommen, ist die Zwischenablage. Kein Hochladen, kein Speichern, kein Dateiname — einfach einfügen, und am Ende ein Knopf, der das Ergebnis wieder mitnimmt.
</p>
</div>
<div class="card">
<h3>Was viele nicht wissen</h3>
<p>
Beim Kopieren landet fast nie nur eine Sache in der Zwischenablage, sondern mehrere Fassungen gleichzeitig. Kopierst du etwas aus einem Textprogramm, liegt dort der reine Text <em>und</em> die formatierte Fassung <em>und</em> manchmal ein Bild. Das Werkzeug darf sich aussuchen, welche es nimmt.
</p>
<div class="term">
<b>Format</b> — die Angabe, um welche Art Inhalt es sich handelt. <code>text/plain</code> heißt reiner Text, <code>text/html</code> formatierter Text, <code>image/png</code> ein Bild. Die Demo unten zeigt dir, was gerade in deiner Zwischenablage liegt.
</div>
</div>
<div class="card">
<h3>Demo: nachsehen, was in der Zwischenablage liegt</h3>
<div class="try">Irgendwo etwas kopieren, dann hier auf dieser Seite einfügen — <strong>Strg + V</strong>, auf dem Mac <strong>Cmd + V</strong>.</div>
<div id="clip-formats" class="output empty">Warte auf das Einfügen …</div>
<div id="clip-content" class="output empty" style="margin-top:8px;min-height:110px">Hier erscheint der Inhalt …</div>
</div>
<div class="card">
<h3>Demo: Text umformen und wieder mitnehmen</h3>
<textarea id="transform-input" placeholder="Text einfügen oder tippen …" style="min-height:80px"></textarea>
<div class="btn-row">
<button class="btn secondary" onclick="transformText('upper')">GROSS</button>
<button class="btn secondary" onclick="transformText('lower')">klein</button>
<button class="btn secondary" onclick="transformText('slug')">für-adressen</button>
<button class="btn secondary" onclick="transformText('reverse')">stehkcürmu</button>
</div>
<div id="transform-output" class="output empty" style="margin-top:10px">Das Ergebnis erscheint hier …</div>
<div class="btn-row">
<button class="btn success" onclick="copyOutput()">📋 Ergebnis kopieren</button>
<span id="copy-status" style="font-size:.85rem;color:var(--success)"></span>
</div>
</div>
<details class="code">
<summary>Wie das im Code aussieht</summary>
<pre><span class="cmt">// Beim Einfügen alle mitgelieferten Formate durchgehen</span>
<span class="fn">document</span>.<span class="fn">addEventListener</span>(<span class="str">'paste'</span>, <span class="kw">async</span> (e) => {
<span class="kw">for</span> (<span class="kw">const</span> teil <span class="kw">of</span> e.clipboardData.items) {
console.log(teil.type); <span class="cmt">// text/plain, text/html, image/png …</span>
<span class="kw">if</span> (teil.type === <span class="str">'text/plain'</span>) {
teil.<span class="fn">getAsString</span>(text => console.log(text));
}
}
});
<span class="cmt">// Etwas in die Zwischenablage legen</span>
<span class="kw">await</span> navigator.clipboard.<span class="fn">writeText</span>(ergebnis);</pre>
</details>
</div>
<!-- ══════════════════ 3 · ADRESSE ══════════════════ -->
<div id="panel-url-state" class="panel">
<div class="pattern-header">
<h2>🔗 Baustein 3 — Eingaben in der Adresse merken</h2>
<p class="lead">
Alles, was jemand eingestellt hat, wandert hinten in die Webadresse. Die Seite lässt sich dann als Lesezeichen ablegen oder verschicken — und wer den Link öffnet, sieht genau denselben Stand. Ganz ohne Konto und ohne Server.
</p>
</div>
<div class="card">
<h3>Der Trick mit dem Rautezeichen</h3>
<p>
Alles in einer Webadresse ab dem <code>#</code> heißt <strong>Anker</strong> und wird beim Aufrufen <em>nicht</em> mitgeschickt — dieser Teil bleibt im Browser. Genau deshalb kann man Einstellungen dort ablegen: Sie stehen im Link, sind aber nie über die Leitung gegangen.
</p>
<p class="muted">
Nur für Harmloses. Was in einem Link steht, steht auch im Verlauf, im Lesezeichen und in jeder Chatnachricht, in die er kopiert wird. Zugangsschlüssel gehören dorthin nicht — für die gibt es Baustein 4.
</p>
</div>
<div class="card">
<h3>Demo: tippen und in die Adresszeile schauen</h3>
<div class="try">Felder ausfüllen und oben die Adresse beobachten. Danach die Seite neu laden — alles ist wieder da.</div>
<div class="field-row">
<div>
<label>Name</label>
<input type="text" id="url-name" placeholder="Fabian" oninput="syncToURL()">
</div>
<div>
<label>Lieblingsfarbe</label>
<input type="text" id="url-color" placeholder="blau" oninput="syncToURL()">
</div>
</div>
<div>
<label class="lbl">Nachricht</label>
<textarea id="url-msg" style="min-height:60px" placeholder="Steht gleich in der Adresse …" oninput="syncToURL()"></textarea>
</div>
<div id="url-preview" class="output" style="margin-top:10px;color:var(--accent2)"></div>
</div>
<div class="card">
<h3>Adresse oder Browser-Speicher — was wofür?</h3>
<div class="two-col">
<div>
<div class="h" style="color:var(--accent)">In die Adresse ✓</div>
<div class="body">Ergebnisse zum Weitergeben<br>Einstellungen als Lesezeichen<br>Wenige, kurze Angaben<br>Nichts Vertrauliches</div>
</div>
<div>
<div class="h" style="color:var(--accent2)">In den Browser-Speicher ✓</div>
<div class="body">Zugangsschlüssel und Passwörter<br>Lange Texte und Entwürfe<br>Was nur dich angeht<br>Was dauerhaft bleiben soll</div>
</div>
</div>
</div>
<details class="code">
<summary>Wie das im Code aussieht</summary>
<pre><span class="cmt">// Stand in die Adresse schreiben</span>
<span class="kw">function</span> <span class="fn">syncToURL</span>() {
<span class="kw">const</span> stand = {
name: <span class="fn">document.getElementById</span>(<span class="str">'name'</span>).value,
farbe: <span class="fn">document.getElementById</span>(<span class="str">'farbe'</span>).value,
};
<span class="fn">location.hash</span> = <span class="fn">encodeURIComponent</span>(<span class="fn">JSON.stringify</span>(stand));
}
<span class="cmt">// Beim Öffnen wieder herauslesen</span>
<span class="kw">if</span> (location.hash) {
<span class="kw">const</span> stand = <span class="fn">JSON.parse</span>(<span class="fn">decodeURIComponent</span>(location.hash.slice(<span class="num">1</span>)));
<span class="fn">document.getElementById</span>(<span class="str">'name'</span>).value = stand.name || <span class="str">''</span>;
}</pre>
</details>
</div>
<!-- ══════════════════ 4 · BROWSER-SPEICHER ══════════════════ -->
<div id="panel-localstorage" class="panel">
<div class="pattern-header">
<h2>💾 Baustein 4 — Merken im Browser</h2>
<p class="lead">
Jede Internetseite bekommt in deinem Browser ein eigenes kleines Notizfach. Was dort liegt, überlebt das Schließen des Tabs und den Neustart des Rechners — verlässt aber niemals das Gerät.
</p>
</div>
<div class="card">
<h3>Was man darüber wissen sollte</h3>
<ul>
<li><strong>Es gehört zur Seite, nicht zu dir.</strong> Ein anderer Browser oder ein privates Fenster sieht ein leeres Fach.</li>
<li><strong>Es geht nirgendwo hin.</strong> Kein Server bekommt etwas davon mit — deshalb ist es der richtige Ort für Zugangsschlüssel.</li>
<li><strong>Es kann verschwinden.</strong> Wer die Browserdaten löscht, löscht es mit. Für Wichtiges also zusätzlich als Datei sichern, siehe Baustein 6.</li>
</ul>
<div class="term">
<b>Zugangsschlüssel</b> — eine lange Zeichenkette, mit der sich ein Programm bei einem Dienst ausweist; ein Passwort für Maschinen. Er darf nie im Code der Datei stehen, denn den kann jeder lesen, der die Seite öffnet. Stattdessen fragt das Werkzeug einmal danach und legt ihn ins Notizfach.
</div>
</div>
<div class="card">
<h3>Demo: Notizblock, der sich selbst speichert</h3>
<div class="try">Etwas tippen, Tab schließen, Seite neu öffnen — der Text ist noch da.</div>
<textarea id="ls-editor" style="min-height:130px" placeholder="Losschreiben … wird automatisch gespeichert."></textarea>
<div class="ls-indicator">
<div class="dot" id="ls-dot"></div>
<span id="ls-status">Noch nichts gespeichert</span>
<span style="margin-left:auto;font-size:.8rem" id="ls-wordcount">0 Wörter</span>
</div>
<div class="btn-row">
<button class="btn danger" onclick="clearDraft()">🗑️ Notiz löschen</button>
</div>
</div>
<div class="card">
<h3>Demo: Zugangsschlüssel aufbewahren</h3>
<p class="muted" style="margin-bottom:10px">Nur zum Anschauen — hier tut jede beliebige Eingabe es, sie wird an niemanden geschickt.</p>
<div style="font-size:.92rem;padding:10px;background:var(--code-bg);border-radius:6px;border:1px solid var(--border)">
Stand: <span id="key-status" style="color:var(--text-muted)">nichts hinterlegt</span>
</div>
<div class="btn-row">
<button class="btn" onclick="promptForKey()">🔑 Beispiel-Schlüssel setzen</button>
<button class="btn danger" onclick="clearKey()">Entfernen</button>
</div>
</div>
<details class="code">
<summary>Wie das im Code aussieht</summary>
<pre><span class="cmt">// Speichern, aber erst 800 ms nach dem letzten Tastendruck —</span>
<span class="cmt">// sonst würde bei jedem Buchstaben geschrieben.</span>
<span class="kw">let</span> timer;
feld.<span class="fn">addEventListener</span>(<span class="str">'input'</span>, () => {
<span class="fn">clearTimeout</span>(timer);
timer = <span class="fn">setTimeout</span>(() => {
<span class="fn">localStorage.setItem</span>(<span class="str">'entwurf'</span>, feld.value);
}, <span class="num">800</span>);
});
<span class="cmt">// Beim Öffnen zurückholen</span>
feld.value = <span class="fn">localStorage.getItem</span>(<span class="str">'entwurf'</span>) || <span class="str">''</span>;
<span class="cmt">// Zugangsschlüssel: einmal fragen, dann merken</span>
<span class="kw">let</span> schluessel = <span class="fn">localStorage.getItem</span>(<span class="str">'schluessel'</span>);
<span class="kw">if</span> (!schluessel) {
schluessel = <span class="fn">prompt</span>(<span class="str">'Bitte Zugangsschlüssel eingeben:'</span>);
<span class="kw">if</span> (schluessel) <span class="fn">localStorage.setItem</span>(<span class="str">'schluessel'</span>, schluessel);
}</pre>
</details>
</div>
<!-- ══════════════════ 5 · DATEIEN ÖFFNEN ══════════════════ -->
<div id="panel-file-input" class="panel">
<div class="pattern-header">
<h2>📂 Baustein 5 — Dateien öffnen, ohne sie herzugeben</h2>
<p class="lead">
Eine Datei in die Seite ziehen und sofort verarbeiten: Der Browser darf sie lesen, ohne dass sie irgendwohin hochgeladen wird. Sie bleibt auf deinem Gerät — bei Tabellen, Verträgen oder Fotos ist das der entscheidende Unterschied.
</p>
</div>
<div class="card">
<h3>Was damit geht</h3>
<ul>
<li>Eine Tabelle einlesen, auswerten und das Ergebnis anzeigen.</li>
<li>Ein Foto anschauen, verkleinern, drehen — ohne dass es je einen fremden Rechner sieht.</li>
<li>Text aus einem gescannten Dokument herausholen (dafür braucht es dann Baustein 9).</li>
</ul>
<p style="margin-top:0.7rem">
Der Browser lässt dabei nur das zu, was du selbst ausgewählt oder hineingezogen hast. Von sich aus kommt eine Seite an keine einzige Datei deines Geräts.
</p>
</div>
<div class="card">
<h3>Demo: Datei hineinziehen oder auswählen</h3>
<div class="try">Am Handy tippen und aus den Dateien auswählen; am Rechner geht auch Hineinziehen.</div>
<div class="drop-zone" id="drop-zone" onclick="document.getElementById('file-input-el').click()">
<div class="big-icon">📁</div>
<p>Datei hierher ziehen oder <strong>zum Auswählen tippen</strong></p>
<p style="font-size:.8rem;margin-top:4px">Text, Tabellen, Bilder</p>
</div>
<input type="file" id="file-input-el" style="display:none" onchange="handleFile(this.files[0])">
<div id="file-output" class="output empty" style="margin-top:10px;min-height:80px">Der Inhalt erscheint hier …</div>
</div>
<details class="code">
<summary>Wie das im Code aussieht</summary>
<pre><span class="cmt">// Eine Textdatei einlesen</span>
<span class="kw">async function</span> <span class="fn">handleFile</span>(datei) {
<span class="kw">const</span> text = <span class="kw">await</span> datei.<span class="fn">text</span>();
console.log(datei.name, datei.size, datei.type);
}
<span class="cmt">// Ein Bild anzeigbar machen</span>
<span class="kw">const</span> adresse = <span class="fn">URL.createObjectURL</span>(datei);
bild.src = adresse;
<span class="cmt">// Hineinziehen erlauben</span>
zone.<span class="fn">addEventListener</span>(<span class="str">'dragover'</span>, e => e.<span class="fn">preventDefault</span>());
zone.<span class="fn">addEventListener</span>(<span class="str">'drop'</span>, e => {
e.<span class="fn">preventDefault</span>();
<span class="fn">handleFile</span>(e.dataTransfer.files[<span class="num">0</span>]);
});</pre>
</details>
</div>
<!-- ══════════════════ 6 · DATEIEN ERZEUGEN ══════════════════ -->
<div id="panel-file-download" class="panel">
<div class="pattern-header">
<h2>⬇️ Baustein 6 — Dateien erzeugen</h2>
<p class="lead">
Umgekehrt geht es genauso: Das Werkzeug baut eine Datei zusammen und legt sie in deinen Download-Ordner. Tabelle, Bild, Termin für den Kalender — alles im Browser gebaut, kein Server beteiligt.
</p>
</div>
<div class="card">
<h3>Wie das funktioniert</h3>
<p>
Der Browser bekommt einen Textblock und die Angabe, was für eine Art Datei das sein soll. Daraus erzeugt er einen unsichtbaren Link und klickt ihn selbst an — was für dich aussieht wie ein ganz normaler Download.
</p>
<div class="term">
<b>Dateiart</b> (technisch <i>MIME-Typ</i>) — die Angabe <code>text/csv</code> oder <code>image/svg+xml</code>. Daran erkennt das Betriebssystem, mit welchem Programm es die Datei öffnen soll. Derselbe Text wird so einmal zur Tabelle und einmal zum Bild.
</div>
</div>
<div class="card">
<h3>Demo: vier Dateiarten erzeugen</h3>
<div class="try">Inhalte anpassen und einen der Knöpfe drücken — die Datei landet in den Downloads.</div>
<div class="field-row">
<div>
<label class="lbl">Tabelle (Spalten durch Komma getrennt)</label>
<textarea id="csv-input" style="min-height:85px">name,alter,ort
Fabian,30,Niederkassel
Simon,40,San Francisco
Alice,25,Berlin</textarea>
</div>
<div>
<label class="lbl">Datenpaket (JSON)</label>
<textarea id="json-input" style="min-height:85px">{"werkzeug": "bausteine", "version": "1.0", "anzahl": 10}</textarea>
</div>
</div>
<div class="btn-row">
<button class="btn" onclick="downloadFile(document.getElementById('csv-input').value, 'daten.csv', 'text/csv')">⬇️ Tabelle</button>
<button class="btn secondary" onclick="downloadJSON()">⬇️ Datenpaket</button>
<button class="btn secondary" onclick="downloadSVG()">⬇️ Bild</button>
<button class="btn secondary" onclick="downloadICS()">⬇️ Kalendertermin</button>
</div>
<div id="dl-status" style="margin-top:8px;font-size:.88rem;color:var(--success)"></div>
</div>
<details class="code">
<summary>Wie das im Code aussieht</summary>
<pre><span class="cmt">// Ein Helfer für alle Dateiarten</span>
<span class="kw">function</span> <span class="fn">downloadFile</span>(inhalt, dateiname, art) {
<span class="kw">const</span> paket = <span class="kw">new</span> <span class="fn">Blob</span>([inhalt], { type: art });
<span class="kw">const</span> adresse = <span class="fn">URL.createObjectURL</span>(paket);
<span class="kw">const</span> link = <span class="fn">document.createElement</span>(<span class="str">'a'</span>);
link.href = adresse;
link.download = dateiname;
link.<span class="fn">click</span>(); <span class="cmt">// unsichtbar anklicken</span>
<span class="fn">URL.revokeObjectURL</span>(adresse); <span class="cmt">// Speicher wieder freigeben</span>
}
<span class="fn">downloadFile</span>(tabelle, <span class="str">'export.csv'</span>, <span class="str">'text/csv'</span>);
<span class="fn">downloadFile</span>(bild, <span class="str">'symbol.svg'</span>, <span class="str">'image/svg+xml'</span>);
<span class="fn">downloadFile</span>(termin, <span class="str">'termin.ics'</span>, <span class="str">'text/calendar'</span>);</pre>
</details>
</div>
<!-- ══════════════════ 7 · DATEN AUS DEM NETZ ══════════════════ -->
<div id="panel-cors" class="panel">
<div class="pattern-header">
<h2>🌐 Baustein 7 — Daten aus dem Netz holen</h2>
<p class="lead">
Manche Dienste geben ihre Daten roh heraus, damit andere Programme damit arbeiten können. Ein Werkzeug aus einer Datei kann sie direkt abfragen und braucht dafür wieder keinen eigenen Server.
</p>
</div>
<div class="card">
<h3>Warum das nicht bei jeder Adresse klappt</h3>
<p>
Der Browser lässt eine Seite nicht einfach bei beliebigen fremden Adressen anfragen. Sonst könnte jede beliebige Seite in deinem Namen bei deiner Bank nachsehen, solange du dort angemeldet bist.
</p>
<p>
Deshalb muss die Gegenseite die Erlaubnis ausdrücklich mitschicken. Diese Erlaubnis heißt <strong>CORS</strong>. Fehlt sie, blockiert der Browser — das ist kein Fehler im Werkzeug, sondern Absicht. Die zweite Demo unten prüft für eine beliebige Adresse, ob die Erlaubnis da ist.
</p>
</div>
<div class="card">
<h3>Demo: Angaben zu einem Python-Paket abfragen</h3>
<div class="try">Ein Name aus dem Python-Paketverzeichnis, zum Beispiel <code>requests</code>, <code>pandas</code> oder <code>flask</code>.</div>
<div style="display:flex;gap:8px">
<input type="text" id="pypi-pkg" value="requests" style="flex:1" placeholder="Paketname …">
<button class="btn" onclick="fetchPyPI()" style="white-space:nowrap">Abfragen ↗</button>
</div>
<div id="pypi-output" style="margin-top:10px"></div>
</div>
<div class="card">
<h3>Demo: gibt eine Adresse die Erlaubnis?</h3>
<div style="display:flex;gap:8px">
<input type="url" id="cors-url" value="https://pypi.org/pypi/requests/json" style="flex:1" placeholder="https://…">
<button class="btn" onclick="checkCORS()" style="white-space:nowrap">Prüfen ↗</button>
</div>
<div id="cors-result" style="margin-top:10px"></div>