ui-lift
by Jinshin2534
README.md
# ui-lift
公開サイトを取り込み、そこで使われているUIコンポーネントとデザイントークンを、
AIがMCP経由で実装に使える形で取り出すツールです。主な利用者はAI(Claude Code等の
コーディングエージェント)で、人はMCPの `open_gallery` が開くギャラリーで取り込み
結果を眺め、不要なものを間引いてライブラリの品質を保ちます。
## これは何ではないか
デザインの参考にするためのツールであり、取り込んだサイトの複製・再公開を
目的としません。
- 画像・ロゴ・フォントの実体(バイナリ)はダウンロードしません。加えて、
取り込み結果には元サイトを指す**アセットURL参照も残りません**(例外は
下記の「意図的に残る参照」の1点のみです)。属性は「捨てるものを列挙する」
のではなく「残すものを列挙する」許可リスト方式です(`capture/snapshot.py`
の `RETAINED_ATTRS` / `_prune_attrs()`): `type`, `alt`, `role`, `disabled`,
`checked`, `required`, `readonly`, `selected`, `multiple`, `open`,
`controls`, `placeholder`, `aria-*` だけが値ごと残り、それ以外の属性は
タグを問わずすべて破棄されます。したがって `<img src>` / `srcset`、
`<video poster>`、`<source src>` はもちろん、`<iframe src>`、
`<embed src>`、`<object data>`、`<track src>`、`<body background>`、
SVG の `<image href>` のように個別に列挙していなかった参照も、次に
出てくる未知のタグ・属性も同じ規則で自動的に落ちます。`href`
(`<a>` / `<area>` / `<link>` など)だけは特別扱いで、仕様が「値」ではなく
「有無」を残すとしているとおり、値を空文字列 `""` に置き換えて
「href が存在した」という構造情報だけを残します。`alt` などの意味を持つ
属性と、寸法・レイアウトを表す `rect` は変わらず残ります。CSS側は特定の
プロパティ名を決め打ちにせず、`CAPTURED_PROPS`(取得対象のCSSプロパティ
一覧)に含まれるどのプロパティであっても、値に `url(...)` 参照が1つでも
含まれていれば取り除きます(`background-image` はもちろん、カスタム
カーソル画像を指定する `cursor: url(...)` のような他のプロパティでも
同様です)。値はカンマ区切りの「レイヤー」の並びとして扱い、`url(...)` を
含むレイヤーだけを落とします。`linear-gradient()` のような他のレイヤーや、
`cursor: url(...), auto` の `auto` のようなキーワードフォールバックは、
元サイトの参照を持たない限りそのまま残ります。全レイヤーが `url(...)`
だけだった場合(安全に残せる断片が何もない場合)に限り、値全体を `none`
に置き換えます。ページの表示自体にはブラウザが元サイトから画像・フォント
を取得しますが、そのバイナリやURLがui-liftの保存物(スナップショット/
ライブラリ/生成コード)に載ることはありません。
- `background-image` が `linear-gradient(...)` と `url(...)` を1つの値の
中で組み合わせている場合、`url(...)` レイヤーだけが落ち、グラデーション
レイヤーは残ります。全レイヤーが `url(...)` のときだけ `none`(背景なし)
になり、複数の背景画像を重ねていたページでは結果がフラットな背景に
見えることがあります。
- **意図的に残る参照**: `get_component` / `get_design_tokens` の出力先頭に
付く出典コメント(下記)には、取得元ページの URL(`source_url`、
ページ全体のURLであってアセットURLではない)がそのまま文字として
載ります。これは帰属表示のための意図した仕様で、`render/html_css.py`
の `_neutralize_html_comment_value` により HTML コメント終端シーケンス
(`-->` / `--!>`)は無害化されるため、値がコメントを抜け出して生きた
マークアップになることはありません。この1箇所以外に、元サイトを指す
参照が出力に残る経路は現時点でありません。
- `<style>` 要素に埋め込まれる CSS 値(`StyleValue.raw`、取得したページの
computed style をそのまま使う)についても、`</style>` のような HTML の
生テキスト終了パターンを含む宣言は丸ごと破棄します(`render/html_css.py`
の `_declaration`)。HTML は `<style>` を「生テキスト要素」として扱い、
CSS の文字列・コメント構文に関係なく `</` + 英字で終端するため、
`</style>` の文字列一致だけでは `</style foo>` や `</STYLE>` を防げず、
ブラウザの`getComputedStyle`が返した値をそのまま埋め込む以上、CSS引用符
の外側にリテラルに現れうる値として一般化して扱う必要があるためです。
- 元サイトの `class` / `id` は保存しません(構造シグネチャの安定のためにも
意図的に捨てています)。生成されるクラス名は `uil-<kind>` 形式の独自命名です。
- robots.txt を尊重します。`Disallow` されているパスは取り込み時に例外
(`RobotsDisallowed`) になり、取り込まれません。
- ログインが必要なページは対象外です。ブラウザは毎回まっさらなコンテキストで
開くだけで、Cookie・認証情報を扱う仕組みは持ちません。
- `get_component` / `get_design_tokens` が返すコードには、必ず取得元URLと
取得日時(と「デザインの参考用であり複製を目的としない」旨)がコメントとして
先頭に入ります。
## セットアップ
```bash
uv sync
uv run playwright install chromium
```
Playwrightは取り込み(`ingest_site`)のたびにHeadless Chromiumを起動します。
`playwright install chromium` を済ませていないと、取り込み時にブラウザ未検出の
エラーになります。
## Claude Code への登録
```bash
claude mcp add ui-lift -- uv --directory /path/to/ui-lift run ui-lift
```
`/path/to/ui-lift` はこのリポジトリの絶対パスに置き換えてください。PyPIには
まだ公開していないので `uvx ui-lift` は使えません。
## MCPツール
| ツール | 用途 |
|---|---|
| `ingest_site(url, viewports=["desktop"], min_occurrences=2)` | サイトを取り込む(ネットワークアクセスあり) |
| `reanalyze_site(site, min_occurrences=2)` | 保存済みスナップショットから、ネットワークに触れずコンポーネント・トークンを作り直す |
| `list_sites()` | 取り込み済みサイト一覧 |
| `search_components(query=None, kind=None, site=None, limit=20)` | 検索(要約のみ返す) |
| `get_component(id, format="html-css", variant="default")` | 貼れるコードを取り出す |
| `get_design_tokens(site, format="css-vars")` | トークン一式を取り出す |
| `open_gallery(site=None)` | ギャラリーを生成しブラウザで開く |
`search_components` と `get_component` を分けているのは、AIが検索結果を全部
読み込まずに済むようにするためです。検索は軽い要約(id / site / kind / name /
occurrences / summary)だけを返し、1つに絞ってから重いコードを `get_component`
で取りに行く想定です。
`viewports` に渡せるのは `mobile`(390×844) / `tablet`(820×1180) / `desktop`
(1440×900) の3つです。複数指定すると、`desktop`(無ければ指定した中の最初の
ビューポート)を基準に、他のビューポートとの間でスタイルの差分を
`Component.responsive` にビューポートごと・ノードパスごとで記録します。
副ビューポートの解析はベストエフォートで、失敗しても `ingest_site` の結果の
`failed_viewports` に記録されるだけで取り込み全体は失敗しません。ただし現状
`responsive` の中身をレンダー出力やMCPのレスポンスとして直接取り出す経路は
まだありません(ライブラリのJSONに保存されるのみです)。
`format` は `html-css` / `css-vars` / `tailwind` の3つです。
- `html-css`: HTML断片+`<style>`ブロック。クラス名は `uil-<kind>` または
そこにタグ・パスを付与したもの。**単独では自己完結しません**:
トークンに乗った値は `var(--color-brand)` のような参照のまま出力され、
そのカスタムプロパティ自体はここでは定義されません(定義するのは
`css-vars` / `get_design_tokens` の役目)。実装先のページに同じトークン
定義がまだ無い場合は `get_design_tokens(site, format="css-vars")` も
合わせて取得し、一緒に貼り付けてください。`get_component` のレスポンス
の `notes` にもこの旨が明記されます。ギャラリー(後述)のプレビューは
この理由から、各コンポーネントの `html-css` 出力だけでなく
`css-vars` 出力もあわせて同じ `<iframe srcdoc>` に埋め込んでいます。
- `css-vars`: `:root { --color-brand: #...; }` 形式のCSSカスタムプロパティ。
トークンのみを扱う形式で、個々のコンポーネントの構造には触れません。
**`get_component(..., format="css-vars")` はエラーにならず、指定した
`id` / `variant` を無視してサイト全体のトークンをそのまま返します**
(実質 `get_design_tokens(site="css-vars")` と同じ結果)。この経路を
使ったことに呼び出し側(AI)が気づけるよう、レスポンスの `notes` に
「id / variant は無視されている」旨が明記されます。出典コメント(取得元
URL・取得日時)は `get_component` / `get_design_tokens` のどちらの経路
でも必ず付きます。コンポーネント個別のコードが欲しい場合は `html-css` か
`tailwind` を使ってください。
- `tailwind`: `tailwind.config.js` の `theme.extend` スニペット+(コンポーネント
指定時は)Tailwindユーティリティクラス付きマークアップ。
`kind` は `button` / `input` / `nav` / `card` / `table` / `list-item` /
`badge` / `hero` / `block` の9種類です。DOM構造とスタイルからの推定なので
外れることがあります。ギャラリーの「種類」ボタンで直せます。
### Tailwind出力のキーに `uil-` 接頭辞が付く理由
`tailwind.config.js` の `theme.extend` はTailwindの組み込みスケールに
**マージされ、同名キーを上書きします**(別名前空間を作るのではありません)。
つまり生成した `spacing["2"]` や `borderRadius["md"]` をそのまま貼り付けると、
既存プロジェクトの `p-2` や `rounded-md` の意味を静かに書き換えてしまいます。
これを避けるため、生成するスケールキーには必ず `uil-` を前置します
(`spacing["uil-2"]`, `borderRadius["uil-md"]` のように)。Tailwindの
組み込みキーは数字や `sm`/`md`/`lg` のような短い名前なので、`uil-` を
付けることで衝突がなくなることを保証しています。
## 使い方の流れ
1. `ingest_site` でサイトを取り込む
2. `open_gallery` で結果を眺め、ゴミを捨てて(`keep=false`)、必要なら名前と
種類を直す
3. 以降は `search_components` → `get_component` でAIが必要なものを取り出す
2 の選別をやるほど 3 の質が上がります。**同じURLを`ingest_site`で再取り込み
すると、人が付けた名前・種類・`keep`(残す/捨てる)の判定はそのまま引き継がれ
ます。** 前回あったコンポーネントが今回のページに見当たらなくなった場合も
削除はせず `stale=true` を立てるだけです(再度現れれば `stale` は自動的に
外れます)。`stale` のコンポーネントは検索から除外されません
(削除ではなく印を付けるだけにしているのが目的なので、引き続き
`search_components` / `get_component` で取り出せます)が、結果の各項目に
`stale` が含まれるので呼び出し側(AI)は見分けられ、出現数が同じ場合は
`stale` のものが後ろに並びます。
### ギャラリー
`open_gallery` はローカルのみで待ち受けるHTTPサーバ(`127.0.0.1`、ポートは
自動割当)を起動し、既定のブラウザでそのURLを開きます。各コンポーネントは
`sandbox=""` を付けた `<iframe srcdoc="...">` の中に個別描画されます。これは、
取り込んだページ由来のCSS(クラス名がコンポーネント間で衝突しうる、任意の
セレクタを含みうる等)がギャラリー自身のUI(他のタイルやボタン)を壊したり
隠したりしないようにするためです。iframeの外に一切漏れません。
画面でできること:
- **名前** ボタン: コンポーネント名を変更
- **種類** ボタン: `kind` を直接入力し直す(無効な値はサーバ側の
`POST /edit` が400で拒否)
- **捨てる** ボタン: `keep=false` にする。以後 `search_components` の結果や
ギャラリー自体(次回生成時)から外れます(ライブラリのJSONからは消えません)
サムネイル画像は撮っていません。ギャラリーは `html-css` 出力をその場で
レンダリングして並べる作りなので、事前に画像を用意する必要がありません
(`Component.screenshot` と `Store.thumb_path` は将来のための予約フィールドで、
現状どこからも書き込まれません)。
## 保存先
既定は `~/.ui-lift/`。環境変数 `UI_LIFT_HOME` で変更できます。
```
<UI_LIFT_HOME>/library/<site-slug>/
site.json # URL・取得日時・デザイントークンを含むサイト情報
components/<id>.json # コンポーネント(IR)。keep=false でも消えず残る
snapshots/<viewport>.json # 再解析用の生スナップショット(ビューポートごと)
```
`site-slug` はURLのホスト名から生成されます(例: `https://example.com/` →
`example-com`)。
スナップショットを残しているので、検出アルゴリズムを改善したときに再取得
せずにライブラリ全体を作り直せます。実際にそれをやるのが `reanalyze_site`
MCPツールです。指定したサイトの保存済みスナップショット(全ビューポート)
を読み、`ingest_site` と全く同じマージ(人が付けた名前・種類・`keep` の
引き継ぎ、消えたコンポーネントへの `stale` 印付け)を経てライブラリを
更新します。ネットワークには一切触れません。保存済みスナップショットが
1つも無いサイトを指定すると、そのサイト名を含むエラーになります。
## 開発
```bash
uv run pytest
```
`analyze/` と `render/` は純粋関数のみで構成されており、テストは保存済みの
スナップショットJSON(または手書きのフィクスチャ)に対して行います。テスト内で
外部サイトにはアクセスしません。E2Eテスト(`tests/test_e2e.py`)は
`tests/fixtures/pages/` のローカルHTMLをテスト内で自前のHTTPサーバから配信し、
それに対してのみ実行します。
TDQS
B3.4/5.0
Scored across 7 tools
Disambiguation5/5
Each tool has a clear, distinct purpose: ingestion (live or snapshot), listing, searching, retrieving components, retrieving tokens, and opening a gallery. No overlaps.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern in snake_case (e.g., ingest_site, list_sites, get_component) with appropriate singular/plural forms.
Tool Count5/5
7 tools is well-scoped for the domain of UI component extraction, covering ingestion, listing, searching, retrieval, and gallery viewing without being excessive.
Completeness4/5
The set covers core workflows (ingest, list, search, retrieve, tokens, open gallery). Minor gaps include missing delete/update for sites or components, but these are not essential for the primary extraction purpose.
Maintenance
ActivityStale
ResponsivenessNo issues