shimwrappercheck 0.2.0 → 0.4.0

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.
Files changed (70) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +203 -180
  3. package/dashboard/README.md +13 -0
  4. package/dashboard/app/{agents → [locale]/agents}/page.tsx +14 -17
  5. package/dashboard/app/{config → [locale]/config}/page.tsx +13 -15
  6. package/dashboard/app/[locale]/error.tsx +25 -0
  7. package/dashboard/app/[locale]/layout.tsx +42 -0
  8. package/dashboard/app/[locale]/not-found.tsx +27 -0
  9. package/dashboard/app/[locale]/page.tsx +53 -0
  10. package/dashboard/app/[locale]/settings/page.tsx +741 -0
  11. package/dashboard/app/api/agents-md/route.ts +2 -8
  12. package/dashboard/app/api/check-tools/route.ts +134 -0
  13. package/dashboard/app/api/config/route.ts +2 -8
  14. package/dashboard/app/api/info/route.ts +26 -0
  15. package/dashboard/app/api/run-checks/route.ts +56 -22
  16. package/dashboard/app/api/settings/route.ts +84 -16
  17. package/dashboard/app/api/status/route.ts +16 -4
  18. package/dashboard/app/api/ui-config/route.ts +62 -0
  19. package/dashboard/app/global-error.tsx +31 -0
  20. package/dashboard/app/globals.css +28 -9
  21. package/dashboard/app/layout.tsx +2 -6
  22. package/dashboard/app/not-found.tsx +22 -0
  23. package/dashboard/components/AvailableChecks.tsx +260 -0
  24. package/dashboard/components/CheckCard.tsx +466 -0
  25. package/dashboard/components/CheckCardList.tsx +156 -0
  26. package/dashboard/components/Header.tsx +65 -0
  27. package/dashboard/components/Icons.tsx +20 -0
  28. package/dashboard/components/LayoutContent.tsx +24 -0
  29. package/dashboard/components/MyShimChecks.tsx +257 -0
  30. package/dashboard/components/Nav.tsx +9 -6
  31. package/dashboard/components/SetDocumentLang.tsx +18 -0
  32. package/dashboard/components/SidebarMyShim.tsx +133 -0
  33. package/dashboard/components/StatusCard.tsx +8 -15
  34. package/dashboard/components/TriggerCommandos.tsx +369 -0
  35. package/dashboard/lib/checks.ts +233 -0
  36. package/dashboard/lib/presets.ts +87 -14
  37. package/dashboard/lib/projectRoot.ts +22 -12
  38. package/dashboard/next-env.d.ts +6 -0
  39. package/dashboard/next.config.js +10 -1
  40. package/dashboard/package.json +12 -7
  41. package/dashboard/scripts/find-port-and-dev.js +63 -0
  42. package/dashboard/tailwind.config.js +1 -4
  43. package/dashboard/tsconfig.json +9 -2
  44. package/package.json +25 -3
  45. package/scripts/ai-code-review.sh +217 -0
  46. package/scripts/ai-deductive-review.js +142 -0
  47. package/scripts/cli.js +8 -1
  48. package/scripts/find-free-port.js +21 -0
  49. package/scripts/git-checked.sh +25 -9
  50. package/scripts/init.js +81 -4
  51. package/scripts/prepublish-clean.js +11 -0
  52. package/scripts/run-checks.sh +120 -0
  53. package/scripts/setup.js +1 -0
  54. package/scripts/shim-runner.js +194 -0
  55. package/scripts/supabase-checked.sh +23 -7
  56. package/scripts/update-readme.js +72 -0
  57. package/templates/.dependency-cruiser.json +35 -0
  58. package/templates/.semgrep.example.yml +19 -0
  59. package/templates/eslint.complexity.json +12 -0
  60. package/templates/git-pre-push +13 -9
  61. package/templates/husky-pre-push +10 -7
  62. package/templates/run-checks.sh +80 -27
  63. package/templates/stryker.config.json +16 -0
  64. package/dashboard/.next/cache/config.json +0 -7
  65. package/dashboard/.next/package.json +0 -1
  66. package/dashboard/.next/routes-manifest.json +0 -1
  67. package/dashboard/.next/trace +0 -1
  68. package/dashboard/app/page.tsx +0 -122
  69. package/dashboard/app/settings/page.tsx +0 -422
  70. package/dashboard/package-lock.json +0 -5307
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Single check card with Info and Settings tabs; optional drag handle.
3
+ * Tool-Status (Scan + Copy-Paste) wird in der Info-Box angezeigt, wenn toolStatus übergeben wird.
4
+ * Location: /components/CheckCard.tsx
5
+ */
6
+ "use client";
7
+
8
+ import { useState } from "react";
9
+ import { useTranslations } from "next-intl";
10
+ import type { CheckDef } from "@/lib/checks";
11
+
12
+ export type ToolStatus = { installed: boolean; label?: string; command?: string };
13
+
14
+ function CopyButton({ text }: { text: string }) {
15
+ const t = useTranslations("common");
16
+ const [copied, setCopied] = useState(false);
17
+ const copy = () => {
18
+ navigator.clipboard.writeText(text).then(() => {
19
+ setCopied(true);
20
+ setTimeout(() => setCopied(false), 1500);
21
+ });
22
+ };
23
+ return (
24
+ <button type="button" className="btn btn-ghost btn-xs text-xs" onClick={copy} title={t("copy")}>
25
+ {copied ? t("copied") : t("copy")}
26
+ </button>
27
+ );
28
+ }
29
+
30
+ export default function CheckCard({
31
+ def,
32
+ enabled,
33
+ onToggle,
34
+ dragHandle,
35
+ checkSettings,
36
+ onSettingsChange,
37
+ compact,
38
+ leftTags,
39
+ statusTag,
40
+ headerExtra,
41
+ inlineStyle,
42
+ hideEnabledToggle,
43
+ orderIndex,
44
+ toolStatus,
45
+ }: {
46
+ def: CheckDef;
47
+ enabled: boolean;
48
+ onToggle: (v: boolean) => void;
49
+ dragHandle?: React.ReactNode;
50
+ checkSettings?: Record<string, unknown>;
51
+ onSettingsChange?: (partial: Record<string, unknown>) => void;
52
+ compact?: boolean;
53
+ /** Kleine Tags (z. B. frontend, backend) */
54
+ leftTags?: string[];
55
+ /** active = grüner Tag, inactive = roter Tag */
56
+ statusTag?: "active" | "inactive";
57
+ /** Zusätzlicher Inhalt rechts im Header (z. B. Aktivieren-Button) */
58
+ headerExtra?: React.ReactNode;
59
+ /** Dark-Border-Style für Sidebar/Available */
60
+ inlineStyle?: boolean;
61
+ /** Wenn true: "Aktiv"-Toggle ausblenden (Status = ob Check in aktiver Liste ist, nicht extra Toggle) */
62
+ hideEnabledToggle?: boolean;
63
+ /** Laufreihenfolge in My Checks (1-based); wird als Nummer-Badge angezeigt */
64
+ orderIndex?: number;
65
+ /** Tool-Status aus /api/check-tools – Anzeige + Copy-Paste in der Box */
66
+ toolStatus?: ToolStatus;
67
+ }) {
68
+ const t = useTranslations("common");
69
+ const tChecks = useTranslations("checks");
70
+ const [tab, setTab] = useState<"info" | "settings">("info");
71
+ const [modalOpen, setModalOpen] = useState(false);
72
+ const [detailsOpen, setDetailsOpen] = useState(false);
73
+ const hasEnabledToggle = !hideEnabledToggle && def.settings.some((s) => s.key === "enabled");
74
+ const checkLabel = (() => {
75
+ try {
76
+ return tChecks(`${def.id}.label`);
77
+ } catch {
78
+ return def.label;
79
+ }
80
+ })();
81
+ const checkSummary = (() => {
82
+ try {
83
+ return tChecks(`${def.id}.summary`);
84
+ } catch {
85
+ return def.summary;
86
+ }
87
+ })();
88
+ const checkInfo = (() => {
89
+ try {
90
+ return tChecks(`${def.id}.info`);
91
+ } catch {
92
+ return def.info;
93
+ }
94
+ })();
95
+
96
+ if (compact) {
97
+ return (
98
+ <div className="space-y-2">
99
+ <h4 className="text-sm font-medium text-white">{checkLabel}</h4>
100
+ {def.settings.map((s) => {
101
+ if (s.type === "boolean" && s.key === "enabled") return null;
102
+ const val = checkSettings?.[s.key] ?? s.default;
103
+ return (
104
+ <div key={s.key}>
105
+ <label className="text-xs text-neutral-400">{s.label}</label>
106
+ {s.type === "boolean" && (
107
+ <input
108
+ type="checkbox"
109
+ className="toggle toggle-sm ml-2"
110
+ checked={val as boolean}
111
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.checked })}
112
+ />
113
+ )}
114
+ {s.type === "number" && (
115
+ <input
116
+ type="number"
117
+ className="input input-sm w-full mt-1 bg-neutral-900 border border-white/30 text-white"
118
+ value={val != null ? String(val) : ""}
119
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value ? Number(e.target.value) : s.default })}
120
+ />
121
+ )}
122
+ {s.type === "string" && (
123
+ <input
124
+ type="text"
125
+ className="input input-sm w-full mt-1 bg-neutral-900 border border-white/30 text-white"
126
+ value={val != null ? String(val) : ""}
127
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value })}
128
+ />
129
+ )}
130
+ {s.type === "select" && (
131
+ <select
132
+ className="select select-sm w-full mt-1 bg-neutral-900 border border-white/30 text-white"
133
+ value={val != null ? String(val) : ""}
134
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value })}
135
+ >
136
+ {s.options?.map((o) => (
137
+ <option key={o.value} value={o.value}>
138
+ {o.label}
139
+ </option>
140
+ ))}
141
+ </select>
142
+ )}
143
+ </div>
144
+ );
145
+ })}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ const borderClass = inlineStyle ? "border-white/80 bg-[#0f0f0f]" : "border-neutral-600 bg-neutral-800/80";
151
+ const borderBottomClass = inlineStyle ? "border-white/20" : "border-neutral-600";
152
+
153
+ return (
154
+ <div className={`border rounded-lg overflow-hidden ${borderClass}`} data-check-card>
155
+ <div
156
+ className={`flex items-center gap-2 py-2 pr-3 border-b flex-wrap ${borderBottomClass} ${dragHandle != null ? "pl-0" : "pl-3"}`}
157
+ >
158
+ {orderIndex != null && (
159
+ <span
160
+ className={`flex items-center justify-center w-6 h-6 rounded bg-white/20 text-white text-xs font-semibold shrink-0 ${dragHandle != null ? "ml-2" : ""}`}
161
+ title={`${t("runOrder")}: ${orderIndex}`}
162
+ >
163
+ {orderIndex}
164
+ </span>
165
+ )}
166
+ {dragHandle != null ? (
167
+ <div className="shrink-0 flex items-stretch border-r border-white/20 self-stretch rounded-l-lg bg-white/5 pl-1.5 pr-1.5 min-h-[2.25rem]">
168
+ {dragHandle}
169
+ </div>
170
+ ) : null}
171
+ <span className={`font-medium text-sm truncate ${dragHandle != null ? "pl-1" : ""}`}>{checkLabel}</span>
172
+ {leftTags?.length ? (
173
+ <span className="flex gap-0.5 shrink-0">
174
+ {leftTags.map((tag) => (
175
+ <span
176
+ key={tag}
177
+ className="text-[9px] leading-tight px-1 py-0.5 rounded border border-white/40 bg-white/5 capitalize"
178
+ >
179
+ {tag}
180
+ </span>
181
+ ))}
182
+ </span>
183
+ ) : null}
184
+ {statusTag ? (
185
+ <span
186
+ className={`text-[9px] leading-tight px-1 py-0.5 rounded shrink-0 ${
187
+ statusTag === "active" ? "bg-green-600/80 text-white" : "bg-red-600/80 text-white"
188
+ }`}
189
+ >
190
+ {statusTag}
191
+ </span>
192
+ ) : null}
193
+ {hasEnabledToggle && (
194
+ <label className="flex items-center gap-1 cursor-pointer shrink-0 ml-auto">
195
+ <input
196
+ type="checkbox"
197
+ className="toggle toggle-sm"
198
+ checked={enabled}
199
+ onChange={(e) => onToggle(e.target.checked)}
200
+ />
201
+ <span className="text-xs">{t("active")}</span>
202
+ </label>
203
+ )}
204
+ <button
205
+ type="button"
206
+ className="btn btn-ghost btn-sm shrink-0 text-white/70 hover:text-white hover:bg-white/10 gap-1"
207
+ onClick={() => setDetailsOpen((o) => !o)}
208
+ title={detailsOpen ? t("collapseDetails") : t("expandDetails")}
209
+ aria-expanded={detailsOpen}
210
+ aria-label={detailsOpen ? t("collapseDetails") : t("expandDetails")}
211
+ >
212
+ <span className="text-xs">{t("details")}</span>
213
+ <svg
214
+ className={`w-4 h-4 transition-transform ${detailsOpen ? "rotate-180" : ""}`}
215
+ fill="none"
216
+ stroke="currentColor"
217
+ viewBox="0 0 24 24"
218
+ >
219
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
220
+ </svg>
221
+ </button>
222
+ <button
223
+ type="button"
224
+ className="btn btn-ghost btn-sm btn-square shrink-0 text-white/70 hover:text-white hover:bg-white/10"
225
+ onClick={() => setModalOpen(true)}
226
+ title={t("showLarger")}
227
+ aria-label={t("showLarger")}
228
+ >
229
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
230
+ <path
231
+ strokeLinecap="round"
232
+ strokeLinejoin="round"
233
+ strokeWidth={2}
234
+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
235
+ />
236
+ </svg>
237
+ </button>
238
+ {headerExtra}
239
+ </div>
240
+ {detailsOpen && (
241
+ <>
242
+ <div className={`flex gap-0 border-b ${borderBottomClass}`}>
243
+ <button
244
+ type="button"
245
+ className={`flex-1 py-2 px-3 text-xs font-medium ${tab === "info" ? "bg-white/20 text-white" : "text-white/70 hover:bg-white/5"} ${inlineStyle ? "" : ""}`}
246
+ onClick={() => setTab("info")}
247
+ >
248
+ {t("info")}
249
+ </button>
250
+ <button
251
+ type="button"
252
+ className={`flex-1 py-2 px-3 text-xs font-medium ${tab === "settings" ? "bg-white/20 text-white" : "text-white/70 hover:bg-white/5"}`}
253
+ onClick={() => setTab("settings")}
254
+ >
255
+ {t("settingsLabel")}
256
+ </button>
257
+ </div>
258
+ <div className={`p-3 text-sm min-h-[4rem] ${inlineStyle ? "text-neutral-300" : "text-neutral-300"}`}>
259
+ {tab === "info" && (
260
+ <>
261
+ <p className="font-medium text-white mb-2">{checkSummary}</p>
262
+ <p className="whitespace-pre-wrap text-neutral-400 text-xs">{checkInfo}</p>
263
+ {toolStatus && (
264
+ <div className="mt-3 pt-2 border-t border-white/10 text-xs">
265
+ <span className="text-neutral-400">{t("toolLabel")}: </span>
266
+ {toolStatus.installed ? (
267
+ <span className="text-green-500">✓ {toolStatus.label ?? t("toolPresent")}</span>
268
+ ) : (
269
+ <>
270
+ <span className="text-amber-500">✗ {toolStatus.label ?? t("toolNotFound")}</span>
271
+ {toolStatus.command && (
272
+ <span className="ml-2 inline-flex items-center gap-1 flex-wrap">
273
+ <code className="bg-black/30 px-1.5 py-0.5 rounded text-[11px] break-all">
274
+ {toolStatus.command}
275
+ </code>
276
+ <CopyButton text={toolStatus.command} />
277
+ </span>
278
+ )}
279
+ </>
280
+ )}
281
+ </div>
282
+ )}
283
+ </>
284
+ )}
285
+ {tab === "settings" && (
286
+ <div className="space-y-2">
287
+ {def.settings.map((s) => {
288
+ if (s.type === "boolean" && s.key === "enabled") return null;
289
+ const val = checkSettings?.[s.key] ?? s.default;
290
+ return (
291
+ <div key={s.key}>
292
+ <label className="text-xs text-neutral-400">{s.label}</label>
293
+ {s.type === "boolean" && (
294
+ <input
295
+ type="checkbox"
296
+ className="toggle toggle-sm ml-2"
297
+ checked={val as boolean}
298
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.checked })}
299
+ />
300
+ )}
301
+ {s.type === "number" && (
302
+ <input
303
+ type="number"
304
+ className="input input-sm input-bordered bg-neutral-900 border-neutral-600 text-white w-full mt-1"
305
+ value={val != null ? String(val) : ""}
306
+ onChange={(e) =>
307
+ onSettingsChange?.({ [s.key]: e.target.value ? Number(e.target.value) : s.default })
308
+ }
309
+ />
310
+ )}
311
+ {s.type === "string" && (
312
+ <input
313
+ type="text"
314
+ className="input input-sm input-bordered bg-neutral-900 border-neutral-600 text-white w-full mt-1"
315
+ value={val != null ? String(val) : ""}
316
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value })}
317
+ />
318
+ )}
319
+ {s.type === "select" && (
320
+ <select
321
+ className="select select-sm select-bordered bg-neutral-900 border-neutral-600 text-white w-full mt-1"
322
+ value={val != null ? String(val) : ""}
323
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value })}
324
+ >
325
+ {s.options?.map((o) => (
326
+ <option key={o.value} value={o.value}>
327
+ {o.label}
328
+ </option>
329
+ ))}
330
+ </select>
331
+ )}
332
+ </div>
333
+ );
334
+ })}
335
+ </div>
336
+ )}
337
+ </div>
338
+ </>
339
+ )}
340
+
341
+ {modalOpen && (
342
+ <dialog open className="modal modal-open">
343
+ <div className="modal-box max-w-2xl max-h-[90vh] overflow-hidden flex flex-col bg-neutral-800 border border-neutral-600">
344
+ <div className="flex items-center justify-between gap-2 border-b border-neutral-600 pb-3 mb-3">
345
+ <h3 className="text-lg font-semibold text-white">{checkLabel}</h3>
346
+ <button
347
+ type="button"
348
+ className="btn btn-ghost btn-sm btn-circle text-white/70 hover:text-white"
349
+ onClick={() => setModalOpen(false)}
350
+ aria-label={t("close")}
351
+ >
352
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
353
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
354
+ </svg>
355
+ </button>
356
+ </div>
357
+ <div className="flex gap-0 border-b border-neutral-600 mb-3">
358
+ <button
359
+ type="button"
360
+ className={`flex-1 py-2 px-3 text-sm font-medium ${tab === "info" ? "bg-white/20 text-white" : "text-white/70 hover:bg-white/5"}`}
361
+ onClick={() => setTab("info")}
362
+ >
363
+ {t("info")}
364
+ </button>
365
+ <button
366
+ type="button"
367
+ className={`flex-1 py-2 px-3 text-sm font-medium ${tab === "settings" ? "bg-white/20 text-white" : "text-white/70 hover:bg-white/5"}`}
368
+ onClick={() => setTab("settings")}
369
+ >
370
+ {t("settingsLabel")}
371
+ </button>
372
+ </div>
373
+ <div className="overflow-y-auto flex-1 text-base pr-2">
374
+ {tab === "info" && (
375
+ <>
376
+ <p className="font-medium text-white text-lg mb-3">{checkSummary}</p>
377
+ <p className="whitespace-pre-wrap text-neutral-300 mb-4">{checkInfo}</p>
378
+ {toolStatus && (
379
+ <div className="pt-3 border-t border-white/10 text-sm">
380
+ <span className="text-neutral-400">{t("toolLabel")}: </span>
381
+ {toolStatus.installed ? (
382
+ <span className="text-green-500">✓ {toolStatus.label ?? t("toolPresent")}</span>
383
+ ) : (
384
+ <>
385
+ <span className="text-amber-500">✗ {toolStatus.label ?? t("toolNotFound")}</span>
386
+ {toolStatus.command && (
387
+ <span className="ml-2 inline-flex items-center gap-1 flex-wrap">
388
+ <code className="bg-black/30 px-1.5 py-0.5 rounded text-sm break-all">
389
+ {toolStatus.command}
390
+ </code>
391
+ <CopyButton text={toolStatus.command} />
392
+ </span>
393
+ )}
394
+ </>
395
+ )}
396
+ </div>
397
+ )}
398
+ </>
399
+ )}
400
+ {tab === "settings" && (
401
+ <div className="space-y-4">
402
+ {def.settings.map((s) => {
403
+ if (s.type === "boolean" && s.key === "enabled") return null;
404
+ const val = checkSettings?.[s.key] ?? s.default;
405
+ return (
406
+ <div key={s.key}>
407
+ <label className="text-sm text-neutral-300 block mb-1">{s.label}</label>
408
+ {s.type === "boolean" && (
409
+ <input
410
+ type="checkbox"
411
+ className="toggle toggle-sm"
412
+ checked={val as boolean}
413
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.checked })}
414
+ />
415
+ )}
416
+ {s.type === "number" && (
417
+ <input
418
+ type="number"
419
+ className="input input-bordered bg-neutral-900 border-neutral-600 text-white w-full"
420
+ value={val != null ? String(val) : ""}
421
+ onChange={(e) =>
422
+ onSettingsChange?.({ [s.key]: e.target.value ? Number(e.target.value) : s.default })
423
+ }
424
+ />
425
+ )}
426
+ {s.type === "string" && (
427
+ <input
428
+ type="text"
429
+ className="input input-bordered bg-neutral-900 border-neutral-600 text-white w-full"
430
+ value={val != null ? String(val) : ""}
431
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value })}
432
+ />
433
+ )}
434
+ {s.type === "select" && (
435
+ <select
436
+ className="select select-bordered bg-neutral-900 border-neutral-600 text-white w-full"
437
+ value={val != null ? String(val) : ""}
438
+ onChange={(e) => onSettingsChange?.({ [s.key]: e.target.value })}
439
+ >
440
+ {s.options?.map((o) => (
441
+ <option key={o.value} value={o.value}>
442
+ {o.label}
443
+ </option>
444
+ ))}
445
+ </select>
446
+ )}
447
+ </div>
448
+ );
449
+ })}
450
+ </div>
451
+ )}
452
+ </div>
453
+ <div className="modal-action mt-4 pt-3 border-t border-neutral-600">
454
+ <button type="button" className="btn btn-primary" onClick={() => setModalOpen(false)}>
455
+ {t("close")}
456
+ </button>
457
+ </div>
458
+ </div>
459
+ <form method="dialog" className="modal-backdrop bg-black/60" onClick={() => setModalOpen(false)}>
460
+ <button type="button">{t("closeLower")}</button>
461
+ </form>
462
+ </dialog>
463
+ )}
464
+ </div>
465
+ );
466
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * List of checks: "simple" = search + name-only cards; default = full cards with drag and Info/Settings tabs.
3
+ * Location: /components/CheckCardList.tsx
4
+ */
5
+ "use client";
6
+
7
+ import { useState, useCallback, useEffect, useMemo } from "react";
8
+ import type { SettingsData } from "@/lib/presets";
9
+ import { CHECK_DEFINITIONS } from "@/lib/checks";
10
+ import CheckCard, { type ToolStatus } from "./CheckCard";
11
+
12
+ export default function CheckCardList({
13
+ settings,
14
+ onSave,
15
+ variant = "full",
16
+ }: {
17
+ settings: SettingsData | null;
18
+ onSave: (s: SettingsData) => void;
19
+ variant?: "simple" | "full";
20
+ }) {
21
+ const order = useMemo(() => settings?.checkOrder ?? [], [settings?.checkOrder]);
22
+ const [search, setSearch] = useState("");
23
+ const [dragId, setDragId] = useState<string | null>(null);
24
+ const [dropIndex, setDropIndex] = useState<number | null>(null);
25
+ const [toolStatusMap, setToolStatusMap] = useState<Record<string, ToolStatus>>({});
26
+
27
+ useEffect(() => {
28
+ fetch("/api/check-tools")
29
+ .then((r) => r.json())
30
+ .then((data) => setToolStatusMap(data.tools ?? {}))
31
+ .catch(() => setToolStatusMap({}));
32
+ }, []);
33
+
34
+ const toggles = settings?.checkToggles ?? {};
35
+ const ordered = order
36
+ .map((id) => CHECK_DEFINITIONS.find((c) => c.id === id))
37
+ .filter(Boolean) as typeof CHECK_DEFINITIONS;
38
+ const rest = CHECK_DEFINITIONS.filter((c) => !order.includes(c.id));
39
+ const list = [...ordered, ...rest];
40
+ const filtered =
41
+ variant === "simple" && search.trim()
42
+ ? list.filter((c) => c.label.toLowerCase().includes(search.trim().toLowerCase()))
43
+ : list;
44
+
45
+ const moveOrder = useCallback(
46
+ (fromId: string, toIndex: number) => {
47
+ const idx = order.indexOf(fromId);
48
+ if (idx === -1 || idx === toIndex) return;
49
+ const next = [...order];
50
+ next.splice(idx, 1);
51
+ next.splice(toIndex, 0, fromId);
52
+ onSave({ ...settings!, checkOrder: next });
53
+ },
54
+ [order, settings, onSave]
55
+ );
56
+
57
+ const handleToggle = (id: string, value: boolean) => {
58
+ if (!settings) return;
59
+ onSave({
60
+ ...settings,
61
+ checkToggles: { ...settings.checkToggles, [id]: value },
62
+ });
63
+ };
64
+
65
+ const handleSettingsChange = (checkId: string, partial: Record<string, unknown>) => {
66
+ if (!settings) return;
67
+ onSave({
68
+ ...settings,
69
+ checkSettings: {
70
+ ...settings.checkSettings,
71
+ [checkId]: { ...(settings.checkSettings as Record<string, Record<string, unknown>>)?.[checkId], ...partial },
72
+ },
73
+ });
74
+ };
75
+
76
+ if (variant === "simple") {
77
+ return (
78
+ <div className="space-y-3">
79
+ <div className="relative shrink-0">
80
+ <input
81
+ type="text"
82
+ placeholder="Suchen..."
83
+ className="input w-full input-sm bg-neutral-800 border border-neutral-500 text-white pl-8 rounded-md placeholder-neutral-500"
84
+ value={search}
85
+ onChange={(e) => setSearch(e.target.value)}
86
+ />
87
+ <svg
88
+ className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-500 pointer-events-none"
89
+ fill="none"
90
+ stroke="currentColor"
91
+ viewBox="0 0 24 24"
92
+ >
93
+ <path
94
+ strokeLinecap="round"
95
+ strokeLinejoin="round"
96
+ strokeWidth={2}
97
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
98
+ />
99
+ </svg>
100
+ </div>
101
+ <ul className="space-y-2 list-none p-0 m-0">
102
+ {filtered.map((def) => (
103
+ <li
104
+ key={def.id}
105
+ className="border border-neutral-500 rounded-md bg-neutral-800 px-3 py-2.5 text-sm text-white"
106
+ >
107
+ {def.label}
108
+ </li>
109
+ ))}
110
+ </ul>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <ul className="space-y-3">
117
+ {list.map((def, index) => (
118
+ <li
119
+ key={def.id}
120
+ className={dropIndex === index ? "ring-1 ring-primary rounded-lg" : ""}
121
+ draggable
122
+ onDragStart={() => setDragId(def.id)}
123
+ onDragEnd={() => {
124
+ setDragId(null);
125
+ setDropIndex(null);
126
+ }}
127
+ onDragOver={(e) => {
128
+ e.preventDefault();
129
+ setDropIndex(index);
130
+ }}
131
+ onDragLeave={() => setDropIndex(null)}
132
+ onDrop={(e) => {
133
+ e.preventDefault();
134
+ if (dragId) moveOrder(dragId, index);
135
+ setDragId(null);
136
+ setDropIndex(null);
137
+ }}
138
+ >
139
+ <CheckCard
140
+ def={def}
141
+ enabled={(toggles as Record<string, boolean>)[def.id] ?? true}
142
+ onToggle={(v) => handleToggle(def.id, v)}
143
+ checkSettings={(settings?.checkSettings as Record<string, Record<string, unknown>>)?.[def.id]}
144
+ onSettingsChange={(partial) => handleSettingsChange(def.id, partial)}
145
+ dragHandle={
146
+ <span className="cursor-grab text-neutral-500 select-none" title="Ziehen zum Sortieren">
147
+ ⋮⋮
148
+ </span>
149
+ }
150
+ toolStatus={toolStatusMap[def.id]}
151
+ />
152
+ </li>
153
+ ))}
154
+ </ul>
155
+ );
156
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Header: "shimwrappercheck" zentriert; rechts Schalter [myshim | Settings].
3
+ * myshim = Ansicht My Shim + Check Library, Settings = Templates & Information.
4
+ * Location: /components/Header.tsx
5
+ */
6
+ "use client";
7
+
8
+ import { useTranslations, useLocale } from "next-intl";
9
+ import { Link, usePathname } from "@/i18n/navigation";
10
+ import { IconSettings } from "@/components/Icons";
11
+
12
+ export default function Header() {
13
+ const t = useTranslations("common");
14
+ const tHeader = useTranslations("header");
15
+ const pathname = usePathname();
16
+ const isSettings = pathname === "/settings";
17
+
18
+ return (
19
+ <header className="h-14 border-b border-white/20 flex items-center justify-between px-6 bg-[#0f0f0f] shrink-0">
20
+ <div className="w-24 flex items-center gap-2" aria-hidden>
21
+ <LocaleSwitcher />
22
+ </div>
23
+ <div className="flex-1 flex justify-center pointer-events-none">
24
+ <span className="text-xl font-semibold text-white">{t("appName")}</span>
25
+ </div>
26
+ <div className="flex items-center gap-0 rounded overflow-hidden border border-white/80">
27
+ <Link
28
+ href="/"
29
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
30
+ !isSettings ? "bg-white text-black" : "text-white hover:bg-white/10"
31
+ }`}
32
+ >
33
+ {t("myshim")}
34
+ </Link>
35
+ <Link
36
+ href="/settings"
37
+ className={`px-4 py-2 text-sm font-medium transition-colors flex items-center gap-1.5 ${
38
+ isSettings ? "bg-white text-black" : "text-white hover:bg-white/10"
39
+ }`}
40
+ aria-label={tHeader("settingsAria")}
41
+ >
42
+ <IconSettings />
43
+ {t("settings")}
44
+ </Link>
45
+ </div>
46
+ </header>
47
+ );
48
+ }
49
+
50
+ function LocaleSwitcher() {
51
+ const pathname = usePathname();
52
+ const locale = useLocale();
53
+ const activeClass = "px-2 py-1 bg-white text-black font-medium";
54
+ const inactiveClass = "px-2 py-1 bg-white/10 hover:bg-white/20 text-white";
55
+ return (
56
+ <div className="flex rounded overflow-hidden border border-white/50 text-xs">
57
+ <Link href={pathname} locale="de" className={locale === "de" ? activeClass : inactiveClass}>
58
+ DE
59
+ </Link>
60
+ <Link href={pathname} locale="en" className={locale === "en" ? activeClass : inactiveClass}>
61
+ EN
62
+ </Link>
63
+ </div>
64
+ );
65
+ }