Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions .changeset/kumo-fixes-for-matt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@cloudflare/kumo": minor
"@cloudflare/kumo-docs-astro": minor
---

fix(cli): resolve broken doc/docs/ls commands by fixing registry path from catalog/ to ai/
fix(dialog): wrap sub-components to isolate @base-ui/react type references from downstream consumers
fix(label): render as `<label>` element with htmlFor support instead of `<span>`
feat(input): add Textarea alias for InputArea
feat(toast): add ToastProvider alias for Toasty
feat(button): require aria-label on icon-only buttons (shape="square" | "circle") via discriminated union
fix(docs): add Tailwind 4 @source directive to usage example, add confirmation dialog recipe, update Select basic example, document icon-only button aria-label pattern
16 changes: 14 additions & 2 deletions packages/kumo-docs-astro/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { resolve } from "path";
import { fileURLToPath } from "url";
import { kumoColorsPlugin } from "./src/lib/vite-plugin-kumo-colors.js";
import { kumoRegistryPlugin } from "./src/lib/vite-plugin-kumo-registry.js";
import { kumoHmrPlugin } from "./src/lib/vite-plugin-kumo-hmr.js";

const __dirname = fileURLToPath(new URL(".", import.meta.url));

Expand Down Expand Up @@ -58,12 +59,23 @@ function getBuildInfo() {

const buildInfo = getBuildInfo();

// Detect dev mode: `astro dev` sets this in process.argv
const isDev = process.argv.includes("dev");

// https://astro.build/config
export default defineConfig({
integrations: [react()],
vite: {
// @ts-expect-error - Vite version mismatch between Astro and @tailwindcss/vite
plugins: [tailwindcss(), kumoColorsPlugin(), kumoRegistryPlugin()],
plugins: [
// @ts-expect-error - Vite version mismatch between Astro and @tailwindcss/vite
tailwindcss(),
kumoColorsPlugin(),
kumoRegistryPlugin(),
// In dev mode, resolve @cloudflare/kumo imports to raw source files
// for instant HMR. In production builds, the normal package.json
// exports (dist/) are used — preserving the real consumer experience.
...(isDev ? [kumoHmrPlugin()] : []),
],

define: {
__KUMO_VERSION__: JSON.stringify(buildInfo.kumoVersion),
Expand Down
14 changes: 12 additions & 2 deletions packages/kumo-docs-astro/src/components/demos/ButtonDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,18 @@ export function ButtonWithIconDemo() {
export function ButtonIconOnlyDemo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Button variant="secondary" shape="square" icon={PlusIcon} />
<Button variant="secondary" shape="circle" icon={PlusIcon} />
<Button
variant="secondary"
shape="square"
icon={PlusIcon}
aria-label="Add item"
/>
<Button
variant="secondary"
shape="circle"
icon={PlusIcon}
aria-label="Add item"
/>
</div>
);
}
Expand Down
46 changes: 45 additions & 1 deletion packages/kumo-docs-astro/src/components/demos/DialogDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dialog, Button } from "@cloudflare/kumo";
import { X } from "@phosphor-icons/react";
import { Warning, X } from "@phosphor-icons/react";

export function DialogBasicDemo() {
return (
Expand Down Expand Up @@ -76,3 +76,47 @@ export function DialogWithActionsDemo() {
</Dialog.Root>
);
}

export function DialogConfirmationDemo() {
return (
<Dialog.Root disablePointerDismissal>
<Dialog.Trigger
render={(p) => (
<Button {...p} variant="destructive">
Delete Project
</Button>
)}
/>
<Dialog className="p-8">
<div className="mb-4 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-kumo-danger/20">
<Warning size={20} className="text-kumo-danger" />
</div>
<Dialog.Title className="text-xl font-semibold">
Delete Project?
</Dialog.Title>
</div>
<Dialog.Description className="text-kumo-subtle">
This action cannot be undone. This will permanently delete the project
and all associated data.
</Dialog.Description>
<div className="mt-8 flex justify-end gap-2">
<Dialog.Close
render={(props) => (
<Button variant="secondary" {...props}>
Cancel
</Button>
)}
/>
<Dialog.Close
render={(props) => (
<Button variant="destructive" {...props}>
Delete
</Button>
)}
/>
</div>
</Dialog>
</Dialog.Root>
);
}
12 changes: 6 additions & 6 deletions packages/kumo-docs-astro/src/components/demos/SelectDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { useState, useEffect } from "react";
import { Select, Text } from "@cloudflare/kumo";

export function SelectBasicDemo() {
const [value, setValue] = useState("Apple");
const [value, setValue] = useState("apple");

return (
<Select
className="w-[200px]"
value={value}
onValueChange={(v) => setValue(v ?? "Apple")}
placeholder="Please select"
onValueChange={(v) => setValue(v ?? "apple")}
items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
>
<Select.Option value="Apple">Apple</Select.Option>
<Select.Option value="Banana">Banana</Select.Option>
<Select.Option value="Cherry">Cherry</Select.Option>
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="banana">Banana</Select.Option>
<Select.Option value="cherry">Cherry</Select.Option>
</Select>
);
}
Expand Down
118 changes: 118 additions & 0 deletions packages/kumo-docs-astro/src/lib/vite-plugin-kumo-hmr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

// Resolve once — points at the sibling kumo package root
const kumoRoot = resolve(__dirname, "../../../kumo");
const kumoSrc = resolve(kumoRoot, "src");

/**
* Map every `@cloudflare/kumo` sub-path export to its source equivalent.
*
* In dev mode Vite will resolve these to the raw .ts/.tsx source files,
* which means file-watcher-based HMR works instantly — no rebuild of
* the kumo package required.
*
* In production builds (astro build) this plugin is NOT loaded, so the
* normal package.json `exports` field is used (dist/), which validates
* the real consumer experience.
*/
const aliases: Record<string, string> = {
// Main barrel — resolves to source index.ts
"@cloudflare/kumo": resolve(kumoSrc, "index.ts"),

// CSS styles — resolve to source CSS
"@cloudflare/kumo/styles/tailwind": resolve(kumoSrc, "styles/kumo.css"),
"@cloudflare/kumo/styles/standalone": resolve(
kumoSrc,
"styles/kumo-standalone.css",
),
"@cloudflare/kumo/styles": resolve(kumoSrc, "styles/kumo.css"),

// JSON registry — these live outside src/ and are NOT built, so same
// path works in dev and prod. We alias anyway so Vite can resolve
// the workspace:* link correctly and watch the file.
"@cloudflare/kumo/ai/component-registry.json": resolve(
kumoRoot,
"ai/component-registry.json",
),
};

/**
* Vite plugin that rewires `@cloudflare/kumo` imports to the raw source
* files of the sibling package during `astro dev`.
*
* **Why not just use `resolve.alias`?**
* `resolve.alias` is a simple prefix match — it can't distinguish
* `@cloudflare/kumo` from `@cloudflare/kumo-figma` without a trailing
* slash, and it can't handle the overlapping sub-path exports cleanly.
* A plugin gives us exact-match control.
*/
export function kumoHmrPlugin() {
return {
name: "vite-plugin-kumo-hmr",
enforce: "pre" as const,

resolveId(source: string) {
// Exact match first (most imports)
if (aliases[source]) {
return aliases[source];
}

// Sub-path component imports: @cloudflare/kumo/components/button
// → packages/kumo/src/components/button/index.ts
if (source.startsWith("@cloudflare/kumo/components/")) {
const componentName = source.replace(
"@cloudflare/kumo/components/",
"",
);
return resolve(kumoSrc, `components/${componentName}/index.ts`);
}

// Primitives: @cloudflare/kumo/primitives/dialog
// → packages/kumo/src/primitives/dialog.ts
if (source.startsWith("@cloudflare/kumo/primitives/")) {
const primitiveName = source.replace(
"@cloudflare/kumo/primitives/",
"",
);
return resolve(kumoSrc, `primitives/${primitiveName}.ts`);
}
if (source === "@cloudflare/kumo/primitives") {
return resolve(kumoSrc, "primitives/index.ts");
}

// Utils barrel
if (source === "@cloudflare/kumo/utils") {
return resolve(kumoSrc, "utils/index.ts");
}

// Catalog barrel
if (source === "@cloudflare/kumo/catalog") {
return resolve(kumoSrc, "catalog/index.ts");
}

// Registry barrel
if (source === "@cloudflare/kumo/registry") {
return resolve(kumoSrc, "registry/index.ts");
}

// Catch-all for any other @cloudflare/kumo/styles/* CSS imports
if (source.startsWith("@cloudflare/kumo/styles/")) {
const styleName = source.replace("@cloudflare/kumo/styles/", "");
return resolve(kumoSrc, `styles/${styleName}.css`);
}

return undefined;
},

configResolved(config: { server: { fs: { allow: string[] } } }) {
// Append kumo source to the existing allow list rather than replacing it.
// Using config() would shallow-merge and override Astro/Vite defaults.
if (config.server?.fs?.allow) {
config.server.fs.allow.push(kumoRoot);
}
},
};
}
11 changes: 9 additions & 2 deletions packages/kumo-docs-astro/src/pages/components/button.astro
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,16 @@ export default function Example() {
<!-- Icon Only -->
<div class="mb-12">
<Heading level={3}>Icon Only</Heading>
<p class="mb-3 text-sm text-kumo-strong">
For icon-only buttons, use <code class="rounded bg-kumo-control px-1 py-0.5 text-xs">shape="square"</code> or
<code class="rounded bg-kumo-control px-1 py-0.5 text-xs">shape="circle"</code> with the
<code class="rounded bg-kumo-control px-1 py-0.5 text-xs">icon</code> prop.
<strong>Always include <code class="rounded bg-kumo-control px-1 py-0.5 text-xs">aria-label</code></strong> for accessibility —
without visible text, screen readers need the label to convey the button's purpose.
</p>
<ComponentExample
code={`<Button variant="secondary" shape="square" icon={PlusIcon} />
<Button variant="secondary" shape="circle" icon={PlusIcon} />`}
code={`<Button variant="secondary" shape="square" icon={PlusIcon} aria-label="Add item" />
<Button variant="secondary" shape="circle" icon={PlusIcon} aria-label="Add item" />`}
>
<ButtonIconOnlyDemo client:visible />
</ComponentExample>
Expand Down
46 changes: 45 additions & 1 deletion packages/kumo-docs-astro/src/pages/components/dialog.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ComponentSection from "../../components/docs/ComponentSection.astro";
import ComponentExample from "../../components/docs/ComponentExample.astro";
import CodeBlock from "../../components/docs/CodeBlock.astro";
import PropsTable from "../../components/docs/PropsTable.astro";
import { DialogBasicDemo, DialogWithActionsDemo } from "../../components/demos/DialogDemo";
import { DialogBasicDemo, DialogWithActionsDemo, DialogConfirmationDemo } from "../../components/demos/DialogDemo";
---

<DocLayout
Expand Down Expand Up @@ -123,6 +123,50 @@ export default function Example() {
</ComponentExample>
</div>

<div>
<Heading level={3}>Confirmation Dialog (Alert)</Heading>
<p class="mb-3 text-sm text-kumo-strong">
For confirmation dialogs that should not be dismissed by clicking outside (similar to an AlertDialog),
use <code class="rounded bg-kumo-control px-1 py-0.5 text-xs">disablePointerDismissal</code> on <code class="rounded bg-kumo-control px-1 py-0.5 text-xs">Dialog.Root</code>.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does disablePointerDismissal also disable escape key dismissal?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ascorbic From my testing... no, disablePointerDismissal does not disable Escape key dismissal.

That actually seems like the correct behavior if we want to align with alertdialog patterns. Pressing Esc should be equivalent to activating the Cancel / Discard action and should return focus to the trigger, per the ARIA Authoring Practices:
https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/examples/alertdialog/

Also, the prop name explicitly refers to pointer dismissal, so it makes sense that it only blocks outside pointer interactions, not keyboard dismissal via Esc.

This ensures the user must explicitly choose an action.
</p>
<ComponentExample code={`<Dialog.Root disablePointerDismissal>
<Dialog.Trigger
render={(p) => (
<Button {...p} variant="destructive">Delete Project</Button>
)}
/>
<Dialog className="p-8">
<div className="mb-4 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-kumo-danger/20">
<Warning size={20} className="text-kumo-danger" />
</div>
<Dialog.Title className="text-xl font-semibold">
Delete Project?
</Dialog.Title>
</div>
<Dialog.Description className="text-kumo-subtle">
This action cannot be undone. This will permanently
delete the project and all associated data.
</Dialog.Description>
<div className="mt-8 flex justify-end gap-2">
<Dialog.Close
render={(props) => (
<Button variant="secondary" {...props}>Cancel</Button>
)}
/>
<Dialog.Close
render={(props) => (
<Button variant="destructive" {...props}>Delete</Button>
)}
/>
</div>
</Dialog>
</Dialog.Root>`}>
<DialogConfirmationDemo client:load />
</ComponentExample>
</div>

<div>
<Heading level={3}>With Actions</Heading>
<ComponentExample code={`<Dialog.Root>
Expand Down
21 changes: 14 additions & 7 deletions packages/kumo-docs-astro/src/pages/components/select.astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,29 @@ import {
<p class="mb-4 flex flex-col gap-4">
<span class="text-xl font-semibold">Basic Usage</span>
<span class="text-kumo-strong">
A simple select component where the display value matches the option
value. Perfect for basic dropdown menus with string-based options.
A simple select component. Use the <code class="rounded bg-kumo-control px-1 py-0.5 text-sm">items</code> prop
to map values to display labels — this controls what text appears in the
trigger when an option is selected. Without <code class="rounded bg-kumo-control px-1 py-0.5 text-sm">items</code>,
the raw value string is shown in the trigger.
</span>
</p>

<ComponentExample
code={`import { Select } from "@cloudflare/kumo";

function App() {
const [value, setValue] = useState("Apple");
const [value, setValue] = useState("apple");

return (
<Select className="w-[200px]" value={value} onValueChange={(v) => setValue(v ?? "Apple")}>
<Select.Option value="Apple">Apple</Select.Option>
<Select.Option value="Banana">Banana</Select.Option>
<Select.Option value="Cherry">Cherry</Select.Option>
<Select
className="w-[200px]"
value={value}
onValueChange={(v) => setValue(v ?? "apple")}
items={{ apple: "Apple", banana: "Banana", cherry: "Cherry" }}
>
<Select.Option value="apple">Apple</Select.Option>
<Select.Option value="banana">Banana</Select.Option>
<Select.Option value="cherry">Cherry</Select.Option>
</Select>
)
}`}
Expand Down
Loading