From e3ce03bbc403878f8138fad64e83090874252004 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 13 Apr 2026 18:11:38 -0400 Subject: [PATCH 01/16] Replace limits cards with inline limit and alert controls --- .gitignore | 1 + src/app/dashboard/[teamSlug]/limits/page.tsx | 13 +- src/features/dashboard/layouts/page.tsx | 2 +- src/features/dashboard/limits/alert-card.tsx | 40 -- src/features/dashboard/limits/limit-card.tsx | 51 -- src/features/dashboard/limits/limit-form.tsx | 492 ++++++++++++------ .../dashboard/limits/limit-section.tsx | 94 ++++ .../dashboard/limits/usage-limits.tsx | 63 ++- 8 files changed, 477 insertions(+), 279 deletions(-) delete mode 100644 src/features/dashboard/limits/alert-card.tsx delete mode 100644 src/features/dashboard/limits/limit-card.tsx create mode 100644 src/features/dashboard/limits/limit-section.tsx diff --git a/.gitignore b/.gitignore index b6b85df6d..8d1a59ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ next-env.d.ts # AI agents and related files CLAUDE.md +.cursor .agent diff --git a/src/app/dashboard/[teamSlug]/limits/page.tsx b/src/app/dashboard/[teamSlug]/limits/page.tsx index e97b7f7b9..334109859 100644 --- a/src/app/dashboard/[teamSlug]/limits/page.tsx +++ b/src/app/dashboard/[teamSlug]/limits/page.tsx @@ -1,6 +1,6 @@ -import UsageLimits from '@/features/dashboard/limits/usage-limits' +import { Page } from '@/features/dashboard/layouts/page' +import { UsageLimits } from '@/features/dashboard/limits/usage-limits' import { HydrateClient, prefetch, trpc } from '@/trpc/server' -import Frame from '@/ui/frame' interface LimitsPageProps { params: Promise<{ teamSlug: string }> @@ -13,14 +13,9 @@ export default async function LimitsPage({ params }: LimitsPageProps) { return ( - + - + ) } diff --git a/src/features/dashboard/layouts/page.tsx b/src/features/dashboard/layouts/page.tsx index ab28a6947..7590f26df 100644 --- a/src/features/dashboard/layouts/page.tsx +++ b/src/features/dashboard/layouts/page.tsx @@ -7,7 +7,7 @@ interface PageProps { } export const Page = ({ children, className }: PageProps) => ( -
+
{children}
) diff --git a/src/features/dashboard/limits/alert-card.tsx b/src/features/dashboard/limits/alert-card.tsx deleted file mode 100644 index d67bcd719..000000000 --- a/src/features/dashboard/limits/alert-card.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client' - -import type { BillingLimit } from '@/core/modules/billing/models' -import { useRouteParams } from '@/lib/hooks/use-route-params' -import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' -import { useDashboard } from '../context' -import LimitForm from './limit-form' - -interface AlertCardProps { - className?: string - value: BillingLimit['alert_amount_gte'] -} - -export default function AlertCard({ className, value }: AlertCardProps) { - const { team } = useDashboard() - const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/limits'>() - - if (!team) return null - - return ( - - - Set a Budget Alert - - - -

- If your team exceeds this threshold in a given month, you'll - receive an alert notification to {team.email}. This will not - result in any interruptions to your service. -

-
-
- ) -} diff --git a/src/features/dashboard/limits/limit-card.tsx b/src/features/dashboard/limits/limit-card.tsx deleted file mode 100644 index f668eb58f..000000000 --- a/src/features/dashboard/limits/limit-card.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client' - -import type { BillingLimit } from '@/core/modules/billing/models' -import { useRouteParams } from '@/lib/hooks/use-route-params' -import { Card, CardContent, CardHeader, CardTitle } from '@/ui/primitives/card' -import { useDashboard } from '../context' -import LimitForm from './limit-form' - -interface LimitCardProps { - className?: string - value: BillingLimit['limit_amount_gte'] -} - -export default function LimitCard({ className, value }: LimitCardProps) { - const { team } = useDashboard() - const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/limits'>() - - if (!team) return null - - return ( - - - Enable Budget Limit - - - -

- If your team exceeds this threshold in a given billing period, - subsequent API requests will be blocked. -

-

- You will automatically receive email notifications when your usage - reaches 50%, 80%, 90%, and 100% of this - limit. -

-
-

- Caution: Enabling a budget limit may cause interruptions to - your service. Once your Budget Limit is reached, your team will not be - able to create new sandboxes in the given billing period unless the - limit is increased. -

-
-
- ) -} diff --git a/src/features/dashboard/limits/limit-form.tsx b/src/features/dashboard/limits/limit-form.tsx index 2d99dfa1a..447747026 100644 --- a/src/features/dashboard/limits/limit-form.tsx +++ b/src/features/dashboard/limits/limit-form.tsx @@ -1,10 +1,10 @@ 'use client' -import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' -import { useForm } from 'react-hook-form' +import { Pencil, Trash2, TriangleAlert } from 'lucide-react' +import { type FormEvent, type ReactNode, useEffect, useState } from 'react' import { z } from 'zod' +import type { BillingLimit } from '@/core/modules/billing/models' import { defaultErrorToast, defaultSuccessToast, @@ -12,75 +12,160 @@ import { } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { useTRPC } from '@/trpc/client' -import { NumberInput } from '@/ui/number-input' import { Button } from '@/ui/primitives/button' import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/ui/primitives/form' + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from '@/ui/primitives/dialog' +import { Input } from '@/ui/primitives/input' interface LimitFormProps { - teamSlug: string className?: string originalValue: number | null + teamSlug: string type: 'limit' | 'alert' } -const formSchema = z.object({ - value: z - .number() - .min(0, 'Value must be greater than or equal to 0') - .nullable(), -}) +interface LimitConfirmationDialogProps { + actions: ReactNode + children: ReactNode + icon: ReactNode + open: boolean + onOpenChange: (open: boolean) => void + title: string +} + +const limitCopy = { + alert: { + clearError: 'Failed to remove billing alert.', + clearSuccess: 'Billing alert removed.', + emptyError: 'Enter a billing alert amount.', + saveError: 'Failed to save billing alert.', + saveSuccess: 'Billing alert saved.', + }, + limit: { + clearError: 'Failed to remove billing limit.', + clearSuccess: 'Billing limit removed.', + emptyError: 'Enter a billing limit amount.', + saveError: 'Failed to save billing limit.', + saveSuccess: 'Billing limit saved.', + }, +} satisfies Record< + LimitFormProps['type'], + { + clearError: string + clearSuccess: string + emptyError: string + saveError: string + saveSuccess: string + } +> + +const currencyFormatter = new Intl.NumberFormat('en-US') + +const limitValueSchema = z + .string() + .trim() + .min(1, 'Enter a value.') + .regex(/^\d+$/, 'Enter a whole USD amount.') + .transform(Number) + .refine((value) => value >= 1, 'Value must be at least 1.') + +// Removes non-digits from a currency draft. Example: "$1,250" -> "1250". +const sanitizeCurrencyInput = (value: string) => value.replace(/\D+/g, '') -type FormData = z.infer +// Formats a numeric billing limit for display. Example: 1250 -> "1,250". +const formatCurrencyValue = (value: number) => currencyFormatter.format(value) + +// Updates the matching limit field in cached billing data. Example: ("alert", 50) -> { alert_amount_gte: 50 }. +const updateLimitValue = ( + limits: BillingLimit | undefined, + type: LimitFormProps['type'], + nextValue: number | null +) => { + if (!limits) return limits + if (type === 'limit') return { ...limits, limit_amount_gte: nextValue } + return { ...limits, alert_amount_gte: nextValue } +} + +const LimitConfirmationDialog = ({ + actions, + children, + icon, + open, + onOpenChange, + title, +}: LimitConfirmationDialogProps) => ( + + +
+
+
+
{icon}
+ + {title} + +
+ + {children} + +
+
+ {actions} +
+
+
+
+) export default function LimitForm({ - teamSlug, className, originalValue, + teamSlug, type, }: LimitFormProps) { - 'use no memo' - - const [isEditing, setIsEditing] = useState(false) + const [draftValue, setDraftValue] = useState( + originalValue === null ? '' : formatCurrencyValue(originalValue) + ) + const [isEditing, setIsEditing] = useState(originalValue === null) + const [isRemoveDialogOpen, setIsRemoveDialogOpen] = useState(false) + const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false) const { toast } = useToast() const trpc = useTRPC() const queryClient = useQueryClient() - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - value: originalValue, - }, - }) + const copy = limitCopy[type] const limitsQueryKey = trpc.billing.getLimits.queryOptions({ teamSlug, }).queryKey + useEffect(() => { + setDraftValue( + originalValue === null ? '' : formatCurrencyValue(originalValue) + ) + setIsEditing(originalValue === null) + }, [originalValue]) + const setLimitMutation = useMutation( trpc.billing.setLimit.mutationOptions({ - onSuccess: () => { - toast( - defaultSuccessToast( - `Billing ${type === 'limit' ? 'limit' : 'alert'} saved.` - ) + onSuccess: (_, variables) => { + queryClient.setQueryData( + limitsQueryKey, + (limits) => updateLimitValue(limits, type, variables.value) ) + toast(defaultSuccessToast(copy.saveSuccess)) + setDraftValue(formatCurrencyValue(variables.value)) setIsEditing(false) + setIsSaveDialogOpen(false) queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, onError: (error) => { - toast( - defaultErrorToast( - error.message || - `Failed to save billing ${type === 'limit' ? 'limit' : 'alert'}.` - ) - ) + toast(defaultErrorToast(error.message || copy.saveError)) }, }) ) @@ -88,150 +173,235 @@ export default function LimitForm({ const clearLimitMutation = useMutation( trpc.billing.clearLimit.mutationOptions({ onSuccess: () => { - toast( - defaultSuccessToast( - `Billing ${type === 'limit' ? 'limit' : 'alert'} cleared.` - ) + queryClient.setQueryData( + limitsQueryKey, + (limits) => updateLimitValue(limits, type, null) ) - setIsEditing(false) - form.reset({ value: null }) + toast(defaultSuccessToast(copy.clearSuccess)) + setDraftValue('') + setIsEditing(true) + setIsRemoveDialogOpen(false) queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, - onError: () => { - toast( - defaultErrorToast( - `Failed to clear billing ${type === 'limit' ? 'limit' : 'alert'}.` - ) - ) + onError: (error) => { + toast(defaultErrorToast(error.message || copy.clearError)) }, }) ) - const handleSave = (data: FormData) => { - if (!data.value) { - toast(defaultErrorToast('Input cannot be empty.')) + const parsedValue = limitValueSchema.safeParse(draftValue) + const nextValue = parsedValue.success ? parsedValue.data : null + const isMutating = setLimitMutation.isPending || clearLimitMutation.isPending + const canSave = + parsedValue.success && nextValue !== originalValue && !isMutating + + const handleCancel = () => { + if (originalValue === null) return + + setDraftValue(formatCurrencyValue(originalValue)) + setIsEditing(false) + } + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + + if (!parsedValue.success) { + toast( + defaultErrorToast( + parsedValue.error.issues[0]?.message || copy.emptyError + ) + ) return } - setLimitMutation.mutate({ - teamSlug, - type, - value: data.value, - }) - } + if (parsedValue.data === originalValue) return + if (type === 'limit') return setIsSaveDialogOpen(true) - const handleClear = () => { - clearLimitMutation.mutate({ - teamSlug, - type, - }) + setLimitMutation.mutate({ teamSlug, type, value: parsedValue.data }) } - const isSaving = setLimitMutation.isPending - const isClearing = clearLimitMutation.isPending + const handleRemove = () => { + if (type === 'limit') return setIsRemoveDialogOpen(true) - if (originalValue === null || isEditing) { + clearLimitMutation.mutate({ teamSlug, type }) + } + + if (originalValue !== null && !isEditing) return ( -
- +
-
- ( - - - $ [USD] - - - { - field.onChange(value) - }} - placeholder={'$'} - /> - {/* { - const value = - e.target.value === '' ? null : Number(e.target.value) - field.onChange(value) - }} - value={field.value ?? ''} - /> */} - - - - )} - /> +
+ $ + + {formatCurrencyValue(originalValue)} + +
+
+ - {originalValue !== null && ( - - )}
- - +
+ {type === 'limit' && ( + + + + + } + icon={} + open={isRemoveDialogOpen} + onOpenChange={setIsRemoveDialogOpen} + title="Remove usage limit?" + > + API limits will be removed and usage will become uncapped. + + )} + ) - } return ( -
-
- {'$ '} - - {originalValue?.toLocaleString()} - -
- - -
+
+ $ + + setDraftValue(sanitizeCurrencyInput(event.target.value)) + } + placeholder="--" + value={draftValue} + /> +
+
+ {originalValue !== null && ( + + )} + +
+ + {type === 'limit' && ( + + + + + } + icon={ + + } + open={isSaveDialogOpen} + onOpenChange={setIsSaveDialogOpen} + title={`Set $${nextValue === null ? '--' : formatCurrencyValue(nextValue)} usage limit?`} + > + If your API usage hits this limit, all requests including sandbox + creation will be blocked. This may interrupt your services until you + raise or remove the limit. + + )} + ) } diff --git a/src/features/dashboard/limits/limit-section.tsx b/src/features/dashboard/limits/limit-section.tsx new file mode 100644 index 000000000..6891ec8dc --- /dev/null +++ b/src/features/dashboard/limits/limit-section.tsx @@ -0,0 +1,94 @@ +'use client' + +import { Bell, CircleDollarSign, TriangleAlert } from 'lucide-react' +import { cn } from '@/lib/utils' +import LimitForm from './limit-form' + +type LimitType = 'limit' | 'alert' + +interface LimitSectionProps { + className?: string + email: string + teamSlug: string + title: string + type: LimitType + value: number | null +} + +const currencyFormatter = new Intl.NumberFormat('en-US') + +const LimitPanelIcon = ({ type }: { type: LimitType }) => { + const iconClassName = + type === 'limit' + ? 'text-accent-warning-highlight' + : 'text-accent-main-highlight' + + return ( +
+ +
+ ) +} + +const LimitSectionInfo = ({ + email, + type, + value, +}: Pick) => { + if (type === 'alert') { + const alertMessage = + value === null + ? `Informative alert will be sent to ${email} when this threshold is reached` + : `Informative alert will be sent to ${email} when the $${currencyFormatter.format(value)} threshold is reached` + + return ( +

+ {alertMessage} +

+ ) + } + + const limitMessage = + value === null + ? 'All API requests are blocked after reaching this limit' + : `All API requests are blocked after reaching $${currencyFormatter.format(value)}` + + return ( +
+

+ + {limitMessage} +

+

+ + Automatic alerts at 50%, 80%, 90% and 100% sent to {email} +

+
+ ) +} + +export const LimitSection = ({ + className, + email, + teamSlug, + title, + type, + value, +}: LimitSectionProps) => { + return ( +
+

+ {title} +

+
+
+ +
+
+ +
+
+ +
+ ) +} diff --git a/src/features/dashboard/limits/usage-limits.tsx b/src/features/dashboard/limits/usage-limits.tsx index ca47b1eda..1ebc61487 100644 --- a/src/features/dashboard/limits/usage-limits.tsx +++ b/src/features/dashboard/limits/usage-limits.tsx @@ -5,15 +5,16 @@ import { useRouteParams } from '@/lib/hooks/use-route-params' import { cn } from '@/lib/utils' import { useTRPC } from '@/trpc/client' import { Skeleton } from '@/ui/primitives/skeleton' -import AlertCard from './alert-card' -import LimitCard from './limit-card' +import { useDashboard } from '../context' +import { LimitSection } from './limit-section' interface UsageLimitsProps { className?: string } -export default function UsageLimits({ className }: UsageLimitsProps) { +export const UsageLimits = ({ className }: UsageLimitsProps) => { const { teamSlug } = useRouteParams<'/dashboard/[teamSlug]/limits'>() + const { team } = useDashboard() const trpc = useTRPC() const { data: limits, isLoading } = useQuery({ @@ -21,25 +22,53 @@ export default function UsageLimits({ className }: UsageLimitsProps) { throwOnError: true, }) - if (isLoading || !limits) { + if (!team) return null + + if (isLoading || !limits) return ( -
-
- - -
-
- - -
+
+ +
) - } return ( -
- - +
+ +
) } + +const LimitsSectionSkeleton = () => ( +
+ + +
+ + +
+
+) From 151d6883d8dd1e918bb3042c96656f99cf530305 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 13 Apr 2026 20:09:03 -0400 Subject: [PATCH 02/16] Refactor each icon into its own file with a common wrapper for shared typing, classes and accessibility --- .../dashboard/limits/limit-section.tsx | 26 +- .../icons/account-settings-icon.tsx | 21 + src/ui/primitives/icons/add-icon.tsx | 20 + src/ui/primitives/icons/alert-ascii-icon.tsx | 55 + src/ui/primitives/icons/alert-icon.tsx | 17 + src/ui/primitives/icons/area-chart-icon.tsx | 13 + src/ui/primitives/icons/arrow-down-icon.tsx | 13 + src/ui/primitives/icons/block-icon.tsx | 13 + src/ui/primitives/icons/build-icon.tsx | 19 + src/ui/primitives/icons/card-icon.tsx | 13 + src/ui/primitives/icons/check-icon.tsx | 13 + src/ui/primitives/icons/chevron-down-icon.tsx | 12 + src/ui/primitives/icons/chevron-left-icon.tsx | 14 + .../primitives/icons/chevron-right-icon.tsx | 14 + src/ui/primitives/icons/chevron-up-icon.tsx | 12 + src/ui/primitives/icons/close-icon.tsx | 13 + src/ui/primitives/icons/code-chevron-icon.tsx | 11 + .../primitives/icons/collapse-left-icon.tsx | 25 + src/ui/primitives/icons/collapse-top-icon.tsx | 25 + src/ui/primitives/icons/copy-icon.tsx | 13 + src/ui/primitives/icons/cpu-icon.tsx | 13 + src/ui/primitives/icons/credits-icon.tsx | 13 + .../{icons.tsx => icons/docs-icon.tsx} | 1397 +---------------- src/ui/primitives/icons/dot-icon.tsx | 8 + src/ui/primitives/icons/edit-icon.tsx | 13 + src/ui/primitives/icons/enterprise-icon.tsx | 14 + src/ui/primitives/icons/expand-down-icon.tsx | 25 + src/ui/primitives/icons/expand-right-icon.tsx | 25 + src/ui/primitives/icons/expiry-icon.tsx | 24 + src/ui/primitives/icons/explore-icon.tsx | 17 + .../primitives/icons/external-link-icon.tsx | 13 + src/ui/primitives/icons/filter-icon.tsx | 28 + src/ui/primitives/icons/gauge-icon.tsx | 13 + src/ui/primitives/icons/github-icon.tsx | 14 + src/ui/primitives/icons/history-icon.tsx | 25 + src/ui/primitives/icons/icon.tsx | 15 + src/ui/primitives/icons/index.ts | 73 + .../primitives/icons/indicator-dots-icon.tsx | 22 + src/ui/primitives/icons/info-icon.tsx | 17 + src/ui/primitives/icons/invoice-icon.tsx | 13 + src/ui/primitives/icons/key-icon.tsx | 19 + src/ui/primitives/icons/limit-ascii-icon.tsx | 55 + src/ui/primitives/icons/list-icon.tsx | 23 + src/ui/primitives/icons/logout-icon.tsx | 13 + src/ui/primitives/icons/memory-icon.tsx | 14 + src/ui/primitives/icons/menu-icon.tsx | 9 + src/ui/primitives/icons/metadata-icon.tsx | 13 + src/ui/primitives/icons/new-tab-icon.tsx | 13 + src/ui/primitives/icons/paused-icon.tsx | 17 + src/ui/primitives/icons/person-icon.tsx | 11 + src/ui/primitives/icons/persons-icon.tsx | 13 + src/ui/primitives/icons/photo-icon.tsx | 13 + src/ui/primitives/icons/pie-chart-icon.tsx | 12 + src/ui/primitives/icons/private-icon.tsx | 11 + src/ui/primitives/icons/refresh-icon.tsx | 13 + src/ui/primitives/icons/remove-icon.tsx | 13 + src/ui/primitives/icons/running-icon.tsx | 13 + src/ui/primitives/icons/sandbox-icon.tsx | 13 + src/ui/primitives/icons/search-icon.tsx | 13 + src/ui/primitives/icons/send-icon.tsx | 11 + src/ui/primitives/icons/settings-icon.tsx | 17 + src/ui/primitives/icons/status-icon.tsx | 15 + src/ui/primitives/icons/storage-icon.tsx | 13 + src/ui/primitives/icons/support-icon.tsx | 13 + src/ui/primitives/icons/template-icon.tsx | 32 + src/ui/primitives/icons/thumbs-down-icon.tsx | 12 + src/ui/primitives/icons/thumbs-up-icon.tsx | 12 + src/ui/primitives/icons/time-icon.tsx | 13 + src/ui/primitives/icons/trash-icon.tsx | 25 + src/ui/primitives/icons/trend-icon.tsx | 13 + src/ui/primitives/icons/types.ts | 8 + src/ui/primitives/icons/unhealthy-icon.tsx | 12 + src/ui/primitives/icons/unpack-icon.tsx | 13 + src/ui/primitives/icons/upgrade-icon.tsx | 12 + src/ui/primitives/icons/usage-icon.tsx | 13 + src/ui/primitives/icons/warning-icon.tsx | 13 + src/ui/primitives/icons/webhook-icon.tsx | 32 + 77 files changed, 1319 insertions(+), 1405 deletions(-) create mode 100644 src/ui/primitives/icons/account-settings-icon.tsx create mode 100644 src/ui/primitives/icons/add-icon.tsx create mode 100644 src/ui/primitives/icons/alert-ascii-icon.tsx create mode 100644 src/ui/primitives/icons/alert-icon.tsx create mode 100644 src/ui/primitives/icons/area-chart-icon.tsx create mode 100644 src/ui/primitives/icons/arrow-down-icon.tsx create mode 100644 src/ui/primitives/icons/block-icon.tsx create mode 100644 src/ui/primitives/icons/build-icon.tsx create mode 100644 src/ui/primitives/icons/card-icon.tsx create mode 100644 src/ui/primitives/icons/check-icon.tsx create mode 100644 src/ui/primitives/icons/chevron-down-icon.tsx create mode 100644 src/ui/primitives/icons/chevron-left-icon.tsx create mode 100644 src/ui/primitives/icons/chevron-right-icon.tsx create mode 100644 src/ui/primitives/icons/chevron-up-icon.tsx create mode 100644 src/ui/primitives/icons/close-icon.tsx create mode 100644 src/ui/primitives/icons/code-chevron-icon.tsx create mode 100644 src/ui/primitives/icons/collapse-left-icon.tsx create mode 100644 src/ui/primitives/icons/collapse-top-icon.tsx create mode 100644 src/ui/primitives/icons/copy-icon.tsx create mode 100644 src/ui/primitives/icons/cpu-icon.tsx create mode 100644 src/ui/primitives/icons/credits-icon.tsx rename src/ui/primitives/{icons.tsx => icons/docs-icon.tsx} (64%) create mode 100644 src/ui/primitives/icons/dot-icon.tsx create mode 100644 src/ui/primitives/icons/edit-icon.tsx create mode 100644 src/ui/primitives/icons/enterprise-icon.tsx create mode 100644 src/ui/primitives/icons/expand-down-icon.tsx create mode 100644 src/ui/primitives/icons/expand-right-icon.tsx create mode 100644 src/ui/primitives/icons/expiry-icon.tsx create mode 100644 src/ui/primitives/icons/explore-icon.tsx create mode 100644 src/ui/primitives/icons/external-link-icon.tsx create mode 100644 src/ui/primitives/icons/filter-icon.tsx create mode 100644 src/ui/primitives/icons/gauge-icon.tsx create mode 100644 src/ui/primitives/icons/github-icon.tsx create mode 100644 src/ui/primitives/icons/history-icon.tsx create mode 100644 src/ui/primitives/icons/icon.tsx create mode 100644 src/ui/primitives/icons/index.ts create mode 100644 src/ui/primitives/icons/indicator-dots-icon.tsx create mode 100644 src/ui/primitives/icons/info-icon.tsx create mode 100644 src/ui/primitives/icons/invoice-icon.tsx create mode 100644 src/ui/primitives/icons/key-icon.tsx create mode 100644 src/ui/primitives/icons/limit-ascii-icon.tsx create mode 100644 src/ui/primitives/icons/list-icon.tsx create mode 100644 src/ui/primitives/icons/logout-icon.tsx create mode 100644 src/ui/primitives/icons/memory-icon.tsx create mode 100644 src/ui/primitives/icons/menu-icon.tsx create mode 100644 src/ui/primitives/icons/metadata-icon.tsx create mode 100644 src/ui/primitives/icons/new-tab-icon.tsx create mode 100644 src/ui/primitives/icons/paused-icon.tsx create mode 100644 src/ui/primitives/icons/person-icon.tsx create mode 100644 src/ui/primitives/icons/persons-icon.tsx create mode 100644 src/ui/primitives/icons/photo-icon.tsx create mode 100644 src/ui/primitives/icons/pie-chart-icon.tsx create mode 100644 src/ui/primitives/icons/private-icon.tsx create mode 100644 src/ui/primitives/icons/refresh-icon.tsx create mode 100644 src/ui/primitives/icons/remove-icon.tsx create mode 100644 src/ui/primitives/icons/running-icon.tsx create mode 100644 src/ui/primitives/icons/sandbox-icon.tsx create mode 100644 src/ui/primitives/icons/search-icon.tsx create mode 100644 src/ui/primitives/icons/send-icon.tsx create mode 100644 src/ui/primitives/icons/settings-icon.tsx create mode 100644 src/ui/primitives/icons/status-icon.tsx create mode 100644 src/ui/primitives/icons/storage-icon.tsx create mode 100644 src/ui/primitives/icons/support-icon.tsx create mode 100644 src/ui/primitives/icons/template-icon.tsx create mode 100644 src/ui/primitives/icons/thumbs-down-icon.tsx create mode 100644 src/ui/primitives/icons/thumbs-up-icon.tsx create mode 100644 src/ui/primitives/icons/time-icon.tsx create mode 100644 src/ui/primitives/icons/trash-icon.tsx create mode 100644 src/ui/primitives/icons/trend-icon.tsx create mode 100644 src/ui/primitives/icons/types.ts create mode 100644 src/ui/primitives/icons/unhealthy-icon.tsx create mode 100644 src/ui/primitives/icons/unpack-icon.tsx create mode 100644 src/ui/primitives/icons/upgrade-icon.tsx create mode 100644 src/ui/primitives/icons/usage-icon.tsx create mode 100644 src/ui/primitives/icons/warning-icon.tsx create mode 100644 src/ui/primitives/icons/webhook-icon.tsx diff --git a/src/features/dashboard/limits/limit-section.tsx b/src/features/dashboard/limits/limit-section.tsx index 6891ec8dc..63375fabe 100644 --- a/src/features/dashboard/limits/limit-section.tsx +++ b/src/features/dashboard/limits/limit-section.tsx @@ -1,7 +1,8 @@ 'use client' -import { Bell, CircleDollarSign, TriangleAlert } from 'lucide-react' +import { Bell, TriangleAlert } from 'lucide-react' import { cn } from '@/lib/utils' +import { AlertAsciiIcon, LimitAsciiIcon } from '@/ui/primitives/icons' import LimitForm from './limit-form' type LimitType = 'limit' | 'alert' @@ -17,15 +18,16 @@ interface LimitSectionProps { const currencyFormatter = new Intl.NumberFormat('en-US') -const LimitPanelIcon = ({ type }: { type: LimitType }) => { - const iconClassName = - type === 'limit' - ? 'text-accent-warning-highlight' - : 'text-accent-main-highlight' +const iconMap: Record = { + limit: LimitAsciiIcon, + alert: AlertAsciiIcon, +} +const LimitPanelIcon = ({ type }: { type: LimitType }) => { + const Icon = iconMap[type] return ( -
- +
+
) } @@ -80,11 +82,9 @@ export const LimitSection = ({

{title}

-
-
- -
-
+
+ +
diff --git a/src/ui/primitives/icons/account-settings-icon.tsx b/src/ui/primitives/icons/account-settings-icon.tsx new file mode 100644 index 000000000..966c71b95 --- /dev/null +++ b/src/ui/primitives/icons/account-settings-icon.tsx @@ -0,0 +1,21 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const AccountSettingsIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/add-icon.tsx b/src/ui/primitives/icons/add-icon.tsx new file mode 100644 index 000000000..132d6f1b6 --- /dev/null +++ b/src/ui/primitives/icons/add-icon.tsx @@ -0,0 +1,20 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const AddIcon = (props: IconProps) => ( + + + + + + + + + + +) diff --git a/src/ui/primitives/icons/alert-ascii-icon.tsx b/src/ui/primitives/icons/alert-ascii-icon.tsx new file mode 100644 index 000000000..a43485d84 --- /dev/null +++ b/src/ui/primitives/icons/alert-ascii-icon.tsx @@ -0,0 +1,55 @@ +import type React from 'react' +import { cn } from '@/lib/utils/index' +import type { IconProps } from '@/ui/primitives/icons/types' + +const LINES = [ + ' ', + ' ', + ' ', + ' ', + ' -********- ', + ' -*-- -*- ', + ' -*- -*- ', + ' **- -** ', + ' -*- -*- ', + ' -**----------**- ', + ' ----**----**---- ', + ' -******- ', + ' ', + ' ', + ' ', + ' ', +] + +const TEXT_STYLE: React.CSSProperties = { + fontSize: '3.8px', + fontFamily: 'var(--font-mono)', + fontWeight: 600, + letterSpacing: '-0.038px', + fontFeatureSettings: "'ss03' 1", +} + +export const AlertAsciiIcon = ({ className, ...props }: IconProps) => ( + + Alert Ascii + {LINES.map((line, i) => ( + + {line} + + ))} + +) diff --git a/src/ui/primitives/icons/alert-icon.tsx b/src/ui/primitives/icons/alert-icon.tsx new file mode 100644 index 000000000..496f71660 --- /dev/null +++ b/src/ui/primitives/icons/alert-icon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const AlertIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/area-chart-icon.tsx b/src/ui/primitives/icons/area-chart-icon.tsx new file mode 100644 index 000000000..264d78af9 --- /dev/null +++ b/src/ui/primitives/icons/area-chart-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const AreaChartIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/arrow-down-icon.tsx b/src/ui/primitives/icons/arrow-down-icon.tsx new file mode 100644 index 000000000..39c95cab4 --- /dev/null +++ b/src/ui/primitives/icons/arrow-down-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ArrowDownIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/block-icon.tsx b/src/ui/primitives/icons/block-icon.tsx new file mode 100644 index 000000000..881e70d52 --- /dev/null +++ b/src/ui/primitives/icons/block-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const BlockIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/build-icon.tsx b/src/ui/primitives/icons/build-icon.tsx new file mode 100644 index 000000000..24cfe63b6 --- /dev/null +++ b/src/ui/primitives/icons/build-icon.tsx @@ -0,0 +1,19 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const BuildIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/card-icon.tsx b/src/ui/primitives/icons/card-icon.tsx new file mode 100644 index 000000000..3db134816 --- /dev/null +++ b/src/ui/primitives/icons/card-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CardIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/check-icon.tsx b/src/ui/primitives/icons/check-icon.tsx new file mode 100644 index 000000000..81c55e422 --- /dev/null +++ b/src/ui/primitives/icons/check-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CheckIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/chevron-down-icon.tsx b/src/ui/primitives/icons/chevron-down-icon.tsx new file mode 100644 index 000000000..f835887f1 --- /dev/null +++ b/src/ui/primitives/icons/chevron-down-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ChevronDownIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/chevron-left-icon.tsx b/src/ui/primitives/icons/chevron-left-icon.tsx new file mode 100644 index 000000000..9e39c16f7 --- /dev/null +++ b/src/ui/primitives/icons/chevron-left-icon.tsx @@ -0,0 +1,14 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ChevronLeftIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/chevron-right-icon.tsx b/src/ui/primitives/icons/chevron-right-icon.tsx new file mode 100644 index 000000000..d16b272d9 --- /dev/null +++ b/src/ui/primitives/icons/chevron-right-icon.tsx @@ -0,0 +1,14 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ChevronRightIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/chevron-up-icon.tsx b/src/ui/primitives/icons/chevron-up-icon.tsx new file mode 100644 index 000000000..ff21e36c9 --- /dev/null +++ b/src/ui/primitives/icons/chevron-up-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ChevronUpIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/close-icon.tsx b/src/ui/primitives/icons/close-icon.tsx new file mode 100644 index 000000000..e2ddbe198 --- /dev/null +++ b/src/ui/primitives/icons/close-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CloseIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/code-chevron-icon.tsx b/src/ui/primitives/icons/code-chevron-icon.tsx new file mode 100644 index 000000000..0f602593c --- /dev/null +++ b/src/ui/primitives/icons/code-chevron-icon.tsx @@ -0,0 +1,11 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CodeChevronIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/collapse-left-icon.tsx b/src/ui/primitives/icons/collapse-left-icon.tsx new file mode 100644 index 000000000..69dc36742 --- /dev/null +++ b/src/ui/primitives/icons/collapse-left-icon.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CollapseLeftIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/collapse-top-icon.tsx b/src/ui/primitives/icons/collapse-top-icon.tsx new file mode 100644 index 000000000..13d4eadd5 --- /dev/null +++ b/src/ui/primitives/icons/collapse-top-icon.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CollapseTopIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/copy-icon.tsx b/src/ui/primitives/icons/copy-icon.tsx new file mode 100644 index 000000000..5668df3cf --- /dev/null +++ b/src/ui/primitives/icons/copy-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CopyIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/cpu-icon.tsx b/src/ui/primitives/icons/cpu-icon.tsx new file mode 100644 index 000000000..eeb3ff3fb --- /dev/null +++ b/src/ui/primitives/icons/cpu-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CpuIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/credits-icon.tsx b/src/ui/primitives/icons/credits-icon.tsx new file mode 100644 index 000000000..94e77e5ba --- /dev/null +++ b/src/ui/primitives/icons/credits-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const CreditsIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons/docs-icon.tsx similarity index 64% rename from src/ui/primitives/icons.tsx rename to src/ui/primitives/icons/docs-icon.tsx index 9fb556d21..a9442a7f7 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons/docs-icon.tsx @@ -1,203 +1,8 @@ -import type React from 'react' -import { cn } from '@/lib/utils/index' +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' -const DEFAULT_CLASS_NAMES = 'size-6' - -interface IconProps extends React.SVGProps { - className?: string - height?: number - width?: number -} - -export const SandboxIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const TemplateIcon = ({ className, ...props }: IconProps) => ( - - - - - - -) - -export const UsageIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const PersonsIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const KeyIcon = ({ className, ...props }: IconProps) => ( - - - - -) -export const SettingsIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const CardIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const GaugeIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const CreditsIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const DocsIcon = ({ className, ...props }: IconProps) => ( - +export const DocsIcon = (props: IconProps) => ( + ( strokeMiterlimit="1.5" strokeWidth="1.66667" /> - -) -export const GithubIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const AddIcon = ({ className, ...props }: IconProps) => ( - - - - - - - - - - -) - -export const AccountSettingsIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const LogoutIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const SearchIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const CopyIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const CheckIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const CollapseLeftIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const CollapseTopIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const ExpandDownIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const ExpandRightIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const UnpackIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ExternalLinkIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const FilterIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const IndicatorDotsIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const UnhealthyIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const TimeIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const CloseIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const NewTabIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const HistoryIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const StatusIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const RunningIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const CpuIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const MemoryIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const MetadataIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const RefreshIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ChevronRightIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ChevronLeftIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const TrashIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const EditIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ChevronDownIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ChevronUpIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const PieChartIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const AreaChartIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const SupportIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const BuildIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const RemoveIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const WarningIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const UpgradeIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const AlertIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const CodeChevronIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ExploreIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const PhotoIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const BlockIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ExpiryIcon = ({ className, ...props }: IconProps) => ( - - - - - -) - -export const PersonIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const PrivateIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const EnterpriseIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const TrendIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ListIcon = ({ className, ...props }: IconProps) => ( - - - - - - -) - -export const StorageIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const InfoIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const SendIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ThumbsUpIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ThumbsDownIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const MenuIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const PausedIcon = ({ className, ...props }: IconProps) => ( - - - - -) - -export const DotIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const ArrowDownIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const InvoiceIcon = ({ className, ...props }: IconProps) => ( - - - -) - -export const WebhookIcon = ({ className, ...props }: IconProps) => ( - - - - - - - - - - - - + ) diff --git a/src/ui/primitives/icons/dot-icon.tsx b/src/ui/primitives/icons/dot-icon.tsx new file mode 100644 index 000000000..7f53b7d94 --- /dev/null +++ b/src/ui/primitives/icons/dot-icon.tsx @@ -0,0 +1,8 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const DotIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/edit-icon.tsx b/src/ui/primitives/icons/edit-icon.tsx new file mode 100644 index 000000000..f6e495590 --- /dev/null +++ b/src/ui/primitives/icons/edit-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const EditIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/enterprise-icon.tsx b/src/ui/primitives/icons/enterprise-icon.tsx new file mode 100644 index 000000000..ba6cecd8b --- /dev/null +++ b/src/ui/primitives/icons/enterprise-icon.tsx @@ -0,0 +1,14 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const EnterpriseIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/expand-down-icon.tsx b/src/ui/primitives/icons/expand-down-icon.tsx new file mode 100644 index 000000000..0bc77a8df --- /dev/null +++ b/src/ui/primitives/icons/expand-down-icon.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ExpandDownIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/expand-right-icon.tsx b/src/ui/primitives/icons/expand-right-icon.tsx new file mode 100644 index 000000000..e10ad3ccc --- /dev/null +++ b/src/ui/primitives/icons/expand-right-icon.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ExpandRightIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/expiry-icon.tsx b/src/ui/primitives/icons/expiry-icon.tsx new file mode 100644 index 000000000..17e80ca52 --- /dev/null +++ b/src/ui/primitives/icons/expiry-icon.tsx @@ -0,0 +1,24 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ExpiryIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/explore-icon.tsx b/src/ui/primitives/icons/explore-icon.tsx new file mode 100644 index 000000000..ef91d5c54 --- /dev/null +++ b/src/ui/primitives/icons/explore-icon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ExploreIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/external-link-icon.tsx b/src/ui/primitives/icons/external-link-icon.tsx new file mode 100644 index 000000000..d10ea7954 --- /dev/null +++ b/src/ui/primitives/icons/external-link-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ExternalLinkIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/filter-icon.tsx b/src/ui/primitives/icons/filter-icon.tsx new file mode 100644 index 000000000..1a690ef82 --- /dev/null +++ b/src/ui/primitives/icons/filter-icon.tsx @@ -0,0 +1,28 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const FilterIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/gauge-icon.tsx b/src/ui/primitives/icons/gauge-icon.tsx new file mode 100644 index 000000000..1717d3c00 --- /dev/null +++ b/src/ui/primitives/icons/gauge-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const GaugeIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/github-icon.tsx b/src/ui/primitives/icons/github-icon.tsx new file mode 100644 index 000000000..dc783e26c --- /dev/null +++ b/src/ui/primitives/icons/github-icon.tsx @@ -0,0 +1,14 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const GithubIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/history-icon.tsx b/src/ui/primitives/icons/history-icon.tsx new file mode 100644 index 000000000..571ee3beb --- /dev/null +++ b/src/ui/primitives/icons/history-icon.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const HistoryIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/icon.tsx b/src/ui/primitives/icons/icon.tsx new file mode 100644 index 000000000..5e740bc2a --- /dev/null +++ b/src/ui/primitives/icons/icon.tsx @@ -0,0 +1,15 @@ +import { cn } from '@/lib/utils/index' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const Icon = ({ className, children, name, ...props }: IconProps) => ( + + {name} + {children} + +) diff --git a/src/ui/primitives/icons/index.ts b/src/ui/primitives/icons/index.ts new file mode 100644 index 000000000..d34728482 --- /dev/null +++ b/src/ui/primitives/icons/index.ts @@ -0,0 +1,73 @@ +export { AccountSettingsIcon } from './account-settings-icon' +export { AddIcon } from './add-icon' +export { AlertAsciiIcon } from './alert-ascii-icon' +export { AlertIcon } from './alert-icon' +export { AreaChartIcon } from './area-chart-icon' +export { ArrowDownIcon } from './arrow-down-icon' +export { BlockIcon } from './block-icon' +export { BuildIcon } from './build-icon' +export { CardIcon } from './card-icon' +export { CheckIcon } from './check-icon' +export { ChevronDownIcon } from './chevron-down-icon' +export { ChevronLeftIcon } from './chevron-left-icon' +export { ChevronRightIcon } from './chevron-right-icon' +export { ChevronUpIcon } from './chevron-up-icon' +export { CloseIcon } from './close-icon' +export { CodeChevronIcon } from './code-chevron-icon' +export { CollapseLeftIcon } from './collapse-left-icon' +export { CollapseTopIcon } from './collapse-top-icon' +export { CopyIcon } from './copy-icon' +export { CpuIcon } from './cpu-icon' +export { CreditsIcon } from './credits-icon' +export { DocsIcon } from './docs-icon' +export { DotIcon } from './dot-icon' +export { EditIcon } from './edit-icon' +export { EnterpriseIcon } from './enterprise-icon' +export { ExpandDownIcon } from './expand-down-icon' +export { ExpandRightIcon } from './expand-right-icon' +export { ExpiryIcon } from './expiry-icon' +export { ExploreIcon } from './explore-icon' +export { ExternalLinkIcon } from './external-link-icon' +export { FilterIcon } from './filter-icon' +export { GaugeIcon } from './gauge-icon' +export { GithubIcon } from './github-icon' +export { HistoryIcon } from './history-icon' +export { IndicatorDotsIcon } from './indicator-dots-icon' +export { InfoIcon } from './info-icon' +export { InvoiceIcon } from './invoice-icon' +export { KeyIcon } from './key-icon' +export { LimitAsciiIcon } from './limit-ascii-icon' +export { ListIcon } from './list-icon' +export { LogoutIcon } from './logout-icon' +export { MemoryIcon } from './memory-icon' +export { MenuIcon } from './menu-icon' +export { MetadataIcon } from './metadata-icon' +export { NewTabIcon } from './new-tab-icon' +export { PausedIcon } from './paused-icon' +export { PersonIcon } from './person-icon' +export { PersonsIcon } from './persons-icon' +export { PhotoIcon } from './photo-icon' +export { PieChartIcon } from './pie-chart-icon' +export { PrivateIcon } from './private-icon' +export { RefreshIcon } from './refresh-icon' +export { RemoveIcon } from './remove-icon' +export { RunningIcon } from './running-icon' +export { SandboxIcon } from './sandbox-icon' +export { SearchIcon } from './search-icon' +export { SendIcon } from './send-icon' +export { SettingsIcon } from './settings-icon' +export { StatusIcon } from './status-icon' +export { StorageIcon } from './storage-icon' +export { SupportIcon } from './support-icon' +export { TemplateIcon } from './template-icon' +export { ThumbsDownIcon } from './thumbs-down-icon' +export { ThumbsUpIcon } from './thumbs-up-icon' +export { TimeIcon } from './time-icon' +export { TrashIcon } from './trash-icon' +export { TrendIcon } from './trend-icon' +export { UnhealthyIcon } from './unhealthy-icon' +export { UnpackIcon } from './unpack-icon' +export { UpgradeIcon } from './upgrade-icon' +export { UsageIcon } from './usage-icon' +export { WarningIcon } from './warning-icon' +export { WebhookIcon } from './webhook-icon' diff --git a/src/ui/primitives/icons/indicator-dots-icon.tsx b/src/ui/primitives/icons/indicator-dots-icon.tsx new file mode 100644 index 000000000..7ceb14653 --- /dev/null +++ b/src/ui/primitives/icons/indicator-dots-icon.tsx @@ -0,0 +1,22 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const IndicatorDotsIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/info-icon.tsx b/src/ui/primitives/icons/info-icon.tsx new file mode 100644 index 000000000..345f8d793 --- /dev/null +++ b/src/ui/primitives/icons/info-icon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const InfoIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/invoice-icon.tsx b/src/ui/primitives/icons/invoice-icon.tsx new file mode 100644 index 000000000..767b4312b --- /dev/null +++ b/src/ui/primitives/icons/invoice-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const InvoiceIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/key-icon.tsx b/src/ui/primitives/icons/key-icon.tsx new file mode 100644 index 000000000..aef7d0f34 --- /dev/null +++ b/src/ui/primitives/icons/key-icon.tsx @@ -0,0 +1,19 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const KeyIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/limit-ascii-icon.tsx b/src/ui/primitives/icons/limit-ascii-icon.tsx new file mode 100644 index 000000000..68d80621e --- /dev/null +++ b/src/ui/primitives/icons/limit-ascii-icon.tsx @@ -0,0 +1,55 @@ +import type React from 'react' +import { cn } from '@/lib/utils/index' +import type { IconProps } from '@/ui/primitives/icons/types' + +const LINES = [ + ' ', + ' ', + ' ', + ' ', + ' ---**--- ', + ' -**------**- ', + ' -**- - -**- ', + ' ** -**- ** ', + ' *- --- -* ', + ' ** ** ', + ' -**- -**- ', + ' -- -- ', + ' ', + ' ', + ' ', + ' ', +] + +const TEXT_STYLE: React.CSSProperties = { + fontSize: '3.8px', + fontFamily: 'var(--font-mono)', + fontWeight: 600, + letterSpacing: '-0.038px', + fontFeatureSettings: "'ss03' 1", +} + +export const LimitAsciiIcon = ({ className, ...props }: IconProps) => ( + + Limit Ascii + {LINES.map((line, i) => ( + + {line} + + ))} + +) diff --git a/src/ui/primitives/icons/list-icon.tsx b/src/ui/primitives/icons/list-icon.tsx new file mode 100644 index 000000000..fa9429a1d --- /dev/null +++ b/src/ui/primitives/icons/list-icon.tsx @@ -0,0 +1,23 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ListIcon = (props: IconProps) => ( + + + + + + +) diff --git a/src/ui/primitives/icons/logout-icon.tsx b/src/ui/primitives/icons/logout-icon.tsx new file mode 100644 index 000000000..39b6eff81 --- /dev/null +++ b/src/ui/primitives/icons/logout-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const LogoutIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/memory-icon.tsx b/src/ui/primitives/icons/memory-icon.tsx new file mode 100644 index 000000000..ddb0ec74d --- /dev/null +++ b/src/ui/primitives/icons/memory-icon.tsx @@ -0,0 +1,14 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const MemoryIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/menu-icon.tsx b/src/ui/primitives/icons/menu-icon.tsx new file mode 100644 index 000000000..bd77d967c --- /dev/null +++ b/src/ui/primitives/icons/menu-icon.tsx @@ -0,0 +1,9 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const MenuIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/metadata-icon.tsx b/src/ui/primitives/icons/metadata-icon.tsx new file mode 100644 index 000000000..8203757ed --- /dev/null +++ b/src/ui/primitives/icons/metadata-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const MetadataIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/new-tab-icon.tsx b/src/ui/primitives/icons/new-tab-icon.tsx new file mode 100644 index 000000000..727fddb79 --- /dev/null +++ b/src/ui/primitives/icons/new-tab-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const NewTabIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/paused-icon.tsx b/src/ui/primitives/icons/paused-icon.tsx new file mode 100644 index 000000000..d196691ff --- /dev/null +++ b/src/ui/primitives/icons/paused-icon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const PausedIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/person-icon.tsx b/src/ui/primitives/icons/person-icon.tsx new file mode 100644 index 000000000..c48d47cbd --- /dev/null +++ b/src/ui/primitives/icons/person-icon.tsx @@ -0,0 +1,11 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const PersonIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/persons-icon.tsx b/src/ui/primitives/icons/persons-icon.tsx new file mode 100644 index 000000000..177b7075c --- /dev/null +++ b/src/ui/primitives/icons/persons-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const PersonsIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/photo-icon.tsx b/src/ui/primitives/icons/photo-icon.tsx new file mode 100644 index 000000000..170d5b6af --- /dev/null +++ b/src/ui/primitives/icons/photo-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const PhotoIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/pie-chart-icon.tsx b/src/ui/primitives/icons/pie-chart-icon.tsx new file mode 100644 index 000000000..0852a0f1b --- /dev/null +++ b/src/ui/primitives/icons/pie-chart-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const PieChartIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/private-icon.tsx b/src/ui/primitives/icons/private-icon.tsx new file mode 100644 index 000000000..b6f0cf5a9 --- /dev/null +++ b/src/ui/primitives/icons/private-icon.tsx @@ -0,0 +1,11 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const PrivateIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/refresh-icon.tsx b/src/ui/primitives/icons/refresh-icon.tsx new file mode 100644 index 000000000..2e95e72ea --- /dev/null +++ b/src/ui/primitives/icons/refresh-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const RefreshIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/remove-icon.tsx b/src/ui/primitives/icons/remove-icon.tsx new file mode 100644 index 000000000..ff56a5724 --- /dev/null +++ b/src/ui/primitives/icons/remove-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const RemoveIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/running-icon.tsx b/src/ui/primitives/icons/running-icon.tsx new file mode 100644 index 000000000..f75e05f94 --- /dev/null +++ b/src/ui/primitives/icons/running-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const RunningIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/sandbox-icon.tsx b/src/ui/primitives/icons/sandbox-icon.tsx new file mode 100644 index 000000000..90fc9076b --- /dev/null +++ b/src/ui/primitives/icons/sandbox-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const SandboxIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/search-icon.tsx b/src/ui/primitives/icons/search-icon.tsx new file mode 100644 index 000000000..522233769 --- /dev/null +++ b/src/ui/primitives/icons/search-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const SearchIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/send-icon.tsx b/src/ui/primitives/icons/send-icon.tsx new file mode 100644 index 000000000..337588d18 --- /dev/null +++ b/src/ui/primitives/icons/send-icon.tsx @@ -0,0 +1,11 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const SendIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/settings-icon.tsx b/src/ui/primitives/icons/settings-icon.tsx new file mode 100644 index 000000000..37dd3ff8d --- /dev/null +++ b/src/ui/primitives/icons/settings-icon.tsx @@ -0,0 +1,17 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const SettingsIcon = (props: IconProps) => ( + + + + +) diff --git a/src/ui/primitives/icons/status-icon.tsx b/src/ui/primitives/icons/status-icon.tsx new file mode 100644 index 000000000..fef399aa3 --- /dev/null +++ b/src/ui/primitives/icons/status-icon.tsx @@ -0,0 +1,15 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const StatusIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/storage-icon.tsx b/src/ui/primitives/icons/storage-icon.tsx new file mode 100644 index 000000000..891f15ade --- /dev/null +++ b/src/ui/primitives/icons/storage-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const StorageIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/support-icon.tsx b/src/ui/primitives/icons/support-icon.tsx new file mode 100644 index 000000000..6a9d04d32 --- /dev/null +++ b/src/ui/primitives/icons/support-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const SupportIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/template-icon.tsx b/src/ui/primitives/icons/template-icon.tsx new file mode 100644 index 000000000..148bba2d2 --- /dev/null +++ b/src/ui/primitives/icons/template-icon.tsx @@ -0,0 +1,32 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const TemplateIcon = (props: IconProps) => ( + + + + + + +) diff --git a/src/ui/primitives/icons/thumbs-down-icon.tsx b/src/ui/primitives/icons/thumbs-down-icon.tsx new file mode 100644 index 000000000..47337f951 --- /dev/null +++ b/src/ui/primitives/icons/thumbs-down-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ThumbsDownIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/thumbs-up-icon.tsx b/src/ui/primitives/icons/thumbs-up-icon.tsx new file mode 100644 index 000000000..c25bf55a6 --- /dev/null +++ b/src/ui/primitives/icons/thumbs-up-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const ThumbsUpIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/time-icon.tsx b/src/ui/primitives/icons/time-icon.tsx new file mode 100644 index 000000000..4dd676a54 --- /dev/null +++ b/src/ui/primitives/icons/time-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const TimeIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/trash-icon.tsx b/src/ui/primitives/icons/trash-icon.tsx new file mode 100644 index 000000000..660e8d069 --- /dev/null +++ b/src/ui/primitives/icons/trash-icon.tsx @@ -0,0 +1,25 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const TrashIcon = (props: IconProps) => ( + + + + + +) diff --git a/src/ui/primitives/icons/trend-icon.tsx b/src/ui/primitives/icons/trend-icon.tsx new file mode 100644 index 000000000..eaab3f465 --- /dev/null +++ b/src/ui/primitives/icons/trend-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const TrendIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/types.ts b/src/ui/primitives/icons/types.ts new file mode 100644 index 000000000..37973e0d8 --- /dev/null +++ b/src/ui/primitives/icons/types.ts @@ -0,0 +1,8 @@ +import type React from 'react' + +export interface IconProps extends React.SVGProps { + className?: string + height?: number + width?: number + name?: string +} diff --git a/src/ui/primitives/icons/unhealthy-icon.tsx b/src/ui/primitives/icons/unhealthy-icon.tsx new file mode 100644 index 000000000..b187dd880 --- /dev/null +++ b/src/ui/primitives/icons/unhealthy-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const UnhealthyIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/unpack-icon.tsx b/src/ui/primitives/icons/unpack-icon.tsx new file mode 100644 index 000000000..83bbc7603 --- /dev/null +++ b/src/ui/primitives/icons/unpack-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const UnpackIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/upgrade-icon.tsx b/src/ui/primitives/icons/upgrade-icon.tsx new file mode 100644 index 000000000..d3acb5504 --- /dev/null +++ b/src/ui/primitives/icons/upgrade-icon.tsx @@ -0,0 +1,12 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const UpgradeIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/usage-icon.tsx b/src/ui/primitives/icons/usage-icon.tsx new file mode 100644 index 000000000..c4454fa86 --- /dev/null +++ b/src/ui/primitives/icons/usage-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const UsageIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/warning-icon.tsx b/src/ui/primitives/icons/warning-icon.tsx new file mode 100644 index 000000000..dac405429 --- /dev/null +++ b/src/ui/primitives/icons/warning-icon.tsx @@ -0,0 +1,13 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const WarningIcon = (props: IconProps) => ( + + + +) diff --git a/src/ui/primitives/icons/webhook-icon.tsx b/src/ui/primitives/icons/webhook-icon.tsx new file mode 100644 index 000000000..30e83fd3b --- /dev/null +++ b/src/ui/primitives/icons/webhook-icon.tsx @@ -0,0 +1,32 @@ +import { Icon } from '@/ui/primitives/icons/icon' +import type { IconProps } from '@/ui/primitives/icons/types' + +export const WebhookIcon = (props: IconProps) => ( + + + + + + + + + + + + +) From e25ca40617be4a3a791c10254035d60172dcae3f Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 13 Apr 2026 20:15:09 -0400 Subject: [PATCH 03/16] Refactor Alert and Limit ASCII icons to use div wrappers instead of SVGs, improving accessibility and layout. Adjusted font size and line height for better visual consistency. --- src/ui/primitives/icons/alert-ascii-icon.tsx | 40 ++++++++------------ src/ui/primitives/icons/limit-ascii-icon.tsx | 40 ++++++++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/src/ui/primitives/icons/alert-ascii-icon.tsx b/src/ui/primitives/icons/alert-ascii-icon.tsx index a43485d84..f9cf611f9 100644 --- a/src/ui/primitives/icons/alert-ascii-icon.tsx +++ b/src/ui/primitives/icons/alert-ascii-icon.tsx @@ -1,6 +1,5 @@ import type React from 'react' import { cn } from '@/lib/utils/index' -import type { IconProps } from '@/ui/primitives/icons/types' const LINES = [ ' ', @@ -22,34 +21,25 @@ const LINES = [ ] const TEXT_STYLE: React.CSSProperties = { - fontSize: '3.8px', + fontSize: '3.802px', fontFamily: 'var(--font-mono)', fontWeight: 600, letterSpacing: '-0.038px', + lineHeight: '4px', fontFeatureSettings: "'ss03' 1", } -export const AlertAsciiIcon = ({ className, ...props }: IconProps) => ( - - Alert Ascii - {LINES.map((line, i) => ( - - {line} - - ))} - +export const AlertAsciiIcon = ({ className }: { className?: string }) => ( +
+
+ {LINES.map((line, i) => ( +

+ {line} +

+ ))} +
+
) diff --git a/src/ui/primitives/icons/limit-ascii-icon.tsx b/src/ui/primitives/icons/limit-ascii-icon.tsx index 68d80621e..775e69374 100644 --- a/src/ui/primitives/icons/limit-ascii-icon.tsx +++ b/src/ui/primitives/icons/limit-ascii-icon.tsx @@ -1,6 +1,5 @@ import type React from 'react' import { cn } from '@/lib/utils/index' -import type { IconProps } from '@/ui/primitives/icons/types' const LINES = [ ' ', @@ -22,34 +21,25 @@ const LINES = [ ] const TEXT_STYLE: React.CSSProperties = { - fontSize: '3.8px', + fontSize: '3.802px', fontFamily: 'var(--font-mono)', fontWeight: 600, letterSpacing: '-0.038px', + lineHeight: '4px', fontFeatureSettings: "'ss03' 1", } -export const LimitAsciiIcon = ({ className, ...props }: IconProps) => ( - - Limit Ascii - {LINES.map((line, i) => ( - - {line} - - ))} - +export const LimitAsciiIcon = ({ className }: { className?: string }) => ( +
+
+ {LINES.map((line, i) => ( +

+ {line} +

+ ))} +
+
) From a2ec03e75edf7c72ff45d699f2c20f5a4f15da98 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 13 Apr 2026 20:18:49 -0400 Subject: [PATCH 04/16] Rollback icon refactor since out of scope of this branch --- .../dashboard/limits/limit-section.tsx | 3 +- .../{icons => }/alert-ascii-icon.tsx | 0 .../{icons/docs-icon.tsx => icons.tsx} | 1397 ++++++++++++++++- .../icons/account-settings-icon.tsx | 21 - src/ui/primitives/icons/add-icon.tsx | 20 - src/ui/primitives/icons/alert-icon.tsx | 17 - src/ui/primitives/icons/area-chart-icon.tsx | 13 - src/ui/primitives/icons/arrow-down-icon.tsx | 13 - src/ui/primitives/icons/block-icon.tsx | 13 - src/ui/primitives/icons/build-icon.tsx | 19 - src/ui/primitives/icons/card-icon.tsx | 13 - src/ui/primitives/icons/check-icon.tsx | 13 - src/ui/primitives/icons/chevron-down-icon.tsx | 12 - src/ui/primitives/icons/chevron-left-icon.tsx | 14 - .../primitives/icons/chevron-right-icon.tsx | 14 - src/ui/primitives/icons/chevron-up-icon.tsx | 12 - src/ui/primitives/icons/close-icon.tsx | 13 - src/ui/primitives/icons/code-chevron-icon.tsx | 11 - .../primitives/icons/collapse-left-icon.tsx | 25 - src/ui/primitives/icons/collapse-top-icon.tsx | 25 - src/ui/primitives/icons/copy-icon.tsx | 13 - src/ui/primitives/icons/cpu-icon.tsx | 13 - src/ui/primitives/icons/credits-icon.tsx | 13 - src/ui/primitives/icons/dot-icon.tsx | 8 - src/ui/primitives/icons/edit-icon.tsx | 13 - src/ui/primitives/icons/enterprise-icon.tsx | 14 - src/ui/primitives/icons/expand-down-icon.tsx | 25 - src/ui/primitives/icons/expand-right-icon.tsx | 25 - src/ui/primitives/icons/expiry-icon.tsx | 24 - src/ui/primitives/icons/explore-icon.tsx | 17 - .../primitives/icons/external-link-icon.tsx | 13 - src/ui/primitives/icons/filter-icon.tsx | 28 - src/ui/primitives/icons/gauge-icon.tsx | 13 - src/ui/primitives/icons/github-icon.tsx | 14 - src/ui/primitives/icons/history-icon.tsx | 25 - src/ui/primitives/icons/icon.tsx | 15 - src/ui/primitives/icons/index.ts | 73 - .../primitives/icons/indicator-dots-icon.tsx | 22 - src/ui/primitives/icons/info-icon.tsx | 17 - src/ui/primitives/icons/invoice-icon.tsx | 13 - src/ui/primitives/icons/key-icon.tsx | 19 - src/ui/primitives/icons/list-icon.tsx | 23 - src/ui/primitives/icons/logout-icon.tsx | 13 - src/ui/primitives/icons/memory-icon.tsx | 14 - src/ui/primitives/icons/menu-icon.tsx | 9 - src/ui/primitives/icons/metadata-icon.tsx | 13 - src/ui/primitives/icons/new-tab-icon.tsx | 13 - src/ui/primitives/icons/paused-icon.tsx | 17 - src/ui/primitives/icons/person-icon.tsx | 11 - src/ui/primitives/icons/persons-icon.tsx | 13 - src/ui/primitives/icons/photo-icon.tsx | 13 - src/ui/primitives/icons/pie-chart-icon.tsx | 12 - src/ui/primitives/icons/private-icon.tsx | 11 - src/ui/primitives/icons/refresh-icon.tsx | 13 - src/ui/primitives/icons/remove-icon.tsx | 13 - src/ui/primitives/icons/running-icon.tsx | 13 - src/ui/primitives/icons/sandbox-icon.tsx | 13 - src/ui/primitives/icons/search-icon.tsx | 13 - src/ui/primitives/icons/send-icon.tsx | 11 - src/ui/primitives/icons/settings-icon.tsx | 17 - src/ui/primitives/icons/status-icon.tsx | 15 - src/ui/primitives/icons/storage-icon.tsx | 13 - src/ui/primitives/icons/support-icon.tsx | 13 - src/ui/primitives/icons/template-icon.tsx | 32 - src/ui/primitives/icons/thumbs-down-icon.tsx | 12 - src/ui/primitives/icons/thumbs-up-icon.tsx | 12 - src/ui/primitives/icons/time-icon.tsx | 13 - src/ui/primitives/icons/trash-icon.tsx | 25 - src/ui/primitives/icons/trend-icon.tsx | 13 - src/ui/primitives/icons/types.ts | 8 - src/ui/primitives/icons/unhealthy-icon.tsx | 12 - src/ui/primitives/icons/unpack-icon.tsx | 13 - src/ui/primitives/icons/upgrade-icon.tsx | 12 - src/ui/primitives/icons/usage-icon.tsx | 13 - src/ui/primitives/icons/warning-icon.tsx | 13 - src/ui/primitives/icons/webhook-icon.tsx | 32 - .../{icons => }/limit-ascii-icon.tsx | 0 77 files changed, 1394 insertions(+), 1197 deletions(-) rename src/ui/primitives/{icons => }/alert-ascii-icon.tsx (100%) rename src/ui/primitives/{icons/docs-icon.tsx => icons.tsx} (64%) delete mode 100644 src/ui/primitives/icons/account-settings-icon.tsx delete mode 100644 src/ui/primitives/icons/add-icon.tsx delete mode 100644 src/ui/primitives/icons/alert-icon.tsx delete mode 100644 src/ui/primitives/icons/area-chart-icon.tsx delete mode 100644 src/ui/primitives/icons/arrow-down-icon.tsx delete mode 100644 src/ui/primitives/icons/block-icon.tsx delete mode 100644 src/ui/primitives/icons/build-icon.tsx delete mode 100644 src/ui/primitives/icons/card-icon.tsx delete mode 100644 src/ui/primitives/icons/check-icon.tsx delete mode 100644 src/ui/primitives/icons/chevron-down-icon.tsx delete mode 100644 src/ui/primitives/icons/chevron-left-icon.tsx delete mode 100644 src/ui/primitives/icons/chevron-right-icon.tsx delete mode 100644 src/ui/primitives/icons/chevron-up-icon.tsx delete mode 100644 src/ui/primitives/icons/close-icon.tsx delete mode 100644 src/ui/primitives/icons/code-chevron-icon.tsx delete mode 100644 src/ui/primitives/icons/collapse-left-icon.tsx delete mode 100644 src/ui/primitives/icons/collapse-top-icon.tsx delete mode 100644 src/ui/primitives/icons/copy-icon.tsx delete mode 100644 src/ui/primitives/icons/cpu-icon.tsx delete mode 100644 src/ui/primitives/icons/credits-icon.tsx delete mode 100644 src/ui/primitives/icons/dot-icon.tsx delete mode 100644 src/ui/primitives/icons/edit-icon.tsx delete mode 100644 src/ui/primitives/icons/enterprise-icon.tsx delete mode 100644 src/ui/primitives/icons/expand-down-icon.tsx delete mode 100644 src/ui/primitives/icons/expand-right-icon.tsx delete mode 100644 src/ui/primitives/icons/expiry-icon.tsx delete mode 100644 src/ui/primitives/icons/explore-icon.tsx delete mode 100644 src/ui/primitives/icons/external-link-icon.tsx delete mode 100644 src/ui/primitives/icons/filter-icon.tsx delete mode 100644 src/ui/primitives/icons/gauge-icon.tsx delete mode 100644 src/ui/primitives/icons/github-icon.tsx delete mode 100644 src/ui/primitives/icons/history-icon.tsx delete mode 100644 src/ui/primitives/icons/icon.tsx delete mode 100644 src/ui/primitives/icons/index.ts delete mode 100644 src/ui/primitives/icons/indicator-dots-icon.tsx delete mode 100644 src/ui/primitives/icons/info-icon.tsx delete mode 100644 src/ui/primitives/icons/invoice-icon.tsx delete mode 100644 src/ui/primitives/icons/key-icon.tsx delete mode 100644 src/ui/primitives/icons/list-icon.tsx delete mode 100644 src/ui/primitives/icons/logout-icon.tsx delete mode 100644 src/ui/primitives/icons/memory-icon.tsx delete mode 100644 src/ui/primitives/icons/menu-icon.tsx delete mode 100644 src/ui/primitives/icons/metadata-icon.tsx delete mode 100644 src/ui/primitives/icons/new-tab-icon.tsx delete mode 100644 src/ui/primitives/icons/paused-icon.tsx delete mode 100644 src/ui/primitives/icons/person-icon.tsx delete mode 100644 src/ui/primitives/icons/persons-icon.tsx delete mode 100644 src/ui/primitives/icons/photo-icon.tsx delete mode 100644 src/ui/primitives/icons/pie-chart-icon.tsx delete mode 100644 src/ui/primitives/icons/private-icon.tsx delete mode 100644 src/ui/primitives/icons/refresh-icon.tsx delete mode 100644 src/ui/primitives/icons/remove-icon.tsx delete mode 100644 src/ui/primitives/icons/running-icon.tsx delete mode 100644 src/ui/primitives/icons/sandbox-icon.tsx delete mode 100644 src/ui/primitives/icons/search-icon.tsx delete mode 100644 src/ui/primitives/icons/send-icon.tsx delete mode 100644 src/ui/primitives/icons/settings-icon.tsx delete mode 100644 src/ui/primitives/icons/status-icon.tsx delete mode 100644 src/ui/primitives/icons/storage-icon.tsx delete mode 100644 src/ui/primitives/icons/support-icon.tsx delete mode 100644 src/ui/primitives/icons/template-icon.tsx delete mode 100644 src/ui/primitives/icons/thumbs-down-icon.tsx delete mode 100644 src/ui/primitives/icons/thumbs-up-icon.tsx delete mode 100644 src/ui/primitives/icons/time-icon.tsx delete mode 100644 src/ui/primitives/icons/trash-icon.tsx delete mode 100644 src/ui/primitives/icons/trend-icon.tsx delete mode 100644 src/ui/primitives/icons/types.ts delete mode 100644 src/ui/primitives/icons/unhealthy-icon.tsx delete mode 100644 src/ui/primitives/icons/unpack-icon.tsx delete mode 100644 src/ui/primitives/icons/upgrade-icon.tsx delete mode 100644 src/ui/primitives/icons/usage-icon.tsx delete mode 100644 src/ui/primitives/icons/warning-icon.tsx delete mode 100644 src/ui/primitives/icons/webhook-icon.tsx rename src/ui/primitives/{icons => }/limit-ascii-icon.tsx (100%) diff --git a/src/features/dashboard/limits/limit-section.tsx b/src/features/dashboard/limits/limit-section.tsx index 63375fabe..0a4784e9e 100644 --- a/src/features/dashboard/limits/limit-section.tsx +++ b/src/features/dashboard/limits/limit-section.tsx @@ -2,7 +2,8 @@ import { Bell, TriangleAlert } from 'lucide-react' import { cn } from '@/lib/utils' -import { AlertAsciiIcon, LimitAsciiIcon } from '@/ui/primitives/icons' +import { AlertAsciiIcon } from '@/ui/primitives/alert-ascii-icon' +import { LimitAsciiIcon } from '@/ui/primitives/limit-ascii-icon' import LimitForm from './limit-form' type LimitType = 'limit' | 'alert' diff --git a/src/ui/primitives/icons/alert-ascii-icon.tsx b/src/ui/primitives/alert-ascii-icon.tsx similarity index 100% rename from src/ui/primitives/icons/alert-ascii-icon.tsx rename to src/ui/primitives/alert-ascii-icon.tsx diff --git a/src/ui/primitives/icons/docs-icon.tsx b/src/ui/primitives/icons.tsx similarity index 64% rename from src/ui/primitives/icons/docs-icon.tsx rename to src/ui/primitives/icons.tsx index a9442a7f7..9fb556d21 100644 --- a/src/ui/primitives/icons/docs-icon.tsx +++ b/src/ui/primitives/icons.tsx @@ -1,8 +1,203 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' +import type React from 'react' +import { cn } from '@/lib/utils/index' -export const DocsIcon = (props: IconProps) => ( - +const DEFAULT_CLASS_NAMES = 'size-6' + +interface IconProps extends React.SVGProps { + className?: string + height?: number + width?: number +} + +export const SandboxIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const TemplateIcon = ({ className, ...props }: IconProps) => ( + + + + + + +) + +export const UsageIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const PersonsIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const KeyIcon = ({ className, ...props }: IconProps) => ( + + + + +) +export const SettingsIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const CardIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const GaugeIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const CreditsIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const DocsIcon = ({ className, ...props }: IconProps) => ( + ( strokeMiterlimit="1.5" strokeWidth="1.66667" /> - + +) +export const GithubIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const AddIcon = ({ className, ...props }: IconProps) => ( + + + + + + + + + + +) + +export const AccountSettingsIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const LogoutIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const SearchIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const CopyIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const CheckIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const CollapseLeftIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const CollapseTopIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const ExpandDownIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const ExpandRightIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const UnpackIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ExternalLinkIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const FilterIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const IndicatorDotsIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const UnhealthyIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const TimeIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const CloseIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const NewTabIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const HistoryIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const StatusIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const RunningIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const CpuIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const MemoryIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const MetadataIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const RefreshIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ChevronRightIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ChevronLeftIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const TrashIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const EditIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ChevronDownIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ChevronUpIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const PieChartIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const AreaChartIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const SupportIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const BuildIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const RemoveIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const WarningIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const UpgradeIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const AlertIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const CodeChevronIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ExploreIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const PhotoIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const BlockIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ExpiryIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const PersonIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const PrivateIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const EnterpriseIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const TrendIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ListIcon = ({ className, ...props }: IconProps) => ( + + + + + + +) + +export const StorageIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const InfoIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const SendIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ThumbsUpIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ThumbsDownIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const MenuIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const PausedIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const DotIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const ArrowDownIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const InvoiceIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const WebhookIcon = ({ className, ...props }: IconProps) => ( + + + + + + + + + + + + ) diff --git a/src/ui/primitives/icons/account-settings-icon.tsx b/src/ui/primitives/icons/account-settings-icon.tsx deleted file mode 100644 index 966c71b95..000000000 --- a/src/ui/primitives/icons/account-settings-icon.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const AccountSettingsIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/add-icon.tsx b/src/ui/primitives/icons/add-icon.tsx deleted file mode 100644 index 132d6f1b6..000000000 --- a/src/ui/primitives/icons/add-icon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const AddIcon = (props: IconProps) => ( - - - - - - - - - - -) diff --git a/src/ui/primitives/icons/alert-icon.tsx b/src/ui/primitives/icons/alert-icon.tsx deleted file mode 100644 index 496f71660..000000000 --- a/src/ui/primitives/icons/alert-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const AlertIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/area-chart-icon.tsx b/src/ui/primitives/icons/area-chart-icon.tsx deleted file mode 100644 index 264d78af9..000000000 --- a/src/ui/primitives/icons/area-chart-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const AreaChartIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/arrow-down-icon.tsx b/src/ui/primitives/icons/arrow-down-icon.tsx deleted file mode 100644 index 39c95cab4..000000000 --- a/src/ui/primitives/icons/arrow-down-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ArrowDownIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/block-icon.tsx b/src/ui/primitives/icons/block-icon.tsx deleted file mode 100644 index 881e70d52..000000000 --- a/src/ui/primitives/icons/block-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const BlockIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/build-icon.tsx b/src/ui/primitives/icons/build-icon.tsx deleted file mode 100644 index 24cfe63b6..000000000 --- a/src/ui/primitives/icons/build-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const BuildIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/card-icon.tsx b/src/ui/primitives/icons/card-icon.tsx deleted file mode 100644 index 3db134816..000000000 --- a/src/ui/primitives/icons/card-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CardIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/check-icon.tsx b/src/ui/primitives/icons/check-icon.tsx deleted file mode 100644 index 81c55e422..000000000 --- a/src/ui/primitives/icons/check-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CheckIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/chevron-down-icon.tsx b/src/ui/primitives/icons/chevron-down-icon.tsx deleted file mode 100644 index f835887f1..000000000 --- a/src/ui/primitives/icons/chevron-down-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ChevronDownIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/chevron-left-icon.tsx b/src/ui/primitives/icons/chevron-left-icon.tsx deleted file mode 100644 index 9e39c16f7..000000000 --- a/src/ui/primitives/icons/chevron-left-icon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ChevronLeftIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/chevron-right-icon.tsx b/src/ui/primitives/icons/chevron-right-icon.tsx deleted file mode 100644 index d16b272d9..000000000 --- a/src/ui/primitives/icons/chevron-right-icon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ChevronRightIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/chevron-up-icon.tsx b/src/ui/primitives/icons/chevron-up-icon.tsx deleted file mode 100644 index ff21e36c9..000000000 --- a/src/ui/primitives/icons/chevron-up-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ChevronUpIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/close-icon.tsx b/src/ui/primitives/icons/close-icon.tsx deleted file mode 100644 index e2ddbe198..000000000 --- a/src/ui/primitives/icons/close-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CloseIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/code-chevron-icon.tsx b/src/ui/primitives/icons/code-chevron-icon.tsx deleted file mode 100644 index 0f602593c..000000000 --- a/src/ui/primitives/icons/code-chevron-icon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CodeChevronIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/collapse-left-icon.tsx b/src/ui/primitives/icons/collapse-left-icon.tsx deleted file mode 100644 index 69dc36742..000000000 --- a/src/ui/primitives/icons/collapse-left-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CollapseLeftIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/collapse-top-icon.tsx b/src/ui/primitives/icons/collapse-top-icon.tsx deleted file mode 100644 index 13d4eadd5..000000000 --- a/src/ui/primitives/icons/collapse-top-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CollapseTopIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/copy-icon.tsx b/src/ui/primitives/icons/copy-icon.tsx deleted file mode 100644 index 5668df3cf..000000000 --- a/src/ui/primitives/icons/copy-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CopyIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/cpu-icon.tsx b/src/ui/primitives/icons/cpu-icon.tsx deleted file mode 100644 index eeb3ff3fb..000000000 --- a/src/ui/primitives/icons/cpu-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CpuIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/credits-icon.tsx b/src/ui/primitives/icons/credits-icon.tsx deleted file mode 100644 index 94e77e5ba..000000000 --- a/src/ui/primitives/icons/credits-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const CreditsIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/dot-icon.tsx b/src/ui/primitives/icons/dot-icon.tsx deleted file mode 100644 index 7f53b7d94..000000000 --- a/src/ui/primitives/icons/dot-icon.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const DotIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/edit-icon.tsx b/src/ui/primitives/icons/edit-icon.tsx deleted file mode 100644 index f6e495590..000000000 --- a/src/ui/primitives/icons/edit-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const EditIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/enterprise-icon.tsx b/src/ui/primitives/icons/enterprise-icon.tsx deleted file mode 100644 index ba6cecd8b..000000000 --- a/src/ui/primitives/icons/enterprise-icon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const EnterpriseIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/expand-down-icon.tsx b/src/ui/primitives/icons/expand-down-icon.tsx deleted file mode 100644 index 0bc77a8df..000000000 --- a/src/ui/primitives/icons/expand-down-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ExpandDownIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/expand-right-icon.tsx b/src/ui/primitives/icons/expand-right-icon.tsx deleted file mode 100644 index e10ad3ccc..000000000 --- a/src/ui/primitives/icons/expand-right-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ExpandRightIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/expiry-icon.tsx b/src/ui/primitives/icons/expiry-icon.tsx deleted file mode 100644 index 17e80ca52..000000000 --- a/src/ui/primitives/icons/expiry-icon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ExpiryIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/explore-icon.tsx b/src/ui/primitives/icons/explore-icon.tsx deleted file mode 100644 index ef91d5c54..000000000 --- a/src/ui/primitives/icons/explore-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ExploreIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/external-link-icon.tsx b/src/ui/primitives/icons/external-link-icon.tsx deleted file mode 100644 index d10ea7954..000000000 --- a/src/ui/primitives/icons/external-link-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ExternalLinkIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/filter-icon.tsx b/src/ui/primitives/icons/filter-icon.tsx deleted file mode 100644 index 1a690ef82..000000000 --- a/src/ui/primitives/icons/filter-icon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const FilterIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/gauge-icon.tsx b/src/ui/primitives/icons/gauge-icon.tsx deleted file mode 100644 index 1717d3c00..000000000 --- a/src/ui/primitives/icons/gauge-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const GaugeIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/github-icon.tsx b/src/ui/primitives/icons/github-icon.tsx deleted file mode 100644 index dc783e26c..000000000 --- a/src/ui/primitives/icons/github-icon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const GithubIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/history-icon.tsx b/src/ui/primitives/icons/history-icon.tsx deleted file mode 100644 index 571ee3beb..000000000 --- a/src/ui/primitives/icons/history-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const HistoryIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/icon.tsx b/src/ui/primitives/icons/icon.tsx deleted file mode 100644 index 5e740bc2a..000000000 --- a/src/ui/primitives/icons/icon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { cn } from '@/lib/utils/index' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const Icon = ({ className, children, name, ...props }: IconProps) => ( - - {name} - {children} - -) diff --git a/src/ui/primitives/icons/index.ts b/src/ui/primitives/icons/index.ts deleted file mode 100644 index d34728482..000000000 --- a/src/ui/primitives/icons/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -export { AccountSettingsIcon } from './account-settings-icon' -export { AddIcon } from './add-icon' -export { AlertAsciiIcon } from './alert-ascii-icon' -export { AlertIcon } from './alert-icon' -export { AreaChartIcon } from './area-chart-icon' -export { ArrowDownIcon } from './arrow-down-icon' -export { BlockIcon } from './block-icon' -export { BuildIcon } from './build-icon' -export { CardIcon } from './card-icon' -export { CheckIcon } from './check-icon' -export { ChevronDownIcon } from './chevron-down-icon' -export { ChevronLeftIcon } from './chevron-left-icon' -export { ChevronRightIcon } from './chevron-right-icon' -export { ChevronUpIcon } from './chevron-up-icon' -export { CloseIcon } from './close-icon' -export { CodeChevronIcon } from './code-chevron-icon' -export { CollapseLeftIcon } from './collapse-left-icon' -export { CollapseTopIcon } from './collapse-top-icon' -export { CopyIcon } from './copy-icon' -export { CpuIcon } from './cpu-icon' -export { CreditsIcon } from './credits-icon' -export { DocsIcon } from './docs-icon' -export { DotIcon } from './dot-icon' -export { EditIcon } from './edit-icon' -export { EnterpriseIcon } from './enterprise-icon' -export { ExpandDownIcon } from './expand-down-icon' -export { ExpandRightIcon } from './expand-right-icon' -export { ExpiryIcon } from './expiry-icon' -export { ExploreIcon } from './explore-icon' -export { ExternalLinkIcon } from './external-link-icon' -export { FilterIcon } from './filter-icon' -export { GaugeIcon } from './gauge-icon' -export { GithubIcon } from './github-icon' -export { HistoryIcon } from './history-icon' -export { IndicatorDotsIcon } from './indicator-dots-icon' -export { InfoIcon } from './info-icon' -export { InvoiceIcon } from './invoice-icon' -export { KeyIcon } from './key-icon' -export { LimitAsciiIcon } from './limit-ascii-icon' -export { ListIcon } from './list-icon' -export { LogoutIcon } from './logout-icon' -export { MemoryIcon } from './memory-icon' -export { MenuIcon } from './menu-icon' -export { MetadataIcon } from './metadata-icon' -export { NewTabIcon } from './new-tab-icon' -export { PausedIcon } from './paused-icon' -export { PersonIcon } from './person-icon' -export { PersonsIcon } from './persons-icon' -export { PhotoIcon } from './photo-icon' -export { PieChartIcon } from './pie-chart-icon' -export { PrivateIcon } from './private-icon' -export { RefreshIcon } from './refresh-icon' -export { RemoveIcon } from './remove-icon' -export { RunningIcon } from './running-icon' -export { SandboxIcon } from './sandbox-icon' -export { SearchIcon } from './search-icon' -export { SendIcon } from './send-icon' -export { SettingsIcon } from './settings-icon' -export { StatusIcon } from './status-icon' -export { StorageIcon } from './storage-icon' -export { SupportIcon } from './support-icon' -export { TemplateIcon } from './template-icon' -export { ThumbsDownIcon } from './thumbs-down-icon' -export { ThumbsUpIcon } from './thumbs-up-icon' -export { TimeIcon } from './time-icon' -export { TrashIcon } from './trash-icon' -export { TrendIcon } from './trend-icon' -export { UnhealthyIcon } from './unhealthy-icon' -export { UnpackIcon } from './unpack-icon' -export { UpgradeIcon } from './upgrade-icon' -export { UsageIcon } from './usage-icon' -export { WarningIcon } from './warning-icon' -export { WebhookIcon } from './webhook-icon' diff --git a/src/ui/primitives/icons/indicator-dots-icon.tsx b/src/ui/primitives/icons/indicator-dots-icon.tsx deleted file mode 100644 index 7ceb14653..000000000 --- a/src/ui/primitives/icons/indicator-dots-icon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const IndicatorDotsIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/info-icon.tsx b/src/ui/primitives/icons/info-icon.tsx deleted file mode 100644 index 345f8d793..000000000 --- a/src/ui/primitives/icons/info-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const InfoIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/invoice-icon.tsx b/src/ui/primitives/icons/invoice-icon.tsx deleted file mode 100644 index 767b4312b..000000000 --- a/src/ui/primitives/icons/invoice-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const InvoiceIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/key-icon.tsx b/src/ui/primitives/icons/key-icon.tsx deleted file mode 100644 index aef7d0f34..000000000 --- a/src/ui/primitives/icons/key-icon.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const KeyIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/list-icon.tsx b/src/ui/primitives/icons/list-icon.tsx deleted file mode 100644 index fa9429a1d..000000000 --- a/src/ui/primitives/icons/list-icon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ListIcon = (props: IconProps) => ( - - - - - - -) diff --git a/src/ui/primitives/icons/logout-icon.tsx b/src/ui/primitives/icons/logout-icon.tsx deleted file mode 100644 index 39b6eff81..000000000 --- a/src/ui/primitives/icons/logout-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const LogoutIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/memory-icon.tsx b/src/ui/primitives/icons/memory-icon.tsx deleted file mode 100644 index ddb0ec74d..000000000 --- a/src/ui/primitives/icons/memory-icon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const MemoryIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/menu-icon.tsx b/src/ui/primitives/icons/menu-icon.tsx deleted file mode 100644 index bd77d967c..000000000 --- a/src/ui/primitives/icons/menu-icon.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const MenuIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/metadata-icon.tsx b/src/ui/primitives/icons/metadata-icon.tsx deleted file mode 100644 index 8203757ed..000000000 --- a/src/ui/primitives/icons/metadata-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const MetadataIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/new-tab-icon.tsx b/src/ui/primitives/icons/new-tab-icon.tsx deleted file mode 100644 index 727fddb79..000000000 --- a/src/ui/primitives/icons/new-tab-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const NewTabIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/paused-icon.tsx b/src/ui/primitives/icons/paused-icon.tsx deleted file mode 100644 index d196691ff..000000000 --- a/src/ui/primitives/icons/paused-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const PausedIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/person-icon.tsx b/src/ui/primitives/icons/person-icon.tsx deleted file mode 100644 index c48d47cbd..000000000 --- a/src/ui/primitives/icons/person-icon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const PersonIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/persons-icon.tsx b/src/ui/primitives/icons/persons-icon.tsx deleted file mode 100644 index 177b7075c..000000000 --- a/src/ui/primitives/icons/persons-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const PersonsIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/photo-icon.tsx b/src/ui/primitives/icons/photo-icon.tsx deleted file mode 100644 index 170d5b6af..000000000 --- a/src/ui/primitives/icons/photo-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const PhotoIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/pie-chart-icon.tsx b/src/ui/primitives/icons/pie-chart-icon.tsx deleted file mode 100644 index 0852a0f1b..000000000 --- a/src/ui/primitives/icons/pie-chart-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const PieChartIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/private-icon.tsx b/src/ui/primitives/icons/private-icon.tsx deleted file mode 100644 index b6f0cf5a9..000000000 --- a/src/ui/primitives/icons/private-icon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const PrivateIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/refresh-icon.tsx b/src/ui/primitives/icons/refresh-icon.tsx deleted file mode 100644 index 2e95e72ea..000000000 --- a/src/ui/primitives/icons/refresh-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const RefreshIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/remove-icon.tsx b/src/ui/primitives/icons/remove-icon.tsx deleted file mode 100644 index ff56a5724..000000000 --- a/src/ui/primitives/icons/remove-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const RemoveIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/running-icon.tsx b/src/ui/primitives/icons/running-icon.tsx deleted file mode 100644 index f75e05f94..000000000 --- a/src/ui/primitives/icons/running-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const RunningIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/sandbox-icon.tsx b/src/ui/primitives/icons/sandbox-icon.tsx deleted file mode 100644 index 90fc9076b..000000000 --- a/src/ui/primitives/icons/sandbox-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const SandboxIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/search-icon.tsx b/src/ui/primitives/icons/search-icon.tsx deleted file mode 100644 index 522233769..000000000 --- a/src/ui/primitives/icons/search-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const SearchIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/send-icon.tsx b/src/ui/primitives/icons/send-icon.tsx deleted file mode 100644 index 337588d18..000000000 --- a/src/ui/primitives/icons/send-icon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const SendIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/settings-icon.tsx b/src/ui/primitives/icons/settings-icon.tsx deleted file mode 100644 index 37dd3ff8d..000000000 --- a/src/ui/primitives/icons/settings-icon.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const SettingsIcon = (props: IconProps) => ( - - - - -) diff --git a/src/ui/primitives/icons/status-icon.tsx b/src/ui/primitives/icons/status-icon.tsx deleted file mode 100644 index fef399aa3..000000000 --- a/src/ui/primitives/icons/status-icon.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const StatusIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/storage-icon.tsx b/src/ui/primitives/icons/storage-icon.tsx deleted file mode 100644 index 891f15ade..000000000 --- a/src/ui/primitives/icons/storage-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const StorageIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/support-icon.tsx b/src/ui/primitives/icons/support-icon.tsx deleted file mode 100644 index 6a9d04d32..000000000 --- a/src/ui/primitives/icons/support-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const SupportIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/template-icon.tsx b/src/ui/primitives/icons/template-icon.tsx deleted file mode 100644 index 148bba2d2..000000000 --- a/src/ui/primitives/icons/template-icon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const TemplateIcon = (props: IconProps) => ( - - - - - - -) diff --git a/src/ui/primitives/icons/thumbs-down-icon.tsx b/src/ui/primitives/icons/thumbs-down-icon.tsx deleted file mode 100644 index 47337f951..000000000 --- a/src/ui/primitives/icons/thumbs-down-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ThumbsDownIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/thumbs-up-icon.tsx b/src/ui/primitives/icons/thumbs-up-icon.tsx deleted file mode 100644 index c25bf55a6..000000000 --- a/src/ui/primitives/icons/thumbs-up-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const ThumbsUpIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/time-icon.tsx b/src/ui/primitives/icons/time-icon.tsx deleted file mode 100644 index 4dd676a54..000000000 --- a/src/ui/primitives/icons/time-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const TimeIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/trash-icon.tsx b/src/ui/primitives/icons/trash-icon.tsx deleted file mode 100644 index 660e8d069..000000000 --- a/src/ui/primitives/icons/trash-icon.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const TrashIcon = (props: IconProps) => ( - - - - - -) diff --git a/src/ui/primitives/icons/trend-icon.tsx b/src/ui/primitives/icons/trend-icon.tsx deleted file mode 100644 index eaab3f465..000000000 --- a/src/ui/primitives/icons/trend-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const TrendIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/types.ts b/src/ui/primitives/icons/types.ts deleted file mode 100644 index 37973e0d8..000000000 --- a/src/ui/primitives/icons/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type React from 'react' - -export interface IconProps extends React.SVGProps { - className?: string - height?: number - width?: number - name?: string -} diff --git a/src/ui/primitives/icons/unhealthy-icon.tsx b/src/ui/primitives/icons/unhealthy-icon.tsx deleted file mode 100644 index b187dd880..000000000 --- a/src/ui/primitives/icons/unhealthy-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const UnhealthyIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/unpack-icon.tsx b/src/ui/primitives/icons/unpack-icon.tsx deleted file mode 100644 index 83bbc7603..000000000 --- a/src/ui/primitives/icons/unpack-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const UnpackIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/upgrade-icon.tsx b/src/ui/primitives/icons/upgrade-icon.tsx deleted file mode 100644 index d3acb5504..000000000 --- a/src/ui/primitives/icons/upgrade-icon.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const UpgradeIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/usage-icon.tsx b/src/ui/primitives/icons/usage-icon.tsx deleted file mode 100644 index c4454fa86..000000000 --- a/src/ui/primitives/icons/usage-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const UsageIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/warning-icon.tsx b/src/ui/primitives/icons/warning-icon.tsx deleted file mode 100644 index dac405429..000000000 --- a/src/ui/primitives/icons/warning-icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const WarningIcon = (props: IconProps) => ( - - - -) diff --git a/src/ui/primitives/icons/webhook-icon.tsx b/src/ui/primitives/icons/webhook-icon.tsx deleted file mode 100644 index 30e83fd3b..000000000 --- a/src/ui/primitives/icons/webhook-icon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Icon } from '@/ui/primitives/icons/icon' -import type { IconProps } from '@/ui/primitives/icons/types' - -export const WebhookIcon = (props: IconProps) => ( - - - - - - - - - - - - -) diff --git a/src/ui/primitives/icons/limit-ascii-icon.tsx b/src/ui/primitives/limit-ascii-icon.tsx similarity index 100% rename from src/ui/primitives/icons/limit-ascii-icon.tsx rename to src/ui/primitives/limit-ascii-icon.tsx From adee0370855c5bc9d8a3d0f08957793644386b36 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Mon, 13 Apr 2026 20:20:13 -0400 Subject: [PATCH 05/16] Add ASCII icon components for alert and limit with styled text representation --- .../dashboard/limits}/alert-ascii-icon.tsx | 0 .../dashboard/limits}/limit-ascii-icon.tsx | 0 src/features/dashboard/limits/limit-section.tsx | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{ui/primitives => features/dashboard/limits}/alert-ascii-icon.tsx (100%) rename src/{ui/primitives => features/dashboard/limits}/limit-ascii-icon.tsx (100%) diff --git a/src/ui/primitives/alert-ascii-icon.tsx b/src/features/dashboard/limits/alert-ascii-icon.tsx similarity index 100% rename from src/ui/primitives/alert-ascii-icon.tsx rename to src/features/dashboard/limits/alert-ascii-icon.tsx diff --git a/src/ui/primitives/limit-ascii-icon.tsx b/src/features/dashboard/limits/limit-ascii-icon.tsx similarity index 100% rename from src/ui/primitives/limit-ascii-icon.tsx rename to src/features/dashboard/limits/limit-ascii-icon.tsx diff --git a/src/features/dashboard/limits/limit-section.tsx b/src/features/dashboard/limits/limit-section.tsx index 0a4784e9e..3081a2639 100644 --- a/src/features/dashboard/limits/limit-section.tsx +++ b/src/features/dashboard/limits/limit-section.tsx @@ -2,8 +2,8 @@ import { Bell, TriangleAlert } from 'lucide-react' import { cn } from '@/lib/utils' -import { AlertAsciiIcon } from '@/ui/primitives/alert-ascii-icon' -import { LimitAsciiIcon } from '@/ui/primitives/limit-ascii-icon' +import { AlertAsciiIcon } from './alert-ascii-icon' +import { LimitAsciiIcon } from './limit-ascii-icon' import LimitForm from './limit-form' type LimitType = 'limit' | 'alert' From 80f766deb6ec6f0c204a7964d72bc90a24683b68 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 12:45:43 -0400 Subject: [PATCH 06/16] Refactor billing limits management by removing the old LimitForm component and replacing it with UsageLimitForm and UsageAlertForm components. Introduce a new RemoveUsageLimitDialog for handling limit removals. Update the limit-section to utilize the new forms and adjust the UsageLimits component accordingly. Enhance currency formatting utilities for better input handling. --- src/features/dashboard/limits/limit-form.tsx | 407 ------------------ .../dashboard/limits/limit-section.tsx | 93 ++-- .../limits/remove-usage-limit-dialog.tsx | 118 +++++ .../dashboard/limits/usage-alert-form.tsx | 226 ++++++++++ .../dashboard/limits/usage-limit-form.tsx | 296 +++++++++++++ .../dashboard/limits/usage-limits.tsx | 10 +- src/lib/utils/currency.ts | 9 + 7 files changed, 708 insertions(+), 451 deletions(-) delete mode 100644 src/features/dashboard/limits/limit-form.tsx create mode 100644 src/features/dashboard/limits/remove-usage-limit-dialog.tsx create mode 100644 src/features/dashboard/limits/usage-alert-form.tsx create mode 100644 src/features/dashboard/limits/usage-limit-form.tsx create mode 100644 src/lib/utils/currency.ts diff --git a/src/features/dashboard/limits/limit-form.tsx b/src/features/dashboard/limits/limit-form.tsx deleted file mode 100644 index 447747026..000000000 --- a/src/features/dashboard/limits/limit-form.tsx +++ /dev/null @@ -1,407 +0,0 @@ -'use client' - -import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Pencil, Trash2, TriangleAlert } from 'lucide-react' -import { type FormEvent, type ReactNode, useEffect, useState } from 'react' -import { z } from 'zod' -import type { BillingLimit } from '@/core/modules/billing/models' -import { - defaultErrorToast, - defaultSuccessToast, - useToast, -} from '@/lib/hooks/use-toast' -import { cn } from '@/lib/utils' -import { useTRPC } from '@/trpc/client' -import { Button } from '@/ui/primitives/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogTitle, -} from '@/ui/primitives/dialog' -import { Input } from '@/ui/primitives/input' - -interface LimitFormProps { - className?: string - originalValue: number | null - teamSlug: string - type: 'limit' | 'alert' -} - -interface LimitConfirmationDialogProps { - actions: ReactNode - children: ReactNode - icon: ReactNode - open: boolean - onOpenChange: (open: boolean) => void - title: string -} - -const limitCopy = { - alert: { - clearError: 'Failed to remove billing alert.', - clearSuccess: 'Billing alert removed.', - emptyError: 'Enter a billing alert amount.', - saveError: 'Failed to save billing alert.', - saveSuccess: 'Billing alert saved.', - }, - limit: { - clearError: 'Failed to remove billing limit.', - clearSuccess: 'Billing limit removed.', - emptyError: 'Enter a billing limit amount.', - saveError: 'Failed to save billing limit.', - saveSuccess: 'Billing limit saved.', - }, -} satisfies Record< - LimitFormProps['type'], - { - clearError: string - clearSuccess: string - emptyError: string - saveError: string - saveSuccess: string - } -> - -const currencyFormatter = new Intl.NumberFormat('en-US') - -const limitValueSchema = z - .string() - .trim() - .min(1, 'Enter a value.') - .regex(/^\d+$/, 'Enter a whole USD amount.') - .transform(Number) - .refine((value) => value >= 1, 'Value must be at least 1.') - -// Removes non-digits from a currency draft. Example: "$1,250" -> "1250". -const sanitizeCurrencyInput = (value: string) => value.replace(/\D+/g, '') - -// Formats a numeric billing limit for display. Example: 1250 -> "1,250". -const formatCurrencyValue = (value: number) => currencyFormatter.format(value) - -// Updates the matching limit field in cached billing data. Example: ("alert", 50) -> { alert_amount_gte: 50 }. -const updateLimitValue = ( - limits: BillingLimit | undefined, - type: LimitFormProps['type'], - nextValue: number | null -) => { - if (!limits) return limits - if (type === 'limit') return { ...limits, limit_amount_gte: nextValue } - return { ...limits, alert_amount_gte: nextValue } -} - -const LimitConfirmationDialog = ({ - actions, - children, - icon, - open, - onOpenChange, - title, -}: LimitConfirmationDialogProps) => ( - - -
-
-
-
{icon}
- - {title} - -
- - {children} - -
-
- {actions} -
-
-
-
-) - -export default function LimitForm({ - className, - originalValue, - teamSlug, - type, -}: LimitFormProps) { - const [draftValue, setDraftValue] = useState( - originalValue === null ? '' : formatCurrencyValue(originalValue) - ) - const [isEditing, setIsEditing] = useState(originalValue === null) - const [isRemoveDialogOpen, setIsRemoveDialogOpen] = useState(false) - const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false) - const { toast } = useToast() - const trpc = useTRPC() - const queryClient = useQueryClient() - const copy = limitCopy[type] - - const limitsQueryKey = trpc.billing.getLimits.queryOptions({ - teamSlug, - }).queryKey - - useEffect(() => { - setDraftValue( - originalValue === null ? '' : formatCurrencyValue(originalValue) - ) - setIsEditing(originalValue === null) - }, [originalValue]) - - const setLimitMutation = useMutation( - trpc.billing.setLimit.mutationOptions({ - onSuccess: (_, variables) => { - queryClient.setQueryData( - limitsQueryKey, - (limits) => updateLimitValue(limits, type, variables.value) - ) - toast(defaultSuccessToast(copy.saveSuccess)) - setDraftValue(formatCurrencyValue(variables.value)) - setIsEditing(false) - setIsSaveDialogOpen(false) - queryClient.invalidateQueries({ queryKey: limitsQueryKey }) - }, - onError: (error) => { - toast(defaultErrorToast(error.message || copy.saveError)) - }, - }) - ) - - const clearLimitMutation = useMutation( - trpc.billing.clearLimit.mutationOptions({ - onSuccess: () => { - queryClient.setQueryData( - limitsQueryKey, - (limits) => updateLimitValue(limits, type, null) - ) - toast(defaultSuccessToast(copy.clearSuccess)) - setDraftValue('') - setIsEditing(true) - setIsRemoveDialogOpen(false) - queryClient.invalidateQueries({ queryKey: limitsQueryKey }) - }, - onError: (error) => { - toast(defaultErrorToast(error.message || copy.clearError)) - }, - }) - ) - - const parsedValue = limitValueSchema.safeParse(draftValue) - const nextValue = parsedValue.success ? parsedValue.data : null - const isMutating = setLimitMutation.isPending || clearLimitMutation.isPending - const canSave = - parsedValue.success && nextValue !== originalValue && !isMutating - - const handleCancel = () => { - if (originalValue === null) return - - setDraftValue(formatCurrencyValue(originalValue)) - setIsEditing(false) - } - - const handleSubmit = (event: FormEvent) => { - event.preventDefault() - - if (!parsedValue.success) { - toast( - defaultErrorToast( - parsedValue.error.issues[0]?.message || copy.emptyError - ) - ) - return - } - - if (parsedValue.data === originalValue) return - if (type === 'limit') return setIsSaveDialogOpen(true) - - setLimitMutation.mutate({ teamSlug, type, value: parsedValue.data }) - } - - const handleRemove = () => { - if (type === 'limit') return setIsRemoveDialogOpen(true) - - clearLimitMutation.mutate({ teamSlug, type }) - } - - if (originalValue !== null && !isEditing) - return ( - <> -
-
- $ - - {formatCurrencyValue(originalValue)} - -
-
- - -
-
- {type === 'limit' && ( - - - - - } - icon={} - open={isRemoveDialogOpen} - onOpenChange={setIsRemoveDialogOpen} - title="Remove usage limit?" - > - API limits will be removed and usage will become uncapped. - - )} - - ) - - return ( - <> -
-
- $ - - setDraftValue(sanitizeCurrencyInput(event.target.value)) - } - placeholder="--" - value={draftValue} - /> -
-
- {originalValue !== null && ( - - )} - -
-
- {type === 'limit' && ( - - - - - } - icon={ - - } - open={isSaveDialogOpen} - onOpenChange={setIsSaveDialogOpen} - title={`Set $${nextValue === null ? '--' : formatCurrencyValue(nextValue)} usage limit?`} - > - If your API usage hits this limit, all requests including sandbox - creation will be blocked. This may interrupt your services until you - raise or remove the limit. - - )} - - ) -} diff --git a/src/features/dashboard/limits/limit-section.tsx b/src/features/dashboard/limits/limit-section.tsx index 3081a2639..59aa866b1 100644 --- a/src/features/dashboard/limits/limit-section.tsx +++ b/src/features/dashboard/limits/limit-section.tsx @@ -4,53 +4,38 @@ import { Bell, TriangleAlert } from 'lucide-react' import { cn } from '@/lib/utils' import { AlertAsciiIcon } from './alert-ascii-icon' import { LimitAsciiIcon } from './limit-ascii-icon' -import LimitForm from './limit-form' +import { UsageAlertForm } from './usage-alert-form' +import { UsageLimitForm } from './usage-limit-form' -type LimitType = 'limit' | 'alert' +interface UsageLimitSectionProps { + className?: string + email: string + teamSlug: string + value: number | null +} -interface LimitSectionProps { +interface UsageAlertSectionProps { className?: string email: string teamSlug: string - title: string - type: LimitType value: number | null } const currencyFormatter = new Intl.NumberFormat('en-US') -const iconMap: Record = { - limit: LimitAsciiIcon, - alert: AlertAsciiIcon, -} - -const LimitPanelIcon = ({ type }: { type: LimitType }) => { - const Icon = iconMap[type] +const LimitPanelIcon = ({ icon }: { icon: typeof LimitAsciiIcon }) => { + const Icon = icon return ( -
+
) } -const LimitSectionInfo = ({ +const UsageLimitSectionInfo = ({ email, - type, value, -}: Pick) => { - if (type === 'alert') { - const alertMessage = - value === null - ? `Informative alert will be sent to ${email} when this threshold is reached` - : `Informative alert will be sent to ${email} when the $${currencyFormatter.format(value)} threshold is reached` - - return ( -

- {alertMessage} -

- ) - } - +}: Pick) => { const limitMessage = value === null ? 'All API requests are blocked after reaching this limit' @@ -70,26 +55,60 @@ const LimitSectionInfo = ({ ) } -export const LimitSection = ({ +const UsageAlertSectionInfo = ({ + email, + value, +}: Pick) => { + const alertMessage = + value === null + ? `Informative alert will be sent to ${email} when this threshold is reached` + : `Informative alert will be sent to ${email} when the $${currencyFormatter.format(value)} threshold is reached` + + return ( +

{alertMessage}

+ ) +} + +export const UsageLimitSection = ({ + className, + email, + teamSlug, + value, +}: UsageLimitSectionProps) => { + return ( +
+

+ Usage Limit +

+
+ +
+ +
+
+ +
+ ) +} + +export const UsageAlertSection = ({ className, email, teamSlug, - title, - type, value, -}: LimitSectionProps) => { +}: UsageAlertSectionProps) => { return (

- {title} + Usage Alert

- +
- +
- +
) } diff --git a/src/features/dashboard/limits/remove-usage-limit-dialog.tsx b/src/features/dashboard/limits/remove-usage-limit-dialog.tsx new file mode 100644 index 000000000..dddcbb7bf --- /dev/null +++ b/src/features/dashboard/limits/remove-usage-limit-dialog.tsx @@ -0,0 +1,118 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { Trash2 } from 'lucide-react' +import { useState } from 'react' +import type { BillingLimit } from '@/core/modules/billing/models' +import { + defaultErrorToast, + defaultSuccessToast, + useToast, +} from '@/lib/hooks/use-toast' +import { useTRPC } from '@/trpc/client' +import { Button } from '@/ui/primitives/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from '@/ui/primitives/dialog' + +interface RemoveUsageLimitDialogProps { + disabled?: boolean + onRemoved: () => void + teamSlug: string +} + +export const RemoveUsageLimitDialog = ({ + disabled = false, + onRemoved, + teamSlug, +}: RemoveUsageLimitDialogProps) => { + const [isOpen, setIsOpen] = useState(false) + const { toast } = useToast() + const trpc = useTRPC() + const queryClient = useQueryClient() + + const limitsQueryKey = trpc.billing.getLimits.queryOptions({ + teamSlug, + }).queryKey + + const clearLimitMutation = useMutation( + trpc.billing.clearLimit.mutationOptions({ + onSuccess: () => { + queryClient.setQueryData( + limitsQueryKey, + (limits) => { + if (!limits) return limits + return { ...limits, limit_amount_gte: null } + } + ) + toast(defaultSuccessToast('Billing limit removed.')) + onRemoved() + setIsOpen(false) + queryClient.invalidateQueries({ queryKey: limitsQueryKey }) + }, + onError: (error) => { + toast( + defaultErrorToast(error.message || 'Failed to remove billing limit.') + ) + }, + }) + ) + + return ( + + + + + +
+
+ Remove usage limit? + + API limits will be removed and usage will become uncapped + +
+
+ + +
+
+
+
+ ) +} diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx new file mode 100644 index 000000000..6fcc5f487 --- /dev/null +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -0,0 +1,226 @@ +'use client' + +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { Pencil } from 'lucide-react' +import { type FormEvent, useEffect, useState } from 'react' +import { z } from 'zod' +import type { BillingLimit } from '@/core/modules/billing/models' +import { + defaultErrorToast, + defaultSuccessToast, + useToast, +} from '@/lib/hooks/use-toast' +import { cn } from '@/lib/utils' +import { + formatCurrencyValue, + sanitizeCurrencyInput, +} from '@/lib/utils/currency' +import { useTRPC } from '@/trpc/client' +import { Button } from '@/ui/primitives/button' +import { Input } from '@/ui/primitives/input' + +interface UsageAlertFormProps { + className?: string + originalValue: number | null + teamSlug: string +} + +const alertValueSchema = z + .string() + .trim() + .min(1, 'Enter a value.') + .regex(/^\d+$/, 'Enter a whole USD amount.') + .transform(Number) + .refine((value) => value >= 1, 'Value must be at least 1.') + +export const UsageAlertForm = ({ + className, + originalValue, + teamSlug, +}: UsageAlertFormProps) => { + const [draftValue, setDraftValue] = useState( + originalValue === null ? '' : formatCurrencyValue(originalValue) + ) + const [isEditing, setIsEditing] = useState(originalValue === null) + const { toast } = useToast() + const trpc = useTRPC() + const queryClient = useQueryClient() + + const limitsQueryKey = trpc.billing.getLimits.queryOptions({ + teamSlug, + }).queryKey + + useEffect(() => { + setDraftValue( + originalValue === null ? '' : formatCurrencyValue(originalValue) + ) + setIsEditing(originalValue === null) + }, [originalValue]) + + const setAlertMutation = useMutation( + trpc.billing.setLimit.mutationOptions({ + onSuccess: (_, variables) => { + queryClient.setQueryData( + limitsQueryKey, + (limits) => { + if (!limits) return limits + return { ...limits, alert_amount_gte: variables.value } + } + ) + toast(defaultSuccessToast('Billing alert saved.')) + setDraftValue(formatCurrencyValue(variables.value)) + setIsEditing(false) + queryClient.invalidateQueries({ queryKey: limitsQueryKey }) + }, + onError: (error) => { + toast(defaultErrorToast(error.message || 'Failed to save billing alert.')) + }, + }) + ) + + const clearAlertMutation = useMutation( + trpc.billing.clearLimit.mutationOptions({ + onSuccess: () => { + queryClient.setQueryData( + limitsQueryKey, + (limits) => { + if (!limits) return limits + return { ...limits, alert_amount_gte: null } + } + ) + toast(defaultSuccessToast('Billing alert removed.')) + setDraftValue('') + setIsEditing(true) + queryClient.invalidateQueries({ queryKey: limitsQueryKey }) + }, + onError: (error) => { + toast( + defaultErrorToast(error.message || 'Failed to remove billing alert.') + ) + }, + }) + ) + + const parsedValue = alertValueSchema.safeParse(draftValue) + const nextValue = parsedValue.success ? parsedValue.data : null + const isMutating = setAlertMutation.isPending || clearAlertMutation.isPending + const canSave = + parsedValue.success && nextValue !== originalValue && !isMutating + + const handleCancel = () => { + if (originalValue === null) return + setDraftValue(formatCurrencyValue(originalValue)) + setIsEditing(false) + } + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + + if (!parsedValue.success) { + toast( + defaultErrorToast( + parsedValue.error.issues[0]?.message || + 'Enter a billing alert amount.' + ) + ) + return + } + + if (parsedValue.data === originalValue) return + setAlertMutation.mutate({ teamSlug, type: 'alert', value: parsedValue.data }) + } + + if (originalValue !== null && !isEditing) + return ( +
+
+ $ + + {formatCurrencyValue(originalValue)} + +
+
+ + +
+
+ ) + + return ( +
+
+ $ + + setDraftValue(sanitizeCurrencyInput(event.target.value)) + } + placeholder="--" + value={draftValue} + /> +
+
+ {originalValue !== null && ( + + )} + +
+
+ ) +} diff --git a/src/features/dashboard/limits/usage-limit-form.tsx b/src/features/dashboard/limits/usage-limit-form.tsx new file mode 100644 index 000000000..6ee3ad3af --- /dev/null +++ b/src/features/dashboard/limits/usage-limit-form.tsx @@ -0,0 +1,296 @@ +'use client' + +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { Pencil, TriangleAlert } from 'lucide-react' +import { type FormEvent, type ReactNode, useEffect, useState } from 'react' +import { z } from 'zod' +import type { BillingLimit } from '@/core/modules/billing/models' +import { + defaultErrorToast, + defaultSuccessToast, + useToast, +} from '@/lib/hooks/use-toast' +import { cn } from '@/lib/utils' +import { + formatCurrencyValue, + sanitizeCurrencyInput, +} from '@/lib/utils/currency' +import { useTRPC } from '@/trpc/client' +import { Button } from '@/ui/primitives/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from '@/ui/primitives/dialog' +import { Input } from '@/ui/primitives/input' +import { RemoveUsageLimitDialog } from './remove-usage-limit-dialog' + +interface UsageLimitFormProps { + className?: string + originalValue: number | null + teamSlug: string +} + +interface LimitConfirmationDialogProps { + actions: ReactNode + children: ReactNode + icon: ReactNode + open: boolean + onOpenChange: (open: boolean) => void + title: string +} + +const saveErrorMessage = 'Failed to save billing limit.' +const saveSuccessMessage = 'Billing limit saved.' +const emptyErrorMessage = 'Enter a billing limit amount.' + +const limitValueSchema = z + .string() + .trim() + .min(1, 'Enter a value.') + .regex(/^\d+$/, 'Enter a whole USD amount.') + .transform(Number) + .refine((value) => value >= 1, 'Value must be at least 1.') + +const LimitConfirmationDialog = ({ + actions, + children, + icon, + open, + onOpenChange, + title, +}: LimitConfirmationDialogProps) => ( + + +
+
+
+
{icon}
+ {title} +
+ + {children} + +
+
+ {actions} +
+
+
+
+) + +export const UsageLimitForm = ({ + className, + originalValue, + teamSlug, +}: UsageLimitFormProps) => { + const [draftValue, setDraftValue] = useState( + originalValue === null ? '' : formatCurrencyValue(originalValue) + ) + const [isEditing, setIsEditing] = useState(originalValue === null) + const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false) + const { toast } = useToast() + const trpc = useTRPC() + const queryClient = useQueryClient() + + const limitsQueryKey = trpc.billing.getLimits.queryOptions({ + teamSlug, + }).queryKey + + useEffect(() => { + setDraftValue( + originalValue === null ? '' : formatCurrencyValue(originalValue) + ) + setIsEditing(originalValue === null) + }, [originalValue]) + + const setLimitMutation = useMutation( + trpc.billing.setLimit.mutationOptions({ + onSuccess: (_, variables) => { + queryClient.setQueryData( + limitsQueryKey, + (limits) => { + if (!limits) return limits + return { ...limits, limit_amount_gte: variables.value } + } + ) + toast(defaultSuccessToast(saveSuccessMessage)) + setDraftValue(formatCurrencyValue(variables.value)) + setIsEditing(false) + setIsSaveDialogOpen(false) + queryClient.invalidateQueries({ queryKey: limitsQueryKey }) + }, + onError: (error) => { + toast(defaultErrorToast(error.message || saveErrorMessage)) + }, + }) + ) + + const parsedValue = limitValueSchema.safeParse(draftValue) + const nextValue = parsedValue.success ? parsedValue.data : null + const isMutating = setLimitMutation.isPending + const canSave = + parsedValue.success && nextValue !== originalValue && !isMutating + + const handleCancel = () => { + if (originalValue === null) return + setDraftValue(formatCurrencyValue(originalValue)) + setIsEditing(false) + } + + const handleSubmit = (event: FormEvent) => { + event.preventDefault() + + if (!parsedValue.success) { + toast( + defaultErrorToast( + parsedValue.error.issues[0]?.message || emptyErrorMessage + ) + ) + return + } + + if (parsedValue.data === originalValue) return + setIsSaveDialogOpen(true) + } + + if (originalValue !== null && !isEditing) + return ( +
+
+ $ + + {formatCurrencyValue(originalValue)} + +
+
+ { + setDraftValue('') + setIsEditing(true) + }} + /> + +
+
+ ) + + return ( + <> +
+
+ $ + + setDraftValue(sanitizeCurrencyInput(event.target.value)) + } + placeholder="--" + value={draftValue} + /> +
+
+ {originalValue !== null && ( + + )} + +
+
+ + + + + } + icon={} + open={isSaveDialogOpen} + onOpenChange={setIsSaveDialogOpen} + title={`Set $${nextValue === null ? '--' : formatCurrencyValue(nextValue)} usage limit?`} + > + If your API usage hits this limit, all requests including sandbox + creation will be blocked. This may interrupt your services until you + raise or remove the limit. + + + ) +} diff --git a/src/features/dashboard/limits/usage-limits.tsx b/src/features/dashboard/limits/usage-limits.tsx index 1ebc61487..16ef0f223 100644 --- a/src/features/dashboard/limits/usage-limits.tsx +++ b/src/features/dashboard/limits/usage-limits.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils' import { useTRPC } from '@/trpc/client' import { Skeleton } from '@/ui/primitives/skeleton' import { useDashboard } from '../context' -import { LimitSection } from './limit-section' +import { UsageAlertSection, UsageLimitSection } from './limit-section' interface UsageLimitsProps { className?: string @@ -44,18 +44,14 @@ export const UsageLimits = ({ className }: UsageLimitsProps) => { className )} > - -
diff --git a/src/lib/utils/currency.ts b/src/lib/utils/currency.ts new file mode 100644 index 000000000..afa97b4df --- /dev/null +++ b/src/lib/utils/currency.ts @@ -0,0 +1,9 @@ +const usdIntegerFormatter = new Intl.NumberFormat('en-US') + +// Removes non-digits from a USD draft value. Example: "$1,250" -> "1250". +const sanitizeCurrencyInput = (value: string) => value.replace(/\D+/g, '') + +// Formats a USD integer for display. Example: 1250 -> "1,250". +const formatCurrencyValue = (value: number) => usdIntegerFormatter.format(value) + +export { formatCurrencyValue, sanitizeCurrencyInput } From f8c7998e676caceb4697dd6763a25409a03ec9a3 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 13:00:26 -0400 Subject: [PATCH 07/16] Refactor dashboard limits management by removing the old limit-section component and replacing it with separate UsageLimitSection and UsageAlertSection components. Update imports in UsageLimits to reflect the new structure. Enhance currency formatting in UsageLimitForm and UsageAlertForm for improved consistency. --- .../dashboard/limits/limit-section.tsx | 114 ------------------ .../dashboard/limits/usage-alert-form.tsx | 8 +- .../dashboard/limits/usage-alert-section.tsx | 53 ++++++++ .../dashboard/limits/usage-limit-form.tsx | 8 +- .../dashboard/limits/usage-limit-section.tsx | 62 ++++++++++ .../dashboard/limits/usage-limits.tsx | 3 +- 6 files changed, 125 insertions(+), 123 deletions(-) delete mode 100644 src/features/dashboard/limits/limit-section.tsx create mode 100644 src/features/dashboard/limits/usage-alert-section.tsx create mode 100644 src/features/dashboard/limits/usage-limit-section.tsx diff --git a/src/features/dashboard/limits/limit-section.tsx b/src/features/dashboard/limits/limit-section.tsx deleted file mode 100644 index 59aa866b1..000000000 --- a/src/features/dashboard/limits/limit-section.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client' - -import { Bell, TriangleAlert } from 'lucide-react' -import { cn } from '@/lib/utils' -import { AlertAsciiIcon } from './alert-ascii-icon' -import { LimitAsciiIcon } from './limit-ascii-icon' -import { UsageAlertForm } from './usage-alert-form' -import { UsageLimitForm } from './usage-limit-form' - -interface UsageLimitSectionProps { - className?: string - email: string - teamSlug: string - value: number | null -} - -interface UsageAlertSectionProps { - className?: string - email: string - teamSlug: string - value: number | null -} - -const currencyFormatter = new Intl.NumberFormat('en-US') - -const LimitPanelIcon = ({ icon }: { icon: typeof LimitAsciiIcon }) => { - const Icon = icon - return ( -
- -
- ) -} - -const UsageLimitSectionInfo = ({ - email, - value, -}: Pick) => { - const limitMessage = - value === null - ? 'All API requests are blocked after reaching this limit' - : `All API requests are blocked after reaching $${currencyFormatter.format(value)}` - - return ( -
-

- - {limitMessage} -

-

- - Automatic alerts at 50%, 80%, 90% and 100% sent to {email} -

-
- ) -} - -const UsageAlertSectionInfo = ({ - email, - value, -}: Pick) => { - const alertMessage = - value === null - ? `Informative alert will be sent to ${email} when this threshold is reached` - : `Informative alert will be sent to ${email} when the $${currencyFormatter.format(value)} threshold is reached` - - return ( -

{alertMessage}

- ) -} - -export const UsageLimitSection = ({ - className, - email, - teamSlug, - value, -}: UsageLimitSectionProps) => { - return ( -
-

- Usage Limit -

-
- -
- -
-
- -
- ) -} - -export const UsageAlertSection = ({ - className, - email, - teamSlug, - value, -}: UsageAlertSectionProps) => { - return ( -
-

- Usage Alert -

-
- -
- -
-
- -
- ) -} diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index 6fcc5f487..135767172 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -139,8 +139,8 @@ export const UsageAlertForm = ({ )} >
- $ - + $ + {formatCurrencyValue(originalValue)}
@@ -183,11 +183,11 @@ export const UsageAlertForm = ({ onSubmit={handleSubmit} >
- $ + $ diff --git a/src/features/dashboard/limits/usage-alert-section.tsx b/src/features/dashboard/limits/usage-alert-section.tsx new file mode 100644 index 000000000..88b1cfdf3 --- /dev/null +++ b/src/features/dashboard/limits/usage-alert-section.tsx @@ -0,0 +1,53 @@ +'use client' + +import { cn } from '@/lib/utils' +import { formatCurrencyValue } from '@/lib/utils/currency' +import { AlertAsciiIcon } from './alert-ascii-icon' +import { UsageAlertForm } from './usage-alert-form' + +interface UsageAlertSectionProps { + className?: string + email: string + teamSlug: string + value: number | null +} + +const UsageAlertSectionInfo = ({ + email, + value, +}: Pick) => { + const thresholdText = + value === null + ? 'this threshold' + : `the $${formatCurrencyValue(value)} threshold` + + return ( +

+ Informative alert will be sent to{' '} + {email} when {thresholdText}{' '} + is reached +

+ ) +} + +export const UsageAlertSection = ({ + className, + email, + teamSlug, + value, +}: UsageAlertSectionProps) => { + return ( +
+

Usage Alert

+
+
+ +
+
+ +
+
+ +
+ ) +} diff --git a/src/features/dashboard/limits/usage-limit-form.tsx b/src/features/dashboard/limits/usage-limit-form.tsx index 6ee3ad3af..ebeb8f366 100644 --- a/src/features/dashboard/limits/usage-limit-form.tsx +++ b/src/features/dashboard/limits/usage-limit-form.tsx @@ -168,8 +168,8 @@ export const UsageLimitForm = ({ )} >
- $ - + $ + {formatCurrencyValue(originalValue)}
@@ -210,11 +210,11 @@ export const UsageLimitForm = ({ onSubmit={handleSubmit} >
- $ + $ diff --git a/src/features/dashboard/limits/usage-limit-section.tsx b/src/features/dashboard/limits/usage-limit-section.tsx new file mode 100644 index 000000000..e55679150 --- /dev/null +++ b/src/features/dashboard/limits/usage-limit-section.tsx @@ -0,0 +1,62 @@ +'use client' + +import { Bell, TriangleAlert } from 'lucide-react' +import { cn } from '@/lib/utils' +import { formatCurrencyValue } from '@/lib/utils/currency' +import { LimitAsciiIcon } from './limit-ascii-icon' +import { UsageLimitForm } from './usage-limit-form' + +interface UsageLimitSectionProps { + className?: string + email: string + teamSlug: string + value: number | null +} + +const UsageLimitSectionInfo = ({ + email, + value, +}: Pick) => { + const limitMessage = + value === null + ? 'All API requests are blocked after reaching this limit' + : `All API requests are blocked after reaching $${formatCurrencyValue(value)}` + + return ( +
+

+ + {limitMessage} +

+

+ + + Automatic alerts at 50%, 80%, 90% and 100% sent to{' '} + {email} + +

+
+ ) +} + +export const UsageLimitSection = ({ + className, + email, + teamSlug, + value, +}: UsageLimitSectionProps) => { + return ( +
+

Usage Limit

+
+
+ +
+
+ +
+
+ +
+ ) +} diff --git a/src/features/dashboard/limits/usage-limits.tsx b/src/features/dashboard/limits/usage-limits.tsx index 16ef0f223..216efb724 100644 --- a/src/features/dashboard/limits/usage-limits.tsx +++ b/src/features/dashboard/limits/usage-limits.tsx @@ -6,7 +6,8 @@ import { cn } from '@/lib/utils' import { useTRPC } from '@/trpc/client' import { Skeleton } from '@/ui/primitives/skeleton' import { useDashboard } from '../context' -import { UsageAlertSection, UsageLimitSection } from './limit-section' +import { UsageAlertSection } from './usage-alert-section' +import { UsageLimitSection } from './usage-limit-section' interface UsageLimitsProps { className?: string From fa3856ca34ce6ebe7034b109064a3f357cd68c53 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 13:02:14 -0400 Subject: [PATCH 08/16] Enhance UsageAlertForm and UsageLimitForm to improve cancel functionality. Introduce shouldShowCancel logic to determine when the cancel button should be displayed, ensuring a clearer user experience when editing alert and limit values. --- src/features/dashboard/limits/usage-alert-form.tsx | 9 +++++++-- src/features/dashboard/limits/usage-limit-form.tsx | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index 135767172..099dd8280 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -106,9 +106,14 @@ export const UsageAlertForm = ({ const isMutating = setAlertMutation.isPending || clearAlertMutation.isPending const canSave = parsedValue.success && nextValue !== originalValue && !isMutating + const shouldShowCancel = originalValue !== null || draftValue.length > 0 const handleCancel = () => { - if (originalValue === null) return + if (originalValue === null) { + setDraftValue('') + return + } + setDraftValue(formatCurrencyValue(originalValue)) setIsEditing(false) } @@ -198,7 +203,7 @@ export const UsageAlertForm = ({ />
- {originalValue !== null && ( + {shouldShowCancel && (
- {originalValue !== null && ( + {shouldShowCancel && ( + + +
+
+
+
+ +
+ {title} +
+ + If your API usage hits this limit, all requests including sandbox + creation will be blocked. + +

+ This may disrupt your services. +

+
+
+ + +
+
+
+ + ) +} diff --git a/src/features/dashboard/limits/usage-limit-form.tsx b/src/features/dashboard/limits/usage-limit-form.tsx index 54c048280..4da0832ac 100644 --- a/src/features/dashboard/limits/usage-limit-form.tsx +++ b/src/features/dashboard/limits/usage-limit-form.tsx @@ -1,8 +1,9 @@ 'use client' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Pencil, TriangleAlert } from 'lucide-react' -import { type FormEvent, type ReactNode, useEffect, useState } from 'react' +import { Pencil } from 'lucide-react' +import type { FormEvent } from 'react' +import { useEffect, useState } from 'react' import { z } from 'zod' import type { BillingLimit } from '@/core/modules/billing/models' import { @@ -17,14 +18,9 @@ import { } from '@/lib/utils/currency' import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogTitle, -} from '@/ui/primitives/dialog' import { Input } from '@/ui/primitives/input' import { RemoveUsageLimitDialog } from './remove-usage-limit-dialog' +import { SetUsageLimitDialog } from './set-usage-limit-dialog' interface UsageLimitFormProps { className?: string @@ -32,15 +28,6 @@ interface UsageLimitFormProps { teamSlug: string } -interface LimitConfirmationDialogProps { - actions: ReactNode - children: ReactNode - icon: ReactNode - open: boolean - onOpenChange: (open: boolean) => void - title: string -} - const saveErrorMessage = 'Failed to save billing limit.' const saveSuccessMessage = 'Billing limit saved.' const emptyErrorMessage = 'Enter a billing limit amount.' @@ -53,37 +40,6 @@ const limitValueSchema = z .transform(Number) .refine((value) => value >= 1, 'Value must be at least 1.') -const LimitConfirmationDialog = ({ - actions, - children, - icon, - open, - onOpenChange, - title, -}: LimitConfirmationDialogProps) => ( - - -
-
-
-
{icon}
- {title} -
- - {children} - -
-
- {actions} -
-
-
-
-) - export const UsageLimitForm = ({ className, originalValue, @@ -93,7 +49,7 @@ export const UsageLimitForm = ({ originalValue === null ? '' : formatCurrencyValue(originalValue) ) const [isEditing, setIsEditing] = useState(originalValue === null) - const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false) + const [isSetDialogOpen, setIsSetDialogOpen] = useState(false) const { toast } = useToast() const trpc = useTRPC() const queryClient = useQueryClient() @@ -122,7 +78,7 @@ export const UsageLimitForm = ({ toast(defaultSuccessToast(saveSuccessMessage)) setDraftValue(formatCurrencyValue(variables.value)) setIsEditing(false) - setIsSaveDialogOpen(false) + setIsSetDialogOpen(false) queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, onError: (error) => { @@ -148,6 +104,24 @@ export const UsageLimitForm = ({ setIsEditing(false) } + const handleSetConfirm = () => { + if (!parsedValue.success) { + toast( + defaultErrorToast( + parsedValue.error.issues[0]?.message || emptyErrorMessage + ) + ) + return + } + + if (parsedValue.data === originalValue) return + setLimitMutation.mutate({ + teamSlug, + type: 'limit', + value: parsedValue.data, + }) + } + const handleSubmit = (event: FormEvent) => { event.preventDefault() @@ -160,8 +134,8 @@ export const UsageLimitForm = ({ return } - if (parsedValue.data === originalValue) return - setIsSaveDialogOpen(true) + if (parsedValue.data === originalValue || isMutating) return + setIsSetDialogOpen(true) } if (originalValue !== null && !isEditing) @@ -206,96 +180,51 @@ export const UsageLimitForm = ({ ) return ( - <> -
-
- $ - - setDraftValue(sanitizeCurrencyInput(event.target.value)) - } - placeholder="--" - value={draftValue} - /> -
-
- {shouldShowCancel && ( - - )} + +
+ $ + + setDraftValue(sanitizeCurrencyInput(event.target.value)) + } + placeholder="--" + value={draftValue} + /> +
+
+ {shouldShowCancel && ( -
- - - - - - } - icon={} - open={isSaveDialogOpen} - onOpenChange={setIsSaveDialogOpen} - title={`Set $${nextValue === null ? '--' : formatCurrencyValue(nextValue)} usage limit?`} - > - If your API usage hits this limit, all requests including sandbox - creation will be blocked. This may interrupt your services until you - raise or remove the limit. - - + )} + +
+ ) } diff --git a/src/features/dashboard/limits/usage-limit-section.tsx b/src/features/dashboard/limits/usage-limit-section.tsx index e55679150..d57175eb8 100644 --- a/src/features/dashboard/limits/usage-limit-section.tsx +++ b/src/features/dashboard/limits/usage-limit-section.tsx @@ -1,8 +1,8 @@ 'use client' -import { Bell, TriangleAlert } from 'lucide-react' import { cn } from '@/lib/utils' import { formatCurrencyValue } from '@/lib/utils/currency' +import { AlertIcon, WarningIcon } from '@/ui/primitives/icons' import { LimitAsciiIcon } from './limit-ascii-icon' import { UsageLimitForm } from './usage-limit-form' @@ -17,19 +17,29 @@ const UsageLimitSectionInfo = ({ email, value, }: Pick) => { - const limitMessage = - value === null - ? 'All API requests are blocked after reaching this limit' - : `All API requests are blocked after reaching $${formatCurrencyValue(value)}` + const isValueSet = value !== null + const limitMessage = isValueSet + ? `All API requests are blocked after reaching $${formatCurrencyValue(value)}` + : 'All API requests are blocked after reaching this limit' return (
-

- +

+ {limitMessage}

- + Automatic alerts at 50%, 80%, 90% and 100% sent to{' '} {email} From 65c7b0cce1f117e5c55f1d2771cb978a8fc61c70 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 13:25:02 -0400 Subject: [PATCH 10/16] Refactor UsageAlertForm and UsageLimitForm to use Input components for displaying currency values. Update setDraftValue to format currency correctly when editing values, enhancing user experience and consistency in currency formatting. --- src/features/dashboard/limits/usage-alert-form.tsx | 12 ++++++++---- src/features/dashboard/limits/usage-limit-form.tsx | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index 099dd8280..cdbca1c0d 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -145,9 +145,13 @@ export const UsageAlertForm = ({ >

$ - - {formatCurrencyValue(originalValue)} - +
- - + {!hideTrigger && ( + + + + )} +
Remove usage limit? @@ -107,7 +119,7 @@ export const RemoveUsageLimitDialog = ({ clearLimitMutation.mutate({ teamSlug, type: 'limit' }) } > - + Remove
diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index cdbca1c0d..581653145 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -1,8 +1,7 @@ 'use client' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Pencil } from 'lucide-react' -import { type FormEvent, useEffect, useState } from 'react' +import { type FormEvent, useEffect, useRef, useState } from 'react' import { z } from 'zod' import type { BillingLimit } from '@/core/modules/billing/models' import { @@ -17,6 +16,7 @@ import { } from '@/lib/utils/currency' import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' +import { EditIcon, TrashIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' interface UsageAlertFormProps { @@ -38,6 +38,7 @@ export const UsageAlertForm = ({ originalValue, teamSlug, }: UsageAlertFormProps) => { + const inputRef = useRef(null) const [draftValue, setDraftValue] = useState( originalValue === null ? '' : formatCurrencyValue(originalValue) ) @@ -57,6 +58,13 @@ export const UsageAlertForm = ({ setIsEditing(originalValue === null) }, [originalValue]) + useEffect(() => { + if (!isEditing) return + inputRef.current?.focus() + const inputLength = inputRef.current?.value.length ?? 0 + inputRef.current?.setSelectionRange(inputLength, inputLength) + }, [isEditing]) + const setAlertMutation = useMutation( trpc.billing.setLimit.mutationOptions({ onSuccess: (_, variables) => { @@ -104,11 +112,20 @@ export const UsageAlertForm = ({ const parsedValue = alertValueSchema.safeParse(draftValue) const nextValue = parsedValue.success ? parsedValue.data : null const isMutating = setAlertMutation.isPending || clearAlertMutation.isPending + const isClearIntent = + isEditing && originalValue !== null && draftValue.length === 0 const canSave = - parsedValue.success && nextValue !== originalValue && !isMutating - const shouldShowCancel = originalValue !== null || draftValue.length > 0 + isEditing && + (isClearIntent || (parsedValue.success && nextValue !== originalValue)) && + !isMutating + const shouldShowCancel = + isEditing && (originalValue !== null || draftValue.length > 0) const handleCancel = () => { + const activeElement = document.activeElement + if (activeElement instanceof HTMLElement) activeElement.blur() + inputRef.current?.blur() + if (originalValue === null) { setDraftValue('') return @@ -120,6 +137,12 @@ export const UsageAlertForm = ({ const handleSubmit = (event: FormEvent) => { event.preventDefault() + if (!isEditing) return + + if (isClearIntent) { + clearAlertMutation.mutate({ teamSlug, type: 'alert' }) + return + } if (!parsedValue.success) { toast( @@ -135,54 +158,6 @@ export const UsageAlertForm = ({ setAlertMutation.mutate({ teamSlug, type: 'alert', value: parsedValue.data }) } - if (originalValue !== null && !isEditing) - return ( -
-
- $ - -
-
- - -
-
- ) - return (
$ + onChange={(event) => { + if (!isEditing) return setDraftValue(sanitizeCurrencyInput(event.target.value)) - } + }} + onFocus={() => { + if (originalValue === null || isEditing) return + setIsEditing(true) + }} placeholder="--" + readOnly={!isEditing && originalValue !== null} value={draftValue} />
- {shouldShowCancel && ( - + {originalValue !== null && !isEditing ? ( + <> + + + + ) : ( + <> + {shouldShowCancel && ( + + )} + + )} -
) diff --git a/src/features/dashboard/limits/usage-limit-form.tsx b/src/features/dashboard/limits/usage-limit-form.tsx index 6f2ca387d..2c700a109 100644 --- a/src/features/dashboard/limits/usage-limit-form.tsx +++ b/src/features/dashboard/limits/usage-limit-form.tsx @@ -1,9 +1,8 @@ 'use client' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { Pencil } from 'lucide-react' import type { FormEvent } from 'react' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { z } from 'zod' import type { BillingLimit } from '@/core/modules/billing/models' import { @@ -18,6 +17,7 @@ import { } from '@/lib/utils/currency' import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' +import { EditIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { RemoveUsageLimitDialog } from './remove-usage-limit-dialog' import { SetUsageLimitDialog } from './set-usage-limit-dialog' @@ -45,10 +45,12 @@ export const UsageLimitForm = ({ originalValue, teamSlug, }: UsageLimitFormProps) => { + const inputRef = useRef(null) const [draftValue, setDraftValue] = useState( originalValue === null ? '' : formatCurrencyValue(originalValue) ) const [isEditing, setIsEditing] = useState(originalValue === null) + const [isRemoveDialogOpen, setIsRemoveDialogOpen] = useState(false) const [isSetDialogOpen, setIsSetDialogOpen] = useState(false) const { toast } = useToast() const trpc = useTRPC() @@ -65,6 +67,13 @@ export const UsageLimitForm = ({ setIsEditing(originalValue === null) }, [originalValue]) + useEffect(() => { + if (!isEditing) return + inputRef.current?.focus() + const inputLength = inputRef.current?.value.length ?? 0 + inputRef.current?.setSelectionRange(inputLength, inputLength) + }, [isEditing]) + const setLimitMutation = useMutation( trpc.billing.setLimit.mutationOptions({ onSuccess: (_, variables) => { @@ -90,11 +99,19 @@ export const UsageLimitForm = ({ const parsedValue = limitValueSchema.safeParse(draftValue) const nextValue = parsedValue.success ? parsedValue.data : null const isMutating = setLimitMutation.isPending + const isRemoveIntent = + isEditing && originalValue !== null && draftValue.length === 0 const canSave = - parsedValue.success && nextValue !== originalValue && !isMutating - const shouldShowCancel = originalValue !== null || draftValue.length > 0 + isEditing && + parsedValue.success && + nextValue !== originalValue && + !isMutating + const shouldShowCancel = + isEditing && (originalValue !== null || draftValue.length > 0) const handleCancel = () => { + inputRef.current?.blur() + if (originalValue === null) { setDraftValue('') return @@ -124,6 +141,12 @@ export const UsageLimitForm = ({ const handleSubmit = (event: FormEvent) => { event.preventDefault() + if (!isEditing) return + + if (isRemoveIntent) { + setIsRemoveDialogOpen(true) + return + } if (!parsedValue.success) { toast( @@ -138,51 +161,6 @@ export const UsageLimitForm = ({ setIsSetDialogOpen(true) } - if (originalValue !== null && !isEditing) - return ( -
-
- $ - -
-
- { - setDraftValue('') - setIsEditing(true) - }} - /> - -
-
- ) - return (
$ + onChange={(event) => { + if (!isEditing) return setDraftValue(sanitizeCurrencyInput(event.target.value)) - } + }} + onFocus={() => { + if (originalValue === null || isEditing) return + setIsEditing(true) + }} placeholder="--" + readOnly={!isEditing && originalValue !== null} value={draftValue} />
- {shouldShowCancel && ( - + {originalValue !== null && !isEditing ? ( + <> + { + setDraftValue('') + setIsEditing(true) + }} + /> + + + ) : ( + <> + {shouldShowCancel && ( + + )} + {isRemoveIntent ? ( + + ) : ( + + )} + )} -
+ { + setDraftValue('') + setIsEditing(true) + }} + /> ) } From 039e670953c5f978b63912c35d2a478abfbd78cb Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 14:12:18 -0400 Subject: [PATCH 12/16] Refactor UsageLimits component to improve loading state handling. Consolidate loading and limits rendering logic into a single conditional block, and adjust layout for better responsiveness by changing max-width. This enhances user experience during data fetching. --- .../dashboard/limits/usage-limits.tsx | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/features/dashboard/limits/usage-limits.tsx b/src/features/dashboard/limits/usage-limits.tsx index 216efb724..a1c43ef23 100644 --- a/src/features/dashboard/limits/usage-limits.tsx +++ b/src/features/dashboard/limits/usage-limits.tsx @@ -25,36 +25,32 @@ export const UsageLimits = ({ className }: UsageLimitsProps) => { if (!team) return null - if (isLoading || !limits) - return ( -
- - -
- ) - return (
- - + {isLoading || !limits ? ( + <> + + + + ) : ( + <> + + + + )}
) } From 7189e3d2250c2aac99b11ef1b9f1b3ab83bfa3ab Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 14:14:08 -0400 Subject: [PATCH 13/16] Run biome format --- src/features/dashboard/limits/usage-alert-form.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index 581653145..dd1b04cbd 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -81,7 +81,9 @@ export const UsageAlertForm = ({ queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, onError: (error) => { - toast(defaultErrorToast(error.message || 'Failed to save billing alert.')) + toast( + defaultErrorToast(error.message || 'Failed to save billing alert.') + ) }, }) ) @@ -155,7 +157,11 @@ export const UsageAlertForm = ({ } if (parsedValue.data === originalValue) return - setAlertMutation.mutate({ teamSlug, type: 'alert', value: parsedValue.data }) + setAlertMutation.mutate({ + teamSlug, + type: 'alert', + value: parsedValue.data, + }) } return ( From 85c4cc79b0c880bb28dc7ce324dea88174da1ed1 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 14:16:33 -0400 Subject: [PATCH 14/16] Add mount check in UsageAlertForm to prevent initial effect execution Introduce a ref to track component mount status, ensuring that the effect for focusing the input field only runs after the component has mounted. This improves the user experience by preventing unintended focus behavior during the initial render. --- src/features/dashboard/limits/usage-alert-form.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index dd1b04cbd..b27f43459 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -38,6 +38,7 @@ export const UsageAlertForm = ({ originalValue, teamSlug, }: UsageAlertFormProps) => { + const hasMountedRef = useRef(false) const inputRef = useRef(null) const [draftValue, setDraftValue] = useState( originalValue === null ? '' : formatCurrencyValue(originalValue) @@ -59,6 +60,11 @@ export const UsageAlertForm = ({ }, [originalValue]) useEffect(() => { + if (!hasMountedRef.current) { + hasMountedRef.current = true + return + } + if (!isEditing) return inputRef.current?.focus() const inputLength = inputRef.current?.value.length ?? 0 From 307ef381ea62a0bbd54067cb579991af37a5cd63 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Tue, 14 Apr 2026 14:49:36 -0400 Subject: [PATCH 15/16] Add currency validation and formatting utilities with comprehensive tests Introduce CurrencyInputSchema for validating positive whole USD amounts, along with utility functions sanitizeCurrencyInput and formatCurrencyValue. Implement unit tests for these utilities to ensure correct functionality, covering various input scenarios. Refactor UsageAlertForm and UsageLimitForm to utilize the new currency validation schema, enhancing user input handling and consistency in currency formatting. --- src/__test__/unit/currency.test.ts | 78 +++++++++ .../limits/remove-usage-limit-dialog.tsx | 2 + .../limits/set-usage-limit-dialog.tsx | 6 +- .../dashboard/limits/usage-alert-form.tsx | 116 +++++++------ .../dashboard/limits/usage-limit-form.tsx | 157 +++++++++--------- src/lib/utils/currency.ts | 12 +- 6 files changed, 244 insertions(+), 127 deletions(-) create mode 100644 src/__test__/unit/currency.test.ts diff --git a/src/__test__/unit/currency.test.ts b/src/__test__/unit/currency.test.ts new file mode 100644 index 000000000..cc810bae3 --- /dev/null +++ b/src/__test__/unit/currency.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { + CurrencyInputSchema, + formatCurrencyValue, + sanitizeCurrencyInput, +} from '@/lib/utils/currency' + +describe('sanitizeCurrencyInput', () => { + it('strips non-digit characters', () => { + expect(sanitizeCurrencyInput('$1,250')).toBe('1250') + }) + + it('preserves digit-only input', () => { + expect(sanitizeCurrencyInput('1250')).toBe('1250') + }) + + it('returns empty string for non-digit input', () => { + expect(sanitizeCurrencyInput('abc')).toBe('') + }) + + it('handles empty string', () => { + expect(sanitizeCurrencyInput('')).toBe('') + }) +}) + +describe('formatCurrencyValue', () => { + it('formats integer with thousands separator', () => { + expect(formatCurrencyValue(1250)).toBe('1,250') + }) + + it('formats small values without separator', () => { + expect(formatCurrencyValue(100)).toBe('100') + }) + + it('formats zero', () => { + expect(formatCurrencyValue(0)).toBe('0') + }) + + it('formats large values', () => { + expect(formatCurrencyValue(1000000)).toBe('1,000,000') + }) +}) + +describe('CurrencyInputSchema', () => { + it('accepts valid digit string', () => { + expect(CurrencyInputSchema.safeParse('100').success).toBe(true) + }) + + it('rejects empty string', () => { + expect(CurrencyInputSchema.safeParse('').success).toBe(false) + }) + + it('rejects whitespace-only string', () => { + expect(CurrencyInputSchema.safeParse(' ').success).toBe(false) + }) + + it('rejects strings with non-digit characters', () => { + expect(CurrencyInputSchema.safeParse('12.50').success).toBe(false) + expect(CurrencyInputSchema.safeParse('1,250').success).toBe(false) + expect(CurrencyInputSchema.safeParse('abc').success).toBe(false) + }) + + it('rejects zero', () => { + expect(CurrencyInputSchema.safeParse('0').success).toBe(false) + }) + + it('accepts minimum value of 1', () => { + expect(CurrencyInputSchema.safeParse('1').success).toBe(true) + }) + + it('trims whitespace before validating', () => { + expect(CurrencyInputSchema.safeParse(' 100 ').success).toBe(true) + }) + + it('accepts large values', () => { + expect(CurrencyInputSchema.safeParse('999999').success).toBe(true) + }) +}) diff --git a/src/features/dashboard/limits/remove-usage-limit-dialog.tsx b/src/features/dashboard/limits/remove-usage-limit-dialog.tsx index ba52fe208..81d1189af 100644 --- a/src/features/dashboard/limits/remove-usage-limit-dialog.tsx +++ b/src/features/dashboard/limits/remove-usage-limit-dialog.tsx @@ -1,3 +1,5 @@ +'use client' + import { useMutation, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import type { BillingLimit } from '@/core/modules/billing/models' diff --git a/src/features/dashboard/limits/set-usage-limit-dialog.tsx b/src/features/dashboard/limits/set-usage-limit-dialog.tsx index 3eaac3b2f..be0282c06 100644 --- a/src/features/dashboard/limits/set-usage-limit-dialog.tsx +++ b/src/features/dashboard/limits/set-usage-limit-dialog.tsx @@ -1,4 +1,5 @@ -import { TriangleAlert } from 'lucide-react' +'use client' + import { Button } from '@/ui/primitives/button' import { Dialog, @@ -7,6 +8,7 @@ import { DialogTitle, DialogTrigger, } from '@/ui/primitives/dialog' +import { WarningIcon } from '@/ui/primitives/icons' interface SetUsageLimitDialogProps { confirmDisabled: boolean @@ -45,7 +47,7 @@ export const SetUsageLimitDialog = ({
- +
{title}
diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index b27f43459..c827a2de2 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -1,7 +1,10 @@ 'use client' +import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { type FormEvent, useEffect, useRef, useState } from 'react' +import type { FormEvent } from 'react' +import { useEffect, useRef, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import { z } from 'zod' import type { BillingLimit } from '@/core/modules/billing/models' import { @@ -11,6 +14,7 @@ import { } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { + CurrencyInputSchema, formatCurrencyValue, sanitizeCurrencyInput, } from '@/lib/utils/currency' @@ -19,20 +23,18 @@ import { Button } from '@/ui/primitives/button' import { EditIcon, TrashIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' +const AlertFormSchema = z.object({ + amount: CurrencyInputSchema, +}) + +type AlertFormValues = z.infer + interface UsageAlertFormProps { className?: string originalValue: number | null teamSlug: string } -const alertValueSchema = z - .string() - .trim() - .min(1, 'Enter a value.') - .regex(/^\d+$/, 'Enter a whole USD amount.') - .transform(Number) - .refine((value) => value >= 1, 'Value must be at least 1.') - export const UsageAlertForm = ({ className, originalValue, @@ -40,9 +42,6 @@ export const UsageAlertForm = ({ }: UsageAlertFormProps) => { const hasMountedRef = useRef(false) const inputRef = useRef(null) - const [draftValue, setDraftValue] = useState( - originalValue === null ? '' : formatCurrencyValue(originalValue) - ) const [isEditing, setIsEditing] = useState(originalValue === null) const { toast } = useToast() const trpc = useTRPC() @@ -52,12 +51,22 @@ export const UsageAlertForm = ({ teamSlug, }).queryKey + const form = useForm({ + resolver: zodResolver(AlertFormSchema), + mode: 'onChange', + defaultValues: { + amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + }, + }) + + const draftValue = form.watch('amount') + useEffect(() => { - setDraftValue( - originalValue === null ? '' : formatCurrencyValue(originalValue) - ) + form.reset({ + amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + }) setIsEditing(originalValue === null) - }, [originalValue]) + }, [originalValue, form.reset]) useEffect(() => { if (!hasMountedRef.current) { @@ -82,7 +91,7 @@ export const UsageAlertForm = ({ } ) toast(defaultSuccessToast('Billing alert saved.')) - setDraftValue(formatCurrencyValue(variables.value)) + form.reset({ amount: formatCurrencyValue(variables.value) }) setIsEditing(false) queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, @@ -105,7 +114,7 @@ export const UsageAlertForm = ({ } ) toast(defaultSuccessToast('Billing alert removed.')) - setDraftValue('') + form.reset({ amount: '' }) setIsEditing(true) queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, @@ -117,14 +126,13 @@ export const UsageAlertForm = ({ }) ) - const parsedValue = alertValueSchema.safeParse(draftValue) - const nextValue = parsedValue.success ? parsedValue.data : null const isMutating = setAlertMutation.isPending || clearAlertMutation.isPending const isClearIntent = isEditing && originalValue !== null && draftValue.length === 0 const canSave = isEditing && - (isClearIntent || (parsedValue.success && nextValue !== originalValue)) && + (isClearIntent || + (form.formState.isValid && Number(draftValue) !== originalValue)) && !isMutating const shouldShowCancel = isEditing && (originalValue !== null || draftValue.length > 0) @@ -135,15 +143,15 @@ export const UsageAlertForm = ({ inputRef.current?.blur() if (originalValue === null) { - setDraftValue('') + form.reset({ amount: '' }) return } - setDraftValue(formatCurrencyValue(originalValue)) + form.reset({ amount: formatCurrencyValue(originalValue) }) setIsEditing(false) } - const handleSubmit = (event: FormEvent) => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault() if (!isEditing) return @@ -152,22 +160,20 @@ export const UsageAlertForm = ({ return } - if (!parsedValue.success) { + const isValid = await form.trigger() + if (!isValid) { toast( defaultErrorToast( - parsedValue.error.issues[0]?.message || + form.formState.errors.amount?.message || 'Enter a billing alert amount.' ) ) return } - if (parsedValue.data === originalValue) return - setAlertMutation.mutate({ - teamSlug, - type: 'alert', - value: parsedValue.data, - }) + const value = Number(form.getValues('amount')) + if (value === originalValue) return + setAlertMutation.mutate({ teamSlug, type: 'alert', value }) } return ( @@ -180,23 +186,33 @@ export const UsageAlertForm = ({ >
$ - { - if (!isEditing) return - setDraftValue(sanitizeCurrencyInput(event.target.value)) - }} - onFocus={() => { - if (originalValue === null || isEditing) return - setIsEditing(true) - }} - placeholder="--" - readOnly={!isEditing && originalValue !== null} - value={draftValue} + ( + { + field.ref(el) + inputRef.current = el + }} + className="prose-value-big text-fg h-auto border-0 bg-transparent px-0 py-0 font-mono shadow-none placeholder:text-fg-tertiary hover:bg-transparent focus:bg-transparent focus:[border-bottom:0] focus:outline-none" + disabled={isMutating} + inputMode="numeric" + onChange={(event) => { + if (!isEditing) return + field.onChange(sanitizeCurrencyInput(event.target.value)) + }} + onBlur={field.onBlur} + onFocus={() => { + if (originalValue === null || isEditing) return + setIsEditing(true) + }} + placeholder="--" + readOnly={!isEditing && originalValue !== null} + value={field.value} + /> + )} />
@@ -223,7 +239,7 @@ export const UsageAlertForm = ({ className="font-sans normal-case" disabled={isMutating} onClick={() => { - setDraftValue(formatCurrencyValue(originalValue)) + form.reset({ amount: formatCurrencyValue(originalValue) }) setIsEditing(true) }} > diff --git a/src/features/dashboard/limits/usage-limit-form.tsx b/src/features/dashboard/limits/usage-limit-form.tsx index 2c700a109..0d9d75df6 100644 --- a/src/features/dashboard/limits/usage-limit-form.tsx +++ b/src/features/dashboard/limits/usage-limit-form.tsx @@ -1,8 +1,10 @@ 'use client' +import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' import type { FormEvent } from 'react' import { useEffect, useRef, useState } from 'react' +import { Controller, useForm } from 'react-hook-form' import { z } from 'zod' import type { BillingLimit } from '@/core/modules/billing/models' import { @@ -12,43 +14,35 @@ import { } from '@/lib/hooks/use-toast' import { cn } from '@/lib/utils' import { + CurrencyInputSchema, formatCurrencyValue, sanitizeCurrencyInput, } from '@/lib/utils/currency' import { useTRPC } from '@/trpc/client' import { Button } from '@/ui/primitives/button' -import { EditIcon } from '@/ui/primitives/icons' +import { EditIcon, TrashIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { RemoveUsageLimitDialog } from './remove-usage-limit-dialog' import { SetUsageLimitDialog } from './set-usage-limit-dialog' +const limitFormSchema = z.object({ + amount: CurrencyInputSchema, +}) + +type LimitFormValues = z.infer + interface UsageLimitFormProps { className?: string originalValue: number | null teamSlug: string } -const saveErrorMessage = 'Failed to save billing limit.' -const saveSuccessMessage = 'Billing limit saved.' -const emptyErrorMessage = 'Enter a billing limit amount.' - -const limitValueSchema = z - .string() - .trim() - .min(1, 'Enter a value.') - .regex(/^\d+$/, 'Enter a whole USD amount.') - .transform(Number) - .refine((value) => value >= 1, 'Value must be at least 1.') - export const UsageLimitForm = ({ className, originalValue, teamSlug, }: UsageLimitFormProps) => { const inputRef = useRef(null) - const [draftValue, setDraftValue] = useState( - originalValue === null ? '' : formatCurrencyValue(originalValue) - ) const [isEditing, setIsEditing] = useState(originalValue === null) const [isRemoveDialogOpen, setIsRemoveDialogOpen] = useState(false) const [isSetDialogOpen, setIsSetDialogOpen] = useState(false) @@ -60,12 +54,22 @@ export const UsageLimitForm = ({ teamSlug, }).queryKey + const form = useForm({ + resolver: zodResolver(limitFormSchema), + mode: 'onChange', + defaultValues: { + amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + }, + }) + + const draftValue = form.watch('amount') + useEffect(() => { - setDraftValue( - originalValue === null ? '' : formatCurrencyValue(originalValue) - ) + form.reset({ + amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + }) setIsEditing(originalValue === null) - }, [originalValue]) + }, [originalValue, form.reset]) useEffect(() => { if (!isEditing) return @@ -84,26 +88,27 @@ export const UsageLimitForm = ({ return { ...limits, limit_amount_gte: variables.value } } ) - toast(defaultSuccessToast(saveSuccessMessage)) - setDraftValue(formatCurrencyValue(variables.value)) + toast(defaultSuccessToast('Billing limit saved.')) + form.reset({ amount: formatCurrencyValue(variables.value) }) setIsEditing(false) setIsSetDialogOpen(false) queryClient.invalidateQueries({ queryKey: limitsQueryKey }) }, onError: (error) => { - toast(defaultErrorToast(error.message || saveErrorMessage)) + toast( + defaultErrorToast(error.message || 'Failed to save billing limit.') + ) }, }) ) - const parsedValue = limitValueSchema.safeParse(draftValue) - const nextValue = parsedValue.success ? parsedValue.data : null + const nextValue = form.formState.isValid ? Number(draftValue) : null const isMutating = setLimitMutation.isPending const isRemoveIntent = isEditing && originalValue !== null && draftValue.length === 0 const canSave = isEditing && - parsedValue.success && + form.formState.isValid && nextValue !== originalValue && !isMutating const shouldShowCancel = @@ -113,33 +118,22 @@ export const UsageLimitForm = ({ inputRef.current?.blur() if (originalValue === null) { - setDraftValue('') + form.reset({ amount: '' }) return } - setDraftValue(formatCurrencyValue(originalValue)) + form.reset({ amount: formatCurrencyValue(originalValue) }) setIsEditing(false) } const handleSetConfirm = () => { - if (!parsedValue.success) { - toast( - defaultErrorToast( - parsedValue.error.issues[0]?.message || emptyErrorMessage - ) - ) - return - } - - if (parsedValue.data === originalValue) return - setLimitMutation.mutate({ - teamSlug, - type: 'limit', - value: parsedValue.data, - }) + if (!form.formState.isValid) return + const value = Number(form.getValues('amount')) + if (value === originalValue) return + setLimitMutation.mutate({ teamSlug, type: 'limit', value }) } - const handleSubmit = (event: FormEvent) => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault() if (!isEditing) return @@ -148,16 +142,19 @@ export const UsageLimitForm = ({ return } - if (!parsedValue.success) { + const isValid = await form.trigger() + if (!isValid) { toast( defaultErrorToast( - parsedValue.error.issues[0]?.message || emptyErrorMessage + form.formState.errors.amount?.message || + 'Enter a billing limit amount.' ) ) return } - if (parsedValue.data === originalValue || isMutating) return + const value = Number(form.getValues('amount')) + if (value === originalValue || isMutating) return setIsSetDialogOpen(true) } @@ -171,36 +168,48 @@ export const UsageLimitForm = ({ >
$ - { - if (!isEditing) return - setDraftValue(sanitizeCurrencyInput(event.target.value)) - }} - onFocus={() => { - if (originalValue === null || isEditing) return - setIsEditing(true) - }} - placeholder="--" - readOnly={!isEditing && originalValue !== null} - value={draftValue} + ( + { + field.ref(el) + inputRef.current = el + }} + className="prose-value-big text-fg h-auto border-0 bg-transparent px-0 py-0 font-mono shadow-none placeholder:text-fg-tertiary hover:bg-transparent focus:bg-transparent focus:[border-bottom:0] focus:outline-none" + disabled={isMutating} + inputMode="numeric" + onChange={(event) => { + if (!isEditing) return + field.onChange(sanitizeCurrencyInput(event.target.value)) + }} + onBlur={field.onBlur} + onFocus={() => { + if (originalValue === null || isEditing) return + setIsEditing(true) + }} + placeholder="--" + readOnly={!isEditing && originalValue !== null} + value={field.value} + /> + )} />
{originalValue !== null && !isEditing ? ( <> - { - setDraftValue('') - setIsEditing(true) - }} - /> + onClick={() => setIsRemoveDialogOpen(true)} + > + + Remove + ) : ( { - setDraftValue('') + form.reset({ amount: '' }) setIsEditing(true) }} /> diff --git a/src/lib/utils/currency.ts b/src/lib/utils/currency.ts index afa97b4df..6936648f7 100644 --- a/src/lib/utils/currency.ts +++ b/src/lib/utils/currency.ts @@ -1,9 +1,19 @@ +import { z } from 'zod' + const usdIntegerFormatter = new Intl.NumberFormat('en-US') +// Validates a string as a positive whole USD amount. Example: "1250" -> valid, "abc" -> invalid. +const CurrencyInputSchema = z + .string() + .trim() + .min(1, 'Enter a value.') + .regex(/^\d+$/, 'Enter a whole USD amount.') + .refine((value) => Number(value) >= 1, 'Value must be at least 1.') + // Removes non-digits from a USD draft value. Example: "$1,250" -> "1250". const sanitizeCurrencyInput = (value: string) => value.replace(/\D+/g, '') // Formats a USD integer for display. Example: 1250 -> "1,250". const formatCurrencyValue = (value: number) => usdIntegerFormatter.format(value) -export { formatCurrencyValue, sanitizeCurrencyInput } +export { CurrencyInputSchema, formatCurrencyValue, sanitizeCurrencyInput } From 58f76f71c713682df1a0c84ece1aa3b9d3011233 Mon Sep 17 00:00:00 2001 From: Sarim Malik Date: Wed, 15 Apr 2026 12:26:01 -0400 Subject: [PATCH 16/16] Refactor currency-related tests and forms for improved consistency and readability Consolidate test cases for currency input sanitization and formatting using parameterized tests. Update UsageAlertForm and UsageLimitForm to utilize a formatted value for better clarity and maintainability. Enhance user interaction by streamlining editing and cancel functionalities, ensuring a more intuitive experience when managing currency values. --- src/__test__/unit/currency.test.ts | 87 ++++++------------- .../dashboard/limits/usage-alert-form.tsx | 40 +++++---- .../dashboard/limits/usage-limit-form.tsx | 38 ++++---- 3 files changed, 73 insertions(+), 92 deletions(-) diff --git a/src/__test__/unit/currency.test.ts b/src/__test__/unit/currency.test.ts index cc810bae3..d78aed6cf 100644 --- a/src/__test__/unit/currency.test.ts +++ b/src/__test__/unit/currency.test.ts @@ -6,73 +6,40 @@ import { } from '@/lib/utils/currency' describe('sanitizeCurrencyInput', () => { - it('strips non-digit characters', () => { - expect(sanitizeCurrencyInput('$1,250')).toBe('1250') - }) - - it('preserves digit-only input', () => { - expect(sanitizeCurrencyInput('1250')).toBe('1250') - }) - - it('returns empty string for non-digit input', () => { - expect(sanitizeCurrencyInput('abc')).toBe('') - }) - - it('handles empty string', () => { - expect(sanitizeCurrencyInput('')).toBe('') + it.each([ + ['$1,250', '1250'], + ['1250', '1250'], + ['abc', ''], + ['', ''], + ])('returns %p -> %p', (value: string, expected: string) => { + expect(sanitizeCurrencyInput(value)).toBe(expected) }) }) describe('formatCurrencyValue', () => { - it('formats integer with thousands separator', () => { - expect(formatCurrencyValue(1250)).toBe('1,250') - }) - - it('formats small values without separator', () => { - expect(formatCurrencyValue(100)).toBe('100') - }) - - it('formats zero', () => { - expect(formatCurrencyValue(0)).toBe('0') - }) - - it('formats large values', () => { - expect(formatCurrencyValue(1000000)).toBe('1,000,000') + it.each([ + [1250, '1,250'], + [100, '100'], + [0, '0'], + [1000000, '1,000,000'], + ])('returns %p -> %p', (value: number, expected: string) => { + expect(formatCurrencyValue(value)).toBe(expected) }) }) describe('CurrencyInputSchema', () => { - it('accepts valid digit string', () => { - expect(CurrencyInputSchema.safeParse('100').success).toBe(true) - }) - - it('rejects empty string', () => { - expect(CurrencyInputSchema.safeParse('').success).toBe(false) - }) - - it('rejects whitespace-only string', () => { - expect(CurrencyInputSchema.safeParse(' ').success).toBe(false) - }) - - it('rejects strings with non-digit characters', () => { - expect(CurrencyInputSchema.safeParse('12.50').success).toBe(false) - expect(CurrencyInputSchema.safeParse('1,250').success).toBe(false) - expect(CurrencyInputSchema.safeParse('abc').success).toBe(false) - }) - - it('rejects zero', () => { - expect(CurrencyInputSchema.safeParse('0').success).toBe(false) - }) - - it('accepts minimum value of 1', () => { - expect(CurrencyInputSchema.safeParse('1').success).toBe(true) - }) - - it('trims whitespace before validating', () => { - expect(CurrencyInputSchema.safeParse(' 100 ').success).toBe(true) - }) - - it('accepts large values', () => { - expect(CurrencyInputSchema.safeParse('999999').success).toBe(true) + it.each(['100', '1', ' 100 ', '999999'])('accepts %p', (value: string) => { + expect(CurrencyInputSchema.safeParse(value).success).toBe(true) + }) + + it.each([ + '', + ' ', + '12.50', + '1,250', + 'abc', + '0', + ])('rejects %p', (value: string) => { + expect(CurrencyInputSchema.safeParse(value).success).toBe(false) }) }) diff --git a/src/features/dashboard/limits/usage-alert-form.tsx b/src/features/dashboard/limits/usage-alert-form.tsx index c827a2de2..90b98db37 100644 --- a/src/features/dashboard/limits/usage-alert-form.tsx +++ b/src/features/dashboard/limits/usage-alert-form.tsx @@ -46,6 +46,8 @@ export const UsageAlertForm = ({ const { toast } = useToast() const trpc = useTRPC() const queryClient = useQueryClient() + const formattedOriginalValue = + originalValue === null ? '' : formatCurrencyValue(originalValue) const limitsQueryKey = trpc.billing.getLimits.queryOptions({ teamSlug, @@ -55,7 +57,7 @@ export const UsageAlertForm = ({ resolver: zodResolver(AlertFormSchema), mode: 'onChange', defaultValues: { - amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + amount: formattedOriginalValue, }, }) @@ -63,10 +65,10 @@ export const UsageAlertForm = ({ useEffect(() => { form.reset({ - amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + amount: formattedOriginalValue, }) setIsEditing(originalValue === null) - }, [originalValue, form.reset]) + }, [formattedOriginalValue, originalValue, form.reset]) useEffect(() => { if (!hasMountedRef.current) { @@ -126,18 +128,27 @@ export const UsageAlertForm = ({ }) ) + const clearAlert = (): void => + clearAlertMutation.mutate({ teamSlug, type: 'alert' }) + const isMutating = setAlertMutation.isPending || clearAlertMutation.isPending + const nextValue = form.formState.isValid ? Number(draftValue) : null const isClearIntent = isEditing && originalValue !== null && draftValue.length === 0 const canSave = isEditing && - (isClearIntent || - (form.formState.isValid && Number(draftValue) !== originalValue)) && + (isClearIntent || (nextValue !== null && nextValue !== originalValue)) && !isMutating const shouldShowCancel = isEditing && (originalValue !== null || draftValue.length > 0) - const handleCancel = () => { + const startEditing = (): void => { + if (originalValue === null) return + form.reset({ amount: formattedOriginalValue }) + setIsEditing(true) + } + + const handleCancel = (): void => { const activeElement = document.activeElement if (activeElement instanceof HTMLElement) activeElement.blur() inputRef.current?.blur() @@ -147,7 +158,7 @@ export const UsageAlertForm = ({ return } - form.reset({ amount: formatCurrencyValue(originalValue) }) + form.reset({ amount: formattedOriginalValue }) setIsEditing(false) } @@ -156,7 +167,7 @@ export const UsageAlertForm = ({ if (!isEditing) return if (isClearIntent) { - clearAlertMutation.mutate({ teamSlug, type: 'alert' }) + clearAlert() return } @@ -205,8 +216,8 @@ export const UsageAlertForm = ({ }} onBlur={field.onBlur} onFocus={() => { - if (originalValue === null || isEditing) return - setIsEditing(true) + if (isEditing) return + startEditing() }} placeholder="--" readOnly={!isEditing && originalValue !== null} @@ -225,9 +236,7 @@ export const UsageAlertForm = ({ className="font-sans normal-case" disabled={isMutating} loading={clearAlertMutation.isPending} - onClick={() => - clearAlertMutation.mutate({ teamSlug, type: 'alert' }) - } + onClick={clearAlert} > Remove @@ -238,10 +247,7 @@ export const UsageAlertForm = ({ size="md" className="font-sans normal-case" disabled={isMutating} - onClick={() => { - form.reset({ amount: formatCurrencyValue(originalValue) }) - setIsEditing(true) - }} + onClick={startEditing} > Edit diff --git a/src/features/dashboard/limits/usage-limit-form.tsx b/src/features/dashboard/limits/usage-limit-form.tsx index 0d9d75df6..cf6eb6153 100644 --- a/src/features/dashboard/limits/usage-limit-form.tsx +++ b/src/features/dashboard/limits/usage-limit-form.tsx @@ -49,6 +49,8 @@ export const UsageLimitForm = ({ const { toast } = useToast() const trpc = useTRPC() const queryClient = useQueryClient() + const formattedOriginalValue = + originalValue === null ? '' : formatCurrencyValue(originalValue) const limitsQueryKey = trpc.billing.getLimits.queryOptions({ teamSlug, @@ -58,7 +60,7 @@ export const UsageLimitForm = ({ resolver: zodResolver(limitFormSchema), mode: 'onChange', defaultValues: { - amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + amount: formattedOriginalValue, }, }) @@ -66,10 +68,10 @@ export const UsageLimitForm = ({ useEffect(() => { form.reset({ - amount: originalValue === null ? '' : formatCurrencyValue(originalValue), + amount: formattedOriginalValue, }) setIsEditing(originalValue === null) - }, [originalValue, form.reset]) + }, [formattedOriginalValue, originalValue, form.reset]) useEffect(() => { if (!isEditing) return @@ -113,8 +115,17 @@ export const UsageLimitForm = ({ !isMutating const shouldShowCancel = isEditing && (originalValue !== null || draftValue.length > 0) + const setLimitTitle = `Set $${nextValue === null ? '--' : formatCurrencyValue(nextValue)} usage limit?` - const handleCancel = () => { + const startEditing = (): void => { + if (originalValue === null) return + form.reset({ amount: formattedOriginalValue }) + setIsEditing(true) + } + + const openRemoveDialog = (): void => setIsRemoveDialogOpen(true) + + const handleCancel = (): void => { inputRef.current?.blur() if (originalValue === null) { @@ -122,11 +133,11 @@ export const UsageLimitForm = ({ return } - form.reset({ amount: formatCurrencyValue(originalValue) }) + form.reset({ amount: formattedOriginalValue }) setIsEditing(false) } - const handleSetConfirm = () => { + const handleSetConfirm = (): void => { if (!form.formState.isValid) return const value = Number(form.getValues('amount')) if (value === originalValue) return @@ -187,8 +198,8 @@ export const UsageLimitForm = ({ }} onBlur={field.onBlur} onFocus={() => { - if (originalValue === null || isEditing) return - setIsEditing(true) + if (isEditing) return + startEditing() }} placeholder="--" readOnly={!isEditing && originalValue !== null} @@ -205,7 +216,7 @@ export const UsageLimitForm = ({ variant="outline" size="md" disabled={isMutating} - onClick={() => setIsRemoveDialogOpen(true)} + onClick={openRemoveDialog} > Remove @@ -216,10 +227,7 @@ export const UsageLimitForm = ({ size="md" className="font-sans normal-case" disabled={isMutating} - onClick={() => { - form.reset({ amount: formatCurrencyValue(originalValue) }) - setIsEditing(true) - }} + onClick={startEditing} > Edit @@ -246,7 +254,7 @@ export const UsageLimitForm = ({ size="md" className="font-sans normal-case" disabled={isMutating} - onClick={() => setIsRemoveDialogOpen(true)} + onClick={openRemoveDialog} > Set @@ -257,7 +265,7 @@ export const UsageLimitForm = ({ onConfirm={handleSetConfirm} onOpenChange={setIsSetDialogOpen} open={isSetDialogOpen} - title={`Set $${nextValue === null ? '--' : formatCurrencyValue(nextValue)} usage limit?`} + title={setLimitTitle} triggerDisabled={!canSave} /> )}