├── .changeset ├── README.md ├── config.json └── early-ducks-hang.md ├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── releases │ └── yarn-4.5.1.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ ├── api.js │ │ └── unsupported-api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── bin │ │ └── prettier.cjs │ ├── index.cjs │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── LICENSE ├── README-ko_kr.md ├── README.md ├── codecov.yml ├── docs ├── .gitignore ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── public │ ├── favicon.ico │ ├── logo-black.png │ ├── logo-white.png │ └── og.png ├── src │ ├── components │ │ ├── index.ts │ │ ├── main.tsx │ │ └── sandpack │ │ │ ├── base-template.ts │ │ │ ├── custom-preset.tsx │ │ │ └── index.tsx │ ├── middleware.ts │ └── pages │ │ ├── _app.tsx │ │ ├── en │ │ ├── _meta.tsx │ │ ├── api │ │ │ ├── _meta.tsx │ │ │ ├── components │ │ │ │ ├── _meta.tsx │ │ │ │ └── overlay-provider.mdx │ │ │ └── utils │ │ │ │ ├── _meta.tsx │ │ │ │ ├── overlay-close-all.mdx │ │ │ │ ├── overlay-close.mdx │ │ │ │ ├── overlay-open-async.mdx │ │ │ │ ├── overlay-open.mdx │ │ │ │ ├── overlay-unmount-all.mdx │ │ │ │ └── overlay-unmount.mdx │ │ ├── docs │ │ │ ├── _meta.tsx │ │ │ ├── guides │ │ │ │ ├── _meta.tsx │ │ │ │ ├── code-comparison.mdx │ │ │ │ ├── faq.mdx │ │ │ │ ├── hooks.mdx │ │ │ │ ├── introduction.mdx │ │ │ │ ├── testing.mdx │ │ │ │ └── think-in-overlay-kit.mdx │ │ │ └── more │ │ │ │ ├── _meta.tsx │ │ │ │ ├── basic.mdx │ │ │ │ └── open-outside-react.mdx │ │ └── index.mdx │ │ └── ko │ │ ├── _meta.tsx │ │ ├── api │ │ ├── _meta.tsx │ │ ├── components │ │ │ ├── _meta.tsx │ │ │ └── overlay-provider.mdx │ │ └── utils │ │ │ ├── _meta.tsx │ │ │ ├── overlay-close-all.mdx │ │ │ ├── overlay-close.mdx │ │ │ ├── overlay-open-async.mdx │ │ │ ├── overlay-open.mdx │ │ │ ├── overlay-unmount-all.mdx │ │ │ └── overlay-unmount.mdx │ │ ├── docs │ │ ├── _meta.tsx │ │ ├── guides │ │ │ ├── _meta.tsx │ │ │ ├── code-comparison.mdx │ │ │ ├── faq.mdx │ │ │ ├── hooks.mdx │ │ │ ├── introduction.mdx │ │ │ ├── testing.mdx │ │ │ └── think-in-overlay-kit.mdx │ │ └── more │ │ │ ├── _meta.tsx │ │ │ ├── basic.mdx │ │ │ └── open-outside-react.mdx │ │ └── index.mdx ├── theme.config.tsx └── tsconfig.json ├── eslint.config.cjs ├── examples ├── react-16 │ └── framer-motion │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ ├── components │ │ │ └── modal.tsx │ │ ├── demo.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts ├── react-17 │ └── framer-motion │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ ├── components │ │ │ └── modal.tsx │ │ ├── demo.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts ├── react-18 │ └── framer-motion │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src │ │ ├── components │ │ │ └── modal.tsx │ │ ├── demo.tsx │ │ ├── main.tsx │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts └── react-19 │ └── framer-motion │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src │ ├── components │ │ └── modal.tsx │ ├── demo.tsx │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── package.json ├── packages ├── .gitignore ├── CHANGELOG.md ├── README.md ├── package.json ├── setup.test.ts ├── src │ ├── context │ │ ├── context.ts │ │ ├── provider │ │ │ ├── content-overlay-controller.tsx │ │ │ └── index.tsx │ │ └── reducer.ts │ ├── event.test.tsx │ ├── event.ts │ ├── index.ts │ └── utils │ │ ├── create-overlay-context.tsx │ │ ├── create-safe-context.ts │ │ ├── create-use-external-events.test.ts │ │ ├── create-use-external-events.ts │ │ ├── emitter.ts │ │ ├── index.ts │ │ └── random-id.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.mts └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "toss/overlay-kit" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "@overlay-kit/framer-motion-react-16", 12 | "@overlay-kit/framer-motion-react-17", 13 | "@overlay-kit/framer-motion-react-18", 14 | "@overlay-kit/framer-motion-react-19" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.changeset/early-ducks-hang.md: -------------------------------------------------------------------------------- 1 | --- 2 | "overlay-kit": patch 3 | --- 4 | 5 | fix: remove unnecessary type assertion from Context.Provider 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jungpaeng 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 9 | 10 | **Related Issue:** Fixes # (issue_number) 11 | 12 | ## Changes 13 | 18 | 19 | ## Motivation and Context 20 | 23 | 24 | ## How Has This Been Tested? 25 | 30 | 31 | ## Screenshots (if appropriate): 32 | 33 | 34 | ## Types of changes 35 | 38 | - [ ] Bug fix (non-breaking change which fixes an issue) 39 | - [ ] New feature (non-breaking change which adds functionality) 40 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 41 | - [ ] Documentation update 42 | 43 | ## Checklist 44 | 47 | - [ ] I have performed a self-review of my own code. 48 | - [ ] My code is commented, particularly in hard-to-understand areas. 49 | - [ ] I have made corresponding changes to the documentation. 50 | - [ ] My changes generate no new warnings. 51 | - [ ] I have added tests that prove my fix is effective or that my feature works. 52 | - [ ] New and existing unit tests pass locally with my changes. 53 | - [ ] Any dependent changes have been merged and published in downstream modules. 54 | 55 | ## Further Comments 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, beta] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | command: 18 | [ 19 | 'yarn workspace overlay-kit run test', 20 | 'yarn workspace overlay-kit run test:attw', 21 | 'yarn workspace overlay-kit run test:publint', 22 | 'yarn lint', 23 | 'yarn workspace @overlay-kit/framer-motion-react-16 run build', 24 | 'yarn workspace @overlay-kit/framer-motion-react-17 run build', 25 | 'yarn workspace @overlay-kit/framer-motion-react-18 run build', 26 | 'yarn workspace @overlay-kit/framer-motion-react-19 run build', 27 | ] 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Install Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version-file: '.nvmrc' 35 | cache: 'yarn' 36 | 37 | - name: Install Dependencies 38 | run: yarn install 39 | 40 | - name: Build Package 41 | run: yarn workspace overlay-kit run build 42 | 43 | - name: Run command 44 | run: ${{ matrix.command }} 45 | - if: matrix.command == 'yarn workspace overlay-kit run test' 46 | uses: codecov/codecov-action@v4 47 | env: 48 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Changesets PR or Publish 2 | 3 | on: 4 | push: 5 | branches: [main, beta] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Install Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version-file: '.nvmrc' 17 | cache: 'yarn' 18 | 19 | - name: Install Dependencies 20 | run: yarn install 21 | 22 | - name: Build Package 23 | run: yarn workspace overlay-kit run build 24 | 25 | - name: Create Changesets Pull Request or Publish to NPM 26 | uses: changesets/action@v1 27 | with: 28 | title: 'chore: version packages' 29 | commit: 'chore: version packages' 30 | version: yarn changeset:version 31 | publish: yarn changeset:publish 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pnp.* 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/plugins 5 | !.yarn/releases 6 | !.yarn/sdks 7 | !.yarn/versions 8 | 9 | node_modules 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.18.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "eslint.nodePath": ".yarn/sdks", 7 | "eslint.experimental.useFlatConfig": true, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | }, 11 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", 12 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 13 | "typescript.enablePromptUseWorkspaceTsdk": true, 14 | "typescript.tsserver.experimental.useVsCodeWatcher": false 15 | } 16 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/bin/eslint.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/bin/eslint.js your application uses 20 | module.exports = absRequire(`eslint/bin/eslint.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint your application uses 20 | module.exports = absRequire(`eslint`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/unsupported-api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require eslint/use-at-your-own-risk 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real eslint/use-at-your-own-risk your application uses 20 | module.exports = absRequire(`eslint/use-at-your-own-risk`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.57.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "eslint": "./bin/eslint.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": "./lib/api.js", 12 | "./use-at-your-own-risk": "./lib/unsupported-api.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/bin/prettier.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier/bin/prettier.cjs 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier/bin/prettier.cjs your application uses 20 | module.exports = absRequire(`prettier/bin/prettier.cjs`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require prettier 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real prettier your application uses 20 | module.exports = absRequire(`prettier`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "3.2.5-sdk", 4 | "main": "./index.cjs", 5 | "type": "commonjs", 6 | "bin": "./bin/prettier.cjs" 7 | } 8 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsc 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsc your application uses 20 | module.exports = absRequire(`typescript/bin/tsc`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/bin/tsserver 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/bin/tsserver your application uses 20 | module.exports = absRequire(`typescript/bin/tsserver`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsc.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript/lib/tsc.js your application uses 20 | module.exports = absRequire(`typescript/lib/tsc.js`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserver.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsserver.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | const moduleWrapper = tsserver => { 20 | if (!process.versions.pnp) { 21 | return tsserver; 22 | } 23 | 24 | const {isAbsolute} = require(`path`); 25 | const pnpApi = require(`pnpapi`); 26 | 27 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 28 | const isPortal = str => str.startsWith("portal:/"); 29 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 30 | 31 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 32 | return `${locator.name}@${locator.reference}`; 33 | })); 34 | 35 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 36 | // doesn't understand. This layer makes sure to remove the protocol 37 | // before forwarding it to TS, and to add it back on all returned paths. 38 | 39 | function toEditorPath(str) { 40 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 41 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 42 | // We also take the opportunity to turn virtual paths into physical ones; 43 | // this makes it much easier to work with workspaces that list peer 44 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 45 | // file instances instead of the real ones. 46 | // 47 | // We only do this to modules owned by the the dependency tree roots. 48 | // This avoids breaking the resolution when jumping inside a vendor 49 | // with peer dep (otherwise jumping into react-dom would show resolution 50 | // errors on react). 51 | // 52 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 53 | if (resolved) { 54 | const locator = pnpApi.findPackageLocator(resolved); 55 | if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { 56 | str = resolved; 57 | } 58 | } 59 | 60 | str = normalize(str); 61 | 62 | if (str.match(/\.zip\//)) { 63 | switch (hostInfo) { 64 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 65 | // VSCode only adds it automatically for supported schemes, 66 | // so we have to do it manually for the `zip` scheme. 67 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 68 | // 69 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 70 | // 71 | // 2021-10-08: VSCode changed the format in 1.61. 72 | // Before | ^zip:/c:/foo/bar.zip/package.json 73 | // After | ^/zip//c:/foo/bar.zip/package.json 74 | // 75 | // 2022-04-06: VSCode changed the format in 1.66. 76 | // Before | ^/zip//c:/foo/bar.zip/package.json 77 | // After | ^/zip/c:/foo/bar.zip/package.json 78 | // 79 | // 2022-05-06: VSCode changed the format in 1.68 80 | // Before | ^/zip/c:/foo/bar.zip/package.json 81 | // After | ^/zip//c:/foo/bar.zip/package.json 82 | // 83 | case `vscode <1.61`: { 84 | str = `^zip:${str}`; 85 | } break; 86 | 87 | case `vscode <1.66`: { 88 | str = `^/zip/${str}`; 89 | } break; 90 | 91 | case `vscode <1.68`: { 92 | str = `^/zip${str}`; 93 | } break; 94 | 95 | case `vscode`: { 96 | str = `^/zip/${str}`; 97 | } break; 98 | 99 | // To make "go to definition" work, 100 | // We have to resolve the actual file system path from virtual path 101 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 102 | case `coc-nvim`: { 103 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 104 | str = resolve(`zipfile:${str}`); 105 | } break; 106 | 107 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 108 | // We have to resolve the actual file system path from virtual path, 109 | // everything else is up to neovim 110 | case `neovim`: { 111 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 112 | str = `zipfile://${str}`; 113 | } break; 114 | 115 | default: { 116 | str = `zip:${str}`; 117 | } break; 118 | } 119 | } else { 120 | str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); 121 | } 122 | } 123 | 124 | return str; 125 | } 126 | 127 | function fromEditorPath(str) { 128 | switch (hostInfo) { 129 | case `coc-nvim`: { 130 | str = str.replace(/\.zip::/, `.zip/`); 131 | // The path for coc-nvim is in format of //zipfile://.yarn/... 132 | // So in order to convert it back, we use .* to match all the thing 133 | // before `zipfile:` 134 | return process.platform === `win32` 135 | ? str.replace(/^.*zipfile:\//, ``) 136 | : str.replace(/^.*zipfile:/, ``); 137 | } break; 138 | 139 | case `neovim`: { 140 | str = str.replace(/\.zip::/, `.zip/`); 141 | // The path for neovim is in format of zipfile:////.yarn/... 142 | return str.replace(/^zipfile:\/\//, ``); 143 | } break; 144 | 145 | case `vscode`: 146 | default: { 147 | return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) 148 | } break; 149 | } 150 | } 151 | 152 | // Force enable 'allowLocalPluginLoads' 153 | // TypeScript tries to resolve plugins using a path relative to itself 154 | // which doesn't work when using the global cache 155 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 156 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 157 | // TypeScript already does local loads and if this code is running the user trusts the workspace 158 | // https://github.com/microsoft/vscode/issues/45856 159 | const ConfiguredProject = tsserver.server.ConfiguredProject; 160 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 161 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 162 | this.projectService.allowLocalPluginLoads = true; 163 | return originalEnablePluginsWithOptions.apply(this, arguments); 164 | }; 165 | 166 | // And here is the point where we hijack the VSCode <-> TS communications 167 | // by adding ourselves in the middle. We locate everything that looks 168 | // like an absolute path of ours and normalize it. 169 | 170 | const Session = tsserver.server.Session; 171 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 172 | let hostInfo = `unknown`; 173 | 174 | Object.assign(Session.prototype, { 175 | onMessage(/** @type {string | object} */ message) { 176 | const isStringMessage = typeof message === 'string'; 177 | const parsedMessage = isStringMessage ? JSON.parse(message) : message; 178 | 179 | if ( 180 | parsedMessage != null && 181 | typeof parsedMessage === `object` && 182 | parsedMessage.arguments && 183 | typeof parsedMessage.arguments.hostInfo === `string` 184 | ) { 185 | hostInfo = parsedMessage.arguments.hostInfo; 186 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { 187 | const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( 188 | // The RegExp from https://semver.org/ but without the caret at the start 189 | /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 190 | ) ?? []).map(Number) 191 | 192 | if (major === 1) { 193 | if (minor < 61) { 194 | hostInfo += ` <1.61`; 195 | } else if (minor < 66) { 196 | hostInfo += ` <1.66`; 197 | } else if (minor < 68) { 198 | hostInfo += ` <1.68`; 199 | } 200 | } 201 | } 202 | } 203 | 204 | const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { 205 | return typeof value === 'string' ? fromEditorPath(value) : value; 206 | }); 207 | 208 | return originalOnMessage.call( 209 | this, 210 | isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) 211 | ); 212 | }, 213 | 214 | send(/** @type {any} */ msg) { 215 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 216 | return typeof value === `string` ? toEditorPath(value) : value; 217 | }))); 218 | } 219 | }); 220 | 221 | return tsserver; 222 | }; 223 | 224 | const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); 225 | // In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. 226 | // Ref https://github.com/microsoft/TypeScript/pull/55326 227 | if (major > 5 || (major === 5 && minor >= 5)) { 228 | moduleWrapper(absRequire(`typescript`)); 229 | } 230 | 231 | // Defer to the real typescript/lib/tsserver.js your application uses 232 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`)); 233 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsserverlibrary.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript/lib/tsserverlibrary.js 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | const moduleWrapper = tsserver => { 20 | if (!process.versions.pnp) { 21 | return tsserver; 22 | } 23 | 24 | const {isAbsolute} = require(`path`); 25 | const pnpApi = require(`pnpapi`); 26 | 27 | const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//); 28 | const isPortal = str => str.startsWith("portal:/"); 29 | const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`); 30 | 31 | const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => { 32 | return `${locator.name}@${locator.reference}`; 33 | })); 34 | 35 | // VSCode sends the zip paths to TS using the "zip://" prefix, that TS 36 | // doesn't understand. This layer makes sure to remove the protocol 37 | // before forwarding it to TS, and to add it back on all returned paths. 38 | 39 | function toEditorPath(str) { 40 | // We add the `zip:` prefix to both `.zip/` paths and virtual paths 41 | if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) { 42 | // We also take the opportunity to turn virtual paths into physical ones; 43 | // this makes it much easier to work with workspaces that list peer 44 | // dependencies, since otherwise Ctrl+Click would bring us to the virtual 45 | // file instances instead of the real ones. 46 | // 47 | // We only do this to modules owned by the the dependency tree roots. 48 | // This avoids breaking the resolution when jumping inside a vendor 49 | // with peer dep (otherwise jumping into react-dom would show resolution 50 | // errors on react). 51 | // 52 | const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str; 53 | if (resolved) { 54 | const locator = pnpApi.findPackageLocator(resolved); 55 | if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) { 56 | str = resolved; 57 | } 58 | } 59 | 60 | str = normalize(str); 61 | 62 | if (str.match(/\.zip\//)) { 63 | switch (hostInfo) { 64 | // Absolute VSCode `Uri.fsPath`s need to start with a slash. 65 | // VSCode only adds it automatically for supported schemes, 66 | // so we have to do it manually for the `zip` scheme. 67 | // The path needs to start with a caret otherwise VSCode doesn't handle the protocol 68 | // 69 | // Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910 70 | // 71 | // 2021-10-08: VSCode changed the format in 1.61. 72 | // Before | ^zip:/c:/foo/bar.zip/package.json 73 | // After | ^/zip//c:/foo/bar.zip/package.json 74 | // 75 | // 2022-04-06: VSCode changed the format in 1.66. 76 | // Before | ^/zip//c:/foo/bar.zip/package.json 77 | // After | ^/zip/c:/foo/bar.zip/package.json 78 | // 79 | // 2022-05-06: VSCode changed the format in 1.68 80 | // Before | ^/zip/c:/foo/bar.zip/package.json 81 | // After | ^/zip//c:/foo/bar.zip/package.json 82 | // 83 | case `vscode <1.61`: { 84 | str = `^zip:${str}`; 85 | } break; 86 | 87 | case `vscode <1.66`: { 88 | str = `^/zip/${str}`; 89 | } break; 90 | 91 | case `vscode <1.68`: { 92 | str = `^/zip${str}`; 93 | } break; 94 | 95 | case `vscode`: { 96 | str = `^/zip/${str}`; 97 | } break; 98 | 99 | // To make "go to definition" work, 100 | // We have to resolve the actual file system path from virtual path 101 | // and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip) 102 | case `coc-nvim`: { 103 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 104 | str = resolve(`zipfile:${str}`); 105 | } break; 106 | 107 | // Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server) 108 | // We have to resolve the actual file system path from virtual path, 109 | // everything else is up to neovim 110 | case `neovim`: { 111 | str = normalize(resolved).replace(/\.zip\//, `.zip::`); 112 | str = `zipfile://${str}`; 113 | } break; 114 | 115 | default: { 116 | str = `zip:${str}`; 117 | } break; 118 | } 119 | } else { 120 | str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); 121 | } 122 | } 123 | 124 | return str; 125 | } 126 | 127 | function fromEditorPath(str) { 128 | switch (hostInfo) { 129 | case `coc-nvim`: { 130 | str = str.replace(/\.zip::/, `.zip/`); 131 | // The path for coc-nvim is in format of //zipfile://.yarn/... 132 | // So in order to convert it back, we use .* to match all the thing 133 | // before `zipfile:` 134 | return process.platform === `win32` 135 | ? str.replace(/^.*zipfile:\//, ``) 136 | : str.replace(/^.*zipfile:/, ``); 137 | } break; 138 | 139 | case `neovim`: { 140 | str = str.replace(/\.zip::/, `.zip/`); 141 | // The path for neovim is in format of zipfile:////.yarn/... 142 | return str.replace(/^zipfile:\/\//, ``); 143 | } break; 144 | 145 | case `vscode`: 146 | default: { 147 | return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`) 148 | } break; 149 | } 150 | } 151 | 152 | // Force enable 'allowLocalPluginLoads' 153 | // TypeScript tries to resolve plugins using a path relative to itself 154 | // which doesn't work when using the global cache 155 | // https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238 156 | // VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but 157 | // TypeScript already does local loads and if this code is running the user trusts the workspace 158 | // https://github.com/microsoft/vscode/issues/45856 159 | const ConfiguredProject = tsserver.server.ConfiguredProject; 160 | const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype; 161 | ConfiguredProject.prototype.enablePluginsWithOptions = function() { 162 | this.projectService.allowLocalPluginLoads = true; 163 | return originalEnablePluginsWithOptions.apply(this, arguments); 164 | }; 165 | 166 | // And here is the point where we hijack the VSCode <-> TS communications 167 | // by adding ourselves in the middle. We locate everything that looks 168 | // like an absolute path of ours and normalize it. 169 | 170 | const Session = tsserver.server.Session; 171 | const {onMessage: originalOnMessage, send: originalSend} = Session.prototype; 172 | let hostInfo = `unknown`; 173 | 174 | Object.assign(Session.prototype, { 175 | onMessage(/** @type {string | object} */ message) { 176 | const isStringMessage = typeof message === 'string'; 177 | const parsedMessage = isStringMessage ? JSON.parse(message) : message; 178 | 179 | if ( 180 | parsedMessage != null && 181 | typeof parsedMessage === `object` && 182 | parsedMessage.arguments && 183 | typeof parsedMessage.arguments.hostInfo === `string` 184 | ) { 185 | hostInfo = parsedMessage.arguments.hostInfo; 186 | if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) { 187 | const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match( 188 | // The RegExp from https://semver.org/ but without the caret at the start 189 | /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ 190 | ) ?? []).map(Number) 191 | 192 | if (major === 1) { 193 | if (minor < 61) { 194 | hostInfo += ` <1.61`; 195 | } else if (minor < 66) { 196 | hostInfo += ` <1.66`; 197 | } else if (minor < 68) { 198 | hostInfo += ` <1.68`; 199 | } 200 | } 201 | } 202 | } 203 | 204 | const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => { 205 | return typeof value === 'string' ? fromEditorPath(value) : value; 206 | }); 207 | 208 | return originalOnMessage.call( 209 | this, 210 | isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON) 211 | ); 212 | }, 213 | 214 | send(/** @type {any} */ msg) { 215 | return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => { 216 | return typeof value === `string` ? toEditorPath(value) : value; 217 | }))); 218 | } 219 | }); 220 | 221 | return tsserver; 222 | }; 223 | 224 | const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10)); 225 | // In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well. 226 | // Ref https://github.com/microsoft/TypeScript/pull/55326 227 | if (major > 5 || (major === 5 && minor >= 5)) { 228 | moduleWrapper(absRequire(`typescript`)); 229 | } 230 | 231 | // Defer to the real typescript/lib/tsserverlibrary.js your application uses 232 | module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`)); 233 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire} = require(`module`); 5 | const {resolve} = require(`path`); 6 | 7 | const relPnpApiPath = "../../../../.pnp.cjs"; 8 | 9 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 10 | const absRequire = createRequire(absPnpApiPath); 11 | 12 | if (existsSync(absPnpApiPath)) { 13 | if (!process.versions.pnp) { 14 | // Setup the environment to be able to require typescript 15 | require(absPnpApiPath).setup(); 16 | } 17 | } 18 | 19 | // Defer to the real typescript your application uses 20 | module.exports = absRequire(`typescript`); 21 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "5.4.5-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "tsc": "./bin/tsc", 8 | "tsserver": "./bin/tsserver" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.5.1.cjs 2 | 3 | packageExtensions: 4 | # Nextra dependencies 5 | "acorn-jsx@^5.0.0": 6 | dependencies: 7 | acorn: ^8.0.0 8 | "langium@3.0.0": 9 | dependencies: 10 | "@chevrotain/regexp-to-ast": 11.0.3 11 | vscode-languageserver-types: 3.17.5 12 | vscode-jsonrpc: 8.2.0 13 | # stylex 14 | "@stylexswc/nextjs-plugin@^0.5.0": 15 | dependencies: 16 | "@stylexswc/webpack-plugin": ^0.5.0 17 | "@stylexswc/webpack-plugin@^0.5.0": 18 | dependencies: 19 | "@stylexswc/rs-compiler": ^0.5.0 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Viva Republica, Inc 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-ko_kr.md: -------------------------------------------------------------------------------- 1 | ![](./docs/public/og.png) 2 | 3 | # overlay-kit · [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/overlay-kit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/overlay-kit/graph/badge.svg?token=JBEAQTL7XK)](https://codecov.io/gh/toss/overlay-kit) 4 | 5 | [English](https://github.com/toss/overlay-kit/blob/main/README.md) | 한국어 6 | 7 | overlay-kit은 React에서 모달, 팝업, 다이얼로그 같은 오버레이를 선언적으로 관리하기 위한 라이브러리예요. 8 | 9 | 복잡한 상태 관리나 불필요한 이벤트 핸들링 없이, 효율적으로 오버레이를 구현할 수 있어요. 10 | 11 | ```sh 12 | npm install overlay-kit 13 | ``` 14 | 15 | ## 예제 16 | 17 | ### 간단한 오버레이 열기 18 | 19 | `overlay.open`을 사용하면 오버레이를 간단하게 열고 닫을 수 있어요. 20 | 21 | ```tsx 22 | import { overlay } from 'overlay-kit'; 23 | 24 | 33 | ``` 34 | 35 | ### 비동기 오버레이 열기 36 | 37 | `overlay.openAsync`를 사용하면 오버레이의 결과를 `Promise`로 처리할 수 있어요. 38 | 39 | ```tsx 40 | import { overlay } from 'overlay-kit'; 41 | 42 | 56 | ``` 57 | 58 | ## overlay-kit을 사용하는 이유 59 | 60 | ### 기존 오버레이 관리의 문제점 61 | 62 | 1. 상태 관리의 복잡성 63 | - useState나 전역 상태를 사용해 직접 오버레이 상태를 관리해야 했어요. 64 | - 상태 관리와 UI 로직이 섞여 코드가 복잡해지고 가독성이 떨어졌어요. 65 | 2. 이벤트 핸들링의 반복 66 | - 열기, 닫기, 결과 반환 같은 이벤트 핸들링 코드를 반복해서 작성해야 했어요. 67 | - 이는 중복 코드를 유발하고 개발 경험을 저하시키는 주요 원인이 되었어요. 68 | 3. 재사용성 부족 69 | - 오버레이에서 값을 반환하려면 callback 함수 등으로 UI와 로직이 강하게 결합되었어요. 70 | - 이로 인해 컴포넌트를 재사용하기 어려웠어요. 71 | 72 | ### overlay-kit의 목표 73 | 74 | 1. React 철학을 따르는 설계 75 | - React는 선언적인 코드를 지향해요. 76 | - overlay-kit은 오버레이를 선언적으로 관리할 수 있게 도와줘요. 77 | 2. 개발 생산성 향상 78 | - 상태 관리와 이벤트 핸들링을 캡슐화해, 개발자는 UI와 비즈니스 로직에만 집중할 수 있어요. 79 | 3. 확장성과 재사용성 강화 80 | - UI와 동작을 분리하고, Promise를 반환하는 방식으로 오버레이의 재사용성을 높였어요. 81 | 82 | ## License 83 | 84 | MIT © Viva Republica, Inc. 자세한 내용은 [LICENSE](https://github.com/toss/overlay-kit/blob/main/LICENSE)를 참고하세요. 85 | 86 | 87 | 88 | 89 | Toss 90 | 91 | 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./docs/public/og.png) 2 | 3 | # overlay-kit · [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/overlay-kit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/overlay-kit/graph/badge.svg?token=JBEAQTL7XK)](https://codecov.io/gh/toss/overlay-kit) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/toss/overlay-kit) 4 | 5 | English | [한국어](https://github.com/toss/overlay-kit/blob/main/README-ko_kr.md) 6 | 7 | overlay-kit is a library for declaratively managing overlays like modals, popups, and dialogs in React. 8 | 9 | You can efficiently implement overlays without complex state management or unnecessary event handling. 10 | 11 | ```sh 12 | npm install overlay-kit 13 | ``` 14 | 15 | ## Example 16 | 17 | ### Opening Simple Overlays 18 | 19 | You can easily open and close overlays using `overlay.open`. 20 | 21 | ```tsx 22 | import { overlay } from 'overlay-kit'; 23 | 24 | 33 | ``` 34 | 35 | ### Opening Asynchronous Overlays 36 | 37 | You can handle overlay results as a `Promise` using `overlay.openAsync`. 38 | 39 | ```tsx 40 | import { overlay } from 'overlay-kit'; 41 | 42 | 56 | ``` 57 | 58 | ## Why use overlay-kit? 59 | 60 | ### Problems with Traditional Overlay Management** 61 | 62 | 1. Complexity of State Management 63 | - Had to manage overlay state directly using useState or global state. 64 | - Code became complex and less readable as state management mixed with UI logic. 65 | 2. Repetitive Event Handling 66 | - Had to repeatedly write event handling code for opening, closing, and returning results. 67 | - This led to code duplication and degraded development experience. 68 | 3. Lack of Reusability 69 | - UI and logic were tightly coupled through callback functions to return values from overlays. 70 | - This made it difficult to reuse components. 71 | 72 | ### Goals of overlay-kit 73 | 74 | 1. Design Following React Philosophy 75 | - React favors declarative code. 76 | - overlay-kit helps manage overlays declaratively. 77 | 2. Improve Development Productivity 78 | - By encapsulating state management and event handling, developers can focus solely on UI and business logic. 79 | 3. Enhance Extensibility and Reusability 80 | - Increased overlay reusability by separating UI and behavior, and returning Promises. 81 | 82 | 83 | ## License 84 | 85 | MIT © Viva Republica, Inc. See [LICENSE](https://github.com/toss/overlay-kit/blob/main/LICENSE) for details. 86 | 87 | 88 | 89 | 90 | Toss 91 | 92 | 93 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | target: 100% 7 | threshold: 30% 8 | 9 | comment: 10 | layout: "header, reach, diff, flags, components" 11 | behavior: default 12 | require_changes: false 13 | require_base: false 14 | require_head: true 15 | hide_project_coverage: false 16 | 17 | ignore: 18 | - "**/*.test-d.*" 19 | - "**/test-utils/*" 20 | 21 | component_management: 22 | individual_components: 23 | - component_id: overlay-kit 24 | name: "overlay-kit" 25 | paths: 26 | - packages/** 27 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .next -------------------------------------------------------------------------------- /docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import stylexPlugin from '@stylexswc/nextjs-plugin'; 2 | import nextra from 'nextra'; 3 | import { remarkSandpack } from 'remark-sandpack'; 4 | 5 | const withNextra = nextra({ 6 | theme: 'nextra-theme-docs', 7 | themeConfig: './theme.config.tsx', 8 | mdxOptions: { 9 | remarkPlugins: [remarkSandpack], 10 | }, 11 | }); 12 | 13 | export default stylexPlugin({ 14 | useCSSLayers: true, 15 | })( 16 | withNextra({ 17 | i18n: { 18 | locales: ['en', 'ko'], 19 | defaultLocale: 'en', 20 | }, 21 | eslint: { ignoreDuringBuilds: true }, 22 | }) 23 | ); 24 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start" 8 | }, 9 | "type": "commonjs", 10 | "dependencies": { 11 | "@codesandbox/sandpack-react": "^2.19.10", 12 | "@react-three/drei": "^9.120.4", 13 | "@react-three/fiber": "^8.17.10", 14 | "@stylexjs/stylex": "^0.9.3", 15 | "@stylexswc/nextjs-plugin": "^0.5.0", 16 | "@suspensive/react": "^2.18.10", 17 | "codehike": "^1.0.4", 18 | "motion": "^11.15.0", 19 | "next": "^15.1.2", 20 | "nextra": "^3.2.5", 21 | "nextra-theme-docs": "^3.2.5", 22 | "react": "^18.0.0", 23 | "react-dom": "^18.0.0", 24 | "remark-sandpack": "^0.0.5", 25 | "three": "^0.171.0", 26 | "zod": "^3.24.1" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "22.10.2", 30 | "@types/react": "^18", 31 | "@types/react-dom": "^18", 32 | "@types/three": "^0", 33 | "typescript": "^5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/overlay-kit/14e5fa14c10af71d8ae746ddc5390454aa274b25/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/overlay-kit/14e5fa14c10af71d8ae746ddc5390454aa274b25/docs/public/logo-black.png -------------------------------------------------------------------------------- /docs/public/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/overlay-kit/14e5fa14c10af71d8ae746ddc5390454aa274b25/docs/public/logo-white.png -------------------------------------------------------------------------------- /docs/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toss/overlay-kit/14e5fa14c10af71d8ae746ddc5390454aa274b25/docs/public/og.png -------------------------------------------------------------------------------- /docs/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './main'; 2 | export * from './sandpack'; 3 | -------------------------------------------------------------------------------- /docs/src/components/sandpack/base-template.ts: -------------------------------------------------------------------------------- 1 | export const baseTemplate = { 2 | files: { 3 | '/hideReactErrorOverlay.css': { 4 | code: ` 5 | body > iframe { 6 | display: none; 7 | }`, 8 | hidden: true, 9 | }, 10 | '/index.tsx': { 11 | code: ` 12 | import React from "react"; 13 | import { createRoot } from "react-dom/client"; 14 | import "./styles.css"; 15 | import App from "./App"; 16 | 17 | const root = createRoot(document.getElementById("root")); 18 | root.render(); 19 | `, 20 | hidden: true, 21 | }, 22 | '/App.tsx': { 23 | code: ` 24 | import { Example } from './Example' 25 | import './hideReactErrorOverlay.css' 26 | 27 | export default function App() { 28 | return ( 29 | 30 | ) 31 | } 32 | `, 33 | hidden: true, 34 | }, 35 | }, 36 | dependencies: { 37 | '@emotion/react': 'latest', 38 | '@emotion/styled': 'latest', 39 | '@mui/material': '^6.4.8', 40 | 'overlay-kit': 'latest', 41 | }, 42 | devDependencies: {}, 43 | }; 44 | -------------------------------------------------------------------------------- /docs/src/components/sandpack/custom-preset.tsx: -------------------------------------------------------------------------------- 1 | import { SandpackCodeEditor, SandpackLayout, SandpackPreview } from '@codesandbox/sandpack-react'; 2 | import stylex from '@stylexjs/stylex'; 3 | import { type ComponentProps, useEffect, useRef, useState } from 'react'; 4 | import type { Sandpack } from '.'; 5 | 6 | export const CustomPreset = ( 7 | props: Pick, 'layoutOptions' | 'editorOptions' | 'previewOptions'> 8 | ) => { 9 | const [horizontalSize, setHorizontalSize] = useState(50); 10 | const dragEventTargetRef = useRef<(EventTarget & HTMLDivElement) | null>(null); 11 | 12 | useEffect(() => { 13 | document.body.addEventListener('mousemove', onDragMove); 14 | document.body.addEventListener('mouseup', stopDragging); 15 | 16 | return () => { 17 | document.body.removeEventListener('mousemove', onDragMove); 18 | document.body.removeEventListener('mouseup', stopDragging); 19 | }; 20 | 21 | function onDragMove(event: MouseEvent) { 22 | if (!dragEventTargetRef.current) return; 23 | 24 | const container = dragEventTargetRef.current.parentElement; 25 | 26 | if (!container) return; 27 | 28 | const direction = dragEventTargetRef.current.dataset.direction as 'horizontal' | 'vertical'; 29 | const isHorizontal = direction === 'horizontal'; 30 | 31 | const { left, top, height, width } = container.getBoundingClientRect(); 32 | const offset = isHorizontal ? ((event.clientX - left) / width) * 100 : ((event.clientY - top) / height) * 100; 33 | const boundaries = Math.min(Math.max(offset, 25), 75); 34 | 35 | setHorizontalSize(boundaries); 36 | container.querySelectorAll(`.sp-stack`).forEach((item) => { 37 | item.style.pointerEvents = 'none'; 38 | }); 39 | } 40 | 41 | function stopDragging() { 42 | const container = dragEventTargetRef.current?.parentElement; 43 | 44 | if (!container) return; 45 | 46 | container.querySelectorAll(`.sp-stack`).forEach((item) => { 47 | item.style.pointerEvents = ''; 48 | }); 49 | 50 | dragEventTargetRef.current = null; 51 | } 52 | }, []); 53 | 54 | return ( 55 | 56 | 68 |
{ 71 | dragEventTargetRef.current = event.currentTarget; 72 | }} 73 | style={{ 74 | left: `calc(${horizontalSize}% - 5px)`, 75 | }} 76 | {...stylex.props(styles.resizeHandler)} 77 | /> 78 | 87 | 88 | ); 89 | }; 90 | 91 | const styles = stylex.create({ 92 | codeEditor: { 93 | flexBasis: 0, 94 | overflow: 'hidden', 95 | height: 400, 96 | }, 97 | resizeHandler: { 98 | position: 'absolute', 99 | top: 0, 100 | bottom: 0, 101 | width: 10, 102 | cursor: 'ew-resize', 103 | }, 104 | preview: { 105 | flexBasis: 0, 106 | height: 400, 107 | }, 108 | }); 109 | -------------------------------------------------------------------------------- /docs/src/components/sandpack/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type CodeEditorProps, 3 | type PreviewProps, 4 | type SandpackLayoutProps, 5 | SandpackProvider, 6 | type SandpackProviderProps, 7 | } from '@codesandbox/sandpack-react'; 8 | import { baseTemplate } from './base-template'; 9 | import { CustomPreset } from './custom-preset'; 10 | 11 | interface SandpackProps extends Omit { 12 | dependencies?: Record; 13 | devDependencies?: Record; 14 | providerOptions?: SandpackProviderProps; 15 | layoutOptions?: SandpackLayoutProps; 16 | editorOptions?: CodeEditorProps; 17 | previewOptions?: PreviewProps & { 18 | showConsole?: boolean; 19 | showConsoleButton?: boolean; 20 | layout?: 'preview' | 'tests' | 'console'; 21 | }; 22 | } 23 | 24 | export const Sandpack = (props: SandpackProps) => { 25 | return ( 26 | 50 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /docs/src/middleware.ts: -------------------------------------------------------------------------------- 1 | export { middleware } from 'nextra/locales'; 2 | 3 | export const config = { 4 | matcher: [ 5 | /* 6 | * Match all request paths except for the ones starting with: 7 | * - api (API routes) 8 | * - _next/static (static files) 9 | * - _next/image (image optimization files) 10 | * - favicon.ico (favicon file) 11 | * - img (image files) 12 | */ 13 | '/((?!api|_next/static|_next/image|favicon.ico|img).*)', 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /docs/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | 3 | export default function App({ Component, pageProps }: AppProps) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/pages/en/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | type: 'page', 4 | display: 'hidden', 5 | theme: { 6 | layout: 'raw', 7 | }, 8 | }, 9 | docs: { 10 | type: 'page', 11 | title: 'Documentation', 12 | }, 13 | api: { 14 | type: 'page', 15 | title: 'API', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | components: { 3 | title: 'Components', 4 | }, 5 | utils: { 6 | title: 'Utils', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/components/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'overlay-provider': { 3 | title: '', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/components/overlay-provider.mdx: -------------------------------------------------------------------------------- 1 | # \ 2 | 3 | `OverlayProvider` determines where overlays are rendered in your React application. 4 | 5 | ```tsx 6 | 7 | 8 | 9 | ``` 10 | 11 | ## Reference 12 | 13 | Since overlays are typically rendered above other elements, they should be placed at the root of your application. 14 | 15 | ```tsx 16 | 17 | 18 | 19 | ``` 20 | 21 | ### Important Notes 22 | 23 | - `OverlayProvider` should be rendered **only once** in your React application. Multiple instances may prevent overlays from working correctly due to context propagation issues. 24 | 25 | ## Usage 26 | 27 | `` provides the context needed to render all overlays. 28 | 29 | Any component that renders overlays must be placed under the `` component. 30 | 31 | ```tsx {18-21} 32 | import React from 'react'; 33 | import { OverlayProvider, overlay } from 'overlay-kit'; 34 | import { Modal } from '@src/components'; 35 | 36 | function App() { 37 | const notify = () => 38 | overlay.open(({ isOpen, close, unmount }) => ( 39 | 40 | {/* Modal content */} 41 | 42 | )); 43 | 44 | return ; 45 | } 46 | 47 | export function Root() { 48 | return ( 49 | 50 | 51 | {/* All overlays will be rendered here */} 52 | 53 | ); 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'overlay-open': { 3 | title: 'overlay.open', 4 | }, 5 | 'overlay-open-async': { 6 | title: 'overlay.openAsync', 7 | }, 8 | 'overlay-close': { 9 | title: 'overlay.close', 10 | }, 11 | 'overlay-close-all': { 12 | title: 'overlay.closeAll', 13 | }, 14 | 'overlay-unmount': { 15 | title: 'overlay.unmount', 16 | }, 17 | 'overlay-unmount-all': { 18 | title: 'overlay.unmountAll', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/overlay-close-all.mdx: -------------------------------------------------------------------------------- 1 | # overlay.closeAll 2 | 3 | `overlay.closeAll` is a function that closes all currently open overlays. 4 | 5 | It removes overlays from the screen but doesn't completely delete them from memory. 6 | 7 | ```ts 8 | overlay.closeAll(); 9 | ``` 10 | 11 | ## Reference 12 | 13 | `overlay.close()` 14 | 15 | Call `overlay.closeAll` when you need to close all open overlays. 16 | 17 | ```tsx 18 | overlay.closeAll(); 19 | ``` 20 | 21 | ### Important Notes 22 | 23 | When this function is called, overlays disappear from the screen but remain in memory and the React element tree. 24 | 25 | To completely remove overlays, you need to additionally call `overlay.unmountAll` after the animations end. 26 | 27 | ## Usage 28 | 29 | You can open multiple overlays and close them all at once using `overlay.closeAll`. 30 | 31 | ```tsx {12} 32 | overlay.open(({ isOpen, close, unmount }) => { 33 | return ; 34 | }); 35 | overlay.open(({ isOpen, close, unmount }) => { 36 | return ; 37 | }); 38 | overlay.open(({ isOpen, close, unmount }) => { 39 | return ; 40 | }); 41 | 42 | // Closes all three overlays above 43 | overlay.closeAll(); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/overlay-close.mdx: -------------------------------------------------------------------------------- 1 | # overlay.close 2 | 3 | `overlay.close` is a function that closes a specific overlay using the provided `overlayId`. 4 | 5 | It removes the overlay from the screen but doesn't completely delete it from memory. 6 | 7 | ```ts 8 | overlay.close(overlayId); 9 | ``` 10 | 11 | ## Reference 12 | 13 | `overlay.close(overlayId)` 14 | 15 | Call `overlay.close` when you need to close a specific overlay. 16 | 17 | ```tsx 18 | overlay.close(overlayId); 19 | ``` 20 | 21 | ### Parameters 22 | 23 | - `overlayId`: The unique ID of the overlay to close. 24 | - This ID is either returned from `overlay.open` or can be directly specified in the `options` object. 25 | 26 | ### Important Notes 27 | 28 | When this function is called, the overlay disappears from the screen but remains in memory and the React element tree. 29 | 30 | To completely remove the overlay, you need to additionally call `overlay.unmount` after the animation ends. 31 | 32 | ## Usage 33 | 34 | ### Closing an Overlay with Auto-generated ID 35 | 36 | Here's how to close an overlay using the ID returned by `overlay.open`. 37 | 38 | ```tsx {5} 39 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 40 | return ; 41 | }); 42 | 43 | overlay.close(overlayId); 44 | ``` 45 | 46 | ### Using a Custom ID 47 | 48 | You can also specify a unique ID when calling `overlay.open` or `overlay.openAsync`. 49 | 50 | ```tsx {10} 51 | const overlayId = 'unique-id'; 52 | 53 | overlay.open( 54 | ({ isOpen, close, unmount }) => { 55 | return ; 56 | }, 57 | { overlayId } 58 | ); 59 | 60 | overlay.close(overlayId); 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/overlay-open-async.mdx: -------------------------------------------------------------------------------- 1 | # overlay.openAsync 2 | 3 | `overlay.openAsync` is used when you need to receive user input (e.g., confirm or cancel) from an overlay. 4 | 5 | ```ts 6 | const result = await overlay.openAsync(controller, options); 7 | ``` 8 | 9 | ## Reference 10 | 11 | `overlay.openAsync(controller, options?)` 12 | 13 | Call `overlay.openAsync` when you need to open an overlay that returns a value. 14 | 15 | ```tsx 16 | overlay.openAsync(({ isOpen, close, unmount }) => { 17 | function confirm() { 18 | close(true); 19 | } 20 | function cancel() { 21 | close(false); 22 | } 23 | 24 | return ; 25 | }); 26 | ``` 27 | 28 | ### Parameters 29 | 30 | - `controller`: The overlay controller function. Returns JSX to render the overlay and receives parameters for overlay state and control functions. 31 | - `isOpen`: A value indicating whether the overlay is open. 32 | - `close`: Function to close the overlay. When called, returns the passed value as a `Promise` and closes the overlay.
The overlay information remains in memory for closing animations. Call `unmount` to completely remove it. 33 | - `unmount`: Function to remove the overlay.
If called immediately with a closing animation, the component may be removed before the animation completes. 34 | - **optional** `options`: Overlay options. 35 | - `overlayId`: Specify a unique ID when opening the overlay. 36 | 37 | ### Return Value 38 | 39 | Returns the value passed to the `close` function. 40 | 41 | ### Important Notes 42 | 43 | When manually specifying an ID, be careful not to duplicate IDs with other overlays. Opening multiple overlays with duplicate IDs may cause unexpected behavior. 44 | 45 | ## Usage 46 | 47 | Below is an example of opening a confirmation dialog (ConfirmDialog) and handling different actions based on user input. 48 | 49 | ```tsx 50 | const result = await overlay.openAsync(({ isOpen, close, unmount }) => { 51 | function confirm() { 52 | close(true); 53 | } 54 | function cancel() { 55 | close(false); 56 | } 57 | 58 | return ; 59 | }); 60 | 61 | if (result === true) { 62 | console.log('User selected confirm.'); 63 | } else { 64 | console.log('User selected cancel.'); 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/overlay-open.mdx: -------------------------------------------------------------------------------- 1 | # overlay.open 2 | 3 | `overlay.open` is used to open overlays such as Alerts or Notifications. 4 | 5 | This function provides utilities for controlling and managing the overlay's state. 6 | 7 | ```ts 8 | const overlayId = overlay.open(controller, options); 9 | ``` 10 | 11 | ## Reference 12 | 13 | `overlay.open(controller, options?)` 14 | 15 | Call `overlay.open` when you need to open an overlay. 16 | 17 | ```tsx 18 | overlay.open(({ isOpen, close, unmount }) => { 19 | return ; 20 | }); 21 | ``` 22 | 23 | ### Parameters 24 | 25 | - `controller`: The overlay controller function. Returns JSX to render the overlay and receives parameters for overlay state and control functions. 26 | - `isOpen`: A value indicating whether the overlay is open. 27 | - `close`: Function to close the overlay.
The overlay information remains in memory to show closing animations. Call `unmount` to completely remove it. 28 | - `unmount`: Function to remove the overlay.
If called immediately with a closing animation, the component may be removed before the animation completes. 29 | - **optional** `options`: Object for passing additional information when opening an overlay. 30 | - `overlayId`: Specify a unique ID when opening the overlay. This ID is used to identify the overlay. 31 | 32 | ### Return Value 33 | 34 | Returns a unique ID for the overlay as a string. If `overlayId` is not specified, a random string is returned. 35 | 36 | ### Important Notes 37 | 38 | When manually specifying an ID, be careful not to duplicate IDs with other overlays. Opening multiple overlays with duplicate IDs may cause unexpected behavior. 39 | 40 | ## Usage 41 | 42 | ### Opening an Overlay 43 | 44 | Here's a simple example of opening and closing an Alert. 45 | 46 | ```tsx 47 | overlay.open(({ isOpen, close, unmount }) => { 48 | return ; 49 | }); 50 | ``` 51 | 52 | ### Opening an Overlay with a Unique ID 53 | 54 | Specifying a unique ID is useful for identifying or closing specific overlays. 55 | 56 | ```tsx 57 | const overlayId = 'unique-overlay-id'; 58 | 59 | overlay.open( 60 | ({ isOpen, close, unmount }) => { 61 | return ; 62 | }, 63 | { overlayId } 64 | ); 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/overlay-unmount-all.mdx: -------------------------------------------------------------------------------- 1 | # overlay.unmountAll 2 | 3 | `overlay.unmountAll` is a function that completely removes all open overlays from both the React element tree and memory. 4 | 5 | ```ts 6 | overlay.unmountAll(); 7 | ``` 8 | 9 | ## Reference 10 | 11 | `overlay.unmountAll()` 12 | 13 | Call `overlay.unmountAll` when you need to free up memory for all overlays. 14 | 15 | ```tsx 16 | overlay.unmountAll(); 17 | ``` 18 | 19 | ### Important Notes 20 | 21 | - When this function is called, overlays are immediately removed from memory, which may cause closing animations to not be displayed. 22 | - For overlays with animations, you should call `overlay.closeAll` first and then call `overlay.unmountAll` after the closing animations complete to provide a smooth user experience. 23 | 24 | ## Interface 25 | 26 | ```tsx 27 | function unmountAll(): void; 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Using Auto-generated IDs 33 | 34 | Here's a simple example of opening multiple overlays and removing them all using `overlay.unmountAll`. 35 | 36 | ```tsx {12} 37 | overlay.open(({ isOpen, close, unmount }) => { 38 | return ; 39 | }); 40 | overlay.open(({ isOpen, close, unmount }) => { 41 | return ; 42 | }); 43 | overlay.open(({ isOpen, close, unmount }) => { 44 | return ; 45 | }); 46 | 47 | // Removes all three overlays above 48 | overlay.unmountAll(); 49 | ``` 50 | 51 | ### With Animations 52 | 53 | For overlays with animations, you should call `overlay.unmountAll` after the closing animations complete to provide a natural user experience. 54 | 55 | ```tsx {7-9} 56 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 57 | return ; 58 | }); 59 | 60 | overlay.closeAll(); 61 | 62 | setTimeout(() => { 63 | overlay.unmountAll(); 64 | }, 1000); 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/src/pages/en/api/utils/overlay-unmount.mdx: -------------------------------------------------------------------------------- 1 | # overlay.unmount 2 | 3 | `overlay.unmount` is a function that completely removes a specific overlay from memory. 4 | 5 | When this function is called, the overlay with the specified `overlayId` is removed from both the React element tree and memory. 6 | 7 | ```ts 8 | overlay.unmount(overlayId); 9 | ``` 10 | 11 | ## Reference 12 | 13 | `overlay.unmount(overlayId)` 14 | 15 | Call `overlay.unmount` when you need to free up memory for a specific overlay. 16 | 17 | ```tsx 18 | overlay.unmount(overlayId); 19 | ``` 20 | 21 | ### Parameters 22 | 23 | - `overlayId`: The unique ID of the overlay to remove. 24 | - This ID is either returned from `overlay.open` or can be directly specified in the `options` object. 25 | 26 | ### Important Notes 27 | 28 | - When this function is called, the overlay is immediately removed from memory, which may cause closing animations to not be displayed. 29 | - For overlays with animations, you should call `overlay.close` first and then call `overlay.unmount` after the closing animation completes to provide a smooth user experience. 30 | 31 | ## Usage 32 | 33 | ### Using Auto-generated ID 34 | 35 | Here's a simple example of opening an overlay and removing it using `overlay.unmount`. 36 | 37 | ```tsx {5} 38 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 39 | return ; 40 | }); 41 | 42 | overlay.unmount(overlayId); 43 | ``` 44 | 45 | ### Using a Custom ID 46 | 47 | You can explicitly specify an ID when calling `overlay.open`. 48 | 49 | ```tsx {10} 50 | const overlayId = 'unique-id'; 51 | 52 | overlay.open( 53 | ({ isOpen, close, unmount }) => { 54 | return ; 55 | }, 56 | { overlayId } 57 | ); 58 | 59 | overlay.unmount(overlayId); 60 | ``` 61 | 62 | ### With Animation 63 | 64 | To remove an overlay with a closing animation from memory, you should call `overlay.unmount` after the animation ends. 65 | 66 | ```tsx {7-9} 67 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 68 | return ; 69 | }); 70 | 71 | overlay.close(overlayId); 72 | 73 | setTimeout(() => { 74 | overlay.unmount(overlayId); 75 | }, 1000); 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | guides: { 3 | title: 'Guides', 4 | }, 5 | more: { 6 | title: 'Learn More', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/guides/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | introduction: { 3 | title: 'Getting Started', 4 | }, 5 | 'think-in-overlay-kit': { 6 | title: 'Think in overlay-kit', 7 | }, 8 | 'code-comparison': { 9 | title: 'Code Comparison', 10 | }, 11 | faq: { 12 | title: 'FAQ', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/guides/code-comparison.mdx: -------------------------------------------------------------------------------- 1 | # Code Comparison 2 | 3 | Using overlay-kit makes managing React overlays much simpler. Let's take a look at the changes through example code. 4 | 5 | ## Before: Traditional React Overlay Implementation 6 | 7 | Before using overlay-kit, there is a lot of boilerplate code, and due to React's Hook rules, the state declaration, state change, and rendering logic are separated, disrupting the code flow. The `isOpen` state declaration, the `onClick` state change, and the `` component that renders based on the state are far apart. 8 | 9 | ```tsx filename="MyPage.tsx" {4,10-12,17-22} 10 | import { useState } from 'react'; 11 | 12 | function MyPage() { 13 | const [isOpen, setIsOpen] = useState(false); 14 | /* Other Hook calls... */ 15 | return ( 16 | <> 17 | {/* Other components... */} 18 | 25 | {/* Other components... */} 26 | { 29 | setIsOpen(false); 30 | }} 31 | /> 32 | 33 | ); 34 | } 35 | ``` 36 | 37 | ## After: Overlay Implementation with overlay-kit 38 | 39 | In contrast, the code using overlay-kit is more cohesive and intuitive. The flow of the code, which states that clicking the button opens the overlay, is clear at a glance. 40 | 41 | ```tsx filename="MyPage.tsx" {10-14} 42 | import { overlay } from 'overlay-kit'; 43 | 44 | function MyPage() { 45 | /* Other Hook calls... */ 46 | 47 | return ( 48 | <> 49 | {/* Other components... */} 50 | 59 | 60 | ); 61 | } 62 | ``` 63 | 64 | The boilerplate code is significantly reduced. You no longer need to manage the overlay state directly. 65 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/guides/faq.mdx: -------------------------------------------------------------------------------- 1 | # FAQ (Frequently Asked Questions) 2 | 3 | Here are common questions and answers about using `overlay-kit`. 4 | 5 | If you have additional questions or find topics not covered in the documentation, please leave your feedback in [GitHub Issues](https://github.com/toss/overlay-kit/issues). 6 | 7 | ## Q. When is overlay-kit most useful? 8 | 9 | **A:** 10 | `overlay-kit` is particularly useful in these situations: 11 | 12 | - **Complex Overlay Management**: Easily manage chained dialogs and nested overlays. 13 | - **Alignment with React Philosophy**: Define UI declaratively instead of managing state. 14 | - **Performance Optimization**: Efficiently handle heavy overlays that frequently open and close. 15 | - **Large Applications**: Provide consistent overlay management across the entire team. 16 | 17 | ## Q. What's the difference between `overlay.open` and `overlay.openAsync`? 18 | 19 | **A:** 20 | These methods differ in how they handle overlays: 21 | 22 | - **`overlay.open`**: Handles basic overlay opening and closing operations. 23 | - **`overlay.openAsync`**: Returns a Promise for handling results asynchronously. 24 | 25 | **Comparison Example**: 26 | 27 | ```tsx 28 | // overlay.open 29 | overlay.open(({ isOpen, close }) => ( 30 | 31 |

Simple overlay

32 |
33 | )); 34 | 35 | // overlay.openAsync 36 | const result = await overlay.openAsync(({ isOpen, close }) => ( 37 | close(false)}> 38 | 39 | 40 | )); 41 | 42 | console.log(result ? 'Yes' : 'No'); 43 | ``` 44 | 45 | ## Q. What's the difference between `close` and `unmount`? 46 | 47 | **A:** 48 | Both close overlays but handle memory differently: 49 | 50 | - **`close`**: Closes the overlay but keeps state in memory. Previous state is restored when reopened. 51 | - **`unmount`**: Completely removes the overlay from memory. Starts with initial state when reopened. 52 | 53 | **Use Cases**: 54 | 55 | - **`close`**: Use for performance optimization with frequently opened/closed overlays. 56 | - **`unmount`**: Use to prevent memory leaks by removing overlays no longer needed. 57 | 58 | ## Q. What's the difference between `overlay.closeAll` and `overlay.unmountAll`? 59 | 60 | **A:** 61 | 62 | - **`overlay.closeAll`**: Closes all open overlays but keeps state in memory. 63 | - **`overlay.unmountAll`**: Completely removes all overlays from memory. 64 | 65 | **Comparison Example**: 66 | 67 | ```tsx 68 | // Close all overlays 69 | overlay.closeAll(); 70 | 71 | // Remove all overlays 72 | overlay.unmountAll(); 73 | ``` 74 | 75 | ## Q. Why does state persist when reopening a closed overlay? 76 | 77 | **A:** 78 | `close` keeps state in memory when closing an overlay. To reset state, use `unmount` to completely remove it from memory. 79 | 80 | ## Q. When should I use `unmount`? 81 | 82 | **A:** 83 | 84 | - Generally, using just `close` is sufficient if the overlay doesn't maintain heavy data. 85 | - Use `unmount` or `unmountAll` when overlays are no longer needed or you need to free up memory. 86 | 87 | ## Q. Which UI libraries can I use with overlay-kit? 88 | 89 | **A:** 90 | `overlay-kit` is not tied to any specific UI library and works with any React-based UI library. 91 | 92 | For example: 93 | 94 | - **Material-UI** 95 | - **Chakra UI** 96 | - **Ant Design** 97 | 98 | ## Q. Does overlay-kit support TypeScript? 99 | 100 | **A:** 101 | Yes, `overlay-kit` has full TypeScript support. 102 | 103 | **Example**: 104 | 105 | ```tsx 106 | const result = await overlay.openAsync(({ isOpen, close }) => ( 107 | close(false)}> 108 | 109 | 110 | )); 111 | ``` 112 | 113 | ## Q. Why isn't my closing animation showing? 114 | 115 | **A:** 116 | Calling `unmount` immediately skips the closing animation. 117 | To maintain the closing animation, call `close` first, then call `unmount` after the animation completes. 118 | 119 | **Example**: 120 | 121 | ```tsx 122 | overlay.open(({ isOpen, close, unmount }) => ( 123 | { 126 | close(); // Run closing animation 127 | setTimeout(() => unmount(), 300); // Remove from memory after animation 128 | }} 129 | > 130 |

Maintain animation

131 |
132 | )); 133 | ``` 134 | 135 | ## Additional Questions 136 | 137 | If you have additional questions or find topics that should be covered in the documentation, please leave your feedback in [GitHub Issues](https://github.com/toss/overlay-kit/issues). 138 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/guides/hooks.mdx: -------------------------------------------------------------------------------- 1 | import { Sandpack } from '@/components'; 2 | 3 | # Hooks 4 | 5 | `overlay-kit` provides `useCurrentOverlay` and `useOverlayData` hooks to manage overlay state globally. 6 | 7 | Using `useCurrentOverlay` and `useOverlayData`, you can implement state-based UX control, focusing, and conditional rendering more flexibly even outside of overlays. 8 | 9 | --- 10 | 11 | ## useCurrentOverlay 12 | 13 | Returns the ID of the currently top-most overlay. 14 | 15 | The overlay ID can be directly specified by passing `overlayId` as the second argument to `overlay.open()` or `overlay.openAsync()` when opening an overlay: 16 | 17 | ```tsx 18 | overlay.open( 19 | ({ isOpen, close }) => , 20 | { overlayId: 'custom-overlayId' } 21 | ); 22 | ``` 23 | If `overlayId` is omitted, it will be automatically generated internally as `overlay-kit-[random number]`. 24 | 25 | You can use this ID to conditionally branch based on whether a specific overlay is open, or for focus/shortcut control. 26 | 27 |
28 | Let's see how the value of `useCurrentOverlay` changes when opening overlays A and B. 29 |
30 | 31 | 32 | ```tsx Example.tsx active 33 | import { OverlayProvider, overlay, useCurrentOverlay } from 'overlay-kit'; 34 | import Button from '@mui/material/Button'; 35 | import Typography from '@mui/material/Typography'; 36 | import { ConfirmDialog } from './confirm-dialog'; 37 | 38 | function App() { 39 | const current = useCurrentOverlay(); 40 | 41 | return ( 42 |
43 | 59 | 60 | 77 | 78 | 79 | Current value: {current} 80 | 81 |
82 | ); 83 | } 84 | 85 | export const Example = () => { 86 | return ( 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | ``` 94 | 95 | ```tsx confirm-dialog.tsx 96 | import Button from '@mui/material/Button'; 97 | import Dialog from '@mui/material/Dialog'; 98 | import DialogTitle from '@mui/material/DialogTitle'; 99 | import DialogContent from '@mui/material/DialogContent'; 100 | import DialogActions from '@mui/material/DialogActions'; 101 | import Typography from '@mui/material/Typography'; 102 | import { overlay } from 'overlay-kit'; 103 | 104 | export function ConfirmDialog({ isOpen, close, title }) { 105 | return ( 106 | 107 | {title} 108 | 109 | This is the content of {title}. 110 | 111 | 112 | 113 | 114 | 115 | ); 116 | } 117 | 118 | ``` 119 | 120 |
121 | 122 | --- 123 | 124 | ## useOverlayData 125 | 126 | Returns information about all overlays currently in memory. 127 | 128 | This includes not only open overlays but also closed overlays that remain in memory. 129 | 130 | Each overlay item consists of the following properties: 131 | 132 | | Property | Type | Description | 133 | |------------------|----------------------------|-------------| 134 | | `id` | `string` | Unique ID that identifies the overlay. Specified as `overlayId` when calling `overlay.open()`, or automatically generated if omitted. | 135 | | `componentKey` | `string` | Internal unique key used for React to correctly render and distinguish overlay UI. A new value is assigned each time it is opened. | 136 | | `isOpen` | `boolean` | Indicates whether the overlay is currently open. Becomes `false` when `close()` is called, and remains in memory until `unmount()`. | 137 | | `controller` | `FC` | React component that actually renders the overlay. The UI function passed to `overlay.open()` is stored in this field. | 138 | 139 | For example, you can check it like this: 140 | 141 | ```tsx 142 | const overlayData = useOverlayData(); 143 | 144 | Object.entries(overlayData).forEach(([id, item]) => { 145 | console.log(id); // overlayId 146 | console.log(item.isOpen); // true / false 147 | console.log(item.controller); // component rendering function 148 | }); 149 | ``` 150 | 151 |
152 | Let's see how the overlay information remaining in `useOverlayData` changes when closing overlays using `close` and `unmount` methods. 153 |
154 | 155 | 156 | ```tsx Example.tsx active 157 | import { OverlayProvider, overlay, useOverlayData } from 'overlay-kit'; 158 | import Button from '@mui/material/Button'; 159 | import Stack from '@mui/material/Stack'; 160 | import List from '@mui/material/List'; 161 | import ListItem from '@mui/material/ListItem'; 162 | import Typography from '@mui/material/Typography'; 163 | import { ConfirmDialog } from './confirm-dialog'; 164 | 165 | function App() { 166 | const overlayData = useOverlayData(); 167 | 168 | const allOverlayIds = Object.keys(overlayData); 169 | const openOverlayIds = allOverlayIds.filter( 170 | (id) => overlayData[id].isOpen 171 | ); 172 | 173 | return ( 174 |
175 | 176 | 192 | 193 | 210 | 211 |
212 | Opened Overlays 213 | 214 | {openOverlayIds.length > 0 ? ( 215 | openOverlayIds.map((id) => {id}) 216 | ) : ( 217 | None 218 | )} 219 | 220 | 221 | 222 | Overlays in Memory 223 | 224 | 225 | {allOverlayIds.length > 0 ? ( 226 | allOverlayIds.map((id) => {id}) 227 | ) : ( 228 | None 229 | )} 230 | 231 |
232 |
233 | ); 234 | } 235 | 236 | export const Example = () => { 237 | return ( 238 | 239 | 240 | 241 | ); 242 | }; 243 | 244 | ``` 245 | 246 | ```tsx confirm-dialog.tsx 247 | import Button from '@mui/material/Button'; 248 | import Dialog from '@mui/material/Dialog'; 249 | import DialogTitle from '@mui/material/DialogTitle'; 250 | import DialogContent from '@mui/material/DialogContent'; 251 | import DialogActions from '@mui/material/DialogActions'; 252 | import Typography from '@mui/material/Typography'; 253 | 254 | export function ConfirmDialog({ isOpen, close, title }) { 255 | return ( 256 | 257 | {title} 258 | 259 | This is the content of {title}. 260 | 261 | 262 | 263 | 264 | 265 | ); 266 | } 267 | 268 | ``` 269 | 270 |
271 | 272 | --- 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/guides/think-in-overlay-kit.mdx: -------------------------------------------------------------------------------- 1 | # Think in overlay-kit 2 | 3 | Let's explore `overlay-kit`, which is based on React's philosophy, and the **Declarative Overlay Pattern** that embodies it. 4 | 5 | ## Why Use overlay-kit 6 | 7 | ### Problems with Traditional Overlay Management 8 | 9 | 1. **Complexity of State Management** 10 | 11 | - Had to manage overlay state directly using `useState` or global state. 12 | - Code became complex and less readable as state management mixed with UI logic. 13 | 14 | 2. **Repetitive Event Handling** 15 | 16 | - Had to repeatedly write event handling code for opening, closing, and returning results. 17 | - This led to code duplication and degraded development experience. 18 | 19 | 3. **Lack of Reusability** 20 | 21 | - UI and logic were tightly coupled through callback functions to return values from overlays. 22 | - This made it difficult to reuse components. 23 | 24 | ### Goals of overlay-kit 25 | 26 | 1. **Design Following React Philosophy** 27 | 28 | - React favors declarative code. 29 | - `overlay-kit` helps manage overlays declaratively. 30 | 31 | 2. **Improve Development Productivity** 32 | 33 | - By encapsulating state management and event handling, developers can focus solely on UI and business logic. 34 | 35 | 3. **Enhance Extensibility and Reusability** 36 | 37 | - Increased overlay reusability by separating UI and behavior, and returning Promises. 38 | 39 | ## Declarative Overlay Pattern 40 | 41 | Traditional overlay management used an **Imperative** approach. 42 | Code was complex and hard to maintain due to state management with `useState` and mixed event handling code. 43 | 44 | The **Declarative Overlay Pattern** allows writing more intuitive and declarative code by managing overlays based on **behavior** rather than state. 45 | 46 | #### Imperative Approach 47 | 48 | State management and event handling were mixed, leading to code duplication and less readability. 49 | 50 | ```tsx 51 | import { useState } from 'react'; 52 | 53 | function Overlay() { 54 | const [isOpen, setIsOpen] = useState(false); 55 | 56 | function handleOpen() { 57 | setIsOpen(true); 58 | } 59 | 60 | function handleClose() { 61 | setIsOpen(false); 62 | } 63 | 64 | return ( 65 | <> 66 | 67 | 68 | Imperative Overlay 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | ``` 78 | 79 | #### Declarative Approach 80 | 81 | State management code was removed, leading to improved readability and maintainability. 82 | 83 | ```tsx 84 | import { overlay } from 'overlay-kit'; 85 | 86 | function Overlay() { 87 | return ( 88 | 95 | 96 | 97 |
98 | )); 99 | }} 100 | > 101 | Open 102 | 103 | ); 104 | } 105 | ``` 106 | 107 | ## Core Principles 108 | 109 | To understand the design of overlay-kit, the following principles are important: 110 | 111 | ### Co-location 112 | 113 | Related code is placed close together to make the code easier to understand. 114 | 115 | All overlay calls, state management, and component definition are handled in one place, leading to better readability. 116 | 117 | ### Minimum API 118 | 119 | overlay-kit provides a simple and concise API. The core APIs are two: 120 | 121 | 1. `overlay.open`: Provides functionality to open and close overlays. 122 | 2. `overlay.openAsync`: Allows processing asynchronous logic by returning values. 123 | 124 | These two APIs leverage general JavaScript patterns to enable implementing various overlays. 125 | 126 | For example, overlay.openAsync returns a Promise, making it easy to apply chaining patterns. 127 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/more/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | basic: { 3 | title: 'Opening Overlays', 4 | }, 5 | 'open-outside-react': { 6 | title: 'Opening Overlays Outside React', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/more/basic.mdx: -------------------------------------------------------------------------------- 1 | import { Sandpack } from '@/components'; 2 | 3 | # Opening Overlays 4 | 5 | ## Opening Simple Overlays 6 | 7 | You can easily open and close overlays using `overlay.open`. 8 | 9 |
10 | 11 | 12 | 13 | ```tsx Example.tsx active 14 | import { OverlayProvider, overlay } from 'overlay-kit'; 15 | import Button from '@mui/material/Button'; 16 | import Dialog from '@mui/material/Dialog'; 17 | import DialogActions from '@mui/material/DialogActions'; 18 | import DialogContent from '@mui/material/DialogContent'; 19 | import DialogContentText from '@mui/material/DialogContentText'; 20 | import DialogTitle from '@mui/material/DialogTitle'; 21 | 22 | function App() { 23 | return ( 24 | 32 | 33 | 34 |
35 | ); 36 | }); 37 | }} 38 | > 39 | Open Confirm Dialog 40 | 41 | ); 42 | } 43 | 44 | export function Example() { 45 | return ( 46 | 47 | 48 | 49 | ); 50 | }; 51 | ``` 52 | 53 | 54 | 55 | ## Opening Asynchronous Overlays 56 | 57 | You can handle overlay results as a `Promise` using `overlay.openAsync`. 58 | 59 |
60 | 61 | 62 | 63 | ```tsx Example.tsx active 64 | import { useState } from 'react'; 65 | import { OverlayProvider, overlay } from 'overlay-kit'; 66 | import Button from '@mui/material/Button'; 67 | import Dialog from '@mui/material/Dialog'; 68 | import DialogActions from '@mui/material/DialogActions'; 69 | import DialogContent from '@mui/material/DialogContent'; 70 | import DialogContentText from '@mui/material/DialogContentText'; 71 | import DialogTitle from '@mui/material/DialogTitle'; 72 | 73 | function App() { 74 | const [result, setResult] = useState(); 75 | 76 | return ( 77 |
78 | 86 | 87 | 88 | 89 | ); 90 | }); 91 | 92 | setResult(result); 93 | }} 94 | > 95 | Open Confirm Dialog 96 | 97 |

result: {result ? 'Y' : 'N'}

98 |
99 | ); 100 | } 101 | 102 | export function Example() { 103 | return ( 104 | 105 | 106 | 107 | ); 108 | }; 109 | ``` 110 | 111 |
112 | 113 | ## Releasing Overlay Memory 114 | 115 | You can completely remove overlays from memory using `unmount`. 116 | 117 | However, if there's a closing animation, calling `unmount` immediately might prevent the animation from showing. 118 | In this case, call `close` first, then execute `unmount` after the animation completes. 119 | 120 | ### Using the `onExit` prop 121 | 122 | By implementing the `onExit` prop that indicates the closing animation has finished, you can remove the overlay immediately after the animation ends. 123 | 124 |
125 | 126 | 127 | 128 | ```tsx Example.tsx active 129 | import { OverlayProvider, overlay } from 'overlay-kit'; 130 | import Button from '@mui/material/Button'; 131 | import { ConfirmDialog } from './confirm-dialog'; 132 | 133 | function App() { 134 | return ( 135 | <> 136 | 145 | 146 | ); 147 | } 148 | 149 | export function Example() { 150 | return ( 151 | 152 | 153 | 154 | ); 155 | }; 156 | ``` 157 | 158 | ```tsx confirm-dialog.tsx 159 | import { useState, useEffect } from 'react'; 160 | import Button from '@mui/material/Button'; 161 | import Dialog from '@mui/material/Dialog'; 162 | import DialogTitle from '@mui/material/DialogTitle'; 163 | import DialogContent from '@mui/material/DialogContent'; 164 | import DialogActions from '@mui/material/DialogActions'; 165 | 166 | export function ConfirmDialog({ isOpen, close, onExit }) { 167 | const [count, setCount] = useState(0); 168 | 169 | useEffect(() => { 170 | return () => onExit(); 171 | }, []); 172 | 173 | return ( 174 | 175 | Are you sure you want to continue? 176 | 177 | 178 |

count: {count}

179 | 180 |
181 | 182 | 183 | 184 | 185 | 186 |
187 | ); 188 | } 189 | ``` 190 | 191 |
192 | 193 | ### Using `setTimeout` 194 | 195 | If there's no `onExit` prop, you can use `setTimeout` to remove the overlay after the animation ends. 196 | Set an appropriate time according to the animation duration. 197 | 198 | In the following code, the `close` function closes the overlay and uses `setTimeout` to call the `unmount` function after 600ms. 199 | 200 |
201 | 202 | 203 | 204 | ```tsx Example.tsx active 205 | import { OverlayProvider, overlay } from 'overlay-kit'; 206 | import Button from '@mui/material/Button'; 207 | import { ConfirmDialog } from './confirm-dialog'; 208 | 209 | function App() { 210 | return ( 211 | <> 212 | 229 | 230 | ); 231 | } 232 | 233 | export function Example() { 234 | return ( 235 | 236 | 237 | 238 | ); 239 | }; 240 | ``` 241 | 242 | ```tsx confirm-dialog.tsx 243 | import { useState, useEffect } from 'react'; 244 | import Button from '@mui/material/Button'; 245 | import Dialog from '@mui/material/Dialog'; 246 | import DialogTitle from '@mui/material/DialogTitle'; 247 | import DialogContent from '@mui/material/DialogContent'; 248 | import DialogActions from '@mui/material/DialogActions'; 249 | 250 | export function ConfirmDialog({ isOpen, close }) { 251 | const [count, setCount] = useState(0); 252 | 253 | return ( 254 | 255 | Are you sure you want to continue? 256 | 257 | 258 |

count: {count}

259 | 260 |
261 | 262 | 263 | 264 | 265 | 266 |
267 | ); 268 | } 269 | ``` 270 | 271 |
272 | -------------------------------------------------------------------------------- /docs/src/pages/en/docs/more/open-outside-react.mdx: -------------------------------------------------------------------------------- 1 | import { Sandpack } from '@/components'; 2 | 3 | # Opening Overlays Outside React 4 | 5 | With `overlay-kit`, you can open overlays from outside React components. 6 | 7 | For example, you can show an overlay to notify users when an error occurs during API calls. 8 | 9 | ```tsx {7-11} 10 | import ky from 'ky'; 11 | import { overlay } from 'overlay-kit'; 12 | 13 | const api = ky.extend({ 14 | hooks: { 15 | afterResponse: [ 16 | (_, __, response) => { 17 | console.log('test:: response', response); 18 | if (response.status >= 400) { 19 | overlay.open(({ isOpen, close }) => ); 20 | } 21 | }, 22 | ], 23 | }, 24 | }); 25 | ``` 26 | 27 | The above code extends `ky` to check the status code after API calls and open an overlay if there's an error. 28 | 29 | ## Complete Example: Opening Overlays After API Requests 30 | 31 | Here's a complete example of showing overlays after API requests. We use `overlay.open` to notify whether the API was successful. 32 | 33 |
34 | 35 | 36 | 37 | ```tsx Example.tsx active 38 | import ky from 'ky'; 39 | import { OverlayProvider, overlay } from 'overlay-kit'; 40 | import Button from '@mui/material/Button'; 41 | import { Alert } from './alert'; 42 | 43 | const api = ky.extend({ 44 | hooks: { 45 | afterResponse: [ 46 | (_, __, response) => { 47 | overlay.open(({ isOpen, close, unmount }) => { 48 | return ; 49 | }); 50 | }, 51 | ], 52 | }, 53 | }); 54 | 55 | function App() { 56 | return ; 57 | } 58 | 59 | export function Example() { 60 | return ( 61 | 62 | 63 | 64 | ); 65 | }; 66 | ``` 67 | 68 | ```tsx alert.tsx 69 | import { useEffect } from 'react'; 70 | import Button from '@mui/material/Button'; 71 | import Dialog from '@mui/material/Dialog'; 72 | import DialogTitle from '@mui/material/DialogTitle'; 73 | import DialogActions from '@mui/material/DialogActions'; 74 | 75 | export function Alert({ isOpen, close, onExit }) { 76 | useEffect(() => { 77 | return () => onExit(); 78 | }, []); 79 | 80 | return ( 81 | 82 | API Response Received 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | ``` 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/src/pages/en/index.mdx: -------------------------------------------------------------------------------- 1 | import { Main } from '@/components'; 2 | 3 |
23 | -------------------------------------------------------------------------------- /docs/src/pages/ko/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | type: 'page', 4 | display: 'hidden', 5 | theme: { 6 | layout: 'raw', 7 | }, 8 | }, 9 | docs: { 10 | type: 'page', 11 | title: '문서보기', 12 | }, 13 | api: { 14 | type: 'page', 15 | title: 'API', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | components: { 3 | title: '컴포넌트', 4 | }, 5 | utils: { 6 | title: '유틸', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/components/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'overlay-provider': { 3 | title: '', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/components/overlay-provider.mdx: -------------------------------------------------------------------------------- 1 | # \ 2 | 3 | `OverlayProvider`는 React 애플리케이션에서 오버레이가 렌더링되는 위치를 결정해요. 4 | 5 | ```tsx 6 | 7 | 8 | 9 | ``` 10 | 11 | ## 레퍼런스 12 | 13 | 일반적으로 오버레이는 다른 요소 위에 렌더링되므로 애플리케이션의 루트에 배치해요. 14 | 15 | ```tsx 16 | 17 | 18 | 19 | ``` 20 | 21 | ### 주의 사항 22 | 23 | - `OverlayProvider`는 React 애플리케이션 전체에서 **단 한 번만** 렌더링해야 해요. 중복 렌더링 시 컨텍스트가 올바르게 전달되지 않아 오버레이가 작동하지 않을 수 있어요. 24 | 25 | ## 사용법 26 | 27 | ``는 모든 오버레이를 렌더링할 수 있는 컨텍스트를 제공해요. 28 | 29 | 오버레이를 렌더링하는 컴포넌트는 모두 `` 컴포넌트 아래에 있어야 해요. 30 | 31 | ```tsx {18-21} 32 | import React from 'react'; 33 | import { OverlayProvider, overlay } from 'overlay-kit'; 34 | import { Modal } from '@src/components'; 35 | 36 | function App() { 37 | const notify = () => 38 | overlay.open(({ isOpen, close, unmount }) => ( 39 | 40 | {/* 모달 내용 */} 41 | 42 | )); 43 | 44 | return ; 45 | } 46 | 47 | export function Root() { 48 | return ( 49 | 50 | 51 | {/* 모든 오버레이는 이곳에 렌더링됩니다. */} 52 | 53 | ); 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | 'overlay-open': { 3 | title: 'overlay.open', 4 | }, 5 | 'overlay-open-async': { 6 | title: 'overlay.openAsync', 7 | }, 8 | 'overlay-close': { 9 | title: 'overlay.close', 10 | }, 11 | 'overlay-close-all': { 12 | title: 'overlay.closeAll', 13 | }, 14 | 'overlay-unmount': { 15 | title: 'overlay.unmount', 16 | }, 17 | 'overlay-unmount-all': { 18 | title: 'overlay.unmountAll', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/overlay-close-all.mdx: -------------------------------------------------------------------------------- 1 | # overlay.closeAll 2 | 3 | `overlay.closeAll`은 현재 열려 있는 모든 오버레이를 닫는 함수예요. 4 | 5 | 화면에서 오버레이를 제거하지만, 메모리에서 완전히 삭제하지는 않아요. 6 | 7 | ```ts 8 | overlay.closeAll(); 9 | ``` 10 | 11 | ## 레퍼런스 12 | 13 | `overlay.close()` 14 | 15 | 열려있는 모든 오버레이를 닫을 때 `overlay.closeAll`을 호출하세요. 16 | 17 | ```tsx 18 | overlay.closeAll(); 19 | ``` 20 | 21 | ### 주의사항 22 | 23 | 이 함수를 호출하면 화면에서 오버레이가 사라지지만, 오버레이는 여전히 메모리와 React 요소 트리에 남아 있어요. 24 | 25 | 오버레이를 완전히 제거하려면 애니메이션이 끝난 후에 `overlay.unmountAll`을 추가로 호출해야 해요. 26 | 27 | ## 사용법 28 | 29 | 여러 개의 오버레이를 열고, `overlay.closeAll`을 사용해 한 번에 닫을 수 있어요. 30 | 31 | ```tsx {12} 32 | overlay.open(({ isOpen, close, unmount }) => { 33 | return ; 34 | }); 35 | overlay.open(({ isOpen, close, unmount }) => { 36 | return ; 37 | }); 38 | overlay.open(({ isOpen, close, unmount }) => { 39 | return ; 40 | }); 41 | 42 | // 위 세 개의 오버레이를 모두 닫습니다. 43 | overlay.closeAll(); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/overlay-close.mdx: -------------------------------------------------------------------------------- 1 | # overlay.close 2 | 3 | `overlay.close`는 지정한 `overlayId`를 사용해 특정 오버레이를 닫는 함수예요. 4 | 5 | 화면에서 오버레이를 제거하지만, 메모리에서 완전히 삭제하지는 않아요. 6 | 7 | ```ts 8 | overlay.close(overlayId); 9 | ``` 10 | 11 | ## 레퍼런스 12 | 13 | `overlay.close(overlayId)` 14 | 15 | 특정 오버레이를 닫을 때 `overlay.close`를 호출하세요. 16 | 17 | ```tsx 18 | overlay.close(overlayId); 19 | ``` 20 | 21 | ### 매개변수 22 | 23 | - `overlayId`: 닫을 오버레이의 고유 ID예요. 24 | - 이 ID는 `overlay.open` 호출 시 반환되거나, `options` 객체에서 `overlayId`를 직접 지정할 수 있어요. 25 | 26 | ### 주의사항 27 | 28 | 이 함수를 호출하면 화면에서 오버레이가 사라지지만, 오버레이는 여전히 메모리와 React 요소 트리에 남아 있어요. 29 | 30 | 오버레이를 완전히 제거하려면 애니메이션이 끝난 후에 `overlay.unmount`를 추가로 호출해야 해요. 31 | 32 | ## 사용법 33 | 34 | ### 자동 생성된 ID로 오버레이 닫기 35 | 36 | `overlay.open`이 반환한 ID를 사용해 오버레이를 닫는 방법이에요. 37 | 38 | ```tsx {5} 39 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 40 | return ; 41 | }); 42 | 43 | overlay.close(overlayId); 44 | ``` 45 | 46 | ### 고유한 ID를 사용하는 경우 47 | 48 | `overlay.open` 또는 `overlay.openAsync` 호출 시 고유한 ID를 직접 지정할 수도 있어요. 49 | 50 | ```tsx {10} 51 | const overlayId = 'unique-id'; 52 | 53 | overlay.open( 54 | ({ isOpen, close, unmount }) => { 55 | return ; 56 | }, 57 | { overlayId } 58 | ); 59 | 60 | overlay.close(overlayId); 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/overlay-open-async.mdx: -------------------------------------------------------------------------------- 1 | # overlay.openAsync 2 | 3 | `overlay.openAsync`는 사용자 입력(예: 확인 또는 취소)을 받아야 하는 오버레이를 열 때 사용해요. 4 | 5 | ```ts 6 | const result = await overlay.openAsync(controller, options); 7 | ``` 8 | 9 | ## 레퍼런스 10 | 11 | `overlay.openAsync(controller, options?)` 12 | 13 | 값을 전달받는 오버레이를 열 때 `overlay.openAsync`을 호출하세요. 14 | 15 | ```tsx 16 | overlay.openAsync(({ isOpen, close, unmount }) => { 17 | function confirm() { 18 | close(true); 19 | } 20 | function cancel() { 21 | close(false); 22 | } 23 | 24 | return ; 25 | }); 26 | ``` 27 | 28 | ### 매개변수 29 | 30 | - `controller`: 오버레이 컨트롤러 함수예요. 오버레이를 렌더링할 JSX를 반환하며, 오버레이의 상태와 제어 함수들을 매개변수로 받아요. 31 | - `isOpen`: 오버레이가 열렸는지 여부를 나타내는 값이에요. 32 | - `close`: 오버레이를 닫는 함수예요. 호출 시 인자로 전달한 값을 `Promise`로 반환하며 오버레이가 닫혀요.
오버레이 닫기 애니메이션 등을 보여주기 위해서 오버레이 정보는 메모리에 계속 남아있어요. 이것을 완전히 제거하려면 `unmount` 함수를 호출하세요. 33 | - `unmount`: 오버레이를 제거하는 함수예요.
오버레이 닫기 애니메이션이 있을 때 `unmount`를 바로 호출하면 오버레이가 닫히기 전에 컴포넌트가 제거되어 애니메이션이 제대로 표시되지 않을 수 있어요. 34 | - **optional** `options`: 오버레이 옵션입니다. 35 | - `overlayId`: 오버레이를 열 때 고유한 ID를 지정해요. 36 | 37 | ### 반환값 38 | 39 | `close` 함수로 전달한 값을 반환해요. 40 | 41 | ### 주의사항 42 | 43 | 수동으로 ID를 지정할 때는 다른 오버레이와 중복되지 않도록 주의하세요. 중복된 ID로 여러 오버레이를 열면 의도치 않은 동작이 발생할 수 있어요. 44 | 45 | ## 사용법 46 | 47 | 아래는 확인 창(ConfirmDialog)을 열어 사용자의 입력에 따라 다르게 동작하는 예제예요. 48 | 49 | ```tsx 50 | const result = await overlay.openAsync(({ isOpen, close, unmount }) => { 51 | function confirm() { 52 | close(true); 53 | } 54 | function cancel() { 55 | close(false); 56 | } 57 | 58 | return ; 59 | }); 60 | 61 | if (result === true) { 62 | console.log('사용자가 확인을 선택했어요.'); 63 | } else { 64 | console.log('사용자가 취소를 선택했어요.'); 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/overlay-open.mdx: -------------------------------------------------------------------------------- 1 | # overlay.open 2 | 3 | `overlay.open`은 경고창(Alert) 또는 알림(Notification)과 같은 오버레이를 열기 위해 사용해요. 4 | 5 | 이 함수는 오버레이의 상태와 제어를 위한 유틸리티를 제공해요. 6 | 7 | ```ts 8 | const overlayId = overlay.open(controller, options); 9 | ``` 10 | 11 | ## 레퍼런스 12 | 13 | `overlay.open(controller, options?)` 14 | 15 | 오버레이를 열 때 `overlay.open`을 호출하세요. 16 | 17 | ```tsx 18 | overlay.open(({ isOpen, close, unmount }) => { 19 | return ; 20 | }); 21 | ``` 22 | 23 | ### 매개변수 24 | 25 | - `controller`: 오버레이 컨트롤러 함수예요. 오버레이를 렌더링할 JSX를 반환하며, 오버레이의 상태와 제어 함수들을 매개변수로 받아요. 26 | - `isOpen`: 오버레이가 열렸는지 여부를 나타내는 값이에요. 27 | - `close`: 오버레이를 닫는 함수예요.
오버레이 닫기 애니메이션 등을 보여주기 위해서 오버레이 정보는 메모리에 계속 남아있어요. 이것을 완전히 제거하려면 `unmount` 함수를 호출하세요. 28 | - `unmount`: 오버레이를 제거하는 함수예요.
오버레이 닫기 애니메이션이 있을 때 `unmount`를 바로 호출하면 오버레이가 닫히기 전에 컴포넌트가 제거되어 애니메이션이 제대로 표시되지 않을 수 있어요. 29 | - **optional** `options`: 오버레이를 열 때 추가 정보를 전달받는 객체예요. 30 | - `overlayId`: 오버레이를 열 때 고유한 ID를 지정해요. 이 ID는 오버레이를 식별하는 데 사용돼요. 31 | 32 | ### 반환값 33 | 34 | 오버레이의 고유한 ID를 문자열로 반환해요. 만약 `overlayId`를 지정하지 않았다면 랜덤한 문자열이 반환돼요. 35 | 36 | ### 주의사항 37 | 38 | 수동으로 ID를 지정할 때는 다른 오버레이와 중복되지 않도록 주의하세요. 중복된 ID로 여러 오버레이를 열면 의도치 않은 동작이 발생할 수 있어요. 39 | 40 | ## 사용법 41 | 42 | ### 오버레이 열기 43 | 44 | 아래 예시는 경고창(Alert)을 열고 닫는 간단한 사용법을 보여줘요. 45 | 46 | ```tsx 47 | overlay.open(({ isOpen, close, unmount }) => { 48 | return ; 49 | }); 50 | ``` 51 | 52 | ### 고유한 ID를 가진 오버레이 열기 53 | 54 | 고유한 ID를 지정하면 특정 오버레이를 식별하거나 특정 오버레이를 닫을 때 유용해요. 55 | 56 | ```tsx 57 | const overlayId = 'unique-overlay-id'; 58 | 59 | overlay.open( 60 | ({ isOpen, close, unmount }) => { 61 | return ; 62 | }, 63 | { overlayId } 64 | ); 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/overlay-unmount-all.mdx: -------------------------------------------------------------------------------- 1 | # overlay.unmountAll 2 | 3 | `overlay.unmountAll`은 열려 있는 모든 오버레이를 React 요소 트리와 메모리에서 완전히 제거하는 함수예요. 4 | 5 | ```ts 6 | overlay.unmountAll(); 7 | ``` 8 | 9 | ## 레퍼런스 10 | 11 | `overlay.unmountAll()` 12 | 13 | 특정 오버레이의 메모리를 해제할 때 `overlay.unmountAll`를 호출하세요. 14 | 15 | ```tsx 16 | overlay.unmountAll(); 17 | ``` 18 | 19 | ### 주의사항 20 | 21 | - 이 함수를 호출하면 즉시 메모리를 해제하기 때문에 오버레이 닫기 애니메이션이 보이지 않을 수 있어요. 22 | - 애니메이션이 있는 오버레이의 경우, `overlay.closeAll`을 호출하고 닫기 애니메이션이 끝난 후 `overlay.unmountAll`을 호출해야 자연스러운 사용자 경험을 제공할 수 있어요. 23 | 24 | ## 인터페이스 25 | 26 | ```tsx 27 | function unmountAll(): void; 28 | ``` 29 | 30 | ## 사용법 31 | 32 | ### 자동으로 생성된 ID를 사용하는 경우 33 | 34 | 아래는 여러 개의 오버레이를 열고, `overlay.unmountAll`을 사용해 모두 제거하는 간단한 예제예요. 35 | 36 | ```tsx {12} 37 | overlay.open(({ isOpen, close, unmount }) => { 38 | return ; 39 | }); 40 | overlay.open(({ isOpen, close, unmount }) => { 41 | return ; 42 | }); 43 | overlay.open(({ isOpen, close, unmount }) => { 44 | return ; 45 | }); 46 | 47 | // 위 세 개의 오버레이를 모두 닫습니다. 48 | overlay.unmountAll(); 49 | ``` 50 | 51 | ### 애니메이션이 있는 경우 52 | 53 | 애니메이션이 적용된 경우에는 닫기 애니메이션이 끝난 후 `overlay.unmountAll`을 호출해야 자연스러운 사용자 경험을 제공할 수 있어요. 54 | 55 | ```tsx {7-9} 56 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 57 | return ; 58 | }); 59 | 60 | overlay.closeAll(); 61 | 62 | setTimeout(() => { 63 | overlay.unmountAll(); 64 | }, 1000); 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/src/pages/ko/api/utils/overlay-unmount.mdx: -------------------------------------------------------------------------------- 1 | # overlay.unmount 2 | 3 | `overlay.unmount`는 특정 오버레이를 메모리에서 완전히 제거하는 함수예요. 4 | 5 | 이 함수를 호출하면 지정한 `overlayId`에 해당하는 오버레이가 React 요소 트리와 메모리에서 삭제돼요. 6 | 7 | ```ts 8 | overlay.unmount(overlayId); 9 | ``` 10 | 11 | ## 레퍼런스 12 | 13 | `overlay.unmount(overlayId)` 14 | 15 | 특정 오버레이의 메모리를 해제할 때 `overlay.unmount`를 호출하세요. 16 | 17 | ```tsx 18 | overlay.unmount(overlayId); 19 | ``` 20 | 21 | ### 매개변수 22 | 23 | - `overlayId`: 닫을 오버레이의 고유 ID예요. 24 | - 이 ID는 `overlay.open` 호출 시 반환되거나, `options` 객체에서 `overlayId`를 직접 지정할 수 있어요. 25 | 26 | ### 주의사항 27 | 28 | - 이 함수를 호출하면 즉시 메모리를 해제하기 때문에 오버레이 닫기 애니메이션이 보이지 않을 수 있어요. 29 | - 애니메이션이 있는 오버레이의 경우, `overlay.close`를 호출하고 닫기 애니메이션이 끝난 후 `overlay.unmount`를 호출해야 자연스러운 사용자 경험을 제공할 수 있어요. 30 | 31 | ## 사용법 32 | 33 | ### 자동으로 생성된 ID를 사용하는 경우 34 | 35 | `overlay.open`이 반환한 ID를 사용해 오버레이를 메모리에서 제거하는 방법이에요. 36 | 37 | ```tsx {5} 38 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 39 | return ; 40 | }); 41 | 42 | overlay.unmount(overlayId); 43 | ``` 44 | 45 | ### 고유한 ID를 사용하는 경우 46 | 47 | `overlay.open` 호출 시 ID를 명시적으로 지정할 수 있어요. 48 | 49 | ```tsx {10} 50 | const overlayId = 'unique-id'; 51 | 52 | overlay.open( 53 | ({ isOpen, close, unmount }) => { 54 | return ; 55 | }, 56 | { overlayId } 57 | ); 58 | 59 | overlay.unmount(overlayId); 60 | ``` 61 | 62 | ### 애니메이션이 있는 경우 63 | 64 | 닫기 애니메이션이 있는 오버레이를 메모리에서 제거하려면 애니메이션이 끝난 후에 `overlay.unmount`를 호출해야 해요. 65 | 66 | ```tsx {7-9} 67 | const overlayId = overlay.open(({ isOpen, close, unmount }) => { 68 | return ; 69 | }); 70 | 71 | overlay.close(overlayId); 72 | 73 | setTimeout(() => { 74 | overlay.unmount(overlayId); 75 | }, 1000); 76 | ``` 77 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | guides: { 3 | title: '가이드', 4 | }, 5 | more: { 6 | title: '더 알아보기', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/guides/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | introduction: { 3 | title: '시작하기', 4 | }, 5 | 'think-in-overlay-kit': { 6 | title: 'overlay-kit으로 생각하기', 7 | }, 8 | 'code-comparison': { 9 | title: '코드 비교', 10 | }, 11 | faq: { 12 | title: 'FAQ', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/guides/code-comparison.mdx: -------------------------------------------------------------------------------- 1 | # 코드 비교 2 | 3 | overlay-kit을 사용함으로써 리액트 오버레이 관리가 훨씬 쉬워졌어요. 예시 코드를 통해 변화를 살펴봐요. 4 | 5 | ## Before: 기존 리액트 오버레이 구현 6 | 7 | overlay-kit을 사용하지 않았을 때의 코드는 다음과 같아요. 많은 보일러플레이트 코드가 필요하고, 리액트 훅의 규칙 때문에 상태 선언, 상태 변화, 렌더링 로직이 분리되어 코드 흐름을 파악하기가 어려웠어요. `isOpen` 상태 선언, `onClick` 상태 변화, 상태에 따라 렌더링되는 `` 컴포넌트가 멀리 떨어져 있어요. 8 | 9 | ```tsx filename="MyPage.tsx" {4,10-12,17-22} 10 | import { useState } from 'react'; 11 | 12 | function MyPage() { 13 | const [isOpen, setIsOpen] = useState(false); 14 | /* Other Hook calls... */ 15 | return ( 16 | <> 17 | {/* Other components... */} 18 | 25 | {/* Other components... */} 26 | { 29 | setIsOpen(false); 30 | }} 31 | /> 32 | 33 | ); 34 | } 35 | ``` 36 | 37 | ## After: overlay-kit 사용 이후 38 | 39 | 반면, overlay-kit을 사용한 코드는 더 일관성 있고 직관적이에요. 버튼을 클릭하면 오버레이가 열리는 흐름이 명확하게 보여요. 40 | 41 | ```tsx filename="MyPage.tsx" {10-14} 42 | import { overlay } from 'overlay-kit'; 43 | 44 | function MyPage() { 45 | /* Other Hook calls... */ 46 | 47 | return ( 48 | <> 49 | {/* Other components... */} 50 | 59 | 60 | ); 61 | } 62 | ``` 63 | 64 | 보일러플레이트 코드가 크게 줄어들었어요. 이제 오버레이 상태를 직접 관리할 필요가 없어졌어요. 65 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/guides/faq.mdx: -------------------------------------------------------------------------------- 1 | # FAQ (Frequently Asked Questions) 2 | 3 | `overlay-kit`을 사용하며 자주 나오는 질문과 답변을 모았습니다. 4 | 5 | 추가로 궁금한 점이 있거나 문서에서 다루지 않은 내용이 있다면 [GitHub Issues](https://github.com/toss/overlay-kit/issues)에 의견을 남겨주세요. 6 | 7 | ## Q. overlay-kit은 어떤 상황에서 가장 유용한가요? 8 | 9 | **A:** 10 | `overlay-kit`은 다음과 같은 상황에서 특히 유용합니다: 11 | 12 | - **복잡한 오버레이 관리**: 체이닝된 다이얼로그, 중첩된 오버레이 등을 간단히 관리할 수 있어요. 13 | - **React 철학과의 일치**: 상태 관리 대신 선언적으로 UI를 정의할 수 있어요. 14 | - **성능 최적화**: 자주 열리고 닫히는 무거운 오버레이를 효율적으로 처리해요. 15 | - **대규모 애플리케이션**: 팀 전체에서 일관된 오버레이 관리 방식을 제공해요. 16 | 17 | ## Q. `overlay.open`과 `overlay.openAsync`의 차이는 무엇인가요? 18 | 19 | **A:** 20 | 두 메서드는 오버레이를 여는 방식에서 차이가 있습니다: 21 | 22 | - **`overlay.open`**: 단순히 오버레이를 열고 닫는 동작을 처리합니다. 23 | - **`overlay.openAsync`**: Promise를 반환해 비동기적으로 결과를 처리할 수 있습니다. 24 | 25 | **비교 예제**: 26 | 27 | ```tsx 28 | // overlay.open 29 | overlay.open(({ isOpen, close }) => ( 30 | 31 |

단순 오버레이

32 |
33 | )); 34 | 35 | // overlay.openAsync 36 | const result = await overlay.openAsync(({ isOpen, close }) => ( 37 | close(false)}> 38 | 39 | 40 | )); 41 | 42 | console.log(result ? 'Yes' : 'No'); 43 | ``` 44 | 45 | ## Q. `close`와 `unmount`의 차이점은 무엇인가요? 46 | 47 | **A:** 48 | 둘 다 오버레이를 닫지만, 메모리 처리 방식에 차이가 있습니다: 49 | 50 | - **`close`**: 오버레이를 닫지만 상태는 메모리에 남아 있어요. 다시 열면 이전 상태가 복원됩니다. 51 | - **`unmount`**: 오버레이를 메모리에서 완전히 제거합니다. 이후 다시 열면 초기 상태로 시작합니다. 52 | 53 | **사용 사례**: 54 | 55 | - **`close`**: 자주 열리고 닫히는 오버레이에서 성능 최적화를 위해 사용해요. 56 | - **`unmount`**: 더 이상 필요 없는 오버레이를 메모리에서 제거해 메모리 릭을 방지할 때 사용해요. 57 | 58 | ## Q. `overlay.closeAll`과 `overlay.unmountAll`의 차이는 무엇인가요? 59 | 60 | **A:** 61 | 62 | - **`overlay.closeAll`**: 모든 열려 있는 오버레이를 닫지만 상태는 메모리에 남아 있어요. 63 | - **`overlay.unmountAll`**: 모든 오버레이를 메모리에서 완전히 제거합니다. 64 | 65 | **비교 예제**: 66 | 67 | ```tsx 68 | // 모든 오버레이 닫기 69 | overlay.closeAll(); 70 | 71 | // 모든 오버레이 제거 72 | overlay.unmountAll(); 73 | ``` 74 | 75 | ## Q. 오버레이를 닫았는데 다시 열면 상태가 유지됩니다. 왜 그런가요? 76 | 77 | **A:** 78 | `close`는 상태를 메모리에 유지한 채 오버레이를 닫아요. 상태를 초기화하려면 `unmount`를 호출해 메모리에서 완전히 제거해야 해요. 79 | 80 | ## Q. 언제 `unmount`를 사용해야 하나요? 81 | 82 | **A:** 83 | 84 | - 오버레이가 무거운 데이터를 유지하지 않는다면 일반적으로 `close`만 사용해도 충분해요. 85 | - 오버레이가 더 이상 필요 없거나 메모리를 확보해야 할 때는 `unmount` 또는 `unmountAll`을 사용하세요. 86 | 87 | ## Q. overlay-kit은 어떤 UI 라이브러리와 함께 사용할 수 있나요? 88 | 89 | **A:** 90 | `overlay-kit`은 특정 UI 라이브러리에 종속적이지 않으며 React 기반의 모든 UI 라이브러리와 호환돼요. 91 | 92 | 예를 들어: 93 | 94 | - **Material-UI** 95 | - **Chakra UI** 96 | - **Ant Design** 97 | 98 | ## Q. overlay-kit은 TypeScript를 지원하나요? 99 | 100 | **A:** 101 | 네, `overlay-kit`은 TypeScript를 완벽히 지원해요. 102 | 103 | **예제**: 104 | 105 | ```tsx 106 | const result = await overlay.openAsync(({ isOpen, close }) => ( 107 | close(false)}> 108 | 109 | 110 | )); 111 | ``` 112 | 113 | ## Q. 오버레이의 닫기 애니메이션이 보이지 않습니다. 이유가 뭔가요? 114 | 115 | **A:** 116 | `unmount`를 바로 호출하면 닫기 애니메이션이 실행되지 않아요. 117 | 닫기 애니메이션을 유지하려면 `close`를 호출하고, 애니메이션이 끝난 뒤 `unmount`를 호출하세요. 118 | 119 | **예제**: 120 | 121 | ```tsx 122 | overlay.open(({ isOpen, close, unmount }) => ( 123 | { 126 | close(); // 닫기 애니메이션 실행 127 | setTimeout(() => unmount(), 300); // 애니메이션 이후 메모리에서 제거 128 | }} 129 | > 130 |

애니메이션 유지

131 |
132 | )); 133 | ``` 134 | 135 | ## 추가 질문 136 | 137 | 추가로 궁금한 사항이 있거나 문서에서 다뤄야 할 내용을 발견하셨다면, [GitHub Issues](https://github.com/toss/overlay-kit/issues)에 의견을 남겨주세요. 138 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/guides/hooks.mdx: -------------------------------------------------------------------------------- 1 | import { Sandpack } from '@/components'; 2 | 3 | # Hooks 4 | 5 | `overlay-kit`은 오버레이 상태를 전역에서 관리할 수 있도록 6 | `useCurrentOverlay`, `useOverlayData` 훅을 제공해요. 7 | 8 | 9 | `useCurrentOverlay`와 `useOverlayData`를 활용하면 10 | 오버레이 외부에서도 상태 기반 UX 제어, 포커싱, 조건적 렌더링 등을 더욱 유연하게 구현할 수 있어요. 11 | 12 | --- 13 | 14 | ## useCurrentOverlay 15 | 16 | 현재 가장 위에 떠 있는 오버레이의 ID를 반환해요. 17 | 18 | 오버레이의 ID는 오버레이를 열 때 `overlay.open()` 또는 `overlay.openAsync()`의 19 | 두 번째 인자로 다음처럼 `overlayId`를 넘겨 직접 지정할 수 있어요: 20 | 21 | ```tsx 22 | overlay.open( 23 | ({ isOpen, close }) => , 24 | { overlayId: 'custom-overlayId' } 25 | ); 26 | ``` 27 | 만약 overlayId를 생략하면 내부에서 랜덤으로 `overlay-kit-[랜덤숫자]`로 ID가 생성돼요. 28 | 29 | 30 | 이 ID를 기반으로 특정 오버레이가 열렸는지 조건을 분기하거나 포커스/단축키 제어 등에 활용할 수 있어요. 31 | 32 |
33 | 오버레이 A와 B를 설정하여 각 오버레이를 열었을 때 useCurrentOverlay의 값이 어떻게 변하는지 살펴볼게요. 34 |
35 | 36 | 37 | ```tsx Example.tsx active 38 | import { OverlayProvider, overlay, useCurrentOverlay } from 'overlay-kit'; 39 | import Button from '@mui/material/Button'; 40 | import Typography from '@mui/material/Typography'; 41 | import { ConfirmDialog } from './confirm-dialog'; 42 | 43 | function App() { 44 | const current = useCurrentOverlay(); 45 | 46 | return ( 47 |
48 | 64 | 65 | 82 | 83 | 84 | 현재 current 값: {current} 85 | 86 |
87 | ); 88 | } 89 | 90 | export const Example = () => { 91 | return ( 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | ``` 99 | 100 | ```tsx confirm-dialog.tsx 101 | import Button from '@mui/material/Button'; 102 | import Dialog from '@mui/material/Dialog'; 103 | import DialogTitle from '@mui/material/DialogTitle'; 104 | import DialogContent from '@mui/material/DialogContent'; 105 | import DialogActions from '@mui/material/DialogActions'; 106 | import Typography from '@mui/material/Typography'; 107 | 108 | export function ConfirmDialog({ isOpen, close, title }) { 109 | return ( 110 | 111 | {title} 112 | 113 | {title} 내용입니다. 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | 122 | ``` 123 | 124 |
125 | 126 | --- 127 | 128 | ## useOverlayData 129 | 130 | 현재 메모리에 존재하는 모든 오버레이 상태 정보를 반환해요. 131 | 132 | 133 | 열린 오버레이뿐만 아니라 닫혔지만 메모리에 남아 있는 오버레이도 포함돼요. 134 | 135 | 136 | 각 오버레이 항목은 다음과 같은 속성으로 구성되어 있어요: 137 | 138 | | 속성명 | 타입 | 설명 | 139 | |------------------|----------------------------|------| 140 | | `id` | `string` | 오버레이를 식별하는 고유 ID예요. `overlay.open()` 시 `overlayId`로 지정하거나, 생략 시 자동으로 생성돼요. | 141 | | `componentKey` | `string` | 내부적으로 사용하는 고유 키로, 오버레이 UI를 React가 올바르게 렌더링하고 구분하는 데 사용돼요. 매번 열릴 때마다 새 값이 할당돼요. | 142 | | `isOpen` | `boolean` | 현재 오버레이가 열려 있는지 여부를 나타내요. `close()`를 호출하면 `false`가 되고, `unmount()`되기 전까지는 메모리에 남아 있어요. | 143 | | `controller` | `FC` | 오버레이를 실제로 렌더링하는 React 컴포넌트예요. `overlay.open()` 시 넘겨준 함수 형태의 UI가 이 필드에 저장돼요. | 144 | 145 | 예를 들어, 다음처럼 확인할 수 있어요: 146 | 147 | ```tsx 148 | const overlayData = useOverlayData(); 149 | 150 | Object.entries(overlayData).forEach(([id, item]) => { 151 | console.log(id); // overlayId 152 | console.log(item.isOpen); // true / false 153 | console.log(item.controller); // 컴포넌트 렌더링 함수 154 | }); 155 | ``` 156 | 157 |
158 | 각각의 오버레이를 `close`와 `unmount` 방식으로 닫았을 때, `useOverlayData`에 남아 있는 오버레이 정보가 어떻게 달라지는지 살펴볼게요. 159 |
160 | 161 | 162 | ```tsx Example.tsx active 163 | import { OverlayProvider, overlay, useOverlayData } from 'overlay-kit'; 164 | import Button from '@mui/material/Button'; 165 | import Stack from '@mui/material/Stack'; 166 | import List from '@mui/material/List'; 167 | import ListItem from '@mui/material/ListItem'; 168 | import Typography from '@mui/material/Typography'; 169 | import { ConfirmDialog } from './confirm-dialog'; 170 | 171 | function App() { 172 | const overlayData = useOverlayData(); 173 | 174 | const allOverlayIds = Object.keys(overlayData); 175 | const openOverlayIds = allOverlayIds.filter( 176 | (id) => overlayData[id].isOpen 177 | ); 178 | 179 | return ( 180 |
181 | 182 | 198 | 199 | 216 | 217 |
218 | 열려 있는 오버레이 219 | 220 | {openOverlayIds.length > 0 ? ( 221 | openOverlayIds.map((id) => {id}) 222 | ) : ( 223 | 없음 224 | )} 225 | 226 | 227 | 228 | 메모리에 남아 있는 오버레이 229 | 230 | 231 | {allOverlayIds.length > 0 ? ( 232 | allOverlayIds.map((id) => {id}) 233 | ) : ( 234 | 없음 235 | )} 236 | 237 |
238 |
239 | ); 240 | } 241 | 242 | export const Example = () => { 243 | return ( 244 | 245 | 246 | 247 | ); 248 | }; 249 | 250 | ``` 251 | 252 | ```tsx confirm-dialog.tsx 253 | import Button from '@mui/material/Button'; 254 | import Dialog from '@mui/material/Dialog'; 255 | import DialogTitle from '@mui/material/DialogTitle'; 256 | import DialogContent from '@mui/material/DialogContent'; 257 | import DialogActions from '@mui/material/DialogActions'; 258 | import Typography from '@mui/material/Typography'; 259 | 260 | export function ConfirmDialog({ isOpen, close, title }) { 261 | return ( 262 | 263 | {title} 264 | 265 | {title} 내용입니다. 266 | 267 | 268 | 269 | 270 | 271 | ); 272 | } 273 | 274 | ``` 275 | 276 |
277 | 278 | --- 279 | 280 | 281 | 282 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/guides/think-in-overlay-kit.mdx: -------------------------------------------------------------------------------- 1 | # overlay-kit으로 생각하기 2 | 3 | React 철학을 기반으로 탄생한 `overlay-kit`과 이를 구체화한 선언적 오버레이 패턴(Declarative Overlay Pattern)에 대해 알아볼게요. 4 | 5 | ## overlay-kit을 사용하는 이유 6 | 7 | ### 기존 오버레이 관리의 문제점 8 | 9 | 1. **상태 관리의 복잡성** 10 | 11 | - `useState`나 전역 상태를 사용해 직접 오버레이 상태를 관리해야 했어요. 12 | - 상태 관리와 UI 로직이 섞여 코드가 복잡해지고 가독성이 떨어졌어요. 13 | 14 | 2. **이벤트 핸들링의 반복** 15 | 16 | - 열기, 닫기, 결과 반환 같은 이벤트 핸들링 코드를 반복해서 작성해야 했어요. 17 | - 이는 중복 코드를 유발하고 개발 경험을 저하시키는 주요 원인이 되었어요. 18 | 19 | 3. **재사용성 부족** 20 | 21 | - 오버레이에서 값을 반환하려면 callback 함수 등으로 UI와 로직이 강하게 결합되었어요. 22 | - 이로 인해 컴포넌트를 재사용하기 어려웠어요. 23 | 24 | ### overlay-kit의 목표 25 | 26 | 1. **React 철학을 따르는 설계** 27 | 28 | - React는 선언적인 코드를 지향해요. 29 | - `overlay-kit`은 오버레이를 선언적으로 관리할 수 있게 도와줘요. 30 | 31 | 2. **개발 생산성 향상** 32 | 33 | - 상태 관리와 이벤트 핸들링을 캡슐화해, 개발자는 UI와 비즈니스 로직에만 집중할 수 있어요. 34 | 35 | 3. **확장성과 재사용성 강화** 36 | 37 | - UI와 동작을 분리하고, Promise를 반환하는 방식으로 오버레이의 재사용성을 높였어요. 38 | 39 | ## Declarative Overlay Pattern 40 | 41 | 기존의 오버레이 관리 방식은 **명령형(Imperative)** 접근을 사용했어요. 42 | `useState`를 사용한 상태 관리와 이벤트 핸들링 코드가 섞여 있어 코드가 복잡하고 유지보수하기 어려웠죠. 43 | 44 | **Declarative Overlay Pattern**은 오버레이를 **상태가 아닌 동작(Behavior)** 중심으로 관리해 더 직관적이고 선언적인 코드를 작성할 수 있도록 해요. 45 | 46 | #### 명령형 접근 방식 47 | 48 | 상태 관리와 이벤트 핸들링이 결합되어 중복 코드가 많고, 가독성이 떨어져요. 49 | 50 | ```tsx 51 | import { useState } from 'react'; 52 | 53 | function Overlay() { 54 | const [isOpen, setIsOpen] = useState(false); 55 | 56 | function handleOpen() { 57 | setIsOpen(true); 58 | } 59 | 60 | function handleClose() { 61 | setIsOpen(false); 62 | } 63 | 64 | return ( 65 | <> 66 | 67 | 68 | 명령형 오버레이 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | ``` 78 | 79 | #### 선언적 접근 방식 80 | 81 | 상태 관리 코드를 제거하고, UI와 동작을 분리해 가독성과 유지보수성이 향상돼요. 82 | 83 | ```tsx 84 | import { overlay } from 'overlay-kit'; 85 | 86 | function Overlay() { 87 | return ( 88 | 95 | 96 | 97 |
98 | )); 99 | }} 100 | > 101 | 열기 102 | 103 | ); 104 | } 105 | ``` 106 | 107 | ## 핵심 원칙 108 | 109 | overlay-kit의 설계를 이해하려면 다음 원칙을 알아야 해요. 110 | 111 | ### 코로케이션(Co-location) 112 | 113 | 연관된 코드를 가까이 배치해 코드를 쉽게 이해할 수 있도록 해요. 114 | 115 | 오버레이 호출, 상태 관리, 컴포넌트 정의를 한곳에서 처리할 수 있어 가독성이 좋아져요. 116 | 117 | ### 최소한의 API 118 | 119 | overlay-kit은 배우기 쉽고 간결한 API를 제공합니다. 핵심 API는 단 두 가지예요. 120 | 121 | 1. `overlay.open`: 오버레이를 열고 닫는 기능을 제공해요. 122 | 2. `overlay.openAsync`: 값을 반환해 비동기 로직을 처리할 수 있어요. 123 | 124 | 이 두 API는 범용적인 JavaScript 패턴을 활용해 다양한 오버레이를 구현할 수 있도록 설계됐어요. 125 | 126 | 예를 들어, overlay.openAsync는 Promise 값을 반환하므로 체이닝 같은 패턴을 쉽게 적용할 수 있어요. 127 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/more/_meta.tsx: -------------------------------------------------------------------------------- 1 | export default { 2 | basic: { 3 | title: '오버레이 열기', 4 | }, 5 | 'open-outside-react': { 6 | title: 'React 외부에서 오버레이 열기', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/more/basic.mdx: -------------------------------------------------------------------------------- 1 | import { Sandpack } from '@/components'; 2 | 3 | # 오버레이 열기 4 | 5 | ## 간단한 오버레이 열기 6 | 7 | `overlay.open`을 사용하면 오버레이를 간단하게 열고 닫을 수 있어요. 8 | 9 |
10 | 11 | 12 | 13 | ```tsx Example.tsx active 14 | import { OverlayProvider, overlay } from 'overlay-kit'; 15 | import Button from '@mui/material/Button'; 16 | import Dialog from '@mui/material/Dialog'; 17 | import DialogActions from '@mui/material/DialogActions'; 18 | import DialogContent from '@mui/material/DialogContent'; 19 | import DialogContentText from '@mui/material/DialogContentText'; 20 | import DialogTitle from '@mui/material/DialogTitle'; 21 | 22 | function App() { 23 | return ( 24 | 32 | 33 | 34 |
35 | ); 36 | }); 37 | }} 38 | > 39 | Confirm Dialog 열기 40 | 41 | ); 42 | } 43 | 44 | export function Example() { 45 | return ( 46 | 47 | 48 | 49 | ); 50 | }; 51 | ``` 52 | 53 | 54 | 55 | ## 비동기 오버레이 열기 56 | 57 | `overlay.openAsync`를 사용하면 오버레이의 결과를 `Promise`로 처리할 수 있어요. 58 | 59 |
60 | 61 | 62 | 63 | ```tsx Example.tsx active 64 | import { useState } from 'react'; 65 | import { OverlayProvider, overlay } from 'overlay-kit'; 66 | import Button from '@mui/material/Button'; 67 | import Dialog from '@mui/material/Dialog'; 68 | import DialogActions from '@mui/material/DialogActions'; 69 | import DialogContent from '@mui/material/DialogContent'; 70 | import DialogContentText from '@mui/material/DialogContentText'; 71 | import DialogTitle from '@mui/material/DialogTitle'; 72 | 73 | function App() { 74 | const [result, setResult] = useState(); 75 | 76 | return ( 77 |
78 | 86 | 87 | 88 | 89 | ); 90 | }); 91 | 92 | setResult(result); 93 | }} 94 | > 95 | Confirm Dialog 열기 96 | 97 |

result: {result ? 'Y' : 'N'}

98 |
99 | ); 100 | } 101 | 102 | export function Example() { 103 | return ( 104 | 105 | 106 | 107 | ); 108 | }; 109 | ``` 110 | 111 |
112 | 113 | ## 오버레이 메모리 해제하기 114 | 115 | `unmount`를 사용하면 오버레이를 메모리에서 완전히 제거할 수 있어요. 116 | 117 | 하지만 닫기 애니메이션이 있는 경우, `unmount`를 바로 호출하면 애니메이션이 보이지 않을 수 있어요. 118 | 이럴 때는 `close`를 호출한 후, 애니메이션이 완료된 뒤 `unmount`를 실행하세요. 119 | 120 | ### `onExit` prop 사용하기 121 | 122 | 오버레이가 닫히는 애니메이션이 종료되었다는 `onExit` prop을 구현하면 애니메이션이 끝난 직후 오버레이를 제거할 수 있어요. 123 | 124 |
125 | 126 | 127 | 128 | ```tsx Example.tsx active 129 | import { OverlayProvider, overlay } from 'overlay-kit'; 130 | import Button from '@mui/material/Button'; 131 | import { ConfirmDialog } from './confirm-dialog'; 132 | 133 | function App() { 134 | return ( 135 | <> 136 | 145 | 146 | ); 147 | } 148 | 149 | export function Example() { 150 | return ( 151 | 152 | 153 | 154 | ); 155 | }; 156 | ``` 157 | 158 | ```tsx confirm-dialog.tsx 159 | import { useState, useEffect } from 'react'; 160 | import Button from '@mui/material/Button'; 161 | import Dialog from '@mui/material/Dialog'; 162 | import DialogTitle from '@mui/material/DialogTitle'; 163 | import DialogContent from '@mui/material/DialogContent'; 164 | import DialogActions from '@mui/material/DialogActions'; 165 | 166 | export function ConfirmDialog({ isOpen, close, onExit }) { 167 | const [count, setCount] = useState(0); 168 | 169 | useEffect(() => { 170 | return () => onExit(); 171 | }, []); 172 | 173 | return ( 174 | 175 | 정말로 계속하시겠어요? 176 | 177 | 178 |

count: {count}

179 | 180 |
181 | 182 | 183 | 184 | 185 | 186 |
187 | ); 188 | } 189 | ``` 190 | 191 |
192 | 193 | ### `setTimeout` 사용하기 194 | 195 | `onExit` prop이 없다면, `setTimeout`을 사용해서 애니메이션이 종료된 후에 오버레이를 제거할 수 있어요. 196 | 애니메이션 지속 시간에 맞춰 적절한 시간을 설정하세요. 197 | 198 | 다음 코드에서 `close` 함수는 오버레이를 닫고, `setTimeout`을 사용해 600ms 후에 `unmount` 함수를 호출해요. 199 | 200 |
201 | 202 | 203 | 204 | ```tsx Example.tsx active 205 | import { OverlayProvider, overlay } from 'overlay-kit'; 206 | import Button from '@mui/material/Button'; 207 | import { ConfirmDialog } from './confirm-dialog'; 208 | 209 | function App() { 210 | return ( 211 | <> 212 | 231 | 232 | ); 233 | } 234 | 235 | export function Example() { 236 | return ( 237 | 238 | 239 | 240 | ); 241 | }; 242 | ``` 243 | 244 | ```tsx confirm-dialog.tsx 245 | import { useState, useEffect } from 'react'; 246 | import Button from '@mui/material/Button'; 247 | import Dialog from '@mui/material/Dialog'; 248 | import DialogTitle from '@mui/material/DialogTitle'; 249 | import DialogContent from '@mui/material/DialogContent'; 250 | import DialogActions from '@mui/material/DialogActions'; 251 | 252 | export function ConfirmDialog({ isOpen, close }) { 253 | const [count, setCount] = useState(0); 254 | 255 | return ( 256 | 257 | 정말로 계속하시겠어요? 258 | 259 | 260 |

count: {count}

261 | 262 |
263 | 264 | 265 | 266 | 267 | 268 |
269 | ); 270 | } 271 | ``` 272 | 273 |
274 | -------------------------------------------------------------------------------- /docs/src/pages/ko/docs/more/open-outside-react.mdx: -------------------------------------------------------------------------------- 1 | import { Sandpack } from '@/components'; 2 | 3 | # React 외부에서 오버레이 열기 4 | 5 | `overlay-kit`을 사용하면 React 컴포넌트 바깥에서도 오버레이를 열 수 있어요. 6 | 7 | 예를 들어, API 호출 중 오류가 발생했을 때 오버레이를 띄워 사용자에게 알릴 수 있어요. 8 | 9 | ```tsx {7-11} 10 | import ky from 'ky'; 11 | import { overlay } from 'overlay-kit'; 12 | 13 | const api = ky.extend({ 14 | hooks: { 15 | afterResponse: [ 16 | (_, __, response) => { 17 | console.log('test:: response', response); 18 | if (response.status >= 400) { 19 | overlay.open(({ isOpen, close }) => ); 20 | } 21 | }, 22 | ], 23 | }, 24 | }); 25 | ``` 26 | 27 | 위 코드는 `ky`를 확장해서 API 호출 이후 상태 코드를 확인한 뒤, 오류가 있을 경우 오버레이를 여는 방식이에요. 28 | 29 | ## 전체 예제: API 요청 후 오버레이 열기 30 | 31 | 다음은 API 요청 이후 오버레이를 띄우는 전체 예제에요. `overlay.open`을 사용해 API를 성공적으로 받았는지 알려줘요. 32 | 33 |
34 | 35 | 36 | 37 | ```tsx Example.tsx active 38 | import ky from 'ky'; 39 | import { OverlayProvider, overlay } from 'overlay-kit'; 40 | import Button from '@mui/material/Button'; 41 | import { Alert } from './alert'; 42 | 43 | const api = ky.extend({ 44 | hooks: { 45 | afterResponse: [ 46 | (_, __, response) => { 47 | overlay.open(({ isOpen, close, unmount }) => { 48 | return ; 49 | }); 50 | }, 51 | ], 52 | }, 53 | }); 54 | 55 | function App() { 56 | return ; 57 | } 58 | 59 | export function Example() { 60 | return ( 61 | 62 | 63 | 64 | ); 65 | }; 66 | ``` 67 | 68 | ```tsx alert.tsx 69 | import { useEffect } from 'react'; 70 | import Button from '@mui/material/Button'; 71 | import Dialog from '@mui/material/Dialog'; 72 | import DialogTitle from '@mui/material/DialogTitle'; 73 | import DialogActions from '@mui/material/DialogActions'; 74 | 75 | export function Alert({ isOpen, close, onExit }) { 76 | useEffect(() => { 77 | return () => onExit(); 78 | }, []); 79 | 80 | return ( 81 | 82 | API 응답을 받았어요 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | ``` 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/src/pages/ko/index.mdx: -------------------------------------------------------------------------------- 1 | import { Main } from '@/components'; 2 | 3 |
23 | -------------------------------------------------------------------------------- /docs/theme.config.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'motion/react'; 2 | import { useRouter } from 'nextra/hooks'; 3 | import { useConfig, type DocsThemeConfig } from 'nextra-theme-docs'; 4 | 5 | const config: DocsThemeConfig = { 6 | logo: () => { 7 | const router = useRouter(); 8 | if (router.pathname === '/ko' || router.pathname === '/en') { 9 | return <>; 10 | } 11 | return ( 12 | 13 | overlay-kit 14 | 15 | ); 16 | }, 17 | head: function Head() { 18 | const config = useConfig<{ description?: string }>(); 19 | const { asPath, defaultLocale, locale } = useRouter(); 20 | 21 | const title = config.title !== 'Index' ? `${config.title} - overlay-kit` : 'overlay-kit'; 22 | const description = config.frontMatter.description ?? 'A library for handling overlays more easily in React'; 23 | const url = 'https://overlay-kit.slash.page' + (defaultLocale === locale ? asPath : `/${locale}${asPath}`); 24 | 25 | return ( 26 | <> 27 | 28 | 29 | 30 | 34 | {title} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }, 44 | main: function Main({ children }: { children: React.ReactNode }) { 45 | const router = useRouter(); 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | }, 53 | footer: { 54 | content: `MIT ${new Date().getFullYear()} © Viva Republica, Inc.`, 55 | }, 56 | project: { 57 | link: 'https://github.com/toss/overlay-kit', 58 | }, 59 | chat: { 60 | link: 'https://discord.gg/vGXbVjP2nY', 61 | }, 62 | docsRepositoryBase: 'https://github.com/toss/overlay-kit/tree/main/docs', 63 | i18n: [ 64 | { locale: 'en', name: 'English' }, 65 | { locale: 'ko', name: '한국어' }, 66 | ], 67 | search: { 68 | placeholder: function Placeholder() { 69 | const router = useRouter(); 70 | 71 | if (router.locale === 'ko') { 72 | return '검색어를 입력하세요...'; 73 | } 74 | 75 | return 'Search documentation...'; 76 | }, 77 | }, 78 | }; 79 | 80 | export default config; 81 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES2017", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "./theme.config.tsx"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const eslint = require('@eslint/js'); 4 | const tseslint = require('typescript-eslint'); 5 | const importPlugin = require('eslint-plugin-import'); 6 | const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended'); 7 | const unicorn = require('eslint-plugin-unicorn'); 8 | const unusedImports = require('eslint-plugin-unused-imports'); 9 | 10 | module.exports = tseslint.config( 11 | { 12 | ignores: ['**/.vitepress/', '**/dist/', '**/esm/', '**/.next/', '**/.next-local/', '.pnp.*', '.yarn/'], 13 | }, 14 | eslint.configs.recommended, 15 | { 16 | plugins: { unicorn }, 17 | rules: { 18 | 'unicorn/filename-case': [ 19 | 'error', 20 | { 21 | case: 'kebabCase', 22 | }, 23 | ], 24 | }, 25 | }, 26 | { 27 | rules: { 28 | 'no-implicit-coercion': 'error', 29 | 'no-warning-comments': [ 30 | 'warn', 31 | { 32 | terms: ['TODO', 'FIXME', 'XXX', 'BUG'], 33 | location: 'anywhere', 34 | }, 35 | ], 36 | curly: ['error', 'all'], 37 | eqeqeq: ['error', 'always', { null: 'ignore' }], 38 | // TypeScript에서 이미 잡고 있는 문제이기 때문에 + location, document 등의 global variable도 잡아서 39 | 'no-undef': 'off', 40 | }, 41 | }, 42 | { 43 | plugins: { 44 | 'unused-imports': unusedImports, 45 | }, 46 | rules: { 47 | 'unused-imports/no-unused-imports': 'error', 48 | 'unused-imports/no-unused-vars': [ 49 | 'error', 50 | { 51 | vars: 'all', 52 | varsIgnorePattern: '^_', 53 | args: 'after-used', 54 | argsIgnorePattern: '^_', 55 | }, 56 | ], 57 | }, 58 | }, 59 | { 60 | plugins: { 61 | '@typescript-eslint': tseslint.plugin, 62 | }, 63 | extends: [...tseslint.configs.recommended], 64 | languageOptions: { 65 | parser: tseslint.parser, 66 | parserOptions: { 67 | ecmaFeatures: { jsx: true }, 68 | }, 69 | }, 70 | rules: { 71 | '@typescript-eslint/consistent-type-imports': [ 72 | 'error', 73 | { 74 | fixStyle: 'inline-type-imports', 75 | }, 76 | ], 77 | '@typescript-eslint/explicit-function-return-type': 'off', 78 | '@typescript-eslint/no-var-requires': 'warn', 79 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'warn', 80 | '@typescript-eslint/no-inferrable-types': 'warn', 81 | '@typescript-eslint/no-empty-function': 'off', 82 | '@typescript-eslint/naming-convention': [ 83 | 'error', 84 | { format: ['camelCase', 'UPPER_CASE', 'PascalCase'], selector: 'variable', leadingUnderscore: 'allow' }, 85 | { format: ['camelCase', 'PascalCase'], selector: 'function' }, 86 | { format: ['PascalCase'], selector: 'interface' }, 87 | { format: ['PascalCase'], selector: 'typeAlias' }, 88 | ], 89 | '@typescript-eslint/explicit-module-boundary-types': 'off', 90 | '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], 91 | '@typescript-eslint/member-ordering': [ 92 | 'error', 93 | { 94 | default: [ 95 | 'public-static-field', 96 | 'private-static-field', 97 | 'public-instance-field', 98 | 'private-instance-field', 99 | 'public-constructor', 100 | 'private-constructor', 101 | 'public-instance-method', 102 | 'private-instance-method', 103 | ], 104 | }, 105 | ], 106 | }, 107 | }, 108 | { 109 | plugins: { 110 | import: importPlugin, 111 | }, 112 | rules: { 113 | 'import/no-duplicates': ['error', { 'prefer-inline': true }], 114 | 'import/order': [ 115 | 2, 116 | { 117 | groups: ['builtin', 'external', ['parent', 'sibling'], 'index'], 118 | alphabetize: { 119 | order: 'asc', 120 | caseInsensitive: false, 121 | }, 122 | }, 123 | ], 124 | }, 125 | }, 126 | eslintPluginPrettierRecommended 127 | ); 128 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@overlay-kit/framer-motion-react-16", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "predev": "yarn workspace overlay-kit build", 8 | "dev": "yarn predev && vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "framer-motion": "^6", 15 | "overlay-kit": "workspace:*", 16 | "react": "^16.8", 17 | "react-dom": "^16.8" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^16.8", 21 | "@types/react-dom": "^16.8", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "^5.4.5", 24 | "vite": "^6.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion, type Variants } from 'framer-motion'; 2 | import { useRef, type PropsWithChildren } from 'react'; 3 | 4 | type ModalProps = { 5 | isOpen?: boolean; 6 | onExit?: () => void; 7 | }; 8 | 9 | export function Modal({ children, isOpen = false, onExit }: PropsWithChildren) { 10 | const prevIsOpenRef = useRef(isOpen); 11 | 12 | if (isOpen !== prevIsOpenRef.current) { 13 | prevIsOpenRef.current = isOpen; 14 | 15 | if (prevIsOpenRef.current === false) { 16 | setTimeout(() => onExit?.(), 300); 17 | } 18 | } 19 | 20 | return ( 21 | {isOpen === true && {children}} 22 | ); 23 | } 24 | 25 | const MODAL_CONTENT_VARIANTS: Variants = { 26 | hidden: { opacity: 0, scale: 0.75 }, 27 | show: { opacity: 1, scale: 1 }, 28 | }; 29 | 30 | function ModalContent({ children, isOpen }: PropsWithChildren) { 31 | return ( 32 |
45 | 58 | {children} 59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/src/demo.tsx: -------------------------------------------------------------------------------- 1 | import { overlay } from 'overlay-kit'; 2 | import { useState } from 'react'; 3 | import { Modal } from './components/modal.tsx'; 4 | 5 | export function Demo() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | function DemoWithState() { 15 | const [isOpen, setIsOpen] = useState(false); 16 | 17 | return ( 18 |
19 |

Demo with useState

20 | 21 | 22 |
23 |

MODAL CONTENT

24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | function DemoWithEsOverlay() { 32 | return ( 33 |
34 |

Demo with overlay-kit

35 | 45 |
46 | 47 | ); 48 | }); 49 | }} 50 | > 51 | open modal 52 | 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayProvider } from 'overlay-kit'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Demo } from './demo.tsx'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | 11 | , 12 | document.getElementById('root')! 13 | ); 14 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-16/framer-motion/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@overlay-kit/framer-motion-react-17", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "predev": "yarn workspace overlay-kit build", 8 | "dev": "yarn predev && vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "framer-motion": "^6", 15 | "overlay-kit": "workspace:*", 16 | "react": "^17", 17 | "react-dom": "^17" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^17", 21 | "@types/react-dom": "^17", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "^5.4.5", 24 | "vite": "^6.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion, type Variants } from 'framer-motion'; 2 | import { useRef, type PropsWithChildren } from 'react'; 3 | 4 | type ModalProps = { 5 | isOpen?: boolean; 6 | onExit?: () => void; 7 | }; 8 | 9 | export function Modal({ children, isOpen = false, onExit }: PropsWithChildren) { 10 | const prevIsOpenRef = useRef(isOpen); 11 | 12 | if (isOpen !== prevIsOpenRef.current) { 13 | prevIsOpenRef.current = isOpen; 14 | 15 | if (prevIsOpenRef.current === false) { 16 | setTimeout(() => onExit?.(), 300); 17 | } 18 | } 19 | 20 | return ( 21 | {isOpen === true && {children}} 22 | ); 23 | } 24 | 25 | const MODAL_CONTENT_VARIANTS: Variants = { 26 | hidden: { opacity: 0, scale: 0.75 }, 27 | show: { opacity: 1, scale: 1 }, 28 | }; 29 | 30 | function ModalContent({ children, isOpen }: PropsWithChildren) { 31 | return ( 32 |
45 | 58 | {children} 59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/src/demo.tsx: -------------------------------------------------------------------------------- 1 | import { overlay } from 'overlay-kit'; 2 | import { useState } from 'react'; 3 | import { Modal } from './components/modal.tsx'; 4 | 5 | export function Demo() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | function DemoWithState() { 15 | const [isOpen, setIsOpen] = useState(false); 16 | 17 | return ( 18 |
19 |

Demo with useState

20 | 21 | 22 |
23 |

MODAL CONTENT

24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | function DemoWithEsOverlay() { 32 | return ( 33 |
34 |

Demo with overlay-kit

35 | 45 |
46 | 47 | ); 48 | }); 49 | }} 50 | > 51 | open modal 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayProvider } from 'overlay-kit'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Demo } from './demo.tsx'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | 10 | 11 | , 12 | document.getElementById('root')! 13 | ); 14 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-17/framer-motion/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@overlay-kit/framer-motion-react-18", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "predev": "yarn workspace overlay-kit build", 8 | "dev": "yarn predev && vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "framer-motion": "^11.2.10", 15 | "overlay-kit": "workspace:*", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.2.0", 21 | "@types/react-dom": "^18.2.0", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "^5.4.5", 24 | "vite": "^6.0.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion, type Variants } from 'framer-motion'; 2 | import { useRef, type PropsWithChildren } from 'react'; 3 | 4 | type ModalProps = { 5 | isOpen?: boolean; 6 | onExit?: () => void; 7 | }; 8 | 9 | export function Modal({ children, isOpen = false, onExit }: PropsWithChildren) { 10 | const prevIsOpenRef = useRef(isOpen); 11 | 12 | if (isOpen !== prevIsOpenRef.current) { 13 | prevIsOpenRef.current = isOpen; 14 | 15 | if (prevIsOpenRef.current === false) { 16 | setTimeout(() => onExit?.(), 300); 17 | } 18 | } 19 | 20 | return ( 21 | {isOpen === true && {children}} 22 | ); 23 | } 24 | 25 | const MODAL_CONTENT_VARIANTS: Variants = { 26 | hidden: { opacity: 0, scale: 0.75 }, 27 | show: { opacity: 1, scale: 1 }, 28 | }; 29 | 30 | function ModalContent({ children, isOpen }: PropsWithChildren) { 31 | return ( 32 |
45 | 58 | {children} 59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/src/demo.tsx: -------------------------------------------------------------------------------- 1 | import { overlay } from 'overlay-kit'; 2 | import { useState } from 'react'; 3 | import { Modal } from './components/modal.tsx'; 4 | 5 | export function Demo() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | function DemoWithState() { 15 | const [isOpen, setIsOpen] = useState(false); 16 | 17 | return ( 18 |
19 |

Demo with useState

20 | 21 | 22 |
23 |

MODAL CONTENT

24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | function DemoWithEsOverlay() { 32 | return ( 33 |
34 |

Demo with overlay-kit

35 | 45 |
46 | 47 | ); 48 | }); 49 | }} 50 | > 51 | open modal 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayProvider } from 'overlay-kit'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { Demo } from './demo.tsx'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-18/framer-motion/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@overlay-kit/framer-motion-react-19", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "predev": "yarn workspace overlay-kit build", 8 | "dev": "yarn predev && vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "framer-motion": "^11.14.1", 15 | "overlay-kit": "workspace:^", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^19.0.1", 21 | "@types/react-dom": "^19.0.2", 22 | "@vitejs/plugin-react": "^4.3.4", 23 | "typescript": "^5.4.5", 24 | "vite": "^5.2.13" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/src/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion, type Variants } from 'framer-motion'; 2 | import { useRef, type PropsWithChildren } from 'react'; 3 | 4 | type ModalProps = { 5 | isOpen?: boolean; 6 | onExit?: () => void; 7 | }; 8 | 9 | export function Modal({ children, isOpen = false, onExit }: PropsWithChildren) { 10 | const prevIsOpenRef = useRef(isOpen); 11 | 12 | if (isOpen !== prevIsOpenRef.current) { 13 | prevIsOpenRef.current = isOpen; 14 | 15 | if (prevIsOpenRef.current === false) { 16 | setTimeout(() => onExit?.(), 300); 17 | } 18 | } 19 | 20 | return ( 21 | {isOpen === true && {children}} 22 | ); 23 | } 24 | 25 | const MODAL_CONTENT_VARIANTS: Variants = { 26 | hidden: { opacity: 0, scale: 0.75 }, 27 | show: { opacity: 1, scale: 1 }, 28 | }; 29 | 30 | function ModalContent({ children, isOpen }: PropsWithChildren) { 31 | return ( 32 |
45 | 58 | {children} 59 | 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/src/demo.tsx: -------------------------------------------------------------------------------- 1 | import { overlay } from 'overlay-kit'; 2 | import { useState } from 'react'; 3 | import { Modal } from './components/modal.tsx'; 4 | 5 | export function Demo() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | function DemoWithState() { 15 | const [isOpen, setIsOpen] = useState(false); 16 | 17 | return ( 18 |
19 |

Demo with useState

20 | 21 | 22 |
23 |

MODAL CONTENT

24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | function DemoWithEsOverlay() { 32 | return ( 33 |
34 |

Demo with overlay-kit

35 | 45 |
46 | 47 | ); 48 | }); 49 | }} 50 | > 51 | open modal 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { OverlayProvider } from 'overlay-kit'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { Demo } from './demo.tsx'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/react-19/framer-motion/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "packageManager": "yarn@4.5.1", 5 | "workspaces": [ 6 | ".", 7 | "packages", 8 | "docs", 9 | "docs-old", 10 | "examples/**/**" 11 | ], 12 | "scripts": { 13 | "lint": "eslint .", 14 | "changeset:version": "yarn changeset version", 15 | "changeset:publish": "yarn changeset publish" 16 | }, 17 | "devDependencies": { 18 | "@changesets/changelog-github": "^0.5.0", 19 | "@changesets/cli": "^2.27.7", 20 | "@eslint/js": "^9.4.0", 21 | "@types/eslint__js": "^8.42.3", 22 | "eslint": "^8.57.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-import": "^2.29.1", 25 | "eslint-plugin-prettier": "^5.1.3", 26 | "eslint-plugin-unicorn": "^53.0.0", 27 | "eslint-plugin-unused-imports": "^3.1.0", 28 | "prettier": "^3.2.5", 29 | "typescript": "^5.4.5", 30 | "typescript-eslint": "^7.12.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | -------------------------------------------------------------------------------- /packages/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # overlay-kit 2 | 3 | ## 1.8.1 4 | 5 | ### Patch Changes 6 | 7 | - [#161](https://github.com/toss/overlay-kit/pull/161) [`ec07614`](https://github.com/toss/overlay-kit/commit/ec0761404eabe27b07a8ad31cf82df34fb3169f7) Thanks [@jungpaeng](https://github.com/jungpaeng)! - feat: Change to allow each overlay provider to have a unique event ID 8 | 9 | Each overlay provider now uses a unique event ID instead of relying on shared global identifiers. 10 | 11 | This change improves event handling accuracy and avoids potential collisions when managing multiple overlays from different providers. 12 | It is backward-compatible for existing overlays that do not explicitly set a provider-specific ID. 13 | 14 | ## 1.8.0 15 | 16 | ### Minor Changes 17 | 18 | - [#149](https://github.com/toss/overlay-kit/pull/149) [`a98a312`](https://github.com/toss/overlay-kit/commit/a98a312249b5ad0006eec16025c4109714a47265) Thanks [@jungpaeng](https://github.com/jungpaeng)! - refactor: migrate event based store 19 | 20 | ### Patch Changes 21 | 22 | - [#151](https://github.com/toss/overlay-kit/pull/151) [`07f42a5`](https://github.com/toss/overlay-kit/commit/07f42a585e8d2bd8ee6ee0841388e3dc39a287a4) Thanks [@jungpaeng](https://github.com/jungpaeng)! - feat: Add component key 23 | 24 | Fixed an issue with overlay components not properly remounting when unmounted and immediately reopened with the same ID. 25 | Added a new `componentKey` property that's separate from `overlayId` to ensure React properly handles component lifecycle. Each time `overlay.open()` is called, a new random `componentKey` is generated internally even when reusing the same `overlayId`. 26 | 27 | This fix resolves scenarios where calling `unmount()` followed by `open()` with the same overlay ID in quick succession would result in the overlay not being visible to users. 28 | 29 | ## 1.7.0 30 | 31 | ### Minor Changes 32 | 33 | - [`8c4d54d`](https://github.com/toss/overlay-kit/commit/8c4d54da25e2609f79be388afeb3b5a40b8d93f5) Thanks [@jungpaeng](https://github.com/jungpaeng)! - feat: export types from content-overlay-controller 34 | 35 | Export `OverlayControllerComponent` and `OverlayAsyncControllerComponent` types from content-overlay-controller module for better type accessibility in consumer projects. 36 | 37 | ## 1.6.2 38 | 39 | ### Patch Changes 40 | 41 | - [#120](https://github.com/toss/overlay-kit/pull/120) [`3362bb7`](https://github.com/toss/overlay-kit/commit/3362bb7560a1a53e1b8d290a4fc1e849f607f730) Thanks [@ho991217](https://github.com/ho991217)! - fix: Enhance overlay reducer state management 42 | 43 | - [#123](https://github.com/toss/overlay-kit/pull/123) [`d02a36c`](https://github.com/toss/overlay-kit/commit/d02a36c3adb711144568fae19b96db52b17dc71d) Thanks [@yongsk0066](https://github.com/yongsk0066)! - fix: remove console log on dispatchOverlay 44 | 45 | - [#119](https://github.com/toss/overlay-kit/pull/119) [`a45f618`](https://github.com/toss/overlay-kit/commit/a45f6181294cab767569ad257c1901767f2d13aa) Thanks [@ho991217](https://github.com/ho991217)! - fix: initializing current overlay after close all overlays 46 | 47 | ## 1.6.1 48 | 49 | ### Patch Changes 50 | 51 | - [#113](https://github.com/toss/overlay-kit/pull/113) [`b57d15b`](https://github.com/toss/overlay-kit/commit/b57d15ba9b64c05d50224a09bd116266109d886c) Thanks [@jungpaeng](https://github.com/jungpaeng)! - Improve overlay unmount logic and add test cases 52 | 53 | - Enhanced current overlay state management during unmount 54 | - When unmounting a middle overlay with multiple overlays open, the last overlay becomes current 55 | - When unmounting the last overlay, the previous overlay becomes current 56 | - Added test cases 57 | - Test for unmounting multiple overlays in different orders 58 | - Test for tracking current overlay state using useCurrentOverlay hook 59 | 60 | ## 1.6.0 61 | 62 | ### Minor Changes 63 | 64 | - [#102](https://github.com/toss/overlay-kit/pull/102) [`becbd90`](https://github.com/toss/overlay-kit/commit/becbd90fa111419c3bcf4088edebc6ce743fdf40) Thanks [@jungpaeng](https://github.com/jungpaeng)! - feat: Add local overlay context support 65 | 66 | - Add `experimental_createOverlayContext` function to create isolated overlay contexts 67 | - Refactor context management to support multiple overlay instances 68 | - Move overlay provider and controller logic into separate files 69 | - Update store management to support local state 70 | - Add documentation for new context creation API 71 | - Improve type definitions and exports 72 | 73 | ## 1.5.0 74 | 75 | ### Minor Changes 76 | 77 | - [#95](https://github.com/toss/overlay-kit/pull/95) [`59f2917`](https://github.com/toss/overlay-kit/commit/59f29179fd61de0d64df94d54cd7110c9cc0e47c) Thanks [@manudeli](https://github.com/manudeli)! - feat: support react 19 78 | 79 | ### Patch Changes 80 | 81 | - [#97](https://github.com/toss/overlay-kit/pull/97) [`a04c030`](https://github.com/toss/overlay-kit/commit/a04c03075bafc8192487c8cc1b837aaf73991760) Thanks [@manudeli](https://github.com/manudeli)! - feat: esm first (support cjs by exports field of package.json) 82 | 83 | - [#98](https://github.com/toss/overlay-kit/pull/98) [`725264a`](https://github.com/toss/overlay-kit/commit/725264abad4813bd33eefb559e869caaa329c33c) Thanks [@manudeli](https://github.com/manudeli)! - fix: update pkg.repository.url 84 | 85 | ## 1.4.1 86 | 87 | ### Patch Changes 88 | 89 | - [#74](https://github.com/toss/overlay-kit/pull/74) [`324dab9`](https://github.com/toss/overlay-kit/commit/324dab92b9bdda007930a4f4e731257b053e5156) Thanks [@jungpaeng](https://github.com/jungpaeng)! - Fix path resolution error by updating import path for 'use-sync-external-store/shim' 90 | 91 | The import path for `use-sync-external-store/shim` was incorrect, causing a path resolution error during build. This change updates the import statement to include `index.js`, resolving the path issue. 92 | 93 | ## 1.4.0 94 | 95 | ### Minor Changes 96 | 97 | - [#72](https://github.com/toss/overlay-kit/pull/72) [`9776fff`](https://github.com/toss/overlay-kit/commit/9776fff2bccc683afb9dfdfa7ad0b568cd902b7d) Thanks [@jungpaeng](https://github.com/jungpaeng)! - Support for React versions 16.8 and 17 98 | 99 | **Related Issue:** Fixes #43 100 | 101 | ### Patch Changes 102 | 103 | - [#64](https://github.com/toss/overlay-kit/pull/64) [`01eaa3c`](https://github.com/toss/overlay-kit/commit/01eaa3c41e367224852cad56bc0214f1bf05ff77) Thanks [@jungpaeng](https://github.com/jungpaeng)! - feat: Add cleanup effect for unmounting 104 | 105 | This commit introduces a useEffect cleanup function in the OverlayProvider component that dispatches a 'REMOVE_ALL' action when the component unmounts. 106 | This change ensures that all overlays are properly cleaned up during testing scenarios, preventing state leakage and side effects from persistent overlays. 107 | 108 | **Related Issue:** Fixes #63 109 | 110 | ## 1.3.0 111 | 112 | ### Minor Changes 113 | 114 | - [#59](https://github.com/toss/overlay-kit/pull/59) [`828fad5`](https://github.com/toss/overlay-kit/commit/828fad59172a96ca0fecb3a027792db96d942ebe) Thanks [@XionWCFM](https://github.com/XionWCFM)! - feat: Add overlayAsync implementation 115 | 116 | This change implements the openAsync method for overly-kit's promise support discussed in #47. 117 | 118 | **Related Issue:** Fixes #47 119 | 120 | ## 1.2.4 121 | 122 | ### Patch Changes 123 | 124 | - [#53](https://github.com/toss/overlay-kit/pull/53) [`6f3c26a`](https://github.com/toss/overlay-kit/commit/6f3c26aef21ab639dcaa0c3134299f87de1c01ff) Thanks [@jungpaeng](https://github.com/jungpaeng)! - fix: Enhance Overlay State Management and Prevent Duplicate Entries 125 | 126 | This change enhances the overlay state management to ensure overlays maintain the correct state when closed and reopened, and prevents duplicate overlay entries. 127 | It addresses issues with the overlay's `current` state not updating correctly in certain scenarios. 128 | 129 | **Related Issue:** Fixes # 46 130 | 131 | - [#58](https://github.com/toss/overlay-kit/pull/58) [`b35ac6f`](https://github.com/toss/overlay-kit/commit/b35ac6fdd14e9438a922b9c29c06753da312bc3e) Thanks [@jungpaeng](https://github.com/jungpaeng)! - fix: state reset issue on overlay reopen 132 | 133 | This change fixes an issue where overlays did not retain their state when reopened without unmounting, even though they were not removed from the DOM. 134 | The overlayReducer has been updated to maintain the state of overlays between close and open cycles, addressing an unintended state reset. 135 | 136 | Related Issue: Fixes #57 137 | 138 | ## 1.2.3 139 | 140 | ### Patch Changes 141 | 142 | - [#50](https://github.com/toss/overlay-kit/pull/50) [`5d7e84d`](https://github.com/toss/overlay-kit/commit/5d7e84d3d096a5510ba4d7953d37824a4af5dfc2) Thanks [@jungpaeng](https://github.com/jungpaeng)! - Fix: Ensure 'current' reflects the last overlay when closing intermediate overlays 143 | 144 | - Resolve issue where 'current' does not update to the last overlay when closing an intermediate overlay 145 | - Add logic to correctly update 'current' in reducer 146 | 147 | ## 1.2.2 148 | 149 | ### Patch Changes 150 | 151 | - [#48](https://github.com/toss/overlay-kit/pull/48) [`2aaa5ea`](https://github.com/toss/overlay-kit/commit/2aaa5eac66ff09ea7477e57b3f2a7d462b6a614a) Thanks [@jungpaeng](https://github.com/jungpaeng)! - fix: Change current value when closing overlay 152 | 153 | ## 1.2.1 154 | 155 | ### Patch Changes 156 | 157 | - [#42](https://github.com/toss/overlay-kit/pull/42) [`f3c8ef3`](https://github.com/toss/overlay-kit/commit/f3c8ef311422ea75ce58c91d7003cb680cfca40b) Thanks [@jgjgill](https://github.com/jgjgill)! - chore: rename to overlayId 158 | 159 | - [#40](https://github.com/toss/overlay-kit/pull/40) [`c0aab02`](https://github.com/toss/overlay-kit/commit/c0aab02c89e5a83351db55d5804cc8815e46cfd7) Thanks [@jungpaeng](https://github.com/jungpaeng)! - Set current to null if no overlay remains on unmount 160 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | ![](../docs/public/og.png) 2 | 3 | # overlay-kit · [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/toss/overlay-kit/blob/main/LICENSE) [![codecov](https://codecov.io/gh/toss/overlay-kit/graph/badge.svg?token=JBEAQTL7XK)](https://codecov.io/gh/toss/overlay-kit) 4 | 5 | English | [한국어](https://github.com/toss/overlay-kit/blob/main/README-ko_kr.md) 6 | 7 | overlay-kit is a library that lets you manage overlays in a simple and declarative way using React. 8 | 9 | ```tsx 10 | import { overlay } from 'overlay-kit'; 11 | 12 | 21 | ``` 22 | 23 | Here are the features overlay-kit provides: 24 | 25 | - **Hassle-free**: overlay-kit makes overlay management straightforward with a simple function call: just call `overlay.open(...)`. See [the code comparison](https://overlay-kit.slash.page/code-comparison.html) for details. 26 | - **Maximum Compatibility**: overlay-kit is compatible with the majority of overlay types. From Material UI to custom component libraries, overlay-kit can handle almost all types of overlays. 27 | - **Promise Integration**: overlay-kit is easy to use with promises when getting results from overlays. 28 | - **Robust Built-in Types**: overlay-kit offers robust types for all functions, ensuring type safety and enhancing the developer experience. 29 | 30 | ## License 31 | 32 | MIT © Viva Republica, Inc. See [LICENSE](https://github.com/toss/overlay-kit/blob/main/LICENSE) for details. 33 | 34 | 35 | 36 | 37 | Toss 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overlay-kit", 3 | "version": "1.8.1", 4 | "description": "Next-generation tools for managing overlays", 5 | "keywords": [ 6 | "overlay", 7 | "react" 8 | ], 9 | "homepage": "https://overlay-kit.slash.page", 10 | "bugs": "https://github.com/toss/overlay-kit/issues", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/toss/overlay-kit.git", 14 | "directory": "packages" 15 | }, 16 | "license": "MIT", 17 | "author": { 18 | "name": "Yongbeen Im", 19 | "email": "been.im@toss.im" 20 | }, 21 | "sideEffects": false, 22 | "type": "module", 23 | "main": "dist/index.cjs", 24 | "module": "dist/index.js", 25 | "types": "dist/index.d.ts", 26 | "files": [ 27 | "dist" 28 | ], 29 | "exports": { 30 | ".": { 31 | "import": { 32 | "types": "./dist/index.d.ts", 33 | "default": "./dist/index.js" 34 | }, 35 | "require": { 36 | "types": "./dist/index.d.cts", 37 | "default": "./dist/index.cjs" 38 | } 39 | }, 40 | "./package.json": "./package.json" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | }, 45 | "scripts": { 46 | "build": "tsup", 47 | "test": "vitest run --coverage --typecheck", 48 | "test:attw": "attw --pack", 49 | "test:publint": "publint" 50 | }, 51 | "devDependencies": { 52 | "@arethetypeswrong/cli": "^0.17.1", 53 | "@testing-library/dom": "^10.4.0", 54 | "@testing-library/jest-dom": "^6.6.3", 55 | "@testing-library/react": "^16.1.0", 56 | "@testing-library/user-event": "^14.5.2", 57 | "@types/react": "^19.0.1", 58 | "@types/react-dom": "^19.0.2", 59 | "@vitejs/plugin-react": "^4.3.4", 60 | "@vitest/coverage-v8": "^2.1.8", 61 | "jsdom": "^25.0.1", 62 | "publint": "^0.2.12", 63 | "react": "^19.0.0", 64 | "react-dom": "^19.0.0", 65 | "tsup": "^8.1.0", 66 | "typescript": "^5.4.5", 67 | "vite": "^6.0.3", 68 | "vitest": "^2.1.8" 69 | }, 70 | "peerDependencies": { 71 | "react": "^16.8 || ^17 || ^18 || ^19", 72 | "react-dom": "^16.8 || ^17 || ^18 || ^19" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/setup.test.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /packages/src/context/context.ts: -------------------------------------------------------------------------------- 1 | import { type OverlayData } from './reducer'; 2 | import { createSafeContext } from '../utils/create-safe-context'; 3 | 4 | export function createOverlaySafeContext() { 5 | const [OverlayContextProvider, useOverlayContext] = createSafeContext('overlay-kit/OverlayContext'); 6 | 7 | function useCurrentOverlay() { 8 | return useOverlayContext().current; 9 | } 10 | 11 | function useOverlayData() { 12 | return useOverlayContext().overlayData; 13 | } 14 | 15 | return { OverlayContextProvider, useCurrentOverlay, useOverlayData }; 16 | } 17 | -------------------------------------------------------------------------------- /packages/src/context/provider/content-overlay-controller.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, useEffect, useRef } from 'react'; 2 | 3 | type OverlayControllerProps = { 4 | overlayId: string; 5 | isOpen: boolean; 6 | close: () => void; 7 | unmount: () => void; 8 | }; 9 | 10 | type OverlayAsyncControllerProps = Omit & { 11 | close: (param: T) => void; 12 | }; 13 | 14 | export type OverlayControllerComponent = FC; 15 | export type OverlayAsyncControllerComponent = FC>; 16 | 17 | type ContentOverlayControllerProps = { 18 | isOpen: boolean; 19 | current: string | null; 20 | overlayId: string; 21 | onMounted: () => void; 22 | onCloseModal: () => void; 23 | onExitModal: () => void; 24 | controller: OverlayControllerComponent; 25 | }; 26 | 27 | export function ContentOverlayController({ 28 | isOpen, 29 | current, 30 | overlayId, 31 | onMounted, 32 | onCloseModal, 33 | onExitModal, 34 | controller: Controller, 35 | }: ContentOverlayControllerProps) { 36 | const prevCurrent = useRef(current); 37 | const onMountedRef = useRef(onMounted); 38 | 39 | /** 40 | * @description Executes when closing and reopening an overlay without unmounting. 41 | */ 42 | if (prevCurrent.current !== current) { 43 | prevCurrent.current = current; 44 | 45 | if (current === overlayId) { 46 | onMountedRef.current(); 47 | } 48 | } 49 | 50 | useEffect(() => { 51 | onMountedRef.current(); 52 | }, []); 53 | 54 | return ; 55 | } 56 | -------------------------------------------------------------------------------- /packages/src/context/provider/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useReducer, type PropsWithChildren } from 'react'; 2 | import { ContentOverlayController } from './content-overlay-controller'; 3 | import { type OverlayEvent, createOverlay } from '../../event'; 4 | import { randomId } from '../../utils/random-id'; 5 | import { createOverlaySafeContext } from '../context'; 6 | import { overlayReducer } from '../reducer'; 7 | 8 | export function createOverlayProvider() { 9 | const overlayId = randomId(); 10 | const { useOverlayEvent, ...overlay } = createOverlay(overlayId); 11 | const { OverlayContextProvider, useCurrentOverlay, useOverlayData } = createOverlaySafeContext(); 12 | 13 | function OverlayProvider({ children }: PropsWithChildren) { 14 | const [overlayState, overlayDispatch] = useReducer(overlayReducer, { 15 | current: null, 16 | overlayOrderList: [], 17 | overlayData: {}, 18 | }); 19 | 20 | const open: OverlayEvent['open'] = useCallback(({ controller, overlayId, componentKey }) => { 21 | overlayDispatch({ 22 | type: 'ADD', 23 | overlay: { 24 | id: overlayId, 25 | componentKey, 26 | isOpen: false, 27 | controller: controller, 28 | }, 29 | }); 30 | }, []); 31 | const close: OverlayEvent['close'] = useCallback((overlayId: string) => { 32 | overlayDispatch({ type: 'CLOSE', overlayId }); 33 | }, []); 34 | const unmount: OverlayEvent['unmount'] = useCallback((overlayId: string) => { 35 | overlayDispatch({ type: 'REMOVE', overlayId }); 36 | }, []); 37 | const closeAll: OverlayEvent['closeAll'] = useCallback(() => { 38 | overlayDispatch({ type: 'CLOSE_ALL' }); 39 | }, []); 40 | const unmountAll: OverlayEvent['unmountAll'] = useCallback(() => { 41 | overlayDispatch({ type: 'REMOVE_ALL' }); 42 | }, []); 43 | 44 | useOverlayEvent({ open, close, unmount, closeAll, unmountAll }); 45 | 46 | useEffect(() => { 47 | return () => { 48 | overlayDispatch({ type: 'REMOVE_ALL' }); 49 | }; 50 | }, []); 51 | 52 | return ( 53 | 54 | {children} 55 | {overlayState.overlayOrderList.map((item) => { 56 | const { 57 | id: currentOverlayId, 58 | componentKey, 59 | isOpen, 60 | controller: currentController, 61 | } = overlayState.overlayData[item]; 62 | 63 | return ( 64 | { 70 | requestAnimationFrame(() => { 71 | overlayDispatch({ type: 'OPEN', overlayId: currentOverlayId }); 72 | }); 73 | }} 74 | onCloseModal={() => overlay.close(currentOverlayId)} 75 | onExitModal={() => overlay.unmount(currentOverlayId)} 76 | controller={currentController} 77 | /> 78 | ); 79 | })} 80 | 81 | ); 82 | } 83 | 84 | return { overlay, OverlayProvider, useCurrentOverlay, useOverlayData }; 85 | } 86 | -------------------------------------------------------------------------------- /packages/src/context/reducer.ts: -------------------------------------------------------------------------------- 1 | import { type OverlayControllerComponent } from './provider/content-overlay-controller'; 2 | 3 | type OverlayId = string; 4 | type OverlayItem = { 5 | /** 6 | * @description The unique identifier for the overlay. 7 | */ 8 | id: OverlayId; 9 | /** 10 | * @description The key for the overlay component. 11 | * This is used to identify the overlay component when it is unmounted. 12 | */ 13 | componentKey: string; 14 | isOpen: boolean; 15 | controller: OverlayControllerComponent; 16 | }; 17 | export type OverlayData = { 18 | current: OverlayId | null; 19 | overlayOrderList: OverlayId[]; 20 | overlayData: Record; 21 | }; 22 | 23 | type OverlayReducerAction = 24 | | { type: 'ADD'; overlay: OverlayItem } 25 | | { type: 'OPEN'; overlayId: string } 26 | | { type: 'CLOSE'; overlayId: string } 27 | | { type: 'REMOVE'; overlayId: string } 28 | | { type: 'CLOSE_ALL' } 29 | | { type: 'REMOVE_ALL' }; 30 | 31 | export function overlayReducer(state: OverlayData, action: OverlayReducerAction): OverlayData { 32 | switch (action.type) { 33 | case 'ADD': { 34 | const isExisted = state.overlayOrderList.includes(action.overlay.id); 35 | 36 | if (isExisted && state.overlayData[action.overlay.id].isOpen === true) { 37 | throw new Error("You can't open the multiple overlays with the same overlayId. Please set a different id."); 38 | } 39 | 40 | return { 41 | current: action.overlay.id, 42 | /** 43 | * @description Brings the overlay to the front when reopened after closing without unmounting. 44 | */ 45 | overlayOrderList: [...state.overlayOrderList.filter((item) => item !== action.overlay.id), action.overlay.id], 46 | overlayData: isExisted 47 | ? state.overlayData 48 | : { 49 | ...state.overlayData, 50 | [action.overlay.id]: action.overlay, 51 | }, 52 | }; 53 | } 54 | case 'OPEN': { 55 | const overlay = state.overlayData[action.overlayId]; 56 | 57 | // ignore if the overlay don't exist or already open 58 | if (overlay == null || overlay.isOpen) { 59 | return state; 60 | } 61 | 62 | return { 63 | ...state, 64 | overlayData: { 65 | ...state.overlayData, 66 | [action.overlayId]: { 67 | ...overlay, 68 | isOpen: true, 69 | }, 70 | }, 71 | }; 72 | } 73 | case 'CLOSE': { 74 | const overlay = state.overlayData[action.overlayId]; 75 | 76 | // ignore if the overlay don't exist or already closed 77 | if (overlay == null || !overlay.isOpen) { 78 | return state; 79 | } 80 | 81 | const openedOverlayOrderList = state.overlayOrderList.filter( 82 | (orderedOverlayId) => state.overlayData[orderedOverlayId].isOpen === true 83 | ); 84 | const targetIndexInOpenedList = openedOverlayOrderList.findIndex((item) => item === action.overlayId); 85 | 86 | /** 87 | * @description If closing the last overlay, specify the overlay before it. 88 | * @description If closing intermediate overlays, specifies the last overlay. 89 | * 90 | * @example open - [1, 2, 3, 4] 91 | * close 2 => current: 4 92 | * close 4 => current: 3 93 | * close 3 => current: 1 94 | * close 1 => current: null 95 | */ 96 | const currentOverlayId = 97 | targetIndexInOpenedList === openedOverlayOrderList.length - 1 98 | ? openedOverlayOrderList[targetIndexInOpenedList - 1] ?? null 99 | : openedOverlayOrderList.at(-1) ?? null; 100 | 101 | return { 102 | ...state, 103 | current: currentOverlayId, 104 | overlayData: { 105 | ...state.overlayData, 106 | [action.overlayId]: { 107 | ...state.overlayData[action.overlayId], 108 | isOpen: false, 109 | }, 110 | }, 111 | }; 112 | } 113 | case 'REMOVE': { 114 | const overlay = state.overlayData[action.overlayId]; 115 | 116 | // ignore if the overlay don't exist 117 | if (overlay == null) { 118 | return state; 119 | } 120 | 121 | const remainingOverlays = state.overlayOrderList.filter((item) => item !== action.overlayId); 122 | if (state.overlayOrderList.length === remainingOverlays.length) { 123 | return state; 124 | } 125 | 126 | const copiedOverlayData = { ...state.overlayData }; 127 | delete copiedOverlayData[action.overlayId]; 128 | 129 | const openedOverlayOrderList = state.overlayOrderList.filter( 130 | (orderedOverlayId) => state.overlayData[orderedOverlayId].isOpen === true 131 | ); 132 | const targetIndexInOpenedList = openedOverlayOrderList.findIndex((item) => item === action.overlayId); 133 | 134 | /** 135 | * @description If unmounting the last overlay, specify the overlay before it. 136 | * @description If unmounting intermediate overlays, specifies the last overlay. 137 | * 138 | * @example open - [1, 2, 3, 4] 139 | * unmount 2 => current: 4 140 | * unmount 4 => current: 3 141 | * unmount 3 => current: 1 142 | * unmount 1 => current: null 143 | */ 144 | const currentOverlayId = 145 | targetIndexInOpenedList === openedOverlayOrderList.length - 1 146 | ? openedOverlayOrderList[targetIndexInOpenedList - 1] ?? null 147 | : openedOverlayOrderList.at(-1) ?? null; 148 | 149 | return { 150 | current: currentOverlayId, 151 | overlayOrderList: remainingOverlays, 152 | overlayData: copiedOverlayData, 153 | }; 154 | } 155 | case 'CLOSE_ALL': { 156 | // ignore if there is no overlay 157 | if (Object.keys(state.overlayData).length === 0) { 158 | return state; 159 | } 160 | 161 | return { 162 | ...state, 163 | current: null, 164 | overlayData: Object.keys(state.overlayData).reduce( 165 | (prev, curr) => ({ 166 | ...prev, 167 | [curr]: { 168 | ...state.overlayData[curr], 169 | isOpen: false, 170 | } satisfies OverlayItem, 171 | }), 172 | {} satisfies Record 173 | ), 174 | }; 175 | } 176 | case 'REMOVE_ALL': { 177 | return { current: null, overlayOrderList: [], overlayData: {} }; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /packages/src/event.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type OverlayAsyncControllerComponent, 3 | type OverlayControllerComponent, 4 | } from './context/provider/content-overlay-controller'; 5 | import { createUseExternalEvents } from './utils'; 6 | import { randomId } from './utils/random-id'; 7 | 8 | export type OverlayEvent = { 9 | open: (args: { controller: OverlayControllerComponent; overlayId: string; componentKey: string }) => void; 10 | close: (overlayId: string) => void; 11 | unmount: (overlayId: string) => void; 12 | closeAll: () => void; 13 | unmountAll: () => void; 14 | }; 15 | 16 | type OpenOverlayOptions = { 17 | overlayId?: string; 18 | }; 19 | 20 | export function createOverlay(overlayId: string) { 21 | const [useOverlayEvent, createEvent] = createUseExternalEvents(`${overlayId}/overlay-kit`); 22 | 23 | const open = (controller: OverlayControllerComponent, options?: OpenOverlayOptions) => { 24 | const overlayId = options?.overlayId ?? randomId(); 25 | const componentKey = randomId(); 26 | const dispatchOpenEvent = createEvent('open'); 27 | 28 | dispatchOpenEvent({ controller, overlayId, componentKey }); 29 | return overlayId; 30 | }; 31 | 32 | const openAsync = async (controller: OverlayAsyncControllerComponent, options?: OpenOverlayOptions) => { 33 | return new Promise((resolve) => { 34 | open((overlayProps, ...deprecatedLegacyContext) => { 35 | /** 36 | * @description close the overlay with resolve 37 | */ 38 | const close = (param: T) => { 39 | resolve(param); 40 | overlayProps.close(); 41 | }; 42 | /** 43 | * @description Passing overridden methods 44 | */ 45 | const props = { ...overlayProps, close }; 46 | return controller(props, ...deprecatedLegacyContext); 47 | }, options); 48 | }); 49 | }; 50 | 51 | const close = createEvent('close'); 52 | const unmount = createEvent('unmount'); 53 | const closeAll = createEvent('closeAll'); 54 | const unmountAll = createEvent('unmountAll'); 55 | 56 | return { open, openAsync, close, unmount, closeAll, unmountAll, useOverlayEvent }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export type { 3 | OverlayControllerComponent, 4 | OverlayAsyncControllerComponent, 5 | } from './context/provider/content-overlay-controller'; 6 | -------------------------------------------------------------------------------- /packages/src/utils/create-overlay-context.tsx: -------------------------------------------------------------------------------- 1 | import { createOverlayProvider } from '../context/provider'; 2 | 3 | export const { overlay, OverlayProvider, useCurrentOverlay, useOverlayData } = createOverlayProvider(); 4 | 5 | // eslint-disable-next-line @typescript-eslint/naming-convention 6 | export function experimental_createOverlayContext() { 7 | return createOverlayProvider(); 8 | } 9 | -------------------------------------------------------------------------------- /packages/src/utils/create-safe-context.ts: -------------------------------------------------------------------------------- 1 | import { type Provider, createContext, useContext } from 'react'; 2 | 3 | type NullSymbolType = typeof NullSymbol; 4 | const NullSymbol = Symbol('Null'); 5 | 6 | export type CreateContextReturn = [Provider, () => T]; 7 | 8 | export function createSafeContext(displayName?: string): CreateContextReturn { 9 | const Context = createContext(NullSymbol); 10 | Context.displayName = displayName ?? 'SafeContext'; 11 | 12 | function useSafeContext() { 13 | const context = useContext(Context); 14 | 15 | if (context === NullSymbol) { 16 | const error = new Error(`[${Context.displayName}]: Provider not found.`); 17 | error.name = '[Error] Context'; 18 | 19 | throw error; 20 | } 21 | 22 | return context; 23 | } 24 | 25 | return [Context.Provider, useSafeContext]; 26 | } 27 | -------------------------------------------------------------------------------- /packages/src/utils/create-use-external-events.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { describe, expect, it, vitest } from 'vitest'; 3 | import { createUseExternalEvents } from './create-use-external-events'; 4 | 5 | describe('createUseExternalEvents는', () => { 6 | it('should be able to generate events.', () => { 7 | type TestEvent = { 8 | event: () => void; 9 | }; 10 | 11 | const [useEvent, createEvent] = createUseExternalEvents('eventPrefix'); 12 | const mockedEvent = vitest.fn(); 13 | 14 | renderHook(() => { 15 | useEvent({ event: mockedEvent }); 16 | }); 17 | 18 | const emitEvent = createEvent('event'); 19 | emitEvent(); 20 | 21 | expect(mockedEvent).toBeCalledTimes(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/src/utils/create-use-external-events.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from 'react'; 2 | import { createEmitter } from './emitter'; 3 | 4 | const emitter = createEmitter(); 5 | function useClientLayoutEffect(...args: Parameters) { 6 | if (typeof document === 'undefined') return; 7 | 8 | useLayoutEffect(...args); 9 | } 10 | 11 | function dispatchEvent(type: string, detail?: Detail) { 12 | emitter.emit(type, detail); 13 | } 14 | 15 | // When creating an event, params can be of any type, so specify the type as any. 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | export function createUseExternalEvents void>>(prefix: string) { 18 | function useExternalEvents(events: EventHandlers) { 19 | const handlers = Object.keys(events).reduce void>>((prev, eventKey) => { 20 | const currentEventKeys = `${prefix}:${eventKey}`; 21 | 22 | return { 23 | ...prev, 24 | [currentEventKeys]: function (event: unknown) { 25 | events[eventKey](event); 26 | } as () => void, 27 | }; 28 | }, {}); 29 | 30 | useClientLayoutEffect(() => { 31 | Object.keys(handlers).forEach((eventKey) => { 32 | emitter.off(eventKey, handlers[eventKey]); 33 | emitter.on(eventKey, handlers[eventKey]); 34 | }); 35 | 36 | return () => 37 | Object.keys(handlers).forEach((eventKey) => { 38 | emitter.off(eventKey, handlers[eventKey]); 39 | }); 40 | }, [handlers]); 41 | } 42 | 43 | function createEvent(event: EventKey) { 44 | return (...payload: Parameters) => dispatchEvent(`${prefix}:${String(event)}`, payload[0]); 45 | } 46 | 47 | return [useExternalEvents, createEvent] as const; 48 | } 49 | -------------------------------------------------------------------------------- /packages/src/utils/emitter.ts: -------------------------------------------------------------------------------- 1 | export type EventType = string | symbol; 2 | 3 | export type Handler = (event: T) => void; 4 | export type WildcardHandler> = (type: keyof T, event: T[keyof T]) => void; 5 | 6 | export type EventHandlerList = Array>; 7 | export type WildCardEventHandlerList> = Array>; 8 | 9 | export type EventHandlerMap> = Map< 10 | keyof Events | '*', 11 | EventHandlerList | WildCardEventHandlerList 12 | >; 13 | 14 | export interface Emitter> { 15 | all: EventHandlerMap; 16 | 17 | on(type: Key, handler: Handler): void; 18 | on(type: '*', handler: WildcardHandler): void; 19 | 20 | off(type: Key, handler?: Handler): void; 21 | off(type: '*', handler: WildcardHandler): void; 22 | 23 | emit(type: Key, event: Events[Key]): void; 24 | emit(type: undefined extends Events[Key] ? Key : never): void; 25 | } 26 | 27 | export function createEmitter>( 28 | all?: EventHandlerMap 29 | ): Emitter { 30 | type GenericEventHandler = Handler | WildcardHandler; 31 | all = all || new Map(); 32 | 33 | return { 34 | all, 35 | on(type: Key, handler: GenericEventHandler) { 36 | const handlers: Array | undefined = all!.get(type); 37 | if (handlers) { 38 | handlers.push(handler); 39 | } else { 40 | all!.set(type, [handler] as EventHandlerList); 41 | } 42 | }, 43 | off(type: Key, handler?: GenericEventHandler) { 44 | const handlers: Array | undefined = all!.get(type); 45 | if (handlers) { 46 | if (handler) { 47 | handlers.splice(handlers.indexOf(handler) >>> 0, 1); 48 | } else { 49 | all!.set(type, []); 50 | } 51 | } 52 | }, 53 | emit(type: Key, evt?: Events[Key]) { 54 | let handlers = all!.get(type); 55 | if (handlers) { 56 | (handlers as EventHandlerList).slice().map((handler) => { 57 | handler(evt!); 58 | }); 59 | } 60 | 61 | handlers = all!.get('*'); 62 | if (handlers) { 63 | (handlers as WildCardEventHandlerList).slice().map((handler) => { 64 | handler(type, evt!); 65 | }); 66 | } 67 | }, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /packages/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-overlay-context'; 2 | export * from './create-use-external-events'; 3 | -------------------------------------------------------------------------------- /packages/src/utils/random-id.ts: -------------------------------------------------------------------------------- 1 | export function randomId() { 2 | return `overlay-kit-${Math.random().toString(36).slice(2, 11)}`; 3 | } 4 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM"], 4 | "types": ["@testing-library/jest-dom"], 5 | "target": "es2016", 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "jsx": "react-jsx" 12 | }, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | format: ['cjs', 'esm'], 5 | entry: ['src/index.ts'], 6 | splitting: false, 7 | minify: true, 8 | dts: true, 9 | clean: true, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import packageJson from './package.json'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | setupFiles: 'setup.test.ts', 8 | environment: 'jsdom', 9 | name: packageJson.name, 10 | dir: './src', 11 | coverage: { 12 | provider: 'v8', 13 | }, 14 | }, 15 | }); 16 | --------------------------------------------------------------------------------