├── .eslintrc.js ├── .github ├── README.md └── workflows │ ├── e2e.yml │ └── validate.yml ├── .gitignore ├── .prettierignore ├── README.md ├── e2e ├── command-palette-basic.spec.ts ├── demo-page.spec.ts └── testUtils │ └── checkMac.ts ├── index.html ├── package-lock.json ├── package.json ├── playwright.config.ts ├── prettier.config.js ├── public ├── images │ ├── branding │ │ ├── logo-dark-horizontal-large.png │ │ ├── logo-dark-horizontal.png │ │ ├── logo-dark-stamp.svg │ │ ├── logo-dark-vertical-large.png │ │ ├── logo-dark-vertical.png │ │ ├── logo-light-favicon.png │ │ ├── logo-light-horizontal-large.png │ │ ├── logo-light-horizontal.png │ │ ├── logo-light-stamp.svg │ │ ├── logo-light-vertical-large.png │ │ └── logo-light-vertical.png │ ├── command-palette-examples │ │ ├── devtools.png │ │ ├── github.png │ │ ├── linear.png │ │ └── tailwind.png │ └── demo-minimal.gif └── snippets │ ├── api-command-palette.html │ ├── api-define-action.html │ ├── api-root.html │ ├── installation-define-actions.html │ ├── installation-integrate-components.html │ ├── installation-packages.html │ └── installation-setup.html ├── scripts └── generateSnippets.ts ├── snippets ├── api-command-palette.snippet.tsx ├── api-define-action.snippet.tsx ├── api-root.snippet.tsx ├── installation-define-actions.snippet.tsx ├── installation-integrate-components.snippet.tsx └── installation-packages.snippet.sh ├── src ├── app │ ├── App.tsx │ ├── index.tsx │ ├── shared │ │ └── Loader │ │ │ ├── Loader.module.css │ │ │ └── Loader.tsx │ ├── utils.module.css │ └── views │ │ ├── app │ │ ├── AppShell.module.css │ │ ├── AppShell.view.tsx │ │ ├── SocialIcons.tsx │ │ └── actions.ts │ │ ├── demo │ │ ├── CustomComponentsDemo │ │ │ ├── CustomComponentsDemo.module.css │ │ │ ├── CustomComponentsDemo.tsx │ │ │ └── components.ts │ │ ├── Demo.module.css │ │ ├── Demo.view.tsx │ │ ├── DynamicActionContextDemo │ │ │ ├── DynamicActionContextDemo.module.css │ │ │ ├── DynamicActionContextDemo.tsx │ │ │ ├── data.ts │ │ │ ├── dynamicContextActions.ts │ │ │ └── types.ts │ │ ├── NestedActionDemo │ │ │ ├── NestedActionDemo.module.css │ │ │ ├── NestedActionDemo.tsx │ │ │ └── nestedActions.ts │ │ ├── actions.ts │ │ ├── demoUtils.module.css │ │ └── types.ts │ │ ├── docs │ │ ├── Api │ │ │ ├── Api.view.tsx │ │ │ ├── ApiCommandPalette.view.tsx │ │ │ ├── ApiDefineAction.view.tsx │ │ │ └── ApiRoot.view.tsx │ │ ├── Docs.view.tsx │ │ ├── DocsShell │ │ │ ├── DocsShell.module.css │ │ │ └── DocsShell.view.tsx │ │ ├── Snippet │ │ │ ├── Snippet.module.css │ │ │ └── Snippet.tsx │ │ ├── docsUtils.module.css │ │ └── introduction │ │ │ ├── Installation.view.tsx │ │ │ └── Overview.view.tsx │ │ └── home │ │ ├── Home.module.css │ │ ├── Home.view.tsx │ │ └── exampleSlider │ │ ├── ExampleSlider.module.css │ │ ├── ExampleSlider.tsx │ │ └── data.ts └── lib │ ├── CommandPalette.module.css │ ├── CommandPalette.tsx │ ├── CommandPalettePortal.tsx │ ├── KbdShortcut │ ├── KbdShortcut.module.css │ ├── KbdShortcut.tsx │ ├── types.ts │ ├── utils.test.ts │ ├── utils.ts │ ├── utilsMac.test.ts │ └── utilsWin.test.ts │ ├── Panel │ ├── Footer │ │ ├── Footer.module.css │ │ └── Footer.tsx │ └── Result │ │ ├── Result.module.css │ │ └── Result.tsx │ ├── Root.tsx │ ├── ScrollAssist │ ├── ScrollAssist.module.css │ └── ScrollAssist.tsx │ ├── StoreContext.tsx │ ├── actionUtils │ ├── actionUtils.test.ts │ └── actionUtils.ts │ ├── constants.ts │ ├── createActionList.ts │ ├── createKbdShortcuts.ts │ ├── createSyncActionsContext.ts │ ├── defineAction.ts │ ├── index.ts │ ├── types.ts │ ├── useControls.ts │ └── utils.module.css ├── tsconfig-node.json ├── tsconfig.json ├── vercel.json ├── vite-pkg.config.ts └── vite.config.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | }, 11 | plugins: ['@typescript-eslint'], 12 | rules: { 13 | '@typescript-eslint/no-unused-vars': [ 14 | 'error', 15 | { 16 | varsIgnorePattern: '^_', 17 | argsIgnorePattern: '^_', 18 | }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Command Palette for Solid.js 4 | Command Palette for Solid.js 5 |
6 |
7 |

8 | 9 |

Boost your users productivity by 10x 🚀

10 | 11 |
12 | 13 | ### Some of the features offered by this library- 14 | 15 | 1. Define actions with a simple config. 16 | 1. Full keyboard support like open with CMD + K, navigate between actions using arrow keys. 17 | 1. Fuzzy search between your actions on title, subtile, keywords. 18 | 1. Bind custom keyboard shortcuts to your actions. They can be single letter, modifier combinations Shift + l or sequences g p. 19 | 1. Enable actions based on dynamic conditions. 20 | 1. Share your app state and methods to run any kind of functionality from actions. 21 | 1. Full static type safety across the board. 22 | 23 | ## Demo 24 | 25 | ![Solid Command Palette Demo](../public/images/demo-minimal.gif) 26 | 27 | Try the full demo on [our documentation site](https://solid-command-palette.vercel.app/demo). 28 | 29 | ## Usage 30 | 31 | #### Install the library 32 | 33 | ```sh 34 | # Core Library 35 | npm install solid-command-palette 36 | 37 | # Peer Dependencies 38 | npm install solid-transition-group tinykeys fuse.js 39 | ``` 40 | 41 | - [solid-transition-group](https://github.com/solidjs/solid-transition-group) (1.6KB): provides advanced animation support. It's the official recommendation from SolidJS team so you might be using it already. 42 | - [tinykeys](https://github.com/jamiebuilds/tinykeys) (700B): provides keyboard shortcut support. You can use this in your app for all kinds of keybindings. 43 | - [fuse.js](https://github.com/krisk/fuse) (5KB): provides fuzzy search support to find relevant actions. 44 | 45 | #### Integrate with app 46 | 47 | ```jsx 48 | // define actions in one module say `actions.ts` 49 | 50 | import { defineAction } from 'solid-command-palette'; 51 | 52 | const minimalAction = defineAction({ 53 | id: 'minimal', 54 | title: 'Minimal Action', 55 | run: () => { 56 | console.log('ran minimal action'); 57 | }, 58 | }); 59 | 60 | const incrementCounterAction = defineAction({ 61 | id: 'increment-counter', 62 | title: 'Increment Counter by 1', 63 | subtitle: 'Press CMD + E to trigger this.', 64 | shortcut: '$mod+e', // $mod = Command on Mac & Control on Windows. 65 | run: ({ rootContext }) => { 66 | rootContext.increment(); 67 | }, 68 | }); 69 | 70 | export const actions = { 71 | [minimalAction.id]: minimalAction, 72 | [incrementCounterAction.id]: incrementCounterAction, 73 | }; 74 | ``` 75 | 76 | ```jsx 77 | // render inside top level Solid component 78 | 79 | import { Root, CommandPalette } from 'solid-command-palette'; 80 | import { actions } from './actions'; 81 | import 'solid-command-palette/pkg-dist/style.css'; 82 | 83 | const App = () => { 84 | const actionsContext = { 85 | increment() { 86 | console.log('increment count state by 1'); 87 | }, 88 | }; 89 | 90 | return ( 91 |
92 | 96 | 97 | 98 |
99 | ); 100 | }; 101 | ``` 102 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: [deployment_status] 4 | 5 | jobs: 6 | test: 7 | timeout-minutes: 60 8 | runs-on: ubuntu-latest 9 | if: github.event.deployment_status.state == 'success' 10 | steps: 11 | - name: Checkout commit 12 | uses: actions/checkout@v2 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: 14 17 | - name: Install Dependencies 18 | run: npm ci 19 | - name: Install Playwright 20 | run: npx playwright install --with-deps 21 | - name: Run E2E tests 22 | run: npm run test:e2e:run 23 | env: 24 | PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }} 25 | - name: Save Test Results 26 | uses: actions/upload-artifact@v2 27 | if: always() 28 | with: 29 | name: playwright-report 30 | path: playwright-report/ 31 | retention-days: 2 32 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | validate: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout commit 14 | uses: actions/checkout@v2 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 14 19 | cache: 'npm' 20 | - name: Install Dependencies 21 | run: npm ci 22 | - name: Check for linting issues 23 | run: npm run lint 24 | - name: Run functional tests 25 | run: npm run test:func:run 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | dist 4 | pkg-dist 5 | coverage 6 | .DS_Store 7 | test-results/ 8 | playwright-report/ 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | dist 4 | public 5 | pkg-dist 6 | coverage 7 | .DS_Store 8 | test-results/ 9 | playwright-report/ 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Command Palette for Solid.js 4 |
5 |
6 |

7 | 8 |

Boost your users productivity by 10x 🚀

9 | 10 |
11 | 12 | ### Some of the features offered by this library- 13 | 14 | 1. Define actions with a simple config. 15 | 1. Full keyboard support like open with CMD + K, navigate between actions using arrow keys. 16 | 1. Fuzzy search between your actions on title, subtile, keywords. 17 | 1. Bind custom keyboard shortcuts to your actions. They can be single letter, modifier combinations Shift + l or sequences g p. 18 | 1. Enable actions based on dynamic conditions. 19 | 1. Share your app state and methods to run any kind of functionality from actions. 20 | 1. Full static type safety across the board. 21 | 22 | ## Demo 23 | 24 | ![Solid Command Palette Demo](./public/images/demo-minimal.gif) 25 | 26 | Try the full demo on [our documentation site](https://solid-command-palette.vercel.app/demo). 27 | 28 | ## Usage 29 | 30 | #### Install the library 31 | 32 | ```sh 33 | # Core Library 34 | npm install solid-command-palette 35 | 36 | # Peer Dependencies 37 | npm install solid-transition-group tinykeys fuse.js 38 | ``` 39 | 40 | - [solid-transition-group](https://github.com/solidjs/solid-transition-group) (1.6KB): provides advanced animation support. It's the official recommendation from SolidJS team so you might be using it already. 41 | - [tinykeys](https://github.com/jamiebuilds/tinykeys) (700B): provides keyboard shortcut support. You can use this in your app for all kinds of keybindings. 42 | - [fuse.js](https://github.com/krisk/fuse) (5KB): provides fuzzy search support to find relevant actions. 43 | 44 | #### Integrate with app 45 | 46 | ```jsx 47 | // define actions in one module say `actions.ts` 48 | 49 | import { defineAction } from 'solid-command-palette'; 50 | 51 | const minimalAction = defineAction({ 52 | id: 'minimal', 53 | title: 'Minimal Action', 54 | run: () => { 55 | console.log('ran minimal action'); 56 | }, 57 | }); 58 | 59 | const incrementCounterAction = defineAction({ 60 | id: 'increment-counter', 61 | title: 'Increment Counter by 1', 62 | subtitle: 'Press CMD + E to trigger this.', 63 | shortcut: '$mod+e', // $mod = Command on Mac & Control on Windows. 64 | run: ({ rootContext }) => { 65 | rootContext.increment(); 66 | }, 67 | }); 68 | 69 | export const actions = { 70 | [minimalAction.id]: minimalAction, 71 | [incrementCounterAction.id]: incrementCounterAction, 72 | }; 73 | ``` 74 | 75 | ```jsx 76 | // render inside top level Solid component 77 | 78 | import { Root, CommandPalette } from 'solid-command-palette'; 79 | import { actions } from './actions'; 80 | import 'solid-command-palette/pkg-dist/style.css'; 81 | 82 | const App = () => { 83 | const actionsContext = { 84 | increment() { 85 | console.log('increment count state by 1'); 86 | }, 87 | }; 88 | 89 | return ( 90 |
91 | 95 | 96 | 97 |
98 | ); 99 | }; 100 | ``` 101 | -------------------------------------------------------------------------------- /e2e/command-palette-basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from '@playwright/test'; 2 | import { checkMac } from './testUtils/checkMac'; 3 | 4 | async function triggerCommandPaletteOpen(page: Page) { 5 | const isMac = await checkMac(page); 6 | 7 | if (isMac) { 8 | await page.keyboard.press('Meta+k'); 9 | } else { 10 | await page.keyboard.press('Control+k'); 11 | } 12 | } 13 | 14 | test.describe('Test basic interactions of Command Palette', () => { 15 | test('should be able to open command palette & run first action', async ({ page }) => { 16 | await page.goto('/demo'); 17 | await triggerCommandPaletteOpen(page); 18 | 19 | await page.click('text=Increment Counter by 1'); 20 | await expect(page.locator('strong >> text=1')).toBeVisible(); 21 | }); 22 | 23 | test('should be able to search for actions in command palette', async ({ page }) => { 24 | await page.goto('/demo'); 25 | await triggerCommandPaletteOpen(page); 26 | 27 | await page.keyboard.type('GitHub'); 28 | 29 | const searchLocator = page.locator('input[type="search"]'); 30 | await expect(searchLocator).toHaveValue('GitHub'); 31 | 32 | const optionLocator = page.locator('[role="combobox"] >> [role="option"]'); 33 | 34 | const optionsNum = await optionLocator.count(); 35 | expect(optionsNum).toBe(1); 36 | 37 | await expect(optionLocator.first()).toContainText('Go to GitHub repo'); 38 | }); 39 | 40 | test('should be able to navigate between actions using keyboard', async ({ page }) => { 41 | await page.goto('/demo'); 42 | await triggerCommandPaletteOpen(page); 43 | 44 | // Navigate to third action by pressing Down key twice. 45 | await page.keyboard.press('ArrowDown'); 46 | await page.keyboard.press('ArrowDown'); 47 | 48 | const optionLocator = page.locator('[role="combobox"] >> [role="option"]'); 49 | let isThirdOptionSelected = await optionLocator.nth(2).getAttribute('aria-selected'); 50 | 51 | expect(isThirdOptionSelected).toEqual('true'); 52 | 53 | // Navigate to second action by pressing Up key once. 54 | await page.keyboard.press('ArrowUp'); 55 | 56 | isThirdOptionSelected = await optionLocator.nth(2).getAttribute('aria-selected'); 57 | const isSecondOptionSelected = await optionLocator.nth(1).getAttribute('aria-selected'); 58 | 59 | expect(isThirdOptionSelected).toEqual('false'); 60 | expect(isSecondOptionSelected).toEqual('true'); 61 | }); 62 | 63 | test('should only render Unmute Audio action when muted', async ({ page }) => { 64 | await page.goto('/demo'); 65 | await triggerCommandPaletteOpen(page); 66 | 67 | await expect(page.locator('[role="combobox"]')).not.toContainText('Unmute'); 68 | 69 | await page.keyboard.press('Escape'); 70 | await page.check('label >> text=Audible'); 71 | await triggerCommandPaletteOpen(page); 72 | await page.click('[role="combobox"] >> text=Unmute'); 73 | 74 | const isUnmuted = await page.isChecked('label >> text=Audible'); 75 | expect(isUnmuted).toBeFalsy(); 76 | }); 77 | 78 | test('should be able to run nested actions', async ({ page }) => { 79 | await page.goto('/demo'); 80 | await triggerCommandPaletteOpen(page); 81 | 82 | await page.keyboard.type('Profile'); 83 | 84 | const optionLocator = page.locator('[role="combobox"] >> [role="option"]'); 85 | 86 | await optionLocator.locator('text=Set profile').click(); 87 | 88 | const searchLocator = page.locator('input[type="search"]'); 89 | await expect(searchLocator).toHaveValue(''); 90 | 91 | const optionsNum = await optionLocator.count(); 92 | expect(optionsNum).toBe(2); 93 | 94 | await optionLocator.locator('text=Set to Work profile').click(); 95 | 96 | const profileStatusLocator = page 97 | .locator('section', { 98 | hasText: 'Nested actions', 99 | }) 100 | .last() 101 | .locator('text=Active profile is work'); 102 | 103 | await expect(profileStatusLocator).toBeVisible(); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /e2e/demo-page.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { checkMac } from './testUtils/checkMac'; 3 | 4 | test.describe('Test basic interactions of Demo Page', () => { 5 | test.beforeEach(async ({ page }) => { 6 | await page.goto('/demo'); 7 | }); 8 | 9 | test('should be able to increment count', async ({ page }) => { 10 | await page.locator('button >> text=Increment Count').click(); 11 | await expect(page.locator('strong >> text=1')).toBeVisible(); 12 | }); 13 | 14 | test('should be able to toggle mute', async ({ page }) => { 15 | const unmuteLabelLocator = page.locator('label >> text=Audible'); 16 | const muteLabelLocator = page.locator('label >> text=Muted'); 17 | 18 | const isMac = await checkMac(page); 19 | const shortcutPrefix = isMac ? '⌘' : 'Ctrl'; 20 | const kbdShortcutLocator = page.locator(`kbd >> text=${shortcutPrefix}U`).first(); 21 | 22 | await unmuteLabelLocator.check(); 23 | await expect(muteLabelLocator).toBeChecked(); 24 | await expect(kbdShortcutLocator).toBeVisible(); 25 | 26 | await muteLabelLocator.uncheck(); 27 | await expect(unmuteLabelLocator).toBeChecked({ 28 | checked: false, 29 | }); 30 | await expect(kbdShortcutLocator).not.toBeVisible(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /e2e/testUtils/checkMac.ts: -------------------------------------------------------------------------------- 1 | import { Page } from '@playwright/test'; 2 | 3 | export async function checkMac(page: Page) { 4 | const platform = await page.evaluate(() => window.navigator.platform); 5 | const isMac = platform.includes('Mac'); 6 | 7 | return isMac; 8 | } 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 17 | 21 | 26 | 30 | Solid Command Palette 31 | 32 | 33 | 34 | 35 |
36 | 37 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-command-palette", 3 | "version": "1.0.0", 4 | "description": "Add a command palette to your Solid.js App", 5 | "info": "Boost your users productivity 10x by integrating our command palette.", 6 | "main": "./pkg-dist/solid-command-palette.cjs.js", 7 | "module": "./pkg-dist/solid-command-palette.es.js", 8 | "types": "./pkg-dist/types", 9 | "sideEffects": false, 10 | "scripts": { 11 | "dev": "vite", 12 | "build": "vite build", 13 | "serve": "vite preview", 14 | "lint:format": "prettier --check .", 15 | "lint:format:fix": "prettier --write .", 16 | "lint:types": "tsc --noEmit", 17 | "lint:types:watch": "npm run lint:types -- --watch", 18 | "lint:syntax": "eslint --ext '.ts,.tsx' 'src/'", 19 | "lint:syntax:fix": "npm run lint:syntax -- --fix", 20 | "lint": "npm run lint:format && npm run lint:syntax && npm run lint:types", 21 | "test:func": "vitest src", 22 | "test:func:run": "vitest run src", 23 | "test:e2e": "playwright test --headed --project='chromium'", 24 | "test:e2e:run": "playwright test", 25 | "test:e2e:open": "playwright open 'http://localhost:3000'", 26 | "test:e2e:list": "playwright test --list", 27 | "gen:types": "tsc --emitDeclarationOnly", 28 | "gen:lib": "vite build -c vite-pkg.config.ts", 29 | "gen:snippets": "ts-node -P tsconfig-node.json ./scripts/generateSnippets.ts", 30 | "prepublishOnly": "npm run gen:lib && npm run gen:types" 31 | }, 32 | "license": "MIT", 33 | "devDependencies": { 34 | "@playwright/test": "^1.19.0", 35 | "@stackblitz/sdk": "^1.6.0", 36 | "@types/node": "^17.0.21", 37 | "@typescript-eslint/eslint-plugin": "^5.14.0", 38 | "@typescript-eslint/parser": "^5.14.0", 39 | "c8": "^7.11.0", 40 | "eslint": "^8.8.0", 41 | "fuse.js": "^6.5.3", 42 | "happy-dom": "^2.31.1", 43 | "prettier": "2.6.0", 44 | "shiki": "^0.10.1", 45 | "solid-app-router": "^0.2.1", 46 | "solid-js": "^1.3.2", 47 | "solid-transition-group": "0.0.8", 48 | "tinykeys": "^1.4.0", 49 | "ts-node": "^10.5.0", 50 | "typescript": "^4.6.2", 51 | "vite": "^2.7.10", 52 | "vite-plugin-solid": "^2.2.1", 53 | "vitest": "^0.3.2" 54 | }, 55 | "peerDependencies": { 56 | "fuse.js": "^6.5.3", 57 | "solid-js": "^1.3.2", 58 | "solid-transition-group": "0.0.8", 59 | "tinykeys": "^1.4.0" 60 | }, 61 | "files": [ 62 | "pkg-dist" 63 | ], 64 | "homepage": "https://solid-command-palette.vercel.app/", 65 | "repository": { 66 | "type": "git", 67 | "url": "github:itaditya/solid-command-palette" 68 | }, 69 | "bugs": "https://github.com/itaditya/solid-command-palette/issues", 70 | "contributors": [ 71 | { 72 | "name": "Aditya Agarwal", 73 | "email": "adityaa803@gmail.com", 74 | "url": "https://devadi.netlify.app" 75 | } 76 | ], 77 | "keywords": [ 78 | "palette", 79 | "actions", 80 | "command", 81 | "CMD+K", 82 | "quick menu", 83 | "search bar", 84 | "solidhack", 85 | "best_ecosystem" 86 | ] 87 | } 88 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | import { devices } from '@playwright/test'; 3 | 4 | const isCi = Boolean(process.env.CI); 5 | 6 | const baseURL = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000'; 7 | const reporter: PlaywrightTestConfig['reporter'] = isCi ? 'html' : 'list'; 8 | 9 | const config: PlaywrightTestConfig = { 10 | testDir: './e2e', 11 | timeout: 30 * 1000, 12 | expect: { 13 | timeout: 5000, 14 | }, 15 | forbidOnly: isCi, 16 | retries: isCi ? 1 : 0, 17 | reporter, 18 | use: { 19 | actionTimeout: 0, 20 | baseURL, 21 | trace: 'on-first-retry', 22 | }, 23 | projects: [ 24 | { 25 | name: 'chromium', 26 | use: { 27 | ...devices['Desktop Chrome'], 28 | }, 29 | }, 30 | { 31 | name: 'firefox', 32 | use: { 33 | ...devices['Desktop Firefox'], 34 | }, 35 | }, 36 | { 37 | name: 'webkit', 38 | use: { ...devices['Desktop Safari'] }, 39 | }, 40 | ], 41 | }; 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | singleAttributePerLine: true, 5 | }; 6 | -------------------------------------------------------------------------------- /public/images/branding/logo-dark-horizontal-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-dark-horizontal-large.png -------------------------------------------------------------------------------- /public/images/branding/logo-dark-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-dark-horizontal.png -------------------------------------------------------------------------------- /public/images/branding/logo-dark-stamp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/branding/logo-dark-vertical-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-dark-vertical-large.png -------------------------------------------------------------------------------- /public/images/branding/logo-dark-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-dark-vertical.png -------------------------------------------------------------------------------- /public/images/branding/logo-light-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-light-favicon.png -------------------------------------------------------------------------------- /public/images/branding/logo-light-horizontal-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-light-horizontal-large.png -------------------------------------------------------------------------------- /public/images/branding/logo-light-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-light-horizontal.png -------------------------------------------------------------------------------- /public/images/branding/logo-light-stamp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/branding/logo-light-vertical-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-light-vertical-large.png -------------------------------------------------------------------------------- /public/images/branding/logo-light-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/branding/logo-light-vertical.png -------------------------------------------------------------------------------- /public/images/command-palette-examples/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/command-palette-examples/devtools.png -------------------------------------------------------------------------------- /public/images/command-palette-examples/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/command-palette-examples/github.png -------------------------------------------------------------------------------- /public/images/command-palette-examples/linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/command-palette-examples/linear.png -------------------------------------------------------------------------------- /public/images/command-palette-examples/tailwind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/command-palette-examples/tailwind.png -------------------------------------------------------------------------------- /public/images/demo-minimal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itaditya/solid-command-palette/595a4a9c7cb7ee535c103669b788297bdbf004c3/public/images/demo-minimal.gif -------------------------------------------------------------------------------- /public/snippets/api-command-palette.html: -------------------------------------------------------------------------------- 1 |
import { CommandPalette } from 'solid-command-palette';
2 | 
3 | <CommandPalette
4 |   /* Placeholder for palette's search field */
5 |   searchPlaceholder="Search actions"
6 | />;
7 | 
-------------------------------------------------------------------------------- /public/snippets/api-define-action.html: -------------------------------------------------------------------------------- 1 |
import { defineAction } from 'solid-command-palette';
 2 | 
 3 | export const myAction = defineAction({
 4 |   /* Unique identifier for your action */
 5 |   id: 'my-action',
 6 | 
 7 |   /* Nest current action inside a parent action */
 8 |   parentActionId: 'my-parent-action',
 9 | 
10 |   /* Short heading. Visible and Searchable */
11 |   title: 'My Action',
12 | 
13 |   /* Long description. Visible and Searchable */
14 |   subtitle: 'My Action',
15 | 
16 |   /* Other words to identify action. Hidden but Searchable */
17 |   keywords: ['example', 'command'],
18 | 
19 |   /* Keyboard shortcut for action. $mod = Cmd (Mac) / Ctrl (Win) */
20 |   shortcut: '$mod+e',
21 | 
22 |   /* Condition for allowing action */
23 |   cond: ({ actionId, rootContext, dynamicContext }) => {
24 |     const isAllowed = someLogic({ rootContext, dynamicContext });
25 |     return isAllowed;
26 |   },
27 | 
28 |   /* Code to execute when action is triggered */
29 |   run: ({ actionId, rootContext, dynamicContext }) => {
30 |     console.log('Run', actionId);
31 |     rootContext.incrementCount();
32 |     rootContext.deleteMessage(dynamicContext.messageId);
33 |   },
34 | });
35 | 
-------------------------------------------------------------------------------- /public/snippets/api-root.html: -------------------------------------------------------------------------------- 1 |
import { Root } from 'solid-command-palette';
 2 | 
 3 | <Root
 4 |   /* map of all actions */
 5 |   actions={{
 6 |     [myAction.id]: myAction,
 7 |     /* ... other actions ... */
 8 |   }}
 9 | 
10 |   /* share App's signals and methods with actions */
11 |   actionsContext={{
12 |     count,
13 |     incrementCount,
14 |     deleteMessage,
15 |   }}
16 | 
17 |   /* Custom components to render in palette */
18 |   components={{
19 |     /* content for action in search result list */
20 |     ResultContent,
21 |   }}
22 | />
23 | 
-------------------------------------------------------------------------------- /public/snippets/installation-define-actions.html: -------------------------------------------------------------------------------- 1 |
import { defineAction } from 'solid-command-palette';
 2 | 
 3 | const minimalAction = defineAction({
 4 |   id: 'minimal',
 5 |   title: 'Minimal Action',
 6 |   run: () => {
 7 |     console.log('ran minimal action');
 8 |   },
 9 | });
10 | 
11 | const incrementCounterAction = defineAction({
12 |   id: 'increment-counter',
13 |   title: 'Increment Counter by 1',
14 |   subtitle: 'Press CMD + E to trigger this.',
15 |   shortcut: '$mod+e', // $mod = Command on Mac & Control on Windows.
16 |   run: ({ rootContext }) => {
17 |     rootContext.increment();
18 |   },
19 | });
20 | 
21 | export const actions = {
22 |   [minimalAction.id]: minimalAction,
23 |   [incrementCounterAction.id]: incrementCounterAction,
24 | };
25 | 
-------------------------------------------------------------------------------- /public/snippets/installation-integrate-components.html: -------------------------------------------------------------------------------- 1 |
import { Root, CommandPalette } from 'solid-command-palette';
 2 | import { actions } from './actions';
 3 | import 'solid-command-palette/pkg-dist/style.css';
 4 | 
 5 | const App = () => {
 6 |   const actionsContext = {
 7 |     increment() {
 8 |       console.log('increment count state by 1');
 9 |     },
10 |   };
11 | 
12 |   return (
13 |     <div class="my-app">
14 |       <Root
15 |         actions={actions}
16 |         actionsContext={actionsContext}
17 |       >
18 |         <CommandPalette />
19 |       </Root>
20 |     </div>
21 |   );
22 | };
23 | 
-------------------------------------------------------------------------------- /public/snippets/installation-packages.html: -------------------------------------------------------------------------------- 1 |
# Core Library
2 | npm install solid-command-palette
3 | 
4 | # Peer Dependencies
5 | npm install solid-transition-group tinykeys fuse.js
6 | 
-------------------------------------------------------------------------------- /public/snippets/installation-setup.html: -------------------------------------------------------------------------------- 1 |
# Core Library
2 | npm install solid-command-palette
3 | 
4 | # Peer Dependencies
5 | npm install solid-transition-group tinykeys fuse.js
6 | 
-------------------------------------------------------------------------------- /scripts/generateSnippets.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs/promises'; 3 | import { getHighlighter, Highlighter } from 'shiki'; 4 | 5 | type Snippet = { 6 | id: string; 7 | lang: string; 8 | }; 9 | type SnippetList = Array; 10 | 11 | async function getSnippetList() { 12 | const snippetFolderPath = path.join(process.cwd(), 'snippets'); 13 | const snippetFilePaths = await fs.readdir(snippetFolderPath); 14 | const snippetIdList: SnippetList = []; 15 | 16 | snippetFilePaths.forEach((file) => { 17 | const fileName = path.basename(file); 18 | const [id, lang] = fileName.split('.snippet.'); 19 | 20 | if (id && lang) { 21 | snippetIdList.push({ 22 | id, 23 | lang, 24 | }); 25 | } 26 | }); 27 | 28 | return snippetIdList; 29 | } 30 | 31 | async function readPlainSnippet(snippet: Snippet) { 32 | const { id, lang } = snippet; 33 | const snippetFilePath = path.join(process.cwd(), 'snippets', `${id}.snippet.${lang}`); 34 | const plainSnippet = await fs.readFile(snippetFilePath, 'utf8'); 35 | return plainSnippet; 36 | } 37 | 38 | async function writeHighlightedSnippet(snippet: Snippet, highlightedSnippet: string) { 39 | const snippetFilePath = path.join(process.cwd(), 'public', 'snippets', `${snippet.id}.html`); 40 | await fs.writeFile(snippetFilePath, highlightedSnippet, 'utf8'); 41 | } 42 | 43 | async function generateSnippet(highlighter: Highlighter, snippet: Snippet) { 44 | const plainSnippet = await readPlainSnippet(snippet); 45 | const highlightedSnippet = highlighter.codeToHtml(plainSnippet, { 46 | lang: snippet.lang, 47 | }); 48 | await writeHighlightedSnippet(snippet, highlightedSnippet); 49 | } 50 | 51 | async function generateSnippets() { 52 | const highlighter = await getHighlighter({ 53 | theme: 'github-dark', 54 | langs: ['tsx', 'sh'], 55 | }); 56 | 57 | const snippetList = await getSnippetList(); 58 | 59 | const promises = snippetList.map((snippet) => { 60 | return generateSnippet(highlighter, snippet); 61 | }); 62 | 63 | await Promise.all(promises); 64 | console.log('Snippets generated!\n', snippetList); 65 | } 66 | 67 | generateSnippets(); 68 | -------------------------------------------------------------------------------- /snippets/api-command-palette.snippet.tsx: -------------------------------------------------------------------------------- 1 | import { CommandPalette } from 'solid-command-palette'; 2 | 3 | ; 7 | -------------------------------------------------------------------------------- /snippets/api-define-action.snippet.tsx: -------------------------------------------------------------------------------- 1 | import { defineAction } from 'solid-command-palette'; 2 | 3 | export const myAction = defineAction({ 4 | /* Unique identifier for your action */ 5 | id: 'my-action', 6 | 7 | /* Nest current action inside a parent action */ 8 | parentActionId: 'my-parent-action', 9 | 10 | /* Short heading. Visible and Searchable */ 11 | title: 'My Action', 12 | 13 | /* Long description. Visible and Searchable */ 14 | subtitle: 'My Action', 15 | 16 | /* Other words to identify action. Hidden but Searchable */ 17 | keywords: ['example', 'command'], 18 | 19 | /* Keyboard shortcut for action. $mod = Cmd (Mac) / Ctrl (Win) */ 20 | shortcut: '$mod+e', 21 | 22 | /* Condition for allowing action */ 23 | cond: ({ actionId, rootContext, dynamicContext }) => { 24 | const isAllowed = someLogic({ rootContext, dynamicContext }); 25 | return isAllowed; 26 | }, 27 | 28 | /* Code to execute when action is triggered */ 29 | run: ({ actionId, rootContext, dynamicContext }) => { 30 | console.log('Run', actionId); 31 | rootContext.incrementCount(); 32 | rootContext.deleteMessage(dynamicContext.messageId); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /snippets/api-root.snippet.tsx: -------------------------------------------------------------------------------- 1 | import { Root } from 'solid-command-palette'; 2 | 3 | 23 | -------------------------------------------------------------------------------- /snippets/installation-define-actions.snippet.tsx: -------------------------------------------------------------------------------- 1 | import { defineAction } from 'solid-command-palette'; 2 | 3 | const minimalAction = defineAction({ 4 | id: 'minimal', 5 | title: 'Minimal Action', 6 | run: () => { 7 | console.log('ran minimal action'); 8 | }, 9 | }); 10 | 11 | const incrementCounterAction = defineAction({ 12 | id: 'increment-counter', 13 | title: 'Increment Counter by 1', 14 | subtitle: 'Press CMD + E to trigger this.', 15 | shortcut: '$mod+e', // $mod = Command on Mac & Control on Windows. 16 | run: ({ rootContext }) => { 17 | rootContext.increment(); 18 | }, 19 | }); 20 | 21 | export const actions = { 22 | [minimalAction.id]: minimalAction, 23 | [incrementCounterAction.id]: incrementCounterAction, 24 | }; 25 | -------------------------------------------------------------------------------- /snippets/installation-integrate-components.snippet.tsx: -------------------------------------------------------------------------------- 1 | import { Root, CommandPalette } from 'solid-command-palette'; 2 | import { actions } from './actions'; 3 | import 'solid-command-palette/pkg-dist/style.css'; 4 | 5 | const App = () => { 6 | const actionsContext = { 7 | increment() { 8 | console.log('increment count state by 1'); 9 | }, 10 | }; 11 | 12 | return ( 13 |
14 | 18 | 19 | 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /snippets/installation-packages.snippet.sh: -------------------------------------------------------------------------------- 1 | # Core Library 2 | npm install solid-command-palette 3 | 4 | # Peer Dependencies 5 | npm install solid-transition-group tinykeys fuse.js 6 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import { Component, lazy } from 'solid-js'; 2 | import { RouteDefinition, useRoutes } from 'solid-app-router'; 3 | import AppShellView from './views/app/AppShell.view'; 4 | import DocsShellView from './views/docs/DocsShell/DocsShell.view'; 5 | import DocsView from './views/docs/Docs.view'; 6 | import ApiView from './views/docs/Api/Api.view'; 7 | import HomeView from './views/home/Home.view'; 8 | 9 | const routes: Array = [ 10 | { 11 | path: '/', 12 | component: AppShellView, 13 | children: [ 14 | { 15 | path: '/docs', 16 | component: DocsShellView, 17 | children: [ 18 | { 19 | path: '/', 20 | component: DocsView, 21 | }, 22 | { 23 | path: '/overview', 24 | component: lazy(() => import('./views/docs/introduction/Overview.view')), 25 | }, 26 | { 27 | path: '/installation', 28 | component: lazy(() => import('./views/docs/introduction/Installation.view')), 29 | }, 30 | { 31 | path: '/api', 32 | children: [ 33 | { path: '/', component: ApiView }, 34 | { 35 | path: '/define-action', 36 | component: lazy(() => import('./views/docs/Api/ApiDefineAction.view')), 37 | }, 38 | { path: '/root', component: lazy(() => import('./views/docs/Api/ApiRoot.view')) }, 39 | { 40 | path: '/command-palette', 41 | component: lazy(() => import('./views/docs/Api/ApiCommandPalette.view')), 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | { 48 | path: '/demo', 49 | component: lazy(() => import('./views/demo/Demo.view')), 50 | }, 51 | { 52 | path: '/', 53 | component: HomeView, 54 | }, 55 | ], 56 | }, 57 | ]; 58 | 59 | const App: Component = () => { 60 | const Routes = useRoutes(routes); 61 | 62 | return ; 63 | }; 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'solid-js/web'; 2 | import { Router } from 'solid-app-router'; 3 | 4 | import App from './App'; 5 | 6 | const appRender = () => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | const rootElem = document.getElementById('root') as HTMLElement; 15 | 16 | render(appRender, rootElem); 17 | -------------------------------------------------------------------------------- /src/app/shared/Loader/Loader.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: contents; 3 | } 4 | 5 | .loader { 6 | color: var(--brand-accent-color-5); 7 | animation-name: anim-rotating-loader; 8 | animation-duration: 600ms; 9 | animation-timing-function: linear; 10 | animation-iteration-count: infinite; 11 | animation-fill-mode: forwards; 12 | } 13 | 14 | .loader[data-size='normal'] { 15 | font-size: 2rem; 16 | } 17 | 18 | .loader[data-size='large'] { 19 | font-size: 4rem; 20 | } 21 | 22 | @keyframes anim-rotating-loader { 23 | from { 24 | transform: rotate(0turn); 25 | } 26 | 27 | to { 28 | transform: rotate(1turn); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import utilStyles from '../../utils.module.css'; 3 | import styles from './Loader.module.css'; 4 | 5 | export interface LoaderProps { 6 | size?: 'normal' | 'large'; 7 | } 8 | 9 | export const Loader: Component = (p) => { 10 | return ( 11 | 15 | 24 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/utils.module.css: -------------------------------------------------------------------------------- 1 | .visuallyHidden { 2 | position: absolute !important; 3 | width: 1px !important; 4 | height: 1px !important; 5 | padding: 0 !important; 6 | margin: -1px !important; 7 | overflow: hidden !important; 8 | clip: rect(0, 0, 0, 0) !important; 9 | white-space: nowrap !important; 10 | word-wrap: normal !important; 11 | border: 0 !important; 12 | } 13 | 14 | .stripSpace { 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | .sizeIconWithFont { 20 | width: 1em; 21 | height: 1em; 22 | } 23 | 24 | .demoAction { 25 | display: inline-flex; 26 | align-items: center; 27 | gap: 10px; 28 | background-color: var(--brand-fg-color); 29 | color: var(--brand-bg-color); 30 | border: none; 31 | border-radius: 4px; 32 | box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 33 | font-size: 1rem; 34 | font-family: inherit; 35 | text-decoration: none; 36 | cursor: pointer; 37 | padding: 10px 20px; 38 | outline-offset: 3px; 39 | transition: all 0.2s ease-out; 40 | } 41 | 42 | .demoAction[data-size='large'] { 43 | gap: 15px; 44 | font-size: 2rem; 45 | padding: 10px 15px; 46 | border-radius: 6px; 47 | } 48 | 49 | .demoAction:hover { 50 | transform: scale(1.01); 51 | } 52 | 53 | .demoAction:active { 54 | background-color: var(--brand-accent-color-6); 55 | } 56 | 57 | .nonFlickerLoader { 58 | visibility: hidden; 59 | animation-name: anim-non-flicker-loader; 60 | animation-delay: 500ms; 61 | animation-fill-mode: forwards; 62 | } 63 | 64 | @keyframes anim-non-flicker-loader { 65 | to { 66 | visibility: visible; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/views/app/AppShell.module.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :global html { 6 | overscroll-behavior: contain; /* disable Pull to Refresh in Firefox */ 7 | } 8 | 9 | :global body { 10 | --brand-fg-color: #000; 11 | --brand-bg-color: #fff; 12 | --brand-accent-color-1: #fafafa; 13 | --brand-accent-color-2: #eaeaea; 14 | --brand-accent-color-3: #999; 15 | --brand-accent-color-4: #73768c; 16 | --brand-accent-color-5: #666; 17 | --brand-accent-color-6: #454754; 18 | --brand-accent-color-7: #333; 19 | --brand-accent-color-8: #111; 20 | --brand-body-font: 'Nunito', sans-serif; 21 | 22 | margin: 0; 23 | padding: 0; 24 | font-family: sans-serif; 25 | overscroll-behavior: contain; /* disable Pull to Refresh in Chrome */ 26 | } 27 | 28 | :global #root { 29 | outline-color: var(--brand-accent-color-8); 30 | font-family: var(--brand-body-font); 31 | } 32 | 33 | .wrapper { 34 | display: flex; 35 | flex-direction: column; 36 | min-height: 100vh; 37 | background-color: var(--brand-bg-color); 38 | } 39 | 40 | .header { 41 | display: flex; 42 | align-items: center; 43 | gap: 40px; 44 | padding: 0 30px; 45 | } 46 | 47 | .heading { 48 | margin: 0; 49 | font-weight: 300; 50 | } 51 | 52 | .headingLink { 53 | display: flex; 54 | align-items: center; 55 | gap: 10px; 56 | color: var(--brand-accent-color-8); 57 | text-decoration: none; 58 | outline-offset: 6px; 59 | } 60 | 61 | .logoStamp { 62 | height: 50px; 63 | transition: all 0.2s ease-in; 64 | } 65 | 66 | .headingLink:hover .logoStamp { 67 | transform: scale(1.01); 68 | filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1)); 69 | } 70 | 71 | .logoText { 72 | display: flex; 73 | flex-direction: column; 74 | gap: 20px; 75 | line-height: 0; 76 | } 77 | 78 | .logoTextPrimary { 79 | font-size: 1.3rem; 80 | color: var(--brand-accent-color-6); 81 | } 82 | 83 | .logoTextSecondary { 84 | font-size: 0.9rem; 85 | color: var(--brand-accent-color-4); 86 | padding-inline-start: 2px; 87 | } 88 | 89 | .navWrapper { 90 | margin-inline-start: auto; 91 | } 92 | 93 | .navList, 94 | .socialList { 95 | list-style: none; 96 | display: flex; 97 | gap: 20px; 98 | } 99 | 100 | .navLink { 101 | display: flex; 102 | align-items: center; 103 | justify-content: center; 104 | font-size: 1rem; 105 | text-decoration: none; 106 | padding-block: 15px; 107 | outline: none; 108 | } 109 | 110 | .navLinkContent { 111 | color: var(--brand-fg-color); 112 | padding: 10px 15px; 113 | border: 2px solid transparent; 114 | border-radius: 4px; 115 | transition: all 0.2s ease-out; 116 | } 117 | 118 | .navLink:not(.activeNavLink):hover .navLinkContent { 119 | background-color: var(--brand-accent-color-2); 120 | } 121 | 122 | .navLink:focus-visible .navLinkContent { 123 | border-color: var(--brand-accent-color-8); 124 | } 125 | 126 | .activeNavLink .navLinkContent { 127 | background-color: var(--brand-accent-color-7); 128 | color: var(--brand-bg-color); 129 | } 130 | 131 | .socialList { 132 | align-items: center; 133 | } 134 | 135 | .socialItem { 136 | line-height: 0; 137 | } 138 | 139 | .socialLink { 140 | display: block; 141 | border-radius: 100%; 142 | text-decoration: none; 143 | color: var(--brand-accent-color-5); 144 | font-size: 2rem; 145 | outline-offset: 5px; 146 | transition: all 0.3s ease-out; 147 | } 148 | 149 | .socialLink:hover { 150 | transform: scale(1.05); 151 | color: var(--brand-accent-color-6); 152 | } 153 | 154 | .main { 155 | display: flex; 156 | flex-direction: column; 157 | flex: 1; 158 | padding-block-end: 100px; 159 | padding-inline: 30px; 160 | border-block: 1px solid var(--brand-accent-color-2); 161 | background-color: var(--brand-accent-color-1); 162 | } 163 | 164 | .footer { 165 | padding: 20px; 166 | display: flex; 167 | justify-content: center; 168 | } 169 | 170 | .creditLink { 171 | color: var(--brand-accent-color-7); 172 | margin-inline-start: 5px; 173 | text-decoration: none; 174 | font-weight: bold; 175 | } 176 | -------------------------------------------------------------------------------- /src/app/views/app/AppShell.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Show } from 'solid-js'; 2 | import { Link, NavLink, NavLinkProps, Outlet, useMatch, useNavigate } from 'solid-app-router'; 3 | import { Root, CommandPalette } from '../../../lib'; 4 | import { SocialIcon, socialsData } from './SocialIcons'; 5 | import { actions } from './actions'; 6 | import utilStyles from '../../utils.module.css'; 7 | import styles from './AppShell.module.css'; 8 | 9 | const HeaderNavLink: Component = (p) => { 10 | return ( 11 | 16 | {p.children} 17 | 18 | ); 19 | }; 20 | 21 | const Main: Component = () => { 22 | const isDemo = useMatch(() => '/demo'); 23 | const navigate = useNavigate(); 24 | 25 | const actionsContext = { 26 | navigate, 27 | }; 28 | 29 | return ( 30 |
31 | } 34 | > 35 | 39 | 40 | 41 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | const AppShellView: Component = () => { 48 | return ( 49 |
50 |
51 |

52 | 56 | 61 | 62 | cmd palette 63 | for Solid.js 64 | 65 | 66 |

67 | 77 |
78 |
    79 |
  • 80 | 84 |
  • 85 |
  • 86 | 90 |
  • 91 |
92 |
93 |
94 |
95 | 106 |
107 | ); 108 | }; 109 | 110 | export default AppShellView; 111 | -------------------------------------------------------------------------------- /src/app/views/app/SocialIcons.tsx: -------------------------------------------------------------------------------- 1 | import { Component, JSX, splitProps } from 'solid-js'; 2 | import utilStyles from '../../utils.module.css'; 3 | 4 | export const socialsData = { 5 | github: { 6 | href: 'https://github.com/itaditya/solid-command-palette', 7 | alt: 'Navigate to GitHub', 8 | icon: 'M12 .3a12 12 0 00-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.07 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 016 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 011.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0012 .3', 9 | }, 10 | twitter: { 11 | href: 'https://twitter.com/dev__adi', 12 | alt: 'Navigate to Twitter', 13 | icon: 'M12,0.1c-6.7,0-12,5.3-12,12s5.3,12,12,12s12-5.4,12-12S18.6,0.1,12,0.1z M17,9.4v0.4c0,3.8-2.6,8-7.5,8 c-1.5,0-2.9-0.5-4.1-1.3c0.2,0,0.4,0,0.7,0c1.2,0,2.3-0.5,3.3-1.2c-1.1,0-2.1-0.8-2.4-2c0.2,0.1,0.3,0.1,0.5,0.1 c0.2,0,0.5-0.1,0.7-0.1C6.9,13,6,11.9,6,10.5v-0.1c0.3,0.2,0.8,0.4,1.2,0.4c-0.7-0.5-1.2-1.4-1.2-2.3c0-0.5,0.1-1.1,0.3-1.4 c1.3,1.7,3.2,2.8,5.4,2.9c-0.1-0.2-0.1-0.4-0.1-0.6c0-1.6,1.2-2.8,2.7-2.8c0.8,0,1.4,0.3,1.9,0.9C17,7.3,17.6,7,18.1,6.7 c-0.2,0.7-0.6,1.2-1.1,1.6c0.5-0.1,1-0.2,1.5-0.4C18.1,8.4,17.6,8.9,17,9.4z', 14 | }, 15 | }; 16 | 17 | export interface Props extends JSX.HTMLAttributes { 18 | href: string; 19 | alt: string; 20 | icon: string; 21 | } 22 | 23 | export const SocialIcon: Component = (p) => { 24 | const [l, others] = splitProps(p, ['alt', 'icon']); 25 | 26 | return ( 27 | 32 | {l.alt} 33 | 37 | 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/app/views/app/actions.ts: -------------------------------------------------------------------------------- 1 | import { defineAction } from '../../../lib'; 2 | 3 | const homeAction = defineAction({ 4 | id: 'navigate-home', 5 | title: 'Go to Home', 6 | subtitle: 'Navigate to the homepage.', 7 | run({ rootContext }) { 8 | if (typeof rootContext.navigate === 'function') { 9 | rootContext.navigate('/'); 10 | } 11 | }, 12 | }); 13 | 14 | const docsAction = defineAction({ 15 | id: 'navigate-docs', 16 | title: 'Go to Docs', 17 | subtitle: 'Read the documentation for Solid Command Palette', 18 | keywords: ['api', 'install', 'start', 'overview'], 19 | shortcut: 'g d', 20 | run({ rootContext }) { 21 | if (typeof rootContext.navigate === 'function') { 22 | rootContext.navigate('/docs'); 23 | } 24 | }, 25 | }); 26 | 27 | const demoAction = defineAction({ 28 | id: 'navigate-demo', 29 | title: 'Try the Demo', 30 | subtitle: 'Explore the demo which showcases all features.', 31 | run({ rootContext }) { 32 | if (typeof rootContext.navigate === 'function') { 33 | rootContext.navigate('/demo'); 34 | } 35 | }, 36 | }); 37 | 38 | const githubAction = defineAction({ 39 | id: 'navigate-github', 40 | title: 'Go to GitHub repo', 41 | keywords: ['oss', 'source', 'code'], 42 | shortcut: 'g h', 43 | run: () => { 44 | window.open( 45 | 'https://github.com/itaditya/solid-command-palette', 46 | '_blank', 47 | 'noopener noreferrer' 48 | ); 49 | }, 50 | }); 51 | 52 | const npmAction = defineAction({ 53 | id: 'navigate-npm', 54 | title: 'Go to npm package', 55 | keywords: ['oss', 'package', 'pkg'], 56 | shortcut: 'g n', 57 | run: () => { 58 | window.open( 59 | 'https://www.npmjs.com/package/solid-command-palette', 60 | '_blank', 61 | 'noopener noreferrer' 62 | ); 63 | }, 64 | }); 65 | 66 | export const actions = { 67 | [homeAction.id]: homeAction, 68 | [docsAction.id]: docsAction, 69 | [demoAction.id]: demoAction, 70 | [githubAction.id]: githubAction, 71 | [npmAction.id]: npmAction, 72 | }; 73 | -------------------------------------------------------------------------------- /src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.module.css: -------------------------------------------------------------------------------- 1 | .resultContent { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | cursor: pointer; 6 | user-select: none; 7 | padding-block: 10px; 8 | padding-inline: var(--scp-gutter-space-inline); 9 | font-family: var(--brand-body-font); 10 | border-radius: 8px; 11 | } 12 | 13 | .resultContent.active { 14 | outline: 1px solid var(--brand-accent-color-8); 15 | outline-offset: -5px; 16 | } 17 | 18 | .resultTitle { 19 | color: var(--brand-accent-color-6); 20 | font-weight: 700; 21 | } 22 | 23 | .resultSubtitle { 24 | color: var(--brand-accent-color-5); 25 | font-size: 12px; 26 | font-weight: 400; 27 | margin-block-start: 5px; 28 | } 29 | 30 | .resultShortcut { 31 | font-size: 0.7rem; 32 | font-family: var(--brand-body-font); 33 | font-weight: 700; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/views/demo/CustomComponentsDemo/CustomComponentsDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Show } from 'solid-js'; 2 | import { KbdShortcut, ResultContentProps } from '../../../../lib'; 3 | import utilStyles from '../../../utils.module.css'; 4 | import styles from './CustomComponentsDemo.module.css'; 5 | 6 | export const DemoResultContent: Component = (p) => { 7 | return ( 8 |
14 |
15 |

{p.action.title}

16 | 17 |

{p.action.subtitle}

18 |
19 |
20 |
21 | 22 | {(shortcut) => ( 23 | 27 | )} 28 | 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/views/demo/CustomComponentsDemo/components.ts: -------------------------------------------------------------------------------- 1 | import { DemoResultContent } from './CustomComponentsDemo'; 2 | 3 | export const components = { 4 | ResultContent: DemoResultContent, 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/views/demo/Demo.module.css: -------------------------------------------------------------------------------- 1 | .demoWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 100px; 5 | } 6 | 7 | .introSection { 8 | min-height: 40vh; 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | gap: 30px; 14 | padding-block-start: 10vh; 15 | } 16 | 17 | .introHeading { 18 | display: contents; 19 | font-size: 3.5rem; 20 | font-weight: 300; 21 | text-align: center; 22 | color: var(--brand-accent-color-3); 23 | } 24 | 25 | .introShortcutKey { 26 | min-width: 85px; 27 | color: var(--brand-accent-color-4); 28 | font-weight: 600; 29 | padding: 0; 30 | } 31 | 32 | .countValue { 33 | font-size: 6rem; 34 | color: var(--brand-accent-color-3); 35 | font-variant-numeric: tabular-nums; 36 | } 37 | 38 | .muteLabel:focus-within { 39 | outline: 2px solid var(--brand-accent-color-8); 40 | outline-offset: 5px; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/views/demo/Demo.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createSignal, Show } from 'solid-js'; 2 | import { useSearchParams } from 'solid-app-router'; 3 | import { Root, CommandPalette, KbdShortcut } from '../../../lib'; 4 | import { actions } from './actions'; 5 | import { NestedActionDemo } from './NestedActionDemo/NestedActionDemo'; 6 | import { DynamicActionContextDemo } from './DynamicActionContextDemo/DynamicActionContextDemo'; 7 | import { components } from './CustomComponentsDemo/components'; 8 | import { Profile } from './types'; 9 | import utilStyles from '../../utils.module.css'; 10 | import demoStyles from './demoUtils.module.css'; 11 | import styles from './Demo.module.css'; 12 | 13 | const DemoView: Component = () => { 14 | const [count, setCount] = createSignal(0); 15 | const [muted, setMuted] = createSignal(false); 16 | const [profile, setProfile] = createSignal('personal'); 17 | const [searchParams] = useSearchParams(); 18 | 19 | const increment = () => { 20 | setCount((prev) => (prev += 1)); 21 | }; 22 | 23 | const unmuteAudio = () => { 24 | setMuted(false); 25 | }; 26 | 27 | const handleMuteInput = () => { 28 | setMuted((old) => !old); 29 | }; 30 | 31 | const handleProfileChange = (event: Event) => { 32 | const targetElem = event.currentTarget as HTMLSelectElement; 33 | 34 | const newProfile = targetElem.value as Profile; 35 | setProfile(newProfile); 36 | }; 37 | 38 | const getCustomProps = () => { 39 | const features = searchParams.feat || []; 40 | 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | const props: Record = {}; 43 | 44 | if (features.includes('components')) { 45 | props.components = components; 46 | } 47 | 48 | if (features.includes('placeholder')) { 49 | props.placeholder = 'Search for an action...'; 50 | } 51 | 52 | return props; 53 | }; 54 | 55 | const actionsContext = { 56 | increment, 57 | muted, 58 | unmuteAudio, 59 | setProfile, 60 | }; 61 | 62 | const customProps = getCustomProps(); 63 | 64 | return ( 65 | 70 |
71 | 72 |
73 |

74 | Bring it up by pressing 75 | 79 |

80 |
81 |
82 |
83 |

Controlling application state

84 |

85 | We have a count signal and an increment function to 86 | increase its value by 1. 87 |

88 |

You can trigger it by clicking on the button below it.

89 |

90 | We have also bound this increment function to our first action and a keyboard shortcut 91 |

92 |
93 |
94 |

99 | Current count is 100 | {count()} 101 |

102 | 108 |

109 | Try holding 110 |

111 |
112 |
113 |
114 |
115 |

Conditionally enable actions

116 |

117 | The Unmute Audio action is only enabled when the muted signal has 118 | value true. 119 |

120 |

121 | The action's cond & run functions can use latest 122 | application state to enable actions or change behavior. 123 |

124 |
125 |
126 |
127 | 148 |
149 | 150 |

151 | Press to unmute. 152 |

153 |
154 |
155 |
156 | 160 | 161 |
162 |
163 | ); 164 | }; 165 | 166 | export default DemoView; 167 | -------------------------------------------------------------------------------- /src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.module.css: -------------------------------------------------------------------------------- 1 | .contactsWrapper { 2 | display: flex; 3 | } 4 | 5 | .contactList { 6 | margin: 0; 7 | padding: 0; 8 | list-style: none; 9 | } 10 | 11 | .contactList:focus-within { 12 | background-color: rgb(0 0 0 / 0.05); 13 | } 14 | 15 | .contactItem { 16 | user-select: none; 17 | position: relative; 18 | color: var(--brand-accent-color-7); 19 | border-inline-start: 2px solid transparent; 20 | transition: all 0.1s ease-out; 21 | } 22 | 23 | .contactItem:not(.active):hover { 24 | transform: translateX(5px); 25 | } 26 | 27 | .contactItem.active { 28 | border-color: var(--brand-accent-color-3); 29 | } 30 | 31 | .contactLabel { 32 | display: block; 33 | padding-block: 15px; 34 | padding-inline: 15px 30px; 35 | cursor: pointer; 36 | } 37 | 38 | .contactInput { 39 | opacity: 0; 40 | position: absolute; 41 | inset-block-start: 0; 42 | } 43 | 44 | .contactDetails { 45 | padding: 10px 20px; 46 | width: 25ch; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/views/demo/DynamicActionContextDemo/DynamicActionContextDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createMemo, createSignal, createUniqueId, For, Show } from 'solid-js'; 2 | import { KbdShortcut, createSyncActionsContext } from '../../../../lib'; 3 | import { ownContactId, contacts, contactActionId } from './data'; 4 | import { InputEventHandler, ContactItemProps, ReceiverContactDetailsProps } from './types'; 5 | import demoStyles from '../demoUtils.module.css'; 6 | import utilStyles from '../../../utils.module.css'; 7 | import styles from './DynamicActionContextDemo.module.css'; 8 | 9 | const ContactItem: Component = (p) => { 10 | const inputId = createUniqueId(); 11 | 12 | return ( 13 |
  • 19 | 25 | 34 |
  • 35 | ); 36 | }; 37 | 38 | const ReceiverContactDetails: Component = (p) => { 39 | createSyncActionsContext(contactActionId, () => { 40 | return { 41 | receiverContactId: p.contactId(), 42 | }; 43 | }); 44 | 45 | return ( 46 |
    47 |

    Receiver Details

    48 |

    {p.contactData().details}

    49 |
    50 | ); 51 | }; 52 | 53 | export const DynamicActionContextDemo: Component = () => { 54 | const [activeContactId, setActiveContactId] = createSignal(ownContactId); 55 | 56 | const activeContactData = createMemo(() => { 57 | const activeContactIdValue = activeContactId(); 58 | const activeContactData = contacts[activeContactIdValue]; 59 | 60 | return activeContactData; 61 | }); 62 | 63 | const handleInput: InputEventHandler = (event) => { 64 | const newValue = event.currentTarget.value; 65 | setActiveContactId(newValue); 66 | }; 67 | 68 | function renderOwnDetails() { 69 | return ( 70 |
    71 |

    Personal Details

    72 |

    {activeContactData().details}

    73 |
    74 | ); 75 | } 76 | 77 | return ( 78 |
    79 |
    80 |

    Dynamically set action context

    81 |

    82 | When you select Andrew or Tobey, the ReceiverContactDetails is rendered. 83 |

    84 |

    85 | While this component is rendered, the profile details are shared using dynamic action 86 | context. 87 |

    88 |

    89 | Try the Send Message to Contact action once after selecting each profile. 90 |

    91 |

    92 | If you have Andrew's or Tobey's profile already opened, their contact id will already be 93 | taken and you'll skip one step. 94 |

    95 |
    96 |
    97 |
    98 | 112 |
    113 | 117 | 121 | 122 |
    123 |
    124 |

    125 | Try pressing 126 |

    127 |
    128 |
    129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /src/app/views/demo/DynamicActionContextDemo/data.ts: -------------------------------------------------------------------------------- 1 | import { ContactId, ContactsMap } from './types'; 2 | 3 | export const ownContactId: ContactId = 'contact-tom'; 4 | 5 | export const contacts: ContactsMap = { 6 | 'contact-tom': { 7 | label: 'Me', 8 | details: 'I am the friendly neighbor-hood Spider-Man who fought Thanos.', 9 | }, 10 | 'contact-andrew': { 11 | label: 'Andrew', 12 | details: "He's the Amazing Spider-Man with deep regrets.", 13 | }, 14 | 'contact-tobey': { 15 | label: 'Tobey', 16 | details: "He's the youth pastor who shoots webbing out of his hands.", 17 | }, 18 | }; 19 | 20 | export const contactActionId = 'message-to-contact-action'; 21 | -------------------------------------------------------------------------------- /src/app/views/demo/DynamicActionContextDemo/dynamicContextActions.ts: -------------------------------------------------------------------------------- 1 | import { defineAction } from '../../../../lib'; 2 | import { contactActionId, contacts } from './data'; 3 | 4 | export const contactAction = defineAction({ 5 | id: contactActionId, 6 | title: 'Send Message to Contact', 7 | subtitle: `It'll not ask for Id if you're on a receiver's profile.`, 8 | shortcut: 'm', 9 | run: ({ dynamicContext }) => { 10 | let receiverContactId = dynamicContext.receiverContactId as string; 11 | 12 | if (!receiverContactId) { 13 | const contactId = prompt('Provide Contact Id of the receiver', ''); 14 | 15 | if (contactId) { 16 | receiverContactId = contactId; 17 | } 18 | } 19 | 20 | const contactLabel = contacts[receiverContactId]?.label || receiverContactId; 21 | const message = prompt(`Type the message for ${contactLabel}`, 'Hello there!'); 22 | alert(`${contactLabel} has been sent the following message:\n${message}`); 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/views/demo/DynamicActionContextDemo/types.ts: -------------------------------------------------------------------------------- 1 | import { JSX, Accessor } from 'solid-js'; 2 | 3 | export type ContactId = string; 4 | 5 | export type ContactData = { 6 | label: string; 7 | details: string; 8 | }; 9 | 10 | export type ContactsMap = Record; 11 | 12 | export type InputEventHandler = JSX.EventHandlerUnion; 13 | 14 | export type ContactItemProps = { 15 | contactId: ContactId; 16 | contactData: ContactData; 17 | isActive: boolean; 18 | onInput: InputEventHandler; 19 | }; 20 | 21 | export type ReceiverContactDetailsProps = { 22 | contactId: Accessor; 23 | contactData: Accessor; 24 | }; 25 | -------------------------------------------------------------------------------- /src/app/views/demo/NestedActionDemo/NestedActionDemo.module.css: -------------------------------------------------------------------------------- 1 | .profileMenu { 2 | appearance: none; 3 | font-size: 1rem; 4 | min-width: 150px; 5 | padding-block: 5px; 6 | padding-inline-start: 20px; 7 | padding-inline-end: 10px; 8 | border-radius: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/views/demo/NestedActionDemo/NestedActionDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { KbdShortcut } from '../../../../lib'; 3 | import { Profile } from '../types'; 4 | import demoStyles from '../demoUtils.module.css'; 5 | import utilStyles from '../../../utils.module.css'; 6 | import styles from './NestedActionDemo.module.css'; 7 | 8 | export interface Props { 9 | profile: Profile; 10 | onProfileChange: (event: Event) => void; 11 | } 12 | 13 | export const NestedActionDemo: Component = (p) => { 14 | return ( 15 |
    16 |
    17 |

    Nested actions

    18 |

    19 | When user selects an action but wants to further choose an option in it, this comes in 20 | handy. 21 |

    22 |

    23 | After selecting Change Profile action, user can choose between Personal & 24 | Work profiles. 25 |

    26 |
    27 |
    28 |

    29 | Active profile is {p.profile} 30 |

    31 | 40 |

    41 | Try / 42 |

    43 |
    44 |
    45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /src/app/views/demo/NestedActionDemo/nestedActions.ts: -------------------------------------------------------------------------------- 1 | import { defineAction } from '../../../../lib'; 2 | 3 | const setProfileAction = defineAction({ 4 | id: 'set-profile', 5 | title: 'Set profile', 6 | subtitle: 'Select this and then choose one of the options', 7 | }); 8 | 9 | const setToPersonalProfileAction = defineAction({ 10 | id: 'set-personal-profile', 11 | parentActionId: setProfileAction.id, 12 | title: 'Set to Personal profile', 13 | shortcut: 'p p', 14 | run: ({ rootContext }) => { 15 | if (typeof rootContext.setProfile === 'function') { 16 | rootContext.setProfile('personal'); 17 | } 18 | }, 19 | }); 20 | 21 | const setToWorkProfileAction = defineAction({ 22 | id: 'set-work-profile', 23 | parentActionId: setProfileAction.id, 24 | title: 'Set to Work profile', 25 | shortcut: 'p w', 26 | run: ({ rootContext }) => { 27 | if (typeof rootContext.setProfile === 'function') { 28 | rootContext.setProfile('work'); 29 | } 30 | }, 31 | }); 32 | 33 | const configureProfileAction = defineAction({ 34 | id: 'configure-profile', 35 | title: 'Configure profile', 36 | subtitle: 'Select this to try 2 levels of nested actions', 37 | }); 38 | 39 | const configurePersonalProfileAction = defineAction({ 40 | id: 'configure-personal-profile', 41 | parentActionId: configureProfileAction.id, 42 | title: 'Configure Personal profile', 43 | }); 44 | 45 | const toggleWifiPersonalAction = defineAction({ 46 | id: 'toggle-wifi-personal', 47 | parentActionId: configurePersonalProfileAction.id, 48 | title: 'Toggle Wi-Fi', 49 | subtitle: 'Configure Wi-Fi settings in Personal profile', 50 | run: () => { 51 | alert('Wifi in Personal profile has been toggled!'); 52 | }, 53 | }); 54 | 55 | const toggleAirplanePersonalAction = defineAction({ 56 | id: 'toggle-airplane-personal', 57 | parentActionId: configurePersonalProfileAction.id, 58 | title: 'Toggle Airplane mode', 59 | subtitle: 'Configure Airplane mode settings in Personal profile', 60 | run: () => { 61 | alert('Airplane mode in Personal profile has been toggled!'); 62 | }, 63 | }); 64 | 65 | const configureWorkProfileAction = defineAction({ 66 | id: 'configure-work-profile', 67 | parentActionId: configureProfileAction.id, 68 | title: 'Configure Work profile', 69 | }); 70 | 71 | const toggleWifiWorkAction = defineAction({ 72 | id: 'toggle-wifi-work', 73 | parentActionId: configureWorkProfileAction.id, 74 | title: 'Toggle Wi-Fi', 75 | subtitle: 'Configure Wi-Fi settings in Work profile', 76 | run: () => { 77 | alert('Wifi in Work profile has been toggled!'); 78 | }, 79 | }); 80 | 81 | const toggleAirplaneWorkAction = defineAction({ 82 | id: 'toggle-airplane-work', 83 | parentActionId: configureWorkProfileAction.id, 84 | title: 'Toggle Airplane mode', 85 | subtitle: 'Configure Airplane mode settings in Work profile', 86 | run: () => { 87 | alert('Airplane mode in Work profile has been toggled!'); 88 | }, 89 | }); 90 | 91 | export const nestedActionsConfig = { 92 | [setProfileAction.id]: setProfileAction, 93 | [setToPersonalProfileAction.id]: setToPersonalProfileAction, 94 | [setToWorkProfileAction.id]: setToWorkProfileAction, 95 | [configureProfileAction.id]: configureProfileAction, 96 | [configurePersonalProfileAction.id]: configurePersonalProfileAction, 97 | [toggleWifiPersonalAction.id]: toggleWifiPersonalAction, 98 | [toggleAirplanePersonalAction.id]: toggleAirplanePersonalAction, 99 | [configureWorkProfileAction.id]: configureWorkProfileAction, 100 | [toggleWifiWorkAction.id]: toggleWifiWorkAction, 101 | [toggleAirplaneWorkAction.id]: toggleAirplaneWorkAction, 102 | }; 103 | -------------------------------------------------------------------------------- /src/app/views/demo/actions.ts: -------------------------------------------------------------------------------- 1 | import { defineAction } from '../../../lib'; 2 | import { contactAction } from './DynamicActionContextDemo/dynamicContextActions'; 3 | import { nestedActionsConfig } from './NestedActionDemo/nestedActions'; 4 | 5 | const incrementCounterAction = defineAction({ 6 | id: 'increment-counter', 7 | title: 'Increment Counter by 1', 8 | subtitle: 'Hold the Command and E keys on Mac together to trigger this.', 9 | shortcut: '$mod+e', 10 | run: ({ rootContext }) => { 11 | if (typeof rootContext.increment === 'function') { 12 | rootContext.increment(); 13 | } 14 | }, 15 | }); 16 | 17 | const loggerAction = defineAction({ 18 | id: 'logger', 19 | title: 'Log a message in the console', 20 | keywords: ['logger', 'print'], 21 | run: () => { 22 | console.log('run logger action'); 23 | }, 24 | }); 25 | 26 | const unmuteAudioAction = defineAction({ 27 | id: 'unmute-audio', 28 | title: 'Unmute Audio', 29 | subtitle: 'Only shown when you have muted the audio', 30 | shortcut: '$mod+u', 31 | cond: ({ rootContext }) => { 32 | if (typeof rootContext.muted !== 'function') { 33 | return false; 34 | } 35 | 36 | return rootContext.muted(); 37 | }, 38 | run: ({ rootContext }) => { 39 | if (typeof rootContext.unmuteAudio === 'function') { 40 | rootContext.unmuteAudio(); 41 | } 42 | }, 43 | }); 44 | 45 | const navigationAction = defineAction({ 46 | id: 'navigate-github', 47 | title: 'Go to GitHub repo', 48 | subtitle: 'First press G then press H. No need to hold them together', 49 | keywords: ['oss', 'source', 'code'], 50 | shortcut: 'g h', 51 | run: () => { 52 | window.open( 53 | 'https://github.com/itaditya/solid-command-palette', 54 | '_blank', 55 | 'noopener noreferrer' 56 | ); 57 | }, 58 | }); 59 | 60 | export const actions = { 61 | [incrementCounterAction.id]: incrementCounterAction, 62 | [loggerAction.id]: loggerAction, 63 | [unmuteAudioAction.id]: unmuteAudioAction, 64 | ...nestedActionsConfig, 65 | [contactAction.id]: contactAction, 66 | [navigationAction.id]: navigationAction, 67 | }; 68 | -------------------------------------------------------------------------------- /src/app/views/demo/demoUtils.module.css: -------------------------------------------------------------------------------- 1 | .demoSection { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | gap: 30px; 6 | } 7 | 8 | .demoInteraction { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | gap: 20px; 13 | min-width: 300px; 14 | min-height: 150px; 15 | border: 1px solid var(--brand-accent-color-3); 16 | border-radius: 10px; 17 | padding-block: 30px; 18 | padding-inline: 50px; 19 | } 20 | 21 | .demoInteractionDesc { 22 | display: flex; 23 | align-items: center; 24 | gap: 10px; 25 | margin: 0; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/views/demo/types.ts: -------------------------------------------------------------------------------- 1 | export type Profile = 'personal' | 'work'; 2 | -------------------------------------------------------------------------------- /src/app/views/docs/Api/Api.view.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'solid-app-router'; 2 | import { Component } from 'solid-js'; 3 | 4 | const ApiView: Component = () => { 5 | return ; 6 | }; 7 | 8 | export default ApiView; 9 | -------------------------------------------------------------------------------- /src/app/views/docs/Api/ApiCommandPalette.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Snippet } from '../Snippet/Snippet'; 3 | import docsStyles from '../docsUtils.module.css'; 4 | 5 | const ApiCommandPaletteView: Component = () => { 6 | return ( 7 |
    8 |

    CommandPalette

    9 |

    10 | It renders the Command Palette in a portal where users can search for actions and trigger 11 | them. 12 |

    13 |

    Render it inside Root

    14 | 15 |
    16 | ); 17 | }; 18 | 19 | export default ApiCommandPaletteView; 20 | -------------------------------------------------------------------------------- /src/app/views/docs/Api/ApiDefineAction.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Link } from 'solid-app-router'; 3 | import { Snippet } from '../Snippet/Snippet'; 4 | import docsStyles from '../docsUtils.module.css'; 5 | 6 | const ApiRootView: Component = () => { 7 | return ( 8 |
    9 |

    defineAction

    10 |

    It helps you define an action with autocomplete suggestions.

    11 |

    12 | Related actions can be grouped in modules and exported. The combined map of actions is 13 | passed to Root 14 |

    15 | 16 |
    17 | ); 18 | }; 19 | 20 | export default ApiRootView; 21 | -------------------------------------------------------------------------------- /src/app/views/docs/Api/ApiRoot.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Snippet } from '../Snippet/Snippet'; 3 | import docsStyles from '../docsUtils.module.css'; 4 | 5 | const ApiRootView: Component = () => { 6 | return ( 7 |
    8 |

    Root

    9 |

    10 | It's responsible for wiring up the state provider, event listeners etc to make the Command 11 | Palette work. 12 |

    13 |

    Render it at the top of your application.

    14 | 15 |
    16 | ); 17 | }; 18 | 19 | export default ApiRootView; 20 | -------------------------------------------------------------------------------- /src/app/views/docs/Docs.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Navigate } from 'solid-app-router'; 3 | 4 | const DocsView: Component = () => { 5 | return ; 6 | }; 7 | 8 | export default DocsView; 9 | -------------------------------------------------------------------------------- /src/app/views/docs/DocsShell/DocsShell.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | flex: 1; 4 | gap: 100px; 5 | padding-top: 30px; 6 | } 7 | 8 | .wrapper :is(h1, h2, h3, h4, h5, h6, p, ul, ol) { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | .wrapper :is(ul, ol) { 14 | list-style: none; 15 | } 16 | 17 | .sidebar { 18 | position: sticky; 19 | top: 30px; 20 | display: flex; 21 | flex-direction: column; 22 | gap: 30px; 23 | height: 100%; 24 | min-width: 250px; 25 | } 26 | 27 | .sidebarNavGroup { 28 | display: flex; 29 | flex-direction: column; 30 | gap: 15px; 31 | } 32 | 33 | .sidebarNavList { 34 | display: flex; 35 | flex-direction: column; 36 | gap: 5px; 37 | } 38 | 39 | .navLink { 40 | text-decoration: none; 41 | display: block; 42 | color: var(--brand-accent-color-6); 43 | padding-block: 8px; 44 | padding-inline-start: 10px; 45 | border-radius: 6px; 46 | transition: background-color 200ms ease-in-out; 47 | } 48 | 49 | .navLink:not(.activeNavLink):hover { 50 | background-color: var(--brand-accent-color-2); 51 | } 52 | 53 | .activeNavLink { 54 | background-color: var(--brand-accent-color-7); 55 | color: var(--brand-bg-color); 56 | } 57 | 58 | .main { 59 | flex: 1; 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | } 64 | 65 | .mainContent { 66 | flex: 1; 67 | height: 100%; 68 | width: 0; /* Ensures width not exceed parent */ 69 | } 70 | -------------------------------------------------------------------------------- /src/app/views/docs/DocsShell/DocsShell.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component, Suspense } from 'solid-js'; 2 | import { NavLink, NavLinkProps, Outlet, useIsRouting } from 'solid-app-router'; 3 | import { Loader } from '../../../shared/Loader/Loader'; 4 | import styles from './DocsShell.module.css'; 5 | 6 | const SidebarNavLink: Component = (p) => { 7 | return ( 8 | 13 | {p.children} 14 | 15 | ); 16 | }; 17 | 18 | const DocsShellView: Component = () => { 19 | const isRouting = useIsRouting(); 20 | 21 | return ( 22 |
    23 | 53 |
    54 | }> 55 |
    56 | 57 |
    58 |
    59 |
    60 |
    61 | ); 62 | }; 63 | 64 | export default DocsShellView; 65 | -------------------------------------------------------------------------------- /src/app/views/docs/Snippet/Snippet.module.css: -------------------------------------------------------------------------------- 1 | .snippet :global(.shiki) { 2 | max-width: 80ch; 3 | overflow: scroll; 4 | padding-block: 20px; 5 | padding-inline: 30px; 6 | margin: 0; 7 | border-radius: 6px; 8 | font-size: 1rem; 9 | } 10 | 11 | .snippet :global(.line) { 12 | display: inline-block; 13 | padding-block: 4px; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/views/docs/Snippet/Snippet.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createResource } from 'solid-js'; 2 | import styles from './Snippet.module.css'; 3 | 4 | type SnippetId = string; 5 | type SnippetContent = string; 6 | 7 | const snippetsCache = new Map(); 8 | 9 | async function fetchSnippet(snippetId: SnippetId) { 10 | if (snippetsCache.has(snippetId)) { 11 | return snippetsCache.get(snippetId); 12 | } 13 | 14 | const apiUrl = `/snippets/${snippetId}.html`; 15 | const response = await fetch(apiUrl); 16 | const snippet = (await response.text()) as SnippetContent; 17 | 18 | snippetsCache.set(snippetId, snippet); 19 | 20 | return snippet; 21 | } 22 | 23 | export interface SnippetProps { 24 | snippetId: SnippetId; 25 | } 26 | 27 | export const Snippet: Component = (p) => { 28 | const [snippet] = createResource(p.snippetId, fetchSnippet); 29 | 30 | return ( 31 |
    36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/views/docs/docsUtils.module.css: -------------------------------------------------------------------------------- 1 | .section { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 20px; 5 | } 6 | 7 | .text { 8 | max-width: 70ch; 9 | font-size: 1.125rem; 10 | line-height: 1.5; 11 | } 12 | 13 | .embedWrapper iframe { 14 | border: none; 15 | background-color: #202327; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/views/docs/introduction/Installation.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { Snippet } from '../Snippet/Snippet'; 3 | import docsStyles from '../docsUtils.module.css'; 4 | 5 | const InstallationView: Component = () => { 6 | return ( 7 |
    8 |
    9 |

    Install Packages

    10 | 11 |
    12 |
    13 |
    14 |

    Define Actions

    15 | 16 |
    17 |
    18 |
    19 |

    Integrate with Components

    20 | 21 |
    22 |
    23 | ); 24 | }; 25 | 26 | export default InstallationView; 27 | -------------------------------------------------------------------------------- /src/app/views/docs/introduction/Overview.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component, onMount } from 'solid-js'; 2 | import sdk from '@stackblitz/sdk'; 3 | import utilStyles from '../../../utils.module.css'; 4 | import docsStyles from '../docsUtils.module.css'; 5 | 6 | const embedExampleData = { 7 | projectId: 'solid-command-palette-example-lite', 8 | initialFile: 'src/App.tsx', 9 | }; 10 | 11 | const OverviewView: Component = () => { 12 | let exampleElem: HTMLDivElement; 13 | 14 | onMount(() => { 15 | sdk.embedProjectId(exampleElem, embedExampleData.projectId, { 16 | view: 'editor', 17 | forceEmbedLayout: true, 18 | openFile: embedExampleData.initialFile, 19 | clickToLoad: true, 20 | height: 600, 21 | }); 22 | }); 23 | 24 | return ( 25 |
    26 |

    Overview

    27 |

    28 | Command Palette lets users perform tasks on your app with just keyboard. No need to drag the 29 | mouse around. Users can fuzzy search to find the action. If the action has a keyboard 30 | shortcut then they can trigger it from anywhere. This increases their productivity by 10x. 31 |

    32 | 33 |

    Live Example

    34 | 44 |
    45 |
    46 |
    47 |
    48 | ); 49 | }; 50 | 51 | export default OverviewView; 52 | -------------------------------------------------------------------------------- /src/app/views/home/Home.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | flex: 1; 5 | gap: 10px; 6 | } 7 | 8 | .main { 9 | flex: 1; 10 | } 11 | 12 | .aside { 13 | flex: 1; 14 | padding-top: 50px; 15 | } 16 | 17 | .title { 18 | font-size: 3rem; 19 | font-weight: 300; 20 | color: var(--brand-accent-color-5); 21 | line-height: 1.5; 22 | } 23 | 24 | .titleSecondary { 25 | display: flex; 26 | align-items: center; 27 | gap: 20px; 28 | } 29 | 30 | .demoShortcut { 31 | background-color: transparent; 32 | color: var(--brand-accent-color-1); 33 | padding: 3px 10px; 34 | font-size: 1.5rem; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/views/home/Home.view.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { KbdShortcut, useControls } from '../../../lib'; 3 | import { ExampleSlider } from './exampleSlider/ExampleSlider'; 4 | import utilStyles from '../../utils.module.css'; 5 | import styles from './Home.module.css'; 6 | 7 | const HomeView: Component = () => { 8 | const { openPalette } = useControls(); 9 | 10 | return ( 11 |
    12 |
    13 |

    14 | Make your tool lighting fast 15 | 16 | with 17 | 28 | 29 |

    30 |
    31 | 34 |
    35 | ); 36 | }; 37 | 38 | export default HomeView; 39 | -------------------------------------------------------------------------------- /src/app/views/home/exampleSlider/ExampleSlider.module.css: -------------------------------------------------------------------------------- 1 | .slidesWrapper { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 60px; 5 | overflow-y: scroll; 6 | height: 60vh; 7 | padding: 0 10px; 8 | scroll-snap-type: block mandatory; 9 | overscroll-behavior: contain; 10 | } 11 | 12 | .exampleWrapper { 13 | display: flex; 14 | flex-direction: column; 15 | gap: 20px; 16 | align-items: center; 17 | scroll-snap-align: start; 18 | } 19 | 20 | .exampleImage { 21 | width: 100%; 22 | height: auto; 23 | border-radius: 12px; 24 | box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 25 | } 26 | 27 | .exampleLink { 28 | color: var(--brand-accent-color-7); 29 | text-decoration: none; 30 | font-weight: bold; 31 | cursor: pointer; 32 | outline-offset: 2px; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/views/home/exampleSlider/ExampleSlider.tsx: -------------------------------------------------------------------------------- 1 | import { Component, For } from 'solid-js'; 2 | import { slides } from './data'; 3 | import utilStyles from '../../../utils.module.css'; 4 | import styles from './ExampleSlider.module.css'; 5 | 6 | export const ExampleSlider: Component = () => { 7 | return ( 8 |
    9 | 10 | {(slide) => ( 11 |
    12 | {`Command 17 |
    18 | {slide.featureContent}{' '} 19 | 25 | {slide.productName} 26 | 27 |
    28 |
    29 | )} 30 |
    31 |
    32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/views/home/exampleSlider/data.ts: -------------------------------------------------------------------------------- 1 | export const slides = [ 2 | { 3 | imgUrl: '/images/command-palette-examples/tailwind.png', 4 | featureContent: 'Navigation becomes faster in', 5 | productUrl: 'https://tailwindcss.com/', 6 | productName: 'Tailwind Docs', 7 | }, 8 | { 9 | imgUrl: '/images/command-palette-examples/linear.png', 10 | featureContent: 'Keyboard Shortcuts can be used in', 11 | productUrl: 'https://linear.app/', 12 | productName: 'Linear', 13 | }, 14 | { 15 | imgUrl: '/images/command-palette-examples/github.png', 16 | featureContent: 'Power-users can quickly perform tasks on', 17 | productUrl: 'https://docs.github.com/en/get-started/using-github/github-command-palette', 18 | productName: 'GitHub', 19 | }, 20 | { 21 | imgUrl: '/images/command-palette-examples/devtools.png', 22 | featureContent: 'New features can be discovered in', 23 | productUrl: 'https://developer.chrome.com/docs/devtools/command-menu/', 24 | productName: 'Devtools', 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /src/lib/CommandPalette.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: fixed; 3 | inset: 0; 4 | display: flex; 5 | justify-content: center; 6 | } 7 | 8 | .wrapper * { 9 | box-sizing: border-box; 10 | } 11 | 12 | .palette { 13 | position: relative; 14 | height: max-content; 15 | } 16 | 17 | .panel { 18 | --scp-gutter-space-inline: 20px; 19 | 20 | display: flex; 21 | flex-direction: column; 22 | background-color: #fff; 23 | width: min(100vw, 600px); 24 | height: 100vh; 25 | border: 0.5px solid #e2e8f0; 26 | border-radius: 8px; 27 | box-shadow: 0 0 50px -12px rgb(0 0 0 / 0.25); 28 | overflow: hidden; 29 | scrollbar-gutter: stable; 30 | font-family: inherit; 31 | transform-origin: 50%; 32 | } 33 | 34 | .animEnter .panel { 35 | opacity: 0; 36 | transform: translateY(50px) scaleX(0.95); 37 | } 38 | 39 | .animEnterActive .panel { 40 | opacity: 1; 41 | transition: opacity, transform 200ms; 42 | } 43 | 44 | .animExit .panel { 45 | opacity: 1; 46 | } 47 | 48 | .animExitActive .panel { 49 | opacity: 0; 50 | transform: translateY(70px) scaleX(0.8); 51 | transition: opacity, transform 300ms, 200ms; 52 | } 53 | 54 | .searchForm { 55 | display: grid; 56 | grid-template-columns: 1fr auto; 57 | align-items: center; 58 | border-block-end: 0.5px solid #e2e8f0; 59 | } 60 | 61 | .searchInput { 62 | grid-row: 1; 63 | grid-column: 1 / -1; 64 | appearance: none; 65 | width: 100%; 66 | border: none; 67 | padding-block: 20px; 68 | padding-inline-start: var(--scp-gutter-space-inline); 69 | padding-inline-end: 80px; 70 | font-size: 20px; 71 | font-family: inherit; 72 | outline: 0; 73 | } 74 | 75 | .searchInput::-webkit-search-cancel-button, 76 | .searchInput::-webkit-search-decoration, 77 | .searchInput::-webkit-search-results-button, 78 | .searchInput::-webkit-search-results-decoration { 79 | display: none; 80 | } 81 | 82 | .closeBtn { 83 | grid-row: 1; 84 | grid-column: 2; 85 | appearance: none; 86 | background: transparent; 87 | border: none; 88 | border-radius: 5px; 89 | padding: 0; 90 | margin-inline-end: var(--scp-gutter-space-inline); 91 | } 92 | 93 | .closeBtn:focus-visible { 94 | outline-offset: 3px; 95 | outline-color: #94a3b8; 96 | } 97 | 98 | @media (min-width: 640px) { 99 | .palette { 100 | margin-block-start: 100px; 101 | } 102 | 103 | .panel { 104 | height: max-content; 105 | max-height: 350px; 106 | } 107 | } 108 | 109 | @media (prefers-reduced-motion: reduce) { 110 | .animEnter .panel { 111 | transform: none; 112 | } 113 | 114 | .animExitActive .panel { 115 | transform: none; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/lib/CommandPalette.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | createEffect, 4 | createSignal, 5 | createUniqueId, 6 | JSX, 7 | onMount, 8 | onCleanup, 9 | Show, 10 | } from 'solid-js'; 11 | import { Transition } from 'solid-transition-group'; 12 | import tinykeys from 'tinykeys'; 13 | import { useStore } from './StoreContext'; 14 | import { CommandPalettePortal } from './CommandPalettePortal'; 15 | import { KbdShortcut } from './KbdShortcut/KbdShortcut'; 16 | import { ScrollAssist } from './ScrollAssist/ScrollAssist'; 17 | import { PanelResult } from './Panel/Result/Result'; 18 | import { PanelFooter } from './Panel/Footer/Footer'; 19 | import { createSearchResultList } from './createActionList'; 20 | import { runAction } from './actionUtils/actionUtils'; 21 | import { ActionId, WrappedAction } from './types'; 22 | import utilStyles from './utils.module.css'; 23 | import styles from './CommandPalette.module.css'; 24 | 25 | type InputEventHandler = JSX.EventHandlerUnion; 26 | type ActiveItemId = null | ActionId; 27 | type UserInteraction = 28 | | 'idle' 29 | | 'search' 30 | | 'navigate-kbd' 31 | | 'navigate-mouse' 32 | | 'navigate-scroll-assist'; 33 | 34 | export interface CommandPaletteProps { 35 | searchPlaceholder?: string; 36 | } 37 | 38 | const CommandPaletteInternal: Component = (p) => { 39 | const [state, storeMethods] = useStore(); 40 | const { closePalette, setSearchText, revertParentAction } = storeMethods; 41 | const resultsList = createSearchResultList(); 42 | const [activeItemId, setActiveItemId] = createSignal(null); 43 | const [userInteraction, setUserInteraction] = createSignal('idle'); 44 | const searchLabelId = createUniqueId(); 45 | const searchInputId = createUniqueId(); 46 | const resultListId = createUniqueId(); 47 | 48 | let wrapperElem: undefined | HTMLDivElement; 49 | let searchInputElem: undefined | HTMLInputElement; 50 | let closeBtnElem: undefined | HTMLButtonElement; 51 | let lastFocusedElem: null | HTMLElement; 52 | 53 | function triggerRun(action: WrappedAction) { 54 | runAction(action, state.actionsContext, storeMethods); 55 | } 56 | 57 | function activatePrevItem() { 58 | const actionsList = resultsList(); 59 | const actionsCount = actionsList.length; 60 | const activeActionId = activeItemId(); 61 | 62 | const currentActionIndex = actionsList.findIndex((action) => action.id === activeActionId); 63 | 64 | if (currentActionIndex < 0) { 65 | return; 66 | } 67 | 68 | const prevActionIndex = (actionsCount + currentActionIndex - 1) % actionsCount; 69 | const prevActionId = actionsList[prevActionIndex].id; 70 | 71 | setActiveItemId(prevActionId); 72 | } 73 | 74 | function activateNextItem() { 75 | const actionsList = resultsList(); 76 | const actionsCount = actionsList.length; 77 | const activeActionId = activeItemId(); 78 | 79 | const currentActionIndex = actionsList.findIndex((action) => action.id === activeActionId); 80 | 81 | if (currentActionIndex < 0) { 82 | return; 83 | } 84 | 85 | const nextActionIndex = (currentActionIndex + 1) % actionsCount; 86 | const nextActionId = actionsList[nextActionIndex].id; 87 | 88 | setActiveItemId(nextActionId); 89 | } 90 | 91 | function handleWrapperClick() { 92 | closePalette(); 93 | } 94 | 95 | function handlePanelClick(event: MouseEvent) { 96 | event.stopPropagation(); 97 | } 98 | 99 | const handleSearchInput: InputEventHandler = (event) => { 100 | const newValue = event.currentTarget.value; 101 | 102 | setUserInteraction('search'); 103 | setSearchText(newValue); 104 | }; 105 | 106 | function handleActionItemSelect(action: WrappedAction) { 107 | triggerRun(action); 108 | } 109 | 110 | function handleActionItemHover(action: WrappedAction) { 111 | setUserInteraction('navigate-mouse'); 112 | setActiveItemId(action.id); 113 | } 114 | 115 | function handleKbdEnter(event: KeyboardEvent) { 116 | const targetElem = event.target as HTMLElement; 117 | 118 | if (closeBtnElem && closeBtnElem.contains(targetElem)) { 119 | return; 120 | } 121 | 122 | event.preventDefault(); 123 | 124 | const activeActionId = activeItemId(); 125 | 126 | if (!activeActionId) { 127 | return null; 128 | } 129 | 130 | const action = state.actions[activeActionId]; 131 | triggerRun(action); 132 | } 133 | 134 | function handleKbdPrev(event: KeyboardEvent) { 135 | event.preventDefault(); 136 | 137 | setUserInteraction('navigate-kbd'); 138 | activatePrevItem(); 139 | } 140 | 141 | function handleKbdNext(event: KeyboardEvent) { 142 | event.preventDefault(); 143 | 144 | setUserInteraction('navigate-kbd'); 145 | activateNextItem(); 146 | } 147 | 148 | function handleKbdFirst(event: KeyboardEvent) { 149 | event.preventDefault(); 150 | 151 | const actionsList = resultsList(); 152 | const firstAction = actionsList[0]; 153 | 154 | if (firstAction) { 155 | setUserInteraction('navigate-kbd'); 156 | setActiveItemId(firstAction.id); 157 | } 158 | } 159 | 160 | function handleKbdLast(event: KeyboardEvent) { 161 | event.preventDefault(); 162 | 163 | const actionsList = resultsList(); 164 | const lastAction = actionsList.at(-1); 165 | 166 | if (lastAction) { 167 | setUserInteraction('navigate-kbd'); 168 | setActiveItemId(lastAction.id); 169 | } 170 | } 171 | 172 | function handleKbdDelete() { 173 | const isSearchEmpty = state.searchText.length <= 0; 174 | 175 | if (isSearchEmpty) { 176 | revertParentAction(); 177 | } 178 | } 179 | 180 | function handleScrollAssistPrev() { 181 | setUserInteraction('navigate-scroll-assist'); 182 | activatePrevItem(); 183 | } 184 | 185 | function handleScrollAssistNext() { 186 | setUserInteraction('navigate-scroll-assist'); 187 | activateNextItem(); 188 | } 189 | 190 | function handleScrollAssistStop() { 191 | setUserInteraction('idle'); 192 | } 193 | 194 | function getScrollAssistStatus() { 195 | if (userInteraction() === 'navigate-mouse') { 196 | return 'available'; 197 | } 198 | 199 | if (userInteraction() === 'navigate-scroll-assist') { 200 | return 'running'; 201 | } 202 | 203 | return 'stopped'; 204 | } 205 | 206 | onMount(() => { 207 | lastFocusedElem = document.activeElement as HTMLElement; 208 | 209 | if (searchInputElem) { 210 | searchInputElem.select(); 211 | } 212 | 213 | if (wrapperElem) { 214 | tinykeys(wrapperElem, { 215 | Escape: (event) => { 216 | event.preventDefault(); 217 | closePalette(); 218 | }, 219 | Enter: handleKbdEnter, 220 | ArrowUp: handleKbdPrev, 221 | ArrowDown: handleKbdNext, 222 | PageUp: handleKbdFirst, 223 | PageDown: handleKbdLast, 224 | Backspace: handleKbdDelete, 225 | }); 226 | } 227 | }); 228 | 229 | onCleanup(() => { 230 | if (lastFocusedElem) { 231 | lastFocusedElem.focus(); 232 | } 233 | 234 | lastFocusedElem = null; 235 | }); 236 | 237 | createEffect(() => { 238 | const actionsList = resultsList(); 239 | const firstResultId = actionsList[0]?.id; 240 | 241 | if (firstResultId) { 242 | setActiveItemId(firstResultId); 243 | } else { 244 | setActiveItemId(null); 245 | } 246 | }); 247 | 248 | return ( 249 |
    254 |
    255 | 261 | 267 |
    277 | 321 | 329 | 330 |
    331 |
    332 |
    333 | ); 334 | }; 335 | 336 | export const CommandPalette: Component = (p) => { 337 | const [state] = useStore(); 338 | 339 | return ( 340 | 341 | 347 | 348 | 349 | 350 | 351 | 352 | ); 353 | }; 354 | -------------------------------------------------------------------------------- /src/lib/CommandPalettePortal.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createRenderEffect, onCleanup } from 'solid-js'; 2 | import { Portal } from 'solid-js/web'; 3 | 4 | type PortalElem = undefined | HTMLDivElement; 5 | 6 | export const CommandPalettePortal: Component = (p) => { 7 | let portalElem: PortalElem; 8 | 9 | createRenderEffect(() => { 10 | if (portalElem) { 11 | return; 12 | } 13 | 14 | const parent = document.body; 15 | const newPortalElem = document.createElement('div'); 16 | newPortalElem.classList.add('command-palette-portal'); 17 | parent.appendChild(newPortalElem); 18 | portalElem = newPortalElem; 19 | }); 20 | 21 | onCleanup(() => { 22 | if (portalElem) { 23 | portalElem.remove(); 24 | portalElem = undefined; 25 | } 26 | }); 27 | 28 | return {p.children}; 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/KbdShortcut.module.css: -------------------------------------------------------------------------------- 1 | .kbdShortcut { 2 | display: flex; 3 | gap: 12px; 4 | } 5 | 6 | .kbdGroup { 7 | display: flex; 8 | gap: 5px; 9 | } 10 | 11 | .kbdKey { 12 | background-color: #f1f5f9; 13 | color: #1e293b; 14 | border: 1px solid #cbd5e1; 15 | border-radius: 5px; 16 | font-family: var(--scp-kbd-font-family, sans-serif); 17 | font-size: inherit; 18 | text-transform: capitalize; 19 | padding: 5px 8px; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/KbdShortcut.tsx: -------------------------------------------------------------------------------- 1 | import { JSX, Component, For, splitProps } from 'solid-js'; 2 | import { getFormattedShortcut } from './utils'; 3 | import { ActionShortcut } from '../types'; 4 | import styles from './KbdShortcut.module.css'; 5 | 6 | export interface KbdShortcutProps extends JSX.HTMLAttributes { 7 | shortcut: ActionShortcut; 8 | } 9 | 10 | export const KbdShortcut: Component = (p) => { 11 | const [l, others] = splitProps(p, ['shortcut', 'class']); 12 | 13 | const formattedShortcut = getFormattedShortcut(l.shortcut); 14 | 15 | const keyClasses = [styles.kbdKey, l.class].join(' '); 16 | 17 | return ( 18 | 22 | 23 | {(group) => ( 24 | 25 | {(key) => {key}} 26 | 27 | )} 28 | 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/types.ts: -------------------------------------------------------------------------------- 1 | import { ActionShortcut } from '../types'; 2 | 3 | export type GetFormattedShortcut = (shortcut: ActionShortcut) => Array>; 4 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest'; 2 | import { getFormattedShortcut } from './utils'; 3 | 4 | describe('Test KbdShortcut on Mac', () => { 5 | test('should format combination shortcut correctly', () => { 6 | const formattedShortcut = getFormattedShortcut('Shift+Alt+k'); 7 | expect(formattedShortcut).toMatchInlineSnapshot(` 8 | [ 9 | [ 10 | "Shift", 11 | "Alt", 12 | "k", 13 | ], 14 | ] 15 | `); 16 | }); 17 | 18 | test('should format sequence shortcut correctly', () => { 19 | const formattedShortcut = getFormattedShortcut('g h'); 20 | expect(formattedShortcut).toMatchInlineSnapshot(` 21 | [ 22 | [ 23 | "g", 24 | ], 25 | [ 26 | "h", 27 | ], 28 | ] 29 | `); 30 | }); 31 | 32 | test('should format combined shortcut correctly', () => { 33 | const formattedShortcut = getFormattedShortcut('Control+k f'); 34 | expect(formattedShortcut).toMatchInlineSnapshot(` 35 | [ 36 | [ 37 | "Ctrl", 38 | "k", 39 | ], 40 | [ 41 | "f", 42 | ], 43 | ] 44 | `); 45 | }); 46 | 47 | test('should format Escape shortcut correctly', () => { 48 | const formattedShortcut = getFormattedShortcut('Escape q'); 49 | expect(formattedShortcut).toMatchInlineSnapshot(` 50 | [ 51 | [ 52 | "Esc", 53 | ], 54 | [ 55 | "q", 56 | ], 57 | ] 58 | `); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/utils.ts: -------------------------------------------------------------------------------- 1 | import { parseKeybinding } from 'tinykeys'; 2 | import { GetFormattedShortcut } from './types'; 3 | 4 | function getFormattedKey(key: string) { 5 | if (key === 'Meta') { 6 | return '⌘'; 7 | } 8 | 9 | if (key === 'Control') { 10 | return 'Ctrl'; 11 | } 12 | 13 | if (key === 'Escape') { 14 | return 'Esc'; 15 | } 16 | 17 | return key; 18 | } 19 | 20 | export const getFormattedShortcut: GetFormattedShortcut = (shortcut) => { 21 | const parsedShortcut = parseKeybinding(shortcut); 22 | 23 | const formattedShortcut = parsedShortcut.map((group) => { 24 | const flatGroup = group.flat(); 25 | const formattedGroup = flatGroup.map(getFormattedKey); 26 | return formattedGroup; 27 | }); 28 | 29 | return formattedShortcut; 30 | }; 31 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/utilsMac.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, vi, expect, beforeAll, afterAll } from 'vitest'; 2 | import { GetFormattedShortcut } from './types'; 3 | 4 | describe('Test KbdShortcut on Mac', () => { 5 | const originalNavigator = window.navigator; 6 | const platformSpy = vi.spyOn(window, 'navigator', 'get'); 7 | let getFormattedShortcut: GetFormattedShortcut; 8 | 9 | beforeAll(async () => { 10 | platformSpy.mockImplementation(() => { 11 | return { 12 | ...originalNavigator, 13 | platform: 'MacIntel', 14 | }; 15 | }); 16 | const utils = await import('./utils'); 17 | getFormattedShortcut = utils.getFormattedShortcut; 18 | }); 19 | 20 | afterAll(() => { 21 | platformSpy.mockRestore(); 22 | }); 23 | 24 | test('should format $mod as Command key', () => { 25 | const formattedShortcut = getFormattedShortcut('$mod+s'); 26 | expect(formattedShortcut).toMatchInlineSnapshot(` 27 | [ 28 | [ 29 | "⌘", 30 | "s", 31 | ], 32 | ] 33 | `); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/lib/KbdShortcut/utilsWin.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, vi, expect, beforeAll, afterAll } from 'vitest'; 2 | import { GetFormattedShortcut } from './types'; 3 | 4 | describe('Test KbdShortcut on Windows', () => { 5 | const originalNavigator = window.navigator; 6 | const platformSpy = vi.spyOn(window, 'navigator', 'get'); 7 | let getFormattedShortcut: GetFormattedShortcut; 8 | 9 | beforeAll(async () => { 10 | platformSpy.mockImplementation(() => { 11 | return { 12 | ...originalNavigator, 13 | platform: 'Win32', 14 | }; 15 | }); 16 | const utils = await import('./utils'); 17 | getFormattedShortcut = utils.getFormattedShortcut; 18 | }); 19 | 20 | afterAll(() => { 21 | platformSpy.mockRestore(); 22 | }); 23 | 24 | test('should format $mod as Control key', () => { 25 | const formattedShortcut = getFormattedShortcut('$mod+s'); 26 | expect(formattedShortcut).toMatchInlineSnapshot(` 27 | [ 28 | [ 29 | "Ctrl", 30 | "s", 31 | ], 32 | ] 33 | `); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/lib/Panel/Footer/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | gap: 15px; 4 | color: #64748b; 5 | font-size: 0.85rem; 6 | border-block-start: 0.5px solid #e2e8f0; 7 | padding-block: 10px; 8 | padding-inline: var(--scp-gutter-space-inline); 9 | } 10 | 11 | .group { 12 | display: inline-flex; 13 | align-items: center; 14 | gap: 5px; 15 | } 16 | 17 | .shortcut { 18 | background-color: #f1f5f9; 19 | color: #0f172a; 20 | border: 1px solid #cbd5e1; 21 | border-radius: 3px; 22 | font-size: 0.75rem; 23 | padding: 3px 2px; 24 | line-height: 0; 25 | } 26 | 27 | .runShortcut { 28 | padding-inline: 5px; 29 | } 30 | 31 | .icon { 32 | width: 1em; 33 | height: 1em; 34 | } 35 | 36 | .iconArrow[data-arrow-direction='down'] { 37 | transform: rotateX(180deg); 38 | } 39 | 40 | .iconReturn { 41 | transform: rotateX(180deg); 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/Panel/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import styles from './Footer.module.css'; 3 | 4 | export interface IconArrowProps { 5 | direction?: 'up' | 'down'; 6 | } 7 | 8 | const IconArrow: Component = (p) => { 9 | return ( 10 | 17 | 23 | 24 | ); 25 | }; 26 | 27 | const IconReturn: Component = () => { 28 | return ( 29 | 35 | 41 | 42 | ); 43 | }; 44 | 45 | export const PanelFooter: Component = () => { 46 | return ( 47 |
    48 |
    49 | Navigate with{' '} 50 | 51 | 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | Select using{' '} 59 | 60 | 61 | 62 |
    63 |
    64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/lib/Panel/Result/Result.module.css: -------------------------------------------------------------------------------- 1 | .resultWrapper { 2 | flex: 1; 3 | overflow: auto; 4 | overscroll-behavior: contain; 5 | } 6 | 7 | .resultList { 8 | list-style: none; 9 | } 10 | 11 | .resultContent { 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | cursor: pointer; 16 | user-select: none; 17 | padding-block: 15px; 18 | padding-inline: var(--scp-gutter-space-inline); 19 | } 20 | 21 | .resultContent.active { 22 | background-color: #f1f5f9; 23 | } 24 | 25 | .resultTitle { 26 | color: #1e293b; 27 | font-weight: normal; 28 | } 29 | 30 | .resultSubtitle { 31 | color: #64748b; 32 | font-size: 13px; 33 | font-weight: 300; 34 | margin-block-start: 7px; 35 | } 36 | 37 | .resultShortcut { 38 | font-size: 0.75rem; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/Panel/Result/Result.tsx: -------------------------------------------------------------------------------- 1 | import { Component, For, Show, createEffect } from 'solid-js'; 2 | import { useStore } from '../../StoreContext'; 3 | import { KbdShortcut } from '../../KbdShortcut/KbdShortcut'; 4 | import { ActionId, WrappedAction, WrappedActionList, ResultContentProps } from '../../types'; 5 | import utilStyles from '../../utils.module.css'; 6 | import styles from './Result.module.css'; 7 | import { Dynamic } from 'solid-js/web'; 8 | 9 | const ResultContent: Component = (p) => { 10 | return ( 11 |
    17 |
    18 |

    {p.action.title}

    19 | 20 |

    {p.action.subtitle}

    21 |
    22 |
    23 |
    24 | 25 | {(shortcut) => ( 26 | 30 | )} 31 | 32 |
    33 |
    34 | ); 35 | }; 36 | 37 | type ActiveItemId = null | ActionId; 38 | type ResultItemElem = undefined | HTMLLIElement; 39 | 40 | interface ResultItemProps { 41 | action: WrappedAction; 42 | activeItemId: ActiveItemId; 43 | onActionItemSelect: (action: WrappedAction) => void; 44 | onActionItemHover: (action: WrappedAction) => void; 45 | } 46 | 47 | const ResultItem: Component = (p) => { 48 | let resultItemElem: ResultItemElem; 49 | let isMoving = false; 50 | 51 | const [state] = useStore(); 52 | const ResultContentComponent = state.components?.ResultContent || ResultContent; 53 | 54 | function isActive() { 55 | return p.action.id === p.activeItemId; 56 | } 57 | 58 | function handleMouseMove(action: WrappedAction) { 59 | if (isMoving) { 60 | return; 61 | } 62 | 63 | isMoving = true; 64 | p.onActionItemHover(action); 65 | } 66 | 67 | function handleMouseLeave() { 68 | isMoving = false; 69 | } 70 | 71 | function handleMouseDown(event: MouseEvent) { 72 | // don't take focus away from search field when item is clicked. 73 | event.preventDefault(); 74 | } 75 | 76 | createEffect(() => { 77 | if (isActive() && resultItemElem) { 78 | resultItemElem.scrollIntoView({ 79 | behavior: 'smooth', 80 | block: 'nearest', 81 | }); 82 | } 83 | }); 84 | 85 | return ( 86 |
  • 96 | 101 |
  • 102 | ); 103 | }; 104 | 105 | export interface PanelResultProps { 106 | activeItemId: ActiveItemId; 107 | resultsList: WrappedActionList; 108 | resultListId: string; 109 | searchLabelId: string; 110 | onActionItemSelect: (action: WrappedAction) => void; 111 | onActionItemHover: (action: WrappedAction) => void; 112 | } 113 | 114 | export const PanelResult: Component = (p) => { 115 | return ( 116 |
    117 |
      123 | 127 |

      128 | Couldn't find any matching actions 129 |

      130 |
    131 | } 132 | > 133 | {(action) => ( 134 | 140 | )} 141 | 142 | 143 |
    144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /src/lib/Root.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { createStore, produce } from 'solid-js/store'; 3 | import { createKbdShortcuts } from './createKbdShortcuts'; 4 | import { getActiveParentAction } from './actionUtils/actionUtils'; 5 | import { rootParentActionId } from './constants'; 6 | import { Provider } from './StoreContext'; 7 | import { RootProps, StoreState, StoreMethods, StoreContext, DynamicContextMap } from './types'; 8 | 9 | const RootInternal: Component = () => { 10 | createKbdShortcuts(); 11 | 12 | return null; 13 | }; 14 | 15 | export const Root: Component = (p) => { 16 | const initialActions = p.actions || {}; 17 | const initialActionsContext = p.actionsContext || {}; 18 | 19 | const [state, setState] = createStore({ 20 | visibility: 'closed', 21 | searchText: '', 22 | activeParentActionIdList: [rootParentActionId], 23 | actions: initialActions, 24 | actionsContext: { 25 | root: initialActionsContext, 26 | dynamic: {}, 27 | }, 28 | components: p.components, 29 | }); 30 | 31 | const storeMethods: StoreMethods = { 32 | setSearchText(newValue) { 33 | setState('searchText', newValue); 34 | }, 35 | setActionsContext(actionId, newData) { 36 | // @ts-expect-error need to figure out nested store setters. 37 | setState('actionsContext', 'dynamic', actionId, newData); 38 | }, 39 | resetActionsContext(actionId) { 40 | setState( 41 | 'actionsContext', 42 | 'dynamic', 43 | produce((dynamicContext) => { 44 | delete dynamicContext[actionId]; 45 | }) 46 | ); 47 | }, 48 | openPalette() { 49 | setState('visibility', 'opened'); 50 | }, 51 | closePalette() { 52 | setState('visibility', 'closed'); 53 | 54 | const hasActiveParent = state.activeParentActionIdList.length > 1; 55 | 56 | if (hasActiveParent) { 57 | storeMethods.setSearchText(''); 58 | storeMethods.resetParentAction(); 59 | } 60 | }, 61 | togglePalette() { 62 | setState('visibility', (prev) => (prev === 'opened' ? 'closed' : 'opened')); 63 | }, 64 | selectParentAction(parentActionId) { 65 | if (parentActionId === rootParentActionId) { 66 | return; 67 | } 68 | 69 | setState('activeParentActionIdList', (old) => { 70 | return [...old, parentActionId]; 71 | }); 72 | storeMethods.setSearchText(''); 73 | }, 74 | revertParentAction() { 75 | setState('activeParentActionIdList', (old) => { 76 | const { isRoot } = getActiveParentAction(old); 77 | if (isRoot) { 78 | return old; 79 | } 80 | 81 | const copiedList = [...old]; 82 | copiedList.pop(); 83 | 84 | return copiedList; 85 | }); 86 | }, 87 | resetParentAction() { 88 | setState('activeParentActionIdList', [rootParentActionId]); 89 | }, 90 | }; 91 | 92 | const store: StoreContext = [state, storeMethods]; 93 | 94 | return ( 95 | 96 | 97 | {p.children} 98 | 99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /src/lib/ScrollAssist/ScrollAssist.module.css: -------------------------------------------------------------------------------- 1 | .scrollShape { 2 | --scp-scroll-shape-height: 60px; 3 | 4 | width: 35%; 5 | height: var(--scp-scroll-shape-height); 6 | position: absolute; 7 | inset-inline-start: 50%; 8 | transform: translateX(-50%); 9 | } 10 | 11 | .scrollShape[data-direction='up'] { 12 | inset-block-start: calc(-1 * var(--scp-scroll-shape-height)); 13 | } 14 | 15 | .scrollShape[data-direction='down'] { 16 | inset-block-start: 100%; 17 | } 18 | 19 | .scrollShape[data-direction='up'][data-status='running'] { 20 | cursor: n-resize; 21 | } 22 | 23 | .scrollShape[data-direction='down'][data-status='running'] { 24 | cursor: s-resize; 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/ScrollAssist/ScrollAssist.tsx: -------------------------------------------------------------------------------- 1 | import { Component, createEffect } from 'solid-js'; 2 | import styles from './ScrollAssist.module.css'; 3 | 4 | type IntervalId = null | ReturnType; 5 | 6 | type ScrollAssistProps = { 7 | direction: 'up' | 'down'; 8 | status: 'available' | 'running' | 'stopped'; 9 | onNavigate: () => void; 10 | onStop: () => void; 11 | }; 12 | 13 | export const ScrollAssist: Component = (p) => { 14 | let intervalId: IntervalId = null; 15 | 16 | function triggerNavigation() { 17 | p.onNavigate(); 18 | } 19 | 20 | function startSelecting() { 21 | triggerNavigation(); 22 | intervalId = setInterval(() => { 23 | if (p.status === 'running') { 24 | triggerNavigation(); 25 | } 26 | }, 500); 27 | } 28 | 29 | function stopSelecting() { 30 | if (intervalId) { 31 | clearInterval(intervalId); 32 | } 33 | 34 | intervalId = null; 35 | } 36 | 37 | function handleMouseEnter() { 38 | if (p.status === 'available') { 39 | startSelecting(); 40 | } 41 | } 42 | 43 | function handleMouseMove(event: MouseEvent) { 44 | if (p.status !== 'running') { 45 | return; 46 | } 47 | 48 | let shouldStop = false; 49 | 50 | if (p.direction === 'up' && event.movementY > 0) { 51 | shouldStop = true; 52 | } 53 | 54 | if (p.direction === 'down' && event.movementY < 0) { 55 | shouldStop = true; 56 | } 57 | 58 | if (shouldStop) { 59 | p.onStop(); 60 | } 61 | } 62 | 63 | function handleMouseLeave() { 64 | p.onStop(); 65 | } 66 | 67 | createEffect(() => { 68 | if (p.status === 'stopped') { 69 | stopSelecting(); 70 | } 71 | }); 72 | 73 | return ( 74 |
    82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/lib/StoreContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'solid-js'; 2 | import { StoreContext } from './types'; 3 | 4 | const storeContext = createContext(); 5 | 6 | export const Provider = storeContext.Provider; 7 | 8 | export function useStore() { 9 | const store = useContext(storeContext); 10 | 11 | if (!store) { 12 | throw new Error('Please use it inside Root component'); 13 | } 14 | 15 | return store; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/actionUtils/actionUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, vi, afterEach, afterAll } from 'vitest'; 2 | import { defineAction } from '../defineAction'; 3 | import { checkActionAllowed, runAction } from './actionUtils'; 4 | 5 | describe('Test Action Utils', () => { 6 | describe('Test checkActionAllowed util', () => { 7 | const baseAction = { 8 | id: 'test-action', 9 | title: 'Test Action', 10 | run: () => { 11 | console.log('test ran'); 12 | }, 13 | }; 14 | 15 | test('should allow action if no condition defined', () => { 16 | const action = defineAction(baseAction); 17 | 18 | const actionsContext = { 19 | root: {}, 20 | dynamic: {}, 21 | }; 22 | 23 | const isAllowed = checkActionAllowed(action, actionsContext); 24 | expect(isAllowed).toBe(true); 25 | }); 26 | 27 | test('should allow action based on root context', () => { 28 | const action = defineAction({ 29 | ...baseAction, 30 | cond: ({ rootContext }) => { 31 | return rootContext.profile === 'work'; 32 | }, 33 | }); 34 | 35 | const failingActionsContext = { 36 | root: { 37 | profile: 'personal', 38 | }, 39 | dynamic: {}, 40 | }; 41 | expect(checkActionAllowed(action, failingActionsContext), 'action is not allowed').toBe( 42 | false 43 | ); 44 | 45 | const passingActionsContext = { 46 | root: { 47 | profile: 'work', 48 | }, 49 | dynamic: {}, 50 | }; 51 | expect(checkActionAllowed(action, passingActionsContext), 'action is allowed').toBe(true); 52 | }); 53 | 54 | test('should allow action based on dynamic context', () => { 55 | const action = defineAction({ 56 | ...baseAction, 57 | cond: ({ dynamicContext }) => { 58 | return dynamicContext.isActive === true; 59 | }, 60 | }); 61 | 62 | const failingActionsContext = { 63 | root: {}, 64 | dynamic: { 65 | 'test-action': { 66 | isActive: false, 67 | }, 68 | 'other-action': { 69 | isActive: true, 70 | }, 71 | }, 72 | }; 73 | expect(checkActionAllowed(action, failingActionsContext), 'action is not allowed').toBe( 74 | false 75 | ); 76 | 77 | const passingActionsContext = { 78 | root: {}, 79 | dynamic: { 80 | 'test-action': { 81 | isActive: true, 82 | }, 83 | 'other-action': { 84 | isActive: false, 85 | }, 86 | }, 87 | }; 88 | expect(checkActionAllowed(action, passingActionsContext), 'action is allowed').toBe(true); 89 | }); 90 | }); 91 | 92 | describe('Test runAction util', () => { 93 | const runMock = vi.fn(); 94 | const selectParentActionMock = vi.fn(); 95 | const closePaletteMock = vi.fn(); 96 | 97 | const baseAction = { 98 | id: 'test-action', 99 | title: 'Test Action', 100 | run: runMock, 101 | }; 102 | 103 | const baseStoreMethods = { 104 | selectParentAction: selectParentActionMock, 105 | closePalette: closePaletteMock, 106 | }; 107 | 108 | afterEach(() => { 109 | runMock.mockClear(); 110 | selectParentActionMock.mockClear(); 111 | closePaletteMock.mockClear(); 112 | }); 113 | 114 | afterAll(() => { 115 | runMock.mockReset(); 116 | selectParentActionMock.mockReset(); 117 | closePaletteMock.mockReset(); 118 | }); 119 | 120 | test('should trigger run callback of the action correctly', () => { 121 | const action = defineAction(baseAction); 122 | 123 | const actionsContext = { 124 | root: { 125 | profile: 'work', 126 | }, 127 | dynamic: { 128 | 'test-action': { 129 | isActive: true, 130 | }, 131 | 'other-action': { 132 | isActive: false, 133 | }, 134 | }, 135 | }; 136 | 137 | runAction(action, actionsContext, baseStoreMethods); 138 | 139 | expect(runMock).toBeCalledWith({ 140 | actionId: 'test-action', 141 | rootContext: { 142 | profile: 'work', 143 | }, 144 | dynamicContext: { 145 | isActive: true, 146 | }, 147 | }); 148 | expect(selectParentActionMock).not.toBeCalled(); 149 | expect(closePaletteMock).toBeCalled(); 150 | }); 151 | 152 | test('should setup nested actions correctly', () => { 153 | const action = defineAction({ 154 | ...baseAction, 155 | id: 'parent-test-action', 156 | run: undefined, 157 | }); 158 | 159 | const actionsContext = { 160 | root: {}, 161 | dynamic: {}, 162 | }; 163 | 164 | runAction(action, actionsContext, baseStoreMethods); 165 | 166 | expect(selectParentActionMock).toBeCalledWith('parent-test-action'); 167 | expect(runMock).not.toBeCalled(); 168 | expect(closePaletteMock).not.toBeCalled(); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /src/lib/actionUtils/actionUtils.ts: -------------------------------------------------------------------------------- 1 | import { KeyBindingMap } from 'tinykeys'; 2 | import { rootParentActionId } from '../constants'; 3 | import { ActionId, ActionsContext, StoreMethods, WrappedAction, WrappedActionList } from '../types'; 4 | 5 | type RunStoreMethods = { 6 | selectParentAction: StoreMethods['selectParentAction']; 7 | closePalette: StoreMethods['closePalette']; 8 | }; 9 | 10 | function getActionContext(action: WrappedAction, actionsContext: ActionsContext) { 11 | const rootContext = actionsContext.root; 12 | const dynamicContext = actionsContext.dynamic[action.id] || {}; 13 | 14 | return { 15 | rootContext, 16 | dynamicContext, 17 | }; 18 | } 19 | 20 | export function checkActionAllowed(action: WrappedAction, actionsContext: ActionsContext) { 21 | if (!action.cond) { 22 | return true; 23 | } 24 | 25 | const { rootContext, dynamicContext } = getActionContext(action, actionsContext); 26 | 27 | const isAllowed = action.cond({ actionId: action.id, rootContext, dynamicContext }); 28 | return isAllowed; 29 | } 30 | 31 | export function runAction( 32 | action: WrappedAction, 33 | actionsContext: ActionsContext, 34 | storeMethods: RunStoreMethods 35 | ) { 36 | const { id, run } = action; 37 | 38 | if (!run) { 39 | storeMethods.selectParentAction(id); 40 | return; 41 | } 42 | 43 | const { rootContext, dynamicContext } = getActionContext(action, actionsContext); 44 | run({ actionId: id, rootContext, dynamicContext }); 45 | storeMethods.closePalette(); 46 | } 47 | 48 | export function getShortcutHandlersMap( 49 | actionsList: WrappedActionList, 50 | actionsContext: ActionsContext, 51 | storeMethods: StoreMethods 52 | ) { 53 | const shortcutMap: KeyBindingMap = {}; 54 | 55 | actionsList.forEach((action) => { 56 | const actionHandler = (event: KeyboardEvent) => { 57 | const targetElem = event.target as HTMLElement; 58 | const shortcutsAttr = targetElem.dataset.cpKbdShortcuts; 59 | 60 | if (shortcutsAttr === 'disabled') { 61 | return; 62 | } 63 | 64 | const isAllowed = checkActionAllowed(action, actionsContext); 65 | 66 | if (!isAllowed) { 67 | return; 68 | } 69 | 70 | event.preventDefault(); 71 | runAction(action, actionsContext, storeMethods); 72 | }; 73 | 74 | const shortcut = action.shortcut; 75 | if (shortcut) { 76 | shortcutMap[shortcut] = actionHandler; 77 | } 78 | }); 79 | 80 | return shortcutMap; 81 | } 82 | 83 | type ActiveParentActionIdListArg = Readonly>; 84 | 85 | export function getActiveParentAction(activeParentActionIdList: ActiveParentActionIdListArg) { 86 | const activeId = activeParentActionIdList.at(-1) || rootParentActionId; 87 | const isRoot = activeId === rootParentActionId; 88 | 89 | return { 90 | activeId, 91 | isRoot, 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const rootParentActionId = '__ROOT__'; 2 | -------------------------------------------------------------------------------- /src/lib/createActionList.ts: -------------------------------------------------------------------------------- 1 | import { createMemo, createEffect } from 'solid-js'; 2 | import Fuse from 'fuse.js'; 3 | import { useStore } from './StoreContext'; 4 | import { checkActionAllowed, getActiveParentAction } from './actionUtils/actionUtils'; 5 | import { WrappedAction } from './types'; 6 | 7 | export function createActionList() { 8 | const [state] = useStore(); 9 | 10 | const actionsList = createMemo(() => { 11 | return Object.values(state.actions); 12 | }); 13 | 14 | return actionsList; 15 | } 16 | 17 | export function createNestedActionList() { 18 | const actionsList = createActionList(); 19 | const [state] = useStore(); 20 | 21 | function nestedActionFilter(action: WrappedAction) { 22 | const { activeId, isRoot } = getActiveParentAction(state.activeParentActionIdList); 23 | 24 | const isAllowed = isRoot || action.parentActionId === activeId; 25 | return isAllowed; 26 | } 27 | 28 | const nestedActionsList = createMemo(() => { 29 | const nestedActionsList = actionsList().filter(nestedActionFilter); 30 | return nestedActionsList; 31 | }); 32 | 33 | return nestedActionsList; 34 | } 35 | 36 | export function createConditionalActionList() { 37 | const [state] = useStore(); 38 | const nestedActionsList = createNestedActionList(); 39 | 40 | function conditionalActionFilter(action: WrappedAction) { 41 | const isAllowed = checkActionAllowed(action, state.actionsContext); 42 | return isAllowed; 43 | } 44 | 45 | const conditionalActionList = createMemo(() => { 46 | const conditionalActionList = nestedActionsList().filter(conditionalActionFilter); 47 | return conditionalActionList; 48 | }); 49 | 50 | return conditionalActionList; 51 | } 52 | 53 | export function createSearchResultList() { 54 | const [state] = useStore(); 55 | const conditionalActionList = createConditionalActionList(); 56 | 57 | const fuse = new Fuse(conditionalActionList(), { 58 | keys: [ 59 | { 60 | name: 'title', 61 | weight: 1, 62 | }, 63 | { 64 | name: 'subtitle', 65 | weight: 0.7, 66 | }, 67 | { 68 | name: 'keywords', 69 | weight: 0.5, 70 | }, 71 | ], 72 | }); 73 | 74 | const resultsList = createMemo(() => { 75 | if (state.searchText.length === 0) { 76 | return conditionalActionList(); 77 | } 78 | 79 | const searchResults = fuse.search(state.searchText); 80 | 81 | const resultsList = searchResults.map((result) => result.item); 82 | return resultsList; 83 | }); 84 | 85 | createEffect(() => { 86 | fuse.setCollection(conditionalActionList()); 87 | }); 88 | 89 | return resultsList; 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/createKbdShortcuts.ts: -------------------------------------------------------------------------------- 1 | import { onMount, onCleanup } from 'solid-js'; 2 | import tinykeys from 'tinykeys'; 3 | import { useStore } from './StoreContext'; 4 | import { createActionList } from './createActionList'; 5 | import { getShortcutHandlersMap } from './actionUtils/actionUtils'; 6 | 7 | type Unsubscribe = null | ReturnType; 8 | 9 | export function createKbdShortcuts() { 10 | const [state, storeMethods] = useStore(); 11 | const { togglePalette } = storeMethods; 12 | const actionsList = createActionList(); 13 | 14 | let unsubscribe: Unsubscribe = null; 15 | 16 | onMount(() => { 17 | const shortcutMap = getShortcutHandlersMap(actionsList(), state.actionsContext, storeMethods); 18 | 19 | const commandPaletteHandler = (event: KeyboardEvent) => { 20 | event.preventDefault(); 21 | togglePalette(); 22 | }; 23 | 24 | unsubscribe = tinykeys(window, { 25 | ...shortcutMap, 26 | '$mod+k': commandPaletteHandler, 27 | }); 28 | }); 29 | 30 | onCleanup(() => { 31 | if (unsubscribe) { 32 | unsubscribe(); 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/createSyncActionsContext.ts: -------------------------------------------------------------------------------- 1 | import { createEffect, onCleanup } from 'solid-js'; 2 | import { useStore } from './StoreContext'; 3 | import { CreateSyncActionsContext } from './types'; 4 | 5 | export const createSyncActionsContext: CreateSyncActionsContext = (actionId, callback) => { 6 | const [_state, { setActionsContext, resetActionsContext }] = useStore(); 7 | 8 | createEffect(() => { 9 | const data = callback(); 10 | setActionsContext(actionId, data); 11 | }); 12 | 13 | onCleanup(() => { 14 | resetActionsContext(actionId); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/defineAction.ts: -------------------------------------------------------------------------------- 1 | import { Action, PartialAction } from './types'; 2 | 3 | export const defineAction = (partialAction: PartialAction): Action => { 4 | const id = partialAction.id || Math.random().toString(); 5 | const parentActionId = partialAction.parentActionId || null; 6 | const title = partialAction.title; 7 | const subtitle = partialAction.subtitle || null; 8 | const keywords = partialAction.keywords || []; 9 | const shortcut = partialAction.shortcut || null; 10 | const run = partialAction.run; 11 | 12 | const normalizedAction = { 13 | id, 14 | parentActionId, 15 | title, 16 | subtitle, 17 | keywords, 18 | shortcut, 19 | cond: partialAction.cond, 20 | run, 21 | }; 22 | 23 | return normalizedAction; 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Root'; 2 | export * from './CommandPalette'; 3 | export * from './KbdShortcut/KbdShortcut'; 4 | export * from './createSyncActionsContext'; 5 | export * from './defineAction'; 6 | export * from './useControls'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'solid-js'; 2 | import { DeepReadonly, Store } from 'solid-js/store'; 3 | 4 | export type ActionId = string; 5 | export type ParentActionId = null | ActionId; 6 | export type ActionShortcut = string; 7 | 8 | export type ActionContext = Record; 9 | 10 | export type DynamicContextMap = Record; 11 | 12 | export interface ActionsContext { 13 | root: ActionContext; 14 | dynamic: DynamicContextMap; 15 | } 16 | 17 | export interface RunArgs { 18 | actionId: ActionId; 19 | rootContext: ActionContext; 20 | dynamicContext: ActionContext; 21 | } 22 | 23 | export interface Action { 24 | id: ActionId; 25 | parentActionId: ParentActionId; 26 | title: string; 27 | subtitle: null | string; 28 | keywords: Array; 29 | /** 30 | * Keyboard Shortcut like `$mod+e`, `Shift+p`. 31 | */ 32 | shortcut: null | ActionShortcut; 33 | /** 34 | * Enable the action conditionally. 35 | */ 36 | cond?: (args: RunArgs) => boolean; 37 | run?: (args: RunArgs) => void; 38 | } 39 | 40 | export type PartialAction = Partial & { 41 | id: ActionId; 42 | title: Action['title']; 43 | }; 44 | 45 | export type Actions = Record; 46 | export type ActionsList = Array; 47 | export type WrappedAction = DeepReadonly; 48 | export type WrappedActionList = Array; 49 | 50 | export interface ResultContentProps { 51 | action: WrappedAction; 52 | isActive: boolean; 53 | } 54 | 55 | export interface Components { 56 | ResultContent: Component; 57 | } 58 | 59 | export interface RootProps { 60 | actions: Actions; 61 | actionsContext: ActionContext; 62 | components?: Components; 63 | } 64 | 65 | export interface StoreState { 66 | visibility: 'opened' | 'closed'; 67 | searchText: string; 68 | activeParentActionIdList: Array; 69 | actions: Actions; 70 | actionsContext: ActionsContext; 71 | components?: Components; 72 | } 73 | 74 | export type StoreStateWrapped = Store; 75 | 76 | export interface StoreMethods { 77 | setSearchText: (newValue: string) => void; 78 | setActionsContext: (actionId: ActionId, newData: ActionContext) => void; 79 | resetActionsContext: (actionId: ActionId) => void; 80 | openPalette: () => void; 81 | closePalette: () => void; 82 | togglePalette: () => void; 83 | selectParentAction: (parentActionId: ActionId) => void; 84 | revertParentAction: () => void; 85 | resetParentAction: () => void; 86 | } 87 | 88 | export type StoreContext = [StoreStateWrapped, StoreMethods]; 89 | 90 | type CreateSyncActionsContextCallback = () => ActionContext; 91 | 92 | export type CreateSyncActionsContext = ( 93 | actionId: ActionId, 94 | callback: CreateSyncActionsContextCallback 95 | ) => void; 96 | -------------------------------------------------------------------------------- /src/lib/useControls.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from './StoreContext'; 2 | 3 | export function useControls() { 4 | const [_state, storeMethods] = useStore(); 5 | 6 | return { 7 | openPalette: storeMethods.openPalette, 8 | closePalette: storeMethods.closePalette, 9 | togglePalette: storeMethods.togglePalette, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/utils.module.css: -------------------------------------------------------------------------------- 1 | .visuallyHidden { 2 | position: absolute !important; 3 | width: 1px !important; 4 | height: 1px !important; 5 | padding: 0 !important; 6 | margin: -1px !important; 7 | overflow: hidden !important; 8 | clip: rect(0, 0, 0, 0) !important; 9 | white-space: nowrap !important; 10 | word-wrap: normal !important; 11 | border: 0 !important; 12 | } 13 | 14 | .stripSpace { 15 | margin: 0; 16 | padding: 0; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig-node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "types": ["node"] 8 | }, 9 | "include": ["./scripts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "jsx": "preserve", 10 | "jsxImportSource": "solid-js", 11 | "types": ["vite/client"], 12 | "declaration": true, 13 | "outDir": "pkg-dist/types" 14 | }, 15 | "include": ["./src"], 16 | "exclude": ["./src/app", "./src/**/*.test.*"] 17 | } 18 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }], 3 | "github": { 4 | "silent": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /vite-pkg.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | import solidPlugin from 'vite-plugin-solid'; 4 | 5 | export default defineConfig({ 6 | plugins: [solidPlugin()], 7 | publicDir: false, 8 | build: { 9 | outDir: 'pkg-dist', 10 | lib: { 11 | entry: path.resolve(__dirname, 'src/lib/index.ts'), 12 | formats: ['es', 'cjs'], 13 | fileName: (format) => `solid-command-palette.${format}.js`, 14 | }, 15 | rollupOptions: { 16 | external: ['solid-js', 'tinykeys', 'fuse.js'], 17 | }, 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vite'; 4 | import solidPlugin from 'vite-plugin-solid'; 5 | 6 | export default defineConfig({ 7 | plugins: [solidPlugin()], 8 | build: { 9 | target: 'esnext', 10 | polyfillDynamicImport: false, 11 | }, 12 | test: { 13 | environment: 'happy-dom', 14 | clearMocks: true, 15 | }, 16 | }); 17 | --------------------------------------------------------------------------------