# @joydea/rich-editor

Notion 風の Markdown リッチエディタ（TipTap / ProseMirror 製）。ツールバー無しで、行頭の「＋」や
「/」でブロック挿入、選択でバブルメニュー、Markdown ショートカット、画像のドラッグリサイズ、GFM テーブル、
@メンション、下書き自動保存（localStorage 復元）に対応します。

- **フレームワーク非依存のコア**（`textarea` を渡すだけ）＋ **Laravel(Blade) 同梱**（`<x-rich-editor>`）
- 保存形式は **Markdown**（`textarea` の値に書き戻される）
- relay / backlog-clone から切り出した独立パッケージ。**backlog-clone 本体は変更していません**

---

## 🚀 配信中（社内 Composer レジストリ）

**`v0.1.1` を社内 Composer レジストリで配信しています。**

- レジストリ: **https://packages.joydea.jp**
- 実体: 自社 VPS 上の [Satis](https://github.com/composer/satis) 静的レジストリ（infra Caddy が配信）
- 配布元: `HIROLAB/rich-editor`（Backlog git）のタグ。パッケージ本体は tar 同梱配信なので、**利用側は Backlog git への直アクセス不要**
- 認証: 現状 **無し**（URL を知っていれば pull 可）

他プロジェクトは以下を `composer.json` に足すだけで入ります（詳細は下記インストール A）。

```jsonc
"repositories": [ { "type": "composer", "url": "https://packages.joydea.jp" } ]
```
```bash
composer require joydea/rich-editor:^0.1
```

---

## インストール

### A. 社内 Composer レジストリから（配信版・推奨）

利用側プロジェクトの `composer.json` にレジストリを追加して require します。

```jsonc
{
  "repositories": [
    { "type": "composer", "url": "https://packages.joydea.jp" }
  ]
}
```
```bash
composer require joydea/rich-editor:^0.1

php artisan vendor:publish --tag=rich-editor-assets   # dist を public/vendor/rich-editor へ
# （任意）php artisan vendor:publish --tag=rich-editor-config
# （任意）php artisan vendor:publish --tag=rich-editor-views
```

ServiceProvider は Laravel のパッケージ自動検出で読み込まれます。以降は「Laravel で使う」へ。

### B. clone してパス参照（オフライン / ローカル改造したいとき）

レジストリを使わず、リポジトリを clone してパス参照でも使えます。

```jsonc
"repositories": [ { "type": "path", "url": "../rich-editor" } ]   // clone した場所
```
```bash
git clone https://joydea.backlog.jp/git/HIROLAB/rich-editor.git rich-editor
composer require joydea/rich-editor:@dev          # path 参照は dev 版なので @dev
php artisan vendor:publish --tag=rich-editor-assets
```

> `dist/`（ビルド済み JS/CSS）は **リポジトリにコミット済み** なので、clone しただけで動きます。
> コードを改造したら `npm install && npm run build` で `dist/` を再生成し、`vendor:publish --tag=rich-editor-assets --force` で反映。

---

## Laravel で使う（Blade）

```blade
{{-- 最小 --}}
<x-rich-editor name="description" :value="old('description', $issue->description)" />

{{-- 画像アップロード＋メンション＋下書きキー --}}
<x-rich-editor
    name="body"
    :value="old('body')"
    placeholder="説明を入力…"
    :upload-url="route('images.store', $project)"
    :mentions="$members"                {{-- [['name'=>'田中','avatar'=>'/a.png','initials'=>'田'], ...] --}}
    min-height="180px"
    :draft-key="'issue:'.$project->id.':'.($issue->id ?? 'new').':body'"
/>
```

`config('rich-editor.inject_assets')` が `true`（既定）なら、コンポーネントの初回描画で CSS/JS を
1 度だけ読み込みます。`@vite` などで自前管理したい場合は `false` にしてください。

### 画像アップロードの契約

`upload-url` へ `multipart/form-data`（フィールド名 `file`）を CSRF 付き POST します。サーバは
`{ "url": "...", "alt": "..." }` を JSON で返してください（`meta[name=csrf-token]` を自動送出）。

---

## 素の JS / 他フレームワークで使う

> Composer レジストリは PHP 向けです。npm レジストリには未公開なので、JS だけで使う場合は
> **ビルド済み `dist/` をコピー** するか、**clone したディレクトリを npm パス指定** で入れます。
> （`composer require` した場合も `vendor/joydea/rich-editor/dist/` に同じ資産が入っています。）

### ビルド済み `dist/` をコピー（ビルド環境不要）

```html
<link rel="stylesheet" href="/assets/rich-editor.css">
<script type="module" src="/assets/rich-editor.js"></script>

<!-- data-rich-editor を付けた textarea は自動でマウントされる -->
<textarea data-rich-editor placeholder="ここに入力…"></textarea>
```

手動マウントや設定注入は `window.RichEditor.mount(...)` で:

```html
<script type="module">
  window.addEventListener('DOMContentLoaded', () => {
    window.RichEditor.mount('#editor', {
      minHeight: '180px',
      upload: async (file) => ({ url: await myUploader(file), alt: file.name }),
      mentions: [{ name: '田中 博人', initials: '田' }],
    });
  });
</script>
```

自動マウントを止めたいときは `<html data-rich-editor-manual>` を付けます。

### バンドラ（Vite / webpack / React / Vue）

```bash
npm i ../rich-editor          # clone した場所に合わせる（npm 未公開のためパス指定）
```
```js
import { mountRichEditor } from '@joydea/rich-editor';
import '@joydea/rich-editor/css';

mountRichEditor('#editor', {
  placeholder: 'ここに入力…',
  minHeight: '180px',
  upload: async (file) => ({ url: await myUploader(file), alt: file.name }),
  // uploadUrl: '/images',   // upload 未指定時の Laravel 風フォールバック（CSRF付きPOST）
  mentions: [{ name: '田中 博人', initials: '田' }],
  onPickFile: (insert) => openMyPicker((f) => insert({
    name: f.name, isImage: f.isImage, previewUrl: f.previewUrl, downloadUrl: f.url,
  })),
});
```

React / Vue でも「ref の `<textarea>` を `mountRichEditor` に渡し、unmount 時に `el._richEditor.destroy()`」
の薄いラッパーを書けばそのまま使えます（コアはバニラ）。

### 公開 API

| API | 説明 |
|---|---|
| `mountRichEditor(target, options)` | セレクタ or textarea をマウント。返り値は TipTap `Editor` |
| `initRichEditors(root?)` | `[data-rich-editor]` をまとめてマウント |
| `initDrafts()` | 下書きの掃除・送信済み判定・送信フック（マウント前に一度） |
| `clearDraft(el)` / `clearDraftByKey(key)` / `draftKeyFor(el)` | 下書き操作 |

マウント後、`textarea._richEditor = { getMarkdown(), setMarkdown(v), focus(), destroy() }` が生えます。

---

## テーマ（見た目の差し替え）

デザイントークンは `--re-*` に名前空間化されています。`:root`（またはエディタの親）で上書きするだけ。

```css
:root {
  --re-accent: #2563eb;
  --re-accent-ink: #1e40af;
  --re-accent-soft: #e0e7ff;
  --re-surface: #fff;
  --re-surface-2: #f5f5f5;
  --re-ink: #111827;
  --re-line: #e5e7eb;
  /* ほか: --re-paper --re-muted --re-faint --re-line-2 --re-accent-press --re-danger --re-danger-ink --re-font-mono */
}
```

---

## Markdown 仕様メモ（サーバ側レンダリング）

- **画像の幅**は標準 Markdown に無いため、タイトル枠へ `![alt](url "width=300")` として埋めます。
  表示側（HTML 変換）で `title=width=NNN` を `style="width:NNNpx"` に読み替えてください。
- **テーブル**は GFM のパイプ表（`| … |`）で出力します。GFM 対応の Markdown レンダラで表示できます。
- 出力 Markdown の HTML 化・サニタイズは利用側の責務です（例: Laravel なら `Str::markdown(..., ['html_input' => 'strip'])`）。

---

## 開発

```bash
npm install
npm run build          # dist/rich-editor.js (+ .umd.cjs) と dist/rich-editor.css を出力
open demo/index.html   # ビルド後、ブラウザで動作確認
```

ディレクトリ構成: `resources/js`（コア）・`resources/css`（同梱テーマ）・`src`（Laravel グルー）・
`resources/views`（Blade）・`dist`（ビルド成果物＝コミット対象）。

---

## 配信の仕組み & 新バージョンの出し方

配信は **自社 VPS(`/opt/infra`) 上の Satis 静的レジストリ** を infra Caddy が
`https://packages.joydea.jp` で配る構成です（Satis 設定・ビルド手順は別リポ `satis-registry` 参照）。

**新バージョンをリリースする手順:**

```bash
# 1) このリポジトリでタグを打って push
git tag v0.2.0 && git push origin v0.2.0

# 2) VPS 側でレジストリを再ビルド（配布元 clone を更新 → Satis build）
git -C /opt/infra/rich-editor pull --ff-only
docker run --rm -e COMPOSER_ALLOW_SUPERUSER=1 \
  -v /opt/infra/satis-registry:/opt/infra/satis-registry \
  -v /opt/infra/rich-editor:/opt/infra/rich-editor -w /opt/infra/satis-registry \
  composer:2 sh -c '
    git config --global --add safe.directory /opt/infra/rich-editor
    [ -f satis-src/bin/satis ] || composer create-project composer/satis satis-src --stability=dev --no-interaction
    php satis-src/bin/satis build satis.json public'
```

Caddy は静的配信なので再ビルド後の再起動は不要（`public/` を差し替えるだけで反映）。

**さらに広く配るなら（任意）**: 公開 Packagist（要リポジトリ公開）や npm publish に移行すれば
`composer require` / `npm i` だけで入るようになります。
