diff --git a/apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts b/apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts new file mode 100644 index 0000000000..87d54c7515 --- /dev/null +++ b/apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts @@ -0,0 +1,42 @@ +import { withWorkspace } from "@/lib/auth"; +import { tb } from "@/lib/tinybird"; +import z from "@/lib/zod"; +import { usageQuerySchema, usageResponse } from "@/lib/zod/schemas/usage"; +import { getFirstAndLastDay } from "@dub/utils"; +import { NextResponse } from "next/server"; + +export const GET = withWorkspace(async ({ searchParams, workspace }) => { + const { resource } = usageQuerySchema.parse(searchParams); + const { billingCycleStart } = workspace; + const { firstDay, lastDay } = getFirstAndLastDay(billingCycleStart); + + const pipe = tb.buildPipe({ + pipe: `v1_usage`, + // we extend this here since we don't need to include all the additional parameters + // in the actual request query schema + parameters: usageQuerySchema.extend({ + workspaceId: z + .string() + .optional() + .transform((v) => { + if (v && !v.startsWith("ws_")) { + return `ws_${v}`; + } else { + return v; + } + }), + start: z.string(), + end: z.string(), + }), + data: usageResponse, + }); + + const response = await pipe({ + resource, + workspaceId: workspace.id, + start: firstDay.toISOString().replace("T", " ").replace("Z", ""), + end: lastDay.toISOString().replace("T", " ").replace("Z", ""), + }); + + return NextResponse.json(response.data); +}); diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/page-client.tsx index 6f4ed8eb4e..98e93defa2 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/page-client.tsx @@ -3,19 +3,25 @@ import useTags from "@/lib/swr/use-tags"; import useUsers from "@/lib/swr/use-users"; import useWorkspace from "@/lib/swr/use-workspace"; -import { Divider } from "@/ui/shared/icons"; -import Infinity from "@/ui/shared/icons/infinity"; import PlanBadge from "@/ui/workspaces/plan-badge"; -import { Button, buttonVariants, InfoTooltip, ProgressBar } from "@dub/ui"; +import { + Button, + buttonVariants, + Icon, + InfoTooltip, + ProgressBar, + useRouterStuff, +} from "@dub/ui"; +import { CircleDollar, CursorRays, Hyperlink } from "@dub/ui/src/icons"; import { cn, getFirstAndLastDay, nFormatter } from "@dub/utils"; import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import { CSSProperties, useMemo, useState } from "react"; import { toast } from "sonner"; +import { UsageChart } from "./usage-chart"; export default function WorkspaceBillingClient() { const router = useRouter(); - const searchParams = useSearchParams(); const { id: workspaceId, @@ -60,10 +66,10 @@ export default function WorkspaceBillingClient() { return (
-
-
-

Plan & Usage

-

+

+
+

Plan and Usage

+

You are currently on the{" "} {plan ? ( @@ -112,44 +118,45 @@ export default function WorkspaceBillingClient() {

)}
-
-
- {conversionEnabled && ( - +
+
+ - )} - = 1000000000) || false} - /> + + {conversionEnabled && ( + + )} +
+
+ +
-
- = 1000000000) || false} - /> +
-
-
-
+
{plan ? (

{plan === "enterprise" @@ -220,6 +225,103 @@ export default function WorkspaceBillingClient() { ); } +function UsageTabCard({ + id, + icon: Icon, + title, + usage: usageProp, + limit: limitProp, + unit, + root, +}: { + id: string; + icon: Icon; + title: string; + usage?: number; + limit?: number; + unit?: string; + root?: boolean; +}) { + const { searchParams, queryParams } = useRouterStuff(); + + const isActive = + searchParams.get("tab") === id || (!searchParams.get("tab") && root); + + const [usage, limit] = + unit === "$" && usageProp !== undefined && limitProp !== undefined + ? [usageProp / 100, limitProp / 100] + : [usageProp, limitProp]; + + const loading = usage === undefined || limit === undefined; + const unlimited = limit !== undefined && limit >= 1000000000; + const warning = !loading && !unlimited && usage >= limit * 0.9; + const remaining = !loading && !unlimited ? Math.max(0, limit - usage) : 0; + + const prefix = unit || ""; + + return ( + + ); +} + function UsageCategory(data: { title: string; unit: string; @@ -234,29 +336,28 @@ function UsageCategory(data: { usage = usage / 100; usageLimit = usageLimit / 100; } + return ( -

+
-

{title}

+

{title}

{numberOnly ? ( -
+
{usage || usage === 0 ? ( -

+

{nFormatter(usage, { full: true })}

) : ( -
- )} - - {usageLimit && usageLimit >= 1000000000 ? ( - - ) : ( -

- {nFormatter(usageLimit, { full: true })} -

+
)} + / +

+ {usageLimit && usageLimit >= 1000000000 + ? "∞" + : nFormatter(usageLimit, { full: true })} +

) : (
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/usage-chart.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/usage-chart.tsx new file mode 100644 index 0000000000..1a2a41b692 --- /dev/null +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/settings/billing/usage-chart.tsx @@ -0,0 +1,137 @@ +import useUsage from "@/lib/swr/use-usage"; +import { Bars } from "@/ui/charts/bars"; +import TimeSeriesChart from "@/ui/charts/time-series-chart"; +import XAxis from "@/ui/charts/x-axis"; +import YAxis from "@/ui/charts/y-axis"; +import { EmptyState } from "@dub/blocks/src/empty-state"; +import { LoadingSpinner } from "@dub/ui"; +import { CircleDollar, CursorRays, Hyperlink } from "@dub/ui/src/icons"; +import { formatDate, nFormatter } from "@dub/utils"; +import { LinearGradient } from "@visx/gradient"; +import { useSearchParams } from "next/navigation"; +import { ComponentProps, Fragment, useMemo } from "react"; + +const RESOURCES = ["links", "events", "revenue"] as const; +const resourceEmptyStates: Record< + (typeof RESOURCES)[number], + ComponentProps +> = { + links: { + icon: Hyperlink, + title: "Links Created", + description: + "No short links have been created in the current billing cycle.", + }, + events: { + icon: CursorRays, + title: "Events Tracked", + description: "No events have been tracked in the current billing cycle.", + }, + revenue: { + icon: CircleDollar, + title: "Revenue Tracked", + description: "No revenue has been tracked in the current billing cycle.", + }, +}; + +export function UsageChart() { + const searchParams = useSearchParams(); + const resource = + RESOURCES.find((r) => r === searchParams.get("tab")) ?? "links"; + + const { usage, loading } = useUsage({ resource }); + + const chartData = useMemo( + () => + usage?.map(({ date, value }) => ({ + date: new Date(date), + values: { usage: resource === "revenue" ? value / 100 : value }, + })), + [usage, resource], + ); + + const allZeroes = useMemo( + () => chartData?.every(({ values }) => values.usage === 0), + [chartData], + ); + + return ( +
+ {chartData && chartData.length > 0 ? ( + !allZeroes ? ( + d.values.usage, + colorClassName: "text-violet-500", + isActive: true, + }, + ]} + tooltipClassName="p-0" + tooltipContent={(d) => { + return ( + <> +

+ {formatDate(d.date)} +

+
+ +
+
+

{resource}

+
+

+ {resource === "revenue" && "$"} + {nFormatter(d.values.usage, { full: true })} +

+ +
+ + ); + }} + > + + + + + + + + `$${nFormatter(v)}` : nFormatter + } + /> + + ) : ( +
+ +
+ ) + ) : ( +
+ {loading ? :

Failed to load usage data

} +
+ )} +
+ ); +} diff --git a/apps/web/lib/swr/use-usage.ts b/apps/web/lib/swr/use-usage.ts new file mode 100644 index 0000000000..02b7b504fb --- /dev/null +++ b/apps/web/lib/swr/use-usage.ts @@ -0,0 +1,30 @@ +import { fetcher } from "@dub/utils"; +import useSWR from "swr"; +import { UsageResponse } from "../types"; +import useWorkspace from "./use-workspace"; + +export default function useUsage({ + resource, +}: { + resource: "links" | "events" | "revenue"; +}) { + const { id } = useWorkspace(); + + const { + data: usage, + error, + isValidating, + } = useSWR( + id && `/api/workspaces/${id}/billing/usage?resource=${resource}`, + fetcher, + { + dedupingInterval: 60000, + }, + ); + + return { + usage, + loading: !usage && !error, + isValidating, + }; +} diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts index 2ea884b144..91149e924a 100644 --- a/apps/web/lib/types.ts +++ b/apps/web/lib/types.ts @@ -10,6 +10,7 @@ import { createLinkBodySchema } from "./zod/schemas/links"; import { createOAuthAppSchema, oAuthAppSchema } from "./zod/schemas/oauth"; import { trackSaleResponseSchema } from "./zod/schemas/sales"; import { tokenSchema } from "./zod/schemas/token"; +import { usageResponse } from "./zod/schemas/usage"; import { createWebhookSchema, webhookEventSchemaTB, @@ -267,3 +268,5 @@ export type TrackCustomerResponse = z.infer; export type TrackLeadResponse = z.infer; export type TrackSaleResponse = z.infer; + +export type UsageResponse = z.infer; diff --git a/apps/web/lib/zod/schemas/usage.ts b/apps/web/lib/zod/schemas/usage.ts new file mode 100644 index 0000000000..e8c8074f17 --- /dev/null +++ b/apps/web/lib/zod/schemas/usage.ts @@ -0,0 +1,10 @@ +import z from "@/lib/zod"; + +export const usageQuerySchema = z.object({ + resource: z.enum(["links", "events", "revenue"]), +}); + +export const usageResponse = z.object({ + date: z.string(), + value: z.number(), +}); diff --git a/apps/web/package.json b/apps/web/package.json index 2f699df886..c28b603c0b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,6 +41,7 @@ "@vercel/functions": "^1.4.2", "@vercel/og": "^0.6.3", "@visx/axis": "^2.14.0", + "@visx/clip-path": "^3.3.0", "@visx/curve": "^3.3.0", "@visx/event": "^2.6.0", "@visx/geo": "^2.10.0", diff --git a/apps/web/ui/charts/areas.tsx b/apps/web/ui/charts/areas.tsx index ece6f5ed97..b468bea1d2 100644 --- a/apps/web/ui/charts/areas.tsx +++ b/apps/web/ui/charts/areas.tsx @@ -13,6 +13,10 @@ export default function Areas({ }) { const { data, series, margin, xScale, yScale, startDate, endDate } = useChartContext(); + + if (!("ticks" in xScale)) + throw new Error("Areas require a time scale (type=area)"); + const { tooltipData } = useChartTooltipContext(); // Data with all values set to zero to animate from diff --git a/apps/web/ui/charts/bars.tsx b/apps/web/ui/charts/bars.tsx new file mode 100644 index 0000000000..6af8d7572b --- /dev/null +++ b/apps/web/ui/charts/bars.tsx @@ -0,0 +1,108 @@ +import { cn } from "@dub/utils"; +import { RectClipPath } from "@visx/clip-path"; +import { LinearGradient } from "@visx/gradient"; +import { Group } from "@visx/group"; +import { BarRounded } from "@visx/shape"; +import { AnimatePresence, motion } from "framer-motion"; +import { useId } from "react"; +import { useChartContext } from "./chart-context"; + +export function Bars({ + seriesStyles, +}: { + seriesStyles?: { + id: string; + gradientClassName?: string; + barClassName?: string; + barFill?: string; + }[]; +}) { + const clipPathId = useId(); + const { + data, + series, + margin, + xScale, + yScale, + width, + height, + startDate, + endDate, + } = useChartContext(); + + if (!("bandwidth" in xScale)) + throw new Error("Bars require a band scale (type=bar)"); + + return ( + + + + {series + .filter(({ isActive }) => isActive) + .map((s) => { + const styles = seriesStyles?.find(({ id }) => id === s.id); + return ( + // Prevent ugly x-scale animations when start/end dates change with unique key + + {/* Bar gradient */} + + + {/* Bars */} + + {data.map((d) => { + const barWidth = xScale.bandwidth(); + const x = xScale(d.date) ?? 0; + const y = yScale(s.valueAccessor(d) ?? 0); + const barHeight = height - y; + const radius = Math.min(barWidth, barHeight) / 2; + return barHeight > 0 ? ( + + ) : null; + })} + + + ); + })} + + + ); +} diff --git a/apps/web/ui/charts/time-series-chart.tsx b/apps/web/ui/charts/time-series-chart.tsx index d157bff073..a9a625215b 100644 --- a/apps/web/ui/charts/time-series-chart.tsx +++ b/apps/web/ui/charts/time-series-chart.tsx @@ -1,7 +1,7 @@ import { cn } from "@dub/utils"; import { Group } from "@visx/group"; import { ParentSize } from "@visx/responsive"; -import { scaleLinear, scaleUtc } from "@visx/scale"; +import { scaleBand, scaleLinear, scaleUtc } from "@visx/scale"; import { Bar, Circle, Line } from "@visx/shape"; import { PropsWithChildren, useMemo, useState } from "react"; import { ChartContext, ChartTooltipContext } from "./chart-context"; @@ -32,6 +32,7 @@ export default function TimeSeriesChart( } function TimeSeriesChartInner({ + type = "area", width: outerWidth, height: outerHeight, children, @@ -45,10 +46,7 @@ function TimeSeriesChartInner({ bottom: 32, left: 4, }, - padding = { - top: 0.1, - bottom: 0.1, - }, + padding: paddingProp, }: { width: number; height: number; @@ -60,6 +58,11 @@ function TimeSeriesChartInner({ left: marginProp.left + (leftAxisMargin ?? 0), }; + const padding = paddingProp ?? { + top: 0.1, + bottom: type === "area" ? 0.1 : 0, + }; + const width = outerWidth - margin.left - margin.right; const height = outerHeight - margin.top - margin.bottom; @@ -81,7 +84,8 @@ function TimeSeriesChartInner({ .filter((v): v is number => v != null); return { - minY: Math.min(...values), + // Start at 0 for bar charts + minY: type === "area" ? Math.min(...values) : Math.min(0, ...values), maxY: Math.max(...values), }; }, [data, series]); @@ -98,14 +102,23 @@ function TimeSeriesChartInner({ nice: true, clamp: true, }), - xScale: scaleUtc({ - domain: [startDate, endDate], - range: [0, width], - }), + xScale: + type === "area" + ? scaleUtc({ + domain: [startDate, endDate], + range: [0, width], + }) + : scaleBand({ + domain: data.map(({ date }) => date), + range: [0, width], + padding: Math.min(0.75, (width / data.length) * 0.02), + align: 0.5, + }), }; - }, [startDate, endDate, minY, maxY, height, width]); + }, [startDate, endDate, minY, maxY, height, width, data.length, type]); const chartContext: ChartContextType = { + type, width, height, data, @@ -146,31 +159,46 @@ function TimeSeriesChartInner({ {children} {/* Tooltip hover line + circle */} - {tooltipData && ( - <> - - - {series - .filter(({ isActive }) => isActive) - .map((s) => ( - - ))} - - )} + {tooltipData && + ("bandwidth" in xScale ? ( + <> + + + ) : ( + <> + + + {series + .filter(({ isActive }) => isActive) + .map((s) => ( + + ))} + + ))} {/* Tooltip hover region */} ({ key={tooltipData.date.toString()} left={(tooltipLeft ?? 0) + margin.left} top={(tooltipTop ?? 0) + margin.top} - offsetLeft={8} + offsetLeft={"bandwidth" in xScale ? xScale.bandwidth() + 8 : 8} offsetTop={12} className="absolute" unstyled={true} diff --git a/apps/web/ui/charts/types.ts b/apps/web/ui/charts/types.ts index 23c5e7de4e..c5ea5c8a8a 100644 --- a/apps/web/ui/charts/types.ts +++ b/apps/web/ui/charts/types.ts @@ -30,6 +30,7 @@ type ChartRequiredProps = { }; type ChartOptionalProps = { + type?: "area" | "bar"; tooltipContent?: (datum: TimeSeriesDatum) => ReactElement | string; tooltipClassName?: string; @@ -61,7 +62,9 @@ export type ChartContext = Required> & { height: number; startDate: Date; endDate: Date; - xScale: ScaleTypeToD3Scale["utc"]; + xScale: + | ScaleTypeToD3Scale["utc"] + | ScaleTypeToD3Scale["band"]; yScale: ScaleTypeToD3Scale["linear"]; minY: number; maxY: number; diff --git a/apps/web/ui/charts/useTooltip.ts b/apps/web/ui/charts/useTooltip.ts index 2cc6a39613..277639a532 100644 --- a/apps/web/ui/charts/useTooltip.ts +++ b/apps/web/ui/charts/useTooltip.ts @@ -49,7 +49,17 @@ export function useTooltip({ ) => { const lp = localPoint(event) || { x: 0 }; const x = lp.x - margin.left; - const x0 = xScale.invert(x); + const x0 = + "invert" in xScale + ? xScale.invert(x) + : (xScale.domain()[ + Math.round((x - xScale.step() * 0.75) / xScale.step()) + ] as Date | undefined); + + if (x0 === undefined) { + visxTooltip.hideTooltip(); + return; + } const index = bisectDate(data, x0, 1); const d0 = data[index - 1]; const d1 = data[index]; diff --git a/apps/web/ui/charts/x-axis.tsx b/apps/web/ui/charts/x-axis.tsx index 71df0e333f..7a634a992e 100644 --- a/apps/web/ui/charts/x-axis.tsx +++ b/apps/web/ui/charts/x-axis.tsx @@ -16,6 +16,11 @@ export type XAxisProps = { */ showGridLines?: boolean; + /** + * Whether to highlight the latest tick label when no other area is hovered + */ + highlightLast?: boolean; + /** * Custom formatting function for tick labels */ @@ -25,6 +30,7 @@ export type XAxisProps = { export default function XAxis({ maxTicks: maxTicksProp, showGridLines = false, + highlightLast = true, tickFormat = (date) => date.toLocaleDateString("en-US", { month: "short", day: "numeric" }), }: XAxisProps) { @@ -67,7 +73,11 @@ export default function XAxis({ textAnchor: idx === 0 ? "start" : idx === length - 1 ? "end" : "middle", fontSize: 12, - fill: (tooltipData ? tooltipData.date === date : idx === length - 1) + fill: ( + tooltipData + ? tooltipData.date === date + : highlightLast && idx === length - 1 + ) ? "#000" : "#00000066", })} diff --git a/packages/blocks/src/empty-state.tsx b/packages/blocks/src/empty-state.tsx index 60a04f6a67..80b1203ac4 100644 --- a/packages/blocks/src/empty-state.tsx +++ b/packages/blocks/src/empty-state.tsx @@ -21,7 +21,7 @@ export function EmptyState({

{title}

{description && ( -

+

{description}{" "} {learnMore && ( + Timeseries data + + +TOKEN "v1_usage_endpoint_read_1647" READ + +NODE day_intervals +SQL > + + % + WITH + toStartOfDay( + toDateTime64({{ DateTime64(start, '2024-09-03 00:00:00.000') }}, 3), + {{ String(timezone, 'UTC') }} + ) AS start, + toStartOfDay( + toDateTime64({{ DateTime64(end, '2024-10-03 00:00:00.000') }}, 3), + {{ String(timezone, 'UTC') }} + ) AS + end + SELECT + arrayJoin( + arrayMap( + x -> toDateTime64(toStartOfDay(toDateTime64(x, 3), {{ String(timezone, 'UTC') }}), 3), + range(toUInt32(start + 86400), toUInt32(end + 86400), + 86400 + ) + ) + ) as interval + + + +NODE workspace_links +SQL > + + % + SELECT link_id + from dub_links_metadata_latest FINAL + WHERE + workspace_id + = {{ + String( + workspaceId, + 'ws_clrei1gld0002vs9mzn93p8ik', + description="The ID of the workspace", + required=True, + ) + }} + AND deleted = 0 + + + +NODE usage_clicks_data +SQL > + + % + SELECT + toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3) AS interval, + uniq(*) as clicks + FROM + dub_click_events_mv + PREWHERE link_id in (SELECT link_id from workspace_links) + WHERE + timestamp >= {{ DateTime(start, '2024-09-03 00:00:00') }} + AND timestamp < {{ DateTime(end, '2024-10-03 00:00:00') }} + GROUP BY interval + ORDER BY interval + + + +NODE usage_leads_data +SQL > + + % + SELECT + toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3) AS interval, + uniq(*) as leads + FROM + dub_lead_events_mv + PREWHERE link_id in (SELECT link_id from workspace_links) + WHERE + timestamp >= {{ DateTime(start, '2024-09-03 00:00:00') }} + AND timestamp < {{ DateTime(end, '2024-10-03 00:00:00') }} + GROUP BY interval + ORDER BY interval + + + +NODE usage_events +SQL > + + SELECT + formatDateTime(di.interval, '%FT%T.000%z') as date, clicks, leads, (clicks + leads) as value + FROM day_intervals as di + LEFT JOIN (SELECT * FROM usage_clicks_data) AS uc ON di.interval = uc.interval + LEFT JOIN (SELECT * FROM usage_leads_data) AS ul ON di.interval = ul.interval + + + +NODE usage_links_data +DESCRIPTION > + undefined + +SQL > + + % + SELECT + toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3) AS interval, + uniq(*) as links + FROM dub_links_metadata_latest FINAL + WHERE + workspace_id + = {{ + String( + workspaceId, + 'ws_clrei1gld0002vs9mzn93p8ik', + description="The ID of the workspace", + required=True, + ) + }} + AND deleted = 0 + AND created_at >= {{ DateTime(start, '2024-09-03 00:00:00') }} + AND created_at < {{ DateTime(end, '2024-10-03 00:00:00') }} + GROUP BY interval + ORDER BY interval + + + +NODE usage_links +SQL > + + % + SELECT formatDateTime(interval, '%FT%T.000%z') as date, links as value + FROM day_intervals + LEFT JOIN usage_links_data USING interval + + +NODE usage_revenue_data +DESCRIPTION > + undefined + +SQL > + + % + SELECT + toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3) AS interval, + sum(amount) as revenue + FROM + dub_sale_events_mv + PREWHERE link_id in (SELECT link_id from workspace_links) + WHERE + timestamp >= {{ DateTime(start, '2024-09-03 00:00:00') }} + AND timestamp < {{ DateTime(end, '2024-10-03 00:00:00') }} + GROUP BY interval + ORDER BY interval + +NODE usage_revenue +SQL > + + % + SELECT formatDateTime(interval, '%FT%T.000%z') as date, revenue as value + FROM day_intervals + LEFT JOIN usage_revenue_data USING interval + +NODE endpoint +SQL > + + % + SELECT * + FROM + {% if resource == 'events' %} usage_events + {% elif resource == 'revenue' %} usage_revenue + {% else %} usage_links + {% end %} + + diff --git a/packages/ui/src/icons/nucleo/circle-dollar.tsx b/packages/ui/src/icons/nucleo/circle-dollar.tsx new file mode 100644 index 0000000000..2ff132cbfa --- /dev/null +++ b/packages/ui/src/icons/nucleo/circle-dollar.tsx @@ -0,0 +1,56 @@ +import { SVGProps } from "react"; + +export function CircleDollar(props: SVGProps) { + return ( + + + + + + + + + ); +} diff --git a/packages/ui/src/icons/nucleo/index.ts b/packages/ui/src/icons/nucleo/index.ts index ce505d8d28..ae0fcfc857 100644 --- a/packages/ui/src/icons/nucleo/index.ts +++ b/packages/ui/src/icons/nucleo/index.ts @@ -20,6 +20,7 @@ export * from "./check2"; export * from "./checkbox-checked-fill"; export * from "./checkbox-unchecked"; export * from "./circle-check"; +export * from "./circle-dollar"; export * from "./circle-dotted"; export * from "./circle-half-dotted-clock"; export * from "./circle-info"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c2a154ab5..e06aafddf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: '@visx/axis': specifier: ^2.14.0 version: 2.14.0(react@18.2.0) + '@visx/clip-path': + specifier: ^3.3.0 + version: 3.3.0(react@18.2.0) '@visx/curve': specifier: ^3.3.0 version: 3.3.0 @@ -10028,6 +10031,16 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@visx/clip-path@3.3.0(react@18.2.0): + resolution: {integrity: sha512-uMuI2M05qZTgUdTSHJGg4VDr2gytGmGyuaC89iByHqNaeMHkrJqQi/cOFAZi4D0dn75p7lVirJijEgDgSpcrMQ==} + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + dependencies: + '@types/react': 18.2.48 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /@visx/curve@2.1.0: resolution: {integrity: sha512-9b6JOnx91gmOQiSPhUOxdsvcnW88fgqfTPKoVgQxidMsD/I3wksixtwo8TR/vtEz2aHzzsEEhlv1qK7Y3yaSDw==} dependencies: