diff --git a/README.md b/README.md index 571687a..d889cfc 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ A **Pagination Table** & **Scroll List** component suite for [CRUD operation][1] 14. [Scroll Boundary](https://mobx-restful-shadcn.idea2.app/) 15. [Scroll List](https://mobx-restful-shadcn.idea2.app/) 16. [Searchable Input](https://mobx-restful-shadcn.idea2.app/) +17. [Editor](https://mobx-restful-shadcn.idea2.app/) ## Installation @@ -195,6 +196,37 @@ export const EditorPage = () => ( ); ``` +### Editor + +```tsx +import { configure } from "mobx"; +import { formToJSON } from "web-utility"; + +import { Editor, OriginalTools, ExtraTools } from "@/components/ui/editor"; + +configure({ enforceActions: "never" }); + +export const EditorPage = () => ( +
{ + event.preventDefault(); + + const { content } = formToJSON(event.currentTarget); + + alert(content); + }} + > + + + +); +``` + ## Development This is a custom component registry built with Next.js and compatible with the `shadcn` CLI. diff --git a/app/page.tsx b/app/page.tsx index a6e9175..46e69fa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,4 @@ import { ComponentCard } from "@/components/example/component-card"; -import { HelloWorld } from "@/registry/new-york/blocks/hello-world/hello-world"; -import { ExampleForm } from "@/registry/new-york/blocks/example-form/example-form"; -import PokemonPage from "@/registry/new-york/blocks/complex-component/page"; -import { ExampleCard } from "@/registry/new-york/blocks/example-with-css/example-card"; import { BadgeBarExample } from "@/registry/new-york/blocks/badge-bar/example"; import { PagerExample } from "@/registry/new-york/blocks/pager/example"; import { ImagePreviewExample } from "@/registry/new-york/blocks/image-preview/example"; @@ -15,46 +11,20 @@ import { RangeInputExample } from "@/registry/new-york/blocks/range-input/exampl import { FilePickerExample } from "@/registry/new-york/blocks/file-picker/example"; import { FormFieldExample } from "@/registry/new-york/blocks/form-field/example"; import { RestTableExample } from "@/registry/new-york/blocks/rest-table/example"; +import { EditorExample } from "@/registry/new-york/blocks/editor/example"; export default function Home() { return (
-

Custom Registry

+

+ MobX-RESTful-Shadcn Registry +

A custom registry for distributing code using shadcn.

- - - - - - - - - - - - - - - - + + + +
); diff --git a/package.json b/package.json index a5f2fc5..1365f88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-restful-shadcn", - "version": "1.5.0", + "version": "1.6.0", "private": true, "scripts": { "postinstall": "shadcn-helper install", @@ -16,6 +16,7 @@ "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "edkit": "^1.2.7", "lodash.debounce": "^4.0.8", "lucide-react": "^0.562.0", "mobx": "^6.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a69a97..f0cc8f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + edkit: + specifier: ^1.2.7 + version: 1.2.7(typescript@5.9.3) lodash.debounce: specifier: ^4.0.8 version: 4.0.8 @@ -581,6 +584,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@modelcontextprotocol/sdk@1.25.1': resolution: {integrity: sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==} engines: {node: '>=18'} @@ -1082,6 +1088,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/turndown@5.0.6': + resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1358,8 +1367,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.10: - resolution: {integrity: sha512-2VIKvDx8Z1a9rTB2eCkdPE5nSe28XnA+qivGnWHoB40hMMt/h1hSz0960Zqsn6ZyxWXUie0EBdElKv8may20AA==} + baseline-browser-mapping@2.9.11: + resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true body-parser@2.2.1: @@ -1376,6 +1385,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-fs-access@0.37.0: + resolution: {integrity: sha512-MKpvZrKtv6pBJ2ACd+VwfS9XauBKTMVZg2UBibypuK1gfiXM7euZjbdKmvRsyxeQRhfzNVQrzCSVGXs19/LP8Q==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1408,8 +1420,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001760: - resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + caniuse-lite@1.0.30001761: + resolution: {integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1619,6 +1631,9 @@ packages: resolution: {integrity: sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} + edkit@1.2.7: + resolution: {integrity: sha512-dCOBN9MMbCaCdSqhnZTSHPe7lu53TQttttjVBxLE/TehsQasuxmqW3ckimVODFaJVci1A6w429j9bebpiU3zKg==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2490,6 +2505,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3267,6 +3287,12 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turndown-plugin-gfm@1.0.2: + resolution: {integrity: sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg==} + + turndown@7.2.2: + resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==} + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -3983,6 +4009,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mixmark-io/domino@2.2.0': {} + '@modelcontextprotocol/sdk@1.25.1(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.7 @@ -4419,6 +4447,8 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/turndown@5.0.6': {} + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -4711,7 +4741,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.9.10: {} + baseline-browser-mapping@2.9.11: {} body-parser@2.2.1: dependencies: @@ -4740,10 +4770,12 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-fs-access@0.37.0: {} + browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.10 - caniuse-lite: 1.0.30001760 + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -4778,7 +4810,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001760: {} + caniuse-lite@1.0.30001761: {} chalk@4.1.2: dependencies: @@ -4952,6 +4984,20 @@ snapshots: '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 + edkit@1.2.7(typescript@5.9.3): + dependencies: + '@swc/helpers': 0.5.17 + '@types/turndown': 5.0.6 + browser-fs-access: 0.37.0 + marked: 15.0.12 + regenerator-runtime: 0.14.1 + turndown: 7.2.2 + turndown-plugin-gfm: 1.0.2 + web-utility: 4.6.4(typescript@5.9.3) + transitivePeerDependencies: + - element-internals-polyfill + - typescript + ee-first@1.1.1: {} electron-to-chromium@1.5.267: {} @@ -5940,6 +5986,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@15.0.12: {} + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -6092,8 +6140,8 @@ snapshots: dependencies: '@next/env': 16.1.0 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.10 - caniuse-lite: 1.0.30001760 + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -6858,6 +6906,12 @@ snapshots: tslib@2.8.1: {} + turndown-plugin-gfm@1.0.2: {} + + turndown@7.2.2: + dependencies: + '@mixmark-io/domino': 2.2.0 + tw-animate-css@1.4.0: {} type-check@0.4.0: diff --git a/registry.json b/registry.json index 4cb3e20..097e3d2 100644 --- a/registry.json +++ b/registry.json @@ -1,60 +1,14 @@ { "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "MobX RESTful Shadcn", + "name": "mobx-restful-shadcn", "homepage": "https://mobx-restful-shadcn.idea2.app", "items": [ - { - "name": "complex-component", - "type": "registry:component", - "title": "Complex Component", - "description": "A complex component showing hooks, libs and components.", - "registryDependencies": ["card"], - "files": [ - { - "path": "registry/new-york/blocks/complex-component/page.tsx", - "type": "registry:page", - "target": "app/pokemon/page.tsx" - }, - { - "path": "registry/new-york/blocks/complex-component/components/pokemon-card.tsx", - "type": "registry:component" - }, - { - "path": "registry/new-york/blocks/complex-component/components/pokemon-image.tsx", - "type": "registry:component" - }, - { - "path": "registry/new-york/blocks/complex-component/lib/pokemon.ts", - "type": "registry:lib" - }, - { - "path": "registry/new-york/blocks/complex-component/hooks/use-pokemon.ts", - "type": "registry:hook" - } - ] - }, - { - "name": "example-with-css", - "type": "registry:component", - "title": "Example with CSS", - "description": "A login form with a CSS file.", - "files": [ - { - "path": "registry/new-york/blocks/example-with-css/example-card.tsx", - "type": "registry:component" - }, - { - "path": "registry/new-york/blocks/example-with-css/example-card.css", - "type": "registry:component" - } - ] - }, { "name": "badge-bar", "type": "registry:component", "title": "Badge Bar", "description": "A component for displaying a list of badges with optional click and delete handlers.", - "registryDependencies": ["badge"], + "registryDependencies": ["badge", "@mobx-restful-shadcn/badge-bar"], "dependencies": ["lucide-react", "web-utility"], "files": [ { @@ -101,7 +55,7 @@ "type": "registry:component", "title": "File Preview", "description": "A file preview component supporting images, audio, video, and documents.", - "registryDependencies": ["dialog"], + "registryDependencies": ["@mobx-restful-shadcn/image-preview"], "dependencies": ["lucide-react"], "files": [ { @@ -134,6 +88,7 @@ "mobx-restful", "lodash.debounce" ], + "registryDependencies": ["@mobx-restful-shadcn/scroll-boundary"], "files": [ { "path": "registry/new-york/blocks/scroll-list/scroll-list.tsx", @@ -167,7 +122,7 @@ "type": "registry:component", "title": "Badge Input", "description": "An input component that displays values as removable badges, supporting multiple entries.", - "registryDependencies": ["badge"], + "registryDependencies": ["@mobx-restful-shadcn/badge-bar"], "dependencies": ["mobx-react", "mobx-react-helper", "web-utility"], "files": [ { @@ -194,7 +149,7 @@ "type": "registry:component", "title": "File Picker", "description": "A file picker component with preview and remove functionality.", - "registryDependencies": ["button"], + "registryDependencies": ["button", "@mobx-restful-shadcn/file-preview"], "dependencies": [ "lucide-react", "mobx", @@ -228,7 +183,7 @@ "type": "registry:component", "title": "File Uploader", "description": "A file uploader component with drag-and-drop support for managing multiple files using MobX.", - "registryDependencies": ["file-picker"], + "registryDependencies": ["@mobx-restful-shadcn/file-picker"], "dependencies": [ "mobx", "mobx-react", @@ -342,6 +297,63 @@ "type": "registry:component" } ] + }, + { + "name": "editor", + "type": "registry:component", + "title": "HTML Editor", + "description": "A lightweight rich text editor based on Edkit and Shadcn UI with various formatting tools.", + "registryDependencies": ["button"], + "dependencies": [ + "edkit", + "lucide-react", + "mobx", + "mobx-react", + "mobx-react-helper", + "web-utility" + ], + "files": [ + { + "path": "registry/new-york/blocks/editor/index.ts", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/editor.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tool.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/index.ts", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/text.ts", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/layout.ts", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/control.ts", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/media.ts", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/color.tsx", + "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/editor/tools/extra.ts", + "type": "registry:component" + } + ] } ] } diff --git a/registry/new-york/blocks/complex-component/components/pokemon-card.tsx b/registry/new-york/blocks/complex-component/components/pokemon-card.tsx deleted file mode 100644 index 2eed576..0000000 --- a/registry/new-york/blocks/complex-component/components/pokemon-card.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { cache } from "react" -import { getPokemon } from "@/registry/new-york/blocks/complex-component/lib/pokemon" -import { Card, CardContent } from "@/registry/new-york/ui/card" -import { PokemonImage } from "@/registry/new-york/blocks/complex-component/components/pokemon-image" - -const cachedGetPokemon = cache(getPokemon) - -export async function PokemonCard({ name }: { name: string }) { - const pokemon = await cachedGetPokemon(name) - - if (!pokemon) { - return null - } - - return ( - - -
- -
-
{pokemon.name}
-
-
- ) -} diff --git a/registry/new-york/blocks/complex-component/components/pokemon-image.tsx b/registry/new-york/blocks/complex-component/components/pokemon-image.tsx deleted file mode 100644 index a3d2461..0000000 --- a/registry/new-york/blocks/complex-component/components/pokemon-image.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client" - -/* eslint-disable @next/next/no-img-element */ -import { usePokemonImage } from "@/registry/new-york/blocks/complex-component/hooks/use-pokemon" - -export function PokemonImage({ - name, - number, -}: { - name: string - number: number -}) { - const imageUrl = usePokemonImage(number) - - if (!imageUrl) { - return null - } - - return {name} -} diff --git a/registry/new-york/blocks/complex-component/hooks/use-pokemon.ts b/registry/new-york/blocks/complex-component/hooks/use-pokemon.ts deleted file mode 100644 index 0201b96..0000000 --- a/registry/new-york/blocks/complex-component/hooks/use-pokemon.ts +++ /dev/null @@ -1,7 +0,0 @@ -"use client" - -// Totally unnecessary hook, but it's a good example of how to use a hook in a custom registry. - -export function usePokemonImage(number: number) { - return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png` -} diff --git a/registry/new-york/blocks/complex-component/lib/pokemon.ts b/registry/new-york/blocks/complex-component/lib/pokemon.ts deleted file mode 100644 index 9aa12f9..0000000 --- a/registry/new-york/blocks/complex-component/lib/pokemon.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { z } from "zod" - -export async function getPokemonList({ limit = 10 }: { limit?: number }) { - try { - const response = await fetch( - `https://pokeapi.co/api/v2/pokemon?limit=${limit}` - ) - return z - .object({ - results: z.array(z.object({ name: z.string() })), - }) - .parse(await response.json()) - } catch (error) { - console.error(error) - return null - } -} - -export async function getPokemon(name: string) { - try { - const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`) - - if (!response.ok) { - throw new Error("Failed to fetch pokemon") - } - - return z - .object({ - name: z.string(), - id: z.number(), - sprites: z.object({ - front_default: z.string(), - }), - stats: z.array( - z.object({ - base_stat: z.number(), - stat: z.object({ - name: z.string(), - }), - }) - ), - }) - .parse(await response.json()) - } catch (error) { - console.error(error) - return null - } -} diff --git a/registry/new-york/blocks/complex-component/page.tsx b/registry/new-york/blocks/complex-component/page.tsx deleted file mode 100644 index bd202d3..0000000 --- a/registry/new-york/blocks/complex-component/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { cache } from "react" -import { PokemonCard } from "@/registry/new-york/blocks/complex-component/components/pokemon-card" -import { getPokemonList } from "@/registry/new-york/blocks/complex-component/lib/pokemon" -const getCachedPokemonList = cache(getPokemonList) - -export default async function Page() { - const pokemons = await getCachedPokemonList({ limit: 12 }) - - if (!pokemons) { - return null - } - - return ( -
-
- {pokemons.results.map((p) => ( - - ))} -
-
- ) -} diff --git a/registry/new-york/blocks/editor/editor.tsx b/registry/new-york/blocks/editor/editor.tsx new file mode 100644 index 0000000..8c4841a --- /dev/null +++ b/registry/new-york/blocks/editor/editor.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { EditorComponent, ImageTool, Tool, editor } from "edkit"; +import { computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { FormComponent, FormComponentProps } from "mobx-react-helper"; +import { createRef } from "react"; +import { Constructor } from "web-utility"; + +import { cn } from "@/lib/utils"; +import { AudioTool, DefaultTools, VideoTool } from "./tools"; + +export interface EditorProps extends FormComponentProps { + className?: string; + tools?: Constructor[]; +} + +export interface Editor extends EditorComponent {} + +@observer +@editor +export class Editor + extends FormComponent + implements EditorComponent +{ + static displayName = "Editor"; + + box = createRef(); + + @observable + accessor cursorPoint = ""; + + @computed + get toolList(): Tool[] { + return (this.observedProps.tools || DefaultTools).map( + (ToolButton) => new ToolButton() + ); + } + + @computed + get imageTool() { + return this.toolList.find((tool) => tool instanceof ImageTool) as ImageTool; + } + + @computed + get audioTool() { + return this.toolList.find((tool) => tool instanceof AudioTool) as AudioTool; + } + + @computed + get videoTool() { + return this.toolList.find((tool) => tool instanceof VideoTool) as VideoTool; + } + + componentDidMount() { + super.componentDidMount(); + + const { defaultValue } = this.props; + + if (defaultValue != null) this.box.current!.innerHTML = defaultValue; + + document.addEventListener("selectionchange", this.updateTools); + } + + componentWillUnmount() { + super.componentWillUnmount(); + + document.removeEventListener("selectionchange", this.updateTools); + } + + updateTools = () => { + if (this.box.current !== document.activeElement) return; + + const { endContainer } = getSelection()?.getRangeAt(0) || {}; + const { x, y } = + (endContainer instanceof Element + ? endContainer + : endContainer?.parentElement + )?.getBoundingClientRect() || {}; + + this.cursorPoint = [x, y] + ""; + }; + + updateValue = (markup: string) => (this.innerValue = markup.trim()); + + render() { + // Don't remove unused variable `cursorPoint`, which is used for triggering updates + const { cursorPoint, toolList, innerValue } = this; + const { name, className } = this.props; + + return ( + <> +
+ {toolList.map((tool) => tool.render(this.box))} +
+
+ this.updateValue(innerHTML) + } + onPaste={this.handlePasteDrop} + onDrop={this.handlePasteDrop} + /> + + + ); + } +} diff --git a/registry/new-york/blocks/editor/example.tsx b/registry/new-york/blocks/editor/example.tsx new file mode 100644 index 0000000..e715fa5 --- /dev/null +++ b/registry/new-york/blocks/editor/example.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { configure } from "mobx"; +import { formToJSON } from "web-utility"; + +import { Button } from "@/components/ui/button"; +import { Editor, OriginalTools, ExtraTools } from "./index"; + +configure({ enforceActions: "never" }); + +export const EditorExample = () => ( +
{ + event.preventDefault(); + + const { content } = formToJSON(event.currentTarget); + + alert(content); + }} + > +
+
+

HTML Editor

+

+ A lightweight rich text editor based on Edkit and Shadcn UI +

+
+ + + +
+
+); diff --git a/registry/new-york/blocks/editor/index.ts b/registry/new-york/blocks/editor/index.ts new file mode 100644 index 0000000..8cf5f0a --- /dev/null +++ b/registry/new-york/blocks/editor/index.ts @@ -0,0 +1,3 @@ +export * from "./tool"; +export * from "./tools"; +export * from "./editor"; diff --git a/registry/new-york/blocks/editor/tool.tsx b/registry/new-york/blocks/editor/tool.tsx new file mode 100644 index 0000000..d942acc --- /dev/null +++ b/registry/new-york/blocks/editor/tool.tsx @@ -0,0 +1,33 @@ +import { ComponentType, RefObject, SVGProps } from "react"; +import { Tool } from "edkit"; +import * as Icons from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +export function renderTool(this: Tool, editor: RefObject) { + const { title, active, icon, usable } = this; + + const IconComponent = + (Icons[icon as keyof typeof Icons] as ComponentType< + SVGProps + >) || Icons.Circle; + + return ( + + ); +} diff --git a/registry/new-york/blocks/editor/tools/color.tsx b/registry/new-york/blocks/editor/tools/color.tsx new file mode 100644 index 0000000..6664c3b --- /dev/null +++ b/registry/new-york/blocks/editor/tools/color.tsx @@ -0,0 +1,100 @@ +import { FC, RefObject } from "react"; +import { + ColorName, + ColorTool, + ForeColorTool as FCT, + BackColorTool as BCT, +} from "edkit"; +import { Type, FileText } from "lucide-react"; + +import { Button } from "@/components/ui/button"; + +export interface ColorSelectorProps + extends Partial> { + icon: "Type" | "FileText"; + type: ColorName; + onChange?: (color: string) => any; +} + +export const ColorSelector: FC = ({ + className = "", + title, + type, + value, + onChange, + icon, +}) => ( + + onChange?.(value)} + /> + + +); + +export function renderColorTool( + this: ColorTool, + editor: RefObject +) { + const { icon, name, colorName } = this; + + return ( + + editor.current && this.execute(editor.current, color) + } + /> + ); +} + +export class ForeColorTool extends FCT { + icon = "Type"; + render = renderColorTool; +} + +export class BackColorTool extends BCT { + icon = "FileText"; + render = renderColorTool; +} diff --git a/registry/new-york/blocks/editor/tools/control.ts b/registry/new-york/blocks/editor/tools/control.ts new file mode 100644 index 0000000..46b2e61 --- /dev/null +++ b/registry/new-york/blocks/editor/tools/control.ts @@ -0,0 +1,28 @@ +import { + UndoTool as UDT, + RedoTool as RDT, + ResetTool as RST, + ClearTool as CT, +} from "edkit"; + +import { renderTool } from "../tool"; + +export class UndoTool extends UDT { + icon = "Undo"; + render = renderTool; +} + +export class RedoTool extends RDT { + icon = "Redo"; + render = renderTool; +} + +export class ResetTool extends RST { + icon = "Eraser"; + render = renderTool; +} + +export class ClearTool extends CT { + icon = "X"; + render = renderTool; +} diff --git a/registry/new-york/blocks/editor/tools/extra.ts b/registry/new-york/blocks/editor/tools/extra.ts new file mode 100644 index 0000000..c488da7 --- /dev/null +++ b/registry/new-york/blocks/editor/tools/extra.ts @@ -0,0 +1,8 @@ +import { CopyMarkdownTool as CMDT } from "edkit"; + +import { renderTool } from "../tool"; + +export class CopyMarkdownTool extends CMDT { + icon = "FileText"; + render = renderTool; +} diff --git a/registry/new-york/blocks/editor/tools/index.ts b/registry/new-york/blocks/editor/tools/index.ts new file mode 100644 index 0000000..a20f282 --- /dev/null +++ b/registry/new-york/blocks/editor/tools/index.ts @@ -0,0 +1,95 @@ +import { + BoldTool, + ItalicTool, + UnderlineTool, + StrikeThroughTool, + H1Tool, + H2Tool, + H3Tool, + FontSizeDownTool, + FontSizeUpTool, + SubscriptTool, + SuperscriptTool, + LinkTool, +} from "./text"; +import { ForeColorTool, BackColorTool } from "./color"; +import { + AlignLeftTool, + AlignCenterTool, + AlignRightTool, + AlignFullTool, + OrderedListTool, + UnorderedListTool, + HorizontalRuleTool, +} from "./layout"; +import { IFrameTool, ImageTool, AudioTool, VideoTool } from "./media"; +import { UndoTool, RedoTool, ResetTool, ClearTool } from "./control"; +import { CopyMarkdownTool } from "./extra"; + +export * from "./text"; +export * from "./color"; +export * from "./layout"; +export * from "./media"; +export * from "./control"; +export * from "./extra"; + +export const TextTools = [ + BoldTool, + ItalicTool, + UnderlineTool, + StrikeThroughTool, + H1Tool, + H2Tool, + H3Tool, + FontSizeDownTool, + FontSizeUpTool, + SubscriptTool, + SuperscriptTool, + LinkTool, +]; +export const ColorTools = [ForeColorTool, BackColorTool]; +export const LayoutTools = [ + AlignLeftTool, + AlignCenterTool, + AlignRightTool, + AlignFullTool, + OrderedListTool, + UnorderedListTool, + HorizontalRuleTool, +]; +export const MediaTools = [IFrameTool, ImageTool, AudioTool, VideoTool]; +export const ControlTools = [UndoTool, RedoTool, ResetTool, ClearTool]; +export const ExtraTools = [CopyMarkdownTool]; + +export const OriginalTools = [ + ...TextTools, + ...ColorTools, + ...LayoutTools, + ...MediaTools, + ...ControlTools, +]; + +export const DefaultTools = [ + BoldTool, + ItalicTool, + UnderlineTool, + StrikeThroughTool, + H1Tool, + H2Tool, + H3Tool, + SubscriptTool, + SuperscriptTool, + ForeColorTool, + BackColorTool, + AlignLeftTool, + AlignCenterTool, + AlignRightTool, + AlignFullTool, + OrderedListTool, + UnorderedListTool, + HorizontalRuleTool, + ImageTool, + UndoTool, + RedoTool, + ClearTool, +]; diff --git a/registry/new-york/blocks/editor/tools/layout.ts b/registry/new-york/blocks/editor/tools/layout.ts new file mode 100644 index 0000000..bab0d11 --- /dev/null +++ b/registry/new-york/blocks/editor/tools/layout.ts @@ -0,0 +1,46 @@ +import { + AlignLeftTool as ALT, + AlignCenterTool as ACT, + AlignRightTool as ART, + AlignFullTool as AFT, + OrderedListTool as OLT, + UnorderedListTool as ULT, + HorizontalRuleTool as HRT, +} from "edkit"; + +import { renderTool } from "../tool"; + +export class AlignLeftTool extends ALT { + icon = "AlignLeft"; + render = renderTool; +} + +export class AlignCenterTool extends ACT { + icon = "AlignCenter"; + render = renderTool; +} + +export class AlignRightTool extends ART { + icon = "AlignRight"; + render = renderTool; +} + +export class AlignFullTool extends AFT { + icon = "AlignJustify"; + render = renderTool; +} + +export class OrderedListTool extends OLT { + icon = "ListOrdered"; + render = renderTool; +} + +export class UnorderedListTool extends ULT { + icon = "List"; + render = renderTool; +} + +export class HorizontalRuleTool extends HRT { + icon = "Minus"; + render = renderTool; +} diff --git a/registry/new-york/blocks/editor/tools/media.ts b/registry/new-york/blocks/editor/tools/media.ts new file mode 100644 index 0000000..6585aa3 --- /dev/null +++ b/registry/new-york/blocks/editor/tools/media.ts @@ -0,0 +1,28 @@ +import { + IFrameTool as FT, + ImageTool as IT, + AudioTool as AT, + VideoTool as VT, +} from "edkit"; + +import { renderTool } from "../tool"; + +export class IFrameTool extends FT { + icon = "Frame"; + render = renderTool; +} + +export class ImageTool extends IT { + icon = "Image"; + render = renderTool; +} + +export class AudioTool extends AT { + icon = "Mic"; + render = renderTool; +} + +export class VideoTool extends VT { + icon = "Video"; + render = renderTool; +} diff --git a/registry/new-york/blocks/editor/tools/text.ts b/registry/new-york/blocks/editor/tools/text.ts new file mode 100644 index 0000000..1f3d345 --- /dev/null +++ b/registry/new-york/blocks/editor/tools/text.ts @@ -0,0 +1,76 @@ +import { + BoldTool as BT, + ItalicTool as IT, + UnderlineTool as UT, + StrikeThroughTool as STT, + H1Tool as H1T, + H2Tool as H2T, + H3Tool as H3T, + FontSizeDownTool as FSDT, + FontSizeUpTool as FSUT, + SubscriptTool as SubST, + SuperscriptTool as SupST, + LinkTool as LT, +} from "edkit"; + +import { renderTool } from "../tool"; + +export class BoldTool extends BT { + icon = "Bold"; + render = renderTool; +} + +export class ItalicTool extends IT { + icon = "Italic"; + render = renderTool; +} + +export class UnderlineTool extends UT { + icon = "Underline"; + render = renderTool; +} + +export class StrikeThroughTool extends STT { + icon = "Strikethrough"; + render = renderTool; +} + +export class H1Tool extends H1T { + icon = "Heading1"; + render = renderTool; +} + +export class H2Tool extends H2T { + icon = "Heading2"; + render = renderTool; +} + +export class H3Tool extends H3T { + icon = "Heading3"; + render = renderTool; +} + +export class FontSizeDownTool extends FSDT { + icon = "ArrowDownAZ"; + render = renderTool; +} + +export class FontSizeUpTool extends FSUT { + icon = "ArrowUpAZ"; + render = renderTool; +} + +export class SubscriptTool extends SubST { + icon = "ArrowDownRight"; + render = renderTool; +} + +export class SuperscriptTool extends SupST { + icon = "ArrowUpRight"; + render = renderTool; +} + +export class LinkTool extends LT { + icon = "Link"; + render = renderTool; +} diff --git a/registry/new-york/blocks/example-form/example-form.tsx b/registry/new-york/blocks/example-form/example-form.tsx deleted file mode 100644 index 6b6bb9f..0000000 --- a/registry/new-york/blocks/example-form/example-form.tsx +++ /dev/null @@ -1,164 +0,0 @@ -"use client" - -import * as React from "react" -import { - Card, - CardTitle, - CardHeader, - CardDescription, - CardContent, - CardFooter, -} from "@/registry/new-york/ui/card" -import { Input } from "@/registry/new-york/ui/input" -import { Label } from "@/registry/new-york/ui/label" -import { Button } from "@/registry/new-york/ui/button" -import { Textarea } from "@/registry/new-york/ui/textarea" -import { z } from "zod" - -const exampleFormSchema = z.object({ - name: z.string().min(1), - email: z.string().email(), - message: z.string().min(1), -}) - -export function ExampleForm() { - const [pending, setPending] = React.useState(false) - const [state, setState] = React.useState({ - defaultValues: { - name: "", - email: "", - message: "", - }, - success: false, - errors: { - name: "", - email: "", - message: "", - }, - }) - - const handleSubmit = React.useCallback( - (e: React.FormEvent) => { - e.preventDefault() - setPending(true) - - const formData = new FormData(e.target as HTMLFormElement) - const data = Object.fromEntries(formData.entries()) - const result = exampleFormSchema.safeParse(data) - - if (!result.success) { - setState({ - ...state, - errors: Object.fromEntries( - Object.entries(result.error.flatten().fieldErrors).map( - ([key, value]) => [key, value?.[0] ?? ""] - ) - ) as Record, - }) - setPending(false) - return - } - - setPending(false) - }, - [state] - ) - - return ( -
- - - How can we help? - - Need help with your project? We're here to assist you. - - - -
- - - {state.errors?.name && ( -

- {state.errors.name} -

- )} -
-
- - - {state.errors?.email && ( -

- {state.errors.email} -

- )} -
-
- -