diff --git a/src/AnimationPanel.scss b/src/AnimationPanel.scss index 513b324..9a94cc7 100644 --- a/src/AnimationPanel.scss +++ b/src/AnimationPanel.scss @@ -1634,12 +1634,125 @@ } .avatar-animation-sidebar__actions button .avatar-material-icon { width: 17px; height: 17px; } -@media (max-width: 760px) { - .avatar-animation-panel { max-height: 72vh; margin: 0 8px 8px; } - .avatar-animation-panel__track-column { flex-basis: 124px; width: 124px !important; } - .avatar-animation-panel__track-header > strong { display: none; } - .avatar-animation-panel__zoom { display: none; } - .avatar-animation-panel__timecode { min-width: auto; } +@media (max-width: 840px) { + .avatar-animation-panel { + min-height: 0; + max-height: 62vh; + max-height: 62dvh; + grid-template-rows: auto minmax(0, 1fr); + margin: 0 8px calc(8px + env(safe-area-inset-bottom, 0px)); + border-radius: 14px; + } + + .avatar-animation-panel__resize-grip { display: none; } + + .avatar-animation-panel__transport { + height: auto; + min-height: 0; + max-height: none; + flex-wrap: wrap; + align-items: center; + gap: 6px 8px; + padding: 8px 10px; + cursor: default; + touch-action: auto; + } + + .avatar-animation-panel__transport-primary, + .avatar-animation-panel__transport-secondary { + display: flex; + flex: 1 1 auto; + min-width: 0; + align-items: center; + flex-wrap: wrap; + gap: 6px; + } + + .avatar-animation-panel__transport-secondary { + justify-content: flex-end; + margin-left: 0; + } + + .avatar-animation-panel__transport button:not(.avatar-app__interaction-mode-option) { + height: 38px; + } + + .avatar-animation-panel__play, + .avatar-animation-panel__close, + .avatar-animation-panel__save-project { + width: 38px !important; + height: 38px !important; + } + + .avatar-animation-panel__more > button { width: 38px; } + + .avatar-animation-panel__timecode { + min-width: auto; + height: 38px; + padding: 0 8px; + } + .avatar-animation-panel__timecode span { display: none; } - .avatar-animation-sidebar__library { grid-template-columns: repeat(auto-fill, 32px); } + + .avatar-animation-panel__speed { + height: 38px; + + > button { + min-width: 30px; + height: 30px; + padding: 0 5px; + } + } + + .avatar-animation-panel__zoom { display: none; } + + .avatar-animation-panel__editor { min-height: 0; } + + .avatar-animation-panel__track-column { + flex-basis: 108px; + width: 108px !important; + } + + .avatar-animation-panel__track-header { + gap: 4px; + padding: 5px 6px; + } + + .avatar-animation-panel__track-header > strong { display: none; } + + .avatar-animation-panel__track-header > button, + .avatar-animation-panel__track-menu > summary { + width: 30px; + height: 30px; + } + + .avatar-animation-panel__clip-keyframes > button { + width: 28px; + height: 28px; + } + + .avatar-animation-panel__more-popover { + max-width: calc(100vw - 32px); + max-height: 46dvh; + overflow-y: auto; + } + + .avatar-animation-sidebar__library { + grid-template-columns: repeat(auto-fill, 36px); + column-gap: 10px; + } + + .avatar-animation-sidebar__asset, + .avatar-animation-sidebar__asset span, + .avatar-animation-sidebar__asset img, + .avatar-animation-sidebar__saved-asset { + width: 36px; + height: 36px; + } +} + +@media (hover: none) { + .avatar-animation-panel__track-menu > summary { opacity: 1; } + .avatar-animation-panel__trim { opacity: .5; } + .avatar-animation-panel__clip[data-selected='true'] .avatar-animation-panel__trim { opacity: 1; } } diff --git a/src/AnimationPanel.tsx b/src/AnimationPanel.tsx index df88f60..662e32f 100644 --- a/src/AnimationPanel.tsx +++ b/src/AnimationPanel.tsx @@ -1572,6 +1572,11 @@ export function AnimationPanel({ } const startPanelResize = (event: PointerEvent) => { if (event.button !== 0) return + if ( + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(max-width: 840px)').matches + ) return const target = event.target as Element const startedFromGrip = target.closest('.avatar-animation-panel__resize-grip') != null if (!startedFromGrip && target.closest('button, input, select, textarea, a, [role="menuitem"]') != null) return diff --git a/src/App.scss b/src/App.scss index 7bc05e1..29735bf 100644 --- a/src/App.scss +++ b/src/App.scss @@ -636,78 +636,267 @@ } } +.avatar-app__mobile-nav { + position: fixed; + z-index: 30; + right: 0; + bottom: 0; + left: 0; + display: flex; + align-items: stretch; + justify-content: space-between; + gap: 2px; + min-height: var(--avatar-mobile-nav-height, 58px); + padding: 4px 6px calc(4px + env(safe-area-inset-bottom, 0px)); + border-top: 1px solid var(--avatar-glass-border); + background: var(--avatar-glass-bg); + box-shadow: 0 -10px 30px color-mix(in srgb, black 16%, transparent); + backdrop-filter: var(--avatar-glass-filter); + -webkit-backdrop-filter: var(--avatar-glass-filter); +} + +.avatar-app__mobile-nav-item { + display: flex; + min-width: 0; + flex: 1 1 0; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + border: 0; + border-radius: 10px; + padding: 5px 2px; + background: transparent; + color: var(--sub-text-color); + cursor: pointer; + font-size: 10px; + font-weight: 700; + line-height: 1.1; + + span { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + svg { + width: 22px; + height: 22px; + flex: 0 0 auto; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.5; + } + + &[aria-current='page'] { + background: var(--primary-soft-bg); + color: var(--primary-text-color); + } + + &:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; + } +} + @media (max-width: 840px) { .avatar-app { - &__workspace { - grid-template-columns: 1fr; - grid-template-rows: auto auto auto; - } + --avatar-shell-padding: 12px; + --avatar-shell-gap: 6px; + &__workspace, &__workspace[data-animation-open='true'] { + display: block; height: auto; min-height: 100vh; - grid-template-rows: auto auto auto auto; + min-height: 100dvh; + grid-template-rows: none; overflow: visible; } - &__workspace[data-controls-collapsed='true'][data-animation-open='true'], - &__workspace[data-resources-collapsed='true'][data-animation-open='true'] { - grid-template-rows: auto auto auto; + &__stage { + min-height: 100vh; + min-height: 100dvh; + grid-template-rows: minmax(0, 1fr); + padding: + calc(var(--avatar-shell-padding) + env(safe-area-inset-top, 0px)) + calc(var(--avatar-shell-padding) + env(safe-area-inset-right, 0px)) + calc(var(--avatar-shell-padding) + env(safe-area-inset-bottom, 0px)) + calc(var(--avatar-shell-padding) + env(safe-area-inset-left, 0px)); } - &__stage { - grid-column: 1; - grid-row: 1; - min-height: auto; - grid-template-rows: auto; + &__workspace[data-resources-collapsed='false'] &__stage, + &__workspace[data-controls-collapsed='false'] &__stage, + &__workspace[data-animation-open='true'] &__stage { + min-height: 44vh; + min-height: 44dvh; } - &__workspace > .avatar-controls--left { - grid-column: 1; - grid-row: 2; + &__preview-art { + max-width: min(78vw, 340px); + + &--hero { + max-width: none; + } } - &__workspace > .avatar-controls--right { - grid-column: 1; - grid-row: 3; + &__camera-tools, + &__stage-actions { + top: calc(var(--avatar-shell-padding) + env(safe-area-inset-top, 0px)); + max-width: calc(50% - 6px); + flex-wrap: wrap; + row-gap: var(--avatar-shell-gap); } - &__workspace[data-resources-collapsed='true'] > .avatar-controls--right { - grid-column: 1; - grid-row: 2; + &__camera-tools { + left: calc(var(--avatar-shell-padding) + env(safe-area-inset-left, 0px)); } - &__workspace[data-controls-collapsed='true'] > .avatar-controls--left { - grid-row: 2; + &__stage-actions { + right: calc(var(--avatar-shell-padding) + env(safe-area-inset-right, 0px)); } - &__workspace[data-animation-open='true'] > .avatar-animation-panel { - grid-row: 4; + &__camera-toggle, + &__home-link, + &__workspace-toggle, + &__resources-toggle, + &__controls-toggle, + &__github-link, + &__language-toggle, + &__save-preset, + &__theme-toggle, + &__animation-toggle { + width: 36px; + height: 36px; + + svg { + width: 16px; + height: 16px; + } } - &__workspace[data-animation-open='true'][data-controls-collapsed='true'] > .avatar-animation-panel, - &__workspace[data-animation-open='true'][data-resources-collapsed='true'] > .avatar-animation-panel { - grid-row: 3; + &__home-link img, + &__workspace-toggle img { + width: 20px; + height: 20px; } - &__workspace[data-animation-open='true'][data-controls-collapsed='true'][data-resources-collapsed='true'] > .avatar-animation-panel { - grid-row: 2; + &__animation-toggle { + left: calc(var(--avatar-shell-padding) + env(safe-area-inset-left, 0px)); + bottom: calc(var(--avatar-shell-padding) + env(safe-area-inset-bottom, 0px)); } - &__preview-art { - max-width: 240px; + &__interaction-mode { + left: calc(var(--avatar-shell-padding) + 42px + env(safe-area-inset-left, 0px)); + bottom: calc(var(--avatar-shell-padding) + env(safe-area-inset-bottom, 0px)); + } - &--hero { - max-width: none; - } + &__workspace[data-animation-open='true'] &__interaction-mode { + left: calc(var(--avatar-shell-padding) + env(safe-area-inset-left, 0px)); + } + + &__workspace[data-mobile-nav='true'] { + --avatar-mobile-nav-height: 58px; + --avatar-mobile-sheet-height: 50dvh; + } + + &__workspace[data-mobile-nav='true'] &__stage { + min-height: 100vh; + min-height: 100dvh; + padding-bottom: calc( + var(--avatar-shell-padding) + var(--avatar-mobile-nav-height) + env(safe-area-inset-bottom, 0px) + ); + } + + &__workspace[data-mobile-nav='true'][data-resources-collapsed='false'] &__stage, + &__workspace[data-mobile-nav='true'][data-controls-collapsed='false'] &__stage, + &__workspace[data-mobile-nav='true'][data-animation-open='true'] &__stage { + padding-bottom: calc( + var(--avatar-shell-padding) + var(--avatar-mobile-nav-height) + + var(--avatar-mobile-sheet-height) + env(safe-area-inset-bottom, 0px) + ); + } + + &__workspace[data-mobile-nav='true'] > .avatar-controls { + position: fixed; + z-index: 28; + top: auto; + right: 8px; + bottom: calc(var(--avatar-mobile-nav-height) + env(safe-area-inset-bottom, 0px)); + left: 8px; + height: auto; + max-height: var(--avatar-mobile-sheet-height); + margin: 0; + border-radius: 16px; + } + + &__workspace[data-mobile-nav='true'] > .avatar-animation-panel { + position: fixed; + z-index: 28; + top: auto; + right: 8px; + bottom: calc(var(--avatar-mobile-nav-height) + env(safe-area-inset-bottom, 0px)); + left: 8px; + margin: 0; + max-height: var(--avatar-mobile-sheet-height); + } + + &__workspace[data-mobile-nav='true'] &__interaction-mode, + &__workspace[data-mobile-nav='true'] .avatar-orientation-control { + bottom: calc( + var(--avatar-shell-padding) + var(--avatar-mobile-nav-height) + env(safe-area-inset-bottom, 0px) + ); + } + + &__workspace[data-mobile-nav='true'][data-resources-collapsed='false'] &__interaction-mode, + &__workspace[data-mobile-nav='true'][data-controls-collapsed='false'] &__interaction-mode, + &__workspace[data-mobile-nav='true'][data-animation-open='true'] &__interaction-mode, + &__workspace[data-mobile-nav='true'][data-resources-collapsed='false'] .avatar-orientation-control, + &__workspace[data-mobile-nav='true'][data-controls-collapsed='false'] .avatar-orientation-control, + &__workspace[data-mobile-nav='true'][data-animation-open='true'] .avatar-orientation-control { + bottom: calc( + var(--avatar-shell-padding) + var(--avatar-mobile-nav-height) + + var(--avatar-mobile-sheet-height) + env(safe-area-inset-bottom, 0px) + ); } } + + .avatar-orientation-control { + right: calc(var(--avatar-shell-padding, 12px) + env(safe-area-inset-right, 0px)); + bottom: calc(var(--avatar-shell-padding, 12px) + env(safe-area-inset-bottom, 0px)); + } + + .avatar-language-switcher__dialog { + max-width: calc(100vw - 24px); + } } -@media (max-width: 520px) { +@media (max-width: 380px) { .avatar-app { - &__stage { - padding: var(--avatar-shell-padding); + &__camera-toggle, + &__home-link, + &__workspace-toggle, + &__resources-toggle, + &__controls-toggle, + &__github-link, + &__language-toggle, + &__save-preset, + &__theme-toggle, + &__animation-toggle { + width: 32px; + height: 32px; + + svg { + width: 15px; + height: 15px; + } + } + + &__interaction-mode { + left: calc(var(--avatar-shell-padding) + 38px + env(safe-area-inset-left, 0px)); } } } diff --git a/src/App.tsx b/src/App.tsx index 0cd7fd8..3ba332b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,6 +76,8 @@ import type { AvatarOutlineStyle, AvatarViewState } from './InteractiveAvatar' +import { AvatarMobileNav } from './AvatarMobileNav' +import type { AvatarMobileSection } from './AvatarMobileNav' import { LanguageSwitcher } from './LanguageSwitcher' import { getAvatarEffectStylePreset } from './avatarEffectStylePresets' import { WorkspaceSidebar } from './WorkspaceSidebar' @@ -1958,7 +1960,11 @@ function App({ const [projectSaveState, setProjectSaveState] = useState<'error' | 'idle' | 'saved' | 'saving'>('idle') const [controlsCollapsed, setControlsCollapsed] = useState(initialConfig.controlsCollapsed) const [controlsWidth, setControlsWidth] = useState(DEFAULT_CONTROLS_WIDTH) - const [resourcesCollapsed, setResourcesCollapsed] = useState(false) + const [resourcesCollapsed, setResourcesCollapsed] = useState(() => ( + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(max-width: 840px)').matches + )) const [resourcesWidth, setResourcesWidth] = useState(DEFAULT_RESOURCE_WIDTH) const [systemDark, setSystemDark] = useState(() => { return typeof window !== 'undefined' && window.matchMedia(SYSTEM_DARK_MEDIA_QUERY).matches @@ -2114,6 +2120,11 @@ function App({ const [keyframeCapturePending, setKeyframeCapturePending] = useState(false) const [interactionControlsDocked, setInteractionControlsDocked] = useState(false) const [stageNarrow, setStageNarrow] = useState(false) + const [isNarrowViewport, setIsNarrowViewport] = useState(() => ( + typeof window !== 'undefined' && + typeof window.matchMedia === 'function' && + window.matchMedia('(max-width: 840px)').matches + )) const [animationThumbnailCapture, setAnimationThumbnailCapture] = useState( null ) @@ -2452,6 +2463,16 @@ function App({ } }, []) + useEffect(() => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return + const mediaQuery = window.matchMedia('(max-width: 840px)') + const handleChange = (event: MediaQueryListEvent) => setIsNarrowViewport(event.matches) + setIsNarrowViewport(mediaQuery.matches) + if (typeof mediaQuery.addEventListener !== 'function') return + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + }, []) + useEffect(() => { if (embedded) return const params = new URLSearchParams(window.location.search) @@ -3421,6 +3442,59 @@ function App({ setAnimationOpen(true) } + const closeAnimationTimeline = () => { + pauseAnimationTimeline() + setAnimationOpen(false) + } + + const mobileNavActive = !embedded && isNarrowViewport + + const mobileNavSection: AvatarMobileSection = (() => { + if (animationOpen) return 'animation' + if (!resourcesCollapsed) return 'build' + if (!controlsCollapsed) { + if (activeTab === 'style') return 'style' + if (activeTab === 'effects') return 'effects' + return 'body' + } + return 'preview' + })() + + const selectMobileNavSection = (section: AvatarMobileSection) => { + if (section === mobileNavSection) { + setResourcesCollapsed(true) + setControlsCollapsed(true) + if (animationOpen) closeAnimationTimeline() + return + } + switch (section) { + case 'preview': + setResourcesCollapsed(true) + setControlsCollapsed(true) + if (animationOpen) closeAnimationTimeline() + break + case 'build': + setActiveTab('build') + setResourcesCollapsed(false) + setControlsCollapsed(true) + if (animationOpen) closeAnimationTimeline() + break + case 'body': + case 'style': + case 'effects': + setActiveTab(section) + setControlsCollapsed(false) + setResourcesCollapsed(true) + if (animationOpen) closeAnimationTimeline() + break + case 'animation': + setResourcesCollapsed(true) + setControlsCollapsed(true) + openAnimationTimeline() + break + } + } + const seekAnimationTimeline = (timeMs: number) => { const timeline = animationTimelineRef.current renderAnimationTimeline(Math.min(Math.max(timeMs, 0), timeline.durationMs)) @@ -6271,6 +6345,7 @@ function App({ className='avatar-app__workspace' data-animation-open={animationOpen} data-controls-collapsed={controlsCollapsed} + data-mobile-nav={mobileNavActive} data-resources-collapsed={resourcesCollapsed} style={{ '--avatar-controls-width': `${controlsWidth}px`, @@ -6314,7 +6389,7 @@ function App({ ? (
{resourcesCollapsed ? renderWorkspaceToggle() : null} - {resourcesCollapsed ? renderResourcesToggle() : null} + {resourcesCollapsed && !mobileNavActive ? renderResourcesToggle() : null} {!stageNarrow || controlsCollapsed ? renderCameraToggle() : null}
) @@ -6323,8 +6398,8 @@ function App({ {!cameraMode && (!stageNarrow || controlsCollapsed) ? renderSavePreset() : null} - {controlsCollapsed ? renderGlobalHeaderActions() : null} - {controlsCollapsed + {controlsCollapsed || mobileNavActive ? renderGlobalHeaderActions() : null} + {controlsCollapsed && !mobileNavActive ? ( + ))} + + ) +}