__('Create WDesignKit Widget', 'sprout-mcp'), 'description' => __( 'Creates a new widget for Elementor, Gutenberg, Gutenberg Core, or Bricks builder. Generates all required files (PHP, JSON config, CSS, JS) with proper boilerplate code. You can provide custom code for each file or use defaults.', 'sprout-mcp', ), 'category' => 'wdesignkit', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string', 'description' => 'Widget display name (e.g. "My Custom Card"). Max 64 characters. Only letters, numbers, spaces, hyphens, and underscores allowed.', 'minLength' => 1, 'maxLength' => 64, ], 'builder' => [ 'type' => 'string', 'description' => 'Target page builder.', 'enum' => ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'], ], 'description' => [ 'type' => 'string', 'description' => 'Short description of the widget. Used in the JSON config and as placeholder text in the default boilerplate. When omitted, a fallback description is auto-generated from the widget name.', ], 'category' => [ 'type' => 'string', 'description' => 'Widget category label shown in the WDesignKit library UI. Defaults to "WDesignKit". MUST match an existing category from wdesignkit/list-categories — arbitrary strings are rejected. Use manage-categories to add a new category first if needed.', 'maxLength' => 64, 'pattern' => '^[a-zA-Z0-9 \\-_.]+$', ], 'helper_link' => [ 'type' => 'string', 'description' => 'Optional documentation URL for this widget (e.g. "https://learn.wdesignkit.com/docs/my-widget/"). Stored in widget JSON metadata and surfaced in the WDesignKit UI as a help/docs link. Leave empty if no documentation page exists yet.', 'format' => 'uri', 'maxLength' => 512, ], 'icon' => [ 'type' => 'string', 'description' => 'Elementor icon class shown next to the widget name in the WDesignKit library (e.g. "eicon-info-box", "eicon-button", "eicon-gallery-grid"). Defaults to "eicon-code". See Elementor icon list for valid values.', 'maxLength' => 64, ], 'keywords' => [ 'type' => 'array', 'description' => 'Search keywords for the widget in the WDesignKit library (e.g. ["card", "info", "icon"]). Helps users find the widget by keyword.', 'items' => ['type' => 'string'], 'maxItems' => 20, ], 'php_code' => [ 'type' => 'string', 'description' => 'Complete PHP file content for the widget. REPLACES the entire generated boilerplate. Must include the opening [ 'type' => 'string', 'description' => 'Complete CSS file content for the widget. If omitted, an empty placeholder stylesheet is generated.', ], 'js_code' => [ 'type' => 'string', 'description' => 'JavaScript for the widget frontend. Executed inside the Elementor element_ready scope — use $scope[0].querySelector() to access DOM elements directly. No jQuery wrapper needed. Leave empty if the widget needs no JavaScript. Example: "let btn = $scope[0].querySelector(\'.wdkit-my-widget__btn\'); btn.addEventListener(\'click\', () => { ... });". For Gutenberg, this replaces the full block registration JS.', ], 'cdn_js' => [ 'type' => 'array', 'description' => 'CDN JavaScript URLs required by the widget (e.g. a GSAP or Swiper CDN link). Stored in Editor_Link so the builder loads them before running the widget JS. Example: ["https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"]. Leave empty when no third-party JS is needed.', 'items' => ['type' => 'string'], 'maxItems' => 10, ], 'cdn_css' => [ 'type' => 'array', 'description' => 'CDN CSS URLs required by the widget (e.g. a Swiper or Font Awesome CDN link). Stored in Editor_Link. Leave empty when no third-party CSS is needed.', 'items' => ['type' => 'string'], 'maxItems' => 10, ], 'version' => [ 'type' => 'string', 'description' => 'Widget version in semver format (e.g. "1.0.0"). Defaults to "1.0.0".', 'pattern' => '^\d+\.\d+\.\d+$', ], 'section_data' => [ 'type' => 'array', 'description' => 'Custom section_data for the widget JSON config — defines every control shown in the WDesignKit builder panel. When provided, used verbatim (overrides auto-parsing). When omitted (recommended), the ability auto-parses register_controls() from php_code and builds section_data automatically. Each element must be an object with "layout" and "style" arrays.', ], 'editor_html' => [ 'type' => 'string', 'description' => 'Custom HTML template for the WDesignKit builder canvas preview. Uses {{control_name}} placeholders that must match the section_data control names. If omitted, a default
template is generated using the standard title/description control hashes.',
],
],
'required' => ['name', 'builder'],
'additionalProperties' => false,
],
'output_schema' => [
'type' => 'object',
'properties' => [
'success' => ['type' => 'boolean'],
'message' => ['type' => 'string'],
'widget' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'builder' => ['type' => 'string'],
'folder' => ['type' => 'string'],
'widget_id' => ['type' => 'string', 'description' => '8-char unique hash. Use with activate-widget / deactivate-widget.'],
'files' => ['type' => 'array'],
'version' => ['type' => 'string'],
'css_class' => ['type' => 'string'],
],
],
],
],
'execute_callback' => 'wdesignkit_mcp_create_widget',
'permission_callback' => 'sprout_mcp_permission_callback',
'meta' => [
'show_in_rest' => true,
'mcp' => ['public' => true],
'annotations' => [
'instructions' => implode("\n", [
'## wdesignkit/create-widget — Usage Rules',
'',
'Creates a new WDesignKit widget with all required files.',
'',
'### ⚠ ALWAYS ask before calling — do NOT call this tool speculatively',
'Before invoking create-widget, confirm BOTH required fields with the user.',
'If either is missing or ambiguous from the user\'s message, ASK first.',
'',
'Ask about builder if not stated:',
' "Which page builder should this widget be for?"',
' Options: Elementor | Gutenberg | Gutenberg Core | Bricks',
'',
'Ask about name if ambiguous:',
' "What should the widget be named?"',
' (Only letters, numbers, spaces, hyphens, underscores — max 64 chars)',
'',
'Only call the tool once you have both confirmed answers.',
'Never guess the builder — a wrong builder creates files in the wrong directory.',
'',
'### Required inputs',
'- name: 1–64 chars. Letters, numbers, spaces, hyphens, underscores ONLY.',
' Special characters are REJECTED with an error — do not pass them. Names must be unique per builder.',
'- builder: one of elementor | gutenberg | gutenberg_core | bricks',
'',
'### Code inputs (php_code, css_code, js_code, cdn_js, cdn_css, section_data, editor_html)',
'- php_code / css_code / js_code: COMPLETE file replacements, not partial snippets.',
'- STRONGLY RECOMMENDED: omit php_code and let the ability generate correct boilerplate.',
' The auto-generated boilerplate includes all required methods and correct naming.',
'- If you provide php_code, the ability auto-corrects common mistakes (class name,',
' get_name, missing asset methods), but providing wrong code still risks errors.',
'',
'### js_code — Elementor widget JS rules',
'- Omit js_code entirely when the widget needs no JavaScript (no empty boilerplate written).',
'- When JS is needed, use $scope[0].querySelector() to access DOM elements.',
' Do NOT wrap in (function($){...})(jQuery) or $(window).on(elementor/...) —',
' WDesignKit executes widget JS in the element_ready scope automatically.',
'Correct pattern:',
' let btn = $scope[0].querySelector(\'.wdkit-my-widget__btn\');',
' btn.addEventListener(\'click\', function() { ... });',
'- cdn_js: array of CDN URLs for third-party JS libraries the widget depends on.',
' These are added to Editor_Link so the builder loads them before widget JS.',
' Example: ["https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"]',
'- cdn_css: same for CSS libraries (e.g. Swiper CSS CDN).',
'',
'### section_data — auto-generated from php_code; provide only to override',
'When you provide php_code, the ability AUTO-PARSES register_controls() and builds',
'matching section_data automatically — no manual JSON work needed.',
'Every add_control / add_responsive_control / add_group_control call is extracted',
'and mapped to the correct WDesignKit JSON control type. Normal/Hover tabs are',
'detected and wrapped in a normalhover container automatically.',
'',
'Control name format in auto-parsed JSON: {wdk_type}_{php_control_name}',
' e.g. PHP "button_text" (TEXT) → JSON "text_button_text"',
' e.g. PHP "button_border" (Group Border) → JSON "border_button_border"',
'',
'You can still provide section_data manually to override the auto-parsed result.',
'editor_html is also auto-generated from the section_data — button-like widgets',
'(with a URL control) get a full template; others get an template.',
'',
'### CRITICAL: Elementor PHP rules (applies when providing php_code)',
'⚠ WRONG CLASS NAME = PHP Fatal Error = entire site crashes.',
' Auto-corrected for ALL naming conventions (WDK_*, My_Widget_*, etc.) — but ONLY if',
' your php_code contains exactly one class declaration. Avoid providing php_code',
' with multiple class definitions.',
'',
'The PHP class name is derived from the generated filename (which includes the hash):',
' file_name = snake_case(widget_name) + "_" + widget_hash',
' class = "Wdkit_" + file_name',
'Example: name="Normal Button", hash="eb2ace"',
' → file: normal_button_eb2ace.php',
' → class: Wdkit_normal_button_eb2ace',
'',
'get_name() MUST return "wb-{widget_hash}" — e.g. return \'wb-eb2ace\';',
' ⚠ Duplicate get_name() across widgets causes silent conflicts.',
'',
'MUST include get_script_depends() — loads the widget .js via wp_upload_dir().',
'MUST include get_style_depends() — loads the widget .css via wp_upload_dir().',
' ⚠ Missing these = CSS/JS never loads on the frontend.',
'',
'Use "use Elementor\\..." imports at the top of the file.',
'',
'### Minimum required Elementor class structure',
'class Wdkit_{name_snake}_{hash} extends Widget_Base {',
' public function get_name() { return \'wb-{hash}\'; }',
' public function get_title() { return esc_html__(\'...\', \'wdesignkit\'); }',
' public function get_icon() { return \'eicon-code tpae-wdkit-logo\'; }',
' public function get_categories() { /* dynamic: reads wkit_builder option; fallback array(\'WDesignKit\') */ }',
' public function get_script_depends() { /* wp_enqueue_script(); return array(handle); */ }',
' public function get_style_depends() { /* wp_enqueue_style(); return array(handle); */ }',
' protected function register_controls() { /* ... */ }',
' protected function render() { /* echo HTML */ }',
'}',
'',
'### Bricks PHP rules',
'- Class must extend \\Bricks\\Element.',
'',
'### Gutenberg PHP rules',
'- Must call register_block_type() inside an init action.',
'',
'### Storage',
'Widgets stored in: wp-content/uploads/wdesignkit/{builder}/{folder}/',
'folder = {name-kebab-case}_{8-char-hash}',
'file = {name-snake-case}_{8-char-hash} (same hash, underscores)',
'',
'### Widget ID',
'The returned widget_id is the 8-char hash (e.g. "kmrd6l24").',
'Use widget_id with activate-widget / deactivate-widget.',
'Use folder with get-widget / update-widget.',
'The widget_id never changes after creation.',
'',
'### After creation',
'- Use wdesignkit/get-widget to read back the generated files.',
'- Use wdesignkit/update-widget to modify code.',
'- Does NOT require WDesignKit cloud login.',
]),
'readonly' => false,
'destructive' => false,
'idempotent' => false,
],
],
]);
function wdesignkit_mcp_create_widget(array $input): array {
if (!defined('WDKIT_BUILDER_PATH')) {
return [
'success' => false,
'message' => 'WDesignKit plugin is not active.',
];
}
// --- Validate name ---
$raw_name = $input['name'] ?? '';
if (!is_string($raw_name) || trim($raw_name) === '') {
return ['success' => false, 'message' => 'Widget name is required.'];
}
// Reject names containing invalid characters — do NOT silently strip.
// Schema declares: only letters, numbers, spaces, hyphens, and underscores allowed.
// Stripping silently mutates the stored name without the caller knowing (QA #86d2p0anc).
$name = sanitize_text_field($raw_name);
if (preg_match('/[^a-zA-Z0-9 \-_]/', $name)) {
return ['success' => false, 'message' => 'Widget name contains invalid characters. Only letters, numbers, spaces, hyphens, and underscores are allowed.'];
}
$name = trim($name);
if (empty($name)) {
return ['success' => false, 'message' => 'Widget name is required.'];
}
if (strlen($name) > 64) {
return ['success' => false, 'message' => 'Widget name is too long. Maximum 64 characters allowed.'];
}
// --- Validate builder ---
$builder = sanitize_text_field($input['builder'] ?? '');
if (empty($builder)) {
return ['success' => false, 'message' => 'Builder type is required.'];
}
$allowed_builders = ['elementor', 'gutenberg', 'gutenberg_core', 'bricks'];
if (!in_array($builder, $allowed_builders, true)) {
return [
'success' => false,
'message' => 'Invalid builder. Must be one of: ' . implode(', ', $allowed_builders),
];
}
// --- Validate optional fields ---
$description = sanitize_text_field($input['description'] ?? '');
// Auto-generate a fallback description from the widget name so the field is never empty
// (prevents the "description is empty" metadata warning in get-widget).
if ($description === '') {
$description = 'A ' . $name . ' widget';
}
$category = sanitize_text_field($input['category'] ?? 'WDesignKit');
$helper_link = esc_url_raw((string) ($input['helper_link'] ?? ''));
$icon = sanitize_text_field((string) ($input['icon'] ?? 'eicon-code'));
$keywords = array_values(array_filter(array_map('sanitize_text_field', (array) ($input['keywords'] ?? []))));
$version = sanitize_text_field($input['version'] ?? '1.0.0');
// Server-side validation for category (mirrors the input_schema pattern + maxLength constraints).
if (strlen($category) > 64) {
return ['success' => false, 'message' => 'Category name is too long. Maximum 64 characters.'];
}
if (!preg_match('/^[a-zA-Z0-9 \-_.]+$/', $category)) {
return ['success' => false, 'message' => 'Category name contains invalid characters. Use letters, numbers, spaces, hyphens, underscores, and dots only.'];
}
// Validate category against registered categories (fixes QA #86d2zp7bd).
// Reject any category not present in wkit_builder option — mirrors list-categories source of truth.
$valid_categories = get_option('wkit_builder', ['WDesignKit']);
if (!is_array($valid_categories)) {
$valid_categories = ['WDesignKit'];
}
if (!in_array('WDesignKit', $valid_categories, true)) {
array_unshift($valid_categories, 'WDesignKit');
}
$valid_lower = array_map('strtolower', $valid_categories);
if (!in_array(strtolower($category), $valid_lower, true)) {
return [
'success' => false,
'message' => "Category '{$category}' does not exist. Use wdesignkit/list-categories to see available categories.",
];
}
// Normalize to canonical casing as stored in the option (e.g. "wdesignkit" → "WDesignKit")
$canonical_index = array_search(strtolower($category), $valid_lower, true);
if ($canonical_index !== false) {
$category = $valid_categories[$canonical_index];
}
if (!preg_match('/^\d+\.\d+\.\d+$/', $version)) {
return ['success' => false, 'message' => 'Invalid version format. Use semver format like "1.0.0".'];
}
// --- Generate folder and file names matching WDesignKit convention ---
// folder: kebab-case (e.g. "my-custom-card")
// file: snake_case (e.g. "my_custom_card")
// Normalize ALL separators: folder is pure kebab-case, file is pure snake_case.
// Without this, a name like "My-Custom Widget" produces "my-custom_widget" (mixed).
$folder_base = sanitize_file_name(strtolower(str_replace([' ', '_'], '-', $name)));
$file_base = sanitize_file_name(strtolower(str_replace([' ', '-'], '_', $name)));
// 8-character unique hex hash — matches the real WDesignKit widget format (e.g. "kmrd6l24")
$widget_hash = substr(md5(uniqid('wdkit', true)), 0, 8);
$folder_name_full = $folder_base . '_' . $widget_hash;
$file_name_full = $file_base . '_' . $widget_hash;
// widget_id = JUST the hash (matches the real widget JSON format: "widget_id":"20hzyv26")
$widget_id = $widget_hash;
$builder_path = trailingslashit(WDKIT_BUILDER_PATH) . $builder;
$widget_dir = $builder_path . '/' . $folder_name_full;
// --- Check for duplicate widget names in same builder (case-insensitive) ---
if (is_dir($builder_path)) {
$existing_folders = @scandir($builder_path);
if (is_array($existing_folders)) {
foreach ($existing_folders as $ef) {
if (in_array($ef, ['.', '..'], true) || !is_dir($builder_path . '/' . $ef)) {
continue;
}
$ef_files = @scandir($builder_path . '/' . $ef);
if (!is_array($ef_files)) {
continue;
}
foreach ($ef_files as $ef_file) {
if (pathinfo($ef_file, PATHINFO_EXTENSION) !== 'json') {
continue;
}
$ef_json_raw = @file_get_contents($builder_path . '/' . $ef . '/' . $ef_file);
if ($ef_json_raw !== false) {
$ef_json = json_decode($ef_json_raw, true);
$ef_name = $ef_json['widget_data']['widgetdata']['name'] ?? '';
if (strtolower($ef_name) === strtolower($name)) {
return [
'success' => false,
'message' => "A widget named '{$name}' already exists for {$builder} (folder: {$ef}). Use a different name or delete the existing widget first.",
];
}
}
break; // Only one JSON per folder
}
}
}
}
if (is_dir($widget_dir)) {
return [
'success' => false,
'message' => "Widget folder already exists: {$builder}/{$folder_name_full}",
];
}
// --- Path traversal guard ---
$real_base = realpath(WDKIT_BUILDER_PATH);
if (!$real_base) {
return ['success' => false, 'message' => 'Widget base path does not exist. Check WDesignKit upload directory.'];
}
$real_base_norm = rtrim(str_replace('\\', '/', $real_base), '/');
$widget_dir_norm = rtrim(str_replace('\\', '/', $builder_path . '/' . $folder_name_full), '/');
if (strpos($widget_dir_norm, $real_base_norm . '/') !== 0) {
return ['success' => false, 'message' => 'Invalid widget path.'];
}
// --- Create directories ---
if (!is_dir($builder_path)) {
wp_mkdir_p($builder_path);
}
wp_mkdir_p($widget_dir);
// PHP class name: PascalCase (e.g. "My Custom Card" → "MyCustomCard")
$class_name = preg_replace('/[^a-zA-Z0-9]/', '', ucwords($name, ' -_'));
// --- Generate or use provided code ---
$php_code = (string) ($input['php_code'] ?? '');
if (empty($php_code)) {
$php_code = wdesignkit_mcp_generate_php(
$builder, $class_name, $name, $folder_name_full, $file_name_full,
$description, $category, $widget_hash, $version
);
} elseif ($builder === 'elementor') {
// Auto-correct common mistakes in AI-provided Elementor PHP:
// wrong class name, wrong get_name() return value, missing asset-enqueue methods.
$php_code = wdesignkit_mcp_fix_elementor_php(
$php_code, $file_name_full, $widget_hash, $folder_name_full, $version
);
}
$js_code = (string) ($input['js_code'] ?? '');
if (empty($js_code)) {
$js_code = wdesignkit_mcp_generate_js(
$builder, $class_name, $name, $folder_name_full, $file_name_full,
$description, $category, $widget_hash
);
}
// CDN links for Editor_Link — sanitise and normalise
$raw_cdn_js = $input['cdn_js'] ?? [];
$raw_cdn_css = $input['cdn_css'] ?? [];
if (is_string($raw_cdn_js)) { $raw_cdn_js = json_decode($raw_cdn_js, true) ?: []; }
if (is_string($raw_cdn_css)) { $raw_cdn_css = json_decode($raw_cdn_css, true) ?: []; }
$cdn_js = array_values(array_filter(array_map('esc_url_raw', (array) $raw_cdn_js)));
$cdn_css = array_values(array_filter(array_map('esc_url_raw', (array) $raw_cdn_css)));
// Editor_Link always needs at least one entry; use '' as placeholder when empty
if (empty($cdn_js)) { $cdn_js = ['']; }
if (empty($cdn_css)) { $cdn_css = ['']; }
// Compute the canonical widget CSS class once — used in both the generated CSS and the response.
// Avoids double '-widget' suffix when the name itself ends with 'Widget'
// (e.g. "My Card Widget" → slug "my-card-widget" → class "wdkit-my-card-widget", not "wdkit-my-card-widget-widget").
$_css_slug = sanitize_title($name);
$widget_css_class = 'wdkit-' . $_css_slug . (substr($_css_slug, -7) === '-widget' ? '' : '-widget');
// Always write a CSS file so the registered style handle resolves without 404.
// Default CSS stubs match the PHP render() output and section_data style selectors:
// .{widget_css_class} → widget container
// .{widget_css_class}__title → element
$css_code = (string) ($input['css_code'] ?? '');
if (empty($css_code)) {
$css_code = ".{$widget_css_class} {\n /* Widget container styles */\n}\n\n"
. ".{$widget_css_class}__title {\n /* Title styles */\n}\n\n"
. ".{$widget_css_class}__desc {\n /* Description styles */\n}\n";
}
// Custom section_data / editor_html — used verbatim when provided so the JSON
// panel controls perfectly mirror the caller's custom PHP register_controls().
// MCP frameworks may deliver array parameters as JSON strings rather than PHP arrays,
// so we accept both: a real PHP array OR a JSON-encoded string that decodes to an array.
$custom_section_data = null;
if (isset($input['section_data'])) {
$sd = $input['section_data'];
if (is_array($sd)) {
$custom_section_data = $sd;
} elseif (is_string($sd) && $sd !== '') {
$decoded = json_decode($sd, true);
if (is_array($decoded)) {
$custom_section_data = $decoded;
}
}
}
$custom_editor_html = isset($input['editor_html']) && is_string($input['editor_html']) && $input['editor_html'] !== ''
? $input['editor_html']
: null;
// Pass the PHP source for auto-parsing ONLY when the caller provided custom php_code.
// When using default boilerplate, generate_section_data() already covers it correctly.
$php_for_parse = !empty($input['php_code']) ? $php_code : '';
// JSON config — widget_id is JUST the 8-char hash (matching real widget format)
$json_config = wdesignkit_mcp_generate_json_config(
$name, $builder, $widget_id, $folder_name_full, $file_name_full,
$description, $version, $category, $helper_link, $icon, $keywords,
$css_code, $js_code, $widget_css_class, $custom_section_data, $custom_editor_html,
$php_for_parse, $cdn_js, $cdn_css
);
// --- Write files ---
$widget_file_base = $widget_dir . '/' . $file_name_full;
$write_errors = [];
// Use file_put_contents() directly — WP_Filesystem() requires admin credentials
// and silently fails in the REST/AJAX context that MCP abilities run under.
// The uploads directory is always writable by PHP, so this is safe.
if (file_put_contents($widget_file_base . '.php', $php_code) === false) {
$write_errors[] = 'PHP';
}
if (file_put_contents($widget_file_base . '.json', wp_json_encode($json_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === false) {
$write_errors[] = 'JSON';
}
if (file_put_contents($widget_file_base . '.js', $js_code) === false) {
$write_errors[] = 'JS';
}
if (file_put_contents($widget_file_base . '.css', $css_code) === false) {
$write_errors[] = 'CSS';
}
if (!empty($write_errors)) {
return [
'success' => false,
'message' => 'Failed to write files: ' . implode(', ', $write_errors) . '. Check directory permissions.',
];
}
return [
'success' => true,
'message' => "Widget '{$name}' created successfully for {$builder}.",
'widget' => [
'name' => $name,
'builder' => $builder,
'folder' => $folder_name_full,
'widget_id' => $widget_id, // 8-char hash — use with activate/deactivate-widget
'files' => [
$file_name_full . '.php',
$file_name_full . '.json',
$file_name_full . '.js',
$file_name_full . '.css',
],
'version' => $version,
'css_class' => $widget_css_class,
],
];
}
/**
* Dispatch to the correct PHP boilerplate generator for the given builder.
*/
function wdesignkit_mcp_generate_php(
string $builder,
string $class_name,
string $name,
string $folder_name,
string $file_name,
string $description,
string $category,
string $widget_hash,
string $version = '1.0.0'
): string {
switch ($builder) {
case 'elementor':
return wdesignkit_mcp_elementor_php(
$class_name, $name, $description, $category,
$file_name, $folder_name, $widget_hash, $version
);
case 'gutenberg':
case 'gutenberg_core':
return wdesignkit_mcp_gutenberg_php(
$builder, $class_name, $name, $folder_name, $file_name,
$description, $widget_hash, $version
);
case 'bricks':
return wdesignkit_mcp_bricks_php(
$class_name, $name, $folder_name, $description,
$category, $file_name, $widget_hash, $version
);
default:
return "start_controls_section(
'content_section',
array(
'label' => esc_html__( 'Content', 'wdesignkit' ),
'tab' => Controls_Manager::TAB_CONTENT,
)
);
\$this->add_control(
'title',
array(
'label' => esc_html__( 'Title', 'wdesignkit' ),
'type' => Controls_Manager::TEXT,
'default' => esc_html__( '{$safe_name}', 'wdesignkit' ),
'ai' => array( 'active' => false ),
)
);
\$this->add_control(
'description',
array(
'label' => esc_html__( 'Description', 'wdesignkit' ),
'type' => Controls_Manager::TEXTAREA,
'default' => esc_html__( '{$safe_desc}', 'wdesignkit' ),
'ai' => array( 'active' => false ),
)
);
\$this->end_controls_section();
// ── Style tab ─────────────────────────────────────────────────────────
// Section name "Widget Style" matches the JSON section_data style section.
// Controls mirror the JSON style controls (dimension, typography, color)
// using Elementor's {{WRAPPER}} selector to scope to this widget instance.
\$this->start_controls_section(
'style_section',
array(
'label' => esc_html__( 'Widget Style', 'wdesignkit' ),
'tab' => Controls_Manager::TAB_STYLE,
)
);
\$this->add_control(
'widget_padding',
array(
'label' => esc_html__( 'Widget Padding', 'wdesignkit' ),
'type' => Controls_Manager::DIMENSIONS,
'size_units' => array( 'px', 'em', 'rem', '%' ),
'selectors' => array(
'{{WRAPPER}} .{$widget_class}' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
),
)
);
\$this->add_group_control(
Group_Control_Typography::get_type(),
array(
'name' => 'title_typography',
'label' => esc_html__( 'Title Typography', 'wdesignkit' ),
'selector' => '{{WRAPPER}} .{$widget_class}__title',
)
);
\$this->add_control(
'title_color',
array(
'label' => esc_html__( 'Title Color', 'wdesignkit' ),
'type' => Controls_Manager::COLOR,
'default' => '#374151',
'selectors' => array(
'{{WRAPPER}} .{$widget_class}__title' => 'color: {{VALUE}};',
),
)
);
\$this->add_group_control(
Group_Control_Typography::get_type(),
array(
'name' => 'description_typography',
'label' => esc_html__( 'Description Typography', 'wdesignkit' ),
'selector' => '{{WRAPPER}} .{$widget_class}__desc',
)
);
\$this->add_control(
'description_color',
array(
'label' => esc_html__( 'Description Color', 'wdesignkit' ),
'type' => Controls_Manager::COLOR,
'default' => '#6b7280',
'selectors' => array(
'{{WRAPPER}} .{$widget_class}__desc' => 'color: {{VALUE}};',
),
)
);
\$this->end_controls_section();
}
/**
* Render widget output.
*
* @since 1.0.0
*/
protected function render() {
\$settings = \$this->get_settings_for_display();
\$title = ! empty( \$settings['title'] ) ? \$settings['title'] : '';
\$description = ! empty( \$settings['description'] ) ? \$settings['description'] : '';
\$output = '';
\$output .= ' template using the first text/textarea controls.
if ($editor_html === null) {
$editor_html = wdesignkit_mcp_generate_editor_html_from_section_data(
$widget_id, $widget_css_class, $file_name, $section_data
);
}
return [
'section_data' => $section_data,
'widget_data' => ['widgetdata' => $widgetdata],
'Editor_Link' => ['links' => [$link_entry]],
'Editor_data' => [
'html' => $editor_html,
'css' => $css_code,
'js' => $js_code,
],
];
}
/**
* Generate the "Need Help?" rawhtml control content — matches the static
* support-links HTML used in every real marketplace widget.
*
* @param string $helper_link Optional docs URL. If non-empty, a "Read Documentation"
* link pointing to it is inserted.
*/
function wdesignkit_mcp_help_html(string $helper_link = ''): string {
// Inline styles match the real marketplace widget format exactly (no vendor prefixes, no spaces in CSS values)
$ls = 'color:var(--e-a-color-txt-accent);text-decoration:none;border-color:transparent';
$bs = 'margin:auto;color:#fff;background:#8072fc;padding:10px 20px;border-radius:5px;'
. 'font-weight:400;font-size:13px;letter-spacing:0.4px;border:1px solid #8072fc;'
. 'box-shadow:0 2px 7px 0 rgba(0,0,0,0.3);';
$docs_html = '';
if (!empty($helper_link)) {
$safe_url = esc_url($helper_link);
$docs_html = ' description)
* - Falls back to the legacy hash-based template only when no text control found.
*/
function wdesignkit_mcp_generate_editor_html_from_section_data(
string $widget_id,
string $widget_css_class,
string $file_name,
array $section_data
): string {
// Flatten all layout-section controls into one list
$layout_controls = [];
foreach ($section_data as $group) {
foreach ($group['layout'] ?? [] as $section) {
foreach ($section['inner_sec'] ?? [] as $ctrl) {
$layout_controls[] = $ctrl;
}
}
}
// Identify key control types
$text_ctrl = null;
$url_ctrl = null;
$icon_ctrl = null;
$align_ctrl = null;
$icon_pos_ctrl = null;
$desc_ctrl = null;
foreach ($layout_controls as $ctrl) {
$t = $ctrl['type'] ?? '';
if ($t === 'text' && $text_ctrl === null) $text_ctrl = $ctrl;
if ($t === 'textarea' && $desc_ctrl === null) $desc_ctrl = $ctrl;
if ($t === 'url' && $url_ctrl === null) $url_ctrl = $ctrl;
if ($t === 'iconscontrol' && $icon_ctrl === null) $icon_ctrl = $ctrl;
if ($t === 'choose' && $align_ctrl === null) $align_ctrl = $ctrl;
if ($t === 'select' && $icon_pos_ctrl === null) $icon_pos_ctrl = $ctrl;
}
$align_class = $align_ctrl ? ' align-{{' . $align_ctrl['name'] . '}}' : '';
// ── Button-like widget ─────────────────────────────────────────────────────
if ($url_ctrl !== null) {
$icon_ph = $icon_ctrl ? "\n {{" . $icon_ctrl['name'] . "}}" : '';
$pos_class = $icon_pos_ctrl ? ' {{' . $icon_pos_ctrl['name'] . '}}' : '';
$text_ph = $text_ctrl
? "\n "
: '';
return '+
element
// .{widget_css_class}__desc →
+
title + optional