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
57 changes: 29 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ Requires Node 24 or later. Tested on React 19; the peer range also accepts React

Optional peers are needed only by the modules that import them. Those modules are not in the root entry, so import them by subpath:

| Peer | Subpaths |
| ------------------------------- | ------------------------------------------------------------- |
| `@radix-ui/react-dialog` | `ui/dialog`, `ui/command-palette`, `ui/mic-permission-dialog` |
| `@radix-ui/react-dropdown-menu` | `ui/menu` |
| `@radix-ui/react-tooltip` | `ui/tooltip` |
| `sonner` | `ui/toast` |
| Peer | Subpaths |
| ------------------------------- | ---------------------------------------------------------------------------------- |
| `@radix-ui/react-dialog` | `ui/dialog`, `ui/command-palette`, `ui/sidebar-search`, `ui/mic-permission-dialog` |
| `@radix-ui/react-dropdown-menu` | `ui/menu` |
| `@radix-ui/react-tooltip` | `ui/tooltip` |
| `sonner` | `ui/toast` |

## Quickstart

Expand Down Expand Up @@ -190,27 +190,28 @@ Everything imports from `@corbits/react-ui`, except rows marked "optional peer",

#### Layout

| Component | What it is |
| ------------------- | -------------------------------------------- |
| `PageShell` | Page margins and scroll ownership |
| `PagePanel` | Bordered page-content frame |
| `TopBar` | Title, breadcrumbs and actions row |
| `Sidebar` | Collapsible navigation sidebar |
| `SidebarPanel` | Sidebar with pins, body and footer |
| `SidebarItemRow` | One sidebar row |
| `ListDetail` | Index on the left, open item on the right |
| `InspectorShell` | Side inspector rail |
| `Section` | Titled block of content |
| `SettingsPanel` | Settings frame with optional save |
| `LibraryPageHeader` | Title, count and controls for a library page |
| `Card` | Bordered content card |
| `EmptyState` | Title, description, icon and action |
| `RichEmptyState` | Empty state with suggested next-step actions |
| `BootScreen` | Screen shown before the app loads |
| `DitherCanvas` | Animated dithered background |
| `CorbitsMark` | Corbits logo |
| `AuthLayout` | Sign-in page layout |
| `LoginForm` | Sign-in and sign-up form, with optional SSO |
| Component | What it is |
| ------------------- | -------------------------------------------------------------- |
| `PageShell` | Page margins and scroll ownership |
| `PagePanel` | Bordered page-content frame |
| `TopBar` | Title, breadcrumbs and actions row |
| `Sidebar` | Collapsible navigation sidebar |
| `SidebarSearch` | Search row labelled Cmd+K (optional peer, `ui/sidebar-search`) |
| `SidebarPanel` | Sidebar with pins, body and footer |
| `SidebarItemRow` | One sidebar row |
| `ListDetail` | Index on the left, open item on the right |
| `InspectorShell` | Side inspector rail |
| `Section` | Titled block of content |
| `SettingsPanel` | Settings frame with optional save |
| `LibraryPageHeader` | Title, count and controls for a library page |
| `Card` | Bordered content card |
| `EmptyState` | Title, description, icon and action |
| `RichEmptyState` | Empty state with suggested next-step actions |
| `BootScreen` | Screen shown before the app loads |
| `DitherCanvas` | Animated dithered background |
| `CorbitsMark` | Corbits logo |
| `AuthLayout` | Sign-in page layout |
| `LoginForm` | Sign-in and sign-up form, with optional SSO |

#### Controls

Expand Down Expand Up @@ -257,7 +258,7 @@ The host app owns the connection to the hub; components only take props.

- Install from npm. Git installs (`github:corbitsdev/react-ui#<sha>`) no longer build `dist/`.
- Per-module subpaths are gone: import from `@corbits/react-ui` (`import { Button } from "@corbits/react-ui"`).
- `ui/dialog`, `ui/command-palette`, `ui/mic-permission-dialog`, `ui/menu`, `ui/tooltip` and `ui/toast` are subpath-only, and their peers are optional: `import { Toaster, toast } from "@corbits/react-ui/ui/toast"`. Install the peer for each subpath you keep.
- `ui/dialog`, `ui/command-palette`, `ui/sidebar-search`, `ui/mic-permission-dialog`, `ui/menu`, `ui/tooltip` and `ui/toast` are subpath-only, and their peers are optional: `import { Toaster, toast } from "@corbits/react-ui/ui/toast"`. Install the peer for each subpath you keep.
- `lib/csv`, `lib/url`, `lib/utils`, `lib/workflow-registry` and `ui/chat-dock-timing` are internal. Replace `cn` with `twMerge(clsx(...))`.
- Stateful components ship `"use client"`. Client wrappers you wrote for them still work, and can go.
- 84 unused modules are removed, `ui/command` is removed, `ActivityBlock` is renamed `ReasoningBlock`, and `AuthLayout` no longer renders `DitherCanvas` by default.
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"types": "./dist/ui/command-palette.d.ts",
"default": "./dist/ui/command-palette.js"
},
"./ui/sidebar-search": {
"types": "./dist/ui/sidebar-search.d.ts",
"default": "./dist/ui/sidebar-search.js"
},
"./ui/dialog": {
"types": "./dist/ui/dialog.d.ts",
"default": "./dist/ui/dialog.js"
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// barrel is one module, so any root import would load that peer. They have
// their own entries in the `exports` map instead:
// @corbits/react-ui/ui/command-palette
// @corbits/react-ui/ui/sidebar-search
// @corbits/react-ui/ui/dialog
// @corbits/react-ui/ui/menu
// @corbits/react-ui/ui/mic-permission-dialog
Expand Down
66 changes: 45 additions & 21 deletions src/ui/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,36 +520,60 @@ export function CommandPaletteInline({
);
}

const commandShortcutSubscribers = new Set<() => void>();

function onCommandShortcutKeyDown(event: KeyboardEvent): void {
if (
event.key.toLowerCase() !== "k" ||
!(event.metaKey || event.ctrlKey) ||
event.repeat
) {
return;
}
const target = event.target as HTMLElement | null;
if (
target?.isContentEditable === true ||
/^(input|textarea|select)$/i.test(target?.tagName ?? "")
) {
return;
}
const subscriber = commandShortcutSubscribers.values().next().value;
if (subscriber === undefined) return;
event.preventDefault();
subscriber();
}

/**
* Binds ⌘K / Ctrl-K to a toggle.
*
* A hook and not built into the palette, because plenty of apps already own
* their global key handling and a second listener fighting theirs is worse
* than no listener at all. It ignores repeats from a held key and lets the
* browser keep its own ⌘K where the user is typing into a text field —
* than no listener at all. Every mounted caller shares one document listener,
* and a shortcut invokes exactly one subscriber, so a host toggle and a search
* row cannot undo each other. A later subscriber keeps the listener when the
* one that attached it unmounts. It ignores repeats from a held key and lets
* the browser keep its own ⌘K where the user is typing into a text field —
* stealing it there is how a palette becomes the thing that eats your search
* box.
* box. Pass `enabled: false` to stay out of the subscriber set.
*/
export function useCommandShortcut(onToggle: () => void, enabled = true): void {
const onToggleRef = useRef(onToggle);
onToggleRef.current = onToggle;

useEffect(() => {
if (!enabled) return;
const onKeyDown = (event: KeyboardEvent) => {
if (
event.key.toLowerCase() !== "k" ||
!(event.metaKey || event.ctrlKey) ||
event.repeat
)
return;
const target = event.target as HTMLElement | null;
if (
target?.isContentEditable === true ||
/^(input|textarea|select)$/i.test(target?.tagName ?? "")
)
return;
event.preventDefault();
onToggle();
const subscriber = () => {
onToggleRef.current();
};
commandShortcutSubscribers.add(subscriber);
if (commandShortcutSubscribers.size === 1) {
document.addEventListener("keydown", onCommandShortcutKeyDown);
}
return () => {
commandShortcutSubscribers.delete(subscriber);
if (commandShortcutSubscribers.size === 0) {
document.removeEventListener("keydown", onCommandShortcutKeyDown);
}
};
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [onToggle, enabled]);
}, [enabled]);
}
Loading
Loading