refactor(styles, alpine): add prompt loading animations and enhance color palette adjustments
这个提交包含在:
@@ -3,6 +3,7 @@ import type { Alpine } from "alpinejs";
|
||||
import { headerMenu } from "./header-menu";
|
||||
import { postLineNum } from "./post-line-num";
|
||||
import { postToc } from "./post-toc";
|
||||
import { initDevcmPromptLoading } from "./prompt-loading";
|
||||
import { themeComment } from "./theme-comment";
|
||||
import { themeMode } from "./theme-mode";
|
||||
import { upvote } from "./upvote";
|
||||
@@ -20,6 +21,7 @@ export const bootDevcmAlpine = (Alpine: Alpine) => {
|
||||
Alpine.data("themeComment", themeComment);
|
||||
|
||||
initDevcmActivityActions();
|
||||
initDevcmPromptLoading();
|
||||
initTypedText();
|
||||
|
||||
Alpine.start();
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
const PROMPT_LINE_SELECTOR = ".devcm-prompt-stream__line.dividing";
|
||||
const LOADING_CLASS = "devcm-prompt-stream__line--loading";
|
||||
const FINISHING_CLASS = "devcm-prompt-stream__line--finishing";
|
||||
const STORAGE_KEY = "devcm:prompt-loading";
|
||||
const ROUTE_PENDING_VALUE = "route";
|
||||
const FINISH_ANIMATION_NAMES = ["devcm-route-tail-shrink", "devcm-route-finish-state"];
|
||||
|
||||
let removeFinishListener: (() => void) | null = null;
|
||||
|
||||
const getPromptLine = () => document.querySelector<HTMLElement>(PROMPT_LINE_SELECTOR);
|
||||
|
||||
const clearFinishListener = () => {
|
||||
removeFinishListener?.();
|
||||
removeFinishListener = null;
|
||||
};
|
||||
|
||||
const setPendingRoute = () => {
|
||||
try {
|
||||
window.sessionStorage.setItem(STORAGE_KEY, ROUTE_PENDING_VALUE);
|
||||
} catch {
|
||||
// Ignore storage failures in private browsing or restricted environments.
|
||||
}
|
||||
};
|
||||
|
||||
const consumePendingRoute = () => {
|
||||
try {
|
||||
const isPending = window.sessionStorage.getItem(STORAGE_KEY) === ROUTE_PENDING_VALUE;
|
||||
window.sessionStorage.removeItem(STORAGE_KEY);
|
||||
return isPending;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetPromptLoading = () => {
|
||||
const line = getPromptLine();
|
||||
|
||||
clearFinishListener();
|
||||
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
|
||||
line.classList.remove(LOADING_CLASS, FINISHING_CLASS);
|
||||
delete line.dataset.devcmLoading;
|
||||
};
|
||||
|
||||
const startPromptLoading = () => {
|
||||
const line = getPromptLine();
|
||||
|
||||
clearFinishListener();
|
||||
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
|
||||
line.classList.remove(FINISHING_CLASS);
|
||||
line.classList.add(LOADING_CLASS);
|
||||
line.dataset.devcmLoading = "true";
|
||||
};
|
||||
|
||||
const startPromptFinishing = () => {
|
||||
const line = getPromptLine();
|
||||
|
||||
clearFinishListener();
|
||||
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleAnimationEnd = (event: AnimationEvent) => {
|
||||
if (FINISH_ANIMATION_NAMES.includes(event.animationName)) {
|
||||
resetPromptLoading();
|
||||
}
|
||||
};
|
||||
|
||||
line.classList.remove(LOADING_CLASS);
|
||||
line.classList.add(FINISHING_CLASS);
|
||||
line.dataset.devcmLoading = "done";
|
||||
line.addEventListener("animationend", handleAnimationEnd);
|
||||
removeFinishListener = () => line.removeEventListener("animationend", handleAnimationEnd);
|
||||
};
|
||||
|
||||
const finishWhenReady = () => {
|
||||
if (document.readyState === "complete") {
|
||||
startPromptFinishing();
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener("load", startPromptFinishing, { once: true });
|
||||
};
|
||||
|
||||
const isReloadNavigation = () => {
|
||||
if (typeof performance === "undefined" || typeof performance.getEntriesByType !== "function") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [navigationEntry] = performance.getEntriesByType("navigation") as PerformanceNavigationTiming[];
|
||||
return navigationEntry?.type === "reload";
|
||||
};
|
||||
|
||||
const resumeInitialPromptLoading = () => {
|
||||
if (consumePendingRoute()) {
|
||||
startPromptFinishing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReloadNavigation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
startPromptLoading();
|
||||
finishWhenReady();
|
||||
};
|
||||
|
||||
const hasModifiedClick = (event: MouseEvent) => event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
|
||||
const isNavigableAnchor = (anchor: HTMLAnchorElement, event: MouseEvent) => {
|
||||
if (event.defaultPrevented || event.button !== 0 || hasModifiedClick(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anchor.hasAttribute("download")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = anchor.getAttribute("target");
|
||||
if (target && target.toLowerCase() !== "_self") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rawHref = anchor.getAttribute("href");
|
||||
if (!rawHref || rawHref === "#" || rawHref.startsWith("#")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const url = new URL(anchor.href, window.location.href);
|
||||
if (url.origin !== window.location.origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const current = new URL(window.location.href);
|
||||
return url.pathname !== current.pathname || url.search !== current.search;
|
||||
};
|
||||
|
||||
export const initDevcmPromptLoading = () => {
|
||||
resumeInitialPromptLoading();
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const target = event.target;
|
||||
|
||||
if (!(event instanceof MouseEvent) || !(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = target.closest<HTMLAnchorElement>("a[href]");
|
||||
|
||||
if (!anchor || !isNavigableAnchor(anchor, event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingRoute();
|
||||
startPromptLoading();
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", startPromptLoading);
|
||||
window.addEventListener("pagehide", (event) => {
|
||||
if (!event.persisted) {
|
||||
startPromptLoading();
|
||||
}
|
||||
});
|
||||
window.addEventListener("pageshow", (event) => {
|
||||
if (event.persisted) {
|
||||
resetPromptLoading();
|
||||
}
|
||||
});
|
||||
};
|
||||
+11
-11
@@ -10,7 +10,7 @@ body.devcm-body {
|
||||
min-height: 0;
|
||||
background: radial-gradient(circle at 46% 0%, rgba(104, 243, 127, 0.11), transparent 28%),
|
||||
radial-gradient(circle at 85% 18%, rgba(255, 176, 0, 0.1), transparent 28%),
|
||||
linear-gradient(180deg, #090b09 0%, #050706 100%);
|
||||
linear-gradient(180deg, #0f130f 0%, #090c09 100%);
|
||||
color: var(--devcm-text);
|
||||
font-family: var(--devcm-mono);
|
||||
overflow: hidden;
|
||||
@@ -58,18 +58,18 @@ body.devcm-body::after {
|
||||
}
|
||||
|
||||
body.devcm-body::before {
|
||||
opacity: 0.16;
|
||||
opacity: 0.24;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.035) 0,
|
||||
rgba(255, 255, 255, 0.035) 1px,
|
||||
rgba(255, 255, 255, 0.055) 0,
|
||||
rgba(255, 255, 255, 0.055) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 176, 0, 0.025) 0,
|
||||
rgba(255, 176, 0, 0.025) 1px,
|
||||
rgba(255, 176, 0, 0.045) 0,
|
||||
rgba(255, 176, 0, 0.045) 1px,
|
||||
transparent 1px,
|
||||
transparent 28px
|
||||
);
|
||||
@@ -77,8 +77,8 @@ body.devcm-body::before {
|
||||
}
|
||||
|
||||
body.devcm-body::after {
|
||||
background: radial-gradient(ellipse at center, transparent 48%, rgba(0, 0, 0, 0.52) 100%);
|
||||
opacity: 0.62;
|
||||
background: radial-gradient(ellipse at center, transparent 55%, rgba(0, 0, 0, 0.38) 100%);
|
||||
opacity: 0.46;
|
||||
}
|
||||
|
||||
.devcm-shell {
|
||||
@@ -105,9 +105,9 @@ html[data-color-scheme="light"] .devcm-shell {
|
||||
min-height: 0;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(90deg, rgba(255, 176, 0, 0.04) 1px, transparent 1px) 0 0 / 28px 28px,
|
||||
linear-gradient(180deg, rgba(255, 176, 0, 0.032) 1px, transparent 1px) 0 0 / 28px 28px,
|
||||
linear-gradient(180deg, rgba(8, 10, 8, 0.98), rgba(6, 8, 6, 0.98));
|
||||
background: linear-gradient(90deg, rgba(255, 176, 0, 0.064) 1px, transparent 1px) 0 0 / 28px 28px,
|
||||
linear-gradient(180deg, rgba(104, 243, 127, 0.04) 1px, transparent 1px) 0 0 / 28px 28px,
|
||||
linear-gradient(180deg, rgba(13, 17, 13, 0.96), rgba(10, 13, 10, 0.96));
|
||||
}
|
||||
|
||||
html[data-color-scheme="light"] .devcm-app {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
:root,
|
||||
html[data-color-scheme="dark"] {
|
||||
color-scheme: dark;
|
||||
--devcm-bg: #070908;
|
||||
--devcm-bg-deep: #040504;
|
||||
--devcm-panel: rgba(12, 14, 11, 0.94);
|
||||
--devcm-panel-soft: rgba(18, 20, 14, 0.88);
|
||||
--devcm-panel-warm: rgba(22, 18, 10, 0.74);
|
||||
--devcm-bg: #0e120e;
|
||||
--devcm-bg-deep: #090c09;
|
||||
--devcm-panel: rgba(18, 22, 16, 0.94);
|
||||
--devcm-panel-soft: rgba(24, 28, 19, 0.88);
|
||||
--devcm-panel-warm: rgba(31, 25, 14, 0.76);
|
||||
--devcm-line: rgba(255, 176, 0, 0.22);
|
||||
--devcm-line-strong: rgba(255, 176, 0, 0.48);
|
||||
--devcm-text: #ddd6b9;
|
||||
|
||||
@@ -53,6 +53,55 @@
|
||||
z-index: 10;
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&.devcm-prompt-stream__line--loading {
|
||||
opacity: 1;
|
||||
|
||||
&::after {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0.78;
|
||||
filter: blur(3px);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 93, 93, 0.04) 0%,
|
||||
rgba(255, 93, 93, 0.28) 52%,
|
||||
rgba(255, 93, 93, 0.46) 78%,
|
||||
rgba(255, 93, 93, 0.72) 100%
|
||||
);
|
||||
animation-name: devcm-route-fill-grow, devcm-route-loading-pulse;
|
||||
animation-duration: 1.28s, 0.72s;
|
||||
animation-timing-function: cubic-bezier(0.18, 0.72, 0.22, 1), ease-in-out;
|
||||
animation-fill-mode: forwards, none;
|
||||
animation-iteration-count: 1, infinite;
|
||||
animation-direction: normal, alternate;
|
||||
transform: translateY(-50%) scaleX(0);
|
||||
transform-origin: left center;
|
||||
transition: opacity 0.18s ease;
|
||||
will-change: transform, opacity, filter;
|
||||
}
|
||||
}
|
||||
|
||||
&.devcm-prompt-stream__line--finishing {
|
||||
opacity: 1;
|
||||
animation: devcm-route-finish-state 0.86s linear forwards;
|
||||
|
||||
&::after {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0.66;
|
||||
filter: blur(3px);
|
||||
clip-path: inset(0 0 0 0);
|
||||
background: linear-gradient(90deg, rgba(255, 93, 93, 0.05) 0%, rgba(255, 93, 93, 0.62) 70%, transparent 100%);
|
||||
animation-name: devcm-route-tail-shrink;
|
||||
animation-duration: 0.86s;
|
||||
animation-timing-function: cubic-bezier(0.48, 0, 0.22, 1);
|
||||
animation-fill-mode: forwards;
|
||||
transform: translateY(-50%);
|
||||
transition: opacity 0.18s ease;
|
||||
will-change: clip-path, opacity, filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 虚线流动动画 - 模拟加载进度
|
||||
@@ -84,3 +133,39 @@
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes devcm-route-loading-pulse {
|
||||
0% {
|
||||
opacity: 0.58;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.78;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes devcm-route-fill-grow {
|
||||
0% {
|
||||
transform: translateY(-50%) scaleX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes devcm-route-tail-shrink {
|
||||
0% {
|
||||
clip-path: inset(0 0 0 0);
|
||||
opacity: 0.66;
|
||||
}
|
||||
100% {
|
||||
clip-path: inset(0 0 0 100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes devcm-route-finish-state {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@ spec:
|
||||
repo: https://git.dev.cm/theme-terminal
|
||||
settingName: "theme-terminal-setting"
|
||||
configMapName: "theme-terminal-configMap"
|
||||
version: 2.0.8
|
||||
version: 2.0.9
|
||||
require: ">=2.22.0"
|
||||
|
||||
在新议题中引用
屏蔽一个用户