diff --git a/package-lock.json b/package-lock.json index 1140d69..e60123f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,51 +1,51 @@ { - "name": "react-starter-proj", + "name": "de-examples-tester", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "modules": true, "packages": { "": { - "name": "react-starter-proj", + "name": "de-examples-tester", "version": "1.0.0", "hasInstallScript": true, "dependencies": { - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@fontsource/inter": "^5.0.16", - "@monaco-editor/react": "^4.7.0", - "@mui/icons-material": "^6.1.9", - "@mui/material": "^6.1.9", - "@tanstack/react-query": "^5.81.2", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "acorn": "^8.12.1", - "acorn-jsx": "^5.3.2", - "acorn-typescript": "^1.4.13", - "acorn-walk": "^8.3.4", - "axios": "^1.6.2", - "dotenv": "^16.4.5", - "fs-extra": "^11.2.0", - "prismjs": "^1.29.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-resizable": "^3.0.5", - "source-map": "^0.7.4", - "sucrase": "^3.35.0", - "typedoc": "^0.25.12" + "@emotion/react": "11.13.5", + "@emotion/styled": "11.13.5", + "@fontsource/inter": "5.0.16", + "@monaco-editor/react": "4.7.0", + "@mui/icons-material": "6.1.9", + "@mui/material": "6.1.9", + "@tanstack/react-query": "5.81.2", + "@types/react": "18.2.48", + "@types/react-dom": "18.2.18", + "acorn": "8.15.0", + "acorn-jsx": "5.3.2", + "acorn-typescript": "1.4.13", + "acorn-walk": "8.3.4", + "axios": "1.9.0", + "dotenv": "16.4.5", + "fs-extra": "11.2.0", + "prismjs": "1.29.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-resizable": "3.0.5", + "source-map": "0.7.4", + "sucrase": "3.35.0", + "typedoc": "0.25.13" }, "devDependencies": { - "@types/node": "^22.13.5", - "@types/prismjs": "^1.26.3", - "@vitejs/plugin-react": "^4.0.0", - "@webflow/designer-extension-typings": "^2.0.25", - "@webflow/webflow-cli": "^1.8.6", - "@xatom/wf-app-hot-reload": "^1.0.5", - "concurrently": "^6.3.0", - "nodemon": "^2.0.22", - "prettier": "^3.2.5", - "typescript": "^5.3.3", - "vite": "^5.0.0" + "@types/node": "22.13.5", + "@types/prismjs": "1.26.3", + "@vitejs/plugin-react": "4.5.2", + "@webflow/designer-extension-typings": "^2.0.31", + "@webflow/webflow-cli": "1.8.6", + "@xatom/wf-app-hot-reload": "1.0.5", + "concurrently": "6.5.1", + "nodemon": "2.0.22", + "prettier": "3.2.5", + "typescript": "5.3.3", + "vite": "5.4.19" } }, "node_modules/@ampproject/remapping": { @@ -4450,9 +4450,9 @@ "dev": true }, "node_modules/@webflow/designer-extension-typings": { - "version": "2.0.25", - "resolved": "https://registry.npmjs.org/@webflow/designer-extension-typings/-/designer-extension-typings-2.0.25.tgz", - "integrity": "sha512-nehI7TnpWzwdTAa0ZceV/7TEeBkjN8JMARHQAyDZXQQm6HmQJaaJaXsUYm4WNpuFjNMMxMM21MH0muZ4tJDUZQ==", + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/@webflow/designer-extension-typings/-/designer-extension-typings-2.0.31.tgz", + "integrity": "sha512-TwjWWBvXTfR2cHtr+KNMdjmZgZdV2ONN61I092jo5Mc5b8aGubvwvfsGAxg66M0WGoBeD+EExTNF0MWJpV9QTQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 43860f1..6339c29 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "react-starter-proj", + "name": "de-examples-tester", "version": "1.0.0", "private": true, "type": "module", @@ -14,41 +14,41 @@ "postinstall": "node scripts/sync-typings.js" }, "devDependencies": { - "@types/node": "^22.13.5", - "@types/prismjs": "^1.26.3", - "@vitejs/plugin-react": "^4.0.0", - "@webflow/designer-extension-typings": "^2.0.25", - "@webflow/webflow-cli": "^1.8.6", - "@xatom/wf-app-hot-reload": "^1.0.5", - "concurrently": "^6.3.0", - "nodemon": "^2.0.22", - "prettier": "^3.2.5", - "typescript": "^5.3.3", - "vite": "^5.0.0" + "@types/node": "22.13.5", + "@types/prismjs": "1.26.3", + "@vitejs/plugin-react": "4.5.2", + "@webflow/designer-extension-typings": "^2.0.31", + "@webflow/webflow-cli": "1.8.6", + "@xatom/wf-app-hot-reload": "1.0.5", + "concurrently": "6.5.1", + "nodemon": "2.0.22", + "prettier": "3.2.5", + "typescript": "5.3.3", + "vite": "5.4.19" }, "dependencies": { - "@emotion/react": "^11.13.5", - "@emotion/styled": "^11.13.5", - "@fontsource/inter": "^5.0.16", - "@monaco-editor/react": "^4.7.0", - "@mui/icons-material": "^6.1.9", - "@mui/material": "^6.1.9", - "@tanstack/react-query": "^5.81.2", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "acorn": "^8.12.1", - "acorn-jsx": "^5.3.2", - "acorn-typescript": "^1.4.13", - "acorn-walk": "^8.3.4", - "axios": "^1.6.2", - "dotenv": "^16.4.5", - "fs-extra": "^11.2.0", - "prismjs": "^1.29.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-resizable": "^3.0.5", - "source-map": "^0.7.4", - "sucrase": "^3.35.0", - "typedoc": "^0.25.12" + "@emotion/react": "11.13.5", + "@emotion/styled": "11.13.5", + "@fontsource/inter": "5.0.16", + "@monaco-editor/react": "4.7.0", + "@mui/icons-material": "6.1.9", + "@mui/material": "6.1.9", + "@tanstack/react-query": "5.81.2", + "@types/react": "18.2.48", + "@types/react-dom": "18.2.18", + "acorn": "8.15.0", + "acorn-jsx": "5.3.2", + "acorn-typescript": "1.4.13", + "acorn-walk": "8.3.4", + "axios": "1.9.0", + "dotenv": "16.4.5", + "fs-extra": "11.2.0", + "prismjs": "1.29.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-resizable": "3.0.5", + "source-map": "0.7.4", + "sucrase": "3.35.0", + "typedoc": "0.25.13" } } diff --git a/src/designer-extension-typings/api.d.ts b/src/designer-extension-typings/api.d.ts index a9dd0e5..1f57d47 100644 --- a/src/designer-extension-typings/api.d.ts +++ b/src/designer-extension-typings/api.d.ts @@ -8,10 +8,20 @@ /// /// /// +/// +/// +/// /// /// -interface WebflowApi { +type AppModeName = 'design' | 'build' | 'preview' | 'edit' | 'comment'; + +interface AppModeChangeEvent { + mode: AppModeName | null; + appModes: {[key in AppMode]: boolean}; +} + +interface SharedApi { /** * Get metadata about the current Site. * @returns A Promise that resolves to a record containing information about the site that is open in the @@ -53,78 +63,64 @@ interface WebflowApi { }>; }>; /** - * Get the currently selected element in the Webflow Designer. - * @returns A promise that resolves to one of the following: - * - null: If no element is currently selected in the Designer - * - AnyElement: an object representing the selected element, which can be of any type. + * Renders the specified element to WHTML format. + * @param element - The element to render + * @returns A promise that resolves to an object containing the WHTML string and shortIdMap, or null * @example * ```ts * const selectedElement = await webflow.getSelectedElement(); * if (selectedElement) { - * // Handle the selected element - * } else { - * // No element is currently selected - * } - * ``` - */ - getSelectedElement(): Promise; - /** - * Sets the currently selected element in the Webflow Designer. - * @returns A promise that resolves to one of the following: - * - null: If no element is able to be currently selected in the Designer - * - AnyElement: an object representing the selected element, which can be of any type. - * @example - * ```ts - * await webflow.setSelectedElement(element); - * ``` - */ - setSelectedElement(element: AnyElement): Promise; - - /** - * Captures a screenshot of the specified element. - * @returns A promise that resolves to a base64 string representing the screenshot of the element. - * @example - * ```ts - * const selectedElement = await webflow.getSelectedElement(); - * if (selectedElement) { - * const screenshot = await webflow.getElementSnapshot(selectedElement); - * console.log('Screenshot:', screenshot); - * }else{ - * console.log('No element selected'); + * const result = await webflow.getWHTML(selectedElement); + * if (result) { + * console.log('WHTML:', result.whtml); + * console.log('Short ID Map:', result.shortIdMap); + * } * } * ``` */ - getElementSnapshot(element: AnyElement): Promise; + getWHTML?( + element: AnyElement + ): Promise}>; elementBuilder(elementPreset: ElementPreset): BuilderElement; - /** - * Get the current media query breakpoint ID. - * @returns A Promise that resolves to a BreakpointId which is a string representing the current media query - * breakpoint. A BreakpointId is one of 'tiny', 'small', 'medium', 'main', 'large', 'xl', 'xxl'. - * @example - * ```ts - * const breakpoint = await webflow.getMediaQuery(); - * console.log('Current Media Query:', breakpoint); - * ``` - */ - getMediaQuery(): Promise; /** - * Get the current pseudo mode. - * @returns A Promise that resolves to a PseudoStateKey which is a string representing the current pseudo mode. - * @example - * ```ts - * const pseudoMode = await webflow.getPseudoMode(); - * console.log('Current Pseudo Mode:', pseudoMode); - * ``` - */ - getPseudoMode(): Promise; + * Parse a WHTML string and insert the resulting element as a child of the anchor element. + * The newly created element will be appended to the end of the anchor's children. + * @param whtml - The WHTML string to parse into an element + * @param anchor - The parent element to append the parsed WHTML element to + * @param position - The position relative to the anchor element where the new element will be inserted. + * - 'before': Insert as a sibling before the anchor element + * - 'after': Insert as a sibling after the anchor element + * - 'append': Insert as the last child of the anchor element (default) + * - 'prepend': Insert as the first child of the anchor element + * - 'replace': Replace the anchor element with the new element + * @returns A Promise that resolves to the newly inserted AnyElement + * @example + * ```ts + * const whtml = '
  • Item 1
  • Item 2
'; + * const body = await allElements.find((el) => el.type === 'Body'); + * // Append as last child (default) + * const element = await webflow.insertElementFromWHTML(whtml, body); + * // Or insert before an existing element + * const existingElement = await webflow.getSelectedElement(); + * const newElement = await webflow.insertElementFromWHTML(whtml, existingElement, 'before'); + * // Or replace an existing element + * const replacedElement = await webflow.insertElementFromWHTML(whtml, existingElement, 'replace'); + * ``` + */ + insertElementFromWHTML?( + whtml: string, + anchor: AnyElement, + position?: 'before' | 'after' | 'append' | 'prepend' | 'replace' + ): Promise; /** * Create a component by promoting a Root Element. * @param name - The name of the component. - * @param rootElement - An Element that will become the Root Element of the Component. + * @param root - An Element that will become the Root Element of the Component. * @returns A Promise resolving to an object containing the newly created Component - with the id property. + * @deprecated Use `registerComponent(options, root)` instead to provide richer metadata. * @example * ```ts * const element = webflow.createDOM('div') @@ -138,7 +134,67 @@ interface WebflowApi { name: string, root: AnyElement | ElementPreset | Component ): Promise; - getComponentByName(string): Promise; + /** + * Create a blank component. + * @param options - Options for creating the blank component. + * @returns A Promise resolving to an object containing the newly created Component - with the id property. + * @example + * ```ts + * const component = await webflow.registerComponent({name: 'Hero Section'}) + * + * // With optional group and description + * const grouped = await webflow.registerComponent({ + * name: 'Hero Section', + * group: 'Sections', + * description: 'A hero section component', + * }) + * ``` + */ + registerComponent(options: ComponentOptions): Promise; + /** + * Duplicate an existing component. + * @param options - Options for the new component, including a required name. + * @param source - The existing Component to duplicate. + * @returns A Promise resolving to the newly created Component. + * @example + * ```ts + * const [original] = await webflow.getAllComponents() + * const copy = await webflow.registerComponent({name: 'Hero Copy'}, original) + * ``` + */ + registerComponent( + options: ComponentOptions, + source: Component + ): Promise; + /** + * Convert an element or element preset into a component. Equivalent to the + * "Convert selection" action in the Designer's "New component" menu. + * Elements do not need to be on the page. You can build the tree with + * `createDOM` first and pass it directly. + * + * When `root` is a canvas `AnyElement`, the source element is replaced + * in-place by a new component instance by default. Pass `replace: false` + * in `options` to skip this substitution and keep the original element. + * @param options - Options for the new component. `name` is required; + * `group`, `description`, and `replace` are optional. + * @param root - The element, element preset, or builder element that becomes the component root. + * @returns A Promise resolving to the newly created Component. + * @example + * ```ts + * // Convert a canvas element and replace it with a component instance (default) + * const el = await webflow.getSelectedElement() + * const card = await webflow.registerComponent({name: 'Card', group: 'UI'}, el) + * + * // Convert without replacing the original element in the canvas + * const card2 = await webflow.registerComponent( + * {name: 'Card 2', replace: false}, + * el + * ) + */ + registerComponent( + options: ComponentOptions, + root: AnyElement | ElementPreset | BuilderElement + ): Promise; /** * Delete a component from the Designer. If there are any instances of the Component within the site, they will * be converted to regular Elements. @@ -170,69 +226,75 @@ interface WebflowApi { */ getAllComponents(): Promise>; /** - * Retrieve a component based on its name and optionally its group. - * Component instance. - * @returns A Promise resolving to the component + * Search site components with optional fuzzy filtering. + * Returns a flat array of {@link ComponentSearchResult} objects in the same order as the + * Components panel (insertion order). When `options.q` is provided, results are filtered + * using FlexSearch (`tokenize: 'full'`) — the same algorithm used by the Components panel. + * + * @param options - Optional search options. + * @param options.q - Search query string. Omit or leave empty to return all components. + * @returns A Promise resolving to an array of {@link ComponentSearchResult} objects. + * * @example * ```ts - * // Fetch a component by name only - * const heroSection = await webflow.getComponentByName('Hero'); - * console.log(heroSection.id); + * // Get all components + * const all = await webflow.searchComponents(); * - * // Fetch a component scoped to a group - * const marketingHero = await webflow.getComponentByName('Marketing', 'Hero'); - * console.log(marketingHero.id); + * // Filter by name + * const heroes = await webflow.searchComponents({ q: 'Hero' }); + * heroes.forEach(c => { + * console.log(c.name, c.instances, c.canEdit, c.library); + * }); * ``` */ - getComponentByName(a: string, b?: string): Promise; + searchComponents( + options?: SearchComponentsOptions + ): Promise; /** - * Retrieves the component that is currently being edited. - * @returns A Promise that resolves to the current component, or null if no component is currently being edited. + * Returns a component reference when the user is editing in-context or on the component canvas, or null if no component is being edited. + * @returns A Promise that resolves to a Component reference or null. * @example * ```ts * const component = await webflow.getCurrentComponent(); * if (component) { * const name = await component.getName(); - * console.log(`Currently editing component: ${name}`); - * } else { - * console.log('Not currently editing a component.'); + * console.log(`Currently editing: ${name}`); * } * ``` */ getCurrentComponent(): Promise; /** - * Searches for Components by name - * @returns A Promise that resolves to an array or objects with information about matching Components, not the `Component` objects themselves. + * Get a Component by its unique identifier. + * @param id - The unique identifier of the component. + * @returns A Promise that resolves to the Component with the given id. * @example * ```ts - * const heroes = await webflow.searchComponents({ q: 'Hero' }); - * console.log(heroes); + * const componentId = '4a669354-353a-97eb-795c-4471b406e043'; + * const component = await webflow.getComponent(componentId); * ``` - */ - searchComponents(options: SearchComponentsOptions?): Promise> + */ + getComponent(id: ComponentId): Promise; /** - * Gets the number of instances of a component. - * @returns A Promise that resolves to the number of instances of the component across the entire site. + * Get a Component by its display name. Only returns native site components. + * Throws if the matching component is a code component. + * @param name - The display name of the component. * @example * ```ts - * // Audit component usage across the site - * const components = await webflow.getAllComponents(); - * for (const component of components) { - * const name = await component.getName(); - * const count = await component.getInstanceCount(); - * console.log(`${name}: ${count} instances`); - * } - * // Guard against removing a component that's still in use - * const hero = components[0]; - * const instanceCount = await hero.getInstanceCount(); - * if (instanceCount > 0) { - * console.log(`Cannot safely remove — ${instanceCount} instances exist`); - * } else { - * await webflow.unregisterComponent(hero); - * } + * const component = await webflow.getComponentByName('Hero'); + * ``` + */ + getComponentByName(name: string): Promise; + /** + * Get a Component by its group and display name. Only returns native site components. + * Throws if the matching component is a code component. + * @param group - The group name the component belongs to. + * @param name - The display name of the component. + * @example + * ```ts + * const component = await webflow.getComponentByName('Marketing', 'Hero'); * ``` */ - getInstanceCount(): Promise; + getComponentByName(group: string, name: string): Promise; /** * Focus the designer on a Component. When a component is in focus, all Globals pertain specifically to that * Component, not the entire Site. @@ -244,46 +306,48 @@ interface WebflowApi { * await webflow.enterComponent(heroComponentInstance); * ``` */ + enterComponent(instance: ComponentElement): Promise; /** - * Open a Component's canvas or a page for editing in the Designer. - * @param target - A Component, ComponentInstance, or page object - * @returns A Promise that resolves when the canvas or page switch is successful. + * Return to the broader context of the entire site or page. + * @returns A Promise that resolves when the page switch is successful. * @example * ```ts - * `// Open a Component canvas by ID - * await webflow.openCanvas({ componentId: 'component-id' }); - * - * // Open a Component canvas by Component reference - * const components = await webflow.getAllComponents(); - * const hero = components[0]; - * await webflow.openCanvas(hero); - * - * // Open a Component canvas via an instance reference - * const selectedElement = await webflow.getSelectedElement(); - * if (selectedElement?.type === 'ComponentInstance') { - * await webflow.openCanvas(selectedElement as ComponentElement); - * } - * - * // Navigate to a page by ID - * await webflow.openCanvas({ pageId: 'page-id' }); + * await webflow.exitComponent(); + * ``` + */ + exitComponent(): Promise; + /** + * Navigate the Designer to a component canvas or page. + * @param options - An object with either pageId or componentId. + * @returns A Promise that resolves when the navigation is complete. + * @example + * ```ts + * // Open a component canvas by component id + * await webflow.openCanvas({componentId: '4a669354-353a-97eb-795c-4471b406e043'}); * - * // Navigate to a page by reference (equivalent to webflow.switchPage) - * const pagesAndFolders = await webflow.getAllPagesAndFolders(); - * const pages = pagesAndFolders?.filter((i): i is Page => i.type === 'Page'); - * await webflow.openCanvas(pages[0]); + * // Open a component canvas by page id + * await webflow.openCanvas({pageId: '123'}); * ``` */ - openCanvas() - enterComponent(instance: ComponentElement): Promise; + openCanvas( + options: OpenCanvasByComponentId | OpenCanvasByPageId + ): Promise; /** - * Return to the broader context of the entire site or page. - * @returns A Promise that resolves when the page switch is successful. + * Navigate the Designer to a component canvas or page using a reference. + * @param reference - A Component, ComponentElement, or Page reference. + * @returns A Promise that resolves when the navigation is complete. * @example * ```ts - * await webflow.exitComponent(); + * // Open a component canvas by component + * const heroComponent = await webflow.getComponent('4a669354-353a-97eb-795c-4471b406e043'); + * await webflow.openCanvas(heroComponent); + * + * // Open a component canvas by page + * const myPage = await webflow.getPage('123'); + * await webflow.openCanvas(myPage); * ``` */ - exitComponent(): Promise; + openCanvas(reference: Component | ComponentElement | Page): Promise; /** * Get Root element. When the designer is focused or "entered" into a Component, this method will get the * outermost element in the Component. @@ -321,7 +385,7 @@ interface WebflowApi { /** * Creates a new style with the provided name. * @param name - The name for the new style - * @param opts - Options for the new style. An object containing the following properties: + * @param options - Options for the new style. An object containing the following properties: * - parent: A Style object representing the parent style block. Used for creating a combo class. * @returns a Promise that resolves to the Style object representing the newly created style. * @example @@ -334,7 +398,6 @@ interface WebflowApi { * ``` */ createStyle(name: string, options?: {parent?: Style}): Promise