feat: tune visual of install instructions page
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
import { tick } from "svelte";
|
||||||
|
|
||||||
|
export function createHeightStageAnimator({
|
||||||
|
durationMs = 360,
|
||||||
|
getElement = () => null,
|
||||||
|
settleDelayMs = 80,
|
||||||
|
setState = () => {},
|
||||||
|
} = {}) {
|
||||||
|
let timer = null;
|
||||||
|
let frame = null;
|
||||||
|
let animationId = 0;
|
||||||
|
|
||||||
|
function clearPending() {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
if (timer) {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
if (frame) {
|
||||||
|
window.cancelAnimationFrame(frame);
|
||||||
|
frame = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function animate(applyChange) {
|
||||||
|
const element = getElement();
|
||||||
|
if (typeof window === "undefined" || !element) {
|
||||||
|
applyChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAnimationId = ++animationId;
|
||||||
|
clearPending();
|
||||||
|
|
||||||
|
const startHeight = Math.ceil(element.getBoundingClientRect().height);
|
||||||
|
if (!startHeight) {
|
||||||
|
applyChange();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({ instant: true, locked: true, style: `height:${startHeight}px;` });
|
||||||
|
await tick();
|
||||||
|
if (currentAnimationId !== animationId) return;
|
||||||
|
|
||||||
|
void element.offsetHeight;
|
||||||
|
applyChange();
|
||||||
|
await tick();
|
||||||
|
if (currentAnimationId !== animationId) return;
|
||||||
|
|
||||||
|
const endHeight = Math.max(1, Math.ceil(element.scrollHeight));
|
||||||
|
frame = window.requestAnimationFrame(() => {
|
||||||
|
frame = null;
|
||||||
|
if (currentAnimationId !== animationId) return;
|
||||||
|
|
||||||
|
setState({ instant: false, locked: true, style: `height:${endHeight}px;` });
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
timer = null;
|
||||||
|
if (currentAnimationId !== animationId) return;
|
||||||
|
setState({ instant: false, locked: false, style: "" });
|
||||||
|
}, durationMs + settleDelayMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
animationId += 1;
|
||||||
|
clearPending();
|
||||||
|
setState({ instant: false, locked: false, style: "" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
animate,
|
||||||
|
destroy,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2013,6 +2013,43 @@ a {
|
|||||||
animation: toast-enter 0.22s ease-out both;
|
animation: toast-enter 0.22s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.motion-height-stage {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-height-stage.motion-height-locked {
|
||||||
|
overflow: hidden;
|
||||||
|
transition: height var(--motion-stage-duration, 360ms) cubic-bezier(0.22, 0.72, 0.2, 1);
|
||||||
|
will-change: height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-height-stage.motion-height-instant {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-enter-card {
|
||||||
|
animation: motion-card-enter var(--motion-enter-duration, 340ms) cubic-bezier(0.22, 0.72, 0.2, 1)
|
||||||
|
backwards;
|
||||||
|
animation-delay: var(--motion-delay, 0ms);
|
||||||
|
transform-origin: var(--motion-origin, center top);
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-fade-up {
|
||||||
|
animation: motion-fade-up var(--motion-enter-duration, 160ms) ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-scale-in {
|
||||||
|
animation: motion-scale-in var(--motion-enter-duration, 240ms) cubic-bezier(0.22, 0.72, 0.2, 1)
|
||||||
|
both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.motion-shimmer {
|
||||||
|
animation: motion-shimmer var(--motion-shimmer-duration, 1.15s) ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes section-enter {
|
@keyframes section-enter {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -2037,6 +2074,56 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes motion-card-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate3d(0, 12px, 0) scale(0.985);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes motion-fade-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes motion-scale-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.975);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes motion-shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 120% 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: -120% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes toast-enter {
|
@keyframes toast-enter {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -2234,11 +2321,16 @@ a {
|
|||||||
.dialog-backdrop,
|
.dialog-backdrop,
|
||||||
.dialog-card,
|
.dialog-card,
|
||||||
.auth-mode-panel,
|
.auth-mode-panel,
|
||||||
|
.motion-enter-card,
|
||||||
|
.motion-fade-up,
|
||||||
|
.motion-scale-in,
|
||||||
|
.motion-shimmer,
|
||||||
.toast {
|
.toast {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-card {
|
.auth-card,
|
||||||
|
.motion-height-stage.motion-height-locked {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte";
|
import { getContext, onDestroy, onMount } from "svelte";
|
||||||
import { cubicOut } from "svelte/easing";
|
|
||||||
import { fade, fly, scale } from "svelte/transition";
|
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
@@ -18,6 +16,7 @@
|
|||||||
import { Select } from "$components/ui/primitives.js";
|
import { Select } from "$components/ui/primitives.js";
|
||||||
import Button from "$components/ui/button.svelte";
|
import Button from "$components/ui/button.svelte";
|
||||||
import Card from "$components/ui/card.svelte";
|
import Card from "$components/ui/card.svelte";
|
||||||
|
import { createHeightStageAnimator } from "$lib/webapp/motion/heightStage.js";
|
||||||
|
|
||||||
export let currentLang = "ru";
|
export let currentLang = "ru";
|
||||||
export let telegramPlatform = "";
|
export let telegramPlatform = "";
|
||||||
@@ -32,6 +31,9 @@
|
|||||||
export let publicMode = false;
|
export let publicMode = false;
|
||||||
|
|
||||||
const installGuidesStore = getContext("installGuidesStore");
|
const installGuidesStore = getContext("installGuidesStore");
|
||||||
|
const STAGE_HEIGHT_ANIMATION_MS = 360;
|
||||||
|
const CARD_STAGGER_MS = 46;
|
||||||
|
const QR_DELAY_EXTRA_MS = 90;
|
||||||
const colorTokens = {
|
const colorTokens = {
|
||||||
amber: "#f59e0b",
|
amber: "#f59e0b",
|
||||||
blue: "#3b82f6",
|
blue: "#3b82f6",
|
||||||
@@ -61,11 +63,23 @@
|
|||||||
let qrDataUrl = "";
|
let qrDataUrl = "";
|
||||||
let lastQrValue = "";
|
let lastQrValue = "";
|
||||||
let qrRequestId = 0;
|
let qrRequestId = 0;
|
||||||
|
let installContentStage;
|
||||||
|
let stageHeightStyle = "";
|
||||||
|
let stageHeightLocked = false;
|
||||||
|
let stageHeightInstant = false;
|
||||||
|
const installStageAnimator = createHeightStageAnimator({
|
||||||
|
durationMs: STAGE_HEIGHT_ANIMATION_MS,
|
||||||
|
getElement: () => installContentStage,
|
||||||
|
settleDelayMs: QR_DELAY_EXTRA_MS + 80,
|
||||||
|
setState: setInstallStageState,
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!publicMode) installGuidesStore?.load();
|
if (!publicMode) installGuidesStore?.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => installStageAnimator.destroy());
|
||||||
|
|
||||||
$: guideState = $installGuidesStore;
|
$: guideState = $installGuidesStore;
|
||||||
$: config = guideState?.config || null;
|
$: config = guideState?.config || null;
|
||||||
$: platforms = Object.entries(config?.platforms || {})
|
$: platforms = Object.entries(config?.platforms || {})
|
||||||
@@ -94,6 +108,11 @@
|
|||||||
$: apps = selectedPlatform?.apps || [];
|
$: apps = selectedPlatform?.apps || [];
|
||||||
$: if (selectedAppIndex >= apps.length) selectedAppIndex = 0;
|
$: if (selectedAppIndex >= apps.length) selectedAppIndex = 0;
|
||||||
$: selectedApp = apps[selectedAppIndex] || apps[0] || null;
|
$: selectedApp = apps[selectedAppIndex] || apps[0] || null;
|
||||||
|
$: selectedBlocks = Array.isArray(selectedApp?.blocks) ? selectedApp.blocks : [];
|
||||||
|
$: hasAppSelector = apps.length > 1;
|
||||||
|
$: stepsDelayOffset = hasAppSelector ? apps.length + 1 : 0;
|
||||||
|
$: qrDelayIndex = stepsDelayOffset + selectedBlocks.length + 1;
|
||||||
|
$: installStageStyle = `${stageHeightStyle} --motion-stage-duration:${STAGE_HEIGHT_ANIMATION_MS}ms;`;
|
||||||
$: guideSubscription = guideState?.subscription || subscription || {};
|
$: guideSubscription = guideState?.subscription || subscription || {};
|
||||||
$: finalSubscriptionLink =
|
$: finalSubscriptionLink =
|
||||||
guideSubscription?.config_link ||
|
guideSubscription?.config_link ||
|
||||||
@@ -132,9 +151,30 @@
|
|||||||
return value ? `--install-icon-color:${value};` : "";
|
return value ? `--install-icon-color:${value};` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setInstallStageState({ instant, locked, style }) {
|
||||||
|
stageHeightInstant = instant;
|
||||||
|
stageHeightLocked = locked;
|
||||||
|
stageHeightStyle = style;
|
||||||
|
}
|
||||||
|
|
||||||
function selectPlatform(key) {
|
function selectPlatform(key) {
|
||||||
selectedPlatformKey = key;
|
if (key === selectedPlatformKey) return;
|
||||||
selectedAppIndex = 0;
|
installStageAnimator.animate(() => {
|
||||||
|
selectedPlatformKey = key;
|
||||||
|
selectedAppIndex = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectApp(index) {
|
||||||
|
if (index === selectedAppIndex) return;
|
||||||
|
installStageAnimator.animate(() => {
|
||||||
|
selectedAppIndex = index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function installMotionStyle(index, extraDelay = 0) {
|
||||||
|
const delay = Math.max(0, index) * CARD_STAGGER_MS + Math.max(0, extraDelay);
|
||||||
|
return `--motion-delay:${delay}ms;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function platformFallbackIcon(key) {
|
function platformFallbackIcon(key) {
|
||||||
@@ -359,7 +399,7 @@
|
|||||||
|
|
||||||
{#if guideState?.loading && !guideState?.loaded}
|
{#if guideState?.loading && !guideState?.loaded}
|
||||||
<div
|
<div
|
||||||
class="install-loading"
|
class="install-loading motion-fade-up"
|
||||||
role="status"
|
role="status"
|
||||||
aria-label={t("wa_install_loading", {}, "Loading instructions...")}
|
aria-label={t("wa_install_loading", {}, "Loading instructions...")}
|
||||||
>
|
>
|
||||||
@@ -375,154 +415,164 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
{:else}
|
{:else}
|
||||||
{#key selectedPlatformKey}
|
<div
|
||||||
{#if apps.length > 1}
|
class="install-content-stage motion-height-stage"
|
||||||
<section
|
class:motion-height-locked={stageHeightLocked}
|
||||||
class="install-selector-block"
|
class:motion-height-instant={stageHeightInstant}
|
||||||
aria-label={t("wa_install_app", {}, "App")}
|
style={installStageStyle}
|
||||||
in:fly={{ y: 8, duration: 180, easing: cubicOut }}
|
bind:this={installContentStage}
|
||||||
out:fade={{ duration: 90 }}
|
>
|
||||||
>
|
{#key selectedPlatformKey}
|
||||||
<div class="install-section-title">
|
{#if hasAppSelector}
|
||||||
<span>{t("wa_install_app", {}, "App")}</span>
|
<section
|
||||||
|
class="install-selector-block motion-enter-card"
|
||||||
|
style={installMotionStyle(0)}
|
||||||
|
aria-label={t("wa_install_app", {}, "App")}
|
||||||
|
>
|
||||||
|
<div class="install-section-title">
|
||||||
|
<span>{t("wa_install_app", {}, "App")}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="install-apps"
|
||||||
|
class:apps-mobile-remainder-one={apps.length % 2 === 1}
|
||||||
|
class:apps-remainder-one={apps.length % 3 === 1}
|
||||||
|
class:apps-remainder-two={apps.length % 3 === 2}
|
||||||
|
>
|
||||||
|
{#each apps as app, index (`${selectedPlatformKey}:${app.name}:${index}`)}
|
||||||
|
<button
|
||||||
|
class="install-app-button attention-wrap motion-enter-card"
|
||||||
|
class:active={selectedAppIndex === index}
|
||||||
|
class:featured={app.featured}
|
||||||
|
style={installMotionStyle(index + 1)}
|
||||||
|
type="button"
|
||||||
|
onclick={() => selectApp(index)}
|
||||||
|
>
|
||||||
|
{#if app.featured}
|
||||||
|
<AttentionDot class="install-feature-star" />
|
||||||
|
{/if}
|
||||||
|
{#if iconSvg(app.svgIconKey)}
|
||||||
|
<span class="install-svg" aria-hidden="true"
|
||||||
|
>{@html iconSvg(app.svgIconKey)}</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<span>{app.name}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
{#if selectedApp}
|
||||||
|
{#key `${selectedPlatformKey}:${selectedAppIndex}:${selectedApp.name}`}
|
||||||
|
<section
|
||||||
|
class="install-steps"
|
||||||
|
aria-label={selectedApp.name}
|
||||||
|
style={installMotionStyle(stepsDelayOffset)}
|
||||||
|
>
|
||||||
|
{#each selectedBlocks as block, blockIndex (`${selectedPlatformKey}:${selectedApp.name}:${blockIndex}:${localized(block.title)}`)}
|
||||||
|
<div
|
||||||
|
class="install-step-motion motion-enter-card"
|
||||||
|
style={installMotionStyle(stepsDelayOffset + blockIndex)}
|
||||||
|
>
|
||||||
|
<Card class="install-step">
|
||||||
|
<div
|
||||||
|
class="install-step-icon"
|
||||||
|
style={iconColorStyle(block.svgIconColor)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{#if iconSvg(block.svgIconKey)}
|
||||||
|
{@html iconSvg(block.svgIconKey)}
|
||||||
|
{:else}
|
||||||
|
<Check size={19} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="install-step-body">
|
||||||
|
<h2>{localized(block.title)}</h2>
|
||||||
|
<p>{localized(block.description)}</p>
|
||||||
|
{#if block.buttons?.length}
|
||||||
|
<div class="install-actions">
|
||||||
|
{#each block.buttons as button}
|
||||||
|
<Button
|
||||||
|
variant={button.type === "copyButton" ? "secondary" : "default"}
|
||||||
|
onclick={() => handleButton(button)}
|
||||||
|
>
|
||||||
|
{#if button.type === "copyButton"}
|
||||||
|
<Copy size={16} />
|
||||||
|
{:else}
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
{/if}
|
||||||
|
{localized(button.text)}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
{/key}
|
||||||
|
{#if finalSubscriptionLink && !publicMode}
|
||||||
|
<div
|
||||||
|
class="install-qr-divider motion-enter-card"
|
||||||
|
style={installMotionStyle(qrDelayIndex, QR_DELAY_EXTRA_MS)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 240 18" preserveAspectRatio="none">
|
||||||
|
<path
|
||||||
|
d="M0 9 Q 4 2 8 9 T 16 9 T 24 9 T 32 9 T 40 9 T 48 9 T 56 9 T 64 9 T 72 9 T 80 9 T 88 9 T 96 9 T 104 9 T 112 9 T 120 9 T 128 9 T 136 9 T 144 9 T 152 9 T 160 9 T 168 9 T 176 9 T 184 9 T 192 9 T 200 9 T 208 9 T 216 9 T 224 9 T 232 9 T 240 9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="install-apps"
|
class="install-subscription-motion motion-enter-card"
|
||||||
class:apps-mobile-remainder-one={apps.length % 2 === 1}
|
style={installMotionStyle(qrDelayIndex + 1, QR_DELAY_EXTRA_MS)}
|
||||||
class:apps-remainder-one={apps.length % 3 === 1}
|
|
||||||
class:apps-remainder-two={apps.length % 3 === 2}
|
|
||||||
>
|
>
|
||||||
{#each apps as app, index (`${selectedPlatformKey}:${app.name}:${index}`)}
|
<Card class="install-subscription-card">
|
||||||
<button
|
<div class="install-subscription-header">
|
||||||
class="install-app-button attention-wrap"
|
<div class="install-subscription-header-icon" aria-hidden="true">
|
||||||
class:active={selectedAppIndex === index}
|
<QrCode size={20} />
|
||||||
class:featured={app.featured}
|
</div>
|
||||||
type="button"
|
<div class="install-subscription-heading">
|
||||||
onclick={() => (selectedAppIndex = index)}
|
<h2>{t("wa_install_subscription_link", {}, "Subscription link")}</h2>
|
||||||
in:scale={{
|
<p>
|
||||||
start: 0.97,
|
{t(
|
||||||
duration: 150 + Math.min(index, 5) * 18,
|
"wa_install_subscription_link_hint",
|
||||||
easing: cubicOut,
|
{},
|
||||||
}}
|
"Scan the QR code or copy the link."
|
||||||
>
|
)}
|
||||||
{#if app.featured}
|
</p>
|
||||||
<AttentionDot class="install-feature-star" />
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
{#if iconSvg(app.svgIconKey)}
|
<div class="install-subscription-body">
|
||||||
<span class="install-svg" aria-hidden="true">{@html iconSvg(app.svgIconKey)}</span
|
<div class="install-qr-wrap" class:ready={qrDataUrl}>
|
||||||
>
|
{#if qrDataUrl}
|
||||||
{/if}
|
<img
|
||||||
<span>{app.name}</span>
|
class="motion-scale-in"
|
||||||
</button>
|
src={qrDataUrl}
|
||||||
{/each}
|
alt={t("wa_install_qr_alt", {}, "Subscription QR code")}
|
||||||
</div>
|
/>
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
{#if selectedApp}
|
|
||||||
{#key `${selectedPlatformKey}:${selectedAppIndex}:${selectedApp.name}`}
|
|
||||||
<section
|
|
||||||
class="install-steps"
|
|
||||||
aria-label={selectedApp.name}
|
|
||||||
in:fly={{ y: 12, duration: 190, easing: cubicOut }}
|
|
||||||
out:fade={{ duration: 90 }}
|
|
||||||
>
|
|
||||||
{#each selectedApp.blocks as block, blockIndex (`${selectedPlatformKey}:${selectedApp.name}:${blockIndex}:${localized(block.title)}`)}
|
|
||||||
<div
|
|
||||||
class="install-step-motion"
|
|
||||||
in:fly={{
|
|
||||||
y: 10,
|
|
||||||
duration: 170 + Math.min(blockIndex, 6) * 18,
|
|
||||||
easing: cubicOut,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Card class="install-step">
|
|
||||||
<div
|
|
||||||
class="install-step-icon"
|
|
||||||
style={iconColorStyle(block.svgIconColor)}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{#if iconSvg(block.svgIconKey)}
|
|
||||||
{@html iconSvg(block.svgIconKey)}
|
|
||||||
{:else}
|
{:else}
|
||||||
<Check size={19} />
|
<span class="install-qr-placeholder motion-shimmer" aria-hidden="true"></span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="install-step-body">
|
<div class="install-actions install-subscription-actions">
|
||||||
<h2>{localized(block.title)}</h2>
|
<Button variant="secondary" onclick={copySubscriptionLink}>
|
||||||
<p>{localized(block.description)}</p>
|
<Copy size={16} />
|
||||||
{#if block.buttons?.length}
|
{t("wa_install_copy_subscription_link", {}, "Copy link")}
|
||||||
<div class="install-actions">
|
</Button>
|
||||||
{#each block.buttons as button}
|
<Button onclick={shareInstallGuide}>
|
||||||
<Button
|
<Share2 size={16} />
|
||||||
variant={button.type === "copyButton" ? "secondary" : "default"}
|
{t("wa_install_share", {}, "Share")}
|
||||||
onclick={() => handleButton(button)}
|
</Button>
|
||||||
>
|
|
||||||
{#if button.type === "copyButton"}
|
|
||||||
<Copy size={16} />
|
|
||||||
{:else}
|
|
||||||
<ExternalLink size={16} />
|
|
||||||
{/if}
|
|
||||||
{localized(button.text)}
|
|
||||||
</Button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
{/key}
|
|
||||||
{#if finalSubscriptionLink && !publicMode}
|
|
||||||
<div class="install-qr-divider" aria-hidden="true" in:fade={{ duration: 180 }}>
|
|
||||||
<svg viewBox="0 0 240 18" preserveAspectRatio="none">
|
|
||||||
<path
|
|
||||||
d="M0 9 Q 4 2 8 9 T 16 9 T 24 9 T 32 9 T 40 9 T 48 9 T 56 9 T 64 9 T 72 9 T 80 9 T 88 9 T 96 9 T 104 9 T 112 9 T 120 9 T 128 9 T 136 9 T 144 9 T 152 9 T 160 9 T 168 9 T 176 9 T 184 9 T 192 9 T 200 9 T 208 9 T 216 9 T 224 9 T 232 9 T 240 9"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="install-subscription-motion"
|
|
||||||
in:fly={{ y: 10, duration: 200, easing: cubicOut }}
|
|
||||||
>
|
|
||||||
<Card class="install-subscription-card">
|
|
||||||
<div class="install-subscription-header">
|
|
||||||
<div class="install-subscription-header-icon" aria-hidden="true">
|
|
||||||
<QrCode size={20} />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="install-subscription-heading">
|
</Card>
|
||||||
<h2>{t("wa_install_subscription_link", {}, "Subscription link")}</h2>
|
</div>
|
||||||
<p>
|
{/if}
|
||||||
{t("wa_install_subscription_link_hint", {}, "Scan the QR code or copy the link.")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="install-subscription-body">
|
|
||||||
{#if qrDataUrl}
|
|
||||||
<div
|
|
||||||
class="install-qr-wrap"
|
|
||||||
in:scale={{ start: 0.96, duration: 180, easing: cubicOut }}
|
|
||||||
>
|
|
||||||
<img src={qrDataUrl} alt={t("wa_install_qr_alt", {}, "Subscription QR code")} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="install-actions install-subscription-actions">
|
|
||||||
<Button variant="secondary" onclick={copySubscriptionLink}>
|
|
||||||
<Copy size={16} />
|
|
||||||
{t("wa_install_copy_subscription_link", {}, "Copy link")}
|
|
||||||
</Button>
|
|
||||||
<Button onclick={shareInstallGuide}>
|
|
||||||
<Share2 size={16} />
|
|
||||||
{t("wa_install_share", {}, "Share")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -595,13 +645,16 @@
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
animation: install-loader-enter 0.16s ease-out both;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-loading :global(.ui-spinner) {
|
.install-loading :global(.ui-spinner) {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.install-content-stage {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.install-selector-block {
|
.install-selector-block {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
@@ -924,16 +977,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.install-qr-wrap {
|
.install-qr-wrap {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
min-width: 172px;
|
||||||
|
max-width: 236px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: color-mix(in srgb, var(--panel-3) 64%, transparent);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
border-color 0.18s ease,
|
||||||
|
background 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-qr-wrap.ready {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-qr-wrap img {
|
.install-qr-wrap img {
|
||||||
@@ -943,6 +1007,20 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.install-qr-placeholder {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
color-mix(in srgb, var(--muted) 9%, transparent) 0%,
|
||||||
|
color-mix(in srgb, var(--muted) 18%, transparent) 42%,
|
||||||
|
color-mix(in srgb, var(--muted) 9%, transparent) 84%
|
||||||
|
);
|
||||||
|
background-size: 220% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.theme-dark) .install-qr-wrap img {
|
:global(.theme-dark) .install-qr-wrap img {
|
||||||
filter: brightness(0) invert(1);
|
filter: brightness(0) invert(1);
|
||||||
}
|
}
|
||||||
@@ -1035,6 +1113,10 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.install-feature-star.attention-dot) {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
.install-apps button.active,
|
.install-apps button.active,
|
||||||
.install-apps button:active,
|
.install-apps button:active,
|
||||||
.install-apps button:hover,
|
.install-apps button:hover,
|
||||||
@@ -1096,16 +1178,4 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes install-loader-enter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user