mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 07:36:40 +00:00
feat(ui): add Modal chrome primitives, migrate file dialogs (#896)
Introduces shared <Modal>, <ModalHeader>, <ModalBody>, <ModalFooter>, and <ModalDestructiveHeader> primitives that wrap shadcn Dialog and encapsulate the canonical modal chrome: cyan rail at the left edge, mono uppercase kicker, italic serif title, footer hint, standardized button order (secondary outline, primary cyan / destructive). Migrates NewFolderDialog and DeleteFileConfirm onto the new primitives as the first proof points. The destructive variant flips the rail and kicker color to the destructive token without changing the chrome recipe.
This commit is contained in:
@@ -1,13 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { AlertTriangle, Loader2, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react';
|
||||
import { Modal, ModalDestructiveHeader, ModalBody, ModalFooter } from '@/components/ui/modal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -75,25 +68,30 @@ export function DeleteFileConfirm({
|
||||
|
||||
const deleteLabel = notEmpty ? 'Delete all' : 'Delete';
|
||||
|
||||
const titleNode = entryName ? (
|
||||
<>
|
||||
Delete <em className="font-display italic text-destructive">{entryName}</em>?
|
||||
</>
|
||||
) : (
|
||||
'Delete item?'
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Trash2 className="w-4 h-4 text-destructive" strokeWidth={1.5} />
|
||||
Delete {entryName ? `"${entryName}"` : 'item'}?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{notEmpty ? (
|
||||
<span className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
This folder is not empty. Delete everything inside?
|
||||
</span>
|
||||
) : (
|
||||
'This action cannot be undone.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Modal open={open} onOpenChange={handleClose} size="sm">
|
||||
<ModalDestructiveHeader
|
||||
kicker={`${stackName.toUpperCase()} · DELETE · IRREVERSIBLE`}
|
||||
title={titleNode}
|
||||
description={`Confirm deletion of ${entryName || 'item'} from ${stackName}.`}
|
||||
/>
|
||||
<ModalBody>
|
||||
{notEmpty ? (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5" strokeWidth={1.5} />
|
||||
<span>This folder is not empty. Delete everything inside?</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">This action cannot be undone.</p>
|
||||
)}
|
||||
|
||||
{isProtected && (
|
||||
<div className="space-y-2">
|
||||
@@ -116,8 +114,10 @@ export function DeleteFileConfirm({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
hint={isProtected ? 'PROTECTED FILE' : notEmpty ? 'NON-EMPTY FOLDER' : undefined}
|
||||
secondary={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -126,10 +126,12 @@ export function DeleteFileConfirm({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
primary={
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
data-testid="delete-confirm-btn"
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
onClick={handleDelete}
|
||||
disabled={deleting || !protectedOk}
|
||||
>
|
||||
@@ -138,8 +140,8 @@ export function DeleteFileConfirm({
|
||||
)}
|
||||
{deleteLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { FolderPlus, Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/ui/modal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -73,19 +66,16 @@ export function NewFolderDialog({
|
||||
if (e.key === 'Enter') void handleCreate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FolderPlus className="w-4 h-4" strokeWidth={1.5} />
|
||||
New Folder
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Enter a name for the new folder.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
const parentLabel = currentDir || stackName;
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={handleClose} size="sm">
|
||||
<ModalHeader
|
||||
kicker={`${stackName.toUpperCase()} · NEW FOLDER`}
|
||||
title="New folder"
|
||||
description="Enter a name for the new folder."
|
||||
/>
|
||||
<ModalBody>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="folder-name">Folder name</Label>
|
||||
<Input
|
||||
@@ -104,8 +94,11 @@ export function NewFolderDialog({
|
||||
<p className="text-xs text-destructive">{validationError}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
hint="PARENT"
|
||||
hintAccent={parentLabel}
|
||||
secondary={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -114,6 +107,8 @@ export function NewFolderDialog({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
primary={
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => void handleCreate()}
|
||||
@@ -124,8 +119,8 @@ export function NewFolderDialog({
|
||||
)}
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user