diff --git a/src/web-ui/public/assets/assistant/claw-avatar-alt.webp b/src/web-ui/public/assets/assistant/claw-avatar-alt.webp new file mode 100644 index 0000000000..98fc137c6a Binary files /dev/null and b/src/web-ui/public/assets/assistant/claw-avatar-alt.webp differ diff --git a/src/web-ui/public/assets/assistant/claw-avatar.webp b/src/web-ui/public/assets/assistant/claw-avatar.webp new file mode 100644 index 0000000000..8253286f0a Binary files /dev/null and b/src/web-ui/public/assets/assistant/claw-avatar.webp differ diff --git a/src/web-ui/public/assets/assistant/defaults-illustration.webp b/src/web-ui/public/assets/assistant/defaults-illustration.webp new file mode 100644 index 0000000000..7e82c1a5d1 Binary files /dev/null and b/src/web-ui/public/assets/assistant/defaults-illustration.webp differ diff --git a/src/web-ui/public/assets/assistant/gallery-companion.webp b/src/web-ui/public/assets/assistant/gallery-companion.webp new file mode 100644 index 0000000000..a6784afb42 Binary files /dev/null and b/src/web-ui/public/assets/assistant/gallery-companion.webp differ diff --git a/src/web-ui/public/panda_1.png b/src/web-ui/public/panda_1.png deleted file mode 100644 index 3a8edcad88..0000000000 Binary files a/src/web-ui/public/panda_1.png and /dev/null differ diff --git a/src/web-ui/public/panda_2.png b/src/web-ui/public/panda_2.png deleted file mode 100644 index d4fbbb4d57..0000000000 Binary files a/src/web-ui/public/panda_2.png and /dev/null differ diff --git a/src/web-ui/public/panda_wink.png b/src/web-ui/public/panda_wink.png deleted file mode 100644 index 2f5407862e..0000000000 Binary files a/src/web-ui/public/panda_wink.png and /dev/null differ diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss b/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss index 4796eb154a..e7e259df31 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.scss @@ -1,109 +1,166 @@ /** * About dialog styles. - * Uses component library Modal for overlay and shell; only content layout here. + * Modal owns the outer surface; this file owns the product, metadata, and CTA layout. */ -@use '../../../component-library/styles/tokens' as *; +.bitfun-about-dialog-overlay { + > .modal.modal--xxlarge[data-testid='about-dialog-modal'] { + width: min(960px, 100%); + max-width: min(960px, 100%); + } -// ==================== App updates (desktop) ==================== + .modal__header { + min-height: 48px; + padding: 8px 24px; + } -.bitfun-about-dialog__update-card { - margin-bottom: 16px; - padding: 12px 14px; - border: 1px dashed var(--bf-appearance-token-border-subtle); - border-radius: 8px; + .modal__title { + color: var(--bf-appearance-token-color-text-primary); + font-size: var(--bf-appearance-token-font-size-lg); + font-weight: var(--bf-appearance-token-font-weight-semibold); + } +} + +.bitfun-about-dialog__modal-content { + overflow: hidden; +} + +.bitfun-about-dialog__content { display: flex; flex-direction: column; - gap: 12px; + height: min(590px, calc(100vh - 112px)); + min-height: min(540px, calc(100vh - 112px)); + overflow: hidden; +} + +.bitfun-about-dialog__body { + display: grid; + grid-template-columns: minmax(330px, 0.38fr) minmax(0, 0.62fr); + flex: 1; + min-height: 0; } -.bitfun-about-dialog__update-card-top { +.bitfun-about-dialog__brand { + position: relative; display: flex; + flex-direction: column; align-items: flex-start; - justify-content: space-between; - gap: 14px; + justify-content: flex-start; + min-width: 0; + padding: 56px 42px 40px 56px; + overflow-y: auto; + + &::after { + position: absolute; + top: 36px; + right: 0; + bottom: 36px; + width: 1px; + background: var(--bf-appearance-token-border-subtle); + content: ''; + } } -.bitfun-about-dialog__update-card-main { - flex: 1; +.bitfun-about-dialog__brand-copy { min-width: 0; } -.bitfun-about-dialog__update-card-head { - display: flex; - align-items: flex-start; - gap: 12px; +.bitfun-about-dialog__title { + margin: 0; + color: var(--bf-appearance-token-color-text-primary); + font-family: var(--bf-appearance-token-font-family-sans); + font-size: clamp(48px, 5vw, 58px); + font-weight: var(--bf-appearance-token-font-weight-normal); + line-height: 0.98; + letter-spacing: -0.05em; } -.bitfun-about-dialog__update-card-icon { - flex-shrink: 0; - width: 36px; - height: 36px; +.bitfun-about-dialog__tagline { + margin: 16px 0 0; + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-base); + line-height: 1.55; +} + +.bitfun-about-dialog__release { + width: 100%; + margin-top: 34px; +} + +.bitfun-about-dialog__release-summary { display: flex; + flex-wrap: wrap; align-items: center; - justify-content: center; - border-radius: 8px; - background: var(--bf-appearance-token-element-bg-subtle); - color: var(--bf-appearance-token-color-accent-400); + gap: 10px; } -.bitfun-about-dialog__update-card-meta { - min-width: 0; - flex: 1; +.bitfun-about-dialog__release-version { + color: var(--bf-appearance-token-color-text-primary); + font-size: var(--bf-appearance-token-font-size-xl); + font-variant-numeric: tabular-nums; + line-height: 1.2; } -.bitfun-about-dialog__update-card-title { - font-size: 12px; - font-weight: 600; - color: var(--bf-appearance-token-color-text-primary); - letter-spacing: 0.02em; +.bitfun-about-dialog__channel-badge { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 2px 10px; + border: 1px solid var(--bf-appearance-token-border-subtle); + border-radius: var(--bf-appearance-token-size-radius-sm); + background: var(--bf-appearance-token-element-bg-subtle); + color: var(--bf-appearance-token-color-text-secondary); + font-size: var(--bf-appearance-token-font-size-sm); + line-height: 1; + white-space: nowrap; } -.bitfun-about-dialog__update-card-hint { - margin: 4px 0 0; - font-size: 11px; - line-height: 1.45; - color: var(--bf-appearance-token-color-text-muted); +.bitfun-about-dialog__update-card { + width: 100%; + margin-top: 20px; } .bitfun-about-dialog__update-card-actions { display: flex; - flex: 0 0 auto; align-items: center; - justify-content: flex-end; - gap: 8px; } -.bitfun-about-dialog__update-card-feedback { - margin-top: 10px; +.bitfun-about-dialog__update-trigger, +.bitfun-about-dialog__update-status-pill { + min-width: 0; + border: 1px solid var(--bf-appearance-token-border-base); + border-radius: 999px; + background: transparent; + color: var(--bf-appearance-token-color-text-secondary); } -.bitfun-about-dialog__update-btn-icon { - flex-shrink: 0; - opacity: 0.9; +.bitfun-about-dialog__update-status-pill svg { + color: var(--bf-appearance-token-color-success); } -.bitfun-about-dialog__update-status { - display: flex; - align-items: flex-start; - gap: 8px; - font-size: 12px; - line-height: 1.45; - color: var(--bf-appearance-token-color-text-muted); +.bitfun-about-dialog__update-btn-icon { + flex-shrink: 0; + opacity: 0.85; - svg { - flex-shrink: 0; - margin-top: 2px; - color: var(--bf-appearance-token-color-success); + &--busy { + animation: about-update-spin 0.9s linear infinite; } +} - &--success { - color: var(--bf-appearance-token-color-text-secondary); +@keyframes about-update-spin { + to { + transform: rotate(360deg); } } +.bitfun-about-dialog__update-feedback { + width: 100%; + margin-top: 10px; +} + .bitfun-about-dialog__update-alert { width: 100%; + text-align: left; } .bitfun-about-dialog__download-status { @@ -125,18 +182,19 @@ height: 100%; border-radius: inherit; background: var(--bf-appearance-token-color-accent-500); - transition: width 0.2s ease; -} + transition: width var(--bf-appearance-token-motion-fast) var(--bf-appearance-token-easing-standard); -.bitfun-about-dialog__download-fill--indeterminate { - width: 38%; - animation: about-update-progress-indeterminate 1.1s ease-in-out infinite; + &--indeterminate { + width: 38%; + animation: about-update-progress-indeterminate 1.1s ease-in-out infinite; + } } @keyframes about-update-progress-indeterminate { 0% { transform: translateX(-105%); } + 100% { transform: translateX(265%); } @@ -147,345 +205,474 @@ align-items: center; justify-content: space-between; gap: 12px; - font-size: 11px; - line-height: 1.4; color: var(--bf-appearance-token-color-text-secondary); + font-size: var(--bf-appearance-token-font-size-sm); + line-height: 1.4; } .bitfun-about-dialog__download-hint { margin: 0; - font-size: 11px; - line-height: 1.45; color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-sm); + line-height: 1.45; } .bitfun-about-dialog__update-installed { display: flex; flex-wrap: wrap; align-items: center; - justify-content: space-between; gap: 10px; } -// ==================== Content area (inside Modal) ==================== - -.bitfun-about-dialog__content { - display: flex; - flex-direction: column; - min-height: 400px; - max-height: min(680px, calc(100vh - 48px)); - overflow: hidden; -} - -// ==================== Hero section ==================== - -.bitfun-about-dialog__hero { - flex-shrink: 0; +.bitfun-about-dialog__update-status { display: flex; - flex-direction: column; - align-items: center; - padding: 32px 32px 24px; - text-align: center; - position: relative; - overflow: hidden; -} - -.bitfun-about-dialog__title { - font-family: var(--bf-appearance-token-font-family-sans); - font-size: 28px; - font-weight: 800; - margin: 0 0 10px 0; - letter-spacing: -0.03em; - color: var(--bf-appearance-token-color-text-primary); - position: relative; - z-index: 1; - animation: about-title-enter 0.5s ease-out 0.1s both; - line-height: 1.2; -} - -@keyframes about-title-enter { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.bitfun-about-dialog__version-badge { - display: inline-flex; - align-items: center; + align-items: flex-start; + gap: 8px; color: var(--bf-appearance-token-color-text-muted); - font-size: 11px; - font-weight: 400; - font-family: 'Consolas', 'Monaco', monospace; - letter-spacing: 0.02em; - position: relative; - z-index: 1; - animation: about-badge-enter 0.5s ease-out 0.15s both; -} + font-size: var(--bf-appearance-token-font-size-sm); + line-height: 1.45; -@keyframes about-badge-enter { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); + svg { + flex-shrink: 0; + margin-top: 2px; } -} -// Decorative divider -.bitfun-about-dialog__divider { - width: 60px; - height: 1px; - background: var(--bf-appearance-token-border-subtle); - margin-top: 18px; - position: relative; - z-index: 1; - animation: about-divider-enter 0.6s ease-out 0.2s both; -} - -@keyframes about-divider-enter { - from { - opacity: 0; - width: 0; - } - to { - opacity: 1; - width: 60px; + &--success svg { + color: var(--bf-appearance-token-color-success); } } -// Decorative dots -.bitfun-about-dialog__dots { - display: flex; - gap: 6px; - margin-top: 12px; - position: relative; - z-index: 1; - animation: about-dots-enter 0.6s ease-out 0.25s both; +.bitfun-about-dialog__dot-matrix { + position: absolute; + bottom: 20px; + left: 14px; + display: grid; + grid-template-columns: repeat(13, 2px); + grid-auto-rows: 2px; + gap: 14px; + width: max-content; span { - width: 3px; - height: 3px; - background: var(--bf-appearance-token-color-text-muted); + display: block; + width: 2px; + height: 2px; border-radius: 50%; - opacity: 0.45; + background: var(--bf-appearance-token-color-text-muted); } } -@keyframes about-dots-enter { - from { opacity: 0; } - to { opacity: 1; } -} - -// ==================== Scrollable area ==================== - -.bitfun-about-dialog__scrollable { - flex: 1; +.bitfun-about-dialog__metadata { + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; min-height: 0; + padding: 22px 40px 22px 38px; overflow-y: auto; - overflow-x: hidden; - padding: 0 24px; -} - -// ==================== Info section ==================== - -.bitfun-about-dialog__info-section { - margin-bottom: 16px; - animation: about-section-enter 0.4s ease-out 0.2s both; -} - -@keyframes about-section-enter { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.bitfun-about-dialog__info-card { - padding: 10px 14px; - border: 1px dashed var(--bf-appearance-token-border-subtle); - border-radius: 8px; + overscroll-behavior: contain; } .bitfun-about-dialog__info-row { - display: flex; + display: grid; + grid-template-columns: 120px minmax(0, 1fr); align-items: center; - justify-content: space-between; - gap: 12px; - padding: 5px 0; - min-height: 28px; - - &:first-child { - padding-top: 0; - } - + gap: 18px; + min-width: 0; + min-height: 70px; + padding: 12px 0; + border-bottom: 1px solid var(--bf-appearance-token-border-subtle); + &:last-child { - padding-bottom: 0; + border-bottom: 0; } } .bitfun-about-dialog__info-label { - flex-shrink: 0; - font-size: 12px; + display: flex; + align-items: center; + gap: 13px; + min-width: 0; color: var(--bf-appearance-token-color-text-secondary); - font-weight: 500; + font-size: var(--bf-appearance-token-font-size-base); + font-weight: var(--bf-appearance-token-font-weight-medium); + line-height: 1.45; + + svg { + flex-shrink: 0; + color: var(--bf-appearance-token-color-text-muted); + } } .bitfun-about-dialog__info-value { - font-size: 12px; + min-width: 0; color: var(--bf-appearance-token-color-text-primary); - font-weight: 400; + font-size: var(--bf-appearance-token-font-size-lg); + font-weight: var(--bf-appearance-token-font-weight-normal); + line-height: 1.45; text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - + overflow-wrap: anywhere; + + &--version { + font-size: var(--bf-appearance-token-font-size-xl); + font-variant-numeric: tabular-nums; + } + &--mono { - font-family: 'Consolas', 'Monaco', monospace; - font-size: 11px; - font-weight: 500; + font-family: var(--bf-appearance-token-font-family-mono); + font-variant-numeric: tabular-nums; + } + + &--branch { + font-size: var(--bf-appearance-token-font-size-base); } } .bitfun-about-dialog__info-value-group { display: flex; align-items: center; - gap: 6px; + justify-content: flex-end; + gap: 8px; min-width: 0; } .bitfun-about-dialog__copy-btn { - flex-shrink: 0; - background: transparent; + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; border: 1px solid transparent; + border-radius: var(--bf-appearance-token-size-radius-sm); + background: transparent; color: var(--bf-appearance-token-color-text-muted); cursor: pointer; - padding: 4px; - border-radius: 4px; + outline: none; transition: - transform 150ms cubic-bezier(0.23, 1, 0.32, 1), - background-color 150ms ease, - border-color 150ms ease, - color 150ms ease; - display: flex; - align-items: center; - + background-color var(--bf-appearance-token-motion-fast) var(--bf-appearance-token-easing-standard), + border-color var(--bf-appearance-token-motion-fast) var(--bf-appearance-token-easing-standard), + color var(--bf-appearance-token-motion-fast) var(--bf-appearance-token-easing-standard), + transform var(--bf-appearance-token-motion-fast) var(--bf-appearance-token-easing-standard); + &:hover { - background: var(--bf-appearance-token-element-bg-subtle); border-color: var(--bf-appearance-token-border-subtle); - color: var(--bf-appearance-token-color-accent-400); + background: var(--bf-appearance-token-element-bg-subtle); + color: var(--bf-appearance-token-color-text-primary); } - + &:active { - transform: scale(0.9); + transform: scale(0.94); + } + + &:focus-visible { + outline: 2px solid var(--bf-appearance-token-color-accent-500); + outline-offset: 1px; } } -// ==================== Footer ==================== +.bitfun-about-dialog__star-callout { + display: grid; + grid-template-columns: minmax(32px, 48px) 40px minmax(0, max-content) minmax(72px, 1fr) auto; + flex: 0 0 auto; + align-items: center; + gap: 12px; + min-height: 132px; + padding: 20px 42px 18px; +} -.bitfun-about-dialog__footer { - flex-shrink: 0; - padding: 16px 24px 20px; - text-align: center; - position: relative; - animation: about-section-enter 0.4s ease-out 0.3s both; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 24px; - right: 24px; - height: 1px; - background: linear-gradient(90deg, transparent 0%, var(--bf-appearance-token-border-subtle) 50%, transparent 100%); +.bitfun-about-dialog__star-icon { + color: var(--bf-appearance-token-color-text-primary); +} + +.bitfun-about-dialog__star-copy { + min-width: 0; +} + +.bitfun-about-dialog__star-title { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + color: var(--bf-appearance-token-color-text-primary); + font-size: var(--bf-appearance-token-font-size-base); + font-weight: var(--bf-appearance-token-font-weight-medium); + line-height: 1.4; + + svg { + flex: 0 0 auto; } } -.bitfun-about-dialog__license { - font-size: 11px; - color: var(--bf-appearance-token-color-text-secondary); - line-height: 1.5; - margin: 0 0 6px 0; +.bitfun-about-dialog__star-description { + margin: 4px 0 0; + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-sm); + line-height: 1.45; +} + +.bitfun-about-dialog__star-rule { + display: block; + width: 100%; + height: 1px; + background: var(--bf-appearance-token-border-strong); +} + +.bitfun-about-dialog__star-button { + min-width: 132px; + border-radius: 999px; + white-space: nowrap; + + svg { + transition: transform var(--bf-appearance-token-motion-fast) var(--bf-appearance-token-easing-standard); + } + + &:hover svg { + transform: translateX(2px); + } +} + +.bitfun-about-dialog__footer { + display: flex; + flex: 0 0 auto; + align-items: center; + min-height: 58px; + padding: 14px 42px; + border-top: 1px solid var(--bf-appearance-token-border-subtle); } .bitfun-about-dialog__copyright { - font-size: 10px; - color: var(--bf-appearance-token-color-text-muted); - opacity: 0.6; margin: 0; - letter-spacing: 0.02em; + color: var(--bf-appearance-token-color-text-muted); + font-size: var(--bf-appearance-token-font-size-sm); + line-height: 1.5; +} + +@media (max-width: 900px) { + .bitfun-about-dialog-overlay { + > .modal.modal--xxlarge[data-testid='about-dialog-modal'] { + width: 100%; + max-width: 100%; + } + } + + .bitfun-about-dialog__body { + grid-template-columns: minmax(300px, 0.37fr) minmax(0, 0.63fr); + } + + .bitfun-about-dialog__brand { + padding-right: 34px; + padding-left: 44px; + } + + .bitfun-about-dialog__metadata { + padding-inline: 32px; + } + + .bitfun-about-dialog__star-callout { + grid-template-columns: minmax(28px, 44px) 36px minmax(0, max-content) minmax(52px, 1fr) auto; + padding-inline: 34px; + } } -// ==================== Responsive layout ==================== +@media (max-width: 760px) { + .bitfun-about-dialog-overlay { + .modal__header { + min-height: 48px; + padding: 8px 18px; + } + } + + .bitfun-about-dialog__modal-content { + overflow-y: auto; + } -@media (max-width: 580px) { .bitfun-about-dialog__content { - min-height: 360px; - max-height: calc(100vh - 32px); + height: auto; + min-height: 0; + overflow: visible; + } + + .bitfun-about-dialog__body { + display: flex; + flex-direction: column; + } + + .bitfun-about-dialog__brand { + justify-content: flex-start; + padding: 30px 28px; + overflow: visible; + border-bottom: 1px solid var(--bf-appearance-token-border-subtle); + + &::after { + display: none; + } } - .bitfun-about-dialog__hero { - padding: 24px 20px 20px; + .bitfun-about-dialog__dot-matrix { + display: none; } .bitfun-about-dialog__title { - font-size: 20px; + font-size: 46px; + } + + .bitfun-about-dialog__release { + margin-top: 26px; + } + + .bitfun-about-dialog__metadata { + justify-content: flex-start; + padding: 8px 28px 18px; + overflow: visible; + } + + .bitfun-about-dialog__info-row { + min-height: 66px; + } + + .bitfun-about-dialog__star-callout { + grid-template-columns: 34px minmax(0, 1fr) auto; + min-height: 120px; + padding: 18px 28px; + } + + .bitfun-about-dialog__star-rule, + .bitfun-about-dialog__star-rule--leading { + display: none; } - .bitfun-about-dialog__scrollable { - padding: 0 16px; + .bitfun-about-dialog__star-button { + min-width: 132px; } .bitfun-about-dialog__footer { - padding: 14px 16px 16px; + padding-inline: 28px; + } +} + +@media (max-width: 520px) { + .bitfun-about-dialog-overlay { + .modal__header { + min-height: 48px; + padding: 8px 14px; + } - &::before { - left: 16px; - right: 16px; + .modal__title { + font-size: var(--bf-appearance-token-font-size-base); } } + + .bitfun-about-dialog__brand { + padding: 26px 20px; + } + + .bitfun-about-dialog__title { + font-size: 40px; + } + + .bitfun-about-dialog__tagline { + margin-top: 10px; + font-size: var(--bf-appearance-token-font-size-sm); + } + + .bitfun-about-dialog__metadata { + padding-inline: 20px; + } + + .bitfun-about-dialog__info-row { + grid-template-columns: 100px minmax(0, 1fr); + gap: 10px; + } + + .bitfun-about-dialog__info-label { + gap: 8px; + font-size: var(--bf-appearance-token-font-size-sm); + + svg { + width: 18px; + height: 18px; + } + } + + .bitfun-about-dialog__info-value { + font-size: var(--bf-appearance-token-font-size-base); + + &--version { + font-size: var(--bf-appearance-token-font-size-lg); + } + + &--branch { + font-size: var(--bf-appearance-token-font-size-sm); + } + } + + .bitfun-about-dialog__star-callout { + grid-template-columns: 1fr; + gap: 14px; + padding: 22px 20px; + } + + .bitfun-about-dialog__star-icon { + display: none; + } + + .bitfun-about-dialog__star-button { + width: 100%; + } + + .bitfun-about-dialog__footer { + padding-inline: 20px; + } } -@media (max-height: 500px) { +@media (max-height: 700px) and (min-width: 761px) { .bitfun-about-dialog__content { - min-height: 320px; + height: min(540px, calc(100vh - 96px)); + min-height: min(500px, calc(100vh - 96px)); } - .bitfun-about-dialog__hero { - padding: 20px 24px 16px; + .bitfun-about-dialog__brand { + padding-block: 28px; } .bitfun-about-dialog__title { - font-size: 20px; - margin-bottom: 8px; + font-size: 48px; + } + + .bitfun-about-dialog__release { + margin-top: 24px; } - .bitfun-about-dialog__divider { - margin-top: 12px; + .bitfun-about-dialog__metadata { + padding-block: 14px; } - .bitfun-about-dialog__dots { - margin-top: 8px; + .bitfun-about-dialog__info-row { + min-height: 62px; + padding-block: 9px; + } + + .bitfun-about-dialog__star-callout { + min-height: 104px; + padding-block: 14px; } .bitfun-about-dialog__footer { - padding: 12px 24px 14px; + min-height: 52px; + padding-block: 10px; } } @media (prefers-reduced-motion: reduce) { + .bitfun-about-dialog__update-btn-icon--busy, .bitfun-about-dialog__download-fill--indeterminate { animation: none; transform: none; } + + .bitfun-about-dialog__copy-btn:active, + .bitfun-about-dialog__star-button:hover svg { + transform: none; + } } diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx index 3d61323222..f54eb7330f 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx @@ -1,17 +1,30 @@ /** * About dialog component. - * Shows app version and license info. - * Uses component library Modal. + * Shows product identity, build metadata, license, app-update status, and the + * persistent GitHub repository entry point. */ import React, { useCallback, useEffect, useState } from 'react'; import { useI18n } from '@/infrastructure/i18n'; -import { Tooltip, Modal, Button, Alert } from '@/component-library'; -import { Copy, Check, Download, CheckCircle2 } from 'lucide-react'; +import { Alert, Button, Modal, Tooltip } from '@/component-library'; import { - getAboutInfo, + ArrowRight, + CalendarDays, + Check, + CheckCircle2, + Code2, + Copy, + GitBranch, + RefreshCw, + ShieldCheck, + Sparkle, + Star, + Tag, +} from 'lucide-react'; +import { + formatBuildDate, formatDisplayedVersion, - formatBuildDate + getAboutInfo, } from '@/shared/utils/version'; import { createLogger } from '@/shared/utils/logger'; import { systemAPI } from '@/infrastructure/api'; @@ -20,9 +33,23 @@ import { canCheckForAppUpdates, isTauriRuntime } from '@/infrastructure/update/t import { UpdateAvailableDialog } from '@/infrastructure/update/UpdateAvailableDialog'; import { useUpdateInstallStore } from '@/infrastructure/update/updateInstallStore'; import { formatUpdateInstallError } from '@/infrastructure/update/updateErrorMessage'; +import { GITHUB_STAR_URL } from '../NavPanel/components/githubStarCtaStorage'; import './AboutDialog.scss'; const log = createLogger('AboutDialog'); +const ABOUT_DOT_MATRIX_COLUMNS = 13; +const ABOUT_DOT_MATRIX_ROWS = 7; +const ABOUT_DOT_MATRIX = Array.from( + { length: ABOUT_DOT_MATRIX_COLUMNS * ABOUT_DOT_MATRIX_ROWS }, + (_, index) => { + const column = index % ABOUT_DOT_MATRIX_COLUMNS; + const row = Math.floor(index / ABOUT_DOT_MATRIX_COLUMNS); + return { + id: index, + opacity: Math.max(0.12, 0.44 - column * 0.017 - row * 0.022), + }; + }, +); interface AboutDialogProps { /** Whether visible */ @@ -33,7 +60,7 @@ interface AboutDialogProps { export const AboutDialog: React.FC = ({ isOpen, - onClose + onClose, }) => { const { t } = useI18n('common'); const [copiedItem, setCopiedItem] = useState(null); @@ -56,12 +83,28 @@ export const AboutDialog: React.FC = ({ version, nativeVersion, nativeRuntime, - import.meta.env.DEV + import.meta.env.DEV, ); const updateProgressPercent = updateProgress.total != null && updateProgress.total > 0 ? Math.min(100, Math.round((updateProgress.downloaded / updateProgress.total) * 100)) : null; + const licenseName = license.type === 'MIT' ? 'MIT License' : license.type; + const licenseCopyright = license.text?.startsWith(`${licenseName} - `) + ? license.text.slice(`${licenseName} - `.length) + : license.text; + const legalCopyright = licenseCopyright + ? `${licenseCopyright.replace(/\.$/, '')}. ${t('about.allRightsReserved')}` + : t('about.copyright'); + + let releaseLabel = t('about.stableBuild'); + if (displayedVersion.endsWith('-dev')) { + releaseLabel = t('about.developmentBuild'); + } else if (version.releaseChannel === 'beta') { + releaseLabel = t('about.betaBuild'); + } else if (version.releaseChannel === 'nightly') { + releaseLabel = t('about.nightlyBuild'); + } useEffect(() => { if (isOpen) { @@ -86,9 +129,8 @@ export const AboutDialog: React.FC = ({ }, [isOpen, nativeRuntime]); const handleCheckForUpdates = useCallback(async () => { - if (!canCheckForAppUpdates()) { - return; - } + if (!canCheckForAppUpdates()) return; + setManualCheckStatus('idle'); setManualCheckErrorMessage(null); setManualCheckBusy(true); @@ -100,16 +142,22 @@ export const AboutDialog: React.FC = ({ setManualData(res); setManualOpen(true); } - } catch (e) { - log.error('check_for_updates failed', e); - const msg = e instanceof Error ? e.message : String(e); - setManualCheckErrorMessage(formatUpdateInstallError(msg, t)); + } catch (error) { + log.error('check_for_updates failed', error); + const message = error instanceof Error ? error.message : String(error); + setManualCheckErrorMessage(formatUpdateInstallError(message, t)); setManualCheckStatus('error'); } finally { setManualCheckBusy(false); } }, [t]); + const handleGithubStar = useCallback(() => { + systemAPI.openExternal(GITHUB_STAR_URL).catch(error => { + log.error('Failed to open the GitHub repository', { url: GITHUB_STAR_URL, error }); + }); + }, []); + const onManualLater = useCallback(() => { setManualOpen(false); setManualData(null); @@ -124,9 +172,9 @@ export const AboutDialog: React.FC = ({ const onRestart = useCallback(async () => { try { await systemAPI.restartApp(); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - useUpdateInstallStore.setState({ status: 'error', error: msg }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + useUpdateInstallStore.setState({ status: 'error', error: message }); } }, []); @@ -134,212 +182,331 @@ export const AboutDialog: React.FC = ({ try { await navigator.clipboard.writeText(text); setCopiedItem(itemId); - setTimeout(() => setCopiedItem(null), 2000); - } catch (err) { - log.error('Failed to copy to clipboard', err); + window.setTimeout(() => setCopiedItem(null), 2000); + } catch (error) { + log.error('Failed to copy to clipboard', error); } }; + const updateState = `${manualCheckBusy ? 'checking' : ''} ${manualCheckStatus} ${updateStatus}`.trim(); + const updateBusy = manualCheckBusy || updateStatus === 'downloading' || updateStatus === 'installed'; + return ( <> - -
- {/* Hero section - product info */} -
-

{version.name}

-
- {t('about.version', { version: displayedVersion })} -
-
-
- - - -
-
- - {/* Scrollable area */} -
- {updateChecksAvailable ? ( -
+
+
-
-
-
-
- -
-
-
- {t('about.updateSectionTitle')} -
-

- {t('about.updateSectionHint')} -

-
-
-
- {manualCheckStatus === 'latest' ? ( -
- - {t('update.noUpdate')} -
- ) : null} - {manualCheckStatus === 'error' && manualCheckErrorMessage ? ( - - ) : null} -
-
-
- + {releaseLabel} +
-
- {updateStatus === 'downloading' ? ( -
+ + {updateChecksAvailable ? (
-
-
- {t('update.backgroundDownloading')} - - {updateProgressPercent != null - ? t('update.progressPercent', { percent: String(updateProgressPercent) }) - : t('update.progressUnknown')} - -
-

- {t('update.backgroundDownloadHint')} -

-
- ) : null} - {updateStatus === 'installed' ? ( -
-
- - {t('update.installedMessage')} + data-bf-part="updateActions" + > + {manualCheckStatus === 'latest' && updateStatus === 'idle' ? ( + + ) : ( + + )} +
+ +
+ {manualCheckStatus === 'error' && manualCheckErrorMessage ? ( + + ) : null} + {updateStatus === 'downloading' ? ( +
+
+
+
+
+ {t('update.backgroundDownloading')} + + {updateProgressPercent != null + ? t('update.progressPercent', { percent: String(updateProgressPercent) }) + : t('update.progressUnknown')} + +
+

+ {t('update.backgroundDownloadHint')} +

+
+ ) : null} + {updateStatus === 'installed' ? ( +
+
+
+ +
+ ) : null} + {updateStatus === 'error' && updateError ? ( + + ) : null} +
- + ) : null} +
+ + +
+ +
+
+
+
- ) : null} - {updateStatus === 'error' && updateError ? ( - - ) : null} -
- ) : !nativeRuntime ? ( -

{t('update.desktopOnly')}

- ) : null} -
-
+ + {displayedVersion} + +
+
- {t('about.buildDate')} +
+
{formatBuildDate(version.buildDate)}
- {version.gitCommit && ( -
- {t('about.commit')} -
- - {version.gitCommit} - +
+
+
+
+ + {version.gitCommit ?? t('about.notAvailable')} + + {version.gitCommit ? ( -
+ ) : null}
- )} +
- {version.gitBranch && ( -
- {t('about.branch')} - {version.gitBranch} +
+
+
- )} -
+ + {version.gitBranch ?? t('about.notAvailable')} + +
+ +
+
+
+ + {licenseName} + +
+
-
- {/* Footer */} -
-

{license.text}

-

- {t('about.copyright')} -

+
+
+ +
+

+ {legalCopyright} +

+
-
- + { container.remove(); }); - it('renders a selected built-in preset as a stable circular identity', () => { + it('maps a retired SVG preset id to the new Claw artwork', () => { act(() => { root.render(); }); const avatar = container.querySelector('[data-bf-component="assistant-avatar"]'); - expect(avatar?.getAttribute('data-bf-preset')).toBe('orbit-nova'); - expect(avatar?.getAttribute('data-bf-family')).toBe('orbit'); + const image = container.querySelector('.assistant-avatar__image') as HTMLImageElement; + expect(avatar?.getAttribute('data-bf-preset')).toBe('claw-orbit'); + expect(avatar?.getAttribute('data-bf-family')).toBe('clawOrbit'); expect(avatar?.getAttribute('style')).toContain('28px'); + expect(image.getAttribute('src')).toBe('/assets/assistant/claw-avatar-alt.webp'); }); it('keeps the legacy emoji when a preset id is missing or unknown', () => { @@ -44,10 +46,27 @@ describe('AssistantAvatar', () => { expect(avatar?.textContent).toContain('🧭'); }); - it('uses a deterministic built-in fallback when no identity marker exists', () => { + it('uses a deterministic new Claw fallback when no identity marker exists', () => { expect(resolveAssistantAvatarPreset(undefined, 'assistant-1').id) .toBe(resolveAssistantAvatarPreset(undefined, 'assistant-1').id); - expect(resolveAssistantAvatarPreset(undefined, 'assistant-1').id) - .not.toBe(''); + expect(resolveAssistantAvatarPreset(undefined).id).toBe('claw'); + }); + + it('renders the new default avatar without discarding the accessible identity', () => { + act(() => { + root.render( + , + ); + }); + + const avatar = container.querySelector('[data-bf-component="assistant-avatar"]'); + const image = container.querySelector('.assistant-avatar__image') as HTMLImageElement; + expect(avatar?.getAttribute('data-bf-family')).toBe('claw'); + expect(avatar?.getAttribute('data-bf-preset')).toBe('claw'); + expect(avatar?.getAttribute('aria-label')).toBe('Claw avatar'); + expect(image.getAttribute('src')).toBe('/assets/assistant/claw-avatar.webp'); }); }); diff --git a/src/web-ui/src/app/components/AssistantAvatar/AssistantAvatar.tsx b/src/web-ui/src/app/components/AssistantAvatar/AssistantAvatar.tsx index 5b53ce3404..a9be09cf5a 100644 --- a/src/web-ui/src/app/components/AssistantAvatar/AssistantAvatar.tsx +++ b/src/web-ui/src/app/components/AssistantAvatar/AssistantAvatar.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { getAssistantAvatarPreset, resolveAssistantAvatarPreset, - type AssistantAvatarFamily, } from './assistantAvatarPresets'; import { firstAvatarGrapheme } from './assistantAvatarValue'; import './AssistantAvatar.scss'; @@ -21,60 +20,6 @@ export interface AssistantAvatarProps { className?: string; } -const PresetArtwork: React.FC<{ - family: AssistantAvatarFamily; - variant: number; -}> = ({ family, variant }) => { - if (family === 'signal') { - return ( - - ); - } - - if (family === 'orbit') { - return ( - - ); - } - - if (family === 'mosaic') { - return ( - - ); - } - - return ( - - ); -}; - const AssistantAvatar: React.FC = ({ presetId, emoji, @@ -88,11 +33,11 @@ const AssistantAvatar: React.FC = ({ }) => { const displayedEmoji = firstAvatarGrapheme(emoji ?? ''); const explicitPreset = getAssistantAvatarPreset(presetId); - const preset = explicitPreset ?? resolveAssistantAvatarPreset(undefined, stableKey || name); + const preset = explicitPreset ?? resolveAssistantAvatarPreset(undefined, stableKey); const usesPreset = Boolean(explicitPreset) || !displayedEmoji; const classes = [ 'assistant-avatar', - usesPreset ? 'is-preset' : 'is-emoji', + usesPreset ? 'is-image' : 'is-emoji', active && 'is-active', status !== 'idle' && `is-${status}`, className, @@ -106,7 +51,6 @@ const AssistantAvatar: React.FC = ({ data-bf-part="root" data-bf-family={usesPreset ? preset.family : 'emoji'} data-bf-preset={usesPreset ? preset.id : undefined} - data-bf-palette={usesPreset ? preset.palette : undefined} data-bf-state={[active && 'active', status !== 'idle' && status].filter(Boolean).join(' ') || undefined} style={{ '--assistant-avatar-size': `${size}px` } as React.CSSProperties} role={decorative ? undefined : 'img'} @@ -115,7 +59,13 @@ const AssistantAvatar: React.FC = ({ >