@nickname4th/pura-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/client/assets/index-0nO8CWi5.css +1 -0
- package/dist/client/assets/index-BI4BBLE6.js +12 -0
- package/dist/client/index.html +13 -0
- package/package.json +69 -0
- package/server/dist/adb.js +183 -0
- package/server/dist/agent.js +179 -0
- package/server/dist/cli.js +286 -0
- package/server/dist/config.js +17 -0
- package/server/dist/device-id.js +13 -0
- package/server/dist/hub.js +287 -0
- package/server/dist/index.js +67 -0
- package/server/dist/network.js +20 -0
- package/server/dist/presence.js +92 -0
- package/server/dist/registry.js +61 -0
- package/server/dist/screenshots.js +59 -0
- package/server/dist/sessions.js +229 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pura contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# pura
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
pura is a LAN Android device mirror for product and design teams. A central Hub shows all online Android devices, while each developer runs a local Agent that talks to their own USB-connected phone through ADB.
|
|
6
|
+
|
|
7
|
+
No login, no cloud, no public tunnel. It is meant for trusted office networks.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
Start the Hub with Docker Compose:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
docker compose up -d
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open the Hub:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
http://<hub-lan-ip>:8787
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On each developer machine, connect an Agent:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @nickname4th/pura-cli connect <hub-lan-ip>:8787 --name "Zhang San"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
On macOS, keep the Agent connected after login or terminal close. Install the CLI globally first so the background service has a stable executable path, then connect with `--background`:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g @nickname4th/pura-cli
|
|
33
|
+
pura-cli connect <hub-lan-ip>:8787 --name "Zhang San" --background
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Publish the local Android device:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @nickname4th/pura-cli connect device --name "Zhang San Pixel 8" --owner "Zhang San" --note "login branch"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Designers can now pick the published machine on the Hub homepage, open the live screen, and click on it with a mouse.
|
|
43
|
+
|
|
44
|
+
## Project Site
|
|
45
|
+
|
|
46
|
+
The GitHub Pages site lives in `site/` and is deployed by `.github/workflows/pages.yml`.
|
|
47
|
+
|
|
48
|
+
After publishing the repository, enable GitHub Pages with GitHub Actions as the source. The public URL will be:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
https://liutianjie.github.io/pura/
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- Node.js 20+ for developer Agents
|
|
57
|
+
- Android platform-tools: `adb`
|
|
58
|
+
- Android USB debugging enabled and authorized on each developer machine
|
|
59
|
+
- Docker and Docker Compose for Hub deployment
|
|
60
|
+
- Hub can reach every Agent over the LAN
|
|
61
|
+
- A modern browser
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
Developers can use pura without installing it permanently:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx @nickname4th/pura-cli --help
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or install globally:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g @nickname4th/pura-cli
|
|
75
|
+
pura-cli --help
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For repository development:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install
|
|
82
|
+
npm run build
|
|
83
|
+
npm link
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Hub Deployment
|
|
87
|
+
|
|
88
|
+
Recommended Docker Compose deployment:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
docker compose up -d
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The included compose file builds the local image by default. To use a published GHCR image:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
PURA_IMAGE=ghcr.io/liutianjie/pura:main docker compose up -d
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Equivalent Node.js deployment:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pura-cli hub --host 0.0.0.0 --port 8787
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Developer Agent
|
|
107
|
+
|
|
108
|
+
Each developer connects their local Agent to the Hub:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pura-cli connect 192.168.100.128:8787 --name "Zhang San"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The Agent listens on `8788` by default and continuously reports local ADB devices to the Hub.
|
|
115
|
+
|
|
116
|
+
If the Hub cannot reach the auto-detected Agent URL, specify it:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pura-cli connect 192.168.100.128:8787 --name "Zhang San" --public-url http://192.168.100.45:8788
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The Agent heartbeat automatically recovers after Wi-Fi or Hub restarts as long as the Agent process is still running. On macOS, install the saved Agent connection as a LaunchAgent so it starts at login and restarts if the terminal is closed:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pura-cli connect 192.168.100.128:8787 --name "Zhang San" --background
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Check or remove the background service:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pura-cli auto-connect --status
|
|
132
|
+
pura-cli auto-connect --uninstall
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Publish Device
|
|
136
|
+
|
|
137
|
+
Connect a phone over USB and confirm it is authorized:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
adb devices -l
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then publish it:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pura-cli connect device --name "Zhang San Pixel 8" --owner "Zhang San" --note "login branch"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If multiple Android devices are connected:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pura-cli connect device --serial RFCY10DHQ3P --name "Samsung S25" --owner "Li Si"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Runtime Model
|
|
156
|
+
|
|
157
|
+
- Hub maintains online Agents and devices, serves the web UI, and proxies video WebSocket/tap requests.
|
|
158
|
+
- Agent runs on each developer machine and owns ADB, screen capture, tap execution, and device metadata.
|
|
159
|
+
- CLI commands:
|
|
160
|
+
- `pura-cli hub`
|
|
161
|
+
- `pura-cli connect <hub>`
|
|
162
|
+
- `pura-cli auto-connect`
|
|
163
|
+
- `pura-cli connect device`
|
|
164
|
+
- `pura-cli devices`
|
|
165
|
+
|
|
166
|
+
## API
|
|
167
|
+
|
|
168
|
+
Hub:
|
|
169
|
+
|
|
170
|
+
- `POST /api/agents/heartbeat`
|
|
171
|
+
- `GET /api/devices`
|
|
172
|
+
- `POST /api/devices/:deviceId/session`
|
|
173
|
+
- `POST /api/devices/:deviceId/tap`
|
|
174
|
+
- `PUT /api/devices/:deviceId/publication`
|
|
175
|
+
- `DELETE /api/devices/:deviceId/publication`
|
|
176
|
+
- `DELETE /api/sessions/:id`
|
|
177
|
+
- `WS /ws/sessions/:id/video`
|
|
178
|
+
|
|
179
|
+
Agent:
|
|
180
|
+
|
|
181
|
+
- `GET /api/devices`
|
|
182
|
+
- `POST /api/devices/:serial/session`
|
|
183
|
+
- `POST /api/devices/:serial/tap`
|
|
184
|
+
- `PUT /api/devices/:serial/publication`
|
|
185
|
+
- `DELETE /api/devices/:serial/publication`
|
|
186
|
+
- `DELETE /api/sessions/:id`
|
|
187
|
+
- `WS /ws/sessions/:id/video`
|
|
188
|
+
|
|
189
|
+
## Environment
|
|
190
|
+
|
|
191
|
+
- `ROLE=hub|agent|standalone`
|
|
192
|
+
- `HOST=0.0.0.0`
|
|
193
|
+
- `PORT=8787`
|
|
194
|
+
- `HUB_URL=http://<hub-ip>:8787`
|
|
195
|
+
- `AGENT_ID`
|
|
196
|
+
- `AGENT_NAME`
|
|
197
|
+
- `PUBLIC_URL=http://<agent-ip>:8788`
|
|
198
|
+
- `ADB_PATH=adb`
|
|
199
|
+
- `STREAM_SIZE` optional; unset uses native device resolution
|
|
200
|
+
- `STREAM_BITRATE=8000000`
|
|
201
|
+
- `STREAM_TIME_LIMIT_SECONDS=180`
|
|
202
|
+
- `INCLUDE_TCP_DEVICES=true`
|
|
203
|
+
- `DATA_DIR=data-agent`
|
|
204
|
+
|
|
205
|
+
## Publishing
|
|
206
|
+
|
|
207
|
+
The npm package is `@nickname4th/pura-cli` and installs the `pura-cli` binary.
|
|
208
|
+
|
|
209
|
+
Release flow:
|
|
210
|
+
|
|
211
|
+
1. Update `version` in `package.json`.
|
|
212
|
+
2. Run `npm run check`, `npm run build`, and `npm pack --dry-run`.
|
|
213
|
+
3. Push a tag like `v0.1.0`.
|
|
214
|
+
4. GitHub Actions publishes `pura-cli` to npm and `ghcr.io/liutianjie/pura` to GHCR.
|
|
215
|
+
|
|
216
|
+
The release workflow requires an `NPM_TOKEN` repository secret.
|
|
217
|
+
|
|
218
|
+
## Notes
|
|
219
|
+
|
|
220
|
+
- The current video path uses Android `screenrecord` H.264 output. No Android app or root is required.
|
|
221
|
+
- Mouse control currently supports tap only.
|
|
222
|
+
- Do not expose Hub or Agent ports directly to the public internet.
|
|
223
|
+
- Agent Docker is intentionally not the default because local USB/ADB access is much smoother with native `pura-cli`.
|
|
224
|
+
- Some Android builds enforce `screenrecord` time limits; the Agent restarts the stream automatically when it exits.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{font-family:Avenir Next,DIN Alternate,Helvetica Neue,sans-serif;color:#e8ece7;background:#101211;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;--panel: #171a18;--panel-2: #20231f;--line: #343b34;--muted: #98a49a;--text: #e8ece7;--green: #9dff74;--amber: #ffbf47;--red: #ff6b62;--steel: #7bb4d6;--button: #d6ff59;--button-text: #15190f;--ease-out: cubic-bezier(.2, 0, 0, 1);--ease-emphasized: cubic-bezier(.05, .7, .1, 1)}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh;background:linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px) 0 0 / 28px 28px,linear-gradient(0deg,rgba(255,255,255,.02) 1px,transparent 1px) 0 0 / 28px 28px,#101211}button{font:inherit}.shell{display:grid;grid-template-columns:320px minmax(0,1fr);min-height:100vh}.rail{display:flex;flex-direction:column;gap:18px;min-height:100vh;padding:22px;background:#121512f0;border-right:1px solid var(--line);animation:panelIn .52s var(--ease-emphasized) both}.brand{display:flex;align-items:center;gap:12px;min-width:0;animation:riseIn .48s var(--ease-emphasized) both}.brandMark{display:grid;place-items:center;width:42px;height:42px;color:var(--button-text);background:var(--button);border-radius:7px}.brand h1,.brand p,.topbar h2,.topbar p{margin:0}.brand h1{font-size:19px;line-height:1.1;letter-spacing:0}.brand p{margin-top:4px;color:var(--muted);font-size:12px;text-transform:uppercase}.refreshButton,.languageButton,.deviceItem,.primary,.secondary{display:inline-flex;align-items:center;justify-content:center;gap:9px;min-height:40px;border:1px solid var(--line);border-radius:7px;cursor:pointer;transition:transform .16s var(--ease-out),border-color .18s var(--ease-out),background-color .18s var(--ease-out),box-shadow .18s var(--ease-out),opacity .18s var(--ease-out)}.railControls{display:grid;grid-template-columns:1fr auto;gap:8px;animation:riseIn .52s var(--ease-emphasized) 80ms both}.refreshButton,.languageButton{color:var(--text);background:#1d211d}.languageButton{min-width:74px;padding:0 12px}.refreshButton:hover,.languageButton:hover,.secondary:hover:not(:disabled){border-color:#617057;background:#242a24;transform:translateY(-1px)}.refreshButton:hover svg{animation:spinOnce .62s var(--ease-emphasized)}.primary:hover:not(:disabled){box-shadow:0 10px 28px #d6ff5933;transform:translateY(-1px)}.refreshButton:active,.languageButton:active,.primary:active:not(:disabled),.secondary:active:not(:disabled),.deviceItem:active{transform:translateY(0) scale(.985)}.deviceList{display:flex;flex-direction:column;gap:9px;min-height:0;overflow:auto;animation:riseIn .52s var(--ease-emphasized) .13s both}.deviceList.compact{max-height:260px}.sectionLabel{display:inline-flex;align-items:center;gap:7px;color:#b5c0b4;font-size:12px;font-weight:700;text-transform:uppercase}.deviceItem{width:100%;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;justify-content:stretch;padding:12px;color:var(--text);background:#171b17;text-align:left;animation:listItemIn .36s var(--ease-emphasized) both}.deviceItem:hover,.deviceItem.selected{border-color:#5f7258;background:#202720;transform:translate(3px)}.deviceItem.selected{box-shadow:inset 3px 0 0 var(--button),0 10px 24px #0000002e}.deviceItem.published{border-color:#9dff7442}.deviceSelectButton{display:flex;align-items:center;gap:9px;min-width:0;padding:0;color:inherit;background:transparent;border:0;cursor:pointer;text-align:left}.deviceManageButton,.iconButton{display:inline-grid;place-items:center;width:32px;height:32px;color:#cbd7c7;background:#ffffff09;border:1px solid rgba(255,255,255,.08);border-radius:7px;cursor:pointer;transition:color .16s var(--ease-out),background-color .16s var(--ease-out),border-color .16s var(--ease-out),transform .16s var(--ease-out)}.deviceManageButton{opacity:0;transform:translate(4px)}.deviceItem:hover .deviceManageButton,.deviceItem.selected .deviceManageButton,.deviceManageButton:focus-visible{opacity:1;transform:translate(0)}.deviceManageButton:hover,.iconButton:hover{color:var(--button);background:#d6ff591a;border-color:#d6ff5957}.deviceCopy{display:grid;min-width:0}.deviceCopy strong,.deviceCopy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deviceCopy strong{font-size:14px}.deviceCopy small{margin-top:3px;color:var(--muted);font-family:SFMono-Regular,Consolas,monospace;font-size:11px}.dot{width:9px;height:9px;margin-left:auto;border-radius:50%;background:var(--red);transition:transform .18s var(--ease-out),box-shadow .18s var(--ease-out)}.dot.device{background:var(--green);box-shadow:0 0 #9dff7400;animation:onlinePulse 2.2s ease-in-out infinite}.dot.unauthorized{background:var(--amber)}.emptyState{display:grid;place-items:center;gap:10px;min-width:min(260px,100%);min-height:140px;padding:24px 28px;color:var(--muted);border:1px dashed var(--line);border-radius:7px;animation:fadeIn .42s var(--ease-out) both}.emptyState.small{min-width:0;min-height:76px;padding:18px;font-size:12px}.workspace{display:flex;flex-direction:column;min-width:0;padding:22px;animation:workspaceIn .56s var(--ease-emphasized) 70ms both}.topbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:14px;animation:riseIn .52s var(--ease-emphasized) .12s both}.eyebrow{color:var(--steel);font-size:12px;text-transform:uppercase}.topbar h2{margin-top:5px;font-size:clamp(22px,3vw,34px);line-height:1.05;letter-spacing:0}.actions{display:flex;gap:10px}.viewTabs{display:inline-flex;gap:4px;padding:4px;background:#111411eb;border:1px solid var(--line);border-radius:8px;animation:riseIn .52s var(--ease-emphasized) .15s both}.viewTabs button{min-height:32px;padding:0 12px;color:var(--muted);background:transparent;border:0;border-radius:6px;cursor:pointer;transition:color .16s var(--ease-out),background-color .16s var(--ease-out),transform .16s var(--ease-out)}.viewTabs button:hover{color:var(--text);transform:translateY(-1px)}.viewTabs button.active{color:var(--button-text);background:var(--button)}.primary,.secondary{padding:0 15px;white-space:nowrap}.primary{color:var(--button-text);background:var(--button);border-color:var(--button);font-weight:700}.secondary{color:var(--text);background:#1d211d}a.secondary,a.iconButton{text-decoration:none}.secondary.danger{color:#ffd8d4;border-color:#ff6b625c}.secondary.danger:hover:not(:disabled){background:#ff6b621f;border-color:#ff6b628c}.primary:disabled,.secondary:disabled{cursor:not-allowed;opacity:.45}.metaStrip{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.infoPill{display:inline-flex;align-items:center;gap:7px;min-height:30px;padding:0 10px;color:#c6cec4;background:#20231fdb;border:1px solid var(--line);border-radius:999px;font-size:12px;animation:chipIn .36s var(--ease-emphasized) both;transition:border-color .18s var(--ease-out),transform .18s var(--ease-out)}.infoPill:hover{border-color:#586355;transform:translateY(-1px)}.infoPill.tone-link{color:#cfe8f8;background:#7bb4d621;border-color:#7bb4d657}.infoPill.tone-screen{color:#f8e4bd;background:#ffbf471f;border-color:#ffbf4752}.infoPill.tone-ready{color:#dbffd0;background:#9dff741f;border-color:#9dff7457}.infoPill.tone-warning{color:#ffe3bd;background:#ffbf4721;border-color:#ffbf476b}.infoPill.tone-gesture{color:#ddd6ff;background:#9b7eff21;border-color:#9b7eff57}.infoPill.tone-agent{color:#cffff2;background:#5bd6b81f;border-color:#5bd6b857}.infoPill.tone-owner{color:#ffd5e7;background:#ff70a81f;border-color:#ff70a857}.cursorToggle{display:inline-flex;align-items:center;gap:7px;min-height:30px;padding:0 10px;color:#aeb9ad;background:#20231fbd;border:1px solid var(--line);border-radius:999px;cursor:pointer;font-size:12px;transition:color .16s var(--ease-out),background-color .16s var(--ease-out),border-color .16s var(--ease-out),transform .16s var(--ease-out)}.cursorToggle:hover{color:var(--text);border-color:#586355;transform:translateY(-1px)}.cursorToggle.active{color:#eaffc5;background:#d6ff591f;border-color:#d6ff5957}.cursorNameControl{display:inline-flex;align-items:center;gap:7px;min-height:30px;padding:0 9px;color:#c6cec4;background:#121512c7;border:1px solid var(--line);border-radius:999px;transition:border-color .16s var(--ease-out),box-shadow .16s var(--ease-out),background-color .16s var(--ease-out)}.cursorNameControl:focus-within{background:#181d17e6;border-color:#d6ff596b;box-shadow:0 0 0 2px #d6ff591a}.cursorNameControl input{width:96px;padding:0;color:var(--text);background:transparent;border:0;outline:none;font-size:12px}.annotationTools{display:inline-flex;gap:4px;padding:4px;background:#111411bd;border:1px solid var(--line);border-radius:999px}.annotationTools button{display:inline-flex;align-items:center;gap:6px;min-height:24px;padding:0 8px;color:#aeb9ad;background:transparent;border:0;border-radius:999px;cursor:pointer;font-size:12px;transition:color .16s var(--ease-out),background-color .16s var(--ease-out),transform .16s var(--ease-out)}.annotationTools button:hover{color:var(--text);background:#ffffff0b;transform:translateY(-1px)}.annotationTools button.active{color:var(--button-text);background:var(--button)}.errorBanner{margin-bottom:14px;padding:12px 14px;color:#ffe5e2;background:#ff6b6229;border:1px solid rgba(255,107,98,.45);border-radius:7px;animation:errorIn .34s var(--ease-out) both}.screenshotResult{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:-4px 0 14px;padding:8px;width:fit-content;color:#dfe9dc;background:#121512d1;border:1px solid var(--line);border-radius:8px}.screenshotResult span,.screenshotResult a,.screenshotResult button{display:inline-flex;align-items:center;gap:7px;min-height:30px;font-size:12px}.phoneGrid{display:grid;gap:16px;min-height:0;animation:riseIn .56s var(--ease-emphasized) .18s both}.phoneGrid.empty{place-items:center;min-height:420px}.gridSummary{display:inline-flex;align-items:baseline;gap:8px;width:fit-content;color:var(--muted);font-size:13px}.gridSummary strong{color:var(--text);font-size:28px;line-height:1}.phoneCards{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}.phoneCard{display:grid;gap:12px;padding:14px;color:var(--text);background:#171b17e0;border:1px solid var(--line);border-radius:8px;text-align:left;animation:listItemIn .42s var(--ease-emphasized) both;transition:transform .18s var(--ease-out),border-color .18s var(--ease-out),background-color .18s var(--ease-out),box-shadow .18s var(--ease-out)}.phoneOpenArea{display:grid;gap:12px;width:100%;padding:0;color:inherit;background:transparent;border:0;text-align:left;cursor:pointer}.phoneCard:hover,.phoneCard.selected{border-color:#d6ff5994;background:#1f261ef0;box-shadow:0 18px 40px #0000003d;transform:translateY(-3px)}.phonePreview{position:relative;display:grid;place-items:center;min-height:230px;padding:12px;background:radial-gradient(circle at 50% 18%,rgba(214,255,89,.1),transparent 34%),#0b0d0b;border:1px solid rgba(255,255,255,.07);border-radius:8px;overflow:hidden}.phoneChrome{position:relative;display:grid;width:min(58%,150px);min-width:92px;max-height:250px;padding:10px 7px 9px;background:linear-gradient(180deg,#222822,#0b0d0b);border:1px solid #3d463a;border-radius:18px;box-shadow:0 18px 45px #00000061;transition:transform .24s var(--ease-out),box-shadow .24s var(--ease-out)}.phoneCard:hover .phoneChrome{box-shadow:0 22px 54px #0000007a;transform:translateY(-2px) scale(1.015)}.speaker{position:absolute;top:5px;left:50%;width:30%;height:3px;background:#4b5447;border-radius:99px;transform:translate(-50%)}.previewScreen{position:relative;display:grid;place-items:center;align-content:center;gap:8px;min-height:0;color:#b9c6b5;background:linear-gradient(160deg,rgba(123,180,214,.16),transparent 42%),linear-gradient(20deg,rgba(214,255,89,.1),transparent 48%),#111511;border:1px solid #30382e;border-radius:12px;overflow:hidden}.previewImage{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;background:#060706;opacity:0;transition:opacity .22s var(--ease-out)}.previewImage.loaded{opacity:1}.previewLoading{position:absolute;inset:0;background:linear-gradient(115deg,transparent 0%,rgba(255,255,255,.07) 38%,transparent 68%) -140% 0 / 240% 100%,radial-gradient(circle at 60% 18%,rgba(214,255,89,.1),transparent 34%),#070907;animation:previewSweep 1.4s var(--ease-out) infinite}.previewScreen small{position:relative;z-index:1;color:var(--muted);font-family:SFMono-Regular,Consolas,monospace;font-size:11px}.previewScreen>svg{position:relative;z-index:1}.previewSignal{position:absolute;right:9px;top:9px;z-index:2;width:8px;height:8px;background:var(--red);border-radius:50%}.previewSignal.device{background:var(--green);animation:onlinePulse 2.2s ease-in-out infinite}.previewSignal.unauthorized{background:var(--amber)}.liveTag{position:absolute;right:10px;top:10px;min-height:24px;padding:4px 8px;color:var(--button-text);background:var(--button);border-radius:999px;font-size:11px;font-weight:800}.phoneMeta{display:grid;gap:4px;min-width:0}.phoneMeta strong,.phoneMeta span,.phoneMeta small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.phoneMeta strong{font-size:15px}.phoneMeta span,.phoneMeta small{color:var(--muted);font-size:12px}.phoneFooter{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px 12px;color:#cbd4c8;font-size:12px}.phoneFooter span{display:inline-flex;align-items:center;gap:7px;flex:1 1 72px;min-width:0}.phoneFooter .dot{flex:0 0 9px;margin-left:0}.phoneActions{display:inline-flex;flex:0 0 auto;flex-wrap:nowrap;gap:6px;margin-left:auto}.phoneAction{display:inline-flex;align-items:center;justify-content:center;min-width:max-content;min-height:32px;padding:0 11px;color:#dbe5d8;background:#1d211d;border:1px solid var(--line);border-radius:6px;cursor:pointer;font-size:12px;line-height:1;white-space:nowrap;transition:background-color .16s var(--ease-out),border-color .16s var(--ease-out),transform .16s var(--ease-out),opacity .16s var(--ease-out)}.phoneAction:hover:not(:disabled){background:#242a24;border-color:#617057;transform:translateY(-1px)}.phoneAction.danger{color:#ffd8d4;border-color:#ff6b6257}.phoneAction:disabled{cursor:not-allowed;opacity:.42}.managerScrim{position:fixed;inset:0;z-index:50;display:grid;place-items:center end;padding:22px;background:#04060480;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);animation:fadeIn .18s var(--ease-out) both}.managerPanel{display:grid;gap:16px;width:min(390px,calc(100vw - 44px));padding:16px;color:var(--text);background:#161916f5;border:1px solid rgba(214,255,89,.22);border-radius:8px;box-shadow:0 24px 80px #00000075;animation:managerIn .26s var(--ease-emphasized) both}.managerHeader{display:flex;align-items:center;justify-content:space-between;gap:12px}.managerHeader div{display:grid;gap:4px;min-width:0}.managerHeader span{color:var(--steel);font-size:12px;font-weight:800;text-transform:uppercase}.managerHeader strong{overflow:hidden;font-size:20px;line-height:1.1;text-overflow:ellipsis;white-space:nowrap}.managerForm{display:grid;gap:12px}.managerForm label{display:grid;gap:6px}.managerForm label span{display:inline-flex;align-items:center;gap:6px;color:#b8c4b6;font-size:12px;font-weight:800}.managerForm input{width:100%;min-height:40px;padding:0 11px;color:var(--text);background:#111411;border:1px solid #394037;border-radius:7px;outline:none;transition:border-color .16s var(--ease-out),box-shadow .16s var(--ease-out),background-color .16s var(--ease-out)}.managerForm input:focus{border-color:var(--button);box-shadow:0 0 0 2px #d6ff5924;background:#151a14}.managerActions{display:flex;justify-content:flex-end;gap:8px;padding-top:4px}.managerScreenshots{display:grid;gap:10px}.managerSectionHeader{display:flex;align-items:center;justify-content:space-between;gap:10px}.managerSectionHeader span{color:#b8c4b6;font-size:12px;font-weight:800}.managerSectionHeader .secondary{min-height:32px;padding:0 10px}.screenshotEmpty{padding:14px;color:var(--muted);background:#ffffff06;border:1px dashed var(--line);border-radius:7px;font-size:12px;text-align:center}.screenshotList{display:grid;gap:8px;max-height:260px;overflow:auto}.screenshotItem{display:grid;grid-template-columns:42px minmax(0,1fr) auto auto;gap:9px;align-items:center;padding:8px;background:#ffffff06;border:1px solid var(--line);border-radius:7px}.screenshotItem img{width:42px;height:58px;object-fit:cover;background:#060706;border:1px solid rgba(255,255,255,.08);border-radius:5px}.screenshotItem div{display:grid;gap:3px;min-width:0}.screenshotItem strong,.screenshotItem span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.screenshotItem strong{color:var(--text);font-size:12px}.screenshotItem span{color:var(--muted);font-size:11px}.focusSurface{display:grid;grid-template-columns:minmax(220px,280px) minmax(0,1fr);gap:14px;align-items:stretch;min-height:0}.controlPanel{display:grid;gap:10px;align-content:start;margin-bottom:0;padding:12px;background:#171a18d1;border:1px solid var(--line);border-radius:8px;animation:riseIn .52s var(--ease-emphasized) .21s both}.controlGroup{display:grid;grid-template-columns:1fr 1fr;gap:8px}.controlLabel{grid-column:1 / -1;color:#b5c0b4;font-size:12px;font-weight:800;text-transform:uppercase}.controlButton{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:34px;padding:0 10px;color:var(--text);background:#1d211d;border:1px solid var(--line);border-radius:7px;cursor:pointer;font-size:12px;white-space:nowrap;transition:transform .15s var(--ease-out),border-color .16s var(--ease-out),background-color .16s var(--ease-out),opacity .16s var(--ease-out)}.controlButton:hover:not(:disabled){border-color:#617057;background:#242a24;transform:translateY(-1px)}.controlButton:active:not(:disabled){transform:scale(.98)}.controlButton:disabled{cursor:not-allowed;opacity:.42}.textControl{display:grid;grid-template-columns:1fr;gap:8px;align-items:center;padding-top:2px}.textControl>svg{display:none}.textControl input{width:100%;min-height:36px;padding:0 10px;color:var(--text);background:#111411;border:1px solid #394037;border-radius:7px;outline:none;transition:border-color .16s var(--ease-out),box-shadow .16s var(--ease-out)}.textControl input:focus{border-color:var(--button);box-shadow:0 0 0 2px #d6ff5924}.publishPanel{display:grid;grid-template-columns:minmax(160px,1fr) minmax(140px,.7fr) minmax(220px,1.2fr) auto;gap:10px;align-items:end;margin-bottom:14px;padding:12px;background:#171a18d1;border:1px solid var(--line);border-radius:8px;animation:riseIn .52s var(--ease-emphasized) .18s both}.publishPanel label{display:grid;gap:6px;min-width:0}.publishPanel label span{display:inline-flex;align-items:center;gap:6px;color:#b5c0b4;font-size:12px;font-weight:700}.publishPanel input{width:100%;min-height:38px;padding:0 10px;color:var(--text);background:#111411;border:1px solid #394037;border-radius:7px;outline:none;transition:border-color .16s var(--ease-out),box-shadow .16s var(--ease-out),background-color .16s var(--ease-out)}.publishPanel input:focus{border-color:#d6ff59;box-shadow:0 0 0 2px #d6ff5924;background:#151a14}.publishActions{display:flex;gap:8px}.stage{min-height:0;display:grid;place-items:center;animation:riseIn .56s var(--ease-emphasized) .24s both}.screenFrame{position:relative;width:min(100%,1100px);aspect-ratio:16 / 10;min-height:360px;max-height:calc(100vh - 180px);background:#060706;border:1px solid #2f352f;border-radius:8px;overflow:hidden;box-shadow:0 24px 90px #0000006b;transition:border-color .26s var(--ease-out),box-shadow .26s var(--ease-out),transform .26s var(--ease-out)}.screenFrame:hover{border-color:#46513f;box-shadow:0 28px 100px #0000007a;transform:translateY(-1px)}.screen{display:block;width:100%;height:100%;object-fit:contain;background:#050605;cursor:crosshair;touch-action:none;-webkit-user-select:none;user-select:none}.statusBadge{position:absolute;right:13px;top:13px;display:inline-flex;align-items:center;gap:8px;min-height:30px;padding:0 10px;color:#dce5da;background:#0a0c0ac2;border:1px solid rgba(255,255,255,.12);border-radius:999px;font-size:12px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);animation:badgeIn .26s var(--ease-emphasized) both}.statusBadge span{width:8px;height:8px;border-radius:50%;background:var(--muted)}.statusBadge.live span{background:var(--green);box-shadow:0 0 14px var(--green)}.statusBadge.connecting span{background:var(--amber);animation:connectingPulse .88s ease-in-out infinite}.statusBadge.error span{background:var(--red)}.standby{position:absolute;inset:0;display:grid;place-items:center;align-content:center;gap:12px;color:#aab4a9;background:linear-gradient(135deg,rgba(214,255,89,.06),transparent 40%),#050605eb;animation:fadeIn .42s var(--ease-out) both}.tapPulse{position:absolute;width:26px;height:26px;margin:-13px 0 0 -13px;pointer-events:none;border:2px solid var(--button);border-radius:50%;animation:pulse .52s var(--ease-emphasized) forwards}.annotationLayer{position:absolute;inset:0;z-index:3;width:100%;height:100%;pointer-events:none}.annotationRect,.annotationDraw{fill:none;stroke:var(--annotation-color);stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke;filter:drop-shadow(0 1px 2px rgba(0,0,0,.34))}.annotationRect{fill:color-mix(in srgb,var(--annotation-color) 14%,transparent);stroke-width:2.5px;stroke-dasharray:7 5}.annotationDraw{stroke-width:4px}.remoteCursor{position:absolute;z-index:4;display:inline-flex;align-items:center;gap:5px;color:var(--cursor-color);pointer-events:none;filter:drop-shadow(0 1px 3px rgba(0,0,0,.38));transform:translate(2px,2px);animation:cursorPop .18s var(--ease-emphasized) both}.remoteCursor svg{fill:#00000057;stroke-width:2.5}.remoteCursor small{max-width:120px;overflow:hidden;padding:4px 8px;color:#0d110d;background:var(--cursor-color);border-radius:999px;font-size:13px;font-weight:800;line-height:1;text-overflow:ellipsis;white-space:nowrap}.deviceItem:nth-of-type(2){animation-delay:45ms}.deviceItem:nth-of-type(3){animation-delay:90ms}.deviceItem:nth-of-type(4){animation-delay:135ms}.deviceItem:nth-of-type(5){animation-delay:.18s}@keyframes panelIn{0%{opacity:0;transform:translate(-18px)}to{opacity:1;transform:translate(0)}}@keyframes workspaceIn{0%{opacity:0;transform:translateY(18px) scale(.992)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes riseIn{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes listItemIn{0%{opacity:0;transform:translate(-10px)}to{opacity:1;transform:translate(0)}}@keyframes chipIn{0%{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes badgeIn{0%{opacity:0;transform:translateY(-6px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes managerIn{0%{opacity:0;transform:translate(14px) scale(.985)}to{opacity:1;transform:translate(0) scale(1)}}@keyframes cursorPop{0%{opacity:0;transform:translate(2px,2px) scale(.92)}to{opacity:1;transform:translate(2px,2px) scale(1)}}@keyframes previewSweep{to{background-position:140% 0,0 0,0 0}}@keyframes errorIn{0%{opacity:0;transform:translate(-8px)}65%{transform:translate(2px)}to{opacity:1;transform:translate(0)}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes spinOnce{to{transform:rotate(360deg)}}@keyframes onlinePulse{0%,to{box-shadow:0 0 #9dff7400}45%{box-shadow:0 0 16px #9dff7485}}@keyframes connectingPulse{0%,to{transform:scale(.82);opacity:.75}50%{transform:scale(1.18);opacity:1}}@keyframes pulse{0%{opacity:.95;transform:scale(.45)}to{opacity:0;transform:scale(1.8)}}@media(max-width:820px){.shell{grid-template-columns:1fr}.rail{min-height:auto;border-right:0;border-bottom:1px solid var(--line);animation-name:riseIn}.workspace{padding:16px}.topbar{align-items:stretch;flex-direction:column}.actions{width:100%}.actions button{flex:1}.screenFrame{min-height:260px;max-height:none}.focusSurface,.publishPanel{grid-template-columns:1fr}.controlGroup{grid-template-columns:repeat(2,minmax(0,1fr))}.publishActions{width:100%}.publishActions button{flex:1}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:1ms!important}}
|