diff --git a/WebminCore.pm b/WebminCore.pm
index c5f45bb9d..8230710ab 100644
--- a/WebminCore.pm
+++ b/WebminCore.pm
@@ -23,7 +23,7 @@ $main::export_to_caller = 1;
# Add functions in web-lib-funcs.pl
# Generated with :
# grep -h "^sub " web-lib-funcs.pl ui-lib.pl | sed -e 's/sub //' | xargs echo
-@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_javascript tempname_dir tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month get_rbac_module_acl supports_rbac supports_ipv6 use_rbac_module_acl execute_command open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules);
+@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_javascript tempname_dir tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month get_rbac_module_acl supports_rbac supports_ipv6 use_rbac_module_acl execute_command open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules);
# Add global variables in web-lib.pl
push(@EXPORT, qw(&unique));
diff --git a/lang/en b/lang/en
index 2c4ba94b6..be0ba621c 100644
--- a/lang/en
+++ b/lang/en
@@ -344,6 +344,11 @@ ui_paging=Showing rows $1 to $2 of $3
ui_rowlabel=$2 in row $1 :
ui_filterbox=Type to filter..
ui_of=of
+ui_success=Success
+ui_info=Information
+ui_warning=Warning
+ui_error=Error
+ui_error_fatal=Fatal Error
header_statusmsg=$1 logged into $2 $3 on $4 ($5)
diff --git a/ui-lib.pl b/ui-lib.pl
index 3e8d802ac..2f7a6284b 100755
--- a/ui-lib.pl
+++ b/ui-lib.pl
@@ -3361,5 +3361,404 @@ return &theme_ui_brh() if (defined(&theme_ui_brh));
return "
\n";
}
+# ui_tag_start(tag, [attrs], [no-new-line])
+# Function to create an opening HTML tag with optional attributes.
+# Attributes are passed as a hash reference and its values are quote escaped.
+sub ui_tag_start
+{
+return theme_ui_tag_start(@_) if (defined(&theme_ui_tag_start));
+my ($tag, $attrs, $nnl) = @_;
+
+# Ensure every tag gets a proper marker class
+$attrs ||= {};
+$attrs->{'class'} = defined($attrs->{class})
+ ? "ui--$tag $attrs->{class}"
+ : "ui--$tag";
+
+# Start building tag
+my $rv = "<$tag";
+
+# Add attributes if provided
+if ($attrs && ref($attrs) eq 'HASH') {
+ foreach my $key (keys %$attrs) {
+ my $value = $attrs->{$key};
+ if (defined($value)) {
+ $value = "e_escape($value, '"');
+ $value =~ tr/\n\t//d;
+ $value =~ s/\s+/ /g;
+ $rv .= " $key=\"$value\"" ;
+ }
+ elsif ($key) {
+ $rv .= " $key";
+ }
+ }
+ }
+
+# Close the opening tag
+$rv .= $nnl ? ">" : ">\n";
+
+# Handle special case for tag
+$rv = "\n$rv" if ($tag eq 'html');
+
+return $rv;
+}
+
+# ui_tag_content(content)
+# Function to handle the content of an HTML tag.
+sub ui_tag_content
+{
+return theme_ui_tag_content(@_) if (defined(&theme_ui_tag_content));
+my ($content) = @_;
+my $rv;
+$rv = $content."\n" if (defined($content));
+return $rv;
+}
+
+# ui_tag_end(tag)
+# Function to create a closing HTML tag.
+sub ui_tag_end
+{
+return theme_ui_tag_end(@_) if (defined(&theme_ui_tag_end));
+my ($tag) = @_;
+return "$tag>\n";
+}
+
+# ui_tag(tag, [content], [attrs])
+# Function to create a complete HTML tag with optional content and attributes.
+sub ui_tag
+{
+return theme_ui_tag(@_) if (defined(&theme_ui_tag));
+my ($tag, $content, $attrs) = @_;
+my $rv = ui_tag_start($tag, $attrs, !defined($content));
+$rv .= ui_tag_content($content) if (defined($content));
+my %void_tags = map { $_ => 1 }
+ qw(
+ area base br col embed hr img input link
+ meta param source track wbr
+ );
+$rv .= ui_tag_end($tag) if (!exists($void_tags{lc($tag)}));
+return $rv;
+}
+
+# ui_alert(content, type, [icon], [attrs])
+# Generates an HTML alert with the specified content, type, and optional icon
+# and attributes.
+#
+# Parameters:
+# content - The main message/body of the alert
+# type - Alert style: "success", "info", "warning", "danger", "danger-fatal"
+# icon - Optional. Controls icon and title display:
+# - If undefined: uses default icon and title for the alert type
+# - If string: uses as icon class with default title
+# - If array ref [icon, title, no_break]:
+# - icon: Icon class
+# - title: Custom title (if undef, uses default for type)
+# - no_break: If 1, no line break after title (space instead)
+# attrs - Optional hash ref of additional HTML attributes for the alert div
+#
+# Examples:
+# ui_alert("Operation completed", "success");
+# ui_alert("Access denied", "danger", "fa-lock");
+# ui_alert("Settings changed", "info", ["fa-info-circle", "", 1]);
+# ui_alert("Server offline", "warning", undef, {id => "server-status"});
+sub ui_alert
+{
+return theme_ui_alert(@_) if (defined(&theme_ui_alert));
+my ($content, $type, $icon, $attrs) = @_;
+
+# Default alert type
+$type ||= 'info';
+
+# Default icons and titles based on type
+my %type_defaults = (
+ 'success' => {
+ 'icon' => 'fa-check-circle',
+ 'title' => $text{'ui_success'}
+ },
+ 'info' => {
+ 'icon' => 'fa-info-circle',
+ 'title' => $text{'ui_info'}
+ },
+ 'warning' => {
+ 'icon' => 'fa-exclamation-triangle',
+ 'title' => $text{'ui_warning'}
+ },
+ 'danger' => {
+ 'icon' => 'fa-bolt',
+ 'title' => $text{'ui_error'}
+ },
+ 'danger-fatal' => {
+ 'icon' => 'fa-exclamation-triangle',
+ 'title' => $text{'ui_error_fatal'}
+ }
+);
+
+my $use_icon = '';
+my $use_title = '';
+my $use_br = 1; # Default to using line break
+
+# Process icon parameter
+if (!defined($icon)) {
+ # Use defaults based on type
+ if ($type_defaults{$type}) {
+ $use_icon = $type_defaults{$type}{'icon'};
+ $use_title = $type_defaults{$type}{'title'};
+ }
+ }
+elsif (ref($icon)) {
+ # Array format [icon_class, title, no_br]
+ if (defined($icon->[0])) {
+ $use_icon = $icon->[0];
+ }
+ else {
+ $use_icon = $type_defaults{$type}{'icon'};
+ }
+
+ # Title: if provided use it, else use default for type
+ if (defined($icon->[1])) {
+ $use_title = $icon->[1];
+ }
+ elsif ($type_defaults{$type}) {
+ $use_title = $type_defaults{$type}{'title'};
+ }
+
+ # Line break flag: 1 = no break, anything else = break
+ $use_br = $icon->[2] ? 0 : 1 if (defined($icon->[2]));
+ }
+else {
+ # String format: just the icon class
+ $use_icon = $icon;
+ $use_title = $type_defaults{$type} ? $type_defaults{$type}{'title'} : '';
+ }
+
+# Prepare attributes for the alert div
+my $all_attrs = $attrs || {};
+
+# Add alert class
+my $class = 'alert';
+$class .= ' alert-'.$type if ($type);
+
+$all_attrs->{'class'} = $all_attrs->{'class'}
+ ? "$class $all_attrs->{'class'}"
+ : $class;
+
+# Build alert
+my $rv = '';
+
+# Start alert container
+$rv .= ui_tag_start('div', $all_attrs);
+
+# Add icon and title if either is available
+if ($use_icon || $use_title) {
+ # Add icon if available
+ if ($use_icon) {
+ $rv .= ui_tag('i', undef, { 'class' => "fa fa-fw $use_icon" });
+ $rv .= ' ';
+ }
+
+ # Add title if available
+ if ($use_title) {
+ $rv .= ui_tag('strong', $use_title);
+ }
+
+ # Add line break if needed
+ if ($use_br) {
+ $rv .= '
';
+ }
+ else {
+ $rv .= ' ';
+ }
+ $rv .= "\n";
+ }
+
+# Add main content
+$rv .= ui_tag_content($content);
+
+# Close alert container
+$rv .= ui_tag_end('div');
+
+return $rv;
+}
+
+# ui_button_icon(text, icon, [attrs])
+# Creates a button with an icon and text
+# Parameters:
+# text - The text/label for the button
+# icon - Icon class
+# attrs - Optional hash ref of additional HTML attributes
+#
+# Examples:
+# ui_button_icon("Save", "save", {class => "primary"})
+# ui_button_icon("Delete", "trash", {type => "submit", name => "delete"})
+sub ui_button_icon
+{
+return theme_ui_button_icon(@_) if (defined(&theme_ui_button_icon));
+my ($text, $icon, $attrs) = @_;
+
+# Default to button type if not specified
+my $all_attrs = $attrs || {};
+$all_attrs->{'type'} ||= 'button';
+
+# Button class
+my $btn_cls = $all_attrs->{'class'};
+$all_attrs->{'class'} = "btn " . ($btn_cls
+ ? ($btn_cls =~ /^btn-/ ? $btn_cls
+ : "btn-$btn_cls") : 'btn-default');
+
+# Build the button
+my $rv = ui_tag_start('button', $all_attrs);
+
+# Add icon if specified
+if ($icon) {
+ my $icon_class = "";
+
+ # Check if icon specifies a specific bundle (fa2)
+ if ($icon =~ /^fa2-/) {
+ $icon_class = "fa2 $icon";
+ }
+ # Check if it already has fa- prefix
+ elsif ($icon =~ /^fa-/) {
+ $icon_class = "fa $icon";
+ }
+ # Otherwise add the default fa- prefix
+ else {
+ $icon_class = "fa fa-$icon";
+ }
+ $rv .= ui_tag('i', undef, {'class' => $icon_class});
+ $rv .= " ";
+ }
+
+# Add text
+$rv .= ui_tag_content($text) if defined($text);
+
+# Close the button
+$rv .= ui_tag_end('button');
+
+return $rv;
+}
+
+# ui_link_icon(href, text, [icon], [attrs])
+# Creates a link with an icon and text
+# Parameters:
+# href - The URL for the link
+# text - The text/label for the link
+# icon - Icon class
+# attrs - Optional hash ref of additional HTML attributes
+#
+# Examples:
+# ui_link_icon("view.cgi?id=1", "View Details", "eye", {class => "primary"})
+# ui_link_icon("docs.html", "Documentation", "book", {target => "_blank"})
+sub ui_link_icon
+{
+return theme_ui_link_icon(@_) if (defined(&theme_ui_link_icon));
+my ($href, $text, $icon, $attrs) = @_;
+
+# Create attribute hash and set href
+my $all_attrs = $attrs || {};
+$all_attrs->{'href'} = $href if (defined($href));
+
+# Button class
+my $btn_cls = $all_attrs->{'class'};
+$all_attrs->{'class'} = "btn " . ($btn_cls
+ ? ($btn_cls =~ /^btn-/ ? $btn_cls
+ : "btn-$btn_cls") : 'btn-default');
+
+# Build the link
+my $rv = ui_tag_start('a', $all_attrs);
+
+# Add icon if specified
+if ($icon) {
+ my $icon_class = "";
+
+ # Check if icon specifies a specific bundle (fa2)
+ if ($icon =~ /^fa2-/) {
+ $icon_class = "fa2 $icon";
+ }
+ # Check if it already has fa- prefix
+ elsif ($icon =~ /^fa-/) {
+ $icon_class = "fa $icon";
+ }
+ # Otherwise add the default fa- prefix
+ else {
+ $icon_class = "fa fa-$icon";
+ }
+ $rv .= ui_tag('i', undef, {'class' => $icon_class});
+ $rv .= " ";
+ }
+
+# Add text
+$rv .= ui_tag_content($text) if (defined($text));
+
+# Close the link
+$rv .= ui_tag_end('a');
+
+return $rv;
+}
+
+# ui_icon(icon, [attrs])
+# Creates an icon element
+# Parameters:
+# icon - Icon class (with or without fa- prefix)
+# attrs - Optional hash ref of additional HTML attributes
+#
+# Examples:
+# ui_icon("search") # Standard icon
+# ui_icon("fa2-warning") # Extended icon set
+sub ui_icon
+{
+return theme_ui_icon(@_) if (defined(&theme_ui_icon));
+my ($icon, $attrs) = @_;
+
+return "" if (!defined($icon)) || $icon eq '';
+
+# Create attribute hash
+my $all_attrs = $attrs || {};
+
+# Process icon class
+my $icon_class = "";
+
+# Check if icon is in a specific bundle
+if ($icon =~ /^fa2-/) {
+ $icon_class = "fa2 $icon";
+ }
+elsif ($icon =~ /^fa-/) {
+ $icon_class = "fa $icon";
+ }
+else {
+ $icon_class = "fa fa-$icon";
+ }
+
+# Make icon always fixed width unless specified otherwise
+$icon_class .= " fa-fw" if ($all_attrs->{'class'} !~ /fa-dw/);
+
+# Add icon class to any existing classes
+if ($all_attrs->{'class'}) {
+ $all_attrs->{'class'} .= " $icon_class";
+} else {
+ $all_attrs->{'class'} = $icon_class;
+ }
+
+# Build the icon tag
+return ui_tag('i', undef, $all_attrs);
+}
+
+# ui_br([attrs])
+# Creates a line break element
+sub ui_br
+{
+return theme_ui_br(@_) if (defined(&theme_ui_br));
+my ($attrs) = @_;
+return ui_tag('br', undef, $attrs);
+}
+
+# ui_p(content, [attrs])
+# Creates a paragraph element with optional content
+sub ui_p
+{
+return theme_ui_p(@_) if (defined(&theme_ui_p));
+my ($content, $attrs) = @_;
+return ui_tag('p', $content, $attrs);
+}
+
1;