blob: 8ac91662792389a97212cb6b6d6934c38444eaae [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 Zeyliger0113be52025-06-07 23:53:41 +000078 .title a {
79 color: inherit;
80 text-decoration: none;
81 transition: opacity 0.2s ease;
82 display: flex;
83 align-items: center;
84 gap: 8px;
85 }
86
87 .title a:hover {
88 opacity: 0.8;
89 text-decoration: underline;
90 }
91
92 .title img {
93 width: 18px;
94 height: 18px;
95 border-radius: 3px;
96 }
97
Philip Zeyligere08c7ff2025-06-06 13:22:12 -070098 .status-indicator {
99 display: flex;
100 align-items: center;
101 gap: 8px;
102 font-size: 14px;
103 }
104
105 .status-dot {
106 width: 8px;
107 height: 8px;
108 border-radius: 50%;
109 flex-shrink: 0;
110 }
111
112 .status-dot.connected {
113 background-color: #28a745;
114 }
115
116 .status-dot.connecting {
117 background-color: #ffc107;
118 animation: pulse 1.5s ease-in-out infinite;
119 }
120
121 .status-dot.disconnected {
122 background-color: #dc3545;
123 }
124
125 .thinking-indicator {
126 display: flex;
127 align-items: center;
128 gap: 6px;
129 color: #6c757d;
130 font-size: 13px;
131 }
132
133 .thinking-dots {
134 display: flex;
135 gap: 2px;
136 }
137
138 .thinking-dot {
139 width: 4px;
140 height: 4px;
141 border-radius: 50%;
142 background-color: #6c757d;
143 animation: thinking 1.4s ease-in-out infinite both;
144 }
145
146 .thinking-dot:nth-child(1) {
147 animation-delay: -0.32s;
148 }
149 .thinking-dot:nth-child(2) {
150 animation-delay: -0.16s;
151 }
152 .thinking-dot:nth-child(3) {
153 animation-delay: 0;
154 }
155
156 @keyframes pulse {
157 0%,
158 100% {
159 opacity: 1;
160 }
161 50% {
162 opacity: 0.5;
163 }
164 }
165
166 @keyframes thinking {
167 0%,
168 80%,
169 100% {
170 transform: scale(0);
171 }
172 40% {
173 transform: scale(1);
174 }
175 }
176 `;
177
178 private getStatusText() {
179 switch (this.connectionStatus) {
180 case "connected":
181 return "Connected";
182 case "connecting":
183 return "Connecting...";
184 case "disconnected":
185 return "Disconnected";
186 default:
187 return "Unknown";
188 }
189 }
190
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000191 private handleViewChange(event: Event) {
192 const select = event.target as HTMLSelectElement;
193 const view = select.value as "chat" | "diff";
194 if (view !== this.currentView) {
195 const changeEvent = new CustomEvent("view-change", {
196 detail: { view },
197 bubbles: true,
198 composed: true,
199 });
200 this.dispatchEvent(changeEvent);
201 }
202 }
203
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700204 render() {
205 return html`
206 <div class="title-container">
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000207 <div class="title-section">
208 <h1 class="title">
209 ${this.skabandAddr
210 ? html`<a
211 href="${this.skabandAddr}"
212 target="_blank"
213 rel="noopener noreferrer"
214 >
215 <img src="${this.skabandAddr}/sketch.dev.png" alt="sketch" />
216 ${this.slug || "Sketch"}
217 </a>`
218 : html`${this.slug || "Sketch"}`}
219 </h1>
220 </div>
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700221
philip.zeyliger6b8b7662025-06-16 03:06:30 +0000222 <div class="right-section">
223 <select
224 class="view-selector"
225 .value=${this.currentView}
226 @change=${this.handleViewChange}
227 >
228 <option value="chat">Chat</option>
229 <option value="diff">Diff</option>
230 </select>
231
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700232 ${this.isThinking
233 ? html`
234 <div class="thinking-indicator">
235 <span>thinking</span>
236 <div class="thinking-dots">
237 <div class="thinking-dot"></div>
238 <div class="thinking-dot"></div>
239 <div class="thinking-dot"></div>
240 </div>
241 </div>
242 `
Autoformatterf964b502025-06-17 04:30:35 +0000243 : html` <span class="status-dot ${this.connectionStatus}"></span> `}
Philip Zeyligere08c7ff2025-06-06 13:22:12 -0700244 </div>
245 </div>
246 `;
247 }
248}