ownremote 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,211 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content">
6
+ <meta name="theme-color" content="#0d0f14">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-capable" content="yes">
9
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
10
+ <title>OwnRemote</title>
11
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23c6f45b' d='M5 3l14 7-6 2-2 6z'/%3E%3C/svg%3E">
12
+ <link rel="stylesheet" href="style.css">
13
+ </head>
14
+ <body>
15
+ <!-- ============ Layar pairing ============ -->
16
+ <main id="pairScreen" class="screen pair hidden">
17
+ <div class="pair-card">
18
+ <div class="brand">
19
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 3l14 7-6 2-2 6z"/></svg>
20
+ <span>OwnRemote</span>
21
+ </div>
22
+ <h1>Connect to your PC</h1>
23
+ <p class="muted">Enter the 6-digit code shown on your PC.</p>
24
+
25
+ <form id="pairForm" autocomplete="off">
26
+ <label class="code-field" for="codeInput">
27
+ <input id="codeInput" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" aria-label="Pairing code">
28
+ <span class="code-slots" aria-hidden="true">
29
+ <i></i><i></i><i></i><i></i><i></i><i></i>
30
+ </span>
31
+ </label>
32
+ <input id="nameInput" class="text-input" maxlength="40" placeholder="Device name (optional)">
33
+ <button id="pairBtn" class="primary" type="submit">Connect</button>
34
+ <p id="pairError" class="error" role="alert"></p>
35
+ </form>
36
+ </div>
37
+ </main>
38
+
39
+ <!-- ============ Layar trackpad ============ -->
40
+ <main id="padScreen" class="screen pad-screen hidden">
41
+ <header class="topbar">
42
+ <div class="conn">
43
+ <i id="connDot" class="dot"></i>
44
+ <div>
45
+ <strong id="pcName">PC</strong>
46
+ <small id="connText">Connecting…</small>
47
+ </div>
48
+ </div>
49
+ <div class="top-actions">
50
+ <button id="screenBtn" class="icon-btn" aria-label="View the PC screen" aria-pressed="false">
51
+ <svg viewBox="0 0 24 24"><path d="M21 3H3a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h7v2H8v2h8v-2h-2v-2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 13H3V5h18z"/></svg>
52
+ </button>
53
+ <button id="kbBtn" class="icon-btn" aria-label="Keyboard" aria-pressed="false">
54
+ <svg viewBox="0 0 24 24"><path d="M20 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zM11 8h2v2h-2zm0 3h2v2h-2zM8 8h2v2H8zm0 3h2v2H8zm-1 2H5v-2h2zm0-3H5V8h2zm9 7H8v-2h8zm0-4h-2v-2h2zm0-3h-2V8h2zm3 3h-2v-2h2zm0-3h-2V8h2z"/></svg>
55
+ </button>
56
+ <button id="settingsBtn" class="icon-btn" aria-label="Settings">
57
+ <svg viewBox="0 0 24 24"><path d="M12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7zm7.4-2.5a7.6 7.6 0 0 0 0-2l2.1-1.6-2-3.4-2.5 1a7.4 7.4 0 0 0-1.7-1L15 3.3h-4l-.4 2.7a7.4 7.4 0 0 0-1.7 1l-2.5-1-2 3.4L6.6 11a7.6 7.6 0 0 0 0 2l-2.1 1.6 2 3.4 2.5-1c.5.4 1.1.7 1.7 1l.4 2.7h4l.4-2.7c.6-.3 1.2-.6 1.7-1l2.5 1 2-3.4z"/></svg>
58
+ </button>
59
+ </div>
60
+ </header>
61
+
62
+ <!-- preview layar PC (hanya aktif saat tombol layar ditekan) -->
63
+ <section id="screenPanel" class="screen-view hidden" aria-label="Layar PC">
64
+ <div id="stage" class="stage">
65
+ <div id="stageInner" class="stage-inner">
66
+ <img id="screenImg" alt="" draggable="false">
67
+ <div id="vCursor" class="vcursor hidden">
68
+ <svg viewBox="0 0 24 24"><path d="M1 1v18.5l5.2-5 3.4 7.7 3.3-1.5-3.4-7.6H17z"/></svg>
69
+ </div>
70
+ </div>
71
+ <div id="screenMsg" class="stage-msg">Loading screen…</div>
72
+ </div>
73
+ <div class="stage-bar">
74
+ <button id="zoomBtn" class="chip" aria-label="Zoom follows the cursor">Zoom 1×</button>
75
+ <button id="qualityBtn" class="chip" aria-label="Quality">Normal</button>
76
+ <button id="expandBtn" class="chip" aria-label="Enlarge preview">⤢</button>
77
+ <span id="screenStats" class="stats"></span>
78
+ </div>
79
+ </section>
80
+
81
+ <section id="touchpad" class="touchpad" aria-label="Trackpad">
82
+ <div class="pad-hint" id="padHint">
83
+ <span><b>1 finger</b> slide = move</span>
84
+ <span><b>tap</b> = left click</span>
85
+ <span><b>2-finger tap</b> = right click</span>
86
+ <span><b>2 fingers</b> slide = scroll</span>
87
+ <span><b>hold</b> then slide = drag</span>
88
+ </div>
89
+ <div id="dragBadge" class="drag-badge hidden">DRAG</div>
90
+ <div id="scrollStrip" class="scroll-strip" aria-label="Scroll"></div>
91
+ </section>
92
+
93
+ <!-- panel keyboard: ketikan langsung terkirim ke PC -->
94
+ <section id="kbPanel" class="kb hidden" aria-label="Keyboard">
95
+ <div class="kb-row">
96
+ <button class="kk mod" data-mod="1">Ctrl</button>
97
+ <button class="kk mod" data-mod="2">Alt</button>
98
+ <button class="kk mod" data-mod="4">Shift</button>
99
+ <button class="kk mod" data-mod="8">Win</button>
100
+ <button class="kk" data-key="esc">Esc</button>
101
+ <button class="kk" data-key="tab">Tab</button>
102
+ </div>
103
+ <div class="kb-row">
104
+ <button class="kk" data-key="left" data-repeat aria-label="Left">←</button>
105
+ <button class="kk" data-key="up" data-repeat aria-label="Up">↑</button>
106
+ <button class="kk" data-key="down" data-repeat aria-label="Down">↓</button>
107
+ <button class="kk" data-key="right" data-repeat aria-label="Right">→</button>
108
+ <button class="kk" data-key="home">Home</button>
109
+ <button class="kk" data-key="end">End</button>
110
+ <button class="kk" data-key="delete" data-repeat>Del</button>
111
+ </div>
112
+ <div class="kb-chips">
113
+ <button class="chip" data-char="c" data-mods="1">Ctrl+C</button>
114
+ <button class="chip" data-char="v" data-mods="1">Ctrl+V</button>
115
+ <button class="chip" data-char="x" data-mods="1">Ctrl+X</button>
116
+ <button class="chip" data-char="z" data-mods="1">Ctrl+Z</button>
117
+ <button class="chip" data-char="a" data-mods="1">Ctrl+A</button>
118
+ <button class="chip" data-char="s" data-mods="1">Ctrl+S</button>
119
+ <button class="chip" data-key="tab" data-mods="2">Alt+Tab</button>
120
+ <button class="chip" data-char="d" data-mods="8">Win+D</button>
121
+ <button class="chip" data-key="f5">F5</button>
122
+ <button class="chip" data-key="f11">F11</button>
123
+ <button class="chip" data-key="prev" aria-label="Previous track">⏮</button>
124
+ <button class="chip" data-key="playpause" aria-label="Play/Pause">⏯</button>
125
+ <button class="chip" data-key="next" aria-label="Next track">⏭</button>
126
+ <button class="chip" data-key="volumedown" aria-label="Volume down">🔉</button>
127
+ <button class="chip" data-key="volumeup" aria-label="Volume up">🔊</button>
128
+ <button class="chip" data-key="mute" aria-label="Mute">🔇</button>
129
+ </div>
130
+ <div class="kb-input">
131
+ <textarea id="kbInput" rows="1" enterkeyhint="enter" autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" aria-label="Type on the PC"></textarea>
132
+ <span class="kb-placeholder" id="kbPlaceholder">Type here, it shows up on your PC</span>
133
+ <button class="kk icon" data-key="backspace" data-repeat aria-label="Backspace">⌫</button>
134
+ <button class="kk icon accent" data-key="enter" aria-label="Enter">⏎</button>
135
+ </div>
136
+ </section>
137
+
138
+ <footer class="buttons">
139
+ <button class="mbtn" data-btn="l">Left</button>
140
+ <button class="mbtn mid" data-btn="m" aria-label="Middle click"><i></i></button>
141
+ <button class="mbtn" data-btn="r">Right</button>
142
+ </footer>
143
+
144
+ <!-- overlay status koneksi -->
145
+ <div id="overlay" class="overlay hidden">
146
+ <div class="overlay-card">
147
+ <p id="overlayText">Disconnected</p>
148
+ <button id="reconnectBtn" class="primary">Reconnect</button>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- panel pengaturan -->
153
+ <div id="sheet" class="sheet hidden" aria-hidden="true">
154
+ <div class="sheet-backdrop" data-close></div>
155
+ <div class="sheet-panel" role="dialog" aria-label="Settings">
156
+ <div class="sheet-grip"></div>
157
+ <h2>Settings</h2>
158
+ <label class="setting">
159
+ <span>Cursor speed <output id="sensOut"></output></span>
160
+ <input id="sens" type="range" min="0.5" max="4" step="0.1">
161
+ </label>
162
+ <label class="setting">
163
+ <span>Acceleration <output id="accelOut"></output></span>
164
+ <input id="accel" type="range" min="0" max="2" step="0.1">
165
+ </label>
166
+ <label class="setting">
167
+ <span>Scroll speed <output id="scrollOut"></output></span>
168
+ <input id="scrollSpeed" type="range" min="1" max="12" step="0.5">
169
+ </label>
170
+ <label class="toggle"><span>Natural scrolling (like a phone)</span><input id="natural" type="checkbox"><i></i></label>
171
+ <label class="toggle"><span>Vibrate on click</span><input id="haptic" type="checkbox"><i></i></label>
172
+
173
+ <h3 class="sheet-section">PC screen preview</h3>
174
+ <div class="preset-row">
175
+ <button class="preset" data-screen-preset="saver">Saver</button>
176
+ <button class="preset" data-screen-preset="normal">Normal</button>
177
+ <button class="preset" data-screen-preset="sharp">Sharp</button>
178
+ </div>
179
+ <label class="setting">
180
+ <span>Frames per second <output id="screenFpsOut"></output></span>
181
+ <input id="screenFps" type="range" min="1" max="15" step="1">
182
+ </label>
183
+ <label class="setting">
184
+ <span>Resolution</span>
185
+ <select id="screenWidth" class="select">
186
+ <option value="640">640 px (saver)</option>
187
+ <option value="960">960 px</option>
188
+ <option value="1280">1280 px</option>
189
+ <option value="1600">1600 px</option>
190
+ <option value="1920">1920 px (native Full HD)</option>
191
+ </select>
192
+ </label>
193
+ <label class="setting">
194
+ <span>Image quality <output id="screenQualityOut"></output></span>
195
+ <input id="screenQuality" type="range" min="20" max="90" step="5">
196
+ </label>
197
+ <p class="muted small" id="screenEstimate"></p>
198
+
199
+ <div class="sheet-actions">
200
+ <button id="fullscreenBtn" class="ghost">Full screen</button>
201
+ <button id="disconnectBtn" class="ghost">Disconnect</button>
202
+ <button id="forgetBtn" class="danger">Forget this PC</button>
203
+ </div>
204
+ <p class="muted small" id="deviceInfo"></p>
205
+ </div>
206
+ </div>
207
+ </main>
208
+
209
+ <script src="app.js"></script>
210
+ </body>
211
+ </html>
@@ -0,0 +1,318 @@
1
+ :root {
2
+ --bg: #0d0f14;
3
+ --surface: #151820;
4
+ --surface-2: #1c2029;
5
+ --line: #262b36;
6
+ --text: #e8ebf2;
7
+ --muted: #8a92a3;
8
+ --accent: #c6f45b;
9
+ --accent-ink: #1a2205;
10
+ --danger: #ff6b6b;
11
+ --ok: #4ade80;
12
+ --warn: #fbbf24;
13
+ --radius: 18px;
14
+ --safe-b: env(safe-area-inset-bottom, 0px);
15
+ --safe-t: env(safe-area-inset-top, 0px);
16
+ }
17
+
18
+ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
19
+
20
+ html, body {
21
+ margin: 0;
22
+ height: 100%;
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
+ overscroll-behavior: none;
27
+ }
28
+
29
+ body { overflow: hidden; }
30
+
31
+ button { font: inherit; color: inherit; cursor: pointer; border: 0; }
32
+
33
+ .hidden { display: none !important; }
34
+ .muted { color: var(--muted); }
35
+ .small { font-size: 12px; }
36
+
37
+ /* --app-h = tinggi area yang terlihat (mengecil saat keyboard HP terbuka), diisi oleh app.js */
38
+ .screen { position: fixed; top: 0; left: 0; right: 0; height: var(--app-h, 100%); }
39
+
40
+ /* ---------------- pairing ---------------- */
41
+ .pair {
42
+ display: grid;
43
+ place-items: center;
44
+ padding: 24px 16px;
45
+ background:
46
+ radial-gradient(600px 400px at 50% -10%, rgba(198, 244, 91, .10), transparent 70%),
47
+ var(--bg);
48
+ overflow-y: auto;
49
+ }
50
+
51
+ .pair-card { width: 100%; max-width: 380px; }
52
+
53
+ .brand {
54
+ display: flex; align-items: center; gap: 8px;
55
+ font-weight: 600; letter-spacing: .02em; color: var(--muted);
56
+ margin-bottom: 36px;
57
+ }
58
+ .brand svg { width: 22px; height: 22px; fill: var(--accent); }
59
+
60
+ .pair h1 { font-size: 28px; margin: 0 0 8px; letter-spacing: -.01em; }
61
+ .pair p { margin: 0 0 28px; line-height: 1.5; }
62
+
63
+ .code-field { position: relative; display: block; margin-bottom: 14px; }
64
+ .code-field input {
65
+ position: absolute; inset: 0; width: 100%; height: 100%;
66
+ opacity: 0; font-size: 16px; /* cegah zoom iOS */
67
+ caret-color: transparent;
68
+ }
69
+ .code-slots { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; pointer-events: none; }
70
+ .code-slots i {
71
+ height: 58px; border-radius: 14px;
72
+ background: var(--surface); border: 1.5px solid var(--line);
73
+ display: grid; place-items: center;
74
+ font: 600 26px/1 ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-style: normal;
75
+ transition: border-color .15s, transform .15s;
76
+ }
77
+ .code-slots i.filled { border-color: #3a4150; }
78
+ .code-slots i.active { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(198, 244, 91, .15); }
79
+ .code-field.shake { animation: shake .35s; }
80
+ @keyframes shake { 20%, 60% { transform: translateX(-6px); } 40%, 80% { transform: translateX(6px); } }
81
+
82
+ .text-input {
83
+ width: 100%; height: 50px; padding: 0 16px; margin-bottom: 14px;
84
+ background: var(--surface); border: 1.5px solid var(--line); border-radius: 14px;
85
+ color: var(--text); font-size: 16px; outline: none;
86
+ }
87
+ .text-input:focus { border-color: #3a4150; }
88
+
89
+ .primary {
90
+ width: 100%; height: 54px; border-radius: 14px;
91
+ background: var(--accent); color: var(--accent-ink);
92
+ font-weight: 700; font-size: 16px;
93
+ transition: transform .1s, opacity .2s;
94
+ }
95
+ .primary:active { transform: scale(.98); }
96
+ .primary:disabled { opacity: .5; }
97
+
98
+ .error { color: var(--danger); min-height: 20px; margin: 14px 0 0 !important; font-size: 14px; }
99
+
100
+ /* ---------------- trackpad ---------------- */
101
+ .pad-screen {
102
+ display: flex; flex-direction: column;
103
+ padding: calc(10px + var(--safe-t)) 12px calc(12px + var(--safe-b));
104
+ gap: 10px;
105
+ user-select: none; -webkit-user-select: none;
106
+ }
107
+
108
+ .topbar { display: flex; align-items: center; justify-content: space-between; padding: 2px 4px; }
109
+ .top-actions { display: flex; gap: 8px; }
110
+ .icon-btn[aria-pressed="true"] { background: var(--accent); }
111
+ .icon-btn[aria-pressed="true"] svg { fill: var(--accent-ink); }
112
+ .conn { display: flex; align-items: center; gap: 10px; min-width: 0; }
113
+ .conn strong { display: block; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
114
+ .conn small { color: var(--muted); font-size: 12px; }
115
+ .dot { width: 10px; height: 10px; border-radius: 50%; background: var(--warn); flex: none; box-shadow: 0 0 0 4px rgba(251, 191, 36, .15); }
116
+ .dot.on { background: var(--ok); box-shadow: 0 0 0 4px rgba(74, 222, 128, .15); }
117
+ .dot.off { background: var(--danger); box-shadow: 0 0 0 4px rgba(255, 107, 107, .15); }
118
+
119
+ .icon-btn { width: 42px; height: 42px; border-radius: 12px; background: var(--surface); display: grid; place-items: center; }
120
+ .icon-btn svg { width: 20px; height: 20px; fill: var(--muted); }
121
+
122
+ .touchpad {
123
+ position: relative; flex: 1; min-height: 0;
124
+ border-radius: var(--radius);
125
+ background:
126
+ radial-gradient(circle at center, rgba(255, 255, 255, .045) 1px, transparent 1.5px) 0 0 / 22px 22px,
127
+ linear-gradient(180deg, var(--surface-2), var(--surface));
128
+ border: 1px solid var(--line);
129
+ touch-action: none;
130
+ overflow: hidden;
131
+ }
132
+
133
+ .pad-hint {
134
+ position: absolute; inset: 0;
135
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
136
+ color: #5d6577; font-size: 13px; pointer-events: none;
137
+ transition: opacity .4s;
138
+ }
139
+ .pad-hint b { color: #7d8597; font-weight: 600; }
140
+ .pad-hint.faded { opacity: 0; }
141
+
142
+ .ripple {
143
+ position: absolute; width: 56px; height: 56px; margin: -28px 0 0 -28px;
144
+ border-radius: 50%; pointer-events: none;
145
+ background: radial-gradient(circle, rgba(198, 244, 91, .35), rgba(198, 244, 91, 0) 70%);
146
+ animation: ripple .45s ease-out forwards;
147
+ }
148
+ @keyframes ripple { from { transform: scale(.4); opacity: 1; } to { transform: scale(1.6); opacity: 0; } }
149
+
150
+ .drag-badge {
151
+ position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
152
+ padding: 4px 12px; border-radius: 999px;
153
+ background: var(--accent); color: var(--accent-ink);
154
+ font-size: 11px; font-weight: 800; letter-spacing: .12em;
155
+ }
156
+
157
+ .scroll-strip {
158
+ position: absolute; top: 12px; bottom: 12px; right: 8px; width: 30px;
159
+ border-radius: 999px;
160
+ background: repeating-linear-gradient(180deg, rgba(255, 255, 255, .07) 0 2px, transparent 2px 12px);
161
+ border: 1px solid rgba(255, 255, 255, .05);
162
+ }
163
+ .scroll-strip.active { background-color: rgba(198, 244, 91, .08); border-color: rgba(198, 244, 91, .3); }
164
+
165
+ .buttons { display: grid; grid-template-columns: 1fr 64px 1fr; gap: 10px; height: 84px; flex: none; }
166
+
167
+ /* ---------------- preview layar ---------------- */
168
+ .screen-view { flex: none; display: flex; flex-direction: column; gap: 6px; }
169
+ .stage {
170
+ position: relative; margin: 0 auto; overflow: hidden;
171
+ width: 100%; aspect-ratio: 16 / 9; /* diganti JS sesuai ukuran monitor */
172
+ border-radius: 12px; background: #000;
173
+ box-shadow: 0 0 0 1px var(--line); /* bukan border: border menggeser area gambar dan membuat koordinat cursor melenceng */
174
+ }
175
+ /* tanpa transisi: gambar & cursor harus selalu sejajar */
176
+ .stage-inner { position: absolute; inset: 0; transform-origin: 0 0; }
177
+ .stage-inner img { display: block; width: 100%; height: 100%; user-select: none; -webkit-user-drag: none; }
178
+ .vcursor {
179
+ position: absolute; left: 0; top: 0; width: 22px; height: 22px;
180
+ transform-origin: 0 0; pointer-events: none; will-change: transform;
181
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .7));
182
+ }
183
+ .vcursor svg { width: 100%; height: 100%; fill: #fff; stroke: #000; stroke-width: 1.4; stroke-linejoin: round; }
184
+ .vcursor::after {
185
+ content: ""; position: absolute; left: -8px; top: -8px; width: 18px; height: 18px; border-radius: 50%;
186
+ border: 2px solid var(--accent); opacity: 0; transform: scale(.3);
187
+ }
188
+ .vcursor.clicked::after { animation: vclick .35s ease-out; }
189
+ .vcursor.right::after { border-color: #60a5fa; }
190
+ @keyframes vclick { 0% { opacity: 1; transform: scale(.3); } 100% { opacity: 0; transform: scale(1.6); } }
191
+
192
+ .stage-msg {
193
+ position: absolute; inset: 0; display: grid; place-items: center; padding: 12px; text-align: center;
194
+ color: var(--muted); font-size: 13px; background: rgba(0, 0, 0, .55);
195
+ }
196
+ .stage-bar { display: flex; align-items: center; gap: 6px; }
197
+ .stage-bar .chip { height: 30px; padding: 0 10px; }
198
+ .stats { margin-left: auto; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
199
+ .screen-big .touchpad { min-height: 90px; flex-basis: 90px; }
200
+
201
+ /* ---------------- keyboard ---------------- */
202
+ .kb { display: flex; flex-direction: column; gap: 6px; flex: none; }
203
+ .kb-open .buttons { display: none; }
204
+ .kb-open .touchpad { min-height: 110px; }
205
+ .kb-open .pad-hint { display: none; }
206
+
207
+ .kb-row { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 6px; }
208
+ .kk {
209
+ height: 40px; border-radius: 10px; padding: 0 4px;
210
+ background: var(--surface-2); border: 1px solid var(--line);
211
+ font-size: 13px; font-weight: 600; color: var(--muted);
212
+ touch-action: manipulation;
213
+ }
214
+ .kk:active, .kk.pressed { background: #2a303c; color: var(--text); }
215
+ .kk.mod.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
216
+ .kk.icon { width: 52px; flex: none; font-size: 18px; height: 46px; }
217
+ .kk.accent { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
218
+
219
+ .kb-chips {
220
+ display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none;
221
+ margin: 0 -12px; padding: 0 12px;
222
+ }
223
+ .kb-chips::-webkit-scrollbar { display: none; }
224
+ .chip {
225
+ flex: none; height: 34px; padding: 0 12px; border-radius: 999px;
226
+ background: var(--surface); border: 1px solid var(--line);
227
+ font-size: 12px; font-weight: 600; color: var(--muted); white-space: nowrap;
228
+ }
229
+ .chip:active, .chip.pressed { border-color: var(--accent); color: var(--text); }
230
+
231
+ .kb-input { position: relative; display: flex; gap: 6px; align-items: center; }
232
+ .kb-input textarea {
233
+ flex: 1; min-width: 0; height: 46px; resize: none;
234
+ padding: 12px 14px; border-radius: 12px;
235
+ background: var(--surface); border: 1.5px solid var(--line); color: var(--text);
236
+ font: 16px/1.4 system-ui, sans-serif; /* 16px mencegah zoom otomatis di iOS */
237
+ outline: none; overflow: hidden; white-space: nowrap;
238
+ }
239
+ .kb-input textarea:focus { border-color: var(--accent); }
240
+ .kb-placeholder {
241
+ position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
242
+ color: #5d6577; font-size: 14px; pointer-events: none;
243
+ max-width: calc(100% - 140px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
244
+ }
245
+ .mbtn {
246
+ border-radius: var(--radius);
247
+ background: var(--surface-2); border: 1px solid var(--line);
248
+ font-weight: 600; font-size: 15px; color: var(--muted);
249
+ touch-action: none;
250
+ transition: background .08s, transform .08s;
251
+ }
252
+ .mbtn.down { background: #2a303c; color: var(--text); transform: scale(.98); border-color: #3a4150; }
253
+ .mbtn.mid { display: grid; place-items: center; }
254
+ .mbtn.mid i { width: 8px; height: 26px; border-radius: 99px; background: #3a4150; }
255
+ .mbtn.mid.down i { background: var(--accent); }
256
+
257
+ /* ---------------- overlay & sheet ---------------- */
258
+ .overlay {
259
+ position: absolute; inset: 0; z-index: 10;
260
+ display: grid; place-items: center; padding: 24px;
261
+ background: rgba(13, 15, 20, .82); backdrop-filter: blur(6px);
262
+ }
263
+ .overlay-card { text-align: center; width: 100%; max-width: 320px; }
264
+ .overlay-card p { white-space: pre-line; font-size: 17px; line-height: 1.5; margin: 0 0 18px; }
265
+
266
+ .sheet { position: absolute; inset: 0; z-index: 20; }
267
+ .sheet-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, .55); animation: fade .2s; }
268
+ .sheet-panel {
269
+ position: absolute; left: 0; right: 0; bottom: 0;
270
+ max-height: 85%; overflow-y: auto;
271
+ background: var(--surface); border-top: 1px solid var(--line);
272
+ border-radius: 22px 22px 0 0;
273
+ padding: 10px 20px calc(20px + var(--safe-b));
274
+ animation: up .25s cubic-bezier(.2, .8, .2, 1);
275
+ }
276
+ @keyframes up { from { transform: translateY(100%); } }
277
+ @keyframes fade { from { opacity: 0; } }
278
+ .sheet-grip { width: 40px; height: 4px; border-radius: 9px; background: var(--line); margin: 0 auto 14px; }
279
+ .sheet h2 { margin: 0 0 16px; font-size: 18px; }
280
+
281
+ .setting { display: block; margin-bottom: 18px; }
282
+ .setting span { display: flex; justify-content: space-between; font-size: 14px; margin-bottom: 10px; }
283
+ .setting output { color: var(--accent); font-variant-numeric: tabular-nums; }
284
+ input[type="range"] { width: 100%; accent-color: var(--accent); }
285
+
286
+ .toggle { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-top: 1px solid var(--line); font-size: 14px; }
287
+ .toggle input { display: none; }
288
+ .toggle i { width: 44px; height: 26px; border-radius: 99px; background: var(--line); position: relative; transition: background .2s; }
289
+ .toggle i::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; transition: transform .2s; }
290
+ .toggle input:checked + i { background: var(--accent); }
291
+ .toggle input:checked + i::after { transform: translateX(18px); }
292
+
293
+ .sheet-section { margin: 22px 0 12px; padding-top: 16px; border-top: 1px solid var(--line); font-size: 15px; }
294
+ .preset-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 18px; }
295
+ .preset {
296
+ height: 38px; border-radius: 10px; font-weight: 600; font-size: 13px;
297
+ background: var(--surface-2); border: 1px solid var(--line); color: var(--muted);
298
+ }
299
+ .preset.on { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
300
+ .select {
301
+ width: 100%; height: 42px; padding: 0 12px; border-radius: 10px;
302
+ background: var(--surface-2); border: 1px solid var(--line); color: var(--text); font: inherit; font-size: 14px;
303
+ }
304
+
305
+ .sheet-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 14px; }
306
+ .sheet-actions .danger { grid-column: 1 / -1; }
307
+ .ghost, .danger { height: 48px; border-radius: 14px; font-weight: 600; }
308
+ .ghost { background: var(--surface-2); border: 1px solid var(--line); }
309
+ .danger { background: rgba(255, 107, 107, .1); color: var(--danger); border: 1px solid rgba(255, 107, 107, .25); }
310
+
311
+ @media (orientation: landscape) and (max-height: 500px) {
312
+ .pad-screen { flex-direction: row; flex-wrap: wrap; }
313
+ .topbar { width: 100%; }
314
+ .touchpad { height: calc(100% - 60px); }
315
+ .buttons { grid-template-columns: 1fr; grid-template-rows: 1fr 40px 1fr; width: 110px; height: calc(100% - 60px); }
316
+ .kb { width: 100%; }
317
+ .kb-open .touchpad { flex-basis: 100%; height: auto; min-height: 60px; }
318
+ }
@@ -0,0 +1,64 @@
1
+ // Client kecil untuk Admin API server (hanya localhost). Dipakai CLI untuk status & manajemen session,
2
+ // baik server berjalan di terminal ini maupun di background.
3
+ const config = require('./config');
4
+
5
+ function target() {
6
+ const cfg = config.loadConfig();
7
+ const rt = config.readRuntime();
8
+ return { port: rt?.port || cfg.port, token: cfg.adminToken };
9
+ }
10
+
11
+ async function call(method, pathname, body, timeoutMs = 2000) {
12
+ const { port, token } = target();
13
+ const res = await fetch(`http://127.0.0.1:${port}/admin${pathname}`, {
14
+ method,
15
+ headers: { 'x-admin-token': token, 'content-type': 'application/json' },
16
+ body: body === undefined ? undefined : JSON.stringify(body),
17
+ signal: AbortSignal.timeout(timeoutMs),
18
+ });
19
+ const data = await res.json().catch(() => ({}));
20
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
21
+ return data;
22
+ }
23
+
24
+ /** Status server yang sedang berjalan, atau null jika tidak ada. */
25
+ async function getStatus() {
26
+ try {
27
+ return await call('GET', '/status', undefined, 800);
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async function waitUntilUp(timeoutMs = 10_000) {
34
+ const end = Date.now() + timeoutMs;
35
+ while (Date.now() < end) {
36
+ const st = await getStatus();
37
+ if (st) return st;
38
+ await new Promise((r) => setTimeout(r, 300));
39
+ }
40
+ return null;
41
+ }
42
+
43
+ async function waitUntilDown(timeoutMs = 5000) {
44
+ const end = Date.now() + timeoutMs;
45
+ while (Date.now() < end) {
46
+ if (!(await getStatus())) return true;
47
+ await new Promise((r) => setTimeout(r, 200));
48
+ }
49
+ return false;
50
+ }
51
+
52
+ module.exports = {
53
+ getStatus,
54
+ waitUntilUp,
55
+ waitUntilDown,
56
+ sessions: () => call('GET', '/sessions'),
57
+ disconnect: (id) => call('POST', `/sessions/${encodeURIComponent(id)}/disconnect`, {}),
58
+ revoke: (id) => call('POST', `/sessions/${encodeURIComponent(id)}/revoke`, {}),
59
+ revokeAll: () => call('POST', '/sessions/revoke-all', {}),
60
+ regenerateCode: () => call('POST', '/pair-code/regenerate', {}),
61
+ setPairing: (allow) => call('POST', '/pairing', { allow }),
62
+ updateSettings: (patch) => call('POST', '/settings', patch),
63
+ shutdown: () => call('POST', '/shutdown', {}),
64
+ };