mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
## Summary Enable `better-tailwindcss/enforce-consistent-class-order` lint rule and auto-fix all 1027 violations across 263 files. Stacked on #9427. ## Changes - **What**: Sort Tailwind classes into consistent order via `eslint --fix` - Enable `enforce-consistent-class-order` as `'error'` in eslint config - Purely cosmetic reordering — no behavioral or visual changes ## Review Focus Mechanical auto-fix PR — all changes are class reordering only. This is the largest diff but lowest risk since it changes no class names, only their order. **Stack:** #9417 → #9427 → **this PR** Fixes #9300 (partial — 3 of 3 rules) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9428-fix-enable-enforce-consistent-class-order-tailwind-lint-rule-31a6d73d3650811c9065f5178ba3e724) by [Unito](https://www.unito.io)
433 lines
9.9 KiB
Vue
433 lines
9.9 KiB
Vue
<template>
|
|
<div v-if="shouldShow" class="whats-new-popup-container left-4">
|
|
<div class="whats-new-popup" @click.stop>
|
|
<!-- Close Button -->
|
|
<Button
|
|
class="close-button absolute top-2 right-2 z-10 size-8 rounded-lg p-2 opacity-50"
|
|
:aria-label="$t('g.close')"
|
|
size="icon-sm"
|
|
variant="muted-textonly"
|
|
@click="closePopup"
|
|
>
|
|
<i class="icon-[lucide--x]" />
|
|
</Button>
|
|
|
|
<!-- Modal Body -->
|
|
<div class="modal-body flex flex-1 flex-col gap-4 px-0 pt-0 pb-2">
|
|
<!-- Release Content -->
|
|
<div
|
|
class="content-text max-h-96 overflow-y-auto"
|
|
v-html="formattedContent"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- Modal Footer -->
|
|
<div
|
|
class="modal-footer flex items-center justify-between gap-4 px-4 pb-4"
|
|
>
|
|
<a
|
|
class="learn-more-link flex items-center gap-2 py-1 text-sm font-normal"
|
|
:href="changelogUrl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
@click="closePopup"
|
|
>
|
|
<i class="icon-[lucide--external-link]"></i>
|
|
{{ $t('whatsNewPopup.learnMore') }}
|
|
</a>
|
|
<div class="footer-actions flex items-center gap-4">
|
|
<Button
|
|
class="h-8"
|
|
size="sm"
|
|
variant="muted-textonly"
|
|
@click="closePopup"
|
|
>
|
|
{{ $t('whatsNewPopup.later') }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { default as DOMPurify } from 'dompurify'
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { useExternalLink } from '@/composables/useExternalLink'
|
|
import { formatVersionAnchor } from '@/utils/formatUtil'
|
|
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
|
|
|
|
import type { ReleaseNote } from '../common/releaseService'
|
|
import { useReleaseStore } from '../common/releaseStore'
|
|
|
|
const { buildDocsUrl } = useExternalLink()
|
|
const releaseStore = useReleaseStore()
|
|
const { t } = useI18n()
|
|
|
|
// Define emits
|
|
const emit = defineEmits<{
|
|
'whats-new-dismissed': []
|
|
}>()
|
|
|
|
// Local state for dismissed status
|
|
const isDismissed = ref(false)
|
|
|
|
// Get latest release from store
|
|
const latestRelease = computed<ReleaseNote | null>(() => {
|
|
return releaseStore.recentRelease
|
|
})
|
|
|
|
// Show popup when on latest version and not dismissed
|
|
const shouldShow = computed(
|
|
() => releaseStore.shouldShowPopup && !isDismissed.value
|
|
)
|
|
|
|
// Generate changelog URL with version anchor (language-aware)
|
|
const changelogUrl = computed(() => {
|
|
const changelogBaseUrl = buildDocsUrl('/changelog', { includeLocale: true })
|
|
if (latestRelease.value?.version) {
|
|
const versionAnchor = formatVersionAnchor(latestRelease.value.version)
|
|
return `${changelogBaseUrl}#${versionAnchor}`
|
|
}
|
|
return changelogBaseUrl
|
|
})
|
|
|
|
const formattedContent = computed(() => {
|
|
if (!latestRelease.value?.content) {
|
|
return DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)
|
|
}
|
|
|
|
try {
|
|
const markdown = latestRelease.value.content
|
|
|
|
// Check if content is meaningful (not just whitespace)
|
|
const trimmedContent = markdown.trim()
|
|
if (!trimmedContent || trimmedContent.replace(/\s+/g, '') === '') {
|
|
return DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)
|
|
}
|
|
|
|
// Extract image and remaining content separately
|
|
const imageMatch = markdown.match(/!\[.*?\]\(.*?\)/)
|
|
const image = imageMatch ? imageMatch[0] : ''
|
|
|
|
// Remove image from content but keep original title
|
|
const contentWithoutImage = markdown.replace(/!\[.*?\]\(.*?\)/, '').trim()
|
|
|
|
// Reorder: image first, then original content
|
|
const reorderedContent = [image, contentWithoutImage]
|
|
.filter(Boolean)
|
|
.join('\n\n')
|
|
|
|
// renderMarkdownToHtml already sanitizes with DOMPurify, so this is safe
|
|
return renderMarkdownToHtml(reorderedContent)
|
|
} catch (error) {
|
|
console.error('Error parsing markdown:', error)
|
|
// Fallback to plain text with line breaks - sanitize the HTML we create
|
|
const fallbackContent = latestRelease.value.content.replace(/\n/g, '<br>')
|
|
return fallbackContent.trim()
|
|
? DOMPurify.sanitize(fallbackContent)
|
|
: DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)
|
|
}
|
|
})
|
|
|
|
const show = () => {
|
|
isDismissed.value = false
|
|
}
|
|
|
|
const hide = () => {
|
|
isDismissed.value = true
|
|
emit('whats-new-dismissed')
|
|
}
|
|
|
|
const closePopup = async () => {
|
|
// Mark "what's new" seen when popup is closed
|
|
if (latestRelease.value) {
|
|
await releaseStore.handleWhatsNewSeen(latestRelease.value.version)
|
|
}
|
|
hide()
|
|
}
|
|
|
|
// Initialize on mount
|
|
onMounted(async () => {
|
|
// Fetch releases if not already loaded
|
|
if (!releaseStore.releases.length) {
|
|
await releaseStore.fetchReleases()
|
|
}
|
|
})
|
|
|
|
// Expose methods for parent component and tests
|
|
defineExpose({
|
|
show,
|
|
hide,
|
|
closePopup
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Popup container - positioning handled by parent */
|
|
.whats-new-popup-container {
|
|
--whats-new-popup-bottom: 1rem;
|
|
|
|
position: absolute;
|
|
bottom: var(--whats-new-popup-bottom);
|
|
z-index: 1000;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.whats-new-popup {
|
|
background: var(--interface-menu-surface);
|
|
border-radius: 8px;
|
|
max-width: 400px;
|
|
width: 400px;
|
|
border: 1px solid var(--interface-menu-stroke);
|
|
box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.2);
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* Modal Body */
|
|
.modal-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
padding: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.modal-header {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.content-text {
|
|
color: var(--text-primary);
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
word-wrap: break-word;
|
|
padding: 0 1rem;
|
|
}
|
|
|
|
/* Style the markdown content */
|
|
/* Title */
|
|
.content-text :deep(*) {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.content-text :deep(h1) {
|
|
color: var(--text-secondary);
|
|
font-family: Inter, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
margin-top: 1rem;
|
|
margin-bottom: 8px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
/* What's new title - targets h2 or strong text after h1 */
|
|
.content-text :deep(h2),
|
|
.content-text :deep(h1 + p strong) {
|
|
color: var(--text-primary);
|
|
font-family: Inter, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
margin: 0 0 8px;
|
|
line-height: 1.429;
|
|
}
|
|
|
|
/* Regular paragraphs - short description */
|
|
.content-text :deep(p) {
|
|
color: var(--text-secondary);
|
|
font-family: Inter, sans-serif;
|
|
margin: 1rem 0;
|
|
}
|
|
|
|
/* List */
|
|
.content-text :deep(ul),
|
|
.content-text :deep(ol) {
|
|
margin-bottom: 0;
|
|
padding-left: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.content-text :deep(ul:first-child),
|
|
.content-text :deep(ol:first-child) {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.content-text :deep(ul:last-child),
|
|
.content-text :deep(ol:last-child) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.content-text :deep(li) {
|
|
margin-bottom: 6px;
|
|
position: relative;
|
|
padding-left: 18px;
|
|
color: var(--text-secondary);
|
|
font-family: Inter, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 1.2102;
|
|
}
|
|
|
|
.content-text :deep(li:last-child) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.content-text :deep(li::before) {
|
|
content: '';
|
|
position: absolute;
|
|
left: 4px;
|
|
top: 7px;
|
|
width: 6px;
|
|
height: 6px;
|
|
border: 2px solid var(--text-secondary);
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
}
|
|
|
|
.content-text :deep(li strong) {
|
|
color: var(--text-secondary);
|
|
font-family: Inter, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 1.2102;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.content-text :deep(li p) {
|
|
margin: 2px 0 0;
|
|
display: inline;
|
|
}
|
|
|
|
/* Code styling */
|
|
.content-text :deep(code) {
|
|
background-color: var(--input-surface);
|
|
border: 1px solid var(--interface-menu-stroke);
|
|
border-radius: 4px;
|
|
padding: 2px 6px;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.content-text :deep(img) {
|
|
width: 100%;
|
|
height: 200px;
|
|
margin: 0 0 16px;
|
|
object-fit: cover;
|
|
display: block;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.content-text :deep(img:first-child) {
|
|
margin: -1rem -1rem 16px;
|
|
width: calc(100% + 2rem);
|
|
border-top-left-radius: 8px;
|
|
border-top-right-radius: 8px;
|
|
border-bottom-left-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
|
|
/* Add border to content when image is present */
|
|
.content-text:has(img:first-child) {
|
|
border-left: 1px solid var(--interface-menu-stroke);
|
|
border-right: 1px solid var(--interface-menu-stroke);
|
|
border-top: 1px solid var(--interface-menu-stroke);
|
|
border-bottom-left-radius: 8px;
|
|
border-bottom-right-radius: 8px;
|
|
margin: -1px;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.content-text :deep(img + h1) {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Secondary headings */
|
|
.content-text :deep(h3) {
|
|
color: var(--text-primary);
|
|
font-family: Inter, sans-serif;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
margin: 16px 0 8px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Modal Footer */
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 16px;
|
|
border-top: none;
|
|
}
|
|
|
|
.footer-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.learn-more-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--text-secondary);
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 1.2102;
|
|
text-decoration: none;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.learn-more-link:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.learn-more-link i {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.action-secondary {
|
|
height: 32px;
|
|
padding: 4px 0;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 1.2102;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.action-secondary:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.action-primary {
|
|
height: 40px;
|
|
padding: 8px 16px;
|
|
background: var(--interface-menu-component-surface-hovered);
|
|
border-radius: 8px;
|
|
border: none;
|
|
color: var(--text-primary);
|
|
font-size: 14px;
|
|
font-weight: 400;
|
|
line-height: 1.2102;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.action-primary:hover {
|
|
background: var(--button-hover-surface);
|
|
}
|
|
</style>
|