make-tweakable について
既存の HTML 成果物に、成果物内で動作する Tweaks パネルを追加するスキルです。少数の調整値をコントロールに結び付け、後で HTML に反映する変更を記録します。
主な機能
- 01選択した値を CSS カスタムプロパティに置換
- 02スキーマでコントロールと初期値を定義
- 03HTML 成果物にフローティングパネルを挿入
- 04File System Access API またはコピー&ペーストで保留中の変更を保存
- 05Shift+T でパネル表示を切り替え
活用シーン
- 01ページプレビューで色やタイポグラフィを調整
- 02余白やレイアウトのバリエーションを対話的に比較
- 03HTML に反映する設定を一時保存
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bluzir/ claude-code-design --skill make-tweakableインストール後、Claude Code で/make-tweakableを入力して読み込まれたか確認します。
詳細
調整可能にする
既存の HTML アーティファクトに、フローティングの「調整」パネルを挿入します。パネルはアーティファクト内に配置され(iframe は使用しません)、永続化には File System Access API を使用します(コピー&ペーストによるフォールバックあり)。
パターン
- 調整可能なデフォルト値を、
:rootの--tweak-プレフィックス付き CSS カスタムプロパティでラップします - デフォルト値を含むマーカーブロック
<script id="__tweak_schema" type="application/json">{...}</script>を追加します - 各キーに紐づくコントロールを備えたフローティングパネル(
<div id="tweaks-panel">)を挿入します - パネルは File System Access API を使って
pending.yamlに更新内容を書き込みます(初回使用時に、対象ファイルを選択するよう求めます) Shift+Tでパネルの表示を切り替えます- ユーザーが
/apply-tweaks <html-path>を実行するか「save tweaks」と伝えると、Claude がpending.yamlを読み込み、Editツールで HTML を編集し、保留中の内容をクリアします
フェーズ 1 — キーを決める
ユーザーがキー($ARGUMENTS)を指定している場合は、それを使用します。指定されていない場合は、アーティファクトから推測します。
- 色(primaryColor、accentColor、bgColor、textColor)
- タイポグラフィ(fontFamily、fontSize、lineHeight)
- 余白(gap、padding)
- レイアウトのバリエーション(dense/comfortable/spacious)
- ダークモードの切り替え
絞り込みます。項目は 20 個ではなく、4〜7 個にしてください。
フェーズ 2 — HTML を --tweak- 変数で使うようにリファクタリングする
キーごとに、HTML 内のハードコードされた値を探し、var(--tweak-keyName) に置き換えます。デフォルト値を :root に設定します。
:root {
--tweak-primaryColor: #D97757;
--tweak-fontSize: 16px;
--tweak-density: 1; /* multiplier */
}
スキーマブロックを追加します。
<script id="__tweak_schema" type="application/json">
{
"primaryColor": { "type": "color", "default": "#D97757" },
"fontSize": { "type": "number", "default": 16, "min": 12, "max": 32, "step": 1, "unit": "px" },
"density": { "type": "number", "default": 1, "min": 0.75, "max": 1.5, "step": 0.05 },
"dark": { "type": "boolean", "default": false }
}
</script>
フェーズ 3 — 調整パネルを挿入する
</body> の前に追加します。
<script>
(async () => {
const schemaEl = document.getElementById('__tweak_schema');
if (!schemaEl) return;
const schema = JSON.parse(schemaEl.textContent);
const values = Object.fromEntries(Object.entries(schema).map(([k, v]) => [k, v.default]));
// Restore from localStorage for in-browser state
const lsKey = '__tweaks_' + location.pathname;
try { Object.assign(values, JSON.parse(localStorage.getItem(lsKey) || '{}')); } catch {}
const panel = document.createElement('div');
panel.id = 'tweaks-panel';
panel.hidden = true;
panel.style.cssText = `
position: fixed; right: 16px; bottom: 16px; z-index: 99999;
font: 13px/1.4 ui-sans-serif, system-ui;
background: #fff; color: #111;
border: 1px solid #ddd; border-radius: 10px;
padding: 14px; min-width: 240px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
`;
const applyValues = () => {
const root = document.documentElement;
for (const [k, v] of Object.entries(values)) {
const meta = schema[k];
if (meta.type === 'color' || meta.type === 'string') root.style.setProperty('--tweak-' + k, v);
else if (meta.type === 'number') root.style.setProperty('--tweak-' + k, v + (meta.unit || ''));
else if (meta.type === 'boolean') root.classList.toggle('tweak-' + k, !!v);
}
try { localStorage.setItem(lsKey, JSON.stringify(values)); } catch {}
};
const header = document.createElement('div');
header.style.cssText = 'font-weight:600;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center';
header.innerHTML = `<span>Tweaks</span><span style="opacity:0.5;font-weight:400;font-size:11px">Shift+T to toggle</span>`;
panel.appendChild(header);
for (const [key, meta] of Object.entries(schema)) {
const row = document.createElement('label');
row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;gap:12px;margin:8px 0';
const label = document.createElement('span');
label.textContent = key;
label.style.cssText = 'font-family:ui-monospace,Menlo;font-size:11px;opacity:0.7';
row.appendChild(label);
let input;
if (meta.type === 'color') {
input = document.createElement('input');
input.type = 'color';
input.value = values[key];
} else if (meta.type === 'number') {
input = document.createElement('input');
input.type = 'range';
input.min = meta.min; input.max = meta.max; input.step = meta.step;
input.value = values[key];
input.style.width = '100px';
} else if (meta.type === 'boolean') {
input = document.createElement('input');
input.type = 'checkbox';
input.checked = !!values[key];
} else {
input = document.createElement('input');
input.type = 'text';
input.value = values[key];
}
input.addEventListener('input', () => {
values[key] = meta.type === 'boolean' ? input.checked :
meta.type === 'number' ? Number(input.value) : input.value;
applyValues();
scheduleWrite();
});
row.appendChild(input);
panel.appendChild(row);
}
// Persistence buttons
const actions = document.createElement('div');
actions.style.cssText = 'display:flex;gap:6px;margin-top:10px;padding-top:10px;border-top:1px solid #eee';
const linkBtn = document.createElement('button');
linkBtn.textContent = 'Link pending.yaml';
linkBtn.style.cssText = 'flex:1;padding:6px 8px;font-size:11px;background:#f5f5f5;border:1px solid #ddd;border-radius:4px;cursor:pointer';
const copyBtn = document.createElement('button');
copyBtn.textContent = 'Copy YAML';
copyBtn.style.cssText = linkBtn.style.cssText;
actions.appendChild(linkBtn);
actions.appendChild(copyBtn);
panel.appendChild(actions);
const status = document.createElement('div');
status.style.cssText = 'margin-top:8px;font-size:10px;color:#888';
status.textContent = 'Not persisted';
panel.appendChild(status);
let fsHandle = null;
let writeTimer = null;
const yamlFor = (v) => Object.entries(v).map(([k,val]) => `${k}: ${JSON.stringify(val)}`).join('\n') + '\n';
const scheduleWrite = () => {
if (!fsHandle) return;
clearTimeout(writeTimer);
writeTimer = setTimeout(async () => {
try {
const w = await fsHandle.createWritable();
await w.write(yamlFor(values));
await w.close();
status.textContent = 'Written to pending.yaml @ ' + new Date().toLocaleTimeString();
} catch (e) {
status.textContent = 'Write failed: ' + e.message;
}
}, 300);
};
linkBtn.addEventListener('click', async () => {
if (!window.showSaveFilePicker) {
status.textContent = 'File System Access API not available — use Copy YAML instead';
return;
}
try {
fsHandle = await window.showSaveFilePicker({
suggestedName: 'pending.yaml',
types: [{ description: 'YAML', accept: { 'text/yaml': ['.yaml'] } }],
});
status.textContent = 'Linked. Writing live.';
scheduleWrite();
} catch (e) {
status.textContent = 'Link cancelled';
}
});
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(yamlFor(values));
status.textContent = 'Copied YAML to clipboard. Paste to Claude and say "apply tweaks"';
} catch (e) {
status.textContent = 'Copy failed: ' + e.message;
}
});
document.body.appendChild(panel);
applyValues();
window.addEventListener('keydown', (e) => {
if (e.shiftKey && (e.key === 'T' || e.key === 't')) {
panel.hidden = !panel.hidden;
}
});
})();
</script>
フェーズ 4 — セッション状態ファイル
Bash(mkdir -p artifacts/tweaks/<session-id>/applied)
Write artifacts/tweaks/<session-id>/state.yaml:
target_html: <path>
phase: idle
last_applied_at: null
session-id = tweaks-{slug}-{YYYYMMDD}。
ユーザーには次のように伝えます。「調整機能が有効になりました。プレビューで Shift+T を押すと開きます。『Link pending.yaml』をクリックし、artifacts/tweaks/<session-id>/pending.yaml を選択してください。これで、パネルは変更のたびにリアルタイムで書き込むようになります。調整が完了したら、『apply tweaks』と伝えるか、/apply-tweaks <html-path> を実行してください。」
フェーズ 5 — 検証する
アーティファクトが外部の .jsx スターターを使用している場合は /serve を実行してから、/done http://127.0.0.1:4567/<html-path> を実行します。パネルが表示され、Shift+T で表示が切り替わり、コンソールエラーがないことを確認します。
永続化に関する注意: File System Access API には http オリジンが必要です(Chrome >= 2024 では file:// でも動作しますが、制約があります)。調整可能なアーティファクトは、常に http 経由で配信することをおすすめします。