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.
- package/AGENTS.md +4 -0
- package/README.md +203 -180
- package/dashboard/README.md +13 -0
- package/dashboard/app/{agents → [locale]/agents}/page.tsx +14 -17
- package/dashboard/app/{config → [locale]/config}/page.tsx +13 -15
- package/dashboard/app/[locale]/error.tsx +25 -0
- package/dashboard/app/[locale]/layout.tsx +42 -0
- package/dashboard/app/[locale]/not-found.tsx +27 -0
- package/dashboard/app/[locale]/page.tsx +53 -0
- package/dashboard/app/[locale]/settings/page.tsx +741 -0
- package/dashboard/app/api/agents-md/route.ts +2 -8
- package/dashboard/app/api/check-tools/route.ts +134 -0
- package/dashboard/app/api/config/route.ts +2 -8
- package/dashboard/app/api/info/route.ts +26 -0
- package/dashboard/app/api/run-checks/route.ts +56 -22
- package/dashboard/app/api/settings/route.ts +84 -16
- package/dashboard/app/api/status/route.ts +16 -4
- package/dashboard/app/api/ui-config/route.ts +62 -0
- package/dashboard/app/global-error.tsx +31 -0
- package/dashboard/app/globals.css +28 -9
- package/dashboard/app/layout.tsx +2 -6
- package/dashboard/app/not-found.tsx +22 -0
- package/dashboard/components/AvailableChecks.tsx +260 -0
- package/dashboard/components/CheckCard.tsx +466 -0
- package/dashboard/components/CheckCardList.tsx +156 -0
- package/dashboard/components/Header.tsx +65 -0
- package/dashboard/components/Icons.tsx +20 -0
- package/dashboard/components/LayoutContent.tsx +24 -0
- package/dashboard/components/MyShimChecks.tsx +257 -0
- package/dashboard/components/Nav.tsx +9 -6
- package/dashboard/components/SetDocumentLang.tsx +18 -0
- package/dashboard/components/SidebarMyShim.tsx +133 -0
- package/dashboard/components/StatusCard.tsx +8 -15
- package/dashboard/components/TriggerCommandos.tsx +369 -0
- package/dashboard/lib/checks.ts +233 -0
- package/dashboard/lib/presets.ts +87 -14
- package/dashboard/lib/projectRoot.ts +22 -12
- package/dashboard/next-env.d.ts +6 -0
- package/dashboard/next.config.js +10 -1
- package/dashboard/package.json +12 -7
- package/dashboard/scripts/find-port-and-dev.js +63 -0
- package/dashboard/tailwind.config.js +1 -4
- package/dashboard/tsconfig.json +9 -2
- package/package.json +25 -3
- package/scripts/ai-code-review.sh +217 -0
- package/scripts/ai-deductive-review.js +142 -0
- package/scripts/cli.js +8 -1
- package/scripts/find-free-port.js +21 -0
- package/scripts/git-checked.sh +25 -9
- package/scripts/init.js +81 -4
- package/scripts/prepublish-clean.js +11 -0
- package/scripts/run-checks.sh +120 -0
- package/scripts/setup.js +1 -0
- package/scripts/shim-runner.js +194 -0
- package/scripts/supabase-checked.sh +23 -7
- package/scripts/update-readme.js +72 -0
- package/templates/.dependency-cruiser.json +35 -0
- package/templates/.semgrep.example.yml +19 -0
- package/templates/eslint.complexity.json +12 -0
- package/templates/git-pre-push +13 -9
- package/templates/husky-pre-push +10 -7
- package/templates/run-checks.sh +80 -27
- package/templates/stryker.config.json +16 -0
- package/dashboard/.next/cache/config.json +0 -7
- package/dashboard/.next/package.json +0 -1
- package/dashboard/.next/routes-manifest.json +0 -1
- package/dashboard/.next/trace +0 -1
- package/dashboard/app/page.tsx +0 -122
- package/dashboard/app/settings/page.tsx +0 -422
- 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
|
+
}
|