Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
149121a
feat: add List, Listbox, and Chip components
claude Apr 9, 2026
e46f595
docs: add documentation and examples for List, Listbox, and Chip
claude Apr 9, 2026
12e6086
fix: redesign Chip, improve List/Listbox selection behavior
claude Apr 9, 2026
8869d28
refactor: unify all item components into a single generic Item
claude Apr 9, 2026
3144467
fix: chip button spacing and sizing
claude Apr 9, 2026
603d3ed
fix: chip button flush to edge using calc() for border compensation
claude Apr 9, 2026
6c316bf
fix: chip button flush to edge via position:relative
claude Apr 9, 2026
503edd5
fix: chip button as small inline circular icon, not full-height bar
claude Apr 10, 2026
d6f4788
fix: chip button stretches full height, pulls outward via negative ma…
claude Apr 10, 2026
f919d92
fix: chip button uses explicit width/height for perfect circle
claude Apr 10, 2026
213ef55
fix: remove unnecessary margin-block on chip button
claude Apr 10, 2026
8002667
fix: restore margin-block and add symmetric positive inside margin on…
claude Apr 10, 2026
83bcc70
fix: chip button as simple inline circular icon
claude Apr 11, 2026
8e47288
fix: add cursor:pointer to Chip for consistency with other interactiv…
claude Apr 11, 2026
56e70b3
fix: add user-select:none to Chip for consistency with Button
claude Apr 11, 2026
79ed1db
refactor: Chip is a native button, Chip.Button is a span
claude Apr 11, 2026
a25ad4e
fix: chip button uses ref + addEventListener so user onClick fires
claude Apr 11, 2026
865049f
golf
developit Apr 11, 2026
b0fa99b
Merge branch 'main' into claude/kinu-missing-components-bmdgf
developit Apr 13, 2026
8ed170b
Merge branch 'main' into claude/kinu-missing-components-bmdgf
developit Apr 13, 2026
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
4 changes: 4 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ export const Button = createSimpleComponent('button', 'button');
- **Menubar**: Horizontal menu of actions
- **NavigationMenu**: Complex nav menu
- **Pagination**: Page controls
- **Item**: Generic selectable item for all list-like containers
- **Combobox**: Input with suggestions
- **List**: Interactive selectable list
- **Listbox**: Non-modal filterable list (inline command palette)
- **Chip**: Badge with icon button
- **ContextMenu**: Right-click menu
- **Drawer**: Bottom sliding panel
- **DropdownMenu**: Triggered action list
Expand Down
58 changes: 58 additions & 0 deletions docs/components/chip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Chip

Clickable pill label with an optional inline action affordance (typically for remove).

## Usage

```tsx
import {Chip} from 'kinu';

<Chip onClick={toggle}>
React
<Chip.Button onClick={onRemove}>×</Chip.Button>
</Chip>
```

## Exports

| Name | Description | Rendered HTML |
| --- | --- | --- |
| Chip | Chip container | `<button k="chip">` |
| ChipButton | Inline action affordance | `<span k="chip-button" role="button">` |
| Chip.Button | Alias of ChipButton | — |

## Props

### ChipProps

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| variant | `ChipVariant` | — | Visual style variant. |
| selected | `boolean` | — | Marks the chip as selected for styling. |

All standard `<button>` props are forwarded.

### ChipButtonProps

All standard `<span>` props are forwarded. `onClick` works via event bubbling.

## Variants

| Variant | Description |
| --- | --- |
| (default) | Secondary muted background. |
| primary | Primary background with primary foreground text. |
| destructive | Destructive red background. |
| outline | Transparent background with border. |

## Notes

- Chip renders as a real `<button>`, so it gets native keyboard activation (Enter/Space), focus ring, and accessible button role for free.
- Chip.Button renders as a `<span>` so it can be nested inside Chip's `<button>` without breaking HTML validity (nested `<button>` elements would be reparented by the HTML parser).
- Chip.Button's click events bubble to Chip, but the component installs a default `onClickCapture` that calls `stopPropagation()`, so clicking the button fires only its own `onClick` — not the Chip's.
- Chip.Button is not independently focusable by default. If you need keyboard access to a remove action, add `tabIndex={0}` and a `keydown` handler yourself, or handle Backspace on the Chip itself.
- Chip.Button uses `aria-hidden="true"` so screen readers read only the Chip's button text, not the `×` glyph.

---

_Source: `src/components/chip/index.tsx`
44 changes: 44 additions & 0 deletions docs/components/item.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Item

Generic selectable item used across all list-like containers: List, Listbox, DropdownMenu, ContextMenu, and Combobox.

## Usage

```tsx
import {Item} from 'kinu';

<Item selected>Inbox</Item>
<Item href="/settings">Settings</Item>
<Item shortcut="⌘K">Command Palette</Item>
<Item destructive>Delete</Item>
```

## Exports

| Name | Description | Rendered HTML |
| --- | --- | --- |
| Item | Selectable item | `<button k="item">` or `<a k="item">` |

Also available as `.Item` on parent components: `List.Item`, `Listbox.Item`, `DropdownMenu.Item`, `ContextMenu.Item`, `Combobox.Item`.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| href | `string` | — | When provided, renders the item as an anchor element. |
| selected | `boolean` | — | Marks the item as selected for styling. |
| shortcut | `string` | — | Shortcut hint rendered on the trailing edge via CSS `::after`. |
| destructive | `boolean` | — | Applies destructive (red) styling to the item. |
| value | `string` | — | Native button value attribute. Used by Combobox to get the selection value. |

## Notes

- Renders as `<button>` by default, or `<a>` when `href` is provided.
- The same component works in every context — the parent container determines the styling and behavior.
- The `shortcut` attribute is pure CSS (no JS), rendered via `::after`.
- Keyboard navigation (arrow keys, Enter) is handled by the parent container.
- In a List, selected items use foreground/background contrast. In menus, they use primary color.

---

_Source: `src/components/item/index.tsx`
44 changes: 44 additions & 0 deletions docs/components/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# List

Interactive selectable list. Items are the generic `Item` component — the same one used in DropdownMenu, Combobox, and everywhere else.

## Usage

```tsx
import {Item, List} from 'kinu';

<List>
<Item selected>Inbox</Item>
<Item>Drafts</Item>
<Item>Sent</Item>
</List>
```

## Exports

| Name | Description | Rendered HTML |
| --- | --- | --- |
| List | List container | `<div k="list">` |
| List.Item | Alias of Item | `<button k="item">` or `<a k="item">` |

## Props

### ListProps

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| variant | `"nav"` | — | Uses accent colors for hover/focus/selected instead of primary. |

### Item Props

See the [Item](/docs/item) docs for the full prop reference.

## Notes

- Selected items automatically use the foreground color as background with contrast-aware text — works in both light and dark mode.
- Use `variant="nav"` on the List for sidebar-style navigation with softer accent hover colors.
- Supports keyboard navigation with arrow keys when focused.

---

_Source: `src/components/list/index.tsx`
54 changes: 54 additions & 0 deletions docs/components/listbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Listbox

Non-modal filterable list for inline search and selection. Like Combobox but always visible.

## Usage

```tsx
import {Item, Listbox, ListboxInput, ListboxList} from 'kinu';

<Listbox>
<ListboxInput placeholder="Filter..." />
<ListboxList>
<Item>Apple</Item>
<Item>Banana</Item>
</ListboxList>
</Listbox>
```

## Exports

| Name | Description | Rendered HTML |
| --- | --- | --- |
| Listbox | Outer container | `<div k="listbox">` |
| ListboxInput | Filter input | `<input k="listbox-input">` |
| ListboxList | Options container | `<div k="listbox-list">` |
| Listbox.Item | Alias of Item | `<button k="item">` |

## Props

### ListboxInputProps

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| value | `string | number | readonly string[] | undefined` | — | Input value for controlled usage. |
| placeholder | `string` | — | Placeholder text for the input. |
| onInput | `(event: InputEvent) => void` | — | Change handler for controlled inputs. |
| disabled | `boolean` | — | Disable the input. |

### Item Props

See the [Item](/docs/item) docs for the full prop reference.

## Notes

- Selection state is developer-controlled — set `selected` on items yourself via `onClick`.
- Filtering only shows/hides items; it does not change selection.
- Shares filtering logic with Combobox via the `filterItems` utility.
- Unlike Combobox, the list is always visible (no dialog/popover).
- Arrow keys navigate items while the input is focused.
- Compose with Dialog to build a command palette.

---

_Source: `src/components/listbox/index.tsx`
35 changes: 35 additions & 0 deletions docs/examples/chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Chip} from 'kinu';
import {useState} from 'preact/hooks';

const initial = ['React', 'Preact', 'Vue', 'Svelte'];

export function Demo() {
const [tags, setTags] = useState(initial);
const [active, setActive] = useState<string | null>(null);
return (
<div style={{display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center'}}>
{tags.map((tag) => (
<Chip
key={tag}
selected={active === tag}
onClick={() => setActive(active === tag ? null : tag)}
>
{tag}
<Chip.Button onClick={() => setTags(tags.filter((t) => t !== tag))}>
×
</Chip.Button>
</Chip>
))}
<Chip variant="primary">Primary</Chip>
<Chip variant="destructive">Destructive</Chip>
<Chip variant="outline">Outline</Chip>
</div>
);
}

export const code = `<Chip selected onClick={toggle}>
React
<Chip.Button onClick={onRemove}>×</Chip.Button>
</Chip>`;

export default {Demo, code};
8 changes: 4 additions & 4 deletions docs/examples/combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {Combobox, ComboboxInput, ComboboxList, ComboboxOption} from 'kinu';
import {Combobox, ComboboxInput, ComboboxList, Item} from 'kinu';

export function Demo() {
return (
<Combobox>
<ComboboxInput />
<ComboboxList>
<ComboboxOption>Apple</ComboboxOption>
<ComboboxOption>Banana</ComboboxOption>
<ComboboxOption>Orange</ComboboxOption>
<Item>Apple</Item>
<Item>Banana</Item>
<Item>Orange</Item>
</ComboboxList>
</Combobox>
);
Expand Down
10 changes: 5 additions & 5 deletions docs/examples/context-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
Item,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
toast,
} from 'kinu';
Expand All @@ -28,11 +28,11 @@ export function Demo() {
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => logText('Cut')}>Cut</ContextMenuItem>
<ContextMenuItem onClick={() => logText('Copy')}>Copy</ContextMenuItem>
<ContextMenuItem onClick={() => logText('Paste')}>
<Item onClick={() => logText('Cut')}>Cut</Item>
<Item onClick={() => logText('Copy')}>Copy</Item>
<Item onClick={() => logText('Paste')}>
Paste
</ContextMenuItem>
</Item>
</ContextMenuContent>
</ContextMenu>
);
Expand Down
14 changes: 7 additions & 7 deletions docs/examples/dropdown-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {
Button,
Item,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
toast,
} from 'kinu';
Expand All @@ -22,15 +22,15 @@ export function Demo() {
<Button variant="outline">Actions ▼</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => logText('Edit clicked')}>
<Item onClick={() => logText('Edit clicked')}>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => logText('Copy clicked')}>
</Item>
<Item onClick={() => logText('Copy clicked')}>
Copy
</DropdownMenuItem>
<DropdownMenuItem onClick={() => logText('Delete clicked')}>
</Item>
<Item onClick={() => logText('Delete clicked')}>
Delete
</DropdownMenuItem>
</Item>
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down
22 changes: 22 additions & 0 deletions docs/examples/item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {Item} from 'kinu';
import {useState} from 'preact/hooks';

export function Demo() {
const [selected, setSelected] = useState<string | null>(null);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: '0.125rem', width: '14rem'}}>
<Item selected={selected === 'a'} onClick={() => setSelected('a')}>Default</Item>
<Item selected={selected === 'b'} onClick={() => setSelected('b')} shortcut="⌘K">With shortcut</Item>
<Item href="/docs">Link item</Item>
<Item destructive>Destructive</Item>
<Item disabled>Disabled</Item>
</div>
);
}

export const code = `<Item selected>Default</Item>
<Item shortcut="⌘K">With shortcut</Item>
<Item href="/docs">Link item</Item>
<Item destructive>Destructive</Item>`;

export default {Demo, code};
48 changes: 48 additions & 0 deletions docs/examples/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {Item, List, Separator} from 'kinu';
import {useState} from 'preact/hooks';

export function Demo() {
const [selected, setSelected] = useState('inbox');
return (
<div style={{display: 'flex', gap: '2rem', flexWrap: 'wrap'}}>
<List style={{width: '14rem'}}>
<Item selected={selected === 'inbox'} onClick={() => setSelected('inbox')}>
Inbox
</Item>
<Item selected={selected === 'drafts'} onClick={() => setSelected('drafts')}>
Drafts
</Item>
<Item selected={selected === 'sent'} onClick={() => setSelected('sent')}>
Sent
</Item>
<Separator />
<Item selected={selected === 'trash'} onClick={() => setSelected('trash')} destructive>
Trash
</Item>
</List>
<List variant="nav" style={{width: '14rem'}}>
<Item selected={selected === 'inbox'} onClick={() => setSelected('inbox')}>
Inbox
</Item>
<Item selected={selected === 'drafts'} onClick={() => setSelected('drafts')}>
Drafts
</Item>
<Item selected={selected === 'sent'} onClick={() => setSelected('sent')}>
Sent
</Item>
</List>
</div>
);
}

export const code = `<List>
<Item selected>Inbox</Item>
<Item>Drafts</Item>
</List>

<List variant="nav">
<Item selected>Inbox</Item>
<Item>Drafts</Item>
</List>`;

export default {Demo, code};
Loading
Loading