@clipbus/plugin-sdk 0.8.4 → 0.8.6
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.
- package/README.md +21 -0
- package/dist/generated/hostBootstrap.generated.d.ts +22 -0
- package/dist/generated/wireConstants.generated.d.ts +2 -0
- package/dist/preview/chrome.d.ts +119 -0
- package/dist/preview/fakeHost.d.ts +30 -0
- package/dist/preview/index.cjs +1670 -0
- package/dist/preview/index.d.cts +51 -0
- package/dist/preview/index.d.ts +51 -0
- package/dist/preview/index.js +1647 -0
- package/dist/preview/inputPanel.d.ts +57 -0
- package/dist/preview/styles.d.ts +23 -0
- package/dist/preview/theme.d.ts +41 -0
- package/dist/preview/types.d.ts +50 -0
- package/dist/preview/wire.d.ts +67 -0
- package/docs/README.md +1 -0
- package/docs/preview.md +322 -0
- package/package.json +11 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire Input panel — collapsible sections for the full WirePayloads snapshot.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Classify wire sections into ordered content / infra groups.
|
|
6
|
+
* - Parse embedded payloadJson strings into readable nested JSON (or raw fallback).
|
|
7
|
+
* - Render the collapsible section DOM into a container element.
|
|
8
|
+
*
|
|
9
|
+
* Per-section toggle is INDEPENDENT (not single-open); single-open behaviour is
|
|
10
|
+
* the OUT accordion's contract only.
|
|
11
|
+
*
|
|
12
|
+
* Pure functions (classifyWireSections, parsePayloadJson) are exported for unit
|
|
13
|
+
* testing, following the same convention as groupScenariosByMode in chrome.ts.
|
|
14
|
+
*/
|
|
15
|
+
import type { WirePayloads } from './wire.js';
|
|
16
|
+
export interface WireSection {
|
|
17
|
+
/** The payload key name (e.g. 'context', 'item', 'attachment'). */
|
|
18
|
+
key: string;
|
|
19
|
+
/** content = item + attachment/draft (user-facing data; open by default).
|
|
20
|
+
* infra = context + theme + locale (plumbing; collapsed by default). */
|
|
21
|
+
kind: 'content' | 'infra';
|
|
22
|
+
/** Whether the section is expanded when first rendered. */
|
|
23
|
+
defaultOpen: boolean;
|
|
24
|
+
/** The raw payload value for this key. */
|
|
25
|
+
value: unknown;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Classify wire payload sections into display order with open/collapsed defaults.
|
|
29
|
+
*
|
|
30
|
+
* Fixed order: context · item · attachment(renderer)/draft(action) · theme · locale
|
|
31
|
+
* content: item + attachment/draft — defaultOpen true (user compares these with output)
|
|
32
|
+
* infra: context + theme + locale — defaultOpen false (plumbing, low signal)
|
|
33
|
+
*/
|
|
34
|
+
export declare function classifyWireSections(payloads: WirePayloads): WireSection[];
|
|
35
|
+
/**
|
|
36
|
+
* Parse a raw value as JSON; never throws.
|
|
37
|
+
*
|
|
38
|
+
* Returns `{ ok: true, value }` when `raw` is a string that JSON.parse()s
|
|
39
|
+
* successfully. Returns `{ ok: false, raw }` on any failure — the caller
|
|
40
|
+
* renders `raw` verbatim (Fail loud: the parse failure is visible, not silent).
|
|
41
|
+
*/
|
|
42
|
+
export declare function parsePayloadJson(raw: unknown): {
|
|
43
|
+
ok: true;
|
|
44
|
+
value: unknown;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
raw: unknown;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Build (or replace) the Wire Input collapsible sections inside `containerEl`.
|
|
51
|
+
*
|
|
52
|
+
* Called on each scenario activation and theme change (the theme section
|
|
53
|
+
* reflects the new preset). Re-calling clears and rebuilds all sections.
|
|
54
|
+
*
|
|
55
|
+
* Sections are independently togglable — opening one does NOT collapse others.
|
|
56
|
+
*/
|
|
57
|
+
export declare function renderWireInput(containerEl: HTMLElement, payloads: WirePayloads): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS for the Wire Bench preview workbench chrome.
|
|
3
|
+
* Injected as a <style> tag at runtime — no framework required.
|
|
4
|
+
*
|
|
5
|
+
* Design: Wire Bench (2026-06-28).
|
|
6
|
+
* - Stable chrome palette (--bench-* tokens) decoupled from native theme
|
|
7
|
+
* presets — the instrument background never changes; only the specimen
|
|
8
|
+
* card + plugin slot recolour when the theme picker switches. This
|
|
9
|
+
* intentionally reverses the prior native-fidelity D3 decision (backdrop
|
|
10
|
+
* was theme-synced). See spec §5.1 and preview.md §theme-decoupling.
|
|
11
|
+
* - Blueprint grid background, Space Grotesk + IBM Plex Mono type system.
|
|
12
|
+
* - Layout: header(controls) + horizontal rule + stage(workspace + dock).
|
|
13
|
+
*
|
|
14
|
+
* Frozen geometry (pixel-identical to HEAD — do not change):
|
|
15
|
+
* .cbp-wb__card-shell — 10px padding / 7px radius / 1px tint border / surfaceElevated bg
|
|
16
|
+
* .cbp-wb__viewport — max-height 800px / overflow-y auto
|
|
17
|
+
* .cbp-wb__webview — width/height 100%
|
|
18
|
+
* .cbp-wb__strip — flex left-aligned / gap 10px
|
|
19
|
+
* .cbp-wb__button — 4px 10px padding / 999px radius
|
|
20
|
+
* Card-side --cbp-wb-* vars are still written by applyTheme() so the card
|
|
21
|
+
* recolours with the theme. The chrome itself uses --bench-* (constant).
|
|
22
|
+
*/
|
|
23
|
+
export declare const previewStyles = "\n@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap');\n\n/* \u2500\u2500 Wire Bench chrome tokens (stable \u2014 independent of native theme preset) \u2500 */\n/* Intentional decoupling: the workbench chrome is a tool, not a specimen. */\n/* The electric violet signal colour (#7B61FF) is chosen to not clash with */\n/* any of the 4 native theme accents (teal / orange / blue / amber). */\n.cbp-wb {\n --bench: #14161D;\n --bench-2: #1A1D26;\n --bench-3: #222634;\n --bench-line: rgba(152,164,196,.12);\n --bench-line-2: rgba(152,164,196,.20);\n --bench-ink: #E8EAF2;\n --bench-ink-dim: #969EB2;\n --bench-ink-faint: #646B7E;\n --bench-signal: #7B61FF;\n --bench-signal-2: #B9A8FF;\n --bench-signal-soft: rgba(123,97,255,.16);\n}\n\n/* \u2500\u2500 Card-side theme vars (graphite defaults; applyTheme() overwrites these) \u2500 */\n/* These track the active preset so the specimen card (card-shell / buttons) */\n/* still recolours when the theme picker changes \u2014 native fidelity for the */\n/* card interior. The chrome itself never reads these for its own background. */\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\u2500 */\n/* Background uses --bench (stable), not --cbp-wb-backdrop (theme-driven). */\n/* Blueprint grid is a visual signature of the instrument chrome. */\n.cbp-wb {\n min-height: 100%;\n padding: 28px 26px 34px;\n font-family: \"Space Grotesk\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n font-size: 14px;\n box-sizing: border-box;\n color: var(--bench-ink);\n background:\n linear-gradient(var(--bench-line) 1px, transparent 1px),\n linear-gradient(90deg, var(--bench-line) 1px, transparent 1px),\n var(--bench);\n background-size: 32px 32px, 32px 32px, auto;\n background-position: -1px -1px, -1px -1px, 0 0;\n -webkit-font-smoothing: antialiased;\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\u2500 */\n/* Restyle every scroll area \u2014 chrome AND plugin content inside the slot \u2014 */\n/* into a thin, translucent bench-toned overlay. Uses bench-ink-dim (stable) */\n/* rather than --cbp-wb-text-secondary so the scrollbar doesn't recolour */\n/* when the theme switches (intentional \u2014 chrome palette is stable). */\n/* The --cbp-wb-* vars still cascade into the plugin slot for its own use. */\n.cbp-wb,\n.cbp-wb * {\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--bench-ink-dim) 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(--bench-ink-dim) 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(--bench-ink-dim) 50%, transparent);\n background-clip: padding-box;\n}\n\n.cbp-wb ::-webkit-scrollbar-corner {\n background: transparent;\n}\n\n/* \u2500\u2500 Header: brand wordmark + controls console \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__header {\n display: flex;\n justify-content: space-between;\n align-items: flex-end;\n gap: 24px;\n flex-wrap: wrap;\n margin-bottom: 16px;\n}\n\n.cbp-wb__brand {\n display: flex;\n flex-direction: column;\n gap: 5px;\n}\n\n.cbp-wb__brand-sn {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11.5px;\n letter-spacing: .16em;\n color: var(--bench-ink-faint);\n text-transform: uppercase;\n}\n\n.cbp-wb__brand-mark {\n font-size: 28px;\n font-weight: 700;\n letter-spacing: -.01em;\n line-height: 1;\n}\n\n.cbp-wb__brand-mark b { color: var(--bench-signal-2); }\n.cbp-wb__brand-mark span { color: var(--bench-ink); }\n\n/* \u2500\u2500 Controls console (Surface / Scenario / Theme / Width) \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: 11px;\n align-items: flex-end;\n flex-wrap: wrap;\n}\n\n.cbp-wb__control {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.cbp-wb__control-label {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11px;\n letter-spacing: .14em;\n text-transform: uppercase;\n color: var(--bench-ink-faint);\n}\n\n/* Scenario + Theme selects */\n.cbp-wb__control select,\n.cbp-wb__scenario-select {\n background: var(--bench-2);\n border: 1px solid var(--bench-line);\n border-radius: 11px;\n color: var(--bench-ink);\n font: inherit;\n font-size: 13px;\n padding: 8px 13px;\n min-width: 158px;\n}\n\n/* Width slider */\n.cbp-wb__width-slider {\n accent-color: var(--bench-signal);\n width: 128px;\n cursor: pointer;\n}\n\n/* Current width value shown inline with the label */\n.cbp-wb__width-val {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n color: var(--bench-ink-dim);\n}\n\n/* \u2500\u2500 Horizontal rule separating header from stage \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__rule {\n height: 1px;\n background: var(--bench-line);\n margin: 0 0 22px;\n}\n\n/* \u2500\u2500 Stage: workspace (hero) left + dock right \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__stage {\n display: flex;\n gap: 22px;\n align-items: flex-start;\n}\n\n/* \u2500\u2500 Workspace \u2014 hero, fills the left 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 */\n.cbp-wb__workspace {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n.cbp-wb__ws-head {\n display: flex;\n align-items: baseline;\n gap: 13px;\n margin-bottom: 13px;\n}\n\n.cbp-wb__ws-kicker {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: .2em;\n color: var(--bench-signal-2);\n}\n\n.cbp-wb__ws-title {\n font-size: 23px;\n font-weight: 700;\n letter-spacing: -.01em;\n}\n\n.cbp-wb__ws-sub {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n color: var(--bench-ink-faint);\n}\n\n/* Mode + scenario chip \u2014 right-aligned in the ws-head */\n.cbp-wb__ws-chip {\n margin-left: auto;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n color: var(--bench-ink-dim);\n background: var(--bench-2);\n border: 1px solid var(--bench-line);\n border-radius: 9px;\n padding: 6px 11px;\n white-space: nowrap;\n}\n\n/* \u2500\u2500 Workspace surface \u2014 glass stage that holds the specimen card \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* min-height fills the viewport column height (hero). The card is anchored */\n/* at the top; excess space becomes the \"growth floor\" labelling the 800 cap. */\n.cbp-wb__ws-surface {\n position: relative;\n border: 1px solid var(--bench-line);\n border-radius: 18px;\n background:\n radial-gradient(120% 64% at 50% 0%, var(--bench-signal-soft), transparent 56%),\n var(--bench-2);\n min-height: calc(100vh - 150px);\n padding: 34px 36px 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n overflow: auto;\n}\n\n/* Violet accent line across the top edge \u2014 signal that this is live */\n.cbp-wb__ws-surface::before {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n height: 2px;\n background: linear-gradient(90deg, transparent, var(--bench-signal-soft), transparent);\n}\n\n/* Small tape label anchored top-left inside the surface */\n.cbp-wb__ws-tape {\n position: absolute;\n top: 13px;\n left: 20px;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11px;\n letter-spacing: .14em;\n text-transform: uppercase;\n color: var(--bench-ink-faint);\n}\n\n/* Wrapper that provides a positioning context for the \"card internals\" tag */\n.cbp-wb__specimen-wrap {\n position: relative;\n margin-top: 20px;\n}\n\n/* Dimensions readout (WxH) below the specimen */\n.cbp-wb__ws-dims {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n color: var(--bench-ink-faint);\n letter-spacing: .06em;\n}\n\n/* Growth floor \u2014 pushes to the bottom of the surface, labels the 800 ceiling */\n/* Turns the blank space below the card into meaningful information: */\n/* \"this gap is the room left before the native height cap\". */\n.cbp-wb__ws-floor {\n margin-top: auto;\n width: 100%;\n padding-top: 16px;\n border-top: 1px dashed var(--bench-line-2);\n display: flex;\n justify-content: center;\n gap: 10px;\n align-items: center;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11.5px;\n letter-spacing: .07em;\n color: var(--bench-ink-faint);\n}\n\n.cbp-wb__ws-floor-pip {\n color: var(--bench-signal-2);\n font-size: 13px;\n}\n\n/* \u2500\u2500 Dock \u2014 two attachment cards, position:sticky \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/* Sticks to the top so the workspace can grow vertically without pushing */\n/* the OUT card out of view. Right column is always in frame. */\n.cbp-wb__dock {\n flex: 0 0 470px;\n position: sticky;\n top: 8px;\n align-self: flex-start;\n max-height: calc(100vh - 54px);\n overflow: auto;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n/* \u2500\u2500 Attachment card (IN and OUT share this 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 */\n.cbp-wb__att {\n position: relative;\n background: var(--bench-2);\n border: 1px solid var(--bench-line);\n border-radius: 18px;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.cbp-wb__att-head {\n display: flex;\n align-items: center;\n gap: 11px;\n padding: 14px 17px;\n border-bottom: 1px solid var(--bench-line);\n flex: none;\n}\n\n.cbp-wb__att-code {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: .16em;\n color: var(--bench-signal-2);\n}\n\n.cbp-wb__att-title {\n font-size: 15.5px;\n font-weight: 700;\n}\n\n.cbp-wb__att-dir {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11.5px;\n color: var(--bench-ink-faint);\n}\n\n.cbp-wb__att-meta {\n margin-left: auto;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n color: var(--bench-ink-dim);\n}\n\n.cbp-wb__att-scroll {\n overflow: auto;\n padding: 7px 0;\n}\n\n.cbp-wb__att--in .cbp-wb__att-scroll { max-height: 474px; }\n.cbp-wb__att--out .cbp-wb__att-scroll { max-height: 320px; }\n\n/* \u2500\u2500 Wire Input sections (IN panel \u2014 host\u2192plugin) \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/* Section dividers; first child has no top border. */\n.cbp-wb__wire-sec {\n border-top: 1px solid var(--bench-line);\n}\n\n.cbp-wb__wire-sec:first-child {\n border-top: 0;\n}\n\n/* Clickable header row: chevron + key + kind tag + collapsed summary */\n.cbp-wb__wire-sec-head {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 11px 17px;\n cursor: pointer;\n user-select: none;\n}\n\n.cbp-wb__wire-sec-chev {\n font-size: 10px;\n width: 10px;\n color: var(--bench-ink-faint);\n flex: none;\n}\n\n.cbp-wb__wire-sec--open .cbp-wb__wire-sec-chev {\n color: var(--bench-signal-2);\n}\n\n.cbp-wb__wire-sec-key {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 13px;\n font-weight: 600;\n color: var(--bench-ink);\n}\n\n.cbp-wb__wire-sec-tag {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 10.5px;\n color: var(--bench-ink-faint);\n letter-spacing: .04em;\n border: 1px solid var(--bench-line);\n border-radius: 5px;\n padding: 1px 6px;\n}\n\n/* Content sections are highlighted \u2014 these are the user-facing payloads */\n.cbp-wb__wire-sec-tag--content {\n color: var(--bench-signal-2);\n border-color: color-mix(in srgb, var(--bench-signal) 35%, transparent);\n}\n\n.cbp-wb__wire-sec-sum {\n margin-left: auto;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11.5px;\n color: var(--bench-ink-faint);\n max-width: 44%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Collapsible payload body \u2014 hidden until the section is open */\n.cbp-wb__wire-sec-body {\n display: none;\n padding: 0 17px 14px 32px;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 13px;\n line-height: 1.65;\n white-space: pre;\n color: var(--bench-ink);\n overflow-x: auto;\n}\n\n.cbp-wb__wire-sec--open .cbp-wb__wire-sec-body {\n display: block;\n}\n\n/* \u2500\u2500 Native Calls accordion (OUT panel \u2014 plugin\u2192host) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* Single-open: exactly one entry is expanded at a time. Newest entry auto- */\n/* opens on arrival and collapses all prior entries. Clicking an open entry */\n/* closes it; clicking a closed entry opens it and closes the rest. */\n.cbp-wb__calls {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 11px 15px;\n}\n\n/* Empty-state label shown before any calls arrive */\n.cbp-wb__calls-empty {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12px;\n color: var(--bench-ink-faint);\n padding: 12px 17px;\n font-style: italic;\n}\n\n/* .cbp-wb__log-entry \u2014 ONE per native call (class name frozen by tests and */\n/* native fidelity contract). Serves as the accordion item outer shell. */\n.cbp-wb__log-entry {\n background: var(--bench-3);\n border: 1px solid var(--bench-line);\n border-radius: 11px;\n overflow: hidden;\n}\n\n/* Open state: signal-tinted border + soft glow */\n.cbp-wb__log-entry--open {\n border-color: color-mix(in srgb, var(--bench-signal) 42%, transparent);\n box-shadow: 0 0 0 1px var(--bench-signal-soft);\n}\n\n/* Clickable top row: chevron + sequence number + method name + summary */\n.cbp-wb__log-entry-top {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 10px 13px;\n cursor: pointer;\n user-select: none;\n}\n\n.cbp-wb__log-entry-chev {\n font-size: 10px;\n width: 10px;\n color: var(--bench-ink-faint);\n flex: none;\n}\n\n.cbp-wb__log-entry--open .cbp-wb__log-entry-chev {\n color: var(--bench-signal-2);\n}\n\n.cbp-wb__log-entry-seq {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11px;\n color: var(--bench-ink-faint);\n}\n\n/* .cbp-wb__log-entry-method \u2014 method name (class name frozen by tests). */\n.cbp-wb__log-entry-method {\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 13px;\n font-weight: 600;\n color: var(--bench-signal-2);\n}\n\n/* One-line payload summary visible in collapsed state; hidden when expanded */\n.cbp-wb__log-entry-sum {\n margin-left: auto;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 11.5px;\n color: var(--bench-ink-faint);\n max-width: 50%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.cbp-wb__log-entry--open .cbp-wb__log-entry-sum {\n display: none;\n}\n\n/* Collapsible payload body \u2014 shown only when the entry is open */\n.cbp-wb__log-entry-body {\n display: none;\n padding: 0 13px 12px 32px;\n font-family: \"IBM Plex Mono\", ui-monospace, Menlo, monospace;\n font-size: 12.5px;\n line-height: 1.6;\n white-space: pre;\n color: var(--bench-ink);\n overflow-x: auto;\n}\n\n.cbp-wb__log-entry--open .cbp-wb__log-entry-body {\n display: block;\n}\n\n/* \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* FROZEN SPECIMEN GEOMETRY \u2014 pixel-identical to HEAD (native measurement */\n/* parity). Class names, padding, radius, border, and overflow rules are all */\n/* verbatim from the prior implementation. Only the surrounding layout */\n/* changed (canvas \u2192 stage / workspace). DO NOT adjust values below. */\n/* \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\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/* Native card shell: 10 pt padding / 7 pt radius / 1 pt accent border / elevated bg */\n/* Geometry source: AttachmentRenderCardShell.swift:24-59 */\n/* Outer card stacks the body (viewport) above the host action strip: */\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 */\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/* Host button strip (inside the card, below the body) */\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 */\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 Responsive reflow \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/* The side-by-side stage assumes a wide display: the renderer card runs up to\n 900px and must sit beside the 470px dock. Below ~1335px the card would clip\n against the dock. Stack instead \u2014 workspace on top at FULL width (the\n specimen is never clipped), dock below as a row of the two attachment cards.\n The dock drops its sticky behaviour here (it's no longer a side rail). */\n@media (max-width: 1335px) {\n .cbp-wb__stage {\n flex-direction: column;\n }\n /* Surface no longer needs to fill the viewport column \u2014 let it wrap the card\n so the dock below is reachable without a long scroll past empty space. */\n .cbp-wb__ws-surface {\n min-height: 420px;\n }\n .cbp-wb__dock {\n position: static;\n flex: none;\n width: 100%;\n max-height: none;\n flex-direction: row;\n align-items: flex-start;\n }\n .cbp-wb__att {\n flex: 1 1 0;\n min-width: 0;\n }\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
|
|
package/docs/preview.md
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
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
|
+
## Wire Input 面板(IN · host→plugin)
|
|
120
|
+
|
|
121
|
+
Wire Bench 在右侧 dock 的 IN 卡片中展示**完整 wire 注入内容**(`WirePayloads`):
|
|
122
|
+
|
|
123
|
+
| 段 | 类型 | 默认状态 |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `context` | infra | 折叠 |
|
|
126
|
+
| `item` | **content** | **展开** |
|
|
127
|
+
| `attachment`(renderer)/ `draft`(action)| **content** | **展开** |
|
|
128
|
+
| `theme` | infra | 折叠 |
|
|
129
|
+
| `locale` | infra | 折叠 |
|
|
130
|
+
|
|
131
|
+
- **content 段**(item + attachment/draft)默认展开,方便与右侧 OUT 输出对照。
|
|
132
|
+
- **infra 段**(context / theme / locale)默认折叠,减少干扰。
|
|
133
|
+
- 每段独立折叠,互不影响(仅 OUT 是单开 accordion)。
|
|
134
|
+
- `attachment.payloadJson` 字段:若为合法 JSON 字符串则**解析为可读嵌套 JSON**;解析失败回退原样字符串(不静默吞错)。
|
|
135
|
+
- scenario 切换或主题切换时面板自动刷新(theme 段反映最新预设)。
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Native Calls 面板(OUT · plugin→host)与 fake host
|
|
140
|
+
|
|
141
|
+
harness 安装一个最小双向 fake host:
|
|
142
|
+
|
|
143
|
+
- **host → plugin**:按 scenario 字段生成 wire payload 并注入;bootstrap、item、attachment、theme 等不需要真实宿主
|
|
144
|
+
- **plugin → host**:所有 `clipbus.*` native 调用被拦截,实时追加到右侧 dock 的 OUT 卡片;`clipbus.window.setHeight` 额外驱动视口高度;其他调用返回安全默认应答
|
|
145
|
+
|
|
146
|
+
OUT 卡片使用**单开 accordion**:
|
|
147
|
+
|
|
148
|
+
- 每条 native 调用产生一个折叠条目(方法名 + payload)。
|
|
149
|
+
- 新到调用**自动展开**并折叠所有旧条目——最新输出始终可见,payload 不会堆叠。
|
|
150
|
+
- 折叠态显示方法名 + 一行 payload 摘要;展开态显示完整缩进 payload。
|
|
151
|
+
- 点击折叠态条目展开它(同时收起其他);点击展开态条目收起它。
|
|
152
|
+
|
|
153
|
+
OUT 让你无需宿主即可验证 `setHeight` / `action.complete` / `clipboard.copyText` 等调用是否在正确时机触发、携带正确 payload。
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 主题预设
|
|
158
|
+
|
|
159
|
+
harness 内置 4 套 native 主题预设,覆盖深色/浅色两种配色方案:
|
|
160
|
+
|
|
161
|
+
| 预设键 | 显示名称 | 配色方案 | accent |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| `graphite`(默认) | Graphite (Dark) | dark | 青绿 `#43C6AC` |
|
|
164
|
+
| `ember` | Ember (Dark) | dark | 暖橙 `#F97316` |
|
|
165
|
+
| `porcelain` | Porcelain (Light) | light | 蓝 `#2563EB` |
|
|
166
|
+
| `sand` | Sand (Light) | light | 暖琥珀 `#D97706` |
|
|
167
|
+
|
|
168
|
+
每套预设包含 12 个 `PluginThemeTokens`(surface、surfaceElevated、textPrimary/Secondary/Tertiary、accent、accentContrast、border、divider、success、warning、danger),数值直接来自 native `AppearanceResolver.swift`,确保预览与真实 native 渲染一致。
|
|
169
|
+
|
|
170
|
+
`scenario.accentHex` 可在任意预设基础上覆盖 accent 颜色(如品牌色匹配),其他 token 保持预设值不变。
|
|
171
|
+
|
|
172
|
+
`defaultDarkTheme` / `defaultLightTheme` 是向后兼容快照,分别由 graphite / porcelain 预设派生。
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 工作台 UI(Wire Bench)
|
|
177
|
+
|
|
178
|
+
工作台代号 **Wire Bench**,整页分为左右两栏:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
┌ WIRE BENCH ················ [Surface][Scenario][Theme][Width] ┐
|
|
182
|
+
├───────────────────────────────────┬───────────────────────────┤
|
|
183
|
+
│ PREVIEW · Workspace (hero) │ IN · Wire Input │
|
|
184
|
+
│ ┌─────────────────────────┐ │ ▶context ▼item ▼attach │
|
|
185
|
+
│ │ specimen card (frozen) │ │ ▶theme ▶locale │
|
|
186
|
+
│ │ [plugin webview] │ ├───────────────────────────┤
|
|
187
|
+
│ │ [button strip] │ │ OUT · Native Calls │
|
|
188
|
+
│ └─────────────────────────┘ │ ▼#N … ▶#N-1 … ▶#1 … │
|
|
189
|
+
│ 生长地板(ceiling ≈ 800px 余量) │ │
|
|
190
|
+
└───────────────────────────────────┴───────────────────────────┘
|
|
191
|
+
左:主工作区(占满列高) 右:dock(position:sticky)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
npm run dev
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Vite 启动后,工作台提供:
|
|
199
|
+
|
|
200
|
+
- **两级模式导航**:顶层 Surface 选择器在 **Renderer**(attachmentRenderer)和 **Action** 两种模式间切换;两种模式的 scenario 均用 `<select>` 下拉选择(文案取自 `scenario.label`)。任何时刻只渲染当前选中 scenario。URL query `?view=&scenario=&theme=` 同步,刷新不丢。
|
|
201
|
+
- **主题切换**(4 预设:Graphite · Ember · Porcelain · Sand):切换时三重作用:① 重注入 wire 主题 topic,插件已注册的 `clipbus.theme.on()` 回调立即触发;② 仅重着色**卡片壳与插件内容**(chrome 本身配色保持稳定,见下方[主题解耦](#主题解耦chrome-稳定--卡片跟-native));③ 在插件挂载槽上更新 `--clipbus-*` CSS 变量——不触发 remount,与 native `theme.onChange` 行为一致。注意:插件的 `--clipbus-accent` 是**全局主题 accent**;`scenario.accentHex` 是该附件的 **tintColor**,只染**卡片边框**(对应 native `record.tintColor`),不进插件配色。
|
|
202
|
+
- **Workspace(主工作区,左列)**:标本卡(frozen specimen)居中展示,下方有**生长地板**标注 `native ceiling ≈ 800px`,把卡片下方空白变成"到硬上限的余量"这一有意义信息。卡片再高只撑左列,不影响右侧 dock。
|
|
203
|
+
- **Dock(右列,`position:sticky`)**:卡片变高时右侧不被推走——IN / OUT 两张附件卡始终在视野内。
|
|
204
|
+
- **IN · Wire Input 面板**:见[上文 Wire Input 面板](#wire-input-面板in--hostplugin)。
|
|
205
|
+
- **OUT · Native Calls 面板**:见[上文 Native Calls 面板](#native-calls-面板out--pluginhost与-fake-host)。
|
|
206
|
+
- **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 + 按钮条。
|
|
207
|
+
- **宽度滑杆**:拖动**卡片外层宽度**(webview 随之按 chrome 内嵌),范围随 view 切换。模式默认(取自 native):renderer 初始 720 px、范围 [560,900];action 初始 350 px、范围 [300,500]。`scenario.viewport.width` 可覆盖初始值;`viewport.widthMin`/`widthMax` 可覆盖滑杆范围。
|
|
208
|
+
- **body 高度**:高度**由插件驱动**。native **没有**宿主侧"量内容定高":`auto` 在 native 解析为 `bounded(80, 800)`,且只认 `clipbus.window.setHeight`——插件**必须**用 SDK `autoFit`(或手动 `setHeight`)自撑,否则卡在 policy 最小高度被裁。harness 在插件首个 `setHeight` 前以内容高度做种子(宽松的预览便利,**非** native 行为)。所有策略统一封顶 native 硬上限 **800**(`AttachmentRenderHeightConstants.ceiling`)。
|
|
209
|
+
- **按钮条**:源自 `scenario.buttons`(及插件 `setButtons`),位于**卡片内部、body 下方、左对齐**(native action row 布局);无按钮时不占位。
|
|
210
|
+
- **主题感知滚动条**:工作台内所有滚动区域(dock 面板 + 插件内容)统一为细、半透明的滚动条,使用稳定的 bench 调色(不随主题变色)。
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## 主题解耦:chrome 稳定 / 卡片跟 native
|
|
215
|
+
|
|
216
|
+
**本节记录一个有意的设计取舍,与前版本(native-fidelity D3)行为不同。**
|
|
217
|
+
|
|
218
|
+
前版本(`2026-06-28-plugin-preview-native-fidelity-design.md` D3):chrome 背板颜色随主题预设 `backdrop` 变色(graphite 深灰、ember 暖棕等)。
|
|
219
|
+
|
|
220
|
+
Wire Bench(`2026-06-28-plugin-preview-wire-bench-design.md` D5)**反转此行为**:
|
|
221
|
+
|
|
222
|
+
- **chrome 自身颜色稳定**(固定冷石板 `#14161D` + blueprint 网格),不随主题切换变色。
|
|
223
|
+
- **卡片壳与插件内容仍跟随主题**:`applyTheme()` 仍写入 `--cbp-wb-accent`(卡片 tint 回退 + 插件 accent)、`--cbp-wb-surface-elevated`、`--cbp-wb-border` 等卡片侧 token,`applyThemeCssVars()` 仍在插件槽更新 `--clipbus-*` 变量。
|
|
224
|
+
|
|
225
|
+
**取舍理由**:目标是「工具 chrome 是工具自己的,标本才是被观察对象」——chrome 与卡片同步变色时二者配色相互掩盖,削弱标本凸显;稳定的 chrome 使标本(native 配色的卡片)始终从工具背景中突出。
|
|
226
|
+
|
|
227
|
+
如需恢复前版本行为(chrome 背板跟主题),在 `styles.ts` 的 `.cbp-wb` 根规则中将 `background` 改回 `var(--cbp-wb-backdrop)`,并在 `chrome.ts` 的 `applyTheme()` 中恢复 `wb.style.setProperty('--cbp-wb-backdrop', preset.backdrop)`。
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Native 卡片壳与组件去框约定
|
|
232
|
+
|
|
233
|
+
harness 在 `slotEl` 外提供 native 风格卡片框,模拟真实宿主为插件提供的统一外壳。因此:
|
|
234
|
+
|
|
235
|
+
> **约定:插件组件不应自绘最外层 `background` / `border` / `border-radius` / `box-shadow` / 外层 `padding`。**
|
|
236
|
+
> 宿主(和 harness)统一提供卡片框;插件在框内布局内容即可。
|
|
237
|
+
|
|
238
|
+
违反此约定的后果:预览与 native 外观一致,但若宿主后续更换卡片样式,插件自绘的外框会与宿主框冲突(双重描边、背景叠加)。`template-plugin` 的全部 renderer 组件(`preview-renderer`、`expanded-renderer`、3 个 `capability-gallery` renderer)均已遵循此约定。
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 超限内滚约定
|
|
243
|
+
|
|
244
|
+
native 宿主在 `clipbus.window.setHeight` 到上限(800 pt)时**直接裁剪**,不为插件提供外部滚动容器。因此,若插件内容可能超出最大高度,插件需在自身内部处理滚动:
|
|
245
|
+
|
|
246
|
+
```css
|
|
247
|
+
/* 示例:expanded-renderer 方案 */
|
|
248
|
+
.outer-shell {
|
|
249
|
+
overflow-y: auto; /* 超出时内部滚动 */
|
|
250
|
+
max-height: 100%; /* 跟随视口高度 */
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`template-plugin` 的 `expanded-renderer` 是此约定的参考实现(外层 shell `overflow-y: auto` + 内容区自适应,兼容 `autoFit` 自动高度策略)。
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 参考实现
|
|
259
|
+
|
|
260
|
+
`plugins/template-plugin/src/preview/preview-host/main.ts` 是规范的薄胶水范例——按 `scenario.mode` 和 `scenario.view` 路由到不同 Vue 组件:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
import { createPreviewWorkbench } from '@clipbus/plugin-sdk/preview';
|
|
264
|
+
import { createApp } from 'vue';
|
|
265
|
+
import { attachmentScenarios } from '../scenarios/attachmentScenarios';
|
|
266
|
+
import { actionScenarios } from '../scenarios/actionScenarios';
|
|
267
|
+
import AttachmentApp from '../../features/preview-renderer/app.vue';
|
|
268
|
+
import ExpandedApp from '../../features/expanded-renderer/app.vue';
|
|
269
|
+
import DraftActionApp from '../../features/capability-gallery/draft-action-ui/app.vue';
|
|
270
|
+
import RendererFixedApp from '../../features/capability-gallery/renderer-fixed-ui/app.vue';
|
|
271
|
+
import RendererAutoApp from '../../features/capability-gallery/renderer-auto-ui/app.vue';
|
|
272
|
+
import RendererBoundedApp from '../../features/capability-gallery/renderer-bounded-ui/app.vue';
|
|
273
|
+
|
|
274
|
+
createPreviewWorkbench(document.getElementById('app')!, {
|
|
275
|
+
scenarios: [...attachmentScenarios, ...actionScenarios],
|
|
276
|
+
mount(slotEl, { scenario }) {
|
|
277
|
+
const Comp =
|
|
278
|
+
scenario.mode === 'action'
|
|
279
|
+
? DraftActionApp
|
|
280
|
+
: scenario.view === 'expanded'
|
|
281
|
+
? ExpandedApp
|
|
282
|
+
: scenario.view === 'gallery-fixed'
|
|
283
|
+
? RendererFixedApp
|
|
284
|
+
: scenario.view === 'gallery-auto'
|
|
285
|
+
? RendererAutoApp
|
|
286
|
+
: scenario.view === 'gallery-bounded'
|
|
287
|
+
? RendererBoundedApp
|
|
288
|
+
: AttachmentApp;
|
|
289
|
+
const app = createApp(Comp);
|
|
290
|
+
app.mount(slotEl);
|
|
291
|
+
return () => app.unmount();
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`createPreviewWorkbench` 接管全部工作台基础设施,`mount` 里只有插件自己的框架调用——这就是完整模式。
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 高级导出
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// 供测试与集成场景使用;日常插件开发只需 createPreviewWorkbench
|
|
304
|
+
|
|
305
|
+
// Wire 工具
|
|
306
|
+
import { computeWirePayloads, injectWire } from '@clipbus/plugin-sdk/preview';
|
|
307
|
+
import { applyThemeCssVars, THEME_CSS_VAR_NAMES } from '@clipbus/plugin-sdk/preview';
|
|
308
|
+
import { installFakeHost } from '@clipbus/plugin-sdk/preview';
|
|
309
|
+
import type { FakeHostHooks, WirePayloads } from '@clipbus/plugin-sdk/preview';
|
|
310
|
+
|
|
311
|
+
// 主题预设(4 套内置预设)
|
|
312
|
+
import { previewThemePresets, getThemePreset, DEFAULT_THEME_KEY } from '@clipbus/plugin-sdk/preview';
|
|
313
|
+
import type { PreviewThemePreset } from '@clipbus/plugin-sdk/preview';
|
|
314
|
+
// 向后兼容快照(由 graphite/porcelain 派生,保留以兼容旧测试)
|
|
315
|
+
import { defaultDarkTheme, defaultLightTheme } from '@clipbus/plugin-sdk/preview';
|
|
316
|
+
|
|
317
|
+
// Chrome 纯函数(测试/集成用)
|
|
318
|
+
import { groupScenariosByMode, resolveViewportWidth, clampWidth } from '@clipbus/plugin-sdk/preview';
|
|
319
|
+
|
|
320
|
+
// Wire 协议常量
|
|
321
|
+
import { PLUGIN_CALL_HANDLER_NAME, WINDOW_SET_HEIGHT_METHOD } from '@clipbus/plugin-sdk/preview';
|
|
322
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipbus/plugin-sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
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": {
|