v2.2
MENU ANIMATIONS
UI SURECART
BUTTONS
<!DOCTYPE html>
<html lang="en">
<head>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Menu Island Configurator - BricksFusion</title>
<style>
:root {
--background: #000;
--card-bg: #1e1e1e;
--card-bg-hover: #252525;
--text-primary: #f2f2f7;
--text-secondary: #8e8e93;
--accent: #ef6013;
--accent-hover: #c64c0c;
--border: #2c2c2e;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--track: #2c2c2e;
--thumb: #ef6013;
--card-radius: 16px;
--input-radius: 8px;
--button-radius: 12px;
--transition: all 0.25s ease;
--font: 'Inter', BlinkMacSystemFont, "San Francisco", "Helvetica Neue", Helvetica, Arial, sans-serif;
--action-bar-height: 70px;
--success: #28a745;
--warning: #ffc107;
--danger: #dc3545;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font);
background-color: var(--background);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding-bottom: var(--action-bar-height);
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--action-bar-height);
background: linear-gradient(145deg, #1a1a1a, #0f0f0f);
border-top: 1px solid var(--border);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 1.5rem;
gap: 1rem;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.breadcrumb-item {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 500;
text-decoration: none;
transition: var(--transition);
padding: 0.5rem 0.75rem;
border-radius: 6px;
}
.breadcrumb-item:hover {
color: var(--text-primary);
background-color: rgba(255, 255, 255, 0.05);
}
.breadcrumb-item.active {
color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.breadcrumb-separator {
color: var(--text-secondary);
font-size: var(--text-xs);
opacity: 0.5;
}
.action-buttons {
display: flex;
align-items: center;
gap: 0.75rem;
}
.action-btn {
padding: 0.6rem 1rem;
background-color: var(--card-bg);
color: var(--text-primary);
font-family: var(--font);
font-size: var(--text-xs);
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--button-radius);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
white-space: nowrap;
}
.action-btn:hover {
background-color: var(--card-bg-hover);
border-color: var(--accent);
transform: translateY(-1px);
}
.action-btn.primary {
background: linear-gradient(90deg, var(--accent), #ff8c51);
border-color: var(--accent);
color: white;
}
.action-btn.primary:hover {
background: linear-gradient(90deg, var(--accent-hover), #e67a3f);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 96, 19, 0.3);
}
.data-attribute-display {
background-color: rgba(50, 50, 50, 0.8);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.5rem 0.75rem;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
cursor: pointer;
transition: var(--transition);
user-select: all;
}
.data-attribute-display:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
}
.container {
max-width: 100%;
margin: 0 auto;
padding: 2rem 1.5rem;
}
.page-header {
text-align: center;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
background: linear-gradient(90deg, var(--accent), #ff8c51);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.page-subtitle {
font-size: var(--text-s);
color: var(--text-secondary);
font-weight: 500;
}
.instructions-toggle {
margin-bottom: 2rem;
}
.instructions-card {
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
transition: var(--transition);
}
.instructions-header {
padding: 1rem 1.5rem;
cursor: pointer;
transition: var(--transition);
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid transparent;
}
.instructions-header:hover {
background-color: var(--card-bg-hover);
}
.instructions-card.expanded .instructions-header {
border-bottom-color: var(--border);
}
.instructions-title {
font-size: var(--text-s);
font-weight: 600;
}
.toggle-icon {
font-size: 1.2em;
transition: transform 0.3s ease;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.instructions-content {
padding: 0 1.5rem;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.instructions-content.show {
max-height: 500px;
padding: 1.5rem;
}
.instructions-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.how-to-use ol {
padding-left: 1.5rem;
}
.how-to-use li {
margin-bottom: 0.75rem;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.6;
}
.how-to-use strong {
color: var(--text-primary);
font-weight: 600;
}
.how-to-use code {
background-color: rgba(50, 50, 50, 0.5);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
color: #ff8c51;
}
.content {
display: grid;
grid-template-columns: 1fr 500px;
gap: 2rem;
align-items: start;
}
.preview-section {
position: sticky;
top: 2rem;
}
.controls-section {
max-width: 500px;
}
.card {
background-color: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
}
.preview-container {
height: 400px;
width: 100%;
position: relative;
overflow: hidden;
border-radius: var(--card-radius);
background-color: #1a1a1a;
border: 1px solid var(--border);
box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: center;
}
.preview-controls {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 10;
}
.preview-btn {
padding: 0.5rem;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
backdrop-filter: blur(5px);
}
.preview-btn:hover {
background-color: var(--accent);
border-color: var(--accent);
}
.preview-btn svg {
width: 18px;
height: 18px;
stroke: currentColor;
}
.background-selector-wrapper {
position: relative;
display: inline-block;
}
.background-selector-btn {
position: relative;
}
.background-selector-btn:hover {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
box-shadow: 0 0 8px rgba(239, 96, 19, 0.3);
}
.hidden-color-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 1;
}
.card-heading {
padding: 1rem 1.5rem;
font-size: var(--text-s);
font-weight: 600;
border-bottom: 1px solid var(--border);
letter-spacing: 0.3px;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions {
display: flex;
gap: 0.5rem;
}
.card-action-btn {
padding: 0.4rem 0.8rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: var(--text-xs);
transition: var(--transition);
}
.card-action-btn:hover {
color: var(--text-primary);
border-color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.card-content {
padding: 1.5rem;
}
.control-group {
margin-bottom: 1.5rem;
position: relative;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.label-text {
font-size: var(--text-xs);
font-weight: 500;
letter-spacing: 0.2px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.help-tooltip {
cursor: help;
opacity: 0.7;
transition: var(--transition);
}
.help-tooltip:hover {
opacity: 1;
color: var(--accent);
}
.value-display {
display: flex;
align-items: center;
gap: 0.5rem;
}
.value-text {
font-size: var(--text-xs);
color: var(--text-secondary);
background-color: rgba(50, 50, 50, 0.5);
padding: 2px 8px;
border-radius: 4px;
min-width: 45px;
text-align: center;
}
.reset-btn {
padding: 0.2rem 0.4rem;
background-color: transparent;
color: var(--text-secondary);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
font-size: 10px;
transition: var(--transition);
}
.reset-btn:hover {
color: var(--danger);
border-color: var(--danger);
background-color: rgba(220, 53, 69, 0.1);
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: var(--track);
border-radius: 3px;
outline: none;
margin: 0.8rem 0;
position: relative;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: var(--thumb);
border-radius: 50%;
cursor: pointer;
transition: var(--transition);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 10px rgba(239, 96, 19, 0.5);
}
.color-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.color-row {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 1rem 1.25rem;
background-color: rgba(30, 30, 30, 0.7);
border: 1px solid var(--border);
border-radius: var(--input-radius);
transition: var(--transition);
}
.color-row:hover {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.1);
}
.color-picker-container {
position: relative;
width: 40px;
height: 40px;
border-radius: 8px;
overflow: hidden;
border: 2px solid var(--border);
cursor: pointer;
transition: var(--transition);
flex-shrink: 0;
background: var(--card-bg);
display: flex;
align-items: center;
justify-content: center;
--selected-color: #ffffff;
}
.color-picker-container:hover {
border-color: var(--accent);
transform: scale(1.05);
box-shadow: 0 0 12px rgba(239, 96, 19, 0.3);
}
.color-picker-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--selected-color, #ffffff);
border-radius: 6px;
transition: var(--transition);
}
input[type="color"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
cursor: pointer;
background: transparent;
opacity: 0;
z-index: 2;
}
.color-input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-label {
font-size: 10px;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-left: 0.25rem;
}
.color-input {
padding: 0.5rem 0.75rem;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: 12px;
transition: var(--transition);
min-width: 0;
}
.color-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(239, 96, 19, 0.2);
outline: none;
}
.color-input.invalid {
border-color: var(--danger);
box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.2);
}
.hex-input,
.hsl-input {
width: 100%;
}
.color-input-group:nth-child(2) {
flex: 0.3;
}
.color-input-group:nth-child(3) {
flex: 0.7;
}
select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--input-radius);
font-family: var(--font);
font-size: var(--text-xs);
color: var(--text-primary);
background-color: var(--card-bg);
margin-bottom: 0.75rem;
outline: none;
transition: var(--transition);
}
select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
input[type="text"] {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--input-radius);
font-family: var(--font);
font-size: var(--text-xs);
color: var(--text-primary);
background-color: var(--card-bg);
outline: none;
transition: var(--transition);
}
input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.notification {
position: fixed;
bottom: calc(var(--action-bar-height) + 1rem);
left: 50%;
background-color: var(--success);
color: white;
padding: 0.75rem 1rem;
border-radius: var(--input-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1001;
transform: translate(-50%, 200px);
opacity: 0;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
font-size: var(--text-xs);
font-weight: 500;
max-width: 320px;
word-wrap: break-word;
line-height: 1.4;
text-align: center;
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--track);
transition: var(--transition);
border-radius: 20px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: white;
transition: var(--transition);
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--accent);
}
input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.dynamic-island {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 16px;
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
z-index: 10;
}
.dynamic-island.expanded {
border-radius: 24px;
padding: 16px;
}
.island-pill {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.island-btn {
font-size: 13px;
font-weight: 600;
color: white;
background: none;
border: none;
cursor: pointer;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s ease;
z-index: 15;
position: relative;
}
.island-btn:hover {
opacity: 0.8;
}
.island-menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
gap: 12px;
padding: 16px;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease;
}
.dynamic-island.expanded .island-pill {
opacity: 0;
visibility: hidden;
}
.dynamic-island.expanded .island-menu {
opacity: 1;
visibility: visible;
}
.menu-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 12px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.menu-item svg {
width: 24px;
height: 24px;
margin-bottom: 8px;
color: white;
}
.menu-item-label {
font-size: 12px;
color: white;
text-align: center;
font-weight: 500;
}
.menu-item-container {
background-color: rgba(50, 50, 50, 0.5);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid var(--border);
}
.icon-selector-btn {
padding: 0.75rem;
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--input-radius);
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: var(--transition);
margin-bottom: 0.75rem;
width: 100%;
font-size: var(--text-xs);
}
.icon-selector-btn:hover {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.icon-preview {
display: flex;
align-items: center;
gap: 0.75rem;
}
.icon-preview svg {
width: 20px;
height: 20px;
color: var(--text-primary);
}
.icon-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 2000;
visibility: hidden;
opacity: 0;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
}
.icon-modal.active {
visibility: visible;
opacity: 1;
}
.icon-modal-content {
width: 90%;
max-width: 700px;
max-height: 80vh;
background-color: var(--card-bg);
border-radius: var(--card-radius);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
overflow: hidden;
transform: translateY(20px);
transition: all 0.3s ease;
border: 1px solid var(--border);
}
.icon-modal.active .icon-modal-content {
transform: translateY(0);
}
.icon-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border);
}
.icon-modal-title {
font-size: var(--text-s);
font-weight: 600;
}
.close-icon-modal {
background: none;
border: none;
font-size: var(--text-s);
cursor: pointer;
color: var(--text-secondary);
transition: var(--transition);
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-icon-modal:hover {
color: var(--text-primary);
background-color: rgba(255, 255, 255, 0.1);
}
.icon-modal-body {
padding: 1.5rem;
overflow-y: auto;
max-height: calc(80vh - 80px);
}
.icon-search {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--input-radius);
background-color: rgba(0, 0, 0, 0.3);
color: var(--text-primary);
font-size: var(--text-xs);
margin-bottom: 1.5rem;
outline: none;
transition: var(--transition);
}
.icon-search:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 1rem;
}
.icon-option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem 0.5rem;
background-color: rgba(50, 50, 50, 0.5);
border: 2px solid transparent;
border-radius: var(--input-radius);
cursor: pointer;
transition: var(--transition);
min-height: 90px;
}
.icon-option:hover {
background-color: rgba(50, 50, 50, 0.8);
border-color: var(--accent);
}
.icon-option.selected {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
}
.icon-option svg {
width: 24px;
height: 24px;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.icon-option-name {
font-size: 10px;
color: var(--text-secondary);
text-align: center;
word-break: break-word;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.preview-section {
position: static;
}
.controls-section {
max-width: 100%;
}
}
@media (max-width: 768px) {
.action-bar {
flex-direction: column;
height: auto;
min-height: var(--action-bar-height);
padding: 0.75rem;
}
.breadcrumb {
order: 1;
width: 100%;
}
.action-buttons {
order: 2;
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
body {
padding-bottom: calc(var(--action-bar-height) + 20px);
}
.notification {
bottom: calc(var(--action-bar-height) + 2rem);
max-width: 280px;
transform: translate(-50%, 250px);
}
.notification.show {
transform: translate(-50%, 0);
opacity: 1;
}
.preview-container {
height: 300px;
}
.page-title {
font-size: 2rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
button:focus-visible,
input:focus-visible,
.action-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent);
}
.custom-svg-container {
margin-top: 1rem;
padding: 1rem;
background-color: rgba(50, 50, 50, 0.3);
border: 1px solid var(--border);
border-radius: var(--input-radius);
}
.custom-svg-input {
width: 100%;
min-height: 100px;
padding: 0.75rem;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: var(--input-radius);
color: var(--text-primary);
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-size: var(--text-xs);
resize: vertical;
margin-bottom: 0.75rem;
}
.custom-svg-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(239, 96, 19, 0.2);
}
.icon-settings-container {
margin-top: 1rem;
padding: 1rem;
background-color: rgba(50, 50, 50, 0.3);
border: 1px solid var(--border);
border-radius: var(--input-radius);
}
.icon-position-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.position-btn {
padding: 0.75rem;
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
}
.position-btn:hover {
border-color: var(--accent);
color: var(--text-primary);
}
.position-btn.active {
background-color: rgba(239, 96, 19, 0.2);
border-color: var(--accent);
color: var(--accent);
}
.file-upload-btn {
display: inline-block;
padding: 0.75rem 1rem;
background-color: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--input-radius);
color: var(--text-primary);
cursor: pointer;
transition: var(--transition);
font-size: var(--text-xs);
margin-bottom: 0.75rem;
}
.file-upload-btn:hover {
border-color: var(--accent);
background-color: rgba(239, 96, 19, 0.1);
}
.file-upload-input {
display: none;
}
.menu-item.icon-left {
flex-direction: row;
}
.menu-item.icon-right {
flex-direction: row-reverse;
}
.menu-item.icon-bottom {
flex-direction: column-reverse;
}
.menu-item.icon-left svg,
.menu-item.icon-right svg {
margin-bottom: 0;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="action-bar">
<nav class="breadcrumb">
<a href="https://bricksfusion.com" class="breadcrumb-item">Home</a>
<span class="breadcrumb-separator">›</span>
<a href="https://bricksfusion.com/menu-animations/" class="breadcrumb-item">Menu animations</a>
<span class="breadcrumb-separator">›</span>
<span class="breadcrumb-item active">Dynamic Menu Island</span>
</nav>
<div class="action-buttons">
<div class="data-attribute-display" id="quick-attribute" title="Click to copy data attribute">
data-menu-island
</div>
<button class="action-btn primary" id="copy-js" title="Copy JavaScript code (Ctrl+D)" data-protection-animation="true">
<span>📋</span>
Copy JS
</button>
<button class="action-btn" id="copy-full-section" title="Copy complete section JSON for Bricks Builder (Ctrl+S)" data-protection-animation="true">
<span>📦</span>
Copy Full Section
</button>
</div>
</div>
<div class="container">
<div class="page-header">
<h1 class="page-title">Dynamic Menu Island</h1>
<p class="page-subtitle">Interactive navigation menu for Bricks Builder</p>
</div>
<div class="instructions-toggle">
<div class="instructions-card" id="instructions-card">
<div class="instructions-header" id="instructions-toggle">
<div class="instructions-title">
How to Use & Code Information
</div>
<span class="toggle-icon">▼</span>
</div>
<div class="instructions-content" id="instructions-content">
<div class="instructions-grid">
<div class="how-to-use">
<ol>
<li>Customize your dynamic menu island using the controls below</li>
<li>Click <strong>Copy JS</strong> to copy the JavaScript code to clipboard</li>
<li>In Bricks Builder, add a <strong>Code</strong> element</li>
<li>Paste the JavaScript code</li>
<li>To add the menu to any section: go to <strong>Section → Style → Attributes</strong>, add <code>data-menu-island</code> as attribute name (leave value empty)</li>
<li><strong>Optional:</strong> Enable "Relative to Container" to position the island within a specific container instead of the viewport</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<div class="content">
<section class="preview-section">
<div class="preview-container" id="preview-container">
<div class="preview-controls">
<div class="background-selector-wrapper">
<button class="preview-btn background-selector-btn" id="background-selector">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12,2 2,7 12,12 22,7"/>
<polyline points="2,17 12,22 22,17"/>
<polyline points="2,12 12,17 22,12"/>
</svg>
</button>
<input type="color" id="preview-background-picker" class="hidden-color-input" value="#1a1a1a" title="Change Preview Background (B)">
</div>
</div>
<div class="dynamic-island" id="preview-island">
<div class="island-pill">
<button class="island-btn" id="preview-btn">Menu</button>
</div>
<div class="island-menu" id="preview-menu">
</div>
</div>
</div>
</section>
<section class="controls-section">
<div class="card">
<div class="card-heading">
Position & Layout
<div class="card-actions">
<button class="card-action-btn" id="reset-position" title="Reset Position">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">Position</span>
</div>
<select id="position-preset">
<option value="top-center">Top Center</option>
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-center" selected>Bottom Center</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Edge Margin
<span class="help-tooltip" title="Distance from viewport edges">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="edge-margin-value">20</span>px</span>
<button class="reset-btn" onclick="resetParameter('edge-margin', 20)">↺</button>
</div>
</div>
<input type="range" id="edge-margin" min="10" max="50" step="1" value="20">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Menu Layout</span>
</div>
<select id="menu-layout">
<option value="2x2" selected>2×2 Grid</option>
<option value="3x2">3×2 Grid</option>
<option value="2x3">2×3 Grid</option>
<option value="3x3">3×3 Grid</option>
</select>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Button Text</span>
</div>
<input type="text" id="button-text" value="Menu" placeholder="Enter button text">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Relative to Container</span>
<span class="help-tooltip" title="Position relative to container with data-menu-island attribute">ℹ</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="relative-to-container">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Appearance
<div class="card-actions">
<button class="card-action-btn" id="reset-appearance" title="Reset Appearance">↺</button>
</div>
</div>
<div class="card-content">
<div class="color-list">
<div class="control-group">
<div class="control-label">
<span class="label-text">Island Background</span>
</div>
<div class="color-row">
<div class="color-picker-container">
<input type="color" id="island-bg" value="#000000">
</div>
<div class="color-input-group">
<span class="color-label">HEX</span>
<input type="text" class="color-input hex-input" id="island-bg-hex" value="#000000" placeholder="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HSL</span>
<input type="text" class="color-input hsl-input" id="island-bg-hsl" placeholder="hsl(0, 0%, 0%)">
</div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Text Color</span>
</div>
<div class="color-row">
<div class="color-picker-container">
<input type="color" id="text-color" value="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HEX</span>
<input type="text" class="color-input hex-input" id="text-color-hex" value="#FFFFFF" placeholder="#FFFFFF">
</div>
<div class="color-input-group">
<span class="color-label">HSL</span>
<input type="text" class="color-input hsl-input" id="text-color-hsl" placeholder="hsl(0, 0%, 100%)">
</div>
</div>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Background Opacity
<span class="help-tooltip" title="Transparency of the island background">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="bg-opacity-value">0.8</span></span>
<button class="reset-btn" onclick="resetParameter('bg-opacity', 0.8)">↺</button>
</div>
</div>
<input type="range" id="bg-opacity" min="0.3" max="1" step="0.05" value="0.8">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Blur Strength
<span class="help-tooltip" title="Background blur effect strength">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="blur-strength-value">10</span>px</span>
<button class="reset-btn" onclick="resetParameter('blur-strength', 10)">↺</button>
</div>
</div>
<input type="range" id="blur-strength" min="0" max="20" step="1" value="10">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Global Icon Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-global-icon" title="Reset Global Settings">↺</button>
<button class="card-action-btn" id="apply-global-icon" title="Apply to All Items">Apply All</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">Default Icon Position</span>
</div>
<div class="icon-position-grid">
<button class="position-btn active" id="global-pos-top" data-position="top">↑ Top</button>
<button class="position-btn" id="global-pos-bottom" data-position="bottom">↓ Bottom</button>
<button class="position-btn" id="global-pos-left" data-position="left">← Left</button>
<button class="position-btn" id="global-pos-right" data-position="right">→ Right</button>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Default Icon Size
<span class="help-tooltip" title="Default size for all icons">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="global-icon-size-value">24</span>px</span>
<button class="reset-btn" onclick="resetParameter('global-icon-size', 24)">↺</button>
</div>
</div>
<input type="range" id="global-icon-size" min="16" max="48" step="2" value="24">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">
Default Icon Spacing
<span class="help-tooltip" title="Default spacing between icon and text">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="global-icon-spacing-value">8</span>px</span>
<button class="reset-btn" onclick="resetParameter('global-icon-spacing', 8)">↺</button>
</div>
</div>
<input type="range" id="global-icon-spacing" min="0" max="20" step="1" value="8">
</div>
</div>
</div>
<div class="card">
<div class="card-heading">
Menu Items
<div class="card-actions">
<button class="card-action-btn" id="reset-menu" title="Reset Menu Items">↺</button>
</div>
</div>
<div class="card-content" id="menu-items-container">
</div>
</div>
<div class="card">
<div class="card-heading">
Animation Settings
<div class="card-actions">
<button class="card-action-btn" id="reset-animation" title="Reset Animation">↺</button>
</div>
</div>
<div class="card-content">
<div class="control-group">
<div class="control-label">
<span class="label-text">
Animation Duration
<span class="help-tooltip" title="Speed of the expand/collapse animation">ℹ</span>
</span>
<div class="value-display">
<span class="value-text"><span id="animation-duration-value">0.3</span>s</span>
<button class="reset-btn" onclick="resetParameter('animation-duration', 0.3)">↺</button>
</div>
</div>
<input type="range" id="animation-duration" min="0.1" max="1" step="0.05" value="0.3">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Animation Easing</span>
</div>
<select id="animation-easing">
<option value="ease">Ease</option>
<option value="ease-in">Ease In</option>
<option value="ease-out">Ease Out</option>
<option value="ease-in-out">Ease In Out</option>
<option value="cubic-bezier(0.34, 1.56, 0.64, 1)" selected>Spring</option>
<option value="cubic-bezier(0.68, -0.6, 0.32, 1.6)">Bounce</option>
</select>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Close on Outside Click</span>
</div>
<label class="toggle-switch">
<input type="checkbox" id="close-on-click" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</section>
</div>
</div>
<div class="notification" id="notification"></div>
<div class="icon-modal" id="icon-modal">
<div class="icon-modal-content">
<div class="icon-modal-header">
<h3 class="icon-modal-title">Select Icon</h3>
<button class="close-icon-modal" id="close-icon-modal">×</button>
</div>
<div class="icon-modal-body">
<input type="text" class="icon-search" id="icon-search" placeholder="Search icons...">
<div class="icon-grid" id="icon-grid"></div>
</div>
</div>
</div>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let islandConfig = {
position: 'bottom-center',
edgeMargin: 20,
buttonText: 'Menu',
relativeToContainer: false,
islandBg: '#000000',
bgOpacity: 0.8,
textColor: '#FFFFFF',
blurStrength: 10,
menuLayout: '2x2',
globalIconSize: 24,
globalIconPosition: 'top',
globalIconSpacing: 8,
menuItems: [
{ icon: 'home', label: 'Home', url: '#', customSvg: '', iconSize: 24, iconPosition: 'top', iconSpacing: 8 },
{ icon: 'user', label: 'Profile', url: '#', customSvg: '', iconSize: 24, iconPosition: 'top', iconSpacing: 8 },
{ icon: 'settings', label: 'Settings', url: '#', customSvg: '', iconSize: 24, iconPosition: 'top', iconSpacing: 8 },
{ icon: 'bell', label: 'Notifications', url: '#', customSvg: '', iconSize: 24, iconPosition: 'top', iconSpacing: 8 }
],
animationDuration: 0.3,
animationEasing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
closeOnClick: true
};
const defaultConfig = JSON.parse(JSON.stringify(islandConfig));
let isExpanded = false;
let currentIconIndex = null;
const availableIcons = [
'home', 'user', 'settings', 'bell', 'menu', 'x', 'search', 'shopping-cart',
'heart', 'star', 'mail', 'phone', 'calendar', 'clock', 'map-pin', 'globe',
'camera', 'image', 'video', 'music', 'mic', 'volume-2', 'headphones', 'wifi',
'download', 'upload', 'cloud', 'folder', 'file', 'file-text', 'archive', 'trash-2',
'edit', 'copy', 'clipboard', 'link', 'external-link', 'share-2', 'send', 'bookmark',
'tag', 'hash', 'at-sign', 'message-circle', 'message-square', 'users', 'user-plus',
'shield', 'lock', 'unlock', 'key', 'credit-card', 'dollar-sign', 'trending-up',
'custom-svg'
];
// Nueva función mejorada para calcular dimensiones dinámicamente
function calculateDynamicDimensions() {
const [cols, rows] = islandConfig.menuLayout.split('x').map(Number);
// Dimensiones reales basadas en CSS: .menu-item padding: 12px + contenido mínimo
const baseCellWidth = 100; // Aumentado para dar más espacio real
const baseCellHeight = 90; // Altura mínima real necesaria
const gap = 12; // gap: 12px en el CSS
const containerPadding = 32; // padding: 16px * 2 = 32px total
// Calcula el espacio extra necesario para iconos horizontales
let maxExtraWidth = 0;
let maxExtraHeight = 0;
// Solo considera los items que realmente se van a mostrar según el layout
const maxItems = getMaxItemsForLayout(islandConfig.menuLayout);
const activeItems = islandConfig.menuItems.slice(0, maxItems);
activeItems.forEach(item => {
if (item.iconPosition === 'left' || item.iconPosition === 'right') {
// Para iconos horizontales, necesitamos más ancho: tamaño icono + spacing + margen adicional
const extraWidth = item.iconSize + item.iconSpacing + 20; // +20px margen adicional
maxExtraWidth = Math.max(maxExtraWidth, extraWidth);
}
// Considerar altura extra para iconos en bottom
if (item.iconPosition === 'bottom') {
const extraHeight = item.iconSpacing + 10; // +10px margen adicional
maxExtraHeight = Math.max(maxExtraHeight, extraHeight);
}
});
// Calcula las dimensiones totales con fórmula correcta
// Width = (base_width + extra_width) * cols + gaps_between_cols + container_padding
const totalWidth = (baseCellWidth + maxExtraWidth) * cols + (gap * Math.max(0, cols - 1)) + containerPadding;
// Height = (base_height + extra_height) * rows + gaps_between_rows + container_padding
const totalHeight = (baseCellHeight + maxExtraHeight) * rows + (gap * Math.max(0, rows - 1)) + containerPadding;
return {
width: Math.max(totalWidth, 280), // Mínimo más realista
height: Math.max(totalHeight, 240)
};
}
// Función legacy para mantener compatibilidad (ahora usa la nueva función mejorada)
function getExpandedDimensions() {
return calculateDynamicDimensions();
}
function initializeUI() {
initInstructions();
initColorInputs();
initMenuItems();
initIconModal();
initEventListeners();
updatePreview();
loadConfiguration();
setTimeout(() => {
showNotification('BricksFusion Dynamic Menu Island Configurator loaded!');
}, 500);
}
function initInstructions() {
const instructionsToggle = document.getElementById('instructions-toggle');
const instructionsContent = document.getElementById('instructions-content');
const instructionsCard = document.getElementById('instructions-card');
const toggleIcon = instructionsToggle.querySelector('.toggle-icon');
instructionsToggle.addEventListener('click', () => {
const isVisible = instructionsContent.classList.contains('show');
if (isVisible) {
instructionsContent.classList.remove('show');
instructionsCard.classList.remove('expanded');
toggleIcon.classList.remove('expanded');
} else {
instructionsContent.classList.add('show');
instructionsCard.classList.add('expanded');
toggleIcon.classList.add('expanded');
}
});
}
function initColorInputs() {
setupColorInput('island-bg', '#000000');
setupColorInput('text-color', '#FFFFFF');
}
function setupColorInput(id, defaultValue) {
const colorInput = document.getElementById(id);
const hexInput = document.getElementById(`${id}-hex`);
const hslInput = document.getElementById(`${id}-hsl`);
const container = colorInput.closest('.color-picker-container');
hslInput.value = hexToHsl(defaultValue);
container.style.setProperty('--selected-color', defaultValue);
colorInput.addEventListener('input', () => {
const color = colorInput.value;
hexInput.value = color;
hslInput.value = hexToHsl(color);
hexInput.classList.remove('invalid');
hslInput.classList.remove('invalid');
container.style.setProperty('--selected-color', color);
if (id === 'island-bg') {
islandConfig.islandBg = color;
} else {
islandConfig.textColor = color;
}
updatePreview();
saveConfiguration();
});
hexInput.addEventListener('input', (e) => {
let hex = formatHex(e.target.value);
e.target.value = hex;
if (isValidHex(hex)) {
colorInput.value = hex;
hslInput.value = hexToHsl(hex);
e.target.classList.remove('invalid');
hslInput.classList.remove('invalid');
container.style.setProperty('--selected-color', hex);
if (id === 'island-bg') {
islandConfig.islandBg = hex;
} else {
islandConfig.textColor = hex;
}
updatePreview();
saveConfiguration();
} else {
e.target.classList.add('invalid');
}
});
hslInput.addEventListener('input', (e) => {
const hsl = e.target.value;
if (isValidHsl(hsl)) {
const hex = hslToHex(hsl);
if (hex) {
colorInput.value = hex;
hexInput.value = hex;
e.target.classList.remove('invalid');
hexInput.classList.remove('invalid');
container.style.setProperty('--selected-color', hex);
if (id === 'island-bg') {
islandConfig.islandBg = hex;
} else {
islandConfig.textColor = hex;
}
updatePreview();
saveConfiguration();
}
} else {
e.target.classList.add('invalid');
}
});
}
function initMenuItems() {
updateMenuItemsUI();
}
function updateMenuItemsUI() {
const container = document.getElementById('menu-items-container');
const layout = document.getElementById('menu-layout').value;
const maxItems = getMaxItemsForLayout(layout);
container.innerHTML = '';
for (let i = 0; i < maxItems; i++) {
if (!islandConfig.menuItems[i]) {
// CORREGIDO: Usar valores globales en lugar de hardcodeados
islandConfig.menuItems[i] = {
icon: availableIcons[i % (availableIcons.length - 1)],
label: `Item ${i + 1}`,
url: '#',
customSvg: '',
iconSize: islandConfig.globalIconSize || 24,
iconPosition: islandConfig.globalIconPosition || 'top',
iconSpacing: islandConfig.globalIconSpacing || 8
};
} else {
// Migración de propiedades legacy
if (!islandConfig.menuItems[i].iconSize && islandConfig.menuItems[i].svgSize) {
islandConfig.menuItems[i].iconSize = islandConfig.menuItems[i].svgSize;
}
if (!islandConfig.menuItems[i].iconPosition) {
islandConfig.menuItems[i].iconPosition = islandConfig.globalIconPosition || 'top';
}
if (islandConfig.menuItems[i].iconSpacing === undefined) {
islandConfig.menuItems[i].iconSpacing = islandConfig.globalIconSpacing || 8;
}
}
const item = islandConfig.menuItems[i];
const itemEl = document.createElement('div');
itemEl.className = 'control-group';
const showCustomSvg = item.icon === 'custom-svg';
const iconDisplay = showCustomSvg ? 'Custom SVG' : item.icon.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
itemEl.innerHTML = `
<div class="control-label">
<span class="label-text">Menu Item ${i + 1}</span>
</div>
<div class="menu-item-container">
<button class="icon-selector-btn" data-index="${i}">
<div class="icon-preview">
${showCustomSvg && item.customSvg ?
`<div style="width: 20px; height: 20px;">${item.customSvg}</div>` :
`<span data-lucide="${item.icon === 'custom-svg' ? 'code' : item.icon}"></span>`
}
<span>${iconDisplay}</span>
</div>
<span>Change Icon</span>
</button>
<input type="text" id="menu-label-${i}" value="${item.label}" placeholder="Label">
<input type="text" id="menu-url-${i}" value="${item.url}" placeholder="URL">
<div class="icon-settings-container">
<div class="control-label">
<span class="label-text">Icon Settings</span>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Icon Position</span>
</div>
<div class="icon-position-grid">
<button class="position-btn ${item.iconPosition === 'top' ? 'active' : ''}" data-index="${i}" data-position="top">↑ Top</button>
<button class="position-btn ${item.iconPosition === 'bottom' ? 'active' : ''}" data-index="${i}" data-position="bottom">↓ Bottom</button>
<button class="position-btn ${item.iconPosition === 'left' ? 'active' : ''}" data-index="${i}" data-position="left">← Left</button>
<button class="position-btn ${item.iconPosition === 'right' ? 'active' : ''}" data-index="${i}" data-position="right">→ Right</button>
</div>
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Icon Size</span>
<span class="value-text"><span id="icon-size-value-${i}">${item.iconSize}</span>px</span>
</div>
<input type="range" id="icon-size-${i}" min="16" max="48" step="2" value="${item.iconSize}">
</div>
<div class="control-group">
<div class="control-label">
<span class="label-text">Icon Spacing</span>
<span class="value-text"><span id="icon-spacing-value-${i}">${item.iconSpacing}</span>px</span>
</div>
<input type="range" id="icon-spacing-${i}" min="0" max="20" step="1" value="${item.iconSpacing}">
</div>
${showCustomSvg ? `
<div class="custom-svg-container">
<div class="control-label">
<span class="label-text">Custom SVG</span>
</div>
<label class="file-upload-btn">
<input type="file" class="file-upload-input" id="svg-upload-${i}" accept=".svg,image/svg+xml">
📁 Upload SVG File
</label>
<textarea class="custom-svg-input" id="custom-svg-${i}" placeholder="Paste your SVG code here or upload a file...">${item.customSvg || ''}</textarea>
</div>
` : ''}
</div>
</div>
`;
container.appendChild(itemEl);
document.getElementById(`menu-label-${i}`).addEventListener('input', (e) => {
islandConfig.menuItems[i].label = e.target.value;
updatePreview();
saveConfiguration();
});
document.getElementById(`menu-url-${i}`).addEventListener('input', (e) => {
islandConfig.menuItems[i].url = e.target.value;
saveConfiguration();
});
document.querySelectorAll(`.position-btn[data-index="${i}"]`).forEach(btn => {
btn.addEventListener('click', (e) => {
const position = btn.dataset.position;
islandConfig.menuItems[i].iconPosition = position;
document.querySelectorAll(`.position-btn[data-index="${i}"]`).forEach(b => {
b.classList.remove('active');
});
btn.classList.add('active');
updatePreview();
saveConfiguration();
});
});
document.getElementById(`icon-size-${i}`).addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById(`icon-size-value-${i}`).textContent = value;
islandConfig.menuItems[i].iconSize = value;
updatePreview();
saveConfiguration();
});
document.getElementById(`icon-spacing-${i}`).addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById(`icon-spacing-value-${i}`).textContent = value;
islandConfig.menuItems[i].iconSpacing = value;
updatePreview();
saveConfiguration();
});
if (showCustomSvg) {
document.getElementById(`custom-svg-${i}`).addEventListener('input', (e) => {
islandConfig.menuItems[i].customSvg = e.target.value;
updatePreview();
saveConfiguration();
});
document.getElementById(`svg-upload-${i}`).addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && file.type === 'image/svg+xml') {
const reader = new FileReader();
reader.onload = (event) => {
const svgContent = event.target.result;
document.getElementById(`custom-svg-${i}`).value = svgContent;
islandConfig.menuItems[i].customSvg = svgContent;
updateMenuItemsUI();
updatePreview();
saveConfiguration();
showNotification('SVG file uploaded successfully!');
};
reader.readAsText(file);
} else {
showNotification('Please upload a valid SVG file', 'error');
}
});
}
}
lucide.createIcons();
document.querySelectorAll('.icon-selector-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
currentIconIndex = parseInt(btn.dataset.index);
openIconModal();
});
});
}
function initIconModal() {
const modal = document.getElementById('icon-modal');
const closeBtn = document.getElementById('close-icon-modal');
const searchInput = document.getElementById('icon-search');
const grid = document.getElementById('icon-grid');
closeBtn.addEventListener('click', () => {
modal.classList.remove('active');
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
searchInput.addEventListener('input', (e) => {
const search = e.target.value.toLowerCase();
renderIconGrid(search);
});
renderIconGrid();
}
function renderIconGrid(search = '') {
const grid = document.getElementById('icon-grid');
grid.innerHTML = '';
const filtered = availableIcons.filter(icon =>
icon.toLowerCase().includes(search)
);
filtered.forEach(icon => {
const option = document.createElement('div');
option.className = 'icon-option';
if (currentIconIndex !== null && islandConfig.menuItems[currentIconIndex].icon === icon) {
option.classList.add('selected');
}
const displayName = icon === 'custom-svg' ?
'Custom SVG' :
icon.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
option.innerHTML = `
<span data-lucide="${icon === 'custom-svg' ? 'code' : icon}"></span>
<span class="icon-option-name">${displayName}</span>
`;
option.addEventListener('click', () => {
if (currentIconIndex !== null) {
islandConfig.menuItems[currentIconIndex].icon = icon;
if (icon !== 'custom-svg') {
islandConfig.menuItems[currentIconIndex].customSvg = '';
}
updateMenuItemsUI();
updatePreview();
saveConfiguration();
document.getElementById('icon-modal').classList.remove('active');
}
});
grid.appendChild(option);
});
lucide.createIcons();
}
function openIconModal() {
const modal = document.getElementById('icon-modal');
modal.classList.add('active');
document.getElementById('icon-search').value = '';
renderIconGrid();
}
function getMaxItemsForLayout(layout) {
const map = {
'2x2': 4,
'3x2': 6,
'2x3': 6,
'3x3': 9
};
return map[layout] || 4;
}
function initEventListeners() {
document.getElementById('quick-attribute').addEventListener('click', () => {
copyToClipboard('data-menu-island');
});
document.getElementById('copy-js').addEventListener('click', () => {
copyJsToClipboard();
});
document.getElementById('copy-full-section').addEventListener('click', () => {
copyFullSectionToClipboard();
});
document.getElementById('reset-position').addEventListener('click', () => {
resetSection('position');
});
document.getElementById('reset-appearance').addEventListener('click', () => {
resetSection('appearance');
});
document.getElementById('reset-menu').addEventListener('click', () => {
resetSection('menu');
});
document.getElementById('reset-animation').addEventListener('click', () => {
resetSection('animation');
});
document.getElementById('reset-global-icon').addEventListener('click', () => {
islandConfig.globalIconPosition = 'top';
islandConfig.globalIconSize = 24;
islandConfig.globalIconSpacing = 8;
document.getElementById('global-icon-size').value = 24;
document.getElementById('global-icon-size-value').textContent = 24;
document.getElementById('global-icon-spacing').value = 8;
document.getElementById('global-icon-spacing-value').textContent = 8;
document.querySelectorAll('.position-btn').forEach(b => {
if (b.id && b.id.startsWith('global-pos-')) {
b.classList.remove('active');
}
});
document.getElementById('global-pos-top').classList.add('active');
saveConfiguration();
showNotification('Global icon settings reset to defaults');
});
document.getElementById('apply-global-icon').addEventListener('click', () => {
applyGlobalIconSettings();
});
document.querySelectorAll('#global-pos-top, #global-pos-bottom, #global-pos-left, #global-pos-right').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.position-btn').forEach(b => {
if (b.id && b.id.startsWith('global-pos-')) {
b.classList.remove('active');
}
});
btn.classList.add('active');
islandConfig.globalIconPosition = btn.dataset.position;
// NUEVO: Aplicar automáticamente a todos los items existentes
islandConfig.menuItems.forEach(item => {
if (item) item.iconPosition = btn.dataset.position;
});
updateMenuItemsUI();
updatePreview();
saveConfiguration();
});
});
document.getElementById('global-icon-size').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById('global-icon-size-value').textContent = value;
islandConfig.globalIconSize = value;
// NUEVO: Aplicar automáticamente a todos los items existentes
islandConfig.menuItems.forEach(item => {
if (item) item.iconSize = value;
});
updateMenuItemsUI();
updatePreview();
saveConfiguration();
});
document.getElementById('global-icon-spacing').addEventListener('input', (e) => {
const value = parseInt(e.target.value);
document.getElementById('global-icon-spacing-value').textContent = value;
islandConfig.globalIconSpacing = value;
// NUEVO: Aplicar automáticamente a todos los items existentes
islandConfig.menuItems.forEach(item => {
if (item) item.iconSpacing = value;
});
updateMenuItemsUI();
updatePreview();
saveConfiguration();
});
document.getElementById('position-preset').addEventListener('change', (e) => {
islandConfig.position = e.target.value;
updatePreview();
saveConfiguration();
});
document.getElementById('menu-layout').addEventListener('change', (e) => {
islandConfig.menuLayout = e.target.value;
updateMenuItemsUI();
updatePreview();
saveConfiguration();
});
document.getElementById('button-text').addEventListener('input', (e) => {
islandConfig.buttonText = e.target.value;
updatePreview();
saveConfiguration();
});
document.getElementById('relative-to-container').addEventListener('change', (e) => {
islandConfig.relativeToContainer = e.target.checked;
updatePreview();
saveConfiguration();
});
document.getElementById('animation-easing').addEventListener('change', (e) => {
islandConfig.animationEasing = e.target.value;
updatePreview();
saveConfiguration();
});
document.getElementById('close-on-click').addEventListener('change', (e) => {
islandConfig.closeOnClick = e.target.checked;
saveConfiguration();
});
const backgroundPicker = document.getElementById('preview-background-picker');
const previewContainer = document.getElementById('preview-container');
backgroundPicker.addEventListener('input', (e) => {
const selectedColor = e.target.value;
previewContainer.style.backgroundColor = selectedColor;
showNotification(`Preview background changed to ${selectedColor}`);
});
previewContainer.style.backgroundColor = '#1a1a1a';
setupRangeInput('edge-margin', 'edgeMargin', 20);
setupRangeInput('bg-opacity', 'bgOpacity', 0.8);
setupRangeInput('blur-strength', 'blurStrength', 10);
setupRangeInput('animation-duration', 'animationDuration', 0.3);
document.getElementById('preview-island').addEventListener('click', togglePreview);
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
if (e.ctrlKey || e.metaKey) {
switch (e.key.toLowerCase()) {
case 'd':
e.preventDefault();
const copyBtn = document.getElementById('copy-js');
if (copyBtn && copyBtn.hasAttribute('data-protection-animation')) {
copyBtn.click();
} else {
copyJsToClipboard();
}
break;
case 's':
e.preventDefault();
const fullSectionBtn = document.getElementById('copy-full-section');
if (fullSectionBtn && fullSectionBtn.hasAttribute('data-protection-animation')) {
fullSectionBtn.click();
} else {
copyFullSectionToClipboard();
}
break;
}
} else {
switch (e.key.toLowerCase()) {
case 'b':
document.getElementById('preview-background-picker').click();
break;
}
}
});
}
function applyGlobalIconSettings() {
const position = islandConfig.globalIconPosition;
const size = islandConfig.globalIconSize;
const spacing = islandConfig.globalIconSpacing;
islandConfig.menuItems.forEach(item => {
item.iconPosition = position;
item.iconSize = size;
item.iconSpacing = spacing;
});
updateMenuItemsUI();
updatePreview();
saveConfiguration();
showNotification('Global icon settings applied to all items!');
}
function setupRangeInput(id, configKey, defaultValue) {
const input = document.getElementById(id);
const valueEl = document.getElementById(`${id}-value`);
input.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
valueEl.textContent = value;
islandConfig[configKey] = value;
updatePreview();
saveConfiguration();
});
}
window.resetParameter = function(id, defaultValue) {
const element = document.getElementById(id);
if (element) {
element.value = defaultValue;
const valueElement = document.getElementById(`${id}-value`);
if (valueElement) {
valueElement.textContent = defaultValue;
}
const configMap = {
'edge-margin': 'edgeMargin',
'bg-opacity': 'bgOpacity',
'blur-strength': 'blurStrength',
'animation-duration': 'animationDuration',
'global-icon-size': 'globalIconSize',
'global-icon-spacing': 'globalIconSpacing'
};
if (configMap[id]) {
islandConfig[configMap[id]] = defaultValue;
}
updatePreview();
saveConfiguration();
showNotification(`${id.replace(/-/g, ' ')} reset to default`);
}
};
function resetSection(section) {
switch(section) {
case 'position':
islandConfig.position = defaultConfig.position;
islandConfig.edgeMargin = defaultConfig.edgeMargin;
islandConfig.menuLayout = defaultConfig.menuLayout;
islandConfig.buttonText = defaultConfig.buttonText;
islandConfig.relativeToContainer = defaultConfig.relativeToContainer;
document.getElementById('position-preset').value = defaultConfig.position;
document.getElementById('edge-margin').value = defaultConfig.edgeMargin;
document.getElementById('edge-margin-value').textContent = defaultConfig.edgeMargin;
document.getElementById('menu-layout').value = defaultConfig.menuLayout;
document.getElementById('button-text').value = defaultConfig.buttonText;
document.getElementById('relative-to-container').checked = defaultConfig.relativeToContainer;
break;
case 'appearance':
islandConfig.islandBg = defaultConfig.islandBg;
islandConfig.bgOpacity = defaultConfig.bgOpacity;
islandConfig.textColor = defaultConfig.textColor;
islandConfig.blurStrength = defaultConfig.blurStrength;
document.getElementById('island-bg').value = defaultConfig.islandBg;
document.getElementById('island-bg-hex').value = defaultConfig.islandBg;
document.getElementById('island-bg-hsl').value = hexToHsl(defaultConfig.islandBg);
document.getElementById('text-color').value = defaultConfig.textColor;
document.getElementById('text-color-hex').value = defaultConfig.textColor;
document.getElementById('text-color-hsl').value = hexToHsl(defaultConfig.textColor);
document.getElementById('bg-opacity').value = defaultConfig.bgOpacity;
document.getElementById('bg-opacity-value').textContent = defaultConfig.bgOpacity;
document.getElementById('blur-strength').value = defaultConfig.blurStrength;
document.getElementById('blur-strength-value').textContent = defaultConfig.blurStrength;
document.querySelector('#island-bg').closest('.color-picker-container').style.setProperty('--selected-color', defaultConfig.islandBg);
document.querySelector('#text-color').closest('.color-picker-container').style.setProperty('--selected-color', defaultConfig.textColor);
break;
case 'menu':
islandConfig.menuItems = JSON.parse(JSON.stringify(defaultConfig.menuItems));
islandConfig.globalIconPosition = defaultConfig.globalIconPosition;
islandConfig.globalIconSize = defaultConfig.globalIconSize;
islandConfig.globalIconSpacing = defaultConfig.globalIconSpacing;
document.getElementById('global-icon-size').value = defaultConfig.globalIconSize;
document.getElementById('global-icon-size-value').textContent = defaultConfig.globalIconSize;
document.getElementById('global-icon-spacing').value = defaultConfig.globalIconSpacing;
document.getElementById('global-icon-spacing-value').textContent = defaultConfig.globalIconSpacing;
document.querySelectorAll('.position-btn').forEach(b => {
if (b.id && b.id.startsWith('global-pos-')) {
b.classList.remove('active');
}
});
document.getElementById('global-pos-top').classList.add('active');
updateMenuItemsUI();
break;
case 'animation':
islandConfig.animationDuration = defaultConfig.animationDuration;
islandConfig.animationEasing = defaultConfig.animationEasing;
islandConfig.closeOnClick = defaultConfig.closeOnClick;
document.getElementById('animation-duration').value = defaultConfig.animationDuration;
document.getElementById('animation-duration-value').textContent = defaultConfig.animationDuration;
document.getElementById('animation-easing').value = defaultConfig.animationEasing;
document.getElementById('close-on-click').checked = defaultConfig.closeOnClick;
break;
}
updatePreview();
saveConfiguration();
showNotification(`${section.charAt(0).toUpperCase() + section.slice(1)} settings reset`);
}
function togglePreview() {
const island = document.getElementById('preview-island');
const menu = document.getElementById('preview-menu');
isExpanded = !isExpanded;
if (isExpanded) {
island.classList.add('expanded');
const dimensions = calculateDynamicDimensions(); // Usa la función mejorada
island.style.width = `${dimensions.width}px`;
island.style.height = `${dimensions.height}px`;
} else {
island.classList.remove('expanded');
island.style.width = '120px';
island.style.height = '48px';
}
}
function updatePreview() {
const island = document.getElementById('preview-island');
const btn = document.getElementById('preview-btn');
const menu = document.getElementById('preview-menu');
const position = islandConfig.position.split('-');
island.style.top = '';
island.style.right = '';
island.style.bottom = '';
island.style.left = '';
island.style.transform = '';
if (position[0] === 'top') {
island.style.top = `${islandConfig.edgeMargin}px`;
} else {
island.style.bottom = `${islandConfig.edgeMargin}px`;
}
if (position[1] === 'left') {
island.style.left = `${islandConfig.edgeMargin}px`;
} else if (position[1] === 'right') {
island.style.right = `${islandConfig.edgeMargin}px`;
} else {
island.style.left = '50%';
island.style.transform = 'translateX(-50%)';
}
btn.textContent = islandConfig.buttonText;
island.style.backgroundColor = hexToRgba(islandConfig.islandBg, islandConfig.bgOpacity);
island.style.backdropFilter = `blur(${islandConfig.blurStrength}px)`;
island.style.webkitBackdropFilter = `blur(${islandConfig.blurStrength}px)`;
island.style.transition = `all ${islandConfig.animationDuration}s ${islandConfig.animationEasing}`;
btn.style.color = islandConfig.textColor;
const [cols, rows] = islandConfig.menuLayout.split('x').map(Number);
menu.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
menu.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
// SOLUCIÓN: Recalcular y aplicar dimensiones dinámicas si está expandido
if (island.classList.contains('expanded')) {
const dimensions = calculateDynamicDimensions();
island.style.width = `${dimensions.width}px`;
island.style.height = `${dimensions.height}px`;
}
menu.innerHTML = '';
const maxItems = getMaxItemsForLayout(islandConfig.menuLayout);
for (let i = 0; i < maxItems && i < islandConfig.menuItems.length; i++) {
const item = islandConfig.menuItems[i];
const menuItem = document.createElement('div');
menuItem.className = 'menu-item';
if (item.iconPosition === 'left') {
menuItem.classList.add('icon-left');
} else if (item.iconPosition === 'right') {
menuItem.classList.add('icon-right');
} else if (item.iconPosition === 'bottom') {
menuItem.classList.add('icon-bottom');
}
if (item.icon === 'custom-svg' && item.customSvg) {
const svgWrapper = document.createElement('div');
svgWrapper.style.width = `${item.iconSize}px`;
svgWrapper.style.height = `${item.iconSize}px`;
if (item.iconPosition === 'left' || item.iconPosition === 'right') {
svgWrapper.style.marginLeft = item.iconPosition === 'right' ? `${item.iconSpacing}px` : '0';
svgWrapper.style.marginRight = item.iconPosition === 'left' ? `${item.iconSpacing}px` : '0';
} else {
svgWrapper.style.marginBottom = item.iconPosition === 'top' ? `${item.iconSpacing}px` : '0';
svgWrapper.style.marginTop = item.iconPosition === 'bottom' ? `${item.iconSpacing}px` : '0';
}
svgWrapper.innerHTML = item.customSvg;
const svg = svgWrapper.querySelector('svg');
if (svg) {
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.fill = islandConfig.textColor;
svg.style.stroke = islandConfig.textColor;
}
menuItem.appendChild(svgWrapper);
} else {
const iconSpan = document.createElement('span');
iconSpan.setAttribute('data-lucide', item.icon === 'custom-svg' ? 'code' : item.icon);
iconSpan.style.width = `${item.iconSize}px`;
iconSpan.style.height = `${item.iconSize}px`;
if (item.iconPosition === 'left' || item.iconPosition === 'right') {
iconSpan.style.marginLeft = item.iconPosition === 'right' ? `${item.iconSpacing}px` : '0';
iconSpan.style.marginRight = item.iconPosition === 'left' ? `${item.iconSpacing}px` : '0';
} else {
iconSpan.style.marginBottom = item.iconPosition === 'top' ? `${item.iconSpacing}px` : '0';
iconSpan.style.marginTop = item.iconPosition === 'bottom' ? `${item.iconSpacing}px` : '0';
}
menuItem.appendChild(iconSpan);
}
const label = document.createElement('span');
label.className = 'menu-item-label';
label.style.color = islandConfig.textColor;
label.textContent = item.label;
menuItem.appendChild(label);
menu.appendChild(menuItem);
}
lucide.createIcons();
menu.querySelectorAll('[data-lucide] svg').forEach((svg, index) => {
if (index < islandConfig.menuItems.length) {
const item = islandConfig.menuItems[index];
svg.style.width = `${item.iconSize}px`;
svg.style.height = `${item.iconSize}px`;
svg.style.color = islandConfig.textColor;
}
});
}
function hexToRgba(hex, opacity) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
function hexToHsl(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
}
function hslToHex(hsl) {
const match = hsl.match(/hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/);
if (!match) return null;
let h = parseInt(match[1]) / 360;
let s = parseInt(match[2]) / 100;
let l = parseInt(match[3]) / 100;
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
const toHex = (c) => {
const hex = Math.round(c * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function isValidHex(hex) {
return /^#[0-9A-F]{6}$/i.test(hex);
}
function isValidHsl(hsl) {
return /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/i.test(hsl);
}
function formatHex(value) {
let hex = value.replace(/[^0-9A-Fa-f#]/g, '');
if (!hex.startsWith('#')) {
hex = '#' + hex;
}
if (hex.length > 7) {
hex = hex.substring(0, 7);
}
return hex.toUpperCase();
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
showNotification('Copied to clipboard!');
})
.catch(err => {
showNotification('Failed to copy to clipboard', 'error');
});
}
function copyJsToClipboard() {
const jsCode = generateJavaScriptCode();
navigator.clipboard.writeText(jsCode)
.then(() => {
showNotification('JavaScript code copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = jsCode;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('JavaScript code copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function copyFullSectionToClipboard() {
const sectionJSON = generateFullSectionJSON();
navigator.clipboard.writeText(sectionJSON)
.then(() => {
showNotification('Full section JSON copied to clipboard!');
})
.catch(err => {
try {
const textArea = document.createElement('textarea');
textArea.value = sectionJSON;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showNotification('Full section JSON copied to clipboard!');
} catch (fallbackErr) {
showNotification('Failed to copy to clipboard. Please try again.', 'error');
}
});
}
function generateUniqueId() {
return Math.random().toString(36).substring(2, 8);
}
function generateFullSectionJSON() {
// Generar IDs únicos para todos los elementos
const sectionId = generateUniqueId();
const containerId = generateUniqueId();
const divId = generateUniqueId();
const codeId = generateUniqueId();
const attributeId = generateUniqueId();
// Obtener el JavaScript actual con la configuración del usuario
const jsCode = generateJavaScriptCode();
// Crear el objeto JSON completo de Bricks Builder
const bricksJSON = {
"content": [
{
"id": sectionId,
"name": "section",
"parent": 0,
"children": [containerId, codeId],
"settings": {
"_padding": {
"top": "10",
"bottom": "10"
}
}
},
{
"id": containerId,
"name": "container",
"parent": sectionId,
"children": [divId],
"settings": {
"_alignItems": "center"
}
},
{
"id": divId,
"name": "div",
"parent": containerId,
"children": [],
"settings": {
"_attributes": [
{
"id": attributeId,
"name": "data-expandable-tabs"
}
]
},
"label": "Dynamic Island Div"
},
{
"id": codeId,
"name": "code",
"parent": sectionId,
"children": [],
"settings": {
"_display": "none",
"javascriptCode": jsCode,
"executeCode": true
},
"label": "Dynamic Island JS"
}
],
"source": "bricksCopiedElements",
"sourceUrl": "https://test.bricksfusion.com",
"version": "2.0.1",
"globalClasses": [],
"globalElements": []
};
return JSON.stringify(bricksJSON, null, 2);
}
// Reemplaza la función generateJavaScriptCode() completa en el código original
function generateJavaScriptCode() {
// ASEGURAR que usamos la configuración más actualizada
const configToExport = JSON.parse(JSON.stringify(islandConfig));
return `(function() {
const config = ${JSON.stringify(configToExport, null, 4)};
const style = document.createElement('style');
style.textContent = \`
.dmi-island {
position: \${config.relativeToContainer ? 'absolute' : 'fixed'};
display: flex;
align-items: center;
justify-content: center;
padding: 6px 16px;
background-color: \${hexToRgba(config.islandBg, config.bgOpacity)};
backdrop-filter: blur(\${config.blurStrength}px);
-webkit-backdrop-filter: blur(\${config.blurStrength}px);
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: all \${config.animationDuration}s \${config.animationEasing};
border: 1px solid rgba(255, 255, 255, 0.1);
overflow: hidden;
z-index: 9999;
width: 120px;
height: 48px;
}
.dmi-island.expanded {
border-radius: 24px;
padding: 16px;
}
.dmi-pill {
display: flex;
align-items: center;
justify-content: center;
transition: opacity \${config.animationDuration}s ease;
}
.dmi-btn {
font-size: 13px;
font-weight: 600;
color: \${config.textColor};
background: none;
border: none;
cursor: pointer;
padding: 8px;
}
.dmi-menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
gap: 12px;
padding: 16px;
opacity: 0;
visibility: hidden;
transition: opacity \${config.animationDuration}s ease;
}
.dmi-island.expanded .dmi-pill {
opacity: 0;
visibility: hidden;
}
.dmi-island.expanded .dmi-menu {
opacity: 1;
visibility: visible;
}
.dmi-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 12px;
cursor: pointer;
transition: background-color 0.2s ease;
text-decoration: none;
}
.dmi-item.icon-left {
flex-direction: row;
}
.dmi-item.icon-right {
flex-direction: row-reverse;
}
.dmi-item.icon-bottom {
flex-direction: column-reverse;
}
.dmi-item:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.dmi-item svg {
color: \${config.textColor};
}
.dmi-label {
font-size: 12px;
color: \${config.textColor};
text-align: center;
font-weight: 500;
font-family: system-ui, -apple-system, sans-serif;
}
\`;
document.head.appendChild(style);
function hexToRgba(hex, opacity) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + opacity + ')';
}
// Nueva función de cálculo dinámico de dimensiones (IDÉNTICA al preview)
function calculateDynamicDimensions() {
const [cols, rows] = config.menuLayout.split('x').map(Number);
// Dimensiones reales basadas en CSS: .menu-item padding: 12px + contenido mínimo
const baseCellWidth = 100; // Aumentado para dar más espacio real
const baseCellHeight = 90; // Altura mínima real necesaria
const gap = 12; // gap: 12px en el CSS
const containerPadding = 32; // padding: 16px * 2 = 32px total
// Calcula el espacio extra necesario para iconos horizontales
let maxExtraWidth = 0;
let maxExtraHeight = 0;
// Solo considera los items que realmente se van a mostrar según el layout
const maxItems = getMaxItemsForLayout();
const activeItems = config.menuItems.slice(0, maxItems);
activeItems.forEach(item => {
if (item.iconPosition === 'left' || item.iconPosition === 'right') {
// Para iconos horizontales, necesitamos más ancho: tamaño icono + spacing + margen adicional
const extraWidth = item.iconSize + item.iconSpacing + 20; // +20px margen adicional
maxExtraWidth = Math.max(maxExtraWidth, extraWidth);
}
// Considerar altura extra para iconos en bottom
if (item.iconPosition === 'bottom') {
const extraHeight = item.iconSpacing + 10; // +10px margen adicional
maxExtraHeight = Math.max(maxExtraHeight, extraHeight);
}
});
// Calcula las dimensiones totales con fórmula correcta
// Width = (base_width + extra_width) * cols + gaps_between_cols + container_padding
const totalWidth = (baseCellWidth + maxExtraWidth) * cols + (gap * Math.max(0, cols - 1)) + containerPadding;
// Height = (base_height + extra_height) * rows + gaps_between_rows + container_padding
const totalHeight = (baseCellHeight + maxExtraHeight) * rows + (gap * Math.max(0, rows - 1)) + containerPadding;
return {
width: Math.max(totalWidth, 280), // Mínimo más realista
height: Math.max(totalHeight, 240)
};
}
function getMaxItemsForLayout() {
const map = {
'2x2': 4,
'3x2': 6,
'2x3': 6,
'3x3': 9
};
return map[config.menuLayout] || 4;
}
function initIsland() {
const targetContainer = config.relativeToContainer
? document.querySelector('[data-menu-island]')
: document.body;
if (!targetContainer) return;
if (config.relativeToContainer && targetContainer !== document.body) {
const computedStyle = getComputedStyle(targetContainer);
if (computedStyle.position === 'static') {
targetContainer.style.position = 'relative';
}
}
const island = document.createElement('div');
island.className = 'dmi-island';
const position = config.position.split('-');
if (position[0] === 'top') {
island.style.top = config.edgeMargin + 'px';
} else {
island.style.bottom = config.edgeMargin + 'px';
}
if (position[1] === 'left') {
island.style.left = config.edgeMargin + 'px';
} else if (position[1] === 'right') {
island.style.right = config.edgeMargin + 'px';
} else {
island.style.left = '50%';
island.style.transform = 'translateX(-50%)';
}
const pill = document.createElement('div');
pill.className = 'dmi-pill';
const btn = document.createElement('button');
btn.className = 'dmi-btn';
btn.textContent = config.buttonText;
pill.appendChild(btn);
const menu = document.createElement('div');
menu.className = 'dmi-menu';
const [cols, rows] = config.menuLayout.split('x').map(Number);
menu.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
menu.style.gridTemplateRows = 'repeat(' + rows + ', 1fr)';
const dimensions = calculateDynamicDimensions();
// CORREGIDO: Solo mostrar los items correspondientes al layout (igual que preview)
const maxItems = getMaxItemsForLayout();
for (let i = 0; i < maxItems && i < config.menuItems.length; i++) {
const item = config.menuItems[i];
const menuItem = document.createElement('a');
menuItem.className = 'dmi-item';
menuItem.href = item.url || '#';
if (item.iconPosition === 'left') {
menuItem.classList.add('icon-left');
} else if (item.iconPosition === 'right') {
menuItem.classList.add('icon-right');
} else if (item.iconPosition === 'bottom') {
menuItem.classList.add('icon-bottom');
}
if (item.icon === 'custom-svg' && item.customSvg) {
const svgWrapper = document.createElement('div');
svgWrapper.style.width = item.iconSize + 'px';
svgWrapper.style.height = item.iconSize + 'px';
if (item.iconPosition === 'left' || item.iconPosition === 'right') {
svgWrapper.style.marginLeft = item.iconPosition === 'right' ? item.iconSpacing + 'px' : '0';
svgWrapper.style.marginRight = item.iconPosition === 'left' ? item.iconSpacing + 'px' : '0';
} else {
svgWrapper.style.marginBottom = item.iconPosition === 'top' ? item.iconSpacing + 'px' : '0';
svgWrapper.style.marginTop = item.iconPosition === 'bottom' ? item.iconSpacing + 'px' : '0';
}
svgWrapper.innerHTML = item.customSvg;
const svg = svgWrapper.querySelector('svg');
if (svg) {
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.fill = config.textColor;
svg.style.stroke = config.textColor;
}
menuItem.appendChild(svgWrapper);
} else {
const iconSpan = document.createElement('span');
iconSpan.setAttribute('data-lucide', item.icon === 'custom-svg' ? 'code' : item.icon);
// SOLUCION PRINCIPAL: Aplicar estilos directamente al contenedor como en preview
iconSpan.style.width = item.iconSize + 'px';
iconSpan.style.height = item.iconSize + 'px';
if (item.iconPosition === 'left' || item.iconPosition === 'right') {
iconSpan.style.marginLeft = item.iconPosition === 'right' ? item.iconSpacing + 'px' : '0';
iconSpan.style.marginRight = item.iconPosition === 'left' ? item.iconSpacing + 'px' : '0';
} else {
iconSpan.style.marginBottom = item.iconPosition === 'top' ? item.iconSpacing + 'px' : '0';
iconSpan.style.marginTop = item.iconPosition === 'bottom' ? item.iconSpacing + 'px' : '0';
}
menuItem.appendChild(iconSpan);
}
const label = document.createElement('span');
label.className = 'dmi-label';
label.textContent = item.label;
menuItem.appendChild(label);
menuItem.addEventListener('click', (e) => {
if (item.url === '#') {
e.preventDefault();
}
setTimeout(() => {
island.classList.remove('expanded');
island.style.width = '120px';
island.style.height = '48px';
}, 100);
});
menu.appendChild(menuItem);
}
island.appendChild(pill);
island.appendChild(menu);
island.addEventListener('click', (e) => {
if (e.target === island || e.target === btn || e.target.closest('.dmi-pill')) {
if (island.classList.contains('expanded')) {
island.classList.remove('expanded');
island.style.width = '120px';
island.style.height = '48px';
} else {
island.classList.add('expanded');
island.style.width = dimensions.width + 'px';
island.style.height = dimensions.height + 'px';
}
}
});
if (config.closeOnClick) {
document.addEventListener('click', (e) => {
if (!island.contains(e.target) && island.classList.contains('expanded')) {
island.classList.remove('expanded');
island.style.width = '120px';
island.style.height = '48px';
}
});
}
targetContainer.appendChild(island);
// SOLUCION: Crear iconos Lucide y aplicar color correcto
if (typeof lucide !== 'undefined') {
lucide.createIcons();
// Aplicar color a los SVGs solo dentro de esta isla específica
island.querySelectorAll('[data-lucide] svg').forEach((svg) => {
svg.style.color = config.textColor;
});
} else {
const script = document.createElement('script');
script.src = 'https://unpkg.com/lucide@latest';
script.onload = () => {
lucide.createIcons();
// Aplicar color a los SVGs solo dentro de esta isla específica
island.querySelectorAll('[data-lucide] svg').forEach((svg) => {
svg.style.color = config.textColor;
});
};
document.head.appendChild(script);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initIsland);
} else {
initIsland();
}
})();`;
}
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
notification.offsetHeight;
notification.style.visibility = 'visible';
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (!notification.classList.contains('show')) {
notification.style.visibility = 'hidden';
}
}, 400);
}, 3000);
}
function saveConfiguration() {
try {
localStorage.setItem('bricksfusion-menu-island-config', JSON.stringify(islandConfig));
} catch (e) {}
}
function loadConfiguration() {
try {
const saved = localStorage.getItem('bricksfusion-menu-island-config');
if (saved) {
const savedConfig = JSON.parse(saved);
Object.assign(islandConfig, savedConfig);
document.getElementById('position-preset').value = savedConfig.position;
document.getElementById('edge-margin').value = savedConfig.edgeMargin;
document.getElementById('edge-margin-value').textContent = savedConfig.edgeMargin;
document.getElementById('button-text').value = savedConfig.buttonText;
document.getElementById('relative-to-container').checked = savedConfig.relativeToContainer;
document.getElementById('island-bg').value = savedConfig.islandBg;
document.getElementById('island-bg-hex').value = savedConfig.islandBg;
document.getElementById('island-bg-hsl').value = hexToHsl(savedConfig.islandBg);
document.getElementById('text-color').value = savedConfig.textColor;
document.getElementById('text-color-hex').value = savedConfig.textColor;
document.getElementById('text-color-hsl').value = hexToHsl(savedConfig.textColor);
document.getElementById('bg-opacity').value = savedConfig.bgOpacity;
document.getElementById('bg-opacity-value').textContent = savedConfig.bgOpacity;
document.getElementById('blur-strength').value = savedConfig.blurStrength;
document.getElementById('blur-strength-value').textContent = savedConfig.blurStrength;
document.getElementById('menu-layout').value = savedConfig.menuLayout;
document.getElementById('animation-duration').value = savedConfig.animationDuration;
document.getElementById('animation-duration-value').textContent = savedConfig.animationDuration;
document.getElementById('animation-easing').value = savedConfig.animationEasing;
document.getElementById('close-on-click').checked = savedConfig.closeOnClick;
if (savedConfig.globalIconSize) {
document.getElementById('global-icon-size').value = savedConfig.globalIconSize;
document.getElementById('global-icon-size-value').textContent = savedConfig.globalIconSize;
}
if (savedConfig.globalIconSpacing) {
document.getElementById('global-icon-spacing').value = savedConfig.globalIconSpacing;
document.getElementById('global-icon-spacing-value').textContent = savedConfig.globalIconSpacing;
}
if (savedConfig.globalIconPosition) {
document.querySelectorAll('.position-btn').forEach(b => {
if (b.id && b.id.startsWith('global-pos-')) {
b.classList.remove('active');
}
});
const posBtn = document.getElementById(`global-pos-${savedConfig.globalIconPosition}`);
if (posBtn) posBtn.classList.add('active');
}
document.querySelector('#island-bg').closest('.color-picker-container').style.setProperty('--selected-color', savedConfig.islandBg);
document.querySelector('#text-color').closest('.color-picker-container').style.setProperty('--selected-color', savedConfig.textColor);
updateMenuItemsUI();
updatePreview();
}
} catch (e) {}
}
initializeUI();
});
</script>
</body>
</html>
Dynamic Island
Creates an iOS-inspired Dynamic Island menu that expands from a small pill into a grid of options. Features smooth animations, backdrop blur, and flexible layouts. Perfect for navigation, quick actions, or compact menus.
Dynamic Island Menu
Click the menu button at the top to see the island expand.
Items
Menu items displayed in the expanded island. Each item needs an icon (HTML/FontAwesome), label text, optional URL, icon size (pixels), icon spacing from label (pixels), and icon position relative to label (top/bottom/left/right).
Default: Empty array
Position
Where the island appears. Options: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right. Top-center mimics iPhone Dynamic Island.
Default: Bottom center
Distance from screen or container edge. Larger values move it further from edges.
Default: 20
When on, island stays inside its container. When off, island is fixed to viewport and scrolls with page.
Default: Off
Appearance
Background color of the island. Dark colors work best with the blur effect.
Default: Black
Transparency of island background. Lower values show more content behind with blur effect.
Default: 0.8
Color of text and icons inside the island. Ensure good contrast with background.
Default: White
Intensity of backdrop blur. Higher values create stronger glass effect. May affect performance.
Default: 10
Menu
Grid layout when expanded. First number is columns, second is rows. 2x2 shows 4 items, 3x3 shows 9 items.
Default: 2x2
Text shown in the collapsed pill state. Keep it short.
Default: Menu
Animation
Speed of expand/collapse animation. Lower is snappier, higher is smoother.
Default: 0.3
Animation curve. Default has bounce. Use "ease" for smooth, "ease-out" for deceleration, or custom cubic-bezier values.
Default: cubic-bezier(0.34, 1.56, 0.64, 1)
Automatically collapse the island when clicking anywhere outside of it.
Default: Off
Performance
This element uses CSS transitions and backdrop-filter for smooth animations and glass morphism effects. No external libraries required. The island dynamically calculates its expanded size based on icon positions and spacing. Works on all modern browsers with backdrop-filter support.