diff --git a/src/components/UrlBuilder/Readme.md b/src/components/UrlBuilder/Readme.md
new file mode 100644
index 00000000..c3fd7ccf
--- /dev/null
+++ b/src/components/UrlBuilder/Readme.md
@@ -0,0 +1,36 @@
+# URL Builder
+
+```jsx
+const handleChange = (value) => {
+ console.log('URL changed:', value);
+};
+
+
+```
\ No newline at end of file
diff --git a/src/components/UrlBuilder/URLPart.js b/src/components/UrlBuilder/URLPart.js
new file mode 100644
index 00000000..baaae5f6
--- /dev/null
+++ b/src/components/UrlBuilder/URLPart.js
@@ -0,0 +1,129 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import URLPartActions from './URLPartActions';
+import URLPartDropdown from './URLPartDropdown';
+
+const URLPart = ({
+ type,
+ value,
+ options,
+ isEditing,
+ isFocused,
+ isDisabled,
+ onEdit,
+ onChange,
+ onClear,
+ onPartClick,
+ onStartEditing,
+ onFinishEditing,
+ inputRef,
+ placeholder
+}) => {
+ const [customInput, setCustomInput] = useState('');
+ const isPlaceholder = !value || value === '';
+
+ const getDisplayValue = () => {
+ if (!value) {
+ return placeholder;
+ }
+ if (type === 'path') {
+ return value ? `/${value}` : placeholder;
+ }
+ return value;
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ handleCustomInput();
+ }
+ };
+
+ const handleCustomInput = () => {
+ if (customInput.trim()) {
+ onChange(type, customInput.trim());
+ setCustomInput('');
+ }
+ onFinishEditing();
+ };
+
+ const handleInputChange = (e) => {
+ const newValue = e.target.value;
+ setCustomInput(newValue);
+ onEdit(e, type);
+ };
+
+ const handleInputBlur = () => {
+ handleCustomInput();
+ };
+
+ return (
+
+ {isEditing && type !== 'method' ? (
+
e.stopPropagation()}
+ disabled={isDisabled}
+ />
+ ) : (
+
onPartClick(type, e)}
+ onDoubleClick={(e) => type !== 'method' && onStartEditing(e, type)}
+ >
+ {getDisplayValue()}
+
+ )}
+
+
+
+
+
+ );
+};
+
+URLPart.propTypes = {
+ type: PropTypes.string.isRequired,
+ value: PropTypes.string,
+ options: PropTypes.arrayOf(PropTypes.string),
+ isEditing: PropTypes.bool,
+ isFocused: PropTypes.bool,
+ isDisabled: PropTypes.bool,
+ onEdit: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ onPartClick: PropTypes.func.isRequired,
+ onStartEditing: PropTypes.func.isRequired,
+ onFinishEditing: PropTypes.func.isRequired,
+ inputRef: PropTypes.object,
+ placeholder: PropTypes.string.isRequired
+};
+
+export default URLPart;
\ No newline at end of file
diff --git a/src/components/UrlBuilder/URLPartActions.js b/src/components/UrlBuilder/URLPartActions.js
new file mode 100644
index 00000000..50b9ca08
--- /dev/null
+++ b/src/components/UrlBuilder/URLPartActions.js
@@ -0,0 +1,48 @@
+import React from "react";
+import PropTypes from "prop-types";
+import Icon from "../Icon";
+
+const URLPartActions = ({
+ type,
+ isEditing,
+ isPlaceholder,
+ isDisabled,
+ onEdit,
+ onClear,
+}) => {
+ if (isEditing || isPlaceholder || isDisabled || type === "method") {
+ return null;
+ }
+
+ return (
+
+
+
+
+ );
+};
+
+URLPartActions.propTypes = {
+ type: PropTypes.string.isRequired,
+ isEditing: PropTypes.bool,
+ isPlaceholder: PropTypes.bool,
+ isDisabled: PropTypes.bool,
+ onEdit: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+};
+
+export default URLPartActions;
diff --git a/src/components/UrlBuilder/URLPartDropdown.js b/src/components/UrlBuilder/URLPartDropdown.js
new file mode 100644
index 00000000..1dcc9ea7
--- /dev/null
+++ b/src/components/UrlBuilder/URLPartDropdown.js
@@ -0,0 +1,78 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Icon from '../Icon';
+
+const URLPartDropdown = ({
+ type,
+ value,
+ options,
+ isVisible,
+ onChange,
+ onStartEditing
+}) => {
+ if (!isVisible) {
+ return null;
+ }
+
+ const handleOptionSelect = (option) => {
+ onChange(type, option);
+ };
+
+ return (
+ e.stopPropagation()}
+ >
+
+ {options.length > 0 ? (
+ <>Select {type} or add custom>
+ ) : (
+ <>Type to customize>
+ )}
+
+ {type !== 'method' && (
+
onStartEditing(e, type)}
+ >
+
+ Add custom value
+
+ )}
+ {options.length > 0 && (
+ <>
+
handleOptionSelect('')}
+ >
+ Clear selection
+
+ {options.map(option => (
+
handleOptionSelect(option)}
+ >
+ {option}
+
+ ))}
+ >
+ )}
+
+ );
+};
+
+URLPartDropdown.propTypes = {
+ type: PropTypes.string.isRequired,
+ value: PropTypes.string,
+ options: PropTypes.arrayOf(PropTypes.string),
+ isVisible: PropTypes.bool,
+ onChange: PropTypes.func.isRequired,
+ onStartEditing: PropTypes.func.isRequired
+};
+
+export default URLPartDropdown;
\ No newline at end of file
diff --git a/src/components/UrlBuilder/index.js b/src/components/UrlBuilder/index.js
new file mode 100644
index 00000000..3c6f0e59
--- /dev/null
+++ b/src/components/UrlBuilder/index.js
@@ -0,0 +1,207 @@
+import React, { useState, useEffect, useRef, useMemo } from "react";
+import PropTypes from "prop-types";
+import URLPart from "./URLPart";
+import "./url-builder.css";
+
+const DEFAULT_OPTIONS = {
+ methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
+ hosts: [],
+ listenPaths: [],
+ endpoints: [],
+ paths: [],
+};
+
+const PLACEHOLDERS = {
+ method: "Select Method",
+ host: "Enter API URL",
+ listenPath: "Enter API version or base path",
+ endpoint: "Enter endpoint or resource path",
+ path: "Enter ID or parameter",
+};
+
+const URLBuilder = ({
+ initialValue = {},
+ options = {},
+ onChange,
+ disabled = false,
+ error,
+}) => {
+ const mergedOptions = useMemo(
+ () => ({
+ methods: options.methods || DEFAULT_OPTIONS.methods,
+ hosts: options.hosts || DEFAULT_OPTIONS.hosts,
+ listenPaths: options.listenPaths || DEFAULT_OPTIONS.listenPaths,
+ endpoints: options.endpoints || DEFAULT_OPTIONS.endpoints,
+ paths: options.paths || DEFAULT_OPTIONS.paths,
+ }),
+ [options]
+ );
+
+ const [selectedParts, setSelectedParts] = useState({
+ method: initialValue.method || "",
+ host: initialValue.host || "",
+ listenPath: initialValue.listenPath || "",
+ endpoint: initialValue.endpoint || "",
+ path: initialValue.path || "",
+ });
+
+ const [focusedPart, setFocusedPart] = useState(null);
+ const [editingPart, setEditingPart] = useState(null);
+ const inputRef = useRef(null);
+
+ useEffect(() => {
+ if (inputRef.current && editingPart) {
+ inputRef.current.focus();
+ }
+ }, [editingPart]);
+
+ useEffect(() => {
+ if (onChange) {
+ onChange(selectedParts);
+ }
+ }, [selectedParts, onChange]);
+
+ const handleEdit = (e, type) => {
+ e.stopPropagation();
+ const value = e.target.value;
+ setSelectedParts((prev) => ({
+ ...prev,
+ [type]: type === "path" ? value.replace(/^\//, "") : value,
+ }));
+ };
+
+ const startEditing = (e, type) => {
+ if (disabled) return;
+
+ e.stopPropagation();
+ setEditingPart(type);
+ setFocusedPart(null);
+ };
+
+ const handlePartClick = (type, e) => {
+ if (disabled) return;
+ e.stopPropagation();
+
+ if (editingPart === type) {
+ return;
+ }
+
+ setFocusedPart(type);
+ setEditingPart(null);
+ };
+
+ const finishEditing = () => {
+ setEditingPart(null);
+ };
+
+ const handlePartChange = (part, value) => {
+ const processedValue = part === "path" ? value.replace(/^\//, "") : value;
+ setSelectedParts((prev) => ({
+ ...prev,
+ [part]: processedValue,
+ }));
+ setFocusedPart(null);
+ setEditingPart(null);
+ };
+
+ const clearSelection = (e, type) => {
+ if (disabled) return;
+ e.stopPropagation();
+ setSelectedParts((prev) => ({
+ ...prev,
+ [type]: "",
+ }));
+ };
+
+ const urlParts = useMemo(
+ () => [
+ {
+ type: "method",
+ value: selectedParts.method,
+ options: mergedOptions.methods,
+ },
+ { type: "host", value: selectedParts.host, options: mergedOptions.hosts },
+ {
+ type: "listenPath",
+ value: selectedParts.listenPath,
+ options: mergedOptions.listenPaths,
+ },
+ {
+ type: "endpoint",
+ value: selectedParts.endpoint,
+ options: mergedOptions.endpoints,
+ },
+ { type: "path", value: selectedParts.path, options: mergedOptions.paths },
+ ],
+ [selectedParts, mergedOptions]
+ );
+
+ return (
+ {
+ setFocusedPart(null);
+ setEditingPart(null);
+ }}
+ >
+
+
+ {urlParts.map(({ type, value, options }) => (
+
+ ))}
+
+
+ {error &&
{error}
}
+
+ );
+};
+
+URLBuilder.propTypes = {
+ initialValue: PropTypes.shape({
+ method: PropTypes.string,
+ host: PropTypes.string,
+ listenPath: PropTypes.string,
+ endpoint: PropTypes.string,
+ path: PropTypes.string,
+ }),
+ options: PropTypes.shape({
+ methods: PropTypes.arrayOf(PropTypes.string),
+ hosts: PropTypes.arrayOf(PropTypes.string),
+ listenPaths: PropTypes.arrayOf(PropTypes.string),
+ endpoints: PropTypes.arrayOf(PropTypes.string),
+ paths: PropTypes.arrayOf(PropTypes.string),
+ }),
+ onChange: PropTypes.func,
+ disabled: PropTypes.bool,
+ error: PropTypes.string,
+};
+
+URLBuilder.defaultProps = {
+ initialValue: {},
+ options: {},
+ onChange: null,
+ disabled: false,
+ error: null,
+};
+
+export default React.memo(URLBuilder);
diff --git a/src/components/UrlBuilder/url-builder.css b/src/components/UrlBuilder/url-builder.css
new file mode 100644
index 00000000..359ae464
--- /dev/null
+++ b/src/components/UrlBuilder/url-builder.css
@@ -0,0 +1,305 @@
+.url-builder {
+ width: 100%;
+ margin: 0 auto;
+ position: relative;
+ font-family: var(--font-family);
+ color: var(--color-text-base);
+}
+
+.url-builder__container {
+ position: relative;
+ width: 100%;
+ padding: var(--spacing-sm);
+ background: white;
+ border: var(--general-border-width) solid var(--color-secondary-dark);
+ border-radius: var(--general-border-radius);
+}
+
+.url-builder__container--error {
+ border-color: var(--color-danger-base);
+}
+
+.url-builder__parts {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--spacing-xs);
+}
+
+.url-builder__part {
+ position: relative;
+ flex-shrink: 0;
+ border-radius: 8px;
+ transition: all 0.15s ease;
+}
+
+.url-builder__part--method {
+ display: flex;
+ align-items: center;
+ background: var(--color-secondary-base);
+ color: var(--color-text-base);
+ font-family: var(--font-family-semi-bold);
+ padding: 1px;
+ border-radius: 6px;
+ margin-right: var(--spacing-xs);
+ position: relative;
+}
+
+.url-builder__part--method::after {
+ content: '';
+ position: absolute;
+ right: -6px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 1px;
+ height: 16px;
+ background: var(--color-secondary-dark);
+}
+
+.url-builder__part--method .url-builder__value {
+ display: flex;
+ align-items: center;
+ background: var(--color-secondary-extra-light);
+ border-radius: 6px;
+ padding: 3px var(--spacing-sm);
+ min-width: 60px;
+ text-align: center;
+}
+
+.url-builder__part--method .url-builder__value:hover {
+ background: var(--color-secondary-light);
+}
+
+.url-builder__part--host {
+ background: var(--color-info-extra-light);
+ color: var(--color-info-dark);
+}
+
+.url-builder__part--listenpath {
+ background: var(--color-primary-extra-light);
+ color: var(--color-primary-dark);
+}
+
+.url-builder__part--endpoint {
+ background: var(--color-success-extra-light);
+ color: var(--color-success-dark);
+}
+
+.url-builder__part--path {
+ background: var(--color-warning-extra-light);
+ color: var(--color-warning-dark);
+}
+
+.url-builder__value {
+ padding: 6px 12px;
+ cursor: pointer;
+ border-radius: 8px;
+ font-size: var(--xs-font-size);
+ line-height: var(--sm-line-height);
+ transition: all 0.15s ease;
+ position: relative;
+}
+
+.url-builder__value:hover {
+ filter: brightness(0.95);
+}
+
+.url-builder__part--placeholder .url-builder__value {
+ color: var(--color-text-light);
+ font-style: italic;
+ border: 1px dashed var(--color-secondary-dark);
+ background-color: white;
+ opacity: 0.8;
+ padding: 6px 24px 6px 12px;
+}
+
+.url-builder__part--placeholder .url-builder__value::after {
+ content: '✎';
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 11px;
+ opacity: 0.5;
+ font-style: normal;
+}
+
+.url-builder__input {
+ padding: 6px 12px;
+ border: var(--general-border-width) solid var(--color-secondary-dark);
+ border-radius: 8px;
+ font-size: var(--xs-font-size);
+ line-height: var(--sm-line-height);
+ outline: none;
+ background: white;
+ color: var(--color-text-base);
+ min-width: 100px;
+ transition: all 0.15s ease;
+}
+
+.url-builder__input:focus {
+ border-color: var(--color-primary-base);
+ box-shadow: 0 0 0 2px var(--color-primary-extra-light);
+}
+
+.url-builder__input::placeholder {
+ color: var(--color-text-light);
+ font-style: italic;
+}
+
+.url-builder__part--focused {
+ box-shadow: 0 0 0 2px var(--color-primary-extra-light);
+}
+
+.url-builder__part--disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.url-builder__part--disabled .url-builder__value {
+ cursor: not-allowed;
+}
+
+.url-builder__actions {
+ position: absolute;
+ top: -3px;
+ right: -3px;
+ display: none;
+ gap: 3px;
+ z-index: 20;
+}
+
+.url-builder__part:hover .url-builder__actions {
+ display: flex;
+}
+
+.url-builder__button {
+ padding: var(--spacing-xs);
+ background: white;
+ border: var(--general-border-width) solid var(--color-secondary-dark);
+ border-radius: 50%;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-text-light);
+ width: 24px;
+ height: 24px;
+}
+
+.url-builder__button:hover {
+ background: var(--color-secondary-light);
+ border-color: var(--color-secondary-extra-dark);
+ color: var(--color-text-base);
+}
+
+.url-builder__button--clear:hover {
+ background: var(--color-danger-extra-light);
+ border-color: var(--color-danger-base);
+ color: var(--color-danger-base);
+}
+
+.url-builder__button .tyk-icon {
+ font-size: 12px;
+}
+
+.url-builder__dropdown {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ min-width: 150px;
+ background: white;
+ border: var(--general-border-width) solid var(--color-secondary-dark);
+ border-radius: 8px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ max-height: 250px;
+ overflow-y: auto;
+ animation: dropdownFadeIn 0.2s ease;
+}
+
+.url-builder__dropdown--method {
+ min-width: 100px;
+}
+
+@keyframes dropdownFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.url-builder__dropdown-header {
+ padding: 8px 12px;
+ font-size: var(--xs-font-size);
+ color: var(--color-text-light);
+ border-bottom: 1px solid var(--color-secondary-base);
+ font-style: italic;
+}
+
+.url-builder__dropdown-clear {
+ padding: 8px 12px;
+ cursor: pointer;
+ color: var(--color-danger-base);
+ border-bottom: 1px solid var(--color-secondary-base);
+ transition: all 0.15s ease;
+ font-size: var(--xs-font-size);
+}
+
+.url-builder__dropdown-clear:hover {
+ background: var(--color-danger-extra-light);
+}
+
+.url-builder__dropdown-item {
+ padding: 8px 12px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ font-size: var(--xs-font-size);
+}
+
+.url-builder__dropdown-item:hover {
+ background: var(--color-secondary-light);
+}
+
+.url-builder__dropdown-item--selected {
+ background: var(--color-primary-extra-light);
+ color: var(--color-primary-base);
+}
+
+.url-builder__dropdown-item--selected:hover {
+ background: var(--color-primary-light);
+}
+
+.url-builder__error {
+ margin-top: var(--spacing-xs);
+ padding: 0 var(--spacing-xs);
+ color: var(--color-danger-base);
+ font-size: var(--xs-font-size);
+ line-height: var(--sm-line-height);
+}
+
+.url-builder__dropdown-custom {
+ padding: 8px 12px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--color-primary-base);
+ border-bottom: 1px solid var(--color-secondary-base);
+ transition: all 0.15s ease;
+ font-size: var(--xs-font-size);
+}
+
+.url-builder__dropdown-custom:hover {
+ background: var(--color-primary-extra-light);
+}
+
+.url-builder__dropdown-icon {
+ font-size: 12px;
+}
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 431f43bb..9139b60d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -29,6 +29,7 @@ export { default as toast } from './components/Toast';
export { default as Tooltip } from './components/Tooltip';
export { default as Table } from './components/Table';
export { default as Stepper } from './components/Stepper';
+export { default as UrlBuilder } from './components/UrlBuilder';
// -- Layout
export { default as Column } from './layout/Column';