From 1c425eea107abc2b7a3594579553e9a32fee194f Mon Sep 17 00:00:00 2001
From: Marcel Wege
Date: Sat, 13 Jun 2026 09:43:18 +0200
Subject: [PATCH 1/5] feat(web-ui): canonical Lume Button on Framer Motion
(#282 follow-up)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Audit finding: 308 buttons, none using a shared component — each was
hand-styled className soup relying on the auto-material CSS matcher, and
only 3 of 308 had any motion. The spec defines five normative button
variants (§4.2) plus focus and a busy recipe (§7.3); without a component
those drift per call site.
New app/_components/ui/Button.tsx — a motion.button with:
- variants primary / secondary / ghost / danger (§4.2), sizes sm/md/lg/icon
- §7.3 busy state: verb + animated dots (lume-busy-dots), never a spinner
- tactile press/hover via Framer whileTap/whileHover (§6.4), gated on
useReducedMotion so it respects prefers-reduced-motion
- real
)}
From b6d32fbe61ff123c8beaafab7d15f8be045eab62 Mon Sep 17 00:00:00 2001
From: Marcel Wege
Date: Sat, 13 Jun 2026 09:50:54 +0200
Subject: [PATCH 2/5] feat(web-ui): Button pill + fullWidth props for the
migration (#282 follow-up)
Audit showed many CTAs are pill-shaped (rounded-full) with icons. Add pill
and fullWidth props so those migrate to the canonical Button without a shape
regression. twMerge resolves the rounded-md/rounded-full conflict.
---
web-ui/app/_components/ui/Button.tsx | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/web-ui/app/_components/ui/Button.tsx b/web-ui/app/_components/ui/Button.tsx
index e5ffb0cee..4d5e44511 100644
--- a/web-ui/app/_components/ui/Button.tsx
+++ b/web-ui/app/_components/ui/Button.tsx
@@ -35,6 +35,10 @@ export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon';
export interface ButtonProps extends Omit, 'children'> {
variant?: ButtonVariant;
size?: ButtonSize;
+ /** Pill radius (badge/CTA chips) instead of the default radius.md corner. */
+ pill?: boolean;
+ /** Stretch to the container width (form submits, full-width CTAs). */
+ fullWidth?: boolean;
/** §7.3 in-flight: replaces the label with `busyLabel` + animated dots. */
busy?: boolean;
busyLabel?: string;
@@ -78,6 +82,8 @@ export const Button = forwardRef(function Button
{
variant = 'primary',
size = 'md',
+ pill = false,
+ fullWidth = false,
busy = false,
busyLabel,
disabled,
@@ -103,7 +109,14 @@ export const Button = forwardRef(function Button
whileTap={animate ? TAP : undefined}
whileHover={animate ? HOVER : undefined}
transition={FEEL_TRANSITION}
- className={cn(BASE, VARIANT[variant], SIZE[size], className)}
+ className={cn(
+ BASE,
+ VARIANT[variant],
+ SIZE[size],
+ pill ? 'rounded-full' : 'rounded-md',
+ fullWidth && 'w-full',
+ className,
+ )}
{...rest}
>
{busy ? (
From be6eab65c050df1126a3c7ce2af1ae3341f205df Mon Sep 17 00:00:00 2001
From: Marcel Wege
Date: Sat, 13 Jun 2026 16:32:14 +0200
Subject: [PATCH 3/5] feat(web-ui): migrate ~80 buttons to the Lume Button
across all areas (#290)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Six sub-agents migrated raw to the canonical Button, one per
disjoint area (builder, store, admin, operator+routines, chat+shared+root,
graph+memory). ~80 clean variant matches migrated; ~234 deliberately
skipped and reported (tabs/toggles, icon chrome, and patterns outside the
four spec variants: bg-inverse fills, danger-with-/40-border, neutral-
bordered-no-bg). Conservative by design — misclassifying is worse than
skipping; the skipped buttons keep the correct Lume look via the material
layer, only the press feel is deferred.
Also: align vitest's `@` alias to tsconfig (`@` -> web-ui root, was ./app).
The divergence was latent until the first `@/app/...` imports landed; tsc
and next build already used the tsconfig mapping, vitest did not.
typecheck 0 errors, production build green, vitest 157/162 (the 5 failures
are the pre-existing Node-26 localStorage env issue, green on CI's Node 22).
---
web-ui/app/_components/ChoiceCard.tsx | 15 ++--
web-ui/app/_components/IssueReportCard.tsx | 10 +--
web-ui/app/_components/StreamToasts.tsx | 9 ++-
.../_components/store/CredentialsEditor.tsx | 16 ++--
.../_components/store/EditFromStoreButton.tsx | 32 +++-----
.../app/_components/store/InstallButton.tsx | 35 ++++-----
.../app/_components/store/RequiresWizard.tsx | 34 ++++-----
.../_components/store/SelfExtensionPanel.tsx | 57 +++++++++------
.../app/_components/store/UploadDropzone.tsx | 14 ++--
.../builder/panels/InspectorControls.tsx | 9 ++-
.../admin/builder/panels/InspectorPanel.tsx | 9 +--
web-ui/app/admin/bulk-promote/page.tsx | 10 ++-
web-ui/app/admin/danger-zone/page.tsx | 17 +++--
web-ui/app/admin/duplicates/page.tsx | 19 +++--
web-ui/app/admin/inconsistencies/page.tsx | 10 ++-
web-ui/app/admin/kg-lifecycle/page.tsx | 10 +--
web-ui/app/admin/kg-priorities/page.tsx | 26 +++----
web-ui/app/admin/memory-backend/page.tsx | 8 +-
web-ui/app/admin/registries/page.tsx | 29 ++++----
web-ui/app/admin/settings/page.tsx | 8 +-
web-ui/app/admin/users/[id]/page.tsx | 22 +++---
web-ui/app/admin/users/page.tsx | 15 ++--
web-ui/app/error.tsx | 10 +--
.../graph/_components/MemoryAclSection.tsx | 9 ++-
web-ui/app/memories/[id]/page.tsx | 10 ++-
.../agents/_components/AgentsDashboard.tsx | 73 ++++++++++---------
.../agents/_components/PluginsDnd.tsx | 33 +++++----
web-ui/app/page.tsx | 9 ++-
.../routines/_components/RoutineActions.tsx | 10 ++-
.../app/routines/_components/RoutineRow.tsx | 11 ++-
.../_components/RoutineTemplateEditor.tsx | 11 ++-
web-ui/app/setup/page.tsx | 12 ++-
.../[id]/_components/BoundariesSection.tsx | 11 ++-
.../[id]/_components/BuilderChatPane.tsx | 9 ++-
.../_components/CulturePresetDropdown.tsx | 11 ++-
.../[id]/_components/InstallDiffModal.tsx | 11 ++-
.../builder/[id]/_components/PagesList.tsx | 9 ++-
.../_components/PersonaTemplateGallery.tsx | 11 ++-
.../[id]/_components/PreviewChatPane.tsx | 9 ++-
.../[id]/_components/SecretsDrawer.tsx | 10 ++-
.../[id]/_components/SetupFieldsEditor.tsx | 10 ++-
.../[id]/_components/SimpleIntakePane.tsx | 11 ++-
.../builder/[id]/_components/SlotEditor.tsx | 10 ++-
.../builder/[id]/_components/SpecEditor.tsx | 10 ++-
.../[id]/_components/ToolBulkImportModal.tsx | 10 ++-
.../[id]/_components/ToolInputSchemaRow.tsx | 11 ++-
.../builder/[id]/_components/ToolList.tsx | 9 ++-
.../[id]/_components/ToolTestModal.tsx | 11 ++-
.../builder/[id]/_components/VersionsTab.tsx | 18 +++--
.../builder/_components/CreateDraftButton.tsx | 16 ++--
.../_components/ImportBundleButton.tsx | 37 +++-------
web-ui/vitest.config.ts | 6 +-
52 files changed, 423 insertions(+), 419 deletions(-)
diff --git a/web-ui/app/_components/ChoiceCard.tsx b/web-ui/app/_components/ChoiceCard.tsx
index 2dcb9400f..43b04e4da 100644
--- a/web-ui/app/_components/ChoiceCard.tsx
+++ b/web-ui/app/_components/ChoiceCard.tsx
@@ -3,6 +3,7 @@
import type React from 'react';
import { useTranslations } from 'next-intl';
+import { Button } from '@/app/_components/ui/Button';
import type { PendingUserChoice } from '../_lib/chatSessions';
export type { PendingUserChoice };
@@ -42,23 +43,17 @@ export function ChoiceCard({
)}
{choice.options.map((opt, idx) => (
- {
onChoose(opt.value);
}}
disabled={disabled}
- className={[
- 'rounded border px-3 py-2 text-xs font-medium transition',
- idx === 0
- ? 'border-[color:var(--accent)] bg-[color:var(--accent)] text-[color:var(--fg-on-dark)] hover:bg-[color:var(--accent)]'
- : 'border-[color:var(--border)] bg-[color:var(--bg-elevated)] text-[color:var(--fg)] hover:border-[color:var(--border-strong)]',
- 'disabled:cursor-not-allowed disabled:opacity-40',
- ].join(' ')}
>
{opt.label}
-
+
))}
diff --git a/web-ui/app/_components/IssueReportCard.tsx b/web-ui/app/_components/IssueReportCard.tsx
index 4b4b54043..701a68399 100644
--- a/web-ui/app/_components/IssueReportCard.tsx
+++ b/web-ui/app/_components/IssueReportCard.tsx
@@ -4,6 +4,7 @@ import type React from 'react';
import { useState } from 'react';
import { useTranslations } from 'next-intl';
+import { Button } from '@/app/_components/ui/Button';
import {
ApiError,
confirmBuilderIssue,
@@ -247,13 +248,8 @@ function SecondaryButton({
onClick: () => void;
}): React.ReactElement {
return (
-
+
{children}
-
+
);
}
diff --git a/web-ui/app/_components/StreamToasts.tsx b/web-ui/app/_components/StreamToasts.tsx
index f1cccfc33..1f900369a 100644
--- a/web-ui/app/_components/StreamToasts.tsx
+++ b/web-ui/app/_components/StreamToasts.tsx
@@ -7,6 +7,7 @@ import { useTranslations } from 'next-intl';
import { AnimatePresence, motion } from 'framer-motion';
import { Ban, X } from 'lucide-react';
+import { Button } from '@/app/_components/ui/Button';
import { useChatSessionsCtx } from '../_lib/chatSessionsContext';
import {
type StreamPhase,
@@ -325,16 +326,16 @@ function AbortConfirmModal({
>
{t('abortConfirmKeep')}
- {
e.stopPropagation();
onConfirm();
}}
- className="rounded-md border border-[color:var(--danger-edge)] bg-transparent px-3 py-2 text-xs font-medium text-[color:var(--danger)] transition hover:bg-[color:var(--danger)]/8"
>
{t('abortConfirmStop')}
-
+
,
diff --git a/web-ui/app/_components/store/CredentialsEditor.tsx b/web-ui/app/_components/store/CredentialsEditor.tsx
index 1ce35733f..cdb385d56 100644
--- a/web-ui/app/_components/store/CredentialsEditor.tsx
+++ b/web-ui/app/_components/store/CredentialsEditor.tsx
@@ -25,6 +25,7 @@ import {
patchInstalledSecrets,
} from '../../_lib/api';
import type { PluginSetupField } from '../../_lib/storeTypes';
+import { Button } from '@/app/_components/ui/Button';
interface CredentialsEditorProps {
pluginId: string;
@@ -354,19 +355,16 @@ export function CredentialsEditor({
-
void onSave()}
disabled={saving || dirtyCount === 0}
- className="inline-flex items-center gap-2 rounded-md bg-[color:var(--accent)] px-3 py-2 text-[12px] font-semibold text-[color:var(--fg-on-dark)] shadow-sm transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
+ busy={saving}
+ busyLabel={`Speichern${dirtyCount > 0 ? ` (${String(dirtyCount)})` : ''}`}
>
- {saving ? (
-
- ) : (
-
- )}
+
Speichern{dirtyCount > 0 ? ` (${String(dirtyCount)})` : ''}
-
+
{savedAt !== null ? (
✓ Gespeichert
diff --git a/web-ui/app/_components/store/EditFromStoreButton.tsx b/web-ui/app/_components/store/EditFromStoreButton.tsx
index e33e0d6cb..7fe816a51 100644
--- a/web-ui/app/_components/store/EditFromStoreButton.tsx
+++ b/web-ui/app/_components/store/EditFromStoreButton.tsx
@@ -5,8 +5,8 @@ import { useRouter } from 'next/navigation';
import { AlertTriangle, Pencil } from 'lucide-react';
import { cloneBuilderDraftFromInstalled } from '../../_lib/api';
-import { cn } from '../../_lib/cn';
import type { CloneFromInstalledResponse } from '../../_lib/builderTypes';
+import { Button } from '@/app/_components/ui/Button';
/**
* Edit-from-Store action (B.6-3). Surfaces on the plugin detail page when
@@ -71,30 +71,18 @@ export function EditFromStoreButton({
return (
-
void handleClick()}
disabled={busy}
- className={cn(
- 'inline-flex w-full items-center justify-center gap-2 rounded-md px-4 py-2',
- 'text-[12px] font-semibold uppercase tracking-[0.18em]',
- 'border border-[color:var(--rule-strong)] bg-[color:var(--paper)] text-[color:var(--ink)]',
- 'transition-colors hover:bg-[color:var(--bg-soft)]',
- 'disabled:cursor-not-allowed disabled:opacity-60',
- )}
+ busy={busy}
+ busyLabel="Klone Draft …"
+ className="text-[12px] uppercase tracking-[0.18em]"
>
- {busy ? (
- <>
-
- Klone Draft …
- >
- ) : (
- <>
-
- Im Builder bearbeiten
- >
- )}
-
+
+ Im Builder bearbeiten
+
{phase.kind === 'failed' ? (
diff --git a/web-ui/app/_components/store/InstallButton.tsx b/web-ui/app/_components/store/InstallButton.tsx
index 10d931c70..651e52454 100644
--- a/web-ui/app/_components/store/InstallButton.tsx
+++ b/web-ui/app/_components/store/InstallButton.tsx
@@ -34,6 +34,7 @@ import { pickLocalized } from '../../_lib/localized';
import { RequiresWizard } from './RequiresWizard';
import { FieldRow, extractValues } from './setupForm';
import { Markdown } from '../Markdown';
+import { Button } from '@/app/_components/ui/Button';
interface InstallButtonProps {
pluginId: string;
@@ -426,15 +427,16 @@ function InstalledPanel({
{update.from ? `${update.from} → ${update.to}` : `→ ${update.to}`}
- void doUpdate()}
disabled={updating}
- className="inline-flex items-center gap-2 rounded-full bg-[color:var(--accent)] px-4 py-2 text-[12px] font-semibold text-[color:var(--accent-fg)] disabled:opacity-60"
+ busy={updating}
+ busyLabel="Aktualisiere …"
>
- {updating ? : null}
- {updating ? 'Aktualisiere …' : 'Aktualisieren'}
-
+ Aktualisieren
+
{updateError ? (
@@ -693,24 +695,19 @@ function InstallDrawer({
Andere Werte landen in der Instanz-Konfiguration.
-
- {submitting ? (
-
- ) : null}
- {submitting ? 'Wird installiert …' : 'Installation bestätigen'}
+ Installation bestätigen
-
+
)}
diff --git a/web-ui/app/_components/store/RequiresWizard.tsx b/web-ui/app/_components/store/RequiresWizard.tsx
index d2a8dafe5..15573273c 100644
--- a/web-ui/app/_components/store/RequiresWizard.tsx
+++ b/web-ui/app/_components/store/RequiresWizard.tsx
@@ -19,6 +19,7 @@ import type {
} from '../../_lib/storeTypes';
import { Chip } from './Chip';
import { FieldRow, extractValues } from './setupForm';
+import { Button } from '@/app/_components/ui/Button';
/**
* RequiresWizard — modal-dialog operator-flow for an
@@ -202,47 +203,38 @@ export function RequiresWizard({
>
Abbrechen
-
Alle installieren
-
+
>
) : phase.kind === 'installing' && phase.requiresForm ? (
-
formRef.current?.requestSubmit()}
- className={cn(
- 'inline-flex items-center gap-2 rounded-full px-4 py-2',
- 'bg-[color:var(--accent)] text-[color:var(--accent-fg)] shadow-[var(--shadow-cta)]',
- )}
>
Schritt bestätigen
-
+
) : phase.kind === 'success' ? (
-
{
onClose();
router.refresh();
}}
- className="rounded-full bg-[color:var(--accent)] px-4 py-2 text-[13px] font-semibold text-[color:var(--accent-fg)]"
>
Schließen
-
+
) : phase.kind === 'error' ? (
{composeError}
) : null}
- void handlePropose()}
disabled={composeBusy || rationale.trim().length === 0 || patchesText.trim().length === 0}
- className="mt-2 inline-flex items-center gap-2 rounded-full bg-[color:var(--accent)] px-4 py-2 text-[12px] font-semibold text-[color:var(--accent-fg)] disabled:opacity-60"
+ busy={composeBusy}
+ busyLabel={t('proposing')}
+ className="mt-2"
>
- {composeBusy ? : null}
- {composeBusy ? t('proposing') : t('propose')}
-
+ {t('propose')}
+
{/* template compose (standalone plugins that expose selfExtend templates) */}
@@ -265,15 +268,17 @@ export function SelfExtensionPanel({ agentId }: { agentId: string }): React.Reac
{tplError ? (
{tplError}
) : null}
-
void handleProposeTemplate()}
disabled={tplBusy || tplRationale.trim().length === 0 || tplId.length === 0}
- className="mt-2 inline-flex items-center gap-2 rounded-full bg-[color:var(--accent)] px-4 py-2 text-[12px] font-semibold text-[color:var(--accent-fg)] disabled:opacity-60"
+ busy={tplBusy}
+ busyLabel={t('proposing')}
+ className="mt-2"
>
- {tplBusy ? : null}
- {tplBusy ? t('proposing') : t('proposeTemplate')}
-
+ {t('proposeTemplate')}
+
) : null}
@@ -363,15 +368,18 @@ export function SelfExtensionPanel({ agentId }: { agentId: string }): React.Reac
) : (
-
void runAction(p.id, () => approveSelfExtensionProposal(p.id))}
- className="inline-flex items-center gap-2 rounded-full bg-[color:var(--accent)] px-3 py-1 text-[12px] font-semibold text-[color:var(--accent-fg)] disabled:opacity-60"
+ busy={actionBusyId === p.id}
+ busyLabel={t('approve')}
>
- {actionBusyId === p.id ? : }
+
{t('approve')}
-
+
void runAction(p.id, async () => {
@@ -395,11 +405,12 @@ export function SelfExtensionPanel({ agentId }: { agentId: string }): React.Reac
router.refresh();
})
}
- className="mt-3 inline-flex items-center gap-2 rounded-full bg-[color:var(--accent)] px-3 py-1 text-[12px] font-semibold text-[color:var(--accent-fg)] disabled:opacity-60"
+ busy={actionBusyId === p.id}
+ busyLabel={t('installing')}
+ className="mt-3"
>
- {actionBusyId === p.id ? : null}
- {actionBusyId === p.id ? t('installing') : t('install')}
-
+ {t('install')}
+
) : null}
))}
diff --git a/web-ui/app/_components/store/UploadDropzone.tsx b/web-ui/app/_components/store/UploadDropzone.tsx
index 5d3a76baa..94ee435a4 100644
--- a/web-ui/app/_components/store/UploadDropzone.tsx
+++ b/web-ui/app/_components/store/UploadDropzone.tsx
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';
import { ApiError, uploadPackage } from '../../_lib/api';
import type { UploadedPackage } from '../../_lib/storeTypes';
import { cn } from '../../_lib/cn';
+import { Button } from '@/app/_components/ui/Button';
type UploadState =
| { kind: 'idle' }
@@ -159,18 +160,13 @@ export function UploadDropzone(): React.ReactElement {
{state.kind === 'idle' && (
- fileInputRef.current?.click()}
- className={cn(
- 'inline-flex items-center gap-2 rounded-full px-4 py-2',
- 'text-[13px] font-semibold',
- 'bg-[color:var(--accent)] text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)]',
- 'hover:brightness-110',
- )}
>
Datei wählen
-
+
oder per Drag & Drop
diff --git a/web-ui/app/admin/builder/panels/InspectorControls.tsx b/web-ui/app/admin/builder/panels/InspectorControls.tsx
index d49194235..0a84a3450 100644
--- a/web-ui/app/admin/builder/panels/InspectorControls.tsx
+++ b/web-ui/app/admin/builder/panels/InspectorControls.tsx
@@ -1,5 +1,7 @@
'use client';
+import { Button } from '@/app/_components/ui/Button';
+
export const inputCls =
'w-full rounded-md border border-[color:var(--border)] bg-transparent px-3 py-2 text-sm outline-none focus:border-[color:var(--accent)]';
@@ -30,13 +32,12 @@ export function SaveButton({
label: string;
}): React.ReactElement {
return (
-
{pending ? '…' : label}
-
+
);
}
diff --git a/web-ui/app/admin/builder/panels/InspectorPanel.tsx b/web-ui/app/admin/builder/panels/InspectorPanel.tsx
index 54b9625cb..daf7f2d52 100644
--- a/web-ui/app/admin/builder/panels/InspectorPanel.tsx
+++ b/web-ui/app/admin/builder/panels/InspectorPanel.tsx
@@ -16,6 +16,7 @@ import {
type SkillNode,
type SubAgentNode,
} from '../../../_lib/agentBuilder';
+import { Button } from '@/app/_components/ui/Button';
import type { BuilderNodeData } from '../nodes/types';
import { Field, inputCls, SaveButton } from './InspectorControls';
@@ -40,13 +41,9 @@ export function InspectorPanel(props: InspectorPanelProps): React.ReactElement {
{t('inspector.title')}
-
+
{t('inspector.close')}
-
+
diff --git a/web-ui/app/admin/bulk-promote/page.tsx b/web-ui/app/admin/bulk-promote/page.tsx
index f6ef24d87..f6d84b1e4 100644
--- a/web-ui/app/admin/bulk-promote/page.tsx
+++ b/web-ui/app/admin/bulk-promote/page.tsx
@@ -3,6 +3,8 @@
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
+
import {
ApiError,
previewBulkPromote,
@@ -185,14 +187,14 @@ export default function BulkPromotePage(): React.ReactElement {
-
void loadPreview()}
disabled={running || previewLoading}
- className="rounded border border-[color:var(--border)] px-3 py-1 text-xs hover:border-[color:var(--border-strong)] disabled:opacity-50"
>
Vorschau aktualisieren
-
+
void trigger()}
diff --git a/web-ui/app/admin/danger-zone/page.tsx b/web-ui/app/admin/danger-zone/page.tsx
index d1e419ceb..47d42f46d 100644
--- a/web-ui/app/admin/danger-zone/page.tsx
+++ b/web-ui/app/admin/danger-zone/page.tsx
@@ -3,6 +3,8 @@
import { useCallback, useMemo, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
+
import {
ApiError,
previewMemoryPurge,
@@ -248,18 +250,18 @@ export default function DangerZonePage(): React.ReactElement {
)}
- void loadPreview()}
disabled={
previewLoading ||
deleting ||
(requiresSelector && trimmedSelector.length === 0)
}
- className="rounded border border-[color:var(--border)] px-3 py-1 text-xs hover:border-[color:var(--border-strong)] disabled:opacity-50"
>
{previewLoading ? 'lädt…' : 'Vorschau'}
-
+
@@ -316,14 +318,13 @@ export default function DangerZonePage(): React.ReactElement {
className="w-full rounded border border-[color:var(--danger-edge)] px-2 py-2 text-sm font-mono"
/>
- void runDelete()}
disabled={!canDelete}
- className="rounded-md border border-[color:var(--danger-edge)] bg-transparent px-4 py-2 text-xs font-semibold text-[color:var(--danger)] transition hover:bg-[color:var(--danger)]/8 disabled:cursor-not-allowed disabled:opacity-40"
>
{deleting ? 'löscht…' : 'Unwiderruflich löschen'}
-
+
>
)}
diff --git a/web-ui/app/admin/duplicates/page.tsx b/web-ui/app/admin/duplicates/page.tsx
index df8610db2..03e2e407b 100644
--- a/web-ui/app/admin/duplicates/page.tsx
+++ b/web-ui/app/admin/duplicates/page.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
import {
listExcerptMergeCandidates,
listMergeCandidates,
@@ -372,14 +373,15 @@ export default function DuplicatesListPage(): React.ReactElement {
{s === 'all' ? 'alle' : s}
))}
-
void load()}
disabled={loading}
- className="ml-auto rounded border border-[color:var(--border)] px-3 py-1 text-xs hover:border-[color:var(--border-strong)] disabled:opacity-50"
+ className="ml-auto"
>
{loading ? 'lädt…' : 'aktualisieren'}
-
+
{error !== null && (
@@ -568,14 +570,15 @@ export default function DuplicatesListPage(): React.ReactElement {
{s === 'all' ? 'alle' : s}
))}
- void loadExcerpts()}
disabled={excerptLoading}
- className="ml-auto rounded border border-[color:var(--border)] px-3 py-1 text-xs hover:border-[color:var(--border-strong)] disabled:opacity-50"
+ className="ml-auto"
>
{excerptLoading ? 'lädt…' : 'aktualisieren'}
-
+
{excerptError !== null && (
diff --git a/web-ui/app/admin/inconsistencies/page.tsx b/web-ui/app/admin/inconsistencies/page.tsx
index 3335db0fa..8bfd7f1d0 100644
--- a/web-ui/app/admin/inconsistencies/page.tsx
+++ b/web-ui/app/admin/inconsistencies/page.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
import {
listInconsistencies,
previewBulkInconsistencyDetect,
@@ -302,14 +303,15 @@ export default function InconsistenciesListPage(): React.ReactElement {
{s === 'all' ? 'alle' : s}
))}
- void load()}
disabled={loading}
- className="ml-auto rounded border border-[color:var(--border)] px-3 py-1 text-xs hover:border-[color:var(--border-strong)] disabled:opacity-50"
+ className="ml-auto"
>
{loading ? 'lädt…' : 'aktualisieren'}
-
+
{error !== null && (
diff --git a/web-ui/app/admin/kg-lifecycle/page.tsx b/web-ui/app/admin/kg-lifecycle/page.tsx
index 44826a29e..b32044063 100644
--- a/web-ui/app/admin/kg-lifecycle/page.tsx
+++ b/web-ui/app/admin/kg-lifecycle/page.tsx
@@ -3,6 +3,8 @@
import { useCallback, useEffect, useState } from 'react';
import type { JSX } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
/**
* Admin → Knowledge-Graph Lifecycle (palaia Phase 4 / OB-73, Slice D).
*
@@ -141,13 +143,9 @@ export default function KgLifecyclePage(): JSX.Element {
cron schedule; the buttons below trigger them on demand.
- void reload()}
- >
+ void reload()}>
Refresh
-
+
{error ? (
diff --git a/web-ui/app/admin/kg-priorities/page.tsx b/web-ui/app/admin/kg-priorities/page.tsx
index ce86c81ec..2252012cb 100644
--- a/web-ui/app/admin/kg-priorities/page.tsx
+++ b/web-ui/app/admin/kg-priorities/page.tsx
@@ -2,6 +2,8 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
/**
* Admin → Knowledge-Graph Priorities (palaia Phase 5 / OB-74 Slice 5).
*
@@ -161,13 +163,9 @@ export default function KgPrioritiesPage(): React.ReactElement {
placeholder="de.byte5.agent.calendar oder orchestrator-default"
className="flex-1 rounded-md border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2 text-sm text-[color:var(--fg-strong)]"
/>
- { void reload(); }}
- className="rounded-md border border-[color:var(--border)] px-4 py-2 text-sm font-medium text-[color:var(--fg-strong)] hover:bg-[color:var(--card)]"
- >
+ { void reload(); }}>
Laden
-
+
@@ -214,8 +212,8 @@ export default function KgPrioritiesPage(): React.ReactElement {
placeholder="Reason (optional)"
className="lg:col-span-3 rounded-md border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2 text-sm text-[color:var(--fg-strong)]"
/>
- {
const w = Number.parseFloat(draftWeight);
void handleUpsert(
@@ -226,10 +224,10 @@ export default function KgPrioritiesPage(): React.ReactElement {
);
}}
disabled={busy !== null}
- className="lg:col-span-1 rounded-md border border-[color:var(--border)] px-4 py-2 text-sm font-medium text-[color:var(--fg-strong)] hover:bg-[color:var(--card)] disabled:opacity-40"
+ className="lg:col-span-1"
>
{busy === 'upsert' ? '…' : 'Add'}
-
+
@@ -285,14 +283,14 @@ export default function KgPrioritiesPage(): React.ReactElement {
{new Date(r.updatedAt).toLocaleString('de-DE')}
- { void handleRemove(r.entryExternalId); }}
disabled={busy === `remove:${r.entryExternalId}`}
- className="rounded-md border border-[color:var(--border)] px-3 py-1 text-xs text-[color:var(--fg-strong)] hover:bg-[color:var(--danger)]/20 disabled:opacity-40"
>
{busy === `remove:${r.entryExternalId}` ? '…' : 'Remove'}
-
+
|
))
diff --git a/web-ui/app/admin/memory-backend/page.tsx b/web-ui/app/admin/memory-backend/page.tsx
index e1b727dcb..4304c62fd 100644
--- a/web-ui/app/admin/memory-backend/page.tsx
+++ b/web-ui/app/admin/memory-backend/page.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
import {
ApiError,
getMemoryBackend,
@@ -216,14 +217,13 @@ export default function MemoryBackendPage(): React.ReactElement {
- void onSave()}
disabled={!canSave}
- className="rounded bg-[color:var(--accent)] px-4 py-2 text-xs font-semibold text-[color:var(--fg-on-dark)] hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
>
{saving ? 'speichert…' : 'Auswahl speichern'}
-
+
diff --git a/web-ui/app/admin/registries/page.tsx b/web-ui/app/admin/registries/page.tsx
index d0ca28663..7e57aab65 100644
--- a/web-ui/app/admin/registries/page.tsx
+++ b/web-ui/app/admin/registries/page.tsx
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import {
ApiError,
StoredRegistry,
@@ -178,14 +179,13 @@ export default function AdminRegistriesPage(): React.ReactElement {
className={inputCls}
/>
- void onAdd()}
disabled={!canAdd}
- className="rounded-md bg-[color:var(--accent)] px-4 py-2 text-sm font-medium text-[color:var(--text-inverse)] disabled:opacity-50"
>
{adding ? '…' : 'Hinzufügen'}
-
+
@@ -217,16 +217,15 @@ export default function AdminRegistriesPage(): React.ReactElement {
-
editing === r.name ? setEditing(null) : startEdit(r)
}
disabled={pending === r.name}
- className="rounded-md border border-[color:var(--border)] px-3 py-2 text-sm font-medium text-[color:var(--fg-strong)] hover:bg-[color:var(--card)] disabled:opacity-50"
>
{editing === r.name ? 'Abbrechen' : 'Bearbeiten'}
-
+
void onDelete(r)}
@@ -257,23 +256,21 @@ export default function AdminRegistriesPage(): React.ReactElement {
/>
- void onSaveEdit(r)}
disabled={pending === r.name}
- className="rounded-md bg-[color:var(--accent)] px-4 py-2 text-sm font-medium text-[color:var(--text-inverse)] disabled:opacity-50"
>
Speichern
-
+
{r.has_token && (
- void onClearToken(r)}
disabled={pending === r.name}
- className="rounded-md border border-[color:var(--border)] px-3 py-2 text-sm font-medium text-[color:var(--fg-muted)] hover:bg-[color:var(--card)] disabled:opacity-50"
>
Token entfernen
-
+
)}
diff --git a/web-ui/app/admin/settings/page.tsx b/web-ui/app/admin/settings/page.tsx
index 022e45f06..37c1b6c7d 100644
--- a/web-ui/app/admin/settings/page.tsx
+++ b/web-ui/app/admin/settings/page.tsx
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
+import { Button } from '@/app/_components/ui/Button';
import {
getSettings,
patchSettings,
@@ -330,14 +331,13 @@ function SettingRow({
className={`${inputCls} flex-1 sm:min-w-[260px]`}
/>
{s.isSet && (
- onImmediate(s.key, null)}
- className="rounded-md border border-[color:var(--border)] px-3 py-2 text-sm font-medium text-[color:var(--fg-muted)] hover:bg-[color:var(--card)] disabled:opacity-50"
>
Entfernen
-
+
)}
) : (
diff --git a/web-ui/app/admin/users/[id]/page.tsx b/web-ui/app/admin/users/[id]/page.tsx
index 3aae45e56..977cd2a41 100644
--- a/web-ui/app/admin/users/[id]/page.tsx
+++ b/web-ui/app/admin/users/[id]/page.tsx
@@ -4,6 +4,7 @@ import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import {
AdminUser,
ApiError,
@@ -210,13 +211,14 @@ export default function AdminUserEditPage(): React.ReactElement {
{patchMessage}
)}
{patchError && {patchError}
}
-
{savingPatch ? 'Speichere …' : 'Speichern'}
-
+
@@ -240,14 +242,13 @@ export default function AdminUserEditPage(): React.ReactElement {
className="rounded-md border border-[color:var(--border)] bg-transparent px-3 py-2 text-sm outline-none focus:border-[color:var(--accent)]"
/>
- void handleResetPassword()}
disabled={resetting}
- className="rounded-md border border-[color:var(--border)] px-4 py-2 text-sm font-medium hover:bg-[color:var(--card)] disabled:opacity-50"
>
{resetting ? 'Setze …' : 'Zurücksetzen'}
-
+
{resetMessage && (
{resetMessage}
@@ -265,14 +266,13 @@ export default function AdminUserEditPage(): React.ReactElement {
Löschen entfernt den Nutzer permanent. Audit-Log behält den Eintrag.
- void handleDelete()}
disabled={deleting}
- className="rounded-md border border-[color:var(--danger-edge)]/40 px-4 py-2 text-sm font-medium text-[color:var(--danger)] hover:bg-[color:var(--danger)]/10 disabled:opacity-50"
>
{deleting ? 'Lösche …' : 'Nutzer löschen'}
-
+
{deleteError && (
{deleteError}
)}
diff --git a/web-ui/app/admin/users/page.tsx b/web-ui/app/admin/users/page.tsx
index 04c90a5d6..03cd080c6 100644
--- a/web-ui/app/admin/users/page.tsx
+++ b/web-ui/app/admin/users/page.tsx
@@ -3,6 +3,7 @@
import Link from 'next/link';
import { useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import {
AdminUser,
ApiError,
@@ -100,13 +101,12 @@ export default function AdminUsersPage(): React.ReactElement {
Nutzer sich einmal über den IdP angemeldet hat.
- setShowCreate((s) => !s)}
- className="rounded-md bg-[color:var(--accent)] px-4 py-2 text-sm font-medium text-[color:var(--text-inverse)]"
>
{showCreate ? 'Abbrechen' : 'Neuen Nutzer anlegen'}
-
+
{showCreate && (
@@ -147,13 +147,14 @@ export default function AdminUsersPage(): React.ReactElement {
{createError && (
{createError}
)}
-
{submitting ? 'Lege an …' : 'Anlegen'}
-
+
)}
diff --git a/web-ui/app/error.tsx b/web-ui/app/error.tsx
index 39f19695d..4832a93a9 100644
--- a/web-ui/app/error.tsx
+++ b/web-ui/app/error.tsx
@@ -3,6 +3,8 @@
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
+import { Button } from '@/app/_components/ui/Button';
+
/**
* Route-level error boundary. Without it, a thrown error during render of the
* chat page (e.g. a chat session persisted by an older schema, whose drifted
@@ -62,13 +64,9 @@ export default function Error({
>
{t('reload')}
-
+
{t('resetData')}
-
+
{t('resetDataHint')}
diff --git a/web-ui/app/graph/_components/MemoryAclSection.tsx b/web-ui/app/graph/_components/MemoryAclSection.tsx
index 638bbf3cd..833feea1b 100644
--- a/web-ui/app/graph/_components/MemoryAclSection.tsx
+++ b/web-ui/app/graph/_components/MemoryAclSection.tsx
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import type { GraphNode } from './graphTypes';
interface AclAuditEntry {
@@ -276,15 +277,15 @@ export default function MemoryAclSection({
>
+ Owner hinzufügen
- void deleteMemory()}
disabled={busy}
- className="rounded border border-[color:var(--danger-edge)] px-2 py-1 text-[11px] text-[color:var(--danger)] hover:bg-[color:var(--danger)]/10 disabled:opacity-50"
title="Memory hart löschen (Audit-Trail bleibt)"
>
Delete
-
+
diff --git a/web-ui/app/memories/[id]/page.tsx b/web-ui/app/memories/[id]/page.tsx
index fa16c1efe..e60d0f704 100644
--- a/web-ui/app/memories/[id]/page.tsx
+++ b/web-ui/app/memories/[id]/page.tsx
@@ -4,6 +4,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
+
import {
deleteMemory,
getMemory,
@@ -475,14 +477,14 @@ export default function MemoryDetailPage(): React.ReactElement {
>
) : (
<>
- void discard()}
disabled={busy}
- className="rounded border border-[color:var(--danger-edge)] px-3 py-1 text-xs text-[color:var(--danger)] hover:bg-[color:var(--danger)]/8 disabled:opacity-50"
>
Löschen
-
+
{t('platformHeading')}
- {
if (!confirm(t('rehydrateConfirm'))) return;
@@ -140,15 +141,15 @@ export function AgentsDashboard({
title={t('rehydrateTooltip')}
>
{t('actionRehydrateFallback')}
-
-
+ run('reload', () => triggerAgentReload())}
>
{t('forceReload')}
-
+
-
props.onPatch({
@@ -547,10 +548,10 @@ function AgentCard(props: {
{agent.status === 'enabled'
? t('actionDisable')
: t('actionEnable')}
-
-
+
props.onPatch({
@@ -560,10 +561,10 @@ function AgentCard(props: {
}
>
{t('actionTogglePrivacy')}
-
-
+ {
if (confirm(t('deleteConfirm', { slug: agent.slug })))
@@ -571,7 +572,7 @@ function AgentCard(props: {
}}
>
{t('actionDelete')}
-
+
@@ -593,9 +594,9 @@ function AgentCard(props: {
>
{t('actionDrain')}
- {
if (confirm(t('killConfirm', { slug: agent.slug })))
@@ -603,7 +604,7 @@ function AgentCard(props: {
}}
>
{t('actionKill')}
-
+
@@ -711,22 +712,22 @@ function BindingsEditor(props: {
{t('bindingsHeading')}
-
{t('bindingsAdd')}
-
-
+
{t('save')}
-
+
{rows.length === 0 ? (
@@ -775,14 +776,14 @@ function BindingsEditor(props: {
className="flex-1 rounded border border-[color:var(--border)] px-2 py-1 font-mono text-xs"
placeholder="28:bot-id-or-@username"
/>
-
remove(idx)}
>
{t('bindingsRemove')}
-
+
))}
diff --git a/web-ui/app/operator/agents/_components/PluginsDnd.tsx b/web-ui/app/operator/agents/_components/PluginsDnd.tsx
index 650d17ee9..fbc1a489a 100644
--- a/web-ui/app/operator/agents/_components/PluginsDnd.tsx
+++ b/web-ui/app/operator/agents/_components/PluginsDnd.tsx
@@ -23,6 +23,7 @@ import { GripVertical } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
+import { Button } from '@/app/_components/ui/Button';
import type {
OperatorAgentDto,
PluginCatalogEntryDto,
@@ -288,14 +289,14 @@ export function PluginsDnd(props: PluginsDndProps): React.ReactElement {
{t('pluginsHeading')}
-
{t('save')}
-
+
{!attached ? (
-
{t('attach')}
-
+
) : (
<>
{hasFields && (
-
{props.expanded ? t('configHide') : t('configShow')}
-
+
)}
-
{t('detach')}
-
+
>
)}
diff --git a/web-ui/app/page.tsx b/web-ui/app/page.tsx
index 1733d03a8..bc81e350e 100644
--- a/web-ui/app/page.tsx
+++ b/web-ui/app/page.tsx
@@ -1480,18 +1480,19 @@ function FollowUpButtons({
{options.map((opt, idx) => (
- {
onChoose(opt.prompt);
}}
disabled={disabled}
title={opt.prompt}
- className="rounded-full border border-[color:var(--border)] bg-[color:var(--bg-elevated)] px-3 py-1 text-[11px] font-medium text-[color:var(--fg)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--bg-soft)] disabled:cursor-not-allowed disabled:opacity-40"
>
{opt.label}
-
+
))}
diff --git a/web-ui/app/routines/_components/RoutineActions.tsx b/web-ui/app/routines/_components/RoutineActions.tsx
index fe892b387..52a73e701 100644
--- a/web-ui/app/routines/_components/RoutineActions.tsx
+++ b/web-ui/app/routines/_components/RoutineActions.tsx
@@ -3,6 +3,7 @@
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
+import { Button } from '@/app/_components/ui/Button';
import {
deleteRoutine,
setRoutineStatus,
@@ -97,14 +98,15 @@ export function RoutineActions({ routine }: Props): React.ReactElement {
>
Jetzt
-
{isPaused ? 'Resume' : 'Pause'}
-
+
Letzte {HISTORY_LIMIT} Runs
-
{loading ? 'Lädt…' : 'Refresh'}
-
+
{error ? (
diff --git a/web-ui/app/routines/_components/RoutineTemplateEditor.tsx b/web-ui/app/routines/_components/RoutineTemplateEditor.tsx
index 1a4eb5b53..d6044617c 100644
--- a/web-ui/app/routines/_components/RoutineTemplateEditor.tsx
+++ b/web-ui/app/routines/_components/RoutineTemplateEditor.tsx
@@ -3,6 +3,7 @@
import { useRouter } from 'next/navigation';
import { useMemo, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import {
previewRoutineTemplate,
setRoutineTemplate,
@@ -259,14 +260,16 @@ export function RoutineTemplateEditor({ routine }: Props): React.ReactElement {
>
{saving ? 'Speichert…' : 'Speichern'}
-
Verwerfen
-
+
{submitError}
)}
-
- {submitting ? t('submitting') : t('submit')}
-
+
+ {t('submit')}
+
);
diff --git a/web-ui/app/store/builder/[id]/_components/BoundariesSection.tsx b/web-ui/app/store/builder/[id]/_components/BoundariesSection.tsx
index 5847950e8..8c90a466d 100644
--- a/web-ui/app/store/builder/[id]/_components/BoundariesSection.tsx
+++ b/web-ui/app/store/builder/[id]/_components/BoundariesSection.tsx
@@ -3,6 +3,8 @@
import { useTranslations } from 'next-intl';
import { useCallback, useMemo, useState, useTransition } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
import { setQualityConfig } from '../../../../_lib/api';
import {
BOUNDARY_CATEGORY_LABELS_DE,
@@ -178,15 +180,16 @@ export function BoundariesSection({
)}
-
{pending ? t('saving') : t('save')}
-
+
{savedAt && (
{t('saved')}
diff --git a/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx b/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx
index 4e9fee4f4..3a8410a18 100644
--- a/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx
+++ b/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx
@@ -1,5 +1,6 @@
'use client';
+import { Button } from '@/app/_components/ui/Button';
import {
useCallback,
useEffect,
@@ -602,15 +603,15 @@ export function BuilderChatPane({
{t('button.stop')}
) : (
- void onSend()}
disabled={input.trim().length === 0}
- className="inline-flex h-[44px] shrink-0 items-center gap-2 rounded-md bg-[color:var(--accent)] px-3 py-2 text-[12px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] transition-opacity hover:opacity-90 disabled:opacity-40"
+ className="h-[44px] shrink-0 text-[12px]"
>
{t('button.send')}
-
+
)}
{inflight ? (
diff --git a/web-ui/app/store/builder/[id]/_components/CulturePresetDropdown.tsx b/web-ui/app/store/builder/[id]/_components/CulturePresetDropdown.tsx
index a95644e04..45490a715 100644
--- a/web-ui/app/store/builder/[id]/_components/CulturePresetDropdown.tsx
+++ b/web-ui/app/store/builder/[id]/_components/CulturePresetDropdown.tsx
@@ -3,6 +3,8 @@
import { useTranslations } from 'next-intl';
import { useCallback, useMemo, useState, useTransition } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
import { setPersonaConfig } from '../../../../_lib/api';
import {
CULTURE_PRESETS,
@@ -121,15 +123,16 @@ export function CulturePresetDropdown({
)}
-
{pending ? t('applying') : t('apply')}
-
+
{canBump ? (
-
void onBumpAndRetry()}
- className="inline-flex items-center gap-2 rounded-md bg-[color:var(--accent)] px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--fg-on-dark)] shadow-sm transition-opacity hover:opacity-90"
+ className="text-[11px] uppercase tracking-[0.16em]"
>
{t('bumpAndPublish', { current: currentVersion, next: bumped ?? '' })}
-
+
) : null}
{chunks},
})}
-
{t('addFirstPage')}
-
+
);
}
diff --git a/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx b/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx
index fbda281ed..7d87e188c 100644
--- a/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx
+++ b/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx
@@ -3,6 +3,8 @@
import { useTranslations } from 'next-intl';
import { useCallback, useState, useTransition } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
import { patchBuilderSpec, setPersonaConfig } from '../../../../_lib/api';
import {
PERSONA_TEMPLATES,
@@ -192,15 +194,16 @@ export function PersonaTemplateGallery({
>
{t('cancel')}
-
{pending ? t('applying') : t('apply')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/PreviewChatPane.tsx b/web-ui/app/store/builder/[id]/_components/PreviewChatPane.tsx
index 3f1e7ef4d..b882bcdc9 100644
--- a/web-ui/app/store/builder/[id]/_components/PreviewChatPane.tsx
+++ b/web-ui/app/store/builder/[id]/_components/PreviewChatPane.tsx
@@ -1,5 +1,6 @@
'use client';
+import { Button } from '@/app/_components/ui/Button';
import {
useCallback,
useEffect,
@@ -587,15 +588,15 @@ export function PreviewChatPane({
{t('stop')}
) : (
- void onSend()}
disabled={input.trim().length === 0}
- className="inline-flex h-[44px] shrink-0 items-center gap-2 rounded-md bg-[color:var(--accent)] px-3 py-2 text-[12px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] disabled:opacity-40"
+ className="h-[44px] shrink-0 text-[12px]"
>
{t('send')}
-
+
)}
{inflight ? (
diff --git a/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx b/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx
index 8ecde05cb..06eec66db 100644
--- a/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx
+++ b/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx
@@ -2,6 +2,8 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
+
+import { Button } from '@/app/_components/ui/Button';
import { AnimatePresence, motion } from 'framer-motion';
import { CheckCircle2, KeyRound, Trash2, X } from 'lucide-react';
@@ -215,11 +217,11 @@ export function SecretsDrawer({
{t('clearAll')}
- void onSave()}
disabled={pending || fields.length === 0}
- className="ml-auto inline-flex items-center gap-2 rounded-md bg-[color:var(--accent)] px-3 py-2 text-[12px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] disabled:opacity-50"
+ className="ml-auto text-[12px]"
>
{pending ? (
@@ -227,7 +229,7 @@ export function SecretsDrawer({
)}
{t('apply')}
-
+
>
diff --git a/web-ui/app/store/builder/[id]/_components/SetupFieldsEditor.tsx b/web-ui/app/store/builder/[id]/_components/SetupFieldsEditor.tsx
index 92736c17a..4cae8b200 100644
--- a/web-ui/app/store/builder/[id]/_components/SetupFieldsEditor.tsx
+++ b/web-ui/app/store/builder/[id]/_components/SetupFieldsEditor.tsx
@@ -2,6 +2,8 @@
import { useCallback, useId, useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
+
+import { Button } from '@/app/_components/ui/Button';
import { Plus, Trash2 } from 'lucide-react';
import { ApiError, patchBuilderSpec } from '../../../../_lib/api';
@@ -314,8 +316,8 @@ function NewFieldForm({
{t('required.on')}
- {
if (!valid) return;
onSubmit({ key: trimmed, type, required });
@@ -323,11 +325,11 @@ function NewFieldForm({
setRequired(false);
}}
disabled={!valid}
- className="ml-auto inline-flex items-center gap-1 rounded-md bg-[color:var(--accent)] px-3 py-2 text-[11px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] disabled:opacity-40"
+ className="ml-auto gap-1 text-[11px]"
>
{t('add')}
-
+
{key && !valid ? (
diff --git a/web-ui/app/store/builder/[id]/_components/SimpleIntakePane.tsx b/web-ui/app/store/builder/[id]/_components/SimpleIntakePane.tsx
index 642c56330..201eb80bb 100644
--- a/web-ui/app/store/builder/[id]/_components/SimpleIntakePane.tsx
+++ b/web-ui/app/store/builder/[id]/_components/SimpleIntakePane.tsx
@@ -2,6 +2,8 @@
import { useCallback, useEffect, useId, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
+
+import { Button } from '@/app/_components/ui/Button';
import { motion } from 'framer-motion';
import { Send, Sparkles, StopCircle } from 'lucide-react';
@@ -316,15 +318,16 @@ export function SimpleIntakePane({
{t('stop')}
) : (
- void onSend()}
disabled={input.trim().length === 0}
- className="inline-flex h-[50px] shrink-0 items-center gap-2 rounded-full bg-[color:var(--accent)] px-4 text-[14px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] transition-all hover:opacity-90 disabled:opacity-40 disabled:shadow-none"
+ className="h-[50px] shrink-0 text-[14px]"
>
{t('send')}
-
+
)}
diff --git a/web-ui/app/store/builder/[id]/_components/SlotEditor.tsx b/web-ui/app/store/builder/[id]/_components/SlotEditor.tsx
index 2be3497d8..b57ae6452 100644
--- a/web-ui/app/store/builder/[id]/_components/SlotEditor.tsx
+++ b/web-ui/app/store/builder/[id]/_components/SlotEditor.tsx
@@ -1,5 +1,6 @@
'use client';
+import { Button } from '@/app/_components/ui/Button';
import {
useCallback,
useEffect,
@@ -401,15 +402,16 @@ export function SlotEditor({
))}
- void flush()}
disabled={status.kind === 'pending'}
- className="ml-auto inline-flex items-center gap-2 rounded-md bg-[color:var(--accent)] px-3 py-1 text-[11px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] disabled:opacity-50"
+ className="ml-auto text-[11px]"
>
{t('save')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/SpecEditor.tsx b/web-ui/app/store/builder/[id]/_components/SpecEditor.tsx
index dfb20a99a..d13abb974 100644
--- a/web-ui/app/store/builder/[id]/_components/SpecEditor.tsx
+++ b/web-ui/app/store/builder/[id]/_components/SpecEditor.tsx
@@ -2,6 +2,8 @@
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
+
+import { Button } from '@/app/_components/ui/Button';
import { AlertCircle, Check, Plus, X } from 'lucide-react';
import { ApiError, patchBuilderSpec } from '../../../../_lib/api';
@@ -540,15 +542,15 @@ function ArrayField({
mono && 'font-mono-num',
)}
/>
-
{t('array.add')}
-
+
);
diff --git a/web-ui/app/store/builder/[id]/_components/ToolBulkImportModal.tsx b/web-ui/app/store/builder/[id]/_components/ToolBulkImportModal.tsx
index 1f249f455..d0b8eb92e 100644
--- a/web-ui/app/store/builder/[id]/_components/ToolBulkImportModal.tsx
+++ b/web-ui/app/store/builder/[id]/_components/ToolBulkImportModal.tsx
@@ -4,6 +4,7 @@ import { AlertTriangle, Check, Upload, X } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useMemo, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import type { JsonPatch, ToolSpec } from '../../../../_lib/builderTypes';
import { cn } from '../../../../_lib/cn';
import {
@@ -158,8 +159,9 @@ export function ToolBulkImportModal({
{t('collisionNote')}
- void onConfirm()}
disabled={
pending ||
@@ -167,7 +169,7 @@ export function ToolBulkImportModal({
result.tools.length === 0 ||
result.tools.length === collisions.length
}
- className="inline-flex items-center gap-1 rounded bg-[color:var(--accent)] px-3 py-1 text-[11px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] disabled:opacity-50"
+ className="gap-1 text-[11px]"
>
{pending ? (
@@ -175,7 +177,7 @@ export function ToolBulkImportModal({
)}
{t('confirmImport')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/ToolInputSchemaRow.tsx b/web-ui/app/store/builder/[id]/_components/ToolInputSchemaRow.tsx
index c6944f709..11c8d2ee1 100644
--- a/web-ui/app/store/builder/[id]/_components/ToolInputSchemaRow.tsx
+++ b/web-ui/app/store/builder/[id]/_components/ToolInputSchemaRow.tsx
@@ -2,6 +2,8 @@
import { ChevronDown, ChevronRight, X } from 'lucide-react';
import { useTranslations } from 'next-intl';
+
+import { Button } from '@/app/_components/ui/Button';
import { useEffect, useId, useState } from 'react';
import { cn } from '../../../../_lib/cn';
@@ -454,18 +456,19 @@ function EnumValuesEditor({
placeholder={t('valueEnterPlaceholder')}
className="flex-1 rounded border border-[color:var(--border)] bg-[color:var(--bg)] px-2 py-1 font-mono-num text-[11px] text-[color:var(--fg-strong)] focus:border-[color:var(--accent)] focus:outline-none"
/>
- {
if (!draft.trim()) return;
onChange([...values, draft.trim()]);
setDraft('');
}}
disabled={!draft.trim()}
- className="rounded bg-[color:var(--accent)] px-2 py-1 text-[11px] font-semibold text-[color:var(--fg-on-dark)] disabled:opacity-40"
+ className="text-[11px]"
>
+
-
+
);
diff --git a/web-ui/app/store/builder/[id]/_components/ToolList.tsx b/web-ui/app/store/builder/[id]/_components/ToolList.tsx
index 3daf511f0..a739984cf 100644
--- a/web-ui/app/store/builder/[id]/_components/ToolList.tsx
+++ b/web-ui/app/store/builder/[id]/_components/ToolList.tsx
@@ -1,5 +1,6 @@
'use client';
+import { Button } from '@/app/_components/ui/Button';
import {
closestCenter,
DndContext,
@@ -149,14 +150,14 @@ export function ToolList({
{t('emptyState')}
-
{t('addFirstTool')}
-
+
setTemplatesOpen(true)}
diff --git a/web-ui/app/store/builder/[id]/_components/ToolTestModal.tsx b/web-ui/app/store/builder/[id]/_components/ToolTestModal.tsx
index 3269f059a..3ca9109f3 100644
--- a/web-ui/app/store/builder/[id]/_components/ToolTestModal.tsx
+++ b/web-ui/app/store/builder/[id]/_components/ToolTestModal.tsx
@@ -2,6 +2,8 @@
import { Play, Save, X } from 'lucide-react';
import { useTranslations } from 'next-intl';
+
+import { Button } from '@/app/_components/ui/Button';
import { useCallback, useEffect, useId, useMemo, useState } from 'react';
import { ApiError, runBuilderPreviewToolCall } from '../../../../_lib/api';
@@ -183,11 +185,12 @@ export function ToolTestModal({
{t('saveTestCase')}
) : null}
-
void onRun()}
disabled={pending}
- className="inline-flex items-center gap-1 rounded bg-[color:var(--accent)] px-3 py-1 text-[11px] font-semibold text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)] disabled:opacity-50"
+ className="gap-1 text-[11px]"
>
{pending ? (
@@ -195,7 +198,7 @@ export function ToolTestModal({
)}
{t('run')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/VersionsTab.tsx b/web-ui/app/store/builder/[id]/_components/VersionsTab.tsx
index 5daefffc6..8fb763f0a 100644
--- a/web-ui/app/store/builder/[id]/_components/VersionsTab.tsx
+++ b/web-ui/app/store/builder/[id]/_components/VersionsTab.tsx
@@ -3,6 +3,8 @@
import { useTranslations } from 'next-intl';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
import {
captureSnapshot,
getSnapshotDiff,
@@ -96,13 +98,13 @@ export function VersionsTab({ draftId }: VersionsTabProps): React.ReactElement {
return (
- setCaptureOpen(true)}
- className="rounded-md bg-[var(--accent)] px-3 py-2 text-sm font-medium text-[color:var(--fg-on-dark)] hover:opacity-90"
+ className="text-sm"
>
{t('createSnapshot')}
-
+
void refresh()}
@@ -388,14 +390,14 @@ function CaptureModal({
>
{t('cancel')}
- void submit()}
disabled={busy}
- className="rounded-md bg-[var(--accent)] px-3 py-2 text-sm font-medium text-[color:var(--fg-on-dark)] disabled:opacity-50"
+ className="text-sm"
>
{t('create')}
-
+
);
diff --git a/web-ui/app/store/builder/_components/CreateDraftButton.tsx b/web-ui/app/store/builder/_components/CreateDraftButton.tsx
index 7ffdfe2b8..ae59a64b2 100644
--- a/web-ui/app/store/builder/_components/CreateDraftButton.tsx
+++ b/web-ui/app/store/builder/_components/CreateDraftButton.tsx
@@ -7,7 +7,7 @@ import { Plus } from 'lucide-react';
import { ApiError, createBuilderDraft } from '../../../_lib/api';
import type { DraftQuotaSnapshot } from '../../../_lib/builderTypes';
-import { cn } from '../../../_lib/cn';
+import { Button } from '@/app/_components/ui/Button';
/**
* Primary "neuer Agent"-Action. One click creates a draft row via POST
@@ -50,22 +50,16 @@ export function CreateDraftButton({
return (
-
{pending ? t('pending') : t('label')}
-
+
{error ? (
{error}
diff --git a/web-ui/app/store/builder/_components/ImportBundleButton.tsx b/web-ui/app/store/builder/_components/ImportBundleButton.tsx
index ffcb2da19..ef74c7801 100644
--- a/web-ui/app/store/builder/_components/ImportBundleButton.tsx
+++ b/web-ui/app/store/builder/_components/ImportBundleButton.tsx
@@ -8,6 +8,7 @@ import { Upload, FileArchive } from 'lucide-react';
import { ApiError, importProfileBundle } from '../../../_lib/api';
import type { ImportBundleSuccess } from '../../../_lib/profileTypes';
import { cn } from '../../../_lib/cn';
+import { Button } from '@/app/_components/ui/Button';
/**
* Phase 2.4 (OB-66) — Bundle-Import-Button + Drag-Drop-Modal.
@@ -28,20 +29,10 @@ export function ImportBundleButton(): React.ReactElement {
return (
<>
- setOpen(true)}
- className={cn(
- 'inline-flex items-center gap-2 rounded-full px-4 py-2 text-[12px] font-semibold',
- 'border border-[color:var(--border)] bg-[color:var(--bg-soft)] text-[color:var(--fg)]',
- 'transition-colors duration-[var(--dur-base)]',
- 'hover:bg-[color:var(--gray-100)] hover:text-[color:var(--fg-strong)]',
- 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--accent)]',
- )}
- >
+ setOpen(true)}>
{t('button')}
-
+
{open ? (
setOpen(false)}
@@ -234,24 +225,18 @@ function ImportBundleModal({
) : null}
-
+
{t('modal.cancel')}
-
-
+ void onSubmit()}
disabled={!file || busy}
- className={cn(
- 'rounded-md bg-[color:var(--accent)] px-3 py-2 text-sm font-medium text-[color:var(--fg-on-dark)]',
- 'shadow-[var(--shadow-cta)] disabled:opacity-50',
- )}
+ busy={busy}
+ busyLabel={t('modal.importing')}
>
- {busy ? t('modal.importing') : t('modal.import')}
-
+ {t('modal.import')}
+
diff --git a/web-ui/vitest.config.ts b/web-ui/vitest.config.ts
index a36467e64..88c473be2 100644
--- a/web-ui/vitest.config.ts
+++ b/web-ui/vitest.config.ts
@@ -22,7 +22,11 @@ export default defineConfig({
},
resolve: {
alias: {
- '@': path.resolve(__dirname, './app'),
+ // Match tsconfig's `@/*` -> `./*` (web-ui root), so `@/app/...` resolves
+ // the same in vitest as in tsc and Next. (The previous `@` -> ./app
+ // diverged from tsconfig; it surfaced once the first `@/app/...` imports
+ // landed.)
+ '@': path.resolve(__dirname, '.'),
},
},
});
From c883f5949b30dcf6fc2b4e2c87f1d39653fad7f7 Mon Sep 17 00:00:00 2001
From: Marcel Wege
Date: Sat, 13 Jun 2026 16:49:59 +0200
Subject: [PATCH 4/5] =?UTF-8?q?feat(web-ui):=20pass=202=20=E2=80=94=20broa?=
=?UTF-8?q?den=20button=20migration=20to=20spec=20variants=20(#290)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Mapped bespoke patterns onto the 4 spec variants (no new variants, per §10):
bg-inverse fills -> primary, neutral-bordered-no-bg -> secondary,
danger-with-/NN-border -> danger. ~68 more buttons across builder/store/
admin/operator/graph. typecheck 0, build green, lint 0 errors, vitest 157/162.
Warning fills and solid-danger fills left for manual per-intent mapping next.
---
.../app/_components/store/InstallButton.tsx | 41 +++++------
.../app/_components/store/RequiresWizard.tsx | 20 +++---
.../_components/store/SelfExtensionPanel.tsx | 10 +--
web-ui/app/admin/auth/page.tsx | 22 ++----
web-ui/app/admin/bulk-promote/page.tsx | 18 +++--
web-ui/app/admin/duplicates/page.tsx | 24 ++++---
web-ui/app/admin/inconsistencies/page.tsx | 12 ++--
web-ui/app/admin/registries/page.tsx | 12 ++--
web-ui/app/admin/topics/page.tsx | 14 ++--
web-ui/app/graph/_components/DetailPanel.tsx | 33 +++++----
web-ui/app/graph/_components/ListView.tsx | 11 +--
.../graph/_components/MemoryAclSection.tsx | 9 +--
web-ui/app/memories/[id]/page.tsx | 68 +++++++++++--------
web-ui/app/memories/page.tsx | 9 +--
web-ui/app/memory/page.tsx | 10 +--
.../agents/_components/AgentsDashboard.tsx | 16 +++--
.../routines/_components/RoutineActions.tsx | 20 +++---
.../_components/RoutineTemplateEditor.tsx | 28 +++++---
.../[id]/_components/AuditTimelinePane.tsx | 10 +--
.../[id]/_components/BuilderChatPane.tsx | 8 +--
.../_components/CulturePresetDropdown.tsx | 9 +--
.../[id]/_components/InstallDiffModal.tsx | 22 +++---
.../builder/[id]/_components/PagesList.tsx | 8 +--
.../[id]/_components/PersonaPillar.tsx | 30 ++++----
.../_components/PersonaTemplateGallery.tsx | 18 ++---
.../[id]/_components/PreviewChatPane.tsx | 17 ++---
.../[id]/_components/PreviewPromptPanel.tsx | 18 ++---
.../builder/[id]/_components/QualityPanel.tsx | 10 +--
.../[id]/_components/SecretsDrawer.tsx | 8 +--
.../[id]/_components/SimpleIntakePane.tsx | 18 ++---
.../builder/[id]/_components/ToolForm.tsx | 10 +--
.../_components/ToolInputSchemaBuilder.tsx | 10 ++-
.../builder/[id]/_components/ToolList.tsx | 36 +++++-----
.../[id]/_components/ToolTestModal.tsx | 9 +--
.../builder/[id]/_components/VersionsTab.tsx | 24 ++-----
.../builder/[id]/_components/Workspace.tsx | 24 +++----
.../store/builder/_components/DraftRow.tsx | 10 +--
37 files changed, 352 insertions(+), 324 deletions(-)
diff --git a/web-ui/app/_components/store/InstallButton.tsx b/web-ui/app/_components/store/InstallButton.tsx
index 651e52454..2f5ee9ed4 100644
--- a/web-ui/app/_components/store/InstallButton.tsx
+++ b/web-ui/app/_components/store/InstallButton.tsx
@@ -109,14 +109,12 @@ export function InstallButton({
if (!enabled) {
return (
-
@@ -125,7 +123,7 @@ export function InstallButton({
Installation blockiert
-
+
{blockingReasons?.length ? (
-
@@ -174,7 +167,7 @@ export function InstallButton({
aria-hidden
/>
)}
-
+
{drawerOpen ? (
- setState({ kind: 'idle' })}
- className="rounded-full bg-[color:var(--bg)] px-4 py-2 text-[12px] font-semibold text-[color:var(--fg-muted)] ring-1 ring-inset ring-[color:var(--border)]"
+ className="font-semibold text-[color:var(--fg-muted)]"
>
Abbrechen
-
+
)}
diff --git a/web-ui/app/_components/store/RequiresWizard.tsx b/web-ui/app/_components/store/RequiresWizard.tsx
index 15573273c..353cfc304 100644
--- a/web-ui/app/_components/store/RequiresWizard.tsx
+++ b/web-ui/app/_components/store/RequiresWizard.tsx
@@ -196,13 +196,15 @@ export function RequiresWizard({
{phase.kind === 'review' ? (
<>
-
Abbrechen
-
+
) : phase.kind === 'error' ? (
-
Schließen
-
+
) : null}
diff --git a/web-ui/app/_components/store/SelfExtensionPanel.tsx b/web-ui/app/_components/store/SelfExtensionPanel.tsx
index 4465c53ec..4989bc711 100644
--- a/web-ui/app/_components/store/SelfExtensionPanel.tsx
+++ b/web-ui/app/_components/store/SelfExtensionPanel.tsx
@@ -380,15 +380,17 @@ export function SelfExtensionPanel({ agentId }: { agentId: string }): React.Reac
{t('approve')}
- setDenyFor(p.id)}
- className="inline-flex items-center gap-2 rounded-full bg-[color:var(--bg-soft)] px-3 py-1 text-[12px] font-semibold text-[color:var(--fg-muted)] ring-1 ring-inset ring-[color:var(--border)]"
+ className="font-semibold text-[color:var(--fg-muted)]"
>
{t('deny')}
-
+
)
) : null}
diff --git a/web-ui/app/admin/auth/page.tsx b/web-ui/app/admin/auth/page.tsx
index d0885c5f7..babe671db 100644
--- a/web-ui/app/admin/auth/page.tsx
+++ b/web-ui/app/admin/auth/page.tsx
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
import {
AdminAuthProvider,
ApiError,
@@ -126,23 +127,14 @@ export default function AdminAuthPage(): React.ReactElement {
{p.active ? 'aktiv' : 'inaktiv'}
- void toggle(p)}
- disabled={pendingId === p.id}
- className={[
- 'rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:opacity-50',
- p.active
- ? 'border border-[color:var(--border)] text-[color:var(--fg-strong)] hover:bg-[color:var(--card)]'
- : 'bg-[color:var(--accent)] text-[color:var(--text-inverse)]',
- ].join(' ')}
+ busy={pendingId === p.id}
+ busyLabel={p.active ? 'Deaktivieren' : 'Aktivieren'}
>
- {pendingId === p.id
- ? '…'
- : p.active
- ? 'Deaktivieren'
- : 'Aktivieren'}
-
+ {p.active ? 'Deaktivieren' : 'Aktivieren'}
+
))}
diff --git a/web-ui/app/admin/bulk-promote/page.tsx b/web-ui/app/admin/bulk-promote/page.tsx
index f6d84b1e4..a4f046d87 100644
--- a/web-ui/app/admin/bulk-promote/page.tsx
+++ b/web-ui/app/admin/bulk-promote/page.tsx
@@ -195,18 +195,16 @@ export default function BulkPromotePage(): React.ReactElement {
>
Vorschau aktualisieren
- void trigger()}
- disabled={
- running ||
- preview === null ||
- !preview.scorerAvailable
- }
- className="rounded bg-[color:var(--bg-inverse)] px-3 py-1 text-xs text-[color:var(--fg-on-dark)] hover:bg-[color:var(--fg-muted)] disabled:opacity-50"
+ disabled={running || preview === null || !preview.scorerAvailable}
+ busy={running}
+ busyLabel="läuft"
>
- {running ? 'läuft…' : 'Bulk-Job starten'}
-
+ Bulk-Job starten
+
diff --git a/web-ui/app/admin/duplicates/page.tsx b/web-ui/app/admin/duplicates/page.tsx
index 03e2e407b..eb8b005a4 100644
--- a/web-ui/app/admin/duplicates/page.tsx
+++ b/web-ui/app/admin/duplicates/page.tsx
@@ -301,16 +301,18 @@ export default function DuplicatesListPage(): React.ReactElement {
}}
className="w-20 rounded border border-[color:var(--border)] bg-transparent px-2 py-1 text-xs"
/>
- void triggerBulkRun()}
disabled={
bulkRunning || (bulkPreview !== null && bulkPreview.unchecked === 0)
}
- className="rounded border border-[color:var(--border-strong)] bg-[color:var(--bg-inverse)] px-3 py-1 text-xs text-[color:var(--fg-on-dark)] disabled:cursor-not-allowed disabled:opacity-50"
+ busy={bulkRunning}
+ busyLabel="läuft"
>
- {bulkRunning ? 'läuft…' : 'Bulk-Detect starten'}
-
+ Bulk-Detect starten
+
{bulkLimit > BULK_CONFIRM_THRESHOLD && (
⚠️ Confirm bei Limit > {BULK_CONFIRM_THRESHOLD}
@@ -500,17 +502,19 @@ export default function DuplicatesListPage(): React.ReactElement {
}}
className="w-20 rounded border border-[color:var(--border)] bg-transparent px-2 py-1 text-xs"
/>
- void triggerExcerptBulkRun()}
disabled={
excerptBulkRunning ||
(excerptBulkPreview !== null && excerptBulkPreview.unchecked === 0)
}
- className="rounded border border-[color:var(--border-strong)] bg-[color:var(--bg-inverse)] px-3 py-1 text-xs text-[color:var(--fg-on-dark)] disabled:cursor-not-allowed disabled:opacity-50"
+ busy={excerptBulkRunning}
+ busyLabel="läuft"
>
- {excerptBulkRunning ? 'läuft…' : 'Bulk-Detect starten'}
-
+ Bulk-Detect starten
+
{excerptBulkLimit > BULK_CONFIRM_THRESHOLD && (
⚠️ Confirm bei Limit > {BULK_CONFIRM_THRESHOLD}
diff --git a/web-ui/app/admin/inconsistencies/page.tsx b/web-ui/app/admin/inconsistencies/page.tsx
index 8bfd7f1d0..f663cd6e5 100644
--- a/web-ui/app/admin/inconsistencies/page.tsx
+++ b/web-ui/app/admin/inconsistencies/page.tsx
@@ -229,18 +229,20 @@ export default function InconsistenciesListPage(): React.ReactElement {
}}
className="w-20 rounded border border-[color:var(--border)] bg-transparent px-2 py-1 text-xs"
/>
- void triggerBulkRun()}
disabled={
bulkRunning ||
bulkPreview?.detectorAvailable === false ||
(bulkPreview !== null && bulkPreview.unchecked === 0)
}
- className="rounded border border-[color:var(--border-strong)] bg-[color:var(--bg-inverse)] px-3 py-1 text-xs text-[color:var(--fg-on-dark)] disabled:cursor-not-allowed disabled:opacity-50"
+ busy={bulkRunning}
+ busyLabel="läuft"
>
- {bulkRunning ? 'läuft…' : 'Bulk-Detect starten'}
-
+ Bulk-Detect starten
+
{bulkLimit > BULK_CONFIRM_THRESHOLD && (
⚠️ Cost-Confirm bei Limit > {BULK_CONFIRM_THRESHOLD}
diff --git a/web-ui/app/admin/registries/page.tsx b/web-ui/app/admin/registries/page.tsx
index 7e57aab65..11c9164cd 100644
--- a/web-ui/app/admin/registries/page.tsx
+++ b/web-ui/app/admin/registries/page.tsx
@@ -226,14 +226,14 @@ export default function AdminRegistriesPage(): React.ReactElement {
>
{editing === r.name ? 'Abbrechen' : 'Bearbeiten'}
- void onDelete(r)}
- disabled={pending === r.name}
- className="rounded-md border border-[color:var(--border)] px-3 py-2 text-sm font-medium text-[color:var(--danger)] hover:bg-[color:var(--danger)]/10 disabled:opacity-50"
+ busy={pending === r.name}
+ busyLabel="Entfernen"
>
- {pending === r.name ? '…' : 'Entfernen'}
-
+ Entfernen
+
diff --git a/web-ui/app/admin/topics/page.tsx b/web-ui/app/admin/topics/page.tsx
index 0e7e48e11..65f167506 100644
--- a/web-ui/app/admin/topics/page.tsx
+++ b/web-ui/app/admin/topics/page.tsx
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
import {
listTopics,
reclusterTopics,
@@ -130,14 +131,17 @@ export default function TopicsListPage(): React.ReactElement {
/>
- void triggerRecluster()}
disabled={running}
- className="mt-3 rounded border border-[color:var(--border-strong)] bg-[color:var(--bg-inverse)] px-3 py-1 text-xs text-[color:var(--fg-on-dark)] disabled:cursor-not-allowed disabled:opacity-50"
+ busy={running}
+ busyLabel="läuft"
+ className="mt-3"
>
- {running ? 'läuft…' : 'Re-Cluster starten'}
-
+ Re-Cluster starten
+
{runError !== null && (
Fehler: {runError}
diff --git a/web-ui/app/graph/_components/DetailPanel.tsx b/web-ui/app/graph/_components/DetailPanel.tsx
index 7f23b7063..dc741ec1c 100644
--- a/web-ui/app/graph/_components/DetailPanel.tsx
+++ b/web-ui/app/graph/_components/DetailPanel.tsx
@@ -1,5 +1,6 @@
'use client';
+import { Button } from '@/app/_components/ui/Button';
import {
type GraphNode,
type MemoryWithAncestors,
@@ -92,14 +93,18 @@ export default function DetailPanel({
))}
- onExpand(node.id)}
disabled={loadingNeighbors}
- className="w-full rounded border border-[color:var(--border)] px-2 py-1 text-[11px] font-semibold hover:border-[color:var(--border-strong)] disabled:opacity-50"
+ busy={loadingNeighbors}
+ busyLabel="lade Nachbarn…"
+ className="text-[11px] font-semibold"
>
- {loadingNeighbors ? 'lade Nachbarn…' : '↔ Nachbarn expandieren'}
-
+ ↔ Nachbarn expandieren
+
{node.type === 'MemorableKnowledge' && (
{neighbors.map((n) => (
- onSelect(n)}
- className="flex items-start gap-2 rounded border border-[color:var(--border)] px-2 py-1 text-left hover:border-[color:var(--border-strong)]"
+ className="items-start justify-start px-2 py-1 text-left"
>
-
+
))}
@@ -258,11 +264,12 @@ function ProvenanceList({
{nodes.map((n) => (
- onSelect(n)}
- className="flex items-start gap-2 rounded border border-[color:var(--border)] bg-[color:var(--bg-elevated)] px-2 py-1 text-left hover:border-[color:var(--accent)]"
+ className="items-start justify-start px-2 py-1 text-left hover:border-[color:var(--accent)]"
>
-
+
))}
diff --git a/web-ui/app/graph/_components/ListView.tsx b/web-ui/app/graph/_components/ListView.tsx
index f38478c48..269ccead7 100644
--- a/web-ui/app/graph/_components/ListView.tsx
+++ b/web-ui/app/graph/_components/ListView.tsx
@@ -1,6 +1,8 @@
'use client';
import { useState } from 'react';
+
+import { Button } from '@/app/_components/ui/Button';
import type {
GraphNode,
NodeType,
@@ -72,13 +74,14 @@ function TurnCard({
{time}
{tools !== undefined && tools={String(tools)}}
-
{open ? '▾' : '▸'} Run-Trace
-
+
{user}
diff --git a/web-ui/app/graph/_components/MemoryAclSection.tsx b/web-ui/app/graph/_components/MemoryAclSection.tsx
index 833feea1b..8ae17af67 100644
--- a/web-ui/app/graph/_components/MemoryAclSection.tsx
+++ b/web-ui/app/graph/_components/MemoryAclSection.tsx
@@ -269,14 +269,15 @@ export default function MemoryAclSection({
disabled={busy}
/>
- void addOwner()}
disabled={busy || addInput.trim().length === 0}
- className="grow rounded border border-[color:var(--border)] px-2 py-1 text-[11px] hover:border-[color:var(--border-strong)] disabled:opacity-50"
+ className="grow px-2 py-1 text-[11px]"
>
+ Owner hinzufügen
-
+
{editing ? (
<>
-
Abbrechen
-
-
+ void save()}
disabled={busy || summary.trim().length === 0}
- className="rounded bg-[color:var(--bg-inverse)] px-3 py-1 text-xs text-[color:var(--fg-on-dark)] hover:bg-[color:var(--fg-muted)] disabled:opacity-50"
+ busy={busy}
+ busyLabel="speichert…"
>
- {busy ? 'speichert…' : 'Speichern'}
-
+ Speichern
+
>
) : (
<>
@@ -485,14 +487,14 @@ export default function MemoryDetailPage(): React.ReactElement {
>
Löschen
-
Bearbeiten
-
+
>
)}
@@ -552,21 +554,23 @@ export default function MemoryDetailPage(): React.ReactElement {
{!isEditing && (
- void copyExcerpt(ex)}
- className="rounded border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[color:var(--fg-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--fg-strong)]"
+ className="px-2 py-0.5 text-[10px] text-[color:var(--fg-muted)]"
title="Snippet in die Zwischenablage kopieren"
>
{justCopied ? '✓ kopiert' : 'kopieren'}
-
-
+ startExcerptEdit(ex)}
- className="rounded border border-[color:var(--border)] px-2 py-0.5 text-[10px] text-[color:var(--fg-muted)] hover:border-[color:var(--border-strong)] hover:text-[color:var(--fg-strong)]"
+ className="px-2 py-0.5 text-[10px] text-[color:var(--fg-muted)]"
>
bearbeiten
-
+
)}
@@ -608,24 +612,28 @@ export default function MemoryDetailPage(): React.ReactElement {
))}
-
Abbrechen
-
-
+ void saveExcerptEdit()}
disabled={
excerptBusy || excerptDraft.trim().length === 0
}
- className="rounded bg-[color:var(--bg-inverse)] px-2 py-0.5 text-[10px] text-[color:var(--fg-on-dark)] hover:bg-[color:var(--fg-muted)] disabled:opacity-50"
+ busy={excerptBusy}
+ busyLabel="speichert…"
+ className="px-2 py-0.5 text-[10px]"
>
- {excerptBusy ? 'speichert…' : 'Speichern'}
-
+ Speichern
+
diff --git a/web-ui/app/memories/page.tsx b/web-ui/app/memories/page.tsx
index 95ade3ef7..4a5ed1e28 100644
--- a/web-ui/app/memories/page.tsx
+++ b/web-ui/app/memories/page.tsx
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
+import { Button } from '@/app/_components/ui/Button';
import {
listMemories,
type MemorableKind,
@@ -80,13 +81,13 @@ export default function MemoriesPage(): React.ReactElement {
Kuratiertes Wissen — ACL-gefiltert auf dich als Owner.
- void load()}
- className="rounded border border-[color:var(--border)] px-2 py-1 text-xs hover:border-[color:var(--border-strong)]"
>
↻ neu laden
-
+
{KIND_FILTERS.map((k) => {
diff --git a/web-ui/app/memory/page.tsx b/web-ui/app/memory/page.tsx
index c8cb31f58..2f7fc7196 100644
--- a/web-ui/app/memory/page.tsx
+++ b/web-ui/app/memory/page.tsx
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from 'react';
import { Markdown } from '../_components/Markdown';
+import { Button } from '@/app/_components/ui/Button';
import { getMemoryBackend, type MemoryBackend } from '../_lib/api';
interface Entry {
@@ -246,13 +247,14 @@ export default function MemoryPage(): React.ReactElement {
{selected}
- void loadFile(selected)}
- className="rounded border border-[color:var(--border)] px-2 py-0.5 hover:border-[color:var(--border-strong)]"
+ className="px-2 py-0.5"
>
↻
-
+
diff --git a/web-ui/app/operator/agents/_components/AgentsDashboard.tsx b/web-ui/app/operator/agents/_components/AgentsDashboard.tsx
index 4c59d3d30..5d28f385a 100644
--- a/web-ui/app/operator/agents/_components/AgentsDashboard.tsx
+++ b/web-ui/app/operator/agents/_components/AgentsDashboard.tsx
@@ -344,13 +344,15 @@ function RoutingTester(props: {
className="rounded border border-[color:var(--border)] px-2 py-1 text-sm"
/>
-
- {running ? t('routingTesterRunning') : t('routingTesterSubmit')}
-
+ {t('routingTesterSubmit')}
+
{error && (
{error}
@@ -449,13 +451,13 @@ function CreateAgentForm(props: {
-
{t('createSubmit')}
-
+
diff --git a/web-ui/app/routines/_components/RoutineActions.tsx b/web-ui/app/routines/_components/RoutineActions.tsx
index 52a73e701..1c64a7d58 100644
--- a/web-ui/app/routines/_components/RoutineActions.tsx
+++ b/web-ui/app/routines/_components/RoutineActions.tsx
@@ -89,15 +89,17 @@ export function RoutineActions({ routine }: Props): React.ReactElement {
return (
-
Jetzt
-
+
{isPaused ? 'Resume' : 'Pause'}
-
Delete
-
+
{error ? (
diff --git a/web-ui/app/routines/_components/RoutineTemplateEditor.tsx b/web-ui/app/routines/_components/RoutineTemplateEditor.tsx
index d6044617c..41f55040d 100644
--- a/web-ui/app/routines/_components/RoutineTemplateEditor.tsx
+++ b/web-ui/app/routines/_components/RoutineTemplateEditor.tsx
@@ -250,16 +250,20 @@ export function RoutineTemplateEditor({ routine }: Props): React.ReactElement {
) : null}
- {
void handleSave();
}}
disabled={saving || draftParseError !== null || !dirty}
- className="rounded-full border border-[color:var(--accent)]/40 bg-[color:var(--accent)]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--accent)] transition hover:border-[color:var(--accent)] disabled:opacity-40"
+ busy={saving}
+ busyLabel="Speichert…"
+ className="text-[11px] font-semibold uppercase tracking-[0.16em]"
>
- {saving ? 'Speichert…' : 'Speichern'}
-
+ Speichern
+
- {
void handlePreview();
}}
disabled={previewLoading}
- className="rounded-full border border-[color:var(--accent)]/40 bg-[color:var(--accent)]/5 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--accent)] transition hover:border-[color:var(--accent)] disabled:opacity-40"
+ busy={previewLoading}
+ busyLabel="Rendert…"
+ className="text-[11px] font-semibold uppercase tracking-[0.16em]"
>
- {previewLoading ? 'Rendert…' : 'Preview'}
-
+ Preview
+
{previewError ? (
diff --git a/web-ui/app/store/builder/[id]/_components/AuditTimelinePane.tsx b/web-ui/app/store/builder/[id]/_components/AuditTimelinePane.tsx
index 84ec8e270..157459bbc 100644
--- a/web-ui/app/store/builder/[id]/_components/AuditTimelinePane.tsx
+++ b/web-ui/app/store/builder/[id]/_components/AuditTimelinePane.tsx
@@ -3,6 +3,8 @@
import { useTranslations } from 'next-intl';
import { useCallback, useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
import { listBuilderAudit, type BuilderAuditEvent } from '../../../../_lib/api';
/**
@@ -164,17 +166,17 @@ export function AuditTimelinePane({ draftId }: AuditTimelinePaneProps): React.Re
{hasMore && (
- {
void loadPage(offset, true);
}}
disabled={loading}
- className="rounded border border-[color:var(--border)] px-3 py-1 text-xs"
>
{loading ? t('loading') : t('loadMore')}
-
+
)}
);
diff --git a/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx b/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx
index 3a8410a18..a59d42d93 100644
--- a/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx
+++ b/web-ui/app/store/builder/[id]/_components/BuilderChatPane.tsx
@@ -594,14 +594,14 @@ export function BuilderChatPane({
className="min-h-[44px] flex-1 resize-none rounded-md border border-[color:var(--border)] bg-[color:var(--bg)] px-3 py-2 text-[13px] leading-snug text-[color:var(--fg-strong)] placeholder:text-[color:var(--fg-subtle)] focus:border-[color:var(--accent)] focus:outline-none disabled:opacity-60"
/>
{inflight ? (
-
{t('button.stop')}
-
+
) : (
{pending ? t('applying') : t('apply')}
-
{t('cancel')}
-
+
)}
diff --git a/web-ui/app/store/builder/[id]/_components/InstallDiffModal.tsx b/web-ui/app/store/builder/[id]/_components/InstallDiffModal.tsx
index abc2d6f1d..5222bae47 100644
--- a/web-ui/app/store/builder/[id]/_components/InstallDiffModal.tsx
+++ b/web-ui/app/store/builder/[id]/_components/InstallDiffModal.tsx
@@ -262,16 +262,11 @@ export function InstallDiffModal({
>
{t('back')}
-
void handleInstall()}
disabled={busy || phase.kind === 'succeeded'}
- className={cn(
- 'inline-flex items-center gap-2 rounded-md px-4 py-2 text-[12px] font-semibold uppercase tracking-[0.18em]',
- 'bg-[color:var(--accent)] text-[color:var(--fg-on-dark)] shadow-[var(--shadow-cta)]',
- 'transition-opacity',
- 'disabled:opacity-50 disabled:cursor-not-allowed',
- )}
+ className="text-[12px] uppercase tracking-[0.18em]"
>
{busy ? (
<>
@@ -286,7 +281,7 @@ export function InstallDiffModal({
) : (
t('publish')
)}
-
+
@@ -564,13 +559,14 @@ function FailureBanner({
{t('bumpAndPublish', { current: currentVersion, next: bumped ?? '' })}
) : null}
-
{t('retry')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/PagesList.tsx b/web-ui/app/store/builder/[id]/_components/PagesList.tsx
index 8e88eb5a8..37829b0fa 100644
--- a/web-ui/app/store/builder/[id]/_components/PagesList.tsx
+++ b/web-ui/app/store/builder/[id]/_components/PagesList.tsx
@@ -192,14 +192,10 @@ export function PagesList({
-
+
{t('addPage')}
-
+
);
}
diff --git a/web-ui/app/store/builder/[id]/_components/PersonaPillar.tsx b/web-ui/app/store/builder/[id]/_components/PersonaPillar.tsx
index 4b8bdf08f..e2033bc13 100644
--- a/web-ui/app/store/builder/[id]/_components/PersonaPillar.tsx
+++ b/web-ui/app/store/builder/[id]/_components/PersonaPillar.tsx
@@ -4,6 +4,8 @@ import { useTranslations } from 'next-intl';
import { useCallback, useMemo, useState, useTransition } from 'react';
import { ChevronDown, ChevronRight, Sparkles } from 'lucide-react';
+import { Button } from '@/app/_components/ui/Button';
+
import { ApiError, setPersonaConfig } from '../../../../_lib/api';
import type { QualityConfig } from '../../../../_lib/builderTypes';
import { cn } from '../../../../_lib/cn';
@@ -201,15 +203,15 @@ export function PersonaPillar({
: t('templateBadge', { template: persona.template })
: t('templateBadgeNone')}
- setGalleryOpen(true)}
disabled={disabled || pending}
- className="rounded border border-[color:var(--border)] px-2 py-1 text-xs"
>
{t('applyTemplate')}
-
+
{galleryOpen && (
-
{t('reset')}
-
-
+
{pending ? t('saving') : t('save')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx b/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx
index 7d87e188c..5ef8bc4ee 100644
--- a/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx
+++ b/web-ui/app/store/builder/[id]/_components/PersonaTemplateGallery.tsx
@@ -101,15 +101,16 @@ export function PersonaTemplateGallery({
{t('title')}
-
{t('close')}
-
+
@@ -185,15 +186,16 @@ export function PersonaTemplateGallery({
)}
-
{t('cancel')}
-
+
{inflight ? (
-
{t('stop')}
-
+
) : (
) : null}
{snap.phase === 'failed' && onFixWithBuilder ? (
-
{t('fixWithBuilder')}
-
+
) : null}
diff --git a/web-ui/app/store/builder/[id]/_components/PreviewPromptPanel.tsx b/web-ui/app/store/builder/[id]/_components/PreviewPromptPanel.tsx
index 2a3df3171..1af129fcb 100644
--- a/web-ui/app/store/builder/[id]/_components/PreviewPromptPanel.tsx
+++ b/web-ui/app/store/builder/[id]/_components/PreviewPromptPanel.tsx
@@ -3,6 +3,8 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
+import { Button } from '@/app/_components/ui/Button';
+
import {
fetchBuilderPreviewPrompt,
type BuilderPreviewPrompt,
@@ -133,26 +135,26 @@ export function PreviewPromptPanel({
})}
)}
- {
void load();
}}
disabled={loading}
- className="rounded border border-[color:var(--border)] px-2 py-1"
>
{loading ? t('loading') : t('refresh')}
-
-
+
{copied ? t('copied') : t('copy')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/QualityPanel.tsx b/web-ui/app/store/builder/[id]/_components/QualityPanel.tsx
index 0c2ae7b95..55cd24b28 100644
--- a/web-ui/app/store/builder/[id]/_components/QualityPanel.tsx
+++ b/web-ui/app/store/builder/[id]/_components/QualityPanel.tsx
@@ -3,6 +3,8 @@
import { useTranslations } from 'next-intl';
import { useCallback, useEffect, useState } from 'react';
+import { Button } from '@/app/_components/ui/Button';
+
import {
fetchBuilderQuality,
type BuilderQualityResult,
@@ -95,17 +97,17 @@ export function QualityPanel({ draftId, refetchKey }: QualityPanelProps): React.
{t('tokensPrefix')} {tokenHealthLabel(data.tokenHealth)}
)}
- {
void load();
}}
disabled={loading}
- className="rounded border border-[color:var(--border)] px-2 py-1"
>
{loading ? t('loading') : t('refresh')}
-
+
diff --git a/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx b/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx
index 06eec66db..40fc3af34 100644
--- a/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx
+++ b/web-ui/app/store/builder/[id]/_components/SecretsDrawer.tsx
@@ -208,15 +208,15 @@ export function SecretsDrawer({