Skip to content
Closed
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
56 changes: 55 additions & 1 deletion packages/kumo-docs-astro/src/components/demos/ButtonDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type React from "react";
import { Button } from "@cloudflare/kumo";
import { PlusIcon } from "@phosphor-icons/react";
import { PlusIcon, ArrowUpRight } from "@phosphor-icons/react";

export function ButtonBasicDemo() {
return (
Expand Down Expand Up @@ -85,3 +86,56 @@ export function ButtonDisabledDemo() {
</Button>
);
}

// Polymorphism / Composition demos

export function ButtonAsLinkDemo() {
return (
<Button
render={
<a
href="https://cloudflare.com"
target="_blank"
rel="noopener noreferrer"
/>
}
variant="primary"
>
Visit Cloudflare
<ArrowUpRight className="ml-1" />
</Button>
);
}

export function ButtonAsLinkVariantsDemo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button render={<a href="#primary" />} variant="primary">
Primary Link
</Button>
<Button render={<a href="#secondary" />} variant="secondary">
Secondary Link
</Button>
<Button render={<a href="#ghost" />} variant="ghost">
Ghost Link
</Button>
</div>
);
}

export function ButtonRenderCallbackDemo() {
return (
<Button
loading={false}
render={(
props: React.HTMLAttributes<HTMLAnchorElement>,
state: { loading: boolean },
) => (
<a {...props} href="#callback">
{state.loading ? "Loading..." : "Render Callback"}
</a>
)}
variant="secondary"
/>
);
}
120 changes: 120 additions & 0 deletions packages/kumo-docs-astro/src/pages/components/button.astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
ButtonIconOnlyDemo,
ButtonLoadingDemo,
ButtonDisabledDemo,
ButtonAsLinkDemo,
ButtonAsLinkVariantsDemo,
ButtonRenderCallbackDemo,
} from "../../components/demos/ButtonDemo";
---

Expand Down Expand Up @@ -181,6 +184,123 @@ export default function Example() {
</div>
</ComponentSection>

<!-- Composition / Polymorphism -->
<ComponentSection>
<Heading level={2} class="mb-6">Composition</Heading>
<p class="mb-6 text-kumo-subtle">
The Button component supports polymorphism via the <code class="bg-kumo-recessed px-1.5 py-0.5 rounded text-sm">render</code> prop,
allowing you to render it as a different element (like an anchor) or compose it with routing libraries
like React Router or Next.js Link.
</p>

<!-- As a Link -->
<div class="mb-12">
<Heading level={3}>As a Link</Heading>
<p class="mb-4 text-kumo-subtle">
Render the button as an anchor element for external links or standard navigation.
</p>
<ComponentExample
code={`<Button
render={<a href="https://cloudflare.com" target="_blank" rel="noopener noreferrer" />}
variant="primary"
>
Visit Cloudflare
<ArrowUpRight className="ml-1" />
</Button>`}
>
<ButtonAsLinkDemo client:visible />
</ComponentExample>
</div>

<!-- With Different Variants -->
<div class="mb-12">
<Heading level={3}>Link Variants</Heading>
<p class="mb-4 text-kumo-subtle">
All button variants work seamlessly with the render prop.
</p>
<ComponentExample
code={`<Button render={<a href="#primary" />} variant="primary">
Primary Link
</Button>
<Button render={<a href="#secondary" />} variant="secondary">
Secondary Link
</Button>
<Button render={<a href="#ghost" />} variant="ghost">
Ghost Link
</Button>`}
>
<ButtonAsLinkVariantsDemo client:visible />
</ComponentExample>
</div>

<!-- With React Router -->
<div class="mb-12">
<Heading level={3}>With React Router</Heading>
<p class="mb-4 text-kumo-subtle">
Compose with React Router's <code class="bg-kumo-recessed px-1.5 py-0.5 rounded text-sm">Link</code> for client-side navigation.
</p>
<CodeBlock
code={`import { Link } from 'react-router-dom';

<Button render={<Link to="/dashboard" />} variant="primary">
Go to Dashboard
</Button>`}
lang="tsx"
/>
</div>

<!-- With Next.js Link -->
<div class="mb-12">
<Heading level={3}>With Next.js</Heading>
<p class="mb-4 text-kumo-subtle">
Compose with Next.js <code class="bg-kumo-recessed px-1.5 py-0.5 rounded text-sm">Link</code> for optimized navigation.
</p>
<CodeBlock
code={`import Link from 'next/link';

<Button render={<Link href="/about" />} variant="secondary">
About Us
</Button>`}
lang="tsx"
/>
</div>

<!-- Render Callback -->
<div class="mb-12">
<Heading level={3}>Render Callback</Heading>
<p class="mb-4 text-kumo-subtle">
For advanced use cases, pass a function to access props and state for conditional rendering.
</p>
<ComponentExample
code={`<Button
loading={false}
render={(props, state) => (
<a {...props} href="#callback">
{state.loading ? "Loading..." : "Render Callback"}
</a>
)}
variant="secondary"
/>`}
>
<ButtonRenderCallbackDemo client:visible />
</ComponentExample>
<p class="mt-4 text-sm text-kumo-subtle">
The callback receives <code class="bg-kumo-recessed px-1 py-0.5 rounded">props</code> (to spread on your element)
and <code class="bg-kumo-recessed px-1 py-0.5 rounded">state</code> (containing <code class="bg-kumo-recessed px-1 py-0.5 rounded">loading</code> and <code class="bg-kumo-recessed px-1 py-0.5 rounded">disabled</code>).
</p>
</div>

<!-- Important Notes -->
<div class="mb-12 rounded-lg border border-kumo-line bg-kumo-elevated p-4">
<Heading level={3} class="mb-2 text-base">Important Notes</Heading>
<ul class="list-inside list-disc space-y-2 text-sm text-kumo-subtle">
<li>The custom element must forward refs and spread all received props on its DOM node.</li>
<li>Button styles, loading states, and disabled states are automatically applied to the rendered element.</li>
<li>Use <code class="bg-kumo-recessed px-1 py-0.5 rounded">LinkButton</code> if you need a simpler API for basic link buttons without framework-specific routing.</li>
</ul>
</div>
</ComponentSection>

<!-- API Reference -->
<ComponentSection>
<Heading level={2}>API Reference</Heading>
Expand Down
20 changes: 14 additions & 6 deletions packages/kumo/ai/component-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,14 @@
"Button": {
"name": "Button",
"type": "component",
"description": "Button component",
"description": "Button component with support for composition via the `render` prop. The `render` prop allows you to replace the underlying `<button>` element with a different element or component (like an anchor or React Router Link), while preserving all Button styling and behavior.",
"importPath": "@cloudflare/kumo",
"category": "Action",
"props": {
"children": {
"type": "ReactNode",
"optional": true
},
"className": {
"type": "string",
"optional": true
},
"icon": {
"type": "ReactNode",
"optional": true
Expand Down Expand Up @@ -340,6 +336,15 @@
},
"default": "secondary"
},
"render": {
"type": "ReactNode",
"optional": true,
"description": "Allows you to replace the component’s HTML element with a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render."
},
"className": {
"type": "string",
"optional": true
},
"id": {
"type": "string",
"optional": true
Expand Down Expand Up @@ -386,7 +391,10 @@
"<Button variant=\"secondary\" icon={PlusIcon}>\n Create Worker\n </Button>",
"<div className=\"flex flex-wrap items-center gap-3\">\n <Button variant=\"secondary\" shape=\"square\" icon={PlusIcon} />\n <Button variant=\"secondary\" shape=\"circle\" icon={PlusIcon} />\n </div>",
"<Button variant=\"primary\" loading>\n Loading...\n </Button>",
"<Button variant=\"secondary\" disabled>\n Disabled\n </Button>"
"<Button variant=\"secondary\" disabled>\n Disabled\n </Button>",
"<Button\n render={\n <a\n href=\"https://cloudflare.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n />\n }\n variant=\"primary\"\n >\n Visit Cloudflare\n <ArrowUpRight className=\"ml-1\" />\n </Button>",
"<div className=\"flex flex-wrap items-center gap-3\">\n <Button render={<a href=\"#primary\" />} variant=\"primary\">\n Primary Link\n </Button>\n <Button render={<a href=\"#secondary\" />} variant=\"secondary\">\n Secondary Link\n </Button>\n <Button render={<a href=\"#ghost\" />} variant=\"ghost\">\n Ghost Link\n </Button>\n </div>",
"<Button\n loading={false}\n render={(\n props: React.HTMLAttributes<HTMLAnchorElement>,\n state: { loading: boolean },\n ) => (\n <a {...props} href=\"#callback\">\n {state.loading ? \"Loading...\" : \"Render Callback\"}\n </a>\n )}\n variant=\"secondary\"\n />"
],
"colors": [
"bg-kumo-base",
Expand Down
53 changes: 51 additions & 2 deletions packages/kumo/ai/component-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ Props:

### Button

Button component
Button component with support for composition via the `render` prop. The `render` prop allows you to replace the underlying `<button>` element with a different element or component (like an anchor or React Router Link), while preserving all Button styling and behavior.

**Type:** component

Expand All @@ -226,7 +226,6 @@ Button component
**Props:**

- `children`: ReactNode
- `className`: string
- `icon`: ReactNode
- `loading`: boolean
- `shape`: enum [default: base]
Expand Down Expand Up @@ -263,6 +262,11 @@ Button component
- `not-disabled`: `not-disabled:hover:border-secondary! not-disabled:hover:bg-kumo-control`
- `disabled`: `disabled:bg-kumo-control/50 disabled:!text-kumo-danger/70`
- `data-state`: `data-[state=open]:bg-kumo-control`
- `render`: ReactNode
Allows you to replace the component’s HTML element with a different tag, or compose it with another component.

Accepts a `ReactElement` or a function that returns the element to render.
- `className`: string
- `id`: string
- `lang`: string
- `title`: string
Expand Down Expand Up @@ -318,6 +322,51 @@ Button component
</div>
```

```tsx
<Button
render={
<a
href="https://cloudflare.com"
target="_blank"
rel="noopener noreferrer"
/>
}
variant="primary"
>
Visit Cloudflare
<ArrowUpRight className="ml-1" />
</Button>
```

```tsx
<div className="flex flex-wrap items-center gap-3">
<Button render={<a href="#primary" />} variant="primary">
Primary Link
</Button>
<Button render={<a href="#secondary" />} variant="secondary">
Secondary Link
</Button>
<Button render={<a href="#ghost" />} variant="ghost">
Ghost Link
</Button>
</div>
```

```tsx
<Button
loading={false}
render={(
props: React.HTMLAttributes<HTMLAnchorElement>,
state: { loading: boolean },
) => (
<a {...props} href="#callback">
{state.loading ? "Loading..." : "Render Callback"}
</a>
)}
variant="secondary"
/>
```


---

Expand Down
3 changes: 2 additions & 1 deletion packages/kumo/ai/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,13 @@ export const BreadcrumbsPropsSchema = z.object({

export const ButtonPropsSchema = z.object({
children: z.union([z.string(), z.number(), z.boolean(), z.null(), DynamicValueSchema]).optional(),
className: z.string().optional(),
icon: z.union([z.string(), z.number(), z.boolean(), z.null(), DynamicValueSchema]).optional(),
loading: z.boolean().optional(),
shape: z.enum(["base", "square", "circle"]).optional(),
size: z.enum(["xs", "sm", "base", "lg"]).optional(),
variant: z.enum(["primary", "secondary", "ghost", "destructive", "secondary-destructive", "outline"]).optional(),
render: z.union([z.string(), z.number(), z.boolean(), z.null(), DynamicValueSchema]).optional(), // Allows you to replace the component’s HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
className: z.string().optional(),
id: z.string().optional(),
lang: z.string().optional(),
title: z.string().optional(),
Expand Down
Loading