├── .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 |
9 |
10 | [](https://www.npmjs.com/package/frimousse)
11 | [](https://www.npmjs.com/package/frimousse)
12 | [](https://bundlephobia.com/package/frimousse)
13 | [](https://github.com/liveblocks/frimousse/actions/workflows/tests.yml)
14 | [](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 |
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 );\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 );\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 );\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 |
--------------------------------------------------------------------------------
/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 |
86 |
87 |
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/site/src/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liveblocks/frimousse/24156fea33e069a2b593c40a1671cb2651609ab9/site/src/app/opengraph-image.png
--------------------------------------------------------------------------------
/site/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Docs } from "@/components/sections/docs";
2 | import { Header } from "@/components/sections/header";
3 |
4 | export default function Page() {
5 | return (
6 | <>
7 |
8 |
9 | >
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/site/src/app/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------
/site/src/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import { config } from "@/config";
2 | import type { MetadataRoute } from "next";
3 |
4 | export default async function sitemap(): Promise {
5 | "use cache";
6 | return [
7 | {
8 | url: config.url,
9 | lastModified: new Date(),
10 | changeFrequency: "monthly",
11 | priority: 1,
12 | },
13 | ];
14 | }
15 |
--------------------------------------------------------------------------------
/site/src/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liveblocks/frimousse/24156fea33e069a2b593c40a1671cb2651609ab9/site/src/app/twitter-image.png
--------------------------------------------------------------------------------
/site/src/components/copy-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useIsMounted } from "@/hooks/use-mounted";
4 | import { cn } from "@/lib/utils";
5 | import { Check, Copy } from "lucide-react";
6 | import { AnimatePresence, type Variants, motion } from "motion/react";
7 | import { useCallback, useRef, useState } from "react";
8 | import { Button } from "./ui/button";
9 |
10 | const COPY_ANIMATION_DURATION = 2000;
11 |
12 | const variants: Variants = {
13 | visible: {
14 | opacity: 1,
15 | scale: 1,
16 | transition: { duration: 0.2 },
17 | },
18 | hidden: {
19 | opacity: 0,
20 | scale: 0.8,
21 | transition: { duration: 0.1 },
22 | },
23 | };
24 |
25 | function CopyButtonIcon({ isAnimating }: { isAnimating: boolean }) {
26 | return (
27 |
28 | {isAnimating ? (
29 |
36 |
37 |
38 | ) : (
39 |
46 |
47 |
48 | )}
49 |
50 | );
51 | }
52 |
53 | export function CopyButton({
54 | text,
55 | className,
56 | label = "Copy code",
57 | }: {
58 | text: string;
59 | className?: string;
60 | label?: string;
61 | }) {
62 | const timeout = useRef(0);
63 | const isMounted = useIsMounted();
64 | const [isAnimating, setIsAnimating] = useState(false);
65 |
66 | const copyToClipboard = useCallback(async (text: string) => {
67 | window.clearTimeout(timeout.current);
68 |
69 | try {
70 | await navigator.clipboard.writeText(text);
71 | } catch {}
72 | }, []);
73 |
74 | const handleCopy = useCallback(() => {
75 | copyToClipboard(text);
76 | setIsAnimating(true);
77 |
78 | setTimeout(() => {
79 | setIsAnimating(false);
80 | }, COPY_ANIMATION_DURATION);
81 | }, [copyToClipboard, text]);
82 |
83 | return (
84 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/site/src/components/logo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { type ComponentProps, useEffect, useState } from "react";
5 |
6 | const ICONS = [Face, Heart, Flash];
7 | const INTERVAL = 400;
8 |
9 | function Face(props: ComponentProps<"svg">) {
10 | return (
11 |
20 | );
21 | }
22 |
23 | function Heart(props: ComponentProps<"svg">) {
24 | return (
25 |
34 | );
35 | }
36 |
37 | function Flash(props: ComponentProps<"svg">) {
38 | return (
39 |
48 | );
49 | }
50 |
51 | export function Logo({
52 | className,
53 | ...props
54 | }: Omit, "children">) {
55 | const [currentIndex, setCurrentIndex] = useState(0);
56 |
57 | useEffect(() => {
58 | const interval = setInterval(() => {
59 | setCurrentIndex((previousIndex) => (previousIndex + 1) % ICONS.length);
60 | }, INTERVAL);
61 |
62 | return () => {
63 | clearInterval(interval);
64 | };
65 | }, []);
66 |
67 | const Icon = ICONS[currentIndex];
68 |
69 | if (!Icon) {
70 | return null;
71 | }
72 |
73 | return (
74 | svg]:absolute [&>svg]:inset-0 [&>svg]:size-full",
77 | className,
78 | )}
79 | {...props}
80 | >
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/site/src/components/permalink-heading.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { getTextContent } from "@/lib/get-text-content";
4 | import { cn } from "@/lib/utils";
5 | import { type ComponentProps, useMemo } from "react";
6 | import slugify from "slugify";
7 |
8 | type Heading = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
9 |
10 | interface PermalinkHeadingProps extends ComponentProps {
11 | as?: Heading;
12 | slug?: string;
13 | slugPrefix?: string;
14 | }
15 |
16 | export function PermalinkHeading({
17 | as = "h1",
18 | slug: customSlug,
19 | slugPrefix,
20 | className,
21 | children,
22 | ...props
23 | }: PermalinkHeadingProps) {
24 | const Heading = as;
25 | const slug = useMemo(() => {
26 | return slugify(
27 | (slugPrefix ? `${slugPrefix} ` : "") +
28 | (customSlug ?? getTextContent(children)),
29 | { lower: true },
30 | );
31 | }, [customSlug, slugPrefix, children]);
32 |
33 | return (
34 |
39 | {children}
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/site/src/components/reactions.tsx:
--------------------------------------------------------------------------------
1 | import { Liveblocks as LiveblocksClient } from "@liveblocks/node";
2 | import {
3 | DEFAULT_REACTIONS,
4 | ROOM_ID,
5 | type ReactionsJson,
6 | } from "liveblocks.config";
7 | import { unstable_cacheLife as cachelife } from "next/cache";
8 | import { type ComponentProps, Suspense } from "react";
9 | import {
10 | Reactions as ClientReactions,
11 | FallbackReactions,
12 | ReactionsList,
13 | } from "./reactions.client";
14 |
15 | const liveblocks = new LiveblocksClient({
16 | secret: process.env.LIVEBLOCKS_SECRET_KEY!,
17 | });
18 |
19 | async function ServerReactions() {
20 | "use cache";
21 |
22 | cachelife("seconds");
23 |
24 | let reactions: ReactionsJson;
25 |
26 | try {
27 | reactions = (await liveblocks.getStorageDocument(ROOM_ID, "json"))
28 | .reactions;
29 | } catch {
30 | reactions = DEFAULT_REACTIONS;
31 | }
32 |
33 | if (!reactions || Object.keys(reactions).length === 0) {
34 | reactions = DEFAULT_REACTIONS;
35 | }
36 |
37 | return ;
38 | }
39 |
40 | export function Reactions(props: Omit, "children">) {
41 | return (
42 |
43 | }>
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/site/src/components/sections/footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { unstable_cacheLife as cacheLife } from "next/cache";
3 | import { type ComponentProps, Suspense } from "react";
4 | import { buttonVariants } from "../ui/button";
5 | import { ThemeSwitcher } from "../ui/theme-switcher";
6 |
7 | async function Year(props: ComponentProps<"time">) {
8 | "use cache";
9 |
10 | cacheLife("hours");
11 |
12 | const year = String(new Date().getFullYear());
13 |
14 | return (
15 |
18 | );
19 | }
20 |
21 | export function Footer() {
22 | return (
23 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/site/src/components/sections/header.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useIsMounted } from "@/hooks/use-mounted";
4 | import { useIsSticky } from "@/hooks/use-sticky";
5 | import { cn } from "@/lib/utils";
6 | import { useRef } from "react";
7 | import { Logo } from "../logo";
8 | import { buttonVariants } from "../ui/button";
9 |
10 | export function StickyHeader({ version }: { version: string }) {
11 | const stickyRef = useRef(null!);
12 | const isSticky = useIsSticky(stickyRef);
13 | const isMounted = useIsMounted();
14 |
15 | return (
16 | <>
17 |
59 |
89 | >
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/site/src/components/sections/header.tsx:
--------------------------------------------------------------------------------
1 | import { readFileSync } from "node:fs";
2 | import { join } from "node:path";
3 | import { Reactions } from "../reactions";
4 | import { StickyHeader } from "./header.client";
5 |
6 | export function Header() {
7 | const pkg = JSON.parse(
8 | readFileSync(join(process.cwd(), "../package.json"), "utf-8"),
9 | );
10 |
11 | return (
12 | <>
13 |
14 |
15 | A lightweight, unstyled, and composable emoji picker for React.
16 |
17 |
18 | >
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/site/src/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import type { ComponentProps } from "react";
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: ComponentProps) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/site/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { type VariantProps, cva } from "class-variance-authority";
4 | import type { ComponentProps } from "react";
5 |
6 | interface ButtonProps
7 | extends ComponentProps<"button">,
8 | VariantProps {
9 | asChild?: boolean;
10 | }
11 |
12 | const buttonVariants = cva(
13 | "transition duration-200 ease-out inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-sm font-medium disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:flex-none",
14 | {
15 | variants: {
16 | variant: {
17 | none: "",
18 | default:
19 | "bg-primary text-primary-foreground hover:bg-primary/80 focus-visible:bg-primary/80 data-[state=open]:bg-primary/80 selection:bg-primary-foreground/20",
20 | secondary:
21 | "bg-muted text-secondary-foreground hover:bg-secondary/60 focus-visible:bg-secondary/60 data-[state=open]:bg-secondary/60 outline-secondary",
22 | ghost:
23 | "hover:bg-muted focus-visible:bg-muted data-[state=open]:bg-muted text-muted-foreground hover:text-secondary-foreground focus-visible:text-secondary-foreground data-[state=open]:text-secondary-foreground",
24 | outline:
25 | "border border-dotted hover:bg-muted focus-visible:bg-muted data-[state=open]:bg-muted text-secondary-foreground hover:text-foreground focus-visible:text-foreground data-[state=open]:text-foreground",
26 | },
27 | size: {
28 | default:
29 | "h-8 px-4 py-2 has-[>svg]:px-3 [&_svg:not([class*='size-'])]:size-4 text-sm",
30 | sm: "h-6 px-1.5 py-0.5 has-[>svg]:px-2 [&_svg:not([class*='size-'])]:size-3.5 text-xs",
31 | icon: "size-8",
32 | },
33 | },
34 | defaultVariants: {
35 | variant: "default",
36 | size: "default",
37 | },
38 | },
39 | );
40 |
41 | function Button({
42 | className,
43 | variant,
44 | size,
45 | asChild = false,
46 | ...props
47 | }: ButtonProps) {
48 | const Component = asChild ? Slot : "button";
49 |
50 | return (
51 |
56 | );
57 | }
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/site/src/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | "use cache";
2 |
3 | import { cn } from "@/lib/utils";
4 | import {
5 | transformerNotationDiff,
6 | transformerNotationErrorLevel,
7 | transformerNotationHighlight,
8 | transformerNotationWordHighlight,
9 | } from "@shikijs/transformers";
10 | import dedent from "dedent";
11 | import type { ComponentProps } from "react";
12 | import type { BundledLanguage } from "shiki";
13 | import { codeToHtml } from "shiki";
14 | import { CopyButton } from "../copy-button";
15 |
16 | const TRANSFORMERS_ANNOTATION_REGEX = /\[!code(?:\s+\w+(:\w+)?)?\]/;
17 |
18 | interface CodeBlockProps extends Omit, "children"> {
19 | lang: BundledLanguage;
20 | children: string;
21 | }
22 |
23 | function removeTransformersAnnotations(code: string): string {
24 | return code
25 | .split("\n")
26 | .filter((line) => !TRANSFORMERS_ANNOTATION_REGEX.test(line))
27 | .join("\n");
28 | }
29 |
30 | export async function CodeBlock({
31 | children,
32 | lang,
33 | className,
34 | ...props
35 | }: CodeBlockProps) {
36 | const code = dedent(children);
37 | const html = await codeToHtml(code, {
38 | lang,
39 | themes: {
40 | light: "github-light",
41 | dark: "github-dark",
42 | },
43 | defaultColor: false,
44 | transformers: [
45 | transformerNotationDiff(),
46 | transformerNotationErrorLevel(),
47 | transformerNotationHighlight(),
48 | transformerNotationWordHighlight(),
49 | ],
50 | });
51 |
52 | return (
53 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/site/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import {
5 | type ComponentPropsWithoutRef,
6 | type ComponentRef,
7 | type HTMLAttributes,
8 | forwardRef,
9 | } from "react";
10 | import { Drawer as DrawerPrimitive } from "vaul";
11 |
12 | const Drawer = DrawerPrimitive.Root;
13 |
14 | const DrawerTrigger = DrawerPrimitive.Trigger;
15 |
16 | const DrawerPortal = DrawerPrimitive.Portal;
17 |
18 | const DrawerClose = DrawerPrimitive.Close;
19 |
20 | const DrawerOverlay = forwardRef<
21 | ComponentRef,
22 | ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 |
31 | const DrawerContent = forwardRef<
32 | ComponentRef,
33 | ComponentPropsWithoutRef
34 | >(({ className, children, ...props }, ref) => (
35 |
36 |
37 |
45 |
46 |
47 |
48 | {children}
49 |
50 |
51 | ));
52 |
53 | const DrawerHeader = ({
54 | className,
55 | ...props
56 | }: HTMLAttributes) => (
57 |
61 | );
62 |
63 | const DrawerFooter = ({
64 | className,
65 | ...props
66 | }: HTMLAttributes) => (
67 |
71 | );
72 |
73 | const DrawerTitle = forwardRef<
74 | ComponentRef,
75 | ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
78 | ));
79 |
80 | const DrawerDescription = forwardRef<
81 | ComponentRef,
82 | ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
89 | ));
90 |
91 | export {
92 | Drawer,
93 | DrawerPortal,
94 | DrawerOverlay,
95 | DrawerTrigger,
96 | DrawerClose,
97 | DrawerContent,
98 | DrawerHeader,
99 | DrawerFooter,
100 | DrawerTitle,
101 | DrawerDescription,
102 | };
103 |
--------------------------------------------------------------------------------
/site/src/components/ui/emoji-picker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import {
5 | type EmojiPickerListCategoryHeaderProps,
6 | type EmojiPickerListEmojiProps,
7 | type EmojiPickerListRowProps,
8 | EmojiPicker as EmojiPickerPrimitive,
9 | type EmojiPickerRootProps,
10 | } from "frimousse";
11 | import type { CSSProperties, ComponentProps } from "react";
12 | import { buttonVariants } from "./button";
13 |
14 | interface EmojiPickerProps extends EmojiPickerRootProps {
15 | autoFocus?: boolean;
16 | }
17 |
18 | function SearchIcon(props: ComponentProps<"svg">) {
19 | return (
20 |
32 | );
33 | }
34 |
35 | function SpinnerIcon(props: ComponentProps<"svg">) {
36 | return (
37 |
49 | );
50 | }
51 |
52 | function EmojiPickerRow({
53 | children,
54 | className,
55 | ...props
56 | }: EmojiPickerListRowProps) {
57 | return (
58 |
65 | {children}
66 |
67 | );
68 | }
69 |
70 | function EmojiPickerEmoji({
71 | emoji,
72 | className,
73 | style,
74 | ...props
75 | }: EmojiPickerListEmojiProps) {
76 | return (
77 |
93 | );
94 | }
95 |
96 | function EmojiPickerCategoryHeader({
97 | category,
98 | className,
99 | ...props
100 | }: EmojiPickerListCategoryHeaderProps) {
101 | return (
102 |
109 | {category.label}
110 |
111 | );
112 | }
113 |
114 | function EmojiPicker({
115 | className,
116 | autoFocus,
117 | columns,
118 | ...props
119 | }: EmojiPickerProps) {
120 | const skinToneSelector = (
121 |
128 | );
129 |
130 | return (
131 |
139 |
140 |
141 |
142 |
146 |
147 |
{skinToneSelector}
148 |
149 |
150 |
151 |
152 |
153 |
154 | No emoji found.
155 |
156 |
164 |
165 |
166 |
167 | {({ emoji }) =>
168 | emoji ? (
169 | <>
170 |
171 | {emoji.emoji}
172 |
173 |
174 | {emoji.label}
175 |
176 | >
177 | ) : (
178 |
179 | Select an emoji…
180 |
181 | )
182 | }
183 |
184 |
{skinToneSelector}
185 |
186 |
187 | );
188 | }
189 |
190 | export { EmojiPicker };
191 |
--------------------------------------------------------------------------------
/site/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import * as PopoverPrimitive from "@radix-ui/react-popover";
5 | import type { ComponentProps } from "react";
6 |
7 | function Popover(props: ComponentProps) {
8 | return ;
9 | }
10 |
11 | function PopoverTrigger(
12 | props: ComponentProps,
13 | ) {
14 | return ;
15 | }
16 |
17 | function PopoverContent({
18 | className,
19 | align = "center",
20 | sideOffset = 4,
21 | children,
22 | ...props
23 | }: ComponentProps) {
24 | return (
25 |
26 |
38 | {children}
39 |
40 |
41 | );
42 | }
43 |
44 | function PopoverAnchor(props: ComponentProps) {
45 | return ;
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
49 |
--------------------------------------------------------------------------------
/site/src/components/ui/properties-list.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import type { ComponentProps } from "react";
3 |
4 | interface PropertiesListRowProps
5 | extends Omit, "name" | "type"> {
6 | name: string;
7 | type?: string;
8 | required?: boolean;
9 | defaultValue?: string;
10 | }
11 |
12 | export function PropertiesList({
13 | children,
14 | className,
15 | ...props
16 | }: ComponentProps<"ul">) {
17 | return (
18 |
28 | );
29 | }
30 |
31 | export function PropertiesListBasicRow({
32 | children,
33 | className,
34 | ...props
35 | }: ComponentProps<"li">) {
36 | return (
37 |
38 | {children}
39 |
40 | );
41 | }
42 |
43 | export function PropertiesListRow({
44 | name,
45 | type,
46 | required,
47 | defaultValue,
48 | children,
49 | className,
50 | ...props
51 | }: PropertiesListRowProps) {
52 | return (
53 |
54 |
55 |
56 | {name}
57 |
58 | {type && (
59 |
60 | {type}
61 |
62 | )}
63 | {required && (
64 |
65 | Required
66 |
67 | )}
68 | {defaultValue && (
69 |
70 | Default is {defaultValue}
71 |
72 | )}
73 |
74 | {children}
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/site/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import * as TabsPrimitive from "@radix-ui/react-tabs";
3 | import type { ComponentProps, ReactNode } from "react";
4 |
5 | interface Tab {
6 | name: string;
7 | label?: ReactNode;
8 | children: ReactNode;
9 | }
10 |
11 | interface TabsProps
12 | extends Omit, "children"> {
13 | tabs: Tab[];
14 | }
15 |
16 | export function Tabs({ tabs, className, ...props }: TabsProps) {
17 | return (
18 |
23 |
27 | {tabs.map((tab) => (
28 |
34 | {tab.label ?? tab.name}
35 |
36 | ))}
37 |
38 | {tabs.map((tab) => (
39 |
45 | {tab.children}
46 |
47 | ))}
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/site/src/components/ui/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import { Monitor, Moon, Sun } from "lucide-react";
5 | import { motion } from "motion/react";
6 | import { useTheme } from "next-themes";
7 | import { type ComponentProps, useDeferredValue } from "react";
8 |
9 | const THEMES = [
10 | {
11 | type: "system",
12 | icon: Monitor,
13 | label: "system theme",
14 | },
15 | {
16 | type: "light",
17 | icon: Sun,
18 | label: "light theme",
19 | },
20 | {
21 | type: "dark",
22 | icon: Moon,
23 | label: "dark theme",
24 | },
25 | ] as const;
26 |
27 | type Theme = (typeof THEMES)[number]["type"];
28 |
29 | interface ThemeSwitcherProps
30 | extends Omit, "onChange" | "value" | "defaultValue"> {
31 | value?: Theme;
32 | onChange?: (theme: Theme) => void;
33 | defaultValue?: Theme;
34 | }
35 |
36 | function ThemeSwitcher({
37 | value,
38 | onChange,
39 | defaultValue,
40 | className,
41 | ...props
42 | }: ThemeSwitcherProps) {
43 | const { theme, setTheme } = useTheme();
44 | const deferredTheme = useDeferredValue(theme, "system");
45 |
46 | return (
47 |
54 | {THEMES.map(({ type, icon: Icon, label }) => {
55 | const isActive = deferredTheme === type;
56 |
57 | return (
58 |
88 | );
89 | })}
90 |
91 | );
92 | }
93 |
94 | export { ThemeSwitcher };
95 |
--------------------------------------------------------------------------------
/site/src/config.ts:
--------------------------------------------------------------------------------
1 | export const config = {
2 | name: "Frimousse — An emoji picker for React",
3 | url: "https://frimousse.liveblocks.io",
4 | description:
5 | "Frimousse is an open-source, lightweight, unstyled, and composable emoji picker for React—originally created for Liveblocks Comments. Styles can be applied with CSS, Tailwind CSS, CSS-in-JS, and more.",
6 | links: {
7 | twitter: "https://x.com/liveblocks",
8 | github: "https://github.com/liveblocks/frimousse",
9 | },
10 | } as const;
11 |
--------------------------------------------------------------------------------
/site/src/examples/colorful-buttons/colorful-buttons-alternate.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { toast } from "@/lib/toast";
4 | import { cn } from "@/lib/utils";
5 | import type { Emoji as EmojiObject } from "frimousse";
6 | import { type ComponentProps, type PointerEvent, useCallback } from "react";
7 | import { ExamplePreview } from "../example-preview";
8 |
9 | interface ListProps extends ComponentProps<"div"> {
10 | rows: number;
11 | columns: number;
12 | }
13 |
14 | interface RowProps extends ComponentProps<"div"> {
15 | index: number;
16 | }
17 |
18 | interface EmojiProps extends ComponentProps<"button"> {
19 | emoji: EmojiObject;
20 | index: number;
21 | }
22 |
23 | function List({ rows, columns, children, ...props }: ListProps) {
24 | const clearActiveEmojis = useCallback(() => {
25 | const emojis = Array.from(document.querySelectorAll("[frimousse-emoji]"));
26 |
27 | for (const emoji of emojis) {
28 | emoji.removeAttribute("data-active");
29 | }
30 | }, []);
31 |
32 | const setActiveEmoji = useCallback(
33 | (event: PointerEvent) => {
34 | clearActiveEmojis();
35 |
36 | const emoji = document.elementFromPoint(event.clientX, event.clientY);
37 |
38 | if (emoji?.hasAttribute("frimousse-emoji")) {
39 | emoji.setAttribute("data-active", "");
40 | }
41 | },
42 | [clearActiveEmojis],
43 | );
44 |
45 | return (
46 |
57 | {children}
58 |
59 | );
60 | }
61 |
62 | function Row({ index, style, className, children, ...props }: RowProps) {
63 | return (
64 |
77 | {children}
78 |
79 | );
80 | }
81 |
82 | function Emoji({
83 | emoji,
84 | index,
85 | style,
86 | className,
87 | children,
88 | ...props
89 | }: EmojiProps) {
90 | return (
91 |
120 | );
121 | }
122 |
123 | export function ColorfulButtonsAlternatePreview() {
124 | return (
125 |
126 |
127 |
128 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | Hover or focus to see the effect
151 |
152 |
153 | );
154 | }
155 |
--------------------------------------------------------------------------------
/site/src/examples/colorful-buttons/colorful-buttons-alternate.tsx:
--------------------------------------------------------------------------------
1 | import { CodeBlock } from "@/components/ui/code-block";
2 | import { Tabs } from "@/components/ui/tabs";
3 | import { cn } from "@/lib/utils";
4 | import type { ComponentProps } from "react";
5 | import { ColorfulButtonsAlternatePreview } from "./colorful-buttons-alternate.client";
6 |
7 | export function ColorfulButtonsAlternate({
8 | className,
9 | ...props
10 | }: Omit, "children">) {
11 | return (
12 |
16 |
17 |
18 |
19 | {`
28 | (
31 |
32 | {children}
33 |
34 | ),
35 | Emoji: ({ emoji, ...props }) => {
36 | return (
37 |
43 | );
44 | },
45 | }}
46 | />
47 | `}
48 | ),
49 | },
50 | {
51 | name: "css",
52 | label: "CSS",
53 | children: (
54 | {`
55 | [frimousse-emoji] {
56 | display: flex;
57 | align-items: center;
58 | justify-content: center;
59 | width: 32px;
60 | height: 32px;
61 | border-radius: 6px;
62 | background: transparent;
63 | font-size: 18px;
64 |
65 | &[data-active] {
66 | [frimousse-row]:nth-child(odd) &:nth-child(3n+1),
67 | [frimousse-row]:nth-child(even) &:nth-child(3n+2) {
68 | background: light-dark(#ffe2e2, #82181a);
69 | }
70 |
71 | [frimousse-row]:nth-child(odd) &:nth-child(3n+2),
72 | [frimousse-row]:nth-child(even) &:nth-child(3n+3) {
73 | background: light-dark(#dcfce7, #0d542b);
74 | }
75 |
76 | [frimousse-row]:nth-child(odd) &:nth-child(3n+3),
77 | [frimousse-row]:nth-child(even) &:nth-child(3n+1) {
78 | background: light-dark(#dbeafe, #1c398e);
79 | }
80 | }
81 | }
82 | `}
83 | ),
84 | },
85 | ]}
86 | />
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/site/src/examples/colorful-buttons/colorful-buttons-blur.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { toast } from "@/lib/toast";
4 | import { cn } from "@/lib/utils";
5 | import type { Emoji as EmojiObject } from "frimousse";
6 | import { type ComponentProps, type PointerEvent, useCallback } from "react";
7 | import { ExamplePreview } from "../example-preview";
8 |
9 | interface ListProps extends ComponentProps<"div"> {
10 | rows: number;
11 | columns: number;
12 | }
13 |
14 | interface RowProps extends ComponentProps<"div"> {
15 | index: number;
16 | }
17 |
18 | interface EmojiProps extends ComponentProps<"button"> {
19 | emoji: EmojiObject;
20 | index: number;
21 | }
22 |
23 | function List({ rows, columns, children, ...props }: ListProps) {
24 | const clearActiveEmojis = useCallback(() => {
25 | const emojis = Array.from(document.querySelectorAll("[frimousse-emoji]"));
26 |
27 | for (const emoji of emojis) {
28 | emoji.removeAttribute("data-active");
29 | }
30 | }, []);
31 |
32 | const setActiveEmoji = useCallback(
33 | (event: PointerEvent) => {
34 | clearActiveEmojis();
35 |
36 | const emoji = document.elementFromPoint(event.clientX, event.clientY);
37 |
38 | if (emoji?.hasAttribute("frimousse-emoji")) {
39 | emoji.setAttribute("data-active", "");
40 | }
41 | },
42 | [clearActiveEmojis],
43 | );
44 |
45 | return (
46 |
57 | {children}
58 |
59 | );
60 | }
61 |
62 | function Row({ index, style, children, ...props }: RowProps) {
63 | return (
64 |
76 | {children}
77 |
78 | );
79 | }
80 |
81 | function Emoji({
82 | emoji,
83 | index,
84 | style,
85 | children,
86 | className,
87 | ...props
88 | }: EmojiProps) {
89 | return (
90 |
124 | );
125 | }
126 |
127 | export function ColorfulButtonsBlurPreview() {
128 | return (
129 |
130 |
131 |
132 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | Hover or focus to see the effect
155 |
156 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/site/src/examples/colorful-buttons/colorful-buttons-blur.tsx:
--------------------------------------------------------------------------------
1 | import { CodeBlock } from "@/components/ui/code-block";
2 | import { Tabs } from "@/components/ui/tabs";
3 | import { cn } from "@/lib/utils";
4 | import type { ComponentProps } from "react";
5 | import { ColorfulButtonsBlurPreview } from "./colorful-buttons-blur.client";
6 |
7 | export function ColorfulButtonsBlur({
8 | className,
9 | ...props
10 | }: Omit, "children">) {
11 | return (
12 |
16 |
17 |
18 |
19 | {`
28 | {
31 | return (
32 |
43 | );
44 | },
45 | }}
46 | />
47 | `}
48 | ),
49 | },
50 | {
51 | name: "css",
52 | label: "CSS",
53 | children: (
54 | {`
55 | [frimousse-emoji] {
56 | position: relative;
57 | display: flex;
58 | align-items: center;
59 | justify-content: center;
60 | width: 32px;
61 | height: 32px;
62 | border-radius: 6px;
63 | background: transparent;
64 | font-size: 18px;
65 | overflow: hidden;
66 |
67 | &::before {
68 | content: var(--emoji);
69 | position: absolute;
70 | inset: 0;
71 | z-index: -1;
72 | display: none;
73 | align-items: center;
74 | justify-content: center;
75 | font-size: 2.5em;
76 | filter: blur(16px) saturate(200%);
77 | }
78 |
79 | &[data-active] {
80 | background: light-dark(rgb(245 245 245 / 80%), rgb(38 38 38 / 80%));
81 |
82 | &::before {
83 | display: flex;
84 | }
85 | }
86 | }
87 | `}
88 | ),
89 | },
90 | ]}
91 | />
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/site/src/examples/example-preview.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { LoaderCircleIcon } from "lucide-react";
3 | import { useInView } from "motion/react";
4 | import { type ComponentProps, useRef } from "react";
5 |
6 | export function ExamplePreview({
7 | children,
8 | className,
9 | ...props
10 | }: ComponentProps<"div">) {
11 | const ref = useRef(null!);
12 | const isInView = useInView(ref);
13 |
14 | return (
15 |
23 | {isInView ? (
24 | children
25 | ) : (
26 |
27 | )}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/shadcnui-popover.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { toast } from "@/lib/toast";
4 | import { useState } from "react";
5 | import { ExamplePreview } from "../example-preview";
6 | import { Button } from "./ui/button";
7 | import {
8 | EmojiPicker,
9 | EmojiPickerContent,
10 | EmojiPickerFooter,
11 | EmojiPickerSearch,
12 | } from "./ui/emoji-picker";
13 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
14 |
15 | export function ShadcnUiPopoverPreview() {
16 | const [isOpen, setIsOpen] = useState(false);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | {
28 | setIsOpen(false);
29 | toast(emoji);
30 | }}
31 | >
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/shadcnui-popover.tsx:
--------------------------------------------------------------------------------
1 | import { buttonVariants } from "@/components/ui/button";
2 | import { CodeBlock } from "@/components/ui/code-block";
3 | import { cn } from "@/lib/utils";
4 | import type { ComponentProps } from "react";
5 | import { ShadcnUiPopoverPreview } from "./shadcnui-popover.client";
6 |
7 | export function ShadcnUiPopover({
8 | className,
9 | ...props
10 | }: Omit, "children">) {
11 | return (
12 |
16 |
45 | {`
46 | "use client";
47 |
48 | import * as React from "react";
49 |
50 | import { Button } from "@/components/ui/button";
51 | import {
52 | EmojiPicker,
53 | EmojiPickerSearch,
54 | EmojiPickerContent,
55 | EmojiPickerFooter,
56 | } from "@/components/ui/emoji-picker";
57 | import {
58 | Popover,
59 | PopoverContent,
60 | PopoverTrigger,
61 | } from "@/components/ui/popover";
62 |
63 | export default function Page() {
64 | const [isOpen, setIsOpen] = React.useState(false);
65 |
66 | return (
67 |
68 |
69 |
70 |
71 |
72 |
73 | {
76 | setIsOpen(false);
77 | console.log(emoji);
78 | }}
79 | >
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | }
89 | `}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/shadcnui.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { toast } from "@/lib/toast";
4 | import { ExamplePreview } from "../example-preview";
5 | import {
6 | EmojiPicker,
7 | EmojiPickerContent,
8 | EmojiPickerSearch,
9 | } from "./ui/emoji-picker";
10 |
11 | export function ShadcnUiPreview() {
12 | return (
13 |
14 | {
17 | toast(emoji);
18 | }}
19 | >
20 |
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/shadcnui.tsx:
--------------------------------------------------------------------------------
1 | import { buttonVariants } from "@/components/ui/button";
2 | import { CodeBlock } from "@/components/ui/code-block";
3 | import { cn } from "@/lib/utils";
4 | import type { ComponentProps } from "react";
5 | import { ShadcnUiPreview } from "./shadcnui.client";
6 |
7 | export function ShadcnUi({
8 | className,
9 | ...props
10 | }: Omit, "children">) {
11 | return (
12 |
16 |
45 | {`
46 | "use client";
47 |
48 | import * as React from "react";
49 |
50 | import {
51 | EmojiPicker,
52 | EmojiPickerSearch,
53 | EmojiPickerContent,
54 | } from "@/components/ui/emoji-picker";
55 |
56 | export default function Page() {
57 | return (
58 |
59 | {
62 | console.log(emoji);
63 | }}
64 | >
65 |
66 |
67 |
68 |
69 | );
70 | }
71 | `}
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { type VariantProps, cva } from "class-variance-authority";
3 | import type * as React from "react";
4 |
5 | import { cn } from "@/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
16 | outline:
17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27 | icon: "size-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | },
35 | );
36 |
37 | function Button({
38 | className,
39 | variant,
40 | size,
41 | asChild = false,
42 | ...props
43 | }: React.ComponentProps<"button"> &
44 | VariantProps & {
45 | asChild?: boolean;
46 | }) {
47 | const Comp = asChild ? Slot : "button";
48 |
49 | return (
50 |
55 | );
56 | }
57 |
58 | export { Button, buttonVariants };
59 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/ui/emoji-picker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | type EmojiPickerListCategoryHeaderProps,
5 | type EmojiPickerListEmojiProps,
6 | type EmojiPickerListRowProps,
7 | EmojiPicker as EmojiPickerPrimitive,
8 | } from "frimousse";
9 | import { LoaderIcon, SearchIcon } from "lucide-react";
10 | import type * as React from "react";
11 |
12 | import { cn } from "@/lib/utils";
13 |
14 | function EmojiPicker({
15 | className,
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
27 | );
28 | }
29 |
30 | function EmojiPickerSearch({
31 | className,
32 | ...props
33 | }: React.ComponentProps) {
34 | return (
35 |
39 |
40 |
45 |
46 | );
47 | }
48 |
49 | function EmojiPickerRow({ children, ...props }: EmojiPickerListRowProps) {
50 | return (
51 |
52 | {children}
53 |
54 | );
55 | }
56 |
57 | function EmojiPickerEmoji({
58 | emoji,
59 | className,
60 | ...props
61 | }: EmojiPickerListEmojiProps) {
62 | return (
63 |
73 | );
74 | }
75 |
76 | function EmojiPickerCategoryHeader({
77 | category,
78 | ...props
79 | }: EmojiPickerListCategoryHeaderProps) {
80 | return (
81 |
86 | {category.label}
87 |
88 | );
89 | }
90 |
91 | function EmojiPickerContent({
92 | className,
93 | ...props
94 | }: React.ComponentProps) {
95 | return (
96 |
101 |
105 |
106 |
107 |
111 | No emoji found.
112 |
113 |
122 |
123 | );
124 | }
125 |
126 | function EmojiPickerFooter({
127 | className,
128 | ...props
129 | }: React.ComponentProps<"div">) {
130 | return (
131 |
139 |
140 | {({ emoji }) =>
141 | emoji ? (
142 | <>
143 |
144 | {emoji.emoji}
145 |
146 |
147 | {emoji.label}
148 |
149 | >
150 | ) : (
151 |
152 | Select an emoji…
153 |
154 | )
155 | }
156 |
157 |
158 | );
159 | }
160 |
161 | export {
162 | EmojiPicker,
163 | EmojiPickerSearch,
164 | EmojiPickerContent,
165 | EmojiPickerFooter,
166 | };
167 |
--------------------------------------------------------------------------------
/site/src/examples/shadcnui/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as PopoverPrimitive from "@radix-ui/react-popover";
4 | import type * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return ;
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return ;
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | );
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return ;
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
49 |
--------------------------------------------------------------------------------
/site/src/examples/usage/usage.client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ExamplePreview } from "@/examples/example-preview";
4 | import { toast } from "@/lib/toast";
5 | import { cn } from "@/lib/utils";
6 | import {
7 | EmojiPicker as EmojiPickerPrimitive,
8 | type EmojiPickerRootProps,
9 | } from "frimousse";
10 |
11 | function EmojiPicker({ className, columns, ...props }: EmojiPickerRootProps) {
12 | return (
13 |
21 |
22 |
23 |
24 | Loading…
25 |
26 |
27 | No emoji found.
28 |
29 | (
33 |
39 | ),
40 | Row: ({ children, ...props }) => (
41 |
42 | {children}
43 |
44 | ),
45 | CategoryHeader: ({ category, ...props }) => (
46 |
50 | {category.label}
51 |
52 | ),
53 | }}
54 | />
55 |
56 |
57 | );
58 | }
59 |
60 | export function UsagePreview() {
61 | return (
62 |
63 | {
65 | toast(emoji);
66 | }}
67 | />
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/site/src/examples/usage/usage.tsx:
--------------------------------------------------------------------------------
1 | import { CodeBlock } from "@/components/ui/code-block";
2 | import { Tabs } from "@/components/ui/tabs";
3 | import { cn } from "@/lib/utils";
4 | import type { ComponentProps } from "react";
5 | import { UsagePreview } from "./usage.client";
6 |
7 | export function Usage({
8 | className,
9 | ...props
10 | }: Omit, "children">) {
11 | return (
12 |
16 |
17 |
18 |
19 | {`
28 | "use client";
29 |
30 | import { EmojiPicker } from "frimousse";
31 |
32 | export function MyEmojiPicker() {
33 | return (
34 |
35 |
36 |
37 |
38 | Loading…
39 |
40 |
41 | No emoji found.
42 |
43 | (
47 |
51 | {category.label}
52 |
53 | ),
54 | Row: ({ children, ...props }) => (
55 |
56 | {children}
57 |
58 | ),
59 | Emoji: ({ emoji, ...props }) => (
60 |
66 | ),
67 | }}
68 | />
69 |
70 |
71 | );
72 | }
73 | `}
74 | ),
75 | },
76 | {
77 | name: "css",
78 | label: "CSS",
79 | children: (
80 | {`
81 | [frimousse-root] {
82 | display: flex;
83 | flex-direction: column;
84 | width: fit-content;
85 | height: 352px;
86 | background: light-dark(#fff, #171717);
87 | isolation: isolate;
88 | }
89 |
90 | [frimousse-search] {
91 | position: relative;
92 | z-index: 10;
93 | appearance: none;
94 | margin-block-start: 8px;
95 | margin-inline: 8px;
96 | padding: 8px 10px;
97 | background: light-dark(#f5f5f5, #262626);
98 | border-radius: 6px;
99 | font-size: 14px;
100 | }
101 |
102 | [frimousse-viewport] {
103 | position: relative;
104 | flex: 1;
105 | outline: none;
106 | }
107 |
108 | [frimousse-loading]
109 | [frimousse-empty], {
110 | position: absolute;
111 | inset: 0;
112 | display: flex;
113 | align-items: center;
114 | justify-content: center;
115 | color: light-dark(#a1a1a1, #737373);
116 | font-size: 14px;
117 | }
118 |
119 | [frimousse-list] {
120 | padding-block-end: 12px;
121 | user-select: none;
122 | }
123 |
124 | [frimousse-category-header] {
125 | padding: 12px 12px 6px;
126 | background: light-dark(#fff, #171717);
127 | color: light-dark(#525252, #a1a1a1);
128 | font-size: 12px;
129 | font-weight: 500;
130 | }
131 |
132 | [frimousse-row] {
133 | padding-inline: 12px;
134 | scroll-margin-block: 12px;
135 | }
136 |
137 | [frimousse-emoji] {
138 | display: flex;
139 | align-items: center;
140 | justify-content: center;
141 | width: 32px;
142 | height: 32px;
143 | border-radius: 6px;
144 | background: transparent;
145 | font-size: 18px;
146 |
147 | &[data-active] {
148 | background: light-dark(#f5f5f5, #262626);
149 | }
150 | }
151 | `}
152 | ),
153 | },
154 | ]}
155 | />
156 |
157 | );
158 | }
159 |
--------------------------------------------------------------------------------
/site/src/hooks/use-initial-render.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useIsInitialRender() {
4 | const [isInitialRender, setIsInitialRender] = useState(true);
5 |
6 | useEffect(() => {
7 | setIsInitialRender(false);
8 | }, []);
9 |
10 | return isInitialRender;
11 | }
12 |
--------------------------------------------------------------------------------
/site/src/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export function useIsMobile() {
4 | const [isMobile, setIsMobile] = useState(undefined);
5 |
6 | useEffect(() => {
7 | const mediaQuery = window.matchMedia("(min-width: 40rem)");
8 | setIsMobile(!mediaQuery.matches);
9 |
10 | const handleChange = (event: MediaQueryListEvent) => {
11 | setIsMobile(!event.matches);
12 | };
13 |
14 | mediaQuery.addEventListener("change", handleChange);
15 |
16 | return () => {
17 | mediaQuery.removeEventListener("change", handleChange);
18 | };
19 | }, []);
20 |
21 | return Boolean(isMobile);
22 | }
23 |
--------------------------------------------------------------------------------
/site/src/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStore } from "react";
2 |
3 | const subscribe = () => () => {};
4 | const getSnapshot = () => true;
5 | const getServerSnapshot = () => false;
6 |
7 | export function useIsMounted() {
8 | const isMounted = useSyncExternalStore(
9 | subscribe,
10 | getSnapshot,
11 | getServerSnapshot,
12 | );
13 |
14 | return isMounted;
15 | }
16 |
--------------------------------------------------------------------------------
/site/src/hooks/use-sticky.ts:
--------------------------------------------------------------------------------
1 | import { type RefObject, useEffect, useState } from "react";
2 |
3 | export function useIsSticky(ref: RefObject) {
4 | const [isSticky, setIsSticky] = useState(false);
5 |
6 | // biome-ignore lint/correctness/useExhaustiveDependencies: The passed ref is expected to be stable
7 | useEffect(() => {
8 | const current = ref.current;
9 |
10 | const observer = new IntersectionObserver(
11 | ([entry]) => setIsSticky((entry?.intersectionRatio ?? 1) < 1),
12 | {
13 | threshold: [1],
14 | },
15 | );
16 |
17 | observer.observe(current as T);
18 |
19 | return () => {
20 | observer.unobserve(current as T);
21 | };
22 | }, []);
23 |
24 | return isSticky;
25 | }
26 |
--------------------------------------------------------------------------------
/site/src/lib/get-fast-bounding-rects.ts:
--------------------------------------------------------------------------------
1 | export function getFastBoundingRects(
2 | elements: Array,
3 | ): Promise