webflow-clipboard 1.0.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.
Files changed (38) hide show
  1. package/README.md +149 -0
  2. package/dist/copy-payload.d.ts +44 -0
  3. package/dist/copy-payload.d.ts.map +1 -0
  4. package/dist/copy-payload.js +163 -0
  5. package/dist/copy-to-webflow-button.d.ts +26 -0
  6. package/dist/copy-to-webflow-button.d.ts.map +1 -0
  7. package/dist/copy-to-webflow-button.js +23 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +14 -0
  11. package/dist/react.d.ts +7 -0
  12. package/dist/react.d.ts.map +1 -0
  13. package/dist/react.js +10 -0
  14. package/dist/use-copy-to-webflow.d.ts +23 -0
  15. package/dist/use-copy-to-webflow.d.ts.map +1 -0
  16. package/dist/use-copy-to-webflow.js +57 -0
  17. package/dist/use-paste-from-webflow.d.ts +20 -0
  18. package/dist/use-paste-from-webflow.d.ts.map +1 -0
  19. package/dist/use-paste-from-webflow.js +80 -0
  20. package/dist-esm/copy-payload.d.ts +44 -0
  21. package/dist-esm/copy-payload.d.ts.map +1 -0
  22. package/dist-esm/copy-payload.mjs +155 -0
  23. package/dist-esm/copy-to-webflow-button.d.ts +26 -0
  24. package/dist-esm/copy-to-webflow-button.d.ts.map +1 -0
  25. package/dist-esm/copy-to-webflow-button.mjs +20 -0
  26. package/dist-esm/index.d.ts +6 -0
  27. package/dist-esm/index.d.ts.map +1 -0
  28. package/dist-esm/index.mjs +5 -0
  29. package/dist-esm/react.d.ts +7 -0
  30. package/dist-esm/react.d.ts.map +1 -0
  31. package/dist-esm/react.mjs +4 -0
  32. package/dist-esm/use-copy-to-webflow.d.ts +23 -0
  33. package/dist-esm/use-copy-to-webflow.d.ts.map +1 -0
  34. package/dist-esm/use-copy-to-webflow.mjs +54 -0
  35. package/dist-esm/use-paste-from-webflow.d.ts +20 -0
  36. package/dist-esm/use-paste-from-webflow.d.ts.map +1 -0
  37. package/dist-esm/use-paste-from-webflow.mjs +77 -0
  38. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # webflow-clipboard
2
+
3
+ Copy to Webflow / Paste from Webflow as a reusable npm library. Put Xscp JSON on the clipboard in the format Webflow Designer expects (so users can **Cmd+V** in Designer), or extract Xscp from a paste event.
4
+
5
+ Use the **core API** in any JS/TS app, or the **React** components for a ready **"Copy to Webflow"** button.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install webflow-clipboard
11
+ # or
12
+ bun add webflow-clipboard
13
+ # or
14
+ pnpm add webflow-clipboard
15
+ ```
16
+
17
+ ## Quick start — React "Copy to Webflow" button
18
+
19
+ ```tsx
20
+ import { CopyToWebflowButton } from "webflow-clipboard/react";
21
+
22
+ // Your Xscp JSON (e.g. from your CMS, Git, or "Paste from Webflow")
23
+ const xscpJson = '{"type":"@webflow/XscpData","payload":{...}}';
24
+
25
+ export function MyComponent() {
26
+ return (
27
+ <CopyToWebflowButton
28
+ xscpJson={xscpJson}
29
+ className="rounded-full bg-black text-white px-4 py-2"
30
+ showMessage
31
+ />
32
+ );
33
+ }
34
+ ```
35
+
36
+ On click, the button copies the JSON to the clipboard so the user can open Webflow Designer and press **Cmd+V** (Mac) or **Ctrl+V** (Windows).
37
+
38
+ ## Core API (no React)
39
+
40
+ Use in any environment (Node scripts, vanilla JS, other frameworks):
41
+
42
+ ```ts
43
+ import {
44
+ writeClipboardXscp,
45
+ normalizeXscp,
46
+ validateCopyPayload,
47
+ getXscpFromPasteEvent,
48
+ } from "webflow-clipboard";
49
+
50
+ // Copy Xscp to clipboard (must run in a user gesture / browser context)
51
+ writeClipboardXscp(xscpJsonString);
52
+
53
+ // Normalize / validate before copy
54
+ const result = validateCopyPayload(rawString);
55
+ if (result.valid) {
56
+ writeClipboardXscp(rawString);
57
+ }
58
+
59
+ // In a paste handler (e.g. onPaste on a contenteditable or textarea)
60
+ element.addEventListener("paste", (e) => {
61
+ const json = getXscpFromPasteEvent(e);
62
+ if (json) {
63
+ e.preventDefault();
64
+ console.log("Pasted Webflow JSON", json);
65
+ }
66
+ });
67
+ ```
68
+
69
+ ## React hooks
70
+
71
+ ### useCopyToWebflow
72
+
73
+ Build your own button or trigger copy from code:
74
+
75
+ ```tsx
76
+ import { useCopyToWebflow } from "webflow-clipboard/react";
77
+
78
+ function MyButton() {
79
+ const { copyToWebflow, status, error, successMessage } = useCopyToWebflow({
80
+ successMessage: "Copied! Paste in Webflow Designer with Cmd+V.",
81
+ });
82
+
83
+ return (
84
+ <div>
85
+ <button onClick={() => copyToWebflow(xscpJson)}>Copy to Webflow</button>
86
+ {error && <p role="alert">{error}</p>}
87
+ {successMessage && <p>{successMessage}</p>}
88
+ </div>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ### usePasteFromWebflow
94
+
95
+ Implement a "Paste from Webflow" area (user copies in Designer, pastes in your app):
96
+
97
+ ```tsx
98
+ import { usePasteFromWebflow } from "webflow-clipboard/react";
99
+
100
+ function PasteArea() {
101
+ const { json, onPaste, error, copyJson, copied } = usePasteFromWebflow();
102
+
103
+ return (
104
+ <div>
105
+ <textarea
106
+ onPaste={onPaste}
107
+ value={json}
108
+ placeholder="Click here, then Cmd+V (after copying in Webflow Designer)"
109
+ readOnly
110
+ />
111
+ {error && <p role="alert">{error}</p>}
112
+ {json && (
113
+ <button onClick={copyJson}>{copied ? "Copied" : "Copy JSON"}</button>
114
+ )}
115
+ </div>
116
+ );
117
+ }
118
+ ```
119
+
120
+ ## API reference
121
+
122
+ ### Core (`webflow-clipboard`)
123
+
124
+ | Export | Description |
125
+ |--------|-------------|
126
+ | `writeClipboardXscp(xscp)` | Copies normalized Xscp to clipboard (both `application/json` and `text/plain`) so Webflow Designer accepts Cmd+V. |
127
+ | `normalizeXscp(raw)` | Normalizes raw Xscp string; throws if invalid. |
128
+ | `normalizeCopyPayload(payload)` | Extracts first JSON object from a string (for payloads with junk). |
129
+ | `validateCopyPayload(payload)` | Returns `{ valid, length }` or `{ valid: false, error, length, tailPreview? }`. |
130
+ | `isXscpPayload(payload)` | Returns true if payload looks like Webflow Xscp. |
131
+ | `getXscpFromPasteEvent(e)` | From a `ClipboardEvent`, returns Xscp string or `null`. |
132
+
133
+ ### React (`webflow-clipboard/react`)
134
+
135
+ | Export | Description |
136
+ |--------|-------------|
137
+ | `CopyToWebflowButton` | Button that takes `xscpJson` and copies on click. |
138
+ | `useCopyToWebflow(options?)` | Hook: `copyToWebflow(xscp)`, `status`, `error`, `successMessage`, `reset`. |
139
+ | `usePasteFromWebflow()` | Hook: `json`, `onPaste`, `error`, `copyJson`, `copied`, `reset`. |
140
+
141
+ ## Why this exists
142
+
143
+ Webflow Designer only accepts paste when the clipboard has `application/json`. Browsers don’t allow setting that via `navigator.clipboard.write()`; this library uses a programmatic **copy** event to set both `application/json` and `text/plain`, so Designer accepts **Cmd+V**.
144
+
145
+ For **paste from Webflow**, Xscp is only exposed on the **paste** event (not on normal clipboard read), so you need a paste handler; `getXscpFromPasteEvent` / `usePasteFromWebflow` do that.
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Ensures Webflow XSCP JSON payload starts with "{" with no leading characters,
3
+ * and has no trailing non-whitespace (only the first complete JSON object).
4
+ * Webflow Designer paste handler requires the clipboard to start exactly with "{".
5
+ */
6
+ export declare function normalizeCopyPayload(payload: string): string;
7
+ export type ValidatePayloadResult = {
8
+ valid: true;
9
+ length: number;
10
+ } | {
11
+ valid: false;
12
+ error: string;
13
+ length: number;
14
+ tailPreview?: string;
15
+ };
16
+ /**
17
+ * Normalizes raw XSCP string: strips BOM, trims, removes junk before
18
+ * {"type":"@webflow/XscpData"}, removes any trailing non-whitespace after the
19
+ * root object, validates with JSON.parse. Throws if invalid.
20
+ * Use for Webflow paste (ClipboardItem application/json).
21
+ */
22
+ export declare function normalizeXscp(raw: string): string;
23
+ /**
24
+ * Copies XSCP to clipboard via the copy event so we can set both
25
+ * application/json and text/plain (Clipboard API write() rejects application/json).
26
+ * Triggers a programmatic copy and intercepts the copy event to set clipboardData.
27
+ */
28
+ export declare function writeClipboardXscp(xscp: string): void;
29
+ /**
30
+ * Returns true if the payload looks like Webflow XSCP JSON (use ClipboardItem path).
31
+ */
32
+ export declare function isXscpPayload(payload: string): boolean;
33
+ /**
34
+ * Extracts Webflow Xscp JSON from a paste (clipboard) event.
35
+ * Use in an onPaste handler: if this returns a string, preventDefault and use it.
36
+ * Returns null if the clipboard doesn't contain Webflow JSON.
37
+ */
38
+ export declare function getXscpFromPasteEvent(e: ClipboardEvent): string | null;
39
+ /**
40
+ * Validates that the payload is parseable JSON. Webflow shows "clipboard is empty"
41
+ * when the payload is invalid or truncated. Use this to warn the user before paste.
42
+ */
43
+ export declare function validateCopyPayload(payload: string): ValidatePayloadResult;
44
+ //# sourceMappingURL=copy-payload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-payload.d.ts","sourceRoot":"","sources":["../src/copy-payload.ts"],"names":[],"mappings":"AAkCA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI1E;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAyBrD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEtD;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAUtE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CA8B1E"}
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeCopyPayload = normalizeCopyPayload;
4
+ exports.normalizeXscp = normalizeXscp;
5
+ exports.writeClipboardXscp = writeClipboardXscp;
6
+ exports.isXscpPayload = isXscpPayload;
7
+ exports.getXscpFromPasteEvent = getXscpFromPasteEvent;
8
+ exports.validateCopyPayload = validateCopyPayload;
9
+ /**
10
+ * Extracts the first complete JSON object from a string (from first "{" to
11
+ * its matching "}"). Used when payload has trailing junk that causes JSON.parse to fail.
12
+ */
13
+ function extractFirstJsonObject(s) {
14
+ const start = s.indexOf("{");
15
+ if (start === -1)
16
+ return s;
17
+ let depth = 0;
18
+ let inString = false;
19
+ let escape = false;
20
+ for (let i = start; i < s.length; i++) {
21
+ const c = s[i];
22
+ if (escape) {
23
+ escape = false;
24
+ continue;
25
+ }
26
+ if (inString) {
27
+ if (c === "\\")
28
+ escape = true;
29
+ else if (c === '"')
30
+ inString = false;
31
+ continue;
32
+ }
33
+ if (c === '"') {
34
+ inString = true;
35
+ continue;
36
+ }
37
+ if (c === "{")
38
+ depth++;
39
+ else if (c === "}") {
40
+ depth--;
41
+ if (depth === 0)
42
+ return s.slice(start, i + 1);
43
+ }
44
+ }
45
+ return s.slice(start);
46
+ }
47
+ /**
48
+ * Ensures Webflow XSCP JSON payload starts with "{" with no leading characters,
49
+ * and has no trailing non-whitespace (only the first complete JSON object).
50
+ * Webflow Designer paste handler requires the clipboard to start exactly with "{".
51
+ */
52
+ function normalizeCopyPayload(payload) {
53
+ const trimmed = payload.trim();
54
+ const firstBrace = trimmed.indexOf("{");
55
+ if (firstBrace === -1)
56
+ return trimmed;
57
+ return extractFirstJsonObject(trimmed.slice(firstBrace));
58
+ }
59
+ const XSCP_PREFIX = '{"type":"@webflow/XscpData"';
60
+ /**
61
+ * Normalizes raw XSCP string: strips BOM, trims, removes junk before
62
+ * {"type":"@webflow/XscpData"}, removes any trailing non-whitespace after the
63
+ * root object, validates with JSON.parse. Throws if invalid.
64
+ * Use for Webflow paste (ClipboardItem application/json).
65
+ */
66
+ function normalizeXscp(raw) {
67
+ let s = raw.replace(/^\uFEFF/, "").trimStart();
68
+ const idx = s.indexOf(XSCP_PREFIX);
69
+ if (idx > 0)
70
+ s = s.slice(idx);
71
+ s = extractFirstJsonObject(s);
72
+ JSON.parse(s); // validate – throws if invalid
73
+ return s;
74
+ }
75
+ /**
76
+ * Copies XSCP to clipboard via the copy event so we can set both
77
+ * application/json and text/plain (Clipboard API write() rejects application/json).
78
+ * Triggers a programmatic copy and intercepts the copy event to set clipboardData.
79
+ */
80
+ function writeClipboardXscp(xscp) {
81
+ const json = normalizeXscp(xscp);
82
+ const onCopy = (e) => {
83
+ e.preventDefault();
84
+ e.clipboardData?.setData("application/json", json);
85
+ e.clipboardData?.setData("text/plain", json);
86
+ };
87
+ document.addEventListener("copy", onCopy, true);
88
+ try {
89
+ const textarea = document.createElement("textarea");
90
+ textarea.value = json;
91
+ textarea.style.position = "fixed";
92
+ textarea.style.left = "-9999px";
93
+ textarea.style.top = "0";
94
+ document.body.appendChild(textarea);
95
+ textarea.focus();
96
+ textarea.select();
97
+ document.execCommand("copy");
98
+ document.body.removeChild(textarea);
99
+ }
100
+ finally {
101
+ document.removeEventListener("copy", onCopy, true);
102
+ }
103
+ }
104
+ /**
105
+ * Returns true if the payload looks like Webflow XSCP JSON (use ClipboardItem path).
106
+ */
107
+ function isXscpPayload(payload) {
108
+ return payload.includes(XSCP_PREFIX);
109
+ }
110
+ const XSCP_MARKER = "@webflow/XscpData";
111
+ /**
112
+ * Extracts Webflow Xscp JSON from a paste (clipboard) event.
113
+ * Use in an onPaste handler: if this returns a string, preventDefault and use it.
114
+ * Returns null if the clipboard doesn't contain Webflow JSON.
115
+ */
116
+ function getXscpFromPasteEvent(e) {
117
+ const dt = e.clipboardData;
118
+ if (!dt)
119
+ return null;
120
+ const json = dt.getData("application/json");
121
+ const text = dt.getData("text/plain");
122
+ if (json && json.includes(XSCP_MARKER))
123
+ return json;
124
+ if (text && text.includes(XSCP_MARKER))
125
+ return text;
126
+ if (json && json.trim().startsWith("{"))
127
+ return json;
128
+ if (text && text.trim().startsWith("{"))
129
+ return text;
130
+ return null;
131
+ }
132
+ /**
133
+ * Validates that the payload is parseable JSON. Webflow shows "clipboard is empty"
134
+ * when the payload is invalid or truncated. Use this to warn the user before paste.
135
+ */
136
+ function validateCopyPayload(payload) {
137
+ const normalized = normalizeCopyPayload(payload);
138
+ const length = normalized.length;
139
+ if (!normalized) {
140
+ return { valid: false, error: "Payload is empty.", length: 0 };
141
+ }
142
+ if (normalized[0] !== "{") {
143
+ return {
144
+ valid: false,
145
+ error: "Payload does not start with {. Webflow requires valid XSCP JSON.",
146
+ length,
147
+ };
148
+ }
149
+ try {
150
+ JSON.parse(normalized);
151
+ return { valid: true, length };
152
+ }
153
+ catch (e) {
154
+ const message = e instanceof Error ? e.message : String(e);
155
+ const tailPreview = length > 80 ? `…${normalized.slice(-80)}` : normalized;
156
+ return {
157
+ valid: false,
158
+ error: message,
159
+ length,
160
+ tailPreview,
161
+ };
162
+ }
163
+ }
@@ -0,0 +1,26 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ export interface CopyToWebflowButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
3
+ /** Xscp JSON string to copy when the button is clicked. */
4
+ xscpJson: string;
5
+ /** Optional success message (default: instruction to paste in Designer). */
6
+ successMessage?: string;
7
+ /** Label when idle. Default: "Copy to Webflow" */
8
+ label?: string;
9
+ /** Label when copied successfully. Default: "Copied" */
10
+ copiedLabel?: string;
11
+ /** Label when error. Default: "Copy failed" */
12
+ errorLabel?: string;
13
+ /** Optional class name for the button (e.g. Tailwind). */
14
+ className?: string;
15
+ /** Optional class for the status/error message container. */
16
+ messageClassName?: string;
17
+ /** Whether to show status/error message below the button. Default: true */
18
+ showMessage?: boolean;
19
+ }
20
+ /**
21
+ * Ready-to-use "Copy to Webflow" button. Pass your Xscp JSON; on click it copies
22
+ * to the clipboard in the format Webflow Designer expects (Cmd+V in Designer).
23
+ * Style with className or wrap in your own UI.
24
+ */
25
+ export declare function CopyToWebflowButton({ xscpJson, successMessage, label, copiedLabel, errorLabel, className, messageClassName, showMessage, children, ...buttonProps }: CopyToWebflowButtonProps): import("react/jsx-runtime").JSX.Element;
26
+ //# sourceMappingURL=copy-to-webflow-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-to-webflow-button.d.ts","sourceRoot":"","sources":["../src/copy-to-webflow-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAGlD,MAAM,WAAW,wBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,cAAc,EACd,KAAoB,EACpB,WAAgC,EAChC,UAA8B,EAC9B,SAAS,EACT,gBAAgB,EAChB,WAAkB,EAClB,QAAQ,EACR,GAAG,WAAW,EACf,EAAE,wBAAwB,2CAqC1B"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CopyToWebflowButton = CopyToWebflowButton;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const use_copy_to_webflow_1 = require("./use-copy-to-webflow");
7
+ const defaultLabel = "Copy to Webflow";
8
+ const defaultCopiedLabel = "Copied";
9
+ const defaultErrorLabel = "Copy failed";
10
+ /**
11
+ * Ready-to-use "Copy to Webflow" button. Pass your Xscp JSON; on click it copies
12
+ * to the clipboard in the format Webflow Designer expects (Cmd+V in Designer).
13
+ * Style with className or wrap in your own UI.
14
+ */
15
+ function CopyToWebflowButton({ xscpJson, successMessage, label = defaultLabel, copiedLabel = defaultCopiedLabel, errorLabel = defaultErrorLabel, className, messageClassName, showMessage = true, children, ...buttonProps }) {
16
+ const { copyToWebflow, status, error, successMessage: msg, } = (0, use_copy_to_webflow_1.useCopyToWebflow)({ successMessage });
17
+ const displayText = status === "ok"
18
+ ? copiedLabel
19
+ : status === "err"
20
+ ? errorLabel
21
+ : label;
22
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => copyToWebflow(xscpJson), disabled: !xscpJson?.trim(), className: className, ...buttonProps, children: children ?? displayText }), showMessage && (error || msg) && ((0, jsx_runtime_1.jsx)("div", { className: messageClassName, role: error ? "alert" : "status", "aria-live": "polite", children: error ?? msg }))] }));
23
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * webflow-clipboard — Copy to Webflow / Paste from Webflow as reusable primitives.
3
+ * Use the core API in any JS/TS app, or the React components for a ready "Copy to Webflow" button.
4
+ */
5
+ export { normalizeCopyPayload, normalizeXscp, writeClipboardXscp, isXscpPayload, validateCopyPayload, getXscpFromPasteEvent, type ValidatePayloadResult, } from "./copy-payload";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * webflow-clipboard — Copy to Webflow / Paste from Webflow as reusable primitives.
4
+ * Use the core API in any JS/TS app, or the React components for a ready "Copy to Webflow" button.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.getXscpFromPasteEvent = exports.validateCopyPayload = exports.isXscpPayload = exports.writeClipboardXscp = exports.normalizeXscp = exports.normalizeCopyPayload = void 0;
8
+ var copy_payload_1 = require("./copy-payload");
9
+ Object.defineProperty(exports, "normalizeCopyPayload", { enumerable: true, get: function () { return copy_payload_1.normalizeCopyPayload; } });
10
+ Object.defineProperty(exports, "normalizeXscp", { enumerable: true, get: function () { return copy_payload_1.normalizeXscp; } });
11
+ Object.defineProperty(exports, "writeClipboardXscp", { enumerable: true, get: function () { return copy_payload_1.writeClipboardXscp; } });
12
+ Object.defineProperty(exports, "isXscpPayload", { enumerable: true, get: function () { return copy_payload_1.isXscpPayload; } });
13
+ Object.defineProperty(exports, "validateCopyPayload", { enumerable: true, get: function () { return copy_payload_1.validateCopyPayload; } });
14
+ Object.defineProperty(exports, "getXscpFromPasteEvent", { enumerable: true, get: function () { return copy_payload_1.getXscpFromPasteEvent; } });
@@ -0,0 +1,7 @@
1
+ export { CopyToWebflowButton } from "./copy-to-webflow-button";
2
+ export type { CopyToWebflowButtonProps } from "./copy-to-webflow-button";
3
+ export { useCopyToWebflow } from "./use-copy-to-webflow";
4
+ export type { CopyToWebflowStatus, UseCopyToWebflowOptions, UseCopyToWebflowReturn, } from "./use-copy-to-webflow";
5
+ export { usePasteFromWebflow } from "./use-paste-from-webflow";
6
+ export type { UsePasteFromWebflowReturn } from "./use-paste-from-webflow";
7
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,YAAY,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC"}
package/dist/react.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.usePasteFromWebflow = exports.useCopyToWebflow = exports.CopyToWebflowButton = void 0;
5
+ var copy_to_webflow_button_1 = require("./copy-to-webflow-button");
6
+ Object.defineProperty(exports, "CopyToWebflowButton", { enumerable: true, get: function () { return copy_to_webflow_button_1.CopyToWebflowButton; } });
7
+ var use_copy_to_webflow_1 = require("./use-copy-to-webflow");
8
+ Object.defineProperty(exports, "useCopyToWebflow", { enumerable: true, get: function () { return use_copy_to_webflow_1.useCopyToWebflow; } });
9
+ var use_paste_from_webflow_1 = require("./use-paste-from-webflow");
10
+ Object.defineProperty(exports, "usePasteFromWebflow", { enumerable: true, get: function () { return use_paste_from_webflow_1.usePasteFromWebflow; } });
@@ -0,0 +1,23 @@
1
+ export type CopyToWebflowStatus = "idle" | "ok" | "err";
2
+ export interface UseCopyToWebflowOptions {
3
+ /** Message shown after successful copy. */
4
+ successMessage?: string;
5
+ }
6
+ export interface UseCopyToWebflowReturn {
7
+ /** Call with Xscp JSON string to copy to clipboard (Webflow-ready). */
8
+ copyToWebflow: (xscpJson: string) => void;
9
+ /** Current status: idle | ok | err */
10
+ status: CopyToWebflowStatus;
11
+ /** Error message when status is "err". */
12
+ error: string | null;
13
+ /** Success message when status is "ok". */
14
+ successMessage: string | null;
15
+ /** Reset status/error to idle. */
16
+ reset: () => void;
17
+ }
18
+ /**
19
+ * Hook to copy Xscp JSON to the clipboard in a format Webflow Designer accepts.
20
+ * Use this to build your own "Copy to Webflow" button or trigger copy from code.
21
+ */
22
+ export declare function useCopyToWebflow(options?: UseCopyToWebflowOptions): UseCopyToWebflowReturn;
23
+ //# sourceMappingURL=use-copy-to-webflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-copy-to-webflow.d.ts","sourceRoot":"","sources":["../src/use-copy-to-webflow.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC;AAKxD,MAAM,WAAW,uBAAuB;IACtC,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,sCAAsC;IACtC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kCAAkC;IAClC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAoDxB"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.useCopyToWebflow = useCopyToWebflow;
5
+ const react_1 = require("react");
6
+ const copy_payload_1 = require("./copy-payload");
7
+ const DEFAULT_SUCCESS_MSG = "Copied. Open Webflow Designer, click canvas or Body, then press Cmd+V (Mac) or Ctrl+V (Windows).";
8
+ /**
9
+ * Hook to copy Xscp JSON to the clipboard in a format Webflow Designer accepts.
10
+ * Use this to build your own "Copy to Webflow" button or trigger copy from code.
11
+ */
12
+ function useCopyToWebflow(options = {}) {
13
+ const { successMessage: customSuccess = DEFAULT_SUCCESS_MSG } = options;
14
+ const [status, setStatus] = (0, react_1.useState)("idle");
15
+ const [error, setError] = (0, react_1.useState)(null);
16
+ const [successMessage, setSuccessMessage] = (0, react_1.useState)(null);
17
+ const reset = (0, react_1.useCallback)(() => {
18
+ setStatus("idle");
19
+ setError(null);
20
+ setSuccessMessage(null);
21
+ }, []);
22
+ const copyToWebflow = (0, react_1.useCallback)((xscpJson) => {
23
+ setError(null);
24
+ setSuccessMessage(null);
25
+ const raw = xscpJson.trim();
26
+ if (!raw) {
27
+ setError("Paste or provide Xscp JSON first.");
28
+ setStatus("err");
29
+ return;
30
+ }
31
+ const validation = (0, copy_payload_1.validateCopyPayload)(raw);
32
+ if (!validation.valid) {
33
+ setError(validation.error);
34
+ setStatus("err");
35
+ return;
36
+ }
37
+ try {
38
+ const normalized = (0, copy_payload_1.isXscpPayload)(raw)
39
+ ? (0, copy_payload_1.normalizeXscp)(raw)
40
+ : (0, copy_payload_1.normalizeCopyPayload)(raw);
41
+ (0, copy_payload_1.writeClipboardXscp)(normalized);
42
+ setStatus("ok");
43
+ setSuccessMessage(customSuccess);
44
+ }
45
+ catch (e) {
46
+ setStatus("err");
47
+ setError(e instanceof Error ? e.message : "Invalid Xscp JSON. Copy failed.");
48
+ }
49
+ }, [customSuccess]);
50
+ return {
51
+ copyToWebflow,
52
+ status,
53
+ error,
54
+ successMessage,
55
+ reset,
56
+ };
57
+ }
@@ -0,0 +1,20 @@
1
+ export interface UsePasteFromWebflowReturn {
2
+ /** Extracted/formatted JSON after a successful paste. */
3
+ json: string;
4
+ /** Handler to attach to an element's onPaste (e.g. textarea). */
5
+ onPaste: (e: React.ClipboardEvent<HTMLTextAreaElement>) => void;
6
+ /** Error message when paste didn't contain Webflow JSON or parsing failed. */
7
+ error: string | null;
8
+ /** Copy current json to clipboard (plain text). */
9
+ copyJson: () => Promise<void>;
10
+ /** Whether the last copy succeeded. */
11
+ copied: boolean;
12
+ /** Clear json and error. */
13
+ reset: () => void;
14
+ }
15
+ /**
16
+ * Hook for a "Paste from Webflow" flow: user pastes in a focused area,
17
+ * you get the extracted Xscp JSON. Use with a textarea or contenteditable.
18
+ */
19
+ export declare function usePasteFromWebflow(): UsePasteFromWebflowReturn;
20
+ //# sourceMappingURL=use-paste-from-webflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-paste-from-webflow.d.ts","sourceRoot":"","sources":["../src/use-paste-from-webflow.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAC;IAChE,8EAA8E;IAC9E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAC;IAChB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,yBAAyB,CA4C/D"}
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ "use client";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.usePasteFromWebflow = usePasteFromWebflow;
5
+ const react_1 = require("react");
6
+ const copy_payload_1 = require("./copy-payload");
7
+ function getJsonFromPasteEvent(e) {
8
+ const json = e.clipboardData.getData("application/json");
9
+ const text = e.clipboardData.getData("text/plain");
10
+ const marker = "@webflow/XscpData";
11
+ if (json && json.includes(marker))
12
+ return json;
13
+ if (text && text.includes(marker))
14
+ return text;
15
+ if (json && json.trim().startsWith("{"))
16
+ return json;
17
+ if (text && text.trim().startsWith("{"))
18
+ return text;
19
+ return null;
20
+ }
21
+ function tryFormatJson(raw) {
22
+ const trimmed = raw.trim();
23
+ const start = trimmed.indexOf("{");
24
+ if (start === -1)
25
+ return raw;
26
+ const jsonStr = trimmed.slice(start);
27
+ try {
28
+ const parsed = JSON.parse(jsonStr);
29
+ return JSON.stringify(parsed, null, 2);
30
+ }
31
+ catch {
32
+ return raw;
33
+ }
34
+ }
35
+ const DEFAULT_PASTE_ERROR = "No Webflow JSON in clipboard. Copy a component in Webflow Designer (Cmd+C), then paste here (Cmd+V).";
36
+ /**
37
+ * Hook for a "Paste from Webflow" flow: user pastes in a focused area,
38
+ * you get the extracted Xscp JSON. Use with a textarea or contenteditable.
39
+ */
40
+ function usePasteFromWebflow() {
41
+ const [json, setJson] = (0, react_1.useState)("");
42
+ const [error, setError] = (0, react_1.useState)(null);
43
+ const [copied, setCopied] = (0, react_1.useState)(false);
44
+ const onPaste = (0, react_1.useCallback)((e) => {
45
+ const raw = getJsonFromPasteEvent(e);
46
+ if (!raw) {
47
+ setError(DEFAULT_PASTE_ERROR);
48
+ return;
49
+ }
50
+ e.preventDefault();
51
+ setError(null);
52
+ try {
53
+ const normalized = raw.includes("@webflow/XscpData")
54
+ ? (0, copy_payload_1.normalizeXscp)(raw)
55
+ : raw;
56
+ setJson(tryFormatJson(normalized));
57
+ }
58
+ catch (err) {
59
+ setError(err instanceof Error ? err.message : "Invalid JSON");
60
+ }
61
+ }, []);
62
+ const copyJson = (0, react_1.useCallback)(async () => {
63
+ if (!json)
64
+ return;
65
+ setCopied(false);
66
+ try {
67
+ await navigator.clipboard.writeText(json);
68
+ setCopied(true);
69
+ setTimeout(() => setCopied(false), 2000);
70
+ }
71
+ catch {
72
+ setError("Copy failed");
73
+ }
74
+ }, [json]);
75
+ const reset = (0, react_1.useCallback)(() => {
76
+ setJson("");
77
+ setError(null);
78
+ }, []);
79
+ return { json, onPaste, error, copyJson, copied, reset };
80
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Ensures Webflow XSCP JSON payload starts with "{" with no leading characters,
3
+ * and has no trailing non-whitespace (only the first complete JSON object).
4
+ * Webflow Designer paste handler requires the clipboard to start exactly with "{".
5
+ */
6
+ export declare function normalizeCopyPayload(payload: string): string;
7
+ export type ValidatePayloadResult = {
8
+ valid: true;
9
+ length: number;
10
+ } | {
11
+ valid: false;
12
+ error: string;
13
+ length: number;
14
+ tailPreview?: string;
15
+ };
16
+ /**
17
+ * Normalizes raw XSCP string: strips BOM, trims, removes junk before
18
+ * {"type":"@webflow/XscpData"}, removes any trailing non-whitespace after the
19
+ * root object, validates with JSON.parse. Throws if invalid.
20
+ * Use for Webflow paste (ClipboardItem application/json).
21
+ */
22
+ export declare function normalizeXscp(raw: string): string;
23
+ /**
24
+ * Copies XSCP to clipboard via the copy event so we can set both
25
+ * application/json and text/plain (Clipboard API write() rejects application/json).
26
+ * Triggers a programmatic copy and intercepts the copy event to set clipboardData.
27
+ */
28
+ export declare function writeClipboardXscp(xscp: string): void;
29
+ /**
30
+ * Returns true if the payload looks like Webflow XSCP JSON (use ClipboardItem path).
31
+ */
32
+ export declare function isXscpPayload(payload: string): boolean;
33
+ /**
34
+ * Extracts Webflow Xscp JSON from a paste (clipboard) event.
35
+ * Use in an onPaste handler: if this returns a string, preventDefault and use it.
36
+ * Returns null if the clipboard doesn't contain Webflow JSON.
37
+ */
38
+ export declare function getXscpFromPasteEvent(e: ClipboardEvent): string | null;
39
+ /**
40
+ * Validates that the payload is parseable JSON. Webflow shows "clipboard is empty"
41
+ * when the payload is invalid or truncated. Use this to warn the user before paste.
42
+ */
43
+ export declare function validateCopyPayload(payload: string): ValidatePayloadResult;
44
+ //# sourceMappingURL=copy-payload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-payload.d.ts","sourceRoot":"","sources":["../src/copy-payload.ts"],"names":[],"mappings":"AAkCA;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAK5D;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAI1E;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAyBrD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAEtD;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAUtE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CA8B1E"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Extracts the first complete JSON object from a string (from first "{" to
3
+ * its matching "}"). Used when payload has trailing junk that causes JSON.parse to fail.
4
+ */
5
+ function extractFirstJsonObject(s) {
6
+ const start = s.indexOf("{");
7
+ if (start === -1)
8
+ return s;
9
+ let depth = 0;
10
+ let inString = false;
11
+ let escape = false;
12
+ for (let i = start; i < s.length; i++) {
13
+ const c = s[i];
14
+ if (escape) {
15
+ escape = false;
16
+ continue;
17
+ }
18
+ if (inString) {
19
+ if (c === "\\")
20
+ escape = true;
21
+ else if (c === '"')
22
+ inString = false;
23
+ continue;
24
+ }
25
+ if (c === '"') {
26
+ inString = true;
27
+ continue;
28
+ }
29
+ if (c === "{")
30
+ depth++;
31
+ else if (c === "}") {
32
+ depth--;
33
+ if (depth === 0)
34
+ return s.slice(start, i + 1);
35
+ }
36
+ }
37
+ return s.slice(start);
38
+ }
39
+ /**
40
+ * Ensures Webflow XSCP JSON payload starts with "{" with no leading characters,
41
+ * and has no trailing non-whitespace (only the first complete JSON object).
42
+ * Webflow Designer paste handler requires the clipboard to start exactly with "{".
43
+ */
44
+ export function normalizeCopyPayload(payload) {
45
+ const trimmed = payload.trim();
46
+ const firstBrace = trimmed.indexOf("{");
47
+ if (firstBrace === -1)
48
+ return trimmed;
49
+ return extractFirstJsonObject(trimmed.slice(firstBrace));
50
+ }
51
+ const XSCP_PREFIX = '{"type":"@webflow/XscpData"';
52
+ /**
53
+ * Normalizes raw XSCP string: strips BOM, trims, removes junk before
54
+ * {"type":"@webflow/XscpData"}, removes any trailing non-whitespace after the
55
+ * root object, validates with JSON.parse. Throws if invalid.
56
+ * Use for Webflow paste (ClipboardItem application/json).
57
+ */
58
+ export function normalizeXscp(raw) {
59
+ let s = raw.replace(/^\uFEFF/, "").trimStart();
60
+ const idx = s.indexOf(XSCP_PREFIX);
61
+ if (idx > 0)
62
+ s = s.slice(idx);
63
+ s = extractFirstJsonObject(s);
64
+ JSON.parse(s); // validate – throws if invalid
65
+ return s;
66
+ }
67
+ /**
68
+ * Copies XSCP to clipboard via the copy event so we can set both
69
+ * application/json and text/plain (Clipboard API write() rejects application/json).
70
+ * Triggers a programmatic copy and intercepts the copy event to set clipboardData.
71
+ */
72
+ export function writeClipboardXscp(xscp) {
73
+ const json = normalizeXscp(xscp);
74
+ const onCopy = (e) => {
75
+ e.preventDefault();
76
+ e.clipboardData?.setData("application/json", json);
77
+ e.clipboardData?.setData("text/plain", json);
78
+ };
79
+ document.addEventListener("copy", onCopy, true);
80
+ try {
81
+ const textarea = document.createElement("textarea");
82
+ textarea.value = json;
83
+ textarea.style.position = "fixed";
84
+ textarea.style.left = "-9999px";
85
+ textarea.style.top = "0";
86
+ document.body.appendChild(textarea);
87
+ textarea.focus();
88
+ textarea.select();
89
+ document.execCommand("copy");
90
+ document.body.removeChild(textarea);
91
+ }
92
+ finally {
93
+ document.removeEventListener("copy", onCopy, true);
94
+ }
95
+ }
96
+ /**
97
+ * Returns true if the payload looks like Webflow XSCP JSON (use ClipboardItem path).
98
+ */
99
+ export function isXscpPayload(payload) {
100
+ return payload.includes(XSCP_PREFIX);
101
+ }
102
+ const XSCP_MARKER = "@webflow/XscpData";
103
+ /**
104
+ * Extracts Webflow Xscp JSON from a paste (clipboard) event.
105
+ * Use in an onPaste handler: if this returns a string, preventDefault and use it.
106
+ * Returns null if the clipboard doesn't contain Webflow JSON.
107
+ */
108
+ export function getXscpFromPasteEvent(e) {
109
+ const dt = e.clipboardData;
110
+ if (!dt)
111
+ return null;
112
+ const json = dt.getData("application/json");
113
+ const text = dt.getData("text/plain");
114
+ if (json && json.includes(XSCP_MARKER))
115
+ return json;
116
+ if (text && text.includes(XSCP_MARKER))
117
+ return text;
118
+ if (json && json.trim().startsWith("{"))
119
+ return json;
120
+ if (text && text.trim().startsWith("{"))
121
+ return text;
122
+ return null;
123
+ }
124
+ /**
125
+ * Validates that the payload is parseable JSON. Webflow shows "clipboard is empty"
126
+ * when the payload is invalid or truncated. Use this to warn the user before paste.
127
+ */
128
+ export function validateCopyPayload(payload) {
129
+ const normalized = normalizeCopyPayload(payload);
130
+ const length = normalized.length;
131
+ if (!normalized) {
132
+ return { valid: false, error: "Payload is empty.", length: 0 };
133
+ }
134
+ if (normalized[0] !== "{") {
135
+ return {
136
+ valid: false,
137
+ error: "Payload does not start with {. Webflow requires valid XSCP JSON.",
138
+ length,
139
+ };
140
+ }
141
+ try {
142
+ JSON.parse(normalized);
143
+ return { valid: true, length };
144
+ }
145
+ catch (e) {
146
+ const message = e instanceof Error ? e.message : String(e);
147
+ const tailPreview = length > 80 ? `…${normalized.slice(-80)}` : normalized;
148
+ return {
149
+ valid: false,
150
+ error: message,
151
+ length,
152
+ tailPreview,
153
+ };
154
+ }
155
+ }
@@ -0,0 +1,26 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ export interface CopyToWebflowButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
3
+ /** Xscp JSON string to copy when the button is clicked. */
4
+ xscpJson: string;
5
+ /** Optional success message (default: instruction to paste in Designer). */
6
+ successMessage?: string;
7
+ /** Label when idle. Default: "Copy to Webflow" */
8
+ label?: string;
9
+ /** Label when copied successfully. Default: "Copied" */
10
+ copiedLabel?: string;
11
+ /** Label when error. Default: "Copy failed" */
12
+ errorLabel?: string;
13
+ /** Optional class name for the button (e.g. Tailwind). */
14
+ className?: string;
15
+ /** Optional class for the status/error message container. */
16
+ messageClassName?: string;
17
+ /** Whether to show status/error message below the button. Default: true */
18
+ showMessage?: boolean;
19
+ }
20
+ /**
21
+ * Ready-to-use "Copy to Webflow" button. Pass your Xscp JSON; on click it copies
22
+ * to the clipboard in the format Webflow Designer expects (Cmd+V in Designer).
23
+ * Style with className or wrap in your own UI.
24
+ */
25
+ export declare function CopyToWebflowButton({ xscpJson, successMessage, label, copiedLabel, errorLabel, className, messageClassName, showMessage, children, ...buttonProps }: CopyToWebflowButtonProps): import("react/jsx-runtime").JSX.Element;
26
+ //# sourceMappingURL=copy-to-webflow-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-to-webflow-button.d.ts","sourceRoot":"","sources":["../src/copy-to-webflow-button.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAGlD,MAAM,WAAW,wBACf,SAAQ,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,cAAc,EACd,KAAoB,EACpB,WAAgC,EAChC,UAA8B,EAC9B,SAAS,EACT,gBAAgB,EAChB,WAAkB,EAClB,QAAQ,EACR,GAAG,WAAW,EACf,EAAE,wBAAwB,2CAqC1B"}
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCopyToWebflow } from "./use-copy-to-webflow.mjs";
4
+ const defaultLabel = "Copy to Webflow";
5
+ const defaultCopiedLabel = "Copied";
6
+ const defaultErrorLabel = "Copy failed";
7
+ /**
8
+ * Ready-to-use "Copy to Webflow" button. Pass your Xscp JSON; on click it copies
9
+ * to the clipboard in the format Webflow Designer expects (Cmd+V in Designer).
10
+ * Style with className or wrap in your own UI.
11
+ */
12
+ export function CopyToWebflowButton({ xscpJson, successMessage, label = defaultLabel, copiedLabel = defaultCopiedLabel, errorLabel = defaultErrorLabel, className, messageClassName, showMessage = true, children, ...buttonProps }) {
13
+ const { copyToWebflow, status, error, successMessage: msg, } = useCopyToWebflow({ successMessage });
14
+ const displayText = status === "ok"
15
+ ? copiedLabel
16
+ : status === "err"
17
+ ? errorLabel
18
+ : label;
19
+ return (_jsxs("div", { children: [_jsx("button", { type: "button", onClick: () => copyToWebflow(xscpJson), disabled: !xscpJson?.trim(), className: className, ...buttonProps, children: children ?? displayText }), showMessage && (error || msg) && (_jsx("div", { className: messageClassName, role: error ? "alert" : "status", "aria-live": "polite", children: error ?? msg }))] }));
20
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * webflow-clipboard — Copy to Webflow / Paste from Webflow as reusable primitives.
3
+ * Use the core API in any JS/TS app, or the React components for a ready "Copy to Webflow" button.
4
+ */
5
+ export { normalizeCopyPayload, normalizeXscp, writeClipboardXscp, isXscpPayload, validateCopyPayload, getXscpFromPasteEvent, type ValidatePayloadResult, } from "./copy-payload";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * webflow-clipboard — Copy to Webflow / Paste from Webflow as reusable primitives.
3
+ * Use the core API in any JS/TS app, or the React components for a ready "Copy to Webflow" button.
4
+ */
5
+ export { normalizeCopyPayload, normalizeXscp, writeClipboardXscp, isXscpPayload, validateCopyPayload, getXscpFromPasteEvent, } from "./copy-payload.mjs";
@@ -0,0 +1,7 @@
1
+ export { CopyToWebflowButton } from "./copy-to-webflow-button";
2
+ export type { CopyToWebflowButtonProps } from "./copy-to-webflow-button";
3
+ export { useCopyToWebflow } from "./use-copy-to-webflow";
4
+ export type { CopyToWebflowStatus, UseCopyToWebflowOptions, UseCopyToWebflowReturn, } from "./use-copy-to-webflow";
5
+ export { usePasteFromWebflow } from "./use-paste-from-webflow";
6
+ export type { UsePasteFromWebflowReturn } from "./use-paste-from-webflow";
7
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,YAAY,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -0,0 +1,4 @@
1
+ "use client";
2
+ export { CopyToWebflowButton } from "./copy-to-webflow-button.mjs";
3
+ export { useCopyToWebflow } from "./use-copy-to-webflow.mjs";
4
+ export { usePasteFromWebflow } from "./use-paste-from-webflow.mjs";
@@ -0,0 +1,23 @@
1
+ export type CopyToWebflowStatus = "idle" | "ok" | "err";
2
+ export interface UseCopyToWebflowOptions {
3
+ /** Message shown after successful copy. */
4
+ successMessage?: string;
5
+ }
6
+ export interface UseCopyToWebflowReturn {
7
+ /** Call with Xscp JSON string to copy to clipboard (Webflow-ready). */
8
+ copyToWebflow: (xscpJson: string) => void;
9
+ /** Current status: idle | ok | err */
10
+ status: CopyToWebflowStatus;
11
+ /** Error message when status is "err". */
12
+ error: string | null;
13
+ /** Success message when status is "ok". */
14
+ successMessage: string | null;
15
+ /** Reset status/error to idle. */
16
+ reset: () => void;
17
+ }
18
+ /**
19
+ * Hook to copy Xscp JSON to the clipboard in a format Webflow Designer accepts.
20
+ * Use this to build your own "Copy to Webflow" button or trigger copy from code.
21
+ */
22
+ export declare function useCopyToWebflow(options?: UseCopyToWebflowOptions): UseCopyToWebflowReturn;
23
+ //# sourceMappingURL=use-copy-to-webflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-copy-to-webflow.d.ts","sourceRoot":"","sources":["../src/use-copy-to-webflow.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC;AAKxD,MAAM,WAAW,uBAAuB;IACtC,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,uEAAuE;IACvE,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,sCAAsC;IACtC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,0CAA0C;IAC1C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,2CAA2C;IAC3C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kCAAkC;IAClC,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,sBAAsB,CAoDxB"}
@@ -0,0 +1,54 @@
1
+ "use client";
2
+ import { useState, useCallback } from "react";
3
+ import { writeClipboardXscp, normalizeXscp, normalizeCopyPayload, isXscpPayload, validateCopyPayload, } from "./copy-payload.mjs";
4
+ const DEFAULT_SUCCESS_MSG = "Copied. Open Webflow Designer, click canvas or Body, then press Cmd+V (Mac) or Ctrl+V (Windows).";
5
+ /**
6
+ * Hook to copy Xscp JSON to the clipboard in a format Webflow Designer accepts.
7
+ * Use this to build your own "Copy to Webflow" button or trigger copy from code.
8
+ */
9
+ export function useCopyToWebflow(options = {}) {
10
+ const { successMessage: customSuccess = DEFAULT_SUCCESS_MSG } = options;
11
+ const [status, setStatus] = useState("idle");
12
+ const [error, setError] = useState(null);
13
+ const [successMessage, setSuccessMessage] = useState(null);
14
+ const reset = useCallback(() => {
15
+ setStatus("idle");
16
+ setError(null);
17
+ setSuccessMessage(null);
18
+ }, []);
19
+ const copyToWebflow = useCallback((xscpJson) => {
20
+ setError(null);
21
+ setSuccessMessage(null);
22
+ const raw = xscpJson.trim();
23
+ if (!raw) {
24
+ setError("Paste or provide Xscp JSON first.");
25
+ setStatus("err");
26
+ return;
27
+ }
28
+ const validation = validateCopyPayload(raw);
29
+ if (!validation.valid) {
30
+ setError(validation.error);
31
+ setStatus("err");
32
+ return;
33
+ }
34
+ try {
35
+ const normalized = isXscpPayload(raw)
36
+ ? normalizeXscp(raw)
37
+ : normalizeCopyPayload(raw);
38
+ writeClipboardXscp(normalized);
39
+ setStatus("ok");
40
+ setSuccessMessage(customSuccess);
41
+ }
42
+ catch (e) {
43
+ setStatus("err");
44
+ setError(e instanceof Error ? e.message : "Invalid Xscp JSON. Copy failed.");
45
+ }
46
+ }, [customSuccess]);
47
+ return {
48
+ copyToWebflow,
49
+ status,
50
+ error,
51
+ successMessage,
52
+ reset,
53
+ };
54
+ }
@@ -0,0 +1,20 @@
1
+ export interface UsePasteFromWebflowReturn {
2
+ /** Extracted/formatted JSON after a successful paste. */
3
+ json: string;
4
+ /** Handler to attach to an element's onPaste (e.g. textarea). */
5
+ onPaste: (e: React.ClipboardEvent<HTMLTextAreaElement>) => void;
6
+ /** Error message when paste didn't contain Webflow JSON or parsing failed. */
7
+ error: string | null;
8
+ /** Copy current json to clipboard (plain text). */
9
+ copyJson: () => Promise<void>;
10
+ /** Whether the last copy succeeded. */
11
+ copied: boolean;
12
+ /** Clear json and error. */
13
+ reset: () => void;
14
+ }
15
+ /**
16
+ * Hook for a "Paste from Webflow" flow: user pastes in a focused area,
17
+ * you get the extracted Xscp JSON. Use with a textarea or contenteditable.
18
+ */
19
+ export declare function usePasteFromWebflow(): UsePasteFromWebflowReturn;
20
+ //# sourceMappingURL=use-paste-from-webflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-paste-from-webflow.d.ts","sourceRoot":"","sources":["../src/use-paste-from-webflow.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,yBAAyB;IACxC,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAC;IAChE,8EAA8E;IAC9E,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAC;IAChB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,yBAAyB,CA4C/D"}
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { useState, useCallback } from "react";
3
+ import { normalizeXscp } from "./copy-payload.mjs";
4
+ function getJsonFromPasteEvent(e) {
5
+ const json = e.clipboardData.getData("application/json");
6
+ const text = e.clipboardData.getData("text/plain");
7
+ const marker = "@webflow/XscpData";
8
+ if (json && json.includes(marker))
9
+ return json;
10
+ if (text && text.includes(marker))
11
+ return text;
12
+ if (json && json.trim().startsWith("{"))
13
+ return json;
14
+ if (text && text.trim().startsWith("{"))
15
+ return text;
16
+ return null;
17
+ }
18
+ function tryFormatJson(raw) {
19
+ const trimmed = raw.trim();
20
+ const start = trimmed.indexOf("{");
21
+ if (start === -1)
22
+ return raw;
23
+ const jsonStr = trimmed.slice(start);
24
+ try {
25
+ const parsed = JSON.parse(jsonStr);
26
+ return JSON.stringify(parsed, null, 2);
27
+ }
28
+ catch {
29
+ return raw;
30
+ }
31
+ }
32
+ const DEFAULT_PASTE_ERROR = "No Webflow JSON in clipboard. Copy a component in Webflow Designer (Cmd+C), then paste here (Cmd+V).";
33
+ /**
34
+ * Hook for a "Paste from Webflow" flow: user pastes in a focused area,
35
+ * you get the extracted Xscp JSON. Use with a textarea or contenteditable.
36
+ */
37
+ export function usePasteFromWebflow() {
38
+ const [json, setJson] = useState("");
39
+ const [error, setError] = useState(null);
40
+ const [copied, setCopied] = useState(false);
41
+ const onPaste = useCallback((e) => {
42
+ const raw = getJsonFromPasteEvent(e);
43
+ if (!raw) {
44
+ setError(DEFAULT_PASTE_ERROR);
45
+ return;
46
+ }
47
+ e.preventDefault();
48
+ setError(null);
49
+ try {
50
+ const normalized = raw.includes("@webflow/XscpData")
51
+ ? normalizeXscp(raw)
52
+ : raw;
53
+ setJson(tryFormatJson(normalized));
54
+ }
55
+ catch (err) {
56
+ setError(err instanceof Error ? err.message : "Invalid JSON");
57
+ }
58
+ }, []);
59
+ const copyJson = useCallback(async () => {
60
+ if (!json)
61
+ return;
62
+ setCopied(false);
63
+ try {
64
+ await navigator.clipboard.writeText(json);
65
+ setCopied(true);
66
+ setTimeout(() => setCopied(false), 2000);
67
+ }
68
+ catch {
69
+ setError("Copy failed");
70
+ }
71
+ }, [json]);
72
+ const reset = useCallback(() => {
73
+ setJson("");
74
+ setError(null);
75
+ }, []);
76
+ return { json, onPaste, error, copyJson, copied, reset };
77
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "webflow-clipboard",
3
+ "version": "1.0.0",
4
+ "description": "Copy to Webflow / Paste from Webflow — put Xscp JSON on the clipboard for Webflow Designer, or extract it from a paste event. Use as a script or as a React \"Copy to Webflow\" button.",
5
+ "license": "MIT",
6
+ "author": "Bejamas",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/bejamas/bejamas.git",
10
+ "directory": "packages/webflow-clipboard"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "sideEffects": false,
16
+ "main": "./dist/index.js",
17
+ "module": "./dist-esm/index.mjs",
18
+ "types": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist/**",
21
+ "dist-esm/**",
22
+ "README.md"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist-esm/index.mjs",
28
+ "require": "./dist/index.js"
29
+ },
30
+ "./react": {
31
+ "types": "./dist/react.d.ts",
32
+ "import": "./dist-esm/react.mjs",
33
+ "require": "./dist/react.js"
34
+ }
35
+ },
36
+ "scripts": {
37
+ "build": "rm -rf dist dist-esm && tsc -p tsconfig.build.cjs.json && tsc -p tsconfig.build.esm.json && node scripts/prepare-esm.cjs",
38
+ "typecheck": "tsc --noEmit",
39
+ "prepublishOnly": "npm run build",
40
+ "publish:npm": "npm run build && npm publish --access public"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=18.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "react": {
47
+ "optional": true
48
+ }
49
+ },
50
+ "devDependencies": {
51
+ "@types/react": "^19.0.0",
52
+ "typescript": "^5.6.0"
53
+ }
54
+ }