forked from
grain.social/grain-pwa
WIP PWA for Grain
1import { LitElement, html, css } from 'lit';
2import '../atoms/grain-avatar.js';
3import '../atoms/grain-spinner.js';
4
5export class GrainCommentInput extends LitElement {
6 static properties = {
7 avatarUrl: { type: String },
8 value: { type: String },
9 placeholder: { type: String },
10 disabled: { type: Boolean },
11 loading: { type: Boolean },
12 focusPhotoUrl: { type: String }
13 };
14
15 static styles = css`
16 :host {
17 display: flex;
18 align-items: center;
19 gap: var(--space-sm);
20 padding: var(--space-sm);
21 border-top: 1px solid var(--color-border);
22 background: var(--color-bg-primary);
23 }
24 .input-wrapper {
25 flex: 1;
26 display: flex;
27 align-items: center;
28 gap: var(--space-sm);
29 background: var(--color-bg-secondary);
30 border-radius: 20px;
31 padding: var(--space-xs) var(--space-sm);
32 }
33 input {
34 flex: 1;
35 background: none;
36 border: none;
37 outline: none;
38 font-size: var(--font-size-sm);
39 color: var(--color-text-primary);
40 font-family: inherit;
41 }
42 input::placeholder {
43 color: var(--color-text-secondary);
44 }
45 input:disabled {
46 opacity: 0.5;
47 }
48 .send-button {
49 display: flex;
50 align-items: center;
51 justify-content: center;
52 background: none;
53 border: none;
54 padding: var(--space-xs);
55 cursor: pointer;
56 color: var(--color-accent);
57 font-size: var(--font-size-sm);
58 font-weight: var(--font-weight-semibold);
59 min-width: 32px;
60 min-height: 20px;
61 }
62 .send-button:disabled {
63 opacity: 0.5;
64 cursor: not-allowed;
65 }
66 .focus-photo {
67 position: relative;
68 flex-shrink: 0;
69 }
70 .focus-photo img {
71 width: 32px;
72 height: 32px;
73 border-radius: 4px;
74 object-fit: cover;
75 }
76 .focus-photo .clear-btn {
77 position: absolute;
78 top: -4px;
79 right: -4px;
80 width: 16px;
81 height: 16px;
82 border-radius: 50%;
83 background: var(--color-bg-elevated);
84 border: 1px solid var(--color-border);
85 display: flex;
86 align-items: center;
87 justify-content: center;
88 cursor: pointer;
89 font-size: 10px;
90 color: var(--color-text-secondary);
91 padding: 0;
92 }
93 `;
94
95 constructor() {
96 super();
97 this.avatarUrl = '';
98 this.value = '';
99 this.placeholder = 'Add a comment...';
100 this.disabled = false;
101 this.loading = false;
102 this.focusPhotoUrl = '';
103 }
104
105 #handleInput(e) {
106 this.value = e.target.value;
107 this.dispatchEvent(new CustomEvent('input-change', {
108 detail: { value: this.value }
109 }));
110 }
111
112 #handleSend() {
113 if (!this.value.trim() || this.disabled || this.loading) return;
114 this.dispatchEvent(new CustomEvent('send', {
115 detail: { value: this.value.trim() }
116 }));
117 }
118
119 #handleClearFocus() {
120 this.dispatchEvent(new CustomEvent('clear-focus'));
121 }
122
123 focus() {
124 this.shadowRoot.querySelector('input')?.focus();
125 }
126
127 clear() {
128 this.value = '';
129 }
130
131 render() {
132 const canSend = this.value.trim() && !this.disabled && !this.loading;
133
134 return html`
135 <grain-avatar src=${this.avatarUrl} size="sm"></grain-avatar>
136 ${this.focusPhotoUrl ? html`
137 <div class="focus-photo">
138 <img src=${this.focusPhotoUrl} alt="Commenting on this photo" />
139 <button class="clear-btn" @click=${this.#handleClearFocus}>×</button>
140 </div>
141 ` : ''}
142 <div class="input-wrapper">
143 <input
144 type="text"
145 .value=${this.value}
146 placeholder=${this.placeholder}
147 ?disabled=${this.disabled || this.loading}
148 @input=${this.#handleInput}
149 />
150 <button
151 class="send-button"
152 type="button"
153 ?disabled=${!canSend}
154 @click=${this.#handleSend}
155 >
156 ${this.loading ? html`<grain-spinner size="16"></grain-spinner>` : 'Post'}
157 </button>
158 </div>
159 `;
160 }
161}
162
163customElements.define('grain-comment-input', GrainCommentInput);