Spaces:
Running
Running
| <script lang="ts"> | |
| /** | |
| * ScrollyTextBox - Unified text box component for scrollytelling sections | |
| * | |
| * Used in both photo scrolly and data visualization scrolly for consistent styling. | |
| * Features active state transitions, responsive sizing, and slot-based content. | |
| */ | |
| import type { Snippet } from 'svelte'; | |
| interface SourceLink { | |
| text: string; | |
| url: string; | |
| } | |
| interface Props { | |
| title?: string; | |
| image?: string | null; | |
| bgColor?: string | null; | |
| active?: boolean; | |
| variant?: 'light' | 'dark'; | |
| maxWidth?: 'narrow' | 'wide'; | |
| source?: SourceLink | null; | |
| imageCredit?: string | null; | |
| children?: Snippet; | |
| } | |
| let { | |
| title = '', | |
| image = null, | |
| bgColor = null, | |
| active = true, | |
| variant = 'light', | |
| maxWidth = 'narrow', | |
| source = null, | |
| imageCredit = null, | |
| children | |
| }: Props = $props(); | |
| </script> | |
| <div | |
| class="scrolly-text-box" | |
| class:active | |
| class:dark={variant === 'dark'} | |
| class:wide={maxWidth === 'wide'} | |
| class:has-image={image} | |
| class:custom-bg={bgColor} | |
| style:--custom-bg={bgColor} | |
| > | |
| {#if image} | |
| <img src={image} alt="" class="box-image" /> | |
| {/if} | |
| {#if title} | |
| <h2 class="box-title">{title}</h2> | |
| {/if} | |
| <div class="box-content"> | |
| {@render children?.()} | |
| </div> | |
| {#if imageCredit || source} | |
| <div class="box-footer"> | |
| {#if imageCredit} | |
| <span class="image-credit">Photo: {imageCredit}</span> | |
| {/if} | |
| {#if imageCredit && source} | |
| <span class="footer-divider">|</span> | |
| {/if} | |
| {#if source} | |
| <a href={source.url} target="_blank" rel="noopener noreferrer" class="source-link"> | |
| Source: {source.text} → | |
| </a> | |
| {/if} | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .scrolly-text-box { | |
| max-width: 380px; | |
| background: rgba(255, 255, 255, 0.96); | |
| backdrop-filter: blur(6px); | |
| -webkit-backdrop-filter: blur(6px); | |
| padding: 1.5rem 1.75rem; | |
| border-radius: 6px; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); | |
| opacity: 0.75; | |
| transform: translateY(8px); | |
| transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); | |
| pointer-events: auto; | |
| } | |
| .scrolly-text-box.active { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| .scrolly-text-box.wide { | |
| max-width: 480px; | |
| } | |
| .scrolly-text-box.has-image { | |
| max-width: 320px; | |
| } | |
| .box-image { | |
| width: 100%; | |
| height: auto; | |
| border-radius: 4px; | |
| margin-bottom: 1rem; | |
| } | |
| .scrolly-text-box.dark { | |
| background: rgba(15, 15, 15, 0.92); | |
| } | |
| .scrolly-text-box.custom-bg { | |
| background: var(--custom-bg); | |
| } | |
| .scrolly-text-box.dark .box-title, | |
| .scrolly-text-box.custom-bg .box-title { | |
| color: rgba(255, 255, 255, 0.95); | |
| } | |
| .scrolly-text-box.dark .box-content, | |
| .scrolly-text-box.custom-bg .box-content { | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .box-title { | |
| font-family: 'Playfair Display', Georgia, serif; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| line-height: 1.25; | |
| margin: 0 0 0.75rem 0; | |
| color: #1a1a1a; | |
| } | |
| .box-content { | |
| font-family: 'Source Sans 3', system-ui, sans-serif; | |
| font-size: 1.05rem; | |
| line-height: 1.65; | |
| color: #2a2a2a; | |
| } | |
| .box-content :global(p) { | |
| margin: 0; | |
| } | |
| .box-content :global(p + p) { | |
| margin-top: 1em; | |
| } | |
| .box-content :global(strong) { | |
| font-weight: 600; | |
| color: #000; | |
| } | |
| .scrolly-text-box.dark .box-content :global(strong) { | |
| color: #fff; | |
| } | |
| /* Category highlight colors - TNH branding */ | |
| .box-content :global(.highlight-env), | |
| .box-content :global(.highlight-dev), | |
| .box-content :global(.highlight-admin), | |
| .box-content :global(.highlight-satra) { | |
| color: white; | |
| padding: 0.1em 0.4em; | |
| border-radius: 3px; | |
| font-weight: 500; | |
| box-decoration-break: clone; | |
| -webkit-box-decoration-break: clone; | |
| } | |
| .box-content :global(.highlight-env) { | |
| background: #35B58B; | |
| } | |
| .box-content :global(.highlight-dev) { | |
| background: #9F3E52; | |
| } | |
| .box-content :global(.highlight-admin) { | |
| background: #E8A84C; | |
| } | |
| .box-content :global(.highlight-satra) { | |
| background: #6B7FD7; | |
| } | |
| /* Footer with source and image credit */ | |
| .box-footer { | |
| margin-top: 1rem; | |
| padding-top: 0.75rem; | |
| border-top: 1px solid rgba(0, 0, 0, 0.1); | |
| font-family: 'Source Sans 3', system-ui, sans-serif; | |
| font-size: 0.8rem; | |
| color: rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .scrolly-text-box.dark .box-footer { | |
| border-top-color: rgba(255, 255, 255, 0.15); | |
| color: rgba(255, 255, 255, 0.5); | |
| } | |
| .image-credit { | |
| font-style: italic; | |
| } | |
| .footer-divider { | |
| opacity: 0.4; | |
| } | |
| .source-link { | |
| color: inherit; | |
| text-decoration: none; | |
| transition: color 0.15s ease; | |
| } | |
| .source-link:hover { | |
| color: rgba(0, 0, 0, 0.8); | |
| text-decoration: underline; | |
| } | |
| .scrolly-text-box.dark .source-link:hover { | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| /* Responsive */ | |
| @media (max-width: 600px) { | |
| .scrolly-text-box { | |
| max-width: 320px; | |
| padding: 1.25rem 1.5rem; | |
| } | |
| .scrolly-text-box.wide { | |
| max-width: 340px; | |
| } | |
| .scrolly-text-box.has-image { | |
| max-width: 280px; | |
| padding: 1rem 1.25rem; | |
| } | |
| .box-image { | |
| margin-bottom: 0.75rem; | |
| } | |
| .box-content { | |
| font-size: 0.95rem; | |
| } | |
| .box-title { | |
| font-size: 1.35rem; | |
| } | |
| } | |
| </style> | |