snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"use client"
|
||||
|
||||
import { Alert, AlertDescription } from "@/registry/bases/radix/ui/alert"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// Agent feature descriptions.
|
||||
const agentFeatures = [
|
||||
{
|
||||
id: "code-reviews",
|
||||
content: (
|
||||
<>
|
||||
<strong>Code reviews</strong> with full codebase context to catch{" "}
|
||||
<strong>hard-to-find</strong> bugs.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "code-suggestions",
|
||||
content: (
|
||||
<>
|
||||
<strong>Code suggestions</strong> validated in sandboxes before you
|
||||
merge.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "root-cause",
|
||||
content: (
|
||||
<>
|
||||
<strong>Root-cause analysis</strong> for production issues with
|
||||
deployment context.{" "}
|
||||
<Badge variant="secondary" className="bg-chart-1 text-chart-5">
|
||||
Requires Observability Plus
|
||||
</Badge>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export function ActivateAgentDialog() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ship faster & safer with Vercel Agent</CardTitle>
|
||||
<CardDescription>
|
||||
Your use is subject to Vercel's{" "}
|
||||
<a href="#">Public Beta Agreement</a> and{" "}
|
||||
<a href="#">AI Product Terms</a>.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<ItemGroup className="gap-0">
|
||||
{agentFeatures.map((feature) => (
|
||||
<Item key={feature.id} size="xs" className="px-0">
|
||||
<ItemMedia variant="icon" className="self-start">
|
||||
<IconPlaceholder
|
||||
lucide="CheckCircle2Icon"
|
||||
tabler="IconCircleCheckFilled"
|
||||
hugeicons="CheckmarkCircle02Icon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiCheckboxCircleLine"
|
||||
className="size-5 fill-primary text-primary-foreground"
|
||||
/>
|
||||
</ItemMedia>
|
||||
<ItemContent>
|
||||
<ItemTitle className="inline leading-relaxed font-normal text-muted-foreground *:[strong]:font-medium *:[strong]:text-foreground">
|
||||
{feature.content}
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Pro teams get $100 in Vercel Agent trial credit for 2 weeks after
|
||||
activation.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end gap-2">
|
||||
<Button variant="outline" className="style-sera:hidden">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="style-sera:w-full">Enable with $100 credits</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import { Area, AreaChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
|
||||
// Monthly visitor data for the area chart.
|
||||
const chartData = [
|
||||
{ month: "January", visitors: 186 },
|
||||
{ month: "February", visitors: 305 },
|
||||
{ month: "March", visitors: 237 },
|
||||
{ month: "April", visitors: 73 },
|
||||
{ month: "May", visitors: 209 },
|
||||
{ month: "June", visitors: 214 },
|
||||
]
|
||||
|
||||
// Chart configuration for the visitors dataset.
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function AnalyticsCard() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-sm data-[size=sm]:pb-0" size="sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Analytics</CardTitle>
|
||||
<CardDescription>
|
||||
418.2K Visitors <Badge>+10%</Badge>
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Button variant="outline" size="sm">
|
||||
View Analytics
|
||||
</Button>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<ChartContainer config={chartConfig} className="aspect-[1/0.35]">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" hideLabel />}
|
||||
defaultIndex={2}
|
||||
/>
|
||||
<Area
|
||||
dataKey="visitors"
|
||||
type="linear"
|
||||
fill="var(--color-visitors)"
|
||||
fillOpacity={0.4}
|
||||
stroke="var(--color-visitors)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
|
||||
export function AnomalyAlert() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-48">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>Get alerted for anomalies</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Automatically monitor your projects for anomalies and get
|
||||
notified.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Upgrade to Observability Plus</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxItem,
|
||||
ComboboxList,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/registry/bases/radix/ui/combobox"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/bases/radix/ui/tooltip"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
// Users available for assignment.
|
||||
const users = [
|
||||
"shadcn",
|
||||
"maxleiter",
|
||||
"evilrabbit",
|
||||
"pranathip",
|
||||
"jorgezreik",
|
||||
"shuding",
|
||||
"rauchg",
|
||||
]
|
||||
|
||||
export function AssignIssue() {
|
||||
const anchor = useComboboxAnchor()
|
||||
return (
|
||||
<Card className="w-full max-w-sm" size="sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle className="text-sm">Assign Issue</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Select users to assign to this issue.
|
||||
</CardDescription>
|
||||
<CardAction>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="outline" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Add user</TooltipContent>
|
||||
</Tooltip>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={users}
|
||||
defaultValue={[users[0]]}
|
||||
>
|
||||
<ComboboxChips ref={anchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((username: string) => (
|
||||
<ComboboxChip key={username}>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{username}
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
placeholder={
|
||||
values.length > 0 ? undefined : "Select a item..."
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No users found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(username: string) => (
|
||||
<ComboboxItem key={username} value={username}>
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{username}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
||||
|
||||
const barChartData = [
|
||||
{ month: "January", desktop: 186, mobile: 80 },
|
||||
{ month: "February", desktop: 305, mobile: 200 },
|
||||
{ month: "March", desktop: 237, mobile: 120 },
|
||||
{ month: "April", desktop: 73, mobile: 190 },
|
||||
{ month: "May", desktop: 209, mobile: 130 },
|
||||
{ month: "June", desktop: 214, mobile: 140 },
|
||||
]
|
||||
|
||||
const barChartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const desktopTotal = barChartData.reduce((sum, item) => sum + item.desktop, 0)
|
||||
const mobileTotal = barChartData.reduce((sum, item) => sum + item.mobile, 0)
|
||||
const desktopDelta = Math.round(
|
||||
((desktopTotal - mobileTotal) / mobileTotal) * 100
|
||||
)
|
||||
const desktopDeltaPrefix = desktopDelta > 0 ? "+" : ""
|
||||
|
||||
export function BarChartCard() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isRounded = !["lyra", "sera"].includes(params.style)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Traffic channels</CardTitle>
|
||||
<CardDescription className="line-clamp-2 text-sm leading-snug">
|
||||
Monthly desktop and mobile traffic for the last six months—compare
|
||||
volume and mix across platforms and devices at a glance.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4 pt-0">
|
||||
<ChartContainer
|
||||
config={barChartConfig}
|
||||
className="max-h-[180px] w-full"
|
||||
>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={barChartData}
|
||||
margin={{ left: 0, right: 0, top: 8, bottom: 0 }}
|
||||
>
|
||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={8}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => String(value).slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dashed" />}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="mobile"
|
||||
fill="var(--color-mobile)"
|
||||
radius={isRounded ? [6, 6, 0, 0] : 0}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div className="grid w-full grid-cols-3 divide-x divide-border/60">
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Desktop
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{desktopTotal.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Mobile
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{mobileTotal.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 text-center">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase">
|
||||
Mix Delta
|
||||
</div>
|
||||
<div className="text-sm font-medium tabular-nums">
|
||||
{desktopDeltaPrefix}
|
||||
{desktopDelta}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View report</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
type AgentState =
|
||||
| "connecting"
|
||||
| "initializing"
|
||||
| "listening"
|
||||
| "speaking"
|
||||
| "thinking"
|
||||
|
||||
type AnimationState = AgentState | undefined
|
||||
|
||||
interface AudioAnalyserOptions {
|
||||
fftSize?: number
|
||||
smoothingTimeConstant?: number
|
||||
minDecibels?: number
|
||||
maxDecibels?: number
|
||||
}
|
||||
|
||||
interface MultiBandVolumeOptions {
|
||||
bands?: number
|
||||
loPass?: number
|
||||
hiPass?: number
|
||||
updateInterval?: number
|
||||
analyserOptions?: AudioAnalyserOptions
|
||||
}
|
||||
|
||||
const multibandDefaults: MultiBandVolumeOptions = {
|
||||
bands: 5,
|
||||
loPass: 100,
|
||||
hiPass: 600,
|
||||
updateInterval: 32,
|
||||
analyserOptions: { fftSize: 2048 },
|
||||
}
|
||||
|
||||
const normalizeDb = (value: number) => {
|
||||
if (value === -Infinity) return 0
|
||||
const minDb = -100
|
||||
const maxDb = -10
|
||||
const db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100
|
||||
return Math.sqrt(db)
|
||||
}
|
||||
|
||||
function createAudioAnalyser(
|
||||
mediaStream: MediaStream,
|
||||
options: AudioAnalyserOptions = {}
|
||||
) {
|
||||
const audioContext = new (window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext)()
|
||||
const source = audioContext.createMediaStreamSource(mediaStream)
|
||||
const analyser = audioContext.createAnalyser()
|
||||
|
||||
if (options.fftSize) analyser.fftSize = options.fftSize
|
||||
if (options.smoothingTimeConstant !== undefined) {
|
||||
analyser.smoothingTimeConstant = options.smoothingTimeConstant
|
||||
}
|
||||
if (options.minDecibels !== undefined)
|
||||
analyser.minDecibels = options.minDecibels
|
||||
if (options.maxDecibels !== undefined)
|
||||
analyser.maxDecibels = options.maxDecibels
|
||||
|
||||
source.connect(analyser)
|
||||
|
||||
const cleanup = () => {
|
||||
source.disconnect()
|
||||
audioContext.close()
|
||||
}
|
||||
|
||||
return { analyser, audioContext, cleanup }
|
||||
}
|
||||
|
||||
function useMultibandVolume(
|
||||
mediaStream?: MediaStream | null,
|
||||
options: MultiBandVolumeOptions = {}
|
||||
) {
|
||||
const opts = useMemo(() => ({ ...multibandDefaults, ...options }), [options])
|
||||
|
||||
const [frequencyBands, setFrequencyBands] = useState<number[]>(() =>
|
||||
new Array(opts.bands).fill(0)
|
||||
)
|
||||
const bandsRef = useRef<number[]>(new Array(opts.bands).fill(0))
|
||||
const frameId = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!mediaStream) {
|
||||
const emptyBands = new Array(opts.bands).fill(0)
|
||||
setTimeout(() => {
|
||||
setFrequencyBands(emptyBands)
|
||||
}, 0)
|
||||
bandsRef.current = emptyBands
|
||||
return
|
||||
}
|
||||
|
||||
const { analyser, cleanup } = createAudioAnalyser(
|
||||
mediaStream,
|
||||
opts.analyserOptions
|
||||
)
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount
|
||||
const dataArray = new Float32Array(bufferLength)
|
||||
const sliceStart = opts.loPass!
|
||||
const sliceEnd = opts.hiPass!
|
||||
const sliceLength = sliceEnd - sliceStart
|
||||
const chunkSize = Math.ceil(sliceLength / opts.bands!)
|
||||
|
||||
let lastUpdate = 0
|
||||
const updateInterval = opts.updateInterval!
|
||||
|
||||
const updateVolume = (timestamp: number) => {
|
||||
if (timestamp - lastUpdate >= updateInterval) {
|
||||
analyser.getFloatFrequencyData(dataArray)
|
||||
|
||||
const chunks = new Array(opts.bands!)
|
||||
|
||||
for (let i = 0; i < opts.bands!; i++) {
|
||||
let sum = 0
|
||||
let count = 0
|
||||
const startIdx = sliceStart + i * chunkSize
|
||||
const endIdx = Math.min(sliceStart + (i + 1) * chunkSize, sliceEnd)
|
||||
|
||||
for (let j = startIdx; j < endIdx; j++) {
|
||||
sum += normalizeDb(dataArray[j])
|
||||
count++
|
||||
}
|
||||
|
||||
chunks[i] = count > 0 ? sum / count : 0
|
||||
}
|
||||
|
||||
let hasChanged = false
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
if (Math.abs(chunks[i] - bandsRef.current[i]) > 0.01) {
|
||||
hasChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
bandsRef.current = chunks
|
||||
setFrequencyBands(chunks)
|
||||
}
|
||||
|
||||
lastUpdate = timestamp
|
||||
}
|
||||
|
||||
frameId.current = requestAnimationFrame(updateVolume)
|
||||
}
|
||||
|
||||
frameId.current = requestAnimationFrame(updateVolume)
|
||||
|
||||
return () => {
|
||||
cleanup()
|
||||
if (frameId.current) {
|
||||
cancelAnimationFrame(frameId.current)
|
||||
}
|
||||
}
|
||||
}, [mediaStream, opts])
|
||||
|
||||
return frequencyBands
|
||||
}
|
||||
|
||||
const generateConnectingSequenceBar = (columns: number): number[][] => {
|
||||
const seq = []
|
||||
for (let x = 0; x < columns; x++) {
|
||||
seq.push([x, columns - 1 - x])
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
const generateListeningSequenceBar = (columns: number): number[][] => {
|
||||
const center = Math.floor(columns / 2)
|
||||
const noIndex = -1
|
||||
return [[center], [noIndex]]
|
||||
}
|
||||
|
||||
const useBarAnimator = (
|
||||
state: AnimationState,
|
||||
columns: number,
|
||||
interval: number
|
||||
): number[] => {
|
||||
const indexRef = useRef(0)
|
||||
const [currentFrame, setCurrentFrame] = useState<number[]>([])
|
||||
const animationFrameId = useRef<number | null>(null)
|
||||
|
||||
const sequence = useMemo(() => {
|
||||
if (state === "thinking" || state === "listening") {
|
||||
return generateListeningSequenceBar(columns)
|
||||
} else if (state === "connecting" || state === "initializing") {
|
||||
return generateConnectingSequenceBar(columns)
|
||||
} else if (state === undefined || state === "speaking") {
|
||||
return [new Array(columns).fill(0).map((_, idx) => idx)]
|
||||
} else {
|
||||
return [[]]
|
||||
}
|
||||
}, [state, columns])
|
||||
|
||||
useEffect(() => {
|
||||
indexRef.current = 0
|
||||
setTimeout(() => {
|
||||
setCurrentFrame(sequence[0] || [])
|
||||
}, 0)
|
||||
}, [sequence])
|
||||
|
||||
useEffect(() => {
|
||||
let startTime = performance.now()
|
||||
|
||||
const animate = (time: DOMHighResTimeStamp) => {
|
||||
const timeElapsed = time - startTime
|
||||
|
||||
if (timeElapsed >= interval) {
|
||||
indexRef.current = (indexRef.current + 1) % sequence.length
|
||||
setCurrentFrame(sequence[indexRef.current] || [])
|
||||
startTime = time
|
||||
}
|
||||
|
||||
animationFrameId.current = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
animationFrameId.current = requestAnimationFrame(animate)
|
||||
|
||||
return () => {
|
||||
if (animationFrameId.current !== null) {
|
||||
cancelAnimationFrame(animationFrameId.current)
|
||||
}
|
||||
}
|
||||
}, [interval, sequence])
|
||||
|
||||
return currentFrame
|
||||
}
|
||||
|
||||
// Memoized Bar component to prevent unnecessary re-renders.
|
||||
const Bar = React.memo<{
|
||||
heightPct: number
|
||||
isHighlighted: boolean
|
||||
state?: AgentState
|
||||
}>(({ heightPct, isHighlighted, state }) => (
|
||||
<div
|
||||
data-highlighted={isHighlighted}
|
||||
className={cn(
|
||||
"max-w-[12px] min-w-[8px] flex-1 transition-all duration-150",
|
||||
"rounded-full",
|
||||
"bg-border data-[highlighted=true]:bg-chart-3",
|
||||
state === "speaking" && "bg-chart-3",
|
||||
state === "thinking" && isHighlighted && "animate-pulse"
|
||||
)}
|
||||
style={{
|
||||
height: `${heightPct}%`,
|
||||
animationDuration: state === "thinking" ? "300ms" : undefined,
|
||||
}}
|
||||
/>
|
||||
))
|
||||
|
||||
Bar.displayName = "Bar"
|
||||
|
||||
const BarVisualizerComponent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
state?: AgentState
|
||||
barCount?: number
|
||||
mediaStream?: MediaStream | null
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
demo?: boolean
|
||||
centerAlign?: boolean
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
>(
|
||||
(
|
||||
{
|
||||
state,
|
||||
barCount = 15,
|
||||
mediaStream,
|
||||
minHeight = 20,
|
||||
maxHeight = 100,
|
||||
demo = false,
|
||||
centerAlign = false,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const realVolumeBands = useMultibandVolume(mediaStream, {
|
||||
bands: barCount,
|
||||
loPass: 100,
|
||||
hiPass: 200,
|
||||
})
|
||||
|
||||
const fakeVolumeBandsRef = useRef<number[]>(new Array(barCount).fill(0.2))
|
||||
const [fakeVolumeBands, setFakeVolumeBands] = useState<number[]>(() =>
|
||||
new Array(barCount).fill(0.2)
|
||||
)
|
||||
const fakeAnimationRef = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (!demo) return
|
||||
|
||||
if (state !== "speaking" && state !== "listening") {
|
||||
const bands = new Array(barCount).fill(0.2)
|
||||
fakeVolumeBandsRef.current = bands
|
||||
setTimeout(() => {
|
||||
setFakeVolumeBands(bands)
|
||||
}, 0)
|
||||
return
|
||||
}
|
||||
|
||||
let lastUpdate = 0
|
||||
const updateInterval = 50
|
||||
const startTime = Date.now() / 1000
|
||||
|
||||
const updateFakeVolume = (timestamp: number) => {
|
||||
if (timestamp - lastUpdate >= updateInterval) {
|
||||
const time = Date.now() / 1000 - startTime
|
||||
const newBands = new Array(barCount)
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const waveOffset = i * 0.5
|
||||
const baseVolume = Math.sin(time * 2 + waveOffset) * 0.3 + 0.5
|
||||
const randomNoise = Math.random() * 0.2
|
||||
newBands[i] = Math.max(0.1, Math.min(1, baseVolume + randomNoise))
|
||||
}
|
||||
|
||||
let hasChanged = false
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
if (Math.abs(newBands[i] - fakeVolumeBandsRef.current[i]) > 0.05) {
|
||||
hasChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanged) {
|
||||
fakeVolumeBandsRef.current = newBands
|
||||
setFakeVolumeBands(newBands)
|
||||
}
|
||||
|
||||
lastUpdate = timestamp
|
||||
}
|
||||
|
||||
fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)
|
||||
}
|
||||
|
||||
fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)
|
||||
|
||||
return () => {
|
||||
if (fakeAnimationRef.current) {
|
||||
cancelAnimationFrame(fakeAnimationRef.current)
|
||||
}
|
||||
}
|
||||
}, [demo, state, barCount])
|
||||
|
||||
const volumeBands = useMemo(
|
||||
() => (demo ? fakeVolumeBands : realVolumeBands),
|
||||
[demo, fakeVolumeBands, realVolumeBands]
|
||||
)
|
||||
|
||||
const highlightedIndices = useBarAnimator(
|
||||
state,
|
||||
barCount,
|
||||
state === "connecting"
|
||||
? 2000 / barCount
|
||||
: state === "thinking"
|
||||
? 150
|
||||
: state === "listening"
|
||||
? 500
|
||||
: 1000
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-state={state}
|
||||
className={cn(
|
||||
"relative flex justify-center gap-1.5",
|
||||
centerAlign ? "items-center" : "items-end",
|
||||
"h-32 w-full overflow-hidden rounded-lg bg-muted p-4",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{volumeBands.map((volume, index) => {
|
||||
const heightPct = Math.min(
|
||||
maxHeight,
|
||||
Math.max(minHeight, volume * 100 + 5)
|
||||
)
|
||||
const isHighlighted = highlightedIndices?.includes(index) ?? false
|
||||
|
||||
return (
|
||||
<Bar
|
||||
key={index}
|
||||
heightPct={heightPct}
|
||||
isHighlighted={isHighlighted}
|
||||
state={state}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarVisualizerComponent.displayName = "BarVisualizerComponent"
|
||||
|
||||
const BarVisualizer = React.memo(
|
||||
BarVisualizerComponent,
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.state === nextProps.state &&
|
||||
prevProps.barCount === nextProps.barCount &&
|
||||
prevProps.mediaStream === nextProps.mediaStream &&
|
||||
prevProps.minHeight === nextProps.minHeight &&
|
||||
prevProps.maxHeight === nextProps.maxHeight &&
|
||||
prevProps.demo === nextProps.demo &&
|
||||
prevProps.centerAlign === nextProps.centerAlign &&
|
||||
prevProps.className === nextProps.className &&
|
||||
JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BarVisualizer.displayName = "BarVisualizer"
|
||||
|
||||
export function BarVisualizerCard() {
|
||||
const [state, setState] = useState<AgentState>("speaking")
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Audio Frequency Visualizer</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time frequency band visualization with animated state transitions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<BarVisualizer
|
||||
state={state}
|
||||
demo={true}
|
||||
barCount={20}
|
||||
minHeight={15}
|
||||
maxHeight={90}
|
||||
className="h-40 max-w-full"
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "connecting" ? "default" : "outline"}
|
||||
onClick={() => setState("connecting")}
|
||||
>
|
||||
Connecting
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "listening" ? "default" : "outline"}
|
||||
onClick={() => setState("listening")}
|
||||
>
|
||||
Listening
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={state === "speaking" ? "default" : "outline"}
|
||||
onClick={() => setState("speaking")}
|
||||
>
|
||||
Speaking
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "@/registry/bases/radix/ui/alert"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/bases/radix/ui/toggle-group"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function BookAppointment() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Book Appointment</CardTitle>
|
||||
<CardDescription>Dr. Sarah Chen · Cardiology</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel>Available on March 18, 2026</FieldLabel>
|
||||
<ToggleGroup type="multiple" spacing={2} defaultValue={["slot-0"]}>
|
||||
{["9:00 AM", "10:30 AM", "11:00 AM", "1:30 PM"].map(
|
||||
(time, index) => (
|
||||
<ToggleGroupItem key={time} value={`slot-${index}`}>
|
||||
{time}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
)}
|
||||
</ToggleGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Alert>
|
||||
<AlertTitle>New patient?</AlertTitle>
|
||||
<AlertDescription>Please arrive 15 minutes early.</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Book Appointment</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { Spinner } from "@/registry/bases/radix/ui/spinner"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/bases/radix/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/bases/radix/ui/tooltip"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function CodespacesCard() {
|
||||
const [isCreatingCodespace, setIsCreatingCodespace] = React.useState(false)
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="codespaces">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="codespaces">Codespaces</TabsTrigger>
|
||||
<TabsTrigger value="local">Local</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="codespaces">
|
||||
<Item size="sm" className="px-1 pt-2">
|
||||
<ItemContent>
|
||||
<ItemTitle>Codespaces</ItemTitle>
|
||||
<ItemDescription>Your workspaces in the cloud</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Create a codespace on main
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<IconPlaceholder
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
/>
|
||||
New with options...
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ContainerIcon"
|
||||
tabler="IconBox"
|
||||
hugeicons="CubeIcon"
|
||||
phosphor="CubeIcon"
|
||||
remixicon="RiBox1Line"
|
||||
/>
|
||||
Configure container
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ZapIcon"
|
||||
tabler="IconBolt"
|
||||
hugeicons="ZapIcon"
|
||||
phosphor="LightningIcon"
|
||||
remixicon="RiFlashlightLine"
|
||||
/>
|
||||
Set up prebuilds
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ServerIcon"
|
||||
tabler="IconServer"
|
||||
hugeicons="ServerStackIcon"
|
||||
phosphor="HardDrivesIcon"
|
||||
remixicon="RiHardDriveLine"
|
||||
/>
|
||||
Manage codespaces
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare2"
|
||||
hugeicons="Share03Icon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
Share deep link
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<IconPlaceholder
|
||||
lucide="InfoIcon"
|
||||
tabler="IconInfoCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="InfoIcon"
|
||||
remixicon="RiInformationLine"
|
||||
/>
|
||||
What are codespaces?
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<Empty className="p-4">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="ServerIcon"
|
||||
tabler="IconServer"
|
||||
hugeicons="ServerStackIcon"
|
||||
phosphor="HardDrivesIcon"
|
||||
remixicon="RiHardDriveLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No codespaces</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
You don't have any codespaces with this repository
|
||||
checked out
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsCreatingCodespace(true)
|
||||
setTimeout(() => {
|
||||
setIsCreatingCodespace(false)
|
||||
}, 2000)
|
||||
}}
|
||||
disabled={isCreatingCodespace}
|
||||
>
|
||||
{isCreatingCodespace ? (
|
||||
<Spinner data-icon="inline-start" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
Create Codespace
|
||||
</Button>
|
||||
<a
|
||||
href="#learn-more"
|
||||
className="text-xs text-muted-foreground underline underline-offset-4"
|
||||
>
|
||||
Learn more about codespaces
|
||||
</a>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<div className="p-1.5 text-xs text-muted-foreground">
|
||||
Codespace usage for this repository is paid for by{" "}
|
||||
<span className="font-medium">shadcn</span>.
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="local">
|
||||
<Item size="sm" className="hidden p-0">
|
||||
<ItemContent>
|
||||
<ItemTitle className="gap-2">
|
||||
<IconPlaceholder
|
||||
lucide="TerminalIcon"
|
||||
tabler="IconTerminal"
|
||||
hugeicons="ComputerTerminal01Icon"
|
||||
phosphor="TerminalIcon"
|
||||
remixicon="RiTerminalBoxLine"
|
||||
className="size-4"
|
||||
/>
|
||||
Clone
|
||||
</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<IconPlaceholder
|
||||
lucide="InfoIcon"
|
||||
tabler="IconInfoCircle"
|
||||
hugeicons="AlertCircleIcon"
|
||||
phosphor="InfoIcon"
|
||||
remixicon="RiInformationLine"
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Which remote URL should I use?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
<Tabs defaultValue="https">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="w-full justify-start border-b *:[button]:flex-0"
|
||||
>
|
||||
<TabsTrigger value="https">HTTPS</TabsTrigger>
|
||||
<TabsTrigger value="ssh">SSH</TabsTrigger>
|
||||
<TabsTrigger value="cli">GitHub CLI</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="rounded-md border bg-muted/30 p-2">
|
||||
<TabsContent value="https">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="https-url" className="sr-only">
|
||||
HTTPS URL
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="https-url"
|
||||
defaultValue="https://github.com/shadcn-ui/ui.git"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Clone using the web URL.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
<TabsContent value="ssh">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="ssh-url" className="sr-only">
|
||||
SSH URL
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="ssh-url"
|
||||
defaultValue="git@github.com:shadcn-ui/ui.git"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Use a password-protected SSH key.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
<TabsContent value="cli">
|
||||
<Field className="gap-2">
|
||||
<FieldLabel htmlFor="cli-command" className="sr-only">
|
||||
CLI Command
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton variant="ghost" size="icon-xs">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="cli-command"
|
||||
defaultValue="gh repo clone shadcn-ui/ui"
|
||||
readOnly
|
||||
/>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Work fast with our official CLI.{" "}
|
||||
<a href="#learn-more">Learn more</a>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
<Separator className="-mx-2 my-2 w-auto!" />
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-1.5"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="MonitorIcon"
|
||||
tabler="IconDeviceDesktop"
|
||||
hugeicons="ComputerIcon"
|
||||
phosphor="MonitorIcon"
|
||||
remixicon="RiComputerLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Open with GitHub Desktop
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="justify-start gap-1.5"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide="DownloadIcon"
|
||||
tabler="IconDownload"
|
||||
hugeicons="DownloadIcon"
|
||||
phosphor="DownloadIcon"
|
||||
remixicon="RiDownloadLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Download ZIP
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
|
||||
export function ContributionsActivity() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Contributions & Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your contributions and activity visibility.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="contributions-activity">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldLegend className="sr-only">
|
||||
Contributions & activity
|
||||
</FieldLegend>
|
||||
<FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="activity-private-profile" />
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="activity-private-profile">
|
||||
Make profile private and hide activity
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enabling this will hide your contributions and activity
|
||||
from your GitHub profile and from social features like
|
||||
followers, stars, feeds, leaderboards and releases.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="contributions-activity" className="style-sera:w-full">
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
// GitHub usernames displayed as contributor avatars.
|
||||
const usernames = [
|
||||
"shadcn",
|
||||
"vercel",
|
||||
"nextjs",
|
||||
"tailwindlabs",
|
||||
"typescript-lang",
|
||||
"eslint",
|
||||
"prettier",
|
||||
"babel",
|
||||
"webpack",
|
||||
"rollup",
|
||||
"parcel",
|
||||
"vite",
|
||||
"react",
|
||||
"vue",
|
||||
"angular",
|
||||
"solid",
|
||||
]
|
||||
|
||||
export function Contributors() {
|
||||
return (
|
||||
<Card className="max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Contributors <Badge variant="secondary">312</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{usernames.map((username) => (
|
||||
<Avatar key={username} className="grayscale">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${username}.png`}
|
||||
alt={username}
|
||||
/>
|
||||
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
|
||||
</Avatar>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<a href="#" className="text-sm underline underline-offset-4">
|
||||
+ 810 contributors
|
||||
</a>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
export function EnvironmentVariables() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Environment Variables</CardTitle>
|
||||
<CardDescription>Production · 8 variables</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
{[
|
||||
{ key: "DATABASE_URL", masked: true },
|
||||
{ key: "NEXT_PUBLIC_API", masked: false },
|
||||
{ key: "STRIPE_SECRET", masked: true },
|
||||
].map((env) => (
|
||||
<div
|
||||
key={env.key}
|
||||
className="flex items-center gap-2 rounded-md px-2.5 py-2 font-mono text-xs ring ring-border"
|
||||
>
|
||||
<span className="font-medium">{env.key}</span>
|
||||
<span className="ml-auto text-muted-foreground">
|
||||
{env.masked ? "••••••••" : "https://api.example.com"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline">Edit</Button>
|
||||
<Button className="ml-auto">Deploy</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/radix/ui/native-select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
|
||||
export function FeedbackForm() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<form id="feedback-form">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="topic">Topic</FieldLabel>
|
||||
<NativeSelect id="topic">
|
||||
<NativeSelectOption value="">Select a topic</NativeSelectOption>
|
||||
<NativeSelectOption value="ai">AI</NativeSelectOption>
|
||||
<NativeSelectOption value="accounts-and-access-controls">
|
||||
Accounts and Access Controls
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="billing">Billing</NativeSelectOption>
|
||||
<NativeSelectOption value="cdn">
|
||||
CDN (Firewall, Caching)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="ci-cd">
|
||||
CI/CD (Builds, Deployments, Environment Variables)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="dashboard-interface">
|
||||
Dashboard Interface (Navigation, UI Issues)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="domains">Domains</NativeSelectOption>
|
||||
<NativeSelectOption value="frameworks">
|
||||
Frameworks
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="marketplace-and-integrations">
|
||||
Marketplace and Integrations
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="observability">
|
||||
Observability (Observability, Logs, Monitoring)
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="storage">Storage</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback">Feedback</FieldLabel>
|
||||
<Textarea
|
||||
id="feedback"
|
||||
placeholder="Your feedback helps us improve..."
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="feedback-form"
|
||||
className="style-sera:w-full"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function FileUpload() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Upload</CardTitle>
|
||||
<CardDescription>Drag and drop or browse</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Empty className="border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<IconPlaceholder
|
||||
lucide="UploadCloudIcon"
|
||||
tabler="IconCloudUpload"
|
||||
hugeicons="CloudUploadIcon"
|
||||
phosphor="CloudArrowUpIcon"
|
||||
remixicon="RiUploadCloudLine"
|
||||
/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Upload files</EmptyTitle>
|
||||
<EmptyDescription>PNG, JPG, PDF up to 10MB</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>Browse Files</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/registry/bases/radix/ui/native-select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
|
||||
export function GithubProfile() {
|
||||
return (
|
||||
<Card className="mx-auto w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile</CardTitle>
|
||||
<CardDescription>Manage your profile information.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form id="profile">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="name">Name</FieldLabel>
|
||||
<Input id="name" placeholder="shadcn" />
|
||||
<FieldDescription>
|
||||
Your name may appear around GitHub where you contribute or are
|
||||
mentioned. You can remove it at any time.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="email">Public Email</FieldLabel>
|
||||
<NativeSelect id="email">
|
||||
<NativeSelectOption value="m@shadcn.com">
|
||||
m@shadcn.com
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption value="m@gmail.com">
|
||||
m@gmail.com
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<FieldDescription>
|
||||
You can manage verified email addresses in your{" "}
|
||||
<a href="#email-settings">email settings</a>.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bio">Bio</FieldLabel>
|
||||
<Textarea
|
||||
id="bio"
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
/>
|
||||
<FieldDescription>
|
||||
You can <span>@mention</span> other users and organizations to
|
||||
link to them.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button form="profile" className="style-sera:w-full">
|
||||
Save Profile
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
"use client"
|
||||
|
||||
import { type IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
const PREVIEW_ICONS = [
|
||||
{
|
||||
lucide: "CopyIcon",
|
||||
tabler: "IconCopy",
|
||||
hugeicons: "Copy01Icon",
|
||||
phosphor: "CopyIcon",
|
||||
remixicon: "RiFileCopyLine",
|
||||
},
|
||||
{
|
||||
lucide: "CircleAlertIcon",
|
||||
tabler: "IconExclamationCircle",
|
||||
hugeicons: "AlertCircleIcon",
|
||||
phosphor: "WarningCircleIcon",
|
||||
remixicon: "RiErrorWarningLine",
|
||||
},
|
||||
{
|
||||
lucide: "TrashIcon",
|
||||
tabler: "IconTrash",
|
||||
hugeicons: "Delete02Icon",
|
||||
phosphor: "TrashIcon",
|
||||
remixicon: "RiDeleteBinLine",
|
||||
},
|
||||
{
|
||||
lucide: "ShareIcon",
|
||||
tabler: "IconShare",
|
||||
hugeicons: "Share03Icon",
|
||||
phosphor: "ShareIcon",
|
||||
remixicon: "RiShareLine",
|
||||
},
|
||||
{
|
||||
lucide: "ShoppingBagIcon",
|
||||
tabler: "IconShoppingBag",
|
||||
hugeicons: "ShoppingBag01Icon",
|
||||
phosphor: "BagIcon",
|
||||
remixicon: "RiShoppingBagLine",
|
||||
},
|
||||
{
|
||||
lucide: "MoreHorizontalIcon",
|
||||
tabler: "IconDots",
|
||||
hugeicons: "MoreHorizontalCircle01Icon",
|
||||
phosphor: "DotsThreeIcon",
|
||||
remixicon: "RiMoreLine",
|
||||
},
|
||||
{
|
||||
lucide: "Loader2Icon",
|
||||
tabler: "IconLoader",
|
||||
hugeicons: "Loading03Icon",
|
||||
phosphor: "SpinnerIcon",
|
||||
remixicon: "RiLoaderLine",
|
||||
},
|
||||
{
|
||||
lucide: "PlusIcon",
|
||||
tabler: "IconPlus",
|
||||
hugeicons: "PlusSignIcon",
|
||||
phosphor: "PlusIcon",
|
||||
remixicon: "RiAddLine",
|
||||
},
|
||||
{
|
||||
lucide: "MinusIcon",
|
||||
tabler: "IconMinus",
|
||||
hugeicons: "MinusSignIcon",
|
||||
phosphor: "MinusIcon",
|
||||
remixicon: "RiSubtractLine",
|
||||
},
|
||||
{
|
||||
lucide: "ArrowLeftIcon",
|
||||
tabler: "IconArrowLeft",
|
||||
hugeicons: "ArrowLeft02Icon",
|
||||
phosphor: "ArrowLeftIcon",
|
||||
remixicon: "RiArrowLeftLine",
|
||||
},
|
||||
{
|
||||
lucide: "ArrowRightIcon",
|
||||
tabler: "IconArrowRight",
|
||||
hugeicons: "ArrowRight02Icon",
|
||||
phosphor: "ArrowRightIcon",
|
||||
remixicon: "RiArrowRightLine",
|
||||
},
|
||||
{
|
||||
lucide: "CheckIcon",
|
||||
tabler: "IconCheck",
|
||||
hugeicons: "Tick02Icon",
|
||||
phosphor: "CheckIcon",
|
||||
remixicon: "RiCheckLine",
|
||||
},
|
||||
{
|
||||
lucide: "ChevronDownIcon",
|
||||
tabler: "IconChevronDown",
|
||||
hugeicons: "ArrowDown01Icon",
|
||||
phosphor: "CaretDownIcon",
|
||||
remixicon: "RiArrowDownSLine",
|
||||
},
|
||||
{
|
||||
lucide: "ChevronRightIcon",
|
||||
tabler: "IconChevronRight",
|
||||
hugeicons: "ArrowRight01Icon",
|
||||
phosphor: "CaretRightIcon",
|
||||
remixicon: "RiArrowRightSLine",
|
||||
},
|
||||
{
|
||||
lucide: "SearchIcon",
|
||||
tabler: "IconSearch",
|
||||
hugeicons: "Search01Icon",
|
||||
phosphor: "MagnifyingGlassIcon",
|
||||
remixicon: "RiSearchLine",
|
||||
},
|
||||
{
|
||||
lucide: "SettingsIcon",
|
||||
tabler: "IconSettings",
|
||||
hugeicons: "Settings01Icon",
|
||||
phosphor: "GearIcon",
|
||||
remixicon: "RiSettingsLine",
|
||||
},
|
||||
] satisfies Record<IconLibraryName, string>[]
|
||||
|
||||
export function IconPreviewGrid() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-8 place-items-center gap-4">
|
||||
{PREVIEW_ICONS.map((icon, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex size-8 items-center justify-center rounded-md ring ring-border style-sera:rounded-none *:[svg]:size-4"
|
||||
>
|
||||
<IconPlaceholder
|
||||
lucide={icon.lucide}
|
||||
tabler={icon.tabler}
|
||||
hugeicons={icon.hugeicons}
|
||||
phosphor={icon.phosphor}
|
||||
remixicon={icon.remixicon}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Separator } from "@/registry/bases/radix/ui/separator"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function InviteTeam() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invite Team</CardTitle>
|
||||
<CardDescription>Add members to your workspace</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{[
|
||||
{ email: "alex@example.com", role: "Editor" },
|
||||
{ email: "sam@example.com", role: "Viewer" },
|
||||
].map((invite) => (
|
||||
<div key={invite.email} className="flex items-center gap-2">
|
||||
<Input defaultValue={invite.email} className="flex-1" />
|
||||
<Select defaultValue={invite.role.toLowerCase()}>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" align="end">
|
||||
<SelectGroup>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="editor">Editor</SelectItem>
|
||||
<SelectItem value="viewer">Viewer</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Button variant="outline">
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
data-icon="inline-start"
|
||||
/>
|
||||
Add another
|
||||
</Button>
|
||||
<Separator />
|
||||
<Field>
|
||||
<FieldLabel htmlFor="invite-link">Or share invite link</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
id="invite-link"
|
||||
defaultValue="https://app.co/invite/x8f2k"
|
||||
readOnly
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton size="icon-xs" aria-label="Copy link">
|
||||
<IconPlaceholder
|
||||
lucide="CopyIcon"
|
||||
tabler="IconCopy"
|
||||
hugeicons="Copy01Icon"
|
||||
phosphor="CopyIcon"
|
||||
remixicon="RiFileCopyLine"
|
||||
/>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">Send Invites</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
115
apps/v4/registry/bases/radix/blocks/preview/cards/invoice.tsx
Normal file
115
apps/v4/registry/bases/radix/blocks/preview/cards/invoice.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/registry/bases/radix/ui/table"
|
||||
|
||||
const INVOICE_ITEMS = [
|
||||
{ item: "Design System License", qty: 1, unitPrice: 499 },
|
||||
{ item: "Priority Support", qty: 12, unitPrice: 99 },
|
||||
{ item: "Custom Components", qty: 3, unitPrice: 250 },
|
||||
] as const
|
||||
|
||||
const subtotal = INVOICE_ITEMS.reduce(
|
||||
(sum, row) => sum + row.qty * row.unitPrice,
|
||||
0
|
||||
)
|
||||
const tax = 0
|
||||
const totalDue = subtotal + tax
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export function Invoice() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Invoice #INV-2847</CardTitle>
|
||||
<CardDescription>Due March 30, 2026</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant="secondary">Pending</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead className="text-right">Qty</TableHead>
|
||||
<TableHead className="text-right">Rate</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{INVOICE_ITEMS.map((row) => (
|
||||
<TableRow key={row.item}>
|
||||
<TableCell>{row.item}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{row.qty}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(row.unitPrice)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(row.qty * row.unitPrice)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(subtotal)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Tax
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">$0.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
Total Due
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{formatCurrency(totalDue)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" size="sm">
|
||||
Download PDF
|
||||
</Button>
|
||||
<Button size="sm" className="ml-auto">
|
||||
Pay Now
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { cn } from "cn"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
const LiveWaveform = ({
|
||||
active = false,
|
||||
processing = false,
|
||||
deviceId,
|
||||
barWidth = 3,
|
||||
barGap = 1,
|
||||
barRadius = 1.5,
|
||||
barColor,
|
||||
fadeEdges = true,
|
||||
fadeWidth = 24,
|
||||
barHeight: baseBarHeight = 4,
|
||||
height = 64,
|
||||
sensitivity = 1,
|
||||
smoothingTimeConstant = 0.8,
|
||||
fftSize = 256,
|
||||
historySize = 60,
|
||||
updateRate = 30,
|
||||
mode = "static",
|
||||
onError,
|
||||
onStreamReady,
|
||||
onStreamEnd,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & {
|
||||
active?: boolean
|
||||
processing?: boolean
|
||||
deviceId?: string
|
||||
barWidth?: number
|
||||
barHeight?: number
|
||||
barGap?: number
|
||||
barRadius?: number
|
||||
barColor?: string
|
||||
fadeEdges?: boolean
|
||||
fadeWidth?: number
|
||||
height?: string | number
|
||||
sensitivity?: number
|
||||
smoothingTimeConstant?: number
|
||||
fftSize?: number
|
||||
historySize?: number
|
||||
updateRate?: number
|
||||
mode?: "scrolling" | "static"
|
||||
onError?: (error: Error) => void
|
||||
onStreamReady?: (stream: MediaStream) => void
|
||||
onStreamEnd?: () => void
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const historyRef = useRef<number[]>([])
|
||||
const analyserRef = useRef<AnalyserNode | null>(null)
|
||||
const audioContextRef = useRef<AudioContext | null>(null)
|
||||
const streamRef = useRef<MediaStream | null>(null)
|
||||
const animationRef = useRef<number>(0)
|
||||
const lastUpdateRef = useRef<number>(0)
|
||||
const processingAnimationRef = useRef<number | null>(null)
|
||||
const lastActiveDataRef = useRef<number[]>([])
|
||||
const transitionProgressRef = useRef(0)
|
||||
const staticBarsRef = useRef<number[]>([])
|
||||
const needsRedrawRef = useRef(true)
|
||||
const gradientCacheRef = useRef<CanvasGradient | null>(null)
|
||||
const lastWidthRef = useRef(0)
|
||||
const heightStyle = typeof height === "number" ? `${height}px` : height
|
||||
|
||||
// Handle canvas resizing.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
const container = containerRef.current
|
||||
if (!canvas || !container) return
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = rect.width * dpr
|
||||
canvas.height = rect.height * dpr
|
||||
canvas.style.width = `${rect.width}px`
|
||||
canvas.style.height = `${rect.height}px`
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (ctx) {
|
||||
ctx.scale(dpr, dpr)
|
||||
}
|
||||
gradientCacheRef.current = null
|
||||
lastWidthRef.current = rect.width
|
||||
needsRedrawRef.current = true
|
||||
})
|
||||
resizeObserver.observe(container)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (processing && !active) {
|
||||
let time = 0
|
||||
transitionProgressRef.current = 0
|
||||
const animateProcessing = () => {
|
||||
time += 0.03
|
||||
transitionProgressRef.current = Math.min(
|
||||
1,
|
||||
transitionProgressRef.current + 0.02
|
||||
)
|
||||
const processingData = []
|
||||
const barCount = Math.floor(
|
||||
(containerRef.current?.getBoundingClientRect().width || 200) /
|
||||
(barWidth + barGap)
|
||||
)
|
||||
if (mode === "static") {
|
||||
const halfCount = Math.floor(barCount / 2)
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const normalizedPosition = (i - halfCount) / halfCount
|
||||
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4
|
||||
const wave1 = Math.sin(time * 1.5 + normalizedPosition * 3) * 0.25
|
||||
const wave2 = Math.sin(time * 0.8 - normalizedPosition * 2) * 0.2
|
||||
const wave3 = Math.cos(time * 2 + normalizedPosition) * 0.15
|
||||
const combinedWave = wave1 + wave2 + wave3
|
||||
const processingValue = (0.2 + combinedWave) * centerWeight
|
||||
let finalValue = processingValue
|
||||
if (
|
||||
lastActiveDataRef.current.length > 0 &&
|
||||
transitionProgressRef.current < 1
|
||||
) {
|
||||
const lastDataIndex = Math.min(
|
||||
i,
|
||||
lastActiveDataRef.current.length - 1
|
||||
)
|
||||
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0
|
||||
finalValue =
|
||||
lastValue * (1 - transitionProgressRef.current) +
|
||||
processingValue * transitionProgressRef.current
|
||||
}
|
||||
processingData.push(Math.max(0.05, Math.min(1, finalValue)))
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const normalizedPosition = (i - barCount / 2) / (barCount / 2)
|
||||
const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4
|
||||
const wave1 = Math.sin(time * 1.5 + i * 0.15) * 0.25
|
||||
const wave2 = Math.sin(time * 0.8 - i * 0.1) * 0.2
|
||||
const wave3 = Math.cos(time * 2 + i * 0.05) * 0.15
|
||||
const combinedWave = wave1 + wave2 + wave3
|
||||
const processingValue = (0.2 + combinedWave) * centerWeight
|
||||
let finalValue = processingValue
|
||||
if (
|
||||
lastActiveDataRef.current.length > 0 &&
|
||||
transitionProgressRef.current < 1
|
||||
) {
|
||||
const lastDataIndex = Math.floor(
|
||||
(i / barCount) * lastActiveDataRef.current.length
|
||||
)
|
||||
const lastValue = lastActiveDataRef.current[lastDataIndex] || 0
|
||||
finalValue =
|
||||
lastValue * (1 - transitionProgressRef.current) +
|
||||
processingValue * transitionProgressRef.current
|
||||
}
|
||||
processingData.push(Math.max(0.05, Math.min(1, finalValue)))
|
||||
}
|
||||
}
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = processingData
|
||||
} else {
|
||||
historyRef.current = processingData
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
processingAnimationRef.current =
|
||||
requestAnimationFrame(animateProcessing)
|
||||
}
|
||||
animateProcessing()
|
||||
return () => {
|
||||
if (processingAnimationRef.current) {
|
||||
cancelAnimationFrame(processingAnimationRef.current)
|
||||
}
|
||||
}
|
||||
} else if (!active && !processing) {
|
||||
const hasData =
|
||||
mode === "static"
|
||||
? staticBarsRef.current.length > 0
|
||||
: historyRef.current.length > 0
|
||||
if (hasData) {
|
||||
let fadeProgress = 0
|
||||
const fadeToIdle = () => {
|
||||
fadeProgress += 0.03
|
||||
if (fadeProgress < 1) {
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = staticBarsRef.current.map(
|
||||
(value) => value * (1 - fadeProgress)
|
||||
)
|
||||
} else {
|
||||
historyRef.current = historyRef.current.map(
|
||||
(value) => value * (1 - fadeProgress)
|
||||
)
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
requestAnimationFrame(fadeToIdle)
|
||||
} else {
|
||||
if (mode === "static") {
|
||||
staticBarsRef.current = []
|
||||
} else {
|
||||
historyRef.current = []
|
||||
}
|
||||
}
|
||||
}
|
||||
fadeToIdle()
|
||||
}
|
||||
}
|
||||
}, [processing, active, barWidth, barGap, mode])
|
||||
|
||||
// Handle microphone setup and teardown.
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop())
|
||||
streamRef.current = null
|
||||
onStreamEnd?.()
|
||||
}
|
||||
if (
|
||||
audioContextRef.current &&
|
||||
audioContextRef.current.state !== "closed"
|
||||
) {
|
||||
audioContextRef.current.close()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
animationRef.current = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
const setupMicrophone = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: deviceId
|
||||
? {
|
||||
deviceId: { exact: deviceId },
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
}
|
||||
: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
})
|
||||
streamRef.current = stream
|
||||
onStreamReady?.(stream)
|
||||
const AudioContextConstructor =
|
||||
window.AudioContext ||
|
||||
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext
|
||||
const audioContext = new AudioContextConstructor()
|
||||
const analyser = audioContext.createAnalyser()
|
||||
analyser.fftSize = fftSize
|
||||
analyser.smoothingTimeConstant = smoothingTimeConstant
|
||||
const source = audioContext.createMediaStreamSource(stream)
|
||||
source.connect(analyser)
|
||||
audioContextRef.current = audioContext
|
||||
analyserRef.current = analyser
|
||||
historyRef.current = []
|
||||
} catch (error) {
|
||||
onError?.(error as Error)
|
||||
}
|
||||
}
|
||||
setupMicrophone()
|
||||
return () => {
|
||||
if (streamRef.current) {
|
||||
streamRef.current.getTracks().forEach((track) => track.stop())
|
||||
streamRef.current = null
|
||||
onStreamEnd?.()
|
||||
}
|
||||
if (
|
||||
audioContextRef.current &&
|
||||
audioContextRef.current.state !== "closed"
|
||||
) {
|
||||
audioContextRef.current.close()
|
||||
audioContextRef.current = null
|
||||
}
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current)
|
||||
animationRef.current = 0
|
||||
}
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
deviceId,
|
||||
fftSize,
|
||||
smoothingTimeConstant,
|
||||
onError,
|
||||
onStreamReady,
|
||||
onStreamEnd,
|
||||
])
|
||||
|
||||
// Animation loop.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
let rafId: number
|
||||
const animate = (currentTime: number) => {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
if (active && currentTime - lastUpdateRef.current > updateRate) {
|
||||
lastUpdateRef.current = currentTime
|
||||
if (analyserRef.current) {
|
||||
const dataArray = new Uint8Array(
|
||||
analyserRef.current.frequencyBinCount
|
||||
)
|
||||
analyserRef.current.getByteFrequencyData(dataArray)
|
||||
if (mode === "static") {
|
||||
const startFreq = Math.floor(dataArray.length * 0.05)
|
||||
const endFreq = Math.floor(dataArray.length * 0.4)
|
||||
const relevantData = dataArray.slice(startFreq, endFreq)
|
||||
const barCount = Math.floor(rect.width / (barWidth + barGap))
|
||||
const halfCount = Math.floor(barCount / 2)
|
||||
const newBars: number[] = []
|
||||
for (let i = halfCount - 1; i >= 0; i--) {
|
||||
const dataIndex = Math.floor(
|
||||
(i / halfCount) * relevantData.length
|
||||
)
|
||||
const value = Math.min(
|
||||
1,
|
||||
(relevantData[dataIndex] / 255) * sensitivity
|
||||
)
|
||||
newBars.push(Math.max(0.05, value))
|
||||
}
|
||||
for (let i = 0; i < halfCount; i++) {
|
||||
const dataIndex = Math.floor(
|
||||
(i / halfCount) * relevantData.length
|
||||
)
|
||||
const value = Math.min(
|
||||
1,
|
||||
(relevantData[dataIndex] / 255) * sensitivity
|
||||
)
|
||||
newBars.push(Math.max(0.05, value))
|
||||
}
|
||||
staticBarsRef.current = newBars
|
||||
lastActiveDataRef.current = newBars
|
||||
} else {
|
||||
let sum = 0
|
||||
const startFreq = Math.floor(dataArray.length * 0.05)
|
||||
const endFreq = Math.floor(dataArray.length * 0.4)
|
||||
const relevantData = dataArray.slice(startFreq, endFreq)
|
||||
for (let i = 0; i < relevantData.length; i++) {
|
||||
sum += relevantData[i]
|
||||
}
|
||||
const average = (sum / relevantData.length / 255) * sensitivity
|
||||
historyRef.current.push(Math.min(1, Math.max(0.05, average)))
|
||||
lastActiveDataRef.current = [...historyRef.current]
|
||||
if (historyRef.current.length > historySize) {
|
||||
historyRef.current.shift()
|
||||
}
|
||||
}
|
||||
needsRedrawRef.current = true
|
||||
}
|
||||
}
|
||||
if (!needsRedrawRef.current && !active) {
|
||||
rafId = requestAnimationFrame(animate)
|
||||
return
|
||||
}
|
||||
needsRedrawRef.current = active
|
||||
ctx.clearRect(0, 0, rect.width, rect.height)
|
||||
const computedBarColor =
|
||||
barColor ||
|
||||
(() => {
|
||||
const style = getComputedStyle(canvas)
|
||||
const color = style.color
|
||||
return color || "#000"
|
||||
})()
|
||||
const step = barWidth + barGap
|
||||
const barCount = Math.floor(rect.width / step)
|
||||
const centerY = rect.height / 2
|
||||
if (mode === "static") {
|
||||
const dataToRender = processing
|
||||
? staticBarsRef.current
|
||||
: active
|
||||
? staticBarsRef.current
|
||||
: staticBarsRef.current.length > 0
|
||||
? staticBarsRef.current
|
||||
: []
|
||||
for (let i = 0; i < barCount && i < dataToRender.length; i++) {
|
||||
const value = dataToRender[i] || 0.1
|
||||
const x = i * step
|
||||
const bh = Math.max(baseBarHeight, value * rect.height * 0.8)
|
||||
const y = centerY - bh / 2
|
||||
ctx.fillStyle = computedBarColor
|
||||
ctx.globalAlpha = 0.4 + value * 0.6
|
||||
if (barRadius > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, barWidth, bh, barRadius)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.fillRect(x, y, barWidth, bh)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < barCount && i < historyRef.current.length; i++) {
|
||||
const dataIndex = historyRef.current.length - 1 - i
|
||||
const value = historyRef.current[dataIndex] || 0.1
|
||||
const x = rect.width - (i + 1) * step
|
||||
const bh = Math.max(baseBarHeight, value * rect.height * 0.8)
|
||||
const y = centerY - bh / 2
|
||||
ctx.fillStyle = computedBarColor
|
||||
ctx.globalAlpha = 0.4 + value * 0.6
|
||||
if (barRadius > 0) {
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x, y, barWidth, bh, barRadius)
|
||||
ctx.fill()
|
||||
} else {
|
||||
ctx.fillRect(x, y, barWidth, bh)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fadeEdges && fadeWidth > 0 && rect.width > 0) {
|
||||
if (!gradientCacheRef.current || lastWidthRef.current !== rect.width) {
|
||||
const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)
|
||||
const fadePercent = Math.min(0.3, fadeWidth / rect.width)
|
||||
gradient.addColorStop(0, "rgba(255,255,255,1)")
|
||||
gradient.addColorStop(fadePercent, "rgba(255,255,255,0)")
|
||||
gradient.addColorStop(1 - fadePercent, "rgba(255,255,255,0)")
|
||||
gradient.addColorStop(1, "rgba(255,255,255,1)")
|
||||
gradientCacheRef.current = gradient
|
||||
lastWidthRef.current = rect.width
|
||||
}
|
||||
ctx.globalCompositeOperation = "destination-out"
|
||||
ctx.fillStyle = gradientCacheRef.current
|
||||
ctx.fillRect(0, 0, rect.width, rect.height)
|
||||
ctx.globalCompositeOperation = "source-over"
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
rafId = requestAnimationFrame(animate)
|
||||
}
|
||||
rafId = requestAnimationFrame(animate)
|
||||
return () => {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId)
|
||||
}
|
||||
}
|
||||
}, [
|
||||
active,
|
||||
processing,
|
||||
sensitivity,
|
||||
updateRate,
|
||||
historySize,
|
||||
barWidth,
|
||||
baseBarHeight,
|
||||
barGap,
|
||||
barRadius,
|
||||
barColor,
|
||||
fadeEdges,
|
||||
fadeWidth,
|
||||
mode,
|
||||
])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative h-full w-full", className)}
|
||||
ref={containerRef}
|
||||
style={{ height: heightStyle }}
|
||||
aria-label={
|
||||
active
|
||||
? "Live audio waveform"
|
||||
: processing
|
||||
? "Processing audio"
|
||||
: "Audio waveform idle"
|
||||
}
|
||||
role="img"
|
||||
{...props}
|
||||
>
|
||||
{!active && !processing && (
|
||||
<div className="absolute top-1/2 right-0 left-0 -translate-y-1/2 border-t-2 border-dotted border-muted-foreground/20" />
|
||||
)}
|
||||
<canvas
|
||||
className="block h-full w-full"
|
||||
ref={canvasRef}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LiveWaveformCard() {
|
||||
const [active, setActive] = useState(false)
|
||||
const [processing, setProcessing] = useState(true)
|
||||
const [mode, setMode] = useState<"static" | "scrolling">("static")
|
||||
|
||||
const handleToggleActive = () => {
|
||||
setActive(!active)
|
||||
if (!active) {
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleProcessing = () => {
|
||||
setProcessing(!processing)
|
||||
if (!processing) {
|
||||
setActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Live Audio Waveform</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time microphone input visualization with audio reactivity
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LiveWaveform
|
||||
active={active}
|
||||
processing={processing}
|
||||
height={80}
|
||||
barWidth={3}
|
||||
barGap={2}
|
||||
mode={mode}
|
||||
fadeEdges={true}
|
||||
barColor="gray"
|
||||
historySize={120}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={active ? "default" : "outline"}
|
||||
onClick={handleToggleActive}
|
||||
>
|
||||
{active ? "Stop" : "Start"} Listening
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={processing ? "default" : "outline"}
|
||||
onClick={handleToggleProcessing}
|
||||
>
|
||||
{processing ? "Stop" : "Start"} Processing
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setMode(mode === "static" ? "scrolling" : "static")}
|
||||
>
|
||||
{mode === "static" ? "Static" : "Scrolling"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/registry/bases/radix/ui/avatar"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
|
||||
export function NoTeamMembers() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-56 border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<AvatarGroup className="grayscale">
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/shadcn.png"
|
||||
alt="@shadcn"
|
||||
/>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Invite your team to collaborate on this project.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button size="sm">Invite Members</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
EmptyDescription,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from "@/registry/bases/radix/ui/empty"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import { Kbd } from "@/registry/bases/radix/ui/kbd"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Empty className="h-72">
|
||||
<EmptyHeader>
|
||||
<EmptyTitle>404 - Not Found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
The page you're looking for doesn't exist. Try searching
|
||||
for what you need below.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<InputGroup className="w-3/4">
|
||||
<InputGroupInput placeholder="Try searching for pages..." />
|
||||
<InputGroupAddon>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
<Kbd>/</Kbd>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<Button variant="link">Go to homepage</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function ObservabilityCard() {
|
||||
return (
|
||||
<Card className="relative w-full max-w-md overflow-hidden pt-0">
|
||||
<div className="absolute inset-0 z-30 aspect-video bg-primary opacity-50 mix-blend-color" />
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
alt="Photo by mymind on Unsplash"
|
||||
title="Photo by mymind on Unsplash"
|
||||
className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale"
|
||||
/>
|
||||
<CardHeader>
|
||||
<CardTitle>Observability Plus is replacing Monitoring</CardTitle>
|
||||
<CardDescription>
|
||||
Switch to the improved way to explore your data, with natural
|
||||
language. Monitoring will no longer be available on the Pro plan in
|
||||
November, 2025
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button>
|
||||
Create Query{" "}
|
||||
<IconPlaceholder
|
||||
lucide="PlusIcon"
|
||||
tabler="IconPlus"
|
||||
hugeicons="PlusSignIcon"
|
||||
phosphor="PlusIcon"
|
||||
remixicon="RiAddLine"
|
||||
data-icon="inline-end"
|
||||
/>
|
||||
</Button>
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
Warning
|
||||
</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { Label, Pie, PieChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
import { Progress } from "@/registry/bases/radix/ui/progress"
|
||||
|
||||
const pieChartData = [
|
||||
{ browser: "chrome", visitors: 275, fill: "var(--color-chrome)" },
|
||||
{ browser: "safari", visitors: 200, fill: "var(--color-safari)" },
|
||||
{ browser: "firefox", visitors: 287, fill: "var(--color-firefox)" },
|
||||
{ browser: "edge", visitors: 173, fill: "var(--color-edge)" },
|
||||
]
|
||||
|
||||
const pieChartConfig = {
|
||||
visitors: {
|
||||
label: "Visitors",
|
||||
},
|
||||
chrome: {
|
||||
label: "Chrome",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
safari: {
|
||||
label: "Safari",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
firefox: {
|
||||
label: "Firefox",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
edge: {
|
||||
label: "Edge",
|
||||
color: "var(--chart-4)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function PieChartCard() {
|
||||
const totalVisitors = pieChartData.reduce(
|
||||
(sum, item) => sum + item.visitors,
|
||||
0
|
||||
)
|
||||
const topBrowser = pieChartData.reduce((max, item) =>
|
||||
item.visitors > max.visitors ? item : max
|
||||
)
|
||||
const topBrowserShare = Math.round(
|
||||
(topBrowser.visitors / totalVisitors) * 100
|
||||
)
|
||||
const topBrowserLabel =
|
||||
pieChartConfig[topBrowser.browser as keyof typeof pieChartConfig]?.label ??
|
||||
"Top"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle>Browser Share</CardTitle>
|
||||
<CardDescription>January - June 2026</CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant="outline">{topBrowserLabel}</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<ChartContainer
|
||||
config={pieChartConfig}
|
||||
className="mx-auto aspect-square max-h-[190px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
dataKey="visitors"
|
||||
nameKey="browser"
|
||||
innerRadius={50}
|
||||
strokeWidth={5}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill="red"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy - 16}
|
||||
className="fill-foreground text-2xl font-bold"
|
||||
>
|
||||
{totalVisitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 4}
|
||||
className="fill-muted-foreground text-xs"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
<ChartLegend
|
||||
content={<ChartLegendContent nameKey="browser" />}
|
||||
className="translate-y-2"
|
||||
/>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
<CardFooter className="flex-col items-stretch gap-2">
|
||||
<div className="flex items-center text-xs">
|
||||
<span className="font-medium">{topBrowserLabel}</span>
|
||||
<span className="ml-auto text-muted-foreground tabular-nums">
|
||||
{topBrowserShare}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={topBrowserShare}
|
||||
className="**:data-[slot=progress-indicator]:bg-chart-3"
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
|
||||
export function ReportBug() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Report Bug</CardTitle>
|
||||
<CardDescription>Help us fix issues faster.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-title">Title</FieldLabel>
|
||||
<Input
|
||||
id="bug-title"
|
||||
placeholder="Brief description of the issue"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-severity">Severity</FieldLabel>
|
||||
<Select defaultValue="medium">
|
||||
<SelectTrigger id="bug-severity" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-component">Component</FieldLabel>
|
||||
<Select defaultValue="dashboard">
|
||||
<SelectTrigger id="bug-component" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="dashboard">Dashboard</SelectItem>
|
||||
<SelectItem value="auth">Auth</SelectItem>
|
||||
<SelectItem value="api">API</SelectItem>
|
||||
<SelectItem value="billing">Billing</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="bug-steps">Steps to reproduce</FieldLabel>
|
||||
<Textarea
|
||||
id="bug-steps"
|
||||
placeholder="1. Go to 2. Click on 3. Observe..."
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="justify-end style-sera:justify-center"
|
||||
>
|
||||
<Button variant="outline" className="style-sera:flex-1">
|
||||
Attach File
|
||||
</Button>
|
||||
<Button className="style-sera:flex-1">Submit Bug</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
|
||||
export function ShippingAddress() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Shipping Address</CardTitle>
|
||||
<CardDescription>Where should we deliver?</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-street">Street address</FieldLabel>
|
||||
<Input id="shipping-street" placeholder="123 Main Street" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-apt">Apt / Suite</FieldLabel>
|
||||
<Input id="shipping-apt" placeholder="Apt 4B" />
|
||||
</Field>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-city">City</FieldLabel>
|
||||
<Input id="shipping-city" placeholder="San Francisco" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-state">State</FieldLabel>
|
||||
<Select defaultValue="CA">
|
||||
<SelectTrigger id="shipping-state" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="CA">California</SelectItem>
|
||||
<SelectItem value="NY">New York</SelectItem>
|
||||
<SelectItem value="TX">Texas</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<FieldGroup className="grid grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-zip">ZIP Code</FieldLabel>
|
||||
<Input id="shipping-zip" placeholder="94102" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="shipping-country">Country</FieldLabel>
|
||||
<Select defaultValue="US">
|
||||
<SelectTrigger id="shipping-country" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="US">United States</SelectItem>
|
||||
<SelectItem value="CA">Canada</SelectItem>
|
||||
<SelectItem value="UK">United Kingdom</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox id="shipping-save" defaultChecked />
|
||||
<FieldLabel htmlFor="shipping-save" className="font-normal">
|
||||
Save as default address
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" className="ml-auto">
|
||||
Save Address
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemGroup,
|
||||
ItemHeader,
|
||||
ItemSeparator,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { Kbd } from "@/registry/bases/radix/ui/kbd"
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "Search", keys: ["⌘", "K"] },
|
||||
{ label: "Quick Actions", keys: ["⌘", "J"] },
|
||||
{ label: "New File", keys: ["⌘", "N"] },
|
||||
{ label: "Save", keys: ["⌘", "S"] },
|
||||
{ label: "Toggle Sidebar", keys: ["⌘", "B"] },
|
||||
] as const
|
||||
|
||||
export function Shortcuts() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-sm font-medium">Shortcuts</div>
|
||||
<ItemGroup className="gap-2 text-muted-foreground" data-size="xs">
|
||||
{shortcuts.map(({ label, keys }, i) => (
|
||||
<React.Fragment key={label}>
|
||||
{i > 0 && <ItemSeparator />}
|
||||
<Item
|
||||
variant="default"
|
||||
size="xs"
|
||||
className="border-0 px-0 py-0"
|
||||
>
|
||||
<ItemHeader>
|
||||
<ItemTitle className="font-normal">{label}</ItemTitle>
|
||||
<ItemActions>
|
||||
<div className="flex gap-1">
|
||||
{keys.map((key) => (
|
||||
<Kbd key={key}>{key}</Kbd>
|
||||
))}
|
||||
</div>
|
||||
</ItemActions>
|
||||
</ItemHeader>
|
||||
</Item>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { Skeleton } from "@/registry/bases/radix/ui/skeleton"
|
||||
|
||||
export function SkeletonLoading() {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="size-10 rounded-full" />
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
<Skeleton className="h-3 w-4/5" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
|
||||
import { Bar, BarChart } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
|
||||
const sleepChartData = [
|
||||
{ hour: "10pm", deep: 0, light: 30, rem: 0 },
|
||||
{ hour: "11pm", deep: 20, light: 10, rem: 0 },
|
||||
{ hour: "12am", deep: 40, light: 0, rem: 10 },
|
||||
{ hour: "1am", deep: 30, light: 5, rem: 15 },
|
||||
{ hour: "2am", deep: 10, light: 20, rem: 30 },
|
||||
{ hour: "3am", deep: 25, light: 10, rem: 20 },
|
||||
{ hour: "4am", deep: 15, light: 25, rem: 10 },
|
||||
{ hour: "5am", deep: 5, light: 35, rem: 15 },
|
||||
{ hour: "6am", deep: 0, light: 20, rem: 25 },
|
||||
]
|
||||
|
||||
const sleepChartConfig = {
|
||||
deep: {
|
||||
label: "Deep",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
light: {
|
||||
label: "Light",
|
||||
color: "var(--chart-2)",
|
||||
},
|
||||
rem: {
|
||||
label: "REM",
|
||||
color: "var(--chart-3)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function SleepReport() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Sleep Report</CardTitle>
|
||||
<CardDescription>Last night · 7h 24m</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<ChartContainer config={sleepChartConfig} className="h-32 w-full">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={sleepChartData}
|
||||
margin={{ left: 0, right: 0, top: 0, bottom: 0 }}
|
||||
barSize={16}
|
||||
>
|
||||
<Bar
|
||||
dataKey="deep"
|
||||
stackId="a"
|
||||
fill="var(--color-deep)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="light"
|
||||
stackId="a"
|
||||
fill="var(--color-light)"
|
||||
radius={0}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="rem"
|
||||
stackId="a"
|
||||
fill="var(--color-rem)"
|
||||
radius={[2, 2, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{[
|
||||
{ label: "Deep", value: "2h 10m" },
|
||||
{ label: "Light", value: "3h 48m" },
|
||||
{ label: "REM", value: "1h 26m" },
|
||||
{ label: "Score", value: "84" },
|
||||
].map((s) => (
|
||||
<div key={s.label} className="text-center">
|
||||
<div className="text-sm font-medium tabular-nums">{s.value}</div>
|
||||
<div className="text-xs text-muted-foreground">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Badge variant="outline">Good</Badge>
|
||||
<Button variant="outline" size="sm" className="ml-auto">
|
||||
Details
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { STYLES } from "@/registry/styles"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function StyleOverview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
|
||||
const currentFontHeading = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.fontHeading),
|
||||
[params.fontHeading]
|
||||
)
|
||||
|
||||
const currentStyle = React.useMemo(
|
||||
() => STYLES.find((style) => style.name === params.style),
|
||||
[params.style]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-6 style-lyra:gap-4 style-mira:gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="cn-font-heading text-2xl font-medium style-lyra:text-lg style-mira:text-lg style-sera:text-lg style-sera:font-semibold style-sera:tracking-wide style-sera:uppercase">
|
||||
{currentStyle?.title} -{" "}
|
||||
{currentFontHeading?.name &&
|
||||
currentFontHeading.name !== currentFont?.name
|
||||
? currentFontHeading.name
|
||||
: currentFont?.name}
|
||||
</div>
|
||||
<div className="line-clamp-2 text-base text-muted-foreground style-lyra:text-sm style-mira:text-sm style-sera:text-sm style-sera:leading-relaxed">
|
||||
Designers love packing quirky glyphs into test phrases. This is a
|
||||
preview of the typography styles.
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
{[
|
||||
"--background",
|
||||
"--foreground",
|
||||
"--primary",
|
||||
"--secondary",
|
||||
"--muted",
|
||||
"--accent",
|
||||
"--border",
|
||||
"--chart-1",
|
||||
"--chart-2",
|
||||
"--chart-3",
|
||||
"--chart-4",
|
||||
"--chart-5",
|
||||
].map((variant) => (
|
||||
<div
|
||||
key={variant}
|
||||
className="flex flex-col flex-wrap items-center gap-2"
|
||||
>
|
||||
<div
|
||||
className="relative aspect-square w-full rounded-lg bg-(--color) after:absolute after:inset-0 after:rounded-lg after:border after:border-border after:mix-blend-darken dark:after:mix-blend-lighten style-sera:rounded-none style-sera:after:rounded-none"
|
||||
style={
|
||||
{
|
||||
"--color": `var(${variant})`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
<div className="hidden max-w-14 truncate font-mono text-[0.60rem] md:block style-lyra:max-w-10 style-mira:max-w-10">
|
||||
{variant}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { Card, CardContent, CardFooter } from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/bases/radix/ui/dialog"
|
||||
import { Field, FieldGroup, FieldLabel } from "@/registry/bases/radix/ui/field"
|
||||
import { Input } from "@/registry/bases/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/bases/radix/ui/select"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function TypographySpecimen() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
|
||||
const currentFontHeading = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.fontHeading),
|
||||
[params.fontHeading]
|
||||
)
|
||||
const headingLabel =
|
||||
currentFontHeading?.name && currentFontHeading.name !== currentFont?.name
|
||||
? currentFontHeading.name
|
||||
: "Inherit"
|
||||
const bodyLabel = currentFont?.name ?? "Default"
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col gap-2">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase">
|
||||
{headingLabel} - {bodyLabel}
|
||||
</div>
|
||||
<p className="cn-font-heading text-2xl font-medium style-sera:text-lg style-sera:font-semibold style-sera:tracking-wider style-sera:uppercase">
|
||||
Designing with rhythm and hierarchy.
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
A strong body style keeps long-form content readable and balances the
|
||||
visual weight of headings.
|
||||
</p>
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||||
Thoughtful spacing and cadence help paragraphs scan quickly without
|
||||
feeling dense.
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full">
|
||||
Share Feedback
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Share Feedback</DialogTitle>
|
||||
<DialogDescription>
|
||||
Let us know how we can improve your experience.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-name">Name</FieldLabel>
|
||||
<Input id="feedback-name" placeholder="Your name" />
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-email">Email</FieldLabel>
|
||||
<Input
|
||||
id="feedback-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-category">Category</FieldLabel>
|
||||
<Select defaultValue="general">
|
||||
<SelectTrigger id="feedback-category" className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="general">General</SelectItem>
|
||||
<SelectItem value="bug">Bug Report</SelectItem>
|
||||
<SelectItem value="feature">Feature Request</SelectItem>
|
||||
<SelectItem value="improvement">Improvement</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="feedback-message">Message</FieldLabel>
|
||||
<Textarea
|
||||
id="feedback-message"
|
||||
placeholder="Tell us what's on your mind..."
|
||||
className="min-h-24 resize-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button>Submit</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/bases/radix/ui/alert-dialog"
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import { ButtonGroup } from "@/registry/bases/radix/ui/button-group"
|
||||
import { Card, CardContent } from "@/registry/bases/radix/ui/card"
|
||||
import { Checkbox } from "@/registry/bases/radix/ui/checkbox"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/bases/radix/ui/dropdown-menu"
|
||||
import { Field, FieldGroup } from "@/registry/bases/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
} from "@/registry/bases/radix/ui/input-group"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/bases/radix/ui/radio-group"
|
||||
import { Slider } from "@/registry/bases/radix/ui/slider"
|
||||
import { Switch } from "@/registry/bases/radix/ui/switch"
|
||||
import { Textarea } from "@/registry/bases/radix/ui/textarea"
|
||||
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||
|
||||
export function UIElements() {
|
||||
const [sliderValue, setSliderValue] = React.useState<number[]>([500])
|
||||
const handleSliderValueChange = React.useCallback(
|
||||
(value: number | readonly number[]) => {
|
||||
if (typeof value === "number") {
|
||||
setSliderValue([value])
|
||||
} else {
|
||||
setSliderValue([...value])
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button>Button</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
</div>
|
||||
<Item variant="outline">
|
||||
<ItemContent>
|
||||
<ItemTitle>Two-factor authentication</ItemTitle>
|
||||
<ItemDescription className="text-pretty xl:hidden 2xl:block">
|
||||
Verify via email or phone number.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions className="hidden md:flex">
|
||||
<Button size="sm" variant="secondary">
|
||||
Enable
|
||||
</Button>
|
||||
</ItemActions>
|
||||
</Item>
|
||||
</div>
|
||||
<Slider
|
||||
value={sliderValue}
|
||||
onValueChange={handleSliderValueChange}
|
||||
max={1000}
|
||||
min={0}
|
||||
step={10}
|
||||
className="flex-1"
|
||||
aria-label="Slider"
|
||||
/>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="Name" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupText>
|
||||
<IconPlaceholder
|
||||
lucide="SearchIcon"
|
||||
tabler="IconSearch"
|
||||
hugeicons="Search01Icon"
|
||||
phosphor="MagnifyingGlassIcon"
|
||||
remixicon="RiSearchLine"
|
||||
/>
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</Field>
|
||||
<Field className="flex-1">
|
||||
<Textarea placeholder="Message" className="resize-none" />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Badge>Badge</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
</div>
|
||||
<RadioGroup defaultValue="apple" className="ml-auto flex w-fit gap-3">
|
||||
<RadioGroupItem value="apple" />
|
||||
<RadioGroupItem value="banana" />
|
||||
</RadioGroup>
|
||||
<div className="flex gap-3">
|
||||
<Checkbox defaultChecked />
|
||||
<Checkbox />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<span className="hidden md:flex style-sera:md:hidden">
|
||||
Alert Dialog
|
||||
</span>
|
||||
<span className="flex md:hidden style-sera:md:flex">
|
||||
Dialog
|
||||
</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Do you want to allow the USB accessory to connect to this
|
||||
device and your data?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Don't allow</AlertDialogCancel>
|
||||
<AlertDialogAction>Allow</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ButtonGroup>
|
||||
<Button variant="outline">
|
||||
<span className="style-sera:hidden">Button Group</span>
|
||||
<span className="hidden style-sera:block">Group</span>
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<IconPlaceholder
|
||||
lucide="ChevronUpIcon"
|
||||
tabler="IconChevronUp"
|
||||
hugeicons="ArrowUp01Icon"
|
||||
phosphor="CaretUpIcon"
|
||||
remixicon="RiArrowUpSLine"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="top" className="w-fit">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Quick Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Mute Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
|
||||
<DropdownMenuItem>Block User</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>Conversation</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Share Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Copy Conversation</DropdownMenuItem>
|
||||
<DropdownMenuItem>Report Conversation</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem variant="destructive">
|
||||
Delete Conversation
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ButtonGroup>
|
||||
<Switch defaultChecked className="ml-auto" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
136
apps/v4/registry/bases/radix/blocks/preview/cards/usage-card.tsx
Normal file
136
apps/v4/registry/bases/radix/blocks/preview/cards/usage-card.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
|
||||
// Usage items with percentage and value.
|
||||
const items = [
|
||||
{
|
||||
name: "Edge Requests",
|
||||
value: "$1.83K",
|
||||
percentage: 67.34,
|
||||
},
|
||||
{
|
||||
name: "Fast Data Transfer",
|
||||
percentage: 52.18,
|
||||
value: "$952.51",
|
||||
},
|
||||
{
|
||||
name: "Monitoring data points",
|
||||
percentage: 89.42,
|
||||
value: "$901.20",
|
||||
},
|
||||
{
|
||||
name: "Web Analytics Events",
|
||||
percentage: 45.67,
|
||||
value: "$603.71",
|
||||
},
|
||||
{
|
||||
name: "ISR Writes",
|
||||
percentage: 26.23,
|
||||
value: "524.52K / 2M",
|
||||
},
|
||||
{
|
||||
name: "Function Duration",
|
||||
percentage: 5.11,
|
||||
value: "5.11 GB Hrs / 1K GB Hrs",
|
||||
},
|
||||
]
|
||||
|
||||
// Circular gauge SVG component for displaying a percentage.
|
||||
function CircularGauge({ percentage }: { percentage: number }) {
|
||||
const normalizedPercentage = Math.min(Math.max(percentage, 0), 100)
|
||||
const circumference = 2 * Math.PI * 42.5
|
||||
const strokePercent = (normalizedPercentage / 100) * circumference
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
height="16"
|
||||
width="16"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 100 100"
|
||||
className="-rotate-90"
|
||||
>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42.5"
|
||||
strokeWidth="12"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="opacity-20"
|
||||
stroke="currentColor"
|
||||
style={{
|
||||
strokeDasharray: `${circumference} ${circumference}`,
|
||||
}}
|
||||
/>
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42.5"
|
||||
strokeWidth="12"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke="currentColor"
|
||||
className="transition-all duration-300"
|
||||
style={{
|
||||
strokeDasharray: `${strokePercent} ${circumference}`,
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function UsageCard() {
|
||||
return (
|
||||
<Card className="w-full max-w-sm gap-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="px-1 text-sm">
|
||||
5 days remaining in cycle
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ItemGroup className="gap-0">
|
||||
{items.map((item) => (
|
||||
<Item
|
||||
key={item.name}
|
||||
size="xs"
|
||||
className="px-0 group-hover/item-group:bg-transparent"
|
||||
asChild
|
||||
>
|
||||
<a href="#">
|
||||
<ItemMedia variant="icon" className="text-primary">
|
||||
<CircularGauge percentage={item.percentage} />
|
||||
</ItemMedia>
|
||||
<ItemContent className="inline-block truncate">
|
||||
<ItemTitle className="inline">{item.name}</ItemTitle>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<span className="font-mono text-xs font-medium text-muted-foreground tabular-nums">
|
||||
{item.value}
|
||||
</span>
|
||||
</ItemActions>
|
||||
</a>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client"
|
||||
|
||||
import { Area, AreaChart, XAxis } from "recharts"
|
||||
|
||||
import { Badge } from "@/registry/bases/radix/ui/badge"
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
type ChartConfig,
|
||||
} from "@/registry/bases/radix/ui/chart"
|
||||
|
||||
const areaChartData = [
|
||||
{ month: "January", desktop: 186 },
|
||||
{ month: "February", desktop: 305 },
|
||||
{ month: "March", desktop: 237 },
|
||||
{ month: "April", desktop: 73 },
|
||||
{ month: "May", desktop: 209 },
|
||||
{ month: "June", desktop: 214 },
|
||||
]
|
||||
|
||||
const areaChartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "var(--chart-1)",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
const latestVisitors = areaChartData[areaChartData.length - 1]?.desktop ?? 0
|
||||
const previousVisitors =
|
||||
areaChartData[areaChartData.length - 2]?.desktop ?? latestVisitors
|
||||
const trendPercent =
|
||||
previousVisitors === 0
|
||||
? 0
|
||||
: Math.round(((latestVisitors - previousVisitors) / previousVisitors) * 100)
|
||||
const trendPrefix = trendPercent > 0 ? "+" : ""
|
||||
|
||||
export function Visitors() {
|
||||
return (
|
||||
<Card className="pb-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Visitors</CardTitle>
|
||||
<CardDescription>Last 6 months </CardDescription>
|
||||
<CardAction>
|
||||
<Badge variant={trendPercent >= 0 ? "secondary" : "destructive"}>
|
||||
{trendPrefix}
|
||||
{trendPercent}% vs last month
|
||||
</Badge>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
<ChartContainer config={areaChartConfig} className="h-48 w-full">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={areaChartData}
|
||||
margin={{ left: 0, right: 0, top: 6, bottom: 0 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
hide
|
||||
axisLine={false}
|
||||
tickMargin={6}
|
||||
tickFormatter={(value) => String(value).slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
/>
|
||||
<Area
|
||||
dataKey="desktop"
|
||||
type="natural"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.15}
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/registry/bases/radix/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/bases/radix/ui/card"
|
||||
|
||||
const FITNESS_WEEKLY_LOAD = [
|
||||
{ day: "M", load: 84 },
|
||||
{ day: "T", load: 52 },
|
||||
{ day: "W", load: 73 },
|
||||
{ day: "T", load: 66 },
|
||||
{ day: "F", load: 91 },
|
||||
{ day: "S", load: 48 },
|
||||
{ day: "S", load: 61 },
|
||||
]
|
||||
|
||||
export function WeeklyFitnessSummary() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Weekly Fitness Summary</CardTitle>
|
||||
<CardDescription>Calories and workout load by day</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-7 gap-1.5">
|
||||
{FITNESS_WEEKLY_LOAD.map((day, index) => (
|
||||
<div
|
||||
key={`${day.day}-${index}`}
|
||||
className="rounded-md p-1.5 text-center ring ring-border"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">{day.day}</div>
|
||||
<div className="relative mt-1 h-16 overflow-hidden rounded-sm bg-muted">
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 rounded-sm bg-chart-3"
|
||||
style={
|
||||
{
|
||||
height: `${day.load}%`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button className="w-full">View details</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user