Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The package expects React 19 and Tailwind CSS 4. Install an exact release so a
consumer upgrades deliberately:

```bash
pnpm add --save-exact @nextide/ui@2.1.0
pnpm add --save-exact @nextide/ui@2.2.0
pnpm add --save-dev --save-exact tailwindcss@4.3.1 @tailwindcss/vite@4.3.1
```

Expand Down
33 changes: 31 additions & 2 deletions apps/playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
BriefcaseBusiness,
CalendarClock,
Check,
Circle,
Database,
FileJson,
FileText,
Filter,
Gauge,
Layers3,
LoaderCircle,
PanelRightClose,
PanelRightOpen,
PanelLeft,
Expand All @@ -31,6 +33,7 @@ import {
Sparkles,
ServerCog,
Video,
X,
} from "lucide-react"

import { AppShell } from "@nextide/ui/blocks/app-shell"
Expand Down Expand Up @@ -2976,8 +2979,34 @@ function BlockPreview({ motionScale }: { motionScale: number }) {
expanded: campaignsExpanded,
action: { label: "Create campaign", icon: <Plus /> },
children: [
{ id: "summer-launch", label: "Summer launch" },
{ id: "partner-rollout", label: "Partner rollout" },
{
id: "summer-launch",
label: "Summer launch",
status: "Completed",
tone: "success" as const,
statusIcon: <Check />,
},
{
id: "partner-rollout",
label: "Partner rollout",
status: "Processing",
tone: "processing" as const,
statusIcon: <LoaderCircle className="motion-safe:animate-spin" />,
},
{
id: "creative-review",
label: "Creative review",
status: "Degraded",
tone: "neutral" as const,
statusIcon: <Circle className="fill-current" />,
},
{
id: "failed-sync",
label: "Failed sync",
status: "Failed",
tone: "danger" as const,
statusIcon: <X />,
},
],
}
: item
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ shadcn/ui, Base UI, Tailwind CSS 4, and React 19.
Pin an exact release so upgrades remain deliberate:

```bash
pnpm add --save-exact @nextide/ui@2.1.0
pnpm add --save-exact @nextide/ui@2.2.0
pnpm add --save-dev --save-exact tailwindcss@4.3.1 @tailwindcss/vite@4.3.1
```

Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nextide/ui",
"version": "2.1.0",
"version": "2.2.0",
"description": "Shared Nextide React components and product interface patterns.",
"type": "module",
"license": "UNLICENSED",
Expand Down
51 changes: 37 additions & 14 deletions packages/ui/src/blocks/navigation-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import {
AutocompletePositioner,
} from "@nextide/ui/components/autocomplete"
import { Kbd } from "@nextide/ui/components/kbd"
import { StatusBadge } from "@nextide/ui/components/status-badge"
import {
StatusBadge,
type StatusBadgeIndicator,
} from "@nextide/ui/components/status-badge"
import { Surface } from "@nextide/ui/components/surface"
import { useContainedScroll } from "@nextide/ui/hooks/use-contained-scroll"
import { cn } from "@nextide/ui/lib/utils"
Expand All @@ -45,6 +48,8 @@ type NavigationPanelItem = {
meta?: string
status?: string
tone?: NavigationPanelStatusTone
statusIcon?: React.ReactNode
statusIndicator?: StatusBadgeIndicator
icon?: React.ReactNode
children?: NavigationPanelItem[]
expanded?: boolean
Expand Down Expand Up @@ -870,14 +875,7 @@ function NavigationPanelNav({
{item.meta}
</small>
) : null}
{item.status ? (
<StatusBadge
tone={item.tone ?? "neutral"}
className="relative z-20 px-1.5 py-0.5"
>
{item.status}
</StatusBadge>
) : null}
<NavigationPanelStatus item={item} />
</span>
) : null}
</span>
Expand Down Expand Up @@ -938,7 +936,7 @@ function NavigationPanelNav({
}}
data-slot="navigation-panel-child"
className={cn(
"group grid min-h-11 w-full grid-cols-[2rem_minmax(0,1fr)] items-center rounded-lg border border-transparent pr-2 text-left text-sm transition-colors max-lg:w-auto max-lg:min-w-max",
"group relative grid min-h-11 w-full grid-cols-[2rem_minmax(0,1fr)] items-center rounded-lg border border-transparent pr-8 text-left text-sm transition-colors max-lg:w-auto max-lg:min-w-max",
childActive
? "bg-nextide-tide/[0.07] text-foreground"
: "text-muted-foreground hover:bg-nextide-panel-strong/70 hover:text-foreground"
Expand Down Expand Up @@ -966,14 +964,13 @@ function NavigationPanelNav({
<span className="truncate font-medium">
{child.label}
</span>
{child.meta || child.status ? (
{child.meta ? (
<small className="truncate text-xs text-muted-foreground max-lg:hidden">
{[child.meta, child.status]
.filter(Boolean)
.join(" · ")}
{child.meta}
</small>
) : null}
</span>
<NavigationPanelStatus item={child} iconOnly />
</button>
)
})}
Expand All @@ -999,6 +996,32 @@ function NavigationPanelNav({
)
}

function NavigationPanelStatus({
item,
iconOnly = false,
}: {
item: NavigationPanelItem
iconOnly?: boolean
}) {
if (!item.status) return null

return (
<StatusBadge
title={iconOnly ? item.status : undefined}
tone={item.tone ?? "neutral"}
size="compact"
icon={item.statusIcon}
indicator={item.statusIndicator}
className={cn(
"relative z-20",
iconOnly && "absolute top-2 right-2 size-5 justify-center p-0"
)}
>
{iconOnly ? <span className="sr-only">{item.status}</span> : item.status}
</StatusBadge>
)
}

function NavigationPanelFooter({
collapsed,
drawerCollapsed,
Expand Down
17 changes: 15 additions & 2 deletions packages/ui/src/components/status-badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ function StatusBadge({
tone,
size,
children,
icon,
indicator = "dot",
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof statusBadgeVariants> & {
icon?: React.ReactNode
indicator?: StatusBadgeIndicator
}) {
const pulse = indicator === "pulse"
Expand All @@ -51,11 +53,22 @@ function StatusBadge({
data-slot="status-badge"
data-tone={tone}
data-size={size ?? "default"}
data-indicator={indicator}
data-indicator={icon ? "icon" : indicator}
className={cn(statusBadgeVariants({ tone, size }), className)}
{...props}
>
{indicator !== "none" ? (
{icon ? (
<span
aria-hidden="true"
data-slot="status-badge-icon"
className={cn(
"grid shrink-0 place-items-center [&_svg]:block",
size === "compact" ? "[&_svg]:size-2.5" : "[&_svg]:size-3"
)}
>
{icon}
</span>
) : indicator !== "none" ? (
<span
data-slot="status-badge-dot"
data-pulse={pulse ? "true" : undefined}
Expand Down
37 changes: 37 additions & 0 deletions tests/e2e/qualification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,43 @@ test("navigation branches keep destinations and create actions distinct", async
await expand.click()

const report = navigation.getByRole("button", { name: "Summer launch" })
const completedBadge = report.locator('[data-slot="status-badge"]')
await expect(completedBadge).toHaveAttribute("data-tone", "success")
await expect(completedBadge.getByText("Completed")).toHaveClass("sr-only")
await expect(
completedBadge.locator('[data-slot="status-badge-icon"]')
).toBeVisible()
const [reportBox, completedBadgeBox] = await Promise.all([
report.boundingBox(),
completedBadge.boundingBox(),
])
expect(completedBadgeBox?.x).toBeGreaterThan(
(reportBox?.x ?? 0) + (reportBox?.width ?? 0) / 2
)
expect(completedBadgeBox?.y).toBeLessThan(
(reportBox?.y ?? 0) + (reportBox?.height ?? 0) / 2
)
await expect(
navigation
.getByRole("button", { name: "Creative review" })
.locator('[data-slot="status-badge"]')
).toHaveAttribute("data-tone", "neutral")
await expect(
navigation
.getByRole("button", { name: "Failed sync" })
.locator('[data-slot="status-badge"]')
).toHaveAttribute("data-tone", "danger")
const processingIcon = navigation
.getByRole("button", { name: "Partner rollout" })
.locator('[data-slot="status-badge-icon"] svg')
await expect
.poll(() => processingIcon.evaluate((icon) => getComputedStyle(icon).animationName))
.not.toBe("none")
await page.emulateMedia({ reducedMotion: "reduce" })
await expect
.poll(() => processingIcon.evaluate((icon) => getComputedStyle(icon).animationName))
.toBe("none")
await page.emulateMedia({ reducedMotion: "no-preference" })
await expect(
navigation.getByRole("button", { name: "Collapse Campaigns" })
).toBeVisible()
Expand Down