Skip to content
Draft
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
10 changes: 10 additions & 0 deletions .changeset/layer-card-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@cloudflare/kumo": minor
---

feat(LayerCard): Add `actions` prop to `LayerCard.Secondary` and `LayerCard.Action` component

- `LayerCard.Secondary` now accepts an `actions` prop for header actions (buttons, menus)
- New `LayerCard.Action` component enforces consistent sizing (sm) and shape (square)
- `LayerCard.Action` supports a `render` prop for custom elements (e.g., links)
- Header height is now consistent (`min-h-10`) whether actions are present or not
173 changes: 160 additions & 13 deletions packages/kumo-docs-astro/src/components/demos/LayerCardDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { LayerCard, Button } from "@cloudflare/kumo";
import { ArrowRightIcon } from "@phosphor-icons/react";
"use client";

import { useState } from "react";
import { LayerCard, Badge, DropdownMenu } from "@cloudflare/kumo";
import {
ArrowRightIcon,
PlusIcon,
DotsThreeIcon,
StarIcon,
FunnelIcon,
} from "@phosphor-icons/react";

export function LayerCardDemo() {
return (
<LayerCard>
<LayerCard.Secondary className="flex items-center justify-between">
<div>Next Steps</div>
<Button
variant="ghost"
size="sm"
shape="square"
aria-label="Go to next steps"
>
<ArrowRightIcon size={16} />
</Button>
<LayerCard.Secondary
actions={
<LayerCard.Action icon={ArrowRightIcon} label="Go to next steps" />
}
>
Next Steps
</LayerCard.Secondary>

<LayerCard.Primary>Get started with Kumo</LayerCard.Primary>
</LayerCard>
);
Expand Down Expand Up @@ -52,3 +56,146 @@ export function LayerCardMultipleDemo() {
</div>
);
}

/**
* LayerCard with badge in the header.
*/
export function LayerCardWithBadgeDemo() {
return (
<LayerCard className="w-[300px]">
<LayerCard.Secondary>
Domains
<Badge variant="neutral">3</Badge>
</LayerCard.Secondary>
<LayerCard.Primary>
<p className="text-sm">example.com</p>
</LayerCard.Primary>
</LayerCard>
);
}

/**
* LayerCard with multiple actions.
*/
export function LayerCardWithActionsDemo() {
return (
<LayerCard className="w-[350px]">
<LayerCard.Secondary
actions={
<>
<LayerCard.Action icon={StarIcon} label="Star" />
<LayerCard.Action
icon={PlusIcon}
label="Add domain"
variant="secondary"
/>
<DropdownMenu>
<DropdownMenu.Trigger>
<LayerCard.Action icon={DotsThreeIcon} label="More actions" />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item>Edit</DropdownMenu.Item>
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</>
}
>
Domains
<Badge variant="neutral">1</Badge>
</LayerCard.Secondary>
<LayerCard.Primary>
<div className="flex items-center gap-2">
<span className="text-kumo-success">✓</span>
<span className="text-sm">api.example.com</span>
</div>
</LayerCard.Primary>
</LayerCard>
);
}

/**
* Side by side comparison: with and without actions.
*/
export function LayerCardComparisonDemo() {
return (
<div className="flex gap-4 items-start">
<LayerCard className="w-[250px]">
<LayerCard.Secondary>No Actions</LayerCard.Secondary>
<LayerCard.Primary>
<p className="text-sm">Same header height</p>
</LayerCard.Primary>
</LayerCard>
<LayerCard className="w-[250px]">
<LayerCard.Secondary
actions={<LayerCard.Action icon={PlusIcon} label="Add" />}
>
With Actions
</LayerCard.Secondary>
<LayerCard.Primary>
<p className="text-sm">Consistent alignment</p>
</LayerCard.Primary>
</LayerCard>
</div>
);
}

/**
* LayerCard with view filter using dropdown radio (instead of tabs).
*/
export function LayerCardWithFilterDemo() {
const [view, setView] = useState("all");

const items = {
all: ["api.example.com", "dashboard.example.com", "archived.example.com"],
active: ["api.example.com", "dashboard.example.com"],
archived: ["archived.example.com"],
};

const filtered = items[view as keyof typeof items];

return (
<LayerCard className="w-[350px]">
<LayerCard.Secondary
actions={
<DropdownMenu>
<DropdownMenu.Trigger>
<LayerCard.Action icon={FunnelIcon} label="Filter view" />
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.RadioGroup value={view} onValueChange={setView}>
<DropdownMenu.RadioItem value="all">
All
<DropdownMenu.RadioItemIndicator />
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="active">
Active
<DropdownMenu.RadioItemIndicator />
</DropdownMenu.RadioItem>
<DropdownMenu.RadioItem value="archived">
Archived
<DropdownMenu.RadioItemIndicator />
</DropdownMenu.RadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenu.Content>
</DropdownMenu>
}
>
Domains
<Badge variant="neutral">{filtered.length}</Badge>
</LayerCard.Secondary>
<LayerCard.Primary>
<div className="space-y-1">
{filtered.map((domain) => (
<div key={domain} className="flex items-center gap-2">
<span className="text-kumo-success">✓</span>
<span className="text-sm">{domain}</span>
</div>
))}
</div>
</LayerCard.Primary>
</LayerCard>
);
}
24 changes: 24 additions & 0 deletions packages/kumo-docs-astro/src/pages/components/layer-card.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
LayerCardDemo,
LayerCardBasicDemo,
LayerCardMultipleDemo,
LayerCardComparisonDemo,
LayerCardWithActionsDemo,
LayerCardWithFilterDemo,
} from "~/components/demos/LayerCardDemo";

{/* Hero Demo */}
Expand Down Expand Up @@ -76,6 +79,27 @@ export default function Example() {
<LayerCardMultipleDemo client:visible />
</ComponentExample>

<Heading level={3}>With Actions</Heading>
<p>Use the `actions` prop on `LayerCard.Secondary` to add buttons or menus.</p>
<ComponentExample demo="LayerCardWithActionsDemo">
<LayerCardWithActionsDemo client:visible />
</ComponentExample>

<Heading level={3}>Comparison: With and Without Actions</Heading>
<ComponentExample demo="LayerCardComparisonDemo">
<LayerCardComparisonDemo client:visible />
</ComponentExample>

<Heading level={3}>View Filter (Alternative to Tabs)</Heading>
<p>
Instead of cramming tabs into the header, use a dropdown with radio items to
switch views. This pattern scales better and maintains consistent header
height.
</p>
<ComponentExample demo="LayerCardWithFilterDemo">
<LayerCardWithFilterDemo client:visible />
</ComponentExample>

</ComponentSection>

{/* API Reference */}
Expand Down
7 changes: 6 additions & 1 deletion packages/kumo/src/components/layer-card/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { LayerCard } from "./layer-card";
export {
LayerCard,
type LayerCardSecondaryProps,
type LayerCardActionProps,
type LayerCardActionRenderProps,
} from "./layer-card";
Loading
Loading