diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx
index 7ae01e1..090efdd 100644
--- a/src/components/Bars.tsx
+++ b/src/components/Bars.tsx
@@ -1,13 +1,6 @@
import { Typewriter } from "./Typewriter";
import { useBars } from "~/context/bars";
-import {
- onMount,
- createSignal,
- Show,
- For,
- onCleanup,
- createEffect
-} from "solid-js";
+import { onMount, createSignal, Show, For, onCleanup } from "solid-js";
import { api } from "~/lib/api";
import { insertSoftHyphens } from "~/lib/client-utils";
import GitHub from "./icons/GitHub";
@@ -102,7 +95,7 @@ export function RightBarContent() {
const handleLinkClick = () => {
if (
typeof window !== "undefined" &&
- window.innerWidth < BREAKPOINTS.MOBILE
+ window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH
) {
setLeftBarVisible(false);
}
diff --git a/src/components/blog/AddAttachmentSection.tsx b/src/components/blog/AddAttachmentSection.tsx
index 49e2d93..a1d8f26 100644
--- a/src/components/blog/AddAttachmentSection.tsx
+++ b/src/components/blog/AddAttachmentSection.tsx
@@ -1,6 +1,6 @@
import { createSignal, createEffect, For, Show } from "solid-js";
import Dropzone from "./Dropzone";
-import XCircle from "~/components/icons/XCircle";
+import AttachmentThumbnail from "~/components/ui/AttachmentThumbnail";
import AddImageToS3 from "~/lib/s3upload";
import { env } from "~/env/client";
import { api } from "~/lib/api";
@@ -147,42 +147,13 @@ export default function AddAttachmentSection(props: AddAttachmentSectionProps) {
{(file) => (
-
-
-
-
+ copyToClipboard(file.key)}
+ onRemove={() => removeImage(file.key)}
+ alt="attachment"
+ />
)}
0}>
@@ -190,45 +161,17 @@ export default function AddAttachmentSection(props: AddAttachmentSectionProps) {
{(file, index) => (
-
-
-
-
+
+ copyToClipboard(newFileHolderKeys()[index()] as string)
+ }
+ onRemove={() =>
+ removeNewImage(index(), newFileHolderKeys()[index()])
+ }
+ alt="new attachment"
+ />
)}
diff --git a/src/components/blog/CommentBlock.tsx b/src/components/blog/CommentBlock.tsx
index 9741e88..7e6a362 100644
--- a/src/components/blog/CommentBlock.tsx
+++ b/src/components/blog/CommentBlock.tsx
@@ -10,7 +10,8 @@ import ReplyIcon from "~/components/icons/ReplyIcon";
import TrashIcon from "~/components/icons/TrashIcon";
import EditIcon from "~/components/icons/EditIcon";
import ThumbsUpEmoji from "~/components/icons/emojis/ThumbsUp";
-import LoadingSpinner from "~/components/LoadingSpinner";
+import Button from "~/components/ui/Button";
+import IconButton from "~/components/ui/IconButton";
import CommentInputBlock from "./CommentInputBlock";
import ReactionBar from "./ReactionBar";
@@ -254,32 +255,46 @@ export default function CommentBlock(props: CommentBlockProps) {
{/* Delete button */}
-
-
+ }
+ variant="danger"
+ loading={deletionLoading()}
+ onClick={deleteCommentTrigger}
+ aria-label="Delete comment"
+ class="z-100"
+ />
{/* Edit and Reply buttons */}
-
+ }
+ onClick={editCommentTrigger}
+ aria-label="Edit comment"
+ class="px-2"
+ />
-
+
+ }
+ onClick={toggleCommentReplyBox}
+ aria-label="Reply to comment"
+ class="z-30"
+ />
{/* Reaction bar */}
diff --git a/src/components/blog/CommentInputBlock.tsx b/src/components/blog/CommentInputBlock.tsx
index 8486ed6..5769d25 100644
--- a/src/components/blog/CommentInputBlock.tsx
+++ b/src/components/blog/CommentInputBlock.tsx
@@ -1,5 +1,6 @@
import { createEffect } from "solid-js";
import type { CommentInputBlockProps } from "~/types/comment";
+import Button from "~/components/ui/Button";
export default function CommentInputBlock(props: CommentInputBlockProps) {
let bodyRef: HTMLTextAreaElement | undefined;
@@ -37,17 +38,13 @@ export default function CommentInputBlock(props: CommentInputBlockProps) {
-
+
diff --git a/src/components/blog/DeletePostButton.tsx b/src/components/blog/DeletePostButton.tsx
index f7f327a..4880322 100644
--- a/src/components/blog/DeletePostButton.tsx
+++ b/src/components/blog/DeletePostButton.tsx
@@ -1,7 +1,7 @@
-import { createSignal, Show } from "solid-js";
+import { createSignal } from "solid-js";
import { api } from "~/lib/api";
import TrashIcon from "~/components/icons/TrashIcon";
-import LoadingSpinner from "~/components/LoadingSpinner";
+import Button from "~/components/ui/Button";
export interface DeletePostButtonProps {
type: string;
@@ -28,14 +28,14 @@ export default function DeletePostButton(props: DeletePostButtonProps) {
return (
);
}
diff --git a/src/components/blog/EditCommentModal.tsx b/src/components/blog/EditCommentModal.tsx
index 324ee25..da224dc 100644
--- a/src/components/blog/EditCommentModal.tsx
+++ b/src/components/blog/EditCommentModal.tsx
@@ -1,6 +1,7 @@
import { createSignal, Show } from "solid-js";
import type { EditCommentModalProps } from "~/types/comment";
import Xmark from "~/components/icons/Xmark";
+import Button from "~/components/ui/Button";
export default function EditCommentModal(props: EditCommentModalProps) {
let bodyRef: HTMLTextAreaElement | undefined;
@@ -52,17 +53,13 @@ export default function EditCommentModal(props: EditCommentModalProps) {
-
+
diff --git a/src/components/blog/PostForm.tsx b/src/components/blog/PostForm.tsx
index 1c0321c..e08bd1e 100644
--- a/src/components/blog/PostForm.tsx
+++ b/src/components/blog/PostForm.tsx
@@ -9,6 +9,7 @@ import AddAttachmentSection from "~/components/blog/AddAttachmentSection";
import XCircle from "~/components/icons/XCircle";
import AddImageToS3 from "~/lib/s3upload";
import Input from "~/components/ui/Input";
+import Button from "~/components/ui/Button";
interface PostFormProps {
mode: "create" | "edit";
@@ -542,23 +543,14 @@ export default function PostForm(props: PostFormProps) {
{/* Submit button */}
-
+ {published() ? "Publish!" : "Save as Draft"}
+
diff --git a/src/components/blog/TagMaker.tsx b/src/components/blog/TagMaker.tsx
index 6ff0072..09cd163 100644
--- a/src/components/blog/TagMaker.tsx
+++ b/src/components/blog/TagMaker.tsx
@@ -1,6 +1,7 @@
import { For } from "solid-js";
import InfoIcon from "~/components/icons/InfoIcon";
import Xmark from "~/components/icons/Xmark";
+import IconButton from "~/components/ui/IconButton";
export interface TagMakerProps {
tagInputValue: string;
@@ -42,13 +43,20 @@ export default function TagMaker(props: TagMakerProps) {
{tag}
-
+ }
onClick={() => props.deleteTag(idx())}
- >
-
-
+ aria-label={`Remove tag ${tag}`}
+ variant="danger"
+ class="bg-mantle bg-opacity-50 absolute inset-0 flex items-center justify-center rounded-xl opacity-0 group-hover:opacity-100"
+ />
)}
diff --git a/src/components/ui/AttachmentThumbnail.tsx b/src/components/ui/AttachmentThumbnail.tsx
new file mode 100644
index 0000000..77da3a9
--- /dev/null
+++ b/src/components/ui/AttachmentThumbnail.tsx
@@ -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 (
+
+
+ }
+ 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]"
+ />
+
+
+ );
+}
+
+export { AttachmentThumbnail };
diff --git a/src/components/ui/IconButton.tsx b/src/components/ui/IconButton.tsx
new file mode 100644
index 0000000..7f6285d
--- /dev/null
+++ b/src/components/ui/IconButton.tsx
@@ -0,0 +1,83 @@
+import { JSX, splitProps, Show } from "solid-js";
+import { Spinner } from "~/components/Spinner";
+
+export interface IconButtonProps extends Omit<
+ JSX.ButtonHTMLAttributes,
+ "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 (
+
+ );
+}
+
+export { IconButton };