├── .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-lock.yaml ├── 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 ├── favicon.svg ├── grid.svg ├── inter-var-latin.woff2 ├── line.svg ├── og.png ├── paco.png ├── rauno.jpeg ├── robots.txt └── vercel.svg ├── styles ├── cmdk │ ├── framer.scss │ ├── linear.scss │ ├── raycast.scss │ └── vercel.scss ├── globals.scss └── index.module.scss ├── tsconfig.json └── vercel.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pacocoursey 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run E2E tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: pnpm/action-setup@v4 # respects packageManager in package.json 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | cache: 'pnpm' 21 | 22 | - run: pnpm install 23 | env: 24 | CI: true 25 | 26 | - run: pnpm build 27 | - run: pnpm test:format 28 | - run: pnpm playwright install --with-deps 29 | - run: pnpm test || exit 1 30 | 31 | - name: Upload test results 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: playwright-report 35 | path: playwright-report.json 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .env 4 | .env.local 5 | .env.development 6 | .env.development.local 7 | *.log 8 | yalc.lock 9 | 10 | .vercel/ 11 | .turbo/ 12 | .next/ 13 | .yalc/ 14 | build/ 15 | dist/ 16 | node_modules/ 17 | .vercel 18 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | dist 3 | pnpm-lock.yaml 4 | .pnpm-store 5 | .vercel 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | tabWidth: 2, 5 | trailingComma: 'all', 6 | printWidth: 120, 7 | } 8 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | > Document is a work in progress! 4 | 5 | ⌘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: 6 | 7 | ```tsx 8 | // No 9 | <> 10 | {items.map((item) => { 11 | return <div>{item}</div> 12 | })} 13 | </> 14 | ``` 15 | 16 | We didn't want to provide a render prop: 17 | 18 | ```tsx 19 | // No 20 | onItemRender={({ item }) => { 21 | return <div>{item}</div> 22 | }} 23 | ``` 24 | 25 | Instead, we wanted to render components: 26 | 27 | ```tsx 28 | // Yes 29 | <Item>My item</Item> 30 | ``` 31 | 32 | Especially, we wanted full component composition: 33 | 34 | ```tsx 35 | // YES 36 | <> 37 | <BlogItems /> 38 | {staticItems} 39 | </> 40 | ``` 41 | 42 | 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. 43 | 44 | However, for a combobox, it is a terrible, terrible constraint that we've spent 2 years fighting. 45 | 46 | ## Approach 47 | 48 | ⌘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. 49 | 50 | ### Discarded approach 51 | 52 | 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. 53 | 54 | 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. 55 | 56 | 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. 57 | 58 | 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. 59 | 60 | ## Example 61 | 62 | ```tsx 63 | <Input value="b" /> 64 | <List> 65 | <Item>A</Item> 66 | <Item>B</Item> 67 | </List> 68 | ``` 69 | 70 | 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: 71 | 72 | ```tsx 73 | <> 74 | {['A', 'B'].map((item) => { 75 | if (matches(item, search)) { 76 | return <Item>{item}</Item> 77 | } 78 | })} 79 | </> 80 | ``` 81 | 82 | But in our case, the item will remain in the React tree and just be removed from the DOM: 83 | 84 | ```tsx 85 | <List> 86 | {/* returns `null`, no DOM created */} 87 | <Item>A</Item> 88 | <Item>B</Item> 89 | </List> 90 | ``` 91 | 92 | ## Performance 93 | 94 | 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. 95 | 96 | ## Groups 97 | 98 | Item mount informs both the root and the parent group, which keeps track of items within it. Each group informs the root. 99 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Paco Coursey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img src="./website/public/og.png" /> 3 | </p> 4 | 5 | # ⌘K [](https://www.npmjs.com/package/cmdk?activeTab=code) [](https://www.npmjs.com/package/cmdk) 6 | 7 | ⌘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. 8 | 9 | Demo and examples: [cmdk.paco.me](https://cmdk.paco.me) 10 | 11 | ## Install 12 | 13 | ```bash 14 | pnpm install cmdk 15 | ``` 16 | 17 | ## Use 18 | 19 | ```tsx 20 | import { Command } from 'cmdk' 21 | 22 | const CommandMenu = () => { 23 | return ( 24 | <Command label="Command Menu"> 25 | <Command.Input /> 26 | <Command.List> 27 | <Command.Empty>No results found.</Command.Empty> 28 | 29 | <Command.Group heading="Letters"> 30 | <Command.Item>a</Command.Item> 31 | <Command.Item>b</Command.Item> 32 | <Command.Separator /> 33 | <Command.Item>c</Command.Item> 34 | </Command.Group> 35 | 36 | <Command.Item>Apple</Command.Item> 37 | </Command.List> 38 | </Command> 39 | ) 40 | } 41 | ``` 42 | 43 | Or in a dialog: 44 | 45 | ```tsx 46 | import { Command } from 'cmdk' 47 | 48 | const CommandMenu = () => { 49 | const [open, setOpen] = React.useState(false) 50 | 51 | // Toggle the menu when ⌘K is pressed 52 | React.useEffect(() => { 53 | const down = (e) => { 54 | if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { 55 | e.preventDefault() 56 | setOpen((open) => !open) 57 | } 58 | } 59 | 60 | document.addEventListener('keydown', down) 61 | return () => document.removeEventListener('keydown', down) 62 | }, []) 63 | 64 | return ( 65 | <Command.Dialog open={open} onOpenChange={setOpen} label="Global Command Menu"> 66 | <Command.Input /> 67 | <Command.List> 68 | <Command.Empty>No results found.</Command.Empty> 69 | 70 | <Command.Group heading="Letters"> 71 | <Command.Item>a</Command.Item> 72 | <Command.Item>b</Command.Item> 73 | <Command.Separator /> 74 | <Command.Item>c</Command.Item> 75 | </Command.Group> 76 | 77 | <Command.Item>Apple</Command.Item> 78 | </Command.List> 79 | </Command.Dialog> 80 | ) 81 | } 82 | ``` 83 | 84 | ## Parts and styling 85 | 86 | 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. 87 | 88 | ### Command `[cmdk-root]` 89 | 90 | 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. 91 | 92 | > **Note** 93 | > 94 | > Values are always trimmed with the [trim()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim) method. 95 | 96 | ```tsx 97 | const [value, setValue] = React.useState('apple') 98 | 99 | return ( 100 | <Command value={value} onValueChange={setValue}> 101 | <Command.Input /> 102 | <Command.List> 103 | <Command.Item>Orange</Command.Item> 104 | <Command.Item>Apple</Command.Item> 105 | </Command.List> 106 | </Command> 107 | ) 108 | ``` 109 | 110 | You can provide a custom `filter` function that is called to rank each item. Note that the value will be trimmed. 111 | 112 | ```tsx 113 | <Command 114 | filter={(value, search) => { 115 | if (value.includes(search)) return 1 116 | return 0 117 | }} 118 | /> 119 | ``` 120 | 121 | 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. 122 | 123 | ```tsx 124 | <Command 125 | filter={(value, search, keywords) => { 126 | const extendValue = value + ' ' + keywords.join(' ') 127 | if (extendValue.includes(search)) return 1 128 | return 0 129 | }} 130 | /> 131 | ``` 132 | 133 | Or disable filtering and sorting entirely: 134 | 135 | ```tsx 136 | <Command shouldFilter={false}> 137 | <Command.List> 138 | {filteredItems.map((item) => { 139 | return ( 140 | <Command.Item key={item} value={item}> 141 | {item} 142 | </Command.Item> 143 | ) 144 | })} 145 | </Command.List> 146 | </Command> 147 | ``` 148 | 149 | 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: 150 | 151 | ```tsx 152 | <Command loop /> 153 | ``` 154 | 155 | ### Dialog `[cmdk-dialog]` `[cmdk-overlay]` 156 | 157 | 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. 158 | 159 | ```tsx 160 | const [open, setOpen] = React.useState(false) 161 | 162 | return ( 163 | <Command.Dialog open={open} onOpenChange={setOpen}> 164 | ... 165 | </Command.Dialog> 166 | ) 167 | ``` 168 | 169 | 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. 170 | 171 | ```tsx 172 | const containerElement = React.useRef(null) 173 | 174 | return ( 175 | <> 176 | <Command.Dialog container={containerElement.current} /> 177 | <div ref={containerElement} /> 178 | </> 179 | ) 180 | ``` 181 | 182 | ### Input `[cmdk-input]` 183 | 184 | All props are forwarded to the underlying `input` element. Can be controlled with the `value` and `onValueChange` props. 185 | 186 | ```tsx 187 | const [search, setSearch] = React.useState('') 188 | 189 | return <Command.Input value={search} onValueChange={setSearch} /> 190 | ``` 191 | 192 | ### List `[cmdk-list]` 193 | 194 | Contains items and groups. Animate height using the `--cmdk-list-height` CSS variable. 195 | 196 | ```css 197 | [cmdk-list] { 198 | min-height: 300px; 199 | height: var(--cmdk-list-height); 200 | max-height: 500px; 201 | transition: height 100ms ease; 202 | } 203 | ``` 204 | 205 | To scroll item into view earlier near the edges of the viewport, use scroll-padding: 206 | 207 | ```css 208 | [cmdk-list] { 209 | scroll-padding-block-start: 8px; 210 | scroll-padding-block-end: 8px; 211 | } 212 | ``` 213 | 214 | ### Item `[cmdk-item]` `[data-disabled?]` `[data-selected?]` 215 | 216 | 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`. 217 | 218 | ```tsx 219 | <Command.Item 220 | onSelect={(value) => console.log('Selected', value)} 221 | // Value is implicity "apple" because of the provided text content 222 | > 223 | Apple 224 | </Command.Item> 225 | ``` 226 | 227 | You can also provide a `keywords` prop to help with filtering. Keywords are trimmed. 228 | 229 | ```tsx 230 | <Command.Item keywords={['fruit', 'apple']}>Apple</Command.Item> 231 | ``` 232 | 233 | ```tsx 234 | <Command.Item 235 | onSelect={(value) => console.log('Selected', value)} 236 | // Value is implicity "apple" because of the provided text content 237 | > 238 | Apple 239 | </Command.Item> 240 | ``` 241 | 242 | You can force an item to always render, regardless of filtering, by passing the `forceMount` prop. 243 | 244 | ### Group `[cmdk-group]` `[hidden?]` 245 | 246 | Groups items together with the given `heading` (`[cmdk-group-heading]`). 247 | 248 | ```tsx 249 | <Command.Group heading="Fruit"> 250 | <Command.Item>Apple</Command.Item> 251 | </Command.Group> 252 | ``` 253 | 254 | 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. 255 | 256 | You can force a group to always render, regardless of filtering, by passing the `forceMount` prop. 257 | 258 | ### Separator `[cmdk-separator]` 259 | 260 | Visible when the search query is empty or `alwaysRender` is true, hidden otherwise. 261 | 262 | ### Empty `[cmdk-empty]` 263 | 264 | Automatically renders when there are no results for the search query. 265 | 266 | ### Loading `[cmdk-loading]` 267 | 268 | You should conditionally render this with `progress` while loading asynchronous items. 269 | 270 | ```tsx 271 | const [loading, setLoading] = React.useState(false) 272 | 273 | return <Command.List>{loading && <Command.Loading>Hang on…</Command.Loading>}</Command.List> 274 | ``` 275 | 276 | ### `useCommandState(state => state.selectedField)` 277 | 278 | 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. 279 | 280 | A good use case would be to render a more detailed empty state, like so: 281 | 282 | ```tsx 283 | const search = useCommandState((state) => state.search) 284 | return <Command.Empty>No results found for "{search}".</Command.Empty> 285 | ``` 286 | 287 | ## Examples 288 | 289 | Code snippets for common use cases. 290 | 291 | ### Nested items 292 | 293 | 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: 294 | 295 | ```tsx 296 | const ref = React.useRef(null) 297 | const [open, setOpen] = React.useState(false) 298 | const [search, setSearch] = React.useState('') 299 | const [pages, setPages] = React.useState([]) 300 | const page = pages[pages.length - 1] 301 | 302 | return ( 303 | <Command 304 | onKeyDown={(e) => { 305 | // Escape goes to previous page 306 | // Backspace goes to previous page when search is empty 307 | if (e.key === 'Escape' || (e.key === 'Backspace' && !search)) { 308 | e.preventDefault() 309 | setPages((pages) => pages.slice(0, -1)) 310 | } 311 | }} 312 | > 313 | <Command.Input value={search} onValueChange={setSearch} /> 314 | <Command.List> 315 | {!page && ( 316 | <> 317 | <Command.Item onSelect={() => setPages([...pages, 'projects'])}>Search projects…</Command.Item> 318 | <Command.Item onSelect={() => setPages([...pages, 'teams'])}>Join a team…</Command.Item> 319 | </> 320 | )} 321 | 322 | {page === 'projects' && ( 323 | <> 324 | <Command.Item>Project A</Command.Item> 325 | <Command.Item>Project B</Command.Item> 326 | </> 327 | )} 328 | 329 | {page === 'teams' && ( 330 | <> 331 | <Command.Item>Team 1</Command.Item> 332 | <Command.Item>Team 2</Command.Item> 333 | </> 334 | )} 335 | </Command.List> 336 | </Command> 337 | ) 338 | ``` 339 | 340 | ### Show sub-items when searching 341 | 342 | If your items have nested sub-items that you only want to reveal when searching, render based on the search state: 343 | 344 | ```tsx 345 | const SubItem = (props) => { 346 | const search = useCommandState((state) => state.search) 347 | if (!search) return null 348 | return <Command.Item {...props} /> 349 | } 350 | 351 | return ( 352 | <Command> 353 | <Command.Input /> 354 | <Command.List> 355 | <Command.Item>Change theme…</Command.Item> 356 | <SubItem>Change theme to dark</SubItem> 357 | <SubItem>Change theme to light</SubItem> 358 | </Command.List> 359 | </Command> 360 | ) 361 | ``` 362 | 363 | ### Asynchronous results 364 | 365 | Render the items as they become available. Filtering and sorting will happen automatically. 366 | 367 | ```tsx 368 | const [loading, setLoading] = React.useState(false) 369 | const [items, setItems] = React.useState([]) 370 | 371 | React.useEffect(() => { 372 | async function getItems() { 373 | setLoading(true) 374 | const res = await api.get('/dictionary') 375 | setItems(res) 376 | setLoading(false) 377 | } 378 | 379 | getItems() 380 | }, []) 381 | 382 | return ( 383 | <Command> 384 | <Command.Input /> 385 | <Command.List> 386 | {loading && <Command.Loading>Fetching words…</Command.Loading>} 387 | {items.map((item) => { 388 | return ( 389 | <Command.Item key={`word-${item}`} value={item}> 390 | {item} 391 | </Command.Item> 392 | ) 393 | })} 394 | </Command.List> 395 | </Command> 396 | ) 397 | ``` 398 | 399 | ### Use inside Popover 400 | 401 | 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. 402 | 403 | ```bash 404 | $ pnpm install @radix-ui/react-popover 405 | ``` 406 | 407 | Render `Command` inside of the popover content: 408 | 409 | ```tsx 410 | import * as Popover from '@radix-ui/react-popover' 411 | 412 | return ( 413 | <Popover.Root> 414 | <Popover.Trigger>Toggle popover</Popover.Trigger> 415 | 416 | <Popover.Content> 417 | <Command> 418 | <Command.Input /> 419 | <Command.List> 420 | <Command.Item>Apple</Command.Item> 421 | </Command.List> 422 | </Command> 423 | </Popover.Content> 424 | </Popover.Root> 425 | ) 426 | ``` 427 | 428 | ### Drop in stylesheets 429 | 430 | You can find global stylesheets to drop in as a starting point for styling. See [website/styles/cmdk](website/styles/cmdk) for examples. 431 | 432 | ## FAQ 433 | 434 | **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. 435 | 436 | **Virtualization?** No. Good performance up to 2,000-3,000 items, though. Read below to bring your own. 437 | 438 | **Filter/sort items manually?** Yes. Pass `shouldFilter={false}` to [Command](#command-cmdk-root). Better memory usage and performance. Bring your own virtualization this way. 439 | 440 | **React 18 safe?** Yes, required. Uses React 18 hooks like `useId` and `useSyncExternalStore`. 441 | 442 | **Unstyled?** Yes, use the listed CSS selectors. 443 | 444 | **Hydration mismatch?** No, likely a bug in your code. Ensure the `open` prop to `Command.Dialog` is `false` on the server. 445 | 446 | **React strict mode safe?** Yes. Open an issue if you notice an issue. 447 | 448 | **Weird/wrong behavior?** Make sure your `Command.Item` has a `key` and unique `value`. 449 | 450 | **Concurrent mode safe?** Maybe, but concurrent mode is not yet real. Uses risky approaches like manual DOM ordering. 451 | 452 | **React server component?** No, it's a client component. 453 | 454 | **Listen for ⌘K automatically?** No, do it yourself to have full control over keybind context. 455 | 456 | **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. 457 | 458 | ## History 459 | 460 | 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_)). 461 | 462 | [use-descendants](https://github.com/pacocoursey/use-descendants) was extracted from the 2019 version. 463 | 464 | ## Testing 465 | 466 | First, install dependencies and Playwright browsers: 467 | 468 | ```bash 469 | pnpm install 470 | pnpm playwright install 471 | ``` 472 | 473 | Then ensure you've built the library: 474 | 475 | ```bash 476 | pnpm build 477 | ``` 478 | 479 | Then run the tests using your local build against real browser engines: 480 | 481 | ```bash 482 | pnpm test 483 | ``` 484 | -------------------------------------------------------------------------------- /cmdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmdk", 3 | "version": "1.1.1", 4 | "license": "MIT", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "exports": { 12 | ".": { 13 | "types": "./dist/index.d.ts", 14 | "import": "./dist/index.mjs", 15 | "require": "./dist/index.js" 16 | } 17 | }, 18 | "scripts": { 19 | "prepublishOnly": "cp ../README.md . && pnpm build", 20 | "postpublish": "rm README.md", 21 | "build": "tsup src", 22 | "dev": "tsup src --watch" 23 | }, 24 | "peerDependencies": { 25 | "react": "^18 || ^19 || ^19.0.0-rc", 26 | "react-dom": "^18 || ^19 || ^19.0.0-rc" 27 | }, 28 | "dependencies": { 29 | "@radix-ui/react-compose-refs": "^1.1.1", 30 | "@radix-ui/react-dialog": "^1.1.6", 31 | "@radix-ui/react-id": "^1.1.0", 32 | "@radix-ui/react-primitive": "^2.0.2" 33 | }, 34 | "devDependencies": { 35 | "@types/react": "18.0.15" 36 | }, 37 | "sideEffects": false, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/pacocoursey/cmdk.git", 41 | "directory": "cmdk" 42 | }, 43 | "bugs": { 44 | "url": "https://github.com/pacocoursey/cmdk/issues" 45 | }, 46 | "homepage": "https://github.com/pacocoursey/cmdk#readme", 47 | "author": { 48 | "name": "Paco", 49 | "url": "https://github.com/pacocoursey" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmdk/src/command-score.ts: -------------------------------------------------------------------------------- 1 | // The scores are arranged so that a continuous match of characters will 2 | // result in a total score of 1. 3 | // 4 | // The best case, this character is a match, and either this is the start 5 | // of the string, or the previous character was also a match. 6 | var SCORE_CONTINUE_MATCH = 1, 7 | // A new match at the start of a word scores better than a new match 8 | // elsewhere as it's more likely that the user will type the starts 9 | // of fragments. 10 | // NOTE: We score word jumps between spaces slightly higher than slashes, brackets 11 | // hyphens, etc. 12 | SCORE_SPACE_WORD_JUMP = 0.9, 13 | SCORE_NON_SPACE_WORD_JUMP = 0.8, 14 | // Any other match isn't ideal, but we include it for completeness. 15 | SCORE_CHARACTER_JUMP = 0.17, 16 | // If the user transposed two letters, it should be significantly penalized. 17 | // 18 | // i.e. "ouch" is more likely than "curtain" when "uc" is typed. 19 | SCORE_TRANSPOSITION = 0.1, 20 | // The goodness of a match should decay slightly with each missing 21 | // character. 22 | // 23 | // i.e. "bad" is more likely than "bard" when "bd" is typed. 24 | // 25 | // This will not change the order of suggestions based on SCORE_* until 26 | // 100 characters are inserted between matches. 27 | PENALTY_SKIPPED = 0.999, 28 | // The goodness of an exact-case match should be higher than a 29 | // case-insensitive match by a small amount. 30 | // 31 | // i.e. "HTML" is more likely than "haml" when "HM" is typed. 32 | // 33 | // This will not change the order of suggestions based on SCORE_* until 34 | // 1000 characters are inserted between matches. 35 | PENALTY_CASE_MISMATCH = 0.9999, 36 | // Match higher for letters closer to the beginning of the word 37 | PENALTY_DISTANCE_FROM_START = 0.9, 38 | // If the word has more characters than the user typed, it should 39 | // be penalised slightly. 40 | // 41 | // i.e. "html" is more likely than "html5" if I type "html". 42 | // 43 | // However, it may well be the case that there's a sensible secondary 44 | // ordering (like alphabetical) that it makes sense to rely on when 45 | // there are many prefix matches, so we don't make the penalty increase 46 | // with the number of tokens. 47 | PENALTY_NOT_COMPLETE = 0.99 48 | 49 | var IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/, 50 | COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g, 51 | IS_SPACE_REGEXP = /[\s-]/, 52 | COUNT_SPACE_REGEXP = /[\s-]/g 53 | 54 | function commandScoreInner( 55 | string, 56 | abbreviation, 57 | lowerString, 58 | lowerAbbreviation, 59 | stringIndex, 60 | abbreviationIndex, 61 | memoizedResults, 62 | ) { 63 | if (abbreviationIndex === abbreviation.length) { 64 | if (stringIndex === string.length) { 65 | return SCORE_CONTINUE_MATCH 66 | } 67 | return PENALTY_NOT_COMPLETE 68 | } 69 | 70 | var memoizeKey = `${stringIndex},${abbreviationIndex}` 71 | if (memoizedResults[memoizeKey] !== undefined) { 72 | return memoizedResults[memoizeKey] 73 | } 74 | 75 | var abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex) 76 | var index = lowerString.indexOf(abbreviationChar, stringIndex) 77 | var highScore = 0 78 | 79 | var score, transposedScore, wordBreaks, spaceBreaks 80 | 81 | while (index >= 0) { 82 | score = commandScoreInner( 83 | string, 84 | abbreviation, 85 | lowerString, 86 | lowerAbbreviation, 87 | index + 1, 88 | abbreviationIndex + 1, 89 | memoizedResults, 90 | ) 91 | if (score > highScore) { 92 | if (index === stringIndex) { 93 | score *= SCORE_CONTINUE_MATCH 94 | } else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) { 95 | score *= SCORE_NON_SPACE_WORD_JUMP 96 | wordBreaks = string.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP) 97 | if (wordBreaks && stringIndex > 0) { 98 | score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length) 99 | } 100 | } else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) { 101 | score *= SCORE_SPACE_WORD_JUMP 102 | spaceBreaks = string.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP) 103 | if (spaceBreaks && stringIndex > 0) { 104 | score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length) 105 | } 106 | } else { 107 | score *= SCORE_CHARACTER_JUMP 108 | if (stringIndex > 0) { 109 | score *= Math.pow(PENALTY_SKIPPED, index - stringIndex) 110 | } 111 | } 112 | 113 | if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) { 114 | score *= PENALTY_CASE_MISMATCH 115 | } 116 | } 117 | 118 | if ( 119 | (score < SCORE_TRANSPOSITION && 120 | lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1)) || 121 | (lowerAbbreviation.charAt(abbreviationIndex + 1) === lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428 122 | lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex)) 123 | ) { 124 | transposedScore = commandScoreInner( 125 | string, 126 | abbreviation, 127 | lowerString, 128 | lowerAbbreviation, 129 | index + 1, 130 | abbreviationIndex + 2, 131 | memoizedResults, 132 | ) 133 | 134 | if (transposedScore * SCORE_TRANSPOSITION > score) { 135 | score = transposedScore * SCORE_TRANSPOSITION 136 | } 137 | } 138 | 139 | if (score > highScore) { 140 | highScore = score 141 | } 142 | 143 | index = lowerString.indexOf(abbreviationChar, index + 1) 144 | } 145 | 146 | memoizedResults[memoizeKey] = highScore 147 | return highScore 148 | } 149 | 150 | function formatInput(string) { 151 | // convert all valid space characters to space so they match each other 152 | return string.toLowerCase().replace(COUNT_SPACE_REGEXP, ' ') 153 | } 154 | 155 | export function commandScore(string: string, abbreviation: string, aliases: string[]): number { 156 | /* NOTE: 157 | * in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase() 158 | * was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster. 159 | */ 160 | string = aliases && aliases.length > 0 ? `${string + ' ' + aliases.join(' ')}` : string 161 | return commandScoreInner(string, abbreviation, formatInput(string), formatInput(abbreviation), 0, 0, {}) 162 | } 163 | -------------------------------------------------------------------------------- /cmdk/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | sourcemap: false, 5 | minify: true, 6 | dts: true, 7 | format: ['esm', 'cjs'], 8 | loader: { 9 | '.js': 'jsx', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmdk-root", 3 | "private": true, 4 | "scripts": { 5 | "build": "pnpm -F cmdk build", 6 | "dev": "pnpm -F cmdk build --watch", 7 | "website": "pnpm -F cmdk-website dev", 8 | "testsite": "pnpm -F cmdk-tests dev", 9 | "format": "prettier '**/*.{js,jsx,ts,tsx,json,md,mdx,css,scss,yaml,yml}' --write", 10 | "preinstall": "npx only-allow pnpm", 11 | "test:format": "prettier '**/*.{js,jsx,ts,tsx,json,md,mdx,css,scss,yaml,yml}' --check", 12 | "test": "playwright test" 13 | }, 14 | "devDependencies": { 15 | "@playwright/test": "1.51.0", 16 | "husky": "^8.0.1", 17 | "lint-staged": "15.2.0", 18 | "prettier": "2.7.1", 19 | "tsup": "8.0.1", 20 | "typescript": "4.6.4" 21 | }, 22 | "packageManager": "pnpm@8.8.0", 23 | "lint-staged": { 24 | "**/*.{js,jsx,ts,tsx,json,md,mdx,css,scss,yaml,yml}": [ 25 | "prettier --write" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { PlaywrightTestConfig, devices } from '@playwright/test' 2 | 3 | const config: PlaywrightTestConfig = { 4 | forbidOnly: !!process.env.CI, 5 | retries: process.env.CI ? 2 : 0, 6 | reporter: process.env.CI ? [['github'], ['json', { outputFile: 'playwright-report.json' }]] : 'list', 7 | testDir: './test', 8 | use: { 9 | trace: 'on-first-retry', 10 | baseURL: 'http://localhost:3000', 11 | }, 12 | timeout: 5000, 13 | webServer: { 14 | command: 'npm run dev', 15 | url: 'http://localhost:3000', 16 | cwd: './test', 17 | reuseExistingServer: !process.env.CI, 18 | }, 19 | projects: [ 20 | { 21 | name: 'webkit', 22 | use: { ...devices['Desktop Safari'], headless: true }, 23 | }, 24 | ], 25 | } 26 | 27 | export default config 28 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'website' 3 | - 'test' 4 | - 'cmdk' 5 | -------------------------------------------------------------------------------- /test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('basic behavior', async () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/') 6 | }) 7 | 8 | test('input props are forwarded', async ({ page }) => { 9 | const input = page.locator(`input[placeholder="Search…"]`) 10 | await expect(input).toHaveCount(1) 11 | }) 12 | 13 | test('item value is derived from textContent', async ({ page }) => { 14 | const item = page.locator(`[cmdk-item][data-value="Item"]`) 15 | await expect(item).toHaveText('Item') 16 | }) 17 | 18 | test('item value prop is preferred over textContent', async ({ page }) => { 19 | const item = page.locator(`[cmdk-item][data-value="xxx"]`) 20 | await expect(item).toHaveText('Value') 21 | }) 22 | 23 | test('item onSelect is called on click', async ({ page }) => { 24 | const item = page.locator(`[cmdk-item][data-value="Item"]`) 25 | await item.click() 26 | expect(await page.evaluate(() => (window as any).onSelect)).toEqual('Item selected') 27 | }) 28 | 29 | test('first item is selected by default', async ({ page }) => { 30 | const item = page.locator(`[cmdk-item][aria-selected="true"]`) 31 | await expect(item).toHaveText('Item') 32 | }) 33 | 34 | test('first item is selected when search changes', async ({ page }) => { 35 | const input = page.locator(`[cmdk-input]`) 36 | await input.type('x') 37 | const selected = page.locator(`[cmdk-item][aria-selected="true"]`) 38 | await expect(selected).toHaveText('Value') 39 | }) 40 | 41 | test('items filter when searching', async ({ page }) => { 42 | const input = page.locator(`[cmdk-input]`) 43 | await input.type('x') 44 | const removed = page.locator(`[cmdk-item][data-value="Item"]`) 45 | const remains = page.locator(`[cmdk-item][data-value="xxx"]`) 46 | await expect(removed).toHaveCount(0) 47 | await expect(remains).toHaveCount(1) 48 | }) 49 | 50 | test('items filter when searching by keywords', async ({ page }) => { 51 | const input = page.locator(`[cmdk-input]`) 52 | await input.type('key') 53 | const removed = page.locator(`[cmdk-item][data-value="xxx"]`) 54 | const remains = page.locator(`[cmdk-item][data-value="Item"]`) 55 | await expect(removed).toHaveCount(0) 56 | await expect(remains).toHaveCount(1) 57 | }) 58 | 59 | test('empty component renders when there are no results', async ({ page }) => { 60 | const input = page.locator('[cmdk-input]') 61 | await input.type('z') 62 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(0) 63 | await expect(page.locator(`[cmdk-empty]`)).toHaveText('No results.') 64 | }) 65 | 66 | test('className is applied to each part', async ({ page }) => { 67 | await expect(page.locator(`.root`)).toHaveCount(1) 68 | await expect(page.locator(`.input`)).toHaveCount(1) 69 | await expect(page.locator(`.list`)).toHaveCount(1) 70 | await expect(page.locator(`.item`)).toHaveCount(2) 71 | await page.locator('[cmdk-input]').type('zzzz') 72 | await expect(page.locator(`.item`)).toHaveCount(0) 73 | await expect(page.locator(`.empty`)).toHaveCount(1) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/dialog.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('dialog', async () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/dialog') 6 | }) 7 | 8 | test('dialog renders in portal', async ({ page }) => { 9 | await expect(page.locator(`[cmdk-dialog]`)).toHaveCount(1) 10 | await expect(page.locator(`[cmdk-overlay]`)).toHaveCount(1) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /test/group.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('group', async () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/group') 6 | }) 7 | 8 | test('groups are shown/hidden based on item matches', async ({ page }) => { 9 | await page.locator(`[cmdk-input]`).type('z') 10 | await expect(page.locator(`[cmdk-group][data-value="Animals"]`)).not.toBeVisible() 11 | await expect(page.locator(`[cmdk-group][data-value="Letters"]`)).toBeVisible() 12 | }) 13 | 14 | test('group can be progressively rendered', async ({ page }) => { 15 | await expect(page.locator(`[cmdk-group][data-value="Numbers"]`)).not.toBeVisible() 16 | await page.locator(`[cmdk-input]`).type('t') 17 | await expect(page.locator(`[cmdk-group][data-value="Animals"]`)).not.toBeVisible() 18 | await expect(page.locator(`[cmdk-group][data-value="Letters"]`)).not.toBeVisible() 19 | await expect(page.locator(`[cmdk-group][data-value="Numbers"]`)).toBeVisible() 20 | }) 21 | 22 | test('mounted group still rendered with filter using forceMount', async ({ page }) => { 23 | await page.locator(`data-testid=forceMount`).click() 24 | await page.locator(`[cmdk-input]`).type('Giraffe') 25 | await expect(page.locator(`[cmdk-group][data-value="Letters"]`)).toBeVisible() 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /test/item.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('item', async () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/item') 6 | }) 7 | 8 | test('mounted item matches search', async ({ page }) => { 9 | await page.locator(`[cmdk-input]`).type('b') 10 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(0) 11 | await page.locator(`data-testid=mount`).click() 12 | await expect(page.locator(`[cmdk-item]`)).toHaveText('B') 13 | }) 14 | 15 | test('mounted item does not match search', async ({ page }) => { 16 | await page.locator(`[cmdk-input]`).type('z') 17 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(0) 18 | await page.locator(`data-testid=mount`).click() 19 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(0) 20 | }) 21 | 22 | test('unmount item that is selected', async ({ page }) => { 23 | await page.locator(`data-testid=mount`).click() 24 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('A') 25 | await page.locator(`data-testid=unmount`).click() 26 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(1) 27 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('B') 28 | }) 29 | 30 | test('unmount item that is the only result', async ({ page }) => { 31 | await page.locator(`data-testid=unmount`).click() 32 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(0) 33 | }) 34 | 35 | test('mount item that is the only result', async ({ page }) => { 36 | await page.locator(`data-testid=unmount`).click() 37 | await expect(page.locator(`[cmdk-empty]`)).toHaveCount(1) 38 | await page.locator(`data-testid=mount`).click() 39 | await expect(page.locator(`[cmdk-empty]`)).toHaveCount(0) 40 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(1) 41 | }) 42 | 43 | test('selected does not change when mounting new items', async ({ page }) => { 44 | await page.locator(`data-testid=mount`).click() 45 | await page.locator(`[cmdk-item][data-value="B"]`).click() 46 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('B') 47 | await page.locator(`data-testid=many`).click() 48 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveText('B') 49 | }) 50 | 51 | test('mounted item still rendered with filter usingForceMount', async ({ page }) => { 52 | await page.locator(`data-testid=forceMount`).click() 53 | await page.locator(`[cmdk-input]`).type('z') 54 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(1) 55 | }) 56 | }) 57 | 58 | test.describe('item advanced', async () => { 59 | test.beforeEach(async ({ page }) => { 60 | await page.goto('/item-advanced') 61 | }) 62 | 63 | test('re-rendering re-matches implicit textContent value', async ({ page }) => { 64 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(2) 65 | await page.locator(`[cmdk-input]`).type('2') 66 | const button = page.locator(`data-testid=increment`) 67 | await button.click() 68 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(0) 69 | await button.click() 70 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(2) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/keybind.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('arrow keybinds', async () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/keybinds') 6 | }) 7 | 8 | test('arrow up/down changes selected item', async ({ page }) => { 9 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 10 | await page.locator(`[cmdk-input]`).press('ArrowDown') 11 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 12 | await page.locator(`[cmdk-input]`).press('ArrowUp') 13 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 14 | }) 15 | 16 | test('meta arrow up/down goes to first and last item', async ({ page }) => { 17 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 18 | await page.locator(`[cmdk-input]`).press('Meta+ArrowDown') 19 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'last') 20 | await page.locator(`[cmdk-input]`).press('Meta+ArrowUp') 21 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 22 | }) 23 | 24 | test('alt arrow up/down goes to next and prev item', async ({ page }) => { 25 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 26 | await page.locator(`[cmdk-input]`).press('Alt+ArrowDown') 27 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 28 | await page.locator(`[cmdk-input]`).press('Alt+ArrowDown') 29 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'Apple') 30 | await page.locator(`[cmdk-input]`).press('Alt+ArrowUp') 31 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 32 | await page.locator(`[cmdk-input]`).press('Alt+ArrowUp') 33 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 34 | }) 35 | }) 36 | 37 | test.describe('vim jk keybinds', async () => { 38 | test.beforeEach(async ({ page }) => { 39 | await page.goto('/keybinds') 40 | }) 41 | 42 | test('ctrl j/k changes selected item', async ({ page }) => { 43 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 44 | await page.locator(`[cmdk-input]`).press('Control+j') 45 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 46 | await page.locator(`[cmdk-input]`).press('Control+k') 47 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 48 | }) 49 | 50 | test('meta ctrl j/k goes to first and last item', async ({ page }) => { 51 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 52 | await page.locator(`[cmdk-input]`).press('Meta+Control+j') 53 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'last') 54 | await page.locator(`[cmdk-input]`).press('Meta+Control+k') 55 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 56 | }) 57 | 58 | test('alt ctrl j/k goes to next and prev item', async ({ page }) => { 59 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 60 | await page.locator(`[cmdk-input]`).press('Alt+Control+j') 61 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 62 | await page.locator(`[cmdk-input]`).press('Alt+Control+j') 63 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'Apple') 64 | await page.locator(`[cmdk-input]`).press('Alt+Control+k') 65 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 66 | await page.locator(`[cmdk-input]`).press('Alt+Control+k') 67 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 68 | }) 69 | }) 70 | 71 | test.describe('vim np keybinds', async () => { 72 | test.beforeEach(async ({ page }) => { 73 | await page.goto('/keybinds') 74 | }) 75 | 76 | test('ctrl n/p changes selected item', async ({ page }) => { 77 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 78 | await page.locator(`[cmdk-input]`).press('Control+n') 79 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 80 | await page.locator(`[cmdk-input]`).press('Control+p') 81 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 82 | }) 83 | 84 | test('meta ctrl n/p goes to first and last item', async ({ page }) => { 85 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 86 | await page.locator(`[cmdk-input]`).press('Meta+Control+n') 87 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'last') 88 | await page.locator(`[cmdk-input]`).press('Meta+Control+p') 89 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 90 | }) 91 | 92 | test('alt ctrl n/p goes to next and prev item', async ({ page }) => { 93 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 94 | await page.locator(`[cmdk-input]`).press('Alt+Control+n') 95 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 96 | await page.locator(`[cmdk-input]`).press('Alt+Control+n') 97 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'Apple') 98 | await page.locator(`[cmdk-input]`).press('Alt+Control+p') 99 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'A') 100 | await page.locator(`[cmdk-input]`).press('Alt+Control+p') 101 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 102 | }) 103 | }) 104 | 105 | test.describe('no-vim keybinds', async () => { 106 | test.beforeEach(async ({ page }) => { 107 | await page.goto('/keybinds?noVim=true') 108 | }) 109 | 110 | test('ctrl j/k does nothing', async ({ page }) => { 111 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 112 | await page.locator(`[cmdk-input]`).press('Control+j') 113 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 114 | await page.locator(`[cmdk-input]`).press('Control+k') 115 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 116 | }) 117 | 118 | test('ctrl n/p does nothing', async ({ page }) => { 119 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 120 | await page.locator(`[cmdk-input]`).press('Control+n') 121 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 122 | await page.locator(`[cmdk-input]`).press('Control+p') 123 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'first') 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /test/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="next" /> 2 | /// <reference types="next/image-types/global" /> 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /test/numeric.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('behavior for numeric values', async () => { 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('/numeric') 6 | }) 7 | 8 | test('items filter correctly on numeric inputs', async ({ page }) => { 9 | const input = page.locator(`[cmdk-input]`) 10 | await input.type('112') 11 | const removed = page.locator(`[cmdk-item][data-value="removed"]`) 12 | const remains = page.locator(`[cmdk-item][data-value="foo.bar112.value"]`) 13 | await expect(removed).toHaveCount(0) 14 | await expect(remains).toHaveCount(1) 15 | }) 16 | 17 | test('items filter correctly on non-numeric inputs', async ({ page }) => { 18 | const input = page.locator(`[cmdk-input]`) 19 | await input.type('bar') 20 | const removed = page.locator(`[cmdk-item][data-value="removed"]`) 21 | const remains = page.locator(`[cmdk-item][data-value="foo.bar112.value"]`) 22 | await expect(removed).toHaveCount(0) 23 | await expect(remains).toHaveCount(1) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmdk-tests", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "next" 6 | }, 7 | "dependencies": { 8 | "@radix-ui/react-portal": "^1.0.4", 9 | "@types/node": "18.0.4", 10 | "@types/react": "18.0.15", 11 | "@types/react-dom": "18.0.6", 12 | "cmdk": "workspace:*", 13 | "next": "13.5.1", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0", 16 | "typescript": "4.7.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../style.css' 2 | 3 | export default function App({ Component, pageProps }) { 4 | return <Component {...pageProps} /> 5 | } 6 | -------------------------------------------------------------------------------- /test/pages/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import * as React from 'react' 3 | 4 | const Page = () => { 5 | const [open, setOpen] = React.useState(false) 6 | 7 | React.useEffect(() => { 8 | setOpen(true) 9 | }, []) 10 | 11 | return ( 12 | <div> 13 | <Command.Dialog open={open} onOpenChange={setOpen}> 14 | <Command.Input placeholder="Search…" /> 15 | <Command.List> 16 | <Command.Empty>No results.</Command.Empty> 17 | <Command.Item onSelect={() => console.log('Item selected')}>Item</Command.Item> 18 | <Command.Item value="xxx">Value</Command.Item> 19 | </Command.List> 20 | </Command.Dialog> 21 | </div> 22 | ) 23 | } 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /test/pages/group.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import * as React from 'react' 3 | 4 | const Page = () => { 5 | const [search, setSearch] = React.useState('') 6 | const [forceMount, setForceMount] = React.useState(false) 7 | 8 | return ( 9 | <div> 10 | <button data-testid="forceMount" onClick={() => setForceMount(!forceMount)}> 11 | Force mount Group Letters 12 | </button> 13 | 14 | <Command> 15 | <Command.Input placeholder="Search…" value={search} onValueChange={setSearch} /> 16 | <Command.List> 17 | <Command.Empty>No results.</Command.Empty> 18 | <Command.Group heading="Animals"> 19 | <Command.Item>Giraffe</Command.Item> 20 | <Command.Item>Chicken</Command.Item> 21 | </Command.Group> 22 | 23 | <Command.Group forceMount={forceMount} heading="Letters"> 24 | <Command.Item>A</Command.Item> 25 | <Command.Item>B</Command.Item> 26 | <Command.Item>Z</Command.Item> 27 | </Command.Group> 28 | 29 | {!!search && ( 30 | <Command.Group heading="Numbers"> 31 | <Command.Item>One</Command.Item> 32 | <Command.Item>Two</Command.Item> 33 | <Command.Item>Three</Command.Item> 34 | </Command.Group> 35 | )} 36 | </Command.List> 37 | </Command> 38 | </div> 39 | ) 40 | } 41 | 42 | export default Page 43 | -------------------------------------------------------------------------------- /test/pages/huge.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import * as React from 'react' 3 | 4 | const items = new Array(1000).fill(0) 5 | 6 | const Page = () => { 7 | return ( 8 | <div> 9 | <React.Profiler 10 | id="huge-command" 11 | onRender={(id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) => { 12 | console.log({ phase, actualDuration, baseDuration }) 13 | }} 14 | > 15 | <Command> 16 | <Command.Input placeholder="Search…" /> 17 | <Command.List> 18 | {items.map((_, i) => { 19 | return <Item key={`item-${i}`} /> 20 | })} 21 | </Command.List> 22 | </Command> 23 | </React.Profiler> 24 | </div> 25 | ) 26 | } 27 | 28 | const Item = () => { 29 | const id = React.useId() 30 | 31 | return <Command.Item key={id}>Item {id}</Command.Item> 32 | } 33 | 34 | export default Page 35 | -------------------------------------------------------------------------------- /test/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | 3 | const Page = () => { 4 | return ( 5 | <div> 6 | <Command className="root"> 7 | <Command.Input placeholder="Search…" className="input" /> 8 | <Command.List className="list"> 9 | <Command.Empty className="empty">No results.</Command.Empty> 10 | <Command.Item 11 | keywords={['key']} 12 | onSelect={() => { 13 | ;(window as any).onSelect = 'Item selected' 14 | }} 15 | className="item" 16 | > 17 | Item 18 | </Command.Item> 19 | <Command.Item value="xxx" className="item"> 20 | Value 21 | </Command.Item> 22 | </Command.List> 23 | </Command> 24 | </div> 25 | ) 26 | } 27 | 28 | export default Page 29 | -------------------------------------------------------------------------------- /test/pages/item-advanced.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import * as React from 'react' 3 | 4 | const Page = () => { 5 | const [count, setCount] = React.useState(0) 6 | 7 | return ( 8 | <div> 9 | <button data-testid="increment" onClick={() => setCount((c) => c + 1)}> 10 | Increment count 11 | </button> 12 | 13 | <Command> 14 | <Command.Input placeholder="Search…" /> 15 | <Command.List> 16 | <Command.Empty>No results.</Command.Empty> 17 | <Command.Item value={`Item A ${count}`}>Item A {count}</Command.Item> 18 | <Command.Item value={`Item B ${count}`}>Item B {count}</Command.Item> 19 | </Command.List> 20 | </Command> 21 | </div> 22 | ) 23 | } 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /test/pages/item.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import * as React from 'react' 3 | 4 | const Page = () => { 5 | const [unmount, setUnmount] = React.useState(false) 6 | const [mount, setMount] = React.useState(false) 7 | const [many, setMany] = React.useState(false) 8 | const [forceMount, setForceMount] = React.useState(false) 9 | 10 | return ( 11 | <div> 12 | <button data-testid="mount" onClick={() => setMount(!mount)}> 13 | Toggle item B 14 | </button> 15 | 16 | <button data-testid="unmount" onClick={() => setUnmount(!unmount)}> 17 | Toggle item A 18 | </button> 19 | 20 | <button data-testid="many" onClick={() => setMany(!many)}> 21 | Toggle many items 22 | </button> 23 | 24 | <button data-testid="forceMount" onClick={() => setForceMount(!forceMount)}> 25 | Force mount item A 26 | </button> 27 | 28 | <Command> 29 | <Command.Input placeholder="Search…" /> 30 | <Command.List> 31 | <Command.Empty>No results.</Command.Empty> 32 | {!unmount && <Command.Item forceMount={forceMount}>A</Command.Item>} 33 | {many && ( 34 | <> 35 | <Command.Item>1</Command.Item> 36 | <Command.Item>2</Command.Item> 37 | <Command.Item>3</Command.Item> 38 | </> 39 | )} 40 | {mount && <Command.Item>B</Command.Item>} 41 | </Command.List> 42 | </Command> 43 | </div> 44 | ) 45 | } 46 | 47 | export default Page 48 | -------------------------------------------------------------------------------- /test/pages/keybinds.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import { useRouter } from 'next/router' 3 | import * as React from 'react' 4 | 5 | const Page = () => { 6 | const { 7 | query: { noVim }, 8 | } = useRouter() 9 | return ( 10 | <div> 11 | <Command vimBindings={!noVim}> 12 | <Command.Input /> 13 | <Command.List> 14 | <Command.Empty>No results.</Command.Empty> 15 | 16 | <Command.Item value="disabled" disabled> 17 | Disabled 18 | </Command.Item> 19 | 20 | <Command.Item value="first">First</Command.Item> 21 | 22 | <Command.Group heading="Letters"> 23 | <Command.Item>A</Command.Item> 24 | <Command.Item>B</Command.Item> 25 | <Command.Item>Z</Command.Item> 26 | </Command.Group> 27 | 28 | <Command.Group heading="Fruits"> 29 | <Command.Item>Apple</Command.Item> 30 | <Command.Item>Banana</Command.Item> 31 | <Command.Item>Orange</Command.Item> 32 | <Command.Item disabled>Dragon Fruit</Command.Item> 33 | <Command.Item>Pear</Command.Item> 34 | </Command.Group> 35 | 36 | <Command.Item value="last">Last</Command.Item> 37 | 38 | <Command.Item value="disabled-3" disabled> 39 | Disabled 3 40 | </Command.Item> 41 | </Command.List> 42 | </Command> 43 | </div> 44 | ) 45 | } 46 | 47 | export default Page 48 | -------------------------------------------------------------------------------- /test/pages/numeric.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | 3 | const Page = () => { 4 | return ( 5 | <div> 6 | <Command className="root"> 7 | <Command.Input placeholder="Search…" className="input" /> 8 | <Command.List className="list"> 9 | <Command.Empty className="empty">No results.</Command.Empty> 10 | <Command.Item value="removed" className="item"> 11 | To be removed 12 | </Command.Item> 13 | <Command.Item value="foo.bar112.value" className="item"> 14 | Not to be removed 15 | </Command.Item> 16 | </Command.List> 17 | </Command> 18 | </div> 19 | ) 20 | } 21 | 22 | export default Page 23 | -------------------------------------------------------------------------------- /test/pages/portal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Command } from 'cmdk' 3 | import * as Portal from '@radix-ui/react-portal' 4 | 5 | const Page = () => { 6 | const [render, setRender] = React.useState(false) 7 | const [search, setSearch] = React.useState('') 8 | const [open, setOpen] = React.useState(true) 9 | React.useEffect(() => setRender(true), []) 10 | if (!render) return null 11 | 12 | return ( 13 | <div> 14 | <button data-testid="controlledSearch" onClick={() => setSearch('eat')}> 15 | Change search value 16 | </button> 17 | <button data-testid="openClosePopover" onClick={() => setOpen((val) => !val)}> 18 | {open ? 'Close' : 'Open'} 19 | </button> 20 | <Command className="root"> 21 | <Command.Input value={search} onValueChange={setSearch} placeholder="Search…" className="input" /> 22 | 23 | <Portal.Root data-portal="true"> 24 | {open && ( 25 | <Command.List className="list"> 26 | <Command.Item className="item">Apple</Command.Item> 27 | <Command.Item className="item">Banana</Command.Item> 28 | <Command.Item className="item">Cherry</Command.Item> 29 | <Command.Item className="item">Dragonfruit</Command.Item> 30 | <Command.Item className="item">Elderberry</Command.Item> 31 | <Command.Item className="item">Fig</Command.Item> 32 | <Command.Item className="item">Grape</Command.Item> 33 | <Command.Item className="item">Honeydew</Command.Item> 34 | <Command.Item className="item">Jackfruit</Command.Item> 35 | <Command.Item className="item">Kiwi</Command.Item> 36 | <Command.Item className="item">Lemon</Command.Item> 37 | <Command.Item className="item">Mango</Command.Item> 38 | <Command.Item className="item">Nectarine</Command.Item> 39 | <Command.Item className="item">Orange</Command.Item> 40 | <Command.Item className="item">Papaya</Command.Item> 41 | <Command.Item className="item">Quince</Command.Item> 42 | <Command.Item className="item">Raspberry</Command.Item> 43 | <Command.Item className="item">Strawberry</Command.Item> 44 | <Command.Item className="item">Tangerine</Command.Item> 45 | <Command.Item className="item">Ugli</Command.Item> 46 | <Command.Item className="item">Watermelon</Command.Item> 47 | <Command.Item className="item">Xigua</Command.Item> 48 | <Command.Item className="item">Yuzu</Command.Item> 49 | <Command.Item className="item">Zucchini</Command.Item> 50 | </Command.List> 51 | )} 52 | </Portal.Root> 53 | </Command> 54 | </div> 55 | ) 56 | } 57 | 58 | export default Page 59 | -------------------------------------------------------------------------------- /test/pages/props.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import { useRouter } from 'next/router' 3 | import * as React from 'react' 4 | 5 | const Page = () => { 6 | const [value, setValue] = React.useState('ant') 7 | const [search, setSearch] = React.useState('') 8 | const [shouldFilter, setShouldFilter] = React.useState(true) 9 | const [customFilter, setCustomFilter] = React.useState(false) 10 | const router = useRouter() 11 | 12 | React.useEffect(() => { 13 | if (router.isReady) { 14 | setShouldFilter(router.query.shouldFilter === 'false' ? false : true) 15 | setCustomFilter(router.query.customFilter === 'true' ? true : false) 16 | setValue((router.query.initialValue as string) ?? 'ant') 17 | } 18 | }, [router.isReady]) 19 | 20 | return ( 21 | <div> 22 | <div data-testid="value">{value}</div> 23 | <div data-testid="search">{search}</div> 24 | 25 | <button data-testid="controlledValue" onClick={() => setValue('anteater')}> 26 | Change value 27 | </button> 28 | <button data-testid="controlledSearch" onClick={() => setSearch('eat')}> 29 | Change search value 30 | </button> 31 | 32 | <Command 33 | shouldFilter={shouldFilter} 34 | value={value} 35 | onValueChange={setValue} 36 | filter={ 37 | customFilter 38 | ? (item: string | undefined, search: string | undefined) => { 39 | console.log(item, search) 40 | if (!search || !item) return 1 41 | return item.endsWith(search) ? 1 : 0 42 | } 43 | : undefined 44 | } 45 | > 46 | <Command.Input placeholder="Search…" value={search} onValueChange={setSearch} /> 47 | <Command.List> 48 | <Command.Item>ant</Command.Item> 49 | <Command.Item>anteater</Command.Item> 50 | </Command.List> 51 | </Command> 52 | </div> 53 | ) 54 | } 55 | 56 | export default Page 57 | -------------------------------------------------------------------------------- /test/props.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | test.describe('props', async () => { 4 | test('results do not change when filtering is disabled', async ({ page }) => { 5 | await page.goto('/props?shouldFilter=false') 6 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(2) 7 | await page.locator(`[cmdk-input]`).type('z') 8 | await expect(page.locator(`[cmdk-item]`)).toHaveCount(2) 9 | }) 10 | 11 | test('results match against custom filter', async ({ page }) => { 12 | await page.goto('/props?customFilter=true') 13 | await page.locator(`[cmdk-input]`).type(`ant`) 14 | await expect(page.locator(`[cmdk-item]`)).toHaveAttribute('data-value', 'ant') 15 | }) 16 | 17 | test('controlled value', async ({ page }) => { 18 | await page.goto('/props') 19 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'ant') 20 | await page.locator(`data-testid=controlledValue`).click() 21 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'anteater') 22 | }) 23 | 24 | test('keep controlled value if empty results', async ({ page }) => { 25 | await page.goto('/props') 26 | await expect(page.locator(`[data-testid=value]`)).toHaveText('ant') 27 | await page.locator(`[cmdk-input]`).fill('d') 28 | await expect(page.locator(`[data-testid=value]`)).toHaveText('') 29 | await page.locator(`[cmdk-input]`).fill('ant') 30 | await expect(page.locator(`[data-testid=value]`)).toHaveText('ant') 31 | }) 32 | 33 | test('controlled search', async ({ page }) => { 34 | await page.goto('/props') 35 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'ant') 36 | await page.locator(`data-testid=controlledSearch`).click() 37 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'anteater') 38 | }) 39 | 40 | test('keep focus on the provided initial value', async ({ page }) => { 41 | await page.goto('/props?initialValue=anteater') 42 | await expect(page.locator(`[cmdk-item][aria-selected="true"]`)).toHaveAttribute('data-value', 'anteater') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/style.css: -------------------------------------------------------------------------------- 1 | [cmdk-item][aria-selected='true'] { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../dialog.test.ts"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react", 15 | "noEmit": true 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 18 | "exclude": ["node_modules", "build", "dist", ".next"] 19 | } 20 | -------------------------------------------------------------------------------- /website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | 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). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | pnpm run dev 9 | ``` 10 | 11 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 12 | 13 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 14 | 15 | [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`. 16 | 17 | 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. 18 | 19 | ## Learn More 20 | 21 | To learn more about Next.js, take a look at the following resources: 22 | 23 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 24 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 25 | 26 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 27 | 28 | ## Deploy on Vercel 29 | 30 | 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. 31 | 32 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 33 | -------------------------------------------------------------------------------- /website/components/cmdk/framer.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | import React from 'react' 3 | 4 | export function FramerCMDK() { 5 | const [value, setValue] = React.useState('Button') 6 | return ( 7 | <div className="framer"> 8 | <Command value={value} onValueChange={(v) => setValue(v)}> 9 | <div cmdk-framer-header=""> 10 | <SearchIcon /> 11 | <Command.Input autoFocus placeholder="Find components, packages, and interactions..." /> 12 | </div> 13 | <Command.List> 14 | <div cmdk-framer-items=""> 15 | <div cmdk-framer-left=""> 16 | <Command.Group heading="Components"> 17 | <Item value="Button" subtitle="Trigger actions"> 18 | <ButtonIcon /> 19 | </Item> 20 | <Item value="Input" subtitle="Retrieve user input"> 21 | <InputIcon /> 22 | </Item> 23 | <Item value="Radio" subtitle="Single choice input"> 24 | <RadioIcon /> 25 | </Item> 26 | <Item value="Badge" subtitle="Annotate context"> 27 | <BadgeIcon /> 28 | </Item> 29 | <Item value="Slider" subtitle="Free range picker"> 30 | <SliderIcon /> 31 | </Item> 32 | <Item value="Avatar" subtitle="Illustrate the user"> 33 | <AvatarIcon /> 34 | </Item> 35 | <Item value="Container" subtitle="Lay out items"> 36 | <ContainerIcon /> 37 | </Item> 38 | </Command.Group> 39 | </div> 40 | <hr cmdk-framer-separator="" /> 41 | <div cmdk-framer-right=""> 42 | {value === 'Button' && <Button />} 43 | {value === 'Input' && <Input />} 44 | {value === 'Badge' && <Badge />} 45 | {value === 'Radio' && <Radio />} 46 | {value === 'Avatar' && <Avatar />} 47 | {value === 'Slider' && <Slider />} 48 | {value === 'Container' && <Container />} 49 | </div> 50 | </div> 51 | </Command.List> 52 | </Command> 53 | </div> 54 | ) 55 | } 56 | 57 | function Button() { 58 | return <button>Primary</button> 59 | } 60 | 61 | function Input() { 62 | return <input type="text" placeholder="Placeholder" /> 63 | } 64 | 65 | function Badge() { 66 | return <div cmdk-framer-badge="">Badge</div> 67 | } 68 | 69 | function Radio() { 70 | return ( 71 | <label cmdk-framer-radio=""> 72 | <input type="radio" defaultChecked /> 73 | Radio Button 74 | </label> 75 | ) 76 | } 77 | 78 | function Slider() { 79 | return ( 80 | <div cmdk-framer-slider=""> 81 | <div /> 82 | </div> 83 | ) 84 | } 85 | 86 | function Avatar() { 87 | return <img src="/rauno.jpeg" alt="Avatar of Rauno" /> 88 | } 89 | 90 | function Container() { 91 | return <div cmdk-framer-container="" /> 92 | } 93 | 94 | function Item({ children, value, subtitle }: { children: React.ReactNode; value: string; subtitle: string }) { 95 | return ( 96 | <Command.Item value={value} onSelect={() => {}}> 97 | <div cmdk-framer-icon-wrapper="">{children}</div> 98 | <div cmdk-framer-item-meta=""> 99 | {value} 100 | <span cmdk-framer-item-subtitle="">{subtitle}</span> 101 | </div> 102 | </Command.Item> 103 | ) 104 | } 105 | 106 | function ButtonIcon() { 107 | return ( 108 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 109 | <path 110 | 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" 111 | fill="currentColor" 112 | fillRule="evenodd" 113 | clipRule="evenodd" 114 | ></path> 115 | </svg> 116 | ) 117 | } 118 | 119 | function InputIcon() { 120 | return ( 121 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 122 | <path 123 | 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" 124 | fill="currentColor" 125 | fillRule="evenodd" 126 | clipRule="evenodd" 127 | ></path> 128 | </svg> 129 | ) 130 | } 131 | 132 | function RadioIcon() { 133 | return ( 134 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 135 | <path 136 | 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" 137 | fill="currentColor" 138 | fillRule="evenodd" 139 | clipRule="evenodd" 140 | ></path> 141 | </svg> 142 | ) 143 | } 144 | 145 | function BadgeIcon() { 146 | return ( 147 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 148 | <path 149 | 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" 150 | fill="currentColor" 151 | fillRule="evenodd" 152 | clipRule="evenodd" 153 | ></path> 154 | </svg> 155 | ) 156 | } 157 | 158 | function ToggleIcon() { 159 | return ( 160 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 161 | <path 162 | 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" 163 | fill="currentColor" 164 | fillRule="evenodd" 165 | clipRule="evenodd" 166 | ></path> 167 | </svg> 168 | ) 169 | } 170 | 171 | function AvatarIcon() { 172 | return ( 173 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 174 | <path 175 | 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" 176 | fill="currentColor" 177 | fillRule="evenodd" 178 | clipRule="evenodd" 179 | ></path> 180 | </svg> 181 | ) 182 | } 183 | 184 | function ContainerIcon() { 185 | return ( 186 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 187 | <path 188 | 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" 189 | fill="currentColor" 190 | fillRule="evenodd" 191 | clipRule="evenodd" 192 | ></path> 193 | </svg> 194 | ) 195 | } 196 | 197 | function SearchIcon() { 198 | return ( 199 | <svg 200 | xmlns="http://www.w3.org/2000/svg" 201 | className="h-6 w-6" 202 | fill="none" 203 | viewBox="0 0 24 24" 204 | stroke="currentColor" 205 | strokeWidth={1.5} 206 | > 207 | <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> 208 | </svg> 209 | ) 210 | } 211 | 212 | function SliderIcon() { 213 | return ( 214 | <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> 215 | <path 216 | 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" 217 | fill="currentColor" 218 | fillRule="evenodd" 219 | clipRule="evenodd" 220 | ></path> 221 | </svg> 222 | ) 223 | } 224 | -------------------------------------------------------------------------------- /website/components/cmdk/linear.tsx: -------------------------------------------------------------------------------- 1 | import { Command } from 'cmdk' 2 | 3 | export function LinearCMDK() { 4 | return ( 5 | <div className="linear"> 6 | <Command> 7 | <div cmdk-linear-badge="">Issue - FUN-343</div> 8 | <Command.Input autoFocus placeholder="Type a command or search..." /> 9 | <Command.List> 10 | <Command.Empty>No results found.</Command.Empty> 11 | {items.map(({ icon, label, shortcut }) => { 12 | return ( 13 | <Command.Item key={label} value={label}> 14 | {icon} 15 | {label} 16 | <div cmdk-linear-shortcuts=""> 17 | {shortcut.map((key) => { 18 | return <kbd key={key}>{key}</kbd> 19 | })} 20 | </div> 21 | </Command.Item> 22 | ) 23 | })} 24 | </Command.List> 25 | </Command> 26 | </div> 27 | ) 28 | } 29 | 30 | const items = [ 31 | { 32 | icon: <AssignToIcon />, 33 | label: 'Assign to...', 34 | shortcut: ['A'], 35 | }, 36 | { 37 | icon: <AssignToMeIcon />, 38 | label: 'Assign to me', 39 | shortcut: ['I'], 40 | }, 41 | { 42 | icon: <ChangeStatusIcon />, 43 | label: 'Change status...', 44 | shortcut: ['S'], 45 | }, 46 | { 47 | icon: <ChangePriorityIcon />, 48 | label: 'Change priority...', 49 | shortcut: ['P'], 50 | }, 51 | { 52 | icon: <ChangeLabelsIcon />, 53 | label: 'Change labels...', 54 | shortcut: ['L'], 55 | }, 56 | { 57 | icon: <RemoveLabelIcon />, 58 | label: 'Remove label...', 59 | shortcut: ['⇧', 'L'], 60 | }, 61 | { 62 | icon: <SetDueDateIcon />, 63 | label: 'Set due date...', 64 | shortcut: ['⇧', 'D'], 65 | }, 66 | ] 67 | 68 | function AssignToIcon() { 69 | return ( 70 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 71 | <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" /> 72 | </svg> 73 | ) 74 | } 75 | 76 | function AssignToMeIcon() { 77 | return ( 78 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 79 | <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" /> 80 | <path 81 | fillRule="evenodd" 82 | clipRule="evenodd" 83 | 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" 84 | /> 85 | <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" /> 86 | </svg> 87 | ) 88 | } 89 | 90 | function ChangeStatusIcon() { 91 | return ( 92 | <svg width="16" height="16" viewBox="-1 -1 15 15" fill="currentColor"> 93 | <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" /> 94 | <path 95 | fillRule="evenodd" 96 | clipRule="evenodd" 97 | 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" 98 | /> 99 | </svg> 100 | ) 101 | } 102 | 103 | function ChangePriorityIcon() { 104 | return ( 105 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 106 | <rect x="1" y="8" width="3" height="6" rx="1"></rect> 107 | <rect x="6" y="5" width="3" height="9" rx="1"></rect> 108 | <rect x="11" y="2" width="3" height="12" rx="1"></rect> 109 | </svg> 110 | ) 111 | } 112 | 113 | function ChangeLabelsIcon() { 114 | return ( 115 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 116 | <path 117 | fillRule="evenodd" 118 | clipRule="evenodd" 119 | 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" 120 | /> 121 | </svg> 122 | ) 123 | } 124 | 125 | function RemoveLabelIcon() { 126 | return ( 127 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 128 | <path 129 | fillRule="evenodd" 130 | clipRule="evenodd" 131 | 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" 132 | /> 133 | </svg> 134 | ) 135 | } 136 | 137 | function SetDueDateIcon() { 138 | return ( 139 | <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> 140 | <path 141 | fillRule="evenodd" 142 | clipRule="evenodd" 143 | 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" 144 | /> 145 | </svg> 146 | ) 147 | } 148 | -------------------------------------------------------------------------------- /website/components/cmdk/raycast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTheme } from 'next-themes' 3 | import * as Popover from '@radix-ui/react-popover' 4 | import { Command } from 'cmdk' 5 | import { Logo, LinearIcon, FigmaIcon, SlackIcon, YouTubeIcon, RaycastIcon } from 'components' 6 | 7 | export function RaycastCMDK() { 8 | const { resolvedTheme: theme } = useTheme() 9 | const [value, setValue] = React.useState('linear') 10 | const inputRef = React.useRef<HTMLInputElement | null>(null) 11 | const listRef = React.useRef(null) 12 | 13 | React.useEffect(() => { 14 | inputRef?.current?.focus() 15 | }, []) 16 | 17 | return ( 18 | <div className="raycast"> 19 | <Command value={value} onValueChange={(v) => setValue(v)}> 20 | <div cmdk-raycast-top-shine="" /> 21 | <Command.Input ref={inputRef} autoFocus placeholder="Search for apps and commands..." /> 22 | <hr cmdk-raycast-loader="" /> 23 | <Command.List ref={listRef}> 24 | <Command.Empty>No results found.</Command.Empty> 25 | <Command.Group heading="Suggestions"> 26 | <Item value="Linear" keywords={['issue', 'sprint']}> 27 | <Logo> 28 | <LinearIcon 29 | style={{ 30 | width: 12, 31 | height: 12, 32 | }} 33 | /> 34 | </Logo> 35 | Linear 36 | </Item> 37 | <Item value="Figma" keywords={['design', 'ui', 'ux']}> 38 | <Logo> 39 | <FigmaIcon /> 40 | </Logo> 41 | Figma 42 | </Item> 43 | <Item value="Slack" keywords={['chat', 'team', 'communication']}> 44 | <Logo> 45 | <SlackIcon /> 46 | </Logo> 47 | Slack 48 | </Item> 49 | <Item value="YouTube" keywords={['video', 'watch', 'stream']}> 50 | <Logo> 51 | <YouTubeIcon /> 52 | </Logo> 53 | YouTube 54 | </Item> 55 | <Item value="Raycast" keywords={['productivity', 'tools', 'apps']}> 56 | <Logo> 57 | <RaycastIcon /> 58 | </Logo> 59 | Raycast 60 | </Item> 61 | </Command.Group> 62 | <Command.Group heading="Commands"> 63 | <Item isCommand value="Clipboard History" keywords={['copy', 'paste', 'clipboard']}> 64 | <Logo> 65 | <ClipboardIcon /> 66 | </Logo> 67 | Clipboard History 68 | </Item> 69 | <Item isCommand value="Import Extension" keywords={['import', 'extension']}> 70 | <HammerIcon /> 71 | Import Extension 72 | </Item> 73 | <Item isCommand value="Manage Extensions" keywords={['manage', 'extension']}> 74 | <HammerIcon /> 75 | Manage Extensions 76 | </Item> 77 | </Command.Group> 78 | </Command.List> 79 | 80 | <div cmdk-raycast-footer=""> 81 | {theme === 'dark' ? <RaycastDarkIcon /> : <RaycastLightIcon />} 82 | 83 | <button cmdk-raycast-open-trigger=""> 84 | Open Application 85 | <kbd>↵</kbd> 86 | </button> 87 | 88 | <hr /> 89 | 90 | <SubCommand listRef={listRef} selectedValue={value} inputRef={inputRef} /> 91 | </div> 92 | </Command> 93 | </div> 94 | ) 95 | } 96 | 97 | function Item({ 98 | children, 99 | value, 100 | keywords, 101 | isCommand = false, 102 | }: { 103 | children: React.ReactNode 104 | value: string 105 | keywords?: string[] 106 | isCommand?: boolean 107 | }) { 108 | return ( 109 | <Command.Item value={value} keywords={keywords} onSelect={() => {}}> 110 | {children} 111 | <span cmdk-raycast-meta="">{isCommand ? 'Command' : 'Application'}</span> 112 | </Command.Item> 113 | ) 114 | } 115 | 116 | function SubCommand({ 117 | inputRef, 118 | listRef, 119 | selectedValue, 120 | }: { 121 | inputRef: React.RefObject<HTMLInputElement> 122 | listRef: React.RefObject<HTMLElement> 123 | selectedValue: string 124 | }) { 125 | const [open, setOpen] = React.useState(false) 126 | 127 | React.useEffect(() => { 128 | function listener(e: KeyboardEvent) { 129 | if (e.key === 'k' && e.metaKey) { 130 | e.preventDefault() 131 | setOpen((o) => !o) 132 | } 133 | } 134 | 135 | document.addEventListener('keydown', listener) 136 | 137 | return () => { 138 | document.removeEventListener('keydown', listener) 139 | } 140 | }, []) 141 | 142 | React.useEffect(() => { 143 | const el = listRef.current 144 | 145 | if (!el) return 146 | 147 | if (open) { 148 | el.style.overflow = 'hidden' 149 | } else { 150 | el.style.overflow = '' 151 | } 152 | }, [open, listRef]) 153 | 154 | return ( 155 | <Popover.Root open={open} onOpenChange={setOpen} modal> 156 | <Popover.Trigger cmdk-raycast-subcommand-trigger="" onClick={() => setOpen(true)} aria-expanded={open}> 157 | Actions 158 | <kbd>⌘</kbd> 159 | <kbd>K</kbd> 160 | </Popover.Trigger> 161 | <Popover.Content 162 | side="top" 163 | align="end" 164 | className="raycast-submenu" 165 | sideOffset={16} 166 | alignOffset={0} 167 | onCloseAutoFocus={(e) => { 168 | e.preventDefault() 169 | inputRef?.current?.focus() 170 | }} 171 | > 172 | <Command> 173 | <Command.List> 174 | <Command.Group heading={selectedValue}> 175 | <SubItem shortcut="↵"> 176 | <WindowIcon /> 177 | Open Application 178 | </SubItem> 179 | <SubItem shortcut="⌘ ↵"> 180 | <FinderIcon /> 181 | Show in Finder 182 | </SubItem> 183 | <SubItem shortcut="⌘ I"> 184 | <FinderIcon /> 185 | Show Info in Finder 186 | </SubItem> 187 | <SubItem shortcut="⌘ ⇧ F"> 188 | <StarIcon /> 189 | Add to Favorites 190 | </SubItem> 191 | </Command.Group> 192 | </Command.List> 193 | <Command.Input placeholder="Search for actions..." /> 194 | </Command> 195 | </Popover.Content> 196 | </Popover.Root> 197 | ) 198 | } 199 | 200 | function SubItem({ children, shortcut }: { children: React.ReactNode; shortcut: string }) { 201 | return ( 202 | <Command.Item> 203 | {children} 204 | <div cmdk-raycast-submenu-shortcuts=""> 205 | {shortcut.split(' ').map((key) => { 206 | return <kbd key={key}>{key}</kbd> 207 | })} 208 | </div> 209 | </Command.Item> 210 | ) 211 | } 212 | 213 | function TerminalIcon() { 214 | return ( 215 | <svg 216 | width="24" 217 | height="24" 218 | viewBox="0 0 24 24" 219 | fill="none" 220 | stroke="currentColor" 221 | strokeWidth="2" 222 | strokeLinecap="round" 223 | strokeLinejoin="round" 224 | > 225 | <polyline points="4 17 10 11 4 5"></polyline> 226 | <line x1="12" y1="19" x2="20" y2="19"></line> 227 | </svg> 228 | ) 229 | } 230 | 231 | function RaycastLightIcon() { 232 | return ( 233 | <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"> 234 | <path 235 | fillRule="evenodd" 236 | clipRule="evenodd" 237 | 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" 238 | fill="#FF6363" 239 | /> 240 | </svg> 241 | ) 242 | } 243 | 244 | function RaycastDarkIcon() { 245 | return ( 246 | <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg"> 247 | <path 248 | fillRule="evenodd" 249 | clipRule="evenodd" 250 | 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" 251 | fill="#FF6363" 252 | /> 253 | </svg> 254 | ) 255 | } 256 | 257 | function WindowIcon() { 258 | return ( 259 | <svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 260 | <path 261 | 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" 262 | stroke="currentColor" 263 | strokeWidth="1.5" 264 | strokeLinecap="round" 265 | strokeLinejoin="round" 266 | /> 267 | </svg> 268 | ) 269 | } 270 | 271 | function FinderIcon() { 272 | return ( 273 | <svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 274 | <path 275 | 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" 276 | stroke="currentColor" 277 | strokeWidth="1.5" 278 | strokeLinecap="round" 279 | strokeLinejoin="round" 280 | /> 281 | </svg> 282 | ) 283 | } 284 | 285 | function StarIcon() { 286 | return ( 287 | <svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 288 | <path 289 | 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" 290 | stroke="currentColor" 291 | strokeWidth="1.5" 292 | strokeLinecap="round" 293 | strokeLinejoin="round" 294 | /> 295 | </svg> 296 | ) 297 | } 298 | 299 | function ClipboardIcon() { 300 | return ( 301 | <div cmdk-raycast-clipboard-icon=""> 302 | <svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 303 | <path 304 | 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" 305 | stroke="currentColor" 306 | strokeWidth="1.5" 307 | strokeLinecap="round" 308 | strokeLinejoin="round" 309 | /> 310 | </svg> 311 | </div> 312 | ) 313 | } 314 | 315 | function HammerIcon() { 316 | return ( 317 | <div cmdk-raycast-hammer-icon=""> 318 | <svg width="32" height="32" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 319 | <path 320 | 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" 321 | stroke="currentColor" 322 | strokeWidth="1.5" 323 | strokeLinecap="round" 324 | strokeLinejoin="round" 325 | /> 326 | </svg> 327 | </div> 328 | ) 329 | } 330 | -------------------------------------------------------------------------------- /website/components/cmdk/vercel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Command } from 'cmdk' 3 | 4 | export function VercelCMDK() { 5 | const ref = React.useRef<HTMLDivElement | null>(null) 6 | const [inputValue, setInputValue] = React.useState('') 7 | 8 | const [pages, setPages] = React.useState<string[]>(['home']) 9 | const activePage = pages[pages.length - 1] 10 | const isHome = activePage === 'home' 11 | 12 | const popPage = React.useCallback(() => { 13 | setPages((pages) => { 14 | const x = [...pages] 15 | x.splice(-1, 1) 16 | return x 17 | }) 18 | }, []) 19 | 20 | const onKeyDown = React.useCallback( 21 | (e: KeyboardEvent) => { 22 | if (isHome || inputValue.length) { 23 | return 24 | } 25 | 26 | if (e.key === 'Backspace') { 27 | e.preventDefault() 28 | popPage() 29 | } 30 | }, 31 | [inputValue.length, isHome, popPage], 32 | ) 33 | 34 | function bounce() { 35 | if (ref.current) { 36 | ref.current.style.transform = 'scale(0.96)' 37 | setTimeout(() => { 38 | if (ref.current) { 39 | ref.current.style.transform = '' 40 | } 41 | }, 100) 42 | 43 | setInputValue('') 44 | } 45 | } 46 | 47 | return ( 48 | <div className="vercel"> 49 | <Command 50 | ref={ref} 51 | onKeyDown={(e: React.KeyboardEvent) => { 52 | if (e.key === 'Enter') { 53 | bounce() 54 | } 55 | 56 | if (isHome || inputValue.length) { 57 | return 58 | } 59 | 60 | if (e.key === 'Backspace') { 61 | e.preventDefault() 62 | popPage() 63 | bounce() 64 | } 65 | }} 66 | > 67 | <div> 68 | {pages.map((p) => ( 69 | <div key={p} cmdk-vercel-badge=""> 70 | {p} 71 | </div> 72 | ))} 73 | </div> 74 | <Command.Input 75 | autoFocus 76 | placeholder="What do you need?" 77 | onValueChange={(value) => { 78 | setInputValue(value) 79 | }} 80 | /> 81 | <Command.List> 82 | <Command.Empty>No results found.</Command.Empty> 83 | {activePage === 'home' && <Home searchProjects={() => setPages([...pages, 'projects'])} />} 84 | {activePage === 'projects' && <Projects />} 85 | </Command.List> 86 | </Command> 87 | </div> 88 | ) 89 | } 90 | 91 | function Home({ searchProjects }: { searchProjects: Function }) { 92 | return ( 93 | <> 94 | <Command.Group heading="Projects"> 95 | <Item 96 | shortcut="S P" 97 | onSelect={() => { 98 | searchProjects() 99 | }} 100 | > 101 | <ProjectsIcon /> 102 | Search Projects... 103 | </Item> 104 | <Item> 105 | <PlusIcon /> 106 | Create New Project... 107 | </Item> 108 | </Command.Group> 109 | <Command.Group heading="Teams"> 110 | <Item shortcut="⇧ P"> 111 | <TeamsIcon /> 112 | Search Teams... 113 | </Item> 114 | <Item> 115 | <PlusIcon /> 116 | Create New Team... 117 | </Item> 118 | </Command.Group> 119 | <Command.Group heading="Help"> 120 | <Item shortcut="⇧ D"> 121 | <DocsIcon /> 122 | Search Docs... 123 | </Item> 124 | <Item> 125 | <FeedbackIcon /> 126 | Send Feedback... 127 | </Item> 128 | <Item> 129 | <ContactIcon /> 130 | Contact Support 131 | </Item> 132 | </Command.Group> 133 | </> 134 | ) 135 | } 136 | 137 | function Projects() { 138 | return ( 139 | <> 140 | <Item>Project 1</Item> 141 | <Item>Project 2</Item> 142 | <Item>Project 3</Item> 143 | <Item>Project 4</Item> 144 | <Item>Project 5</Item> 145 | <Item>Project 6</Item> 146 | </> 147 | ) 148 | } 149 | 150 | function Item({ 151 | children, 152 | shortcut, 153 | onSelect = () => {}, 154 | }: { 155 | children: React.ReactNode 156 | shortcut?: string 157 | onSelect?: (value: string) => void 158 | }) { 159 | return ( 160 | <Command.Item onSelect={onSelect}> 161 | {children} 162 | {shortcut && ( 163 | <div cmdk-vercel-shortcuts=""> 164 | {shortcut.split(' ').map((key) => { 165 | return <kbd key={key}>{key}</kbd> 166 | })} 167 | </div> 168 | )} 169 | </Command.Item> 170 | ) 171 | } 172 | 173 | function ProjectsIcon() { 174 | return ( 175 | <svg 176 | fill="none" 177 | height="24" 178 | shapeRendering="geometricPrecision" 179 | stroke="currentColor" 180 | strokeLinecap="round" 181 | strokeLinejoin="round" 182 | strokeWidth="1.5" 183 | viewBox="0 0 24 24" 184 | width="24" 185 | > 186 | <path d="M3 3h7v7H3z"></path> 187 | <path d="M14 3h7v7h-7z"></path> 188 | <path d="M14 14h7v7h-7z"></path> 189 | <path d="M3 14h7v7H3z"></path> 190 | </svg> 191 | ) 192 | } 193 | 194 | function PlusIcon() { 195 | return ( 196 | <svg 197 | fill="none" 198 | height="24" 199 | shapeRendering="geometricPrecision" 200 | stroke="currentColor" 201 | strokeLinecap="round" 202 | strokeLinejoin="round" 203 | strokeWidth="1.5" 204 | viewBox="0 0 24 24" 205 | width="24" 206 | > 207 | <path d="M12 5v14"></path> 208 | <path d="M5 12h14"></path> 209 | </svg> 210 | ) 211 | } 212 | 213 | function TeamsIcon() { 214 | return ( 215 | <svg 216 | fill="none" 217 | height="24" 218 | shapeRendering="geometricPrecision" 219 | stroke="currentColor" 220 | strokeLinecap="round" 221 | strokeLinejoin="round" 222 | strokeWidth="1.5" 223 | viewBox="0 0 24 24" 224 | width="24" 225 | > 226 | <path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path> 227 | <circle cx="9" cy="7" r="4"></circle> 228 | <path d="M23 21v-2a4 4 0 00-3-3.87"></path> 229 | <path d="M16 3.13a4 4 0 010 7.75"></path> 230 | </svg> 231 | ) 232 | } 233 | 234 | function CopyIcon() { 235 | return ( 236 | <svg 237 | fill="none" 238 | height="24" 239 | shapeRendering="geometricPrecision" 240 | stroke="currentColor" 241 | strokeLinecap="round" 242 | strokeLinejoin="round" 243 | strokeWidth="1.5" 244 | viewBox="0 0 24 24" 245 | width="24" 246 | > 247 | <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> 248 | </svg> 249 | ) 250 | } 251 | 252 | function DocsIcon() { 253 | return ( 254 | <svg 255 | fill="none" 256 | height="24" 257 | shapeRendering="geometricPrecision" 258 | stroke="currentColor" 259 | strokeLinecap="round" 260 | strokeLinejoin="round" 261 | strokeWidth="1.5" 262 | viewBox="0 0 24 24" 263 | width="24" 264 | > 265 | <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"></path> 266 | <path d="M14 2v6h6"></path> 267 | <path d="M16 13H8"></path> 268 | <path d="M16 17H8"></path> 269 | <path d="M10 9H8"></path> 270 | </svg> 271 | ) 272 | } 273 | 274 | function FeedbackIcon() { 275 | return ( 276 | <svg 277 | fill="none" 278 | height="24" 279 | shapeRendering="geometricPrecision" 280 | stroke="currentColor" 281 | strokeLinecap="round" 282 | strokeLinejoin="round" 283 | strokeWidth="1.5" 284 | viewBox="0 0 24 24" 285 | width="24" 286 | > 287 | <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> 288 | </svg> 289 | ) 290 | } 291 | 292 | function ContactIcon() { 293 | return ( 294 | <svg 295 | fill="none" 296 | height="24" 297 | shapeRendering="geometricPrecision" 298 | stroke="currentColor" 299 | strokeLinecap="round" 300 | strokeLinejoin="round" 301 | strokeWidth="1.5" 302 | viewBox="0 0 24 24" 303 | width="24" 304 | > 305 | <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> 306 | <path d="M22 6l-10 7L2 6"></path> 307 | </svg> 308 | ) 309 | } 310 | -------------------------------------------------------------------------------- /website/components/code/code.module.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | border-radius: 12px; 3 | padding: 16px; 4 | backdrop-filter: blur(10px); 5 | border: 1px solid var(--gray6); 6 | position: relative; 7 | line-height: 16px; 8 | background: var(--lowContrast); 9 | white-space: pre-wrap; 10 | box-shadow: rgb(0 0 0 / 10%) 0px 5px 30px -5px; 11 | 12 | @media (prefers-color-scheme: dark) { 13 | background: var(--grayA2); 14 | } 15 | 16 | button { 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | width: 32px; 21 | height: 32px; 22 | background: var(--grayA3); 23 | border-radius: 8px; 24 | position: absolute; 25 | top: 12px; 26 | right: 12px; 27 | color: var(--gray11); 28 | cursor: copy; 29 | transition: color 150ms ease, background 150ms ease, transform 150ms ease; 30 | 31 | &:hover { 32 | color: var(--gray12); 33 | background: var(--grayA4); 34 | } 35 | 36 | &:active { 37 | color: var(--gray12); 38 | background: var(--grayA5); 39 | transform: scale(0.96); 40 | } 41 | } 42 | } 43 | 44 | .shine { 45 | @media (prefers-color-scheme: dark) { 46 | background: linear-gradient( 47 | 90deg, 48 | rgba(56, 189, 248, 0), 49 | var(--gray5) 20%, 50 | var(--gray9) 67.19%, 51 | rgba(236, 72, 153, 0) 52 | ); 53 | height: 1px; 54 | position: absolute; 55 | top: -1px; 56 | width: 97%; 57 | z-index: -1; 58 | } 59 | } 60 | 61 | @media (max-width: 640px) { 62 | .root { 63 | :global(.token-line) { 64 | font-size: 11px !important; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /website/components/code/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import copy from 'copy-to-clipboard' 3 | import Highlight, { defaultProps } from 'prism-react-renderer' 4 | import styles from './code.module.scss' 5 | import { CopyIcon } from 'components/icons' 6 | 7 | const theme = { 8 | plain: { 9 | color: 'var(--gray12)', 10 | fontSize: 12, 11 | fontFamily: 'Menlo, monospace', 12 | }, 13 | styles: [ 14 | { 15 | types: ['comment'], 16 | style: { 17 | color: 'var(--gray9)', 18 | }, 19 | }, 20 | { 21 | types: ['atrule', 'keyword', 'attr-name', 'selector'], 22 | style: { 23 | color: 'var(--gray10)', 24 | }, 25 | }, 26 | { 27 | types: ['punctuation', 'operator'], 28 | style: { 29 | color: 'var(--gray9)', 30 | }, 31 | }, 32 | { 33 | types: ['class-name', 'function', 'tag'], 34 | style: { 35 | color: 'var(--gray12)', 36 | }, 37 | }, 38 | ], 39 | } 40 | 41 | export function Code({ children }: { children: string }) { 42 | return ( 43 | <Highlight {...defaultProps} theme={theme} code={children} language="jsx"> 44 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 45 | <pre className={`${className} ${styles.root}`} style={style}> 46 | <button 47 | aria-label="Copy Code" 48 | onClick={() => { 49 | copy(children) 50 | }} 51 | > 52 | <CopyIcon /> 53 | </button> 54 | <div className={styles.shine} /> 55 | {tokens.map((line, i) => ( 56 | <div key={i} {...getLineProps({ line, key: i })}> 57 | {line.map((token, key) => ( 58 | <span key={i} {...getTokenProps({ token, key })} /> 59 | ))} 60 | </div> 61 | ))} 62 | </pre> 63 | )} 64 | </Highlight> 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /website/components/icons/icons.module.scss: -------------------------------------------------------------------------------- 1 | .blurLogo { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | position: relative; 6 | border-radius: 4px; 7 | overflow: hidden; 8 | box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.015); 9 | 10 | .bg { 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | position: absolute; 15 | z-index: 1; 16 | pointer-events: none; 17 | user-select: none; 18 | top: 0; 19 | left: 0; 20 | width: 100%; 21 | height: 100%; 22 | transform: scale(1.5) translateZ(0); 23 | filter: blur(12px) opacity(0.4) saturate(100%); 24 | transition: filter 150ms ease; 25 | } 26 | 27 | .inner { 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | object-fit: cover; 32 | width: 100%; 33 | height: 100%; 34 | user-select: none; 35 | pointer-events: none; 36 | border-radius: inherit; 37 | z-index: 2; 38 | 39 | svg { 40 | width: 14px; 41 | height: 14px; 42 | filter: drop-shadow(0 4px 4px rgba(0, 0, 0, 0.16)); 43 | transition: filter 150ms ease; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /website/components/icons/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from './icons.module.scss' 2 | 3 | export function FigmaIcon() { 4 | return ( 5 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"> 6 | <path fill="#e64a19" d="M26,17h-8c-3.866,0-7-3.134-7-7v0c0-3.866,3.134-7,7-7h8V17z" /> 7 | <path fill="#7c4dff" d="M25,31h-7c-3.866,0-7-3.134-7-7v0c0-3.866,3.134-7,7-7h7V31z" /> 8 | <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" /> 9 | <path fill="#ff7043" d="M32,17h-7V3h7c3.866,0,7,3.134,7,7v0C39,13.866,35.866,17,32,17z" /> 10 | <circle cx="32" cy="24" r="7" fill="#29b6f6" /> 11 | </svg> 12 | ) 13 | } 14 | 15 | export function RaycastIcon() { 16 | return ( 17 | <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg"> 18 | <path 19 | fillRule="evenodd" 20 | clipRule="evenodd" 21 | 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" 22 | fill="#FF6363" 23 | /> 24 | </svg> 25 | ) 26 | } 27 | 28 | export function YouTubeIcon() { 29 | return ( 30 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"> 31 | <path 32 | fill="#FF3D00" 33 | 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" 34 | /> 35 | <path fill="#FFF" d="M20 31L20 17 32 24z" /> 36 | </svg> 37 | ) 38 | } 39 | 40 | export function SlackIcon() { 41 | return ( 42 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"> 43 | <path 44 | fill="#33d375" 45 | 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" 46 | /> 47 | <path 48 | fill="#33d375" 49 | 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" 50 | /> 51 | <path 52 | fill="#40c4ff" 53 | 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" 54 | /> 55 | <path 56 | fill="#40c4ff" 57 | 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" 58 | /> 59 | <path 60 | fill="#e91e63" 61 | 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" 62 | /> 63 | <path 64 | fill="#e91e63" 65 | 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" 66 | /> 67 | <path 68 | fill="#ffc107" 69 | 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" 70 | /> 71 | <path 72 | fill="#ffc107" 73 | 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" 74 | /> 75 | </svg> 76 | ) 77 | } 78 | 79 | export function VercelIcon() { 80 | return ( 81 | <svg aria-label="Vercel Logo" fill="var(--highContrast)" height="26" viewBox="0 0 75 65"> 82 | <path d="M37.59.25l36.95 64H.64l36.95-64z"></path> 83 | </svg> 84 | ) 85 | } 86 | 87 | export function LinearIcon({ style }: { style?: Object }) { 88 | return ( 89 | <svg width="64" height="64" viewBox="0 0 64 64" fill="none" style={style}> 90 | <path 91 | d="M0.403013 37.3991L26.6009 63.597C13.2225 61.3356 2.66442 50.7775 0.403013 37.3991Z" 92 | fill="#5E6AD2" 93 | ></path> 94 | <path 95 | 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" 96 | fill="#5E6AD2" 97 | ></path> 98 | <path 99 | 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" 100 | fill="#5E6AD2" 101 | ></path> 102 | <path 103 | 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" 104 | fill="#5E6AD2" 105 | ></path> 106 | </svg> 107 | ) 108 | } 109 | 110 | export function Logo({ children, size = '20px' }: { children: React.ReactNode; size?: string }) { 111 | return ( 112 | <div 113 | className={styles.blurLogo} 114 | style={{ 115 | width: size, 116 | height: size, 117 | }} 118 | > 119 | <div className={styles.bg} aria-hidden> 120 | {children} 121 | </div> 122 | <div className={styles.inner}>{children}</div> 123 | </div> 124 | ) 125 | } 126 | 127 | export function CopyIcon() { 128 | return ( 129 | <svg width="16" height="16" strokeWidth="1.5" viewBox="0 0 24 24" fill="none"> 130 | <path 131 | 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" 132 | stroke="currentColor" 133 | strokeLinecap="round" 134 | strokeLinejoin="round" 135 | /> 136 | <path 137 | 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" 138 | stroke="currentColor" 139 | strokeLinecap="round" 140 | strokeLinejoin="round" 141 | /> 142 | </svg> 143 | ) 144 | } 145 | 146 | export function CopiedIcon() { 147 | return ( 148 | <svg width="16" height="16" strokeWidth="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 149 | <path d="M5 13L9 17L19 7" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" /> 150 | </svg> 151 | ) 152 | } 153 | 154 | export function GitHubIcon() { 155 | return ( 156 | <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> 157 | <path 158 | 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" 159 | fill="currentColor" 160 | /> 161 | </svg> 162 | ) 163 | } 164 | 165 | export function FramerIcon() { 166 | return ( 167 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 24"> 168 | <path 169 | 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" 170 | fill="var(--highContrast)" 171 | ></path> 172 | </svg> 173 | ) 174 | } 175 | -------------------------------------------------------------------------------- /website/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cmdk/framer' 2 | export * from './cmdk/linear' 3 | export * from './cmdk/vercel' 4 | export * from './cmdk/raycast' 5 | export * from './icons' 6 | export * from './code' 7 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="next" /> 2 | /// <reference types="next/image-types/global" /> 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmdk-website", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "pnpm -F 'cmdk-website^...' build && next build", 8 | "start": "next start", 9 | "lint": "eslint --ext .tsx" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-popover": "^0.1.6", 13 | "cmdk": "workspace:*", 14 | "copy-to-clipboard": "^3.3.1", 15 | "framer-motion": "^6.5.1", 16 | "next": "13.5.1", 17 | "next-seo": "^5.5.0", 18 | "next-themes": "^0.2.0", 19 | "prism-react-renderer": "^1.3.5", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "sass": "^1.53.0" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "18.0.4", 26 | "@types/react": "18.0.15", 27 | "@types/react-dom": "18.0.6", 28 | "babel-eslint": "^10.1.0", 29 | "eslint": "^8.19.0", 30 | "eslint-config-next": "12.2.2", 31 | "eslint-config-prettier": "^8.5.0", 32 | "eslint-plugin-prettier": "^4.2.1", 33 | "eslint-plugin-react": "^7.30.1", 34 | "husky": "^8.0.1", 35 | "lint-staged": "^13.0.3", 36 | "typescript": "4.7.4" 37 | }, 38 | "lint-staged": { 39 | "*.{tsx},*.{ts},*.{mdx}": [ 40 | "eslint --fix" 41 | ] 42 | }, 43 | "husky": { 44 | "hooks": { 45 | "pre-commit": "lint-staged" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import 'styles/globals.scss' 2 | 3 | import 'styles/cmdk/vercel.scss' 4 | import 'styles/cmdk/linear.scss' 5 | import 'styles/cmdk/raycast.scss' 6 | import 'styles/cmdk/framer.scss' 7 | 8 | import type { AppProps } from 'next/app' 9 | import { ThemeProvider } from 'next-themes' 10 | import { NextSeo } from 'next-seo' 11 | import Head from 'next/head' 12 | 13 | const title = '⌘K' 14 | const description = 'Fast, composable, unstyled command menu for React' 15 | const siteUrl = 'https://cmdk.paco.me' 16 | 17 | export default function App({ Component, pageProps }: AppProps) { 18 | return ( 19 | <> 20 | <Head> 21 | <link rel="shortcut icon" href="/favicon.svg" /> 22 | <meta name="twitter:card" content="summary_large_image" /> 23 | </Head> 24 | <NextSeo 25 | title={`${description} — ${title}`} 26 | description={description} 27 | openGraph={{ 28 | type: 'website', 29 | url: siteUrl, 30 | title, 31 | description: description + '.', 32 | images: [ 33 | { 34 | url: `${siteUrl}/og.png`, 35 | alt: title, 36 | }, 37 | ], 38 | }} 39 | /> 40 | <ThemeProvider disableTransitionOnChange attribute="class"> 41 | <Component {...pageProps} /> 42 | </ThemeProvider> 43 | </> 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-sync-scripts */ 2 | import React from 'react' 3 | import NextDocument, { Html, Head, Main, NextScript } from 'next/document' 4 | 5 | export default class Document extends NextDocument { 6 | render() { 7 | return ( 8 | <Html lang="en"> 9 | <Head> 10 | <link rel="preload" href="/inter-var-latin.woff2" as="font" type="font/woff2" crossOrigin="anonymous" /> 11 | </Head> 12 | <body> 13 | <Main /> 14 | <NextScript /> 15 | </body> 16 | </Html> 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /website/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from 'styles/index.module.scss' 2 | import React from 'react' 3 | import { AnimatePresence, AnimateSharedLayout, motion, MotionProps, useInView } from 'framer-motion' 4 | import { 5 | FramerCMDK, 6 | LinearCMDK, 7 | LinearIcon, 8 | VercelCMDK, 9 | VercelIcon, 10 | RaycastCMDK, 11 | RaycastIcon, 12 | CopyIcon, 13 | FramerIcon, 14 | GitHubIcon, 15 | Code, 16 | CopiedIcon, 17 | } from 'components' 18 | import packageJSON from '../../cmdk/package.json' 19 | 20 | type TTheme = { 21 | theme: Themes 22 | setTheme: Function 23 | } 24 | 25 | type Themes = 'linear' | 'raycast' | 'vercel' | 'framer' 26 | 27 | const ThemeContext = React.createContext<TTheme>({} as TTheme) 28 | 29 | export default function Index() { 30 | const [theme, setTheme] = React.useState<Themes>('raycast') 31 | 32 | return ( 33 | <main className={styles.main}> 34 | <div className={styles.content}> 35 | <div className={styles.meta}> 36 | <div className={styles.info}> 37 | <VersionBadge /> 38 | <h1>⌘K</h1> 39 | <p>Fast, composable, unstyled command menu for React.</p> 40 | </div> 41 | 42 | <div className={styles.buttons}> 43 | <InstallButton /> 44 | <GitHubButton /> 45 | </div> 46 | </div> 47 | 48 | <AnimatePresence exitBeforeEnter initial={false}> 49 | {theme === 'framer' && ( 50 | <CMDKWrapper key="framer"> 51 | <FramerCMDK /> 52 | </CMDKWrapper> 53 | )} 54 | {theme === 'vercel' && ( 55 | <CMDKWrapper key="vercel"> 56 | <VercelCMDK /> 57 | </CMDKWrapper> 58 | )} 59 | {theme === 'linear' && ( 60 | <CMDKWrapper key="linear"> 61 | <LinearCMDK /> 62 | </CMDKWrapper> 63 | )} 64 | {theme === 'raycast' && ( 65 | <CMDKWrapper key="raycast"> 66 | <RaycastCMDK /> 67 | </CMDKWrapper> 68 | )} 69 | </AnimatePresence> 70 | 71 | <ThemeContext.Provider value={{ theme, setTheme }}> 72 | <ThemeSwitcher /> 73 | </ThemeContext.Provider> 74 | 75 | <div aria-hidden className={styles.line} /> 76 | 77 | <Codeblock /> 78 | </div> 79 | <Footer /> 80 | </main> 81 | ) 82 | } 83 | 84 | function CMDKWrapper(props: MotionProps & { children: React.ReactNode }) { 85 | return ( 86 | <motion.div 87 | initial={{ opacity: 0, scale: 0.98 }} 88 | animate={{ opacity: 1, scale: 1 }} 89 | exit={{ opacity: 0, scale: 0.98 }} 90 | transition={{ duration: 0.2 }} 91 | style={{ 92 | height: 475, 93 | }} 94 | {...props} 95 | /> 96 | ) 97 | } 98 | 99 | ////////////////////////////////////////////////////////////////// 100 | 101 | function InstallButton() { 102 | const [copied, setCopied] = React.useState(false) 103 | 104 | return ( 105 | <button 106 | className={styles.installButton} 107 | onClick={async () => { 108 | try { 109 | await navigator.clipboard.writeText(`npm install cmdk`) 110 | setCopied(true) 111 | setTimeout(() => { 112 | setCopied(false) 113 | }, 2000) 114 | } catch (e) {} 115 | }} 116 | > 117 | npm install cmdk 118 | <span>{copied ? <CopiedIcon /> : <CopyIcon />}</span> 119 | </button> 120 | ) 121 | } 122 | 123 | function GitHubButton() { 124 | return ( 125 | <a 126 | href="https://github.com/pacocoursey/cmdk" 127 | target="_blank" 128 | rel="noopener noreferrer" 129 | className={styles.githubButton} 130 | > 131 | <GitHubIcon /> 132 | pacocoursey/cmdk 133 | </a> 134 | ) 135 | } 136 | 137 | ////////////////////////////////////////////////////////////////// 138 | 139 | const themes = [ 140 | { 141 | icon: <RaycastIcon />, 142 | key: 'raycast', 143 | }, 144 | { 145 | icon: <LinearIcon />, 146 | key: 'linear', 147 | }, 148 | { 149 | icon: <VercelIcon />, 150 | key: 'vercel', 151 | }, 152 | { 153 | icon: <FramerIcon />, 154 | key: 'framer', 155 | }, 156 | ] 157 | 158 | function ThemeSwitcher() { 159 | const { theme, setTheme } = React.useContext(ThemeContext) 160 | const ref = React.useRef<HTMLButtonElement | null>(null) 161 | const [showArrowKeyHint, setShowArrowKeyHint] = React.useState(false) 162 | 163 | React.useEffect(() => { 164 | function listener(e: KeyboardEvent) { 165 | const themeNames = themes.map((t) => t.key) 166 | 167 | if (e.key === 'ArrowRight') { 168 | const currentIndex = themeNames.indexOf(theme) 169 | const nextIndex = currentIndex + 1 170 | const nextItem = themeNames[nextIndex] 171 | 172 | if (nextItem) { 173 | setTheme(nextItem) 174 | } 175 | } 176 | 177 | if (e.key === 'ArrowLeft') { 178 | const currentIndex = themeNames.indexOf(theme) 179 | const prevIndex = currentIndex - 1 180 | const prevItem = themeNames[prevIndex] 181 | 182 | if (prevItem) { 183 | setTheme(prevItem) 184 | } 185 | } 186 | } 187 | 188 | document.addEventListener('keydown', listener) 189 | 190 | return () => { 191 | document.removeEventListener('keydown', listener) 192 | } 193 | // eslint-disable-next-line react-hooks/exhaustive-deps 194 | }, [theme]) 195 | 196 | return ( 197 | <div className={styles.switcher}> 198 | <motion.span 199 | className={styles.arrow} 200 | initial={false} 201 | animate={{ 202 | opacity: showArrowKeyHint ? 1 : 0, 203 | x: showArrowKeyHint ? -24 : 0, 204 | }} 205 | style={{ 206 | left: 100, 207 | }} 208 | > 209 | ← 210 | </motion.span> 211 | <AnimateSharedLayout> 212 | {themes.map(({ key, icon }) => { 213 | const isActive = theme === key 214 | return ( 215 | <button 216 | ref={ref} 217 | key={key} 218 | data-selected={isActive} 219 | onClick={() => { 220 | setTheme(key) 221 | if (showArrowKeyHint === false) { 222 | setShowArrowKeyHint(true) 223 | } 224 | }} 225 | > 226 | {icon} 227 | {key} 228 | {isActive && ( 229 | <motion.div 230 | layoutId="activeTheme" 231 | transition={{ 232 | type: 'spring', 233 | stiffness: 250, 234 | damping: 27, 235 | mass: 1, 236 | }} 237 | className={styles.activeTheme} 238 | /> 239 | )} 240 | </button> 241 | ) 242 | })} 243 | </AnimateSharedLayout> 244 | <motion.span 245 | className={styles.arrow} 246 | initial={false} 247 | animate={{ 248 | opacity: showArrowKeyHint ? 1 : 0, 249 | x: showArrowKeyHint ? 20 : 0, 250 | }} 251 | style={{ 252 | right: 100, 253 | }} 254 | > 255 | → 256 | </motion.span> 257 | </div> 258 | ) 259 | } 260 | ////////////////////////////////////////////////////////////////// 261 | 262 | function Codeblock() { 263 | const code = `import { Command } from 'cmdk'; 264 | 265 | <Command.Dialog open={open} onOpenChange={setOpen}> 266 | <Command.Input /> 267 | 268 | <Command.List> 269 | {loading && <Command.Loading>Hang on…</Command.Loading>} 270 | 271 | <Command.Empty>No results found.</Command.Empty> 272 | 273 | <Command.Group heading="Fruits"> 274 | <Command.Item>Apple</Command.Item> 275 | <Command.Item>Orange</Command.Item> 276 | <Command.Separator /> 277 | <Command.Item>Pear</Command.Item> 278 | <Command.Item>Blueberry</Command.Item> 279 | </Command.Group> 280 | 281 | <Command.Item>Fish</Command.Item> 282 | </Command.List> 283 | </Command.Dialog>` 284 | 285 | return ( 286 | <div className={styles.codeBlock}> 287 | <div className={styles.line2} aria-hidden /> 288 | <div className={styles.line3} aria-hidden /> 289 | <Code>{code}</Code> 290 | </div> 291 | ) 292 | } 293 | 294 | ////////////////////////////////////////////////////////////////// 295 | 296 | function VersionBadge() { 297 | return <span className={styles.versionBadge}>v{packageJSON.version}</span> 298 | } 299 | 300 | function Footer() { 301 | const ref = React.useRef<HTMLElement | null>(null) 302 | const isInView = useInView(ref, { 303 | once: true, 304 | margin: '100px', 305 | }) 306 | return ( 307 | <footer ref={ref} className={styles.footer} data-animate={isInView}> 308 | <div className={styles.footerText}> 309 | Crafted by{' '} 310 | <a href="https://paco.me" target="_blank" rel="noopener noreferrer"> 311 | <img src="/paco.png" alt="Avatar of Paco" /> 312 | Paco 313 | </a>{' '} 314 | and{' '} 315 | <a href="https://rauno.me" target="_blank" rel="noopener noreferrer"> 316 | <img src="/rauno.jpeg" alt="Avatar of Rauno" /> 317 | Rauno 318 | </a> 319 | </div> 320 | <RaunoSignature /> 321 | <PacoSignature /> 322 | </footer> 323 | ) 324 | } 325 | 326 | function RaunoSignature() { 327 | return ( 328 | <motion.svg 329 | initial={{ opacity: 1 }} 330 | whileInView={{ opacity: 0 }} 331 | transition={{ delay: 2.5 }} 332 | viewport={{ once: true }} 333 | className={styles.raunoSignature} 334 | width="356" 335 | height="118" 336 | viewBox="0 0 356 118" 337 | fill="none" 338 | > 339 | <path 340 | 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" 341 | stroke="currentColor" 342 | strokeWidth="3" 343 | strokeLinecap="round" 344 | pathLength={1} 345 | /> 346 | </motion.svg> 347 | ) 348 | } 349 | 350 | function PacoSignature() { 351 | return ( 352 | <motion.svg 353 | className={styles.pacoSignature} 354 | width="892" 355 | height="235" 356 | viewBox="0 0 892 235" 357 | fill="none" 358 | initial={{ opacity: 1 }} 359 | whileInView={{ opacity: 0 }} 360 | transition={{ delay: 2.5 }} 361 | viewport={{ once: true }} 362 | > 363 | <path 364 | 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" 365 | stroke="currentColor" 366 | strokeWidth="9" 367 | pathLength={1} 368 | /> 369 | <path 370 | 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" 371 | stroke="currentColor" 372 | strokeWidth="9" 373 | pathLength={1} 374 | /> 375 | <path 376 | 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" 377 | stroke="currentColor" 378 | strokeWidth="9" 379 | strokeLinecap="round" 380 | pathLength={1} 381 | /> 382 | </motion.svg> 383 | ) 384 | } 385 | -------------------------------------------------------------------------------- /website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect width="100" height="100" rx="16" fill="black"/> 3 | <path d="M35.7346 74.8571C33.9551 74.8571 32.3252 74.4158 30.8449 73.533C29.3645 72.635 28.1832 71.4326 27.3009 69.9258C26.4336 68.419 26 66.7599 26 64.9487C26 63.1223 26.4336 61.4557 27.3009 59.9489C28.1832 58.4421 29.3645 57.2397 30.8449 56.3417C32.3252 55.4437 33.9551 54.9947 35.7346 54.9947H40.9383V45.8168H35.7346C33.9551 45.8168 32.3252 45.3754 30.8449 44.4927C29.3645 43.5947 28.1832 42.3999 27.3009 40.9083C26.4336 39.4015 26 37.7348 26 35.9084C26 34.082 26.4336 32.423 27.3009 30.9314C28.1832 29.4246 29.3645 28.2298 30.8449 27.347C32.3252 26.449 33.9551 26 35.7346 26C37.529 26 39.1664 26.449 40.6467 27.347C42.1271 28.2298 43.3084 29.4246 44.1907 30.9314C45.0729 32.423 45.514 34.082 45.514 35.9084V41.1594H54.5308V35.9084C54.5308 34.082 54.9645 32.423 55.8318 30.9314C56.714 29.4246 57.8879 28.2298 59.3533 27.347C60.8336 26.449 62.471 26 64.2654 26C66.0598 26 67.6897 26.449 69.1551 27.347C70.6355 28.2298 71.8093 29.4246 72.6766 30.9314C73.5589 32.423 74 34.082 74 35.9084C74 37.7348 73.5589 39.4015 72.6766 40.9083C71.8093 42.3999 70.6355 43.5947 69.1551 44.4927C67.6897 45.3754 66.0598 45.8168 64.2654 45.8168H59.1065V54.9947H64.2654C66.0598 54.9947 67.6897 55.4437 69.1551 56.3417C70.6355 57.2397 71.8093 58.4421 72.6766 59.9489C73.5589 61.4557 74 63.1223 74 64.9487C74 66.7599 73.5589 68.419 72.6766 69.9258C71.8093 71.4326 70.6355 72.635 69.1551 73.533C67.6897 74.4158 66.0598 74.8571 64.2654 74.8571C62.471 74.8571 60.8336 74.4158 59.3533 73.533C57.8879 72.635 56.714 71.4326 55.8318 69.9258C54.9645 68.419 54.5308 66.7599 54.5308 64.9487V59.6521H45.514V64.9487C45.514 66.7599 45.0729 68.419 44.1907 69.9258C43.3084 71.4326 42.1271 72.635 40.6467 73.533C39.1664 74.4158 37.529 74.8571 35.7346 74.8571ZM35.7346 70.1997C36.6916 70.1997 37.5589 69.9638 38.3364 69.492C39.129 69.0202 39.757 68.3885 40.2206 67.5971C40.6991 66.7904 40.9383 65.9076 40.9383 64.9487V59.6521H35.7346C34.7925 59.6521 33.9252 59.8956 33.1327 60.3826C32.3551 60.8545 31.7346 61.4937 31.271 62.3004C30.8075 63.0919 30.5757 63.9746 30.5757 64.9487C30.5757 65.9076 30.8075 66.7904 31.271 67.5971C31.7346 68.3885 32.3551 69.0202 33.1327 69.492C33.9252 69.9638 34.7925 70.1997 35.7346 70.1997ZM35.7346 41.1594H40.9383V35.9084C40.9383 34.9343 40.6991 34.0515 40.2206 33.2601C39.757 32.4686 39.129 31.837 38.3364 31.3652C37.5589 30.8933 36.6916 30.6574 35.7346 30.6574C34.7925 30.6574 33.9252 30.8933 33.1327 31.3652C32.3551 31.837 31.7346 32.4686 31.271 33.2601C30.8075 34.0515 30.5757 34.9343 30.5757 35.9084C30.5757 36.8825 30.8075 37.7729 31.271 38.5796C31.7346 39.371 32.3551 40.0027 33.1327 40.4745C33.9252 40.9311 34.7925 41.1594 35.7346 41.1594ZM59.1065 41.1594H64.2654C65.2224 41.1594 66.0897 40.9311 66.8673 40.4745C67.6449 40.0027 68.2654 39.371 68.729 38.5796C69.1925 37.7729 69.4243 36.8825 69.4243 35.9084C69.4243 34.9343 69.1925 34.0515 68.729 33.2601C68.2654 32.4686 67.6449 31.837 66.8673 31.3652C66.0897 30.8933 65.2224 30.6574 64.2654 30.6574C63.3084 30.6574 62.4336 30.8933 61.6411 31.3652C60.8636 31.837 60.243 32.4686 59.7794 33.2601C59.3308 34.0515 59.1065 34.9343 59.1065 35.9084V41.1594ZM64.2654 70.1997C65.2224 70.1997 66.0897 69.9638 66.8673 69.492C67.6449 69.0202 68.2654 68.3885 68.729 67.5971C69.1925 66.7904 69.4243 65.9076 69.4243 64.9487C69.4243 63.9746 69.1925 63.0919 68.729 62.3004C68.2654 61.4937 67.6449 60.8545 66.8673 60.3826C66.0897 59.8956 65.2224 59.6521 64.2654 59.6521H59.1065V64.9487C59.1065 65.9076 59.3308 66.7904 59.7794 67.5971C60.243 68.3885 60.8636 69.0202 61.6411 69.492C62.4336 69.9638 63.3084 70.1997 64.2654 70.1997ZM45.514 54.9947H54.5308V45.8168H45.514V54.9947Z" fill="white"/> 4 | </svg> 5 | -------------------------------------------------------------------------------- /website/public/grid.svg: -------------------------------------------------------------------------------- 1 | 2 | <svg id="download" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"> 3 | <path id="Path_3" data-name="Path 3" d="M96,95h4v1H96v4H95V96H86v4H85V96H76v4H75V96H66v4H65V96H56v4H55V96H46v4H45V96H36v4H35V96H26v4H25V96H16v4H15V96H0V95H15V86H0V85H15V76H0V75H15V66H0V65H15V56H0V55H15V46H0V45H15V36H0V35H15V26H0V25H15V16H0V15H15V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h9V0h1V15h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96v9h4v1H96Zm-1,0V86H86v9ZM85,95V86H76v9ZM75,95V86H66v9ZM65,95V86H56v9ZM55,95V86H46v9ZM45,95V86H36v9ZM35,95V86H26v9ZM25,95V86H16v9ZM16,85h9V76H16Zm10,0h9V76H26Zm10,0h9V76H36Zm10,0h9V76H46Zm10,0h9V76H56Zm10,0h9V76H66Zm10,0h9V76H76Zm10,0h9V76H86Zm9-10V66H86v9ZM85,75V66H76v9ZM75,75V66H66v9ZM65,75V66H56v9ZM55,75V66H46v9ZM45,75V66H36v9ZM35,75V66H26v9ZM25,75V66H16v9ZM16,65h9V56H16Zm10,0h9V56H26Zm10,0h9V56H36Zm10,0h9V56H46Zm10,0h9V56H56Zm10,0h9V56H66Zm10,0h9V56H76Zm10,0h9V56H86Zm9-10V46H86v9ZM85,55V46H76v9ZM75,55V46H66v9ZM65,55V46H56v9ZM55,55V46H46v9ZM45,55V46H36v9ZM35,55V46H26v9ZM25,55V46H16v9ZM16,45h9V36H16Zm10,0h9V36H26Zm10,0h9V36H36Zm10,0h9V36H46Zm10,0h9V36H56Zm10,0h9V36H66Zm10,0h9V36H76Zm10,0h9V36H86Zm9-10V26H86v9ZM85,35V26H76v9ZM75,35V26H66v9ZM65,35V26H56v9ZM55,35V26H46v9ZM45,35V26H36v9ZM35,35V26H26v9ZM25,35V26H16v9ZM16,25h9V16H16Zm10,0h9V16H26Zm10,0h9V16H36Zm10,0h9V16H46Zm10,0h9V16H56Zm10,0h9V16H66Zm10,0h9V16H76Zm10,0h9V16H86Z" fill="rgba(255,255,255,0.1)" fill-rule="evenodd" opacity="0.5"/> 4 | <path id="Path_4" data-name="Path 4" d="M6,5V0H5V5H0V6H5v94H6V6h94V5Z" fill="rgba(255,255,255,0.1)" fill-rule="evenodd"/> 5 | </svg> 6 | -------------------------------------------------------------------------------- /website/public/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacocoursey/cmdk/fb4ea04e9ec211777fbb39c6104e3c5f2ee107d2/website/public/inter-var-latin.woff2 -------------------------------------------------------------------------------- /website/public/line.svg: -------------------------------------------------------------------------------- 1 | <svg width="80" height="16" viewBox="0 0 80 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path d="M74.5815 11.4427C72.258 11.4427 69.4013 8.42846 69.2798 8.30105C67.9756 6.78413 66.6561 5.43495 64.7154 5.43495C62.588 5.43495 60.0887 8.26052 60.0626 8.28866L59.8463 8.52446C58.4684 10.0248 57.1669 11.4426 54.8493 11.4426C52.5258 11.4426 49.6691 8.42835 49.5476 8.30094C48.2434 6.78402 46.9239 5.43484 44.9832 5.43484C42.8558 5.43484 40.3565 8.26039 40.3304 8.28853L40.1265 8.50339C38.553 10.1455 37.3111 11.4425 35.1178 11.4425C32.7935 11.4425 29.9363 8.4282 29.8161 8.30079C28.5111 6.78387 27.1909 5.43469 25.2517 5.43469C23.1243 5.43469 20.625 8.26024 20.5989 8.28839L20.3804 8.52639C19.0037 10.026 17.7017 11.4416 15.3846 11.4416C13.0604 11.4416 10.2032 8.42731 10.083 8.2999C8.77799 6.78298 7.45778 5.4338 5.51852 5.4338C3.58704 5.4338 1.82704 6.495 0.923702 8.20361C0.811578 8.41772 0.545371 8.49948 0.330537 8.38663C0.116426 8.27233 0.0346651 8.00757 0.148243 7.79346C1.20437 5.79772 3.26158 4.55643 5.51787 4.55643C7.82769 4.55643 9.36121 6.11604 10.7342 7.71253C11.4706 8.49596 13.7695 10.5635 15.384 10.5635C17.3162 10.5635 18.3823 9.40244 19.7328 7.93253L19.9448 7.70177C20.0497 7.5817 22.7284 4.55659 25.25 4.55659C27.5598 4.55659 29.0934 6.1162 30.4663 7.7127C31.2026 8.49613 33.5017 10.5636 35.1161 10.5636C36.9347 10.5636 37.9524 9.50242 39.4919 7.89585L39.6843 7.69476C39.7834 7.58046 42.4621 4.55606 44.983 4.55606C47.2941 4.55606 48.8278 6.11567 50.1993 7.71216C50.9358 8.49414 53.2346 10.5623 54.8491 10.5623C56.7821 10.5623 57.8496 9.40061 59.2009 7.92846L59.4115 7.69987C59.5163 7.57979 62.195 4.55469 64.7154 4.55469C67.0265 4.55469 68.5602 6.1143 69.9317 7.71079C70.668 8.49277 72.9671 10.561 74.5815 10.561C76.4341 10.561 78.1593 9.56272 79.0859 7.95616C79.2067 7.74566 79.4743 7.67261 79.6856 7.79485C79.8954 7.91566 79.9671 8.18403 79.8469 8.39381C78.7632 10.276 76.7458 11.4429 74.5813 11.4429L74.5815 11.4427Z" fill="hsl(0, 0%, 31.2%)"/> 3 | </svg> 4 | -------------------------------------------------------------------------------- /website/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacocoursey/cmdk/fb4ea04e9ec211777fbb39c6104e3c5f2ee107d2/website/public/og.png -------------------------------------------------------------------------------- /website/public/paco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacocoursey/cmdk/fb4ea04e9ec211777fbb39c6104e3c5f2ee107d2/website/public/paco.png -------------------------------------------------------------------------------- /website/public/rauno.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacocoursey/cmdk/fb4ea04e9ec211777fbb39c6104e3c5f2ee107d2/website/public/rauno.jpeg -------------------------------------------------------------------------------- /website/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | <svg width="283" height="64" viewBox="0 0 283 64" fill="none" 2 | xmlns="http://www.w3.org/2000/svg"> 3 | <path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/> 4 | </svg> -------------------------------------------------------------------------------- /website/styles/cmdk/framer.scss: -------------------------------------------------------------------------------- 1 | .framer { 2 | [cmdk-root] { 3 | max-width: 640px; 4 | width: 100%; 5 | padding: 8px; 6 | background: #ffffff; 7 | border-radius: 16px; 8 | overflow: hidden; 9 | font-family: var(--font-sans); 10 | border: 1px solid var(--gray6); 11 | box-shadow: var(--cmdk-shadow); 12 | outline: none; 13 | 14 | .dark & { 15 | background: var(--gray2); 16 | } 17 | } 18 | 19 | [cmdk-framer-header] { 20 | display: flex; 21 | align-items: center; 22 | gap: 8px; 23 | height: 48px; 24 | padding: 0 8px; 25 | border-bottom: 1px solid var(--gray5); 26 | margin-bottom: 12px; 27 | padding-bottom: 8px; 28 | 29 | svg { 30 | width: 20px; 31 | height: 20px; 32 | color: var(--gray9); 33 | transform: translateY(1px); 34 | } 35 | } 36 | 37 | [cmdk-input] { 38 | font-family: var(--font-sans); 39 | border: none; 40 | width: 100%; 41 | font-size: 16px; 42 | outline: none; 43 | background: var(--bg); 44 | color: var(--gray12); 45 | 46 | &::placeholder { 47 | color: var(--gray9); 48 | } 49 | } 50 | 51 | [cmdk-item] { 52 | content-visibility: auto; 53 | 54 | cursor: pointer; 55 | border-radius: 12px; 56 | font-size: 14px; 57 | display: flex; 58 | align-items: center; 59 | gap: 12px; 60 | color: var(--gray12); 61 | padding: 8px 8px; 62 | margin-right: 8px; 63 | font-weight: 500; 64 | transition: all 150ms ease; 65 | transition-property: none; 66 | 67 | &[data-selected='true'] { 68 | background: var(--blue9); 69 | color: #ffffff; 70 | 71 | [cmdk-framer-item-subtitle] { 72 | color: #ffffff; 73 | } 74 | } 75 | 76 | &[data-disabled='true'] { 77 | color: var(--gray8); 78 | cursor: not-allowed; 79 | } 80 | 81 | & + [cmdk-item] { 82 | margin-top: 4px; 83 | } 84 | 85 | svg { 86 | width: 16px; 87 | height: 16px; 88 | color: #ffffff; 89 | } 90 | } 91 | 92 | [cmdk-framer-icon-wrapper] { 93 | display: flex; 94 | align-items: center; 95 | justify-content: center; 96 | min-width: 32px; 97 | height: 32px; 98 | background: orange; 99 | border-radius: 8px; 100 | } 101 | 102 | [cmdk-framer-item-meta] { 103 | display: flex; 104 | flex-direction: column; 105 | gap: 4px; 106 | } 107 | 108 | [cmdk-framer-item-subtitle] { 109 | font-size: 12px; 110 | font-weight: 400; 111 | color: var(--gray11); 112 | } 113 | 114 | [cmdk-framer-items] { 115 | min-height: 308px; 116 | display: flex; 117 | } 118 | 119 | [cmdk-framer-left] { 120 | width: 40%; 121 | } 122 | 123 | [cmdk-framer-separator] { 124 | width: 1px; 125 | border: 0; 126 | margin-right: 8px; 127 | background: var(--gray6); 128 | } 129 | 130 | [cmdk-framer-right] { 131 | display: flex; 132 | align-items: center; 133 | justify-content: center; 134 | border-radius: 8px; 135 | margin-left: 8px; 136 | width: 60%; 137 | 138 | button { 139 | width: 120px; 140 | height: 40px; 141 | background: var(--blue9); 142 | border-radius: 6px; 143 | font-weight: 500; 144 | color: white; 145 | font-size: 14px; 146 | } 147 | 148 | input[type='text'] { 149 | height: 40px; 150 | width: 160px; 151 | border: 1px solid var(--gray6); 152 | background: #ffffff; 153 | border-radius: 6px; 154 | padding: 0 8px; 155 | font-size: 14px; 156 | font-family: var(--font-sans); 157 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.08); 158 | 159 | &::placeholder { 160 | color: var(--gray9); 161 | } 162 | 163 | @media (prefers-color-scheme: dark) { 164 | background: var(--gray3); 165 | } 166 | } 167 | 168 | [cmdk-framer-radio] { 169 | display: flex; 170 | align-items: center; 171 | gap: 4px; 172 | color: var(--gray12); 173 | font-weight: 500; 174 | font-size: 14px; 175 | accent-color: var(--blue9); 176 | 177 | input { 178 | width: 20px; 179 | height: 20px; 180 | } 181 | } 182 | 183 | img { 184 | width: 40px; 185 | height: 40px; 186 | border-radius: 9999px; 187 | border: 1px solid var(--gray6); 188 | } 189 | 190 | [cmdk-framer-container] { 191 | width: 100px; 192 | height: 100px; 193 | background: var(--blue9); 194 | border-radius: 16px; 195 | } 196 | 197 | [cmdk-framer-badge] { 198 | background: var(--blue3); 199 | padding: 0 8px; 200 | height: 28px; 201 | font-size: 14px; 202 | line-height: 28px; 203 | color: var(--blue11); 204 | border-radius: 9999px; 205 | font-weight: 500; 206 | } 207 | 208 | [cmdk-framer-slider] { 209 | height: 20px; 210 | width: 200px; 211 | background: linear-gradient(90deg, var(--blue9) 40%, var(--gray3) 0%); 212 | border-radius: 9999px; 213 | 214 | div { 215 | width: 20px; 216 | height: 20px; 217 | background: #ffffff; 218 | border-radius: 9999px; 219 | box-shadow: 0 1px 3px -1px rgba(0, 0, 0, 0.32); 220 | transform: translateX(70px); 221 | } 222 | } 223 | } 224 | 225 | [cmdk-list] { 226 | overflow: auto; 227 | } 228 | 229 | [cmdk-separator] { 230 | height: 1px; 231 | width: 100%; 232 | background: var(--gray5); 233 | margin: 4px 0; 234 | } 235 | 236 | [cmdk-group-heading] { 237 | user-select: none; 238 | font-size: 12px; 239 | color: var(--gray11); 240 | padding: 0 8px; 241 | display: flex; 242 | align-items: center; 243 | margin-bottom: 8px; 244 | } 245 | 246 | [cmdk-empty] { 247 | font-size: 14px; 248 | padding: 32px; 249 | white-space: pre-wrap; 250 | color: var(--gray11); 251 | } 252 | } 253 | 254 | @media (max-width: 640px) { 255 | .framer { 256 | [cmdk-framer-icon-wrapper] { 257 | } 258 | 259 | [cmdk-framer-item-subtitle] { 260 | display: none; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /website/styles/cmdk/linear.scss: -------------------------------------------------------------------------------- 1 | .linear { 2 | [cmdk-root] { 3 | max-width: 640px; 4 | width: 100%; 5 | background: #ffffff; 6 | border-radius: 8px; 7 | overflow: hidden; 8 | padding: 0; 9 | font-family: var(--font-sans); 10 | box-shadow: var(--cmdk-shadow); 11 | outline: none; 12 | 13 | .dark & { 14 | background: linear-gradient(136.61deg, rgb(39, 40, 43) 13.72%, rgb(45, 46, 49) 74.3%); 15 | } 16 | } 17 | 18 | [cmdk-linear-badge] { 19 | height: 24px; 20 | padding: 0 8px; 21 | font-size: 12px; 22 | color: var(--gray11); 23 | background: var(--gray3); 24 | border-radius: 4px; 25 | width: fit-content; 26 | display: flex; 27 | align-items: center; 28 | margin: 16px 16px 0; 29 | } 30 | 31 | [cmdk-linear-shortcuts] { 32 | display: flex; 33 | margin-left: auto; 34 | gap: 8px; 35 | 36 | kbd { 37 | font-family: var(--font-sans); 38 | font-size: 13px; 39 | color: var(--gray11); 40 | } 41 | } 42 | 43 | [cmdk-input] { 44 | font-family: var(--font-sans); 45 | border: none; 46 | width: 100%; 47 | font-size: 18px; 48 | padding: 20px; 49 | outline: none; 50 | background: var(--bg); 51 | color: var(--gray12); 52 | border-bottom: 1px solid var(--gray6); 53 | border-radius: 0; 54 | caret-color: #6e5ed2; 55 | margin: 0; 56 | 57 | &::placeholder { 58 | color: var(--gray9); 59 | } 60 | } 61 | 62 | [cmdk-item] { 63 | content-visibility: auto; 64 | 65 | cursor: pointer; 66 | height: 48px; 67 | font-size: 14px; 68 | display: flex; 69 | align-items: center; 70 | gap: 12px; 71 | padding: 0 16px; 72 | color: var(--gray12); 73 | user-select: none; 74 | will-change: background, color; 75 | transition: all 150ms ease; 76 | transition-property: none; 77 | position: relative; 78 | 79 | &[data-selected='true'] { 80 | background: var(--gray3); 81 | 82 | svg { 83 | color: var(--gray12); 84 | } 85 | 86 | &:after { 87 | content: ''; 88 | position: absolute; 89 | left: 0; 90 | z-index: 123; 91 | width: 3px; 92 | height: 100%; 93 | background: #5f6ad2; 94 | } 95 | } 96 | 97 | &[data-disabled='true'] { 98 | color: var(--gray8); 99 | cursor: not-allowed; 100 | } 101 | 102 | &:active { 103 | transition-property: background; 104 | background: var(--gray4); 105 | } 106 | 107 | & + [cmdk-item] { 108 | margin-top: 4px; 109 | } 110 | 111 | svg { 112 | width: 16px; 113 | height: 16px; 114 | color: var(--gray10); 115 | } 116 | } 117 | 118 | [cmdk-list] { 119 | height: min(300px, var(--cmdk-list-height)); 120 | max-height: 400px; 121 | overflow: auto; 122 | overscroll-behavior: contain; 123 | transition: 100ms ease; 124 | transition-property: height; 125 | } 126 | 127 | [cmdk-group-heading] { 128 | user-select: none; 129 | font-size: 12px; 130 | color: var(--gray11); 131 | padding: 0 8px; 132 | display: flex; 133 | align-items: center; 134 | } 135 | 136 | [cmdk-empty] { 137 | font-size: 14px; 138 | display: flex; 139 | align-items: center; 140 | justify-content: center; 141 | height: 64px; 142 | white-space: pre-wrap; 143 | color: var(--gray11); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /website/styles/cmdk/raycast.scss: -------------------------------------------------------------------------------- 1 | .raycast { 2 | [cmdk-root] { 3 | max-width: 640px; 4 | width: 100%; 5 | background: var(--gray1); 6 | border-radius: 12px; 7 | padding: 8px 0; 8 | font-family: var(--font-sans); 9 | box-shadow: var(--cmdk-shadow); 10 | border: 1px solid var(--gray6); 11 | position: relative; 12 | outline: none; 13 | 14 | .dark & { 15 | background: var(--gray2); 16 | border: 0; 17 | 18 | &:after { 19 | content: ''; 20 | background: linear-gradient( 21 | to right, 22 | var(--gray6) 20%, 23 | var(--gray6) 40%, 24 | var(--gray10) 50%, 25 | var(--gray10) 55%, 26 | var(--gray6) 70%, 27 | var(--gray6) 100% 28 | ); 29 | z-index: -1; 30 | position: absolute; 31 | border-radius: 12px; 32 | top: -1px; 33 | left: -1px; 34 | width: calc(100% + 2px); 35 | height: calc(100% + 2px); 36 | animation: shine 3s ease forwards 0.1s; 37 | background-size: 200% auto; 38 | } 39 | 40 | &:before { 41 | content: ''; 42 | z-index: -1; 43 | position: absolute; 44 | border-radius: 12px; 45 | top: -1px; 46 | left: -1px; 47 | width: calc(100% + 2px); 48 | height: calc(100% + 2px); 49 | box-shadow: 0 0 0 1px transparent; 50 | animation: border 1s linear forwards 0.5s; 51 | } 52 | } 53 | 54 | kbd { 55 | font-family: var(--font-sans); 56 | background: var(--gray3); 57 | color: var(--gray11); 58 | height: 20px; 59 | width: 20px; 60 | border-radius: 4px; 61 | padding: 0 4px; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | 66 | &:first-of-type { 67 | margin-left: 8px; 68 | } 69 | } 70 | } 71 | 72 | [cmdk-input] { 73 | font-family: var(--font-sans); 74 | border: none; 75 | width: 100%; 76 | font-size: 15px; 77 | padding: 8px 16px; 78 | outline: none; 79 | background: var(--bg); 80 | color: var(--gray12); 81 | 82 | &::placeholder { 83 | color: var(--gray9); 84 | } 85 | } 86 | 87 | [cmdk-raycast-top-shine] { 88 | .dark & { 89 | background: linear-gradient( 90 | 90deg, 91 | rgba(56, 189, 248, 0), 92 | var(--gray5) 20%, 93 | var(--gray9) 67.19%, 94 | rgba(236, 72, 153, 0) 95 | ); 96 | height: 1px; 97 | position: absolute; 98 | top: -1px; 99 | width: 100%; 100 | z-index: -1; 101 | opacity: 0; 102 | animation: showTopShine 0.1s ease forwards 0.2s; 103 | } 104 | } 105 | 106 | [cmdk-raycast-loader] { 107 | --loader-color: var(--gray9); 108 | border: 0; 109 | width: 100%; 110 | width: 100%; 111 | left: 0; 112 | height: 1px; 113 | background: var(--gray6); 114 | position: relative; 115 | overflow: visible; 116 | display: block; 117 | margin-top: 12px; 118 | margin-bottom: 12px; 119 | 120 | &:after { 121 | content: ''; 122 | width: 50%; 123 | height: 1px; 124 | position: absolute; 125 | background: linear-gradient(90deg, transparent 0%, var(--loader-color) 50%, transparent 100%); 126 | top: -1px; 127 | opacity: 0; 128 | animation-duration: 1.5s; 129 | animation-delay: 1s; 130 | animation-timing-function: ease; 131 | animation-name: loading; 132 | } 133 | } 134 | 135 | [cmdk-item] { 136 | content-visibility: auto; 137 | 138 | cursor: pointer; 139 | height: 40px; 140 | border-radius: 8px; 141 | font-size: 14px; 142 | display: flex; 143 | align-items: center; 144 | gap: 8px; 145 | padding: 0 8px; 146 | color: var(--gray12); 147 | user-select: none; 148 | will-change: background, color; 149 | transition: all 150ms ease; 150 | transition-property: none; 151 | 152 | &[data-selected='true'] { 153 | background: var(--gray4); 154 | color: var(--gray12); 155 | } 156 | 157 | &[data-disabled='true'] { 158 | color: var(--gray8); 159 | cursor: not-allowed; 160 | } 161 | 162 | &:active { 163 | transition-property: background; 164 | background: var(--gray4); 165 | } 166 | 167 | &:first-child { 168 | margin-top: 8px; 169 | } 170 | 171 | & + [cmdk-item] { 172 | margin-top: 4px; 173 | } 174 | 175 | svg { 176 | width: 18px; 177 | height: 18px; 178 | } 179 | } 180 | 181 | [cmdk-raycast-meta] { 182 | margin-left: auto; 183 | color: var(--gray11); 184 | font-size: 13px; 185 | } 186 | 187 | [cmdk-list] { 188 | padding: 0 8px; 189 | height: 393px; 190 | overflow: auto; 191 | overscroll-behavior: contain; 192 | scroll-padding-block-end: 40px; 193 | transition: 100ms ease; 194 | transition-property: height; 195 | padding-bottom: 40px; 196 | } 197 | 198 | [cmdk-raycast-open-trigger], 199 | [cmdk-raycast-subcommand-trigger] { 200 | color: var(--gray11); 201 | padding: 0px 4px 0px 8px; 202 | border-radius: 6px; 203 | font-weight: 500; 204 | font-size: 12px; 205 | height: 28px; 206 | letter-spacing: -0.25px; 207 | } 208 | 209 | [cmdk-raycast-clipboard-icon], 210 | [cmdk-raycast-hammer-icon] { 211 | width: 20px; 212 | height: 20px; 213 | border-radius: 6px; 214 | display: flex; 215 | align-items: center; 216 | justify-content: center; 217 | color: #ffffff; 218 | 219 | svg { 220 | width: 14px; 221 | height: 14px; 222 | } 223 | } 224 | 225 | [cmdk-raycast-clipboard-icon] { 226 | background: linear-gradient(to bottom, #f55354, #eb4646); 227 | } 228 | 229 | [cmdk-raycast-hammer-icon] { 230 | background: linear-gradient(to bottom, #6cb9a3, #2c6459); 231 | } 232 | 233 | [cmdk-raycast-open-trigger] { 234 | display: flex; 235 | align-items: center; 236 | color: var(--gray12); 237 | } 238 | 239 | [cmdk-raycast-subcommand-trigger] { 240 | display: flex; 241 | align-items: center; 242 | gap: 4px; 243 | right: 8px; 244 | bottom: 8px; 245 | 246 | svg { 247 | width: 14px; 248 | height: 14px; 249 | } 250 | 251 | hr { 252 | height: 100%; 253 | background: var(--gray6); 254 | border: 0; 255 | width: 1px; 256 | } 257 | 258 | &[aria-expanded='true'], 259 | &:hover { 260 | background: var(--gray4); 261 | 262 | kbd { 263 | background: var(--gray7); 264 | } 265 | } 266 | } 267 | 268 | [cmdk-separator] { 269 | height: 1px; 270 | width: 100%; 271 | background: var(--gray5); 272 | margin: 4px 0; 273 | } 274 | 275 | *:not([hidden]) + [cmdk-group] { 276 | margin-top: 8px; 277 | } 278 | 279 | [cmdk-group-heading] { 280 | user-select: none; 281 | font-size: 12px; 282 | color: var(--gray11); 283 | padding: 0 8px; 284 | display: flex; 285 | align-items: center; 286 | } 287 | 288 | [cmdk-raycast-footer] { 289 | display: flex; 290 | height: 40px; 291 | align-items: center; 292 | width: 100%; 293 | position: absolute; 294 | background: var(--gray1); 295 | bottom: 0; 296 | padding: 8px; 297 | border-top: 1px solid var(--gray6); 298 | border-radius: 0 0 12px 12px; 299 | 300 | svg { 301 | width: 20px; 302 | height: 20px; 303 | filter: grayscale(1); 304 | margin-right: auto; 305 | } 306 | 307 | hr { 308 | height: 12px; 309 | width: 1px; 310 | border: 0; 311 | background: var(--gray6); 312 | margin: 0 4px 0px 12px; 313 | } 314 | 315 | @media (prefers-color-scheme: dark) { 316 | background: var(--gray2); 317 | } 318 | } 319 | 320 | [cmdk-dialog] { 321 | z-index: var(--layer-portal); 322 | position: fixed; 323 | left: 50%; 324 | top: var(--page-top); 325 | transform: translateX(-50%); 326 | 327 | [cmdk] { 328 | width: 640px; 329 | transform-origin: center center; 330 | animation: dialogIn var(--transition-fast) forwards; 331 | } 332 | 333 | &[data-state='closed'] [cmdk] { 334 | animation: dialogOut var(--transition-fast) forwards; 335 | } 336 | } 337 | 338 | [cmdk-empty] { 339 | font-size: 14px; 340 | display: flex; 341 | align-items: center; 342 | justify-content: center; 343 | height: 64px; 344 | white-space: pre-wrap; 345 | color: var(--gray11); 346 | } 347 | } 348 | 349 | @keyframes loading { 350 | 0% { 351 | opacity: 0; 352 | transform: translateX(0); 353 | } 354 | 355 | 50% { 356 | opacity: 1; 357 | transform: translateX(100%); 358 | } 359 | 360 | 100% { 361 | opacity: 0; 362 | transform: translateX(0); 363 | } 364 | } 365 | 366 | @keyframes shine { 367 | to { 368 | background-position: 200% center; 369 | opacity: 0; 370 | } 371 | } 372 | 373 | @keyframes border { 374 | to { 375 | box-shadow: 0 0 0 1px var(--gray6); 376 | } 377 | } 378 | 379 | @keyframes showTopShine { 380 | to { 381 | opacity: 1; 382 | } 383 | } 384 | 385 | .raycast-submenu { 386 | [cmdk-root] { 387 | display: flex; 388 | flex-direction: column; 389 | width: 320px; 390 | border: 1px solid var(--gray6); 391 | background: var(--gray2); 392 | border-radius: 8px; 393 | } 394 | 395 | [cmdk-list] { 396 | padding: 8px; 397 | overflow: auto; 398 | overscroll-behavior: contain; 399 | transition: 100ms ease; 400 | transition-property: height; 401 | } 402 | 403 | [cmdk-item] { 404 | height: 40px; 405 | 406 | cursor: pointer; 407 | height: 40px; 408 | border-radius: 8px; 409 | font-size: 13px; 410 | display: flex; 411 | align-items: center; 412 | gap: 8px; 413 | padding: 0 8px; 414 | color: var(--gray12); 415 | user-select: none; 416 | will-change: background, color; 417 | transition: all 150ms ease; 418 | transition-property: none; 419 | 420 | &[aria-selected='true'] { 421 | background: var(--gray5); 422 | color: var(--gray12); 423 | 424 | [cmdk-raycast-submenu-shortcuts] kbd { 425 | background: var(--gray7); 426 | } 427 | } 428 | 429 | &[aria-disabled='true'] { 430 | color: var(--gray8); 431 | cursor: not-allowed; 432 | } 433 | 434 | svg { 435 | width: 16px; 436 | height: 16px; 437 | } 438 | 439 | [cmdk-raycast-submenu-shortcuts] { 440 | display: flex; 441 | margin-left: auto; 442 | gap: 2px; 443 | 444 | kbd { 445 | font-family: var(--font-sans); 446 | background: var(--gray5); 447 | color: var(--gray11); 448 | height: 20px; 449 | width: 20px; 450 | border-radius: 4px; 451 | padding: 0 4px; 452 | font-size: 12px; 453 | display: flex; 454 | align-items: center; 455 | justify-content: center; 456 | 457 | &:first-of-type { 458 | margin-left: 8px; 459 | } 460 | } 461 | } 462 | } 463 | 464 | [cmdk-group-heading] { 465 | text-transform: capitalize; 466 | font-size: 12px; 467 | color: var(--gray11); 468 | font-weight: 500; 469 | margin-bottom: 8px; 470 | margin-top: 8px; 471 | margin-left: 4px; 472 | } 473 | 474 | [cmdk-input] { 475 | padding: 12px; 476 | font-family: var(--font-sans); 477 | border: 0; 478 | border-top: 1px solid var(--gray6); 479 | font-size: 13px; 480 | background: transparent; 481 | margin-top: auto; 482 | width: 100%; 483 | outline: 0; 484 | border-radius: 0; 485 | } 486 | 487 | animation-duration: 0.2s; 488 | animation-timing-function: ease; 489 | animation-fill-mode: forwards; 490 | transform-origin: var(--radix-popover-content-transform-origin); 491 | 492 | &[data-state='open'] { 493 | animation-name: slideIn; 494 | } 495 | 496 | &[data-state='closed'] { 497 | animation-name: slideOut; 498 | } 499 | 500 | [cmdk-empty] { 501 | display: flex; 502 | align-items: center; 503 | justify-content: center; 504 | height: 64px; 505 | white-space: pre-wrap; 506 | font-size: 14px; 507 | color: var(--gray11); 508 | } 509 | } 510 | 511 | @keyframes slideIn { 512 | 0% { 513 | opacity: 0; 514 | transform: scale(0.96); 515 | } 516 | 517 | 100% { 518 | opacity: 1; 519 | transform: scale(1); 520 | } 521 | } 522 | 523 | @keyframes slideOut { 524 | 0% { 525 | opacity: 1; 526 | transform: scale(1); 527 | } 528 | 529 | 100% { 530 | opacity: 0; 531 | transform: scale(0.96); 532 | } 533 | } 534 | 535 | @media (max-width: 640px) { 536 | .raycast { 537 | [cmdk-input] { 538 | font-size: 16px; 539 | } 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /website/styles/cmdk/vercel.scss: -------------------------------------------------------------------------------- 1 | .vercel { 2 | [cmdk-root] { 3 | max-width: 640px; 4 | width: 100%; 5 | padding: 8px; 6 | background: #ffffff; 7 | border-radius: 12px; 8 | overflow: hidden; 9 | font-family: var(--font-sans); 10 | border: 1px solid var(--gray6); 11 | box-shadow: var(--cmdk-shadow); 12 | transition: transform 100ms ease; 13 | outline: none; 14 | 15 | .dark & { 16 | background: rgba(22, 22, 22, 0.7); 17 | } 18 | } 19 | 20 | [cmdk-input] { 21 | font-family: var(--font-sans); 22 | border: none; 23 | width: 100%; 24 | font-size: 17px; 25 | padding: 8px 8px 16px 8px; 26 | outline: none; 27 | background: var(--bg); 28 | color: var(--gray12); 29 | border-bottom: 1px solid var(--gray6); 30 | margin-bottom: 16px; 31 | border-radius: 0; 32 | 33 | &::placeholder { 34 | color: var(--gray9); 35 | } 36 | } 37 | 38 | [cmdk-vercel-badge] { 39 | height: 20px; 40 | background: var(--grayA3); 41 | display: inline-flex; 42 | align-items: center; 43 | padding: 0 8px; 44 | font-size: 12px; 45 | color: var(--grayA11); 46 | border-radius: 4px; 47 | margin: 4px 0 4px 4px; 48 | user-select: none; 49 | text-transform: capitalize; 50 | font-weight: 500; 51 | } 52 | 53 | [cmdk-item] { 54 | content-visibility: auto; 55 | 56 | cursor: pointer; 57 | height: 48px; 58 | border-radius: 8px; 59 | font-size: 14px; 60 | display: flex; 61 | align-items: center; 62 | gap: 8px; 63 | padding: 0 16px; 64 | color: var(--gray11); 65 | user-select: none; 66 | will-change: background, color; 67 | transition: all 150ms ease; 68 | transition-property: none; 69 | 70 | &[data-selected='true'] { 71 | background: var(--grayA3); 72 | color: var(--gray12); 73 | } 74 | 75 | &[data-disabled='true'] { 76 | color: var(--gray8); 77 | cursor: not-allowed; 78 | } 79 | 80 | &:active { 81 | transition-property: background; 82 | background: var(--gray4); 83 | } 84 | 85 | & + [cmdk-item] { 86 | margin-top: 4px; 87 | } 88 | 89 | svg { 90 | width: 18px; 91 | height: 18px; 92 | } 93 | } 94 | 95 | [cmdk-list] { 96 | height: min(330px, calc(var(--cmdk-list-height))); 97 | max-height: 400px; 98 | overflow: auto; 99 | overscroll-behavior: contain; 100 | transition: 100ms ease; 101 | transition-property: height; 102 | } 103 | 104 | [cmdk-vercel-shortcuts] { 105 | display: flex; 106 | margin-left: auto; 107 | gap: 8px; 108 | 109 | kbd { 110 | font-family: var(--font-sans); 111 | font-size: 12px; 112 | min-width: 20px; 113 | padding: 4px; 114 | height: 20px; 115 | border-radius: 4px; 116 | color: var(--gray11); 117 | background: var(--gray4); 118 | display: inline-flex; 119 | align-items: center; 120 | justify-content: center; 121 | text-transform: uppercase; 122 | } 123 | } 124 | 125 | [cmdk-separator] { 126 | height: 1px; 127 | width: 100%; 128 | background: var(--gray5); 129 | margin: 4px 0; 130 | } 131 | 132 | *:not([hidden]) + [cmdk-group] { 133 | margin-top: 8px; 134 | } 135 | 136 | [cmdk-group-heading] { 137 | user-select: none; 138 | font-size: 12px; 139 | color: var(--gray11); 140 | padding: 0 8px; 141 | display: flex; 142 | align-items: center; 143 | margin-bottom: 8px; 144 | } 145 | 146 | [cmdk-empty] { 147 | font-size: 14px; 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | height: 48px; 152 | white-space: pre-wrap; 153 | color: var(--gray11); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /website/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-style: normal; 4 | font-weight: 100 900; // Range of weights supported 5 | font-display: optional; 6 | src: url(/inter-var-latin.woff2) format('woff2'); 7 | 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, 8 | U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 9 | } 10 | 11 | ::selection { 12 | background: hotpink; 13 | color: white; 14 | } 15 | 16 | html, 17 | body { 18 | padding: 0; 19 | margin: 0; 20 | font-family: var(--font-sans); 21 | } 22 | 23 | body { 24 | background: var(--app-bg); 25 | overflow-x: hidden; 26 | } 27 | 28 | button { 29 | background: none; 30 | font-family: var(--font-sans); 31 | padding: 0; 32 | border: 0; 33 | } 34 | 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5, 40 | h6, 41 | p { 42 | margin: 0; 43 | } 44 | 45 | a { 46 | color: inherit; 47 | text-decoration: none; 48 | } 49 | 50 | *, 51 | *::after, 52 | *::before { 53 | box-sizing: border-box; 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | } 57 | 58 | :root { 59 | --font-sans: 'Inter', --apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, 60 | Droid Sans, Helvetica Neue, sans-serif; 61 | --app-bg: var(--gray1); 62 | --cmdk-shadow: 0 16px 70px rgb(0 0 0 / 20%); 63 | 64 | --lowContrast: #ffffff; 65 | --highContrast: #000000; 66 | 67 | --gray1: hsl(0, 0%, 99%); 68 | --gray2: hsl(0, 0%, 97.3%); 69 | --gray3: hsl(0, 0%, 95.1%); 70 | --gray4: hsl(0, 0%, 93%); 71 | --gray5: hsl(0, 0%, 90.9%); 72 | --gray6: hsl(0, 0%, 88.7%); 73 | --gray7: hsl(0, 0%, 85.8%); 74 | --gray8: hsl(0, 0%, 78%); 75 | --gray9: hsl(0, 0%, 56.1%); 76 | --gray10: hsl(0, 0%, 52.3%); 77 | --gray11: hsl(0, 0%, 43.5%); 78 | --gray12: hsl(0, 0%, 9%); 79 | 80 | --grayA1: hsla(0, 0%, 0%, 0.012); 81 | --grayA2: hsla(0, 0%, 0%, 0.027); 82 | --grayA3: hsla(0, 0%, 0%, 0.047); 83 | --grayA4: hsla(0, 0%, 0%, 0.071); 84 | --grayA5: hsla(0, 0%, 0%, 0.09); 85 | --grayA6: hsla(0, 0%, 0%, 0.114); 86 | --grayA7: hsla(0, 0%, 0%, 0.141); 87 | --grayA8: hsla(0, 0%, 0%, 0.22); 88 | --grayA9: hsla(0, 0%, 0%, 0.439); 89 | --grayA10: hsla(0, 0%, 0%, 0.478); 90 | --grayA11: hsla(0, 0%, 0%, 0.565); 91 | --grayA12: hsla(0, 0%, 0%, 0.91); 92 | 93 | --blue1: hsl(206, 100%, 99.2%); 94 | --blue2: hsl(210, 100%, 98%); 95 | --blue3: hsl(209, 100%, 96.5%); 96 | --blue4: hsl(210, 98.8%, 94%); 97 | --blue5: hsl(209, 95%, 90.1%); 98 | --blue6: hsl(209, 81.2%, 84.5%); 99 | --blue7: hsl(208, 77.5%, 76.9%); 100 | --blue8: hsl(206, 81.9%, 65.3%); 101 | --blue9: hsl(206, 100%, 50%); 102 | --blue10: hsl(208, 100%, 47.3%); 103 | --blue11: hsl(211, 100%, 43.2%); 104 | --blue12: hsl(211, 100%, 15%); 105 | } 106 | 107 | .dark { 108 | --app-bg: var(--gray1); 109 | 110 | --lowContrast: #000000; 111 | --highContrast: #ffffff; 112 | 113 | --gray1: hsl(0, 0%, 8.5%); 114 | --gray2: hsl(0, 0%, 11%); 115 | --gray3: hsl(0, 0%, 13.6%); 116 | --gray4: hsl(0, 0%, 15.8%); 117 | --gray5: hsl(0, 0%, 17.9%); 118 | --gray6: hsl(0, 0%, 20.5%); 119 | --gray7: hsl(0, 0%, 24.3%); 120 | --gray8: hsl(0, 0%, 31.2%); 121 | --gray9: hsl(0, 0%, 43.9%); 122 | --gray10: hsl(0, 0%, 49.4%); 123 | --gray11: hsl(0, 0%, 62.8%); 124 | --gray12: hsl(0, 0%, 93%); 125 | 126 | --grayA1: hsla(0, 0%, 100%, 0); 127 | --grayA2: hsla(0, 0%, 100%, 0.026); 128 | --grayA3: hsla(0, 0%, 100%, 0.056); 129 | --grayA4: hsla(0, 0%, 100%, 0.077); 130 | --grayA5: hsla(0, 0%, 100%, 0.103); 131 | --grayA6: hsla(0, 0%, 100%, 0.129); 132 | --grayA7: hsla(0, 0%, 100%, 0.172); 133 | --grayA8: hsla(0, 0%, 100%, 0.249); 134 | --grayA9: hsla(0, 0%, 100%, 0.386); 135 | --grayA10: hsla(0, 0%, 100%, 0.446); 136 | --grayA11: hsla(0, 0%, 100%, 0.592); 137 | --grayA12: hsla(0, 0%, 100%, 0.923); 138 | 139 | --blue1: hsl(212, 35%, 9.2%); 140 | --blue2: hsl(216, 50%, 11.8%); 141 | --blue3: hsl(214, 59.4%, 15.3%); 142 | --blue4: hsl(214, 65.8%, 17.9%); 143 | --blue5: hsl(213, 71.2%, 20.2%); 144 | --blue6: hsl(212, 77.4%, 23.1%); 145 | --blue7: hsl(211, 85.1%, 27.4%); 146 | --blue8: hsl(211, 89.7%, 34.1%); 147 | --blue9: hsl(206, 100%, 50%); 148 | --blue10: hsl(209, 100%, 60.6%); 149 | --blue11: hsl(210, 100%, 66.1%); 150 | --blue12: hsl(206, 98%, 95.8%); 151 | } 152 | -------------------------------------------------------------------------------- /website/styles/index.module.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100vw; 3 | min-height: 100vh; 4 | position: relative; 5 | display: flex; 6 | justify-content: center; 7 | padding: 120px 24px 160px 24px; 8 | 9 | &:before { 10 | background: radial-gradient(circle, rgba(2, 0, 36, 0) 0, var(--gray1) 100%); 11 | position: absolute; 12 | content: ''; 13 | z-index: 2; 14 | width: 100%; 15 | height: 100%; 16 | top: 0; 17 | } 18 | 19 | &:after { 20 | content: ''; 21 | background-image: url('/grid.svg'); 22 | z-index: -1; 23 | position: absolute; 24 | width: 100%; 25 | height: 100%; 26 | top: 0; 27 | opacity: 0.2; 28 | filter: invert(1); 29 | 30 | @media (prefers-color-scheme: dark) { 31 | filter: unset; 32 | } 33 | } 34 | 35 | h1 { 36 | font-size: 32px; 37 | color: var(--gray12); 38 | font-weight: 600; 39 | letter-spacing: -2px; 40 | line-height: 40px; 41 | } 42 | 43 | p { 44 | color: var(--gray11); 45 | margin-top: 8px; 46 | font-size: 16px; 47 | } 48 | } 49 | 50 | .content { 51 | height: fit-content; 52 | position: relative; 53 | z-index: 3; 54 | width: 100%; 55 | max-width: 640px; 56 | 57 | &:after { 58 | background-image: radial-gradient(at 27% 37%, hsla(215, 98%, 61%, 1) 0px, transparent 50%), 59 | radial-gradient(at 97% 21%, hsla(256, 98%, 72%, 1) 0px, transparent 50%), 60 | radial-gradient(at 52% 99%, hsla(354, 98%, 61%, 1) 0px, transparent 50%), 61 | radial-gradient(at 10% 29%, hsla(133, 96%, 67%, 1) 0px, transparent 50%), 62 | radial-gradient(at 97% 96%, hsla(38, 60%, 74%, 1) 0px, transparent 50%), 63 | radial-gradient(at 33% 50%, hsla(222, 67%, 73%, 1) 0px, transparent 50%), 64 | radial-gradient(at 79% 53%, hsla(343, 68%, 79%, 1) 0px, transparent 50%); 65 | position: absolute; 66 | content: ''; 67 | z-index: 2; 68 | width: 100%; 69 | height: 100%; 70 | filter: blur(100px) saturate(150%); 71 | z-index: -1; 72 | top: 80px; 73 | opacity: 0.2; 74 | transform: translateZ(0); 75 | 76 | @media (prefers-color-scheme: dark) { 77 | opacity: 0.1; 78 | } 79 | } 80 | } 81 | 82 | .meta { 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | margin-bottom: 48px; 87 | flex-wrap: wrap; 88 | gap: 16px; 89 | } 90 | 91 | .buttons { 92 | display: flex; 93 | flex-direction: column; 94 | align-items: flex-end; 95 | gap: 12px; 96 | transform: translateY(12px); 97 | } 98 | 99 | .githubButton, 100 | .installButton, 101 | .switcher button { 102 | height: 40px; 103 | color: var(--gray12); 104 | border-radius: 9999px; 105 | font-size: 14px; 106 | transition-duration: 150ms; 107 | transition-property: background, color, transform; 108 | transition-timing-function: ease-in; 109 | will-change: transform; 110 | } 111 | 112 | .githubButton { 113 | width: 177px; 114 | padding: 0 12px; 115 | display: inline-flex; 116 | align-items: center; 117 | gap: 8px; 118 | font-weight: 500; 119 | 120 | &:hover { 121 | background: var(--grayA3); 122 | } 123 | 124 | &:active { 125 | background: var(--grayA5); 126 | transform: scale(0.97); 127 | } 128 | 129 | &:focus-visible { 130 | outline: 0; 131 | outline: 2px solid var(--gray7); 132 | } 133 | } 134 | 135 | .installButton { 136 | background: var(--grayA3); 137 | display: flex; 138 | align-items: center; 139 | gap: 16px; 140 | padding: 0px 8px 0 16px; 141 | cursor: copy; 142 | font-weight: 500; 143 | 144 | &:hover { 145 | background: var(--grayA4); 146 | 147 | span { 148 | background: var(--grayA5); 149 | 150 | svg { 151 | color: var(--gray12); 152 | } 153 | } 154 | } 155 | 156 | &:focus-visible { 157 | outline: 0; 158 | outline: 2px solid var(--gray7); 159 | outline-offset: 2px; 160 | } 161 | 162 | &:active { 163 | background: var(--gray5); 164 | transform: scale(0.97); 165 | } 166 | 167 | span { 168 | width: 28px; 169 | height: 28px; 170 | display: flex; 171 | align-items: center; 172 | justify-content: center; 173 | margin-left: auto; 174 | background: var(--grayA3); 175 | border-radius: 9999px; 176 | transition: background 150ms ease; 177 | 178 | svg { 179 | size: 16px; 180 | color: var(--gray11); 181 | transition: color 150ms ease; 182 | } 183 | } 184 | } 185 | 186 | .switcher { 187 | display: grid; 188 | grid-template-columns: repeat(4, 100px); 189 | align-items: center; 190 | justify-content: center; 191 | gap: 4px; 192 | margin-top: 48px; 193 | position: relative; 194 | 195 | button { 196 | height: 32px; 197 | line-height: 32px; 198 | display: flex; 199 | align-items: center; 200 | margin: auto; 201 | gap: 8px; 202 | padding: 0 16px; 203 | border-radius: 9999px; 204 | color: var(--gray11); 205 | font-size: 14px; 206 | cursor: pointer; 207 | user-select: none; 208 | position: relative; 209 | text-transform: capitalize; 210 | 211 | &:hover { 212 | color: var(--gray12); 213 | } 214 | 215 | &:active { 216 | transform: scale(0.96); 217 | } 218 | 219 | &:focus-visible { 220 | outline: 0; 221 | outline: 2px solid var(--gray7); 222 | } 223 | 224 | svg { 225 | width: 14px; 226 | height: 14px; 227 | } 228 | 229 | &[data-selected='true'] { 230 | color: var(--gray12); 231 | 232 | &:hover .activeTheme { 233 | background: var(--grayA6); 234 | } 235 | 236 | &:active { 237 | transform: scale(0.96); 238 | 239 | .activeTheme { 240 | background: var(--grayA7); 241 | } 242 | } 243 | } 244 | } 245 | 246 | .activeTheme { 247 | background: var(--grayA5); 248 | border-radius: 9999px; 249 | height: 32px; 250 | width: 100%; 251 | top: 0; 252 | position: absolute; 253 | left: 0; 254 | } 255 | 256 | .arrow { 257 | color: var(--gray11); 258 | user-select: none; 259 | position: absolute; 260 | } 261 | } 262 | 263 | .header { 264 | position: absolute; 265 | left: 0; 266 | top: -64px; 267 | gap: 8px; 268 | background: var(--gray3); 269 | padding: 4px; 270 | display: flex; 271 | align-items: center; 272 | border-radius: 9999px; 273 | 274 | button { 275 | display: flex; 276 | align-items: center; 277 | justify-content: center; 278 | width: 28px; 279 | height: 28px; 280 | padding: 4px; 281 | border-radius: 9999px; 282 | color: var(--gray11); 283 | 284 | svg { 285 | width: 16px; 286 | height: 16px; 287 | } 288 | 289 | &[aria-selected='true'] { 290 | background: #ffffff; 291 | color: var(--gray12); 292 | box-shadow: 0px 2px 5px -2px rgb(0 0 0 / 15%), 0 1px 3px -1px rgb(0 0 0 / 20%); 293 | } 294 | } 295 | } 296 | 297 | .versionBadge { 298 | display: inline-flex; 299 | align-items: center; 300 | justify-content: center; 301 | color: var(--grayA11); 302 | background: var(--grayA3); 303 | padding: 4px 8px; 304 | border-radius: 4px; 305 | font-weight: 500; 306 | font-size: 14px; 307 | margin-bottom: 8px; 308 | 309 | @media (prefers-color-scheme: dark) { 310 | background: var(--grayA2); 311 | } 312 | } 313 | 314 | .codeBlock { 315 | margin-top: 72px; 316 | position: relative; 317 | } 318 | 319 | .footer { 320 | display: flex; 321 | align-items: center; 322 | gap: 4px; 323 | width: fit-content; 324 | margin: 32px auto; 325 | bottom: 16px; 326 | color: var(--gray11); 327 | font-size: 13px; 328 | z-index: 3; 329 | position: absolute; 330 | bottom: 0; 331 | 332 | a { 333 | display: inline-flex; 334 | align-items: center; 335 | gap: 4px; 336 | color: var(--gray12); 337 | font-weight: 500; 338 | border-radius: 9999px; 339 | padding: 4px; 340 | margin: 0 -2px; 341 | transition: background 150ms ease; 342 | 343 | &:hover, 344 | &:focus-visible { 345 | background: var(--grayA4); 346 | outline: 0; 347 | } 348 | } 349 | 350 | img { 351 | width: 20px; 352 | height: 20px; 353 | border: 1px solid var(--gray5); 354 | border-radius: 9999px; 355 | } 356 | } 357 | 358 | .line { 359 | height: 20px; 360 | width: 180px; 361 | margin: 64px auto; 362 | background-image: url('/line.svg'); 363 | filter: invert(1); 364 | mask-image: linear-gradient(90deg, transparent, #fff 4rem, #fff calc(100% - 4rem), transparent); 365 | 366 | @media (prefers-color-scheme: dark) { 367 | filter: unset; 368 | } 369 | } 370 | 371 | .line2 { 372 | height: 1px; 373 | width: 300px; 374 | background: var(--gray7); 375 | position: absolute; 376 | top: 0; 377 | mask-image: linear-gradient(90deg, transparent, #fff 4rem, #fff calc(100% - 4rem), transparent); 378 | } 379 | 380 | .line3 { 381 | height: 300px; 382 | width: calc(100% + 32px); 383 | position: absolute; 384 | top: -16px; 385 | left: -16px; 386 | 387 | border-radius: 16px 16px 0 0; 388 | --size: 1px; 389 | --gradient: linear-gradient(to top, var(--gray1), var(--gray7)); 390 | 391 | &::before { 392 | content: ''; 393 | position: absolute; 394 | inset: 0; 395 | border-radius: inherit; 396 | padding: var(--size); 397 | background: linear-gradient(to top, var(--gray1), var(--gray7)); 398 | mask: linear-gradient(black, black) content-box, linear-gradient(black, black); 399 | mask-composite: exclude; 400 | transform: translateZ(0); 401 | 402 | @media (prefers-color-scheme: dark) { 403 | mask: none; 404 | mask-composite: none; 405 | opacity: 0.2; 406 | backdrop-filter: blur(20px); 407 | } 408 | } 409 | } 410 | 411 | .raunoSignature, 412 | .pacoSignature { 413 | position: absolute; 414 | height: fit-content; 415 | color: var(--gray11); 416 | pointer-events: none; 417 | } 418 | 419 | .raunoSignature { 420 | width: 120px; 421 | stroke-dashoffset: 1; 422 | stroke-dasharray: 1; 423 | right: -48px; 424 | } 425 | 426 | .pacoSignature { 427 | width: 120px; 428 | stroke-dashoffset: 1; 429 | stroke-dasharray: 1; 430 | left: -8px; 431 | } 432 | 433 | .footerText { 434 | display: flex; 435 | display: flex; 436 | align-items: center; 437 | gap: 4px; 438 | opacity: 0; 439 | } 440 | 441 | .footer[data-animate='true'] { 442 | .raunoSignature path { 443 | animation: drawRaunoSignature 1.5s ease forwards 0.5s; 444 | } 445 | 446 | .pacoSignature path { 447 | animation: drawPacoSignature 0.8s linear forwards 0.5s; 448 | } 449 | 450 | .footerText { 451 | animation: showFooter 1s linear forwards 3s; 452 | } 453 | } 454 | 455 | @keyframes drawPacoSignature { 456 | 100% { 457 | stroke-dashoffset: 0; 458 | } 459 | } 460 | 461 | @keyframes drawRaunoSignature { 462 | 100% { 463 | stroke-dashoffset: 0; 464 | } 465 | } 466 | 467 | @keyframes showFooter { 468 | 100% { 469 | opacity: 1; 470 | } 471 | } 472 | 473 | @media (max-width: 640px) { 474 | .main { 475 | padding-top: 24px; 476 | padding-bottom: 120px; 477 | } 478 | 479 | .switcher { 480 | grid-template-columns: repeat(2, 100px); 481 | gap: 16px; 482 | 483 | .arrow { 484 | display: none; 485 | } 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "." 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /website/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/inter-var-latin.woff2", 5 | "headers": [ 6 | { 7 | "key": "Cache-Control", 8 | "value": "public, max-age=31536000, immutable" 9 | } 10 | ] 11 | } 12 | ] 13 | } 14 | --------------------------------------------------------------------------------