プラグイン開発ガイド
概要
Conomaプラグインは、アプリ(フルスクリーンiframe)とウィジェット(ダッシュボードグリッド内iframe)の いずれか、または両方を提供できる拡張機能です。HTML + CSS + JavaScriptで開発できます。
クイックスタート: 5分で作るウィジェット
最小構成のウィジェットプラグインを作成してみましょう。
1. ディレクトリを作成
mkdir my-widget && cd my-widget2. 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 } }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 nullsecret — マスク付き入力
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') // "東京"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')設定変更の反映
ユーザーが設定モーダルを閉じると、ウィジェットの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(直接)
ウィジェットのライフサイクル
- 読み込み — iframeが生成され、
widget.htmlが読み込まれる - SDK初期化 —
tabane-sdk.jsがホストにテーマを要求し、CSS変数を適用 - 初期化 — プラグインの
init()等でストレージから設定を読み取り、UIを構築 - 更新 —
setIntervalでデータを定期取得・表示更新 - 設定変更 — ユーザーが設定モーダルを閉じるとiframeが再生成(1に戻る)
- 削除 — ユーザーがウィジェットを削除すると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 が必要ローカルテスト
- ZIPを作成
- Conomaのプラグイン管理 →「ZIPから追加」
- ダッシュボードでウィジェットを追加して動作確認
- 修正したら再度ZIPを作成 → 一度削除 → 再インストール
カタログ公開
Conomaのプラグインカタログに公開するには:
- ZIPとアイコンをSupabase Storageにアップロード
pluginsテーブルにメタデータを登録- ユーザーはアプリ内カタログからインストール可能に
バージョン更新
カタログのバージョンがインストール済みと異なる場合、プラグイン管理画面に「更新」ボタンが表示されます。 ダッシュボードにも更新通知バナーが表示されます。
実践例
例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
+ '¤t=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)でテキスト色を指定(ハードコードしない) - 背景色は透明にして、ホストのウィジェット背景設定に従う
- ボーダーやシャドウは控えめに(ホスト側の設定と競合するため)