WIP PWA for Grain
at main 163 lines 4.1 kB view raw
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}>&times;</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);