common components
This commit is contained in:
parent
11eca48354
commit
c0fd8671e4
11 changed files with 203 additions and 78 deletions
|
|
@ -22,6 +22,7 @@
|
|||
"@tanstack/react-router-devtools": "latest",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router-plugin": "^1.132.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
|
|
|||
53
apps/pharmanager/src/components/ui/Button.tsx
Normal file
53
apps/pharmanager/src/components/ui/Button.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "#/lib/cn";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-1.5 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-blue-600 text-white hover:bg-blue-700",
|
||||
danger: "bg-red-600 text-white hover:bg-red-700",
|
||||
outline:
|
||||
"border border-slate-200 bg-white text-slate-700 hover:bg-slate-50",
|
||||
ghost: "text-slate-500 hover:bg-slate-100 hover:text-slate-900",
|
||||
"ghost-blue":
|
||||
"text-slate-500 hover:text-blue-600 hover:bg-blue-50",
|
||||
"ghost-red":
|
||||
"text-slate-500 hover:text-red-600 hover:bg-red-50",
|
||||
"ghost-dark":
|
||||
"text-slate-400 hover:bg-slate-800 hover:text-white",
|
||||
},
|
||||
size: {
|
||||
default: "px-4 py-2",
|
||||
sm: "px-3 py-1.5 text-xs",
|
||||
lg: "px-6 py-2.5",
|
||||
icon: "w-8 h-8 p-0",
|
||||
"icon-sm": "w-4 h-4 p-0 rounded-full",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends ComponentPropsWithoutRef<"button">,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
39
apps/pharmanager/src/components/ui/Input.tsx
Normal file
39
apps/pharmanager/src/components/ui/Input.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "#/lib/cn";
|
||||
|
||||
export const inputVariants = cva(
|
||||
"px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 w-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-slate-200 focus:border-blue-600",
|
||||
error: "border-red-600",
|
||||
transparent: "border-none bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface InputProps
|
||||
extends ComponentPropsWithoutRef<"input">,
|
||||
VariantProps<typeof inputVariants> {}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, variant, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={cn(inputVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
36
apps/pharmanager/src/components/ui/Textarea.tsx
Normal file
36
apps/pharmanager/src/components/ui/Textarea.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { forwardRef, type ComponentPropsWithoutRef } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "#/lib/cn";
|
||||
|
||||
export const textareaVariants = cva(
|
||||
"w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white resize-y min-h-[80px] transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-slate-200 focus:border-blue-600",
|
||||
error: "border-red-600",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface TextareaProps
|
||||
extends ComponentPropsWithoutRef<"textarea">,
|
||||
VariantProps<typeof textareaVariants> {}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(textareaVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Textarea.displayName = "Textarea";
|
||||
3
apps/pharmanager/src/components/ui/index.ts
Normal file
3
apps/pharmanager/src/components/ui/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { Button, type ButtonProps, buttonVariants } from "./Button";
|
||||
export { Input, type InputProps, inputVariants } from "./Input";
|
||||
export { Textarea, type TextareaProps, textareaVariants } from "./Textarea";
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { ArrowLeft, Pencil, Trash2, Truck } from "lucide-react";
|
||||
import { Button, buttonVariants } from "#/components/ui";
|
||||
import {
|
||||
useGetDistributorById,
|
||||
useRemoveDistributor,
|
||||
|
|
@ -56,7 +57,7 @@ function DistributorDetailsPage() {
|
|||
</p>
|
||||
<Link
|
||||
to="/distributors"
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
className={buttonVariants({ variant: "primary" })}
|
||||
>
|
||||
Back to Distributors
|
||||
</Link>
|
||||
|
|
@ -124,21 +125,14 @@ function DistributorDetailsPage() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 mt-6 pt-5 border-t border-slate-200">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Button variant="primary">
|
||||
<Pencil className="w-[15px] h-[15px]" />
|
||||
Edit Distributor
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700 transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDelete}>
|
||||
<Trash2 className="w-[15px] h-[15px]" />
|
||||
Delete Distributor
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useForm } from "react-hook-form";
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useCreateDistributor, trpc } from "shared-react";
|
||||
import { Button, Input, Textarea, buttonVariants } from "#/components/ui";
|
||||
|
||||
const formSchema = z.object({
|
||||
agency: z.string().min(1, "Agency name is required"),
|
||||
|
|
@ -81,11 +82,11 @@ function AddDistributorPage() {
|
|||
Agency Name{" "}
|
||||
<span className="text-red-600 ml-0.5">*</span>
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
variant={errors.agency ? "error" : "default"}
|
||||
{...register("agency")}
|
||||
placeholder="e.g. MediDistributors"
|
||||
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.agency ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||
/>
|
||||
{errors.agency && (
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
|
|
@ -99,11 +100,11 @@ function AddDistributorPage() {
|
|||
Contact Person{" "}
|
||||
<span className="text-red-600 ml-0.5">*</span>
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
variant={errors.contact ? "error" : "default"}
|
||||
{...register("contact")}
|
||||
placeholder="e.g. Rahul Mehta"
|
||||
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.contact ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||
/>
|
||||
{errors.contact && (
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
|
|
@ -117,8 +118,9 @@ function AddDistributorPage() {
|
|||
Contact Mobile{" "}
|
||||
<span className="text-red-600 ml-0.5">*</span>
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="tel"
|
||||
variant={errors.mobile ? "error" : "default"}
|
||||
{...register("mobile", {
|
||||
onChange: (e) => {
|
||||
e.target.value = e.target.value
|
||||
|
|
@ -127,7 +129,6 @@ function AddDistributorPage() {
|
|||
},
|
||||
})}
|
||||
placeholder="e.g. 9876500001"
|
||||
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.mobile ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||
/>
|
||||
{errors.mobile && (
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
|
|
@ -140,13 +141,12 @@ function AddDistributorPage() {
|
|||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Address
|
||||
</label>
|
||||
<textarea
|
||||
<Textarea
|
||||
{...register("address", {
|
||||
setValueAs: (v: string) => v || null,
|
||||
})}
|
||||
rows={3}
|
||||
placeholder="Full address of the distributor"
|
||||
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white resize-y min-h-[80px] transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -154,20 +154,19 @@ function AddDistributorPage() {
|
|||
<div className="flex justify-end gap-3 pt-5 mt-2 border-t border-slate-200">
|
||||
<Link
|
||||
to="/distributors"
|
||||
className="inline-flex items-center px-5 py-2.5 border border-slate-200 rounded-md text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 transition-colors"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending}
|
||||
className="inline-flex items-center gap-1.5 px-6 py-2.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<Plus className="w-[15px] h-[15px]" />
|
||||
{createMutation.isPending
|
||||
? "Saving..."
|
||||
: "Save Distributor"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{createMutation.error && (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
|||
import { Search, Plus, Pencil, Trash2, Truck } from "lucide-react";
|
||||
import { GridTable } from "#/components/GridTable";
|
||||
import type { GridTableColumn } from "#/components/GridTable";
|
||||
import { Button, buttonVariants } from "#/components/ui";
|
||||
import {
|
||||
useListDistributors,
|
||||
useRemoveDistributor,
|
||||
|
|
@ -68,21 +69,21 @@ function makeColumns(
|
|||
size: 90,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md text-slate-500 hover:text-blue-600 hover:bg-blue-50 transition-colors"
|
||||
<Button
|
||||
variant="ghost-blue"
|
||||
size="icon"
|
||||
aria-label={`Edit ${row.agency}`}
|
||||
>
|
||||
<Pencil className="w-[15px] h-[15px]" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md text-slate-500 hover:text-red-600 hover:bg-red-50 transition-colors"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost-red"
|
||||
size="icon"
|
||||
aria-label={`Delete ${row.agency}`}
|
||||
onClick={() => onDelete(row)}
|
||||
>
|
||||
<Trash2 className="w-[15px] h-[15px]" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -161,7 +162,7 @@ function DistributorsIndexPage() {
|
|||
</div>
|
||||
<Link
|
||||
to="/distributors/add"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
className={buttonVariants({ variant: "primary" })}
|
||||
>
|
||||
<Plus className="w-[15px] h-[15px]" />
|
||||
Add Distributor
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Layers,
|
||||
ImageIcon,
|
||||
} from "lucide-react";
|
||||
import { Button, buttonVariants } from "#/components/ui";
|
||||
import { useGetStorageById, useRemoveStorage, trpc } from "shared-react";
|
||||
|
||||
export const Route = createFileRoute("/storage/$id")({
|
||||
|
|
@ -58,7 +59,7 @@ function StorageDetailsPage() {
|
|||
</p>
|
||||
<Link
|
||||
to="/storage"
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
className={buttonVariants({ variant: "primary" })}
|
||||
>
|
||||
Back to Storage
|
||||
</Link>
|
||||
|
|
@ -91,21 +92,14 @@ function StorageDetailsPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Button variant="primary">
|
||||
<Pencil className="w-[15px] h-[15px]" />
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-red-600 text-white rounded-md text-sm font-medium hover:bg-red-700 transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button variant="danger" onClick={handleDelete}>
|
||||
<Trash2 className="w-[15px] h-[15px]" />
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useCreateStorage, trpc } from "shared-react";
|
||||
import { Button, Input, Textarea, buttonVariants } from "#/components/ui";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Rack name is required"),
|
||||
|
|
@ -132,11 +133,11 @@ function AddStoragePage() {
|
|||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Rack Name <span className="text-red-600 ml-0.5">*</span>
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
variant={errors.name ? "error" : "default"}
|
||||
{...register("name")}
|
||||
placeholder="e.g. Rack A-01, Cold Storage 1"
|
||||
className={`w-full px-3.5 py-2.5 border rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 ${errors.name ? "border-red-600" : "border-slate-200 focus:border-blue-600"}`}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
|
|
@ -149,13 +150,12 @@ function AddStoragePage() {
|
|||
<label className="block text-sm font-medium text-slate-900 mb-1.5">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
<Textarea
|
||||
{...register("description", {
|
||||
setValueAs: (v: string) => v || null,
|
||||
})}
|
||||
placeholder="Describe what this rack stores, its location, special conditions..."
|
||||
rows={3}
|
||||
className="w-full px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white resize-y min-h-[80px] transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -168,21 +168,22 @@ function AddStoragePage() {
|
|||
onRemove={handleRemoveAlias}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={aliasInput}
|
||||
onChange={(e) => setAliasInput(e.target.value)}
|
||||
onKeyDown={handleAliasKeyDown}
|
||||
placeholder="Type an alias and press Enter or comma"
|
||||
className="flex-1 px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
className="flex-1 w-auto"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddAlias}
|
||||
className="px-3 py-2.5 border border-slate-200 rounded-md text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Common names or abbreviations for this rack
|
||||
|
|
@ -198,21 +199,22 @@ function AddStoragePage() {
|
|||
onRemove={handleRemoveImage}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={imageInput}
|
||||
onChange={(e) => setImageInput(e.target.value)}
|
||||
onKeyDown={handleImageKeyDown}
|
||||
placeholder="Image filename (e.g. rack-a01-photo.jpg)"
|
||||
className="flex-1 px-3.5 py-2.5 border border-slate-200 rounded-md text-sm text-slate-900 bg-white transition-colors focus:outline-none focus:ring-[3px] focus:ring-blue-100 focus:border-blue-600"
|
||||
className="flex-1 w-auto"
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddImage}
|
||||
className="px-3 py-2.5 border border-slate-200 rounded-md text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Add filenames or URLs for rack photos
|
||||
|
|
@ -220,17 +222,16 @@ function AddStoragePage() {
|
|||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-5 border-t border-slate-200">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending}
|
||||
className="inline-flex items-center gap-1.5 px-5 py-2.5 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
<Plus className="w-[15px] h-[15px]" />
|
||||
{createMutation.isPending ? "Saving..." : "Save Rack"}
|
||||
</button>
|
||||
</Button>
|
||||
<Link
|
||||
to="/storage"
|
||||
className="inline-flex items-center px-5 py-2.5 border border-slate-200 rounded-md text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 transition-colors"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
|
|
@ -262,13 +263,14 @@ function AliasList({
|
|||
className="inline-flex items-center gap-1 px-2.5 py-1 bg-slate-100 rounded-full text-sm"
|
||||
>
|
||||
{alias}
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost-red"
|
||||
size="icon-sm"
|
||||
onClick={() => onRemove(i)}
|
||||
className="w-4 h-4 flex items-center justify-center rounded-full hover:bg-red-600 hover:text-white transition-colors text-slate-500"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -291,13 +293,15 @@ function ImageList({
|
|||
className="relative w-20 h-20 rounded-md border border-slate-200 bg-slate-50 flex items-center justify-center"
|
||||
>
|
||||
<ImageIcon className="w-8 h-8 text-slate-300" />
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="icon-sm"
|
||||
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full text-xs"
|
||||
onClick={() => onRemove(i)}
|
||||
className="absolute -top-1.5 -right-1.5 w-5 h-5 rounded-full bg-red-600 text-white flex items-center justify-center text-xs hover:bg-red-700 transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
|
|||
import { Search, Plus, Pencil, Trash2, Warehouse } from "lucide-react";
|
||||
import { GridTable } from "#/components/GridTable";
|
||||
import type { GridTableColumn } from "#/components/GridTable";
|
||||
import { Button, buttonVariants } from "#/components/ui";
|
||||
import { useListStorage, useRemoveStorage, trpc } from "shared-react";
|
||||
|
||||
interface Rack {
|
||||
|
|
@ -61,21 +62,21 @@ function makeColumns(
|
|||
size: 80,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md text-slate-500 hover:text-blue-600 hover:bg-blue-50 transition-colors"
|
||||
<Button
|
||||
variant="ghost-blue"
|
||||
size="icon"
|
||||
aria-label={`Edit ${row.name}`}
|
||||
>
|
||||
<Pencil className="w-[15px] h-[15px]" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-8 h-8 flex items-center justify-center rounded-md text-slate-500 hover:text-red-600 hover:bg-red-50 transition-colors"
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost-red"
|
||||
size="icon"
|
||||
aria-label={`Delete ${row.name}`}
|
||||
onClick={() => onDelete(row)}
|
||||
>
|
||||
<Trash2 className="w-[15px] h-[15px]" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -153,7 +154,7 @@ function StorageIndexPage() {
|
|||
</div>
|
||||
<Link
|
||||
to="/storage/add"
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
className={buttonVariants({ variant: "primary" })}
|
||||
>
|
||||
<Plus className="w-[15px] h-[15px]" />
|
||||
Add Rack
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue