diff --git a/api/package.json b/api/package.json index 8ebe4c7e..e37c839d 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { "name": "@imput/cobalt-api", "description": "save what you love", - "version": "10.0.0", + "version": "10.1.0", "author": "imput", "exports": "./src/cobalt.js", "type": "module", diff --git a/api/src/config.js b/api/src/config.js index fc1d5d29..5f3e52cc 100644 --- a/api/src/config.js +++ b/api/src/config.js @@ -34,10 +34,15 @@ const env = { externalProxy: process.env.API_EXTERNAL_PROXY, + turnstileSitekey: process.env.TURNSTILE_SITEKEY, turnstileSecret: process.env.TURNSTILE_SECRET, jwtSecret: process.env.JWT_SECRET, jwtLifetime: process.env.JWT_EXPIRY || 120, + sessionEnabled: process.env.TURNSTILE_SITEKEY + && process.env.TURNSTILE_SECRET + && process.env.JWT_SECRET, + enabledServices, } diff --git a/api/src/core/api.js b/api/src/core/api.js index fdd13ce1..78d4359e 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -49,6 +49,7 @@ export const runAPI = (express, app, __dirname) => { url: env.apiURL, startTime: `${startTimestamp}`, durationLimit: env.durationLimit, + turnstileSitekey: env.sessionEnabled ? env.turnstileSitekey : undefined, services: [...env.enabledServices].map(e => { return friendlyServiceName(e); }), @@ -109,16 +110,14 @@ export const runAPI = (express, app, __dirname) => { if (!acceptRegex.test(req.header('Accept'))) { return fail(res, "error.api.header.accept"); } - if (!acceptRegex.test(req.header('Content-Type'))) { return fail(res, "error.api.header.content_type"); } - next(); }); app.post('/', (req, res, next) => { - if (!env.turnstileSecret || !env.jwtSecret) { + if (!env.sessionEnabled) { return next(); } @@ -160,7 +159,7 @@ export const runAPI = (express, app, __dirname) => { }); app.post("/session", async (req, res) => { - if (!env.turnstileSecret || !env.jwtSecret) { + if (!env.sessionEnabled) { return fail(res, "error.api.auth.not_configured") } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d316277..220a2cf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,41 +84,25 @@ importers: packages/version-info: {} web: - dependencies: + devDependencies: + '@eslint/js': + specifier: ^9.5.0 + version: 9.8.0 '@fontsource-variable/noto-sans-mono': specifier: ^5.0.20 version: 5.0.20 '@fontsource/ibm-plex-mono': specifier: ^5.0.13 version: 5.0.13 + '@fontsource/redaction-10': + specifier: ^5.0.2 + version: 5.0.2 '@imput/libav.js-remux-cli': specifier: ^5.5.6 version: 5.5.6 '@imput/version-info': specifier: workspace:^ version: link:../packages/version-info - '@tabler/icons-svelte': - specifier: 3.6.0 - version: 3.6.0(svelte@4.2.18) - '@vitejs/plugin-basic-ssl': - specifier: ^1.1.0 - version: 1.1.0(vite@5.3.5(@types/node@20.14.14)) - mime: - specifier: ^4.0.4 - version: 4.0.4 - sveltekit-i18n: - specifier: ^2.4.2 - version: 2.4.2(svelte@4.2.18) - ts-deepmerge: - specifier: ^7.0.0 - version: 7.0.1 - devDependencies: - '@eslint/js': - specifier: ^9.5.0 - version: 9.8.0 - '@fontsource/redaction-10': - specifier: ^5.0.2 - version: 5.0.2 '@sveltejs/adapter-static': specifier: ^3.0.2 version: 3.0.2(@sveltejs/kit@2.5.19(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.18)(vite@5.3.5(@types/node@20.14.14)))(svelte@4.2.18)(vite@5.3.5(@types/node@20.14.14))) @@ -128,6 +112,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^3.0.0 version: 3.1.1(svelte@4.2.18)(vite@5.3.5(@types/node@20.14.14)) + '@tabler/icons-svelte': + specifier: 3.6.0 + version: 3.6.0(svelte@4.2.18) '@types/eslint__js': specifier: ^8.42.3 version: 8.42.3 @@ -137,9 +124,15 @@ importers: '@types/node': specifier: ^20.14.10 version: 20.14.14 + '@vitejs/plugin-basic-ssl': + specifier: ^1.1.0 + version: 1.1.0(vite@5.3.5(@types/node@20.14.14)) compare-versions: specifier: ^6.1.0 version: 6.1.1 + dotenv: + specifier: ^16.0.1 + version: 16.4.5 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -149,6 +142,9 @@ importers: mdsvex: specifier: ^0.11.2 version: 0.11.2(svelte@4.2.18) + mime: + specifier: ^4.0.4 + version: 4.0.4 svelte: specifier: ^4.2.7 version: 4.2.18 @@ -158,6 +154,12 @@ importers: svelte-preprocess: specifier: ^6.0.2 version: 6.0.2(postcss@8.4.40)(svelte@4.2.18)(typescript@5.5.4) + sveltekit-i18n: + specifier: ^2.4.2 + version: 2.4.2(svelte@4.2.18) + ts-deepmerge: + specifier: ^7.0.1 + version: 7.0.1 tslib: specifier: ^2.4.1 version: 2.6.3 diff --git a/web/i18n/en/a11y/save.json b/web/i18n/en/a11y/save.json index 3a92de64..2dc85154 100644 --- a/web/i18n/en/a11y/save.json +++ b/web/i18n/en/a11y/save.json @@ -1,5 +1,6 @@ { "link_area": "link input area", + "link_area.turnstile": "link input area. checking if you're not a robot.", "clear_input": "clear input", "download": "download", "download.think": "processing the link...", diff --git a/web/i18n/en/about.json b/web/i18n/en/about.json index e566faaa..b441512e 100644 --- a/web/i18n/en/about.json +++ b/web/i18n/en/about.json @@ -12,5 +12,19 @@ "community.twitter": "news account on twitter", "community.github": "github repo", "community.email": "support email", - "community.telegram": "news channel on telegram" + "community.telegram": "news channel on telegram", + + "heading.general": "general terms", + "heading.licenses": "licenses", + "heading.summary": "best way to save what you love", + "heading.privacy": "leading privacy", + "heading.speed": "blazing speed", + "heading.community": "open community", + "heading.local": "on-device processing", + "heading.saving": "saving", + "heading.encryption": "encryption", + "heading.plausible": "anonymous traffic analytics", + "heading.cloudflare": "web privacy & security", + "heading.responsibility": "user responsibilities", + "heading.abuse": "reporting abuse" } diff --git a/web/i18n/en/about/credits.md b/web/i18n/en/about/credits.md new file mode 100644 index 00000000..27266ea4 --- /dev/null +++ b/web/i18n/en/about/credits.md @@ -0,0 +1,37 @@ + + +
+ + +meowbalt is cobalt's speedy mascot. he is an extremely expressive cat that loves fast internet. + +all amazing drawings of meowbalt that you see in cobalt were made by [GlitchyPSI](https://glitchypsi.xyz/). +he is also the original designer of the character. + +you cannot use or modify GlitchyPSI's artworks of meowbalt without his explicit permission. + +you cannot use or modify the meowbalt character design commercially or in any form that isn't fan art. +
+ +
+ + +cobalt processing server is open source and licensed under [AGPL-3.0]({docs.apiLicense}). + +cobalt frontend is [source first](https://sourcefirst.com/) and licensed under [CC-BY-NC-SA 4.0]({docs.webLicense}). +we decided to use this license to stop grifters from profiting off our work & from creating malicious clones that deceive people and hurt our public identity. + +we rely on many open source libraries, create & distribute our own. +you can see the full list of dependencies on [github]({contacts.github}). +
diff --git a/web/i18n/en/about/general.md b/web/i18n/en/about/general.md new file mode 100644 index 00000000..333e119e --- /dev/null +++ b/web/i18n/en/about/general.md @@ -0,0 +1,79 @@ + + +
+ + +cobalt lets you save anything from your favorite websites: video, audio, photos or gifs — cobalt can do it all! + +no ads, trackers, or paywalls, no nonsense. just a convenient web app that works everywhere. +
+ +
+ + +all requests to backend are anonymous and all tunnels are encrypted. +we have a strict zero log policy and don't track *anything* about individual people. + +to avoid caching or storing downloaded files, cobalt processes them on-the-fly, sending processed pieces directly to client. +this technology is used when your request needs additional processing, such as when source service stores video & audio in separate files. + +for even higher level of protection, you can [ask cobalt to always tunnel everything](/settings/privacy#tunnel). +when enabled, cobalt will proxy everything through itself. no one will know what you download, even your network provider/admin. +all they'll see is that you're using cobalt. +
+ +
+ + +since we don't rely on any existing downloaders and develop our own from ground up, +cobalt is extremely efficient and a processing server can run on basically any hardware. + +main processing instances are hosted on several dedicated servers in several countries, +to reduce latency and distribute the traffic. + +we constantly improve our infrastructure along with our long-standing partner, [royalehosting.net]({partners.royalehosting})! +you're in good hands, and will get what you need within seconds. +
+ +
+ + +cobalt is used by countless artists, educators, and content creators to do what they love. +we're always on the line with our community and work together to create even more useful tools for them. +feel free to [join the conversation](/about/community)! + +we believe that the future of the internet is open, which is why cobalt is [source first](https://sourcefirst.com/) and [easily self-hostable]({docs.instanceHosting}). you can [check the source code & contribute to cobalt]({contacts.github}) +at any time, we welcome all contributions and suggestions. + +you can use any processing instances hosted by the community, including your own. +if your friend hosts one, just ask them for a domain and [add it in instance settings](/settings/instances#community). +
+ +
+ + +new features, such as [remuxing](/remux), work on-device. +on-device processing is efficient and never sends anything over the internet. +it perfectly aligns with our future goal of moving as much processing as possible to client. + +
diff --git a/web/i18n/en/about/privacy.md b/web/i18n/en/about/privacy.md new file mode 100644 index 00000000..b19ca762 --- /dev/null +++ b/web/i18n/en/about/privacy.md @@ -0,0 +1,76 @@ + + +
+ + +cobalt's privacy policy is simple: we don't collect or store anything about you. what you do is solely your business, not ours or anyone else's. + +these terms are applicable only when using the official cobalt instance. in other cases, you may need to contact the hoster for accurate info. +
+ +
+ + +tools that use on-device processing work offline, locally, and never send any data anywhere. they are explicitly marked as such whenever applicable. +
+ +
+ + +when using saving functionality, in some cases cobalt will encrypt & temporarily store information needed for tunneling. it's stored in processing server's RAM for 90 seconds and irreversibly purged afterwards. no one has access to it, even instance owners, as long as they don't modify the official cobalt image. + +processed/tunneled files are never cached anywhere. everything is tunneled live. cobalt's saving functionality is essentially a fancy proxy service. +
+ +
+ + +temporarily stored tunnel data is encrypted using the AES-256 standard. decryption keys are only included in the access link and never logged/cached/stored anywhere. only the end user has access to the link & encryption keys. keys are generated uniquely for each requested tunnel. +
+ +{#if env.PLAUSIBLE_ENABLED} +
+ + +for sake of privacy, we use [plausible's anonymous traffic analytics](https://plausible.io/) to get an approximate number of active cobalt users. no identifiable information about you or your requests is ever stored. all data is anonymized and aggregated. the plausible instance we use is hosted & managed by us. + +plausible doesn't use cookies and is fully compliant with GDPR, CCPA, and PECR. + +[learn more about plausible's dedication to privacy.](https://plausible.io/privacy-focused-web-analytics) + +if you wish to opt out of anonymous analytics, you can do it in privacy settings. +
+{/if} + +
+ + +we use cloudflare services for ddos & bot protection. we also use cloudflare pages for deploying & hosting the static web app. all of these are required to provide the best experience for everyone. it's the most private & reliable provider that we know of. + +cloudflare is fully compliant with GDPR and HIPAA. + +[learn more about cloudflare's dedication to privacy.](https://www.cloudflare.com/trust-hub/privacy-and-data-protection/) +
diff --git a/web/i18n/en/about/terms.md b/web/i18n/en/about/terms.md new file mode 100644 index 00000000..453030cf --- /dev/null +++ b/web/i18n/en/about/terms.md @@ -0,0 +1,47 @@ + + +
+ + +these terms are applicable only when using the official cobalt instance. in other cases, you may need to contact the hoster for accurate info. +
+ +
+ + +saving functionality simplifies downloading content from the internet and takes zero liability for what the saved content is used for. processing servers work like advanced proxies and don't ever write any content to disk. everything is handled in RAM and permanently purged once the tunnel is done. we have no downloading logs and can't identify anyone. + +[you can read more about how tunnels work in our privacy policy.](/about/privacy) +
+ +
+ + +you (end user) are responsible for what you do with our tools, how you use and distribute resulting content. please be mindful when using content of others and always credit original creators. make sure you don't violate any terms or licenses. + +when used in educational purposes, always cite sources and credit original creators. + +fair use and credits benefit everyone. +
+ +
+ + +we have no way of detecting abusive behavior automatically, as cobalt is 100% anonymous. +however, you can report such activities to us and we will do our best to comply manually: [safety@imput.net](mailto:safety@imput.net) +
diff --git a/web/i18n/en/button.json b/web/i18n/en/button.json index 849ac98d..1ea7fb41 100644 --- a/web/i18n/en/button.json +++ b/web/i18n/en/button.json @@ -7,6 +7,7 @@ "download": "download", "share": "share", "copy": "copy", + "copy.section": "copy the section link", "copied": "copied", "import": "import", "continue": "continue", diff --git a/web/i18n/en/error.json b/web/i18n/en/error.json index 6a730052..67dd911c 100644 --- a/web/i18n/en/error.json +++ b/web/i18n/en/error.json @@ -8,6 +8,8 @@ "tunnel.probe": "couldn't verify whether you can download this file. try again in a few seconds!", + "captcha_ongoing": "still checking if you're not a bot. wait for the spinner to disappear and try again.\n\nif it takes too long, please let us know! we use cloudflare turnstile for bot protection and sometimes it blocks people for no reason.", + "api.auth.jwt.missing": "couldn't confirm whether you're not a robot because the processing server didn't receive the human access token. try again in a few seconds or reload the page!", "api.auth.jwt.invalid": "couldn't confirm whether you're not a robot because your human access token expired and wasn't renewed. try again in a few seconds or reload the page!", "api.auth.turnstile.missing": "couldn't confirm whether you're not a robot because the processing server didn't receive the human access token. try again in a few seconds or reload the page!", @@ -36,7 +38,7 @@ "api.content.too_long": "the media you requested is too long. current duration limit is {{ limit }} minutes. try something shorter instead!", "api.content.video.unavailable": "i can't access this video. it may be restricted on {{ service }}'s side. have you pasted the right link?", - "api.content.video.live": "this video is currently live, so i can't download it yet. wait for the livestream to finish, and then try again!", + "api.content.video.live": "this video is currently live, so i can't download it yet. wait for the live stream to finish, and then try again!", "api.content.video.private": "this video is private, so i cannot access it. change its visibility or try another one!", "api.content.video.age": "this video is age-restricted, so i can't access it anonymously. try another one!", "api.content.video.region": "this video is region locked, and the processing server is in a different location. try another one!", @@ -45,7 +47,7 @@ "api.content.post.private": "this post is from a private account, so i can't access it. have you pasted the right link?", "api.content.post.age": "this post is age-restricted, so i can't access it anonymously. have you pasted the right link?", - "api.youtube.codec": "youtube didn't return anything with your preferred codec & resolution. try another set of settings!", + "api.youtube.codec": "youtube didn't return anything with your preferred video codec. try another one in settings!", "api.youtube.decipher": "youtube updated its decipher algorithm and i couldn't extract the info about the video.\n\ntry again in a few seconds, but if issue sticks, contact us for support.", "api.youtube.login": "couldn't get this video because youtube labeled me as a bot. this is potentially caused by the processing instance not having any active account tokens. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!", "api.youtube.token_expired": "couldn't get this video because the youtube token expired and i couldn't refresh it. try again in a few seconds, but if it still doesn't work, tell the instance owner about this error!" diff --git a/web/i18n/en/settings.json b/web/i18n/en/settings.json index 90b2e0f6..fba788e2 100644 --- a/web/i18n/en/settings.json +++ b/web/i18n/en/settings.json @@ -91,7 +91,7 @@ "language.auto.title": "automatic selection", "language.auto.description": "cobalt will use your browser's default language if translation is available. if not, english will be used instead.", "language.preferred.title": "preferred language", - "language.preferred.description": "this language will be used when automatic selection is disabled. any text that isn't translated will be displayed in english.", + "language.preferred.description": "this language will be used when automatic selection is disabled. any text that isn't translated will be displayed in english.\n\nwe use community-sourced translations for languages other than english, russian, and czech. they may be inaccurate or incomplete.", "privacy.analytics": "anonymous traffic analytics", "privacy.analytics.title": "don't contribute to analytics", diff --git a/web/package.json b/web/package.json index 67c779bc..69de03ca 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@imput/cobalt-web", - "version": "10.0.0", + "version": "10.1.0", "type": "module", "private": true, "scripts": { @@ -25,35 +25,34 @@ "homepage": "https://cobalt.tools/", "devDependencies": { "@eslint/js": "^9.5.0", + "@fontsource-variable/noto-sans-mono": "^5.0.20", + "@fontsource/ibm-plex-mono": "^5.0.13", "@fontsource/redaction-10": "^5.0.2", + "@imput/libav.js-remux-cli": "^5.5.6", + "@imput/version-info": "workspace:^", "@sveltejs/adapter-static": "^3.0.2", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@tabler/icons-svelte": "3.6.0", "@types/eslint__js": "^8.42.3", "@types/fluent-ffmpeg": "^2.1.25", "@types/node": "^20.14.10", + "@vitejs/plugin-basic-ssl": "^1.1.0", "compare-versions": "^6.1.0", + "dotenv": "^16.0.1", "eslint": "^8.57.0", "glob": "^10.4.5", "mdsvex": "^0.11.2", + "mime": "^4.0.4", "svelte": "^4.2.7", "svelte-check": "^3.6.0", "svelte-preprocess": "^6.0.2", + "sveltekit-i18n": "^2.4.2", + "ts-deepmerge": "^7.0.1", "tslib": "^2.4.1", "turnstile-types": "^1.2.2", "typescript": "^5.4.5", "typescript-eslint": "^7.13.1", "vite": "^5.0.3" - }, - "dependencies": { - "@fontsource-variable/noto-sans-mono": "^5.0.20", - "@fontsource/ibm-plex-mono": "^5.0.13", - "@imput/libav.js-remux-cli": "^5.5.6", - "@imput/version-info": "workspace:^", - "@tabler/icons-svelte": "3.6.0", - "@vitejs/plugin-basic-ssl": "^1.1.0", - "mime": "^4.0.4", - "sveltekit-i18n": "^2.4.2", - "ts-deepmerge": "^7.0.0" } } diff --git a/web/src/components/dialog/PickerItem.svelte b/web/src/components/dialog/PickerItem.svelte index 7edf8c68..4a515a6e 100644 --- a/web/src/components/dialog/PickerItem.svelte +++ b/web/src/components/dialog/PickerItem.svelte @@ -14,6 +14,7 @@ export let number: number; let imageLoaded = false; + const isTunnel = new URL(item.url).pathname === "/tunnel"; $: itemType = item.type ?? "photo"; @@ -23,6 +24,7 @@ on:click={() => downloadFile({ url: item.url, + urlType: isTunnel ? "tunnel" : "redirect", })} >
diff --git a/web/src/components/dialog/SavingDialog.svelte b/web/src/components/dialog/SavingDialog.svelte index f71e2b9f..ec719aed 100644 --- a/web/src/components/dialog/SavingDialog.svelte +++ b/web/src/components/dialog/SavingDialog.svelte @@ -10,6 +10,8 @@ shareFile, } from "$lib/download"; + import type { CobaltFileUrlType } from "$lib/types/api"; + import DialogContainer from "$components/dialog/DialogContainer.svelte"; import Meowbalt from "$components/misc/Meowbalt.svelte"; @@ -22,12 +24,14 @@ import IconFileDownload from "@tabler/icons-svelte/IconFileDownload.svelte"; import CopyIcon from "$components/misc/CopyIcon.svelte"; + export let id: string; export let dismissable = true; export let bodyText: string = ""; export let url: string = ""; export let file: File | undefined = undefined; + export let urlType: CobaltFileUrlType | undefined = undefined; let close: () => void; @@ -55,7 +59,7 @@
- {#if device.supports.directDownload} + {#if device.supports.directDownload && !(device.is.iOS && urlType === "redirect")}
{#each Object.entries(PRESET_DONATION_AMOUNTS) as [ amount, component ]} - - - - - + + + {/each}
diff --git a/web/src/components/donate/DonationOption.svelte b/web/src/components/donate/DonationOption.svelte index 91bcee7b..7e5ca917 100644 --- a/web/src/components/donate/DonationOption.svelte +++ b/web/src/components/donate/DonationOption.svelte @@ -1,18 +1,24 @@ - diff --git a/web/src/components/misc/AboutPageWrapper.svelte b/web/src/components/misc/AboutPageWrapper.svelte new file mode 100644 index 00000000..eb46647b --- /dev/null +++ b/web/src/components/misc/AboutPageWrapper.svelte @@ -0,0 +1,9 @@ + + + +
+ +
diff --git a/web/src/components/misc/SectionHeading.svelte b/web/src/components/misc/SectionHeading.svelte new file mode 100644 index 00000000..4645f2d0 --- /dev/null +++ b/web/src/components/misc/SectionHeading.svelte @@ -0,0 +1,91 @@ + + +
+

{title}

+ {#if beta} +
{$t("general.beta")}
+ {/if} + +
+ + diff --git a/web/src/components/misc/Turnstile.svelte b/web/src/components/misc/Turnstile.svelte index 29914995..f1c9b625 100644 --- a/web/src/components/misc/Turnstile.svelte +++ b/web/src/components/misc/Turnstile.svelte @@ -1,14 +1,14 @@
-
-

{title}

- {#if beta} -
{$t("general.beta")}
- {/if} -
+
@@ -82,27 +86,6 @@ } } - .settings-content-header { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 8px; - } - - .beta-label { - display: flex; - justify-content: center; - align-items: center; - border-radius: 5px; - padding: 0 5px; - background: var(--secondary); - color: var(--primary); - font-size: 11px; - font-weight: 500; - line-height: 0; - text-transform: uppercase; - } - @media screen and (max-width: 750px) { .settings-content { padding: var(--padding); diff --git a/web/src/components/sidebar/Sidebar.svelte b/web/src/components/sidebar/Sidebar.svelte index df03e6ea..ca3a963e 100644 --- a/web/src/components/sidebar/Sidebar.svelte +++ b/web/src/components/sidebar/Sidebar.svelte @@ -70,7 +70,6 @@ #sidebar-tabs { height: 100%; - width: var(--sidebar-width); justify-content: space-between; padding: var(--sidebar-inner-padding); padding-bottom: var(--border-radius); @@ -110,7 +109,6 @@ overflow-x: scroll; padding-bottom: 0; padding: var(--sidebar-inner-padding) 0; - width: unset; height: fit-content; } diff --git a/web/src/components/sidebar/SidebarTab.svelte b/web/src/components/sidebar/SidebarTab.svelte index 267d55f1..9f3b51ef 100644 --- a/web/src/components/sidebar/SidebarTab.svelte +++ b/web/src/components/sidebar/SidebarTab.svelte @@ -28,11 +28,6 @@ $: if (isTabActive && tab) { showTab(tab); - - tab.classList.add("animate"); - setTimeout(() => { - tab.classList.remove("animate"); - }, 250); } @@ -59,11 +54,11 @@ flex-direction: column; align-items: center; text-align: center; - gap: 5px; - padding: var(--padding) 5px; + gap: 3px; + padding: var(--padding) 3px; color: var(--sidebar-highlight); font-size: var(--sidebar-font-size); - opacity: 0.8; + opacity: 0.75; height: fit-content; border-radius: var(--border-radius); transition: transform 0.2s; @@ -72,12 +67,14 @@ text-decoration-line: none; position: relative; scroll-behavior: smooth; + + cursor: pointer; } .sidebar-tab :global(svg) { stroke-width: 1.2px; - height: 21px; - width: 21px; + height: 22px; + width: 22px; } :global([data-iphone="true"] .sidebar-tab svg) { @@ -88,19 +85,17 @@ color: var(--sidebar-bg); background: var(--sidebar-highlight); opacity: 1; - transition: none; transform: none; + transition: none; + animation: pressButton 0.3s; + cursor: default; } - :global(.sidebar-tab.animate) { - animation: pressButton 0.2s; - } - - .sidebar-tab:active:not(.active) { + .sidebar-tab:not(.active):active { transform: scale(0.95); } - :global([data-reduce-motion="true"]) .sidebar-tab:active:not(.active) { + :global([data-reduce-motion="true"]) .sidebar-tab:active { transform: none; } @@ -112,10 +107,10 @@ @keyframes pressButton { 0% { - transform: scale(0.95); + transform: scale(0.9); } 50% { - transform: scale(1.02); + transform: scale(1.015); } 100% { transform: scale(1); @@ -127,6 +122,7 @@ opacity: 1; background-color: var(--sidebar-hover); } + .sidebar-tab:hover:not(.active) { opacity: 1; background-color: var(--sidebar-hover); @@ -149,10 +145,10 @@ @keyframes pressButton { 0% { - transform: scale(0.9); + transform: scale(0.8); } - 60% { - transform: scale(1.015); + 50% { + transform: scale(1.02); } 100% { transform: scale(1); diff --git a/web/src/components/subnav/PageNav.svelte b/web/src/components/subnav/PageNav.svelte index c3255c78..7fa46b0e 100644 --- a/web/src/components/subnav/PageNav.svelte +++ b/web/src/components/subnav/PageNav.svelte @@ -73,7 +73,10 @@ {:else} {#if pageSubtitle} -