diff --git a/public/default-avatar.png b/src/assets/default-avatar.png similarity index 100% rename from public/default-avatar.png rename to src/assets/default-avatar.png diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..2e3b818 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/AppGrid.ts b/src/components/AppGrid.ts index 30c8ba7..c2da182 100644 --- a/src/components/AppGrid.ts +++ b/src/components/AppGrid.ts @@ -14,26 +14,52 @@ export class AppGrid extends LitElement { name: string; }[] = []; + override connectedCallback() { + super.connectedCallback(); + this.style.setProperty('--icons-count', this.apps.length.toString()); + } + static override styles = [ css` :host { display: grid; grid-template-columns: 80px; justify-content: space-around; - gap: 20px; - padding: 30px; + gap: 28px; + padding: 42px 30px; margin-top: 10px; - + --stagger-delay: calc(var(--animation-duration) * 2 / var(--icons-count)); + @media (min-width: 500px) { grid-template-columns: repeat(2, 80px); + grid-auto-rows: minmax(120px, auto); } @media (min-width: 768px) { grid-template-columns: repeat(3, 80px); + gap: 32px 42px; } @media (min-width: 1024px) { grid-template-columns: repeat(4, 80px); + grid-auto-flow: dense; + } + } + + arx-app-icon { + opacity: 0; + transform: translateY(20px) scale(0.8); + animation: float-in var(--transition) forwards; + } + + @keyframes float-in { + 0% { + opacity: 0; + transform: translateY(20px) scale(0.8); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); } } `, @@ -43,12 +69,13 @@ export class AppGrid extends LitElement { return html` ${map( this.apps, - (app) => html` + (app, index) => html` `, )} diff --git a/src/components/AppIcon.ts b/src/components/AppIcon.ts index 30f6990..72438d5 100644 --- a/src/components/AppIcon.ts +++ b/src/components/AppIcon.ts @@ -2,6 +2,7 @@ import { LitElement, css, html } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import '@components/EveLink'; +import { ifDefined } from 'lit/directives/if-defined.js'; import { when } from 'lit/directives/when.js'; @customElement('arx-app-icon') @@ -18,43 +19,56 @@ export class AppIcon extends LitElement { @property() name = 'App'; + @property({ type: Boolean }) + small = false; + + @property({ type: Boolean }) + selected = false; + private iconElement?: HTMLElement; static override styles = [ css` :host { display: flex; - --animation-speed: 0.25s; + --shadow-opacity: 0.2; + --hover-lift: -6px; + --tap-scale: 0.92; } .app-icon { display: flex; flex-direction: column; align-items: center; - gap: 8px; + gap: 10px; text-decoration: none; - padding: 6px; + padding: 8px; border-radius: 18px; - transition: transform var(--animation-speed) - cubic-bezier(0.175, 0.885, 0.32, 1.275); + transition: transform var(--transition); + position: relative; + will-change: transform; + perspective: 800px; } .app-icon:hover { - transform: translateY(-2px); + transform: translateY(var(--hover-lift)); } .icon { width: 96px; height: 96px; border-radius: 24px; - box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12); + box-shadow: 0 3px 8px rgba(0, 0, 0, var(--shadow-opacity)); display: flex; justify-content: center; align-items: center; border: 1px solid rgba(255, 255, 255, 0.2); overflow: hidden; position: relative; - transition: all var(--animation-speed) ease-out; + transition: + transform var(--overshoot-transition), + box-shadow var(--overshoot-transition); + transform-style: preserve-3d; &::before { content: ""; position: absolute; @@ -66,7 +80,7 @@ export class AppIcon extends LitElement { ); pointer-events: none; opacity: 1; - transition: opacity var(--animation-speed) ease-out; + transition: opacity var(--transition); } &::after { @@ -74,13 +88,14 @@ export class AppIcon extends LitElement { position: absolute; inset: 0; background: linear-gradient( - var(--gradient-angle), - rgba(255, 255, 255, 0.25) 20%, - rgba(255, 255, 255, 0) 60% + var(--gradient-angle, 145deg), + rgba(255, 255, 255, 0.35) 10%, + rgba(255, 255, 255, 0) 70% ); pointer-events: none; opacity: 0; - transition: opacity var(--animation-speed) ease-out; + transition: opacity var(--undershoot-transition); + z-index: 1; } &:hover::before { @@ -92,17 +107,26 @@ export class AppIcon extends LitElement { } } + :host([small]) .icon { + width: 64px; + height: 64px; + border-radius: 12px; + } + .icon-svg { color: white; filter: drop-shadow( - calc(cos(var(--gradient-angle)) * 2px) - calc(sin(var(--gradient-angle)) * 2px) rgba(0, 0, 0, 0.2) + calc(cos(var(--gradient-angle, 145deg)) * 3px) + calc(sin(var(--gradient-angle, 145deg)) * 3px) + rgba(0, 0, 0, 0.25) ); display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; + transition: transform var(--overshoot-transition); + z-index: 2; } .app-name { @@ -114,16 +138,53 @@ export class AppIcon extends LitElement { color: var(--color-base-content); white-space: nowrap; text-decoration: none; - transition: all var(--animation-speed) ease-out; + transition: all var(--transition); + transform: translateZ(0); + opacity: 0.85; + } + + :host([selected]) .app-name { + color: var(--color-primary-content); + } + + :host([small]) .app-name { + font-size: 14px; + margin-top: 2px; } .app-icon:hover .icon { - transform: scale(1.08); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25); + transform: scale(1.05) translateZ(10px) rotateX(var(--rotate-x, 0deg)) rotateY(var(--rotate-y, 0deg)); + box-shadow: + 0 10px 20px -5px rgba(0, 0, 0, calc(var(--shadow-opacity) * 1.5)), + 0 0 0 1px rgba(255, 255, 255, 0.15); + } + + :host([small]) .app-icon:hover .icon { + transform: scale(1.05) translateZ(5px) rotateX(var(--rotate-x, 0deg)) rotateY(var(--rotate-y, 0deg)); + } + + .app-icon:hover .app-name { + opacity: 1; + transform: translateY(2px) scale(1.02); + } + + .app-icon:hover .icon-svg { + transform: scale(1.08) translateZ(15px); + } + + :host([small]) .app-icon:hover .icon-svg { + transform: scale(1.08) translateZ(8px); } .app-icon:active .icon { - transform: scale(0.96); + transform: scale(var(--tap-scale)) translateZ(0); + box-shadow: 0 2px 5px rgba(0, 0, 0, calc(var(--shadow-opacity) * 0.8)); + transition: all var(--transition); + } + + .app-icon:active .app-name { + transform: translateY(0) scale(0.98); + transition: all var(--transition); } `, ]; @@ -132,6 +193,7 @@ export class AppIcon extends LitElement { this.iconElement = this.shadowRoot?.querySelector('.icon') as HTMLElement; if (this.iconElement) { this.iconElement.addEventListener('mousemove', this.handleMouseMove); + this.iconElement.addEventListener('mouseleave', this.handleMouseLeave); } } @@ -139,6 +201,7 @@ export class AppIcon extends LitElement { super.disconnectedCallback(); if (this.iconElement) { this.iconElement.removeEventListener('mousemove', this.handleMouseMove); + this.iconElement.removeEventListener('mouseleave', this.handleMouseLeave); } } @@ -153,7 +216,32 @@ export class AppIcon extends LitElement { const dy = e.clientY - centerY; const angle = Math.atan2(dy, dx) * (180 / Math.PI); + const maxTilt = 20; + const percentX = (e.clientX - rect.left) / rect.width - 0.5; + const percentY = (e.clientY - rect.top) / rect.height - 0.5; + + const tiltX = -percentY * maxTilt * 1.2; + const tiltY = percentX * maxTilt; + this.iconElement.style.setProperty('--gradient-angle', `${angle}deg`); + this.iconElement.style.setProperty('--rotate-x', `${tiltX}deg`); + this.iconElement.style.setProperty('--rotate-y', `${tiltY}deg`); + }; + + handleMouseLeave = () => { + if (!this.iconElement) return; + + this.iconElement.style.transition = 'transform var(--transition)'; + this.iconElement.style.setProperty('--rotate-x', '0deg'); + this.iconElement.style.setProperty('--rotate-y', '0deg'); + + this.iconElement.addEventListener( + 'transitionend', + () => { + this.iconElement!.style.transition = ''; + }, + { once: true }, + ); }; override render() { @@ -162,12 +250,13 @@ export class AppIcon extends LitElement {
${when( this.icon, - () => html``, + () => + html``, )}
${this.name} diff --git a/src/components/Calendar/CalendarEventDialog.ts b/src/components/Calendar/CalendarEventDialog.ts index b2bfcae..e655369 100644 --- a/src/components/Calendar/CalendarEventDialog.ts +++ b/src/components/Calendar/CalendarEventDialog.ts @@ -1,11 +1,13 @@ import { LitElement, css, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { type Ref, createRef, ref } from 'lit/directives/ref.js'; import { when } from 'lit/directives/when.js'; import '@components/General/Button'; import { StyledInput } from '@components/General/Input'; import { StyledTextarea } from '@components/General/Textarea'; import { StyledToggle } from '@components/General/Toggle'; +import type { EveDialog } from '../General/Dialog'; interface InputEvent extends Event { detail: { @@ -18,6 +20,8 @@ export class CalendarEventDialog extends LitElement { @property({ type: Boolean }) open = false; + dialogRef: Ref = createRef(); + @state() private newEvent = { title: '', @@ -49,7 +53,8 @@ export class CalendarEventDialog extends LitElement { `; private handleClose() { - this.dispatchEvent(new CustomEvent('close')); + this.dialogRef.value?.close(false); + this.dispatchEvent(new CustomEvent('close', { bubbles: false })); } private handleInputChange(e: Event) { @@ -77,7 +82,7 @@ export class CalendarEventDialog extends LitElement { if (!this.open) return html``; return html` - + ) { - if (changedProps.has('open') && this.open) { - this._previousFocus = document.activeElement as HTMLElement; - // Focus the dialog container after rendering - setTimeout(() => { - const container = this.shadowRoot?.querySelector('.dialog-container') as HTMLElement; - if (container) container.focus(); - }, 50); - } else if (changedProps.has('open') && !this.open && this._previousFocus) { - this._previousFocus.focus(); + if (changedProps.has('open')) { + if (this.open) this.show(); + else this.close(); } } @@ -129,24 +139,36 @@ export class EveDialog extends LitElement { } private _handleOverlayClick(e: MouseEvent) { - if (this.closeOnOverlayClick && e.target === e.currentTarget) { - this.close(); - } + if (this.closeOnOverlayClick && e.target === e.currentTarget) this.close(); } show() { this.open = true; + this.style.display = 'block'; + this.previousScrollTop = this.rootWindow?.scrollTop ?? 0; + this.style.height = `${this.rootWindow!.clientHeight * 2}px`; // this is a hack to prevent the overlay background from showing through + this.rootWindow?.style.setProperty('overflow', 'hidden', 'important'); + this.rootWindow?.scrollTo({ + top: 0, + behavior: 'smooth', + }); } - close() { + close(triggerEvent = true) { this.open = false; - this.dispatchEvent(new CustomEvent('close')); + this.style.display = 'none'; + this.rootWindow?.style.setProperty('overflow', 'auto', 'important'); + this.rootWindow?.scrollTo({ + top: this.previousScrollTop, + behavior: 'smooth', + }); + if (triggerEvent) this.dispatchEvent(new CustomEvent('close')); } override render() { return html`
diff --git a/src/components/Header.ts b/src/components/Header.ts index e8745c9..2ef9272 100644 --- a/src/components/Header.ts +++ b/src/components/Header.ts @@ -35,10 +35,6 @@ export class Header extends LitElement { header { background: var(--color-primary); backdrop-filter: blur(10px); - border: var(--border) solid var(--color-primary-content); - box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px) - calc(var(--depth) * 8px) - oklch(from var(--color-base-content) l c h / 0.2); height: var(--font-2xl, 3rem); font-size: var(--font-md, 1rem); transition: all 0.3s ease; diff --git a/src/components/NostrAvatar.ts b/src/components/NostrAvatar.ts index fbd43d3..caad32d 100644 --- a/src/components/NostrAvatar.ts +++ b/src/components/NostrAvatar.ts @@ -1,8 +1,8 @@ +import defaultAvatar from '@assets/default-avatar.png'; import type { NDKUserProfile } from '@nostr-dev-kit/ndk'; import { LitElement, css, html } from 'lit-element'; import { customElement, property } from 'lit/decorators.js'; type AvatarSize = 'short' | 'medium' | 'large' | 'huge'; -import defaultAvatar from '@/default-avatar.png'; @customElement('arx-nostr-avatar') export class ArxNostrAvatar extends LitElement { diff --git a/src/components/Sidebar.ts b/src/components/Sidebar.ts new file mode 100644 index 0000000..253f7a1 --- /dev/null +++ b/src/components/Sidebar.ts @@ -0,0 +1,267 @@ +import defaultAvatar from '@assets/default-avatar.png'; +import logo from '@assets/logo.png'; +import type { NDKUserProfile } from '@nostr-dev-kit/ndk'; +import { LitElement, css, html } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { map } from 'lit/directives/map.js'; + +@customElement('arx-sidebar') +export default class Sidebar extends LitElement { + @property({ type: String }) + currentPath = ''; + + @property({ type: String }) + userNpub = ''; + + @property({ type: Object }) + userProfile: NDKUserProfile | undefined = undefined; + + @property({ type: Array }) + apps = [ + { + id: 1, + href: 'beacon', + name: 'Beacon', + color: '#FF8C00', + icon: 'fa-solid:sun', + }, + { + id: 2, + href: 'arbor', + name: 'Arbor', + color: '#FF4040', + icon: 'fa-solid:tree', + }, + { + id: 3, + href: 'wallet', + name: 'Wallet', + color: '#1E90FF', + icon: 'fa-solid:spa', + }, + { + id: 4, + href: 'settings', + name: 'Settings', + color: '#7B68EE', + icon: 'fa-solid:tools', + }, + ]; + + static override styles = css` + :host { + overflow-x: hidden; + background: var(--color-base-200); + padding: 0 0 1.5rem 0; + display: flex; + flex-direction: column; + align-items: center; + overflow-y: overlay; + scrollbar-width: none; + position: relative; + height: 100%; + } + + ::-webkit-scrollbar { + width: 4px; + height: 4px; + background: transparent; + } + + ::-webkit-scrollbar-track { + background: transparent; + margin: 3px; + } + + ::-webkit-scrollbar-thumb { + background: rgba(100, 100, 100, 0.4); + border-radius: 10px; + transition: all 0.3s ease; + } + + ::-webkit-scrollbar-thumb:hover { + background: rgba(100, 100, 100, 0.7); + } + + :host:hover { + scrollbar-color: rgba(100, 100, 100, 0.4) transparent; + } + + :host::after { + content: ''; + position: absolute; + right: 0; + top: 0; + width: 10px; + height: 100%; + opacity: 0; + pointer-events: none; + background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.03)); + transition: opacity 0.3s ease; + } + + :host:hover::after { + opacity: 1; + } + + .app-icon-small { + margin-bottom: 1.5rem; + width: 80px; + } + + .logo { + height: 64px; + margin-bottom: 1.5rem; + cursor: pointer; + transition: transform 0.2s ease, filter 0.3s ease; + border-radius: var(--radius-field); + padding: 8px; + background: none; + filter: + drop-shadow(0 0 2px rgba(0, 0, 0, 1)) + drop-shadow(0 0 1px rgba(255, 255, 255, 1)); + } + + .logo-container { + width: 100%; + background: var(--color-accent); + display: flex; + justify-content: center; + padding: 12px 0; + margin-bottom: 1.5rem; + position: relative; + } + + .logo-container::after { + content: ''; + position: absolute; + bottom: -5px; + left: 0; + right: 0; + height: 6px; + background: rgba(0, 0, 0, 0.08); + border-radius: 50%; + filter: blur(3px); + } + + .logo-container .logo { + margin-bottom: 0; + transform: translateY(0); + } + + .logo:hover { + transform: translateY(-2px) scale(1.02); + filter: + drop-shadow(0 0 3px rgba(0, 0, 0, 0.8)) + drop-shadow(0 0 2px rgba(255, 255, 255, 0.6)); + } + + .logo:active { + transform: translateY(1px) scale(0.98); + filter: + drop-shadow(0 0 1px rgba(0, 0, 0, 0.6)); + transition-duration: 0.1s; + } + + .app-icon-small.active { + transform: scale(1.15); + position: relative; + z-index: 10; + } + + .app-icon-small.active::before { + content: ''; + position: absolute; + top: -8px; + left: -8px; + right: -8px; + bottom: -8px; + background: var(--color-accent); + z-index: -1; + } + + .sidebar-item { + position: relative; + margin-bottom: 1.5rem; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .sidebar-item:hover { + transform: translateY(-2px); + } + + .profile-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + object-fit: cover; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + border: 2px solid var(--color-base-300); + } + + .profile-avatar.active { + border-color: var(--color-primary); + transform: scale(1.15); + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15); + } + + .profile-item { + margin-bottom: 1.5rem; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + display: flex; + justify-content: center; + } + + .profile-item:hover .profile-avatar { + transform: translateY(-2px) scale(1.05); + } + `; + + private handleLogoClick() { + this.dispatchEvent(new CustomEvent('navigate', { detail: 'home' })); + } + + override render() { + const activePath = this.currentPath.split('/')[0]; + + return html` + + ${map( + this.apps, + (app) => html` + + `, + )} +
+ + `; + } +} diff --git a/src/default-avatar.png b/src/default-avatar.png deleted file mode 100644 index 02e81ef..0000000 Binary files a/src/default-avatar.png and /dev/null differ diff --git a/src/electron/main.ts b/src/electron/main.ts index c2eb3d8..14a0d67 100644 --- a/src/electron/main.ts +++ b/src/electron/main.ts @@ -38,9 +38,11 @@ ipcMain.handle('relay:status', () => { function createWindow(): void { const mainWindow = new BrowserWindow({ - width: 1024, + width: 1366, height: 768, show: false, + minWidth: 1366, + minHeight: 768, autoHideMenuBar: true, webPreferences: { preload: path.join(__dirname, '../preload/preload.mjs'), diff --git a/src/main.ts b/src/main.ts index 45d1d50..af68c9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,12 @@ -import './style.css'; -import '@components/ErrorView'; -import '@components/NostrAvatar'; -import '@components/LoadingView'; -import '@components/NostrProfile'; import '@components/Breadcrumbs'; +import '@components/ErrorView'; import '@components/Header'; -import '@routes/router'; import '@components/LoadingView'; +import '@components/NostrAvatar'; +import '@components/NostrProfile'; +import '@routes/router'; import type EveRouter from '@routes/router'; +import './style.css'; function checkRelayUp() { return new Promise((resolve, reject) => { diff --git a/src/routes/Home.ts b/src/routes/Home.ts index 081c5a7..929dddf 100644 --- a/src/routes/Home.ts +++ b/src/routes/Home.ts @@ -193,7 +193,7 @@ export class Home extends LitElement {
diff --git a/src/routes/Settings.ts b/src/routes/Settings.ts index ab245d3..ec13bab 100644 --- a/src/routes/Settings.ts +++ b/src/routes/Settings.ts @@ -1,5 +1,5 @@ -import defaultAvatar from '@/default-avatar.png'; import { getSigner, getUserProfile, ndk } from '@/ndk'; +import defaultAvatar from '@assets/default-avatar.png'; import type { ArxInputChangeEvent } from '@components/General/Input'; import { NDKEvent, type NDKUserProfile } from '@nostr-dev-kit/ndk'; import { LitElement, css, html } from 'lit'; @@ -62,6 +62,7 @@ export class EveSettings extends LitElement { @state() private profile: NDKUserProfile | undefined; @state() private error: string | undefined; @state() private darkMode = false; + @state() private pageTransitions = true; @state() private relayStatus: { running: boolean; pid: number | null; logs: string[] } = { running: false, pid: null, @@ -77,6 +78,7 @@ export class EveSettings extends LitElement { try { this.profile = await getUserProfile(); this.darkMode = localStorage.getItem('darkMode') === 'true'; + this.pageTransitions = localStorage.getItem('pageTransitions') !== 'false'; this.updateRelayStatus(); this.loading = false; } catch (err) { @@ -117,6 +119,12 @@ export class EveSettings extends LitElement { document.body.classList.toggle('dark', this.darkMode); } + private togglePageTransitions() { + this.pageTransitions = !this.pageTransitions; + localStorage.setItem('pageTransitions', this.pageTransitions.toString()); + location.reload(); + } + private reset() { if (!confirm('Are you sure you want to reset the app?')) return; localStorage.clear(); @@ -133,12 +141,17 @@ export class EveSettings extends LitElement { return html` - + this.toggleDarkMode()} > + this.togglePageTransitions()} + > ${when( diff --git a/src/routes/router.ts b/src/routes/router.ts index 82cd92c..51410f4 100644 --- a/src/routes/router.ts +++ b/src/routes/router.ts @@ -1,4 +1,5 @@ import '@components/InitialSetup'; +import '@components/Sidebar'; import '@routes/404Page'; import '@routes/Arbor/Home'; import '@routes/Arbor/NewPost'; @@ -10,10 +11,14 @@ import '@routes/Profile'; import '@routes/Settings'; import '@routes/Wallet'; +import { getNpub, getUserProfile } from '@/ndk'; +import type { NDKUserProfile } from '@nostr-dev-kit/ndk'; import { spread } from '@open-wc/lit-helpers'; import { LitElement, css } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { keyed } from 'lit/directives/keyed.js'; +import { type Ref, createRef, ref } from 'lit/directives/ref.js'; +import { when } from 'lit/directives/when.js'; import { type StaticValue, html, literal } from 'lit/static-html.js'; export interface RouteParams { @@ -24,7 +29,6 @@ interface Route { pattern: string; params: RouteParams; component: StaticValue; - // component: typeof LitElement | ((params: RouteParams) => typeof LitElement); title?: string; meta?: Record; } @@ -90,17 +94,34 @@ export default class EveRouter extends LitElement { @state() private currentIndex = -1; + @state() + private isTransitioning = false; + @property() public ccnSetup = false; + windowContentRef: Ref = createRef(); + pageTransitions = true; + private beforeEachGuards: ((to: Route, from: Route | null) => boolean)[] = []; private afterEachHooks: ((to: Route, from: Route | null) => void)[] = []; + @state() + private userProfile: NDKUserProfile | undefined = undefined; + + @state() + private userNpub = ''; + static override styles = css` :host { - height: 100vh; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; display: grid; grid-template-rows: auto 1fr; + grid-template-columns: 100px 1fr; overflow: hidden; } @@ -124,9 +145,34 @@ export default class EveRouter extends LitElement { ::-webkit-scrollbar-thumb:hover { background: var(--color-neutral); } - + .window { overflow: auto; + grid-column: 2; + grid-row: 2; + position: relative; + } + + .window::after { + content: ''; + position: absolute; + right: 0; + top: 0; + width: 10px; + height: 100%; + opacity: 0; + pointer-events: none; + background: linear-gradient(to right, transparent, rgba(0, 0, 0, 0.03)); + transition: opacity 0.3s ease; + } + + .window:hover::after { + opacity: 1; + } + + arx-sidebar { + grid-column: 1; + grid-row: 1 / span 2; } .window-content { @@ -135,22 +181,58 @@ export default class EveRouter extends LitElement { height: 100%; margin: 0 auto; padding: 1rem; + opacity: 1; + transform-origin: left center; + transform: perspective(1200px) translateX(0); + transition: var(--transition); + backface-visibility: hidden; + filter: blur(0px); + } + + .window-content::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 4px; + height: 100%; + opacity: 0; + transition: var(--transition); + } + + .window-content.transitioning { + overflow: hidden; + transform: perspective(1200px) translateX(50vw); + filter: blur(50px); + } + + .window-content.transitioning::after { + opacity: 1; } .hide-overflow { overflow: hidden; } + + arx-header { + grid-column: 2; + grid-row: 1; + } `; constructor() { super(); this.initializeRouter(); + this.pageTransitions = localStorage.getItem('pageTransitions') !== 'false'; if (this.ccnSetup) window.relay.start(localStorage.getItem('encryption_key')!); } override connectedCallback(): void { super.connectedCallback(); this.setupEventListeners(); + if (this.ccnSetup) { + this.loadUserProfile(); + } } override disconnectedCallback(): void { @@ -171,11 +253,11 @@ export default class EveRouter extends LitElement { window.addEventListener('popstate', this.handlePopState.bind(this)); } - private handleHashChange(): void { + private async handleHashChange(): Promise { const newPath = this.currentPath; if (newPath !== this.history[this.currentIndex]) { + await this.requestUpdateWithTransition(); this.updateHistory(newPath); - this.requestUpdate(); } } @@ -252,7 +334,7 @@ export default class EveRouter extends LitElement { const canProceed = this.beforeEachGuards.every((guard) => guard(to, from)); if (canProceed) { - this.requestUpdate(); + await this.requestUpdateWithTransition(); for (const hook of this.afterEachHooks) { hook(to, from); } @@ -263,6 +345,25 @@ export default class EveRouter extends LitElement { } } + async requestUpdateWithTransition(): Promise { + if (!this.windowContentRef.value) return this.requestUpdate(); + if (!this.pageTransitions) return this.requestUpdate(); + this.isTransitioning = true; + this.requestUpdate(); + + await new Promise((resolve) => { + this.windowContentRef.value!.addEventListener( + 'transitionend', + () => { + this.isTransitioning = false; + resolve(true); + }, + { once: true }, + ); + }); + this.requestUpdate(); + } + goBack(): void { if (this.currentIndex > 0) { this.currentIndex--; @@ -283,7 +384,7 @@ export default class EveRouter extends LitElement { renderSetup() { return html` -
+
this.finishSetup()} @@ -293,9 +394,25 @@ export default class EveRouter extends LitElement { `; } + async loadUserProfile() { + try { + this.userNpub = await getNpub(); + this.userProfile = await getUserProfile(); + } catch (error) { + console.error('Failed to load user profile:', error); + } + } + override render() { if (!this.ccnSetup) return this.renderSetup(); + return html` + this.navigate(e.detail)} + > 0} ?canGoForward=${this.currentIndex < this.history.length - 1} @@ -306,10 +423,14 @@ export default class EveRouter extends LitElement { title="Eve" >
-
- ${keyed( - this.currentRoute.params, - html` +
+ ${when( + this.isTransitioning, + () => html``, + () => + keyed( + this.currentRoute.params, + html` <${this.currentRoute.component} ${spread(this.currentRoute.params)} path=${this.currentPath} @@ -318,6 +439,7 @@ export default class EveRouter extends LitElement { @go-forward=${this.goForward} > `, + ), )}
diff --git a/src/style.css b/src/style.css index cee2999..a66b6de 100644 --- a/src/style.css +++ b/src/style.css @@ -6,8 +6,13 @@ --space-sm: clamp(1rem, 1.5vw, 1.5rem); --space-md: clamp(2rem, 3vw, 3rem); - --animation-curve: cubic-bezier(0.68, -0.55, 0.265, 1.55); - --transition: 0.3s var(--animation-curve); + --undershoot-curve: cubic-bezier(0.38, 0, 0.618, 0.88); + --animation-curve: cubic-bezier(0.18, 0, 0.618, 1); + --overshoot-curve: cubic-bezier(0.38, 0, 0.618, 1.12); + --animation-duration: 275ms; + --transition: var(--animation-duration) var(--animation-curve); + --overshoot-transition: calc(var(--animation-duration) * 1.2) var(--overshoot-curve); + --undershoot-transition: calc(var(--animation-duration) * 0.8) var(--undershoot-curve); --color-base-100: oklch(98% 0.016 73.684); --color-base-200: oklch(95% 0.038 75.164);