├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── examples └── simple-example │ ├── .gitignore │ ├── favicon.svg │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── App.tsx │ ├── main.tsx │ ├── style.css │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.js ├── images └── solid-tiptap.png ├── lerna.json ├── package.json ├── packages └── solid-tiptap │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── pridepack.json │ ├── src │ ├── Editor.tsx │ └── index.ts │ └── tsconfig.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # next.js build output 64 | .next 65 | 66 | # nuxt.js build output 67 | .nuxt 68 | 69 | # vuepress build output 70 | .vuepress/dist 71 | 72 | # Serverless directories 73 | .serverless 74 | 75 | # FuseBox cache 76 | .fusebox/ 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alexis Munsayac 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solid-tiptap 2 | 3 | > SolidJS bindings for [tiptap](https://www.tiptap.dev/) 4 | 5 | [![NPM](https://img.shields.io/npm/v/solid-tiptap.svg)](https://www.npmjs.com/package/solid-tiptap) [![JavaScript Style Guide](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript) [![Open in StackBlitz](https://img.shields.io/badge/Open%20in-StackBlitz-blue?style=flat-square&logo=stackblitz)](https://stackblitz.com/github/lxsmnsyc/solid-tiptap/tree/main/examples/simple-example) 6 | 7 | ![solid-tiptap example](https://github.com/lxsmnsyc/solid-tiptap/blob/main/images/solid-tiptap.png?raw=true) 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm i @tiptap/core @tiptap/pm solid-tiptap 13 | ``` 14 | 15 | ```bash 16 | yarn add @tiptap/core @tiptap/pm solid-tiptap 17 | ``` 18 | 19 | ```bash 20 | pnpm add @tiptap/core @tiptap/pm solid-tiptap 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```jsx 26 | import { createTiptapEditor } from 'solid-tiptap'; 27 | import StarterKit from '@tiptap/starter-kit'; 28 | 29 | function App() { 30 | let ref!: HTMLDivElement; 31 | 32 | const editor = createTiptapEditor(() => ({ 33 | element: ref!, 34 | extensions: [ 35 | StarterKit, 36 | ], 37 | content: `

Example Text

`, 38 | })); 39 | 40 | return
; 41 | } 42 | ``` 43 | 44 | ### Transactions 45 | 46 | `createEditorTransaction` provides a way to reactively subscribe to editor changes. 47 | 48 | ```ts 49 | const isBold = createEditorTransaction( 50 | () => props.editor, // Editor instance from createTiptapEditor 51 | (editor) => editor.isActive('bold'), 52 | ); 53 | 54 | createEffect(() => { 55 | if (isBold()) { 56 | // do something 57 | } 58 | }); 59 | ``` 60 | 61 | There are out-of-the-box utilities that wraps `createEditorTransaction` based on the [Editor API](https://www.tiptap.dev/api/editor): 62 | 63 | - `useEditorCharacterCount`: reactively subscribe to `getCharacterCount`. 64 | 65 | ```js 66 | const count = useEditorCharacterCount(() => props.editor); 67 | 68 | createEffect(() => { 69 | console.log('Character Count:', count()); 70 | }); 71 | ``` 72 | 73 | - `useEditorHTML`: reactively subscribe to `getEditorHTML`. 74 | 75 | ```js 76 | const html = useEditorHTML(() => props.editor); 77 | 78 | createEffect(() => { 79 | updateHTML(html()); 80 | }); 81 | ``` 82 | 83 | - `useEditorIsActive`: reactively subscribe to `isActive`. 84 | 85 | ```js 86 | const isHeading = useEditorIsActive(() => props.editor, () => 'heading', { 87 | level: 1, 88 | }); 89 | 90 | createEffect(() => { 91 | if (isHeading()) { 92 | // do something 93 | } 94 | }); 95 | ``` 96 | 97 | - `useEditorIsEditable`: reactively subscribe to `isEditable`. 98 | 99 | ```js 100 | const isEditable = useEditorIsEditable(() => props.editor); 101 | 102 | createEffect(() => { 103 | if (isEditable()) { 104 | // do something 105 | } 106 | }); 107 | ``` 108 | 109 | - `useEditorIsEmpty`: reactively subscribe to `isEmpty`. 110 | 111 | ```js 112 | const isEmpty = useEditorIsEmpty(() => props.editor); 113 | 114 | createEffect(() => { 115 | if (isEmpty()) { 116 | // do something 117 | } 118 | }); 119 | ``` 120 | 121 | - `useEditorIsFocused`: reactively subscribe to `isFocused`. 122 | 123 | ```js 124 | const isFocused = useEditorIsFocused(() => props.editor); 125 | 126 | createEffect(() => { 127 | if (isFocused()) { 128 | // do something 129 | } 130 | }); 131 | ``` 132 | 133 | - `useEditorJSON`: reactively subscribe to `getJSON`. 134 | 135 | ```js 136 | const json = useEditorJSON(() => props.editor); 137 | 138 | createEffect(() => { 139 | const data = json(); 140 | 141 | if (data) { 142 | uploadJSON(data); 143 | } 144 | }); 145 | ``` 146 | 147 | ## Tips 148 | 149 | Since `createTiptapEditor` may return `undefined` (the instanciation is scheduled), and you're planning to pass it to another component (e.g. creating `BubbleMenu` or Toolbar), it is recommended to use ternary evaluation (e.g. ``) to check if the editor is `undefined` or not. 150 | 151 | ```js 152 | const editor = createTiptapEditor(() => ({ ... })); 153 | 154 | 155 | {(instance) => } 156 | 157 | ``` 158 | 159 | ## Sponsors 160 | 161 | ![Sponsors](https://github.com/lxsmnsyc/sponsors/blob/main/sponsors.svg?raw=true) 162 | 163 | ## License 164 | 165 | MIT © [lxsmnsyc](https://github.com/lxsmnsyc) 166 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignore": ["node_modules/**/*"] 5 | }, 6 | "vcs": { 7 | "useIgnoreFile": true 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "ignore": ["node_modules/**/*"], 12 | "rules": { 13 | "a11y": { 14 | "noAccessKey": "error", 15 | "noAriaHiddenOnFocusable": "off", 16 | "noAriaUnsupportedElements": "error", 17 | "noAutofocus": "error", 18 | "noBlankTarget": "error", 19 | "noDistractingElements": "error", 20 | "noHeaderScope": "error", 21 | "noInteractiveElementToNoninteractiveRole": "error", 22 | "noNoninteractiveElementToInteractiveRole": "error", 23 | "noNoninteractiveTabindex": "error", 24 | "noPositiveTabindex": "error", 25 | "noRedundantAlt": "error", 26 | "noRedundantRoles": "error", 27 | "noSvgWithoutTitle": "error", 28 | "useAltText": "error", 29 | "useAnchorContent": "error", 30 | "useAriaActivedescendantWithTabindex": "error", 31 | "useAriaPropsForRole": "error", 32 | "useButtonType": "error", 33 | "useHeadingContent": "error", 34 | "useHtmlLang": "error", 35 | "useIframeTitle": "warn", 36 | "useKeyWithClickEvents": "warn", 37 | "useKeyWithMouseEvents": "warn", 38 | "useMediaCaption": "error", 39 | "useValidAnchor": "error", 40 | "useValidAriaProps": "error", 41 | "useValidAriaRole": "error", 42 | "useValidAriaValues": "error", 43 | "useValidLang": "error" 44 | }, 45 | "complexity": { 46 | "noBannedTypes": "error", 47 | "noExcessiveCognitiveComplexity": "error", 48 | "noExtraBooleanCast": "error", 49 | "noForEach": "error", 50 | "noMultipleSpacesInRegularExpressionLiterals": "warn", 51 | "noStaticOnlyClass": "error", 52 | "noThisInStatic": "error", 53 | "noUselessCatch": "error", 54 | "noUselessConstructor": "error", 55 | "noUselessEmptyExport": "error", 56 | "noUselessFragments": "error", 57 | "noUselessLabel": "error", 58 | "noUselessRename": "error", 59 | "noUselessSwitchCase": "error", 60 | "noUselessThisAlias": "error", 61 | "noUselessTypeConstraint": "error", 62 | "noVoid": "off", 63 | "noWith": "error", 64 | "useArrowFunction": "error", 65 | "useFlatMap": "error", 66 | "useLiteralKeys": "error", 67 | "useOptionalChain": "warn", 68 | "useRegexLiterals": "error", 69 | "useSimpleNumberKeys": "error", 70 | "useSimplifiedLogicExpression": "error" 71 | }, 72 | "correctness": { 73 | "noChildrenProp": "error", 74 | "noConstantCondition": "error", 75 | "noConstAssign": "error", 76 | "noConstructorReturn": "error", 77 | "noEmptyCharacterClassInRegex": "error", 78 | "noEmptyPattern": "error", 79 | "noGlobalObjectCalls": "error", 80 | "noInnerDeclarations": "error", 81 | "noInvalidConstructorSuper": "error", 82 | "noInvalidNewBuiltin": "error", 83 | "noNewSymbol": "error", 84 | "noNonoctalDecimalEscape": "error", 85 | "noPrecisionLoss": "error", 86 | "noRenderReturnValue": "error", 87 | "noSelfAssign": "error", 88 | "noSetterReturn": "error", 89 | "noStringCaseMismatch": "error", 90 | "noSwitchDeclarations": "error", 91 | "noUndeclaredVariables": "error", 92 | "noUnnecessaryContinue": "error", 93 | "noUnreachable": "error", 94 | "noUnreachableSuper": "error", 95 | "noUnsafeFinally": "error", 96 | "noUnsafeOptionalChaining": "error", 97 | "noUnusedLabels": "error", 98 | "noUnusedVariables": "error", 99 | "noVoidElementsWithChildren": "error", 100 | "noVoidTypeReturn": "error", 101 | "useExhaustiveDependencies": "error", 102 | "useHookAtTopLevel": "error", 103 | "useIsNan": "error", 104 | "useValidForDirection": "error", 105 | "useYield": "error" 106 | }, 107 | "performance": { 108 | "noAccumulatingSpread": "error", 109 | "noDelete": "off" 110 | }, 111 | "security": { 112 | "noDangerouslySetInnerHtml": "error", 113 | "noDangerouslySetInnerHtmlWithChildren": "error" 114 | }, 115 | "style": { 116 | "noArguments": "error", 117 | "noCommaOperator": "off", 118 | "noDefaultExport": "off", 119 | "noImplicitBoolean": "off", 120 | "noInferrableTypes": "error", 121 | "noNamespace": "error", 122 | "noNegationElse": "error", 123 | "noNonNullAssertion": "off", 124 | "noParameterAssign": "off", 125 | "noParameterProperties": "off", 126 | "noRestrictedGlobals": "error", 127 | "noShoutyConstants": "error", 128 | "noUnusedTemplateLiteral": "error", 129 | "noUselessElse": "error", 130 | "noVar": "error", 131 | "useAsConstAssertion": "error", 132 | "useBlockStatements": "error", 133 | "useCollapsedElseIf": "error", 134 | "useConst": "error", 135 | "useDefaultParameterLast": "error", 136 | "useEnumInitializers": "error", 137 | "useExponentiationOperator": "error", 138 | "useFragmentSyntax": "error", 139 | "useLiteralEnumMembers": "error", 140 | "useNamingConvention": "off", 141 | "useNumericLiterals": "error", 142 | "useSelfClosingElements": "error", 143 | "useShorthandArrayType": "error", 144 | "useShorthandAssign": "error", 145 | "useSingleCaseStatement": "error", 146 | "useSingleVarDeclarator": "error", 147 | "useTemplate": "off", 148 | "useWhile": "error" 149 | }, 150 | "suspicious": { 151 | "noApproximativeNumericConstant": "error", 152 | "noArrayIndexKey": "error", 153 | "noAssignInExpressions": "error", 154 | "noAsyncPromiseExecutor": "error", 155 | "noCatchAssign": "error", 156 | "noClassAssign": "error", 157 | "noCommentText": "error", 158 | "noCompareNegZero": "error", 159 | "noConfusingLabels": "error", 160 | "noConfusingVoidType": "error", 161 | "noConsoleLog": "warn", 162 | "noConstEnum": "off", 163 | "noControlCharactersInRegex": "error", 164 | "noDebugger": "off", 165 | "noDoubleEquals": "error", 166 | "noDuplicateCase": "error", 167 | "noDuplicateClassMembers": "error", 168 | "noDuplicateJsxProps": "error", 169 | "noDuplicateObjectKeys": "error", 170 | "noDuplicateParameters": "error", 171 | "noEmptyInterface": "error", 172 | "noExplicitAny": "warn", 173 | "noExtraNonNullAssertion": "error", 174 | "noFallthroughSwitchClause": "error", 175 | "noFunctionAssign": "error", 176 | "noGlobalIsFinite": "error", 177 | "noGlobalIsNan": "error", 178 | "noImplicitAnyLet": "off", 179 | "noImportAssign": "error", 180 | "noLabelVar": "error", 181 | "noMisleadingInstantiator": "error", 182 | "noMisrefactoredShorthandAssign": "off", 183 | "noPrototypeBuiltins": "error", 184 | "noRedeclare": "error", 185 | "noRedundantUseStrict": "error", 186 | "noSelfCompare": "off", 187 | "noShadowRestrictedNames": "error", 188 | "noSparseArray": "off", 189 | "noUnsafeDeclarationMerging": "error", 190 | "noUnsafeNegation": "error", 191 | "useDefaultSwitchClauseLast": "error", 192 | "useGetterReturn": "error", 193 | "useIsArray": "error", 194 | "useNamespaceKeyword": "error", 195 | "useValidTypeof": "error" 196 | }, 197 | "nursery": { 198 | "noDuplicateJsonKeys": "off", 199 | "noEmptyBlockStatements": "error", 200 | "noEmptyTypeParameters": "error", 201 | "noGlobalEval": "off", 202 | "noGlobalAssign": "error", 203 | "noInvalidUseBeforeDeclaration": "error", 204 | "noMisleadingCharacterClass": "error", 205 | "noNodejsModules": "off", 206 | "noThenProperty": "warn", 207 | "noUnusedImports": "error", 208 | "noUnusedPrivateClassMembers": "error", 209 | "noUselessLoneBlockStatements": "error", 210 | "noUselessTernary": "error", 211 | "useAwait": "error", 212 | "useConsistentArrayType": "error", 213 | "useExportType": "error", 214 | "useFilenamingConvention": "off", 215 | "useForOf": "warn", 216 | "useGroupedTypeImport": "error", 217 | "useImportRestrictions": "off", 218 | "useImportType": "error", 219 | "useNodejsImportProtocol": "warn", 220 | "useNumberNamespace": "error", 221 | "useShorthandFunctionType": "warn" 222 | } 223 | } 224 | }, 225 | "formatter": { 226 | "enabled": true, 227 | "ignore": ["node_modules/**/*"], 228 | "formatWithErrors": false, 229 | "indentWidth": 2, 230 | "indentStyle": "space", 231 | "lineEnding": "lf", 232 | "lineWidth": 80 233 | }, 234 | "organizeImports": { 235 | "enabled": true, 236 | "ignore": ["node_modules/**/*"] 237 | }, 238 | "javascript": { 239 | "formatter": { 240 | "enabled": true, 241 | "arrowParentheses": "asNeeded", 242 | "bracketSameLine": false, 243 | "bracketSpacing": true, 244 | "indentWidth": 2, 245 | "indentStyle": "space", 246 | "jsxQuoteStyle": "double", 247 | "lineEnding": "lf", 248 | "lineWidth": 80, 249 | "quoteProperties": "asNeeded", 250 | "quoteStyle": "single", 251 | "semicolons": "always", 252 | "trailingComma": "all" 253 | }, 254 | "globals": [], 255 | "parser": { 256 | "unsafeParameterDecoratorsEnabled": true 257 | } 258 | }, 259 | "json": { 260 | "formatter": { 261 | "enabled": true, 262 | "indentWidth": 2, 263 | "indentStyle": "space", 264 | "lineEnding": "lf", 265 | "lineWidth": 80 266 | }, 267 | "parser": { 268 | "allowComments": false, 269 | "allowTrailingCommas": false 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /examples/simple-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local -------------------------------------------------------------------------------- /examples/simple-example/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/simple-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/simple-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-example", 3 | "type": "module", 4 | "version": "0.7.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "autoprefixer": "^10.4.17", 12 | "postcss": "^8.4.33", 13 | "typescript": "^5.3.3", 14 | "vite": "^5.0.12", 15 | "vite-plugin-solid": "^2.9.1" 16 | }, 17 | "dependencies": { 18 | "@tailwindcss/typography": "^0.5.10", 19 | "@tiptap/core": "^2.1.16", 20 | "@tiptap/extension-bubble-menu": "^2.1.16", 21 | "@tiptap/pm": "^2.1.16", 22 | "@tiptap/starter-kit": "^2.1.16", 23 | "solid-js": "^1.8.12", 24 | "solid-tiptap": "0.7.0", 25 | "tailwindcss": "^3.4.1", 26 | "terracotta": "^1.0.4" 27 | }, 28 | "private": true, 29 | "publishConfig": { 30 | "access": "restricted" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/simple-example/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/simple-example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { Editor } from '@tiptap/core'; 2 | import BubbleMenu from '@tiptap/extension-bubble-menu'; 3 | import StarterKit from '@tiptap/starter-kit'; 4 | import type { JSX } from 'solid-js'; 5 | import { Show, createSignal } from 'solid-js'; 6 | import { createEditorTransaction, createTiptapEditor } from 'solid-tiptap'; 7 | import { Toggle, Toolbar } from 'terracotta'; 8 | 9 | function ParagraphIcon( 10 | props: JSX.IntrinsicElements['svg'] & { title: string }, 11 | ): JSX.Element { 12 | return ( 13 | 20 | {props.title} 21 | 22 | 23 | ); 24 | } 25 | 26 | function CodeIcon( 27 | props: JSX.IntrinsicElements['svg'] & { title: string }, 28 | ): JSX.Element { 29 | return ( 30 | 37 | {props.title} 38 | 39 | 40 | ); 41 | } 42 | 43 | function CodeBlockIcon( 44 | props: JSX.IntrinsicElements['svg'] & { title: string }, 45 | ): JSX.Element { 46 | return ( 47 | 54 | {props.title} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | function OrderedListIcon( 62 | props: JSX.IntrinsicElements['svg'] & { title: string }, 63 | ): JSX.Element { 64 | return ( 65 | 72 | {props.title} 73 | 77 | 78 | ); 79 | } 80 | 81 | function BulletListIcon( 82 | props: JSX.IntrinsicElements['svg'] & { title: string }, 83 | ): JSX.Element { 84 | return ( 85 | 92 | {props.title} 93 | 97 | 98 | ); 99 | } 100 | 101 | function BlockquoteIcon( 102 | props: JSX.IntrinsicElements['svg'] & { title: string }, 103 | ): JSX.Element { 104 | return ( 105 | 112 | {props.title} 113 | 114 | 115 | ); 116 | } 117 | 118 | const CONTENT = ` 119 |

120 | Hi there, 121 |

122 |

123 | this is a basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: 124 |

125 | 133 |

134 | Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block: 135 |

136 |
body {
137 |   display: none;
138 | }
139 |

140 | I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. 141 |

142 |
143 | Wow, that’s amazing. Good work, boy! 👏 144 |
145 | — Mom 146 |
147 | `; 148 | 149 | function Separator() { 150 | return ( 151 |