{"id":25961686,"url":"https://github.com/ttab/textbit","last_synced_at":"2025-07-31T23:32:01.520Z","repository":{"id":205824783,"uuid":"678341803","full_name":"ttab/textbit","owner":"ttab","description":null,"archived":false,"fork":false,"pushed_at":"2025-02-14T14:33:59.000Z","size":1912,"stargazers_count":8,"open_issues_count":11,"forks_count":1,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-02-14T15:32:44.628Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ttab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-14T10:28:30.000Z","updated_at":"2025-01-30T23:10:00.000Z","dependencies_parsed_at":"2023-11-23T08:41:18.132Z","dependency_job_id":"dfd48883-8c3f-4825-b0bd-94f98d572769","html_url":"https://github.com/ttab/textbit","commit_stats":null,"previous_names":["ttab/textbit"],"tags_count":77,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ttab%2Ftextbit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ttab%2Ftextbit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ttab%2Ftextbit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ttab%2Ftextbit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ttab","download_url":"https://codeload.github.com/ttab/textbit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241912986,"owners_count":20041457,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-03-04T19:41:00.137Z","updated_at":"2025-07-31T23:32:01.508Z","avatar_url":"https://github.com/ttab.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Textbit editable\n\n## Description\n\nAn unstyled editable component with an easy to use plugin framework for creating custom rich text editors in React applications. Based on Slate. See [Slate documentation](https://docs.slatejs.org/) for more information on Slate. As it is early in development basic functionality and types can and will change.\n\n## Development\n\nInstallation and local usage.\n\n```\nnpm i\nnpm run dev\n```\n\nBuilding ESM and CJS.\n\n```\nnpm run build`\n```\n\nThis will produce both an ESM and CJS modules in as well as a typescript definition (_index.d.ts_) file in `dist/`.\n\n## Using it in your project\n\n### Installing\n\nTextbit is available as NPM package published on Github. To install the Textbit package in your project add `@ttab:registry=https://npm.pkg.github.com/` to your `.npmrc`. It should look something like below.\n\n```\nregistry=https://registry.npmjs.org/\n@ttab:registry=https://npm.pkg.github.com/\n```\n\nThen it's just a matter of installing it using your favourite package manager.\n\n```\nnpm i @ttab/textbit\n```\n\n### Basic usage\n\nBelow is the basic structure of the components and their usage. The example is lacking necessary styling and actions. Gutter, Menu and Toolbar components all receive a `className` property for styling. Clone the repo and see the directory `local/` for a more thorough example including additional link, list item plugins and example CSS.\n\n**MyEditor.tsx**\n\n```jsx\nimport React, { useState } from 'react'\nimport Textbit, {\n  Menu,\n  Toolbar,\n  usePluginRegistry,\n  useTextbit\n} from '@ttab/textbit'\nimport './editor-variables.css'\nimport {\n  Plugin1,\n  Plugin2\n} from 'plugin-bundle'\n\nconst initialValue: TBDescendant[] = [\n  {\n    type: 'core/text',\n    id: '538345e5-bacc-48f9-8ef1-a219891b60eb',\n    class: 'text',\n    children: [\n      { text: '' }\n    ]\n  }\n]\n\nfunction MyEditor() {\n  return (\n    \u003cTextbit.Root\n      verbose={true}\n      plugins={[\n        Plugin1(),\n        Plugin2({\n          option1: true,\n          option2: false\n        })\n      ]}\n    \u003e\n      \u003cTextbit.Editable\n        value={initialValue}\n        onChange={value =\u003e {\n          console.log(value, null, 2)\n        }}\n      \u003e\n        \u003cTextbit.DropMarker /\u003e\n\n        \u003cTextbit.Gutter\u003e\n          \u003cMenu.Root\u003e\n             \u003cMenu.Trigger\u003e⋮\u003c/Menu.Trigger\u003e\n             \u003cMenu.Content\u003e\n              \u003cMenu.Group\u003e\n                  \u003cMenu.Item key=\"title\" action={}\u003e\n                    \u003cMenu.Icon/\u003e\n                    \u003cMenu.Label\u003eText\u003c/Menu.Label\u003e\n                    \u003cMenu.Hotkey\u003emod+0\u003c/Menu.Hotkey\u003e\n                  \u003c/Menu.Item\u003e\n                  \u003cMenu.Item key=\"bodytext\" action={}\u003e\n                    \u003cMenu.Icon/\u003e\n                    \u003cMenu.Label/\u003e\n                    \u003cMenu.Hotkey/\u003e\n                  \u003c/Menu.Item\u003e\n              \u003c/Menu.Group\u003e\n            \u003c/Menu.Content\u003e\n          \u003cMenu.Root\u003e\n        \u003c/Textbit.Gutter\u003e\n\n        \u003cToolbar.Root\u003e\n          \u003cToolbar.Group\u003e\n            \u003cToolbar.Item key=\"bold\" action={}/\u003e\n            \u003cToolbar.Item key=\"italic\" action={}/\u003e\n          \u003c/Toolbar.Group\u003e\n        \u003c/Toolbar.Root\u003e\n\n      \u003c/Textbit.Editable\u003e\n    \u003c/Textbit.Root\u003e\n  )\n}\n```\n\n# Component Reference\n\n## Textbit.Root\n\nTop level Texbit component. Receives all plugins. Base plugins is exported from Textbit as `Textbit.Plugins[]`.\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| verbose | boolean | Optional, default false|\n| autoFocus | boolean | Optional, default false|\n| onBlur | React.FocusEventHandler\u003cHTMLDivElement\u003e | Optional |\n| onFocus | React.FocusEventHandler\u003cHTMLDivElement\u003e | Optional |\n| plugins | Plugin.Definition[] | |\n| debounce | number? | Optional debounce time for calling `onChange()` handler. Defaults to 250 ms.\n| debounceSpellcheck | number? | Optional debounce time for calling `onSpellcheck()` handler. Defaults to 1250 ms.\n\n### Provides PluginRegistryContext\n\nPluginRegistryContext: access through convenience hook `usePluginRegistry()`.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| plugins | Plugin.Definition[] | All registered plugins |\n| components | Map\u003cstring, PluginRegistryComponent\u003e | Slate element render components |\n| actions | PluginRegistryAction[] | Convenience structure |\n| verbose | boolean | Output extra info about plugins and settings in the browsers developer console |\n| debounce | number | Optional, set debounce value for onChange(), default 250ms |\n| placeholder | string | Optional, placeholder text for entire editor, default is empty. Should not be combined with _placeholders_. |\n| placeholders | 'none' | 'single' | 'multiple' | Optional, controls how placeholders are used. Single will display one placeholder for entire editor. Multiple will display plugins placeholders on each textline. Default is 'single' if the _placeholder_ property is set, otherwise 'none'. |\n| dispatch | Dispatch\u003cPluginRegistryReducerAction\u003e | Add or delete plugins |\n\n### Provides TextbitContext, useTextbit()\n\nTextbitContext: access through convenience hook `useTextbit()`.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| characters | number | Number of characters in article |\n| words | number | Number of words in article |\n| verbose | boolean | Output extra info on console |\n| autoFocus | boolean | Whether autoFocus is true or false |\n| onBlur | React.FocusEventHandler\u003cHTMLDivElement\u003e | Event handler for when editor looses focus |\n| onFocus | React.FocusEventHandler\u003cHTMLDivElement\u003e | Event handler for when editor receives focus |\n\n---\n\n## Textbit.Editable\n\nEditable area component, acts as wrapper around Slate.\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| value | Descendant[] | Optional, initial content |\n| onChange | (Descendant[] =\u003e void) | Function to receive all changes |\n| onSpellcheck | onSpellcheck?: (texts: string[]) =\u003e Array\u003cArray\u003c{\u003cbr\u003e str: string,\u003cbr\u003e pos: number,\u003cbr\u003e sub: string[]\u003cbr\u003e }\u003e\u003e | Optional, callback function to handle spellchecking of strings |\n| dir | \"ltr\" \\| \"rtl\" | Optional, defaults to _ltr_ |\n| lang | string | Optional langage (e.g en, en-BR, sv, sv_FI). Falls back to html document language, then browser language and last \"en\".\n| yjsEditor | BaseEditor | BaseEditor created with `withYjs()` and `withCursors()` |\n| gutter | boolean | Optional, defaults to true (render gutter). |\n| className | string |  |\n| readOnly | boolean | Optional, defaults to false |\n| children | React.ReactNode \\| undefined  |  |\n| ref | React.LegacyRef\u003cHTMLDivElement\u003e | Provides reference to Slate Editable dom node |\n\n### Provides GutterContext (_used internally_)\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| gutter | boolean | |\n| setOffset | ({ left: number, top: number }) =\u003e void | |\n| offset | { left: number, top: number } | |\n\n### Data attribute\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-state] | \"focused\" \\| \"\" | Indicate whether editor has focus or not. |\n\n## Example\n\nBasic, not complete, example of using it with Yjs.\n\n```javascript\nconst editor = useMemo(() =\u003e {\n  return withYHistory(\n    withCursors(\n      withYjs(\n        createEditor(),\n        provider.document.get('content', Y.XmlText)\n      ),\n      provider.awareness,\n      { data: user as unknown as Record\u003cstring, unknown\u003e }\n    )\n  )\n}, [provider.awareness, provider.document, user])\n```\n\n```jsx\n\u003cTextbit.Editable yjsEditor={editor} /\u003e\n```\n\n---\n\n## Textbit.Element\n\nCan be used to wrap all elements in plugin components. Provides data state attribute used for styling.\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| children |  |  |\n\n### Data attribute\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-state] | \"active\" \\| \"inactive\" | The values \"active\" or \"inactive\" indicates whether the cursor is in the element or the element is part of a selection. |\n\n### Styling spelling errors\n\nWhen using the spellchecking functionality words (or combination of words) that are misspelled\nare rendered as `\u003cspan\u003e` child elements having the data attribute `data-spelling-error`. This\ncan be used to style all the spelling errors. See context menu handling for handling spelling errors\nin more detail.\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-spelling-error] | string | Id of individual spelling error |\n\n**Using a CSS style rule**\n\n```css\n[data-spelling-error] {\n  text-decoration: underline;\n  text-decoration-style: dotted;\n  text-decoration-color: rgb(239, 68, 68);\n}\n```\n\n**Using Tailwind**\n\n```JSX\nreturn (\n  \u003cTextbit.Editable\n    onSpellcheck={async (texts) =\u003e checkSpelling(texts)}\n    className=\"outline-none h-full dark:text-slate-100 [\u0026_[data-spelling-error]]:underline [\u0026_[data-spelling-error]]:decoration-dotted [\u0026_[data-spelling-error]]:decoration-red-500\"\n  \u003e\n    \u003cContextMenu /\u003e\n  \u003c/Textbit.Editable\u003e\n)\n```\n\n---\n\n## Textbit.DropMarker\n\nProvides a drop marker indicator. Handles positioning and displaying automatically. Provides a html data attribute to use for styling when dragOver is happening and what type of dragOver is wanted. If `data-dragover` is `between` a line should be displayed between elements. This is the default behaviour. If a plugin component has property `droppable` set to `true` the droppable marker will encompass the whole element component. The `data-dragover` attribute will be set to `around`.\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n\n### Data attribute\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-dragover] | \"none\" \\| \"between\" \\| \"around\" | True when dragover is active |\n\n## Textbit.Gutter\n\nProvides a gutter for the content tool menu. Handles positioning automatically. Allows placement to the left or right of the content area. Context is used internally. Has inline styling for size and relative positioning of children.\n\n---\n\n## Menu.Root\n\nRoot component for the Menu structure.\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string | |\n\n### Data attribute\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-state] | \"open\" \\| \"closed\" | |\n\n### Provides context (_used internally_)\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| isOpen | boolean | |\n| setIsOpen: | (boolean) =\u003e void | |\n\n## Menu.Trigger\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string | |\n\n## Menu.Content\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string | |\n\n## Menu.Group\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string | |\n\n## Menu.Item\n\n### Props\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| action | PluginRegistryAction | _Retrieved from hook usePluginRegistry()_ |\n\n### Data attribute\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-state] | \"active\" \\| \"inactive\" | Cursor or selection on content type. |\n\n### Provides context (_used internally_)\n\n| Name | Type |\n| ----------- | ----------- |\n| active | boolean |\n| action | PluginRegistryAction |\n\n### Example\n\n```jsx\nconst { actions } = usePluginRegistry()\n\n// ...\n\n{actions.filter(action =\u003e !['leaf', 'generic', 'inline'].includes(action.plugin.class)).map(action =\u003e {\n  \u003cMenu.Item\n    className=\"ct-item\"\n    key={`${action.plugin.class}-${action.plugin.name}-${action.title}`}\n    action={action}\n  \u003e\n    \u003cMenu.Icon className=\"ct-icon\" /\u003e\n    \u003cMenu.Label className=\"ct-label\"\u003e{action.title}\u003c/Menu.Label\u003e\n    \u003cMenu.Hotkey className=\"ct-hotkey\" /\u003e\n  \u003c/Menu.Item\u003e\n  })}\n```\n\n## Menu.Icon\n\nDisplay an icon in the menu item. Can be automatic or overridden by children.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| children |  | Optional. Overrides default action tool icon |\n\n## Menu.Label\n\nDisplay a label for the menu item. Can be automatic or overridden by children when for example different translations are needed.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| children |  | Optional. Overrides default label |\n\n## Menu.Hotkey\n\nDisplays a keyboard shortcut. If no children are provided it will automatically transform shortcuts from, for example, `mod+b` per platform to `ctrl+b` or `cmd+b`.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| children |  | Optional. Overrides default \"translation\" of action keyboard shortcut |\n\n---\n\n## Toolbar.Root\n\nRoot component around the context toolbox in the editor area providing access to tools like bold, links etc. Handles some style inline for hiding/showing the toolbox through manipulating _position_, _z-index_, _opacity_, _top_ and _left_.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n\n## Toolbar.Group\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n\n## Toolbar.Item\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| action | PluginRegistryAction | _Retrieved from hook usePluginRegistry()_ |\n\n### Data attribute\n\n| Name | Value | Description |\n| ----------- | ----------- | ----------- |\n| [data-state] | \"active\" \\| \"inactive\" | Cursor or selection on leaf or inline like bold, italic, link. |\n\n### Example\n\n```jsx\nconst { actions } = usePluginRegistry()\n\n// ...\n\n{actions.filter(action =\u003e ['inline'].includes(action.plugin.class)).map(action =\u003e {\n  \u003cToolbar.Item\n    className=\"ctx-item\"\n    action={action} key={`${action.plugin.class}-${action.plugin.name}-${action.title}`}\n  /\u003e\n})}\n```\n\n---\n\n## ContextMenu.Root\n\nRoot component around the context menu in the editor area providing a way to display spelling suggestions on spelling errors.\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n\n## ## ContextMenu.Group\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n\n## ## ContextMenu.Item\n\n| Name | Type | Description |\n| ----------- | ----------- | ----------- |\n| className | string |  |\n| func | () =\u003e void | Callback function to execute on click |\n\n### useContextMenuHints() hook\n\nProvides context click context hints, like which slate node, offset and spelling suggestions if they exist.\n\n```typescript\ninterface {\n    isOpen: boolean\n    position?: {\n      x: number,\n      y: number\n    }\n    target?: HTMLElement\n    event?: MouseEvent\n    nodeEntry?: NodeEntry\n    spelling?: {\n      text: string\n      suggestions: string[]\n      range?: Range\n      apply: (replacementString: string) =\u003e void\n    }\n  }\n```\n\n### Example\n\n```jsx\n\u003cContextMenu.Root className='textbit-contextmenu'\u003e\n  {!!spelling?.suggestions \u0026\u0026\n    \u003cContextMenu.Group className='textbit-contextmenu-group' key='spelling-suggestions'\u003e\n      {spelling.suggestions.map(suggestion =\u003e {\n        return (\n          \u003cContextMenu.Item\n            className='textbit-contextmenu-item'\n            key={suggestion}\n            func={() =\u003e {\n              spelling.apply(suggestion)\n            }}\n          \u003e\n            {suggestion}\n          \u003c/ContextMenu.Item\u003e\n        )\n      })}\n    \u003c/ContextMenu.Group\u003e\n  }\n\u003c/ContextMenu.Root\u003e\n```\n\n## Plugin development\n\nContent objects are handled by plugins. Plugins are defined by a structure which define hooks, render components and other parts of the plugin. Examples below should outline the general structure, they are not complete.\n\nPlugins can be either\n\n| class | |\n| ----------- | ----------- |\n| leaf | Bold, italic, etc. |\n| inline | Inline blocks in the text, like links. |\n| text | Normal text of various types. |\n| _textblock_ | **deprecated** _Use block instead._ |\n| block | Regular block elements like image, video. Automatically becomes draggable. |\n| void | Non editable objects, like a spinning loader. Should seldom be used.|\n| generic| Non rendered plugins. Like transforming input characters. |\n\n### Examples\n\n**Bold example**\n\n```jsx\nimport { BoldIcon } from 'lucide-react'\n\nconst Bold: Plugin.InitFunction = () =\u003e {\n  return {\n    class: 'leaf',\n    name: 'core/bold',\n    actions: [{\n      name: 'toggle-bold',\n      tool: () =\u003e \u003cBoldIcon style={{ width: '0.8em', height: '0.8em' }} /\u003e,\n      hotkey: 'mod+b',\n      handler: () =\u003e true\n    }],\n    getStyle: () =\u003e {\n      return 'font-bold'\n    }\n  }\n}\n```\n\n**Blockquote example**\n\n```jsx\nconst Blockquote: Plugin.InitFunction = () =\u003e {\n  return {\n    class: 'textblock',\n    name: 'core/blockquote',\n    actions: [\n      {\n        name: 'set-blockquote',\n        title: 'Blockquote',\n        tool: () =\u003e \u003cMessageSquareQuoteIcon style={{ width: '1em', height: '1em' }} /\u003e,\n        hotkey: 'mod+shift+2',\n        handler: actionHandler,\n        visibility: (element) =\u003e {\n          return [\n            true, // Always visible\n            true, // Always enabled\n            (element.type === 'core/blockquote') // Active when isBlockquote\n          ]\n        }\n      }\n    ],\n    componentEntry: {\n      class: 'textblock',\n      component: BlockquoteComponent,\n      constraints: {\n        normalizeNode: normalizeBlockquote // Render function for main/wrapper component\n      },\n      children: [\n        {\n          type: 'body',\n          class: 'text',\n          component: BlockquoteBody // Render the quote\n        },\n        {\n          type: 'caption',\n          class: 'text',\n          component: BlockquoteCaption // Render the caption\n        }\n      ]\n    }\n  }\n}\n```\n\n### Component rendering\n\nA component is used to render an element. One plugin can have many different child components and even allow other plugin components as children.\n\nEach component receives the props\n\n| class | Type | Description |\n| ----------- | ----------- | ----------- |\n| children | `TBElement[]` | Child components that should be rendered |\n| element | `TBElement` | The actual element being rendered |\n| rootNode | `TBElement` | If the rendered component is a child node, rootNode gives access to the topmost root node which carries properties etc |\n| options | `Record\u003cstring, unknown\u003e` | An object with plugin options provided at plugin instantiation |\n\nUsing the hook `useAction()` it is possible to call a named action defined in the plugin specification, including providing a argument object (`Record\u003cstring, unknown\u003e`).\n\n_If a child component is using a html element as its rendered root element (e.g `\u003ctr\u003e`, `\u003ctd\u003e`, etc) the child component must be defined as a ForwardedRef component. This allows Textbit to not add extra wrapper elements._\n\n**Example**\n\n```javascript\nimport { useAction, type Plugin } from '@ttab/textbit'\n\nexport const Factbox = ({ children, element }: Plugin.ComponentProps): JSX.Element =\u003e {\n  const setFactIsChecked = useAction('core/factbox', 'fact-is-checked') // Use a defined action in a specified plugin\n\n  return \u003c\u003e\n    \u003ca\n      href=\"#\"\n      contentEditable={false}\n      onMouseDown={event) =\u003e {\n        // Prevent href click\n        event.preventDefault()\n\n         // Call the specified action\n        setFactIsChecked({\n          state: true\n        })\n      }}\n    \u003e\n      Set is factchecked\n    \u003c/a\u003e\n    \u003cdiv\u003e\n      {children}\n    \u003c/div\u003e\n  \u003c/\u003e\n}\n```\n\n```javascript\nimport { forwardRef, type PropsWithChildren } from 'react'\n\nexport const TableRow = forwardRef\u003cHTMLTableRowElement, PropsWithChildren\u003e(({ children }, ref) =\u003e (\n  \u003ctr ref={ref}\u003e{children}\u003c/tr\u003e\n))\n\nTableRow.displayName = 'TableRow'\n```\n\n### TextbitElement\n\nThe format of an element, or content object, in Texbit is based on Slate Element. Note the use of `properties` which is used to carry data about the element as well as how formatting like bold, italic et al is added directly on the text node.\n\n**A text element of type 'h1'**\n\n```json\n{\n    type: 'core/text',\n    id: '538345e5-bacc-48f9-8ef1-a219891b60eb',\n    class: 'text',\n    properties: {\n      type: 'h1'\n    },\n    children: [\n      { text: 'Better music?' }\n    ]\n  }\n```\n\n**Example of bold/italic**\n\n```json\n{\n    type: 'core/text',\n    id: '538345e5-bacc-48f9-8ef0-1219891b60ef',\n    class: 'text',\n    children: [\n      { text: 'An example paragraph  with ' },\n      {\n        text: 'stronger',\n        'core/bold': true,\n        'core/italic': true\n      },\n      {\n        text: ' text.'\n      }\n    ]\n  }\n```\n\n**Image example**\n\n```json\n{\n  id: '538345e5-bacc-48f9-8ef0-1219891b60ef',\n  class: 'block',\n  type: 'core/image',\n  properties: {\n    type: 'image/png',\n    src: 'https://www...image.png',\n    title: 'My image',\n    size: '234300,\n    width: 1024,\n    height: 986\n  },\n  children: [\n    {\n      type: 'core/image/image',\n      class: 'text',\n      children: [{ text: '' }]\n    },\n    {\n      type: 'core/image/text',\n      class: 'text',\n      children: [{ text: 'An image of people taken 2001 in the countryside' }]\n    },\n    {\n      type: 'core/image/altText',\n      class: 'text',\n      children: [{ text: 'Three people by a tree' }]\n    }\n  ]\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fttab%2Ftextbit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fttab%2Ftextbit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fttab%2Ftextbit/lists"}