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
17 changes: 17 additions & 0 deletions .changeset/hotkeys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@ark-ui/react': minor
'@ark-ui/solid': minor
'@ark-ui/svelte': minor
'@ark-ui/vue': minor
---

Add a `hotkeys` entrypoint with hooks for registering and inspecting keyboard shortcuts, built on `@zag-js/hotkeys`.

- `useHotkey` and `useHotkeys` register commands. `mod+K` resolves per platform, and `G > H` sequences are handled by
the same hook rather than a separate one.
- `useHotkeyRegistrations` returns the registered commands with their metadata (`label`, `description`, `category`,
`keywords`), so a command palette or shortcut dialog can render from the same registration that binds the key.
- `usePressedKeys` and `useIsKeyPressed` track live key state.
- `useHotkeyRecorder` records a chord or a sequence for rebinding UIs.
- `usePlatform` and `useFormatHotkey` render shortcuts for the current platform without a hydration mismatch.
- `HotkeysProvider` shares one store and sets default options, active scopes, conflict behavior, and sequence timeout.
12 changes: 12 additions & 0 deletions .changeset/presence-enter-complete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@ark-ui/react': minor
'@ark-ui/solid': minor
'@ark-ui/svelte': minor
'@ark-ui/vue': minor
---

**Presence**: Add `onEnterComplete`, called once the enter animation finishes, mirroring the existing `onExitComplete`.
Vue exposes it as the `enter-complete` emit.

> Affects Color Picker, Combobox, Date Picker, Dialog, Drawer, Floating Panel, Hover Card, Menu, Popover, Select,
> Tooltip, and Tour.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,6 @@ vite.config.mts.timestamp-*
# Release
.npmrc
release.sh

# playwright-mcp session artifacts
.playwright-mcp
210 changes: 210 additions & 0 deletions .storybook/modules/command-palette.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
.Backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
z-index: calc(var(--demo-popover-z-index) + var(--layer-index, 0));

&[data-state='open'] {
animation: palette-fade-in 0.15s ease-out;
}

&[data-state='closed'] {
animation: palette-fade-out 0.1s ease-in;
}
}

.Positioner {
display: flex;
align-items: flex-start;
justify-content: center;
position: fixed;
inset: 0;
padding-top: 12vh;
z-index: calc(var(--demo-popover-z-index) + var(--layer-index, 0));
overscroll-behavior-y: none;
}

.Content {
display: flex;
flex-direction: column;
width: min(34rem, calc(100vw - 2rem));
max-height: min(28rem, calc(100vh - 20vh));
overflow: hidden;
background: var(--demo-bg-popover);
border: 1px solid var(--demo-border-muted);
border-radius: 0.875rem;
box-shadow: var(--demo-shadow-xl);
color: var(--demo-neutral-fg);

&[data-state='open'] {
animation: palette-scale-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}

&[data-state='closed'] {
animation: palette-fade-out 0.1s ease-in;
}
}

.Root {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
}

.Control {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0 1rem;
border-bottom: 1px solid var(--demo-border-muted);
flex-shrink: 0;

& > svg {
width: 1.0625rem;
height: 1.0625rem;
color: var(--demo-neutral-fg-muted);
flex-shrink: 0;
}
}

.Input {
flex: 1;
height: 3.25rem;
border: none;
outline: none;
background: transparent;
color: var(--demo-neutral-fg);
font-size: 0.9375rem;
font-family: inherit;

&::placeholder {
color: var(--demo-neutral-fg-muted);
}
}

.Content_list {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 0.5rem;
outline: none;
overscroll-behavior: contain;
scroll-padding-block: 0.5rem;
scrollbar-width: thin;
scrollbar-color: var(--demo-border-emphasized) transparent;
}

.ItemGroup {
display: flex;
flex-direction: column;
gap: 0.125rem;

& + & {
margin-top: 0.375rem;
}
}

.ItemGroupLabel {
padding: 0.375rem 0.625rem 0.25rem;
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--demo-neutral-fg-muted);
}

.Item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.5rem 0.625rem;
border-radius: 0.5rem;
font-size: 0.875rem;
user-select: none;
outline: none;
cursor: pointer;
transition: background 0.1s ease;

&[data-highlighted] {
background: var(--demo-neutral-subtle);
}
}

.ItemText {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.Shortcut {
display: inline-flex;
align-items: center;
gap: 0.125rem;
padding: 0.1875rem 0.4375rem;
border: 1px solid var(--demo-border-muted);
border-radius: 0.375rem;
background: var(--demo-neutral-subtle);
color: var(--demo-neutral-fg-muted);
font-family: inherit;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}

.Empty {
padding: 2rem 1rem;
text-align: center;
font-size: 0.875rem;
color: var(--demo-neutral-fg-muted);
}

.Footer {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 1rem;
border-top: 1px solid var(--demo-border-muted);
font-size: 0.75rem;
color: var(--demo-neutral-fg-muted);
flex-shrink: 0;
}

.FooterHint {
display: inline-flex;
align-items: center;
gap: 0.3125rem;
}

@keyframes palette-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes palette-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

@keyframes palette-scale-in {
from {
opacity: 0;
transform: translateY(-6px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
Loading