@squiz/formatted-text-editor 1.12.0-alpha.10 → 1.12.0-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.eslintignore +6 -0
  2. package/.eslintrc.json +33 -0
  3. package/CHANGELOG.md +48 -0
  4. package/README.md +16 -56
  5. package/build.js +11 -0
  6. package/cypress/e2e/bold.spec.cy.ts +18 -0
  7. package/cypress/global.d.ts +9 -0
  8. package/cypress/support/commands.ts +130 -0
  9. package/cypress/support/e2e.ts +20 -0
  10. package/cypress/tsconfig.json +8 -0
  11. package/cypress.config.ts +7 -0
  12. package/demo/App.tsx +11 -0
  13. package/demo/index.html +13 -0
  14. package/demo/index.scss +11 -0
  15. package/demo/main.tsx +10 -0
  16. package/demo/public/favicon-dxp.svg +3 -0
  17. package/demo/vite-env.d.ts +1 -0
  18. package/file-transformer.js +1 -0
  19. package/jest.config.ts +27 -0
  20. package/lib/index.css +3 -0
  21. package/package.json +4 -6
  22. package/src/Editor/Editor.spec.tsx +88 -0
  23. package/src/Editor/Editor.tsx +44 -0
  24. package/src/Editor/editor.scss +36 -0
  25. package/src/EditorToolbar/EditorToolbar.tsx +40 -0
  26. package/src/EditorToolbar/Tools/Bold/BoldButton.spec.tsx +19 -0
  27. package/src/EditorToolbar/Tools/Bold/BoldButton.tsx +30 -0
  28. package/src/EditorToolbar/Tools/Italic/ItalicButton.spec.tsx +19 -0
  29. package/src/EditorToolbar/Tools/Italic/ItalicButton.tsx +30 -0
  30. package/src/EditorToolbar/Tools/TextAlign/CenterAlign/CenterAlignButton.tsx +31 -0
  31. package/src/EditorToolbar/Tools/TextAlign/JustifyAlign/JustifyAlignButton.tsx +31 -0
  32. package/src/EditorToolbar/Tools/TextAlign/LeftAlign/LeftAlignButton.tsx +31 -0
  33. package/src/EditorToolbar/Tools/TextAlign/RightAlign/RightAlignButton.tsx +31 -0
  34. package/src/EditorToolbar/Tools/TextAlign/TextAlignButtons.tsx +21 -0
  35. package/src/EditorToolbar/Tools/Underline/Underline.spec.tsx +19 -0
  36. package/src/EditorToolbar/Tools/Underline/UnderlineButton.tsx +30 -0
  37. package/src/EditorToolbar/editor-toolbar.scss +26 -0
  38. package/src/FormattedTextEditor.tsx +12 -0
  39. package/src/index.scss +4 -0
  40. package/src/index.ts +3 -0
  41. package/src/ui/ToolbarButton/ToolbarButton.tsx +25 -0
  42. package/src/ui/ToolbarButton/toolbar-button.scss +40 -0
  43. package/tsconfig.json +21 -0
  44. package/vite.config.ts +11 -0
package/.eslintignore ADDED
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ .prettierrc.json
4
+ .eslintrc.json
5
+ src/vite-env.d.ts
6
+ vite.config.ts
package/.eslintrc.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "env": {
3
+ "jest/globals": true,
4
+ "cypress/globals": true
5
+ },
6
+ "plugins": ["cypress"],
7
+ "extends": [
8
+ // By extending from a plugin config, we can get recommended rules without having to add them manually.
9
+ "plugin:react/recommended",
10
+ "plugin:jsx-a11y/recommended",
11
+ "plugin:cypress/recommended",
12
+ // This disables the formatting rules in ESLint that Prettier is going to be responsible for handling.
13
+ // Make sure it"s always the last config, so it gets the chance to override other configs.
14
+ "eslint-config-prettier"
15
+ ],
16
+ "settings": {
17
+ "react": {
18
+ // Tells eslint-plugin-react to automatically detect the version of React to use.
19
+ "version": "detect"
20
+ },
21
+ // Tells eslint how to resolve imports
22
+ "import/resolver": {
23
+ "node": {
24
+ "paths": ["src"],
25
+ "extensions": [".js", ".jsx", ".ts", ".tsx"]
26
+ }
27
+ }
28
+ },
29
+ "rules": {
30
+ // suppress errors for missing 'import React' in files
31
+ "react/react-in-jsx-scope": "off"
32
+ }
33
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [1.6.1-alpha.5](https://gitlab.squiz.net/developer-experience/cmp/compare/v1.6.1-alpha.3...v1.6.1-alpha.5) (2023-02-02)
7
+
8
+ **Note:** Version bump only for package @squiz/formatted-text-editor
9
+
10
+
11
+
12
+
13
+
14
+ ## [1.6.1-alpha.4](https://gitlab.squiz.net/developer-experience/cmp/compare/v1.6.1-alpha.3...v1.6.1-alpha.4) (2023-02-02)
15
+
16
+ **Note:** Version bump only for package @squiz/formatted-text-editor
17
+
18
+
19
+
20
+
21
+
22
+ ## [1.6.1-alpha.3](https://gitlab.squiz.net/developer-experience/cmp/compare/v1.6.1-alpha.2...v1.6.1-alpha.3) (2023-02-02)
23
+
24
+ **Note:** Version bump only for package @squiz/formatted-text-editor
25
+
26
+
27
+
28
+
29
+
30
+ ## [1.6.1-alpha.2](https://gitlab.squiz.net/developer-experience/cmp/compare/v1.6.0...v1.6.1-alpha.2) (2023-02-02)
31
+
32
+ **Note:** Version bump only for package @squiz/formatted-text-editor
33
+
34
+
35
+
36
+
37
+
38
+ ## [1.6.1-alpha.1](https://gitlab.squiz.net/developer-experience/cmp/compare/v1.6.0...v1.6.1-alpha.1) (2023-02-02)
39
+
40
+ **Note:** Version bump only for package @squiz/formatted-text-editor
41
+
42
+
43
+
44
+
45
+
46
+ ## [1.6.1-alpha.0](https://gitlab.squiz.net/developer-experience/cmp/compare/v1.6.0...v1.6.1-alpha.0) (2023-02-02)
47
+
48
+ **Note:** Version bump only for package @squiz/formatted-text-editor
package/README.md CHANGED
@@ -1,44 +1,39 @@
1
1
  # DXP Formatted text editor component
2
2
 
3
- The Squiz DXP formatted text editor, built using [Remirror](https://remirror.io/) React library.
3
+ This repo contains the necessary code to develop and publish an NPM package for creating formatted text editors for use in Squiz products.
4
4
 
5
- It is intended that this package is used for implementing rich text editor capabilities using the DXP Content Store.
6
-
7
- ## Tools and requirements
8
-
9
- Tools that relate to standards, publishing and code base health should be available from the monorepo root. Package specific tools and requirements will be listed below.
5
+ The formatted text editor is using the [Remirror](https://remirror.io/) React library.
10
6
 
11
7
  ### Requirements
12
8
 
13
- There are no tools that you need locally that are specific to this repo. Please refer to the monorepo README for more information.
14
-
15
- ### Optional tools
16
-
17
- - [Volta](https://volta.sh/): Provided you have `Volta` installed as soon as you are in the `formatted-text-editor` directory you should be on the correct Node version.
9
+ - [Volta](https://volta.sh/)
10
+ - [Node/NPM](https://nodejs.org/en/)
18
11
 
19
- ## Working locally
12
+ ### Working locally
20
13
 
21
- ### Installation
14
+ Provided you have `Volta` installed as soon as you are in the `formatted-text-editor` directory you should be on the correct Node version.
15
+ See the `package.json` for the specific version.
22
16
 
23
- Please refer to the monorepo README for getting up and running.
17
+ Now run the following commands from the `packages/formatted-text-editor/` directory.
24
18
 
25
- ### Local development
26
-
27
- You can preview the formatted text editor in your local environment by running the following command from the `packages/formatted-text-editor` directory:
19
+ ```sh
20
+ npm i
21
+ ```
28
22
 
29
23
  ```sh
30
24
  npm run dev
31
25
  ```
32
-
33
26
  This will expose `http://localhost:5173/` and this can be viewed in the browser.
34
27
 
35
28
  > TIP: Make sure you have already installed the node modules at the root level of the repo.
36
29
 
37
- ### Testing
30
+ ## WIP: Contributing
31
+
32
+ ## Testing
38
33
 
39
34
  Tests must be written when contributing to this package. Tests are automatically run in CI and test failure will result in unmerged code.
40
35
 
41
- #### Unit testing
36
+ ### Unit testing
42
37
 
43
38
  This package using Jest to perform Unit testing. In addition to Jest we are using a few Testing Library utilities:
44
39
 
@@ -56,7 +51,7 @@ Or if you'd like to "watch" for changes:
56
51
  npm run test:watch
57
52
  ```
58
53
 
59
- #### End to end testing
54
+ ### End to end testing
60
55
 
61
56
  This package uses [Cypress](https://docs.cypress.io/) for end to end testing.
62
57
 
@@ -65,38 +60,3 @@ To run tests locally you can run:
65
60
  npm run test:e2e
66
61
  ```
67
62
  Cypress is configured to look at a preview dev environment on `http://localhost:8080`.
68
-
69
-
70
- ## Publishing
71
-
72
- This package will be automatically compiled and published following the process defined by the monorepo. Please see the README at the root of the monorepo for more details.
73
-
74
- We may manually publish this repo following these steps (please note that you should only ever do this from the `develop` branch):
75
-
76
- 1. Run a clean, CI install, compile and publish from the root directory
77
-
78
- ```
79
- npm run clean:all
80
- npm ci
81
- npm run compile
82
- npx lerna publish -y --conventional-commits --ci --exact --conventional-prerelease
83
- ```
84
-
85
- 2. Move into the editor directory and check that the lib directory looks correct for publishing
86
-
87
- ```
88
- cd packages/formatted-text-editor
89
- ls -la packages/formatted-text-editor/lib
90
- ```
91
-
92
- 3. Execute the NPM publishing command from the package. Watch for any errors in the console
93
-
94
- ```
95
- npm publish
96
- ```
97
-
98
- 4. Check that the package was published and pushed to NPM
99
-
100
- ```
101
- npm view @squiz/formatted-text-editor
102
- ```
package/build.js ADDED
@@ -0,0 +1,11 @@
1
+ const esbuild = require('esbuild');
2
+ const { sassPlugin } = require('esbuild-sass-plugin');
3
+
4
+ esbuild
5
+ .build({
6
+ entryPoints: ['src/index.scss'],
7
+ bundle: true,
8
+ outdir: 'lib',
9
+ plugins: [sassPlugin({ type: 'css' })],
10
+ })
11
+ .catch(() => process.exit(1));
@@ -0,0 +1,18 @@
1
+ describe('The formatted text editor renders', () => {
2
+ it('Should toggle bold on highlighted text', () => {
3
+ cy.visit('/');
4
+
5
+ cy.get('.ProseMirror.remirror-editor')
6
+ .clear()
7
+ .type('It was the best of times it was the worst of times.')
8
+ .setSelection('best of times');
9
+
10
+ cy.findAllByRole('button', { name: 'Bold (cmd+B)' }).first().click();
11
+
12
+ cy.get('strong').should('include.text', 'best of times');
13
+
14
+ cy.findAllByRole('button', { name: 'Bold (cmd+B)' }).first().click();
15
+
16
+ cy.get('strong').should('not.exist');
17
+ });
18
+ });
@@ -0,0 +1,9 @@
1
+ /// <reference types="cypress" />
2
+
3
+ declare namespace Cypress {
4
+ interface Chainable {
5
+ // Custom command
6
+ selection(subject: any, fn?: any): void;
7
+ setSelection(subject: any, query?: any, endQuery?: any): void;
8
+ }
9
+ }
@@ -0,0 +1,130 @@
1
+ // eslint-disable-next-line @typescript-eslint/triple-slash-reference
2
+ /// <reference path="../global.d.ts" />
3
+
4
+ import '@testing-library/cypress/add-commands';
5
+
6
+ // ***********************************************
7
+ // This example commands.ts shows you how to
8
+ // create various custom commands and overwrite
9
+ // existing commands.
10
+ //
11
+ // For more comprehensive examples of custom
12
+ // commands please read more here:
13
+ // https://on.cypress.io/custom-commands
14
+ // ***********************************************
15
+ //
16
+ //
17
+ // -- This is a parent command --
18
+ // Cypress.Commands.add('login', (email, password) => { ... })
19
+ //
20
+ //
21
+ // -- This is a child command --
22
+ // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
23
+ //
24
+ //
25
+ // -- This is a dual command --
26
+ // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
27
+ //
28
+ //
29
+ // -- This will overwrite an existing command --
30
+ // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
31
+ //
32
+ // declare global {
33
+ // namespace Cypress {
34
+ // interface Chainable {
35
+ // login(email: string, password: string): Chainable<void>
36
+ // drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
37
+ // dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
38
+ // visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
39
+ // }
40
+ // }
41
+ // }
42
+
43
+ /**
44
+ * Credits
45
+ * @Bkucera: https://github.com/cypress-io/cypress/issues/2839#issuecomment-447012818
46
+ * @Phrogz: https://stackoverflow.com/a/10730777/1556245
47
+ *
48
+ * Usage
49
+ * ```
50
+ * // Types "foo" and then selects "fo"
51
+ * cy.get('input')
52
+ * .type('foo')
53
+ * .setSelection('fo')
54
+ *
55
+ * // Types "foo", "bar", "baz", and "qux" on separate lines, then selects "foo", "bar", and "baz"
56
+ * cy.get('textarea')
57
+ * .type('foo{enter}bar{enter}baz{enter}qux{enter}')
58
+ * .setSelection('foo', 'baz')
59
+ *
60
+ * // Types "foo" and then sets the cursor before the last letter
61
+ * cy.get('input')
62
+ * .type('foo')
63
+ * .setCursorAfter('fo')
64
+ *
65
+ * // Types "foo" and then sets the cursor at the beginning of the word
66
+ * cy.get('input')
67
+ * .type('foo')
68
+ * .setCursorBefore('foo')
69
+ *
70
+ * // `setSelection` can alternatively target starting and ending nodes using query strings,
71
+ * // plus specific offsets. The queries are processed via `Element.querySelector`.
72
+ * cy.get('body')
73
+ * .setSelection({
74
+ * anchorQuery: 'ul > li > p', // required
75
+ * anchorOffset: 2 // default: 0
76
+ * focusQuery: 'ul > li > p:last-child', // default: anchorQuery
77
+ * focusOffset: 0 // default: 0
78
+ * })
79
+ */
80
+
81
+ // Low level command reused by `setSelection` and low level command `setCursor`
82
+ Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => {
83
+ cy.wrap(subject).trigger('mousedown').then(fn).trigger('mouseup');
84
+
85
+ cy.document().trigger('selectionchange');
86
+ return cy.wrap(subject);
87
+ });
88
+
89
+ Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => {
90
+ return cy.wrap(subject).selection(($el: any[]) => {
91
+ if (typeof query === 'string') {
92
+ const anchorNode = getTextNode($el[0], query);
93
+ const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode;
94
+ const anchorOffset = anchorNode.wholeText.indexOf(query);
95
+ const focusOffset = endQuery
96
+ ? focusNode.wholeText.indexOf(endQuery) + endQuery.length
97
+ : anchorOffset + query.length;
98
+ setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
99
+ } else if (typeof query === 'object') {
100
+ const el = $el[0];
101
+ const anchorNode = getTextNode(el.querySelector(query.anchorQuery));
102
+ const anchorOffset = query.anchorOffset || 0;
103
+ const focusNode = query.focusQuery ? getTextNode(el.querySelector(query.focusQuery)) : anchorNode;
104
+ const focusOffset = query.focusOffset || 0;
105
+ setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
106
+ }
107
+ });
108
+ });
109
+
110
+ // Helper functions
111
+ function getTextNode(el: HTMLElement, match?: any) {
112
+ const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
113
+ if (!match) {
114
+ return walk.nextNode();
115
+ }
116
+
117
+ // const nodes = [];
118
+ let node;
119
+ while ((node = walk.nextNode())) {
120
+ if (node.wholeText.includes(match)) {
121
+ return node;
122
+ }
123
+ }
124
+ }
125
+
126
+ function setBaseAndExtent(...args) {
127
+ const document = args[0].ownerDocument;
128
+ document.getSelection().removeAllRanges();
129
+ document.getSelection().setBaseAndExtent(...args);
130
+ }
@@ -0,0 +1,20 @@
1
+ // ***********************************************************
2
+ // This example support/e2e.ts is processed and
3
+ // loaded automatically before your test files.
4
+ //
5
+ // This is a great place to put global configuration and
6
+ // behavior that modifies Cypress.
7
+ //
8
+ // You can change the location of this file or turn off
9
+ // automatically serving support files with the
10
+ // 'supportFile' configuration option.
11
+ //
12
+ // You can read more here:
13
+ // https://on.cypress.io/configuration
14
+ // ***********************************************************
15
+
16
+ // Import commands.js using ES2015 syntax:
17
+ import './commands';
18
+
19
+ // Alternatively you can use CommonJS syntax:
20
+ // require('./commands')
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["es5", "dom"],
5
+ "types": ["cypress", "@testing-library/cypress", "node"]
6
+ },
7
+ "include": ["**/*.ts"]
8
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'cypress';
2
+
3
+ export default defineConfig({
4
+ e2e: {
5
+ baseUrl: 'http://localhost:8080',
6
+ },
7
+ });
package/demo/App.tsx ADDED
@@ -0,0 +1,11 @@
1
+ import FormattedTextEditor from '../src/FormattedTextEditor';
2
+
3
+ function App() {
4
+ return (
5
+ <div className="App">
6
+ <FormattedTextEditor />
7
+ </div>
8
+ );
9
+ }
10
+
11
+ export default App;
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon-dxp.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Formatted text editor - DXP package (SQUIZ)</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="./main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,11 @@
1
+ /* Global variables */
2
+ :root {
3
+ --grey-200: #ededed;
4
+ --grey-300: #e0e0e0;
5
+ --grey-600: #707070;
6
+ --blue-100: #e6f1fa;
7
+ --blue-300: #0774d2;
8
+ --border-radius: 4px;
9
+ }
10
+
11
+ @import '../src/index.scss';
package/demo/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.scss';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ );
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 16 16">
2
+ <path fill="#1D9BE2" fill-rule="evenodd" d="M4 0a4 4 0 0 0-4 4v8a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4H4Zm-.883 5.766a4.01 4.01 0 0 0 1.66.909l5.334 1.49c.634.177 1.069-.628.574-1.062L5.532 2.59c-.829-.745-2.255-.827-3.001 0-1.078 1.342-.326 2.328.586 3.176Zm10.137-3.195c.848.761 1.05 2.188.203 2.95l-1.183.92a.903.903 0 0 1-1.134-.02l-1.091-.913a1.846 1.846 0 0 1 .137-2.937c.848-.761 2.221-.761 3.068 0Zm-2.03 6.754a4.01 4.01 0 0 1 1.66.909c.911.848 1.663 1.834.585 3.176-.746.827-2.172.745-3 0L5.314 8.897c-.495-.434-.06-1.239.574-1.061l5.334 1.49ZM2.745 13.43c-.848-.761-1.05-2.188-.203-2.95l1.183-.92a.903.903 0 0 1 1.134.02l1.091.913a1.846 1.846 0 0 1-.137 2.937c-.847.761-2.221.761-3.068 0Z" clip-rule="evenodd"/>
3
+ </svg>
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1 @@
1
+ module.exports = {};
package/jest.config.ts ADDED
@@ -0,0 +1,27 @@
1
+ export default {
2
+ preset: 'ts-jest',
3
+ maxWorkers: 1,
4
+ clearMocks: true,
5
+ collectCoverage: true,
6
+ collectCoverageFrom: ['src/**/*.{ts,tsx}'],
7
+ errorOnDeprecated: true,
8
+ fakeTimers: {
9
+ enableGlobally: true,
10
+ },
11
+ resetMocks: true,
12
+ resetModules: true,
13
+ testEnvironment: 'jsdom',
14
+ moduleNameMapper: {
15
+ '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css|scss)$':
16
+ '<rootDir>/file-transformer.js',
17
+ },
18
+ // TODO: enable once directory structure is sorted and we have tests/more complete code being written.
19
+ // coverageThreshold: {
20
+ // global: {
21
+ // branches: 90,
22
+ // functions: 90,
23
+ // lines: 90,
24
+ // statements: 90,
25
+ // },
26
+ // },
27
+ };
package/lib/index.css CHANGED
@@ -3783,6 +3783,9 @@ button:active .remirror-menu-pane-shortcut,
3783
3783
  .formatted-text-editor .remirror-editor:active,
3784
3784
  .formatted-text-editor .remirror-editor :focus {
3785
3785
  }
3786
+ .formatted-text-editor .remirror-editor p {
3787
+ display: block;
3788
+ }
3786
3789
  .remirror-theme .ProseMirror:active,
3787
3790
  .remirror-theme .ProseMirror:focus {
3788
3791
  box-shadow: none;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.12.0-alpha.10",
3
+ "version": "1.12.0-alpha.14",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -15,9 +15,6 @@
15
15
  "test:watch": "jest --watch",
16
16
  "test:e2e": "vite build && vite preview --port 8080 & cypress open"
17
17
  },
18
- "files": [
19
- "lib/**"
20
- ],
21
18
  "dependencies": {
22
19
  "@mui/icons-material": "5.11.0",
23
20
  "@remirror/react": "2.0.25"
@@ -57,5 +54,6 @@
57
54
  },
58
55
  "volta": {
59
56
  "node": "16.19.0"
60
- }
61
- }
57
+ },
58
+ "gitHead": "76fa4ca4f3c3fa3b0a808232474df640550871cc"
59
+ }
@@ -0,0 +1,88 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import Editor from './Editor';
4
+ import { fireEvent, render, screen } from '@testing-library/react';
5
+
6
+ describe('Formatted text editor', () => {
7
+ it('Renders the text editor', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
10
+ });
11
+
12
+ it('Renders the placeholder if there is no content', () => {
13
+ render(<Editor />);
14
+ expect(document.querySelector(`[data-placeholder='Write something']`)).toBeInTheDocument();
15
+ });
16
+
17
+ it('Renders the bold button', () => {
18
+ render(<Editor />);
19
+ expect(screen.getByRole('button', { name: 'Bold (cmd+B)' })).toBeInTheDocument();
20
+ });
21
+
22
+ it('Renders the italic button', () => {
23
+ render(<Editor />);
24
+ expect(screen.getByRole('button', { name: 'Italic (cmd+I)' })).toBeInTheDocument();
25
+ });
26
+
27
+ it('Renders the underline button', () => {
28
+ render(<Editor />);
29
+ expect(screen.getByRole('button', { name: 'Underline (cmd+U)' })).toBeInTheDocument();
30
+ });
31
+
32
+ it('Renders the align left button', () => {
33
+ render(<Editor />);
34
+ expect(screen.getByRole('button', { name: 'Align left' })).toBeInTheDocument();
35
+ });
36
+
37
+ it('Applies left alignment styling to text when clicked', () => {
38
+ const { baseElement } = render(<Editor />);
39
+ expect(baseElement).toBeTruthy();
40
+
41
+ expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeFalsy();
42
+
43
+ const leftAlignButton = baseElement.querySelector('button[title="Align left"]') as HTMLButtonElement;
44
+ expect(leftAlignButton).toBeTruthy();
45
+
46
+ fireEvent.click(leftAlignButton);
47
+ expect(baseElement.querySelector('p[data-node-text-align="left"]')).toBeTruthy();
48
+ });
49
+
50
+ it('Applies center alignment styling to text when clicked', () => {
51
+ const { baseElement } = render(<Editor />);
52
+ expect(baseElement).toBeTruthy();
53
+
54
+ expect(baseElement.querySelector('p[data-node-text-align="center"]')).toBeFalsy();
55
+
56
+ const centerAlignButton = baseElement.querySelector('button[title="Align center"]') as HTMLButtonElement;
57
+ expect(centerAlignButton).toBeTruthy();
58
+
59
+ fireEvent.click(centerAlignButton);
60
+ expect(baseElement.querySelector('p[data-node-text-align="center"]')).toBeTruthy();
61
+ });
62
+
63
+ it('Applies right alignment styling to text when clicked', () => {
64
+ const { baseElement } = render(<Editor />);
65
+ expect(baseElement).toBeTruthy();
66
+
67
+ expect(baseElement.querySelector('p[data-node-text-align="right"]')).toBeFalsy();
68
+
69
+ const rightAlignButton = baseElement.querySelector('button[title="Align right"]') as HTMLButtonElement;
70
+ expect(rightAlignButton).toBeTruthy();
71
+
72
+ fireEvent.click(rightAlignButton);
73
+ expect(baseElement.querySelector('p[data-node-text-align="right"]')).toBeTruthy();
74
+ });
75
+
76
+ it('Applies justify alignment styling to text when clicked', () => {
77
+ const { baseElement } = render(<Editor />);
78
+ expect(baseElement).toBeTruthy();
79
+
80
+ expect(baseElement.querySelector('p[data-node-text-align="justify"]')).toBeFalsy();
81
+
82
+ const justifyAlignButton = baseElement.querySelector('button[title="Justify"]') as HTMLButtonElement;
83
+ expect(justifyAlignButton).toBeTruthy();
84
+
85
+ fireEvent.click(justifyAlignButton);
86
+ expect(baseElement.querySelector('p[data-node-text-align="justify"]')).toBeTruthy();
87
+ });
88
+ });
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import {
3
+ BoldExtension,
4
+ ItalicExtension,
5
+ NodeFormattingExtension,
6
+ UnderlineExtension,
7
+ wysiwygPreset,
8
+ } from 'remirror/extensions';
9
+ import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
10
+ import { RemirrorContentType, Extension } from '@remirror/core';
11
+ import { EditorToolbar } from '../EditorToolbar/EditorToolbar';
12
+
13
+ type EditorProps = {
14
+ content?: RemirrorContentType;
15
+ };
16
+
17
+ const Editor = ({ content }: EditorProps) => {
18
+ const { manager, state, setState } = useRemirror({
19
+ extensions: () => [
20
+ ...(wysiwygPreset() as Extension[]),
21
+ new BoldExtension(),
22
+ new ItalicExtension(),
23
+ new NodeFormattingExtension(),
24
+ new UnderlineExtension(),
25
+ ],
26
+ content,
27
+ selection: 'start',
28
+ stringHandler: 'html',
29
+ });
30
+
31
+ const handleChange = (parameter: { state: any }) => {
32
+ setState(parameter.state);
33
+ };
34
+
35
+ return (
36
+ <Remirror manager={manager} state={state} onChange={handleChange} placeholder="Write something">
37
+ <EditorToolbar manager={manager} />
38
+ <EditorComponent />
39
+ <EditorToolbar manager={manager} isPopover />
40
+ </Remirror>
41
+ );
42
+ };
43
+
44
+ export default Editor;
@@ -0,0 +1,36 @@
1
+ .formatted-text-editor {
2
+ &.editor-wrapper {
3
+ background: white;
4
+ border: 2px solid var(--grey-300);
5
+ border-radius: 4px;
6
+ }
7
+
8
+ .remirror-editor-wrapper {
9
+ padding-top: 0;
10
+ color: var(--grey-600);
11
+ }
12
+
13
+ .remirror-editor {
14
+ box-shadow: none;
15
+ background: white;
16
+ border-radius: 0 0 4px 4px;
17
+
18
+ &:active,
19
+ :focus {
20
+ /* some focus state */
21
+ }
22
+
23
+ p {
24
+ /* Make sure content aligned with "text-align: justify" is justified */
25
+ display: block;
26
+ }
27
+ }
28
+ }
29
+
30
+ /* remove existing style */
31
+ .remirror-theme .ProseMirror {
32
+ &:active,
33
+ &:focus {
34
+ box-shadow: none;
35
+ }
36
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { Toolbar, FloatingToolbar } from '@remirror/react-components';
3
+ import { RemirrorManager } from 'remirror';
4
+ import ItalicButton from './Tools/Italic/ItalicButton';
5
+ import UnderlineButton from './Tools/Underline/UnderlineButton';
6
+ import BoldButton from './Tools/Bold/BoldButton';
7
+ import TextAlignButtons from './Tools/TextAlign/TextAlignButtons';
8
+
9
+ type EditorToolbarProps = {
10
+ manager: RemirrorManager<any>;
11
+ isPopover?: boolean;
12
+ };
13
+
14
+ // The editor main toolbar
15
+ export const EditorToolbar = ({ manager, isPopover }: EditorToolbarProps) => {
16
+ const extensionNames: Record<string, true> = {};
17
+
18
+ manager.extensions.forEach((extension) => {
19
+ extensionNames[extension.name] = true;
20
+ });
21
+
22
+ return (
23
+ <>
24
+ {!isPopover ? (
25
+ <Toolbar className="remirror-toolbar editor-toolbar">
26
+ {extensionNames.bold && <BoldButton />}
27
+ {extensionNames.italic && <ItalicButton />}
28
+ {extensionNames.underline && <UnderlineButton />}
29
+ {extensionNames.nodeFormatting && <TextAlignButtons />}
30
+ </Toolbar>
31
+ ) : (
32
+ <FloatingToolbar className="remirror-floating-popover">
33
+ {extensionNames.bold && <BoldButton />}
34
+ {extensionNames.italic && <ItalicButton />}
35
+ {extensionNames.underline && <UnderlineButton />}
36
+ </FloatingToolbar>
37
+ )}
38
+ </>
39
+ );
40
+ };
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import Editor from '../../../Editor/Editor';
5
+
6
+ describe('Bold button', () => {
7
+ it('Renders the bold button', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('button', { name: 'Bold (cmd+B)' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('Activates the button if clicked', () => {
13
+ render(<Editor />);
14
+ expect(screen.getByRole('button', { name: 'Bold (cmd+B)' }).classList.contains('btn')).toBeTruthy();
15
+ const bold = screen.getByRole('button', { name: 'Bold (cmd+B)' });
16
+ fireEvent.click(bold);
17
+ expect(bold.classList.contains('is-active')).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { BoldExtension } from '@remirror/extension-bold';
4
+ import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatBoldRoundedIcon from '@mui/icons-material/FormatBoldRounded';
6
+
7
+ const BoldButton = () => {
8
+ const { toggleBold } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<BoldExtension>();
12
+ const enabled = toggleBold.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleBold.enabled()) {
15
+ chain.toggleBold().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <ToolbarButton
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.bold()}
24
+ icon={<FormatBoldRoundedIcon />}
25
+ label="Bold (cmd+B)"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default BoldButton;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import Editor from '../../../Editor/Editor';
5
+
6
+ describe('Italic button', () => {
7
+ it('Renders the italic button', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('button', { name: 'Italic (cmd+I)' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('Activates the button if clicked', () => {
13
+ render(<Editor />);
14
+ expect(screen.getByRole('button', { name: 'Italic (cmd+I)' }).classList.contains('btn')).toBeTruthy();
15
+ const italic = screen.getByRole('button', { name: 'Italic (cmd+I)' });
16
+ fireEvent.click(italic);
17
+ expect(italic.classList.contains('is-active')).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { ItalicExtension } from '@remirror/extension-italic';
4
+ import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatItalicRoundedIcon from '@mui/icons-material/FormatItalicRounded';
6
+
7
+ const ItalicButton = () => {
8
+ const { toggleItalic } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<ItalicExtension>();
12
+ const enabled = toggleItalic.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleItalic.enabled()) {
15
+ chain.toggleItalic().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <ToolbarButton
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.italic()}
24
+ icon={<FormatItalicRoundedIcon />}
25
+ label="Italic (cmd+I)"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default ItalicButton;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands } from '@remirror/react';
3
+ import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
+ import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatAlignCenterIcon from '@mui/icons-material/FormatAlignCenter';
6
+
7
+ const CenterAlignButton = () => {
8
+ const { centerAlign } = useCommands<NodeFormattingExtension>();
9
+ const chain = useChainedCommands();
10
+
11
+ const handleSelect = () => {
12
+ if (centerAlign.enabled()) {
13
+ chain.centerAlign().focus().run();
14
+ }
15
+ };
16
+
17
+ const active = centerAlign.active?.() || false;
18
+ const enabled = centerAlign.enabled();
19
+
20
+ return (
21
+ <ToolbarButton
22
+ handleOnClick={handleSelect}
23
+ isDisabled={!enabled}
24
+ isActive={active}
25
+ icon={<FormatAlignCenterIcon />}
26
+ label="Align center"
27
+ />
28
+ );
29
+ };
30
+
31
+ export default CenterAlignButton;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands } from '@remirror/react';
3
+ import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
+ import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatAlignJustifyIcon from '@mui/icons-material/FormatAlignJustify';
6
+
7
+ const JustifyAlignButton = () => {
8
+ const { justifyAlign } = useCommands<NodeFormattingExtension>();
9
+ const chain = useChainedCommands();
10
+
11
+ const handleSelect = () => {
12
+ if (justifyAlign.enabled()) {
13
+ chain.justifyAlign().focus().run();
14
+ }
15
+ };
16
+
17
+ const active = justifyAlign.active?.() || false;
18
+ const enabled = justifyAlign.enabled();
19
+
20
+ return (
21
+ <ToolbarButton
22
+ handleOnClick={handleSelect}
23
+ isDisabled={!enabled}
24
+ isActive={active}
25
+ icon={<FormatAlignJustifyIcon />}
26
+ label="Justify"
27
+ />
28
+ );
29
+ };
30
+
31
+ export default JustifyAlignButton;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands } from '@remirror/react';
3
+ import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
+ import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatAlignLeftIcon from '@mui/icons-material/FormatAlignLeft';
6
+
7
+ const LeftAlignButton = () => {
8
+ const { leftAlign } = useCommands<NodeFormattingExtension>();
9
+ const chain = useChainedCommands();
10
+
11
+ const handleSelect = () => {
12
+ if (leftAlign.enabled()) {
13
+ chain.leftAlign().focus().run();
14
+ }
15
+ };
16
+
17
+ const active = leftAlign.active?.() || false;
18
+ const enabled = leftAlign.enabled();
19
+
20
+ return (
21
+ <ToolbarButton
22
+ handleOnClick={handleSelect}
23
+ isDisabled={!enabled}
24
+ isActive={active}
25
+ icon={<FormatAlignLeftIcon />}
26
+ label="Align left"
27
+ />
28
+ );
29
+ };
30
+
31
+ export default LeftAlignButton;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { useCommands, useChainedCommands } from '@remirror/react';
3
+ import { NodeFormattingExtension } from '@remirror/extension-node-formatting';
4
+ import ToolbarButton from '../../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatAlignRightIcon from '@mui/icons-material/FormatAlignRight';
6
+
7
+ const RightAlignButton = () => {
8
+ const { rightAlign } = useCommands<NodeFormattingExtension>();
9
+ const chain = useChainedCommands();
10
+
11
+ const handleSelect = () => {
12
+ if (rightAlign.enabled()) {
13
+ chain.rightAlign().focus().run();
14
+ }
15
+ };
16
+
17
+ const active = rightAlign.active?.() || false;
18
+ const enabled = rightAlign.enabled();
19
+
20
+ return (
21
+ <ToolbarButton
22
+ handleOnClick={handleSelect}
23
+ isDisabled={!enabled}
24
+ isActive={active}
25
+ icon={<FormatAlignRightIcon />}
26
+ label="Align right"
27
+ />
28
+ );
29
+ };
30
+
31
+ export default RightAlignButton;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import LeftAlignButton from './LeftAlign/LeftAlignButton';
3
+ import CenterAlignButton from './CenterAlign/CenterAlignButton';
4
+ import RightAlignButton from './RightAlign/RightAlignButton';
5
+ import JustifyAlignButton from './JustifyAlign/JustifyAlignButton';
6
+ import { VerticalDivider } from '@remirror/react-components';
7
+
8
+ const TextAlignButtons = () => {
9
+ return (
10
+ <>
11
+ <VerticalDivider className="editor-divider" />
12
+ <LeftAlignButton />
13
+ <CenterAlignButton />
14
+ <RightAlignButton />
15
+ <JustifyAlignButton />
16
+ <VerticalDivider className="editor-divider" />
17
+ </>
18
+ );
19
+ };
20
+
21
+ export default TextAlignButtons;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { render, screen, fireEvent } from '@testing-library/react';
4
+ import Editor from '../../../Editor/Editor';
5
+
6
+ describe('Underline button', () => {
7
+ it('Renders the underline button', () => {
8
+ render(<Editor />);
9
+ expect(screen.getByRole('button', { name: 'Underline (cmd+U)' })).toBeInTheDocument();
10
+ });
11
+
12
+ it('Activates the button if clicked', () => {
13
+ render(<Editor />);
14
+ expect(screen.getByRole('button', { name: 'Underline (cmd+U)' }).classList.contains('btn')).toBeTruthy();
15
+ const underline = screen.getByRole('button', { name: 'Underline (cmd+U)' });
16
+ fireEvent.click(underline);
17
+ expect(underline.classList.contains('is-active')).toBeTruthy();
18
+ });
19
+ });
@@ -0,0 +1,30 @@
1
+ import React from 'react';
2
+ import { useCommands, useActive, useChainedCommands } from '@remirror/react';
3
+ import { UnderlineExtension } from '@remirror/extension-underline';
4
+ import ToolbarButton from '../../../ui/ToolbarButton/ToolbarButton';
5
+ import FormatUnderlinedRoundedIcon from '@mui/icons-material/FormatUnderlinedRounded';
6
+
7
+ const UnderlineButton = () => {
8
+ const { toggleUnderline } = useCommands();
9
+ const chain = useChainedCommands();
10
+
11
+ const active = useActive<UnderlineExtension>();
12
+ const enabled = toggleUnderline.enabled();
13
+ const handleSelect = () => {
14
+ if (toggleUnderline.enabled()) {
15
+ chain.toggleUnderline().focus().run();
16
+ }
17
+ };
18
+
19
+ return (
20
+ <ToolbarButton
21
+ handleOnClick={handleSelect}
22
+ isDisabled={!enabled}
23
+ isActive={active.underline()}
24
+ icon={<FormatUnderlinedRoundedIcon />}
25
+ label="Underline (cmd+U)"
26
+ />
27
+ );
28
+ };
29
+
30
+ export default UnderlineButton;
@@ -0,0 +1,26 @@
1
+ .formatted-text-editor .editor-toolbar {
2
+ border-bottom: 2px solid var(--grey-200);
3
+ background-color: white;
4
+ padding: 0.25rem;
5
+ border-radius: 0.25rem;
6
+
7
+ display: flex;
8
+ justify-items: center;
9
+
10
+ > *:not(:first-child) {
11
+ margin: 0 0 0 2px;
12
+ }
13
+
14
+ .editor-divider {
15
+ margin: -4px 2px -4px 4px;
16
+ border-width: 1px;
17
+ }
18
+ }
19
+
20
+ .remirror-floating-popover {
21
+ padding: 4px;
22
+ border: 1px var(--grey-200);
23
+ border-radius: 6px;
24
+ background-color: white;
25
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04), 0 1px 12px 4px rgba(0, 0, 0, 0.12);
26
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import Editor from './Editor/Editor';
3
+
4
+ const FormattedTextEditor = () => {
5
+ return (
6
+ <div className="remirror-theme formatted-text-editor editor-wrapper">
7
+ <Editor />
8
+ </div>
9
+ );
10
+ };
11
+
12
+ export default FormattedTextEditor;
package/src/index.scss ADDED
@@ -0,0 +1,4 @@
1
+ @import 'remirror/styles/all.css';
2
+ @import './Editor/editor.scss';
3
+ @import './EditorToolbar/editor-toolbar.scss';
4
+ @import './ui/ToolbarButton/toolbar-button.scss';
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import FormattedTextEditor from './FormattedTextEditor';
2
+
3
+ export { FormattedTextEditor };
@@ -0,0 +1,25 @@
1
+ import React, { ReactElement } from 'react';
2
+
3
+ type ToolbarButtonProps = {
4
+ handleOnClick: () => void;
5
+ isDisabled: boolean;
6
+ isActive: boolean;
7
+ icon: ReactElement;
8
+ label: string;
9
+ };
10
+ const ToolbarButton = ({ handleOnClick, isDisabled, isActive, icon, label }: ToolbarButtonProps) => {
11
+ return (
12
+ <button
13
+ aria-label={label}
14
+ title={label}
15
+ type="button"
16
+ onClick={handleOnClick}
17
+ disabled={isDisabled}
18
+ className={`btn toolbar-button ${isActive ? 'is-active' : ''}`}
19
+ >
20
+ {icon}
21
+ </button>
22
+ );
23
+ };
24
+
25
+ export default ToolbarButton;
@@ -0,0 +1,40 @@
1
+ .btn {
2
+ display: flex;
3
+ align-items: center;
4
+ font-weight: 400;
5
+ text-align: center;
6
+ white-space: nowrap;
7
+ vertical-align: middle;
8
+ touch-action: manipulation;
9
+ cursor: pointer;
10
+ background-image: none;
11
+ border: 1px solid transparent;
12
+ border-radius: var(--border-radius);
13
+
14
+ &:active,
15
+ &:hover,
16
+ &:focus {
17
+ background-color: rgba(black, 0.04);
18
+ }
19
+
20
+ &.disabled,
21
+ &[disabled] {
22
+ cursor: not-allowed;
23
+ opacity: 0.5;
24
+ }
25
+ }
26
+
27
+ .toolbar-button {
28
+ background-color: white;
29
+ color: var(--grey-600);
30
+ padding: 4px;
31
+
32
+ ~ .toolbar-button {
33
+ margin-left: 2px;
34
+ }
35
+
36
+ &.is-active {
37
+ background-color: var(--blue-100);
38
+ color: var(--blue-300);
39
+ }
40
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "react",
17
+ "declaration": true,
18
+ "outDir": "./lib"
19
+ },
20
+ "files": ["src/index.ts"]
21
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ root: 'demo',
7
+ build: {
8
+ outDir: 'build/demo',
9
+ },
10
+ plugins: [react()],
11
+ });