├── .github ├── assets │ ├── header.svg │ ├── logo-dark.svg │ └── logo-light.svg └── workflows │ ├── continuous-releases.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .release-it.json ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── biome.jsonc ├── package-lock.json ├── package.json ├── site ├── .env.example ├── components.json ├── liveblocks.config.ts ├── next-env.d.ts ├── next.config.ts ├── package.json ├── postcss.config.js ├── public │ ├── llms.txt │ └── registry │ │ ├── emoji-picker.json │ │ └── v0 │ │ ├── README.md │ │ ├── emoji-picker-popover.json │ │ └── emoji-picker.json ├── src │ ├── app │ │ ├── api │ │ │ └── liveblocks-auth │ │ │ │ ├── __tests__ │ │ │ │ └── create-user-id.test.ts │ │ │ │ ├── create-user-id.ts │ │ │ │ └── route.ts │ │ ├── icon.svg │ │ ├── inter-variable.woff2 │ │ ├── layout.client.tsx │ │ ├── layout.tsx │ │ ├── opengraph-image.png │ │ ├── page.tsx │ │ ├── robots.txt │ │ ├── sitemap.ts │ │ ├── styles.css │ │ └── twitter-image.png │ ├── components │ │ ├── copy-button.tsx │ │ ├── logo.tsx │ │ ├── permalink-heading.tsx │ │ ├── reactions.client.tsx │ │ ├── reactions.tsx │ │ ├── sections │ │ │ ├── docs.tsx │ │ │ ├── footer.tsx │ │ │ ├── header.client.tsx │ │ │ └── header.tsx │ │ ├── theme-provider.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── code-block.tsx │ │ │ ├── drawer.tsx │ │ │ ├── emoji-picker.tsx │ │ │ ├── popover.tsx │ │ │ ├── properties-list.tsx │ │ │ ├── tabs.tsx │ │ │ └── theme-switcher.tsx │ ├── config.ts │ ├── examples │ │ ├── colorful-buttons │ │ │ ├── colorful-buttons-alternate.client.tsx │ │ │ ├── colorful-buttons-alternate.tsx │ │ │ ├── colorful-buttons-blur.client.tsx │ │ │ └── colorful-buttons-blur.tsx │ │ ├── example-preview.tsx │ │ ├── shadcnui │ │ │ ├── shadcnui-popover.client.tsx │ │ │ ├── shadcnui-popover.tsx │ │ │ ├── shadcnui.client.tsx │ │ │ ├── shadcnui.tsx │ │ │ └── ui │ │ │ │ ├── button.tsx │ │ │ │ ├── emoji-picker.tsx │ │ │ │ └── popover.tsx │ │ └── usage │ │ │ ├── usage.client.tsx │ │ │ └── usage.tsx │ ├── hooks │ │ ├── use-initial-render.ts │ │ ├── use-mobile.ts │ │ ├── use-mounted.ts │ │ └── use-sticky.ts │ └── lib │ │ ├── get-fast-bounding-rects.ts │ │ ├── get-text-content.ts │ │ ├── toast.tsx │ │ └── utils.ts ├── test │ └── setup-jsdom.ts ├── tsconfig.json ├── turbo.json └── vitest.config.ts ├── src ├── components │ ├── __tests__ │ │ └── emoji-picker.test.browser.tsx │ └── emoji-picker.tsx ├── constants.ts ├── data │ ├── __tests__ │ │ ├── emoji-picker.test.ts │ │ └── emoji.test.ts │ ├── emoji-picker.ts │ └── emoji.ts ├── hooks.ts ├── index.ts ├── store.ts ├── types.ts └── utils │ ├── __tests__ │ ├── capitalize.test.ts │ ├── chunk.test.ts │ ├── compare.test.ts │ ├── format-as-shortcode.test.ts │ ├── get-skin-tone-variations.test.ts │ ├── is-emoji-supported.test.browser.ts │ ├── noop.test.ts │ ├── range.test.ts │ ├── request-idle-callback.test.ts │ ├── storage.test.ts │ ├── store.test.tsx │ ├── use-stable-callback.test.ts │ └── validate.test.ts │ ├── capitalize.ts │ ├── chunk.ts │ ├── compare.ts │ ├── format-as-shortcode.ts │ ├── get-skin-tone-variations.ts │ ├── is-emoji-supported.ts │ ├── noop.ts │ ├── range.ts │ ├── request-idle-callback.ts │ ├── storage.ts │ ├── store.tsx │ ├── use-layout-effect.ts │ ├── use-stable-callback.ts │ └── validate.ts ├── test ├── setup-browser.ts ├── setup-emojibase.ts └── setup-jsdom.ts ├── tsconfig.json ├── tsup.config.ts ├── turbo.json ├── vitest.config.ts └── vitest.workspace.ts /.github/workflows/continuous-releases.yml: -------------------------------------------------------------------------------- 1 | name: Continuous releases 2 | on: 3 | pull_request: 4 | concurrency: 5 | group: ${{ github.workflow }} 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout commit 11 | uses: actions/checkout@v4 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: 23 16 | cache: npm 17 | - name: Install dependencies 18 | run: npm ci 19 | - name: Build package 20 | run: npm run build 21 | - name: Release package on pkg.pr.new 22 | run: npx pkg-pr-new publish --compact --comment=off -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | increment: 6 | description: "Release increment (e.g. patch, minor, major, none (for existing prereleases))" 7 | required: false 8 | type: choice 9 | options: 10 | - patch 11 | - minor 12 | - major 13 | - none 14 | prerelease: 15 | description: "Prerelease tag (e.g. beta, alpha)" 16 | required: false 17 | type: string 18 | dry-run: 19 | description: 'Run the release without publishing for testing (set to "true")' 20 | required: false 21 | default: "false" 22 | concurrency: 23 | group: ${{ github.workflow }} 24 | run-name: "Release (increment: ${{ github.event.inputs.increment }}, prerelease: ${{ github.event.inputs.prerelease || 'none' }}${{ github.event.inputs.dry-run == 'true' && ', dry run' || '' }})" 25 | jobs: 26 | release: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout commit 30 | uses: actions/checkout@v4 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 23 35 | cache: npm 36 | - name: Setup git config 37 | run: | 38 | git config user.name "github-actions[bot]" 39 | git config user.email "github-actions[bot]@users.noreply.github.com" 40 | - name: Setup npm config 41 | run: | 42 | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 43 | env: 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | - name: Install dependencies 46 | run: npm ci 47 | - name: Release package 48 | run: | 49 | ARGS="" 50 | if [[ "${{ github.event.inputs.increment }}" != "none" ]]; then 51 | ARGS+="${{ github.event.inputs.increment }}" 52 | fi 53 | if [[ -n "${{ github.event.inputs.prerelease }}" ]]; then 54 | ARGS+=" --preRelease=${{ github.event.inputs.prerelease }}" 55 | fi 56 | if [[ "${{ github.event.inputs.dry-run }}" == "true" ]]; then 57 | ARGS+=" --dry-run" 58 | fi 59 | npm run release -- $ARGS --ci 60 | env: 61 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | concurrency: 8 | group: ${{ github.ref }} 9 | cancel-in-progress: true 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | steps: 15 | - name: Checkout commit 16 | uses: actions/checkout@v4 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 23 21 | cache: npm 22 | - name: Install dependencies 23 | run: npm ci 24 | - name: Build package 25 | run: npx turbo run frimousse#build 26 | - name: Run linting 27 | run: npm run lint 28 | test: 29 | runs-on: ubuntu-latest 30 | timeout-minutes: 5 31 | steps: 32 | - name: Checkout commit 33 | uses: actions/checkout@v4 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: 23 38 | cache: npm 39 | - name: Install dependencies 40 | run: npm ci 41 | - name: Install Playwright’s browser 42 | run: npx playwright install chromium 43 | - name: Run tests 44 | run: npx turbo run test 45 | - uses: actions/upload-artifact@v4 46 | if: failure() 47 | with: 48 | name: tests 49 | path: | 50 | **/__tests__/__screenshots__/ 51 | retention-days: 7 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .npmrc 2 | .idea/ 3 | .next/ 4 | .turbo/ 5 | .vercel/ 6 | .vim/ 7 | build/ 8 | coverage/ 9 | dist/ 10 | node_modules/ 11 | 12 | *.tsbuildinfo 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | .env.development.local 19 | .env.local 20 | .env.production.local 21 | .env.test.local 22 | 23 | *.pem 24 | .DS_Store 25 | 26 | __screenshots__ 27 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/release-it@18/schema/release-it.json", 3 | "plugins": { 4 | "@release-it/keep-a-changelog": { 5 | "filename": "CHANGELOG.md", 6 | "addUnreleased": true 7 | } 8 | }, 9 | "git": { 10 | "requireBranch": "main", 11 | "commitMessage": "Release v${version}", 12 | "tagName": "v${version}" 13 | }, 14 | "github": { 15 | "release": true, 16 | "releaseName": "v${version}" 17 | }, 18 | "hooks": { 19 | "before:init": ["npm run build", "npm run lint"], 20 | "after:bump": "npm run format -- package.json" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "biomejs.biome", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "quickfix.biome": "explicit", 6 | "source.fixAll.biome": "explicit", 7 | "source.organizeImports.biome": "explicit" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.2.0] - 2025-04-02 4 | 5 | - When setting `emojiVersion` on `EmojiPicker.Root`, this version of Emojibase’s data will be fetched instead of `latest`. 6 | - Add `emojibaseUrl` prop on `EmojiPicker.Root` to allow choosing where Emojibase’s data is fetched from: another CDN, self-hosted files, etc. 7 | 8 | ## [0.1.1] - 2025-03-31 9 | 10 | - Fix `EmojiPicker.Search` controlled value not updating search results when updated externally. (e.g. other input, manually, etc) 11 | 12 | ## [0.1.0] - 2025-03-18 13 | 14 | - Initial release. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Liveblocks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Frimousse 4 | 5 | 6 | Frimousse 7 | 8 |

9 | 10 | [![npm](https://img.shields.io/npm/v/frimousse?labelColor=651&color=fc0)](https://www.npmjs.com/package/frimousse) 11 | [![downloads](https://img.shields.io/npm/dm/frimousse?label=downloads&labelColor=651&color=fc0)](https://www.npmjs.com/package/frimousse) 12 | [![size](https://img.shields.io/bundlephobia/minzip/frimousse?label=size&labelColor=651&color=fc0)](https://bundlephobia.com/package/frimousse) 13 | [![tests](https://img.shields.io/github/actions/workflow/status/liveblocks/frimousse/.github/workflows/tests.yml?label=tests&labelColor=651&color=fc0)](https://github.com/liveblocks/frimousse/actions/workflows/tests.yml) 14 | [![license](https://img.shields.io/github/license/liveblocks/frimousse?labelColor=651&color=fc0)](https://github.com/liveblocks/frimousse/blob/main/LICENSE) 15 | 16 | A lightweight, unstyled, and composable emoji picker for React. 17 | 18 | - ⚡️ **Lightweight and fast**: Dependency-free, tree-shakable, and virtualized with minimal re-renders 19 | - 🎨 **Unstyled and composable**: Bring your own styles and compose parts as you want 20 | - 🔄 **Always up-to-date**: Latest emoji data is fetched when needed and cached locally 21 | - 🔣 **No � symbols**: Unsupported emojis are automatically hidden 22 | - ♿️ **Accessible**: Keyboard navigable and screen reader-friendly 23 | 24 | Various emoji pickers. 25 | 26 | ## Installation 27 | 28 | ```bash 29 | npm i frimousse 30 | ``` 31 | 32 | If you are using [shadcn/ui](https://ui.shadcn.com/), you can also install it as a pre-built component via the [shadcn CLI](https://ui.shadcn.com/docs/cli). 33 | 34 | ```bash 35 | npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker 36 | ``` 37 | 38 | Learn more in the [shadcn/ui](#shadcnui) section. 39 | 40 | ## Usage 41 | 42 | Import the `EmojiPicker` parts and create your own component by composing them. 43 | 44 | ```tsx 45 | import { EmojiPicker } from "frimousse"; 46 | 47 | export function MyEmojiPicker() { 48 | return ( 49 | 50 | 51 | 52 | Loading… 53 | No emoji found. 54 | 55 | 56 | 57 | ); 58 | } 59 | ``` 60 | 61 | Apart from a few sizing and overflow defaults, the parts don’t have any styles out-of-the-box. Being composable, you can bring your own styles and apply them however you want: [Tailwind CSS](https://tailwindcss.com/), CSS-in-JS, vanilla CSS via inline styles, classes, or by targeting the `[frimousse-*]` attributes present on each part. 62 | 63 | You might want to use it in a popover rather than on its own. Frimousse only provides the emoji picker itself so if you don’t have a popover component in your app yet, there are several libraries available: [Radix UI](https://www.radix-ui.com/primitives/docs/components/popover), [Base UI](https://base-ui.com/react/components/popover), [Headless UI](https://headlessui.com/react/popover), and [React Aria](https://react-spectrum.adobe.com/react-aria/Popover.html), to name a few. 64 | 65 | ### shadcn/ui 66 | 67 | If you are using [shadcn/ui](https://ui.shadcn.com/), you can install a pre-built version which integrates with the existing shadcn/ui variables via the [shadcn CLI](https://ui.shadcn.com/docs/cli). 68 | 69 | ```bash 70 | npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker 71 | ``` 72 | 73 | It can be composed and combined with other shadcn/ui components like [Popover](https://ui.shadcn.com/docs/components/popover). 74 | 75 | ## Documentation 76 | 77 | Find the full documentation and examples on [frimousse.liveblocks.io](https://frimousse.liveblocks.io). 78 | 79 | ## Miscellaneous 80 | 81 | The name [“frimousse”](https://en.wiktionary.org/wiki/frimousse) means “little face” in French, and it can also refer to smileys and emoticons. 82 | 83 | The emoji picker component was originally created for the [Liveblocks Comments](https://liveblocks.io/comments) default components, within [`@liveblocks/react-ui`](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-react-ui). 84 | 85 | ## Credits 86 | 87 | The emoji data is based on [Emojibase](https://emojibase.dev/). 88 | 89 | ## Contributing 90 | 91 | All contributions are welcome! If you find a bug or have a feature request, feel free to create an [issue](https://github.com/liveblocks/frimousse/issues) or a [PR](https://github.com/liveblocks/frimousse/pulls). 92 | 93 | The project is setup as a monorepo with the `frimousse` package at the root and [frimousse.liveblocks.io](https://frimousse.liveblocks.io) in the `site` directory. 94 | 95 | ### Development 96 | 97 | Install dependencies and start development builds from the root. 98 | 99 | ```bash 100 | npm i 101 | npm run dev 102 | ``` 103 | 104 | The site can be used as a development playground since it’s built with the root package via [Turborepo](https://turbo.build/repo). 105 | 106 | ```bash 107 | npm run dev:site 108 | ``` 109 | 110 | ### Tests 111 | 112 | The package has 95%+ test coverage with [Vitest](https://vitest.dev/). Some tests use Vitest’s [browser mode](https://vitest.dev/guide/browser-testing) with [Playwright](https://playwright.dev/), make sure to install the required browser first. 113 | 114 | ```bash 115 | npx playwright install chromium 116 | ``` 117 | 118 | Run the tests. 119 | 120 | ```bash 121 | npm run test:coverage 122 | ``` 123 | 124 | ### Releases 125 | 126 | Releases are triggered from [a GitHub action](.github/workflows/release.yml) via [release-it](https://github.com/release-it/release-it), and continuous releases are automatically triggered for every commit in PRs via [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). 127 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "formatWithErrors": true, 6 | "indentStyle": "space", 7 | "lineWidth": 80 8 | }, 9 | "organizeImports": { "enabled": true }, 10 | "assists": { 11 | "enabled": true, 12 | "actions": { 13 | "source": { 14 | "sortJsxProps": "on" 15 | } 16 | } 17 | }, 18 | "linter": { 19 | "enabled": true, 20 | "rules": { 21 | "recommended": true, 22 | "correctness": { 23 | "noUnusedFunctionParameters": "error", 24 | "noUnusedVariables": "error", 25 | "noUnusedImports": "error", 26 | "useExhaustiveDependencies": { 27 | "level": "error", 28 | "options": { 29 | "hooks": [ 30 | { "name": "useCreateStore", "stableResult": true }, 31 | { "name": "useStore", "stableResult": true }, 32 | { "name": "useEmojiPickerStore", "stableResult": true } 33 | ] 34 | } 35 | } 36 | }, 37 | "suspicious": { 38 | "noExplicitAny": "off", 39 | "noArrayIndexKey": "off" 40 | }, 41 | "style": { 42 | "noNonNullAssertion": "off" 43 | }, 44 | "a11y": { 45 | "useSemanticElements": "off", 46 | "noAutofocus": "off" 47 | }, 48 | "nursery": { 49 | "useSortedClasses": { 50 | "fix": "safe", 51 | "level": "error", 52 | "options": { 53 | "functions": ["classNames", "clsx", "cn"] 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "css": { 60 | "parser": { 61 | "cssModules": true 62 | } 63 | }, 64 | "vcs": { 65 | "enabled": true, 66 | "clientKind": "git", 67 | "useIgnoreFile": true, 68 | "defaultBranch": "main" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frimousse", 3 | "description": "A lightweight, unstyled, and composable emoji picker for React.", 4 | "version": "0.2.0", 5 | "license": "MIT", 6 | "packageManager": "npm@11.1.0", 7 | "type": "module", 8 | "workspaces": [".", "site"], 9 | "sideEffects": false, 10 | "main": "./dist/index.cjs", 11 | "types": "./dist/index.d.cts", 12 | "exports": { 13 | ".": { 14 | "import": { 15 | "types": "./dist/index.d.ts", 16 | "default": "./dist/index.js" 17 | }, 18 | "require": { 19 | "types": "./dist/index.d.cts", 20 | "default": "./dist/index.cjs" 21 | } 22 | } 23 | }, 24 | "files": ["dist/**", "README.md", "LICENSE"], 25 | "scripts": { 26 | "dev": "tsup --watch", 27 | "dev:site": "turbo run dev --filter=site", 28 | "build": "tsup --minify", 29 | "build:site": "turbo run build --filter=site", 30 | "test": "vitest run --silent", 31 | "test:watch": "vitest watch --silent", 32 | "test:coverage": "npm run test -- --coverage", 33 | "format": "biome check --write --assists-enabled=true", 34 | "lint": "turbo run lint:tsc lint:biome lint:package", 35 | "lint:tsc": "tsc --noEmit", 36 | "lint:biome": "biome lint", 37 | "lint:package": "publint --strict && attw --pack", 38 | "release": "release-it" 39 | }, 40 | "peerDependencies": { 41 | "react": "^18 || ^19" 42 | }, 43 | "devDependencies": { 44 | "@arethetypeswrong/cli": "^0.17.4", 45 | "@biomejs/biome": "^1.9.4", 46 | "@release-it/keep-a-changelog": "^6.0.0", 47 | "@testing-library/jest-dom": "^6.6.3", 48 | "@testing-library/react": "^16.2.0", 49 | "@types/react": "^19.0.10", 50 | "@vitest/browser": "^3.0.8", 51 | "@vitest/coverage-v8": "^3.0.8", 52 | "emojibase": "^16.0.0", 53 | "emojibase-data": "^16.0.2", 54 | "jsdom": "^26.0.0", 55 | "pkg-pr-new": "^0.0.41", 56 | "playwright": "^1.51.0", 57 | "publint": "^0.3.9", 58 | "release-it": "^18.1.2", 59 | "tsup": "^8.4.0", 60 | "turbo": "^2.4.4", 61 | "typescript": "^5.8.2", 62 | "vitest": "^3.0.8", 63 | "vitest-browser-react": "^0.1.1", 64 | "vitest-fetch-mock": "^0.4.5" 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/liveblocks/frimousse/issues" 68 | }, 69 | "repository": { 70 | "type": "git", 71 | "url": "git+https://github.com/liveblocks/frimousse.git" 72 | }, 73 | "homepage": "https://frimousse.liveblocks.io", 74 | "keywords": [ 75 | "emoji", 76 | "emoji picker", 77 | "react", 78 | "unstyled", 79 | "component", 80 | "emojibase", 81 | "liveblocks" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /site/.env.example: -------------------------------------------------------------------------------- 1 | # https://liveblocks.io/dashboard/apikeys 2 | LIVEBLOCKS_SECRET_KEY= 3 | -------------------------------------------------------------------------------- /site/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/styles.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /site/liveblocks.config.ts: -------------------------------------------------------------------------------- 1 | import type { LiveMap } from "@liveblocks/client"; 2 | 3 | export type Reactions = LiveMap>; 4 | 5 | export type ReactionsJson = Record>; 6 | 7 | export type ReactionsJsonEntries = [string, Record][]; 8 | 9 | declare global { 10 | interface Liveblocks { 11 | Storage: { 12 | reactions: Reactions; 13 | }; 14 | StorageJson: { 15 | reactions: ReactionsJson; 16 | }; 17 | } 18 | } 19 | 20 | export const ROOM_ID = "frimousse"; 21 | 22 | export const CREATED_AT_KEY = "@createdAt"; 23 | export const UPDATED_AT_KEY = "@updatedAt"; 24 | export const DEFAULT_KEYS = [CREATED_AT_KEY, UPDATED_AT_KEY]; 25 | export const DEFAULT_KEYS_COUNT = DEFAULT_KEYS.length; 26 | 27 | // Roughly 3 rows of reactions on largest breakpoint 28 | export const MAX_REACTIONS = 30; 29 | 30 | export function sortReactions( 31 | [, dataA]: [string, LiveMap | ReadonlyMap], 32 | [, dataB]: [string, LiveMap | ReadonlyMap], 33 | ) { 34 | return (dataB.get(CREATED_AT_KEY) ?? 0) - (dataA.get(CREATED_AT_KEY) ?? 0); 35 | } 36 | 37 | export function sortReactionsEntries( 38 | [, dataA]: ReactionsJsonEntries[number], 39 | [, dataB]: ReactionsJsonEntries[number], 40 | ) { 41 | return (dataB[CREATED_AT_KEY] ?? 0) - (dataA[CREATED_AT_KEY] ?? 0); 42 | } 43 | 44 | function createDefaultReactions(emojis: string[]) { 45 | const reactions: ReactionsJson = {}; 46 | 47 | for (const [index, emoji] of Object.entries( 48 | emojis.slice(0, MAX_REACTIONS).reverse(), 49 | )) { 50 | if (Number(index) > MAX_REACTIONS) { 51 | break; 52 | } 53 | 54 | reactions[emoji] = { 55 | [CREATED_AT_KEY]: Number(index), 56 | [UPDATED_AT_KEY]: Number(index), 57 | }; 58 | 59 | // Initialize reactions pseudo-randomly between 1 and 15 60 | const seed = (Number(index) * 9301 + 49297) % 233280; 61 | const count = (seed % 15) + 1; 62 | 63 | for (let i = 0; i < count; i++) { 64 | reactions[emoji][`#${i}`] = 1; 65 | } 66 | } 67 | 68 | return reactions; 69 | } 70 | 71 | export const DEFAULT_REACTIONS = createDefaultReactions([ 72 | "😊", 73 | "👋", 74 | "🎨", 75 | "💬", 76 | "🌱", 77 | "🫶", 78 | "🌈", 79 | "🔥", 80 | "🫰", 81 | "🌚", 82 | "👋", 83 | "🏳️‍🌈", 84 | "✨", 85 | "📚", 86 | "🎵", 87 | "👸", 88 | "🤓", 89 | "🔮", 90 | "🗿", 91 | "🏳️‍⚧️", 92 | "😶", 93 | "🥖", 94 | "🦋", 95 | "🌸", 96 | "🎹", 97 | "🎉", 98 | "🤔", 99 | "🧩", 100 | "🐈‍⬛", 101 | "🧶", 102 | "🪀", 103 | "🥸", 104 | "🪁", 105 | "🤌", 106 | "🪐", 107 | "🌹", 108 | "🎼", 109 | "🤹", 110 | "👀", 111 | "🍂", 112 | "🍬", 113 | "🍭", 114 | "🎀", 115 | "🎈", 116 | "🤩", 117 | "👒", 118 | "🏝️", 119 | "🌊", 120 | "😵‍💫", 121 | "🥁", 122 | "🎶", 123 | ]); 124 | -------------------------------------------------------------------------------- /site/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /site/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | experimental: { 5 | dynamicIO: true, 6 | useCache: true, 7 | ppr: true, 8 | }, 9 | async rewrites() { 10 | return [ 11 | { 12 | source: "/r/:path*", 13 | destination: "/registry/:path*.json", 14 | }, 15 | ]; 16 | }, 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "test": "vitest run --silent", 10 | "test:watch": "vitest watch --silent", 11 | "format": "biome check --write --assists-enabled=true", 12 | "lint:tsc": "tsc --noEmit", 13 | "lint:biome": "biome lint" 14 | }, 15 | "dependencies": { 16 | "@liveblocks/client": "^2.20.0", 17 | "@liveblocks/node": "^2.20.0", 18 | "@liveblocks/react": "^2.20.0", 19 | "@number-flow/react": "^0.5.7", 20 | "@radix-ui/react-popover": "^1.1.6", 21 | "@radix-ui/react-slot": "^1.1.2", 22 | "@radix-ui/react-tabs": "^1.1.3", 23 | "@shikijs/transformers": "^3.2.1", 24 | "@vercel/functions": "^2.0.0", 25 | "class-variance-authority": "^0.7.1", 26 | "clsx": "^2.1.1", 27 | "dedent": "^1.5.3", 28 | "frimousse": "file:../", 29 | "lucide-react": "^0.482.0", 30 | "motion": "^12.5.0", 31 | "next": "15.3.0-canary.10", 32 | "next-themes": "^0.4.6", 33 | "react": "^19.0.0", 34 | "react-dom": "^19.0.0", 35 | "react-error-boundary": "^5.0.0", 36 | "shiki": "^3.2.1", 37 | "slugify": "^1.6.6", 38 | "sonner": "^2.0.1", 39 | "tailwind-merge": "^3.0.2", 40 | "tailwindcss-animate": "^1.0.7", 41 | "vaul": "^1.1.2" 42 | }, 43 | "devDependencies": { 44 | "@tailwindcss/postcss": "^4.1.2", 45 | "postcss": "^8.5.3", 46 | "tailwindcss": "^4.1.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /site/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /site/public/llms.txt: -------------------------------------------------------------------------------- 1 | # Frimousse 2 | 3 | A lightweight, unstyled, and composable emoji picker for React. 4 | 5 | - ⚡️ **Lightweight and fast**: Dependency-free, tree-shakable, and virtualized with minimal re-renders 6 | - 🎨 **Unstyled and composable**: Bring your own styles and compose parts as you want 7 | - 🔄 **Always up-to-date**: Latest emoji data is fetched when needed and cached locally 8 | - 🔣 **No � symbols**: Unsupported emojis are automatically hidden 9 | - ♿️ **Accessible**: Keyboard navigable and screen reader-friendly 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm i frimousse 15 | ``` 16 | 17 | If you are using [shadcn/ui](https://ui.shadcn.com/), you can also install it as a pre-built component via the [shadcn CLI](https://ui.shadcn.com/docs/cli). 18 | 19 | ```bash 20 | npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker 21 | ``` 22 | 23 | Learn more in the [shadcn/ui](#shadcnui) section. 24 | 25 | ## Usage 26 | 27 | Import the `EmojiPicker` parts and create your own component by composing them. 28 | 29 | ```tsx 30 | import { EmojiPicker } from "frimousse"; 31 | 32 | export function MyEmojiPicker() { 33 | return ( 34 | 35 | 36 | 37 | Loading… 38 | No emoji found. 39 | 40 | 41 | 42 | ); 43 | } 44 | ``` 45 | 46 | Apart from a few sizing and overflow defaults, the parts don’t have any styles out-of-the-box. Being composable, you can bring your own styles and apply them however you want: [Tailwind CSS](https://tailwindcss.com/), CSS-in-JS, vanilla CSS via inline styles, classes, or by targeting the `[frimousse-*]` attributes present on each part. 47 | 48 | You might want to use it in a popover rather than on its own. Frimousse only provides the emoji picker itself so if you don’t have a popover component in your app yet, there are several libraries available: [Radix UI](https://www.radix-ui.com/primitives/docs/components/popover), [Base UI](https://base-ui.com/react/components/popover), [Headless UI](https://headlessui.com/react/popover), and [React Aria](https://react-spectrum.adobe.com/react-aria/Popover.html), to name a few. 49 | 50 | ### shadcn/ui 51 | 52 | If you are using [shadcn/ui](https://ui.shadcn.com/), you can install a pre-built version which integrates with the existing shadcn/ui variables via the [shadcn CLI](https://ui.shadcn.com/docs/cli). 53 | 54 | ```bash 55 | npx shadcn@latest add https://frimousse.liveblocks.io/r/emoji-picker 56 | ``` 57 | 58 | It can be composed and combined with other shadcn/ui components like [Popover](https://ui.shadcn.com/docs/components/popover). 59 | 60 | ## Documentation 61 | 62 | Find the full documentation and examples on [frimousse.liveblocks.io](https://frimousse.liveblocks.io). 63 | 64 | ## Miscellaneous 65 | 66 | The name [“frimousse”](https://en.wiktionary.org/wiki/frimousse) means “little face” in French, and it can also refer to smileys and emoticons. 67 | 68 | The emoji picker component was originally created for the [Liveblocks Comments](https://liveblocks.io/comments) default components, within [`@liveblocks/react-ui`](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-react-ui). 69 | 70 | ## Credits 71 | 72 | The emoji data is based on [Emojibase](https://emojibase.dev/). 73 | -------------------------------------------------------------------------------- /site/public/registry/emoji-picker.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "homepage": "https://frimousse.liveblocks.io/", 4 | "author": "Liveblocks (https://liveblocks.io/)", 5 | "name": "emoji-picker", 6 | "type": "registry:ui", 7 | "dependencies": ["frimousse", "lucide-react"], 8 | "files": [ 9 | { 10 | "type": "registry:ui", 11 | "path": "src/examples/shadcnui/ui/emoji-picker.tsx", 12 | "target": "components/ui/emoji-picker.tsx", 13 | "content": "\"use client\";\n\nimport {\n type EmojiPickerListCategoryHeaderProps,\n type EmojiPickerListEmojiProps,\n type EmojiPickerListRowProps,\n EmojiPicker as EmojiPickerPrimitive,\n} from \"frimousse\";\nimport { LoaderIcon, SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction EmojiPicker({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction EmojiPickerSearch({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n );\n}\n\nfunction EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) {\n return (\n
\n {children}\n
\n );\n}\n\nfunction EmojiPickerEmoji({\n emoji,\n className,\n ...props\n}: EmojiPickerListEmojiProps) {\n return (\n \n {emoji.emoji}\n \n );\n}\n\nfunction EmojiPickerCategoryHeader({\n category,\n ...props\n}: EmojiPickerListCategoryHeaderProps) {\n return (\n \n {category.label}\n \n );\n}\n\nfunction EmojiPickerContent({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n \n No emoji found.\n \n \n \n );\n}\n\nfunction EmojiPickerFooter({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n \n \n {({ emoji }) =>\n emoji ? (\n <>\n
\n {emoji.emoji}\n
\n \n {emoji.label}\n \n \n ) : (\n \n Select an emoji…\n \n )\n }\n
\n \n );\n}\n\nexport {\n EmojiPicker,\n EmojiPickerSearch,\n EmojiPickerContent,\n EmojiPickerFooter,\n};" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /site/public/registry/v0/README.md: -------------------------------------------------------------------------------- 1 | These registry items aren’t meant to be installed via the shadcn CLI, they’re specifically built to be opened as examples in [v0](https://v0.dev/). 2 | 3 | ### Tailwind CSS v4 4 | 5 | These examples are built for Tailwind CSS v3 as v0 doesn’t support Tailwind CSS v4 yet: 6 | - `outline-hidden` → `outline-none` 7 | - `rounded-sm` → `rounded-md` 8 | - `max-w-(--frimousse-viewport-width)` → `max-w-[--frimousse-viewport-width]` 9 | 10 | When Tailwind CSS v4 is supported, the `components/ui/emoji-picker.tsx` entries (duplicated from `/public/registry/emoji-picker.json`) should be removed and replaced by `"registryDependencies": ["https://frimousse.liveblocks.io/r/emoji-picker"]`. -------------------------------------------------------------------------------- /site/public/registry/v0/emoji-picker-popover.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "homepage": "https://frimousse.liveblocks.io/", 4 | "author": "Liveblocks (https://liveblocks.io/)", 5 | "name": "emoji-picker-popover", 6 | "type": "registry:block", 7 | "categories": ["emoji-picker"], 8 | "dependencies": ["frimousse", "lucide-react"], 9 | "registryDependencies": ["popover"], 10 | "files": [ 11 | { 12 | "type": "registry:ui", 13 | "path": "src/examples/shadcnui/ui/emoji-picker.tsx", 14 | "target": "components/ui/emoji-picker.tsx", 15 | "content": "\"use client\";\n\nimport {\n type EmojiPickerListCategoryHeaderProps,\n type EmojiPickerListEmojiProps,\n type EmojiPickerListRowProps,\n EmojiPicker as EmojiPickerPrimitive,\n} from \"frimousse\";\nimport { LoaderIcon, SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction EmojiPicker({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction EmojiPickerSearch({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n );\n}\n\nfunction EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) {\n return (\n
\n {children}\n
\n );\n}\n\nfunction EmojiPickerEmoji({\n emoji,\n className,\n ...props\n}: EmojiPickerListEmojiProps) {\n return (\n \n {emoji.emoji}\n \n );\n}\n\nfunction EmojiPickerCategoryHeader({\n category,\n ...props\n}: EmojiPickerListCategoryHeaderProps) {\n return (\n \n {category.label}\n \n );\n}\n\nfunction EmojiPickerContent({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n \n No emoji found.\n \n \n \n );\n}\n\nfunction EmojiPickerFooter({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n \n \n {({ emoji }) =>\n emoji ? (\n <>\n
\n {emoji.emoji}\n
\n \n {emoji.label}\n \n \n ) : (\n \n Select an emoji…\n \n )\n }\n
\n \n );\n}\n\nexport {\n EmojiPicker,\n EmojiPickerSearch,\n EmojiPickerContent,\n EmojiPickerFooter,\n};" 16 | }, 17 | { 18 | "type": "registry:page", 19 | "path": "src/examples/shadcnui/shadcnui-popover.tsx", 20 | "target": "app/page.tsx", 21 | "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n EmojiPicker,\n EmojiPickerSearch,\n EmojiPickerContent,\n EmojiPickerFooter,\n} from \"@/components/ui/emoji-picker\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\n\nexport default function Page() {\n const [isOpen, setIsOpen] = React.useState(false);\n\n return (\n
\n \n \n \n \n \n {\n setIsOpen(false);\n console.log(emoji);\n }}\n >\n \n \n \n \n \n \n
\n );\n}" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /site/public/registry/v0/emoji-picker.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "homepage": "https://frimousse.liveblocks.io/", 4 | "author": "Liveblocks (https://liveblocks.io/)", 5 | "name": "emoji-picker", 6 | "type": "registry:block", 7 | "categories": ["emoji-picker"], 8 | "dependencies": ["frimousse", "lucide-react"], 9 | "files": [ 10 | { 11 | "type": "registry:ui", 12 | "path": "src/examples/shadcnui/ui/emoji-picker.tsx", 13 | "target": "components/ui/emoji-picker.tsx", 14 | "content": "\"use client\";\n\nimport {\n type EmojiPickerListCategoryHeaderProps,\n type EmojiPickerListEmojiProps,\n type EmojiPickerListRowProps,\n EmojiPicker as EmojiPickerPrimitive,\n} from \"frimousse\";\nimport { LoaderIcon, SearchIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction EmojiPicker({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction EmojiPickerSearch({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n );\n}\n\nfunction EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) {\n return (\n
\n {children}\n
\n );\n}\n\nfunction EmojiPickerEmoji({\n emoji,\n className,\n ...props\n}: EmojiPickerListEmojiProps) {\n return (\n \n {emoji.emoji}\n \n );\n}\n\nfunction EmojiPickerCategoryHeader({\n category,\n ...props\n}: EmojiPickerListCategoryHeaderProps) {\n return (\n \n {category.label}\n \n );\n}\n\nfunction EmojiPickerContent({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n \n No emoji found.\n \n \n \n );\n}\n\nfunction EmojiPickerFooter({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n \n \n {({ emoji }) =>\n emoji ? (\n <>\n
\n {emoji.emoji}\n
\n \n {emoji.label}\n \n \n ) : (\n \n Select an emoji…\n \n )\n }\n
\n \n );\n}\n\nexport {\n EmojiPicker,\n EmojiPickerSearch,\n EmojiPickerContent,\n EmojiPickerFooter,\n};" 15 | }, 16 | { 17 | "type": "registry:page", 18 | "path": "src/examples/shadcnui/shadcnui.tsx", 19 | "target": "app/page.tsx", 20 | "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n EmojiPicker,\n EmojiPickerSearch,\n EmojiPickerContent,\n} from \"@/components/ui/emoji-picker\";\n\nexport default function Page() {\n return (\n
\n {\n console.log(emoji);\n }}\n >\n \n \n \n
\n );\n}\n" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /site/src/app/api/liveblocks-auth/__tests__/create-user-id.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createUserId } from "../create-user-id"; 3 | 4 | function generateRandomIp() { 5 | const octet1 = Math.floor(Math.random() * 223) + 1; 6 | const octet2 = Math.floor(Math.random() * 256); 7 | const octet3 = Math.floor(Math.random() * 256); 8 | const octet4 = Math.floor(Math.random() * 256); 9 | 10 | return `${octet1}.${octet2}.${octet3}.${octet4}`; 11 | } 12 | 13 | describe("createUserId", () => { 14 | it("should generate consistent user IDs for the same IP", () => { 15 | const userId1 = createUserId("127.0.0.1"); 16 | const userId2 = createUserId("127.0.0.1"); 17 | 18 | expect(userId1).toBe(userId2); 19 | }); 20 | 21 | it("should generate different user IDs for different IPs", () => { 22 | const userId1 = createUserId("127.0.0.1"); 23 | const userId2 = createUserId("192.168.1.1"); 24 | 25 | expect(userId1).not.toBe(userId2); 26 | }); 27 | 28 | it("should generate different user IDs for the same IP with different salts", () => { 29 | const ip = "127.0.0.1"; 30 | 31 | const userId1 = createUserId(ip, "123"); 32 | const userId2 = createUserId(ip, "456"); 33 | 34 | expect(userId1).not.toBe(userId2); 35 | }); 36 | 37 | it("should have minimal conflicts", () => { 38 | const samples = 100000; 39 | const userIds = new Set(); 40 | const conflicts: Array<{ ip: string; userId: string }> = []; 41 | 42 | for (const ip of Array.from({ length: samples }, generateRandomIp)) { 43 | const userId = createUserId(ip); 44 | 45 | if (userIds.has(userId)) { 46 | conflicts.push({ ip, userId }); 47 | } else { 48 | userIds.add(userId); 49 | } 50 | } 51 | 52 | // Less than 0.1% chance of conflict 53 | expect(conflicts.length / samples).toBeLessThan(0.001); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /site/src/app/api/liveblocks-auth/create-user-id.ts: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto"; 2 | 3 | export function createUserId(ip = "0.0.0.0", salt = "") { 4 | return crypto 5 | .createHash("sha256") 6 | .update(ip + salt) 7 | .digest("base64") 8 | .slice(0, 5); 9 | } 10 | -------------------------------------------------------------------------------- /site/src/app/api/liveblocks-auth/route.ts: -------------------------------------------------------------------------------- 1 | import { Liveblocks } from "@liveblocks/node"; 2 | import { ipAddress } from "@vercel/functions"; 3 | import { type NextRequest, NextResponse } from "next/server"; 4 | import { createUserId } from "./create-user-id"; 5 | 6 | const liveblocks = new Liveblocks({ 7 | secret: process.env.LIVEBLOCKS_SECRET_KEY!, 8 | }); 9 | 10 | export async function POST(request: NextRequest) { 11 | if (!process.env.LIVEBLOCKS_SECRET_KEY) { 12 | return new NextResponse("Missing LIVEBLOCKS_SECRET_KEY", { status: 403 }); 13 | } 14 | 15 | const userId = createUserId( 16 | ipAddress(request), 17 | process.env.LIVEBLOCKS_USER_ID_SALT, 18 | ); 19 | const session = liveblocks.prepareSession(userId); 20 | session.allow("*", session.FULL_ACCESS); 21 | const { status, body } = await session.authorize(); 22 | 23 | return new NextResponse(body, { status }); 24 | } 25 | -------------------------------------------------------------------------------- /site/src/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /site/src/app/inter-variable.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liveblocks/frimousse/24156fea33e069a2b593c40a1671cb2651609ab9/site/src/app/inter-variable.woff2 -------------------------------------------------------------------------------- /site/src/app/layout.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useLayoutEffect } from "react"; 4 | 5 | const IOS_REGEX = /iPad|iPhone/; 6 | const SCALE_REGEX = /maximum\-scale=[0-9\.]+/g; 7 | 8 | export function DynamicMaximumScaleMeta() { 9 | useLayoutEffect(() => { 10 | if (!IOS_REGEX.test(navigator.userAgent)) { 11 | return; 12 | } 13 | 14 | const meta = document.querySelector("meta[name=viewport]"); 15 | 16 | if (!meta) { 17 | return; 18 | } 19 | 20 | const content = meta.getAttribute("content") ?? ""; 21 | 22 | meta.setAttribute( 23 | "content", 24 | SCALE_REGEX.test(content) 25 | ? content.replace(SCALE_REGEX, "maximum-scale=1.0") 26 | : `${content}, maximum-scale=1.0`, 27 | ); 28 | 29 | return () => { 30 | meta.setAttribute("content", content); 31 | }; 32 | }, []); 33 | 34 | return null; 35 | } 36 | -------------------------------------------------------------------------------- /site/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "@/components/sections/footer"; 2 | import { cn } from "@/lib/utils"; 3 | import type { Metadata } from "next"; 4 | import { ThemeProvider } from "next-themes"; 5 | import { JetBrains_Mono } from "next/font/google"; 6 | import localFont from "next/font/local"; 7 | import type { PropsWithChildren } from "react"; 8 | import { Toaster } from "sonner"; 9 | import { DynamicMaximumScaleMeta } from "./layout.client"; 10 | import "./styles.css"; 11 | import { config } from "@/config"; 12 | 13 | const inter = localFont({ 14 | src: "./inter-variable.woff2", 15 | variable: "--font-inter", 16 | }); 17 | 18 | const jetbrainsMono = JetBrains_Mono({ 19 | subsets: ["latin"], 20 | variable: "--font-jetbrains-mono", 21 | }); 22 | 23 | export const metadata: Metadata = { 24 | title: { 25 | default: config.name, 26 | template: `%s — ${config.name}`, 27 | }, 28 | metadataBase: new URL(config.url), 29 | alternates: { 30 | canonical: "/", 31 | }, 32 | description: config.description, 33 | keywords: [ 34 | "emoji", 35 | "emoji picker", 36 | "react", 37 | "unstyled", 38 | "component", 39 | "emojibase", 40 | "liveblocks", 41 | ], 42 | authors: [ 43 | { 44 | name: "Liveblocks", 45 | url: "https://liveblocks.io", 46 | }, 47 | ], 48 | creator: "Liveblocks", 49 | openGraph: { 50 | type: "website", 51 | locale: "en_US", 52 | url: config.url, 53 | title: config.name, 54 | description: config.description, 55 | siteName: config.name, 56 | }, 57 | twitter: { 58 | card: "summary_large_image", 59 | title: config.name, 60 | description: config.description, 61 | creator: "@liveblocks", 62 | }, 63 | }; 64 | 65 | export default function RootLayout({ children }: PropsWithChildren) { 66 | return ( 67 | 68 | 69 | 75 | 76 | 77 |
83 | {children} 84 |
85 |