wip bsky client for the web & android bbell.vt3e.cat

feat: onboarding flow

vt3e.cat aedb3dfd 43473234

verified
+1543 -1013
+2 -1
.prettierrc.json
··· 2 2 "$schema": "https://json.schemastore.org/prettierrc", 3 3 "semi": false, 4 4 "singleQuote": true, 5 - "printWidth": 100 5 + "printWidth": 100, 6 + "useTabs": true 6 7 }
+1 -1
README.md
··· 1 - # scilla 1 + # bluebell
+1 -1
bun.lock
··· 2 2 "lockfileVersion": 1, 3 3 "workspaces": { 4 4 "": { 5 - "name": "scilla", 5 + "name": "bluebell", 6 6 "dependencies": { 7 7 "@atcute/atproto": "^3.1.9", 8 8 "@atcute/bluesky": "^3.2.14",
+10 -10
index.html
··· 1 1 <!doctype html> 2 2 <html lang=""> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <link rel="icon" href="/scilla.svg" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <title>Scilla</title> 8 - </head> 9 - <body> 10 - <div id="app"></div> 11 - <script type="module" src="/src/main.ts"></script> 12 - </body> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" href="/bluebell.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>Bluebell</title> 8 + </head> 9 + <body> 10 + <div id="app"></div> 11 + <script type="module" src="/src/main.ts"></script> 12 + </body> 13 13 </html>
+1 -1
package.json
··· 1 1 { 2 - "name": "scilla", 2 + "name": "bluebell", 3 3 "version": "0.0.0", 4 4 "private": true, 5 5 "type": "module",
+15
public/bluebell.svg
··· 1 + <svg width="2048" height="2048" viewBox="0 0 2048 2048" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M987.563 1947C987.563 1781.17 931.206 1421.28 907.606 1321.98M910.736 1335.59C668.562 262.68 1109.08 -180.576 1208 282.138" stroke="currentColor" stroke-width="88" stroke-linecap="round"/> 3 + <path d="M617.738 666.068C606.197 729.561 509.353 829.454 495.303 825.304C486.773 822.784 480.806 716.134 486.773 680.178C497.311 616.685 514.011 571.201 553.51 556.216C598.67 539.083 633.78 577.812 617.738 666.068Z" fill="currentColor"/> 4 + <path d="M556.329 589.832C551.514 598.531 554.661 609.484 563.358 614.296C572.055 619.108 583.009 615.956 587.824 607.257L572.076 598.545L556.329 589.832ZM572.076 598.545L587.824 607.257C613.283 561.261 638.617 543.859 659.909 539.252C681.48 534.584 704.062 541.903 726.334 556.669C748.482 571.353 768.083 591.977 782.398 609.457C789.483 618.108 795.116 625.795 798.957 631.289C800.875 634.032 802.34 636.218 803.308 637.691C803.792 638.428 804.151 638.985 804.381 639.345C804.495 639.524 804.578 639.654 804.627 639.732C804.651 639.771 804.667 639.797 804.675 639.809C804.679 639.815 804.681 639.818 804.681 639.818C804.681 639.818 804.679 639.815 804.679 639.815C804.676 639.811 804.674 639.806 819.956 630.291C835.239 620.775 835.235 620.769 835.23 620.762C835.228 620.758 835.223 620.75 835.219 620.743C835.21 620.729 835.199 620.712 835.186 620.691C835.16 620.649 835.125 620.593 835.081 620.524C834.994 620.386 834.873 620.195 834.719 619.954C834.41 619.471 833.968 618.784 833.396 617.915C832.254 616.176 830.595 613.701 828.462 610.65C824.2 604.556 818.018 596.122 810.253 586.64C794.868 567.854 772.634 544.165 746.234 526.662C719.958 509.241 687.282 496.498 652.308 504.066C617.056 511.694 584.56 538.826 556.329 589.832L572.076 598.545Z" fill="currentColor"/> 5 + <path d="M995.078 1949.53C920.134 1932.8 1008.65 1889.33 669.422 1724.28C410.502 1598.3 269 1185.6 269 1185.6C269 1185.6 356.31 1151.84 660.39 1271.77C995.078 1450.15 1049.27 1961.62 995.078 1949.53Z" fill="currentColor"/> 6 + <path d="M987.531 1947.96C1061.03 1925.7 969.548 1888.95 1295.65 1699.07C1544.54 1554.14 1655.16 1132.03 1655.16 1132.03C1655.16 1132.03 1565.59 1104.87 1271.22 1247.13C950.631 1449.97 934.382 1964.06 987.531 1947.96Z" fill="currentColor"/> 7 + <path d="M1408.24 600.896C1477.66 835.595 1403.46 985.856 1403.46 985.856C1403.46 985.856 1258.89 909.353 1213.69 651.343C1162.61 452.667 1164.75 280.314 1218.48 266.384C1272.2 252.453 1345.74 362.793 1408.24 600.896Z" fill="currentColor"/> 8 + <path d="M1408.24 600.896C1477.66 835.595 1403.46 985.856 1403.46 985.856C1403.46 985.856 1258.89 909.353 1213.69 651.343C1162.61 452.667 1164.75 280.314 1218.48 266.384C1272.2 252.453 1345.74 362.793 1408.24 600.896Z" fill="currentColor"/> 9 + <path d="M1466.1 588.695C1595.8 796.004 1566.66 958.806 1566.66 958.806C1566.66 958.806 1359.17 962.032 1292.22 689.823C1243.23 490.616 1145.74 345.677 1193.75 317.751C1241.77 289.826 1342.14 376.242 1466.1 588.695Z" fill="currentColor"/> 10 + <path d="M1466.1 588.695C1595.8 796.004 1566.66 958.806 1566.66 958.806C1566.66 958.806 1359.17 962.032 1292.22 689.823C1243.23 490.616 1145.74 345.677 1193.75 317.751C1241.77 289.826 1342.14 376.242 1466.1 588.695Z" fill="currentColor"/> 11 + <path d="M1152.67 688.891C1129.77 968.065 1256.13 1059.6 1256.13 1059.6C1256.13 1059.6 1431.95 939.059 1353.6 687.257C1292.66 491.404 1305.63 316.101 1250.14 316.553C1194.66 317.004 1151.93 442.653 1152.67 688.891Z" fill="currentColor"/> 12 + <path d="M1152.67 688.891C1129.77 968.065 1256.13 1059.6 1256.13 1059.6C1256.13 1059.6 1431.95 939.059 1353.6 687.257C1292.66 491.404 1305.63 316.101 1250.14 316.553C1194.66 317.004 1151.93 442.653 1152.67 688.891Z" fill="currentColor"/> 13 + <path d="M1818.47 663.391C1818.47 663.391 1784.36 802.892 1635.2 817.435C1486.03 831.978 1231.76 548.596 1216.35 268.124C1216.35 268.124 1347.03 215.34 1492.36 501.48C1608.37 729.898 1818.47 663.391 1818.47 663.391Z" fill="currentColor"/> 14 + <path d="M899.979 923.673C899.979 923.673 1001.86 1024.5 1136.47 958.337C1271.07 892.174 1339.62 516.98 1206.3 270.043C1206.3 270.043 1067.47 293.909 1093.13 614.084C1113.61 869.671 899.979 923.673 899.979 923.673Z" fill="currentColor"/> 15 + </svg>
+11 -11
public/oauth-client-metadata.json
··· 1 1 { 2 - "client_id": "http://localhost:5173/oauth-client-metadata.json", 3 - "client_name": "Scilla", 4 - "client_uri": "http://localhost:5173", 5 - "logo_uri": "http://localhost:5173/favicon.ico", 6 - "redirect_uris": ["http://localhost:5173/oauth/callback"], 7 - "scope": "atproto transition:generic", 8 - "grant_types": ["authorization_code", "refresh_token"], 9 - "response_types": ["code"], 10 - "token_endpoint_auth_method": "none", 11 - "application_type": "web", 12 - "dpop_bound_access_tokens": true 2 + "client_id": "http://localhost:5173/oauth-client-metadata.json", 3 + "client_name": "Bluebell", 4 + "client_uri": "http://localhost:5173", 5 + "logo_uri": "http://localhost:5173/favicon.ico", 6 + "redirect_uris": ["http://localhost:5173/oauth/callback"], 7 + "scope": "atproto transition:generic", 8 + "grant_types": ["authorization_code", "refresh_token"], 9 + "response_types": ["code"], 10 + "token_endpoint_auth_method": "none", 11 + "application_type": "web", 12 + "dpop_bound_access_tokens": true 13 13 }
-3
public/scilla.svg
··· 1 - <svg width="2048" height="2048" viewBox="0 0 2048 2048" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <rect width="2048" height="2048" fill="#301B92"/> 3 - </svg>
+56 -19
src/App.vue
··· 8 8 import TabStack from '@/components/Navigation/TabStack.vue' 9 9 import NavigationBar from '@/components/Navigation/NavigationBar.vue' 10 10 import OAuthCallback from '@/views/Auth/OAuthCallback.vue' 11 + import OnboardingFlow from '@/views/Onboarding/OnboardingFlow.vue' 11 12 12 13 import { stackRoots, type StackRootNames } from './router' 13 14 ··· 20 21 const tabs: StackRootNames[] = stackRoots.map((p) => p.name) 21 22 22 23 const isCallback = ref(window.location.pathname.includes('/oauth/callback')) 24 + 25 + const hasSeenIntro = localStorage.getItem('bluebell-intro-complete') === 'true' 26 + const showIntro = ref(!hasSeenIntro && !isCallback.value) 27 + 23 28 auth.init() 29 + 24 30 const onAuthComplete = () => { 25 31 window.history.replaceState(null, '', '/') 26 - 27 32 theme.init() 28 33 env.init() 29 34 auth.init() 30 35 isCallback.value = false 36 + } 37 + 38 + const onIntroComplete = (action: 'stay' | 'login') => { 39 + localStorage.setItem('bluebell-intro-complete', 'false') 40 + showIntro.value = false 41 + nav.init() 42 + 43 + if (action === 'login') { 44 + setTimeout(() => { 45 + nav.push('login') 46 + }, 100) 47 + } 31 48 } 32 49 33 50 onMounted(async () => { 34 51 theme.init() 35 52 env.init() 36 53 37 - if (!isCallback.value) { 54 + if (!showIntro.value && !isCallback.value) { 38 55 nav.init() 39 56 } 40 57 }) 41 58 </script> 42 59 43 60 <template> 61 + <Transition name="intro-fade"> 62 + <OnboardingFlow v-if="showIntro" @complete="onIntroComplete" /> 63 + </Transition> 64 + 44 65 <Transition name="app-fade" mode="in-out"> 45 - <OAuthCallback v-if="isCallback" @complete="onAuthComplete" class="view-layer" /> 66 + <div v-if="!showIntro" class="app-root"> 67 + <OAuthCallback v-if="isCallback" @complete="onAuthComplete" class="view-layer" /> 46 68 47 - <div v-else class="app-shell view-layer"> 48 - <div class="skip-links"> 49 - <a href="#main-content" id="skip-to-content" class="skip-link"> skip to main content </a> 50 - <a href="#navigation-bar" class="skip-link"> skip to navigation </a> 51 - </div> 69 + <div v-else class="app-shell view-layer"> 70 + <div class="skip-links"> 71 + <a href="#main-content" id="skip-to-content" class="skip-link"> skip to main content </a> 72 + <a href="#navigation-bar" class="skip-link"> skip to navigation </a> 73 + </div> 52 74 53 - <div class="viewport" id="main-content"> 54 - <TabStack 55 - v-for="t in tabs" 56 - :key="t" 57 - :tab="t" 58 - v-show="activeTab === t" 59 - :class="{ active: activeTab === t }" 60 - /> 75 + <div class="viewport" id="main-content"> 76 + <TabStack 77 + v-for="t in tabs" 78 + :key="t" 79 + :tab="t" 80 + v-show="activeTab === t" 81 + :class="{ active: activeTab === t }" 82 + /> 83 + </div> 84 + <NavigationBar ref="navBar" /> 61 85 </div> 62 - <NavigationBar ref="navBar" /> 63 86 </div> 64 87 </Transition> 65 88 </template> 66 89 67 90 <style scoped> 91 + .app-root { 92 + width: 100%; 93 + height: 100%; 94 + } 95 + 68 96 .view-layer { 69 97 position: absolute; 70 98 inset: 0; ··· 72 100 height: 100vh; 73 101 } 74 102 75 - .app-fade-enter-active, 76 - .app-fade-leave-active { 103 + .intro-fade-leave-active { 77 104 transition: opacity 0.8s ease; 105 + } 106 + .intro-fade-leave-to { 107 + opacity: 0; 108 + } 109 + 110 + .app-fade-enter-active { 111 + transition: opacity 1s ease 0.2s; 112 + } 113 + .app-fade-leave-active { 114 + transition: opacity 0.5s ease; 78 115 } 79 116 80 117 .app-fade-enter-from {
src/assets/bluebell.webp

This is a binary file and will not be displayed.

+15
src/assets/icons/bluebell.svg
··· 1 + <svg width="2048" height="2048" viewBox="0 0 2048 2048" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M987.563 1947C987.563 1781.17 931.206 1421.28 907.606 1321.98M910.736 1335.59C668.562 262.68 1109.08 -180.576 1208 282.138" stroke="currentColor" stroke-width="88" stroke-linecap="round"/> 3 + <path d="M617.738 666.068C606.197 729.561 509.353 829.454 495.303 825.304C486.773 822.784 480.806 716.134 486.773 680.178C497.311 616.685 514.011 571.201 553.51 556.216C598.67 539.083 633.78 577.812 617.738 666.068Z" fill="currentColor"/> 4 + <path d="M556.329 589.832C551.514 598.531 554.661 609.484 563.358 614.296C572.055 619.108 583.009 615.956 587.824 607.257L572.076 598.545L556.329 589.832ZM572.076 598.545L587.824 607.257C613.283 561.261 638.617 543.859 659.909 539.252C681.48 534.584 704.062 541.903 726.334 556.669C748.482 571.353 768.083 591.977 782.398 609.457C789.483 618.108 795.116 625.795 798.957 631.289C800.875 634.032 802.34 636.218 803.308 637.691C803.792 638.428 804.151 638.985 804.381 639.345C804.495 639.524 804.578 639.654 804.627 639.732C804.651 639.771 804.667 639.797 804.675 639.809C804.679 639.815 804.681 639.818 804.681 639.818C804.681 639.818 804.679 639.815 804.679 639.815C804.676 639.811 804.674 639.806 819.956 630.291C835.239 620.775 835.235 620.769 835.23 620.762C835.228 620.758 835.223 620.75 835.219 620.743C835.21 620.729 835.199 620.712 835.186 620.691C835.16 620.649 835.125 620.593 835.081 620.524C834.994 620.386 834.873 620.195 834.719 619.954C834.41 619.471 833.968 618.784 833.396 617.915C832.254 616.176 830.595 613.701 828.462 610.65C824.2 604.556 818.018 596.122 810.253 586.64C794.868 567.854 772.634 544.165 746.234 526.662C719.958 509.241 687.282 496.498 652.308 504.066C617.056 511.694 584.56 538.826 556.329 589.832L572.076 598.545Z" fill="currentColor"/> 5 + <path d="M995.078 1949.53C920.134 1932.8 1008.65 1889.33 669.422 1724.28C410.502 1598.3 269 1185.6 269 1185.6C269 1185.6 356.31 1151.84 660.39 1271.77C995.078 1450.15 1049.27 1961.62 995.078 1949.53Z" fill="currentColor"/> 6 + <path d="M987.531 1947.96C1061.03 1925.7 969.548 1888.95 1295.65 1699.07C1544.54 1554.14 1655.16 1132.03 1655.16 1132.03C1655.16 1132.03 1565.59 1104.87 1271.22 1247.13C950.631 1449.97 934.382 1964.06 987.531 1947.96Z" fill="currentColor"/> 7 + <path d="M1408.24 600.896C1477.66 835.595 1403.46 985.856 1403.46 985.856C1403.46 985.856 1258.89 909.353 1213.69 651.343C1162.61 452.667 1164.75 280.314 1218.48 266.384C1272.2 252.453 1345.74 362.793 1408.24 600.896Z" fill="currentColor"/> 8 + <path d="M1408.24 600.896C1477.66 835.595 1403.46 985.856 1403.46 985.856C1403.46 985.856 1258.89 909.353 1213.69 651.343C1162.61 452.667 1164.75 280.314 1218.48 266.384C1272.2 252.453 1345.74 362.793 1408.24 600.896Z" fill="currentColor"/> 9 + <path d="M1466.1 588.695C1595.8 796.004 1566.66 958.806 1566.66 958.806C1566.66 958.806 1359.17 962.032 1292.22 689.823C1243.23 490.616 1145.74 345.677 1193.75 317.751C1241.77 289.826 1342.14 376.242 1466.1 588.695Z" fill="currentColor"/> 10 + <path d="M1466.1 588.695C1595.8 796.004 1566.66 958.806 1566.66 958.806C1566.66 958.806 1359.17 962.032 1292.22 689.823C1243.23 490.616 1145.74 345.677 1193.75 317.751C1241.77 289.826 1342.14 376.242 1466.1 588.695Z" fill="currentColor"/> 11 + <path d="M1152.67 688.891C1129.77 968.065 1256.13 1059.6 1256.13 1059.6C1256.13 1059.6 1431.95 939.059 1353.6 687.257C1292.66 491.404 1305.63 316.101 1250.14 316.553C1194.66 317.004 1151.93 442.653 1152.67 688.891Z" fill="currentColor"/> 12 + <path d="M1152.67 688.891C1129.77 968.065 1256.13 1059.6 1256.13 1059.6C1256.13 1059.6 1431.95 939.059 1353.6 687.257C1292.66 491.404 1305.63 316.101 1250.14 316.553C1194.66 317.004 1151.93 442.653 1152.67 688.891Z" fill="currentColor"/> 13 + <path d="M1818.47 663.391C1818.47 663.391 1784.36 802.892 1635.2 817.435C1486.03 831.978 1231.76 548.596 1216.35 268.124C1216.35 268.124 1347.03 215.34 1492.36 501.48C1608.37 729.898 1818.47 663.391 1818.47 663.391Z" fill="currentColor"/> 14 + <path d="M899.979 923.673C899.979 923.673 1001.86 1024.5 1136.47 958.337C1271.07 892.174 1339.62 516.98 1206.3 270.043C1206.3 270.043 1067.47 293.909 1093.13 614.084C1113.61 869.671 899.979 923.673 899.979 923.673Z" fill="currentColor"/> 15 + </svg>
-3
src/assets/icons/scilla.svg
··· 1 - <svg width="2048" height="2048" viewBox="0 0 2048 2048" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 - <rect width="2048" height="2048" fill="#301B92"/> 3 - </svg>
+105 -105
src/components/Navigation/NavigationBar.vue
··· 3 3 import NavigationItem from './NavItem.vue' 4 4 import SVG from '@/components/UI/SVG.vue' 5 5 import { stackRoots } from '@/router/index' 6 - import ScillaLogo from '@/assets/icons/scilla.svg?raw' 6 + import BluebellLogo from '@/assets/icons/bluebell.svg?raw' 7 7 8 8 import { useNavigationStore } from '@/stores/navigation' 9 9 const nav = useNavigationStore() ··· 13 13 </script> 14 14 15 15 <template> 16 - <nav 17 - ref="navRef" 18 - class="navigation-bar" 19 - id="navigation-bar" 20 - role="tablist" 21 - aria-label="Main navigation" 22 - > 23 - <ul class="menu" role="presentation"> 24 - <li class="desktop-logo" role="presentation"> 25 - <SVG :icon="ScillaLogo"></SVG> 26 - </li> 16 + <nav 17 + ref="navRef" 18 + class="navigation-bar" 19 + id="navigation-bar" 20 + role="tablist" 21 + aria-label="Main navigation" 22 + > 23 + <ul class="menu" role="presentation"> 24 + <li class="desktop-logo" role="presentation"> 25 + <SVG :icon="BluebellLogo"></SVG> 26 + </li> 27 27 28 - <NavigationItem 29 - v-for="route in stackRoots" 30 - :item="route" 31 - :key="route.name" 32 - role="tab" 33 - :aria-selected="nav.activeTab === route.name" 34 - :tabindex="nav.activeTab === route.name ? 0 : -1" 35 - /> 36 - </ul> 37 - </nav> 28 + <NavigationItem 29 + v-for="route in stackRoots" 30 + :item="route" 31 + :key="route.name" 32 + role="tab" 33 + :aria-selected="nav.activeTab === route.name" 34 + :tabindex="nav.activeTab === route.name ? 0 : -1" 35 + /> 36 + </ul> 37 + </nav> 38 38 </template> 39 39 40 40 <style scoped lang="scss"> 41 41 .navigation-bar { 42 - --gap: 0.5rem; 43 - background: hsla(var(--mantle) / 1); 44 - border-top: 1px solid hsla(var(--surface2) / 0.25); 42 + --gap: 0.5rem; 43 + background: hsla(var(--mantle) / 1); 44 + border-top: 1px solid hsla(var(--surface2) / 0.25); 45 45 46 - width: 100%; 47 - max-width: 100vw; 48 - padding-bottom: calc(var(--safe-area-inset-bottom) + 0.25rem); 49 - padding-top: 0.25rem; 46 + width: 100%; 47 + max-width: 100vw; 48 + padding-bottom: calc(var(--safe-area-inset-bottom) + 0.25rem); 49 + padding-top: 0.25rem; 50 50 51 - position: fixed; 52 - bottom: 0; 53 - z-index: 100; 54 - transition-property: 55 - color, background-color, box-shadow, outline, border-color, border-radius, font-weight, opacity, 56 - backdrop-filter, filter, width, bottom, left; 51 + position: fixed; 52 + bottom: 0; 53 + z-index: 100; 54 + transition-property: 55 + color, background-color, box-shadow, outline, border-color, border-radius, font-weight, opacity, 56 + backdrop-filter, filter, width, bottom, left; 57 57 58 - .desktop-logo { 59 - display: none; 60 - color: white; 58 + .desktop-logo { 59 + display: none; 60 + color: white; 61 61 62 - :deep(svg) { 63 - background: hsla(var(--accent) / 0.05); 64 - color: hsla(var(--accent) / 1); 65 - border-radius: var(--radius-sm); 66 - &:hover { 67 - transform: scale(1.1); 68 - background: hsla(var(--accent) / 0.1); 69 - } 70 - &:active { 71 - transform: scale(0.95); 72 - } 73 - } 74 - } 62 + :deep(svg) { 63 + background: hsla(var(--accent) / 0.05); 64 + color: hsla(var(--accent) / 1); 65 + border-radius: var(--radius-sm); 66 + &:hover { 67 + transform: scale(1.1); 68 + background: hsla(var(--accent) / 0.1); 69 + } 70 + &:active { 71 + transform: scale(0.95); 72 + } 73 + } 74 + } 75 75 76 - .menu { 77 - display: flex; 78 - justify-content: space-around; 79 - align-items: center; 80 - max-width: 500px; 81 - margin: 0 auto; 82 - list-style: none; 83 - gap: var(--gap); 84 - padding: 0 1rem; 85 - } 76 + .menu { 77 + display: flex; 78 + justify-content: space-around; 79 + align-items: center; 80 + max-width: 500px; 81 + margin: 0 auto; 82 + list-style: none; 83 + gap: var(--gap); 84 + padding: 0 1rem; 85 + } 86 86 87 - @media (min-width: 640px) { 88 - padding: 1rem 0.5rem; 89 - position: relative; 90 - bottom: auto; 91 - left: auto; 92 - width: fit-content; 93 - height: 100%; 94 - border-top: none; 95 - background: transparent; 96 - backdrop-filter: none; 97 - order: -1; 87 + @media (min-width: 640px) { 88 + padding: 1rem 0.5rem; 89 + position: relative; 90 + bottom: auto; 91 + left: auto; 92 + width: fit-content; 93 + height: 100%; 94 + border-top: none; 95 + background: transparent; 96 + backdrop-filter: none; 97 + order: -1; 98 98 99 - .desktop-logo { 100 - display: flex; 101 - justify-content: center; 102 - padding: 0.5rem; 99 + .desktop-logo { 100 + display: flex; 101 + justify-content: center; 102 + padding: 0.5rem; 103 103 104 - :deep(svg) { 105 - width: 2.5rem; 106 - height: 2.5rem; 107 - display: block; 108 - } 109 - } 104 + :deep(svg) { 105 + width: 2.5rem; 106 + height: 2.5rem; 107 + display: block; 108 + } 109 + } 110 110 111 - .menu { 112 - flex-direction: column; 113 - justify-content: flex-start; 114 - gap: 0.5rem; 115 - width: 100%; 116 - padding: 0; 117 - } 118 - } 111 + .menu { 112 + flex-direction: column; 113 + justify-content: flex-start; 114 + gap: 0.5rem; 115 + width: 100%; 116 + padding: 0; 117 + } 118 + } 119 119 120 - @media (min-width: 1024px) { 121 - width: 260px; 122 - padding-right: 1.5rem; 123 - .menu { 124 - align-items: stretch; 125 - } 120 + @media (min-width: 1024px) { 121 + width: 260px; 122 + padding-right: 1.5rem; 123 + .menu { 124 + align-items: stretch; 125 + } 126 126 127 - .desktop-logo { 128 - justify-content: flex-start; 129 - padding-left: 1rem; 127 + .desktop-logo { 128 + justify-content: flex-start; 129 + padding-left: 1rem; 130 130 131 - :deep(svg) { 132 - width: 2.5rem; 133 - height: 2.5rem; 134 - } 135 - } 136 - } 131 + :deep(svg) { 132 + width: 2.5rem; 133 + height: 2.5rem; 134 + } 135 + } 136 + } 137 137 } 138 138 </style>
+157 -157
src/stores/auth.ts
··· 1 1 import { defineStore } from 'pinia' 2 2 import { ref, computed, markRaw } from 'vue' 3 3 import { 4 - configureOAuth, 5 - createAuthorizationUrl, 6 - finalizeAuthorization, 7 - getSession, 8 - deleteStoredSession, 9 - OAuthUserAgent, 10 - type Session, 4 + configureOAuth, 5 + createAuthorizationUrl, 6 + finalizeAuthorization, 7 + getSession, 8 + deleteStoredSession, 9 + OAuthUserAgent, 10 + type Session, 11 11 } from '@atcute/oauth-browser-client' 12 12 import { 13 - CompositeDidDocumentResolver, 14 - LocalActorResolver, 15 - PlcDidDocumentResolver, 16 - WebDidDocumentResolver, 17 - XrpcHandleResolver, 13 + CompositeDidDocumentResolver, 14 + LocalActorResolver, 15 + PlcDidDocumentResolver, 16 + WebDidDocumentResolver, 17 + XrpcHandleResolver, 18 18 } from '@atcute/identity-resolver' 19 19 import { Client, simpleFetchHandler } from '@atcute/client' 20 20 import type { ActorIdentifier } from '@atcute/lexicons' 21 21 import type { DidString } from '@/types/atproto.ts' 22 22 23 23 export const useAuthStore = defineStore('auth', () => { 24 - const session = ref<Session | null>(null) 25 - const agent = ref<OAuthUserAgent | null>(null) 26 - const isLoading = ref(true) 27 - const isProcessingCallback = ref(false) 28 - const error = ref<string | null>(null) 24 + const session = ref<Session | null>(null) 25 + const agent = ref<OAuthUserAgent | null>(null) 26 + const isLoading = ref(true) 27 + const isProcessingCallback = ref(false) 28 + const error = ref<string | null>(null) 29 29 30 - const isAuthenticated = computed(() => !!session.value) 31 - const activeDid = ref<string | null>(localStorage.getItem('scilla-active-did')) 32 - const userDid = computed(() => session.value?.info.sub) 30 + const isAuthenticated = computed(() => !!session.value) 31 + const activeDid = ref<string | null>(localStorage.getItem('bluebell-active-did')) 32 + const userDid = computed(() => session.value?.info.sub) 33 33 34 - function init() { 35 - configureOAuth({ 36 - metadata: { 37 - client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 38 - redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI, 39 - }, 40 - identityResolver: new LocalActorResolver({ 41 - handleResolver: new XrpcHandleResolver({ serviceUrl: 'https://public.api.bsky.app' }), 42 - didDocumentResolver: new CompositeDidDocumentResolver({ 43 - methods: { 44 - plc: new PlcDidDocumentResolver(), 45 - web: new WebDidDocumentResolver(), 46 - }, 47 - }), 48 - }), 49 - }) 34 + function init() { 35 + configureOAuth({ 36 + metadata: { 37 + client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 38 + redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI, 39 + }, 40 + identityResolver: new LocalActorResolver({ 41 + handleResolver: new XrpcHandleResolver({ serviceUrl: 'https://public.api.bsky.app' }), 42 + didDocumentResolver: new CompositeDidDocumentResolver({ 43 + methods: { 44 + plc: new PlcDidDocumentResolver(), 45 + web: new WebDidDocumentResolver(), 46 + }, 47 + }), 48 + }), 49 + }) 50 50 51 - if (activeDid.value) { 52 - resumeSession(activeDid.value) 53 - } else { 54 - isLoading.value = false 55 - } 56 - } 51 + if (activeDid.value) { 52 + resumeSession(activeDid.value) 53 + } else { 54 + isLoading.value = false 55 + } 56 + } 57 57 58 - async function resumeSession(did: string) { 59 - try { 60 - isLoading.value = true 61 - const restoredSession = await getSession(did as DidString, { 62 - allowStale: true, 63 - }) 64 - session.value = restoredSession 65 - agent.value = markRaw(new OAuthUserAgent(restoredSession)) 66 - } catch (err) { 67 - console.error('Failed to resume session', err) 68 - } finally { 69 - isLoading.value = false 70 - } 71 - } 58 + async function resumeSession(did: string) { 59 + try { 60 + isLoading.value = true 61 + const restoredSession = await getSession(did as DidString, { 62 + allowStale: true, 63 + }) 64 + session.value = restoredSession 65 + agent.value = markRaw(new OAuthUserAgent(restoredSession)) 66 + } catch (err) { 67 + console.error('Failed to resume session', err) 68 + } finally { 69 + isLoading.value = false 70 + } 71 + } 72 72 73 - async function login(input: string) { 74 - isLoading.value = true 75 - error.value = null 73 + async function login(input: string) { 74 + isLoading.value = true 75 + error.value = null 76 76 77 - try { 78 - const scope = import.meta.env.VITE_OAUTH_SCOPE 79 - let authUrl: URL 77 + try { 78 + const scope = import.meta.env.VITE_OAUTH_SCOPE 79 + let authUrl: URL 80 80 81 - if (input.startsWith('http://') || input.startsWith('https://')) { 82 - authUrl = await createAuthorizationUrl({ 83 - target: { type: 'pds', serviceUrl: input }, 84 - scope, 85 - }) 86 - } else { 87 - try { 88 - authUrl = await createAuthorizationUrl({ 89 - target: { type: 'account', identifier: input as ActorIdentifier }, 90 - scope, 91 - }) 92 - } catch (err) { 93 - if (err instanceof Error && err.message === 'failed to resolve handle') { 94 - authUrl = await attemptFallbackPdsLogin(input, scope) 95 - } else { 96 - console.error(err) 97 - throw err 98 - } 99 - } 100 - } 81 + if (input.startsWith('http://') || input.startsWith('https://')) { 82 + authUrl = await createAuthorizationUrl({ 83 + target: { type: 'pds', serviceUrl: input }, 84 + scope, 85 + }) 86 + } else { 87 + try { 88 + authUrl = await createAuthorizationUrl({ 89 + target: { type: 'account', identifier: input as ActorIdentifier }, 90 + scope, 91 + }) 92 + } catch (err) { 93 + if (err instanceof Error && err.message === 'failed to resolve handle') { 94 + authUrl = await attemptFallbackPdsLogin(input, scope) 95 + } else { 96 + console.error(err) 97 + throw err 98 + } 99 + } 100 + } 101 101 102 - window.location.assign(authUrl) 103 - } catch (err) { 104 - isLoading.value = false 105 - error.value = err instanceof Error ? err.message : 'Failed to start login' 106 - console.log(error) 107 - } 108 - } 102 + window.location.assign(authUrl) 103 + } catch (err) { 104 + isLoading.value = false 105 + error.value = err instanceof Error ? err.message : 'Failed to start login' 106 + console.log(error) 107 + } 108 + } 109 109 110 - async function attemptFallbackPdsLogin(input: string, scope: string): Promise<URL> { 111 - const serviceUrl = `https://${input}` 110 + async function attemptFallbackPdsLogin(input: string, scope: string): Promise<URL> { 111 + const serviceUrl = `https://${input}` 112 112 113 - const handler = simpleFetchHandler({ service: serviceUrl }) 114 - const client = new Client({ handler }) 115 - const { ok } = await client.get('com.atproto.server.describeServer') 113 + const handler = simpleFetchHandler({ service: serviceUrl }) 114 + const client = new Client({ handler }) 115 + const { ok } = await client.get('com.atproto.server.describeServer') 116 116 117 - if (!ok) throw new Error('Failed to resolve handle and server is not a valid PDS') 117 + if (!ok) throw new Error('Failed to resolve handle and server is not a valid PDS') 118 118 119 - return createAuthorizationUrl({ 120 - target: { type: 'pds', serviceUrl }, 121 - scope, 122 - }) 123 - } 119 + return createAuthorizationUrl({ 120 + target: { type: 'pds', serviceUrl }, 121 + scope, 122 + }) 123 + } 124 124 125 - async function handleCallback() { 126 - if (session.value) return true 127 - if (isProcessingCallback.value) return true 125 + async function handleCallback() { 126 + if (session.value) return true 127 + if (isProcessingCallback.value) return true 128 128 129 - const hash = window.location.hash 130 - if (!hash || hash.length < 2) { 131 - return false 132 - } 129 + const hash = window.location.hash 130 + if (!hash || hash.length < 2) { 131 + return false 132 + } 133 133 134 - try { 135 - isProcessingCallback.value = true 136 - isLoading.value = true 134 + try { 135 + isProcessingCallback.value = true 136 + isLoading.value = true 137 137 138 - const params = new URLSearchParams(hash.slice(1)) 139 - window.history.replaceState(null, '', window.location.pathname + window.location.search) 138 + const params = new URLSearchParams(hash.slice(1)) 139 + window.history.replaceState(null, '', window.location.pathname + window.location.search) 140 140 141 - const result = await finalizeAuthorization(params) 142 - session.value = result.session 143 - agent.value = markRaw(new OAuthUserAgent(result.session)) 141 + const result = await finalizeAuthorization(params) 142 + session.value = result.session 143 + agent.value = markRaw(new OAuthUserAgent(result.session)) 144 144 145 - activeDid.value = result.session.info.sub 146 - localStorage.setItem('scilla-active-did', result.session.info.sub) 145 + activeDid.value = result.session.info.sub 146 + localStorage.setItem('bluebell-active-did', result.session.info.sub) 147 147 148 - return true 149 - } catch (err: unknown) { 150 - console.log(err) 151 - if (err instanceof Error) error.value = err.message || 'Login failed' 152 - return false 153 - } finally { 154 - isLoading.value = false 155 - isProcessingCallback.value = false 156 - } 157 - } 148 + return true 149 + } catch (err: unknown) { 150 + console.log(err) 151 + if (err instanceof Error) error.value = err.message || 'Login failed' 152 + return false 153 + } finally { 154 + isLoading.value = false 155 + isProcessingCallback.value = false 156 + } 157 + } 158 158 159 - async function logout() { 160 - if (activeDid.value) { 161 - try { 162 - if (agent.value) await agent.value.signOut() 163 - } catch (e) { 164 - console.error('Error during sign out', e) 165 - deleteStoredSession(activeDid.value as DidString) 166 - } 167 - } 159 + async function logout() { 160 + if (activeDid.value) { 161 + try { 162 + if (agent.value) await agent.value.signOut() 163 + } catch (e) { 164 + console.error('Error during sign out', e) 165 + deleteStoredSession(activeDid.value as DidString) 166 + } 167 + } 168 168 169 - session.value = null 170 - agent.value = null 171 - activeDid.value = null 172 - localStorage.removeItem('scilla-active-did') 173 - } 169 + session.value = null 170 + agent.value = null 171 + activeDid.value = null 172 + localStorage.removeItem('bluebell-active-did') 173 + } 174 174 175 - function getRpc() { 176 - if (!agent.value) throw new Error('Not logged in') 177 - return new Client({ handler: agent.value }) 178 - } 175 + function getRpc() { 176 + if (!agent.value) throw new Error('Not logged in') 177 + return new Client({ handler: agent.value }) 178 + } 179 179 180 - return { 181 - session, 182 - isAuthenticated, 183 - isLoading, 184 - error, 185 - userDid, 186 - init, 187 - login, 188 - handleCallback, 189 - logout, 190 - getRpc, 191 - } 180 + return { 181 + session, 182 + isAuthenticated, 183 + isLoading, 184 + error, 185 + userDid, 186 + init, 187 + login, 188 + handleCallback, 189 + logout, 190 + getRpc, 191 + } 192 192 })
+252 -252
src/stores/theme.ts
··· 3 3 import { useEnvironmentStore } from './environment' 4 4 5 5 export interface ThemeDefinition { 6 - id: string 7 - name: string 8 - type: 'light' | 'dark' 9 - variables: Record<string, string> 6 + id: string 7 + name: string 8 + type: 'light' | 'dark' 9 + variables: Record<string, string> 10 10 } 11 11 12 12 const latte: ThemeDefinition = { 13 - id: 'latte', 14 - name: 'Latte', 15 - type: 'light', 16 - variables: { 17 - rosewater: '10.8 58.824% 66.667%', 18 - flamingo: '0 59.763% 66.863%', 19 - pink: '316.034 73.418% 69.02%', 20 - mauve: '266.044 85.047% 58.039%', 21 - red: '347.077 86.667% 44.118%', 22 - maroon: '354.783 76.303% 58.627%', 23 - peach: '21.975 99.184% 51.961%', 24 - yellow: '34.948 76.984% 49.412%', 25 - green: '109.231 57.635% 39.804%', 26 - teal: '183.231 73.864% 34.51%', 27 - sky: '197.067 96.567% 45.686%', 28 - sapphire: '188.859 69.953% 41.765%', 29 - blue: '219.907 91.489% 53.922%', 30 - lavender: '230.935 97.203% 71.961%', 31 - text: '233.793 16.022% 35.49%', 32 - subtext1: '233.333 12.796% 41.373%', 33 - subtext0: '232.8 10.373% 47.255%', 34 - overlay2: '232.174 9.623% 53.137%', 35 - overlay1: '231.429 10.048% 59.02%', 36 - overlay0: '228 11.236% 65.098%', 37 - surface2: '226.667 12.162% 70.98%', 38 - surface1: '225 13.559% 76.863%', 39 - surface0: '222.857 15.909% 82.745%', 40 - base: '220 23.077% 94.902%', 41 - mantle: '220 21.951% 91.961%', 42 - crust: '220 20.69% 88.627%', 43 - }, 13 + id: 'latte', 14 + name: 'Latte', 15 + type: 'light', 16 + variables: { 17 + rosewater: '10.8 58.824% 66.667%', 18 + flamingo: '0 59.763% 66.863%', 19 + pink: '316.034 73.418% 69.02%', 20 + mauve: '266.044 85.047% 58.039%', 21 + red: '347.077 86.667% 44.118%', 22 + maroon: '354.783 76.303% 58.627%', 23 + peach: '21.975 99.184% 51.961%', 24 + yellow: '34.948 76.984% 49.412%', 25 + green: '109.231 57.635% 39.804%', 26 + teal: '183.231 73.864% 34.51%', 27 + sky: '197.067 96.567% 45.686%', 28 + sapphire: '188.859 69.953% 41.765%', 29 + blue: '219.907 91.489% 53.922%', 30 + lavender: '230.935 97.203% 71.961%', 31 + text: '233.793 16.022% 35.49%', 32 + subtext1: '233.333 12.796% 41.373%', 33 + subtext0: '232.8 10.373% 47.255%', 34 + overlay2: '232.174 9.623% 53.137%', 35 + overlay1: '231.429 10.048% 59.02%', 36 + overlay0: '228 11.236% 65.098%', 37 + surface2: '226.667 12.162% 70.98%', 38 + surface1: '225 13.559% 76.863%', 39 + surface0: '222.857 15.909% 82.745%', 40 + base: '220 23.077% 94.902%', 41 + mantle: '220 21.951% 91.961%', 42 + crust: '220 20.69% 88.627%', 43 + }, 44 44 } 45 45 46 46 const frappe: ThemeDefinition = { 47 - id: 'frappe', 48 - name: 'Frappé', 49 - type: 'dark', 50 - variables: { 51 - rosewater: '10.286 57.377% 88.039%', 52 - flamingo: '0 58.537% 83.922%', 53 - pink: '316 73.171% 83.922%', 54 - mauve: '276.667 59.016% 76.078%', 55 - red: '358.812 67.785% 70.784%', 56 - maroon: '357.778 65.854% 75.882%', 57 - peach: '20.331 79.085% 70%', 58 - yellow: '39.529 62.044% 73.137%', 59 - green: '95.833 43.902% 67.843%', 60 - teal: '171.549 39.227% 64.51%', 61 - sky: '189.091 47.826% 72.941%', 62 - sapphire: '198.621 55.414% 69.216%', 63 - blue: '221.633 74.242% 74.118%', 64 - lavender: '238.909 66.265% 83.725%', 65 - text: '227.234 70.149% 86.863%', 66 - subtext1: '226.667 43.689% 79.804%', 67 - subtext0: '228.293 29.496% 72.745%', 68 - overlay2: '227.692 22.286% 65.686%', 69 - overlay1: '226.667 16.981% 58.431%', 70 - overlay0: '229.091 13.36% 51.569%', 71 - surface2: '228 13.274% 44.314%', 72 - surface1: '227.143 14.737% 37.255%', 73 - surface0: '230 15.584% 30.196%', 74 - base: '229.091 18.644% 23.137%', 75 - mantle: '230.526 18.812% 19.804%', 76 - crust: '229.412 19.54% 17.059%', 77 - }, 47 + id: 'frappe', 48 + name: 'Frappé', 49 + type: 'dark', 50 + variables: { 51 + rosewater: '10.286 57.377% 88.039%', 52 + flamingo: '0 58.537% 83.922%', 53 + pink: '316 73.171% 83.922%', 54 + mauve: '276.667 59.016% 76.078%', 55 + red: '358.812 67.785% 70.784%', 56 + maroon: '357.778 65.854% 75.882%', 57 + peach: '20.331 79.085% 70%', 58 + yellow: '39.529 62.044% 73.137%', 59 + green: '95.833 43.902% 67.843%', 60 + teal: '171.549 39.227% 64.51%', 61 + sky: '189.091 47.826% 72.941%', 62 + sapphire: '198.621 55.414% 69.216%', 63 + blue: '221.633 74.242% 74.118%', 64 + lavender: '238.909 66.265% 83.725%', 65 + text: '227.234 70.149% 86.863%', 66 + subtext1: '226.667 43.689% 79.804%', 67 + subtext0: '228.293 29.496% 72.745%', 68 + overlay2: '227.692 22.286% 65.686%', 69 + overlay1: '226.667 16.981% 58.431%', 70 + overlay0: '229.091 13.36% 51.569%', 71 + surface2: '228 13.274% 44.314%', 72 + surface1: '227.143 14.737% 37.255%', 73 + surface0: '230 15.584% 30.196%', 74 + base: '229.091 18.644% 23.137%', 75 + mantle: '230.526 18.812% 19.804%', 76 + crust: '229.412 19.54% 17.059%', 77 + }, 78 78 } 79 79 80 80 const macchiato: ThemeDefinition = { 81 - id: 'macchiato', 82 - name: 'Macchiato', 83 - type: 'dark', 84 - variables: { 85 - rosewater: '10 57.692% 89.804%', 86 - flamingo: '0 58.333% 85.882%', 87 - pink: '316.071 73.684% 85.098%', 88 - mauve: '266.512 82.692% 79.608%', 89 - red: '351.176 73.913% 72.941%', 90 - maroon: '355.059 71.429% 76.667%', 91 - peach: '21.356 85.507% 72.941%', 92 - yellow: '40.253 69.912% 77.843%', 93 - green: '105.217 48.252% 71.961%', 94 - teal: '171.081 46.835% 69.02%', 95 - sky: '188.78 59.42% 72.941%', 96 - sapphire: '198.641 65.605% 69.216%', 97 - blue: '220.189 82.813% 74.902%', 98 - lavender: '234.462 82.278% 84.51%', 99 - text: '227.442 68.254% 87.647%', 100 - subtext1: '228 39.216% 80%', 101 - subtext0: '227.368 26.761% 72.157%', 102 - overlay2: '228.333 20% 64.706%', 103 - overlay1: '227.647 15.455% 56.863%', 104 - overlay0: '230.323 12.351% 49.216%', 105 - surface2: '229.655 13.744% 41.373%', 106 - surface1: '231.111 15.607% 33.922%', 107 - surface0: '230.4 18.797% 26.078%', 108 - base: '231.818 23.404% 18.431%', 109 - mantle: '233.333 23.077% 15.294%', 110 - crust: '235.714 22.581% 12.157%', 111 - }, 81 + id: 'macchiato', 82 + name: 'Macchiato', 83 + type: 'dark', 84 + variables: { 85 + rosewater: '10 57.692% 89.804%', 86 + flamingo: '0 58.333% 85.882%', 87 + pink: '316.071 73.684% 85.098%', 88 + mauve: '266.512 82.692% 79.608%', 89 + red: '351.176 73.913% 72.941%', 90 + maroon: '355.059 71.429% 76.667%', 91 + peach: '21.356 85.507% 72.941%', 92 + yellow: '40.253 69.912% 77.843%', 93 + green: '105.217 48.252% 71.961%', 94 + teal: '171.081 46.835% 69.02%', 95 + sky: '188.78 59.42% 72.941%', 96 + sapphire: '198.641 65.605% 69.216%', 97 + blue: '220.189 82.813% 74.902%', 98 + lavender: '234.462 82.278% 84.51%', 99 + text: '227.442 68.254% 87.647%', 100 + subtext1: '228 39.216% 80%', 101 + subtext0: '227.368 26.761% 72.157%', 102 + overlay2: '228.333 20% 64.706%', 103 + overlay1: '227.647 15.455% 56.863%', 104 + overlay0: '230.323 12.351% 49.216%', 105 + surface2: '229.655 13.744% 41.373%', 106 + surface1: '231.111 15.607% 33.922%', 107 + surface0: '230.4 18.797% 26.078%', 108 + base: '231.818 23.404% 18.431%', 109 + mantle: '233.333 23.077% 15.294%', 110 + crust: '235.714 22.581% 12.157%', 111 + }, 112 112 } 113 113 114 114 const mocha: ThemeDefinition = { 115 - id: 'mocha', 116 - name: 'Mocha', 117 - type: 'dark', 118 - variables: { 119 - rosewater: '9.6 55.556% 91.176%', 120 - flamingo: '0 58.73% 87.647%', 121 - pink: '316.471 71.831% 86.078%', 122 - mauve: '267.407 83.505% 80.98%', 123 - red: '343.269 81.25% 74.902%', 124 - maroon: '350.4 65.217% 77.451%', 125 - peach: '22.957 92% 75.49%', 126 - yellow: '41.351 86.047% 83.137%', 127 - green: '115.455 54.098% 76.078%', 128 - teal: '170 57.353% 73.333%', 129 - sky: '189.184 71.014% 72.941%', 130 - sapphire: '198.5 75.949% 69.02%', 131 - blue: '217.168 91.87% 75.882%', 132 - lavender: '231.892 97.368% 85.098%', 133 - text: '226.154 63.934% 88.039%', 134 - subtext1: '226.667 35.294% 80%', 135 - subtext0: '227.647 23.611% 71.765%', 136 - overlay2: '228.387 16.757% 63.725%', 137 - overlay1: '229.655 12.775% 55.49%', 138 - overlay0: '230.769 10.744% 47.451%', 139 - surface2: '232.5 12% 39.216%', 140 - surface1: '234.286 13.208% 31.176%', 141 - surface0: '236.842 16.239% 22.941%', 142 - base: '240 21.053% 14.902%', 143 - mantle: '240 21.311% 11.961%', 144 - crust: '240 22.727% 8.627%', 145 - }, 115 + id: 'mocha', 116 + name: 'Mocha', 117 + type: 'dark', 118 + variables: { 119 + rosewater: '9.6 55.556% 91.176%', 120 + flamingo: '0 58.73% 87.647%', 121 + pink: '316.471 71.831% 86.078%', 122 + mauve: '267.407 83.505% 80.98%', 123 + red: '343.269 81.25% 74.902%', 124 + maroon: '350.4 65.217% 77.451%', 125 + peach: '22.957 92% 75.49%', 126 + yellow: '41.351 86.047% 83.137%', 127 + green: '115.455 54.098% 76.078%', 128 + teal: '170 57.353% 73.333%', 129 + sky: '189.184 71.014% 72.941%', 130 + sapphire: '198.5 75.949% 69.02%', 131 + blue: '217.168 91.87% 75.882%', 132 + lavender: '231.892 97.368% 85.098%', 133 + text: '226.154 63.934% 88.039%', 134 + subtext1: '226.667 35.294% 80%', 135 + subtext0: '227.647 23.611% 71.765%', 136 + overlay2: '228.387 16.757% 63.725%', 137 + overlay1: '229.655 12.775% 55.49%', 138 + overlay0: '230.769 10.744% 47.451%', 139 + surface2: '232.5 12% 39.216%', 140 + surface1: '234.286 13.208% 31.176%', 141 + surface0: '236.842 16.239% 22.941%', 142 + base: '240 21.053% 14.902%', 143 + mantle: '240 21.311% 11.961%', 144 + crust: '240 22.727% 8.627%', 145 + }, 146 146 } 147 147 148 148 export const AccentColours = [ 149 - 'rosewater', 150 - 'flamingo', 151 - 'pink', 152 - 'mauve', 153 - 'red', 154 - 'maroon', 155 - 'peach', 156 - 'yellow', 157 - 'green', 158 - 'teal', 159 - 'sky', 160 - 'sapphire', 161 - 'blue', 162 - 'lavender', 149 + 'rosewater', 150 + 'flamingo', 151 + 'pink', 152 + 'mauve', 153 + 'red', 154 + 'maroon', 155 + 'peach', 156 + 'yellow', 157 + 'green', 158 + 'teal', 159 + 'sky', 160 + 'sapphire', 161 + 'blue', 162 + 'lavender', 163 163 ] as const 164 164 export type AccentColour = (typeof AccentColours)[number] 165 165 export const themes = [latte, frappe, macchiato, mocha] 166 166 167 167 const STORAGE_KEYS = { 168 - FOLLOW_SYSTEM: 'scilla-theme-follow', 169 - PREFERRED_LIGHT: 'scilla-theme-light', 170 - PREFERRED_DARK: 'scilla-theme-dark', 171 - CURRENT_MODE: 'scilla-theme-mode', 172 - ACCENT_COLOUR: 'scilla-theme-accent', 168 + FOLLOW_SYSTEM: 'bluebell-theme-follow', 169 + PREFERRED_LIGHT: 'bluebell-theme-light', 170 + PREFERRED_DARK: 'bluebell-theme-dark', 171 + CURRENT_MODE: 'bluebell-theme-mode', 172 + ACCENT_COLOUR: 'bluebell-theme-accent', 173 173 } 174 174 175 175 export const useThemeStore = defineStore('theme', () => { 176 - const env = useEnvironmentStore() 176 + const env = useEnvironmentStore() 177 177 178 - const followSystem = ref(true) 179 - const preferredLight = ref<string>('scilla-light') 180 - const preferredDark = ref<string>('scilla-dark') 181 - const currentMode = ref<'light' | 'dark'>('dark') 182 - const preferredAccent = ref<AccentColour>('mauve') 178 + const followSystem = ref(true) 179 + const preferredLight = ref<string>('latte') 180 + const preferredDark = ref<string>('mocha') 181 + const currentMode = ref<'light' | 'dark'>('dark') 182 + const preferredAccent = ref<AccentColour>('mauve') 183 183 184 - const activeTheme = computed(() => { 185 - let targetId: string 184 + const activeTheme = computed(() => { 185 + let targetId: string 186 186 187 - if (followSystem.value) { 188 - targetId = env.prefersDarkScheme ? preferredDark.value : preferredLight.value 189 - } else { 190 - targetId = currentMode.value === 'dark' ? preferredDark.value : preferredLight.value 191 - } 187 + if (followSystem.value) { 188 + targetId = env.prefersDarkScheme ? preferredDark.value : preferredLight.value 189 + } else { 190 + targetId = currentMode.value === 'dark' ? preferredDark.value : preferredLight.value 191 + } 192 192 193 - return themes.find((t) => t.id === targetId) || mocha 194 - }) 193 + return themes.find((t) => t.id === targetId) || mocha 194 + }) 195 195 196 - function setFollowSystem(val: boolean) { 197 - followSystem.value = val 198 - } 196 + function setFollowSystem(val: boolean) { 197 + followSystem.value = val 198 + } 199 199 200 - function setPreferredLight(themeId: string) { 201 - if (themes.find((t) => t.id === themeId && t.type === 'light')) { 202 - preferredLight.value = themeId 203 - currentMode.value = 'light' 204 - } 205 - } 200 + function setPreferredLight(themeId: string) { 201 + if (themes.find((t) => t.id === themeId && t.type === 'light')) { 202 + preferredLight.value = themeId 203 + currentMode.value = 'light' 204 + } 205 + } 206 206 207 - function setPreferredDark(themeId: string) { 208 - if (themes.find((t) => t.id === themeId && t.type === 'dark')) { 209 - preferredDark.value = themeId 210 - currentMode.value = 'dark' 211 - } 212 - } 207 + function setPreferredDark(themeId: string) { 208 + if (themes.find((t) => t.id === themeId && t.type === 'dark')) { 209 + preferredDark.value = themeId 210 + currentMode.value = 'dark' 211 + } 212 + } 213 213 214 - function setAccent(colour: AccentColour) { 215 - if (AccentColours.includes(colour)) { 216 - preferredAccent.value = colour 217 - } 218 - } 214 + function setAccent(colour: AccentColour) { 215 + if (AccentColours.includes(colour)) { 216 + preferredAccent.value = colour 217 + } 218 + } 219 219 220 - function applyTheme() { 221 - const root = document.documentElement 222 - const theme = activeTheme.value 223 - const accentKey = preferredAccent.value 220 + function applyTheme() { 221 + const root = document.documentElement 222 + const theme = activeTheme.value 223 + const accentKey = preferredAccent.value 224 224 225 - Object.entries(theme.variables).forEach(([key, value]) => { 226 - root.style.setProperty(`--${key}`, value) 227 - }) 225 + Object.entries(theme.variables).forEach(([key, value]) => { 226 + root.style.setProperty(`--${key}`, value) 227 + }) 228 228 229 - const accentValue = theme.variables[accentKey] 230 - if (accentValue) root.style.setProperty('--accent', accentValue) 229 + const accentValue = theme.variables[accentKey] 230 + if (accentValue) root.style.setProperty('--accent', accentValue) 231 231 232 - root.setAttribute('data-theme', theme.id) 232 + root.setAttribute('data-theme', theme.id) 233 233 234 - const metaThemeColor = document.querySelector('meta[name="theme-colour"]') 235 - if (metaThemeColor) { 236 - metaThemeColor.setAttribute('content', `hsl(${theme.variables.mantle})`) 237 - } 238 - } 234 + const metaThemeColor = document.querySelector('meta[name="theme-colour"]') 235 + if (metaThemeColor) { 236 + metaThemeColor.setAttribute('content', `hsl(${theme.variables.mantle})`) 237 + } 238 + } 239 239 240 - function init() { 241 - const storedFollow = localStorage.getItem(STORAGE_KEYS.FOLLOW_SYSTEM) 242 - if (storedFollow !== null) { 243 - followSystem.value = storedFollow === 'true' 244 - } 240 + function init() { 241 + const storedFollow = localStorage.getItem(STORAGE_KEYS.FOLLOW_SYSTEM) 242 + if (storedFollow !== null) { 243 + followSystem.value = storedFollow === 'true' 244 + } 245 245 246 - const storedLight = localStorage.getItem(STORAGE_KEYS.PREFERRED_LIGHT) 247 - if (storedLight && themes.some((t) => t.id === storedLight)) { 248 - preferredLight.value = storedLight 249 - } 246 + const storedLight = localStorage.getItem(STORAGE_KEYS.PREFERRED_LIGHT) 247 + if (storedLight && themes.some((t) => t.id === storedLight)) { 248 + preferredLight.value = storedLight 249 + } 250 250 251 - const storedDark = localStorage.getItem(STORAGE_KEYS.PREFERRED_DARK) 252 - if (storedDark && themes.some((t) => t.id === storedDark)) { 253 - preferredDark.value = storedDark 254 - } 251 + const storedDark = localStorage.getItem(STORAGE_KEYS.PREFERRED_DARK) 252 + if (storedDark && themes.some((t) => t.id === storedDark)) { 253 + preferredDark.value = storedDark 254 + } 255 255 256 - const storedMode = localStorage.getItem(STORAGE_KEYS.CURRENT_MODE) 257 - if (storedMode === 'light' || storedMode === 'dark') { 258 - currentMode.value = storedMode 259 - } else { 260 - currentMode.value = env.prefersDarkScheme ? 'dark' : 'light' 261 - } 256 + const storedMode = localStorage.getItem(STORAGE_KEYS.CURRENT_MODE) 257 + if (storedMode === 'light' || storedMode === 'dark') { 258 + currentMode.value = storedMode 259 + } else { 260 + currentMode.value = env.prefersDarkScheme ? 'dark' : 'light' 261 + } 262 262 263 - const storedAccent = localStorage.getItem(STORAGE_KEYS.ACCENT_COLOUR) as AccentColour 264 - if (storedAccent && AccentColours.includes(storedAccent)) { 265 - preferredAccent.value = storedAccent 266 - } 263 + const storedAccent = localStorage.getItem(STORAGE_KEYS.ACCENT_COLOUR) as AccentColour 264 + if (storedAccent && AccentColours.includes(storedAccent)) { 265 + preferredAccent.value = storedAccent 266 + } 267 267 268 - watch(followSystem, (val) => { 269 - localStorage.setItem(STORAGE_KEYS.FOLLOW_SYSTEM, String(val)) 270 - if (!val) { 271 - currentMode.value = env.prefersDarkScheme ? 'dark' : 'light' 272 - } 273 - }) 274 - watch(preferredLight, (val) => localStorage.setItem(STORAGE_KEYS.PREFERRED_LIGHT, val)) 275 - watch(preferredDark, (val) => localStorage.setItem(STORAGE_KEYS.PREFERRED_DARK, val)) 276 - watch(currentMode, (val) => localStorage.setItem(STORAGE_KEYS.CURRENT_MODE, val)) 277 - watch(preferredAccent, (val) => localStorage.setItem(STORAGE_KEYS.ACCENT_COLOUR, val)) 268 + watch(followSystem, (val) => { 269 + localStorage.setItem(STORAGE_KEYS.FOLLOW_SYSTEM, String(val)) 270 + if (!val) { 271 + currentMode.value = env.prefersDarkScheme ? 'dark' : 'light' 272 + } 273 + }) 274 + watch(preferredLight, (val) => localStorage.setItem(STORAGE_KEYS.PREFERRED_LIGHT, val)) 275 + watch(preferredDark, (val) => localStorage.setItem(STORAGE_KEYS.PREFERRED_DARK, val)) 276 + watch(currentMode, (val) => localStorage.setItem(STORAGE_KEYS.CURRENT_MODE, val)) 277 + watch(preferredAccent, (val) => localStorage.setItem(STORAGE_KEYS.ACCENT_COLOUR, val)) 278 278 279 - watch( 280 - [activeTheme, preferredAccent, () => env.prefersDarkScheme], 281 - () => { 282 - applyTheme() 283 - }, 284 - { immediate: true }, 285 - ) 286 - } 279 + watch( 280 + [activeTheme, preferredAccent, () => env.prefersDarkScheme], 281 + () => { 282 + applyTheme() 283 + }, 284 + { immediate: true }, 285 + ) 286 + } 287 287 288 - return { 289 - themes, 290 - AccentColours, 291 - followSystem, 292 - preferredLight, 293 - preferredDark, 294 - preferredAccent, 295 - activeTheme, 296 - setFollowSystem, 297 - setPreferredLight, 298 - setPreferredDark, 299 - setAccent, 300 - init, 301 - } 288 + return { 289 + themes, 290 + AccentColours, 291 + followSystem, 292 + preferredLight, 293 + preferredDark, 294 + preferredAccent, 295 + activeTheme, 296 + setFollowSystem, 297 + setPreferredLight, 298 + setPreferredDark, 299 + setAccent, 300 + init, 301 + } 302 302 })
+188 -188
src/views/Auth/LoginPage.vue
··· 1 1 <script setup lang="ts"> 2 2 import { ref, watch } from 'vue' 3 3 import { 4 - IconArrowForwardRounded, 5 - IconOpenInNewRounded, 6 - IconAlternateEmailRounded, 7 - IconCloseRounded, 8 - IconProgressActivity, 4 + IconArrowForwardRounded, 5 + IconOpenInNewRounded, 6 + IconAlternateEmailRounded, 7 + IconCloseRounded, 8 + IconProgressActivity, 9 9 } from '@iconify-prerendered/vue-material-symbols' 10 10 import { simpleFetchHandler, Client, ok } from '@atcute/client' 11 11 ··· 28 28 const hasError = ref(false) 29 29 30 30 const handleSubmit = async () => { 31 - if (!handle.value) return 32 - await auth.login(handle.value) 31 + if (!handle.value) return 32 + await auth.login(handle.value) 33 33 } 34 34 const pdsSubmit = async (pds: string) => { 35 - await auth.login(pds) 35 + await auth.login(pds) 36 36 } 37 37 38 38 let debounceTimer: ReturnType<typeof setTimeout> 39 39 40 40 watch(handle, (newHandle) => { 41 - clearTimeout(debounceTimer) 42 - hasError.value = false 43 - loading.value = true 41 + clearTimeout(debounceTimer) 42 + hasError.value = false 43 + loading.value = true 44 44 45 - if (!newHandle) { 46 - profilePicture.value = null 47 - loading.value = false 48 - return 49 - } 45 + if (!newHandle) { 46 + profilePicture.value = null 47 + loading.value = false 48 + return 49 + } 50 50 51 - debounceTimer = setTimeout(async () => { 52 - try { 53 - const cleanHandle = newHandle.startsWith('@') ? newHandle.slice(1) : newHandle 54 - const data = ok( 55 - await client.get('app.bsky.actor.getProfile', { 56 - params: { actor: cleanHandle }, 57 - }), 58 - ) 51 + debounceTimer = setTimeout(async () => { 52 + try { 53 + const cleanHandle = newHandle.startsWith('@') ? newHandle.slice(1) : newHandle 54 + const data = ok( 55 + await client.get('app.bsky.actor.getProfile', { 56 + params: { actor: cleanHandle }, 57 + }), 58 + ) 59 59 60 - profilePicture.value = data.avatar || null 61 - hasError.value = false 62 - } catch (error) { 63 - profilePicture.value = null 64 - hasError.value = true 65 - console.error('Error fetching profile:', error) 66 - } finally { 67 - loading.value = false 68 - } 69 - }, 500) 60 + profilePicture.value = data.avatar || null 61 + hasError.value = false 62 + } catch (error) { 63 + profilePicture.value = null 64 + hasError.value = true 65 + console.error('Error fetching profile:', error) 66 + } finally { 67 + loading.value = false 68 + } 69 + }, 500) 70 70 }) 71 71 72 72 const providerList: Array<{ name: string; subtitle?: string; url: string }> = [ 73 - { name: 'Bluesky PBC', subtitle: 'bsky.social', url: 'https://bsky.social/' }, 74 - { name: 'selfhosted.social', url: 'https://selfhosted.social/' }, 75 - { 76 - name: 'Tophhie Social', 77 - subtitle: 'pds.tophhie.cloud', 78 - url: 'https://pds.tophhie.cloud', 79 - }, 80 - { name: 'Blacksky', subtitle: 'A PDS for the black community.', url: 'https://blacksky.app/' }, 81 - { name: 'Spark', subtitle: 'pds.sprk.so', url: 'https://pds.sprk.so' }, 73 + { name: 'Bluesky PBC', subtitle: 'bsky.social', url: 'https://bsky.social/' }, 74 + { name: 'selfhosted.social', url: 'https://selfhosted.social/' }, 75 + { 76 + name: 'Tophhie Social', 77 + subtitle: 'pds.tophhie.cloud', 78 + url: 'https://pds.tophhie.cloud', 79 + }, 80 + { name: 'Blacksky', subtitle: 'A PDS for the black community.', url: 'https://blacksky.app/' }, 81 + { name: 'Spark', subtitle: 'pds.sprk.so', url: 'https://pds.sprk.so' }, 82 82 ] 83 83 </script> 84 84 85 85 <template> 86 - <PageLayout title="Login"> 87 - <div class="login-view"> 88 - <div class="header"> 89 - <h1 class="title">Sign in</h1> 90 - <p class="subtitle">Enter your AT Protocol handle to continue.</p> 91 - </div> 86 + <PageLayout title="Login"> 87 + <div class="login-view"> 88 + <div class="header"> 89 + <h1 class="title">Sign in</h1> 90 + <p class="subtitle">Enter your AT Protocol handle to continue.</p> 91 + </div> 92 92 93 - <form @submit.prevent="handleSubmit" class="form-stack"> 94 - <TextInput 95 - v-model="handle" 96 - placeholder="awesome.cat" 97 - :error="auth.error || undefined" 98 - @keydown.enter="handleSubmit" 99 - autofocus 100 - > 101 - <template #prefix> 102 - <div v-if="loading" class="text-input-prefix spin"> 103 - <IconProgressActivity /> 104 - </div> 105 - <div v-else-if="profilePicture" class="input-avatar"> 106 - <img :src="profilePicture" alt="Avatar" /> 107 - </div> 108 - <div v-else class="text-input-prefix"> 109 - <IconCloseRounded v-if="hasError" style="color: hsl(var(--danger))" /> 110 - <IconAlternateEmailRounded v-else /> 111 - </div> 112 - </template> 113 - </TextInput> 93 + <form @submit.prevent="handleSubmit" class="form-stack"> 94 + <TextInput 95 + v-model="handle" 96 + placeholder="awesome.cat" 97 + :error="auth.error || undefined" 98 + @keydown.enter="handleSubmit" 99 + autofocus 100 + > 101 + <template #prefix> 102 + <div v-if="loading" class="text-input-prefix spin"> 103 + <IconProgressActivity /> 104 + </div> 105 + <div v-else-if="profilePicture" class="input-avatar"> 106 + <img :src="profilePicture" alt="Avatar" /> 107 + </div> 108 + <div v-else class="text-input-prefix"> 109 + <IconCloseRounded v-if="hasError" style="color: hsl(var(--danger))" /> 110 + <IconAlternateEmailRounded v-else /> 111 + </div> 112 + </template> 113 + </TextInput> 114 114 115 - <div class="actions"> 116 - <Button type="button" variant="subtle-alt" @click="showCreateAccount = true"> 117 - Create account 118 - </Button> 119 - <Button type="submit" variant="primary" :loading="auth.isLoading" :disabled="!handle"> 120 - Next <IconArrowForwardRounded /> 121 - </Button> 122 - </div> 123 - </form> 124 - </div> 115 + <div class="actions"> 116 + <Button type="button" variant="subtle-alt" @click="showCreateAccount = true"> 117 + Create account 118 + </Button> 119 + <Button type="submit" variant="primary" :loading="auth.isLoading" :disabled="!handle"> 120 + Next <IconArrowForwardRounded /> 121 + </Button> 122 + </div> 123 + </form> 124 + </div> 125 125 126 - <Modal v-model:open="showCreateAccount" title="Create Account"> 127 - <div class="create-account-content"> 128 - <p class="modal-text"> 129 - To use Scilla, you need to create an Atmosphere account. Here are some open providers 130 - where you can register. 131 - </p> 126 + <Modal v-model:open="showCreateAccount" title="Create Account"> 127 + <div class="create-account-content"> 128 + <p class="modal-text"> 129 + To use Bluebell, you need to create an Atmosphere account. Here are some open providers 130 + where you can register. 131 + </p> 132 132 133 - <div class="provider-list"> 134 - <ListItem 135 - v-for="provider in providerList" 136 - :key="provider.name" 137 - :title="provider.name" 138 - :subtitle="provider.subtitle" 139 - @click="pdsSubmit(provider.url)" 140 - clickable 141 - :disabled="auth.isLoading" 142 - > 143 - <template #end><IconOpenInNewRounded /></template> 144 - </ListItem> 145 - </div> 133 + <div class="provider-list"> 134 + <ListItem 135 + v-for="provider in providerList" 136 + :key="provider.name" 137 + :title="provider.name" 138 + :subtitle="provider.subtitle" 139 + @click="pdsSubmit(provider.url)" 140 + clickable 141 + :disabled="auth.isLoading" 142 + > 143 + <template #end><IconOpenInNewRounded /></template> 144 + </ListItem> 145 + </div> 146 146 147 - <p class="modal-subtext"> 148 - Make sure to to read the provider's terms of service and privacy policy before creating an 149 - account. 150 - </p> 151 - </div> 152 - <template #footer> 153 - <Button variant="ghost" @click="showCreateAccount = false">Close</Button> 154 - </template> 155 - </Modal> 156 - </PageLayout> 147 + <p class="modal-subtext"> 148 + Make sure to to read the provider's terms of service and privacy policy before creating an 149 + account. 150 + </p> 151 + </div> 152 + <template #footer> 153 + <Button variant="ghost" @click="showCreateAccount = false">Close</Button> 154 + </template> 155 + </Modal> 156 + </PageLayout> 157 157 </template> 158 158 159 159 <style scoped lang="scss"> 160 160 .login-view { 161 - max-width: 400px; 162 - width: 100%; 163 - display: flex; 164 - flex-direction: column; 165 - gap: 1rem; 161 + max-width: 400px; 162 + width: 100%; 163 + display: flex; 164 + flex-direction: column; 165 + gap: 1rem; 166 166 } 167 167 168 168 .header { 169 - display: flex; 170 - flex-direction: column; 171 - gap: 0.5rem; 169 + display: flex; 170 + flex-direction: column; 171 + gap: 0.5rem; 172 172 173 - .title { 174 - font-size: 2rem; 175 - font-weight: 800; 176 - color: hsl(var(--text)); 177 - letter-spacing: -0.02em; 178 - } 173 + .title { 174 + font-size: 2rem; 175 + font-weight: 800; 176 + color: hsl(var(--text)); 177 + letter-spacing: -0.02em; 178 + } 179 179 180 - .subtitle { 181 - font-size: 1rem; 182 - color: hsl(var(--subtext0)); 183 - line-height: 1.5; 184 - } 180 + .subtitle { 181 + font-size: 1rem; 182 + color: hsl(var(--subtext0)); 183 + line-height: 1.5; 184 + } 185 185 } 186 186 187 187 .form-stack { 188 - display: flex; 189 - flex-direction: column; 190 - gap: 1rem; 188 + display: flex; 189 + flex-direction: column; 190 + gap: 1rem; 191 191 192 - .actions { 193 - display: flex; 194 - justify-content: flex-end; 195 - gap: 1rem; 196 - } 192 + .actions { 193 + display: flex; 194 + justify-content: flex-end; 195 + gap: 1rem; 196 + } 197 197 198 - .text-input-prefix { 199 - display: flex; 200 - align-items: center; 198 + .text-input-prefix { 199 + display: flex; 200 + align-items: center; 201 201 202 - &.spin { 203 - animation: spin 1s linear infinite; 204 - color: hsl(var(--subtext0)); 202 + &.spin { 203 + animation: spin 1s linear infinite; 204 + color: hsl(var(--subtext0)); 205 205 206 - @keyframes spin { 207 - from { 208 - transform: rotate(0deg); 209 - } 210 - to { 211 - transform: rotate(360deg); 212 - } 213 - } 214 - } 215 - } 206 + @keyframes spin { 207 + from { 208 + transform: rotate(0deg); 209 + } 210 + to { 211 + transform: rotate(360deg); 212 + } 213 + } 214 + } 215 + } 216 216 217 - .input-avatar { 218 - display: flex; 219 - align-items: center; 220 - justify-content: center; 221 - padding-left: 0.5rem; 222 - padding-right: 0.5rem; 223 - height: 100%; 217 + .input-avatar { 218 + display: flex; 219 + align-items: center; 220 + justify-content: center; 221 + padding-left: 0.5rem; 222 + padding-right: 0.5rem; 223 + height: 100%; 224 224 225 - img { 226 - width: 2rem; 227 - height: 2rem; 225 + img { 226 + width: 2rem; 227 + height: 2rem; 228 228 229 - border-radius: 50%; 230 - object-fit: cover; 231 - background-color: hsl(var(--surface0)); 232 - border: 1px solid hsl(var(--surface2)); 233 - } 234 - } 229 + border-radius: 50%; 230 + object-fit: cover; 231 + background-color: hsl(var(--surface0)); 232 + border: 1px solid hsl(var(--surface2)); 233 + } 234 + } 235 235 } 236 236 237 237 .create-account-content { 238 - display: flex; 239 - flex-direction: column; 240 - gap: 1rem; 241 - padding-top: 0.5rem; 238 + display: flex; 239 + flex-direction: column; 240 + gap: 1rem; 241 + padding-top: 0.5rem; 242 242 243 - .modal-text { 244 - color: hsl(var(--text)); 245 - line-height: 1.5; 246 - } 243 + .modal-text { 244 + color: hsl(var(--text)); 245 + line-height: 1.5; 246 + } 247 247 248 - .modal-subtext { 249 - font-size: 0.875rem; 250 - color: hsl(var(--subtext0)); 251 - line-height: 1.4; 252 - } 248 + .modal-subtext { 249 + font-size: 0.875rem; 250 + color: hsl(var(--subtext0)); 251 + line-height: 1.4; 252 + } 253 253 254 - .provider-list { 255 - display: flex; 256 - flex-direction: column; 257 - border: 1px solid hsla(var(--surface2) / 0.5); 258 - border-radius: var(--radius-lg); 259 - overflow: hidden; 260 - } 254 + .provider-list { 255 + display: flex; 256 + flex-direction: column; 257 + border: 1px solid hsla(var(--surface2) / 0.5); 258 + border-radius: var(--radius-lg); 259 + overflow: hidden; 260 + } 261 261 } 262 262 </style>
+261 -261
src/views/Auth/OAuthCallback.vue
··· 2 2 import { onMounted, ref, computed } from 'vue' 3 3 import { useAuthStore } from '@/stores/auth' 4 4 import SVG from '@/components/UI/SVG.vue' 5 - import ScillaLogo from '@/assets/icons/scilla.svg?raw' 5 + import BluebellLogo from '@/assets/icons/bluebell.svg?raw' 6 6 import { IconArrowForwardRounded } from '@iconify-prerendered/vue-material-symbols' 7 7 import Button from '@/components/UI/BaseButton.vue' 8 8 const emit = defineEmits<{ 9 - (e: 'complete'): void 9 + (e: 'complete'): void 10 10 }>() 11 11 12 12 const auth = useAuthStore() ··· 21 21 const loadingBar = ref<HTMLElement | null>(null) 22 22 23 23 const displayName = computed(() => { 24 - if (!profile.value) return '' 25 - return profile.value.displayName || profile.value.handle 24 + if (!profile.value) return '' 25 + return profile.value.displayName || profile.value.handle 26 26 }) 27 27 28 28 const bannerUrl = computed(() => profile.value?.banner) 29 29 const avatarUrl = computed(() => profile.value?.avatar) 30 30 31 31 const proceed = async () => { 32 - isExiting.value = true 33 - await new Promise((r) => setTimeout(r, 600)) 34 - emit('complete') 32 + isExiting.value = true 33 + await new Promise((r) => setTimeout(r, 600)) 34 + emit('complete') 35 35 } 36 36 37 37 onMounted(async () => { 38 - try { 39 - const style = loadingBar.value?.style 38 + try { 39 + const style = loadingBar.value?.style 40 40 41 - loadingMessage.value = 'verifying... ' 42 - style?.setProperty('--scale', 1 / 3) 41 + loadingMessage.value = 'verifying... ' 42 + style?.setProperty('--scale', 1 / 3) 43 43 44 - const success = await auth.handleCallback() 45 - if (!success && !auth.isAuthenticated) throw new Error('Auth failed') 44 + const success = await auth.handleCallback() 45 + if (!success && !auth.isAuthenticated) throw new Error('Auth failed') 46 46 47 - loadingMessage.value = 'finding you...' 48 - style?.setProperty('--scale', 2 / 3) 47 + loadingMessage.value = 'finding you...' 48 + style?.setProperty('--scale', 2 / 3) 49 49 50 - const rpc = auth.getRpc() 51 - const { data } = await rpc.get('app.bsky.actor.getProfile', { 52 - params: { actor: auth.session?.info.sub }, 53 - }) 50 + const rpc = auth.getRpc() 51 + const { data } = await rpc.get('app.bsky.actor.getProfile', { 52 + params: { actor: auth.session?.info.sub }, 53 + }) 54 54 55 - if (data.banner) { 56 - const img = new Image() 57 - img.src = data.banner 58 - } 55 + if (data.banner) { 56 + const img = new Image() 57 + img.src = data.banner 58 + } 59 59 60 - profile.value = data 61 - loadingMessage.value = "nothing's happening this is just a spacer" 62 - style?.setProperty('--scale', '1') 60 + profile.value = data 61 + loadingMessage.value = "nothing's happening this is just a spacer" 62 + style?.setProperty('--scale', '1') 63 63 64 - await new Promise((r) => setTimeout(r, 750)) 65 - state.value = 'success' 66 - } catch (e: any) { 67 - console.error(e) 68 - state.value = 'error' 69 - errorMsg.value = e.message || 'Something went wrong' 70 - setTimeout(() => (window.location.href = '/login'), 3000) 71 - } 64 + await new Promise((r) => setTimeout(r, 750)) 65 + state.value = 'success' 66 + } catch (e: any) { 67 + console.error(e) 68 + state.value = 'error' 69 + errorMsg.value = e.message || 'Something went wrong' 70 + setTimeout(() => (window.location.href = '/login'), 3000) 71 + } 72 72 }) 73 73 </script> 74 74 75 75 <template> 76 - <div class="callback-screen" :class="{ 'is-exiting': isExiting }"> 77 - <div 78 - class="bg-layer" 79 - :class="{ 'has-image': !!bannerUrl && state === 'success' }" 80 - :style="{ 81 - backgroundImage: bannerUrl && state === 'success' ? `url(${bannerUrl})` : undefined, 82 - '--opacity': state === 'success' ? 1 : 0, 83 - }" 84 - > 85 - <div class="bg-overlay"></div> 86 - </div> 76 + <div class="callback-screen" :class="{ 'is-exiting': isExiting }"> 77 + <div 78 + class="bg-layer" 79 + :class="{ 'has-image': !!bannerUrl && state === 'success' }" 80 + :style="{ 81 + backgroundImage: bannerUrl && state === 'success' ? `url(${bannerUrl})` : undefined, 82 + '--opacity': state === 'success' ? 1 : 0, 83 + }" 84 + > 85 + <div class="bg-overlay"></div> 86 + </div> 87 87 88 - <Transition name="fade-up" mode="out-in"> 89 - <div v-if="state === 'loading'" class="content-layer loading-layout"> 90 - <div class="logo-mark"> 91 - <SVG :icon="ScillaLogo" /> 92 - </div> 93 - <div class="loading-bar-track"> 94 - <div class="loading-bar-fill" ref="loadingBar"></div> 95 - </div> 96 - <p class="loading-text">{{ loadingMessage }}</p> 97 - </div> 88 + <Transition name="fade-up" mode="out-in"> 89 + <div v-if="state === 'loading'" class="content-layer loading-layout"> 90 + <div class="logo-mark"> 91 + <SVG :icon="BluebellLogo" /> 92 + </div> 93 + <div class="loading-bar-track"> 94 + <div class="loading-bar-fill" ref="loadingBar"></div> 95 + </div> 96 + <p class="loading-text">{{ loadingMessage }}</p> 97 + </div> 98 98 99 - <div v-else-if="state === 'success'" class="content-layer success-layout"> 100 - <div class="avatar-hero"> 101 - <img v-if="avatarUrl" :src="avatarUrl" alt="Avatar" /> 102 - <div v-else class="avatar-fallback"><SVG :icon="ScillaLogo" /></div> 103 - </div> 99 + <div v-else-if="state === 'success'" class="content-layer success-layout"> 100 + <div class="avatar-hero"> 101 + <img v-if="avatarUrl" :src="avatarUrl" alt="Avatar" /> 102 + <div v-else class="avatar-fallback"><SVG :icon="BluebellLogo" /></div> 103 + </div> 104 104 105 - <div class="text-hero"> 106 - <h1 class="greeting">Hi,</h1> 107 - <h1 class="name">{{ displayName }}!</h1> 108 - </div> 105 + <div class="text-hero"> 106 + <h1 class="greeting">Hi,</h1> 107 + <h1 class="name">{{ displayName }}!</h1> 108 + </div> 109 109 110 - <Button class="start-btn" @click="proceed" size="lg" variant="primary"> 111 - <span>meow!</span> 112 - <IconArrowForwardRounded /> 113 - </Button> 114 - </div> 110 + <Button class="start-btn" @click="proceed" size="lg" variant="primary"> 111 + <span>meow!</span> 112 + <IconArrowForwardRounded /> 113 + </Button> 114 + </div> 115 115 116 - <div v-else-if="state === 'error'" class="content-layer error-layout"> 117 - <h1 class="error-title">Connection Failed</h1> 118 - <p class="error-desc">{{ errorMsg }}</p> 119 - </div> 120 - </Transition> 121 - </div> 116 + <div v-else-if="state === 'error'" class="content-layer error-layout"> 117 + <h1 class="error-title">Connection Failed</h1> 118 + <p class="error-desc">{{ errorMsg }}</p> 119 + </div> 120 + </Transition> 121 + </div> 122 122 </template> 123 123 124 124 <style scoped lang="scss"> 125 125 .callback-screen { 126 - position: fixed; 127 - inset: 0; 128 - background-color: hsl(var(--base)); 129 - display: flex; 130 - align-items: center; 131 - justify-content: center; 132 - overflow: hidden; 133 - z-index: 9999; 134 - perspective: 1000px; 126 + position: fixed; 127 + inset: 0; 128 + background-color: hsl(var(--base)); 129 + display: flex; 130 + align-items: center; 131 + justify-content: center; 132 + overflow: hidden; 133 + z-index: 9999; 134 + perspective: 1000px; 135 135 } 136 136 137 137 .bg-layer { 138 - position: absolute; 139 - inset: -20px; 140 - background-color: hsl(var(--base)); 141 - background-size: cover; 142 - background-position: center; 143 - opacity: var(--opacity, 0); 138 + position: absolute; 139 + inset: -20px; 140 + background-color: hsl(var(--base)); 141 + background-size: cover; 142 + background-position: center; 143 + opacity: var(--opacity, 0); 144 144 145 - transition: all 1s cubic-bezier(0.25, 1, 0.5, 1); 146 - will-change: transform, opacity, filter; 145 + transition: all 1s cubic-bezier(0.25, 1, 0.5, 1); 146 + will-change: transform, opacity, filter; 147 147 148 - &::before { 149 - content: ''; 150 - position: absolute; 151 - inset: 0; 152 - background: radial-gradient(circle at 50% 120%, hsla(var(--accent) / 0.15), transparent 70%); 153 - opacity: 1; 154 - transition: opacity 1s ease; 155 - } 148 + &::before { 149 + content: ''; 150 + position: absolute; 151 + inset: 0; 152 + background: radial-gradient(circle at 50% 120%, hsla(var(--accent) / 0.15), transparent 70%); 153 + opacity: 1; 154 + transition: opacity 1s ease; 155 + } 156 156 157 - &.has-image { 158 - filter: blur(40px) saturate(1.2); 159 - transform: scale(1.1); 160 - &::before { 161 - opacity: 1; 162 - } 163 - } 157 + &.has-image { 158 + filter: blur(40px) saturate(1.2); 159 + transform: scale(1.1); 160 + &::before { 161 + opacity: 1; 162 + } 163 + } 164 164 } 165 165 166 166 .bg-overlay { 167 - position: absolute; 168 - inset: 0; 169 - background: linear-gradient(to bottom, hsla(var(--base) / 0.8), hsla(var(--base) / 0.95)); 167 + position: absolute; 168 + inset: 0; 169 + background: linear-gradient(to bottom, hsla(var(--base) / 0.8), hsla(var(--base) / 0.95)); 170 170 } 171 171 172 172 .content-layer { 173 - position: relative; 174 - z-index: 10; 175 - width: 100%; 176 - max-width: 600px; 177 - padding: 2rem; 178 - display: flex; 179 - flex-direction: column; 180 - align-items: center; 181 - text-align: center; 173 + position: relative; 174 + z-index: 10; 175 + width: 100%; 176 + max-width: 600px; 177 + padding: 2rem; 178 + display: flex; 179 + flex-direction: column; 180 + align-items: center; 181 + text-align: center; 182 182 183 - will-change: transform, opacity, filter; 184 - transition: all 0.6s cubic-bezier(0.6, 0, 0.4, 1); 183 + will-change: transform, opacity, filter; 184 + transition: all 0.6s cubic-bezier(0.6, 0, 0.4, 1); 185 185 } 186 186 187 187 .loading-layout { 188 - gap: 1rem; 188 + gap: 1rem; 189 189 190 - .logo-mark { 191 - width: 3rem; 192 - height: 3rem; 193 - color: hsl(var(--text)); 194 - opacity: 0.2; 195 - } 190 + .logo-mark { 191 + width: 3rem; 192 + height: 3rem; 193 + color: hsl(var(--text)); 194 + opacity: 0.2; 195 + } 196 196 197 - .loading-bar-track { 198 - width: 200px; 199 - height: 2px; 200 - background: hsla(var(--surface2) / 0.5); 201 - border-radius: 2px; 202 - overflow: hidden; 203 - } 197 + .loading-bar-track { 198 + width: 200px; 199 + height: 2px; 200 + background: hsla(var(--surface2) / 0.5); 201 + border-radius: 2px; 202 + overflow: hidden; 203 + } 204 204 205 - .loading-bar-fill { 206 - --scale: 0; 207 - height: 100%; 208 - width: 100%; 209 - background: hsl(var(--accent)); 210 - transform-origin: left; 211 - transform: scaleX(var(--scale)); 212 - } 205 + .loading-bar-fill { 206 + --scale: 0; 207 + height: 100%; 208 + width: 100%; 209 + background: hsl(var(--accent)); 210 + transform-origin: left; 211 + transform: scaleX(var(--scale)); 212 + } 213 213 214 - .loading-text { 215 - font-size: 0.875rem; 216 - color: hsl(var(--subtext0)); 217 - letter-spacing: 0.05em; 218 - text-transform: uppercase; 219 - font-weight: 600; 220 - } 214 + .loading-text { 215 + font-size: 0.875rem; 216 + color: hsl(var(--subtext0)); 217 + letter-spacing: 0.05em; 218 + text-transform: uppercase; 219 + font-weight: 600; 220 + } 221 221 } 222 222 223 223 .success-layout { 224 - gap: 1.5rem; 224 + gap: 1.5rem; 225 225 226 - .avatar-hero { 227 - width: 8rem; 228 - height: 8rem; 229 - border-radius: 50%; 230 - overflow: hidden; 231 - box-shadow: 0 20px 50px -10px hsla(var(--accent) / 0.3); 232 - animation: scaleIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); 226 + .avatar-hero { 227 + width: 8rem; 228 + height: 8rem; 229 + border-radius: 50%; 230 + overflow: hidden; 231 + box-shadow: 0 20px 50px -10px hsla(var(--accent) / 0.3); 232 + animation: scaleIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); 233 233 234 - img { 235 - width: 100%; 236 - height: 100%; 237 - object-fit: cover; 238 - } 234 + img { 235 + width: 100%; 236 + height: 100%; 237 + object-fit: cover; 238 + } 239 239 240 - .avatar-fallback { 241 - width: 100%; 242 - height: 100%; 243 - background: hsl(var(--surface1)); 244 - display: flex; 245 - align-items: center; 246 - justify-content: center; 247 - color: hsl(var(--accent)); 248 - padding: 2rem; 249 - } 250 - } 240 + .avatar-fallback { 241 + width: 100%; 242 + height: 100%; 243 + background: hsl(var(--surface1)); 244 + display: flex; 245 + align-items: center; 246 + justify-content: center; 247 + color: hsl(var(--accent)); 248 + padding: 2rem; 249 + } 250 + } 251 251 252 - .text-hero { 253 - display: flex; 254 - flex-direction: column; 255 - gap: 0.25rem; 252 + .text-hero { 253 + display: flex; 254 + flex-direction: column; 255 + gap: 0.25rem; 256 256 257 - .greeting { 258 - font-size: 1.5rem; 259 - font-weight: 500; 260 - color: hsl(var(--subtext0)); 261 - animation: slideUp 0.6s ease 0.1s backwards; 262 - } 257 + .greeting { 258 + font-size: 1.5rem; 259 + font-weight: 500; 260 + color: hsl(var(--subtext0)); 261 + animation: slideUp 0.6s ease 0.1s backwards; 262 + } 263 263 264 - .name { 265 - font-size: 3rem; 266 - font-weight: 800; 267 - color: hsl(var(--accent)); 268 - animation: slideUp 0.6s ease 0.2s backwards; 269 - } 270 - } 264 + .name { 265 + font-size: 3rem; 266 + font-weight: 800; 267 + color: hsl(var(--accent)); 268 + animation: slideUp 0.6s ease 0.2s backwards; 269 + } 270 + } 271 271 272 - .start-btn { 273 - background: hsl(var(--text)); 274 - color: hsl(var(--base)); 275 - border: none; 276 - padding: 1rem 2rem; 277 - border-radius: 99px; 278 - font-size: 1rem; 279 - font-weight: 600; 280 - cursor: pointer; 281 - display: flex; 282 - align-items: center; 283 - gap: 0.75rem; 284 - animation: slideUp 0.6s ease 0.3s backwards; 272 + .start-btn { 273 + background: hsl(var(--text)); 274 + color: hsl(var(--base)); 275 + border: none; 276 + padding: 1rem 2rem; 277 + border-radius: 99px; 278 + font-size: 1rem; 279 + font-weight: 600; 280 + cursor: pointer; 281 + display: flex; 282 + align-items: center; 283 + gap: 0.75rem; 284 + animation: slideUp 0.6s ease 0.3s backwards; 285 285 286 - &:hover { 287 - transform: translateY(-2px); 288 - box-shadow: 0 10px 20px -5px hsla(var(--accent) / 0.3); 289 - } 290 - &:active { 291 - transform: translateY(0); 292 - box-shadow: 0 5px 20px -5px hsla(var(--accent) / 0.3); 293 - } 294 - } 286 + &:hover { 287 + transform: translateY(-2px); 288 + box-shadow: 0 10px 20px -5px hsla(var(--accent) / 0.3); 289 + } 290 + &:active { 291 + transform: translateY(0); 292 + box-shadow: 0 5px 20px -5px hsla(var(--accent) / 0.3); 293 + } 294 + } 295 295 } 296 296 297 297 .error-layout { 298 - .error-title { 299 - color: hsl(var(--red)); 300 - font-size: 1.5rem; 301 - font-weight: 700; 302 - } 303 - .error-desc { 304 - color: hsl(var(--subtext0)); 305 - } 298 + .error-title { 299 + color: hsl(var(--red)); 300 + font-size: 1.5rem; 301 + font-weight: 700; 302 + } 303 + .error-desc { 304 + color: hsl(var(--subtext0)); 305 + } 306 306 } 307 307 308 308 .is-exiting { 309 - .content-layer { 310 - transform: scale(1.2); 311 - opacity: 0; 312 - filter: blur(15px); 313 - } 309 + .content-layer { 310 + transform: scale(1.2); 311 + opacity: 0; 312 + filter: blur(15px); 313 + } 314 314 315 - .bg-layer { 316 - transform: scale(1.15); 317 - opacity: 0; 318 - transition-duration: 0.8s; 319 - } 315 + .bg-layer { 316 + transform: scale(1.15); 317 + opacity: 0; 318 + transition-duration: 0.8s; 319 + } 320 320 } 321 321 322 322 @keyframes progress { 323 - 0% { 324 - transform: scaleX(0) translateX(0%); 325 - } 326 - 50% { 327 - transform: scaleX(0.5) translateX(0%); 328 - } 329 - 100% { 330 - transform: scaleX(1) translateX(0%); 331 - } 323 + 0% { 324 + transform: scaleX(0) translateX(0%); 325 + } 326 + 50% { 327 + transform: scaleX(0.5) translateX(0%); 328 + } 329 + 100% { 330 + transform: scaleX(1) translateX(0%); 331 + } 332 332 } 333 333 334 334 @keyframes scaleIn { 335 - from { 336 - transform: scale(0.5); 337 - opacity: 0; 338 - } 339 - to { 340 - transform: scale(1); 341 - opacity: 1; 342 - } 335 + from { 336 + transform: scale(0.5); 337 + opacity: 0; 338 + } 339 + to { 340 + transform: scale(1); 341 + opacity: 1; 342 + } 343 343 } 344 344 345 345 @keyframes slideUp { 346 - from { 347 - transform: translateY(20px); 348 - opacity: 0; 349 - } 350 - to { 351 - transform: translateY(0); 352 - opacity: 1; 353 - } 346 + from { 347 + transform: translateY(20px); 348 + opacity: 0; 349 + } 350 + to { 351 + transform: translateY(0); 352 + opacity: 1; 353 + } 354 354 } 355 355 356 356 .fade-up-enter-active, 357 357 .fade-up-leave-active { 358 - transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 358 + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); 359 359 } 360 360 .fade-up-enter-from { 361 - opacity: 0; 362 - transform: translateY(20px); 361 + opacity: 0; 362 + transform: translateY(20px); 363 363 } 364 364 .fade-up-leave-to { 365 - opacity: 0; 366 - transform: translateY(-20px); 365 + opacity: 0; 366 + transform: translateY(-20px); 367 367 } 368 368 </style>
+163
src/views/Onboarding/OnboardingFlow.vue
··· 1 + <script setup lang="ts"> 2 + import { ref, computed } from 'vue' 3 + import BluebellImg from '@/assets/bluebell.webp' 4 + 5 + import IntroStep from './steps/IntroStep.vue' 6 + import ThemeStep from './steps/ThemeStep.vue' 7 + import AuthStep from './steps/AuthStep.vue' 8 + 9 + const emit = defineEmits<{ 10 + (e: 'complete'): void 11 + }>() 12 + 13 + const steps = [IntroStep, ThemeStep, AuthStep] 14 + const currentStepIndex = ref(0) 15 + const isExiting = ref(false) 16 + 17 + const currentStepComponent = computed(() => steps[currentStepIndex.value]) 18 + 19 + const bgBlur = computed(() => { 20 + if (isExiting.value) return '20px' 21 + return `${currentStepIndex.value * 2}px` 22 + }) 23 + 24 + const bgScale = computed(() => { 25 + if (isExiting.value) return 1.2 26 + return 1 + currentStepIndex.value * 0.05 27 + }) 28 + 29 + const nextStep = () => { 30 + if (currentStepIndex.value < steps.length - 1) { 31 + currentStepIndex.value++ 32 + } else { 33 + finish() 34 + } 35 + } 36 + 37 + const finish = () => { 38 + isExiting.value = true 39 + setTimeout(() => { 40 + emit('complete') 41 + }, 800) 42 + } 43 + </script> 44 + 45 + <template> 46 + <div class="onboarding-root" :class="{ 'is-exiting': isExiting }"> 47 + <div 48 + class="bg-layer" 49 + :style="{ 50 + backgroundImage: `url(${BluebellImg})`, 51 + filter: `blur(${bgBlur})`, 52 + transform: `scale(${bgScale})`, 53 + }" 54 + ></div> 55 + 56 + <div class="bg-overlay"></div> 57 + 58 + <div class="content-layer"> 59 + <Transition name="step-slide" mode="out-in"> 60 + <component 61 + :is="currentStepComponent" 62 + @next="nextStep" 63 + @finish="finish" 64 + :key="currentStepIndex" 65 + /> 66 + </Transition> 67 + </div> 68 + </div> 69 + </template> 70 + 71 + <style scoped lang="scss"> 72 + .onboarding-root { 73 + position: fixed; 74 + inset: 0; 75 + z-index: 9999; 76 + background-color: hsl(var(--base)); 77 + display: flex; 78 + flex-direction: column; 79 + overflow: hidden; 80 + } 81 + 82 + .bg-layer { 83 + position: absolute; 84 + inset: -20px; 85 + background-size: cover; 86 + background-position: center; 87 + background-repeat: no-repeat; 88 + 89 + opacity: 1; 90 + transition: 91 + opacity 0.8s ease, 92 + transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), 93 + filter 0.8s ease; 94 + will-change: transform, opacity, filter; 95 + 96 + &::after { 97 + content: ''; 98 + position: absolute; 99 + inset: 0; 100 + } 101 + } 102 + 103 + .bg-overlay { 104 + position: absolute; 105 + inset: 0; 106 + background: linear-gradient( 107 + to bottom, 108 + hsla(var(--base) / 0.2) 0%, 109 + hsla(var(--base) / 0.8) 60%, 110 + hsla(var(--base) / 0.9) 80% 111 + ); 112 + opacity: 1; 113 + transition: 114 + opacity 0.8s ease, 115 + transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), 116 + filter 0.8s ease; 117 + will-change: transform, opacity, filter; 118 + } 119 + 120 + .content-layer { 121 + position: relative; 122 + z-index: 10; 123 + flex: 1; 124 + display: flex; 125 + flex-direction: column; 126 + justify-content: flex-end; 127 + padding: 2rem; 128 + padding-bottom: calc(2rem + env(safe-area-inset-bottom)); 129 + max-width: 600px; 130 + width: 100%; 131 + margin: 0 auto; 132 + } 133 + 134 + .is-exiting { 135 + .content-layer { 136 + transform: scale(1.1); 137 + opacity: 0; 138 + filter: blur(16px); 139 + transition: all 0.8s cubic-bezier(0.6, 0, 0.4, 1); 140 + } 141 + .bg-layer { 142 + opacity: 0; 143 + } 144 + .bg-overlay { 145 + opacity: 0; 146 + } 147 + } 148 + 149 + .step-slide-enter-active, 150 + .step-slide-leave-active { 151 + transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); 152 + } 153 + 154 + .step-slide-enter-from { 155 + opacity: 0; 156 + transform: translateY(20px); 157 + } 158 + 159 + .step-slide-leave-to { 160 + opacity: 0; 161 + transform: translateY(-20px); 162 + } 163 + </style>
+73
src/views/Onboarding/steps/AuthStep.vue
··· 1 + <script setup lang="ts"> 2 + import Button from '@/components/UI/BaseButton.vue' 3 + import { 4 + IconLoginRounded, 5 + IconArrowForwardRounded, 6 + } from '@iconify-prerendered/vue-material-symbols' 7 + import { useNavigationStore } from '@/stores/navigation' 8 + 9 + const nav = useNavigationStore() 10 + const emit = defineEmits<{ 11 + (e: 'finish'): void 12 + }>() 13 + 14 + const handleLogin = () => { 15 + nav.push('login') 16 + emit('finish') 17 + } 18 + 19 + const handleSkip = () => { 20 + emit('finish') 21 + } 22 + </script> 23 + 24 + <template> 25 + <div class="step-content"> 26 + <div class="text-group"> 27 + <h2 class="title">Almost there...</h2> 28 + <p class="subtitle"> 29 + Sign in to your Atmosphere account to start posting, or take a look around first. 30 + </p> 31 + </div> 32 + 33 + <div class="action-stack"> 34 + <Button class="action-btn" size="lg" variant="secondary" @click="handleSkip"> 35 + <span>Skip for now</span> 36 + <IconArrowForwardRounded /> 37 + </Button> 38 + 39 + <Button class="action-btn" size="lg" variant="primary" @click="handleLogin"> 40 + <span>Sign In</span> 41 + <IconLoginRounded /> 42 + </Button> 43 + </div> 44 + </div> 45 + </template> 46 + 47 + <style scoped lang="scss"> 48 + .step-content { 49 + display: flex; 50 + flex-direction: column; 51 + gap: 2rem; 52 + width: 100%; 53 + } 54 + 55 + .text-group { 56 + .title { 57 + font-size: 2rem; 58 + font-weight: 800; 59 + color: hsl(var(--text)); 60 + margin-bottom: 0.5rem; 61 + } 62 + .subtitle { 63 + color: hsl(var(--subtext0)); 64 + font-size: 1rem; 65 + } 66 + } 67 + 68 + .action-stack { 69 + display: flex; 70 + justify-content: flex-end; 71 + gap: 1rem; 72 + } 73 + </style>
+85
src/views/Onboarding/steps/IntroStep.vue
··· 1 + <script setup lang="ts"> 2 + import SVG from '@/components/UI/SVG.vue' 3 + import BluebellLogo from '@/assets/icons/bluebell.svg?raw' 4 + import Button from '@/components/UI/BaseButton.vue' 5 + import { IconArrowForwardRounded } from '@iconify-prerendered/vue-material-symbols' 6 + 7 + defineEmits<{ 8 + (e: 'next'): void 9 + }>() 10 + </script> 11 + 12 + <template> 13 + <div class="step-content"> 14 + <div class="hero-section"> 15 + <div class="logo-mark"> 16 + <SVG :icon="BluebellLogo" /> 17 + </div> 18 + 19 + <div class="text-group"> 20 + <h1 class="title">Bluebell</h1> 21 + <p class="subtitle">A beautiful, fast client for the Atmosphere & Bluesky.</p> 22 + </div> 23 + </div> 24 + 25 + <div class="action-stack"> 26 + <Button class="action-btn" size="lg" variant="primary" @click="$emit('next')"> 27 + <span>Get Started</span> 28 + <IconArrowForwardRounded /> 29 + </Button> 30 + </div> 31 + </div> 32 + </template> 33 + 34 + <style scoped lang="scss"> 35 + .step-content { 36 + display: flex; 37 + flex-direction: column; 38 + gap: 3rem; 39 + width: 100%; 40 + } 41 + 42 + .hero-section { 43 + display: flex; 44 + flex-direction: column; 45 + gap: 1.5rem; 46 + } 47 + 48 + .logo-mark { 49 + width: 5rem; 50 + height: 5rem; 51 + color: hsl(var(--accent)); 52 + 53 + :deep(svg) { 54 + width: 100%; 55 + height: 100%; 56 + } 57 + } 58 + 59 + .text-group { 60 + display: flex; 61 + flex-direction: column; 62 + gap: 0.5rem; 63 + 64 + .title { 65 + font-size: 3.5rem; 66 + font-weight: 800; 67 + line-height: 1; 68 + letter-spacing: -0.03em; 69 + color: hsl(var(--text)); 70 + } 71 + 72 + .subtitle { 73 + font-size: 1.25rem; 74 + color: hsl(var(--subtext0)); 75 + line-height: 1.4; 76 + max-width: 80%; 77 + } 78 + } 79 + 80 + .action-stack { 81 + display: flex; 82 + justify-content: flex-end; 83 + gap: 1rem; 84 + } 85 + </style>
+147
src/views/Onboarding/steps/ThemeStep.vue
··· 1 + <script setup lang="ts"> 2 + import { computed } from 'vue' 3 + import { useThemeStore } from '@/stores/theme' 4 + import Button from '@/components/UI/BaseButton.vue' 5 + import { 6 + IconArrowForwardRounded, 7 + IconCheckCircleRounded, 8 + } from '@iconify-prerendered/vue-material-symbols' 9 + 10 + const themeStore = useThemeStore() 11 + const themes = computed(() => themeStore.themes) 12 + 13 + const selectTheme = (themeId: string, type: 'light' | 'dark') => { 14 + themeStore.setFollowSystem(false) 15 + if (type === 'dark') themeStore.setPreferredDark(themeId) 16 + else themeStore.setPreferredLight(themeId) 17 + } 18 + 19 + defineEmits<{ 20 + (e: 'next'): void 21 + }>() 22 + </script> 23 + 24 + <template> 25 + <div class="step-content"> 26 + <div class="text-group"> 27 + <h2 class="title">Pick a flavour</h2> 28 + <p class="subtitle">Choose a theme that suits your eyes (you can change this later).</p> 29 + </div> 30 + 31 + <div class="theme-grid"> 32 + <button 33 + v-for="t in themes" 34 + :key="t.id" 35 + class="theme-card" 36 + :class="{ active: themeStore.activeTheme.id === t.id }" 37 + @click="selectTheme(t.id, t.type)" 38 + :style="{ 39 + '--t-base': `hsl(${t.variables.base})`, 40 + '--t-text': `hsl(${t.variables.text})`, 41 + '--t-blue': `hsl(${t.variables.blue})`, 42 + }" 43 + > 44 + <div class="card-bg"></div> 45 + <div class="card-content"> 46 + <span class="theme-name">{{ t.name }}</span> 47 + <div 48 + aria-hidden="true" 49 + :class="{ 'active-ring': true, active: themeStore.activeTheme.id === t.id }" 50 + > 51 + <IconCheckCircleRounded /> 52 + </div> 53 + </div> 54 + </button> 55 + </div> 56 + 57 + <div class="action-stack"> 58 + <Button class="action-btn" size="lg" variant="primary" @click="$emit('next')"> 59 + <span>Next</span> 60 + <IconArrowForwardRounded /> 61 + </Button> 62 + </div> 63 + </div> 64 + </template> 65 + 66 + <style scoped lang="scss"> 67 + .step-content { 68 + display: flex; 69 + flex-direction: column; 70 + gap: 2rem; 71 + width: 100%; 72 + } 73 + 74 + .text-group { 75 + .title { 76 + font-size: 2rem; 77 + font-weight: 800; 78 + color: hsl(var(--text)); 79 + margin-bottom: 0.5rem; 80 + } 81 + .subtitle { 82 + color: hsl(var(--subtext0)); 83 + font-size: 1rem; 84 + } 85 + } 86 + 87 + .theme-grid { 88 + display: grid; 89 + grid-template-columns: 1fr 1fr; 90 + gap: 1rem; 91 + } 92 + 93 + .theme-card { 94 + position: relative; 95 + height: 4rem; 96 + border: none; 97 + background: transparent; 98 + padding: 0; 99 + cursor: pointer; 100 + border-radius: var(--radius-md); 101 + overflow: hidden; 102 + 103 + .card-bg { 104 + position: absolute; 105 + inset: 0; 106 + background-color: var(--t-base); 107 + border: 2px solid hsla(var(--t-text) / 0.1); 108 + border-radius: var(--radius-md); 109 + } 110 + 111 + .card-content { 112 + position: relative; 113 + z-index: 1; 114 + height: 100%; 115 + display: flex; 116 + align-items: center; 117 + justify-content: space-between; 118 + padding: 0 1rem; 119 + } 120 + 121 + .theme-name { 122 + font-weight: 700; 123 + color: var(--t-text); 124 + } 125 + 126 + .active-ring { 127 + color: var(--t-blue); 128 + font-size: 1.25rem; 129 + opacity: 0; 130 + 131 + &.active { 132 + opacity: 1; 133 + } 134 + } 135 + 136 + &.active .card-bg { 137 + border-color: var(--t-blue); 138 + background-color: color-mix(in srgb, var(--t-base), var(--t-blue) 5%); 139 + } 140 + } 141 + 142 + .action-stack { 143 + display: flex; 144 + justify-content: flex-end; 145 + gap: 1rem; 146 + } 147 + </style>