@clipbus/plugin-sdk 0.8.3 → 0.8.5

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,19 @@
1
+ /**
2
+ * CSS for the preview workbench chrome.
3
+ * Injected as a <style> tag at runtime — no framework required.
4
+ *
5
+ * All colours are driven by CSS custom properties set by applyTheme():
6
+ * --cbp-wb-backdrop : chrome background base colour
7
+ * --cbp-wb-accent : accent colour (tabs, card border)
8
+ * --cbp-wb-surface-elevated: card shell background
9
+ * --cbp-wb-border : subtle border colour
10
+ * --cbp-wb-text : primary text
11
+ * --cbp-wb-text-secondary : secondary / label text
12
+ *
13
+ * Preset defaults (graphite) are set as initial values so there is no flash of
14
+ * unstyled content before applyTheme() is called in start().
15
+ *
16
+ * Geometry values (padding, radius, border-width) are hardcoded because they
17
+ * come from native measurements, not from theme tokens.
18
+ */
19
+ export declare const previewStyles = "\n/* \u2500\u2500 CSS variable defaults (graphite dark) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb {\n --cbp-wb-backdrop: #16181C;\n --cbp-wb-accent: #43C6AC;\n --cbp-wb-surface-elevated: rgba(35,37,42,.78);\n --cbp-wb-border: rgba(255,255,255,.10);\n --cbp-wb-text: #F3F4F6;\n --cbp-wb-text-secondary: #C2C6CF;\n}\n\n/* \u2500\u2500 Root chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb {\n min-height: 100%;\n padding: 24px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n font-size: 13px;\n box-sizing: border-box;\n color: var(--cbp-wb-text);\n background: var(--cbp-wb-backdrop);\n}\n\n*, .cbp-wb * {\n box-sizing: border-box;\n}\n\n/* \u2500\u2500 Theme-aware scrollbars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* The default OS / WebKit scrollbar is an opaque bar (white on dark themes)\n that reads as jarring. Restyle every scroll area under the workbench \u2014 chrome\n (log panel) AND plugin content inside the slot \u2014 into a thin, translucent,\n theme-synced overlay that mirrors the native host's subtle scrollbars. The\n --cbp-wb-* vars cascade from the workbench root into the plugin slot, so one\n rule set covers both surfaces and recolours automatically on theme switch. */\n.cbp-wb,\n.cbp-wb * {\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--cbp-wb-text-secondary) 30%, transparent) transparent;\n}\n\n.cbp-wb ::-webkit-scrollbar {\n width: 9px;\n height: 9px;\n}\n\n.cbp-wb ::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.cbp-wb ::-webkit-scrollbar-thumb {\n background-color: color-mix(in srgb, var(--cbp-wb-text-secondary) 30%, transparent);\n border-radius: 999px;\n border: 2px solid transparent;\n background-clip: padding-box;\n}\n\n.cbp-wb ::-webkit-scrollbar-thumb:hover {\n background-color: color-mix(in srgb, var(--cbp-wb-text-secondary) 50%, transparent);\n background-clip: padding-box;\n}\n\n.cbp-wb ::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* \u2500\u2500 Controls bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb__controls {\n display: flex;\n gap: 12px;\n flex-wrap: wrap;\n align-items: flex-end;\n margin-bottom: 20px;\n}\n\n.cbp-wb__control {\n display: grid;\n gap: 6px;\n}\n\n.cbp-wb__control-label {\n font-size: 11px;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--cbp-wb-text-secondary);\n}\n\n.cbp-wb__control select,\n.cbp-wb__scenario-select {\n min-width: 160px;\n padding: 10px 12px;\n border-radius: 12px;\n border: 1px solid var(--cbp-wb-border);\n background: color-mix(in srgb, var(--cbp-wb-backdrop) 70%, transparent);\n color: var(--cbp-wb-text);\n font-size: 13px;\n font-family: inherit;\n}\n\n/* \u2500\u2500 Width slider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb__width-slider {\n width: 140px;\n accent-color: var(--cbp-wb-accent);\n cursor: pointer;\n}\n\n/* \u2500\u2500 Canvas layout (host frame on top, native-call log below) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb__canvas {\n display: flex;\n flex-direction: column;\n gap: 20px;\n align-items: center;\n}\n\n/* Host frame hugs the fixed-width viewport (no flex:1 stretch) so a wide\n browser window leaves neutral backdrop on the sides instead of a large\n empty framed area \u2014 the centered, native-like card the viewport expects. */\n.cbp-wb__host-frame {\n min-width: 0;\n padding: 16px;\n border-radius: 16px;\n background: color-mix(in srgb, var(--cbp-wb-surface-elevated) 40%, transparent);\n border: 1px solid var(--cbp-wb-border);\n display: flex;\n flex-direction: column;\n align-items: center;\n}\n\n/* \u2500\u2500 Frame title \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb__frame-title {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n margin-bottom: 12px;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n color: var(--cbp-wb-text-secondary);\n width: 100%;\n}\n\n/* \u2500\u2500 Viewport shell: centers the viewport horizontally \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb__viewport-shell {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n width: 100%;\n}\n\n/* Body clip box. NO border-radius of its own \u2014 the card shell (its parent)\n owns the rounding. A radius here would clip the card's own corners/border. */\n.cbp-wb__viewport {\n width: 100%;\n /* Native hard ceiling (AttachmentRenderHeightConstants.ceiling = 800): the\n body never grows past this even under the 'auto' content-driven policy. */\n max-height: 800px;\n /* At the ceiling, content SCROLLS inside the (fixed) card chrome \u2014 it does\n not clip away or push the card taller. */\n overflow-y: auto;\n overflow-x: hidden;\n transition: height 0.1s ease;\n}\n\n/* \u2500\u2500 Native card shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* Geometry source: AttachmentRenderCardShell.swift:24-59 */\n/* 10 pt padding / 7 pt continuous radius / 1 pt accent border / elevated bg */\n/* Outer card: stacks the body (viewport) above the host action strip, like */\n/* native VStack { body; actions }. Width is set inline; height auto-wraps. */\n.cbp-wb__card-shell {\n display: flex;\n flex-direction: column;\n gap: 8px; /* native cardSpacing: body \u2194 action strip */\n padding: 10px; /* native cardPadding */\n border-radius: 7px; /* native radius.panelInner (replaces the 20px viewport clip) */\n /* Border tint = the attachment's tintColor (--cbp-wb-card-tint, set per\n scenario from scenario.accentHex), falling back to the theme accent \u2014\n mirrors native, where the card border uses record.tintColor. */\n border: 1px solid color-mix(in srgb, var(--cbp-wb-card-tint, var(--cbp-wb-accent)) 26%, transparent);\n background: var(--cbp-wb-surface-elevated);\n /* No box-shadow per native spec */\n}\n\n.cbp-wb__webview {\n width: 100%;\n height: 100%;\n /* Fixed/bounded body shorter than its content \u2192 scroll inside. (Only the one\n element whose content actually exceeds its box scrolls, so there is never a\n double scrollbar: in 'auto' the viewport scrolls; in fixed/bounded the\n webview does.) */\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n/* \u2500\u2500 Host button strip (inside the card, below the body) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* Native action row: HStack { buttons; Spacer } \u2192 left-aligned, compact. */\n.cbp-wb__strip {\n display: flex;\n flex-wrap: wrap;\n gap: 10px; /* native action HStack spacing */\n justify-content: flex-start; /* left-aligned, not centered */\n}\n\n/* No buttons \u2192 no strip box, and the card's flex gap reserves nothing. */\n.cbp-wb__strip:empty {\n display: none;\n}\n\n.cbp-wb__button {\n appearance: none;\n border: 1px solid var(--cbp-wb-border);\n border-radius: 999px;\n padding: 4px 10px; /* native actionPadding \u2248 3 v / 8 h \u2014 was 10/16 (too large) */\n background: color-mix(in srgb, var(--cbp-wb-surface-elevated) 60%, transparent);\n color: var(--cbp-wb-text-secondary);\n font-size: 12px;\n font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n}\n\n.cbp-wb__button--primary {\n background: var(--cbp-wb-accent);\n color: var(--cbp-wb-backdrop);\n border-color: var(--cbp-wb-accent);\n}\n\n/* \u2500\u2500 Log panel (fixed-width right column) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n.cbp-wb__log {\n width: 100%;\n max-width: 720px;\n padding: 14px 16px;\n border-radius: 16px;\n background: color-mix(in srgb, var(--cbp-wb-surface-elevated) 50%, transparent);\n border: 1px solid var(--cbp-wb-border);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n gap: 0;\n}\n\n.cbp-wb__log-title {\n margin: 0 0 10px;\n font-size: 12px;\n font-weight: 800;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--cbp-wb-text);\n}\n\n.cbp-wb__log-empty {\n font-size: 12px;\n color: color-mix(in srgb, var(--cbp-wb-text-secondary) 60%, transparent);\n font-style: italic;\n}\n\n.cbp-wb__log-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-height: 220px;\n overflow-y: auto;\n}\n\n.cbp-wb__log-entry {\n border-radius: 8px;\n padding: 8px 10px;\n background: color-mix(in srgb, var(--cbp-wb-surface-elevated) 60%, transparent);\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n font-size: 11px;\n line-height: 1.5;\n word-break: break-all;\n}\n\n.cbp-wb__log-entry-method {\n font-weight: 700;\n color: var(--cbp-wb-accent);\n}\n\n.cbp-wb__log-entry-seq {\n font-weight: 400;\n color: var(--cbp-wb-text-secondary);\n margin-left: 6px;\n}\n\n.cbp-wb__log-entry-payload {\n margin-top: 2px;\n color: var(--cbp-wb-text-secondary);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Native preview theme presets.
3
+ *
4
+ * 4 presets hand-ported from the native macOS AppearanceResolver:
5
+ * platform/macos/Sources/Shared/Appearance/AppearanceResolver.swift:43-553
6
+ * mapped via:
7
+ * PluginThemeTokenMapper.swift:14-28
8
+ *
9
+ * `backdrop` is not a native PluginThemeToken — it is the underlying window-
10
+ * material base color added by this preview layer so that semi-transparent
11
+ * surface tokens composite correctly in the browser (which has no vibrancy).
12
+ */
13
+ import type { PluginThemeTokens, PluginThemeTokenSnapshot } from '../generated/data.generated.js';
14
+ export interface PreviewThemePreset {
15
+ key: 'graphite' | 'ember' | 'porcelain' | 'sand';
16
+ /** Display name shown in the theme selector. */
17
+ label: string;
18
+ scheme: 'dark' | 'light';
19
+ /** Chrome backdrop base color (approximates native window material). */
20
+ backdrop: string;
21
+ /** The 12 plugin theme tokens, verbatim from AppearanceResolver. */
22
+ tokens: PluginThemeTokens;
23
+ }
24
+ /**
25
+ * The four built-in preview theme presets in display order.
26
+ * Values are verbatim from AppearanceResolver.swift.
27
+ */
28
+ export declare const previewThemePresets: readonly PreviewThemePreset[];
29
+ export declare const DEFAULT_THEME_KEY = "graphite";
30
+ /**
31
+ * Resolve a preset by key, with graceful fallback.
32
+ *
33
+ * Backward-compat mappings:
34
+ * 'dark' → graphite (was the only dark theme before presets)
35
+ * 'light' → porcelain (was the only light theme before presets)
36
+ * undefined → graphite
37
+ * unknown key → graphite
38
+ */
39
+ export declare function getThemePreset(key?: string): PreviewThemePreset;
40
+ export declare const defaultDarkTheme: PluginThemeTokenSnapshot;
41
+ export declare const defaultLightTheme: PluginThemeTokenSnapshot;
@@ -0,0 +1,50 @@
1
+ import type { PluginClipboardItem, PluginAttachmentPayload } from '../generated/data.generated.js';
2
+ export interface PreviewViewport {
3
+ /** Initial viewport width (px). Defaults to the per-mode width when omitted. */
4
+ width?: number;
5
+ heightPolicy: 'fixed' | 'auto' | 'bounded';
6
+ height?: number;
7
+ min?: number;
8
+ max?: number;
9
+ /** Override the mode-default minimum viewport width (px). */
10
+ widthMin?: number;
11
+ /** Override the mode-default maximum viewport width (px). */
12
+ widthMax?: number;
13
+ }
14
+ export interface PreviewScenario {
15
+ id: string;
16
+ label: string;
17
+ mode: 'attachmentRenderer' | 'action';
18
+ /**
19
+ * Plugin identifier surfaced to the plugin via context.pluginID.
20
+ * Some renderers validate attachment.owner against it, so scenarios that
21
+ * exercise that path should set it. Defaults to a harness placeholder.
22
+ */
23
+ pluginID?: string;
24
+ accentHex?: string;
25
+ item: PluginClipboardItem;
26
+ /** Attachment renderer mode payload. */
27
+ attachment?: PluginAttachmentPayload;
28
+ /** Action mode draft payload. */
29
+ draft?: Record<string, unknown>;
30
+ buttons?: {
31
+ id: string;
32
+ title: string;
33
+ isEnabled: boolean;
34
+ }[];
35
+ defaultButtonID?: string;
36
+ viewport?: PreviewViewport;
37
+ /** Opaque hint forwarded to the mount adapter. */
38
+ view?: string;
39
+ }
40
+ export interface PreviewWorkbenchOptions {
41
+ scenarios: PreviewScenario[];
42
+ /**
43
+ * Called each time a scenario becomes active.
44
+ * Mount the plugin UI into `slotEl` and return a cleanup function.
45
+ */
46
+ mount(slotEl: HTMLElement, ctx: {
47
+ scenario: PreviewScenario;
48
+ }): () => void;
49
+ defaultViewport?: Partial<PreviewViewport>;
50
+ }
@@ -0,0 +1,67 @@
1
+ import type { PluginThemeTokenSnapshot } from '../generated/data.generated.js';
2
+ import type { PluginContextPayload, PluginLocalePayload } from '../generated/hostBootstrap.generated.js';
3
+ import type { PluginClipboardItem, PluginAttachmentPayload } from '../generated/data.generated.js';
4
+ import type { PreviewScenario } from './types.js';
5
+ /**
6
+ * Backward-compat alias. Now accepts any preset key string ('graphite', 'ember',
7
+ * 'porcelain', 'sand') as well as the legacy 'dark' / 'light' shorthands.
8
+ */
9
+ export type PreviewThemeKey = string;
10
+ export interface WirePayloads {
11
+ mode: PreviewScenario['mode'];
12
+ context: PluginContextPayload;
13
+ item: PluginClipboardItem;
14
+ attachment?: PluginAttachmentPayload;
15
+ draft?: Record<string, unknown>;
16
+ theme: PluginThemeTokenSnapshot;
17
+ locale: PluginLocalePayload;
18
+ }
19
+ /**
20
+ * Pure function: derive wire payloads from a scenario.
21
+ * No side-effects; safe to call multiple times.
22
+ *
23
+ * @param themeKey - Preset key ('graphite' | 'ember' | 'porcelain' | 'sand')
24
+ * or legacy shorthand ('dark' → graphite, 'light' → porcelain). Selects the
25
+ * base token snapshot so switching the theme picker drives the injected theme,
26
+ * mirroring how the real host pushes theme over the wire. Defaults to
27
+ * DEFAULT_THEME_KEY ('graphite').
28
+ */
29
+ export declare function computeWirePayloads(scenario: PreviewScenario, themeKey?: string): WirePayloads;
30
+ /**
31
+ * Inject wire payloads into the target window using the generated emitters.
32
+ * In v1 (same-document), `target` must be the current window.
33
+ * The `target` parameter is a forward-looking seam for future iframe support.
34
+ *
35
+ * No hardcoded global names or event names — all delegation flows through the
36
+ * generated emitAttachmentBootstrap / emitActionBootstrap functions.
37
+ */
38
+ export declare function injectWire(_target: Window, payloads: WirePayloads): void;
39
+ /**
40
+ * Map of PluginThemeTokens field → CSS custom property name, mirroring the
41
+ * native host contract (PluginThemeTokenContracts.swift CodingKeys). The real
42
+ * macOS host applies the theme snapshot as these `:root` CSS variables on the
43
+ * plugin WebView; the wire theme topic alone does NOT style the DOM. The
44
+ * preview must apply them too, otherwise plugin `var(--clipbus-*)` references
45
+ * fall back to their hardcoded light defaults and become unreadable on a dark
46
+ * host card.
47
+ */
48
+ export declare const THEME_CSS_VAR_NAMES: {
49
+ readonly surface: "--clipbus-surface";
50
+ readonly surfaceElevated: "--clipbus-surface-elevated";
51
+ readonly textPrimary: "--clipbus-text-primary";
52
+ readonly textSecondary: "--clipbus-text-secondary";
53
+ readonly textTertiary: "--clipbus-text-tertiary";
54
+ readonly accent: "--clipbus-accent";
55
+ readonly accentContrast: "--clipbus-accent-contrast";
56
+ readonly border: "--clipbus-border";
57
+ readonly divider: "--clipbus-divider";
58
+ readonly success: "--clipbus-success";
59
+ readonly warning: "--clipbus-warning";
60
+ readonly danger: "--clipbus-danger";
61
+ };
62
+ /**
63
+ * Apply a theme snapshot to `target` as `--clipbus-*` CSS variables, mirroring
64
+ * the native host. Called on every scenario activation and theme change so the
65
+ * mounted plugin restyles in place (the plugin reads these via `var()`).
66
+ */
67
+ export declare function applyThemeCssVars(target: HTMLElement, snapshot: PluginThemeTokenSnapshot): void;
package/docs/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  | [permissions.md](./permissions.md) | 权限模型:manifest 声明、受门控的 verb、最小权限原则 |
20
20
  | [capability-detection.md](./capability-detection.md) | 能力检测:`has()` 门控、UI/Node 侧 catch 差异、老宿主降级 |
21
21
  | [faq.md](./faq.md) | 常见坑点 Q&A |
22
+ | [preview.md](./preview.md) | 开发期预览 harness(`@clipbus/plugin-sdk/preview`):`createPreviewWorkbench`、scenario / viewport 配置、fake host、调用日志 |
22
23
 
23
24
  ## 同包参考
24
25
 
@@ -0,0 +1,259 @@
1
+ # 开发期预览 harness
2
+
3
+ > 本文档属于 @clipbus/plugin-sdk 开发文档 · 返回[文档地图](./README.md)
4
+
5
+ `@clipbus/plugin-sdk/preview` 提供**框架中立**(纯 DOM/TypeScript,不依赖 Vue 或任何 UI 框架)的**开发期**预览 harness。无需真实 macOS 宿主进程,即可在浏览器里迭代插件 UI 的外观与行为。
6
+
7
+ - **宿主 → 插件注入**:wire payload(bootstrap、item、attachment、theme、draft……)由 harness 根据 scenario 定义按宿主协议生成,与真实宿主注入方式同源。
8
+ - **插件 → 宿主调用**:所有 `clipbus.*` native 调用被拦截并实时追加到「调用日志」面板;`clipbus.window.setHeight` 额外驱动视口高度;其他调用返回安全默认应答,不抛错。
9
+ - **视口**:宽度按模式默认(renderer 720 / action 350;renderer 取自 native 主面板全宽 workspace 区,action 为紧凑 action 卡宽),可被 `scenario.viewport.width` 覆盖,宽度滑杆按 view 切换范围(renderer [560,900]、action [300,500]);高度跟随插件真实调用的 `clipbus.window.setHeight`(`auto`/`bounded` 策略,统一封顶 native 硬上限 800),行为贴近 native。
10
+
11
+ **这是纯开发期工具**,不打包进线上 plugin 产物。
12
+
13
+ ---
14
+
15
+ ## 快速开始
16
+
17
+ 插件保留自己的 Vite + `index.html` + `npm run dev`。在 preview 入口 `main.ts` 里调用 `createPreviewWorkbench`:
18
+
19
+ ```ts
20
+ import { createPreviewWorkbench } from '@clipbus/plugin-sdk/preview';
21
+ import { createApp } from 'vue';
22
+ import MyApp from '../features/my-feature/app.vue';
23
+ import { myScenarios } from './scenarios';
24
+
25
+ createPreviewWorkbench(document.getElementById('app')!, {
26
+ scenarios: myScenarios,
27
+ mount(slotEl, { scenario }) {
28
+ // 在 slotEl 里挂载插件 UI,返回 cleanup
29
+ const app = createApp(MyApp);
30
+ app.mount(slotEl);
31
+ return () => app.unmount();
32
+ },
33
+ });
34
+ ```
35
+
36
+ `mount` 在每次 scenario 激活时由 harness 调用,负责在 `slotEl` 里挂载插件 UI 并返回 cleanup 函数。harness 自动处理:工作台 chrome(两级模式/scenario 导航、4 预设主题切换器、native 卡片壳、宽度滑杆、按钮条)、fake host 安装、wire 注入与 `--clipbus-*` CSS 变量镜像、scenario 切换时的 cleanup 与重挂。
37
+
38
+ ---
39
+
40
+ ## `createPreviewWorkbench(root, opts)`
41
+
42
+ ```ts
43
+ function createPreviewWorkbench(root: HTMLElement, opts: PreviewWorkbenchOptions): void
44
+ ```
45
+
46
+ | 参数 | 类型 | 说明 |
47
+ |---|---|---|
48
+ | `root` | `HTMLElement` | 工作台挂载的容器(通常是 `document.getElementById('app')`) |
49
+ | `opts.scenarios` | `PreviewScenario[]` | scenario 列表;至少一个 |
50
+ | `opts.mount` | `(slotEl, { scenario }) => () => void` | 每次 scenario 激活时调用;在 `slotEl` 里挂载插件 UI,返回 cleanup |
51
+ | `opts.defaultViewport` | `Partial<PreviewViewport>?` | 所有 scenario 的视口默认值;可被 scenario 自身的 `viewport` 字段覆盖 |
52
+
53
+ ---
54
+
55
+ ## `PreviewScenario` 字段参考
56
+
57
+ ```ts
58
+ interface PreviewScenario {
59
+ id: string;
60
+ label: string;
61
+ mode: 'attachmentRenderer' | 'action';
62
+ pluginID?: string;
63
+ accentHex?: string;
64
+ item: PluginClipboardItem;
65
+ attachment?: PluginAttachmentPayload; // attachmentRenderer 模式
66
+ draft?: Record<string, unknown>; // action 模式
67
+ buttons?: { id: string; title: string; isEnabled: boolean }[];
68
+ defaultButtonID?: string;
69
+ viewport?: PreviewViewport;
70
+ view?: string;
71
+ }
72
+ ```
73
+
74
+ | 字段 | 说明 |
75
+ |---|---|
76
+ | `id` | scenario 唯一标识符 |
77
+ | `label` | 选择器里显示的名称 |
78
+ | `mode` | `'attachmentRenderer'`(渲染器 WebView)或 `'action'`(draft action WebView) |
79
+ | `pluginID` | 注入给插件 `context.pluginID` 的插件 ID;若 attachment.owner 校验依赖此字段,应显式设置;未设置时 harness 使用占位符 |
80
+ | `accentHex` | 强调色十六进制字符串(e.g. `'#0f766e'`) |
81
+ | `item` | 当前 clipboard item(`PluginClipboardItem`) |
82
+ | `attachment` | attachmentRenderer 模式的 attachment payload(`PluginAttachmentPayload`) |
83
+ | `draft` | action 模式的 initialDraft(`Record<string, unknown>`) |
84
+ | `buttons` | 按钮条种子:id / title / isEnabled |
85
+ | `defaultButtonID` | 默认高亮按钮的 id |
86
+ | `viewport` | 该 scenario 的视口配置;未设置时使用 `opts.defaultViewport` |
87
+ | `view` | 透传给 `mount` adapter 的不透明 hint(e.g. `'compact'`/`'expanded'`) |
88
+
89
+ ---
90
+
91
+ ## `PreviewViewport` 字段参考
92
+
93
+ ```ts
94
+ interface PreviewViewport {
95
+ width?: number;
96
+ heightPolicy: 'fixed' | 'auto' | 'bounded';
97
+ height?: number;
98
+ min?: number;
99
+ max?: number;
100
+ widthMin?: number;
101
+ widthMax?: number;
102
+ }
103
+ ```
104
+
105
+ | 字段 | 说明 |
106
+ |---|---|
107
+ | `width` | 初始视口宽度(px);未设置时取模式默认(renderer 720 / action 350) |
108
+ | `heightPolicy` | `'fixed'`:高度锁定为 `height`;`'auto'`:完全跟随 `clipbus.window.setHeight` 调用;`'bounded'`:跟随 setHeight,限在 `[min, max]` 范围内 |
109
+ | `height` | `'fixed'` policy 下的高度(px) |
110
+ | `min` | `'bounded'` policy 下的最小高度(px) |
111
+ | `max` | `'bounded'` policy 下的最大高度(px) |
112
+ | `widthMin` | 覆盖该 scenario 的宽度滑杆最小值(px);未设置时取模式默认(renderer 560 / action 300) |
113
+ | `widthMax` | 覆盖该 scenario 的宽度滑杆最大值(px);未设置时取模式默认(renderer 900 / action 500) |
114
+
115
+ **宽度**:由工作台宽度滑杆控制,以 `scenario.viewport.width`(或模式默认)为初始值,在 `[widthMin, widthMax]` 范围内可拖动。**高度**行为贴近 native:插件调用 `clipbus.window.setHeight({height})` 时 harness 立即同步视口高度(`auto`/`bounded` 策略),与 native 行为一致。
116
+
117
+ ---
118
+
119
+ ## 调用日志与 fake host
120
+
121
+ harness 安装一个最小双向 fake host:
122
+
123
+ - **host → plugin**:按 scenario 字段生成 wire payload 并注入;bootstrap、item、attachment、theme 等不需要真实宿主
124
+ - **plugin → host**:所有 `clipbus.*` native 调用被拦截,实时追加到「调用日志」面板,格式为 `[method] JSON payload`;`clipbus.window.setHeight` 额外驱动视口高度;其他调用返回安全默认应答
125
+
126
+ 调用日志让你无需宿主即可验证 `setHeight` / `action.complete` / `clipboard.copyText` 等调用是否在正确时机触发,以及携带正确 payload。
127
+
128
+ ---
129
+
130
+ ## 主题预设
131
+
132
+ harness 内置 4 套 native 主题预设,覆盖深色/浅色两种配色方案:
133
+
134
+ | 预设键 | 显示名称 | 配色方案 | accent |
135
+ |---|---|---|---|
136
+ | `graphite`(默认) | Graphite (Dark) | dark | 青绿 `#43C6AC` |
137
+ | `ember` | Ember (Dark) | dark | 暖橙 `#F97316` |
138
+ | `porcelain` | Porcelain (Light) | light | 蓝 `#2563EB` |
139
+ | `sand` | Sand (Light) | light | 暖琥珀 `#D97706` |
140
+
141
+ 每套预设包含 12 个 `PluginThemeTokens`(surface、surfaceElevated、textPrimary/Secondary/Tertiary、accent、accentContrast、border、divider、success、warning、danger),数值直接来自 native `AppearanceResolver.swift`,确保预览与真实 native 渲染一致。
142
+
143
+ `scenario.accentHex` 可在任意预设基础上覆盖 accent 颜色(如品牌色匹配),其他 token 保持预设值不变。
144
+
145
+ `defaultDarkTheme` / `defaultLightTheme` 是向后兼容快照,分别由 graphite / porcelain 预设派生。
146
+
147
+ ---
148
+
149
+ ## 工作台 UI
150
+
151
+ ```sh
152
+ npm run dev
153
+ ```
154
+
155
+ Vite 启动后,工作台提供:
156
+
157
+ - **两级模式导航**:顶层选择器在 **Renderer**(attachmentRenderer)和 **Action** 两种模式间切换;两种模式的 scenario 均用 `<select>` 下拉选择(文案取自 `scenario.label`)。任何时刻只渲染当前选中 scenario。URL query `?view=&scenario=&theme=` 同步,刷新不丢。
158
+ - **主题切换**(4 预设:Graphite · Ember · Porcelain · Sand):切换同时三重作用:① 重注入 wire 主题 topic,插件已注册的 `clipbus.theme.on()` 回调立即触发;② 重着色工作台背板/卡片/控件;③ 在插件挂载槽上更新 `--clipbus-*` CSS 变量(镜像 native host 在 `:root` 的变量注入)——不触发 remount,与 native `theme.onChange` 行为一致。注意:插件的 `--clipbus-accent` 是**全局主题 accent**;`scenario.accentHex` 是该附件的 **tintColor**,只染**卡片边框**(对应 native `record.tintColor`),不进插件配色。
159
+ - **Native 卡片壳**:卡片框(10 px padding / 7 px 圆角(native `radius.panelInner`)/ 1 px 描边(色取 `scenario.accentHex` 附件 tint,回退主题 accent)/ surfaceElevated 背景)是**外层卡片**,纵向堆叠 **body(插件挂载槽 `slotEl`)+ 按钮条**,对应 native `AttachmentRenderCardShell` 的 `VStack { body; actions }`。卡片宽度由滑杆控制并居中,webview 按卡片 chrome(每边 11 px = 10 padding + 1 border)内嵌;卡片高度自动包裹 body + 按钮条,宽浏览器下两侧只剩中性背板色。
160
+ - **宽度滑杆**:拖动**卡片外层宽度**(webview 随之按 chrome 内嵌),范围随 view 切换。模式默认(取自 native):renderer 初始 720 px、范围 [560,900](主面板全宽 workspace 区);action 初始 350 px、范围 [300,500](紧凑 action 卡)。`scenario.viewport.width` 可覆盖初始值;`viewport.widthMin`/`widthMax` 可覆盖滑杆范围。
161
+ - **body 高度**:高度**由插件驱动**。native **没有**宿主侧"量内容定高":`auto` 在 native 解析为 `bounded(80, 800)`,且只认 `clipbus.window.setHeight`——插件**必须**用 SDK `autoFit`(或手动 `setHeight`)自撑,否则卡在 policy 最小高度被裁(gallery-auto 在 native 的坑正源于此)。harness 在插件首个 `setHeight` 前以内容高度做种子(宽松的预览便利,**非** native 行为:native 以 policy 最小高度做种子并裁切,故漏调 `autoFit` 在 preview 看着正常却会在 native 崩)。插件调 `clipbus.window.setHeight`(`auto`/`bounded`)时同步 body 高度,`bounded` 触顶后由插件自身内部滚动。所有策略统一封顶 native 硬上限 **800**(`AttachmentRenderHeightConstants.ceiling`)。
162
+ - **按钮条**:源自 `scenario.buttons`(及插件 `setButtons`),位于**卡片内部、body 下方、左对齐**(native action row 布局);无按钮时不占位。
163
+ - **调用日志**:实时记录所有 native 调用与 payload,面板置于**预览卡片下方**(全宽)
164
+ - **主题感知滚动条**:工作台内所有滚动区域(chrome 日志面板 + 插件内容及其嵌套滚动器)统一为细、半透明、随主题变色的滚动条,替换突兀的默认(深色主题下发白)OS 滚动条,镜像 native 宿主的低调滚动条
165
+
166
+ ---
167
+
168
+ ## Native 卡片壳与组件去框约定
169
+
170
+ harness 在 `slotEl` 外提供 native 风格卡片框,模拟真实宿主为插件提供的统一外壳。因此:
171
+
172
+ > **约定:插件组件不应自绘最外层 `background` / `border` / `border-radius` / `box-shadow` / 外层 `padding`。**
173
+ > 宿主(和 harness)统一提供卡片框;插件在框内布局内容即可。
174
+
175
+ 违反此约定的后果:预览与 native 外观一致,但若宿主后续更换卡片样式,插件自绘的外框会与宿主框冲突(双重描边、背景叠加)。`template-plugin` 的全部 renderer 组件(`preview-renderer`、`expanded-renderer`、3 个 `capability-gallery` renderer)均已遵循此约定。
176
+
177
+ ---
178
+
179
+ ## 超限内滚约定
180
+
181
+ native 宿主在 `clipbus.window.setHeight` 到上限(800 pt)时**直接裁剪**,不为插件提供外部滚动容器。因此,若插件内容可能超出最大高度,插件需在自身内部处理滚动:
182
+
183
+ ```css
184
+ /* 示例:expanded-renderer 方案 */
185
+ .outer-shell {
186
+ overflow-y: auto; /* 超出时内部滚动 */
187
+ max-height: 100%; /* 跟随视口高度 */
188
+ }
189
+ ```
190
+
191
+ `template-plugin` 的 `expanded-renderer` 是此约定的参考实现(外层 shell `overflow-y: auto` + 内容区自适应,兼容 `autoFit` 自动高度策略)。
192
+
193
+ ---
194
+
195
+ ## 参考实现
196
+
197
+ `plugins/template-plugin/src/preview/preview-host/main.ts` 是规范的薄胶水范例——按 `scenario.mode` 和 `scenario.view` 路由到不同 Vue 组件:
198
+
199
+ ```ts
200
+ import { createPreviewWorkbench } from '@clipbus/plugin-sdk/preview';
201
+ import { createApp } from 'vue';
202
+ import { attachmentScenarios } from '../scenarios/attachmentScenarios';
203
+ import { actionScenarios } from '../scenarios/actionScenarios';
204
+ import AttachmentApp from '../../features/preview-renderer/app.vue';
205
+ import ExpandedApp from '../../features/expanded-renderer/app.vue';
206
+ import DraftActionApp from '../../features/capability-gallery/draft-action-ui/app.vue';
207
+ import RendererFixedApp from '../../features/capability-gallery/renderer-fixed-ui/app.vue';
208
+ import RendererAutoApp from '../../features/capability-gallery/renderer-auto-ui/app.vue';
209
+ import RendererBoundedApp from '../../features/capability-gallery/renderer-bounded-ui/app.vue';
210
+
211
+ createPreviewWorkbench(document.getElementById('app')!, {
212
+ scenarios: [...attachmentScenarios, ...actionScenarios],
213
+ mount(slotEl, { scenario }) {
214
+ const Comp =
215
+ scenario.mode === 'action'
216
+ ? DraftActionApp
217
+ : scenario.view === 'expanded'
218
+ ? ExpandedApp
219
+ : scenario.view === 'gallery-fixed'
220
+ ? RendererFixedApp
221
+ : scenario.view === 'gallery-auto'
222
+ ? RendererAutoApp
223
+ : scenario.view === 'gallery-bounded'
224
+ ? RendererBoundedApp
225
+ : AttachmentApp;
226
+ const app = createApp(Comp);
227
+ app.mount(slotEl);
228
+ return () => app.unmount();
229
+ },
230
+ });
231
+ ```
232
+
233
+ `createPreviewWorkbench` 接管全部工作台基础设施,`mount` 里只有插件自己的框架调用——这就是完整模式。
234
+
235
+ ---
236
+
237
+ ## 高级导出
238
+
239
+ ```ts
240
+ // 供测试与集成场景使用;日常插件开发只需 createPreviewWorkbench
241
+
242
+ // Wire 工具
243
+ import { computeWirePayloads, injectWire } from '@clipbus/plugin-sdk/preview';
244
+ import { applyThemeCssVars, THEME_CSS_VAR_NAMES } from '@clipbus/plugin-sdk/preview';
245
+ import { installFakeHost } from '@clipbus/plugin-sdk/preview';
246
+ import type { FakeHostHooks, WirePayloads } from '@clipbus/plugin-sdk/preview';
247
+
248
+ // 主题预设(4 套内置预设)
249
+ import { previewThemePresets, getThemePreset, DEFAULT_THEME_KEY } from '@clipbus/plugin-sdk/preview';
250
+ import type { PreviewThemePreset } from '@clipbus/plugin-sdk/preview';
251
+ // 向后兼容快照(由 graphite/porcelain 派生,保留以兼容旧测试)
252
+ import { defaultDarkTheme, defaultLightTheme } from '@clipbus/plugin-sdk/preview';
253
+
254
+ // Chrome 纯函数(测试/集成用)
255
+ import { groupScenariosByMode, resolveViewportWidth, clampWidth } from '@clipbus/plugin-sdk/preview';
256
+
257
+ // Wire 协议常量
258
+ import { PLUGIN_CALL_HANDLER_NAME, WINDOW_SET_HEIGHT_METHOD } from '@clipbus/plugin-sdk/preview';
259
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipbus/plugin-sdk",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "description": "Typed SDK for authoring Clipbus plugins — runtime (Node.js) and UI (WebView) helpers generated from the Clipbus plugin wire contract.",
6
6
  "keywords": [
@@ -58,6 +58,16 @@
58
58
  "types": "./dist/validate/index.d.cts",
59
59
  "default": "./dist/validate/index.cjs"
60
60
  }
61
+ },
62
+ "./preview": {
63
+ "import": {
64
+ "types": "./dist/preview/index.d.ts",
65
+ "default": "./dist/preview/index.js"
66
+ },
67
+ "require": {
68
+ "types": "./dist/preview/index.d.cts",
69
+ "default": "./dist/preview/index.cjs"
70
+ }
61
71
  }
62
72
  },
63
73
  "bin": {