Eine Build-Pipeline zur Isolierung externer Claude-Designfähigkeiten im Produktionscode
TuBrief 편집팀
2026년 9월 11일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Für Entwickler, die ohne Designer im Alleingang einen Full-Stack-Service aufbauen, sind externe Design-Skill-Dokumente äußerst verlockend. Man kopiert einen 5.000 Zeilen langen Design-System-Prompt, den man auf Twitter oder GitHub gefunden hat, fügt ihn in die Claude-Systemanweisungen ein, und hofft, dass die Benutzeroberfläche dadurch sauber aussieht.
In der Realität tritt genau das Gegenteil ein. Schon beim Absenden der ersten Nachricht erscheint eine Token-Limit-Warnung, und die API-Antwortzeit zieht sich endlos in die Länge. Der vom Modell generierte Code packt unkenntliche Inline-Styles in die Komponenten oder importiert ein Framer Motion-Modul, das nicht einmal im Projekt installiert ist. Schließlich korrigiert man die beschädigte UI bis in die Morgenstunden manuell und kehrt zum ursprünglichen shadcn/ui-Standard-Template zurück.
Das Problem liegt nicht im ästhetischen Empfinden des Modells. Die Ursache ist die Art und Weise, wie unstrukturierter Fließtext-Markdown ungefiltert in den System-Prompt gekippt wird. Man muss die sprachlichen Stilbeschreibungen in maschinenlesbare Token-Spezifikationen umwandeln und eine Pipeline aufbauen, die den vom Modell generierten Code vor dem Erreichen der Produktionsdateien per Isolationstest überprüft.
Das direkte Einfügen des gesamten Design-System-Markdowns in den Prompt führt zu massiver Token-Verschwendung. Ästhetische Floskeln wie „ein tiefes Navyblau, das beim Benutzer Vertrauen erweckt“ sind für die Layout-Erstellung völlig nutzlos. Solche Sätze verbrauchen im Sprachmodell Rechenressourcen, die stattdessen für TypeScript-Interfaces oder Validierungslogik benötigt würden.
Man streicht die Beschreibungen in natürlicher Sprache und baut den System-Prompt stattdessen mit JSON-Objekten im Format der W3C Design Tokens Community Group (DTCG) neu auf. Übrig bleiben nur Farben, Abstände und Eckenradien.
`json
{
"color": {
"background": {
"surface": { "type": "color" },
"canvas": { "type": "color" }
},
"action": {
"primary": { "type": "color" },
"primary-hover": { "type": "color" }
}
},
"spacing": {
"compact": { "type": "dimension" },
"comfortable": { "type": "dimension" }
},
"radius": {
"md": { "type": "dimension" }
}
}
`
Diese standardisierte Token-Spezifikation wird mit Anthropic Prompt Caching kombiniert. Basis-Tokens und System-Rolle werden als fester Block definiert und mit cache_control: { type: "ephemeral" } versehen.
`typescript
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
export async function requestComponentGeneration(componentBrief: string) {
return await anthropic.messages.create({
model: "claude-3-5-sonnet-20241022",
max_tokens: 4096,
system: [
{
type: "text",
text: Next.js und Tailwind CSS Komponentengenerator. Verwende ausschließlich Klassen, die in der folgenden DTCG-Token-Spezifikation definiert sind, und gib keine Inline-Styles (style-Attribut) aus. [DTCG_TOKENS_JSON_DATA],
cache_control: { type: "ephemeral" },
},
],
messages: [
{
role: "user",
content: componentBrief,
},
],
});
}
`
Durch das Weglassen von Fließtextdokumenten schrumpft die Prompt-Größe von rund 10.000 Tokens auf etwa 1.200 Tokens. Da Prompt-Cache-Lesevorgänge laut Anthropic-Dokumentation nur mit 10 % der Standard-Eingabetoken-Gebühren berechnet werden, sinken die API-Kosten bei wiederholten Aufrufen um über 70 %.
Bei der Einbindung externer Skills schleicht Claude sich oft willkürliche Werte wie style={{ marginTop: '13px' }} ein. Auch beliebige Utility-Klassen in eckigen Klammern (w-[342px]) werden wahllos erzeugt. Solcher Code untergräbt schrittweise das globale Style-System.
Durch das Einbinden von eslint-plugin-react und eslint-plugin-tailwindcss in die Flat Config von eslint.config.mjs werden diese bereits in der Build-Phase herausgefiltert.
`javascript
import reactPlugin from "eslint-plugin-react";
import tailwindPlugin from "eslint-plugin-tailwindcss";
export default [
{
files: ["**/.{ts,tsx}"],
plugins: {
react: reactPlugin,
tailwindcss: tailwindPlugin,
},
settings: {
tailwindcss: {
callees: ["cn", "cva"],
config: "./tailwind.config.ts",
},
},
rules: {
"react/forbid-dom-props": [
"error",
{
forbid: [
{
propName: "style",
message: "Inline-Styles sind nicht zulässig. Verwenden Sie die zugewiesenen Tailwind-Utilities.",
},
],
},
],
"tailwindcss/no-arbitrary-value": "error",
"tailwindcss/no-custom-classname": [
"error",
{
whitelist: ["animate-."],
},
],
},
},
];
`
Die extrahierten Tokens werden auf theme.extend in tailwind.config.ts abgebildet. Das Überschreiben der Standardpalette würde Farben beschädigen, auf die interne shadcn/ui-Primitiven verweisen.
`typescript
import type { Config } from "tailwindcss";
import themeTokens from "./build/tailwind/theme.json";
const config: Config = {
content: ["./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
surface: themeTokens.color.background.surface,
canvas: themeTokens.color.background.canvas,
action: {
primary: themeTokens.color.action.primary,
"primary-hover": themeTokens.color.action["primary-hover"],
},
},
borderRadius: {
token: themeTokens.radius.md,
},
},
},
plugins: [require("tailwindcss-animate")],
};
export default config;
`
Interaktionen werden über Class Variance Authority (CVA) Varianten isoliert. Ein direktes Modifizieren von Radix-UI-Primitiven zerstört die Eigenschaften des Barrierefreiheitsbaums (ARIA).
`typescript
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-token text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-action-primary text-white hover:bg-action-hover active:scale-[0.98]",
secondary: "bg-surface text-foreground hover:bg-surface/80",
ghost: "hover:bg-surface text-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-8 px-3 text-xs",
lg: "h-12 px-8 text-base",
},
motion: {
subtle: "transition-all duration-150 ease-out",
expressive: "transition-all duration-300 cubic-bezier(0.16, 1, 0.3, 1)",
},
},
defaultVariants: {
variant: "default",
size: "default",
motion: "subtle",
},
}
);
export interface ActionButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
asChild?: boolean;
}
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
({ className, variant, size, motion, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, motion, className }))}
ref={ref}
{...props}
/>
);
}
);
ActionButton.displayName = "ActionButton";
`
Wird Claude aufgefordert, auffällige Animationen zu erstellen, greift es gewohnheitsmäßig zu Framer Motion. Gemäß Bundlephobia-Messungen belegt Framer Motion selbst nach Gzip-Komprimierung rund 60 KB. Sobald man für ein paar bewegte Buttons 60 KB hinzufügt, gerät die Ladezeit-Metrik LCP ins Wanken.
Im System-Prompt wird der Import von Drittanbieter-Bibliotheken untersagt und festgelegt, dass ausschließlich Eigenschaften für den Browser-Compositor-Thread verwendet werden dürfen.
`typescript
// Nicht empfohlene Form, die im Main-Thread ein Layout-Reflow auslöst
//
// Empfohlene Form, die im GPU-Compositor-Thread verarbeitet wird
`
Geometrische Eigenschaften wie top, left, width und height berechnen den Layout-Baum bei jedem Frame neu. Im Gegensatz dazu werden das transform-gpu-Utility und opacity unabhängig vom Main-Thread in einer GPU-eigenen Ebene verarbeitet, wodurch flüssige 60 FPS gewährleistet bleiben.
Auch Unschärfe-Effekte im Hintergrund (backdrop-filter: blur()) werden in Scrollbereichen weggelassen. Auf leistungsschwachen mobilen Browsern berechnet ein Blur-Filter innerhalb des Scroll-Viewports die Texturen bei jedem Frame neu, wodurch die Bildwiederholrate unter 20 FPS sinkt. Die Verwendung von einfarbigen, halbtransparenten Alphakanälen (bg-background/80 und ein 1px Rand) anstelle von Blur verhindert Probleme mit der Videospeicherauslastung.
Das direkte Schieben von Claude generiertem Code in das Next.js app/-Verzeichnis führt zu zerstörten Layouts und Hydratationsfehlern. Komponenten werden stattdessen zuerst in einer isolierten Storybook-Umgebung gerendert und erst nach bestandenem Test in die Produktion zusammengeführt.
Durch das Hinzufügen von axe-playwright zu .storybook/test-runner.js wird die Barrierefreiheit nach WCAG 2.1 AA überprüft.
`javascript
const { injectAxe, checkA11y } = require("axe-playwright");
module.exports = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await page.waitForSelector("#storybook-root", { state: "attached" });
await checkA11y(page, "#storybook-root", {
detailedReport: true,
axeOptions: {
runOnly: {
type: "tag",
values: ["wcag2a", "wcag2aa"],
},
},
});
},
};
`
Schlägt ein Test fehl, werden die Fehlermeldungen direkt an Claude zurückgegeben.
`text
[AI-Feedback-Loop: Prompt zur präzisen Komponentenkorrektur]
Der generierte Code hat den axe-core Barrierefreiheitstest nicht bestanden.
Analysieren Sie das Protokoll und geben Sie Code aus, der unter Beibehaltung der bestehenden Struktur nur die Mängel behebt.
[Fehlerdetails]
Verstoß: color-contrast
Fehlerhafter Knoten:
Grund: Kontrastverhältnis beträgt 2.8:1 und unterschreitet den WCAG AA-Standard von 4.5:1.
Lösungsanweisung: Ersetzen Sie das Text-Utility durch text-foreground oder text-action-primary-hover.
`
Es wird ein Shell-Skript erstellt, das visuelle Regressionen (Visual Regression) per Playwright als Schnappschuss erfasst und Branches nur dann erstellt, wenn alle Prüfungen erfolgreich waren.
`typescript
import { test, expect } from "@playwright/test";
import storybookManifest from "../../storybook-static/index.json";
const stories = Object.values(storybookManifest.entries).filter(
(entry) => entry.type === "story"
);
for (const story of stories) {
test(Schnappschuss-Prüfung: ${story.title} - ${story.name}, async ({ page }) => {
await page.goto(/iframe.html?id=${story.id}&viewMode=story);
await page.waitForSelector("#storybook-root");
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot(`${story.id}.png`, {
animations: "disabled",
maxDiffPixelRatio: 0.01,
threshold: 0.2,
});
});
}
`
`bash
#!/usr/bin/env bash
set -e
COMPONENT_NAME=$1
if [ -z "$COMPONENT_NAME" ]; then
echo "Fehler: Bitte geben Sie den Namen der zu prüfenden Komponente ein."
exit 1
fi
echo "1. Lint-Guardrail-Prüfung"
pnpm eslint "src/components/ui/${COMPONENT_NAME}.tsx" --max-warnings=0
echo "2. Statischer Storybook-Build"
pnpm build-storybook --quiet
echo "3. Storybook Barrierefreiheits-CLI-Test"
pnpm test-storybook
echo "4. Playwright visueller Regressions-Schnappschussvergleich"
pnpm playwright test tests/visual/component-regression.spec.ts
echo "5. Bestanden: Branch erstellen und PR senden"
git checkout -b "feature/ui-{COMPONENT_NAME}.tsx"
git commit -m "feat(ui): {COMPONENT_NAME}"
gh pr create --title "feat(ui): ${COMPONENT_NAME} hinzufügen" --body "Eine Komponente, die die Storybook A11y- und Playwright-Regressionstests bestanden hat."
`
Man muss nicht mehr bei jeder verzerrten Ansicht die Browser-Entwicklertools öffnen und an Abständen herumschrauben. Durch das Übergeben der Prüfregeln an ein Terminal-Skript lässt sich die UI auch allein sicher erstellen.