dsh-showcase 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-showcase",
3
+ factory: (require) => {
4
+ const module = { exports: {} };
5
+ const exports = module.exports;
6
+ const React = require("react");
7
+ const { useEffect, useRef, useState } = React;
8
+
9
+ const inject = ["slots", "locale", "connection"];
10
+
11
+ const RPC_CHANNEL = "/showcase-layout-summary";
12
+ const LOCALE_NAMESPACE = "showcase.layoutSummary.settings";
13
+ const STYLE_ID = "dsh-showcase-settings-style";
14
+ const THEME_VALUES = ["frontier-signal", "blue-big-fish"];
15
+
16
+ const messages = {
17
+ zh: {
18
+ title: "布局证据产物",
19
+ description: "先完成采集与测试,再在交付或视觉审核节点生成摘要;海报是可选的重产物。",
20
+ legend: "海报风格",
21
+ dijiang: "终末地帝江号",
22
+ dijiangHint: "冷白工程纸面、硬黑标题、亮黄模块与校准色。",
23
+ fish: "蓝色大肥鱼",
24
+ fishHint: "白底角色主视觉、钴蓝冲击字与金色贴纸。",
25
+ posterLegend: "生成自包含 HTML 海报",
26
+ posterHint: "关闭时只写入 Markdown,不创建或覆盖海报;已有 HTML 不会删除。打开后下一次调用会同时写入海报。",
27
+ posterToggleLabel: "切换是否生成自包含 HTML 海报",
28
+ posterOn: "已开启",
29
+ posterOff: "已关闭",
30
+ saving: "正在保存…",
31
+ saved: "已保存,下一次调用生效。",
32
+ failed: "保存失败,请重试。",
33
+ loading: "正在读取插件设置…",
34
+ unavailable: "插件设置服务当前不可用。",
35
+ },
36
+ en: {
37
+ title: "Layout evidence artifacts",
38
+ description: "Capture and test first, then generate at the delivery or visual-review checkpoint. The poster is optional and heavier.",
39
+ legend: "Poster theme",
40
+ dijiang: "Dijiang",
41
+ dijiangHint: "Cool-white industrial sheet, hard black type, bright-yellow modules, and calibration accents.",
42
+ fish: "Blue Big Fish",
43
+ fishHint: "White character key visual, cobalt impact type, and small gold stickers.",
44
+ posterLegend: "Generate self-contained HTML poster",
45
+ posterHint: "Off writes only Markdown and does not create, overwrite, or delete an existing poster. On also writes the HTML poster on the next call.",
46
+ posterToggleLabel: "Toggle self-contained HTML poster generation",
47
+ posterOn: "On",
48
+ posterOff: "Off",
49
+ saving: "Saving…",
50
+ saved: "Saved. The next tool call will use it.",
51
+ failed: "Could not save the setting. Try again.",
52
+ loading: "Loading plugin settings…",
53
+ unavailable: "Plugin settings are currently unavailable.",
54
+ },
55
+ };
56
+
57
+ const styles = `
58
+ .dsh-showcase-settings{position:relative;isolation:isolate;list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-background-base);overflow:hidden}
59
+ .dsh-showcase-settings::after{position:absolute;z-index:2;top:0;left:0;width:42%;height:2px;content:"";pointer-events:none;background:var(--dsw-alias-state-business-primary);opacity:0;transform:translateX(-110%)}
60
+ .dsh-showcase-settings[data-load-state=loading]::after,.dsh-showcase-settings[data-save-state=saving]::after{opacity:1;animation:dsh-showcase-settings-progress 1.05s cubic-bezier(.22,1,.36,1) infinite}
61
+ .dsh-showcase-settings__head{padding:16px 18px 13px;border-bottom:1px solid var(--dsw-alias-border-l2)}
62
+ .dsh-showcase-settings__title{margin:0;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:650}
63
+ .dsh-showcase-settings__description{margin:4px 0 0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}
64
+ .dsh-showcase-settings__body{padding:16px 18px 17px}
65
+ .dsh-showcase-settings__legend{margin:0 0 10px;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600}
66
+ .dsh-showcase-settings__choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
67
+ .dsh-showcase-settings__choice{position:relative;isolation:isolate;min-width:0;min-height:76px;padding:12px;text-align:left;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-background-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;overflow:hidden;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:border-color .18s cubic-bezier(.22,1,.36,1),background-color .18s cubic-bezier(.22,1,.36,1),transform .14s cubic-bezier(.22,1,.36,1)}
68
+ .dsh-showcase-settings__choice::before{position:absolute;top:10px;right:10px;width:7px;height:7px;border-radius:50%;content:"";pointer-events:none;background:var(--dsw-alias-state-business-primary);opacity:0;transform:scale(.35);transition:opacity .16s ease,transform .18s cubic-bezier(.22,1,.36,1)}
69
+ .dsh-showcase-settings__choice::after{position:absolute;right:11px;bottom:0;left:11px;height:2px;content:"";pointer-events:none;background:var(--dsw-alias-state-business-primary);transform:scaleX(0);transform-origin:left;transition:transform .2s cubic-bezier(.22,1,.36,1)}
70
+ .dsh-showcase-settings__choice[aria-checked=true]{border-color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,var(--dsw-alias-background-elevated))}
71
+ .dsh-showcase-settings__choice[aria-checked=true]::before{opacity:1;transform:scale(1)}
72
+ .dsh-showcase-settings__choice[aria-checked=true]::after{transform:scaleX(1)}
73
+ .dsh-showcase-settings__choice:not(:disabled):active{transform:translateY(0) scale(.99)}
74
+ .dsh-showcase-settings__choice:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
75
+ .dsh-showcase-settings__choice:disabled{cursor:not-allowed;opacity:.58;transform:none}
76
+ .dsh-showcase-settings__name{display:block;font-size:13px;font-weight:650}
77
+ .dsh-showcase-settings__hint{display:block;margin-top:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.45}
78
+ .dsh-showcase-settings__toggle-row{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:16px;padding-top:15px;border-top:1px solid var(--dsw-alias-border-l2)}
79
+ .dsh-showcase-settings__toggle-copy{min-width:0}
80
+ .dsh-showcase-settings__toggle{position:relative;isolation:isolate;flex:0 0 auto;width:50px;height:44px;padding:0;border:0;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent}
81
+ .dsh-showcase-settings__toggle::before{position:absolute;inset:9px 1px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;content:"";background:var(--dsw-alias-background-elevated);transition:background-color .18s cubic-bezier(.22,1,.36,1),border-color .18s cubic-bezier(.22,1,.36,1)}
82
+ .dsh-showcase-settings__toggle::after{position:absolute;top:13px;left:5px;width:18px;height:18px;border-radius:50%;content:"";background:var(--dsw-alias-label-tertiary);transform:translateX(0);transition:transform .2s cubic-bezier(.22,1,.36,1),background-color .18s ease}
83
+ .dsh-showcase-settings__toggle[aria-checked=true]::before{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary)}
84
+ .dsh-showcase-settings__toggle[aria-checked=true]::after{background:var(--dsw-alias-background-base);transform:translateX(22px)}
85
+ .dsh-showcase-settings__toggle:not(:disabled):active::after{transform:translateX(0) scale(.86)}
86
+ .dsh-showcase-settings__toggle[aria-checked=true]:not(:disabled):active::after{transform:translateX(22px) scale(.86)}
87
+ .dsh-showcase-settings__toggle:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:3px}
88
+ .dsh-showcase-settings__toggle:disabled{cursor:not-allowed;opacity:.58}
89
+ .dsh-showcase-settings__toggle-status{display:flex;align-items:center;gap:6px;margin-top:4px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:600}
90
+ .dsh-showcase-settings__toggle-status::before{flex:0 0 auto;width:5px;height:5px;border-radius:50%;content:"";background:var(--dsw-alias-label-tertiary);transition:background-color .16s ease,transform .16s cubic-bezier(.22,1,.36,1)}
91
+ .dsh-showcase-settings__toggle-status[data-enabled=true]::before{background:var(--dsw-alias-state-business-primary);transform:scale(1.2)}
92
+ .dsh-showcase-settings__status{display:flex;align-items:center;gap:7px;min-height:18px;margin:9px 0 0;color:var(--dsw-alias-label-tertiary);font-size:11px}
93
+ .dsh-showcase-settings__status::before{flex:0 0 auto;width:6px;height:6px;border-radius:50%;content:"";background:var(--dsw-alias-label-tertiary);opacity:0;transform:scale(.5);transition:opacity .16s ease,transform .16s cubic-bezier(.22,1,.36,1),background-color .16s ease}
94
+ .dsh-showcase-settings__status[data-state=saving]::before,.dsh-showcase-settings__status[data-state=saved]::before,.dsh-showcase-settings__status[data-state=failed]::before{opacity:1;transform:scale(1)}
95
+ .dsh-showcase-settings__status[data-state=saving]::before{animation:dsh-showcase-status-pulse .8s ease-in-out infinite}
96
+ .dsh-showcase-settings__status[data-state=saved]::before{background:var(--dsw-alias-state-business-primary)}
97
+ .dsh-showcase-settings__status[data-state=failed]{color:var(--dsw-alias-state-danger-primary)}
98
+ .dsh-showcase-settings__status[data-state=failed]::before{background:var(--dsw-alias-state-danger-primary)}
99
+ .dsh-showcase-settings__loading-choices{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
100
+ .dsh-showcase-settings__loading-choice{height:76px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-background-elevated)}
101
+ .dsh-showcase-settings__loading-line{height:8px;margin:13px 12px 0;border-radius:4px;background:color-mix(in srgb,var(--dsw-alias-label-tertiary) 22%,transparent);animation:dsh-showcase-loading-pulse 1.1s ease-in-out infinite}
102
+ .dsh-showcase-settings__loading-line:last-child{width:58%;margin-top:9px;animation-delay:.12s}
103
+ .dsh-showcase-settings__loading-toggle{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:16px;padding-top:15px;border-top:1px solid var(--dsw-alias-border-l2)}
104
+ .dsh-showcase-settings__loading-toggle-copy{flex:1;min-width:0}
105
+ .dsh-showcase-settings__loading-switch{flex:0 0 auto;width:50px;height:26px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-background-elevated);animation:dsh-showcase-loading-pulse 1.1s ease-in-out infinite}
106
+ @keyframes dsh-showcase-settings-progress{0%{transform:translateX(-110%)}100%{transform:translateX(340%)}}
107
+ @keyframes dsh-showcase-loading-pulse{0%,100%{opacity:.45}50%{opacity:1}}
108
+ @keyframes dsh-showcase-status-pulse{0%,100%{opacity:.45;transform:scale(.75)}50%{opacity:1;transform:scale(1)}}
109
+ @media(hover:hover){.dsh-showcase-settings__choice:not(:disabled):hover{border-color:var(--dsw-alias-label-tertiary);background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,var(--dsw-alias-background-elevated));transform:translateY(-1px)}.dsh-showcase-settings__choice[aria-checked=true]:not(:disabled):hover{border-color:var(--dsw-alias-state-business-primary);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 14%,var(--dsw-alias-background-elevated))}.dsh-showcase-settings__choice:not(:disabled):hover:active{transform:translateY(0) scale(.99)}.dsh-showcase-settings__toggle:not(:disabled):hover::before{border-color:var(--dsw-alias-label-tertiary);background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,var(--dsw-alias-background-elevated))}.dsh-showcase-settings__toggle[aria-checked=true]:not(:disabled):hover::before{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-primary)}}
110
+ @media(max-width:640px){.dsh-showcase-settings__choices{grid-template-columns:1fr}.dsh-showcase-settings__toggle-row{align-items:flex-start;flex-direction:column;gap:8px}.dsh-showcase-settings__toggle{margin-left:-1px}.dsh-showcase-settings__status{overflow-wrap:anywhere}}
111
+ @media(max-width:640px){.dsh-showcase-settings__loading-choices{grid-template-columns:1fr}.dsh-showcase-settings__loading-toggle{align-items:flex-start;flex-direction:column;gap:8px}}
112
+ @media(max-width:480px){.dsh-showcase-settings__head,.dsh-showcase-settings__body{padding-inline:10px}.dsh-showcase-settings__choice{min-height:44px;padding:9px}.dsh-showcase-settings__choice::before{top:8px;right:8px}.dsh-showcase-settings__hint{display:none}.dsh-showcase-settings__description{overflow-wrap:anywhere}.dsh-showcase-settings__toggle-copy{max-width:100%;overflow-wrap:anywhere}.dsh-showcase-settings__loading-choice{height:44px}.dsh-showcase-settings__loading-line{margin-top:11px}.dsh-showcase-settings__loading-line:last-child{display:none}}
113
+ @media(forced-colors:active){.dsh-showcase-settings__choice[aria-checked=true]{border-color:Highlight}.dsh-showcase-settings__choice::before,.dsh-showcase-settings__choice::after{background:Highlight}.dsh-showcase-settings__toggle::before{border-color:ButtonText;background:Canvas}.dsh-showcase-settings__toggle::after{background:ButtonText}.dsh-showcase-settings__toggle[aria-checked=true]::before{background:Highlight}.dsh-showcase-settings__toggle[aria-checked=true]::after{background:HighlightText}}
114
+ @media(prefers-reduced-motion:reduce){.dsh-showcase-settings::after,.dsh-showcase-settings__choice,.dsh-showcase-settings__choice::before,.dsh-showcase-settings__choice::after,.dsh-showcase-settings__toggle::before,.dsh-showcase-settings__toggle::after,.dsh-showcase-settings__toggle-status::before,.dsh-showcase-settings__status::before{transition:none}.dsh-showcase-settings[data-load-state=loading]::after,.dsh-showcase-settings[data-save-state=saving]::after{animation:none;opacity:1;transform:none}.dsh-showcase-settings__loading-line,.dsh-showcase-settings__loading-switch,.dsh-showcase-settings__status[data-state=saving]::before{animation:none}.dsh-showcase-settings__choice:hover,.dsh-showcase-settings__choice:not(:disabled):active{transform:none}}
115
+ `;
116
+
117
+ function installStyles() {
118
+ if (document.getElementById(STYLE_ID)) return;
119
+ const tag = document.createElement("style");
120
+ tag.id = STYLE_ID;
121
+ tag.textContent = styles;
122
+ document.head.append(tag);
123
+ }
124
+
125
+ function createThemeCard(rpc) {
126
+ return function ThemeCard({ t }) {
127
+ const [theme, setTheme] = useState();
128
+ const [generatePoster, setGeneratePoster] = useState(false);
129
+ const [loadState, setLoadState] = useState("loading");
130
+ const [saveState, setSaveState] = useState("idle");
131
+ const mountedRef = useRef(true);
132
+ const savingRef = useRef(false);
133
+ const choiceRefs = useRef({});
134
+ useEffect(() => {
135
+ let active = true;
136
+ mountedRef.current = true;
137
+ rpc.call(RPC_CHANNEL, "get", {}).then((result) => {
138
+ if (!active) return;
139
+ const value = result.ok ? result.value : undefined;
140
+ const validTheme = value?.theme === "frontier-signal" || value?.theme === "blue-big-fish";
141
+ const validPosterSetting = value?.generatePoster === undefined || typeof value?.generatePoster === "boolean";
142
+ if (validTheme && validPosterSetting) {
143
+ setTheme(value.theme);
144
+ if (typeof value.generatePoster === "boolean") setGeneratePoster(value.generatePoster);
145
+ setLoadState("ready");
146
+ } else {
147
+ setLoadState("unavailable");
148
+ }
149
+ }).catch(() => {
150
+ if (active) setLoadState("unavailable");
151
+ });
152
+ return () => {
153
+ active = false;
154
+ mountedRef.current = false;
155
+ };
156
+ }, []);
157
+
158
+ if (loadState !== "ready") {
159
+ const loading = loadState === "loading";
160
+ return React.createElement("li", {
161
+ className: "dsh-showcase-settings",
162
+ "data-load-state": loadState,
163
+ "aria-busy": loading,
164
+ },
165
+ React.createElement("div", { className: "dsh-showcase-settings__head" },
166
+ React.createElement("h3", { className: "dsh-showcase-settings__title" }, t("title")),
167
+ React.createElement("p", {
168
+ className: "dsh-showcase-settings__description",
169
+ role: "status",
170
+ "aria-live": "polite",
171
+ }, t(loadState))),
172
+ loading && React.createElement("div", { className: "dsh-showcase-settings__body", "aria-hidden": true },
173
+ React.createElement("div", { className: "dsh-showcase-settings__loading-choices" },
174
+ ...THEME_VALUES.map((value) => React.createElement("div", {
175
+ className: "dsh-showcase-settings__loading-choice",
176
+ key: value,
177
+ },
178
+ React.createElement("div", { className: "dsh-showcase-settings__loading-line" }),
179
+ React.createElement("div", { className: "dsh-showcase-settings__loading-line" })))),
180
+ React.createElement("div", { className: "dsh-showcase-settings__loading-toggle" },
181
+ React.createElement("div", { className: "dsh-showcase-settings__loading-toggle-copy" },
182
+ React.createElement("div", { className: "dsh-showcase-settings__loading-line" }),
183
+ React.createElement("div", { className: "dsh-showcase-settings__loading-line" })),
184
+ React.createElement("div", { className: "dsh-showcase-settings__loading-switch" }))));
185
+ }
186
+
187
+ const disabled = saveState === "saving";
188
+ const save = async (patch, rollback = () => {}) => {
189
+ if (savingRef.current) return;
190
+ savingRef.current = true;
191
+ setSaveState("saving");
192
+ try {
193
+ const result = await rpc.call(RPC_CHANNEL, "set", patch);
194
+ if (!mountedRef.current) return;
195
+ const value = result.ok ? result.value : undefined;
196
+ const validTheme = value?.theme === "frontier-signal" || value?.theme === "blue-big-fish";
197
+ const validPosterSetting = value?.generatePoster === undefined || typeof value?.generatePoster === "boolean";
198
+ if (validTheme && validPosterSetting) {
199
+ setTheme(value.theme);
200
+ if (typeof value.generatePoster === "boolean") setGeneratePoster(value.generatePoster);
201
+ setSaveState("saved");
202
+ } else {
203
+ rollback();
204
+ setSaveState("failed");
205
+ }
206
+ } catch {
207
+ if (mountedRef.current) {
208
+ rollback();
209
+ setSaveState("failed");
210
+ }
211
+ } finally {
212
+ savingRef.current = false;
213
+ }
214
+ };
215
+ const choose = (nextTheme) => {
216
+ if (savingRef.current || nextTheme === theme) return;
217
+ const previousTheme = theme;
218
+ setTheme(nextTheme);
219
+ save({ theme: nextTheme }, () => setTheme(previousTheme));
220
+ };
221
+ const togglePoster = () => {
222
+ if (savingRef.current) return;
223
+ const previousValue = generatePoster;
224
+ setGeneratePoster(!previousValue);
225
+ save({ generatePoster: !previousValue }, () => setGeneratePoster(previousValue));
226
+ };
227
+ const moveChoiceFocus = (event, value) => {
228
+ const currentIndex = THEME_VALUES.indexOf(value);
229
+ let nextIndex;
230
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") nextIndex = (currentIndex + 1) % THEME_VALUES.length;
231
+ if (event.key === "ArrowLeft" || event.key === "ArrowUp") nextIndex = (currentIndex - 1 + THEME_VALUES.length) % THEME_VALUES.length;
232
+ if (event.key === "Home") nextIndex = 0;
233
+ if (event.key === "End") nextIndex = THEME_VALUES.length - 1;
234
+ if (nextIndex === undefined) return;
235
+ event.preventDefault();
236
+ const nextTheme = THEME_VALUES[nextIndex];
237
+ choiceRefs.current[nextTheme]?.focus();
238
+ choose(nextTheme);
239
+ };
240
+
241
+ const choice = (value, nameKey, hintKey) => React.createElement("button", {
242
+ type: "button",
243
+ role: "radio",
244
+ "aria-checked": theme === value,
245
+ className: "dsh-showcase-settings__choice",
246
+ disabled,
247
+ tabIndex: theme === value ? 0 : -1,
248
+ ref: (node) => {
249
+ choiceRefs.current[value] = node;
250
+ },
251
+ onClick: () => choose(value),
252
+ onKeyDown: (event) => moveChoiceFocus(event, value),
253
+ },
254
+ React.createElement("span", { className: "dsh-showcase-settings__name" }, t(nameKey)),
255
+ React.createElement("span", { className: "dsh-showcase-settings__hint" }, t(hintKey)));
256
+
257
+ const status = saveState === "saving"
258
+ ? t("saving")
259
+ : saveState === "saved"
260
+ ? t("saved")
261
+ : saveState === "failed"
262
+ ? t("failed")
263
+ : "";
264
+
265
+ return React.createElement("li", {
266
+ className: "dsh-showcase-settings",
267
+ "data-save-state": saveState,
268
+ "aria-busy": disabled,
269
+ },
270
+ React.createElement("div", { className: "dsh-showcase-settings__head" },
271
+ React.createElement("h3", { className: "dsh-showcase-settings__title" }, t("title")),
272
+ React.createElement("p", { className: "dsh-showcase-settings__description" }, t("description"))),
273
+ React.createElement("div", { className: "dsh-showcase-settings__body" },
274
+ React.createElement("p", { className: "dsh-showcase-settings__legend", id: "dsh-showcase-theme-label" }, t("legend")),
275
+ React.createElement("div", {
276
+ className: "dsh-showcase-settings__choices",
277
+ role: "radiogroup",
278
+ "aria-labelledby": "dsh-showcase-theme-label",
279
+ "aria-busy": disabled,
280
+ },
281
+ choice("frontier-signal", "dijiang", "dijiangHint"),
282
+ choice("blue-big-fish", "fish", "fishHint")),
283
+ React.createElement("div", { className: "dsh-showcase-settings__toggle-row" },
284
+ React.createElement("div", { className: "dsh-showcase-settings__toggle-copy" },
285
+ React.createElement("span", { className: "dsh-showcase-settings__name", id: "dsh-showcase-poster-label" }, t("posterLegend")),
286
+ React.createElement("span", { className: "dsh-showcase-settings__hint", id: "dsh-showcase-poster-hint" }, t("posterHint")),
287
+ React.createElement("span", {
288
+ className: "dsh-showcase-settings__toggle-status",
289
+ "data-enabled": generatePoster,
290
+ }, generatePoster ? t("posterOn") : t("posterOff"))),
291
+ React.createElement("button", {
292
+ type: "button",
293
+ role: "switch",
294
+ "aria-checked": generatePoster,
295
+ "aria-label": t("posterToggleLabel"),
296
+ "aria-labelledby": "dsh-showcase-poster-label",
297
+ "aria-describedby": "dsh-showcase-poster-hint",
298
+ className: "dsh-showcase-settings__toggle",
299
+ disabled,
300
+ onClick: togglePoster,
301
+ })),
302
+ React.createElement("p", {
303
+ className: "dsh-showcase-settings__status",
304
+ "data-state": saveState,
305
+ role: "status",
306
+ "aria-live": "polite",
307
+ "aria-atomic": true,
308
+ }, status)));
309
+ };
310
+ }
311
+
312
+ function apply(ctx) {
313
+ installStyles();
314
+ ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, messages), "dsh-showcase: settings locale");
315
+ const ThemeCard = createThemeCard(ctx.get("connection").rpc);
316
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
317
+ name: "settings.plugin.item",
318
+ id: "showcase-layout-summary",
319
+ order: 30,
320
+ locale: LOCALE_NAMESPACE,
321
+ }, ThemeCard));
322
+ }
323
+
324
+ exports.apply = apply;
325
+ exports.inject = inject;
326
+ return module.exports;
327
+ },
328
+ });
@@ -0,0 +1,18 @@
1
+ export const name: string;
2
+ export const inject: string[];
3
+ export type ShowcaseTheme = "frontier-signal" | "blue-big-fish";
4
+ export type ShowcaseSettings = {
5
+ theme: ShowcaseTheme;
6
+ generatePoster: boolean;
7
+ };
8
+ export const SETTINGS_NAMESPACE: string;
9
+ export const SETTINGS_RPC_CHANNEL: string;
10
+ export const Config: {
11
+ (value?: { theme?: ShowcaseTheme; generatePoster?: boolean }): ShowcaseSettings;
12
+ toJSON(): unknown;
13
+ };
14
+ export function apply(ctx: {
15
+ connection: { rpc: { handle(channel: string, handler: (endpoint: string, payload: unknown) => Promise<unknown>, options: { authority: "loopback" }): unknown } };
16
+ settings: { register(namespace: string, schema: unknown, options: { base: ShowcaseSettings }): { get(): ShowcaseSettings; update(patch: Partial<ShowcaseSettings>): Promise<void> } };
17
+ tools: { register(definition: unknown): void };
18
+ }, config?: { theme?: ShowcaseTheme; generatePoster?: boolean }): void;
@@ -0,0 +1,130 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ import { generateLayoutSummary } from "./layout-summary.js";
5
+
6
+ export const name = "showcase-layout-summary";
7
+ export const inject = ["tools", "settings", "connection"];
8
+ export const SETTINGS_NAMESPACE = settingsNamespace("showcase-layout-summary");
9
+ export const SETTINGS_RPC_CHANNEL = "/showcase-layout-summary";
10
+ export const Config = z.object({
11
+ theme: z.union(["frontier-signal", "blue-big-fish"])
12
+ .default("frontier-signal")
13
+ .description("生成 HTML 海报时使用的视觉风格:终末地帝江号或蓝色大肥鱼。主题只从插件设置读取。"),
14
+ generatePoster: z.boolean()
15
+ .default(false)
16
+ .description("是否生成自包含 HTML 海报。关闭时只生成 Markdown 摘要。"),
17
+ });
18
+
19
+ const outputSchema = {
20
+ type: "object",
21
+ additionalProperties: false,
22
+ properties: {
23
+ locale: { type: "string", required: true, enum: ["zh-CN", "en"] },
24
+ outputPath: { type: "string", required: true, description: "Project-relative path to the generated Markdown file." },
25
+ posterPath: { type: "string", description: "Project-relative path to the self-contained evidence poster when generated." },
26
+ posterGenerated: { type: "boolean", required: true, description: "Whether this call wrote a self-contained HTML poster." },
27
+ sections: { type: "array", required: true, items: { type: "string" } },
28
+ theme: { type: "string", required: true },
29
+ themeKey: { type: "string", required: true, enum: ["frontier-signal", "blue-big-fish"] },
30
+ breakpoints: { type: "array", required: true, items: { type: "string" } },
31
+ stages: { type: "array", required: true, items: { type: "string" } },
32
+ freshnessWarnings: { type: "array", required: true, items: { type: "string" } },
33
+ testCount: { type: "integer", required: true },
34
+ redactionCount: { type: "integer", required: true },
35
+ },
36
+ };
37
+
38
+ export function apply(ctx, config = {}) {
39
+ const entry = Config(config);
40
+ let settings;
41
+ let settingsRegistrationError;
42
+ try {
43
+ settings = ctx.settings.register(SETTINGS_NAMESPACE, Config, { base: entry });
44
+ } catch (error) {
45
+ // A malformed value left by an older plugin version should not prevent the
46
+ // tool itself from registering. Keep defaults readable and make writes
47
+ // fail explicitly until the host-side setting is repaired or cleared.
48
+ settingsRegistrationError = error instanceof Error ? error.message : String(error);
49
+ settings = {
50
+ get: () => entry,
51
+ async update() {
52
+ throw new Error(`Stored plugin settings could not be loaded: ${settingsRegistrationError}`);
53
+ },
54
+ };
55
+ }
56
+ const readSettings = () => {
57
+ const value = settings.get?.() ?? entry;
58
+ return {
59
+ theme: value.theme === "blue-big-fish" || value.theme === "frontier-signal" ? value.theme : entry.theme,
60
+ generatePoster: typeof value.generatePoster === "boolean" ? value.generatePoster : entry.generatePoster,
61
+ };
62
+ };
63
+ ctx.connection.rpc.handle(SETTINGS_RPC_CHANNEL, async (endpoint, payload) => {
64
+ if (endpoint === "get") {
65
+ return { ok: true, value: readSettings() };
66
+ }
67
+ if (endpoint !== "set" || typeof payload !== "object" || payload === null || Array.isArray(payload)) {
68
+ return { ok: false, error: { code: "bad-request", message: "Expected get or set with theme or generatePoster.", details: { issues: [] } } };
69
+ }
70
+ const unknownKeys = Object.keys(payload).filter((key) => key !== "theme" && key !== "generatePoster");
71
+ if (unknownKeys.length) {
72
+ return { ok: false, error: { code: "bad-request", message: `Unsupported settings fields: ${unknownKeys.join(", ")}.`, details: { issues: [] } } };
73
+ }
74
+ const patch = {};
75
+ if ("theme" in payload) {
76
+ const theme = payload.theme;
77
+ if (theme !== "frontier-signal" && theme !== "blue-big-fish") {
78
+ return { ok: false, error: { code: "bad-request", message: "Unsupported showcase theme.", details: { issues: [] } } };
79
+ }
80
+ patch.theme = theme;
81
+ }
82
+ if ("generatePoster" in payload) {
83
+ if (typeof payload.generatePoster !== "boolean") {
84
+ return { ok: false, error: { code: "bad-request", message: "generatePoster must be a boolean.", details: { issues: [] } } };
85
+ }
86
+ patch.generatePoster = payload.generatePoster;
87
+ }
88
+ if (!Object.keys(patch).length) {
89
+ return { ok: false, error: { code: "bad-request", message: "Provide theme or generatePoster to update.", details: { issues: [] } } };
90
+ }
91
+ try {
92
+ await settings.update(patch);
93
+ } catch (error) {
94
+ return { ok: false, error: { code: "settings-rejected", message: error instanceof Error ? error.message : String(error), details: { ns: SETTINGS_NAMESPACE } } };
95
+ }
96
+ return { ok: true, value: readSettings() };
97
+ }, { authority: "loopback" });
98
+ ctx.tools.register(defineTool({
99
+ name: "showcase_layout_summary",
100
+ description: "Generate a local Markdown layout summary from the current .showcase/report.json checkpoint. For visual UI projects, call it once after responsive captures are ready; for non-visual projects, call it after the report and tests are ready. Partial or failed reports are valid review checkpoints, but do not call after every code edit, status check, or intermediate tweak. Markdown is the lightweight default; generate an HTML poster only when the plugin setting allows it or the user explicitly requests a poster for this call. Reads only project files, writes only inside the project, and never uploads content.",
101
+ parameters: {
102
+ projectPath: { type: "string", description: "Absolute or relative path to the project root.", required: true },
103
+ reportPath: { type: "string", description: "Optional project-relative report path. Defaults to .showcase/report.json." },
104
+ outputPath: { type: "string", description: "Optional Markdown path under .showcase. Must end in .md or .markdown. Defaults to .showcase/layout-summary.md." },
105
+ locale: { type: "string", description: "Output language: zh-CN (default) or en.", enum: ["zh-CN", "en"], default: "zh-CN" },
106
+ posterPath: { type: "string", description: "Optional HTML poster path under .showcase. Must end in .html or .htm. Defaults to .showcase/layout-poster.html." },
107
+ generatePoster: { type: "boolean", description: "Optional one-call override. Pass true only when the user explicitly wants the HTML poster; pass false to keep this call Markdown-only. Omit to use the persistent plugin setting. This does not change the setting." },
108
+ appPath: { type: "string", description: "Optional project-relative layout source override. If omitted, the plugin discovers a suitable source file." },
109
+ cssPath: { type: "string", description: "Optional project-relative stylesheet override. If omitted, the plugin discovers a suitable CSS file." },
110
+ },
111
+ output: {
112
+ schema: outputSchema,
113
+ render(_args, value) {
114
+ const english = value.locale === "en";
115
+ const labels = english ? { heading: "Local layout artifacts generated", markdown: "Markdown", poster: "Poster", posterDisabled: "Not generated for this call", freshness: "Freshness warnings", sections: "Sections", theme: "Theme", stages: "Stages", breakpoints: "Breakpoints", tests: "Tests", redactions: "redactions" } : { heading: "本地布局产物已生成", markdown: "Markdown", poster: "海报", posterDisabled: "本次未生成", freshness: "时效警告", sections: "区段", theme: "风格", stages: "阶段", breakpoints: "断点", tests: "测试", redactions: "已脱敏" };
116
+ const punctuation = english ? "." : "。";
117
+ const poster = value.posterGenerated ? value.posterPath : labels.posterDisabled;
118
+ return [{ type: "text", text: `${labels.heading}${punctuation}\n${labels.markdown}: ${value.outputPath}\n${labels.poster}: ${poster}\n${labels.freshness}: ${value.freshnessWarnings.length}${punctuation}\n${labels.sections}: ${value.sections.join(", ")}\n${labels.theme}: ${value.theme}\n${labels.stages}: ${value.stages.join(", ")}\n${labels.breakpoints}: ${value.breakpoints.join(", ")}\n${labels.tests}: ${value.testCount}; ${labels.redactions}: ${value.redactionCount}${punctuation}` }];
119
+ },
120
+ },
121
+ async execute({ projectPath, reportPath, outputPath, posterPath, appPath, cssPath, locale, generatePoster: requestedGeneratePoster }) {
122
+ if (requestedGeneratePoster !== undefined && typeof requestedGeneratePoster !== "boolean") {
123
+ throw new Error("generatePoster must be a boolean.");
124
+ }
125
+ const current = readSettings();
126
+ const shouldGeneratePoster = typeof requestedGeneratePoster === "boolean" ? requestedGeneratePoster : current.generatePoster;
127
+ return generateLayoutSummary({ projectPath, reportPath, outputPath, posterPath, appPath, cssPath, locale, theme: current.theme, generatePoster: shouldGeneratePoster });
128
+ },
129
+ }));
130
+ }
@@ -0,0 +1,31 @@
1
+ export type LayoutSummaryResult = {
2
+ locale: "zh-CN" | "en";
3
+ outputPath: string;
4
+ posterPath?: string;
5
+ posterGenerated: boolean;
6
+ sections: string[];
7
+ theme: string;
8
+ themeKey: "frontier-signal" | "blue-big-fish";
9
+ freshnessWarnings: string[];
10
+ breakpoints: string[];
11
+ stages: string[];
12
+ testCount: number;
13
+ redactionCount: number;
14
+ };
15
+
16
+ export function generateLayoutSummary(options: {
17
+ projectPath: string;
18
+ reportPath?: string;
19
+ /** Optional Markdown output under .showcase; must end in .md or .markdown. */
20
+ outputPath?: string;
21
+ /** Optional HTML poster output under .showcase; must end in .html or .htm. */
22
+ posterPath?: string;
23
+ /** Optional project-relative layout source override. Auto-discovered when omitted. */
24
+ appPath?: string;
25
+ /** Optional project-relative stylesheet override. Auto-discovered when omitted. */
26
+ cssPath?: string;
27
+ locale?: "zh-CN" | "en";
28
+ theme?: "frontier-signal" | "blue-big-fish";
29
+ /** Generate the self-contained HTML poster. Defaults to true for direct API compatibility. */
30
+ generatePoster?: boolean;
31
+ }): Promise<LayoutSummaryResult>;