@tatamicks/core
v1.0.12
Published
A headless, extensible document editor for React with grid-based layout system
Readme
@tatamicks/core
React 向けのヘッドレスで拡張可能なドキュメントエディターです。グリッドベースのレイアウト、複数ページの Book スキーマ、プラグインによるブロック拡張、編集 UI を組み合わせるための ActionBar / Sidebar を提供します。
特徴
- グリッドレイアウト — 列/行グリッドにブロックを配置。
fr、mm、pxなどの単位を扱えます。 - 3 つのモード —
FORMはテンプレート作成、EDITは値入力、VIEWは読み取り専用表示です。 - プラグインシステム — 組み込みブロックに加えて、独自の
BlockPluginをcreatePluginRegistry([...plugins])に渡すだけで登録できます。 - コンポーザブル UI —
ActionBar、Sidebar、DefaultSelectionActionBarOverlayをそのまま使うか、必要なパネルだけ差し替えられます。 - Undo / Redo —
useNoteContextが履歴、選択、ページ移動、値管理をまとめて配線します。 - バリデーションと印刷 — 入力検証の表示、複数ページ印刷、印刷用 CSS 生成に対応します。
インストール
npm install @tatamicks/core react react-domアプリのエントリーポイントで CSS を読み込んでください。
import "@tatamicks/core/styles";Next.js App Router では app/layout.tsx、Pages Router では pages/_app.tsx のようなグローバル CSS を読み込める場所で import します。Vite / SPA では main.tsx や App.tsx で読み込めます。
クイックスタート
import { useState } from "react";
import {
ActionBar,
CheckboxPlugin,
DEFAULT_BOOK,
DefaultSelectionActionBarOverlay,
Note,
NoteMode,
SelectPlugin,
Sidebar,
TextPlugin,
createPluginRegistry,
getDefaultActionBarSections,
getDefaultSidebarTabs,
useNoteContext,
} from "@tatamicks/core";
import "@tatamicks/core/styles";
const pluginRegistry = createPluginRegistry([
TextPlugin,
CheckboxPlugin,
SelectPlugin,
]);
export default function App() {
const [mode, setMode] = useState<NoteMode>(NoteMode.FORM);
const { context } = useNoteContext({
initialBook: DEFAULT_BOOK,
pluginRegistry,
});
return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<ActionBar sections={getDefaultActionBarSections({ context })} />
<div style={{ display: "flex", gap: 8, padding: 8 }}>
<button type="button" onClick={() => setMode(NoteMode.FORM)}>
テンプレート作成
</button>
<button type="button" onClick={() => setMode(NoteMode.EDIT)}>
値入力
</button>
<button type="button" onClick={() => setMode(NoteMode.VIEW)}>
閲覧
</button>
</div>
<div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
<div style={{ flex: 1, overflow: "auto", padding: 16 }}>
<div ref={context.containerRef} style={{ position: "relative" }}>
<Note mode={mode} context={context} />
<DefaultSelectionActionBarOverlay context={context} />
</div>
</div>
<Sidebar tabs={getDefaultSidebarTabs({ context })} />
</div>
</div>
);
}中心 API
Note
Note は mode に応じて NoteForm / NoteEdit / NoteView を切り替えるトップレベルコンポーネントです。状態は context に集約します。
<Note
mode={NoteMode.FORM}
context={context}
showValidation={false}
scale={1}
className="my-note"
/>NoteForm、NoteEdit、NoteView も export されています。Note を使うと実行時にモードを切り替えられますが、ページ固定のモードが分かっている場合は各コンポーネントを直接使う方がシンプルです。
useNoteContext
フルエディターを組み立てるための統合フックです。Book 履歴、入力値、バインディング、アクション、ページ移動、選択状態をまとめた context を返します。
const { context } = useNoteContext({
initialBook,
pluginRegistry,
defaultValues,
extra,
});containerRef は context.containerRef 経由でアクセスします。独自の履歴層だけが必要な場合は、低レベル API として useBookHistory({ initialBook, maxHistory }) も利用できます。
データモデル
Book
interface Book {
paper: Paper;
pages: [Page, ...Page[]];
metaData?: Record<string, Value>;
}Page
interface Page {
grid: Grid;
blocks: Block[];
blockDefaults?: BlockDefaults;
metaData?: Record<string, Value>;
}Paper
interface Paper {
size: PaperSize;
margin: PaperMargin;
orientation?: boolean;
autoWidth?: boolean;
autoHeight?: boolean;
}Block
interface Block<P = Record<string, Value>> {
id: string;
kind: string;
layout: { x: number; y: number; w: number; h: number };
props: P;
style?: BlockStyle;
behavior?: BlockBehavior;
hiddenBinding?: HiddenBinding;
initValue?: Value;
}props の解決順序は低い順に PropDef.defaultProps、page.blockDefaults[kind]、block.props です。後からマージされる値ほど優先されます。
プラグイン
組み込みプラグイン
TextPlugin、CheckboxPlugin、SelectPlugin はメインエントリから import できます。
ButtonPlugin、StepperPlugin、NoteBlockPlugin は Advanced プラグインとして @tatamicks/core/canvas からのみ公開されています。
// 基本プラグイン(@tatamicks/core)
import {
CheckboxPlugin,
SelectPlugin,
TextPlugin,
createPluginRegistry,
} from "@tatamicks/core";
// Advanced プラグイン(@tatamicks/core/canvas)
import {
ButtonPlugin,
NoteBlockPlugin,
StepperPlugin,
} from "@tatamicks/core/canvas";
const pluginRegistry = createPluginRegistry([
TextPlugin,
CheckboxPlugin,
SelectPlugin,
ButtonPlugin,
StepperPlugin,
NoteBlockPlugin,
]);createPluginRegistry には BlockPlugin を直接渡します。追加の wrapper 型や registry 用 plugin 型をユーザー側で用意する必要はありません。
カスタムプラグイン
import { forwardRef, useImperativeHandle, useRef } from "react";
import {
alignmentProp,
paddingProp,
placeholderProp,
} from "@tatamicks/core/canvas";
import type {
BaseBlockProps,
BlockPlugin,
BlockRef,
RendererProps,
} from "@tatamicks/core/canvas";
interface LabelProps extends BaseBlockProps {
label: string;
placeholder: string;
}
const LabelRenderer = forwardRef<
BlockRef,
RendererProps<LabelProps, string>
>(({ props, value, onChange, readOnly }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
}));
return (
<label style={{ display: "grid", gap: 4 }}>
<span>{props.label}</span>
<input
ref={inputRef}
value={value ?? ""}
placeholder={props.placeholder}
readOnly={readOnly}
onChange={(event) => onChange(event.target.value)}
/>
</label>
);
});
LabelRenderer.displayName = "LabelRenderer";
export const LabelPlugin: BlockPlugin<LabelProps, string> = {
kind: "label",
meta: {
displayName: "ラベル入力",
description: "ラベル付きのテキスト入力",
defaultSize: { w: 4, h: 2 },
},
Renderer: LabelRenderer,
properties: [
alignmentProp,
paddingProp,
placeholderProp,
{
kind: "labelText",
defaultProps: { label: "Label" },
},
],
validateProps: (raw): LabelProps => {
const value =
typeof raw === "object" && raw !== null
? (raw as Record<string, unknown>)
: {};
return {
label: typeof value.label === "string" ? value.label : "Label",
placeholder:
typeof value.placeholder === "string" ? value.placeholder : "",
};
},
validateValue: (raw): string | null => {
return typeof raw === "string" ? raw : null;
},
};PropDef
properties に渡す各 PropDef は、プロパティパネルに表示する設定単位です。
interface PropDef {
kind: string;
defaultProps: Record<string, Value>;
component?: React.ComponentType<{
value: Value;
onChange: (value: Value) => void;
readOnly?: boolean;
}>;
}組み込みの alignmentProp、paddingProp、fontStyleProp、placeholderProp、requiredProp はすべて PropDef です。独自 UI が必要な場合は component を指定し、省略時は既定の編集 UI にフォールバックします。
ActionBar / Sidebar
既定 UI は useNoteContext の context を渡すだけで動作します。
<ActionBar sections={getDefaultActionBarSections({ context })} />
<Sidebar tabs={getDefaultSidebarTabs({ context })} />
<DefaultSelectionActionBarOverlay context={context} />getDefaultActionBarSections() や getDefaultSidebarTabs() の戻り値は配列なので、filter / map で一部を差し替えられます。
値の管理
useNoteContext は入力値(EDIT / VIEW モードで各ブロックに入力された値)を内部で管理します。
現在の全入力値は context.values(Record<string, Value>)から読み取れます。
値の保存
外部に保存したい場合(API 送信・LocalStorage など)は useEffect で変化を監視します。
const { context } = useNoteContext({ initialBook, pluginRegistry });
// context.values が変わるたびにバックエンドへ送信する例
useEffect(() => {
save(context.values);
}, [context.values]);値の復元
保存済みの値を復元するには defaultValues に渡してコンポーネントをマウントします。
defaultValues はマウント時のみ参照されます(詳細は useNoteContext の JSDoc を参照)。
if (!savedValues) return <Loading />;
return (
<MyEditor
key={documentId}
initialBook={template}
defaultValues={savedValues}
/>
);不要な値のクリーン
ブロックを削除すると context.values にそのブロックの値が残ります。
保存前に cleanValues でブロックが存在しないエントリを除去できます。
import { cleanValues } from "@tatamicks/core";
const valuesToSave = cleanValues(context.book, context.values);シリアライズと検証
import {
deserializeBook,
parseBook,
serializeBook,
validateBook,
} from "@tatamicks/core";
const json = serializeBook(book);
const restored = deserializeBook(json);
const parsed = parseBook(JSON.parse(json));
const errors = validateBook(parsed);validateBook は Book 全体の構造検証ではなく、現在はブロック ID の重複検出とページあたりのブロック数上限チェックを行います。未検証データを Book として扱う前には parseBook または deserializeBook を使ってください。
値のシリアライズ
context.values は JSON シリアライズ可能なので JSON.stringify でそのまま保存できます。復元時は deserializeValues で検証してから使います。
import { deserializeValues } from "@tatamicks/core";
// 保存
const valuesJson = JSON.stringify(context.values);
// 復元(JSON が不正または無効な Value が含まれる場合は例外を投げる)
const restoredValues = deserializeValues(valuesJson);ページ操作
import { addPage, movePage, removePage, setPage } from "@tatamicks/core";
const withNewPage = addPage(book);
const moved = movePage(withNewPage, 0, 1);
const removed = removePage(moved, 1);
const updated = setPage(removed, 0, nextPage);印刷
import { printNote } from "@tatamicks/core";
import type { PrintSettings } from "@tatamicks/core";
// コンテキストをそのまま渡すだけで印刷できる
printNote(context);
// 用紙サイズ・向き・余白を上書きして印刷する場合
const settings: PrintSettings = {
paperSize: "A4",
orientation: false, // false = 縦向き
margin: { top: "10mm", bottom: "10mm", left: "15mm", right: "15mm" },
};
printNote(context, settings);低レベル API として resolveEffectivePaper() は @tatamicks/core/canvas サブパスから export されています。
import { resolveEffectivePaper } from "@tatamicks/core/canvas";TypeScript
主要な型は @tatamicks/core から export されています。
// @tatamicks/core からインポートできる型
import type {
ActionContext,
BindingContext,
BuiltinActionId,
Block,
BlockBehavior,
Book,
Grid,
NoteContext,
Page,
Paper,
PluginRegistry,
PrintMargin,
PrintSettings,
Value,
} from "@tatamicks/core";
// カスタムプラグイン実装に必要な型は @tatamicks/core/canvas からインポートする
import type {
BaseBlockProps,
BlockPlugin,
BlockPluginMeta,
BlockRef,
PropDef,
RendererProps,
} from "@tatamicks/core/canvas";v0.x からの移行
現在の公開スキーマ名は Book / Page です。古い資料で NoteBook / FormSchema と呼んでいるものは、それぞれ現在の Book / Page に相当します。
組み込みブロックを追加する(Contributor 向け)
新しいブロックを src/canvas/blocks/ に追加する手順です。
ファイル構成
src/canvas/blocks/<kindName>/ ディレクトリを作成し、以下のファイルを追加します。
| ファイル | 役割 |
|---|---|
| types.ts | <Kind>BlockProps 型定義 |
| props.ts | PropDef[] 定義(プロパティパネル設定) |
| renderer.tsx | <Kind>Renderer — forwardRef コンポーネント |
| plugin.ts | <Kind>Plugin: BlockPlugin<...> 本体 |
| index.ts | plugin.ts を re-export |
| __tests__/<kind>Plugin.test.ts | プラグイン単体テスト |
| __stories__/<Kind>.stories.tsx | Storybook ストーリー |
チェックリスト
types.ts—<Kind>BlockProps extends BaseBlockPropsを定義するprops.ts—properties: PropDef[]とdefaultPropsを定義するrenderer.tsx—forwardRef<BlockRef, RendererProps<...>>で実装しuseImperativeHandleでfocus()を公開するplugin.ts—BlockPlugin<KindProps, KindValue>として組み立て、validateProps/validateValueを実装する(値はValue互換型のみ。日付はDateインスタンスではなく ISO 8601 文字列で表現)index.ts—export { <Kind>Plugin } from "./plugin";のみを記述するsrc/canvas/blocks/index.ts—export * from "./<kindName>";を追記し、basePlugins.tsのデフォルトリストにも追加する__tests__/<kind>Plugin.test.ts—validateProps/validateValueのユニットテスト(エッジケース含む)を追加する__stories__/<Kind>.stories.tsx—Defaultなど代表ストーリーを最低1件追加する- typecheck / lint / unit test / build がすべて通ることを確認する
旧 @tatamicks/text、@tatamicks/checkbox、@tatamicks/select は @tatamicks/core に統合されています。
| 旧 import | 新 import |
|-----------|-----------|
| import { TextPlugin } from "@tatamicks/text" | import { TextPlugin } from "@tatamicks/core" |
| import { CheckboxPlugin } from "@tatamicks/checkbox" | import { CheckboxPlugin } from "@tatamicks/core" |
| import { SelectPlugin } from "@tatamicks/select" | import { SelectPlugin } from "@tatamicks/core" |
移行の詳細は docs/20_migration_guide.md を参照してください。
ライセンス
MIT
