mcpField
R
react-to-axure

react-to-axure

github.com/lintendoデザイン・UI13780 回インストール2 日前に更新
リポジトリ

react-to-axure について

React コンポーネントを Axure にインポートできる形式へ変換するスキルです。プロトタイプ向けの宣言、ランタイムラッパー、ビルド出力の要件を扱います。

主な機能

  1. 01イベント、アクション、変数、設定、データの宣言
  2. 02forwardRef と useImperativeHandle によるラップ
  3. 03ビジュアル設定パネル項目の定義
  4. 04IIFE 出力と React 依存関係の外部化
  5. 05Axure 出力ファイルとリソースマニフェストの生成

活用シーン

  1. 01既存の React コンポーネントを Axure プロトタイプで利用
  2. 02コンポーネントの状態やイベントを Axure と連携
  3. 03コンポーネントの props 向け設定パネルを生成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/lintendo/axhub-skills --skill react-to-axure

インストール後、Claude Code で/react-to-axureを入力して読み込まれたか確認します。

同梱ファイルreferences/axure-types.tsreferences/config-panel-types.ts

詳細

React を Axure にエクスポート

概要

このスキルは、任意の React コンポーネントまたはアプリケーションを、Axure プロトタイププラットフォームにインポート可能な標準コンポーネントに変換するのに役立ちます。変換後、コンポーネントは以下の機能を備えます。

  • 設定パネル:Axure エディターで視覚的に設定できる項目(色、テキスト、数値など)を提供します
  • イベントシステム:Axure プロトタイプにカスタムイベントを送信します
  • アクションシステム:Axure プロトタイプからの外部呼び出しに応答します
  • 変数の公開:内部状態を Axure プロトタイプから読み取れるようにします
  • データ接続:Axure プロトタイプから外部データソースを受け取ります

このスキルでは、次の 2 種類の成果物を生成します。

  1. 設定宣言:5 つの定数(EVENT_LIST / ACTION_LIST / VAR_LIST / CONFIG_LIST / DATA_LIST)
  2. ランタイムラッパー:forwardRef + useImperativeHandle を使用したコンポーネントコード

また、ホストツールで利用する Axure エクスポート成果物は、デフォルトで共通の artifact ディレクトリに書き込まれます。

.axhub/make/artifacts/axure/<resource-id>/
├── index-bundle.json
├── axure-json.json
├── cover.svg
└── manifest.json

対象プロジェクトで .axhub/make/project.json を使用してリソースを記述する場合は、make-server やその他のホストが直接読み取り、コピー、ダウンロードできるように、対応する resources.prototypes[].artifacts.axure にこれらのパスを登録する必要があります。

使用する場面

  • 既存の React コンポーネントを Axure プロトタイプで使用したい場合
  • React アプリケーションを Axure プロトタイプシステムと双方向に連携させたい場合
  • React コンポーネント用の標準化された設定パネル定義を生成したい場合

参照ファイル

このスキルには完全な型定義ファイルが付属しており、対象プロジェクトにそのままコピーして使用できます。

  • references/axure-types.ts:コアインターフェース(AxureProps、AxureHandle)とユーティリティ関数
  • references/config-panel-types.ts:設定パネルコンポーネントの完全な型定義

.axhub/make/project.json のリソースメタデータの例:

{
  "artifacts": {
    "axure": {
      "caseId": "home",
      "manifestPath": ".axhub/make/artifacts/axure/home/manifest.json",
      "indexBundlePath": ".axhub/make/artifacts/axure/home/index-bundle.json",
      "axureJsonPath": ".axhub/make/artifacts/axure/home/axure-json.json",
      "coverSvgPath": ".axhub/make/artifacts/axure/home/cover.svg"
    }
  }
}

変換ワークフロー

ステップ 1:ソースコンポーネントを分析する

対象の React コンポーネントについて、以下の点を分析します。

  1. Props インターフェース:コンポーネントが現在受け取る props は何ですか?
  2. 内部状態:公開が必要な useState / useReducer の状態はありますか?
  3. インタラクション:ユーザー操作のイベント(click、change、submit など)は何ですか?
  4. 外部依存関係:コンポーネントはどのサードパーティーライブラリに依存していますか?
  5. DOM 操作:DOM を直接操作しますか(グラフライブラリ、地図ライブラリなど)?

変換チェックリストを作成し、マッピングが必要な props → config、state → var、callback → event/action を列挙します。

ステップ 2:型定義をコピーする

references/axure-types.ts を対象プロジェクトの共通ディレクトリ(例:src/common/axure-types.ts)にコピーします。設定パネルの詳細な型が必要な場合は、references/config-panel-types.ts もコピーします。

ステップ 3:インターフェース層を定義する

分析結果に基づいて、次の 5 つの定数定義を生成します。

3.1 イベントリスト EVENT_LIST

コンポーネントが外部に発火できるイベントを定義します。

import type { EventItem } from './path-to/axure-types';

const EVENT_LIST: EventItem[] = [
  { name: 'onClick', desc: '点击时触发' },
  { name: 'onChange', desc: '值改变时触发,payload 为新值的 JSON 字符串' }
];

ルール:

  • name:イベント名。camelCase の使用を推奨します
  • desc:イベントの説明。発火するタイミングを記述します
  • payload:省略可能。payload の形式を記述します(payload は必ず文字列型にしてください。複雑なデータには JSON.stringify が必要です)
3.2 アクションリスト ACTION_LIST

コンポーネントが応答できる外部呼び出しを定義します。

import type { Action } from './path-to/axure-types';

const ACTION_LIST: Action[] = [
  { name: 'reset', desc: '重置组件到初始状态' },
  { name: 'setValue', desc: '设置值,参数格式:JSON 字符串', params: 'JSON string' }
];

ルール:

  • params は必ず文字列型にしてください。複雑なパラメーターは JSON.parse で解析する必要があります
  • desc にはパラメーターの形式を記述します
3.3 変数リスト VAR_LIST

コンポーネントが公開する内部状態を定義します。

import type { KeyDesc } from './path-to/axure-types';

const VAR_LIST: KeyDesc[] = [
  { name: 'current_value', desc: '当前值' },
  { name: 'is_loading', desc: '是否加载中' }
];

ルール:

  • name は必ず snake_case(小文字 + 数字 + アンダースコア)にしてください
  • 外部からの読み取りが必要な状態のみを公開し、内部実装の詳細は公開しないでください
3.4 設定項目リスト CONFIG_LIST

設定パネルで設定可能な項目を定義します。

import type { ConfigItem } from './path-to/axure-types';

const CONFIG_LIST: ConfigItem[] = [
  {
    type: 'input',
    attributeId: 'title',
    displayName: '标题',
    info: '组件顶部显示的标题文本',
    initialValue: '默认标题'
  },
  {
    type: 'colorPicker',
    attributeId: 'primaryColor',
    displayName: '主色调',
    info: '组件的主题颜色',
    initialValue: '#1890ff'
  },
  {
    type: 'inputNumber',
    attributeId: 'maxCount',
    displayName: '最大数量',
    info: '允许的最大数量',
    initialValue: 10,
    min: 1,
    max: 100
  },
  {
    type: 'switch',
    attributeId: 'showBorder',
    displayName: '显示边框',
    info: '是否显示组件边框',
    initialValue: true
  },
  {
    type: 'select',
    attributeId: 'size',
    displayName: '尺寸',
    info: '组件尺寸',
    initialValue: 'medium',
    options: [
      { label: '小', value: 'small' },
      { label: '中', value: 'medium' },
      { label: '大', value: 'large' }
    ]
  }
];

使用可能な type(詳細な定義は references/config-panel-types.ts を参照):

type用途initialValue の型
inputテキスト入力string
inputNumber数値入力(min/max/step をサポート)number
checkboxチェックボックスboolean
switchスイッチboolean
sliderスライダー(min/max が必要)number
selectドロップダウン選択(options が必要)string | number
colorPickerカラーピッカーstring(HEX 形式)
autoCompleteオートコンプリート入力string
arrayData配列データエディターstring[]
tableテーブルデータエディターany[]
mapキーと値のペアのエディターRecord<string, any>
fontSettingフォント設定(色/サイズ/太さなど)オブジェクト
lineSetting線の設定オブジェクト
groupグループコンテナー(children を含む)—
collapse折りたたみパネルコンテナー—

マッピング方針(元の Props → ConfigItem):

元の Props の型推奨する ConfigItem type
stringinput
numberinputNumber
booleanswitch または checkbox
enum / unionselect(列挙値を options にマッピング)
色文字列colorPicker
string[]arrayData
複雑なオブジェクト/配列table または map
3.5 データ項目リスト DATA_LIST

コンポーネントが受け取る外部データ構造を定義します。

import type { DataDesc } from './path-to/axure-types';

const DATA_LIST: DataDesc[] = [
  {
    name: 'items',
    desc: '列表数据',
    keys: [
      { name: 'id', desc: '唯一标识' },
      { name: 'title', desc: '标题' },
      { name: 'value', desc: '值' }
    ]
  }
];

適用場面:リストやテーブルなど、データ駆動型コンポーネントに適しています。純粋な表示用または設定駆動のコンポーネントでは、空配列 [] を設定できます。

ステップ 4:コンポーネントをラップする

元のコンポーネントを forwardRef + useImperativeHandle でラップします。

import React, { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import type { AxureProps, AxureHandle } from './path-to/axure-types';

const WrappedComponent = forwardRef<AxureHandle, AxureProps>(
  function WrappedComponent(innerProps, ref) {
    // ① 安全解构 props(防御性编程)
    const dataSource = innerProps && innerProps.data ? innerProps.data : {};
    const configSource = innerProps && innerProps.config ? innerProps.config : {};
    const onEventHandler = typeof innerProps.onEvent === 'function'
      ? innerProps.onEvent
      : function () { return undefined; };
    const container = innerProps && innerProps.container ? innerProps.container : null;

    // ② 从 config 读取配置值(使用类型检查,避免 || 运算符)
    const title = typeof configSource.title === 'string' && configSource.title
      ? configSource.title
      : '默认标题';
    const maxCount = typeof configSource.maxCount === 'number'
      ? configSource.maxCount
      : 10;

    // ③ 组件内部状态
    const countState = useState(0);
    const count = countState[0];
    const setCount = countState[1];

    // ④ 事件触发(payload 必须是字符串)
    const emitEvent = useCallback(function (eventName: string, payload?: string) {
      try {
        onEventHandler(eventName, payload);
      } catch (error) {
        console.warn('事件触发失败:', eventName, error);
      }
    }, [onEventHandler]);

    // ⑤ 动作处理(params 必须是字符串)
    const handleAction = useCallback(function (name: string, params?: string) {
      switch (name) {
        case 'reset':
          setCount(0);
          break;
        case 'setValue':
          if (params) {
            try {
              const parsed = JSON.parse(params);
              if (typeof parsed.count === 'number') {
                setCount(parsed.count);
              }
            } catch (e) {
              console.warn('参数解析失败:', e);
            }
          }
          break;
      }
    }, []);

    // ⑥ 暴露 Handle 接口
    useImperativeHandle(ref, function () {
      return {
        getVar: function (name: string) {
          const vars: Record<string, any> = {
            current_value: count,
          };
          return vars[name];
        },
        fireAction: handleAction,
        eventList: EVENT_LIST,
        actionList: ACTION_LIST,
        varList: VAR_LIST,
        configList: CONFIG_LIST,
        dataList: DATA_LIST
      };
    }, [count, handleAction]);

    // ⑦ 渲染原始组件(透传改造后的 props)
    return <OriginalComponent title={title} maxCount={maxCount} />;
  }
);

export default WrappedComponent;
ステップ 5:ビルド出力を調整する

ビルド成果物は、以下の要件を満たす必要があります。

  • 出力形式:IIFE(即時実行関数)。グローバル変数名は UserComponent
  • 依存関係の外部化:React と ReactDOM は必ず外部化し、成果物にバンドルしないでください
  • 単一ファイル出力:最終成果物は単一の JS ファイル + 任意の CSS ファイルとします

Vite のビルド設定例:

// vite.config.js
export default {
  build: {
    lib: {
      entry: 'src/index.tsx',
      name: 'UserComponent',
      formats: ['iife'],
      fileName: () => 'index.js'
    },
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
      }
    }
  }
};
ステップ 6:検証チェックリスト

変換が完了したら、各項目を確認します。

  • コンポーネントが forwardRef<AxureHandle, AxureProps> でラップされている
  • useImperativeHandle ですべての 5 つのリスト(eventList, actionList, varList, configList, dataList)が正しく公開されている
  • export default でラップ済みコンポーネントがエクスポートされている
  • すべての event payload が文字列型である
  • すべての action params が文字列型である
  • VAR_LIST 内の name が snake_case を使用している
  • CONFIG_LIST の各項目に type、attributeId、displayName、initialValue がある
  • props の分割代入に防御的プログラミング(innerProps && innerProps.xxx)を使用し、ES6 の分割代入を使用していない
  • config 値の読み取りに型チェックを使用し、|| 演算子を使用していない(0/false が誤判定されるのを防ぐため)
  • ビルド成果物の形式が IIFE で、グローバル変数名が UserComponent である
  • React/ReactDOM が外部化され、重複してバンドルされていない

変換例

例 1:シンプルな表示コンポーネント

変換前(元の React コンポーネント):

function StatusCard({ title, status, color }) {
  return (
    <div style={{ borderLeft: `4px solid ${color}` }}>
      <h3>{title}</h3>
      <span>{status}</span>
    </div>
  );
}

変換後(Axure 互換コンポーネント):

import React, { forwardRef, useImperativeHandle } from 'react';
import type { AxureProps, AxureHandle, EventItem, Action, KeyDesc, ConfigItem, DataDesc } from './path-to/axure-types';

const EVENT_LIST: EventItem[] = [];
const ACTION_LIST: Action[] = [];
const VAR_LIST: KeyDesc[] = [
  { name: 'current_status', desc: '当前状态文本' }
];
const CONFIG_LIST: ConfigItem[] = [
  { type: 'input', attributeId: 'title', displayName: '标题', initialValue: '状态卡片' },
  { type: 'input', attributeId: 'status', displayName: '状态', initialValue: '正常' },
  { type: 'colorPicker', attributeId: 'color', displayName: '状态颜色', initialValue: '#52c41a' }
];
const DATA_LIST: DataDesc[] = [];

const Component = forwardRef<AxureHandle, AxureProps>(function StatusCard(innerProps, ref) {
  const configSource = innerProps && innerProps.config ? innerProps.config : {};
  const title = typeof configSource.title === 'string' && configSource.title ? configSource.title : '状态卡片';
  const status = typeof configSource.status === 'string' && configSource.status ? configSource.status : '正常';
  const color = typeof configSource.color === 'string' && configSource.color ? configSource.color : '#52c41a';

  useImperativeHandle(ref, function () {
    return {
      getVar: function (name: string) { return name === 'current_status' ? status : undefined; },
      fireAction: function () {},
      eventList: EVENT_LIST,
      actionList: ACTION_LIST,
      varList: VAR_LIST,
      configList: CONFIG_LIST,
      dataList: DATA_LIST
    };
  }, [status]);

  return (
    <div style={{ borderLeft: '4px solid ' + color, padding: '12px' }}>
      <h3>{title}</h3>
      <span>{status}</span>
    </div>
  );
});

export default Component;
示例 2:交互型组件(带事件和动作)

改造前:

function Counter({ initial = 0, onCountChange }) {
  const [count, setCount] = useState(initial);
  return (
    <div>
      <span>{count}</span>
      <button onClick={() => { setCount(c => c + 1); onCountChange?.(count + 1); }}>+1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

改造后:

import React, { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import type { AxureProps, AxureHandle, EventItem, Action, KeyDesc, ConfigItem, DataDesc } from './path-to/axure-types';

const EVENT_LIST: EventItem[] = [{ name: 'onCountChange', desc: '计数改变时触发' }];
const ACTION_LIST: Action[] = [
  { name: 'increment', desc: '计数 +1' },
  { name: 'reset', desc: '重置计数' },
  { name: 'setCount', desc: '设置计数值,参数:数字字符串', params: 'number string' }
];
const VAR_LIST: KeyDesc[] = [{ name: 'count', desc: '当前计数值' }];
const CONFIG_LIST: ConfigItem[] = [
  { type: 'inputNumber', attributeId: 'initial', displayName: '初始值', initialValue: 0, min: 0 }
];
const DATA_LIST: DataDesc[] = [];

const Component = forwardRef<AxureHandle, AxureProps>(function Counter(innerProps, ref) {
  const configSource = innerProps && innerProps.config ? innerProps.config : {};
  const onEventHandler = typeof innerProps.onEvent === 'function'
    ? innerProps.onEvent : function () {};
  const initial = typeof configSource.initial === 'number' ? configSource.initial : 0;

  const countState = useState(initial);
  const count = countState[0];
  const setCount = countState[1];

  const emitEvent = useCallback(function (eventName: string, payload?: string) {
    try { onEventHandler(eventName, payload); } catch (e) { console.warn(e); }
  }, [onEventHandler]);

  const increment = useCallback(function () {
    setCount(function (prev) {
      var next = prev + 1;
      emitEvent('onCountChange', String(next));
      return next;
    });
  }, [emitEvent]);

  const reset = useCallback(function () {
    setCount(initial);
    emitEvent('onCountChange', String(initial));
  }, [initial, emitEvent]);

  const handleAction = useCallback(function (name: string, params?: string) {
    switch (name) {
      case 'increment': increment(); break;
      case 'reset': reset(); break;
      case 'setCount':
        if (params) {
          var n = parseInt(params, 10);
          if (!isNaN(n)) { setCount(n); emitEvent('onCountChange', String(n)); }
        }
        break;
    }
  }, [increment, reset, emitEvent]);

  useImperativeHandle(ref, function () {
    return {
      getVar: function (name: string) { return name === 'count' ? count : undefined; },
      fireAction: handleAction,
      eventList: EVENT_LIST,
      actionList: ACTION_LIST,
      varList: VAR_LIST,
      configList: CONFIG_LIST,
      dataList: DATA_LIST
    };
  }, [count, handleAction]);

  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
});

export default Component;
示例 3:数据驱动型组件

改造前:

function UserList({ users, onSelect }) {
  return (
    <ul>
      {users.map(u => (
        <li key={u.id} onClick={() => onSelect(u)}>
          {u.name} - {u.email}
        </li>
      ))}
    </ul>
  );
}

改造后:

import React, { forwardRef, useImperativeHandle, useState, useCallback } from 'react';
import type { AxureProps, AxureHandle, EventItem, Action, KeyDesc, ConfigItem, DataDesc } from './path-to/axure-types';

const EVENT_LIST: EventItem[] = [{ name: 'onSelect', desc: '选中用户时触发,payload 为用户 JSON' }];
const ACTION_LIST: Action[] = [{ name: 'clearSelection', desc: '清除选中' }];
const VAR_LIST: KeyDesc[] = [{ name: 'selected_user_id', desc: '当前选中的用户 ID' }];
const CONFIG_LIST: ConfigItem[] = [
  { type: 'input', attributeId: 'emptyText', displayName: '空状态文本', initialValue: '暂无数据' }
];
const DATA_LIST: DataDesc[] = [
  {
    name: 'users',
    desc: '用户列表数据',
    keys: [
      { name: 'id', desc: '用户 ID' },
      { name: 'name', desc: '用户姓名' },
      { name: 'email', desc: '用户邮箱' }
    ]
  }
];

const Component = forwardRef<AxureHandle, AxureProps>(function UserList(innerProps, ref) {
  const dataSource = innerProps && innerProps.data ? innerProps.data : {};
  const configSource = innerProps && innerProps.config ? innerProps.config : {};
  const onEventHandler = typeof innerProps.onEvent === 'function'
    ? innerProps.onEvent : function () {};
  const emptyText = typeof configSource.emptyText === 'string' && configSource.emptyText
    ? configSource.emptyText : '暂无数据';
  var users = Array.isArray(dataSource.users) ? dataSource.users : [];

  const selectedState = useState<string | null>(null);
  const selectedId = selectedState[0];
  const setSelectedId = selectedState[1];

  const emitEvent = useCallback(function (eventName: string, payload?: string) {
    try { onEventHandler(eventName, payload); } catch (e) { console.warn(e); }
  }, [onEventHandler]);

  const handleAction = useCallback(function (name: string) {
    if (name === 'clearSelection') setSelectedId(null);
  }, []);

  useImperativeHandle(ref, function () {
    return {
      getVar: function (name: string) { return name === 'selected_user_id' ? selectedId : undefined; },
      fireAction: handleAction,
      eventList: EVENT_LIST,
      actionList: ACTION_LIST,
      varList: VAR_LIST,
      configList: CONFIG_LIST,
      dataList: DATA_LIST
    };
  }, [selectedId, handleAction]);

  if (users.length === 0) return <div>{emptyText}</div>;

  return (
    <ul>
      {users.map(function (u: any) {
        return (
          <li key={u.id} onClick={function () {
            setSelectedId(u.id);
            emitEvent('onSelect', JSON.stringify(u));
          }}>
            {u.name} - {u.email}
          </li>
        );
      })}
    </ul>
  );
});

export default Component;

常见问题

Q: 组件使用了图表库(ECharts、D3)等直接操作 DOM 的库怎么办?

使用 innerProps.container 获取 DOM 容器,在 useEffect 中初始化图表引擎:

const container = innerProps && innerProps.container ? innerProps.container : null;

useEffect(function () {
  if (!container) return;
  const chart = echarts.init(container);
  chart.setOption({ /* ... */ });
  return function () { chart.dispose(); };
}, [container]);

return null; // 直接使用 container 渲染时返回 null
Q: 组件的 props 很复杂(嵌套对象),如何映射到 CONFIG_LIST?
  • 扁平化:将嵌套属性用 attributeId 的点分隔路径表示,如 'style.fontSize'
  • 分组:使用 type: 'group' 或 type: 'collapse' 组织相关配置项
  • 复杂结构:使用 table 或 map 类型编辑器
Q: 原始组件用了 props 解构、箭头函数,需要改写吗?

建议改写以提升兼容性:

  • 使用 function 关键字替代箭头函数
  • 使用 innerProps && innerProps.xxx 替代解构(防御性编程)
  • 使用 useState 返回值的数组索引访问而非解构
Q: 如何处理组件的样式?
  • 将 CSS 放在同目录的 style.css 中并 import './style.css'
  • 使用带组件前缀的类名避免全局冲突(如 my-chart-container)
  • 避免全局样式污染

类型参考

完整的类型定义见本技能的 references/ 目录:

  • references/axure-types.ts:核心接口和工具函数

    • AxureProps:组件 Props 接口(data、config、onEvent、container)
    • AxureHandle:组件 Handle 接口(getVar、fireAction、5 个列表)
    • EventItem:事件定义 { name, desc, payload? }
    • Action:动作定义 { name, desc, params? }
    • KeyDesc:变量/数据字段定义 { name, desc }(name 必须 snake_case)
    • DataDesc:数据源定义 { name, desc, keys: KeyDesc[] }
    • ConfigItem:配置面板项定义
    • safeEmitEvent、createEventEmitter:事件触发工具函数
    • getConfigValue、getDataValue:安全取值工具函数
  • references/config-panel-types.ts:配置面板组件详细类型

    • InputConfig、InputNumberConfig、CheckboxConfig、SliderConfig
    • SelectConfig、AutoCompleteConfig、ColorPickerConfig
    • ArrayDataConfig、TableConfig、MapConfig
    • FontSettingConfig、LineSettingConfig、PointSettingConfig
    • GroupConfig:分组布局

コメント