# @layers-app/editor-core

Public Plugin SDK for [`@layers-app/editor`](https://www.npmjs.com/package/@layers-app/editor). Use it to build first-party or third-party editor plugins.

> For most consumers this package is a **transitive dependency** of `@layers-app/editor` — you don't install it directly. Install it explicitly only when you're authoring a plugin.

## Install

```bash
npm install @layers-app/editor-core
```

Peer dependencies (install as needed): `react`, `react-dom`, `lexical`, `@lexical/react`, `@mantine/core`, `@mantine/hooks`, `@mantine/dropzone`, `@mantine/carousel`, `@hugeicons/react`, `@hugeicons/core-free-icons`, `@tabler/icons-react`, `@tanstack/react-query`, `i18next`, `react-i18next`, `lodash-es`.

## What's exported

### Plugin infrastructure types

- `PluginConfig` — full plugin contract consumed by `<Editor plugins={[...]} />`
- `SlashMenuConfig` — slash-menu entry for a plugin
- `PluginModeConfig` — which editor modes a plugin is enabled in (comment / default / editor)
- `UploadData`, `AlignmentType`, `MediaPlatform`

### Hooks

- `useChunkedUpload({ endpoints, ... })` — chunked file upload with resume/retry
- `useCancelUpload()` — AbortController helper shared by File, Images, Video uploaders

### UI primitives

- `ActionIcon`, `MobileDrawer`, `Select`, `SelectListOptionButton`
- `AlignmentMenu` — simple left/center/right picker
- `NodeAlignmentMenu` — wired-up menu for any Lexical node exposing `setAlignment`
- `ResizableContainer` — media block with resize handles, touch support, alignment-aware cursors
- Upload UI: `ActionButtons`, `ProgressBar`, `UploadLoaderIndicator`, `UploadProgressPanel`

### Media utilities

- `parseVideoURL(url, isLink?)` — recognizes YouTube, VK, Rutube, uploaded UUIDs
- `insertMediaNode(editor, node)` — basic Lexical-safe node insertion
- `deleteNode(editor, nodeOrKey)`
- `formatTime(seconds)` — `M:SS` / `H:MM:SS` formatting

### Environment & utilities

- `IS_APPLE`, `IS_IOS`, `IS_ANDROID`, `IS_SAFARI`, `IS_FIREFOX`, `IS_CHROME`, `IS_TOUCH_DEVICE`, `IS_HOVER_CAPABLE`, `IS_TOUCH_TABLET`
- `isMobile`, `CAN_USE_DOM`
- `getAllowedControls(alignment)` — resize handles allowed for a given alignment

## Building a plugin — step by step

A plugin is a `PluginConfig` object passed to `<Editor plugins={[yourPlugin(config)]} />`. Below is the minimum needed to ship one.

### 1. Define a Lexical node

Your plugin owns a custom Lexical node. This is what gets serialized into the editor state.

```ts
// node.ts
import { DecoratorNode, type LexicalCommand, type NodeKey, createCommand } from 'lexical';

export type CalendarPayload = { date: string };

export class CalendarNode extends DecoratorNode<JSX.Element> {
  __date: string;

  static getType(): string { return 'calendar'; }
  static clone(node: CalendarNode): CalendarNode { return new CalendarNode(node.__date, node.__key); }

  constructor(date: string, key?: NodeKey) {
    super(key);
    this.__date = date;
  }

  createDOM(): HTMLElement { return document.createElement('div'); }
  updateDOM(): false { return false; }

  decorate(): JSX.Element {
    return <CalendarBlock date={this.__date} />;
  }

  exportJSON() { return { type: 'calendar', date: this.__date, version: 1 }; }
  static importJSON(json: ReturnType<CalendarNode['exportJSON']>): CalendarNode {
    return new CalendarNode(json.date);
  }
}

export const INSERT_CALENDAR_COMMAND: LexicalCommand<CalendarPayload> =
  createCommand('INSERT_CALENDAR_COMMAND');
```

### 2. Write the plugin component

The component listens to commands and inserts your node.

```tsx
// CalendarPlugin.tsx
import { useEffect } from 'react';

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { COMMAND_PRIORITY_LOW } from 'lexical';

import { insertMediaNode } from '@layers-app/editor-core';

import { CalendarNode, INSERT_CALENDAR_COMMAND } from './node';

export default function CalendarPlugin(): null {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    if (!editor.hasNodes([CalendarNode])) {
      throw new Error('CalendarPlugin: CalendarNode not registered');
    }

    return editor.registerCommand(
      INSERT_CALENDAR_COMMAND,
      (payload) => {
        const node = new CalendarNode(payload.date);
        insertMediaNode(editor, node);
        return true;
      },
      COMMAND_PRIORITY_LOW,
    );
  }, [editor]);

  return null;
}
```

### 3. Wire up the factory

The factory returns the `PluginConfig` consumed by `<Editor>`.

```tsx
// factory.tsx
import { Calendar01Icon } from '@hugeicons/core-free-icons';
import { HugeiconsIcon } from '@hugeicons/react';

import type { PluginConfig } from '@layers-app/editor-core';

import CalendarPlugin from './CalendarPlugin';
import { CalendarNode, INSERT_CALENDAR_COMMAND } from './node';

export type CalendarPluginConfig = {
  /* config fields available to your component via context */
  defaultDate?: string;
};

export function calendarPlugin(config: CalendarPluginConfig): PluginConfig {
  return {
    key: 'calendar',
    node: CalendarNode,
    Plugin: CalendarPlugin,
    Provider: ({ children }) => <>{children}</>, // or your own context provider
    modes: { comment: false, default: true, editor: true },
    slashMenu: {
      parentKey: 'media',
      name: 'editor.calendar.name',
      description: 'editor.calendar.description',
      icon: <HugeiconsIcon icon={Calendar01Icon} size={16} />,
      keywords: ['calendar', 'date', 'event'],
      command: INSERT_CALENDAR_COMMAND,
      commandPayload: new Date().toISOString().slice(0, 10),
    },
  };
}
```

### 4. Use it in a host application

```tsx
import { Editor } from '@layers-app/editor';
import { calendarPlugin } from '@your-org/editor-calendar';

const config = useMemo(() => calendarPlugin({ defaultDate: '2026-01-01' }), []);

<Editor plugins={[config]} ... />
```

## Reference implementation

[`@layers-app/editor-video`](https://www.npmjs.com/package/@layers-app/editor-video) is the canonical reference — a fully-featured plugin (chunked upload, custom player UI, settings modal, subtitles, chapters) built using only `@layers-app/editor-core`. Its source is the best example of how to use everything in this SDK.

## Conventions

- **Node `getType()`** must be unique across all plugins in the editor.
- **Command names** (e.g. `INSERT_CALENDAR_COMMAND`) should be plugin-prefixed.
- **i18n keys** live in the host application; the plugin only references key names.
- **Icons**: import from `@hugeicons/core-free-icons` + `@hugeicons/react`.
- **Styles**: scoped to the plugin's components; if using SCSS, bundle via the package itself.

## Architectural constraint

editor-core must **never** import from `@layers-app/editor`. Plugins that follow this constraint can be developed and shipped independently of the editor source.

Cross-plugin imports are also discouraged — share via `editor-core` instead.

## License

MIT
