diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index 902c1533..073617d5 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -359,24 +359,306 @@ display: block; } -.view-all-btn { +.recipes-header { display: flex; - align-items: center; - gap: 5px; - padding: 6px 12px; - background-color: var(--lora-accent); - color: var(--lora-text); - border: none; - border-radius: var(--border-radius-sm); - cursor: pointer; - transition: background-color 0.2s; - font-size: 13px; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-3); + padding: var(--space-2) 0 var(--space-3); + margin-bottom: var(--space-2); + border-bottom: 1px solid var(--lora-border); } -.view-all-btn:hover { - opacity: 0.9; +.recipes-header__text { + display: flex; + flex-direction: column; + gap: 6px; + max-width: 520px; } +.recipes-header__eyebrow { + font-size: 0.75em; + text-transform: uppercase; + letter-spacing: 0.12em; + font-weight: 600; + color: var(--text-color); + opacity: 0.6; +} + +.recipes-header__text h3 { + margin: 0; + font-size: 1.1em; + line-height: 1.4; +} + +.recipes-header__description { + margin: 0; + font-size: 0.9em; + line-height: 1.5; + color: var(--text-color); + opacity: 0.75; +} + +.recipes-header__view-all { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: 8px 14px; + border: 1px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.35); + background: transparent; + color: var(--lora-accent); + border-radius: var(--border-radius-sm); + cursor: pointer; + font-size: 0.9em; + font-weight: 600; + transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease; +} + +.recipes-header__view-all i { + font-size: 0.85em; +} + +.recipes-header__view-all:hover, +.recipes-header__view-all:focus-visible { + background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15); + border-color: var(--lora-accent); + outline: none; + transform: translateY(-1px); +} + +.recipes-header__view-all:active { + transform: translateY(0); +} + +.recipes-card-grid { + max-width: none; + margin: var(--space-3) 0 0; + padding: 0; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: var(--space-3); + row-gap: var(--space-3); +} + +.recipe-card { + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-base); + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 320px; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} + +.recipe-card:hover { + transform: translateY(-4px); + border-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.6); + box-shadow: 0 16px 32px rgba(17, 17, 26, 0.18); +} + +.recipe-card:focus-visible { + outline: 2px solid var(--lora-accent); + outline-offset: 3px; +} + +.recipe-card__media { + position: relative; + overflow: hidden; + aspect-ratio: 4 / 3; + background: var(--lora-surface); +} + +.recipe-card__media img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.25s ease; +} + +.recipe-card:hover .recipe-card__media img { + transform: scale(1.02); +} + +.recipe-card__media::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 36%; + background: linear-gradient(180deg, transparent 0%, rgba(12, 13, 24, 0.55) 100%); + pointer-events: none; +} + +.recipe-card__media-top { + position: absolute; + top: var(--space-1); + right: var(--space-1); + display: flex; + gap: var(--space-1); +} + +.recipe-card__copy { + background: rgba(15, 21, 40, 0.6); + border: none; + border-radius: 999px; + color: white; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease, opacity 0.2s ease; +} + +.recipe-card__copy i { + font-size: 0.85em; +} + +.recipe-card__copy:hover, +.recipe-card__copy:focus-visible { + background: rgba(15, 21, 40, 0.8); + transform: translateY(-1px); + outline: none; +} + +.recipe-card__copy:active { + transform: translateY(0); +} + +[data-theme="light"] .recipe-card__copy { + background: rgba(255, 255, 255, 0.85); + color: rgba(17, 23, 41, 0.8); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08); +} + +[data-theme="light"] .recipe-card__copy:hover, +[data-theme="light"] .recipe-card__copy:focus-visible { + background: rgba(255, 255, 255, 0.95); +} + +.recipe-card__body { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding: var(--space-2); + flex: 1; +} + +.recipe-card__title { + margin: 0; + font-size: 1.05em; + line-height: 1.4; + font-weight: 600; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.recipe-card__meta { + display: flex; + flex-wrap: wrap; + gap: var(--space-1); +} + +.recipe-card__badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 999px; + font-size: 0.78em; + font-weight: 600; + line-height: 1; + background: rgba(255, 255, 255, 0.08); + color: var(--text-color); +} + +.recipe-card__badge i { + font-size: 0.85em; +} + +.recipe-card__badge--base { + background: rgba(255, 255, 255, 0.1); + color: var(--text-color); +} + +[data-theme="light"] .recipe-card__badge { + background: rgba(15, 23, 42, 0.08); +} + +[data-theme="light"] .recipe-card__badge--base { + background: rgba(15, 23, 42, 0.12); +} + +.recipe-card__badge--ready { + background: rgba(34, 197, 94, 0.18); + color: #4ade80; +} + +.recipe-card__badge--missing { + background: rgba(234, 179, 8, 0.2); + color: #facc15; +} + +.recipe-card__badge--empty { + background: rgba(148, 163, 184, 0.18); + color: #e2e8f0; +} + +[data-theme="light"] .recipe-card__badge--ready { + color: #157347; + background: rgba(76, 167, 120, 0.16); +} + +[data-theme="light"] .recipe-card__badge--missing { + color: #9f580a; + background: rgba(245, 199, 43, 0.22); +} + +[data-theme="light"] .recipe-card__badge--empty { + color: rgba(71, 85, 105, 0.9); + background: rgba(148, 163, 184, 0.2); +} + +.recipe-card__cta { + display: inline-flex; + align-items: center; + gap: var(--space-1); + color: var(--lora-accent); + font-size: 0.9em; + font-weight: 600; + pointer-events: none; +} + +.recipe-card__cta i { + font-size: 0.85em; + transition: transform 0.2s ease; +} + +.recipe-card:hover .recipe-card__cta i { + transform: translateX(4px); +} + +@media (max-width: 900px) { + .recipes-header { + flex-direction: column; + align-items: flex-start; + } + + .recipes-header__view-all { + align-self: flex-start; + } +} + +@media (max-width: 640px) { + .recipes-card-grid { + grid-template-columns: 1fr; + } +} + + /* Loading, error and empty states */ .recipes-loading, .recipes-error, @@ -491,4 +773,4 @@ display: flex; align-items: center; justify-content: center; -} \ No newline at end of file +} diff --git a/static/js/components/shared/RecipeTab.js b/static/js/components/shared/RecipeTab.js index ffa439c9..dc4e56ab 100644 --- a/static/js/components/shared/RecipeTab.js +++ b/static/js/components/shared/RecipeTab.js @@ -61,80 +61,178 @@ function renderRecipes(tabElement, recipes, loraName, loraHash) { return; } - // Create header with count and view all button const headerElement = document.createElement('div'); headerElement.className = 'recipes-header'; - headerElement.innerHTML = ` -

Found ${recipes.length} recipe${recipes.length > 1 ? 's' : ''} using this Lora

- - `; - - // Add click handler for "View All" button - headerElement.querySelector('.view-all-btn').addEventListener('click', () => { + + const headerText = document.createElement('div'); + headerText.className = 'recipes-header__text'; + + const eyebrow = document.createElement('span'); + eyebrow.className = 'recipes-header__eyebrow'; + eyebrow.textContent = 'Linked recipes'; + headerText.appendChild(eyebrow); + + const title = document.createElement('h3'); + title.textContent = `${recipes.length} recipe${recipes.length > 1 ? 's' : ''} using this Lora`; + headerText.appendChild(title); + + const description = document.createElement('p'); + description.className = 'recipes-header__description'; + description.textContent = loraName ? + `Discover workflows crafted for ${loraName}.` : + 'Discover workflows crafted for this model.'; + headerText.appendChild(description); + + headerElement.appendChild(headerText); + + const viewAllButton = document.createElement('button'); + viewAllButton.className = 'recipes-header__view-all'; + viewAllButton.type = 'button'; + viewAllButton.title = 'View all recipes in Recipes page'; + + const viewAllIcon = document.createElement('i'); + viewAllIcon.className = 'fas fa-external-link-alt'; + viewAllIcon.setAttribute('aria-hidden', 'true'); + + const viewAllLabel = document.createElement('span'); + viewAllLabel.textContent = 'View all recipes'; + + viewAllButton.append(viewAllIcon, viewAllLabel); + headerElement.appendChild(viewAllButton); + + viewAllButton.addEventListener('click', () => { navigateToRecipesPage(loraName, loraHash); }); - - // Create grid container for recipe cards + const cardGrid = document.createElement('div'); - cardGrid.className = 'card-grid'; + cardGrid.className = 'card-grid recipes-card-grid'; - // Create recipe cards matching the structure in recipes.html recipes.forEach(recipe => { - // Get basic info const baseModel = recipe.base_model || ''; const loras = recipe.loras || []; const lorasCount = loras.length; const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length; const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0; + const statusClass = lorasCount === 0 ? 'empty' : (allLorasAvailable ? 'ready' : 'missing'); + let statusLabel; + + if (lorasCount === 0) { + statusLabel = 'No linked LoRAs'; + } else if (allLorasAvailable) { + statusLabel = `${lorasCount} LoRA${lorasCount > 1 ? 's' : ''} ready`; + } else { + statusLabel = `Missing ${missingLorasCount} of ${lorasCount}`; + } - // Ensure file_url exists, fallback to file_path if needed const imageUrl = recipe.file_url || (recipe.file_path ? `/loras_static/root1/preview/${recipe.file_path.split('/').pop()}` : '/loras_static/images/no-preview.png'); - - // Create card element matching the structure in recipes.html - const card = document.createElement('div'); - card.className = 'model-card'; + + const card = document.createElement('article'); + card.className = 'recipe-card'; card.dataset.filePath = recipe.file_path || ''; card.dataset.title = recipe.title || ''; card.dataset.created = recipe.created_date || ''; card.dataset.id = recipe.id || ''; - - card.innerHTML = ` -
- ${recipe.title} -
- ${baseModel ? `${baseModel}` : ''} -
- -
-
- -
- `; - - // Add event listeners for action buttons - card.querySelector('.fa-copy').addEventListener('click', (e) => { - e.stopPropagation(); + + card.setAttribute('role', 'button'); + card.setAttribute('tabindex', '0'); + card.setAttribute('aria-label', recipe.title ? `View recipe ${recipe.title}` : 'View recipe details'); + + const media = document.createElement('div'); + media.className = 'recipe-card__media'; + + const image = document.createElement('img'); + image.loading = 'lazy'; + image.src = imageUrl; + image.alt = recipe.title ? `${recipe.title} preview` : 'Recipe preview'; + media.appendChild(image); + + const mediaTop = document.createElement('div'); + mediaTop.className = 'recipe-card__media-top'; + + const copyButton = document.createElement('button'); + copyButton.className = 'recipe-card__copy'; + copyButton.type = 'button'; + copyButton.title = 'Copy recipe syntax'; + copyButton.setAttribute('aria-label', 'Copy recipe syntax'); + + const copyIcon = document.createElement('i'); + copyIcon.className = 'fas fa-copy'; + copyIcon.setAttribute('aria-hidden', 'true'); + copyButton.appendChild(copyIcon); + + mediaTop.appendChild(copyButton); + media.appendChild(mediaTop); + + const body = document.createElement('div'); + body.className = 'recipe-card__body'; + + const titleElement = document.createElement('h4'); + titleElement.className = 'recipe-card__title'; + titleElement.textContent = recipe.title || 'Untitled recipe'; + titleElement.title = recipe.title || 'Untitled recipe'; + body.appendChild(titleElement); + + const meta = document.createElement('div'); + meta.className = 'recipe-card__meta'; + + if (baseModel) { + const baseBadge = document.createElement('span'); + baseBadge.className = 'recipe-card__badge recipe-card__badge--base'; + baseBadge.textContent = baseModel; + baseBadge.title = baseModel; + meta.appendChild(baseBadge); + } + + const statusBadge = document.createElement('span'); + statusBadge.className = `recipe-card__badge recipe-card__badge--${statusClass}`; + + const statusIcon = document.createElement('i'); + statusIcon.className = 'fas fa-layer-group'; + statusIcon.setAttribute('aria-hidden', 'true'); + statusBadge.appendChild(statusIcon); + + const statusText = document.createElement('span'); + statusText.textContent = statusLabel; + statusBadge.appendChild(statusText); + + statusBadge.title = getLoraStatusTitle(lorasCount, missingLorasCount); + meta.appendChild(statusBadge); + + body.appendChild(meta); + + const cta = document.createElement('div'); + cta.className = 'recipe-card__cta'; + + const ctaText = document.createElement('span'); + ctaText.textContent = 'View details'; + + const ctaIcon = document.createElement('i'); + ctaIcon.className = 'fas fa-arrow-right'; + ctaIcon.setAttribute('aria-hidden', 'true'); + + cta.append(ctaText, ctaIcon); + body.appendChild(cta); + + copyButton.addEventListener('click', (event) => { + event.stopPropagation(); copyRecipeSyntax(recipe.id); }); - - // Add click handler for the entire card + card.addEventListener('click', () => { navigateToRecipeDetails(recipe.id); }); - - // Add card to grid + + card.addEventListener('keydown', (event) => { + if (event.target !== card) return; + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + navigateToRecipeDetails(recipe.id); + } + }); + + card.append(media, body); cardGrid.appendChild(card); }); @@ -226,4 +324,4 @@ function navigateToRecipeDetails(recipeId) { // Directly navigate to recipes page window.location.href = '/loras/recipes'; -} \ No newline at end of file +}