WIP PWA for Grain

feat: add grain-button atom with loading state

+84 -22
+79
src/components/atoms/grain-button.js
··· 1 + import { LitElement, html, css } from 'lit'; 2 + import './grain-spinner.js'; 3 + 4 + export class GrainButton extends LitElement { 5 + static properties = { 6 + variant: { type: String }, 7 + loading: { type: Boolean }, 8 + loadingText: { type: String }, 9 + disabled: { type: Boolean, reflect: true } 10 + }; 11 + 12 + static styles = css` 13 + :host { 14 + display: inline-block; 15 + } 16 + button { 17 + display: flex; 18 + align-items: center; 19 + justify-content: center; 20 + gap: 6px; 21 + border: none; 22 + padding: 8px 16px; 23 + border-radius: 6px; 24 + font-size: var(--font-size-sm); 25 + font-weight: var(--font-weight-semibold); 26 + font-family: inherit; 27 + cursor: pointer; 28 + transition: opacity 0.15s; 29 + } 30 + button:disabled { 31 + opacity: 0.5; 32 + cursor: not-allowed; 33 + } 34 + button.primary { 35 + background: var(--color-accent, #0066cc); 36 + color: white; 37 + } 38 + button.secondary { 39 + background: transparent; 40 + color: var(--color-text-primary); 41 + border: 1px solid var(--color-border); 42 + } 43 + button.danger { 44 + background: #ff4444; 45 + color: white; 46 + } 47 + button.ghost { 48 + background: transparent; 49 + color: var(--color-text-secondary); 50 + padding: 8px; 51 + } 52 + `; 53 + 54 + constructor() { 55 + super(); 56 + this.variant = 'primary'; 57 + this.loading = false; 58 + this.loadingText = ''; 59 + this.disabled = false; 60 + } 61 + 62 + render() { 63 + return html` 64 + <button 65 + class=${this.variant} 66 + ?disabled=${this.disabled || this.loading} 67 + > 68 + ${this.loading ? html` 69 + <grain-spinner size="16"></grain-spinner> 70 + ${this.loadingText || html`<slot></slot>`} 71 + ` : html` 72 + <slot></slot> 73 + `} 74 + </button> 75 + `; 76 + } 77 + } 78 + 79 + customElements.define('grain-button', GrainButton);
+5 -22
src/components/pages/grain-create-gallery.js
··· 3 3 import { auth } from '../../services/auth.js'; 4 4 import { draftGallery } from '../../services/draft-gallery.js'; 5 5 import '../atoms/grain-icon.js'; 6 - import '../atoms/grain-spinner.js'; 6 + import '../atoms/grain-button.js'; 7 7 8 8 const UPLOAD_BLOB_MUTATION = ` 9 9 mutation UploadBlob($data: String!, $mimeType: String!) { ··· 68 68 margin-left: -8px; 69 69 cursor: pointer; 70 70 color: var(--color-text-primary); 71 - } 72 - .post-button { 73 - display: flex; 74 - align-items: center; 75 - gap: 6px; 76 - background: var(--color-accent, #0066cc); 77 - color: white; 78 - border: none; 79 - padding: 8px 16px; 80 - border-radius: 6px; 81 - font-weight: var(--font-weight-semibold); 82 - cursor: pointer; 83 - } 84 - .post-button:disabled { 85 - opacity: 0.5; 86 - cursor: not-allowed; 87 71 } 88 72 .photo-strip { 89 73 display: flex; ··· 287 271 <button class="back-button" @click=${this.#handleBack}> 288 272 <grain-icon name="back" size="20"></grain-icon> 289 273 </button> 290 - <button 291 - class="post-button" 274 + <grain-button 292 275 ?disabled=${!this.#canPost} 276 + ?loading=${this._posting} 277 + loadingText="Posting..." 293 278 @click=${this.#handlePost} 294 - > 295 - ${this._posting ? html`<grain-spinner size="16"></grain-spinner> Posting...` : 'Post'} 296 - </button> 279 + >Post</grain-button> 297 280 </div> 298 281 299 282 <div class="photo-strip">