feat(dashboard): replace antd Spin with OctopSpinner globally

Match the boot-splash loading ring on first paint, and hyphenate the octop-assistant skill name.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jubaoliang
2026-09-03 17:58:41 +08:00
committed by jubaoliang
co-authored by Cursor
parent a2160cdccd
commit f3d301f92c
5 changed files with 142 additions and 2 deletions
+8 -1
View File
@@ -16,6 +16,7 @@ import OidcComplete from "./pages/Login/OidcComplete";
import SetupPage from "./pages/Setup";
import InvitePage from "./pages/Invite";
import AuthGuard from "./components/AuthGuard";
import OctopSpinner from "./components/OctopSpinner";
import { AntdAppProvider } from "./components/AntdAppProvider";
import GlobalErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider, useTheme } from "./context/ThemeContext";
@@ -29,6 +30,7 @@ import { brandTokensFor } from "./styles/themePalettes";
import "./styles/theme-vars.css";
import "./styles/layout.css";
import "./styles/form-override.css";
import "./styles/spin-override.css";
const GlobalStyle = createGlobalStyle`
* {
@@ -114,7 +116,12 @@ function ThemedApp() {
};
return (
<ConfigProvider theme={themeConfig} prefixCls="octop" locale={antdLocale}>
<ConfigProvider
theme={themeConfig}
prefixCls="octop"
locale={antdLocale}
spin={{ indicator: <OctopSpinner /> }}
>
<AntdAppProvider>
<DesktopChromeProvider value={desktopChrome}>
{desktopChrome ? (
@@ -0,0 +1,66 @@
// Sizing vars come from `.octop-spin` in styles/spin-override.css so the
// indicator can follow antd's size prop; fallbacks cover standalone usage.
.host {
position: relative;
display: inline-block;
width: var(--octop-spinner-size, 24px);
height: var(--octop-spinner-size, 24px);
font-size: 0;
vertical-align: middle;
}
.ring {
position: absolute;
inset: 0;
box-sizing: border-box;
border: var(--octop-spinner-ring-width, 2.5px) solid
color-mix(in srgb, var(--fn-color-brand) 20%, transparent);
border-top-color: var(--fn-color-brand);
border-radius: 50%;
animation: octopSpinnerSpin 0.85s linear infinite;
}
// Percent box, not `inset`: an absolutely positioned replaced element with
// `width: auto` falls back to its intrinsic size (logo.svg is 512px).
.logo {
position: absolute;
top: 16%;
left: 16%;
display: none;
width: 68%;
height: 68%;
border-radius: 22%;
object-fit: contain;
animation: octopSpinnerBreathe 1.6s ease-in-out infinite;
}
// Only the large ring is roomy enough for a legible logo; smaller spins sit in
// selects, list rows and inline labels and stay ring-only.
:global(.octop-spin-lg) .logo {
display: block;
}
@keyframes octopSpinnerSpin {
to {
transform: rotate(360deg);
}
}
@keyframes octopSpinnerBreathe {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(0.94);
opacity: 0.88;
}
}
@media (prefers-reduced-motion: reduce) {
.ring,
.logo {
animation: none;
}
}
@@ -0,0 +1,28 @@
import styles from "./OctopSpinner.module.less";
const LOGO = `${import.meta.env.BASE_URL}logo.svg`;
interface OctopSpinnerProps {
/** antd appends `${prefixCls}-dot` when this is the ConfigProvider indicator. */
className?: string;
}
/**
* Loading indicator matching the first-paint boot splash in index.html:
* brand-colored ring around the breathing Octop logo. Wired globally as the
* antd Spin indicator; only `size="large"` shows the logo.
*/
export default function OctopSpinner({ className }: OctopSpinnerProps) {
return (
<span className={[styles.host, className].filter(Boolean).join(" ")}>
<span className={styles.ring} />
<img
className={styles.logo}
src={LOGO}
alt=""
aria-hidden
draggable={false}
/>
</span>
);
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Layout contract for the global antd Spin indicator (OctopSpinner).
* antd sizes and positions its default dot from the `dotSize` token, so every
* geometry rule it derives (box, nested-pattern centering, tip gap) has to be
* restated from one size variable per Spin size.
*/
.octop-spin {
--octop-spinner-size: 24px;
--octop-spinner-ring-width: 2.5px;
}
.octop-spin-lg {
--octop-spinner-size: 48px;
--octop-spinner-ring-width: 4px;
}
.octop-spin-sm {
--octop-spinner-size: 18px;
--octop-spinner-ring-width: 2px;
}
.octop-spin .octop-spin-dot {
width: var(--octop-spinner-size) !important;
height: var(--octop-spinner-size) !important;
}
.octop-spin-nested-loading .octop-spin .octop-spin-dot {
margin: calc(var(--octop-spinner-size) / -2) !important;
}
/* With a tip, lift the ring and drop the text below it. */
.octop-spin-nested-loading .octop-spin-show-text .octop-spin-dot {
margin-top: calc(var(--octop-spinner-size) / -2 - 12px) !important;
}
.octop-spin-nested-loading .octop-spin-show-text .octop-spin-text {
padding-top: calc(var(--octop-spinner-size) / 2 - 4px) !important;
}
@@ -1,5 +1,5 @@
---
name: octop_assistant
name: octop-assistant
description: >-
帮助用户配置和管理 Octop 自身。当用户提出以下类型的问题时使用此 skill:
配置或切换 LLM 模型与 Provider;添加或管理 IM 通道(飞书、企业微信、QQ 等);