continued UI consolidation
This commit is contained in:
60
src/components/ui/AttachmentThumbnail.tsx
Normal file
60
src/components/ui/AttachmentThumbnail.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Show } from "solid-js";
|
||||
import type { JSX } from "solid-js";
|
||||
import IconButton from "./IconButton";
|
||||
import XCircle from "~/components/icons/XCircle";
|
||||
|
||||
export interface AttachmentThumbnailProps {
|
||||
/** The URL of the file (either S3 URL or data URL) */
|
||||
fileUrl: string;
|
||||
/** Whether the file is a video */
|
||||
isVideo: boolean;
|
||||
/** Callback when the copy button is clicked */
|
||||
onCopy: () => void;
|
||||
/** Callback when the remove button is clicked */
|
||||
onRemove: () => void;
|
||||
/** Alt text for the image */
|
||||
alt?: string;
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export default function AttachmentThumbnail(props: AttachmentThumbnailProps) {
|
||||
return (
|
||||
<div class={props.class}>
|
||||
<IconButton
|
||||
icon={
|
||||
<XCircle
|
||||
height={24}
|
||||
width={24}
|
||||
stroke={"currentColor"}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
}
|
||||
onClick={props.onRemove}
|
||||
aria-label={`Remove ${props.alt || "attachment"}`}
|
||||
variant="danger"
|
||||
class="hover:bg-crust hover:bg-opacity-80 absolute z-10 ml-4 pb-[120px]"
|
||||
/>
|
||||
<button type="button" onClick={props.onCopy} class="relative">
|
||||
<Show
|
||||
when={props.isVideo}
|
||||
fallback={
|
||||
<img
|
||||
src={props.fileUrl}
|
||||
class="mx-4 my-auto h-36 w-36 object-cover"
|
||||
alt={props.alt || "attachment"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<video
|
||||
src={props.fileUrl}
|
||||
class="mx-4 my-auto h-36 w-36 object-cover"
|
||||
controls
|
||||
/>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentThumbnail };
|
||||
83
src/components/ui/IconButton.tsx
Normal file
83
src/components/ui/IconButton.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { JSX, splitProps, Show } from "solid-js";
|
||||
import { Spinner } from "~/components/Spinner";
|
||||
|
||||
export interface IconButtonProps extends Omit<
|
||||
JSX.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
"children"
|
||||
> {
|
||||
icon: JSX.Element;
|
||||
"aria-label": string;
|
||||
variant?: "ghost" | "danger" | "primary";
|
||||
size?: "sm" | "md" | "lg";
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function IconButton(props: IconButtonProps) {
|
||||
const [local, others] = splitProps(props, [
|
||||
"icon",
|
||||
"aria-label",
|
||||
"variant",
|
||||
"size",
|
||||
"loading",
|
||||
"disabled",
|
||||
"class"
|
||||
]);
|
||||
|
||||
const variant = () => local.variant || "ghost";
|
||||
const size = () => local.size || "md";
|
||||
|
||||
const baseClasses =
|
||||
"inline-flex items-center justify-center rounded transition-all duration-200 ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-blue focus-visible:ring-offset-2";
|
||||
|
||||
const variantClasses = () => {
|
||||
const isDisabledOrLoading = local.disabled || local.loading;
|
||||
|
||||
switch (variant()) {
|
||||
case "ghost":
|
||||
return isDisabledOrLoading
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "text-text hover:bg-surface0/50 active:scale-95";
|
||||
case "danger":
|
||||
return isDisabledOrLoading
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "text-red hover:bg-red/10 active:scale-95";
|
||||
case "primary":
|
||||
return isDisabledOrLoading
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "text-blue hover:bg-blue/10 active:scale-95";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const sizeClasses = () => {
|
||||
switch (size()) {
|
||||
case "sm":
|
||||
return "p-1";
|
||||
case "md":
|
||||
return "p-2";
|
||||
case "lg":
|
||||
return "p-3";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
{...others}
|
||||
type="button"
|
||||
disabled={local.disabled || local.loading}
|
||||
aria-label={local["aria-label"]}
|
||||
aria-busy={local.loading}
|
||||
aria-disabled={local.disabled}
|
||||
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${local.class || ""}`}
|
||||
>
|
||||
<Show when={local.loading} fallback={local.icon}>
|
||||
<Spinner size={20} />
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export { IconButton };
|
||||
Reference in New Issue
Block a user