Showing preview only (205K chars total). Download the full file or copy to clipboard to get everything.
Repository: dip/cmdk
Branch: main
Commit: dd2250ed6084
Files: 66
Total size: 189.6 KB
Directory structure:
gitextract_vtq8gdaf/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .prettierignore
├── .prettierrc.js
├── ARCHITECTURE.md
├── LICENSE.md
├── README.md
├── cmdk/
│ ├── package.json
│ ├── src/
│ │ ├── command-score.ts
│ │ └── index.tsx
│ └── tsup.config.ts
├── package.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── test/
│ ├── basic.test.ts
│ ├── dialog.test.ts
│ ├── group.test.ts
│ ├── item.test.ts
│ ├── keybind.test.ts
│ ├── next-env.d.ts
│ ├── numeric.test.ts
│ ├── package.json
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── dialog.tsx
│ │ ├── group.tsx
│ │ ├── huge.tsx
│ │ ├── index.tsx
│ │ ├── item-advanced.tsx
│ │ ├── item.tsx
│ │ ├── keybinds.tsx
│ │ ├── numeric.tsx
│ │ ├── portal.tsx
│ │ └── props.tsx
│ ├── props.test.ts
│ ├── style.css
│ └── tsconfig.json
├── tsconfig.json
└── website/
├── .eslintrc.json
├── .gitignore
├── README.md
├── components/
│ ├── cmdk/
│ │ ├── framer.tsx
│ │ ├── linear.tsx
│ │ ├── raycast.tsx
│ │ └── vercel.tsx
│ ├── code/
│ │ ├── code.module.scss
│ │ └── index.tsx
│ ├── icons/
│ │ ├── icons.module.scss
│ │ └── index.tsx
│ └── index.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages/
│ ├── _app.tsx
│ ├── _document.tsx
│ └── index.tsx
├── public/
│ └── robots.txt
├── styles/
│ ├── cmdk/
│ │ ├── framer.scss
│ │ ├── linear.scss
│ │ ├── raycast.scss
│ │ └── vercel.scss
│ ├── globals.scss
│ └── index.module.scss
├── tsconfig.json
└── vercel.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: pacocoursey
================================================
FILE: .github/workflows/test.yml
================================================
name: Run E2E tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 # respects packageManager in package.json
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
- run: pnpm install
env:
CI: true
- run: pnpm build
- run: pnpm test:format
- run: pnpm playwright install --with-deps
- run: pnpm test || exit 1
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report.json
================================================
FILE: .gitignore
================================================
.DS_Store
.idea
.env
.env.local
.env.development
.env.development.local
*.log
yalc.lock
.vercel/
.turbo/
.next/
.yalc/
build/
dist/
node_modules/
.vercel
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm lint-staged
================================================
FILE: .prettierignore
================================================
.next
dist
pnpm-lock.yaml
.pnpm-store
.vercel
================================================
FILE: .prettierrc.js
================================================
module.exports = {
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
printWidth: 120,
}
================================================
FILE: ARCHITECTURE.md
================================================
# Architecture
> Document is a work in progress!
⌘K is born from a simple constraint: can you write a combobox with filtering and sorting using the [compound component](https://kentcdodds.com/blog/compound-components-with-react-hooks) approach? We didn't want to render items manually from an array:
```tsx
// No
<>
{items.map((item) => {
return <div>{item}</div>
})}
</>
```
We didn't want to provide a render prop:
```tsx
// No
onItemRender={({ item }) => {
return <div>{item}</div>
}}
```
Instead, we wanted to render components:
```tsx
// Yes
<Item>My item</Item>
```
Especially, we wanted full component composition:
```tsx
// YES
<>
<BlogItems />
{staticItems}
</>
```
Compound components are natural and easy to write. A few months after exploring this library, we were pleased to see [Radix UI](https://www.radix-ui.com) released using this exact approach of component structure – setting the standard for ease of use and composability.
However, for a combobox, it is a terrible, terrible constraint that we've spent 2 years fighting.
## Approach
⌘K always keeps every item and group rendered in the React tree. Each item and group adds or removes itself from the DOM based on the search input. The DOM is authoritative. Item selection order is based on the DOM order, which is based on the React render order, which the consumer provides.
### Discarded approach
We did not use `React.Children` iteration because it will not support component composition. There is no way to "peek inside" the items contained within `<BlogItems />`, so those items cannot be filtered.
We did not use an object-based data array for each item, like `{ name: "Logout", action: () => logout() }` because this is strict and limiting. In reality, the interface of those objects grows with edge-cases, like `image`, `detailedSubTitle`, `hideWhenRootSearch`, etc. We prefer that you have full control of item rendering, including icons, keyboard shortcuts, and styling. Don't want an item shown? Don't render it. Only want to show an item under condition xyz? Render it.
We did not use a render prop because they are an inelegant pattern and quickly fall to long, centralised if-else logic chains. For example, if you want a fancy sparkle rainbow item, you need a new if statement to render that item specially.
The original approach for tracking which item was selected was to keep an index 0..n. But it's impossible to know which Item is in which position within the React tree when React Strict Mode is enabled, because `useEffect` runs twice and `useRef` cannot be used for stable IDs. <sup>This may be possible with `useId`, now.</sup> We created [use-descendants](https://github.com/pacocoursey/use-descendants) to track relative component indeces, but abandoned it because it could not work in Strict Mode, and will be incompatible with upcoming concurrent mode. Now, we track the selected item with its value, because it is stable across item mounts and unmounts.
## Example
```tsx
<Input value="b" />
<List>
<Item>A</Item>
<Item>B</Item>
</List>
```
The "A" item should not be shown! But we cannot remove it from the React tree, because the user controls it. In most cases, this is easy because the rendered items is sourced from a backing data array:
```tsx
<>
{['A', 'B'].map((item) => {
if (matches(item, search)) {
return <Item>{item}</Item>
}
})}
</>
```
But in our case, the item will remain in the React tree and just be removed from the DOM:
```tsx
<List>
{/* returns `null`, no DOM created */}
<Item>A</Item>
<Item>B</Item>
</List>
```
## Performance
This is more expensive memory wise, because if there are 2,000 items but the list is filtered to only 2 items, we still allocate memory for 2,000 instances of the Item component. But it's our only option! Thankfully we can still keep the DOM size to 2 items.
## Groups
Item mount informs both the root and the parent group, which keeps track of items within it. Each group informs the root.
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2022 Paco Coursey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
<img src="./website/public/og.png" />
</p>
# ⌘K [](https://www.npmjs.com/package/cmdk?activeTab=code) [](https://www.npmjs.com/package/cmdk)
⌘K is a command menu React component that can also be used as an accessible combobox. You render items, it filters and sorts them automatically. ⌘K supports a fully composable API <sup><sup>[How?](/ARCHITECTURE.md)</sup></sup>, so you can wrap items in other components or even as static JSX.
## Install
```bash
pnpm install cmdk
```
## Use
```tsx
import { Command } from 'cmdk'
const CommandMenu = () => {
return (
<Command label="Command Menu">
<Command.Input />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Letters">
<Command.Item>a</Command.Item>
<Command.Item>b</Command.Item>
<Command.Separator />
<Command.Item>c</Command.Item>
</Command.Group>
<Command.Item>Apple</Command.Item>
</Command.List>
</Command>
)
}
```
Or in a dialog:
```tsx
import { Command } from 'cmdk'
const CommandMenu = () => {
const [open, setOpen] = React.useState(false)
// Toggle the menu when ⌘K is pressed
React.useEffect(() => {
const down = (e) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
return (
<Command.Dialog open={open} onOpenChange={setOpen} label="Global Command Menu">
<Command.Input />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Letters">
<Command.Item>a</Command.Item>
<Command.Item>b</Command.Item>
<Command.Separator />
<Command.Item>c</Command.Item>
</Command.Group>
<Command.Item>Apple</Command.Item>
</Command.List>
</Command.Dialog>
)
}
```
## Parts and styling
All parts forward props, including `ref`, to an appropriate element. Each part has a specific data-attribute (starting with `cmdk-`) that can be used for styling.
### Command `[cmdk-root]`
Render this to show the command menu inline, or use [Dialog](#dialog-cmdk-dialog-cmdk-overlay) to render in a elevated context. Can be controlled with the `value` and `onValueChange` props.
> **Note**
>
> Values are always trimmed with the [trim()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim) method.
```tsx
const [value, setValue] = React.useState('apple')
return (
<Command value={value} onValueChange={setValue}>
<Command.Input />
<Command.List>
<Command.Item>Orange</Command.Item>
<Command.Item>Apple</Command.Item>
</Command.List>
</Command>
)
```
You can provide a custom `filter` function that is called to rank each item. Note that the value will be trimmed.
```tsx
<Command
filter={(value, search) => {
if (value.includes(search)) return 1
return 0
}}
/>
```
A third argument, `keywords`, can also be provided to the filter function. Keywords act as aliases for the item value, and can also affect the rank of the item. Keywords are trimmed.
```tsx
<Command
filter={(value, search, keywords) => {
const extendValue = value + ' ' + keywords.join(' ')
if (extendValue.includes(search)) return 1
return 0
}}
/>
```
Or disable filtering and sorting entirely:
```tsx
<Command shouldFilter={false}>
<Command.List>
{filteredItems.map((item) => {
return (
<Command.Item key={item} value={item}>
{item}
</Command.Item>
)
})}
</Command.List>
</Command>
```
You can make the arrow keys wrap around the list (when you reach the end, it goes back to the first item) by setting the `loop` prop:
```tsx
<Command loop />
```
### Dialog `[cmdk-dialog]` `[cmdk-overlay]`
Props are forwarded to [Command](#command-cmdk-root). Composes Radix UI's Dialog component. The overlay is always rendered. See the [Radix Documentation](https://www.radix-ui.com/docs/primitives/components/dialog) for more information. Can be controlled with the `open` and `onOpenChange` props.
```tsx
const [open, setOpen] = React.useState(false)
return (
<Command.Dialog open={open} onOpenChange={setOpen}>
...
</Command.Dialog>
)
```
You can provide a `container` prop that accepts an HTML element that is forwarded to Radix UI's Dialog Portal component to specify which element the Dialog should portal into (defaults to `body`). See the [Radix Documentation](https://www.radix-ui.com/docs/primitives/components/dialog#portal) for more information.
```tsx
const containerElement = React.useRef(null)
return (
<>
<Command.Dialog container={containerElement.current} />
<div ref={containerElement} />
</>
)
```
### Input `[cmdk-input]`
All props are forwarded to the underlying `input` element. Can be controlled with the `value` and `onValueChange` props.
```tsx
const [search, setSearch] = React.useState('')
return <Command.Input value={search} onValueChange={setSearch} />
```
### List `[cmdk-list]`
Contains items and groups. Animate height using the `--cmdk-list-height` CSS variable.
```css
[cmdk-list] {
min-height: 300px;
height: var(--cmdk-list-height);
max-height: 500px;
transition: height 100ms ease;
}
```
To scroll item into view earlier near the edges of the viewport, use scroll-padding:
```css
[cmdk-list] {
scroll-padding-block-start: 8px;
scroll-padding-block-end: 8px;
}
```
### Item `[cmdk-item]` `[data-disabled?]` `[data-selected?]`
Item that becomes active on pointer enter. You should provide a unique `value` for each item, but it will be automatically inferred from the `.textContent`.
```tsx
<Command.Item
onSelect={(value) => console.log('Selected', value)}
// Value is implicity "apple" because of the provided text content
>
Apple
</Command.Item>
```
You can also provide a `keywords` prop to help with filtering. Keywords are trimmed.
```tsx
<Command.Item keywords={['fruit', 'apple']}>Apple</Command.Item>
```
```tsx
<Command.Item
onSelect={(value) => console.log('Selected', value)}
// Value is implicity "apple" because of the provided text content
>
Apple
</Command.Item>
```
You can force an item to always render, regardless of filtering, by passing the `forceMount` prop.
### Group `[cmdk-group]` `[hidden?]`
Groups items together with the given `heading` (`[cmdk-group-heading]`).
```tsx
<Command.Group heading="Fruit">
<Command.Item>Apple</Command.Item>
</Command.Group>
```
Groups will not unmount from the DOM, rather the `hidden` attribute is applied to hide it from view. This may be relevant in your styling.
You can force a group to always render, regardless of filtering, by passing the `forceMount` prop.
### Separator `[cmdk-separator]`
Visible when the search query is empty or `alwaysRender` is true, hidden otherwise.
### Empty `[cmdk-empty]`
Automatically renders when there are no results for the search query.
### Loading `[cmdk-loading]`
You should conditionally render this with `progress` while loading asynchronous items.
```tsx
const [loading, setLoading] = React.useState(false)
return <Command.List>{loading && <Command.Loading>Hang on…</Command.Loading>}</Command.List>
```
### `useCommandState(state => state.selectedField)`
Hook that composes [`useSyncExternalStore`](https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore). Pass a function that returns a slice of the command menu state to re-render when that slice changes. This hook is provided for advanced use cases and should not be commonly used.
A good use case would be to render a more detailed empty state, like so:
```tsx
const search = useCommandState((state) => state.search)
return <Command.Empty>No results found for "{search}".</Command.Empty>
```
## Examples
Code snippets for common use cases.
### Nested items
Often selecting one item should navigate deeper, with a more refined set of items. For example selecting "Change theme…" should show new items "Dark theme" and "Light theme". We call these sets of items "pages", and they can be implemented with simple state:
```tsx
const ref = React.useRef(null)
const [open, setOpen] = React.useState(false)
const [search, setSearch] = React.useState('')
const [pages, setPages] = React.useState([])
const page = pages[pages.length - 1]
return (
<Command
onKeyDown={(e) => {
// Escape goes to previous page
// Backspace goes to previous page when search is empty
if (e.key === 'Escape' || (e.key === 'Backspace' && !search)) {
e.preventDefault()
setPages((pages) => pages.slice(0, -1))
}
}}
>
<Command.Input value={search} onValueChange={setSearch} />
<Command.List>
{!page && (
<>
<Command.Item onSelect={() => setPages([...pages, 'projects'])}>Search projects…</Command.Item>
<Command.Item onSelect={() => setPages([...pages, 'teams'])}>Join a team…</Command.Item>
</>
)}
{page === 'projects' && (
<>
<Command.Item>Project A</Command.Item>
<Command.Item>Project B</Command.Item>
</>
)}
{page === 'teams' && (
<>
<Command.Item>Team 1</Command.Item>
<Command.Item>Team 2</Command.Item>
</>
)}
</Command.List>
</Command>
)
```
### Show sub-items when searching
If your items have nested sub-items that you only want to reveal when searching, render based on the search state:
```tsx
const SubItem = (props) => {
const search = useCommandState((state) => state.search)
if (!search) return null
return <Command.Item {...props} />
}
return (
<Command>
<Command.Input />
<Command.List>
<Command.Item>Change theme…</Command.Item>
<SubItem>Change theme to dark</SubItem>
<SubItem>Change theme to light</SubItem>
</Command.List>
</Command>
)
```
### Asynchronous results
Render the items as they become available. Filtering and sorting will happen automatically.
```tsx
const [loading, setLoading] = React.useState(false)
const [items, setItems] = React.useState([])
React.useEffect(() => {
async function getItems() {
setLoading(true)
const res = await api.get('/dictionary')
setItems(res)
setLoading(false)
}
getItems()
}, [])
return (
<Command>
<Command.Input />
<Command.List>
{loading && <Command.Loading>Fetching words…</Command.Loading>}
{items.map((item) => {
return (
<Command.Item key={`word-${item}`} value={item}>
{item}
</Command.Item>
)
})}
</Command.List>
</Command>
)
```
### Use inside Popover
We recommend using the [Radix UI popover](https://www.radix-ui.com/docs/primitives/components/popover) component. ⌘K relies on the Radix UI Dialog component, so this will reduce your bundle size a bit due to shared dependencies.
```bash
$ pnpm install @radix-ui/react-popover
```
Render `Command` inside of the popover content:
```tsx
import * as Popover from '@radix-ui/react-popover'
return (
<Popover.Root>
<Popover.Trigger>Toggle popover</Popover.Trigger>
<Popover.Content>
<Command>
<Command.Input />
<Command.List>
<Command.Item>Apple</Command.Item>
</Command.List>
</Command>
</Popover.Content>
</Popover.Root>
)
```
### Drop in stylesheets
You can find global stylesheets to drop in as a starting point for styling. See [website/styles/cmdk](website/styles/cmdk) for examples.
## FAQ
**Accessible?** Yes. Labeling, aria attributes, and DOM ordering tested with Voice Over and Chrome DevTools. [Dialog](#dialog-cmdk-dialog-cmdk-overlay) composes an accessible Dialog implementation.
**Virtualization?** No. Good performance up to 2,000-3,000 items, though. Read below to bring your own.
**Filter/sort items manually?** Yes. Pass `shouldFilter={false}` to [Command](#command-cmdk-root). Better memory usage and performance. Bring your own virtualization this way.
**React 18 safe?** Yes, required. Uses React 18 hooks like `useId` and `useSyncExternalStore`.
**Unstyled?** Yes, use the listed CSS selectors.
**Hydration mismatch?** No, likely a bug in your code. Ensure the `open` prop to `Command.Dialog` is `false` on the server.
**React strict mode safe?** Yes. Open an issue if you notice an issue.
**Weird/wrong behavior?** Make sure your `Command.Item` has a `key` and unique `value`.
**Concurrent mode safe?** Maybe, but concurrent mode is not yet real. Uses risky approaches like manual DOM ordering.
**React server component?** No, it's a client component.
**Listen for ⌘K automatically?** No, do it yourself to have full control over keybind context.
**React Native?** No, and no plans to support it. If you build a React Native version, let us know and we'll link your repository here.
## History
Written in 2019 by Paco ([@pacocoursey](https://twitter.com/pacocoursey)) to see if a composable combobox API was possible. Used for the Vercel command menu and autocomplete by Rauno ([@raunofreiberg](https://twitter.com/raunofreiberg)) in 2020. Re-written independently in 2022 with a simpler and more performant approach. Ideas and help from Shu ([@shuding\_](https://twitter.com/shuding_)).
[use-descendants](https://github.com/pacocoursey/use-descendants) was extracted from the 2019 version.
## Testing
First, install dependencies and Playwright browsers:
```bash
pnpm install
pnpm playwright install
```
Then ensure you've built the library:
```bash
pnpm build
```
Then run the tests using your local build against real browser engines:
```bash
pnpm test
```
================================================
FILE: cmdk/package.json
================================================
{
"name": "cmdk",
"version": "1.1.1",
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"scripts": {
"prepublishOnly": "cp ../README.md . && pnpm build",
"postpublish": "rm README.md",
"build": "tsup src",
"dev": "tsup src --watch"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
},
"dependencies": {
"@radix-ui/react-compose-refs": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-id": "^1.1.0",
"@radix-ui/react-primitive": "^2.0.2"
},
"devDependencies": {
"@types/react": "18.0.15"
},
"sideEffects": false,
"repository": {
"type": "git",
"url": "git+https://github.com/pacocoursey/cmdk.git",
"directory": "cmdk"
},
"bugs": {
"url": "https://github.com/pacocoursey/cmdk/issues"
},
"homepage": "https://github.com/pacocoursey/cmdk#readme",
"author": {
"name": "Paco",
"url": "https://github.com/pacocoursey"
}
}
================================================
FILE: cmdk/src/command-score.ts
================================================
// The scores are arranged so that a continuous match of characters will
// result in a total score of 1.
//
// The best case, this character is a match, and either this is the start
// of the string, or the previous character was also a match.
var SCORE_CONTINUE_MATCH = 1,
// A new match at the start of a word scores better than a new match
// elsewhere as it's more likely that the user will type the starts
// of fragments.
// NOTE: We score word jumps between spaces slightly higher than slashes, brackets
// hyphens, etc.
SCORE_SPACE_WORD_JUMP = 0.9,
SCORE_NON_SPACE_WORD_JUMP = 0.8,
// Any other match isn't ideal, but we include it for completeness.
SCORE_CHARACTER_JUMP = 0.17,
// If the user transposed two letters, it should be significantly penalized.
//
// i.e. "ouch" is more likely than "curtain" when "uc" is typed.
SCORE_TRANSPOSITION = 0.1,
// The goodness of a match should decay slightly with each missing
// character.
//
// i.e. "bad" is more likely than "bard" when "bd" is typed.
//
// This will not change the order of suggestions based on SCORE_* until
// 100 characters are inserted between matches.
PENALTY_SKIPPED = 0.999,
// The goodness of an exact-case match should be higher than a
// case-insensitive match by a small amount.
//
// i.e. "HTML" is more likely than "haml" when "HM" is typed.
//
// This will not change the order of suggestions based on SCORE_* until
// 1000 characters are inserted between matches.
PENALTY_CASE_MISMATCH = 0.9999,
// Match higher for letters closer to the beginning of the word
PENALTY_DISTANCE_FROM_START = 0.9,
// If the word has more characters than the user typed, it should
// be penalised slightly.
//
// i.e. "html" is more likely than "html5" if I type "html".
//
// However, it may well be the case that there's a sensible secondary
// ordering (like alphabetical) that it makes sense to rely on when
// there are many prefix matches, so we don't make the penalty increase
// with the number of tokens.
PENALTY_NOT_COMPLETE = 0.99
var IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/,
COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g,
IS_SPACE_REGEXP = /[\s-]/,
COUNT_SPACE_REGEXP = /[\s-]/g
function commandScoreInner(
string,
abbreviation,
lowerString,
lowerAbbreviation,
stringIndex,
abbreviationIndex,
memoizedResults,
) {
if (abbreviationIndex === abbreviation.length) {
if (stringIndex === string.length) {
return SCORE_CONTINUE_MATCH
}
return PENALTY_NOT_COMPLETE
}
var memoizeKey = `${stringIndex},${abbreviationIndex}`
if (memoizedResults[memoizeKey] !== undefined) {
return memoizedResults[memoizeKey]
}
var abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex)
var index = lowerString.indexOf(abbreviationChar, stringIndex)
var highScore = 0
var score, transposedScore, wordBreaks, spaceBreaks
while (index >= 0) {
score = commandScoreInner(
string,
abbreviation,
lowerString,
lowerAbbreviation,
index + 1,
abbreviationIndex + 1,
memoizedResults,
)
if (score > highScore) {
if (index === stringIndex) {
score *= SCORE_CONTINUE_MATCH
} else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) {
score *= SCORE_NON_SPACE_WORD_JUMP
wordBreaks = string.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP)
if (wordBreaks && stringIndex > 0) {
score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length)
}
} else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) {
score *= SCORE_SPACE_WORD_JUMP
spaceBreaks = string.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP)
if (spaceBreaks && stringIndex > 0) {
score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length)
}
} else {
score *= SCORE_CHARACTER_JUMP
if (stringIndex > 0) {
score *= Math.pow(PENALTY_SKIPPED, index - stringIndex)
}
}
if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) {
score *= PENALTY_CASE_MISMATCH
}
}
if (
(score < SCORE_TRANSPOSITION &&
lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1)) ||
(lowerAbbreviation.charAt(abbreviationIndex + 1) === lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428
lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex))
) {
transposedScore = commandScoreInner(
string,
abbreviation,
lowerString,
lowerAbbreviation,
index + 1,
abbreviationIndex + 2,
memoizedResults,
)
if (transposedScore * SCORE_TRANSPOSITION > score) {
score = transposedScore * SCORE_TRANSPOSITION
}
}
if (score > highScore) {
highScore = score
}
index = lowerString.indexOf(abbreviationChar, index + 1)
}
memoizedResults[memoizeKey] = highScore
return highScore
}
function formatInput(string) {
// convert all valid space characters to space so they match each other
return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' ')
}
export function commandScore(string: string, abbreviation: string, aliases: string[]): number {
/* NOTE:
* in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase()
* was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster.
*/
string = aliases && aliases.length > 0 ? `${string + ' ' + aliases.join(' ')}` : string
return commandScoreInner(string, abbreviation, formatInput(string), formatInput(abbreviation), 0, 0, {})
}
================================================
FILE: cmdk/src/index.tsx
================================================
'use client'
import * as RadixDialog from '@radix-ui/react-dialog'
import * as React from 'react'
import { commandScore } from './command-score'
import { Primitive } from '@radix-ui/react-primitive'
import { useId } from '@radix-ui/react-id'
import { composeRefs } from '@radix-ui/react-compose-refs'
type Children = { children?: React.ReactNode }
type DivProps = React.ComponentPropsWithoutRef<typeof Primitive.div>
type LoadingProps = Children &
DivProps & {
/** Estimated progress of loading asynchronous options. */
progress?: number
/**
* Accessible label for this loading progressbar. Not shown visibly.
*/
label?: string
}
type EmptyProps = Children & DivProps & {}
type SeparatorProps = DivProps & {
/** Whether this separator should always be rendered. Useful if you disable automatic filtering. */
alwaysRender?: boolean
}
type DialogProps = RadixDialog.DialogProps &
CommandProps & {
/** Provide a className to the Dialog overlay. */
overlayClassName?: string
/** Provide a className to the Dialog content. */
contentClassName?: string
/** Provide a custom element the Dialog should portal into. */
container?: HTMLElement
}
type ListProps = Children &
DivProps & {
/**
* Accessible label for this List of suggestions. Not shown visibly.
*/
label?: string
}
type ItemProps = Children &
Omit<DivProps, 'disabled' | 'onSelect' | 'value'> & {
/** Whether this item is currently disabled. */
disabled?: boolean
/** Event handler for when this item is selected, either via click or keyboard selection. */
onSelect?: (value: string) => void
/**
* A unique value for this item.
* If no value is provided, it will be inferred from `children` or the rendered `textContent`. If your `textContent` changes between renders, you _must_ provide a stable, unique `value`.
*/
value?: string
/** Optional keywords to match against when filtering. */
keywords?: string[]
/** Whether this item is forcibly rendered regardless of filtering. */
forceMount?: boolean
}
type GroupProps = Children &
Omit<DivProps, 'heading' | 'value'> & {
/** Optional heading to render for this group. */
heading?: React.ReactNode
/** If no heading is provided, you must provide a value that is unique for this group. */
value?: string
/** Whether this group is forcibly rendered regardless of filtering. */
forceMount?: boolean
}
type InputProps = Omit<React.ComponentPropsWithoutRef<typeof Primitive.input>, 'value' | 'onChange' | 'type'> & {
/**
* Optional controlled state for the value of the search input.
*/
value?: string
/**
* Event handler called when the search value changes.
*/
onValueChange?: (search: string) => void
}
type CommandFilter = (value: string, search: string, keywords?: string[]) => number
type CommandProps = Children &
DivProps & {
/**
* Accessible label for this command menu. Not shown visibly.
*/
label?: string
/**
* Optionally set to `false` to turn off the automatic filtering and sorting.
* If `false`, you must conditionally render valid items based on the search query yourself.
*/
shouldFilter?: boolean
/**
* Custom filter function for whether each command menu item should matches the given search query.
* It should return a number between 0 and 1, with 1 being the best match and 0 being hidden entirely.
* By default, uses the `command-score` library.
*/
filter?: CommandFilter
/**
* Optional default item value when it is initially rendered.
*/
defaultValue?: string
/**
* Optional controlled state of the selected command menu item.
*/
value?: string
/**
* Event handler called when the selected item of the menu changes.
*/
onValueChange?: (value: string) => void
/**
* Optionally set to `true` to turn on looping around when using the arrow keys.
*/
loop?: boolean
/**
* Optionally set to `true` to disable selection via pointer events.
*/
disablePointerSelection?: boolean
/**
* Set to `false` to disable ctrl+n/j/p/k shortcuts. Defaults to `true`.
*/
vimBindings?: boolean
}
type Context = {
value: (id: string, value: string, keywords?: string[]) => void
item: (id: string, groupId: string) => () => void
group: (id: string) => () => void
filter: () => boolean
label: string
getDisablePointerSelection: () => boolean
// Ids
listId: string
labelId: string
inputId: string
// Refs
listInnerRef: React.RefObject<HTMLDivElement | null>
}
type State = {
search: string
value: string
selectedItemId?: string
filtered: { count: number; items: Map<string, number>; groups: Set<string> }
}
type Store = {
subscribe: (callback: () => void) => () => void
snapshot: () => State
setState: <K extends keyof State>(key: K, value: State[K], opts?: any) => void
emit: () => void
}
type Group = {
id: string
forceMount?: boolean
}
const GROUP_SELECTOR = `[cmdk-group=""]`
const GROUP_ITEMS_SELECTOR = `[cmdk-group-items=""]`
const GROUP_HEADING_SELECTOR = `[cmdk-group-heading=""]`
const ITEM_SELECTOR = `[cmdk-item=""]`
const VALID_ITEM_SELECTOR = `${ITEM_SELECTOR}:not([aria-disabled="true"])`
const SELECT_EVENT = `cmdk-item-select`
const VALUE_ATTR = `data-value`
const defaultFilter: CommandFilter = (value, search, keywords) => commandScore(value, search, keywords)
const CommandContext = React.createContext<Context>(undefined)
const useCommand = () => React.useContext(CommandContext)
const StoreContext = React.createContext<Store>(undefined)
const useStore = () => React.useContext(StoreContext)
const GroupContext = React.createContext<Group>(undefined)
const Command = React.forwardRef<HTMLDivElement, CommandProps>((props, forwardedRef) => {
const state = useLazyRef<State>(() => ({
/** Value of the search query. */
search: '',
/** Currently selected item value. */
value: props.value ?? props.defaultValue ?? '',
/** Currently selected item id. */
selectedItemId: undefined,
filtered: {
/** The count of all visible items. */
count: 0,
/** Map from visible item id to its search score. */
items: new Map(),
/** Set of groups with at least one visible item. */
groups: new Set(),
},
}))
const allItems = useLazyRef<Set<string>>(() => new Set()) // [...itemIds]
const allGroups = useLazyRef<Map<string, Set<string>>>(() => new Map()) // groupId → [...itemIds]
const ids = useLazyRef<Map<string, { value: string; keywords?: string[] }>>(() => new Map()) // id → { value, keywords }
const listeners = useLazyRef<Set<() => void>>(() => new Set()) // [...rerenders]
const propsRef = useAsRef(props)
const {
label,
children,
value,
onValueChange,
filter,
shouldFilter,
loop,
disablePointerSelection = false,
vimBindings = true,
...etc
} = props
const listId = useId()
const labelId = useId()
const inputId = useId()
const listInnerRef = React.useRef<HTMLDivElement>(null)
const schedule = useScheduleLayoutEffect()
/** Controlled mode `value` handling. */
useLayoutEffect(() => {
if (value !== undefined) {
const v = value.trim()
state.current.value = v
store.emit()
}
}, [value])
useLayoutEffect(() => {
schedule(6, scrollSelectedIntoView)
}, [])
const store: Store = React.useMemo(() => {
return {
subscribe: (cb) => {
listeners.current.add(cb)
return () => listeners.current.delete(cb)
},
snapshot: () => {
return state.current
},
setState: (key, value, opts) => {
if (Object.is(state.current[key], value)) return
state.current[key] = value
if (key === 'search') {
// Filter synchronously before emitting back to children
filterItems()
sort()
schedule(1, selectFirstItem)
} else if (key === 'value') {
// Force focus input or root so accessibility works
if (document.activeElement.hasAttribute('cmdk-input') || document.activeElement.hasAttribute('cmdk-root')) {
const input = document.getElementById(inputId)
if (input) input.focus()
else document.getElementById(listId)?.focus()
}
schedule(7, () => {
state.current.selectedItemId = getSelectedItem()?.id
store.emit()
})
// opts is a boolean referring to whether it should NOT be scrolled into view
if (!opts) {
// Scroll the selected item into view
schedule(5, scrollSelectedIntoView)
}
if (propsRef.current?.value !== undefined) {
// If controlled, just call the callback instead of updating state internally
const newValue = (value ?? '') as string
propsRef.current.onValueChange?.(newValue)
return
}
}
// Notify subscribers that state has changed
store.emit()
},
emit: () => {
listeners.current.forEach((l) => l())
},
}
}, [])
const context: Context = React.useMemo(
() => ({
// Keep id → {value, keywords} mapping up-to-date
value: (id, value, keywords) => {
if (value !== ids.current.get(id)?.value) {
ids.current.set(id, { value, keywords })
state.current.filtered.items.set(id, score(value, keywords))
schedule(2, () => {
sort()
store.emit()
})
}
},
// Track item lifecycle (mount, unmount)
item: (id, groupId) => {
allItems.current.add(id)
// Track this item within the group
if (groupId) {
if (!allGroups.current.has(groupId)) {
allGroups.current.set(groupId, new Set([id]))
} else {
allGroups.current.get(groupId).add(id)
}
}
// Batch this, multiple items can mount in one pass
// and we should not be filtering/sorting/emitting each time
schedule(3, () => {
filterItems()
sort()
// Could be initial mount, select the first item if none already selected
if (!state.current.value) {
selectFirstItem()
}
store.emit()
})
return () => {
ids.current.delete(id)
allItems.current.delete(id)
state.current.filtered.items.delete(id)
const selectedItem = getSelectedItem()
// Batch this, multiple items could be removed in one pass
schedule(4, () => {
filterItems()
// The item removed have been the selected one,
// so selection should be moved to the first
if (selectedItem?.getAttribute('id') === id) selectFirstItem()
store.emit()
})
}
},
// Track group lifecycle (mount, unmount)
group: (id) => {
if (!allGroups.current.has(id)) {
allGroups.current.set(id, new Set())
}
return () => {
ids.current.delete(id)
allGroups.current.delete(id)
}
},
filter: () => {
return propsRef.current.shouldFilter
},
label: label || props['aria-label'],
getDisablePointerSelection: () => {
return propsRef.current.disablePointerSelection
},
listId,
inputId,
labelId,
listInnerRef,
}),
[],
)
function score(value: string, keywords?: string[]) {
const filter = propsRef.current?.filter ?? defaultFilter
return value ? filter(value, state.current.search, keywords) : 0
}
/** Sorts items by score, and groups by highest item score. */
function sort() {
if (
!state.current.search ||
// Explicitly false, because true | undefined is the default
propsRef.current.shouldFilter === false
) {
return
}
const scores = state.current.filtered.items
// Sort the groups
const groups: [string, number][] = []
state.current.filtered.groups.forEach((value) => {
const items = allGroups.current.get(value)
// Get the maximum score of the group's items
let max = 0
items.forEach((item) => {
const score = scores.get(item)
max = Math.max(score, max)
})
groups.push([value, max])
})
// Sort items within groups to bottom
// Sort items outside of groups
// Sort groups to bottom (pushes all non-grouped items to the top)
const listInsertionElement = listInnerRef.current
// Sort the items
getValidItems()
.sort((a, b) => {
const valueA = a.getAttribute('id')
const valueB = b.getAttribute('id')
return (scores.get(valueB) ?? 0) - (scores.get(valueA) ?? 0)
})
.forEach((item) => {
const group = item.closest(GROUP_ITEMS_SELECTOR)
if (group) {
group.appendChild(item.parentElement === group ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`))
} else {
listInsertionElement.appendChild(
item.parentElement === listInsertionElement ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`),
)
}
})
groups
.sort((a, b) => b[1] - a[1])
.forEach((group) => {
const element = listInnerRef.current?.querySelector(
`${GROUP_SELECTOR}[${VALUE_ATTR}="${encodeURIComponent(group[0])}"]`,
)
element?.parentElement.appendChild(element)
})
}
function selectFirstItem() {
const item = getValidItems().find((item) => item.getAttribute('aria-disabled') !== 'true')
const value = item?.getAttribute(VALUE_ATTR)
store.setState('value', value || undefined)
}
/** Filters the current items. */
function filterItems() {
if (
!state.current.search ||
// Explicitly false, because true | undefined is the default
propsRef.current.shouldFilter === false
) {
state.current.filtered.count = allItems.current.size
// Do nothing, each item will know to show itself because search is empty
return
}
// Reset the groups
state.current.filtered.groups = new Set()
let itemCount = 0
// Check which items should be included
for (const id of allItems.current) {
const value = ids.current.get(id)?.value ?? ''
const keywords = ids.current.get(id)?.keywords ?? []
const rank = score(value, keywords)
state.current.filtered.items.set(id, rank)
if (rank > 0) itemCount++
}
// Check which groups have at least 1 item shown
for (const [groupId, group] of allGroups.current) {
for (const itemId of group) {
if (state.current.filtered.items.get(itemId) > 0) {
state.current.filtered.groups.add(groupId)
break
}
}
}
state.current.filtered.count = itemCount
}
function scrollSelectedIntoView() {
const item = getSelectedItem()
if (item) {
if (item.parentElement?.firstChild === item) {
// First item in Group, ensure heading is in view
item.closest(GROUP_SELECTOR)?.querySelector(GROUP_HEADING_SELECTOR)?.scrollIntoView({ block: 'nearest' })
}
// Ensure the item is always in view
item.scrollIntoView({ block: 'nearest' })
}
}
/** Getters */
function getSelectedItem() {
return listInnerRef.current?.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`)
}
function getValidItems() {
return Array.from(listInnerRef.current?.querySelectorAll(VALID_ITEM_SELECTOR) || [])
}
/** Setters */
function updateSelectedToIndex(index: number) {
const items = getValidItems()
const item = items[index]
if (item) store.setState('value', item.getAttribute(VALUE_ATTR))
}
function updateSelectedByItem(change: 1 | -1) {
const selected = getSelectedItem()
const items = getValidItems()
const index = items.findIndex((item) => item === selected)
// Get item at this index
let newSelected = items[index + change]
if (propsRef.current?.loop) {
newSelected =
index + change < 0
? items[items.length - 1]
: index + change === items.length
? items[0]
: items[index + change]
}
if (newSelected) store.setState('value', newSelected.getAttribute(VALUE_ATTR))
}
function updateSelectedByGroup(change: 1 | -1) {
const selected = getSelectedItem()
let group = selected?.closest(GROUP_SELECTOR)
let item: HTMLElement
while (group && !item) {
group = change > 0 ? findNextSibling(group, GROUP_SELECTOR) : findPreviousSibling(group, GROUP_SELECTOR)
item = group?.querySelector(VALID_ITEM_SELECTOR)
}
if (item) {
store.setState('value', item.getAttribute(VALUE_ATTR))
} else {
updateSelectedByItem(change)
}
}
const last = () => updateSelectedToIndex(getValidItems().length - 1)
const next = (e: React.KeyboardEvent) => {
e.preventDefault()
if (e.metaKey) {
// Last item
last()
} else if (e.altKey) {
// Next group
updateSelectedByGroup(1)
} else {
// Next item
updateSelectedByItem(1)
}
}
const prev = (e: React.KeyboardEvent) => {
e.preventDefault()
if (e.metaKey) {
// First item
updateSelectedToIndex(0)
} else if (e.altKey) {
// Previous group
updateSelectedByGroup(-1)
} else {
// Previous item
updateSelectedByItem(-1)
}
}
return (
<Primitive.div
ref={forwardedRef}
tabIndex={-1}
{...etc}
cmdk-root=""
onKeyDown={(e) => {
etc.onKeyDown?.(e)
// Check if IME composition is finished before triggering key binds
// This prevents unwanted triggering while user is still inputting text with IME
// e.keyCode === 229 is for the CJK IME with Legacy Browser [https://w3c.github.io/uievents/#determine-keydown-keyup-keyCode]
// isComposing is for the CJK IME with Modern Browser [https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/isComposing]
const isComposing = e.nativeEvent.isComposing || e.keyCode === 229
if (e.defaultPrevented || isComposing) {
return
}
switch (e.key) {
case 'n':
case 'j': {
// vim keybind down
if (vimBindings && e.ctrlKey) {
next(e)
}
break
}
case 'ArrowDown': {
next(e)
break
}
case 'p':
case 'k': {
// vim keybind up
if (vimBindings && e.ctrlKey) {
prev(e)
}
break
}
case 'ArrowUp': {
prev(e)
break
}
case 'Home': {
// First item
e.preventDefault()
updateSelectedToIndex(0)
break
}
case 'End': {
// Last item
e.preventDefault()
last()
break
}
case 'Enter': {
// Trigger item onSelect
e.preventDefault()
const item = getSelectedItem()
if (item) {
const event = new Event(SELECT_EVENT)
item.dispatchEvent(event)
}
}
}
}}
>
<label
cmdk-label=""
htmlFor={context.inputId}
id={context.labelId}
// Screen reader only
style={srOnlyStyles}
>
{label}
</label>
{SlottableWithNestedChildren(props, (child) => (
<StoreContext.Provider value={store}>
<CommandContext.Provider value={context}>{child}</CommandContext.Provider>
</StoreContext.Provider>
))}
</Primitive.div>
)
})
/**
* Command menu item. Becomes active on pointer enter or through keyboard navigation.
* Preferably pass a `value`, otherwise the value will be inferred from `children` or
* the rendered item's `textContent`.
*/
const Item = React.forwardRef<HTMLDivElement, ItemProps>((props, forwardedRef) => {
const id = useId()
const ref = React.useRef<HTMLDivElement>(null)
const groupContext = React.useContext(GroupContext)
const context = useCommand()
const propsRef = useAsRef(props)
const forceMount = propsRef.current?.forceMount ?? groupContext?.forceMount
useLayoutEffect(() => {
if (!forceMount) {
return context.item(id, groupContext?.id)
}
}, [forceMount])
const value = useValue(id, ref, [props.value, props.children, ref], props.keywords)
const store = useStore()
const selected = useCmdk((state) => state.value && state.value === value.current)
const render = useCmdk((state) =>
forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.items.get(id) > 0,
)
React.useEffect(() => {
const element = ref.current
if (!element || props.disabled) return
element.addEventListener(SELECT_EVENT, onSelect)
return () => element.removeEventListener(SELECT_EVENT, onSelect)
}, [render, props.onSelect, props.disabled])
function onSelect() {
select()
propsRef.current.onSelect?.(value.current)
}
function select() {
store.setState('value', value.current, true)
}
if (!render) return null
const { disabled, value: _, onSelect: __, forceMount: ___, keywords: ____, ...etc } = props
return (
<Primitive.div
ref={composeRefs(ref, forwardedRef)}
{...etc}
id={id}
cmdk-item=""
role="option"
aria-disabled={Boolean(disabled)}
aria-selected={Boolean(selected)}
data-disabled={Boolean(disabled)}
data-selected={Boolean(selected)}
onPointerMove={disabled || context.getDisablePointerSelection() ? undefined : select}
onClick={disabled ? undefined : onSelect}
>
{props.children}
</Primitive.div>
)
})
/**
* Group command menu items together with a heading.
* Grouped items are always shown together.
*/
const Group = React.forwardRef<HTMLDivElement, GroupProps>((props, forwardedRef) => {
const { heading, children, forceMount, ...etc } = props
const id = useId()
const ref = React.useRef<HTMLDivElement>(null)
const headingRef = React.useRef<HTMLDivElement>(null)
const headingId = useId()
const context = useCommand()
const render = useCmdk((state) =>
forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.groups.has(id),
)
useLayoutEffect(() => {
return context.group(id)
}, [])
useValue(id, ref, [props.value, props.heading, headingRef])
const contextValue = React.useMemo(() => ({ id, forceMount }), [forceMount])
return (
<Primitive.div
ref={composeRefs(ref, forwardedRef)}
{...etc}
cmdk-group=""
role="presentation"
hidden={render ? undefined : true}
>
{heading && (
<div ref={headingRef} cmdk-group-heading="" aria-hidden id={headingId}>
{heading}
</div>
)}
{SlottableWithNestedChildren(props, (child) => (
<div cmdk-group-items="" role="group" aria-labelledby={heading ? headingId : undefined}>
<GroupContext.Provider value={contextValue}>{child}</GroupContext.Provider>
</div>
))}
</Primitive.div>
)
})
/**
* A visual and semantic separator between items or groups.
* Visible when the search query is empty or `alwaysRender` is true, hidden otherwise.
*/
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>((props, forwardedRef) => {
const { alwaysRender, ...etc } = props
const ref = React.useRef<HTMLDivElement>(null)
const render = useCmdk((state) => !state.search)
if (!alwaysRender && !render) return null
return <Primitive.div ref={composeRefs(ref, forwardedRef)} {...etc} cmdk-separator="" role="separator" />
})
/**
* Command menu input.
* All props are forwarded to the underyling `input` element.
*/
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, forwardedRef) => {
const { onValueChange, ...etc } = props
const isControlled = props.value != null
const store = useStore()
const search = useCmdk((state) => state.search)
const selectedItemId = useCmdk((state) => state.selectedItemId)
const context = useCommand()
React.useEffect(() => {
if (props.value != null) {
store.setState('search', props.value)
}
}, [props.value])
return (
<Primitive.input
ref={forwardedRef}
{...etc}
cmdk-input=""
autoComplete="off"
autoCorrect="off"
spellCheck={false}
aria-autocomplete="list"
role="combobox"
aria-expanded={true}
aria-controls={context.listId}
aria-labelledby={context.labelId}
aria-activedescendant={selectedItemId}
id={context.inputId}
type="text"
value={isControlled ? props.value : search}
onChange={(e) => {
if (!isControlled) {
store.setState('search', e.target.value)
}
onValueChange?.(e.target.value)
}}
/>
)
})
/**
* Contains `Item`, `Group`, and `Separator`.
* Use the `--cmdk-list-height` CSS variable to animate height based on the number of results.
*/
const List = React.forwardRef<HTMLDivElement, ListProps>((props, forwardedRef) => {
const { children, label = 'Suggestions', ...etc } = props
const ref = React.useRef<HTMLDivElement>(null)
const height = React.useRef<HTMLDivElement>(null)
const selectedItemId = useCmdk((state) => state.selectedItemId)
const context = useCommand()
React.useEffect(() => {
if (height.current && ref.current) {
const el = height.current
const wrapper = ref.current
let animationFrame
const observer = new ResizeObserver(() => {
animationFrame = requestAnimationFrame(() => {
const height = el.offsetHeight
wrapper.style.setProperty(`--cmdk-list-height`, height.toFixed(1) + 'px')
})
})
observer.observe(el)
return () => {
cancelAnimationFrame(animationFrame)
observer.unobserve(el)
}
}
}, [])
return (
<Primitive.div
ref={composeRefs(ref, forwardedRef)}
{...etc}
cmdk-list=""
role="listbox"
tabIndex={-1}
aria-activedescendant={selectedItemId}
aria-label={label}
id={context.listId}
>
{SlottableWithNestedChildren(props, (child) => (
<div ref={composeRefs(height, context.listInnerRef)} cmdk-list-sizer="">
{child}
</div>
))}
</Primitive.div>
)
})
/**
* Renders the command menu in a Radix Dialog.
*/
const Dialog = React.forwardRef<HTMLDivElement, DialogProps>((props, forwardedRef) => {
const { open, onOpenChange, overlayClassName, contentClassName, container, ...etc } = props
return (
<RadixDialog.Root open={open} onOpenChange={onOpenChange}>
<RadixDialog.Portal container={container}>
<RadixDialog.Overlay cmdk-overlay="" className={overlayClassName} />
<RadixDialog.Content aria-label={props.label} cmdk-dialog="" className={contentClassName}>
<Command ref={forwardedRef} {...etc} />
</RadixDialog.Content>
</RadixDialog.Portal>
</RadixDialog.Root>
)
})
/**
* Automatically renders when there are no results for the search query.
*/
const Empty = React.forwardRef<HTMLDivElement, EmptyProps>((props, forwardedRef) => {
const render = useCmdk((state) => state.filtered.count === 0)
if (!render) return null
return <Primitive.div ref={forwardedRef} {...props} cmdk-empty="" role="presentation" />
})
/**
* You should conditionally render this with `progress` while loading asynchronous items.
*/
const Loading = React.forwardRef<HTMLDivElement, LoadingProps>((props, forwardedRef) => {
const { progress, children, label = 'Loading...', ...etc } = props
return (
<Primitive.div
ref={forwardedRef}
{...etc}
cmdk-loading=""
role="progressbar"
aria-valuenow={progress}
aria-valuemin={0}
aria-valuemax={100}
aria-label={label}
>
{SlottableWithNestedChildren(props, (child) => (
<div aria-hidden>{child}</div>
))}
</Primitive.div>
)
})
const pkg = Object.assign(Command, {
List,
Item,
Input,
Group,
Separator,
Dialog,
Empty,
Loading,
})
export { useCmdk as useCommandState }
export { pkg as Command }
export { defaultFilter }
export { Command as CommandRoot }
export { List as CommandList }
export { Item as CommandItem }
export { Input as CommandInput }
export { Group as CommandGroup }
export { Separator as CommandSeparator }
export { Dialog as CommandDialog }
export { Empty as CommandEmpty }
export { Loading as CommandLoading }
/**
*
*
* Helpers
*
*
*/
function findNextSibling(el: Element, selector: string) {
let sibling = el.nextElementSibling
while (sibling) {
if (sibling.matches(selector)) return sibling
sibling = sibling.nextElementSibling
}
}
function findPreviousSibling(el: Element, selector: string) {
let sibling = el.previousElementSibling
while (sibling) {
if (sibling.matches(selector)) return sibling
sibling = sibling.previousElementSibling
}
}
function useAsRef<T>(data: T) {
const ref = React.useRef<T>(data)
useLayoutEffect(() => {
ref.current = data
})
return ref
}
const useLayoutEffect = typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
function useLazyRef<T>(fn: () => T) {
const ref = React.useRef<T>()
if (ref.current === undefined) {
ref.current = fn()
}
return ref as React.MutableRefObject<T>
}
/** Run a selector against the store state. */
function useCmdk<T = any>(selector: (state: State) => T): T {
const store = useStore()
const cb = () => selector(store.snapshot())
return React.useSyncExternalStore(store.subscribe, cb, cb)
}
function useValue(
id: string,
ref: React.RefObject<HTMLElement>,
deps: (string | React.ReactNode | React.RefObject<HTMLElement>)[],
aliases: string[] = [],
) {
const valueRef = React.useRef<string>()
const context = useCommand()
useLayoutEffect(() => {
const value = (() => {
for (const part of deps) {
if (typeof part === 'string') {
return part.trim()
}
if (typeof part === 'object' && 'current' in part) {
if (part.current) {
return part.current.textContent?.trim()
}
return valueRef.current
}
}
})()
const keywords = aliases.map((alias) => alias.trim())
context.value(id, value, keywords)
ref.current?.setAttribute(VALUE_ATTR, value)
valueRef.current = value
})
return valueRef
}
/** Imperatively run a function on the next layout effect cycle. */
const useScheduleLayoutEffect = () => {
const [s, ss] = React.useState<object>()
const fns = useLazyRef(() => new Map<string | number, () => void>())
useLayoutEffect(() => {
fns.current.forEach((f) => f())
fns.current = new Map()
}, [s])
return (id: string | number, cb: () => void) => {
fns.current.set(id, cb)
ss({})
}
}
function renderChildren(children: React.ReactElement) {
const childrenType = children.type as any
// The children is a component
if (typeof childrenType === 'function') return childrenType(children.props)
// The children is a component with `forwardRef`
else if ('render' in childrenType) return childrenType.render(children.props)
// It's a string, boolean, etc.
else return children
}
function SlottableWithNestedChildren(
{ asChild, children }: { asChild?: boolean; children?: React.ReactNode },
render: (child: React.ReactNode) => JSX.Element,
) {
if (asChild && React.isValidElement(children)) {
return React.cloneElement(renderChildren(children), { ref: (children as any).ref }, render(children.props.children))
}
return render(children)
}
const srOnlyStyles = {
position: 'absolute',
width: '1px',
height: '1px',
padding: '0',
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
borderWidth: '0',
} as const
================================================
FILE: cmdk/tsup.config.ts
================================================
import { defineConfig } from 'tsup'
export default defineConfig({
sourcemap: false,
minify: true,
dts: true,
format: ['esm', 'cjs'],
loader: {
'.js': 'jsx',
},
})
================================================
FILE: package.json
================================================
{
"name": "cmdk-root",
"private": true,
"scripts": {
"build": "pnpm -F cmdk build",
"dev": "pnpm -F cmdk build --watch",
"website": "pnpm -F cmdk-website dev",
"testsite": "pnpm -F cmdk-tests dev",
"format": "prettier '**/*.{js,jsx,ts,tsx,json,md,mdx,css,scss,yaml,yml}' --write",
"preinstall": "npx only-allow pnpm",
"test:format": "prettier '**/*.{js,jsx,ts,tsx,json,md,mdx,css,scss,yaml,yml}' --check",
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "1.51.0",
"husky": "^8.0.1",
"lint-staged": "15.2.0",
"prettier": "2.7.1",
"tsup": "8.0.1",
"typescript": "4.6.4"
},
"packageManager": "pnpm@8.8.0",
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json,md,mdx,css,scss,yaml,yml}": [
"prettier --write"
]
}
}
================================================
FILE: playwright.config.ts
================================================
import { PlaywrightTestConfig, devices } from '@playwright/test'
const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [['github'], ['json', { outputFile: 'playwright-report.json' }]] : 'list',
testDir: './test',
use: {
trace: 'on-first-retry',
baseURL: 'http://localhost:3000',
},
timeout: 5000,
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
cwd: './test',
reuseExistingServer: !process.env.CI,
},
projects: [
{
name: 'webkit',
use: { ...devices['Desktop Safari'], headless: true },
},
],
}
export default config
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- 'website'
- 'test'
- 'cmdk'
================================================
FILE: test/basic.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('basic behavior', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})
test('input props are forwarded', async ({ page }) => {
const input = page.locator(`input[placeholder="Search…"]`)
await expect(input).toHaveCount(1)
})
test('item value is derived from textContent', async ({ page }) => {
const item = page.locator(`[cmdk-item][data-value="Item"]`)
await expect(item).toHaveText('Item')
})
test('item value prop is preferred over textContent', async ({ page }) => {
const item = page.locator(`[cmdk-item][data-value="xxx"]`)
await expect(item).toHaveText('Value')
})
test('item onSelect is called on click', async ({ page }) => {
const item = page.locator(`[cmdk-item][data-value="Item"]`)
await item.click()
expect(await page.evaluate(() => (window as any).onSelect)).toEqual('Item selected')
})
test('first item is selected by default', async ({ page }) => {
const item = page.locator(`[cmdk-item][aria-selected="true"]`)
await expect(item).toHaveText('Item')
})
test('first item is selected when search changes', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('x')
const selected = page.locator(`[cmdk-item][aria-selected="true"]`)
await expect(selected).toHaveText('Value')
})
test('items filter when searching', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('x')
const removed = page.locator(`[cmdk-item][data-value="Item"]`)
const remains = page.locator(`[cmdk-item][data-value="xxx"]`)
await expect(removed).toHaveCount(0)
await expect(remains).toHaveCount(1)
})
test('items filter when searching by keywords', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('key')
const removed = page.locator(`[cmdk-item][data-value="xxx"]`)
const remains = page.locator(`[cmdk-item][data-value="Item"]`)
await expect(removed).toHaveCount(0)
await expect(remains).toHaveCount(1)
})
test('empty component renders when there are no results', async ({ page }) => {
const input = page.locator('[cmdk-input]')
await input.type('z')
await expect(page.locator(`[cmdk-item]`)).toHaveCount(0)
await expect(page.locator(`[cmdk-empty]`)).toHaveText('No results.')
})
test('className is applied to each part', async ({ page }) => {
await expect(page.locator(`.root`)).toHaveCount(1)
await expect(page.locator(`.input`)).toHaveCount(1)
await expect(page.locator(`.list`)).toHaveCount(1)
await expect(page.locator(`.item`)).toHaveCount(2)
await page.locator('[cmdk-input]').type('zzzz')
await expect(page.locator(`.item`)).toHaveCount(0)
await expect(page.locator(`.empty`)).toHaveCount(1)
})
})
================================================
FILE: test/dialog.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('dialog', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/dialog')
})
test('dialog renders in portal', async ({ page }) => {
await expect(page.locator(`[cmdk-dialog]`)).toHaveCount(1)
await expect(page.locator(`[cmdk-overlay]`)).toHaveCount(1)
})
})
================================================
FILE: test/group.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('group', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/group')
})
test('groups are shown/hidden based on item matches', async ({ page }) => {
await page.locator(`[cmdk-input]`).type('z')
await expect(page.locator(`[cmdk-group][data-value="Animals"]`)).not.toBeVisible()
await expect(page.locator(`[cmdk-group][data-value="Letters"]`)).toBeVisible()
})
test('group can be progressively rendered', async ({ page }) => {
await expect(page.locator(`[cmdk-group][data-value="Numbers"]`)).not.toBeVisible()
await page.locator(`[cmdk-input]`).type('t')
await expect(page.locator(`[cmdk-group][data-value="Animals"]`)).not.toBeVisible()
await expect(page.locator(`[cmdk-group][data-value="Letters"]`)).not.toBeVisible()
await expect(page.locator(`[cmdk-group][data-value="Numbers"]`)).toBeVisible()
})
test('mounted group still rendered with filter using forceMount', async ({ page }) => {
await page.locator(`data-testid=forceMount`).click()
await page.locator(`[cmdk-input]`).type('Giraffe')
await expect(page.locator(`[cmdk-group][data-value="Letters"]`)).toBeVisible()
})
})
================================================
FILE: test/item.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('item', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/item')
})
test('mounted item matches search', async ({ page }) => {
await page.locator(`[cmdk-input]`).type('b')
await expect(page.locator(`[cmdk-item]`)).toHaveCount(0)
await page.locator(`data-testid=mount`).click()
await expect(page.locator(`[cmdk-item]`)).toHaveText('B')
})
test('mounted item does not match search', async ({ page }) => {
await page.locator(`[cmdk-input]`).type('z')
await expect(page.locator(`[cmdk-item]`)).toHaveCount(0)
await page.locator(`data-testid=mount`).click()
await expect(page.locator(`[cmdk-item]`)).toHaveCount(0)
})
test('unmount item that is selected', async ({ page }) => {
await page.locator(`data-testid=mount`).click()
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('A')
await page.locator(`data-testid=unmount`).click()
await expect(page.locator(`[cmdk-item]`)).toHaveCount(1)
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('B')
})
test('unmount item that is the only result', async ({ page }) => {
await page.locator(`data-testid=unmount`).click()
await expect(page.locator(`[cmdk-item]`)).toHaveCount(0)
})
test('mount item that is the only result', async ({ page }) => {
await page.locator(`data-testid=unmount`).click()
await expect(page.locator(`[cmdk-empty]`)).toHaveCount(1)
await page.locator(`data-testid=mount`).click()
await expect(page.locator(`[cmdk-empty]`)).toHaveCount(0)
await expect(page.locator(`[cmdk-item]`)).toHaveCount(1)
})
test('selected does not change when mounting new items', async ({ page }) => {
await page.locator(`data-testid=mount`).click()
await page.locator(`[cmdk-item][data-value="B"]`).click()
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('B')
await page.locator(`data-testid=many`).click()
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('B')
})
test('mounted item still rendered with filter usingForceMount', async ({ page }) => {
await page.locator(`data-testid=forceMount`).click()
await page.locator(`[cmdk-input]`).type('z')
await expect(page.locator(`[cmdk-item]`)).toHaveCount(1)
})
})
test.describe('item advanced', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/item-advanced')
})
test('re-rendering re-matches implicit textContent value', async ({ page }) => {
await expect(page.locator(`[cmdk-item]`)).toHaveCount(2)
await page.locator(`[cmdk-input]`).type('2')
const button = page.locator(`data-testid=increment`)
await button.click()
await expect(page.locator(`[cmdk-item]`)).toHaveCount(0)
await button.click()
await expect(page.locator(`[cmdk-item]`)).toHaveCount(2)
})
})
================================================
FILE: test/keybind.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('arrow keybinds', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/keybinds')
})
test('arrow up/down changes selected item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('ArrowDown')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('ArrowUp')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('meta arrow up/down goes to first and last item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Meta+ArrowDown')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'last')
await page.locator(`[cmdk-input]`).press('Meta+ArrowUp')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('alt arrow up/down goes to next and prev item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Alt+ArrowDown')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Alt+ArrowDown')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'Apple')
await page.locator(`[cmdk-input]`).press('Alt+ArrowUp')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Alt+ArrowUp')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
})
test.describe('vim jk keybinds', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/keybinds')
})
test('ctrl j/k changes selected item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Control+j')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Control+k')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('meta ctrl j/k goes to first and last item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Meta+Control+j')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'last')
await page.locator(`[cmdk-input]`).press('Meta+Control+k')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('alt ctrl j/k goes to next and prev item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Alt+Control+j')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Alt+Control+j')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'Apple')
await page.locator(`[cmdk-input]`).press('Alt+Control+k')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Alt+Control+k')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
})
test.describe('vim np keybinds', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/keybinds')
})
test('ctrl n/p changes selected item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Control+n')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Control+p')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('meta ctrl n/p goes to first and last item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Meta+Control+n')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'last')
await page.locator(`[cmdk-input]`).press('Meta+Control+p')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('alt ctrl n/p goes to next and prev item', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Alt+Control+n')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Alt+Control+n')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'Apple')
await page.locator(`[cmdk-input]`).press('Alt+Control+p')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A')
await page.locator(`[cmdk-input]`).press('Alt+Control+p')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
})
test.describe('no-vim keybinds', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/keybinds?noVim=true')
})
test('ctrl j/k does nothing', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Control+j')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Control+k')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
test('ctrl n/p does nothing', async ({ page }) => {
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Control+n')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
await page.locator(`[cmdk-input]`).press('Control+p')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first')
})
})
================================================
FILE: test/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
================================================
FILE: test/numeric.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('behavior for numeric values', async () => {
test.beforeEach(async ({ page }) => {
await page.goto('/numeric')
})
test('items filter correctly on numeric inputs', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('112')
const removed = page.locator(`[cmdk-item][data-value="removed"]`)
const remains = page.locator(`[cmdk-item][data-value="foo.bar112.value"]`)
await expect(removed).toHaveCount(0)
await expect(remains).toHaveCount(1)
})
test('items filter correctly on non-numeric inputs', async ({ page }) => {
const input = page.locator(`[cmdk-input]`)
await input.type('bar')
const removed = page.locator(`[cmdk-item][data-value="removed"]`)
const remains = page.locator(`[cmdk-item][data-value="foo.bar112.value"]`)
await expect(removed).toHaveCount(0)
await expect(remains).toHaveCount(1)
})
})
================================================
FILE: test/package.json
================================================
{
"name": "cmdk-tests",
"version": "0.0.0",
"scripts": {
"dev": "next"
},
"dependencies": {
"@radix-ui/react-portal": "^1.0.4",
"@types/node": "18.0.4",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"cmdk": "workspace:*",
"next": "13.5.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.7.4"
}
}
================================================
FILE: test/pages/_app.tsx
================================================
import '../style.css'
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
================================================
FILE: test/pages/dialog.tsx
================================================
import { Command } from 'cmdk'
import * as React from 'react'
const Page = () => {
const [open, setOpen] = React.useState(false)
React.useEffect(() => {
setOpen(true)
}, [])
return (
<div>
<Command.Dialog open={open} onOpenChange={setOpen}>
<Command.Input placeholder="Search…" />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Item onSelect={() => console.log('Item selected')}>Item</Command.Item>
<Command.Item value="xxx">Value</Command.Item>
</Command.List>
</Command.Dialog>
</div>
)
}
export default Page
================================================
FILE: test/pages/group.tsx
================================================
import { Command } from 'cmdk'
import * as React from 'react'
const Page = () => {
const [search, setSearch] = React.useState('')
const [forceMount, setForceMount] = React.useState(false)
return (
<div>
<button data-testid="forceMount" onClick={() => setForceMount(!forceMount)}>
Force mount Group Letters
</button>
<Command>
<Command.Input placeholder="Search…" value={search} onValueChange={setSearch} />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Group heading="Animals">
<Command.Item>Giraffe</Command.Item>
<Command.Item>Chicken</Command.Item>
</Command.Group>
<Command.Group forceMount={forceMount} heading="Letters">
<Command.Item>A</Command.Item>
<Command.Item>B</Command.Item>
<Command.Item>Z</Command.Item>
</Command.Group>
{!!search && (
<Command.Group heading="Numbers">
<Command.Item>One</Command.Item>
<Command.Item>Two</Command.Item>
<Command.Item>Three</Command.Item>
</Command.Group>
)}
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/huge.tsx
================================================
import { Command } from 'cmdk'
import * as React from 'react'
const items = new Array(1000).fill(0)
const Page = () => {
return (
<div>
<React.Profiler
id="huge-command"
onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) => {
console.log({ phase, actualDuration, baseDuration })
}}
>
<Command>
<Command.Input placeholder="Search…" />
<Command.List>
{items.map((_, i) => {
return <Item key={`item-${i}`} />
})}
</Command.List>
</Command>
</React.Profiler>
</div>
)
}
const Item = () => {
const id = React.useId()
return <Command.Item key={id}>Item {id}</Command.Item>
}
export default Page
================================================
FILE: test/pages/index.tsx
================================================
import { Command } from 'cmdk'
const Page = () => {
return (
<div>
<Command className="root">
<Command.Input placeholder="Search…" className="input" />
<Command.List className="list">
<Command.Empty className="empty">No results.</Command.Empty>
<Command.Item
keywords={['key']}
onSelect={() => {
;(window as any).onSelect = 'Item selected'
}}
className="item"
>
Item
</Command.Item>
<Command.Item value="xxx" className="item">
Value
</Command.Item>
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/item-advanced.tsx
================================================
import { Command } from 'cmdk'
import * as React from 'react'
const Page = () => {
const [count, setCount] = React.useState(0)
return (
<div>
<button data-testid="increment" onClick={() => setCount((c) => c + 1)}>
Increment count
</button>
<Command>
<Command.Input placeholder="Search…" />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Item value={`Item A ${count}`}>Item A {count}</Command.Item>
<Command.Item value={`Item B ${count}`}>Item B {count}</Command.Item>
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/item.tsx
================================================
import { Command } from 'cmdk'
import * as React from 'react'
const Page = () => {
const [unmount, setUnmount] = React.useState(false)
const [mount, setMount] = React.useState(false)
const [many, setMany] = React.useState(false)
const [forceMount, setForceMount] = React.useState(false)
return (
<div>
<button data-testid="mount" onClick={() => setMount(!mount)}>
Toggle item B
</button>
<button data-testid="unmount" onClick={() => setUnmount(!unmount)}>
Toggle item A
</button>
<button data-testid="many" onClick={() => setMany(!many)}>
Toggle many items
</button>
<button data-testid="forceMount" onClick={() => setForceMount(!forceMount)}>
Force mount item A
</button>
<Command>
<Command.Input placeholder="Search…" />
<Command.List>
<Command.Empty>No results.</Command.Empty>
{!unmount && <Command.Item forceMount={forceMount}>A</Command.Item>}
{many && (
<>
<Command.Item>1</Command.Item>
<Command.Item>2</Command.Item>
<Command.Item>3</Command.Item>
</>
)}
{mount && <Command.Item>B</Command.Item>}
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/keybinds.tsx
================================================
import { Command } from 'cmdk'
import { useRouter } from 'next/router'
import * as React from 'react'
const Page = () => {
const {
query: { noVim },
} = useRouter()
return (
<div>
<Command vimBindings={!noVim}>
<Command.Input />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Item value="disabled" disabled>
Disabled
</Command.Item>
<Command.Item value="first">First</Command.Item>
<Command.Group heading="Letters">
<Command.Item>A</Command.Item>
<Command.Item>B</Command.Item>
<Command.Item>Z</Command.Item>
</Command.Group>
<Command.Group heading="Fruits">
<Command.Item>Apple</Command.Item>
<Command.Item>Banana</Command.Item>
<Command.Item>Orange</Command.Item>
<Command.Item disabled>Dragon Fruit</Command.Item>
<Command.Item>Pear</Command.Item>
</Command.Group>
<Command.Item value="last">Last</Command.Item>
<Command.Item value="disabled-3" disabled>
Disabled 3
</Command.Item>
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/numeric.tsx
================================================
import { Command } from 'cmdk'
const Page = () => {
return (
<div>
<Command className="root">
<Command.Input placeholder="Search…" className="input" />
<Command.List className="list">
<Command.Empty className="empty">No results.</Command.Empty>
<Command.Item value="removed" className="item">
To be removed
</Command.Item>
<Command.Item value="foo.bar112.value" className="item">
Not to be removed
</Command.Item>
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/portal.tsx
================================================
import * as React from 'react'
import { Command } from 'cmdk'
import * as Portal from '@radix-ui/react-portal'
const Page = () => {
const [render, setRender] = React.useState(false)
const [search, setSearch] = React.useState('')
const [open, setOpen] = React.useState(true)
React.useEffect(() => setRender(true), [])
if (!render) return null
return (
<div>
<button data-testid="controlledSearch" onClick={() => setSearch('eat')}>
Change search value
</button>
<button data-testid="openClosePopover" onClick={() => setOpen((val) => !val)}>
{open ? 'Close' : 'Open'}
</button>
<Command className="root">
<Command.Input value={search} onValueChange={setSearch} placeholder="Search…" className="input" />
<Portal.Root data-portal="true">
{open && (
<Command.List className="list">
<Command.Item className="item">Apple</Command.Item>
<Command.Item className="item">Banana</Command.Item>
<Command.Item className="item">Cherry</Command.Item>
<Command.Item className="item">Dragonfruit</Command.Item>
<Command.Item className="item">Elderberry</Command.Item>
<Command.Item className="item">Fig</Command.Item>
<Command.Item className="item">Grape</Command.Item>
<Command.Item className="item">Honeydew</Command.Item>
<Command.Item className="item">Jackfruit</Command.Item>
<Command.Item className="item">Kiwi</Command.Item>
<Command.Item className="item">Lemon</Command.Item>
<Command.Item className="item">Mango</Command.Item>
<Command.Item className="item">Nectarine</Command.Item>
<Command.Item className="item">Orange</Command.Item>
<Command.Item className="item">Papaya</Command.Item>
<Command.Item className="item">Quince</Command.Item>
<Command.Item className="item">Raspberry</Command.Item>
<Command.Item className="item">Strawberry</Command.Item>
<Command.Item className="item">Tangerine</Command.Item>
<Command.Item className="item">Ugli</Command.Item>
<Command.Item className="item">Watermelon</Command.Item>
<Command.Item className="item">Xigua</Command.Item>
<Command.Item className="item">Yuzu</Command.Item>
<Command.Item className="item">Zucchini</Command.Item>
</Command.List>
)}
</Portal.Root>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/pages/props.tsx
================================================
import { Command } from 'cmdk'
import { useRouter } from 'next/router'
import * as React from 'react'
const Page = () => {
const [value, setValue] = React.useState('ant')
const [search, setSearch] = React.useState('')
const [shouldFilter, setShouldFilter] = React.useState(true)
const [customFilter, setCustomFilter] = React.useState(false)
const router = useRouter()
React.useEffect(() => {
if (router.isReady) {
setShouldFilter(router.query.shouldFilter === 'false' ? false : true)
setCustomFilter(router.query.customFilter === 'true' ? true : false)
setValue((router.query.initialValue as string) ?? 'ant')
}
}, [router.isReady])
return (
<div>
<div data-testid="value">{value}</div>
<div data-testid="search">{search}</div>
<button data-testid="controlledValue" onClick={() => setValue('anteater')}>
Change value
</button>
<button data-testid="controlledSearch" onClick={() => setSearch('eat')}>
Change search value
</button>
<Command
shouldFilter={shouldFilter}
value={value}
onValueChange={setValue}
filter={
customFilter
? (item: string | undefined, search: string | undefined) => {
console.log(item, search)
if (!search || !item) return 1
return item.endsWith(search) ? 1 : 0
}
: undefined
}
>
<Command.Input placeholder="Search…" value={search} onValueChange={setSearch} />
<Command.List>
<Command.Item>ant</Command.Item>
<Command.Item>anteater</Command.Item>
</Command.List>
</Command>
</div>
)
}
export default Page
================================================
FILE: test/props.test.ts
================================================
import { expect, test } from '@playwright/test'
test.describe('props', async () => {
test('results do not change when filtering is disabled', async ({ page }) => {
await page.goto('/props?shouldFilter=false')
await expect(page.locator(`[cmdk-item]`)).toHaveCount(2)
await page.locator(`[cmdk-input]`).type('z')
await expect(page.locator(`[cmdk-item]`)).toHaveCount(2)
})
test('results match against custom filter', async ({ page }) => {
await page.goto('/props?customFilter=true')
await page.locator(`[cmdk-input]`).type(`ant`)
await expect(page.locator(`[cmdk-item]`)).toHaveAttribute('data-value', 'ant')
})
test('controlled value', async ({ page }) => {
await page.goto('/props')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'ant')
await page.locator(`data-testid=controlledValue`).click()
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'anteater')
})
test('keep controlled value if empty results', async ({ page }) => {
await page.goto('/props')
await expect(page.locator(`[data-testid=value]`)).toHaveText('ant')
await page.locator(`[cmdk-input]`).fill('d')
await expect(page.locator(`[data-testid=value]`)).toHaveText('')
await page.locator(`[cmdk-input]`).fill('ant')
await expect(page.locator(`[data-testid=value]`)).toHaveText('ant')
})
test('controlled search', async ({ page }) => {
await page.goto('/props')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'ant')
await page.locator(`data-testid=controlledSearch`).click()
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'anteater')
})
test('keep focus on the provided initial value', async ({ page }) => {
await page.goto('/props?initialValue=anteater')
await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'anteater')
})
})
================================================
FILE: test/style.css
================================================
[cmdk-item][aria-selected='true'] {
color: red;
}
================================================
FILE: test/tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../dialog.test.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"noEmit": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
"exclude": ["node_modules", "build", "dist", ".next"]
}
================================================
FILE: website/.eslintrc.json
================================================
{
"extends": "next/core-web-vitals"
}
================================================
FILE: website/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
================================================
FILE: website/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
pnpm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
================================================
FILE: website/components/cmdk/framer.tsx
================================================
import { Command } from 'cmdk'
import React from 'react'
export function FramerCMDK() {
const [value, setValue] = React.useState('Button')
return (
<div className="framer">
<Command value={value} onValueChange={(v) => setValue(v)}>
<div cmdk-framer-header="">
<SearchIcon />
<Command.Input autoFocus placeholder="Find components, packages, and interactions..." />
</div>
<Command.List>
<div cmdk-framer-items="">
<div cmdk-framer-left="">
<Command.Group heading="Components">
<Item value="Button" subtitle="Trigger actions">
<ButtonIcon />
</Item>
<Item value="Input" subtitle="Retrieve user input">
<InputIcon />
</Item>
<Item value="Radio" subtitle="Single choice input">
<RadioIcon />
</Item>
<Item value="Badge" subtitle="Annotate context">
<BadgeIcon />
</Item>
<Item value="Slider" subtitle="Free range picker">
<SliderIcon />
</Item>
<Item value="Avatar" subtitle="Illustrate the user">
<AvatarIcon />
</Item>
<Item value="Container" subtitle="Lay out items">
<ContainerIcon />
</Item>
</Command.Group>
</div>
<hr cmdk-framer-separator="" />
<div cmdk-framer-right="">
{value === 'Button' && <Button />}
{value === 'Input' && <Input />}
{value === 'Badge' && <Badge />}
{value === 'Radio' && <Radio />}
{value === 'Avatar' && <Avatar />}
{value === 'Slider' && <Slider />}
{value === 'Container' && <Container />}
</div>
</div>
</Command.List>
</Command>
</div>
)
}
function Button() {
return <button>Primary</button>
}
function Input() {
return <input type="text" placeholder="Placeholder" />
}
function Badge() {
return <div cmdk-framer-badge="">Badge</div>
}
function Radio() {
return (
<label cmdk-framer-radio="">
<input type="radio" defaultChecked />
Radio Button
</label>
)
}
function Slider() {
return (
<div cmdk-framer-slider="">
<div />
</div>
)
}
function Avatar() {
return <img src="/rauno.jpeg" alt="Avatar of Rauno" />
}
function Container() {
return <div cmdk-framer-container="" />
}
function Item({ children, value, subtitle }: { children: React.ReactNode; value: string; subtitle: string }) {
return (
<Command.Item value={value} onSelect={() => {}}>
<div cmdk-framer-icon-wrapper="">{children}</div>
<div cmdk-framer-item-meta="">
{value}
<span cmdk-framer-item-subtitle="">{subtitle}</span>
</div>
</Command.Item>
)
}
function ButtonIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 5H13C13.5523 5 14 5.44772 14 6V9C14 9.55228 13.5523 10 13 10H2C1.44772 10 1 9.55228 1 9V6C1 5.44772 1.44772 5 2 5ZM0 6C0 4.89543 0.895431 4 2 4H13C14.1046 4 15 4.89543 15 6V9C15 10.1046 14.1046 11 13 11H2C0.89543 11 0 10.1046 0 9V6ZM4.5 6.75C4.08579 6.75 3.75 7.08579 3.75 7.5C3.75 7.91421 4.08579 8.25 4.5 8.25C4.91421 8.25 5.25 7.91421 5.25 7.5C5.25 7.08579 4.91421 6.75 4.5 6.75ZM6.75 7.5C6.75 7.08579 7.08579 6.75 7.5 6.75C7.91421 6.75 8.25 7.08579 8.25 7.5C8.25 7.91421 7.91421 8.25 7.5 8.25C7.08579 8.25 6.75 7.91421 6.75 7.5ZM10.5 6.75C10.0858 6.75 9.75 7.08579 9.75 7.5C9.75 7.91421 10.0858 8.25 10.5 8.25C10.9142 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 10.9142 6.75 10.5 6.75Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function InputIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 1C6.22386 1 6 1.22386 6 1.5C6 1.77614 6.22386 2 6.5 2C7.12671 2 7.45718 2.20028 7.65563 2.47812C7.8781 2.78957 8 3.28837 8 4V11C8 11.7116 7.8781 12.2104 7.65563 12.5219C7.45718 12.7997 7.12671 13 6.5 13C6.22386 13 6 13.2239 6 13.5C6 13.7761 6.22386 14 6.5 14C7.37329 14 8.04282 13.7003 8.46937 13.1031C8.47976 13.0886 8.48997 13.0739 8.5 13.0591C8.51003 13.0739 8.52024 13.0886 8.53063 13.1031C8.95718 13.7003 9.62671 14 10.5 14C10.7761 14 11 13.7761 11 13.5C11 13.2239 10.7761 13 10.5 13C9.87329 13 9.54282 12.7997 9.34437 12.5219C9.1219 12.2104 9 11.7116 9 11V4C9 3.28837 9.1219 2.78957 9.34437 2.47812C9.54282 2.20028 9.87329 2 10.5 2C10.7761 2 11 1.77614 11 1.5C11 1.22386 10.7761 1 10.5 1C9.62671 1 8.95718 1.29972 8.53063 1.89688C8.52024 1.91143 8.51003 1.92611 8.5 1.9409C8.48997 1.92611 8.47976 1.91143 8.46937 1.89688C8.04282 1.29972 7.37329 1 6.5 1ZM14 5H11V4H14C14.5523 4 15 4.44772 15 5V10C15 10.5523 14.5523 11 14 11H11V10H14V5ZM6 4V5H1L1 10H6V11H1C0.447715 11 0 10.5523 0 10V5C0 4.44772 0.447715 4 1 4H6Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function RadioIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.49985 0.877045C3.84216 0.877045 0.877014 3.84219 0.877014 7.49988C0.877014 11.1575 3.84216 14.1227 7.49985 14.1227C11.1575 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 3.84219 11.1575 0.877045 7.49985 0.877045ZM1.82701 7.49988C1.82701 4.36686 4.36683 1.82704 7.49985 1.82704C10.6328 1.82704 13.1727 4.36686 13.1727 7.49988C13.1727 10.6329 10.6328 13.1727 7.49985 13.1727C4.36683 13.1727 1.82701 10.6329 1.82701 7.49988ZM7.49999 9.49999C8.60456 9.49999 9.49999 8.60456 9.49999 7.49999C9.49999 6.39542 8.60456 5.49999 7.49999 5.49999C6.39542 5.49999 5.49999 6.39542 5.49999 7.49999C5.49999 8.60456 6.39542 9.49999 7.49999 9.49999Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function BadgeIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3.5 6H11.5C12.3284 6 13 6.67157 13 7.5C13 8.32843 12.3284 9 11.5 9H3.5C2.67157 9 2 8.32843 2 7.5C2 6.67157 2.67157 6 3.5 6ZM1 7.5C1 6.11929 2.11929 5 3.5 5H11.5C12.8807 5 14 6.11929 14 7.5C14 8.88071 12.8807 10 11.5 10H3.5C2.11929 10 1 8.88071 1 7.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function ToggleIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.5 4C8.567 4 7 5.567 7 7.5C7 9.433 8.567 11 10.5 11C12.433 11 14 9.433 14 7.5C14 5.567 12.433 4 10.5 4ZM7.67133 11C6.65183 10.175 6 8.91363 6 7.5C6 6.08637 6.65183 4.82498 7.67133 4H4.5C2.567 4 1 5.567 1 7.5C1 9.433 2.567 11 4.5 11H7.67133ZM0 7.5C0 5.01472 2.01472 3 4.5 3H10.5C12.9853 3 15 5.01472 15 7.5C15 9.98528 12.9853 12 10.5 12H4.5C2.01472 12 0 9.98528 0 7.5Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function AvatarIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0.877014 7.49988C0.877014 3.84219 3.84216 0.877045 7.49985 0.877045C11.1575 0.877045 14.1227 3.84219 14.1227 7.49988C14.1227 11.1575 11.1575 14.1227 7.49985 14.1227C3.84216 14.1227 0.877014 11.1575 0.877014 7.49988ZM7.49985 1.82704C4.36683 1.82704 1.82701 4.36686 1.82701 7.49988C1.82701 8.97196 2.38774 10.3131 3.30727 11.3213C4.19074 9.94119 5.73818 9.02499 7.50023 9.02499C9.26206 9.02499 10.8093 9.94097 11.6929 11.3208C12.6121 10.3127 13.1727 8.97172 13.1727 7.49988C13.1727 4.36686 10.6328 1.82704 7.49985 1.82704ZM10.9818 11.9787C10.2839 10.7795 8.9857 9.97499 7.50023 9.97499C6.01458 9.97499 4.71624 10.7797 4.01845 11.9791C4.97952 12.7272 6.18765 13.1727 7.49985 13.1727C8.81227 13.1727 10.0206 12.727 10.9818 11.9787ZM5.14999 6.50487C5.14999 5.207 6.20212 4.15487 7.49999 4.15487C8.79786 4.15487 9.84999 5.207 9.84999 6.50487C9.84999 7.80274 8.79786 8.85487 7.49999 8.85487C6.20212 8.85487 5.14999 7.80274 5.14999 6.50487ZM7.49999 5.10487C6.72679 5.10487 6.09999 5.73167 6.09999 6.50487C6.09999 7.27807 6.72679 7.90487 7.49999 7.90487C8.27319 7.90487 8.89999 7.27807 8.89999 6.50487C8.89999 5.73167 8.27319 5.10487 7.49999 5.10487Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function ContainerIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 1.5C2 1.77614 1.77614 2 1.5 2C1.22386 2 1 1.77614 1 1.5C1 1.22386 1.22386 1 1.5 1C1.77614 1 2 1.22386 2 1.5ZM5 13H10V2L5 2L5 13ZM4 13C4 13.5523 4.44772 14 5 14H10C10.5523 14 11 13.5523 11 13V2C11 1.44772 10.5523 1 10 1H5C4.44772 1 4 1.44771 4 2V13ZM13.5 2C13.7761 2 14 1.77614 14 1.5C14 1.22386 13.7761 1 13.5 1C13.2239 1 13 1.22386 13 1.5C13 1.77614 13.2239 2 13.5 2ZM2 3.5C2 3.77614 1.77614 4 1.5 4C1.22386 4 1 3.77614 1 3.5C1 3.22386 1.22386 3 1.5 3C1.77614 3 2 3.22386 2 3.5ZM13.5 4C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3C13.2239 3 13 3.22386 13 3.5C13 3.77614 13.2239 4 13.5 4ZM2 5.5C2 5.77614 1.77614 6 1.5 6C1.22386 6 1 5.77614 1 5.5C1 5.22386 1.22386 5 1.5 5C1.77614 5 2 5.22386 2 5.5ZM13.5 6C13.7761 6 14 5.77614 14 5.5C14 5.22386 13.7761 5 13.5 5C13.2239 5 13 5.22386 13 5.5C13 5.77614 13.2239 6 13.5 6ZM2 7.5C2 7.77614 1.77614 8 1.5 8C1.22386 8 1 7.77614 1 7.5C1 7.22386 1.22386 7 1.5 7C1.77614 7 2 7.22386 2 7.5ZM13.5 8C13.7761 8 14 7.77614 14 7.5C14 7.22386 13.7761 7 13.5 7C13.2239 7 13 7.22386 13 7.5C13 7.77614 13.2239 8 13.5 8ZM2 9.5C2 9.77614 1.77614 10 1.5 10C1.22386 10 1 9.77614 1 9.5C1 9.22386 1.22386 9 1.5 9C1.77614 9 2 9.22386 2 9.5ZM13.5 10C13.7761 10 14 9.77614 14 9.5C14 9.22386 13.7761 9 13.5 9C13.2239 9 13 9.22386 13 9.5C13 9.77614 13.2239 10 13.5 10ZM2 11.5C2 11.7761 1.77614 12 1.5 12C1.22386 12 1 11.7761 1 11.5C1 11.2239 1.22386 11 1.5 11C1.77614 11 2 11.2239 2 11.5ZM13.5 12C13.7761 12 14 11.7761 14 11.5C14 11.2239 13.7761 11 13.5 11C13.2239 11 13 11.2239 13 11.5C13 11.7761 13.2239 12 13.5 12ZM2 13.5C2 13.7761 1.77614 14 1.5 14C1.22386 14 1 13.7761 1 13.5C1 13.2239 1.22386 13 1.5 13C1.77614 13 2 13.2239 2 13.5ZM13.5 14C13.7761 14 14 13.7761 14 13.5C14 13.2239 13.7761 13 13.5 13C13.2239 13 13 13.2239 13 13.5C13 13.7761 13.2239 14 13.5 14Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
function SearchIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
)
}
function SliderIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M10.3004 7.49991C10.3004 8.4943 9.49426 9.30041 8.49988 9.30041C7.50549 9.30041 6.69938 8.4943 6.69938 7.49991C6.69938 6.50553 7.50549 5.69942 8.49988 5.69942C9.49426 5.69942 10.3004 6.50553 10.3004 7.49991ZM11.205 8C10.9699 9.28029 9.84816 10.2504 8.49988 10.2504C7.1516 10.2504 6.0299 9.28029 5.79473 8H0.5C0.223858 8 0 7.77614 0 7.5C0 7.22386 0.223858 7 0.5 7H5.7947C6.0298 5.71962 7.15154 4.74942 8.49988 4.74942C9.84822 4.74942 10.97 5.71962 11.2051 7H14.5C14.7761 7 15 7.22386 15 7.5C15 7.77614 14.7761 8 14.5 8H11.205Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
)
}
================================================
FILE: website/components/cmdk/linear.tsx
================================================
import { Command } from 'cmdk'
export function LinearCMDK() {
return (
<div className="linear">
<Command>
<div cmdk-linear-badge="">Issue - FUN-343</div>
<Command.Input autoFocus placeholder="Type a command or search..." />
<Command.List>
<Command.Empty>No results found.</Command.Empty>
{items.map(({ icon, label, shortcut }) => {
return (
<Command.Item key={label} value={label}>
{icon}
{label}
<div cmdk-linear-shortcuts="">
{shortcut.map((key) => {
return <kbd key={key}>{key}</kbd>
})}
</div>
</Command.Item>
)
})}
</Command.List>
</Command>
</div>
)
}
const items = [
{
icon: <AssignToIcon />,
label: 'Assign to...',
shortcut: ['A'],
},
{
icon: <AssignToMeIcon />,
label: 'Assign to me',
shortcut: ['I'],
},
{
icon: <ChangeStatusIcon />,
label: 'Change status...',
shortcut: ['S'],
},
{
icon: <ChangePriorityIcon />,
label: 'Change priority...',
shortcut: ['P'],
},
{
icon: <ChangeLabelsIcon />,
label: 'Change labels...',
shortcut: ['L'],
},
{
icon: <RemoveLabelIcon />,
label: 'Remove label...',
shortcut: ['⇧', 'L'],
},
{
icon: <SetDueDateIcon />,
label: 'Set due date...',
shortcut: ['⇧', 'D'],
},
]
function AssignToIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M7 7a2.5 2.5 0 10.001-4.999A2.5 2.5 0 007 7zm0 1c-1.335 0-4 .893-4 2.667v.666c0 .367.225.667.5.667h2.049c.904-.909 2.417-1.911 4.727-2.009v-.72a.27.27 0 01.007-.063C9.397 8.404 7.898 8 7 8zm4.427 2.028a.266.266 0 01.286.032l2.163 1.723a.271.271 0 01.013.412l-2.163 1.97a.27.27 0 01-.452-.2v-.956c-3.328.133-5.282 1.508-5.287 1.535a.27.27 0 01-.266.227h-.022a.27.27 0 01-.249-.271c0-.046 1.549-3.328 5.824-3.509v-.72a.27.27 0 01.153-.243z" />
</svg>
)
}
function AssignToMeIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M7.00003 7C8.38128 7 9.50003 5.88125 9.50003 4.5C9.50003 3.11875 8.38128 2 7.00003 2C5.61878 2 4.50003 3.11875 4.50003 4.5C4.50003 5.88125 5.61878 7 7.00003 7Z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.00005 8C5.66505 8 3.00006 8.89333 3.00006 10.6667V11.3333C3.00006 11.7 3.22506 12 3.50006 12H3.98973C4.01095 11.9415 4.04535 11.8873 4.09266 11.8425L7.21783 8.88444C7.28966 8.81658 7.38297 8.77917 7.4796 8.77949C7.69459 8.78018 7.86826 8.96356 7.86753 9.1891L7.86214 10.629C9.00553 10.5858 10.0366 10.4354 10.9441 10.231C10.5539 8.74706 8.22087 8 7.00005 8Z"
/>
<path d="M6.72511 14.718C6.80609 14.7834 6.91767 14.7955 7.01074 14.749C7.10407 14.7036 7.16321 14.6087 7.16295 14.5047L7.1605 13.7849C11.4352 13.5894 12.9723 10.3023 12.9722 10.2563C12.9722 10.1147 12.8634 9.9971 12.7225 9.98626L12.7009 9.98634C12.5685 9.98689 12.4561 10.0833 12.4351 10.2142C12.4303 10.2413 10.4816 11.623 7.15364 11.7666L7.1504 10.8116C7.14981 10.662 7.02829 10.5412 6.87896 10.5418C6.81184 10.5421 6.74721 10.5674 6.69765 10.6127L4.54129 12.5896C4.43117 12.6906 4.42367 12.862 4.52453 12.9723C4.53428 12.9829 4.54488 12.9928 4.55621 13.0018L6.72511 14.718Z" />
</svg>
)
}
function ChangeStatusIcon() {
return (
<svg width="16" height="16" viewBox="-1 -1 15 15" fill="currentColor">
<path d="M10.5714 7C10.5714 8.97245 8.97245 10.5714 7 10.5714L6.99975 3.42857C8.9722 3.42857 10.5714 5.02755 10.5714 7Z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7 12.5C10.0376 12.5 12.5 10.0376 12.5 7C12.5 3.96243 10.0376 1.5 7 1.5C3.96243 1.5 1.5 3.96243 1.5 7C1.5 10.0376 3.96243 12.5 7 12.5ZM7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14Z"
/>
</svg>
)
}
function ChangePriorityIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<rect x="1" y="8" width="3" height="6" rx="1"></rect>
<rect x="6" y="5" width="3" height="9" rx="1"></rect>
<rect x="11" y="2" width="3" height="12" rx="1"></rect>
</svg>
)
}
function ChangeLabelsIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.2105 4C10.6337 4 11.0126 4.18857 11.24 4.48L14 8L11.24 11.52C11.0126 11.8114 10.6337 12 10.2105 12L3.26316 11.9943C2.56842 11.9943 2 11.4857 2 10.8571V5.14286C2 4.51429 2.56842 4.00571 3.26316 4.00571L10.2105 4ZM11.125 9C11.6773 9 12.125 8.55228 12.125 8C12.125 7.44772 11.6773 7 11.125 7C10.5727 7 10.125 7.44772 10.125 8C10.125 8.55228 10.5727 9 11.125 9Z"
/>
</svg>
)
}
function RemoveLabelIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.2105 4C10.6337 4 11.0126 4.18857 11.24 4.48L14 8L11.24 11.52C11.0126 11.8114 10.6337 12 10.2105 12L3.26316 11.9943C2.56842 11.9943 2 11.4857 2 10.8571V5.14286C2 4.51429 2.56842 4.00571 3.26316 4.00571L10.2105 4ZM11.125 9C11.6773 9 12.125 8.55228 12.125 8C12.125 7.44772 11.6773 7 11.125 7C10.5727 7 10.125 7.44772 10.125 8C10.125 8.55228 10.5727 9 11.125 9Z"
/>
</svg>
)
}
function SetDueDateIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15 5C15 2.79086 13.2091 1 11 1H5C2.79086 1 1 2.79086 1 5V11C1 13.2091 2.79086 15 5 15H6.25C6.66421 15 7 14.6642 7 14.25C7 13.8358 6.66421 13.5 6.25 13.5H5C3.61929 13.5 2.5 12.3807 2.5 11V6H13.5V6.25C13.5 6.66421 13.8358 7 14.25 7C14.6642 7 15 6.66421 15 6.25V5ZM11.5001 8C11.9143 8 12.2501 8.33579 12.2501 8.75V10.75L14.2501 10.75C14.6643 10.75 15.0001 11.0858 15.0001 11.5C15.0001 11.9142 14.6643 12.25 14.2501 12.25L12.2501 12.25V14.25C12.2501 14.6642 11.9143 15 11.5001 15C11.0859 15 10.7501 14.6642 10.7501 14.25V12.25H8.75C8.33579 12.25 8 11.9142 8 11.5C8 11.0858 8.33579 10.75 8.75 10.75L10.7501 10.75V8.75C10.7501 8.33579 11.0859 8 11.5001 8Z"
/>
</svg>
)
}
================================================
FILE: website/components/cmdk/raycast.tsx
================================================
import React from 'react'
import { useTheme } from 'next-themes'
import * as Popover from '@radix-ui/react-popover'
import { Command } from 'cmdk'
import { Logo, LinearIcon, FigmaIcon, SlackIcon, YouTubeIcon, RaycastIcon } from 'components'
export function RaycastCMDK() {
const { resolvedTheme: theme } = useTheme()
const [value, setValue] = React.useState('linear')
const inputRef = React.useRef<HTMLInputElement | null>(null)
const listRef = React.useRef(null)
React.useEffect(() => {
inputRef?.current?.focus()
}, [])
return (
<div className="raycast">
<Command value={value} onValueChange={(v) => setValue(v)}>
<div cmdk-raycast-top-shine="" />
<Command.Input ref={inputRef} autoFocus placeholder="Search for apps and commands..." />
<hr cmdk-raycast-loader="" />
<Command.List ref={listRef}>
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Suggestions">
<Item value="Linear" keywords={['issue', 'sprint']}>
<Logo>
<LinearIcon
style={{
width: 12,
height: 12,
}}
/>
</Logo>
Linear
</Item>
<Item value="Figma" keywords={['design', 'ui', 'ux']}>
<Logo>
<FigmaIcon />
</Logo>
Figma
</Item>
<Item value="Slack" keywords={['chat', 'team', 'communication']}>
<Logo>
<SlackIcon />
</Logo>
Slack
</Item>
<Item value="YouTube" keywords={['video', 'watch', 'stream']}>
<Logo>
<YouTubeIcon />
</Logo>
YouTube
</Item>
<Item value="Raycast" keywords={['productivity', 'tools', 'apps']}>
<Logo>
<RaycastIcon />
</Logo>
Raycast
</Item>
</Command.Group>
<Command.Group heading="Commands">
<Item isCommand value="Clipboard History" keywords={['copy', 'paste', 'clipboard']}>
<Logo>
<ClipboardIcon />
</Logo>
Clipboard History
</Item>
<Item isCommand value="Import Extension" keywords={['import', 'extension']}>
<HammerIcon />
Import Extension
</Item>
<Item isCommand value="Manage Extensions" keywords={['manage', 'extension']}>
<HammerIcon />
Manage Extensions
</Item>
</Command.Group>
</Command.List>
<div cmdk-raycast-footer="">
{theme === 'dark' ? <RaycastDarkIcon /> : <RaycastLightIcon />}
<button cmdk-raycast-open-trigger="">
Open Application
<kbd>↵</kbd>
</button>
<hr />
<SubCommand listRef={listRef} selectedValue={value} inputRef={inputRef} />
</div>
</Command>
</div>
)
}
function Item({
children,
value,
keywords,
isCommand = false,
}: {
children: React.ReactNode
value: string
keywords?: string[]
isCommand?: boolean
}) {
return (
<Command.Item value={value} keywords={keywords} onSelect={() => {}}>
{children}
<span cmdk-raycast-meta="">{isCommand ? 'Command' : 'Application'}</span>
</Command.Item>
)
}
function SubCommand({
inputRef,
listRef,
selectedValue,
}: {
inputRef: React.RefObject<HTMLInputElement>
listRef: React.RefObject<HTMLElement>
selectedValue: string
}) {
const [open, setOpen] = React.useState(false)
React.useEffect(() => {
function listener(e: KeyboardEvent) {
if (e.key === 'k' && e.metaKey) {
e.preventDefault()
setOpen((o) => !o)
}
}
document.addEventListener('keydown', listener)
return () => {
document.removeEventListener('keydown', listener)
}
}, [])
React.useEffect(() => {
const el = listRef.current
if (!el) return
if (open) {
el.style.overflow = 'hidden'
} else {
el.style.overflow = ''
}
}, [open, listRef])
return (
<Popover.Root open={open} onOpenChange={setOpen} modal>
<Popover.Trigger cmdk-raycast-subcommand-trigger="" onClick={() => setOpen(true)} aria-expanded={open}>
Actions
<kbd>⌘</kbd>
<kbd>K</kbd>
</Popover.Trigger>
<Popover.Content
side="top"
align="end"
className="raycast-submenu"
sideOffset={16}
alignOffset={0}
onCloseAutoFocus={(e) => {
e.preventDefault()
inputRef?.current?.focus()
}}
>
<Command>
<Command.List>
<Command.Group heading={selectedValue}>
<SubItem shortcut="↵">
<WindowIcon />
Open Application
</SubItem>
<SubItem shortcut="⌘ ↵">
<FinderIcon />
Show in Finder
</SubItem>
<SubItem shortcut="⌘ I">
<FinderIcon />
Show Info in Finder
</SubItem>
<SubItem shortcut="⌘ ⇧ F">
<StarIcon />
Add to Favorites
</SubItem>
</Command.Group>
</Command.List>
<Command.Input placeholder="Search for actions..." />
</Command>
</Popover.Content>
</Popover.Root>
)
}
function SubItem({ children, shortcut }: { children: React.ReactNode; shortcut: string }) {
return (
<Command.Item>
{children}
<div cmdk-raycast-submenu-shortcuts="">
{shortcut.split(' ').map((key) => {
return <kbd key={key}>{key}</kbd>
})}
</div>
</Command.Item>
)
}
function TerminalIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
)
}
function RaycastLightIcon() {
return (
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M934.302 511.971L890.259 556.017L723.156 388.902V300.754L934.302 511.971ZM511.897 89.5373L467.854 133.583L634.957 300.698H723.099L511.897 89.5373ZM417.334 184.275L373.235 228.377L445.776 300.923H533.918L417.334 184.275ZM723.099 490.061V578.209L795.641 650.755L839.74 606.652L723.099 490.061ZM697.868 653.965L723.099 628.732H395.313V300.754L370.081 325.987L322.772 278.675L278.56 322.833L325.869 370.146L300.638 395.379V446.071L228.097 373.525L183.997 417.627L300.638 534.275V634.871L133.59 467.925L89.4912 512.027L511.897 934.461L555.996 890.359L388.892 723.244H489.875L606.516 839.892L650.615 795.79L578.074 723.244H628.762L653.994 698.011L701.303 745.323L745.402 701.221L697.868 653.965Z"
fill="#FF6363"
/>
</svg>
)
}
function RaycastDarkIcon() {
return (
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M301.144 634.799V722.856L90 511.712L134.244 467.804L301.144 634.799ZM389.201 722.856H301.144L512.288 934L556.34 889.996L389.201 722.856ZM889.996 555.956L934 511.904L512.096 90L468.092 134.052L634.799 300.952H534.026L417.657 184.679L373.605 228.683L446.065 301.144H395.631V628.561H723.048V577.934L795.509 650.395L839.561 606.391L723.048 489.878V389.105L889.996 555.956ZM323.17 278.926L279.166 322.978L326.385 370.198L370.39 326.145L323.17 278.926ZM697.855 653.61L653.994 697.615L701.214 744.834L745.218 700.782L697.855 653.61ZM228.731 373.413L184.679 417.465L301.144 533.93V445.826L228.731 373.413ZM578.174 722.856H490.07L606.535 839.321L650.587 795.269L578.174 722.856Z"
fill="#FF6363"
/>
</svg>
)
}
function WindowIcon() {
return (
<svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.25 4.75V3.75C14.25 2.64543 13.3546 1.75 12.25 1.75H3.75C2.64543 1.75 1.75 2.64543 1.75 3.75V4.75M14.25 4.75V12.25C14.25 13.3546 13.3546 14.25 12.25 14.25H3.75C2.64543 14.25 1.75 13.3546 1.75 12.25V4.75M14.25 4.75H1.75"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function FinderIcon() {
return (
<svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 4.75V6.25M11 4.75V6.25M8.75 1.75H3.75C2.64543 1.75 1.75 2.64543 1.75 3.75V12.25C1.75 13.3546 2.64543 14.25 3.75 14.25H8.75M8.75 1.75H12.25C13.3546 1.75 14.25 2.64543 14.25 3.75V12.25C14.25 13.3546 13.3546 14.25 12.25 14.25H8.75M8.75 1.75L7.08831 7.1505C6.9202 7.69686 7.32873 8.25 7.90037 8.25C8.36961 8.25 8.75 8.63039 8.75 9.09963V14.25M5 10.3203C5 10.3203 5.95605 11.25 8 11.25C10.0439 11.25 11 10.3203 11 10.3203"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function StarIcon() {
return (
<svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.43376 2.17103C7.60585 1.60966 8.39415 1.60966 8.56624 2.17103L9.61978 5.60769C9.69652 5.85802 9.92611 6.02873 10.186 6.02873H13.6562C14.2231 6.02873 14.4665 6.75397 14.016 7.10088L11.1582 9.3015C10.9608 9.45349 10.8784 9.71341 10.9518 9.95262L12.0311 13.4735C12.2015 14.0292 11.5636 14.4777 11.1051 14.1246L8.35978 12.0106C8.14737 11.847 7.85263 11.847 7.64022 12.0106L4.89491 14.1246C4.43638 14.4777 3.79852 14.0292 3.96889 13.4735L5.04824 9.95262C5.12157 9.71341 5.03915 9.45349 4.84178 9.3015L1.98404 7.10088C1.53355 6.75397 1.77692 6.02873 2.34382 6.02873H5.81398C6.07389 6.02873 6.30348 5.85802 6.38022 5.60769L7.43376 2.17103Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function ClipboardIcon() {
return (
<div cmdk-raycast-clipboard-icon="">
<svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.07512 2.75H4.75C3.64543 2.75 2.75 3.64543 2.75 4.75V12.25C2.75 13.3546 3.64543 14.25 4.75 14.25H11.25C12.3546 14.25 13.25 13.3546 13.25 12.25V4.75C13.25 3.64543 12.3546 2.75 11.25 2.75H9.92488M9.88579 3.02472L9.5934 4.04809C9.39014 4.75952 8.73989 5.25 8 5.25V5.25C7.26011 5.25 6.60986 4.75952 6.4066 4.04809L6.11421 3.02472C5.93169 2.38591 6.41135 1.75 7.07573 1.75H8.92427C9.58865 1.75 10.0683 2.3859 9.88579 3.02472Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
)
}
function HammerIcon() {
return (
<div cmdk-raycast-hammer-icon="">
<svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.73762 6.19288L2.0488 11.2217C1.6504 11.649 1.6504 12.3418 2.0488 12.769L3.13083 13.9295C3.52923 14.3568 4.17515 14.3568 4.57355 13.9295L9.26238 8.90071M6.73762 6.19288L7.0983 5.80605C7.4967 5.37877 7.4967 4.686 7.0983 4.25872L6.01627 3.09822L6.37694 2.71139C7.57213 1.42954 9.50991 1.42954 10.7051 2.71139L13.9512 6.19288C14.3496 6.62017 14.3496 7.31293 13.9512 7.74021L12.8692 8.90071C12.4708 9.328 11.8248 9.328 11.4265 8.90071L11.0658 8.51388C10.6674 8.0866 10.0215 8.0866 9.62306 8.51388L9.26238 8.90071M6.73762 6.19288L9.26238 8.90071"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
)
}
================================================
FILE: website/components/cmdk/vercel.tsx
================================================
import React from 'react'
import { Command } from 'cmdk'
export function VercelCMDK() {
const ref = React.useRef<HTMLDivElement | null>(null)
const [inputValue, setInputValue] = React.useState('')
const [pages, setPages] = React.useState<string[]>(['home'])
const activePage = pages[pages.length - 1]
const isHome = activePage === 'home'
const popPage = React.useCallback(() => {
setPages((pages) => {
const x = [...pages]
x.splice(-1, 1)
return x
})
}, [])
const onKeyDown = React.useCallback(
(e: KeyboardEvent) => {
if (isHome || inputValue.length) {
return
}
if (e.key === 'Backspace') {
e.preventDefault()
popPage()
}
},
[inputValue.length, isHome, popPage],
)
function bounce() {
if (ref.current) {
ref.current.style.transform = 'scale(0.96)'
setTimeout(() => {
if (ref.current) {
ref.current.style.transform = ''
}
}, 100)
setInputValue('')
}
}
return (
<div className="vercel">
<Command
ref={ref}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
bounce()
}
if (isHome || inputValue.length) {
return
}
if (e.key === 'Backspace') {
e.preventDefault()
popPage()
bounce()
}
}}
>
<div>
{pages.map((p) => (
<div key={p} cmdk-vercel-badge="">
{p}
</div>
))}
</div>
<Command.Input
autoFocus
placeholder="What do you need?"
onValueChange={(value) => {
setInputValue(value)
}}
/>
<Command.List>
<Command.Empty>No results found.</Command.Empty>
{activePage === 'home' && <Home searchProjects={() => setPages([...pages, 'projects'])} />}
{activePage === 'projects' && <Projects />}
</Command.List>
</Command>
</div>
)
}
function Home({ searchProjects }: { searchProjects: Function }) {
return (
<>
<Command.Group heading="Projects">
<Item
shortcut="S P"
onSelect={() => {
searchProjects()
}}
>
<ProjectsIcon />
Search Projects...
</Item>
<Item>
<PlusIcon />
Create New Project...
</Item>
</Command.Group>
<Command.Group heading="Teams">
<Item shortcut="⇧ P">
<TeamsIcon />
Search Teams...
</Item>
<Item>
<PlusIcon />
Create New Team...
</Item>
</Command.Group>
<Command.Group heading="Help">
<Item shortcut="⇧ D">
<DocsIcon />
Search Docs...
</Item>
<Item>
<FeedbackIcon />
Send Feedback...
</Item>
<Item>
<ContactIcon />
Contact Support
</Item>
</Command.Group>
</>
)
}
function Projects() {
return (
<>
<Item>Project 1</Item>
<Item>Project 2</Item>
<Item>Project 3</Item>
<Item>Project 4</Item>
<Item>Project 5</Item>
<Item>Project 6</Item>
</>
)
}
function Item({
children,
shortcut,
onSelect = () => {},
}: {
children: React.ReactNode
shortcut?: string
onSelect?: (value: string) => void
}) {
return (
<Command.Item onSelect={onSelect}>
{children}
{shortcut && (
<div cmdk-vercel-shortcuts="">
{shortcut.split(' ').map((key) => {
return <kbd key={key}>{key}</kbd>
})}
</div>
)}
</Command.Item>
)
}
function ProjectsIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M3 3h7v7H3z"></path>
<path d="M14 3h7v7h-7z"></path>
<path d="M14 14h7v7h-7z"></path>
<path d="M3 14h7v7H3z"></path>
</svg>
)
}
function PlusIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M12 5v14"></path>
<path d="M5 12h14"></path>
</svg>
)
}
function TeamsIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 00-3-3.87"></path>
<path d="M16 3.13a4 4 0 010 7.75"></path>
</svg>
)
}
function CopyIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z"></path>
</svg>
)
}
function DocsIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"></path>
<path d="M14 2v6h6"></path>
<path d="M16 13H8"></path>
<path d="M16 17H8"></path>
<path d="M10 9H8"></path>
</svg>
)
}
function FeedbackIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"></path>
</svg>
)
}
function ContactIcon() {
return (
<svg
fill="none"
height="24"
shapeRendering="geometricPrecision"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
viewBox="0 0 24 24"
width="24"
>
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<path d="M22 6l-10 7L2 6"></path>
</svg>
)
}
================================================
FILE: website/components/code/code.module.scss
================================================
.root {
border-radius: 12px;
padding: 16px;
backdrop-filter: blur(10px);
border: 1px solid var(--gray6);
position: relative;
line-height: 16px;
background: var(--lowContrast);
white-space: pre-wrap;
box-shadow: rgb(0 0 0 / 10%) 0px 5px 30px -5px;
@media (prefers-color-scheme: dark) {
background: var(--grayA2);
}
button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: var(--grayA3);
border-radius: 8px;
position: absolute;
top: 12px;
right: 12px;
color: var(--gray11);
cursor: copy;
transition: color 150ms ease, background 150ms ease, transform 150ms ease;
&:hover {
color: var(--gray12);
background: var(--grayA4);
}
&:active {
color: var(--gray12);
background: var(--grayA5);
transform: scale(0.96);
}
}
}
.shine {
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
rgba(56, 189, 248, 0),
var(--gray5) 20%,
var(--gray9) 67.19%,
rgba(236, 72, 153, 0)
);
height: 1px;
position: absolute;
top: -1px;
width: 97%;
z-index: -1;
}
}
@media (max-width: 640px) {
.root {
:global(.token-line) {
font-size: 11px !important;
}
}
}
================================================
FILE: website/components/code/index.tsx
================================================
import React from 'react'
import copy from 'copy-to-clipboard'
import Highlight, { defaultProps } from 'prism-react-renderer'
import styles from './code.module.scss'
import { CopyIcon } from 'components/icons'
const theme = {
plain: {
color: 'var(--gray12)',
fontSize: 12,
fontFamily: 'Menlo, monospace',
},
styles: [
{
types: ['comment'],
style: {
color: 'var(--gray9)',
},
},
{
types: ['atrule', 'keyword', 'attr-name', 'selector'],
style: {
color: 'var(--gray10)',
},
},
{
types: ['punctuation', 'operator'],
style: {
color: 'var(--gray9)',
},
},
{
types: ['class-name', 'function', 'tag'],
style: {
color: 'var(--gray12)',
},
},
],
}
export function Code({ children }: { children: string }) {
return (
<Highlight {...defaultProps} theme={theme} code={children} language="jsx">
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={`${className} ${styles.root}`} style={style}>
<button
aria-label="Copy Code"
onClick={() => {
copy(children)
}}
>
<CopyIcon />
</button>
<div className={styles.shine} />
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={i} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
================================================
FILE: website/components/icons/icons.module.scss
================================================
.blurLogo {
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: 4px;
overflow: hidden;
box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.015);
.bg {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 1;
pointer-events: none;
user-select: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: scale(1.5) translateZ(0);
filter: blur(12px) opacity(0.4) saturate(100%);
transition: filter 150ms ease;
}
.inner {
display: flex;
align-items: center;
justify-content: center;
object-fit: cover;
width: 100%;
height: 100%;
user-select: none;
pointer-events: none;
border-radius: inherit;
z-index: 2;
svg {
width: 14px;
height: 14px;
filter: drop-shadow(0 4px 4px rgba(0, 0, 0, 0.16));
transition: filter 150ms ease;
}
}
}
================================================
FILE: website/components/icons/index.tsx
================================================
import styles from './icons.module.scss'
export function FigmaIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
<path fill="#e64a19" d="M26,17h-8c-3.866,0-7-3.134-7-7v0c0-3.866,3.134-7,7-7h8V17z" />
<path fill="#7c4dff" d="M25,31h-7c-3.866,0-7-3.134-7-7v0c0-3.866,3.134-7,7-7h7V31z" />
<path fill="#66bb6a" d="M18,45L18,45c-3.866,0-7-3.134-7-7v0c0-3.866,3.134-7,7-7h7v7C25,41.866,21.866,45,18,45z" />
<path fill="#ff7043" d="M32,17h-7V3h7c3.866,0,7,3.134,7,7v0C39,13.866,35.866,17,32,17z" />
<circle cx="32" cy="24" r="7" fill="#29b6f6" />
</svg>
)
}
export function RaycastIcon() {
return (
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7 18.073V20.994L0 13.994L1.46 12.534L7 18.075V18.073ZM9.921 20.994H7L14 27.994L15.46 26.534L9.921 20.994V20.994ZM26.535 15.456L27.996 13.994L13.996 -0.00598145L12.538 1.46002L18.077 6.99802H14.73L10.864 3.14002L9.404 4.60002L11.809 7.00402H10.129V17.87H20.994V16.19L23.399 18.594L24.859 17.134L20.994 13.268V9.92102L26.534 15.456H26.535ZM7.73 6.27002L6.265 7.73202L7.833 9.29802L9.294 7.83802L7.73 6.27002ZM20.162 18.702L18.702 20.164L20.268 21.732L21.73 20.27L20.162 18.702V18.702ZM4.596 9.40402L3.134 10.866L7 14.732V11.809L4.596 9.40402ZM16.192 21H13.268L17.134 24.866L18.596 23.404L16.192 21Z"
fill="#FF6363"
/>
</svg>
)
}
export function YouTubeIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
<path
fill="#FF3D00"
d="M43.2,33.9c-0.4,2.1-2.1,3.7-4.2,4c-3.3,0.5-8.8,1.1-15,1.1c-6.1,0-11.6-0.6-15-1.1c-2.1-0.3-3.8-1.9-4.2-4C4.4,31.6,4,28.2,4,24c0-4.2,0.4-7.6,0.8-9.9c0.4-2.1,2.1-3.7,4.2-4C12.3,9.6,17.8,9,24,9c6.2,0,11.6,0.6,15,1.1c2.1,0.3,3.8,1.9,4.2,4c0.4,2.3,0.9,5.7,0.9,9.9C44,28.2,43.6,31.6,43.2,33.9z"
/>
<path fill="#FFF" d="M20 31L20 17 32 24z" />
</svg>
)
}
export function SlackIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
<path
fill="#33d375"
d="M33,8c0-2.209-1.791-4-4-4s-4,1.791-4,4c0,1.254,0,9.741,0,11c0,2.209,1.791,4,4,4s4-1.791,4-4 C33,17.741,33,9.254,33,8z"
/>
<path
fill="#33d375"
d="M43,19c0,2.209-1.791,4-4,4c-1.195,0-4,0-4,0s0-2.986,0-4c0-2.209,1.791-4,4-4S43,16.791,43,19z"
/>
<path
fill="#40c4ff"
d="M8,14c-2.209,0-4,1.791-4,4s1.791,4,4,4c1.254,0,9.741,0,11,0c2.209,0,4-1.791,4-4s-1.791-4-4-4 C17.741,14,9.254,14,8,14z"
/>
<path
fill="#40c4ff"
d="M19,4c2.209,0,4,1.791,4,4c0,1.195,0,4,0,4s-2.986,0-4,0c-2.209,0-4-1.791-4-4S16.791,4,19,4z"
/>
<path
fill="#e91e63"
d="M14,39.006C14,41.212,15.791,43,18,43s4-1.788,4-3.994c0-1.252,0-9.727,0-10.984 c0-2.206-1.791-3.994-4-3.994s-4,1.788-4,3.994C14,29.279,14,37.754,14,39.006z"
/>
<path
fill="#e91e63"
d="M4,28.022c0-2.206,1.791-3.994,4-3.994c1.195,0,4,0,4,0s0,2.981,0,3.994c0,2.206-1.791,3.994-4,3.994 S4,30.228,4,28.022z"
/>
<path
fill="#ffc107"
d="M39,33c2.209,0,4-1.791,4-4s-1.791-4-4-4c-1.254,0-9.741,0-11,0c-2.209,0-4,1.791-4,4s1.791,4,4,4 C29.258,33,37.746,33,39,33z"
/>
<path
fill="#ffc107"
d="M28,43c-2.209,0-4-1.791-4-4c0-1.195,0-4,0-4s2.986,0,4,0c2.209,0,4,1.791,4,4S30.209,43,28,43z"
/>
</svg>
)
}
export function VercelIcon() {
return (
<svg aria-label="Vercel Logo" fill="var(--highContrast)" height="26" viewBox="0 0 75 65">
<path d="M37.59.25l36.95 64H.64l36.95-64z"></path>
</svg>
)
}
export function LinearIcon({ style }: { style?: Object }) {
return (
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" style={style}>
<path
d="M0.403013 37.3991L26.6009 63.597C13.2225 61.3356 2.66442 50.7775 0.403013 37.3991Z"
fill="#5E6AD2"
></path>
<path
d="M0 30.2868L33.7132 64C35.7182 63.8929 37.6742 63.6013 39.5645 63.142L0.85799 24.4355C0.398679 26.3259 0.10713 28.2818 0 30.2868Z"
fill="#5E6AD2"
></path>
<path
d="M2.53593 19.4042L44.5958 61.4641C46.1277 60.8066 47.598 60.0331 48.9956 59.1546L4.84543 15.0044C3.96691 16.402 3.19339 17.8723 2.53593 19.4042Z"
fill="#5E6AD2"
></path>
<path
d="M7.69501 11.1447C13.5677 4.32093 22.2677 0 31.9769 0C49.6628 0 64 14.3372 64 32.0231C64 41.7323 59.6791 50.4323 52.8553 56.305L7.69501 11.1447Z"
fill="#5E6AD2"
></path>
</svg>
)
}
export function Logo({ children, size = '20px' }: { children: React.ReactNode; size?: string }) {
return (
<div
className={styles.blurLogo}
style={{
width: size,
height: size,
}}
>
<div className={styles.bg} aria-hidden>
{children}
</div>
<div className={styles.inner}>{children}</div>
</div>
)
}
export function CopyIcon() {
return (
<svg width="16" height="16" strokeWidth="1.5" viewBox="0 0 24 24" fill="none">
<path
d="M19.4 20H9.6C9.26863 20 9 19.7314 9 19.4V9.6C9 9.26863 9.26863 9 9.6 9H19.4C19.7314 9 20 9.26863 20 9.6V19.4C20 19.7314 19.7314 20 19.4 20Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15 9V4.6C15 4.26863 14.7314 4 14.4 4H4.6C4.26863 4 4 4.26863 4 4.6V14.4C4 14.7314 4.26863 15 4.6 15H9"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export function CopiedIcon() {
return (
<svg width="16" height="16" strokeWidth="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 13L9 17L19 7" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
export function GitHubIcon() {
return (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7 0.175049C3.128 0.175049 0 3.30305 0 7.17505C0 10.259 2.013 12.885 4.79 13.825C5.14 13.891 5.272 13.672 5.272 13.497V12.316C3.325 12.731 2.909 11.375 2.909 11.375C2.581 10.565 2.122 10.347 2.122 10.347C1.488 9.90905 2.166 9.93105 2.166 9.93105C2.866 9.97505 3.237 10.653 3.237 10.653C3.872 11.725 4.878 11.419 5.272 11.243C5.338 10.784 5.512 10.478 5.709 10.303C4.156 10.128 2.516 9.51605 2.516 6.84705C2.516 6.08105 2.778 5.46905 3.237 4.96605C3.172 4.79105 2.931 4.06905 3.303 3.10605C3.303 3.10605 3.893 2.90905 5.228 3.82805C5.79831 3.67179 6.38668 3.5911 6.978 3.58805C7.568 3.58805 8.181 3.67505 8.728 3.82805C10.063 2.93105 10.653 3.10605 10.653 3.10605C11.025 4.06905 10.784 4.79105 10.719 4.96605C11.179 5.44605 11.441 6.08105 11.441 6.84605C11.441 9.53705 9.8 10.128 8.247 10.303C8.487 10.522 8.728 10.937 8.728 11.593V13.519C8.728 13.716 8.859 13.934 9.209 13.847C11.988 12.884 14 10.259 14 7.17505C14 3.30305 10.872 0.175049 7 0.175049V0.175049Z"
fill="currentColor"
/>
</svg>
)
}
export function FramerIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24">
<path
d="M 16 0 L 16 8 L 8 8 L 0 0 Z M 0 8 L 8 8 L 16 16 L 8 16 L 8 24 L 0 16 Z"
fill="var(--highContrast)"
></path>
</svg>
)
}
================================================
FILE: website/components/index.ts
================================================
export * from './cmdk/framer'
export * from './cmdk/linear'
export * from './cmdk/vercel'
export * from './cmdk/raycast'
export * from './icons'
export * from './code'
================================================
FILE: website/next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
================================================
FILE: website/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig
================================================
FILE: website/package.json
================================================
{
"name": "cmdk-website",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "pnpm -F 'cmdk-website^...' build && next build",
"start": "next start",
"lint": "eslint --ext .tsx"
},
"dependencies": {
"@radix-ui/react-popover": "^0.1.6",
"cmdk": "workspace:*",
"copy-to-clipboard": "^3.3.1",
"framer-motion": "^6.5.1",
"next": "13.5.1",
"next-seo": "^5.5.0",
"next-themes": "^0.2.0",
"prism-react-renderer": "^1.3.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"sass": "^1.53.0"
},
"devDependencies": {
"@types/node": "18.0.4",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"babel-eslint": "^10.1.0",
"eslint": "^8.19.0",
"eslint-config-next": "12.2.2",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"typescript": "4.7.4"
},
"lint-staged": {
"*.{tsx},*.{ts},*.{mdx}": [
"eslint --fix"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
}
}
================================================
FILE: website/pages/_app.tsx
================================================
import 'styles/globals.scss'
import 'styles/cmdk/vercel.scss'
import 'styles/cmdk/linear.scss'
import 'styles/cmdk/raycast.scss'
import 'styles/cmdk/framer.scss'
import type { AppProps } from 'next/app'
import { ThemeProvider } from 'next-themes'
import { NextSeo } from 'next-seo'
import Head from 'next/head'
const title = '⌘K'
const description = 'Fast, composable, unstyled command menu for React'
const siteUrl = 'https://cmdk.paco.me'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<link rel="shortcut icon" href="/favicon.svg" />
<meta name="twitter:card" content="summary_large_image" />
</Head>
<NextSeo
title={`${description} — ${title}`}
description={description}
openGraph={{
type: 'website',
url: siteUrl,
title,
description: description + '.',
images: [
{
url: `${siteUrl}/og.png`,
alt: title,
},
],
}}
/>
<ThemeProvider disableTransitionOnChange attribute="class">
<Component {...pageProps} />
</ThemeProvider>
</>
)
}
================================================
FILE: website/pages/_document.tsx
================================================
/* eslint-disable @next/next/no-sync-scripts */
import React from 'react'
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
export default class Document extends NextDocument {
render() {
return (
<Html lang="en">
<Head>
<link rel="preload" href="/inter-var-latin.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
================================================
FILE: website/pages/index.tsx
================================================
import styles from 'styles/index.module.scss'
import React from 'react'
import { AnimatePresence, AnimateSharedLayout, motion, MotionProps, useInView } from 'framer-motion'
import {
FramerCMDK,
LinearCMDK,
LinearIcon,
VercelCMDK,
VercelIcon,
RaycastCMDK,
RaycastIcon,
CopyIcon,
FramerIcon,
GitHubIcon,
Code,
CopiedIcon,
} from 'components'
import packageJSON from '../../cmdk/package.json'
type TTheme = {
theme: Themes
setTheme: Function
}
type Themes = 'linear' | 'raycast' | 'vercel' | 'framer'
const ThemeContext = React.createContext<TTheme>({} as TTheme)
export default function Index() {
const [theme, setTheme] = React.useState<Themes>('raycast')
return (
<main className={styles.main}>
<div className={styles.content}>
<div className={styles.meta}>
<div className={styles.info}>
<VersionBadge />
<h1>⌘K</h1>
<p>Fast, composable, unstyled command menu for React.</p>
</div>
<div className={styles.buttons}>
<InstallButton />
<GitHubButton />
</div>
</div>
<AnimatePresence exitBeforeEnter initial={false}>
{theme === 'framer' && (
<CMDKWrapper key="framer">
<FramerCMDK />
</CMDKWrapper>
)}
{theme === 'vercel' && (
<CMDKWrapper key="vercel">
<VercelCMDK />
</CMDKWrapper>
)}
{theme === 'linear' && (
<CMDKWrapper key="linear">
<LinearCMDK />
</CMDKWrapper>
)}
{theme === 'raycast' && (
<CMDKWrapper key="raycast">
<RaycastCMDK />
</CMDKWrapper>
)}
</AnimatePresence>
<ThemeContext.Provider value={{ theme, setTheme }}>
<ThemeSwitcher />
</ThemeContext.Provider>
<div aria-hidden className={styles.line} />
<Codeblock />
</div>
<Footer />
</main>
)
}
function CMDKWrapper(props: MotionProps & { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.2 }}
style={{
height: 475,
}}
{...props}
/>
)
}
//////////////////////////////////////////////////////////////////
function InstallButton() {
const [copied, setCopied] = React.useState(false)
return (
<button
className={styles.installButton}
onClick={async () => {
try {
await navigator.clipboard.writeText(`npm install cmdk`)
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
} catch (e) {}
}}
>
npm install cmdk
<span>{copied ? <CopiedIcon /> : <CopyIcon />}</span>
</button>
)
}
function GitHubButton() {
return (
<a
href="https://github.com/pacocoursey/cmdk"
target="_blank"
rel="noopener noreferrer"
className={styles.githubButton}
>
<GitHubIcon />
pacocoursey/cmdk
</a>
)
}
//////////////////////////////////////////////////////////////////
const themes = [
{
icon: <RaycastIcon />,
key: 'raycast',
},
{
icon: <LinearIcon />,
key: 'linear',
},
{
icon: <VercelIcon />,
key: 'vercel',
},
{
icon: <FramerIcon />,
key: 'framer',
},
]
function ThemeSwitcher() {
const { theme, setTheme } = React.useContext(ThemeContext)
const ref = React.useRef<HTMLButtonElement | null>(null)
const [showArrowKeyHint, setShowArrowKeyHint] = React.useState(false)
React.useEffect(() => {
function listener(e: KeyboardEvent) {
const themeNames = themes.map((t) => t.key)
if (e.key === 'ArrowRight') {
const currentIndex = themeNames.indexOf(theme)
const nextIndex = currentIndex + 1
const nextItem = themeNames[nextIndex]
if (nextItem) {
setTheme(nextItem)
}
}
if (e.key === 'ArrowLeft') {
const currentIndex = themeNames.indexOf(theme)
const prevIndex = currentIndex - 1
const prevItem = themeNames[prevIndex]
if (prevItem) {
setTheme(prevItem)
}
}
}
document.addEventListener('keydown', listener)
return () => {
document.removeEventListener('keydown', listener)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme])
return (
<div className={styles.switcher}>
<motion.span
className={styles.arrow}
initial={false}
animate={{
opacity: showArrowKeyHint ? 1 : 0,
x: showArrowKeyHint ? -24 : 0,
}}
style={{
left: 100,
}}
>
←
</motion.span>
<AnimateSharedLayout>
{themes.map(({ key, icon }) => {
const isActive = theme === key
return (
<button
ref={ref}
key={key}
data-selected={isActive}
onClick={() => {
setTheme(key)
if (showArrowKeyHint === false) {
setShowArrowKeyHint(true)
}
}}
>
{icon}
{key}
{isActive && (
<motion.div
layoutId="activeTheme"
transition={{
type: 'spring',
stiffness: 250,
damping: 27,
mass: 1,
}}
className={styles.activeTheme}
/>
)}
</button>
)
})}
</AnimateSharedLayout>
<motion.span
className={styles.arrow}
initial={false}
animate={{
opacity: showArrowKeyHint ? 1 : 0,
x: showArrowKeyHint ? 20 : 0,
}}
style={{
right: 100,
}}
>
→
</motion.span>
</div>
)
}
//////////////////////////////////////////////////////////////////
function Codeblock() {
const code = `import { Command } from 'cmdk';
<Command.Dialog open={open} onOpenChange={setOpen}>
<Command.Input />
<Command.List>
{loading && <Command.Loading>Hang on…</Command.Loading>}
<Command.Empty>No results found.</Command.Empty>
<Command.Group heading="Fruits">
<Command.Item>Apple</Command.Item>
<Command.Item>Orange</Command.Item>
<Command.Separator />
<Command.Item>Pear</Command.Item>
<Command.Item>Blueberry</Command.Item>
</Command.Group>
<Command.Item>Fish</Command.Item>
</Command.List>
</Command.Dialog>`
return (
<div className={styles.codeBlock}>
<div className={styles.line2} aria-hidden />
<div className={styles.line3} aria-hidden />
<Code>{code}</Code>
</div>
)
}
//////////////////////////////////////////////////////////////////
function VersionBadge() {
return <span className={styles.versionBadge}>v{packageJSON.version}</span>
}
function Footer() {
const ref = React.useRef<HTMLElement | null>(null)
const isInView = useInView(ref, {
once: true,
margin: '100px',
})
return (
<footer ref={ref} className={styles.footer} data-animate={isInView}>
<div className={styles.footerText}>
Crafted by{' '}
<a href="https://paco.me" target="_blank" rel="noopener noreferrer">
<img src="/paco.png" alt="Avatar of Paco" />
Paco
</a>{' '}
and{' '}
<a href="https://rauno.me" target="_blank" rel="noopener noreferrer">
<img src="/rauno.jpeg" alt="Avatar of Rauno" />
Rauno
</a>
</div>
<RaunoSignature />
<PacoSignature />
</footer>
)
}
function RaunoSignature() {
return (
<motion.svg
initial={{ opacity: 1 }}
whileInView={{ opacity: 0 }}
transition={{ delay: 2.5 }}
viewport={{ once: true }}
className={styles.raunoSignature}
width="356"
height="118"
viewBox="0 0 356 118"
fill="none"
>
<path
d="M39.6522 10.8727C32.0622 19.9486 23.7402 27.7351 17.4485 37.93C14.1895 43.2106 10.8425 48.7619 8.15072 54.3365M2 4.56219C30.9703 4.28687 59.8154 4.46461 88.706 2M5.10832 31.8394C13.3342 30.3515 21.957 30.4518 30.2799 30.1261C32.4305 30.042 44.8189 31.0777 46.043 28.5427M35.5504 60.1056C40.7881 57.8276 45.1269 55.9145 45.2348 49.7269C45.2992 46.04 42.3852 43.6679 39.7347 41.6068C37.1441 39.5922 35.2035 40.7255 34.7931 43.7239C34.4752 46.0474 34.2899 48.3127 37.0257 48.7777C42.1989 49.6571 48.6039 49.4477 53.6739 48.0927C55.9963 47.472 58.0383 46.5469 59.7769 44.897C61.5598 43.2051 59.4798 48.3421 59.2622 48.8504C57.0455 54.0293 55.0028 57.9764 61.8826 60.0079C65.247 61.0013 68.6702 59.0371 71.8755 58.2384C74.4094 57.607 78.1527 57.4135 79.4538 54.7188C80.3093 52.9471 79.5946 45.3814 78.0185 44.19C77.8193 44.0395 70.1595 58.7844 70.5548 61.5199C71.083 65.1755 85.5921 60.8116 87.8354 59.9155C93.0005 57.8521 101.259 42.1787 98.0502 46.7216C96.0097 49.6102 94.8149 54.7237 94.0336 58.1224C93.9591 58.4465 92.9251 63.1692 94.3224 62.558C100.1 60.0307 107.906 58.9913 111.843 53.589C116.212 47.5929 116.624 39.2412 120.13 32.719C123.998 25.5256 110.938 47.1508 110.652 55.3129C110.53 58.8278 110.847 62.2658 113.478 64.8739C115.031 66.4132 118.704 68.7663 120.95 67.3511C122.633 66.2906 122.854 63.0236 123.332 61.285C123.533 60.558 124.804 54.7916 125.523 57.8018C127.423 65.7487 134.234 63.8099 139.205 59.3585C141.166 57.6021 143.163 55.3598 143.895 52.7674C144.073 52.137 144.083 50.0543 142.883 50.96C140.761 52.5616 132.552 63.5513 136.828 65.8799C140.973 68.1366 147.493 69.2386 151.211 66.0229C153.763 63.8167 155.807 60.4623 157.011 57.3295C157.374 56.3842 159.996 48.1819 158.697 47.5545C157.253 46.8572 157.109 52.813 157.414 53.5674C158.282 55.7108 161.296 55.7058 163.208 55.4606C164.958 55.2361 168.071 54.7284 169.248 53.2144C170.028 52.2114 170.241 55.5535 170.738 56.7227C172.225 60.2188 177.289 62.6928 181.044 61.096C183.988 59.8437 186.231 55.0676 189.15 54.6094C192.701 54.052 190.67 50.7455 188.287 49.8024C180.738 46.815 172.87 57.705 176.69 64.571C177.646 66.2894 181.226 63.8978 182.329 63.5067C188.555 61.2998 194.823 59.1513 199.465 54.2015C200.301 53.3106 200.377 52.9071 199.546 54.504C197.173 59.0586 195.315 63.8749 193.213 68.5549C190.335 74.9632 187.327 81.8528 182.771 87.2918C171.982 100.172 154.827 106.815 139.004 110.814C107.54 118.768 70.3986 118.508 39.9452 106.375C37.0775 105.233 32.6626 103.665 30.3512 101.309C28.0213 98.9348 36.0214 97.3532 39.3217 96.9357C56.758 94.7296 74.5289 94.2763 92.0549 93.4762C135.849 91.4768 179.752 90.2295 223.344 85.2523C252.079 81.9713 280.556 77.0898 308.262 68.6373C317.289 65.8833 330.847 60.7964 339.74 56.4402C358.309 47.3441 339.301 55.8458 353.656 47.521M100.748 33.252C100.877 36.5762 102.167 37.0453 102.123 33.916"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
pathLength={1}
/>
</motion.svg>
)
}
function PacoSignature() {
return (
<motion.svg
className={styles.pacoSignature}
width="892"
height="235"
viewBox="0 0 892 235"
fill="none"
initial={{ opacity: 1 }}
whileInView={{ opacity: 0 }}
transition={{ delay: 2.5 }}
viewport={{ once: true }}
>
<path
d="M86.684 24.8853C84.684 64.5519 81.884 144.085 86.684 144.885M39.684 8.88526C68.3506 0.385261 131.984 -7.11474 157.184 30.8853C182.384 68.8853 96.3507 111.719 50.184 128.385C26.8506 138.885 -14.116 162.085 8.68398 170.885"
stroke="currentColor"
strokeWidth="9"
pathLength={1}
/>
<path
d="M325.184 46.8853C294.184 43.5519 231.684 60.3853 244.684 143.885C280.184 193.385 371.684 142.885 388.684 134.885C399.684 127.552 420.584 112.885 416.184 112.885C410.684 112.885 428.184 129.385 437.684 130.385C447.184 131.385 481.184 110.885 489.684 114.885C498.184 118.885 542.684 129.885 550.684 172.885C558.684 215.885 534.684 226.385 526.684 231.385C518.684 236.385 481.184 214.885 483.184 199.385C485.184 183.885 502.684 152.885 520.684 143.885C538.684 134.885 618.684 83.3853 762.684 83.3853C877.884 83.3853 894.351 80.7186 888.184 79.3853"
stroke="currentColor"
strokeWidth="9"
pathLength={1}
/>
<path
d="M143.988 132.079C142.168 132.664 140.426 133.273 138.785 134.307C137.602 135.052 136.639 136.008 135.799 137.117C135.239 137.856 134.695 138.701 134.743 139.671C134.853 141.857 138.728 140.36 139.712 139.916C141.396 139.157 142.992 138.066 144.34 136.808C144.939 136.249 145.832 135.423 145.673 134.488C145.427 133.044 141.601 133.881 140.843 134.019C139.375 134.287 137.534 134.645 137.388 136.418C137.251 138.081 139.708 137.088 140.469 136.738C140.847 136.565 144.28 134.356 143.343 133.705C142.07 132.819 140.471 134.865 139.691 135.619C139.27 136.026 137.078 137.59 138.577 138.061C139.847 138.46 141.551 137.108 142.437 136.392C142.594 136.265 143.818 135.405 143.658 135.075C143.455 134.656 142.071 134.774 141.749 134.808C140.582 134.932 139.512 135.552 138.55 136.184C138.184 136.424 137.281 136.915 137.654 137.144C137.914 137.302 138.113 137.435 138.401 137.549C139.178 137.856 140.088 137.628 140.832 137.255C142.185 136.579 143.389 135.45 144.457 134.382C144.666 134.173 145.14 133.692 145.14 133.374C145.14 133.019 144.968 132.525 144.612 132.367C143.862 132.033 143.442 132.242 142.719 132.58C142.217 132.814 141.792 133.137 141.397 133.518C140.401 134.48 139.261 135.281 138.273 136.259C137.694 136.832 136.936 137.472 136.561 138.22C136.275 138.794 136.605 139.184 137.239 139.031C138.012 138.844 138.778 138.695 139.558 138.551C140.026 138.465 140.57 138.301 140.725 137.837"
stroke="currentColor"
strokeWidth="9"
strokeLinecap="round"
pathLength={1}
/>
</motion.svg>
)
}
================================================
FILE: website/public/robots.txt
================================================
User-agent: *
Disallow:
================================================
FILE: website/styles/cmdk/framer.scss
================================================
.framer {
[cmdk-root] {
max-width: 640px;
width: 100%;
padding: 8px;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
font-family: var(--font-sans);
border: 1px solid var(--gray6);
box-shadow: var(--cmdk-shadow);
outline: none;
.dark & {
background: var(--gray2);
}
}
[cmdk-framer-header] {
display: flex;
align-items: center;
gap: 8px;
height: 48px;
padding: 0 8px;
border-bottom: 1px solid var(--gray5);
margin-bottom: 12px;
padding-bottom: 8px;
svg {
width: 20px;
height: 20px;
color: var(--gray9);
transform: translateY(1px);
}
}
[cmdk-input] {
font-family: var(--font-sans);
border: none;
width: 100%;
font-size: 16px;
outline: none;
background: var(--bg);
color: var(--gray12);
&::placeholder {
color: var(--gray9);
}
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
border-radius: 12px;
font-size: 14px;
display: flex;
align-items: center;
gap: 12px;
color: var(--gray12);
padding: 8px 8px;
margin-right: 8px;
font-weight: 500;
transition: all 150ms ease;
transition-property: none;
&[data-selected='true'] {
background: var(--blue9);
color: #ffffff;
[cmdk-framer-item-subtitle] {
color: #ffffff;
}
}
&[data-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
& + [cmdk-item] {
margin-top: 4px;
}
svg {
width: 16px;
height: 16px;
color: #ffffff;
}
}
[cmdk-framer-icon-wrapper] {
display: flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
background: orange;
border-radius: 8px;
}
[cmdk-framer-item-meta] {
display: flex;
flex-direction: column;
gap: 4px;
}
[cmdk-framer-item-subtitle] {
font-size: 12px;
font-weight: 400;
color: var(--gray11);
}
[cmdk-framer-items] {
min-height: 308px;
display: flex;
}
[cmdk-framer-left] {
width: 40%;
}
[cmdk-framer-separator] {
width: 1px;
border: 0;
margin-right: 8px;
background: var(--gray6);
}
[cmdk-framer-right] {
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
margin-left: 8px;
width: 60%;
button {
width: 120px;
height: 40px;
background: var(--blue9);
border-radius: 6px;
font-weight: 500;
color: white;
font-size: 14px;
}
input[type='text'] {
height: 40px;
width: 160px;
border: 1px solid var(--gray6);
background: #ffffff;
border-radius: 6px;
padding: 0 8px;
font-size: 14px;
font-family: var(--font-sans);
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.08);
&::placeholder {
color: var(--gray9);
}
@media (prefers-color-scheme: dark) {
background: var(--gray3);
}
}
[cmdk-framer-radio] {
display: flex;
align-items: center;
gap: 4px;
color: var(--gray12);
font-weight: 500;
font-size: 14px;
accent-color: var(--blue9);
input {
width: 20px;
height: 20px;
}
}
img {
width: 40px;
height: 40px;
border-radius: 9999px;
border: 1px solid var(--gray6);
}
[cmdk-framer-container] {
width: 100px;
height: 100px;
background: var(--blue9);
border-radius: 16px;
}
[cmdk-framer-badge] {
background: var(--blue3);
padding: 0 8px;
height: 28px;
font-size: 14px;
line-height: 28px;
color: var(--blue11);
border-radius: 9999px;
font-weight: 500;
}
[cmdk-framer-slider] {
height: 20px;
width: 200px;
background: linear-gradient(90deg, var(--blue9) 40%, var(--gray3) 0%);
border-radius: 9999px;
div {
width: 20px;
height: 20px;
background: #ffffff;
border-radius: 9999px;
box-shadow: 0 1px 3px -1px rgba(0, 0, 0, 0.32);
transform: translateX(70px);
}
}
}
[cmdk-list] {
overflow: auto;
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--gray5);
margin: 4px 0;
}
[cmdk-group-heading] {
user-select: none;
font-size: 12px;
color: var(--gray11);
padding: 0 8px;
display: flex;
align-items: center;
margin-bottom: 8px;
}
[cmdk-empty] {
font-size: 14px;
padding: 32px;
white-space: pre-wrap;
color: var(--gray11);
}
}
@media (max-width: 640px) {
.framer {
[cmdk-framer-icon-wrapper] {
}
[cmdk-framer-item-subtitle] {
display: none;
}
}
}
================================================
FILE: website/styles/cmdk/linear.scss
================================================
.linear {
[cmdk-root] {
max-width: 640px;
width: 100%;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
padding: 0;
font-family: var(--font-sans);
box-shadow: var(--cmdk-shadow);
outline: none;
.dark & {
background: linear-gradient(136.61deg, rgb(39, 40, 43) 13.72%, rgb(45, 46, 49) 74.3%);
}
}
[cmdk-linear-badge] {
height: 24px;
padding: 0 8px;
font-size: 12px;
color: var(--gray11);
background: var(--gray3);
border-radius: 4px;
width: fit-content;
display: flex;
align-items: center;
margin: 16px 16px 0;
}
[cmdk-linear-shortcuts] {
display: flex;
margin-left: auto;
gap: 8px;
kbd {
font-family: var(--font-sans);
font-size: 13px;
color: var(--gray11);
}
}
[cmdk-input] {
font-family: var(--font-sans);
border: none;
width: 100%;
font-size: 18px;
padding: 20px;
outline: none;
background: var(--bg);
color: var(--gray12);
border-bottom: 1px solid var(--gray6);
border-radius: 0;
caret-color: #6e5ed2;
margin: 0;
&::placeholder {
color: var(--gray9);
}
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
height: 48px;
font-size: 14px;
display: flex;
align-items: center;
gap: 12px;
padding: 0 16px;
color: var(--gray12);
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
position: relative;
&[data-selected='true'] {
background: var(--gray3);
svg {
color: var(--gray12);
}
&:after {
content: '';
position: absolute;
left: 0;
z-index: 123;
width: 3px;
height: 100%;
background: #5f6ad2;
}
}
&[data-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
&:active {
transition-property: background;
background: var(--gray4);
}
& + [cmdk-item] {
margin-top: 4px;
}
svg {
width: 16px;
height: 16px;
color: var(--gray10);
}
}
[cmdk-list] {
height: min(300px, var(--cmdk-list-height));
max-height: 400px;
overflow: auto;
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
}
[cmdk-group-heading] {
user-select: none;
font-size: 12px;
color: var(--gray11);
padding: 0 8px;
display: flex;
align-items: center;
}
[cmdk-empty] {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
height: 64px;
white-space: pre-wrap;
color: var(--gray11);
}
}
================================================
FILE: website/styles/cmdk/raycast.scss
================================================
.raycast {
[cmdk-root] {
max-width: 640px;
width: 100%;
background: var(--gray1);
border-radius: 12px;
padding: 8px 0;
font-family: var(--font-sans);
box-shadow: var(--cmdk-shadow);
border: 1px solid var(--gray6);
position: relative;
outline: none;
.dark & {
background: var(--gray2);
border: 0;
&:after {
content: '';
background: linear-gradient(
to right,
var(--gray6) 20%,
var(--gray6) 40%,
var(--gray10) 50%,
var(--gray10) 55%,
var(--gray6) 70%,
var(--gray6) 100%
);
z-index: -1;
position: absolute;
border-radius: 12px;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
animation: shine 3s ease forwards 0.1s;
background-size: 200% auto;
}
&:before {
content: '';
z-index: -1;
position: absolute;
border-radius: 12px;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: calc(100% + 2px);
box-shadow: 0 0 0 1px transparent;
animation: border 1s linear forwards 0.5s;
}
}
kbd {
font-family: var(--font-sans);
background: var(--gray3);
color: var(--gray11);
height: 20px;
width: 20px;
border-radius: 4px;
padding: 0 4px;
display: flex;
align-items: center;
justify-content: center;
&:first-of-type {
margin-left: 8px;
}
}
}
[cmdk-input] {
font-family: var(--font-sans);
border: none;
width: 100%;
font-size: 15px;
padding: 8px 16px;
outline: none;
background: var(--bg);
color: var(--gray12);
&::placeholder {
color: var(--gray9);
}
}
[cmdk-raycast-top-shine] {
.dark & {
background: linear-gradient(
90deg,
rgba(56, 189, 248, 0),
var(--gray5) 20%,
var(--gray9) 67.19%,
rgba(236, 72, 153, 0)
);
height: 1px;
position: absolute;
top: -1px;
width: 100%;
z-index: -1;
opacity: 0;
animation: showTopShine 0.1s ease forwards 0.2s;
}
}
[cmdk-raycast-loader] {
--loader-color: var(--gray9);
border: 0;
width: 100%;
left: 0;
height: 1px;
background: var(--gray6);
position: relative;
overflow: visible;
display: block;
margin-top: 12px;
margin-bottom: 12px;
&:after {
content: '';
width: 50%;
height: 1px;
position: absolute;
background: linear-gradient(90deg, transparent 0%, var(--loader-color) 50%, transparent 100%);
top: -1px;
opacity: 0;
animation-duration: 1.5s;
animation-delay: 1s;
animation-timing-function: ease;
animation-name: loading;
}
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
height: 40px;
border-radius: 8px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
color: var(--gray12);
user-select: none;
will-change: background, color;
transition: all 150ms ease;
&[data-selected='true'] {
background: var(--gray4);
color: var(--gray12);
}
&[data-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
&:active {
transition-property: background;
background: var(--gray4);
}
&:first-child {
margin-top: 8px;
}
& + [cmdk-item] {
margin-top: 4px;
}
svg {
width: 18px;
height: 18px;
}
}
[cmdk-raycast-meta] {
margin-left: auto;
color: var(--gray11);
font-size: 13px;
}
[cmdk-list] {
padding: 0 8px;
height: 393px;
overflow: auto;
overscroll-behavior: contain;
scroll-padding-block-end: 40px;
transition: 100ms ease;
transition-property: height;
padding-bottom: 40px;
}
[cmdk-raycast-open-trigger],
[cmdk-raycast-subcommand-trigger] {
color: var(--gray11);
padding: 0px 4px 0px 8px;
border-radius: 6px;
font-weight: 500;
font-size: 12px;
height: 28px;
letter-spacing: -0.25px;
}
[cmdk-raycast-clipboard-icon],
[cmdk-raycast-hammer-icon] {
width: 20px;
height: 20px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
svg {
width: 14px;
height: 14px;
}
}
[cmdk-raycast-clipboard-icon] {
background: linear-gradient(to bottom, #f55354, #eb4646);
}
[cmdk-raycast-hammer-icon] {
background: linear-gradient(to bottom, #6cb9a3, #2c6459);
}
[cmdk-raycast-open-trigger] {
display: flex;
align-items: center;
color: var(--gray12);
}
[cmdk-raycast-subcommand-trigger] {
display: flex;
align-items: center;
gap: 4px;
right: 8px;
bottom: 8px;
svg {
width: 14px;
height: 14px;
}
hr {
height: 100%;
background: var(--gray6);
border: 0;
width: 1px;
}
&[aria-expanded='true'],
&:hover {
background: var(--gray4);
kbd {
background: var(--gray7);
}
}
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--gray5);
margin: 4px 0;
}
*:not([hidden]) + [cmdk-group] {
margin-top: 8px;
}
[cmdk-group-heading] {
user-select: none;
font-size: 12px;
color: var(--gray11);
padding: 0 8px;
display: flex;
align-items: center;
}
[cmdk-raycast-footer] {
display: flex;
height: 40px;
align-items: center;
width: 100%;
position: absolute;
background: var(--gray1);
bottom: 0;
padding: 8px;
border-top: 1px solid var(--gray6);
border-radius: 0 0 12px 12px;
svg {
width: 20px;
height: 20px;
filter: grayscale(1);
margin-right: auto;
}
hr {
height: 12px;
width: 1px;
border: 0;
background: var(--gray6);
margin: 0 4px 0px 12px;
}
@media (prefers-color-scheme: dark) {
background: var(--gray2);
}
}
[cmdk-dialog] {
z-index: var(--layer-portal);
position: fixed;
left: 50%;
top: var(--page-top);
transform: translateX(-50%);
[cmdk] {
width: 640px;
transform-origin: center center;
animation: dialogIn var(--transition-fast) forwards;
}
&[data-state='closed'] [cmdk] {
animation: dialogOut var(--transition-fast) forwards;
}
}
[cmdk-empty] {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
height: 64px;
white-space: pre-wrap;
color: var(--gray11);
}
}
@keyframes loading {
0% {
opacity: 0;
transform: translateX(0);
}
50% {
opacity: 1;
transform: translateX(100%);
}
100% {
opacity: 0;
transform: translateX(0);
}
}
@keyframes shine {
to {
background-position: 200% center;
opacity: 0;
}
}
@keyframes border {
to {
box-shadow: 0 0 0 1px var(--gray6);
}
}
@keyframes showTopShine {
to {
opacity: 1;
}
}
.raycast-submenu {
[cmdk-root] {
display: flex;
flex-direction: column;
width: 320px;
border: 1px solid var(--gray6);
background: var(--gray2);
border-radius: 8px;
}
[cmdk-list] {
padding: 8px;
overflow: auto;
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
}
[cmdk-item] {
cursor: pointer;
height: 40px;
border-radius: 8px;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
color: var(--gray12);
user-select: none;
will-change: background, color;
transition: all 150ms ease;
&[aria-selected='true'] {
background: var(--gray5);
color: var(--gray12);
[cmdk-raycast-submenu-shortcuts] kbd {
background: var(--gray7);
}
}
&[aria-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
svg {
width: 16px;
height: 16px;
}
[cmdk-raycast-submenu-shortcuts] {
display: flex;
margin-left: auto;
gap: 2px;
kbd {
font-family: var(--font-sans);
background: var(--gray5);
color: var(--gray11);
height: 20px;
width: 20px;
border-radius: 4px;
padding: 0 4px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
&:first-of-type {
margin-left: 8px;
}
}
}
}
[cmdk-group-heading] {
text-transform: capitalize;
font-size: 12px;
color: var(--gray11);
font-weight: 500;
margin-bottom: 8px;
margin-top: 8px;
margin-left: 4px;
}
[cmdk-input] {
padding: 12px;
font-family: var(--font-sans);
border: 0;
border-top: 1px solid var(--gray6);
font-size: 13px;
background: transparent;
margin-top: auto;
width: 100%;
outline: 0;
border-radius: 0;
}
animation-duration: 0.2s;
animation-timing-function: ease;
animation-fill-mode: forwards;
transform-origin: var(--radix-popover-content-transform-origin);
&[data-state='open'] {
animation-name: slideIn;
}
&[data-state='closed'] {
animation-name: slideOut;
}
[cmdk-empty] {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
white-space: pre-wrap;
font-size: 14px;
color: var(--gray11);
}
}
@keyframes slideIn {
0% {
opacity: 0;
transform: scale(0.96);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideOut {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.96);
}
}
@media (max-width: 640px) {
.raycast {
[cmdk-input] {
font-size: 16px;
}
}
}
================================================
FILE: website/styles/cmdk/vercel.scss
================================================
.vercel {
[cmdk-root] {
max-width: 640px;
width: 100%;
padding: 8px;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
font-family: var(--font-sans);
border: 1px solid var(--gray6);
box-shadow: var(--cmdk-shadow);
transition: transform 100ms ease;
outline: none;
.dark & {
background: rgba(22, 22, 22, 0.7);
}
}
[cmdk-input] {
font-family: var(--font-sans);
border: none;
width: 100%;
font-size: 17px;
padding: 8px 8px 16px 8px;
outline: none;
background: var(--bg);
color: var(--gray12);
border-bottom: 1px solid var(--gray6);
margin-bottom: 16px;
border-radius: 0;
&::placeholder {
color: var(--gray9);
}
}
[cmdk-vercel-badge] {
height: 20px;
background: var(--grayA3);
display: inline-flex;
align-items: center;
padding: 0 8px;
font-size: 12px;
color: var(--grayA11);
border-radius: 4px;
margin: 4px 0 4px 4px;
user-select: none;
text-transform: capitalize;
font-weight: 500;
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
height: 48px;
border-radius: 8px;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px;
color: var(--gray11);
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
&[data-selected='true'] {
background: var(--grayA3);
color: var(--gray12);
}
&[data-disabled='true'] {
color: var(--gray8);
cursor: not-allowed;
}
&:active {
transition-property: background;
background: var(--gray4);
}
& + [cmdk-item] {
margin-top: 4px;
}
svg {
width: 18px;
height: 18px;
}
}
[cmdk-list] {
height: min(330px, calc(var(--cmdk-list-height)));
max-height: 400px;
overflow: auto;
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
}
[cmdk-vercel-shortcuts] {
display: flex;
margin-left: auto;
gap: 8px;
kbd {
font-family: var(--font-sans);
font-size: 12px;
min-width: 20px;
padding: 4px;
height: 20px;
border-radius: 4px;
color: var(--gray11);
background: var(--gray4);
display: inline-flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
}
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--gray5);
margin: 4px 0;
}
*:not([hidden]) + [cmdk-group] {
margin-top: 8px;
}
[cmdk-group-heading] {
user-select: none;
font-size: 12px;
color: var(--gray11);
padding: 0 8px;
display: flex;
align-items: center;
margin-bottom: 8px;
}
[cmdk-empty] {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
height: 48px;
white-space: pre-wrap;
color: var(--gray11);
}
}
================================================
FILE: website/styles/globals.scss
================================================
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900; // Range of weights supported
font-display: optional;
src: url(/inter-var-latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
::selection {
background: hotpink;
color: white;
}
html,
body {
padding: 0;
margin: 0;
font-family: var(--font-sans);
}
body {
background: var(--app-bg);
overflow-x: hidden;
}
button {
background: none;
font-family: var(--font-sans);
padding: 0;
border: 0;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
*,
*::after,
*::before {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:root {
--font-sans: 'Inter', --apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
Droid Sans, Helvetica Neue, sans-serif;
--app-bg: var(--gray1);
--cmdk-shadow: 0 16px 70px rgb(0 0 0 / 20%);
--lowContrast: #ffffff;
--highContrast: #000000;
--gray1: hsl(0, 0%, 99%);
--gray2: hsl(0, 0%, 97.3%);
--gray3: hsl(0, 0%, 95.1%);
--gray4: hsl(0, 0%, 93%);
--gray5: hsl(0, 0%, 90.9%);
--gray6: hsl(0, 0%, 88.7%);
--gray7: hsl(0, 0%, 85.8%);
--gray8: hsl(0, 0%, 78%);
--gray9: hsl(0, 0%, 56.1%);
--gray10: hsl(0, 0%, 52.3%);
--gray11: hsl(0, 0%, 43.5%);
--gray12: hsl(0, 0%, 9%);
--grayA1: hsla(0, 0%, 0%, 0.012);
--grayA2: hsla(0, 0%, 0%, 0.027);
--grayA3: hsla(0, 0%, 0%, 0.047);
--grayA4: hsla(0, 0%, 0%, 0.071);
--grayA5: hsla(0, 0%, 0%, 0.09);
--grayA6: hsla(0, 0%, 0%, 0.114);
--grayA7: hsla(0, 0%, 0%, 0.141);
--grayA8: hsla(0, 0%, 0%, 0.22);
--grayA9: hsla(0, 0%, 0%, 0.439);
--grayA10: hsla(0, 0%, 0%, 0.478);
--grayA11: hsla(0, 0%, 0%, 0.565);
--grayA12: hsla(0, 0%, 0%, 0.91);
--blue1: hsl(206, 100%, 99.2%);
--blue2: hsl(210, 100%, 98%);
--blue3: hsl(209, 100%, 96.5%);
--blue4: hsl(210, 98.8%, 94%);
--blue5: hsl(209, 95%, 90.1%);
--blue6: hsl(209, 81.2%, 84.5%);
--blue7: hsl(208, 77.5%, 76.9%);
--blue8: hsl(206, 81.9%, 65.3%);
--blue9: hsl(206, 100%, 50%);
--blue10: hsl(208, 100%, 47.3%);
--blue11: hsl(211, 100%, 43.2%);
--blue12: hsl(211, 100%, 15%);
}
.dark {
--app-bg: var(--gray1);
--lowContrast: #000000;
--highContrast: #ffffff;
--gray1: hsl(0, 0%, 8.5%);
--gray2: hsl(0, 0%, 11%);
--gray3: hsl(0, 0%, 13.6%);
--gray4: hsl(0, 0%, 15.8%);
--gray5: hsl(0, 0%, 17.9%);
--gray6: hsl(0, 0%, 20.5%);
--gray7: hsl(0, 0%, 24.3%);
--gray8: hsl(0, 0%, 31.2%);
--gray9: hsl(0, 0%, 43.9%);
--gray10: hsl(0, 0%, 49.4%);
--gray11: hsl(0, 0%, 62.8%);
--gray12: hsl(0, 0%, 93%);
--grayA1: hsla(0, 0%, 100%, 0);
--grayA2: hsla(0, 0%, 100%, 0.026);
--grayA3: hsla(0, 0%, 100%, 0.056);
--grayA4: hsla(0, 0%, 100%, 0.077);
--grayA5: hsla(0, 0%, 100%, 0.103);
--grayA6: hsla(0, 0%, 100%, 0.129);
--grayA7: hsla(0, 0%, 100%, 0.172);
--grayA8: hsla(0, 0%, 100%, 0.249);
--grayA9: hsla(0, 0%, 100%, 0.386);
--grayA10: hsla(0, 0%, 100%, 0.446);
--grayA11: hsla(0, 0%, 100%, 0.592);
--grayA12: hsla(0, 0%, 100%, 0.923);
--blue1: hsl(212, 35%, 9.2%);
--blue2: hsl(216, 50%, 11.8%);
--blue3: hsl(214, 59.4%, 15.3%);
--blue4: hsl(214, 65.8%, 17.9%);
--blue5: hsl(213, 71.2%, 20.2%);
--blue6: hsl(212, 77.4%, 23.1%);
--blue7: hsl(211, 85.1%, 27.4%);
--blue8: hsl(211, 89.7%, 34.1%);
--blue9: hsl(206, 100%, 50%);
--blue10: hsl(209, 100%, 60.6%);
--blue11: hsl(210, 100%, 66.1%);
--blue12: hsl(206, 98%, 95.8%);
}
================================================
FILE: website/styles/index.module.scss
================================================
.main {
width: 100vw;
min-height: 100vh;
position: relative;
display: flex;
justify-content: center;
padding: 120px 24px 160px 24px;
&:before {
background: radial-gradient(circle, rgba(2, 0, 36, 0) 0, var(--gray1) 100%);
position: absolute;
content: '';
z-index: 2;
width: 100%;
height: 100%;
top: 0;
}
&:after {
content: '';
background-image: url('/grid.svg');
z-index: -1;
position: absolute;
width: 100%;
height: 100%;
top: 0;
opacity: 0.2;
filter: invert(1);
@media (prefers-color-scheme: dark) {
filter: unset;
}
}
h1 {
font-size: 32px;
color: var(--gray12);
font-weight: 600;
letter-spacing: -2px;
line-height: 40px;
}
p {
color: var(--gray11);
margin-top: 8px;
font-size: 16px;
}
}
.content {
height: fit-content;
position: relative;
z-index: 3;
width: 100%;
max-width: 640px;
&:after {
background-image: radial-gradient(at 27% 37%, hsla(215, 98%, 61%, 1) 0px, transparent 50%),
radial-gradient(at 97% 21%, hsla(256, 98%, 72%, 1) 0px, transparent 50%),
radial-gradient(at 52% 99%, hsla(354, 98%, 61%, 1) 0px, transparent 50%),
radial-gradient(at 10% 29%, hsla(133, 96%, 67%, 1) 0px, transparent 50%),
radial-gradient(at 97% 96%, hsla(38, 60%, 74%, 1) 0px, transparent 50%),
radial-gradient(at 33% 50%, hsla(222, 67%, 73%, 1) 0px, transparent 50%),
radial-gradient(at 79% 53%, hsla(343, 68%, 79%, 1) 0px, transparent 50%);
position: absolute;
content: '';
z-index: 2;
width: 100%;
height: 100%;
filter: blur(100px) saturate(150%);
z-index: -1;
top: 80px;
opacity: 0.2;
transform: translateZ(0);
@media (prefers-color-scheme: dark) {
opacity: 0.1;
}
}
}
.meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 48px;
flex-wrap: wrap;
gap: 16px;
}
.buttons {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
transform: translateY(12px);
}
.githubButton,
.installButton,
.switcher button {
height: 40px;
color: var(--gray12);
border-radius: 9999px;
font-size: 14px;
transition-duration: 150ms;
transition-property: background, color, transform;
transition-timing-function: ease-in;
will-change: transform;
}
.githubButton {
width: 177px;
padding: 0 12px;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 500;
&:hover {
background: var(--grayA3);
}
&:active {
background: var(--grayA5);
transform: scale(0.97);
}
&:focus-visible {
outline: 0;
outline: 2px solid var(--gray7);
}
}
.installButton {
background: var(--grayA3);
display: flex;
align-items: center;
gap: 16px;
padding: 0px 8px 0 16px;
cursor: copy;
font-weight: 500;
&:hover {
background: var(--grayA4);
span {
background: var(--grayA5);
svg {
color: var(--gray12);
}
}
}
&:focus-visible {
outline: 0;
outline: 2px solid var(--gray7);
outline-offset: 2px;
}
&:active {
background: var(--gray5);
transform: scale(0.97);
}
span {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
background: var(--grayA3);
border-radius: 9999px;
transition: background 150ms ease;
svg {
size: 16px;
color: var(--gray11);
transition: color 150ms ease;
}
}
}
.switcher {
display: grid;
grid-template-columns: repeat(4, 100px);
align-items: center;
justify-content: center;
gap: 4px;
margin-top: 48px;
position: relative;
button {
height: 32px;
line-height: 32px;
display: flex;
align-items: center;
margin: auto;
gap: 8px;
padding: 0 16px;
border-radius: 9999px;
color: var(--gray11);
font-size: 14px;
cursor: pointer;
user-select: none;
position: relative;
text-transform: capitalize;
&:hover {
color: var(--gray12);
}
&:active {
transform: scale(0.96);
}
&:focus-visible {
outline: 0;
outline: 2px solid var(--gray7);
}
svg {
width: 14px;
height: 14px;
}
&[data-selected='true'] {
color: var(--gray12);
&:hover .activeTheme {
background: var(--grayA6);
}
&:active {
transform: scale(0.96);
.activeTheme {
background: var(--grayA7);
}
}
}
}
.activeTheme {
background: var(--grayA5);
border-radius: 9999px;
height: 32px;
width: 100%;
top: 0;
position: absolute;
left: 0;
}
.arrow {
color: var(--gray11);
user-select: none;
position: absolute;
}
}
.header {
position: absolute;
left: 0;
top: -64px;
gap: 8px;
background: var(--gray3);
padding: 4px;
display: flex;
align-items: center;
border-radius: 9999px;
button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 4px;
border-radius: 9999px;
color: var(--gray11);
svg {
width: 16px;
height: 16px;
}
&[aria-selected='true'] {
background: #ffffff;
color: var(--gray12);
box-shadow: 0px 2px 5px -2px rgb(0 0 0 / 15%), 0 1px 3px -1px rgb(0 0 0 / 20%);
}
}
}
.versionBadge {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--grayA11);
background: var(--grayA3);
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
font-size: 14px;
margin-bottom: 8px;
@media (prefers-color-scheme: dark) {
background: var(--grayA2);
}
}
.codeBlock {
margin-top: 72px;
position: relative;
}
.footer {
display: flex;
align-items: center;
gap: 4px;
width: fit-content;
margin: 32px auto;
bottom: 16px;
color: var(--gray11);
font-size: 13px;
z-index: 3;
position: absolute;
bottom: 0;
a {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--gray12);
font-weight: 500;
border-radius: 9999px;
padding: 4px;
margin: 0 -2px;
transition: background 150ms ease;
&:hover,
&:focus-visible {
background: var(--grayA4);
outline: 0;
}
}
img {
width: 20px;
height: 20px;
border: 1px solid var(--gray5);
border-radius: 9999px;
}
}
.line {
height: 20px;
width: 180px;
margin: 64px auto;
background-image: url('/line.svg');
filter: invert(1);
mask-image: linear-gradient(90deg, transparent, #fff 4rem, #fff calc(100% - 4rem), transparent);
@media (prefers-color-scheme: dark) {
filter: unset;
}
}
.line2 {
height: 1px;
width: 300px;
background: var(--gray7);
position: absolute;
top: 0;
mask-image: linear-gradient(90deg, transparent, #fff 4rem, #fff calc(100% - 4rem), transparent);
}
.line3 {
height: 300px;
width: calc(100% + 32px);
position: absolute;
top: -16px;
left: -16px;
border-radius: 16px 16px 0 0;
--size: 1px;
--gradient: linear-gradient(to top, var
gitextract_vtq8gdaf/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .prettierignore
├── .prettierrc.js
├── ARCHITECTURE.md
├── LICENSE.md
├── README.md
├── cmdk/
│ ├── package.json
│ ├── src/
│ │ ├── command-score.ts
│ │ └── index.tsx
│ └── tsup.config.ts
├── package.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── test/
│ ├── basic.test.ts
│ ├── dialog.test.ts
│ ├── group.test.ts
│ ├── item.test.ts
│ ├── keybind.test.ts
│ ├── next-env.d.ts
│ ├── numeric.test.ts
│ ├── package.json
│ ├── pages/
│ │ ├── _app.tsx
│ │ ├── dialog.tsx
│ │ ├── group.tsx
│ │ ├── huge.tsx
│ │ ├── index.tsx
│ │ ├── item-advanced.tsx
│ │ ├── item.tsx
│ │ ├── keybinds.tsx
│ │ ├── numeric.tsx
│ │ ├── portal.tsx
│ │ └── props.tsx
│ ├── props.test.ts
│ ├── style.css
│ └── tsconfig.json
├── tsconfig.json
└── website/
├── .eslintrc.json
├── .gitignore
├── README.md
├── components/
│ ├── cmdk/
│ │ ├── framer.tsx
│ │ ├── linear.tsx
│ │ ├── raycast.tsx
│ │ └── vercel.tsx
│ ├── code/
│ │ ├── code.module.scss
│ │ └── index.tsx
│ ├── icons/
│ │ ├── icons.module.scss
│ │ └── index.tsx
│ └── index.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages/
│ ├── _app.tsx
│ ├── _document.tsx
│ └── index.tsx
├── public/
│ └── robots.txt
├── styles/
│ ├── cmdk/
│ │ ├── framer.scss
│ │ ├── linear.scss
│ │ ├── raycast.scss
│ │ └── vercel.scss
│ ├── globals.scss
│ └── index.module.scss
├── tsconfig.json
└── vercel.json
SYMBOL INDEX (123 symbols across 12 files)
FILE: cmdk/src/command-score.ts
function commandScoreInner (line 54) | function commandScoreInner(
function formatInput (line 150) | function formatInput(string) {
function commandScore (line 155) | function commandScore(string: string, abbreviation: string, aliases: str...
FILE: cmdk/src/index.tsx
type Children (line 10) | type Children = { children?: React.ReactNode }
type DivProps (line 11) | type DivProps = React.ComponentPropsWithoutRef<typeof Primitive.div>
type LoadingProps (line 13) | type LoadingProps = Children &
type EmptyProps (line 23) | type EmptyProps = Children & DivProps & {}
type SeparatorProps (line 24) | type SeparatorProps = DivProps & {
type DialogProps (line 28) | type DialogProps = RadixDialog.DialogProps &
type ListProps (line 37) | type ListProps = Children &
type ItemProps (line 44) | type ItemProps = Children &
type GroupProps (line 60) | type GroupProps = Children &
type InputProps (line 69) | type InputProps = Omit<React.ComponentPropsWithoutRef<typeof Primitive.i...
type CommandFilter (line 79) | type CommandFilter = (value: string, search: string, keywords?: string[]...
type CommandProps (line 80) | type CommandProps = Children &
type Context (line 123) | type Context = {
type State (line 137) | type State = {
type Store (line 143) | type Store = {
type Group (line 149) | type Group = {
constant GROUP_SELECTOR (line 154) | const GROUP_SELECTOR = `[cmdk-group=""]`
constant GROUP_ITEMS_SELECTOR (line 155) | const GROUP_ITEMS_SELECTOR = `[cmdk-group-items=""]`
constant GROUP_HEADING_SELECTOR (line 156) | const GROUP_HEADING_SELECTOR = `[cmdk-group-heading=""]`
constant ITEM_SELECTOR (line 157) | const ITEM_SELECTOR = `[cmdk-item=""]`
constant VALID_ITEM_SELECTOR (line 158) | const VALID_ITEM_SELECTOR = `${ITEM_SELECTOR}:not([aria-disabled="true"])`
constant SELECT_EVENT (line 159) | const SELECT_EVENT = `cmdk-item-select`
constant VALUE_ATTR (line 160) | const VALUE_ATTR = `data-value`
function score (line 362) | function score(value: string, keywords?: string[]) {
function sort (line 368) | function sort() {
function selectFirstItem (line 428) | function selectFirstItem() {
function filterItems (line 435) | function filterItems() {
function scrollSelectedIntoView (line 472) | function scrollSelectedIntoView() {
function getSelectedItem (line 488) | function getSelectedItem() {
function getValidItems (line 492) | function getValidItems() {
function updateSelectedToIndex (line 498) | function updateSelectedToIndex(index: number) {
function updateSelectedByItem (line 504) | function updateSelectedByItem(change: 1 | -1) {
function updateSelectedByGroup (line 524) | function updateSelectedByGroup(change: 1 | -1) {
function onSelect (line 693) | function onSelect() {
function select (line 698) | function select() {
function findNextSibling (line 963) | function findNextSibling(el: Element, selector: string) {
function findPreviousSibling (line 972) | function findPreviousSibling(el: Element, selector: string) {
function useAsRef (line 981) | function useAsRef<T>(data: T) {
function useLazyRef (line 993) | function useLazyRef<T>(fn: () => T) {
function useCmdk (line 1004) | function useCmdk<T = any>(selector: (state: State) => T): T {
function useValue (line 1010) | function useValue(
function renderChildren (line 1061) | function renderChildren(children: React.ReactElement) {
function SlottableWithNestedChildren (line 1071) | function SlottableWithNestedChildren(
FILE: test/pages/_app.tsx
function App (line 3) | function App({ Component, pageProps }) {
FILE: website/components/cmdk/framer.tsx
function FramerCMDK (line 4) | function FramerCMDK() {
function Button (line 57) | function Button() {
function Input (line 61) | function Input() {
function Badge (line 65) | function Badge() {
function Radio (line 69) | function Radio() {
function Slider (line 78) | function Slider() {
function Avatar (line 86) | function Avatar() {
function Container (line 90) | function Container() {
function Item (line 94) | function Item({ children, value, subtitle }: { children: React.ReactNode...
function ButtonIcon (line 106) | function ButtonIcon() {
function InputIcon (line 119) | function InputIcon() {
function RadioIcon (line 132) | function RadioIcon() {
function BadgeIcon (line 145) | function BadgeIcon() {
function ToggleIcon (line 158) | function ToggleIcon() {
function AvatarIcon (line 171) | function AvatarIcon() {
function ContainerIcon (line 184) | function ContainerIcon() {
function SearchIcon (line 197) | function SearchIcon() {
function SliderIcon (line 212) | function SliderIcon() {
FILE: website/components/cmdk/linear.tsx
function LinearCMDK (line 3) | function LinearCMDK() {
function AssignToIcon (line 68) | function AssignToIcon() {
function AssignToMeIcon (line 76) | function AssignToMeIcon() {
function ChangeStatusIcon (line 90) | function ChangeStatusIcon() {
function ChangePriorityIcon (line 103) | function ChangePriorityIcon() {
function ChangeLabelsIcon (line 113) | function ChangeLabelsIcon() {
function RemoveLabelIcon (line 125) | function RemoveLabelIcon() {
function SetDueDateIcon (line 137) | function SetDueDateIcon() {
FILE: website/components/cmdk/raycast.tsx
function RaycastCMDK (line 7) | function RaycastCMDK() {
function Item (line 97) | function Item({
function SubCommand (line 116) | function SubCommand({
function SubItem (line 200) | function SubItem({ children, shortcut }: { children: React.ReactNode; sh...
function TerminalIcon (line 213) | function TerminalIcon() {
function RaycastLightIcon (line 231) | function RaycastLightIcon() {
function RaycastDarkIcon (line 244) | function RaycastDarkIcon() {
function WindowIcon (line 257) | function WindowIcon() {
function FinderIcon (line 271) | function FinderIcon() {
function StarIcon (line 285) | function StarIcon() {
function ClipboardIcon (line 299) | function ClipboardIcon() {
function HammerIcon (line 315) | function HammerIcon() {
FILE: website/components/cmdk/vercel.tsx
function VercelCMDK (line 4) | function VercelCMDK() {
function Home (line 91) | function Home({ searchProjects }: { searchProjects: Function }) {
function Projects (line 137) | function Projects() {
function Item (line 150) | function Item({
function ProjectsIcon (line 173) | function ProjectsIcon() {
function PlusIcon (line 194) | function PlusIcon() {
function TeamsIcon (line 213) | function TeamsIcon() {
function CopyIcon (line 234) | function CopyIcon() {
function DocsIcon (line 252) | function DocsIcon() {
function FeedbackIcon (line 274) | function FeedbackIcon() {
function ContactIcon (line 292) | function ContactIcon() {
FILE: website/components/code/index.tsx
function Code (line 41) | function Code({ children }: { children: string }) {
FILE: website/components/icons/index.tsx
function FigmaIcon (line 3) | function FigmaIcon() {
function RaycastIcon (line 15) | function RaycastIcon() {
function YouTubeIcon (line 28) | function YouTubeIcon() {
function SlackIcon (line 40) | function SlackIcon() {
function VercelIcon (line 79) | function VercelIcon() {
function LinearIcon (line 87) | function LinearIcon({ style }: { style?: Object }) {
function Logo (line 110) | function Logo({ children, size = '20px' }: { children: React.ReactNode; ...
function CopyIcon (line 127) | function CopyIcon() {
function CopiedIcon (line 146) | function CopiedIcon() {
function GitHubIcon (line 154) | function GitHubIcon() {
function FramerIcon (line 165) | function FramerIcon() {
FILE: website/pages/_app.tsx
function App (line 17) | function App({ Component, pageProps }: AppProps) {
FILE: website/pages/_document.tsx
class Document (line 5) | class Document extends NextDocument {
method render (line 6) | render() {
FILE: website/pages/index.tsx
type TTheme (line 20) | type TTheme = {
type Themes (line 25) | type Themes = 'linear' | 'raycast' | 'vercel' | 'framer'
function Index (line 29) | function Index() {
function CMDKWrapper (line 84) | function CMDKWrapper(props: MotionProps & { children: React.ReactNode }) {
function InstallButton (line 101) | function InstallButton() {
function GitHubButton (line 123) | function GitHubButton() {
function ThemeSwitcher (line 158) | function ThemeSwitcher() {
function Codeblock (line 262) | function Codeblock() {
function VersionBadge (line 296) | function VersionBadge() {
function Footer (line 300) | function Footer() {
function RaunoSignature (line 326) | function RaunoSignature() {
function PacoSignature (line 350) | function PacoSignature() {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (206K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 20,
"preview": "github: pacocoursey\n"
},
{
"path": ".github/workflows/test.yml",
"chars": 691,
"preview": "name: Run E2E tests\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n test:\n runs-on: u"
},
{
"path": ".gitignore",
"chars": 155,
"preview": ".DS_Store\n.idea\n.env\n.env.local\n.env.development\n.env.development.local\n*.log\nyalc.lock\n\n.vercel/\n.turbo/\n.next/\n.yalc/\n"
},
{
"path": ".husky/pre-commit",
"chars": 59,
"preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\npnpm lint-staged\n"
},
{
"path": ".prettierignore",
"chars": 46,
"preview": ".next\ndist\npnpm-lock.yaml\n.pnpm-store\n.vercel\n"
},
{
"path": ".prettierrc.js",
"chars": 115,
"preview": "module.exports = {\n semi: false,\n singleQuote: true,\n tabWidth: 2,\n trailingComma: 'all',\n printWidth: 120,\n}\n"
},
{
"path": "ARCHITECTURE.md",
"chars": 4013,
"preview": "# Architecture\n\n> Document is a work in progress!\n\n⌘K is born from a simple constraint: can you write a combobox with fi"
},
{
"path": "LICENSE.md",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2022 Paco Coursey\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 13955,
"preview": "<p align=\"center\">\n<img src=\"./website/public/og.png\" />\n</p>\n\n# ⌘K [ => {\n test.beforeEach(async ("
},
{
"path": "test/dialog.test.ts",
"chars": 356,
"preview": "import { expect, test } from '@playwright/test'\n\ntest.describe('dialog', async () => {\n test.beforeEach(async ({ page }"
},
{
"path": "test/group.test.ts",
"chars": 1225,
"preview": "import { expect, test } from '@playwright/test'\n\ntest.describe('group', async () => {\n test.beforeEach(async ({ page })"
},
{
"path": "test/item.test.ts",
"chars": 2946,
"preview": "import { expect, test } from '@playwright/test'\n\ntest.describe('item', async () => {\n test.beforeEach(async ({ page }) "
},
{
"path": "test/keybind.test.ts",
"chars": 7218,
"preview": "import { expect, test } from '@playwright/test'\n\ntest.describe('arrow keybinds', async () => {\n test.beforeEach(async ("
},
{
"path": "test/next-env.d.ts",
"chars": 201,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "test/numeric.test.ts",
"chars": 962,
"preview": "import { expect, test } from '@playwright/test'\n\ntest.describe('behavior for numeric values', async () => {\n test.befor"
},
{
"path": "test/package.json",
"chars": 371,
"preview": "{\n \"name\": \"cmdk-tests\",\n \"version\": \"0.0.0\",\n \"scripts\": {\n \"dev\": \"next\"\n },\n \"dependencies\": {\n \"@radix-ui"
},
{
"path": "test/pages/_app.tsx",
"chars": 119,
"preview": "import '../style.css'\n\nexport default function App({ Component, pageProps }) {\n return <Component {...pageProps} />\n}\n"
},
{
"path": "test/pages/dialog.tsx",
"chars": 623,
"preview": "import { Command } from 'cmdk'\nimport * as React from 'react'\n\nconst Page = () => {\n const [open, setOpen] = React.useS"
},
{
"path": "test/pages/group.tsx",
"chars": 1260,
"preview": "import { Command } from 'cmdk'\nimport * as React from 'react'\n\nconst Page = () => {\n const [search, setSearch] = React."
},
{
"path": "test/pages/huge.tsx",
"chars": 784,
"preview": "import { Command } from 'cmdk'\nimport * as React from 'react'\n\nconst items = new Array(1000).fill(0)\n\nconst Page = () =>"
},
{
"path": "test/pages/index.tsx",
"chars": 703,
"preview": "import { Command } from 'cmdk'\n\nconst Page = () => {\n return (\n <div>\n <Command className=\"root\">\n <Comm"
},
{
"path": "test/pages/item-advanced.tsx",
"chars": 650,
"preview": "import { Command } from 'cmdk'\nimport * as React from 'react'\n\nconst Page = () => {\n const [count, setCount] = React.us"
},
{
"path": "test/pages/item.tsx",
"chars": 1323,
"preview": "import { Command } from 'cmdk'\nimport * as React from 'react'\n\nconst Page = () => {\n const [unmount, setUnmount] = Reac"
},
{
"path": "test/pages/keybinds.tsx",
"chars": 1257,
"preview": "import { Command } from 'cmdk'\nimport { useRouter } from 'next/router'\nimport * as React from 'react'\n\nconst Page = () ="
},
{
"path": "test/pages/numeric.tsx",
"chars": 596,
"preview": "import { Command } from 'cmdk'\n\nconst Page = () => {\n return (\n <div>\n <Command className=\"root\">\n <Comm"
},
{
"path": "test/pages/portal.tsx",
"chars": 2626,
"preview": "import * as React from 'react'\nimport { Command } from 'cmdk'\nimport * as Portal from '@radix-ui/react-portal'\n\nconst Pa"
},
{
"path": "test/pages/props.tsx",
"chars": 1735,
"preview": "import { Command } from 'cmdk'\nimport { useRouter } from 'next/router'\nimport * as React from 'react'\n\nconst Page = () ="
},
{
"path": "test/props.test.ts",
"chars": 2038,
"preview": "import { expect, test } from '@playwright/test'\n\ntest.describe('props', async () => {\n test('results do not change when"
},
{
"path": "test/style.css",
"chars": 52,
"preview": "[cmdk-item][aria-selected='true'] {\n color: red;\n}\n"
},
{
"path": "test/tsconfig.json",
"chars": 531,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"sk"
},
{
"path": "tsconfig.json",
"chars": 534,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es2018\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n "
},
{
"path": "website/.eslintrc.json",
"chars": 40,
"preview": "{\n \"extends\": \"next/core-web-vitals\"\n}\n"
},
{
"path": "website/.gitignore",
"chars": 371,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "website/README.md",
"chars": 1569,
"preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
},
{
"path": "website/components/cmdk/framer.tsx",
"chars": 12086,
"preview": "import { Command } from 'cmdk'\nimport React from 'react'\n\nexport function FramerCMDK() {\n const [value, setValue] = Rea"
},
{
"path": "website/components/cmdk/linear.tsx",
"chars": 6347,
"preview": "import { Command } from 'cmdk'\n\nexport function LinearCMDK() {\n return (\n <div className=\"linear\">\n <Command>\n "
},
{
"path": "website/components/cmdk/raycast.tsx",
"chars": 12061,
"preview": "import React from 'react'\nimport { useTheme } from 'next-themes'\nimport * as Popover from '@radix-ui/react-popover'\nimpo"
},
{
"path": "website/components/cmdk/vercel.tsx",
"chars": 7038,
"preview": "import React from 'react'\nimport { Command } from 'cmdk'\n\nexport function VercelCMDK() {\n const ref = React.useRef<HTML"
},
{
"path": "website/components/code/code.module.scss",
"chars": 1314,
"preview": ".root {\n border-radius: 12px;\n padding: 16px;\n backdrop-filter: blur(10px);\n border: 1px solid var(--gray6);\n posit"
},
{
"path": "website/components/code/index.tsx",
"chars": 1620,
"preview": "import React from 'react'\nimport copy from 'copy-to-clipboard'\nimport Highlight, { defaultProps } from 'prism-react-rend"
},
{
"path": "website/components/icons/icons.module.scss",
"chars": 953,
"preview": ".blurLogo {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n border-radius: 4p"
},
{
"path": "website/components/icons/index.tsx",
"chars": 7439,
"preview": "import styles from './icons.module.scss'\n\nexport function FigmaIcon() {\n return (\n <svg xmlns=\"http://www.w3.org/200"
},
{
"path": "website/components/index.ts",
"chars": 168,
"preview": "export * from './cmdk/framer'\nexport * from './cmdk/linear'\nexport * from './cmdk/vercel'\nexport * from './cmdk/raycast'"
},
{
"path": "website/next-env.d.ts",
"chars": 201,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "website/next.config.js",
"chars": 137,
"preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n reactStrictMode: true,\n swcMinify: true,\n}\n\nmodule.expo"
},
{
"path": "website/package.json",
"chars": 1148,
"preview": "{\n \"name\": \"cmdk-website\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": "
},
{
"path": "website/pages/_app.tsx",
"chars": 1192,
"preview": "import 'styles/globals.scss'\n\nimport 'styles/cmdk/vercel.scss'\nimport 'styles/cmdk/linear.scss'\nimport 'styles/cmdk/rayc"
},
{
"path": "website/pages/_document.tsx",
"chars": 499,
"preview": "/* eslint-disable @next/next/no-sync-scripts */\nimport React from 'react'\nimport NextDocument, { Html, Head, Main, NextS"
},
{
"path": "website/pages/index.tsx",
"chars": 14034,
"preview": "import styles from 'styles/index.module.scss'\nimport React from 'react'\nimport { AnimatePresence, AnimateSharedLayout, m"
},
{
"path": "website/public/robots.txt",
"chars": 24,
"preview": "User-agent: *\nDisallow:\n"
},
{
"path": "website/styles/cmdk/framer.scss",
"chars": 4821,
"preview": ".framer {\n [cmdk-root] {\n max-width: 640px;\n width: 100%;\n padding: 8px;\n background: #ffffff;\n border-r"
},
{
"path": "website/styles/cmdk/linear.scss",
"chars": 2729,
"preview": ".linear {\n [cmdk-root] {\n max-width: 640px;\n width: 100%;\n background: #ffffff;\n border-radius: 8px;\n ov"
},
{
"path": "website/styles/cmdk/raycast.scss",
"chars": 9957,
"preview": ".raycast {\n [cmdk-root] {\n max-width: 640px;\n width: 100%;\n background: var(--gray1);\n border-radius: 12px;"
},
{
"path": "website/styles/cmdk/vercel.scss",
"chars": 3019,
"preview": ".vercel {\n [cmdk-root] {\n max-width: 640px;\n width: 100%;\n padding: 8px;\n background: #ffffff;\n border-r"
},
{
"path": "website/styles/globals.scss",
"chars": 3691,
"preview": "@font-face {\n font-family: 'Inter';\n font-style: normal;\n font-weight: 100 900; // Range of weights supported\n font-"
},
{
"path": "website/styles/index.module.scss",
"chars": 8774,
"preview": ".main {\n width: 100vw;\n min-height: 100vh;\n position: relative;\n display: flex;\n justify-content: center;\n padding"
},
{
"path": "website/tsconfig.json",
"chars": 529,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"sk"
},
{
"path": "website/vercel.json",
"chars": 215,
"preview": "{\n \"headers\": [\n {\n \"source\": \"/inter-var-latin.woff2\",\n \"headers\": [\n {\n \"key\": \"Cache-Co"
}
]
About this extraction
This page contains the full source code of the dip/cmdk GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (189.6 KB), approximately 63.1k tokens, and a symbol index with 123 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.