blob: 510f60a33471359457b9bcb60afc3b08b41216f8 [file] [log] [blame]
Philip Zeyligere08c7ff2025-06-06 13:22:12 -07001import { css, html, LitElement } from "lit";
2import { customElement, property } from "lit/decorators.js";
3import { ConnectionStatus } from "../data";
4
5@customElement("mobile-title")
6export class MobileTitle extends LitElement {
7 @property({ type: String })
8 connectionStatus: ConnectionStatus = "disconnected";
9
10 @property({ type: Boolean })
11 isThinking = false;
12
Philip Zeyliger0113be52025-06-07 23:53:41 +000013 @property({ type: String })
14 skabandAddr?: string;
15
philip.zeyliger6b8b7662025-06-16 03:06:30 +000016 @property({ type: String })
17 currentView: "chat" | "diff" = "chat";
18
19 @property({ type: String })
20 slug: string = "";
21
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070022 static styles = css`
23 :host {
24 display: block;
25 background-color: #f8f9fa;
26 border-bottom: 1px solid #e9ecef;
27 padding: 12px 16px;
28 }
29
30 .title-container {
31 display: flex;
philip.zeyliger6b8b7662025-06-16 03:06:30 +000032 align-items: flex-start;
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070033 justify-content: space-between;
34 }
35
philip.zeyliger6b8b7662025-06-16 03:06:30 +000036 .title-section {
37 flex: 1;
38 min-width: 0;
39 }
40
41 .right-section {
42 display: flex;
43 align-items: center;
44 gap: 12px;
45 }
46
47 .view-selector {
48 background: none;
49 border: 1px solid #e9ecef;
50 border-radius: 6px;
51 padding: 6px 8px;
52 font-size: 14px;
53 font-weight: 500;
54 cursor: pointer;
55 transition: all 0.2s ease;
56 color: #495057;
57 min-width: 60px;
58 }
59
60 .view-selector:hover {
61 background-color: #f8f9fa;
62 border-color: #dee2e6;
63 }
64
65 .view-selector:focus {
66 outline: none;
67 border-color: #007acc;
68 box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
69 }
70
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070071 .title {
72 font-size: 18px;
73 font-weight: 600;
74 color: #212529;
75 margin: 0;
76 }
77
philip.zeyliger6b8b7662025-06-16 03:06:30 +000078
79
Philip Zeyliger0113be52025-06-07 23:53:41 +000080 .title a {
81 color: inherit;
82 text-decoration: none;
83 transition: opacity 0.2s ease;
84 display: flex;
85 align-items: center;
86 gap: 8px;
87 }
88
89 .title a:hover {
90 opacity: 0.8;
91 text-decoration: underline;
92 }
93
94 .title img {
95 width: 18px;
96 height: 18px;
97 border-radius: 3px;
98 }
99
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700100 .status-indicator {
101 display: flex;
102 align-items: center;
103 gap: 8px;
104 font-size: 14px;
105 }
106
107 .status-dot {
108 width: 8px;
109 height: 8px;
110 border-radius: 50%;
111 flex-shrink: 0;
112 }
113
114 .status-dot.connected {
115 background-color: #28a745;
116 }
117
118 .status-dot.connecting {
119 background-color: #ffc107;
120 animation: pulse 1.5s ease-in-out infinite;
121 }
122
123 .status-dot.disconnected {
124 background-color: #dc3545;
125 }
126
127 .thinking-indicator {
128 display: flex;
129 align-items: center;
130 gap: 6px;
131 color: #6c757d;
132 font-size: 13px;
133 }
134
135 .thinking-dots {
136 display: flex;
137 gap: 2px;
138 }
139
140 .thinking-dot {
141 width: 4px;
142 height: 4px;
143 border-radius: 50%;
144 background-color: #6c757d;
145 animation: thinking 1.4s ease-in-out infinite both;
146 }
147
148 .thinking-dot:nth-child(1) {
149 animation-delay: -0.32s;
150 }
151 .thinking-dot:nth-child(2) {
152 animation-delay: -0.16s;
153 }
154 .thinking-dot:nth-child(3) {
155 animation-delay: 0;
156 }
157
158 @keyframes pulse {
159 0%,
160 100% {
161 opacity: 1;
162 }
163 50% {
164 opacity: 0.5;
165 }
166 }
167
168 @keyframes thinking {
169 0%,
170 80%,
171 100% {
172 transform: scale(0);
173 }
174 40% {
175 transform: scale(1);
176 }
177 }
178 `;
179
180 private getStatusText() {
181 switch (this.connectionStatus) {
182 case "connected":
183 return "Connected";
184 case "connecting":
185 return "Connecting...";
186 case "disconnected":
187 return "Disconnected";
188 default:
189 return "Unknown";
190 }
191 }
192
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000193 private handleViewChange(event: Event) {
194 const select = event.target as HTMLSelectElement;
195 const view = select.value as "chat" | "diff";
196 if (view !== this.currentView) {
197 const changeEvent = new CustomEvent("view-change", {
198 detail: { view },
199 bubbles: true,
200 composed: true,
201 });
202 this.dispatchEvent(changeEvent);
203 }
204 }
205
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700206 render() {
207 return html`
208 <div class="title-container">
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000209 <div class="title-section">
210 <h1 class="title">
211 ${this.skabandAddr
212 ? html`<a
213 href="${this.skabandAddr}"
214 target="_blank"
215 rel="noopener noreferrer"
216 >
217 <img src="${this.skabandAddr}/sketch.dev.png" alt="sketch" />
218 ${this.slug || "Sketch"}
219 </a>`
220 : html`${this.slug || "Sketch"}`}
221 </h1>
222 </div>
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700223
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000224 <div class="right-section">
225 <select
226 class="view-selector"
227 .value=${this.currentView}
228 @change=${this.handleViewChange}
229 >
230 <option value="chat">Chat</option>
231 <option value="diff">Diff</option>
232 </select>
233
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700234 ${this.isThinking
235 ? html`
236 <div class="thinking-indicator">
237 <span>thinking</span>
238 <div class="thinking-dots">
239 <div class="thinking-dot"></div>
240 <div class="thinking-dot"></div>
241 <div class="thinking-dot"></div>
242 </div>
243 </div>
244 `
245 : html`
246 <span class="status-dot ${this.connectionStatus}"></span>
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700247 `}
248 </div>
249 </div>
250 `;
251 }
252}