conoma
← ドキュメント

プラグイン開発ガイド

概要

Conomaプラグインは、アプリ(フルスクリーンiframe)とウィジェット(ダッシュボードグリッド内iframe)の いずれか、または両方を提供できる拡張機能です。HTML + CSS + JavaScriptで開発できます。

アプリ専用
サイドバーに表示されるフルスクリーンアプリ
例: アンテナ
ウィジェット専用
ダッシュボードに配置する小型ウィジェット
例: 天気
両方
アプリとウィジェットの両方を提供
例: メモ、電卓

クイックスタート: 5分で作るウィジェット

最小構成のウィジェットプラグインを作成してみましょう。

1. ディレクトリを作成

mkdir my-widget && cd my-widget

2. manifest.json を作成

{
  "id": "com.example.hello",
  "name": "Hello Widget",
  "version": "1.0.0",
  "description": "はじめてのウィジェット",
  "widget": {
    "entry": "widget.html",
    "minW": 3,
    "minH": 2,
    "defaultW": 4,
    "defaultH": 3
  }
}

3. widget.html を作成

<!DOCTYPE html>
<html>
<head>
  <script src="../sdk/tabane-sdk.js"></script>
  <style>
    body {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100vh;
      margin: 0;
      font-family: sans-serif;
      color: var(--plugin-text, #e0e0f0);
    }
  </style>
</head>
<body>
  <h1>Hello, Conoma!</h1>
</body>
</html>

4. ZIPにパッケージ

zip -r hello-widget.zip .

5. インストール

Conomaのプラグイン管理 →「ZIPから追加」→ hello-widget.zip を選択

ダッシュボードの編集モードでウィジェット一覧に「Hello Widget」が表示されます。

ディレクトリ構成

my-plugin/
├── manifest.json    # 必須: プラグイン定義
├── icon.svg         # 推奨: アイコン(SVG, 24x24推奨)
├── app.html         # アプリのエントリーポイント
├── widget.html      # ウィジェットのエントリーポイント
├── styles.css       # スタイルシート
└── *.js             # JavaScriptモジュール

全ファイルは tabane-plugin://{pluginId}/{path} プロトコルで配信されます。 相対パスで参照可能です。

manifest.json 完全リファレンス

{
  "id": "com.example.my-plugin",   // 一意のID(逆ドメイン形式推奨)
  "name": "プラグイン名",            // 表示名
  "version": "1.0.0",              // セマンティックバージョン
  "description": "説明文",           // 説明
  "category": "ツール",              // カテゴリ(任意)
  "icon": "icon.svg",              // アイコンファイル(任意)
  "app": {                         // アプリ定義(省略可)
    "entry": "app.html",           //   HTMLエントリーポイント
    "singleTab": true              //   true: 単一タブ(デフォルト)
  },
  "widget": {                      // ウィジェット定義(省略可)
    "entry": "widget.html",        //   HTMLエントリーポイント
    "minW": 3,                     //   最小幅(グリッド単位)
    "minH": 3,                     //   最小高さ
    "defaultW": 4,                 //   デフォルト幅
    "defaultH": 4,                 //   デフォルト高さ
    "colors": [...],               //   カスタムカラー(任意)
    "settings": [...]              //   設定フィールド(任意)
  }
}

グリッドサイズの目安

3×2 〜 4×3時計、ステータス表示
4×3 〜 6×4天気、リスト表示
6×4 〜 12×6画像、データテーブル

SDK リファレンス (conomaSDK)

プラグインHTML内で共有SDKを読み込むと window.conomaSDK が利用可能になります。

<script src="../sdk/tabane-sdk.js"></script>

後方互換性のため window.tabaneSDK もエイリアスとして利用可能です。

ストレージ

キーバリュー形式の永続ストレージ。JSON値を保存できます。

// 保存
await conomaSDK.storage.set('count', 42)
await conomaSDK.storage.set('config', { theme: 'dark', limit: 10 })

// 取得
const count = await conomaSDK.storage.get('count')  // 42
const config = await conomaSDK.storage.get('config') // { theme: 'dark', limit: 10 }

// 全データ取得
const all = await conomaSDK.storage.getAll()
// { count: 42, config: { theme: 'dark', limit: 10 } }
ℹ️ウィジェットのストレージはインスタンスごとに独立します。同じプラグインのウィジェットを2つ配置しても、データは共有されません。アプリのストレージはプラグインIDで共有されます。

HTTP リクエスト (fetch)

外部APIを呼び出せます。メインプロセス経由のプロキシのため、CORS / CSP の制約を受けません。

const res = await conomaSDK.fetch('https://api.example.com/data')

// レスポンス型
// { ok: boolean, status: number, body: string }

if (res.ok) {
  const data = JSON.parse(res.body)
  console.log(data)
} else {
  console.error('Error:', res.status)
}
⚠️レスポンスの body は常に文字列です。JSONの場合は JSON.parse(res.body) で変換してください。

通知

await conomaSDK.notify('新しいメッセージ', '3件の未読があります')

OSのデスクトップ通知を表示します。

テーマ・プラグインID

const theme = await conomaSDK.getTheme()     // 'dark' | 'light' | 'system'
const pluginId = await conomaSDK.getPluginId() // 'com.example.my-plugin'

ウィジェット設定 (widget.settings)

manifestで設定フィールドを宣言すると、ホストが自動的に設定UIを生成します。 ユーザーが設定した値はプラグインストレージに保存され、conomaSDK.storage で読み取れます。

text — テキスト入力

自由入力のテキストフィールド。タイトル、URL、メモなどに。

// manifest.json
{ "id": "title", "type": "text", "label": "タイトル", "placeholder": "カスタムタイトル" }

// widget.html で読み取り
const title = await conomaSDK.storage.get('title')
// → ユーザーが入力した文字列 or null

secret — マスク付き入力

APIキーやトークンなど、画面上でマスク表示したい値に。「表示/隠す」トグル付き。

// manifest.json
{ "id": "apiKey", "type": "secret", "label": "APIキー", "placeholder": "sk-..." }

// widget.html で読み取り
const apiKey = await conomaSDK.storage.get('apiKey')

select — ドロップダウン選択

固定の選択肢から1つを選ぶ。表示モード、更新間隔、言語などに。

// manifest.json
{
  "id": "mode",
  "type": "select",
  "label": "表示モード",
  "options": [
    { "value": "compact", "label": "コンパクト" },
    { "value": "detail", "label": "詳細" }
  ]
}

// widget.html で読み取り
const mode = await conomaSDK.storage.get('mode')
// → "compact" or "detail" or null(未選択)

location — 地域検索

都市名で検索して緯度・経度を保存。天気やローカル情報の取得に。検索にはNominatim(OpenStreetMap)を使用。

// manifest.json
{ "id": "location", "type": "location", "label": "地域", "placeholder": "都市名で検索" }

// widget.html で読み取り
const lat = await conomaSDK.storage.get('lat')   // 35.6762
const lon = await conomaSDK.storage.get('lon')   // 139.6503
const city = await conomaSDK.storage.get('city') // "東京"
ℹ️location タイプは id フィールドに関係なく、常に lat, lon, city キーに保存されます。

photos — 写真アップロード

複数の画像をアップロードしてBase64 Data URLとして保存。ギャラリー、背景画像などに。

// manifest.json
{ "id": "photos", "type": "photos", "label": "写真" }

// widget.html で読み取り
const count = await conomaSDK.storage.get('photoCount') // 3
const photo0 = await conomaSDK.storage.get('photo_0')   // "data:image/jpeg;base64,..."
const photo1 = await conomaSDK.storage.get('photo_1')
const photo2 = await conomaSDK.storage.get('photo_2')
⚠️写真はBase64エンコードされてlocalStorageに保存されるため、大量の高解像度画像はパフォーマンスに影響する可能性があります。

設定変更の反映

ユーザーが設定モーダルを閉じると、ウィジェットのiframeが自動的にリロードされます。 特別な処理なしで、init() 関数で最新の設定値を読み込むだけでOKです。

カスタムカラー (widget.colors)

ダッシュボードのスタイル設定でユーザーがカスタマイズ可能な色を定義できます。

// manifest.json
"colors": [
  { "id": "accent", "name": "アクセント色", "default": "#4a6cf7" },
  { "id": "bg", "name": "背景色", "default": "rgba(0,0,0,0.3)" }
]

CSS変数 --plugin-{id} として自動的に利用可能:

body {
  color: var(--plugin-text, #e0e0f0);       /* ホストが提供 */
  background: var(--plugin-bg, rgba(0,0,0,0.3)); /* プラグイン定義 */
}
.button {
  background: var(--plugin-accent, #4a6cf7); /* プラグイン定義 */
}

ホスト提供のCSS変数

全プラグインで自動的に利用可能な変数:

--plugin-text文字色
--plugin-accentアクセント色
--plugin-surfaceコントロール面の色
--plugin-muted補助文字色

サンドボックス

プラグインは sandbox="allow-scripts allow-same-origin" の iframe 内で実行されます。

✅ 許可

  • JavaScript実行
  • SDK経由のストレージ読み書き
  • SDK経由のHTTPリクエスト
  • 位置情報API(geolocation)
  • Canvas / WebGL
  • Web Audio API
  • CSS アニメーション / トランジション

🚫 制限

  • ファイルシステムアクセス
  • Node.js API
  • ホストページのDOM操作
  • 直接的な外部HTTPリクエスト
  • フォーム送信 / ポップアップ
  • Cookie / IndexedDB(直接)

ウィジェットのライフサイクル

  1. 読み込み — iframeが生成され、widget.html が読み込まれる
  2. SDK初期化tabane-sdk.js がホストにテーマを要求し、CSS変数を適用
  3. 初期化 — プラグインの init() 等でストレージから設定を読み取り、UIを構築
  4. 更新setInterval でデータを定期取得・表示更新
  5. 設定変更 — ユーザーが設定モーダルを閉じるとiframeが再生成(1に戻る)
  6. 削除 — ユーザーがウィジェットを削除するとiframeが破棄
// 推奨パターン
async function init() {
  const settings = await conomaSDK.storage.getAll()
  // 設定に基づいてUIを構築
  render(settings)
}

function render(settings) {
  // DOMを更新
}

// 初期化
init()

// 定期更新(必要に応じて)
setInterval(init, 60 * 1000)

配布とバージョン管理

ZIPパッケージ

プラグインのルートディレクトリを直接ZIPに圧縮(フォルダを含めない):

cd my-plugin/
zip -r my-plugin.zip .
# ZIPのルートに manifest.json が必要

ローカルテスト

  1. ZIPを作成
  2. Conomaのプラグイン管理 →「ZIPから追加」
  3. ダッシュボードでウィジェットを追加して動作確認
  4. 修正したら再度ZIPを作成 → 一度削除 → 再インストール

カタログ公開

Conomaのプラグインカタログに公開するには:

  1. ZIPとアイコンをSupabase Storageにアップロード
  2. plugins テーブルにメタデータを登録
  3. ユーザーはアプリ内カタログからインストール可能に

バージョン更新

カタログのバージョンがインストール済みと異なる場合、プラグイン管理画面に「更新」ボタンが表示されます。 ダッシュボードにも更新通知バナーが表示されます。

実践例

例1: 外部API連携ウィジェット

APIキーを設定で管理し、データを定期取得して表示

// manifest.json
{
  "id": "com.example.api-widget",
  "name": "API連携",
  "version": "1.0.0",
  "description": "外部APIからデータを取得",
  "widget": {
    "entry": "widget.html",
    "minW": 4, "minH": 3,
    "defaultW": 5, "defaultH": 4,
    "settings": [
      { "id": "apiKey", "type": "secret", "label": "APIキー" },
      { "id": "endpoint", "type": "text", "label": "エンドポイント",
        "placeholder": "https://api.example.com/data" },
      { "id": "interval", "type": "select", "label": "更新間隔",
        "options": [
          { "value": "60", "label": "1分" },
          { "value": "300", "label": "5分" },
          { "value": "900", "label": "15分" }
        ]}
    ]
  }
}
<!-- widget.html -->
<!DOCTYPE html>
<html>
<head>
  <script src="../sdk/tabane-sdk.js"></script>
  <style>
    body {
      margin: 0; padding: 16px;
      font-family: sans-serif;
      color: var(--plugin-text, #e0e0f0);
    }
    .error { color: #f87171; }
    .data { font-size: 14px; }
    .setup { color: var(--plugin-muted); font-size: 13px; }
  </style>
</head>
<body>
  <div id="app" class="setup">設定からAPIキーを入力してください</div>
  <script>
    async function init() {
      const s = await conomaSDK.storage.getAll()
      if (!s.apiKey || !s.endpoint) return

      const res = await conomaSDK.fetch(
        s.endpoint + '?key=' + s.apiKey
      )
      const app = document.getElementById('app')
      if (res.ok) {
        const data = JSON.parse(res.body)
        app.className = 'data'
        app.textContent = JSON.stringify(data, null, 2)
      } else {
        app.className = 'error'
        app.textContent = 'エラー: ' + res.status
      }
    }

    init()
    conomaSDK.storage.get('interval').then(function(sec) {
      setInterval(init, (parseInt(sec) || 300) * 1000)
    })
  </script>
</body>
</html>

例2: 天気ウィジェット(location設定)

設定で地域を選択し、Open-Meteo APIから天気を取得

// manifest.json の settings
"settings": [
  { "id": "location", "type": "location", "label": "地域",
    "placeholder": "都市名で検索" }
]

// widget.html で利用
async function init() {
  const s = await conomaSDK.storage.getAll()
  if (!s.lat || !s.lon) {
    showMessage('設定から地域を選択してください')
    return
  }
  const res = await conomaSDK.fetch(
    'https://api.open-meteo.com/v1/forecast'
    + '?latitude=' + s.lat
    + '&longitude=' + s.lon
    + '&current=temperature_2m,weather_code'
  )
  if (res.ok) {
    const data = JSON.parse(res.body)
    showWeather(data.current, s.city)
  }
}

例3: フォトギャラリー(photos設定)

設定から写真をアップロードし、スライドショー表示

// manifest.json の settings
"settings": [
  { "id": "mode", "type": "select", "label": "表示モード",
    "options": [
      { "value": "slideshow", "label": "スライドショー" },
      { "value": "random", "label": "ランダム" }
    ]},
  { "id": "interval", "type": "select", "label": "切替間隔",
    "options": [
      { "value": "5", "label": "5秒" },
      { "value": "10", "label": "10秒" }
    ]},
  { "id": "photos", "type": "photos", "label": "写真" }
]

// widget.html で利用
async function init() {
  const s = await conomaSDK.storage.getAll()
  const count = parseInt(s.photoCount || '0')
  const photos = []
  for (let i = 0; i < count; i++) {
    if (s['photo_' + i]) photos.push(s['photo_' + i])
  }
  if (photos.length === 0) return

  if (s.mode === 'random') {
    showPhoto(photos[Math.floor(Math.random() * photos.length)])
  } else {
    startSlideshow(photos, parseInt(s.interval || '10'))
  }
}

開発Tips

デバッグ

  • 開発者ツール(Cmd+Option+I)でプラグインiframeのconsole.logを確認
  • iframeのセレクターで対象プラグインのコンテキストに切り替え
  • conomaSDK.storage.getAll() で現在の保存データを確認

パフォーマンス

  • API呼び出しは必要最低限の頻度に(5分以上推奨)
  • DOMの更新は差分のみ。全体の再描画は避ける
  • 画像はリサイズしてからストレージに保存
  • 大量データはページネーションで分割表示

テーマ対応

  • 必ず var(--plugin-text) でテキスト色を指定(ハードコードしない)
  • 背景色は透明にして、ホストのウィジェット背景設定に従う
  • ボーダーやシャドウは控えめに(ホスト側の設定と競合するため)