diff --git a/platform_web/components/AppShell.test.tsx b/platform_web/components/AppShell.test.tsx
index c049ac6..5dfb39e 100644
--- a/platform_web/components/AppShell.test.tsx
+++ b/platform_web/components/AppShell.test.tsx
@@ -37,6 +37,21 @@ describe("AppShell mobile navigation", () => {
expect(html).toContain("关闭导航菜单");
});
+ it("renders the animation layer above the workspace background", () => {
+ const html = renderToStaticMarkup(
+ undefined}>
+ 页面内容
+
+ );
+
+ expect(html.indexOf("workspace-background-layer")).toBeLessThan(html.indexOf("global-particle-layer"));
+ expect(html).toContain("global-orbital-construct");
+ expect(html).toContain("global-particle-lightfall");
+ expect(html).toContain("global-particle-side-ray");
+ expect(html).not.toContain("global-particle-orbit");
+ expect(html).not.toContain("global-particle-frame");
+ });
+
it("uses bounded edge-swipe thresholds and accessible dismissal", () => {
expect(appShellSource).toContain("start.x <= 28 && deltaX >= 56");
expect(appShellSource).toContain("deltaX <= -56");
diff --git a/platform_web/components/MagicalParticleLayer.tsx b/platform_web/components/MagicalParticleLayer.tsx
index 9776235..9f4e4ee 100644
--- a/platform_web/components/MagicalParticleLayer.tsx
+++ b/platform_web/components/MagicalParticleLayer.tsx
@@ -11,23 +11,53 @@ const particleSlots = [
{ x: "92%", y: "86%" }
];
+const orbitalConstructSlots = [
+ { x: "76%", y: "82%", size: "min(54vw, 680px)", drift: "-6deg" },
+ { x: "14%", y: "58%", size: "min(34vw, 420px)", drift: "18deg" },
+ { x: "94%", y: "22%", size: "min(24vw, 300px)", drift: "-26deg" }
+];
+
type ParticleStyle = CSSProperties & {
"--particle-index": string;
"--particle-x": string;
"--particle-y": string;
};
+type OrbitalConstructStyle = CSSProperties & {
+ "--construct-index": string;
+ "--construct-x": string;
+ "--construct-y": string;
+ "--construct-size": string;
+ "--construct-drift": string;
+};
+
export function MagicalParticleLayer() {
return (
<>
-
-
+
+
+
+
+
+ {orbitalConstructSlots.map((construct, index) => (
+
+ ))}
-
-
{particleSlots.map((slot, index) => (
{
expect(serversPageSource).toContain('className="provider-form dialog-form"');
expect(serversPageSource).toContain('name="profileKey"');
expect(serversPageSource).toContain("runtimeBindingFields");
+ expect(serversPageSource).toContain("createProfileUnavailable");
+ expect(serversPageSource).toContain("当前插件没有声明运行配置");
expect(serversPageSource).toContain("updateBinding(field.key");
expect(serversPageSource).toContain('type={field.sensitive ? "password" : "text"}');
for (const forbidden of ["secret://", "/Users/", "/var/run/", "unix://", "tcp://"]) {
diff --git a/platform_web/pages/ServersPage.tsx b/platform_web/pages/ServersPage.tsx
index d558986..000e182 100644
--- a/platform_web/pages/ServersPage.tsx
+++ b/platform_web/pages/ServersPage.tsx
@@ -139,6 +139,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
const createPending = operations.isPending("platform", "创建服务器");
const canManageServers = session.capabilities.includes("servers.manage");
const selectedCreatePlugin = plugins.find((plugin) => plugin.id === form.pluginId);
+ const createProfileOptions = selectedCreatePlugin?.runtimeProfiles?.lifecycleProfiles ?? [];
+ const createProfileUnavailable = Boolean(selectedCreatePlugin && createProfileOptions.length === 0);
const createBindingFields = runtimeBindingFields(selectedCreatePlugin, form.profileKey);
function updateForm(event: ChangeEvent) {
@@ -442,13 +444,19 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
+ {createProfileUnavailable && (
+
+
+
当前插件没有声明运行配置,请重新注册完整 manifest 后刷新。
+
+ )}
{createBindingFields.map((field) => (
-