feat: dark-tech UI redesign + markdown patch cards

Redesign the frontend with a dark-tech theme: add Tailwind + PostCSS,
design tokens, and shadcn-style primitives (Button/Card/Input/Dialog/
DropdownMenu/Tabs/ScrollArea/etc.); restyle the app shell, navigation,
sidebar tree, content view, markdown rendering, editors, modals and
settings panels. Behavior/props unchanged; Font Awesome replaced with
lucide-react.

Add the patch cards feature UI: patch-queries hooks and a PatchCards
component rendered below the markdown body, with an Add Patch button
and create/edit dialog.

Fix tree expandability: folders with an index page now expand on name
click (and navigate), and the chevron+folder icon is one larger toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-16 17:28:13 +01:00
parent 045c7c51d6
commit 952387d50f
54 changed files with 4503 additions and 1765 deletions

View File

@@ -1,8 +1,21 @@
import React, { useState } from 'react';
import { Plus, Trash2, Copy, KeyRound } from 'lucide-react';
import { useCreateApiKey } from '../../utils/queries/apikey-queries';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '../ui/dialog';
import { Button } from '../ui/button';
import { Input, Label } from '../ui/input';
const AVAILABLE_ROLES = ['guest', 'creator', 'admin'];
const SELECT_CLASS =
"flex h-9 w-full rounded-md border border-input bg-background/60 px-3 py-1 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50";
const ApiKeyCreationModal = ({ isOpen, onClose }) => {
const [name, setName] = useState('');
const [roles, setRoles] = useState(['guest']);
@@ -54,111 +67,102 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => {
};
const getRemainingRoles = (currentIndex) => {
return AVAILABLE_ROLES.filter(role =>
return AVAILABLE_ROLES.filter(role =>
!roles.find((r, i) => r === role && i !== currentIndex)
);
};
if (!isOpen) return null;
return (
<div className="modal is-active">
<div className="modal-background" onClick={onClose}></div>
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Create API Key</p>
<button className="delete" aria-label="close" onClick={onClose}></button>
</header>
<section className="modal-card-body">
{!generatedKey ? (
<div>
<div className="field">
<label className="label">Name</label>
<div className="control">
<input
className="input"
type="text"
placeholder="API key name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
<div className="field">
<label className="label">Roles</label>
<Dialog open={isOpen} onOpenChange={(o) => { if (!o) onClose(); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create API Key</DialogTitle>
</DialogHeader>
{!generatedKey ? (
<div className="space-y-5">
<div className="space-y-2">
<Label htmlFor="api-key-name">Name</Label>
<Input
id="api-key-name"
type="text"
placeholder="API key name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Roles</Label>
<div className="space-y-2">
{roles.map((role, index) => (
<div key={index} className="field has-addons">
<div className="control">
<div className="select">
<select
value={role}
onChange={(e) => handleRoleChange(index, e.target.value)}
>
{getRemainingRoles(index).map(availableRole => (
<option key={availableRole} value={availableRole}>
{availableRole}
</option>
))}
</select>
</div>
</div>
<div className="control">
<button
className="button is-danger"
onClick={() => handleRemoveRole(index)}
disabled={roles.length === 1}
>
Delete
</button>
</div>
<div key={index} className="flex items-center gap-2">
<select
className={SELECT_CLASS}
value={role}
onChange={(e) => handleRoleChange(index, e.target.value)}
>
{getRemainingRoles(index).map(availableRole => (
<option key={availableRole} value={availableRole}>
{availableRole}
</option>
))}
</select>
<Button
variant="destructive"
size="icon"
onClick={() => handleRemoveRole(index)}
disabled={roles.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<button
className="button is-info mt-2"
onClick={handleAddRole}
disabled={roles.length === AVAILABLE_ROLES.length}
>
Add Role
</button>
</div>
<Button
variant="outline"
size="sm"
onClick={handleAddRole}
disabled={roles.length === AVAILABLE_ROLES.length}
>
<Plus className="h-4 w-4" /> Add Role
</Button>
</div>
) : (
<div>
<div className="notification is-warning">
Please copy your API key immediately! It will only be displayed once!
</div>
<div className="field">
<label className="label">Your API Key:</label>
<div className="control">
<input
className="input"
type="text"
value={generatedKey.key}
readOnly
/>
</div>
</div>
<button className="button is-info" onClick={handleCopy}>
Copy
</button>
</div>
) : (
<div className="space-y-4">
<div className="rounded-lg border border-secondary/40 bg-secondary/10 px-4 py-3 font-mono text-sm text-secondary">
Please copy your API key immediately! It will only be displayed once!
</div>
)}
</section>
<footer className="modal-card-foot">
<div className="space-y-2">
<Label htmlFor="generated-api-key">Your API Key</Label>
<Input
id="generated-api-key"
className="font-mono"
type="text"
value={generatedKey.key}
readOnly
/>
</div>
<Button onClick={handleCopy}>
<Copy className="h-4 w-4" /> Copy
</Button>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Close
</Button>
{!generatedKey && (
<button
className="button is-primary"
<Button
onClick={handleGenerate}
disabled={createApiKeyMutation.isLoading || !name.trim()}
>
Generate
</button>
<KeyRound className="h-4 w-4" /> Generate
</Button>
)}
<button className="button" onClick={onClose}>Close</button>
</footer>
</div>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default ApiKeyCreationModal;
export default ApiKeyCreationModal;