feat: add icon prop to ActionButton for better loading UX

- Add optional icon prop to ActionButton component
- When loading and icon is provided, spinner replaces icon while text remains
- Improves UX by showing context during loading states
- Update all ActionButton usages to pass icons explicitly
- Maintains backwards compatibility for buttons without icons
This commit is contained in:
Claude
2025-12-05 08:29:56 +00:00
parent de98873a06
commit 62482772a9
13 changed files with 60 additions and 42 deletions

View File

@@ -145,8 +145,8 @@ export default function AssetsSettingsPage() {
{ onSettled: () => setDialogOpen(false) },
)
}
icon={<Trash2 className="mr-2 size-4" />}
>
<Trash2 className="mr-2 size-4" />
{t("actions.delete")}
</ActionButton>
)}

View File

@@ -105,8 +105,8 @@ export default function BrokenLinksPage() {
loading={isRecrawling}
onClick={() => recrawlBookmark({ bookmarkId: b.id })}
className="flex items-center gap-2"
icon={<RefreshCw className="size-4" />}
>
<RefreshCw className="size-4" />
{t("actions.recrawl")}
</ActionButton>
<ActionButton
@@ -114,8 +114,8 @@ export default function BrokenLinksPage() {
onClick={() => deleteBookmark({ bookmarkId: b.id })}
loading={isDeleting}
className="flex items-center gap-2"
icon={<Trash2 className="size-4" />}
>
<Trash2 className="size-4" />
{t("actions.delete")}
</ActionButton>
</TableCell>

View File

@@ -321,9 +321,8 @@ export default function BulkBookmarksAction() {
variant="ghost"
key={name}
onClick={action}
>
{Icon}
</ActionButtonWithTooltip>
icon={Icon}
/>
),
)}
</div>

View File

@@ -156,9 +156,8 @@ export default function ManageListsModal({
onClick={() =>
deleteFromList({ bookmarkId, listId: list.id })
}
>
<X className="size-4" />
</ActionButton>
icon={<X className="size-4" />}
/>
</li>
))}
</ul>

View File

@@ -72,9 +72,8 @@ function AISummary({
aria-label={isExpanded ? "Collapse" : "Expand"}
loading={isResummarizing}
onClick={() => resummarize({ bookmarkId })}
>
<RefreshCw size={16} />
</ActionButton>
icon={<RefreshCw size={16} />}
/>
<ActionButton
size="none"
variant="none"
@@ -85,9 +84,8 @@ function AISummary({
onClick={() =>
updateBookmark({ bookmarkId, summary: null })
}
>
<Trash2 size={16} />
</ActionButton>
icon={<Trash2 size={16} />}
/>
</>
)}
<button

View File

@@ -83,9 +83,8 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
favourited: !bookmark.favourited,
});
}}
>
<FavouritedActionIcon favourited={bookmark.favourited} />
</ActionButton>
icon={<FavouritedActionIcon favourited={bookmark.favourited} />}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{bookmark.favourited
@@ -105,9 +104,8 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
archived: !bookmark.archived,
});
}}
>
<ArchivedActionIcon archived={bookmark.archived} />
</ActionButton>
icon={<ArchivedActionIcon archived={bookmark.archived} />}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{bookmark.archived ? t("actions.unarchive") : t("actions.archive")}

View File

@@ -121,8 +121,8 @@ export default function RuleList({
deleteRule({ id: rule.id });
setDialogOpen(true);
}}
icon={<Trash2 className="mr-2 h-4 w-4" />}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("actions.delete")}
</ActionButton>
)}

View File

@@ -130,8 +130,8 @@ export function PromptEditor() {
loading={isCreating}
variant="default"
className="items-center"
icon={<Plus className="mr-2 size-4" />}
>
<Plus className="mr-2 size-4" />
{t("actions.add")}
</ActionButton>
</form>
@@ -253,8 +253,8 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
variant="secondary"
type="submit"
className="items-center"
icon={<Save className="mr-2 size-4" />}
>
<Save className="mr-2 size-4" />
{t("actions.save")}
</ActionButton>
<ActionButton
@@ -263,8 +263,8 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
onClick={() => deletePrompt({ promptId: prompt.id })}
className="items-center"
type="button"
icon={<Trash2 className="mr-2 size-4" />}
>
<Trash2 className="mr-2 size-4" />
{t("actions.delete")}
</ActionButton>
</form>

View File

@@ -196,8 +196,8 @@ function BackupConfigurationForm() {
type="submit"
loading={isUpdating}
className="items-center"
icon={<Save className="mr-2 size-4" />}
>
<Save className="mr-2 size-4" />
{t("settings.backups.configuration.save_settings")}
</ActionButton>
</form>
@@ -314,8 +314,8 @@ function BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) {
onClick={() => deleteBackup({ backupId: backup.id })}
className="items-center"
type="button"
icon={<Trash2 className="mr-2 size-4" />}
>
<Trash2 className="mr-2 size-4" />
{t("settings.backups.list.actions.delete_backup")}
</ActionButton>
)}
@@ -370,8 +370,8 @@ function BackupsList() {
loading={isTriggering}
variant="default"
className="items-center"
icon={<Play className="mr-2 size-4" />}
>
<Play className="mr-2 size-4" />
{t("settings.backups.list.create_backup_now")}
</ActionButton>
</div>

View File

@@ -180,8 +180,8 @@ export function FeedsEditorDialog() {
loading={isCreating}
variant="default"
className="items-center"
icon={<Plus className="mr-2 size-4" />}
>
<Plus className="mr-2 size-4" />
Add
</ActionButton>
</DialogFooter>
@@ -328,8 +328,8 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
})}
type="submit"
className="items-center"
icon={<Save className="mr-2 size-4" />}
>
<Save className="mr-2 size-4" />
{t("actions.save")}
</ActionButton>
</DialogFooter>
@@ -424,9 +424,8 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
variant="ghost"
className="items-center"
onClick={() => fetchNow({ feedId: feed.id })}
>
<ArrowDownToLine className="size-4" />
</ActionButton>
icon={<ArrowDownToLine className="size-4" />}
/>
</TooltipTrigger>
<TooltipContent>{t("actions.fetch_now")}</TooltipContent>
</Tooltip>
@@ -440,8 +439,8 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
onClick={() => deleteFeed({ feedId: feed.id })}
className="items-center"
type="button"
icon={<Trash2 className="mr-2 size-4" />}
>
<Trash2 className="mr-2 size-4" />
{t("actions.delete")}
</ActionButton>
)}

View File

@@ -168,8 +168,8 @@ export function WebhooksEditorDialog() {
loading={isCreating}
variant="default"
className="items-center"
icon={<Plus className="mr-2 size-4" />}
>
<Plus className="mr-2 size-4" />
Add
</ActionButton>
</DialogFooter>
@@ -291,8 +291,8 @@ export function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) {
})}
type="submit"
className="items-center"
icon={<Save className="mr-2 size-4" />}
>
<Save className="mr-2 size-4" />
{t("actions.save")}
</ActionButton>
</DialogFooter>
@@ -410,8 +410,8 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) {
});
})}
className="items-center"
icon={<Trash2 className="mr-2 size-4" />}
>
<Trash2 className="mr-2 size-4" />
{t("actions.delete")}
</ActionButton>
<ActionButton
@@ -421,8 +421,8 @@ export function EditTokenDialog({ webhook }: { webhook: ZWebhook }) {
})}
type="submit"
className="items-center"
icon={<Save className="mr-2 size-4" />}
>
<Save className="mr-2 size-4" />
{t("actions.save")}
</ActionButton>
</DialogFooter>
@@ -462,8 +462,8 @@ export function WebhookRow({ webhook }: { webhook: ZWebhook }) {
onClick={() => deleteWebhook({ webhookId: webhook.id })}
className="items-center"
type="button"
icon={<Trash2 className="mr-2 size-4" />}
>
<Trash2 className="mr-2 size-4" />
{t("actions.delete")}
</ActionButton>
)}

View File

@@ -15,11 +15,20 @@ interface ActionButtonProps extends ButtonProps {
loading: boolean;
spinner?: React.ReactNode;
ignoreDemoMode?: boolean;
icon?: React.ReactNode;
}
const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
(
{ children, loading, spinner, disabled, ignoreDemoMode = false, ...props },
{
children,
loading,
spinner,
disabled,
ignoreDemoMode = false,
icon,
...props
},
ref,
) => {
const clientConfig = useClientConfig();
@@ -31,9 +40,25 @@ const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
} else if (loading) {
disabled = true;
}
// Determine button content based on loading state and icon prop
let content;
if (icon) {
// If icon is provided, show spinner instead of icon when loading, keep text
content = (
<>
{loading ? spinner : icon}
{children}
</>
);
} else {
// Fallback to old behavior: replace entire content with spinner when loading
content = loading ? spinner : children;
}
return (
<Button ref={ref} {...props} disabled={disabled}>
{loading ? spinner : children}
{content}
</Button>
);
},

View File

@@ -278,8 +278,8 @@ export default function ToolbarPlugin({
onClick={() => {
onSave?.();
}}
icon={<Save className="size-4" />}
>
<Save className="size-4" />
Save
</ActionButton>
)}