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
7 changes: 7 additions & 0 deletions .changeset/bright-tools-gather.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@karnstack/dowel": minor
---

Add Accordion, Breadcrumbs, Calendar and DatePicker, Collapsible, Combobox, CommandMenu, ContextMenu, Drawer, FileUpload and Dropzone, List collection primitives, Pagination, Progress, Slider, Toast with a global manager, ToggleGroup, and TreeView.

Calendar and DatePicker use React Aria behavior and timezone-safe `CalendarDate` values from `@internationalized/date`.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The system theme is the default. Pass `light` or `dark` to pin a theme.

## Components

Alert Dialog · Avatar · Badge · Button · Callout · Checkbox · Composer · Data Table · Dialog · Empty State · Icon Button · Input · Kbd · Menu · Native Select · Popover · Property Picker · Radio Group · Search Field · Select · Separator · Sidebar · Skeleton · Spinner · Status · Switch · Tabs · Tooltip
Alert Dialog · Avatar · Badge · Button · Callout · Checkbox · Combobox · Command Menu · Composer · Data Table · Dialog · Drawer · Empty State · Icon Button · Input · Kbd · List · Menu · Native Select · Popover · Progress · Property Picker · Radio Group · Search Field · Select · Separator · Sidebar · Skeleton · Spinner · Status · Switch · Tabs · Toast · Tooltip

Every component is keyboard tested and axe checked. The rendered catalog is contrast tested against WCAG 2.2 AA in light and dark. Visual controls own their appearance. Layout primitives accept application shell hooks.

Expand Down
148 changes: 26 additions & 122 deletions apps/docs/src/components/command-search.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,39 @@
import { Dialog, Input, Kbd } from "@karnstack/dowel";
import { CommandMenu } from "@karnstack/dowel";
import { useNavigate } from "@tanstack/react-router";
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";

import { componentNav } from "../lib/nav";
import { SearchIcon } from "./icons";

const allComponents = {
title: "All components",
to: "/components" as const,
summary: "Browse every component in the library.",
};
import { nav } from "../lib/nav";

export function CommandSearch({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const navigate = useNavigate();
const [query, setQuery] = useState("");
const [activeIndex, setActiveIndex] = useState(0);
const results = useMemo(() => {
const normalized = query.trim().toLowerCase();
const items = [allComponents, ...componentNav];
if (!normalized) return items;
return items.filter((item) =>
`${item.title} ${item.summary}`.toLowerCase().includes(normalized),
);
}, [query]);

useEffect(() => setActiveIndex(0), [query]);
useEffect(() => {
if (!open) setQuery("");
}, [open]);

function openResult(index: number) {
const result = results[index];
if (!result) return;
onOpenChange(false);
void navigate({ to: result.to });
}
const items = useMemo(
() =>
nav.flatMap((section) =>
section.items.map((item) => ({
id: `${section.title}:${item.to}`,
label: item.title,
description: item.summary,
group: section.title,
keywords: [section.title],
onSelect: () => void navigate({ to: item.to }),
})),
),
[navigate],
);

return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Popup variant="bare">
<div className="docs-command" role="search">
<div className="sr-only">
<Dialog.Title>Search components</Dialog.Title>
</div>
<div className="docs-command-input">
<SearchIcon />
<Input
autoFocus
aria-activedescendant={
results[activeIndex]
? `component-search-option-${activeIndex}`
: undefined
}
aria-autocomplete="list"
aria-controls="component-search-results"
aria-expanded="true"
aria-label="Search components"
name="component-search"
placeholder="Search components..."
role="combobox"
value={query}
variant="bare"
onChange={(event) => setQuery(event.currentTarget.value)}
onKeyDown={(event) => {
if (event.key === "ArrowDown") {
event.preventDefault();
if (!results.length) return;
setActiveIndex((index) =>
Math.min(index + 1, results.length - 1),
);
} else if (event.key === "ArrowUp") {
event.preventDefault();
if (!results.length) return;
setActiveIndex((index) => Math.max(index - 1, 0));
} else if (event.key === "Enter") {
event.preventDefault();
openResult(activeIndex);
}
}}
/>
<Kbd keys={["Esc"]} />
</div>

<div
className="docs-command-results"
id="component-search-results"
role="listbox"
>
{results.length ? (
results.map((result, index) => (
<button
aria-selected={index === activeIndex}
className="docs-command-result"
id={`component-search-option-${index}`}
key={result.to}
role="option"
type="button"
onClick={() => openResult(index)}
onPointerMove={() => setActiveIndex(index)}
>
<span>{result.title}</span>
<small>{result.summary}</small>
</button>
))
) : (
<p className="docs-command-empty">No components found.</p>
)}
</div>

<div className="docs-command-footer" aria-hidden="true">
<span>
<Kbd keys={["↑"]} />
<Kbd keys={["↓"]} />
Navigate
</span>
<span>
<Kbd keys={["Enter"]} />
Open
</span>
</div>
</div>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
<CommandMenu
label="Search documentation"
items={items}
defaultOpen
onOpenChange={onOpenChange}
placeholder="Search documentation..."
emptyText="No matching pages"
footer="↑↓ Navigate · ↵ Open · Esc Close"
/>
);
}
Loading
Loading